Two bash scripts that collapse the squash-merge PR workflow into one command each — ship a change, and tidy up afterward.
./git-ship.sh "Fix typo in hero copy" # branch → commit → push → open PR
./git-sync.sh # re-sync main, delete merged branchesBuilt for the pattern most small teams and solo projects land on: work on main,
squash-merge PRs, let a host like Vercel or Netlify build a preview per PR and
production on merge.
Squash-merge is pleasant right up until you look at your local clone. Merging
rewrites your commits into a single new SHA on main, so afterward git considers
both your local main and the feature branch diverged from origin — even
though the work has already shipped. The fix is a fetch, a reset --hard, a
branch delete, and remembering which branches were actually merged.
git-sync.sh does that. git-ship.sh handles the other end: getting a change
from your working tree into an open PR without thinking about branch naming,
staging, or gh pr create flags.
Drop both scripts anywhere in a repo (a scripts/ directory is the usual home)
and make them executable:
curl -O https://raw.githubusercontent.com/conching/git-ship/main/git-ship.sh
curl -O https://raw.githubusercontent.com/conching/git-ship/main/git-sync.sh
chmod +x git-ship.sh git-sync.shBoth cd to the repo root themselves, so they run from any subdirectory.
Requires: bash, git, and the gh CLI
authenticated against the repo (gh auth status to check).
Run it whenever you've finished a change.
- If you're on
main, syncs toorigin/mainand cuts a branch namedfix/<slugified-subject>-<disambiguator>. If you're already on a feature branch, it uses that one. - Stages everything (
git add -A) and commits with your subject. - Pushes with
--force-with-lease. - Opens a PR against
main— or, if one is already open for the branch, reports its URL instead of erroring.
./git-ship.sh "Fix typo in hero copy"
# → creates branch fix/fix-typo-in-hero-copy-1234
# → commits, pushes, opens PR, prints the URLAn optional second argument becomes the PR body:
./git-ship.sh "Add rate limiting" "Caps the public API at 60 req/min per IP."Because it re-pushes with --force-with-lease, running it again after review
feedback updates the same PR rather than opening a second one.
Run it anytime — it's idempotent.
- Stashes uncommitted work if there is any.
- Resets local
maintoorigin/main. - Asks GitHub which of your local feature branches have merged PRs, and deletes those.
- Pops the stash back.
These scripts run reset --hard, so the sharp edges got attention:
- Uncommitted work is stashed before any reset, in both scripts, and popped afterward. The stash is tagged with a timestamp so it's findable if something goes sideways mid-run.
--force-with-lease, never--force— a push aborts if someone else pushed to your branch in the meantime.- Preflight checks for
ghbeing installed and authenticated, with the fix command printed on failure. set -euo pipefailthroughout, so a failed step stops the script rather than cascading.- Only branches with merged PRs are deleted, confirmed against GitHub — not inferred from local state.
Worth knowing before you adopt them:
- Your default branch is
main. - You squash-merge PRs.
- Feature branches are yours alone —
--force-with-leaseis appropriate for a branch nobody else commits to, and not for a shared one. git-ship.shstages all pending changes. It is not the tool for splitting a working tree into separate commits.
MIT