I am currently working on a Django project, where I am making major changes to the database scheme. Last night, I realized my fear of commitments!
My project uses Git for version control, and after I create new migrations (which in the Django world is a change of database fields and structure), I often hesitate to commit because of the fear that I would commit something wrong, a breaking change. And I hate breaking changes!
Ultimately, this is a post about two major steps that I learned during my journey: Undoing a Git commit and undoing a migration in Django.
Undo a Git commit
There is a whole branch of science with lots of “researchers” that explore how Git commits are created and how they can be avoided. Sometimes it happens: You are enjoying your coding session and working on some cool new feature or some sensible refactorings, when suddenly… the muscle memory kicks in. For me, it’s
gitc . and the damage is done. I even take time to set a nice commit message, and then I realize it. Damn, I forgot to run the tests (in case you have tests), or forgot to recompile, or an exception was just thrown from the lines you just edited.
Hopefully, when you realize it, you have not yet pushed your changes. Or created more commits. Try this if you f***ed up. If not, stay away from the keyboard. Do not escalate the problem. Do not touch anything. Analyze the current situation: You just committed something to your local repo. It was bad, shameful and it’s just luck that you noticed. Why did you even introduce an alias for
git commit? You realize how inconsiderate that was. That has happened so often. You have already been there. You should know what to do. Well, you don’t, but you know there is this one StackOverflow answer with lots of upvotes that everyone gets when they desperately search for “undo git commit”.
git reset HEAD~
Phew… that was close. Now it looks like you never created that commit. You saved the day! Fix the stupid bug that made your program crash. Remove that filthy problem that you introduced and did not test. Everything is alright. Your hands type
gitc . again. You can not control it, but now you feel safe. Except… your old, wonderfully hand-crafted commit message is gone. What was it again? You do not remember, do you? It was so precise, so describing and still concise. That is also not the first time this happened. There was a way to recover the old message, wasn’t there? Abort the commit now! You go back to the one and only StackOverflow answer. It was there, you know it…
git commit -c ORIG_HEAD
Wonderful, isn’t it? Your commit message is back. You are happy. Your commit is a beautiful mixture of code, poetry and sweat. You write down the two commands that were your friends in this rough situation. You know you will need them again…
Undo a migration in Django
You are a careful coder. You have learned from previous mistakes in committing and applying changes too fast. You changed only one or two models of your application. You go for a dry run, which will not apply any changes, just show them:
./manage.py makemigrations --dry-run Migrations for 'myapp': myapp/migrations/0012_auto_20180204_1632.py - Alter field player on progress
Okay, that is exactly what you modified. So far, so good. Now it is time for the real deal:
Nothing suspicious happens. No warnings this time. You proceed with the decisive step: migrating. This means that a migration, a “change request” for your database, will be applied.
./manage.py migrate CommandError: Conflicting migrations detected [...]
Conflicts! You tried to avoid a conflict, but there it is. Is it even your fault? Is the database okay or did you just trash it? Were the changes applied or was there a rollback? So many questions and nobody gives you an answer. You were my best friend, and you betrayed me, Django…
In the panic of the moment, it can happen that you want to manually remove the migration file. Delete the file, problem is gone, right? Wrong. Deleting the file only makes it worse. Django can undo most migrations easily, but you need the files created by the
First, show all migrations for the conflicting app:
./manage.py showmigrations myapp myapp [X] 0001_initial [X] 0002_auto_20170924_1943 [X] 0003_auto_20170927_1056 [X] 0004_auto_20170928_1943 [X] 0005_auto_20170929_1943 [X] 0006_auto_20180201_1713 [X] 0007_auto_20180201_1809 [X] 0008_auto_20180203_2249 [X] 0009_auto_20180203_2251 [X] 0010_auto_20180204_0028 [X] 0011_auto_20180204_1422 [X] 0012_auto_20180204_1633
Migrations are numbered, and the last migration, 0012, is marked, which means it was applied. But you got an error, remember? So, we want to undo the last migration. For this, we need the previous number, 0011 for my case.
./manage.py migrate myapp 0011 Operations to perform: Target specific migration: 0011_auto_20180204_1422, from myapp Running migrations: Rendering model states... DONE Unapplying myapp.0012_auto_20180204_1633... OK
How cool is that? You just reversed your wrongdoings. Now you can remove the migration file “myapp/migrations/0012_auto_20180204_1633.py” and find out why your change in the model was conflicting. This is left as an exercise to the reader, and constitutes a nice example of how much fun it is to debug such hidden DB issues.