Git Tricks I Wish I Knew

When you first start using Git, everything goes perfectly fine as long as you don’t ever mess up. Perfect commit messages right from the get-go, no temporary or throwaway commits needed for any reason, never forgetting to pull, etc. But that’s not how reality works: often you will forget to commit files like an idiot and your next commit message will read, “forgot to push files”. Fortunately Git has ways to correct these mistakes.

For beginners: git add and commit

Git’s history is built on commits. The way you tell Git what you want to commit next is through the staging area, and to add something to the staging area you just run git add. Sometimes when a file isn’t being tracked by Git, git add will also make Git track the file. This is why people run git add --a and then git commit -a -m "msg" whenever they want to add a new file. (Don’t do this, by the way, it’s terrible practice.)

You can do git commit FILE as a shortcut instead of running git add FILE; git commit. But a lot of beginners do not get the difference between git add and git commit. If that describes you, try this for a day: never add any arguments or flags after git commit, and you’ll get the idea really quick.

“Flags” includes -m, and if you’re using said flag your commit messages are probably not detailed enough. When editing your commit message, you see what files are being committed. That way, you will never accidentally commit a file you didn’t mean to, because you will have ample time to see exactly what you are committing.

Global .gitignore

If you are a soydev on MacOS or Windows (Mac users are by far the worst offenders), you probably have committed .DS_Store or desktop.ini once in your lives. First off, that is a sign your workflow is stupid and you don’t know what you’re doing: you should never git add --a; git commit -a unless you absolutely know what is being committed.1 But even then, it’s good to have some safety against ever committing garbage like .DS_Store: you will never have a legitimate file named .DS_Store, so it’s perfectly fine to just gitignore it.

But instead of adding it to the .gitignore of each project, you can instead add it to your personal global .gitignore. This means that, across all local repositories, you will never be able to add any files in your personal gitignore to the staging area.

You can read this GitHub Gist, it’s quite comprehensive. If you follow the links in the comments you will also find the default locations for the git config/ignore files. I will tell you explicitly that for Linux, you want to create the files ~/.config/git/ignore and ~/.config/git/config. If the directory ~/.config/git doesn’t exist (it probably won’t), make that too.

What I recommend is you make a git repository in your dotfiles, and then clone it in ~/.config. (The repository should then be ~/.config/git.) This is not so feasible in GitHub because you don’t have nesting (groups and subgroups in GitLab), but I still think you should make separate repositories: your git config files have nothing to do with your vimrc or whatever else you have.

Here is where my advice becomes more personal taste. I think your project .gitignore should only contain things that everyone will see. That’s why my TeX projects don’t have a .gitignore; my build files all go inside a build directory, because I use my own custom script called dlatexmk. And OS-specific files like .DS_Store definitely should not be in a .gitignore.23

pull = fetch + merge/rebase

A lot of beginners don’t know what git pull does and just assume it magically updates their repository.

Even though your local branch might be named master and push to a remote branch named master, your local and remote branches are two totally separate things. You just synchronize the two via — you guessed it — git push and git pull.

Just google git pull = fetch + merge to get a more complete picture, dozens of guides on the internet exist about this already.

If you forget to pull before making some changes, committing, and trying to push, you’re typically left in a quite precarious situation. You can either try to merge and get a messy commit you never wanted, or (more frequently) give up, re-clone the repo, and copy your changes there. But if, say, the remote had file A changed and you changed file B, then your commits are independent and present no conflicts. In this case, wouldn’t it be nice if, say, you could make it as if you were working on the latest commit from the remote?

And that’s exactly what rebasing does: it applies your changes on top of the remote branch’s. Thus git pull --rebase will do the job.

Staging part of a file

When I only wanted to commit part of my code I would usually manually delete it, commit my file, then undo. Turns out you don’t have to do this: git add -p FILE gives you a way to do this interactively.

It will give you options [y, n, q, a, d, e, ?] for each hunk, and sometimes s as well. Most options are quite obvious, but the two I find most useful are e and s. The e option lets you edit the patch line-by-line, so you can remove lines starting with + or - to change what you stage.

rebase -i

If you haven’t pushed to master (it’s OK if you’ve pushed to feature/dev branches) and realize that a commit is stupid or you forgot to add something to a commit or whatever, you can use git rebase -i to drop, squash, reword, or reorder your changes.

If you have pushed to remote, you will need to perform git rebase -i HEAD~n where n is however many commits back you want to go. So git rebase -i HEAD~5, for instance.

At this point the remote will reject your changes, because your local branch and remote branch have divergent histories. You can do git push -f to force Git to accept your changes. Needless to say, this will rewrite history and is an awful idea if anyone relies on your branch history at all. (Which is why dev branches are good.)

  1. It’s probably okay if you’re just committing config files, especially if you’re migrating stuff. If you’re making ~/.config/git a repository, for example.↩︎

  2. The GitHub Gist I linked to agrees with me, so clearly I’m not the only one who thinks this way.↩︎

  3. If I added .DS_Store to a project-level .gitignore that you frequently push to, it’s probably because I think you’re an idiot, not because I forgot what goes into a gitignore.↩︎