139

I have 57 local branches. I know I made a change to a certain file in one of them, but I'm not sure which one. Is there some kind of command I can run to find which branches contain changes to a certain file?

Dustin
  • 3,965
  • 5
  • 27
  • 33

5 Answers5

122

Find all branches which contain a change to FILENAME (even if before the (non-recorded) branch point)

FILENAME="<filename>"
git log --all --format=%H $FILENAME | while read f; do git branch --contains $f; done | sort -u

Manually inspect:

gitk --all --date-order -- $FILENAME

Find all changes to FILENAME not merged to master:

git for-each-ref --format="%(refname:short)" refs/heads | grep -v master | while read br; do git cherry master $br | while read x h; do if [ "`git log -n 1 --format=%H $h -- $FILENAME`" = "$h" ]; then echo $br; fi; done; done | sort -u
BMW
  • 42,880
  • 12
  • 99
  • 116
Seth Robertson
  • 30,608
  • 7
  • 64
  • 57
  • 1
    Do I need to replace anything in this command besides FILENAME? It returns all 57 branch names. – Dustin Jun 06 '11 at 22:01
  • @Dustin: If all 57 branches contain that filename, then all 57 branches contain a change which includes that filename. Branches start from the oldest revision reachable on that branch, even if that revision existed before you created the branch (the branch is in some senses retroactively created). I think you need to define your problem better. Do you know what the change is? Could you use `git log -Schange …` or `git log --grep LOGMESSAGE …` (with … representing the rest of the command I mentioned). – Seth Robertson Jun 06 '11 at 22:08
  • 2
    @Dustin: Another option is to use `gitk --all -- filename` which will graphically show you all of the changes to that file. If you can identify the commit in question, then you can use `git branch --contains` to see what branches the commit has migrated to. If you want to see what branch the commit in question was *originally* created on, then google git-what-branch, but be aware that fast-forward merges can obscure that information. – Seth Robertson Jun 06 '11 at 22:10
  • @Seth: I couldn't remember the commit message or exact code change. I just have a general idea. The problem is that it is not merged with master yet, so in order to use gitk I would still have to checkout each branch to find what I'm looking for. It would be nice if I could say, "git, show me the branches that made changes to FILENAME since its branch point" – Dustin Jun 06 '11 at 22:14
  • @Dustin: `gitk --all [--date-order] -- filename` does NOT make you check out each branch. It will show you all changes on all branches. – Seth Robertson Jun 06 '11 at 22:19
  • @Dustin: See edit for git-cherry solution. Your clue that the change was not merged to master may help you. – Seth Robertson Jun 06 '11 at 22:27
  • 1
    You could write the first line more efficiently `git log --all --format='--contains %H' "$file" | xargs git branch` – kojiro Mar 21 '14 at 17:33
  • @Geo: gitk is sometimes an extra package. Try `apt-get install gitk` or `dnf install gitk` or the appropriate similar command for whatever OS you are running. gitk was actually the reason I first switched to git, though I quickly found tons of other good reasons. – Seth Robertson Jun 09 '19 at 02:56
74

All you need is

git log --all -- path/to/file/filename

If you want to know the branch right away you can also use:

git log --all --format=%5 -- path/to/file/filename | xargs -I{} -n 1 echo {} found in && git branch --contains {}

Further, if you had any renames, you may want to include --follow for the Git log command.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Adam Dymitruk
  • 124,556
  • 26
  • 146
  • 141
19

Looks like this is still a problem without an appropriate solution. I don't have enough credits to comment, so here's my little contribution.

Seth Robertson's 1st solution kinda worked for me, but only gave me local branches, among which where many false positives, probably because of merges from the stable branch.

Adam Dymitruk's 2nd solution didn't work for me at all. For starters, what's --format=%5? It isn't recognized by git, I couldn't find anything about it and I couldn't get it to work with other format options.

But his 1st solution combined with the --source option and with a simple grep proved helpful:

git log --all --source -- <filename> | grep -o "refs/.*" | sort -u

This gives me a number of remote tags and branches and one local branch, where I made the latest changes to the file. Not sure how complete this is.

UPDATE as per @nealmcb 's request, sorting branches by most recent change:

First, you could change the grep to "refs/heads/.*", which will give you the local branches only. If there are only a few branches, you could examine the latest commit of each one like this:

git log -1 <branch> -- <filename>

If there are more branches and you really want to automate this, you can combine the two commands using xargs, git log formatting and another sort into this one-liner:

git log --all --source -- <filename> | grep -o "refs/heads/.*" | sort -u | xargs -I '{}' git log -1 --format=%aI%x20%S '{}' -- <filename> | sort -r

This will result in output like this:

2020-05-07T15:10:59+02:00 refs/heads/branch1
2020-05-05T16:11:52+02:00 refs/heads/branch3
2020-03-27T11:45:48+00:00 refs/heads/branch2
Simpleton
  • 632
  • 5
  • 22
  • Great - thank you. Now how about sorting by most recent change? – nealmcb May 04 '20 at 17:12
  • The utility of that last long command is _**OMGWTFBBQ**_ level AMAZING. Absolutely beautiful. Don't let the votes on the other answers from an eight-year head start fool you. That's gold, Jerry! (except this time it really is) – ruffin Feb 15 '23 at 15:00
6

I know this question is ancient, but I kept coming back to it before developing my own solution. I feel it's more elegant and thanks to use of merge base filters out unwanted branches.

#!/bin/bash

file=$1
base=${2:-master}

b=$(tput bold) # Pretty print
n=$(tput sgr0)

echo "Searching for branches with changes to $file related to the $base branch"

# We look through all the local branches using plumbing
for branch in $(git for-each-ref --format='%(refname:short)' refs/heads/); do
  # We're establishing a shared ancestor between base and branch, to only find forward changes.  
  merge_base=$(git merge-base $base $branch)
  # Check if there are any changes in a given path.
  changes=$(git diff $merge_base..$branch --stat -- $file)

  if [[ ! -z $changes ]]; then
    echo "Branch: ${b}$branch${n} | Merge Base: $merge_base"
    # Show change statistics pretty formatted
    git diff $merge_base..$branch --stat -- $file
  fi
done

If you put it in PATH as git-find-changes (with executable permissions) - you can then call it with git find-changes /path

Example output for git find-changes app/models/

Branch: update_callbacks | Merge base: db07d23b5d9600d88ba0864aca8fe79aad14e55b
 app/models/api/callback_config.rb | 28 ++++++++++++++++++++++++++++
 1 file changed, 28 insertions(+)
Branch: repackaging | Merge base: 76578b9b7ee373fbe541a9e39cf93cf5ff150c73
 app/models/order.rb                 | 38 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 38 insertions(+)
Toby Speight
  • 27,591
  • 48
  • 66
  • 103
Marcin Raczkowski
  • 1,500
  • 1
  • 18
  • 26
  • Thank you so much for this - it really helped me out today. One small amendment - the usage should be git-find-changes once you've added it to your path. You missed the hyphen between git and find. – Phil Jul 20 '22 at 12:18
  • 1
    Glad it worked for you @Phil but there's no error :) `git` has this handy feature that it will allow you to issue any command in format `git command-name` as long as `git-command-name` executable is in $PATH. it will also display help (if provided) with `git help command-name` this example does not provide that help but that's why I shown it like that in the example – Marcin Raczkowski Jul 21 '22 at 21:57
  • 1
    Needs a run through Shellcheck (e.g. we need to quote `$file` in case some idiot has whitespace in filenames). And we can easily `[ "$changes" ]` instead of `[[ ! -z $changes ]]` to make this a portable (faster) shell script. – Toby Speight Jun 07 '23 at 10:34
0

The following is an inelegant brute-force method, but I expect it should work. Make sure you've stashed any uncommitted changes first as it will switch which branch you are currently on.

for branch in $(git for-each-ref --format="%(refname:short)" refs/heads); do
    git checkout $branch && git grep SOMETHING
done
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
whiteinge
  • 595
  • 1
  • 7
  • 15