I think the problem is that when I'm using command line, I'm doing something on a local clone, rather than on the remote, unlike on the web interface.
That's correct.
It's important to remember that Git is a distributed version control system, and as such—and/or perhaps through socialist impulses1—Git doesn't believe1 that any one Git repository is superior to any other Git repository. In particular, why should the Git on GitHub with a repository with a dev
and a master
be any better than your Git on your machine working with a repository with a master
?
Note that if you made your GitHub repository by using the "fork a repository" button on the GitHub web interface, your GitHub repository is itself a clone of a third repository. The GitHub web interface "pull request" buttons are meant for delivering a request (via email and/or web) to whoever maintains this third repository, to take commits out of your GitHub repository. This all gets very confusing, and it might help to name each repository.2 Let's name your own repository Fred, and call the one on GitHub Gertie.
GitHub tries to hide a lot of complexity, but in my opinion, this just makes everything even more confusing. Ignore all the webby buttons, at least at first.
The next key is to realize that while each separate repository has its own names—Fred has Fred's master
and Gertie has Gertie's master
—they all share the actual commits, or at least, those commits that they have. Git identifies each commit by a unique hash ID—these look like 0afbf6caa5b16dcfa3074982e5b48e27d452dbbb
, though you can shorten them to something less scary as long as it's still unique.3 If some particular commit's hash ID is 0afbf6c...
, you either have the thing with that ID in your repository, in which case you have the commit too, or you don't.
Meanwhile, these names—Fred's master
and Gertie's master
—serve to identify one particular hash ID. If both Fred and Gertie have the commit, they can both have their master
identify that hash ID. If one of the two doesn't have the commit, though, they can't have the name point to that commit. A repository must have the commit in order for the name to point to it (store its ID)!
1Don't anthropomorphize computers, they hate that.
2As long as you don't name them all Bruce. Or is that as long as you do name them all Bruce?
3The problem is, how do you know how short you can go? Git has its own internal code to figure that out, but that doesn't help us poor humans.
Transferring commits
There are two operations that transfer commits (and all the other objects that go with them) from one Git repository to another. These are git fetch
and git push
. They're actually pretty similar; they're just named after the direction of transfer, as seen from whichever Git it is that starts the transferring. You git fetch
commits from them to you, and you git push
commits from you to them.
When you're doing this transferring, there are two parts to the job:
- Identify which commits to transfer (by their big ugly hash IDs).
- Set some name(s) on the end that gets commits, so as to remember the big ugly hash IDs.
This second step is where things are not symmetric.
In either case, though, you start by running git fetch
or git push
. This tells your Git (Fred) to call up the other Git (Gertie). Your Git, Fred, then asks Gertie what she has, or offers to send things to Gertie. They exchange information about the commit hash IDs and figure out who's missing what, and then send the objects. This gets the appropriate commits—and with them, any associated stuff like file names and contents—into Fred or into Gertie as needed.
Finally, whoever is sending also sends a name. So if you're doing git push
, you have Fred send Gertie a name, like dev
or master
, as part of a request: Please set your dev
or your master
to this hash ID. It's up to Gertie, following rules set up by GitHub, to decide whether to allow this operation. If she says no, you can try using a git push --force
, which changes this from "polite request" to "command", but Gertie can still say no.
In the fetch
direction, things are different. Gertie sends Fred all her commits (that Fred doesn't have yet), along with all of her branch names. Fred then takes her names—her dev
and her master
—and sets his origin/dev
and origin/master
. That way, Fred does not disturb Fred's dev
(if there is one), nor touch Fred's master
. And that's what these remote-tracking names, the ones that start with origin/
, are about: they're just Fred's way of remembering what Fred saw on Gertie, the last time Fred talked to Gertie.
Because of that last point, if you have Fred call up Gertie with git push
, and Fred offers Gertie some new commit(s) and asks her to set her master
or her dev
and she accepts, Fred will update Fred's origin/master
or origin/dev
as well. That's an opportunistic update: Fred knows Gertie took the commits, so now Gertie's name must identify the specific commit Fred suggested.
Putting these two pieces together
How do I synchronize my outdated (remote) dev branch with my master branch?
This means you want Gertie's dev
to identify the commit labeled e
in your diagram.
You can find anything that identifies commit e
on your end—such as your name master
, or your name origin/master
, or your name HEAD
, if all of those do in fact identify commit e
. (You can use git rev-parse name
to find out what commit name
identifies. Or, run git log --all --decorate --oneline --graph
: remember this as Git Log with A DOG. In fact, you probably should make an alias, git adog
, that runs git log --all --decorate --oneline --graph
. For Hysterical Raisins, my alias for this is git lola
.) Or, of course, you can use the raw hash ID. Then you would normally run:
git push origin <thing-that-identifies-e>:dev
The name or hash ID on the left side of the colon is anything that identifies the commit you want to be sure Gertie has, and that you want to ask Gertie to set something to. The name on the right—which must be a name—is the name you'll ask Gertie to set. You want her to set her dev
, so that's the name that must appear on the right.
When you do this, though, Gertie will see you asking her to move her dev
from where it is now, identifying commit c
, to identifying commit e
. She will refuse! This particular movement will cause Gertie to drop commit c
from her commit graph entirely, as she'll have no name for commit c
. That kind of operation—the one that loses commits—is a non fast forward, in Git terminology, and requires a "forced push".
Hence, what you'll really need is:
git push --force origin <thing-that-identifies-e>:dev
to turn the polite request ("do this thing that will lose commit c
forever") into a command ("yes, do it even though it will lose c
!"). Given the Git rules that GitHub uses by default, Gertie will then set her dev
to point to commit e
, and will lose commit c
entirely.
If you want Gertie to keep commit c
, despite having her move her dev
so that her dev
identifies commit e
, have her set a new name to point to c
, preferably before having her move her dev
. If not, well, removing commits is OK, as long as you mean it and know what you're doing. Note that you currently have a name for c
, but that name is origin/dev
, and as soon as Gertie says "OK I've changed my dev
", your Git—Fred—will update your origin/dev
correspondingly, so you too will lose commit c
.4
4You'll actually keep it for at least another 30 days, through what Git calls reflogs. Servers like GitHub usually don't keep reflogs, though, so they tend to shed unreachable commits immediately.
A side note: your master
cannot point to anything unstaged
(local) branch 'master' (refers to unstaged changes '*')
It's worth remembering that anything in your work-tree is not in Git at all, and anything you've staged but not yet committed is not saved anywhere permanent. Your branch name master
must point to a commit. So your local master
probably points not to *
, but rather to e
.
Staging a file really means copying it into what Git calls the index, overwriting the version that was there before. The index, also called the staging area and sometimes the cache, is where you build up what git commit
will write as the next commit. Until it's actually committed, though, it's just "the index" and it does not have a commit hash ID. It's changeable—commits are read-only and hence entirely unchangeable—so it has no ID and no branch name can hold the commit ID it doesn't have yet.
(The actual commit ID it gets, when you make the commit, depends on the time at which you make the commit, among other things.)