docs reference Repos & auth
18-repo-lifecycle.md ReferenceJul 17, 2026

Repos & auth

These commands cover the edges of GitStack: getting signed in, and getting a repository to exist on the server in the first place. Everything else in the CLI assumes you have a session token and a repo to work in. Auth commands sit at the top level (gs login, gs logout, gs whoami); repository lifecycle lives under gs repo, with gs clone and gs init as the two shortcuts you reach for most.

Every command below also accepts the global --plain, --json, --no-color, and --verbose flags. See CLI conventions for those.

gs login#

Sign in to GitStack through a browser handshake and store a session token locally.

bash
gs login [--server <url>] [--device-name <label>]
Flag Description
--server <url> GitStack endpoint. Defaults to https://gitstack.sh, the hosted service, and that is the value you want. The flag exists for testing gs against a development build.
--device-name <label> Label for this device in your account. Defaults to the machine hostname.
Flag
--server <url>
Description
GitStack endpoint. Defaults to https://gitstack.sh, the hosted service, and that is the value you want. The flag exists for testing gs against a development build.
Flag
--device-name <label>
Description
Label for this device in your account. Defaults to the machine hostname.

gs login prints a URL, opens it in your browser, and waits for you to approve the device there. Once the server confirms, gs writes the token to your config file and installs a git credential helper so plain git push can authenticate too. Token and config paths live in Configuration & files.

bash
gs login --device-name work-laptop
gs whoami
Headless machines

If gs cannot open a browser (no desktop session over SSH), it prints the URL and keeps polling. Open the URL on any device. The handshake waits up to ten minutes before it gives up.

gs logout#

Revoke every device token on your account and clear local config.

bash
gs logout

This command takes no flags. It signs out every device you have registered, not just the current machine, then deletes the local token and config file. It is safe to run when you are already signed out.

Logout is account-wide

gs logout revokes all of your device tokens, so every other machine you use gets signed out too. To revoke a single device, use the devices page in the web UI instead.

gs whoami#

Print who you are signed in as, which server, and your registered devices.

bash
gs whoami

This command takes no flags. It reads your local config, asks the server who the token belongs to, and lists your devices. When no config is present it prints Not signed in. and exits cleanly.

gs clone#

Clone a repository from the GitStack server, or from a raw git URL.

bash
gs clone [--partial] [--sparse <folder>]... <ns>/<name> | <url> [dir]

A namespace/name argument is expanded to the server's clone URL. A bare name resolves against your own namespace. A full http(s)://, git@, ssh://, or file:// URL is passed straight through to git clone. The optional dir sets the checkout directory.

By default the clone is a full one, with every version of every file.

Flag Effect
--partial Download every commit, but only the file contents the checkout needs. An older version of a file downloads from the server the first time a command needs it, for example git blame, git log -p, or checking out an old commit. Builds, tests and editors work as in any clone. Those history commands need a connection to the server.
--sparse <folder> Check out only this folder. The clone is partial, so files in other folders are not downloaded. Repeat it for more folders, or add one later with git sparse-checkout add <folder>.
Flag
--partial
Effect
Download every commit, but only the file contents the checkout needs. An older version of a file downloads from the server the first time a command needs it, for example git blame, git log -p, or checking out an old commit. Builds, tests and editors work as in any clone. Those history commands need a connection to the server.
Flag
--sparse <folder>
Effect
Check out only this folder. The clone is partial, so files in other folders are not downloaded. Repeat it for more folders, or add one later with git sparse-checkout add <folder>.
bash
gs clone acme/payments
gs clone acme/payments payments-fork
gs clone --partial acme/payments
gs clone --sparse services/api acme/payments
gs clone https://gitstack.sh/acme/payments.git
The name form needs a session

gs clone acme/payments reads your config to find the server and namespace, so you must be logged in. The raw-URL form does not, since it hands the URL directly to git.

gs init#

Turn an existing directory into a new GitStack repository, creating it on the server.

bash
gs init [dir] [--repo <name>] [--branch <name>] [--github] [--github-owner <owner>] [--visibility <private|public>]
Flag Description
--repo <name> Repo name on the server. Accepts ns/name or bare name. Defaults to the directory basename.
--branch <name> Branch to check out after init. Defaults to main.
--github Also create and mirror a GitHub repository.
--github-owner <owner> GitHub user or org for --github. Defaults to your GitHub login.
--visibility <private|public> Visibility of the GitHub repo. Defaults to private.
Flag
--repo <name>
Description
Repo name on the server. Accepts ns/name or bare name. Defaults to the directory basename.
Flag
--branch <name>
Description
Branch to check out after init. Defaults to main.
Flag
--github
Description
Also create and mirror a GitHub repository.
Flag
--github-owner <owner>
Description
GitHub user or org for --github. Defaults to your GitHub login.
Flag
--visibility <private|public>
Description
Visibility of the GitHub repo. Defaults to private.

gs init prepares the working directory, creates the repo on the server, points origin at the server clone URL, and checks out the branch. Run it with no argument to use the current directory. With --github the server also creates the linked GitHub repo and wires up the mirror, same as gs repo create --github.

bash
cd payments
gs init --repo payments
gs init --repo payments --github --visibility public
init creates, clone connects

gs init registers a brand new repo on the server, so it fails if that name already exists. To connect to a repo that is already there, use gs clone.

gs repo create#

Create an empty repository on the server, optionally mirrored to GitHub.

bash
gs repo create <name> [--github] [--github-owner <owner>] [--visibility <private|public>] [--seed] [--clone]
Flag Description
--github Also create and mirror a GitHub repository.
--github-owner <owner> GitHub user or org for --github. Defaults to your GitHub login.
--visibility <private|public> Visibility of the GitHub repo. Defaults to private.
--seed Create the legacy empty initial commit on main.
--clone Clone the new repository into ./<name> after creating it.
Flag
--github
Description
Also create and mirror a GitHub repository.
Flag
--github-owner <owner>
Description
GitHub user or org for --github. Defaults to your GitHub login.
Flag
--visibility <private|public>
Description
Visibility of the GitHub repo. Defaults to private.
Flag
--seed
Description
Create the legacy empty initial commit on main.
Flag
--clone
Description
Clone the new repository into ./<name> after creating it.

The name argument accepts namespace/name or a bare name in your own namespace. On success the command prints the clone URL. With --github the server also creates the linked GitHub repo and wires up the mirror. With --clone the command then clones the new repo into a directory named after it, the same as running gs clone yourself; it works with or without --github.

bash
gs repo create acme/payments
gs repo create acme/payments --github --visibility public
gs repo create acme/payments --clone
--seed is GitStack-only

You cannot combine --seed with --github. The seed commit is a GitStack-hosted artifact, so pairing the two is rejected.

gs repo delete#

Delete a repository you own. This is destructive and cannot be undone.

bash
gs repo delete <ns>/<name> | <name> [--force]
Flag Description
--force, -f Skip the confirmation prompt.
Flag
--force, -f
Description
Skip the confirmation prompt.

A bare name resolves against your own namespace. In an interactive terminal the command asks Delete <repo> and all of its reviews? This cannot be undone. and waits for y. Deleting a repo removes its registry entry and bare repo on the server; it does not touch a linked GitHub mirror.

bash
gs repo delete acme/payments
gs repo delete acme/payments --force
No prompt means no delete

In a non-interactive session (a pipe or CI job) without --force, delete refuses rather than block on a prompt nobody can answer. Pass --force when you script it. Any linked GitHub mirror is left in place.

gs repo visibility#

Make a repository public or private.

bash
gs repo visibility <public|private> [<ns>/<name>] [--force]
Flag Description
--force, -f Skip the confirmation prompt.
Flag
--force, -f
Description
Skip the confirmation prompt.

Omit the repo to target the one in the current directory. Only the repo's owners can change it. In an interactive terminal the command asks Make <repo> public? (or private) and waits for y. A repo that already has that visibility is left as it is, with no prompt. The change applies to GitStack alone; a linked GitHub repository keeps its own setting.

bash
gs repo visibility public acme/payments
gs repo visibility private --force

Making a repo private counts its collaborators toward your plan's member limit, and the command refuses when that would go over it. In a non-interactive session without --force, it refuses rather than wait on a prompt.

gs repo import github#

Import a GitHub repository into GitStack. Before running it, install the GitStack GitHub App on the source repository (see the callout below).

bash
gs repo import github <owner>/<repo>

This command takes no flags of its own. The server pulls the GitHub repo, keeps it linked as a mirror, and prints the clone URL of the new GitStack repo. From then on GitStack is the system of record and the GitHub copy stays connected, so issues, comments, and checks flow both ways.

bash
gs repo import github acme/payments
Needs the GitStack GitHub App

The import requires the GitStack GitHub App installed on the source repository. Without it the server rejects the import and prints the install link. To install it up front, open gitstack.sh/github/install/start while signed in, and grant the app access to the repository you want to import.

gs repo restore#

Re-anchor a freshly restored server's base branch from a healthy local clone. This is a disaster-recovery path, not a routine command.

bash
gs repo restore [--repo <name>] [--branch <name>] [--base <ref>] [--remote <name>]
Flag Description
--repo <name> Repo name. Defaults to inferring it from the origin remote.
--branch <name> Stack branch to restore. Defaults to the current branch.
--base <ref> Local base branch or ref. Defaults to main.
--remote <name> Git remote to push through. Defaults to origin.
Flag
--repo <name>
Description
Repo name. Defaults to inferring it from the origin remote.
Flag
--branch <name>
Description
Stack branch to restore. Defaults to the current branch.
Flag
--base <ref>
Description
Local base branch or ref. Defaults to main.
Flag
--remote <name>
Description
Git remote to push through. Defaults to origin.

When a server is rebuilt from backup and its base branch is behind, run gs repo restore from a clone that still has the good history. It re-anchors main on the server to the local base, then republishes the current branch and its manifest.

bash
gs repo restore --repo acme/payments
Needs a local manifest

Restore reads the branch's stack manifest and refuses when it finds fewer than one review. Run it from inside a healthy clone that still has the stack.

gs repo resync-github#

Push GitStack's branches to the GitHub mirror where GitHub has fallen behind.

bash
gs repo resync-github [--force <branch>]... [<ns>/<name>]

Omit the argument to target the repo in the current directory. Use it when an automatic mirror push was rejected (a missing App permission, a GitHub outage) and the GitHub copy is now stale. The server does the pushes in the background and returns right away, so the output reads started, not finished.

A resync only pushes a branch when GitHub doesn't have it, or still has the commit GitStack last pushed there. A branch someone moved on GitHub directly, such as a deploy branch rolled back with a raw git push, is left alone and listed in the output. To overwrite it with GitStack's version anyway, name it (or keep GitHub's version with gs repo pull-github):

bash
gs repo resync-github acme/payments
gs repo resync-github --force production acme/payments

If GitHub can't be read at that moment, the resync pushes nothing and asks you to try again. GitHub-managed branches ([mirror] github_managed in .gitstack/policy.toml) are never pushed, and --force refuses them.

It runs in the background

Because the resync is asynchronous, check per-branch results with gs repo mirror-status rather than waiting on this command.

gs repo mirror-status#

Show each branch's GitHub mirror state, checked against the branches GitHub has right now.

bash
gs repo mirror-status [<ns>/<name>]

This command takes no flags of its own. Omit the argument to target the repo in the current directory. Each branch shows one of:

State Meaning
ok GitHub and GitStack have the same commit. A branch pushed to GitHub some other way is noted as updated outside GitStack.
missing GitHub doesn't have the branch. A resync pushes it.
stale GitHub still has the commit GitStack last pushed, and GitStack has moved on. A resync pushes it.
moved Someone changed the branch on GitHub directly. gs leaves it alone; pick a side with gs repo pull-github --force (keep GitHub's) or gs repo resync-github --force (keep GitStack's).
managed The branch is GitHub-managed ([mirror] github_managed in .gitstack/policy.toml): GitStack follows GitHub and never pushes it.
failing The last mirror push failed, with the error and the last commit confirmed on GitHub. It stays failing until the branch's next successful push, or until a pull from GitHub finds the branch in sync.
State
ok
Meaning
GitHub and GitStack have the same commit. A branch pushed to GitHub some other way is noted as updated outside GitStack.
State
missing
Meaning
GitHub doesn't have the branch. A resync pushes it.
State
stale
Meaning
GitHub still has the commit GitStack last pushed, and GitStack has moved on. A resync pushes it.
State
moved
Meaning
Someone changed the branch on GitHub directly. gs leaves it alone; pick a side with gs repo pull-github --force (keep GitHub's) or gs repo resync-github --force (keep GitStack's).
State
managed
Meaning
The branch is GitHub-managed ([mirror] github_managed in .gitstack/policy.toml): GitStack follows GitHub and never pushes it.
State
failing
Meaning
The last mirror push failed, with the error and the last commit confirmed on GitHub. It stays failing until the branch's next successful push, or until a pull from GitHub finds the branch in sync.

The output ends with the command to run next, if any.

bash
gs repo mirror-status acme/payments

gs repo pull-github#

Bring GitHub's branches into GitStack.

bash
gs repo pull-github [--force <branch>]... [--accept-owner-files] [--dry-run] [<ns>/<name>]

Omit the argument to target the repo in the current directory. It never pushes to GitHub. Each branch is reported as up to date, fast-forwarded to GitHub's commit, created (it existed only on GitHub), or skipped: when GitHub and GitStack differ, or GitStack is ahead, the branch is left as it is. Name a branch with --force to set GitStack's copy to GitHub's version, for example after a rollback done on GitHub:

bash
gs repo pull-github --force production acme/payments

--force never overwrites the default branch (land a revert there instead) or a branch with open reviews (land or abandon them first); those are reported as skipped. --dry-run shows what would change without changing anything. Only repository owners can pull. GitStack also pulls every branch GitHub has moved ahead on its own, every 15 minutes.

The default branch is held when GitHub's commits change .gitstack/policy.toml or checks.toml, since those change only with a repo owner's approval. After checking the change on GitHub, a repo owner takes it with --accept-owner-files:

bash
gs repo pull-github --accept-owner-files acme/payments

gs repo prune#

Delete branches already merged into the default branch, on both GitStack and its GitHub mirror. Prune is part of the cleanup flow and is documented in Landing & cleanup.

bash
gs repo prune [<ns>/<name>]

Read a repo without a clone#

These commands read a repository over the API, from any directory, with no clone. Each takes the repo as its first argument (a bare name resolves against your own namespace). A rev is a branch, tag or commit; when a branch and a tag share a name, the branch wins. All of them accept --json.

bash
gs repo cat acme/payments main:src/auth/login.go
gs repo ls acme/payments main:src/auth
gs repo search acme/payments "func Login" --path "src/**"
gs repo symbols acme/payments Login
gs repo refs acme/payments Login --from src/auth/login.go --line 42
gs repo log acme/payments main -- src/auth/login.go
gs repo diff acme/payments main...fix-login --stat
Command Shows
gs repo cat <repo> <rev>:<path> A file's contents
gs repo ls <repo> <rev>[:<path>] A folder's entries, folders ending in /
gs repo search <repo> <query> Matching lines as path:line:text. --regex takes an RE2 expression, --case and --word narrow the match, --path limits it to files matching a glob, and --rev picks what to search
gs repo symbols <repo> <query> Functions, types, constants and variables whose name matches
gs repo refs <repo> <name> Where a name is defined and used. --from and --line say where you saw it, so the right definition comes first
gs repo log <repo> [<rev> [-- <path>]] Commits, newest first, optionally only those touching a path. -n sets how many (50 by default, at most 500)
gs repo diff <repo> <base>...<head> [-- <path>] What head changes since it split from base. <base>..<head> compares the two tips directly. --stat lists files with line counts and --name-only lists paths
Command
gs repo cat <repo> <rev>:<path>
Shows
A file's contents
Command
gs repo ls <repo> <rev>[:<path>]
Shows
A folder's entries, folders ending in /
Command
gs repo search <repo> <query>
Shows
Matching lines as path:line:text. --regex takes an RE2 expression, --case and --word narrow the match, --path limits it to files matching a glob, and --rev picks what to search
Command
gs repo symbols <repo> <query>
Shows
Functions, types, constants and variables whose name matches
Command
gs repo refs <repo> <name>
Shows
Where a name is defined and used. --from and --line say where you saw it, so the right definition comes first
Command
gs repo log <repo> [<rev> [-- <path>]]
Shows
Commits, newest first, optionally only those touching a path. -n sets how many (50 by default, at most 500)
Command
gs repo diff <repo> <base>...<head> [-- <path>]
Shows
What head changes since it split from base. <base>..<head> compares the two tips directly. --stat lists files with line counts and --name-only lists paths

A large diff is cut at 100 files, 2 MiB in total, or 512 KiB for one file. gs repo diff says when it cut one and prints the command that fetches that file's diff on its own.

Edit a repo without a clone#

gs repo get and gs repo commit make small edits from any directory, with no clone. get downloads the files you name (a folder brings everything in it) into a folder and records the commit they came from. Edit them with any tool, then commit sends back only the files that changed, and the server builds the commit on that recorded commit.

bash
gs repo get acme/payments main src/auth --into ./work
# edit ./work/src/auth/login.go
gs repo commit acme/payments --from ./work --branch fix-login \
  -m "auth: reject empty passwords" --review -t "auth: reject empty passwords"
# add ./work/src/auth/empty_test.go
gs repo commit acme/payments --from ./work -m "auth: test the empty case" \
  --add src/auth/empty_test.go
Command Does
gs repo get <repo> <rev> <path>... --into <dir> Writes each file at its repo path under <dir> and records the repo and commit in <dir>/.gs-repo.json. A file you already edited there is never overwritten. Symlinks and submodules are skipped
gs repo commit <repo> --from <dir> --branch <branch> -m <message> Commits the downloaded files that differ from the recorded commit. --add <path> (repeatable) commits a new file you created in the folder; anything else in the folder is listed and never sent. --delete <path> (repeatable) deletes a file. --review -t <title> opens a review with the commit. The folder then records the new commit, so the next commit builds on it and goes to the same branch
Command
gs repo get <repo> <rev> <path>... --into <dir>
Does
Writes each file at its repo path under <dir> and records the repo and commit in <dir>/.gs-repo.json. A file you already edited there is never overwritten. Symlinks and submodules are skipped
Command
gs repo commit <repo> --from <dir> --branch <branch> -m <message>
Does
Commits the downloaded files that differ from the recorded commit. --add <path> (repeatable) commits a new file you created in the folder; anything else in the folder is listed and never sent. --delete <path> (repeatable) deletes a file. --review -t <title> opens a review with the commit. The folder then records the new commit, so the next commit builds on it and goes to the same branch

The commit lands on the branch the way a push of it would:

  • A new branch is created.
  • On a branch with a stack, the commit adds a patchset to the top review, or opens a review on top with --review.
  • An existing branch that moved since the get refuses the commit (branch_moved), so you never overwrite work you did not see. Get the files again into a new folder and redo the edit.
  • A review below the top of a stack is edited from a clone.

Builds and tests run in CI (gs review ci). To build and test locally, clone instead: gs clone --partial downloads file contents only as they are needed.