./ahmedhashim

One-liners: git sync

According to my shell history, git sync is my most-used Git command.

I jump between repos all day, and the first thing I want in each one is the default branch with the latest changes. git sync checks it out and pulls, then deletes any local branches whose remote copy is gone.

Here’s what it looks like in my ~/.gitconfig:

[fetch]
  prune = true
[pull]
  rebase = true
[alias]
  db = !git symbolic-ref refs/remotes/origin/HEAD --short | sed 's@origin/@@'
  sync = !git checkout $(git db) && git pull && git worktree prune && git for-each-ref --format '%(refname:short) %(upstream:track)' refs/heads | awk '$2 ~ /gone/ {print $1}' | xargs -r git branch -D

Breaking it down

Prune on every fetch:

[fetch]
  prune = true

This deletes remote-tracking refs for branches that no longer exist on the remote. Without it, origin/* holds on to deleted branches until you prune by hand, and sync has no way to tell which local branches are done.

Make pull rebase by default:

[pull]
  rebase = true

This makes git pull rebase your local commits on top of what it fetched instead of merging. History stays linear and you don’t pick up a merge commit on every pull.

Look up the default branch instead of hardcoding it:

[alias]
  db = !git symbolic-ref refs/remotes/origin/HEAD --short | sed 's@origin/@@'

origin/HEAD points at the remote’s default branch, so stripping origin/ leaves a name you can check out. The same alias works whether a repo uses main or master.

Git sets origin/HEAD when you clone, but it won’t update it if the default branch changes later. git remote set-head origin --auto refreshes it, and also creates it in repos that started with git init instead of a clone.

And the sync one-liner:

[alias]
  sync = !git checkout $(git db) && git pull && git worktree prune && git for-each-ref --format '%(refname:short) %(upstream:track)' refs/heads | awk '$2 ~ /gone/ {print $1}' | xargs -r git branch -D

The leading ! makes this a shell alias, so pipes and command substitution work. Here’s what each piece does, in order:

  • git checkout $(git db): switch to the default branch.
  • git pull: fetch and rebase any local commits onto the updated default branch.
  • git worktree prune: forget worktrees whose directories no longer exist, since Git won’t delete a branch that’s checked out in one.
  • git for-each-ref --format '%(refname:short) %(upstream:track)' refs/heads: list every local branch next to its tracking state, which reads [gone] once the upstream is deleted.
  • awk '$2 ~ /gone/ {print $1}' | xargs -r git branch -D: extract those branch names and delete them.

-D is intentional. A squash merge lands a branch as one new commit on the default branch, so Git never sees the original branch as merged and -d will refuse to delete it. Once the upstream is gone, I treat the branch as disposable. If you keep unpushed work on branches whose remote was deleted, use -d and clean up the squash-merged ones by hand.