likejudo
likejudo

Reputation: 3736

Git - if I delete a commit from a parent branch, will it also disappear from child branch that was created while commit was on the parent branch?

I have added a feature on a parent branch. It has been committed and pushed to origin. It has since been decided to not implement this feature just yet. So I want to delete the commit but to save the work for future. How should I do it?

I am thinking of creating a child branch from the parent branch so it will contain the commit and then delete the commit from the parent branch.

But if I delete the commit from the parent branch, will it also disappear from the child branch?

Upvotes: 0

Views: 1322

Answers (2)

torek
torek

Reputation: 489035

You literally can't delete the commit—not directly, anyway. So I suppose the answer is "no", sort of. But you can forget the commit. That makes the answer more complicated.

Branches do not have parent/child relationships, either. Commits have parent/child relationships.

The way this works is actually very simple:

  • Each Git commit has a unique hash ID. That hash ID—a big ugly string of letters and digits, which you'll see in git log output for instance—is in effect the true name of the commit. It's how Git finds the commit.
  • Each Git commit stores two things: its main data, which is a snapshot of all of your files, and some metadata: information about the commit, such as who made it, when, and why (your log message). In that metadata, Git includes the hash ID of the commit's parent commit.

This means that each commit points backwards to its predecessor. If you have a series of commits, with big ugly hash IDs, in a sequence, we can draw them like this if we let uppercase letters stand in for the actual hash IDs:

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

Here H is the latest commit. It contains, as part of its metadata, the hash ID of earlier commit G. So Git can extract G from the repository, using the metadata part of H. Of course, G contains the hash ID of earlier commit F, and so on.

So now, we add one more bullet point:

  • A branch name holds the hash ID of the last commit in the chain.

That is, if we get a little lazy about drawing the internal arrows—which all point backwards—we can draw this as:

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

The name master holds the hash ID of commit H, so master points to H. The metadata of commit H holds the hash ID of commit G, so H points to G. G points to F, which points on back still earlier.

If you have more than one branch name, the two branch names can both point to the same commit:

...--F--G--H   <-- master, br1

or they can point to different commits:

...--F--G--H   <-- master
            \
             I   <-- br1

It's the commits that really matter here. Branch names just let us—and Git—find the commits.

When you use git checkout or git switch to select a branch by name, Git:

  • extracts the commit's frozen-for-all-time files into a work area, so that you can see and work on them
  • attaches the special name HEAD to the branch name, so that Git can find both the current branch (the name HEAD is attached to) and the current commit (the commit that the name points-to).

We can draw that like this:

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

or:

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

In one drawing, the current branch is master and the current commit is H; in the other, the current branch is br1 and the current commit is I.

To make a new commit, Git:

  • writes out the snapshot (using Git's index, which we won't cover here);
  • sets the new commit's metadata, using:
    • your name and email address, from your git config user.name and so on;
    • "now" as the date-and-time-stamp;
    • your log message;
    • the current commit hash ID as the parent of the new commit; and
  • last, now that the new commit exists, writes the new commit's hash ID into the current branch name.

So if we're on commit I via name br1, and we make a new commit J, we get:

...--F--G--H   <-- master
            \
             I--J   <-- br1 (HEAD)

All you can ever do this way is add commits.

You can add new branch names any time you like. This has no effect on the commits themselves, though we might need more lines to draw the graph here as plain text. For instance, given the above, we can add a brexit branch name pointing to commit I:

git branch brexit br1~1

which produces:

...--F--G--H   <-- master
            \
             I   <-- brexit
              \
               J   <-- br1 (HEAD)

We can also delete branch names at any time, e.g., git branch -d brexit. This has no effect on the commits themselves:

...--F--G--H   <-- master
            \
             I
              \
               J   <-- br1 (HEAD)

It's just that the name that finds commit I directly is now gone. We can still find commit I: we just start at J and work backwards. (And we can shrink the drawing to use fewer lines, if we like.)

Branch names are, in effect, just labels that point to one specific commit, each. You can have many names for a commit, or a commit with no names at all. These branch names have one special property: you can attach HEAD to them, and make new commits, and the branch names move along with you as you make the new commits.

"Undoing" a commit: git revert adds a commit

If you use git revert to back out a commit, Git just adds a new commit that undoes the effect of the previous commit. For instance, if you run:

git revert <hash-of-I>

while we're in the above state, we get:

...--F--G--H   <-- master
            \
             I--J--K   <-- br1 (HEAD)

where commit K is just a commit that backs-out whatever we did in commit I.

There is a pattern here, and it boils down to another very simple rule: nothing can change any existing commit.

"Undoing a commit": we can forget commits with git reset

What if we had a command that could reach in and yank a label off some existing commit and paste it onto some other commit? Suppose, for instance, we decide to make the name br1 point to commit J again. Here's what that might look like:

...--F--G--H   <-- master
            \
             I--J   <-- br1 (HEAD)
                 \
                  K   ???

As before, we had to move commit K to draw this. It's still there, but there's a really important question: How can we find commit K?

The answer is: we can't! It's lost. If we saved its hash ID somewhere—if we wrote it down on a scrap of paper, for instance—we could get it that way. But if we've forgotten the hash ID, it's really hard to find.

There is a command that does exactly this: git reset. (Actually, there are multiple commands that can move branch names, but we'll stick with git reset here.) If we run:

git reset --hard <hash-of-J>

Git will move the name back one step, in the direction the internal arrows go, but opposite the normal direction for branch growth. Git will save commit K's hash ID somewhere, in case we want it back, and keep it around for a while, but the saved hash ID doesn't show up in normal everyday Git commands and you won't see commit K any more. It seems to be gone.

Aside: You might wonder why this is git reset --hard. That has to do with the index, which we aren't going into, and your work-tree. The git reset command is complicated! It has multiple modes, some of which do more than others. The --hard part tells Git: reset my work-tree too. Your work-tree is not really part of the Git repository. The files you see here, and edit to work with them, are yours and Git won't normally overwrite them, or remove them, unless you tell it to do so; but git reset --hard means overwrite my files.

Since these files aren't in the repository, Git can't get them back after overwriting them. So be very careful with git reset --hard. If you have done a git add and git commit, the commit holds snapshots of the files. Those snapshots are in Git and are read-only and saved forever, or at least, for as long as you can find the commit. So those copies are safe. It's your work-tree copies that are just temporary, and can be wiped out easily.

When you do reset a commit away, so that you can't find it, Git will eventually forget the hash ID itself. By default, the log entry that saves the hash ID is erased after 30 days. Once that happens, Git can delete the commit entirely.

The key is reachability

The real key here, to whether a commit remains forever or is eventually deleted, is something called reachability. For much more about this, see Think Like (a) Git, but in a nutshell, consider this state:

...--G--H   <-- master
         \
          I--J   <-- br1 (HEAD)
              \
               K--L   <-- br2

Lots of people would call br2 a "child branch" of br1, but Git doesn't think that way. Git thinks, instead, br2 points to L, and therefore all the commits up to and including L are reachable from br2. Git will start at L and work backwards: K, J, I, H, G, and so on.

Meanwhile, by starting from br1, all commits up through J are reachable: Git starts at J, then goes back to I and so on. By starting from master, all commits up through H are reachable. Note that commits up through H are on all three branches and commits up through J are on two.

If we now use git reset --hard to move br1 back one step, we get:

...--G--H   <-- master
         \
          I   <-- br1 (HEAD)
           \
            J--K--L   <-- br2

That is, commit J is still there, and still reachable—but now it's only on br1.

Conclusion

That's the big difference between revert and reset: git revert adds a new commit, while git reset lets you move a branch name around (and with --hard, clobbers your work-tree files). With revert, the original commits are definitely still around; with reset, whether you can find any commits you have reset away, depends on what other names are still around to find commits, and the links between the commits.

As michid answered, you probably don't want to use git reset here, because you have done a git push already. This took the new commit you made with git commit and handed it off to some other Git repository. They will retain your commit: Gits do not "like" to forget commits. To get them to forget your commit is harder, and usually not a good idea—though how hard depends on how widespread that commit got in the meantime.

Since revert adds a commit, and Gits are greedy and will add new commits as fast as they see them, it's easy to get the other Git to add an "undo" style commit.

Upvotes: 3

michid
michid

Reputation: 10824

When you say "delete the commit" I assume you mean you revert the commit with git revert. This will add a new commit to the current branch, which essentially undos everything from the previous commit. Any other branch containing the reverted commit will not be affected by this. So in your case the commit in question will not disappear from the child branch.

Upvotes: 2

Related Questions