Ken Muse

Git Line Staging & Patch Editing


In last week’s post, we examined the Git command line to understand the built-in functionality. With Microsoft’s line-staging support in Visual Studio in preview, it’s good to understand how to use the native Git feature. Along the way, you’ll gain an understanding of why IDE support for this is so valuable.

The interactive command-line tool can provide a great way to selectively choose hunks of code to stage. That said, it can only split code when it sees unmodified code between sections of modifications. This makes it more challenging to selectively add portions of code in this way. Last week, we staged and committed a simple file:

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

And the working directory had a more complete file that was in progress:

 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}

Now, we want to commit just the Increment function. If we try to use git add -p Counter.cs, we see an issue:

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

The option to split (s) is not available. Because Git cannot see an unmodified element between Increment and Decrement, it cannot split the code into hunks in this mode. We have to use e (edit) to jump into the modification process and take control. Optionally, you can also use git add -e {file} to skip the interactive prompts and directly work with your editor. Now might be the right time to review the steps to configure your text editor in Git

The editor shows you the same Diff output as before, highlighting the parts of the file that have been changed:

 1diff --git a/Counter.cs b/Counter.cs
 2index cad5822..f17a9e8 100644
 3--- a/Counter.cs
 4+++ b/Counter.cs
 5@@ -1,8 +1,19 @@
 6 public class Counter
 7 {
 8     private int total = 0;
 9     
10     public Counter(int start)
11     {
12+        this.total = start;
13+    }
14+
15+    public int Increment()
16+    {
17+        return ++this.total;
18+    }
19+
20+    public int Decrement()
21+    {
22+         return --this.total;
23     }
24 }
25\ No newline at end of file

This view of the file has a VERY important difference that you need to understand to use this mode. If you look carefully, lines 6 to 24 all start with one of three characters:

  • A space ( ). This indicates an unmodified hnk of code, such as lines 6-11.
  • A plus (+). This indicates a change that will add code to the file, such as lines 12-22.
  • a minus (-). While not shown here, it indicates a line that is being removed or replaced.

In this view, there is always a leading character that is not part of the original file. This character is an instruction to Git that allows you to select how to handle applying the changes. At a minimum, Git needs to be able to map every existing line to one of these three states. If it can’t, the attempt to edit the patch will fail and you’ll have to start over.

To add new lines, start with a + character. Lines that are being added and which didn’t previously exist have another special characteristic — you can delete them without any problems. Git is adding the content, so it does not have to map this line to an existing line. To remove something from the change list, we simply delete the line here. To be clear, this does not impact the file in the working directory. This only impacts what will be staged. It’s a line-level staging process.

To remove lines use the - character. Removing a line implies that Git is aware the line already exists. To cancel a line being removed, replace the minus with a space character. This will instruct Git to leave the line unmodified. To remove an existing line, replace the space with a minus.

Updating a line is a combination of removing the old line and adding a new line. For example, to rename the class to Counter2, I might edit the Diff to look like this:

1@@ -1,8 +1,19 @@
2+public class Counter2
3-public class Counter
4 {

Notice that the opening bracket remains unmodified. The line containing the rename is modified to use a -, and a new line is added with a leading + that contains the change. Easy, right?

So, let’s look at how we modify the diff to only add the Increment function without modifying the constructor or adding other functions. First, we need to delete line 12 (the code inside the constructor), then lines 19-22 (which have the Decrement function).

What about Lines 13 and 23? Git has identified that as a possible change, and sees the unmodified closing brace as being associated with the Decrement function. Because the unmodified line has to be matched and accounted for, we have two options:

  1. We can leave line 13 alone and remove the closing brace (line 18) from Increment. Line 13 will close the constructor, and line 23 will now close the Increment function. Git will see line 23 as the unmodified line. This is the least work, but the most likely to introduce an error if you have many changes in a file. The result:

     1diff --git a/Counter.cs b/Counter.cs
     2index cad5822..f17a9e8 100644
     3--- a/Counter.cs
     4+++ b/Counter.cs
     5@@ -1,8 +1,19 @@
     6 public class Counter
     7 {
     8    private int total = 0;
     9
    10    public Counter(int start)
    11    {
    12+   }
    13+
    14+   public int Increment()
    15+   {
    16+       return ++this.total;
    17    }
    18 }
    19\ No newline at end of file
    
  2. Change line 13 to be unmodified by replacing the + with a space. This allows it to match the existing, unmodified line. Then, remove line 23 since it is no longer needed. The Increment function is now left intact. Personally, I chose this approach because it makes it easier to understand the intention of the changes, and its less likely to result in a mistake when I’m editing. Those modifications instead look like this:

     1diff --git a/Counter.cs b/Counter.cs
     2index cad5822..f17a9e8 100644
     3--- a/Counter.cs
     4+++ b/Counter.cs
     5@@ -1,8 +1,19 @@
     6 public class Counter
     7 {
     8    private int total = 0;
     9
    10    public Counter(int start)
    11    {
    12    }
    13+
    14+    public int Increment()
    15+    {
    16+        return ++this.total;
    17+    }
    18 }
    19\ No newline at end of file
    

Notice that in both cases, we leave the header lines at the top intact. We’re just editing the code that is being added/removed/updated. Save the file and close the window. If everything worked, the Increment function is now independently staged. If there were mistakes, you’ll see the following message:

error: patch failed: Counter.cs:1
error: Counter.cs: patch does not apply
fatal: Could not apply '.git/ADD_EDIT.patch'

The message indicates that there were one or more lines that Git couldn’t reconcile with the original unmodified lines. As a result, you’ll need to restart the editing process. And be a bit more careful this time around!

All of this brings us back to why support for this in Visual Studio 2022 will be a valuable enhancement. It eliminates the manual editing of the markers and ensures the rules are followed for identifying the original code. Instead of remembering the +, -, and characters, you can instead simply hover over the code and click Stage Change. At the moment, there are a few known issues, but that’s why this is still in preview.

Happy DevOp’ing!