1

I am using github and have only two branches, one for development (dev) and one master. I recently created the dev branch from master, but have since made a few pushes to (remote) master, and have a few unstaged changes in (local) master, so the remote dev branch is behind a few commits. (And the local dev branch is still non-existent, AFAICT.)

I suppose the pictorial diagram would be something like this:

        c  (remote) branch 'dev' 
       /         
  a---b---d---e--*  (remote) branch 'master' (refers to commit 'e')
                     (local) branch 'master' (refers to unstaged changes '*') 
                               ^
                               |
                              HEAD (refers to branch 'master')

Question:

How do I synchronize my outdated (remote) dev branch with my master branch?
(And getting the unstaged changes into the (remote) dev branch.)


I understand there are 2 options to do this:

  1. using the github web interface
  2. using git from command line.

So it would be great to understand how to do both.

I've read dozens of SO answers (including this, however it seem that every answer is different and I'm not able to get this straight in my head. 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.

BTW, the web interface has two Pull Request buttons, one for each branch. (Which one is the right one to use, in my case?)

enter image description here enter image description here


My current status is:

## I started this with:
## git clone https://github.com/nobody/repo.git
## I then edited, pushed some files and created 'dev' branch via GH web.
## Finally I edited some more files, so now I have:

$ git status

On branch master
Your branch is up-to-date with 'origin/master'.
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

        modified:   CHANGELOG.md
        modified:   README.md

$ git branch -a

* master
  remotes/origin/HEAD -> origin/master
  remotes/origin/dev
  remotes/origin/master

PS. For some reason, when Googling my question, I only get results for "syncing a fork", which clearly is not the same thing as what I am asking about here.

not2qubit
  • 14,531
  • 8
  • 95
  • 135

1 Answers1

2

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.)

torek
  • 448,244
  • 59
  • 642
  • 775
  • Wow! That was one great educational explanation of what's going on! Thank you for taking the time for this thorough explanation. However, although much more clear now, I still fail to see: (a) what I need to do to save what I've already got (the un-staged stuff), (b) what commands I need to do to sync `dev` to `master` so that I can start using *dev* instead of *master* to develop on? Perhaps it is easier to just delete *dev* and create a new one? But I wanted to avoid that. (c) How to ensure that the `dev` history remain continuous in the future. – not2qubit Mar 29 '18 at 18:49
  • Ah, I thought (based on your good graph drawing) that you were further along in some Git tutorial. :-) You should read up on the difference between the *work-tree* (where files are in their normal everyday form) and the *index* aka *staging area* (where Git has the files stored in special Git form, but you can replace them at any time). Running `git commit` turns the index into a new commit, freezing the stored special-Git-form files, so that overwriting the index copy doesn't lose the saved for-(mostly-unless-deleted)-ever committed version. – torek Mar 29 '18 at 18:57
  • To create a branch named `dev` in your own local repository, there are two main Git commands: `git branch`, and `git checkout -b`. The details are a bit long for a comment, unfortunately. The key is that branch names are just pointers, pointing to existing commits. – torek Mar 29 '18 at 18:59
  • Ultimately, if you have a commit—or a whole chain of commits—whose position in the graph is "wrong", you are forced to *copy* the commits to new ones. This is what `git rebase` really does. There are a bunch of side issues that are consequences of this, which make some argue that no one should ever rebase. – torek Mar 29 '18 at 19:26
  • I can only agree with @torek. While this question is nicely formatted and well phrased I think **a lot** of your questions could be answered by simply doing a tutorial on git. Personally I can recommend the [Git Book](https://git-scm.com/book/en/v2). After reading that you will probably be able to answer questions on git to your colleagues. – Sascha Wolf Mar 29 '18 at 20:59