Browse Source
The merge-recursive machiery is one of the most complex parts of the system that accumulated cruft over time. This large series cleans up the implementation quite a bit. * en/merge-recursive-cleanup: (26 commits) merge-recursive: fix the fix to the diff3 common ancestor label merge-recursive: fix the diff3 common ancestor label for virtual commits merge-recursive: alphabetize include list merge-recursive: add sanity checks for relevant merge_options merge-recursive: rename MERGE_RECURSIVE_* to MERGE_VARIANT_* merge-recursive: split internal fields into a separate struct merge-recursive: avoid losing output and leaking memory holding that output merge-recursive: comment and reorder the merge_options fields merge-recursive: consolidate unnecessary fields in merge_options merge-recursive: move some definitions around to clean up the header merge-recursive: rename merge_options argument to opt in header merge-recursive: rename 'mrtree' to 'result_tree', for clarity merge-recursive: use common name for ancestors/common/base_list merge-recursive: fix some overly long lines cache-tree: share code between functions writing an index as a tree merge-recursive: don't force external callers to do our logging merge-recursive: remove useless parameter in merge_trees() merge-recursive: exit early if index != head Ensure index matches head before invoking merge machinery, round N merge-recursive: remove another implicit dependency on the_repository ...maint

12 changed files with 736 additions and 334 deletions
@ -1,104 +1,124 @@
@@ -1,104 +1,124 @@
|
||||
#ifndef MERGE_RECURSIVE_H |
||||
#define MERGE_RECURSIVE_H |
||||
|
||||
#include "string-list.h" |
||||
#include "unpack-trees.h" |
||||
#include "strbuf.h" |
||||
|
||||
struct commit; |
||||
|
||||
struct commit_list; |
||||
struct object_id; |
||||
struct repository; |
||||
struct tree; |
||||
|
||||
struct merge_options_internal; |
||||
struct merge_options { |
||||
struct repository *repo; |
||||
|
||||
/* ref names used in console messages and conflict markers */ |
||||
const char *ancestor; |
||||
const char *branch1; |
||||
const char *branch2; |
||||
|
||||
/* rename related options */ |
||||
int detect_renames; |
||||
enum { |
||||
MERGE_RECURSIVE_NORMAL = 0, |
||||
MERGE_RECURSIVE_OURS, |
||||
MERGE_RECURSIVE_THEIRS |
||||
MERGE_DIRECTORY_RENAMES_NONE = 0, |
||||
MERGE_DIRECTORY_RENAMES_CONFLICT = 1, |
||||
MERGE_DIRECTORY_RENAMES_TRUE = 2 |
||||
} detect_directory_renames; |
||||
int rename_limit; |
||||
int rename_score; |
||||
int show_rename_progress; |
||||
|
||||
/* xdiff-related options (patience, ignore whitespace, ours/theirs) */ |
||||
long xdl_opts; |
||||
enum { |
||||
MERGE_VARIANT_NORMAL = 0, |
||||
MERGE_VARIANT_OURS, |
||||
MERGE_VARIANT_THEIRS |
||||
} recursive_variant; |
||||
const char *subtree_shift; |
||||
|
||||
/* console output related options */ |
||||
int verbosity; |
||||
unsigned buffer_output; /* 1: output at end, 2: keep buffered */ |
||||
struct strbuf obuf; /* output buffer; if buffer_output == 2, caller |
||||
* must handle and call strbuf_release */ |
||||
|
||||
/* miscellaneous control options */ |
||||
const char *subtree_shift; |
||||
unsigned renormalize : 1; |
||||
long xdl_opts; |
||||
int verbosity; |
||||
int detect_directory_renames; |
||||
int diff_detect_rename; |
||||
int merge_detect_rename; |
||||
int diff_rename_limit; |
||||
int merge_rename_limit; |
||||
int rename_score; |
||||
int needed_rename_limit; |
||||
int show_rename_progress; |
||||
int call_depth; |
||||
struct strbuf obuf; |
||||
struct hashmap current_file_dir_set; |
||||
struct string_list df_conflict_file_set; |
||||
struct unpack_trees_options unpack_opts; |
||||
struct index_state orig_index; |
||||
struct repository *repo; |
||||
|
||||
/* internal fields used by the implementation */ |
||||
struct merge_options_internal *priv; |
||||
}; |
||||
|
||||
void init_merge_options(struct merge_options *opt, struct repository *repo); |
||||
|
||||
/* parse the option in s and update the relevant field of opt */ |
||||
int parse_merge_opt(struct merge_options *opt, const char *s); |
||||
|
||||
/* |
||||
* For dir_rename_entry, directory names are stored as a full path from the |
||||
* toplevel of the repository and do not include a trailing '/'. Also: |
||||
* |
||||
* dir: original name of directory being renamed |
||||
* non_unique_new_dir: if true, could not determine new_dir |
||||
* new_dir: final name of directory being renamed |
||||
* possible_new_dirs: temporary used to help determine new_dir; see comments |
||||
* in get_directory_renames() for details |
||||
* RETURN VALUES: All the merge_* functions below return a value as follows: |
||||
* > 0 Merge was clean |
||||
* = 0 Merge had conflicts |
||||
* < 0 Merge hit an unexpected and unrecoverable problem (e.g. disk |
||||
* full) and aborted merge part-way through. |
||||
*/ |
||||
struct dir_rename_entry { |
||||
struct hashmap_entry ent; /* must be the first member! */ |
||||
char *dir; |
||||
unsigned non_unique_new_dir:1; |
||||
struct strbuf new_dir; |
||||
struct string_list possible_new_dirs; |
||||
}; |
||||
|
||||
struct collision_entry { |
||||
struct hashmap_entry ent; /* must be the first member! */ |
||||
char *target_file; |
||||
struct string_list source_files; |
||||
unsigned reported_already:1; |
||||
}; |
||||
|
||||
static inline int merge_detect_rename(struct merge_options *o) |
||||
{ |
||||
return o->merge_detect_rename >= 0 ? o->merge_detect_rename : |
||||
o->diff_detect_rename >= 0 ? o->diff_detect_rename : 1; |
||||
} |
||||
/* |
||||
* rename-detecting three-way merge, no recursion. |
||||
* |
||||
* Outputs: |
||||
* - See RETURN VALUES above |
||||
* - No commit is created |
||||
* - opt->repo->index has the new index |
||||
* - $GIT_INDEX_FILE is not updated |
||||
* - The working tree is updated with results of the merge |
||||
*/ |
||||
int merge_trees(struct merge_options *opt, |
||||
struct tree *head, |
||||
struct tree *merge, |
||||
struct tree *merge_base); |
||||
|
||||
/* merge_trees() but with recursive ancestor consolidation */ |
||||
int merge_recursive(struct merge_options *o, |
||||
/* |
||||
* merge_recursive is like merge_trees() but with recursive ancestor |
||||
* consolidation and, if the commit is clean, creation of a commit. |
||||
* |
||||
* NOTE: empirically, about a decade ago it was determined that with more |
||||
* than two merge bases, optimal behavior was found when the |
||||
* merge_bases were passed in the order of oldest commit to newest |
||||
* commit. Also, merge_bases will be consumed (emptied) so make a |
||||
* copy if you need it. |
||||
* |
||||
* Outputs: |
||||
* - See RETURN VALUES above |
||||
* - If merge is clean, a commit is created and its address written to *result |
||||
* - opt->repo->index has the new index |
||||
* - $GIT_INDEX_FILE is not updated |
||||
* - The working tree is updated with results of the merge |
||||
*/ |
||||
int merge_recursive(struct merge_options *opt, |
||||
struct commit *h1, |
||||
struct commit *h2, |
||||
struct commit_list *ancestors, |
||||
struct commit_list *merge_bases, |
||||
struct commit **result); |
||||
|
||||
/* rename-detecting three-way merge, no recursion */ |
||||
int merge_trees(struct merge_options *o, |
||||
struct tree *head, |
||||
struct tree *merge, |
||||
struct tree *common, |
||||
struct tree **result); |
||||
|
||||
/* |
||||
* "git-merge-recursive" can be fed trees; wrap them into |
||||
* virtual commits and call merge_recursive() proper. |
||||
* merge_recursive_generic can operate on trees instead of commits, by |
||||
* wrapping the trees into virtual commits, and calling merge_recursive(). |
||||
* It also writes out the in-memory index to disk if the merge is successful. |
||||
* |
||||
* Outputs: |
||||
* - See RETURN VALUES above |
||||
* - If merge is clean, a commit is created and its address written to *result |
||||
* - opt->repo->index has the new index |
||||
* - $GIT_INDEX_FILE is updated |
||||
* - The working tree is updated with results of the merge |
||||
*/ |
||||
int merge_recursive_generic(struct merge_options *o, |
||||
int merge_recursive_generic(struct merge_options *opt, |
||||
const struct object_id *head, |
||||
const struct object_id *merge, |
||||
int num_ca, |
||||
const struct object_id **ca, |
||||
int num_merge_bases, |
||||
const struct object_id **merge_bases, |
||||
struct commit **result); |
||||
|
||||
void init_merge_options(struct merge_options *o, |
||||
struct repository *repo); |
||||
struct tree *write_tree_from_memory(struct merge_options *o); |
||||
|
||||
int parse_merge_opt(struct merge_options *out, const char *s); |
||||
|
||||
#endif |
||||
|
@ -0,0 +1,202 @@
@@ -0,0 +1,202 @@
|
||||
#!/bin/sh |
||||
|
||||
test_description='recursive merge diff3 style conflict markers' |
||||
|
||||
. ./test-lib.sh |
||||
|
||||
# Setup: |
||||
# L1 |
||||
# \ |
||||
# ? |
||||
# / |
||||
# R1 |
||||
# |
||||
# Where: |
||||
# L1 and R1 both have a file named 'content' but have no common history |
||||
# |
||||
|
||||
test_expect_success 'setup no merge base' ' |
||||
test_create_repo no_merge_base && |
||||
( |
||||
cd no_merge_base && |
||||
|
||||
git checkout -b L && |
||||
test_commit A content A && |
||||
|
||||
git checkout --orphan R && |
||||
test_commit B content B |
||||
) |
||||
' |
||||
|
||||
test_expect_success 'check no merge base' ' |
||||
( |
||||
cd no_merge_base && |
||||
|
||||
git checkout L^0 && |
||||
|
||||
test_must_fail git -c merge.conflictstyle=diff3 merge --allow-unrelated-histories -s recursive R^0 && |
||||
|
||||
grep "|||||| empty tree" content |
||||
) |
||||
' |
||||
|
||||
# Setup: |
||||
# L1 |
||||
# / \ |
||||
# master ? |
||||
# \ / |
||||
# R1 |
||||
# |
||||
# Where: |
||||
# L1 and R1 have modified the same file ('content') in conflicting ways |
||||
# |
||||
|
||||
test_expect_success 'setup unique merge base' ' |
||||
test_create_repo unique_merge_base && |
||||
( |
||||
cd unique_merge_base && |
||||
|
||||
test_commit base content "1 |
||||
2 |
||||
3 |
||||
4 |
||||
5 |
||||
" && |
||||
|
||||
git branch L && |
||||
git branch R && |
||||
|
||||
git checkout L && |
||||
test_commit L content "1 |
||||
2 |
||||
3 |
||||
4 |
||||
5 |
||||
7" && |
||||
|
||||
git checkout R && |
||||
git rm content && |
||||
test_commit R renamed "1 |
||||
2 |
||||
3 |
||||
4 |
||||
5 |
||||
six" |
||||
) |
||||
' |
||||
|
||||
test_expect_success 'check unique merge base' ' |
||||
( |
||||
cd unique_merge_base && |
||||
|
||||
git checkout L^0 && |
||||
MASTER=$(git rev-parse --short master) && |
||||
|
||||
test_must_fail git -c merge.conflictstyle=diff3 merge -s recursive R^0 && |
||||
|
||||
grep "|||||| $MASTER:content" renamed |
||||
) |
||||
' |
||||
|
||||
# Setup: |
||||
# L1---L2--L3 |
||||
# / \ / \ |
||||
# master X1 ? |
||||
# \ / \ / |
||||
# R1---R2--R3 |
||||
# |
||||
# Where: |
||||
# commits L1 and R1 have modified the same file in non-conflicting ways |
||||
# X1 is an auto-generated merge-base used when merging L1 and R1 |
||||
# commits L2 and R2 are merges of R1 and L1 into L1 and R1, respectively |
||||
# commits L3 and R3 both modify 'content' in conflicting ways |
||||
# |
||||
|
||||
test_expect_success 'setup multiple merge bases' ' |
||||
test_create_repo multiple_merge_bases && |
||||
( |
||||
cd multiple_merge_bases && |
||||
|
||||
test_commit initial content "1 |
||||
2 |
||||
3 |
||||
4 |
||||
5" && |
||||
|
||||
git branch L && |
||||
git branch R && |
||||
|
||||
# Create L1 |
||||
git checkout L && |
||||
test_commit L1 content "0 |
||||
1 |
||||
2 |
||||
3 |
||||
4 |
||||
5" && |
||||
|
||||
# Create R1 |
||||
git checkout R && |
||||
test_commit R1 content "1 |
||||
2 |
||||
3 |
||||
4 |
||||
5 |
||||
6" && |
||||
|
||||
# Create L2 |
||||
git checkout L && |
||||
git merge R1 && |
||||
|
||||
# Create R2 |
||||
git checkout R && |
||||
git merge L1 && |
||||
|
||||
# Create L3 |
||||
git checkout L && |
||||
test_commit L3 content "0 |
||||
1 |
||||
2 |
||||
3 |
||||
4 |
||||
5 |
||||
A" && |
||||
|
||||
# Create R3 |
||||
git checkout R && |
||||
git rm content && |
||||
test_commit R3 renamed "0 |
||||
2 |
||||
3 |
||||
4 |
||||
5 |
||||
six" |
||||
) |
||||
' |
||||
|
||||
test_expect_success 'check multiple merge bases' ' |
||||
( |
||||
cd multiple_merge_bases && |
||||
|
||||
git checkout L^0 && |
||||
|
||||
test_must_fail git -c merge.conflictstyle=diff3 merge -s recursive R^0 && |
||||
|
||||
grep "|||||| merged common ancestors:content" renamed |
||||
) |
||||
' |
||||
|
||||
test_expect_success 'rebase describes fake ancestor base' ' |
||||
test_create_repo rebase && |
||||
( |
||||
cd rebase && |
||||
test_commit base file && |
||||
test_commit master file && |
||||
git checkout -b side HEAD^ && |
||||
test_commit side file && |
||||
test_must_fail git -c merge.conflictstyle=diff3 rebase master && |
||||
grep "||||||| constructed merge base" file |
||||
) |
||||
' |
||||
|
||||
test_done |
Loading…
Reference in new issue