Edit: let me start with the philosophy question (which I missed earlier).
Git philosophy question: are "ancestry" and "history" (a.k.a. chronology) distinct things?...
Not really. Well, maybe: "ancestry" and "history" are the same thing, or at least, deeply connected. Chronology ... is often irrelevant. Git stores commits, and the commits are, or at least form, the history.
Each commit has some set of parents: usually just one parent, but merge commits have two or more, and at least one commit in the repository—called the root commit—has no parents. In general, you make a new commit by selecting an existing commit, selecting or creating a tree (snapshot) to go into the new commit, and writing out a new commit with:
- you as the author and committer, and "now" as the time stamps of the new commit;
- your tree as the snapshot;
- an arbitrary commit message; and
- your current commit's ID—and perhaps more commit IDs—as the parent(s) of the new commit.
Once made, a commit is frozen in time forever: it literally cannot be changed, as its hash ID—its "true name" in terms of how Git finds it in the database—is a cryptographic checksum of its contents. Change the contents in any way, even just one bit, and you get a different checksum, which means a new and different commit (and meanwhile the old commit remains in the repository).
The special case of a root commit occurs when you make a commit without starting with a current commit. This obviously has to be the case for the first commit in a new, empty repository: there are no previous commits, so the first commit cannot have a parent. After that, all normal commits have some existing commit as their parent, though you can create new root commits.
The result is that commits string together, pointing "backwards in time", regardless of any time-stamp stored inside the commit. We can draw a small three-commit repository like this, with single uppercase letters standing in for the actual commit hashes:
A <-B <-C
Commit C
is the latest (even if its timestamp says it was made in the 1970s); commit B
is C
's parent; and root commit A
is B
's parent. These facts are embedded in each commit, which is read-only and incorruptible.
The way Git finds C
is through a branch name, like master
. C
has some hash ID—the cryptographic checksum of C
's contents—and Git places that hash ID in a key-value store, with the key being the name master
. So given master
, Git finds C
, which Git uses to find B
, and so on. To place a new commit onto the master
branch, we would now write some new stuff, run git add
and git commit
, and get a new commit D
with a new snapshot, whose parent is commit C
. Whatever the hash ID of D
is, Git would write that into master
, and now master
will point to new commit D
.
This is how the history and the ancestry are so deeply intertwingled. We can, at any time, given all the starting-point names (branches, tags, and any other names), find all the commits in the repository through these linkages. Drawing the linkages produces the commit graph.
The git log
command does not necessarily, always, show you commits in the order they occur in this graph. For instance and in particular, if git log
has, in its "commits to show" queue, two or more commits, it must pick one to show first. The one it picks is based on the sorting criteria you specify on the git log
command line. If it has only one commit to show, though, it shows that one. Having shown that one commit, git log
normally adds that commit's parents (all of them) to the queue, unless they have already been shown.
The default for git log
is to show you the HEAD
commit, by putting that (single) commit into the queue. There's now just one commit in the queue, so Git removes it and shows it. If that's an ordinary, single-parent commit, you will see it, and then git log
will put its (single) parent into the now-empty queue. Git will then show you the parent: you will see commits in their graph order, regardless of any commit time-stamps.
Since merge commits have, by definition, at least two parents, the act of showing a merge commit normally drops two as-yet-unseen commits into the queue of commits to show. It's at this point that time-stamps may (and by default, do) enter the picture. If you don't specify any particular sorting criterion, the one Git uses by default is: show the commit with a higher commit time-stamp first. So now chronology—with the meaning time stamps stored in commits, which may have nothing to do with when the commits were actually made—have some effect.
(Note that you can adjust one or both time stamps on any new commit at the time you make it by changing your computer's clock, or more easily and reproducibly, by setting GIT_AUTHOR_DATE
and/or GIT_COMMITTER_DATE
environment variables when running git commit
. Some commands, including git commit
itself, also take --author-date
flags and the like.)
On to the rest of the issue
As several people have told you, you can't get what you want, in Git.
Moreover, this drawing is nonsensical:
A---B---C develop
/ \
D---E---F---G---C master
as it contains two different commits that are both labeled C
. You cannot change the existing commit, and any new commit you make will, by definition, have a new and different hash ID.
So is this drawing, for the same reason:
A---B---C develop
/ \
D---E---F---G===C--- master
If, however, we replace the extra C
with a completely different commit—one with a different hash ID—then we can get the former graph, which now looks like:
A---B---C <-- develop
/ \
D---E---F---G---H <-- master
That is, the name master
selects commit H
, whose history includes all the commits, while the name develop
selects existing commit C
, whose history goes back through B
to A
and then on back to E
and so on. Or, if you prefer, we can get the graph:
A---B---C <-- develop
/
D---E---F---G---H <-- master
The snapshot associated with commit H
can be whatever you want it to be. (To make such a snapshot entirely manually, you can work within your work-tree, run git add
to copy updated work-tree files over their copies currently in the index, and then write git write-tree
to write out index to form the tree for the snapshot for H
—but you don't seem to need this.)
If you want commit H
to have the same snapshot as commit C
, so that git diff develop master
produces no output at all, that's particularly easy: we can use Git's so-called plumbing commands to create commit H
from the tree for commit C
. There are three steps, in the end:
vim /tmp/msg
# write appropriate log message to file /tmp/msg
Next, create new commit H
and save its hash ID somewhere. (I use a variable here. You can then inspect it with git log --graph $h
for instance, if you want, before you actually commit to using it anywhere.)
h=$(git commit-tree -p master -p develop -F /tmp/msg develop^{tree})
This creates the commit with two parents, master
and develop
(i.e., G
and C
) in that order. If you want just one parent, leave out one -p
and argument.
Finally, if all looks good and you're on master
and wish to make H
become the tip commit, reset or fast-forward-merge to H
:
git checkout master # if necessary
git merge --ff-only $h
This updates the current branch name—i.e., master—to point to commit $h
(i.e., new commit H
), and updates the index and work-tree correspondingly, so that the contents of commit H
now occupy the usual staging and working spaces.
(Incidentally, if you do make final commit H
with two parents, and make the first parent be commit G
and the second commit C
but use commit C
's content, that would be the equivalent of git merge -s theirs
, if it existed. It doesn't, but you can synthesize it any number of ways, including the one above. See also VonC's answer here and some of the related links.)