Skip to content

Commit f3ef6b6

Browse files
author
Junio C Hamano
committed
Custom low-level merge driver support.
This allows users to specify custom low-level merge driver per path, using the attributes mechanism. Just like you can specify one of built-in "text", "binary", "union" low-level merge drivers by saying: * merge=text .gitignore merge=union *.jpg merge=binary pick a name of your favorite merge driver, and assign it as the value of the 'merge' attribute. A custom low-level merge driver is defined via the config mechanism. This patch introduces 'merge.driver', a multi-valued configuration. Its value is the name (i.e. the one you use as the value of 'merge' attribute) followed by a command line specification. The command line can contain %O, %A, and %B to be interpolated with the names of temporary files that hold the common ancestor version, the version from your branch, and the version from the other branch, and the resulting command is spawned. The low-level merge driver is expected to update the temporary file for your branch (i.e. %A) with the result and exit with status 0 for a clean merge, and non-zero status for a conflicted merge. A new test in t6026 demonstrates a sample usage. Signed-off-by: Junio C Hamano <junkio@cox.net>
1 parent 47579ef commit f3ef6b6

File tree

2 files changed

+235
-13
lines changed

2 files changed

+235
-13
lines changed

merge-recursive.c

Lines changed: 165 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include "unpack-trees.h"
1616
#include "path-list.h"
1717
#include "xdiff-interface.h"
18+
#include "interpolate.h"
1819
#include "attr.h"
1920

2021
static int subtree_merge;
@@ -661,12 +662,14 @@ static void fill_mm(const unsigned char *sha1, mmfile_t *mm)
661662
}
662663

663664
/* Low-level merge functions */
664-
typedef int (*ll_merge_fn)(mmfile_t *orig,
665+
typedef int (*ll_merge_fn)(const char *cmd,
666+
mmfile_t *orig,
665667
mmfile_t *src1, const char *name1,
666668
mmfile_t *src2, const char *name2,
667669
mmbuffer_t *result);
668670

669-
static int ll_xdl_merge(mmfile_t *orig,
671+
static int ll_xdl_merge(const char *cmd__unused,
672+
mmfile_t *orig,
670673
mmfile_t *src1, const char *name1,
671674
mmfile_t *src2, const char *name2,
672675
mmbuffer_t *result)
@@ -681,7 +684,8 @@ static int ll_xdl_merge(mmfile_t *orig,
681684
result);
682685
}
683686

684-
static int ll_union_merge(mmfile_t *orig,
687+
static int ll_union_merge(const char *cmd__unused,
688+
mmfile_t *orig,
685689
mmfile_t *src1, const char *name1,
686690
mmfile_t *src2, const char *name2,
687691
mmbuffer_t *result)
@@ -690,7 +694,8 @@ static int ll_union_merge(mmfile_t *orig,
690694
long size;
691695
const int marker_size = 7;
692696

693-
int status = ll_xdl_merge(orig, src1, NULL, src2, NULL, result);
697+
int status = ll_xdl_merge(cmd__unused, orig,
698+
src1, NULL, src2, NULL, result);
694699
if (status <= 0)
695700
return status;
696701
size = result->size;
@@ -721,7 +726,8 @@ static int ll_union_merge(mmfile_t *orig,
721726
return 0;
722727
}
723728

724-
static int ll_binary_merge(mmfile_t *orig,
729+
static int ll_binary_merge(const char *cmd__unused,
730+
mmfile_t *orig,
725731
mmfile_t *src1, const char *name1,
726732
mmfile_t *src2, const char *name2,
727733
mmbuffer_t *result)
@@ -743,24 +749,169 @@ static struct {
743749
const char *name;
744750
ll_merge_fn fn;
745751
} ll_merge_fns[] = {
746-
{ "text", ll_xdl_merge },
747752
{ "binary", ll_binary_merge },
753+
{ "text", ll_xdl_merge },
748754
{ "union", ll_union_merge },
749755
{ NULL, NULL },
750756
};
751757

752-
static ll_merge_fn find_ll_merge_fn(void *merge_attr)
758+
static void create_temp(mmfile_t *src, char *path)
753759
{
760+
int fd;
761+
762+
strcpy(path, ".merge_file_XXXXXX");
763+
fd = mkstemp(path);
764+
if (fd < 0)
765+
die("unable to create temp-file");
766+
if (write_in_full(fd, src->ptr, src->size) != src->size)
767+
die("unable to write temp-file");
768+
close(fd);
769+
}
770+
771+
static int ll_ext_merge(const char *cmd,
772+
mmfile_t *orig,
773+
mmfile_t *src1, const char *name1,
774+
mmfile_t *src2, const char *name2,
775+
mmbuffer_t *result)
776+
{
777+
char temp[3][50];
778+
char cmdbuf[2048];
779+
struct interp table[] = {
780+
{ "%O" },
781+
{ "%A" },
782+
{ "%B" },
783+
};
784+
struct child_process child;
785+
const char *args[20];
786+
int status, fd, i;
787+
struct stat st;
788+
789+
result->ptr = NULL;
790+
result->size = 0;
791+
create_temp(orig, temp[0]);
792+
create_temp(src1, temp[1]);
793+
create_temp(src2, temp[2]);
794+
795+
interp_set_entry(table, 0, temp[0]);
796+
interp_set_entry(table, 1, temp[1]);
797+
interp_set_entry(table, 2, temp[2]);
798+
799+
interpolate(cmdbuf, sizeof(cmdbuf), cmd, table, 3);
800+
801+
memset(&child, 0, sizeof(child));
802+
child.argv = args;
803+
args[0] = "sh";
804+
args[1] = "-c";
805+
args[2] = cmdbuf;
806+
args[3] = NULL;
807+
808+
status = run_command(&child);
809+
if (status < -ERR_RUN_COMMAND_FORK)
810+
; /* failure in run-command */
811+
else
812+
status = -status;
813+
fd = open(temp[1], O_RDONLY);
814+
if (fd < 0)
815+
goto bad;
816+
if (fstat(fd, &st))
817+
goto close_bad;
818+
result->size = st.st_size;
819+
result->ptr = xmalloc(result->size + 1);
820+
if (read_in_full(fd, result->ptr, result->size) != result->size) {
821+
free(result->ptr);
822+
result->ptr = NULL;
823+
result->size = 0;
824+
}
825+
close_bad:
826+
close(fd);
827+
bad:
828+
for (i = 0; i < 3; i++)
829+
unlink(temp[i]);
830+
return status;
831+
}
832+
833+
/*
834+
* merge.default and merge.driver configuration items
835+
*/
836+
static struct user_merge_fn {
837+
struct user_merge_fn *next;
838+
const char *name;
839+
char *cmdline;
840+
char b_[1];
841+
} *ll_user_merge_fns, **ll_user_merge_fns_tail;
842+
843+
static int read_merge_config(const char *var, const char *value)
844+
{
845+
struct user_merge_fn *fn;
846+
int blen, nlen;
847+
848+
if (strcmp(var, "merge.driver"))
849+
return 0;
850+
if (!value)
851+
return error("%s: lacks value", var);
852+
/*
853+
* merge.driver is a multi-valued configuration, whose value is
854+
* of form:
855+
*
856+
* name command-line
857+
*
858+
* The command-line will be interpolated with the following
859+
* tokens and is given to the shell:
860+
*
861+
* %O - temporary file name for the merge base.
862+
* %A - temporary file name for our version.
863+
* %B - temporary file name for the other branches' version.
864+
*
865+
* The external merge driver should write the results in the file
866+
* named by %A, and signal that it has done with exit status 0.
867+
*/
868+
for (nlen = -1, blen = 0; value[blen]; blen++)
869+
if (nlen < 0 && isspace(value[blen]))
870+
nlen = blen;
871+
if (nlen < 0)
872+
return error("%s '%s': lacks command line", var, value);
873+
fn = xcalloc(1, sizeof(struct user_merge_fn) + blen + 1);
874+
memcpy(fn->b_, value, blen + 1);
875+
fn->name = fn->b_;
876+
fn->b_[nlen] = 0;
877+
fn->cmdline = fn->b_ + nlen + 1;
878+
fn->next = *ll_user_merge_fns_tail;
879+
*ll_user_merge_fns_tail = fn;
880+
return 0;
881+
}
882+
883+
static void initialize_ll_merge(void)
884+
{
885+
if (ll_user_merge_fns_tail)
886+
return;
887+
ll_user_merge_fns_tail = &ll_user_merge_fns;
888+
git_config(read_merge_config);
889+
}
890+
891+
static ll_merge_fn find_ll_merge_fn(void *merge_attr, const char **cmdline)
892+
{
893+
struct user_merge_fn *fn;
754894
const char *name;
755895
int i;
756896

757-
if (ATTR_TRUE(merge_attr) || ATTR_UNSET(merge_attr))
897+
initialize_ll_merge();
898+
899+
if (ATTR_TRUE(merge_attr))
758900
return ll_xdl_merge;
759901
else if (ATTR_FALSE(merge_attr))
760902
return ll_binary_merge;
903+
else if (ATTR_UNSET(merge_attr))
904+
return ll_xdl_merge;
905+
else
906+
name = merge_attr;
907+
908+
for (fn = ll_user_merge_fns; fn; fn = fn->next) {
909+
if (!strcmp(fn->name, name)) {
910+
*cmdline = fn->cmdline;
911+
return ll_ext_merge;
912+
}
913+
}
761914

762-
/* Otherwise merge_attr may name the merge function */
763-
name = merge_attr;
764915
for (i = 0; ll_merge_fns[i].name; i++)
765916
if (!strcmp(ll_merge_fns[i].name, name))
766917
return ll_merge_fns[i].fn;
@@ -793,6 +944,7 @@ static int ll_merge(mmbuffer_t *result_buf,
793944
int merge_status;
794945
void *merge_attr;
795946
ll_merge_fn fn;
947+
const char *driver = NULL;
796948

797949
name1 = xstrdup(mkpath("%s:%s", branch1, a->path));
798950
name2 = xstrdup(mkpath("%s:%s", branch2, b->path));
@@ -802,9 +954,10 @@ static int ll_merge(mmbuffer_t *result_buf,
802954
fill_mm(b->sha1, &src2);
803955

804956
merge_attr = git_path_check_merge(a->path);
805-
fn = find_ll_merge_fn(merge_attr);
957+
fn = find_ll_merge_fn(merge_attr, &driver);
806958

807-
merge_status = fn(&orig, &src1, name1, &src2, name2, result_buf);
959+
merge_status = fn(driver, &orig,
960+
&src1, name1, &src2, name2, result_buf);
808961

809962
free(name1);
810963
free(name2);

t/t6026-merge-attr.sh

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,9 @@ test_expect_success setup '
3030
echo Side >>$f && git add $f || break
3131
done &&
3232
test_tick &&
33-
git commit -m Side
33+
git commit -m Side &&
3434
35+
git tag anchor
3536
'
3637

3738
test_expect_success merge '
@@ -69,4 +70,72 @@ test_expect_success 'check merge result in working tree' '
6970
7071
'
7172

73+
cat >./custom-merge <<\EOF
74+
#!/bin/sh
75+
76+
orig="$1" ours="$2" theirs="$3" exit="$4"
77+
(
78+
echo "orig is $orig"
79+
echo "ours is $ours"
80+
echo "theirs is $theirs"
81+
echo "=== orig ==="
82+
cat "$orig"
83+
echo "=== ours ==="
84+
cat "$ours"
85+
echo "=== theirs ==="
86+
cat "$theirs"
87+
) >"$ours+"
88+
cat "$ours+" >"$ours"
89+
rm -f "$ours+"
90+
exit "$exit"
91+
EOF
92+
chmod +x ./custom-merge
93+
94+
test_expect_success 'custom merge backend' '
95+
96+
echo "* merge=union" >.gitattributes &&
97+
echo "text merge=custom" >>.gitattributes &&
98+
99+
git reset --hard anchor &&
100+
git config --replace-all \
101+
merge.driver "custom ./custom-merge %O %A %B 0" &&
102+
103+
git merge master &&
104+
105+
cmp binary union &&
106+
sed -e 1,3d text >check-1 &&
107+
o=$(git-unpack-file master^:text) &&
108+
a=$(git-unpack-file side^:text) &&
109+
b=$(git-unpack-file master:text) &&
110+
sh -c "./custom-merge $o $a $b 0" &&
111+
sed -e 1,3d $a >check-2 &&
112+
cmp check-1 check-2 &&
113+
rm -f $o $a $b
114+
'
115+
116+
test_expect_success 'custom merge backend' '
117+
118+
git reset --hard anchor &&
119+
git config --replace-all \
120+
merge.driver "custom ./custom-merge %O %A %B 1" &&
121+
122+
if git merge master
123+
then
124+
echo "Eh? should have conflicted"
125+
false
126+
else
127+
echo "Ok, conflicted"
128+
fi &&
129+
130+
cmp binary union &&
131+
sed -e 1,3d text >check-1 &&
132+
o=$(git-unpack-file master^:text) &&
133+
a=$(git-unpack-file anchor:text) &&
134+
b=$(git-unpack-file master:text) &&
135+
sh -c "./custom-merge $o $a $b 0" &&
136+
sed -e 1,3d $a >check-2 &&
137+
cmp check-1 check-2 &&
138+
rm -f $o $a $b
139+
'
140+
72141
test_done

0 commit comments

Comments
 (0)