6

When resolving merge conflicts, most of the time it's because two people inserted at the same point of a file. And I always do the same: Use the new code from the left side, copy the new code from the right side and append it after the code from the left side.

This got me wondering, if I always do the same, it should be possible to automate this. Can I tell Git to trust that both new chunks can just be used one after the other?

I think, if the insertions would be a few lines apart (a situation Git resolves automatically), it wouldn't be any less likely to introduce bugs. I am going to check the result anyway.

(Currently I am using DiffMerge as my mergetool, if that makes any difference.)

AndreKR
  • 32,613
  • 18
  • 106
  • 168
  • What if you have two branches, one of which introduced a `return 1;` statement, and the other introduced a `return -1;` statement? I would argue that only one (if any) of those is likely to be correct, and that simply concatenating the two is exactly the wrong thing to do. Correct merging in all but trivial cases requires actual thinking to do correctly, and generally requires that the merger have a decent understanding of what the surrounding code is supposed to be doing, in order to make intelligent decisions... – twalberg Jun 23 '15 at 19:29
  • 1
    The existance of things like [Semantic Merge](https://www.semanticmerge.com/) is proof that merging changes in a single file is in general non-trivial. – poke Jun 23 '15 at 19:54
  • 1
    @twalberg If the two returns would be in the same function but a few lines apart, Git would merge them just fine and I dare to say that's equally likely to intruduce a bug. The time I save by not having to merge them manuallly I can spend on double-checking any semantic implications. – AndreKR Jun 23 '15 at 20:19
  • @AndreKR Correct. However, I was referring to the case where two branches introduced different lines at the exact same spot... – twalberg Jun 23 '15 at 21:18
  • With the volume of merging done in some projects using git, I think you can take it as read that anything git refuses to do automatically has been tried and found error-prone. – jthill Jun 28 '15 at 18:47

1 Answers1

9

You would need to declare a merge driver for that:

This is assigned in a .gitattributes done in the destination branch (the one where you are doing the merge)

echo yourFiles merge=addTheirs>.gitattributes
git add .gitattributes
git commit -m "record addTheirs merge driver"

(replace yourFiles by the pattern of files you want to see that merge resolution applied)

But it also needs to be configured locally:

git config merge.addTheirs.name "keep additions only"
git config merge.addTheirs.driver addTheirs.sh

The trick is on the addTheirs scripts, called with %O, %A, %B (ancestor, ours, their)

A diff -u %A %B will give you hunks of diff like:

@@ -1,11 +1,11 @@
 line 1
 line 2
-line 3 from master
+line 3 from dev
 line 4
 line 5
 line 6
 line 7
 line 8
+line 8bis from dev
 line 9
 line 10
-line 11

Even though the diff header is missing, a patch would still be able to add new lines and removes old one (and what you want is to keep ours, and add theirs).
On Windows, you can take a gnu patch, but you need to add a manifest.

patch -u -p0 $3 $2

You could try to filter out the deletions from the patch before applying it.

patch=$(diff -u $2 $3 | grep -v "^-")

But the hunk header (@@ -1,11 +1,11 @@) would no longer match the number of lines expected (if you only add 2 lines and removes 0, it should end with +1,13, nit +1,11)

You need to process your patch in order to:

  • filter out the deletions
  • adjust the hunk headers

That means the addTheirs.sh (to put anywhere in your path, even on Windows) could be:

#!/bin/bash

patch=$(diff -u $2 $3) 
echo "${patch}" > f.pp

patch2=$(./padd f.pp)
echo "$patch2" > f.p

patch -u -p0 $2 -i f.p

(diff is part of the 200+ unix commands part of the git-for-windows package, so again, all of this works on Windows or on Unix)

The 'padd' (patch add) utility is a script removing any deletion line from each hunk, and updating the hunk header to keep track of the actual line number.

I made mine in Go (https://golang.org/, simply unzip a go distro anywhere you want and add it to your PATH)

Copy the following in a padd.go file, and type go build padd.go: you get the padd executable that the merge driver can call to adjust the patch.

package main

import (
    "fmt"
    "io/ioutil"
    "os"
    "regexp"
    "strconv"
    "strings"
)

// @@ 1,11 1,13 @@ <= extract prefix '@@ 1, 11 1,' and counter '13'
var chunkre = regexp.MustCompile(`(?m)^(@@.*,)(\d+)\s+@@.*$`)
var patch = ""

func main() {
    fname := os.Args[1]
    f := ""
    if b, err := ioutil.ReadFile(fname); err != nil {
        panic(err)
    } else {
        f = string(b)
    }
    lines := strings.Split(f, "\n")

    prefix := ""
    counter := 0
    var err error
    hunk := ""
    for _, line := range lines {
        snbadd := chunkre.FindAllStringSubmatch(line, -1)
        if len(snbadd) > 0 {
            updatePatch(hunk, prefix, counter)
            hunk = ""
            prefix = snbadd[0][1]
            if counter, err = strconv.Atoi(snbadd[0][2]); err != nil {
                panic(err)
            }
        } else if prefix != "" {
            if strings.HasPrefix(line, "-") {
                counter = counter + 1
                line = " " + line[1:]
            }
            hunk = hunk + line + "\n"
        }
    }
    updatePatch(hunk, prefix, counter)
    fmt.Println(patch)
}

func updatePatch(hunk, prefix string, counter int) {
    if hunk != "" {
        header := prefix + fmt.Sprintf("%d @@\n", counter)
        patch = patch + header + hunk
    }
}
Community
  • 1
  • 1
VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
  • Ok, I created a file, committed it to master, added a line and changed a different line, committed it, went back, added another line in the same spot and changed the same line differently, committed it to branch "test", then used your method to merge "test" into "master". In fact both conflicts were automatically resolved, but now it's identical to the version in "master". There was an error: http://pastebin.com/U3ADUVjv – AndreKR Jun 28 '15 at 00:45
  • @AndreKR what was the content of f.p and f.pp? – VonC Jun 28 '15 at 05:55
  • One newline (0x0A) in both files. – AndreKR Jul 01 '15 at 00:21