./ahmedhashim

One-liners: git sync

According to my shell history, git sync is what I use the most when using Git.

I have a bunch of repos that I switch between all day. So when I go to a repo, I want to be on the default branch and have the newest changes. git sync will check that out and pull, then delete local branches that don’t have a remote branch anymore.

This is what I have 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 will delete remote-tracking refs for branches that don’t exist on the remote anymore. If you don’t do this, you’ll have stale origin/* branches, and sync will have no idea which local branches to delete.

Always rebase when pulling:

[pull]
  rebase = true

This will make git pull rebase your changes instead of doing a merge, so your history stays linear without 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 will always point to the default branch on the remote, so if you remove origin/, you now have a branch you can check out. This also works whether someone is using main or master.

When you clone a repo, Git will automatically create origin/HEAD for you. If the default branch gets changed on the remote, this won’t update. You can use git remote set-head origin --auto to update it. This will also create origin/HEAD if you created your repo using git init and didn’t clone.

And here’s the sync command:

[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

Having ! at the beginning creates a shell alias, which allows us to use pipes and command substitution. Here’s what each part 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.

We want -D because when you squash merge a branch into your default branch, Git will think your branch was never merged into it, so -d won’t allow you to delete the branch. Since the upstream is deleted, I no longer care about my branch. If you have work on a branch that you haven’t pushed and you want to keep the branch, use -d. Then just delete your squash-merged branches by hand.