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.
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. |
--server <url>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>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.
gs login --device-name work-laptop
gs whoami
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.
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.
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.
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.
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>. |
--partialgit 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>git sparse-checkout add <folder>.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
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.
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. |
--repo <name>ns/name or bare name. Defaults to the directory basename.--branch <name>main.--github--github-owner <owner>--github. Defaults to your GitHub login.--visibility <private|public>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.
cd payments
gs init --repo payments
gs init --repo payments --github --visibility public
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.
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. |
--github--github-owner <owner>--github. Defaults to your GitHub login.--visibility <private|public>private.--seedmain.--clone./<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.
gs repo create acme/payments
gs repo create acme/payments --github --visibility public
gs repo create acme/payments --clone
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.
gs repo delete <ns>/<name> | <name> [--force]
| Flag | Description |
|---|---|
--force, -f |
Skip the confirmation prompt. |
--force, -fA 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.
gs repo delete acme/payments
gs repo delete acme/payments --force
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.
gs repo visibility <public|private> [<ns>/<name>] [--force]
| Flag | Description |
|---|---|
--force, -f |
Skip the confirmation prompt. |
--force, -fOmit 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.
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).
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.
gs repo import github acme/payments
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.
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. |
--repo <name>origin remote.--branch <name>--base <ref>main.--remote <name>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.
gs repo restore --repo acme/payments
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.
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):
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.
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.
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. |
okmissingstalemovedgs repo pull-github --force (keep GitHub's) or gs repo resync-github --force (keep GitStack's).managed[mirror] github_managed in .gitstack/policy.toml): GitStack follows GitHub and never pushes it.failingfailing 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.
gs repo mirror-status acme/payments
gs repo pull-github#
Bring GitHub's branches into GitStack.
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:
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:
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.
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.
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 |
gs repo cat <repo> <rev>:<path>gs repo ls <repo> <rev>[:<path>]/gs repo search <repo> <query>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 searchgs repo symbols <repo> <query>gs repo refs <repo> <name>--from and --line say where you saw it, so the right definition comes firstgs repo log <repo> [<rev> [-- <path>]]-n sets how many (50 by default, at most 500)gs repo diff <repo> <base>...<head> [-- <path>]<base>..<head> compares the two tips directly. --stat lists files with line counts and --name-only lists pathsA 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.
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 |
gs repo get <repo> <rev> <path>... --into <dir><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 skippedgs repo commit <repo> --from <dir> --branch <branch> -m <message>--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 branchThe 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
getrefuses 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.