In my opinion, the key to understanding here is this:
Git Stores Snapshots.
What this means is that every commit is a full snapshot of all of the files for that commit. There are no "changes" at all, there is only a full snapshot, every time.
Now, internally, the storage format is more complicated (and compressive, ultimately using the concept of changes as well, though not exactly the way one might expect). But from the "what things look like" point of view, and how you should think about what Git sees, each commit is simply a full and complete snapshot of every file. So if you have one commit where `foo.rb` exists, and `bar.br` does not, that snapshot has a `foo.rb` but no `bar.rb`. If you make a second snapshot, in which `foo.rb` no longer exists but `bar.rb` does now, that second snapshot, well, has those files.
The tricky part is that you normally ask Git to *compare* two snapshots (at least for "what changed" purposes). When you do that, Git extracts both snapshots and, well, compares them. If `foo.rb` has been removed and `bar.rb` has been added, Git then goes on to compare the *contents* of those two files.
If the contents match exactly, and you've asked Git to "find renames", Git will always say that the file that vanished from the first commit, only to be created identically under a new name in the second, was "renamed", rather than the one file being deleted and the second added.
If the contents match "fuzzily" (for some value and algorithm of fuzz-factor), Git may also say "renamed". You can control this with `--find-renames=<value>`. The key idea here is that Git is *finding* renames: either exact-same-contents, or "sufficiently similar" contents, based on remove-and-add pairs.
Since Git only *stores* snapshots, you can get two different results from comparing the same two commits. All you have to do to get this is to adjust whether Git checks for renames at all, and if so, to what extent.
These rules apply to `git show`, `git diff`, `git merge`, and even the diffstat that `git commit` optionally shows after a commit. For this reason, all the "compare some commits" commands -- including `git merge` -- take this `--find-renames=<value>` option. Detection of renames can be countermanded entirely with `--no-renames`.
This is why -- and when -- making two separate commits, one with "exact same content for deleted-file-D vs added-file-A", followed by later changes to new file A, helps: if you compare the commit that has file D to the middle commit, the two files match exactly, and any rename detection you have turned on finds that rename. If you then compare the middle commit to the final commit, file A exists in both, so Git shows changes to file A. But as soon as you compare the original file-D-containing commit to the final file-A-updated commit, you run into the original issue again: to detect this as a rename, you may need to allow rather generous rename detection.
If, in the future, Git gets fancier rename detection, comparing the original commit directly against the final one could find the rename automatically. So:
It *could* be improved. Doing so in a way that works for more than just some special cases -- e.g., in a way that works for ordinary text, or graphical images, for instance, rather than just for Ruby sources (or just C sources, or C++, or Swift, or Python, or whatever) -- seems particularly tricky. Some degree of ignoring white-space changes would probably help multiple cases, though.
Chris