Git mastery

Some common tasks. All of these are available on the web.

TODO

  • Work with git checkout --orphan gh-pages to see how to set up a completely empty branch.
  • Using git submodules, develop a personal use case.
  • git comparing local and remote repositories.
  • Blog post: “To rebase or not to rebase.” If still applicable, consider drawing from the Ruby Parley discussion on rebase/Cowboy by Ben F. on May 10, 2013. Here’s when to merge and when to rebase.
  • For WIP, instead of making a commit you can make a stash and push it: git stash; git push origin stash@{2}:refs/heads/otherstash

Modify specific commit

Suppose, for whatever reason, you want to change a commit back in your commit history. That is, you want to edit just that commit. This is not difficult, and can be done as follows.

Adapted from SO:

Suppose you want to change some code back in commit c0bec38, run

$ git rebase --interactive c0bec38^

edit c0bec38 a note about closures
pick 82afaf2 default arg for lambdas
pick d096072 some concrete examples of isotropic vectors
pick 88a35c9 tfidf - formatting

Choose the appropriate commit and modify ‘pick’ to ‘edit’. After making changes and commiting, tell git to amend all following commits without editing on each:

$ git commit --all --amend --no-edit

Then $ git rebase --continue

Note that this changes the SHA-1 of the edited commit and every commit forward from the edited commit. This is the usual consequence of using git rebase, and force pushing the rewritten history will break anyone else’s clone.

git filter-branch

Super useful stuff.

Extracting a subdirectory

From Github’s “Splitting a subfolder out into a new repository” we get this little pearl:

git filter-branch --prune-empty --subdirectory-filter tfidf master

Changing committer information

Here’s a spiffy way to change all the author and committer information in an existing git repository, by defining an alias.

[alias] change-commits = "!f() { VAR=$1; OLD=$2; NEW=$3; shift 3; git filter-branch --env-filter \"if [[ $`echo $VAR` = \\\"$OLD\\\" ]]; then export $VAR=\\\"$NEW\\\"; fi\" $@; }; f" #from https://help.github.com/articles/remove-sensitive-data #remove-file = "!f() { git filter-branch -f --index-filter \"git rm --cached --ignore-unmatch $1\" --prune-empty --tag-name-filter cat -- --all; }; f"

I need to work up a scratch git repo locally to explore this script alias in more depth.

Invoke it with the following:

git change-commits GIT_AUTHOR_EMAIL "david.doolin@gmail.com" "johnhenry7@yahoo.com" -f

The -f is force, which will be necessary after the first change.

The values which can be changed are:

  • GIT_AUTHOR_NAME
  • GIT_AUTHOR_EMAIL
  • GIT_COMMITTER_NAME
  • GIT_COMMITTER_EMAIL

Interactive rebase for changing committer information

pick abcd Someone else's commit pick defg my bad commit 1 exec git commit --amend --author="New Author Name <email@address.com>" -C HEAD pick 1234 my bad commit 2 exec git commit --amend --author="New Author Name <email@address.com>" -C HEAD

Managing identities

All my game stuff goes under the “johnhenry” identity, to keep a nice firewall between work and play. Here’s what needed to be done to fix up the consim github repo.

First is updating the identity locally to override the global identity:

  • git config user.email "johnhenry7@yahoo.com"
  • git config user.name "John Henry"

Next, fix all the previous commit information:

  • $ git filter-branch -f --env-filter "GIT_AUTHOR_NAME='John Henry'; GIT_AUTHOR_EMAIL='johnhenry7@yahoo.com'; GIT_COMMITTER_NAME='John Henry'; GIT_COMMITTER_EMAIL='johnhenry7@yahoo.com';" HEAD

Note that each of these can be changed independently or in any combination. There is no need to change all at once.

Finally, change the remote with a force push:

  • git push -f origin master

The usual caveats for force pushing apply: don’t be a jerk if someone else is working from the same branch.

For a single commit

Reconfigure author and email, then git commit --amend --reset-author

Here’s a spiffy gist: https://gist.github.com/johnhenry/7f10d823530afd83a6bc

Replace a file with a file from another branch

Sometimes a bad merge or rebase messes up a file irreparably. In this case it can be better to replace the entire file with it’s counterpart known to be good in another branch.

Suppose our rebase of Ruby Koans screwed up the local copy of README.rdoc. Here’s how to fix that:

git remote update # e.g., file is in upstream
git checkout upstream/master -- README.rdoc
git add README.rdoc
git commit -m"Replaced botched README.rdoc with good from upstream/master"

Not difficult at all.

Finding a particular file revision

The following was quited from SO:

  • git log path should do what you want. From the git log man:

[–]

Show only commits that affect any of the specified paths. To prevent confusion with options and branch names, paths may need to be prefixed with “– “ to separate them from options or refnames.

Then, git checkout <commit hash> filename.

File differences between commits

Many ways, here are a few:

  • git show –name-only

There is a good StackOverflow page for this.

git remote update

Here’s a useful command, especially if working with a system like git flow: git remote update. git remote update is equivalent to git fetch --all.

Here’s an example from Ruby Cabochons:

$ git remote --verbose update
Fetching origin
From bitbucket.org:doolin/cabochons
 = [up to date] feature/sti-sans-rails -> origin/feature/sti-sans-rails
 = [up to date] develop    -> origin/develop
 = [up to date] feature/controllers-sans-rails -> origin/feature/controllers-sans-rails
 = [up to date] feature/stubs-mocks -> origin/feature/stubs-mocks
 = [up to date] master     -> origin/master
$

The --verbose flag lists the relevant branches which are fetched.

From the git-remote man page:

update

Fetch updates for a named set of remotes in the repository as defined by remotes.<group>. If a named group is not specified on the command line, the configuration parameter remotes.default will be used; if remotes.default is not defined, all remotes which do not have the configuration parameter remote.<name>.skipDefaultUpdate set to true will be updated. (See git-config(1)).

With –prune option, prune all the remotes that are updated.

Deleting a commit

From SO, How to delete a git commit: git reset --hard HEAD~1

What is the behavior when `--soft` is used instead of `--hard`?

Notes:

  • If this was a model or a scaffold or something which is a result of a “pure” code generate event (e.g., rails generate), check for the equivalent of a rails destroy and back out only those changes.
  • For migrations, it may be necessary to do the following:
    • rake db:drop to get rid of everything.
    • rake db:migrate to re-run the migrations.
    • rake db:test:prepare
  • --hard will blow away the cache and any changes not stashed.
  • Destroying a commit is different than “uncommitting.”

Uncommitting

Answering the question above, git reset --soft HEAD^ should leave the changes from that commit instead of destroying them. Since --soft is the default option, git reset HEAD^ uncommits. (TODO: Verify.)

git discard unstaged changes

Here’s a little kata:

mkdir gittest
cd gittest/
git init
vi file1.txt # edit 1
git add file1.txt
# git commit
vi file1.txt
git diff
git reset -- file1.txt # does nothing, haven't committed...
vi file1.txt
git commit -m"edit 1"
vi file1.txt # edit 2
git reset -- file1.txt # does nothing
git add file1.txt
git status # staged
git reset -- file1.txt
git status # unstaged
git checkout -- file1.txt # remove changes
less file1.txt

git editing and splitting commits

This is a good StackOverflow article on editing commits. Good git-rebase man page too.

This came about as I moved cucumber from :test, :development to just :test to shut off some warning messages. The procedure went like this:

  • git rebase -i HEAD~2
  • git reset HEAD^
  • git add -p to select the commits correctly.
  • Commit for each add, as appropriate.
  • git rebase --continue
  • git rebase -i HEAD~3 to squash like commits.

This turned out to be fairly easy.

Working with commit dates

For example:

GIT_COMMITTER_DATE="November 9, 2014, 2:41 PM" git commit --date="September 21, 2014, 9:45 AM"

Force pushing with +

This bears deeper scrutiny: git push origin +master