As mentioned earlier, Git stash isn’t ideal for when you’re switching branches. A better use case for Git stash is breaking down commits.

There is a lot written already about so-called “commit hygiene”, and there are many opinions about it.
It can be really beneficial if each commit tells its own story.
Each commit makes one functional change at a time, preferably accompanied with a well-written commit message.
In a workflow where you have smaller commits, it is easier for code reviews to go through the commits one by one and understand the story step by step.
Whenever it’s needed, it also enables you to revert a smaller set of changes.

Imagine you have a Go project, and this is an example of what you’ve started with:

package main

import "fmt"

func Greet() {
	fmt.Print("Hello world!")
}

func main() {
	Greet()
}

This piece of code prints “Hello world!” when you run it.
For various reasons, you need to refactor this. After making a bunch of changes, you end up with:

package main

import (
	"fmt"
	"io"
	"os"
	"time"
)

var now = time.Now

func Format(whom string) string {
	greeting := "Hello"

	if h := now().Hour(); 6 < h && h < 12 {
		greeting = "Good morning"
	}

	return fmt.Sprintf("%v %v!", greeting, whom)
}

func Greet(w io.Writer) {
	fmt.Fprint(w, Format("world"))
}

func main() {
	Greet(os.Stdout)
}

The main functionality remains the same, but there are a few feature changes:

  • You can specify whom to greet.
  • You can specify where to write the greeting.
  • The greeting will differ depending on the time of day.

In this scenario, we like to commit each of these functional changes separately.
In many situations, you would be able to use git add -p to stage small hunks of code at once, but in this case, the changes are too intertwined.
And this is where git stash comes in real handy.
In the next steps below, we’ll use it as a backup, where we save the end result in a stash, apply it, and then undo the changes we don’t need for the current functional change.
Because the end result is stored in a stash, we can repeat this process for each commit we want to make.

Let’s have a look:

git stash push --include-untracked

This saves all your local changes in a stash, and you can start breaking down changes into separate commits.
The option --include-untracked will also include files that were never committed, which is useful if you’ve added new files.

Now we can start working on the first commit.
Type git stash apply to bring the changes from the stash back into your local working tree:

$ git stash apply
On branch main
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:   main.go

no changes added to commit (use "git add" and/or "git commit -a")

Open main.go in your favorite editor, and modify it so it includes the changes to add whom.
This might look something like:

package main

import (
	"fmt"
)

func Greet(whom string) string {
	return fmt.Sprintf("Hello %v!", whom)
}

func main() {
	fmt.Print(Greet("world"))
}

In this process, you can throw away all unwanted changes because the end result is safely stored away in a stash.
This means you can adapt the code so it properly compiles and ensures the tests pass with these changes.
When you’re happy, these changes can be committed as usual:

git add .
git commit -m "allow caller to specify whom to greet"

We can repeat these steps for the next commit.
Type git stash apply to get started.
Unfortunately, this might give you conflicts:

$ git stash apply
Auto-merging main.go
CONFLICT (content): Merge conflict in main.go
Recorded preimage for 'main.go'
On branch main
Unmerged paths:
  (use "git restore --staged <file>..." to unstage)
  (use "git add <file>..." to mark resolution)
	both modified:   main.go

no changes added to commit (use "git add" and/or "git commit -a")

Resolving conflicts is outside the scope of this article, but there is a quick way to restore the changes from the stash that may work well in such cases:

git restore --theirs .
git restore --staged .

Let’s look at what that’s doing. The git restore --theirs command will tell Git to resolve the conflict by taking all changes from theirs.
In this case, theirs is the stash, which will apply the changes from there.
The git restore --staged . command will unstage these changes, meaning they are no longer added to the index and are omitted the next time you type git commit.

Now you can start hacking on the code again, and eventually you might end up with something like:

package main

import (
	"fmt"
	"io"
	"os"
)

func Greet(w io.Writer, whom string) {
	fmt.Fprintf(w, "Hello %v!", whom)
}

func main() {
	Greet(os.Stdout, "world")
}

Here you can repeat the usual commands to write another commit:

git add .
git commit -m "allow caller to specify where to write the greeting to"

For the final commit, simply run:

git stash apply
git checkout --theirs .
git reset HEAD
git add .
git commit -m "use different greeting in the morning"

And you’re done! You end up with a history of three commits added, and each commit adds one feature change at a time.

Similar Posts