Selectively Merging Changes from a Git Commit

Consider the following situation: you’re the maintainer of an awesome open-source project, and you have the following file hello.py somewhere in your codebase:

def main():
    do_some_stuff()
    do_some_other_stuff()
    # ...
    return 0

if __name__ == "__main__":
    main()

Alice comes along and decides she wants to contribute. She creates a pull request where she’s added a two features. Alice’s commit makes your code look a little something like this:

def main():
    do_some_stuff()
    do_some_other_stuff()

    # ...

    # Alice's feature 1
    if something:
        foo()
    else:
        bar()

    # Alice's feature 2
    while something:
        print(foo())

    return 0

if __name__ == "__main__":
    main()

You think feature one is awesome, but feature two isn’t quite as great. So, you decide that you want to merge in feature one, but omit feature two. How do you do this?

The Solution

What we want to do here is selectively merge in different changes from a Git commit. To my surprise, this wasn’t something that was super easy to find online. It was difficult trying to articulate the problem to find suitable results on Google. Turns out, though, the solution is super simple!

Here’s the command that you need:

git checkout --patch

Let’s say Alice has made her changes on the branch develop in commit 414dba.... You want to merge these changes to the tip of your master branch. You would run:

git checkout --patch 414dba...

This will bring up a nice interactive interface, like this:

diff --git b/hello.py a/hello.py
index ec3c25b..6e894dd 100644
--- b/hello.py
+++ a/hello.py
@@ -1,7 +1,19 @@
 def main():
     do_some_stuff()
     do_some_other_stuff()
+
     # ...
+
+    # Alice's feature 1
+    if something:
+        foo()
+    else:
+        bar()
+
+    # Alice's feature 2
+    while something:
+        print(foo())
+
     return 0
 
 if __name__ == "__main__":
Apply this hunk to index and worktree [y,n,q,a,d,/,s,e,?]? 

This case is easy since we only have one hunk to deal with. Now, Git is pretty much asking us ‘what do you want me to do with these changes?’ Let’s see…

  • We can type ‘y’, which will apply the entire hunk. In this case, we don’t want to do this; we want to keep feature one and scrap feature two.
  • We can type ‘n’, which will won’t apply the hunk at all. In this case, we don’t want to do this; we need to keep feature one.

In this case, we want to hit ‘e’. This brings up a text edit where we can manually edit the hunk. We will be greeted with this:

# Manual hunk edit mode -- see bottom for a quick guide.
@@ -1,7 +1,19 @@
 def main():
     do_some_stuff()
     do_some_other_stuff()
+
     # ...
+
+    # Alice's feature 1
+    if something:
+        foo()
+    else:
+        bar()
+
+    # Alice's feature 2
+    while something:
+        print(foo())
+
     return 0
 
 if __name__ == "__main__":
# ---
# To remove '-' lines, make them ' ' lines (context).
# To remove '+' lines, delete them.
# Lines starting with # will be removed.
# 
# If the patch applies cleanly, the edited hunk will immediately be
# marked for applying.
# If it does not apply cleanly, you will be given an opportunity to
# edit again.  If all lines of the hunk are removed, then the edit is
# aborted and the hunk is left unchanged.

Following the guide at the bottom, all we need to do is delete the feature two lines that we don’t want, leaving us with:

# Manual hunk edit mode -- see bottom for a quick guide.
@@ -1,7 +1,19 @@
 def main():
     do_some_stuff()
     do_some_other_stuff()
+
     # ...
+
+    # Alice's feature 1
+    if something:
+        foo()
+    else:
+        bar()
+
     return 0
 
 if __name__ == "__main__":
# ---
# To remove '-' lines, make them ' ' lines (context).
# To remove '+' lines, delete them.
# Lines starting with # will be removed.
# 
# If the patch applies cleanly, the edited hunk will immediately be
# marked for applying.
# If it does not apply cleanly, you will be given an opportunity to
# edit again.  If all lines of the hunk are removed, then the edit is
# aborted and the hunk is left unchanged.

If we save and quit, Git will progress onto the next hunk and allow us to make a similar choice. In this case, we only have the one hunk.

All that’s left to do is commit the changes that Git has applied for us, and you’re done!

This toy example is only to show you what is possible using this command. It might not seem immediately useful. However, in the real world, commits tend to be more complex, and if you want to selectively extract changes from a commit before you merge it, this is a great way to do it.

Written on August 14, 2018