Ken Muse

Git Line Staging


Yesterday, Microsoft mentioned a preview of line-staging(interactive staging) is available in Visual Studio 2022 (17.2 Preview 1 and later). This functionality has been requested in the community for a long time, so it’s great to see that moving forward! The latest VS 2002 preview is available here. At the same time, I’ve seen a few folks wanting to know … what is it? 🤔

Essentially, Visual Studio now understands one more feature of Git. The IDE now allows you to choose to stage (and commit_ a subset of changes within a file. You can selectively choose to commit portions of the code. This allows you to be more selective with your commit activities without having to risk losing any of the code you’ve been working on. The ability to do these kind of granular activities is one of the reasons why the staging (index) concept in Git is so powerful. The announcement does a great job of walking through the new feature for Visual Studio. To understand why this is such an important addition, I want to walk through how to use this capability using the Git command line.

If you’ve spent much time with Git, you’re likely already comfortable with invoking git add to stage a change. To be able to stage parts of a file, you just add --patch or -p to that command. This will allow you to use an interactive mode to select the portions of the file to be staged. When you do this, the original file remains in the working directory, but the “hunks” of code you chose will be staged and ready for commit.

For example, let’s start with a file that contains a single class and commit it to the repository:

1public class Counter
2{
3    int total = 0;
4    
5    public Counter()
6    {
7    }
8}

To demonstrate the line staging functionality, we’ll update the constructor and add some functions:

 1public class Counter
 2{
 3    private int total = 0;
 4    
 5    public Counter(int start)
 6    {
 7        this.total = start;
 8    }
 9
10    public int Increment()
11    {
12        return ++this.total;
13    }
14
15    public int Decrement()
16    {
17         return --this.total;
18    }
19}

That’s a lot of new functionality 😄. Let’s assume that we’ve decided we just want to commit the new signature on the constructor, but not the implementation or the two functions.

Run git -p Counter.cs, and you will then be prompted with something similar to the following:

 1@@ -2,7 +2,18 @@ public class Counter
 2 {
 3     private int total = 0;
 4
 5-    public Counter()
 6+    public Counter(int start)
 7     {
 8+        this.total = start;
 9+    }
10+
11+    public int Increment()
12+    {
13+        return ++this.total;
14+    }
15+
16+    public int Decrement()
17+    {
18+         return --this.total;
19     }
20 }
21\ No newline at end of file
22(1/1) Stage this hunk [y,n,q,a,d,s,e,?]?

At this point, you can use the interactive menu, starting with ? for help:

1y - stage this hunk
2n - do not stage this hunk
3q - quit; do not stage this hunk or any of the remaining ones
4a - stage this hunk and all later hunks in the file
5d - do not stage this hunk or any of the later hunks in the file
6s - split the current hunk into smaller hunks
7e - manually edit the current hunk
8? - print help

The process will step you through each “hunk” in the file. You can then choose y or n to approve staging that hunk. To get line-level selection, we use the s option. This splits the hunk into smaller blocks anywhere that two sets of changes are separated by unchanged code. In this case, the diff output shows that line 7 is considered “unchanged” (no + or -), so s will split the code into two hunks. We will now be able to step through those and approve each hunk individually. This first hunk contains this piece:

1Split into 2 hunks.
2@@ -2,5 +2,5 @@
3{
4    private int total = 0;
5
6-    public Counter()
7+    public Counter(int start)
8    {
9(1/2) Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]?

This is the hunk we want to stage, so press y. Git now proceeds to the next hunk and prompts us again:

 1@@ -6,3 +6,14 @@
 2     {
 3+        this.total = start;
 4+    }
 5+
 6+    public int Increment()
 7+    {
 8+        return ++this.total;
 9+    }
10+
11+    public int Decrement()
12+    {
13+         return --this.total;
14     }
15 }
16\ No newline at end of file
17(2/2) Stage this hunk [y,n,q,a,d,K,g,/,e,?]?

We want to wait to commit this hunk, so select n. Because there are no further hunks, the process completes and the code is staged. We can verify this with git diff --cached Counter.cs. The --cached command allows us to see the difference between the currently committed file and the staged content:

 1@@ -0,0 +1,8 @@
 2+public class Counter
 3+{
 4+    private int total = 0;
 5+
 6+    public Counter(int start)
 7+    {
 8+    }
 9+}
10\ No newline at end of file

Our working version of the file is unchanged and contains all of the lines of code, but only the signature for the new constructor has been staged. If we now run git commit, that constructor signature will be committed to the local repository, while the other changes remain safely in the working directory.

What if we need more control over the process – granular choice of the individual lines being added (or removed)? That can be accomplished with either git add -e or using the e option to open an editor to select the changes. That’s a post for a future time …

One special note – this functionality requires an additional step if you are working with a new, untracked file. Because new files don’t have existing content to reference, you will see No changes. if you attempt to run git add -p {file}. You have to make Git aware of the fact it needs to interact with a new file that will be added. You can use git add --intent-to-add {file} (or its shortcut git add -N {file}). Essentially, this adds the path to the index without any content. Once you’ve done that, everything will behave as expected.

Hope that helps to de-mystify things just a bit!