diff options
72 files changed, 1499 insertions, 307 deletions
diff --git a/Documentation/RelNotes/2.32.0.txt b/Documentation/RelNotes/2.32.0.txt index 5c329d5a1b..c746ad11c9 100644 --- a/Documentation/RelNotes/2.32.0.txt +++ b/Documentation/RelNotes/2.32.0.txt @@ -57,6 +57,23 @@ UI, Workflows & Features * "git clone --reject-shallow" option fails the clone as soon as we notice that we are cloning from a shallow repository. + * A configuration variable has been added to force tips of certain + refs to be given a reachability bitmap. + + * "gitweb" learned "e-mail privacy" feature to redact strings that + look like e-mail addresses on various pages. + + * "git apply --3way" has always been "to fall back to 3-way merge + only when straight application fails". Swap the order of falling + back so that 3-way is always attempted first (only when the option + is given, of course) and then straight patch application is used as + a fallback when it fails. + + * "git apply" now takes "--3way" and "--cached" at the same time, and + work and record results only in the index. + + * The command line completion (in contrib/) has learned that + CHERRY_PICK_HEAD is a possible pseudo-ref. Performance, Internal Implementation, Development Support etc. @@ -98,6 +115,17 @@ Performance, Internal Implementation, Development Support etc. * Generate [ec]tags under $(QUIET_GEN). + * Clean-up codepaths that implements "git send-email --validate" + option and improves the message from it. + + * The last remnant of gettext-poison has been removed. + + * The test framework has been taught to optionally turn the default + merge strategy to "ort" throughout the system where we use + three-way merges internally, like cherry-pick, rebase etc., + primarily to enhance its test coverage (the strategy has been + available as an explicit "-s ort" choice). + Fixes since v2.31 ----------------- @@ -174,6 +202,19 @@ Fixes since v2.31 as directory separator. (merge 9a7f1ce8b7 rs/daemon-sanitize-dir-sep later to maint). + * A NULL-dereference bug has been corrected in an error codepath in + "git for-each-ref", "git branch --list" etc. + (merge c685450880 jk/ref-filter-segfault-fix later to maint). + + * Streamline the codepath to fix the UTF-8 encoding issues in the + argv[] and the prefix on macOS. + (merge c7d0e61016 tb/precompose-prefix-simplify later to maint). + + * The command-line completion script (in contrib/) had a couple of + references that would have given a warning under the "-u" (nounset) + option. + (merge c5c0548d79 vs/completion-with-set-u later to maint). + * Other code cleanup, docfix, build fix, etc. (merge f451960708 dl/cat-file-doc-cleanup later to maint). (merge 12604a8d0c sv/t9801-test-path-is-file-cleanup later to maint). @@ -186,3 +227,5 @@ Fixes since v2.31 (merge 2be927f3d1 ab/diff-no-index-tests later to maint). (merge 76593c09bb ab/detox-gettext-tests later to maint). (merge 28e29ee38b jc/doc-format-patch-clarify later to maint). + (merge fc12b6fdde fm/user-manual-use-preface later to maint). + (merge dba94e3a85 cc/test-helper-bloom-usage-fix later to maint). diff --git a/Documentation/config/pack.txt b/Documentation/config/pack.txt index 3da4ea98e2..c0844d8d8e 100644 --- a/Documentation/config/pack.txt +++ b/Documentation/config/pack.txt @@ -122,6 +122,21 @@ pack.useSparse:: commits contain certain types of direct renames. Default is `true`. +pack.preferBitmapTips:: + When selecting which commits will receive bitmaps, prefer a + commit at the tip of any reference that is a suffix of any value + of this configuration over any other commits in the "selection + window". ++ +Note that setting this configuration to `refs/foo` does not mean that +the commits at the tips of `refs/foo/bar` and `refs/foo/baz` will +necessarily be selected. This is because commits are selected for +bitmaps from within a series of windows of variable length. ++ +If a commit at the tip of any reference which is a suffix of any value +of this configuration is seen in a window, it is immediately given +preference over any other commit in that window. + pack.writeBitmaps (deprecated):: This is a deprecated synonym for `repack.writeBitmaps`. diff --git a/Documentation/git-apply.txt b/Documentation/git-apply.txt index 91d9a8601c..aa1ae56a25 100644 --- a/Documentation/git-apply.txt +++ b/Documentation/git-apply.txt @@ -84,12 +84,13 @@ OPTIONS -3:: --3way:: - When the patch does not apply cleanly, fall back on 3-way merge if - the patch records the identity of blobs it is supposed to apply to, - and we have those blobs available locally, possibly leaving the + Attempt 3-way merge if the patch records the identity of blobs it is supposed + to apply to and we have those blobs available locally, possibly leaving the conflict markers in the files in the working tree for the user to - resolve. This option implies the `--index` option, and is incompatible - with the `--reject` and the `--cached` options. + resolve. This option implies the `--index` option unless the + `--cached` option is used, and is incompatible with the `--reject` option. + When used with the `--cached` option, any conflicts are left at higher stages + in the cache. --build-fake-ancestor=<file>:: Newer 'git diff' output has embedded 'index information' diff --git a/Documentation/gitattributes.txt b/Documentation/gitattributes.txt index 0a60472bb5..cfcfa800c2 100644 --- a/Documentation/gitattributes.txt +++ b/Documentation/gitattributes.txt @@ -845,6 +845,8 @@ patterns are available: - `rust` suitable for source code in the Rust language. +- `scheme` suitable for source code in the Scheme language. + - `tex` suitable for source code for LaTeX documents. diff --git a/Documentation/gitweb.conf.txt b/Documentation/gitweb.conf.txt index 7963a79ba9..34b1d6e224 100644 --- a/Documentation/gitweb.conf.txt +++ b/Documentation/gitweb.conf.txt @@ -751,6 +751,17 @@ default font sizes or lineheights are changed (e.g. via adding extra CSS stylesheet in `@stylesheets`), it may be appropriate to change these values. +email-privacy:: + Redact e-mail addresses from the generated HTML, etc. content. + This obscures e-mail addresses retrieved from the author/committer + and comment sections of the Git log. + It is meant to hinder web crawlers that harvest and abuse addresses. + Such crawlers may not respect robots.txt. + Note that users and user tools also see the addresses as redacted. + If Gitweb is not the final step in a workflow then subsequent steps + may misbehave because of the redacted information they receive. + Disabled by default. + highlight:: Server-side syntax highlight support in "blob" view. It requires `$highlight_bin` program to be available (see the description of diff --git a/Documentation/technical/reftable.txt b/Documentation/technical/reftable.txt index 3ef169af27..d7c3b645cf 100644 --- a/Documentation/technical/reftable.txt +++ b/Documentation/technical/reftable.txt @@ -1011,8 +1011,13 @@ reftable stack, reload `tables.list`, and delete any tables no longer mentioned in `tables.list`. Irregular program exit may still leave about unused files. In this case, a -cleanup operation can read `tables.list`, note its modification timestamp, and -delete any unreferenced `*.ref` files that are older. +cleanup operation should proceed as follows: + +* take a lock `tables.list.lock` to prevent concurrent modifications +* refresh the reftable stack, by reading `tables.list` +* for each `*.ref` file, remove it if +** it is not mentioned in `tables.list`, and +** its max update_index is not beyond the max update_index of the stack Alternatives considered diff --git a/Documentation/user-manual.txt b/Documentation/user-manual.txt index fd480b8645..f9e54b8674 100644 --- a/Documentation/user-manual.txt +++ b/Documentation/user-manual.txt @@ -1,5 +1,8 @@ = Git User Manual +[preface] +== Introduction + Git is a fast distributed revision control system. This manual is designed to be readable by someone with basic UNIX @@ -693,6 +693,7 @@ X = PROGRAMS += $(patsubst %.o,git-%$X,$(PROGRAM_OBJS)) TEST_BUILTINS_OBJS += test-advise.o +TEST_BUILTINS_OBJS += test-bitmap.o TEST_BUILTINS_OBJS += test-bloom.o TEST_BUILTINS_OBJS += test-chmtime.o TEST_BUILTINS_OBJS += test-config.o @@ -752,6 +753,7 @@ TEST_BUILTINS_OBJS += test-submodule-nested-repo-config.o TEST_BUILTINS_OBJS += test-subprocess.o TEST_BUILTINS_OBJS += test-trace2.o TEST_BUILTINS_OBJS += test-urlmatch-normalization.o +TEST_BUILTINS_OBJS += test-userdiff.o TEST_BUILTINS_OBJS += test-wildmatch.o TEST_BUILTINS_OBJS += test-windows-named-pipe.o TEST_BUILTINS_OBJS += test-write-cache.o @@ -134,8 +134,6 @@ int check_apply_state(struct apply_state *state, int force_apply) if (state->apply_with_reject && state->threeway) return error(_("--reject and --3way cannot be used together.")); - if (state->cached && state->threeway) - return error(_("--cached and --3way cannot be used together.")); if (state->threeway) { if (is_not_gitdir) return error(_("--3way outside a repository")); @@ -3570,10 +3568,10 @@ static int try_threeway(struct apply_state *state, write_object_file("", 0, blob_type, &pre_oid); else if (get_oid(patch->old_oid_prefix, &pre_oid) || read_blob_object(&buf, &pre_oid, patch->old_mode)) - return error(_("repository lacks the necessary blob to fall back on 3-way merge.")); + return error(_("repository lacks the necessary blob to perform 3-way merge.")); if (state->apply_verbosity > verbosity_silent) - fprintf(stderr, _("Falling back to three-way merge...\n")); + fprintf(stderr, _("Performing three-way merge...\n")); img = strbuf_detach(&buf, &len); prepare_image(&tmp_image, img, len, 1); @@ -3605,7 +3603,7 @@ static int try_threeway(struct apply_state *state, if (status < 0) { if (state->apply_verbosity > verbosity_silent) fprintf(stderr, - _("Failed to fall back on three-way merge...\n")); + _("Failed to perform three-way merge...\n")); return status; } @@ -3638,10 +3636,9 @@ static int apply_data(struct apply_state *state, struct patch *patch, if (load_preimage(state, &image, patch, st, ce) < 0) return -1; - if (patch->direct_to_threeway || - apply_fragments(state, &image, patch) < 0) { + if (!state->threeway || try_threeway(state, &image, patch, st, ce) < 0) { /* Note: with --reject, apply_fragments() returns 0 */ - if (!state->threeway || try_threeway(state, &image, patch, st, ce) < 0) + if (patch->direct_to_threeway || apply_fragments(state, &image, patch) < 0) return -1; } patch->result = image.buf; @@ -4647,7 +4644,12 @@ static int write_out_results(struct apply_state *state, struct patch *list) } string_list_clear(&cpath, 0); - repo_rerere(state->repo, 0); + /* + * rerere relies on the partially merged result being in the working + * tree with conflict markers, but that isn't written with --cached. + */ + if (!state->cached) + repo_rerere(state->repo, 0); } return errs; @@ -5018,7 +5020,7 @@ int apply_parse_options(int argc, const char **argv, OPT_BOOL(0, "apply", force_apply, N_("also apply the patch (use with --stat/--summary/--check)")), OPT_BOOL('3', "3way", &state->threeway, - N_( "attempt three-way merge if a patch does not apply")), + N_( "attempt three-way merge, fall back on normal patch if that fails")), OPT_FILENAME(0, "build-fake-ancestor", &state->fake_ancestor, N_("build a temporary index based on embedded index information")), /* Think twice before adding "--nul" synonym to this */ @@ -344,6 +344,7 @@ void remove_merge_branch_state(struct repository *r) unlink(git_path_merge_rr(r)); unlink(git_path_merge_msg(r)); unlink(git_path_merge_mode(r)); + unlink(git_path_auto_merge(r)); save_autostash(git_path_merge_autostash(r)); } diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c index 525c2d8552..247a08d024 100644 --- a/builtin/pack-objects.c +++ b/builtin/pack-objects.c @@ -3527,7 +3527,8 @@ static int get_object_list_from_bitmap(struct rev_info *revs) &reuse_packfile_bitmap)) { assert(reuse_packfile_objects); nr_result += reuse_packfile_objects; - display_progress(progress_state, nr_result); + nr_seen += reuse_packfile_objects; + display_progress(progress_state, nr_seen); } traverse_bitmap_commit_list(bitmap_git, revs, @@ -3547,6 +3548,37 @@ static void record_recent_commit(struct commit *commit, void *data) oid_array_append(&recent_objects, &commit->object.oid); } +static int mark_bitmap_preferred_tip(const char *refname, + const struct object_id *oid, int flags, + void *_data) +{ + struct object_id peeled; + struct object *object; + + if (!peel_iterated_oid(oid, &peeled)) + oid = &peeled; + + object = parse_object_or_die(oid, refname); + if (object->type == OBJ_COMMIT) + object->flags |= NEEDS_BITMAP; + + return 0; +} + +static void mark_bitmap_preferred_tips(void) +{ + struct string_list_item *item; + const struct string_list *preferred_tips; + + preferred_tips = bitmap_preferred_tips(the_repository); + if (!preferred_tips) + return; + + for_each_string_list_item(item, preferred_tips) { + for_each_ref_in(item->string, mark_bitmap_preferred_tip, NULL); + } +} + static void get_object_list(int ac, const char **av) { struct rev_info revs; @@ -3601,6 +3633,9 @@ static void get_object_list(int ac, const char **av) if (use_delta_islands) load_delta_islands(the_repository, progress); + if (write_bitmap_index) + mark_bitmap_preferred_tips(); + if (prepare_revision_walk(&revs)) die(_("revision walk setup failed")); mark_edges_uninteresting(&revs, show_edge, sparse); diff --git a/builtin/rebase.c b/builtin/rebase.c index 783b526f6e..ed1da1760e 100644 --- a/builtin/rebase.c +++ b/builtin/rebase.c @@ -738,6 +738,7 @@ static int finish_rebase(struct rebase_options *opts) int ret = 0; delete_ref(NULL, "REBASE_HEAD", NULL, REF_NO_DEREF); + unlink(git_path_auto_merge(the_repository)); apply_autostash(state_dir_path("autostash", opts)); close_object_store(the_repository->objects); /* diff --git a/ci/run-build-and-tests.sh b/ci/run-build-and-tests.sh index a66b5e8c75..d19be40544 100755 --- a/ci/run-build-and-tests.sh +++ b/ci/run-build-and-tests.sh @@ -16,6 +16,7 @@ linux-gcc) export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main make test export GIT_TEST_SPLIT_INDEX=yes + export GIT_TEST_MERGE_ALGORITHM=recursive export GIT_TEST_FULL_IN_PACK_ARRAY=true export GIT_TEST_OE_SIZE=10 export GIT_TEST_OE_DELTA_SIZE=5 diff --git a/compat/precompose_utf8.c b/compat/precompose_utf8.c index ec560565a8..cce1d57a46 100644 --- a/compat/precompose_utf8.c +++ b/compat/precompose_utf8.c @@ -60,10 +60,12 @@ void probe_utf8_pathname_composition(void) strbuf_release(&path); } -static inline const char *precompose_string_if_needed(const char *in) +const char *precompose_string_if_needed(const char *in) { size_t inlen; size_t outlen; + if (!in) + return NULL; if (has_non_ascii(in, (size_t)-1, &inlen)) { iconv_t ic_prec; char *out; @@ -96,10 +98,7 @@ const char *precompose_argv_prefix(int argc, const char **argv, const char *pref argv[i] = precompose_string_if_needed(argv[i]); i++; } - if (prefix) { - prefix = precompose_string_if_needed(prefix); - } - return prefix; + return precompose_string_if_needed(prefix); } diff --git a/compat/precompose_utf8.h b/compat/precompose_utf8.h index d70b84665c..fea06cf28a 100644 --- a/compat/precompose_utf8.h +++ b/compat/precompose_utf8.h @@ -29,6 +29,7 @@ typedef struct { } PREC_DIR; const char *precompose_argv_prefix(int argc, const char **argv, const char *prefix); +const char *precompose_string_if_needed(const char *in); void probe_utf8_pathname_composition(void); PREC_DIR *precompose_utf8_opendir(const char *dirname); @@ -1180,20 +1180,6 @@ static void die_bad_number(const char *name, const char *value) } } -NORETURN -static void die_bad_bool(const char *name, const char *value) -{ - if (!strcmp(name, "GIT_TEST_GETTEXT_POISON")) - /* - * We explicitly *don't* use _() here since it would - * cause an infinite loop with _() needing to call - * use_gettext_poison(). - */ - die("bad boolean config value '%s' for '%s'", value, name); - else - die(_("bad boolean config value '%s' for '%s'"), value, name); -} - int git_config_int(const char *name, const char *value) { int ret; @@ -1268,7 +1254,7 @@ int git_config_bool(const char *name, const char *value) { int v = git_parse_maybe_bool(value); if (v < 0) - die_bad_bool(name, value); + die(_("bad boolean config value '%s' for '%s'"), value, name); return v; } diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash index e1a66954fe..dfa735ea62 100644 --- a/contrib/completion/git-completion.bash +++ b/contrib/completion/git-completion.bash @@ -77,7 +77,7 @@ __git_find_repo_path () test -d "$__git_dir" && __git_repo_path="$__git_dir" elif [ -n "${GIT_DIR-}" ]; then - test -d "${GIT_DIR-}" && + test -d "$GIT_DIR" && __git_repo_path="$GIT_DIR" elif [ -d .git ]; then __git_repo_path=.git @@ -427,7 +427,7 @@ __gitcomp_builtin () if [ -z "$options" ]; then local completion_helper - if [ "$GIT_COMPLETION_SHOW_ALL" = "1" ]; then + if [ "${GIT_COMPLETION_SHOW_ALL-}" = "1" ]; then completion_helper="--git-completion-helper-all" else completion_helper="--git-completion-helper" @@ -744,7 +744,7 @@ __git_refs () track="" ;; *) - for i in HEAD FETCH_HEAD ORIG_HEAD MERGE_HEAD REBASE_HEAD; do + for i in HEAD FETCH_HEAD ORIG_HEAD MERGE_HEAD REBASE_HEAD CHERRY_PICK_HEAD; do case "$i" in $match*) if [ -e "$dir/$i" ]; then @@ -1910,7 +1910,7 @@ _git_help () return ;; esac - if test -n "$GIT_TESTING_ALL_COMMAND_LIST" + if test -n "${GIT_TESTING_ALL_COMMAND_LIST-}" then __gitcomp "$GIT_TESTING_ALL_COMMAND_LIST $(__git --list-cmds=alias,list-guide) gitk" else diff --git a/diffcore-rename.c b/diffcore-rename.c index 36a98f9c49..963ca58221 100644 --- a/diffcore-rename.c +++ b/diffcore-rename.c @@ -371,7 +371,7 @@ struct dir_rename_info { struct strintmap idx_map; struct strmap dir_rename_guess; struct strmap *dir_rename_count; - struct strset *relevant_source_dirs; + struct strintmap *relevant_source_dirs; unsigned setup; }; @@ -407,6 +407,28 @@ static const char *get_highest_rename_path(struct strintmap *counts) return highest_destination_dir; } +static char *UNKNOWN_DIR = "/"; /* placeholder -- short, illegal directory */ + +static int dir_rename_already_determinable(struct strintmap *counts) +{ + struct hashmap_iter iter; + struct strmap_entry *entry; + int first = 0, second = 0, unknown = 0; + strintmap_for_each_entry(counts, &iter, entry) { + const char *destination_dir = entry->key; + intptr_t count = (intptr_t)entry->value; + if (!strcmp(destination_dir, UNKNOWN_DIR)) { + unknown = count; + } else if (count >= first) { + second = first; + first = count; + } else if (count >= second) { + second = count; + } + } + return first > second + unknown; +} + static void increment_count(struct dir_rename_info *info, char *old_dir, char *new_dir) @@ -429,7 +451,7 @@ static void increment_count(struct dir_rename_info *info, } static void update_dir_rename_counts(struct dir_rename_info *info, - struct strset *dirs_removed, + struct strintmap *dirs_removed, const char *oldname, const char *newname) { @@ -461,10 +483,12 @@ static void update_dir_rename_counts(struct dir_rename_info *info, return; while (1) { + int drd_flag = NOT_RELEVANT; + /* Get old_dir, skip if its directory isn't relevant. */ dirname_munge(old_dir); if (info->relevant_source_dirs && - !strset_contains(info->relevant_source_dirs, old_dir)) + !strintmap_contains(info->relevant_source_dirs, old_dir)) break; /* Get new_dir */ @@ -509,16 +533,31 @@ static void update_dir_rename_counts(struct dir_rename_info *info, } } - if (strset_contains(dirs_removed, old_dir)) + /* + * Above we suggested that we'd keep recording renames for + * all ancestor directories where the trailing directories + * matched, i.e. for + * "a/b/c/d/e/foo.c" -> "a/b/some/thing/else/e/foo.c" + * we'd increment rename counts for each of + * a/b/c/d/e/ => a/b/some/thing/else/e/ + * a/b/c/d/ => a/b/some/thing/else/ + * However, we only need the rename counts for directories + * in dirs_removed whose value is RELEVANT_FOR_SELF. + * However, we add one special case of also recording it for + * first_time_in_loop because find_basename_matches() can + * use that as a hint to find a good pairing. + */ + if (dirs_removed) + drd_flag = strintmap_get(dirs_removed, old_dir); + if (drd_flag == RELEVANT_FOR_SELF || first_time_in_loop) increment_count(info, old_dir, new_dir); - else - break; + first_time_in_loop = 0; + if (drd_flag == NOT_RELEVANT) + break; /* If we hit toplevel directory ("") for old or new dir, quit */ if (!*old_dir || !*new_dir) break; - - first_time_in_loop = 0; } /* Free resources we don't need anymore */ @@ -527,8 +566,8 @@ static void update_dir_rename_counts(struct dir_rename_info *info, } static void initialize_dir_rename_info(struct dir_rename_info *info, - struct strset *relevant_sources, - struct strset *dirs_removed, + struct strintmap *relevant_sources, + struct strintmap *dirs_removed, struct strmap *dir_rename_count) { struct hashmap_iter iter; @@ -555,12 +594,13 @@ static void initialize_dir_rename_info(struct dir_rename_info *info, info->relevant_source_dirs = dirs_removed; /* might be NULL */ } else { info->relevant_source_dirs = xmalloc(sizeof(struct strintmap)); - strset_init(info->relevant_source_dirs); - strset_for_each_entry(relevant_sources, &iter, entry) { + strintmap_init(info->relevant_source_dirs, 0 /* unused */); + strintmap_for_each_entry(relevant_sources, &iter, entry) { char *dirname = get_dirname(entry->key); if (!dirs_removed || - strset_contains(dirs_removed, dirname)) - strset_add(info->relevant_source_dirs, dirname); + strintmap_contains(dirs_removed, dirname)) + strintmap_set(info->relevant_source_dirs, + dirname, 0 /* value irrelevant */); free(dirname); } } @@ -624,7 +664,7 @@ void partial_clear_dir_rename_count(struct strmap *dir_rename_count) } static void cleanup_dir_rename_info(struct dir_rename_info *info, - struct strset *dirs_removed, + struct strintmap *dirs_removed, int keep_dir_rename_count) { struct hashmap_iter iter; @@ -644,7 +684,7 @@ static void cleanup_dir_rename_info(struct dir_rename_info *info, /* relevant_source_dirs */ if (info->relevant_source_dirs && info->relevant_source_dirs != dirs_removed) { - strset_clear(info->relevant_source_dirs); + strintmap_clear(info->relevant_source_dirs); FREE_AND_NULL(info->relevant_source_dirs); } @@ -659,18 +699,22 @@ static void cleanup_dir_rename_info(struct dir_rename_info *info, /* * Although dir_rename_count was passed in * diffcore_rename_extended() and we want to keep it around and - * return it to that caller, we first want to remove any data + * return it to that caller, we first want to remove any counts in + * the maps associated with UNKNOWN_DIR entries and any data * associated with directories that weren't renamed. */ strmap_for_each_entry(info->dir_rename_count, &iter, entry) { const char *source_dir = entry->key; struct strintmap *counts = entry->value; - if (!strset_contains(dirs_removed, source_dir)) { + if (!strintmap_get(dirs_removed, source_dir)) { string_list_append(&to_remove, source_dir); strintmap_clear(counts); continue; } + + if (strintmap_contains(counts, UNKNOWN_DIR)) + strintmap_remove(counts, UNKNOWN_DIR); } for (i = 0; i < to_remove.nr; ++i) strmap_remove(info->dir_rename_count, @@ -770,8 +814,8 @@ static int idx_possible_rename(char *filename, struct dir_rename_info *info) static int find_basename_matches(struct diff_options *options, int minimum_score, struct dir_rename_info *info, - struct strset *relevant_sources, - struct strset *dirs_removed) + struct strintmap *relevant_sources, + struct strintmap *dirs_removed) { /* * When I checked in early 2020, over 76% of file renames in linux @@ -863,7 +907,7 @@ static int find_basename_matches(struct diff_options *options, /* Skip irrelevant sources */ if (relevant_sources && - !strset_contains(relevant_sources, filename)) + !strintmap_contains(relevant_sources, filename)) continue; /* @@ -994,7 +1038,7 @@ static int find_renames(struct diff_score *mx, int minimum_score, int copies, struct dir_rename_info *info, - struct strset *dirs_removed) + struct strintmap *dirs_removed) { int count = 0, i; @@ -1019,7 +1063,7 @@ static int find_renames(struct diff_score *mx, } static void remove_unneeded_paths_from_src(int detecting_copies, - struct strset *interesting) + struct strintmap *interesting) { int i, new_num_src; @@ -1061,7 +1105,7 @@ static void remove_unneeded_paths_from_src(int detecting_copies, continue; /* If we don't care about the source path, skip it */ - if (interesting && !strset_contains(interesting, one->path)) + if (interesting && !strintmap_contains(interesting, one->path)) continue; if (new_num_src < i) @@ -1073,9 +1117,136 @@ static void remove_unneeded_paths_from_src(int detecting_copies, rename_src_nr = new_num_src; } +static void handle_early_known_dir_renames(struct dir_rename_info *info, + struct strintmap *relevant_sources, + struct strintmap *dirs_removed) +{ + /* + * Directory renames are determined via an aggregate of all renames + * under them and using a "majority wins" rule. The fact that + * "majority wins", though, means we don't need all the renames + * under the given directory, we only need enough to ensure we have + * a majority. + */ + + int i, new_num_src; + struct hashmap_iter iter; + struct strmap_entry *entry; + + if (!dirs_removed || !relevant_sources) + return; /* nothing to cull */ + if (break_idx) + return; /* culling incompatbile with break detection */ + + /* + * Supplement dir_rename_count with number of potential renames, + * marking all potential rename sources as mapping to UNKNOWN_DIR. + */ + for (i = 0; i < rename_src_nr; i++) { + char *old_dir; + struct diff_filespec *one = rename_src[i].p->one; + + /* + * sources that are part of a rename will have already been + * removed by a prior call to remove_unneeded_paths_from_src() + */ + assert(!one->rename_used); + + old_dir = get_dirname(one->path); + while (*old_dir != '\0' && + NOT_RELEVANT != strintmap_get(dirs_removed, old_dir)) { + char *freeme = old_dir; + + increment_count(info, old_dir, UNKNOWN_DIR); + old_dir = get_dirname(old_dir); + + /* Free resources we don't need anymore */ + free(freeme); + } + /* + * old_dir and new_dir free'd in increment_count, but + * get_dirname() gives us a new pointer we need to free for + * old_dir. Also, if the loop runs 0 times we need old_dir + * to be freed. + */ + free(old_dir); + } + + /* + * For any directory which we need a potential rename detected for + * (i.e. those marked as RELEVANT_FOR_SELF in dirs_removed), check + * whether we have enough renames to satisfy the "majority rules" + * requirement such that detecting any more renames of files under + * it won't change the result. For any such directory, mark that + * we no longer need to detect a rename for it. However, since we + * might need to still detect renames for an ancestor of that + * directory, use RELEVANT_FOR_ANCESTOR. + */ + strmap_for_each_entry(info->dir_rename_count, &iter, entry) { + /* entry->key is source_dir */ + struct strintmap *counts = entry->value; + + if (strintmap_get(dirs_removed, entry->key) == + RELEVANT_FOR_SELF && + dir_rename_already_determinable(counts)) { + strintmap_set(dirs_removed, entry->key, + RELEVANT_FOR_ANCESTOR); + } + } + + for (i = 0, new_num_src = 0; i < rename_src_nr; i++) { + struct diff_filespec *one = rename_src[i].p->one; + int val; + + val = strintmap_get(relevant_sources, one->path); + + /* + * sources that were not found in relevant_sources should + * have already been removed by a prior call to + * remove_unneeded_paths_from_src() + */ + assert(val != -1); + + if (val == RELEVANT_LOCATION) { + int removable = 1; + char *dir = get_dirname(one->path); + while (1) { + char *freeme = dir; + int res = strintmap_get(dirs_removed, dir); + + /* Quit if not found or irrelevant */ + if (res == NOT_RELEVANT) + break; + /* If RELEVANT_FOR_SELF, can't remove */ + if (res == RELEVANT_FOR_SELF) { + removable = 0; + break; + } + /* Else continue searching upwards */ + assert(res == RELEVANT_FOR_ANCESTOR); + dir = get_dirname(dir); + free(freeme); + } + free(dir); + if (removable) { + strintmap_set(relevant_sources, one->path, + RELEVANT_NO_MORE); + continue; + } + } + + if (new_num_src < i) + memcpy(&rename_src[new_num_src], &rename_src[i], + sizeof(struct diff_rename_src)); + new_num_src++; + } + + rename_src_nr = new_num_src; +} + void diffcore_rename_extended(struct diff_options *options, - struct strset *relevant_sources, - struct strset *dirs_removed, + struct strintmap *relevant_sources, + struct strintmap *dirs_removed, struct strmap *dir_rename_count) { int detect_rename = options->detect_rename; @@ -1208,9 +1379,16 @@ void diffcore_rename_extended(struct diff_options *options, * Cull sources, again: * - remove ones involved in renames (found via basenames) * - remove ones not found in relevant_sources + * and + * - remove ones in relevant_sources which are needed only + * for directory renames IF no ancestory directory + * actually needs to know any more individual path + * renames under them */ trace2_region_enter("diff", "cull basename", options->repo); remove_unneeded_paths_from_src(want_copies, relevant_sources); + handle_early_known_dir_renames(&info, relevant_sources, + dirs_removed); trace2_region_leave("diff", "cull basename", options->repo); } diff --git a/diffcore.h b/diffcore.h index d76982f220..f5c6de4841 100644 --- a/diffcore.h +++ b/diffcore.h @@ -8,8 +8,8 @@ struct diff_options; struct repository; +struct strintmap; struct strmap; -struct strset; struct userdiff_driver; /* This header file is internal between diff.c and its diff transformers @@ -161,13 +161,26 @@ struct diff_filepair *diff_queue(struct diff_queue_struct *, struct diff_filespec *); void diff_q(struct diff_queue_struct *, struct diff_filepair *); +/* dir_rename_relevance: the reason we want rename information for a dir */ +enum dir_rename_relevance { + NOT_RELEVANT = 0, + RELEVANT_FOR_ANCESTOR = 1, + RELEVANT_FOR_SELF = 2 +}; +/* file_rename_relevance: the reason(s) we want rename information for a file */ +enum file_rename_relevance { + RELEVANT_NO_MORE = 0, /* i.e. NOT relevant */ + RELEVANT_CONTENT = 1, + RELEVANT_LOCATION = 2 +}; + void partial_clear_dir_rename_count(struct strmap *dir_rename_count); void diffcore_break(struct repository *, int); void diffcore_rename(struct diff_options *); void diffcore_rename_extended(struct diff_options *options, - struct strset *relevant_sources, - struct strset *dirs_removed, + struct strintmap *relevant_sources, + struct strintmap *dirs_removed, struct strmap *dir_rename_count); void diffcore_merge_broken(void); void diffcore_pickaxe(struct diff_options *); diff --git a/fetch-pack.c b/fetch-pack.c index 6e68276aa3..2318ebe680 100644 --- a/fetch-pack.c +++ b/fetch-pack.c @@ -1251,7 +1251,7 @@ static int send_fetch_request(struct fetch_negotiator *negotiator, int fd_out, if (hash_algo_by_ptr(the_hash_algo) != hash_algo) die(_("mismatched algorithms: client %s; server %s"), the_hash_algo->name, hash_name); - packet_write_fmt(fd_out, "object-format=%s", the_hash_algo->name); + packet_buf_write(&req_buf, "object-format=%s", the_hash_algo->name); } else if (hash_algo_by_ptr(the_hash_algo) != GIT_HASH_SHA1) { die(_("the server does not support algorithm '%s'"), the_hash_algo->name); diff --git a/git-compat-util.h b/git-compat-util.h index 9ddf9d7044..a508dbe5a3 100644 --- a/git-compat-util.h +++ b/git-compat-util.h @@ -256,6 +256,11 @@ static inline const char *precompose_argv_prefix(int argc, const char **argv, co { return prefix; } +static inline const char *precompose_string_if_needed(const char *in) +{ + return in; +} + #define probe_utf8_pathname_composition() #endif diff --git a/git-send-email.perl b/git-send-email.perl index f5bbf1647e..175da07d94 100755 --- a/git-send-email.perl +++ b/git-send-email.perl @@ -212,22 +212,31 @@ my $dump_aliases = 0; my $multiedit; my $editor; +sub system_or_msg { + my ($args, $msg) = @_; + system(@$args); + my $signalled = $? & 127; + my $exit_code = $? >> 8; + return unless $signalled or $exit_code; + + return sprintf(__("fatal: command '%s' died with exit code %d"), + $args->[0], $exit_code); +} + +sub system_or_die { + my $msg = system_or_msg(@_); + die $msg if $msg; +} + sub do_edit { if (!defined($editor)) { $editor = Git::command_oneline('var', 'GIT_EDITOR'); } + my $die_msg = __("the editor exited uncleanly, aborting everything"); if (defined($multiedit) && !$multiedit) { - map { - system('sh', '-c', $editor.' "$@"', $editor, $_); - if (($? & 127) || ($? >> 8)) { - die(__("the editor exited uncleanly, aborting everything")); - } - } @_; + system_or_die(['sh', '-c', $editor.' "$@"', $editor, $_], $die_msg) for @_; } else { - system('sh', '-c', $editor.' "$@"', $editor, @_); - if (($? & 127) || ($? >> 8)) { - die(__("the editor exited uncleanly, aborting everything")); - } + system_or_die(['sh', '-c', $editor.' "$@"', $editor, @_], $die_msg); } } @@ -698,9 +707,7 @@ if (@rev_list_opts) { if ($validate) { foreach my $f (@files) { unless (-p $f) { - my $error = validate_patch($f, $target_xfer_encoding); - $error and die sprintf(__("fatal: %s: %s\nwarning: no patches were sent\n"), - $f, $error); + validate_patch($f, $target_xfer_encoding); } } } @@ -1952,11 +1959,14 @@ sub validate_patch { chdir($repo->wc_path() or $repo->repo_path()) or die("chdir: $!"); local $ENV{"GIT_DIR"} = $repo->repo_path(); - $hook_error = "rejected by sendemail-validate hook" - if system($validate_hook, $target); + $hook_error = system_or_msg([$validate_hook, $target]); chdir($cwd_save) or die("chdir: $!"); } - return $hook_error if $hook_error; + if ($hook_error) { + die sprintf(__("fatal: %s: rejected by sendemail-validate hook\n" . + "%s\n" . + "warning: no patches were sent\n"), $fn, $hook_error); + } } # Any long lines will be automatically fixed if we use a suitable transfer @@ -1966,7 +1976,8 @@ sub validate_patch { or die sprintf(__("unable to open %s: %s\n"), $fn, $!); while (my $line = <$fh>) { if (length($line) > 998) { - return sprintf(__("%s: patch contains a line longer than 998 characters"), $.); + die sprintf(__("fatal: %s:%d is longer than 998 characters\n" . + "warning: no patches were sent\n"), $fn, $.); } } } @@ -423,7 +423,7 @@ static int run_builtin(struct cmd_struct *p, int argc, const char **argv) int nongit_ok; prefix = setup_git_directory_gently(&nongit_ok); } - prefix = precompose_argv_prefix(argc, argv, prefix); + precompose_argv_prefix(argc, argv, NULL); if (use_pager == -1 && p->option & (RUN_SETUP | RUN_SETUP_GENTLY) && !(p->option & DELAY_PAGER_CONFIG)) use_pager = check_pager_config(p->cmd); diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index 0959a782ec..e09e024a09 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -569,6 +569,15 @@ our %feature = ( 'sub' => \&feature_extra_branch_refs, 'override' => 0, 'default' => []}, + + # Redact e-mail addresses. + + # To enable system wide have in $GITWEB_CONFIG + # $feature{'email-privacy'}{'default'} = [1]; + 'email-privacy' => { + 'sub' => sub { feature_bool('email-privacy', @_) }, + 'override' => 1, + 'default' => [0]}, ); sub gitweb_get_feature { @@ -3449,6 +3458,13 @@ sub parse_date { return %date; } +sub hide_mailaddrs_if_private { + my $line = shift; + return $line unless gitweb_check_feature('email-privacy'); + $line =~ s/<[^@>]+@[^>]+>/<redacted>/g; + return $line; +} + sub parse_tag { my $tag_id = shift; my %tag; @@ -3465,7 +3481,7 @@ sub parse_tag { } elsif ($line =~ m/^tag (.+)$/) { $tag{'name'} = $1; } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) { - $tag{'author'} = $1; + $tag{'author'} = hide_mailaddrs_if_private($1); $tag{'author_epoch'} = $2; $tag{'author_tz'} = $3; if ($tag{'author'} =~ m/^([^<]+) <([^>]*)>/) { @@ -3513,7 +3529,7 @@ sub parse_commit_text { } elsif ((!defined $withparents) && ($line =~ m/^parent ($oid_regex)$/)) { push @parents, $1; } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) { - $co{'author'} = to_utf8($1); + $co{'author'} = hide_mailaddrs_if_private(to_utf8($1)); $co{'author_epoch'} = $2; $co{'author_tz'} = $3; if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) { @@ -3523,7 +3539,7 @@ sub parse_commit_text { $co{'author_name'} = $co{'author'}; } } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) { - $co{'committer'} = to_utf8($1); + $co{'committer'} = hide_mailaddrs_if_private(to_utf8($1)); $co{'committer_epoch'} = $2; $co{'committer_tz'} = $3; if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) { @@ -3568,9 +3584,10 @@ sub parse_commit_text { if (! defined $co{'title'} || $co{'title'} eq "") { $co{'title'} = $co{'title_short'} = '(no commit message)'; } - # remove added spaces + # remove added spaces, redact e-mail addresses if applicable. foreach my $line (@commit_lines) { $line =~ s/^ //; + $line = hide_mailaddrs_if_private($line); } $co{'comment'} = \@commit_lines; @@ -7489,7 +7506,8 @@ sub git_log_generic { -accesskey => "n", -title => "Alt-n"}, "next"); } my $patch_max = gitweb_get_feature('patches'); - if ($patch_max && !defined $file_name) { + if ($patch_max && !defined $file_name && + !gitweb_check_feature('email-privacy')) { if ($patch_max < 0 || @commitlist <= $patch_max) { $paging_nav .= " ⋅ " . $cgi->a({-href => href(action=>"patches", -replay=>1)}, @@ -7550,7 +7568,8 @@ sub git_commit { } @$parents ) . ')'; } - if (gitweb_check_feature('patches') && @$parents <= 1) { + if (gitweb_check_feature('patches') && @$parents <= 1 && + !gitweb_check_feature('email-privacy')) { $formats_nav .= " | " . $cgi->a({-href => href(action=>"patch", -replay=>1)}, "patch"); @@ -7863,7 +7882,8 @@ sub git_commitdiff { $formats_nav = $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)}, "raw"); - if ($patch_max && @{$co{'parents'}} <= 1) { + if ($patch_max && @{$co{'parents'}} <= 1 && + !gitweb_check_feature('email-privacy')) { $formats_nav .= " | " . $cgi->a({-href => href(action=>"patch", -replay=>1)}, "patch"); diff --git a/merge-ort.c b/merge-ort.c index 5e118a85ee..b1795d838e 100644 --- a/merge-ort.c +++ b/merge-ort.c @@ -18,6 +18,7 @@ #include "merge-ort.h" #include "alloc.h" +#include "attr.h" #include "blob.h" #include "cache-tree.h" #include "commit.h" @@ -25,6 +26,7 @@ #include "diff.h" #include "diffcore.h" #include "dir.h" +#include "entry.h" #include "ll-merge.h" #include "object-store.h" #include "revision.h" @@ -73,8 +75,12 @@ struct rename_info { /* * dirs_removed: directories removed on a given side of history. + * + * The keys of dirs_removed[side] are the directories that were removed + * on the given side of history. The value of the strintmap for each + * directory is a value from enum dir_rename_relevance. */ - struct strset dirs_removed[3]; + struct strintmap dirs_removed[3]; /* * dir_rename_count: tracking where parts of a directory were renamed to @@ -95,18 +101,20 @@ struct rename_info { struct strmap dir_renames[3]; /* - * relevant_sources: deleted paths for which we need rename detection + * relevant_sources: deleted paths wanted in rename detection, and why * * relevant_sources is a set of deleted paths on each side of * history for which we need rename detection. If a path is deleted * on one side of history, we need to detect if it is part of a * rename if either - * * we need to detect renames for an ancestor directory * * the file is modified/deleted on the other side of history + * * we need to detect renames for an ancestor directory * If neither of those are true, we can skip rename detection for - * that path. + * that path. The reason is stored as a value from enum + * file_rename_relevance, as the reason can inform the algorithm in + * diffcore_rename_extended(). */ - struct strset relevant_sources[3]; + struct strintmap relevant_sources[3]; /* * dir_rename_mask: @@ -215,6 +223,16 @@ struct merge_options_internal { struct rename_info renames; /* + * attr_index: hacky minimal index used for renormalization + * + * renormalization code _requires_ an index, though it only needs to + * find a .gitattributes file within the index. So, when + * renormalization is important, we create a special index with just + * that one file. + */ + struct index_state attr_index; + + /* * current_dir_name, toplevel_dir: temporary vars * * These are used in collect_merge_info_callback(), and will set the @@ -362,8 +380,8 @@ static void clear_or_reinit_internal_opts(struct merge_options_internal *opti, int i; void (*strmap_func)(struct strmap *, int) = reinitialize ? strmap_partial_clear : strmap_clear; - void (*strset_func)(struct strset *) = - reinitialize ? strset_partial_clear : strset_clear; + void (*strintmap_func)(struct strintmap *) = + reinitialize ? strintmap_partial_clear : strintmap_clear; /* * We marked opti->paths with strdup_strings = 0, so that we @@ -393,9 +411,12 @@ static void clear_or_reinit_internal_opts(struct merge_options_internal *opti, string_list_clear(&opti->paths_to_free, 0); opti->paths_to_free.strdup_strings = 0; + if (opti->attr_index.cache_nr) /* true iff opt->renormalize */ + discard_index(&opti->attr_index); + /* Free memory used by various renames maps */ for (i = MERGE_SIDE1; i <= MERGE_SIDE2; ++i) { - strset_func(&renames->dirs_removed[i]); + strintmap_func(&renames->dirs_removed[i]); partial_clear_dir_rename_count(&renames->dir_rename_count[i]); if (!reinitialize) @@ -403,7 +424,7 @@ static void clear_or_reinit_internal_opts(struct merge_options_internal *opti, strmap_func(&renames->dir_renames[i], 0); - strset_func(&renames->relevant_sources[i]); + strintmap_func(&renames->relevant_sources[i]); } if (!reinitialize) { @@ -673,8 +694,11 @@ static void add_pair(struct merge_options *opt, unsigned content_relevant = (match_mask == 0); unsigned location_relevant = (dir_rename_mask == 0x07); - if (content_relevant || location_relevant) - strset_add(&renames->relevant_sources[side], pathname); + if (content_relevant || location_relevant) { + /* content_relevant trumps location_relevant */ + strintmap_set(&renames->relevant_sources[side], pathname, + content_relevant ? RELEVANT_CONTENT : RELEVANT_LOCATION); + } } one = alloc_filespec(pathname); @@ -729,10 +753,41 @@ static void collect_rename_info(struct merge_options *opt, if (dirmask == 1 || dirmask == 3 || dirmask == 5) { /* absent_mask = 0x07 - dirmask; sides = absent_mask/2 */ unsigned sides = (0x07 - dirmask)/2; + unsigned relevance = (renames->dir_rename_mask == 0x07) ? + RELEVANT_FOR_ANCESTOR : NOT_RELEVANT; + /* + * Record relevance of this directory. However, note that + * when collect_merge_info_callback() recurses into this + * directory and calls collect_rename_info() on paths + * within that directory, if we find a path that was added + * to this directory on the other side of history, we will + * upgrade this value to RELEVANT_FOR_SELF; see below. + */ if (sides & 1) - strset_add(&renames->dirs_removed[1], fullname); + strintmap_set(&renames->dirs_removed[1], fullname, + relevance); if (sides & 2) - strset_add(&renames->dirs_removed[2], fullname); + strintmap_set(&renames->dirs_removed[2], fullname, + relevance); + } + + /* + * Here's the block that potentially upgrades to RELEVANT_FOR_SELF. + * When we run across a file added to a directory. In such a case, + * find the directory of the file and upgrade its relevance. + */ + if (renames->dir_rename_mask == 0x07 && + (filemask == 2 || filemask == 4)) { + /* + * Need directory rename for parent directory on other side + * of history from added file. Thus + * side = (~filemask & 0x06) >> 1 + * or + * side = 3 - (filemask/2). + */ + unsigned side = 3 - (filemask >> 1); + strintmap_set(&renames->dirs_removed[side], dirname, + RELEVANT_FOR_SELF); } if (filemask == 0 || filemask == 7) @@ -1147,6 +1202,63 @@ static int merge_submodule(struct merge_options *opt, return 0; } +static void initialize_attr_index(struct merge_options *opt) +{ + /* + * The renormalize_buffer() functions require attributes, and + * annoyingly those can only be read from the working tree or from + * an index_state. merge-ort doesn't have an index_state, so we + * generate a fake one containing only attribute information. + */ + struct merged_info *mi; + struct index_state *attr_index = &opt->priv->attr_index; + struct cache_entry *ce; + + attr_index->initialized = 1; + + if (!opt->renormalize) + return; + + mi = strmap_get(&opt->priv->paths, GITATTRIBUTES_FILE); + if (!mi) + return; + + if (mi->clean) { + int len = strlen(GITATTRIBUTES_FILE); + ce = make_empty_cache_entry(attr_index, len); + ce->ce_mode = create_ce_mode(mi->result.mode); + ce->ce_flags = create_ce_flags(0); + ce->ce_namelen = len; + oidcpy(&ce->oid, &mi->result.oid); + memcpy(ce->name, GITATTRIBUTES_FILE, len); + add_index_entry(attr_index, ce, + ADD_CACHE_OK_TO_ADD | ADD_CACHE_OK_TO_REPLACE); + get_stream_filter(attr_index, GITATTRIBUTES_FILE, &ce->oid); + } else { + int stage, len; + struct conflict_info *ci; + + ASSIGN_AND_VERIFY_CI(ci, mi); + for (stage = 0; stage < 3; stage++) { + unsigned stage_mask = (1 << stage); + + if (!(ci->filemask & stage_mask)) + continue; + len = strlen(GITATTRIBUTES_FILE); + ce = make_empty_cache_entry(attr_index, len); + ce->ce_mode = create_ce_mode(ci->stages[stage].mode); + ce->ce_flags = create_ce_flags(stage); + ce->ce_namelen = len; + oidcpy(&ce->oid, &ci->stages[stage].oid); + memcpy(ce->name, GITATTRIBUTES_FILE, len); + add_index_entry(attr_index, ce, + ADD_CACHE_OK_TO_ADD | ADD_CACHE_OK_TO_REPLACE); + get_stream_filter(attr_index, GITATTRIBUTES_FILE, + &ce->oid); + } + } +} + static int merge_3way(struct merge_options *opt, const char *path, const struct object_id *o, @@ -1161,6 +1273,9 @@ static int merge_3way(struct merge_options *opt, char *base, *name1, *name2; int merge_status; + if (!opt->priv->attr_index.initialized) + initialize_attr_index(opt); + ll_opts.renormalize = opt->renormalize; ll_opts.extra_marker_size = extra_marker_size; ll_opts.xdl_opts = opt->xdl_opts; @@ -1199,7 +1314,7 @@ static int merge_3way(struct merge_options *opt, merge_status = ll_merge(result_buf, path, &orig, base, &src1, name1, &src2, name2, - opt->repo->index, &ll_opts); + &opt->priv->attr_index, &ll_opts); free(base); free(name1); @@ -1511,6 +1626,9 @@ static void get_provisional_directory_renames(struct merge_options *opt, } } + if (max == 0) + continue; + if (bad_max == max) { path_msg(opt, source_dir, 0, _("CONFLICT (directory rename split): " @@ -1519,18 +1637,7 @@ static void get_provisional_directory_renames(struct merge_options *opt, "no destination getting a majority of the " "files."), source_dir); - /* - * We should mark this as unclean IF something attempts - * to use this rename. We do not yet have the logic - * in place to detect if this directory rename is being - * used, and optimizations that reduce the number of - * renames cause this to falsely trigger. For now, - * just disable it, causing t6423 testcase 2a to break. - * We'll later fix the detection, and when we do we - * will re-enable setting *clean to 0 (and thereby fix - * t6423 testcase 2a). - */ - /* *clean = 0; */ + *clean = 0; } else { strmap_put(&renames->dir_renames[side], source_dir, (void*)best); @@ -2160,7 +2267,7 @@ static inline int possible_side_renames(struct rename_info *renames, unsigned side_index) { return renames->pairs[side_index].nr > 0 && - !strset_empty(&renames->relevant_sources[side_index]); + !strintmap_empty(&renames->relevant_sources[side_index]); } static inline int possible_renames(struct rename_info *renames) @@ -2361,7 +2468,7 @@ static int detect_and_process_renames(struct merge_options *opt, clean &= collect_renames(opt, &combined, MERGE_SIDE2, &renames->dir_renames[1], &renames->dir_renames[2]); - QSORT(combined.queue, combined.nr, compare_pairs); + STABLE_QSORT(combined.queue, combined.nr, compare_pairs); trace2_region_leave("merge", "directory renames", opt->repo); trace2_region_enter("merge", "process renames", opt->repo); @@ -2431,6 +2538,61 @@ static int string_list_df_name_compare(const char *one, const char *two) return onelen - twolen; } +static int read_oid_strbuf(struct merge_options *opt, + const struct object_id *oid, + struct strbuf *dst) +{ + void *buf; + enum object_type type; + unsigned long size; + buf = read_object_file(oid, &type, &size); + if (!buf) + return err(opt, _("cannot read object %s"), oid_to_hex(oid)); + if (type != OBJ_BLOB) { + free(buf); + return err(opt, _("object %s is not a blob"), oid_to_hex(oid)); + } + strbuf_attach(dst, buf, size, size + 1); + return 0; +} + +static int blob_unchanged(struct merge_options *opt, + const struct version_info *base, + const struct version_info *side, + const char *path) +{ + struct strbuf basebuf = STRBUF_INIT; + struct strbuf sidebuf = STRBUF_INIT; + int ret = 0; /* assume changed for safety */ + const struct index_state *idx = &opt->priv->attr_index; + + if (!idx->initialized) + initialize_attr_index(opt); + + if (base->mode != side->mode) + return 0; + if (oideq(&base->oid, &side->oid)) + return 1; + + if (read_oid_strbuf(opt, &base->oid, &basebuf) || + read_oid_strbuf(opt, &side->oid, &sidebuf)) + goto error_return; + /* + * Note: binary | is used so that both renormalizations are + * performed. Comparison can be skipped if both files are + * unchanged since their sha1s have already been compared. + */ + if (renormalize_buffer(idx, path, basebuf.buf, basebuf.len, &basebuf) | + renormalize_buffer(idx, path, sidebuf.buf, sidebuf.len, &sidebuf)) + ret = (basebuf.len == sidebuf.len && + !memcmp(basebuf.buf, sidebuf.buf, basebuf.len)); + +error_return: + strbuf_release(&basebuf); + strbuf_release(&sidebuf); + return ret; +} + struct directory_versions { /* * versions: list of (basename -> version_info) @@ -2491,22 +2653,15 @@ static void write_tree(struct object_id *result_oid, size_t hash_size) { size_t maxlen = 0, extra; - unsigned int nr = versions->nr - offset; + unsigned int nr; struct strbuf buf = STRBUF_INIT; - struct string_list relevant_entries = STRING_LIST_INIT_NODUP; int i; - /* - * We want to sort the last (versions->nr-offset) entries in versions. - * Do so by abusing the string_list API a bit: make another string_list - * that contains just those entries and then sort them. - * - * We won't use relevant_entries again and will let it just pop off the - * stack, so there won't be allocation worries or anything. - */ - relevant_entries.items = versions->items + offset; - relevant_entries.nr = versions->nr - offset; - QSORT(relevant_entries.items, relevant_entries.nr, tree_entry_order); + assert(offset <= versions->nr); + nr = versions->nr - offset; + if (versions->nr) + /* No need for STABLE_QSORT -- filenames must be unique */ + QSORT(versions->items + offset, nr, tree_entry_order); /* Pre-allocate some space in buf */ extra = hash_size + 8; /* 8: 6 for mode, 1 for space, 1 for NUL char */ @@ -3017,8 +3172,13 @@ static void process_entry(struct merge_options *opt, modify_branch = (side == 1) ? opt->branch1 : opt->branch2; delete_branch = (side == 1) ? opt->branch2 : opt->branch1; - if (ci->path_conflict && - oideq(&ci->stages[0].oid, &ci->stages[side].oid)) { + if (opt->renormalize && + blob_unchanged(opt, &ci->stages[0], &ci->stages[side], + path)) { + ci->merged.is_null = 1; + ci->merged.clean = 1; + } else if (ci->path_conflict && + oideq(&ci->stages[0].oid, &ci->stages[side].oid)) { /* * This came from a rename/delete; no action to take, * but avoid printing "modify/delete" conflict notice @@ -3190,23 +3350,27 @@ static int checkout(struct merge_options *opt, return ret; } -static int record_conflicted_index_entries(struct merge_options *opt, - struct index_state *index, - struct strmap *paths, - struct strmap *conflicted) +static int record_conflicted_index_entries(struct merge_options *opt) { struct hashmap_iter iter; struct strmap_entry *e; + struct index_state *index = opt->repo->index; + struct checkout state = CHECKOUT_INIT; int errs = 0; int original_cache_nr; - if (strmap_empty(conflicted)) + if (strmap_empty(&opt->priv->conflicted)) return 0; + /* If any entries have skip_worktree set, we'll have to check 'em out */ + state.force = 1; + state.quiet = 1; + state.refresh_cache = 1; + state.istate = index; original_cache_nr = index->cache_nr; /* Put every entry from paths into plist, then sort */ - strmap_for_each_entry(conflicted, &iter, e) { + strmap_for_each_entry(&opt->priv->conflicted, &iter, e) { const char *path = e->key; struct conflict_info *ci = e->value; int pos; @@ -3247,9 +3411,23 @@ static int record_conflicted_index_entries(struct merge_options *opt, * the higher order stages. Thus, we need override * the CE_SKIP_WORKTREE bit and manually write those * files to the working disk here. - * - * TODO: Implement this CE_SKIP_WORKTREE fixup. */ + if (ce_skip_worktree(ce)) { + struct stat st; + + if (!lstat(path, &st)) { + char *new_name = unique_path(&opt->priv->paths, + path, + "cruft"); + + path_msg(opt, path, 1, + _("Note: %s not up to date and in way of checking out conflicted version; old copy renamed to %s"), + path, new_name); + errs |= rename(path, new_name); + free(new_name); + } + errs |= checkout_entry(ce, &state, NULL, NULL); + } /* * Mark this cache entry for removal and instead add @@ -3281,6 +3459,11 @@ static int record_conflicted_index_entries(struct merge_options *opt, * entries we added to the end into their right locations. */ remove_marked_cache_entries(index, 1); + /* + * No need for STABLE_QSORT -- cmp_cache_name_compare sorts primarily + * on filename and secondarily on stage, and (name, stage #) are a + * unique tuple. + */ QSORT(index->cache, index->cache_nr, cmp_cache_name_compare); return errs; @@ -3294,7 +3477,8 @@ void merge_switch_to_result(struct merge_options *opt, { assert(opt->priv == NULL); if (result->clean >= 0 && update_worktree_and_index) { - struct merge_options_internal *opti = result->priv; + const char *filename; + FILE *fp; trace2_region_enter("merge", "checkout", opt->repo); if (checkout(opt, head, result->tree)) { @@ -3305,14 +3489,22 @@ void merge_switch_to_result(struct merge_options *opt, trace2_region_leave("merge", "checkout", opt->repo); trace2_region_enter("merge", "record_conflicted", opt->repo); - if (record_conflicted_index_entries(opt, opt->repo->index, - &opti->paths, - &opti->conflicted)) { + opt->priv = result->priv; + if (record_conflicted_index_entries(opt)) { /* failure to function */ + opt->priv = NULL; result->clean = -1; return; } + opt->priv = NULL; trace2_region_leave("merge", "record_conflicted", opt->repo); + + trace2_region_enter("merge", "write_auto_merge", opt->repo); + filename = git_path_auto_merge(opt->repo); + fp = xfopen(filename, "w"); + fprintf(fp, "%s\n", oid_to_hex(&result->tree->object.oid)); + fclose(fp); + trace2_region_leave("merge", "write_auto_merge", opt->repo); } if (display_update_msgs) { @@ -3357,6 +3549,8 @@ void merge_finalize(struct merge_options *opt, { struct merge_options_internal *opti = result->priv; + if (opt->renormalize) + git_attr_set_direction(GIT_ATTR_CHECKIN); assert(opt->priv == NULL); clear_or_reinit_internal_opts(opti, 0); @@ -3365,6 +3559,23 @@ void merge_finalize(struct merge_options *opt, /*** Function Grouping: helper functions for merge_incore_*() ***/ +static struct tree *shift_tree_object(struct repository *repo, + struct tree *one, struct tree *two, + const char *subtree_shift) +{ + struct object_id shifted; + + if (!*subtree_shift) { + shift_tree(repo, &one->object.oid, &two->object.oid, &shifted, 0); + } else { + shift_tree_by(repo, &one->object.oid, &two->object.oid, &shifted, + subtree_shift); + } + if (oideq(&two->object.oid, &shifted)) + return two; + return lookup_tree(repo, &shifted); +} + static inline void set_commit_tree(struct commit *c, struct tree *t) { c->maybe_tree = t; @@ -3432,6 +3643,10 @@ static void merge_start(struct merge_options *opt, struct merge_result *result) /* Default to histogram diff. Actually, just hardcode it...for now. */ opt->xdl_opts = DIFF_WITH_ALG(opt, HISTOGRAM_DIFF); + /* Handle attr direction stuff for renormalization */ + if (opt->renormalize) + git_attr_set_direction(GIT_ATTR_CHECKOUT); + /* Initialization of opt->priv, our internal merge data */ trace2_region_enter("merge", "allocate/init", opt->repo); if (opt->priv) { @@ -3444,14 +3659,14 @@ static void merge_start(struct merge_options *opt, struct merge_result *result) /* Initialization of various renames fields */ renames = &opt->priv->renames; for (i = MERGE_SIDE1; i <= MERGE_SIDE2; i++) { - strset_init_with_options(&renames->dirs_removed[i], - NULL, 0); + strintmap_init_with_options(&renames->dirs_removed[i], + NOT_RELEVANT, NULL, 0); strmap_init_with_options(&renames->dir_rename_count[i], NULL, 1); strmap_init_with_options(&renames->dir_renames[i], NULL, 0); - strset_init_with_options(&renames->relevant_sources[i], - NULL, 0); + strintmap_init_with_options(&renames->relevant_sources[i], + 0, NULL, 0); } /* @@ -3490,6 +3705,13 @@ static void merge_ort_nonrecursive_internal(struct merge_options *opt, { struct object_id working_tree_oid; + if (opt->subtree_shift) { + side2 = shift_tree_object(opt->repo, side1, side2, + opt->subtree_shift); + merge_base = shift_tree_object(opt->repo, side1, merge_base, + opt->subtree_shift); + } + trace2_region_enter("merge", "collect_merge_info", opt->repo); if (collect_merge_info(opt, merge_base, side1, side2) != 0) { /* diff --git a/merge-recursive.c b/merge-recursive.c index ed31f9496c..7618303f7b 100644 --- a/merge-recursive.c +++ b/merge-recursive.c @@ -1075,6 +1075,11 @@ static int merge_3way(struct merge_options *opt, read_mmblob(&src1, &a->oid); read_mmblob(&src2, &b->oid); + /* + * FIXME: Using a->path for normalization rules in ll_merge could be + * wrong if we renamed from a->path to b->path. We should use the + * target path for where the file will be written. + */ merge_status = ll_merge(result_buf, a->path, &orig, base, &src1, name1, &src2, name2, opt->repo->index, &ll_opts); @@ -1154,6 +1159,8 @@ static void print_commit(struct commit *commit) struct strbuf sb = STRBUF_INIT; struct pretty_print_context ctx = {0}; ctx.date_mode.type = DATE_NORMAL; + /* FIXME: Merge this with output_commit_title() */ + assert(!merge_remote_util(commit)); format_commit_message(commit, " %h: %m %s", &sb, &ctx); fprintf(stderr, "%s\n", sb.buf); strbuf_release(&sb); @@ -1177,6 +1184,11 @@ static int merge_submodule(struct merge_options *opt, int search = !opt->priv->call_depth; /* store a in result in case we fail */ + /* FIXME: This is the WRONG resolution for the recursive case when + * we need to be careful to avoid accidentally matching either side. + * Should probably use o instead there, much like we do for merging + * binaries. + */ oidcpy(result, a); /* we can not handle deletion conflicts */ @@ -1301,6 +1313,13 @@ static int merge_mode_and_contents(struct merge_options *opt, if ((S_IFMT & a->mode) != (S_IFMT & b->mode)) { result->clean = 0; + /* + * FIXME: This is a bad resolution for recursive case; for + * the recursive case we want something that is unlikely to + * accidentally match either side. Also, while it makes + * sense to prefer regular files over symlinks, it doesn't + * make sense to prefer regular files over submodules. + */ if (S_ISREG(a->mode)) { result->blob.mode = a->mode; oidcpy(&result->blob.oid, &a->oid); @@ -1349,6 +1368,7 @@ static int merge_mode_and_contents(struct merge_options *opt, free(result_buf.ptr); if (ret) return ret; + /* FIXME: bug, what if modes didn't match? */ result->clean = (merge_status == 0); } else if (S_ISGITLINK(a->mode)) { result->clean = merge_submodule(opt, &result->blob.oid, @@ -2663,6 +2683,14 @@ static int process_renames(struct merge_options *opt, struct string_list b_by_dst = STRING_LIST_INIT_NODUP; const struct rename *sre; + /* + * FIXME: As string-list.h notes, it's O(n^2) to build a sorted + * string_list one-by-one, but O(n log n) to build it unsorted and + * then sort it. Note that as we build the list, we do not need to + * check if the existing destination path is already in the list, + * because the structure of diffcore_rename guarantees we won't + * have duplicates. + */ for (i = 0; i < a_renames->nr; i++) { sre = a_renames->items[i].util; string_list_insert(&a_by_dst, sre->pair->two->path)->util @@ -3601,6 +3629,15 @@ static int merge_recursive_internal(struct merge_options *opt, return err(opt, _("merge returned no commit")); } + /* + * FIXME: Since merge_recursive_internal() is only ever called by + * places that ensure the index is loaded first + * (e.g. builtin/merge.c, rebase/sequencer, etc.), in the common + * case where the merge base was unique that means when we get here + * we immediately discard the index and re-read it, which is a + * complete waste of time. We should only be discarding and + * re-reading if we were forced to recurse. + */ discard_index(opt->repo->index); if (!opt->priv->call_depth) repo_read_index(opt->repo); diff --git a/pack-bitmap.c b/pack-bitmap.c index b4513f8672..3ed15431cd 100644 --- a/pack-bitmap.c +++ b/pack-bitmap.c @@ -13,6 +13,7 @@ #include "repository.h" #include "object-store.h" #include "list-objects-filter-options.h" +#include "config.h" /* * An entry on the bitmap index, representing the bitmap for a given @@ -1351,6 +1352,24 @@ void test_bitmap_walk(struct rev_info *revs) free_bitmap_index(bitmap_git); } +int test_bitmap_commits(struct repository *r) +{ + struct bitmap_index *bitmap_git = prepare_bitmap_git(r); + struct object_id oid; + MAYBE_UNUSED void *value; + + if (!bitmap_git) + die("failed to load bitmap indexes"); + + kh_foreach(bitmap_git->bitmaps, oid, value, { + printf("%s\n", oid_to_hex(&oid)); + }); + + free_bitmap_index(bitmap_git); + + return 0; +} + int rebuild_bitmap(const uint32_t *reposition, struct ewah_bitmap *source, struct bitmap *dest) @@ -1512,3 +1531,8 @@ off_t get_disk_usage_from_bitmap(struct bitmap_index *bitmap_git, return total; } + +const struct string_list *bitmap_preferred_tips(struct repository *r) +{ + return repo_config_get_value_multi(r, "pack.preferbitmaptips"); +} diff --git a/pack-bitmap.h b/pack-bitmap.h index 36d99930d8..78f2b3ff79 100644 --- a/pack-bitmap.h +++ b/pack-bitmap.h @@ -5,6 +5,7 @@ #include "khash.h" #include "pack.h" #include "pack-objects.h" +#include "string-list.h" struct commit; struct repository; @@ -49,6 +50,7 @@ void traverse_bitmap_commit_list(struct bitmap_index *, struct rev_info *revs, show_reachable_fn show_reachable); void test_bitmap_walk(struct rev_info *revs); +int test_bitmap_commits(struct repository *r); struct bitmap_index *prepare_bitmap_walk(struct rev_info *revs, struct list_objects_filter_options *filter); int reuse_partial_packfile_from_bitmap(struct bitmap_index *, @@ -90,4 +92,6 @@ void bitmap_writer_finish(struct pack_idx_entry **index, const char *filename, uint16_t options); +const struct string_list *bitmap_preferred_tips(struct repository *r); + #endif @@ -1534,5 +1534,6 @@ REPO_GIT_PATH_FUNC(merge_rr, "MERGE_RR") REPO_GIT_PATH_FUNC(merge_mode, "MERGE_MODE") REPO_GIT_PATH_FUNC(merge_head, "MERGE_HEAD") REPO_GIT_PATH_FUNC(merge_autostash, "MERGE_AUTOSTASH") +REPO_GIT_PATH_FUNC(auto_merge, "AUTO_MERGE") REPO_GIT_PATH_FUNC(fetch_head, "FETCH_HEAD") REPO_GIT_PATH_FUNC(shallow, "shallow") @@ -176,6 +176,7 @@ struct path_cache { const char *merge_mode; const char *merge_head; const char *merge_autostash; + const char *auto_merge; const char *fetch_head; const char *shallow; }; @@ -191,6 +192,7 @@ const char *git_path_merge_rr(struct repository *r); const char *git_path_merge_mode(struct repository *r); const char *git_path_merge_head(struct repository *r); const char *git_path_merge_autostash(struct repository *r); +const char *git_path_auto_merge(struct repository *r); const char *git_path_fetch_head(struct repository *r); const char *git_path_shallow(struct repository *r); diff --git a/ref-filter.c b/ref-filter.c index f0bd32f714..a0adb4551d 100644 --- a/ref-filter.c +++ b/ref-filter.c @@ -1608,7 +1608,7 @@ static int get_object(struct ref_array_item *ref, int deref, struct object **obj if (oi->info.contentp) { *obj = parse_object_buffer(the_repository, &oi->oid, oi->type, oi->size, oi->content, &eaten); - if (!obj) { + if (!*obj) { if (!eaten) free(oi->content); return strbuf_addf_ret(err, -1, _("parse_object_buffer failed on %s for %s"), diff --git a/sequencer.c b/sequencer.c index fd183b5593..c29a36824c 100644 --- a/sequencer.c +++ b/sequencer.c @@ -2281,6 +2281,7 @@ static int do_pick_commit(struct repository *r, refs_delete_ref(get_main_ref_store(r), "", "CHERRY_PICK_HEAD", NULL, 0); unlink(git_path_merge_msg(r)); + unlink(git_path_auto_merge(r)); fprintf(stderr, _("dropping %s %s -- patch contents already upstream\n"), oid_to_hex(&commit->object.oid), msg.subject); @@ -2644,6 +2645,8 @@ void sequencer_post_commit_cleanup(struct repository *r, int verbose) need_cleanup = 1; } + unlink(git_path_auto_merge(r)); + if (!need_cleanup) return; @@ -4304,6 +4307,7 @@ static int pick_commits(struct repository *r, unlink(rebase_path_stopped_sha()); unlink(rebase_path_amend()); unlink(git_path_merge_head(r)); + unlink(git_path_auto_merge(r)); delete_ref(NULL, "REBASE_HEAD", NULL, REF_NO_DEREF); if (item->command == TODO_BREAK) { @@ -4717,6 +4721,7 @@ static int commit_staged_changes(struct repository *r, return error(_("could not commit staged changes.")); unlink(rebase_path_amend()); unlink(git_path_merge_head(r)); + unlink(git_path_auto_merge(r)); if (final_fixup) { unlink(rebase_path_fixup_msg()); unlink(rebase_path_squash_msg()); @@ -1274,18 +1274,10 @@ const char *setup_git_directory_gently(int *nongit_ok) * the GIT_PREFIX environment variable must always match. For details * see Documentation/config/alias.txt. */ - if (nongit_ok && *nongit_ok) { + if (nongit_ok && *nongit_ok) startup_info->have_repository = 0; - startup_info->prefix = NULL; - setenv(GIT_PREFIX_ENVIRONMENT, "", 1); - } else { + else startup_info->have_repository = 1; - startup_info->prefix = prefix; - if (prefix) - setenv(GIT_PREFIX_ENVIRONMENT, prefix, 1); - else - setenv(GIT_PREFIX_ENVIRONMENT, "", 1); - } /* * Not all paths through the setup code will call 'set_git_dir()' (which @@ -1311,6 +1303,22 @@ const char *setup_git_directory_gently(int *nongit_ok) if (startup_info->have_repository) repo_set_hash_algo(the_repository, repo_fmt.hash_algo); } + /* + * Since precompose_string_if_needed() needs to look at + * the core.precomposeunicode configuration, this + * has to happen after the above block that finds + * out where the repository is, i.e. a preparation + * for calling git_config_get_bool(). + */ + if (prefix) { + prefix = precompose_string_if_needed(prefix); + startup_info->prefix = prefix; + setenv(GIT_PREFIX_ENVIRONMENT, prefix, 1); + } else { + startup_info->prefix = NULL; + setenv(GIT_PREFIX_ENVIRONMENT, "", 1); + } + strbuf_release(&dir); strbuf_release(&gitdir); diff --git a/t/annotate-tests.sh b/t/annotate-tests.sh index 29ce89090d..d3b299e75c 100644 --- a/t/annotate-tests.sh +++ b/t/annotate-tests.sh @@ -479,22 +479,26 @@ test_expect_success 'blame -L ^:RE (absolute: end-of-file)' ' check_count -f hello.c -L$n -L^:ma.. F 4 G 1 H 1 ' -test_expect_success 'setup -L :funcname with userdiff driver' ' - echo "fortran-* diff=fortran" >.gitattributes && - fortran_file=fortran-external-function && - orig_file="$TEST_DIRECTORY/t4018/$fortran_file" && - cp "$orig_file" . && - git add "$fortran_file" && - GIT_AUTHOR_NAME="A" GIT_AUTHOR_EMAIL="A@test.git" \ - git commit -m "add fortran file" && - sed -e "s/ChangeMe/IWasChanged/" <"$orig_file" >"$fortran_file" && - git add "$fortran_file" && - GIT_AUTHOR_NAME="B" GIT_AUTHOR_EMAIL="B@test.git" \ - git commit -m "change fortran file" -' - test_expect_success 'blame -L :funcname with userdiff driver' ' - check_count -f fortran-external-function -L:RIGHT A 7 B 1 + cat >file.template <<-\EOF && + DO NOT MATCH THIS LINE + function RIGHT(a, b) result(c) + AS THE DEFAULT DRIVER WOULD + + integer, intent(in) :: ChangeMe + EOF + + fortran_file=file.f03 && + test_when_finished "rm .gitattributes" && + echo "$fortran_file diff=fortran" >.gitattributes && + + test_commit --author "A <A@test.git>" \ + "add" "$fortran_file" \ + "$(cat file.template)" && + test_commit --author "B <B@test.git>" \ + "change" "$fortran_file" \ + "$(cat file.template | sed -e s/ChangeMe/IWasChanged/)" && + check_count -f "$fortran_file" -L:RIGHT A 3 B 1 ' test_expect_success 'setup incremental' ' diff --git a/t/helper/test-bitmap.c b/t/helper/test-bitmap.c new file mode 100644 index 0000000000..134a1e9d76 --- /dev/null +++ b/t/helper/test-bitmap.c @@ -0,0 +1,24 @@ +#include "test-tool.h" +#include "cache.h" +#include "pack-bitmap.h" + +static int bitmap_list_commits(void) +{ + return test_bitmap_commits(the_repository); +} + +int cmd__bitmap(int argc, const char **argv) +{ + setup_git_directory(); + + if (argc != 2) + goto usage; + + if (!strcmp(argv[1], "list-commits")) + return bitmap_list_commits(); + +usage: + usage("\ttest-tool bitmap list-commits"); + + return -1; +} diff --git a/t/helper/test-bloom.c b/t/helper/test-bloom.c index 2a1ae3dae6..ad3ef1cd77 100644 --- a/t/helper/test-bloom.c +++ b/t/helper/test-bloom.c @@ -48,7 +48,7 @@ static void get_bloom_filter_for_commit(const struct object_id *commit_oid) static const char *bloom_usage = "\n" " test-tool bloom get_murmur3 <string>\n" " test-tool bloom generate_filter <string> [<string>...]\n" -" test-tool get_filter_for_commit <commit-hex>\n"; +" test-tool bloom get_filter_for_commit <commit-hex>\n"; int cmd__bloom(int argc, const char **argv) { diff --git a/t/helper/test-tool.c b/t/helper/test-tool.c index 287aa60023..c5bd0c6d4c 100644 --- a/t/helper/test-tool.c +++ b/t/helper/test-tool.c @@ -15,6 +15,7 @@ struct test_cmd { static struct test_cmd cmds[] = { { "advise", cmd__advise_if_enabled }, + { "bitmap", cmd__bitmap }, { "bloom", cmd__bloom }, { "chmtime", cmd__chmtime }, { "config", cmd__config }, @@ -72,6 +73,7 @@ static struct test_cmd cmds[] = { { "submodule-nested-repo-config", cmd__submodule_nested_repo_config }, { "subprocess", cmd__subprocess }, { "trace2", cmd__trace2 }, + { "userdiff", cmd__userdiff }, { "urlmatch-normalization", cmd__urlmatch_normalization }, { "xml-encode", cmd__xml_encode }, { "wildmatch", cmd__wildmatch }, diff --git a/t/helper/test-tool.h b/t/helper/test-tool.h index 9ea4b31011..e8069a3b22 100644 --- a/t/helper/test-tool.h +++ b/t/helper/test-tool.h @@ -5,6 +5,7 @@ #include "git-compat-util.h" int cmd__advise_if_enabled(int argc, const char **argv); +int cmd__bitmap(int argc, const char **argv); int cmd__bloom(int argc, const char **argv); int cmd__chmtime(int argc, const char **argv); int cmd__config(int argc, const char **argv); @@ -62,6 +63,7 @@ int cmd__submodule_config(int argc, const char **argv); int cmd__submodule_nested_repo_config(int argc, const char **argv); int cmd__subprocess(int argc, const char **argv); int cmd__trace2(int argc, const char **argv); +int cmd__userdiff(int argc, const char **argv); int cmd__urlmatch_normalization(int argc, const char **argv); int cmd__xml_encode(int argc, const char **argv); int cmd__wildmatch(int argc, const char **argv); diff --git a/t/helper/test-userdiff.c b/t/helper/test-userdiff.c new file mode 100644 index 0000000000..f013f8a31e --- /dev/null +++ b/t/helper/test-userdiff.c @@ -0,0 +1,46 @@ +#include "test-tool.h" +#include "cache.h" +#include "userdiff.h" +#include "config.h" + +static int driver_cb(struct userdiff_driver *driver, + enum userdiff_driver_type type, void *priv) +{ + enum userdiff_driver_type *want_type = priv; + if (type & *want_type && driver->funcname.pattern) + puts(driver->name); + return 0; +} + +static int cmd__userdiff_config(const char *var, const char *value, void *cb) +{ + if (userdiff_config(var, value) < 0) + return -1; + return 0; +} + +int cmd__userdiff(int argc, const char **argv) +{ + enum userdiff_driver_type want = 0; + if (argc != 2) + return 1; + + if (!strcmp(argv[1], "list-drivers")) + want = (USERDIFF_DRIVER_TYPE_BUILTIN | + USERDIFF_DRIVER_TYPE_CUSTOM); + else if (!strcmp(argv[1], "list-builtin-drivers")) + want = USERDIFF_DRIVER_TYPE_BUILTIN; + else if (!strcmp(argv[1], "list-custom-drivers")) + want = USERDIFF_DRIVER_TYPE_CUSTOM; + else + return error("unknown argument %s", argv[1]); + + if (want & USERDIFF_DRIVER_TYPE_CUSTOM) { + setup_git_directory(); + git_config(cmd__userdiff_config, NULL); + } + + for_each_userdiff_driver(driver_cb, &want); + + return 0; +} diff --git a/t/t3437-rebase-fixup-options.sh b/t/t3437-rebase-fixup-options.sh index d0bdc7ed02..c023fefd68 100755 --- a/t/t3437-rebase-fixup-options.sh +++ b/t/t3437-rebase-fixup-options.sh @@ -157,7 +157,7 @@ test_expect_success 'sequence of fixup, fixup -C & squash --signoff works' ' git -c commit.status=false rebase -ik --signoff A && git diff-tree --exit-code --patch HEAD B3 -- && test_cmp_rev HEAD^ A && - test_i18ncmp "$TEST_DIRECTORY/t3437/expected-squash-message" \ + test_cmp "$TEST_DIRECTORY/t3437/expected-squash-message" \ actual-squash-message ' @@ -191,7 +191,7 @@ test_expect_success 'sequence squash, fixup & fixup -c gives combined message' ' FAKE_LINES="1 squash 2 fixup 3 fixup_-c 4" \ FAKE_MESSAGE_COPY=actual-combined-message \ git -c commit.status=false rebase -i A && - test_i18ncmp "$TEST_DIRECTORY/t3437/expected-combined-message" \ + test_cmp "$TEST_DIRECTORY/t3437/expected-combined-message" \ actual-combined-message && test_cmp_rev HEAD^ A ' @@ -204,7 +204,7 @@ test_expect_success 'fixup -C works upon --autosquash with amend!' ' --signoff A && git diff-tree --exit-code --patch HEAD B3 -- && test_cmp_rev HEAD^ A && - test_i18ncmp "$TEST_DIRECTORY/t3437/expected-squash-message" \ + test_cmp "$TEST_DIRECTORY/t3437/expected-squash-message" \ actual-squash-message ' diff --git a/t/t3512-cherry-pick-submodule.sh b/t/t3512-cherry-pick-submodule.sh index 822f2d4bfb..c657840db3 100755 --- a/t/t3512-cherry-pick-submodule.sh +++ b/t/t3512-cherry-pick-submodule.sh @@ -8,8 +8,11 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME . ./test-lib.sh . "$TEST_DIRECTORY"/lib-submodule-update.sh -KNOWN_FAILURE_NOFF_MERGE_DOESNT_CREATE_EMPTY_SUBMODULE_DIR=1 -KNOWN_FAILURE_NOFF_MERGE_ATTEMPTS_TO_MERGE_REMOVED_SUBMODULE_FILES=1 +if test "$GIT_TEST_MERGE_ALGORITHM" != ort +then + KNOWN_FAILURE_NOFF_MERGE_DOESNT_CREATE_EMPTY_SUBMODULE_DIR=1 + KNOWN_FAILURE_NOFF_MERGE_ATTEMPTS_TO_MERGE_REMOVED_SUBMODULE_FILES=1 +fi test_submodule_switch "cherry-pick" test_expect_success 'unrelated submodule/file conflict is ignored' ' diff --git a/t/t3513-revert-submodule.sh b/t/t3513-revert-submodule.sh index a759f12cbb..74cd96e582 100755 --- a/t/t3513-revert-submodule.sh +++ b/t/t3513-revert-submodule.sh @@ -30,7 +30,10 @@ git_revert () { git revert HEAD } -KNOWN_FAILURE_NOFF_MERGE_DOESNT_CREATE_EMPTY_SUBMODULE_DIR=1 +if test "$GIT_TEST_MERGE_ALGORITHM" != ort +then + KNOWN_FAILURE_NOFF_MERGE_DOESNT_CREATE_EMPTY_SUBMODULE_DIR=1 +fi test_submodule_switch_func "git_revert" test_done diff --git a/t/t4018-diff-funcname.sh b/t/t4018-diff-funcname.sh index 9675bc17db..740696c8f7 100755 --- a/t/t4018-diff-funcname.sh +++ b/t/t4018-diff-funcname.sh @@ -25,33 +25,26 @@ test_expect_success 'setup' ' echo B >B.java ' +test_expect_success 'setup: test-tool userdiff' ' + # Make sure additions to builtin_drivers are sorted + test_when_finished "rm builtin-drivers.sorted" && + test-tool userdiff list-builtin-drivers >builtin-drivers && + test_file_not_empty builtin-drivers && + sort <builtin-drivers >builtin-drivers.sorted && + test_cmp builtin-drivers.sorted builtin-drivers && + + # Ditto, but "custom" requires the .git directory and config + # to be setup and read. + test_when_finished "rm custom-drivers.sorted" && + test-tool userdiff list-custom-drivers >custom-drivers && + test_file_not_empty custom-drivers && + sort <custom-drivers >custom-drivers.sorted && + test_cmp custom-drivers.sorted custom-drivers +' + diffpatterns=" - ada - bash - bibtex - cpp - csharp - css - dts - elixir - fortran - fountain - golang - html - java - markdown - matlab - objc - pascal - perl - php - python - ruby - rust - tex - custom1 - custom2 - custom3 + $(cat builtin-drivers) + $(cat custom-drivers) " for p in $diffpatterns @@ -101,13 +94,7 @@ test_expect_success 'setup hunk header tests' ' # check each individual file for i in $(git ls-files) do - if grep broken "$i" >/dev/null 2>&1 - then - result=failure - else - result=success - fi - test_expect_$result "hunk header: $i" " + test_expect_success "hunk header: $i" " git diff -U1 $i >actual && grep '@@ .* @@.*RIGHT' actual " diff --git a/t/t4018/README b/t/t4018/README index 283e01cca1..2d25b2b4fc 100644 --- a/t/t4018/README +++ b/t/t4018/README @@ -7,9 +7,6 @@ at least two lines from the line that must appear in the hunk header. The text that must appear in the hunk header must contain the word "right", but in all upper-case, like in the title above. -To mark a test case that highlights a malfunction, insert the word -BROKEN in all lower-case somewhere in the file. - This text is a bit twisted and out of order, but it is itself a test case for the default hunk header pattern. Know what you are doing if you change it. diff --git a/t/t4018/scheme-class b/t/t4018/scheme-class new file mode 100644 index 0000000000..e5e07b43fb --- /dev/null +++ b/t/t4018/scheme-class @@ -0,0 +1,7 @@ +(define book-class% + (class* () object% RIGHT + (field (pages 5)) + (field (ChangeMe 5)) + (define/public (letters) + (* pages 500)) + (super-new))) diff --git a/t/t4018/scheme-def b/t/t4018/scheme-def new file mode 100644 index 0000000000..1e2673da96 --- /dev/null +++ b/t/t4018/scheme-def @@ -0,0 +1,4 @@ +(def (some-func x y z) RIGHT + (let ((a x) + (b y)) + (ChangeMe a b))) diff --git a/t/t4018/scheme-def-variant b/t/t4018/scheme-def-variant new file mode 100644 index 0000000000..d857a61d64 --- /dev/null +++ b/t/t4018/scheme-def-variant @@ -0,0 +1,4 @@ +(defmethod {print point} RIGHT + (lambda (self) + (with ((point x y) self) + (printf "{ChangeMe x:~a y:~a}~n" x y)))) diff --git a/t/t4018/scheme-define-slash-public b/t/t4018/scheme-define-slash-public new file mode 100644 index 0000000000..39a93a1600 --- /dev/null +++ b/t/t4018/scheme-define-slash-public @@ -0,0 +1,7 @@ +(define bar-class% + (class object% + (field (info 5)) + (define/public (foo) RIGHT + (+ info 42) + (* info ChangeMe)) + (super-new))) diff --git a/t/t4018/scheme-define-syntax b/t/t4018/scheme-define-syntax new file mode 100644 index 0000000000..7d5e99e0fc --- /dev/null +++ b/t/t4018/scheme-define-syntax @@ -0,0 +1,8 @@ +(define-syntax define-test-suite RIGHT + (syntax-rules () + ((_ suite-name (name test) ChangeMe ...) + (define suite-name + (let ((tests + `((name . ,test) ...))) + (lambda () + (run-suite 'suite-name tests))))))) diff --git a/t/t4018/scheme-define-variant b/t/t4018/scheme-define-variant new file mode 100644 index 0000000000..911708854d --- /dev/null +++ b/t/t4018/scheme-define-variant @@ -0,0 +1,4 @@ +(define* (some-func x y z) RIGHT + (let ((a x) + (b y)) + (ChangeMe a b))) diff --git a/t/t4018/scheme-library b/t/t4018/scheme-library new file mode 100644 index 0000000000..82ea3df510 --- /dev/null +++ b/t/t4018/scheme-library @@ -0,0 +1,11 @@ +(library (my-helpers id-stuff) RIGHT + (export find-dup) + (import (ChangeMe)) + (define (find-dup l) + (and (pair? l) + (let loop ((rest (cdr l))) + (cond + [(null? rest) (find-dup (cdr l))] + [(bound-identifier=? (car l) (car rest)) + (car rest)] + [else (loop (cdr rest))]))))) diff --git a/t/t4018/scheme-local-define b/t/t4018/scheme-local-define new file mode 100644 index 0000000000..bc6d8aebbe --- /dev/null +++ b/t/t4018/scheme-local-define @@ -0,0 +1,4 @@ +(define (higher-order) + (define local-function RIGHT + (lambda (x) + (car "this is" "ChangeMe")))) diff --git a/t/t4018/scheme-module b/t/t4018/scheme-module new file mode 100644 index 0000000000..edfae0ebf7 --- /dev/null +++ b/t/t4018/scheme-module @@ -0,0 +1,6 @@ +(module A RIGHT + (export with-display-exception) + (extern (display-exception display-exception ChangeMe)) + (def (with-display-exception thunk) + (with-catch (lambda (e) (display-exception e (current-error-port)) e) + thunk))) diff --git a/t/t4018/scheme-top-level-define b/t/t4018/scheme-top-level-define new file mode 100644 index 0000000000..624743c22b --- /dev/null +++ b/t/t4018/scheme-top-level-define @@ -0,0 +1,4 @@ +(define (some-func x y z) RIGHT + (let ((a x) + (b y)) + (ChangeMe a b))) diff --git a/t/t4018/scheme-user-defined-define b/t/t4018/scheme-user-defined-define new file mode 100644 index 0000000000..35fe7cc9bf --- /dev/null +++ b/t/t4018/scheme-user-defined-define @@ -0,0 +1,6 @@ +(define-test-suite record\ case-tests RIGHT + (record-case-1 (lambda (fail) + (let ((a (make-foo 1 2))) + (record-case a + ((bar x) (ChangeMe)) + ((foo a b) (+ a b))))))) diff --git a/t/t4034-diff-words.sh b/t/t4034-diff-words.sh index 56f1e62a97..ee7721ab91 100755 --- a/t/t4034-diff-words.sh +++ b/t/t4034-diff-words.sh @@ -325,6 +325,7 @@ test_language_driver perl test_language_driver php test_language_driver python test_language_driver ruby +test_language_driver scheme test_language_driver tex test_expect_success 'word-diff with diff.sbe' ' diff --git a/t/t4034/scheme/expect b/t/t4034/scheme/expect new file mode 100644 index 0000000000..496cd5de8c --- /dev/null +++ b/t/t4034/scheme/expect @@ -0,0 +1,11 @@ +<BOLD>diff --git a/pre b/post<RESET> +<BOLD>index 74b6605..63b6ac4 100644<RESET> +<BOLD>--- a/pre<RESET> +<BOLD>+++ b/post<RESET> +<CYAN>@@ -1,6 +1,6 @@<RESET> +(define (<RED>myfunc a b<RESET><GREEN>my-func first second<RESET>) + ; This is a <RED>really<RESET><GREEN>(moderately)<RESET> cool function. + (<RED>this\place<RESET><GREEN>that\place<RESET> (+ 3 4)) + (define <RED>some-text<RESET><GREEN>|a greeting|<RESET> "hello") + (let ((c (<RED>+ a b<RESET><GREEN>add1 first<RESET>))) + (format "one more than the total is %d" (<RED>add1<RESET><GREEN>+<RESET> c <GREEN>second<RESET>)))) diff --git a/t/t4034/scheme/post b/t/t4034/scheme/post new file mode 100644 index 0000000000..63b6ac4f87 --- /dev/null +++ b/t/t4034/scheme/post @@ -0,0 +1,6 @@ +(define (my-func first second) + ; This is a (moderately) cool function. + (that\place (+ 3 4)) + (define |a greeting| "hello") + (let ((c (add1 first))) + (format "one more than the total is %d" (+ c second)))) diff --git a/t/t4034/scheme/pre b/t/t4034/scheme/pre new file mode 100644 index 0000000000..74b6605357 --- /dev/null +++ b/t/t4034/scheme/pre @@ -0,0 +1,6 @@ +(define (myfunc a b) + ; This is a really cool function. + (this\place (+ 3 4)) + (define some-text "hello") + (let ((c (+ a b))) + (format "one more than the total is %d" (add1 c)))) diff --git a/t/t4108-apply-threeway.sh b/t/t4108-apply-threeway.sh index d62db3fbe1..65147efdea 100755 --- a/t/t4108-apply-threeway.sh +++ b/t/t4108-apply-threeway.sh @@ -160,4 +160,74 @@ test_expect_success 'apply -3 with add/add conflict (dirty working tree)' ' test_cmp three.save three ' +test_expect_success 'apply -3 with ambiguous repeating file' ' + git reset --hard && + test_write_lines 1 2 1 2 1 2 1 2 1 2 1 >one_two_repeat && + git add one_two_repeat && + git commit -m "init one" && + test_write_lines 1 2 1 2 1 2 1 2 one 2 1 >one_two_repeat && + git commit -a -m "change one" && + + git diff HEAD~ >Repeat.diff && + git reset --hard HEAD~ && + + test_write_lines 1 2 1 2 1 2 one 2 1 2 one >one_two_repeat && + git commit -a -m "change surrounding one" && + + git apply --index --3way Repeat.diff && + test_write_lines 1 2 1 2 1 2 one 2 one 2 one >expect && + + test_cmp expect one_two_repeat +' + +test_expect_success 'apply with --3way --cached clean apply' ' + # Merging side should be similar to applying this patch + git diff ...side >P.diff && + + # The corresponding cleanly applied merge + git reset --hard && + git checkout main~ && + git merge --no-commit side && + git ls-files -s >expect.ls && + + # should succeed + git reset --hard && + git checkout main~ && + git apply --cached --3way P.diff && + git ls-files -s >actual.ls && + print_sanitized_conflicted_diff >actual.diff && + + # The cache should resemble the corresponding merge + # (both files at stage #0) + test_cmp expect.ls actual.ls && + # However the working directory should not change + >expect.diff && + test_cmp expect.diff actual.diff +' + +test_expect_success 'apply with --3way --cached and conflicts' ' + # Merging side should be similar to applying this patch + git diff ...side >P.diff && + + # The corresponding conflicted merge + git reset --hard && + git checkout main^0 && + test_must_fail git merge --no-commit side && + git ls-files -s >expect.ls && + + # should fail to apply + git reset --hard && + git checkout main^0 && + test_must_fail git apply --cached --3way P.diff && + git ls-files -s >actual.ls && + print_sanitized_conflicted_diff >actual.diff && + + # The cache should resemble the corresponding merge + # (one file at stage #0, one file at stages #1 #2 #3) + test_cmp expect.ls actual.ls && + # However the working directory should not change + >expect.diff && + test_cmp expect.diff actual.diff +' + test_done diff --git a/t/t5310-pack-bitmaps.sh b/t/t5310-pack-bitmaps.sh index 40b9f63244..b02838750e 100755 --- a/t/t5310-pack-bitmaps.sh +++ b/t/t5310-pack-bitmaps.sh @@ -461,6 +461,29 @@ test_expect_success 'truncated bitmap fails gracefully (cache)' ' test_i18ngrep corrupted.bitmap.index stderr ' +test_expect_success 'enumerating progress counts pack-reused objects' ' + count=$(git rev-list --objects --all --count) && + git repack -adb && + + # check first with only reused objects; confirm that our progress + # showed the right number, and also that we did pack-reuse as expected. + # Check only the final "done" line of the meter (there may be an + # arbitrary number of intermediate lines ending with CR). + GIT_PROGRESS_DELAY=0 \ + git pack-objects --all --stdout --progress \ + </dev/null >/dev/null 2>stderr && + grep "Enumerating objects: $count, done" stderr && + grep "pack-reused $count" stderr && + + # now the same but with one non-reused object + git commit --allow-empty -m "an extra commit object" && + GIT_PROGRESS_DELAY=0 \ + git pack-objects --all --stdout --progress \ + </dev/null >/dev/null 2>stderr && + grep "Enumerating objects: $((count+1)), done" stderr && + grep "pack-reused $count" stderr +' + # have_delta <obj> <expected_base> # # Note that because this relies on cat-file, it might find _any_ copy of an @@ -554,4 +577,42 @@ test_expect_success 'fetch with bitmaps can reuse old base' ' ) ' +test_expect_success 'pack.preferBitmapTips' ' + git init repo && + test_when_finished "rm -fr repo" && + ( + cd repo && + + # create enough commits that not all are receive bitmap + # coverage even if they are all at the tip of some reference. + test_commit_bulk --message="%s" 103 && + + git rev-list HEAD >commits.raw && + sort <commits.raw >commits && + + git log --format="create refs/tags/%s %H" HEAD >refs && + git update-ref --stdin <refs && + + git repack -adb && + test-tool bitmap list-commits | sort >bitmaps && + + # remember which commits did not receive bitmaps + comm -13 bitmaps commits >before && + test_file_not_empty before && + + # mark the commits which did not receive bitmaps as preferred, + # and generate the bitmap again + perl -pe "s{^}{create refs/tags/include/$. }" <before | + git update-ref --stdin && + git -c pack.preferBitmapTips=refs/tags/include repack -adb && + + # finally, check that the commit(s) without bitmap coverage + # are not the same ones as before + test-tool bitmap list-commits | sort >bitmaps && + comm -13 bitmaps commits >after && + + ! test_cmp before after + ) +' + test_done diff --git a/t/t5572-pull-submodule.sh b/t/t5572-pull-submodule.sh index 29537f4798..4f92a116e1 100755 --- a/t/t5572-pull-submodule.sh +++ b/t/t5572-pull-submodule.sh @@ -42,8 +42,11 @@ git_pull_noff () { $2 git pull --no-ff } -KNOWN_FAILURE_NOFF_MERGE_DOESNT_CREATE_EMPTY_SUBMODULE_DIR=1 -KNOWN_FAILURE_NOFF_MERGE_ATTEMPTS_TO_MERGE_REMOVED_SUBMODULE_FILES=1 +if test "$GIT_TEST_MERGE_ALGORITHM" != ort +then + KNOWN_FAILURE_NOFF_MERGE_DOESNT_CREATE_EMPTY_SUBMODULE_DIR=1 + KNOWN_FAILURE_NOFF_MERGE_ATTEMPTS_TO_MERGE_REMOVED_SUBMODULE_FILES=1 +fi test_submodule_switch_func "git_pull_noff" test_expect_success 'pull --recurse-submodule setup' ' diff --git a/t/t6300-for-each-ref.sh b/t/t6300-for-each-ref.sh index cac7f443d0..9e0214076b 100755 --- a/t/t6300-for-each-ref.sh +++ b/t/t6300-for-each-ref.sh @@ -945,9 +945,9 @@ test_failing_trailer_option () { test_expect_success "$title" ' # error message cannot be checked under i18n test_must_fail git for-each-ref --format="%($option)" refs/heads/main 2>actual && - test_i18ncmp expect actual && + test_cmp expect actual && test_must_fail git for-each-ref --format="%(contents:$option)" refs/heads/main 2>actual && - test_i18ncmp expect actual + test_cmp expect actual ' } @@ -966,7 +966,7 @@ test_expect_success 'if arguments, %(contents:trailers) shows error if colon is fatal: unrecognized %(contents) argument: trailersonly EOF test_must_fail git for-each-ref --format="%(contents:trailersonly)" 2>actual && - test_i18ncmp expect actual + test_cmp expect actual ' test_expect_success 'basic atom: head contents:trailers' ' @@ -1134,4 +1134,14 @@ test_expect_success 'for-each-ref --ignore-case works on multiple sort keys' ' test_cmp expect actual ' +test_expect_success 'for-each-ref reports broken tags' ' + git tag -m "good tag" broken-tag-good HEAD && + git cat-file tag broken-tag-good >good && + sed s/commit/blob/ <good >bad && + bad=$(git hash-object -w -t tag bad) && + git update-ref refs/tags/broken-tag-bad $bad && + test_must_fail git for-each-ref --format="%(*objectname)" \ + refs/tags/broken-tag-* +' + test_done diff --git a/t/t6423-merge-rename-directories.sh b/t/t6423-merge-rename-directories.sh index 379aac0103..7134769149 100755 --- a/t/t6423-merge-rename-directories.sh +++ b/t/t6423-merge-rename-directories.sh @@ -4797,7 +4797,7 @@ test_setup_12f () { ) } -test_expect_merge_algorithm failure success '12f: Trivial directory resolve, caching, all kinds of fun' ' +test_expect_merge_algorithm failure failure '12f: Trivial directory resolve, caching, all kinds of fun' ' test_setup_12f && ( cd 12f && diff --git a/t/t6428-merge-conflicts-sparse.sh b/t/t6428-merge-conflicts-sparse.sh new file mode 100755 index 0000000000..7e8bf497f8 --- /dev/null +++ b/t/t6428-merge-conflicts-sparse.sh @@ -0,0 +1,158 @@ +#!/bin/sh + +test_description="merge cases" + +# The setup for all of them, pictorially, is: +# +# A +# o +# / \ +# O o ? +# \ / +# o +# B +# +# To help make it easier to follow the flow of tests, they have been +# divided into sections and each test will start with a quick explanation +# of what commits O, A, and B contain. +# +# Notation: +# z/{b,c} means files z/b and z/c both exist +# x/d_1 means file x/d exists with content d1. (Purpose of the +# underscore notation is to differentiate different +# files that might be renamed into each other's paths.) + +. ./test-lib.sh +. "$TEST_DIRECTORY"/lib-merge.sh + + +# Testcase basic, conflicting changes in 'numerals' + +test_setup_numerals () { + test_create_repo numerals_$1 && + ( + cd numerals_$1 && + + >README && + test_write_lines I II III >numerals && + git add README numerals && + test_tick && + git commit -m "O" && + + git branch O && + git branch A && + git branch B && + + git checkout A && + test_write_lines I II III IIII >numerals && + git add numerals && + test_tick && + git commit -m "A" && + + git checkout B && + test_write_lines I II III IV >numerals && + git add numerals && + test_tick && + git commit -m "B" && + + cat <<-EOF >expected-index && + H README + M numerals + M numerals + M numerals + EOF + + cat <<-EOF >expected-merge + I + II + III + <<<<<<< HEAD + IIII + ======= + IV + >>>>>>> B^0 + EOF + + ) +} + +test_expect_success 'conflicting entries written to worktree even if sparse' ' + test_setup_numerals plain && + ( + cd numerals_plain && + + git checkout A^0 && + + test_path_is_file README && + test_path_is_file numerals && + + git sparse-checkout init && + git sparse-checkout set README && + + test_path_is_file README && + test_path_is_missing numerals && + + test_must_fail git merge -s recursive B^0 && + + git ls-files -t >index_files && + test_cmp expected-index index_files && + + test_path_is_file README && + test_path_is_file numerals && + + test_cmp expected-merge numerals && + + # 4 other files: + # * expected-merge + # * expected-index + # * index_files + # * others + git ls-files -o >others && + test_line_count = 4 others + ) +' + +test_expect_merge_algorithm failure success 'present-despite-SKIP_WORKTREE handled reasonably' ' + test_setup_numerals in_the_way && + ( + cd numerals_in_the_way && + + git checkout A^0 && + + test_path_is_file README && + test_path_is_file numerals && + + git sparse-checkout init && + git sparse-checkout set README && + + test_path_is_file README && + test_path_is_missing numerals && + + echo foobar >numerals && + + test_must_fail git merge -s recursive B^0 && + + git ls-files -t >index_files && + test_cmp expected-index index_files && + + test_path_is_file README && + test_path_is_file numerals && + + test_cmp expected-merge numerals && + + # There should still be a file with "foobar" in it + grep foobar * && + + # 5 other files: + # * expected-merge + # * expected-index + # * index_files + # * others + # * whatever name was given to the numerals file that had + # "foobar" in it + git ls-files -o >others && + test_line_count = 5 others + ) +' + +test_done diff --git a/t/t6437-submodule-merge.sh b/t/t6437-submodule-merge.sh index 0f92bcf326..e5e89c2045 100755 --- a/t/t6437-submodule-merge.sh +++ b/t/t6437-submodule-merge.sh @@ -6,6 +6,7 @@ GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME . ./test-lib.sh +. "$TEST_DIRECTORY"/lib-merge.sh # # history @@ -328,7 +329,7 @@ test_expect_success 'setup file/submodule conflict' ' ) ' -test_expect_failure 'file/submodule conflict' ' +test_expect_merge_algorithm failure success 'file/submodule conflict' ' test_when_finished "git -C file-submodule reset --hard" && ( cd file-submodule && @@ -437,7 +438,7 @@ test_expect_failure 'directory/submodule conflict; keep submodule clean' ' ) ' -test_expect_failure !FAIL_PREREQS 'directory/submodule conflict; should not treat submodule files as untracked or in the way' ' +test_expect_merge_algorithm failure success !FAIL_PREREQS 'directory/submodule conflict; should not treat submodule files as untracked or in the way' ' test_when_finished "git -C directory-submodule/path reset --hard" && test_when_finished "git -C directory-submodule reset --hard" && ( diff --git a/t/t6438-submodule-directory-file-conflicts.sh b/t/t6438-submodule-directory-file-conflicts.sh index 04bf4be7d7..8df67a0ef9 100755 --- a/t/t6438-submodule-directory-file-conflicts.sh +++ b/t/t6438-submodule-directory-file-conflicts.sh @@ -12,8 +12,11 @@ test_submodule_switch "merge --ff" test_submodule_switch "merge --ff-only" -KNOWN_FAILURE_NOFF_MERGE_DOESNT_CREATE_EMPTY_SUBMODULE_DIR=1 -KNOWN_FAILURE_NOFF_MERGE_ATTEMPTS_TO_MERGE_REMOVED_SUBMODULE_FILES=1 +if test "$GIT_TEST_MERGE_ALGORITHM" != ort +then + KNOWN_FAILURE_NOFF_MERGE_DOESNT_CREATE_EMPTY_SUBMODULE_DIR=1 + KNOWN_FAILURE_NOFF_MERGE_ATTEMPTS_TO_MERGE_REMOVED_SUBMODULE_FILES=1 +fi test_submodule_switch "merge --no-ff" test_done diff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh index 1a1caf8f2e..65b3035371 100755 --- a/t/t9001-send-email.sh +++ b/t/t9001-send-email.sh @@ -415,15 +415,23 @@ test_expect_success $PREREQ 'reject long lines' ' z512=$z64$z64$z64$z64$z64$z64$z64$z64 && clean_fake_sendmail && cp $patches longline.patch && - echo $z512$z512 >>longline.patch && + cat >>longline.patch <<-EOF && + $z512$z512 + not a long line + $z512$z512 + EOF test_must_fail git send-email \ --from="Example <nobody@example.com>" \ --to=nobody@example.com \ --smtp-server="$(pwd)/fake.sendmail" \ --transfer-encoding=8bit \ $patches longline.patch \ - 2>errors && - grep longline.patch errors + 2>actual && + cat >expect <<-\EOF && + fatal: longline.patch:35 is longer than 998 characters + warning: no patches were sent + EOF + test_cmp expect actual ' test_expect_success $PREREQ 'no patch was sent' ' @@ -527,22 +535,33 @@ test_expect_success $PREREQ "--validate respects relative core.hooksPath path" ' --to=nobody@example.com \ --smtp-server="$(pwd)/fake.sendmail" \ --validate \ - longline.patch 2>err && + longline.patch 2>actual && test_path_is_file my-hooks.ran && - grep "rejected by sendemail-validate" err + cat >expect <<-EOF && + fatal: longline.patch: rejected by sendemail-validate hook + fatal: command '"'"'$(pwd)/my-hooks/sendemail-validate'"'"' died with exit code 1 + warning: no patches were sent + EOF + test_cmp expect actual ' test_expect_success $PREREQ "--validate respects absolute core.hooksPath path" ' - test_config core.hooksPath "$(pwd)/my-hooks" && + hooks_path="$(pwd)/my-hooks" && + test_config core.hooksPath "$hooks_path" && test_when_finished "rm my-hooks.ran" && test_must_fail git send-email \ --from="Example <nobody@example.com>" \ --to=nobody@example.com \ --smtp-server="$(pwd)/fake.sendmail" \ --validate \ - longline.patch 2>err && + longline.patch 2>actual && test_path_is_file my-hooks.ran && - grep "rejected by sendemail-validate" err + cat >expect <<-EOF && + fatal: longline.patch: rejected by sendemail-validate hook + fatal: command '"'"'$hooks_path/sendemail-validate'"'"' died with exit code 1 + warning: no patches were sent + EOF + test_cmp expect actual ' for enc in 7bit 8bit quoted-printable base64 diff --git a/t/test-lib-functions.sh b/t/test-lib-functions.sh index 6348e8d733..b823c14027 100644 --- a/t/test-lib-functions.sh +++ b/t/test-lib-functions.sh @@ -1025,13 +1025,6 @@ test_cmp_bin () { cmp "$@" } -# Wrapper for test_cmp which used to be used for -# GIT_TEST_GETTEXT_POISON=false. Only here as a shim for other -# in-flight changes. Should not be used and will be removed soon. -test_i18ncmp () { - test_cmp "$@" -} - # Wrapper for grep which used to be used for # GIT_TEST_GETTEXT_POISON=false. Only here as a shim for other # in-flight changes. Should not be used and will be removed soon. diff --git a/t/test-lib.sh b/t/test-lib.sh index d3f6af6a65..3dec266221 100644 --- a/t/test-lib.sh +++ b/t/test-lib.sh @@ -448,6 +448,8 @@ export EDITOR GIT_DEFAULT_HASH="${GIT_TEST_DEFAULT_HASH:-sha1}" export GIT_DEFAULT_HASH +GIT_TEST_MERGE_ALGORITHM="${GIT_TEST_MERGE_ALGORITHM:-ort}" +export GIT_TEST_MERGE_ALGORITHM # Tests using GIT_TRACE typically don't want <timestamp> <file>:<line> output GIT_TRACE_BARE=1 diff --git a/userdiff.c b/userdiff.c index 3f81a2261c..3c3bbe38b0 100644 --- a/userdiff.c +++ b/userdiff.c @@ -44,6 +44,46 @@ PATTERNS("bash", /* -- */ /* Characters not in the default $IFS value */ "[^ \t]+"), +PATTERNS("bibtex", + "(@[a-zA-Z]{1,}[ \t]*\\{{0,1}[ \t]*[^ \t\"@',\\#}{~%]*).*$", + /* -- */ + "[={}\"]|[^={}\" \t]+"), +PATTERNS("cpp", + /* Jump targets or access declarations */ + "!^[ \t]*[A-Za-z_][A-Za-z_0-9]*:[[:space:]]*($|/[/*])\n" + /* functions/methods, variables, and compounds at top level */ + "^((::[[:space:]]*)?[A-Za-z_].*)$", + /* -- */ + "[a-zA-Z_][a-zA-Z0-9_]*" + "|[-+0-9.e]+[fFlL]?|0[xXbB]?[0-9a-fA-F]+[lLuU]*" + "|[-+*/<>%&^|=!]=|--|\\+\\+|<<=?|>>=?|&&|\\|\\||::|->\\*?|\\.\\*"), +PATTERNS("csharp", + /* Keywords */ + "!^[ \t]*(do|while|for|if|else|instanceof|new|return|switch|case|throw|catch|using)\n" + /* Methods and constructors */ + "^[ \t]*(((static|public|internal|private|protected|new|virtual|sealed|override|unsafe|async)[ \t]+)*[][<>@.~_[:alnum:]]+[ \t]+[<>@._[:alnum:]]+[ \t]*\\(.*\\))[ \t]*$\n" + /* Properties */ + "^[ \t]*(((static|public|internal|private|protected|new|virtual|sealed|override|unsafe)[ \t]+)*[][<>@.~_[:alnum:]]+[ \t]+[@._[:alnum:]]+)[ \t]*$\n" + /* Type definitions */ + "^[ \t]*(((static|public|internal|private|protected|new|unsafe|sealed|abstract|partial)[ \t]+)*(class|enum|interface|struct)[ \t]+.*)$\n" + /* Namespace */ + "^[ \t]*(namespace[ \t]+.*)$", + /* -- */ + "[a-zA-Z_][a-zA-Z0-9_]*" + "|[-+0-9.e]+[fFlL]?|0[xXbB]?[0-9a-fA-F]+[lL]?" + "|[-+*/<>%&^|=!]=|--|\\+\\+|<<=?|>>=?|&&|\\|\\||::|->"), +IPATTERN("css", + "![:;][[:space:]]*$\n" + "^[:[@.#]?[_a-z0-9].*$", + /* -- */ + /* + * This regex comes from W3C CSS specs. Should theoretically also + * allow ISO 10646 characters U+00A0 and higher, + * but they are not handled in this regex. + */ + "-?[_a-zA-Z][-_a-zA-Z0-9]*" /* identifiers */ + "|-?[0-9]+|\\#[0-9a-fA-F]+" /* numbers */ +), PATTERNS("dts", "!;\n" "!=\n" @@ -83,7 +123,9 @@ IPATTERN("fortran", * they would have been matched above as a variable anyway. */ "|[-+]?[0-9.]+([AaIiDdEeFfLlTtXx][Ss]?[-+]?[0-9.]*)?(_[a-zA-Z0-9][a-zA-Z0-9_]*)?" "|//|\\*\\*|::|[/<>=]="), -IPATTERN("fountain", "^((\\.[^.]|(int|ext|est|int\\.?/ext|i/e)[. ]).*)$", +IPATTERN("fountain", + "^((\\.[^.]|(int|ext|est|int\\.?/ext|i/e)[. ]).*)$", + /* -- */ "[^ \t-]+"), PATTERNS("golang", /* Functions */ @@ -94,7 +136,9 @@ PATTERNS("golang", "[a-zA-Z_][a-zA-Z0-9_]*" "|[-+0-9.eE]+i?|0[xX]?[0-9a-fA-F]+i?" "|[-+*/<>%&^|=!:]=|--|\\+\\+|<<=?|>>=?|&\\^=?|&&|\\|\\||<-|\\.{3}"), -PATTERNS("html", "^[ \t]*(<[Hh][1-6]([ \t].*)?>.*)$", +PATTERNS("html", + "^[ \t]*(<[Hh][1-6]([ \t].*)?>.*)$", + /* -- */ "[^<>= \t]+"), PATTERNS("java", "!^[ \t]*(catch|do|for|if|instanceof|new|return|switch|throw|while)\n" @@ -106,6 +150,7 @@ PATTERNS("java", "|--|\\+\\+|<<=?|>>>?=?|&&|\\|\\|"), PATTERNS("markdown", "^ {0,3}#{1,6}[ \t].*", + /* -- */ "[^<>= \t]+"), PATTERNS("matlab", /* @@ -114,6 +159,7 @@ PATTERNS("matlab", * that is understood by both. */ "^[[:space:]]*((classdef|function)[[:space:]].*)$|^(%%%?|##)[[:space:]].*$", + /* -- */ "[a-zA-Z_][a-zA-Z0-9_]*|[-+0-9.e]+|[=~<>]=|\\.[*/\\^']|\\|\\||&&"), PATTERNS("objc", /* Negate C statements that can look like functions */ @@ -129,9 +175,8 @@ PATTERNS("objc", "|[-+0-9.e]+[fFlL]?|0[xXbB]?[0-9a-fA-F]+[lL]?" "|[-+*/<>%&^|=!]=|--|\\+\\+|<<=?|>>=?|&&|\\|\\||::|->"), PATTERNS("pascal", - "^(((class[ \t]+)?(procedure|function)|constructor|destructor|interface|" - "implementation|initialization|finalization)[ \t]*.*)$" - "\n" + "^(((class[ \t]+)?(procedure|function)|constructor|destructor|interface" + "|implementation|initialization|finalization)[ \t]*.*)$\n" "^(.*=[ \t]*(class|record).*)$", /* -- */ "[a-zA-Z_][a-zA-Z0-9_]*" @@ -174,13 +219,15 @@ PATTERNS("php", "[a-zA-Z_][a-zA-Z0-9_]*" "|[-+0-9.e]+|0[xXbB]?[0-9a-fA-F]+" "|[-+*/<>%&^|=!.]=|--|\\+\\+|<<=?|>>=?|===|&&|\\|\\||::|->"), -PATTERNS("python", "^[ \t]*((class|(async[ \t]+)?def)[ \t].*)$", +PATTERNS("python", + "^[ \t]*((class|(async[ \t]+)?def)[ \t].*)$", /* -- */ "[a-zA-Z_][a-zA-Z0-9_]*" "|[-+0-9.e]+[jJlL]?|0[xX]?[0-9a-fA-F]+[lL]?" "|[-+*/<>%&^|=!]=|//=?|<<=?|>>=?|\\*\\*=?"), /* -- */ -PATTERNS("ruby", "^[ \t]*((class|module|def)[ \t].*)$", +PATTERNS("ruby", + "^[ \t]*((class|module|def)[ \t].*)$", /* -- */ "(@|@@|\\$)?[a-zA-Z_][a-zA-Z0-9_]*" "|[-+0-9.e]+|0[xXbB]?[0-9a-fA-F]+|\\?(\\\\C-)?(\\\\M-)?." @@ -191,46 +238,17 @@ PATTERNS("rust", "[a-zA-Z_][a-zA-Z0-9_]*" "|[0-9][0-9_a-fA-Fiosuxz]*(\\.([0-9]*[eE][+-]?)?[0-9_fF]*)?" "|[-+*\\/<>%&^|=!:]=|<<=?|>>=?|&&|\\|\\||->|=>|\\.{2}=|\\.{3}|::"), -PATTERNS("bibtex", "(@[a-zA-Z]{1,}[ \t]*\\{{0,1}[ \t]*[^ \t\"@',\\#}{~%]*).*$", - "[={}\"]|[^={}\" \t]+"), -PATTERNS("tex", "^(\\\\((sub)*section|chapter|part)\\*{0,1}\\{.*)$", - "\\\\[a-zA-Z@]+|\\\\.|[a-zA-Z0-9\x80-\xff]+"), -PATTERNS("cpp", - /* Jump targets or access declarations */ - "!^[ \t]*[A-Za-z_][A-Za-z_0-9]*:[[:space:]]*($|/[/*])\n" - /* functions/methods, variables, and compounds at top level */ - "^((::[[:space:]]*)?[A-Za-z_].*)$", - /* -- */ - "[a-zA-Z_][a-zA-Z0-9_]*" - "|[-+0-9.e]+[fFlL]?|0[xXbB]?[0-9a-fA-F]+[lLuU]*" - "|[-+*/<>%&^|=!]=|--|\\+\\+|<<=?|>>=?|&&|\\|\\||::|->\\*?|\\.\\*"), -PATTERNS("csharp", - /* Keywords */ - "!^[ \t]*(do|while|for|if|else|instanceof|new|return|switch|case|throw|catch|using)\n" - /* Methods and constructors */ - "^[ \t]*(((static|public|internal|private|protected|new|virtual|sealed|override|unsafe|async)[ \t]+)*[][<>@.~_[:alnum:]]+[ \t]+[<>@._[:alnum:]]+[ \t]*\\(.*\\))[ \t]*$\n" - /* Properties */ - "^[ \t]*(((static|public|internal|private|protected|new|virtual|sealed|override|unsafe)[ \t]+)*[][<>@.~_[:alnum:]]+[ \t]+[@._[:alnum:]]+)[ \t]*$\n" - /* Type definitions */ - "^[ \t]*(((static|public|internal|private|protected|new|unsafe|sealed|abstract|partial)[ \t]+)*(class|enum|interface|struct)[ \t]+.*)$\n" - /* Namespace */ - "^[ \t]*(namespace[ \t]+.*)$", - /* -- */ - "[a-zA-Z_][a-zA-Z0-9_]*" - "|[-+0-9.e]+[fFlL]?|0[xXbB]?[0-9a-fA-F]+[lL]?" - "|[-+*/<>%&^|=!]=|--|\\+\\+|<<=?|>>=?|&&|\\|\\||::|->"), -IPATTERN("css", - "![:;][[:space:]]*$\n" - "^[:[@.#]?[_a-z0-9].*$", - /* -- */ +PATTERNS("scheme", + "^[\t ]*(\\(((define|def(struct|syntax|class|method|rules|record|proto|alias)?)[-*/ \t]|(library|module|struct|class)[*+ \t]).*)$", /* - * This regex comes from W3C CSS specs. Should theoretically also - * allow ISO 10646 characters U+00A0 and higher, - * but they are not handled in this regex. + * R7RS valid identifiers include any sequence enclosed + * within vertical lines having no backslashes */ - "-?[_a-zA-Z][-_a-zA-Z0-9]*" /* identifiers */ - "|-?[0-9]+|\\#[0-9a-fA-F]+" /* numbers */ -), + "\\|([^\\\\]*)\\|" + /* All other words should be delimited by spaces or parentheses */ + "|([^][)(}{[ \t])+"), +PATTERNS("tex", "^(\\\\((sub)*section|chapter|part)\\*{0,1}\\{.*)$", + "\\\\[a-zA-Z@]+|\\\\.|[a-zA-Z0-9\x80-\xff]+"), { "default", NULL, -1, { NULL, 0 } }, }; #undef PATTERNS @@ -250,20 +268,33 @@ static struct userdiff_driver driver_false = { { NULL, 0 } }; -static struct userdiff_driver *userdiff_find_by_namelen(const char *k, size_t len) +struct find_by_namelen_data { + const char *name; + size_t len; + struct userdiff_driver *driver; +}; + +static int userdiff_find_by_namelen_cb(struct userdiff_driver *driver, + enum userdiff_driver_type type, void *priv) { - int i; - for (i = 0; i < ndrivers; i++) { - struct userdiff_driver *drv = drivers + i; - if (!strncmp(drv->name, k, len) && !drv->name[len]) - return drv; - } - for (i = 0; i < ARRAY_SIZE(builtin_drivers); i++) { - struct userdiff_driver *drv = builtin_drivers + i; - if (!strncmp(drv->name, k, len) && !drv->name[len]) - return drv; + struct find_by_namelen_data *cb_data = priv; + + if (!strncmp(driver->name, cb_data->name, cb_data->len) && + !driver->name[cb_data->len]) { + cb_data->driver = driver; + return 1; /* tell the caller to stop iterating */ } - return NULL; + return 0; +} + +static struct userdiff_driver *userdiff_find_by_namelen(const char *name, size_t len) +{ + struct find_by_namelen_data udcbdata = { + .name = name, + .len = len, + }; + for_each_userdiff_driver(userdiff_find_by_namelen_cb, &udcbdata); + return udcbdata.driver; } static int parse_funcname(struct userdiff_funcname *f, const char *k, @@ -370,3 +401,36 @@ struct userdiff_driver *userdiff_get_textconv(struct repository *r, return driver; } + +static int for_each_userdiff_driver_list(each_userdiff_driver_fn fn, + enum userdiff_driver_type type, void *cb_data, + struct userdiff_driver *drv, + int drv_size) +{ + int i; + int ret; + for (i = 0; i < drv_size; i++) { + struct userdiff_driver *item = drv + i; + if ((ret = fn(item, type, cb_data))) + return ret; + } + return 0; +} + +int for_each_userdiff_driver(each_userdiff_driver_fn fn, void *cb_data) +{ + int ret; + + ret = for_each_userdiff_driver_list(fn, USERDIFF_DRIVER_TYPE_CUSTOM, + cb_data, drivers, ndrivers); + if (ret) + return ret; + + ret = for_each_userdiff_driver_list(fn, USERDIFF_DRIVER_TYPE_BUILTIN, + cb_data, builtin_drivers, + ARRAY_SIZE(builtin_drivers)); + if (ret) + return ret; + + return 0; +} diff --git a/userdiff.h b/userdiff.h index 203057e13e..aee91bc77e 100644 --- a/userdiff.h +++ b/userdiff.h @@ -21,6 +21,12 @@ struct userdiff_driver { struct notes_cache *textconv_cache; int textconv_want_cache; }; +enum userdiff_driver_type { + USERDIFF_DRIVER_TYPE_BUILTIN = 1<<0, + USERDIFF_DRIVER_TYPE_CUSTOM = 1<<1, +}; +typedef int (*each_userdiff_driver_fn)(struct userdiff_driver *, + enum userdiff_driver_type, void *); int userdiff_config(const char *k, const char *v); struct userdiff_driver *userdiff_find_by_name(const char *name); @@ -34,4 +40,11 @@ struct userdiff_driver *userdiff_find_by_path(struct index_state *istate, struct userdiff_driver *userdiff_get_textconv(struct repository *r, struct userdiff_driver *driver); +/* + * Iterate over all userdiff drivers. The userdiff_driver_type + * argument to each_userdiff_driver_fn indicates their type. Return + * non-zero to exit early from the loop. + */ +int for_each_userdiff_driver(each_userdiff_driver_fn, void *); + #endif /* USERDIFF */ |
