In general, git restore
and git revert
are extremely different. The way you are using them reduces the amount of difference, but they could remain quite different—or the difference could be reduced to nothing!
In other words, without more information, it's not really possible to answer your question.
Long
The way to understand this is to begin with the fact that Git is really about commits, rather than files or branches. Commits are the primary permanent data in a Git repository. You will, at various times, hook your Git repository to some other Git repositories, and at those times, the paired-up repositories will give and receive commits—not files, not branches, but commits. (Despite the fact that the commits are paramount, branch names are still important, because they let you—and Git—find the commits. We won't go over that properly here though.)
Each commit has two parts: its main data part, which holds a full snapshot of every file that Git knew about at the time you (or whoever) made the commit, and its metadata part, which holds information about the commit, such as who made it—name and email address—and when (date and time stamps) and so on.
Each commit is also numbered, with a unique hash ID. These hash IDs look random, but are not random at all: each one is a cryptographic checksum of the full contents of the commit.
When you pair up two Gits, they can figure out which commits each other has just by looking at these commit numbers. Given any hash ID, any Git can inspect its object database (commits are one of four types of objects) and see if it has that object—in this case, that commit. If so, because the checksum is computed the same way by every Git, the two Gits have the same object if and only if both Gits have the object with that unique ID.
Because the number is a checksum of the data, it's literally impossible to change anything inside a commit. If you do take a commit out of the database, modify it somehow, and shove it back in, what you get is a new commit with a new and different unique hash ID.
But this also means that the files saved inside a commit1 are completely and totally read-only, frozen for all time. You literally can't work on these files. So you don't: you work on other files—files that are not in Git.
1Technically, the files are more sort of along side the commits than in them. They're found through tree objects, which contain their names, and blob objects, which hold their content in automatically-de-duplicated form. The commit merely contains the hash ID of the top level tree object. In any case, though, they are still frozen for all time.
The files you work on are not inside Git
I don't know of any Git user who does not actually want to use their files. But the files that Git stores are in this frozen (and compressed and de-duplicated) Git-only format. So what Git does is to extract a commit, into what Git calls your working tree or work-tree. This is where you can see and work on your files. But these files are not stored inside Git: they're not frozen and saved forever, they are just everyday ordinary computer files.
This means that, like pretty much all version control systems, there are in effect two versions of each file that are "active": there's the frozen copy in the current commit, that you've picked out with git checkout
or git switch
. Separately, there is the usable copy in your work-tree.
Git actually keeps a third copy—in the frozen and de-duplicated format, so that it's not really a copy after all—in what Git calls the index or staging area, but we won't really cover that properly here. I mention it because git restore
can be told to write over both your work-tree copy and this "staged for commit" copy, and you are using that form in your sample command.
git restore
The git restore
command is about overwriting your work-tree and/or staging-area copies of files. To do this, you must pick some source, and then some path name or names. Your example is:
git restore --worktree --stage --source=18f5766 .
The source is therefore commit 18f5766
: some existing commit, with a snapshot of all the files that Git knew about at this time. The path you picked is .
, meaning the current directory. This command will reach into that particular commit and find its files relative to the current directory. It will copy those files out into Git's index and your work-tree.
So now, any files that were in that commit have been changed back to whatever they looked like at that time. This affected both your work-tree copy (which you can see and work with) and the index/staging-area copy (which Git will use in the next commit you make). No new commit exists yet.
git revert
The git revert
command is about backing out some set of changes. (In Mercurial, this command is named hg backout
, which I think is a better name for it.)
In order to talk about changes we now need to make a small side trip into more details about each commit. While each commit holds a snapshot (as its data), each commit also has some metadata, as we already noted. In that metadata, each commit stores the hash ID of its immediate predecessor or parent commit. What this means is that, given a linear chain of commits, we can draw them like this:
... <-F <-G <-H ...
Here H
stands in for the actual hash ID of some commit. Inside commit H
, we have a snapshot, but we also have the actual hash ID of earlier commit G
. So given the hash H
, Git can extract both snapshots, and compare them.
The comparison reveals which files were modified: if a file in G
matches its counterpart in H
, nothing happened to that file. If they're different, Git can compare the contents and figure out what changed. That's how Git shows us each commit as a change, instead of as a snapshot: to view commit H
, Git actually looks at both G
and H
. Then, if we are using git log
, Git steps back to earlier commit G
, views both F
and G
, and shows us the changes between those, then steps back again, and so on. (In other words, Git works backwards—which explains a lot about Git.)
So, what git revert
does can be described—with a little loss of precision—as:
- Find what changed in the commit I name.
- Then, reverse-apply those changes to the current commit. That is, if what changed included deleting line 42 of file
hitchhiker.txt
, put that line back in the current file (wherever it goes, which might not be line 42 any more). If what changed included adding some line somewhere, take that line out.
- Then, if all went well, make a new commit from the result.
That's not quite how you are using this command, though. Your command is:
git revert --no-commit 18f5766..HEAD
The --no-commit
option tells Git not to make a new commit as it does each revert. The two-dots here ... well, now things get more complicated again, and we need to make another side trip.
In a simple linear chain like:
... <-F <-G <-H
each commit has one parent, and it's easy to start with the last commit—the tip of a branch, for instance—and work backwards one commit at a time. Such a chain can be drawn a little more compactly, adding more information as well, as:
...--F--G--H <-- branch-name (HEAD)
which indicates that H
is the last commit on branch branch-name
, and that we are currently using that branch name as our current branch, and therefore using commit H
as our current commit. That is, Git's index and our work-tree will initially have been set up to match commit H
, when we used git checkout
or git switch
to select branch-name
and therefore commit H
.
The two-dot syntax allows us to pick some commit in this chain and say: work backwards as usual, but stop here. That is, if F
stands in for actual hash ID 18f5766
, and we write:
18f5766..HEAD
we're saying: Start with H
, then step back to G
, but then stop because we are not supposed to use 18f5766
at all.
Suppose you have this kind of linear chain
Suppose that we do in fact have this setup, so that git revert -no-commit 18f5766..HEAD
will first undo whatever happened in commit H
, without committing, then undo whatever happened in commit G
, without committing, then stop.
Having undone these two steps and then stopping, we will now have, in Git's index and our work-tree, the same files that are in commit F
. So this git revert
will produce the same result as the git restore
earlier.1
1Unlike git checkout
, git restore
defaults to --no-overlay
mode (git checkout
defaults to overlay
), so if commit G
or H
added a new file, this git restore
will delete it. Using git checkout 18f5766 -- .
would have failed to delete the file.
There's a small but bad bug in --no-overlay
mode that has been around since it was added to git checkout
that is being fixed in the next release of Git (2.28.1 I assume). This doesn't affect this kind of git restore
but does affect git restore
with wildcarded pathspecs. This can delete files that should have been restored. Fortunately you can get them from the commit you're restoring-from, provided you notice they have gone missing!
But what if you have a branch-y chain?
The linear chain we drew above:
...--F--G--H <-- branch-name (HEAD)
is not the only possibility here. We could have:
...--F
\
G--H <-- branch-name (HEAD)
/
...--E
for instance. In this case, the git restore
of commit F
will make Git's index and your work-tree match commit F
as before, but the git revert
will go awry: it will attempt to undo H
, then attempt to undo G
—which will fail because G
is a merge commit—then attempt to undo commit E
and any earlier commits that come before E
, as long as those commits are not reachable from commit F
(that is, are not ancestors of F
).
Any revert with a merge in the chain is problematic in general. The git restore
command has no issues here as it simply copies out of the tree—the saved snapshot—of some existing commit; there's no question of walking through multiple commits.