Summary
gr wraps git and gh. For three operations, the wrapped tool offers both a safe-or-precise form and a convenient one, and gr exposes only the convenient one. The result is that any workflow needing the precise form has to drop out of gr and drive the underlying tool per-repo by hand, which is the one thing a multi-repo orchestrator exists to avoid.
These are one class, not three requests: the wrapper is narrower than what it wraps, along the axis that matters for non-interactive use.
The three members
Measured against a build of current dev:
| Command |
gr offers |
The wrapped tool also offers |
Missing from gr |
gr commit |
-m, --message <MESSAGE> |
git commit -F, --file <file> |
file input |
gr pr create |
-t, --title <TITLE>, -b, --body <BODY> |
gh pr create -F, --body-file <file> |
file input |
gr push |
-f, --force |
git push --force-with-lease[=<refname>[:<expect>]] |
lease semantics |
Why file input, specifically
Passing text as a command-line argument requires it to be correctly escaped at every call site, in whatever shell is running. That is a per-call-site correctness obligation that file input removes as a class rather than mitigates:
- Multi-line content. PR bodies and commit messages are routinely multi-paragraph. Embedding them in an argument means embedding newlines in shell syntax.
- Content the shell interprets. Backticks,
$(...), $VAR, !, and quote characters are ordinary in technical prose — code identifiers, shell examples, negations. Each is a place where a subtly wrong quoting form silently changes or drops text rather than failing loudly.
- Machine-generated text. Content produced by another program is already a file or a stream. Forcing it through
argv means re-serialising it and re-escaping it, purely to satisfy the interface.
- Permanence. A commit message becomes immutable history the moment it is pushed. A body that lost a line to a quoting error is not fixable in place afterwards.
- Byte-exact workflows. Anywhere the text is reviewed or approved before publication, the reviewed artifact is a file, and the value of that review depends on the published bytes being the same bytes. An interface that only accepts arguments puts a transformation step between the two.
This is not a hypothetical need — it is why git grew -F and why gh ships --body-file. gr wraps both and drops the capability.
One argument I want to explicitly not make: command-line length limits are a real ceiling but not the practical motivation. ARG_MAX measured 1048576 on the machine I tested, and a 1 MiB single argument passed, which is far above any realistic PR body. The problem is escaping and fidelity, not size.
Why lease semantics, specifically
git push --force overwrites whatever is on the remote, including commits someone else pushed since your last fetch. --force-with-lease refuses unless the remote ref is still where you last observed it, which converts a silent overwrite into a loud refusal.
That distinction matters more in gr than in git, not less, for two reasons:
gr push -f is a fan-out. One invocation force-pushes across every selected repo, so a single command multiplies the blast radius by the number of repos.
- Force-pushing is most common exactly when a branch is under concurrent review — amending in response to review feedback, rebasing onto a moved base. That is precisely the window in which someone else may have pushed to the same branch.
The workaround today is to read the remote ref, compare it to the expected value, and only then force — a manual lease. It works, and it is three commands per repo of exactly the bookkeeping the flag exists to do.
Suggested direction
gr commit: add -F, --message-file <file>, mutually exclusive with -m.
gr pr create: add --body-file <file>, mutually exclusive with -b. A --title-file is worth considering for symmetry but is much less pressing — titles are single-line and short.
gr push: add --force-with-lease, and consider whether -f should warn when a lease form is available. Whether --force should eventually require an explicit opt-in is a policy question worth separating from this issue.
Where a value is read from a file, the file's bytes should reach the underlying tool unmodified — no trailing-newline normalisation, no stripping — since the whole point of the form is fidelity.
Test note
For the file-input flags, a regression test wants content that a shell would mangle if the implementation ever routed it back through one: backticks, $(...), an embedded single quote, and a blank line. Assert the stored message or body matches the input file byte for byte. A test using plain ASCII prose passes whether or not the implementation is correct, so it would not discriminate.
For --force-with-lease, the discriminating pair is: (1) remote unchanged since fetch, push succeeds; (2) remote advanced by another writer, push is refused and the remote commit survives. Without the second case, an implementation that accepted the flag and ignored it goes green.
Summary
grwrapsgitandgh. For three operations, the wrapped tool offers both a safe-or-precise form and a convenient one, andgrexposes only the convenient one. The result is that any workflow needing the precise form has to drop out ofgrand drive the underlying tool per-repo by hand, which is the one thing a multi-repo orchestrator exists to avoid.These are one class, not three requests: the wrapper is narrower than what it wraps, along the axis that matters for non-interactive use.
The three members
Measured against a build of current
dev:groffersgrgr commit-m, --message <MESSAGE>git commit -F, --file <file>gr pr create-t, --title <TITLE>,-b, --body <BODY>gh pr create -F, --body-file <file>gr push-f, --forcegit push --force-with-lease[=<refname>[:<expect>]]Why file input, specifically
Passing text as a command-line argument requires it to be correctly escaped at every call site, in whatever shell is running. That is a per-call-site correctness obligation that file input removes as a class rather than mitigates:
$(...),$VAR,!, and quote characters are ordinary in technical prose — code identifiers, shell examples, negations. Each is a place where a subtly wrong quoting form silently changes or drops text rather than failing loudly.argvmeans re-serialising it and re-escaping it, purely to satisfy the interface.This is not a hypothetical need — it is why
gitgrew-Fand whyghships--body-file.grwraps both and drops the capability.One argument I want to explicitly not make: command-line length limits are a real ceiling but not the practical motivation.
ARG_MAXmeasured 1048576 on the machine I tested, and a 1 MiB single argument passed, which is far above any realistic PR body. The problem is escaping and fidelity, not size.Why lease semantics, specifically
git push --forceoverwrites whatever is on the remote, including commits someone else pushed since your last fetch.--force-with-leaserefuses unless the remote ref is still where you last observed it, which converts a silent overwrite into a loud refusal.That distinction matters more in
grthan ingit, not less, for two reasons:gr push -fis a fan-out. One invocation force-pushes across every selected repo, so a single command multiplies the blast radius by the number of repos.The workaround today is to read the remote ref, compare it to the expected value, and only then force — a manual lease. It works, and it is three commands per repo of exactly the bookkeeping the flag exists to do.
Suggested direction
gr commit: add-F, --message-file <file>, mutually exclusive with-m.gr pr create: add--body-file <file>, mutually exclusive with-b. A--title-fileis worth considering for symmetry but is much less pressing — titles are single-line and short.gr push: add--force-with-lease, and consider whether-fshould warn when a lease form is available. Whether--forceshould eventually require an explicit opt-in is a policy question worth separating from this issue.Where a value is read from a file, the file's bytes should reach the underlying tool unmodified — no trailing-newline normalisation, no stripping — since the whole point of the form is fidelity.
Test note
For the file-input flags, a regression test wants content that a shell would mangle if the implementation ever routed it back through one: backticks,
$(...), an embedded single quote, and a blank line. Assert the stored message or body matches the input file byte for byte. A test using plain ASCII prose passes whether or not the implementation is correct, so it would not discriminate.For
--force-with-lease, the discriminating pair is: (1) remote unchanged since fetch, push succeeds; (2) remote advanced by another writer, push is refused and the remote commit survives. Without the second case, an implementation that accepted the flag and ignored it goes green.