Getting a fresh start in a Git repository

Sometimes, the software in a git repository you work with starts to act wonky, especially when running tests, and for no particular reason that you can discern! I saw this recently when I was playing with bundler in some puppet module repos I was setting up to use Travis CI. My rspec tests started failing with nonsensical errors, such as saying it couldn’t find a class that was very much present on disk. What’s worse is that I did a fresh clone of the repo from GitHub into another directory and it worked flawlessly! What the hell? Thankfully, a little kvetching on twitter led to some help from David Schmitt:

//platform.twitter.com/widgets.js

David dropped some great wisdom here (thanks!). Let’s unpack it and understand it.

Git clean

git clean is the first command David suggested. I don’t like reading help pages (where’s the fun in that?), so ignoring the meat of git help clean and just looking at arguments shows us there’s a flag n that does a noop so we can test it. The fdx flags all have specific meanings as well. Here’s the noop output:

[rnelson0@build02 controlrepo:production±]$ git clean -fdxn
Would remove .bundle/
Would remove Gemfile.lock
Would remove dist/profile/.bundle/
Would remove dist/profile/Gemfile.lock
Would remove dist/profile/coverage/
Would remove dist/profile/spec/fixtures/manifests/
Would remove dist/profile/spec/fixtures/modules/
Would remove dist/profile/vendor/
Would remove vendor/

But why those files? I poked at git a little bit and it’s only apparent why one of those paths is being removed:

[rnelson0@build02 controlrepo:production±]$ git status
# On branch production
# Changed but not updated:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#       modified:   Gemfile
#
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#
#       dist/profile/coverage/
no changes added to commit (use "git add" and/or "git commit -a")

A few of those paths I remember putting into a .gitignore:

[rnelson0@build02 controlrepo:production±]$ cat .gitignore
Gemfile.lock
vendor
.bundle
[rnelson0@build02 controlrepo:production±]$ cat dist/profile/.gitignore
spec/fixtures/manifests
spec/fixtures/modules
Gemfile.lock
vendor
.bundle

Git clean is removing the files that are untracked or ignored. If we had read the help first, we would have seen, “Cleans the working tree by recursively removing files that are not under version control, starting from the current directory.” The -x flag explains why git status wasn’t showing the same list as git clean, as it ignores the ignore rules (double negatives are so next-gen). Also, note the end of the help description: starting from the current directory. Let’s look at the difference between running this at the top level or a few levels down:

[rnelson0@build02 controlrepo:production±]$ git clean -fdxn
Would remove .bundle/
Would remove Gemfile.lock
Would remove dist/profile/.bundle/
Would remove dist/profile/Gemfile.lock
Would remove dist/profile/coverage/
Would remove dist/profile/spec/fixtures/manifests/
Would remove dist/profile/spec/fixtures/modules/
Would remove dist/profile/vendor/
Would remove vendor/
[rnelson0@build02 controlrepo:production±]$ cd dist/profile/
[rnelson0@build02 profile:production±]$ git clean -fdxn
Would remove .bundle/
Would remove Gemfile.lock
Would remove coverage/
Would remove spec/fixtures/manifests/
Would remove spec/fixtures/modules/
Would remove vendor/

This gives us somewhat granular control of what we clean up. This could be valuable if we believe the issue is in dist/profile and don’t want to re-run bundle again. Run git clean -fdx now and let’s see where we end up.

Git reset

Depending on what you’ve changed, you may have more work to do, or you may need to skip this step or be more selective about it. Once the untracked files are removed, we are left with any tracked files that are changed. As an example, I’ve “accidentally” deleted a required line from my Gemfile:

[rnelson0@build02 controlrepo:production±]$ git status
# On branch production
# Changed but not updated:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#       modified:   Gemfile
#
no changes added to commit (use "git add" and/or "git commit -a")
[rnelson0@build02 controlrepo:production±]$ git diff
diff --git a/Gemfile b/Gemfile
index 65ddb99..924adf7 100644
--- a/Gemfile
+++ b/Gemfile
@@ -6,7 +6,6 @@ group :development, :test do
   gem 'puppetlabs_spec_helper', :require => false
   gem 'puppet-lint', :require => false
   gem 'rake', :require => false
-  gem 'rspec-puppet', :require => false
   gem 'generate-puppetfile'
 end

Especially now that we’ve removed the bundled gems, we are missing a vital dependency. If we re-run bundle install now, we’re actually worse off than before. We need that file to go back to the way it is. If we have made some changes we want to keep, we can restore some files from HEAD (the last commited version) with git checkout <path>:

[rnelson0@build02 controlrepo:production±]$ git checkout Gemfile
[rnelson0@build02 controlrepo:production]$ git status
# On branch production
nothing to commit (working directory clean)

If we have multiple changes and we don’t care about them, or we simply have to get back to a known good state, that’s where git reset –hard comes into play. This will reset ALL changes to tracked files, and there is no noop flag to test the change. This can be destructive so do not do this unless you know you don’t need the changes. I’ve made a bogus change to environment.conf as well so that we have multiple files to reset. Here’s what that looks like:

[rnelson0@build02 controlrepo:production±]$ git status
# On branch production
# Changed but not updated:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#       modified:   Gemfile
#       modified:   environment.conf
#
no changes added to commit (use "git add" and/or "git commit -a")
[rnelson0@build02 controlrepo:production±]$ git reset --hard
HEAD is now at 4a95034 Merge pull request #17 from rnelson0/generate_puppetfile
[rnelson0@build02 controlrepo:production]$ git status
# On branch production
nothing to commit (working directory clean)

Summary

Now we’re back to square one – same as if we had done a git clone in another location, but we still have any other local branches we may need. We also have a good understanding of when to use each or both commands, being only as destructive as we need to be. Hopefully this will help someone else save 4 hours of banging their head against a Software Defined Wall on a Sunday morning!

One thought on “Getting a fresh start in a Git repository

  1. Pingback: Modern rspec-puppet practices | rnelson0

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s