Git-fu: useful shortcuts

Originally published Sep 22, 2017

Git is complicated. It's an extremely powerful tool, but it's also easy to get burned -- I've received the dreaded `detached HEAD` warning on multiple occasions and been at a complete loss as to why that had happened. However, as with any tool, I've continued to learn to use it, and now I'm fairly comfortable performing tasks that a year ago would have stumped me. ## Tools Git is a wonderful tool, but it becomes much easier when you have other tools layered on top of it. ### GitHub for Atom I spend nearly my entire day in [Atom], so when they released [GitHub for Atom], I was thrilled. Now, I can stage and unstage files as well as individual lines, right from the editor. It's majorly boosted my productivity. ### `git-plus` This is an Atom package that enables a wide array of Git functionality. While _GitHub for Atom_ mostly "lives" in the right-hand dock, `git-plus` adds context menu options throughout the UI, as well as commands accessible from the palette. They each have their strengths, and I'm always glad I have both installed. ### Sourcetree [Sourcetree] is an amazing app that I've been using for years. I almost never use it for common tasks such as committing, merging, and pushing. Instead, what I find useful is that it gives a clear, configurable visual display of the commit graph, and I can right-click on any commit to get its hash. Generally I'll use these hashes for more advanced operations, such as starting an interactive rebase from a specific commit. You can also cherry-pick or revert any single commit directly from the UI. > (tip) To install on Mac with Homebrew Cask: `brew cask install sourcetree` ### `bitbucket-git-helpers` [bitbucket-git-helpers](https://github.com/unixorn/bitbucket-git-helpers.plugin.zsh) is a handy set of aliases useful for quickly opening your browser to Bitbucket pages related to the repo (pull requests, create pull request). ## Aliases Add these under the `[alias]` section of your `~/.gitconfig` to take advantage of them. ### `git this` This one's super handy: it prints the name of the current branch. Pretty simple, but it comes in handy when working with branches with long names, or chaining together commands. ```ini this = rev-parse --abbrev-ref HEAD ``` ### `git puot` Push the current branch to an upstream with the matching name. I usually use this for feature branches. ```ini puot = !git push -u origin $(git this) ``` ### `git cod` I can't tell you how many times I've meant to create a feature branch from `develop`, but instead accidentally created a feature branch off of whatever feature I was working on at the time. This command prevents me from doing that. Running `git cod feat/foobar` creates a new `feat/foobar` branch from upstream develop. It's usually a good idea to run `git fetch` before doing this as well, to make sure the new branch is up-to-date. ```ini cod = checkout origin/develop -b ``` ## Playbooks Here are a handful of common tasks. ### Figure out what branch I was on If you're like me, you may switch branches multiple times over the course of a day, but have trouble remembering what branch you were on. To list the 10 branches with most recent commits, run this command: ```bash git branch --sort=-committerdate | head ``` If you've got [aliases](/snippets/git) set up, you can write a slightly shorter version: ```bash git recent | head ``` ### Interactive rebase This process has immeasurably improved my ability to write clean PRs. As I go, I try to make very small commits. Then, when I'm ready, I perform an interactive rebase, to merge together commits that are too tiny to stand on their own, reword some awkward commit messages, and remove commits that I only made as a quick test. If I were preparing a PR for the `develop` branch, I'd run: ```bash git rebase -i --autosquash origin/develop ``` This would open Vim with a text file resembling the following: ```txt pick 6841ac5 Do not check in this change! pick 9bd806c Implement feature X pick 102bda9 Implement feature Y pick 3011ca5 Fix bug in feature X pick 9002550 Write tests for feature X pick e781dae Write tests for faeture Y ``` As you can see, it's a little rough, and I might not want to check it in this way. I might edit the file to look like this: ```txt drop 6841ac5 Do not check in this change! pick 9bd806c Implement feature X fixup 3011ca5 Fix bug in feature X pick 9002550 Write tests for feature X pick 102bda9 Implement feature Y reword e781dae Write tests for faeture Y ``` The `drop` command ignores a commit, the `fixup` command squashes a commit with the previous one, the `reword` command allows you to rewrite a commit message, and the `pick` command leaves that commit alone. There are also several other things you can do here; full instructions are available when you run the command. Save and quit, and Git will roll through the commit history, pausing if there is a merge conflict or to allow you to reword a commit message. ### Reverse a merge First, check out the branch that has the merge you wish to reverse: ```bash git checkout develop ``` Next, get the ID of the merge commit. I like to do this from Sourcetree by right-clicking a commit and choosing "Copy SHA-1 to Clipboard", but you can also do this with `git log`. Finally, run the following command, making sure to paste in the correct hash for `$HASH`: ```bash git revert -m 1 $HASH ``` ### Skip tests for a single commit I like to have my projects set up to run tests before I push. Sometimes, though, I have to push a branch in an incomplete state, or delete a remote branch while I have active changes. I used to remove the git pre-push hook, perform the push, then re-enable it. Turns out there's a much easier way: the `--no-verify` flag. For instance, if I want to push some incomplete work on the `feat/foo-bar` branch: ```bash git push --no-verify -u origin feat/foo-bar ``` ### Check out multiple branches Sometimes, you just need to do more than one thing at a time. Generally I'll either use `git stash` or a `WIP` ("work in progress") commit to put things on hold while I work in another branch. However, there's another command that's useful in some instances where you really do want another copy of the project on your hard drive. The neat thing about this command is that you can work on another copy of a local branch, without having to first push it to your remote. To create a new working copy of the `develop` branch, run this command: ```bash git worktree add ../new-working-copy develop ``` This will check out the `develop` branch into the `../new-working-copy` directory. You can change these arguments as you need to. One caveat to watch out for: you can't check out the same branch in both your original and new working copy at the same time. This is mildly annoying, but far better than getting your repo into a state where you can't reason about what's going on. If you get an `already checked out` error after doing this, either change to a different branch in your other working copy, or else delete the working copy and run `git worktree prune` if you no longer need it. > (discover) > - [Parallelize development using git worktrees](https://spin.atomicobject.com/2016/06/26/parallelize-development-git-worktrees/) > - [`git-worktree` documentation](https://git-scm.com/docs/git-worktree) ## Listing all tracked files This one is useful when you're making structural changes to a repo, and want a complete list of all the files that are tracked on a given branch or commit. To display all files at the tip of the `master` branch, run: ```bash git ls-tree -r master --name-only ``` More generally, if you have the `git this` alias enabled, you can run: ```bash git ls-tree -r $(git this) --name-only ``` to see all files on the active branch. ## Publishing a directory to a branch This trick is great if you want to publish to a `gh-pages` branch! Thanks to [Ryan Palo] for a [great post](https://dev.to/rpalo/publish-single-directories-to-another-branch--l8b "Dev.to: 'Publish single directories to another branch'") outlining how this works. [Ryan Palo]: https://dev.to/rpalo "Ryan Palo on Dev.to" Run your build -- let's say it builds into a folder called `/public/`. Then, make sure this folder isn't ignored via `.gitignore`, and `git add` and commit. Then, run the command: ```bash git subtree push --prefix public origin gh-pages ``` If you're running this in a CI environment, you can keep the built assets out of your working branch if you don't push after committing the built assets. It's a good idea to have the build step modify the `.gitignore` as well, to continue to prevent people from accidentally committing their built assets. ## Pre-empting a merge conflict **Note:** I'm not 100% certain about this trick; I'll update this page as I learn more. When a branch contains a merge conflict, there are traditionally 2 ways of dealing with it. 1. **Merge.** This allows you to avoid rewriting git history, at the cost of creating a tangled mess of branches and merges. 2. **Rebase.** This allows you to have a relatively linear history, at the cost of having to rewrite history (and possibly irritating teammates). In addition, you'll need to run through the *entire* set of commits on your branch, possibly with a large number of conflicts. However, there's a third option: "steal" the changes which create the conflict, to prevent it from happening in the first place. Here's the magic sauce: ```bash # Note: this is still a work in progress; might break things! git format-patch ${COMMON_ANCESTOR}^..${MERGE_TARGET} --stdout | git am -3 ``` `COMMON_ANCESTOR` here is the most recent commit that both branches have in common. You can use a tag for this if it's easiest, or you can pull up a hash using any graphical tool. `MERGE_TARGET` is the branch you're trying to merge into. [`git am`](https://git-scm.com/docs/git-am) will cherry-pick each commit in the set and apply it to your branch. The `-3` flag configures `am` to stop when there's a conflict; if you are already comfortable with interactive rebasing, you'll find the interface familiar. If you only want to solve the conflict in a subset of files, specify the fileset as arguments to `format-patch`: ```bash # Note: this is still a work in progress; might break things! git format-patch ${COMMON_ANCESTOR}^..${MERGE_TARGET} --stdout package.json src/__tests__ | git am -3 ``` Finally, if you want to update the files, but stop before committing, replace `git am -3` with `git apply -3`. This allows you to apply your "merge" as a single commit. > (tip) If you have the `git this` alias installed, you can automatically find the common ancestor: > ```bash > # Note: this is still a work in progress; might break things! > git format-patch "$(git merge-base $(git this) $MERGE_TARGET)^..${MERGE_TARGET}" --stdout package.json src/__tests__ | git am -3 > ``` ## My .gitconfig You can see my `.gitconfig` [here](/snippets/git). [Atom]: https://atom.io/ "Atom editor" [GitHub for Atom]: https://github.atom.io/ "'GitHub for Atom' extension" [Sourcetree]: https://www.sourcetreeapp.com/ "'Sourcetree' app"