This is my recommendation: Don't start with git pull. Start with git fetch. Work with git fetch and other Git commands for a while, until you really "get" what git fetch and the other Git commands do. Then you're ready to use git pull, if you choose, because git pull simply means:
- run
git fetch; then
- run a second Git command, usually
git merge, but you can choose git rebase instead.
This makes git pull a convenient shortcut, because it turns out that after git fetch, you very often want a git merge or git rebase. But I often like to stick a git log or other command in between, and when using git pull, you can't do that. It turns out the convenience command is ... inconvenient.
The bigger problem newbies have with git pull is that it seems to be Magic. This makes it impossible to think about what's really happening. I had this same problem for a while when I first used Git about 15 years ago. Learning that I could separate git pull into git fetch + second command was one of the keys to understanding Git.
Things to know
The things you need to know include the following:
Git is not about files, but rather about commits. So you need to look at which commits you have, and which commits some other Git repository (such as the one over on origin) has.
Commits contain files. In fact, every commit has a full snapshot of every file. These are stored in a compressed, read-only, Git-only format, that only Git can read and literally nothing—not even Git itself—can overwrite. Once a file is saved in a commit, then, it's completely safe inside that commit. Nothing can change it. As long as you still have that commit, you still have that file too.
But this in turn means that the files that you can see and work on / with are not in Git. The files you see and work on are ordinary, read/write files. Since the files inside a Git commit aren't ordinary files, these must be different files. And that's just what they are. When you use git checkout or the newfangled git switch, you pick out some commit, from some branch, that Git should copy out. Git finds all the files that are inside that commit and copies them out to your work area, which Git calls your working tree or work-tree.
Once you have a solid grasp of this, you then need to realize a few more things:
Git makes new commits, not from what you have in your working tree, but rather from what Git has in Git's index. This thing—"the index"—has two more names: it's also called the staging area, and sometimes—rarely these days, mostly in flags like --cached—the cache. (Why it has three names is a little bit of a mystery, but staging area is the newest of the three names and also the most descriptive, so it's clear that at least part of the reason is that the original names were bad.)
You can think of Git's index as your proposed next commit. When you first check out some particular commit with git checkout or git switch, Git copies the entire commit into the index / staging-area. So now the proposed next commit, and the current commit, and your working tree, all have the same files. There are therefore three copies of each file lying around:
- the unchangeable, saved-forever copy in the current commit, which is totally safe from changes because it's in a commit;
- the proposed-next-commit copy in Git's index, which you can change; and
- the copy you can see and edit, in your working tree.
What all this means is that you do your work in your working tree, and then you run git add on any files you updated. You can run it on every file (git add .) or tell Git to find updated files (git add -u) if you like: these tell Git figure out what I changed, more or less. Or you can just run it directly on a file you just changed. What git add is doing here is copying the updated, working-tree copy of the file back into Git's index. This changes the proposed next commit.
When you run git status:
- Git compares the current commit (
HEAD) to the index / staging-area. Any differences between these two are staged for commit, i.e., this is what's different between HEAD and the proposed next commit. The new commit will still be a full snapshot of every file, but only some files are changed.
- Separately, Git compares the index / staging-area to your working tree. Any differences between these two are
not staged for commit. That is, you could run git add on these changes, to update the proposed-next-commit copy of the file, which might change the output from the earlier HEAD-vs-index diff.
When you run git commit, Git just packages up whatever is in Git's index at that time and uses that to make the new commit.
A branch name like master just helps Git find some specific commit. When you're on master and run git commit, Git:
- packages up the files in its index;
- adds your name, email address, etc., to make a commit out of all of this;
- writes out the new commit; and
- makes the name
master identify the new commit.
The new commit links back to what was the last commit, before.
A mental image of commits
What all this means is that we can draw what's happening, which I find very helpful. Let's say we start with a small repository with just a few commits, and just one branch name, master or main, that identifies the last commit in this repository:
... <-F <-G <-H <--master
Each commit has a number, a big ugly hexadecimal number, that is unique to that one particular commit. We call this number a hash ID. Making a commit assigns the new commit a hash ID. No other commit, in any Git repository—yours or anyone else's—can ever have that hash ID. That's why the number is so big and so ugly that no one can deal with it.
Instead of dealing with the numbers, then, I've used a single uppercase letter to stand in for the numbers. Here H is the hash ID of the last commit in branch master. Commit H has a permanent, safely-saved snapshot of every file. It also has some metadata—some stuff to say that you made it (if you did make it), when you made it, your log message, and so on. Inside that metadata, Git has added the raw hash ID of earlier commit G.
We say that commit H points to earlier commit G. Commit G, of course, has a snapshot and metadata. The metadata tells us who made G, when, and why—their log message—and has in it the hash ID of still-earlier commit F. So G points to F.
Commit F, of course, has all this same stuff, so it points back to some still-earlier commit, and so on. This repeats all the way through history—which is nothing but these commits—until we get to the very first commit ever. It can't point backwards to some earlier commit, so it just doesn't, and that's how Git knows that history ends (er, starts?) here.
Note that in all of this, Git works backwards. It has to, because branch names find the last commit. The last commit finds an earlier commit, which finds an even-earlier commit, and so on.
Note that the branch name points to the last commit. To add a new commit, then, we just take this string of commits:
...--F--G--H <-- master
and create a new commit I that points back to existing commit H:
...--F--G--H
\
I
If we're on branch master when we do this, that tells Git: Now that we have new commit I, make the name master point to I, which means we get this:
...--F--G--H
\
I <-- master
and now there's no reason to draw I on a separate line:
...--F--G--H--I <-- master
means the same thing.
git fetch
What git fetch does is to call up some other Git and get commits from that other Git. To call up some other Git, we need a URL: ssh://git@github.com/user/repo.git, or https://github.com/user/repo.git, or whatever. We have Git save that URL for us, under a name like origin, so that we don't have to keep typing it in all the time.
We then run:
git fetch origin
or even:
git fetch origin master
This has our Git call up their Git, over at the URL saved under the name origin. Their Git lists out, for our Git, all of their branches (and tags and other stuff).1 Our Git then picks out any interesting ones—they're all interesting, unless we used the second form where we said only pay attention to their master—and checks with their Git to see if they have any commits that they have, that we want and don't have.
(This involves using the commit numbers, and is why the commit numbers must be unique. But that's a topic for another discussion.)
In general, Git is greedy: any commit they have that we don't, we want! Our Git grabs all of their new-to-us commits from them, using the fetch protocol. Of course, all our commits, we already have, and all the commits we got from them earlier, we already have. Our Git and their Git talk and our Git figures out a good way to get just the new stuff, avoiding re-downloading any files even in new commits if we already have those files. (That is, the fetch goes fast and doesn't fetch stuff we already have, even inside new commits. That, too, is a topic for another discussion, though.)
Last, once we have these new commits, our Git updates some names that help our Git keep track of these new commits. These are our remote-tracking names,2 of the form origin/master for instance.
Suppose that, while we were making commit I, they acquired some new commit J. So before we do anything—make a commit or run git fetch—we have, in our repository, this:
...--G--H <-- master, origin/master
We make our new commit and have:
...--G--H <-- origin/master
\
I <-- master
(There's a reason now to put I and master on a separate line, so that we can have origin/master still point to H.)
If we now run git fetch, we find they have a new commit, which we'll get and draw as J:
J <-- origin/master
/
...--G--H
\
I <-- master
Note that commits up through H are on both of these chains; it's only I and J that diverge. We could draw either I or J on the same line as H here, too: I just chose to draw origin/master on a separate line this time, to emphasize the shared nature of commits up through and including H.
Note that all of these commits are read-only, and none of them are our working-tree files. If we have commit I checked out, that's fine: that means commit I is our current commit, and that Git's index and our working tree match commit I, or are getting ready for a new commit, or whatever. Adding commit J to the repository via git fetch has no effect on any work we're doing right now. The git fetch is quite safe. We can do this part any time.
1In a repository with a lot of branches and tags, this can take a lot of time and involve megabytes of data, just for the listing, so the git fetch origin master version can, in modern Git, limit the listing and speed this up. This doesn't matter for most repositories, which mostly just have maybe a dozen branches and a few hundred or thousand tags; it's repositories like the one for Chrome, that have 50+ MiB of branch and tag names, where this is a problem.
2Git calls these remote-tracking branch names. They're our Git's memory of their Git's branch names. I find the word branch redundant here, so I just use the phrase remote-tracking names. Git has lots of kinds of names: branch names, tag names, notes names, refs/stash, names it uses temporarily during git bisect, and so on. Remote-tracking names are just another kind of name. Each name holds just one hash ID—that's how they all work, regardless of what kind of name they are.
It's the second command that messes with our working tree files
Now that we have, say:
J <-- origin/master
/
...--G--H
\
I <-- master (HEAD)
we can run some second Git command, like git pull would do. This second command will mess with our files—the ones in our working tree.
Whether we choose git merge or git rebase, both commands need to be able to overwrite our working tree. So now we need, for sure, to run git status and make sure it says that everything is "clean".
The command we choose—merge vs rebase—and exactly what it does, are interesting questions. There are already plenty of answers about them on StackOverflow though. Knowing that git pull runs git fetch—which is safe—and then a second command that's not "safe" in this same sense, will get you pretty far.