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:
@rnelson0 git clean -fdx && git reset –hard #hugops
— David Schmitt (@dev_el_ops) November 15, 2015
//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”