History modes
Choose how a feature's commits appear on your public repo, and whose name they carry.
Your private branch can be as messy as you like. When you publish, you pick what the public history looks like: every commit as you made it, one commit, or commits you regroup and reword. The examples publish a feature add-search with two commits.
| Mode | What goes public | Good for |
|---|---|---|
preserve |
Every feature commit, as written | Tidy histories you want to keep |
squash |
One commit, with a message you write | Work in progress with noisy commits |
curated |
The groups, order and messages you choose | Large features with a story to tell |
Pass --mode to promote. Without it, promote uses promote.default_mode from .ghostbranch.yml, which is preserve unless you change it.
Preserve
Section titled “Preserve”preserve publishes each feature commit as it is, with its message and its date. Two things are taken out: the overlay’s paths and any internal sections. A commit left empty after that is dropped, and the plan says so (2 public commit(s), 1 dropped as empty).
If every commit is left empty, there’s nothing to publish, in any mode. The gate then stops with NOTHING_TO_PUBLISH (reason all_stripped) before it pushes anything or starts CI. So does a feature with no commits of its own yet (reason no_commits). See A feature with nothing to publish.
Commit messages are published exactly as written, trailers such as Co-Authored-By: included. Ghostbranch doesn’t strip or rewrite them. If a coding agent adds its own trailer and you’d rather it didn’t go public, tidy the messages on your branch first, or use squash or curated.
Squash
Section titled “Squash”squash publishes the whole feature as one commit. Write its message with -m:
Without -m, promote stops and asks for one: error: a commit message is required: pass -m/--message [MISSING_INPUT]. The pull request’s title is the message’s first line, unless you pass --title.
Curated
Section titled “Curated”curated lets you regroup, reorder and reword the commits. Write a JSON file that names each feature commit exactly once, in the order to publish, with a message for each group. The commits come from the gate’s result (ghostbranch gate add-search --json, under data.commits, each with its source SHA, subject, author and files):
{"commits": [ {"from": ["893f3408d9cad3ce18bbd77685e8d2f5a6b0f6c5", "09278daf2c364e1770327ce8df39e1e8d47f3e73"], "message": "Add search to the docs"}]}This one folds both commits into one. Then:
With more than one group, add --title for the pull request. A file that leaves a commit out, or names one twice, is refused before anything is built:
With --json, error.details lists the SHAs that are missing, repeated, unknown or ambiguous. Reordering commits that change the same lines can fail with CURATE_CONFLICT or CURATE_TREE_MISMATCH: keep those in order, or reorder them on your branch first. Whatever the groups, the last public commit always ends on exactly the files the gate tested.
Leaks in older commits
Section titled “Leaks in older commits”The mode decides what the leak scan reads. A preserve publish checks every commit, so a secret added and later deleted still blocks it. squash and curated check what they publish, so a feature whose older commit held a secret can be published as one commit, or with that commit grouped with the one that removed it, and the secret never ships.
Authors
Section titled “Authors”promote.author_email in .ghostbranch.yml decides whose name each public commit carries:
noreply(the default) publishes every commit as the publishing account’s GitHub noreply identity, such asyou <12345+you@users.noreply.github.com>. Your local email never goes public.keeppublishes each commit’s own author, as recorded on your machine.maplooks each author’s email up inpromote.author_map, and publishes the identity it names:
promote: author_email: map author_map: "you@laptop.local": "Your Name <you@example.com>"Under map, an author who isn’t in the map stops the publish with AUTHOR_UNMAPPED, and error.details.emails lists them. A person adds them to the map, or switches back to noreply.
Commits someone made on the pull request itself, which feature pull brought into your feature, always keep their own authors. A squash publish credits them with a Co-authored-by: trailer. See Updating an open PR.
Squash and merge on GitHub
Section titled “Squash and merge on GitHub”GitHub’s merge button has three choices, and only two of them keep what preserve published:
- Create a merge commit keeps every commit as published, and adds one merge commit on
main. - Rebase and merge keeps every commit, but copies each one onto
main. - Squash and merge replaces them all with one new commit on
main. The commits, messages and authors you chose to publish one by one are gone frommain’s history: it is much as if you had published withsquash, but with a message GitHub wrote.
So merge a preserve pull request with a merge commit or a rebase. If you always want one commit per pull request, publish with squash instead, which lets you write that commit’s message yourself and is checked by the gate as one commit.
On a stack, it does the same to each pull request: one commit per pull request.
Errors you may see
Section titled “Errors you may see”CURATE_INVALIDexit 2The --curate file is malformed, or its groups don't name every feature commit exactly once.
- not JSON of the form {"commits": [{"from": [<sha>...], "message": "..."}]}
- a group with no commits or an empty message
- a SHA that is missing, repeated, ambiguous, or not one of the feature's commits
Fix the file using error.details (missing, repeated, unknown, ambiguous). The feature's commits are listed in the gate result's commits.
CURATE_CONFLICTexit 4Replaying the curated groups in their order hit a conflict.
Change the group order in the --curate file so commits that touch the same lines keep their original order, or reorder the commits on your feature branch yourself. Ghostbranch never resolves conflicts.
CURATE_TREE_MISMATCHexit 4The curated replay ends on a different tree from the one the gate's CI tested.
Keep commits that touch the same lines in their original order, or reorder on your feature branch, gate again, and use preserve.
AUTHOR_UNMAPPEDexit 1promote.author_email is map, and some commit authors are not in promote.author_map. Commits feature pull brought in from the public PR keep their authors and are never unmapped.
A person adds the emails in error.details.emails to promote.author_map in .ghostbranch.yml on the overlay branch, or switches author_email to noreply.
This needs a person.