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.)
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.↩︎The GitHub Gist I linked to agrees with me, so clearly I’m not the only one who thinks this way.↩︎
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.↩︎