Things You Want to Do in Git and How to Do Them
Subscribe with RSS
Preface
Git is a very powerful tool, but infamously it's also a very confusing one. And that has additional effects on how developers use it. New developers often struggle with how long their commits should be, but part of that is just that they're not comfortable with git's tools, because they naturally force you into certain sizes due to convenience.
When you make a commit, think--will I or someone else likely have to git revert
it? In that case, I should make the change as isolating as possible, so that the revert
is as peaceful as possible.
Am I about to make changes that might blow up and force me to git reset
to an earlier state? Well, maybe I should commit my peaceful changes right now so they don't end up as collateral damage.
Just keep thinking about how you'll use git's tools when you inevitably fuck up, and how you can make your commits as convenient as possible for your future self, and that will guide you on the proper length and breadth of your commits.
Now, many articles try to instill in readers a fundamental understanding of how git works, but if most people are like me that just causes my eyes to glaze over as a quick google search turns into a lecture. I think you'll just get it as you use git more. At first, you just have things you want to do, and no idea how to do them, because git's commands are seriously unintuitive once you do most things (like, why does checkout do 3 completely different tasks?)
Some General Tips
Commit Hashes
Just copy the first 5-6 characters; you don't need the whole thing, just enough to make it unique.
Commit ranges
Most commands can work on a range with <hash a>..<hash b>
. For example, git cherry-pick asd341..fa3745af
.
Note the ..
, ...
does something different which is almost certainly not what you want.
Things Happen in the Active Branch
This is obvious if you ever think about how git is implemented, but at first, it may not be obvious what
git merge master
does. Is it merging master
to the current branch, or merging my current branch to master
? It's the former.
git merge master
will merge master onto the current branch.
git cherry-pick <hash>
will apply the commit onto the current branch.
Basically, things happen to the current branch.
HEAD
Instead of hashes, you can do things relatively. HEAD~1
refers to the commit just before the most recent one, for instance. In practice, I personally tend to just use commit hashes, but can it be useful sometimes.
Branches as Backups
I didn't get this at first, but branches are really very isolating. If you're afraid you're going to break something horribly, git checkout -b branchname-backup
/git branch branchname-backup
.
Now, regardless of how much you destroy your current branch with whatever black magic you're pasting into your command line from Stack Overflow, you'll always have that frozen snapshot of your repo waiting for you when you inevitably give up.
It's really very liberating, especially early on, because with the like 5 different ways to undo things with varying degrees of severity it can be daunting to actually code without fear like git's supposed to empower you to do.
The basic cycle
git pull master
to get up to date
git checkout -b myname-feature
to get a branch of your own
git add <files>
that you changed (and yes, you can git add .
to add everything in the current directory, but be careful)
git commit
and if you don't write anything, the commit will be aborted (also, you can git commit -m "multiline commits" -m "without text editors" -m "wow!"
)
git push -u origin myname-feature
the first time, git push
after that
git branch -D myname-feature
when it gets merged into master and you don't need it cluttering your autocomplete anymore
Woops forgot a file
git add <missing_file>
git commit --amend
You can also add --no-edit
if you don't want to change the commit message (or just don't change it when it prompts you)
Added too many files
git reset --soft HEAD~1
git reset HEAD <badfile>
git commit
Now we're getting into WTF territory. The first reset will take the commit, and basically return it to the state just before you committed (so they've been add
ed, but not committed). The second reset will "unstage" it, or "un-add" it. Then you just commit again, minus the file you didn't want.
Undo
Everything is on fire please go back
Make everything go away
git reset --hard <commit hash>
Will remove all changes after that hash and remove those commits from history. Basically it's like you never made those bad changes. They don't exist anymore. If you want to see them again, you can't (which in practice isn't a problem much of the time).
There's usually a bunch of warnings when people talk about it, but let's be honest, most of the time this is what you want because you know everything after that commit is garbage.
But I still want some of the changes
git reset <commit hash>
implicitly is --mixed
Will also remove the changes and history, but the changed files will still be on-disk, but not staged unlike soft, so you get another chance to pick and choose what you permanently delete.
i.e you might do
git reset 1a34abd
git add <one_good_file>
git commit -m "everything else is garbage though"
When do I use which reset variant?
--hard
when you just want everything to go away
--mixed
when the things you want are few
--soft
when you want most things, just excluding a few files
Mixed and soft are pretty interchangeable, it's just convenience.
My coworker broke my stuff on master by accident
get revert <bad commit hash>
Unlike the above, this makes a new commit that undoes the bad commit. Very common when for undoing bad things on a shared branch.
Which one should I use?
Reset is cleaner. It's like the bad commits never happened. Revert is safer. Everything is still in history, and it's additive, so it's nicer for everyone else on the shared branch.
In general, if you're on a feature branch on your own, reset is probably what you want. If you're on a shared branch, you should do revert.
I just want this one file to go back
git checkout <happy time hash> -- file
git commit
This is usually how you want to do it. Note that this is additive, we're not removing anything.
I need to update my feature branch but don't want it to be ugly
This is a fairly common pattern at larger companies with a mono-repo. You want to keep up to date with master/main, but you don't want your history to look like
commit c1a3647asdfasdfasdfa3fdf43c0f5fdac43e3768 (HEAD -> master)
Merge: 728f23c 1b0a8b3
Author: me
Date: Today 1:00 pm
I added a button
<insert 1000 random commits from elsewhere in the monorepo>
commit 54aa6fc2919735cfcc10c4f6c83c9232c9788094
Author: me
Date: Today 12:50 pm
I fixed my config files
Instead of git merge master
, you do git rebase master
. Then it'll look like
commit c1a3647asdfasdfasdfa3fdf43c0f5fdac43e3768 (HEAD -> master)
Merge: 728f23c 1b0a8b3
Author: me
Date: Today 1:00 pm
I added a button
commit 54aa6fc2919735cfcc10c4f6c83c9232c9788094
Author: me
Date: Today 12:50 pm
I fixed my config files
<insert 1000 random commits from elsewhere in the monorepo>
See, it "rewrote" history, as if all those random commits elsewhere in your company happened before you branched off. Nice and clean. But, one gotcha. To update your remote branch you need to do
git push -f
-f
is a little scary, but otherwise git will be incredibly confused and try to have you pull from your remote branch. This is, naturally, a workflow for a feature branch with you alone. Don't -f
on a shared branch.
Redo
I want to transplant some commits from another branch to this branch
git cherry-pick <hash>
It will selectively apply the commits (or commits, if you use a range) to the current branch. Basically, if don't want to merge everything from another branch, just some things.
I just want another file from another branch
git checkout <branch_name> -- file1 file2
This will add those files (and stage them! You just have to commit afterwards). No commit history, which may or may not be what you want. Also useful for when you committed multiple files (or the file has had a lot of commits over the ages) so you can't really use cherrypick.
I branched off a branch that got merged into master and I want my branch to be off of master now
Okay, that title is a little confusing. Here's the situation: I branched off of master to add profile images, let's say it's stu2b50-profile-images
. I submitted it as a PR, but I wanted to keep working on a vignette effect (but want to keep my PRs short), so I git checkout -b stu2b50-profile-images-vignette
and keep working on it.
Eventually, stu2b50-profile-images
gets squashed and merged into master. But now I want stu2b50-profile-images-vignette
to depend not on the old branch, but master, with its squashed commit, so I can eventually submit for review peacefully.
Here's what I'm going to do
<on master>
git checkout -b stu2b50-pfimage-vignette //the new home for my branch
git rebase --onto stu2b50-pfimage-vignette stu2b50-profile-images stu2b50-profile-images-vignette
You can see the command as
git rebase —-onto <place-to-put-it> <last-change-that-should-NOT-move> <change to move>
Of course, you can play with what branch names you change. For instance, to keep the original branch name
<on stu2b50-profile-images-vignette>
git branch temp
git checkout master
git branch -D stu2b50-profile-images-vignette
git checkout -b stu2b50-profile-images-vignette
git rebase --onto-stu2b50-profile-images-vignette stu2b50-profile-images temp
Now stu2b50-profile-images-vignette
is a brand new branch branching off of master with my new vignette effect.
I want to do <complicated thing>
You can probably achieve it by making some temporary branches and git rebase --interactive
. Just leaving this here so you know what to google; it's probably git rebase
, since most "complicated things" involve a messed up commit history.