Reputation: 3736
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
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:
git log
output for instance—is in effect the true name of the commit. It's how Git finds the 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:
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:
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:
git config user.name
and so on;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.
git revert
adds a commitIf 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.
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. Thegit 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; butgit 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 agit add
andgit 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 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
.
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
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