diff --git a/Documentation/RelNotes/2.56.0.adoc b/Documentation/RelNotes/2.56.0.adoc index 0f8676fe93f158..c4bf8b3228f6fd 100644 --- a/Documentation/RelNotes/2.56.0.adoc +++ b/Documentation/RelNotes/2.56.0.adoc @@ -85,6 +85,11 @@ UI, Workflows & Features syscall fails has also been improved to name both the source and the destination. + * 'git add' has been taught a new '--resolved' option to stage + conflict-resolved paths, while leaving unrelated local changes + unstaged. It scans the unmerged paths for leftover conflict + markers and aborts if any are found. + Performance, Internal Implementation, Development Support etc. -------------------------------------------------------------- @@ -334,6 +339,22 @@ Performance, Internal Implementation, Development Support etc. exit codes (0 for success, 1 for non-ancestor, 128 for errors) and to ensure it cannot be combined with '--all'. + * The 'TRACE2_ANCESTRY' prerequisite in the 't0213' test script has been + refined to avoid failures under user-mode emulation by verifying that + the ancestry collector reports the expected process names rather than + the emulator binary name. + + * Concurrent downloads of packfiles via packfile URIs and dumb HTTP are + safer by avoiding concurrent appends to the staging file. Opening in + read-write mode with separate file offsets prevents corruption and + preserves resumability. 'fetch-pack' now tolerates pre-existing + '.keep' files. + + * The 'ssh-agent' tests in 't7528' have been fixed to work when the + user's login shell is csh-like, by explicitly passing '-s' to + 'ssh-agent' to force Bourne shell syntax. + (merge d5dd17756d kl/t7528-ssh-agent-for-csh-users later to maint). + Fixes since v2.55 ----------------- diff --git a/Documentation/git-add.adoc b/Documentation/git-add.adoc index 941135dc637d90..16b06e38e185d1 100644 --- a/Documentation/git-add.adoc +++ b/Documentation/git-add.adoc @@ -11,7 +11,7 @@ SYNOPSIS git add [--verbose | -v] [--dry-run | -n] [--force | -f] [--interactive | -i] [--patch | -p] [--edit | -e] [--[no-]all | -A | --[no-]ignore-removal | [--update | -u]] [--sparse] [--intent-to-add | -N] [--refresh] [--ignore-errors] [--ignore-missing] [--renormalize] - [--chmod=(+|-)x] [--pathspec-from-file= [--pathspec-file-nul]] + [--resolved] [--chmod=(+|-)x] [--pathspec-from-file= [--pathspec-file-nul]] [--] [...] DESCRIPTION @@ -195,6 +195,14 @@ for `git add --no-all ...`, i.e. ignored removed files. while a _CRLF_ cleans to _LF_, a _CRCRLF_ sequence is only partially cleaned to _CRLF_. +`--resolved`:: + Update the index for unmerged paths matching __ where + no conflict markers remain in the working tree. Unmerged paths + without conflict markers (including binary files and file + deletions) are staged as resolved, while any path with leftover + conflict markers causes the command to refuse to stage any files. + Cannot be combined with `-u` or `-A`. + `--chmod=(+|-)x`:: Override the executable bit of the added files. The executable bit is only changed in the index, the files on disk are left diff --git a/Documentation/git-http-fetch.adoc b/Documentation/git-http-fetch.adoc index 2200f073c47120..45e0d3d07c73cf 100644 --- a/Documentation/git-http-fetch.adoc +++ b/Documentation/git-http-fetch.adoc @@ -48,13 +48,15 @@ commit-id:: line (which is not expected in this case), 'git http-fetch' fetches the packfile directly at the given URL and uses index-pack to generate corresponding .idx and .keep files. - The hash is used to determine the name of the temporary file and is - arbitrary. The output of index-pack is printed to stdout. Requires - --index-pack-args. + The hash is used to determine the name of the temporary file. It need + not be the pack hash, but it must uniquely identify the pack contents + for resumption. The output of index-pack is printed to stdout. Requires + one or more --index-pack-arg options. ---index-pack-args=:: - For internal use only. The command to run on the contents of the - downloaded pack. Arguments are URL-encoded separated by spaces. +--index-pack-arg=:: + For internal use only. The first instance specifies the command run on + the contents of the downloaded pack. Subsequent instances specify its + arguments. --recover:: Verify that everything reachable from target is fetched. Used after diff --git a/builtin/add.c b/builtin/add.c index 60ffbede2be58a..eab8f03cad31d6 100644 --- a/builtin/add.c +++ b/builtin/add.c @@ -26,6 +26,7 @@ #include "strvec.h" #include "submodule.h" #include "add-interactive.h" +#include "merge-ll.h" static const char * const builtin_add_usage[] = { N_("git add [] [--] ..."), @@ -35,6 +36,7 @@ static int patch_interactive, add_interactive, edit_interactive; static struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT; static int take_worktree_changes; static int add_renormalize; +static int add_resolved; static int pathspec_file_nul; static int include_sparse; static const char *pathspec_from_file; @@ -265,6 +267,7 @@ static struct option builtin_add_options[] = { OPT__FORCE(&ignored_too, N_("allow adding otherwise ignored files"), 0), OPT_BOOL('u', "update", &take_worktree_changes, N_("update tracked files")), OPT_BOOL(0, "renormalize", &add_renormalize, N_("renormalize EOL of tracked files (implies -u)")), + OPT_BOOL(0, "resolved", &add_resolved, N_("add conflict-resolved tracked files")), OPT_BOOL('N', "intent-to-add", &intent_to_add, N_("record only the fact that the path will be added later")), OPT_BOOL('A', "all", &addremove_explicit, N_("add changes from all tracked and untracked files")), OPT_CALLBACK_F(0, "ignore-removal", &addremove_explicit, @@ -379,6 +382,76 @@ static int add_files(struct repository *repo, struct dir_struct *dir, int flags) return exit_status; } +static int failed_to_add(int flags, const char *path) +{ + if (!(flags & ADD_CACHE_IGNORE_ERRORS)) + die(_("updating file '%s' failed"), path); + return 1; +} + +static int add_resolved_files(struct repository *repo, + const struct pathspec *pathspec, + int flags) +{ + struct index_state *istate = repo->index; + struct string_list unmerged_paths = STRING_LIST_INIT_DUP; + struct string_list unresolved_paths = STRING_LIST_INIT_DUP; + int exit_status = 0; + size_t i; + + for (i = 0; i < istate->cache_nr; i++) { + struct cache_entry *ce = istate->cache[i]; + if (!ce_stage(ce)) + continue; + if (pathspec->nr && !ce_path_match(istate, ce, pathspec, NULL)) + continue; + if (!unmerged_paths.nr || + strcmp(unmerged_paths.items[unmerged_paths.nr - 1].string, ce->name)) + string_list_append(&unmerged_paths, ce->name); + } + + if (!unmerged_paths.nr) { + string_list_clear(&unmerged_paths, 0); + return 0; + } + + for (i = 0; i < unmerged_paths.nr; i++) { + const char *path = unmerged_paths.items[i].string; + struct stat st; + + if (!lstat(path, &st) && S_ISREG(st.st_mode)) { + if (has_conflict_markers(istate, path)) + string_list_append(&unresolved_paths, path); + } + } + + if (unresolved_paths.nr) { + struct strbuf sb = STRBUF_INIT; + for (i = 0; i < unresolved_paths.nr; i++) + strbuf_addf(&sb, "\t%s\n", unresolved_paths.items[i].string); + die(_("the following paths still have conflict markers:\n%s"), sb.buf); + } + + for (i = 0; i < unmerged_paths.nr; i++) { + const char *path = unmerged_paths.items[i].string; + struct stat st; + + if (lstat(path, &st)) { + if (errno != ENOENT) + die_errno(_("cannot lstat: '%s'"), path); + if (remove_file_from_index_with_flags(istate, path, flags)) + exit_status = failed_to_add(flags, path); + } else { + if (add_file_to_index(istate, path, flags)) + exit_status = failed_to_add(flags, path); + } + } + + string_list_clear(&unmerged_paths, 0); + string_list_clear(&unresolved_paths, 0); + return exit_status; +} + int cmd_add(int argc, const char **argv, const char *prefix, @@ -438,8 +511,9 @@ int cmd_add(int argc, else if (take_worktree_changes && ADDREMOVE_DEFAULT) addremove = 0; /* "-u" was given but not "-A" */ - if (addremove && take_worktree_changes) - die(_("options '%s' and '%s' cannot be used together"), "-A", "-u"); + die_for_incompatible_opt3(take_worktree_changes, "-u/--update", + 0 < addremove_explicit, "-A/--all", + add_resolved, "--resolved"); if (!show_only && ignore_missing) die(_("the option '%s' requires '%s'"), "--ignore-missing", "--dry-run"); @@ -448,8 +522,11 @@ int cmd_add(int argc, chmod_arg[1] != 'x' || chmod_arg[2])) die(_("--chmod param '%s' must be either -x or +x"), chmod_arg); - add_new_files = !take_worktree_changes && !refresh_only && !add_renormalize; - require_pathspec = !(take_worktree_changes || (0 < addremove_explicit)); + add_new_files = !take_worktree_changes && !refresh_only && + !add_renormalize && !add_resolved; + require_pathspec = !(take_worktree_changes || + (0 < addremove_explicit) || + add_resolved); repo_hold_locked_index(repo, &lock_file, LOCK_DIE_ON_ERROR); @@ -481,7 +558,8 @@ int cmd_add(int argc, return 0; } - if (!take_worktree_changes && addremove_explicit < 0 && pathspec.nr) + if (!take_worktree_changes && !add_resolved && + addremove_explicit < 0 && pathspec.nr) /* Turn "git add pathspec..." to "git add -A pathspec..." */ addremove = 1; @@ -584,7 +662,9 @@ int cmd_add(int argc, odb_transaction_begin_or_die(repo->objects, &transaction, 0); ps_matched = xcalloc(pathspec.nr, 1); - if (add_renormalize) + if (add_resolved) + exit_status |= add_resolved_files(repo, &pathspec, flags); + else if (add_renormalize) exit_status |= renormalize_tracked_files(repo, &pathspec, flags); else exit_status |= add_files_to_cache(repo, prefix, diff --git a/diff.c b/diff.c index 589c1969e45a4e..cfe515af4e1759 100644 --- a/diff.c +++ b/diff.c @@ -3519,29 +3519,6 @@ struct checkdiff_t { int last_line_kind; }; -static int is_conflict_marker(const char *line, int marker_size, unsigned long len) -{ - char firstchar; - int cnt; - - if (len < marker_size + 1) - return 0; - firstchar = line[0]; - switch (firstchar) { - case '=': case '>': case '<': case '|': - break; - default: - return 0; - } - for (cnt = 1; cnt < marker_size; cnt++) - if (line[cnt] != firstchar) - return 0; - /* line[1] through line[marker_size-1] are same as firstchar */ - if (len < marker_size + 1 || !isspace(line[marker_size])) - return 0; - return 1; -} - static void checkdiff_consume_hunk(void *priv, long ob UNUSED, long on UNUSED, long nb, long nn UNUSED, @@ -3571,7 +3548,7 @@ static int checkdiff_consume(void *priv, char *line, unsigned long len) if (line[0] == '+') { unsigned bad; data->lineno++; - if (is_conflict_marker(line + 1, marker_size, len - 1)) { + if (is_conflict_marker_line(line + 1, len - 1, marker_size)) { data->status |= 1; fprintf(data->o->file, "%s%s:%d: leftover conflict marker\n", diff --git a/fetch-pack.c b/fetch-pack.c index 922a9b25812c68..626f799712ec2e 100644 --- a/fetch-pack.c +++ b/fetch-pack.c @@ -1854,9 +1854,10 @@ static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args, } for (i = 0; i < packfile_uris.nr; i++) { + bool created_keep; int j; struct child_process cmd = CHILD_PROCESS_INIT; - char packname[GIT_MAX_HEXSZ + 1]; + char packhash[GIT_MAX_HEXSZ + 1]; const char *uri = packfile_uris.items[i].string + the_hash_algo->hexsz + 1; @@ -1874,16 +1875,17 @@ static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args, if (start_command(&cmd)) die("fetch-pack: unable to spawn http-fetch"); - if (read_in_full(cmd.out, packname, 5) < 0 || - memcmp(packname, "keep\t", 5)) - die("fetch-pack: expected keep then TAB at start of http-fetch output"); + if (read_in_full(cmd.out, packhash, 5) != 5 || + (memcmp(packhash, "keep\t", 5) && + memcmp(packhash, "pack\t", 5))) + die("fetch-pack: expected pack or keep then TAB at start of http-fetch output"); + created_keep = !memcmp(packhash, "keep\t", 5); - if (read_in_full(cmd.out, packname, - the_hash_algo->hexsz + 1) < 0 || - packname[the_hash_algo->hexsz] != '\n') - die("fetch-pack: expected hash then LF at end of http-fetch output"); - - packname[the_hash_algo->hexsz] = '\0'; + if (read_in_full(cmd.out, packhash, + the_hash_algo->hexsz + 1) != the_hash_algo->hexsz + 1 || + packhash[the_hash_algo->hexsz] != '\n') + die("fetch-pack: expected hash then LF in http-fetch output"); + packhash[the_hash_algo->hexsz] = '\0'; parse_gitmodules_oids(cmd.out, &fsck_options.gitmodules_found); @@ -1892,16 +1894,17 @@ static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args, if (finish_command(&cmd)) die("fetch-pack: unable to finish http-fetch"); - if (memcmp(packfile_uris.items[i].string, packname, + if (memcmp(packfile_uris.items[i].string, packhash, the_hash_algo->hexsz)) die("fetch-pack: pack downloaded from %s does not match expected hash %.*s", uri, (int) the_hash_algo->hexsz, packfile_uris.items[i].string); - string_list_append_nodup(pack_lockfiles, - xstrfmt("%s/pack/pack-%s.keep", - repo_get_object_directory(the_repository), - packname)); + if (created_keep) + string_list_append_nodup(pack_lockfiles, + xstrfmt("%s/pack/pack-%s.keep", + repo_get_object_directory(the_repository), + packhash)); } string_list_clear(&packfile_uris, 0); strvec_clear(&index_pack_args); diff --git a/http-fetch.c b/http-fetch.c index f9b6ecb0616fe0..05f68f306a5821 100644 --- a/http-fetch.c +++ b/http-fetch.c @@ -70,7 +70,8 @@ static void fetch_single_packfile(struct object_id *packfile_hash, if (start_active_slot(preq->slot)) { run_active_slot(preq->slot); - if (results.curl_result != CURLE_OK) { + if (results.curl_result != CURLE_OK && + results.http_code != 416) { struct url_info url; char *nurl = url_normalize(preq->url, &url); if (!nurl || !git_env_bool("GIT_TRACE_REDACT", 1)) { @@ -155,7 +156,7 @@ int cmd_main(int argc, const char **argv) if (packfile) { if (!index_pack_args.nr) - die(_("the option '%s' requires '%s'"), "--packfile", "--index-pack-args"); + die(_("the option '%s' requires '%s'"), "--packfile", "--index-pack-arg"); fetch_single_packfile(&packfile_hash, argv[arg], index_pack_args.v); @@ -164,7 +165,7 @@ int cmd_main(int argc, const char **argv) } if (index_pack_args.nr) - die(_("the option '%s' requires '%s'"), "--index-pack-args", "--packfile"); + die(_("the option '%s' requires '%s'"), "--index-pack-arg", "--packfile"); if (commits_on_stdin) { commits = walker_targets_stdin(&commit_id, &write_ref); diff --git a/http-push.c b/http-push.c index 94a1fac9ab0fcd..786a2e9c0d0546 100644 --- a/http-push.c +++ b/http-push.c @@ -595,7 +595,8 @@ static void finish_request(struct transfer_request *request) } else if (request->state == RUN_FETCH_PACKED) { int fail = 1; - if (request->curl_result != CURLE_OK) { + if (request->curl_result != CURLE_OK && + request->http_code != 416) { fprintf(stderr, "Unable to get pack file %s\n%s", request->url, curl_errorstr); } else { diff --git a/http-walker.c b/http-walker.c index b58a3b2a92be38..abafca84d65441 100644 --- a/http-walker.c +++ b/http-walker.c @@ -451,7 +451,8 @@ static int http_fetch_pack(struct walker *walker, struct alt_base *repo, if (start_active_slot(preq->slot)) { run_active_slot(preq->slot); - if (results.curl_result != CURLE_OK) { + if (results.curl_result != CURLE_OK && + results.http_code != 416) { error("Unable to get pack file %s\n%s", preq->url, curl_errorstr); goto abort; diff --git a/http.c b/http.c index caccf2108e4479..a0d399b2745ae6 100644 --- a/http.c +++ b/http.c @@ -2688,10 +2688,13 @@ int finish_http_pack_request(struct http_pack_request *preq) int tmpfile_fd; int ret = 0; + /* Another downloader may unlink the staging path while we index it. */ + tmpfile_fd = xdup(fileno(preq->packfile)); fclose(preq->packfile); preq->packfile = NULL; - - tmpfile_fd = xopen(preq->tmpfile.buf, O_RDONLY); + if (lseek(tmpfile_fd, 0, SEEK_SET) < 0) + die_errno("unable to seek local file %s for pack", + preq->tmpfile.buf); ip.git_cmd = 1; ip.in = tmpfile_fd; @@ -2704,13 +2707,8 @@ int finish_http_pack_request(struct http_pack_request *preq) else ip.no_stdout = 1; - if (run_command(&ip)) { + if (run_command(&ip)) ret = -1; - goto cleanup; - } - -cleanup: - close(tmpfile_fd); unlink(preq->tmpfile.buf); return ret; } @@ -2738,22 +2736,45 @@ struct http_pack_request *new_http_pack_request( struct http_pack_request *new_direct_http_pack_request( const unsigned char *packed_git_hash, char *url) { - off_t prev_posn = 0; + off_t prev_posn; struct http_pack_request *preq; + int fd; CALLOC_ARRAY(preq, 1); strbuf_init(&preq->tmpfile, 0); - preq->url = url; odb_pack_name(the_repository, &preq->tmpfile, packed_git_hash, "pack"); strbuf_addstr(&preq->tmpfile, ".temp"); - preq->packfile = fopen(preq->tmpfile.buf, "a"); - if (!preq->packfile) { - error("Unable to open local file %s for pack", - preq->tmpfile.buf); + /* + * MinGW's non-append O_RDWR open grants FILE_SHARE_DELETE only for an + * existing file; reopen a newly created file so others may unlink it. + */ + for (;;) { + fd = open(preq->tmpfile.buf, O_RDWR); + if (fd >= 0 || errno != ENOENT) + break; + fd = open(preq->tmpfile.buf, O_RDWR | O_CREAT | O_EXCL, 0666); + if (fd >= 0) { + close(fd); + continue; + } + if (errno != EEXIST) + break; + } + if (fd < 0) { + error_errno("unable to open local file %s for pack", + preq->tmpfile.buf); goto abort; } + prev_posn = lseek(fd, 0, SEEK_END); + if (prev_posn < 0) { + error_errno("unable to seek local file %s for pack", + preq->tmpfile.buf); + close(fd); + goto abort; + } + preq->packfile = xfdopen(fd, "w"); preq->slot = get_active_slot(); preq->headers = object_request_headers(); @@ -2762,12 +2783,7 @@ struct http_pack_request *new_direct_http_pack_request( curl_easy_setopt(preq->slot->curl, CURLOPT_URL, preq->url); curl_easy_setopt(preq->slot->curl, CURLOPT_HTTPHEADER, preq->headers); - /* - * If there is data present from a previous transfer attempt, - * resume where it left off - */ - prev_posn = ftello(preq->packfile); - if (prev_posn>0) { + if (prev_posn > 0) { if (http_is_verbose) fprintf(stderr, "Resuming fetch of pack %s at byte %"PRIuMAX"\n", diff --git a/merge-ll.c b/merge-ll.c index fafe2c91971856..ef5287dee8f11b 100644 --- a/merge-ll.c +++ b/merge-ll.c @@ -468,3 +468,59 @@ int ll_merge_marker_size(struct index_state *istate, const char *path) } return marker_size; } + +int is_conflict_marker_line(const char *line, unsigned long len, int marker_size) +{ + char firstchar; + int cnt; + + if (len < marker_size + 1) + return 0; + + firstchar = line[0]; + switch (firstchar) { + case '=': case '>': case '<': case '|': + break; + default: + return 0; + } + + for (cnt = 1; cnt < marker_size; cnt++) { + if (line[cnt] != firstchar) + return 0; + } + + if (((firstchar == '<') || (firstchar == '>')) && + line[marker_size] != ' ') + return 0; + + if (!isspace((unsigned char)line[marker_size])) + return 0; + + return firstchar; +} + +int has_conflict_markers(struct index_state *istate, const char *path) +{ + FILE *f; + struct strbuf sb = STRBUF_INIT; + int marker_size = ll_merge_marker_size(istate, path); + int has_markers = 0; + + f = fopen(path, "r"); + if (!f) + return 0; + + while (strbuf_getwholeline(&sb, f, '\n') != EOF) { + if (is_conflict_marker_line(sb.buf, sb.len, marker_size)) { + has_markers = 1; + break; + } + if (buffer_is_binary(sb.buf, + ULONG_MAX <= sb.len ? ULONG_MAX : sb.len)) + break; + } + fclose(f); + strbuf_release(&sb); + return has_markers; +} diff --git a/merge-ll.h b/merge-ll.h index d038ee0c1e81f7..f26aef238d2c21 100644 --- a/merge-ll.h +++ b/merge-ll.h @@ -109,6 +109,8 @@ enum ll_merge_result ll_merge(mmbuffer_t *result_buf, const struct ll_merge_options *opts); int ll_merge_marker_size(struct index_state *istate, const char *path); +int is_conflict_marker_line(const char *line, unsigned long len, int marker_size); +int has_conflict_markers(struct index_state *istate, const char *path); void reset_merge_attributes(void); #endif diff --git a/read-cache-ll.h b/read-cache-ll.h index 71b87615ebc6d3..8eb266cfd13308 100644 --- a/read-cache-ll.h +++ b/read-cache-ll.h @@ -391,11 +391,14 @@ int remove_index_entry_at(struct index_state *, int pos); void remove_marked_cache_entries(struct index_state *istate, int invalidate); int remove_file_from_index(struct index_state *, const char *path); +int remove_file_from_index_with_flags(struct index_state *, const char *, int); + #define ADD_CACHE_VERBOSE 1 #define ADD_CACHE_PRETEND 2 #define ADD_CACHE_IGNORE_ERRORS 4 #define ADD_CACHE_IGNORE_REMOVAL 8 #define ADD_CACHE_INTENT 16 + /* * These two are used to add the contents of the file at path * to the index, marking the working tree up-to-date by storing diff --git a/read-cache.c b/read-cache.c index 6c449f393d8d4d..c0769848587b1a 100644 --- a/read-cache.c +++ b/read-cache.c @@ -636,6 +636,20 @@ int remove_file_from_index(struct index_state *istate, const char *path) return 0; } +int remove_file_from_index_with_flags(struct index_state *istate, + const char *path, + int flags) +{ + int verbose = flags & (ADD_CACHE_VERBOSE | ADD_CACHE_PRETEND); + int pretend = flags & ADD_CACHE_PRETEND; + + if (verbose) + printf(_("remove '%s'\n"), path); + if (pretend) + return 0; + return remove_file_from_index(istate, path); +} + static int compare_name(struct cache_entry *ce, const char *path, int namelen) { return namelen != ce_namelen(ce) || memcmp(path, ce->name, namelen); @@ -3909,32 +3923,33 @@ static int fix_unmerged_status(struct diff_filepair *p, } static int skip_submodule(const char *path, - struct repository *repo, - struct pathspec *pathspec, - int ignored_too) -{ - struct stat st; - const struct submodule *sub; - int pathspec_matches = 0; - int ps_i; - char *norm_pathspec = NULL; - - /* Only consider if path is a directory */ - if (lstat(path, &st) || !S_ISDIR(st.st_mode)) + struct repository *repo, + struct pathspec *pathspec, + int ignored_too) +{ + struct stat st; + const struct submodule *sub; + int pathspec_matches = 0; + int ps_i; + char *norm_pathspec = NULL; + + /* Only consider if path is a directory */ + if (lstat(path, &st) || !S_ISDIR(st.st_mode)) return 0; - /* Check if it's a submodule with ignore=all */ - sub = submodule_from_path(repo, null_oid(the_hash_algo), path); - if (!sub || !sub->name || !sub->ignore || strcmp(sub->ignore, "all")) + /* Check if it's a submodule with ignore=all */ + sub = submodule_from_path(repo, null_oid(the_hash_algo), path); + if (!sub || !sub->name || !sub->ignore || strcmp(sub->ignore, "all")) return 0; - trace_printf("ignore=all: %s\n", path); - trace_printf("pathspec %s\n", (pathspec && pathspec->nr) - ? "has pathspec" - : "no pathspec"); + trace_printf("ignore=all: %s\n", path); + trace_printf("pathspec %s\n", + ((pathspec && pathspec->nr) + ? "has pathspec" + : "no pathspec")); - /* Check if submodule path is explicitly mentioned in pathspec */ - if (pathspec) { + /* Check if submodule path is explicitly mentioned in pathspec */ + if (pathspec) { for (ps_i = 0; ps_i < pathspec->nr; ps_i++) { const char *m = pathspec->items[ps_i].match; if (!m) @@ -3948,28 +3963,29 @@ static int skip_submodule(const char *path, } FREE_AND_NULL(norm_pathspec); } - } + } - /* If explicitly matched and forced, allow adding */ - if (pathspec_matches) { + /* If explicitly matched and forced, allow adding */ + if (pathspec_matches) { if (ignored_too && ignored_too > 0) { trace_printf("Add submodule due to --force: %s\n", path); return 0; } else { advise_if_enabled(ADVICE_ADD_IGNORED_FILE, - _("Skipping submodule due to ignore=all: %s\n" - "Use --force if you really want to add the submodule."), path); + _("Skipping submodule due to ignore=all: %s\n" + "Use --force if you really want to " + "add the submodule."), path); return 1; } - } + } - /* No explicit pathspec match -> skip silently */ - trace_printf("Pathspec to submodule does not match explicitly: %s\n", path); - return 1; + /* No explicit pathspec match -> skip silently */ + trace_printf("Pathspec to submodule does not match explicitly: %s\n", path); + return 1; } static void update_callback(struct diff_queue_struct *q, - struct diff_options *opt UNUSED, void *cbdata) + struct diff_options *opt UNUSED, void *cbdata) { int i; struct update_callback_data *data = cbdata; @@ -3979,7 +3995,7 @@ static void update_callback(struct diff_queue_struct *q, const char *path = p->one->path; if (!data->include_sparse && - !path_in_sparse_checkout(path, data->index)) + !path_in_sparse_checkout(path, data->index)) continue; switch (fix_unmerged_status(p, data)) { @@ -3988,8 +4004,8 @@ static void update_callback(struct diff_queue_struct *q, case DIFF_STATUS_MODIFIED: case DIFF_STATUS_TYPE_CHANGED: if (skip_submodule(path, data->repo, - data->pathspec, - data->ignored_too)) + data->pathspec, + data->ignored_too)) continue; if (add_file_to_index(data->index, path, data->flags)) { @@ -4001,10 +4017,7 @@ static void update_callback(struct diff_queue_struct *q, case DIFF_STATUS_DELETED: if (data->flags & ADD_CACHE_IGNORE_REMOVAL) break; - if (!(data->flags & ADD_CACHE_PRETEND)) - remove_file_from_index(data->index, path); - if (data->flags & (ADD_CACHE_PRETEND|ADD_CACHE_VERBOSE)) - printf(_("remove '%s'\n"), path); + remove_file_from_index_with_flags(data->index, path, data->flags); break; } } diff --git a/rerere.c b/rerere.c index 0015e25050df76..3d3bd0db16a737 100644 --- a/rerere.c +++ b/rerere.c @@ -331,33 +331,6 @@ static int rerere_file_getline(struct strbuf *sb, struct rerere_io *io_) return strbuf_getwholeline(sb, io->input, '\n'); } -/* - * Require the exact number of conflict marker letters, no more, no - * less, followed by SP or any whitespace - * (including LF). - */ -static int is_cmarker(char *buf, int marker_char, int marker_size) -{ - int want_sp; - - /* - * The beginning of our version and the end of their version - * always are labeled like "<<<<< ours" or ">>>>> theirs", - * hence we set want_sp for them. Note that the version from - * the common ancestor in diff3-style output is not always - * labelled (e.g. "||||| common" is often seen but "|||||" - * alone is also valid), so we do not set want_sp. - */ - want_sp = (marker_char == '<') || (marker_char == '>'); - - while (marker_size--) - if (*buf++ != marker_char) - return 0; - if (want_sp && *buf != ' ') - return 0; - return isspace(*buf); -} - static void rerere_strbuf_putconflict(struct strbuf *buf, int ch, size_t size) { strbuf_addchars(buf, ch, size); @@ -375,7 +348,8 @@ static int handle_conflict(struct strbuf *out, struct rerere_io *io, int has_conflicts = -1; while (!io->getline(&buf, io)) { - if (is_cmarker(buf.buf, '<', marker_size)) { + int marker = is_conflict_marker_line(buf.buf, buf.len, marker_size); + if (marker == '<') { if (handle_conflict(&conflict, io, marker_size, NULL) < 0) break; if (hunk == RR_SIDE_1) @@ -383,15 +357,15 @@ static int handle_conflict(struct strbuf *out, struct rerere_io *io, else strbuf_addbuf(&two, &conflict); strbuf_release(&conflict); - } else if (is_cmarker(buf.buf, '|', marker_size)) { + } else if (marker == '|') { if (hunk != RR_SIDE_1) break; hunk = RR_ORIGINAL; - } else if (is_cmarker(buf.buf, '=', marker_size)) { + } else if (marker == '=') { if (hunk != RR_SIDE_1 && hunk != RR_ORIGINAL) break; hunk = RR_SIDE_2; - } else if (is_cmarker(buf.buf, '>', marker_size)) { + } else if (marker == '>') { if (hunk != RR_SIDE_2) break; if (strbuf_cmp(&one, &two) > 0) @@ -442,7 +416,7 @@ static int handle_path(unsigned char *hash, struct rerere_io *io, int marker_siz git_hash_init(&ctx, the_hash_algo); while (!io->getline(&buf, io)) { - if (is_cmarker(buf.buf, '<', marker_size)) { + if (is_conflict_marker_line(buf.buf, buf.len, marker_size) == '<') { has_conflicts = handle_conflict(&out, io, marker_size, hash ? &ctx : NULL); if (has_conflicts < 0) diff --git a/t/meson.build b/t/meson.build index a25f37d2f5ae7d..2133c840da63dc 100644 --- a/t/meson.build +++ b/t/meson.build @@ -305,6 +305,7 @@ integration_tests = [ 't2204-add-ignored.sh', 't2205-add-worktree-config.sh', 't2206-add-submodule-ignored.sh', + 't2207-add-resolved.sh', 't2300-cd-to-toplevel.sh', 't2400-worktree-add.sh', 't2401-worktree-prune.sh', diff --git a/t/t0213-trace2-ancestry.sh b/t/t0213-trace2-ancestry.sh index a2b9536da83152..2eb86c195274c2 100755 --- a/t/t0213-trace2-ancestry.sh +++ b/t/t0213-trace2-ancestry.sh @@ -31,12 +31,15 @@ PATH="$TTDIR:$PATH" && export PATH # no cmd_ancestry event is emitted. We detect this at runtime and # skip the format-specific tests accordingly. -# Determine if cmd_ancestry is supported on this platform. +# Enable these tests only when cmd_ancestry reports real process names. +# The procinfo stub emits no event; under user-mode emulation (e.g. +# qemu-user) /proc reports the emulator, not the guest. Spawn test-tool +# from test-tool and require "test-tool" in the child's ancestry. test_expect_success 'detect cmd_ancestry support' ' test_when_finished "rm -f trace.detect" && GIT_TRACE2_BRIEF=1 GIT_TRACE2="$(pwd)/trace.detect" \ - test-tool trace2 001return 0 && - if grep -q "^cmd_ancestry" trace.detect + test-tool trace2 004child test-tool trace2 001return 0 && + if grep -q "^cmd_ancestry.*test-tool" trace.detect then test_set_prereq TRACE2_ANCESTRY fi diff --git a/t/t2207-add-resolved.sh b/t/t2207-add-resolved.sh new file mode 100755 index 00000000000000..1b88efcb997bba --- /dev/null +++ b/t/t2207-add-resolved.sh @@ -0,0 +1,108 @@ +#!/bin/sh + +test_description='git add --resolved + +Test that "git add --resolved" stages conflict-resolved paths and +refuses to stage when conflict markers remain.' + +. ./test-lib.sh + +test_expect_success 'setup repo' ' + echo base >file1.txt && + echo base >file2.txt && + echo base >file3.txt && + echo base >file4.txt && + git add file1.txt file2.txt file3.txt file4.txt && + git commit -m initial && + + git branch topic && + echo "ours 1" >file1.txt && + echo "ours 2" >file2.txt && + echo "ours 3" >file3.txt && + git commit -a -m ours && + + git checkout topic && + echo "theirs 1" >file1.txt && + echo "theirs 2" >file2.txt && + echo "theirs 3" >file3.txt && + git commit -a -m theirs && + + git checkout @{-1} +' + +test_expect_success 'git add --resolved refuses files with conflict markers' ' + test_when_finished "git reset --hard HEAD" && + test_must_fail git merge topic && + echo "resolved 1" >file1.txt && + test_must_fail git add --resolved 2>err && + test_grep "the following paths still have conflict markers:" err && + test_grep "file2.txt" err && + test_grep "file3.txt" err && + # Index should remain unmerged for all files + git ls-files -u file1.txt >unmerged && + test_line_count = 3 unmerged +' + +test_expect_success 'git add --resolved succeeds when all conflict markers are removed' ' + test_when_finished "git reset --hard HEAD" && + test_must_fail git merge topic && + echo "resolved 1" >file1.txt && + echo "resolved 2" >file2.txt && + echo "resolved 3" >file3.txt && + git add --resolved && + git ls-files -u >unmerged && + test_must_be_empty unmerged && + git ls-files -s file1.txt file2.txt file3.txt >staged && + test_line_count = 3 staged +' + +test_expect_success 'git add --resolved ignores unconflicted modified files' ' + test_when_finished "git reset --hard HEAD" && + echo "unconflicted local change" >>file4.txt && + test_must_fail git merge topic && + echo "resolved 1" >file1.txt && + echo "resolved 2" >file2.txt && + echo "resolved 3" >file3.txt && + git add --resolved && + # file1, file2, file3 should be staged as resolved + git ls-files -u >unmerged && + test_must_be_empty unmerged && + # file4 should remain unstaged in working tree + git diff file4.txt >diff_out && + test_grep "unconflicted local change" diff_out && + git diff --cached file4.txt >cached_out && + test_must_be_empty cached_out +' + +test_expect_success 'git add --resolved handles file removals' ' + test_when_finished "git reset --hard HEAD" && + test_must_fail git merge topic && + echo "resolved 1" >file1.txt && + rm file2.txt && + echo "resolved 3" >file3.txt && + git add --resolved && + git ls-files -s file2.txt >out && + test_must_be_empty out +' + +test_expect_success 'git add --resolved honors pathspec' ' + test_when_finished "git reset --hard HEAD" && + test_must_fail git merge topic && + echo "resolved 1" >file1.txt && + # file2.txt and file3.txt still have conflict markers, + # but pathspec targets only file1.txt + git add --resolved file1.txt && + git ls-files -u file1.txt >unmerged1 && + test_must_be_empty unmerged1 && + git ls-files -u file2.txt >unmerged2 && + test_line_count = 3 unmerged2 +' + +test_expect_success 'git add --resolved incompatibility with -u and -A' ' + test_must_fail git add --resolved -u 2>err1 && + test_grep "cannot be used together" err1 && + test_must_fail git add --resolved -A 2>err2 && + test_grep "cannot be used together" err2 +' + +test_done diff --git a/t/t5550-http-fetch-dumb.sh b/t/t5550-http-fetch-dumb.sh index f00eeae48f8554..b5758f1c9ca1c2 100755 --- a/t/t5550-http-fetch-dumb.sh +++ b/t/t5550-http-fetch-dumb.sh @@ -293,6 +293,210 @@ test_expect_success 'http-fetch --packfile' ' git -C packfileclient cat-file -e "$HASH" ' +test_expect_success 'http-fetch --packfile accepts an already complete partial' ' + git init packfileclient-complete && + p=$(cd "$HTTPD_DOCUMENT_ROOT_PATH"/repo_pack.git && + ls objects/pack/pack-*.pack) && + packhash=$(basename "$p" .pack) && + packhash=${packhash#pack-} && + tmpfile="packfileclient-complete/.git/objects/pack/pack-$packhash.pack.temp" && + cp "$HTTPD_DOCUMENT_ROOT_PATH/repo_pack.git/$p" "$tmpfile" && + chmod u+w "$tmpfile" && + GIT_TRACE_CURL="$TRASH_DIRECTORY/complete.trace" \ + git -C packfileclient-complete http-fetch --packfile="$packhash" \ + --index-pack-arg=index-pack \ + --index-pack-arg=--stdin --index-pack-arg=--keep \ + "$HTTPD_URL/dumb/repo_pack.git/$p" >out && + test_grep "416 Requested Range Not Satisfiable" complete.trace && + test_path_is_missing "$tmpfile" && + git -C packfileclient-complete cat-file -e "$HASH" +' + +test_expect_success 'http-fetch --packfile resumes a partial download' ' + git init packfileclient-resume && + p=$(cd "$HTTPD_DOCUMENT_ROOT_PATH"/repo_pack.git && + ls objects/pack/pack-*.pack) && + tmpfile="packfileclient-resume/.git/objects/pack/pack-$ARBITRARY.pack.temp" && + test_copy_bytes 64 <"$HTTPD_DOCUMENT_ROOT_PATH/repo_pack.git/$p" >"$tmpfile" && + GIT_TRACE_CURL="$TRASH_DIRECTORY/resume.trace" \ + git -C packfileclient-resume http-fetch --packfile="$ARBITRARY" \ + --index-pack-arg=index-pack --index-pack-arg=--stdin \ + --index-pack-arg=--keep \ + "$HTTPD_URL/dumb/repo_pack.git/$p" >out && + test_grep "Range: bytes=64-" resume.trace && + test_path_is_missing "$tmpfile" && + git -C packfileclient-resume cat-file -e "$HASH" +' + +test_expect_success 'http-fetch --packfile permits unlink while indexing' ' + git init packfileclient-unlink && + p=$(cd "$HTTPD_DOCUMENT_ROOT_PATH"/repo_pack.git && + ls objects/pack/pack-*.pack) && + tmpfile="packfileclient-unlink/.git/objects/pack/pack-$ARBITRARY.pack.temp" && + write_script git-unlink-index-pack <<-\EOF && + test -f "$GIT_TEST_PACK_TEMP" || exit 1 + rm "$GIT_TEST_PACK_TEMP" || exit 1 + exec git index-pack "$@" + EOF + test_when_finished "rm -f git-unlink-index-pack" && + PATH="$TRASH_DIRECTORY:$PATH" \ + GIT_TEST_PACK_TEMP="$TRASH_DIRECTORY/$tmpfile" \ + git -C packfileclient-unlink http-fetch --packfile="$ARBITRARY" \ + --index-pack-arg=unlink-index-pack \ + --index-pack-arg=--stdin --index-pack-arg=--keep \ + "$HTTPD_URL/dumb/repo_pack.git/$p" >out && + test_path_is_missing "$tmpfile" && + git -C packfileclient-unlink cat-file -e "$HASH" +' + +test_expect_success PERL,PIPE 'concurrent http-fetch --packfile cannot corrupt an overlapping download' ' + git init packfileclient-overlap && + blob=$(test-tool genrandom pack-overlap 2m | + git -C "$HTTPD_DOCUMENT_ROOT_PATH"/repo_pack.git \ + hash-object -w --stdin) && + packhash=$(printf "%s\n" "$blob" | + git -C "$HTTPD_DOCUMENT_ROOT_PATH"/repo_pack.git \ + pack-objects "$TRASH_DIRECTORY/overlap-pack") && + pack="$TRASH_DIRECTORY/overlap-pack-$packhash.pack" && + tmpfile="packfileclient-overlap/.git/objects/pack/pack-$packhash.pack.temp" && + mkfifo server-ready first-ready && + exec 7<>server-ready && + exec 8<>first-ready && + write_script slow-pack-server "$PERL_PATH" <<-\EOF && + use strict; + use warnings; + use IO::Socket::INET; + + my ($packfile, $server_ready, $first_ready) = @ARGV; + my $completed = 0; + END { + if (!$completed) { + signal_ready($server_ready, "failed"); + signal_ready($first_ready, "failed"); + } + } + + $SIG{ALRM} = sub { die "timed out serving concurrent pack requests\n" }; + alarm 60; + + open(my $in, "<:raw", $packfile) or die "open $packfile: $!"; + my $pack = do { local $/; <$in> }; + close($in) or die "close $packfile: $!"; + my $server = IO::Socket::INET->new(LocalAddr => "127.0.0.1", + LocalPort => 0, Proto => "tcp", Listen => 2, ReuseAddr => 1) + or die "listen: $!"; + + sub signal_ready { + my ($file, $value) = @_; + open(my $out, ">", $file) or die "open $file: $!"; + print $out "$value\n" or die "write $file: $!"; + close($out) or die "close $file: $!"; + } + + sub write_all { + my ($out, $data) = @_; + my $offset = 0; + while ($offset < length($data)) { + my $written = syswrite($out, $data, + length($data) - $offset, $offset); + defined($written) && $written or die "write response: $!"; + $offset += $written; + } + } + + sub start_response { + my $out = $server->accept() or die "accept: $!"; + <$out> or die "read request: $!"; + my $start = 0; + while (<$out>) { + last if /^\r?\n$/; + $start = $1 if /^Range: bytes=(\d+)-/i; + } + $start < length($pack) or die "invalid range $start"; + my $length = length($pack) - $start; + my $middle = int($length / 2); + my $status = $start ? "206 Partial Content" : "200 OK"; + my $headers = "HTTP/1.1 $status\r\n" . + "Content-Length: $length\r\n" . + ($start ? "Content-Range: bytes $start-" . + (length($pack) - 1) . "/" . length($pack) . "\r\n" : "") . + "Connection: close\r\n\r\n"; + write_all($out, $headers); + write_all($out, substr($pack, $start, $middle)); + return ($out, $start + $middle); + } + + signal_ready($server_ready, $server->sockport()); + my ($first, $first_pos) = start_response(); + signal_ready($first_ready, "ready"); + my ($second, $second_pos) = start_response(); + write_all($first, substr($pack, $first_pos)); + write_all($second, substr($pack, $second_pos)); + close($first) or die "close first response: $!"; + close($second) or die "close second response: $!"; + $completed = 1; + alarm 0; + EOF + { + "$TRASH_DIRECTORY/slow-pack-server" "$pack" \ + "$TRASH_DIRECTORY/server-ready" \ + "$TRASH_DIRECTORY/first-ready" >server.log 2>&1 & + server_pid=$! + } && + test_when_finished " + kill $server_pid 2>/dev/null || : + wait $server_pid 2>/dev/null || : + exec 7>&- + exec 8>&- + rm -f server-ready first-ready slow-pack-server + " && + read port <&7 && + url="http://127.0.0.1:$port/pack" && + { + ( + if ! GIT_TRACE_CURL="$TRASH_DIRECTORY/overlap-first.trace" \ + GIT_TRACE_CURL_NO_DATA=1 \ + git -C packfileclient-overlap http-fetch --packfile="$packhash" \ + --index-pack-arg=index-pack \ + --index-pack-arg=--stdin --index-pack-arg=--keep \ + "$url" >first.out + then + echo failed >"$TRASH_DIRECTORY/first-ready" && + exit 1 + fi + ) & + first_pid=$! + } && + test_when_finished " + kill $first_pid 2>/dev/null || : + wait $first_pid 2>/dev/null || : + " && + read ready <&8 && + test "$ready" = ready && + test_path_is_file "$tmpfile" && + { + GIT_TRACE_CURL="$TRASH_DIRECTORY/overlap-second.trace" \ + GIT_TRACE_CURL_NO_DATA=1 \ + git -C packfileclient-overlap http-fetch --packfile="$packhash" \ + --index-pack-arg=index-pack \ + --index-pack-arg=--stdin --index-pack-arg=--keep \ + "$url" >second.out & + second_pid=$! + } && + test_when_finished " + kill $second_pid 2>/dev/null || : + wait $second_pid 2>/dev/null || : + " && + wait "$second_pid" && + wait "$first_pid" && + wait "$server_pid" && + printf "keep\t%s\npack\t%s\n" "$packhash" "$packhash" | sort >expect && + sort first.out second.out >actual && + test_cmp expect actual && + test_path_is_missing "$tmpfile" && + git -C packfileclient-overlap cat-file -e "$blob" +' + test_expect_success 'fetch notices corrupt pack' ' cp -R "$HTTPD_DOCUMENT_ROOT_PATH"/repo_pack.git "$HTTPD_DOCUMENT_ROOT_PATH"/repo_bad1.git && (cd "$HTTPD_DOCUMENT_ROOT_PATH"/repo_bad1.git && diff --git a/t/t5702-protocol-v2.sh b/t/t5702-protocol-v2.sh index 74a2b7730bf3da..0f05286de8b4df 100755 --- a/t/t5702-protocol-v2.sh +++ b/t/t5702-protocol-v2.sh @@ -1291,6 +1291,37 @@ test_expect_success 'packfile URIs with fetch instead of clone' ' fetch "$HTTPD_URL/smart/http_parent" ' +test_expect_success 'packfile URI preserves an existing keep file' ' + P="$HTTPD_DOCUMENT_ROOT_PATH/http_parent" && + rm -rf "$P" http_child keep.expect && + + git init "$P" && + git -C "$P" config uploadpack.allowsidebandall true && + + echo my-blob >"$P/my-blob" && + git -C "$P" add my-blob && + git -C "$P" commit -m x && + configure_exclusion "$P" my-blob >h && + + git init http_child && + packhash=$(cat packh) && + keep="http_child/.git/objects/pack/pack-$packhash.keep" && + echo pre-existing >"$keep" && + cp "$keep" keep.expect && + + GIT_TEST_SIDEBAND_ALL=1 \ + git -C http_child -c protocol.version=2 \ + -c fetch.uriprotocols=http,https \ + fetch "$HTTPD_URL/smart/http_parent" && + + test_path_is_file \ + "http_child/.git/objects/pack/pack-$packhash.pack" && + test_path_is_file \ + "http_child/.git/objects/pack/pack-$packhash.idx" && + test_cmp keep.expect "$keep" && + git -C http_child cat-file -e "$(cat h)" +' + test_expect_success 'fetching with valid packfile URI but invalid hash fails' ' P="$HTTPD_DOCUMENT_ROOT_PATH/http_parent" && rm -rf "$P" http_child log && diff --git a/t/t7528-signed-commit-ssh.sh b/t/t7528-signed-commit-ssh.sh index 44396460e6f144..991678264e0536 100755 --- a/t/t7528-signed-commit-ssh.sh +++ b/t/t7528-signed-commit-ssh.sh @@ -82,7 +82,7 @@ test_expect_success GPGSSH 'create signed commits' ' test_expect_success GPGSSH 'sign commits using literal public keys with ssh-agent' ' test_when_finished "test_unconfig commit.gpgsign" && test_config gpg.format ssh && - eval $(ssh-agent -T || ssh-agent) && + eval $(ssh-agent -T -s || ssh-agent -s) && test_when_finished "kill ${SSH_AGENT_PID}" && test_when_finished "test_unconfig user.signingkey" && mkdir tmpdir &&