git agony part 2: what does it mean when git won't commit, pull or push a file?

Viewed 838

The basic git ontology remains illusive and deeply frustrating.

We have a master repository at github.com. Let's say we have 1 file, an R file, in that repository.

  1. I clone.

  2. My collaborator clones.

  3. She edits. She commits. She pushes.

  4. I edit. I commit. I push. I can't as she has already pushed.

  5. So I pull. I merge the code. I get it working OK. I push again. I can't.

! [rejected] HEAD -> master (non-fast-forward) error: failed to push some refs to 'https://github.com/PeterLeopold/myRepo' hint: Updates were rejected because the tip of your current branch is behind hint: its remote counterpart. Integrate the remote changes (e.g. hint: 'git pull ...') before pushing again. hint: See the 'Note about fast-forwards' in 'git push --help' for details.

  1. I pull again. I can't.

error: You have not concluded your merge (MERGE_HEAD exists). hint: Please, commit your changes before merging. fatal: Exiting because of unfinished merge.

  1. I commit again. I can't.

error: Committing is not possible because you have unmerged files. hint: Fix them up in the work tree, and then use 'git add/rm ' hint: as appropriate to mark resolution and make a commit. fatal: Exiting because of an unresolved conflict.

U ksUpgrade.R

[The conflict is perfectly well resolved!]

So. Now I have perfectly working merged code and no way to send it back to the repository. So I email it to my collaborator . . .

What am I missing? Lemme guess . . . git is still in alpha?

P.S. In a previous question I learned that 100% of the collateral work product (files, figures, tables) cannot be versioned/shared by github as these files are generated each time the code is executed and git must always try to merge bytes and cannot simply layer them (unless they are marked binary, which they aren't, so to maximize productivity one must deceive git. Yes, deceiving git is a pro-tip and it is documented!)

My collaborator and I are not yet using branches or forks. Those failure modes remain around the corner.

(This experience is vaguely reminiscent of fundamentally unresolvable class-path incompatibilities in java.)

Tell me it gets better . . . :<


Here is the "git status" output. You'll notice I've been busy trying to delete every file that isn't an R or Rmd file. I've largely but not perfectly succeeded.

Changes not staged for commit:

(use "git add/rm ..." to update what will be committed)

(use "git restore ..." to discard changes in working directory)

   deleted:    README.html

   deleted:    README.md

   deleted:    SourceRawData.csv

   modified:   age_dcu.png

   modified:   bar.af.png

   modified:   bar.all.png

   modified:   bar.ca.png

   deleted:    ksPrepForSSSSS.csv

   deleted:    prepForTTTTT.csv

   deleted:    senxxx.csv

   deleted:    timaaaa.csv

Untracked files:

(use "git add ..." to include in what will be committed)

   tables for manuscript_2.docx

   tables for manuscript_3.docx

   ~$bles for manuscript_2.docx

   ~$bles for manuscript_3.docx
2 Answers

You can try the following steps :

  • Note the <commit-id> of the commit you made in point 4 using git log.
  • Reset the state of the repository to that commit git reset --hard <commit-id> (you will lose all uncommitted changes)
  • Perform fetch operation in case there are any new changes
  • Perform merge operation (probably using --no-commit --no-ff)
  • Commit the changes
  • Push to remote

Distributed, collaborative work is hard.

Git tries to make it easy. Git fails. Don't blame Git ... well, do blame it some, because its attempt to make it easy has a bunch of flaws. :-) But here's where things went wrong:

So I pull. I merge the code. I get it working OK. I push again. I can't.

This is because you have not yet committed your merge.

You say you've resolved them (and I'll believe you) but now you have to tell Git that you have resolved them. This is a separate step. You must use git add or git rm to tell Git about this.

I have a very long answer below; read whatever you like from it.

What to know about commits

You need a good mental model so you don't get lost. Start with this: Git is not really about files, nor branches. Git is really about commits. To get stuff done with Git, you need commits. A repository is mostly a big collection of commits. So you need to know: what, exactly, is a commit?

A commit comes in two parts:

  • It holds a snapshot of all files. This is not changes to files, but the actual files themselves. The snapshot is the main data of a commit.

  • And, it holds some metadata, or information about the commit: who made it and when, for instance.

These parts are all completely, totally read-only, frozen for all time. Meanwhile, to find a commit, you—or at least Git—need to know its name.

The name of a commit is some big ugly hash ID such as 9fadedd637b312089337d73c3ed8447e9f0aa775. These things are quite useless for human consumption; at most, you can cut-and-paste one, or use an abbreviated one when necessary. But it's important to remember that these are Git's true names for each commit. Every commit gets a unique one: once you make a new commit, no other commit, anywhere, any time, can have that name, from then on. In a sense, that hash name was reserved to that commit even before you made it—but the hash depends on the exact second at which you made it, so it's basically impossible to predict a future ID.

A key piece of metadata inside each commit is the raw hash ID of its previous commit. This makes each commit sort of like a pearl on a chain, except that each pearl connects only backwards. Git can't record the hash ID of a future commit because it's impossible to predict, and once made, no commit can never change, so these links can only go backwards.

When we have something with a commit hash ID in it, we say that this thing points to the given commit. So each commit points back to its predecessor:

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

where I've used uppercase letters to stand in for real hash IDs. The last one in the chain is H, so H points to G, which points to F, and so on. If we follow the chain long enough, we'll eventually find the first commit ever made, which—being first—just doesn't point backwards after all.

What to know about branch names

There's actually a fair bit to know, but let's just start with the easy part. Each Git repository—each clone—has its own branch names. Each branch name just holds the hash ID of one commit.

Let's draw a simple chain of just three commits:

A--B--C   <-- master

The name master points to C; C points to B; and B points to A, which is the first commit, so there we stop. Meanwhile, I've gotten too lazy (well...) to draw the arrows between the commits. We know they go one way only—backwards—and that's sufficient; once they're made they can't change so we can just ignore the direction here when it's convenient. The arrow in a branch name, though, can change.

Let's add a second branch name now, dev for development. We'll make it point to C too, like this:

A--B--C   <-- master, dev

Now we need to know: which branch name are we using? Git remembers this by attaching a special name, HEAD, written in all uppercase like this,1 to one of the branch names. The name to which HEAD is attached is the current branch, and the commit to which that name points is the current commit. So whichever name we attach HEAD to here, the current commit will be C. Let's attach HEAD to dev:

A--B--C   <-- master, dev (HEAD)

Now, let's make a new commit, in the usual not-making-any-merges way (which you're already familiar with). This new commit will get some big ugly hash ID but we'll just call it D:

A--B--C   <-- master
       \
        D   <-- dev (HEAD)

The name HEAD is still attached to the name dev, but now the name dev points to the new commit D. The new commit points back to the old commit C as its parent, because we were using commit C when we made D.

When we do this, we make the branch name point to the last or latest commit. It's all automatic. The current branch name advances over the new commit. The old commits remain findable, by starting at the end and working backwards. That's what Git does all the time: it works backwards.


1On Windows and MacOS, you can often get away with typing head in lowercase. This is a bad habit, because (a) it doesn't usually work on Linux, where Git just doesn't understand it, and (b) it stops working when you start using git worktree add, in an even worse way than it doesn't work on Linux: it starts picking the wrong commit.


The index and your work-tree

It's time to talk about the index and your work-tree. We glossed right over this earlier, because you already know how to make a new commit like D, but it's important to get to all the details now, because we'll need them for merging.

The files that are inside a commit are frozen for all time. They literally cannot be changed. They're also stored in a special, compressed, Git-only format. This means that they're fine for archival, but quite useless for getting any actual work done. To do some work with the files inside a commit, we have to get Git to copy those files out, and turn them back into usable, ordinary files.

The usable files go in your work-tree, where you can see them and work on them. This gets us to the first point about files vs commits: The files inside a commit are not the files you can see and work on. You have to take those files out of a commit first.

The git checkout command does this. There's an easy—but wrong—way to describe how git checkout does this: we pick a commit, like C or D, using a branch name like master or dev. Git then extracts the files from that commit.

In theory, that's enough: we have one copy of all the committed files, in the commit, and another, usable copy of each file, in your work-tree. You could work on them, and then commit again and make a new commit from the files in your work-tree. That would be easy, and that's what some other version control systems, do. Alas, that's not what Git does.

Instead, Git keeps a third copy of each file, sort of in between the committed copy and the usable one.2 That in-between copy goes in Git's index. Git fills it in, when you first do a git checkout, from the commit you've selected. So there are three copies of each file at all times: the frozen one in the commit, the index's copy, and your work-tree copy that you can see and work with.

This name—"index"—is terrible. So Git often calls it the staging area these days, which describes how you can use it. The index copy of a file is in the frozen format, ready to go into the next commit. If you change the work-tree copy, you probably want to copy the work-tree copy back into the index, replacing the ready-to-freeze copy. That's what git add does: it copies from your work-tree into Git's index, making the file staged and therefore updated for committing.

The file was there before! Well, it was, that is, unless it wasn't—if it's a totally new file, it's not in HEAD now: it's only in the index and work-tree now.

So, a file is staged for commit if the index copy does not match the HEAD copy. If the file is totally new, of course that doesn't match; otherwise, it matches, or doesn't, in the obvious way.

Because your work-tree is a regular set of files and directories/folders on your computer, you can also create work-tree files that you don't copy into the index. A file that is in the work-tree, but not in the index, is an untracked file. Git will often complain about untracked files: git status will keep informing you that they exist. (You can make it shut up about this, but we'll leave that for later.)

This gets us to the second point about files and commits: The files that will go into the next commit are the files that are in Git's index, not the ones you can see in your work-tree. When you run git commit, Git just packages up whatever is in the index at that point. That's what becomes the next snapshot.

There are a lot of subtleties about the index and work-tree that we just won't cover at all here. Git's index is perhaps its trickiest part, especially because you can't really see it. You can't really see commits all that well either, but at least git log tells you about them. There's no user-facing command that tells you much about the index, except for git status ... and git status tells you about only by comparing things (see more about status below).


2Technically, what's in the index is a mode, a file name, and a reference to an internal Git blob object. But unless you start working with some of Git's internal commands, you can just think of the index as holding a separate copy.


Branches eventually diverge ... or not

When we had our example three-commit repository, we made a fourth commit on dev and got:

A--B--C   <-- master
       \
        D   <-- dev (HEAD)

We can now git checkout master:

A--B--C   <-- master (HEAD)
       \
        D   <-- dev

This removes commit D from the index-and-work-tree and puts commit C into them. Well, it does as long as we don't have uncommitted work. If we do, things get complicated. Assume it does, anyway.

Now we can make some changes, git add them—which you now know copies updated work-tree files back into the index, making them ready to commit—and git commit to make new commit E:

        E   <-- master (HEAD)
       /
A--B--C
       \
        D   <-- dev

When this goes on for a while, you get stuff that looks like this:

          o--o--o   <-- branch1
         /
...--o--o
         \
          o--o   <-- branch2

and so on. But sometimes you just have:

A--B--C   <-- master (HEAD)
       \
        D   <-- dev

In this kind of situation, where you have a branch name that points to a commit that's also part of an earlier branch—commit C is on both branches, which is another Git peculiarity—and there's no divergence, git merge dev will spot this case and won't actually do any merging. It just moves the name master forward, and checks out the other commit:

git merge dev

results in:

A--B--C
       \
        D   <-- master (HEAD), dev

(after which we can just draw them all on one line again).

Git calls this kind of non-merge a fast-forward merge. Sometimes, this is what you want. Sometimes it's not. Sometimes it's just not possible anyway.

There is no right answer to a question about when you want a fast-forward merge, and when you don't. But if you don't want one, and Git thinks it should, you can tell Git don't do a fast-forward, do a real merge. In any case, when branches have diverged—when you have, for instance:

          I--J   <-- branch1
         /
...--G--H
         \
          K--L   <-- branch2

you need to do a real merge.

Real merges

When Git does a real merge, it needs to overwrite its index and your work-tree. This means they must be "clean" (match the current commit) when you start, at least in the general case. If they are not clean, you will get an error and the merge won't even start, except for some special cases.

You already know now that the index holds the next proposed commit. But when you start a merge, Git expands the index. Instead of holding just one set of files, Git makes the index hold three sets of files. These three sets of files go in numbered staging slots within the index.

The merge is called a three-way merge, perhaps because it has three inputs. The three inputs are:

  • a merge base commit, which Git finds on its own;
  • your current commit, as found via HEAD and the current branch name; and
  • one other commit, which you select by your git merge command.

In this case, let's say you do a git checkout branch1; git merge branch2 so that you have:

          I--J   <-- branch1 (HEAD)
         /
...--G--H
         \
          K--L   <-- branch2

The merge base is the best shared commit: a commit that is on both branches, that's not too far away from each branch tip. The branch tips are commits J and L, and it's clear here that the best shared commit is therefore commit H.3

So what Git does now is, for each file in the snapshot for H, copy that file to slot #1 in the index. If the files are named README.md and bar.all.png, the index now has a README.md #1 and a bar.all.png #1. For each file in the snapshot for J, copy that file to slot #2 in the index. If J has the same two files, those go into README.md slot 2 and bar.all.png slot 2. Then Git reads the files from L into the index, to slot 3.

Now that there's a copy of each file from each commit in each index slot, Git starts resolving the files. The resolved copies go in "slot zero", which is the normal non-merge index state.

  • If all three copies match, any copy will do. Rip all three out of the three numbered slots and put one into slot zero. (The work-tree copy is fine, leave it alone.)

  • If your copy is different, but the base and theirs are the same, take your copy. Remove theirs and put yours into slot zero. (The work-tree copy is still fine because we used yours.)

  • If their copy is different, but the base and yours are the same, take their copy. (Replace the work-tree copy with theirs.)

  • If all three are different, Git actually has to work hard.

When the three copies in the three staging slots are all different, Git will actually try to merge the files. Whether and when Git can do this is a bit complicated. If Git thinks it has merged these correctly on its own, Git will write the resulting file to both your work-tree and its own slot-zero, and remove the other three copies. If Git thinks it hasn't merged these correctly, Git will leave its messy merge attempt in your work-tree—unless it thinks the file is binary, in which case it doesn't touch the file at all—and leave all three copies in the three numbered slots.

These numbered slots, if any are still in use, mark the merge as conflicted. Your job is to somehow—any way you like—come up with the right merged file and copy that into index slot zero, clearing out slots 1, 2, and 3. One easy way to do that is—at least usually—to edit the messy merge that's in your work-tree, then run git add, which copies to the index and does all the slot-resetting.

There are some other ways to get merge conflicts, which we will only touch on lightly. Suppose you removed a file, and they modified it, so that the base copy and their copy are different, but you just don't have a copy at all. This causes a merge conflict too, but this time, slot #2—the --ours copy—is just empty. You still have unmerged files, but now you have to merge this some other way. You're likely to still use git add, which still works the same way, or maybe git rm to remove the file from whatever slots it's in.

In any case, if you have a merge failure, this is your fate. You are in this unmerged state. You must complete, or else abort, the merge. Those are your only choices. You must resolve all the unresolved index entries. This is one reason why you need to know about the index.4 You can only commit after resolving all the conflicts.

Once you do have a merge commit, you have this:

          I--J
         /    \
...--G--H      M   <-- branch1 (HEAD)
         \    /
          K--L   <-- branch2

The new merge commit has a snapshot as usual—that's based on whatever you put in the index, in the normal slot zero position—and has two parents. It points back to existing commit J just like a normal commit would, but it also points back to commit L, the one you selected for merging.

You can continue adding normal (non-merge) commits:

          I--J
         /    \
...--G--H      M--N--O--P   <-- branch1 (HEAD)
         \    /
          K--L   <-- branch2

and the graph just grows normally, with the branch name continuing to point to the last commit in the branch. Note that commits K-L, which used to be only on branch2, are now on both branches, but commits I-J-M-N-O-P are all only on branch1: you can start at P and work backwards to all commits, following both links from M, but you cannot work backwards from L and reach M or J or I.


3In more tangled graphs, it's not always clear where the merge base is. In some cases, there is more than one "best" commit, and things get a bit complicated. We won't worry about those here.

4There are multiple reasons to know about the index, but the fact that you have to manipulate it, when merging, is perhaps the most important one. But untracked files can't be explained correctly without knowing about the index, either.


git fetch and git push

Git is distributed. This means that you have a repository full of commits, but so does someone else. In common setups, so do more than one other "someone else": there might be multiple contributors, and in any case, you and they may well have all agreed that there's some central repository that is the repository: that all other clones are mere shadows of this centralized store.

This isn't how Git works internally. In Git, every repository is a peer: there's no more-masterful or less-masterful Git repo. But it's easy to pretend that the Git repository over on, say, GitHub, is the "real one". There's nothing wrong with working that way. Just remember that the GitHub repository is just another clone, and works the same way as any Git clone, in terms of fetching and pushing and branch names and so on.

Now, again, every Git repository has its own branch names. What Gits share are commits. The commits have unique hash IDs, so each Git can tell whether it has all the commits that the other one has, or not.

When both you and someone else clone a central repository, you both start out with the same set of commits. Your Gits also take the central repository's branch names, and copy them to your own remote-tracking names like origin/master and origin/develop. These names are private to your own repositories too: it's just that every time you connect your Git to the central one, your Git can read out their branch names, and update your remote-tracking names.

Having made remote-tracking names, your Git repository now creates one branch name for you, typically master. This name points to the same commit as your origin/master, which your Git set up to point to the same commit, by hash ID, as their Git:

...--G--H   <-- master (HEAD), origin/master

Let's see what happens next.

  1. I clone [from the central Git].
  2. My collaborator clones [from the central Git].
  3. She edits. She commits. She pushes.

When she ran git commit, she got a new, unique hash ID, different from every other hash ID. We'll just call it I. Her Git updated her master:

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

Then she ran git push.

Her Git now calls up the central Git. Her Git says to that central Git: Hey, I've been told to offer you the commit with hash I. Its parent is H. Do you want I? They say: I haven't seen I, gimme! I do have H already, though. Just give me commit I.

They stick commit I into their repository, temporarily, with no name:

...--G--H   <-- master
         \
          I   [no name]

Remember, this is the view in the central repository. I've left out HEAD because we do not care what branch they have checked out, if any, and if it's GitHub, they don't have anything checked out at all.5 The last part of her git push is that her Git asks their Git: Please, if it's OK, set your master to point to I now.

They have no reason to object, and she has permission to make the request, so they do that. Their Git now has:

...--G--H--I   <-- master

and her Git updates her origin/master so that she has:

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

You, on the other hand, still have this:

...--G--H   <-- master (HEAD), origin/master

Your Git is not in touch with the central Git, so your Git has no idea that there is a new commit I.

I edit. I commit. I push.

You edit and commit, and make a new commit with a new unique hash ID. Let's draw that:

...--G--H   <-- origin/master
         \
          J   <-- master (HEAD)

You run git push, so your Git calls up the central Git and offers commit J (with parent H). They take J and put it in temporary quarantine:

...--G--H--I   <-- master
         \
          J   [no name]

Now your Git asks their Git: Please, if it's OK, set your master to point to commit J. But this time they say no! They say: If I do that, I'll lose the ability to find commit I. Gits work backwards. From I, it finds H, and then G and so on. From J, it can find H, but H only points backwards. Their Git can tell that if they moved their master to point to J, they'd lose commit I. So they just say no: not a fast-forward.

This fast-forward term matches the one we saw earlier with git merge operations that don't actually merge. Git needs the result of the change to a branch name, as proposed by git push, to be one of these fast-forward operations.

This is why your push was rejected. Now you need to take corrective action.


5Servers usually sport bare repositories, which have no work-tree. This is necessary because changing a branch name while someone's working on the branch messes with everything. Despite this, server bare repositories still have a HEAD, which controls a feature that few people care much about and we won't go into here.


About git pull

To fix things up, you first need to get your collaborator's new commit. That's a git fetch operation. You can run git fetch at this point—it's always safe—and your Git repository will adjust like this:

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

Your Git has added shared commit I to your collection, and updated your origin/* names to match theirs. Your branch names are all unchanged.

Now, though, you need to somehow combine what you did, in commit J, with what she did, in commit I. This combining step is—or can be, anyway—a plain old ordinary git merge.

You can run:

git merge origin/master

which tells your Git: Find the common merge base between my commit J, as found by my HEAD, and commit I, as found by origin/master. Then, put files from H into slot 1, files from J into slot 2, and files from I into slot 3. Then attempt to merge.

The git pull command is a convenience command that simply combines run git fetch with a second Git command, usually git merge. If Git is able to combine your work and her work on its own, Git will commit the result.

(Note: merging isn't your only option. You can also use git rebase. However, rebase is actually more complicated than merging, and this answer is already too long...)

When, exactly, can Git combine work?

To answer this question, go back to the fact that commits are snapshots. We have:

       J   <-- master (HEAD)
      /
...--H
      \
       I   <-- origin/master

Git has to compare the snapshot in H, the merge base, to the snapshot in J, your commit. Whatever you changed, that's the kind of thing Git has to do to the files in commit H. But Git also has to compare the snapshot in H to the snapshot in I: her commit. Whatever she changed, that's the kind of thing Git has to do to the files in commit H.

When you've made wholesale changes in your commit—such as "remove a file"—and she's made piecemeal ones, like "modify the same file", Git won't be able to combine these at all. The only changes Git can combine are the ones that are made on lines (or in totally separate files, of course). Git is very line-oriented. Git will run the internal equivalent of:

git diff --find-renames <hash-of-H> <hash-of-J>    # what you changed
git diff --find-renames <hash-of-H> <hash-of-I>    # what she changed

If these diffs show that she changed line 47 of bar.all.png, you'd better not have changed line 47, or Git won't know how to combine these.

Of course, *.png files don't have lines. So Git will never be able to combine these! Git is the wrong tool here. If you have an image-merging tool, consider extracting all three PNG files from the three index slots and running the image-merging tool on those three files.

Observing the commit graph

One key to getting anything done in Git is to observe the graph of commits, so that you can see where your commits diverge from someone else's commits, and where they join up. To view the graph, consider fancy graphical viewers (which may draw nice ones), or use git log --decorate --oneline --graph. The D, O, and G options give you output that looks like this:

*   9fadedd637 (HEAD -> master, origin/master, origin/HEAD) Merge branch 'ds/default-pack-use-sparse-to-true'
|\  
| * 2d657ab95f pack-objects: flip the use of GIT_TEST_PACK_SPARSE
| * de3a864114 config: set pack.useSparse=true by default
* | 3bab5d5625 The second batch post 2.26 cycle
[snip]

The current commit, from HEAD, is at the top. It's a merge commit so it has two links to previous commits. Git puts each commit on a line by itself—one line, due to the --oneline option—and the graph drawing is pretty crude, but usable.

More about git status

The git status command has two different modes that it will choose automatically depending on whether you have unmerged files.

If you have no unmerged files—if everything in the index is at stage zero—the output from git status will list just the staged files and the unstaged files. If you do have unmerged files, though, Git will show the unmerged files instead of any unstaged ones.

When everything is at stage zero, git status runs, internally, two quick git diffs:

  • The first compares HEAD vs index. Whatever is different here, Git says staged for commit. For files that match, Git says nothing.
  • The second compares the index vs your work-tree. Whatever is different here, Git says not staged for commit. For files that match, Git says nothing.

So this lets you view what's in the index, sort of: you see how it compares, rather than what's actually in it.

About git stash

Another answer recommends git stash. I generally avoid git stash. What it does is make commits, then use git reset --hard to erase whatever you have done in the index and your work-tree (though what you did is saved in the commits it made). The commits it makes are not on any branch, and are hard to see and use, even by Git standards. So I think it's better just to make ordinary commits.

Note that because Git makes commits from the index, git stash becomes helpless in the face of a conflicted merge. Git literally cannot commit a conflicted merge. It probably should be able to—there should be a way to suspend and resume a merge—but it can't.

Related