Let's start with this:
Possible theory I now consider: could it be the result of git commit --amend?
Yes.
Now let's go back to this, because there is a hidden assumption here, which is a key factor:
I have been using git in a repository for which I am the solo contributor. The repository is hosted on GitHub.
This isn't really right. What you mean here is that there is a repository on GitHub. It's not the repository: it is one of several, or many. In this case, it is one of two. The other one is on your own machine!
For a proper explanation, see any of many of my longer answers. See also the web site Think Like (a) Git, which has a lot of important background. But for short, remember that there are two repositories here: yours, and the Git repository on GitHub.
When you make a commit, it's uniquely identified by its hash ID—a big ugly string of hexadecimal digits, such as cfacddfb5395e0fa3676fbc1e19457c45c7c3529. That's what your Git stores in your branch name master. Each commit also stores the hash ID of its previous, or parent, commit(s). A merge commit is a commit with at least two parents. When a branch name, or a commit, stores the hash ID of a commit, we say that this name or this commit points to the targeted commit.
We can draw such things as (horizontally oriented) graphs with the latest commit towards the right:
...<-F <-G <-H <--master
Git attaches the name HEAD to a branch name, so that if you have multiple branch names, it knows which is your current branch. The arrows inside commits cannot change—nothing inside any commit can ever change—so we don't really need to draw the internal arrows:
...--F--G--H <-- master (HEAD)
When you run git commit, the normal procedure is to freeze whatever is in your index right now into a new snapshot, make the snapshot's parent be the current commit, using the pointer coming from the branch name to which HEAD is attached:
...--F--G--H--I
and then Git puts the hash ID of the new commit into the current branch name, so that it now points to the new commit:
...--F--G--H--I <-- master (HEAD)
How git commit --amend works
We just saw that git commit:
- freezes the current index into a snapshot;
- writes out a new commit with the current commit as its parent;
- writes the new commit's hash into the current branch.
All that --amend does is to change to change step 2: instead of using the current commit as the new commit's parent, --amend uses the current commit's parents as the new commit's parents. (If there are multiple parents—if the current commit is a merge—the new commit gets all of the current commit's parents. If it's a normal single-parent non-merge commit, the new commit gets one new parent.) So instead of going from ...-F-G-H to ...-F-G-H-I, we get:
H / ...--F--G--I <-- master (HEAD)
That is, --amend shoves the current commit out of the way.
This works fine if you haven't pushed
If you never pushed commit H anywhere, amending it now works fine. The new commit I takes over; commit H vanishes, never to be seen again. (You can find it in your own repository for a while—at least 30 days by default, until the reflog entries that remember it expire. But it does not show up in normal git log output.)
But it fails if you have
But if you have sent commit H to another Git—such as the repository you are storing on GitHub—then any time you contact them, they'll offer you commit H back again, as being an important commit on their master branch. That is what happened in this case.
You had pushed commit H to GitHub. You then had your Git shove your H out of the way, as above, but GitHub still had H. Your Git remembers this, though if your Git had forgotten or you erased its memory, your Git would be reminded the next time you had your Git call up the Git on GitHub. So now you have this:
H <-- origin/master / ...--F--G--I <-- master (HEAD)
From now on, as you work in your repository, commit H remains visible. You made commits J (271056c1c34444dbb3b8c2b6a67efae67f27b44b) and K (798afefabe24027a955853771072755c08318b47):
H <-- origin/master / ...--F--G--I--J--K <-- master (HEAD)
At this point, you ran git pull, which had your Git call up the Git at GitHub and get any new commits from GitHub (there were none) and update your origin/master to match their master (still pointing to commit H). Your git pull then had your Git merge their commit H with your commit K, making a new two-parent merge commit L:
H_ <-- origin/master / `----__ ...--F--G--I--J--K-=L <-- master (HEAD)
and that's what you see now. (Well, now you also asked the GitHub Git to set their master to L, which they did, so your own Git repository's origin/master goes to L too.)
To fix it, you must get all other Gits to forget H
After you "amend" H, you must find all other Git repositories that had H and convince them to use I instead. In this case, there is only one other Git involved: the one at GitHub. You can run:
git push origin master
which has your Git call up the Git at GitHub and politely request that they change their master to point to your replacement commit I. But if you do this, you will find that they say no, I won't do that, because that would lose commit H. The form of this complaint is:
! [rejected] master -> master (non-fast-forward)
In this case, you know what the problem is, and that you want them to lose (forget) commit H. So you can use git push --force, or the fancier variant, git push --force-with-lease.
These convert your request (please update your master) to a command: update your master! GitHub could still refuse (and will if you are not authorized to do this), but in general, they will obey the command where they rejected the request. The --force-with-lease option works as a safety check: instead of just saying do this anyway, your Git says to theirs: I think you have your master set to <H's hash>. If so, replace it. If not, let me know. This is what you might use on a shared repository, in case other people are also pushing to the GitHub master.