Here’s What Tactical Git Is

The author of Dependency Injection in .NET and Code That Fits in Your Head talks about his approach to Git and git stash, which allows for great flexibility in working with code. We share the experience of Mark Siman before the start C# development course.


In the film Free Solo, rock climber Alex Honnold practiced free climbing to conquer Mount El Capitan, in Yosemite. It’s a good movie, but in case you haven’t seen it, free solo climbing is when you climb a rock without protective gear, harness or ropes. Here is El Capitan, just to give you an idea, it is 914 meters of sheer cliff:

If you lose your grip and fall, you will die. Free climbing is an incredible effort, but Honnold conquers rock with one move at a time; after all, this article is about working with Git.

Preservation

Honnold did not just climb El Capitan freely. For this, he deliberately trained. The documentary shows him climbing El Capitan many times in protective gear, planning a route and climbing it several times.

On every ascent, he uses ropes, harnesses and various rope attachments. Honnold doesn’t fall far: the rope, harness and harness stop the fall at the last fixation point, almost like a game save.

By heavily changing the code, even when moving to a new implementation, you can create savepoints to avoid disaster. Like Alex Honnold, you can fix the code to have a better chance of getting to the next successful build.

Custom Editing

When you edit code, you move from one operating state to another, but while you edit, the code doesn’t always run or compile. Consider this interface:

public interface IReservationsRepository
{
    Task Create(Reservation reservation);
 
    Task<IReadOnlyCollection<Reservation>> ReadReservations(
        DateTime dateTime);
 
    Task<Reservation?> ReadReservation(Guid id);
 
    Task Update(Reservation reservation);
 
    Task Delete(Guid id);
}

This code, like most of the code in this article, is from my book. Code That Fits in Your Head. As I describe in the section on pattern Strangler Fig, at some point I had to add a new method to the interface. It should have been overloaded by the ReadReservations method with this signature:

Task<IReadOnlyCollection<Reservation>> ReadReservations(DateTime min, DateTime max);

However, as soon as you start typing this method definition, the code will stop working:

Task<IReadOnlyCollection<Reservation>> ReadReservations(
    DateTime dateTime);
 
T
 
Task<Reservation?> ReadReservation(Guid id);

If you’re working in Visual Studio, the editor will immediately underline the code with red squiggly lines to indicate parsing failure.

All method declarations must be entered to make all the squiggly lines disappear, but even then the code will not compile. An interface definition may be syntactically correct, but adding a new method will break other code. The codebase contains classes that implement the IReservationsRepository interface, but none of these classes define the newly added method. The compiler knows about this and complains:

Error CS0535 ‘SqlReservationsRepository’ does not implement interface member ‘IReservationsRepository.ReadReservations(DateTime, DateTime)’

There is nothing wrong. I’m just emphasizing that code editing involves transitioning between two work states:

In the film, the climb is dangerous, but there is a particularly dangerous maneuver that Alex Honnold must make because he cannot find a safe route.

Most of the time he ascends by safe methods, moving from position to position in short movements and never losing traction when the center of gravity shifts: it is safer.

Microcommits

You can’t edit code without breaking it, but you can take small, deliberate steps to commit changes to Git every time the code compiles and tests pass.

Tim Oettinger [работавший в консалтинговой компании Роберта С. Мартина Object Mentor] calls it microcommits. Not only do you have to commit all changes every time tests and compiles pass, but you consciously have to move forward so that the distance between two commits is as short as possible.

If you can think of alternative code changes, choose the path with the smallest steps: why make dangerous jumps when you can advance in small, controlled movements?

Git is an amazingly flexible tool. Most people don’t think of him that way. They start programming, and can only commit changes hours later in order to push a branch to a remote repository. Tim Oettinger doesn’t do that, and neither do I. I work tactically with Git, and I’ll tell you how.

Adding a method to an interface

As I said above, I wanted to add a ReadReservations overload to the IReservationsRepository interface. The reason for wanting is covered in Code That Fits Your Head, but that’s not the point, it’s about using Git in small increments.

By adding a method to an existing interface, you break compilation of the codebase as long as there are classes that implement that interface. How to deal with this? Is it just to move forward implementing a new method, or are there other approaches? The alternative is to advance in smaller steps.

Rely on the compiler, as stated in Working Effectively with Legacy Code. Compiler errors will point to classes that don’t have a new method; in the example codebase, these are SqlReservationRepository and FakeDatabase.

Open these files, copy the declaration of the ReadReservations method to your clipboard, and stash the changes:

$ git stash

Saved working directory and index state WIP on tactical-git: [...]

The code is working again. Find the right place to add a method to one of the classes that implement the interface.

SQL Implementation

I’ll start with the SqlReservationsRepository class. Moving to the line where I want to add a new method, I insert its declaration:

Task<IReadOnlyCollection<Reservation>> ReadReservations(DateTime min, DateTime max);

The code doesn’t compile because the method ends with a semicolon and doesn’t have a body. I make the method public, remove the semicolon, and add curly braces:

public Task<IReadOnlyCollection<Reservation>> ReadReservations(DateTime min, DateTime max)
{

}

The code still doesn’t compile: the declaration promises to return a value, but the method doesn’t have a body. How to quickly come to a working system?

public Task<IReadOnlyCollection<Reservation>> ReadReservations(DateTime min, DateTime max)
{
    throw new NotImplementedException();
}

You may not want to commit code in Git that throws NoImplementedException, but the new method does not calling code. All tests pass and the code compiles because it hasn’t changed. Let’s commit the changes:

$ git add . && git commit
[tactical-git 085e3ea] Add ReadReservations overload to SQL repo
 1 file changed, 5 insertions(+)

This is a save point. Saving progress allows you to return when something else appears. You don’t need to send the code anywhere, and if NoImplementedException makes you feel uncomfortable, take comfort in the fact that this exception only exists on your hard drive.

The transition from the old working state to the new took less than a minute. Naturally, the next step is to implement a new method. You can consider these increments by using TDD along the way and committing changes after successful compilation and testing, and refactoring, assuming following the refactoring checklist red-green.

I won’t do it here because I’m trying to store a SqlReservationsRepository like humble object. The implementation will have cyclomatic complexityequal to 2. Given the complexity of writing and maintaining a database integration test, I think this is low enough to justify the test being dropped. But, if you don’t agree, there’s nothing stopping you from adding tests at this stage.

public async Task<IReadOnlyCollection<Reservation>> ReadReservations(DateTime min, DateTime max)
{
    const string readByRangeSql = @"
        SELECT [PublicId], [Date], [Name], [Email], [Quantity]
        FROM [dbo].[Reservations]
        WHERE @Min <= [Date] AND [Date] <= @Max";
 
    var result = new List<Reservation>();
 
    using var conn = new SqlConnection(ConnectionString);
    using var cmd = new SqlCommand(readByRangeSql, conn);
    cmd.Parameters.AddWithValue("@Min", min);
    cmd.Parameters.AddWithValue("@Max", max);
 
    await conn.OpenAsync().ConfigureAwait(false);
    using var rdr = await cmd.ExecuteReaderAsync().ConfigureAwait(false);
    while (await rdr.ReadAsync().ConfigureAwait(false))
        result.Add(
            new Reservation(
                (Guid)rdr["PublicId"],
                (DateTime)rdr["Date"],
                new Email((string)rdr["Email"]),
                new Name((string)rdr["Name"]),
                (int)rdr["Quantity"]));
 
    return result.AsReadOnly();
}

Of course, this takes more than a minute, but if you’ve done this sort of thing before, it’ll probably take less time, especially if you’ve previously obtained the result of a SELECT, perhaps by experimenting with the query editor. The code compiles again, all tests pass. Fixing the changes:

$ git add . && git commit
[tactical-git 6f1e07e] Implement ReadReservations overload in SQL repo
 1 file changed, 25 insertions(+), 2 deletions(-)

We have two commits and all the code works, and it didn’t take long to code between commits.

Stub Implementation

Another class that implements IReservationsRepository is called FakeDatabase. This is plug, a kind of understudy, only to support automated testing. The new method is implemented in exactly the same way as in SqlReservationsRepository.

First add this method:

public Task<IReadOnlyCollection<Reservation>> ReadReservations(DateTime min, DateTime max)
{
    throw new NotImplementedException();
}

The code compiles, all tests pass. Fixing the changes:

$ git add . && git commit
[tactical-git c5d3fba] Add ReadReservations overload to FakeDatabase
 1 file changed, 5 insertions(+)

Let’s add an implementation:

public Task<IReadOnlyCollection<Reservation>> ReadReservations(DateTime min, DateTime max)
{
    return Task.FromResult<IReadOnlyCollection<Reservation>>(
        this.Where(r => min <= r.At && r.At <= max).ToList());
}

And again the code compiles, the tests pass:

$ git add . && git commit
[tactical-git e258575] Implement FakeDatabase.ReadReservations overload
 1 file changed, 2 insertions(+), 1 deletion(-)

Each of these commits only takes a few minutes; that’s the whole point. By committing frequently, you leave savepoints; if something goes wrong, you can retreat to them.

Let’s change the interface

Keep in mind that methods are added pending changes to the IReservationsRepository interface, but the interface itself hasn’t changed yet: I’ve hidden its change. Now the new method is used wherever it should be, i.e. in SqlReservationsRepository and in FakeDatabase.

Revert hidden changes:

$ git stash pop
On branch tactical-git
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   Restaurant.RestApi/IReservationsRepository.cs

no changes added to commit (use "git add" and/or "git commit -a")
Dropped refs/stash@{0} (4703ba9e2bca72aeafa11f859577b478ff406ff9)

The code re-adds an overload of the ReadReservations method to the interface. When I first tried to add it, the code didn’t compile: the classes that implement the interface didn’t have this method. In other words, the code compiles right away and all tests pass.

Fixing the changes:

$ git add . && git commit
[tactical-git de440df] Add ReadReservations overload to repo interface
 1 file changed, 2 insertions(+)

That’s all. Using git stash tactically, we’ve broken down the long maneuver into five safer steps.

Tactical Git

Someone once mentioned in passing that you should never move more than five minutes away from a commit. Same idea here. When you start editing the code, do yourself a favor by moving so that you get to a new working state in five minutes.

This does not mean that a commit needs to be made. every five minutes. It’s okay to have time to think. Sometimes, to allow myself to think about a problem, I go for a run or go shopping. Sometimes I just sit and look at the code, or I start editing without a good plan, and that’s okay too… When I work with code, inspiration often comes to me. And then the code can be inconsistent.

Maybe it compiles; or not. Everything is fine: you can always return to the last save point. Often I dump work, hiding the results of raw experiments. So I don’t throw away anything that could become valuable, but I start with a clean slate. The git stash command is probably the one I use most often to increase agility; secondly, the ability to move locally between branches is useful.

Sometimes I do a quick, dirty prototype on one branch, and when I feel like I understand the right direction, I commit to that branch, git reset the work to a more appropriate commit, create a new branch and do the work again, but now with tests or something else.

The ability to hide changes is great when you find that the code you’re writing right now needs something else, like a helper method that isn’t there yet. Stash the changes in stash, add what you just learned about, commit it, and revert the stashed changes. An example is in subsection 11.1.3 Separate refactoring of test and production code in Code That Fits in Your Head.

Often I use git rebase. I am not a supporter associations commits, but have no qualms about reordering the commits in my local Git branches. As long as I don’t share commits with the world, rewriting the commit history can be helpful.

Git allows you to experiment, try one direction and abandon it if it starts to feel like a dead end. Just save or commit your changes, go back to the previous savepoint and try the alternate direction.

Keep in mind that you can leave as many unfinished branches as you want on your hard drive. You don’t have to send them anywhere. This is what I call tactical use of Git. Maneuvers performed to become more productive in small things. Artifacts from these movements remain on your local hard drive unless you choose to share them.

Conclusion

Git is a tool with more potential than most people think. Typically, programmers only want to sync their work with other people when they feel the need to git push and git pull. While this is a useful and important feature of Git, if that’s all you’re doing, then you can use a centralized version control system.

The value of Git is in the tactical advantage. You can experiment, make mistakes, thrash around and strain on your computer, and reset your work with git reset at any time if things get too complicated.

You saw an example of adding an interface method, only to realize that it takes more work than you might think. Instead of pushing through a poorly planned, unsafe maneuver without a clear conclusion, simply back off, hide the changes, move in small steps, and finally bring back the hidden changes.

Just like a rock climber trains with ropes and harnesses, Git lets you move in small steps with fallbacks. Use this to your advantage.

And we will help to pump skills or from the very beginning to master a profession that is in demand at any time:

Choose another in-demand profession.

Brief catalog of courses and professions

Data Science and Machine Learning

Python, web development

Mobile development

Java and C#

From basics to depth

As well as

Similar Posts

Leave a Reply Cancel reply