Sunshine
Sunshine

Reputation: 334

Add files to LAST commit Pushed on Github Branch

I've just pushed a commit onto Github but forgot to add 2 files to it

How do I take it back and add the files without making second commit ?

Does git commit --amend --no-edit work on a commit already pushed to remote ?

Upvotes: 0

Views: 552

Answers (1)

torek
torek

Reputation: 488501

The comments are correct: you must force-push the result. The reason is that git commit --amend does not actually change a commit, because commits cannot be changed (not even by Git itself). What git commit --amend does is kick one commit off the end of a branch while making a new and improved commit to put at the end of the branch instead. The other Git repository involved will refuse to kick their commit (your commit, really) off the end of their branch to match.

A picture is worth some words

A picture makes this much clearer, in my opinion. Let's draw commits like this:

<-H

These represent the commit itself—the internal Git object and all of its ancillary details—as an uppercase-letter-and-arrow. The actual commit holds two things: a snapshot of all files, and some metadata. Neither part can be changed at all.

The snapshot makes up an archive (much like a tar or rar or winzip archive). Unlike a standard archive, though, the files are in a special, weird, Git-ized form, where they can be shared (de-duplicated). Because they're read-only, it's safe for this commit to share, in storage, all the files that exactly match any file in any other commit. (In fact, they can even be shared within a single commit, if the contents match.)

The metadata part of a commit holds stuff like your name and email address—as seen in your user.name and user.email setting—and the date-and-time-stamps for when you made the commit. It holds your log message, where you write why you made the commit. And, it holds for Git the hash ID of the previous commit: that's our arrow coming out of commit H.

What makes Git work—well, one part of what makes Git work, at least—is that each commit has one of these arrows, pointing to the previous commit:

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

H "points to" (contains the raw hash ID of) earlier commit G. G, being a commit, has both snapshot and metadata, and points to still-earlier commit F. That commit has both snapshot and metadata, and so on.

This means that Git needs just the raw hash ID of the last commit in the chain. Git stores this last-commit-in-the-chain hash ID in a branch name such as develop or feature/tall or main or whatever:

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

The name main points to the last commit H. From there, Git can find all previous commits.

To add a new commit on to the chain, as we normally do, we first check out the desired commit-and-branch via the branch name:

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

Git extracts all the (read-only, compressed, Git-ified, and de-duplicated / shared) files from H so that we can see them and work on and with them. we do our work and git add our updates and run git commit, and Git packages up all the files into a new compressed/Git-ified/de-duplicated snapshot in a new commit I:

...--F--G--H   <-- main (HEAD)
            \
             I

Commit I points back to H because H is our current commit. (Commit I gets its unique hash ID during the writing-out operation; until then, we don't know what its hash ID will be, as that depends on the exact snapshot and the previous commit and the date and time and the log message: basically everything you're going to put in, some time in the future, but we don't know what or when until you've done it.)

As soon as commit I actually exists inside the repository, Git immediately write's I's new hash ID, whatever it turns out to be, into the current branch name. This means we now have:

...--F--G--H
            \
             I   <-- main (HEAD)

The name has changed. The new commit exists. No existing commit has been changed at all. (This is why commits only point backwards: H can point to G, because G existed when we made H. H can't point to I because until now, I did not exist, and we had no idea what hash ID it would get. But I can point to H, because H exists when we make I.)

There's no reason to draw the bend in the graph any more, so we can draw this as:

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

and fit it all on one line. (Using git log --graph, you'll see Git draws things vertically, with newer commits towards the top, instead of horizontally with newer commits towards the right. Git also doesn't use single uppercase letters, of course, as that would limit us to 26 or so commits, depending on whose alphabet we use.)

What git commit --amend does is simple: it does not change the existing commits (it can't). Instead, it attaches the new commit I to H's parent, like this:

...--F--G--H
         \
          I   <-- main (HEAD)

Git writes the new commit's hash ID into the name as usual. We can't draw this graph without putting in the kink, but we can draw it like this:

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

(The --no-edit option tells Git to copy the original commit message, and don't bring up the editor to let you change it. The default is to copy the message but do bring up the editor.)

Why --force or --force-with-lease or similar

We now get to the interesting thing, which is why a force-push is required.

Whenever we make new commits, they exist only in our own Git repository. But eventually we might want to give these commits to someone else. So we have our Git call up some other Git software, somewhere, and send them our new commits—the ones we have that they don't, that they'll need. After we've sent these commits, we have to ask or tell them to create or update a branch name in their Git repository.

That is, they may well have:

...--F--G--H   <-- main [in their Git repository]

in their repository. We've added new commit I; they don't have it.

If we've added I to the end of H:

...--F--G--H--I   <-- main [in our Git repository]

we'll send them I and then ask them to change their main to point to I. They may check for permissions first (this is outside the Git code proper as it has no authentication model, but is done by hosting providers as an obviously-necessary add-on) and in general they'll say okay to this, because I simply adds on. Commit H remains reachable.

But if we've made new commit I have commit G as its parent:

          H
         /
...--F--G--I   <-- main [in our Git repository]

and we ask them, politely, to make their name main point to commit I, they will refuse, on the grounds that if they did, they'd "lose" their commit H. Git finds commits by starting from names, like main, and working backwards, and new commit I doesn't find commit H. Once the commit is "lost", it might actually get removed entirely. (Whether and when this happens is another, much more complicated question.) So they just say no, I won't do that because it would lose commits, which in Git jargon is reported as a non-fast-forward error.

The thing is, we want them to drop commit H. Commit I is our new-and-improved replacement for commit H. To get them to understand that we do in fact mean replace commit H with new and improved commit I, we set the force flag on our git push operation.

There are several different ways to set the force flag. One is unconditional, and it just turns the git push request into a command: Here are some commits. And now: I command you, set your branch name _______ to hash ID _______!

The reason to use --force-with-lease is that it offers a bit of safety. Here, instead of a raw set your branch command, we have a conditional: I believe your name _______ is set to hash ID _______. If so, set it to _________ instead, even if that's a non-fast-forward! Let me know what happened. Our Git gets the expected hash ID from our origin/main (assuming git push origin and other reasonable assumptions). If we're in sync with them when we do this git push --force-with-lease origin main, the hash IDs will match up and, if we have the right permissions, they will obey our command and set their main to point to commit I, just as we want them to.

Upvotes: 2

Related Questions