Locally testing destructive Git actions
Saturday, 24 May, 2014 — git development
My team is responsible for maintaining a production line in our various Git repositories. After releases, we’re responsible for merging the development line to the production line, so any bug fix release starts from the basis of what’s currently running in production.
Making a longer story shorter, we had a release that we cancelled and decided to roll into a future release. But this was after we’d already made the merge to the production branch. We further had a mix of fast-forward commits with no merge commit and non-fast-forward merges across the different repositories that comprise a product release. We want to get our production line back to where production is actually at.
I researched a few strategies, centered around git revert
and git reset
for how to step forward and wanted to test them out.
Since each Git clone is complete unto itself (and we use a single remote versus multiple points of authority), I have the entire public history. So, it’s very easy to make a copy of a repo, make a bare clone as a “remote” and test changes locally.
Before we start
First, some words about safety. Out of an abundance of caution, I recommend the following before we start the experiment:
- Make sure your remote origin is up-to-date
- Make sure you are up-to-date with the remote origin
- Make sure the remote origin is backed-up
- Disable networking on your machine until the experiment is concluded
I also need your understanding that I’m describing procedures for experimentation, not trying to solve for your problem. We’re going to use git reset
below and it is a destructive action. You own your data and are much better equipped to understand your repository and situation. Act with prudence.
If you’re looking for info on addressing various changes to repositories, you’d do worse than having a look at Kai Howelmeyer’s “Git Undo by Example” post.
Local and destructive testing
Our safety pre-check complete, here’s how to test destructive Git repository changes locally:
Determine where you’re going to be working on this test. I recommend something like
git-experiment
underneath/tmp
.1
$ mkdir -p /tmp/git-experiement/
Copy the repository in question to
git-experiment
, or clone if it’s a small repository. For this example, let’s pretend this repository is namedsmith
. I’m going to refer to this as the local clone.1
$ cp -R /path/to/repo/smith /tmp/git-experiment/smith
Create a bare clone of
smith
in a newremotes
directory. This will allow us to push the local clone to what we’ll call the remote.1 2 3
$ cd /tmp/git-experiment $ mkdir remotes $ git clone --bare smith remotes/smith.git
Go into your experimental copy of
smith
and open the.git/config
file and update the url for the remote origin to point to your bare clone ofsmith
.1 2
$ cd smith/.git $ vim config
Before:
1 2 3 4 5 6 7 8 9
[remote "origin"] url = git@example.com:smith.git fetch = +refs/heads/*:refs/remotes/origin/* [branch "master"] remote = origin merge = refs/heads/master [branch "production"] remote = origin merge = refs/heads/production
After:
1 2 3 4 5 6 7 8 9
[remote "origin"] url = file:///tmp/git-experiment/remotes/smith.git fetch = +refs/heads/*:refs/remotes/origin/* [branch "master"] remote = origin merge = refs/heads/master [branch "production"] remote = origin merge = refs/heads/production
Go into the remote clone of
smith
. Instead of.git/config
, openconfig
here. Remove the remoteorigin
block. Also remove branch info that ties back toorigin
. This isn’t strictly necessary, but we’re just going to keep things clean and not run the risk of pushing changes where they shouldn’t go.1 2
$ cd ../remotes/smith.git $ vim config
Before:
1 2 3 4 5 6 7 8 9
[remote "origin"] url = git@example.com:smith.git fetch = +refs/heads/*:refs/remotes/origin/* [branch "master"] remote = origin merge = refs/heads/master [branch "production"] remote = origin merge = refs/heads/production
After:
1 2 3 4
[branch "master"] merge = refs/heads/master [branch "production"] merge = refs/heads/production
With this done, clone the bare remote repository as a second working copy, if you’d like to check the workflow once you publish your destructive change.
1 2
$ cd /tmp/git-experiment $ git clone remotes/smith.git smith-two
Since there’s a good chance you’ll want to try a few different approaches of your destructive change, make a copy of your test set-up and set it as read only:
1 2 3
$ cd /tmp $ cp -R git-experiment git-experiment.orig $ chmod -R ugo-w git-experiment.orig
Go into your experimental clone and make your destructive change and see what happens with it. You can safely push to your remote and see how that process works with doing a push and then
git fetch
ing orgit pull
ing changes into the second copy.In my case, I was experimenting with reverting branch merges and resetting and doing
git push --force
to publish changes. Below, I’m usingHEAD@{3}
but this could just as easily be a hash reference or a tag.1 2 3 4
$ cd /tmp/git-experiment/smith $ git checkout production $ git reset --hard HEAD@{3} $ git push --force
The
--force
is important here, because otherwise, Git believes you to be doing something it doesn’t think you should. And 99 percent of the time, Git would be correct, but we’re explicitly seeking to do something destructive.Go check the results of your experiment in the second local clone:
1 2 3
$ cd /tmp/git-experiment/smith-two $ git checkout production $ git pull --rebase
When you’re done, package everything up with a note or just
rm -rf /tmp/git-experiment
. If you’re saving the iteration, give it a meaningful name:1 2
$ cd /tmp $ tar -czf git-experiment.git-reset.tgz git-experiment
Results and Iteration
The first experiment is complete. You can shake out what to do and discuss it with your team, safely not affecting what other users are seeing on a repository. Determine what your next steps are, then move your original test aside, copy in the back-up and run further iterations.
In particular, you may have other questions to consider, such as determining what happens if you force a merge commit, provide tags, delete branches, and so on. What’s it going to be like publishing the changes? What’s it going to be like trying to fetch or pull the changes? Are there any other workflow considerations? How are you going to inform other users?
Hopefully, these experiments help you sort that out and provide a well-known path for your users. This contained experiment gives you a margin of safety to answer those questions.
Happy experimenting.