loading...

Slice and dice your commits with these git flags

cdanielsen profile image Christian Danielsen ・6 min read

Ah git. The canonical example of "easy to learn, acquire a PhD to master". Perhaps someday I'll learn about the subtle mysteries of git bisect or git rev-parse (wut?) but for now, here are some practical (and simple!) commands you can use every day:

git add --pick

You always only work on one thing at a time in between commits, right? Uh, yeah, me too! 😁 For those rare times you don't, you can use this command to only add selected parts of a changed file. git adorably calls these selections "hunks" 💪

Let's see what this would look like! Here's a repo with just one file that I've made changes to, with the changes displayed via git diff:

$ git diff
diff --git a/sweet-sweet-file.js b/sweet-sweet-file.js
index 85db7f4..e3752aa 100644
--- a/sweet-sweet-file.js
+++ b/sweet-sweet-file.js
@@ -1,3 +1,7 @@
// sweet-sweet-file.js

+export const relevantFunction = () => 'I am so relevant to this commit';
+
 export const initialFunction = () => 'Ahoy!';
+
+export const notReleventFunction = () => 'I am so related to something else';

Here the + signs indicate lines in the file that are additions that haven't been added to the staging area yet (- signs would indicate removed lines, but we don't have any).

I'm ready to commit relevantFunction, but got distracted and also fleshed out notRelevantFunction, so I don't want to just git add this file in its entirety. Enter git add --pick (or git add -p for short), which presents me with a command line interface for picking which parts of the file I want to add to the staging area:

$ git add -p
diff --git a/sweet-sweet-file.js b/sweet-sweet-file.js
index 85db7f4..e3752aa 100644
--- a/sweet-sweet-file.js
+++ b/sweet-sweet-file.js
@@ -1,3 +1,7 @@
 // sweet-sweet-file.js

+export const relevantFunction = () => 'I am so relevant to this commit';
+
 export const initialFunction = () => 'Ahoy!';
+
+export const notReleventFunction = () => 'I am so related to something else';
Stage this hunk [y,n,q,a,d,s,e,?]?

Ooo, cryptic one letter options! You can git get more detail by entering ?

Stage this hunk [y,n,q,a,d,s,e,?]? ?
y - stage this hunk
n - do not stage this hunk
q - quit; do not stage this hunk or any of the remaining ones
a - stage this hunk and all later hunks in the file
d - do not stage this hunk or any of the later hunks in the file
s - split the current hunk into smaller hunks
e - manually edit the current hunk
? - print help
@@ -1,3 +1,7 @@
 // sweet-sweet-file.js

+export const relevantFunction = () => 'I am so relevant to this commit';
+
 export const initialFunction = () => 'Ahoy!';
+
+export const notReleventFunction = () => 'I am so related to something else';
Stage this hunk [y,n,q,a,d,s,e,?]?

Now we're talking! Git will work its way through the file and identify "hunks" of changes, asking you what you want to do with each one. y will add the hunk, n will skip it, and q will get you out of here.

There are some additional interesting options, particularly s, which allows you to get really surgical and split up hunks into smaller pieces! This is exactly what we want here, as we only want relevantFunction. Let's do it:

Stage this hunk [y,n,q,a,d,s,e,?]? s
Split into 2 hunks.
@@ -1,3 +1,5 @@
 // sweet-sweet-file.js

+export const relevantFunction = () => 'I am so relevant to this commit';
+
 export const initialFunction = () => 'Ahoy!';
Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]?

Hooray! Now we can hit y to just add this part of the file that we want, and then n to discard the second hunk with notRelevantFunction in it, returning us to the command line as we've reached the end of the file:

Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]? y
@@ -3 +5,3 @@
 export const initialFunction = () => 'Ahoy!';
+
+export const notReleventFunction = () => 'I am so related to something else';
Stage this hunk [y,n,q,a,d,K,g,/,e,?]? n

$

Let's see where we stand with git status:

$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        modified:   sweet-sweet-file.js

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

        modified:   sweet-sweet-file.js

The first time I used this command, it blew my mind a bit: sweet-sweet-file.js is both staged... and not-staged! 🤯 Once you get past the idea of committing files and instead committing changes (which git is perfectly able and willing to do) this feels totally normal (and awesome!).

git diff --staged

Now that we've partially committed a file, how can we confirm we did it right? Let's try git diff:

$ git diff
diff --git a/sweet-sweet-file.js b/sweet-sweet-file.js
index c48b8c3..e3752aa 100644
--- a/sweet-sweet-file.js
+++ b/sweet-sweet-file.js
@@ -3,3 +3,5 @@
 export const relevantFunction = () => 'I am so relevant to this commit';

 export const initialFunction = () => 'Ahoy!';
+
+export const notReleventFunction = () => 'I am so related to something else';

Probably what we should expect: git is telling us that notRelevantFunction is an un-added addition to sweet-sweet-file.js. With no options, git diff will just look at what's different between the file's existing state and what's staged and/or committed. If you just want to see the difference between what's staged and what's committed, you can add the --staged flag (--cached also works!):

$ git diff --staged
diff --git a/sweet-sweet-file.js b/sweet-sweet-file.js
index 85db7f4..c48b8c3 100644
--- a/sweet-sweet-file.js
+++ b/sweet-sweet-file.js
@@ -1,3 +1,5 @@
 // sweet-sweet-file.js

+export const relevantFunction = () => 'I am so relevant to this commit';
+
 export const initialFunction = () => 'Ahoy!';

There we go -- confirmation that committing what's in the staging area will just commit relevantFunction 🎉

git checkout --pick

Sometimes instead of affirmatively adding hunks, it's easier to just subtract a few from an otherwise solid set of changes to a file.

You can use git checkout <file> to reset a file to it's last committed state, but if you only want to remove a few things, --pick works for git checkout too! Think of it as basically the opposite of git add --pick. Here we've made another set of changes:

$ git diff
diff --git a/sweet-sweet-file.js b/sweet-sweet-file.js
index c48b8c3..cb9c9c7 100644
--- a/sweet-sweet-file.js
+++ b/sweet-sweet-file.js
@@ -1,5 +1,9 @@
 // sweet-sweet-file.js

+export const no = () => 'This is something experimental that should go away';
+
 export const relevantFunction = () => 'I am so relevant to this commit';

 export const initialFunction = () => 'Ahoy!';
+
+export const anEvenBetterFunction = () => 'This function is so good';

Let's rid ourselves of the no function, so we can just have the changes we want and commit the entire file:

$ git checkout --pick
diff --git a/sweet-sweet-file.js b/sweet-sweet-file.js
index c48b8c3..cb9c9c7 100644
--- a/sweet-sweet-file.js
+++ b/sweet-sweet-file.js
@@ -1,5 +1,9 @@
 // sweet-sweet-file.js

+export const no = () => 'This is something experimental that should go away';
+
 export const relevantFunction = () => 'I am so relevant to this commit';

 export const initialFunction = () => 'Ahoy!';
+
+export const anEvenBetterFunction = () => 'This function is so good';
Discard this hunk from worktree [y,n,q,a,d,s,e,?]?

Notice the new command line prompt asking if you want to Discard this hunk from the "worktree" (your unstaged/uncommitted changes). Like before, we need to split the hunk in two:

Discard this hunk from worktree [y,n,q,a,d,s,e,?]? s
Split into 2 hunks.
@@ -1,5 +1,7 @@
 // sweet-sweet-file.js

+export const no = () => 'This is something experimental that should go away';
+
 export const relevantFunction = () => 'I am so relevant to this commit';

 export const initialFunction = () => 'Ahoy!';
Discard this hunk from worktree [y,n,q,a,d,j,J,g,/,e,?]?

Then rid ourselves of the hunk we want to discard by entering y:

Discard this hunk from worktree [y,n,q,a,d,j,J,g,/,e,?]? y
@@ -3,3 +5,5 @@
 export const relevantFunction = () => 'I am so relevant to this commit';

 export const initialFunction = () => 'Ahoy!';
+
+export const anEvenBetterFunction = () => 'This function is so good';
Discard this hunk from worktree [y,n,q,a,d,K,g,/,e,?]?

And keeping the one we want (i.e. don't discard) with n, returning us to the command line. Now we can use plain old git diff to see that only the changes we want are ready to be git add'ed:

λ git diff
diff --git a/sweet-sweet-file.js b/sweet-sweet-file.js
index c48b8c3..cb8545c 100644
--- a/sweet-sweet-file.js
+++ b/sweet-sweet-file.js
@@ -3,3 +3,5 @@
 export const relevantFunction = () => 'I am so relevant to this commit';

 export const initialFunction = () => 'Ahoy!';
+
+export const anEvenBetterFunction = () => 'This function is so good';

Conclusion

Adding some options to git add, git checkout and git diff allows you to commit more surgically within files, and make sure you did it right! Are there other options for fine grain commit polishing? Let me know in the comments!

Posted on by:

cdanielsen profile

Christian Danielsen

@cdanielsen

I sling JavaScript; Dog named after Walter Cronkite

Discussion

pic
Editor guide