Based on the output from git reset
, the commit you're trying to remove is the first, last, and only commit. This is what is causing the problem.
It's always technically impossible to remove any commit, but normally we can get Git to stop seeing the last commit in a chain of commits, using the git reset
command you're trying to use. The reason for this is simple enough, but requires a bit of background knowledge about Git.
A Git repository is, at its heart, a big collection (or database) of commits. Nothing else really matters: it's really the commits that matter. You may have branches and tags, but the point of these branches and tags is to help you (and Git) find commits, and the reason you want them is that the "true name" of a commit is some big ugly random-looking hash ID number, such as cefe983a320c03d7843ac78e73bd513a27806845
. This isn't something anyone is going to remember, but a simple name like master
or main
will make Git remember the number for you.
Now, each commit has one of these numbers, and that one number is unique to that commit. It's not merely unique in your Git repository. It's unique in every Git repository in the universe. That number means that commit, never any other commit. So two Git repositories, meeting in the dead of night (or a sunny day) over some internet connection, can tell which commits each other has, and hence one can obtain only those commits that are new to it. The two Gits have the same commit if they both have one with the same number.
That's what a lot of this is about: the numbers let your Git talk to another Git, and the two will immediately—or at least very quickly—know who has what, and hence a git fetch
or git push
can package up something pretty small and deliver that.
But, given this feature of commits, there's another trick your Git software will use with your repository. Namely, each commit:
- stores a full snapshot of every file, as of the form that file had at the time you (or whoever) made the commit; and
- stores some metadata, or information about the commit itself: who made it, when, and so on.
In the metadata for any one given commit, Git stores the raw hash ID of the previous commit (or sometimes commits, plural: these are merge commits). What this means is that commits link to earlier commits. The commits form a chain. This chain works backwards, so Git itself works backwards too.
Given some chain of commits, we can draw a picture of that chain. Let's use single uppercase letters like H
(for Hash) to stand in for real commit hash IDs, because the real hash IDs are too big and ugly for us. Then if H
is the latest commit, H
has, stored inside it, the hash ID of some earlier commit. Let's call that commit G
. We'll say that commit H
points to earlier commit G
:
G <-H
But commit G
has, stored inside its metadata, the hash ID of some still-earlier commit F
. So we haven't drawn the above quite right. Let's fix that:
... <-F <-G <-H
Commit F
, of course, points to some still-earlier commit, which points backwards yet again, and so on.
In order to find a commit, Git needs the hash ID. So somehow, we have to give Git the actual hash ID for the latest commit H
, if we want to use that latest commit H
.
What we do is have Git store that hash ID for us, in a branch name. We then say that the branch name points to the commit, and when we draw that in, we end up with:
... <-F <-G <-H <-- main
That is, the name main
, which is easy for us humans to remember, holds the hash ID of the latest commit H
. So that lets us—and Git—find H
easily. Then, commit H
points backwards to earlier commit G
, which lets Git find it easily. (We might have to run git log
and let Git show us the hash IDs, for instance, to find it ourselves, but Git can find it easily.) Commit G
lets Git find F
easily, and so on: so Git works backwards, just as we said earlier.
When we use git reset
the way you are—with git reset --hard HEAD^
—what we're doing is telling Git: First, find the latest commit H
(that's the HEAD
part here) and then step back once to an earlier commit G
(that's the ^
suffix). Then make the branch name point to that commit. So if we start with:
...--F--G--H <-- main (HEAD)
—I've gotten a bit lazy about drawing in the internal arrows—and we run this kind of reset
command, we end up with:
H
/
...--F--G <-- main (HEAD)
Commit H
is still in the repository. We don't actually remove it: we can't do that directly. Git will eventually clean it out, on Git's own timetable, whatever that may be.1 But because we find the latest commit using the name, and the name now finds commit G
instead of commit H
, we'll think that commit H
is gone: we won't see it any more when we use the branch name.
This works great for a while: we can keep re-setting commits away:
E--F--G--H
/
A--B--C--D <-- main (HEAD)
eventually becomes:
B--C--D--E--F--G--H
/
A <-- main (HEAD)
Or, if we only actually have one commit to start with, we start with:
A <-- main (HEAD)
Note that A
doesn't point backwards to any earlier commit, because there is no earlier commit. So using HEAD^
, we ask Git to find the commit that comes before A
, and no commit comes before A
and we get an error.
1The default usually involves waiting at least 30 days. We can push this ahead using some janitorial type commands. On sites like GitHub, commits may never get cleaned up.
So: what can you do here?
There is one option for "deleting" this commit. As before, it doesn't actually go away, it just gets lost until Git's janitorial functions clean it up someday (if ever). The option we have here is to delete the branch name.
There are several problems with this. The most important one is that you cannot delete the branch name that you are using. You're using main
or master
right now, so you can't delete it.
To fix this problem, you need to create a new branch name. But a branch name must point to a commit. You only have one commit, and that's commit A
. So all your branch names have to point to commit A
.
The main solution to this problem is simple: delete the repository completely. With no repository, there are no commits, and no branches, and hence no commit A
. If you can use that solution, that's the easy one, so do that.
You might object to this solution for some reason, and in fact there's one other solution. It's a bit tricky though:
Consider a new, totally-empty repository, that you make by running git init
.
In this new repository, you are on some branch (master
or main
) but there are no commits.
Since there are no commits, there are no branch names either.
This means you're "on" a branch name that does not exist.
This special weird situation is necessary in a new, empty repository, so it has to exist in Git. Git will allow you to re-create this situation even in a non-empty repository, using the --orphan
flag to git checkout
or git switch
.
So, to get off the one branch that does exist in order to hold the one commit A
that does exist, we will use this special feature:
git checkout --orphan newbranch
Your state is now this:
A <-- main
[newbranch (HEAD)]
You are now on a branch named newbranch
that does not exist and therefore does not point to any commit. This state is weird! But it does allow you to delete the name main
, using git branch -D main
or git branch -d --force main
.
You can now create a new "first" commit. We'll call it B
here since commit A
still exists—you just can't find commit A
unless you wrote down its hash ID somewhere before you deleted the branch name.
When you make your new initial (or "root") commit, Git will now create the branch name at this time:
A [abandoned]
B <-- newbranch (HEAD)
You can now go on making more new commits as usual:
A [abandoned]
B--C <-- newbranch (HEAD)
and so on. Eventually—some time after 30 days or so—Git will probably finally take out the trash and drop commit A
entirely, so that even if you did write down its hash ID, and go look for it, it won't exist any more.