Once, a client asked me to revert a dbt project to the state it was in two weeks ago. Long story short, there were some changes to reporting that required us to go back in time.
It sounded easy enough. Although I hadn't done something to this scale, I had reverted, reset, and restored things before. What could have been a easy switch, however, ended up becoming a drawn-out ordeal - mainly because I didn't truly understand the role of git HEAD. Today, I'll walk through how I eventually solved the problem and share what I learned in the process.
Context
Two weeks prior, I had started a major refactoring process. It touched a lot of models and changed the logic of some marts tables, which were deliverables to stakeholders.
While the refactor was succesful, the client hadn't pointed all downstream processes to the new models yet. In the midst of this, the stakeholders wanted an immediate change to their reporting. Without going into the weeds, it was determined that it'd be easier to temporarily revert the project back to the state before the refactors and adjust a few lines of code to fulfill stakeholder requirements, rather than updating processes outside of dbt to point to the new models.
"I've done something like this before - I'll just do a checkout or reset"
And so I found the commit hash from two weeks ago using git log. Then, I checked out the commit git checkout <commit hash> and created a new branch from it git checkout -b <branch>. When I pushed the changes and attempted to merge to master, I was met with No commits to merge.
I changed my approach and executed a hard git reset on a copy of my development branch. What appeared in my directory looked good - everything had reverted to the pre-refactor state. I pushed my changes and tried to merge with master. Same issue: No commits to merge.
Where's my HEAD?
In both cases, the fundamental issue was with a detached/re-directed HEAD. A git HEAD is a reference to the current check-out commit in your repository.
With a normal, attached HEAD, the HEAD references a branch, which in turn references the latest commit on that branch. We can check this by exploring our .git folder in the order below. In this example, our HEAD is pointing at the master branch, which points to the latest commit on that branch.

On the contrary, a detached HEAD will no longer be tied to a branch and instead point directly to a commit hash.

With both a git reset --hard and a git checkout <commit hash>, the HEAD actually becomes detached or moves to another reference.
To illustrate, let's say we have the following commits on master and we want to revert to commit B:
A → B → C → D
-
With a
git reset --hard, the HEAD stays on your branch but moves its tip to B. Commits C and D are no longer in the branch's history. -
With a
git checkout <commit hash>, the HEAD becomes detached from the branch entirely and directly references commit B. We then need to create a new branch from the checkout to push (as at this point, what we checked out is branch-less), but the issue persists: the HEAD moved backwards.
In both cases, the HEAD moved backwards to B. With git reset , the HEAD points to the same branch but moves backwards. With git checkout , the HEAD points to a different branch and moves backwards. Now, when I try to merge to master, this is what git sees in the branch history:
A → B
As a result, the master branch sees my development branch as being behind and says that there are no new commits to merge.
Solution - Revert, Preserve, Attach
The solution, then, has to achieve all three conditions below:
- Revert to a previous commit
- Preserve all history
- Do all this without moving or detaching the HEAD
There are several ways to achieve this. After doing some research, we determined the cleanest approach would be the below series of commands (assuming we are already on master and have pulled):
git checkout -b revert-mastergit rm -rf .git checkout <commit hash> -- .git commit -m "revert master to state from 2 weeks ago"git push origin revert-master
The key is the third command: git checkout <commit hash> –-. With the previous command in step two, I remove all files in the revert-master branch. Next, instead of just checking out a prior commit, we added the argument –- .. This argument tells git to just retrieve the files from that commmit without moving the HEAD. The HEAD stays on the same branch and on the commit that I just reverted to. Now when I git commit, the HEAD moves forward instead of backward.
A → B → C → D → E (revert master)
As a result, master sees commit E, where I revert all my changes, correctly as an update when I push and merge.
I had to re-run my dbt project, but this was a much smoother process because of the tests we had built to track changes to models after major refactors. We verified the changes through the tests and delivered the new tables to stakeholders. When I have to revert back to the latest commit, I'll repeat these steps to bring master back to the latest state.
