Add gerrit-workflow skill Teaches AI agents how to push for review, update patch sets, handle Change-Id trailers, work with dependent change stacks, and diagnose common Gerrit push errors. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> Change-Id: Ief2ee7fd6b5ff9e5e82beeede5fe6a92f4173b05
diff --git a/docs/claude-code.md b/docs/claude-code.md index 4e07859..1287cb2 100644 --- a/docs/claude-code.md +++ b/docs/claude-code.md
@@ -89,3 +89,20 @@ the user to run it. See [Interactive Setup](configuration.md#interactive-setup-gerritsetup) in the configuration guide for details. + +## `gerrit-workflow` skill + +`skills/gerrit-workflow/SKILL.md` teaches the model how to drive the git side of +Gerrit Code Review — pushing for review via `refs/for/<branch>`, creating new +patch sets by amending without disturbing the `Change-Id` trailer, working with +dependent change stacks and topics, and diagnosing common push errors such as +`missing Change-Id in commit message footer` or `prohibited by Gerrit`. + +Unlike `/gerrit:setup`, this skill is model-invocable: Claude Code activates it +automatically whenever a task involves Gerrit git operations, so you do not need +to invoke it by name. Two reference files ship alongside it: + +- `references/gerrit-vs-github.md` — a side-by-side comparison of Gerrit and + GitHub/GitLab workflows for common tasks. +- `references/change-id-details.md` — a deep dive on `Change-Id` creation, + squashing, cherry-picks, and edge cases.
diff --git a/skills/gerrit-workflow/SKILL.md b/skills/gerrit-workflow/SKILL.md new file mode 100644 index 0000000..bcfe2e5 --- /dev/null +++ b/skills/gerrit-workflow/SKILL.md
@@ -0,0 +1,247 @@ +--- +name: gerrit-workflow +description: This skill should be used when the user asks to "submit a change to Gerrit", "push for review", "upload a patch", "update a change in Gerrit", "add a patch set", "work with Gerrit", "push to refs/for", "fix a missing Change-Id", "amend a Gerrit change", or any task involving git operations in a repository hosted on Gerrit Code Review. Also use when the user encounters Gerrit push errors such as "missing Change-Id in commit message footer" or "prohibited by Gerrit". +allowed-tools: Bash, Read +--- + +# Gerrit Workflow + +Gerrit is a code review system that sits in front of a git repository. The key +difference from GitHub/GitLab: **changes go through a review queue before +landing in the branch**. This changes several git workflows in important ways. + +## Core Concepts + +### Changes and Patch Sets + +A **Change** is the unit of review in Gerrit — it corresponds to one logical +commit. Each change has: + +- A numeric **Change number** (e.g., `12345`) +- A **Change-Id** trailer in the commit message (e.g., + `Change-Id: Ic8aaa0728a43936cd4c6e1ed590e01ba8f0fbf5b`) + +When a commit is revised and re-uploaded, the new version becomes a new **patch +set** on the same change. Gerrit associates them via the `Change-Id` trailer — +this is what makes the `Change-Id` essential. + +### The Magic `refs/for/<branch>` Ref + +Gerrit intercepts pushes to `refs/for/<branch>`. These pushes never actually +create that ref; instead Gerrit creates or updates a Change for review. To land +code in `master`: + +``` +# NOT this (bypasses review): +git push origin HEAD:refs/heads/master + +# This — submits for review: +git push origin HEAD:refs/for/master +``` + +### The commit-msg Hook + +Gerrit provides a `commit-msg` hook that auto-inserts `Change-Id` trailers. +Without it, the `Change-Id` must be added manually. To install: + +```bash +# Via curl (HTTP): +curl -Lo .git/hooks/commit-msg <gerrit-url>/tools/hooks/commit-msg +chmod u+x .git/hooks/commit-msg + +# Via scp (SSH): +scp -p -P 29418 <user>@<gerrit-host>:hooks/commit-msg .git/hooks/ +chmod u+x .git/hooks/commit-msg +``` + +Check if it is already installed: + +```bash +ls -la .git/hooks/commit-msg +``` + +## Common Workflows + +### 1. Create a New Change + +```bash +# Make changes, then commit (hook adds Change-Id automatically) +git add <files> +git commit -m "Fix the foo widget" + +# Verify Change-Id was added: +git log -1 + +# Push for review against a target branch: +git push origin HEAD:refs/for/master +``` + +The server prints the URL of the new change. + +### 2. Update an Existing Change (New Patch Set) + +After receiving review feedback, amend the commit. **Do not modify or remove the +`Change-Id` line** — Gerrit uses it to associate the new commit with the +existing change. + +```bash +# Edit files, then: +git add <files> +git commit --amend # keep Change-Id unchanged in the editor + +# Push again — Gerrit creates patch set 2 on the same change: +git push origin HEAD:refs/for/master +``` + +### 3. Work on Multiple Independent Changes + +Each change should be its own commit on its own local branch: + +```bash +git checkout -b fix-foo origin/master +# make change A, commit, push for review + +git checkout -b fix-bar origin/master +# make change B, commit, push for review +``` + +Do not stack unrelated changes on a single branch unless they are intentionally +dependent (see below). + +### 4. Dependent / Stacked Changes + +Gerrit supports chains of dependent changes. Push multiple commits in one push; +Gerrit creates one change per commit, linked in a relation chain: + +```bash +git checkout -b feature origin/master +git commit -m "Step 1: add plumbing" # Change-Id: I111... +git commit -m "Step 2: wire up the UI" # Change-Id: I222... +git push origin HEAD:refs/for/master # creates two linked changes +``` + +Each change can be reviewed and submitted independently, but Gerrit shows the +dependency chain. + +### 5. Rebase and Re-push + +When the target branch has moved on, rebase and re-push. Keep Change-Ids intact: + +```bash +git fetch origin +git rebase origin/master # Change-Ids are preserved automatically +git push origin HEAD:refs/for/master +``` + +### 6. Squashing Commits Before Push + +When squashing several commits, keep exactly **one** `Change-Id` — prefer the +one already known to Gerrit (i.e., the one that was previously pushed, if any). +Remove the others: + +```bash +git rebase -i origin/master # squash commits +# In the resulting commit message, delete all but one Change-Id line +git push origin HEAD:refs/for/master +``` + +### 7. Cherry-pick to Another Branch + +To propose the same fix on a maintenance branch, cherry-pick and **generate a +new Change-Id** (delete the old one so the hook creates a fresh one): + +```bash +git checkout -b backport-3.10 origin/stable-3.10 +git cherry-pick <commit-sha> +git commit --amend # delete the Change-Id line; save +# hook regenerates a new Change-Id +git push origin HEAD:refs/for/stable-3.10 +``` + +Or, keep the original Change-Id to have Gerrit treat it as a replacement for the +same change on that branch. + +## Push Options + +Additional metadata can be sent with the push via `%` options or `-o`: + +```bash +# Add a topic (groups related changes): +git push origin HEAD:refs/for/master%topic=my-feature + +# Request specific reviewers: +git push origin HEAD:refs/for/master%r=alice@example.com,cc=bob@example.com + +# Mark as work-in-progress: +git push origin HEAD:refs/for/master%wip + +# Mark ready for review: +git push origin HEAD:refs/for/master%ready + +# Apply a review label on push: +git push origin HEAD:refs/for/master%l=Verified+1 + +# Suppress notifications: +git push origin HEAD:refs/for/master%notify=NONE + +# Combine options with commas: +git push origin HEAD:refs/for/master%topic=my-feature,r=alice@example.com +``` + +## Fetching Changes + +To check out an existing change locally (e.g., to test it): + +```bash +# The change page shows a "Download" command; it looks like: +git fetch origin refs/changes/45/12345/3 && git checkout FETCH_HEAD + +# Where: refs/changes/<last-two-digits-of-change-num>/<change-num>/<patch-set> +``` + +## Common Errors and Fixes + +### `missing Change-Id in commit message footer` + +The commit has no `Change-Id`. Fix: + +```bash +# If hook is installed, amend will add it: +git commit --amend --no-edit + +# If hook is NOT installed, add it manually: +# Copy the Change-Id from the Gerrit web UI change page, then: +git commit --amend # paste "Change-Id: I..." into the footer +``` + +### `! [remote rejected] ... (prohibited by Gerrit)` + +Direct push to `refs/heads/*` was rejected. Push to `refs/for/<branch>` instead. + +### `Change xxx: patch set already exists` + +The commit hash hasn't changed since the last push (no new patch set was made). +Amend the commit to create a distinct object, then push again. + +### `! [remote rejected] ... (no new changes)` + +All commits being pushed are already known to Gerrit. This often means the +branch is already up to date with the target. + +## Critical Rules + +- **Never push directly to `refs/heads/*`** unless intentionally bypassing + review (requires special permissions). +- **Never change or remove the `Change-Id` trailer** when amending a commit that + is already under review — doing so creates a second, orphaned change. +- **One logical change = one commit.** Gerrit reviews commits, not branches. +- **Always verify `Change-Id` is present** with `git log -1` before pushing. +- **Keep Change-Ids when rebasing** — git preserves them through rebase + automatically. + +## Additional Resources + +- **`references/gerrit-vs-github.md`** — Side-by-side comparison of Gerrit and + GitHub/GitLab workflows for common tasks +- **`references/change-id-details.md`** — Deep dive on Change-Id: creation, + squashing, cherry-picks, and edge cases
diff --git a/skills/gerrit-workflow/references/change-id-details.md b/skills/gerrit-workflow/references/change-id-details.md new file mode 100644 index 0000000..bd1b59b --- /dev/null +++ b/skills/gerrit-workflow/references/change-id-details.md
@@ -0,0 +1,176 @@ +# Change-Id: Deep Dive + +## What Is a Change-Id? + +A `Change-Id` is a SHA-1-like identifier prefixed with `I` (uppercase i), placed +as a git trailer in the commit message footer. Example: + +``` +Fix the foo widget + +We want a bar, because it improves the foo by providing more +wizbangery to the dowhatimeanery. + +Bug: #42 +Change-Id: Ic8aaa0728a43936cd4c6e1ed590e01ba8f0fbf5b +Signed-off-by: A. U. Thor <author@example.com> +``` + +The `Change-Id` must be in the **last paragraph** of the commit message (the +"footer"). It can coexist with `Signed-off-by`, `Acked-by`, `Bug:`, `Fixes:`, +etc. + +## How the commit-msg Hook Creates Change-Ids + +The hook creates a virtual git commit object from the tree SHA, parent SHA, +author info, committer timestamp, and the proposed message. The SHA-1 of that +virtual object becomes the `Change-Id`. Because the committer timestamp and +parent SHA are included, the same message on two different commits produces two +different Change-Ids. + +The hook **does nothing** if a `Change-Id` line is already present in the +footer. + +## When to Keep the Same Change-Id + +Keep the existing `Change-Id` unchanged when: + +- **Amending a commit** to address review feedback — this adds a new patch set + to the existing change. +- **Rebasing a commit** — git preserves trailers through rebase automatically. +- **Cherry-picking to the same branch** with the intention of updating the same + review. +- **Updating the commit message** for any reason (spelling fix, adding a + `Signed-off-by`, etc.). +- **Backporting** to a different branch — the backport is a separate change on a + separate branch, but logically has the same commit "intent". See + [Uniqueness](#uniqueness-and-scope) below. + +## When to Generate a New Change-Id + +Delete the `Change-Id` line (or the entire trailer) and let the hook regenerate +a fresh one when: + +- **Starting a completely new change** from an existing commit that happened to + have a Change-Id. +- You want to split one change into two separate reviews. + +To force a new `Change-Id` on an existing commit: + +```bash +git commit --amend # remove the Change-Id line, save +# hook runs and inserts a new Change-Id +git log -1 # verify the new Change-Id +``` + +## Squashing Commits + +When squashing two or more commits that each have a `Change-Id`, keep exactly +**one**: + +1. Prefer the one already uploaded to Gerrit (someone may have left comments on + it). +2. If both were uploaded, ask the user if they have a preference or keep the one + with existing comments on it — then abandon the orphaned change in the Gerrit + web UI. +3. If neither was uploaded, keep any one and delete the rest. + +Interactive rebase example: + +```bash +git rebase -i origin/master +# Mark commits as "squash" or "fixup" +# In the resulting commit message editor: +# Keep ONE Change-Id line, delete the others +``` + +## The Link Footer Alternative + +Some projects (e.g., the Linux kernel) use a `Link` footer instead of +`Change-Id`: + +``` +Link: https://gerrit-review.googlesource.com/id/Ic8aaa0728a43936cd4c6e1ed590e01ba8f0fbf5b +``` + +The hook generates this style when `gerrit.reviewUrl` is set in the git config: + +```bash +git config gerrit.reviewUrl https://gerrit-review.googlesource.com/ +``` + +Gerrit recognizes both styles. The base URL in the `Link` footer must match the +server's configured base URL. + +## Disabling Change-Id Generation + +To prevent the hook from adding a `Change-Id` (e.g., for fixup or squash +commits): + +```bash +git config gerrit.createChangeId false +``` + +Or, create a commit whose subject begins with a lowercase word followed by `!` +(e.g., `nopush!`). + +To force insertion even for such commits: + +```bash +git config gerrit.createChangeId always +``` + +## Diagnosing Change-Id Problems + +### Missing Change-Id push rejection + +When Gerrit is configured to require a Change-Id (the default), pushing without +one produces: + +``` +! [remote rejected] HEAD -> refs/for/master (missing Change-Id in commit message footer) +``` + +Fix: amend the commit to add a `Change-Id` line in the footer, or install the +`commit-msg` hook so it is added automatically on future commits. + +Note: some repositories are configured with +`Require Change-Id in commit message = FALSE`, which suppresses this error. +Behavior varies per repo. + +### Auto-generated Change-Id fallback + +If you push a commit without a Change-Id and Gerrit accepts it (repo opt-out), +Gerrit auto-generates one and displays it in the web UI. This ID is **not** in +the commit message, so a subsequent amended push will not be associated with +that change unless you manually copy the displayed `Change-Id` line into the +commit message footer before pushing again. + +Check whether a commit has a `Change-Id`: + +```bash +git log -1 --format='%B' | grep '^Change-Id:' +``` + +Check if the Change-Id is in the footer (last paragraph), not the body: + +```bash +# The footer is separated from the body by a blank line. +# If Change-Id appears before the last blank-line-delimited paragraph, Gerrit rejects it. +# Use the `%(trailers)` format to leverage `git interpret-trailers` for parsing. +git log -1 --format='%(trailers:key=Change-Id)' +``` + +Fix a `Change-Id` that landed in the wrong place: + +```bash +git commit --amend +# Move the Change-Id line to the very end of the message (after a blank line) +``` + +## Uniqueness and Scope + +A `Change-Id` is unique per (repository, branch) combination. The same +`Change-Id` **can** appear on different branches (e.g., when a commit is +cherry-picked to a maintenance branch). Gerrit distinguishes them by repository +and branch, so there is no conflict.
diff --git a/skills/gerrit-workflow/references/gerrit-vs-github.md b/skills/gerrit-workflow/references/gerrit-vs-github.md new file mode 100644 index 0000000..409d17c --- /dev/null +++ b/skills/gerrit-workflow/references/gerrit-vs-github.md
@@ -0,0 +1,95 @@ +# Gerrit vs GitHub/GitLab: Workflow Comparison + +This reference maps common GitHub/GitLab operations to their Gerrit equivalents. + +## Creating a Change for Review + +| GitHub/GitLab | Gerrit | +| --------------------------- | ---------------------------------------------------------------------------------------------------- | +| Push a branch, open a PR/MR | Push commits to `refs/for/<branch>` | +| `git push origin my-branch` | `git push origin HEAD:refs/for/master` | +| Open PR in the web UI | Gerrit change is created automatically on push to `refs/for/<branch>` | +| Branch stays in the remote | No branch under `refs/heads/` is created; the change has per-patchset branches under `refs/changes/` | + +## Updating a Change + +| GitHub/GitLab | Gerrit | +| ---------------------------------- | ------------------------------------------------------------- | +| Push more commits to the PR branch | Amend the commit, push to `refs/for/<branch>` again | +| Force-push the branch | `git commit --amend` + `git push origin HEAD:refs/for/master` | +| PR is updated with new commits | Gerrit adds a new patch set to the same change | + +**Critical difference:** GitHub tracks changes by branch name; Gerrit tracks +them by `Change-Id` trailer in the commit message. Changing the `Change-Id` +creates an entirely new change. + +## Merging / Submitting + +| GitHub/GitLab | Gerrit | +| ------------------------------------ | ------------------------------------------------------------------------ | +| Merge button in the web UI | Submit button in the Gerrit web UI (after approvals) | +| Merge commit or squash merge | Configured per-project (fast-forward, merge, rebase, cherry-pick) | +| PR branch can be deleted after merge | No branch to delete — the commit lands directly in `refs/heads/<branch>` | + +## Reviewing Others' Work + +| GitHub/GitLab | Gerrit | +| ------------------------------ | --------------------------------------------------------------------------------------------------------------- | +| Fetch the PR branch | Fetch the change ref `refs/changes/NN/changeNum/patchSetNum` (where `NN` is the last two digits of `changeNum`) | +| Comment on lines in the UI | Same in Gerrit's web UI | +| "Approve" or "Request changes" | Vote on labels (e.g., `Code-Review +2` to approve, `-2` to block) | + +## Fetching the Latest Branch + +```bash +# Same in both (assuming remote name is `origin`): +git fetch origin +git rebase origin/master +``` + +## Working With Multiple Commits (a Change series) + +**GitHub/GitLab:** A PR can have many commits; reviewers see all of them. + +**Gerrit:** Each commit is its own change. For a feature that spans multiple +commits: + +- Each commit gets its own change number and is reviewed independently. +- Each commit should be reviewable standalone (self-contained). +- The relation chain is shown in the Gerrit UI. +- Changes can be submitted once their parent change (if any) is submittable or + can be submitted together via topics (if the Gerrit server is configured for + topic submit). + +## Topics and Hashtags (Grouping Related Changes) + +Gerrit topics group related changes across one or more repositories: + +```bash +git push origin HEAD:refs/for/master%topic=my-feature-name +``` + +Changes with the same topic appear together in search results and (if the server +configuration is enabled) the "Submitted Together" section of the review screen. + +Hashtags are similar, but you can have multiple hashtags on a single change and +they don't impact submit behavior in any way. Hashtags should be preferred +unless the submit grouping behavior is enabled and needed. + +## No Pull, No Merge Commits From Branches + +In a Gerrit workflow: + +- **Don't** create merge commits to incorporate remote changes — rebase instead. +- **Don't** create long-lived feature branches — each change is a single commit. +- **Don't** `git pull` (which merges) — use `git fetch` + `git rebase`. + +```bash +# Update your local work on top of origin: +git fetch origin +git rebase origin/master + +# If you have a stacked series: +git rebase origin/master # rebases the whole stack; Change-Ids are preserved +git push origin HEAD:refs/for/master # updates all changes in the stack +```