mas
mas

Reputation: 1255

Git branching on its own

I have been using git in a repository for which I am the solo contributor. The repository is hosted on Github. For some reason, git occasionally seems to branch on Github without me changing a thing. I make changes to this repo on one computer only and don't modify a thing on Github. Yet, as you can see in this shortened selection of git log --oneline --decorate --graph --all, I seem to have to do this randomly. The commit 271056c is duplicated at cfacddf on Github.

What could cause this? Is it a bug?

Possible theory I now consider: could it be the result of git commit --amend?

Output of git log --oneline --decorate --graph --all:

*   d030d88 (HEAD -> master, origin/master, origin/HEAD) Merge branch 'master' of ***
|\
| * cfacddf Fixed a few things:
* | 798afef modified:   tasks.txt
* | 2c19ea0 Paginated:
* | 271056c Fixed a few things:
|/
* 63a429c Added lock icon to annotation headers

Output of git log:

commit d030d8820456b0f129d2d3e5167ed88f87ebe028
Merge: 798afef cfacddf
Author: ***
Date:   Wed Oct 3 16:49:00 2018 -0400

    Merge branch 'master' of https://github.com/malan88/icc

commit 798afefabe24027a955853771072755c08318b47
Author: Michael Alan <[email protected]>
Date:   Wed Oct 3 16:48:54 2018 -0400

    modified:   tasks.txt

commit 2c19ea0b735d148f0057852c196254c4258c2c6b
Author: ***
Date:   Wed Oct 3 16:42:20 2018 -0400

    Paginated:

    - Author index
    - Book index
    - User Annotations
    - Tag index

commit 271056c1c34444dbb3b8c2b6a67efae67f27b44b
Author: ***
Date:   Wed Oct 3 15:24:14 2018 -0400

    Fixed a few things:

    - hash_id bug
    - line_number_form on read.html bug
    - horribly inefficient permissions check on line edits on read.html
    - Added lock change mechanism for when admin doesn't want to edit
        annotation
    - cleaned up some bad whitespace
    - Bug that didn't include query parameters in next parameter (e.g.
    pagenumbers, etc)

commit cfacddfb5395e0fa3676fbc1e19457c45c7c3529
Author: ***
Date:   Wed Oct 3 15:24:14 2018 -0400

    Fixed a few things:

    - hash_id bug
    - line_number_form on read.html bug
    - horribly inefficient permissions check on line edits on read.html
    - Added lock change mechanism for when admin doesn't want to edit
        annotation
    - cleaned up some bad whitespace
    - Bug that didn't include query parameters in next parameter (e.g.
    pagenumbers, etc)

commit cfacddfb5395e0fa3676fbc1e19457c45c7c3529
Author: ***
Date:   Wed Oct 3 15:24:14 2018 -0400

    Fixed a few things:

    - hash_id bug
    - line_number_form on read.html bug
    - horribly inefficient permissions check on line edits on read.html
    - Added lock change mechanism for when admin doesn't want to edit
        annotation
    - cleaned up some bad whitespace

commit 63a429c828cec06fa30e4994f3912fab387c7a4c
Author: ***
Date:   Wed Oct 3 15:21:58 2018 -0400

    Added lock icon to annotation headers

Upvotes: 0

Views: 46

Answers (1)

torek
torek

Reputation: 490068

Let's start with this:

Possible theory I now consider: could it be the result of git commit --amend?

Yes.

Now let's go back to this, because there is a hidden assumption here, which is a key factor:

I have been using git in a repository for which I am the solo contributor. The repository is hosted on GitHub.

This isn't really right. What you mean here is that there is a repository on GitHub. It's not the repository: it is one of several, or many. In this case, it is one of two. The other one is on your own machine!

For a proper explanation, see any of many of my longer answers. See also the web site Think Like (a) Git, which has a lot of important background. But for short, remember that there are two repositories here: yours, and the Git repository on GitHub.

When you make a commit, it's uniquely identified by its hash ID—a big ugly string of hexadecimal digits, such as cfacddfb5395e0fa3676fbc1e19457c45c7c3529. That's what your Git stores in your branch name master. Each commit also stores the hash ID of its previous, or parent, commit(s). A merge commit is a commit with at least two parents. When a branch name, or a commit, stores the hash ID of a commit, we say that this name or this commit points to the targeted commit.

We can draw such things as (horizontally oriented) graphs with the latest commit towards the right:

...<-F  <-G  <-H   <--master

Git attaches the name HEAD to a branch name, so that if you have multiple branch names, it knows which is your current branch. The arrows inside commits cannot change—nothing inside any commit can ever change—so we don't really need to draw the internal arrows:

...--F--G--H   <-- master (HEAD)

When you run git commit, the normal procedure is to freeze whatever is in your index right now into a new snapshot, make the snapshot's parent be the current commit, using the pointer coming from the branch name to which HEAD is attached:

...--F--G--H--I

and then Git puts the hash ID of the new commit into the current branch name, so that it now points to the new commit:

...--F--G--H--I   <-- master (HEAD)

How git commit --amend works

We just saw that git commit:

  1. freezes the current index into a snapshot;
  2. writes out a new commit with the current commit as its parent;
  3. writes the new commit's hash into the current branch.

All that --amend does is to change to change step 2: instead of using the current commit as the new commit's parent, --amend uses the current commit's parents as the new commit's parents. (If there are multiple parents—if the current commit is a merge—the new commit gets all of the current commit's parents. If it's a normal single-parent non-merge commit, the new commit gets one new parent.) So instead of going from ...-F-G-H to ...-F-G-H-I, we get:

          H
         /
...--F--G--I   <-- master (HEAD)

That is, --amend shoves the current commit out of the way.

This works fine if you haven't pushed

If you never pushed commit H anywhere, amending it now works fine. The new commit I takes over; commit H vanishes, never to be seen again. (You can find it in your own repository for a while—at least 30 days by default, until the reflog entries that remember it expire. But it does not show up in normal git log output.)

But it fails if you have

But if you have sent commit H to another Git—such as the repository you are storing on GitHub—then any time you contact them, they'll offer you commit H back again, as being an important commit on their master branch. That is what happened in this case.

You had pushed commit H to GitHub. You then had your Git shove your H out of the way, as above, but GitHub still had H. Your Git remembers this, though if your Git had forgotten or you erased its memory, your Git would be reminded the next time you had your Git call up the Git on GitHub. So now you have this:

          H   <-- origin/master
         /
...--F--G--I   <-- master (HEAD)

From now on, as you work in your repository, commit H remains visible. You made commits J (271056c1c34444dbb3b8c2b6a67efae67f27b44b) and K (798afefabe24027a955853771072755c08318b47):

          H   <-- origin/master
         /
...--F--G--I--J--K   <-- master (HEAD)

At this point, you ran git pull, which had your Git call up the Git at GitHub and get any new commits from GitHub (there were none) and update your origin/master to match their master (still pointing to commit H). Your git pull then had your Git merge their commit H with your commit K, making a new two-parent merge commit L:

          H_  <-- origin/master
         /  `----__
...--F--G--I--J--K-=L   <-- master (HEAD)

and that's what you see now. (Well, now you also asked the GitHub Git to set their master to L, which they did, so your own Git repository's origin/master goes to L too.)

To fix it, you must get all other Gits to forget H

After you "amend" H, you must find all other Git repositories that had H and convince them to use I instead. In this case, there is only one other Git involved: the one at GitHub. You can run:

git push origin master

which has your Git call up the Git at GitHub and politely request that they change their master to point to your replacement commit I. But if you do this, you will find that they say no, I won't do that, because that would lose commit H. The form of this complaint is:

! [rejected]        master -> master (non-fast-forward)

In this case, you know what the problem is, and that you want them to lose (forget) commit H. So you can use git push --force, or the fancier variant, git push --force-with-lease.

These convert your request (please update your master) to a command: update your master! GitHub could still refuse (and will if you are not authorized to do this), but in general, they will obey the command where they rejected the request. The --force-with-lease option works as a safety check: instead of just saying do this anyway, your Git says to theirs: I think you have your master set to <H's hash>. If so, replace it. If not, let me know. This is what you might use on a shared repository, in case other people are also pushing to the GitHub master.

Upvotes: 4

Related Questions