Tool to find most recent commit that is changed by a previous change or change in workspace

257 Views Asked by At

TL;DR

(You'll probaly misunderstand this, so better skip to the next section ;)

I'd like to find the (previous) commits a patch (commit or staged/unstaged workspace change) would change.

TL;DR: continue reading, but skip "Background" ...

Background

I'll describe my workflow here, so you can understand what I need.

On my development branch I frequently commit small changes in a lot of WIP commits. When I'm happy with my overall change, then I interactively rebase all these WIP commits to make up a clean history. I squash commits into other commits and write a detailed commit message.

There are three cases where I'd like to note the previous commit another commit should be squashed into.

  • When I notice that there is a bug that I've introduced in one of the previous WIP commits
  • When I'd like to explicitly merge the most recent change into a certain WIP commit
  • Mentioning the commit ID that introduced a bug within the bugfix's commit message

Then I make a commit noting the commit that change (bugfix) has to be squashed into.

A history might look like this:

[WIP] Fixed some bug [SQUASH with '[WIP] Made some sloppy change'] (A)
...
[WIP] Made some sloppy change (B)
...

(Sometimes I produce more than 15 commits within 4 hours)

Since I do such minor patches very often I thought about an automated solution.

What I want to know

When a patch ...

  • removes and adds lines (change), I'd like to get the commit that has introduced or recently changed that line
  • adds lines, I'd like to get the commits of the lines before and after that insertion that where recently changed or introduces
  • removes lines only, I'd like to get the commits that added or recently modified these lines

How I do this the manual way

TL;DR: skip this section and continue reading the "Example..." section ...

When preparing for commit (A), I search for the commit (B) this way:

  • Examine local change and remember the affecte line numbers by issuing
    • git diff if the change has not been commited yet
    • git log -1 -p if the change has already been comitted
  • Find the commit that affected the memorized line(s) before the patch was made by issuing
    • git blame HEAD -- path/to/file if the change has not been commited yet
    • git blame HEAD~1 -- path/to/file if the change has already been comitted

Afterwards, in both cases then I get the commit message for those commit IDs using git log <COMMIT ID>

Example for add and change (change not commited yet):

This is the patch ( git diff )

diff --git a/Gruntfile.js b/Gruntfile.js
index 9af9384..380726c 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -82,6 +82,7 @@ module.exports = function(grunt) {
                less: {
                        build: {
                                options: {
+                                       dumpLineNumbers: "comments",
                                        paths: ["target/"]
                                },

@@ -141,7 +142,7 @@ module.exports = function(grunt) {

                        src: {
                                files: 'src/**',
-                               tasks: ['copy:source-tree', 'copy:devel-source-tree']
+                               tasks: ['copy:source-tree', 'copy:devel-source-tree', 'less']
                        }
                }
        });

This is git blame -L82,+8 HEAD -- Gruntfile.js and git blame -L142,+8 HEAD -- Gruntfile.js

f39f25c0 (Nobody 2014-05-17 16:08:20 +0200 82)          less: {
f39f25c0 (Nobody 2014-05-17 16:08:20 +0200 83)                  build: {
f39f25c0 (Nobody 2014-05-17 16:08:20 +0200 84)                          options: {
f39f25c0 (Nobody 2014-05-17 16:08:20 +0200 85)                                  paths: ["target/"]
f39f25c0 (Nobody 2014-05-17 16:08:20 +0200 86)                          },
f39f25c0 (Nobody 2014-05-17 16:08:20 +0200 87)
f39f25c0 (Nobody 2014-05-17 16:08:20 +0200 88)                          files: {
f39f25c0 (Nobody 2014-05-17 16:08:20 +0200 89)                                          "target/styles.css": "src/less/styles.less"


e2e46b59 (Nobody 2014-05-03 10:30:34 +0200 142)                         src: {
e2e46b59 (Nobody 2014-05-03 10:30:34 +0200 143)                                 files: 'src/**',
e2e46b59 (Nobody 2014-05-03 10:30:34 +0200 144)                                 tasks: ['copy:source-tree', 'copy:devel-source-tree']
f9a635c0 (Nobody 2014-04-21 16:03:38 +0200 145)                         }
f9a635c0 (Nobody 2014-04-21 16:03:38 +0200 146)                 }
f9a635c0 (Nobody 2014-04-21 16:03:38 +0200 147)         });
5128dcdc (Nobody 2014-04-20 12:56:05 +0200 148)
f9a635c0 (Nobody 2014-04-21 16:03:38 +0200 149)         grunt.task.loadTasks("tasks/");

These are the commit messages ( git log --oneline -1 f39f25c0 and git log --oneline -1 e2e46b59 )

f39f25c [WIP] * Changed to less (left .css file)
e2e46b5 [WIP] * Changed watch configuration

Now I know, that I have to commit both hunks individually mentioning individual commits.

Solutions

Yes, I read the git-blame man page, but I did not found a suitable option that does this out of box (I'm sure that I might have missed something).

Yes, I've also tried to search the internet for this. But, huh, it took me one page to describe it - how should I formulate the golden Google search expression to find this?

Yes, as you'll see in the next section, I have my own ideas.

My approach

I would create a script that will parse the patches generated by git log -p or git diff. It would output all line numbers that a patch affects using the rules in the "What I want to know" section. Then a bash script looks up the commit IDs by examining the git blame HEAD[~1] output, extracting the commit ID. The last step is to get the commit messages. Another step could be, to decorate the whole patch output (of multiple chunks and files) with the commit messages just found.

(Actually at the moment I'm writing a Python script that does the patch parsing)

Alternative approach

Maybe this can be accomplished by extracting the commit column from git blame -- <FILE> and git blame HEAD -- <FILE> and feeding both outputs into diff, then grep -C <N>ing for 00000000, etc. to find the commit IDs that are affected.

But I think this is fragile and not as reliable as the above method. And I need to parse the patch of the blame's diff either.

(I've tried it already. The hardest part is the diff output parsing.)

Try to reduce it to one question.

Is there a standard Git tool that combines the power of git blame with the data provided by git log by showing the commits that where changed by applying a commit?

I hope my question is clear enough. :)

Be secific.

I'm sure, I am.

1

There are 1 best solutions below

2
On

To help with the latter part, you should really look into git rebase -i --autosquash. Autosquash rebase will look for specially formatted commits to automatically reorder your commits when you rebase and mark them for squash/fixup. From git help rebase:

   --autosquash, --no-autosquash
       When the commit log message begins with "squash! ..." (or "fixup!
       ..."), and there is a commit whose title begins with the same ...,
       automatically modify the todo list of rebase -i so that the commit
       marked for squashing comes right after the commit to be modified,
       and change the action of the moved commit from pick to squash (or
       fixup).

       This option is only valid when the --interactive option is used.

       If the --autosquash option is enabled by default using the
       configuration variable rebase.autosquash, this option can be used
       to override and disable this setting.

As for automatically looking up the last changes, doing it automatically will always be fragile.

Given the following history:

[WIP] Fixed sloppy change     (A)
[WIP] Intermediate change     (B)
[WIP] Sloppy change           (C)

If you had an intermediate commit (B) that modifies the same line as (A) and (C) but you don't want your commit (A) to be merged with (B) but with the older (C) instead, you could very easily accidentally squash the wrong commits.

Also often times the commit you want to merge to would have no conflicting edits with your current changes so no amount of magic can make a script prophesize what you wanted to do. Scripting this part is, therefore, of marginal value, instead I find that using a GUI-based history browsers aid this step tremendously (I used gitg on Linux and gitx on Mac but any history browsers would do).