jakub.g
jakub.g

Reputation: 41418

Get only git branches which match / don't match the fetch specs from `.git/config`, and delete non-matching ones

Suppose I have a .git/config as follows:

[remote "origin"]
        url = [email protected]:some/repo.git
        fetch = +refs/heads/main:refs/remotes/origin/main
        fetch = +refs/heads/staging*:refs/remotes/origin/staging*
        fetch = +refs/heads/jakub/*:refs/remotes/origin/jakub/*

I want a git command that will filter the git branch output and only print the branches which match the refspecs from .git/config, i.e. the branches whose corresponding remote tracking branches are actively being updated when I run parameterless git fetch.

Is there such a git command?

E.g. I want it to print:

main
staging-1
staging-2
jakub/foo
jakub/bar

I want it to filter out all non-matching branches like:

bob/foo
bob/bar
other

Conversely, I also may want to only get non-matching ones.

Update: The ultimate goal is to delete non-matching branches from local clone, and only keep the matching ones. I downloaded a reference mirror clone which has all branches (though they don't have a git tracking relationship), I updated my .git/config to minimize the subset of branches that are fetched, and I want to get rid of the other branches.

Upvotes: 2

Views: 94

Answers (2)

jakub.g
jakub.g

Reputation: 41418

A solution which doesn't rely on git tracking status, but only on branch names

  • Assuming the .git/config does not have negative refspecs (starting with ^)
  • Assuming the current branch is one that won't be deleted

The following command seems to do what I want:

git config --get-all remote.origin.fetch | sed 's|^+||' | cut -d':' -f1 | sed 's|refs/heads/||' | xargs git branch --list | cut -c3- | sort | uniq

Explanation:

  • git config --get-all remote.origin.fetch reads the fetch specs
  • sed strips the leading + if encountered
  • cut splits on : and takes left hand side (src)
  • sed removes leading refs/heads/
  • then I run git branch --list on each line of input, to find matching branches for every refspec
  • cut removes first two columns from git branch output
  • sort | uniq is to remove duplicates (unlikely but possible)

To get the inverse, i.e. only the non-matching branches, one could do:

the-command-above > /tmp/matching.txt
git branch | cut -c3- > /tmp/all.txt
comm -23 all.txt matching.txt > /tmp/not-matching.txt

We make sure that both files are sorted (git branch is sorted by default), and then, use comm which is the unix utility which compares two sorted files.


To delete the non-matching branches, a naive approach would be to try:

cat /tmp/not-matching.txt | xargs git branch -D

This is fast and mostly works, except if there are branches which differ by case only, because git can't create two lock files with "same" name but differing by case only, at the same time.

A workaround for this would be:

cat /tmp/not-matching.txt | xargs -n1 git branch -D

...to only delete one branch at a time, however this is very slow when number of branches is huge.

Something that has best of both: first delete one-by-one the conflicting-case branches; and then in parallel, the others.

cat /tmp/not-matching.txt | tr '[:upper:]' '[:lower:]' | uniq -d > /tmp/dupe-branches-with-conflicting-case.txt
cat /tmp/dupe-branches-with-conflicting-case.txt | xargs -I {} grep -i {} /tmp/not-matching.txt | xargs -n1 git branch -D
cat /tmp/not-matching.txt | xargs git branch -D

Explanation:

  • We lowercase branch names and use uniq -d to detect which branch names are duplicated. Say we have FooBar and FOOBAR, they will get lowercased to foobar and uniq -d will find `foobar.
  • Then for each of such matches (like foobar), we grep -i (case insenstive) the file with branches to delete, to find back FooBar and FOOBAR, and we delete them one by one (xargs -n1) so that git doesn't crash.
  • Then we delete all branches in one shot (xargs without -n1) which is very fast. Note: This will result in error: branch FOOBAR not found since we deleted them in previous step. You may want to add error handling for that case, or suppress errors in the output like > /dev/null 2>&1.

Upvotes: 4

Romain Valeri
Romain Valeri

Reputation: 22057

Is there such a git command?

Yes, there is.

git branch, the "porcelain" branch listing command, contrary to what I've written in first answer, does allow this, even without switching to its plumbing counterpart git for-each-ref. The useful option they share being conditional formats. Thanks to @LeGEC for pointing that out.

Try this format:

--format="%(if)%(upstream:short)%(then)%(refname:short)%(end)"

The full command in alias would be

# alias "tb" for Tracked Branches for instance
git config --global alias.tb '!git branch --format="%(if)%(upstream:short)%(then)%(refname:short)%(end)" | sort -u'

(%(if)%(upstream:short) tests if the branch has a remote set.)

Edit: and since you @jakub suggested how to get the inverse...

# alias "ntb" for Non Tracked Branches
git config --global alias.ntb '!git branch --format="%(if)%(upstream:short)%(then)%(else)%(refname:short)%(end)" | sort -u'

Upvotes: 5

Related Questions