walls.corpus

By Nathan L. Walls

Locally testing destructive Git actions

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:

  1. 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/
    
  2. 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 named smith. I’m going to refer to this as the local clone.

    1
    
    $ cp -R /path/to/repo/smith /tmp/git-experiment/smith
    
  3. Create a bare clone of smith in a new remotes 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
    
  4. 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 of smith.

    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
    
  5. Go into the remote clone of smith. Instead of .git/config, open config here. Remove the remote origin block. Also remove branch info that ties back to origin. 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
    
  6. 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
    
  7. 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
    
  8. 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 fetching or git pulling 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 using HEAD@{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.

  9. 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
    
  10. 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.