Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to find merge commits in history done using "git merge -s ours" option?

Misusing the "-s ours" merge option when merging origin/develop into local develop essentially throws away work done since the last time develop was updated. I've manually found two instances of this in the git history by deliberately looking through the history for files I know have went missing.

Is there a way to find all commits that were done using the "-s ours" merge option?

I imagine it's possible by comparing merge commits with their parents and checking if only one parents branches contents were actually taken but I've got no idea how to translate this theory into a git command. What command do I even start with?

like image 721
Craig J Avatar asked Jan 29 '18 15:01

Craig J


People also ask

Which command is used to display merge history in git?

The most basic and powerful tool to do this is the git log command. By default, with no arguments, git log lists the commits made in that repository in reverse chronological order — that is, the most recent commits show up first.

Does git merge keep history?

In the Conceptual Overview section, we saw how a feature branch can incorporate upstream changes from main using either git merge or git rebase . Merging is a safe option that preserves the entire history of your repository, while rebasing creates a linear history by moving your feature branch onto the tip of main .

What happens to commits after merge?

This sort of commit is called a merge commit. And that's not all. After the merge, the working tree and the index and the HEAD (the new merge commit) are all synchronized. In other words, Git does not merely make a new commit; it also changes the state of your working tree (and the index).

How do I know if a commit is merged?

You just need to check the length of the `parents`. If there are two or more commits, it's a merge commit, otherwise it's a regular commit.


1 Answers

TL;DR (but untested)

As a shell script:

git rev-list --merges HEAD |
while read rev; do
    thistree=$(git rev-parse $rev^{tree})
    p1tree=$(git rev-parse $rev^1^{tree})
    if [ $thistree = $p1tree ]; then
        echo "commit $rev has the effect of -s ours"
    fi
done

Long

The immediate answer is "no" (but read on). The actual strategy used is not recorded automatically. (If the individuals doing all the merges were very self-disciplined, perhaps they recorded that information in every merge-commit message. Based on your problem description, this clearly didn't happen....) But that's not necessarily the interesting problem anyway. It's possible for someone to create a merge that has the effect of -s ours without actually using the ours strategy.

The more interesting question is automatically finding merges that have the effect of -s ours, whether or not the option was used; and that is easily automated.

The problem reduces to:

  • Find merge commits: the tool for this is git rev-list.
  • Test merge commits to see if they had the effect of -s ours: the tool for this is ... well, see below; there are several possibilities.

Finding merge commits is trivial once you know how git rev-list enumerates commits. Like git log, you give git rev-list a starting point, and Git then works backwards from that point, through history that is reachable from that commit, until it runs out of history or runs into a commit you've told it to use as a stopping point.

Typically, a good starting point is HEAD, which is the commit you have checked out right now, which is probably the tip commit of a branch. You can even give multiple starting points, such as --branches (every tip of every branch) or --all (every commit with a label, whether that label is a branch name, a tag name, a remote-tracking name, or even one of the special names like refs/stash for the stash).

You then want to restrict the output to listing only merge commits. A merge commit is any commit with at least two parents: --min-parents=2. There's a synonym for this that you might prefer, though, so we get, e.g.:

git rev-list --merges HEAD

to start from where you are now and work backwards through all commits reachable from HEAD. (For more on this reachability concept, see Think Like (a) Git.)

The output from git rev-list is a stream of commit hash IDs that meet the rev-list criteria, in this case, --merges (every reachable merge commit): not very useful to humans, but great for computer programs. Now we just need to write the program that identifies the interesting merges.

We already saw that a merge commit is a commit with at least two parents. What -s ours or equivalent does is tell Git that the new merge commit's snapshot (its stored tree) should match one of the two parents, specifically the first parent. You might want to relax your conditions and check whether the tree matches that of the second parent (a theirs-strategy merge, except that there's no built in -s theirs), or for octopus merges, any of the parents, but "matches first parent" will probably do, and is slightly easier.

The two obvious ways to do this are:

  • Compare the merge commit's tree with its first parent's tree: git diff (or its plumbing equivalent) with the two specific commits or tree hashes.
  • Or, since we only care about the entire tree, just compare the top level tree hash directly. This checks for a pure exact match; using a full git diff or equivalent would allow you to relax the match criteria a bit if necessary.

We'll code the second one—comparing the stored tree hash in the merge and in its first parent—in shell-script since it's perhaps faster and in any case demonstrates more of the various Git tools. We start with the generic "read every commit hash" loop:

while read rev; do ...; done

Each revision will be available within the loop as $rev.

Now we simply turn $rev (a commit) into its own tree:

git rev-parse $rev^{tree}

which uses the gitrevisions syntax to find the specified commit's tree hash ID. We must save that in a variable:

thistree=$(git rev-parse $rev^{tree})

We then want to find the first parent's tree. The first parent is, in gitrevisions syntax, ${rev}^1. The braces here are technically unnecessary so I'll omit them. Then we want that commit's tree, which we need to save in another variable:

p1tree=$(git rev-parse $rev^1^{tree})

If the two trees match, the commit $rev has the effect of -s ours, whether or not it was made with -s ours, so we should print its hash ID, or even run git show on it.

The final version of this loop (putting all the parts together) is the one at the top of this answer.

like image 160
torek Avatar answered Sep 28 '22 03:09

torek