Code review (gh pr)#
pw_ghish: GitHub CLI-like interface for Gerrit code reviews and CI checks
./gh pr maps GitHub CLI pull request commands (gh pr) to Gerrit Code
Review. You can create changes, upload new patchsets, edit commit trailers,
reply to inline comment threads, and submit CLs from the command line.
For a step-by-step walkthrough of creating, iterating on, and landing a Gerrit change, see Life of a PR.
Quick reference#
# View review status for the current branch and your open CLs:
$ ./gh pr status
# Inspect the active change and all inline comment threads:
$ ./gh pr view --comments
# Push a local commit as a new Gerrit change and start presubmits:
$ ./gh pr create -r "reviewer@google.com" --cq --auto
# Upload a new patchset to the active Gerrit change:
$ ./gh pr push --cq
# Reply to an inline comment thread and mark as resolved:
$ ./gh pr comment --path pw_string/string.cc --line 42 -m "Done." --resolved
# Enable automated submission once review and CI checks pass:
$ ./gh pr merge --auto
Creating changes: pr create#
Pushes your local HEAD commit to Gerrit as a new change:
$ ./gh pr create -r "reviewer@google.com" --cq --auto
Supported flags#
-t, --title <str>and-b, --body <str>: Create a new Git commit before pushing.-B, --base <branch>: Target base branch (e.g.sandbox/experiment). Defaults to the upstream tracking branch or repository default.-r, --reviewer <email|@owners>: Add reviewers to the change (pass@ownersor--ownerto automatically select code owners for the modified files).--owner: Automatically resolve and add code owners for the modified files (shorthand for-r @owners).-c, --cc <email>: CC users on the change.--auto(alias--auto-submit): Vote the host’s auto-submit label upon upload.--trigger [1|2](alias--cq [1|2]): Trigger a presubmit / Commit-Queue dry run (default1when omitted; specify2to submit). See Disambiguating --cq, --auto, and pr merge for how--trigger/--cq,--auto, andpr mergeinteract.-d, --draft: Push as a work-in-progress (WIP) draft.--publish: Publish pending draft comments on upload.--stack: Allow pushing multiple commits as a stack of Gerrit changes.--force: Force creation even if a change with thisChange-Idalready exists on Gerrit. (There is no-fshorthand becauseghuses-ffor--fill.)-o, --push-option <opt>: Pass Gerrit push options (e.g.-o topic=my-feature).--no-verify: Bypass local pre-push Git hooks.
Existing change guard#
If a change with the same Change-Id already exists on Gerrit, pr create
stops with an error showing the existing change URL and directs you to use
pr push instead. This prevents confusion between creating a new change and
uploading a new patchset to an existing one. Pass --force to bypass this
check.
Multi-commit stack guard#
In Gerrit, every unpushed commit in your branch history creates a separate
Change List (CL). If more than one commit would be pushed, pr create stops
and displays the commit count and target branch, requiring --stack to
proceed:
$ ./gh pr create --stack
Automated Change-Id hook installation#
Gerrit requires a Change-Id: I... footer in each commit message. If your
commit is missing a Change-Id, pr create automatically downloads the
Gerrit commit-msg hook if absent and runs git commit --amend --no-edit
before pushing.
Uploading patchsets: pr push#
Uploads local commits to Gerrit as a new patchset on an existing change.
Available as ./gh pr push, top-level ./gh push, or ./gh pr upload:
# Push a new patchset for the current branch:
$ ./gh pr push
# Push using the top-level alias:
$ ./gh push
# Push, mark ready for review, and auto-assign code owners:
$ ./gh pr push --owner --ready --auto
Supported flags#
-B, --base <branch>: Override the target merge branch recorded on Gerrit.-r, --reviewer <email|@owners>: Add reviewers to the change (pass@ownersor--ownerto automatically select code owners for the modified files).--owner: Automatically resolve and add code owners for the modified files (shorthand for-r @owners).-c, --cc <email>: CC users on the change.--ready: Mark the change as ready for review (removes WIP status).-d, --draft: Mark the change as a work-in-progress (WIP) draft.--auto(alias--auto-submit): Vote the host’s auto-submit label.--trigger [1|2](alias--cq [1|2]): Trigger a presubmit / Commit-Queue dry run (default1when omitted; specify2to submit). If the local commit matches the latest patchset on Gerrit, votes are applied via the Gerrit REST API without re-pushing.--publish: Publish pending draft comments on upload.--stack: Allow pushing multiple commits as a stack of Gerrit changes.--force: Force upload even if no change with thisChange-Idexists on Gerrit.-o, --push-option <opt>: Pass Gerrit push options (e.g.-o topic=my-feature).--no-verify: Bypass local pre-push Git hooks.
Target branch discovery and stack guard#
When updating an existing change, pr push queries Gerrit by the commit’s
Change-Id to determine the target branch recorded on the server (for
example, sandbox/my-experiment) and pushes to
refs/for/<recorded-branch>. Use -B, --base <branch> to override the
target branch explicitly. If no change matching HEAD’s Change-Id exists
on Gerrit (for example, if git commit --amend -m replaced the original
Change-Id), pr push inspects HEAD@{1} in the local Git reflog,
reports the exact clobbered Change-Id and CL number if HEAD@{1} matched
an existing change on Gerrit, and directs you to restore the original
Change-Id or run pr create (pass --force to bypass).
Like pr create, pr push requires --stack when uploading multiple
unpushed commits. With --stack, both commands verify every commit in the
stack for missing, multiple, or duplicate Change-Id trailers, unsquashed
fixup!/squash! commits, and stack Change-Id continuity.
Editing metadata: pr edit#
Updates reviewers, assignees, topic, hashtags, and votes of an existing change through the Gerrit REST API without pushing a new patchset:
# Add a reviewer and trigger a presubmit / CQ dry run:
$ ./gh pr edit 413992 --add-reviewer colleague@google.com --trigger
# Set a Gerrit topic and add hashtags:
$ ./gh pr edit 413992 --topic my-feature --add-hashtag triage
# Take yourself out of the attention set and put a reviewer in it:
$ ./gh pr edit 413992 --remove-attention me --add-attention colleague@google.com
Supported flags#
--add-reviewer <email|@owners>/--remove-reviewer <email>: Add or remove reviewers (pass@ownersor--add-ownerto automatically select code owners for the modified files).--add-owner: Automatically resolve and add code owners for the modified files (shorthand for--add-reviewer @owners).--add-assignee <email>/--remove-assignee <email>: Add or remove assignees.--remove-assigneeremoves the user as a reviewer.--add-attention <email|me>/--remove-attention <email|me>: Add a user to, or remove them from, the change’s attention set without changing their reviewer or CC status.me(or@me) means yourself. These use Gerrit’s attention set endpoints, so no change message is posted.--add-label <Label=Value>: Apply a Gerrit label vote (e.g.--add-label Commit-Queue=1).--trigger [0|1|2](alias--cq [0|1|2]): Vote onCommit-QueueorPresubmit-Ready(default1;0removes vote).--topic <str>/--remove-topic: Set or remove the Gerrit topic.--add-hashtag <str>/--remove-hashtag <str>: Add or remove Gerrit hashtags.
Editing commit messages and bug trailers#
Unlike GitHub (where pull request titles and descriptions live in the server
database independently of Git commits), Gerrit stores the CL description inside
the Git commit message of each patchset. Editing the commit message remotely on
Gerrit without updating your local Git commit causes the next ./gh pr push
or git rebase to overwrite the remote edit.
For this reason, commit-message flags on pr edit (--title, --body,
--message, --bug, and --fixed) are currently disabled
(b/567763970) and print
instructions for editing the local Git commit message surgically:
# 1. Dump the current commit message to a temporary file:
$ git log -1 --format=%B HEAD > "$(git rev-parse --git-dir)/COMMIT_EDITMSG_TMP"
# 2. Edit "$(git rev-parse --git-dir)/COMMIT_EDITMSG_TMP" surgically (keeping the existing Change-Id: line intact),
# then apply the updated message and upload a new patchset:
$ git commit --amend --only -F "$(git rev-parse --git-dir)/COMMIT_EDITMSG_TMP"
$ ./gh pr push
For an earlier commit in a multi-CL stack, use git rebase -i while
preserving every commit’s Change-Id: footer, then run
./gh pr push --stack.
GitHub #123 closing keyword guard#
Gerrit ignores GitHub prose keywords like Fixes #456. If pr create or
pr push detects close/fix/resolve #<number> in a commit
message, it stops and directs you to use Bug: b/<id> or Fixed: b/<id>
trailers instead.
Inspecting changes and status#
Review dashboard: pr status#
Displays a summary of:
Current branch: Shows the active change number, title, target branch, patchset number, submittability, label votes (e.g.
Code-Review,Presubmit-Verified,Lint), tryjob check status, and a summary of unresolved comment threads and unpublished drafts (previewing up to two unresolved threads and standalone[DRAFT]comments inline with[PS<N>]tags and message snippets).Created by you: Lists open changes you authored (scoped to the last 30 days by default).
Requesting a code review from you: Lists changes awaiting your review (scoped to the last 30 days by default).
# Default view (last 30 days + active branch):
$ ./gh pr status
# Include open changes older than 30 days:
$ ./gh pr status --all
Listing open changes: pr list#
Lists open changes for the current repository (automatically scoped to the
current Git remote’s Gerrit project; pass --all-projects to query across all
projects on the Gerrit host):
$ ./gh pr list
$ ./gh pr list --all-projects
$ ./gh pr list --limit 10 --state open
$ ./gh pr list --author "hepler@google.com" --base main
$ ./gh pr list --json number,title,state,branch
Inspecting a change: pr view#
Displays change metadata, including owner, reviewers, attention set status,
and current review scores. When called without arguments, pr view inspects
the active change for your current branch:
# View active change on current branch:
$ ./gh pr view
# Include inline comment threads and unpublished drafts grouped by file and line:
$ ./gh pr view 413992 --comments
# Output JSON (strictly validated against supported fields, including comments and drafts):
$ ./gh pr view 413992 --json number,title,state,author,files,bug,bugs,comments,drafts
pr view --comments renders each thread with its [PS<N>] patchset tag,
[DRAFT] indicator, and [resolved] / [unresolved] status (or
Comments: None when no comments or drafts exist).
pr view --json bug,bugs parses commit message trailers on the patchset:
bug returns a comma-separated string, while bugs returns objects with
{"id": "b/123456", "closes": true} (true for Fixed:, false for
Bug:).
Viewing patch diffs: pr diff#
Displays the unified diff of the latest patchset, or a specific revision:
$ ./gh pr diff
$ ./gh pr diff 413992
$ ./gh pr diff 413992/2
Local checkout and cherry-pick#
Fetch a remote patchset into your local checkout:
# Fetch and checkout change at FETCH_HEAD:
$ ./gh pr checkout 413992
# Checkout a specific patchset:
$ ./gh pr checkout 413992/3
# Cherry-pick a change onto the current branch:
$ ./gh pr cherry-pick 413992
Reviewing, commenting, and landing#
Reviewing changes: pr review#
Submits review scores, Commit-Queue votes, or staged draft comments on a Gerrit CL:
# Trigger Commit-Queue dry run on the active change (Commit-Queue+1):
$ ./gh pr review --cq
# Approve active change (Code-Review+2) and trigger CQ dry run:
$ ./gh pr review --approve --cq
# Approve a specific change with a comment:
$ ./gh pr review 413992 --approve -m "Looks good."
# Vote Code-Review-1 with a comment:
$ ./gh pr review 413992 --request-changes -m "Please address formatting."
# Publish all staged draft comments on the change without uploading a patchset:
$ ./gh pr review 413992 --publish
Posting inline comments: pr comment#
Posts change-level, file-level, or inline review comments (via -m, --message
or -b, --body). When --path (and optionally --line) is provided,
pw_ghish checks for an existing thread at that target across patchsets and
appends your comment as a reply while keeping any unrelated drafts private
(Drafts: KEEP):
# Reply to an inline thread and mark as resolved:
$ ./gh pr comment 413992 --path pw_string/string.cc --line 42 \
-m "Fixed, using pw::Status." --resolved
# Save or update an inline comment as a private draft without publishing:
$ ./gh pr comment 413992 --path pw_string/string.cc --line 42 \
-m "Consider std::string_view" --draft
# Delete an unpublished draft comment on a file and line:
$ ./gh pr comment 413992 --path pw_string/string.cc --line 42 --delete-draft
# Explicitly target or disambiguate a specific patchset:
$ ./gh pr comment 413992/1 --path pw_string/string.cc --line 42 -m "Ack."
When --draft is used on a thread or line that already has an unpublished
draft from you, pw_ghish updates that draft in place (preserving Side
and character Range) rather than creating a duplicate draft. If multiple
unpublished drafts exist at the same location across different patchsets,
pr comment refuses to guess and prompts you to disambiguate with
--patchset <N> or <id>/<N>. --resolved requires --path (and
--line for inline threads) so a change-level comment cannot accidentally
resolve a thread.
Lifecycle transitions: pr ready, pr close, and pr reopen#
Control the work-in-progress (WIP) and abandoned state of a change:
# Mark a WIP change ready for review:
$ ./gh pr ready 413992
# Mark ready for review and assign module code owners:
$ ./gh pr ready 413992 --owner
# Convert an active change back to WIP (draft) with a message:
$ ./gh pr ready 413992 --undo -m "Holding for upstream refactor."
# Abandon or restore a change in Gerrit:
$ ./gh pr close 413992
$ ./gh pr reopen 413992
Automatic code-owner selection: --owner and @owners#
Whereas GitHub automatically assigns CODEOWNERS when a pull request leaves
draft state, Gerrit’s code-owners plugin does not automatically add
reviewers when a CL is uploaded or marked ready. Pass --owner on
pr ready, pr create, or pr push, --add-owner on pr edit,
or @owners to -r, --reviewer / --add-reviewer to resolve and add
the appropriate module owners for the CL’s touched files:
$ ./gh pr ready --owner
$ ./gh pr push --ready --owner
$ ./gh pr edit --add-owner
--owner / @owners queries Gerrit’s code-owners API and local
OWNERS files to select the minimal set of non-author module owners covering
the modified directories, falling back to the project’s review rotation (such
as gwsq-pigweed) when no module-specific owner applies.
Checking CI status: pr checks#
./gh pr checks queries LUCI Buildbucket tryjobs as well as Gerrit
SubmitRequirements and automated verification labels (such as
Android-Build-Verified, Presubmit-Verified, Lint-Verified, and
Kokoro) for the active change. It returns exit code 0 when all blocking
checks and automated Gerrit gates pass, 8 while running or pending, and
1 on failure:
$ ./gh pr checks
$ ./gh pr checks --watch --fail-fast
$ ./gh pr checks --all
For full documentation on pr checks and gh run, see
CI & tryjobs (gh run).
Submitting changes: pr merge#
Submits a change to the target branch:
# Enable automated submission once presubmits and reviews pass (recommended):
$ ./gh pr merge 413992 --auto
# Trigger Commit-Queue+2 submission directly:
$ ./gh pr merge 413992 --cq
# Attempt immediate submit (requires all checks and approvals to already be satisfied):
$ ./gh pr merge 413992
If you run ./gh pr merge without flags while submit requirements are still
pending, Gerrit returns HTTP 409 Conflict and pw_ghish prints the command to
enable --auto or --cq.
Disambiguating --cq, --auto, and pr merge#
In GitHub, pushing a branch automatically triggers CI and gh pr merge merges
the branch directly. In Gerrit and LUCI, code uploads, presubmit dry runs
(Commit-Queue+1 or Presubmit-Ready+1), auto-submit
(Pigweed-Auto-Submit+1 or Auto-Submit+1), and final submission
(Commit-Queue+2) are controlled by Gerrit label votes.
pw_ghish dynamically inspects the Gerrit change’s available labels when
applying --cq, --auto, and pr review --approve:
Presubmit / CQ label: Automatically votes
Commit-QueueorPresubmit-Readydepending on which label the Gerrit project defines, and clamps--cqto the label’s maximum supported value.Approval score:
pr review --approveinspects theCode-Reviewlabel’s allowedValuesmap on the change and votes the maximum allowed positive score (+2on standard Gerrit hosts, or+1on repositories configured with a-1..+1scale).Topics-Not-Supported pre-flight guard: If a project sets a
Topics-Not-SupportedGerrit submit requirement orforbid_topics = truein.ghish.toml,pr create,pr push, andpr editreject--topicand-o topic=...before pushing.
You can apply these votes either when uploading a patchset (via flags on
pr create or pr push) or on an existing patchset without pushing
code (via pr review or pr merge):
Action |
Gerrit Vote / API |
During Upload |
Without Upload |
|---|---|---|---|
Run CI tryjobs (dry run) |
|
|
|
Arm auto-submit |
|
|
|
Submit via Commit Queue |
|
|
|
Direct REST submit |
Gerrit |
(n/a) |
|
Tip
Note the difference in --cq defaults: pr push --cq and
pr review --cq default to +1 (a presubmit dry run),
whereas pr merge --cq votes +2 to land the change.
Targeting changes#
Every ./gh pr subcommand that accepts an optional [<id>] target (such
as view, diff, checkout, checks, review, comment,
merge, close, reopen, ready, and edit) supports the
following target formats:
Omitted argument: Resolves the active change from your current Git branch or
HEADcommitChange-Id.Change number: A numeric Gerrit change ID (e.g.
472267) or change ID with patchset suffix (e.g.472267/3).Gerrit URL: Full web or REST URLs (e.g.
https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/472267orhttps://pigweed-review.googlesource.com/c/pigweed/pigweed/+/472267/3). The Gerrit host in the URL is used automatically unless--hostis specified.Shortlink: Built-in shortlinks such as
pwrev/472267,pwrev.dev/i/472267,fxrev/472267,fxr/472267,fxrev.dev/i/472267,ag/472267,aosp/472267,crrev.com/c/472267, orcrrev.com/i/472267(includinggo/<shortlink>andgoto.google.com/<shortlink>forms), plus any custom shortlink prefixes configured in.ghish.toml([gerrit.shortlinks]) orgit config ghish.gerrit.shortlink.<prefix>.Branch name: Local branch names (e.g.
my-feature,cl/472267,change-472267), resolved by inspecting the branch tip commit’sChange-Idorbranch.<name>.gerrit-change-idin Git config.
Comparison with GitHub CLI (gh pr)#
Command / Flag |
Gerrit Behavior in |
Difference from Upstream |
|---|---|---|
|
Pushes |
Stops if |
|
Uploads a new patchset to an existing Gerrit CL. |
Gerrit adaptation (replaces |
|
Queries Gerrit REST API for change metadata, votes, and |
Supports |
|
Updates reviewers, assignees, topic, hashtags, and votes via REST. |
Adds |
|
Filters changes by Gerrit |
Gerrit 3.8+ removed assignees; |
|
Filters by Gerrit vote predicate (e.g. |
Queries Gerrit label scores rather than GitHub issue labels. |
|
Votes |
In Gerrit, |
|
Posts change-level or inline threaded comments ( |
Adds |
|
Marks a WIP change ready for review, or WIP with |
Adds |
|
Submits the CL directly, via |
Does not support |
Several pr flags intentionally omit single-letter shorthands because
upstream gh binds those letters to other meanings: --auto (-a is
--assignee), --publish (-p is --project), --force on
checkout (-f is --fill), --cq (-q is --jq), and
--message on edit and merge (-m is --milestone and
--merge). See Compat policy for the cross-CLI
flag compatibility policy.