Ask HN: What made you finally grok Git?
I've been using it for over 7 years, and while I've memorized a bunch of commands and have some idea of what the commit tree is, I'm perpetually bewildered by some of its messages or the behavior of some commands - and the only way I can fix major trainwrecks is by nuking a branch (when I'm lucky, the entire repository when not) and copy-pasting stuff until it's back to working state.
93 comments
[ 3.2 ms ] story [ 196 ms ] threadGit for ages 4 and up - https://www.youtube.com/watch?v=1ffBJ4sVUb4
But I don't think I should need to fully understand its internals for it to be "grokkable", an elegant thing could still be grokkable regardless.
It's possible though that the tasks: diffing, branching, merging, rebasing, tagging, etc., are too elusive for "elegance", ha ha.
I might add though that simple diffing, branching, merging I do more or less grok. Perhaps if git had stopped there, like my understanding did, I would feel more comfortable with it ... imposed limitations be damned. I suppose in fact I only use git in this limited capacity anyway. It's only when someone else on the team does something clever (or I screw something up)....
I wouldn't be able to tell you too much about the internals of VSCode.
The 'trouble' with git is, though, that what happens when you run the wrong command can vary from "easy to fix" all the way to "you just lost your work, you shouldn't have run that command".
Which means you need to either be very careful anytime you run git, or you need to learn more about git to avoid the footgun aspects of it.
The downside to this though is the type of novice most likely to make a mistake in "losing" their data will also not know how to recover their data.
If it's been checked into a commit, sure.
But if you discard changes which haven't been checked in, then the work is lost.
oh and never join any project that uses github and force squash-commits-on-pullrequest-merge. that is a sign that nobody there knows git or they don't care about code history.
edit: and yes, some rare times, rebase -i and moving things around will cause local conflicts. do not fear them. resolve and continue. it's all code you just wrote, should be easy and is part of the understanding process.
First, color me offended! Second... you're not completely wrong. My git-fu is not that strong. Would you help me improve it by explaining why squashing via the GitHub UI is so bad? To me it feels like an easy way to condense 1 PR into 1 commit. For some workflows, that makes much more sense than trying to get everyone to play by the same commit semantics. But I can see how it's not as "pure" in capturing the meaning behind your commits. Maybe it would be helpful to provide an example of a situation where squashing with the GitHub UI could cause problems?
thats squash on merge.
it's a stop gap hack to fix shitty teams who write books with chapters like "chapter 1" , "chapter two I guess, still broken", chapter 3 final" , chapter 4 final final".
Then it just became a matter of mapping git CLI commands to how they manipulate the graph.
Edit: yeah, it's acyclic not acrylic :).
Just goes to show the power of understanding the fundamentals of CS :-)
If you ask me, there's very little actual git to grok. Version control existed before git and many of the same things applied for "grokking" it. It just so happened that the implementations of other systems were worse and made certain operations expensive and error or conflict prone, while in git they're easy, safe and fast. Lots of developers have always been "bad" at version control and in some places I've been there was an entire team of people that did nothing but resolve merge conflicts (stupid idea if you ask me but hey).
Version control 101: It's just a graph of commits.
If it helps, think of it like a tree in the park or your yard. Just that trees don't have branches growing back into the trunk but if you only ever rebase without merging that's actually what your commit graph looks like.
What personally made me realize how git is so awesome is that it's all just labels. This is where some of the implementation details do come in but you don't need to go deeper than realizing that a branch is nothing but a label, not special or different from the main branch at all. You can freely relabel the graph any way you want. Think of the tree in your yard and buy a label-maker. Attach a label to the top of the trunk with `master` on it. Every few inches on the trunk imagine a commit (with a hash identifying it, on an actual tree, use the number of inches it's away from the ground). Where a branch goes off, do the same thing, tip of the branch gets a label. All operations in git you can imagine as using a saw and wood glue and your label maker now.
More implementation detail: go to the `.git` folder and take a look around. You can see this in action right there. Ignore the binary blobs part. Look at the other files and you see that each of the labels in the graph is just a file w/ the exact name of the label and the contents is nothing but the commit hash that the label is stuck to. It doesn't get easier than that. Now you know how to move labels around in the graph without even using git commands! Any old editor works but I recommend `vi`. Try it!
Remotes are easy as well. They're just another source of labels. You have your own set of labels locally (and don't need a server) and remotes allow you to see other people's sets of labels. By default most people probably only have one other set of labels, which is where they cloned stuff from, but you can set up any number of remotes and see any other person's personal set of labels and have a copy of it on your local machine. Think of it as your neighbour coming over with his own label maker. He's also brought some branches from his tree and started gluing them to your trunk ;)
`git fetch` just retrieves the latest copy of labels from a remote including any commits that are not present on your end yet. `git push` pushes your opinion of what the labels should be to a remote, including any actual commits that are not present on the other side yet. The remote can reject that because it thinks it knows better (i.e. only allowing fast-forwards). This is where the analogy falls apart a bit because it's hard to make copies of commits (parts of your tree you sawed out) but imagine you could walk over to your neighbour and give them a copy of parts of your tree just like they gave you parts of theirs.
Fast-forwards are also relatively easy to explain I think. It's nothing other than only allowing labels to be pushed along a path on the graph instead of freely moving labels around. For that to work, both you and the remote have to agree on what the graph looks like for this to be possib...
Reminds me a bit of that very popular / upvoted / quoted StackOverflow answer by Jim Dennis to a vim newbie question:
"Your problem with vim is that you do not grok vi.":
https://stackoverflow.com/questions/1218390/what-is-your-mos...
On a lower level, a commit is itself a bunch of pointers to files. Different commits can share files. Think about copy-on-write.
- Untracked: Files that Git doesn't know about.
- Tracked: Files that Git knows about. Further divided into unstaged files (will not be part of the next commit) and staged files (will be part of the next commit).
What helped me here was to know that it's possible to track a (yet untracked) file without staging it:
I do think understanding that it's a graph is the first step to grokking git though. The other stuff makes more sense after that (at least in my experience).
Personally I think the most difficult part of git to understand is merges. I get doing the final merge to master in the pull request model, but people merging branches willy nilly is something I find unnecessarily confusing and would prefer git didn't support at all.
one has yet to witness the abomination that is svn, sourcesafe, or "software working on my machine" delivered by a zip file in order repent and mend ones ways
Messing with existing commits can be fine as long as you're messing with the only existing version of that commit. Commits that exist in other branches, have been shared with other people, have been pushed to remote, etc, should be left alone. Rebasing local, unpushed single commits in your one working branch is fine. Rebasing anything else will cause problems. If rebase is causing you problems, do not rebase.
Also: small commits, short branches, merge often.
You just merge main in. Don't worry about extra commits that appear. PR tools like Github won't make people review them if you've done it all right, they can tell what a Merge Commit looks like (don't go deleting the auto-generated messages for merge commits unless you know what you're doing).
Once you merge main into your out-of-date branch, it'll be up to date. And when you merge your branch back into main after the PR, it will work out. This is the standard way you would work on a branch with a coworker, instead of working alone.
There's nothing wrong with merging main in.
If you absolutely MUST have a perfect history of your commits on top of THE CURRENT state of main, start a new branch, start a new PR, IMO. Just don't change public history.
I just rebase and force push. I really don't get all the people that say avoid doing that. If you're the only one working on that branch, there's no risk.
If you're working with other people on the branch, and no one has unpushed changes, it's also fine. Everyone will just need to reset to the origin version of the branch to keep working.
If people do have unpushed changes and you force push, then they need to spend some time untangling it. Which is annoying, but it's not the end of the world. I don't get all the fear about it.
Don't rebase and force push master, that'll piss everyone off, sure. But doing it in branches is fine in most cases.
This is where `git pull --rebase` could save you all some time. You can even modify your git configs if this happens enough.
I rebase PRs all the time, since they are my branches and my colleagues don't depend on them. If they do, I communicate with them so they know what I'm doing.
Things get very messy when you rebase a branch that multiple people branched off from, without them knowing you are going to rebase etc.
For people who love a clean, linear history, this can be frustrating, because it creates a new commit that doesn't add anything new, it just merges two branches. At my current project, PRs get merged very slowly, and after every PR gets merged, we merge master back into all the branches, which has lead to a ridiculous number of merge commits, and the guy who loves clean history won't stop complaining about it.
The problem is: if you rebase instead, you don't actually change the old commit, you make a new commit that contains the same content change as the old one. But it's a new commit, different from the one you already pushed to remote. So now you want to push your new rebased commit (as well as everything from master) to remote, and git says that remote has a commit that you don't have locally. So it wants to rebase or merge that remote commit onto your local rebased commit, despite the fact that they contain the same change! Now if you choose to rebase your local changes onto the remote commit, it rebases all the commits that you just merged from master, creating new commits for them. Then pushes them to remote. Then you still can't merge your PR, because it now still doesn't have the original (unrebased) commits from master, because you just rebased them. But you've duplicated a ton of commits. This is a mess.
There are 3 ways around this:
* Replace the first rebase with a merge, merging master into your local branch.
* Replace the second rebase with a merge, from when you pulled remote when you wanted to push, duplicating the one commit you meant to rebase.
* throw away the old PR and create a new one.
The first and third are the good options. The second is bad because it has a duplicate commit, but it's not nearly as bad as when you tried to rebase twice in a row.
Seriously, if stuff gets complicated, throw away your PR. Or avoid it getting complicated by always merging. If you want clean history, your only option is to throw away your PR every time you merge master back into your branch. It's either that or accept extra merge commits.
A dirty shortcut to creating a new PR is to force push. This will throw away the original commit on remote and overwrite it with your new rebased commits. This will work fine as long as nobody has checked out that branch in the mean time. If someone else has checked it out and pushes to your remote branch again, they may still reintroduce the old unrebased commit, and you'll still have that duplicate commit.
I usually just accept extra merge commits. Sometimes I force push, but I really try not to. You have to be sure nobody else is using that branch. In a complicated setup with lots of developers, automated tests and build servers, I'm reluctant to count on that.
I've yet to build a big feature that wasn't worked on by 4 or 5 people at the same time. Since they're different vertical slices, they're different branches (and tickets). But even then you might have a frontend and backend person collaborating on the same branch for a small slice of functionality.
This is also not a problem as even with one branch for two people who work with it. In git there are constantly more actual branches around than you "realize". E.g. here there's 3 branches going around and you just merge/rebase them. One branch is the one in the shared repo. The other two branches are the local branches of the same name on each of the two developer's machines. They need merging or rebasing just like you merge/rebase with `master` itself.
Depending on what you changed, you'll have weird looking history and conflicts to solve (or commits to skip). Which needs some head wrapping around but git itself so far has never been confused by my "reckless force pushing" on a shared branch. It helps if you don't have two people actually changing the same parts of the code though. Then you're in a world of hurt for conflict resolution but it's not the force pushing part that creates the hurt. That's just always bad.
Before you bring this back into `master` you'll want to squash all this into one commit and thus all the weird history ceases to exist.
Also, communication is key. If someone force pushes, tell the other person and help them resolve any rebasing/merging conflicts w/ your knowledge of the changes you made. If two people sit in opposite corners, force pushing "their world view" without tell each other, you definitely will be loosing code.
Although I do use git from the cli (or magit) primarily, this knowledge has helped me pick-up front end tools intuitively and have helped others when I've never used their preferred tool. All of this doesn't seem possible unless you've put in the effort learning what's going on behind the scenes. That is to say, git itself it neither user friendly nor intuitive it must be learned.
Knowing mercurial (hg) beforehand did slow my learning progress a bit with all the false-friends. You may find it easier to learn if you don't have to re-learn some naming.
[1] https://git-scm.com/book/en/v2
I think one thing that’ll help OP, is to learn everything on the command line. The fine grain control is worth the extra effort. I’ve hated almost all git GUIs in most IDEs because they won’t let me do weird things like grafting branches which is a very advanced concept for them but once you understand git, it is a very simple operation.
Are you using it from the command line? I feel that helps a lot. Get a good log alias[1] and get hacking. I almost exclusively use the official CLI, with the exception of git-gui for committing and rolling back patches (lines / hunks). (I find it's easier than `add -p` and that CTRL+J is a good time saver.)
1: As an example, https://github.com/aib/dotfiles/blob/master/.gitconfig#L29
Well, I think it's untidy to leave commits like "fix typo" or "PR feedback" in a change request, even if the whole changeset gets squashed.
1. `git status` + gitk before and after git commands
2. guess upfront what the result of git commands will be (in terms of gitk / git status)
3. be fluent with commit, push, pull, checkout, reset, merge, and possibly rebase (I call it required but it's opinionated) - you can safely ignore the other git api in 96% of workflow
In particular the golden rule of never rebasing a public branch has served me very well and I never seem to run into the "merge hell" that others complain of.
Better still, check it as you commit, by enabling the config option commit.verbose (e.g. `git config --global commit.verbose 1`), so that the full patch that is to be committed is shown in COMMIT_EDITMSG, not just the file names that you touched. This removes the race condition between review and commit.
(While on this topic, I also recommend commit.cleanup = scissors, and reading through `git config --help`, or at least skimming it for basic familiarity with what options are available to you. A few more that I like: merge.conflictStyle = diff3, diff.algorithm = histogram, mergetool.keepBackup = false.)
https://www.youtube.com/watch?v=ZDR433b0HJY
https://git-scm.com/book/en/v2/Git-Internals-Plumbing-and-Po...
It made me understand how simple git really it under the hood, you just need to understand how the commands operate on this simple data structure.
You could then graduate to forking a real repo. Again experiment fearlessly, with slightly more realistic scenarios.
https://jwiegley.github.io/git-from-the-bottom-up/
Concise, grounded posts that focus on the "behind the scenes" of git. After reading this I felt a much more grounded understanding of git.
I don't recall if it's explicitly covered in there, but because I'd read this I understood things like what happens to my commits after I "delete" them (e.g. remove the last reference to them / hard reset to an old commit, etc).
I avoided getting into Git for years (long ago when it was new, before GitHub) and found the man pages unhelpful.
Git From The Bottom Up totally turned it around for me.
Ever since then I've been comfortable using Git in all sorts of advanced ways, such as constructing repos from old software backups, editing decades of history to separate out publishable parts from proprietary or inappropriate parts while keeping the history, fixing broken repo issues, as well as regular daily use the way most people use it.
And best of all it keeps a backup of your entire repository so you can undo whatever git command you just did.
You can build a valid git repo with simple unix shell commands, and that really helped me to understand the magic behind the git commands:
https://git-scm.com/book/en/v2/Git-Internals-Git-Objects