143

So I found the question about how to view the change history of a file, but the change history of this particular file is huge and I'm really only interested in the changes of a particular method. So would it be possible to see the change history for just that particular method?

I know this would require git to analyze the code and that the analysis would be different for different languages, but method/function declarations look very similar in most languages, so I thought maybe someone has implemented this feature.

The language I'm currently working with is Objective-C and the SCM I'm currently using is git, but I would be interested to know if this feature exists for any SCM/language.

Erik B
  • 40,889
  • 25
  • 119
  • 135
  • 1
    I've seen such functionality in Git GSoG proposal. – Vi. Jan 24 '11 at 12:25
  • Is this the proposal you were talking about? http://lists-archives.org/git/715169-gsoc-draft-proposal-line-level-history-browser.html – Erik B Jan 24 '11 at 13:40
  • 1
    possible duplicate of [Retrieve the commit log for a specific line in a file?](http://stackoverflow.com/questions/8435343/retrieve-the-commit-log-for-a-specific-line-in-a-file) – László Papp Oct 10 '14 at 08:26
  • @lpapp the question here was asked 10 months earlier, the other question should be marked as dupe of this one (if they are dupes at all). – Dirty-flow Oct 13 '14 at 06:47
  • @Dirty-flow: who cares when it was asked? The point is that it duplicates the information, that question _is_ answered, and evaluated much more useful than this by the community within less time. – László Papp Oct 13 '14 at 07:10
  • 2
    @lpapp Those are two completely different questions. You could possibly write a script that resolves a function name to a range of lines and then use the technique from that question to get the history for those lines, but in itself it does not answer this question. – Erik B Oct 15 '14 at 09:51
  • If you say so ... but then the question really has nothing to do with git, as it is just getting the lines for a function in some source file. – László Papp Oct 15 '14 at 09:53

7 Answers7

162

Recent versions of git log learned a special form of the -L parameter:

-L :<funcname>:<file>

Trace the evolution of the line range given by "<start>,<end>" (or the function name regex <funcname>) within the <file>. You may not give any pathspec limiters. This is currently limited to a walk starting from a single revision, i.e., you may only give zero or one positive revision arguments. You can specify this option more than once.
...
If “:<funcname>” is given in place of <start> and <end>, it is a regular expression that denotes the range from the first funcname line that matches <funcname>, up to the next funcname line. “:<funcname>” searches from the end of the previous -L range, if any, otherwise from the start of file. “^:<funcname>” searches from the start of file.

In other words: if you ask Git to git log -L :myfunction:path/to/myfile.c, it will now happily print the change history of that function.

Community
  • 1
  • 1
eckes
  • 64,417
  • 29
  • 168
  • 201
  • 39
    this may work for objective-c out of the box but if you are doing this for other languages (e.g. Python, Ruby etc.) you may need to add the appropriate configuration inside a .gitattributes file in order for git to recognise function/method declarations in that language. For python use *.py diff=python, for ruby use *.rb diff=ruby – samaspin Jul 09 '17 at 10:35
  • 3
    How does git trace the function? – nn0p Oct 30 '17 at 16:42
  • 2
    @nn0p I assume by having a syntax knowledge of several languages and thus knowing how to isolate a function and trace her changes. – JasonGenX Mar 06 '18 at 17:33
  • 4
    Extending @samaspin's comment, for other languages you can refer to the docs here : https://git-scm.com/docs/gitattributes#_generating_diff_text – edhgoose Apr 26 '18 at 11:09
  • 1
    This doesn't work for languages like Scala and Java and even C which use nested curly braces (regular expressions can't handle that in general), and even the start,end form doesn't work when the function is moved in the file and modified in the same commit. – Robin Green Nov 23 '19 at 08:40
  • 1
    @eckes how about the member function of the class? `git log -L :Class_Name::foo_fun():path/to/myfile.c,` seems does not work indeed. – John Apr 14 '22 at 06:34
  • Downvote, since it completely ignores the part about methods. Would you be so kind to incorporate the useful comments here into your answer? – Yaroslav Nikitenko Aug 14 '22 at 19:53
  • Using single quote characters is possible. You can write the whole function's signature if you like. For instance, in Rust: `git log -L '/pub fn my_func/':src/lib.rs` – pedropedro Jul 19 '23 at 08:37
17

Using git gui blame is hard to make use of in scripts, and whilst git log -G and git log --pickaxe can each show you when the method definition appeared or disappeared, I haven't found any way to make them list all changes made to the body of your method.

However, you can use gitattributes and the textconv property to piece together a solution that does just that. Although these features were originally intended to help you work with binary files, they work just as well here.

The key is to have Git remove from the file all lines except the ones you're interested in before doing any diff operations. Then git log, git diff, etc. will see only the area you're interested in.

Here's the outline of what I do in another language; you can tweak it for your own needs.

  • Write a short shell script (or other program) that takes one argument -- the name of a source file -- and outputs only the interesting part of that file (or nothing if none of it is interesting). For example, you might use sed as follows:

    #!/bin/sh
    sed -n -e '/^int my_func(/,/^}/ p' "$1"
    
  • Define a Git textconv filter for your new script. (See the gitattributes man page for more details.) The name of the filter and the location of the command can be anything you like.

    $ git config diff.my_filter.textconv /path/to/my_script
    
  • Tell Git to use that filter before calculating diffs for the file in question.

    $ echo "my_file diff=my_filter" >> .gitattributes
    
  • Now, if you use -G. (note the .) to list all the commits that produce visible changes when your filter is applied, you will have exactly those commits that you're interested in. Any other options that use Git's diff routines, such as --patch, will also get this restricted view.

    $ git log -G. --patch my_file
    
  • Voilà!

One useful improvement you might want to make is to have your filter script take a method name as its first argument (and the file as its second). This lets you specify a new method of interest just by calling git config, rather than having to edit your script. For example, you might say:

$ git config diff.my_filter.textconv "/path/to/my_command other_func"

Of course, the filter script can do whatever you like, take more arguments, or whatever: there's a lot of flexibility beyond what I've shown here.

Paul Whittaker
  • 3,817
  • 3
  • 25
  • 20
  • 1
    The take more than one argument didn't work for me, but putting the function name in hard works fine and this is really awesome ! – qwertzguy Mar 06 '14 at 18:52
  • Brilliant, though I wonder how (in)convenient it is to switch between lots of different methods. Also, do you know of a program that can pull out an entire C-like function? – nafg Oct 16 '15 at 01:02
15

The closest thing you can do is to determine the position of your function in the file (e.g. say your function i_am_buggy is at lines 241-263 of foo/bar.c), then run something to the effect of:

git log -p -L 200,300:foo/bar.c

This will open less (or an equivalent pager). Now you can type in /i_am_buggy (or your pager equivalent) and start stepping through the changes.

This might even work, depending on your code style:

git log -p -L /int i_am_buggy\(/,+30:foo/bar.c

This limits the search from the first hit of that regex (ideally your function declaration) to thirty lines after that. The end argument can also be a regexp, although detecting that with regexp's is an iffier proposition.

badp
  • 11,409
  • 3
  • 61
  • 89
  • Sweet! FYI, this is new in Git v1.8.4. (Guess I should upgrade.) Though a more precise solution would be nice... like if someone scripts up Paul Whittaker's answer. – Greg Price Feb 20 '14 at 22:00
  • @GregPrice [Apparently the edges of the search can even be regular expressions](http://git-scm.com/docs/git-log), so you can at least have a more or less precise start point. – badp Feb 20 '14 at 22:27
  • Oh, wow. In fact: instead of writing your own regexp you can just say `-L ":int myfunc:foo/bar.c"` and limit to the function with that name. This is fantastic -- thanks for the pointer! Now if only the function detection were a little more reliable... – Greg Price Mar 09 '14 at 20:41
13

git log has an option '-G' could be used to find all differences.

-G Look for differences whose added or removed line matches the given <regex>.

Just give it a proper regex of the function name you care about. For example,

$ git log --oneline -G'^int commit_tree'
40d52ff make commit_tree a library function
81b50f3 Move 'builtin-*' into a 'builtin/' subdirectory
7b9c0a6 git-commit-tree: make it usable from other builtins
Mark Amery
  • 143,130
  • 81
  • 406
  • 459
lht
  • 358
  • 1
  • 5
  • 11
  • 6
    I did not run the command, but looks to me like this command would only show the commits that touch the line that matches the regex, which isn't the entire method/function. – Erik B Oct 15 '13 at 08:16
  • if you want more context, you can replace `--oneline` with `-p` – random-forest-cat Jan 21 '15 at 22:42
  • 4
    But what if a change was made 20 lines into the method? – nafg Oct 16 '15 at 00:57
  • +1. For me, the top answer worked but only showed the most recent commit. Possibly due to rebases or the function moving about a few times, not sure. By searching for the actual line of code instead of the function it was in (albeit a one-liner) this answer allowed me to easily spot the commit I was looking for. Thankfully I usually write useful commit messages! – Dave S Mar 17 '19 at 10:51
7

The correct way is to use git log -L :function:path/to/file as explained in eckes answer.

But in addition, if your function is very long, you may want to see only the changes that various commit had introduced, not the whole function lines, included unmodified, for each commit that maybe touch only one of these lines. Like a normal diff does.

Normally git log can view differences with -p, but this not work with -L. So you have to grep git log -L to show only involved lines and commits/files header to contextualize them. The trick here is to match only terminal colored lines, adding --color switch, with a regex. Finally:

git log -L :function:path/to/file --color | grep --color=never -E -e "^(^[\[[0-9;]*[a-zA-Z])+" -3

Note that ^[ should be actual, literal ^[. You can type them by pressing ^V^[ in bash, that is Ctrl + V, Ctrl + [. Reference here.

Also last -3 switch, allows to print 3 lines of output context, before and after each matched line. You may want to adjust it to your needs.

Hazzard17
  • 633
  • 7
  • 14
6
  1. Show function history with git log -L :<funcname>:<file> as showed in eckes's answer and git doc

    If it shows nothing, refer to Defining a custom hunk-header to add something like *.java diff=java to the .gitattributes file to support your language.

  2. Show function history between commits with git log commit1..commit2 -L :functionName:filePath

  3. Show overloaded function history (there may be many function with same name, but with different parameters) with git log -L :sum\(double:filepath

VLAZ
  • 26,331
  • 9
  • 49
  • 67
LF00
  • 27,015
  • 29
  • 156
  • 295
2

git blame shows you who last changed each line of the file; you can specify the lines to examine so as to avoid getting the history of lines outside your function.

Will
  • 73,905
  • 40
  • 169
  • 246