Search code examples
gitgit-pullgit-fetch

Git: How to check if any commits were deleted?


I want to ensure that no commits are deleted (or squashed) from my local repository by fetch from a remote repository.

Also I need to reveal to me, if any commits were deleted (or squashed) at the remote repository.

How can I make sure that my fetch or pull does not delete any commits (on my local PC)?

Another related question: How to make Git to delete local commits corresponding to deleted remote commits?


Solution

  • There are some problems with this question, and maybe that's why it got down-voted, but we can make something out of it.

    Let's take a look at what's actually stored in a Git repository, drawn as a graph, before and after git fetch, to see what I mean.

    We'll start with this:

              A   <-- master
             /
    ...--o--*   <-- origin/master
             \
              B--C   <-- origin/dev
    

    Here, in the local repository, before running git fetch origin, we have:

    • some number of shared commits, starting with commit * and working backwards;
    • one commit that is "ours", commit A, on our master, that is not on origin/master
    • two commits that are the upstream's only, on their dev which is our origin/dev: commits B and C.

    Now we run git fetch, which prints some output, which for some reason we don't see or have forgotten. We then inspect our graph carefully again and discover this:

              A   <-- master
             /
    ...--o--*--D   <-- origin/master
                \
                 E   <-- origin/dev
    

    That is, the two commits I labeled B and C are no longer reachable at all, from any of the origin/* names, while new commits D and E have appeared. The name origin/master, which is our Git's memory of their master, now identifies commit D, while the name origin/dev identifies commit E.

    It's reasonably likely—but impossible to tell for sure from this amnesiac view—that commit D is the result of squashing commits B and C, and possibly some additional commits that we never had. Commit E is probably entirely new. On the other hand, we just don't know.

    We probably have more information than this, though. In particular, if we have reflogs enabled, we will have origin/master@{1} and origin/dev@{1}. These will point to commits * and C respectively, and we'll retain both C and B through the origin/master@{1} reflog.

    We might also have a different starting picture. For instance, maybe our starting image looks like this instead:

              A   <-- master
             /
    ...--o--*   <-- origin/master
             \
              B--C   <-- dev, origin/dev
    

    In this case our ending picture looks like this:

              A   <-- master
             /
    ...--o--*--D   <-- origin/master
             \  \
              \  E   <-- origin/dev
               \
                B--C   <-- dev
    

    What to know

    There are several things to note here:

    1. We never lose any commits to which our own branch names point.
    2. We never update any of our own branch names at all.
    3. We do update our remote-tracking names, but when we do, if reflogs are enabled, their previous values get saved in our reflogs for those names.

    Hence:

    I want to ensure that no commits are deleted (or squashed) from my local repository by fetch from a remote repository.

    This is just automatically true, if by "no commits" you mean "no commits to which I have names other than remote-tracking names".

    Also I need to reveal to me, if any commits were deleted (or squashed) at the remote repository.

    Here, you can look at the difference between any updated remote-tracking names—note that git fetch tells you which names it is updating—and their saved reflog values. If, for instance, origin/master and origin/dev were updated, you can compare origin/master@{1} with origin/master.

    At this point, several of the special syntaxes described in the gitrevisions documentation become quite handy. In particular the two-dot and three-dot set-difference and symmetric-difference operators tell us just what we want to know:

    git rev-list origin/master@{1}..origin/master
    

    produces the list of commits added to origin/master, and:

    git rev-list origin/master..origin/master@{1}
    

    produces the list of commits removed from origin/master. We still have all of those commits in our repository since the reflog names point to them; they won't vanish until the reflog entries expire.

    We can use the three-dot syntax along with --left-right, or similar entities, to enumerate and identify commits added and removed, marking which "side" of the symmetric difference they are on:

    git rev-list --left-right origin/master@{1}...origin/master
    

    Using all of this information we can automatically guess, if we really care, which commits might be rebases or squashes of which other commits, but this is a doomed effort in some cases (e.g., resolved merge conflicts).

    Another related question: How to make Git to delete local commits corresponding to deleted remote commits?

    This is trickier as it depends on what you mean. However, if you mean what I am guessing you mean, the --fork-point operation of git rebase is actually designed to handle this. The fork-point code uses the reflog of the upstream branch. For details, see Git rebase - commit select in fork-point mode.