* [PATCH 00/15] Build in merge @ 2008-06-27 16:21 Miklos Vajna 2008-06-27 16:21 ` [PATCH 01/15] Move split_cmdline() to alias.c Miklos Vajna 0 siblings, 1 reply; 82+ messages in thread From: Miklos Vajna @ 2008-06-27 16:21 UTC (permalink / raw To: Junio C Hamano; +Cc: git, Johannes Schindelin, Olivier Marin Hello, Dscho gave me a detailed review on builtin-merge, so I'm sending an updated series. It's based on 49646c5 in git.git, so hopefully it includes all the work Junio did recently in 'pu'. (PARSE_OPT_ARGV0_IS_AN_OPTION removal, etc.) Changes: * "git-fmt-merge-msg: make it usable from other builtins" - Small cleanup. * "Build in merge" - When a custom message was given for a merge, the result was not identical to the one git-merge.sh had. I added a testcase for this and fixed up builtin-merge to pass the test. - Memleak fix in restore_state() - Cleanup in finish_up_to_date() and squash_message() - run_hook() now first checks if the executable bit is set and returns immediately if not. - Make merge_name() a bit more readable, like *(ptr+1) -> ptr[1]. Also fixed a missing initialization in this function. - split_merge_strategies(): while (1) -> for (;;) - Simplified path_list_append_strategy() by removing unnecessary parameters. - cmd_merge(): Added more comments to make the code more readable, fixed a memory leak, optimized the case when --no-commit is used. - merge_one_remote(): Fixed a bug which for example caused a segfault when building with -fno-inline. - Fixed up color handling when showing the diff after a merge, as noticed by Olivier Marin. - Fixed up the "Updating foo..bar" message which was like "Updating bar..bar", as pointed out by Olivier Marin. * Dscho's two patches: These introduces strbuf_initf() which can be used instead of strbuf_init() and strbuf_addf(). Modified builtin-merge.c to use strbuf_initf() where possible. * Extended t7601-merge-pull-config.sh to make sure git-merge picks up the best strategy when no strategy can handle the merge without conflicts. The "interdiff" is available via git diff 49646c5..d1c62b2 in git://repo.or.cz/git/vmiklos.git. I'm sending the whole series to avoid complexity, but in fact I only changed the following patches: * "git-fmt-merge-msg: make it usable from other builtins" * "Add new test case to ensure git-merge prepends the custom merge message" * "Build in merge" Johannes Schindelin (2): Add strbuf_vaddf(), use it in strbuf_addf(), and add strbuf_initf() strbuf_vaddf(): support %*s, too Junio C Hamano (2): Introduce get_merge_bases_many() Introduce reduce_heads() Miklos Vajna (11): Move split_cmdline() to alias.c Move commit_list_count() to commit.c Move parse-options's skip_prefix() to git-compat-util.h Add new test to ensure git-merge handles pull.twohead and pull.octopus Move read_cache_unmerged() to read-cache.c git-fmt-merge-msg: make it usable from other builtins Introduce get_octopus_merge_bases() in commit.c Add new test to ensure git-merge handles more than 25 refs. Add new test case to ensure git-merge reduces octopus parents when possible Add new test case to ensure git-merge prepends the custom merge message Build in merge .gitignore | 1 + Makefile | 6 +- alias.c | 54 ++ builtin-fmt-merge-msg.c | 155 ++-- builtin-merge-recursive.c | 8 - builtin-merge.c | 1143 +++++++++++++++++++++++++ builtin-read-tree.c | 24 - builtin-remote.c | 39 +- builtin.h | 4 + cache.h | 3 + commit.c | 136 +++- commit.h | 4 + git-merge.sh => contrib/examples/git-merge.sh | 0 git-compat-util.h | 6 + git.c | 54 +-- parse-options.c | 6 - read-cache.c | 31 + strbuf.c | 140 +++- strbuf.h | 3 + t/t0000-basic.sh | 8 + t/t7601-merge-pull-config.sh | 129 +++ t/t7602-merge-octopus-many.sh | 52 ++ t/t7603-merge-reduce-heads.sh | 63 ++ t/t7604-merge-custom-message.sh | 37 + test-strbuf.c | 17 + 25 files changed, 1917 insertions(+), 206 deletions(-) create mode 100644 builtin-merge.c rename git-merge.sh => contrib/examples/git-merge.sh (100%) create mode 100755 t/t7601-merge-pull-config.sh create mode 100755 t/t7602-merge-octopus-many.sh create mode 100755 t/t7603-merge-reduce-heads.sh create mode 100755 t/t7604-merge-custom-message.sh create mode 100644 test-strbuf.c ^ permalink raw reply [flat|nested] 82+ messages in thread
* [PATCH 01/15] Move split_cmdline() to alias.c 2008-06-27 16:21 [PATCH 00/15] Build in merge Miklos Vajna @ 2008-06-27 16:21 ` Miklos Vajna 2008-06-27 16:21 ` [PATCH 02/15] Move commit_list_count() to commit.c Miklos Vajna 2008-06-29 14:05 ` [PATCH 01/15] Move split_cmdline() to alias.c Olivier Marin 0 siblings, 2 replies; 82+ messages in thread From: Miklos Vajna @ 2008-06-27 16:21 UTC (permalink / raw To: Junio C Hamano; +Cc: git, Johannes Schindelin, Olivier Marin, Junio C Hamano split_cmdline() is currently used for aliases only, but later it can be useful for other builtins as well. Move it to alias.c for now, indicating that originally it's for aliases, but we'll have it in libgit this way. Signed-off-by: Miklos Vajna <vmiklos@frugalware.org> Signed-off-by: Junio C Hamano <gitster@pobox.com> --- alias.c | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ cache.h | 1 + git.c | 53 ----------------------------------------------------- 3 files changed, 55 insertions(+), 53 deletions(-) diff --git a/alias.c b/alias.c index 995f3e6..ccb1108 100644 --- a/alias.c +++ b/alias.c @@ -21,3 +21,57 @@ char *alias_lookup(const char *alias) git_config(alias_lookup_cb, NULL); return alias_val; } + +int split_cmdline(char *cmdline, const char ***argv) +{ + int src, dst, count = 0, size = 16; + char quoted = 0; + + *argv = xmalloc(sizeof(char*) * size); + + /* split alias_string */ + (*argv)[count++] = cmdline; + for (src = dst = 0; cmdline[src];) { + char c = cmdline[src]; + if (!quoted && isspace(c)) { + cmdline[dst++] = 0; + while (cmdline[++src] + && isspace(cmdline[src])) + ; /* skip */ + if (count >= size) { + size += 16; + *argv = xrealloc(*argv, sizeof(char*) * size); + } + (*argv)[count++] = cmdline + dst; + } else if (!quoted && (c == '\'' || c == '"')) { + quoted = c; + src++; + } else if (c == quoted) { + quoted = 0; + src++; + } else { + if (c == '\\' && quoted != '\'') { + src++; + c = cmdline[src]; + if (!c) { + free(*argv); + *argv = NULL; + return error("cmdline ends with \\"); + } + } + cmdline[dst++] = c; + src++; + } + } + + cmdline[dst] = 0; + + if (quoted) { + free(*argv); + *argv = NULL; + return error("unclosed quote"); + } + + return count; +} + diff --git a/cache.h b/cache.h index c8954ef..d5463d8 100644 --- a/cache.h +++ b/cache.h @@ -830,5 +830,6 @@ int report_path_error(const char *ps_matched, const char **pathspec, int prefix_ void overlay_tree_on_cache(const char *tree_name, const char *prefix); char *alias_lookup(const char *alias); +int split_cmdline(char *cmdline, const char ***argv); #endif /* CACHE_H */ diff --git a/git.c b/git.c index 59f0fcc..2fbe96b 100644 --- a/git.c +++ b/git.c @@ -90,59 +90,6 @@ static int handle_options(const char*** argv, int* argc, int* envchanged) return handled; } -static int split_cmdline(char *cmdline, const char ***argv) -{ - int src, dst, count = 0, size = 16; - char quoted = 0; - - *argv = xmalloc(sizeof(char*) * size); - - /* split alias_string */ - (*argv)[count++] = cmdline; - for (src = dst = 0; cmdline[src];) { - char c = cmdline[src]; - if (!quoted && isspace(c)) { - cmdline[dst++] = 0; - while (cmdline[++src] - && isspace(cmdline[src])) - ; /* skip */ - if (count >= size) { - size += 16; - *argv = xrealloc(*argv, sizeof(char*) * size); - } - (*argv)[count++] = cmdline + dst; - } else if(!quoted && (c == '\'' || c == '"')) { - quoted = c; - src++; - } else if (c == quoted) { - quoted = 0; - src++; - } else { - if (c == '\\' && quoted != '\'') { - src++; - c = cmdline[src]; - if (!c) { - free(*argv); - *argv = NULL; - return error("cmdline ends with \\"); - } - } - cmdline[dst++] = c; - src++; - } - } - - cmdline[dst] = 0; - - if (quoted) { - free(*argv); - *argv = NULL; - return error("unclosed quote"); - } - - return count; -} - static int handle_alias(int *argcp, const char ***argv) { int envchanged = 0, ret = 0, saved_errno = errno; -- 1.5.6 ^ permalink raw reply related [flat|nested] 82+ messages in thread
* [PATCH 02/15] Move commit_list_count() to commit.c 2008-06-27 16:21 ` [PATCH 01/15] Move split_cmdline() to alias.c Miklos Vajna @ 2008-06-27 16:21 ` Miklos Vajna 2008-06-27 16:21 ` [PATCH 03/15] Move parse-options's skip_prefix() to git-compat-util.h Miklos Vajna 2008-06-29 14:05 ` [PATCH 01/15] Move split_cmdline() to alias.c Olivier Marin 1 sibling, 1 reply; 82+ messages in thread From: Miklos Vajna @ 2008-06-27 16:21 UTC (permalink / raw To: Junio C Hamano; +Cc: git, Johannes Schindelin, Olivier Marin, Junio C Hamano This function is useful outside builtin-merge-recursive, for example in builtin-merge. Signed-off-by: Miklos Vajna <vmiklos@frugalware.org> Signed-off-by: Junio C Hamano <gitster@pobox.com> --- builtin-merge-recursive.c | 8 -------- commit.c | 8 ++++++++ commit.h | 1 + 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/builtin-merge-recursive.c b/builtin-merge-recursive.c index 43bf6aa..3731853 100644 --- a/builtin-merge-recursive.c +++ b/builtin-merge-recursive.c @@ -42,14 +42,6 @@ static struct tree *shift_tree_object(struct tree *one, struct tree *two) * - *(int *)commit->object.sha1 set to the virtual id. */ -static unsigned commit_list_count(const struct commit_list *l) -{ - unsigned c = 0; - for (; l; l = l->next ) - c++; - return c; -} - static struct commit *make_virtual_commit(struct tree *tree, const char *comment) { struct commit *commit = xcalloc(1, sizeof(struct commit)); diff --git a/commit.c b/commit.c index e2d8624..bbf9c75 100644 --- a/commit.c +++ b/commit.c @@ -325,6 +325,14 @@ struct commit_list *commit_list_insert(struct commit *item, struct commit_list * return new_list; } +unsigned commit_list_count(const struct commit_list *l) +{ + unsigned c = 0; + for (; l; l = l->next ) + c++; + return c; +} + void free_commit_list(struct commit_list *list) { while (list) { diff --git a/commit.h b/commit.h index 2d94d41..7f8c5ee 100644 --- a/commit.h +++ b/commit.h @@ -41,6 +41,7 @@ int parse_commit_buffer(struct commit *item, void *buffer, unsigned long size); int parse_commit(struct commit *item); struct commit_list * commit_list_insert(struct commit *item, struct commit_list **list_p); +unsigned commit_list_count(const struct commit_list *l); struct commit_list * insert_by_date(struct commit *item, struct commit_list **list); void free_commit_list(struct commit_list *list); -- 1.5.6 ^ permalink raw reply related [flat|nested] 82+ messages in thread
* [PATCH 03/15] Move parse-options's skip_prefix() to git-compat-util.h 2008-06-27 16:21 ` [PATCH 02/15] Move commit_list_count() to commit.c Miklos Vajna @ 2008-06-27 16:21 ` Miklos Vajna 2008-06-27 16:21 ` [PATCH 04/15] Add new test to ensure git-merge handles pull.twohead and pull.octopus Miklos Vajna 0 siblings, 1 reply; 82+ messages in thread From: Miklos Vajna @ 2008-06-27 16:21 UTC (permalink / raw To: Junio C Hamano; +Cc: git, Johannes Schindelin, Olivier Marin, Junio C Hamano builtin-remote.c and parse-options.c both have a skip_prefix() function, for the same purpose. Move parse-options's one to git-compat-util.h and let builtin-remote use it as well. Signed-off-by: Miklos Vajna <vmiklos@frugalware.org> Signed-off-by: Junio C Hamano <gitster@pobox.com> --- builtin-remote.c | 39 ++++++++++++++++++++++++++------------- git-compat-util.h | 6 ++++++ parse-options.c | 6 ------ 3 files changed, 32 insertions(+), 19 deletions(-) diff --git a/builtin-remote.c b/builtin-remote.c index 145dd85..1491354 100644 --- a/builtin-remote.c +++ b/builtin-remote.c @@ -29,12 +29,6 @@ static inline int postfixcmp(const char *string, const char *postfix) return strcmp(string + len1 - len2, postfix); } -static inline const char *skip_prefix(const char *name, const char *prefix) -{ - return !name ? "" : - prefixcmp(name, prefix) ? name : name + strlen(prefix); -} - static int opt_parse_track(const struct option *opt, const char *arg, int not) { struct path_list *list = opt->value; @@ -182,12 +176,18 @@ static int config_read_branches(const char *key, const char *value, void *cb) info->remote = xstrdup(value); } else { char *space = strchr(value, ' '); - value = skip_prefix(value, "refs/heads/"); + const char *ptr = skip_prefix(value, "refs/heads/"); + if (ptr) + value = ptr; while (space) { char *merge; merge = xstrndup(value, space - value); path_list_append(merge, &info->merge); - value = skip_prefix(space + 1, "refs/heads/"); + ptr = skip_prefix(space + 1, "refs/heads/"); + if (ptr) + value = ptr; + else + value = space + 1; space = strchr(value, ' '); } path_list_append(xstrdup(value), &info->merge); @@ -219,7 +219,12 @@ static int handle_one_branch(const char *refname, refspec.dst = (char *)refname; if (!remote_find_tracking(states->remote, &refspec)) { struct path_list_item *item; - const char *name = skip_prefix(refspec.src, "refs/heads/"); + const char *name, *ptr; + ptr = skip_prefix(refspec.src, "refs/heads/"); + if (ptr) + name = ptr; + else + name = refspec.src; /* symbolic refs pointing nowhere were handled already */ if ((flags & REF_ISSYMREF) || unsorted_path_list_has_path(&states->tracked, @@ -248,6 +253,7 @@ static int get_ref_states(const struct ref *ref, struct ref_states *states) struct path_list *target = &states->tracked; unsigned char sha1[20]; void *util = NULL; + const char *ptr; if (!ref->peer_ref || read_ref(ref->peer_ref->name, sha1)) target = &states->new; @@ -256,8 +262,10 @@ static int get_ref_states(const struct ref *ref, struct ref_states *states) if (hashcmp(sha1, ref->new_sha1)) util = &states; } - path_list_append(skip_prefix(ref->name, "refs/heads/"), - target)->util = util; + ptr = skip_prefix(ref->name, "refs/heads/"); + if (!ptr) + ptr = ref->name; + path_list_append(ptr, target)->util = util; } free_refs(fetch_map); @@ -522,10 +530,15 @@ static int show(int argc, const char **argv) "es" : ""); for (i = 0; i < states.remote->push_refspec_nr; i++) { struct refspec *spec = states.remote->push + i; + const char *p = "", *q = ""; + if (spec->src) + p = skip_prefix(spec->src, "refs/heads/"); + if (spec->dst) + q = skip_prefix(spec->dst, "refs/heads/"); printf(" %s%s%s%s", spec->force ? "+" : "", - skip_prefix(spec->src, "refs/heads/"), + p ? p : spec->src, spec->dst ? ":" : "", - skip_prefix(spec->dst, "refs/heads/")); + q ? q : spec->dst); } printf("\n"); } diff --git a/git-compat-util.h b/git-compat-util.h index 6f94a81..31edc98 100644 --- a/git-compat-util.h +++ b/git-compat-util.h @@ -127,6 +127,12 @@ extern void set_warn_routine(void (*routine)(const char *warn, va_list params)); extern int prefixcmp(const char *str, const char *prefix); +static inline const char *skip_prefix(const char *str, const char *prefix) +{ + size_t len = strlen(prefix); + return strncmp(str, prefix, len) ? NULL : str + len; +} + #ifdef NO_MMAP #ifndef PROT_READ diff --git a/parse-options.c b/parse-options.c index b8bde2b..bbc3ca4 100644 --- a/parse-options.c +++ b/parse-options.c @@ -22,12 +22,6 @@ static inline const char *get_arg(struct optparse_t *p) return *++p->argv; } -static inline const char *skip_prefix(const char *str, const char *prefix) -{ - size_t len = strlen(prefix); - return strncmp(str, prefix, len) ? NULL : str + len; -} - static int opterror(const struct option *opt, const char *reason, int flags) { if (flags & OPT_SHORT) -- 1.5.6 ^ permalink raw reply related [flat|nested] 82+ messages in thread
* [PATCH 04/15] Add new test to ensure git-merge handles pull.twohead and pull.octopus 2008-06-27 16:21 ` [PATCH 03/15] Move parse-options's skip_prefix() to git-compat-util.h Miklos Vajna @ 2008-06-27 16:21 ` Miklos Vajna 2008-06-27 16:21 ` [PATCH 05/15] Move read_cache_unmerged() to read-cache.c Miklos Vajna ` (2 more replies) 0 siblings, 3 replies; 82+ messages in thread From: Miklos Vajna @ 2008-06-27 16:21 UTC (permalink / raw To: Junio C Hamano; +Cc: git, Johannes Schindelin, Olivier Marin Test if the given strategies are used and test the case when multiple strategies are configured using a space separated list. Also test if the best strategy is picked if none is specified. This is done by adding a simple test case where recursive detects a rename, but resolve does not, and verify that finally merge will pick up the previous. Signed-off-by: Miklos Vajna <vmiklos@frugalware.org> --- t/t7601-merge-pull-config.sh | 129 ++++++++++++++++++++++++++++++++++++++++++ 1 files changed, 129 insertions(+), 0 deletions(-) create mode 100755 t/t7601-merge-pull-config.sh diff --git a/t/t7601-merge-pull-config.sh b/t/t7601-merge-pull-config.sh new file mode 100755 index 0000000..be622f3 --- /dev/null +++ b/t/t7601-merge-pull-config.sh @@ -0,0 +1,129 @@ +#!/bin/sh + +test_description='git-merge + +Testing pull.* configuration parsing.' + +. ./test-lib.sh + +test_expect_success 'setup' ' + echo c0 >c0.c && + git add c0.c && + git commit -m c0 && + git tag c0 && + echo c1 >c1.c && + git add c1.c && + git commit -m c1 && + git tag c1 && + git reset --hard c0 && + echo c2 >c2.c && + git add c2.c && + git commit -m c2 && + git tag c2 + git reset --hard c0 && + echo c3 >c3.c && + git add c3.c && + git commit -m c3 && + git tag c3 +' + +test_expect_success 'merge c1 with c2' ' + git reset --hard c1 && + test -f c0.c && + test -f c1.c && + test ! -f c2.c && + test ! -f c3.c && + git merge c2 && + test -f c1.c && + test -f c2.c +' + +test_expect_success 'merge c1 with c2 (ours in pull.twohead)' ' + git reset --hard c1 && + git config pull.twohead ours && + git merge c2 && + test -f c1.c && + ! test -f c2.c +' + +test_expect_success 'merge c1 with c2 and c3 (recursive in pull.octopus)' ' + git reset --hard c1 && + git config pull.octopus "recursive" && + test_must_fail git merge c2 c3 && + test "$(git rev-parse c1)" = "$(git rev-parse HEAD)" +' + +test_expect_success 'merge c1 with c2 and c3 (recursive and octopus in pull.octopus)' ' + git reset --hard c1 && + git config pull.octopus "recursive octopus" && + git merge c2 c3 && + test "$(git rev-parse c1)" != "$(git rev-parse HEAD)" && + test "$(git rev-parse c1)" = "$(git rev-parse HEAD^1)" && + test "$(git rev-parse c2)" = "$(git rev-parse HEAD^2)" && + test "$(git rev-parse c3)" = "$(git rev-parse HEAD^3)" + test "$(git rev-parse c3)" = "$(git rev-parse HEAD^3)" && + git diff --exit-code && + test -f c0.c && + test -f c1.c && + test -f c2.c && + test -f c3.c +' + +conflict_count() +{ + eval $1=`{ + git diff-files --name-only + git ls-files --unmerged + } | wc -l` +} + +# c4 - c5 +# \ c6 +# +# There are two conflicts here: +# +# 1) Because foo.c is renamed to bar.c, recursive will handle this, +# resolve won't. +# +# 2) One in conflict.c and that will always fail. + +test_expect_success 'setup conflicted merge' ' + git reset --hard c0 && + echo A >conflict.c && + git add conflict.c && + echo contents >foo.c && + git add foo.c && + git commit -m c4 && + git tag c4 && + echo B >conflict.c && + git add conflict.c && + git mv foo.c bar.c && + git commit -m c5 && + git tag c5 && + git reset --hard c4 && + echo C >conflict.c && + git add conflict.c && + echo secondline >> foo.c && + git add foo.c && + git commit -m c6 && + git tag c6 +' + +# First do the merge with resolve and recursive then verify that +# recusive is choosen. + +test_expect_success 'merge picks up the best result' ' + git config pull.twohead "recursive resolve" && + git reset --hard c5 && + git merge -s resolve c6 + conflict_count resolve_count && + git reset --hard c5 && + git merge -s recursive c6 + conflict_count recursive_count && + git reset --hard c5 && + git merge c6 + conflict_count auto_count && + test "$auto_count" = "$recursive_count" +' + +test_done -- 1.5.6 ^ permalink raw reply related [flat|nested] 82+ messages in thread
* [PATCH 05/15] Move read_cache_unmerged() to read-cache.c 2008-06-27 16:21 ` [PATCH 04/15] Add new test to ensure git-merge handles pull.twohead and pull.octopus Miklos Vajna @ 2008-06-27 16:21 ` Miklos Vajna 2008-06-27 16:21 ` [PATCH 06/15] git-fmt-merge-msg: make it usable from other builtins Miklos Vajna 2008-06-29 13:30 ` [PATCH 04/15] Add new test to ensure git-merge handles pull.twohead and pull.octopus Olivier Marin 2008-07-04 16:34 ` [PATCH 04/15] " Mike Ralphson 2 siblings, 1 reply; 82+ messages in thread From: Miklos Vajna @ 2008-06-27 16:21 UTC (permalink / raw To: Junio C Hamano; +Cc: git, Johannes Schindelin, Olivier Marin, Junio C Hamano builtin-read-tree has a read_cache_unmerged() which is useful for other builtins, for example builtin-merge uses it as well. Move it to read-cache.c to avoid code duplication. Signed-off-by: Miklos Vajna <vmiklos@frugalware.org> Signed-off-by: Junio C Hamano <gitster@pobox.com> --- builtin-read-tree.c | 24 ------------------------ cache.h | 2 ++ read-cache.c | 31 +++++++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 24 deletions(-) diff --git a/builtin-read-tree.c b/builtin-read-tree.c index 5a09e17..72a6de3 100644 --- a/builtin-read-tree.c +++ b/builtin-read-tree.c @@ -29,30 +29,6 @@ static int list_tree(unsigned char *sha1) return 0; } -static int read_cache_unmerged(void) -{ - int i; - struct cache_entry **dst; - struct cache_entry *last = NULL; - - read_cache(); - dst = active_cache; - for (i = 0; i < active_nr; i++) { - struct cache_entry *ce = active_cache[i]; - if (ce_stage(ce)) { - remove_name_hash(ce); - if (last && !strcmp(ce->name, last->name)) - continue; - cache_tree_invalidate_path(active_cache_tree, ce->name); - last = ce; - continue; - } - *dst++ = ce; - } - active_nr = dst - active_cache; - return !!last; -} - static void prime_cache_tree_rec(struct cache_tree *it, struct tree *tree) { struct tree_desc desc; diff --git a/cache.h b/cache.h index d5463d8..7fb6c70 100644 --- a/cache.h +++ b/cache.h @@ -254,6 +254,7 @@ static inline void remove_name_hash(struct cache_entry *ce) #define read_cache() read_index(&the_index) #define read_cache_from(path) read_index_from(&the_index, (path)) +#define read_cache_unmerged() read_index_unmerged(&the_index) #define write_cache(newfd, cache, entries) write_index(&the_index, (newfd)) #define discard_cache() discard_index(&the_index) #define unmerged_cache() unmerged_index(&the_index) @@ -356,6 +357,7 @@ extern int init_db(const char *template_dir, unsigned int flags); /* Initialize and use the cache information */ extern int read_index(struct index_state *); extern int read_index_from(struct index_state *, const char *path); +extern int read_index_unmerged(struct index_state *); extern int write_index(const struct index_state *, int newfd); extern int discard_index(struct index_state *); extern int unmerged_index(const struct index_state *); diff --git a/read-cache.c b/read-cache.c index f83de8c..d801f9d 100644 --- a/read-cache.c +++ b/read-cache.c @@ -1410,3 +1410,34 @@ int write_index(const struct index_state *istate, int newfd) } return ce_flush(&c, newfd); } + +/* + * Read the index file that is potentially unmerged into given + * index_state, dropping any unmerged entries. Returns true is + * the index is unmerged. Callers who want to refuse to work + * from an unmerged state can call this and check its return value, + * instead of calling read_cache(). + */ +int read_index_unmerged(struct index_state *istate) +{ + int i; + struct cache_entry **dst; + struct cache_entry *last = NULL; + + read_index(istate); + dst = istate->cache; + for (i = 0; i < istate->cache_nr; i++) { + struct cache_entry *ce = istate->cache[i]; + if (ce_stage(ce)) { + remove_name_hash(ce); + if (last && !strcmp(ce->name, last->name)) + continue; + cache_tree_invalidate_path(istate->cache_tree, ce->name); + last = ce; + continue; + } + *dst++ = ce; + } + istate->cache_nr = dst - istate->cache; + return !!last; +} -- 1.5.6 ^ permalink raw reply related [flat|nested] 82+ messages in thread
* [PATCH 06/15] git-fmt-merge-msg: make it usable from other builtins 2008-06-27 16:21 ` [PATCH 05/15] Move read_cache_unmerged() to read-cache.c Miklos Vajna @ 2008-06-27 16:21 ` Miklos Vajna 2008-06-27 16:22 ` [PATCH 07/15] Introduce get_octopus_merge_bases() in commit.c Miklos Vajna 0 siblings, 1 reply; 82+ messages in thread From: Miklos Vajna @ 2008-06-27 16:21 UTC (permalink / raw To: Junio C Hamano; +Cc: git, Johannes Schindelin, Olivier Marin Move all functionality (except config and option parsing) from cmd_fmt_merge_msg() to fmt_merge_msg(), so that other builtins can use it without a child process. All functions have been changed to use strbufs, and now only cmd_fmt_merge_msg() reads directly from a file / writes anything to stdout. Signed-off-by: Miklos Vajna <vmiklos@frugalware.org> --- builtin-fmt-merge-msg.c | 155 +++++++++++++++++++++++++++-------------------- builtin.h | 3 + 2 files changed, 92 insertions(+), 66 deletions(-) diff --git a/builtin-fmt-merge-msg.c b/builtin-fmt-merge-msg.c index b892621..dbd7d2d 100644 --- a/builtin-fmt-merge-msg.c +++ b/builtin-fmt-merge-msg.c @@ -159,23 +159,24 @@ static int handle_line(char *line) } static void print_joined(const char *singular, const char *plural, - struct list *list) + struct list *list, struct strbuf *out) { if (list->nr == 0) return; if (list->nr == 1) { - printf("%s%s", singular, list->list[0]); + strbuf_addf(out, "%s%s", singular, list->list[0]); } else { int i; - printf("%s", plural); + strbuf_addstr(out, plural); for (i = 0; i < list->nr - 1; i++) - printf("%s%s", i > 0 ? ", " : "", list->list[i]); - printf(" and %s", list->list[list->nr - 1]); + strbuf_addf(out, "%s%s", i > 0 ? ", " : "", list->list[i]); + strbuf_addf(out, " and %s", list->list[list->nr - 1]); } } static void shortlog(const char *name, unsigned char *sha1, - struct commit *head, struct rev_info *rev, int limit) + struct commit *head, struct rev_info *rev, int limit, + struct strbuf *out) { int i, count = 0; struct commit *commit; @@ -232,15 +233,15 @@ static void shortlog(const char *name, unsigned char *sha1, } if (count > limit) - printf("\n* %s: (%d commits)\n", name, count); + strbuf_addf(out, "\n* %s: (%d commits)\n", name, count); else - printf("\n* %s:\n", name); + strbuf_addf(out, "\n* %s:\n", name); for (i = 0; i < subjects.nr; i++) if (i >= limit) - printf(" ...\n"); + strbuf_addf(out, " ...\n"); else - printf(" %s\n", subjects.list[i]); + strbuf_addf(out, " %s\n", subjects.list[i]); clear_commit_marks((struct commit *)branch, flags); clear_commit_marks(head, flags); @@ -251,43 +252,13 @@ static void shortlog(const char *name, unsigned char *sha1, free_list(&subjects); } -int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix) -{ - int limit = 20, i = 0; +int fmt_merge_msg(int merge_summary, struct strbuf *in, struct strbuf *out) { + int limit = 20, i = 0, pos = 0; char line[1024]; - FILE *in = stdin; - const char *sep = ""; + char *p = line, *sep = ""; unsigned char head_sha1[20]; const char *current_branch; - git_config(fmt_merge_msg_config, NULL); - - while (argc > 1) { - if (!strcmp(argv[1], "--log") || !strcmp(argv[1], "--summary")) - merge_summary = 1; - else if (!strcmp(argv[1], "--no-log") - || !strcmp(argv[1], "--no-summary")) - merge_summary = 0; - else if (!strcmp(argv[1], "-F") || !strcmp(argv[1], "--file")) { - if (argc < 3) - die ("Which file?"); - if (!strcmp(argv[2], "-")) - in = stdin; - else { - fclose(in); - in = fopen(argv[2], "r"); - if (!in) - die("cannot open %s", argv[2]); - } - argc--; argv++; - } else - break; - argc--; argv++; - } - - if (argc > 1) - usage(fmt_merge_msg_usage); - /* get current branch */ current_branch = resolve_ref("HEAD", head_sha1, 1, NULL); if (!current_branch) @@ -295,75 +266,127 @@ int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix) if (!prefixcmp(current_branch, "refs/heads/")) current_branch += 11; - while (fgets(line, sizeof(line), in)) { + /* get a line */ + while (pos < in->len) { + int len; + char *newline; + + p = in->buf + pos; + newline = strchr(p, '\n'); + len = newline ? newline - p : strlen(p); + pos += len + !!newline; i++; - if (line[0] == 0) - continue; - if (handle_line(line)) - die ("Error in line %d: %s", i, line); + p[len] = 0; + if (handle_line(p)) + die ("Error in line %d: %.*s", i, len, p); } - printf("Merge "); + strbuf_addstr(out, "Merge "); for (i = 0; i < srcs.nr; i++) { struct src_data *src_data = srcs.payload[i]; const char *subsep = ""; - printf(sep); + strbuf_addstr(out, sep); sep = "; "; if (src_data->head_status == 1) { - printf(srcs.list[i]); + strbuf_addstr(out, srcs.list[i]); continue; } if (src_data->head_status == 3) { subsep = ", "; - printf("HEAD"); + strbuf_addstr(out, "HEAD"); } if (src_data->branch.nr) { - printf(subsep); + strbuf_addstr(out, subsep); subsep = ", "; - print_joined("branch ", "branches ", &src_data->branch); + print_joined("branch ", "branches ", &src_data->branch, + out); } if (src_data->r_branch.nr) { - printf(subsep); + strbuf_addstr(out, subsep); subsep = ", "; print_joined("remote branch ", "remote branches ", - &src_data->r_branch); + &src_data->r_branch, out); } if (src_data->tag.nr) { - printf(subsep); + strbuf_addstr(out, subsep); subsep = ", "; - print_joined("tag ", "tags ", &src_data->tag); + print_joined("tag ", "tags ", &src_data->tag, out); } if (src_data->generic.nr) { - printf(subsep); - print_joined("commit ", "commits ", &src_data->generic); + strbuf_addstr(out, subsep); + print_joined("commit ", "commits ", &src_data->generic, + out); } if (strcmp(".", srcs.list[i])) - printf(" of %s", srcs.list[i]); + strbuf_addf(out, " of %s", srcs.list[i]); } if (!strcmp("master", current_branch)) - putchar('\n'); + strbuf_addch(out, '\n'); else - printf(" into %s\n", current_branch); + strbuf_addf(out, " into %s\n", current_branch); if (merge_summary) { struct commit *head; struct rev_info rev; head = lookup_commit(head_sha1); - init_revisions(&rev, prefix); + init_revisions(&rev, NULL); rev.commit_format = CMIT_FMT_ONELINE; rev.ignore_merges = 1; rev.limited = 1; for (i = 0; i < origins.nr; i++) shortlog(origins.list[i], origins.payload[i], - head, &rev, limit); + head, &rev, limit, out); } + return 0; +} + +int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix) +{ + FILE *in = stdin; + struct strbuf input, output; + int ret; + + git_config(fmt_merge_msg_config, NULL); + + while (argc > 1) { + if (!strcmp(argv[1], "--log") || !strcmp(argv[1], "--summary")) + merge_summary = 1; + else if (!strcmp(argv[1], "--no-log") + || !strcmp(argv[1], "--no-summary")) + merge_summary = 0; + else if (!strcmp(argv[1], "-F") || !strcmp(argv[1], "--file")) { + if (argc < 3) + die ("Which file?"); + if (!strcmp(argv[2], "-")) + in = stdin; + else { + fclose(in); + in = fopen(argv[2], "r"); + if (!in) + die("cannot open %s", argv[2]); + } + argc--; argv++; + } else + break; + argc--; argv++; + } + + if (argc > 1) + usage(fmt_merge_msg_usage); - /* No cleanup yet; is standalone anyway */ + strbuf_init(&input, 0); + if (strbuf_read(&input, fileno(in), 0) < 0) + die("could not read input file %s", strerror(errno)); + strbuf_init(&output, 0); + ret = fmt_merge_msg(merge_summary, &input, &output); + if (ret) + return ret; + printf("%s", output.buf); return 0; } diff --git a/builtin.h b/builtin.h index b460b2d..2b01fea 100644 --- a/builtin.h +++ b/builtin.h @@ -2,6 +2,7 @@ #define BUILTIN_H #include "git-compat-util.h" +#include "strbuf.h" extern const char git_version_string[]; extern const char git_usage_string[]; @@ -11,6 +12,8 @@ extern void list_common_cmds_help(void); extern void help_unknown_cmd(const char *cmd); extern void prune_packed_objects(int); extern int read_line_with_nul(char *buf, int size, FILE *file); +extern int fmt_merge_msg(int merge_summary, struct strbuf *in, + struct strbuf *out); extern int cmd_add(int argc, const char **argv, const char *prefix); extern int cmd_annotate(int argc, const char **argv, const char *prefix); -- 1.5.6 ^ permalink raw reply related [flat|nested] 82+ messages in thread
* [PATCH 07/15] Introduce get_octopus_merge_bases() in commit.c 2008-06-27 16:21 ` [PATCH 06/15] git-fmt-merge-msg: make it usable from other builtins Miklos Vajna @ 2008-06-27 16:22 ` Miklos Vajna 2008-06-27 16:22 ` [PATCH 08/15] Add new test to ensure git-merge handles more than 25 refs Miklos Vajna 0 siblings, 1 reply; 82+ messages in thread From: Miklos Vajna @ 2008-06-27 16:22 UTC (permalink / raw To: Junio C Hamano; +Cc: git, Johannes Schindelin, Olivier Marin, Junio C Hamano This is like get_merge_bases() but it works for multiple heads, like show-branch --merge-base. Signed-off-by: Miklos Vajna <vmiklos@frugalware.org> Signed-off-by: Junio C Hamano <gitster@pobox.com> --- commit.c | 27 +++++++++++++++++++++++++++ commit.h | 1 + 2 files changed, 28 insertions(+), 0 deletions(-) diff --git a/commit.c b/commit.c index bbf9c75..6052ca3 100644 --- a/commit.c +++ b/commit.c @@ -600,6 +600,33 @@ static struct commit_list *merge_bases(struct commit *one, struct commit *two) return result; } +struct commit_list *get_octopus_merge_bases(struct commit_list *in) +{ + struct commit_list *i, *j, *k, *ret = NULL; + struct commit_list **pptr = &ret; + + for (i = in; i; i = i->next) { + if (!ret) + pptr = &commit_list_insert(i->item, pptr)->next; + else { + struct commit_list *new = NULL, *end = NULL; + + for (j = ret; j; j = j->next) { + struct commit_list *bases; + bases = get_merge_bases(i->item, j->item, 1); + if (!new) + new = bases; + else + end->next = bases; + for (k = bases; k; k = k->next) + end = k; + } + ret = new; + } + } + return ret; +} + struct commit_list *get_merge_bases(struct commit *one, struct commit *two, int cleanup) { diff --git a/commit.h b/commit.h index 7f8c5ee..dcec7fb 100644 --- a/commit.h +++ b/commit.h @@ -121,6 +121,7 @@ int read_graft_file(const char *graft_file); struct commit_graft *lookup_commit_graft(const unsigned char *sha1); extern struct commit_list *get_merge_bases(struct commit *rev1, struct commit *rev2, int cleanup); +extern struct commit_list *get_octopus_merge_bases(struct commit_list *in); extern int register_shallow(const unsigned char *sha1); extern int unregister_shallow(const unsigned char *sha1); -- 1.5.6 ^ permalink raw reply related [flat|nested] 82+ messages in thread
* [PATCH 08/15] Add new test to ensure git-merge handles more than 25 refs. 2008-06-27 16:22 ` [PATCH 07/15] Introduce get_octopus_merge_bases() in commit.c Miklos Vajna @ 2008-06-27 16:22 ` Miklos Vajna 2008-06-27 16:22 ` [PATCH 09/15] Introduce get_merge_bases_many() Miklos Vajna 2008-06-27 17:06 ` [PATCH 08/15] Add new test to ensure git-merge handles more than 25 refs Miklos Vajna 0 siblings, 2 replies; 82+ messages in thread From: Miklos Vajna @ 2008-06-27 16:22 UTC (permalink / raw To: Junio C Hamano; +Cc: git, Johannes Schindelin, Olivier Marin, Junio C Hamano The old shell version handled only 25 refs but we no longer have this limitation. Add a test to make sure this limitation will not be introduced again in the future. Signed-off-by: Miklos Vajna <vmiklos@frugalware.org> Signed-off-by: Junio C Hamano <gitster@pobox.com> --- t/t7602-merge-octopus-many.sh | 52 +++++++++++++++++++++++++++++++++++++++++ 1 files changed, 52 insertions(+), 0 deletions(-) create mode 100755 t/t7602-merge-octopus-many.sh diff --git a/t/t7602-merge-octopus-many.sh b/t/t7602-merge-octopus-many.sh new file mode 100755 index 0000000..f3a4bb2 --- /dev/null +++ b/t/t7602-merge-octopus-many.sh @@ -0,0 +1,52 @@ +#!/bin/sh + +test_description='git-merge + +Testing octopus merge with more than 25 refs.' + +. ./test-lib.sh + +test_expect_success 'setup' ' + echo c0 > c0.c && + git add c0.c && + git commit -m c0 && + git tag c0 && + i=1 && + while test $i -le 30 + do + git reset --hard c0 && + echo c$i > c$i.c && + git add c$i.c && + git commit -m c$i && + git tag c$i && + i=`expr $i + 1` || return 1 + done +' + +test_expect_failure 'merge c1 with c2, c3, c4, ... c29' ' + git reset --hard c1 && + i=2 && + refs="" && + while test $i -le 30 + do + refs="$refs c$i" + i=`expr $i + 1` + done + git merge $refs && + test "$(git rev-parse c1)" != "$(git rev-parse HEAD)" && + i=1 && + while test $i -le 30 + do + test "$(git rev-parse c$i)" = "$(git rev-parse HEAD^$i)" && + i=`expr $i + 1` || return 1 + done && + git diff --exit-code && + i=1 && + while test $i -le 30 + do + test -f c$i.c && + i=`expr $i + 1` || return 1 + done +' + +test_done -- 1.5.6 ^ permalink raw reply related [flat|nested] 82+ messages in thread
* [PATCH 09/15] Introduce get_merge_bases_many() 2008-06-27 16:22 ` [PATCH 08/15] Add new test to ensure git-merge handles more than 25 refs Miklos Vajna @ 2008-06-27 16:22 ` Miklos Vajna 2008-06-27 16:22 ` [PATCH 10/15] Introduce reduce_heads() Miklos Vajna 2008-06-27 17:06 ` [PATCH 08/15] Add new test to ensure git-merge handles more than 25 refs Miklos Vajna 1 sibling, 1 reply; 82+ messages in thread From: Miklos Vajna @ 2008-06-27 16:22 UTC (permalink / raw To: Junio C Hamano; +Cc: git, Johannes Schindelin, Olivier Marin, Junio C Hamano From: Junio C Hamano <gitster@pobox.com> This introduces a new function get_merge_bases_many() which is a natural extension of two commit merge base computation. It is given one commit (one) and a set of other commits (twos), and computes the merge base of one and a merge across other commits. This is mostly useful to figure out the common ancestor when iterating over heads during an octopus merge. When making an octopus between commits A, B, C and D, we first merge tree of A and B, and then try to merge C with it. If we were making pairwise merge, we would be recording the tree resulting from the merge between A and B as a commit, say M, and then the next round we will be computing the merge base between M and C. o---C...* / . o---B...M / . o---o---A But during an octopus merge, we actually do not create a commit M. In order to figure out that the common ancestor to use for this merge, instead of computing the merge base between C and M, we can call merge_bases_many() with one set to C and twos containing A and B. Signed-off-by: Junio C Hamano <gitster@pobox.com> --- commit.c | 56 ++++++++++++++++++++++++++++++++++++++------------------ 1 files changed, 38 insertions(+), 18 deletions(-) diff --git a/commit.c b/commit.c index 6052ca3..cafed26 100644 --- a/commit.c +++ b/commit.c @@ -533,26 +533,34 @@ static struct commit *interesting(struct commit_list *list) return NULL; } -static struct commit_list *merge_bases(struct commit *one, struct commit *two) +static struct commit_list *merge_bases_many(struct commit *one, int n, struct commit **twos) { struct commit_list *list = NULL; struct commit_list *result = NULL; + int i; - if (one == two) - /* We do not mark this even with RESULT so we do not - * have to clean it up. - */ - return commit_list_insert(one, &result); + for (i = 0; i < n; i++) { + if (one == twos[i]) + /* + * We do not mark this even with RESULT so we do not + * have to clean it up. + */ + return commit_list_insert(one, &result); + } if (parse_commit(one)) return NULL; - if (parse_commit(two)) - return NULL; + for (i = 0; i < n; i++) { + if (parse_commit(twos[i])) + return NULL; + } one->object.flags |= PARENT1; - two->object.flags |= PARENT2; insert_by_date(one, &list); - insert_by_date(two, &list); + for (i = 0; i < n; i++) { + twos[i]->object.flags |= PARENT2; + insert_by_date(twos[i], &list); + } while (interesting(list)) { struct commit *commit; @@ -627,21 +635,26 @@ struct commit_list *get_octopus_merge_bases(struct commit_list *in) return ret; } -struct commit_list *get_merge_bases(struct commit *one, - struct commit *two, int cleanup) +struct commit_list *get_merge_bases_many(struct commit *one, + int n, + struct commit **twos, + int cleanup) { struct commit_list *list; struct commit **rslt; struct commit_list *result; int cnt, i, j; - result = merge_bases(one, two); - if (one == two) - return result; + result = merge_bases_many(one, n, twos); + for (i = 0; i < n; i++) { + if (one == twos[i]) + return result; + } if (!result || !result->next) { if (cleanup) { clear_commit_marks(one, all_flags); - clear_commit_marks(two, all_flags); + for (i = 0; i < n; i++) + clear_commit_marks(twos[i], all_flags); } return result; } @@ -659,12 +672,13 @@ struct commit_list *get_merge_bases(struct commit *one, free_commit_list(result); clear_commit_marks(one, all_flags); - clear_commit_marks(two, all_flags); + for (i = 0; i < n; i++) + clear_commit_marks(twos[i], all_flags); for (i = 0; i < cnt - 1; i++) { for (j = i+1; j < cnt; j++) { if (!rslt[i] || !rslt[j]) continue; - result = merge_bases(rslt[i], rslt[j]); + result = merge_bases_many(rslt[i], 1, &rslt[j]); clear_commit_marks(rslt[i], all_flags); clear_commit_marks(rslt[j], all_flags); for (list = result; list; list = list->next) { @@ -686,6 +700,12 @@ struct commit_list *get_merge_bases(struct commit *one, return result; } +struct commit_list *get_merge_bases(struct commit *one, struct commit *two, + int cleanup) +{ + return get_merge_bases_many(one, 1, &two, cleanup); +} + int in_merge_bases(struct commit *commit, struct commit **reference, int num) { struct commit_list *bases, *b; -- 1.5.6 ^ permalink raw reply related [flat|nested] 82+ messages in thread
* [PATCH 10/15] Introduce reduce_heads() 2008-06-27 16:22 ` [PATCH 09/15] Introduce get_merge_bases_many() Miklos Vajna @ 2008-06-27 16:22 ` Miklos Vajna 2008-06-27 16:22 ` [PATCH 11/15] Add strbuf_vaddf(), use it in strbuf_addf(), and add strbuf_initf() Miklos Vajna 0 siblings, 1 reply; 82+ messages in thread From: Miklos Vajna @ 2008-06-27 16:22 UTC (permalink / raw To: Junio C Hamano; +Cc: git, Johannes Schindelin, Olivier Marin, Junio C Hamano From: Junio C Hamano <gitster@pobox.com> The new function reduce_heads() is given a list of commits, and removes ones that can be reached from other commits in the list. It is useful for reducing the commits randomly thrown at the git-merge command and remove redundant commits that the user shouldn't have given to it. The implementation uses the get_merge_bases_many() introduced in the previous commit. If the merge base between one commit taken from the list and the remaining commits is the commit itself, that means the commit is reachable from some of the other commits. Signed-off-by: Junio C Hamano <gitster@pobox.com> --- commit.c | 45 +++++++++++++++++++++++++++++++++++++++++++++ commit.h | 2 ++ 2 files changed, 47 insertions(+), 0 deletions(-) diff --git a/commit.c b/commit.c index cafed26..d20b14e 100644 --- a/commit.c +++ b/commit.c @@ -725,3 +725,48 @@ int in_merge_bases(struct commit *commit, struct commit **reference, int num) free_commit_list(bases); return ret; } + +struct commit_list *reduce_heads(struct commit_list *heads) +{ + struct commit_list *p; + struct commit_list *result = NULL, **tail = &result; + struct commit **other; + size_t num_head, num_other; + + if (!heads) + return NULL; + + /* Avoid unnecessary reallocations */ + for (p = heads, num_head = 0; p; p = p->next) + num_head++; + other = xcalloc(sizeof(*other), num_head); + + /* For each commit, see if it can be reached by others */ + for (p = heads; p; p = p->next) { + struct commit_list *q, *base; + + num_other = 0; + for (q = heads; q; q = q->next) { + if (p == q) + continue; + other[num_other++] = q->item; + } + if (num_other) { + base = get_merge_bases_many(p->item, num_other, other, 1); + } else + base = NULL; + /* + * If p->item does not have anything common with other + * commits, there won't be any merge base. If it is + * reachable from some of the others, p->item will be + * the merge base. If its history is connected with + * others, but p->item is not reachable by others, we + * will get something other than p->item back. + */ + if (!base || (base->item != p->item)) + tail = &(commit_list_insert(p->item, tail)->next); + free_commit_list(base); + } + free(other); + return result; +} diff --git a/commit.h b/commit.h index dcec7fb..2acfc79 100644 --- a/commit.h +++ b/commit.h @@ -140,4 +140,6 @@ static inline int single_parent(struct commit *commit) return commit->parents && !commit->parents->next; } +struct commit_list *reduce_heads(struct commit_list *heads); + #endif /* COMMIT_H */ -- 1.5.6 ^ permalink raw reply related [flat|nested] 82+ messages in thread
* [PATCH 11/15] Add strbuf_vaddf(), use it in strbuf_addf(), and add strbuf_initf() 2008-06-27 16:22 ` [PATCH 10/15] Introduce reduce_heads() Miklos Vajna @ 2008-06-27 16:22 ` Miklos Vajna 2008-06-27 16:22 ` [PATCH 12/15] strbuf_vaddf(): support %*s, too Miklos Vajna 2008-06-28 2:00 ` [PATCH 11/15] Add strbuf_vaddf(), use it in strbuf_addf(), and add strbuf_initf() Junio C Hamano 0 siblings, 2 replies; 82+ messages in thread From: Miklos Vajna @ 2008-06-27 16:22 UTC (permalink / raw To: Junio C Hamano Cc: git, Johannes Schindelin, Olivier Marin, Johannes Schindelin From: Johannes Schindelin <johannes.schindelin@gmx.de> The most common use of addf() was to init a strbuf and addf() right away. Since it is so common, it makes sense to have a function strbuf_initf() to wrap both calls into one. To do that, we implement a (really minimal) vaddf() lookalike to vsprintf(). At the moment, it only handles %u, %i, %d, %l, %o, %x and %X with size indicators '<number>', ' <number>' and '0<number>', as well as %c and %s, the latter with size indicators '.*' in addition to the same size indicators as for numbers. Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de> --- .gitignore | 1 + Makefile | 4 +- strbuf.c | 134 +++++++++++++++++++++++++++++++++++++++++++++++------ strbuf.h | 3 + t/t0000-basic.sh | 8 +++ test-strbuf.c | 17 +++++++ 6 files changed, 150 insertions(+), 17 deletions(-) create mode 100644 test-strbuf.c diff --git a/.gitignore b/.gitignore index 4ff2fec..9281395 100644 --- a/.gitignore +++ b/.gitignore @@ -151,6 +151,7 @@ test-genrandom test-match-trees test-parse-options test-sha1 +test-strbuf common-cmds.h *.tar.gz *.dsc diff --git a/Makefile b/Makefile index 314339d..1b43171 100644 --- a/Makefile +++ b/Makefile @@ -1230,7 +1230,7 @@ endif ### Testing rules -TEST_PROGRAMS = test-chmtime$X test-genrandom$X test-date$X test-delta$X test-sha1$X test-match-trees$X test-absolute-path$X test-parse-options$X +TEST_PROGRAMS = test-chmtime$X test-genrandom$X test-date$X test-delta$X test-sha1$X test-match-trees$X test-absolute-path$X test-parse-options$X test-strbuf$X all:: $(TEST_PROGRAMS) @@ -1249,6 +1249,8 @@ test-delta$X: diff-delta.o patch-delta.o test-parse-options$X: parse-options.o +test-strbuf$X: strbuf.o + .PRECIOUS: $(patsubst test-%$X,test-%.o,$(TEST_PROGRAMS)) test-%$X: test-%.o $(GITLIBS) diff --git a/strbuf.c b/strbuf.c index 4aed752..72b3585 100644 --- a/strbuf.c +++ b/strbuf.c @@ -122,28 +122,130 @@ void strbuf_adddup(struct strbuf *sb, size_t pos, size_t len) strbuf_setlen(sb, sb->len + len); } +static int number_length(unsigned long number, long base) +{ + int length = 1; + while (number >= base) { + number /= base; + length++; + } + return length; +} + +/* + * Only supports %u, %i, %d, %l, %o, %x and %X with size indicators + * '<number>', '0<number>', and ' <number>', + * as well as %c, + * and %s with size indicators '<number>', ' <number>' and '.*'. + */ +void strbuf_vaddf(struct strbuf *sb, const char *fmt, va_list ap) +{ + while (*fmt) { + char fill = '\0'; + int size = -1, max_size = -1; + char *p = (char *)fmt; + + if (*p != '%' || *(++p) == '%') { + strbuf_addch(sb, *p++); + fmt = p; + continue; + } + if (*p == ' ' || *p == '0') + fill = *p++; + if (isdigit(*p)) + size = (int)strtol(p, &p, 10); + else if (!prefixcmp(p, ".*")) { + max_size = va_arg(ap, int); + p += 2; + } + switch (*p) { + case 's': { + const char *s = va_arg(ap, const char *); + if (fill) { + int len = size - strlen(s); + while (len-- > 0) + strbuf_addch(sb, fill); + } + while (*s && max_size--) + strbuf_addch(sb, *s++); + break; + } + case 'c': + strbuf_addch(sb, va_arg(ap, int)); + break; + case 'u': + case 'i': + case 'l': + case 'd': + case 'o': + case 'x': + case 'X': { + int base = *p == 'x' || *p == 'X' ? 16 : + *p == 'o' ? 8 : 10; + int negative = 0, len; + unsigned long number, power; + + if (*p == 'u') + number = va_arg(ap, unsigned int); + else { + long signed_number; + if (*p == 'l') + signed_number = va_arg(ap, long); + else + signed_number = va_arg(ap, int); + if (signed_number < 0) { + negative = 1; + number = -signed_number; + } else + number = signed_number; + } + + /* pad */ + len = number_length(number, base); + while (size-- > len + negative) + strbuf_addch(sb, fill ? fill : ' '); + if (negative) + strbuf_addch(sb, '-'); + + /* output number */ + power = 1; + while (len-- > 1) + power *= base; + while (power) { + int digit = number / power; + strbuf_addch(sb, digit < 10 ? '0' + digit + : *p + 'A' - 'X' + digit - 10); + number -= digit * power; + power /= base; + } + + break; + } + default: + /* unknown / invalid format: copy verbatim */ + strbuf_insert(sb, sb->len, fmt, p - fmt + 1); + } + fmt = p + (*p != '\0'); + } +} + void strbuf_addf(struct strbuf *sb, const char *fmt, ...) { - int len; va_list ap; - if (!strbuf_avail(sb)) - strbuf_grow(sb, 64); va_start(ap, fmt); - len = vsnprintf(sb->buf + sb->len, sb->alloc - sb->len, fmt, ap); + strbuf_vaddf(sb, fmt, ap); + va_end(ap); +} + +void strbuf_initf(struct strbuf *sb, const char *fmt, ...) +{ + va_list ap; + + strbuf_init(sb, strlen(fmt) + 64); + va_start(ap, fmt); + strbuf_vaddf(sb, fmt, ap); va_end(ap); - if (len < 0) - die("your vsnprintf is broken"); - if (len > strbuf_avail(sb)) { - strbuf_grow(sb, len); - va_start(ap, fmt); - len = vsnprintf(sb->buf + sb->len, sb->alloc - sb->len, fmt, ap); - va_end(ap); - if (len > strbuf_avail(sb)) { - die("this should not happen, your snprintf is broken"); - } - } - strbuf_setlen(sb, sb->len + len); } void strbuf_expand(struct strbuf *sb, const char *format, expand_fn_t fn, diff --git a/strbuf.h b/strbuf.h index faec229..d7c7aaf 100644 --- a/strbuf.h +++ b/strbuf.h @@ -106,8 +106,11 @@ extern void strbuf_adddup(struct strbuf *sb, size_t pos, size_t len); typedef size_t (*expand_fn_t) (struct strbuf *sb, const char *placeholder, void *context); extern void strbuf_expand(struct strbuf *sb, const char *format, expand_fn_t fn, void *context); +extern void strbuf_vaddf(struct strbuf *sb, const char *fmt, va_list args); __attribute__((format(printf,2,3))) extern void strbuf_addf(struct strbuf *sb, const char *fmt, ...); +__attribute__((format(printf,2,3))) +extern void strbuf_initf(struct strbuf *sb, const char *fmt, ...); extern size_t strbuf_fread(struct strbuf *, size_t, FILE *); /* XXX: if read fails, any partial read is undone */ diff --git a/t/t0000-basic.sh b/t/t0000-basic.sh index 690f80a..7c7ca4c 100755 --- a/t/t0000-basic.sh +++ b/t/t0000-basic.sh @@ -311,6 +311,14 @@ test_expect_success 'absolute path works as expected' ' test "$sym" = "$(test-absolute-path "$dir2/syml")" ' +test_expect_success 'strbuf_initf() works as expected' ' + + eval $(test-strbuf) && + test ! -z "$result" && + test "$result" = "$expect" + +' + test_expect_success 'very long name in the index handled sanely' ' a=a && # 1 diff --git a/test-strbuf.c b/test-strbuf.c new file mode 100644 index 0000000..479fa08 --- /dev/null +++ b/test-strbuf.c @@ -0,0 +1,17 @@ +#include "cache.h" +#include "strbuf.h" + +int main(int argc, char **argv) +{ + struct strbuf buf; +#define TEST_FORMAT \ + "'%%%.*s,%x,%05X,%u,%i,% 4d,%3d,%c,%3d'", \ + 5, "Hello, World!", 27, 27, -1, -1, 1, 5, ':', 1234 + + strbuf_initf(&buf, TEST_FORMAT); + printf("result=%s\n", buf.buf); + printf("expect=" TEST_FORMAT); + strbuf_release(&buf); + + return 0; +} -- 1.5.6 ^ permalink raw reply related [flat|nested] 82+ messages in thread
* [PATCH 12/15] strbuf_vaddf(): support %*s, too 2008-06-27 16:22 ` [PATCH 11/15] Add strbuf_vaddf(), use it in strbuf_addf(), and add strbuf_initf() Miklos Vajna @ 2008-06-27 16:22 ` Miklos Vajna 2008-06-27 16:22 ` [PATCH 13/15] Add new test case to ensure git-merge reduces octopus parents when possible Miklos Vajna 2008-06-28 2:00 ` [PATCH 11/15] Add strbuf_vaddf(), use it in strbuf_addf(), and add strbuf_initf() Junio C Hamano 1 sibling, 1 reply; 82+ messages in thread From: Miklos Vajna @ 2008-06-27 16:22 UTC (permalink / raw To: Junio C Hamano Cc: git, Johannes Schindelin, Olivier Marin, Johannes Schindelin From: Johannes Schindelin <johannes.schindelin@gmx.de> The recent addition of graph.c broke things with our custom implementation of strbuf_vaddf(), because "%*s" was not yet supported (because it did not have any users before). Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de> --- strbuf.c | 6 ++++++ 1 files changed, 6 insertions(+), 0 deletions(-) diff --git a/strbuf.c b/strbuf.c index 72b3585..bd5dc80 100644 --- a/strbuf.c +++ b/strbuf.c @@ -154,6 +154,12 @@ void strbuf_vaddf(struct strbuf *sb, const char *fmt, va_list ap) fill = *p++; if (isdigit(*p)) size = (int)strtol(p, &p, 10); + else if (*p == '*') { + size = va_arg(ap, int); + if (!fill) + fill = ' '; + p++; + } else if (!prefixcmp(p, ".*")) { max_size = va_arg(ap, int); p += 2; -- 1.5.6 ^ permalink raw reply related [flat|nested] 82+ messages in thread
* [PATCH 13/15] Add new test case to ensure git-merge reduces octopus parents when possible 2008-06-27 16:22 ` [PATCH 12/15] strbuf_vaddf(): support %*s, too Miklos Vajna @ 2008-06-27 16:22 ` Miklos Vajna 2008-06-27 16:22 ` [PATCH 14/15] Add new test case to ensure git-merge prepends the custom merge message Miklos Vajna 0 siblings, 1 reply; 82+ messages in thread From: Miklos Vajna @ 2008-06-27 16:22 UTC (permalink / raw To: Junio C Hamano; +Cc: git, Johannes Schindelin, Olivier Marin, Junio C Hamano The old shell version used show-branch --independent to filter for the ones that cannot be reached from any other reference. The new C version uses reduce_heads() from commit.c for this, so add test to ensure it works as expected. Signed-off-by: Miklos Vajna <vmiklos@frugalware.org> Signed-off-by: Junio C Hamano <gitster@pobox.com> --- t/t7603-merge-reduce-heads.sh | 63 +++++++++++++++++++++++++++++++++++++++++ 1 files changed, 63 insertions(+), 0 deletions(-) create mode 100755 t/t7603-merge-reduce-heads.sh diff --git a/t/t7603-merge-reduce-heads.sh b/t/t7603-merge-reduce-heads.sh new file mode 100755 index 0000000..17b19dc --- /dev/null +++ b/t/t7603-merge-reduce-heads.sh @@ -0,0 +1,63 @@ +#!/bin/sh + +test_description='git-merge + +Testing octopus merge when reducing parents to independent branches.' + +. ./test-lib.sh + +# 0 - 1 +# \ 2 +# \ 3 +# \ 4 - 5 +# +# So 1, 2, 3 and 5 should be kept, 4 should be avoided. + +test_expect_success 'setup' ' + echo c0 > c0.c && + git add c0.c && + git commit -m c0 && + git tag c0 && + echo c1 > c1.c && + git add c1.c && + git commit -m c1 && + git tag c1 && + git reset --hard c0 && + echo c2 > c2.c && + git add c2.c && + git commit -m c2 && + git tag c2 && + git reset --hard c0 && + echo c3 > c3.c && + git add c3.c && + git commit -m c3 && + git tag c3 && + git reset --hard c0 && + echo c4 > c4.c && + git add c4.c && + git commit -m c4 && + git tag c4 && + echo c5 > c5.c && + git add c5.c && + git commit -m c5 && + git tag c5 +' + +test_expect_success 'merge c1 with c2, c3, c4, c5' ' + git reset --hard c1 && + git merge c2 c3 c4 c5 && + test "$(git rev-parse c1)" != "$(git rev-parse HEAD)" && + test "$(git rev-parse c1)" = "$(git rev-parse HEAD^1)" && + test "$(git rev-parse c2)" = "$(git rev-parse HEAD^2)" && + test "$(git rev-parse c3)" = "$(git rev-parse HEAD^3)" && + test "$(git rev-parse c5)" = "$(git rev-parse HEAD^4)" && + git diff --exit-code && + test -f c0.c && + test -f c1.c && + test -f c2.c && + test -f c3.c && + test -f c4.c && + test -f c5.c +' + +test_done -- 1.5.6 ^ permalink raw reply related [flat|nested] 82+ messages in thread
* [PATCH 14/15] Add new test case to ensure git-merge prepends the custom merge message 2008-06-27 16:22 ` [PATCH 13/15] Add new test case to ensure git-merge reduces octopus parents when possible Miklos Vajna @ 2008-06-27 16:22 ` Miklos Vajna 2008-06-27 16:22 ` [PATCH 15/15] Build in merge Miklos Vajna 0 siblings, 1 reply; 82+ messages in thread From: Miklos Vajna @ 2008-06-27 16:22 UTC (permalink / raw To: Junio C Hamano; +Cc: git, Johannes Schindelin, Olivier Marin There was no test for this before, so the testsuite passed, even in case the merge summary was missing from the merge commit message. Signed-off-by: Miklos Vajna <vmiklos@frugalware.org> --- t/t7604-merge-custom-message.sh | 37 +++++++++++++++++++++++++++++++++++++ 1 files changed, 37 insertions(+), 0 deletions(-) create mode 100755 t/t7604-merge-custom-message.sh diff --git a/t/t7604-merge-custom-message.sh b/t/t7604-merge-custom-message.sh new file mode 100755 index 0000000..6081677 --- /dev/null +++ b/t/t7604-merge-custom-message.sh @@ -0,0 +1,37 @@ +#!/bin/sh + +test_description='git-merge + +Testing merge when using a custom message for the merge commit.' + +. ./test-lib.sh + +test_expect_success 'setup' ' + echo c0 > c0.c && + git add c0.c && + git commit -m c0 && + git tag c0 && + echo c1 > c1.c && + git add c1.c && + git commit -m c1 && + git tag c1 && + git reset --hard c0 && + echo c2 > c2.c && + git add c2.c && + git commit -m c2 && + git tag c2 +' + +cat >expected <<\EOF +custom message + +Merge commit 'c2' +EOF +test_expect_success 'merge c2 with a custom message' ' + git reset --hard c1 && + git merge -m "custom message" c2 && + git cat-file commit HEAD | sed -e "1,/^$/d" > actual && + test_cmp expected actual +' + +test_done -- 1.5.6 ^ permalink raw reply related [flat|nested] 82+ messages in thread
* [PATCH 15/15] Build in merge 2008-06-27 16:22 ` [PATCH 14/15] Add new test case to ensure git-merge prepends the custom merge message Miklos Vajna @ 2008-06-27 16:22 ` Miklos Vajna 2008-06-27 17:09 ` Miklos Vajna 0 siblings, 1 reply; 82+ messages in thread From: Miklos Vajna @ 2008-06-27 16:22 UTC (permalink / raw To: Junio C Hamano; +Cc: git, Johannes Schindelin, Olivier Marin Mentored-by: Johannes Schindelin <Johannes.Schindelin@gmx.de> Signed-off-by: Miklos Vajna <vmiklos@frugalware.org> --- Makefile | 2 +- builtin-merge.c | 1143 +++++++++++++++++++++++++ builtin.h | 1 + git-merge.sh => contrib/examples/git-merge.sh | 0 git.c | 1 + t/t7602-merge-octopus-many.sh | 2 +- 6 files changed, 1147 insertions(+), 2 deletions(-) create mode 100644 builtin-merge.c rename git-merge.sh => contrib/examples/git-merge.sh (100%) diff --git a/Makefile b/Makefile index 1b43171..38135cd 100644 --- a/Makefile +++ b/Makefile @@ -240,7 +240,6 @@ SCRIPT_SH += git-lost-found.sh SCRIPT_SH += git-merge-octopus.sh SCRIPT_SH += git-merge-one-file.sh SCRIPT_SH += git-merge-resolve.sh -SCRIPT_SH += git-merge.sh SCRIPT_SH += git-merge-stupid.sh SCRIPT_SH += git-mergetool.sh SCRIPT_SH += git-parse-remote.sh @@ -512,6 +511,7 @@ BUILTIN_OBJS += builtin-ls-remote.o BUILTIN_OBJS += builtin-ls-tree.o BUILTIN_OBJS += builtin-mailinfo.o BUILTIN_OBJS += builtin-mailsplit.o +BUILTIN_OBJS += builtin-merge.o BUILTIN_OBJS += builtin-merge-base.o BUILTIN_OBJS += builtin-merge-file.o BUILTIN_OBJS += builtin-merge-ours.o diff --git a/builtin-merge.c b/builtin-merge.c new file mode 100644 index 0000000..0d56dca --- /dev/null +++ b/builtin-merge.c @@ -0,0 +1,1143 @@ +/* + * Builtin "git merge" + * + * Copyright (c) 2008 Miklos Vajna <vmiklos@frugalware.org> + * + * Based on git-merge.sh by Junio C Hamano. + */ + +#include "cache.h" +#include "parse-options.h" +#include "builtin.h" +#include "run-command.h" +#include "path-list.h" +#include "diff.h" +#include "refs.h" +#include "commit.h" +#include "diffcore.h" +#include "revision.h" +#include "unpack-trees.h" +#include "cache-tree.h" +#include "dir.h" +#include "utf8.h" +#include "log-tree.h" +#include "color.h" + +enum strategy { + DEFAULT_TWOHEAD = 1, + DEFAULT_OCTOPUS = 2, + NO_FAST_FORWARD = 4, + NO_TRIVIAL = 8 +}; + +static const char * const builtin_merge_usage[] = { + "git-merge [options] <remote>...", + "git-merge [options] <msg> HEAD <remote>", + NULL +}; + +static int show_diffstat = 1, option_log, squash; +static int option_commit = 1, allow_fast_forward = 1; +static int allow_trivial = 1, have_message; +static struct strbuf merge_msg; +static struct commit_list *remoteheads; +static unsigned char head[20]; +static struct path_list use_strategies; +static const char *branch; + +static struct path_list_item strategy_items[] = { + { "recur", (void *)NO_TRIVIAL }, + { "recursive", (void *)(DEFAULT_TWOHEAD | NO_TRIVIAL) }, + { "octopus", (void *)DEFAULT_OCTOPUS }, + { "resolve", (void *)0 }, + { "stupid", (void *)0 }, + { "ours", (void *)(NO_FAST_FORWARD | NO_TRIVIAL) }, + { "subtree", (void *)(NO_FAST_FORWARD | NO_TRIVIAL) }, +}; +static struct path_list strategies = { strategy_items, + ARRAY_SIZE(strategy_items), 0, 0 }; + +static const char *pull_twohead, *pull_octopus; + +static int option_parse_message(const struct option *opt, + const char *arg, int unset) +{ + struct strbuf *buf = opt->value; + + if (unset) + strbuf_setlen(buf, 0); + else { + strbuf_addf(buf, "%s\n\n", arg); + have_message = 1; + } + return 0; +} + +static struct path_list_item *unsorted_path_list_lookup(const char *path, + struct path_list *list) +{ + int i; + + if (!path) + return NULL; + + for (i = 0; i < list->nr; i++) + if (!strcmp(path, list->items[i].path)) + return &list->items[i]; + return NULL; +} + +static inline void path_list_append_strategy( + struct path_list_item *item) +{ + path_list_append(item->path, &use_strategies)->util = item->util; +} + +static int option_parse_strategy(const struct option *opt, + const char *arg, int unset) +{ + int i; + struct path_list_item *item = + unsorted_path_list_lookup(arg, &strategies); + + if (unset) + return 0; + + if (item) + path_list_append_strategy(item); + else { + struct strbuf err; + strbuf_init(&err, 0); + for (i = 0; i < strategies.nr; i++) + strbuf_addf(&err, " %s", strategies.items[i].path); + fprintf(stderr, "Could not find merge strategy '%s'.\n", arg); + fprintf(stderr, "Available strategies are:%s.\n", err.buf); + exit(1); + } + return 0; +} + +static int option_parse_n(const struct option *opt, + const char *arg, int unset) +{ + show_diffstat = unset; + return 0; +} + +static struct option builtin_merge_options[] = { + { OPTION_CALLBACK, 'n', NULL, NULL, NULL, + "do not show a diffstat at the end of the merge", + PARSE_OPT_NOARG, option_parse_n }, + OPT_BOOLEAN(0, "stat", &show_diffstat, + "show a diffstat at the end of the merge"), + OPT_BOOLEAN(0, "summary", &show_diffstat, "(synonym to --stat)"), + OPT_BOOLEAN(0, "log", &option_log, + "add list of one-line log to merge commit message"), + OPT_BOOLEAN(0, "squash", &squash, + "create a single commit instead of doing a merge"), + OPT_BOOLEAN(0, "commit", &option_commit, + "perform a commit if the merge succeeds (default)"), + OPT_BOOLEAN(0, "ff", &allow_fast_forward, + "allow fast forward (default)"), + OPT_CALLBACK('s', "strategy", &use_strategies, "strategy", + "merge strategy to use", option_parse_strategy), + OPT_CALLBACK('m', "message", &merge_msg, "message", + "message to be used for the merge commit (if any)", + option_parse_message), + OPT_END() +}; + +/* Cleans up metadata that is uninteresting after a succeeded merge. */ +static void dropsave(void) +{ + unlink(git_path("MERGE_HEAD")); + unlink(git_path("MERGE_MSG")); + unlink(git_path("MERGE_STASH")); +} + +static void save_state(void) +{ + int fd; + struct child_process stash; + const char *argv[] = {"stash", "create", NULL}; + + fd = open(git_path("MERGE_STASH"), O_WRONLY | O_CREAT, 0666); + if (fd < 0) + die("Could not write to %s", git_path("MERGE_STASH")); + memset(&stash, 0, sizeof(stash)); + stash.argv = argv; + stash.out = fd; + stash.git_cmd = 1; + run_command(&stash); +} + +static void reset_hard(unsigned const char *sha1, int verbose) +{ + struct tree *tree; + struct unpack_trees_options opts; + struct tree_desc t; + + memset(&opts, 0, sizeof(opts)); + opts.head_idx = -1; + opts.src_index = &the_index; + opts.dst_index = &the_index; + opts.update = 1; + opts.reset = 1; + if (verbose) + opts.verbose_update = 1; + + tree = parse_tree_indirect(sha1); + if (!tree) + die("failed to unpack %s tree object", sha1_to_hex(sha1)); + parse_tree(tree); + init_tree_desc(&t, tree->buffer, tree->size); + if (unpack_trees(1, &t, &opts)) + exit(128); /* We've already reported the error, finish dying */ +} + +static void restore_state(void) +{ + struct strbuf sb; + const char *args[] = { "stash", "apply", NULL, NULL }; + + if (access(git_path("MERGE_STASH"), R_OK) < 0) + return; + + reset_hard(head, 1); + + strbuf_init(&sb, 0); + if (strbuf_read_file(&sb, git_path("MERGE_STASH"), 0) < 0) + die("could not read MERGE_STASH: %s", strerror(errno)); + args[2] = sb.buf; + + /* + * It is OK to ignore error here, for example when there was + * nothing to restore. + */ + run_command_v_opt(args, RUN_GIT_CMD); + + strbuf_release(&sb); + refresh_cache(REFRESH_QUIET); +} + +/* This is called when no merge was necessary. */ +static void finish_up_to_date(const char *msg) +{ + printf("%s%s\n", squash ? " (nothing to squash)" : "", msg); + dropsave(); +} + +static void squash_message(void) +{ + struct rev_info rev; + struct commit *commit; + struct strbuf out; + struct commit_list *j; + int fd; + + printf("Squash commit -- not updating HEAD\n"); + fd = open(git_path("SQUASH_MSG"), O_WRONLY | O_CREAT, 0666); + if (fd < 0) + die("Could not write to %s", git_path("SQUASH_MSG")); + + init_revisions(&rev, NULL); + rev.ignore_merges = 1; + rev.commit_format = CMIT_FMT_MEDIUM; + + commit = lookup_commit(head); + commit->object.flags |= UNINTERESTING; + add_pending_object(&rev, &commit->object, NULL); + + for (j = remoteheads; j; j = j->next) + add_pending_object(&rev, &j->item->object, NULL); + + setup_revisions(0, NULL, &rev, NULL); + if (prepare_revision_walk(&rev)) + die("revision walk setup failed"); + + strbuf_initf(&out, "Squashed commit of the following:\n"); + while ((commit = get_revision(&rev)) != NULL) { + strbuf_addch(&out, '\n'); + strbuf_addf(&out, "commit %s\n", + sha1_to_hex(commit->object.sha1)); + pretty_print_commit(rev.commit_format, commit, &out, rev.abbrev, + NULL, NULL, rev.date_mode, 0); + } + write(fd, out.buf, out.len); + close(fd); + strbuf_release(&out); +} + +static int run_hook(const char *name) +{ + struct child_process hook; + const char *argv[3], *env[2]; + char index[PATH_MAX]; + + argv[0] = git_path("hooks/%s", name); + if (access(argv[0], X_OK) < 0) + return 0; + + snprintf(index, sizeof(index), "GIT_INDEX_FILE=%s", get_index_file()); + env[0] = index; + env[1] = NULL; + + if (squash) + argv[1] = "1"; + else + argv[1] = "0"; + argv[2] = NULL; + + memset(&hook, 0, sizeof(hook)); + hook.argv = argv; + hook.no_stdin = 1; + hook.stdout_to_stderr = 1; + hook.env = env; + + return run_command(&hook); +} + +static void finish(const unsigned char *new_head, const char *msg) +{ + struct strbuf reflog_message; + + if (!msg) + strbuf_initf(&reflog_message, "%s", + getenv("GIT_REFLOG_ACTION")); + else { + printf("%s\n", msg); + strbuf_initf(&reflog_message, "%s: %s", + getenv("GIT_REFLOG_ACTION"), msg); + } + if (squash) { + squash_message(); + } else { + if (!merge_msg.len) + printf("No merge message -- not updating HEAD\n"); + else { + const char *argv_gc_auto[] = { "gc", "--auto", NULL }; + update_ref(reflog_message.buf, "HEAD", + new_head, head, 0, + DIE_ON_ERR); + /* + * We ignore errors in 'gc --auto', since the + * user should see them. + */ + run_command_v_opt(argv_gc_auto, RUN_GIT_CMD); + } + } + if (new_head && show_diffstat) { + struct diff_options opts; + diff_setup(&opts); + opts.output_format |= + DIFF_FORMAT_SUMMARY | DIFF_FORMAT_DIFFSTAT; + opts.detect_rename = DIFF_DETECT_RENAME; + if (diff_use_color_default > 0) + DIFF_OPT_SET(&opts, COLOR_DIFF); + diff_tree_sha1(head, new_head, "", &opts); + diffcore_std(&opts); + diff_flush(&opts); + } + + /* Run a post-merge hook */ + run_hook("post-merge"); + + strbuf_release(&reflog_message); +} + +/* Get the name for the merge commit's message. */ +static void merge_name(const char *remote, struct strbuf *msg) +{ + struct object *remote_head; + unsigned char branch_head[20], buf_sha[20]; + struct strbuf buf; + char *ptr; + int len = 0; + + memset(branch_head, 0, sizeof(branch_head)); + remote_head = peel_to_type(remote, 0, NULL, OBJ_COMMIT); + if (!remote_head) + return; + + strbuf_initf(&buf, "refs/heads/%s", remote); + get_sha1(buf.buf, branch_head); + + if (!hashcmp(remote_head->sha1, branch_head)) { + strbuf_addf(msg, "%s\t\tbranch '%s' of .\n", + sha1_to_hex(branch_head), remote); + return; + } + /* See if remote matches <name>~<number>, or <name>^ */ + ptr = strrchr(remote, '^'); + if (ptr && ptr[1] == '\0') + len = ptr-remote; + else { + ptr = strrchr(remote, '~'); + if (ptr && ptr[1] != '0' && isdigit(ptr[1])) { + len = ptr-remote; + ptr++; + for (ptr++; *ptr; ptr++) + if (!isdigit(*ptr)) { + len = 0; + break; + } + } + } + if (len) { + struct strbuf truname = STRBUF_INIT; + strbuf_addstr(&truname, remote); + strbuf_setlen(&truname, len); + if (!get_sha1(truname.buf, buf_sha)) { + strbuf_addf(msg, + "%s\t\tbranch '%s' (early part) of .\n", + sha1_to_hex(remote_head->sha1), truname.buf); + return; + } + } + + if (!strcmp(remote, "FETCH_HEAD") && + !access(git_path("FETCH_HEAD"), R_OK)) { + FILE *fp; + struct strbuf line; + char *ptr; + + strbuf_init(&line, 0); + fp = fopen(git_path("FETCH_HEAD"), "r"); + if (!fp) + die("could not open %s for reading: %s", + git_path("FETCH_HEAD"), strerror(errno)); + strbuf_getline(&line, fp, '\n'); + fclose(fp); + ptr = strstr(line.buf, "\tnot-for-merge\t"); + if (ptr) + strbuf_remove(&line, ptr-line.buf+1, 13); + strbuf_addbuf(msg, &line); + strbuf_release(&line); + return; + } + strbuf_addf(msg, "%s\t\tcommit '%s'\n", + sha1_to_hex(remote_head->sha1), remote); +} + +int git_merge_config(const char *k, const char *v, void *cb) +{ + if (branch && !prefixcmp(k, "branch.") && + !prefixcmp(k + 7, branch) && + !strcmp(k + 7 + strlen(branch), ".mergeoptions")) { + const char **argv; + int argc; + char *buf; + + buf = xstrdup(v); + argc = split_cmdline(buf, &argv); + argv = xrealloc(argv, sizeof(*argv) * (argc + 2)); + memmove(argv + 1, argv, sizeof(*argv) * (argc + 1)); + argc++; + parse_options(argc, argv, builtin_merge_options, + builtin_merge_usage, 0); + free(buf); + } + + if (!strcmp(k, "merge.diffstat") || !strcmp(k, "merge.stat")) + show_diffstat = git_config_bool(k, v); + else if (!strcmp(k, "pull.twohead")) + return git_config_string(&pull_twohead, k, v); + else if (!strcmp(k, "pull.octopus")) + return git_config_string(&pull_octopus, k, v); + return 0; +} + +static int read_tree_trivial(unsigned char *common, unsigned char *head, + unsigned char *one) +{ + int i, nr_trees = 0; + struct tree *trees[MAX_UNPACK_TREES]; + struct tree_desc t[MAX_UNPACK_TREES]; + struct unpack_trees_options opts; + + memset(&opts, 0, sizeof(opts)); + opts.head_idx = -1; + opts.src_index = &the_index; + opts.dst_index = &the_index; + opts.update = 1; + opts.verbose_update = 1; + opts.trivial_merges_only = 1; + opts.merge = 1; + trees[nr_trees] = parse_tree_indirect(common); + if (!trees[nr_trees++]) + return -1; + trees[nr_trees] = parse_tree_indirect(head); + if (!trees[nr_trees++]) + return -1; + trees[nr_trees] = parse_tree_indirect(one); + if (!trees[nr_trees++]) + return -1; + opts.fn = threeway_merge; + cache_tree_free(&active_cache_tree); + opts.head_idx = 2; + for (i = 0; i < nr_trees; i++) { + parse_tree(trees[i]); + init_tree_desc(t+i, trees[i]->buffer, trees[i]->size); + } + if (unpack_trees(nr_trees, t, &opts)) + return -1; + return 0; +} + +static int commit_tree_trivial(const char *msg, unsigned const char *tree, + struct commit_list *parents, unsigned char *ret) +{ + struct commit_list *i; + struct strbuf buf; + int encoding_is_utf8; + + /* Not having i18n.commitencoding is the same as having utf-8 */ + encoding_is_utf8 = is_encoding_utf8(git_commit_encoding); + + strbuf_init(&buf, 8192); /* should avoid reallocs for the headers */ + strbuf_addf(&buf, "tree %s\n", sha1_to_hex(tree)); + + for (i = parents; i; i = i->next) + strbuf_addf(&buf, "parent %s\n", + sha1_to_hex(i->item->object.sha1)); + + /* Person/date information */ + strbuf_addf(&buf, "author %s\n", + git_author_info(IDENT_ERROR_ON_NO_NAME)); + strbuf_addf(&buf, "committer %s\n", + git_committer_info(IDENT_ERROR_ON_NO_NAME)); + if (!encoding_is_utf8) + strbuf_addf(&buf, "encoding %s\n", git_commit_encoding); + strbuf_addch(&buf, '\n'); + + /* And add the comment */ + strbuf_addstr(&buf, msg); + + write_sha1_file(buf.buf, buf.len, commit_type, ret); + strbuf_release(&buf); + return *ret; +} + +static void write_tree_trivial(unsigned char *sha1) +{ + if (write_cache_as_tree(sha1, 0, NULL)) + die("git write-tree failed to write a tree"); +} + +static int try_merge_strategy(char *strategy, struct commit_list *common, + struct strbuf *head_arg) +{ + const char **args; + int i = 0, ret; + struct commit_list *j; + struct strbuf buf; + + args = xmalloc((4 + commit_list_count(common) + + commit_list_count(remoteheads)) * sizeof(char *)); + strbuf_initf(&buf, "merge-%s", strategy); + args[i++] = buf.buf; + for (j = common; j; j = j->next) + args[i++] = xstrdup(sha1_to_hex(j->item->object.sha1)); + args[i++] = "--"; + args[i++] = head_arg->buf; + for (j = remoteheads; j; j = j->next) + args[i++] = xstrdup(sha1_to_hex(j->item->object.sha1)); + args[i] = NULL; + ret = run_command_v_opt(args, RUN_GIT_CMD); + strbuf_release(&buf); + i = 1; + for (j = common; j; j = j->next) + free((void *)args[i++]); + i += 2; + for (j = remoteheads; j; j = j->next) + free((void *)args[i++]); + free(args); + return -ret; +} + +static void count_diff_files(struct diff_queue_struct *q, + struct diff_options *opt, void *data) +{ + int *count = data; + + (*count) += q->nr; +} + +static int count_unmerged_entries(void) +{ + const struct index_state *state = &the_index; + int i, ret = 0; + + for (i = 0; i < state->cache_nr; i++) + if (ce_stage(state->cache[i])) + ret++; + + return ret; +} + +static int merge_one_remote(unsigned char *head, unsigned char *remote) +{ + struct tree *trees[MAX_UNPACK_TREES]; + struct unpack_trees_options opts; + struct tree_desc t[MAX_UNPACK_TREES]; + int i, fd, nr_trees = 0; + struct dir_struct dir; + struct lock_file *lock_file = xcalloc(1, sizeof(struct lock_file)); + + if (read_cache_unmerged()) + die("you need to resolve your current index first"); + + fd = hold_locked_index(lock_file, 1); + + memset(&trees, 0, sizeof(trees)); + memset(&opts, 0, sizeof(opts)); + memset(&t, 0, sizeof(t)); + dir.show_ignored = 1; + dir.exclude_per_dir = ".gitignore"; + opts.dir = &dir; + + opts.head_idx = 1; + opts.src_index = &the_index; + opts.dst_index = &the_index; + opts.update = 1; + opts.verbose_update = 1; + opts.merge = 1; + opts.fn = twoway_merge; + + trees[nr_trees] = parse_tree_indirect(head); + if (!trees[nr_trees++]) + return -1; + trees[nr_trees] = parse_tree_indirect(remote); + if (!trees[nr_trees++]) + return -1; + for (i = 0; i < nr_trees; i++) { + parse_tree(trees[i]); + init_tree_desc(t+i, trees[i]->buffer, trees[i]->size); + } + if (unpack_trees(nr_trees, t, &opts)) + return -1; + if (write_cache(fd, active_cache, active_nr) || + commit_locked_index(lock_file)) + die("unable to write new index file"); + return 0; +} + +static void split_merge_strategies(const char *string, struct path_list *list) +{ + char *p, *q, *buf; + + if (!string) + return; + + list->strdup_paths = 1; + buf = xstrdup(string); + q = buf; + for (;;) { + p = strchr(q, ' '); + if (!p) { + path_list_append(q, list); + free(buf); + return; + } else { + *p = '\0'; + path_list_append(q, list); + q = ++p; + } + } +} + +static void add_strategies(const char *string, enum strategy strategy) +{ + struct path_list list; + int i; + + memset(&list, 0, sizeof(list)); + split_merge_strategies(string, &list); + if (list.nr) { + for (i = 0; i < list.nr; i++) { + struct path_list_item *item; + + item = unsorted_path_list_lookup(list.items[i].path, + &strategies); + if (item) + path_list_append_strategy(item); + } + return; + } + for (i = 0; i < strategies.nr; i++) + if ((enum strategy)strategies.items[i].util & strategy) + path_list_append_strategy(&strategies.items[i]); +} + +int cmd_merge(int argc, const char **argv, const char *prefix) +{ + unsigned char sha1[20], result_tree[20]; + struct object *second_token = NULL; + struct strbuf buf, head_arg; + int flag, head_invalid, i, single_strategy; + int best_cnt = -1, merge_was_ok = 0, automerge_was_ok = 0; + struct commit_list *common = NULL; + struct path_list_item *best_strategy = NULL, *wt_strategy = NULL; + struct commit_list **remotes = &remoteheads; + + setup_work_tree(); + if (unmerged_cache()) + die("You are in the middle of a conflicted merge."); + + /* + * Check if we are _not_ on a detached HEAD, i.e. if there is a + * current branch. + */ + branch = resolve_ref("HEAD", sha1, 0, &flag); + if (branch && flag & REF_ISSYMREF) { + const char *ptr = skip_prefix(branch, "refs/heads/"); + if (ptr) + branch = ptr; + } + + git_config(git_merge_config, NULL); + + /* for color.diff and diff.color */ + git_config(git_diff_ui_config, NULL); + + /* for color.ui */ + if (diff_use_color_default == -1) + diff_use_color_default = git_use_color_default; + + argc = parse_options(argc, argv, builtin_merge_options, + builtin_merge_usage, 0); + + if (squash) { + if (!allow_fast_forward) + die("You cannot combine --squash with --no-ff."); + option_commit = 0; + } + + if (!argc) + usage_with_options(builtin_merge_usage, + builtin_merge_options); + + /* + * This could be traditional "merge <msg> HEAD <commit>..." and + * the way we can tell it is to see if the second token is HEAD, + * but some people might have misused the interface and used a + * committish that is the same as HEAD there instead. + * Traditional format never would have "-m" so it is an + * additional safety measure to check for it. + */ + strbuf_init(&buf, 0); + strbuf_init(&head_arg, 0); + if (argc > 1) + second_token = peel_to_type(argv[1], 0, NULL, OBJ_COMMIT); + head_invalid = get_sha1("HEAD", head); + + if (!have_message && second_token && + !hashcmp(second_token->sha1, head)) { + strbuf_addstr(&merge_msg, argv[0]); + strbuf_addstr(&head_arg, argv[1]); + argv += 2; + argc -= 2; + } else if (head_invalid) { + struct object *remote_head; + /* + * If the merged head is a valid one there is no reason + * to forbid "git merge" into a branch yet to be born. + * We do the same for "git pull". + */ + if (argc != 1) + die("Can merge only exactly one commit into " + "empty head"); + remote_head = peel_to_type(argv[0], 0, NULL, OBJ_COMMIT); + if (!remote_head) + die("%s - not something we can merge", argv[0]); + update_ref("initial pull", "HEAD", remote_head->sha1, NULL, 0, + DIE_ON_ERR); + reset_hard(remote_head->sha1, 0); + return 0; + } else { + /* We are invoked directly as the first-class UI. */ + strbuf_addstr(&head_arg, "HEAD"); + /* + * All the rest are the commits being merged; + * prepare the standard merge summary message to + * be appended to the given message. If remote + * is invalid we will die later in the common + * codepath so we discard the error in this + * loop. + */ + struct strbuf msg; + + strbuf_init(&msg, 0); + for (i = 0; i < argc; i++) + merge_name(argv[i], &msg); + fmt_merge_msg(option_log, &msg, &merge_msg); + if (merge_msg.len) + strbuf_setlen(&merge_msg, merge_msg.len-1); + } + + if (head_invalid || !argc) + usage_with_options(builtin_merge_usage, + builtin_merge_options); + + strbuf_addstr(&buf, "merge"); + for (i = 0; i < argc; i++) + strbuf_addf(&buf, " %s", argv[i]); + setenv("GIT_REFLOG_ACTION", buf.buf, 0); + strbuf_reset(&buf); + + for (i = 0; i < argc; i++) { + struct object *o; + + o = peel_to_type(argv[i], 0, NULL, OBJ_COMMIT); + if (!o) + die("%s - not something we can merge", argv[i]); + remotes = &commit_list_insert(lookup_commit(o->sha1), + remotes)->next; + + strbuf_addf(&buf, "GITHEAD_%s", sha1_to_hex(o->sha1)); + setenv(buf.buf, argv[i], 1); + strbuf_reset(&buf); + } + + if (!use_strategies.nr) { + if (!remoteheads->next) + add_strategies(pull_twohead, DEFAULT_TWOHEAD); + else + add_strategies(pull_octopus, DEFAULT_OCTOPUS); + } + + for (i = 0; i < use_strategies.nr; i++) { + if ((unsigned int)use_strategies.items[i].util & + NO_FAST_FORWARD) + allow_fast_forward = 0; + if ((unsigned int)use_strategies.items[i].util & NO_TRIVIAL) + allow_trivial = 0; + } + + if (!remoteheads->next) + common = get_merge_bases(lookup_commit(head), + remoteheads->item, 1); + else { + struct commit_list *list = remoteheads; + commit_list_insert(lookup_commit(head), &list); + common = get_octopus_merge_bases(list); + free(list); + } + + update_ref("updating ORIG_HEAD", "ORIG_HEAD", head, NULL, 0, + DIE_ON_ERR); + + if (!common) + ; /* No common ancestors found. We need a real merge. */ + else if (!remoteheads->next && + !hashcmp(common->item->object.sha1, + remoteheads->item->object.sha1)) { + /* + * If head can reach all the remote heads then we are up + * to date. + */ + finish_up_to_date("Already up-to-date."); + return 0; + } else if (allow_fast_forward && !remoteheads->next && + !hashcmp(common->item->object.sha1, head)) { + /* Again the most common case of merging one remote. */ + struct strbuf msg; + struct object *o; + char hex[41]; + + strcpy(hex, find_unique_abbrev(head, DEFAULT_ABBREV)); + + printf("Updating %s..%s\n", + hex, + find_unique_abbrev(remoteheads->item->object.sha1, + DEFAULT_ABBREV)); + refresh_cache(REFRESH_QUIET); + strbuf_initf(&msg, "%s", "Fast forward"); + if (have_message) + strbuf_addstr(&msg, + " (no commit created; -m option ignored)"); + o = peel_to_type(sha1_to_hex(remoteheads->item->object.sha1), + 0, NULL, OBJ_COMMIT); + if (!o) + return 0; + + if (merge_one_remote(head, remoteheads->item->object.sha1)) + return 0; + + finish(o->sha1, msg.buf); + dropsave(); + return 0; + } else if (!remoteheads->next && common->next) + ; + /* + * We are not doing octopus and not fast forward. Need + * a real merge. + */ + else if (!remoteheads->next && option_commit) { + /* + * We are not doing octopus, not fast forward, and have + * only one common. + */ + refresh_cache(REFRESH_QUIET); + if (allow_trivial) { + /* See if it is really trivial. */ + git_committer_info(IDENT_ERROR_ON_NO_NAME); + printf("Trying really trivial in-index merge...\n"); + if (!read_tree_trivial(common->item->object.sha1, + head, remoteheads->item->object.sha1)) { + unsigned char result_tree[20], + result_commit[20]; + struct commit_list parent; + + write_tree_trivial(result_tree); + printf("Wonderful.\n"); + parent.item = remoteheads->item; + parent.next = NULL; + commit_tree_trivial(merge_msg.buf, + result_tree, &parent, + result_commit); + finish(result_commit, "In-index merge"); + dropsave(); + return 0; + } + printf("Nope.\n"); + } + } else { + /* + * An octopus. If we can reach all the remote we are up + * to date. + */ + int up_to_date = 1; + struct commit_list *j; + + for (j = remoteheads; j; j = j->next) { + struct commit_list *common_one; + + /* + * Here we *have* to calculate the individual + * merge_bases again, otherwise "git merge HEAD^ + * HEAD^^" would be missed. + */ + common_one = get_merge_bases(lookup_commit(head), + j->item, 1); + if (hashcmp(common_one->item->object.sha1, + j->item->object.sha1)) { + up_to_date = 0; + break; + } + } + if (up_to_date) { + finish_up_to_date("Already up-to-date. Yeeah!"); + return 0; + } + } + + /* We are going to make a new commit. */ + git_committer_info(IDENT_ERROR_ON_NO_NAME); + + /* + * At this point, we need a real merge. No matter what strategy + * we use, it would operate on the index, possibly affecting the + * working tree, and when resolved cleanly, have the desired + * tree in the index -- this means that the index must be in + * sync with the head commit. The strategies are responsible + * to ensure this. + */ + if (use_strategies.nr != 1) { + /* + * Stash away the local changes so that we can try more + * than one. + */ + save_state(); + single_strategy = 0; + } else { + unlink(git_path("MERGE_STASH")); + single_strategy = 1; + } + + for (i = 0; i < use_strategies.nr; i++) { + int ret; + if (i) { + printf("Rewinding the tree to pristine...\n"); + restore_state(); + } + if (!single_strategy) + printf("Trying merge strategy %s...\n", + use_strategies.items[i].path); + /* + * Remember which strategy left the state in the working + * tree. + */ + wt_strategy = &use_strategies.items[i]; + + ret = try_merge_strategy(use_strategies.items[i].path, + common, &head_arg); + if (!option_commit && !ret) { + merge_was_ok = 1; + /* + * This is necessary here just to avoid writing + * the tree, but later we will *not* exit with + * status code 1 because merge_was_ok is set. + */ + ret = 1; + } + + if (ret) { + /* + * The backend exits with 1 when conflicts are + * left to be resolved, with 2 when it does not + * handle the given merge at all. + */ + if (ret == 1) { + int cnt = 0; + struct rev_info rev; + + if (read_cache() < 0) + die("failed to read the cache"); + + /* Check how many files differ. */ + init_revisions(&rev, ""); + setup_revisions(0, NULL, &rev, NULL); + rev.diffopt.output_format |= + DIFF_FORMAT_CALLBACK; + rev.diffopt.format_callback = count_diff_files; + rev.diffopt.format_callback_data = &cnt; + run_diff_files(&rev, 0); + + /* + * Check how many unmerged entries are + * there. + */ + cnt += count_unmerged_entries(); + + if (best_cnt <= 0 || cnt <= best_cnt) { + best_strategy = + &use_strategies.items[i]; + best_cnt = cnt; + } + } + if (merge_was_ok) + break; + else + continue; + } + + /* Automerge succeeded. */ + write_tree_trivial(result_tree); + automerge_was_ok = 1; + break; + } + + /* + * If we have a resulting tree, that means the strategy module + * auto resolved the merge cleanly. + */ + if (automerge_was_ok) { + struct commit_list *parents = NULL, *j; + unsigned char result_commit[20]; + + free_commit_list(common); + if (allow_fast_forward) { + parents = remoteheads; + commit_list_insert(lookup_commit(head), &parents); + parents = reduce_heads(parents); + } else { + struct commit_list **pptr = &parents; + + pptr = &commit_list_insert(lookup_commit(head), + pptr)->next; + for (j = remoteheads; j; j = j->next) + pptr = &commit_list_insert(j->item, pptr)->next; + } + free_commit_list(remoteheads); + strbuf_addch(&merge_msg, '\n'); + commit_tree_trivial(merge_msg.buf, result_tree, parents, + result_commit); + free_commit_list(parents); + strbuf_addf(&buf, "Merge made by %s.", wt_strategy->path); + finish(result_commit, buf.buf); + strbuf_release(&buf); + dropsave(); + return 0; + } + + /* + * Pick the result from the best strategy and have the user fix + * it up. + */ + if (!best_strategy) { + restore_state(); + if (use_strategies.nr > 1) + fprintf(stderr, + "No merge strategy handled the merge.\n"); + else + fprintf(stderr, "Merge with strategy %s failed.\n", + use_strategies.items[0].path); + return 2; + } else if (best_strategy == wt_strategy) + ; /* We already have its result in the working tree. */ + else { + printf("Rewinding the tree to pristine...\n"); + restore_state(); + printf("Using the %s to prepare resolving by hand.\n", + best_strategy->path); + try_merge_strategy(best_strategy->path, common, &head_arg); + } + + if (squash) + finish(NULL, NULL); + else { + int fd; + struct commit_list *j; + + for (j = remoteheads; j; j = j->next) + strbuf_addf(&buf, "%s\n", + sha1_to_hex(j->item->object.sha1)); + fd = open(git_path("MERGE_HEAD"), O_WRONLY | O_CREAT, 0666); + if (fd < 0) + die("Could open %s for writing", + git_path("MERGE_HEAD")); + if (write_in_full(fd, buf.buf, buf.len) != buf.len) + die("Could not write to %s", git_path("MERGE_HEAD")); + close(fd); + strbuf_addch(&merge_msg, '\n'); + fd = open(git_path("MERGE_MSG"), O_WRONLY | O_CREAT, 0666); + if (fd < 0) + die("Could open %s for writing", git_path("MERGE_MSG")); + if (write_in_full(fd, merge_msg.buf, merge_msg.len) != + merge_msg.len) + die("Could not write to %s", git_path("MERGE_MSG")); + close(fd); + } + + if (merge_was_ok) { + fprintf(stderr, "Automatic merge went well; " + "stopped before committing as requested\n"); + return 0; + } else { + FILE *fp; + int pos; + const char *argv_rerere[] = { "rerere", NULL }; + + fp = fopen(git_path("MERGE_MSG"), "a"); + if (!fp) + die("Could open %s for writing", git_path("MERGE_MSG")); + fprintf(fp, "\nConflicts:\n"); + for (pos = 0; pos < active_nr; pos++) { + struct cache_entry *ce = active_cache[pos]; + + if (ce_stage(ce)) { + fprintf(fp, "\t%s\n", ce->name); + while (pos + 1 < active_nr && + !strcmp(ce->name, + active_cache[pos + 1]->name)) + pos++; + } + } + fclose(fp); + run_command_v_opt(argv_rerere, RUN_GIT_CMD); + printf("Automatic merge failed; " + "fix conflicts and then commit the result.\n"); + return 1; + } +} diff --git a/builtin.h b/builtin.h index 2b01fea..8bf5280 100644 --- a/builtin.h +++ b/builtin.h @@ -60,6 +60,7 @@ extern int cmd_ls_tree(int argc, const char **argv, const char *prefix); extern int cmd_ls_remote(int argc, const char **argv, const char *prefix); extern int cmd_mailinfo(int argc, const char **argv, const char *prefix); extern int cmd_mailsplit(int argc, const char **argv, const char *prefix); +extern int cmd_merge(int argc, const char **argv, const char *prefix); extern int cmd_merge_base(int argc, const char **argv, const char *prefix); extern int cmd_merge_ours(int argc, const char **argv, const char *prefix); extern int cmd_merge_file(int argc, const char **argv, const char *prefix); diff --git a/git-merge.sh b/contrib/examples/git-merge.sh similarity index 100% rename from git-merge.sh rename to contrib/examples/git-merge.sh diff --git a/git.c b/git.c index 2fbe96b..770aadd 100644 --- a/git.c +++ b/git.c @@ -271,6 +271,7 @@ static void handle_internal_command(int argc, const char **argv) { "ls-remote", cmd_ls_remote }, { "mailinfo", cmd_mailinfo }, { "mailsplit", cmd_mailsplit }, + { "merge", cmd_merge, RUN_SETUP | NEED_WORK_TREE }, { "merge-base", cmd_merge_base, RUN_SETUP }, { "merge-file", cmd_merge_file }, { "merge-ours", cmd_merge_ours, RUN_SETUP }, diff --git a/t/t7602-merge-octopus-many.sh b/t/t7602-merge-octopus-many.sh index f3a4bb2..fcb8285 100755 --- a/t/t7602-merge-octopus-many.sh +++ b/t/t7602-merge-octopus-many.sh @@ -23,7 +23,7 @@ test_expect_success 'setup' ' done ' -test_expect_failure 'merge c1 with c2, c3, c4, ... c29' ' +test_expect_success 'merge c1 with c2, c3, c4, ... c29' ' git reset --hard c1 && i=2 && refs="" && -- 1.5.6 ^ permalink raw reply related [flat|nested] 82+ messages in thread
* [PATCH 15/15] Build in merge 2008-06-27 16:22 ` [PATCH 15/15] Build in merge Miklos Vajna @ 2008-06-27 17:09 ` Miklos Vajna 0 siblings, 0 replies; 82+ messages in thread From: Miklos Vajna @ 2008-06-27 17:09 UTC (permalink / raw To: Junio C Hamano; +Cc: git, Johannes Schindelin, Olivier Marin Mentored-by: Johannes Schindelin <Johannes.Schindelin@gmx.de> Signed-off-by: Miklos Vajna <vmiklos@frugalware.org> --- On Fri, Jun 27, 2008 at 06:22:08PM +0200, Miklos Vajna <vmiklos@frugalware.org> wrote: > diff --git a/t/t7602-merge-octopus-many.sh > b/t/t7602-merge-octopus-many.sh > index f3a4bb2..fcb8285 100755 > --- a/t/t7602-merge-octopus-many.sh > +++ b/t/t7602-merge-octopus-many.sh > @@ -23,7 +23,7 @@ test_expect_success 'setup' ' > done > ' > > -test_expect_failure 'merge c1 with c2, c3, c4, ... c29' ' > +test_expect_success 'merge c1 with c2, c3, c4, ... c29' ' > git reset --hard c1 && > i=2 && > refs="" && This is an unrelated hunk, patch below which does not contains this. Makefile | 2 +- builtin-merge.c | 1143 +++++++++++++++++++++++++ builtin.h | 1 + git-merge.sh => contrib/examples/git-merge.sh | 0 git.c | 1 + 5 files changed, 1146 insertions(+), 1 deletions(-) create mode 100644 builtin-merge.c rename git-merge.sh => contrib/examples/git-merge.sh (100%) diff --git a/Makefile b/Makefile index 425d1ca..6c642cd 100644 --- a/Makefile +++ b/Makefile @@ -240,7 +240,6 @@ SCRIPT_SH += git-lost-found.sh SCRIPT_SH += git-merge-octopus.sh SCRIPT_SH += git-merge-one-file.sh SCRIPT_SH += git-merge-resolve.sh -SCRIPT_SH += git-merge.sh SCRIPT_SH += git-merge-stupid.sh SCRIPT_SH += git-mergetool.sh SCRIPT_SH += git-parse-remote.sh @@ -514,6 +513,7 @@ BUILTIN_OBJS += builtin-ls-remote.o BUILTIN_OBJS += builtin-ls-tree.o BUILTIN_OBJS += builtin-mailinfo.o BUILTIN_OBJS += builtin-mailsplit.o +BUILTIN_OBJS += builtin-merge.o BUILTIN_OBJS += builtin-merge-base.o BUILTIN_OBJS += builtin-merge-file.o BUILTIN_OBJS += builtin-merge-ours.o diff --git a/builtin-merge.c b/builtin-merge.c new file mode 100644 index 0000000..0d56dca --- /dev/null +++ b/builtin-merge.c @@ -0,0 +1,1143 @@ +/* + * Builtin "git merge" + * + * Copyright (c) 2008 Miklos Vajna <vmiklos@frugalware.org> + * + * Based on git-merge.sh by Junio C Hamano. + */ + +#include "cache.h" +#include "parse-options.h" +#include "builtin.h" +#include "run-command.h" +#include "path-list.h" +#include "diff.h" +#include "refs.h" +#include "commit.h" +#include "diffcore.h" +#include "revision.h" +#include "unpack-trees.h" +#include "cache-tree.h" +#include "dir.h" +#include "utf8.h" +#include "log-tree.h" +#include "color.h" + +enum strategy { + DEFAULT_TWOHEAD = 1, + DEFAULT_OCTOPUS = 2, + NO_FAST_FORWARD = 4, + NO_TRIVIAL = 8 +}; + +static const char * const builtin_merge_usage[] = { + "git-merge [options] <remote>...", + "git-merge [options] <msg> HEAD <remote>", + NULL +}; + +static int show_diffstat = 1, option_log, squash; +static int option_commit = 1, allow_fast_forward = 1; +static int allow_trivial = 1, have_message; +static struct strbuf merge_msg; +static struct commit_list *remoteheads; +static unsigned char head[20]; +static struct path_list use_strategies; +static const char *branch; + +static struct path_list_item strategy_items[] = { + { "recur", (void *)NO_TRIVIAL }, + { "recursive", (void *)(DEFAULT_TWOHEAD | NO_TRIVIAL) }, + { "octopus", (void *)DEFAULT_OCTOPUS }, + { "resolve", (void *)0 }, + { "stupid", (void *)0 }, + { "ours", (void *)(NO_FAST_FORWARD | NO_TRIVIAL) }, + { "subtree", (void *)(NO_FAST_FORWARD | NO_TRIVIAL) }, +}; +static struct path_list strategies = { strategy_items, + ARRAY_SIZE(strategy_items), 0, 0 }; + +static const char *pull_twohead, *pull_octopus; + +static int option_parse_message(const struct option *opt, + const char *arg, int unset) +{ + struct strbuf *buf = opt->value; + + if (unset) + strbuf_setlen(buf, 0); + else { + strbuf_addf(buf, "%s\n\n", arg); + have_message = 1; + } + return 0; +} + +static struct path_list_item *unsorted_path_list_lookup(const char *path, + struct path_list *list) +{ + int i; + + if (!path) + return NULL; + + for (i = 0; i < list->nr; i++) + if (!strcmp(path, list->items[i].path)) + return &list->items[i]; + return NULL; +} + +static inline void path_list_append_strategy( + struct path_list_item *item) +{ + path_list_append(item->path, &use_strategies)->util = item->util; +} + +static int option_parse_strategy(const struct option *opt, + const char *arg, int unset) +{ + int i; + struct path_list_item *item = + unsorted_path_list_lookup(arg, &strategies); + + if (unset) + return 0; + + if (item) + path_list_append_strategy(item); + else { + struct strbuf err; + strbuf_init(&err, 0); + for (i = 0; i < strategies.nr; i++) + strbuf_addf(&err, " %s", strategies.items[i].path); + fprintf(stderr, "Could not find merge strategy '%s'.\n", arg); + fprintf(stderr, "Available strategies are:%s.\n", err.buf); + exit(1); + } + return 0; +} + +static int option_parse_n(const struct option *opt, + const char *arg, int unset) +{ + show_diffstat = unset; + return 0; +} + +static struct option builtin_merge_options[] = { + { OPTION_CALLBACK, 'n', NULL, NULL, NULL, + "do not show a diffstat at the end of the merge", + PARSE_OPT_NOARG, option_parse_n }, + OPT_BOOLEAN(0, "stat", &show_diffstat, + "show a diffstat at the end of the merge"), + OPT_BOOLEAN(0, "summary", &show_diffstat, "(synonym to --stat)"), + OPT_BOOLEAN(0, "log", &option_log, + "add list of one-line log to merge commit message"), + OPT_BOOLEAN(0, "squash", &squash, + "create a single commit instead of doing a merge"), + OPT_BOOLEAN(0, "commit", &option_commit, + "perform a commit if the merge succeeds (default)"), + OPT_BOOLEAN(0, "ff", &allow_fast_forward, + "allow fast forward (default)"), + OPT_CALLBACK('s', "strategy", &use_strategies, "strategy", + "merge strategy to use", option_parse_strategy), + OPT_CALLBACK('m', "message", &merge_msg, "message", + "message to be used for the merge commit (if any)", + option_parse_message), + OPT_END() +}; + +/* Cleans up metadata that is uninteresting after a succeeded merge. */ +static void dropsave(void) +{ + unlink(git_path("MERGE_HEAD")); + unlink(git_path("MERGE_MSG")); + unlink(git_path("MERGE_STASH")); +} + +static void save_state(void) +{ + int fd; + struct child_process stash; + const char *argv[] = {"stash", "create", NULL}; + + fd = open(git_path("MERGE_STASH"), O_WRONLY | O_CREAT, 0666); + if (fd < 0) + die("Could not write to %s", git_path("MERGE_STASH")); + memset(&stash, 0, sizeof(stash)); + stash.argv = argv; + stash.out = fd; + stash.git_cmd = 1; + run_command(&stash); +} + +static void reset_hard(unsigned const char *sha1, int verbose) +{ + struct tree *tree; + struct unpack_trees_options opts; + struct tree_desc t; + + memset(&opts, 0, sizeof(opts)); + opts.head_idx = -1; + opts.src_index = &the_index; + opts.dst_index = &the_index; + opts.update = 1; + opts.reset = 1; + if (verbose) + opts.verbose_update = 1; + + tree = parse_tree_indirect(sha1); + if (!tree) + die("failed to unpack %s tree object", sha1_to_hex(sha1)); + parse_tree(tree); + init_tree_desc(&t, tree->buffer, tree->size); + if (unpack_trees(1, &t, &opts)) + exit(128); /* We've already reported the error, finish dying */ +} + +static void restore_state(void) +{ + struct strbuf sb; + const char *args[] = { "stash", "apply", NULL, NULL }; + + if (access(git_path("MERGE_STASH"), R_OK) < 0) + return; + + reset_hard(head, 1); + + strbuf_init(&sb, 0); + if (strbuf_read_file(&sb, git_path("MERGE_STASH"), 0) < 0) + die("could not read MERGE_STASH: %s", strerror(errno)); + args[2] = sb.buf; + + /* + * It is OK to ignore error here, for example when there was + * nothing to restore. + */ + run_command_v_opt(args, RUN_GIT_CMD); + + strbuf_release(&sb); + refresh_cache(REFRESH_QUIET); +} + +/* This is called when no merge was necessary. */ +static void finish_up_to_date(const char *msg) +{ + printf("%s%s\n", squash ? " (nothing to squash)" : "", msg); + dropsave(); +} + +static void squash_message(void) +{ + struct rev_info rev; + struct commit *commit; + struct strbuf out; + struct commit_list *j; + int fd; + + printf("Squash commit -- not updating HEAD\n"); + fd = open(git_path("SQUASH_MSG"), O_WRONLY | O_CREAT, 0666); + if (fd < 0) + die("Could not write to %s", git_path("SQUASH_MSG")); + + init_revisions(&rev, NULL); + rev.ignore_merges = 1; + rev.commit_format = CMIT_FMT_MEDIUM; + + commit = lookup_commit(head); + commit->object.flags |= UNINTERESTING; + add_pending_object(&rev, &commit->object, NULL); + + for (j = remoteheads; j; j = j->next) + add_pending_object(&rev, &j->item->object, NULL); + + setup_revisions(0, NULL, &rev, NULL); + if (prepare_revision_walk(&rev)) + die("revision walk setup failed"); + + strbuf_initf(&out, "Squashed commit of the following:\n"); + while ((commit = get_revision(&rev)) != NULL) { + strbuf_addch(&out, '\n'); + strbuf_addf(&out, "commit %s\n", + sha1_to_hex(commit->object.sha1)); + pretty_print_commit(rev.commit_format, commit, &out, rev.abbrev, + NULL, NULL, rev.date_mode, 0); + } + write(fd, out.buf, out.len); + close(fd); + strbuf_release(&out); +} + +static int run_hook(const char *name) +{ + struct child_process hook; + const char *argv[3], *env[2]; + char index[PATH_MAX]; + + argv[0] = git_path("hooks/%s", name); + if (access(argv[0], X_OK) < 0) + return 0; + + snprintf(index, sizeof(index), "GIT_INDEX_FILE=%s", get_index_file()); + env[0] = index; + env[1] = NULL; + + if (squash) + argv[1] = "1"; + else + argv[1] = "0"; + argv[2] = NULL; + + memset(&hook, 0, sizeof(hook)); + hook.argv = argv; + hook.no_stdin = 1; + hook.stdout_to_stderr = 1; + hook.env = env; + + return run_command(&hook); +} + +static void finish(const unsigned char *new_head, const char *msg) +{ + struct strbuf reflog_message; + + if (!msg) + strbuf_initf(&reflog_message, "%s", + getenv("GIT_REFLOG_ACTION")); + else { + printf("%s\n", msg); + strbuf_initf(&reflog_message, "%s: %s", + getenv("GIT_REFLOG_ACTION"), msg); + } + if (squash) { + squash_message(); + } else { + if (!merge_msg.len) + printf("No merge message -- not updating HEAD\n"); + else { + const char *argv_gc_auto[] = { "gc", "--auto", NULL }; + update_ref(reflog_message.buf, "HEAD", + new_head, head, 0, + DIE_ON_ERR); + /* + * We ignore errors in 'gc --auto', since the + * user should see them. + */ + run_command_v_opt(argv_gc_auto, RUN_GIT_CMD); + } + } + if (new_head && show_diffstat) { + struct diff_options opts; + diff_setup(&opts); + opts.output_format |= + DIFF_FORMAT_SUMMARY | DIFF_FORMAT_DIFFSTAT; + opts.detect_rename = DIFF_DETECT_RENAME; + if (diff_use_color_default > 0) + DIFF_OPT_SET(&opts, COLOR_DIFF); + diff_tree_sha1(head, new_head, "", &opts); + diffcore_std(&opts); + diff_flush(&opts); + } + + /* Run a post-merge hook */ + run_hook("post-merge"); + + strbuf_release(&reflog_message); +} + +/* Get the name for the merge commit's message. */ +static void merge_name(const char *remote, struct strbuf *msg) +{ + struct object *remote_head; + unsigned char branch_head[20], buf_sha[20]; + struct strbuf buf; + char *ptr; + int len = 0; + + memset(branch_head, 0, sizeof(branch_head)); + remote_head = peel_to_type(remote, 0, NULL, OBJ_COMMIT); + if (!remote_head) + return; + + strbuf_initf(&buf, "refs/heads/%s", remote); + get_sha1(buf.buf, branch_head); + + if (!hashcmp(remote_head->sha1, branch_head)) { + strbuf_addf(msg, "%s\t\tbranch '%s' of .\n", + sha1_to_hex(branch_head), remote); + return; + } + /* See if remote matches <name>~<number>, or <name>^ */ + ptr = strrchr(remote, '^'); + if (ptr && ptr[1] == '\0') + len = ptr-remote; + else { + ptr = strrchr(remote, '~'); + if (ptr && ptr[1] != '0' && isdigit(ptr[1])) { + len = ptr-remote; + ptr++; + for (ptr++; *ptr; ptr++) + if (!isdigit(*ptr)) { + len = 0; + break; + } + } + } + if (len) { + struct strbuf truname = STRBUF_INIT; + strbuf_addstr(&truname, remote); + strbuf_setlen(&truname, len); + if (!get_sha1(truname.buf, buf_sha)) { + strbuf_addf(msg, + "%s\t\tbranch '%s' (early part) of .\n", + sha1_to_hex(remote_head->sha1), truname.buf); + return; + } + } + + if (!strcmp(remote, "FETCH_HEAD") && + !access(git_path("FETCH_HEAD"), R_OK)) { + FILE *fp; + struct strbuf line; + char *ptr; + + strbuf_init(&line, 0); + fp = fopen(git_path("FETCH_HEAD"), "r"); + if (!fp) + die("could not open %s for reading: %s", + git_path("FETCH_HEAD"), strerror(errno)); + strbuf_getline(&line, fp, '\n'); + fclose(fp); + ptr = strstr(line.buf, "\tnot-for-merge\t"); + if (ptr) + strbuf_remove(&line, ptr-line.buf+1, 13); + strbuf_addbuf(msg, &line); + strbuf_release(&line); + return; + } + strbuf_addf(msg, "%s\t\tcommit '%s'\n", + sha1_to_hex(remote_head->sha1), remote); +} + +int git_merge_config(const char *k, const char *v, void *cb) +{ + if (branch && !prefixcmp(k, "branch.") && + !prefixcmp(k + 7, branch) && + !strcmp(k + 7 + strlen(branch), ".mergeoptions")) { + const char **argv; + int argc; + char *buf; + + buf = xstrdup(v); + argc = split_cmdline(buf, &argv); + argv = xrealloc(argv, sizeof(*argv) * (argc + 2)); + memmove(argv + 1, argv, sizeof(*argv) * (argc + 1)); + argc++; + parse_options(argc, argv, builtin_merge_options, + builtin_merge_usage, 0); + free(buf); + } + + if (!strcmp(k, "merge.diffstat") || !strcmp(k, "merge.stat")) + show_diffstat = git_config_bool(k, v); + else if (!strcmp(k, "pull.twohead")) + return git_config_string(&pull_twohead, k, v); + else if (!strcmp(k, "pull.octopus")) + return git_config_string(&pull_octopus, k, v); + return 0; +} + +static int read_tree_trivial(unsigned char *common, unsigned char *head, + unsigned char *one) +{ + int i, nr_trees = 0; + struct tree *trees[MAX_UNPACK_TREES]; + struct tree_desc t[MAX_UNPACK_TREES]; + struct unpack_trees_options opts; + + memset(&opts, 0, sizeof(opts)); + opts.head_idx = -1; + opts.src_index = &the_index; + opts.dst_index = &the_index; + opts.update = 1; + opts.verbose_update = 1; + opts.trivial_merges_only = 1; + opts.merge = 1; + trees[nr_trees] = parse_tree_indirect(common); + if (!trees[nr_trees++]) + return -1; + trees[nr_trees] = parse_tree_indirect(head); + if (!trees[nr_trees++]) + return -1; + trees[nr_trees] = parse_tree_indirect(one); + if (!trees[nr_trees++]) + return -1; + opts.fn = threeway_merge; + cache_tree_free(&active_cache_tree); + opts.head_idx = 2; + for (i = 0; i < nr_trees; i++) { + parse_tree(trees[i]); + init_tree_desc(t+i, trees[i]->buffer, trees[i]->size); + } + if (unpack_trees(nr_trees, t, &opts)) + return -1; + return 0; +} + +static int commit_tree_trivial(const char *msg, unsigned const char *tree, + struct commit_list *parents, unsigned char *ret) +{ + struct commit_list *i; + struct strbuf buf; + int encoding_is_utf8; + + /* Not having i18n.commitencoding is the same as having utf-8 */ + encoding_is_utf8 = is_encoding_utf8(git_commit_encoding); + + strbuf_init(&buf, 8192); /* should avoid reallocs for the headers */ + strbuf_addf(&buf, "tree %s\n", sha1_to_hex(tree)); + + for (i = parents; i; i = i->next) + strbuf_addf(&buf, "parent %s\n", + sha1_to_hex(i->item->object.sha1)); + + /* Person/date information */ + strbuf_addf(&buf, "author %s\n", + git_author_info(IDENT_ERROR_ON_NO_NAME)); + strbuf_addf(&buf, "committer %s\n", + git_committer_info(IDENT_ERROR_ON_NO_NAME)); + if (!encoding_is_utf8) + strbuf_addf(&buf, "encoding %s\n", git_commit_encoding); + strbuf_addch(&buf, '\n'); + + /* And add the comment */ + strbuf_addstr(&buf, msg); + + write_sha1_file(buf.buf, buf.len, commit_type, ret); + strbuf_release(&buf); + return *ret; +} + +static void write_tree_trivial(unsigned char *sha1) +{ + if (write_cache_as_tree(sha1, 0, NULL)) + die("git write-tree failed to write a tree"); +} + +static int try_merge_strategy(char *strategy, struct commit_list *common, + struct strbuf *head_arg) +{ + const char **args; + int i = 0, ret; + struct commit_list *j; + struct strbuf buf; + + args = xmalloc((4 + commit_list_count(common) + + commit_list_count(remoteheads)) * sizeof(char *)); + strbuf_initf(&buf, "merge-%s", strategy); + args[i++] = buf.buf; + for (j = common; j; j = j->next) + args[i++] = xstrdup(sha1_to_hex(j->item->object.sha1)); + args[i++] = "--"; + args[i++] = head_arg->buf; + for (j = remoteheads; j; j = j->next) + args[i++] = xstrdup(sha1_to_hex(j->item->object.sha1)); + args[i] = NULL; + ret = run_command_v_opt(args, RUN_GIT_CMD); + strbuf_release(&buf); + i = 1; + for (j = common; j; j = j->next) + free((void *)args[i++]); + i += 2; + for (j = remoteheads; j; j = j->next) + free((void *)args[i++]); + free(args); + return -ret; +} + +static void count_diff_files(struct diff_queue_struct *q, + struct diff_options *opt, void *data) +{ + int *count = data; + + (*count) += q->nr; +} + +static int count_unmerged_entries(void) +{ + const struct index_state *state = &the_index; + int i, ret = 0; + + for (i = 0; i < state->cache_nr; i++) + if (ce_stage(state->cache[i])) + ret++; + + return ret; +} + +static int merge_one_remote(unsigned char *head, unsigned char *remote) +{ + struct tree *trees[MAX_UNPACK_TREES]; + struct unpack_trees_options opts; + struct tree_desc t[MAX_UNPACK_TREES]; + int i, fd, nr_trees = 0; + struct dir_struct dir; + struct lock_file *lock_file = xcalloc(1, sizeof(struct lock_file)); + + if (read_cache_unmerged()) + die("you need to resolve your current index first"); + + fd = hold_locked_index(lock_file, 1); + + memset(&trees, 0, sizeof(trees)); + memset(&opts, 0, sizeof(opts)); + memset(&t, 0, sizeof(t)); + dir.show_ignored = 1; + dir.exclude_per_dir = ".gitignore"; + opts.dir = &dir; + + opts.head_idx = 1; + opts.src_index = &the_index; + opts.dst_index = &the_index; + opts.update = 1; + opts.verbose_update = 1; + opts.merge = 1; + opts.fn = twoway_merge; + + trees[nr_trees] = parse_tree_indirect(head); + if (!trees[nr_trees++]) + return -1; + trees[nr_trees] = parse_tree_indirect(remote); + if (!trees[nr_trees++]) + return -1; + for (i = 0; i < nr_trees; i++) { + parse_tree(trees[i]); + init_tree_desc(t+i, trees[i]->buffer, trees[i]->size); + } + if (unpack_trees(nr_trees, t, &opts)) + return -1; + if (write_cache(fd, active_cache, active_nr) || + commit_locked_index(lock_file)) + die("unable to write new index file"); + return 0; +} + +static void split_merge_strategies(const char *string, struct path_list *list) +{ + char *p, *q, *buf; + + if (!string) + return; + + list->strdup_paths = 1; + buf = xstrdup(string); + q = buf; + for (;;) { + p = strchr(q, ' '); + if (!p) { + path_list_append(q, list); + free(buf); + return; + } else { + *p = '\0'; + path_list_append(q, list); + q = ++p; + } + } +} + +static void add_strategies(const char *string, enum strategy strategy) +{ + struct path_list list; + int i; + + memset(&list, 0, sizeof(list)); + split_merge_strategies(string, &list); + if (list.nr) { + for (i = 0; i < list.nr; i++) { + struct path_list_item *item; + + item = unsorted_path_list_lookup(list.items[i].path, + &strategies); + if (item) + path_list_append_strategy(item); + } + return; + } + for (i = 0; i < strategies.nr; i++) + if ((enum strategy)strategies.items[i].util & strategy) + path_list_append_strategy(&strategies.items[i]); +} + +int cmd_merge(int argc, const char **argv, const char *prefix) +{ + unsigned char sha1[20], result_tree[20]; + struct object *second_token = NULL; + struct strbuf buf, head_arg; + int flag, head_invalid, i, single_strategy; + int best_cnt = -1, merge_was_ok = 0, automerge_was_ok = 0; + struct commit_list *common = NULL; + struct path_list_item *best_strategy = NULL, *wt_strategy = NULL; + struct commit_list **remotes = &remoteheads; + + setup_work_tree(); + if (unmerged_cache()) + die("You are in the middle of a conflicted merge."); + + /* + * Check if we are _not_ on a detached HEAD, i.e. if there is a + * current branch. + */ + branch = resolve_ref("HEAD", sha1, 0, &flag); + if (branch && flag & REF_ISSYMREF) { + const char *ptr = skip_prefix(branch, "refs/heads/"); + if (ptr) + branch = ptr; + } + + git_config(git_merge_config, NULL); + + /* for color.diff and diff.color */ + git_config(git_diff_ui_config, NULL); + + /* for color.ui */ + if (diff_use_color_default == -1) + diff_use_color_default = git_use_color_default; + + argc = parse_options(argc, argv, builtin_merge_options, + builtin_merge_usage, 0); + + if (squash) { + if (!allow_fast_forward) + die("You cannot combine --squash with --no-ff."); + option_commit = 0; + } + + if (!argc) + usage_with_options(builtin_merge_usage, + builtin_merge_options); + + /* + * This could be traditional "merge <msg> HEAD <commit>..." and + * the way we can tell it is to see if the second token is HEAD, + * but some people might have misused the interface and used a + * committish that is the same as HEAD there instead. + * Traditional format never would have "-m" so it is an + * additional safety measure to check for it. + */ + strbuf_init(&buf, 0); + strbuf_init(&head_arg, 0); + if (argc > 1) + second_token = peel_to_type(argv[1], 0, NULL, OBJ_COMMIT); + head_invalid = get_sha1("HEAD", head); + + if (!have_message && second_token && + !hashcmp(second_token->sha1, head)) { + strbuf_addstr(&merge_msg, argv[0]); + strbuf_addstr(&head_arg, argv[1]); + argv += 2; + argc -= 2; + } else if (head_invalid) { + struct object *remote_head; + /* + * If the merged head is a valid one there is no reason + * to forbid "git merge" into a branch yet to be born. + * We do the same for "git pull". + */ + if (argc != 1) + die("Can merge only exactly one commit into " + "empty head"); + remote_head = peel_to_type(argv[0], 0, NULL, OBJ_COMMIT); + if (!remote_head) + die("%s - not something we can merge", argv[0]); + update_ref("initial pull", "HEAD", remote_head->sha1, NULL, 0, + DIE_ON_ERR); + reset_hard(remote_head->sha1, 0); + return 0; + } else { + /* We are invoked directly as the first-class UI. */ + strbuf_addstr(&head_arg, "HEAD"); + /* + * All the rest are the commits being merged; + * prepare the standard merge summary message to + * be appended to the given message. If remote + * is invalid we will die later in the common + * codepath so we discard the error in this + * loop. + */ + struct strbuf msg; + + strbuf_init(&msg, 0); + for (i = 0; i < argc; i++) + merge_name(argv[i], &msg); + fmt_merge_msg(option_log, &msg, &merge_msg); + if (merge_msg.len) + strbuf_setlen(&merge_msg, merge_msg.len-1); + } + + if (head_invalid || !argc) + usage_with_options(builtin_merge_usage, + builtin_merge_options); + + strbuf_addstr(&buf, "merge"); + for (i = 0; i < argc; i++) + strbuf_addf(&buf, " %s", argv[i]); + setenv("GIT_REFLOG_ACTION", buf.buf, 0); + strbuf_reset(&buf); + + for (i = 0; i < argc; i++) { + struct object *o; + + o = peel_to_type(argv[i], 0, NULL, OBJ_COMMIT); + if (!o) + die("%s - not something we can merge", argv[i]); + remotes = &commit_list_insert(lookup_commit(o->sha1), + remotes)->next; + + strbuf_addf(&buf, "GITHEAD_%s", sha1_to_hex(o->sha1)); + setenv(buf.buf, argv[i], 1); + strbuf_reset(&buf); + } + + if (!use_strategies.nr) { + if (!remoteheads->next) + add_strategies(pull_twohead, DEFAULT_TWOHEAD); + else + add_strategies(pull_octopus, DEFAULT_OCTOPUS); + } + + for (i = 0; i < use_strategies.nr; i++) { + if ((unsigned int)use_strategies.items[i].util & + NO_FAST_FORWARD) + allow_fast_forward = 0; + if ((unsigned int)use_strategies.items[i].util & NO_TRIVIAL) + allow_trivial = 0; + } + + if (!remoteheads->next) + common = get_merge_bases(lookup_commit(head), + remoteheads->item, 1); + else { + struct commit_list *list = remoteheads; + commit_list_insert(lookup_commit(head), &list); + common = get_octopus_merge_bases(list); + free(list); + } + + update_ref("updating ORIG_HEAD", "ORIG_HEAD", head, NULL, 0, + DIE_ON_ERR); + + if (!common) + ; /* No common ancestors found. We need a real merge. */ + else if (!remoteheads->next && + !hashcmp(common->item->object.sha1, + remoteheads->item->object.sha1)) { + /* + * If head can reach all the remote heads then we are up + * to date. + */ + finish_up_to_date("Already up-to-date."); + return 0; + } else if (allow_fast_forward && !remoteheads->next && + !hashcmp(common->item->object.sha1, head)) { + /* Again the most common case of merging one remote. */ + struct strbuf msg; + struct object *o; + char hex[41]; + + strcpy(hex, find_unique_abbrev(head, DEFAULT_ABBREV)); + + printf("Updating %s..%s\n", + hex, + find_unique_abbrev(remoteheads->item->object.sha1, + DEFAULT_ABBREV)); + refresh_cache(REFRESH_QUIET); + strbuf_initf(&msg, "%s", "Fast forward"); + if (have_message) + strbuf_addstr(&msg, + " (no commit created; -m option ignored)"); + o = peel_to_type(sha1_to_hex(remoteheads->item->object.sha1), + 0, NULL, OBJ_COMMIT); + if (!o) + return 0; + + if (merge_one_remote(head, remoteheads->item->object.sha1)) + return 0; + + finish(o->sha1, msg.buf); + dropsave(); + return 0; + } else if (!remoteheads->next && common->next) + ; + /* + * We are not doing octopus and not fast forward. Need + * a real merge. + */ + else if (!remoteheads->next && option_commit) { + /* + * We are not doing octopus, not fast forward, and have + * only one common. + */ + refresh_cache(REFRESH_QUIET); + if (allow_trivial) { + /* See if it is really trivial. */ + git_committer_info(IDENT_ERROR_ON_NO_NAME); + printf("Trying really trivial in-index merge...\n"); + if (!read_tree_trivial(common->item->object.sha1, + head, remoteheads->item->object.sha1)) { + unsigned char result_tree[20], + result_commit[20]; + struct commit_list parent; + + write_tree_trivial(result_tree); + printf("Wonderful.\n"); + parent.item = remoteheads->item; + parent.next = NULL; + commit_tree_trivial(merge_msg.buf, + result_tree, &parent, + result_commit); + finish(result_commit, "In-index merge"); + dropsave(); + return 0; + } + printf("Nope.\n"); + } + } else { + /* + * An octopus. If we can reach all the remote we are up + * to date. + */ + int up_to_date = 1; + struct commit_list *j; + + for (j = remoteheads; j; j = j->next) { + struct commit_list *common_one; + + /* + * Here we *have* to calculate the individual + * merge_bases again, otherwise "git merge HEAD^ + * HEAD^^" would be missed. + */ + common_one = get_merge_bases(lookup_commit(head), + j->item, 1); + if (hashcmp(common_one->item->object.sha1, + j->item->object.sha1)) { + up_to_date = 0; + break; + } + } + if (up_to_date) { + finish_up_to_date("Already up-to-date. Yeeah!"); + return 0; + } + } + + /* We are going to make a new commit. */ + git_committer_info(IDENT_ERROR_ON_NO_NAME); + + /* + * At this point, we need a real merge. No matter what strategy + * we use, it would operate on the index, possibly affecting the + * working tree, and when resolved cleanly, have the desired + * tree in the index -- this means that the index must be in + * sync with the head commit. The strategies are responsible + * to ensure this. + */ + if (use_strategies.nr != 1) { + /* + * Stash away the local changes so that we can try more + * than one. + */ + save_state(); + single_strategy = 0; + } else { + unlink(git_path("MERGE_STASH")); + single_strategy = 1; + } + + for (i = 0; i < use_strategies.nr; i++) { + int ret; + if (i) { + printf("Rewinding the tree to pristine...\n"); + restore_state(); + } + if (!single_strategy) + printf("Trying merge strategy %s...\n", + use_strategies.items[i].path); + /* + * Remember which strategy left the state in the working + * tree. + */ + wt_strategy = &use_strategies.items[i]; + + ret = try_merge_strategy(use_strategies.items[i].path, + common, &head_arg); + if (!option_commit && !ret) { + merge_was_ok = 1; + /* + * This is necessary here just to avoid writing + * the tree, but later we will *not* exit with + * status code 1 because merge_was_ok is set. + */ + ret = 1; + } + + if (ret) { + /* + * The backend exits with 1 when conflicts are + * left to be resolved, with 2 when it does not + * handle the given merge at all. + */ + if (ret == 1) { + int cnt = 0; + struct rev_info rev; + + if (read_cache() < 0) + die("failed to read the cache"); + + /* Check how many files differ. */ + init_revisions(&rev, ""); + setup_revisions(0, NULL, &rev, NULL); + rev.diffopt.output_format |= + DIFF_FORMAT_CALLBACK; + rev.diffopt.format_callback = count_diff_files; + rev.diffopt.format_callback_data = &cnt; + run_diff_files(&rev, 0); + + /* + * Check how many unmerged entries are + * there. + */ + cnt += count_unmerged_entries(); + + if (best_cnt <= 0 || cnt <= best_cnt) { + best_strategy = + &use_strategies.items[i]; + best_cnt = cnt; + } + } + if (merge_was_ok) + break; + else + continue; + } + + /* Automerge succeeded. */ + write_tree_trivial(result_tree); + automerge_was_ok = 1; + break; + } + + /* + * If we have a resulting tree, that means the strategy module + * auto resolved the merge cleanly. + */ + if (automerge_was_ok) { + struct commit_list *parents = NULL, *j; + unsigned char result_commit[20]; + + free_commit_list(common); + if (allow_fast_forward) { + parents = remoteheads; + commit_list_insert(lookup_commit(head), &parents); + parents = reduce_heads(parents); + } else { + struct commit_list **pptr = &parents; + + pptr = &commit_list_insert(lookup_commit(head), + pptr)->next; + for (j = remoteheads; j; j = j->next) + pptr = &commit_list_insert(j->item, pptr)->next; + } + free_commit_list(remoteheads); + strbuf_addch(&merge_msg, '\n'); + commit_tree_trivial(merge_msg.buf, result_tree, parents, + result_commit); + free_commit_list(parents); + strbuf_addf(&buf, "Merge made by %s.", wt_strategy->path); + finish(result_commit, buf.buf); + strbuf_release(&buf); + dropsave(); + return 0; + } + + /* + * Pick the result from the best strategy and have the user fix + * it up. + */ + if (!best_strategy) { + restore_state(); + if (use_strategies.nr > 1) + fprintf(stderr, + "No merge strategy handled the merge.\n"); + else + fprintf(stderr, "Merge with strategy %s failed.\n", + use_strategies.items[0].path); + return 2; + } else if (best_strategy == wt_strategy) + ; /* We already have its result in the working tree. */ + else { + printf("Rewinding the tree to pristine...\n"); + restore_state(); + printf("Using the %s to prepare resolving by hand.\n", + best_strategy->path); + try_merge_strategy(best_strategy->path, common, &head_arg); + } + + if (squash) + finish(NULL, NULL); + else { + int fd; + struct commit_list *j; + + for (j = remoteheads; j; j = j->next) + strbuf_addf(&buf, "%s\n", + sha1_to_hex(j->item->object.sha1)); + fd = open(git_path("MERGE_HEAD"), O_WRONLY | O_CREAT, 0666); + if (fd < 0) + die("Could open %s for writing", + git_path("MERGE_HEAD")); + if (write_in_full(fd, buf.buf, buf.len) != buf.len) + die("Could not write to %s", git_path("MERGE_HEAD")); + close(fd); + strbuf_addch(&merge_msg, '\n'); + fd = open(git_path("MERGE_MSG"), O_WRONLY | O_CREAT, 0666); + if (fd < 0) + die("Could open %s for writing", git_path("MERGE_MSG")); + if (write_in_full(fd, merge_msg.buf, merge_msg.len) != + merge_msg.len) + die("Could not write to %s", git_path("MERGE_MSG")); + close(fd); + } + + if (merge_was_ok) { + fprintf(stderr, "Automatic merge went well; " + "stopped before committing as requested\n"); + return 0; + } else { + FILE *fp; + int pos; + const char *argv_rerere[] = { "rerere", NULL }; + + fp = fopen(git_path("MERGE_MSG"), "a"); + if (!fp) + die("Could open %s for writing", git_path("MERGE_MSG")); + fprintf(fp, "\nConflicts:\n"); + for (pos = 0; pos < active_nr; pos++) { + struct cache_entry *ce = active_cache[pos]; + + if (ce_stage(ce)) { + fprintf(fp, "\t%s\n", ce->name); + while (pos + 1 < active_nr && + !strcmp(ce->name, + active_cache[pos + 1]->name)) + pos++; + } + } + fclose(fp); + run_command_v_opt(argv_rerere, RUN_GIT_CMD); + printf("Automatic merge failed; " + "fix conflicts and then commit the result.\n"); + return 1; + } +} diff --git a/builtin.h b/builtin.h index 2b01fea..8bf5280 100644 --- a/builtin.h +++ b/builtin.h @@ -60,6 +60,7 @@ extern int cmd_ls_tree(int argc, const char **argv, const char *prefix); extern int cmd_ls_remote(int argc, const char **argv, const char *prefix); extern int cmd_mailinfo(int argc, const char **argv, const char *prefix); extern int cmd_mailsplit(int argc, const char **argv, const char *prefix); +extern int cmd_merge(int argc, const char **argv, const char *prefix); extern int cmd_merge_base(int argc, const char **argv, const char *prefix); extern int cmd_merge_ours(int argc, const char **argv, const char *prefix); extern int cmd_merge_file(int argc, const char **argv, const char *prefix); diff --git a/git-merge.sh b/contrib/examples/git-merge.sh similarity index 100% rename from git-merge.sh rename to contrib/examples/git-merge.sh diff --git a/git.c b/git.c index 2fbe96b..770aadd 100644 --- a/git.c +++ b/git.c @@ -271,6 +271,7 @@ static void handle_internal_command(int argc, const char **argv) { "ls-remote", cmd_ls_remote }, { "mailinfo", cmd_mailinfo }, { "mailsplit", cmd_mailsplit }, + { "merge", cmd_merge, RUN_SETUP | NEED_WORK_TREE }, { "merge-base", cmd_merge_base, RUN_SETUP }, { "merge-file", cmd_merge_file }, { "merge-ours", cmd_merge_ours, RUN_SETUP }, -- 1.5.6 ^ permalink raw reply related [flat|nested] 82+ messages in thread
* Re: [PATCH 11/15] Add strbuf_vaddf(), use it in strbuf_addf(), and add strbuf_initf() 2008-06-27 16:22 ` [PATCH 11/15] Add strbuf_vaddf(), use it in strbuf_addf(), and add strbuf_initf() Miklos Vajna 2008-06-27 16:22 ` [PATCH 12/15] strbuf_vaddf(): support %*s, too Miklos Vajna @ 2008-06-28 2:00 ` Junio C Hamano 2008-06-28 2:33 ` Miklos Vajna 2008-06-28 17:33 ` [PATCH 11/15] Add strbuf_vaddf(), use it in strbuf_addf(), and add strbuf_initf() Johannes Schindelin 1 sibling, 2 replies; 82+ messages in thread From: Junio C Hamano @ 2008-06-28 2:00 UTC (permalink / raw To: Miklos Vajna; +Cc: git, Johannes Schindelin, Olivier Marin Do you really think these two patches belong to the series, I seriously have to wonder? The series uses strbuf_initf() from 7 call sites to save a few lines per each call site, and for that you have a 140-line patch to strbuf.c (in aggregate between this one and the "Oops, I need to support this, too" one after this). This new code that is not essential to the series need to be carefully vetted to avoid risks of potentially affecting existing 70+ users of strbuf_addf(), which used to use (hopefully more trustworthy) vsnprintf() from the system, but now uses the new and unproven strbuf_vaddf() that has "Only supports %<these>" disclaimer at the top. I am not saying the strbuf_vaddf() patch is not worth considering. It might help platforms without a working vsnprintf(). But its merit needs to be defended separately, and I do not want "merge in C" series to be taken hostage by it. Other than that, I did not see anything obviously wrong in the diff between the previous round and this series. Thanks. ^ permalink raw reply [flat|nested] 82+ messages in thread
* Re: [PATCH 11/15] Add strbuf_vaddf(), use it in strbuf_addf(), and add strbuf_initf() 2008-06-28 2:00 ` [PATCH 11/15] Add strbuf_vaddf(), use it in strbuf_addf(), and add strbuf_initf() Junio C Hamano @ 2008-06-28 2:33 ` Miklos Vajna 2008-06-28 2:38 ` [PATCH 13/13] Build in merge Miklos Vajna 2008-06-28 17:33 ` [PATCH 11/15] Add strbuf_vaddf(), use it in strbuf_addf(), and add strbuf_initf() Johannes Schindelin 1 sibling, 1 reply; 82+ messages in thread From: Miklos Vajna @ 2008-06-28 2:33 UTC (permalink / raw To: Junio C Hamano; +Cc: git, Johannes Schindelin, Olivier Marin [-- Attachment #1: Type: text/plain, Size: 572 bytes --] On Fri, Jun 27, 2008 at 07:00:53PM -0700, Junio C Hamano <gitster@pobox.com> wrote: > Do you really think these two patches belong to the series, I seriously > have to wonder? Well, no. I was suggested to include it there, but you have reason about it should be a different series. > Other than that, I did not see anything obviously wrong in the diff > between the previous round and this series. OK, I'll send the main patch without the strbuf_initf() calls, and then you can just fetch from git/vmiklos.git or drop Dscho's two patches from my topic branch. Thanks. [-- Attachment #2: Type: application/pgp-signature, Size: 197 bytes --] ^ permalink raw reply [flat|nested] 82+ messages in thread
* [PATCH 13/13] Build in merge 2008-06-28 2:33 ` Miklos Vajna @ 2008-06-28 2:38 ` Miklos Vajna 2008-06-29 7:46 ` Junio C Hamano 0 siblings, 1 reply; 82+ messages in thread From: Miklos Vajna @ 2008-06-28 2:38 UTC (permalink / raw To: Junio C Hamano; +Cc: git, Johannes Schindelin, Olivier Marin Mentored-by: Johannes Schindelin <Johannes.Schindelin@gmx.de> Signed-off-by: Miklos Vajna <vmiklos@frugalware.org> --- On Sat, Jun 28, 2008 at 04:33:55AM +0200, Miklos Vajna <vmiklos@frugalware.org> wrote: > OK, I'll send the main patch without the strbuf_initf() calls Here it is. Makefile | 2 +- builtin-merge.c | 1148 +++++++++++++++++++++++++ builtin.h | 1 + git-merge.sh => contrib/examples/git-merge.sh | 0 git.c | 1 + 5 files changed, 1151 insertions(+), 1 deletions(-) create mode 100644 builtin-merge.c rename git-merge.sh => contrib/examples/git-merge.sh (100%) diff --git a/Makefile b/Makefile index 3584b8c..dab29b0 100644 --- a/Makefile +++ b/Makefile @@ -240,7 +240,6 @@ SCRIPT_SH += git-lost-found.sh SCRIPT_SH += git-merge-octopus.sh SCRIPT_SH += git-merge-one-file.sh SCRIPT_SH += git-merge-resolve.sh -SCRIPT_SH += git-merge.sh SCRIPT_SH += git-merge-stupid.sh SCRIPT_SH += git-mergetool.sh SCRIPT_SH += git-parse-remote.sh @@ -514,6 +513,7 @@ BUILTIN_OBJS += builtin-ls-remote.o BUILTIN_OBJS += builtin-ls-tree.o BUILTIN_OBJS += builtin-mailinfo.o BUILTIN_OBJS += builtin-mailsplit.o +BUILTIN_OBJS += builtin-merge.o BUILTIN_OBJS += builtin-merge-base.o BUILTIN_OBJS += builtin-merge-file.o BUILTIN_OBJS += builtin-merge-ours.o diff --git a/builtin-merge.c b/builtin-merge.c new file mode 100644 index 0000000..39bfbd1 --- /dev/null +++ b/builtin-merge.c @@ -0,0 +1,1148 @@ +/* + * Builtin "git merge" + * + * Copyright (c) 2008 Miklos Vajna <vmiklos@frugalware.org> + * + * Based on git-merge.sh by Junio C Hamano. + */ + +#include "cache.h" +#include "parse-options.h" +#include "builtin.h" +#include "run-command.h" +#include "path-list.h" +#include "diff.h" +#include "refs.h" +#include "commit.h" +#include "diffcore.h" +#include "revision.h" +#include "unpack-trees.h" +#include "cache-tree.h" +#include "dir.h" +#include "utf8.h" +#include "log-tree.h" +#include "color.h" + +enum strategy { + DEFAULT_TWOHEAD = 1, + DEFAULT_OCTOPUS = 2, + NO_FAST_FORWARD = 4, + NO_TRIVIAL = 8 +}; + +static const char * const builtin_merge_usage[] = { + "git-merge [options] <remote>...", + "git-merge [options] <msg> HEAD <remote>", + NULL +}; + +static int show_diffstat = 1, option_log, squash; +static int option_commit = 1, allow_fast_forward = 1; +static int allow_trivial = 1, have_message; +static struct strbuf merge_msg; +static struct commit_list *remoteheads; +static unsigned char head[20]; +static struct path_list use_strategies; +static const char *branch; + +static struct path_list_item strategy_items[] = { + { "recur", (void *)NO_TRIVIAL }, + { "recursive", (void *)(DEFAULT_TWOHEAD | NO_TRIVIAL) }, + { "octopus", (void *)DEFAULT_OCTOPUS }, + { "resolve", (void *)0 }, + { "stupid", (void *)0 }, + { "ours", (void *)(NO_FAST_FORWARD | NO_TRIVIAL) }, + { "subtree", (void *)(NO_FAST_FORWARD | NO_TRIVIAL) }, +}; +static struct path_list strategies = { strategy_items, + ARRAY_SIZE(strategy_items), 0, 0 }; + +static const char *pull_twohead, *pull_octopus; + +static int option_parse_message(const struct option *opt, + const char *arg, int unset) +{ + struct strbuf *buf = opt->value; + + if (unset) + strbuf_setlen(buf, 0); + else { + strbuf_addf(buf, "%s\n\n", arg); + have_message = 1; + } + return 0; +} + +static struct path_list_item *unsorted_path_list_lookup(const char *path, + struct path_list *list) +{ + int i; + + if (!path) + return NULL; + + for (i = 0; i < list->nr; i++) + if (!strcmp(path, list->items[i].path)) + return &list->items[i]; + return NULL; +} + +static inline void path_list_append_strategy( + struct path_list_item *item) +{ + path_list_append(item->path, &use_strategies)->util = item->util; +} + +static int option_parse_strategy(const struct option *opt, + const char *arg, int unset) +{ + int i; + struct path_list_item *item = + unsorted_path_list_lookup(arg, &strategies); + + if (unset) + return 0; + + if (item) + path_list_append_strategy(item); + else { + struct strbuf err; + strbuf_init(&err, 0); + for (i = 0; i < strategies.nr; i++) + strbuf_addf(&err, " %s", strategies.items[i].path); + fprintf(stderr, "Could not find merge strategy '%s'.\n", arg); + fprintf(stderr, "Available strategies are:%s.\n", err.buf); + exit(1); + } + return 0; +} + +static int option_parse_n(const struct option *opt, + const char *arg, int unset) +{ + show_diffstat = unset; + return 0; +} + +static struct option builtin_merge_options[] = { + { OPTION_CALLBACK, 'n', NULL, NULL, NULL, + "do not show a diffstat at the end of the merge", + PARSE_OPT_NOARG, option_parse_n }, + OPT_BOOLEAN(0, "stat", &show_diffstat, + "show a diffstat at the end of the merge"), + OPT_BOOLEAN(0, "summary", &show_diffstat, "(synonym to --stat)"), + OPT_BOOLEAN(0, "log", &option_log, + "add list of one-line log to merge commit message"), + OPT_BOOLEAN(0, "squash", &squash, + "create a single commit instead of doing a merge"), + OPT_BOOLEAN(0, "commit", &option_commit, + "perform a commit if the merge succeeds (default)"), + OPT_BOOLEAN(0, "ff", &allow_fast_forward, + "allow fast forward (default)"), + OPT_CALLBACK('s', "strategy", &use_strategies, "strategy", + "merge strategy to use", option_parse_strategy), + OPT_CALLBACK('m', "message", &merge_msg, "message", + "message to be used for the merge commit (if any)", + option_parse_message), + OPT_END() +}; + +/* Cleans up metadata that is uninteresting after a succeeded merge. */ +static void dropsave(void) +{ + unlink(git_path("MERGE_HEAD")); + unlink(git_path("MERGE_MSG")); + unlink(git_path("MERGE_STASH")); +} + +static void save_state(void) +{ + int fd; + struct child_process stash; + const char *argv[] = {"stash", "create", NULL}; + + fd = open(git_path("MERGE_STASH"), O_WRONLY | O_CREAT, 0666); + if (fd < 0) + die("Could not write to %s", git_path("MERGE_STASH")); + memset(&stash, 0, sizeof(stash)); + stash.argv = argv; + stash.out = fd; + stash.git_cmd = 1; + run_command(&stash); +} + +static void reset_hard(unsigned const char *sha1, int verbose) +{ + struct tree *tree; + struct unpack_trees_options opts; + struct tree_desc t; + + memset(&opts, 0, sizeof(opts)); + opts.head_idx = -1; + opts.src_index = &the_index; + opts.dst_index = &the_index; + opts.update = 1; + opts.reset = 1; + if (verbose) + opts.verbose_update = 1; + + tree = parse_tree_indirect(sha1); + if (!tree) + die("failed to unpack %s tree object", sha1_to_hex(sha1)); + parse_tree(tree); + init_tree_desc(&t, tree->buffer, tree->size); + if (unpack_trees(1, &t, &opts)) + exit(128); /* We've already reported the error, finish dying */ +} + +static void restore_state(void) +{ + struct strbuf sb; + const char *args[] = { "stash", "apply", NULL, NULL }; + + if (access(git_path("MERGE_STASH"), R_OK) < 0) + return; + + reset_hard(head, 1); + + strbuf_init(&sb, 0); + if (strbuf_read_file(&sb, git_path("MERGE_STASH"), 0) < 0) + die("could not read MERGE_STASH: %s", strerror(errno)); + args[2] = sb.buf; + + /* + * It is OK to ignore error here, for example when there was + * nothing to restore. + */ + run_command_v_opt(args, RUN_GIT_CMD); + + strbuf_release(&sb); + refresh_cache(REFRESH_QUIET); +} + +/* This is called when no merge was necessary. */ +static void finish_up_to_date(const char *msg) +{ + printf("%s%s\n", squash ? " (nothing to squash)" : "", msg); + dropsave(); +} + +static void squash_message(void) +{ + struct rev_info rev; + struct commit *commit; + struct strbuf out; + struct commit_list *j; + int fd; + + printf("Squash commit -- not updating HEAD\n"); + fd = open(git_path("SQUASH_MSG"), O_WRONLY | O_CREAT, 0666); + if (fd < 0) + die("Could not write to %s", git_path("SQUASH_MSG")); + + init_revisions(&rev, NULL); + rev.ignore_merges = 1; + rev.commit_format = CMIT_FMT_MEDIUM; + + commit = lookup_commit(head); + commit->object.flags |= UNINTERESTING; + add_pending_object(&rev, &commit->object, NULL); + + for (j = remoteheads; j; j = j->next) + add_pending_object(&rev, &j->item->object, NULL); + + setup_revisions(0, NULL, &rev, NULL); + if (prepare_revision_walk(&rev)) + die("revision walk setup failed"); + + strbuf_init(&out, 0); + strbuf_addstr(&out, "Squashed commit of the following:\n"); + while ((commit = get_revision(&rev)) != NULL) { + strbuf_addch(&out, '\n'); + strbuf_addf(&out, "commit %s\n", + sha1_to_hex(commit->object.sha1)); + pretty_print_commit(rev.commit_format, commit, &out, rev.abbrev, + NULL, NULL, rev.date_mode, 0); + } + write(fd, out.buf, out.len); + close(fd); + strbuf_release(&out); +} + +static int run_hook(const char *name) +{ + struct child_process hook; + const char *argv[3], *env[2]; + char index[PATH_MAX]; + + argv[0] = git_path("hooks/%s", name); + if (access(argv[0], X_OK) < 0) + return 0; + + snprintf(index, sizeof(index), "GIT_INDEX_FILE=%s", get_index_file()); + env[0] = index; + env[1] = NULL; + + if (squash) + argv[1] = "1"; + else + argv[1] = "0"; + argv[2] = NULL; + + memset(&hook, 0, sizeof(hook)); + hook.argv = argv; + hook.no_stdin = 1; + hook.stdout_to_stderr = 1; + hook.env = env; + + return run_command(&hook); +} + +static void finish(const unsigned char *new_head, const char *msg) +{ + struct strbuf reflog_message; + + strbuf_init(&reflog_message, 0); + if (!msg) + strbuf_addstr(&reflog_message, getenv("GIT_REFLOG_ACTION")); + else { + printf("%s\n", msg); + strbuf_addf(&reflog_message, "%s: %s", + getenv("GIT_REFLOG_ACTION"), msg); + } + if (squash) { + squash_message(); + } else { + if (!merge_msg.len) + printf("No merge message -- not updating HEAD\n"); + else { + const char *argv_gc_auto[] = { "gc", "--auto", NULL }; + update_ref(reflog_message.buf, "HEAD", + new_head, head, 0, + DIE_ON_ERR); + /* + * We ignore errors in 'gc --auto', since the + * user should see them. + */ + run_command_v_opt(argv_gc_auto, RUN_GIT_CMD); + } + } + if (new_head && show_diffstat) { + struct diff_options opts; + diff_setup(&opts); + opts.output_format |= + DIFF_FORMAT_SUMMARY | DIFF_FORMAT_DIFFSTAT; + opts.detect_rename = DIFF_DETECT_RENAME; + if (diff_use_color_default > 0) + DIFF_OPT_SET(&opts, COLOR_DIFF); + diff_tree_sha1(head, new_head, "", &opts); + diffcore_std(&opts); + diff_flush(&opts); + } + + /* Run a post-merge hook */ + run_hook("post-merge"); + + strbuf_release(&reflog_message); +} + +/* Get the name for the merge commit's message. */ +static void merge_name(const char *remote, struct strbuf *msg) +{ + struct object *remote_head; + unsigned char branch_head[20], buf_sha[20]; + struct strbuf buf; + char *ptr; + int len = 0; + + memset(branch_head, 0, sizeof(branch_head)); + remote_head = peel_to_type(remote, 0, NULL, OBJ_COMMIT); + if (!remote_head) + return; + + strbuf_init(&buf, 0); + strbuf_addstr(&buf, "refs/heads/"); + strbuf_addstr(&buf, remote); + get_sha1(buf.buf, branch_head); + + if (!hashcmp(remote_head->sha1, branch_head)) { + strbuf_addf(msg, "%s\t\tbranch '%s' of .\n", + sha1_to_hex(branch_head), remote); + return; + } + /* See if remote matches <name>~<number>, or <name>^ */ + ptr = strrchr(remote, '^'); + if (ptr && ptr[1] == '\0') + len = ptr-remote; + else { + ptr = strrchr(remote, '~'); + if (ptr && ptr[1] != '0' && isdigit(ptr[1])) { + len = ptr-remote; + ptr++; + for (ptr++; *ptr; ptr++) + if (!isdigit(*ptr)) { + len = 0; + break; + } + } + } + if (len) { + struct strbuf truname = STRBUF_INIT; + strbuf_addstr(&truname, remote); + strbuf_setlen(&truname, len); + if (!get_sha1(truname.buf, buf_sha)) { + strbuf_addf(msg, + "%s\t\tbranch '%s' (early part) of .\n", + sha1_to_hex(remote_head->sha1), truname.buf); + return; + } + } + + if (!strcmp(remote, "FETCH_HEAD") && + !access(git_path("FETCH_HEAD"), R_OK)) { + FILE *fp; + struct strbuf line; + char *ptr; + + strbuf_init(&line, 0); + fp = fopen(git_path("FETCH_HEAD"), "r"); + if (!fp) + die("could not open %s for reading: %s", + git_path("FETCH_HEAD"), strerror(errno)); + strbuf_getline(&line, fp, '\n'); + fclose(fp); + ptr = strstr(line.buf, "\tnot-for-merge\t"); + if (ptr) + strbuf_remove(&line, ptr-line.buf+1, 13); + strbuf_addbuf(msg, &line); + strbuf_release(&line); + return; + } + strbuf_addf(msg, "%s\t\tcommit '%s'\n", + sha1_to_hex(remote_head->sha1), remote); +} + +int git_merge_config(const char *k, const char *v, void *cb) +{ + if (branch && !prefixcmp(k, "branch.") && + !prefixcmp(k + 7, branch) && + !strcmp(k + 7 + strlen(branch), ".mergeoptions")) { + const char **argv; + int argc; + char *buf; + + buf = xstrdup(v); + argc = split_cmdline(buf, &argv); + argv = xrealloc(argv, sizeof(*argv) * (argc + 2)); + memmove(argv + 1, argv, sizeof(*argv) * (argc + 1)); + argc++; + parse_options(argc, argv, builtin_merge_options, + builtin_merge_usage, 0); + free(buf); + } + + if (!strcmp(k, "merge.diffstat") || !strcmp(k, "merge.stat")) + show_diffstat = git_config_bool(k, v); + else if (!strcmp(k, "pull.twohead")) + return git_config_string(&pull_twohead, k, v); + else if (!strcmp(k, "pull.octopus")) + return git_config_string(&pull_octopus, k, v); + return 0; +} + +static int read_tree_trivial(unsigned char *common, unsigned char *head, + unsigned char *one) +{ + int i, nr_trees = 0; + struct tree *trees[MAX_UNPACK_TREES]; + struct tree_desc t[MAX_UNPACK_TREES]; + struct unpack_trees_options opts; + + memset(&opts, 0, sizeof(opts)); + opts.head_idx = -1; + opts.src_index = &the_index; + opts.dst_index = &the_index; + opts.update = 1; + opts.verbose_update = 1; + opts.trivial_merges_only = 1; + opts.merge = 1; + trees[nr_trees] = parse_tree_indirect(common); + if (!trees[nr_trees++]) + return -1; + trees[nr_trees] = parse_tree_indirect(head); + if (!trees[nr_trees++]) + return -1; + trees[nr_trees] = parse_tree_indirect(one); + if (!trees[nr_trees++]) + return -1; + opts.fn = threeway_merge; + cache_tree_free(&active_cache_tree); + opts.head_idx = 2; + for (i = 0; i < nr_trees; i++) { + parse_tree(trees[i]); + init_tree_desc(t+i, trees[i]->buffer, trees[i]->size); + } + if (unpack_trees(nr_trees, t, &opts)) + return -1; + return 0; +} + +static int commit_tree_trivial(const char *msg, unsigned const char *tree, + struct commit_list *parents, unsigned char *ret) +{ + struct commit_list *i; + struct strbuf buf; + int encoding_is_utf8; + + /* Not having i18n.commitencoding is the same as having utf-8 */ + encoding_is_utf8 = is_encoding_utf8(git_commit_encoding); + + strbuf_init(&buf, 8192); /* should avoid reallocs for the headers */ + strbuf_addf(&buf, "tree %s\n", sha1_to_hex(tree)); + + for (i = parents; i; i = i->next) + strbuf_addf(&buf, "parent %s\n", + sha1_to_hex(i->item->object.sha1)); + + /* Person/date information */ + strbuf_addf(&buf, "author %s\n", + git_author_info(IDENT_ERROR_ON_NO_NAME)); + strbuf_addf(&buf, "committer %s\n", + git_committer_info(IDENT_ERROR_ON_NO_NAME)); + if (!encoding_is_utf8) + strbuf_addf(&buf, "encoding %s\n", git_commit_encoding); + strbuf_addch(&buf, '\n'); + + /* And add the comment */ + strbuf_addstr(&buf, msg); + + write_sha1_file(buf.buf, buf.len, commit_type, ret); + strbuf_release(&buf); + return *ret; +} + +static void write_tree_trivial(unsigned char *sha1) +{ + if (write_cache_as_tree(sha1, 0, NULL)) + die("git write-tree failed to write a tree"); +} + +static int try_merge_strategy(char *strategy, struct commit_list *common, + struct strbuf *head_arg) +{ + const char **args; + int i = 0, ret; + struct commit_list *j; + struct strbuf buf; + + args = xmalloc((4 + commit_list_count(common) + + commit_list_count(remoteheads)) * sizeof(char *)); + strbuf_init(&buf, 0); + strbuf_addf(&buf, "merge-%s", strategy); + args[i++] = buf.buf; + for (j = common; j; j = j->next) + args[i++] = xstrdup(sha1_to_hex(j->item->object.sha1)); + args[i++] = "--"; + args[i++] = head_arg->buf; + for (j = remoteheads; j; j = j->next) + args[i++] = xstrdup(sha1_to_hex(j->item->object.sha1)); + args[i] = NULL; + ret = run_command_v_opt(args, RUN_GIT_CMD); + strbuf_release(&buf); + i = 1; + for (j = common; j; j = j->next) + free((void *)args[i++]); + i += 2; + for (j = remoteheads; j; j = j->next) + free((void *)args[i++]); + free(args); + return -ret; +} + +static void count_diff_files(struct diff_queue_struct *q, + struct diff_options *opt, void *data) +{ + int *count = data; + + (*count) += q->nr; +} + +static int count_unmerged_entries(void) +{ + const struct index_state *state = &the_index; + int i, ret = 0; + + for (i = 0; i < state->cache_nr; i++) + if (ce_stage(state->cache[i])) + ret++; + + return ret; +} + +static int merge_one_remote(unsigned char *head, unsigned char *remote) +{ + struct tree *trees[MAX_UNPACK_TREES]; + struct unpack_trees_options opts; + struct tree_desc t[MAX_UNPACK_TREES]; + int i, fd, nr_trees = 0; + struct dir_struct dir; + struct lock_file *lock_file = xcalloc(1, sizeof(struct lock_file)); + + if (read_cache_unmerged()) + die("you need to resolve your current index first"); + + fd = hold_locked_index(lock_file, 1); + + memset(&trees, 0, sizeof(trees)); + memset(&opts, 0, sizeof(opts)); + memset(&t, 0, sizeof(t)); + dir.show_ignored = 1; + dir.exclude_per_dir = ".gitignore"; + opts.dir = &dir; + + opts.head_idx = 1; + opts.src_index = &the_index; + opts.dst_index = &the_index; + opts.update = 1; + opts.verbose_update = 1; + opts.merge = 1; + opts.fn = twoway_merge; + + trees[nr_trees] = parse_tree_indirect(head); + if (!trees[nr_trees++]) + return -1; + trees[nr_trees] = parse_tree_indirect(remote); + if (!trees[nr_trees++]) + return -1; + for (i = 0; i < nr_trees; i++) { + parse_tree(trees[i]); + init_tree_desc(t+i, trees[i]->buffer, trees[i]->size); + } + if (unpack_trees(nr_trees, t, &opts)) + return -1; + if (write_cache(fd, active_cache, active_nr) || + commit_locked_index(lock_file)) + die("unable to write new index file"); + return 0; +} + +static void split_merge_strategies(const char *string, struct path_list *list) +{ + char *p, *q, *buf; + + if (!string) + return; + + list->strdup_paths = 1; + buf = xstrdup(string); + q = buf; + for (;;) { + p = strchr(q, ' '); + if (!p) { + path_list_append(q, list); + free(buf); + return; + } else { + *p = '\0'; + path_list_append(q, list); + q = ++p; + } + } +} + +static void add_strategies(const char *string, enum strategy strategy) +{ + struct path_list list; + int i; + + memset(&list, 0, sizeof(list)); + split_merge_strategies(string, &list); + if (list.nr) { + for (i = 0; i < list.nr; i++) { + struct path_list_item *item; + + item = unsorted_path_list_lookup(list.items[i].path, + &strategies); + if (item) + path_list_append_strategy(item); + } + return; + } + for (i = 0; i < strategies.nr; i++) + if ((enum strategy)strategies.items[i].util & strategy) + path_list_append_strategy(&strategies.items[i]); +} + +int cmd_merge(int argc, const char **argv, const char *prefix) +{ + unsigned char sha1[20], result_tree[20]; + struct object *second_token = NULL; + struct strbuf buf, head_arg; + int flag, head_invalid, i, single_strategy; + int best_cnt = -1, merge_was_ok = 0, automerge_was_ok = 0; + struct commit_list *common = NULL; + struct path_list_item *best_strategy = NULL, *wt_strategy = NULL; + struct commit_list **remotes = &remoteheads; + + setup_work_tree(); + if (unmerged_cache()) + die("You are in the middle of a conflicted merge."); + + /* + * Check if we are _not_ on a detached HEAD, i.e. if there is a + * current branch. + */ + branch = resolve_ref("HEAD", sha1, 0, &flag); + if (branch && flag & REF_ISSYMREF) { + const char *ptr = skip_prefix(branch, "refs/heads/"); + if (ptr) + branch = ptr; + } + + git_config(git_merge_config, NULL); + + /* for color.diff and diff.color */ + git_config(git_diff_ui_config, NULL); + + /* for color.ui */ + if (diff_use_color_default == -1) + diff_use_color_default = git_use_color_default; + + argc = parse_options(argc, argv, builtin_merge_options, + builtin_merge_usage, 0); + + if (squash) { + if (!allow_fast_forward) + die("You cannot combine --squash with --no-ff."); + option_commit = 0; + } + + if (!argc) + usage_with_options(builtin_merge_usage, + builtin_merge_options); + + /* + * This could be traditional "merge <msg> HEAD <commit>..." and + * the way we can tell it is to see if the second token is HEAD, + * but some people might have misused the interface and used a + * committish that is the same as HEAD there instead. + * Traditional format never would have "-m" so it is an + * additional safety measure to check for it. + */ + strbuf_init(&buf, 0); + strbuf_init(&head_arg, 0); + if (argc > 1) + second_token = peel_to_type(argv[1], 0, NULL, OBJ_COMMIT); + head_invalid = get_sha1("HEAD", head); + + if (!have_message && second_token && + !hashcmp(second_token->sha1, head)) { + strbuf_addstr(&merge_msg, argv[0]); + strbuf_addstr(&head_arg, argv[1]); + argv += 2; + argc -= 2; + } else if (head_invalid) { + struct object *remote_head; + /* + * If the merged head is a valid one there is no reason + * to forbid "git merge" into a branch yet to be born. + * We do the same for "git pull". + */ + if (argc != 1) + die("Can merge only exactly one commit into " + "empty head"); + remote_head = peel_to_type(argv[0], 0, NULL, OBJ_COMMIT); + if (!remote_head) + die("%s - not something we can merge", argv[0]); + update_ref("initial pull", "HEAD", remote_head->sha1, NULL, 0, + DIE_ON_ERR); + reset_hard(remote_head->sha1, 0); + return 0; + } else { + /* We are invoked directly as the first-class UI. */ + strbuf_addstr(&head_arg, "HEAD"); + /* + * All the rest are the commits being merged; + * prepare the standard merge summary message to + * be appended to the given message. If remote + * is invalid we will die later in the common + * codepath so we discard the error in this + * loop. + */ + struct strbuf msg; + + strbuf_init(&msg, 0); + for (i = 0; i < argc; i++) + merge_name(argv[i], &msg); + fmt_merge_msg(option_log, &msg, &merge_msg); + if (merge_msg.len) + strbuf_setlen(&merge_msg, merge_msg.len-1); + } + + if (head_invalid || !argc) + usage_with_options(builtin_merge_usage, + builtin_merge_options); + + strbuf_addstr(&buf, "merge"); + for (i = 0; i < argc; i++) + strbuf_addf(&buf, " %s", argv[i]); + setenv("GIT_REFLOG_ACTION", buf.buf, 0); + strbuf_reset(&buf); + + for (i = 0; i < argc; i++) { + struct object *o; + + o = peel_to_type(argv[i], 0, NULL, OBJ_COMMIT); + if (!o) + die("%s - not something we can merge", argv[i]); + remotes = &commit_list_insert(lookup_commit(o->sha1), + remotes)->next; + + strbuf_addf(&buf, "GITHEAD_%s", sha1_to_hex(o->sha1)); + setenv(buf.buf, argv[i], 1); + strbuf_reset(&buf); + } + + if (!use_strategies.nr) { + if (!remoteheads->next) + add_strategies(pull_twohead, DEFAULT_TWOHEAD); + else + add_strategies(pull_octopus, DEFAULT_OCTOPUS); + } + + for (i = 0; i < use_strategies.nr; i++) { + if ((unsigned int)use_strategies.items[i].util & + NO_FAST_FORWARD) + allow_fast_forward = 0; + if ((unsigned int)use_strategies.items[i].util & NO_TRIVIAL) + allow_trivial = 0; + } + + if (!remoteheads->next) + common = get_merge_bases(lookup_commit(head), + remoteheads->item, 1); + else { + struct commit_list *list = remoteheads; + commit_list_insert(lookup_commit(head), &list); + common = get_octopus_merge_bases(list); + free(list); + } + + update_ref("updating ORIG_HEAD", "ORIG_HEAD", head, NULL, 0, + DIE_ON_ERR); + + if (!common) + ; /* No common ancestors found. We need a real merge. */ + else if (!remoteheads->next && + !hashcmp(common->item->object.sha1, + remoteheads->item->object.sha1)) { + /* + * If head can reach all the remote heads then we are up + * to date. + */ + finish_up_to_date("Already up-to-date."); + return 0; + } else if (allow_fast_forward && !remoteheads->next && + !hashcmp(common->item->object.sha1, head)) { + /* Again the most common case of merging one remote. */ + struct strbuf msg; + struct object *o; + char hex[41]; + + strcpy(hex, find_unique_abbrev(head, DEFAULT_ABBREV)); + + printf("Updating %s..%s\n", + hex, + find_unique_abbrev(remoteheads->item->object.sha1, + DEFAULT_ABBREV)); + refresh_cache(REFRESH_QUIET); + strbuf_init(&msg, 0); + strbuf_addstr(&msg, "Fast forward"); + if (have_message) + strbuf_addstr(&msg, + " (no commit created; -m option ignored)"); + o = peel_to_type(sha1_to_hex(remoteheads->item->object.sha1), + 0, NULL, OBJ_COMMIT); + if (!o) + return 0; + + if (merge_one_remote(head, remoteheads->item->object.sha1)) + return 0; + + finish(o->sha1, msg.buf); + dropsave(); + return 0; + } else if (!remoteheads->next && common->next) + ; + /* + * We are not doing octopus and not fast forward. Need + * a real merge. + */ + else if (!remoteheads->next && option_commit) { + /* + * We are not doing octopus, not fast forward, and have + * only one common. + */ + refresh_cache(REFRESH_QUIET); + if (allow_trivial) { + /* See if it is really trivial. */ + git_committer_info(IDENT_ERROR_ON_NO_NAME); + printf("Trying really trivial in-index merge...\n"); + if (!read_tree_trivial(common->item->object.sha1, + head, remoteheads->item->object.sha1)) { + unsigned char result_tree[20], + result_commit[20]; + struct commit_list parent; + + write_tree_trivial(result_tree); + printf("Wonderful.\n"); + parent.item = remoteheads->item; + parent.next = NULL; + commit_tree_trivial(merge_msg.buf, + result_tree, &parent, + result_commit); + finish(result_commit, "In-index merge"); + dropsave(); + return 0; + } + printf("Nope.\n"); + } + } else { + /* + * An octopus. If we can reach all the remote we are up + * to date. + */ + int up_to_date = 1; + struct commit_list *j; + + for (j = remoteheads; j; j = j->next) { + struct commit_list *common_one; + + /* + * Here we *have* to calculate the individual + * merge_bases again, otherwise "git merge HEAD^ + * HEAD^^" would be missed. + */ + common_one = get_merge_bases(lookup_commit(head), + j->item, 1); + if (hashcmp(common_one->item->object.sha1, + j->item->object.sha1)) { + up_to_date = 0; + break; + } + } + if (up_to_date) { + finish_up_to_date("Already up-to-date. Yeeah!"); + return 0; + } + } + + /* We are going to make a new commit. */ + git_committer_info(IDENT_ERROR_ON_NO_NAME); + + /* + * At this point, we need a real merge. No matter what strategy + * we use, it would operate on the index, possibly affecting the + * working tree, and when resolved cleanly, have the desired + * tree in the index -- this means that the index must be in + * sync with the head commit. The strategies are responsible + * to ensure this. + */ + if (use_strategies.nr != 1) { + /* + * Stash away the local changes so that we can try more + * than one. + */ + save_state(); + single_strategy = 0; + } else { + unlink(git_path("MERGE_STASH")); + single_strategy = 1; + } + + for (i = 0; i < use_strategies.nr; i++) { + int ret; + if (i) { + printf("Rewinding the tree to pristine...\n"); + restore_state(); + } + if (!single_strategy) + printf("Trying merge strategy %s...\n", + use_strategies.items[i].path); + /* + * Remember which strategy left the state in the working + * tree. + */ + wt_strategy = &use_strategies.items[i]; + + ret = try_merge_strategy(use_strategies.items[i].path, + common, &head_arg); + if (!option_commit && !ret) { + merge_was_ok = 1; + /* + * This is necessary here just to avoid writing + * the tree, but later we will *not* exit with + * status code 1 because merge_was_ok is set. + */ + ret = 1; + } + + if (ret) { + /* + * The backend exits with 1 when conflicts are + * left to be resolved, with 2 when it does not + * handle the given merge at all. + */ + if (ret == 1) { + int cnt = 0; + struct rev_info rev; + + if (read_cache() < 0) + die("failed to read the cache"); + + /* Check how many files differ. */ + init_revisions(&rev, ""); + setup_revisions(0, NULL, &rev, NULL); + rev.diffopt.output_format |= + DIFF_FORMAT_CALLBACK; + rev.diffopt.format_callback = count_diff_files; + rev.diffopt.format_callback_data = &cnt; + run_diff_files(&rev, 0); + + /* + * Check how many unmerged entries are + * there. + */ + cnt += count_unmerged_entries(); + + if (best_cnt <= 0 || cnt <= best_cnt) { + best_strategy = + &use_strategies.items[i]; + best_cnt = cnt; + } + } + if (merge_was_ok) + break; + else + continue; + } + + /* Automerge succeeded. */ + write_tree_trivial(result_tree); + automerge_was_ok = 1; + break; + } + + /* + * If we have a resulting tree, that means the strategy module + * auto resolved the merge cleanly. + */ + if (automerge_was_ok) { + struct commit_list *parents = NULL, *j; + unsigned char result_commit[20]; + + free_commit_list(common); + if (allow_fast_forward) { + parents = remoteheads; + commit_list_insert(lookup_commit(head), &parents); + parents = reduce_heads(parents); + } else { + struct commit_list **pptr = &parents; + + pptr = &commit_list_insert(lookup_commit(head), + pptr)->next; + for (j = remoteheads; j; j = j->next) + pptr = &commit_list_insert(j->item, pptr)->next; + } + free_commit_list(remoteheads); + strbuf_addch(&merge_msg, '\n'); + commit_tree_trivial(merge_msg.buf, result_tree, parents, + result_commit); + free_commit_list(parents); + strbuf_addf(&buf, "Merge made by %s.", wt_strategy->path); + finish(result_commit, buf.buf); + strbuf_release(&buf); + dropsave(); + return 0; + } + + /* + * Pick the result from the best strategy and have the user fix + * it up. + */ + if (!best_strategy) { + restore_state(); + if (use_strategies.nr > 1) + fprintf(stderr, + "No merge strategy handled the merge.\n"); + else + fprintf(stderr, "Merge with strategy %s failed.\n", + use_strategies.items[0].path); + return 2; + } else if (best_strategy == wt_strategy) + ; /* We already have its result in the working tree. */ + else { + printf("Rewinding the tree to pristine...\n"); + restore_state(); + printf("Using the %s to prepare resolving by hand.\n", + best_strategy->path); + try_merge_strategy(best_strategy->path, common, &head_arg); + } + + if (squash) + finish(NULL, NULL); + else { + int fd; + struct commit_list *j; + + for (j = remoteheads; j; j = j->next) + strbuf_addf(&buf, "%s\n", + sha1_to_hex(j->item->object.sha1)); + fd = open(git_path("MERGE_HEAD"), O_WRONLY | O_CREAT, 0666); + if (fd < 0) + die("Could open %s for writing", + git_path("MERGE_HEAD")); + if (write_in_full(fd, buf.buf, buf.len) != buf.len) + die("Could not write to %s", git_path("MERGE_HEAD")); + close(fd); + strbuf_addch(&merge_msg, '\n'); + fd = open(git_path("MERGE_MSG"), O_WRONLY | O_CREAT, 0666); + if (fd < 0) + die("Could open %s for writing", git_path("MERGE_MSG")); + if (write_in_full(fd, merge_msg.buf, merge_msg.len) != + merge_msg.len) + die("Could not write to %s", git_path("MERGE_MSG")); + close(fd); + } + + if (merge_was_ok) { + fprintf(stderr, "Automatic merge went well; " + "stopped before committing as requested\n"); + return 0; + } else { + FILE *fp; + int pos; + const char *argv_rerere[] = { "rerere", NULL }; + + fp = fopen(git_path("MERGE_MSG"), "a"); + if (!fp) + die("Could open %s for writing", git_path("MERGE_MSG")); + fprintf(fp, "\nConflicts:\n"); + for (pos = 0; pos < active_nr; pos++) { + struct cache_entry *ce = active_cache[pos]; + + if (ce_stage(ce)) { + fprintf(fp, "\t%s\n", ce->name); + while (pos + 1 < active_nr && + !strcmp(ce->name, + active_cache[pos + 1]->name)) + pos++; + } + } + fclose(fp); + run_command_v_opt(argv_rerere, RUN_GIT_CMD); + printf("Automatic merge failed; " + "fix conflicts and then commit the result.\n"); + return 1; + } +} diff --git a/builtin.h b/builtin.h index 2b01fea..8bf5280 100644 --- a/builtin.h +++ b/builtin.h @@ -60,6 +60,7 @@ extern int cmd_ls_tree(int argc, const char **argv, const char *prefix); extern int cmd_ls_remote(int argc, const char **argv, const char *prefix); extern int cmd_mailinfo(int argc, const char **argv, const char *prefix); extern int cmd_mailsplit(int argc, const char **argv, const char *prefix); +extern int cmd_merge(int argc, const char **argv, const char *prefix); extern int cmd_merge_base(int argc, const char **argv, const char *prefix); extern int cmd_merge_ours(int argc, const char **argv, const char *prefix); extern int cmd_merge_file(int argc, const char **argv, const char *prefix); diff --git a/git-merge.sh b/contrib/examples/git-merge.sh similarity index 100% rename from git-merge.sh rename to contrib/examples/git-merge.sh diff --git a/git.c b/git.c index 2fbe96b..770aadd 100644 --- a/git.c +++ b/git.c @@ -271,6 +271,7 @@ static void handle_internal_command(int argc, const char **argv) { "ls-remote", cmd_ls_remote }, { "mailinfo", cmd_mailinfo }, { "mailsplit", cmd_mailsplit }, + { "merge", cmd_merge, RUN_SETUP | NEED_WORK_TREE }, { "merge-base", cmd_merge_base, RUN_SETUP }, { "merge-file", cmd_merge_file }, { "merge-ours", cmd_merge_ours, RUN_SETUP }, -- 1.5.6 ^ permalink raw reply related [flat|nested] 82+ messages in thread
* Re: [PATCH 13/13] Build in merge 2008-06-28 2:38 ` [PATCH 13/13] Build in merge Miklos Vajna @ 2008-06-29 7:46 ` Junio C Hamano 2008-06-29 8:11 ` Jakub Narebski 2008-06-30 1:36 ` Miklos Vajna 0 siblings, 2 replies; 82+ messages in thread From: Junio C Hamano @ 2008-06-29 7:46 UTC (permalink / raw To: Miklos Vajna; +Cc: git, Johannes Schindelin, Olivier Marin Miklos Vajna <vmiklos@frugalware.org> writes: > Mentored-by: Johannes Schindelin <Johannes.Schindelin@gmx.de> > Signed-off-by: Miklos Vajna <vmiklos@frugalware.org> > --- > > On Sat, Jun 28, 2008 at 04:33:55AM +0200, Miklos Vajna <vmiklos@frugalware.org> wrote: >> OK, I'll send the main patch without the strbuf_initf() calls > > Here it is. Sorry, this file is huge and very time consuming to review. Here are some pieces. The parts I did not comment I haven't looked at. > +/* Get the name for the merge commit's message. */ > +static void merge_name(const char *remote, struct strbuf *msg) > +{ > + struct object *remote_head; > + unsigned char branch_head[20], buf_sha[20]; > + struct strbuf buf; > + char *ptr; > + int len = 0; > + > + memset(branch_head, 0, sizeof(branch_head)); > + remote_head = peel_to_type(remote, 0, NULL, OBJ_COMMIT); > + if (!remote_head) > + return; Hmm. This is a faithful translation of scripted version, but I wonder what should happen when we got a non-commit here... > + > + strbuf_init(&buf, 0); > + strbuf_addstr(&buf, "refs/heads/"); > + strbuf_addstr(&buf, remote); > + get_sha1(buf.buf, branch_head); This does not correspond to the computation of $bh in the scripted version that makes sure "remote" is actually a bare name of branch, e.g. "master", without any adornment like "master~5^3~8. Your code would succeed and leave the same object name in branch_head[] as remote_head->sha1, wouldn't it? > + if (!hashcmp(remote_head->sha1, branch_head)) { > + strbuf_addf(msg, "%s\t\tbranch '%s' of .\n", > + sha1_to_hex(branch_head), remote); > + return; > + } > + /* See if remote matches <name>~<number>, or <name>^ */ The scripted version did not handle <name>^, so this is an extension. Don't you want also handle <name>^^^ if we are extending it? > + ptr = strrchr(remote, '^'); > + if (ptr && ptr[1] == '\0') > + len = ptr-remote; > + else { > + ptr = strrchr(remote, '~'); > + if (ptr && ptr[1] != '0' && isdigit(ptr[1])) { > + len = ptr-remote; > + ptr++; > + for (ptr++; *ptr; ptr++) > + if (!isdigit(*ptr)) { > + len = 0; > + break; > + } > + } > + } > + if (len) { > + struct strbuf truname = STRBUF_INIT; > + strbuf_addstr(&truname, remote); > + strbuf_setlen(&truname, len); > + if (!get_sha1(truname.buf, buf_sha)) { Again, isn't this wrong? You are not making sure truname is the name of existing local branch. HEAD@{7}~23 will pass get_sha1() but you are not merging an early part of HEAD@{7} branch. > + strbuf_addf(msg, > + "%s\t\tbranch '%s' (early part) of .\n", > + sha1_to_hex(remote_head->sha1), truname.buf); > + return; > + } > + } > +int cmd_merge(int argc, const char **argv, const char *prefix) > +{ This is an ultra-huge function. I wonder if it can further split up to make it easier to maintain. > + unsigned char sha1[20], result_tree[20]; > + struct object *second_token = NULL; > + struct strbuf buf, head_arg; > + int flag, head_invalid, i, single_strategy; > + int best_cnt = -1, merge_was_ok = 0, automerge_was_ok = 0; > + struct commit_list *common = NULL; > + struct path_list_item *best_strategy = NULL, *wt_strategy = NULL; > + struct commit_list **remotes = &remoteheads; > + > + setup_work_tree(); > + if (unmerged_cache()) > + die("You are in the middle of a conflicted merge."); > + > + /* > + * Check if we are _not_ on a detached HEAD, i.e. if there is a > + * current branch. > + */ > + branch = resolve_ref("HEAD", sha1, 0, &flag); You are resolving for non-reading, so before an initial commit, you won't get any error from this call, which is good. And sha1[] will be cleared. Otherwise you learn where the HEAD is in sha1[] at this point. > + if (branch && flag & REF_ISSYMREF) { > + const char *ptr = skip_prefix(branch, "refs/heads/"); > + if (ptr) > + branch = ptr; > + } > + > + git_config(git_merge_config, NULL); > + > + /* for color.diff and diff.color */ > + git_config(git_diff_ui_config, NULL); > + > + /* for color.ui */ > + if (diff_use_color_default == -1) > + diff_use_color_default = git_use_color_default; > + > + argc = parse_options(argc, argv, builtin_merge_options, > + builtin_merge_usage, 0); > + > + if (squash) { > + if (!allow_fast_forward) > + die("You cannot combine --squash with --no-ff."); > + option_commit = 0; > + } > + > + if (!argc) > + usage_with_options(builtin_merge_usage, > + builtin_merge_options); > + > + /* > + * This could be traditional "merge <msg> HEAD <commit>..." and > + * the way we can tell it is to see if the second token is HEAD, > + * but some people might have misused the interface and used a > + * committish that is the same as HEAD there instead. > + * Traditional format never would have "-m" so it is an > + * additional safety measure to check for it. > + */ > + strbuf_init(&buf, 0); > + strbuf_init(&head_arg, 0); > + if (argc > 1) > + second_token = peel_to_type(argv[1], 0, NULL, OBJ_COMMIT); If the second token was a string that could resolve to an object name that does not peel to commit (say "merge -m 'HEAD^{tree}' other"), you will get a complaint fro mpeel-to-type "I expected a commit but you gave something else". You (or more likely Dscho) might have said "that won't matter in practice", but I think you should really do get_sha1() followed by lookup_commit_reference_gently() here to avoid the errors. > + head_invalid = get_sha1("HEAD", head); You've already done this earlier with resolve_ref() haven't you? > + if (!have_message && second_token && > + !hashcmp(second_token->sha1, head)) { Isn't this wrong if head_invalid is true? > + strbuf_addstr(&merge_msg, argv[0]); > + strbuf_addstr(&head_arg, argv[1]); > + argv += 2; > + argc -= 2; I do not think there is any point using strbuf for head_arg. Shouldn't it simply be a "const char *"? > + } else if (head_invalid) { > + struct object *remote_head; > + /* > + * If the merged head is a valid one there is no reason > + * to forbid "git merge" into a branch yet to be born. > + * We do the same for "git pull". > + */ > + if (argc != 1) > + die("Can merge only exactly one commit into " > + "empty head"); > + remote_head = peel_to_type(argv[0], 0, NULL, OBJ_COMMIT); Here we _do_ expect it to be a commit, so peel_to_type() is fine. > + if (!remote_head) > + die("%s - not something we can merge", argv[0]); > + update_ref("initial pull", "HEAD", remote_head->sha1, NULL, 0, > + DIE_ON_ERR); > + reset_hard(remote_head->sha1, 0); > + return 0; Makes one wonder reset_hard() (aka "read-tree --reset -u HEAD") ever fail and return here (iow, without calling die()). The answer is luckily no in this case, but it is somewhat unnerving to reviewers. > + } else { > + /* We are invoked directly as the first-class UI. */ > + strbuf_addstr(&head_arg, "HEAD"); > + /* > + * All the rest are the commits being merged; > + * prepare the standard merge summary message to > + * be appended to the given message. If remote > + * is invalid we will die later in the common > + * codepath so we discard the error in this > + * loop. > + */ > + struct strbuf msg; Decl-after-statement. > + strbuf_init(&msg, 0); > + for (i = 0; i < argc; i++) > + merge_name(argv[i], &msg); > + fmt_merge_msg(option_log, &msg, &merge_msg); > + if (merge_msg.len) > + strbuf_setlen(&merge_msg, merge_msg.len-1); > + } > + > + if (head_invalid || !argc) > + usage_with_options(builtin_merge_usage, > + builtin_merge_options); > + strbuf_addstr(&buf, "merge"); > + for (i = 0; i < argc; i++) > + strbuf_addf(&buf, " %s", argv[i]); > + setenv("GIT_REFLOG_ACTION", buf.buf, 0); > + strbuf_reset(&buf); > + > + for (i = 0; i < argc; i++) { > + struct object *o; > + > + o = peel_to_type(argv[i], 0, NULL, OBJ_COMMIT); > + if (!o) > + die("%s - not something we can merge", argv[i]); > + remotes = &commit_list_insert(lookup_commit(o->sha1), > + remotes)->next; > + > + strbuf_addf(&buf, "GITHEAD_%s", sha1_to_hex(o->sha1)); > + setenv(buf.buf, argv[i], 1); > + strbuf_reset(&buf); > + } > + > + if (!use_strategies.nr) { > + if (!remoteheads->next) > + add_strategies(pull_twohead, DEFAULT_TWOHEAD); > + else > + add_strategies(pull_octopus, DEFAULT_OCTOPUS); > + } > + > + for (i = 0; i < use_strategies.nr; i++) { > + if ((unsigned int)use_strategies.items[i].util & > + NO_FAST_FORWARD) > + allow_fast_forward = 0; > + if ((unsigned int)use_strategies.items[i].util & NO_TRIVIAL) > + allow_trivial = 0; Can we abstract out these ugly casts? Any code that use path_list to store anything but list of paths (i.e. some value keyed with string) tends to have this readability issue. > + } > + > + if (!remoteheads->next) > + common = get_merge_bases(lookup_commit(head), > + remoteheads->item, 1); > + else { > + struct commit_list *list = remoteheads; > + commit_list_insert(lookup_commit(head), &list); > + common = get_octopus_merge_bases(list); > + free(list); > + } > + > + update_ref("updating ORIG_HEAD", "ORIG_HEAD", head, NULL, 0, > + DIE_ON_ERR); > + > + if (!common) > + ; /* No common ancestors found. We need a real merge. */ > + else if (!remoteheads->next && > + !hashcmp(common->item->object.sha1, > + remoteheads->item->object.sha1)) { Wouldn't the latter be "common->item == remoteheads->item" simply? You do not have the check to make sure there is only one common ancestor (scripted version compares $common and $1 textually to achieve this), and checking only the first one of them. Is this correct? > + /* > + * If head can reach all the remote heads then we are up > + * to date. > + */ The comment is wrong --- you are doing "... but first the most common case of merging one remote" here. > + finish_up_to_date("Already up-to-date."); > + return 0; > + } else if (allow_fast_forward && !remoteheads->next && > + !hashcmp(common->item->object.sha1, head)) { > + /* Again the most common case of merging one remote. */ Here again you are not checking there is only one common, and checking only the first one of them. > + struct strbuf msg; > + struct object *o; > + char hex[41]; > + > + strcpy(hex, find_unique_abbrev(head, DEFAULT_ABBREV)); > + > + printf("Updating %s..%s\n", > + hex, > + find_unique_abbrev(remoteheads->item->object.sha1, > + DEFAULT_ABBREV)); > + refresh_cache(REFRESH_QUIET); > + strbuf_init(&msg, 0); > + strbuf_addstr(&msg, "Fast forward"); > + if (have_message) > + strbuf_addstr(&msg, > + " (no commit created; -m option ignored)"); > + o = peel_to_type(sha1_to_hex(remoteheads->item->object.sha1), > + 0, NULL, OBJ_COMMIT); > + if (!o) > + return 0; > + > + if (merge_one_remote(head, remoteheads->item->object.sha1)) > + return 0; Isn't "merge_one_remote()" just a "git checkout" after fast-forward? The function feels misnamed. > + finish(o->sha1, msg.buf); > + dropsave(); > + return 0; > + } else if (!remoteheads->next && common->next) > + ; Here you are checking common->next but earlier if/elseif chain didn't so it is too late. > + /* > + * We are not doing octopus and not fast forward. Need > + * a real merge. > + */ > + else if (!remoteheads->next && option_commit) { > + /* > + * We are not doing octopus, not fast forward, and have > + * only one common. Here again you did not check "have only one common" did you? > + */ > + refresh_cache(REFRESH_QUIET); > + if (allow_trivial) { > + /* See if it is really trivial. */ > + git_committer_info(IDENT_ERROR_ON_NO_NAME); > + printf("Trying really trivial in-index merge...\n"); > + if (!read_tree_trivial(common->item->object.sha1, > + head, remoteheads->item->object.sha1)) { > + unsigned char result_tree[20], > + result_commit[20]; > + struct commit_list parent; > + > + write_tree_trivial(result_tree); > + printf("Wonderful.\n"); > + parent.item = remoteheads->item; > + parent.next = NULL; > + commit_tree_trivial(merge_msg.buf, > + result_tree, &parent, > + result_commit); > + finish(result_commit, "In-index merge"); > + dropsave(); > + return 0; > + } > + printf("Nope.\n"); > + } There weren't any good way to squelch error messages selectively from the trivial one in the scripted version and that is the only reason we surround read-tree with "Trying..." and "Wonderful/Nope.". Literal translation to make sure you get identical output in the first round of this series is good, but after the code stabilizes, we may want to squelch these messages. Something to keep in mind but not now. ^ permalink raw reply [flat|nested] 82+ messages in thread
* Re: [PATCH 13/13] Build in merge 2008-06-29 7:46 ` Junio C Hamano @ 2008-06-29 8:11 ` Jakub Narebski 2008-06-30 1:36 ` Miklos Vajna 1 sibling, 0 replies; 82+ messages in thread From: Jakub Narebski @ 2008-06-29 8:11 UTC (permalink / raw To: git Junio C Hamano wrote: > Miklos Vajna <vmiklos@frugalware.org> writes: >> + if (!remote_head) >> + die("%s - not something we can merge", argv[0]); >> + update_ref("initial pull", "HEAD", remote_head->sha1, NULL, 0, >> + DIE_ON_ERR); >> + reset_hard(remote_head->sha1, 0); >> + return 0; > > Makes one wonder reset_hard() (aka "read-tree --reset -u HEAD") ever fail > and return here (iow, without calling die()). The answer is luckily no > in this case, but it is somewhat unnerving to reviewers. Do we have some guidelines on how to mark such non-returning calls, or how to mark unreachable code (splint, formerly lclint, uses for this /*@ unreachable @*/ annotation)? -- Jakub Narebski Warsaw, Poland ShadeHawk on #git ^ permalink raw reply [flat|nested] 82+ messages in thread
* Re: [PATCH 13/13] Build in merge 2008-06-29 7:46 ` Junio C Hamano 2008-06-29 8:11 ` Jakub Narebski @ 2008-06-30 1:36 ` Miklos Vajna 2008-06-30 1:39 ` Miklos Vajna 2008-06-30 5:40 ` Junio C Hamano 1 sibling, 2 replies; 82+ messages in thread From: Miklos Vajna @ 2008-06-30 1:36 UTC (permalink / raw To: Junio C Hamano; +Cc: git, Johannes Schindelin, Olivier Marin [-- Attachment #1: Type: text/plain, Size: 11348 bytes --] On Sun, Jun 29, 2008 at 12:46:09AM -0700, Junio C Hamano <gitster@pobox.com> wrote: > > +/* Get the name for the merge commit's message. */ > > +static void merge_name(const char *remote, struct strbuf *msg) > > +{ > > + struct object *remote_head; > > + unsigned char branch_head[20], buf_sha[20]; > > + struct strbuf buf; > > + char *ptr; > > + int len = 0; > > + > > + memset(branch_head, 0, sizeof(branch_head)); > > + remote_head = peel_to_type(remote, 0, NULL, OBJ_COMMIT); > > + if (!remote_head) > > + return; > > Hmm. This is a faithful translation of scripted version, but I wonder > what should happen when we got a non-commit here... Hm, I think we do not consider that case normal, at least git fsck (since commit 6232f62) checks for it. I replaced the silent return with a die(). > > + > > + strbuf_init(&buf, 0); > > + strbuf_addstr(&buf, "refs/heads/"); > > + strbuf_addstr(&buf, remote); > > + get_sha1(buf.buf, branch_head); > > This does not correspond to the computation of $bh in the scripted version > that makes sure "remote" is actually a bare name of branch, e.g. "master", > without any adornment like "master~5^3~8. Your code would succeed and > leave the same object name in branch_head[] as remote_head->sha1, wouldn't > it? I replaced it with a dwim_ref() call, to achieve the same behaviour. > > + if (!hashcmp(remote_head->sha1, branch_head)) { > > + strbuf_addf(msg, "%s\t\tbranch '%s' of .\n", > > + sha1_to_hex(branch_head), remote); > > + return; > > + } > > + /* See if remote matches <name>~<number>, or <name>^ */ > > The scripted version did not handle <name>^, so this is an extension. > Don't you want also handle <name>^^^ if we are extending it? I did so, now it accepts <name>^, <name>^^, <name>^^^, etc. > > + ptr = strrchr(remote, '^'); > > + if (ptr && ptr[1] == '\0') > > + len = ptr-remote; > > + else { > > + ptr = strrchr(remote, '~'); > > + if (ptr && ptr[1] != '0' && isdigit(ptr[1])) { > > + len = ptr-remote; > > + ptr++; > > + for (ptr++; *ptr; ptr++) > > + if (!isdigit(*ptr)) { > > + len = 0; > > + break; > > + } > > + } > > + } > > + if (len) { > > + struct strbuf truname = STRBUF_INIT; > > + strbuf_addstr(&truname, remote); > > + strbuf_setlen(&truname, len); > > + if (!get_sha1(truname.buf, buf_sha)) { > > Again, isn't this wrong? You are not making sure truname is the name of > existing local branch. HEAD@{7}~23 will pass get_sha1() but you are not > merging an early part of HEAD@{7} branch. Now I'm using dwim_ref() here as well. > > + strbuf_addf(msg, > > + "%s\t\tbranch '%s' (early part) of .\n", > > + sha1_to_hex(remote_head->sha1), truname.buf); > > + return; > > + } > > + } > > > +int cmd_merge(int argc, const char **argv, const char *prefix) > > +{ > > This is an ultra-huge function. I wonder if it can further split up to > make it easier to maintain. Yes, just like the scripted version. Hm no, that was even longer. (I mean I already introduced a lot of static functions to make the C equivalent of the "main" part of git-merge.sh shorter.) OK, it was 474 lines long today, but now I introduced 3 new static functions and that made it "only" 410 lines long. > > + /* > > + * This could be traditional "merge <msg> HEAD <commit>..." and > > + * the way we can tell it is to see if the second token is HEAD, > > + * but some people might have misused the interface and used a > > + * committish that is the same as HEAD there instead. > > + * Traditional format never would have "-m" so it is an > > + * additional safety measure to check for it. > > + */ > > + strbuf_init(&buf, 0); > > + strbuf_init(&head_arg, 0); > > + if (argc > 1) > > + second_token = peel_to_type(argv[1], 0, NULL, OBJ_COMMIT); > > If the second token was a string that could resolve to an object name that > does not peel to commit (say "merge -m 'HEAD^{tree}' other"), you will get > a complaint fro mpeel-to-type "I expected a commit but you gave something > else". You (or more likely Dscho) might have said "that won't matter in > practice", but I think you should really do get_sha1() followed by > lookup_commit_reference_gently() here to avoid the errors. Fixed. > > > + head_invalid = get_sha1("HEAD", head); > > You've already done this earlier with resolve_ref() haven't you? Ah yes. I had the global 'head' and the local 'sha1' variable for the same purpose, now I got rid of the local 'sha1' variable in cmd_merge() so resolve_ref() writes now to the 'head' variable and then this line is not necessary, as I can write head_invalid right after resolve_ref(). > > > + if (!have_message && second_token && > > + !hashcmp(second_token->sha1, head)) { > > Isn't this wrong if head_invalid is true? if head_invalid is true, then 'head' will be filled with 0s, hashcmp() will never return 0 so the condition will be never true. That's what the shell version: head_commit=$(git rev-parse --verify "HEAD" 2>/dev/null) does as well. > > + strbuf_addstr(&merge_msg, argv[0]); > > + strbuf_addstr(&head_arg, argv[1]); > > + argv += 2; > > + argc -= 2; > > I do not think there is any point using strbuf for head_arg. Shouldn't it > simply be a "const char *"? Now it is. > > + if (!remote_head) > > + die("%s - not something we can merge", argv[0]); > > + update_ref("initial pull", "HEAD", remote_head->sha1, NULL, 0, > > + DIE_ON_ERR); > > + reset_hard(remote_head->sha1, 0); > > + return 0; > > Makes one wonder reset_hard() (aka "read-tree --reset -u HEAD") ever fail > and return here (iow, without calling die()). The answer is luckily no > in this case, but it is somewhat unnerving to reviewers. Actually reset_hard does not return if an error occures: if (unpack_trees(1, &t, &opts)) exit(128); /* We've already reported the error, finish dying */ That's exactly how we already have it in builtin-commit. > > > + } else { > > + /* We are invoked directly as the first-class UI. */ > > + strbuf_addstr(&head_arg, "HEAD"); > > + /* > > + * All the rest are the commits being merged; > > + * prepare the standard merge summary message to > > + * be appended to the given message. If remote > > + * is invalid we will die later in the common > > + * codepath so we discard the error in this > > + * loop. > > + */ > > + struct strbuf msg; > > Decl-after-statement. You already fixed this. :-) > > + for (i = 0; i < use_strategies.nr; i++) { > > + if ((unsigned int)use_strategies.items[i].util & > > + NO_FAST_FORWARD) > > + allow_fast_forward = 0; > > + if ((unsigned int)use_strategies.items[i].util & NO_TRIVIAL) > > + allow_trivial = 0; > > Can we abstract out these ugly casts? Any code that use path_list to > store anything but list of paths (i.e. some value keyed with string) tends > to have this readability issue. If you don't cast, you can't use the & operator. If I change the path_list_item's util to be an unsigned number then I break fast-export. I think if we _really_ want to get rid of those casts, we could have something like: diff --git a/path-list.h b/path-list.h index ca2cbba..1f57e81 100644 --- a/path-list.h +++ b/path-list.h @@ -4,6 +4,7 @@ struct path_list_item { char *path; void *util; + unsigned int flags; }; struct path_list { But I'm not sure if that's a good idea. Also, fast-export will still have casts after such a change. > > + if (!common) > > + ; /* No common ancestors found. We need a real merge. */ > > + else if (!remoteheads->next && > > + !hashcmp(common->item->object.sha1, > > + remoteheads->item->object.sha1)) { > > Wouldn't the latter be "common->item == remoteheads->item" simply? Right, changed. > You do not have the check to make sure there is only one common ancestor > (scripted version compares $common and $1 textually to achieve this), and > checking only the first one of them. Is this correct? Yes. I changed it to: else if (!remoteheads->next && !common->next && common->item == remoteheads->item) { And now I have the check. > > > + /* > > + * If head can reach all the remote heads then we are up > > + * to date. > > + */ > > The comment is wrong --- you are doing "... but first the most common case > of merging one remote" here. I changed it to match the shell version: /* * If head can reach all the merge then we are up to * date. * but first the most common case of merging one remote. */ > > + finish_up_to_date("Already up-to-date."); > > + return 0; > > + } else if (allow_fast_forward && !remoteheads->next && > > + !hashcmp(common->item->object.sha1, head)) { > > + /* Again the most common case of merging one remote. */ > > Here again you are not checking there is only one common, and checking > only the first one of them. Changed to: } else if (allow_fast_forward && !remoteheads->next && !common->next && !hashcmp(common->item->object.sha1, head)) { Which should add the proper check. > > + if (merge_one_remote(head, remoteheads->item->object.sha1)) > > + return 0; > > Isn't "merge_one_remote()" just a "git checkout" after fast-forward? The > function feels misnamed. Thanks. I'm terribly bad at naming. Renamed to checkout_fast_forward(). > > + finish(o->sha1, msg.buf); > > + dropsave(); > > + return 0; > > + } else if (!remoteheads->next && common->next) > > + ; > > Here you are checking common->next but earlier if/elseif chain didn't so > it is too late. Now, that I do, I think the condition is OK. > > + else if (!remoteheads->next && option_commit) { > > + /* > > + * We are not doing octopus, not fast forward, and have > > + * only one common. > > Here again you did not check "have only one common" did you? Actually the shell version did not check here, either, but yes, I would have to. Now I do. > > + printf("Trying really trivial in-index merge...\n"); > > + if (!read_tree_trivial(common->item->object.sha1, > > + head, remoteheads->item->object.sha1)) { > > + unsigned char result_tree[20], > > + result_commit[20]; > > + struct commit_list parent; > > + > > + write_tree_trivial(result_tree); > > + printf("Wonderful.\n"); > > + parent.item = remoteheads->item; > > + parent.next = NULL; > > + commit_tree_trivial(merge_msg.buf, > > + result_tree, &parent, > > + result_commit); > > + finish(result_commit, "In-index merge"); > > + dropsave(); > > + return 0; > > + } > > + printf("Nope.\n"); > > + } > > There weren't any good way to squelch error messages selectively from the > trivial one in the scripted version and that is the only reason we > surround read-tree with "Trying..." and "Wonderful/Nope.". Literal > translation to make sure you get identical output in the first round of > this series is good, but after the code stabilizes, we may want to squelch > these messages. Something to keep in mind but not now. OK. [-- Attachment #2: Type: application/pgp-signature, Size: 197 bytes --] ^ permalink raw reply related [flat|nested] 82+ messages in thread
* [PATCH 13/13] Build in merge 2008-06-30 1:36 ` Miklos Vajna @ 2008-06-30 1:39 ` Miklos Vajna 2008-06-30 5:44 ` Junio C Hamano 2008-06-30 22:44 ` [PATCH 13/13] " Olivier Marin 2008-06-30 5:40 ` Junio C Hamano 1 sibling, 2 replies; 82+ messages in thread From: Miklos Vajna @ 2008-06-30 1:39 UTC (permalink / raw To: Junio C Hamano; +Cc: git, Johannes Schindelin, Olivier Marin Mentored-by: Johannes Schindelin <Johannes.Schindelin@gmx.de> Signed-off-by: Miklos Vajna <vmiklos@frugalware.org> --- Interdiff since the previous version: git diff 9824cf3..8611c7b Individual commits: git log 8611c7b Makefile | 2 +- builtin-merge.c | 1173 +++++++++++++++++++++++++ builtin.h | 1 + git-merge.sh => contrib/examples/git-merge.sh | 0 git.c | 1 + t/t7602-merge-octopus-many.sh | 2 +- 6 files changed, 1177 insertions(+), 2 deletions(-) create mode 100644 builtin-merge.c rename git-merge.sh => contrib/examples/git-merge.sh (100%) diff --git a/Makefile b/Makefile index b003e3e..3d8c3d2 100644 --- a/Makefile +++ b/Makefile @@ -240,7 +240,6 @@ SCRIPT_SH += git-lost-found.sh SCRIPT_SH += git-merge-octopus.sh SCRIPT_SH += git-merge-one-file.sh SCRIPT_SH += git-merge-resolve.sh -SCRIPT_SH += git-merge.sh SCRIPT_SH += git-merge-stupid.sh SCRIPT_SH += git-mergetool.sh SCRIPT_SH += git-parse-remote.sh @@ -511,6 +510,7 @@ BUILTIN_OBJS += builtin-ls-remote.o BUILTIN_OBJS += builtin-ls-tree.o BUILTIN_OBJS += builtin-mailinfo.o BUILTIN_OBJS += builtin-mailsplit.o +BUILTIN_OBJS += builtin-merge.o BUILTIN_OBJS += builtin-merge-base.o BUILTIN_OBJS += builtin-merge-file.o BUILTIN_OBJS += builtin-merge-ours.o diff --git a/builtin-merge.c b/builtin-merge.c new file mode 100644 index 0000000..187038c --- /dev/null +++ b/builtin-merge.c @@ -0,0 +1,1173 @@ +/* + * Builtin "git merge" + * + * Copyright (c) 2008 Miklos Vajna <vmiklos@frugalware.org> + * + * Based on git-merge.sh by Junio C Hamano. + */ + +#include "cache.h" +#include "parse-options.h" +#include "builtin.h" +#include "run-command.h" +#include "path-list.h" +#include "diff.h" +#include "refs.h" +#include "commit.h" +#include "diffcore.h" +#include "revision.h" +#include "unpack-trees.h" +#include "cache-tree.h" +#include "dir.h" +#include "utf8.h" +#include "log-tree.h" +#include "color.h" + +enum strategy { + DEFAULT_TWOHEAD = 1, + DEFAULT_OCTOPUS = 2, + NO_FAST_FORWARD = 4, + NO_TRIVIAL = 8 +}; + +static const char * const builtin_merge_usage[] = { + "git-merge [options] <remote>...", + "git-merge [options] <msg> HEAD <remote>", + NULL +}; + +static int show_diffstat = 1, option_log, squash; +static int option_commit = 1, allow_fast_forward = 1; +static int allow_trivial = 1, have_message; +static struct strbuf merge_msg; +static struct commit_list *remoteheads; +static unsigned char head[20]; +static struct path_list use_strategies; +static const char *branch; + +static struct path_list_item strategy_items[] = { + { "recur", (void *)NO_TRIVIAL }, + { "recursive", (void *)(DEFAULT_TWOHEAD | NO_TRIVIAL) }, + { "octopus", (void *)DEFAULT_OCTOPUS }, + { "resolve", (void *)0 }, + { "stupid", (void *)0 }, + { "ours", (void *)(NO_FAST_FORWARD | NO_TRIVIAL) }, + { "subtree", (void *)(NO_FAST_FORWARD | NO_TRIVIAL) }, +}; +static struct path_list strategies = { strategy_items, + ARRAY_SIZE(strategy_items), 0, 0 }; + +static const char *pull_twohead, *pull_octopus; + +static int option_parse_message(const struct option *opt, + const char *arg, int unset) +{ + struct strbuf *buf = opt->value; + + if (unset) + strbuf_setlen(buf, 0); + else { + strbuf_addf(buf, "%s\n\n", arg); + have_message = 1; + } + return 0; +} + +static struct path_list_item *unsorted_path_list_lookup(const char *path, + struct path_list *list) +{ + int i; + + if (!path) + return NULL; + + for (i = 0; i < list->nr; i++) + if (!strcmp(path, list->items[i].path)) + return &list->items[i]; + return NULL; +} + +static inline void path_list_append_strategy(struct path_list_item *item) +{ + path_list_append(item->path, &use_strategies)->util = item->util; +} + +static int option_parse_strategy(const struct option *opt, + const char *arg, int unset) +{ + int i; + struct path_list_item *item = unsorted_path_list_lookup(arg, &strategies); + + if (unset) + return 0; + + if (item) + path_list_append_strategy(item); + else { + struct strbuf err; + strbuf_init(&err, 0); + for (i = 0; i < strategies.nr; i++) + strbuf_addf(&err, " %s", strategies.items[i].path); + fprintf(stderr, "Could not find merge strategy '%s'.\n", arg); + fprintf(stderr, "Available strategies are:%s.\n", err.buf); + exit(1); + } + return 0; +} + +static int option_parse_n(const struct option *opt, + const char *arg, int unset) +{ + show_diffstat = unset; + return 0; +} + +static struct option builtin_merge_options[] = { + { OPTION_CALLBACK, 'n', NULL, NULL, NULL, + "do not show a diffstat at the end of the merge", + PARSE_OPT_NOARG, option_parse_n }, + OPT_BOOLEAN(0, "stat", &show_diffstat, + "show a diffstat at the end of the merge"), + OPT_BOOLEAN(0, "summary", &show_diffstat, "(synonym to --stat)"), + OPT_BOOLEAN(0, "log", &option_log, + "add list of one-line log to merge commit message"), + OPT_BOOLEAN(0, "squash", &squash, + "create a single commit instead of doing a merge"), + OPT_BOOLEAN(0, "commit", &option_commit, + "perform a commit if the merge succeeds (default)"), + OPT_BOOLEAN(0, "ff", &allow_fast_forward, + "allow fast forward (default)"), + OPT_CALLBACK('s', "strategy", &use_strategies, "strategy", + "merge strategy to use", option_parse_strategy), + OPT_CALLBACK('m', "message", &merge_msg, "message", + "message to be used for the merge commit (if any)", + option_parse_message), + OPT_END() +}; + +/* Cleans up metadata that is uninteresting after a succeeded merge. */ +static void dropsave(void) +{ + unlink(git_path("MERGE_HEAD")); + unlink(git_path("MERGE_MSG")); + unlink(git_path("MERGE_STASH")); +} + +static void save_state(void) +{ + int fd; + struct child_process stash; + const char *argv[] = {"stash", "create", NULL}; + + fd = open(git_path("MERGE_STASH"), O_WRONLY | O_CREAT, 0666); + if (fd < 0) + die("Could not write to %s", git_path("MERGE_STASH")); + memset(&stash, 0, sizeof(stash)); + stash.argv = argv; + stash.out = fd; + stash.git_cmd = 1; + run_command(&stash); +} + +static void reset_hard(unsigned const char *sha1, int verbose) +{ + struct tree *tree; + struct unpack_trees_options opts; + struct tree_desc t; + + memset(&opts, 0, sizeof(opts)); + opts.head_idx = -1; + opts.src_index = &the_index; + opts.dst_index = &the_index; + opts.update = 1; + opts.reset = 1; + if (verbose) + opts.verbose_update = 1; + + tree = parse_tree_indirect(sha1); + if (!tree) + die("failed to unpack %s tree object", sha1_to_hex(sha1)); + parse_tree(tree); + init_tree_desc(&t, tree->buffer, tree->size); + if (unpack_trees(1, &t, &opts)) + exit(128); /* We've already reported the error, finish dying */ +} + +static void restore_state(void) +{ + struct strbuf sb; + const char *args[] = { "stash", "apply", NULL, NULL }; + + if (access(git_path("MERGE_STASH"), R_OK) < 0) + return; + + reset_hard(head, 1); + + strbuf_init(&sb, 0); + if (strbuf_read_file(&sb, git_path("MERGE_STASH"), 0) < 0) + die("could not read MERGE_STASH: %s", strerror(errno)); + args[2] = sb.buf; + + /* + * It is OK to ignore error here, for example when there was + * nothing to restore. + */ + run_command_v_opt(args, RUN_GIT_CMD); + + strbuf_release(&sb); + refresh_cache(REFRESH_QUIET); +} + +/* This is called when no merge was necessary. */ +static void finish_up_to_date(const char *msg) +{ + printf("%s%s\n", squash ? " (nothing to squash)" : "", msg); + dropsave(); +} + +static void squash_message(void) +{ + struct rev_info rev; + struct commit *commit; + struct strbuf out; + struct commit_list *j; + int fd; + + printf("Squash commit -- not updating HEAD\n"); + fd = open(git_path("SQUASH_MSG"), O_WRONLY | O_CREAT, 0666); + if (fd < 0) + die("Could not write to %s", git_path("SQUASH_MSG")); + + init_revisions(&rev, NULL); + rev.ignore_merges = 1; + rev.commit_format = CMIT_FMT_MEDIUM; + + commit = lookup_commit(head); + commit->object.flags |= UNINTERESTING; + add_pending_object(&rev, &commit->object, NULL); + + for (j = remoteheads; j; j = j->next) + add_pending_object(&rev, &j->item->object, NULL); + + setup_revisions(0, NULL, &rev, NULL); + if (prepare_revision_walk(&rev)) + die("revision walk setup failed"); + + strbuf_init(&out, 0); + strbuf_addstr(&out, "Squashed commit of the following:\n"); + while ((commit = get_revision(&rev)) != NULL) { + strbuf_addch(&out, '\n'); + strbuf_addf(&out, "commit %s\n", + sha1_to_hex(commit->object.sha1)); + pretty_print_commit(rev.commit_format, commit, &out, rev.abbrev, + NULL, NULL, rev.date_mode, 0); + } + write(fd, out.buf, out.len); + close(fd); + strbuf_release(&out); +} + +static int run_hook(const char *name) +{ + struct child_process hook; + const char *argv[3], *env[2]; + char index[PATH_MAX]; + + argv[0] = git_path("hooks/%s", name); + if (access(argv[0], X_OK) < 0) + return 0; + + snprintf(index, sizeof(index), "GIT_INDEX_FILE=%s", get_index_file()); + env[0] = index; + env[1] = NULL; + + if (squash) + argv[1] = "1"; + else + argv[1] = "0"; + argv[2] = NULL; + + memset(&hook, 0, sizeof(hook)); + hook.argv = argv; + hook.no_stdin = 1; + hook.stdout_to_stderr = 1; + hook.env = env; + + return run_command(&hook); +} + +static void finish(const unsigned char *new_head, const char *msg) +{ + struct strbuf reflog_message; + + strbuf_init(&reflog_message, 0); + if (!msg) + strbuf_addstr(&reflog_message, getenv("GIT_REFLOG_ACTION")); + else { + printf("%s\n", msg); + strbuf_addf(&reflog_message, "%s: %s", + getenv("GIT_REFLOG_ACTION"), msg); + } + if (squash) { + squash_message(); + } else { + if (!merge_msg.len) + printf("No merge message -- not updating HEAD\n"); + else { + const char *argv_gc_auto[] = { "gc", "--auto", NULL }; + update_ref(reflog_message.buf, "HEAD", + new_head, head, 0, + DIE_ON_ERR); + /* + * We ignore errors in 'gc --auto', since the + * user should see them. + */ + run_command_v_opt(argv_gc_auto, RUN_GIT_CMD); + } + } + if (new_head && show_diffstat) { + struct diff_options opts; + diff_setup(&opts); + opts.output_format |= + DIFF_FORMAT_SUMMARY | DIFF_FORMAT_DIFFSTAT; + opts.detect_rename = DIFF_DETECT_RENAME; + if (diff_use_color_default > 0) + DIFF_OPT_SET(&opts, COLOR_DIFF); + diff_tree_sha1(head, new_head, "", &opts); + diffcore_std(&opts); + diff_flush(&opts); + } + + /* Run a post-merge hook */ + run_hook("post-merge"); + + strbuf_release(&reflog_message); +} + +/* Get the name for the merge commit's message. */ +static void merge_name(const char *remote, struct strbuf *msg) +{ + struct object *remote_head; + unsigned char branch_head[20], buf_sha[20]; + struct strbuf buf; + char *ptr; + int len = 0; + char *ref; + + memset(branch_head, 0, sizeof(branch_head)); + remote_head = peel_to_type(remote, 0, NULL, OBJ_COMMIT); + if (!remote_head) + die("'%s' does not point to a commit", remote); + + strbuf_init(&buf, 0); + strbuf_addstr(&buf, "refs/heads/"); + strbuf_addstr(&buf, remote); + dwim_ref(buf.buf, buf.len, branch_head, &ref); + + if (!hashcmp(remote_head->sha1, branch_head)) { + strbuf_addf(msg, "%s\t\tbranch '%s' of .\n", + sha1_to_hex(branch_head), remote); + return; + } + /* See if remote matches <name>~<number>, or <name>^ */ + ptr = strrchr(remote, '^'); + if (ptr && ptr[1] == '\0') { + len = strlen(remote); + while ((ptr = (char *)memrchr(remote, '^', len))) + if (ptr && ptr[1] == '\0') + len = ptr - remote - 1; + else + break; + } + else { + ptr = strrchr(remote, '~'); + if (ptr && ptr[1] != '0' && isdigit(ptr[1])) { + len = ptr-remote; + ptr++; + for (ptr++; *ptr; ptr++) + if (!isdigit(*ptr)) { + len = 0; + break; + } + } + } + if (len) { + struct strbuf truname = STRBUF_INIT; + strbuf_addstr(&truname, remote); + strbuf_setlen(&truname, len); + if (dwim_ref(truname.buf, truname.len, buf_sha, &ref)) { + strbuf_addf(msg, + "%s\t\tbranch '%s' (early part) of .\n", + sha1_to_hex(remote_head->sha1), truname.buf); + return; + } + } + + if (!strcmp(remote, "FETCH_HEAD") && + !access(git_path("FETCH_HEAD"), R_OK)) { + FILE *fp; + struct strbuf line; + char *ptr; + + strbuf_init(&line, 0); + fp = fopen(git_path("FETCH_HEAD"), "r"); + if (!fp) + die("could not open %s for reading: %s", + git_path("FETCH_HEAD"), strerror(errno)); + strbuf_getline(&line, fp, '\n'); + fclose(fp); + ptr = strstr(line.buf, "\tnot-for-merge\t"); + if (ptr) + strbuf_remove(&line, ptr-line.buf+1, 13); + strbuf_addbuf(msg, &line); + strbuf_release(&line); + return; + } + strbuf_addf(msg, "%s\t\tcommit '%s'\n", + sha1_to_hex(remote_head->sha1), remote); +} + +int git_merge_config(const char *k, const char *v, void *cb) +{ + if (branch && !prefixcmp(k, "branch.") && + !prefixcmp(k + 7, branch) && + !strcmp(k + 7 + strlen(branch), ".mergeoptions")) { + const char **argv; + int argc; + char *buf; + + buf = xstrdup(v); + argc = split_cmdline(buf, &argv); + argv = xrealloc(argv, sizeof(*argv) * (argc + 2)); + memmove(argv + 1, argv, sizeof(*argv) * (argc + 1)); + argc++; + parse_options(argc, argv, builtin_merge_options, + builtin_merge_usage, 0); + free(buf); + } + + if (!strcmp(k, "merge.diffstat") || !strcmp(k, "merge.stat")) + show_diffstat = git_config_bool(k, v); + else if (!strcmp(k, "pull.twohead")) + return git_config_string(&pull_twohead, k, v); + else if (!strcmp(k, "pull.octopus")) + return git_config_string(&pull_octopus, k, v); + return 0; +} + +static int read_tree_trivial(unsigned char *common, unsigned char *head, + unsigned char *one) +{ + int i, nr_trees = 0; + struct tree *trees[MAX_UNPACK_TREES]; + struct tree_desc t[MAX_UNPACK_TREES]; + struct unpack_trees_options opts; + + memset(&opts, 0, sizeof(opts)); + opts.head_idx = -1; + opts.src_index = &the_index; + opts.dst_index = &the_index; + opts.update = 1; + opts.verbose_update = 1; + opts.trivial_merges_only = 1; + opts.merge = 1; + trees[nr_trees] = parse_tree_indirect(common); + if (!trees[nr_trees++]) + return -1; + trees[nr_trees] = parse_tree_indirect(head); + if (!trees[nr_trees++]) + return -1; + trees[nr_trees] = parse_tree_indirect(one); + if (!trees[nr_trees++]) + return -1; + opts.fn = threeway_merge; + cache_tree_free(&active_cache_tree); + opts.head_idx = 2; + for (i = 0; i < nr_trees; i++) { + parse_tree(trees[i]); + init_tree_desc(t+i, trees[i]->buffer, trees[i]->size); + } + if (unpack_trees(nr_trees, t, &opts)) + return -1; + return 0; +} + +static int commit_tree_trivial(const char *msg, unsigned const char *tree, + struct commit_list *parents, unsigned char *ret) +{ + struct commit_list *i; + struct strbuf buf; + int encoding_is_utf8; + + /* Not having i18n.commitencoding is the same as having utf-8 */ + encoding_is_utf8 = is_encoding_utf8(git_commit_encoding); + + strbuf_init(&buf, 8192); /* should avoid reallocs for the headers */ + strbuf_addf(&buf, "tree %s\n", sha1_to_hex(tree)); + + for (i = parents; i; i = i->next) + strbuf_addf(&buf, "parent %s\n", + sha1_to_hex(i->item->object.sha1)); + + /* Person/date information */ + strbuf_addf(&buf, "author %s\n", + git_author_info(IDENT_ERROR_ON_NO_NAME)); + strbuf_addf(&buf, "committer %s\n", + git_committer_info(IDENT_ERROR_ON_NO_NAME)); + if (!encoding_is_utf8) + strbuf_addf(&buf, "encoding %s\n", git_commit_encoding); + strbuf_addch(&buf, '\n'); + + /* And add the comment */ + strbuf_addstr(&buf, msg); + + write_sha1_file(buf.buf, buf.len, commit_type, ret); + strbuf_release(&buf); + return *ret; +} + +static void write_tree_trivial(unsigned char *sha1) +{ + if (write_cache_as_tree(sha1, 0, NULL)) + die("git write-tree failed to write a tree"); +} + +static int try_merge_strategy(char *strategy, struct commit_list *common, + const char *head_arg) +{ + const char **args; + int i = 0, ret; + struct commit_list *j; + struct strbuf buf; + + args = xmalloc((4 + commit_list_count(common) + + commit_list_count(remoteheads)) * sizeof(char *)); + strbuf_init(&buf, 0); + strbuf_addf(&buf, "merge-%s", strategy); + args[i++] = buf.buf; + for (j = common; j; j = j->next) + args[i++] = xstrdup(sha1_to_hex(j->item->object.sha1)); + args[i++] = "--"; + args[i++] = head_arg; + for (j = remoteheads; j; j = j->next) + args[i++] = xstrdup(sha1_to_hex(j->item->object.sha1)); + args[i] = NULL; + ret = run_command_v_opt(args, RUN_GIT_CMD); + strbuf_release(&buf); + i = 1; + for (j = common; j; j = j->next) + free((void *)args[i++]); + i += 2; + for (j = remoteheads; j; j = j->next) + free((void *)args[i++]); + free(args); + return -ret; +} + +static void count_diff_files(struct diff_queue_struct *q, + struct diff_options *opt, void *data) +{ + int *count = data; + + (*count) += q->nr; +} + +static int count_unmerged_entries(void) +{ + const struct index_state *state = &the_index; + int i, ret = 0; + + for (i = 0; i < state->cache_nr; i++) + if (ce_stage(state->cache[i])) + ret++; + + return ret; +} + +static int checkout_fast_forward(unsigned char *head, unsigned char *remote) +{ + struct tree *trees[MAX_UNPACK_TREES]; + struct unpack_trees_options opts; + struct tree_desc t[MAX_UNPACK_TREES]; + int i, fd, nr_trees = 0; + struct dir_struct dir; + struct lock_file *lock_file = xcalloc(1, sizeof(struct lock_file)); + + if (read_cache_unmerged()) + die("you need to resolve your current index first"); + + fd = hold_locked_index(lock_file, 1); + + memset(&trees, 0, sizeof(trees)); + memset(&opts, 0, sizeof(opts)); + memset(&t, 0, sizeof(t)); + dir.show_ignored = 1; + dir.exclude_per_dir = ".gitignore"; + opts.dir = &dir; + + opts.head_idx = 1; + opts.src_index = &the_index; + opts.dst_index = &the_index; + opts.update = 1; + opts.verbose_update = 1; + opts.merge = 1; + opts.fn = twoway_merge; + + trees[nr_trees] = parse_tree_indirect(head); + if (!trees[nr_trees++]) + return -1; + trees[nr_trees] = parse_tree_indirect(remote); + if (!trees[nr_trees++]) + return -1; + for (i = 0; i < nr_trees; i++) { + parse_tree(trees[i]); + init_tree_desc(t+i, trees[i]->buffer, trees[i]->size); + } + if (unpack_trees(nr_trees, t, &opts)) + return -1; + if (write_cache(fd, active_cache, active_nr) || + commit_locked_index(lock_file)) + die("unable to write new index file"); + return 0; +} + +static void split_merge_strategies(const char *string, struct path_list *list) +{ + char *p, *q, *buf; + + if (!string) + return; + + list->strdup_paths = 1; + buf = xstrdup(string); + q = buf; + for (;;) { + p = strchr(q, ' '); + if (!p) { + path_list_append(q, list); + free(buf); + return; + } else { + *p = '\0'; + path_list_append(q, list); + q = ++p; + } + } +} + +static void add_strategies(const char *string, enum strategy strategy) +{ + struct path_list list; + int i; + + memset(&list, 0, sizeof(list)); + split_merge_strategies(string, &list); + if (list.nr) { + for (i = 0; i < list.nr; i++) { + struct path_list_item *item; + + item = unsorted_path_list_lookup(list.items[i].path, + &strategies); + if (item) + path_list_append_strategy(item); + } + return; + } + for (i = 0; i < strategies.nr; i++) + if ((enum strategy)strategies.items[i].util & strategy) + path_list_append_strategy(&strategies.items[i]); +} + +static int merge_trivial(void) +{ + unsigned char result_tree[20], result_commit[20]; + struct commit_list parent; + + write_tree_trivial(result_tree); + printf("Wonderful.\n"); + parent.item = remoteheads->item; + parent.next = NULL; + commit_tree_trivial(merge_msg.buf, result_tree, &parent, + result_commit); + finish(result_commit, "In-index merge"); + dropsave(); + return 0; +} + +static int finish_automerge(struct commit_list *common, + unsigned char *result_tree, + struct path_list_item *wt_strategy) +{ + struct commit_list *parents = NULL, *j; + struct strbuf buf = STRBUF_INIT; + unsigned char result_commit[20]; + + free_commit_list(common); + if (allow_fast_forward) { + parents = remoteheads; + commit_list_insert(lookup_commit(head), &parents); + parents = reduce_heads(parents); + } else { + struct commit_list **pptr = &parents; + + pptr = &commit_list_insert(lookup_commit(head), + pptr)->next; + for (j = remoteheads; j; j = j->next) + pptr = &commit_list_insert(j->item, pptr)->next; + } + free_commit_list(remoteheads); + strbuf_addch(&merge_msg, '\n'); + commit_tree_trivial(merge_msg.buf, result_tree, parents, + result_commit); + free_commit_list(parents); + strbuf_addf(&buf, "Merge made by %s.", wt_strategy->path); + finish(result_commit, buf.buf); + strbuf_release(&buf); + dropsave(); + return 0; +} + +static int suggest_conflicts(void) +{ + FILE *fp; + int pos; + + fp = fopen(git_path("MERGE_MSG"), "a"); + if (!fp) + die("Could open %s for writing", git_path("MERGE_MSG")); + fprintf(fp, "\nConflicts:\n"); + for (pos = 0; pos < active_nr; pos++) { + struct cache_entry *ce = active_cache[pos]; + + if (ce_stage(ce)) { + fprintf(fp, "\t%s\n", ce->name); + while (pos + 1 < active_nr && + !strcmp(ce->name, + active_cache[pos + 1]->name)) + pos++; + } + } + fclose(fp); + rerere(); + printf("Automatic merge failed; " + "fix conflicts and then commit the result.\n"); + return 1; +} + +int cmd_merge(int argc, const char **argv, const char *prefix) +{ + unsigned char result_tree[20]; + struct commit *second_token = NULL; + struct strbuf buf; + const char *head_arg; + int flag, head_invalid = 0, i, single_strategy; + int best_cnt = -1, merge_was_ok = 0, automerge_was_ok = 0; + struct commit_list *common = NULL; + struct path_list_item *best_strategy = NULL, *wt_strategy = NULL; + struct commit_list **remotes = &remoteheads; + + setup_work_tree(); + if (unmerged_cache()) + die("You are in the middle of a conflicted merge."); + + /* + * Check if we are _not_ on a detached HEAD, i.e. if there is a + * current branch. + */ + branch = resolve_ref("HEAD", head, 0, &flag); + if (branch && flag & REF_ISSYMREF) { + const char *ptr = skip_prefix(branch, "refs/heads/"); + if (ptr) + branch = ptr; + } else + head_invalid = 1; + + git_config(git_merge_config, NULL); + + /* for color.diff and diff.color */ + git_config(git_diff_ui_config, NULL); + + /* for color.ui */ + if (diff_use_color_default == -1) + diff_use_color_default = git_use_color_default; + + argc = parse_options(argc, argv, builtin_merge_options, + builtin_merge_usage, 0); + + if (squash) { + if (!allow_fast_forward) + die("You cannot combine --squash with --no-ff."); + option_commit = 0; + } + + if (!argc) + usage_with_options(builtin_merge_usage, + builtin_merge_options); + + /* + * This could be traditional "merge <msg> HEAD <commit>..." and + * the way we can tell it is to see if the second token is HEAD, + * but some people might have misused the interface and used a + * committish that is the same as HEAD there instead. + * Traditional format never would have "-m" so it is an + * additional safety measure to check for it. + */ + strbuf_init(&buf, 0); + if (argc > 1) { + unsigned char second_sha1[20]; + + if (get_sha1(argv[1], second_sha1)) + die("Not a valid ref: %s", argv[1]); + second_token = lookup_commit_reference_gently(second_sha1, 0); + if (!second_token) + die("'%s' is not a commit", argv[1]); + } + + if (!have_message && second_token && + !hashcmp(second_token->object.sha1, head)) { + strbuf_addstr(&merge_msg, argv[0]); + head_arg = argv[1]; + argv += 2; + argc -= 2; + } else if (head_invalid) { + struct object *remote_head; + /* + * If the merged head is a valid one there is no reason + * to forbid "git merge" into a branch yet to be born. + * We do the same for "git pull". + */ + if (argc != 1) + die("Can merge only exactly one commit into " + "empty head"); + remote_head = peel_to_type(argv[0], 0, NULL, OBJ_COMMIT); + if (!remote_head) + die("%s - not something we can merge", argv[0]); + update_ref("initial pull", "HEAD", remote_head->sha1, NULL, 0, + DIE_ON_ERR); + reset_hard(remote_head->sha1, 0); + return 0; + } else { + struct strbuf msg; + + /* We are invoked directly as the first-class UI. */ + head_arg = "HEAD"; + + /* + * All the rest are the commits being merged; + * prepare the standard merge summary message to + * be appended to the given message. If remote + * is invalid we will die later in the common + * codepath so we discard the error in this + * loop. + */ + strbuf_init(&msg, 0); + for (i = 0; i < argc; i++) + merge_name(argv[i], &msg); + fmt_merge_msg(option_log, &msg, &merge_msg); + if (merge_msg.len) + strbuf_setlen(&merge_msg, merge_msg.len-1); + } + + if (head_invalid || !argc) + usage_with_options(builtin_merge_usage, + builtin_merge_options); + + strbuf_addstr(&buf, "merge"); + for (i = 0; i < argc; i++) + strbuf_addf(&buf, " %s", argv[i]); + setenv("GIT_REFLOG_ACTION", buf.buf, 0); + strbuf_reset(&buf); + + for (i = 0; i < argc; i++) { + struct object *o; + + o = peel_to_type(argv[i], 0, NULL, OBJ_COMMIT); + if (!o) + die("%s - not something we can merge", argv[i]); + remotes = &commit_list_insert(lookup_commit(o->sha1), + remotes)->next; + + strbuf_addf(&buf, "GITHEAD_%s", sha1_to_hex(o->sha1)); + setenv(buf.buf, argv[i], 1); + strbuf_reset(&buf); + } + + if (!use_strategies.nr) { + if (!remoteheads->next) + add_strategies(pull_twohead, DEFAULT_TWOHEAD); + else + add_strategies(pull_octopus, DEFAULT_OCTOPUS); + } + + for (i = 0; i < use_strategies.nr; i++) { + if ((unsigned int)use_strategies.items[i].util & + NO_FAST_FORWARD) + allow_fast_forward = 0; + if ((unsigned int)use_strategies.items[i].util & NO_TRIVIAL) + allow_trivial = 0; + } + + if (!remoteheads->next) + common = get_merge_bases(lookup_commit(head), + remoteheads->item, 1); + else { + struct commit_list *list = remoteheads; + commit_list_insert(lookup_commit(head), &list); + common = get_octopus_merge_bases(list); + free(list); + } + + update_ref("updating ORIG_HEAD", "ORIG_HEAD", head, NULL, 0, + DIE_ON_ERR); + + if (!common) + ; /* No common ancestors found. We need a real merge. */ + else if (!remoteheads->next && !common->next && + common->item == remoteheads->item) { + /* + * If head can reach all the merge then we are up to date. + * but first the most common case of merging one remote. + */ + finish_up_to_date("Already up-to-date."); + return 0; + } else if (allow_fast_forward && !remoteheads->next && + !common->next && + !hashcmp(common->item->object.sha1, head)) { + /* Again the most common case of merging one remote. */ + struct strbuf msg; + struct object *o; + char hex[41]; + + strcpy(hex, find_unique_abbrev(head, DEFAULT_ABBREV)); + + printf("Updating %s..%s\n", + hex, + find_unique_abbrev(remoteheads->item->object.sha1, + DEFAULT_ABBREV)); + refresh_cache(REFRESH_QUIET); + strbuf_init(&msg, 0); + strbuf_addstr(&msg, "Fast forward"); + if (have_message) + strbuf_addstr(&msg, + " (no commit created; -m option ignored)"); + o = peel_to_type(sha1_to_hex(remoteheads->item->object.sha1), + 0, NULL, OBJ_COMMIT); + if (!o) + return 0; + + if (checkout_fast_forward(head, remoteheads->item->object.sha1)) + return 0; + + finish(o->sha1, msg.buf); + dropsave(); + return 0; + } else if (!remoteheads->next && common->next) + ; + /* + * We are not doing octopus and not fast forward. Need + * a real merge. + */ + else if (!remoteheads->next && !common->next && option_commit) { + /* + * We are not doing octopus, not fast forward, and have + * only one common. + */ + refresh_cache(REFRESH_QUIET); + if (allow_trivial) { + /* See if it is really trivial. */ + git_committer_info(IDENT_ERROR_ON_NO_NAME); + printf("Trying really trivial in-index merge...\n"); + if (!read_tree_trivial(common->item->object.sha1, + head, remoteheads->item->object.sha1)) + return merge_trivial(); + printf("Nope.\n"); + } + } else { + /* + * An octopus. If we can reach all the remote we are up + * to date. + */ + int up_to_date = 1; + struct commit_list *j; + + for (j = remoteheads; j; j = j->next) { + struct commit_list *common_one; + + /* + * Here we *have* to calculate the individual + * merge_bases again, otherwise "git merge HEAD^ + * HEAD^^" would be missed. + */ + common_one = get_merge_bases(lookup_commit(head), + j->item, 1); + if (hashcmp(common_one->item->object.sha1, + j->item->object.sha1)) { + up_to_date = 0; + break; + } + } + if (up_to_date) { + finish_up_to_date("Already up-to-date. Yeeah!"); + return 0; + } + } + + /* We are going to make a new commit. */ + git_committer_info(IDENT_ERROR_ON_NO_NAME); + + /* + * At this point, we need a real merge. No matter what strategy + * we use, it would operate on the index, possibly affecting the + * working tree, and when resolved cleanly, have the desired + * tree in the index -- this means that the index must be in + * sync with the head commit. The strategies are responsible + * to ensure this. + */ + if (use_strategies.nr != 1) { + /* + * Stash away the local changes so that we can try more + * than one. + */ + save_state(); + single_strategy = 0; + } else { + unlink(git_path("MERGE_STASH")); + single_strategy = 1; + } + + for (i = 0; i < use_strategies.nr; i++) { + int ret; + if (i) { + printf("Rewinding the tree to pristine...\n"); + restore_state(); + } + if (!single_strategy) + printf("Trying merge strategy %s...\n", + use_strategies.items[i].path); + /* + * Remember which strategy left the state in the working + * tree. + */ + wt_strategy = &use_strategies.items[i]; + + ret = try_merge_strategy(use_strategies.items[i].path, + common, head_arg); + if (!option_commit && !ret) { + merge_was_ok = 1; + /* + * This is necessary here just to avoid writing + * the tree, but later we will *not* exit with + * status code 1 because merge_was_ok is set. + */ + ret = 1; + } + + if (ret) { + /* + * The backend exits with 1 when conflicts are + * left to be resolved, with 2 when it does not + * handle the given merge at all. + */ + if (ret == 1) { + int cnt = 0; + struct rev_info rev; + + if (read_cache() < 0) + die("failed to read the cache"); + + /* Check how many files differ. */ + init_revisions(&rev, ""); + setup_revisions(0, NULL, &rev, NULL); + rev.diffopt.output_format |= + DIFF_FORMAT_CALLBACK; + rev.diffopt.format_callback = count_diff_files; + rev.diffopt.format_callback_data = &cnt; + run_diff_files(&rev, 0); + + /* + * Check how many unmerged entries are + * there. + */ + cnt += count_unmerged_entries(); + + if (best_cnt <= 0 || cnt <= best_cnt) { + best_strategy = + &use_strategies.items[i]; + best_cnt = cnt; + } + } + if (merge_was_ok) + break; + else + continue; + } + + /* Automerge succeeded. */ + write_tree_trivial(result_tree); + automerge_was_ok = 1; + break; + } + + /* + * If we have a resulting tree, that means the strategy module + * auto resolved the merge cleanly. + */ + if (automerge_was_ok) + return finish_automerge(common, result_tree, wt_strategy); + + /* + * Pick the result from the best strategy and have the user fix + * it up. + */ + if (!best_strategy) { + restore_state(); + if (use_strategies.nr > 1) + fprintf(stderr, + "No merge strategy handled the merge.\n"); + else + fprintf(stderr, "Merge with strategy %s failed.\n", + use_strategies.items[0].path); + return 2; + } else if (best_strategy == wt_strategy) + ; /* We already have its result in the working tree. */ + else { + printf("Rewinding the tree to pristine...\n"); + restore_state(); + printf("Using the %s to prepare resolving by hand.\n", + best_strategy->path); + try_merge_strategy(best_strategy->path, common, head_arg); + } + + if (squash) + finish(NULL, NULL); + else { + int fd; + struct commit_list *j; + + for (j = remoteheads; j; j = j->next) + strbuf_addf(&buf, "%s\n", + sha1_to_hex(j->item->object.sha1)); + fd = open(git_path("MERGE_HEAD"), O_WRONLY | O_CREAT, 0666); + if (fd < 0) + die("Could open %s for writing", + git_path("MERGE_HEAD")); + if (write_in_full(fd, buf.buf, buf.len) != buf.len) + die("Could not write to %s", git_path("MERGE_HEAD")); + close(fd); + strbuf_addch(&merge_msg, '\n'); + fd = open(git_path("MERGE_MSG"), O_WRONLY | O_CREAT, 0666); + if (fd < 0) + die("Could open %s for writing", git_path("MERGE_MSG")); + if (write_in_full(fd, merge_msg.buf, merge_msg.len) != + merge_msg.len) + die("Could not write to %s", git_path("MERGE_MSG")); + close(fd); + } + + if (merge_was_ok) { + fprintf(stderr, "Automatic merge went well; " + "stopped before committing as requested\n"); + return 0; + } else + return suggest_conflicts(); +} diff --git a/builtin.h b/builtin.h index 2b01fea..8bf5280 100644 --- a/builtin.h +++ b/builtin.h @@ -60,6 +60,7 @@ extern int cmd_ls_tree(int argc, const char **argv, const char *prefix); extern int cmd_ls_remote(int argc, const char **argv, const char *prefix); extern int cmd_mailinfo(int argc, const char **argv, const char *prefix); extern int cmd_mailsplit(int argc, const char **argv, const char *prefix); +extern int cmd_merge(int argc, const char **argv, const char *prefix); extern int cmd_merge_base(int argc, const char **argv, const char *prefix); extern int cmd_merge_ours(int argc, const char **argv, const char *prefix); extern int cmd_merge_file(int argc, const char **argv, const char *prefix); diff --git a/git-merge.sh b/contrib/examples/git-merge.sh similarity index 100% rename from git-merge.sh rename to contrib/examples/git-merge.sh diff --git a/git.c b/git.c index 2fbe96b..770aadd 100644 --- a/git.c +++ b/git.c @@ -271,6 +271,7 @@ static void handle_internal_command(int argc, const char **argv) { "ls-remote", cmd_ls_remote }, { "mailinfo", cmd_mailinfo }, { "mailsplit", cmd_mailsplit }, + { "merge", cmd_merge, RUN_SETUP | NEED_WORK_TREE }, { "merge-base", cmd_merge_base, RUN_SETUP }, { "merge-file", cmd_merge_file }, { "merge-ours", cmd_merge_ours, RUN_SETUP }, diff --git a/t/t7602-merge-octopus-many.sh b/t/t7602-merge-octopus-many.sh index f3a4bb2..fcb8285 100755 --- a/t/t7602-merge-octopus-many.sh +++ b/t/t7602-merge-octopus-many.sh @@ -23,7 +23,7 @@ test_expect_success 'setup' ' done ' -test_expect_failure 'merge c1 with c2, c3, c4, ... c29' ' +test_expect_success 'merge c1 with c2, c3, c4, ... c29' ' git reset --hard c1 && i=2 && refs="" && -- 1.5.6.1 ^ permalink raw reply related [flat|nested] 82+ messages in thread
* Re: [PATCH 13/13] Build in merge 2008-06-30 1:39 ` Miklos Vajna @ 2008-06-30 5:44 ` Junio C Hamano 2008-06-30 17:41 ` Alex Riesen 2008-07-01 2:13 ` Miklos Vajna 2008-06-30 22:44 ` [PATCH 13/13] " Olivier Marin 1 sibling, 2 replies; 82+ messages in thread From: Junio C Hamano @ 2008-06-30 5:44 UTC (permalink / raw To: Miklos Vajna; +Cc: git, Johannes Schindelin, Olivier Marin Miklos Vajna <vmiklos@frugalware.org> writes: > diff --git a/builtin-merge.c b/builtin-merge.c > new file mode 100644 > index 0000000..187038c > --- /dev/null > +++ b/builtin-merge.c > @@ -0,0 +1,1173 @@ > ... > +static void save_state(void) > +{ > + int fd; > + struct child_process stash; > + const char *argv[] = {"stash", "create", NULL}; > + > + fd = open(git_path("MERGE_STASH"), O_WRONLY | O_CREAT, 0666); > + if (fd < 0) > + die("Could not write to %s", git_path("MERGE_STASH")); > + memset(&stash, 0, sizeof(stash)); > + stash.argv = argv; > + stash.out = fd; > + stash.git_cmd = 1; > + run_command(&stash); > +} I first thought "heh, that's clever" until I noticed that we use "stash create" with "stash apply" these days instead of cpio for this. I suspect that we can do away without leaving the stash in this temporary file, but that comment applies to the scripted version as well. By the way, it would be consistent to name counterpart to dropsave in the scripted version as "drop_save" if you use "save_state" and "restore_state". > +static void reset_hard(unsigned const char *sha1, int verbose) > +{ > + struct tree *tree; > + struct unpack_trees_options opts; > + struct tree_desc t; > + > + memset(&opts, 0, sizeof(opts)); > + opts.head_idx = -1; > + opts.src_index = &the_index; > + opts.dst_index = &the_index; > + opts.update = 1; > + opts.reset = 1; > + if (verbose) > + opts.verbose_update = 1; > + > + tree = parse_tree_indirect(sha1); > + if (!tree) > + die("failed to unpack %s tree object", sha1_to_hex(sha1)); > + parse_tree(tree); > + init_tree_desc(&t, tree->buffer, tree->size); > + if (unpack_trees(1, &t, &opts)) > + exit(128); /* We've already reported the error, finish dying */ > +} Isn't this trashing all the cached stat info from the index? If this is emulating "reset --hard", it also should set opts.merge and do oneway_merge, after reading the current index in, I think. Resetting the index and the working tree is not particularly performance critical part, but trashing the cached stat info would hurt the performance of everything that reads the index after this function returns quite badly. I suspect that you might be better off forking the real thing (reset --hard) if you cannot get it right here. > +/* Get the name for the merge commit's message. */ > +static void merge_name(const char *remote, struct strbuf *msg) > ... > + strbuf_init(&buf, 0); > + strbuf_addstr(&buf, "refs/heads/"); > + strbuf_addstr(&buf, remote); > + dwim_ref(buf.buf, buf.len, branch_head, &ref); > + if (!hashcmp(remote_head->sha1, branch_head)) { > + strbuf_addf(msg, "%s\t\tbranch '%s' of .\n", > + sha1_to_hex(branch_head), remote); > + return; > + } Hmm, why not resolve_ref() so that it does not dwim at all? The point of the code is so that you can be confident that 'blah' *is* a local branch when you say "branch 'blah'". > + /* See if remote matches <name>~<number>, or <name>^ */ > + ptr = strrchr(remote, '^'); > + if (ptr && ptr[1] == '\0') { > + len = strlen(remote); > + while ((ptr = (char *)memrchr(remote, '^', len))) > + if (ptr && ptr[1] == '\0') > + len = ptr - remote - 1; > + else > + break; That's a funny way to say: for (len = 0, ptr = remote + strlen(remote); remote < ptr && ptr[-1] == '^'; ptr--) len++; > + if (len) { > + struct strbuf truname = STRBUF_INIT; > + strbuf_addstr(&truname, remote); > + strbuf_setlen(&truname, len); > + if (dwim_ref(truname.buf, truname.len, buf_sha, &ref)) { > + strbuf_addf(msg, > + "%s\t\tbranch '%s' (early part) of .\n", > + sha1_to_hex(remote_head->sha1), truname.buf); > + return; Isn't this wrong? Giving "v1.5.6~20" to this code will strip ~20 and make remote = "v1.5.6", to which dwim_ref() will happily say Ok, and you end up saying "branch 'v1.5.6' (early part)", don't you? > +static int read_tree_trivial(unsigned char *common, unsigned char *head, > + unsigned char *one) > +{ > + int i, nr_trees = 0; > + struct tree *trees[MAX_UNPACK_TREES]; > + struct tree_desc t[MAX_UNPACK_TREES]; > + struct unpack_trees_options opts; > + > + memset(&opts, 0, sizeof(opts)); > + opts.head_idx = -1; Is this the correct head_idx value for this three-way merge? I think it should be 2 but please double check. > +static int commit_tree_trivial(const char *msg, unsigned const char *tree, > + struct commit_list *parents, unsigned char *ret) > +{ > ... > +} We may want to have another patch before this one to abstract most of cmd_commit_tree() out, perhaps? > +int cmd_merge(int argc, const char **argv, const char *prefix) > ... > + /* > + * This could be traditional "merge <msg> HEAD <commit>..." and > + * the way we can tell it is to see if the second token is HEAD, > + * but some people might have misused the interface and used a > + * committish that is the same as HEAD there instead. > + * Traditional format never would have "-m" so it is an > + * additional safety measure to check for it. > + */ > + strbuf_init(&buf, 0); > + if (argc > 1) { > + unsigned char second_sha1[20]; > + > + if (get_sha1(argv[1], second_sha1)) > + die("Not a valid ref: %s", argv[1]); > + second_token = lookup_commit_reference_gently(second_sha1, 0); > + if (!second_token) > + die("'%s' is not a commit", argv[1]); Interesting. This _superficially_ is quite wrong, because the purpose of this part of the code is to tell if we got old-style invocation, and we should not barfing merely because what we got is _not_ old-style. If it is not old-style, then it would be new-style, and the logic to tell if it is old-style should ideally not have much knowledge about the new-style invocation to say "hey, that's an incocrrect new-style invocation". By the way, this part should probably be in a separate function: static int is_old_style_invocation(int ac, const char **gv); Old-style invocation of "git merge" (primarily by "git pull") was to call it as: git merge "message here" HEAD $commit1 $commit2... and it checks the second token ("HEAD" in the above, but people can misuse the interface to name the current branch name). If the second token is not a ref that resolves to a commit, all you know is that this is _not_ an old-style invocation, and calling the program with new-style is not a crime. The only reason this is wrong only superficially is because new style invocation would always be: git merge [options] $commit1 $commit2... after stripping the options, and these seemingly wrong die() will complain when you try to create an Octopus with the new-style syntax and the parameter given as the second remote parent is not a commit. So the logic is wrong, the fact that the user gets the same error message for incorrect old-style invocation (perhaps "git merge <msg> HAED $commit") and incorrect new-style invocation "git merge $commit1 $nonsense" is just an accident, and the end result does not hurt, but asks for a "Huh? why does it check and complain only the second parent here but not the first one?". It is interesting, but feels quite dirty. > + } > + > + if (!have_message && second_token && > + !hashcmp(second_token->object.sha1, head)) { You need to know that resolve_ref() cleared head[] when head_invalid is true when reading this code to notice that, unlike the previous round of this patch, it is Ok not to check head_invalid is fine here. I somehow feel it is an unnecessary optimization/obfuscation. But once you have "is_old_style_invocation" suggested earlier, this part would look much cleaner and the above comment would become unnecessary. > + for (i = 0; i < argc; i++) { > + struct object *o; > + > + o = peel_to_type(argv[i], 0, NULL, OBJ_COMMIT); > + if (!o) > + die("%s - not something we can merge", argv[i]); > + remotes = &commit_list_insert(lookup_commit(o->sha1), > + remotes)->next; > + > + strbuf_addf(&buf, "GITHEAD_%s", sha1_to_hex(o->sha1)); > + setenv(buf.buf, argv[i], 1); > + strbuf_reset(&buf); > + } > + > + o = peel_to_type(sha1_to_hex(remoteheads->item->object.sha1), > + 0, NULL, OBJ_COMMIT); > + if (!o) > + return 0; > + > + if (checkout_fast_forward(head, remoteheads->item->object.sha1)) > + return 0; When o does not peel well, or checkout_fast_forward() returns failure, that would be a failure case, wouldn't it? Why return 0? Maybe you misread "exit" in shell scripts? It does not mean exit(0); it means "exit with the same exit status as the last command". So new_head=$(git rev-parse ...) && git read-tree -m -u ... && finish || exit will exit non-zero if any of the commands chained by && fails. > + if (allow_trivial) { > + /* See if it is really trivial. */ > + git_committer_info(IDENT_ERROR_ON_NO_NAME); > + printf("Trying really trivial in-index merge...\n"); > + if (!read_tree_trivial(common->item->object.sha1, > + head, remoteheads->item->object.sha1)) > + return merge_trivial(); > + printf("Nope.\n"); Nicer, much nicer. > + /* > + * At this point, we need a real merge. No matter what strategy > + * we use, it would operate on the index, possibly affecting the > + * working tree, and when resolved cleanly, have the desired > + * tree in the index -- this means that the index must be in > + * sync with the head commit. The strategies are responsible > + * to ensure this. > + */ > + if (use_strategies.nr != 1) { > + /* > + * Stash away the local changes so that we can try more > + * than one. > + */ > + save_state(); > + single_strategy = 0; > + } else { > + unlink(git_path("MERGE_STASH")); > + single_strategy = 1; I think s/single_strategy/(use_strategies.nr == 1)/ in the remainder of the code would be taking advantage of working in C ;-) > + if (ret) { > + /* > + * The backend exits with 1 when conflicts are > + * left to be resolved, with 2 when it does not > + * handle the given merge at all. > + */ > + if (ret == 1) { Probably from here til ... > + int cnt = 0; > ... > + cnt += count_unmerged_entries(); ... here should be a separate "evaluate_result()" function. > + if (best_cnt <= 0 || cnt <= best_cnt) { > + best_strategy = > + &use_strategies.items[i]; > + best_cnt = cnt; > + } > + } > + if (merge_was_ok) > + break; > + else > + continue; > + } > + > + /* Automerge succeeded. */ > + write_tree_trivial(result_tree); > + automerge_was_ok = 1; > + break; > + } The part of the file I did not comment in this message (except for the "cast" thing) looked straightforward enough. I did not very carefully hunt for bugs, but I did not see anything obviously wrong. Thanks. ^ permalink raw reply [flat|nested] 82+ messages in thread
* Re: [PATCH 13/13] Build in merge 2008-06-30 5:44 ` Junio C Hamano @ 2008-06-30 17:41 ` Alex Riesen 2008-07-01 2:13 ` Miklos Vajna 1 sibling, 0 replies; 82+ messages in thread From: Alex Riesen @ 2008-06-30 17:41 UTC (permalink / raw To: Junio C Hamano; +Cc: Miklos Vajna, git, Johannes Schindelin, Olivier Marin 2008/6/30 Junio C Hamano <gitster@pobox.com>: > Miklos Vajna <vmiklos@frugalware.org> writes: >> + /* See if remote matches <name>~<number>, or <name>^ */ >> + ptr = strrchr(remote, '^'); >> + if (ptr && ptr[1] == '\0') { >> + len = strlen(remote); >> + while ((ptr = (char *)memrchr(remote, '^', len))) >> + if (ptr && ptr[1] == '\0') >> + len = ptr - remote - 1; >> + else >> + break; > > That's a funny way to say: > > for (len = 0, ptr = remote + strlen(remote); > remote < ptr && ptr[-1] == '^'; > ptr--) > len++; > Besides, Cygwin has no memrchr ^ permalink raw reply [flat|nested] 82+ messages in thread
* Re: [PATCH 13/13] Build in merge 2008-06-30 5:44 ` Junio C Hamano 2008-06-30 17:41 ` Alex Riesen @ 2008-07-01 2:13 ` Miklos Vajna 2008-07-01 2:22 ` [PATCH 13/14] git-commit-tree: make it usable from other builtins Miklos Vajna 2008-07-01 2:22 ` [PATCH 14/14] Build in merge Miklos Vajna 1 sibling, 2 replies; 82+ messages in thread From: Miklos Vajna @ 2008-07-01 2:13 UTC (permalink / raw To: Junio C Hamano; +Cc: git, Johannes Schindelin, Olivier Marin [-- Attachment #1: Type: text/plain, Size: 11364 bytes --] On Sun, Jun 29, 2008 at 10:44:43PM -0700, Junio C Hamano <gitster@pobox.com> wrote: > > +static void save_state(void) > > +{ > > + int fd; > > + struct child_process stash; > > + const char *argv[] = {"stash", "create", NULL}; > > + > > + fd = open(git_path("MERGE_STASH"), O_WRONLY | O_CREAT, 0666); > > + if (fd < 0) > > + die("Could not write to %s", git_path("MERGE_STASH")); > > + memset(&stash, 0, sizeof(stash)); > > + stash.argv = argv; > > + stash.out = fd; > > + stash.git_cmd = 1; > > + run_command(&stash); > > +} > > I first thought "heh, that's clever" until I noticed that we use "stash > create" with "stash apply" these days instead of cpio for this. I suspect > that we can do away without leaving the stash in this temporary file, but > that comment applies to the scripted version as well. We can. I just did it. ;-) > By the way, it would be consistent to name counterpart to dropsave in the > scripted version as "drop_save" if you use "save_state" and "restore_state". OK, renamed. > > > +static void reset_hard(unsigned const char *sha1, int verbose) > > +{ > > + struct tree *tree; > > + struct unpack_trees_options opts; > > + struct tree_desc t; > > + > > + memset(&opts, 0, sizeof(opts)); > > + opts.head_idx = -1; > > + opts.src_index = &the_index; > > + opts.dst_index = &the_index; > > + opts.update = 1; > > + opts.reset = 1; > > + if (verbose) > > + opts.verbose_update = 1; > > + > > + tree = parse_tree_indirect(sha1); > > + if (!tree) > > + die("failed to unpack %s tree object", sha1_to_hex(sha1)); > > + parse_tree(tree); > > + init_tree_desc(&t, tree->buffer, tree->size); > > + if (unpack_trees(1, &t, &opts)) > > + exit(128); /* We've already reported the error, finish dying */ > > +} > > Isn't this trashing all the cached stat info from the index? If this is > emulating "reset --hard", it also should set opts.merge and do > oneway_merge, after reading the current index in, I think. Resetting the > index and the working tree is not particularly performance critical part, > but trashing the cached stat info would hurt the performance of everything > that reads the index after this function returns quite badly. I suspect > that you might be better off forking the real thing (reset --hard) if you > cannot get it right here. I just realized that builtin-reset forks read-tree as well, so I did almost the same. > > +/* Get the name for the merge commit's message. */ > > +static void merge_name(const char *remote, struct strbuf *msg) > > ... > > + strbuf_init(&buf, 0); > > + strbuf_addstr(&buf, "refs/heads/"); > > + strbuf_addstr(&buf, remote); > > + dwim_ref(buf.buf, buf.len, branch_head, &ref); > > + if (!hashcmp(remote_head->sha1, branch_head)) { > > + strbuf_addf(msg, "%s\t\tbranch '%s' of .\n", > > + sha1_to_hex(branch_head), remote); > > + return; > > + } > > Hmm, why not resolve_ref() so that it does not dwim at all? The point of > the code is so that you can be confident that 'blah' *is* a local branch > when you say "branch 'blah'". Yes. I'm now using resolve_ref(). > > + /* See if remote matches <name>~<number>, or <name>^ */ > > + ptr = strrchr(remote, '^'); > > + if (ptr && ptr[1] == '\0') { > > + len = strlen(remote); > > + while ((ptr = (char *)memrchr(remote, '^', len))) > > + if (ptr && ptr[1] == '\0') > > + len = ptr - remote - 1; > > + else > > + break; > > That's a funny way to say: > > for (len = 0, ptr = remote + strlen(remote); > remote < ptr && ptr[-1] == '^'; > ptr--) > len++; Ah, and this way I don't need memrchr(), which was pointed out to be problemtic on Cygwin. > > > + if (len) { > > + struct strbuf truname = STRBUF_INIT; > > + strbuf_addstr(&truname, remote); > > + strbuf_setlen(&truname, len); > > + if (dwim_ref(truname.buf, truname.len, buf_sha, &ref)) { > > + strbuf_addf(msg, > > + "%s\t\tbranch '%s' (early part) of .\n", > > + sha1_to_hex(remote_head->sha1), truname.buf); > > + return; > > Isn't this wrong? Giving "v1.5.6~20" to this code will strip ~20 and make > remote = "v1.5.6", to which dwim_ref() will happily say Ok, and you end up > saying "branch 'v1.5.6' (early part)", don't you? Right. Now I do strbuf_addstr(&truname, "refs/heads/"); Before appending the remote name to truname, so that should exclude tags. > > +static int read_tree_trivial(unsigned char *common, unsigned char *head, > > + unsigned char *one) > > +{ > > + int i, nr_trees = 0; > > + struct tree *trees[MAX_UNPACK_TREES]; > > + struct tree_desc t[MAX_UNPACK_TREES]; > > + struct unpack_trees_options opts; > > + > > + memset(&opts, 0, sizeof(opts)); > > + opts.head_idx = -1; > > Is this the correct head_idx value for this three-way merge? I think it > should be 2 but please double check. Yes, you are right. I just checked builtin-read-tree and it's 2, not -1. > > +static int commit_tree_trivial(const char *msg, unsigned const char *tree, > > + struct commit_list *parents, unsigned char *ret) > > +{ > > ... > > +} > > We may want to have another patch before this one to abstract most of > cmd_commit_tree() out, perhaps? Done. And now builtin-merge uses commit_tree() as well. > > +int cmd_merge(int argc, const char **argv, const char *prefix) > > ... > > + /* > > + * This could be traditional "merge <msg> HEAD <commit>..." and > > + * the way we can tell it is to see if the second token is HEAD, > > + * but some people might have misused the interface and used a > > + * committish that is the same as HEAD there instead. > > + * Traditional format never would have "-m" so it is an > > + * additional safety measure to check for it. > > + */ > > + strbuf_init(&buf, 0); > > + if (argc > 1) { > > + unsigned char second_sha1[20]; > > + > > + if (get_sha1(argv[1], second_sha1)) > > + die("Not a valid ref: %s", argv[1]); > > + second_token = lookup_commit_reference_gently(second_sha1, 0); > > + if (!second_token) > > + die("'%s' is not a commit", argv[1]); > > Interesting. > > This _superficially_ is quite wrong, because the purpose of this part of > the code is to tell if we got old-style invocation, and we should not > barfing merely because what we got is _not_ old-style. If it is not > old-style, then it would be new-style, and the logic to tell if it is > old-style should ideally not have much knowledge about the new-style > invocation to say "hey, that's an incocrrect new-style invocation". By > the way, this part should probably be in a separate function: > > static int is_old_style_invocation(int ac, const char **gv); OK, I broke out is_old_style_invocation() from cmd_merge(). > Old-style invocation of "git merge" (primarily by "git pull") was > to call it as: > > git merge "message here" HEAD $commit1 $commit2... > > and it checks the second token ("HEAD" in the above, but people can misuse > the interface to name the current branch name). If the second token is > not a ref that resolves to a commit, all you know is that this is _not_ an > old-style invocation, and calling the program with new-style is not a > crime. > > The only reason this is wrong only superficially is because new style > invocation would always be: > > git merge [options] $commit1 $commit2... > > after stripping the options, and these seemingly wrong die() will complain > when you try to create an Octopus with the new-style syntax and the > parameter given as the second remote parent is not a commit. So the logic > is wrong, the fact that the user gets the same error message for incorrect > old-style invocation (perhaps "git merge <msg> HAED $commit") and > incorrect new-style invocation "git merge $commit1 $nonsense" is just an > accident, and the end result does not hurt, but asks for a "Huh? why does > it check and complain only the second parent here but not the first one?". > > It is interesting, but feels quite dirty. Now if the second token is a valid SHA1 then I die() if it's not a commit, but otherwise I just assume it's a new-style invocation. > > + if (!have_message && second_token && > > + !hashcmp(second_token->object.sha1, head)) { > > You need to know that resolve_ref() cleared head[] when head_invalid is > true when reading this code to notice that, unlike the previous round of > this patch, it is Ok not to check head_invalid is fine here. I somehow > feel it is an unnecessary optimization/obfuscation. > > But once you have "is_old_style_invocation" suggested earlier, this part > would look much cleaner and the above comment would become unnecessary. Yes, now it's just: if (!have_message && is_old_style_invocation(argc, argv)) { > > > + for (i = 0; i < argc; i++) { > > + struct object *o; > > + > > + o = peel_to_type(argv[i], 0, NULL, OBJ_COMMIT); > > + if (!o) > > + die("%s - not something we can merge", argv[i]); > > + remotes = &commit_list_insert(lookup_commit(o->sha1), > > + remotes)->next; > > + > > + strbuf_addf(&buf, "GITHEAD_%s", sha1_to_hex(o->sha1)); > > + setenv(buf.buf, argv[i], 1); > > + strbuf_reset(&buf); > > + } > > + > > + o = peel_to_type(sha1_to_hex(remoteheads->item->object.sha1), > > + 0, NULL, OBJ_COMMIT); > > + if (!o) > > + return 0; > > + > > + if (checkout_fast_forward(head, remoteheads->item->object.sha1)) > > + return 0; > > When o does not peel well, or checkout_fast_forward() returns failure, > that would be a failure case, wouldn't it? Why return 0? > > Maybe you misread "exit" in shell scripts? It does not mean exit(0); it > means "exit with the same exit status as the last command". So > > new_head=$(git rev-parse ...) && > git read-tree -m -u ... && > finish || exit > > will exit non-zero if any of the commands chained by && fails. Thanks, that was the case. I thought "false || exit" exits with status code 0. > > + /* > > + * At this point, we need a real merge. No matter what strategy > > + * we use, it would operate on the index, possibly affecting the > > + * working tree, and when resolved cleanly, have the desired > > + * tree in the index -- this means that the index must be in > > + * sync with the head commit. The strategies are responsible > > + * to ensure this. > > + */ > > + if (use_strategies.nr != 1) { > > + /* > > + * Stash away the local changes so that we can try more > > + * than one. > > + */ > > + save_state(); > > + single_strategy = 0; > > + } else { > > + unlink(git_path("MERGE_STASH")); > > + single_strategy = 1; > > I think s/single_strategy/(use_strategies.nr == 1)/ in the remainder of the > code would be taking advantage of working in C ;-) I dropped single_strategy. > > > + if (ret) { > > + /* > > + * The backend exits with 1 when conflicts are > > + * left to be resolved, with 2 when it does not > > + * handle the given merge at all. > > + */ > > + if (ret == 1) { > > Probably from here til ... > > > + int cnt = 0; > > ... > > + cnt += count_unmerged_entries(); > > ... here should be a separate "evaluate_result()" function. Done. [-- Attachment #2: Type: application/pgp-signature, Size: 197 bytes --] ^ permalink raw reply [flat|nested] 82+ messages in thread
* [PATCH 13/14] git-commit-tree: make it usable from other builtins 2008-07-01 2:13 ` Miklos Vajna @ 2008-07-01 2:22 ` Miklos Vajna 2008-07-01 5:07 ` Johannes Schindelin 2008-07-01 5:50 ` Junio C Hamano 2008-07-01 2:22 ` [PATCH 14/14] Build in merge Miklos Vajna 1 sibling, 2 replies; 82+ messages in thread From: Miklos Vajna @ 2008-07-01 2:22 UTC (permalink / raw To: Junio C Hamano; +Cc: git, Johannes Schindelin, Olivier Marin Move all functionality (except option parsing) from cmd_commit_tree() to commit_tree(), so that other builtins can use it without a child process. Signed-off-by: Miklos Vajna <vmiklos@frugalware.org> --- This patch is against master, you need to pull from me or first rebase mv/merge-in-c against current master. I did it this way to avoid an unnecessary conflict with Dscho's recent patch to commit-tree (ef98c5c). builtin-commit-tree.c | 71 ++++++++++++++++++++++++++++-------------------- builtin.h | 4 +++ 2 files changed, 45 insertions(+), 30 deletions(-) diff --git a/builtin-commit-tree.c b/builtin-commit-tree.c index 3881f6c..7a9a309 100644 --- a/builtin-commit-tree.c +++ b/builtin-commit-tree.c @@ -45,41 +45,19 @@ static const char commit_utf8_warn[] = "You may want to amend it after fixing the message, or set the config\n" "variable i18n.commitencoding to the encoding your project uses.\n"; -int cmd_commit_tree(int argc, const char **argv, const char *prefix) +int commit_tree(const char *msg, unsigned char *tree, + struct commit_list *parents, unsigned char *ret) { - int i; - struct commit_list *parents = NULL; - unsigned char tree_sha1[20]; - unsigned char commit_sha1[20]; - struct strbuf buffer; int encoding_is_utf8; + struct strbuf buffer; - git_config(git_default_config, NULL); - - if (argc < 2) - usage(commit_tree_usage); - if (get_sha1(argv[1], tree_sha1)) - die("Not a valid object name %s", argv[1]); - - check_valid(tree_sha1, OBJ_TREE); - for (i = 2; i < argc; i += 2) { - unsigned char sha1[20]; - const char *a, *b; - a = argv[i]; b = argv[i+1]; - if (!b || strcmp(a, "-p")) - usage(commit_tree_usage); - - if (get_sha1(b, sha1)) - die("Not a valid object name %s", b); - check_valid(sha1, OBJ_COMMIT); - new_parent(lookup_commit(sha1), &parents); - } + check_valid(tree, OBJ_TREE); /* Not having i18n.commitencoding is the same as having utf-8 */ encoding_is_utf8 = is_encoding_utf8(git_commit_encoding); strbuf_init(&buffer, 8192); /* should avoid reallocs for the headers */ - strbuf_addf(&buffer, "tree %s\n", sha1_to_hex(tree_sha1)); + strbuf_addf(&buffer, "tree %s\n", sha1_to_hex(tree)); /* * NOTE! This ordering means that the same exact tree merged with a @@ -102,14 +80,47 @@ int cmd_commit_tree(int argc, const char **argv, const char *prefix) strbuf_addch(&buffer, '\n'); /* And add the comment */ - if (strbuf_read(&buffer, 0, 0) < 0) - die("git-commit-tree: read returned %s", strerror(errno)); + strbuf_addstr(&buffer, msg); /* And check the encoding */ if (encoding_is_utf8 && !is_utf8(buffer.buf)) fprintf(stderr, commit_utf8_warn); - if (!write_sha1_file(buffer.buf, buffer.len, commit_type, commit_sha1)) { + return write_sha1_file(buffer.buf, buffer.len, commit_type, ret); +} + +int cmd_commit_tree(int argc, const char **argv, const char *prefix) +{ + int i; + struct commit_list *parents = NULL; + unsigned char tree_sha1[20]; + unsigned char commit_sha1[20]; + struct strbuf buffer = STRBUF_INIT; + + git_config(git_default_config, NULL); + + if (argc < 2) + usage(commit_tree_usage); + if (get_sha1(argv[1], tree_sha1)) + die("Not a valid object name %s", argv[1]); + + for (i = 2; i < argc; i += 2) { + unsigned char sha1[20]; + const char *a, *b; + a = argv[i]; b = argv[i+1]; + if (!b || strcmp(a, "-p")) + usage(commit_tree_usage); + + if (get_sha1(b, sha1)) + die("Not a valid object name %s", b); + check_valid(sha1, OBJ_COMMIT); + new_parent(lookup_commit(sha1), &parents); + } + + if (strbuf_read(&buffer, 0, 0) < 0) + die("git-commit-tree: read returned %s", strerror(errno)); + + if (!commit_tree(buffer.buf, tree_sha1, parents, commit_sha1)) { printf("%s\n", sha1_to_hex(commit_sha1)); return 0; } diff --git a/builtin.h b/builtin.h index 2b01fea..05ee56f 100644 --- a/builtin.h +++ b/builtin.h @@ -3,6 +3,8 @@ #include "git-compat-util.h" #include "strbuf.h" +#include "cache.h" +#include "commit.h" extern const char git_version_string[]; extern const char git_usage_string[]; @@ -14,6 +16,8 @@ extern void prune_packed_objects(int); extern int read_line_with_nul(char *buf, int size, FILE *file); extern int fmt_merge_msg(int merge_summary, struct strbuf *in, struct strbuf *out); +extern int commit_tree(const char *msg, unsigned char *tree, + struct commit_list *parents, unsigned char *ret); extern int cmd_add(int argc, const char **argv, const char *prefix); extern int cmd_annotate(int argc, const char **argv, const char *prefix); -- 1.5.6.1 ^ permalink raw reply related [flat|nested] 82+ messages in thread
* Re: [PATCH 13/14] git-commit-tree: make it usable from other builtins 2008-07-01 2:22 ` [PATCH 13/14] git-commit-tree: make it usable from other builtins Miklos Vajna @ 2008-07-01 5:07 ` Johannes Schindelin 2008-07-01 5:50 ` Junio C Hamano 1 sibling, 0 replies; 82+ messages in thread From: Johannes Schindelin @ 2008-07-01 5:07 UTC (permalink / raw To: Miklos Vajna; +Cc: Junio C Hamano, git, Olivier Marin Hi, On Tue, 1 Jul 2008, Miklos Vajna wrote: > I did it this way to avoid an unnecessary conflict with Dscho's recent > patch to commit-tree (ef98c5c). Oh, sorry! Funnily enough, I committed and tested this change in my clone of builtin-merge before I realized that I am in the wrong directory ;-) Ciao, Dscho ^ permalink raw reply [flat|nested] 82+ messages in thread
* Re: [PATCH 13/14] git-commit-tree: make it usable from other builtins 2008-07-01 2:22 ` [PATCH 13/14] git-commit-tree: make it usable from other builtins Miklos Vajna 2008-07-01 5:07 ` Johannes Schindelin @ 2008-07-01 5:50 ` Junio C Hamano 2008-07-01 12:09 ` Miklos Vajna 1 sibling, 1 reply; 82+ messages in thread From: Junio C Hamano @ 2008-07-01 5:50 UTC (permalink / raw To: Miklos Vajna; +Cc: git, Johannes Schindelin, Olivier Marin Miklos Vajna <vmiklos@frugalware.org> writes: > This patch is against master, you need to pull from me or first rebase > mv/merge-in-c against current master. That's a bad practice. It makes it much harder to review the incremental changes from the previous round. I'll cope, though. ^ permalink raw reply [flat|nested] 82+ messages in thread
* Re: [PATCH 13/14] git-commit-tree: make it usable from other builtins 2008-07-01 5:50 ` Junio C Hamano @ 2008-07-01 12:09 ` Miklos Vajna 0 siblings, 0 replies; 82+ messages in thread From: Miklos Vajna @ 2008-07-01 12:09 UTC (permalink / raw To: Junio C Hamano; +Cc: git, Johannes Schindelin, Olivier Marin [-- Attachment #1: Type: text/plain, Size: 592 bytes --] On Mon, Jun 30, 2008 at 10:50:49PM -0700, Junio C Hamano <gitster@pobox.com> wrote: > Miklos Vajna <vmiklos@frugalware.org> writes: > > > This patch is against master, you need to pull from me or first rebase > > mv/merge-in-c against current master. > > That's a bad practice. It makes it much harder to review the incremental > changes from the previous round. Sorry. I just did this way because in my patch I wanted to let cmd_commit_tree() use a commit_list and I saw Dscho already did it, and I saw that the patch is already in master. > I'll cope, though. Thanks. [-- Attachment #2: Type: application/pgp-signature, Size: 197 bytes --] ^ permalink raw reply [flat|nested] 82+ messages in thread
* [PATCH 14/14] Build in merge 2008-07-01 2:13 ` Miklos Vajna 2008-07-01 2:22 ` [PATCH 13/14] git-commit-tree: make it usable from other builtins Miklos Vajna @ 2008-07-01 2:22 ` Miklos Vajna 2008-07-01 2:37 ` [PATCH 00/14] " Miklos Vajna 1 sibling, 1 reply; 82+ messages in thread From: Miklos Vajna @ 2008-07-01 2:22 UTC (permalink / raw To: Junio C Hamano; +Cc: git, Johannes Schindelin, Olivier Marin Mentored-by: Johannes Schindelin <Johannes.Schindelin@gmx.de> Signed-off-by: Miklos Vajna <vmiklos@frugalware.org> --- Interdiff since the previous version: git diff fab9109..b86e8ad Individual commits: git log b86e8ad Makefile | 2 +- builtin-merge.c | 1148 +++++++++++++++++++++++++ builtin.h | 1 + git-merge.sh => contrib/examples/git-merge.sh | 0 git.c | 1 + t/t7602-merge-octopus-many.sh | 2 +- 6 files changed, 1152 insertions(+), 2 deletions(-) create mode 100644 builtin-merge.c rename git-merge.sh => contrib/examples/git-merge.sh (100%) diff --git a/Makefile b/Makefile index bf77292..fbc53e9 100644 --- a/Makefile +++ b/Makefile @@ -240,7 +240,6 @@ SCRIPT_SH += git-lost-found.sh SCRIPT_SH += git-merge-octopus.sh SCRIPT_SH += git-merge-one-file.sh SCRIPT_SH += git-merge-resolve.sh -SCRIPT_SH += git-merge.sh SCRIPT_SH += git-merge-stupid.sh SCRIPT_SH += git-mergetool.sh SCRIPT_SH += git-parse-remote.sh @@ -515,6 +514,7 @@ BUILTIN_OBJS += builtin-ls-remote.o BUILTIN_OBJS += builtin-ls-tree.o BUILTIN_OBJS += builtin-mailinfo.o BUILTIN_OBJS += builtin-mailsplit.o +BUILTIN_OBJS += builtin-merge.o BUILTIN_OBJS += builtin-merge-base.o BUILTIN_OBJS += builtin-merge-file.o BUILTIN_OBJS += builtin-merge-ours.o diff --git a/builtin-merge.c b/builtin-merge.c new file mode 100644 index 0000000..bb8d985 --- /dev/null +++ b/builtin-merge.c @@ -0,0 +1,1148 @@ +/* + * Builtin "git merge" + * + * Copyright (c) 2008 Miklos Vajna <vmiklos@frugalware.org> + * + * Based on git-merge.sh by Junio C Hamano. + */ + +#include "cache.h" +#include "parse-options.h" +#include "builtin.h" +#include "run-command.h" +#include "path-list.h" +#include "diff.h" +#include "refs.h" +#include "commit.h" +#include "diffcore.h" +#include "revision.h" +#include "unpack-trees.h" +#include "cache-tree.h" +#include "dir.h" +#include "utf8.h" +#include "log-tree.h" +#include "color.h" + +enum strategy { + DEFAULT_TWOHEAD = 1, + DEFAULT_OCTOPUS = 2, + NO_FAST_FORWARD = 4, + NO_TRIVIAL = 8 +}; + +static const char * const builtin_merge_usage[] = { + "git-merge [options] <remote>...", + "git-merge [options] <msg> HEAD <remote>", + NULL +}; + +static int show_diffstat = 1, option_log, squash; +static int option_commit = 1, allow_fast_forward = 1; +static int allow_trivial = 1, have_message; +static struct strbuf merge_msg; +static struct commit_list *remoteheads; +static unsigned char head[20], stash[20]; +static struct path_list use_strategies; +static const char *branch; + +static struct path_list_item strategy_items[] = { + { "recur", (void *)NO_TRIVIAL }, + { "recursive", (void *)(DEFAULT_TWOHEAD | NO_TRIVIAL) }, + { "octopus", (void *)DEFAULT_OCTOPUS }, + { "resolve", (void *)0 }, + { "stupid", (void *)0 }, + { "ours", (void *)(NO_FAST_FORWARD | NO_TRIVIAL) }, + { "subtree", (void *)(NO_FAST_FORWARD | NO_TRIVIAL) }, +}; +static struct path_list strategies = { strategy_items, + ARRAY_SIZE(strategy_items), 0, 0 }; + +static const char *pull_twohead, *pull_octopus; + +static int option_parse_message(const struct option *opt, + const char *arg, int unset) +{ + struct strbuf *buf = opt->value; + + if (unset) + strbuf_setlen(buf, 0); + else { + strbuf_addf(buf, "%s\n\n", arg); + have_message = 1; + } + return 0; +} + +static struct path_list_item *unsorted_path_list_lookup(const char *path, + struct path_list *list) +{ + int i; + + if (!path) + return NULL; + + for (i = 0; i < list->nr; i++) + if (!strcmp(path, list->items[i].path)) + return &list->items[i]; + return NULL; +} + +static inline void path_list_append_strategy(struct path_list_item *item) +{ + path_list_append(item->path, &use_strategies)->util = item->util; +} + +static int option_parse_strategy(const struct option *opt, + const char *arg, int unset) +{ + int i; + struct path_list_item *item = unsorted_path_list_lookup(arg, &strategies); + + if (unset) + return 0; + + if (item) + path_list_append_strategy(item); + else { + struct strbuf err; + strbuf_init(&err, 0); + for (i = 0; i < strategies.nr; i++) + strbuf_addf(&err, " %s", strategies.items[i].path); + fprintf(stderr, "Could not find merge strategy '%s'.\n", arg); + fprintf(stderr, "Available strategies are:%s.\n", err.buf); + exit(1); + } + return 0; +} + +static int option_parse_n(const struct option *opt, + const char *arg, int unset) +{ + show_diffstat = unset; + return 0; +} + +static struct option builtin_merge_options[] = { + { OPTION_CALLBACK, 'n', NULL, NULL, NULL, + "do not show a diffstat at the end of the merge", + PARSE_OPT_NOARG, option_parse_n }, + OPT_BOOLEAN(0, "stat", &show_diffstat, + "show a diffstat at the end of the merge"), + OPT_BOOLEAN(0, "summary", &show_diffstat, "(synonym to --stat)"), + OPT_BOOLEAN(0, "log", &option_log, + "add list of one-line log to merge commit message"), + OPT_BOOLEAN(0, "squash", &squash, + "create a single commit instead of doing a merge"), + OPT_BOOLEAN(0, "commit", &option_commit, + "perform a commit if the merge succeeds (default)"), + OPT_BOOLEAN(0, "ff", &allow_fast_forward, + "allow fast forward (default)"), + OPT_CALLBACK('s', "strategy", &use_strategies, "strategy", + "merge strategy to use", option_parse_strategy), + OPT_CALLBACK('m', "message", &merge_msg, "message", + "message to be used for the merge commit (if any)", + option_parse_message), + OPT_END() +}; + +/* Cleans up metadata that is uninteresting after a succeeded merge. */ +static void drop_save(void) +{ + unlink(git_path("MERGE_HEAD")); + unlink(git_path("MERGE_MSG")); +} + +static void save_state(void) +{ + int len; + struct child_process cp; + struct strbuf buffer = STRBUF_INIT; + const char *argv[] = {"stash", "create", NULL}; + + memset(&cp, 0, sizeof(cp)); + cp.argv = argv; + cp.out = -1; + cp.git_cmd = 1; + + if (start_command(&cp)) + die("could not run stash."); + len = strbuf_read(&buffer, cp.out, 1024); + close(cp.out); + + if (finish_command(&cp) || len < 0) + die("stash failed"); + else if (!len) + return; + strbuf_setlen(&buffer, buffer.len-1); + if (get_sha1(buffer.buf, stash)) + die("not a valid object: %s", buffer.buf); +} + +static void reset_hard(unsigned const char *sha1, int verbose) +{ + int i = 0; + const char *args[6]; + + args[i++] = "read-tree"; + if (verbose) + args[i++] = "-v"; + args[i++] = "--reset"; + args[i++] = "-u"; + args[i++] = sha1_to_hex(sha1); + args[i] = NULL; + + if (run_command_v_opt(args, RUN_GIT_CMD)) + die("read-tree failed"); +} + +static void restore_state(void) +{ + struct strbuf sb; + const char *args[] = { "stash", "apply", NULL, NULL }; + + if (is_null_sha1(stash)) + return; + + reset_hard(head, 1); + + strbuf_init(&sb, 0); + args[2] = sha1_to_hex(stash); + + /* + * It is OK to ignore error here, for example when there was + * nothing to restore. + */ + run_command_v_opt(args, RUN_GIT_CMD); + + strbuf_release(&sb); + refresh_cache(REFRESH_QUIET); +} + +/* This is called when no merge was necessary. */ +static void finish_up_to_date(const char *msg) +{ + printf("%s%s\n", squash ? " (nothing to squash)" : "", msg); + drop_save(); +} + +static void squash_message(void) +{ + struct rev_info rev; + struct commit *commit; + struct strbuf out; + struct commit_list *j; + int fd; + + printf("Squash commit -- not updating HEAD\n"); + fd = open(git_path("SQUASH_MSG"), O_WRONLY | O_CREAT, 0666); + if (fd < 0) + die("Could not write to %s", git_path("SQUASH_MSG")); + + init_revisions(&rev, NULL); + rev.ignore_merges = 1; + rev.commit_format = CMIT_FMT_MEDIUM; + + commit = lookup_commit(head); + commit->object.flags |= UNINTERESTING; + add_pending_object(&rev, &commit->object, NULL); + + for (j = remoteheads; j; j = j->next) + add_pending_object(&rev, &j->item->object, NULL); + + setup_revisions(0, NULL, &rev, NULL); + if (prepare_revision_walk(&rev)) + die("revision walk setup failed"); + + strbuf_init(&out, 0); + strbuf_addstr(&out, "Squashed commit of the following:\n"); + while ((commit = get_revision(&rev)) != NULL) { + strbuf_addch(&out, '\n'); + strbuf_addf(&out, "commit %s\n", + sha1_to_hex(commit->object.sha1)); + pretty_print_commit(rev.commit_format, commit, &out, rev.abbrev, + NULL, NULL, rev.date_mode, 0); + } + write(fd, out.buf, out.len); + close(fd); + strbuf_release(&out); +} + +static int run_hook(const char *name) +{ + struct child_process hook; + const char *argv[3], *env[2]; + char index[PATH_MAX]; + + argv[0] = git_path("hooks/%s", name); + if (access(argv[0], X_OK) < 0) + return 0; + + snprintf(index, sizeof(index), "GIT_INDEX_FILE=%s", get_index_file()); + env[0] = index; + env[1] = NULL; + + if (squash) + argv[1] = "1"; + else + argv[1] = "0"; + argv[2] = NULL; + + memset(&hook, 0, sizeof(hook)); + hook.argv = argv; + hook.no_stdin = 1; + hook.stdout_to_stderr = 1; + hook.env = env; + + return run_command(&hook); +} + +static void finish(const unsigned char *new_head, const char *msg) +{ + struct strbuf reflog_message; + + strbuf_init(&reflog_message, 0); + if (!msg) + strbuf_addstr(&reflog_message, getenv("GIT_REFLOG_ACTION")); + else { + printf("%s\n", msg); + strbuf_addf(&reflog_message, "%s: %s", + getenv("GIT_REFLOG_ACTION"), msg); + } + if (squash) { + squash_message(); + } else { + if (!merge_msg.len) + printf("No merge message -- not updating HEAD\n"); + else { + const char *argv_gc_auto[] = { "gc", "--auto", NULL }; + update_ref(reflog_message.buf, "HEAD", + new_head, head, 0, + DIE_ON_ERR); + /* + * We ignore errors in 'gc --auto', since the + * user should see them. + */ + run_command_v_opt(argv_gc_auto, RUN_GIT_CMD); + } + } + if (new_head && show_diffstat) { + struct diff_options opts; + diff_setup(&opts); + opts.output_format |= + DIFF_FORMAT_SUMMARY | DIFF_FORMAT_DIFFSTAT; + opts.detect_rename = DIFF_DETECT_RENAME; + if (diff_use_color_default > 0) + DIFF_OPT_SET(&opts, COLOR_DIFF); + if (diff_setup_done(&opts) < 0) + die("diff_setup_done failed"); + diff_tree_sha1(head, new_head, "", &opts); + diffcore_std(&opts); + diff_flush(&opts); + } + + /* Run a post-merge hook */ + run_hook("post-merge"); + + strbuf_release(&reflog_message); +} + +/* Get the name for the merge commit's message. */ +static void merge_name(const char *remote, struct strbuf *msg) +{ + struct object *remote_head; + unsigned char branch_head[20], buf_sha[20]; + struct strbuf buf; + const char *ptr; + int len = 0; + + memset(branch_head, 0, sizeof(branch_head)); + remote_head = peel_to_type(remote, 0, NULL, OBJ_COMMIT); + if (!remote_head) + die("'%s' does not point to a commit", remote); + + strbuf_init(&buf, 0); + strbuf_addstr(&buf, "refs/heads/"); + strbuf_addstr(&buf, remote); + resolve_ref(buf.buf, branch_head, 0, 0); + + if (!hashcmp(remote_head->sha1, branch_head)) { + strbuf_addf(msg, "%s\t\tbranch '%s' of .\n", + sha1_to_hex(branch_head), remote); + return; + } + /* See if remote matches <name>~<number>, or <name>^ */ + ptr = strrchr(remote, '^'); + if (ptr && ptr[1] == '\0') { + for (len = 0, ptr = remote + strlen(remote); + remote < ptr && ptr[-1] == '^'; + ptr--) + len++; + } + else { + ptr = strrchr(remote, '~'); + if (ptr && ptr[1] != '0' && isdigit(ptr[1])) { + len = ptr-remote; + ptr++; + for (ptr++; *ptr; ptr++) + if (!isdigit(*ptr)) { + len = 0; + break; + } + } + } + if (len) { + struct strbuf truname = STRBUF_INIT; + strbuf_addstr(&truname, "refs/heads/"); + strbuf_addstr(&truname, remote); + strbuf_setlen(&truname, len+11); + if (resolve_ref(truname.buf, buf_sha, 0, 0)) { + strbuf_addf(msg, + "%s\t\tbranch '%s' (early part) of .\n", + sha1_to_hex(remote_head->sha1), truname.buf); + return; + } + } + + if (!strcmp(remote, "FETCH_HEAD") && + !access(git_path("FETCH_HEAD"), R_OK)) { + FILE *fp; + struct strbuf line; + char *ptr; + + strbuf_init(&line, 0); + fp = fopen(git_path("FETCH_HEAD"), "r"); + if (!fp) + die("could not open %s for reading: %s", + git_path("FETCH_HEAD"), strerror(errno)); + strbuf_getline(&line, fp, '\n'); + fclose(fp); + ptr = strstr(line.buf, "\tnot-for-merge\t"); + if (ptr) + strbuf_remove(&line, ptr-line.buf+1, 13); + strbuf_addbuf(msg, &line); + strbuf_release(&line); + return; + } + strbuf_addf(msg, "%s\t\tcommit '%s'\n", + sha1_to_hex(remote_head->sha1), remote); +} + +int git_merge_config(const char *k, const char *v, void *cb) +{ + if (branch && !prefixcmp(k, "branch.") && + !prefixcmp(k + 7, branch) && + !strcmp(k + 7 + strlen(branch), ".mergeoptions")) { + const char **argv; + int argc; + char *buf; + + buf = xstrdup(v); + argc = split_cmdline(buf, &argv); + argv = xrealloc(argv, sizeof(*argv) * (argc + 2)); + memmove(argv + 1, argv, sizeof(*argv) * (argc + 1)); + argc++; + parse_options(argc, argv, builtin_merge_options, + builtin_merge_usage, 0); + free(buf); + } + + if (!strcmp(k, "merge.diffstat") || !strcmp(k, "merge.stat")) + show_diffstat = git_config_bool(k, v); + else if (!strcmp(k, "pull.twohead")) + return git_config_string(&pull_twohead, k, v); + else if (!strcmp(k, "pull.octopus")) + return git_config_string(&pull_octopus, k, v); + return git_diff_ui_config(k, v, cb); +} + +static int read_tree_trivial(unsigned char *common, unsigned char *head, + unsigned char *one) +{ + int i, nr_trees = 0; + struct tree *trees[MAX_UNPACK_TREES]; + struct tree_desc t[MAX_UNPACK_TREES]; + struct unpack_trees_options opts; + + memset(&opts, 0, sizeof(opts)); + opts.head_idx = 2; + opts.src_index = &the_index; + opts.dst_index = &the_index; + opts.update = 1; + opts.verbose_update = 1; + opts.trivial_merges_only = 1; + opts.merge = 1; + trees[nr_trees] = parse_tree_indirect(common); + if (!trees[nr_trees++]) + return -1; + trees[nr_trees] = parse_tree_indirect(head); + if (!trees[nr_trees++]) + return -1; + trees[nr_trees] = parse_tree_indirect(one); + if (!trees[nr_trees++]) + return -1; + opts.fn = threeway_merge; + cache_tree_free(&active_cache_tree); + opts.head_idx = 2; + for (i = 0; i < nr_trees; i++) { + parse_tree(trees[i]); + init_tree_desc(t+i, trees[i]->buffer, trees[i]->size); + } + if (unpack_trees(nr_trees, t, &opts)) + return -1; + return 0; +} + +static void write_tree_trivial(unsigned char *sha1) +{ + if (write_cache_as_tree(sha1, 0, NULL)) + die("git write-tree failed to write a tree"); +} + +static int try_merge_strategy(char *strategy, struct commit_list *common, + const char *head_arg) +{ + const char **args; + int i = 0, ret; + struct commit_list *j; + struct strbuf buf; + + args = xmalloc((4 + commit_list_count(common) + + commit_list_count(remoteheads)) * sizeof(char *)); + strbuf_init(&buf, 0); + strbuf_addf(&buf, "merge-%s", strategy); + args[i++] = buf.buf; + for (j = common; j; j = j->next) + args[i++] = xstrdup(sha1_to_hex(j->item->object.sha1)); + args[i++] = "--"; + args[i++] = head_arg; + for (j = remoteheads; j; j = j->next) + args[i++] = xstrdup(sha1_to_hex(j->item->object.sha1)); + args[i] = NULL; + ret = run_command_v_opt(args, RUN_GIT_CMD); + strbuf_release(&buf); + i = 1; + for (j = common; j; j = j->next) + free((void *)args[i++]); + i += 2; + for (j = remoteheads; j; j = j->next) + free((void *)args[i++]); + free(args); + return -ret; +} + +static void count_diff_files(struct diff_queue_struct *q, + struct diff_options *opt, void *data) +{ + int *count = data; + + (*count) += q->nr; +} + +static int count_unmerged_entries(void) +{ + const struct index_state *state = &the_index; + int i, ret = 0; + + for (i = 0; i < state->cache_nr; i++) + if (ce_stage(state->cache[i])) + ret++; + + return ret; +} + +static int checkout_fast_forward(unsigned char *head, unsigned char *remote) +{ + struct tree *trees[MAX_UNPACK_TREES]; + struct unpack_trees_options opts; + struct tree_desc t[MAX_UNPACK_TREES]; + int i, fd, nr_trees = 0; + struct dir_struct dir; + struct lock_file *lock_file = xcalloc(1, sizeof(struct lock_file)); + + if (read_cache_unmerged()) + die("you need to resolve your current index first"); + + fd = hold_locked_index(lock_file, 1); + + memset(&trees, 0, sizeof(trees)); + memset(&opts, 0, sizeof(opts)); + memset(&t, 0, sizeof(t)); + dir.show_ignored = 1; + dir.exclude_per_dir = ".gitignore"; + opts.dir = &dir; + + opts.head_idx = 1; + opts.src_index = &the_index; + opts.dst_index = &the_index; + opts.update = 1; + opts.verbose_update = 1; + opts.merge = 1; + opts.fn = twoway_merge; + + trees[nr_trees] = parse_tree_indirect(head); + if (!trees[nr_trees++]) + return -1; + trees[nr_trees] = parse_tree_indirect(remote); + if (!trees[nr_trees++]) + return -1; + for (i = 0; i < nr_trees; i++) { + parse_tree(trees[i]); + init_tree_desc(t+i, trees[i]->buffer, trees[i]->size); + } + if (unpack_trees(nr_trees, t, &opts)) + return -1; + if (write_cache(fd, active_cache, active_nr) || + commit_locked_index(lock_file)) + die("unable to write new index file"); + return 0; +} + +static void split_merge_strategies(const char *string, struct path_list *list) +{ + char *p, *q, *buf; + + if (!string) + return; + + list->strdup_paths = 1; + buf = xstrdup(string); + q = buf; + for (;;) { + p = strchr(q, ' '); + if (!p) { + path_list_append(q, list); + free(buf); + return; + } else { + *p = '\0'; + path_list_append(q, list); + q = ++p; + } + } +} + +static void add_strategies(const char *string, enum strategy strategy) +{ + struct path_list list; + int i; + + memset(&list, 0, sizeof(list)); + split_merge_strategies(string, &list); + if (list.nr) { + for (i = 0; i < list.nr; i++) { + struct path_list_item *item; + + item = unsorted_path_list_lookup(list.items[i].path, + &strategies); + if (item) + path_list_append_strategy(item); + } + return; + } + for (i = 0; i < strategies.nr; i++) + if ((enum strategy)strategies.items[i].util & strategy) + path_list_append_strategy(&strategies.items[i]); +} + +static int merge_trivial(void) +{ + unsigned char result_tree[20], result_commit[20]; + struct commit_list parent; + + write_tree_trivial(result_tree); + printf("Wonderful.\n"); + parent.item = remoteheads->item; + parent.next = NULL; + commit_tree(merge_msg.buf, result_tree, &parent, result_commit); + finish(result_commit, "In-index merge"); + drop_save(); + return 0; +} + +static int finish_automerge(struct commit_list *common, + unsigned char *result_tree, + struct path_list_item *wt_strategy) +{ + struct commit_list *parents = NULL, *j; + struct strbuf buf = STRBUF_INIT; + unsigned char result_commit[20]; + + free_commit_list(common); + if (allow_fast_forward) { + parents = remoteheads; + commit_list_insert(lookup_commit(head), &parents); + parents = reduce_heads(parents); + } else { + struct commit_list **pptr = &parents; + + pptr = &commit_list_insert(lookup_commit(head), + pptr)->next; + for (j = remoteheads; j; j = j->next) + pptr = &commit_list_insert(j->item, pptr)->next; + } + free_commit_list(remoteheads); + strbuf_addch(&merge_msg, '\n'); + commit_tree(merge_msg.buf, result_tree, parents, result_commit); + strbuf_addf(&buf, "Merge made by %s.", wt_strategy->path); + finish(result_commit, buf.buf); + strbuf_release(&buf); + drop_save(); + return 0; +} + +static int suggest_conflicts(void) +{ + FILE *fp; + int pos; + + fp = fopen(git_path("MERGE_MSG"), "a"); + if (!fp) + die("Could open %s for writing", git_path("MERGE_MSG")); + fprintf(fp, "\nConflicts:\n"); + for (pos = 0; pos < active_nr; pos++) { + struct cache_entry *ce = active_cache[pos]; + + if (ce_stage(ce)) { + fprintf(fp, "\t%s\n", ce->name); + while (pos + 1 < active_nr && + !strcmp(ce->name, + active_cache[pos + 1]->name)) + pos++; + } + } + fclose(fp); + rerere(); + printf("Automatic merge failed; " + "fix conflicts and then commit the result.\n"); + return 1; +} + +static inline unsigned nth_strategy_flags(struct path_list *s, int nth) +{ + return (unsigned) s->items[nth].util; +} + +static struct commit *is_old_style_invocation(int argc, const char **argv) +{ + struct commit *second_token = NULL; + if (argc > 1) { + unsigned char second_sha1[20]; + + if (get_sha1(argv[1], second_sha1)) + return NULL; + second_token = lookup_commit_reference_gently(second_sha1, 0); + if (!second_token) + die("'%s' is not a commit", argv[1]); + if (hashcmp(second_token->object.sha1, head)) + return NULL; + } + return second_token; +} + +static int evaluate_result(void) +{ + int cnt = 0; + struct rev_info rev; + + if (read_cache() < 0) + die("failed to read the cache"); + + /* Check how many files differ. */ + init_revisions(&rev, ""); + setup_revisions(0, NULL, &rev, NULL); + rev.diffopt.output_format |= + DIFF_FORMAT_CALLBACK; + rev.diffopt.format_callback = count_diff_files; + rev.diffopt.format_callback_data = &cnt; + run_diff_files(&rev, 0); + + /* + * Check how many unmerged entries are + * there. + */ + cnt += count_unmerged_entries(); + + return cnt; +} + +int cmd_merge(int argc, const char **argv, const char *prefix) +{ + unsigned char result_tree[20]; + struct strbuf buf; + const char *head_arg; + int flag, head_invalid = 0, i; + int best_cnt = -1, merge_was_ok = 0, automerge_was_ok = 0; + struct commit_list *common = NULL; + struct path_list_item *best_strategy = NULL, *wt_strategy = NULL; + struct commit_list **remotes = &remoteheads; + + setup_work_tree(); + if (unmerged_cache()) + die("You are in the middle of a conflicted merge."); + + /* + * Check if we are _not_ on a detached HEAD, i.e. if there is a + * current branch. + */ + branch = resolve_ref("HEAD", head, 0, &flag); + if (branch && flag & REF_ISSYMREF) { + const char *ptr = skip_prefix(branch, "refs/heads/"); + if (ptr) + branch = ptr; + } else + head_invalid = 1; + + git_config(git_merge_config, NULL); + + /* for color.ui */ + if (diff_use_color_default == -1) + diff_use_color_default = git_use_color_default; + + argc = parse_options(argc, argv, builtin_merge_options, + builtin_merge_usage, 0); + + if (squash) { + if (!allow_fast_forward) + die("You cannot combine --squash with --no-ff."); + option_commit = 0; + } + + if (!argc) + usage_with_options(builtin_merge_usage, + builtin_merge_options); + + /* + * This could be traditional "merge <msg> HEAD <commit>..." and + * the way we can tell it is to see if the second token is HEAD, + * but some people might have misused the interface and used a + * committish that is the same as HEAD there instead. + * Traditional format never would have "-m" so it is an + * additional safety measure to check for it. + */ + strbuf_init(&buf, 0); + + if (!have_message && is_old_style_invocation(argc, argv)) { + strbuf_addstr(&merge_msg, argv[0]); + head_arg = argv[1]; + argv += 2; + argc -= 2; + } else if (head_invalid) { + struct object *remote_head; + /* + * If the merged head is a valid one there is no reason + * to forbid "git merge" into a branch yet to be born. + * We do the same for "git pull". + */ + if (argc != 1) + die("Can merge only exactly one commit into " + "empty head"); + remote_head = peel_to_type(argv[0], 0, NULL, OBJ_COMMIT); + if (!remote_head) + die("%s - not something we can merge", argv[0]); + update_ref("initial pull", "HEAD", remote_head->sha1, NULL, 0, + DIE_ON_ERR); + reset_hard(remote_head->sha1, 0); + return 0; + } else { + struct strbuf msg; + + /* We are invoked directly as the first-class UI. */ + head_arg = "HEAD"; + + /* + * All the rest are the commits being merged; + * prepare the standard merge summary message to + * be appended to the given message. If remote + * is invalid we will die later in the common + * codepath so we discard the error in this + * loop. + */ + strbuf_init(&msg, 0); + for (i = 0; i < argc; i++) + merge_name(argv[i], &msg); + fmt_merge_msg(option_log, &msg, &merge_msg); + if (merge_msg.len) + strbuf_setlen(&merge_msg, merge_msg.len-1); + } + + if (head_invalid || !argc) + usage_with_options(builtin_merge_usage, + builtin_merge_options); + + strbuf_addstr(&buf, "merge"); + for (i = 0; i < argc; i++) + strbuf_addf(&buf, " %s", argv[i]); + setenv("GIT_REFLOG_ACTION", buf.buf, 0); + strbuf_reset(&buf); + + for (i = 0; i < argc; i++) { + struct object *o; + + o = peel_to_type(argv[i], 0, NULL, OBJ_COMMIT); + if (!o) + die("%s - not something we can merge", argv[i]); + remotes = &commit_list_insert(lookup_commit(o->sha1), + remotes)->next; + + strbuf_addf(&buf, "GITHEAD_%s", sha1_to_hex(o->sha1)); + setenv(buf.buf, argv[i], 1); + strbuf_reset(&buf); + } + + if (!use_strategies.nr) { + if (!remoteheads->next) + add_strategies(pull_twohead, DEFAULT_TWOHEAD); + else + add_strategies(pull_octopus, DEFAULT_OCTOPUS); + } + + for (i = 0; i < use_strategies.nr; i++) { + if (nth_strategy_flags(&use_strategies, i) & NO_FAST_FORWARD) + allow_fast_forward = 0; + if (nth_strategy_flags(&use_strategies, i) & NO_TRIVIAL) + allow_trivial = 0; + } + + if (!remoteheads->next) + common = get_merge_bases(lookup_commit(head), + remoteheads->item, 1); + else { + struct commit_list *list = remoteheads; + commit_list_insert(lookup_commit(head), &list); + common = get_octopus_merge_bases(list); + free(list); + } + + update_ref("updating ORIG_HEAD", "ORIG_HEAD", head, NULL, 0, + DIE_ON_ERR); + + if (!common) + ; /* No common ancestors found. We need a real merge. */ + else if (!remoteheads->next && !common->next && + common->item == remoteheads->item) { + /* + * If head can reach all the merge then we are up to date. + * but first the most common case of merging one remote. + */ + finish_up_to_date("Already up-to-date."); + return 0; + } else if (allow_fast_forward && !remoteheads->next && + !common->next && + !hashcmp(common->item->object.sha1, head)) { + /* Again the most common case of merging one remote. */ + struct strbuf msg; + struct object *o; + char hex[41]; + + strcpy(hex, find_unique_abbrev(head, DEFAULT_ABBREV)); + + printf("Updating %s..%s\n", + hex, + find_unique_abbrev(remoteheads->item->object.sha1, + DEFAULT_ABBREV)); + refresh_cache(REFRESH_QUIET); + strbuf_init(&msg, 0); + strbuf_addstr(&msg, "Fast forward"); + if (have_message) + strbuf_addstr(&msg, + " (no commit created; -m option ignored)"); + o = peel_to_type(sha1_to_hex(remoteheads->item->object.sha1), + 0, NULL, OBJ_COMMIT); + if (!o) + return 1; + + if (checkout_fast_forward(head, remoteheads->item->object.sha1)) + return 1; + + finish(o->sha1, msg.buf); + drop_save(); + return 0; + } else if (!remoteheads->next && common->next) + ; + /* + * We are not doing octopus and not fast forward. Need + * a real merge. + */ + else if (!remoteheads->next && !common->next && option_commit) { + /* + * We are not doing octopus, not fast forward, and have + * only one common. + */ + refresh_cache(REFRESH_QUIET); + if (allow_trivial) { + /* See if it is really trivial. */ + git_committer_info(IDENT_ERROR_ON_NO_NAME); + printf("Trying really trivial in-index merge...\n"); + if (!read_tree_trivial(common->item->object.sha1, + head, remoteheads->item->object.sha1)) + return merge_trivial(); + printf("Nope.\n"); + } + } else { + /* + * An octopus. If we can reach all the remote we are up + * to date. + */ + int up_to_date = 1; + struct commit_list *j; + + for (j = remoteheads; j; j = j->next) { + struct commit_list *common_one; + + /* + * Here we *have* to calculate the individual + * merge_bases again, otherwise "git merge HEAD^ + * HEAD^^" would be missed. + */ + common_one = get_merge_bases(lookup_commit(head), + j->item, 1); + if (hashcmp(common_one->item->object.sha1, + j->item->object.sha1)) { + up_to_date = 0; + break; + } + } + if (up_to_date) { + finish_up_to_date("Already up-to-date. Yeeah!"); + return 0; + } + } + + /* We are going to make a new commit. */ + git_committer_info(IDENT_ERROR_ON_NO_NAME); + + /* + * At this point, we need a real merge. No matter what strategy + * we use, it would operate on the index, possibly affecting the + * working tree, and when resolved cleanly, have the desired + * tree in the index -- this means that the index must be in + * sync with the head commit. The strategies are responsible + * to ensure this. + */ + if (use_strategies.nr != 1) { + /* + * Stash away the local changes so that we can try more + * than one. + */ + save_state(); + } else { + memcpy(stash, null_sha1, 20); + } + + for (i = 0; i < use_strategies.nr; i++) { + int ret; + if (i) { + printf("Rewinding the tree to pristine...\n"); + restore_state(); + } + if (use_strategies.nr != 1) + printf("Trying merge strategy %s...\n", + use_strategies.items[i].path); + /* + * Remember which strategy left the state in the working + * tree. + */ + wt_strategy = &use_strategies.items[i]; + + ret = try_merge_strategy(use_strategies.items[i].path, + common, head_arg); + if (!option_commit && !ret) { + merge_was_ok = 1; + /* + * This is necessary here just to avoid writing + * the tree, but later we will *not* exit with + * status code 1 because merge_was_ok is set. + */ + ret = 1; + } + + if (ret) { + /* + * The backend exits with 1 when conflicts are + * left to be resolved, with 2 when it does not + * handle the given merge at all. + */ + if (ret == 1) { + int cnt = evaluate_result(); + + if (best_cnt <= 0 || cnt <= best_cnt) { + best_strategy = + &use_strategies.items[i]; + best_cnt = cnt; + } + } + if (merge_was_ok) + break; + else + continue; + } + + /* Automerge succeeded. */ + write_tree_trivial(result_tree); + automerge_was_ok = 1; + break; + } + + /* + * If we have a resulting tree, that means the strategy module + * auto resolved the merge cleanly. + */ + if (automerge_was_ok) + return finish_automerge(common, result_tree, wt_strategy); + + /* + * Pick the result from the best strategy and have the user fix + * it up. + */ + if (!best_strategy) { + restore_state(); + if (use_strategies.nr > 1) + fprintf(stderr, + "No merge strategy handled the merge.\n"); + else + fprintf(stderr, "Merge with strategy %s failed.\n", + use_strategies.items[0].path); + return 2; + } else if (best_strategy == wt_strategy) + ; /* We already have its result in the working tree. */ + else { + printf("Rewinding the tree to pristine...\n"); + restore_state(); + printf("Using the %s to prepare resolving by hand.\n", + best_strategy->path); + try_merge_strategy(best_strategy->path, common, head_arg); + } + + if (squash) + finish(NULL, NULL); + else { + int fd; + struct commit_list *j; + + for (j = remoteheads; j; j = j->next) + strbuf_addf(&buf, "%s\n", + sha1_to_hex(j->item->object.sha1)); + fd = open(git_path("MERGE_HEAD"), O_WRONLY | O_CREAT, 0666); + if (fd < 0) + die("Could open %s for writing", + git_path("MERGE_HEAD")); + if (write_in_full(fd, buf.buf, buf.len) != buf.len) + die("Could not write to %s", git_path("MERGE_HEAD")); + close(fd); + strbuf_addch(&merge_msg, '\n'); + fd = open(git_path("MERGE_MSG"), O_WRONLY | O_CREAT, 0666); + if (fd < 0) + die("Could open %s for writing", git_path("MERGE_MSG")); + if (write_in_full(fd, merge_msg.buf, merge_msg.len) != + merge_msg.len) + die("Could not write to %s", git_path("MERGE_MSG")); + close(fd); + } + + if (merge_was_ok) { + fprintf(stderr, "Automatic merge went well; " + "stopped before committing as requested\n"); + return 0; + } else + return suggest_conflicts(); +} diff --git a/builtin.h b/builtin.h index 05ee56f..0e605d4 100644 --- a/builtin.h +++ b/builtin.h @@ -64,6 +64,7 @@ extern int cmd_ls_tree(int argc, const char **argv, const char *prefix); extern int cmd_ls_remote(int argc, const char **argv, const char *prefix); extern int cmd_mailinfo(int argc, const char **argv, const char *prefix); extern int cmd_mailsplit(int argc, const char **argv, const char *prefix); +extern int cmd_merge(int argc, const char **argv, const char *prefix); extern int cmd_merge_base(int argc, const char **argv, const char *prefix); extern int cmd_merge_ours(int argc, const char **argv, const char *prefix); extern int cmd_merge_file(int argc, const char **argv, const char *prefix); diff --git a/git-merge.sh b/contrib/examples/git-merge.sh similarity index 100% rename from git-merge.sh rename to contrib/examples/git-merge.sh diff --git a/git.c b/git.c index 2fbe96b..770aadd 100644 --- a/git.c +++ b/git.c @@ -271,6 +271,7 @@ static void handle_internal_command(int argc, const char **argv) { "ls-remote", cmd_ls_remote }, { "mailinfo", cmd_mailinfo }, { "mailsplit", cmd_mailsplit }, + { "merge", cmd_merge, RUN_SETUP | NEED_WORK_TREE }, { "merge-base", cmd_merge_base, RUN_SETUP }, { "merge-file", cmd_merge_file }, { "merge-ours", cmd_merge_ours, RUN_SETUP }, diff --git a/t/t7602-merge-octopus-many.sh b/t/t7602-merge-octopus-many.sh index f3a4bb2..fcb8285 100755 --- a/t/t7602-merge-octopus-many.sh +++ b/t/t7602-merge-octopus-many.sh @@ -23,7 +23,7 @@ test_expect_success 'setup' ' done ' -test_expect_failure 'merge c1 with c2, c3, c4, ... c29' ' +test_expect_success 'merge c1 with c2, c3, c4, ... c29' ' git reset --hard c1 && i=2 && refs="" && -- 1.5.6.1 ^ permalink raw reply related [flat|nested] 82+ messages in thread
* [PATCH 00/14] Build in merge 2008-07-01 2:22 ` [PATCH 14/14] Build in merge Miklos Vajna @ 2008-07-01 2:37 ` Miklos Vajna 2008-07-01 2:37 ` [PATCH 08/14] Add new test to ensure git-merge handles more than 25 refs Miklos Vajna 0 siblings, 1 reply; 82+ messages in thread From: Miklos Vajna @ 2008-07-01 2:37 UTC (permalink / raw To: Junio C Hamano; +Cc: git, Johannes Schindelin, Olivier Marin Sorry for the resend, but I again squashed an unrelated hunk to the last patch, so I'm sending the changed patches: 08: Add new test to ensure git-merge handles more than 25 refs. 13: git-commit-tree: make it usable from other builtins 14: Build in merge I really hope I won't overlook this next time. The interdiff from the previous version is empty, I just moved that unrelated hunk from patch 13 to patch 08. Junio C Hamano (2): Introduce get_merge_bases_many() Introduce reduce_heads() Miklos Vajna (12): Move split_cmdline() to alias.c Move commit_list_count() to commit.c Move parse-options's skip_prefix() to git-compat-util.h Add new test to ensure git-merge handles pull.twohead and pull.octopus Move read_cache_unmerged() to read-cache.c git-fmt-merge-msg: make it usable from other builtins Introduce get_octopus_merge_bases() in commit.c Add new test to ensure git-merge handles more than 25 refs. Add new test case to ensure git-merge reduces octopus parents when possible Add new test case to ensure git-merge prepends the custom merge message git-commit-tree: make it usable from other builtins Build in merge Makefile | 2 +- alias.c | 54 ++ builtin-commit-tree.c | 71 +- builtin-fmt-merge-msg.c | 155 ++-- builtin-merge-recursive.c | 8 - builtin-merge.c | 1148 +++++++++++++++++++++++++ builtin-read-tree.c | 24 - builtin-remote.c | 39 +- builtin.h | 8 + cache.h | 3 + commit.c | 136 +++- commit.h | 4 + git-merge.sh => contrib/examples/git-merge.sh | 0 git-compat-util.h | 6 + git.c | 54 +-- parse-options.c | 6 - read-cache.c | 31 + t/t7601-merge-pull-config.sh | 129 +++ t/t7602-merge-octopus-many.sh | 52 ++ t/t7603-merge-reduce-heads.sh | 63 ++ t/t7604-merge-custom-message.sh | 37 + 21 files changed, 1811 insertions(+), 219 deletions(-) create mode 100644 builtin-merge.c rename git-merge.sh => contrib/examples/git-merge.sh (100%) create mode 100755 t/t7601-merge-pull-config.sh create mode 100755 t/t7602-merge-octopus-many.sh create mode 100755 t/t7603-merge-reduce-heads.sh create mode 100755 t/t7604-merge-custom-message.sh ^ permalink raw reply [flat|nested] 82+ messages in thread
* [PATCH 08/14] Add new test to ensure git-merge handles more than 25 refs. 2008-07-01 2:37 ` [PATCH 00/14] " Miklos Vajna @ 2008-07-01 2:37 ` Miklos Vajna 2008-07-01 2:37 ` [PATCH 13/14] git-commit-tree: make it usable from other builtins Miklos Vajna 0 siblings, 1 reply; 82+ messages in thread From: Miklos Vajna @ 2008-07-01 2:37 UTC (permalink / raw To: Junio C Hamano; +Cc: git, Johannes Schindelin, Olivier Marin The old shell version handled only 25 refs but we no longer have this limitation. Add a test to make sure this limitation will not be introduced again in the future. Signed-off-by: Miklos Vajna <vmiklos@frugalware.org> --- t/t7602-merge-octopus-many.sh | 52 +++++++++++++++++++++++++++++++++++++++++ 1 files changed, 52 insertions(+), 0 deletions(-) create mode 100755 t/t7602-merge-octopus-many.sh diff --git a/t/t7602-merge-octopus-many.sh b/t/t7602-merge-octopus-many.sh new file mode 100755 index 0000000..fcb8285 --- /dev/null +++ b/t/t7602-merge-octopus-many.sh @@ -0,0 +1,52 @@ +#!/bin/sh + +test_description='git-merge + +Testing octopus merge with more than 25 refs.' + +. ./test-lib.sh + +test_expect_success 'setup' ' + echo c0 > c0.c && + git add c0.c && + git commit -m c0 && + git tag c0 && + i=1 && + while test $i -le 30 + do + git reset --hard c0 && + echo c$i > c$i.c && + git add c$i.c && + git commit -m c$i && + git tag c$i && + i=`expr $i + 1` || return 1 + done +' + +test_expect_success 'merge c1 with c2, c3, c4, ... c29' ' + git reset --hard c1 && + i=2 && + refs="" && + while test $i -le 30 + do + refs="$refs c$i" + i=`expr $i + 1` + done + git merge $refs && + test "$(git rev-parse c1)" != "$(git rev-parse HEAD)" && + i=1 && + while test $i -le 30 + do + test "$(git rev-parse c$i)" = "$(git rev-parse HEAD^$i)" && + i=`expr $i + 1` || return 1 + done && + git diff --exit-code && + i=1 && + while test $i -le 30 + do + test -f c$i.c && + i=`expr $i + 1` || return 1 + done +' + +test_done -- 1.5.6.1 ^ permalink raw reply related [flat|nested] 82+ messages in thread
* [PATCH 13/14] git-commit-tree: make it usable from other builtins 2008-07-01 2:37 ` [PATCH 08/14] Add new test to ensure git-merge handles more than 25 refs Miklos Vajna @ 2008-07-01 2:37 ` Miklos Vajna 2008-07-01 2:37 ` [PATCH 14/14] Build in merge Miklos Vajna 0 siblings, 1 reply; 82+ messages in thread From: Miklos Vajna @ 2008-07-01 2:37 UTC (permalink / raw To: Junio C Hamano; +Cc: git, Johannes Schindelin, Olivier Marin Move all functionality (except option parsing) from cmd_commit_tree() to commit_tree(), so that other builtins can use it without a child process. Signed-off-by: Miklos Vajna <vmiklos@frugalware.org> --- builtin-commit-tree.c | 71 ++++++++++++++++++++++++++++-------------------- builtin.h | 4 +++ 2 files changed, 45 insertions(+), 30 deletions(-) diff --git a/builtin-commit-tree.c b/builtin-commit-tree.c index 3881f6c..7a9a309 100644 --- a/builtin-commit-tree.c +++ b/builtin-commit-tree.c @@ -45,41 +45,19 @@ static const char commit_utf8_warn[] = "You may want to amend it after fixing the message, or set the config\n" "variable i18n.commitencoding to the encoding your project uses.\n"; -int cmd_commit_tree(int argc, const char **argv, const char *prefix) +int commit_tree(const char *msg, unsigned char *tree, + struct commit_list *parents, unsigned char *ret) { - int i; - struct commit_list *parents = NULL; - unsigned char tree_sha1[20]; - unsigned char commit_sha1[20]; - struct strbuf buffer; int encoding_is_utf8; + struct strbuf buffer; - git_config(git_default_config, NULL); - - if (argc < 2) - usage(commit_tree_usage); - if (get_sha1(argv[1], tree_sha1)) - die("Not a valid object name %s", argv[1]); - - check_valid(tree_sha1, OBJ_TREE); - for (i = 2; i < argc; i += 2) { - unsigned char sha1[20]; - const char *a, *b; - a = argv[i]; b = argv[i+1]; - if (!b || strcmp(a, "-p")) - usage(commit_tree_usage); - - if (get_sha1(b, sha1)) - die("Not a valid object name %s", b); - check_valid(sha1, OBJ_COMMIT); - new_parent(lookup_commit(sha1), &parents); - } + check_valid(tree, OBJ_TREE); /* Not having i18n.commitencoding is the same as having utf-8 */ encoding_is_utf8 = is_encoding_utf8(git_commit_encoding); strbuf_init(&buffer, 8192); /* should avoid reallocs for the headers */ - strbuf_addf(&buffer, "tree %s\n", sha1_to_hex(tree_sha1)); + strbuf_addf(&buffer, "tree %s\n", sha1_to_hex(tree)); /* * NOTE! This ordering means that the same exact tree merged with a @@ -102,14 +80,47 @@ int cmd_commit_tree(int argc, const char **argv, const char *prefix) strbuf_addch(&buffer, '\n'); /* And add the comment */ - if (strbuf_read(&buffer, 0, 0) < 0) - die("git-commit-tree: read returned %s", strerror(errno)); + strbuf_addstr(&buffer, msg); /* And check the encoding */ if (encoding_is_utf8 && !is_utf8(buffer.buf)) fprintf(stderr, commit_utf8_warn); - if (!write_sha1_file(buffer.buf, buffer.len, commit_type, commit_sha1)) { + return write_sha1_file(buffer.buf, buffer.len, commit_type, ret); +} + +int cmd_commit_tree(int argc, const char **argv, const char *prefix) +{ + int i; + struct commit_list *parents = NULL; + unsigned char tree_sha1[20]; + unsigned char commit_sha1[20]; + struct strbuf buffer = STRBUF_INIT; + + git_config(git_default_config, NULL); + + if (argc < 2) + usage(commit_tree_usage); + if (get_sha1(argv[1], tree_sha1)) + die("Not a valid object name %s", argv[1]); + + for (i = 2; i < argc; i += 2) { + unsigned char sha1[20]; + const char *a, *b; + a = argv[i]; b = argv[i+1]; + if (!b || strcmp(a, "-p")) + usage(commit_tree_usage); + + if (get_sha1(b, sha1)) + die("Not a valid object name %s", b); + check_valid(sha1, OBJ_COMMIT); + new_parent(lookup_commit(sha1), &parents); + } + + if (strbuf_read(&buffer, 0, 0) < 0) + die("git-commit-tree: read returned %s", strerror(errno)); + + if (!commit_tree(buffer.buf, tree_sha1, parents, commit_sha1)) { printf("%s\n", sha1_to_hex(commit_sha1)); return 0; } diff --git a/builtin.h b/builtin.h index 2b01fea..05ee56f 100644 --- a/builtin.h +++ b/builtin.h @@ -3,6 +3,8 @@ #include "git-compat-util.h" #include "strbuf.h" +#include "cache.h" +#include "commit.h" extern const char git_version_string[]; extern const char git_usage_string[]; @@ -14,6 +16,8 @@ extern void prune_packed_objects(int); extern int read_line_with_nul(char *buf, int size, FILE *file); extern int fmt_merge_msg(int merge_summary, struct strbuf *in, struct strbuf *out); +extern int commit_tree(const char *msg, unsigned char *tree, + struct commit_list *parents, unsigned char *ret); extern int cmd_add(int argc, const char **argv, const char *prefix); extern int cmd_annotate(int argc, const char **argv, const char *prefix); -- 1.5.6.1 ^ permalink raw reply related [flat|nested] 82+ messages in thread
* [PATCH 14/14] Build in merge 2008-07-01 2:37 ` [PATCH 13/14] git-commit-tree: make it usable from other builtins Miklos Vajna @ 2008-07-01 2:37 ` Miklos Vajna 2008-07-01 6:23 ` Junio C Hamano 2008-07-01 7:27 ` [PATCH 14/14] " Junio C Hamano 0 siblings, 2 replies; 82+ messages in thread From: Miklos Vajna @ 2008-07-01 2:37 UTC (permalink / raw To: Junio C Hamano; +Cc: git, Johannes Schindelin, Olivier Marin Mentored-by: Johannes Schindelin <Johannes.Schindelin@gmx.de> Signed-off-by: Miklos Vajna <vmiklos@frugalware.org> --- Makefile | 2 +- builtin-merge.c | 1148 +++++++++++++++++++++++++ builtin.h | 1 + git-merge.sh => contrib/examples/git-merge.sh | 0 git.c | 1 + 5 files changed, 1151 insertions(+), 1 deletions(-) create mode 100644 builtin-merge.c rename git-merge.sh => contrib/examples/git-merge.sh (100%) diff --git a/Makefile b/Makefile index bf77292..fbc53e9 100644 --- a/Makefile +++ b/Makefile @@ -240,7 +240,6 @@ SCRIPT_SH += git-lost-found.sh SCRIPT_SH += git-merge-octopus.sh SCRIPT_SH += git-merge-one-file.sh SCRIPT_SH += git-merge-resolve.sh -SCRIPT_SH += git-merge.sh SCRIPT_SH += git-merge-stupid.sh SCRIPT_SH += git-mergetool.sh SCRIPT_SH += git-parse-remote.sh @@ -515,6 +514,7 @@ BUILTIN_OBJS += builtin-ls-remote.o BUILTIN_OBJS += builtin-ls-tree.o BUILTIN_OBJS += builtin-mailinfo.o BUILTIN_OBJS += builtin-mailsplit.o +BUILTIN_OBJS += builtin-merge.o BUILTIN_OBJS += builtin-merge-base.o BUILTIN_OBJS += builtin-merge-file.o BUILTIN_OBJS += builtin-merge-ours.o diff --git a/builtin-merge.c b/builtin-merge.c new file mode 100644 index 0000000..bb8d985 --- /dev/null +++ b/builtin-merge.c @@ -0,0 +1,1148 @@ +/* + * Builtin "git merge" + * + * Copyright (c) 2008 Miklos Vajna <vmiklos@frugalware.org> + * + * Based on git-merge.sh by Junio C Hamano. + */ + +#include "cache.h" +#include "parse-options.h" +#include "builtin.h" +#include "run-command.h" +#include "path-list.h" +#include "diff.h" +#include "refs.h" +#include "commit.h" +#include "diffcore.h" +#include "revision.h" +#include "unpack-trees.h" +#include "cache-tree.h" +#include "dir.h" +#include "utf8.h" +#include "log-tree.h" +#include "color.h" + +enum strategy { + DEFAULT_TWOHEAD = 1, + DEFAULT_OCTOPUS = 2, + NO_FAST_FORWARD = 4, + NO_TRIVIAL = 8 +}; + +static const char * const builtin_merge_usage[] = { + "git-merge [options] <remote>...", + "git-merge [options] <msg> HEAD <remote>", + NULL +}; + +static int show_diffstat = 1, option_log, squash; +static int option_commit = 1, allow_fast_forward = 1; +static int allow_trivial = 1, have_message; +static struct strbuf merge_msg; +static struct commit_list *remoteheads; +static unsigned char head[20], stash[20]; +static struct path_list use_strategies; +static const char *branch; + +static struct path_list_item strategy_items[] = { + { "recur", (void *)NO_TRIVIAL }, + { "recursive", (void *)(DEFAULT_TWOHEAD | NO_TRIVIAL) }, + { "octopus", (void *)DEFAULT_OCTOPUS }, + { "resolve", (void *)0 }, + { "stupid", (void *)0 }, + { "ours", (void *)(NO_FAST_FORWARD | NO_TRIVIAL) }, + { "subtree", (void *)(NO_FAST_FORWARD | NO_TRIVIAL) }, +}; +static struct path_list strategies = { strategy_items, + ARRAY_SIZE(strategy_items), 0, 0 }; + +static const char *pull_twohead, *pull_octopus; + +static int option_parse_message(const struct option *opt, + const char *arg, int unset) +{ + struct strbuf *buf = opt->value; + + if (unset) + strbuf_setlen(buf, 0); + else { + strbuf_addf(buf, "%s\n\n", arg); + have_message = 1; + } + return 0; +} + +static struct path_list_item *unsorted_path_list_lookup(const char *path, + struct path_list *list) +{ + int i; + + if (!path) + return NULL; + + for (i = 0; i < list->nr; i++) + if (!strcmp(path, list->items[i].path)) + return &list->items[i]; + return NULL; +} + +static inline void path_list_append_strategy(struct path_list_item *item) +{ + path_list_append(item->path, &use_strategies)->util = item->util; +} + +static int option_parse_strategy(const struct option *opt, + const char *arg, int unset) +{ + int i; + struct path_list_item *item = unsorted_path_list_lookup(arg, &strategies); + + if (unset) + return 0; + + if (item) + path_list_append_strategy(item); + else { + struct strbuf err; + strbuf_init(&err, 0); + for (i = 0; i < strategies.nr; i++) + strbuf_addf(&err, " %s", strategies.items[i].path); + fprintf(stderr, "Could not find merge strategy '%s'.\n", arg); + fprintf(stderr, "Available strategies are:%s.\n", err.buf); + exit(1); + } + return 0; +} + +static int option_parse_n(const struct option *opt, + const char *arg, int unset) +{ + show_diffstat = unset; + return 0; +} + +static struct option builtin_merge_options[] = { + { OPTION_CALLBACK, 'n', NULL, NULL, NULL, + "do not show a diffstat at the end of the merge", + PARSE_OPT_NOARG, option_parse_n }, + OPT_BOOLEAN(0, "stat", &show_diffstat, + "show a diffstat at the end of the merge"), + OPT_BOOLEAN(0, "summary", &show_diffstat, "(synonym to --stat)"), + OPT_BOOLEAN(0, "log", &option_log, + "add list of one-line log to merge commit message"), + OPT_BOOLEAN(0, "squash", &squash, + "create a single commit instead of doing a merge"), + OPT_BOOLEAN(0, "commit", &option_commit, + "perform a commit if the merge succeeds (default)"), + OPT_BOOLEAN(0, "ff", &allow_fast_forward, + "allow fast forward (default)"), + OPT_CALLBACK('s', "strategy", &use_strategies, "strategy", + "merge strategy to use", option_parse_strategy), + OPT_CALLBACK('m', "message", &merge_msg, "message", + "message to be used for the merge commit (if any)", + option_parse_message), + OPT_END() +}; + +/* Cleans up metadata that is uninteresting after a succeeded merge. */ +static void drop_save(void) +{ + unlink(git_path("MERGE_HEAD")); + unlink(git_path("MERGE_MSG")); +} + +static void save_state(void) +{ + int len; + struct child_process cp; + struct strbuf buffer = STRBUF_INIT; + const char *argv[] = {"stash", "create", NULL}; + + memset(&cp, 0, sizeof(cp)); + cp.argv = argv; + cp.out = -1; + cp.git_cmd = 1; + + if (start_command(&cp)) + die("could not run stash."); + len = strbuf_read(&buffer, cp.out, 1024); + close(cp.out); + + if (finish_command(&cp) || len < 0) + die("stash failed"); + else if (!len) + return; + strbuf_setlen(&buffer, buffer.len-1); + if (get_sha1(buffer.buf, stash)) + die("not a valid object: %s", buffer.buf); +} + +static void reset_hard(unsigned const char *sha1, int verbose) +{ + int i = 0; + const char *args[6]; + + args[i++] = "read-tree"; + if (verbose) + args[i++] = "-v"; + args[i++] = "--reset"; + args[i++] = "-u"; + args[i++] = sha1_to_hex(sha1); + args[i] = NULL; + + if (run_command_v_opt(args, RUN_GIT_CMD)) + die("read-tree failed"); +} + +static void restore_state(void) +{ + struct strbuf sb; + const char *args[] = { "stash", "apply", NULL, NULL }; + + if (is_null_sha1(stash)) + return; + + reset_hard(head, 1); + + strbuf_init(&sb, 0); + args[2] = sha1_to_hex(stash); + + /* + * It is OK to ignore error here, for example when there was + * nothing to restore. + */ + run_command_v_opt(args, RUN_GIT_CMD); + + strbuf_release(&sb); + refresh_cache(REFRESH_QUIET); +} + +/* This is called when no merge was necessary. */ +static void finish_up_to_date(const char *msg) +{ + printf("%s%s\n", squash ? " (nothing to squash)" : "", msg); + drop_save(); +} + +static void squash_message(void) +{ + struct rev_info rev; + struct commit *commit; + struct strbuf out; + struct commit_list *j; + int fd; + + printf("Squash commit -- not updating HEAD\n"); + fd = open(git_path("SQUASH_MSG"), O_WRONLY | O_CREAT, 0666); + if (fd < 0) + die("Could not write to %s", git_path("SQUASH_MSG")); + + init_revisions(&rev, NULL); + rev.ignore_merges = 1; + rev.commit_format = CMIT_FMT_MEDIUM; + + commit = lookup_commit(head); + commit->object.flags |= UNINTERESTING; + add_pending_object(&rev, &commit->object, NULL); + + for (j = remoteheads; j; j = j->next) + add_pending_object(&rev, &j->item->object, NULL); + + setup_revisions(0, NULL, &rev, NULL); + if (prepare_revision_walk(&rev)) + die("revision walk setup failed"); + + strbuf_init(&out, 0); + strbuf_addstr(&out, "Squashed commit of the following:\n"); + while ((commit = get_revision(&rev)) != NULL) { + strbuf_addch(&out, '\n'); + strbuf_addf(&out, "commit %s\n", + sha1_to_hex(commit->object.sha1)); + pretty_print_commit(rev.commit_format, commit, &out, rev.abbrev, + NULL, NULL, rev.date_mode, 0); + } + write(fd, out.buf, out.len); + close(fd); + strbuf_release(&out); +} + +static int run_hook(const char *name) +{ + struct child_process hook; + const char *argv[3], *env[2]; + char index[PATH_MAX]; + + argv[0] = git_path("hooks/%s", name); + if (access(argv[0], X_OK) < 0) + return 0; + + snprintf(index, sizeof(index), "GIT_INDEX_FILE=%s", get_index_file()); + env[0] = index; + env[1] = NULL; + + if (squash) + argv[1] = "1"; + else + argv[1] = "0"; + argv[2] = NULL; + + memset(&hook, 0, sizeof(hook)); + hook.argv = argv; + hook.no_stdin = 1; + hook.stdout_to_stderr = 1; + hook.env = env; + + return run_command(&hook); +} + +static void finish(const unsigned char *new_head, const char *msg) +{ + struct strbuf reflog_message; + + strbuf_init(&reflog_message, 0); + if (!msg) + strbuf_addstr(&reflog_message, getenv("GIT_REFLOG_ACTION")); + else { + printf("%s\n", msg); + strbuf_addf(&reflog_message, "%s: %s", + getenv("GIT_REFLOG_ACTION"), msg); + } + if (squash) { + squash_message(); + } else { + if (!merge_msg.len) + printf("No merge message -- not updating HEAD\n"); + else { + const char *argv_gc_auto[] = { "gc", "--auto", NULL }; + update_ref(reflog_message.buf, "HEAD", + new_head, head, 0, + DIE_ON_ERR); + /* + * We ignore errors in 'gc --auto', since the + * user should see them. + */ + run_command_v_opt(argv_gc_auto, RUN_GIT_CMD); + } + } + if (new_head && show_diffstat) { + struct diff_options opts; + diff_setup(&opts); + opts.output_format |= + DIFF_FORMAT_SUMMARY | DIFF_FORMAT_DIFFSTAT; + opts.detect_rename = DIFF_DETECT_RENAME; + if (diff_use_color_default > 0) + DIFF_OPT_SET(&opts, COLOR_DIFF); + if (diff_setup_done(&opts) < 0) + die("diff_setup_done failed"); + diff_tree_sha1(head, new_head, "", &opts); + diffcore_std(&opts); + diff_flush(&opts); + } + + /* Run a post-merge hook */ + run_hook("post-merge"); + + strbuf_release(&reflog_message); +} + +/* Get the name for the merge commit's message. */ +static void merge_name(const char *remote, struct strbuf *msg) +{ + struct object *remote_head; + unsigned char branch_head[20], buf_sha[20]; + struct strbuf buf; + const char *ptr; + int len = 0; + + memset(branch_head, 0, sizeof(branch_head)); + remote_head = peel_to_type(remote, 0, NULL, OBJ_COMMIT); + if (!remote_head) + die("'%s' does not point to a commit", remote); + + strbuf_init(&buf, 0); + strbuf_addstr(&buf, "refs/heads/"); + strbuf_addstr(&buf, remote); + resolve_ref(buf.buf, branch_head, 0, 0); + + if (!hashcmp(remote_head->sha1, branch_head)) { + strbuf_addf(msg, "%s\t\tbranch '%s' of .\n", + sha1_to_hex(branch_head), remote); + return; + } + /* See if remote matches <name>~<number>, or <name>^ */ + ptr = strrchr(remote, '^'); + if (ptr && ptr[1] == '\0') { + for (len = 0, ptr = remote + strlen(remote); + remote < ptr && ptr[-1] == '^'; + ptr--) + len++; + } + else { + ptr = strrchr(remote, '~'); + if (ptr && ptr[1] != '0' && isdigit(ptr[1])) { + len = ptr-remote; + ptr++; + for (ptr++; *ptr; ptr++) + if (!isdigit(*ptr)) { + len = 0; + break; + } + } + } + if (len) { + struct strbuf truname = STRBUF_INIT; + strbuf_addstr(&truname, "refs/heads/"); + strbuf_addstr(&truname, remote); + strbuf_setlen(&truname, len+11); + if (resolve_ref(truname.buf, buf_sha, 0, 0)) { + strbuf_addf(msg, + "%s\t\tbranch '%s' (early part) of .\n", + sha1_to_hex(remote_head->sha1), truname.buf); + return; + } + } + + if (!strcmp(remote, "FETCH_HEAD") && + !access(git_path("FETCH_HEAD"), R_OK)) { + FILE *fp; + struct strbuf line; + char *ptr; + + strbuf_init(&line, 0); + fp = fopen(git_path("FETCH_HEAD"), "r"); + if (!fp) + die("could not open %s for reading: %s", + git_path("FETCH_HEAD"), strerror(errno)); + strbuf_getline(&line, fp, '\n'); + fclose(fp); + ptr = strstr(line.buf, "\tnot-for-merge\t"); + if (ptr) + strbuf_remove(&line, ptr-line.buf+1, 13); + strbuf_addbuf(msg, &line); + strbuf_release(&line); + return; + } + strbuf_addf(msg, "%s\t\tcommit '%s'\n", + sha1_to_hex(remote_head->sha1), remote); +} + +int git_merge_config(const char *k, const char *v, void *cb) +{ + if (branch && !prefixcmp(k, "branch.") && + !prefixcmp(k + 7, branch) && + !strcmp(k + 7 + strlen(branch), ".mergeoptions")) { + const char **argv; + int argc; + char *buf; + + buf = xstrdup(v); + argc = split_cmdline(buf, &argv); + argv = xrealloc(argv, sizeof(*argv) * (argc + 2)); + memmove(argv + 1, argv, sizeof(*argv) * (argc + 1)); + argc++; + parse_options(argc, argv, builtin_merge_options, + builtin_merge_usage, 0); + free(buf); + } + + if (!strcmp(k, "merge.diffstat") || !strcmp(k, "merge.stat")) + show_diffstat = git_config_bool(k, v); + else if (!strcmp(k, "pull.twohead")) + return git_config_string(&pull_twohead, k, v); + else if (!strcmp(k, "pull.octopus")) + return git_config_string(&pull_octopus, k, v); + return git_diff_ui_config(k, v, cb); +} + +static int read_tree_trivial(unsigned char *common, unsigned char *head, + unsigned char *one) +{ + int i, nr_trees = 0; + struct tree *trees[MAX_UNPACK_TREES]; + struct tree_desc t[MAX_UNPACK_TREES]; + struct unpack_trees_options opts; + + memset(&opts, 0, sizeof(opts)); + opts.head_idx = 2; + opts.src_index = &the_index; + opts.dst_index = &the_index; + opts.update = 1; + opts.verbose_update = 1; + opts.trivial_merges_only = 1; + opts.merge = 1; + trees[nr_trees] = parse_tree_indirect(common); + if (!trees[nr_trees++]) + return -1; + trees[nr_trees] = parse_tree_indirect(head); + if (!trees[nr_trees++]) + return -1; + trees[nr_trees] = parse_tree_indirect(one); + if (!trees[nr_trees++]) + return -1; + opts.fn = threeway_merge; + cache_tree_free(&active_cache_tree); + opts.head_idx = 2; + for (i = 0; i < nr_trees; i++) { + parse_tree(trees[i]); + init_tree_desc(t+i, trees[i]->buffer, trees[i]->size); + } + if (unpack_trees(nr_trees, t, &opts)) + return -1; + return 0; +} + +static void write_tree_trivial(unsigned char *sha1) +{ + if (write_cache_as_tree(sha1, 0, NULL)) + die("git write-tree failed to write a tree"); +} + +static int try_merge_strategy(char *strategy, struct commit_list *common, + const char *head_arg) +{ + const char **args; + int i = 0, ret; + struct commit_list *j; + struct strbuf buf; + + args = xmalloc((4 + commit_list_count(common) + + commit_list_count(remoteheads)) * sizeof(char *)); + strbuf_init(&buf, 0); + strbuf_addf(&buf, "merge-%s", strategy); + args[i++] = buf.buf; + for (j = common; j; j = j->next) + args[i++] = xstrdup(sha1_to_hex(j->item->object.sha1)); + args[i++] = "--"; + args[i++] = head_arg; + for (j = remoteheads; j; j = j->next) + args[i++] = xstrdup(sha1_to_hex(j->item->object.sha1)); + args[i] = NULL; + ret = run_command_v_opt(args, RUN_GIT_CMD); + strbuf_release(&buf); + i = 1; + for (j = common; j; j = j->next) + free((void *)args[i++]); + i += 2; + for (j = remoteheads; j; j = j->next) + free((void *)args[i++]); + free(args); + return -ret; +} + +static void count_diff_files(struct diff_queue_struct *q, + struct diff_options *opt, void *data) +{ + int *count = data; + + (*count) += q->nr; +} + +static int count_unmerged_entries(void) +{ + const struct index_state *state = &the_index; + int i, ret = 0; + + for (i = 0; i < state->cache_nr; i++) + if (ce_stage(state->cache[i])) + ret++; + + return ret; +} + +static int checkout_fast_forward(unsigned char *head, unsigned char *remote) +{ + struct tree *trees[MAX_UNPACK_TREES]; + struct unpack_trees_options opts; + struct tree_desc t[MAX_UNPACK_TREES]; + int i, fd, nr_trees = 0; + struct dir_struct dir; + struct lock_file *lock_file = xcalloc(1, sizeof(struct lock_file)); + + if (read_cache_unmerged()) + die("you need to resolve your current index first"); + + fd = hold_locked_index(lock_file, 1); + + memset(&trees, 0, sizeof(trees)); + memset(&opts, 0, sizeof(opts)); + memset(&t, 0, sizeof(t)); + dir.show_ignored = 1; + dir.exclude_per_dir = ".gitignore"; + opts.dir = &dir; + + opts.head_idx = 1; + opts.src_index = &the_index; + opts.dst_index = &the_index; + opts.update = 1; + opts.verbose_update = 1; + opts.merge = 1; + opts.fn = twoway_merge; + + trees[nr_trees] = parse_tree_indirect(head); + if (!trees[nr_trees++]) + return -1; + trees[nr_trees] = parse_tree_indirect(remote); + if (!trees[nr_trees++]) + return -1; + for (i = 0; i < nr_trees; i++) { + parse_tree(trees[i]); + init_tree_desc(t+i, trees[i]->buffer, trees[i]->size); + } + if (unpack_trees(nr_trees, t, &opts)) + return -1; + if (write_cache(fd, active_cache, active_nr) || + commit_locked_index(lock_file)) + die("unable to write new index file"); + return 0; +} + +static void split_merge_strategies(const char *string, struct path_list *list) +{ + char *p, *q, *buf; + + if (!string) + return; + + list->strdup_paths = 1; + buf = xstrdup(string); + q = buf; + for (;;) { + p = strchr(q, ' '); + if (!p) { + path_list_append(q, list); + free(buf); + return; + } else { + *p = '\0'; + path_list_append(q, list); + q = ++p; + } + } +} + +static void add_strategies(const char *string, enum strategy strategy) +{ + struct path_list list; + int i; + + memset(&list, 0, sizeof(list)); + split_merge_strategies(string, &list); + if (list.nr) { + for (i = 0; i < list.nr; i++) { + struct path_list_item *item; + + item = unsorted_path_list_lookup(list.items[i].path, + &strategies); + if (item) + path_list_append_strategy(item); + } + return; + } + for (i = 0; i < strategies.nr; i++) + if ((enum strategy)strategies.items[i].util & strategy) + path_list_append_strategy(&strategies.items[i]); +} + +static int merge_trivial(void) +{ + unsigned char result_tree[20], result_commit[20]; + struct commit_list parent; + + write_tree_trivial(result_tree); + printf("Wonderful.\n"); + parent.item = remoteheads->item; + parent.next = NULL; + commit_tree(merge_msg.buf, result_tree, &parent, result_commit); + finish(result_commit, "In-index merge"); + drop_save(); + return 0; +} + +static int finish_automerge(struct commit_list *common, + unsigned char *result_tree, + struct path_list_item *wt_strategy) +{ + struct commit_list *parents = NULL, *j; + struct strbuf buf = STRBUF_INIT; + unsigned char result_commit[20]; + + free_commit_list(common); + if (allow_fast_forward) { + parents = remoteheads; + commit_list_insert(lookup_commit(head), &parents); + parents = reduce_heads(parents); + } else { + struct commit_list **pptr = &parents; + + pptr = &commit_list_insert(lookup_commit(head), + pptr)->next; + for (j = remoteheads; j; j = j->next) + pptr = &commit_list_insert(j->item, pptr)->next; + } + free_commit_list(remoteheads); + strbuf_addch(&merge_msg, '\n'); + commit_tree(merge_msg.buf, result_tree, parents, result_commit); + strbuf_addf(&buf, "Merge made by %s.", wt_strategy->path); + finish(result_commit, buf.buf); + strbuf_release(&buf); + drop_save(); + return 0; +} + +static int suggest_conflicts(void) +{ + FILE *fp; + int pos; + + fp = fopen(git_path("MERGE_MSG"), "a"); + if (!fp) + die("Could open %s for writing", git_path("MERGE_MSG")); + fprintf(fp, "\nConflicts:\n"); + for (pos = 0; pos < active_nr; pos++) { + struct cache_entry *ce = active_cache[pos]; + + if (ce_stage(ce)) { + fprintf(fp, "\t%s\n", ce->name); + while (pos + 1 < active_nr && + !strcmp(ce->name, + active_cache[pos + 1]->name)) + pos++; + } + } + fclose(fp); + rerere(); + printf("Automatic merge failed; " + "fix conflicts and then commit the result.\n"); + return 1; +} + +static inline unsigned nth_strategy_flags(struct path_list *s, int nth) +{ + return (unsigned) s->items[nth].util; +} + +static struct commit *is_old_style_invocation(int argc, const char **argv) +{ + struct commit *second_token = NULL; + if (argc > 1) { + unsigned char second_sha1[20]; + + if (get_sha1(argv[1], second_sha1)) + return NULL; + second_token = lookup_commit_reference_gently(second_sha1, 0); + if (!second_token) + die("'%s' is not a commit", argv[1]); + if (hashcmp(second_token->object.sha1, head)) + return NULL; + } + return second_token; +} + +static int evaluate_result(void) +{ + int cnt = 0; + struct rev_info rev; + + if (read_cache() < 0) + die("failed to read the cache"); + + /* Check how many files differ. */ + init_revisions(&rev, ""); + setup_revisions(0, NULL, &rev, NULL); + rev.diffopt.output_format |= + DIFF_FORMAT_CALLBACK; + rev.diffopt.format_callback = count_diff_files; + rev.diffopt.format_callback_data = &cnt; + run_diff_files(&rev, 0); + + /* + * Check how many unmerged entries are + * there. + */ + cnt += count_unmerged_entries(); + + return cnt; +} + +int cmd_merge(int argc, const char **argv, const char *prefix) +{ + unsigned char result_tree[20]; + struct strbuf buf; + const char *head_arg; + int flag, head_invalid = 0, i; + int best_cnt = -1, merge_was_ok = 0, automerge_was_ok = 0; + struct commit_list *common = NULL; + struct path_list_item *best_strategy = NULL, *wt_strategy = NULL; + struct commit_list **remotes = &remoteheads; + + setup_work_tree(); + if (unmerged_cache()) + die("You are in the middle of a conflicted merge."); + + /* + * Check if we are _not_ on a detached HEAD, i.e. if there is a + * current branch. + */ + branch = resolve_ref("HEAD", head, 0, &flag); + if (branch && flag & REF_ISSYMREF) { + const char *ptr = skip_prefix(branch, "refs/heads/"); + if (ptr) + branch = ptr; + } else + head_invalid = 1; + + git_config(git_merge_config, NULL); + + /* for color.ui */ + if (diff_use_color_default == -1) + diff_use_color_default = git_use_color_default; + + argc = parse_options(argc, argv, builtin_merge_options, + builtin_merge_usage, 0); + + if (squash) { + if (!allow_fast_forward) + die("You cannot combine --squash with --no-ff."); + option_commit = 0; + } + + if (!argc) + usage_with_options(builtin_merge_usage, + builtin_merge_options); + + /* + * This could be traditional "merge <msg> HEAD <commit>..." and + * the way we can tell it is to see if the second token is HEAD, + * but some people might have misused the interface and used a + * committish that is the same as HEAD there instead. + * Traditional format never would have "-m" so it is an + * additional safety measure to check for it. + */ + strbuf_init(&buf, 0); + + if (!have_message && is_old_style_invocation(argc, argv)) { + strbuf_addstr(&merge_msg, argv[0]); + head_arg = argv[1]; + argv += 2; + argc -= 2; + } else if (head_invalid) { + struct object *remote_head; + /* + * If the merged head is a valid one there is no reason + * to forbid "git merge" into a branch yet to be born. + * We do the same for "git pull". + */ + if (argc != 1) + die("Can merge only exactly one commit into " + "empty head"); + remote_head = peel_to_type(argv[0], 0, NULL, OBJ_COMMIT); + if (!remote_head) + die("%s - not something we can merge", argv[0]); + update_ref("initial pull", "HEAD", remote_head->sha1, NULL, 0, + DIE_ON_ERR); + reset_hard(remote_head->sha1, 0); + return 0; + } else { + struct strbuf msg; + + /* We are invoked directly as the first-class UI. */ + head_arg = "HEAD"; + + /* + * All the rest are the commits being merged; + * prepare the standard merge summary message to + * be appended to the given message. If remote + * is invalid we will die later in the common + * codepath so we discard the error in this + * loop. + */ + strbuf_init(&msg, 0); + for (i = 0; i < argc; i++) + merge_name(argv[i], &msg); + fmt_merge_msg(option_log, &msg, &merge_msg); + if (merge_msg.len) + strbuf_setlen(&merge_msg, merge_msg.len-1); + } + + if (head_invalid || !argc) + usage_with_options(builtin_merge_usage, + builtin_merge_options); + + strbuf_addstr(&buf, "merge"); + for (i = 0; i < argc; i++) + strbuf_addf(&buf, " %s", argv[i]); + setenv("GIT_REFLOG_ACTION", buf.buf, 0); + strbuf_reset(&buf); + + for (i = 0; i < argc; i++) { + struct object *o; + + o = peel_to_type(argv[i], 0, NULL, OBJ_COMMIT); + if (!o) + die("%s - not something we can merge", argv[i]); + remotes = &commit_list_insert(lookup_commit(o->sha1), + remotes)->next; + + strbuf_addf(&buf, "GITHEAD_%s", sha1_to_hex(o->sha1)); + setenv(buf.buf, argv[i], 1); + strbuf_reset(&buf); + } + + if (!use_strategies.nr) { + if (!remoteheads->next) + add_strategies(pull_twohead, DEFAULT_TWOHEAD); + else + add_strategies(pull_octopus, DEFAULT_OCTOPUS); + } + + for (i = 0; i < use_strategies.nr; i++) { + if (nth_strategy_flags(&use_strategies, i) & NO_FAST_FORWARD) + allow_fast_forward = 0; + if (nth_strategy_flags(&use_strategies, i) & NO_TRIVIAL) + allow_trivial = 0; + } + + if (!remoteheads->next) + common = get_merge_bases(lookup_commit(head), + remoteheads->item, 1); + else { + struct commit_list *list = remoteheads; + commit_list_insert(lookup_commit(head), &list); + common = get_octopus_merge_bases(list); + free(list); + } + + update_ref("updating ORIG_HEAD", "ORIG_HEAD", head, NULL, 0, + DIE_ON_ERR); + + if (!common) + ; /* No common ancestors found. We need a real merge. */ + else if (!remoteheads->next && !common->next && + common->item == remoteheads->item) { + /* + * If head can reach all the merge then we are up to date. + * but first the most common case of merging one remote. + */ + finish_up_to_date("Already up-to-date."); + return 0; + } else if (allow_fast_forward && !remoteheads->next && + !common->next && + !hashcmp(common->item->object.sha1, head)) { + /* Again the most common case of merging one remote. */ + struct strbuf msg; + struct object *o; + char hex[41]; + + strcpy(hex, find_unique_abbrev(head, DEFAULT_ABBREV)); + + printf("Updating %s..%s\n", + hex, + find_unique_abbrev(remoteheads->item->object.sha1, + DEFAULT_ABBREV)); + refresh_cache(REFRESH_QUIET); + strbuf_init(&msg, 0); + strbuf_addstr(&msg, "Fast forward"); + if (have_message) + strbuf_addstr(&msg, + " (no commit created; -m option ignored)"); + o = peel_to_type(sha1_to_hex(remoteheads->item->object.sha1), + 0, NULL, OBJ_COMMIT); + if (!o) + return 1; + + if (checkout_fast_forward(head, remoteheads->item->object.sha1)) + return 1; + + finish(o->sha1, msg.buf); + drop_save(); + return 0; + } else if (!remoteheads->next && common->next) + ; + /* + * We are not doing octopus and not fast forward. Need + * a real merge. + */ + else if (!remoteheads->next && !common->next && option_commit) { + /* + * We are not doing octopus, not fast forward, and have + * only one common. + */ + refresh_cache(REFRESH_QUIET); + if (allow_trivial) { + /* See if it is really trivial. */ + git_committer_info(IDENT_ERROR_ON_NO_NAME); + printf("Trying really trivial in-index merge...\n"); + if (!read_tree_trivial(common->item->object.sha1, + head, remoteheads->item->object.sha1)) + return merge_trivial(); + printf("Nope.\n"); + } + } else { + /* + * An octopus. If we can reach all the remote we are up + * to date. + */ + int up_to_date = 1; + struct commit_list *j; + + for (j = remoteheads; j; j = j->next) { + struct commit_list *common_one; + + /* + * Here we *have* to calculate the individual + * merge_bases again, otherwise "git merge HEAD^ + * HEAD^^" would be missed. + */ + common_one = get_merge_bases(lookup_commit(head), + j->item, 1); + if (hashcmp(common_one->item->object.sha1, + j->item->object.sha1)) { + up_to_date = 0; + break; + } + } + if (up_to_date) { + finish_up_to_date("Already up-to-date. Yeeah!"); + return 0; + } + } + + /* We are going to make a new commit. */ + git_committer_info(IDENT_ERROR_ON_NO_NAME); + + /* + * At this point, we need a real merge. No matter what strategy + * we use, it would operate on the index, possibly affecting the + * working tree, and when resolved cleanly, have the desired + * tree in the index -- this means that the index must be in + * sync with the head commit. The strategies are responsible + * to ensure this. + */ + if (use_strategies.nr != 1) { + /* + * Stash away the local changes so that we can try more + * than one. + */ + save_state(); + } else { + memcpy(stash, null_sha1, 20); + } + + for (i = 0; i < use_strategies.nr; i++) { + int ret; + if (i) { + printf("Rewinding the tree to pristine...\n"); + restore_state(); + } + if (use_strategies.nr != 1) + printf("Trying merge strategy %s...\n", + use_strategies.items[i].path); + /* + * Remember which strategy left the state in the working + * tree. + */ + wt_strategy = &use_strategies.items[i]; + + ret = try_merge_strategy(use_strategies.items[i].path, + common, head_arg); + if (!option_commit && !ret) { + merge_was_ok = 1; + /* + * This is necessary here just to avoid writing + * the tree, but later we will *not* exit with + * status code 1 because merge_was_ok is set. + */ + ret = 1; + } + + if (ret) { + /* + * The backend exits with 1 when conflicts are + * left to be resolved, with 2 when it does not + * handle the given merge at all. + */ + if (ret == 1) { + int cnt = evaluate_result(); + + if (best_cnt <= 0 || cnt <= best_cnt) { + best_strategy = + &use_strategies.items[i]; + best_cnt = cnt; + } + } + if (merge_was_ok) + break; + else + continue; + } + + /* Automerge succeeded. */ + write_tree_trivial(result_tree); + automerge_was_ok = 1; + break; + } + + /* + * If we have a resulting tree, that means the strategy module + * auto resolved the merge cleanly. + */ + if (automerge_was_ok) + return finish_automerge(common, result_tree, wt_strategy); + + /* + * Pick the result from the best strategy and have the user fix + * it up. + */ + if (!best_strategy) { + restore_state(); + if (use_strategies.nr > 1) + fprintf(stderr, + "No merge strategy handled the merge.\n"); + else + fprintf(stderr, "Merge with strategy %s failed.\n", + use_strategies.items[0].path); + return 2; + } else if (best_strategy == wt_strategy) + ; /* We already have its result in the working tree. */ + else { + printf("Rewinding the tree to pristine...\n"); + restore_state(); + printf("Using the %s to prepare resolving by hand.\n", + best_strategy->path); + try_merge_strategy(best_strategy->path, common, head_arg); + } + + if (squash) + finish(NULL, NULL); + else { + int fd; + struct commit_list *j; + + for (j = remoteheads; j; j = j->next) + strbuf_addf(&buf, "%s\n", + sha1_to_hex(j->item->object.sha1)); + fd = open(git_path("MERGE_HEAD"), O_WRONLY | O_CREAT, 0666); + if (fd < 0) + die("Could open %s for writing", + git_path("MERGE_HEAD")); + if (write_in_full(fd, buf.buf, buf.len) != buf.len) + die("Could not write to %s", git_path("MERGE_HEAD")); + close(fd); + strbuf_addch(&merge_msg, '\n'); + fd = open(git_path("MERGE_MSG"), O_WRONLY | O_CREAT, 0666); + if (fd < 0) + die("Could open %s for writing", git_path("MERGE_MSG")); + if (write_in_full(fd, merge_msg.buf, merge_msg.len) != + merge_msg.len) + die("Could not write to %s", git_path("MERGE_MSG")); + close(fd); + } + + if (merge_was_ok) { + fprintf(stderr, "Automatic merge went well; " + "stopped before committing as requested\n"); + return 0; + } else + return suggest_conflicts(); +} diff --git a/builtin.h b/builtin.h index 05ee56f..0e605d4 100644 --- a/builtin.h +++ b/builtin.h @@ -64,6 +64,7 @@ extern int cmd_ls_tree(int argc, const char **argv, const char *prefix); extern int cmd_ls_remote(int argc, const char **argv, const char *prefix); extern int cmd_mailinfo(int argc, const char **argv, const char *prefix); extern int cmd_mailsplit(int argc, const char **argv, const char *prefix); +extern int cmd_merge(int argc, const char **argv, const char *prefix); extern int cmd_merge_base(int argc, const char **argv, const char *prefix); extern int cmd_merge_ours(int argc, const char **argv, const char *prefix); extern int cmd_merge_file(int argc, const char **argv, const char *prefix); diff --git a/git-merge.sh b/contrib/examples/git-merge.sh similarity index 100% rename from git-merge.sh rename to contrib/examples/git-merge.sh diff --git a/git.c b/git.c index 2fbe96b..770aadd 100644 --- a/git.c +++ b/git.c @@ -271,6 +271,7 @@ static void handle_internal_command(int argc, const char **argv) { "ls-remote", cmd_ls_remote }, { "mailinfo", cmd_mailinfo }, { "mailsplit", cmd_mailsplit }, + { "merge", cmd_merge, RUN_SETUP | NEED_WORK_TREE }, { "merge-base", cmd_merge_base, RUN_SETUP }, { "merge-file", cmd_merge_file }, { "merge-ours", cmd_merge_ours, RUN_SETUP }, -- 1.5.6.1 ^ permalink raw reply related [flat|nested] 82+ messages in thread
* Re: [PATCH 14/14] Build in merge 2008-07-01 2:37 ` [PATCH 14/14] Build in merge Miklos Vajna @ 2008-07-01 6:23 ` Junio C Hamano 2008-07-01 12:50 ` Miklos Vajna 2008-07-01 7:27 ` [PATCH 14/14] " Junio C Hamano 1 sibling, 1 reply; 82+ messages in thread From: Junio C Hamano @ 2008-07-01 6:23 UTC (permalink / raw To: Miklos Vajna; +Cc: git, Johannes Schindelin, Olivier Marin Miklos Vajna <vmiklos@frugalware.org> writes: > +static int read_tree_trivial(unsigned char *common, unsigned char *head, > + unsigned char *one) > +{ > + int i, nr_trees = 0; > + struct tree *trees[MAX_UNPACK_TREES]; > + struct tree_desc t[MAX_UNPACK_TREES]; > + struct unpack_trees_options opts; > + > + memset(&opts, 0, sizeof(opts)); > + opts.head_idx = 2; Do you still need this assignment here? > +int cmd_merge(int argc, const char **argv, const char *prefix) > +{ > + unsigned char result_tree[20]; > + struct strbuf buf; > + const char *head_arg; > + int flag, head_invalid = 0, i; > + int best_cnt = -1, merge_was_ok = 0, automerge_was_ok = 0; > + struct commit_list *common = NULL; > + struct path_list_item *best_strategy = NULL, *wt_strategy = NULL; > + struct commit_list **remotes = &remoteheads; > + > + setup_work_tree(); > + if (unmerged_cache()) > + die("You are in the middle of a conflicted merge."); > + > + /* > + * Check if we are _not_ on a detached HEAD, i.e. if there is a > + * current branch. > + */ > + branch = resolve_ref("HEAD", head, 0, &flag); > + if (branch && flag & REF_ISSYMREF) { > + const char *ptr = skip_prefix(branch, "refs/heads/"); > + if (ptr) > + branch = ptr; > + } else > + head_invalid = 1; Wait a minute... Are you calling a detached HEAD as head_invalid here? I am not too much worried about variable naming, but you later do ... > + if (!have_message && is_old_style_invocation(argc, argv)) { > ... > + } else if (head_invalid) { > + struct object *remote_head; > + /* > + * If the merged head is a valid one there is no reason > + * to forbid "git merge" into a branch yet to be born. > + * We do the same for "git pull". > + */ > + if (argc != 1) > + die("Can merge only exactly one commit into " > + "empty head"); Which is about HEAD pointing at a branch that isn't born yet. They are two very different concepts. Either the above "else if (head_invalid)" is wrong, or more likely the setting of head_invalid we saw earlier is wrong. Probably what you meant was: - "char *branch" points at either "HEAD" (when detached) or the name of the branch (e.g. "master" when "refs/heads/master"); - "unsigned char head[]" stores the commit object name of the current HEAD (or 0{40} if the current branch is unborn); - set head_invalid to true only when the current branch is unborn. So perhaps... branch = resolve_ref("HEAD", head, 0, &flag); if (branch && (flag & REF_ISSYMREF) && !prefixcmp(branch, "refs/heads/")) branch += 11; head_invalid = is_null_sha1(head); And probably we can even drop (flag & REF_ISSYMREF) from the above safely. Do we even care if the head is detached in this program? I doubt it. > ... > + strbuf_init(&msg, 0); > + strbuf_addstr(&msg, "Fast forward"); > + if (have_message) > + strbuf_addstr(&msg, > + " (no commit created; -m option ignored)"); > + o = peel_to_type(sha1_to_hex(remoteheads->item->object.sha1), > + 0, NULL, OBJ_COMMIT); > + if (!o) > + return 1; > + > + if (checkout_fast_forward(head, remoteheads->item->object.sha1)) > + return 1; Not exiting with 0 status from around here upon error is an improvement, but does the user see sensible error messages in addition to the exit status? Getting warmer ;-) ^ permalink raw reply [flat|nested] 82+ messages in thread
* Re: [PATCH 14/14] Build in merge 2008-07-01 6:23 ` Junio C Hamano @ 2008-07-01 12:50 ` Miklos Vajna 2008-07-01 13:18 ` Miklos Vajna 0 siblings, 1 reply; 82+ messages in thread From: Miklos Vajna @ 2008-07-01 12:50 UTC (permalink / raw To: Junio C Hamano; +Cc: git, Johannes Schindelin, Olivier Marin [-- Attachment #1: Type: text/plain, Size: 4040 bytes --] On Mon, Jun 30, 2008 at 11:23:17PM -0700, Junio C Hamano <gitster@pobox.com> wrote: > Miklos Vajna <vmiklos@frugalware.org> writes: > > > +static int read_tree_trivial(unsigned char *common, unsigned char *head, > > + unsigned char *one) > > +{ > > + int i, nr_trees = 0; > > + struct tree *trees[MAX_UNPACK_TREES]; > > + struct tree_desc t[MAX_UNPACK_TREES]; > > + struct unpack_trees_options opts; > > + > > + memset(&opts, 0, sizeof(opts)); > > + opts.head_idx = 2; > > Do you still need this assignment here? No, it was duplicated. > > +int cmd_merge(int argc, const char **argv, const char *prefix) > > +{ > > + unsigned char result_tree[20]; > > + struct strbuf buf; > > + const char *head_arg; > > + int flag, head_invalid = 0, i; > > + int best_cnt = -1, merge_was_ok = 0, automerge_was_ok = 0; > > + struct commit_list *common = NULL; > > + struct path_list_item *best_strategy = NULL, *wt_strategy = NULL; > > + struct commit_list **remotes = &remoteheads; > > + > > + setup_work_tree(); > > + if (unmerged_cache()) > > + die("You are in the middle of a conflicted merge."); > > + > > + /* > > + * Check if we are _not_ on a detached HEAD, i.e. if there is a > > + * current branch. > > + */ > > + branch = resolve_ref("HEAD", head, 0, &flag); > > + if (branch && flag & REF_ISSYMREF) { > > + const char *ptr = skip_prefix(branch, "refs/heads/"); > > + if (ptr) > > + branch = ptr; > > + } else > > + head_invalid = 1; > > Wait a minute... Are you calling a detached HEAD as head_invalid here? I > am not too much worried about variable naming, but you later do ... > > > + if (!have_message && is_old_style_invocation(argc, argv)) { > > ... > > + } else if (head_invalid) { > > + struct object *remote_head; > > + /* > > + * If the merged head is a valid one there is no reason > > + * to forbid "git merge" into a branch yet to be born. > > + * We do the same for "git pull". > > + */ > > + if (argc != 1) > > + die("Can merge only exactly one commit into " > > + "empty head"); > > Which is about HEAD pointing at a branch that isn't born yet. They are > two very different concepts. > > Either the above "else if (head_invalid)" is wrong, or more likely the > setting of head_invalid we saw earlier is wrong. > > Probably what you meant was: > > - "char *branch" points at either "HEAD" (when detached) or > the name of the branch (e.g. "master" when "refs/heads/master"); > > - "unsigned char head[]" stores the commit object name of the > current HEAD (or 0{40} if the current branch is unborn); > > - set head_invalid to true only when the current branch is unborn. > > So perhaps... > > branch = resolve_ref("HEAD", head, 0, &flag); > if (branch && (flag & REF_ISSYMREF) && !prefixcmp(branch, "refs/heads/")) > branch += 11; > head_invalid = is_null_sha1(head); > > And probably we can even drop (flag & REF_ISSYMREF) from the above safely. > Do we even care if the head is detached in this program? I doubt it. You are right. I though we care about it, but now I realized such a comment was only in git-merge.annotated (in Dscho's fork), not in git-merge.sh. And yes, I mixed up head_invalid for two purposes. Thanks for clearing the situation. > > + strbuf_init(&msg, 0); > > + strbuf_addstr(&msg, "Fast forward"); > > + if (have_message) > > + strbuf_addstr(&msg, > > + " (no commit created; -m option ignored)"); > > + o = peel_to_type(sha1_to_hex(remoteheads->item->object.sha1), > > + 0, NULL, OBJ_COMMIT); > > + if (!o) > > + return 1; > > + > > + if (checkout_fast_forward(head, remoteheads->item->object.sha1)) > > + return 1; > > Not exiting with 0 status from around here upon error is an improvement, > but does the user see sensible error messages in addition to the exit > status? Now he/she does. :-) (I updated my working branch on repo.or.cz, will send a patch soon, as well.) [-- Attachment #2: Type: application/pgp-signature, Size: 197 bytes --] ^ permalink raw reply [flat|nested] 82+ messages in thread
* [PATCH 14/14] Build in merge 2008-07-01 12:50 ` Miklos Vajna @ 2008-07-01 13:18 ` Miklos Vajna 2008-07-01 23:55 ` Junio C Hamano 2008-07-06 8:50 ` Junio C Hamano 0 siblings, 2 replies; 82+ messages in thread From: Miklos Vajna @ 2008-07-01 13:18 UTC (permalink / raw To: Junio C Hamano; +Cc: git, Johannes Schindelin, Olivier Marin Mentored-by: Johannes Schindelin <Johannes.Schindelin@gmx.de> Signed-off-by: Miklos Vajna <vmiklos@frugalware.org> --- Interdiff: git diff 01a7dae..7709df7 Individual commits (there is only one change this time): git log 7709df7 Ah and please don't forget to apply the testsuite fix I sent in $gmane/86981. (Or is it easier if I resend the whole series?) Makefile | 2 +- builtin-merge.c | 1158 +++++++++++++++++++++++++ builtin.h | 1 + git-merge.sh => contrib/examples/git-merge.sh | 0 git.c | 1 + 5 files changed, 1161 insertions(+), 1 deletions(-) create mode 100644 builtin-merge.c rename git-merge.sh => contrib/examples/git-merge.sh (100%) diff --git a/Makefile b/Makefile index bf77292..fbc53e9 100644 --- a/Makefile +++ b/Makefile @@ -240,7 +240,6 @@ SCRIPT_SH += git-lost-found.sh SCRIPT_SH += git-merge-octopus.sh SCRIPT_SH += git-merge-one-file.sh SCRIPT_SH += git-merge-resolve.sh -SCRIPT_SH += git-merge.sh SCRIPT_SH += git-merge-stupid.sh SCRIPT_SH += git-mergetool.sh SCRIPT_SH += git-parse-remote.sh @@ -515,6 +514,7 @@ BUILTIN_OBJS += builtin-ls-remote.o BUILTIN_OBJS += builtin-ls-tree.o BUILTIN_OBJS += builtin-mailinfo.o BUILTIN_OBJS += builtin-mailsplit.o +BUILTIN_OBJS += builtin-merge.o BUILTIN_OBJS += builtin-merge-base.o BUILTIN_OBJS += builtin-merge-file.o BUILTIN_OBJS += builtin-merge-ours.o diff --git a/builtin-merge.c b/builtin-merge.c new file mode 100644 index 0000000..b261993 --- /dev/null +++ b/builtin-merge.c @@ -0,0 +1,1158 @@ +/* + * Builtin "git merge" + * + * Copyright (c) 2008 Miklos Vajna <vmiklos@frugalware.org> + * + * Based on git-merge.sh by Junio C Hamano. + */ + +#include "cache.h" +#include "parse-options.h" +#include "builtin.h" +#include "run-command.h" +#include "path-list.h" +#include "diff.h" +#include "refs.h" +#include "commit.h" +#include "diffcore.h" +#include "revision.h" +#include "unpack-trees.h" +#include "cache-tree.h" +#include "dir.h" +#include "utf8.h" +#include "log-tree.h" +#include "color.h" + +enum strategy { + DEFAULT_TWOHEAD = 1, + DEFAULT_OCTOPUS = 2, + NO_FAST_FORWARD = 4, + NO_TRIVIAL = 8 +}; + +static const char * const builtin_merge_usage[] = { + "git-merge [options] <remote>...", + "git-merge [options] <msg> HEAD <remote>", + NULL +}; + +static int show_diffstat = 1, option_log, squash; +static int option_commit = 1, allow_fast_forward = 1; +static int allow_trivial = 1, have_message; +static struct strbuf merge_msg; +static struct commit_list *remoteheads; +static unsigned char head[20], stash[20]; +static struct path_list use_strategies; +static const char *branch; + +static struct path_list_item strategy_items[] = { + { "recur", (void *)NO_TRIVIAL }, + { "recursive", (void *)(DEFAULT_TWOHEAD | NO_TRIVIAL) }, + { "octopus", (void *)DEFAULT_OCTOPUS }, + { "resolve", (void *)0 }, + { "stupid", (void *)0 }, + { "ours", (void *)(NO_FAST_FORWARD | NO_TRIVIAL) }, + { "subtree", (void *)(NO_FAST_FORWARD | NO_TRIVIAL) }, +}; +static struct path_list strategies = { strategy_items, + ARRAY_SIZE(strategy_items), 0, 0 }; + +static const char *pull_twohead, *pull_octopus; + +static int option_parse_message(const struct option *opt, + const char *arg, int unset) +{ + struct strbuf *buf = opt->value; + + if (unset) + strbuf_setlen(buf, 0); + else { + strbuf_addf(buf, "%s\n\n", arg); + have_message = 1; + } + return 0; +} + +static struct path_list_item *unsorted_path_list_lookup(const char *path, + struct path_list *list) +{ + int i; + + if (!path) + return NULL; + + for (i = 0; i < list->nr; i++) + if (!strcmp(path, list->items[i].path)) + return &list->items[i]; + return NULL; +} + +static inline void path_list_append_strategy(struct path_list_item *item) +{ + path_list_append(item->path, &use_strategies)->util = item->util; +} + +static int option_parse_strategy(const struct option *opt, + const char *arg, int unset) +{ + int i; + struct path_list_item *item = unsorted_path_list_lookup(arg, &strategies); + + if (unset) + return 0; + + if (item) + path_list_append_strategy(item); + else { + struct strbuf err; + strbuf_init(&err, 0); + for (i = 0; i < strategies.nr; i++) + strbuf_addf(&err, " %s", strategies.items[i].path); + fprintf(stderr, "Could not find merge strategy '%s'.\n", arg); + fprintf(stderr, "Available strategies are:%s.\n", err.buf); + exit(1); + } + return 0; +} + +static int option_parse_n(const struct option *opt, + const char *arg, int unset) +{ + show_diffstat = unset; + return 0; +} + +static struct option builtin_merge_options[] = { + { OPTION_CALLBACK, 'n', NULL, NULL, NULL, + "do not show a diffstat at the end of the merge", + PARSE_OPT_NOARG, option_parse_n }, + OPT_BOOLEAN(0, "stat", &show_diffstat, + "show a diffstat at the end of the merge"), + OPT_BOOLEAN(0, "summary", &show_diffstat, "(synonym to --stat)"), + OPT_BOOLEAN(0, "log", &option_log, + "add list of one-line log to merge commit message"), + OPT_BOOLEAN(0, "squash", &squash, + "create a single commit instead of doing a merge"), + OPT_BOOLEAN(0, "commit", &option_commit, + "perform a commit if the merge succeeds (default)"), + OPT_BOOLEAN(0, "ff", &allow_fast_forward, + "allow fast forward (default)"), + OPT_CALLBACK('s', "strategy", &use_strategies, "strategy", + "merge strategy to use", option_parse_strategy), + OPT_CALLBACK('m', "message", &merge_msg, "message", + "message to be used for the merge commit (if any)", + option_parse_message), + OPT_END() +}; + +/* Cleans up metadata that is uninteresting after a succeeded merge. */ +static void drop_save(void) +{ + unlink(git_path("MERGE_HEAD")); + unlink(git_path("MERGE_MSG")); +} + +static void save_state(void) +{ + int len; + struct child_process cp; + struct strbuf buffer = STRBUF_INIT; + const char *argv[] = {"stash", "create", NULL}; + + memset(&cp, 0, sizeof(cp)); + cp.argv = argv; + cp.out = -1; + cp.git_cmd = 1; + + if (start_command(&cp)) + die("could not run stash."); + len = strbuf_read(&buffer, cp.out, 1024); + close(cp.out); + + if (finish_command(&cp) || len < 0) + die("stash failed"); + else if (!len) + return; + strbuf_setlen(&buffer, buffer.len-1); + if (get_sha1(buffer.buf, stash)) + die("not a valid object: %s", buffer.buf); +} + +static void reset_hard(unsigned const char *sha1, int verbose) +{ + int i = 0; + const char *args[6]; + + args[i++] = "read-tree"; + if (verbose) + args[i++] = "-v"; + args[i++] = "--reset"; + args[i++] = "-u"; + args[i++] = sha1_to_hex(sha1); + args[i] = NULL; + + if (run_command_v_opt(args, RUN_GIT_CMD)) + die("read-tree failed"); +} + +static void restore_state(void) +{ + struct strbuf sb; + const char *args[] = { "stash", "apply", NULL, NULL }; + + if (is_null_sha1(stash)) + return; + + reset_hard(head, 1); + + strbuf_init(&sb, 0); + args[2] = sha1_to_hex(stash); + + /* + * It is OK to ignore error here, for example when there was + * nothing to restore. + */ + run_command_v_opt(args, RUN_GIT_CMD); + + strbuf_release(&sb); + refresh_cache(REFRESH_QUIET); +} + +/* This is called when no merge was necessary. */ +static void finish_up_to_date(const char *msg) +{ + printf("%s%s\n", squash ? " (nothing to squash)" : "", msg); + drop_save(); +} + +static void squash_message(void) +{ + struct rev_info rev; + struct commit *commit; + struct strbuf out; + struct commit_list *j; + int fd; + + printf("Squash commit -- not updating HEAD\n"); + fd = open(git_path("SQUASH_MSG"), O_WRONLY | O_CREAT, 0666); + if (fd < 0) + die("Could not write to %s", git_path("SQUASH_MSG")); + + init_revisions(&rev, NULL); + rev.ignore_merges = 1; + rev.commit_format = CMIT_FMT_MEDIUM; + + commit = lookup_commit(head); + commit->object.flags |= UNINTERESTING; + add_pending_object(&rev, &commit->object, NULL); + + for (j = remoteheads; j; j = j->next) + add_pending_object(&rev, &j->item->object, NULL); + + setup_revisions(0, NULL, &rev, NULL); + if (prepare_revision_walk(&rev)) + die("revision walk setup failed"); + + strbuf_init(&out, 0); + strbuf_addstr(&out, "Squashed commit of the following:\n"); + while ((commit = get_revision(&rev)) != NULL) { + strbuf_addch(&out, '\n'); + strbuf_addf(&out, "commit %s\n", + sha1_to_hex(commit->object.sha1)); + pretty_print_commit(rev.commit_format, commit, &out, rev.abbrev, + NULL, NULL, rev.date_mode, 0); + } + write(fd, out.buf, out.len); + close(fd); + strbuf_release(&out); +} + +static int run_hook(const char *name) +{ + struct child_process hook; + const char *argv[3], *env[2]; + char index[PATH_MAX]; + + argv[0] = git_path("hooks/%s", name); + if (access(argv[0], X_OK) < 0) + return 0; + + snprintf(index, sizeof(index), "GIT_INDEX_FILE=%s", get_index_file()); + env[0] = index; + env[1] = NULL; + + if (squash) + argv[1] = "1"; + else + argv[1] = "0"; + argv[2] = NULL; + + memset(&hook, 0, sizeof(hook)); + hook.argv = argv; + hook.no_stdin = 1; + hook.stdout_to_stderr = 1; + hook.env = env; + + return run_command(&hook); +} + +static void finish(const unsigned char *new_head, const char *msg) +{ + struct strbuf reflog_message; + + strbuf_init(&reflog_message, 0); + if (!msg) + strbuf_addstr(&reflog_message, getenv("GIT_REFLOG_ACTION")); + else { + printf("%s\n", msg); + strbuf_addf(&reflog_message, "%s: %s", + getenv("GIT_REFLOG_ACTION"), msg); + } + if (squash) { + squash_message(); + } else { + if (!merge_msg.len) + printf("No merge message -- not updating HEAD\n"); + else { + const char *argv_gc_auto[] = { "gc", "--auto", NULL }; + update_ref(reflog_message.buf, "HEAD", + new_head, head, 0, + DIE_ON_ERR); + /* + * We ignore errors in 'gc --auto', since the + * user should see them. + */ + run_command_v_opt(argv_gc_auto, RUN_GIT_CMD); + } + } + if (new_head && show_diffstat) { + struct diff_options opts; + diff_setup(&opts); + opts.output_format |= + DIFF_FORMAT_SUMMARY | DIFF_FORMAT_DIFFSTAT; + opts.detect_rename = DIFF_DETECT_RENAME; + if (diff_use_color_default > 0) + DIFF_OPT_SET(&opts, COLOR_DIFF); + if (diff_setup_done(&opts) < 0) + die("diff_setup_done failed"); + diff_tree_sha1(head, new_head, "", &opts); + diffcore_std(&opts); + diff_flush(&opts); + } + + /* Run a post-merge hook */ + run_hook("post-merge"); + + strbuf_release(&reflog_message); +} + +/* Get the name for the merge commit's message. */ +static void merge_name(const char *remote, struct strbuf *msg) +{ + struct object *remote_head; + unsigned char branch_head[20], buf_sha[20]; + struct strbuf buf; + const char *ptr; + int len, early; + + memset(branch_head, 0, sizeof(branch_head)); + remote_head = peel_to_type(remote, 0, NULL, OBJ_COMMIT); + if (!remote_head) + die("'%s' does not point to a commit", remote); + + strbuf_init(&buf, 0); + strbuf_addstr(&buf, "refs/heads/"); + strbuf_addstr(&buf, remote); + resolve_ref(buf.buf, branch_head, 0, 0); + + if (!hashcmp(remote_head->sha1, branch_head)) { + strbuf_addf(msg, "%s\t\tbranch '%s' of .\n", + sha1_to_hex(branch_head), remote); + return; + } + + /* See if remote matches <name>^^^.. or <name>~<number> */ + for (len = 0, ptr = remote + strlen(remote); + remote < ptr && ptr[-1] == '^'; + ptr--) + len++; + if (len) + early = 1; + else { + early = 0; + ptr = strrchr(remote, '~'); + if (ptr) { + int seen_nonzero = 0; + + len++; /* count ~ */ + while (*++ptr && isdigit(*ptr)) { + seen_nonzero |= (*ptr != '0'); + len++; + } + if (*ptr) + len = 0; /* not ...~<number> */ + else if (seen_nonzero) + early = 1; + else if (len == 1) + early = 1; /* "name~" is "name~1"! */ + } + } + if (len) { + struct strbuf truname = STRBUF_INIT; + strbuf_addstr(&truname, "refs/heads/"); + strbuf_addstr(&truname, remote); + strbuf_setlen(&truname, len+11); + if (resolve_ref(truname.buf, buf_sha, 0, 0)) { + strbuf_addf(msg, + "%s\t\tbranch '%s'%s of .\n", + sha1_to_hex(remote_head->sha1), + truname.buf, + (early ? " (early part)" : "")); + return; + } + } + + if (!strcmp(remote, "FETCH_HEAD") && + !access(git_path("FETCH_HEAD"), R_OK)) { + FILE *fp; + struct strbuf line; + char *ptr; + + strbuf_init(&line, 0); + fp = fopen(git_path("FETCH_HEAD"), "r"); + if (!fp) + die("could not open %s for reading: %s", + git_path("FETCH_HEAD"), strerror(errno)); + strbuf_getline(&line, fp, '\n'); + fclose(fp); + ptr = strstr(line.buf, "\tnot-for-merge\t"); + if (ptr) + strbuf_remove(&line, ptr-line.buf+1, 13); + strbuf_addbuf(msg, &line); + strbuf_release(&line); + return; + } + strbuf_addf(msg, "%s\t\tcommit '%s'\n", + sha1_to_hex(remote_head->sha1), remote); +} + +int git_merge_config(const char *k, const char *v, void *cb) +{ + if (branch && !prefixcmp(k, "branch.") && + !prefixcmp(k + 7, branch) && + !strcmp(k + 7 + strlen(branch), ".mergeoptions")) { + const char **argv; + int argc; + char *buf; + + buf = xstrdup(v); + argc = split_cmdline(buf, &argv); + argv = xrealloc(argv, sizeof(*argv) * (argc + 2)); + memmove(argv + 1, argv, sizeof(*argv) * (argc + 1)); + argc++; + parse_options(argc, argv, builtin_merge_options, + builtin_merge_usage, 0); + free(buf); + } + + if (!strcmp(k, "merge.diffstat") || !strcmp(k, "merge.stat")) + show_diffstat = git_config_bool(k, v); + else if (!strcmp(k, "pull.twohead")) + return git_config_string(&pull_twohead, k, v); + else if (!strcmp(k, "pull.octopus")) + return git_config_string(&pull_octopus, k, v); + return git_diff_ui_config(k, v, cb); +} + +static int read_tree_trivial(unsigned char *common, unsigned char *head, + unsigned char *one) +{ + int i, nr_trees = 0; + struct tree *trees[MAX_UNPACK_TREES]; + struct tree_desc t[MAX_UNPACK_TREES]; + struct unpack_trees_options opts; + + memset(&opts, 0, sizeof(opts)); + opts.head_idx = 2; + opts.src_index = &the_index; + opts.dst_index = &the_index; + opts.update = 1; + opts.verbose_update = 1; + opts.trivial_merges_only = 1; + opts.merge = 1; + trees[nr_trees] = parse_tree_indirect(common); + if (!trees[nr_trees++]) + return -1; + trees[nr_trees] = parse_tree_indirect(head); + if (!trees[nr_trees++]) + return -1; + trees[nr_trees] = parse_tree_indirect(one); + if (!trees[nr_trees++]) + return -1; + opts.fn = threeway_merge; + cache_tree_free(&active_cache_tree); + for (i = 0; i < nr_trees; i++) { + parse_tree(trees[i]); + init_tree_desc(t+i, trees[i]->buffer, trees[i]->size); + } + if (unpack_trees(nr_trees, t, &opts)) + return -1; + return 0; +} + +static void write_tree_trivial(unsigned char *sha1) +{ + if (write_cache_as_tree(sha1, 0, NULL)) + die("git write-tree failed to write a tree"); +} + +static int try_merge_strategy(char *strategy, struct commit_list *common, + const char *head_arg) +{ + const char **args; + int i = 0, ret; + struct commit_list *j; + struct strbuf buf; + + args = xmalloc((4 + commit_list_count(common) + + commit_list_count(remoteheads)) * sizeof(char *)); + strbuf_init(&buf, 0); + strbuf_addf(&buf, "merge-%s", strategy); + args[i++] = buf.buf; + for (j = common; j; j = j->next) + args[i++] = xstrdup(sha1_to_hex(j->item->object.sha1)); + args[i++] = "--"; + args[i++] = head_arg; + for (j = remoteheads; j; j = j->next) + args[i++] = xstrdup(sha1_to_hex(j->item->object.sha1)); + args[i] = NULL; + ret = run_command_v_opt(args, RUN_GIT_CMD); + strbuf_release(&buf); + i = 1; + for (j = common; j; j = j->next) + free((void *)args[i++]); + i += 2; + for (j = remoteheads; j; j = j->next) + free((void *)args[i++]); + free(args); + return -ret; +} + +static void count_diff_files(struct diff_queue_struct *q, + struct diff_options *opt, void *data) +{ + int *count = data; + + (*count) += q->nr; +} + +static int count_unmerged_entries(void) +{ + const struct index_state *state = &the_index; + int i, ret = 0; + + for (i = 0; i < state->cache_nr; i++) + if (ce_stage(state->cache[i])) + ret++; + + return ret; +} + +static int checkout_fast_forward(unsigned char *head, unsigned char *remote) +{ + struct tree *trees[MAX_UNPACK_TREES]; + struct unpack_trees_options opts; + struct tree_desc t[MAX_UNPACK_TREES]; + int i, fd, nr_trees = 0; + struct dir_struct dir; + struct lock_file *lock_file = xcalloc(1, sizeof(struct lock_file)); + + if (read_cache_unmerged()) + die("you need to resolve your current index first"); + + fd = hold_locked_index(lock_file, 1); + + memset(&trees, 0, sizeof(trees)); + memset(&opts, 0, sizeof(opts)); + memset(&t, 0, sizeof(t)); + dir.show_ignored = 1; + dir.exclude_per_dir = ".gitignore"; + opts.dir = &dir; + + opts.head_idx = 1; + opts.src_index = &the_index; + opts.dst_index = &the_index; + opts.update = 1; + opts.verbose_update = 1; + opts.merge = 1; + opts.fn = twoway_merge; + + trees[nr_trees] = parse_tree_indirect(head); + if (!trees[nr_trees++]) + return -1; + trees[nr_trees] = parse_tree_indirect(remote); + if (!trees[nr_trees++]) + return -1; + for (i = 0; i < nr_trees; i++) { + parse_tree(trees[i]); + init_tree_desc(t+i, trees[i]->buffer, trees[i]->size); + } + if (unpack_trees(nr_trees, t, &opts)) + return -1; + if (write_cache(fd, active_cache, active_nr) || + commit_locked_index(lock_file)) + die("unable to write new index file"); + return 0; +} + +static void split_merge_strategies(const char *string, struct path_list *list) +{ + char *p, *q, *buf; + + if (!string) + return; + + list->strdup_paths = 1; + buf = xstrdup(string); + q = buf; + for (;;) { + p = strchr(q, ' '); + if (!p) { + path_list_append(q, list); + free(buf); + return; + } else { + *p = '\0'; + path_list_append(q, list); + q = ++p; + } + } +} + +static void add_strategies(const char *string, enum strategy strategy) +{ + struct path_list list; + int i; + + memset(&list, 0, sizeof(list)); + split_merge_strategies(string, &list); + if (list.nr) { + for (i = 0; i < list.nr; i++) { + struct path_list_item *item; + + item = unsorted_path_list_lookup(list.items[i].path, + &strategies); + if (item) + path_list_append_strategy(item); + } + return; + } + for (i = 0; i < strategies.nr; i++) + if ((enum strategy)strategies.items[i].util & strategy) + path_list_append_strategy(&strategies.items[i]); +} + +static int merge_trivial(void) +{ + unsigned char result_tree[20], result_commit[20]; + struct commit_list parent; + + write_tree_trivial(result_tree); + printf("Wonderful.\n"); + parent.item = remoteheads->item; + parent.next = NULL; + commit_tree(merge_msg.buf, result_tree, &parent, result_commit); + finish(result_commit, "In-index merge"); + drop_save(); + return 0; +} + +static int finish_automerge(struct commit_list *common, + unsigned char *result_tree, + struct path_list_item *wt_strategy) +{ + struct commit_list *parents = NULL, *j; + struct strbuf buf = STRBUF_INIT; + unsigned char result_commit[20]; + + free_commit_list(common); + if (allow_fast_forward) { + parents = remoteheads; + commit_list_insert(lookup_commit(head), &parents); + parents = reduce_heads(parents); + } else { + struct commit_list **pptr = &parents; + + pptr = &commit_list_insert(lookup_commit(head), + pptr)->next; + for (j = remoteheads; j; j = j->next) + pptr = &commit_list_insert(j->item, pptr)->next; + } + free_commit_list(remoteheads); + strbuf_addch(&merge_msg, '\n'); + commit_tree(merge_msg.buf, result_tree, parents, result_commit); + strbuf_addf(&buf, "Merge made by %s.", wt_strategy->path); + finish(result_commit, buf.buf); + strbuf_release(&buf); + drop_save(); + return 0; +} + +static int suggest_conflicts(void) +{ + FILE *fp; + int pos; + + fp = fopen(git_path("MERGE_MSG"), "a"); + if (!fp) + die("Could open %s for writing", git_path("MERGE_MSG")); + fprintf(fp, "\nConflicts:\n"); + for (pos = 0; pos < active_nr; pos++) { + struct cache_entry *ce = active_cache[pos]; + + if (ce_stage(ce)) { + fprintf(fp, "\t%s\n", ce->name); + while (pos + 1 < active_nr && + !strcmp(ce->name, + active_cache[pos + 1]->name)) + pos++; + } + } + fclose(fp); + rerere(); + printf("Automatic merge failed; " + "fix conflicts and then commit the result.\n"); + return 1; +} + +static inline unsigned nth_strategy_flags(struct path_list *s, int nth) +{ + return (unsigned) s->items[nth].util; +} + +static struct commit *is_old_style_invocation(int argc, const char **argv) +{ + struct commit *second_token = NULL; + if (argc > 1) { + unsigned char second_sha1[20]; + + if (get_sha1(argv[1], second_sha1)) + return NULL; + second_token = lookup_commit_reference_gently(second_sha1, 0); + if (!second_token) + die("'%s' is not a commit", argv[1]); + if (hashcmp(second_token->object.sha1, head)) + return NULL; + } + return second_token; +} + +static int evaluate_result(void) +{ + int cnt = 0; + struct rev_info rev; + + if (read_cache() < 0) + die("failed to read the cache"); + + /* Check how many files differ. */ + init_revisions(&rev, ""); + setup_revisions(0, NULL, &rev, NULL); + rev.diffopt.output_format |= + DIFF_FORMAT_CALLBACK; + rev.diffopt.format_callback = count_diff_files; + rev.diffopt.format_callback_data = &cnt; + run_diff_files(&rev, 0); + + /* + * Check how many unmerged entries are + * there. + */ + cnt += count_unmerged_entries(); + + return cnt; +} + +int cmd_merge(int argc, const char **argv, const char *prefix) +{ + unsigned char result_tree[20]; + struct strbuf buf; + const char *head_arg; + int flag, head_invalid = 0, i; + int best_cnt = -1, merge_was_ok = 0, automerge_was_ok = 0; + struct commit_list *common = NULL; + struct path_list_item *best_strategy = NULL, *wt_strategy = NULL; + struct commit_list **remotes = &remoteheads; + + setup_work_tree(); + if (unmerged_cache()) + die("You are in the middle of a conflicted merge."); + + /* + * Check if we are _not_ on a detached HEAD, i.e. if there is a + * current branch. + */ + branch = resolve_ref("HEAD", head, 0, &flag); + if (branch && !prefixcmp(branch, "refs/heads/")) + branch += 11; + if (is_null_sha1(head)) + head_invalid = 1; + + git_config(git_merge_config, NULL); + + /* for color.ui */ + if (diff_use_color_default == -1) + diff_use_color_default = git_use_color_default; + + argc = parse_options(argc, argv, builtin_merge_options, + builtin_merge_usage, 0); + + if (squash) { + if (!allow_fast_forward) + die("You cannot combine --squash with --no-ff."); + option_commit = 0; + } + + if (!argc) + usage_with_options(builtin_merge_usage, + builtin_merge_options); + + /* + * This could be traditional "merge <msg> HEAD <commit>..." and + * the way we can tell it is to see if the second token is HEAD, + * but some people might have misused the interface and used a + * committish that is the same as HEAD there instead. + * Traditional format never would have "-m" so it is an + * additional safety measure to check for it. + */ + strbuf_init(&buf, 0); + + if (!have_message && is_old_style_invocation(argc, argv)) { + strbuf_addstr(&merge_msg, argv[0]); + head_arg = argv[1]; + argv += 2; + argc -= 2; + } else if (head_invalid) { + struct object *remote_head; + /* + * If the merged head is a valid one there is no reason + * to forbid "git merge" into a branch yet to be born. + * We do the same for "git pull". + */ + if (argc != 1) + die("Can merge only exactly one commit into " + "empty head"); + remote_head = peel_to_type(argv[0], 0, NULL, OBJ_COMMIT); + if (!remote_head) + die("%s - not something we can merge", argv[0]); + update_ref("initial pull", "HEAD", remote_head->sha1, NULL, 0, + DIE_ON_ERR); + reset_hard(remote_head->sha1, 0); + return 0; + } else { + struct strbuf msg; + + /* We are invoked directly as the first-class UI. */ + head_arg = "HEAD"; + + /* + * All the rest are the commits being merged; + * prepare the standard merge summary message to + * be appended to the given message. If remote + * is invalid we will die later in the common + * codepath so we discard the error in this + * loop. + */ + strbuf_init(&msg, 0); + for (i = 0; i < argc; i++) + merge_name(argv[i], &msg); + fmt_merge_msg(option_log, &msg, &merge_msg); + if (merge_msg.len) + strbuf_setlen(&merge_msg, merge_msg.len-1); + } + + if (head_invalid || !argc) + usage_with_options(builtin_merge_usage, + builtin_merge_options); + + strbuf_addstr(&buf, "merge"); + for (i = 0; i < argc; i++) + strbuf_addf(&buf, " %s", argv[i]); + setenv("GIT_REFLOG_ACTION", buf.buf, 0); + strbuf_reset(&buf); + + for (i = 0; i < argc; i++) { + struct object *o; + + o = peel_to_type(argv[i], 0, NULL, OBJ_COMMIT); + if (!o) + die("%s - not something we can merge", argv[i]); + remotes = &commit_list_insert(lookup_commit(o->sha1), + remotes)->next; + + strbuf_addf(&buf, "GITHEAD_%s", sha1_to_hex(o->sha1)); + setenv(buf.buf, argv[i], 1); + strbuf_reset(&buf); + } + + if (!use_strategies.nr) { + if (!remoteheads->next) + add_strategies(pull_twohead, DEFAULT_TWOHEAD); + else + add_strategies(pull_octopus, DEFAULT_OCTOPUS); + } + + for (i = 0; i < use_strategies.nr; i++) { + if (nth_strategy_flags(&use_strategies, i) & NO_FAST_FORWARD) + allow_fast_forward = 0; + if (nth_strategy_flags(&use_strategies, i) & NO_TRIVIAL) + allow_trivial = 0; + } + + if (!remoteheads->next) + common = get_merge_bases(lookup_commit(head), + remoteheads->item, 1); + else { + struct commit_list *list = remoteheads; + commit_list_insert(lookup_commit(head), &list); + common = get_octopus_merge_bases(list); + free(list); + } + + update_ref("updating ORIG_HEAD", "ORIG_HEAD", head, NULL, 0, + DIE_ON_ERR); + + if (!common) + ; /* No common ancestors found. We need a real merge. */ + else if (!remoteheads->next && !common->next && + common->item == remoteheads->item) { + /* + * If head can reach all the merge then we are up to date. + * but first the most common case of merging one remote. + */ + finish_up_to_date("Already up-to-date."); + return 0; + } else if (allow_fast_forward && !remoteheads->next && + !common->next && + !hashcmp(common->item->object.sha1, head)) { + /* Again the most common case of merging one remote. */ + struct strbuf msg; + struct object *o; + char hex[41]; + + strcpy(hex, find_unique_abbrev(remoteheads->item->object.sha1, + DEFAULT_ABBREV)); + + printf("Updating %s..%s\n", + find_unique_abbrev(head, DEFAULT_ABBREV), + hex); + refresh_cache(REFRESH_QUIET); + strbuf_init(&msg, 0); + strbuf_addstr(&msg, "Fast forward"); + if (have_message) + strbuf_addstr(&msg, + " (no commit created; -m option ignored)"); + o = peel_to_type(sha1_to_hex(remoteheads->item->object.sha1), + 0, NULL, OBJ_COMMIT); + if (!o) { + error("%s is not a commit", hex); + return 1; + } + + if (checkout_fast_forward(head, remoteheads->item->object.sha1)) { + error("failed to fast-forward to %s", hex); + return 1; + } + + finish(o->sha1, msg.buf); + drop_save(); + return 0; + } else if (!remoteheads->next && common->next) + ; + /* + * We are not doing octopus and not fast forward. Need + * a real merge. + */ + else if (!remoteheads->next && !common->next && option_commit) { + /* + * We are not doing octopus, not fast forward, and have + * only one common. + */ + refresh_cache(REFRESH_QUIET); + if (allow_trivial) { + /* See if it is really trivial. */ + git_committer_info(IDENT_ERROR_ON_NO_NAME); + printf("Trying really trivial in-index merge...\n"); + if (!read_tree_trivial(common->item->object.sha1, + head, remoteheads->item->object.sha1)) + return merge_trivial(); + printf("Nope.\n"); + } + } else { + /* + * An octopus. If we can reach all the remote we are up + * to date. + */ + int up_to_date = 1; + struct commit_list *j; + + for (j = remoteheads; j; j = j->next) { + struct commit_list *common_one; + + /* + * Here we *have* to calculate the individual + * merge_bases again, otherwise "git merge HEAD^ + * HEAD^^" would be missed. + */ + common_one = get_merge_bases(lookup_commit(head), + j->item, 1); + if (hashcmp(common_one->item->object.sha1, + j->item->object.sha1)) { + up_to_date = 0; + break; + } + } + if (up_to_date) { + finish_up_to_date("Already up-to-date. Yeeah!"); + return 0; + } + } + + /* We are going to make a new commit. */ + git_committer_info(IDENT_ERROR_ON_NO_NAME); + + /* + * At this point, we need a real merge. No matter what strategy + * we use, it would operate on the index, possibly affecting the + * working tree, and when resolved cleanly, have the desired + * tree in the index -- this means that the index must be in + * sync with the head commit. The strategies are responsible + * to ensure this. + */ + if (use_strategies.nr != 1) { + /* + * Stash away the local changes so that we can try more + * than one. + */ + save_state(); + } else { + memcpy(stash, null_sha1, 20); + } + + for (i = 0; i < use_strategies.nr; i++) { + int ret; + if (i) { + printf("Rewinding the tree to pristine...\n"); + restore_state(); + } + if (use_strategies.nr != 1) + printf("Trying merge strategy %s...\n", + use_strategies.items[i].path); + /* + * Remember which strategy left the state in the working + * tree. + */ + wt_strategy = &use_strategies.items[i]; + + ret = try_merge_strategy(use_strategies.items[i].path, + common, head_arg); + if (!option_commit && !ret) { + merge_was_ok = 1; + /* + * This is necessary here just to avoid writing + * the tree, but later we will *not* exit with + * status code 1 because merge_was_ok is set. + */ + ret = 1; + } + + if (ret) { + /* + * The backend exits with 1 when conflicts are + * left to be resolved, with 2 when it does not + * handle the given merge at all. + */ + if (ret == 1) { + int cnt = evaluate_result(); + + if (best_cnt <= 0 || cnt <= best_cnt) { + best_strategy = + &use_strategies.items[i]; + best_cnt = cnt; + } + } + if (merge_was_ok) + break; + else + continue; + } + + /* Automerge succeeded. */ + write_tree_trivial(result_tree); + automerge_was_ok = 1; + break; + } + + /* + * If we have a resulting tree, that means the strategy module + * auto resolved the merge cleanly. + */ + if (automerge_was_ok) + return finish_automerge(common, result_tree, wt_strategy); + + /* + * Pick the result from the best strategy and have the user fix + * it up. + */ + if (!best_strategy) { + restore_state(); + if (use_strategies.nr > 1) + fprintf(stderr, + "No merge strategy handled the merge.\n"); + else + fprintf(stderr, "Merge with strategy %s failed.\n", + use_strategies.items[0].path); + return 2; + } else if (best_strategy == wt_strategy) + ; /* We already have its result in the working tree. */ + else { + printf("Rewinding the tree to pristine...\n"); + restore_state(); + printf("Using the %s to prepare resolving by hand.\n", + best_strategy->path); + try_merge_strategy(best_strategy->path, common, head_arg); + } + + if (squash) + finish(NULL, NULL); + else { + int fd; + struct commit_list *j; + + for (j = remoteheads; j; j = j->next) + strbuf_addf(&buf, "%s\n", + sha1_to_hex(j->item->object.sha1)); + fd = open(git_path("MERGE_HEAD"), O_WRONLY | O_CREAT, 0666); + if (fd < 0) + die("Could open %s for writing", + git_path("MERGE_HEAD")); + if (write_in_full(fd, buf.buf, buf.len) != buf.len) + die("Could not write to %s", git_path("MERGE_HEAD")); + close(fd); + strbuf_addch(&merge_msg, '\n'); + fd = open(git_path("MERGE_MSG"), O_WRONLY | O_CREAT, 0666); + if (fd < 0) + die("Could open %s for writing", git_path("MERGE_MSG")); + if (write_in_full(fd, merge_msg.buf, merge_msg.len) != + merge_msg.len) + die("Could not write to %s", git_path("MERGE_MSG")); + close(fd); + } + + if (merge_was_ok) { + fprintf(stderr, "Automatic merge went well; " + "stopped before committing as requested\n"); + return 0; + } else + return suggest_conflicts(); +} diff --git a/builtin.h b/builtin.h index 05ee56f..0e605d4 100644 --- a/builtin.h +++ b/builtin.h @@ -64,6 +64,7 @@ extern int cmd_ls_tree(int argc, const char **argv, const char *prefix); extern int cmd_ls_remote(int argc, const char **argv, const char *prefix); extern int cmd_mailinfo(int argc, const char **argv, const char *prefix); extern int cmd_mailsplit(int argc, const char **argv, const char *prefix); +extern int cmd_merge(int argc, const char **argv, const char *prefix); extern int cmd_merge_base(int argc, const char **argv, const char *prefix); extern int cmd_merge_ours(int argc, const char **argv, const char *prefix); extern int cmd_merge_file(int argc, const char **argv, const char *prefix); diff --git a/git-merge.sh b/contrib/examples/git-merge.sh similarity index 100% rename from git-merge.sh rename to contrib/examples/git-merge.sh diff --git a/git.c b/git.c index 2fbe96b..770aadd 100644 --- a/git.c +++ b/git.c @@ -271,6 +271,7 @@ static void handle_internal_command(int argc, const char **argv) { "ls-remote", cmd_ls_remote }, { "mailinfo", cmd_mailinfo }, { "mailsplit", cmd_mailsplit }, + { "merge", cmd_merge, RUN_SETUP | NEED_WORK_TREE }, { "merge-base", cmd_merge_base, RUN_SETUP }, { "merge-file", cmd_merge_file }, { "merge-ours", cmd_merge_ours, RUN_SETUP }, -- 1.5.6.1 ^ permalink raw reply related [flat|nested] 82+ messages in thread
* Re: [PATCH 14/14] Build in merge 2008-07-01 13:18 ` Miklos Vajna @ 2008-07-01 23:55 ` Junio C Hamano 2008-07-02 7:43 ` Miklos Vajna 2008-07-06 8:50 ` Junio C Hamano 1 sibling, 1 reply; 82+ messages in thread From: Junio C Hamano @ 2008-07-01 23:55 UTC (permalink / raw To: Miklos Vajna; +Cc: git, Johannes Schindelin, Olivier Marin Miklos Vajna <vmiklos@frugalware.org> writes: > Mentored-by: Johannes Schindelin <Johannes.Schindelin@gmx.de> > Signed-off-by: Miklos Vajna <vmiklos@frugalware.org> > --- > > Interdiff: git diff 01a7dae..7709df7 > > Individual commits (there is only one change this time): git log 7709df7 > > Ah and please don't forget to apply the testsuite fix I sent in > $gmane/86981. (Or is it easier if I resend the whole series?) Are you sure about that? I've been deliberately fixing that patch. As far as I understand it, your series: - starts out building infrastructure; - adds tests; - finally replaces git-merge with the new implementation. and the test in question comes when git-merge built from that version is still the scripted version with limitation. So if you build and test after applying individual steps (you sure do that, don't you?), the test has to start with expecting failure (from the scripted git-merge that has the octopus limitation) and then later changed to expect success when git-merge is replaced. Have you looked at the series I fixed up before pushing out in 'pu' last night, specifically 5948e2a and 01a7dae? If you actually built and tested 5948e2a, you'd notice that the test in that commit *must* expect failure. ^ permalink raw reply [flat|nested] 82+ messages in thread
* Re: [PATCH 14/14] Build in merge 2008-07-01 23:55 ` Junio C Hamano @ 2008-07-02 7:43 ` Miklos Vajna 0 siblings, 0 replies; 82+ messages in thread From: Miklos Vajna @ 2008-07-02 7:43 UTC (permalink / raw To: Junio C Hamano; +Cc: git, Johannes Schindelin, Olivier Marin [-- Attachment #1: Type: text/plain, Size: 366 bytes --] On Tue, Jul 01, 2008 at 04:55:21PM -0700, Junio C Hamano <gitster@pobox.com> wrote: > Have you looked at the series I fixed up before pushing out in 'pu' last > night, specifically 5948e2a and 01a7dae? If you actually built and tested > 5948e2a, you'd notice that the test in that commit *must* expect failure. Aah. Now I see the point, thanks for explaining! :-) [-- Attachment #2: Type: application/pgp-signature, Size: 197 bytes --] ^ permalink raw reply [flat|nested] 82+ messages in thread
* Re: [PATCH 14/14] Build in merge 2008-07-01 13:18 ` Miklos Vajna 2008-07-01 23:55 ` Junio C Hamano @ 2008-07-06 8:50 ` Junio C Hamano 2008-07-06 9:43 ` Junio C Hamano ` (2 more replies) 1 sibling, 3 replies; 82+ messages in thread From: Junio C Hamano @ 2008-07-06 8:50 UTC (permalink / raw To: Miklos Vajna; +Cc: git, Johannes Schindelin, Olivier Marin Miklos Vajna <vmiklos@frugalware.org> writes: > diff --git a/builtin-merge.c b/builtin-merge.c > new file mode 100644 > index 0000000..b261993 > --- /dev/null > +++ b/builtin-merge.c > @@ -0,0 +1,1158 @@ > +/* > + * Builtin "git merge" > + * > + * Copyright (c) 2008 Miklos Vajna <vmiklos@frugalware.org> > + * > + * Based on git-merge.sh by Junio C Hamano. > + */ > + > +#include "cache.h" > +#include "parse-options.h" > +#include "builtin.h" > +#include "run-command.h" > +#include "path-list.h" > +#include "diff.h" > +#include "refs.h" > +#include "commit.h" > +#include "diffcore.h" > +#include "revision.h" > +#include "unpack-trees.h" > +#include "cache-tree.h" > +#include "dir.h" > +#include "utf8.h" > +#include "log-tree.h" > +#include "color.h" > + > +enum strategy { > + DEFAULT_TWOHEAD = 1, > + DEFAULT_OCTOPUS = 2, > + NO_FAST_FORWARD = 4, > + NO_TRIVIAL = 8 > +}; Usually "enum foo" consists of possible values of "foo". But this is not a list of strategies. These are possible attributes to strategies. > +static const char * const builtin_merge_usage[] = { > + "git-merge [options] <remote>...", > + "git-merge [options] <msg> HEAD <remote>", > + NULL > +}; > + > +static int show_diffstat = 1, option_log, squash; > +static int option_commit = 1, allow_fast_forward = 1; > +static int allow_trivial = 1, have_message; > +static struct strbuf merge_msg; > +static struct commit_list *remoteheads; > +static unsigned char head[20], stash[20]; > +static struct path_list use_strategies; > +static const char *branch; > + > +static struct path_list_item strategy_items[] = { > + { "recur", (void *)NO_TRIVIAL }, > + { "recursive", (void *)(DEFAULT_TWOHEAD | NO_TRIVIAL) }, > + { "octopus", (void *)DEFAULT_OCTOPUS }, > + { "resolve", (void *)0 }, > + { "stupid", (void *)0 }, > + { "ours", (void *)(NO_FAST_FORWARD | NO_TRIVIAL) }, > + { "subtree", (void *)(NO_FAST_FORWARD | NO_TRIVIAL) }, > +}; > +static struct path_list strategies = { strategy_items, > + ARRAY_SIZE(strategy_items), 0, 0 }; This declaration is funnily line-wrapped. static struct path_list strategies = { strategy_items, ARRAY_SIZE(strategy_items), 0, 0, }; But more problematic is that a path_list is inherently a dynamic data structure (you can add and it reallocs), and this use of relying on the knowledge that you happen to never add anything (nor subtract anything) from the list is a mere hack. If on the other hand you (and more importantly other people who touch this implementation later) will never add or remove items from this "strategies" array, you should make sure at the interface level that nobody can -- one way to do so is not to abuse path_list for something like this. Come to think of it, wasn't the reason why the earlier "Why do you need such distracting casts all over the place?" issue came up in another patch because of this kind of (ab)use of path_list, which is an inappropriate data structure for the job? You would perhaps define: #define DEFAULT_TWOHEAD (1<<0) #define DEFAULT_OCTOPUS (1<<1) #define NO_FAST_FORWARD (1<<2) #define NO_TRIVIAL (1<<3) static struct strategy { char *name; unsigned attr; } all_strategy[] = { { "octopus", DEFAULT_OCTOPUS }, { "ours", (NO_FAST_FORWARD | NO_TRIVIAL) }, { "recur", NO_TRIVIAL }, { "recursive", (DEFAULT_TWOHEAD | NO_TRIVIAL) }, { "resolve", 0 }, { "stupid", 0 }, { "subtree", (NO_FAST_FORWARD | NO_TRIVIAL) }, }; And "unsorted_path_list_lookup()" can now become much more natural, perhaps: static struct strategy *get_strategy(const char *name); which has a more natural function signature and much better name. Then, you would keep an array of pointers into all_strategy[] array to represent the list of "-s strategy" given by the user: static struct strategy *use_strategy; static int use_strategy_alloc, use_strategy_nr; and have a function that use s the standard ALLOC_GROW() and friends to grow this. The function will be named and written more naturally (i.e. path_list_append_strategy() can go) --- this does not have anything to do with path_list, but it is about "merge strategy". But 99.9% of the time you would not have more than one elements ;-). ^ permalink raw reply [flat|nested] 82+ messages in thread
* Re: [PATCH 14/14] Build in merge 2008-07-06 8:50 ` Junio C Hamano @ 2008-07-06 9:43 ` Junio C Hamano 2008-07-07 17:17 ` Miklos Vajna 2008-07-06 12:38 ` [PATCH 14/14] " Johannes Schindelin 2008-07-07 17:24 ` [PATCH] " Miklos Vajna 2 siblings, 1 reply; 82+ messages in thread From: Junio C Hamano @ 2008-07-06 9:43 UTC (permalink / raw To: Miklos Vajna; +Cc: git, Johannes Schindelin, Olivier Marin Junio C Hamano <gitster@pobox.com> writes: > You would perhaps define: > > #define DEFAULT_TWOHEAD (1<<0) > #define DEFAULT_OCTOPUS (1<<1) > #define NO_FAST_FORWARD (1<<2) > #define NO_TRIVIAL (1<<3) > > static struct strategy { > char *name; > unsigned attr; > } all_strategy[] = { > { "octopus", DEFAULT_OCTOPUS }, > { "ours", (NO_FAST_FORWARD | NO_TRIVIAL) }, > { "recur", NO_TRIVIAL }, > { "recursive", (DEFAULT_TWOHEAD | NO_TRIVIAL) }, > { "resolve", 0 }, > { "stupid", 0 }, > { "subtree", (NO_FAST_FORWARD | NO_TRIVIAL) }, > }; > > And "unsorted_path_list_lookup()" can now become much more natural, > perhaps: > > static struct strategy *get_strategy(const char *name); > > which has a more natural function signature and much better name. > > Then, you would keep an array of pointers into all_strategy[] array to > represent the list of "-s strategy" given by the user: > > static struct strategy *use_strategy; > static int use_strategy_alloc, use_strategy_nr; Sorry, I have an obvious typo here. "use_strategy" will be dynamic array of pointers into all_strategy[] so its definition would be: static struct strategy **use_strategy; > and have a function that use s the standard ALLOC_GROW() and friends to > grow this. The function will be named and written more naturally > (i.e. path_list_append_strategy() can go) --- this does not have anything > to do with path_list, but it is about "merge strategy". ^ permalink raw reply [flat|nested] 82+ messages in thread
* Re: [PATCH 14/14] Build in merge 2008-07-06 9:43 ` Junio C Hamano @ 2008-07-07 17:17 ` Miklos Vajna 2008-07-07 18:15 ` Junio C Hamano 0 siblings, 1 reply; 82+ messages in thread From: Miklos Vajna @ 2008-07-07 17:17 UTC (permalink / raw To: Junio C Hamano; +Cc: git, Johannes Schindelin, Olivier Marin [-- Attachment #1: Type: text/plain, Size: 1193 bytes --] On Sun, Jul 06, 2008 at 02:43:41AM -0700, Junio C Hamano <gitster@pobox.com> wrote: > > Then, you would keep an array of pointers into all_strategy[] array to > > represent the list of "-s strategy" given by the user: > > > > static struct strategy *use_strategy; > > static int use_strategy_alloc, use_strategy_nr; > > Sorry, I have an obvious typo here. "use_strategy" will be dynamic array > of pointers into all_strategy[] so its definition would be: > > static struct strategy **use_strategy; I think there are two possibilities here: 1) Append custom strategies to all_strategy and have only pointers in use_strategy. This is what you suggest in your second mail. 2) Copy the names and attributes from all_strategy to use_strategies and append custom strategies there. This is what I do at the moment. I think it's better not to modify all_strategy, it serves as a reference, for example later this would allow us to check if the used merge strategy is a predefined or a custom one. Or is there any strong reason introducing all_strategy_alloc, all_strategy_nr and using ALLOC_GROW() with all_strategy instead of with use_strategy? Thanks. [-- Attachment #2: Type: application/pgp-signature, Size: 197 bytes --] ^ permalink raw reply [flat|nested] 82+ messages in thread
* Re: [PATCH 14/14] Build in merge 2008-07-07 17:17 ` Miklos Vajna @ 2008-07-07 18:15 ` Junio C Hamano 2008-07-07 23:42 ` [PATCH] " Miklos Vajna 0 siblings, 1 reply; 82+ messages in thread From: Junio C Hamano @ 2008-07-07 18:15 UTC (permalink / raw To: Miklos Vajna; +Cc: git, Johannes Schindelin, Olivier Marin Miklos Vajna <vmiklos@frugalware.org> writes: > I think it's better not to modify all_strategy, it serves as a > reference, for example later this would allow us to check if the used > merge strategy is a predefined or a custom one. I do not get you on this point. Which one is nicer? (1) Have two lists, perhaps all_* and user_*. The logic that finds a strategy searches in two lists. The logic that checks if a given strategy is built-in checks if it is on all_* list. (2) Have a single list, but add a boolean "unsigned is_builtin:1" to each element of it. The logic that finds a strategy looks in this single list. The logic that checks if a given strategy is built-in looks at the strategy instance and it has the bit already. You seem to be advocating (1) but I do not understand why... ^ permalink raw reply [flat|nested] 82+ messages in thread
* [PATCH] Build in merge 2008-07-07 18:15 ` Junio C Hamano @ 2008-07-07 23:42 ` Miklos Vajna 2008-07-08 0:32 ` Junio C Hamano 0 siblings, 1 reply; 82+ messages in thread From: Miklos Vajna @ 2008-07-07 23:42 UTC (permalink / raw To: To: Junio C Hamano; +Cc: git, Johannes Schindelin, Olivier Marin Mentored-by: Johannes Schindelin <Johannes.Schindelin@gmx.de> Signed-off-by: Miklos Vajna <vmiklos@frugalware.org> --- On Mon, Jul 07, 2008 at 11:15:09AM -0700, Junio C Hamano <gitster@pobox.com> wrote: > I do not get you on this point. Which one is nicer? > > (1) Have two lists, perhaps all_* and user_*. The logic that finds a > strategy searches in two lists. The logic that checks if a given > strategy is built-in checks if it is on all_* list. > > (2) Have a single list, but add a boolean "unsigned is_builtin:1" to > each > element of it. The logic that finds a strategy looks in this > single > list. The logic that checks if a given strategy is built-in > looks at > the strategy instance and it has the bit already. > > You seem to be advocating (1) but I do not understand why... Ah, OK. For now, I just added an "unsigned enabled:1;". Later we can add an "unsigned is_buildin:1;" as well, but currently we die with earlier with a "Could not find merge strategy" error message, so is_builtin would be always true. So here is a version, this time without the use_strategies list. Interdiff: git diff 638c51f..1e74660 Makefile | 2 +- builtin-merge.c | 1142 +++++++++++++++++++++++++ builtin.h | 1 + git-merge.sh => contrib/examples/git-merge.sh | 0 git.c | 1 + t/t7602-merge-octopus-many.sh | 2 +- 6 files changed, 1146 insertions(+), 2 deletions(-) create mode 100644 builtin-merge.c rename git-merge.sh => contrib/examples/git-merge.sh (100%) diff --git a/Makefile b/Makefile index bf77292..fbc53e9 100644 --- a/Makefile +++ b/Makefile @@ -240,7 +240,6 @@ SCRIPT_SH += git-lost-found.sh SCRIPT_SH += git-merge-octopus.sh SCRIPT_SH += git-merge-one-file.sh SCRIPT_SH += git-merge-resolve.sh -SCRIPT_SH += git-merge.sh SCRIPT_SH += git-merge-stupid.sh SCRIPT_SH += git-mergetool.sh SCRIPT_SH += git-parse-remote.sh @@ -515,6 +514,7 @@ BUILTIN_OBJS += builtin-ls-remote.o BUILTIN_OBJS += builtin-ls-tree.o BUILTIN_OBJS += builtin-mailinfo.o BUILTIN_OBJS += builtin-mailsplit.o +BUILTIN_OBJS += builtin-merge.o BUILTIN_OBJS += builtin-merge-base.o BUILTIN_OBJS += builtin-merge-file.o BUILTIN_OBJS += builtin-merge-ours.o diff --git a/builtin-merge.c b/builtin-merge.c new file mode 100644 index 0000000..6d801dd --- /dev/null +++ b/builtin-merge.c @@ -0,0 +1,1142 @@ +/* + * Builtin "git merge" + * + * Copyright (c) 2008 Miklos Vajna <vmiklos@frugalware.org> + * + * Based on git-merge.sh by Junio C Hamano. + */ + +#include "cache.h" +#include "parse-options.h" +#include "builtin.h" +#include "run-command.h" +#include "diff.h" +#include "refs.h" +#include "commit.h" +#include "diffcore.h" +#include "revision.h" +#include "unpack-trees.h" +#include "cache-tree.h" +#include "dir.h" +#include "utf8.h" +#include "log-tree.h" +#include "color.h" + +#define DEFAULT_TWOHEAD (1<<0) +#define DEFAULT_OCTOPUS (1<<1) +#define NO_FAST_FORWARD (1<<2) +#define NO_TRIVIAL (1<<3) + +struct strategy { + const char *name; + unsigned attr; + unsigned enabled:1; +}; + +static const char * const builtin_merge_usage[] = { + "git-merge [options] <remote>...", + "git-merge [options] <msg> HEAD <remote>", + NULL +}; + +static int show_diffstat = 1, option_log, squash; +static int option_commit = 1, allow_fast_forward = 1; +static int allow_trivial = 1, have_message; +static struct strbuf merge_msg; +static struct commit_list *remoteheads; +static unsigned char head[20], stash[20]; +static const char *branch; + +static struct strategy all_strategy[] = { + { "recur", NO_TRIVIAL, 0 }, + { "recursive", DEFAULT_TWOHEAD | NO_TRIVIAL, 0 }, + { "octopus", DEFAULT_OCTOPUS, 0 }, + { "resolve", 0, 0 }, + { "stupid", 0, 0 }, + { "ours", NO_FAST_FORWARD | NO_TRIVIAL, 0 }, + { "subtree", NO_FAST_FORWARD | NO_TRIVIAL, 0 }, +}; +/* + * This contains the number of enabled variables, so you don't have to + * scan through all_strategies all the time. + */ +static int enabled_strategies; + +static const char *pull_twohead, *pull_octopus; + +static int option_parse_message(const struct option *opt, + const char *arg, int unset) +{ + struct strbuf *buf = opt->value; + + if (unset) + strbuf_setlen(buf, 0); + else { + strbuf_addf(buf, "%s\n\n", arg); + have_message = 1; + } + return 0; +} + +static void enable_strategy(const char *name) +{ + int i; + struct strbuf err; + + enabled_strategies++; + for (i = 0; i < ARRAY_SIZE(all_strategy); i++) + if (!strcmp(all_strategy[i].name, name)) { + all_strategy[i].enabled = 1; + return; + } + + strbuf_init(&err, 0); + for (i = 0; i < ARRAY_SIZE(all_strategy); i++) + strbuf_addf(&err, " %s", all_strategy[i].name); + fprintf(stderr, "Could not find merge strategy '%s'.\n", name); + fprintf(stderr, "Available strategies are:%s.\n", err.buf); + exit(1); +} + +static int option_parse_strategy(const struct option *opt, + const char *arg, int unset) +{ + if (unset) + return 0; + + enable_strategy(arg); + return 0; +} + +static int option_parse_n(const struct option *opt, + const char *arg, int unset) +{ + show_diffstat = unset; + return 0; +} + +static struct option builtin_merge_options[] = { + { OPTION_CALLBACK, 'n', NULL, NULL, NULL, + "do not show a diffstat at the end of the merge", + PARSE_OPT_NOARG, option_parse_n }, + OPT_BOOLEAN(0, "stat", &show_diffstat, + "show a diffstat at the end of the merge"), + OPT_BOOLEAN(0, "summary", &show_diffstat, "(synonym to --stat)"), + OPT_BOOLEAN(0, "log", &option_log, + "add list of one-line log to merge commit message"), + OPT_BOOLEAN(0, "squash", &squash, + "create a single commit instead of doing a merge"), + OPT_BOOLEAN(0, "commit", &option_commit, + "perform a commit if the merge succeeds (default)"), + OPT_BOOLEAN(0, "ff", &allow_fast_forward, + "allow fast forward (default)"), + OPT_CALLBACK('s', "strategy", &all_strategy, "strategy", + "merge strategy to use", option_parse_strategy), + OPT_CALLBACK('m', "message", &merge_msg, "message", + "message to be used for the merge commit (if any)", + option_parse_message), + OPT_END() +}; + +/* Cleans up metadata that is uninteresting after a succeeded merge. */ +static void drop_save(void) +{ + unlink(git_path("MERGE_HEAD")); + unlink(git_path("MERGE_MSG")); +} + +static void save_state(void) +{ + int len; + struct child_process cp; + struct strbuf buffer = STRBUF_INIT; + const char *argv[] = {"stash", "create", NULL}; + + memset(&cp, 0, sizeof(cp)); + cp.argv = argv; + cp.out = -1; + cp.git_cmd = 1; + + if (start_command(&cp)) + die("could not run stash."); + len = strbuf_read(&buffer, cp.out, 1024); + close(cp.out); + + if (finish_command(&cp) || len < 0) + die("stash failed"); + else if (!len) + return; + strbuf_setlen(&buffer, buffer.len-1); + if (get_sha1(buffer.buf, stash)) + die("not a valid object: %s", buffer.buf); +} + +static void reset_hard(unsigned const char *sha1, int verbose) +{ + int i = 0; + const char *args[6]; + + args[i++] = "read-tree"; + if (verbose) + args[i++] = "-v"; + args[i++] = "--reset"; + args[i++] = "-u"; + args[i++] = sha1_to_hex(sha1); + args[i] = NULL; + + if (run_command_v_opt(args, RUN_GIT_CMD)) + die("read-tree failed"); +} + +static void restore_state(void) +{ + struct strbuf sb; + const char *args[] = { "stash", "apply", NULL, NULL }; + + if (is_null_sha1(stash)) + return; + + reset_hard(head, 1); + + strbuf_init(&sb, 0); + args[2] = sha1_to_hex(stash); + + /* + * It is OK to ignore error here, for example when there was + * nothing to restore. + */ + run_command_v_opt(args, RUN_GIT_CMD); + + strbuf_release(&sb); + refresh_cache(REFRESH_QUIET); +} + +/* This is called when no merge was necessary. */ +static void finish_up_to_date(const char *msg) +{ + printf("%s%s\n", squash ? " (nothing to squash)" : "", msg); + drop_save(); +} + +static void squash_message(void) +{ + struct rev_info rev; + struct commit *commit; + struct strbuf out; + struct commit_list *j; + int fd; + + printf("Squash commit -- not updating HEAD\n"); + fd = open(git_path("SQUASH_MSG"), O_WRONLY | O_CREAT, 0666); + if (fd < 0) + die("Could not write to %s", git_path("SQUASH_MSG")); + + init_revisions(&rev, NULL); + rev.ignore_merges = 1; + rev.commit_format = CMIT_FMT_MEDIUM; + + commit = lookup_commit(head); + commit->object.flags |= UNINTERESTING; + add_pending_object(&rev, &commit->object, NULL); + + for (j = remoteheads; j; j = j->next) + add_pending_object(&rev, &j->item->object, NULL); + + setup_revisions(0, NULL, &rev, NULL); + if (prepare_revision_walk(&rev)) + die("revision walk setup failed"); + + strbuf_init(&out, 0); + strbuf_addstr(&out, "Squashed commit of the following:\n"); + while ((commit = get_revision(&rev)) != NULL) { + strbuf_addch(&out, '\n'); + strbuf_addf(&out, "commit %s\n", + sha1_to_hex(commit->object.sha1)); + pretty_print_commit(rev.commit_format, commit, &out, rev.abbrev, + NULL, NULL, rev.date_mode, 0); + } + write(fd, out.buf, out.len); + close(fd); + strbuf_release(&out); +} + +static int run_hook(const char *name) +{ + struct child_process hook; + const char *argv[3], *env[2]; + char index[PATH_MAX]; + + argv[0] = git_path("hooks/%s", name); + if (access(argv[0], X_OK) < 0) + return 0; + + snprintf(index, sizeof(index), "GIT_INDEX_FILE=%s", get_index_file()); + env[0] = index; + env[1] = NULL; + + if (squash) + argv[1] = "1"; + else + argv[1] = "0"; + argv[2] = NULL; + + memset(&hook, 0, sizeof(hook)); + hook.argv = argv; + hook.no_stdin = 1; + hook.stdout_to_stderr = 1; + hook.env = env; + + return run_command(&hook); +} + +static void finish(const unsigned char *new_head, const char *msg) +{ + struct strbuf reflog_message; + + strbuf_init(&reflog_message, 0); + if (!msg) + strbuf_addstr(&reflog_message, getenv("GIT_REFLOG_ACTION")); + else { + printf("%s\n", msg); + strbuf_addf(&reflog_message, "%s: %s", + getenv("GIT_REFLOG_ACTION"), msg); + } + if (squash) { + squash_message(); + } else { + if (!merge_msg.len) + printf("No merge message -- not updating HEAD\n"); + else { + const char *argv_gc_auto[] = { "gc", "--auto", NULL }; + update_ref(reflog_message.buf, "HEAD", + new_head, head, 0, + DIE_ON_ERR); + /* + * We ignore errors in 'gc --auto', since the + * user should see them. + */ + run_command_v_opt(argv_gc_auto, RUN_GIT_CMD); + } + } + if (new_head && show_diffstat) { + struct diff_options opts; + diff_setup(&opts); + opts.output_format |= + DIFF_FORMAT_SUMMARY | DIFF_FORMAT_DIFFSTAT; + opts.detect_rename = DIFF_DETECT_RENAME; + if (diff_use_color_default > 0) + DIFF_OPT_SET(&opts, COLOR_DIFF); + if (diff_setup_done(&opts) < 0) + die("diff_setup_done failed"); + diff_tree_sha1(head, new_head, "", &opts); + diffcore_std(&opts); + diff_flush(&opts); + } + + /* Run a post-merge hook */ + run_hook("post-merge"); + + strbuf_release(&reflog_message); +} + +/* Get the name for the merge commit's message. */ +static void merge_name(const char *remote, struct strbuf *msg) +{ + struct object *remote_head; + unsigned char branch_head[20], buf_sha[20]; + struct strbuf buf; + const char *ptr; + int len, early; + + memset(branch_head, 0, sizeof(branch_head)); + remote_head = peel_to_type(remote, 0, NULL, OBJ_COMMIT); + if (!remote_head) + die("'%s' does not point to a commit", remote); + + strbuf_init(&buf, 0); + strbuf_addstr(&buf, "refs/heads/"); + strbuf_addstr(&buf, remote); + resolve_ref(buf.buf, branch_head, 0, 0); + + if (!hashcmp(remote_head->sha1, branch_head)) { + strbuf_addf(msg, "%s\t\tbranch '%s' of .\n", + sha1_to_hex(branch_head), remote); + return; + } + + /* See if remote matches <name>^^^.. or <name>~<number> */ + for (len = 0, ptr = remote + strlen(remote); + remote < ptr && ptr[-1] == '^'; + ptr--) + len++; + if (len) + early = 1; + else { + early = 0; + ptr = strrchr(remote, '~'); + if (ptr) { + int seen_nonzero = 0; + + len++; /* count ~ */ + while (*++ptr && isdigit(*ptr)) { + seen_nonzero |= (*ptr != '0'); + len++; + } + if (*ptr) + len = 0; /* not ...~<number> */ + else if (seen_nonzero) + early = 1; + else if (len == 1) + early = 1; /* "name~" is "name~1"! */ + } + } + if (len) { + struct strbuf truname = STRBUF_INIT; + strbuf_addstr(&truname, "refs/heads/"); + strbuf_addstr(&truname, remote); + strbuf_setlen(&truname, len+11); + if (resolve_ref(truname.buf, buf_sha, 0, 0)) { + strbuf_addf(msg, + "%s\t\tbranch '%s'%s of .\n", + sha1_to_hex(remote_head->sha1), + truname.buf, + (early ? " (early part)" : "")); + return; + } + } + + if (!strcmp(remote, "FETCH_HEAD") && + !access(git_path("FETCH_HEAD"), R_OK)) { + FILE *fp; + struct strbuf line; + char *ptr; + + strbuf_init(&line, 0); + fp = fopen(git_path("FETCH_HEAD"), "r"); + if (!fp) + die("could not open %s for reading: %s", + git_path("FETCH_HEAD"), strerror(errno)); + strbuf_getline(&line, fp, '\n'); + fclose(fp); + ptr = strstr(line.buf, "\tnot-for-merge\t"); + if (ptr) + strbuf_remove(&line, ptr-line.buf+1, 13); + strbuf_addbuf(msg, &line); + strbuf_release(&line); + return; + } + strbuf_addf(msg, "%s\t\tcommit '%s'\n", + sha1_to_hex(remote_head->sha1), remote); +} + +int git_merge_config(const char *k, const char *v, void *cb) +{ + if (branch && !prefixcmp(k, "branch.") && + !prefixcmp(k + 7, branch) && + !strcmp(k + 7 + strlen(branch), ".mergeoptions")) { + const char **argv; + int argc; + char *buf; + + buf = xstrdup(v); + argc = split_cmdline(buf, &argv); + argv = xrealloc(argv, sizeof(*argv) * (argc + 2)); + memmove(argv + 1, argv, sizeof(*argv) * (argc + 1)); + argc++; + parse_options(argc, argv, builtin_merge_options, + builtin_merge_usage, 0); + free(buf); + } + + if (!strcmp(k, "merge.diffstat") || !strcmp(k, "merge.stat")) + show_diffstat = git_config_bool(k, v); + else if (!strcmp(k, "pull.twohead")) + return git_config_string(&pull_twohead, k, v); + else if (!strcmp(k, "pull.octopus")) + return git_config_string(&pull_octopus, k, v); + return git_diff_ui_config(k, v, cb); +} + +static int read_tree_trivial(unsigned char *common, unsigned char *head, + unsigned char *one) +{ + int i, nr_trees = 0; + struct tree *trees[MAX_UNPACK_TREES]; + struct tree_desc t[MAX_UNPACK_TREES]; + struct unpack_trees_options opts; + + memset(&opts, 0, sizeof(opts)); + opts.head_idx = 2; + opts.src_index = &the_index; + opts.dst_index = &the_index; + opts.update = 1; + opts.verbose_update = 1; + opts.trivial_merges_only = 1; + opts.merge = 1; + trees[nr_trees] = parse_tree_indirect(common); + if (!trees[nr_trees++]) + return -1; + trees[nr_trees] = parse_tree_indirect(head); + if (!trees[nr_trees++]) + return -1; + trees[nr_trees] = parse_tree_indirect(one); + if (!trees[nr_trees++]) + return -1; + opts.fn = threeway_merge; + cache_tree_free(&active_cache_tree); + for (i = 0; i < nr_trees; i++) { + parse_tree(trees[i]); + init_tree_desc(t+i, trees[i]->buffer, trees[i]->size); + } + if (unpack_trees(nr_trees, t, &opts)) + return -1; + return 0; +} + +static void write_tree_trivial(unsigned char *sha1) +{ + if (write_cache_as_tree(sha1, 0, NULL)) + die("git write-tree failed to write a tree"); +} + +static int try_merge_strategy(const char *strategy, struct commit_list *common, + const char *head_arg) +{ + const char **args; + int i = 0, ret; + struct commit_list *j; + struct strbuf buf; + + args = xmalloc((4 + commit_list_count(common) + + commit_list_count(remoteheads)) * sizeof(char *)); + strbuf_init(&buf, 0); + strbuf_addf(&buf, "merge-%s", strategy); + args[i++] = buf.buf; + for (j = common; j; j = j->next) + args[i++] = xstrdup(sha1_to_hex(j->item->object.sha1)); + args[i++] = "--"; + args[i++] = head_arg; + for (j = remoteheads; j; j = j->next) + args[i++] = xstrdup(sha1_to_hex(j->item->object.sha1)); + args[i] = NULL; + ret = run_command_v_opt(args, RUN_GIT_CMD); + strbuf_release(&buf); + i = 1; + for (j = common; j; j = j->next) + free((void *)args[i++]); + i += 2; + for (j = remoteheads; j; j = j->next) + free((void *)args[i++]); + free(args); + return -ret; +} + +static void count_diff_files(struct diff_queue_struct *q, + struct diff_options *opt, void *data) +{ + int *count = data; + + (*count) += q->nr; +} + +static int count_unmerged_entries(void) +{ + const struct index_state *state = &the_index; + int i, ret = 0; + + for (i = 0; i < state->cache_nr; i++) + if (ce_stage(state->cache[i])) + ret++; + + return ret; +} + +static int checkout_fast_forward(unsigned char *head, unsigned char *remote) +{ + struct tree *trees[MAX_UNPACK_TREES]; + struct unpack_trees_options opts; + struct tree_desc t[MAX_UNPACK_TREES]; + int i, fd, nr_trees = 0; + struct dir_struct dir; + struct lock_file *lock_file = xcalloc(1, sizeof(struct lock_file)); + + if (read_cache_unmerged()) + die("you need to resolve your current index first"); + + fd = hold_locked_index(lock_file, 1); + + memset(&trees, 0, sizeof(trees)); + memset(&opts, 0, sizeof(opts)); + memset(&t, 0, sizeof(t)); + dir.show_ignored = 1; + dir.exclude_per_dir = ".gitignore"; + opts.dir = &dir; + + opts.head_idx = 1; + opts.src_index = &the_index; + opts.dst_index = &the_index; + opts.update = 1; + opts.verbose_update = 1; + opts.merge = 1; + opts.fn = twoway_merge; + + trees[nr_trees] = parse_tree_indirect(head); + if (!trees[nr_trees++]) + return -1; + trees[nr_trees] = parse_tree_indirect(remote); + if (!trees[nr_trees++]) + return -1; + for (i = 0; i < nr_trees; i++) { + parse_tree(trees[i]); + init_tree_desc(t+i, trees[i]->buffer, trees[i]->size); + } + if (unpack_trees(nr_trees, t, &opts)) + return -1; + if (write_cache(fd, active_cache, active_nr) || + commit_locked_index(lock_file)) + die("unable to write new index file"); + return 0; +} + +static void split_merge_strategies(const char *string, struct strategy **list, + int *nr, int *alloc) +{ + char *p, *q, *buf; + + if (!string) + return; + + buf = xstrdup(string); + q = buf; + for (;;) { + p = strchr(q, ' '); + if (!p) { + ALLOC_GROW(*list, *nr + 1, *alloc); + (*list)[(*nr)++].name = xstrdup(q); + free(buf); + return; + } else { + *p = '\0'; + ALLOC_GROW(*list, *nr + 1, *alloc); + (*list)[(*nr)++].name = xstrdup(q); + q = ++p; + } + } +} + +static void add_strategies(const char *string, unsigned attr) +{ + struct strategy *list = NULL; + int list_alloc = 0, list_nr = 0, i; + + memset(&list, 0, sizeof(list)); + split_merge_strategies(string, &list, &list_nr, &list_alloc); + if (list != NULL) { + for (i = 0; i < list_nr; i++) + enable_strategy(list[i].name); + return; + } + for (i = 0; i < ARRAY_SIZE(all_strategy); i++) + if (all_strategy[i].attr & attr) + enable_strategy(all_strategy[i].name); +} + +static int merge_trivial(void) +{ + unsigned char result_tree[20], result_commit[20]; + struct commit_list parent; + + write_tree_trivial(result_tree); + printf("Wonderful.\n"); + parent.item = remoteheads->item; + parent.next = NULL; + commit_tree(merge_msg.buf, result_tree, &parent, result_commit); + finish(result_commit, "In-index merge"); + drop_save(); + return 0; +} + +static int finish_automerge(struct commit_list *common, + unsigned char *result_tree, + const char *wt_strategy) +{ + struct commit_list *parents = NULL, *j; + struct strbuf buf = STRBUF_INIT; + unsigned char result_commit[20]; + + free_commit_list(common); + if (allow_fast_forward) { + parents = remoteheads; + commit_list_insert(lookup_commit(head), &parents); + parents = reduce_heads(parents); + } else { + struct commit_list **pptr = &parents; + + pptr = &commit_list_insert(lookup_commit(head), + pptr)->next; + for (j = remoteheads; j; j = j->next) + pptr = &commit_list_insert(j->item, pptr)->next; + } + free_commit_list(remoteheads); + strbuf_addch(&merge_msg, '\n'); + commit_tree(merge_msg.buf, result_tree, parents, result_commit); + strbuf_addf(&buf, "Merge made by %s.", wt_strategy); + finish(result_commit, buf.buf); + strbuf_release(&buf); + drop_save(); + return 0; +} + +static int suggest_conflicts(void) +{ + FILE *fp; + int pos; + + fp = fopen(git_path("MERGE_MSG"), "a"); + if (!fp) + die("Could open %s for writing", git_path("MERGE_MSG")); + fprintf(fp, "\nConflicts:\n"); + for (pos = 0; pos < active_nr; pos++) { + struct cache_entry *ce = active_cache[pos]; + + if (ce_stage(ce)) { + fprintf(fp, "\t%s\n", ce->name); + while (pos + 1 < active_nr && + !strcmp(ce->name, + active_cache[pos + 1]->name)) + pos++; + } + } + fclose(fp); + rerere(); + printf("Automatic merge failed; " + "fix conflicts and then commit the result.\n"); + return 1; +} + +static struct commit *is_old_style_invocation(int argc, const char **argv) +{ + struct commit *second_token = NULL; + if (argc > 1) { + unsigned char second_sha1[20]; + + if (get_sha1(argv[1], second_sha1)) + return NULL; + second_token = lookup_commit_reference_gently(second_sha1, 0); + if (!second_token) + die("'%s' is not a commit", argv[1]); + if (hashcmp(second_token->object.sha1, head)) + return NULL; + } + return second_token; +} + +static int evaluate_result(void) +{ + int cnt = 0; + struct rev_info rev; + + if (read_cache() < 0) + die("failed to read the cache"); + + /* Check how many files differ. */ + init_revisions(&rev, ""); + setup_revisions(0, NULL, &rev, NULL); + rev.diffopt.output_format |= + DIFF_FORMAT_CALLBACK; + rev.diffopt.format_callback = count_diff_files; + rev.diffopt.format_callback_data = &cnt; + run_diff_files(&rev, 0); + + /* + * Check how many unmerged entries are + * there. + */ + cnt += count_unmerged_entries(); + + return cnt; +} + +int cmd_merge(int argc, const char **argv, const char *prefix) +{ + unsigned char result_tree[20]; + struct strbuf buf; + const char *head_arg; + int flag, head_invalid = 0, i, first_strategy = 1; + int best_cnt = -1, merge_was_ok = 0, automerge_was_ok = 0; + struct commit_list *common = NULL; + const char *best_strategy = NULL, *wt_strategy = NULL; + struct commit_list **remotes = &remoteheads; + + setup_work_tree(); + if (unmerged_cache()) + die("You are in the middle of a conflicted merge."); + + /* + * Check if we are _not_ on a detached HEAD, i.e. if there is a + * current branch. + */ + branch = resolve_ref("HEAD", head, 0, &flag); + if (branch && !prefixcmp(branch, "refs/heads/")) + branch += 11; + if (is_null_sha1(head)) + head_invalid = 1; + + git_config(git_merge_config, NULL); + + /* for color.ui */ + if (diff_use_color_default == -1) + diff_use_color_default = git_use_color_default; + + argc = parse_options(argc, argv, builtin_merge_options, + builtin_merge_usage, 0); + + if (squash) { + if (!allow_fast_forward) + die("You cannot combine --squash with --no-ff."); + option_commit = 0; + } + + if (!argc) + usage_with_options(builtin_merge_usage, + builtin_merge_options); + + /* + * This could be traditional "merge <msg> HEAD <commit>..." and + * the way we can tell it is to see if the second token is HEAD, + * but some people might have misused the interface and used a + * committish that is the same as HEAD there instead. + * Traditional format never would have "-m" so it is an + * additional safety measure to check for it. + */ + strbuf_init(&buf, 0); + + if (!have_message && is_old_style_invocation(argc, argv)) { + strbuf_addstr(&merge_msg, argv[0]); + head_arg = argv[1]; + argv += 2; + argc -= 2; + } else if (head_invalid) { + struct object *remote_head; + /* + * If the merged head is a valid one there is no reason + * to forbid "git merge" into a branch yet to be born. + * We do the same for "git pull". + */ + if (argc != 1) + die("Can merge only exactly one commit into " + "empty head"); + remote_head = peel_to_type(argv[0], 0, NULL, OBJ_COMMIT); + if (!remote_head) + die("%s - not something we can merge", argv[0]); + update_ref("initial pull", "HEAD", remote_head->sha1, NULL, 0, + DIE_ON_ERR); + reset_hard(remote_head->sha1, 0); + return 0; + } else { + struct strbuf msg; + + /* We are invoked directly as the first-class UI. */ + head_arg = "HEAD"; + + /* + * All the rest are the commits being merged; + * prepare the standard merge summary message to + * be appended to the given message. If remote + * is invalid we will die later in the common + * codepath so we discard the error in this + * loop. + */ + strbuf_init(&msg, 0); + for (i = 0; i < argc; i++) + merge_name(argv[i], &msg); + fmt_merge_msg(option_log, &msg, &merge_msg); + if (merge_msg.len) + strbuf_setlen(&merge_msg, merge_msg.len-1); + } + + if (head_invalid || !argc) + usage_with_options(builtin_merge_usage, + builtin_merge_options); + + strbuf_addstr(&buf, "merge"); + for (i = 0; i < argc; i++) + strbuf_addf(&buf, " %s", argv[i]); + setenv("GIT_REFLOG_ACTION", buf.buf, 0); + strbuf_reset(&buf); + + for (i = 0; i < argc; i++) { + struct object *o; + + o = peel_to_type(argv[i], 0, NULL, OBJ_COMMIT); + if (!o) + die("%s - not something we can merge", argv[i]); + remotes = &commit_list_insert(lookup_commit(o->sha1), + remotes)->next; + + strbuf_addf(&buf, "GITHEAD_%s", sha1_to_hex(o->sha1)); + setenv(buf.buf, argv[i], 1); + strbuf_reset(&buf); + } + + if (!enabled_strategies) { + if (!remoteheads->next) + add_strategies(pull_twohead, DEFAULT_TWOHEAD); + else + add_strategies(pull_octopus, DEFAULT_OCTOPUS); + } + + for (i = 0; i < ARRAY_SIZE(all_strategy); i++) { + if (!all_strategy[i].enabled) + continue; + if (all_strategy[i].attr & NO_FAST_FORWARD) + allow_fast_forward = 0; + if (all_strategy[i].attr & NO_TRIVIAL) + allow_trivial = 0; + } + + if (!remoteheads->next) + common = get_merge_bases(lookup_commit(head), + remoteheads->item, 1); + else { + struct commit_list *list = remoteheads; + commit_list_insert(lookup_commit(head), &list); + common = get_octopus_merge_bases(list); + free(list); + } + + update_ref("updating ORIG_HEAD", "ORIG_HEAD", head, NULL, 0, + DIE_ON_ERR); + + if (!common) + ; /* No common ancestors found. We need a real merge. */ + else if (!remoteheads->next && !common->next && + common->item == remoteheads->item) { + /* + * If head can reach all the merge then we are up to date. + * but first the most common case of merging one remote. + */ + finish_up_to_date("Already up-to-date."); + return 0; + } else if (allow_fast_forward && !remoteheads->next && + !common->next && + !hashcmp(common->item->object.sha1, head)) { + /* Again the most common case of merging one remote. */ + struct strbuf msg; + struct object *o; + char hex[41]; + + strcpy(hex, find_unique_abbrev(head, DEFAULT_ABBREV)); + + printf("Updating %s..%s\n", + hex, + find_unique_abbrev(remoteheads->item->object.sha1, + DEFAULT_ABBREV)); + refresh_cache(REFRESH_QUIET); + strbuf_init(&msg, 0); + strbuf_addstr(&msg, "Fast forward"); + if (have_message) + strbuf_addstr(&msg, + " (no commit created; -m option ignored)"); + o = peel_to_type(sha1_to_hex(remoteheads->item->object.sha1), + 0, NULL, OBJ_COMMIT); + if (!o) + return 1; + + if (checkout_fast_forward(head, remoteheads->item->object.sha1)) + return 1; + + finish(o->sha1, msg.buf); + drop_save(); + return 0; + } else if (!remoteheads->next && common->next) + ; + /* + * We are not doing octopus and not fast forward. Need + * a real merge. + */ + else if (!remoteheads->next && !common->next && option_commit) { + /* + * We are not doing octopus, not fast forward, and have + * only one common. + */ + refresh_cache(REFRESH_QUIET); + if (allow_trivial) { + /* See if it is really trivial. */ + git_committer_info(IDENT_ERROR_ON_NO_NAME); + printf("Trying really trivial in-index merge...\n"); + if (!read_tree_trivial(common->item->object.sha1, + head, remoteheads->item->object.sha1)) + return merge_trivial(); + printf("Nope.\n"); + } + } else { + /* + * An octopus. If we can reach all the remote we are up + * to date. + */ + int up_to_date = 1; + struct commit_list *j; + + for (j = remoteheads; j; j = j->next) { + struct commit_list *common_one; + + /* + * Here we *have* to calculate the individual + * merge_bases again, otherwise "git merge HEAD^ + * HEAD^^" would be missed. + */ + common_one = get_merge_bases(lookup_commit(head), + j->item, 1); + if (hashcmp(common_one->item->object.sha1, + j->item->object.sha1)) { + up_to_date = 0; + break; + } + } + if (up_to_date) { + finish_up_to_date("Already up-to-date. Yeeah!"); + return 0; + } + } + + /* We are going to make a new commit. */ + git_committer_info(IDENT_ERROR_ON_NO_NAME); + + /* + * At this point, we need a real merge. No matter what strategy + * we use, it would operate on the index, possibly affecting the + * working tree, and when resolved cleanly, have the desired + * tree in the index -- this means that the index must be in + * sync with the head commit. The strategies are responsible + * to ensure this. + */ + if (enabled_strategies != 1) { + /* + * Stash away the local changes so that we can try more + * than one. + */ + save_state(); + } else { + memcpy(stash, null_sha1, 20); + } + + for (i = 0; i < ARRAY_SIZE(all_strategy); i++) { + int ret; + if (!all_strategy[i].enabled) + continue; + if (first_strategy) { + first_strategy = 0; + printf("Rewinding the tree to pristine...\n"); + restore_state(); + } + if (enabled_strategies != 1) + printf("Trying merge strategy %s...\n", + all_strategy[i].name); + /* + * Remember which strategy left the state in the working + * tree. + */ + wt_strategy = all_strategy[i].name; + + ret = try_merge_strategy(all_strategy[i].name, + common, head_arg); + if (!option_commit && !ret) { + merge_was_ok = 1; + /* + * This is necessary here just to avoid writing + * the tree, but later we will *not* exit with + * status code 1 because merge_was_ok is set. + */ + ret = 1; + } + + if (ret) { + /* + * The backend exits with 1 when conflicts are + * left to be resolved, with 2 when it does not + * handle the given merge at all. + */ + if (ret == 1) { + int cnt = evaluate_result(); + + if (best_cnt <= 0 || cnt <= best_cnt) { + best_strategy = all_strategy[i].name; + best_cnt = cnt; + } + } + if (merge_was_ok) + break; + else + continue; + } + + /* Automerge succeeded. */ + write_tree_trivial(result_tree); + automerge_was_ok = 1; + break; + } + + /* + * If we have a resulting tree, that means the strategy module + * auto resolved the merge cleanly. + */ + if (automerge_was_ok) + return finish_automerge(common, result_tree, wt_strategy); + + /* + * Pick the result from the best strategy and have the user fix + * it up. + */ + if (!best_strategy) { + restore_state(); + if (enabled_strategies > 1) + fprintf(stderr, + "No merge strategy handled the merge.\n"); + else + fprintf(stderr, "Merge with strategy %s failed.\n", + wt_strategy); + return 2; + } else if (best_strategy == wt_strategy) + ; /* We already have its result in the working tree. */ + else { + printf("Rewinding the tree to pristine...\n"); + restore_state(); + printf("Using the %s to prepare resolving by hand.\n", + best_strategy); + try_merge_strategy(best_strategy, common, head_arg); + } + + if (squash) + finish(NULL, NULL); + else { + int fd; + struct commit_list *j; + + for (j = remoteheads; j; j = j->next) + strbuf_addf(&buf, "%s\n", + sha1_to_hex(j->item->object.sha1)); + fd = open(git_path("MERGE_HEAD"), O_WRONLY | O_CREAT, 0666); + if (fd < 0) + die("Could open %s for writing", + git_path("MERGE_HEAD")); + if (write_in_full(fd, buf.buf, buf.len) != buf.len) + die("Could not write to %s", git_path("MERGE_HEAD")); + close(fd); + strbuf_addch(&merge_msg, '\n'); + fd = open(git_path("MERGE_MSG"), O_WRONLY | O_CREAT, 0666); + if (fd < 0) + die("Could open %s for writing", git_path("MERGE_MSG")); + if (write_in_full(fd, merge_msg.buf, merge_msg.len) != + merge_msg.len) + die("Could not write to %s", git_path("MERGE_MSG")); + close(fd); + } + + if (merge_was_ok) { + fprintf(stderr, "Automatic merge went well; " + "stopped before committing as requested\n"); + return 0; + } else + return suggest_conflicts(); +} diff --git a/builtin.h b/builtin.h index 05ee56f..0e605d4 100644 --- a/builtin.h +++ b/builtin.h @@ -64,6 +64,7 @@ extern int cmd_ls_tree(int argc, const char **argv, const char *prefix); extern int cmd_ls_remote(int argc, const char **argv, const char *prefix); extern int cmd_mailinfo(int argc, const char **argv, const char *prefix); extern int cmd_mailsplit(int argc, const char **argv, const char *prefix); +extern int cmd_merge(int argc, const char **argv, const char *prefix); extern int cmd_merge_base(int argc, const char **argv, const char *prefix); extern int cmd_merge_ours(int argc, const char **argv, const char *prefix); extern int cmd_merge_file(int argc, const char **argv, const char *prefix); diff --git a/git-merge.sh b/contrib/examples/git-merge.sh similarity index 100% rename from git-merge.sh rename to contrib/examples/git-merge.sh diff --git a/git.c b/git.c index 2fbe96b..770aadd 100644 --- a/git.c +++ b/git.c @@ -271,6 +271,7 @@ static void handle_internal_command(int argc, const char **argv) { "ls-remote", cmd_ls_remote }, { "mailinfo", cmd_mailinfo }, { "mailsplit", cmd_mailsplit }, + { "merge", cmd_merge, RUN_SETUP | NEED_WORK_TREE }, { "merge-base", cmd_merge_base, RUN_SETUP }, { "merge-file", cmd_merge_file }, { "merge-ours", cmd_merge_ours, RUN_SETUP }, diff --git a/t/t7602-merge-octopus-many.sh b/t/t7602-merge-octopus-many.sh index f3a4bb2..fcb8285 100755 --- a/t/t7602-merge-octopus-many.sh +++ b/t/t7602-merge-octopus-many.sh @@ -23,7 +23,7 @@ test_expect_success 'setup' ' done ' -test_expect_failure 'merge c1 with c2, c3, c4, ... c29' ' +test_expect_success 'merge c1 with c2, c3, c4, ... c29' ' git reset --hard c1 && i=2 && refs="" && -- 1.5.6.1.322.ge904b.dirty ^ permalink raw reply related [flat|nested] 82+ messages in thread
* Re: [PATCH] Build in merge 2008-07-07 23:42 ` [PATCH] " Miklos Vajna @ 2008-07-08 0:32 ` Junio C Hamano 2008-07-08 0:53 ` Junio C Hamano 2008-07-08 1:00 ` Miklos Vajna 0 siblings, 2 replies; 82+ messages in thread From: Junio C Hamano @ 2008-07-08 0:32 UTC (permalink / raw To: Miklos Vajna; +Cc: To: Junio C Hamano, git, Johannes Schindelin, Olivier Marin Miklos Vajna <vmiklos@frugalware.org> writes: > Mentored-by: Johannes Schindelin <Johannes.Schindelin@gmx.de> > Signed-off-by: Miklos Vajna <vmiklos@frugalware.org> > --- > > On Mon, Jul 07, 2008 at 11:15:09AM -0700, Junio C Hamano <gitster@pobox.com> wrote: >> I do not get you on this point. Which one is nicer? >> >> (1) Have two lists, perhaps all_* and user_*. The logic that finds a >> strategy searches in two lists. The logic that checks if a given >> strategy is built-in checks if it is on all_* list. >> >> (2) Have a single list, but add a boolean "unsigned is_builtin:1" to >> each >> element of it. The logic that finds a strategy looks in this >> single >> list. The logic that checks if a given strategy is built-in >> looks at >> the strategy instance and it has the bit already. >> >> You seem to be advocating (1) but I do not understand why... > > Ah, OK. For now, I just added an "unsigned enabled:1;". Later we can add > an "unsigned is_buildin:1;" as well, but currently we die with earlier > with a "Could not find merge strategy" error message, so is_builtin > would be always true. > > So here is a version, this time without the use_strategies list. That is not what I meant. I am afraid perhaps I misunderstood what you were talking about. When/if you allow user defined new strategies, then you have a choice: (1) find "git-merge-*" in path, add them to the single all_strategies[] list (but you will do the ALLOC_GROW() business so you would need to use the one you currently have as static form to prime the real list), and look for "foo" strategy when "-s foo" is given from that single list, or (2) find "git-merge-*" in path, add them to a separate user_strategies[] list, and look for "foo" strategy when "-s foo" is given from the user_strategies[] list and all_strategies[] list (all_strategies[] should perhaps be renamed to builtin_strategies[] if you go that route). The comparison I gave was between the above two. But the change you are talking about is completely different, isn't it? The part that records which strategies were specified from the command line *in what order* via "-s foo" switches should remain list of pointers into "struct strategy", which is called "struct strategy **use_strategies" in the code and corresponds to the $use_strategies variable in the scripted version. The order of these is important, as that defines in which order the strategies are tried [*1*]. If you go route (1), these pointers will all be pointing at elements in all_strategies[]; with route (2) they may be pointing at either all_strageties[] element or user_strategies[] element. If you are never going to say "available strategies are these" after you start supporting user-defined strategy, then you do not necessarily need to do the "find 'git-merge-*' in path, add them to ..." step above, in which case it would be Ok not to scan the path and add them to all_strategies[] (in route (1)) nor user_strategies[] (in route (2)). Instead, you would just create a new "struct strategy" instance lazily when the user gave "-s foo" and "foo" is not one of the built-in strategy. You would put that at the tail of "struct strategy **use_strategy" array, and iterate over use_strategy in the order they are given on the command line. [Footnote] *1* Personally, I find the importance of this dubious in practice, as I said earlier, I do not think it would work well to try different strategies and pick the best one --- evaluating which result is the *best* is difficult. If you want to stay compatible with the scripted version, however, you cannot just mark entries in all_strategies[] with boolean and iterate over them in the order that all_strageties[] define them. You need to try them in the order the user specified. ^ permalink raw reply [flat|nested] 82+ messages in thread
* Re: [PATCH] Build in merge 2008-07-08 0:32 ` Junio C Hamano @ 2008-07-08 0:53 ` Junio C Hamano 2008-07-08 1:18 ` Miklos Vajna 2008-07-08 1:00 ` Miklos Vajna 1 sibling, 1 reply; 82+ messages in thread From: Junio C Hamano @ 2008-07-08 0:53 UTC (permalink / raw To: Miklos Vajna; +Cc: To: Junio C Hamano, git, Johannes Schindelin, Olivier Marin Junio C Hamano <gitster@pobox.com> writes: > That is not what I meant. I am afraid perhaps I misunderstood what you > were talking about. > ... > The comparison I gave was between the above two. But the change you are > talking about is completely different, isn't it? > > The part that records which strategies were specified from the command > line *in what order* via "-s foo" switches should remain list of pointers > into "struct strategy", which is called "struct strategy **use_strategies" > in the code and corresponds to the $use_strategies variable in the > scripted version. Here is what I meant to suggest; sorry for confusing you with an initial typo of having only one pointer in front of use_strategies "array of pointers". Applies on top of your previous round. -- builtin-merge.c | 58 +++++++++++++++++++++++++++--------------------------- 1 files changed, 29 insertions(+), 29 deletions(-) diff --git a/builtin-merge.c b/builtin-merge.c index 7312997..b2e702a 100644 --- a/builtin-merge.c +++ b/builtin-merge.c @@ -44,7 +44,7 @@ static int allow_trivial = 1, have_message; static struct strbuf merge_msg; static struct commit_list *remoteheads; static unsigned char head[20], stash[20]; -static struct strategy *use_strategies; +static struct strategy **use_strategies; static size_t use_strategies_nr, use_strategies_alloc; static const char *branch; @@ -74,44 +74,44 @@ static int option_parse_message(const struct option *opt, return 0; } -static int strategy_lookup(const char *path) +static struct strategy *get_strategy(const char *name) { int i; - if (!path) - return -1; + if (!name) + return NULL; for (i = 0; i < ARRAY_SIZE(all_strategy); i++) - if (!strcmp(path, all_strategy[i].name)) - return all_strategy[i].attr; - return -1; + if (!strcmp(name, all_strategy[i].name)) + return &all_strategy[i]; + return NULL; } -static inline void append_strategy(const char *name, unsigned attr) +static void append_strategy(struct strategy *s) { ALLOC_GROW(use_strategies, use_strategies_nr + 1, use_strategies_alloc); - use_strategies[use_strategies_nr].name = name; - use_strategies[use_strategies_nr++].attr = attr; + use_strategies[use_strategies_nr++] = s; } static int option_parse_strategy(const struct option *opt, - const char *arg, int unset) + const char *name, int unset) { - int i, attr; + int i; + struct strategy *s; if (unset) return 0; - attr = strategy_lookup(arg); + s = get_strategy(name); - if (attr >= 0) - append_strategy(arg, attr); + if (s) + append_strategy(s); else { struct strbuf err; strbuf_init(&err, 0); for (i = 0; i < ARRAY_SIZE(all_strategy); i++) strbuf_addf(&err, " %s", all_strategy[i].name); - fprintf(stderr, "Could not find merge strategy '%s'.\n", arg); + fprintf(stderr, "Could not find merge strategy '%s'.\n", name); fprintf(stderr, "Available strategies are:%s.\n", err.buf); exit(1); } @@ -643,18 +643,18 @@ static void add_strategies(const char *string, unsigned attr) split_merge_strategies(string, &list, &list_nr, &list_alloc); if (list != NULL) { for (i = 0; i < list_nr; i++) { - int attr; + struct strategy *s; - attr = strategy_lookup(list[i].name); - if (attr >= 0) - append_strategy(list[i].name, attr); + s = get_strategy(list[i].name); + if (s) + append_strategy(s); } return; } for (i = 0; i < ARRAY_SIZE(all_strategy); i++) if (all_strategy[i].attr & attr) - append_strategy(all_strategy[i].name, - all_strategy[i].attr); + append_strategy(&all_strategy[i]); + } static int merge_trivial(void) @@ -903,9 +903,9 @@ int cmd_merge(int argc, const char **argv, const char *prefix) } for (i = 0; i < use_strategies_nr; i++) { - if (use_strategies[i].attr & NO_FAST_FORWARD) + if (use_strategies[i]->attr & NO_FAST_FORWARD) allow_fast_forward = 0; - if (use_strategies[i].attr & NO_TRIVIAL) + if (use_strategies[i]->attr & NO_TRIVIAL) allow_trivial = 0; } @@ -1043,14 +1043,14 @@ int cmd_merge(int argc, const char **argv, const char *prefix) } if (use_strategies_nr != 1) printf("Trying merge strategy %s...\n", - use_strategies[i].name); + use_strategies[i]->name); /* * Remember which strategy left the state in the working * tree. */ - wt_strategy = use_strategies[i].name; + wt_strategy = use_strategies[i]->name; - ret = try_merge_strategy(use_strategies[i].name, + ret = try_merge_strategy(use_strategies[i]->name, common, head_arg); if (!option_commit && !ret) { merge_was_ok = 1; @@ -1072,7 +1072,7 @@ int cmd_merge(int argc, const char **argv, const char *prefix) int cnt = evaluate_result(); if (best_cnt <= 0 || cnt <= best_cnt) { - best_strategy = use_strategies[i].name; + best_strategy = use_strategies[i]->name; best_cnt = cnt; } } @@ -1106,7 +1106,7 @@ int cmd_merge(int argc, const char **argv, const char *prefix) "No merge strategy handled the merge.\n"); else fprintf(stderr, "Merge with strategy %s failed.\n", - use_strategies[0].name); + use_strategies[0]->name); return 2; } else if (best_strategy == wt_strategy) ; /* We already have its result in the working tree. */ ^ permalink raw reply related [flat|nested] 82+ messages in thread
* [PATCH] Build in merge 2008-07-08 0:53 ` Junio C Hamano @ 2008-07-08 1:18 ` Miklos Vajna 0 siblings, 0 replies; 82+ messages in thread From: Miklos Vajna @ 2008-07-08 1:18 UTC (permalink / raw To: Junio C Hamano; +Cc: git, Johannes Schindelin, Olivier Marin Mentored-by: Johannes Schindelin <Johannes.Schindelin@gmx.de> Signed-off-by: Miklos Vajna <vmiklos@frugalware.org> --- On Mon, Jul 07, 2008 at 05:53:23PM -0700, Junio C Hamano <gitster@pobox.com> wrote: > Here is what I meant to suggest; sorry for confusing you with an > initial > typo of having only one pointer in front of use_strategies "array of > pointers". Applies on top of your previous round. Funny enough, I just did almost the same before reading your mail, except that I did not do the suggested strategy_lookup() -> get_strategy() rename and such. Here is a squashed patch. (Previous round + your patch.) Makefile | 2 +- builtin-merge.c | 1153 +++++++++++++++++++++++++ builtin.h | 1 + git-merge.sh => contrib/examples/git-merge.sh | 0 git.c | 1 + t/t7602-merge-octopus-many.sh | 2 +- 6 files changed, 1157 insertions(+), 2 deletions(-) create mode 100644 builtin-merge.c rename git-merge.sh => contrib/examples/git-merge.sh (100%) diff --git a/Makefile b/Makefile index bf77292..fbc53e9 100644 --- a/Makefile +++ b/Makefile @@ -240,7 +240,6 @@ SCRIPT_SH += git-lost-found.sh SCRIPT_SH += git-merge-octopus.sh SCRIPT_SH += git-merge-one-file.sh SCRIPT_SH += git-merge-resolve.sh -SCRIPT_SH += git-merge.sh SCRIPT_SH += git-merge-stupid.sh SCRIPT_SH += git-mergetool.sh SCRIPT_SH += git-parse-remote.sh @@ -515,6 +514,7 @@ BUILTIN_OBJS += builtin-ls-remote.o BUILTIN_OBJS += builtin-ls-tree.o BUILTIN_OBJS += builtin-mailinfo.o BUILTIN_OBJS += builtin-mailsplit.o +BUILTIN_OBJS += builtin-merge.o BUILTIN_OBJS += builtin-merge-base.o BUILTIN_OBJS += builtin-merge-file.o BUILTIN_OBJS += builtin-merge-ours.o diff --git a/builtin-merge.c b/builtin-merge.c new file mode 100644 index 0000000..d6bd144 --- /dev/null +++ b/builtin-merge.c @@ -0,0 +1,1153 @@ +/* + * Builtin "git merge" + * + * Copyright (c) 2008 Miklos Vajna <vmiklos@frugalware.org> + * + * Based on git-merge.sh by Junio C Hamano. + */ + +#include "cache.h" +#include "parse-options.h" +#include "builtin.h" +#include "run-command.h" +#include "diff.h" +#include "refs.h" +#include "commit.h" +#include "diffcore.h" +#include "revision.h" +#include "unpack-trees.h" +#include "cache-tree.h" +#include "dir.h" +#include "utf8.h" +#include "log-tree.h" +#include "color.h" + +#define DEFAULT_TWOHEAD (1<<0) +#define DEFAULT_OCTOPUS (1<<1) +#define NO_FAST_FORWARD (1<<2) +#define NO_TRIVIAL (1<<3) + +struct strategy { + const char *name; + unsigned attr; +}; + +static const char * const builtin_merge_usage[] = { + "git-merge [options] <remote>...", + "git-merge [options] <msg> HEAD <remote>", + NULL +}; + +static int show_diffstat = 1, option_log, squash; +static int option_commit = 1, allow_fast_forward = 1; +static int allow_trivial = 1, have_message; +static struct strbuf merge_msg; +static struct commit_list *remoteheads; +static unsigned char head[20], stash[20]; +static struct strategy **use_strategies; +static size_t use_strategies_nr, use_strategies_alloc; +static const char *branch; + +static struct strategy all_strategy[] = { + { "recur", NO_TRIVIAL }, + { "recursive", DEFAULT_TWOHEAD | NO_TRIVIAL }, + { "octopus", DEFAULT_OCTOPUS }, + { "resolve", 0 }, + { "stupid", 0 }, + { "ours", NO_FAST_FORWARD | NO_TRIVIAL }, + { "subtree", NO_FAST_FORWARD | NO_TRIVIAL }, +}; + +static const char *pull_twohead, *pull_octopus; + +static int option_parse_message(const struct option *opt, + const char *arg, int unset) +{ + struct strbuf *buf = opt->value; + + if (unset) + strbuf_setlen(buf, 0); + else { + strbuf_addf(buf, "%s\n\n", arg); + have_message = 1; + } + return 0; +} + +static struct strategy *get_strategy(const char *name) +{ + int i; + + if (!name) + return NULL; + + for (i = 0; i < ARRAY_SIZE(all_strategy); i++) + if (!strcmp(name, all_strategy[i].name)) + return &all_strategy[i]; + return NULL; +} + +static void append_strategy(struct strategy *s) +{ + ALLOC_GROW(use_strategies, use_strategies_nr + 1, use_strategies_alloc); + use_strategies[use_strategies_nr++] = s; +} + +static int option_parse_strategy(const struct option *opt, + const char *name, int unset) +{ + int i; + struct strategy *s; + + if (unset) + return 0; + + s = get_strategy(name); + + if (s) + append_strategy(s); + else { + struct strbuf err; + strbuf_init(&err, 0); + for (i = 0; i < ARRAY_SIZE(all_strategy); i++) + strbuf_addf(&err, " %s", all_strategy[i].name); + fprintf(stderr, "Could not find merge strategy '%s'.\n", name); + fprintf(stderr, "Available strategies are:%s.\n", err.buf); + exit(1); + } + return 0; +} + +static int option_parse_n(const struct option *opt, + const char *arg, int unset) +{ + show_diffstat = unset; + return 0; +} + +static struct option builtin_merge_options[] = { + { OPTION_CALLBACK, 'n', NULL, NULL, NULL, + "do not show a diffstat at the end of the merge", + PARSE_OPT_NOARG, option_parse_n }, + OPT_BOOLEAN(0, "stat", &show_diffstat, + "show a diffstat at the end of the merge"), + OPT_BOOLEAN(0, "summary", &show_diffstat, "(synonym to --stat)"), + OPT_BOOLEAN(0, "log", &option_log, + "add list of one-line log to merge commit message"), + OPT_BOOLEAN(0, "squash", &squash, + "create a single commit instead of doing a merge"), + OPT_BOOLEAN(0, "commit", &option_commit, + "perform a commit if the merge succeeds (default)"), + OPT_BOOLEAN(0, "ff", &allow_fast_forward, + "allow fast forward (default)"), + OPT_CALLBACK('s', "strategy", &use_strategies, "strategy", + "merge strategy to use", option_parse_strategy), + OPT_CALLBACK('m', "message", &merge_msg, "message", + "message to be used for the merge commit (if any)", + option_parse_message), + OPT_END() +}; + +/* Cleans up metadata that is uninteresting after a succeeded merge. */ +static void drop_save(void) +{ + unlink(git_path("MERGE_HEAD")); + unlink(git_path("MERGE_MSG")); +} + +static void save_state(void) +{ + int len; + struct child_process cp; + struct strbuf buffer = STRBUF_INIT; + const char *argv[] = {"stash", "create", NULL}; + + memset(&cp, 0, sizeof(cp)); + cp.argv = argv; + cp.out = -1; + cp.git_cmd = 1; + + if (start_command(&cp)) + die("could not run stash."); + len = strbuf_read(&buffer, cp.out, 1024); + close(cp.out); + + if (finish_command(&cp) || len < 0) + die("stash failed"); + else if (!len) + return; + strbuf_setlen(&buffer, buffer.len-1); + if (get_sha1(buffer.buf, stash)) + die("not a valid object: %s", buffer.buf); +} + +static void reset_hard(unsigned const char *sha1, int verbose) +{ + int i = 0; + const char *args[6]; + + args[i++] = "read-tree"; + if (verbose) + args[i++] = "-v"; + args[i++] = "--reset"; + args[i++] = "-u"; + args[i++] = sha1_to_hex(sha1); + args[i] = NULL; + + if (run_command_v_opt(args, RUN_GIT_CMD)) + die("read-tree failed"); +} + +static void restore_state(void) +{ + struct strbuf sb; + const char *args[] = { "stash", "apply", NULL, NULL }; + + if (is_null_sha1(stash)) + return; + + reset_hard(head, 1); + + strbuf_init(&sb, 0); + args[2] = sha1_to_hex(stash); + + /* + * It is OK to ignore error here, for example when there was + * nothing to restore. + */ + run_command_v_opt(args, RUN_GIT_CMD); + + strbuf_release(&sb); + refresh_cache(REFRESH_QUIET); +} + +/* This is called when no merge was necessary. */ +static void finish_up_to_date(const char *msg) +{ + printf("%s%s\n", squash ? " (nothing to squash)" : "", msg); + drop_save(); +} + +static void squash_message(void) +{ + struct rev_info rev; + struct commit *commit; + struct strbuf out; + struct commit_list *j; + int fd; + + printf("Squash commit -- not updating HEAD\n"); + fd = open(git_path("SQUASH_MSG"), O_WRONLY | O_CREAT, 0666); + if (fd < 0) + die("Could not write to %s", git_path("SQUASH_MSG")); + + init_revisions(&rev, NULL); + rev.ignore_merges = 1; + rev.commit_format = CMIT_FMT_MEDIUM; + + commit = lookup_commit(head); + commit->object.flags |= UNINTERESTING; + add_pending_object(&rev, &commit->object, NULL); + + for (j = remoteheads; j; j = j->next) + add_pending_object(&rev, &j->item->object, NULL); + + setup_revisions(0, NULL, &rev, NULL); + if (prepare_revision_walk(&rev)) + die("revision walk setup failed"); + + strbuf_init(&out, 0); + strbuf_addstr(&out, "Squashed commit of the following:\n"); + while ((commit = get_revision(&rev)) != NULL) { + strbuf_addch(&out, '\n'); + strbuf_addf(&out, "commit %s\n", + sha1_to_hex(commit->object.sha1)); + pretty_print_commit(rev.commit_format, commit, &out, rev.abbrev, + NULL, NULL, rev.date_mode, 0); + } + write(fd, out.buf, out.len); + close(fd); + strbuf_release(&out); +} + +static int run_hook(const char *name) +{ + struct child_process hook; + const char *argv[3], *env[2]; + char index[PATH_MAX]; + + argv[0] = git_path("hooks/%s", name); + if (access(argv[0], X_OK) < 0) + return 0; + + snprintf(index, sizeof(index), "GIT_INDEX_FILE=%s", get_index_file()); + env[0] = index; + env[1] = NULL; + + if (squash) + argv[1] = "1"; + else + argv[1] = "0"; + argv[2] = NULL; + + memset(&hook, 0, sizeof(hook)); + hook.argv = argv; + hook.no_stdin = 1; + hook.stdout_to_stderr = 1; + hook.env = env; + + return run_command(&hook); +} + +static void finish(const unsigned char *new_head, const char *msg) +{ + struct strbuf reflog_message; + + strbuf_init(&reflog_message, 0); + if (!msg) + strbuf_addstr(&reflog_message, getenv("GIT_REFLOG_ACTION")); + else { + printf("%s\n", msg); + strbuf_addf(&reflog_message, "%s: %s", + getenv("GIT_REFLOG_ACTION"), msg); + } + if (squash) { + squash_message(); + } else { + if (!merge_msg.len) + printf("No merge message -- not updating HEAD\n"); + else { + const char *argv_gc_auto[] = { "gc", "--auto", NULL }; + update_ref(reflog_message.buf, "HEAD", + new_head, head, 0, + DIE_ON_ERR); + /* + * We ignore errors in 'gc --auto', since the + * user should see them. + */ + run_command_v_opt(argv_gc_auto, RUN_GIT_CMD); + } + } + if (new_head && show_diffstat) { + struct diff_options opts; + diff_setup(&opts); + opts.output_format |= + DIFF_FORMAT_SUMMARY | DIFF_FORMAT_DIFFSTAT; + opts.detect_rename = DIFF_DETECT_RENAME; + if (diff_use_color_default > 0) + DIFF_OPT_SET(&opts, COLOR_DIFF); + if (diff_setup_done(&opts) < 0) + die("diff_setup_done failed"); + diff_tree_sha1(head, new_head, "", &opts); + diffcore_std(&opts); + diff_flush(&opts); + } + + /* Run a post-merge hook */ + run_hook("post-merge"); + + strbuf_release(&reflog_message); +} + +/* Get the name for the merge commit's message. */ +static void merge_name(const char *remote, struct strbuf *msg) +{ + struct object *remote_head; + unsigned char branch_head[20], buf_sha[20]; + struct strbuf buf; + const char *ptr; + int len, early; + + memset(branch_head, 0, sizeof(branch_head)); + remote_head = peel_to_type(remote, 0, NULL, OBJ_COMMIT); + if (!remote_head) + die("'%s' does not point to a commit", remote); + + strbuf_init(&buf, 0); + strbuf_addstr(&buf, "refs/heads/"); + strbuf_addstr(&buf, remote); + resolve_ref(buf.buf, branch_head, 0, 0); + + if (!hashcmp(remote_head->sha1, branch_head)) { + strbuf_addf(msg, "%s\t\tbranch '%s' of .\n", + sha1_to_hex(branch_head), remote); + return; + } + + /* See if remote matches <name>^^^.. or <name>~<number> */ + for (len = 0, ptr = remote + strlen(remote); + remote < ptr && ptr[-1] == '^'; + ptr--) + len++; + if (len) + early = 1; + else { + early = 0; + ptr = strrchr(remote, '~'); + if (ptr) { + int seen_nonzero = 0; + + len++; /* count ~ */ + while (*++ptr && isdigit(*ptr)) { + seen_nonzero |= (*ptr != '0'); + len++; + } + if (*ptr) + len = 0; /* not ...~<number> */ + else if (seen_nonzero) + early = 1; + else if (len == 1) + early = 1; /* "name~" is "name~1"! */ + } + } + if (len) { + struct strbuf truname = STRBUF_INIT; + strbuf_addstr(&truname, "refs/heads/"); + strbuf_addstr(&truname, remote); + strbuf_setlen(&truname, len+11); + if (resolve_ref(truname.buf, buf_sha, 0, 0)) { + strbuf_addf(msg, + "%s\t\tbranch '%s'%s of .\n", + sha1_to_hex(remote_head->sha1), + truname.buf, + (early ? " (early part)" : "")); + return; + } + } + + if (!strcmp(remote, "FETCH_HEAD") && + !access(git_path("FETCH_HEAD"), R_OK)) { + FILE *fp; + struct strbuf line; + char *ptr; + + strbuf_init(&line, 0); + fp = fopen(git_path("FETCH_HEAD"), "r"); + if (!fp) + die("could not open %s for reading: %s", + git_path("FETCH_HEAD"), strerror(errno)); + strbuf_getline(&line, fp, '\n'); + fclose(fp); + ptr = strstr(line.buf, "\tnot-for-merge\t"); + if (ptr) + strbuf_remove(&line, ptr-line.buf+1, 13); + strbuf_addbuf(msg, &line); + strbuf_release(&line); + return; + } + strbuf_addf(msg, "%s\t\tcommit '%s'\n", + sha1_to_hex(remote_head->sha1), remote); +} + +int git_merge_config(const char *k, const char *v, void *cb) +{ + if (branch && !prefixcmp(k, "branch.") && + !prefixcmp(k + 7, branch) && + !strcmp(k + 7 + strlen(branch), ".mergeoptions")) { + const char **argv; + int argc; + char *buf; + + buf = xstrdup(v); + argc = split_cmdline(buf, &argv); + argv = xrealloc(argv, sizeof(*argv) * (argc + 2)); + memmove(argv + 1, argv, sizeof(*argv) * (argc + 1)); + argc++; + parse_options(argc, argv, builtin_merge_options, + builtin_merge_usage, 0); + free(buf); + } + + if (!strcmp(k, "merge.diffstat") || !strcmp(k, "merge.stat")) + show_diffstat = git_config_bool(k, v); + else if (!strcmp(k, "pull.twohead")) + return git_config_string(&pull_twohead, k, v); + else if (!strcmp(k, "pull.octopus")) + return git_config_string(&pull_octopus, k, v); + return git_diff_ui_config(k, v, cb); +} + +static int read_tree_trivial(unsigned char *common, unsigned char *head, + unsigned char *one) +{ + int i, nr_trees = 0; + struct tree *trees[MAX_UNPACK_TREES]; + struct tree_desc t[MAX_UNPACK_TREES]; + struct unpack_trees_options opts; + + memset(&opts, 0, sizeof(opts)); + opts.head_idx = 2; + opts.src_index = &the_index; + opts.dst_index = &the_index; + opts.update = 1; + opts.verbose_update = 1; + opts.trivial_merges_only = 1; + opts.merge = 1; + trees[nr_trees] = parse_tree_indirect(common); + if (!trees[nr_trees++]) + return -1; + trees[nr_trees] = parse_tree_indirect(head); + if (!trees[nr_trees++]) + return -1; + trees[nr_trees] = parse_tree_indirect(one); + if (!trees[nr_trees++]) + return -1; + opts.fn = threeway_merge; + cache_tree_free(&active_cache_tree); + for (i = 0; i < nr_trees; i++) { + parse_tree(trees[i]); + init_tree_desc(t+i, trees[i]->buffer, trees[i]->size); + } + if (unpack_trees(nr_trees, t, &opts)) + return -1; + return 0; +} + +static void write_tree_trivial(unsigned char *sha1) +{ + if (write_cache_as_tree(sha1, 0, NULL)) + die("git write-tree failed to write a tree"); +} + +static int try_merge_strategy(const char *strategy, struct commit_list *common, + const char *head_arg) +{ + const char **args; + int i = 0, ret; + struct commit_list *j; + struct strbuf buf; + + args = xmalloc((4 + commit_list_count(common) + + commit_list_count(remoteheads)) * sizeof(char *)); + strbuf_init(&buf, 0); + strbuf_addf(&buf, "merge-%s", strategy); + args[i++] = buf.buf; + for (j = common; j; j = j->next) + args[i++] = xstrdup(sha1_to_hex(j->item->object.sha1)); + args[i++] = "--"; + args[i++] = head_arg; + for (j = remoteheads; j; j = j->next) + args[i++] = xstrdup(sha1_to_hex(j->item->object.sha1)); + args[i] = NULL; + ret = run_command_v_opt(args, RUN_GIT_CMD); + strbuf_release(&buf); + i = 1; + for (j = common; j; j = j->next) + free((void *)args[i++]); + i += 2; + for (j = remoteheads; j; j = j->next) + free((void *)args[i++]); + free(args); + return -ret; +} + +static void count_diff_files(struct diff_queue_struct *q, + struct diff_options *opt, void *data) +{ + int *count = data; + + (*count) += q->nr; +} + +static int count_unmerged_entries(void) +{ + const struct index_state *state = &the_index; + int i, ret = 0; + + for (i = 0; i < state->cache_nr; i++) + if (ce_stage(state->cache[i])) + ret++; + + return ret; +} + +static int checkout_fast_forward(unsigned char *head, unsigned char *remote) +{ + struct tree *trees[MAX_UNPACK_TREES]; + struct unpack_trees_options opts; + struct tree_desc t[MAX_UNPACK_TREES]; + int i, fd, nr_trees = 0; + struct dir_struct dir; + struct lock_file *lock_file = xcalloc(1, sizeof(struct lock_file)); + + if (read_cache_unmerged()) + die("you need to resolve your current index first"); + + fd = hold_locked_index(lock_file, 1); + + memset(&trees, 0, sizeof(trees)); + memset(&opts, 0, sizeof(opts)); + memset(&t, 0, sizeof(t)); + dir.show_ignored = 1; + dir.exclude_per_dir = ".gitignore"; + opts.dir = &dir; + + opts.head_idx = 1; + opts.src_index = &the_index; + opts.dst_index = &the_index; + opts.update = 1; + opts.verbose_update = 1; + opts.merge = 1; + opts.fn = twoway_merge; + + trees[nr_trees] = parse_tree_indirect(head); + if (!trees[nr_trees++]) + return -1; + trees[nr_trees] = parse_tree_indirect(remote); + if (!trees[nr_trees++]) + return -1; + for (i = 0; i < nr_trees; i++) { + parse_tree(trees[i]); + init_tree_desc(t+i, trees[i]->buffer, trees[i]->size); + } + if (unpack_trees(nr_trees, t, &opts)) + return -1; + if (write_cache(fd, active_cache, active_nr) || + commit_locked_index(lock_file)) + die("unable to write new index file"); + return 0; +} + +static void split_merge_strategies(const char *string, struct strategy **list, + int *nr, int *alloc) +{ + char *p, *q, *buf; + + if (!string) + return; + + buf = xstrdup(string); + q = buf; + for (;;) { + p = strchr(q, ' '); + if (!p) { + ALLOC_GROW(*list, *nr + 1, *alloc); + (*list)[(*nr)++].name = xstrdup(q); + free(buf); + return; + } else { + *p = '\0'; + ALLOC_GROW(*list, *nr + 1, *alloc); + (*list)[(*nr)++].name = xstrdup(q); + q = ++p; + } + } +} + +static void add_strategies(const char *string, unsigned attr) +{ + struct strategy *list = NULL; + int list_alloc = 0, list_nr = 0, i; + + memset(&list, 0, sizeof(list)); + split_merge_strategies(string, &list, &list_nr, &list_alloc); + if (list != NULL) { + for (i = 0; i < list_nr; i++) { + struct strategy *s; + + s = get_strategy(list[i].name); + if (s) + append_strategy(s); + } + return; + } + for (i = 0; i < ARRAY_SIZE(all_strategy); i++) + if (all_strategy[i].attr & attr) + append_strategy(&all_strategy[i]); + +} + +static int merge_trivial(void) +{ + unsigned char result_tree[20], result_commit[20]; + struct commit_list parent; + + write_tree_trivial(result_tree); + printf("Wonderful.\n"); + parent.item = remoteheads->item; + parent.next = NULL; + commit_tree(merge_msg.buf, result_tree, &parent, result_commit); + finish(result_commit, "In-index merge"); + drop_save(); + return 0; +} + +static int finish_automerge(struct commit_list *common, + unsigned char *result_tree, + const char *wt_strategy) +{ + struct commit_list *parents = NULL, *j; + struct strbuf buf = STRBUF_INIT; + unsigned char result_commit[20]; + + free_commit_list(common); + if (allow_fast_forward) { + parents = remoteheads; + commit_list_insert(lookup_commit(head), &parents); + parents = reduce_heads(parents); + } else { + struct commit_list **pptr = &parents; + + pptr = &commit_list_insert(lookup_commit(head), + pptr)->next; + for (j = remoteheads; j; j = j->next) + pptr = &commit_list_insert(j->item, pptr)->next; + } + free_commit_list(remoteheads); + strbuf_addch(&merge_msg, '\n'); + commit_tree(merge_msg.buf, result_tree, parents, result_commit); + strbuf_addf(&buf, "Merge made by %s.", wt_strategy); + finish(result_commit, buf.buf); + strbuf_release(&buf); + drop_save(); + return 0; +} + +static int suggest_conflicts(void) +{ + FILE *fp; + int pos; + + fp = fopen(git_path("MERGE_MSG"), "a"); + if (!fp) + die("Could open %s for writing", git_path("MERGE_MSG")); + fprintf(fp, "\nConflicts:\n"); + for (pos = 0; pos < active_nr; pos++) { + struct cache_entry *ce = active_cache[pos]; + + if (ce_stage(ce)) { + fprintf(fp, "\t%s\n", ce->name); + while (pos + 1 < active_nr && + !strcmp(ce->name, + active_cache[pos + 1]->name)) + pos++; + } + } + fclose(fp); + rerere(); + printf("Automatic merge failed; " + "fix conflicts and then commit the result.\n"); + return 1; +} + +static struct commit *is_old_style_invocation(int argc, const char **argv) +{ + struct commit *second_token = NULL; + if (argc > 1) { + unsigned char second_sha1[20]; + + if (get_sha1(argv[1], second_sha1)) + return NULL; + second_token = lookup_commit_reference_gently(second_sha1, 0); + if (!second_token) + die("'%s' is not a commit", argv[1]); + if (hashcmp(second_token->object.sha1, head)) + return NULL; + } + return second_token; +} + +static int evaluate_result(void) +{ + int cnt = 0; + struct rev_info rev; + + if (read_cache() < 0) + die("failed to read the cache"); + + /* Check how many files differ. */ + init_revisions(&rev, ""); + setup_revisions(0, NULL, &rev, NULL); + rev.diffopt.output_format |= + DIFF_FORMAT_CALLBACK; + rev.diffopt.format_callback = count_diff_files; + rev.diffopt.format_callback_data = &cnt; + run_diff_files(&rev, 0); + + /* + * Check how many unmerged entries are + * there. + */ + cnt += count_unmerged_entries(); + + return cnt; +} + +int cmd_merge(int argc, const char **argv, const char *prefix) +{ + unsigned char result_tree[20]; + struct strbuf buf; + const char *head_arg; + int flag, head_invalid = 0, i; + int best_cnt = -1, merge_was_ok = 0, automerge_was_ok = 0; + struct commit_list *common = NULL; + const char *best_strategy = NULL, *wt_strategy = NULL; + struct commit_list **remotes = &remoteheads; + + setup_work_tree(); + if (unmerged_cache()) + die("You are in the middle of a conflicted merge."); + + /* + * Check if we are _not_ on a detached HEAD, i.e. if there is a + * current branch. + */ + branch = resolve_ref("HEAD", head, 0, &flag); + if (branch && !prefixcmp(branch, "refs/heads/")) + branch += 11; + if (is_null_sha1(head)) + head_invalid = 1; + + git_config(git_merge_config, NULL); + + /* for color.ui */ + if (diff_use_color_default == -1) + diff_use_color_default = git_use_color_default; + + argc = parse_options(argc, argv, builtin_merge_options, + builtin_merge_usage, 0); + + if (squash) { + if (!allow_fast_forward) + die("You cannot combine --squash with --no-ff."); + option_commit = 0; + } + + if (!argc) + usage_with_options(builtin_merge_usage, + builtin_merge_options); + + /* + * This could be traditional "merge <msg> HEAD <commit>..." and + * the way we can tell it is to see if the second token is HEAD, + * but some people might have misused the interface and used a + * committish that is the same as HEAD there instead. + * Traditional format never would have "-m" so it is an + * additional safety measure to check for it. + */ + strbuf_init(&buf, 0); + + if (!have_message && is_old_style_invocation(argc, argv)) { + strbuf_addstr(&merge_msg, argv[0]); + head_arg = argv[1]; + argv += 2; + argc -= 2; + } else if (head_invalid) { + struct object *remote_head; + /* + * If the merged head is a valid one there is no reason + * to forbid "git merge" into a branch yet to be born. + * We do the same for "git pull". + */ + if (argc != 1) + die("Can merge only exactly one commit into " + "empty head"); + remote_head = peel_to_type(argv[0], 0, NULL, OBJ_COMMIT); + if (!remote_head) + die("%s - not something we can merge", argv[0]); + update_ref("initial pull", "HEAD", remote_head->sha1, NULL, 0, + DIE_ON_ERR); + reset_hard(remote_head->sha1, 0); + return 0; + } else { + struct strbuf msg; + + /* We are invoked directly as the first-class UI. */ + head_arg = "HEAD"; + + /* + * All the rest are the commits being merged; + * prepare the standard merge summary message to + * be appended to the given message. If remote + * is invalid we will die later in the common + * codepath so we discard the error in this + * loop. + */ + strbuf_init(&msg, 0); + for (i = 0; i < argc; i++) + merge_name(argv[i], &msg); + fmt_merge_msg(option_log, &msg, &merge_msg); + if (merge_msg.len) + strbuf_setlen(&merge_msg, merge_msg.len-1); + } + + if (head_invalid || !argc) + usage_with_options(builtin_merge_usage, + builtin_merge_options); + + strbuf_addstr(&buf, "merge"); + for (i = 0; i < argc; i++) + strbuf_addf(&buf, " %s", argv[i]); + setenv("GIT_REFLOG_ACTION", buf.buf, 0); + strbuf_reset(&buf); + + for (i = 0; i < argc; i++) { + struct object *o; + + o = peel_to_type(argv[i], 0, NULL, OBJ_COMMIT); + if (!o) + die("%s - not something we can merge", argv[i]); + remotes = &commit_list_insert(lookup_commit(o->sha1), + remotes)->next; + + strbuf_addf(&buf, "GITHEAD_%s", sha1_to_hex(o->sha1)); + setenv(buf.buf, argv[i], 1); + strbuf_reset(&buf); + } + + if (!use_strategies) { + if (!remoteheads->next) + add_strategies(pull_twohead, DEFAULT_TWOHEAD); + else + add_strategies(pull_octopus, DEFAULT_OCTOPUS); + } + + for (i = 0; i < use_strategies_nr; i++) { + if (use_strategies[i]->attr & NO_FAST_FORWARD) + allow_fast_forward = 0; + if (use_strategies[i]->attr & NO_TRIVIAL) + allow_trivial = 0; + } + + if (!remoteheads->next) + common = get_merge_bases(lookup_commit(head), + remoteheads->item, 1); + else { + struct commit_list *list = remoteheads; + commit_list_insert(lookup_commit(head), &list); + common = get_octopus_merge_bases(list); + free(list); + } + + update_ref("updating ORIG_HEAD", "ORIG_HEAD", head, NULL, 0, + DIE_ON_ERR); + + if (!common) + ; /* No common ancestors found. We need a real merge. */ + else if (!remoteheads->next && !common->next && + common->item == remoteheads->item) { + /* + * If head can reach all the merge then we are up to date. + * but first the most common case of merging one remote. + */ + finish_up_to_date("Already up-to-date."); + return 0; + } else if (allow_fast_forward && !remoteheads->next && + !common->next && + !hashcmp(common->item->object.sha1, head)) { + /* Again the most common case of merging one remote. */ + struct strbuf msg; + struct object *o; + char hex[41]; + + strcpy(hex, find_unique_abbrev(head, DEFAULT_ABBREV)); + + printf("Updating %s..%s\n", + hex, + find_unique_abbrev(remoteheads->item->object.sha1, + DEFAULT_ABBREV)); + refresh_cache(REFRESH_QUIET); + strbuf_init(&msg, 0); + strbuf_addstr(&msg, "Fast forward"); + if (have_message) + strbuf_addstr(&msg, + " (no commit created; -m option ignored)"); + o = peel_to_type(sha1_to_hex(remoteheads->item->object.sha1), + 0, NULL, OBJ_COMMIT); + if (!o) + return 1; + + if (checkout_fast_forward(head, remoteheads->item->object.sha1)) + return 1; + + finish(o->sha1, msg.buf); + drop_save(); + return 0; + } else if (!remoteheads->next && common->next) + ; + /* + * We are not doing octopus and not fast forward. Need + * a real merge. + */ + else if (!remoteheads->next && !common->next && option_commit) { + /* + * We are not doing octopus, not fast forward, and have + * only one common. + */ + refresh_cache(REFRESH_QUIET); + if (allow_trivial) { + /* See if it is really trivial. */ + git_committer_info(IDENT_ERROR_ON_NO_NAME); + printf("Trying really trivial in-index merge...\n"); + if (!read_tree_trivial(common->item->object.sha1, + head, remoteheads->item->object.sha1)) + return merge_trivial(); + printf("Nope.\n"); + } + } else { + /* + * An octopus. If we can reach all the remote we are up + * to date. + */ + int up_to_date = 1; + struct commit_list *j; + + for (j = remoteheads; j; j = j->next) { + struct commit_list *common_one; + + /* + * Here we *have* to calculate the individual + * merge_bases again, otherwise "git merge HEAD^ + * HEAD^^" would be missed. + */ + common_one = get_merge_bases(lookup_commit(head), + j->item, 1); + if (hashcmp(common_one->item->object.sha1, + j->item->object.sha1)) { + up_to_date = 0; + break; + } + } + if (up_to_date) { + finish_up_to_date("Already up-to-date. Yeeah!"); + return 0; + } + } + + /* We are going to make a new commit. */ + git_committer_info(IDENT_ERROR_ON_NO_NAME); + + /* + * At this point, we need a real merge. No matter what strategy + * we use, it would operate on the index, possibly affecting the + * working tree, and when resolved cleanly, have the desired + * tree in the index -- this means that the index must be in + * sync with the head commit. The strategies are responsible + * to ensure this. + */ + if (use_strategies_nr != 1) { + /* + * Stash away the local changes so that we can try more + * than one. + */ + save_state(); + } else { + memcpy(stash, null_sha1, 20); + } + + for (i = 0; i < use_strategies_nr; i++) { + int ret; + if (i) { + printf("Rewinding the tree to pristine...\n"); + restore_state(); + } + if (use_strategies_nr != 1) + printf("Trying merge strategy %s...\n", + use_strategies[i]->name); + /* + * Remember which strategy left the state in the working + * tree. + */ + wt_strategy = use_strategies[i]->name; + + ret = try_merge_strategy(use_strategies[i]->name, + common, head_arg); + if (!option_commit && !ret) { + merge_was_ok = 1; + /* + * This is necessary here just to avoid writing + * the tree, but later we will *not* exit with + * status code 1 because merge_was_ok is set. + */ + ret = 1; + } + + if (ret) { + /* + * The backend exits with 1 when conflicts are + * left to be resolved, with 2 when it does not + * handle the given merge at all. + */ + if (ret == 1) { + int cnt = evaluate_result(); + + if (best_cnt <= 0 || cnt <= best_cnt) { + best_strategy = use_strategies[i]->name; + best_cnt = cnt; + } + } + if (merge_was_ok) + break; + else + continue; + } + + /* Automerge succeeded. */ + write_tree_trivial(result_tree); + automerge_was_ok = 1; + break; + } + + /* + * If we have a resulting tree, that means the strategy module + * auto resolved the merge cleanly. + */ + if (automerge_was_ok) + return finish_automerge(common, result_tree, wt_strategy); + + /* + * Pick the result from the best strategy and have the user fix + * it up. + */ + if (!best_strategy) { + restore_state(); + if (use_strategies_nr > 1) + fprintf(stderr, + "No merge strategy handled the merge.\n"); + else + fprintf(stderr, "Merge with strategy %s failed.\n", + use_strategies[0]->name); + return 2; + } else if (best_strategy == wt_strategy) + ; /* We already have its result in the working tree. */ + else { + printf("Rewinding the tree to pristine...\n"); + restore_state(); + printf("Using the %s to prepare resolving by hand.\n", + best_strategy); + try_merge_strategy(best_strategy, common, head_arg); + } + + if (squash) + finish(NULL, NULL); + else { + int fd; + struct commit_list *j; + + for (j = remoteheads; j; j = j->next) + strbuf_addf(&buf, "%s\n", + sha1_to_hex(j->item->object.sha1)); + fd = open(git_path("MERGE_HEAD"), O_WRONLY | O_CREAT, 0666); + if (fd < 0) + die("Could open %s for writing", + git_path("MERGE_HEAD")); + if (write_in_full(fd, buf.buf, buf.len) != buf.len) + die("Could not write to %s", git_path("MERGE_HEAD")); + close(fd); + strbuf_addch(&merge_msg, '\n'); + fd = open(git_path("MERGE_MSG"), O_WRONLY | O_CREAT, 0666); + if (fd < 0) + die("Could open %s for writing", git_path("MERGE_MSG")); + if (write_in_full(fd, merge_msg.buf, merge_msg.len) != + merge_msg.len) + die("Could not write to %s", git_path("MERGE_MSG")); + close(fd); + } + + if (merge_was_ok) { + fprintf(stderr, "Automatic merge went well; " + "stopped before committing as requested\n"); + return 0; + } else + return suggest_conflicts(); +} diff --git a/builtin.h b/builtin.h index 05ee56f..0e605d4 100644 --- a/builtin.h +++ b/builtin.h @@ -64,6 +64,7 @@ extern int cmd_ls_tree(int argc, const char **argv, const char *prefix); extern int cmd_ls_remote(int argc, const char **argv, const char *prefix); extern int cmd_mailinfo(int argc, const char **argv, const char *prefix); extern int cmd_mailsplit(int argc, const char **argv, const char *prefix); +extern int cmd_merge(int argc, const char **argv, const char *prefix); extern int cmd_merge_base(int argc, const char **argv, const char *prefix); extern int cmd_merge_ours(int argc, const char **argv, const char *prefix); extern int cmd_merge_file(int argc, const char **argv, const char *prefix); diff --git a/git-merge.sh b/contrib/examples/git-merge.sh similarity index 100% rename from git-merge.sh rename to contrib/examples/git-merge.sh diff --git a/git.c b/git.c index 2fbe96b..770aadd 100644 --- a/git.c +++ b/git.c @@ -271,6 +271,7 @@ static void handle_internal_command(int argc, const char **argv) { "ls-remote", cmd_ls_remote }, { "mailinfo", cmd_mailinfo }, { "mailsplit", cmd_mailsplit }, + { "merge", cmd_merge, RUN_SETUP | NEED_WORK_TREE }, { "merge-base", cmd_merge_base, RUN_SETUP }, { "merge-file", cmd_merge_file }, { "merge-ours", cmd_merge_ours, RUN_SETUP }, diff --git a/t/t7602-merge-octopus-many.sh b/t/t7602-merge-octopus-many.sh index f3a4bb2..fcb8285 100755 --- a/t/t7602-merge-octopus-many.sh +++ b/t/t7602-merge-octopus-many.sh @@ -23,7 +23,7 @@ test_expect_success 'setup' ' done ' -test_expect_failure 'merge c1 with c2, c3, c4, ... c29' ' +test_expect_success 'merge c1 with c2, c3, c4, ... c29' ' git reset --hard c1 && i=2 && refs="" && -- 1.5.6.1.322.ge904b.dirty ^ permalink raw reply related [flat|nested] 82+ messages in thread
* Re: [PATCH] Build in merge 2008-07-08 0:32 ` Junio C Hamano 2008-07-08 0:53 ` Junio C Hamano @ 2008-07-08 1:00 ` Miklos Vajna 2008-07-08 1:05 ` Junio C Hamano 1 sibling, 1 reply; 82+ messages in thread From: Miklos Vajna @ 2008-07-08 1:00 UTC (permalink / raw To: Junio C Hamano; +Cc: git, Johannes Schindelin, Olivier Marin [-- Attachment #1: Type: text/plain, Size: 3544 bytes --] On Mon, Jul 07, 2008 at 05:32:50PM -0700, Junio C Hamano <gitster@pobox.com> wrote: > > So here is a version, this time without the use_strategies list. > > That is not what I meant. I am afraid perhaps I misunderstood what you > were talking about. > > When/if you allow user defined new strategies, then you have a choice: > > (1) find "git-merge-*" in path, add them to the single all_strategies[] > list (but you will do the ALLOC_GROW() business so you would need to > use the one you currently have as static form to prime the real list), > and look for "foo" strategy when "-s foo" is given from that single > list, or > > (2) find "git-merge-*" in path, add them to a separate user_strategies[] > list, and look for "foo" strategy when "-s foo" is given from the > user_strategies[] list and all_strategies[] list (all_strategies[] > should perhaps be renamed to builtin_strategies[] if you go that > route). OK, I see now. Actually I think the primary problem with a custom strategy is that we do not know its flags, IOW if it handles an octopus, etc. So I think there are two questions here: 1) How to tell git-merge the flags of a custom strategy? Or: is it necessary at all? I could imagine the following situations: 1) A project has code in a repo, some documentation and po files. The first two can be merged with builtin strategies, the later probably needs a custom merge driver. So, in most cases recursive is fine, but sometimes the maintainer wants to say 'git pull -s po'. In this case the flags of 'po' does not really matter. 2) Someone is not happy with the current recursive strategy and writes from scratch a new one. He/she puts it to pull.twohead, so it will be tried before recursive. To sum up: I am not sure what would be the benefit of being able to specify flags for strategies. However, if we want so, it would be good to discuss how it should be done. > The part that records which strategies were specified from the command > line *in what order* via "-s foo" switches should remain list of pointers > into "struct strategy", which is called "struct strategy **use_strategies" > in the code and corresponds to the $use_strategies variable in the > scripted version. The order of these is important, as that defines in > which order the strategies are tried [*1*]. If you go route (1), these > pointers will all be pointing at elements in all_strategies[]; with route > (2) they may be pointing at either all_strageties[] element or > user_strategies[] element. I see the problem, I lost the order at the moment. > If you are never going to say "available strategies are these" after you > start supporting user-defined strategy, then you do not necessarily need > to do the "find 'git-merge-*' in path, add them to ..." step above, in > which case it would be Ok not to scan the path and add them to > all_strategies[] (in route (1)) nor user_strategies[] (in route (2)). > Instead, you would just create a new "struct strategy" instance lazily > when the user gave "-s foo" and "foo" is not one of the built-in strategy. > You would put that at the tail of "struct strategy **use_strategy" array, > and iterate over use_strategy in the order they are given on the command > line. I guess it would be nice to adjust the error message and scanning path, but that's something - I guess - that should be done after the rewrite is complete and there are no more issues with it. [-- Attachment #2: Type: application/pgp-signature, Size: 197 bytes --] ^ permalink raw reply [flat|nested] 82+ messages in thread
* Re: [PATCH] Build in merge 2008-07-08 1:00 ` Miklos Vajna @ 2008-07-08 1:05 ` Junio C Hamano 2008-07-08 1:41 ` Miklos Vajna 0 siblings, 1 reply; 82+ messages in thread From: Junio C Hamano @ 2008-07-08 1:05 UTC (permalink / raw To: Miklos Vajna; +Cc: git, Johannes Schindelin, Olivier Marin Miklos Vajna <vmiklos@frugalware.org> writes: > I could imagine the following situations: > > 1) A project has code in a repo, some documentation and po files. > > The first two can be merged with builtin strategies, the later probably > needs a custom merge driver. Per-path merge is probably better handled with custom ll-merge driver anyway. See gitattributes(5). > 2) Someone is not happy with the current recursive strategy and writes > from scratch a new one. He/she puts it to pull.twohead, so it will be > tried before recursive. That is fine. > To sum up: I am not sure what would be the benefit of being able to > specify flags for strategies. However, if we want so, it would be good > to discuss how it should be done. It wasn't *me* ;-) who wanted to add these "flags". I think it does not matter what "my-strategy" does unless "-s my-strategy" (or pull.twohead) is explicitly given by the user, and at that time, DEFAULT_* options should not matter. It probably is Ok to allow fast forward and trivial merges to them. We'll see. >> The part that records which strategies were specified from the command >> line *in what order* via "-s foo" switches should remain list of pointers >> into "struct strategy", which is called "struct strategy **use_strategies" >> in the code and corresponds to the $use_strategies variable in the >> scripted version. The order of these is important, as that defines in >> which order the strategies are tried [*1*]. If you go route (1), these >> pointers will all be pointing at elements in all_strategies[]; with route >> (2) they may be pointing at either all_strageties[] element or >> user_strategies[] element. > > I see the problem, I lost the order at the moment. That's Ok. See the other patch on top of your previous one. ^ permalink raw reply [flat|nested] 82+ messages in thread
* Re: [PATCH] Build in merge 2008-07-08 1:05 ` Junio C Hamano @ 2008-07-08 1:41 ` Miklos Vajna 0 siblings, 0 replies; 82+ messages in thread From: Miklos Vajna @ 2008-07-08 1:41 UTC (permalink / raw To: Junio C Hamano; +Cc: git, Johannes Schindelin, Olivier Marin [-- Attachment #1: Type: text/plain, Size: 1232 bytes --] On Mon, Jul 07, 2008 at 06:05:43PM -0700, Junio C Hamano <gitster@pobox.com> wrote: > > 1) A project has code in a repo, some documentation and po files. > > > > The first two can be merged with builtin strategies, the later probably > > needs a custom merge driver. > > Per-path merge is probably better handled with custom ll-merge driver > anyway. See gitattributes(5). Ah, thanks. I did not know ll-merge supports custom drivers as well. > It wasn't *me* ;-) who wanted to add these "flags". > > I think it does not matter what "my-strategy" does unless "-s my-strategy" > (or pull.twohead) is explicitly given by the user, and at that time, > DEFAULT_* options should not matter. It probably is Ok to allow fast > forward and trivial merges to them. We'll see. OK, so at first round I think we could avoid flags for custom strategies. For the error message, I think the output could be something like git help --all, which splits commands based on being in GIT_EXEC_PATH or somewhere else in PATH. Also, currently (the shell version and the C one as well) we silently ignore the config setting if it's set to semething invalid. What about changing that to a similar error message as well? [-- Attachment #2: Type: application/pgp-signature, Size: 197 bytes --] ^ permalink raw reply [flat|nested] 82+ messages in thread
* Re: [PATCH 14/14] Build in merge 2008-07-06 8:50 ` Junio C Hamano 2008-07-06 9:43 ` Junio C Hamano @ 2008-07-06 12:38 ` Johannes Schindelin 2008-07-06 19:39 ` Junio C Hamano 2008-07-07 17:24 ` [PATCH] " Miklos Vajna 2 siblings, 1 reply; 82+ messages in thread From: Johannes Schindelin @ 2008-07-06 12:38 UTC (permalink / raw To: Junio C Hamano; +Cc: Miklos Vajna, git, Olivier Marin Hi, On Sun, 6 Jul 2008, Junio C Hamano wrote: > Miklos Vajna <vmiklos@frugalware.org> writes: > > > diff --git a/builtin-merge.c b/builtin-merge.c > > new file mode 100644 > > index 0000000..b261993 > > --- /dev/null > > +++ b/builtin-merge.c > > @@ -0,0 +1,1158 @@ > > +/* > > + * Builtin "git merge" > > + * > > + * Copyright (c) 2008 Miklos Vajna <vmiklos@frugalware.org> > > + * > > + * Based on git-merge.sh by Junio C Hamano. > > + */ > > + > > +#include "cache.h" > > +#include "parse-options.h" > > +#include "builtin.h" > > +#include "run-command.h" > > +#include "path-list.h" > > +#include "diff.h" > > +#include "refs.h" > > +#include "commit.h" > > +#include "diffcore.h" > > +#include "revision.h" > > +#include "unpack-trees.h" > > +#include "cache-tree.h" > > +#include "dir.h" > > +#include "utf8.h" > > +#include "log-tree.h" > > +#include "color.h" > > + > > +enum strategy { > > + DEFAULT_TWOHEAD = 1, > > + DEFAULT_OCTOPUS = 2, > > + NO_FAST_FORWARD = 4, > > + NO_TRIVIAL = 8 > > +}; > > Usually "enum foo" consists of possible values of "foo". But this is > not a list of strategies. These are possible attributes to strategies. My fault. I avoid ugly #defines, so I suggested using an enum. > > +static const char * const builtin_merge_usage[] = { > > + "git-merge [options] <remote>...", > > + "git-merge [options] <msg> HEAD <remote>", > > + NULL > > +}; > > + > > +static int show_diffstat = 1, option_log, squash; > > +static int option_commit = 1, allow_fast_forward = 1; > > +static int allow_trivial = 1, have_message; > > +static struct strbuf merge_msg; > > +static struct commit_list *remoteheads; > > +static unsigned char head[20], stash[20]; > > +static struct path_list use_strategies; > > +static const char *branch; > > + > > +static struct path_list_item strategy_items[] = { > > + { "recur", (void *)NO_TRIVIAL }, > > + { "recursive", (void *)(DEFAULT_TWOHEAD | NO_TRIVIAL) }, > > + { "octopus", (void *)DEFAULT_OCTOPUS }, > > + { "resolve", (void *)0 }, > > + { "stupid", (void *)0 }, > > + { "ours", (void *)(NO_FAST_FORWARD | NO_TRIVIAL) }, > > + { "subtree", (void *)(NO_FAST_FORWARD | NO_TRIVIAL) }, > > +}; > > +static struct path_list strategies = { strategy_items, > > + ARRAY_SIZE(strategy_items), 0, 0 }; > > This declaration is funnily line-wrapped. > > static struct path_list strategies = { > strategy_items, ARRAY_SIZE(strategy_items), 0, 0, > }; > > But more problematic is that a path_list is inherently a dynamic data > structure (you can add and it reallocs), and this use of relying on the > knowledge that you happen to never add anything (nor subtract anything) > from the list is a mere hack. If on the other hand you (and more > importantly other people who touch this implementation later) will never > add or remove items from this "strategies" array, you should make sure > at the interface level that nobody can -- one way to do so is not to > abuse path_list for something like this. > > Come to think of it, wasn't the reason why the earlier "Why do you need > such distracting casts all over the place?" issue came up in another > patch because of this kind of (ab)use of path_list, which is an > inappropriate data structure for the job? Well, it is not really an abuse if you think of path_list as a string_list, which it really is, and which should have happened a long time ago, but I gave up. Anyway, the use of string_list here was originally to make lookup slightly more convenient, using an API, instead of reinventing the wheel over and over and over again, as can be seen nicely in some parts of Git's source code. Given that we want to be able to add other strategies, I would have rather suggested fixing string_list to heed "alloc == 0" and _not_ realloc() in that case. But given that you seem so sick and tired of string_list, and rather have a code duplication, I will not argue to that end anymore. Tired, Dscho ^ permalink raw reply [flat|nested] 82+ messages in thread
* Re: [PATCH 14/14] Build in merge 2008-07-06 12:38 ` [PATCH 14/14] " Johannes Schindelin @ 2008-07-06 19:39 ` Junio C Hamano 0 siblings, 0 replies; 82+ messages in thread From: Junio C Hamano @ 2008-07-06 19:39 UTC (permalink / raw To: Johannes Schindelin; +Cc: Miklos Vajna, git, Olivier Marin Johannes Schindelin <Johannes.Schindelin@gmx.de> writes: > But given that you seem so sick and tired of string_list, and rather have > a code duplication, I will not argue to that end anymore. You are probably very confused if you think I am saying I'd rather have duplication. Look at "unsorted_path_list_lookup()" in builtin-merge.c and think again. Look at "path_list_append_strategy()" in builtin-merge.c and think again. If you are adding the same amount of code _anyway_, why not write using more appropriate data structure for the job? The path_list has its uses. It's wonderful when you have existing structures that you would need to keep in core anyway and being able to look them up via string keys. But that does not mean it is (nor should be "improved" to be) a good match for other forms of (ab)uses. The way it was used by initializing with pointer to a static array location is clearly a misuse. When using ->util field to store things other than pointers to preexisting structures, the use of the API becomes clunky and we discussed this issue about another patch. That's all I was saying. ^ permalink raw reply [flat|nested] 82+ messages in thread
* [PATCH] Build in merge 2008-07-06 8:50 ` Junio C Hamano 2008-07-06 9:43 ` Junio C Hamano 2008-07-06 12:38 ` [PATCH 14/14] " Johannes Schindelin @ 2008-07-07 17:24 ` Miklos Vajna 2008-07-07 17:35 ` Miklos Vajna 2 siblings, 1 reply; 82+ messages in thread From: Miklos Vajna @ 2008-07-07 17:24 UTC (permalink / raw To: To: Junio C Hamano; +Cc: git, Johannes Schindelin, Olivier Marin Mentored-by: Johannes Schindelin <Johannes.Schindelin@gmx.de> Signed-off-by: Miklos Vajna <vmiklos@frugalware.org> --- On Sun, Jul 06, 2008 at 01:50:05AM -0700, Junio C Hamano <gitster@pobox.com> wrote: > But more problematic is that a path_list is inherently a dynamic data > structure (you can add and it reallocs), and this use of relying on > the > knowledge that you happen to never add anything (nor subtract > anything) > from the list is a mere hack. If on the other hand you (and more > importantly other people who touch this implementation later) will > never > add or remove items from this "strategies" array, you should make sure > at > the interface level that nobody can -- one way to do so is not to > abuse > path_list for something like this. > > Come to think of it, wasn't the reason why the earlier "Why do you > need > such distracting casts all over the place?" issue came up in another > patch > because of this kind of (ab)use of path_list, which is an > inappropriate > data structure for the job? Here is an updated version without using path_list at all. Makefile | 2 +- builtin-merge.c | 1153 +++++++++++++++++++++++++ builtin.h | 1 + git-merge.sh => contrib/examples/git-merge.sh | 0 git.c | 1 + t/t7602-merge-octopus-many.sh | 2 +- 6 files changed, 1157 insertions(+), 2 deletions(-) create mode 100644 builtin-merge.c rename git-merge.sh => contrib/examples/git-merge.sh (100%) diff --git a/Makefile b/Makefile index bf77292..fbc53e9 100644 --- a/Makefile +++ b/Makefile @@ -240,7 +240,6 @@ SCRIPT_SH += git-lost-found.sh SCRIPT_SH += git-merge-octopus.sh SCRIPT_SH += git-merge-one-file.sh SCRIPT_SH += git-merge-resolve.sh -SCRIPT_SH += git-merge.sh SCRIPT_SH += git-merge-stupid.sh SCRIPT_SH += git-mergetool.sh SCRIPT_SH += git-parse-remote.sh @@ -515,6 +514,7 @@ BUILTIN_OBJS += builtin-ls-remote.o BUILTIN_OBJS += builtin-ls-tree.o BUILTIN_OBJS += builtin-mailinfo.o BUILTIN_OBJS += builtin-mailsplit.o +BUILTIN_OBJS += builtin-merge.o BUILTIN_OBJS += builtin-merge-base.o BUILTIN_OBJS += builtin-merge-file.o BUILTIN_OBJS += builtin-merge-ours.o diff --git a/builtin-merge.c b/builtin-merge.c new file mode 100644 index 0000000..7312997 --- /dev/null +++ b/builtin-merge.c @@ -0,0 +1,1153 @@ +/* + * Builtin "git merge" + * + * Copyright (c) 2008 Miklos Vajna <vmiklos@frugalware.org> + * + * Based on git-merge.sh by Junio C Hamano. + */ + +#include "cache.h" +#include "parse-options.h" +#include "builtin.h" +#include "run-command.h" +#include "diff.h" +#include "refs.h" +#include "commit.h" +#include "diffcore.h" +#include "revision.h" +#include "unpack-trees.h" +#include "cache-tree.h" +#include "dir.h" +#include "utf8.h" +#include "log-tree.h" +#include "color.h" + +#define DEFAULT_TWOHEAD (1<<0) +#define DEFAULT_OCTOPUS (1<<1) +#define NO_FAST_FORWARD (1<<2) +#define NO_TRIVIAL (1<<3) + +struct strategy { + const char *name; + unsigned attr; +}; + +static const char * const builtin_merge_usage[] = { + "git-merge [options] <remote>...", + "git-merge [options] <msg> HEAD <remote>", + NULL +}; + +static int show_diffstat = 1, option_log, squash; +static int option_commit = 1, allow_fast_forward = 1; +static int allow_trivial = 1, have_message; +static struct strbuf merge_msg; +static struct commit_list *remoteheads; +static unsigned char head[20], stash[20]; +static struct strategy *use_strategies; +static size_t use_strategies_nr, use_strategies_alloc; +static const char *branch; + +static struct strategy all_strategy[] = { + { "recur", NO_TRIVIAL }, + { "recursive", DEFAULT_TWOHEAD | NO_TRIVIAL }, + { "octopus", DEFAULT_OCTOPUS }, + { "resolve", 0 }, + { "stupid", 0 }, + { "ours", NO_FAST_FORWARD | NO_TRIVIAL }, + { "subtree", NO_FAST_FORWARD | NO_TRIVIAL }, +}; + +static const char *pull_twohead, *pull_octopus; + +static int option_parse_message(const struct option *opt, + const char *arg, int unset) +{ + struct strbuf *buf = opt->value; + + if (unset) + strbuf_setlen(buf, 0); + else { + strbuf_addf(buf, "%s\n\n", arg); + have_message = 1; + } + return 0; +} + +static int strategy_lookup(const char *path) +{ + int i; + + if (!path) + return -1; + + for (i = 0; i < ARRAY_SIZE(all_strategy); i++) + if (!strcmp(path, all_strategy[i].name)) + return all_strategy[i].attr; + return -1; +} + +static inline void append_strategy(const char *name, unsigned attr) +{ + ALLOC_GROW(use_strategies, use_strategies_nr + 1, use_strategies_alloc); + use_strategies[use_strategies_nr].name = name; + use_strategies[use_strategies_nr++].attr = attr; +} + +static int option_parse_strategy(const struct option *opt, + const char *arg, int unset) +{ + int i, attr; + + if (unset) + return 0; + + attr = strategy_lookup(arg); + + if (attr >= 0) + append_strategy(arg, attr); + else { + struct strbuf err; + strbuf_init(&err, 0); + for (i = 0; i < ARRAY_SIZE(all_strategy); i++) + strbuf_addf(&err, " %s", all_strategy[i].name); + fprintf(stderr, "Could not find merge strategy '%s'.\n", arg); + fprintf(stderr, "Available strategies are:%s.\n", err.buf); + exit(1); + } + return 0; +} + +static int option_parse_n(const struct option *opt, + const char *arg, int unset) +{ + show_diffstat = unset; + return 0; +} + +static struct option builtin_merge_options[] = { + { OPTION_CALLBACK, 'n', NULL, NULL, NULL, + "do not show a diffstat at the end of the merge", + PARSE_OPT_NOARG, option_parse_n }, + OPT_BOOLEAN(0, "stat", &show_diffstat, + "show a diffstat at the end of the merge"), + OPT_BOOLEAN(0, "summary", &show_diffstat, "(synonym to --stat)"), + OPT_BOOLEAN(0, "log", &option_log, + "add list of one-line log to merge commit message"), + OPT_BOOLEAN(0, "squash", &squash, + "create a single commit instead of doing a merge"), + OPT_BOOLEAN(0, "commit", &option_commit, + "perform a commit if the merge succeeds (default)"), + OPT_BOOLEAN(0, "ff", &allow_fast_forward, + "allow fast forward (default)"), + OPT_CALLBACK('s', "strategy", &use_strategies, "strategy", + "merge strategy to use", option_parse_strategy), + OPT_CALLBACK('m', "message", &merge_msg, "message", + "message to be used for the merge commit (if any)", + option_parse_message), + OPT_END() +}; + +/* Cleans up metadata that is uninteresting after a succeeded merge. */ +static void drop_save(void) +{ + unlink(git_path("MERGE_HEAD")); + unlink(git_path("MERGE_MSG")); +} + +static void save_state(void) +{ + int len; + struct child_process cp; + struct strbuf buffer = STRBUF_INIT; + const char *argv[] = {"stash", "create", NULL}; + + memset(&cp, 0, sizeof(cp)); + cp.argv = argv; + cp.out = -1; + cp.git_cmd = 1; + + if (start_command(&cp)) + die("could not run stash."); + len = strbuf_read(&buffer, cp.out, 1024); + close(cp.out); + + if (finish_command(&cp) || len < 0) + die("stash failed"); + else if (!len) + return; + strbuf_setlen(&buffer, buffer.len-1); + if (get_sha1(buffer.buf, stash)) + die("not a valid object: %s", buffer.buf); +} + +static void reset_hard(unsigned const char *sha1, int verbose) +{ + int i = 0; + const char *args[6]; + + args[i++] = "read-tree"; + if (verbose) + args[i++] = "-v"; + args[i++] = "--reset"; + args[i++] = "-u"; + args[i++] = sha1_to_hex(sha1); + args[i] = NULL; + + if (run_command_v_opt(args, RUN_GIT_CMD)) + die("read-tree failed"); +} + +static void restore_state(void) +{ + struct strbuf sb; + const char *args[] = { "stash", "apply", NULL, NULL }; + + if (is_null_sha1(stash)) + return; + + reset_hard(head, 1); + + strbuf_init(&sb, 0); + args[2] = sha1_to_hex(stash); + + /* + * It is OK to ignore error here, for example when there was + * nothing to restore. + */ + run_command_v_opt(args, RUN_GIT_CMD); + + strbuf_release(&sb); + refresh_cache(REFRESH_QUIET); +} + +/* This is called when no merge was necessary. */ +static void finish_up_to_date(const char *msg) +{ + printf("%s%s\n", squash ? " (nothing to squash)" : "", msg); + drop_save(); +} + +static void squash_message(void) +{ + struct rev_info rev; + struct commit *commit; + struct strbuf out; + struct commit_list *j; + int fd; + + printf("Squash commit -- not updating HEAD\n"); + fd = open(git_path("SQUASH_MSG"), O_WRONLY | O_CREAT, 0666); + if (fd < 0) + die("Could not write to %s", git_path("SQUASH_MSG")); + + init_revisions(&rev, NULL); + rev.ignore_merges = 1; + rev.commit_format = CMIT_FMT_MEDIUM; + + commit = lookup_commit(head); + commit->object.flags |= UNINTERESTING; + add_pending_object(&rev, &commit->object, NULL); + + for (j = remoteheads; j; j = j->next) + add_pending_object(&rev, &j->item->object, NULL); + + setup_revisions(0, NULL, &rev, NULL); + if (prepare_revision_walk(&rev)) + die("revision walk setup failed"); + + strbuf_init(&out, 0); + strbuf_addstr(&out, "Squashed commit of the following:\n"); + while ((commit = get_revision(&rev)) != NULL) { + strbuf_addch(&out, '\n'); + strbuf_addf(&out, "commit %s\n", + sha1_to_hex(commit->object.sha1)); + pretty_print_commit(rev.commit_format, commit, &out, rev.abbrev, + NULL, NULL, rev.date_mode, 0); + } + write(fd, out.buf, out.len); + close(fd); + strbuf_release(&out); +} + +static int run_hook(const char *name) +{ + struct child_process hook; + const char *argv[3], *env[2]; + char index[PATH_MAX]; + + argv[0] = git_path("hooks/%s", name); + if (access(argv[0], X_OK) < 0) + return 0; + + snprintf(index, sizeof(index), "GIT_INDEX_FILE=%s", get_index_file()); + env[0] = index; + env[1] = NULL; + + if (squash) + argv[1] = "1"; + else + argv[1] = "0"; + argv[2] = NULL; + + memset(&hook, 0, sizeof(hook)); + hook.argv = argv; + hook.no_stdin = 1; + hook.stdout_to_stderr = 1; + hook.env = env; + + return run_command(&hook); +} + +static void finish(const unsigned char *new_head, const char *msg) +{ + struct strbuf reflog_message; + + strbuf_init(&reflog_message, 0); + if (!msg) + strbuf_addstr(&reflog_message, getenv("GIT_REFLOG_ACTION")); + else { + printf("%s\n", msg); + strbuf_addf(&reflog_message, "%s: %s", + getenv("GIT_REFLOG_ACTION"), msg); + } + if (squash) { + squash_message(); + } else { + if (!merge_msg.len) + printf("No merge message -- not updating HEAD\n"); + else { + const char *argv_gc_auto[] = { "gc", "--auto", NULL }; + update_ref(reflog_message.buf, "HEAD", + new_head, head, 0, + DIE_ON_ERR); + /* + * We ignore errors in 'gc --auto', since the + * user should see them. + */ + run_command_v_opt(argv_gc_auto, RUN_GIT_CMD); + } + } + if (new_head && show_diffstat) { + struct diff_options opts; + diff_setup(&opts); + opts.output_format |= + DIFF_FORMAT_SUMMARY | DIFF_FORMAT_DIFFSTAT; + opts.detect_rename = DIFF_DETECT_RENAME; + if (diff_use_color_default > 0) + DIFF_OPT_SET(&opts, COLOR_DIFF); + if (diff_setup_done(&opts) < 0) + die("diff_setup_done failed"); + diff_tree_sha1(head, new_head, "", &opts); + diffcore_std(&opts); + diff_flush(&opts); + } + + /* Run a post-merge hook */ + run_hook("post-merge"); + + strbuf_release(&reflog_message); +} + +/* Get the name for the merge commit's message. */ +static void merge_name(const char *remote, struct strbuf *msg) +{ + struct object *remote_head; + unsigned char branch_head[20], buf_sha[20]; + struct strbuf buf; + const char *ptr; + int len, early; + + memset(branch_head, 0, sizeof(branch_head)); + remote_head = peel_to_type(remote, 0, NULL, OBJ_COMMIT); + if (!remote_head) + die("'%s' does not point to a commit", remote); + + strbuf_init(&buf, 0); + strbuf_addstr(&buf, "refs/heads/"); + strbuf_addstr(&buf, remote); + resolve_ref(buf.buf, branch_head, 0, 0); + + if (!hashcmp(remote_head->sha1, branch_head)) { + strbuf_addf(msg, "%s\t\tbranch '%s' of .\n", + sha1_to_hex(branch_head), remote); + return; + } + + /* See if remote matches <name>^^^.. or <name>~<number> */ + for (len = 0, ptr = remote + strlen(remote); + remote < ptr && ptr[-1] == '^'; + ptr--) + len++; + if (len) + early = 1; + else { + early = 0; + ptr = strrchr(remote, '~'); + if (ptr) { + int seen_nonzero = 0; + + len++; /* count ~ */ + while (*++ptr && isdigit(*ptr)) { + seen_nonzero |= (*ptr != '0'); + len++; + } + if (*ptr) + len = 0; /* not ...~<number> */ + else if (seen_nonzero) + early = 1; + else if (len == 1) + early = 1; /* "name~" is "name~1"! */ + } + } + if (len) { + struct strbuf truname = STRBUF_INIT; + strbuf_addstr(&truname, "refs/heads/"); + strbuf_addstr(&truname, remote); + strbuf_setlen(&truname, len+11); + if (resolve_ref(truname.buf, buf_sha, 0, 0)) { + strbuf_addf(msg, + "%s\t\tbranch '%s'%s of .\n", + sha1_to_hex(remote_head->sha1), + truname.buf, + (early ? " (early part)" : "")); + return; + } + } + + if (!strcmp(remote, "FETCH_HEAD") && + !access(git_path("FETCH_HEAD"), R_OK)) { + FILE *fp; + struct strbuf line; + char *ptr; + + strbuf_init(&line, 0); + fp = fopen(git_path("FETCH_HEAD"), "r"); + if (!fp) + die("could not open %s for reading: %s", + git_path("FETCH_HEAD"), strerror(errno)); + strbuf_getline(&line, fp, '\n'); + fclose(fp); + ptr = strstr(line.buf, "\tnot-for-merge\t"); + if (ptr) + strbuf_remove(&line, ptr-line.buf+1, 13); + strbuf_addbuf(msg, &line); + strbuf_release(&line); + return; + } + strbuf_addf(msg, "%s\t\tcommit '%s'\n", + sha1_to_hex(remote_head->sha1), remote); +} + +int git_merge_config(const char *k, const char *v, void *cb) +{ + if (branch && !prefixcmp(k, "branch.") && + !prefixcmp(k + 7, branch) && + !strcmp(k + 7 + strlen(branch), ".mergeoptions")) { + const char **argv; + int argc; + char *buf; + + buf = xstrdup(v); + argc = split_cmdline(buf, &argv); + argv = xrealloc(argv, sizeof(*argv) * (argc + 2)); + memmove(argv + 1, argv, sizeof(*argv) * (argc + 1)); + argc++; + parse_options(argc, argv, builtin_merge_options, + builtin_merge_usage, 0); + free(buf); + } + + if (!strcmp(k, "merge.diffstat") || !strcmp(k, "merge.stat")) + show_diffstat = git_config_bool(k, v); + else if (!strcmp(k, "pull.twohead")) + return git_config_string(&pull_twohead, k, v); + else if (!strcmp(k, "pull.octopus")) + return git_config_string(&pull_octopus, k, v); + return git_diff_ui_config(k, v, cb); +} + +static int read_tree_trivial(unsigned char *common, unsigned char *head, + unsigned char *one) +{ + int i, nr_trees = 0; + struct tree *trees[MAX_UNPACK_TREES]; + struct tree_desc t[MAX_UNPACK_TREES]; + struct unpack_trees_options opts; + + memset(&opts, 0, sizeof(opts)); + opts.head_idx = 2; + opts.src_index = &the_index; + opts.dst_index = &the_index; + opts.update = 1; + opts.verbose_update = 1; + opts.trivial_merges_only = 1; + opts.merge = 1; + trees[nr_trees] = parse_tree_indirect(common); + if (!trees[nr_trees++]) + return -1; + trees[nr_trees] = parse_tree_indirect(head); + if (!trees[nr_trees++]) + return -1; + trees[nr_trees] = parse_tree_indirect(one); + if (!trees[nr_trees++]) + return -1; + opts.fn = threeway_merge; + cache_tree_free(&active_cache_tree); + for (i = 0; i < nr_trees; i++) { + parse_tree(trees[i]); + init_tree_desc(t+i, trees[i]->buffer, trees[i]->size); + } + if (unpack_trees(nr_trees, t, &opts)) + return -1; + return 0; +} + +static void write_tree_trivial(unsigned char *sha1) +{ + if (write_cache_as_tree(sha1, 0, NULL)) + die("git write-tree failed to write a tree"); +} + +static int try_merge_strategy(const char *strategy, struct commit_list *common, + const char *head_arg) +{ + const char **args; + int i = 0, ret; + struct commit_list *j; + struct strbuf buf; + + args = xmalloc((4 + commit_list_count(common) + + commit_list_count(remoteheads)) * sizeof(char *)); + strbuf_init(&buf, 0); + strbuf_addf(&buf, "merge-%s", strategy); + args[i++] = buf.buf; + for (j = common; j; j = j->next) + args[i++] = xstrdup(sha1_to_hex(j->item->object.sha1)); + args[i++] = "--"; + args[i++] = head_arg; + for (j = remoteheads; j; j = j->next) + args[i++] = xstrdup(sha1_to_hex(j->item->object.sha1)); + args[i] = NULL; + ret = run_command_v_opt(args, RUN_GIT_CMD); + strbuf_release(&buf); + i = 1; + for (j = common; j; j = j->next) + free((void *)args[i++]); + i += 2; + for (j = remoteheads; j; j = j->next) + free((void *)args[i++]); + free(args); + return -ret; +} + +static void count_diff_files(struct diff_queue_struct *q, + struct diff_options *opt, void *data) +{ + int *count = data; + + (*count) += q->nr; +} + +static int count_unmerged_entries(void) +{ + const struct index_state *state = &the_index; + int i, ret = 0; + + for (i = 0; i < state->cache_nr; i++) + if (ce_stage(state->cache[i])) + ret++; + + return ret; +} + +static int checkout_fast_forward(unsigned char *head, unsigned char *remote) +{ + struct tree *trees[MAX_UNPACK_TREES]; + struct unpack_trees_options opts; + struct tree_desc t[MAX_UNPACK_TREES]; + int i, fd, nr_trees = 0; + struct dir_struct dir; + struct lock_file *lock_file = xcalloc(1, sizeof(struct lock_file)); + + if (read_cache_unmerged()) + die("you need to resolve your current index first"); + + fd = hold_locked_index(lock_file, 1); + + memset(&trees, 0, sizeof(trees)); + memset(&opts, 0, sizeof(opts)); + memset(&t, 0, sizeof(t)); + dir.show_ignored = 1; + dir.exclude_per_dir = ".gitignore"; + opts.dir = &dir; + + opts.head_idx = 1; + opts.src_index = &the_index; + opts.dst_index = &the_index; + opts.update = 1; + opts.verbose_update = 1; + opts.merge = 1; + opts.fn = twoway_merge; + + trees[nr_trees] = parse_tree_indirect(head); + if (!trees[nr_trees++]) + return -1; + trees[nr_trees] = parse_tree_indirect(remote); + if (!trees[nr_trees++]) + return -1; + for (i = 0; i < nr_trees; i++) { + parse_tree(trees[i]); + init_tree_desc(t+i, trees[i]->buffer, trees[i]->size); + } + if (unpack_trees(nr_trees, t, &opts)) + return -1; + if (write_cache(fd, active_cache, active_nr) || + commit_locked_index(lock_file)) + die("unable to write new index file"); + return 0; +} + +static void split_merge_strategies(const char *string, struct strategy **list, + int *nr, int *alloc) +{ + char *p, *q, *buf; + + if (!string) + return; + + buf = xstrdup(string); + q = buf; + for (;;) { + p = strchr(q, ' '); + if (!p) { + ALLOC_GROW(*list, *nr + 1, *alloc); + (*list)[(*nr)++].name = xstrdup(q); + free(buf); + return; + } else { + *p = '\0'; + ALLOC_GROW(*list, *nr + 1, *alloc); + (*list)[(*nr)++].name = xstrdup(q); + q = ++p; + } + } +} + +static void add_strategies(const char *string, unsigned attr) +{ + struct strategy *list = NULL; + int list_alloc = 0, list_nr = 0, i; + + memset(&list, 0, sizeof(list)); + split_merge_strategies(string, &list, &list_nr, &list_alloc); + if (list != NULL) { + for (i = 0; i < list_nr; i++) { + int attr; + + attr = strategy_lookup(list[i].name); + if (attr >= 0) + append_strategy(list[i].name, attr); + } + return; + } + for (i = 0; i < ARRAY_SIZE(all_strategy); i++) + if (all_strategy[i].attr & attr) + append_strategy(all_strategy[i].name, + all_strategy[i].attr); +} + +static int merge_trivial(void) +{ + unsigned char result_tree[20], result_commit[20]; + struct commit_list parent; + + write_tree_trivial(result_tree); + printf("Wonderful.\n"); + parent.item = remoteheads->item; + parent.next = NULL; + commit_tree(merge_msg.buf, result_tree, &parent, result_commit); + finish(result_commit, "In-index merge"); + drop_save(); + return 0; +} + +static int finish_automerge(struct commit_list *common, + unsigned char *result_tree, + const char *wt_strategy) +{ + struct commit_list *parents = NULL, *j; + struct strbuf buf = STRBUF_INIT; + unsigned char result_commit[20]; + + free_commit_list(common); + if (allow_fast_forward) { + parents = remoteheads; + commit_list_insert(lookup_commit(head), &parents); + parents = reduce_heads(parents); + } else { + struct commit_list **pptr = &parents; + + pptr = &commit_list_insert(lookup_commit(head), + pptr)->next; + for (j = remoteheads; j; j = j->next) + pptr = &commit_list_insert(j->item, pptr)->next; + } + free_commit_list(remoteheads); + strbuf_addch(&merge_msg, '\n'); + commit_tree(merge_msg.buf, result_tree, parents, result_commit); + strbuf_addf(&buf, "Merge made by %s.", wt_strategy); + finish(result_commit, buf.buf); + strbuf_release(&buf); + drop_save(); + return 0; +} + +static int suggest_conflicts(void) +{ + FILE *fp; + int pos; + + fp = fopen(git_path("MERGE_MSG"), "a"); + if (!fp) + die("Could open %s for writing", git_path("MERGE_MSG")); + fprintf(fp, "\nConflicts:\n"); + for (pos = 0; pos < active_nr; pos++) { + struct cache_entry *ce = active_cache[pos]; + + if (ce_stage(ce)) { + fprintf(fp, "\t%s\n", ce->name); + while (pos + 1 < active_nr && + !strcmp(ce->name, + active_cache[pos + 1]->name)) + pos++; + } + } + fclose(fp); + rerere(); + printf("Automatic merge failed; " + "fix conflicts and then commit the result.\n"); + return 1; +} + +static struct commit *is_old_style_invocation(int argc, const char **argv) +{ + struct commit *second_token = NULL; + if (argc > 1) { + unsigned char second_sha1[20]; + + if (get_sha1(argv[1], second_sha1)) + return NULL; + second_token = lookup_commit_reference_gently(second_sha1, 0); + if (!second_token) + die("'%s' is not a commit", argv[1]); + if (hashcmp(second_token->object.sha1, head)) + return NULL; + } + return second_token; +} + +static int evaluate_result(void) +{ + int cnt = 0; + struct rev_info rev; + + if (read_cache() < 0) + die("failed to read the cache"); + + /* Check how many files differ. */ + init_revisions(&rev, ""); + setup_revisions(0, NULL, &rev, NULL); + rev.diffopt.output_format |= + DIFF_FORMAT_CALLBACK; + rev.diffopt.format_callback = count_diff_files; + rev.diffopt.format_callback_data = &cnt; + run_diff_files(&rev, 0); + + /* + * Check how many unmerged entries are + * there. + */ + cnt += count_unmerged_entries(); + + return cnt; +} + +int cmd_merge(int argc, const char **argv, const char *prefix) +{ + unsigned char result_tree[20]; + struct strbuf buf; + const char *head_arg; + int flag, head_invalid = 0, i; + int best_cnt = -1, merge_was_ok = 0, automerge_was_ok = 0; + struct commit_list *common = NULL; + const char *best_strategy = NULL, *wt_strategy = NULL; + struct commit_list **remotes = &remoteheads; + + setup_work_tree(); + if (unmerged_cache()) + die("You are in the middle of a conflicted merge."); + + /* + * Check if we are _not_ on a detached HEAD, i.e. if there is a + * current branch. + */ + branch = resolve_ref("HEAD", head, 0, &flag); + if (branch && !prefixcmp(branch, "refs/heads/")) + branch += 11; + if (is_null_sha1(head)) + head_invalid = 1; + + git_config(git_merge_config, NULL); + + /* for color.ui */ + if (diff_use_color_default == -1) + diff_use_color_default = git_use_color_default; + + argc = parse_options(argc, argv, builtin_merge_options, + builtin_merge_usage, 0); + + if (squash) { + if (!allow_fast_forward) + die("You cannot combine --squash with --no-ff."); + option_commit = 0; + } + + if (!argc) + usage_with_options(builtin_merge_usage, + builtin_merge_options); + + /* + * This could be traditional "merge <msg> HEAD <commit>..." and + * the way we can tell it is to see if the second token is HEAD, + * but some people might have misused the interface and used a + * committish that is the same as HEAD there instead. + * Traditional format never would have "-m" so it is an + * additional safety measure to check for it. + */ + strbuf_init(&buf, 0); + + if (!have_message && is_old_style_invocation(argc, argv)) { + strbuf_addstr(&merge_msg, argv[0]); + head_arg = argv[1]; + argv += 2; + argc -= 2; + } else if (head_invalid) { + struct object *remote_head; + /* + * If the merged head is a valid one there is no reason + * to forbid "git merge" into a branch yet to be born. + * We do the same for "git pull". + */ + if (argc != 1) + die("Can merge only exactly one commit into " + "empty head"); + remote_head = peel_to_type(argv[0], 0, NULL, OBJ_COMMIT); + if (!remote_head) + die("%s - not something we can merge", argv[0]); + update_ref("initial pull", "HEAD", remote_head->sha1, NULL, 0, + DIE_ON_ERR); + reset_hard(remote_head->sha1, 0); + return 0; + } else { + struct strbuf msg; + + /* We are invoked directly as the first-class UI. */ + head_arg = "HEAD"; + + /* + * All the rest are the commits being merged; + * prepare the standard merge summary message to + * be appended to the given message. If remote + * is invalid we will die later in the common + * codepath so we discard the error in this + * loop. + */ + strbuf_init(&msg, 0); + for (i = 0; i < argc; i++) + merge_name(argv[i], &msg); + fmt_merge_msg(option_log, &msg, &merge_msg); + if (merge_msg.len) + strbuf_setlen(&merge_msg, merge_msg.len-1); + } + + if (head_invalid || !argc) + usage_with_options(builtin_merge_usage, + builtin_merge_options); + + strbuf_addstr(&buf, "merge"); + for (i = 0; i < argc; i++) + strbuf_addf(&buf, " %s", argv[i]); + setenv("GIT_REFLOG_ACTION", buf.buf, 0); + strbuf_reset(&buf); + + for (i = 0; i < argc; i++) { + struct object *o; + + o = peel_to_type(argv[i], 0, NULL, OBJ_COMMIT); + if (!o) + die("%s - not something we can merge", argv[i]); + remotes = &commit_list_insert(lookup_commit(o->sha1), + remotes)->next; + + strbuf_addf(&buf, "GITHEAD_%s", sha1_to_hex(o->sha1)); + setenv(buf.buf, argv[i], 1); + strbuf_reset(&buf); + } + + if (!use_strategies) { + if (!remoteheads->next) + add_strategies(pull_twohead, DEFAULT_TWOHEAD); + else + add_strategies(pull_octopus, DEFAULT_OCTOPUS); + } + + for (i = 0; i < use_strategies_nr; i++) { + if (use_strategies[i].attr & NO_FAST_FORWARD) + allow_fast_forward = 0; + if (use_strategies[i].attr & NO_TRIVIAL) + allow_trivial = 0; + } + + if (!remoteheads->next) + common = get_merge_bases(lookup_commit(head), + remoteheads->item, 1); + else { + struct commit_list *list = remoteheads; + commit_list_insert(lookup_commit(head), &list); + common = get_octopus_merge_bases(list); + free(list); + } + + update_ref("updating ORIG_HEAD", "ORIG_HEAD", head, NULL, 0, + DIE_ON_ERR); + + if (!common) + ; /* No common ancestors found. We need a real merge. */ + else if (!remoteheads->next && !common->next && + common->item == remoteheads->item) { + /* + * If head can reach all the merge then we are up to date. + * but first the most common case of merging one remote. + */ + finish_up_to_date("Already up-to-date."); + return 0; + } else if (allow_fast_forward && !remoteheads->next && + !common->next && + !hashcmp(common->item->object.sha1, head)) { + /* Again the most common case of merging one remote. */ + struct strbuf msg; + struct object *o; + char hex[41]; + + strcpy(hex, find_unique_abbrev(head, DEFAULT_ABBREV)); + + printf("Updating %s..%s\n", + hex, + find_unique_abbrev(remoteheads->item->object.sha1, + DEFAULT_ABBREV)); + refresh_cache(REFRESH_QUIET); + strbuf_init(&msg, 0); + strbuf_addstr(&msg, "Fast forward"); + if (have_message) + strbuf_addstr(&msg, + " (no commit created; -m option ignored)"); + o = peel_to_type(sha1_to_hex(remoteheads->item->object.sha1), + 0, NULL, OBJ_COMMIT); + if (!o) + return 1; + + if (checkout_fast_forward(head, remoteheads->item->object.sha1)) + return 1; + + finish(o->sha1, msg.buf); + drop_save(); + return 0; + } else if (!remoteheads->next && common->next) + ; + /* + * We are not doing octopus and not fast forward. Need + * a real merge. + */ + else if (!remoteheads->next && !common->next && option_commit) { + /* + * We are not doing octopus, not fast forward, and have + * only one common. + */ + refresh_cache(REFRESH_QUIET); + if (allow_trivial) { + /* See if it is really trivial. */ + git_committer_info(IDENT_ERROR_ON_NO_NAME); + printf("Trying really trivial in-index merge...\n"); + if (!read_tree_trivial(common->item->object.sha1, + head, remoteheads->item->object.sha1)) + return merge_trivial(); + printf("Nope.\n"); + } + } else { + /* + * An octopus. If we can reach all the remote we are up + * to date. + */ + int up_to_date = 1; + struct commit_list *j; + + for (j = remoteheads; j; j = j->next) { + struct commit_list *common_one; + + /* + * Here we *have* to calculate the individual + * merge_bases again, otherwise "git merge HEAD^ + * HEAD^^" would be missed. + */ + common_one = get_merge_bases(lookup_commit(head), + j->item, 1); + if (hashcmp(common_one->item->object.sha1, + j->item->object.sha1)) { + up_to_date = 0; + break; + } + } + if (up_to_date) { + finish_up_to_date("Already up-to-date. Yeeah!"); + return 0; + } + } + + /* We are going to make a new commit. */ + git_committer_info(IDENT_ERROR_ON_NO_NAME); + + /* + * At this point, we need a real merge. No matter what strategy + * we use, it would operate on the index, possibly affecting the + * working tree, and when resolved cleanly, have the desired + * tree in the index -- this means that the index must be in + * sync with the head commit. The strategies are responsible + * to ensure this. + */ + if (use_strategies_nr != 1) { + /* + * Stash away the local changes so that we can try more + * than one. + */ + save_state(); + } else { + memcpy(stash, null_sha1, 20); + } + + for (i = 0; i < use_strategies_nr; i++) { + int ret; + if (i) { + printf("Rewinding the tree to pristine...\n"); + restore_state(); + } + if (use_strategies_nr != 1) + printf("Trying merge strategy %s...\n", + use_strategies[i].name); + /* + * Remember which strategy left the state in the working + * tree. + */ + wt_strategy = use_strategies[i].name; + + ret = try_merge_strategy(use_strategies[i].name, + common, head_arg); + if (!option_commit && !ret) { + merge_was_ok = 1; + /* + * This is necessary here just to avoid writing + * the tree, but later we will *not* exit with + * status code 1 because merge_was_ok is set. + */ + ret = 1; + } + + if (ret) { + /* + * The backend exits with 1 when conflicts are + * left to be resolved, with 2 when it does not + * handle the given merge at all. + */ + if (ret == 1) { + int cnt = evaluate_result(); + + if (best_cnt <= 0 || cnt <= best_cnt) { + best_strategy = use_strategies[i].name; + best_cnt = cnt; + } + } + if (merge_was_ok) + break; + else + continue; + } + + /* Automerge succeeded. */ + write_tree_trivial(result_tree); + automerge_was_ok = 1; + break; + } + + /* + * If we have a resulting tree, that means the strategy module + * auto resolved the merge cleanly. + */ + if (automerge_was_ok) + return finish_automerge(common, result_tree, wt_strategy); + + /* + * Pick the result from the best strategy and have the user fix + * it up. + */ + if (!best_strategy) { + restore_state(); + if (use_strategies_nr > 1) + fprintf(stderr, + "No merge strategy handled the merge.\n"); + else + fprintf(stderr, "Merge with strategy %s failed.\n", + use_strategies[0].name); + return 2; + } else if (best_strategy == wt_strategy) + ; /* We already have its result in the working tree. */ + else { + printf("Rewinding the tree to pristine...\n"); + restore_state(); + printf("Using the %s to prepare resolving by hand.\n", + best_strategy); + try_merge_strategy(best_strategy, common, head_arg); + } + + if (squash) + finish(NULL, NULL); + else { + int fd; + struct commit_list *j; + + for (j = remoteheads; j; j = j->next) + strbuf_addf(&buf, "%s\n", + sha1_to_hex(j->item->object.sha1)); + fd = open(git_path("MERGE_HEAD"), O_WRONLY | O_CREAT, 0666); + if (fd < 0) + die("Could open %s for writing", + git_path("MERGE_HEAD")); + if (write_in_full(fd, buf.buf, buf.len) != buf.len) + die("Could not write to %s", git_path("MERGE_HEAD")); + close(fd); + strbuf_addch(&merge_msg, '\n'); + fd = open(git_path("MERGE_MSG"), O_WRONLY | O_CREAT, 0666); + if (fd < 0) + die("Could open %s for writing", git_path("MERGE_MSG")); + if (write_in_full(fd, merge_msg.buf, merge_msg.len) != + merge_msg.len) + die("Could not write to %s", git_path("MERGE_MSG")); + close(fd); + } + + if (merge_was_ok) { + fprintf(stderr, "Automatic merge went well; " + "stopped before committing as requested\n"); + return 0; + } else + return suggest_conflicts(); +} diff --git a/builtin.h b/builtin.h index 05ee56f..0e605d4 100644 --- a/builtin.h +++ b/builtin.h @@ -64,6 +64,7 @@ extern int cmd_ls_tree(int argc, const char **argv, const char *prefix); extern int cmd_ls_remote(int argc, const char **argv, const char *prefix); extern int cmd_mailinfo(int argc, const char **argv, const char *prefix); extern int cmd_mailsplit(int argc, const char **argv, const char *prefix); +extern int cmd_merge(int argc, const char **argv, const char *prefix); extern int cmd_merge_base(int argc, const char **argv, const char *prefix); extern int cmd_merge_ours(int argc, const char **argv, const char *prefix); extern int cmd_merge_file(int argc, const char **argv, const char *prefix); diff --git a/git-merge.sh b/contrib/examples/git-merge.sh similarity index 100% rename from git-merge.sh rename to contrib/examples/git-merge.sh diff --git a/git.c b/git.c index 2fbe96b..770aadd 100644 --- a/git.c +++ b/git.c @@ -271,6 +271,7 @@ static void handle_internal_command(int argc, const char **argv) { "ls-remote", cmd_ls_remote }, { "mailinfo", cmd_mailinfo }, { "mailsplit", cmd_mailsplit }, + { "merge", cmd_merge, RUN_SETUP | NEED_WORK_TREE }, { "merge-base", cmd_merge_base, RUN_SETUP }, { "merge-file", cmd_merge_file }, { "merge-ours", cmd_merge_ours, RUN_SETUP }, diff --git a/t/t7602-merge-octopus-many.sh b/t/t7602-merge-octopus-many.sh index f3a4bb2..fcb8285 100755 --- a/t/t7602-merge-octopus-many.sh +++ b/t/t7602-merge-octopus-many.sh @@ -23,7 +23,7 @@ test_expect_success 'setup' ' done ' -test_expect_failure 'merge c1 with c2, c3, c4, ... c29' ' +test_expect_success 'merge c1 with c2, c3, c4, ... c29' ' git reset --hard c1 && i=2 && refs="" && -- 1.5.6.1.322.ge904b.dirty ^ permalink raw reply related [flat|nested] 82+ messages in thread
* Re: [PATCH] Build in merge 2008-07-07 17:24 ` [PATCH] " Miklos Vajna @ 2008-07-07 17:35 ` Miklos Vajna 0 siblings, 0 replies; 82+ messages in thread From: Miklos Vajna @ 2008-07-07 17:35 UTC (permalink / raw To: Junio C Hamano; +Cc: git, Johannes Schindelin, Olivier Marin [-- Attachment #1: Type: text/plain, Size: 307 bytes --] On Mon, Jul 07, 2008 at 07:24:20PM +0200, Miklos Vajna <vmiklos@frugalware.org> wrote: > Here is an updated version without using path_list at all. Sorry forgot the interdiff and the log: - interdiff: git diff 2ed1884..10d5724 - log: git log c255d12 (1 commit) $ git grep -c path.list builtin-merge.c 0 [-- Attachment #2: Type: application/pgp-signature, Size: 197 bytes --] ^ permalink raw reply [flat|nested] 82+ messages in thread
* Re: [PATCH 14/14] Build in merge 2008-07-01 2:37 ` [PATCH 14/14] Build in merge Miklos Vajna 2008-07-01 6:23 ` Junio C Hamano @ 2008-07-01 7:27 ` Junio C Hamano 2008-07-01 12:55 ` Miklos Vajna 1 sibling, 1 reply; 82+ messages in thread From: Junio C Hamano @ 2008-07-01 7:27 UTC (permalink / raw To: Miklos Vajna; +Cc: git, Johannes Schindelin, Olivier Marin Miklos Vajna <vmiklos@frugalware.org> writes: > + /* See if remote matches <name>~<number>, or <name>^ */ > + ptr = strrchr(remote, '^'); > + if (ptr && ptr[1] == '\0') { > + for (len = 0, ptr = remote + strlen(remote); > + remote < ptr && ptr[-1] == '^'; > + ptr--) > + len++; > + } > + else { > + ptr = strrchr(remote, '~'); > + if (ptr && ptr[1] != '0' && isdigit(ptr[1])) { > + len = ptr-remote; > + ptr++; > + for (ptr++; *ptr; ptr++) > + if (!isdigit(*ptr)) { > + len = 0; > + break; > + } > + } > + } I still have problems with the above. I'd write it this way: int len, early; ... /* See if remote matches <name>^^^.. or <name>~<number> */ for (len = 0, ptr = remote + strlen(remote); remote < ptr && ptr[-1] == '^'; ptr--) len++; if (len) early = 1; else { early = 0; ptr = strrchr(remote, '~'); if (ptr) { int seen_nonzero = 0; len++; /* count ~ */ while (*++ptr && isdigit(*ptr)) { seen_nonzero |= (*ptr != '0'); len++; } if (*ptr) len = 0; /* not ...~<number> */ else if (seen_nonzero) early = 1; else if (len == 1) early = 1; /* "name~" is "name~1"! */ } } if (len) { struct strbuf truname = STRBUF_INIT; strbuf_addstr(&truname, "refs/heads/"); strbuf_addstr(&truname, remote); strbuf_setlen(&truname, len+11); if (resolve_ref(truname.buf, buf_sha, 0, 0)) { strbuf_addf(msg, "%s\t\tbranch '%s'%s of .\n", sha1_to_hex(remote_head->sha1), truname.buf, (early ? " (early part)" : "")); return; } } The first loop is obvious. If the tail end is ^, we set "len" and see if the remainder is a branch name (and if that is the case we are always talking about an early part of it of the branch). Otherwise, we do want to say "early part" if "$name~<number>" is given, and another special case is "$name~" which is "$name~1" these days. As long as number is not zero we would want to say "early part". Otherwise we would want to say it is a branch itself, not its early part. I'll queue the fixed-up result in 'pu', but I have to tend to other topics before I can actually publish. Together with the fix to "head_invalid" confusion I mentioned in another message squashed in to this commit, all the tests now finally seem to pass on the topic branch. Oh, by the way, you sent this and the previous round without marking them as RFC nor WIP, even though they obviously did not even pass the test suite. For example, without the head_invalid fix, anything that runs merge on detached head, most notably "git rebase -i", would not work at all. ^ permalink raw reply [flat|nested] 82+ messages in thread
* Re: [PATCH 14/14] Build in merge 2008-07-01 7:27 ` [PATCH 14/14] " Junio C Hamano @ 2008-07-01 12:55 ` Miklos Vajna 0 siblings, 0 replies; 82+ messages in thread From: Miklos Vajna @ 2008-07-01 12:55 UTC (permalink / raw To: Junio C Hamano; +Cc: git, Johannes Schindelin, Olivier Marin [-- Attachment #1: Type: text/plain, Size: 991 bytes --] On Tue, Jul 01, 2008 at 12:27:48AM -0700, Junio C Hamano <gitster@pobox.com> wrote: > I'll queue the fixed-up result in 'pu', but I have to tend to other topics > before I can actually publish. Together with the fix to "head_invalid" > confusion I mentioned in another message squashed in to this commit, all > the tests now finally seem to pass on the topic branch. > > Oh, by the way, you sent this and the previous round without marking them > as RFC nor WIP, even though they obviously did not even pass the test > suite. For example, without the head_invalid fix, anything that runs > merge on detached head, most notably "git rebase -i", would not work at > all. Thanks for pointing that out. I remember I used to run 'make test' before sending a patch, but that took a lot of time and then I used to run only t*merge*.sh, which - it turns out - was a bad idea, since I haven't noticed breaking t3404. I'm now running a full 'make test' before I send the patch. [-- Attachment #2: Type: application/pgp-signature, Size: 197 bytes --] ^ permalink raw reply [flat|nested] 82+ messages in thread
* Re: [PATCH 13/13] Build in merge 2008-06-30 1:39 ` Miklos Vajna 2008-06-30 5:44 ` Junio C Hamano @ 2008-06-30 22:44 ` Olivier Marin 2008-06-30 22:58 ` Miklos Vajna 1 sibling, 1 reply; 82+ messages in thread From: Olivier Marin @ 2008-06-30 22:44 UTC (permalink / raw To: Miklos Vajna; +Cc: Junio C Hamano, git, Johannes Schindelin Miklos Vajna a écrit : > > +static void finish(const unsigned char *new_head, const char *msg) > +{ [...] > + if (new_head && show_diffstat) { > + struct diff_options opts; > + diff_setup(&opts); > + opts.output_format |= > + DIFF_FORMAT_SUMMARY | DIFF_FORMAT_DIFFSTAT; > + opts.detect_rename = DIFF_DETECT_RENAME; > + if (diff_use_color_default > 0) > + DIFF_OPT_SET(&opts, COLOR_DIFF); Ah, I missed that. You should call diff_setup_done(), to finish diff_setup() and have a recursive diffstat. For example: $ git checkout -b test c0f5c69 $ git merge 2dce956 Updating c0f5c69..2dce956 Fast forward help.c | 22 ++++++++++++++-------- 1 files changed, 14 insertions(+), 8 deletions(-) is wrong. Correct diffstat is: $ git diff --stat c0f5c69..2dce956 Documentation/gitcli.txt | 37 +++++++++++++++++++++++++++++++++---- Documentation/gittutorial-2.txt | 10 +++++----- help.c | 22 ++++++++++++++-------- t/test-lib.sh | 2 +- 4 files changed, 53 insertions(+), 18 deletions(-) > + diff_tree_sha1(head, new_head, "", &opts); > + diffcore_std(&opts); > + diff_flush(&opts); > + } [...] > +int cmd_merge(int argc, const char **argv, const char *prefix) > +{ [...] > + git_config(git_merge_config, NULL); > + > + /* for color.diff and diff.color */ > + git_config(git_diff_ui_config, NULL); Also, what about doing "return git_diff_ui_config(k, v, cb)" at the end of git_merge_config() instead, to avoid parsing config files twice. > + > + /* for color.ui */ > + if (diff_use_color_default == -1) > + diff_use_color_default = git_use_color_default; > + Olivier. ^ permalink raw reply [flat|nested] 82+ messages in thread
* Re: [PATCH 13/13] Build in merge 2008-06-30 22:44 ` [PATCH 13/13] " Olivier Marin @ 2008-06-30 22:58 ` Miklos Vajna 0 siblings, 0 replies; 82+ messages in thread From: Miklos Vajna @ 2008-06-30 22:58 UTC (permalink / raw To: Olivier Marin; +Cc: Junio C Hamano, git, Johannes Schindelin [-- Attachment #1: Type: text/plain, Size: 1474 bytes --] On Tue, Jul 01, 2008 at 12:44:23AM +0200, Olivier Marin <dkr+ml.git@free.fr> wrote: > > + if (new_head && show_diffstat) { > > + struct diff_options opts; > > + diff_setup(&opts); > > + opts.output_format |= > > + DIFF_FORMAT_SUMMARY | DIFF_FORMAT_DIFFSTAT; > > + opts.detect_rename = DIFF_DETECT_RENAME; > > + if (diff_use_color_default > 0) > > + DIFF_OPT_SET(&opts, COLOR_DIFF); > > Ah, I missed that. You should call diff_setup_done(), to finish diff_setup() > and have a recursive diffstat. > > For example: > > $ git checkout -b test c0f5c69 > $ git merge 2dce956 > Updating c0f5c69..2dce956 > Fast forward > help.c | 22 ++++++++++++++-------- > 1 files changed, 14 insertions(+), 8 deletions(-) > > is wrong. Correct diffstat is: > > $ git diff --stat c0f5c69..2dce956 > Documentation/gitcli.txt | 37 +++++++++++++++++++++++++++++++++---- > Documentation/gittutorial-2.txt | 10 +++++----- > help.c | 22 ++++++++++++++-------- > t/test-lib.sh | 2 +- > 4 files changed, 53 insertions(+), 18 deletions(-) Fixed in my working branch. > > + git_config(git_merge_config, NULL); > > + > > + /* for color.diff and diff.color */ > > + git_config(git_diff_ui_config, NULL); > > Also, what about doing "return git_diff_ui_config(k, v, cb)" at the end of > git_merge_config() instead, to avoid parsing config files twice. Sure, changed. Thanks! :) [-- Attachment #2: Type: application/pgp-signature, Size: 197 bytes --] ^ permalink raw reply [flat|nested] 82+ messages in thread
* Re: [PATCH 13/13] Build in merge 2008-06-30 1:36 ` Miklos Vajna 2008-06-30 1:39 ` Miklos Vajna @ 2008-06-30 5:40 ` Junio C Hamano 2008-06-30 22:48 ` Miklos Vajna 1 sibling, 1 reply; 82+ messages in thread From: Junio C Hamano @ 2008-06-30 5:40 UTC (permalink / raw To: Miklos Vajna; +Cc: git, Johannes Schindelin, Olivier Marin Miklos Vajna <vmiklos@frugalware.org> writes: >> > + if (!remote_head) >> > + die("%s - not something we can merge", argv[0]); >> > + update_ref("initial pull", "HEAD", remote_head->sha1, NULL, 0, >> > + DIE_ON_ERR); >> > + reset_hard(remote_head->sha1, 0); >> > + return 0; >> >> Makes one wonder reset_hard() (aka "read-tree --reset -u HEAD") ever fail >> and return here (iow, without calling die()). The answer is luckily no >> in this case, but it is somewhat unnerving to reviewers. > > Actually reset_hard does not return if an error occures: I know that; didn't I already say "Luckily no"? The point was it was not apparent from the above 6 lines alone. >> > + for (i = 0; i < use_strategies.nr; i++) { >> > + if ((unsigned int)use_strategies.items[i].util & >> > + NO_FAST_FORWARD) >> > + allow_fast_forward = 0; >> > + if ((unsigned int)use_strategies.items[i].util & NO_TRIVIAL) >> > + allow_trivial = 0; >> >> Can we abstract out these ugly casts? Any code that use path_list to >> store anything but list of paths (i.e. some value keyed with string) tends >> to have this readability issue. > > If you don't cast, you can't use the & operator. If I change the > path_list_item's util to be an unsigned number then I break fast-export. > I think if we _really_ want to get rid of those casts, we could have > something like: No, no, no. That is not what I meant. The places that use use_strategies in your code knows too much about the internal implementation detail of path_list, while path_list pretends to be a general purpose "table keyed with string" facility. The fact is that the table is not a very useful general purpose abstraction unless you are pointing at some structures that exist regardless of your use of path_list (e.g. you have some "struct object" and you hold pointers in a path_list). It does not work very well as an abstraction for use case like yours. With something like: static inline unsigned nth_strategy_flags(struct path_list *s, int nth) { return (unsigned) s->items[nth].util; } the checks would be more like: if (nth_strategy_flags(&use_strategies, i) & NO_FAST_FORWARD) ... or even: static inline check_nth_strategy_flags(struct path_list_item *i, unsigned flags) { return !((unsigned) i->util & flags); } if (check_nth_strategy_flags(&use_strategies,items[i], NO_FAST_FORWARD) ... either of which would be much easier on the eye. > Actually the shell version did not check here, either, but yes, I would > have to. Now I do. The scripted did not have to do so explicitly, as the one before that have already caught "more than one common" case in the case arm before this part. It is already known that "have only one common" condition is true when you reached the corresponding part of the scripted version. ^ permalink raw reply [flat|nested] 82+ messages in thread
* Re: [PATCH 13/13] Build in merge 2008-06-30 5:40 ` Junio C Hamano @ 2008-06-30 22:48 ` Miklos Vajna 0 siblings, 0 replies; 82+ messages in thread From: Miklos Vajna @ 2008-06-30 22:48 UTC (permalink / raw To: Junio C Hamano; +Cc: git, Johannes Schindelin, Olivier Marin [-- Attachment #1: Type: text/plain, Size: 2370 bytes --] On Sun, Jun 29, 2008 at 10:40:53PM -0700, Junio C Hamano <gitster@pobox.com> wrote: > > Actually reset_hard does not return if an error occures: > > I know that; didn't I already say "Luckily no"? The point was it was not > apparent from the above 6 lines alone. Ah, OK. > > >> > + for (i = 0; i < use_strategies.nr; i++) { > >> > + if ((unsigned int)use_strategies.items[i].util & > >> > + NO_FAST_FORWARD) > >> > + allow_fast_forward = 0; > >> > + if ((unsigned int)use_strategies.items[i].util & NO_TRIVIAL) > >> > + allow_trivial = 0; > >> > >> Can we abstract out these ugly casts? Any code that use path_list to > >> store anything but list of paths (i.e. some value keyed with string) tends > >> to have this readability issue. > > > > If you don't cast, you can't use the & operator. If I change the > > path_list_item's util to be an unsigned number then I break fast-export. > > I think if we _really_ want to get rid of those casts, we could have > > something like: > > No, no, no. That is not what I meant. > > The places that use use_strategies in your code knows too much about the > internal implementation detail of path_list, while path_list pretends to > be a general purpose "table keyed with string" facility. The fact is that > the table is not a very useful general purpose abstraction unless you are > pointing at some structures that exist regardless of your use of path_list > (e.g. you have some "struct object" and you hold pointers in a path_list). > It does not work very well as an abstraction for use case like yours. > > With something like: > > static inline unsigned nth_strategy_flags(struct path_list *s, int nth) > { > return (unsigned) s->items[nth].util; > } > > the checks would be more like: > > if (nth_strategy_flags(&use_strategies, i) & NO_FAST_FORWARD) > ... > > or even: > > static inline check_nth_strategy_flags(struct path_list_item *i, unsigned flags) > { > return !((unsigned) i->util & flags); > } > > if (check_nth_strategy_flags(&use_strategies,items[i], NO_FAST_FORWARD) > ... > > either of which would be much easier on the eye. Probably this is subjective, but I think the previous form is easier to read, so I choose that one. [-- Attachment #2: Type: application/pgp-signature, Size: 197 bytes --] ^ permalink raw reply [flat|nested] 82+ messages in thread
* Re: [PATCH 11/15] Add strbuf_vaddf(), use it in strbuf_addf(), and add strbuf_initf() 2008-06-28 2:00 ` [PATCH 11/15] Add strbuf_vaddf(), use it in strbuf_addf(), and add strbuf_initf() Junio C Hamano 2008-06-28 2:33 ` Miklos Vajna @ 2008-06-28 17:33 ` Johannes Schindelin 2008-06-29 8:07 ` Junio C Hamano 1 sibling, 1 reply; 82+ messages in thread From: Johannes Schindelin @ 2008-06-28 17:33 UTC (permalink / raw To: Junio C Hamano; +Cc: Miklos Vajna, git, Olivier Marin Hi, On Fri, 27 Jun 2008, Junio C Hamano wrote: > Do you really think these two patches belong to the series, I seriously > have to wonder? Heh. I suggested it, as I thought this was a perfect excuse to introduce strbuf_initf(), which has been on my wishlist since long. Anyway, I'm fine with having them only in my personal fork. Ciao, Dscho ^ permalink raw reply [flat|nested] 82+ messages in thread
* Re: [PATCH 11/15] Add strbuf_vaddf(), use it in strbuf_addf(), and add strbuf_initf() 2008-06-28 17:33 ` [PATCH 11/15] Add strbuf_vaddf(), use it in strbuf_addf(), and add strbuf_initf() Johannes Schindelin @ 2008-06-29 8:07 ` Junio C Hamano 2008-06-29 13:40 ` Johannes Schindelin 0 siblings, 1 reply; 82+ messages in thread From: Junio C Hamano @ 2008-06-29 8:07 UTC (permalink / raw To: Johannes Schindelin; +Cc: Miklos Vajna, git, Olivier Marin Johannes Schindelin <Johannes.Schindelin@gmx.de> writes: > Heh. I suggested it, as I thought this was a perfect excuse to introduce > strbuf_initf(), which has been on my wishlist since long. We may want to consider the change for its own merit, if there is a reason for the vaddf patch, such as "this is needed because platform X and Y do not have working vsnprintf()". Perhaps we add compat/* something. > Anyway, I'm fine with having them only in my personal fork. It will cost you some "trust point" next time you try to sneak something in as a part of a largely unrelated topic. Please don't. ^ permalink raw reply [flat|nested] 82+ messages in thread
* Re: [PATCH 11/15] Add strbuf_vaddf(), use it in strbuf_addf(), and add strbuf_initf() 2008-06-29 8:07 ` Junio C Hamano @ 2008-06-29 13:40 ` Johannes Schindelin 2008-06-29 20:17 ` Alex Riesen 0 siblings, 1 reply; 82+ messages in thread From: Johannes Schindelin @ 2008-06-29 13:40 UTC (permalink / raw To: Junio C Hamano; +Cc: Miklos Vajna, git, Olivier Marin Hi, On Sun, 29 Jun 2008, Junio C Hamano wrote: > Johannes Schindelin <Johannes.Schindelin@gmx.de> writes: > > > Anyway, I'm fine with having them only in my personal fork. > > It will cost you some "trust point" next time you try to sneak something > in as a part of a largely unrelated topic. Please don't. Fine. Fine! I thought it made the code more readable, and that it also might have helped other sites where you have to strbuf_init() first, and often need curly brackets only because of that. Making for ultra-ugly code. Please note that I also added a simple test case for the vaddf() function, so your "trying to sneak through" is only half as bad as you made it sound. Also, please note that the situation with SNPRINTF_RETURNS_BOGUS works, but is a thorn in this developers eye. Anyway, I will not try to post this patch again, Dscho ^ permalink raw reply [flat|nested] 82+ messages in thread
* Re: [PATCH 11/15] Add strbuf_vaddf(), use it in strbuf_addf(), and add strbuf_initf() 2008-06-29 13:40 ` Johannes Schindelin @ 2008-06-29 20:17 ` Alex Riesen 2008-06-29 20:24 ` Junio C Hamano 0 siblings, 1 reply; 82+ messages in thread From: Alex Riesen @ 2008-06-29 20:17 UTC (permalink / raw To: Johannes Schindelin; +Cc: Junio C Hamano, Miklos Vajna, git, Olivier Marin Johannes Schindelin, Sun, Jun 29, 2008 15:40:57 +0200: > On Sun, 29 Jun 2008, Junio C Hamano wrote: > > Johannes Schindelin <Johannes.Schindelin@gmx.de> writes: > > > > > Anyway, I'm fine with having them only in my personal fork. > > > > It will cost you some "trust point" next time you try to sneak something > > in as a part of a largely unrelated topic. Please don't. > > Fine. Fine! > See the positive side: you just won a "please review me carefully" point. These have no negative side effects, usually. ^ permalink raw reply [flat|nested] 82+ messages in thread
* Re: [PATCH 11/15] Add strbuf_vaddf(), use it in strbuf_addf(), and add strbuf_initf() 2008-06-29 20:17 ` Alex Riesen @ 2008-06-29 20:24 ` Junio C Hamano 2008-06-29 20:30 ` Sverre Rabbelier 0 siblings, 1 reply; 82+ messages in thread From: Junio C Hamano @ 2008-06-29 20:24 UTC (permalink / raw To: Alex Riesen; +Cc: Johannes Schindelin, Miklos Vajna, git, Olivier Marin Alex Riesen <raa.lkml@gmail.com> writes: > Johannes Schindelin, Sun, Jun 29, 2008 15:40:57 +0200: >> On Sun, 29 Jun 2008, Junio C Hamano wrote: >> > Johannes Schindelin <Johannes.Schindelin@gmx.de> writes: >> > >> > > Anyway, I'm fine with having them only in my personal fork. >> > >> > It will cost you some "trust point" next time you try to sneak something >> > in as a part of a largely unrelated topic. Please don't. >> >> Fine. Fine! > > See the positive side: you just won a "please review me carefully" > point. These have no negative side effects, usually. Heh, he hasn't _yet_. I said "next time" didn't I? ^ permalink raw reply [flat|nested] 82+ messages in thread
* Re: [PATCH 11/15] Add strbuf_vaddf(), use it in strbuf_addf(), and add strbuf_initf() 2008-06-29 20:24 ` Junio C Hamano @ 2008-06-29 20:30 ` Sverre Rabbelier 0 siblings, 0 replies; 82+ messages in thread From: Sverre Rabbelier @ 2008-06-29 20:30 UTC (permalink / raw To: Junio C Hamano Cc: Alex Riesen, Johannes Schindelin, Miklos Vajna, git, Olivier Marin On Sun, Jun 29, 2008 at 10:24 PM, Junio C Hamano <gitster@pobox.com> wrote: > Heh, he hasn't _yet_. I said "next time" didn't I? Now you sound like the bad guy from "Inspector Gadget" :P. "Next time Dscho, next time!!!" -- Cheers, Sverre Rabbelier ^ permalink raw reply [flat|nested] 82+ messages in thread
* [PATCH 08/15] Add new test to ensure git-merge handles more than 25 refs. 2008-06-27 16:22 ` [PATCH 08/15] Add new test to ensure git-merge handles more than 25 refs Miklos Vajna 2008-06-27 16:22 ` [PATCH 09/15] Introduce get_merge_bases_many() Miklos Vajna @ 2008-06-27 17:06 ` Miklos Vajna 1 sibling, 0 replies; 82+ messages in thread From: Miklos Vajna @ 2008-06-27 17:06 UTC (permalink / raw To: Junio C Hamano; +Cc: git, Johannes Schindelin, Olivier Marin The old shell version handled only 25 refs but we no longer have this limitation. Add a test to make sure this limitation will not be introduced again in the future. Signed-off-by: Miklos Vajna <vmiklos@frugalware.org> --- On Fri, Jun 27, 2008 at 06:22:01PM +0200, Miklos Vajna <vmiklos@frugalware.org> wrote: > +test_expect_failure 'merge c1 with c2, c3, c4, ... c29' ' I just noticed this was wrong here. The tests passed because a later commit had a hunk to fix this but this fix is unrelated there. So here is the right version. t/t7602-merge-octopus-many.sh | 52 +++++++++++++++++++++++++++++++++++++++++ 1 files changed, 52 insertions(+), 0 deletions(-) create mode 100755 t/t7602-merge-octopus-many.sh diff --git a/t/t7602-merge-octopus-many.sh b/t/t7602-merge-octopus-many.sh new file mode 100755 index 0000000..fcb8285 --- /dev/null +++ b/t/t7602-merge-octopus-many.sh @@ -0,0 +1,52 @@ +#!/bin/sh + +test_description='git-merge + +Testing octopus merge with more than 25 refs.' + +. ./test-lib.sh + +test_expect_success 'setup' ' + echo c0 > c0.c && + git add c0.c && + git commit -m c0 && + git tag c0 && + i=1 && + while test $i -le 30 + do + git reset --hard c0 && + echo c$i > c$i.c && + git add c$i.c && + git commit -m c$i && + git tag c$i && + i=`expr $i + 1` || return 1 + done +' + +test_expect_success 'merge c1 with c2, c3, c4, ... c29' ' + git reset --hard c1 && + i=2 && + refs="" && + while test $i -le 30 + do + refs="$refs c$i" + i=`expr $i + 1` + done + git merge $refs && + test "$(git rev-parse c1)" != "$(git rev-parse HEAD)" && + i=1 && + while test $i -le 30 + do + test "$(git rev-parse c$i)" = "$(git rev-parse HEAD^$i)" && + i=`expr $i + 1` || return 1 + done && + git diff --exit-code && + i=1 && + while test $i -le 30 + do + test -f c$i.c && + i=`expr $i + 1` || return 1 + done +' + +test_done -- 1.5.6 ^ permalink raw reply related [flat|nested] 82+ messages in thread
* Re: [PATCH 04/15] Add new test to ensure git-merge handles pull.twohead and pull.octopus 2008-06-27 16:21 ` [PATCH 04/15] Add new test to ensure git-merge handles pull.twohead and pull.octopus Miklos Vajna 2008-06-27 16:21 ` [PATCH 05/15] Move read_cache_unmerged() to read-cache.c Miklos Vajna @ 2008-06-29 13:30 ` Olivier Marin 2008-06-29 14:51 ` [PATCH] " Miklos Vajna 2008-07-04 16:34 ` [PATCH 04/15] " Mike Ralphson 2 siblings, 1 reply; 82+ messages in thread From: Olivier Marin @ 2008-06-29 13:30 UTC (permalink / raw To: Miklos Vajna; +Cc: Junio C Hamano, git, Johannes Schindelin, Olivier Marin Miklos Vajna a écrit : > [...] > +test_expect_success 'setup' ' > + echo c0 >c0.c && > + git add c0.c && > + git commit -m c0 && > + git tag c0 && > + echo c1 >c1.c && > + git add c1.c && > + git commit -m c1 && > + git tag c1 && > + git reset --hard c0 && > + echo c2 >c2.c && > + git add c2.c && > + git commit -m c2 && > + git tag c2 Missing &&? > + git reset --hard c0 && > + echo c3 >c3.c && > + git add c3.c && > + git commit -m c3 && > + git tag c3 > +' [...] > +test_expect_success 'merge c1 with c2 and c3 (recursive and octopus in pull.octopus)' ' > + git reset --hard c1 && > + git config pull.octopus "recursive octopus" && > + git merge c2 c3 && > + test "$(git rev-parse c1)" != "$(git rev-parse HEAD)" && > + test "$(git rev-parse c1)" = "$(git rev-parse HEAD^1)" && > + test "$(git rev-parse c2)" = "$(git rev-parse HEAD^2)" && > + test "$(git rev-parse c3)" = "$(git rev-parse HEAD^3)" Duplicate the next line? > + test "$(git rev-parse c3)" = "$(git rev-parse HEAD^3)" && > + git diff --exit-code && > + test -f c0.c && > + test -f c1.c && > + test -f c2.c && > + test -f c3.c > +' > + > +conflict_count() > +{ > + eval $1=`{ > + git diff-files --name-only > + git ls-files --unmerged > + } | wc -l` > +} > + > +# c4 - c5 > +# \ c6 > +# > +# There are two conflicts here: > +# > +# 1) Because foo.c is renamed to bar.c, recursive will handle this, > +# resolve won't. > +# > +# 2) One in conflict.c and that will always fail. > + > +test_expect_success 'setup conflicted merge' ' > + git reset --hard c0 && > + echo A >conflict.c && > + git add conflict.c && > + echo contents >foo.c && > + git add foo.c && > + git commit -m c4 && > + git tag c4 && > + echo B >conflict.c && > + git add conflict.c && > + git mv foo.c bar.c && > + git commit -m c5 && > + git tag c5 && > + git reset --hard c4 && > + echo C >conflict.c && > + git add conflict.c && > + echo secondline >> foo.c && > + git add foo.c && > + git commit -m c6 && > + git tag c6 > +' > + > +# First do the merge with resolve and recursive then verify that > +# recusive is choosen. > + > +test_expect_success 'merge picks up the best result' ' > + git config pull.twohead "recursive resolve" && > + git reset --hard c5 && > + git merge -s resolve c6 > + conflict_count resolve_count && > + git reset --hard c5 && > + git merge -s recursive c6 > + conflict_count recursive_count && > + git reset --hard c5 && > + git merge c6 > + conflict_count auto_count && > + test "$auto_count" = "$recursive_count" > +' Should not "$auto_count" != "$resolve_count" also be tested to be sure that recursive has been choosen? Olivier. ^ permalink raw reply [flat|nested] 82+ messages in thread
* [PATCH] Add new test to ensure git-merge handles pull.twohead and pull.octopus 2008-06-29 13:30 ` [PATCH 04/15] Add new test to ensure git-merge handles pull.twohead and pull.octopus Olivier Marin @ 2008-06-29 14:51 ` Miklos Vajna 2008-06-29 15:11 ` Miklos Vajna 0 siblings, 1 reply; 82+ messages in thread From: Miklos Vajna @ 2008-06-29 14:51 UTC (permalink / raw To: Olivier Marin; +Cc: Junio C Hamano, git, Johannes Schindelin Test if the given strategies are used and test the case when multiple strategies are configured using a space separated list. Also test if the best strategy is picked if none is specified. This is done by adding a simple test case where recursive detects a rename, but resolve does not, and verify that finally merge will pick up the previous. Signed-off-by: Miklos Vajna <vmiklos@frugalware.org> Signed-off-by: Junio C Hamano <gitster@pobox.com> --- On Sun, Jun 29, 2008 at 03:30:56PM +0200, Olivier Marin <dkr+ml.git@free.fr> wrote: > > + git tag c2 > > Missing &&? Fixed. > > + test "$(git rev-parse c3)" = "$(git rev-parse HEAD^3)" > > Duplicate the next line? > > > + test "$(git rev-parse c3)" = "$(git rev-parse HEAD^3)" && Yes, it was duplicated. > > + test "$auto_count" = "$recursive_count" > > +' > > Should not "$auto_count" != "$resolve_count" also be tested to be > sure that recursive has been choosen? Sure, changed. Updated patch below. t/t7601-merge-pull-config.sh | 129 ++++++++++++++++++++++++++++++++++++++++++ 1 files changed, 129 insertions(+), 0 deletions(-) create mode 100755 t/t7601-merge-pull-config.sh diff --git a/t/t7601-merge-pull-config.sh b/t/t7601-merge-pull-config.sh new file mode 100755 index 0000000..32585f8 --- /dev/null +++ b/t/t7601-merge-pull-config.sh @@ -0,0 +1,129 @@ +#!/bin/sh + +test_description='git-merge + +Testing pull.* configuration parsing.' + +. ./test-lib.sh + +test_expect_success 'setup' ' + echo c0 >c0.c && + git add c0.c && + git commit -m c0 && + git tag c0 && + echo c1 >c1.c && + git add c1.c && + git commit -m c1 && + git tag c1 && + git reset --hard c0 && + echo c2 >c2.c && + git add c2.c && + git commit -m c2 && + git tag c2 && + git reset --hard c0 && + echo c3 >c3.c && + git add c3.c && + git commit -m c3 && + git tag c3 +' + +test_expect_success 'merge c1 with c2' ' + git reset --hard c1 && + test -f c0.c && + test -f c1.c && + test ! -f c2.c && + test ! -f c3.c && + git merge c2 && + test -f c1.c && + test -f c2.c +' + +test_expect_success 'merge c1 with c2 (ours in pull.twohead)' ' + git reset --hard c1 && + git config pull.twohead ours && + git merge c2 && + test -f c1.c && + ! test -f c2.c +' + +test_expect_success 'merge c1 with c2 and c3 (recursive in pull.octopus)' ' + git reset --hard c1 && + git config pull.octopus "recursive" && + test_must_fail git merge c2 c3 && + test "$(git rev-parse c1)" = "$(git rev-parse HEAD)" +' + +test_expect_success 'merge c1 with c2 and c3 (recursive and octopus in pull.octopus)' ' + git reset --hard c1 && + git config pull.octopus "recursive octopus" && + git merge c2 c3 && + test "$(git rev-parse c1)" != "$(git rev-parse HEAD)" && + test "$(git rev-parse c1)" = "$(git rev-parse HEAD^1)" && + test "$(git rev-parse c2)" = "$(git rev-parse HEAD^2)" && + test "$(git rev-parse c3)" = "$(git rev-parse HEAD^3)" && + git diff --exit-code && + test -f c0.c && + test -f c1.c && + test -f c2.c && + test -f c3.c +' + +conflict_count() +{ + eval $1=`{ + git diff-files --name-only + git ls-files --unmerged + } | wc -l` +} + +# c4 - c5 +# \ c6 +# +# There are two conflicts here: +# +# 1) Because foo.c is renamed to bar.c, recursive will handle this, +# resolve won't. +# +# 2) One in conflict.c and that will always fail. + +test_expect_success 'setup conflicted merge' ' + git reset --hard c0 && + echo A >conflict.c && + git add conflict.c && + echo contents >foo.c && + git add foo.c && + git commit -m c4 && + git tag c4 && + echo B >conflict.c && + git add conflict.c && + git mv foo.c bar.c && + git commit -m c5 && + git tag c5 && + git reset --hard c4 && + echo C >conflict.c && + git add conflict.c && + echo secondline >> foo.c && + git add foo.c && + git commit -m c6 && + git tag c6 +' + +# First do the merge with resolve and recursive then verify that +# recusive is choosen. + +test_expect_success 'merge picks up the best result' ' + git config pull.twohead "recursive resolve" && + git reset --hard c5 && + git merge -s resolve c6 + conflict_count resolve_count && + git reset --hard c5 && + git merge -s recursive c6 + conflict_count recursive_count && + git reset --hard c5 && + git merge c6 + conflict_count auto_count && + test "$auto_count" = "$recursive_count" && + test "$auto_count" != "$resolve_count" +' + +test_done -- 1.5.6 ^ permalink raw reply related [flat|nested] 82+ messages in thread
* Re: [PATCH] Add new test to ensure git-merge handles pull.twohead and pull.octopus 2008-06-29 14:51 ` [PATCH] " Miklos Vajna @ 2008-06-29 15:11 ` Miklos Vajna 0 siblings, 0 replies; 82+ messages in thread From: Miklos Vajna @ 2008-06-29 15:11 UTC (permalink / raw To: Olivier Marin; +Cc: Junio C Hamano, git, Johannes Schindelin [-- Attachment #1: Type: text/plain, Size: 203 bytes --] On Sun, Jun 29, 2008 at 04:51:38PM +0200, Miklos Vajna <vmiklos@frugalware.org> wrote: > Signed-off-by: Junio C Hamano <gitster@pobox.com> Oops, I forgot to remove this Signed-off-by. Fixed in my fork. [-- Attachment #2: Type: application/pgp-signature, Size: 197 bytes --] ^ permalink raw reply [flat|nested] 82+ messages in thread
* Re: [PATCH 04/15] Add new test to ensure git-merge handles pull.twohead and pull.octopus 2008-06-27 16:21 ` [PATCH 04/15] Add new test to ensure git-merge handles pull.twohead and pull.octopus Miklos Vajna 2008-06-27 16:21 ` [PATCH 05/15] Move read_cache_unmerged() to read-cache.c Miklos Vajna 2008-06-29 13:30 ` [PATCH 04/15] Add new test to ensure git-merge handles pull.twohead and pull.octopus Olivier Marin @ 2008-07-04 16:34 ` Mike Ralphson 2008-07-05 0:26 ` Miklos Vajna 2 siblings, 1 reply; 82+ messages in thread From: Mike Ralphson @ 2008-07-04 16:34 UTC (permalink / raw To: Miklos Vajna, Junio C Hamano; +Cc: git, Johannes Schindelin, Olivier Marin 2008/6/27 Miklos Vajna <vmiklos@frugalware.org>: > > Test if the given strategies are used and test the case when multiple > strategies are configured using a space separated list. > > Also test if the best strategy is picked if none is specified. This is > done by adding a simple test case where recursive detects a rename, but > resolve does not, and verify that finally merge will pick up the > previous. > > Signed-off-by: Miklos Vajna <vmiklos@frugalware.org> > --- > t/t7601-merge-pull-config.sh | 129 ++++++++++++++++++++++++++++++++++++++++++ > + > +conflict_count() > +{ > + eval $1=`{ > + git diff-files --name-only > + git ls-files --unmerged > + } | wc -l` > +} > + This here causes the test to fail on AIX (and likely other OS, such as apparently OSX) where wc -l outputs whitespace. See http://article.gmane.org/gmane.comp.version-control.git/80450 Here we want the line count not just a return value, so is the following acceptable? diff --git a/t/t7601-merge-pull-config.sh b/t/t7601-merge-pull-config.sh index 32585f8..9b6097d 100755 --- a/t/t7601-merge-pull-config.sh +++ b/t/t7601-merge-pull-config.sh @@ -73,7 +73,7 @@ conflict_count() eval $1=`{ git diff-files --name-only git ls-files --unmerged - } | wc -l` + } | wc -l | tr -d \ ` } # c4 - c5 Signed-off-by: Mike Ralphson <mike@abacus.co.uk> Anyway, I thought we preferred $() to backticks? I do apologise for not being around for the earlier comedy breakage on AIX, my sysadmins decided to 'improve' our firewall rules which cut my automated builds off just after the final rc for 1.5.6, then I was on vacation miles from any computers. Mike ^ permalink raw reply related [flat|nested] 82+ messages in thread
* Re: [PATCH 04/15] Add new test to ensure git-merge handles pull.twohead and pull.octopus 2008-07-04 16:34 ` [PATCH 04/15] " Mike Ralphson @ 2008-07-05 0:26 ` Miklos Vajna 2008-07-05 0:32 ` [PATCH] Fix t7601-merge-pull-config.sh on AIX Miklos Vajna 0 siblings, 1 reply; 82+ messages in thread From: Miklos Vajna @ 2008-07-05 0:26 UTC (permalink / raw To: Mike Ralphson; +Cc: Junio C Hamano, git, Johannes Schindelin, Olivier Marin [-- Attachment #1: Type: text/plain, Size: 1204 bytes --] On Fri, Jul 04, 2008 at 05:34:05PM +0100, Mike Ralphson <mike.ralphson@gmail.com> wrote: > > +conflict_count() > > +{ > > + eval $1=`{ > > + git diff-files --name-only > > + git ls-files --unmerged > > + } | wc -l` > > +} > > + > > This here causes the test to fail on AIX (and likely other OS, such as > apparently OSX) where wc -l outputs whitespace. See > http://article.gmane.org/gmane.comp.version-control.git/80450 > > Here we want the line count not just a return value, so is the > following acceptable? > > diff --git a/t/t7601-merge-pull-config.sh b/t/t7601-merge-pull-config.sh > index 32585f8..9b6097d 100755 > --- a/t/t7601-merge-pull-config.sh > +++ b/t/t7601-merge-pull-config.sh > @@ -73,7 +73,7 @@ conflict_count() > eval $1=`{ > git diff-files --name-only > git ls-files --unmerged > - } | wc -l` > + } | wc -l | tr -d \ ` > } At least it does not break the test for me on Linux. But haven't this cause you problems in git-merge.sh? I copied this code chunk from there. > Anyway, I thought we preferred $() to backticks? See above, but sure, we can. [-- Attachment #2: Type: application/pgp-signature, Size: 197 bytes --] ^ permalink raw reply [flat|nested] 82+ messages in thread
* [PATCH] Fix t7601-merge-pull-config.sh on AIX 2008-07-05 0:26 ` Miklos Vajna @ 2008-07-05 0:32 ` Miklos Vajna 2008-07-05 1:49 ` Junio C Hamano 0 siblings, 1 reply; 82+ messages in thread From: Miklos Vajna @ 2008-07-05 0:32 UTC (permalink / raw To: Junio C Hamano; +Cc: Johannes Schindelin, Olivier Marin, git, Mike Ralphson The test failed on AIX (and likely other OS, such as apparently OSX) where wc -l outputs whitespace. Signed-off-by: Mike Ralphson <mike@abacus.co.uk> Signed-off-by: Miklos Vajna <vmiklos@frugalware.org> --- Here is the same, with backticks avoided, and with a proper commit message. t/t7601-merge-pull-config.sh | 4 ++-- 1 files changed, 2 insertions(+), 2 deletions(-) diff --git a/t/t7601-merge-pull-config.sh b/t/t7601-merge-pull-config.sh index 32585f8..12f71ad 100755 --- a/t/t7601-merge-pull-config.sh +++ b/t/t7601-merge-pull-config.sh @@ -70,10 +70,10 @@ test_expect_success 'merge c1 with c2 and c3 (recursive and octopus in pull.octo conflict_count() { - eval $1=`{ + eval $1=$({ git diff-files --name-only git ls-files --unmerged - } | wc -l` + } | wc -l | tr -d \ ) } # c4 - c5 -- 1.5.6.1 ^ permalink raw reply related [flat|nested] 82+ messages in thread
* Re: [PATCH] Fix t7601-merge-pull-config.sh on AIX 2008-07-05 0:32 ` [PATCH] Fix t7601-merge-pull-config.sh on AIX Miklos Vajna @ 2008-07-05 1:49 ` Junio C Hamano 0 siblings, 0 replies; 82+ messages in thread From: Junio C Hamano @ 2008-07-05 1:49 UTC (permalink / raw To: Miklos Vajna; +Cc: Johannes Schindelin, Olivier Marin, git, Mike Ralphson Miklos Vajna <vmiklos@frugalware.org> writes: > diff --git a/t/t7601-merge-pull-config.sh b/t/t7601-merge-pull-config.sh > index 32585f8..12f71ad 100755 > --- a/t/t7601-merge-pull-config.sh > +++ b/t/t7601-merge-pull-config.sh > @@ -70,10 +70,10 @@ test_expect_success 'merge c1 with c2 and c3 (recursive and octopus in pull.octo > > conflict_count() > { > - eval $1=`{ > + eval $1=$({ > git diff-files --name-only > git ls-files --unmerged > - } | wc -l` > + } | wc -l | tr -d \ ) > } In any case, this feels like an unnecessary use of eval. The call site you have look like this: conflict_count resolve_count but it is more natural if you are programming in shell to call it like: resolve_count=$(count_conflicts) and it is more natural to write count_conflicts like this: count_conflicts () { { git diff-files --name-only --diff-filter=U git ls-files --unmerged } | wc -l } But I am puzzled about the alledged *breakage* -- look at your call sites. reset --hard .. do a merge .. conflict_count count_one reset --hard .. do another merge .. conlict_count count_two reset --hard .. do yet another merge .. conlict_count count_three test "$count_three" = "$count_two" At any point, you do not do numerical comparison, and I do not think extra whitespace from other "wc" implementations matter, as long as they are consistent. If you are going to do numerical comparison in later versions, you can just drop the dq around parameters of test: test $count_three = $count_two ^ permalink raw reply [flat|nested] 82+ messages in thread
* Re: [PATCH 01/15] Move split_cmdline() to alias.c 2008-06-27 16:21 ` [PATCH 01/15] Move split_cmdline() to alias.c Miklos Vajna 2008-06-27 16:21 ` [PATCH 02/15] Move commit_list_count() to commit.c Miklos Vajna @ 2008-06-29 14:05 ` Olivier Marin 2008-06-29 14:15 ` Johannes Schindelin 1 sibling, 1 reply; 82+ messages in thread From: Olivier Marin @ 2008-06-29 14:05 UTC (permalink / raw To: Miklos Vajna; +Cc: Junio C Hamano, git, Johannes Schindelin, Olivier Marin Miklos Vajna a écrit : > split_cmdline() is currently used for aliases only, but later it can be > useful for other builtins as well. Move it to alias.c for now, > indicating that originally it's for aliases, but we'll have it in libgit > this way. This function does not trim cmdline. Perhaps, the following patch can be inserted after 1/15. -- >8 -- From: Olivier Marin <dkr@freesurf.fr> Date: Sat, 28 Jun 2008 13:06:21 +0200 Subject: [PATCH] split_cmdline(): ignore whitespace at start/end of cmdline Signed-off-by: Olivier Marin <dkr@freesurf.fr> --- alias.c | 11 +++++++---- 1 files changed, 7 insertions(+), 4 deletions(-) diff --git a/alias.c b/alias.c index ccb1108..7b69d18 100644 --- a/alias.c +++ b/alias.c @@ -24,14 +24,16 @@ char *alias_lookup(const char *alias) int split_cmdline(char *cmdline, const char ***argv) { - int src, dst, count = 0, size = 16; + int src = 0, dst, count = 0, size = 16; char quoted = 0; *argv = xmalloc(sizeof(char*) * size); /* split alias_string */ - (*argv)[count++] = cmdline; - for (src = dst = 0; cmdline[src];) { + while (cmdline[src] && isspace(cmdline[src])) + src++; + (*argv)[count++] = cmdline + src; + for (dst = src; cmdline[src];) { char c = cmdline[src]; if (!quoted && isspace(c)) { cmdline[dst++] = 0; @@ -42,7 +44,8 @@ int split_cmdline(char *cmdline, const char ***argv) size += 16; *argv = xrealloc(*argv, sizeof(char*) * size); } - (*argv)[count++] = cmdline + dst; + if (cmdline[src]) + (*argv)[count++] = cmdline + dst; } else if (!quoted && (c == '\'' || c == '"')) { quoted = c; src++; -- 1.5.6.1.103.g191a2.dirty ^ permalink raw reply related [flat|nested] 82+ messages in thread
* Re: [PATCH 01/15] Move split_cmdline() to alias.c 2008-06-29 14:05 ` [PATCH 01/15] Move split_cmdline() to alias.c Olivier Marin @ 2008-06-29 14:15 ` Johannes Schindelin 2008-06-29 14:29 ` Olivier Marin 0 siblings, 1 reply; 82+ messages in thread From: Johannes Schindelin @ 2008-06-29 14:15 UTC (permalink / raw To: Olivier Marin; +Cc: Miklos Vajna, Junio C Hamano, git, Olivier Marin [-- Attachment #1: Type: TEXT/PLAIN, Size: 615 bytes --] Hi, On Sun, 29 Jun 2008, Olivier Marin wrote: > Miklos Vajna a écrit : > > split_cmdline() is currently used for aliases only, but later it can be > > useful for other builtins as well. Move it to alias.c for now, > > indicating that originally it's for aliases, but we'll have it in libgit > > this way. > > This function does not trim cmdline. As the string comes either from the config (where it is trimmed), or from the command line (where the user can be stup^W^Wask for whitespace explicitely), I do not see much merit in this patch. Unless you can provide an example where it fails, of course, Dscho ^ permalink raw reply [flat|nested] 82+ messages in thread
* Re: [PATCH 01/15] Move split_cmdline() to alias.c 2008-06-29 14:15 ` Johannes Schindelin @ 2008-06-29 14:29 ` Olivier Marin 2008-06-29 14:43 ` Johannes Schindelin 0 siblings, 1 reply; 82+ messages in thread From: Olivier Marin @ 2008-06-29 14:29 UTC (permalink / raw To: Johannes Schindelin; +Cc: Miklos Vajna, Junio C Hamano, git, Olivier Marin Johannes Schindelin a écrit : > > As the string comes either from the config (where it is trimmed), or from > the command line (where the user can be stup^W^Wask for whitespace > explicitely), I do not see much merit in this patch. You are right, today it is not a problem because the usage is really limited but Miklos's intention seems to make the function usable by everyone. As we do not know how it will be used in the future, I think it is safer with my patch. Olivier. ^ permalink raw reply [flat|nested] 82+ messages in thread
* Re: [PATCH 01/15] Move split_cmdline() to alias.c 2008-06-29 14:29 ` Olivier Marin @ 2008-06-29 14:43 ` Johannes Schindelin 2008-06-30 22:51 ` Olivier Marin 0 siblings, 1 reply; 82+ messages in thread From: Johannes Schindelin @ 2008-06-29 14:43 UTC (permalink / raw To: Olivier Marin; +Cc: Miklos Vajna, Junio C Hamano, git, Olivier Marin [-- Attachment #1: Type: TEXT/PLAIN, Size: 1815 bytes --] Hi, On Sun, 29 Jun 2008, Olivier Marin wrote: > Johannes Schindelin a écrit : > > > > As the string comes either from the config (where it is trimmed), or > > from the command line (where the user can be stup^W^Wask for > > whitespace explicitely), I do not see much merit in this patch. > > You are right, today it is not a problem because the usage is really > limited but Miklos's intention seems to make the function usable by > everyone. As we do not know how it will be used in the future, I think > it is safer with my patch. I am generally not a fan of crossing bridges miles before you reach them. But if you want to keep running with this, you should add at least a few tests to show what you patch solves, and that it solves it. Preferably with a "test_expect_failure" patch, and then a patch fixing it and changing that _failure to _success. Here's a starter: -- snipsnap -- diff --git a/Makefile b/Makefile index d3e339a..83a9a30 100644 --- a/Makefile +++ b/Makefile @@ -1229,7 +1229,7 @@ endif ### Testing rules -TEST_PROGRAMS = test-chmtime$X test-genrandom$X test-date$X test-delta$X test-sha1$X test-match-trees$X test-absolute-path$X test-parse-options$X test-strbuf$X +TEST_PROGRAMS = test-chmtime$X test-genrandom$X test-date$X test-delta$X test-sha1$X test-match-trees$X test-absolute-path$X test-parse-options$X test-strbuf$X test-cmdline$X all:: $(TEST_PROGRAMS) diff --git a/test-cmdline.c b/test-cmdline.c new file mode 100644 index 0000000..2f6d9eb --- /dev/null +++ b/test-cmdline.c @@ -0,0 +1,15 @@ +#include "cache.h" + +int main(int argc, char **argv) +{ + int i = 0; + const char **new_argv; + + if (argc < 2 || (argc = split_cmdline(argv[1], &new_argv)) < 0) + return 1; + + while (i < argc) + printf("arg %d: '%s'\n", i++, *new_argv++); + + return 0; +} ^ permalink raw reply related [flat|nested] 82+ messages in thread
* Re: [PATCH 01/15] Move split_cmdline() to alias.c 2008-06-29 14:43 ` Johannes Schindelin @ 2008-06-30 22:51 ` Olivier Marin 0 siblings, 0 replies; 82+ messages in thread From: Olivier Marin @ 2008-06-30 22:51 UTC (permalink / raw To: Johannes Schindelin; +Cc: Miklos Vajna, Junio C Hamano, git Johannes Schindelin a écrit : > > I am generally not a fan of crossing bridges miles before you reach them. > OK, as you want. :-) I don't really care about this patch, anyway. Olivier. ^ permalink raw reply [flat|nested] 82+ messages in thread
* Re: [PATCH 04/10] Add new test to ensure git-merge handles pull.twohead and pull.octopus @ 2008-06-05 22:58 Junio C Hamano 2008-06-07 0:47 ` [PATCH] " Miklos Vajna 0 siblings, 1 reply; 82+ messages in thread From: Junio C Hamano @ 2008-06-05 22:58 UTC (permalink / raw To: Miklos Vajna; +Cc: git Miklos Vajna <vmiklos@frugalware.org> writes: > Test if the given strategies are used and test the case when multiple > strategies are configured using a space separated list. > > Signed-off-by: Miklos Vajna <vmiklos@frugalware.org> > --- > t/t7601-merge-pull-config.sh | 57 ++++++++++++++++++++++++++++++++++++++++++ > 1 files changed, 57 insertions(+), 0 deletions(-) > create mode 100755 t/t7601-merge-pull-config.sh > > diff --git a/t/t7601-merge-pull-config.sh b/t/t7601-merge-pull-config.sh > new file mode 100755 > index 0000000..cc595ac > --- /dev/null > +++ b/t/t7601-merge-pull-config.sh > @@ -0,0 +1,57 @@ > +#!/bin/sh > + > +test_description='git-merge > + > +Testing pull.* configuration parsing.' > + > +. ./test-lib.sh > + > +test_expect_success 'setup' ' > + echo c0 >c0.c && > + git add c0.c && > + git commit -m c0 && > + git tag c0 && > + echo c1 >c1.c && > + git add c1.c && > + git commit -m c1 && > + git tag c1 && > + git reset --hard c0 && > + echo c2 >c2.c && > + git add c2.c && > + git commit -m c2 && > + git tag c2 > + git reset --hard c0 && > + echo c3 >c3.c && > + git add c3.c && > + git commit -m c3 && > + git tag c3 > +' > + > +test_expect_success 'merge c1 with c2' ' > + git reset --hard c1 && test that c0 and c1 do and c2 and c3 do not exist here, as it is cheap, and otherwise you may end up chasing wild-goose when somebody breaks git-reset. No need to do so in later tests in this script, but it is a cheap protection for yourself from others' mistakes ;-). > + git merge c2 && > + test -e c1.c && > + test -e c2.c > +' Nobody runs V7 that lacked "test -e" to run these test scripts, but you expect them to be regular files at this point of the test, so the correct way to spell these is with "test -f". In general, you are better off training yourself to think if you can use "test -f" before blindly using "test -e". > +test_expect_success 'merge c1 with c2 (ours in pull.twohead)' ' > + git reset --hard c1 && > + git config pull.twohead ours && > + git merge c2 && > + test -e c1.c && > + ! test -e c2.c > +' > + > +test_expect_success 'merge c1 with c2 and c3 (recursive in pull.octopus)' ' > + git reset --hard c1 && > + git config pull.octopus "recursive" && > + ! git merge c2 c3 Is it because it should dump core, or is it because the command should decline to work, gracefully failing with an error message and non-zero exit status? Use "test_must_fail" to check for the latter. Don't you want to check how it fails and in what shape the command leaves the work tree? I am assuming that recursive sees more than one "remote" head and declines to work without touching work tree nor the index, so if that is what you expect, you should check for that. Otherwise, a regression that loses local changes will go unnoticed. > +' > + > +test_expect_success 'merge c1 with c2 and c3 (recursive and octopus in pull.octopus)' ' > + git reset --hard c1 && > + git config pull.octopus "recursive octopus" && > + git merge c2 c3 Likewise, don't you want to check the result of the merge? Not just "merge exited with 0", but you would want to see that the HEAD has advanced, it has the expected parents, there is no unexpected local changes (because you did not have any when you started the merge), and it has the expected tree contents. > +' > + > +test_done > -- > 1.5.6.rc0.dirty ^ permalink raw reply [flat|nested] 82+ messages in thread
* [PATCH] Add new test to ensure git-merge handles pull.twohead and pull.octopus 2008-06-05 22:58 [PATCH 04/10] Add new test to ensure git-merge handles pull.twohead and pull.octopus Junio C Hamano @ 2008-06-07 0:47 ` Miklos Vajna 0 siblings, 0 replies; 82+ messages in thread From: Miklos Vajna @ 2008-06-07 0:47 UTC (permalink / raw To: Junio C Hamano; +Cc: git Test if the given strategies are used and test the case when multiple strategies are configured using a space separated list. Signed-off-by: Miklos Vajna <vmiklos@frugalware.org> --- On Thu, Jun 05, 2008 at 03:58:23PM -0700, Junio C Hamano <gitster@pobox.com> wrote: > > +test_expect_success 'merge c1 with c2' ' > > + git reset --hard c1 && > > test that c0 and c1 do and c2 and c3 do not exist here, as it is > cheap, > and otherwise you may end up chasing wild-goose when somebody breaks > git-reset. No need to do so in later tests in this script, but it is > a > cheap protection for yourself from others' mistakes ;-). Done. > > + git merge c2 && > > + test -e c1.c && > > + test -e c2.c > > +' > > Nobody runs V7 that lacked "test -e" to run these test scripts, but > you > expect them to be regular files at this point of the test, so the > correct > way to spell these is with "test -f". > > In general, you are better off training yourself to think if you can > use > "test -f" before blindly using "test -e". Sure, corrected. > > +test_expect_success 'merge c1 with c2 and c3 (recursive in > > pull.octopus)' ' > > + git reset --hard c1 && > > + git config pull.octopus "recursive" && > > + ! git merge c2 c3 > > Is it because it should dump core, or is it because the command should > decline to work, gracefully failing with an error message and non-zero > exit status? Use "test_must_fail" to check for the latter. Obviously the later, corrected. > Don't you want to check how it fails and in what shape the command > leaves > the work tree? I am assuming that recursive sees more than one > "remote" > head and declines to work without touching work tree nor the index, so > if > that is what you expect, you should check for that. Otherwise, a > regression that loses local changes will go unnoticed. Hm yes. I added checks to ensure nothing happened. > > +test_expect_success 'merge c1 with c2 and c3 (recursive and octopus > > in pull.octopus)' ' > > + git reset --hard c1 && > > + git config pull.octopus "recursive octopus" && > > + git merge c2 c3 > > Likewise, don't you want to check the result of the merge? Not just > "merge exited with 0", but you would want to see that the HEAD has > advanced, it has the expected parents, there is no unexpected local > changes (because you did not have any when you started the merge), and > it > has the expected tree contents. Corrected. I'm sending the version I just pushed to my working branch. t/t7601-merge-pull-config.sh | 72 ++++++++++++++++++++++++++++++++++++++++++ 1 files changed, 72 insertions(+), 0 deletions(-) create mode 100755 t/t7601-merge-pull-config.sh diff --git a/t/t7601-merge-pull-config.sh b/t/t7601-merge-pull-config.sh new file mode 100755 index 0000000..c0b550e --- /dev/null +++ b/t/t7601-merge-pull-config.sh @@ -0,0 +1,72 @@ +#!/bin/sh + +test_description='git-merge + +Testing pull.* configuration parsing.' + +. ./test-lib.sh + +test_expect_success 'setup' ' + echo c0 >c0.c && + git add c0.c && + git commit -m c0 && + git tag c0 && + echo c1 >c1.c && + git add c1.c && + git commit -m c1 && + git tag c1 && + git reset --hard c0 && + echo c2 >c2.c && + git add c2.c && + git commit -m c2 && + git tag c2 + git reset --hard c0 && + echo c3 >c3.c && + git add c3.c && + git commit -m c3 && + git tag c3 +' + +test_expect_success 'merge c1 with c2' ' + git reset --hard c1 && + test -f c0.c && + test -f c1.c && + test ! -f c2.c && + test ! -f c3.c && + git merge c2 && + test -f c1.c && + test -f c2.c +' + +test_expect_success 'merge c1 with c2 (ours in pull.twohead)' ' + git reset --hard c1 && + git config pull.twohead ours && + git merge c2 && + test -f c1.c && + ! test -f c2.c +' + +test_expect_success 'merge c1 with c2 and c3 (recursive in pull.octopus)' ' + git reset --hard c1 && + git config pull.octopus "recursive" && + test_must_fail git merge c2 c3 && + test "$(git rev-parse c1)" = "$(git rev-parse HEAD)" +' + +test_expect_success 'merge c1 with c2 and c3 (recursive and octopus in pull.octopus)' ' + git reset --hard c1 && + git config pull.octopus "recursive octopus" && + git merge c2 c3 && + test "$(git rev-parse c1)" != "$(git rev-parse HEAD)" && + test "$(git rev-parse c1)" = "$(git rev-parse HEAD^1)" && + test "$(git rev-parse c2)" = "$(git rev-parse HEAD^2)" && + test "$(git rev-parse c3)" = "$(git rev-parse HEAD^3)" + test "$(git rev-parse c3)" = "$(git rev-parse HEAD^3)" && + git diff --exit-code && + test -f c0.c && + test -f c1.c && + test -f c2.c && + test -f c3.c +' + +test_done -- 1.5.6.rc0.dirty ^ permalink raw reply related [flat|nested] 82+ messages in thread
end of thread, other threads:[~2008-07-08 1:42 UTC | newest] Thread overview: 82+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2008-06-27 16:21 [PATCH 00/15] Build in merge Miklos Vajna 2008-06-27 16:21 ` [PATCH 01/15] Move split_cmdline() to alias.c Miklos Vajna 2008-06-27 16:21 ` [PATCH 02/15] Move commit_list_count() to commit.c Miklos Vajna 2008-06-27 16:21 ` [PATCH 03/15] Move parse-options's skip_prefix() to git-compat-util.h Miklos Vajna 2008-06-27 16:21 ` [PATCH 04/15] Add new test to ensure git-merge handles pull.twohead and pull.octopus Miklos Vajna 2008-06-27 16:21 ` [PATCH 05/15] Move read_cache_unmerged() to read-cache.c Miklos Vajna 2008-06-27 16:21 ` [PATCH 06/15] git-fmt-merge-msg: make it usable from other builtins Miklos Vajna 2008-06-27 16:22 ` [PATCH 07/15] Introduce get_octopus_merge_bases() in commit.c Miklos Vajna 2008-06-27 16:22 ` [PATCH 08/15] Add new test to ensure git-merge handles more than 25 refs Miklos Vajna 2008-06-27 16:22 ` [PATCH 09/15] Introduce get_merge_bases_many() Miklos Vajna 2008-06-27 16:22 ` [PATCH 10/15] Introduce reduce_heads() Miklos Vajna 2008-06-27 16:22 ` [PATCH 11/15] Add strbuf_vaddf(), use it in strbuf_addf(), and add strbuf_initf() Miklos Vajna 2008-06-27 16:22 ` [PATCH 12/15] strbuf_vaddf(): support %*s, too Miklos Vajna 2008-06-27 16:22 ` [PATCH 13/15] Add new test case to ensure git-merge reduces octopus parents when possible Miklos Vajna 2008-06-27 16:22 ` [PATCH 14/15] Add new test case to ensure git-merge prepends the custom merge message Miklos Vajna 2008-06-27 16:22 ` [PATCH 15/15] Build in merge Miklos Vajna 2008-06-27 17:09 ` Miklos Vajna 2008-06-28 2:00 ` [PATCH 11/15] Add strbuf_vaddf(), use it in strbuf_addf(), and add strbuf_initf() Junio C Hamano 2008-06-28 2:33 ` Miklos Vajna 2008-06-28 2:38 ` [PATCH 13/13] Build in merge Miklos Vajna 2008-06-29 7:46 ` Junio C Hamano 2008-06-29 8:11 ` Jakub Narebski 2008-06-30 1:36 ` Miklos Vajna 2008-06-30 1:39 ` Miklos Vajna 2008-06-30 5:44 ` Junio C Hamano 2008-06-30 17:41 ` Alex Riesen 2008-07-01 2:13 ` Miklos Vajna 2008-07-01 2:22 ` [PATCH 13/14] git-commit-tree: make it usable from other builtins Miklos Vajna 2008-07-01 5:07 ` Johannes Schindelin 2008-07-01 5:50 ` Junio C Hamano 2008-07-01 12:09 ` Miklos Vajna 2008-07-01 2:22 ` [PATCH 14/14] Build in merge Miklos Vajna 2008-07-01 2:37 ` [PATCH 00/14] " Miklos Vajna 2008-07-01 2:37 ` [PATCH 08/14] Add new test to ensure git-merge handles more than 25 refs Miklos Vajna 2008-07-01 2:37 ` [PATCH 13/14] git-commit-tree: make it usable from other builtins Miklos Vajna 2008-07-01 2:37 ` [PATCH 14/14] Build in merge Miklos Vajna 2008-07-01 6:23 ` Junio C Hamano 2008-07-01 12:50 ` Miklos Vajna 2008-07-01 13:18 ` Miklos Vajna 2008-07-01 23:55 ` Junio C Hamano 2008-07-02 7:43 ` Miklos Vajna 2008-07-06 8:50 ` Junio C Hamano 2008-07-06 9:43 ` Junio C Hamano 2008-07-07 17:17 ` Miklos Vajna 2008-07-07 18:15 ` Junio C Hamano 2008-07-07 23:42 ` [PATCH] " Miklos Vajna 2008-07-08 0:32 ` Junio C Hamano 2008-07-08 0:53 ` Junio C Hamano 2008-07-08 1:18 ` Miklos Vajna 2008-07-08 1:00 ` Miklos Vajna 2008-07-08 1:05 ` Junio C Hamano 2008-07-08 1:41 ` Miklos Vajna 2008-07-06 12:38 ` [PATCH 14/14] " Johannes Schindelin 2008-07-06 19:39 ` Junio C Hamano 2008-07-07 17:24 ` [PATCH] " Miklos Vajna 2008-07-07 17:35 ` Miklos Vajna 2008-07-01 7:27 ` [PATCH 14/14] " Junio C Hamano 2008-07-01 12:55 ` Miklos Vajna 2008-06-30 22:44 ` [PATCH 13/13] " Olivier Marin 2008-06-30 22:58 ` Miklos Vajna 2008-06-30 5:40 ` Junio C Hamano 2008-06-30 22:48 ` Miklos Vajna 2008-06-28 17:33 ` [PATCH 11/15] Add strbuf_vaddf(), use it in strbuf_addf(), and add strbuf_initf() Johannes Schindelin 2008-06-29 8:07 ` Junio C Hamano 2008-06-29 13:40 ` Johannes Schindelin 2008-06-29 20:17 ` Alex Riesen 2008-06-29 20:24 ` Junio C Hamano 2008-06-29 20:30 ` Sverre Rabbelier 2008-06-27 17:06 ` [PATCH 08/15] Add new test to ensure git-merge handles more than 25 refs Miklos Vajna 2008-06-29 13:30 ` [PATCH 04/15] Add new test to ensure git-merge handles pull.twohead and pull.octopus Olivier Marin 2008-06-29 14:51 ` [PATCH] " Miklos Vajna 2008-06-29 15:11 ` Miklos Vajna 2008-07-04 16:34 ` [PATCH 04/15] " Mike Ralphson 2008-07-05 0:26 ` Miklos Vajna 2008-07-05 0:32 ` [PATCH] Fix t7601-merge-pull-config.sh on AIX Miklos Vajna 2008-07-05 1:49 ` Junio C Hamano 2008-06-29 14:05 ` [PATCH 01/15] Move split_cmdline() to alias.c Olivier Marin 2008-06-29 14:15 ` Johannes Schindelin 2008-06-29 14:29 ` Olivier Marin 2008-06-29 14:43 ` Johannes Schindelin 2008-06-30 22:51 ` Olivier Marin -- strict thread matches above, loose matches on Subject: below -- 2008-06-05 22:58 [PATCH 04/10] Add new test to ensure git-merge handles pull.twohead and pull.octopus Junio C Hamano 2008-06-07 0:47 ` [PATCH] " Miklos Vajna
Code repositories for project(s) associated with this public inbox https://80x24.org/mirrors/git.git This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox; as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).