Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

A new merge stragety 'subtree'.

This merge strategy largely piggy-backs on git-merge-recursive.
When merging trees A and B, if B corresponds to a subtree of A,
B is first adjusted to match the tree structure of A, instead of
reading the trees at the same level.  This adjustment is also
done to the common ancestor tree.

If you are pulling updates from git-gui repository into git.git
repository, the root level of the former corresponds to git-gui/
subdirectory of the latter.  The tree object of git-gui's toplevel
is wrapped in a fake tree object, whose sole entry has name 'git-gui'
and records object name of the true tree, before being used by
the 3-way merge code.

If you are merging the other way, only the git-gui/ subtree of
git.git is extracted and merged into git-gui's toplevel.

The detection of corresponding subtree is done by comparing the
pathnames and types in the toplevel of the tree.

Heuristics galore!  That's the git way ;-).

Signed-off-by: Junio C Hamano <junkio@cox.net>
  • Loading branch information...
commit 68faf68938ee943fc251c702f2027e4dfda354db 1 parent ee9693e
Junio C Hamano authored
View
2  .gitignore
@@ -77,6 +77,7 @@ git-merge-ours
git-merge-recursive
git-merge-resolve
git-merge-stupid
+git-merge-subtree
git-mergetool
git-mktag
git-mktree
@@ -148,6 +149,7 @@ test-chmtime
test-date
test-delta
test-dump-cache-tree
+test-match-trees
common-cmds.h
*.tar.gz
*.dsc
View
10 Makefile
@@ -251,6 +251,8 @@ BUILT_INS = \
# what 'all' will build and 'install' will install, in gitexecdir
ALL_PROGRAMS = $(PROGRAMS) $(SCRIPTS)
+ALL_PROGRAMS += git-merge-subtree$X
+
# what 'all' will build but not install in gitexecdir
OTHER_PROGRAMS = git$X gitweb/gitweb.cgi
ifndef NO_TCLTK
@@ -299,7 +301,7 @@ LIB_OBJS = \
server-info.o setup.o sha1_file.o sha1_name.o strbuf.o \
tag.o tree.o usage.o config.o environment.o ctype.o copy.o \
revision.o pager.o tree-walk.o xdiff-interface.o \
- write_or_die.o trace.o list-objects.o grep.o \
+ write_or_die.o trace.o list-objects.o grep.o match-trees.o \
alloc.o merge-file.o path-list.o help.o unpack-trees.o $(DIFF_OBJS) \
color.o wt-status.o archive-zip.o archive-tar.o shallow.o utf8.o \
convert.o
@@ -725,6 +727,9 @@ git$X: git.c common-cmds.h $(BUILTIN_OBJS) $(GITLIBS) GIT-CFLAGS
help.o: common-cmds.h
+git-merge-subtree$X: git-merge-recursive$X
+ rm -f $@ && ln git-merge-recursive$X $@
+
$(BUILT_INS): git$X
$(QUIET_BUILT_IN)rm -f $@ && ln git$X $@
@@ -942,6 +947,9 @@ test-dump-cache-tree$X: dump-cache-tree.o $(GITLIBS)
test-sha1$X: test-sha1.o $(GITLIBS)
$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) $(LIBS)
+test-match-trees$X: test-match-trees.o $(GITLIBS)
+ $(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) $(LIBS)
+
test-chmtime$X: test-chmtime.c
$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $<
View
3  cache.h
@@ -496,4 +496,7 @@ extern void trace_argv_printf(const char **argv, int count, const char *format,
extern int convert_to_git(const char *path, char **bufp, unsigned long *sizep);
extern int convert_to_working_tree(const char *path, char **bufp, unsigned long *sizep);
+/* match-trees.c */
+void shift_tree(const unsigned char *, const unsigned char *, unsigned char *, int);
+
#endif /* CACHE_H */
View
4 git-merge.sh
@@ -16,10 +16,10 @@ test -z "$(git ls-files -u)" ||
LF='
'
-all_strategies='recur recursive octopus resolve stupid ours'
+all_strategies='recur recursive octopus resolve stupid ours subtree'
default_twohead_strategies='recursive'
default_octopus_strategies='octopus'
-no_trivial_merge_strategies='ours'
+no_trivial_merge_strategies='ours subtree'
use_strategies=
index_merge=t
View
304 match-trees.c
@@ -0,0 +1,304 @@
+#include "cache.h"
+#include "tree.h"
+#include "tree-walk.h"
+
+static int score_missing(unsigned mode, const char *path)
+{
+ int score;
+
+ if (S_ISDIR(mode))
+ score = -1000;
+ else if (S_ISLNK(mode))
+ score = -500;
+ else
+ score = -50;
+ return score;
+}
+
+static int score_differs(unsigned mode1, unsigned mode2, const char *path)
+{
+ int score;
+
+ if (S_ISDIR(mode1) != S_ISDIR(mode2))
+ score = -100;
+ else if (S_ISLNK(mode1) != S_ISLNK(mode2))
+ score = -50;
+ else
+ score = -5;
+ return score;
+}
+
+static int score_matches(unsigned mode1, unsigned mode2, const char *path)
+{
+ int score;
+
+ /* Heh, we found SHA-1 collisions between different kind of objects */
+ if (S_ISDIR(mode1) != S_ISDIR(mode2))
+ score = -100;
+ else if (S_ISLNK(mode1) != S_ISLNK(mode2))
+ score = -50;
+
+ else if (S_ISDIR(mode1))
+ score = 1000;
+ else if (S_ISLNK(mode1))
+ score = 500;
+ else
+ score = 250;
+ return score;
+}
+
+/*
+ * Inspect two trees, and give a score that tells how similar they are.
+ */
+static int score_trees(const unsigned char *hash1, const unsigned char *hash2)
+{
+ struct tree_desc one;
+ struct tree_desc two;
+ void *one_buf, *two_buf;
+ int score = 0;
+ enum object_type type;
+ unsigned long size;
+
+ one_buf = read_sha1_file(hash1, &type, &size);
+ if (!one_buf)
+ die("unable to read tree (%s)", sha1_to_hex(hash1));
+ if (type != OBJ_TREE)
+ die("%s is not a tree", sha1_to_hex(hash1));
+ init_tree_desc(&one, one_buf, size);
+ two_buf = read_sha1_file(hash2, &type, &size);
+ if (!two_buf)
+ die("unable to read tree (%s)", sha1_to_hex(hash2));
+ if (type != OBJ_TREE)
+ die("%s is not a tree", sha1_to_hex(hash2));
+ init_tree_desc(&two, two_buf, size);
+ while (one.size | two.size) {
+ const unsigned char *elem1 = elem1;
+ const unsigned char *elem2 = elem2;
+ const char *path1 = path1;
+ const char *path2 = path2;
+ unsigned mode1 = mode1;
+ unsigned mode2 = mode2;
+ int cmp;
+
+ if (one.size)
+ elem1 = tree_entry_extract(&one, &path1, &mode1);
+ if (two.size)
+ elem2 = tree_entry_extract(&two, &path2, &mode2);
+
+ if (!one.size) {
+ /* two has more entries */
+ score += score_missing(mode2, path2);
+ update_tree_entry(&two);
+ continue;
+ }
+ if (!two.size) {
+ /* two lacks this entry */
+ score += score_missing(mode1, path1);
+ update_tree_entry(&one);
+ continue;
+ }
+ cmp = base_name_compare(path1, strlen(path1), mode1,
+ path2, strlen(path2), mode2);
+ if (cmp < 0) {
+ /* path1 does not appear in two */
+ score += score_missing(mode1, path1);
+ update_tree_entry(&one);
+ continue;
+ }
+ else if (cmp > 0) {
+ /* path2 does not appear in one */
+ score += score_missing(mode2, path2);
+ update_tree_entry(&two);
+ continue;
+ }
+ else if (hashcmp(elem1, elem2))
+ /* they are different */
+ score += score_differs(mode1, mode2, path1);
+ else
+ /* same subtree or blob */
+ score += score_matches(mode1, mode2, path1);
+ update_tree_entry(&one);
+ update_tree_entry(&two);
+ }
+ free(one_buf);
+ free(two_buf);
+ return score;
+}
+
+/*
+ * Match one itself and its subtrees with two and pick the best match.
+ */
+static void match_trees(const unsigned char *hash1,
+ const unsigned char *hash2,
+ int *best_score,
+ char **best_match,
+ char *base,
+ int recurse_limit)
+{
+ struct tree_desc one;
+ void *one_buf;
+ enum object_type type;
+ unsigned long size;
+
+ one_buf = read_sha1_file(hash1, &type, &size);
+ if (!one_buf)
+ die("unable to read tree (%s)", sha1_to_hex(hash1));
+ if (type != OBJ_TREE)
+ die("%s is not a tree", sha1_to_hex(hash1));
+ init_tree_desc(&one, one_buf, size);
+
+ while (one.size) {
+ const char *path;
+ const unsigned char *elem;
+ unsigned mode;
+ int score;
+
+ elem = tree_entry_extract(&one, &path, &mode);
+ if (!S_ISDIR(mode))
+ goto next;
+ score = score_trees(elem, hash2);
+ if (*best_score < score) {
+ char *newpath;
+ newpath = xmalloc(strlen(base) + strlen(path) + 1);
+ sprintf(newpath, "%s%s", base, path);
+ free(*best_match);
+ *best_match = newpath;
+ *best_score = score;
+ }
+ if (recurse_limit) {
+ char *newbase;
+ newbase = xmalloc(strlen(base) + strlen(path) + 2);
+ sprintf(newbase, "%s%s/", base, path);
+ match_trees(elem, hash2, best_score, best_match,
+ newbase, recurse_limit - 1);
+ free(newbase);
+ }
+
+ next:
+ update_tree_entry(&one);
+ }
+ free(one_buf);
+}
+
+/*
+ * A tree "hash1" has a subdirectory at "prefix". Come up with a
+ * tree object by replacing it with another tree "hash2".
+ */
+static int splice_tree(const unsigned char *hash1,
+ char *prefix,
+ const unsigned char *hash2,
+ unsigned char *result)
+{
+ char *subpath;
+ int toplen;
+ char *buf;
+ unsigned long sz;
+ struct tree_desc desc;
+ unsigned char *rewrite_here;
+ const unsigned char *rewrite_with;
+ unsigned char subtree[20];
+ enum object_type type;
+ int status;
+
+ subpath = strchr(prefix, '/');
+ if (!subpath)
+ toplen = strlen(prefix);
+ else {
+ toplen = subpath - prefix;
+ subpath++;
+ }
+
+ buf = read_sha1_file(hash1, &type, &sz);
+ if (!buf)
+ die("cannot read tree %s", sha1_to_hex(hash1));
+ init_tree_desc(&desc, buf, sz);
+
+ rewrite_here = NULL;
+ while (desc.size) {
+ const char *name;
+ unsigned mode;
+ const unsigned char *sha1;
+
+ sha1 = tree_entry_extract(&desc, &name, &mode);
+ if (strlen(name) == toplen &&
+ !memcmp(name, prefix, toplen)) {
+ if (!S_ISDIR(mode))
+ die("entry %s in tree %s is not a tree",
+ name, sha1_to_hex(hash1));
+ rewrite_here = (unsigned char *) sha1;
+ break;
+ }
+ update_tree_entry(&desc);
+ }
+ if (!rewrite_here)
+ die("entry %.*s not found in tree %s",
+ toplen, prefix, sha1_to_hex(hash1));
+ if (subpath) {
+ status = splice_tree(rewrite_here, subpath, hash2, subtree);
+ if (status)
+ return status;
+ rewrite_with = subtree;
+ }
+ else
+ rewrite_with = hash2;
+ hashcpy(rewrite_here, rewrite_with);
+ status = write_sha1_file(buf, sz, tree_type, result);
+ free(buf);
+ return status;
+}
+
+/*
+ * We are trying to come up with a merge between one and two that
+ * results in a tree shape similar to one. The tree two might
+ * correspond to a subtree of one, in which case it needs to be
+ * shifted down by prefixing otherwise empty directories. On the
+ * other hand, it could cover tree one and we might need to pick a
+ * subtree of it.
+ */
+void shift_tree(const unsigned char *hash1,
+ const unsigned char *hash2,
+ unsigned char *shifted,
+ int depth_limit)
+{
+ char *add_prefix;
+ char *del_prefix;
+ int add_score, del_score;
+
+ add_score = del_score = score_trees(hash1, hash2);
+ add_prefix = xcalloc(1, 1);
+ del_prefix = xcalloc(1, 1);
+
+ /*
+ * See if one's subtree resembles two; if so we need to prefix
+ * two with a few fake trees to match the prefix.
+ */
+ match_trees(hash1, hash2, &add_score, &add_prefix, "", depth_limit);
+
+ /*
+ * See if two's subtree resembles one; if so we need to
+ * pick only subtree of two.
+ */
+ match_trees(hash2, hash1, &del_score, &del_prefix, "", depth_limit);
+
+ /* Assume we do not have to do any shifting */
+ hashcpy(shifted, hash2);
+
+ if (add_score < del_score) {
+ /* We need to pick a subtree of two */
+ unsigned mode;
+
+ if (!*del_prefix)
+ return;
+
+ if (get_tree_entry(hash2, del_prefix, shifted, &mode))
+ die("cannot find path %s in tree %s",
+ del_prefix, sha1_to_hex(hash2));
+ return;
+ }
+
+ if (!*add_prefix)
+ return;
+
+ splice_tree(hash1, add_prefix, hash2, shifted);
+}
+
View
29 merge-recursive.c
@@ -16,6 +16,22 @@
#include "path-list.h"
#include "xdiff-interface.h"
+static int subtree_merge;
+
+static struct tree *shift_tree_object(struct tree *one, struct tree *two)
+{
+ unsigned char shifted[20];
+
+ /*
+ * NEEDSWORK: this limits the recursion depth to hardcoded
+ * value '2' to avoid excessive overhead.
+ */
+ shift_tree(one->object.sha1, two->object.sha1, shifted, 2);
+ if (!hashcmp(two->object.sha1, shifted))
+ return two;
+ return lookup_tree(shifted);
+}
+
/*
* A virtual commit has
* - (const char *)commit->util set to the name, and
@@ -1137,6 +1153,12 @@ static int merge_trees(struct tree *head,
struct tree **result)
{
int code, clean;
+
+ if (subtree_merge) {
+ merge = shift_tree_object(head, merge);
+ common = shift_tree_object(head, common);
+ }
+
if (sha_eq(common->object.sha1, merge->object.sha1)) {
output(0, "Already uptodate!");
*result = head;
@@ -1342,6 +1364,13 @@ int main(int argc, char *argv[])
struct lock_file *lock = xcalloc(1, sizeof(struct lock_file));
int index_fd;
+ if (argv[0]) {
+ int namelen = strlen(argv[0]);
+ if (8 < namelen &&
+ !strcmp(argv[0] + namelen - 8, "-subtree"))
+ subtree_merge = 1;
+ }
+
git_config(merge_config);
if (getenv("GIT_MERGE_VERBOSITY"))
verbosity = strtol(getenv("GIT_MERGE_VERBOSITY"), NULL, 10);
View
24 test-match-trees.c
@@ -0,0 +1,24 @@
+#include "cache.h"
+#include "tree.h"
+
+int main(int ac, char **av)
+{
+ unsigned char hash1[20], hash2[20], shifted[20];
+ struct tree *one, *two;
+
+ if (get_sha1(av[1], hash1))
+ die("cannot parse %s as an object name", av[1]);
+ if (get_sha1(av[2], hash2))
+ die("cannot parse %s as an object name", av[2]);
+ one = parse_tree_indirect(hash1);
+ if (!one)
+ die("not a treeish %s", av[1]);
+ two = parse_tree_indirect(hash2);
+ if (!two)
+ die("not a treeish %s", av[2]);
+
+ shift_tree(one->object.sha1, two->object.sha1, shifted, -1);
+ printf("shifted: %s\n", sha1_to_hex(shifted));
+
+ exit(0);
+}
Please sign in to comment.
Something went wrong with that request. Please try again.