20160430

Revert 2016 edition

Five years ago I wrote about how reverting can speed you up. The main argument is that retrying to go from a new broken state to a new good state can be incredibly hard. The problem is that you did too many steps and you don't know which of the steps actually broke your system.

My experience is that it is hard for people to let go and try a new start. I always see more of a positive edge. You still know all the things you learnt in the process. Making smaller steps will give you predictable progress. Carrying on might lead to nothing. It is high risk.

I'm reverting more on the enjoyable work days. I did something I didn't know yet how it would work out. I learnt something: it doesn't work this way and the problem is a bit more challenging than I though initially. Failures are the most interesting bits of information. I remember my failures way more vividly than the stuff that actually worked. Digesting failures fully gives you lots of information and an opportunity to grow.

I don't like to arrive on a development battle field with corpses all over the place. There are twenty different changes you made from the last good state - all potentially breaking - and now I should find the one line change that broke it all. I want to see how we got from the pristine state of goodness to this. This is the safe approach. Every developer should be able to minimize the breaking change. It's fine to not know the solution for this minimal problem. This is where the investigation starts.

The easiest way to go from a broken state to a good state is to undo all your changes. Go to the known good state and make smaller steps from there.

With git this all became cheaper and safer. The main tools are git add and git stash.

If you're making progress and everything works like it should you can do git add -A between all good states to stage them. Using staged changes is a lightweight way to safe your work. It's the right tool if the change is not big enough that commit is worth it.

In example I'll use bash conditional expressions as a stand-in for a test suite. It tests the presents or absence of a file.

We start with an initial commit that we know works:
$ cd $(mktemp -d)
$ git init
Initialized empty Git repository
$ echo a > a
$ [ -f a ] && echo works || echo broken # run test
works
$ git add -A
$ git commit -m'working'
[master (root-commit) 263f908] working
 1 file changed, 1 insertion(+)
 create mode 100644 a
Now we can change the source, run our test and use add to stage the change.

$ echo -n b > a
$ [ "$(cat a)" == "b" ] && echo works || echo broken #run test
works
$ git add -A
Then we can do another change, find out that it was breaking the test and revert back by checking out the change we staged.

$ echo -n c > a
$ [ "$(cat a)" == "b" ] && echo works || echo broken #run test
broken
$ git checkout a
$ [ "$(cat a)" == "b" ] && echo works || echo broken #run test
works
We can repeat git add until we are ready to commit. This process adds minimum overhead.

Using git stash we make safe points on the way. This will help recover if something went wrong in the process, for example if we recovered to the wrong intermediate state.

The same session with some safe points added.
$ cd $(mktemp -d)
$ git init
$ echo a > a
$ [ -f a ] && echo works || echo broken # run test
$ git add -A
$ git commit -m'working'

$ echo -n b > a
$ [ "$(cat a)" == "b" ] && echo works || echo broken #run test
works
$ git add -A

$ echo -n c > a
$ [ "$(cat a)" == "b" ] && echo works || echo broken #run test
broken
$ git stash
Saved working directory and index state WIP on master: df9056f working
HEAD is now at df9056f working

$ # Oh, no the test was actually wrong!
$ [ "$(cat a)" == "c" ] && echo works || echo broken #run test
broken

$ git stash apply
On branch master
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:   a

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

$ [ "$(cat a)" == "c" ] && echo works || echo broken #run test
works
$ git add -A
$ git commit -m"working again"
[master 3faee04] working again
 1 file changed, 1 insertion(+), 1 deletion(-)
I'd advise to not use stash pop. Apply leaves the safe points in case you tried to applied the stash incorrectly. You can always recover from all your safe points starting from the last commit and you will never lose work. They can be safely discarded once you reached a git commit.

A process with more overhead but similar results is to use git add, git commit, git revert and git rebase -i. You use commit regularly between states and you revert bad states. Finally you squash with git rebase -i to have a clean history. Depending on your preference this process or stash is the better choice. Try both and you'll see which one is the appropriate for your situation.

Coming back to the old post about reverting, git stash is the cheapest way to clean a workspace. You can fire away git stash left and right. If you were wrong you can always go back and scavenge the bits of the changes that were important after all. Given these tools the overall process got massively better.

No comments:

Post a Comment