diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 83ec786c5a49d0..e0b9a0d82b684f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -79,7 +79,7 @@ test:osx: stage: test needs: [ ] tags: - - saas-macos-medium-m1 + - saas-macos-large-m2pro variables: TEST_OUTPUT_DIRECTORY: "/Volumes/RAMDisk" before_script: @@ -104,10 +104,10 @@ test:osx: image: macos-15-xcode-16 CC: clang - jobname: osx-reftable - image: macos-15-xcode-16 + image: macos-26-xcode-26 CC: clang - jobname: osx-meson - image: macos-15-xcode-16 + image: macos-26-xcode-26 CC: clang artifacts: paths: diff --git a/Documentation/RelNotes/2.55.0.adoc b/Documentation/RelNotes/2.55.0.adoc index 36e49d73803abf..2d3871cf34712e 100644 --- a/Documentation/RelNotes/2.55.0.adoc +++ b/Documentation/RelNotes/2.55.0.adoc @@ -39,6 +39,15 @@ UI, Workflows & Features * The command line parser for "git diff" learned a few options take only non-negative integers. + * The graph output from commands like "git log --graph" can now be + limited to a specified number of lanes, preventing overly wide output + in repositories with many branches. + + * The fsmonitor daemon has been implemented for Linux. + + * "git cat-file --batch" learns an in-line command "mailmap" + that lets the user toggle use of mailmap. + Performance, Internal Implementation, Development Support etc. -------------------------------------------------------------- @@ -98,6 +107,14 @@ Performance, Internal Implementation, Development Support etc. * Many uses of the_repository has been updated to use a more appropriate struct repository instance in setup.c codepath. + * Revision traversal optimization. + + * Build update. + + * The logic to lazy-load trees from the commit-graph has been made + more robust by falling back to reading the commit object when + the commit-graph is no longer available. + Fixes since v2.54 ----------------- @@ -206,6 +223,13 @@ Fixes since v2.54 (i.e., "https://example.com" not "https://example.com/"). (merge b92387cd55 jk/dumb-http-alternate-fix later to maint). + * "git bisect" now uses the selected terms (e.g., old/new) more + consistently in its output. + (merge cb55991825 jr/bisect-custom-terms-in-output later to maint). + + * Update GitLab CI jobs that exercise macOS. + (merge 62319b49bb ps/gitlab-ci-macOS-improvements later to maint). + * Other code cleanup, docfix, build fix, etc. (merge 80f4b802e9 ja/doc-difftool-synopsis-style later to maint). (merge b96490241e jc/doc-timestamps-in-stat later to maint). @@ -224,3 +248,6 @@ Fixes since v2.54 (merge 499f9048e0 ps/t3903-cover-stash-include-untracked later to maint). (merge b56ab270aa jk/sq-dequote-cleanup later to maint). (merge 29d9fdcf10 rs/use-builtin-add-overflow-explicitly-on-clang later to maint). + (merge d9982e8290 ed/check-connected-close-err-fd-2.53 later to maint). + (merge 1740cc35d0 ed/check-connected-close-err-fd later to maint). + (merge f4d7eb3d1c sp/doc-range-diff-takes-notes later to maint). diff --git a/Documentation/config/fsmonitor--daemon.adoc b/Documentation/config/fsmonitor--daemon.adoc index 671f9b94628446..6f8386e29150ff 100644 --- a/Documentation/config/fsmonitor--daemon.adoc +++ b/Documentation/config/fsmonitor--daemon.adoc @@ -4,8 +4,8 @@ fsmonitor.allowRemote:: behavior. Only respected when `core.fsmonitor` is set to `true`. fsmonitor.socketDir:: - This Mac OS-specific option, if set, specifies the directory in + This Mac OS and Linux-specific option, if set, specifies the directory in which to create the Unix domain socket used for communication between the fsmonitor daemon and various Git commands. The directory must - reside on a native Mac OS filesystem. Only respected when `core.fsmonitor` + reside on a native filesystem. Only respected when `core.fsmonitor` is set to `true`. diff --git a/Documentation/git-cat-file.adoc b/Documentation/git-cat-file.adoc index c139f55a168d61..86b9181599317e 100644 --- a/Documentation/git-cat-file.adoc +++ b/Documentation/git-cat-file.adoc @@ -174,6 +174,11 @@ flush:: since the beginning or since the last flush was issued. When `--buffer` is used, no output will come until a `flush` is issued. When `--buffer` is not used, commands are flushed each time without issuing `flush`. + +`mailmap ()`:: + Enable or disable mailmap for subsequent commands. The `` + argument accepts the same boolean values as linkgit:git-config[1]. + The mailmap data is read upon the first use and only once. -- + diff --git a/Documentation/git-fsmonitor--daemon.adoc b/Documentation/git-fsmonitor--daemon.adoc index 8fe5241b08b007..12fa866a64ecc9 100644 --- a/Documentation/git-fsmonitor--daemon.adoc +++ b/Documentation/git-fsmonitor--daemon.adoc @@ -76,9 +76,9 @@ repositories; this may be overridden by setting `fsmonitor.allowRemote` to correctly with all network-mounted repositories, so such use is considered experimental. -On Mac OS, the inter-process communication (IPC) between various Git +On Mac OS and Linux, the inter-process communication (IPC) between various Git commands and the fsmonitor daemon is done via a Unix domain socket (UDS) -- a -special type of file -- which is supported by native Mac OS filesystems, +special type of file -- which is supported by native Mac OS and Linux filesystems, but not on network-mounted filesystems, NTFS, or FAT32. Other filesystems may or may not have the needed support; the fsmonitor daemon is not guaranteed to work with these filesystems and such use is considered experimental. @@ -87,13 +87,33 @@ By default, the socket is created in the `.git` directory. However, if the `.git` directory is on a network-mounted filesystem, it will instead be created at `$HOME/.git-fsmonitor-*` unless `$HOME` itself is on a network-mounted filesystem, in which case you must set the configuration -variable `fsmonitor.socketDir` to the path of a directory on a Mac OS native +variable `fsmonitor.socketDir` to the path of a directory on a native filesystem in which to create the socket file. If none of the above directories (`.git`, `$HOME`, or `fsmonitor.socketDir`) -is on a native Mac OS file filesystem the fsmonitor daemon will report an +is on a native filesystem the fsmonitor daemon will report an error that will cause the daemon and the currently running command to exit. +LINUX CAVEATS +~~~~~~~~~~~~~ + +On Linux, the fsmonitor daemon uses inotify to monitor filesystem events. +The inotify system has per-user limits on the number of watches that can +be created. The default limit is typically 8192 watches per user. + +For large repositories with many directories, you may need to increase +this limit. Check the current limit with: + + cat /proc/sys/fs/inotify/max_user_watches + +To temporarily increase the limit: + + sudo sysctl fs.inotify.max_user_watches=65536 + +To make the change permanent, add to `/etc/sysctl.conf`: + + fs.inotify.max_user_watches=65536 + CONFIGURATION ------------- diff --git a/Documentation/git-range-diff.adoc b/Documentation/git-range-diff.adoc index 880557084533fb..5cc5e2ed56734b 100644 --- a/Documentation/git-range-diff.adoc +++ b/Documentation/git-range-diff.adoc @@ -11,7 +11,7 @@ SYNOPSIS git range-diff [--color=[]] [--no-color] [] [--no-dual-color] [--creation-factor=] [--left-only | --right-only] [--diff-merges=] - [--remerge-diff] + [--remerge-diff] [--no-notes | --notes[=]] ( | ... | ) [[--] ...] diff --git a/Documentation/rev-list-options.adoc b/Documentation/rev-list-options.adoc index 2d195a147456ea..94a7b1c065dba8 100644 --- a/Documentation/rev-list-options.adoc +++ b/Documentation/rev-list-options.adoc @@ -1259,6 +1259,12 @@ This implies the `--topo-order` option by default, but the in between them in that case. If __ is specified, it is the string that will be shown instead of the default one. +`--graph-lane-limit=`:: + When `--graph` is used, limit the number of graph lanes to be shown. + Lanes over the limit are replaced with a truncation mark '~'. + By default it is set to 0 (no limit), zero and negative values + are ignored and treated as no limit. + ifdef::git-rev-list[] `--count`:: Print a number stating how many commits would have been diff --git a/Makefile b/Makefile index a43b8ee0674df8..b31ecb07564a73 100644 --- a/Makefile +++ b/Makefile @@ -416,7 +416,7 @@ include shared.mak # If your platform has OS-specific ways to tell if a repo is incompatible with # fsmonitor (whether the hook or IPC daemon version), set FSMONITOR_OS_SETTINGS # to the "" of the corresponding `compat/fsmonitor/fsm-settings-.c` -# that implements the `fsm_os_settings__*()` routines. +# and `compat/fsmonitor/fsm-ipc-.c` files. # # Define LINK_FUZZ_PROGRAMS if you want `make all` to also build the fuzz test # programs in oss-fuzz/. @@ -2374,13 +2374,13 @@ ifdef FSMONITOR_DAEMON_BACKEND COMPAT_CFLAGS += -DHAVE_FSMONITOR_DAEMON_BACKEND COMPAT_OBJS += compat/fsmonitor/fsm-listen-$(FSMONITOR_DAEMON_BACKEND).o COMPAT_OBJS += compat/fsmonitor/fsm-health-$(FSMONITOR_DAEMON_BACKEND).o - COMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_DAEMON_BACKEND).o endif ifdef FSMONITOR_OS_SETTINGS COMPAT_CFLAGS += -DHAVE_FSMONITOR_OS_SETTINGS + COMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_OS_SETTINGS).o COMPAT_OBJS += compat/fsmonitor/fsm-settings-$(FSMONITOR_OS_SETTINGS).o - COMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_OS_SETTINGS).o + COMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_DAEMON_BACKEND).o endif ifdef WITH_BREAKING_CHANGES diff --git a/bisect.c b/bisect.c index ef17a442e55d2c..905a9afb057989 100644 --- a/bisect.c +++ b/bisect.c @@ -711,7 +711,7 @@ static enum bisect_error error_if_skipped_commits(struct commit_list *tried, return BISECT_OK; printf("There are only 'skip'ped commits left to test.\n" - "The first %s commit could be any of:\n", term_bad); + "The first '%s' commit could be any of:\n", term_bad); for ( ; tried; tried = tried->next) printf("%s\n", oid_to_hex(&tried->item->object.oid)); @@ -810,7 +810,7 @@ static enum bisect_error handle_bad_merge_base(void) "between %s and [%s].\n"), bad_hex, bad_hex, good_hex); } else { - fprintf(stderr, _("The merge base %s is %s.\n" + fprintf(stderr, _("The merge base %s is '%s'.\n" "This means the first '%s' commit is " "between %s and [%s].\n"), bad_hex, term_bad, term_good, bad_hex, good_hex); @@ -820,9 +820,9 @@ static enum bisect_error handle_bad_merge_base(void) return BISECT_MERGE_BASE_CHECK; } - fprintf(stderr, _("Some %s revs are not ancestors of the %s rev.\n" + fprintf(stderr, _("Some '%s' revs are not ancestors of the '%s' rev.\n" "git bisect cannot work properly in this case.\n" - "Maybe you mistook %s and %s revs?\n"), + "Maybe you mistook '%s' and '%s' revs?\n"), term_good, term_bad, term_good, term_bad); return BISECT_FAILED; } @@ -835,7 +835,7 @@ static void handle_skipped_merge_base(const struct object_id *mb) warning(_("the merge base between %s and [%s] " "must be skipped.\n" - "So we cannot be sure the first %s commit is " + "So we cannot be sure the first '%s' commit is " "between %s and %s.\n" "We continue anyway."), bad_hex, good_hex, term_bad, mb_hex, bad_hex); @@ -928,7 +928,7 @@ static enum bisect_error check_good_are_ancestors_of_bad(struct repository *r, struct commit **rev; if (!current_bad_oid) - return error(_("a %s revision is needed"), term_bad); + return error(_("a '%s' revision is needed"), term_bad); filename = repo_git_path(the_repository, "BISECT_ANCESTORS_OK"); @@ -1090,7 +1090,7 @@ enum bisect_error bisect_next_all(struct repository *r, const char *prefix) res = error_if_skipped_commits(tried, NULL); if (res < 0) goto cleanup; - printf(_("%s was both %s and %s\n"), + printf(_("%s was both '%s' and '%s'\n"), oid_to_hex(current_bad_oid), term_good, term_bad); @@ -1113,7 +1113,7 @@ enum bisect_error bisect_next_all(struct repository *r, const char *prefix) res = error_if_skipped_commits(tried, current_bad_oid); if (res) goto cleanup; - printf("%s is the first %s commit\n", oid_to_hex(bisect_rev), + printf("%s is the first '%s' commit\n", oid_to_hex(bisect_rev), term_bad); show_commit(revs.commits->item); diff --git a/builtin/bisect.c b/builtin/bisect.c index 4520e585d0677f..606698b21ef7fe 100644 --- a/builtin/bisect.c +++ b/builtin/bisect.c @@ -465,13 +465,16 @@ static void bisect_print_status(const struct bisect_terms *terms) return; if (!state.nr_good && !state.nr_bad) - bisect_log_printf(_("status: waiting for both good and bad commits\n")); + bisect_log_printf(_("status: waiting for both '%s' and '%s' commits\n"), + terms->term_good, terms->term_bad); else if (state.nr_good) - bisect_log_printf(Q_("status: waiting for bad commit, %d good commit known\n", - "status: waiting for bad commit, %d good commits known\n", - state.nr_good), state.nr_good); + bisect_log_printf(Q_("status: waiting for '%s' commit, %d '%s' commit known\n", + "status: waiting for '%s' commit, %d '%s' commits known\n", + state.nr_good), + terms->term_bad, state.nr_good, terms->term_good); else - bisect_log_printf(_("status: waiting for good commit(s), bad commit known\n")); + bisect_log_printf(_("status: waiting for '%s' commit(s), '%s' commit known\n"), + terms->term_good, terms->term_bad); } static int bisect_next_check(const struct bisect_terms *terms, @@ -513,8 +516,8 @@ static int bisect_terms(struct bisect_terms *terms, const char *option) return error(_("no terms defined")); if (!option) { - printf(_("Your current terms are %s for the old state\n" - "and %s for the new state.\n"), + printf(_("Your current terms are '%s' for the old state\n" + "and '%s' for the new state.\n"), terms->term_good, terms->term_bad); return 0; } @@ -632,7 +635,7 @@ static int bisect_skipped_commits(struct bisect_terms *terms) strbuf_reset(&commit_name); repo_format_commit_message(the_repository, commit, "%s", &commit_name, &pp); - fprintf(fp, "# possible first %s commit: [%s] %s\n", + fprintf(fp, "# possible first '%s' commit: [%s] %s\n", terms->term_bad, oid_to_hex(&commit->object.oid), commit_name.buf); } @@ -663,7 +666,7 @@ static int bisect_successful(struct bisect_terms *terms) repo_format_commit_message(the_repository, commit, "%s", &commit_name, &pp); - res = append_to_file(git_path_bisect_log(), "# first %s commit: [%s] %s\n", + res = append_to_file(git_path_bisect_log(), "# first '%s' commit: [%s] %s\n", terms->term_bad, oid_to_hex(&commit->object.oid), commit_name.buf); @@ -1262,14 +1265,14 @@ static int bisect_run(struct bisect_terms *terms, int argc, const char **argv) int rc = verify_good(terms, command.buf); is_first_run = 0; if (rc < 0 || 128 <= rc) { - error(_("unable to verify %s on good" - " revision"), command.buf); + error(_("unable to verify %s on '%s' revision"), + command.buf, terms->term_good); res = BISECT_FAILED; break; } if (rc == res) { - error(_("bogus exit code %d for good revision"), - rc); + error(_("bogus exit code %d for '%s' revision"), + rc, terms->term_good); res = BISECT_FAILED; break; } @@ -1314,7 +1317,7 @@ static int bisect_run(struct bisect_terms *terms, int argc, const char **argv) puts(_("bisect run success")); res = BISECT_OK; } else if (res == BISECT_INTERNAL_SUCCESS_1ST_BAD_FOUND) { - puts(_("bisect found first bad commit")); + printf(_("bisect found first '%s' commit\n"), terms->term_bad); res = BISECT_OK; } else if (res) { error(_("bisect run failed: 'git bisect %s'" diff --git a/builtin/cat-file.c b/builtin/cat-file.c index d9fbad535868bb..fa45f774d7a8b7 100644 --- a/builtin/cat-file.c +++ b/builtin/cat-file.c @@ -57,6 +57,20 @@ static int use_mailmap; static char *replace_idents_using_mailmap(char *, size_t *); +/* + * The mailmap is initialized with .strdup_strings set to 0, + * but read_mailmap() sets the bit to 1 (this is true even when + * not a single mailmap entry is read), so it can be used for + * lazy loading. + */ +static void load_mailmap(void) +{ + if (mailmap.strdup_strings) + return; + + read_mailmap(the_repository, &mailmap); +} + static char *replace_idents_using_mailmap(char *object_buf, size_t *size) { struct strbuf sb = STRBUF_INIT; @@ -692,6 +706,20 @@ static void parse_cmd_info(struct batch_options *opt, batch_one_object(line, output, opt, data); } +static void parse_cmd_mailmap(struct batch_options *opt UNUSED, + const char *line, + struct strbuf *output UNUSED, + struct expand_data *data UNUSED) +{ + use_mailmap = git_parse_maybe_bool(line); + + if (use_mailmap < 0) + die(_("mailmap: invalid boolean '%s'"), line); + + if (use_mailmap) + load_mailmap(); +} + static void dispatch_calls(struct batch_options *opt, struct strbuf *output, struct expand_data *data, @@ -725,9 +753,10 @@ static const struct parse_cmd { parse_cmd_fn_t fn; unsigned takes_args; } commands[] = { - { "contents", parse_cmd_contents, 1}, - { "info", parse_cmd_info, 1}, - { "flush", NULL, 0}, + { "contents", parse_cmd_contents, 1 }, + { "info", parse_cmd_info, 1 }, + { "flush", NULL, 0 }, + { "mailmap", parse_cmd_mailmap, 1 }, }; static void batch_objects_command(struct batch_options *opt, @@ -1131,7 +1160,7 @@ int cmd_cat_file(int argc, opt_epts = (opt == 'e' || opt == 'p' || opt == 't' || opt == 's'); if (use_mailmap) - read_mailmap(the_repository, &mailmap); + load_mailmap(); switch (batch.objects_filter.choice) { case LOFC_DISABLED: diff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c index 242c594646d1f5..f920cf3a8202f6 100644 --- a/builtin/fsmonitor--daemon.c +++ b/builtin/fsmonitor--daemon.c @@ -16,7 +16,7 @@ #include "fsmonitor--daemon.h" #include "simple-ipc.h" -#include "khash.h" +#include "strmap.h" #include "run-command.h" #include "trace.h" #include "trace2.h" @@ -86,6 +86,8 @@ static int do_as_client__send_stop(void) { struct strbuf answer = STRBUF_INIT; int ret; + int max_wait_ms = 30000; + int elapsed_ms = 0; ret = fsmonitor_ipc__send_command("quit", &answer); @@ -96,8 +98,16 @@ static int do_as_client__send_stop(void) return ret; trace2_region_enter("fsm_client", "polling-for-daemon-exit", NULL); - while (fsmonitor_ipc__get_state() == IPC_STATE__LISTENING) + while (fsmonitor_ipc__get_state() == IPC_STATE__LISTENING) { + if (elapsed_ms >= max_wait_ms) { + trace2_region_leave("fsm_client", + "polling-for-daemon-exit", NULL); + return error(_("daemon did not stop within %d seconds"), + max_wait_ms / 1000); + } sleep_millisec(50); + elapsed_ms += 50; + } trace2_region_leave("fsm_client", "polling-for-daemon-exit", NULL); return 0; @@ -197,20 +207,31 @@ static enum fsmonitor_cookie_item_result with_lock__wait_for_cookie( unlink(cookie_pathname.buf); /* - * Technically, this is an infinite wait (well, unless another - * thread sends us an abort). I'd like to change this to - * use `pthread_cond_timedwait()` and return an error/timeout - * and let the caller do the trivial response thing, but we - * don't have that routine in our thread-utils. - * - * After extensive beta testing I'm not really worried about - * this. Also note that the above open() and unlink() calls - * will cause at least two FS events on that path, so the odds - * of getting stuck are pretty slim. + * Wait for the listener thread to observe the cookie file. + * Time out after a short interval so that the client + * does not hang forever if the filesystem does not deliver + * events (e.g., on certain container/overlay filesystems + * where inotify watches succeed but events never arrive). */ - while (cookie->result == FCIR_INIT) - pthread_cond_wait(&state->cookies_cond, - &state->main_lock); + { + struct timeval now; + struct timespec ts; + int err = 0; + + gettimeofday(&now, NULL); + ts.tv_sec = now.tv_sec + 1; + ts.tv_nsec = now.tv_usec * 1000; + + while (cookie->result == FCIR_INIT && !err) + err = pthread_cond_timedwait(&state->cookies_cond, + &state->main_lock, + &ts); + if (err == ETIMEDOUT && cookie->result == FCIR_INIT) { + trace_printf_key(&trace_fsmonitor, + "cookie_wait timed out"); + cookie->result = FCIR_ERROR; + } + } done: hashmap_remove(&state->cookies, &cookie->entry, NULL); @@ -653,8 +674,6 @@ static int fsmonitor_parse_client_token(const char *buf_token, return 0; } -KHASH_INIT(str, const char *, int, 0, kh_str_hash_func, kh_str_hash_equal) - static int do_handle_client(struct fsmonitor_daemon_state *state, const char *command, ipc_server_reply_cb *reply, @@ -671,8 +690,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state, const struct fsmonitor_batch *batch; struct fsmonitor_batch *remainder = NULL; intmax_t count = 0, duplicates = 0; - kh_str_t *shown; - int hash_ret; + struct strset shown = STRSET_INIT; int do_trivial = 0; int do_flush = 0; int do_cookie = 0; @@ -861,14 +879,14 @@ static int do_handle_client(struct fsmonitor_daemon_state *state, * so walk the batch list backwards from the current head back * to the batch (sequence number) they named. * - * We use khash to de-dup the list of pathnames. + * We use a strset to de-dup the list of pathnames. * * NEEDSWORK: each batch contains a list of interned strings, * so we only need to do pointer comparisons here to build the * hash table. Currently, we're still comparing the string * values. */ - shown = kh_init_str(); + strset_init_with_options(&shown, NULL, 0); for (batch = batch_head; batch && batch->batch_seq_nr > requested_oldest_seq_nr; batch = batch->next) { @@ -878,11 +896,9 @@ static int do_handle_client(struct fsmonitor_daemon_state *state, const char *s = batch->interned_paths[k]; size_t s_len; - if (kh_get_str(shown, s) != kh_end(shown)) + if (!strset_add(&shown, s)) duplicates++; else { - kh_put_str(shown, s, &hash_ret); - trace_printf_key(&trace_fsmonitor, "send[%"PRIuMAX"]: %s", count, s); @@ -909,8 +925,6 @@ static int do_handle_client(struct fsmonitor_daemon_state *state, total_response_len += payload.len; } - kh_release_str(shown); - pthread_mutex_lock(&state->main_lock); if (token_data->client_ref_count > 0) @@ -954,6 +968,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state, trace2_data_intmax("fsmonitor", the_repository, "response/count/duplicates", duplicates); cleanup: + strset_clear(&shown); strbuf_release(&response_token); strbuf_release(&requested_token_id); strbuf_release(&payload); @@ -1405,6 +1420,15 @@ static int fsmonitor_run_daemon(void) done: pthread_cond_destroy(&state.cookies_cond); pthread_mutex_destroy(&state.main_lock); + { + struct hashmap_iter iter; + struct fsmonitor_cookie_item *cookie; + + hashmap_for_each_entry(&state.cookies, &iter, cookie, entry) + free(cookie->name); + hashmap_clear_and_free(&state.cookies, + struct fsmonitor_cookie_item, entry); + } fsm_listen__dtor(&state); fsm_health__dtor(&state); @@ -1420,7 +1444,7 @@ static int fsmonitor_run_daemon(void) return err; } -static int try_to_run_foreground_daemon(int detach_console MAYBE_UNUSED) +static int try_to_run_foreground_daemon(int detach_console) { /* * Technically, we don't need to probe for an existing daemon @@ -1440,10 +1464,21 @@ static int try_to_run_foreground_daemon(int detach_console MAYBE_UNUSED) fflush(stderr); } + if (detach_console) { #ifdef GIT_WINDOWS_NATIVE - if (detach_console) FreeConsole(); +#else + /* + * Create a new session so that the daemon is detached + * from the parent's process group. This prevents + * shells with job control (e.g. bash with "set -m") + * from waiting on the daemon when they wait for a + * foreground command that implicitly spawned it. + */ + if (setsid() == -1) + warning_errno(_("setsid failed")); #endif + } return !!fsmonitor_run_daemon(); } @@ -1506,6 +1541,7 @@ static int try_to_start_background_daemon(void) cp.no_stdin = 1; cp.no_stdout = 1; cp.no_stderr = 1; + cp.close_fd_above_stderr = 1; sbgr = start_bg_command(&cp, bg_wait_cb, NULL, fsmonitor__start_timeout_sec); diff --git a/builtin/rev-parse.c b/builtin/rev-parse.c index 0a01ff7a753938..bb882678fe2a9e 100644 --- a/builtin/rev-parse.c +++ b/builtin/rev-parse.c @@ -10,6 +10,7 @@ #include "builtin.h" #include "abspath.h" +#include "bisect.h" #include "config.h" #include "commit.h" #include "environment.h" @@ -940,13 +941,23 @@ int cmd_rev_parse(int argc, continue; } if (!strcmp(arg, "--bisect")) { + char *prefix; + char *term_bad = NULL; + char *term_good = NULL; struct refs_for_each_ref_options opts = { 0 }; - opts.prefix = "refs/bisect/bad"; + read_bisect_terms(&term_bad, &term_good); + prefix = xstrfmt("refs/bisect/%s", term_bad); + opts.prefix = prefix; refs_for_each_ref_ext(get_main_ref_store(the_repository), show_reference, NULL, &opts); - opts.prefix = "refs/bisect/good"; + free(prefix); + prefix = xstrfmt("refs/bisect/%s", term_good); + opts.prefix = prefix; refs_for_each_ref_ext(get_main_ref_store(the_repository), anti_reference, NULL, &opts); + free(prefix); + free(term_good); + free(term_bad); continue; } if (opt_with_value(arg, "--branches", &arg)) { diff --git a/commit-reach.c b/commit-reach.c index 5a52be90a66f5a..9b3ea46d6f2824 100644 --- a/commit-reach.c +++ b/commit-reach.c @@ -1143,7 +1143,6 @@ void ahead_behind(struct repository *r, struct commit_and_index { struct commit *commit; - unsigned int index; timestamp_t generation; }; @@ -1183,7 +1182,6 @@ void tips_reachable_from_bases(struct repository *r, for (size_t i = 0; i < tips_nr; i++) { commits[i].commit = tips[i]; - commits[i].index = i; commits[i].generation = commit_graph_generation(tips[i]); } @@ -1191,6 +1189,9 @@ void tips_reachable_from_bases(struct repository *r, QSORT(commits, tips_nr, compare_commit_and_index_by_generation); min_generation = commits[0].generation; + for (size_t i = 0; i < tips_nr; i++) + commits[i].commit->object.flags |= RESULT; + while (bases) { repo_parse_commit(r, bases->item); commit_list_insert(bases->item, &stack); @@ -1201,20 +1202,16 @@ void tips_reachable_from_bases(struct repository *r, int explored_all_parents = 1; struct commit_list *p; struct commit *c = stack->item; - timestamp_t c_gen = commit_graph_generation(c); /* Does it match any of our tips? */ - for (size_t j = min_generation_index; j < tips_nr; j++) { - if (c_gen < commits[j].generation) - break; - - if (commits[j].commit == c) { - tips[commits[j].index]->object.flags |= mark; + { + if (c->object.flags & RESULT) { + c->object.flags |= mark; - if (j == min_generation_index) { - unsigned int k = j + 1; + if (commits[min_generation_index].commit->object.flags & mark) { + unsigned int k = min_generation_index + 1; while (k < tips_nr && - (tips[commits[k].index]->object.flags & mark)) + (commits[k].commit->object.flags & mark)) k++; /* Terminate early if all found. */ @@ -1250,6 +1247,8 @@ void tips_reachable_from_bases(struct repository *r, } done: + for (size_t i = 0; i < tips_nr; i++) + commits[i].commit->object.flags &= ~RESULT; free(commits); repo_clear_commit_marks(r, SEEN); commit_list_free(stack); diff --git a/commit.c b/commit.c index e3e7352e69682d..fd8723502ed332 100644 --- a/commit.c +++ b/commit.c @@ -434,6 +434,27 @@ static inline void set_commit_tree(struct commit *c, struct tree *t) c->maybe_tree = t; } +static void load_tree_from_commit_contents(struct repository *r, struct commit *commit) +{ + enum object_type type; + unsigned long size; + char *buf; + const char *p; + struct object_id tree_oid; + + buf = odb_read_object(r->objects, &commit->object.oid, &type, &size); + if (!buf) + return; + + if (type == OBJ_COMMIT && + skip_prefix(buf, "tree ", &p) && + !parse_oid_hex_algop(p, &tree_oid, &p, r->hash_algo) && + *p == '\n') + set_commit_tree(commit, lookup_tree(r, &tree_oid)); + + free(buf); +} + struct tree *repo_get_commit_tree(struct repository *r, const struct commit *commit) { @@ -443,7 +464,17 @@ struct tree *repo_get_commit_tree(struct repository *r, if (commit_graph_position(commit) != COMMIT_NOT_FROM_GRAPH) return get_commit_tree_in_graph(r, commit); - return NULL; + /* + * This is either a corrupt commit, or one which we partially loaded + * from a graph file but then subsequently threw away the graph data. + * + * Optimistically assume it's the latter and try to reload from + * scratch. This gives a performance penalty if it really is a corrupt + * commit, but presumably that happens rarely (and only once per + * process). + */ + load_tree_from_commit_contents(r, (struct commit *)commit); + return commit->maybe_tree; } struct object_id *get_commit_tree_oid(const struct commit *commit) diff --git a/compat/fsmonitor/fsm-health-linux.c b/compat/fsmonitor/fsm-health-linux.c new file mode 100644 index 00000000000000..43d67c4b8b9efa --- /dev/null +++ b/compat/fsmonitor/fsm-health-linux.c @@ -0,0 +1,33 @@ +#include "git-compat-util.h" +#include "config.h" +#include "fsmonitor-ll.h" +#include "fsm-health.h" +#include "fsmonitor--daemon.h" + +/* + * The Linux fsmonitor implementation uses inotify which has its own + * mechanisms for detecting filesystem unmount and other events that + * would require the daemon to shutdown. Therefore, we don't need + * a separate health thread like Windows does. + * + * These stub functions satisfy the interface requirements. + */ + +int fsm_health__ctor(struct fsmonitor_daemon_state *state UNUSED) +{ + return 0; +} + +void fsm_health__dtor(struct fsmonitor_daemon_state *state UNUSED) +{ + return; +} + +void fsm_health__loop(struct fsmonitor_daemon_state *state UNUSED) +{ + return; +} + +void fsm_health__stop_async(struct fsmonitor_daemon_state *state UNUSED) +{ +} diff --git a/compat/fsmonitor/fsm-ipc-darwin.c b/compat/fsmonitor/fsm-ipc-unix.c similarity index 100% rename from compat/fsmonitor/fsm-ipc-darwin.c rename to compat/fsmonitor/fsm-ipc-unix.c diff --git a/compat/fsmonitor/fsm-listen-linux.c b/compat/fsmonitor/fsm-listen-linux.c new file mode 100644 index 00000000000000..e3dca14b620ee3 --- /dev/null +++ b/compat/fsmonitor/fsm-listen-linux.c @@ -0,0 +1,746 @@ +#include "git-compat-util.h" +#include "dir.h" +#include "fsmonitor-ll.h" +#include "fsm-listen.h" +#include "fsmonitor--daemon.h" +#include "fsmonitor-path-utils.h" +#include "gettext.h" +#include "simple-ipc.h" +#include "string-list.h" +#include "trace.h" + +#include + +/* + * Safe value to bitwise OR with rest of mask for + * kernels that do not support IN_MASK_CREATE + */ +#ifndef IN_MASK_CREATE +#define IN_MASK_CREATE 0x00000000 +#endif + +enum shutdown_reason { + SHUTDOWN_CONTINUE = 0, + SHUTDOWN_STOP, + SHUTDOWN_ERROR, + SHUTDOWN_FORCE +}; + +struct watch_entry { + struct hashmap_entry ent; + int wd; + uint32_t cookie; + const char *dir; +}; + +struct rename_entry { + struct hashmap_entry ent; + time_t whence; + uint32_t cookie; + const char *dir; +}; + +struct fsm_listen_data { + int fd_inotify; + enum shutdown_reason shutdown; + struct hashmap watches; + struct hashmap renames; + struct hashmap revwatches; +}; + +static int watch_entry_cmp(const void *cmp_data UNUSED, + const struct hashmap_entry *eptr, + const struct hashmap_entry *entry_or_key, + const void *keydata UNUSED) +{ + const struct watch_entry *e1, *e2; + + e1 = container_of(eptr, const struct watch_entry, ent); + e2 = container_of(entry_or_key, const struct watch_entry, ent); + return e1->wd != e2->wd; +} + +static int revwatches_entry_cmp(const void *cmp_data UNUSED, + const struct hashmap_entry *eptr, + const struct hashmap_entry *entry_or_key, + const void *keydata UNUSED) +{ + const struct watch_entry *e1, *e2; + + e1 = container_of(eptr, const struct watch_entry, ent); + e2 = container_of(entry_or_key, const struct watch_entry, ent); + return strcmp(e1->dir, e2->dir); +} + +static int rename_entry_cmp(const void *cmp_data UNUSED, + const struct hashmap_entry *eptr, + const struct hashmap_entry *entry_or_key, + const void *keydata UNUSED) +{ + const struct rename_entry *e1, *e2; + + e1 = container_of(eptr, const struct rename_entry, ent); + e2 = container_of(entry_or_key, const struct rename_entry, ent); + return e1->cookie != e2->cookie; +} + +/* + * Register an inotify watch, add watch descriptor to path mapping + * and the reverse mapping. + */ +static int add_watch(const char *path, struct fsm_listen_data *data) +{ + const char *interned = strintern(path); + struct watch_entry *w1, *w2; + + /* add the inotify watch, don't allow watches to be modified */ + int wd = inotify_add_watch(data->fd_inotify, interned, + (IN_ALL_EVENTS | IN_ONLYDIR | IN_MASK_CREATE) + ^ IN_ACCESS ^ IN_CLOSE ^ IN_OPEN); + if (wd < 0) { + if (errno == ENOENT || errno == ENOTDIR) + return 0; /* directory was deleted or is not a directory */ + if (errno == EEXIST) + return 0; /* watch already exists, no action needed */ + if (errno == ENOSPC) + return error(_("inotify watch limit reached; " + "increase fs.inotify.max_user_watches")); + return error_errno(_("inotify_add_watch('%s') failed"), interned); + } + + /* add watch descriptor -> directory mapping */ + CALLOC_ARRAY(w1, 1); + w1->wd = wd; + w1->dir = interned; + hashmap_entry_init(&w1->ent, memhash(&w1->wd, sizeof(int))); + hashmap_add(&data->watches, &w1->ent); + + /* add directory -> watch descriptor mapping */ + CALLOC_ARRAY(w2, 1); + w2->wd = wd; + w2->dir = interned; + hashmap_entry_init(&w2->ent, strhash(w2->dir)); + hashmap_add(&data->revwatches, &w2->ent); + + return 0; +} + +/* + * Remove the inotify watch, the watch descriptor to path mapping + * and the reverse mapping. + */ +static void remove_watch(struct watch_entry *w, struct fsm_listen_data *data) +{ + struct watch_entry k1, k2, *w1, *w2; + + /* remove watch, ignore error if kernel already did it */ + if (inotify_rm_watch(data->fd_inotify, w->wd) && errno != EINVAL) + error_errno(_("inotify_rm_watch() failed")); + + k1.wd = w->wd; + hashmap_entry_init(&k1.ent, memhash(&k1.wd, sizeof(int))); + w1 = hashmap_remove_entry(&data->watches, &k1, ent, NULL); + if (!w1) + BUG("double remove of watch for '%s'", w->dir); + + if (w1->cookie) + BUG("removing watch for '%s' which has a pending rename", w1->dir); + + k2.dir = w->dir; + hashmap_entry_init(&k2.ent, strhash(k2.dir)); + w2 = hashmap_remove_entry(&data->revwatches, &k2, ent, NULL); + if (!w2) + BUG("double remove of reverse watch for '%s'", w->dir); + + /* w1->dir and w2->dir are interned strings, we don't own them */ + free(w1); + free(w2); +} + +/* + * Check for stale directory renames. + * + * https://man7.org/linux/man-pages/man7/inotify.7.html + * + * Allow for some small timeout to account for the fact that insertion of the + * IN_MOVED_FROM+IN_MOVED_TO event pair is not atomic, and the possibility that + * there may not be any IN_MOVED_TO event. + * + * If the IN_MOVED_TO event is not received within the timeout then events have + * been missed and the monitor is in an inconsistent state with respect to the + * filesystem. + */ +static int check_stale_dir_renames(struct hashmap *renames, time_t max_age) +{ + struct rename_entry *re; + struct hashmap_iter iter; + + hashmap_for_each_entry(renames, &iter, re, ent) { + if (re->whence <= max_age) + return -1; + } + return 0; +} + +/* + * Track pending renames. + * + * Tracking is done via an event cookie to watch descriptor mapping. + * + * A rename is not complete until matching an IN_MOVED_TO event is received + * for a corresponding IN_MOVED_FROM event. + */ +static void add_dir_rename(uint32_t cookie, const char *path, + struct fsm_listen_data *data) +{ + struct watch_entry k, *w; + struct rename_entry *re; + + /* lookup the watch descriptor for the given path */ + k.dir = path; + hashmap_entry_init(&k.ent, strhash(path)); + w = hashmap_get_entry(&data->revwatches, &k, ent, NULL); + if (!w) { + /* + * This can happen in rare cases where the directory was + * moved before we had a chance to add a watch on it. + * Just ignore this rename. + */ + trace_printf_key(&trace_fsmonitor, + "no watch found for rename from '%s'", path); + return; + } + w->cookie = cookie; + + /* add the pending rename to match against later */ + CALLOC_ARRAY(re, 1); + re->dir = w->dir; + re->cookie = w->cookie; + re->whence = time(NULL); + hashmap_entry_init(&re->ent, memhash(&re->cookie, sizeof(uint32_t))); + hashmap_add(&data->renames, &re->ent); +} + +/* + * Handle directory renames + * + * Once an IN_MOVED_TO event is received, lookup the rename tracking information + * via the event cookie and use this information to update the watch. + */ +static void rename_dir(uint32_t cookie, const char *path, + struct fsm_listen_data *data) +{ + struct rename_entry rek, *re; + struct watch_entry k, *w; + + /* lookup a pending rename to match */ + rek.cookie = cookie; + hashmap_entry_init(&rek.ent, memhash(&rek.cookie, sizeof(uint32_t))); + re = hashmap_get_entry(&data->renames, &rek, ent, NULL); + if (re) { + k.dir = re->dir; + hashmap_entry_init(&k.ent, strhash(k.dir)); + w = hashmap_get_entry(&data->revwatches, &k, ent, NULL); + if (w) { + w->cookie = 0; /* rename handled */ + remove_watch(w, data); + if (add_watch(path, data)) + trace_printf_key(&trace_fsmonitor, + "failed to add watch for renamed dir '%s'", + path); + } else { + /* Directory was moved out of watch tree */ + trace_printf_key(&trace_fsmonitor, + "no matching watch for rename to '%s'", path); + } + hashmap_remove_entry(&data->renames, &rek, ent, NULL); + free(re); + } else { + /* Directory was moved from outside the watch tree */ + trace_printf_key(&trace_fsmonitor, + "no matching cookie for rename to '%s'", path); + } +} + +/* + * Recursively add watches to every directory under path + */ +static int register_inotify(const char *path, + struct fsmonitor_daemon_state *state, + struct fsmonitor_batch *batch) +{ + DIR *dir; + const char *rel; + struct strbuf current = STRBUF_INIT; + struct dirent *de; + struct stat fs; + int ret = -1; + + dir = opendir(path); + if (!dir) { + if (errno == ENOENT || errno == ENOTDIR) + return 0; /* directory was deleted */ + return error_errno(_("opendir('%s') failed"), path); + } + + while ((de = readdir_skip_dot_and_dotdot(dir)) != NULL) { + strbuf_reset(¤t); + strbuf_addf(¤t, "%s/%s", path, de->d_name); + if (lstat(current.buf, &fs)) { + if (errno == ENOENT) + continue; /* file was deleted */ + error_errno(_("lstat('%s') failed"), current.buf); + goto failed; + } + + /* recurse into directory */ + if (S_ISDIR(fs.st_mode)) { + if (add_watch(current.buf, state->listen_data)) + goto failed; + if (register_inotify(current.buf, state, batch)) + goto failed; + } else if (batch) { + rel = current.buf + state->path_worktree_watch.len + 1; + trace_printf_key(&trace_fsmonitor, "explicitly adding '%s'", rel); + fsmonitor_batch__add_path(batch, rel); + } + } + ret = 0; + +failed: + strbuf_release(¤t); + if (closedir(dir) < 0) + return error_errno(_("closedir('%s') failed"), path); + return ret; +} + +static int em_rename_dir_from(uint32_t mask) +{ + return ((mask & IN_ISDIR) && (mask & IN_MOVED_FROM)); +} + +static int em_rename_dir_to(uint32_t mask) +{ + return ((mask & IN_ISDIR) && (mask & IN_MOVED_TO)); +} + +static int em_remove_watch(uint32_t mask) +{ + return (mask & IN_DELETE_SELF); +} + +static int em_dir_renamed(uint32_t mask) +{ + return ((mask & IN_ISDIR) && (mask & IN_MOVE)); +} + +static int em_dir_created(uint32_t mask) +{ + return ((mask & IN_ISDIR) && (mask & IN_CREATE)); +} + +static int em_dir_deleted(uint32_t mask) +{ + return ((mask & IN_ISDIR) && (mask & IN_DELETE)); +} + +static int em_force_shutdown(uint32_t mask) +{ + return (mask & IN_UNMOUNT) || (mask & IN_Q_OVERFLOW); +} + +static int em_ignore(uint32_t mask) +{ + return (mask & IN_IGNORED) || (mask & IN_MOVE_SELF); +} + +static void log_mask_set(const char *path, uint32_t mask) +{ + struct strbuf msg = STRBUF_INIT; + + if (mask & IN_ACCESS) + strbuf_addstr(&msg, "IN_ACCESS|"); + if (mask & IN_MODIFY) + strbuf_addstr(&msg, "IN_MODIFY|"); + if (mask & IN_ATTRIB) + strbuf_addstr(&msg, "IN_ATTRIB|"); + if (mask & IN_CLOSE_WRITE) + strbuf_addstr(&msg, "IN_CLOSE_WRITE|"); + if (mask & IN_CLOSE_NOWRITE) + strbuf_addstr(&msg, "IN_CLOSE_NOWRITE|"); + if (mask & IN_OPEN) + strbuf_addstr(&msg, "IN_OPEN|"); + if (mask & IN_MOVED_FROM) + strbuf_addstr(&msg, "IN_MOVED_FROM|"); + if (mask & IN_MOVED_TO) + strbuf_addstr(&msg, "IN_MOVED_TO|"); + if (mask & IN_CREATE) + strbuf_addstr(&msg, "IN_CREATE|"); + if (mask & IN_DELETE) + strbuf_addstr(&msg, "IN_DELETE|"); + if (mask & IN_DELETE_SELF) + strbuf_addstr(&msg, "IN_DELETE_SELF|"); + if (mask & IN_MOVE_SELF) + strbuf_addstr(&msg, "IN_MOVE_SELF|"); + if (mask & IN_UNMOUNT) + strbuf_addstr(&msg, "IN_UNMOUNT|"); + if (mask & IN_Q_OVERFLOW) + strbuf_addstr(&msg, "IN_Q_OVERFLOW|"); + if (mask & IN_IGNORED) + strbuf_addstr(&msg, "IN_IGNORED|"); + if (mask & IN_ISDIR) + strbuf_addstr(&msg, "IN_ISDIR|"); + + strbuf_strip_suffix(&msg, "|"); + + trace_printf_key(&trace_fsmonitor, "inotify_event: '%s', mask=%#8.8x %s", + path, mask, msg.buf); + + strbuf_release(&msg); +} + +int fsm_listen__ctor(struct fsmonitor_daemon_state *state) +{ + int fd; + int ret = 0; + struct fsm_listen_data *data; + + CALLOC_ARRAY(data, 1); + state->listen_data = data; + state->listen_error_code = -1; + data->fd_inotify = -1; + data->shutdown = SHUTDOWN_ERROR; + + fd = inotify_init1(O_NONBLOCK); + if (fd < 0) { + FREE_AND_NULL(state->listen_data); + return error_errno(_("inotify_init1() failed")); + } + + data->fd_inotify = fd; + + hashmap_init(&data->watches, watch_entry_cmp, NULL, 0); + hashmap_init(&data->renames, rename_entry_cmp, NULL, 0); + hashmap_init(&data->revwatches, revwatches_entry_cmp, NULL, 0); + + if (add_watch(state->path_worktree_watch.buf, data)) + ret = -1; + else if (register_inotify(state->path_worktree_watch.buf, state, NULL)) + ret = -1; + else if (state->nr_paths_watching > 1) { + if (add_watch(state->path_gitdir_watch.buf, data)) + ret = -1; + else if (register_inotify(state->path_gitdir_watch.buf, state, NULL)) + ret = -1; + } + + if (!ret) { + state->listen_error_code = 0; + data->shutdown = SHUTDOWN_CONTINUE; + } + + return ret; +} + +void fsm_listen__dtor(struct fsmonitor_daemon_state *state) +{ + struct fsm_listen_data *data; + struct hashmap_iter iter; + struct watch_entry *w; + struct watch_entry **to_remove; + size_t nr_to_remove = 0, alloc_to_remove = 0; + size_t i; + int fd; + + if (!state || !state->listen_data) + return; + + data = state->listen_data; + fd = data->fd_inotify; + + /* + * Collect all entries first, then remove them. + * We can't modify the hashmap while iterating over it. + */ + to_remove = NULL; + hashmap_for_each_entry(&data->watches, &iter, w, ent) { + ALLOC_GROW(to_remove, nr_to_remove + 1, alloc_to_remove); + to_remove[nr_to_remove++] = w; + } + + for (i = 0; i < nr_to_remove; i++) { + to_remove[i]->cookie = 0; /* ignore any pending renames */ + remove_watch(to_remove[i], data); + } + free(to_remove); + + hashmap_clear(&data->watches); + + hashmap_clear(&data->revwatches); /* remove_watch freed the entries */ + + hashmap_clear_and_free(&data->renames, struct rename_entry, ent); + + FREE_AND_NULL(state->listen_data); + + if (fd >= 0 && (close(fd) < 0)) + error_errno(_("closing inotify file descriptor failed")); +} + +void fsm_listen__stop_async(struct fsmonitor_daemon_state *state) +{ + if (state && state->listen_data && + state->listen_data->shutdown == SHUTDOWN_CONTINUE) + state->listen_data->shutdown = SHUTDOWN_STOP; +} + +/* + * Process a single inotify event and queue for publication. + */ +static int process_event(const char *path, + const struct inotify_event *event, + struct fsmonitor_batch **batch, + struct string_list *cookie_list, + struct fsmonitor_daemon_state *state) +{ + const char *rel; + const char *last_sep; + + switch (fsmonitor_classify_path_absolute(state, path)) { + case IS_INSIDE_DOT_GIT_WITH_COOKIE_PREFIX: + case IS_INSIDE_GITDIR_WITH_COOKIE_PREFIX: + /* Use just the filename of the cookie file. */ + last_sep = find_last_dir_sep(path); + string_list_append(cookie_list, + last_sep ? last_sep + 1 : path); + break; + case IS_INSIDE_DOT_GIT: + case IS_INSIDE_GITDIR: + break; + case IS_DOT_GIT: + case IS_GITDIR: + /* + * If .git directory is deleted or renamed away, + * we have to quit. + */ + if (em_dir_deleted(event->mask)) { + trace_printf_key(&trace_fsmonitor, + "event: gitdir removed"); + state->listen_data->shutdown = SHUTDOWN_FORCE; + goto done; + } + + if (em_dir_renamed(event->mask)) { + trace_printf_key(&trace_fsmonitor, + "event: gitdir renamed"); + state->listen_data->shutdown = SHUTDOWN_FORCE; + goto done; + } + break; + case IS_WORKDIR_PATH: + /* normal events in the working directory */ + if (trace_pass_fl(&trace_fsmonitor)) + log_mask_set(path, event->mask); + + if (!*batch) + *batch = fsmonitor_batch__new(); + + rel = path + state->path_worktree_watch.len + 1; + fsmonitor_batch__add_path(*batch, rel); + + if (em_dir_deleted(event->mask)) + break; + + /* received IN_MOVE_FROM, add tracking for expected IN_MOVE_TO */ + if (em_rename_dir_from(event->mask)) + add_dir_rename(event->cookie, path, state->listen_data); + + /* received IN_MOVE_TO, update watch to reflect new path */ + if (em_rename_dir_to(event->mask)) { + rename_dir(event->cookie, path, state->listen_data); + if (register_inotify(path, state, *batch)) { + state->listen_data->shutdown = SHUTDOWN_ERROR; + goto done; + } + } + + if (em_dir_created(event->mask)) { + if (add_watch(path, state->listen_data)) { + state->listen_data->shutdown = SHUTDOWN_ERROR; + goto done; + } + if (register_inotify(path, state, *batch)) { + state->listen_data->shutdown = SHUTDOWN_ERROR; + goto done; + } + } + break; + case IS_OUTSIDE_CONE: + default: + trace_printf_key(&trace_fsmonitor, + "ignoring '%s'", path); + break; + } + return 0; +done: + return -1; +} + +/* + * Read the inotify event stream and pre-process events before further + * processing and eventual publishing. + */ +static void handle_events(struct fsmonitor_daemon_state *state) +{ + /* See https://man7.org/linux/man-pages/man7/inotify.7.html */ + char buf[4096] + __attribute__ ((aligned(__alignof__(struct inotify_event)))); + + struct hashmap *watches = &state->listen_data->watches; + struct fsmonitor_batch *batch = NULL; + struct string_list cookie_list = STRING_LIST_INIT_DUP; + struct watch_entry k, *w; + struct strbuf path = STRBUF_INIT; + const struct inotify_event *event; + int fd = state->listen_data->fd_inotify; + ssize_t len; + char *ptr, *p; + + for (;;) { + len = read(fd, buf, sizeof(buf)); + if (len == -1) { + if (errno == EAGAIN || errno == EINTR) + goto done; + error_errno(_("reading inotify message stream failed")); + state->listen_data->shutdown = SHUTDOWN_ERROR; + goto done; + } + + /* nothing to read */ + if (len == 0) + goto done; + + /* Loop over all events in the buffer. */ + for (ptr = buf; ptr < buf + len; + ptr += sizeof(struct inotify_event) + event->len) { + + event = (const struct inotify_event *)ptr; + + if (em_ignore(event->mask)) + continue; + + /* File system was unmounted or event queue overflowed */ + if (em_force_shutdown(event->mask)) { + if (trace_pass_fl(&trace_fsmonitor)) + log_mask_set("forcing shutdown", event->mask); + state->listen_data->shutdown = SHUTDOWN_FORCE; + goto done; + } + + k.wd = event->wd; + hashmap_entry_init(&k.ent, memhash(&k.wd, sizeof(int))); + + w = hashmap_get_entry(watches, &k, ent, NULL); + if (!w) { + /* Watch was removed, skip event */ + continue; + } + + /* directory watch was removed */ + if (em_remove_watch(event->mask)) { + remove_watch(w, state->listen_data); + continue; + } + + strbuf_reset(&path); + strbuf_addf(&path, "%s/%s", w->dir, event->name); + + p = fsmonitor__resolve_alias(path.buf, &state->alias); + if (!p) + p = strbuf_detach(&path, NULL); + + if (process_event(p, event, &batch, &cookie_list, state)) { + free(p); + goto done; + } + free(p); + } + strbuf_reset(&path); + fsmonitor_publish(state, batch, &cookie_list); + string_list_clear(&cookie_list, 0); + batch = NULL; + } +done: + strbuf_release(&path); + fsmonitor_batch__free_list(batch); + string_list_clear(&cookie_list, 0); +} + +/* + * Non-blocking read of the inotify events stream. The inotify fd is polled + * frequently to help minimize the number of queue overflows. + */ +void fsm_listen__loop(struct fsmonitor_daemon_state *state) +{ + int poll_num; + /* + * Interval in seconds between checks for stale directory renames. + * A directory rename that is not completed within this window + * (i.e. no matching IN_MOVED_TO for an IN_MOVED_FROM) indicates + * missed events, forcing a shutdown. + */ + const int interval = 1; + time_t checked = time(NULL); + struct pollfd fds[1]; + + fds[0].fd = state->listen_data->fd_inotify; + fds[0].events = POLLIN; + + /* + * Our fs event listener is now running, so it's safe to start + * serving client requests. + */ + ipc_server_start_async(state->ipc_server_data); + + for (;;) { + switch (state->listen_data->shutdown) { + case SHUTDOWN_CONTINUE: + poll_num = poll(fds, 1, 50); + if (poll_num == -1) { + if (errno == EINTR) + continue; + error_errno(_("polling inotify message stream failed")); + state->listen_data->shutdown = SHUTDOWN_ERROR; + continue; + } + + if ((time(NULL) - checked) >= interval) { + checked = time(NULL); + if (check_stale_dir_renames(&state->listen_data->renames, + checked - interval)) { + trace_printf_key(&trace_fsmonitor, + "missed IN_MOVED_TO events, forcing shutdown"); + state->listen_data->shutdown = SHUTDOWN_FORCE; + continue; + } + } + + if (poll_num > 0 && (fds[0].revents & POLLIN)) + handle_events(state); + + continue; + case SHUTDOWN_ERROR: + state->listen_error_code = -1; + ipc_server_stop_async(state->ipc_server_data); + break; + case SHUTDOWN_FORCE: + state->listen_error_code = 0; + ipc_server_stop_async(state->ipc_server_data); + break; + case SHUTDOWN_STOP: + default: + state->listen_error_code = 0; + break; + } + return; + } +} diff --git a/compat/fsmonitor/fsm-path-utils-linux.c b/compat/fsmonitor/fsm-path-utils-linux.c new file mode 100644 index 00000000000000..c9866b1b24ca8e --- /dev/null +++ b/compat/fsmonitor/fsm-path-utils-linux.c @@ -0,0 +1,217 @@ +#include "git-compat-util.h" +#include "fsmonitor-ll.h" +#include "fsmonitor-path-utils.h" +#include "gettext.h" +#include "trace.h" + +#include + +#ifdef HAVE_LINUX_MAGIC_H +#include +#endif + +/* + * Filesystem magic numbers for remote filesystems. + * Defined here if not available in linux/magic.h. + */ +#ifndef CIFS_SUPER_MAGIC +#define CIFS_SUPER_MAGIC 0xff534d42 +#endif +#ifndef SMB_SUPER_MAGIC +#define SMB_SUPER_MAGIC 0x517b +#endif +#ifndef SMB2_SUPER_MAGIC +#define SMB2_SUPER_MAGIC 0xfe534d42 +#endif +#ifndef NFS_SUPER_MAGIC +#define NFS_SUPER_MAGIC 0x6969 +#endif +#ifndef AFS_SUPER_MAGIC +#define AFS_SUPER_MAGIC 0x5346414f +#endif +#ifndef CODA_SUPER_MAGIC +#define CODA_SUPER_MAGIC 0x73757245 +#endif +#ifndef FUSE_SUPER_MAGIC +#define FUSE_SUPER_MAGIC 0x65735546 +#endif + +/* + * Check if filesystem type is a remote filesystem. + */ +static int is_remote_fs(unsigned long f_type) +{ + switch (f_type) { + case CIFS_SUPER_MAGIC: + case SMB_SUPER_MAGIC: + case SMB2_SUPER_MAGIC: + case NFS_SUPER_MAGIC: + case AFS_SUPER_MAGIC: + case CODA_SUPER_MAGIC: + case FUSE_SUPER_MAGIC: + return 1; + default: + return 0; + } +} + +/* + * Map filesystem magic numbers to human-readable names as a fallback + * when /proc/mounts is unavailable. This only covers the remote and + * special filesystems in is_remote_fs() above; local filesystems are + * never flagged as incompatible, so we do not need their names here. + */ +static const char *get_fs_typename(unsigned long f_type) +{ + switch (f_type) { + case CIFS_SUPER_MAGIC: + return "cifs"; + case SMB_SUPER_MAGIC: + return "smb"; + case SMB2_SUPER_MAGIC: + return "smb2"; + case NFS_SUPER_MAGIC: + return "nfs"; + case AFS_SUPER_MAGIC: + return "afs"; + case CODA_SUPER_MAGIC: + return "coda"; + case FUSE_SUPER_MAGIC: + return "fuse"; + default: + return "unknown"; + } +} + +/* + * Find the mount point for a given path by reading /proc/mounts. + * + * statfs(2) gives us f_type (the magic number) but not the human-readable + * filesystem type string. We scan /proc/mounts to find the mount entry + * whose path is the longest prefix of ours and whose f_fsid matches, + * which gives us the fstype string (e.g. "nfs", "ext4") for logging. + */ +static char *find_mount(const char *path, const struct statfs *path_fs) +{ + FILE *fp; + struct strbuf line = STRBUF_INIT; + struct strbuf match = STRBUF_INIT; + struct strbuf fstype = STRBUF_INIT; + char *result = NULL; + + fp = fopen("/proc/mounts", "r"); + if (!fp) + return NULL; + + while (strbuf_getline(&line, fp) != EOF) { + char *fields[6]; + char *p = line.buf; + int i; + + /* Parse mount entry: device mountpoint fstype options dump pass */ + for (i = 0; i < 6 && p; i++) { + fields[i] = p; + p = strchr(p, ' '); + if (p) + *p++ = '\0'; + } + + if (i >= 3) { + const char *mountpoint = fields[1]; + const char *type = fields[2]; + struct statfs mount_fs; + + /* Check if this mount point is a prefix of our path */ + if (starts_with(path, mountpoint) && + (path[strlen(mountpoint)] == '/' || + path[strlen(mountpoint)] == '\0')) { + /* Check if filesystem ID matches */ + if (statfs(mountpoint, &mount_fs) == 0 && + !memcmp(&mount_fs.f_fsid, &path_fs->f_fsid, + sizeof(mount_fs.f_fsid))) { + /* Keep the longest matching mount point */ + if (strlen(mountpoint) > match.len) { + strbuf_reset(&match); + strbuf_addstr(&match, mountpoint); + strbuf_reset(&fstype); + strbuf_addstr(&fstype, type); + } + } + } + } + } + + fclose(fp); + strbuf_release(&line); + strbuf_release(&match); + + if (fstype.len) + result = strbuf_detach(&fstype, NULL); + else + strbuf_release(&fstype); + + return result; +} + +int fsmonitor__get_fs_info(const char *path, struct fs_info *fs_info) +{ + struct statfs fs; + + if (statfs(path, &fs) == -1) { + int saved_errno = errno; + trace_printf_key(&trace_fsmonitor, "statfs('%s') failed: %s", + path, strerror(saved_errno)); + errno = saved_errno; + return -1; + } + + trace_printf_key(&trace_fsmonitor, + "statfs('%s') [type 0x%08lx]", + path, (unsigned long)fs.f_type); + + fs_info->is_remote = is_remote_fs(fs.f_type); + + /* + * Try to get filesystem type from /proc/mounts for a more + * descriptive name. + */ + fs_info->typename = find_mount(path, &fs); + if (!fs_info->typename) + fs_info->typename = xstrdup(get_fs_typename(fs.f_type)); + + trace_printf_key(&trace_fsmonitor, + "'%s' is_remote: %d, typename: %s", + path, fs_info->is_remote, fs_info->typename); + + return 0; +} + +int fsmonitor__is_fs_remote(const char *path) +{ + struct fs_info fs; + + if (fsmonitor__get_fs_info(path, &fs)) + return -1; + + free(fs.typename); + + return fs.is_remote; +} + +/* + * No-op for Linux - we don't have firmlinks like macOS. + */ +int fsmonitor__get_alias(const char *path UNUSED, + struct alias_info *info UNUSED) +{ + return 0; +} + +/* + * No-op for Linux - we don't have firmlinks like macOS. + */ +char *fsmonitor__resolve_alias(const char *path UNUSED, + const struct alias_info *info UNUSED) +{ + return NULL; +} diff --git a/compat/fsmonitor/fsm-settings-darwin.c b/compat/fsmonitor/fsm-settings-unix.c similarity index 100% rename from compat/fsmonitor/fsm-settings-darwin.c rename to compat/fsmonitor/fsm-settings-unix.c diff --git a/compat/win32/pthread.c b/compat/win32/pthread.c index 7e93146963ec56..398caa96029718 100644 --- a/compat/win32/pthread.c +++ b/compat/win32/pthread.c @@ -66,3 +66,29 @@ int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex) return err_win_to_posix(GetLastError()); return 0; } + +int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, + const struct timespec *abstime) +{ + struct timeval now; + long long now_ms, deadline_ms; + DWORD timeout_ms; + + gettimeofday(&now, NULL); + now_ms = (long long)now.tv_sec * 1000 + now.tv_usec / 1000; + deadline_ms = (long long)abstime->tv_sec * 1000 + + abstime->tv_nsec / 1000000; + + if (deadline_ms <= now_ms) + return ETIMEDOUT; + else + timeout_ms = (DWORD)(deadline_ms - now_ms); + + if (SleepConditionVariableCS(cond, mutex, timeout_ms) == 0) { + DWORD err = GetLastError(); + if (err == ERROR_TIMEOUT) + return ETIMEDOUT; + return err_win_to_posix(err); + } + return 0; +} diff --git a/compat/win32/pthread.h b/compat/win32/pthread.h index ccacc5a53ba976..d80df8d12af2dc 100644 --- a/compat/win32/pthread.h +++ b/compat/win32/pthread.h @@ -64,6 +64,8 @@ int win32_pthread_join(pthread_t *thread, void **value_ptr); pthread_t pthread_self(void); int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); +int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, + const struct timespec *abstime); static inline void NORETURN pthread_exit(void *ret) { diff --git a/config.mak.uname b/config.mak.uname index ce5e7de7792724..f9a5ad97209dfd 100644 --- a/config.mak.uname +++ b/config.mak.uname @@ -69,6 +69,16 @@ ifeq ($(uname_S),Linux) BASIC_CFLAGS += -std=c99 endif LINK_FUZZ_PROGRAMS = YesPlease + + # The builtin FSMonitor on Linux builds upon Simple-IPC. Both require + # Unix domain sockets and PThreads. + ifndef NO_PTHREADS + ifndef NO_UNIX_SOCKETS + FSMONITOR_DAEMON_BACKEND = linux + FSMONITOR_OS_SETTINGS = unix + BASIC_CFLAGS += -DHAVE_LINUX_MAGIC_H + endif + endif endif ifeq ($(uname_S),GNU/kFreeBSD) HAVE_ALLOCA_H = YesPlease @@ -168,7 +178,7 @@ ifeq ($(uname_S),Darwin) ifndef NO_PTHREADS ifndef NO_UNIX_SOCKETS FSMONITOR_DAEMON_BACKEND = darwin - FSMONITOR_OS_SETTINGS = darwin + FSMONITOR_OS_SETTINGS = unix endif endif diff --git a/connected.c b/connected.c index 6718503649da8a..7e269768327238 100644 --- a/connected.c +++ b/connected.c @@ -76,6 +76,8 @@ int check_connected(oid_iterate_fn fn, void *cb_data, promisor_pack_found: ; } while ((oid = fn(cb_data)) != NULL); + if (opt->err_fd) + close(opt->err_fd); return 0; } diff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt index 9d6b98ecb6baf3..a57c4b464fa456 100644 --- a/contrib/buildsystems/CMakeLists.txt +++ b/contrib/buildsystems/CMakeLists.txt @@ -292,23 +292,26 @@ endif() if(SUPPORTS_SIMPLE_IPC) if(CMAKE_SYSTEM_NAME STREQUAL "Windows") - add_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND) - list(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-win32.c) - list(APPEND compat_SOURCES compat/fsmonitor/fsm-health-win32.c) - list(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-win32.c) - list(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-win32.c) - - add_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS) - list(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-win32.c) + set(FSMONITOR_DAEMON_BACKEND "win32") + set(FSMONITOR_OS_SETTINGS "win32") elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin") + set(FSMONITOR_DAEMON_BACKEND "darwin") + set(FSMONITOR_OS_SETTINGS "unix") + elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") + set(FSMONITOR_DAEMON_BACKEND "linux") + set(FSMONITOR_OS_SETTINGS "unix") + add_compile_definitions(HAVE_LINUX_MAGIC_H) + endif() + + if(FSMONITOR_DAEMON_BACKEND) add_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND) - list(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-darwin.c) - list(APPEND compat_SOURCES compat/fsmonitor/fsm-health-darwin.c) - list(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-darwin.c) - list(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-darwin.c) + list(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-${FSMONITOR_DAEMON_BACKEND}.c) + list(APPEND compat_SOURCES compat/fsmonitor/fsm-health-${FSMONITOR_DAEMON_BACKEND}.c) + list(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-${FSMONITOR_OS_SETTINGS}.c) + list(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-${FSMONITOR_DAEMON_BACKEND}.c) add_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS) - list(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-darwin.c) + list(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-${FSMONITOR_OS_SETTINGS}.c) endif() endif() @@ -1151,8 +1154,8 @@ endif() file(STRINGS ${CMAKE_SOURCE_DIR}/GIT-BUILD-OPTIONS.in git_build_options NEWLINE_CONSUME) string(REPLACE "@BROKEN_PATH_FIX@" "" git_build_options "${git_build_options}") string(REPLACE "@DIFF@" "'${DIFF}'" git_build_options "${git_build_options}") -string(REPLACE "@FSMONITOR_DAEMON_BACKEND@" "win32" git_build_options "${git_build_options}") -string(REPLACE "@FSMONITOR_OS_SETTINGS@" "win32" git_build_options "${git_build_options}") +string(REPLACE "@FSMONITOR_DAEMON_BACKEND@" "${FSMONITOR_DAEMON_BACKEND}" git_build_options "${git_build_options}") +string(REPLACE "@FSMONITOR_OS_SETTINGS@" "${FSMONITOR_OS_SETTINGS}" git_build_options "${git_build_options}") string(REPLACE "@GITWEBDIR@" "'${GITWEBDIR}'" git_build_options "${git_build_options}") string(REPLACE "@GIT_INTEROP_MAKE_OPTS@" "" git_build_options "${git_build_options}") string(REPLACE "@GIT_PERF_LARGE_REPO@" "" git_build_options "${git_build_options}") diff --git a/fsmonitor-ipc.c b/fsmonitor-ipc.c index f1b163111194fb..6112d130644f04 100644 --- a/fsmonitor-ipc.c +++ b/fsmonitor-ipc.c @@ -61,6 +61,9 @@ static int spawn_daemon(void) cmd.git_cmd = 1; cmd.no_stdin = 1; + cmd.no_stdout = 1; + cmd.no_stderr = 1; + cmd.close_fd_above_stderr = 1; cmd.trace2_child_class = "fsmonitor"; strvec_pushl(&cmd.args, "fsmonitor--daemon", "start", NULL); diff --git a/graph.c b/graph.c index 26f6fbf000aef5..842282685f6cef 100644 --- a/graph.c +++ b/graph.c @@ -317,6 +317,15 @@ struct git_graph { struct strbuf prefix_buf; }; +static inline int graph_needs_truncation(struct git_graph *graph, int lane) +{ + int max = graph->revs->graph_max_lanes; + /* + * Ignore values <= 0, meaning no limit. + */ + return max > 0 && lane >= max; +} + static const char *diff_output_prefix_callback(struct diff_options *opt, void *data) { struct git_graph *graph = data; @@ -696,6 +705,19 @@ static void graph_update_columns(struct git_graph *graph) } } + /* + * If graph_max_lanes is set, cap the width + */ + if (graph->revs->graph_max_lanes > 0) { + /* + * width of "| " per lanes plus truncation mark "~ ". + * Allow commits from merges to align to the merged lane. + */ + int max_width = graph->revs->graph_max_lanes * 2 + 2; + if (graph->width > max_width) + graph->width = max_width; + } + /* * Shrink mapping_size to be the minimum necessary */ @@ -846,6 +868,10 @@ static void graph_output_padding_line(struct git_graph *graph, * Output a padding row, that leaves all branch lines unchanged */ for (i = 0; i < graph->num_new_columns; i++) { + if (graph_needs_truncation(graph, i)) { + graph_line_addstr(line, "~ "); + break; + } graph_line_write_column(line, &graph->new_columns[i], '|'); graph_line_addch(line, ' '); } @@ -903,6 +929,9 @@ static void graph_output_pre_commit_line(struct git_graph *graph, seen_this = 1; graph_line_write_column(line, col, '|'); graph_line_addchars(line, ' ', graph->expansion_row); + } else if (seen_this && graph_needs_truncation(graph, i)) { + graph_line_addstr(line, "~ "); + break; } else if (seen_this && (graph->expansion_row == 0)) { /* * This is the first line of the pre-commit output. @@ -994,6 +1023,16 @@ static void graph_draw_octopus_merge(struct git_graph *graph, struct graph_line col = &graph->new_columns[j]; graph_line_write_column(line, col, '-'); + + /* + * Commit is at commit_index, each iteration move one lane to + * the right from the commit. + */ + if (graph_needs_truncation(graph, graph->commit_index + 1 + i)) { + graph_line_addstr(line, "~ "); + break; + } + graph_line_write_column(line, col, (i == dashed_parents - 1) ? '.' : '-'); } @@ -1028,8 +1067,17 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line seen_this = 1; graph_output_commit_char(graph, line); + if (graph_needs_truncation(graph, i)) { + graph_line_addch(line, ' '); + break; + } + if (graph->num_parents > 2) graph_draw_octopus_merge(graph, line); + } else if (graph_needs_truncation(graph, i)) { + graph_line_addstr(line, "~ "); + seen_this = 1; + break; } else if (seen_this && (graph->edges_added > 1)) { graph_line_write_column(line, col, '\\'); } else if (seen_this && (graph->edges_added == 1)) { @@ -1065,13 +1113,46 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line /* * Update graph->state + * + * If the commit is a merge and the first parent is in a visible lane, + * then the GRAPH_POST_MERGE is needed to draw the merge lane. + * + * If the commit is over the truncation limit, but the first parent is on + * a visible lane, then we still need the merge lane but truncated. + * + * If both commit and first parent are over the truncation limit, then + * there's no need to draw the merge lane because it would work as a + * padding lane. */ - if (graph->num_parents > 1) - graph_update_state(graph, GRAPH_POST_MERGE); - else if (graph_is_mapping_correct(graph)) + if (graph->num_parents > 1) { + if (!graph_needs_truncation(graph, graph->commit_index)) { + graph_update_state(graph, GRAPH_POST_MERGE); + } else { + struct commit_list *p = first_interesting_parent(graph); + int lane; + + /* + * graph->num_parents are found using first_interesting_parent + * and next_interesting_parent so it can't be a scenario + * where num_parents > 1 and there are no interesting parents + */ + if (!p) + BUG("num_parents > 1 but no interesting parent"); + + lane = graph_find_new_column_by_commit(graph, p->item); + + if (!graph_needs_truncation(graph, lane)) + graph_update_state(graph, GRAPH_POST_MERGE); + else if (graph_is_mapping_correct(graph)) + graph_update_state(graph, GRAPH_PADDING); + else + graph_update_state(graph, GRAPH_COLLAPSING); + } + } else if (graph_is_mapping_correct(graph)) { graph_update_state(graph, GRAPH_PADDING); - else + } else { graph_update_state(graph, GRAPH_COLLAPSING); + } } static const char merge_chars[] = {'/', '|', '\\'}; @@ -1109,6 +1190,7 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l int par_column; int idx = graph->merge_layout; char c; + int truncated = 0; seen_this = 1; for (j = 0; j < graph->num_parents; j++) { @@ -1117,23 +1199,56 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l c = merge_chars[idx]; graph_line_write_column(line, &graph->new_columns[par_column], c); + + /* + * j counts parents, it needs to be halved to be + * comparable with i. Don't truncate if there are + * no more lanes to print (end of the lane) + */ + if (graph_needs_truncation(graph, j / 2 + i) && + j / 2 + i <= graph->num_columns) { + if ((j + i * 2) % 2 != 0) + graph_line_addch(line, ' '); + graph_line_addstr(line, "~ "); + truncated = 1; + break; + } + if (idx == 2) { - if (graph->edges_added > 0 || j < graph->num_parents - 1) + /* + * Check if the next lane needs truncation + * to avoid having the padding doubled + */ + if (graph_needs_truncation(graph, (j + 1) / 2 + i) && + j < graph->num_parents - 1) { + graph_line_addstr(line, "~ "); + truncated = 1; + break; + } else if (graph->edges_added > 0 || j < graph->num_parents - 1) graph_line_addch(line, ' '); } else { idx++; } parents = next_interesting_parent(graph, parents); } + if (truncated) + break; if (graph->edges_added == 0) graph_line_addch(line, ' '); - + } else if (graph_needs_truncation(graph, i)) { + graph_line_addstr(line, "~ "); + break; } else if (seen_this) { if (graph->edges_added > 0) graph_line_write_column(line, col, '\\'); else graph_line_write_column(line, col, '|'); - graph_line_addch(line, ' '); + /* + * If it's between two lanes and next would be truncated, + * don't add space padding. + */ + if (!graph_needs_truncation(graph, i + 1)) + graph_line_addch(line, ' '); } else { graph_line_write_column(line, col, '|'); if (graph->merge_layout != 0 || i != graph->commit_index - 1) { @@ -1164,6 +1279,7 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l short used_horizontal = 0; int horizontal_edge = -1; int horizontal_edge_target = -1; + int truncated = 0; /* * Swap the mapping and old_mapping arrays @@ -1279,26 +1395,35 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l */ for (i = 0; i < graph->mapping_size; i++) { int target = graph->mapping[i]; - if (target < 0) - graph_line_addch(line, ' '); - else if (target * 2 == i) - graph_line_write_column(line, &graph->new_columns[target], '|'); - else if (target == horizontal_edge_target && - i != horizontal_edge - 1) { - /* - * Set the mappings for all but the - * first segment to -1 so that they - * won't continue into the next line. - */ - if (i != (target * 2)+3) - graph->mapping[i] = -1; - used_horizontal = 1; - graph_line_write_column(line, &graph->new_columns[target], '_'); + + if (!truncated && graph_needs_truncation(graph, i / 2)) { + graph_line_addstr(line, "~ "); + truncated = 1; + } + + if (target < 0) { + if (!truncated) + graph_line_addch(line, ' '); + } else if (target * 2 == i) { + if (!truncated) + graph_line_write_column(line, &graph->new_columns[target], '|'); + } else if (target == horizontal_edge_target && + i != horizontal_edge - 1) { + /* + * Set the mappings for all but the + * first segment to -1 so that they + * won't continue into the next line. + */ + if (i != (target * 2)+3) + graph->mapping[i] = -1; + used_horizontal = 1; + if (!truncated) + graph_line_write_column(line, &graph->new_columns[target], '_'); } else { if (used_horizontal && i < horizontal_edge) graph->mapping[i] = -1; - graph_line_write_column(line, &graph->new_columns[target], '/'); - + if (!truncated) + graph_line_write_column(line, &graph->new_columns[target], '/'); } } @@ -1372,6 +1497,11 @@ static void graph_padding_line(struct git_graph *graph, struct strbuf *sb) for (i = 0; i < graph->num_columns; i++) { struct column *col = &graph->columns[i]; + if (graph_needs_truncation(graph, i)) { + graph_line_addstr(&line, "~ "); + break; + } + graph_line_write_column(&line, col, '|'); if (col->commit == graph->commit && graph->num_parents > 2) { diff --git a/meson.build b/meson.build index 664d8313295a26..064fe2e2f1f4e5 100644 --- a/meson.build +++ b/meson.build @@ -1351,10 +1351,17 @@ else endif fsmonitor_backend = '' +fsmonitor_os = '' if host_machine.system() == 'windows' fsmonitor_backend = 'win32' + fsmonitor_os = 'win32' +elif host_machine.system() == 'linux' and threads.found() and compiler.has_header('linux/magic.h') + fsmonitor_backend = 'linux' + fsmonitor_os = 'unix' + libgit_c_args += '-DHAVE_LINUX_MAGIC_H' elif host_machine.system() == 'darwin' fsmonitor_backend = 'darwin' + fsmonitor_os = 'unix' libgit_dependencies += dependency('CoreServices') endif if fsmonitor_backend != '' @@ -1363,14 +1370,14 @@ if fsmonitor_backend != '' compat_sources += [ 'compat/fsmonitor/fsm-health-' + fsmonitor_backend + '.c', - 'compat/fsmonitor/fsm-ipc-' + fsmonitor_backend + '.c', + 'compat/fsmonitor/fsm-ipc-' + fsmonitor_os + '.c', 'compat/fsmonitor/fsm-listen-' + fsmonitor_backend + '.c', 'compat/fsmonitor/fsm-path-utils-' + fsmonitor_backend + '.c', - 'compat/fsmonitor/fsm-settings-' + fsmonitor_backend + '.c', + 'compat/fsmonitor/fsm-settings-' + fsmonitor_os + '.c', ] endif build_options_config.set_quoted('FSMONITOR_DAEMON_BACKEND', fsmonitor_backend) -build_options_config.set_quoted('FSMONITOR_OS_SETTINGS', fsmonitor_backend) +build_options_config.set_quoted('FSMONITOR_OS_SETTINGS', fsmonitor_os) if not get_option('b_sanitize').contains('address') and get_option('regex').allowed() and compiler.has_header('regex.h') and compiler.get_define('REG_STARTEND', prefix: '#include ') != '' build_options_config.set('NO_REGEX', '') diff --git a/revision.c b/revision.c index e1970b9c5d34ed..5693618be4ec81 100644 --- a/revision.c +++ b/revision.c @@ -2610,6 +2610,8 @@ static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg } else if (!strcmp(arg, "--no-graph")) { graph_clear(revs->graph); revs->graph = NULL; + } else if (skip_prefix(arg, "--graph-lane-limit=", &optarg)) { + revs->graph_max_lanes = parse_count(optarg); } else if (!strcmp(arg, "--encode-email-headers")) { revs->encode_email_headers = 1; } else if (!strcmp(arg, "--no-encode-email-headers")) { @@ -3179,6 +3181,10 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, struct s if (revs->no_walk && revs->graph) die(_("options '%s' and '%s' cannot be used together"), "--no-walk", "--graph"); + + if (revs->graph_max_lanes > 0 && !revs->graph) + die(_("the option '%s' requires '%s'"), "--graph-lane-limit", "--graph"); + if (!revs->reflog_info && revs->grep_filter.use_reflog_filter) die(_("the option '%s' requires '%s'"), "--grep-reflog", "--walk-reflogs"); diff --git a/revision.h b/revision.h index 584f1338b5e323..c9a11827cc702e 100644 --- a/revision.h +++ b/revision.h @@ -305,6 +305,7 @@ struct rev_info { /* Display history graph */ struct git_graph *graph; + int graph_max_lanes; /* special limits */ int skip_count; diff --git a/run-command.c b/run-command.c index 28202a81d83d8b..e70a8a387b9042 100644 --- a/run-command.c +++ b/run-command.c @@ -545,6 +545,7 @@ static void atfork_parent(struct atfork_state *as) "restoring signal mask"); #endif } + #endif /* GIT_WINDOWS_NATIVE */ static inline void set_cloexec(int fd) @@ -831,6 +832,17 @@ int start_command(struct child_process *cmd) child_close(cmd->out); } + if (cmd->close_fd_above_stderr) { + long max_fd = sysconf(_SC_OPEN_MAX); + int fd; + if (max_fd < 0 || max_fd > 4096) + max_fd = 4096; + for (fd = 3; fd < max_fd; fd++) { + if (fd != child_notifier) + close(fd); + } + } + if (cmd->dir && chdir(cmd->dir)) child_die(CHILD_ERR_CHDIR); diff --git a/run-command.h b/run-command.h index 8ca496d7bdebfd..c2fad4f0f8380c 100644 --- a/run-command.h +++ b/run-command.h @@ -143,6 +143,15 @@ struct child_process { unsigned stdout_to_stderr:1; unsigned clean_on_exit:1; unsigned wait_after_clean:1; + + /** + * Close file descriptors 3 and above in the child after forking + * but before exec. This prevents the child from inheriting + * pipe endpoints or other descriptors from the parent + * environment (e.g., the test harness). + */ + unsigned close_fd_above_stderr:1; + void (*clean_on_exit_handler)(struct child_process *process); }; diff --git a/t/t1500-rev-parse.sh b/t/t1500-rev-parse.sh index 98c5a772bdad8d..38067d95f7f12b 100755 --- a/t/t1500-rev-parse.sh +++ b/t/t1500-rev-parse.sh @@ -337,6 +337,31 @@ test_expect_success 'rev-parse --bisect includes bad, excludes good' ' test_cmp expect actual ' +test_expect_success 'rev-parse --bisect works with alternate terms' ' + test_commit_bulk 6 && + + git bisect start --term-old=known --term-new=curious && + + git update-ref refs/bisect/curious-1 HEAD~1 && + git update-ref refs/bisect/bad HEAD~2 && + git update-ref refs/bisect/curious-3 HEAD~3 && + git update-ref refs/bisect/known-3 HEAD~3 && + git update-ref refs/bisect/curious-4 HEAD~4 && + git update-ref refs/bisect/good HEAD~4 && + + # Note: refs/bisect/bad and refs/bisect/goood should be ignored because this + # is a bisect with custom terms (known/curious) + cat >expect <<-EOF && + refs/bisect/curious-1 + refs/bisect/curious-3 + refs/bisect/curious-4 + ^refs/bisect/known-3 + EOF + + git rev-parse --symbolic-full-name --bisect >actual && + test_cmp expect actual +' + test_expect_success '--short= truncates to the actual hash length' ' git rev-parse HEAD >expect && git rev-parse --short=100 HEAD >actual && diff --git a/t/t4203-mailmap.sh b/t/t4203-mailmap.sh index 74b7ddccb26d59..249548eb9bc159 100755 --- a/t/t4203-mailmap.sh +++ b/t/t4203-mailmap.sh @@ -1133,6 +1133,111 @@ test_expect_success 'git cat-file --batch-command returns correct size with --us test_cmp expect actual ' +test_expect_success 'git cat-file --batch-command mailmap yes enables mailmap mid-stream' ' + test_when_finished "rm .mailmap" && + cat >.mailmap <<-\EOF && + C O Mitter Orig + EOF + commit_sha=$(git rev-parse HEAD) && + git cat-file commit HEAD >commit_no_mailmap.out && + git cat-file --use-mailmap commit HEAD >commit_mailmap.out && + size_no_mailmap=$(wc -c actual && + echo $commit_sha commit $size_no_mailmap >expect && + echo $commit_sha commit $size_mailmap >>expect && + test_cmp expect actual +' + +test_expect_success 'git cat-file --batch-command mailmap no disables mailmap mid-stream' ' + test_when_finished "rm .mailmap" && + cat >.mailmap <<-\EOF && + C O Mitter Orig + EOF + commit_sha=$(git rev-parse HEAD) && + git cat-file commit HEAD >commit_no_mailmap.out && + git cat-file --use-mailmap commit HEAD >commit_mailmap.out && + size_no_mailmap=$(wc -c actual && + echo $commit_sha commit $size_mailmap >expect && + echo $commit_sha commit $size_no_mailmap >>expect && + test_cmp expect actual +' + +test_expect_success 'git cat-file --batch-command mailmap works in --buffer mode' ' + test_when_finished "rm .mailmap" && + cat >.mailmap <<-\EOF && + C O Mitter Orig + EOF + commit_sha=$(git rev-parse HEAD) && + git cat-file commit HEAD >commit_no_mailmap.out && + git cat-file --use-mailmap commit HEAD >commit_mailmap.out && + size_no_mailmap=$(wc -c actual && + echo $commit_sha commit $size_mailmap >expect && + echo $commit_sha commit $size_no_mailmap >>expect && + test_cmp expect actual +' + +test_expect_success 'git cat-file --batch-command mailmap no overrides startup --mailmap' ' + test_when_finished "rm .mailmap" && + cat >.mailmap <<-\EOF && + C O Mitter Orig + EOF + commit_sha=$(git rev-parse HEAD) && + git cat-file --use-mailmap commit HEAD >commit_mailmap.out && + size_mailmap=$(wc -c commit_no_mailmap.out && + size_no_mailmap=$(wc -c actual && + echo $commit_sha commit $size_mailmap >expect && + echo $commit_sha commit $size_no_mailmap >>expect && + test_cmp expect actual +' + +test_expect_success 'git cat-file --batch-command mailmap yes overrides startup --no-mailmap' ' + test_when_finished "rm .mailmap" && + cat >.mailmap <<-\EOF && + C O Mitter Orig + EOF + commit_sha=$(git rev-parse HEAD) && + git cat-file commit HEAD >commit_no_mailmap.out && + size_no_mailmap=$(wc -c commit_mailmap.out && + size_mailmap=$(wc -c actual && + echo $commit_sha commit $size_no_mailmap >expect && + echo $commit_sha commit $size_mailmap >>expect && + test_cmp expect actual +' + +test_expect_success 'git cat-file --batch-command mailmap accepts true/false' ' + test_when_finished "rm .mailmap" && + cat >.mailmap <<-\EOF && + C O Mitter Orig + EOF + commit_sha=$(git rev-parse HEAD) && + git cat-file commit HEAD >commit_no_mailmap.out && + size_no_mailmap=$(wc -c commit_mailmap.out && + size_mailmap=$(wc -c actual && + echo $commit_sha commit $size_mailmap >expect && + echo $commit_sha commit $size_no_mailmap >>expect && + test_cmp expect actual +' + +test_expect_success 'git cat-file --batch-command mailmap rejects invalid boolean' ' + echo "mailmap maybe" >in && + test_must_fail git cat-file --batch-command err && + test_grep "mailmap: invalid boolean .*maybe" err +' + test_expect_success 'git cat-file --mailmap works with different author and committer' ' test_when_finished "rm .mailmap" && cat >.mailmap <<-\EOF && diff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh index 28d0779a8c599e..1612f05f1b39ce 100755 --- a/t/t4215-log-skewed-merges.sh +++ b/t/t4215-log-skewed-merges.sh @@ -370,4 +370,148 @@ test_expect_success 'log --graph with multiple tips' ' EOF ' +test_expect_success 'log --graph --graph-lane-limit=2 limited to two lanes' ' + check_graph --graph-lane-limit=2 M_7 <<-\EOF + *-. 7_M4 + |\ \ + | | * 7_G + | | * 7_F + | * ~ 7_E + | * ~ 7_D + * | ~ 7_C + | |/ + |/| + * | 7_B + |/ + * 7_A + EOF +' + +test_expect_success 'log --graph --graph-lane-limit=1 truncate mid octopus merge' ' + check_graph --graph-lane-limit=1 M_7 <<-\EOF + *-~ 7_M4 + |\~ + | ~ 7_G + | ~ 7_F + | * 7_E + | * 7_D + * ~ 7_C + | ~ + |/~ + * ~ 7_B + |/ + * 7_A + EOF +' + +test_expect_success 'log --graph --graph-lane-limit=3 limited to three lanes' ' + check_graph --graph-lane-limit=3 M_1 M_3 M_5 M_7 <<-\EOF + * 7_M1 + |\ + | | * 7_M2 + | | |\ + | | | * 7_H + | | | ~ 7_M3 + | | | ~ 7_J + | | | ~ 7_I + | | | ~ 7_M4 + | |_|_~ + |/| | ~ + | | |_~ + | |/| ~ + | | | ~ + | | |/~ + | | * ~ 7_G + | | | ~ + | | |/~ + | | * ~ 7_F + | * | ~ 7_E + | | |/~ + | |/| ~ + | * | ~ 7_D + | | |/ + | |/| + * | | 7_C + | |/ + |/| + * | 7_B + |/ + * 7_A + EOF +' + +test_expect_success 'log --graph --graph-lane-limit=6 check if it only shows first of 3 parent merge' ' + check_graph --graph-lane-limit=6 M_1 M_3 M_5 M_7 <<-\EOF + * 7_M1 + |\ + | | * 7_M2 + | | |\ + | | | * 7_H + | | | | * 7_M3 + | | | | |\ + | | | | | * 7_J + | | | | * | 7_I + | | | | | | * 7_M4 + | |_|_|_|_|/~ + |/| | | | |/~ + | | |_|_|/| ~ + | |/| | | |/ + | | | |_|/| + | | |/| | | + | | * | | | 7_G + | | | |_|/ + | | |/| | + | | * | | 7_F + | * | | | 7_E + | | |/ / + | |/| | + | * | | 7_D + | | |/ + | |/| + * | | 7_C + | |/ + |/| + * | 7_B + |/ + * 7_A + EOF +' + +test_expect_success 'log --graph --graph-lane-limit=7 check if it shows all 3 parent merge' ' + check_graph --graph-lane-limit=7 M_1 M_3 M_5 M_7 <<-\EOF + * 7_M1 + |\ + | | * 7_M2 + | | |\ + | | | * 7_H + | | | | * 7_M3 + | | | | |\ + | | | | | * 7_J + | | | | * | 7_I + | | | | | | * 7_M4 + | |_|_|_|_|/|\ + |/| | | | |/ / + | | |_|_|/| / + | |/| | | |/ + | | | |_|/| + | | |/| | | + | | * | | | 7_G + | | | |_|/ + | | |/| | + | | * | | 7_F + | * | | | 7_E + | | |/ / + | |/| | + | * | | 7_D + | | |/ + | |/| + * | | 7_C + | |/ + |/| + * | 7_B + |/ + * 7_A + EOF +' + test_done diff --git a/t/t5604-clone-reference.sh b/t/t5604-clone-reference.sh index 470bfb610ce075..c232ab8c15a59d 100755 --- a/t/t5604-clone-reference.sh +++ b/t/t5604-clone-reference.sh @@ -360,4 +360,27 @@ test_expect_success SYMLINKS 'clone repo with symlinked objects directory' ' grep "is a symlink, refusing to clone with --local" err ' +test_expect_success 'dissociate from repo with commit graph' ' + git init orig && + # We are trying to make sure the dissociated repo can + # find the tree of the tip commit, so the test could still + # serve its purpose with an empty tree. But having actual + # content future-proofs us against any kind of internal + # empty-tree optimizations. + echo content >orig/file && + git -C orig add . && + git -C orig commit -m foo && + + # We will use graph.git as our "local" source to dissociate + # from. + git clone --bare orig graph.git && + git -C graph.git commit-graph write --reachable && + + # And then finally clone orig, using graph.git to get our objects. This + # must be non-bare so that we perform the checkout step, which will + # need to access the tree of HEAD, which we will have originally loaded + # via the commit graph. + git clone --no-local --reference graph.git --dissociate orig clone +' + test_done diff --git a/t/t6030-bisect-porcelain.sh b/t/t6030-bisect-porcelain.sh index 1ba9ca219e5da9..081116220a4560 100755 --- a/t/t6030-bisect-porcelain.sh +++ b/t/t6030-bisect-porcelain.sh @@ -258,7 +258,7 @@ test_expect_success 'bisect skip: successful result' ' git bisect start $HASH4 $HASH1 && git bisect skip && git bisect bad > my_bisect_log.txt && - grep "$HASH2 is the first bad commit" my_bisect_log.txt + grep "$HASH2 is the first '\''bad'\'' commit" my_bisect_log.txt ' # $HASH1 is good, $HASH4 is bad, we skip $HASH3 and $HASH2 @@ -269,7 +269,7 @@ test_expect_success 'bisect skip: cannot tell between 3 commits' ' git bisect start $HASH4 $HASH1 && git bisect skip && test_expect_code 2 git bisect skip >my_bisect_log.txt && - grep "first bad commit could be any of" my_bisect_log.txt && + grep "first '\''bad'\'' commit could be any of" my_bisect_log.txt && ! grep $HASH1 my_bisect_log.txt && grep $HASH2 my_bisect_log.txt && grep $HASH3 my_bisect_log.txt && @@ -285,7 +285,7 @@ test_expect_success 'bisect skip: cannot tell between 2 commits' ' git bisect start $HASH4 $HASH1 && git bisect skip && test_expect_code 2 git bisect good >my_bisect_log.txt && - grep "first bad commit could be any of" my_bisect_log.txt && + grep "first '\''bad'\'' commit could be any of" my_bisect_log.txt && ! grep $HASH1 my_bisect_log.txt && ! grep $HASH2 my_bisect_log.txt && grep $HASH3 my_bisect_log.txt && @@ -304,7 +304,7 @@ test_expect_success 'bisect skip: with commit both bad and skipped' ' git bisect good $HASH1 && git bisect skip && test_expect_code 2 git bisect good >my_bisect_log.txt && - grep "first bad commit could be any of" my_bisect_log.txt && + grep "first '\''bad'\'' commit could be any of" my_bisect_log.txt && ! grep $HASH1 my_bisect_log.txt && ! grep $HASH2 my_bisect_log.txt && grep $HASH3 my_bisect_log.txt && @@ -348,8 +348,8 @@ test_expect_success 'git bisect run: args, stdout and stderr with no arguments' test_bisect_run_args <<-'EOF_ARGS' 6<<-EOF_OUT 7<<-'EOF_ERR' EOF_ARGS running './run.sh' - $HASH4 is the first bad commit - bisect found first bad commit + $HASH4 is the first 'bad' commit + bisect found first 'bad' commit EOF_OUT EOF_ERR " @@ -359,8 +359,8 @@ test_expect_success 'git bisect run: args, stdout and stderr: "--" argument' " <--> EOF_ARGS running './run.sh' '--' - $HASH4 is the first bad commit - bisect found first bad commit + $HASH4 is the first 'bad' commit + bisect found first 'bad' commit EOF_OUT EOF_ERR " @@ -373,8 +373,8 @@ test_expect_success 'git bisect run: args, stdout and stderr: "--log foo --no-lo EOF_ARGS running './run.sh' '--log' 'foo' '--no-log' 'bar' - $HASH4 is the first bad commit - bisect found first bad commit + $HASH4 is the first 'bad' commit + bisect found first 'bad' commit EOF_OUT EOF_ERR " @@ -384,8 +384,8 @@ test_expect_success 'git bisect run: args, stdout and stderr: "--bisect-start" a <--bisect-start> EOF_ARGS running './run.sh' '--bisect-start' - $HASH4 is the first bad commit - bisect found first bad commit + $HASH4 is the first 'bad' commit + bisect found first 'bad' commit EOF_OUT EOF_ERR " @@ -418,7 +418,7 @@ test_expect_success 'git bisect run: unable to verify on good' " fi EOF cat <<-'EOF' >expect && - unable to verify './fail.sh' on good revision + unable to verify './fail.sh' on 'good' revision EOF test_when_finished 'git bisect reset' && git bisect start && @@ -439,7 +439,7 @@ test_expect_success '"git bisect run" simple case' ' git bisect good $HASH1 && git bisect bad $HASH4 && git bisect run ./test_script.sh >my_bisect_log.txt && - grep "$HASH3 is the first bad commit" my_bisect_log.txt && + grep "$HASH3 is the first '\''bad'\'' commit" my_bisect_log.txt && git bisect reset ' @@ -461,7 +461,7 @@ test_expect_success '"git bisect run" with more complex "git bisect start"' ' EOF git bisect start $HASH4 $HASH1 && git bisect run ./test_script.sh >my_bisect_log.txt && - grep "$HASH4 is the first bad commit" my_bisect_log.txt && + grep "$HASH4 is the first '\''bad'\'' commit" my_bisect_log.txt && git bisect reset ' @@ -474,7 +474,7 @@ test_expect_success 'bisect run accepts exit code 126 as bad' ' git bisect good $HASH1 && git bisect bad $HASH4 && git bisect run ./test_script.sh >my_bisect_log.txt && - grep "$HASH3 is the first bad commit" my_bisect_log.txt + grep "$HASH3 is the first '\''bad'\'' commit" my_bisect_log.txt ' test_expect_success POSIXPERM 'bisect run fails with non-executable test script' ' @@ -485,7 +485,7 @@ test_expect_success POSIXPERM 'bisect run fails with non-executable test script' git bisect good $HASH1 && git bisect bad $HASH4 && test_must_fail git bisect run ./not-executable.sh >my_bisect_log.txt && - ! grep "is the first bad commit" my_bisect_log.txt + ! grep "is the first '\''bad'\'' commit" my_bisect_log.txt ' test_expect_success 'bisect run accepts exit code 127 as bad' ' @@ -497,7 +497,7 @@ test_expect_success 'bisect run accepts exit code 127 as bad' ' git bisect good $HASH1 && git bisect bad $HASH4 && git bisect run ./test_script.sh >my_bisect_log.txt && - grep "$HASH3 is the first bad commit" my_bisect_log.txt + grep "$HASH3 is the first '\''bad'\'' commit" my_bisect_log.txt ' test_expect_success 'bisect run fails with missing test script' ' @@ -507,7 +507,7 @@ test_expect_success 'bisect run fails with missing test script' ' git bisect good $HASH1 && git bisect bad $HASH4 && test_must_fail git bisect run ./does-not-exist.sh >my_bisect_log.txt && - ! grep "is the first bad commit" my_bisect_log.txt + ! grep "is the first '\''bad'\'' commit" my_bisect_log.txt ' # $HASH1 is good, $HASH5 is bad, we skip $HASH3 @@ -520,14 +520,14 @@ test_expect_success 'bisect skip: add line and then a new test' ' git bisect start $HASH5 $HASH1 && git bisect skip && git bisect good > my_bisect_log.txt && - grep "$HASH5 is the first bad commit" my_bisect_log.txt && + grep "$HASH5 is the first '\''bad'\'' commit" my_bisect_log.txt && git bisect log > log_to_replay.txt && git bisect reset ' test_expect_success 'bisect skip and bisect replay' ' git bisect replay log_to_replay.txt > my_bisect_log.txt && - grep "$HASH5 is the first bad commit" my_bisect_log.txt && + grep "$HASH5 is the first '\''bad'\'' commit" my_bisect_log.txt && git bisect reset ' @@ -541,7 +541,7 @@ test_expect_success 'bisect run & skip: cannot tell between 2' ' EOF git bisect start $HASH6 $HASH1 && test_expect_code 2 git bisect run ./test_script.sh >my_bisect_log.txt && - grep "first bad commit could be any of" my_bisect_log.txt && + grep "first '\''bad'\'' commit could be any of" my_bisect_log.txt && ! grep $HASH3 my_bisect_log.txt && ! grep $HASH6 my_bisect_log.txt && grep $HASH4 my_bisect_log.txt && @@ -560,7 +560,7 @@ test_expect_success 'bisect run & skip: find first bad' ' EOF git bisect start $HASH7 $HASH1 && git bisect run ./test_script.sh >my_bisect_log.txt && - grep "$HASH6 is the first bad commit" my_bisect_log.txt + grep "$HASH6 is the first '\''bad'\'' commit" my_bisect_log.txt ' test_expect_success 'bisect skip only one range' ' @@ -569,7 +569,7 @@ test_expect_success 'bisect skip only one range' ' git bisect skip $HASH1..$HASH5 && test "$HASH6" = "$(git rev-parse --verify HEAD)" && test_must_fail git bisect bad > my_bisect_log.txt && - grep "first bad commit could be any of" my_bisect_log.txt + grep "first '\''bad'\'' commit could be any of" my_bisect_log.txt ' test_expect_success 'bisect skip many ranges' ' @@ -578,7 +578,7 @@ test_expect_success 'bisect skip many ranges' ' git bisect skip $HASH2 $HASH2.. ..$HASH5 && test "$HASH6" = "$(git rev-parse --verify HEAD)" && test_must_fail git bisect bad > my_bisect_log.txt && - grep "first bad commit could be any of" my_bisect_log.txt + grep "first '\''bad'\'' commit could be any of" my_bisect_log.txt ' test_expect_success 'bisect starting with a detached HEAD' ' @@ -594,7 +594,7 @@ test_expect_success 'bisect starting with a detached HEAD' ' test_expect_success 'bisect errors out if bad and good are mistaken' ' git bisect reset && test_must_fail git bisect start $HASH2 $HASH4 2> rev_list_error && - test_grep "mistook good and bad" rev_list_error && + test_grep "mistook '\''good'\'' and '\''bad'\''" rev_list_error && git bisect reset ' @@ -610,7 +610,7 @@ test_expect_success 'bisect does not create a "bisect" branch' ' rev_hash6=$(git rev-parse --verify HEAD) && test "$rev_hash6" = "$HASH6" && git bisect good > my_bisect_log.txt && - grep "$HASH7 is the first bad commit" my_bisect_log.txt && + grep "$HASH7 is the first '\''bad'\'' commit" my_bisect_log.txt && git bisect reset && rev_hash6=$(git rev-parse --verify bisect) && test "$rev_hash6" = "$HASH6" && @@ -703,7 +703,7 @@ test_expect_success '"git bisect run --first-parent" simple case' ' git bisect good $HASH4 && git bisect bad $B_HASH && git bisect run ./test_script.sh >my_bisect_log.txt && - grep "$B_HASH is the first bad commit" my_bisect_log.txt && + grep "$B_HASH is the first '\''bad'\'' commit" my_bisect_log.txt && git bisect reset && test_path_is_missing .git/BISECT_FIRST_PARENT ' @@ -777,7 +777,7 @@ test_expect_success 'restricting bisection on one dir' ' para1=$(git rev-parse --verify HEAD) && test "$para1" = "$PARA_HASH1" && git bisect bad > my_bisect_log.txt && - grep "$PARA_HASH1 is the first bad commit" my_bisect_log.txt + grep "$PARA_HASH1 is the first '\''bad'\'' commit" my_bisect_log.txt ' test_expect_success 'restricting bisection on one dir and a file' ' @@ -795,7 +795,7 @@ test_expect_success 'restricting bisection on one dir and a file' ' para1=$(git rev-parse --verify HEAD) && test "$para1" = "$PARA_HASH1" && git bisect good > my_bisect_log.txt && - grep "$PARA_HASH4 is the first bad commit" my_bisect_log.txt + grep "$PARA_HASH4 is the first '\''bad'\'' commit" my_bisect_log.txt ' test_expect_success 'skipping away from skipped commit' ' @@ -826,7 +826,7 @@ test_expect_success 'test bisection on bare repo - --no-checkout specified' ' "test \$(git rev-list BISECT_HEAD ^$HASH2 --max-count=1 | wc -l) = 0" \ >../nocheckout.log ) && - grep "$HASH3 is the first bad commit" nocheckout.log + grep "$HASH3 is the first '\''bad'\'' commit" nocheckout.log ' @@ -841,7 +841,7 @@ test_expect_success 'test bisection on bare repo - --no-checkout defaulted' ' "test \$(git rev-list BISECT_HEAD ^$HASH2 --max-count=1 | wc -l) = 0" \ >../defaulted.log ) && - grep "$HASH3 is the first bad commit" defaulted.log + grep "$HASH3 is the first '\''bad'\'' commit" defaulted.log ' # @@ -969,7 +969,7 @@ cat > expected.bisect-log < into . git bisect good $HASH3 -# first bad commit: [$HASH4] Add <4: Ciao for now> into . +# first 'bad' commit: [$HASH4] Add <4: Ciao for now> into . EOF test_expect_success 'bisect log: successful result' ' @@ -988,8 +988,8 @@ git bisect start '$HASH4' '$HASH2' # skip: [$HASH3] Add <3: Another new day for git> into . git bisect skip $HASH3 # only skipped commits left to test -# possible first bad commit: [$HASH4] Add <4: Ciao for now> into . -# possible first bad commit: [$HASH3] Add <3: Another new day for git> into . +# possible first 'bad' commit: [$HASH4] Add <4: Ciao for now> into . +# possible first 'bad' commit: [$HASH3] Add <3: Another new day for git> into . EOF test_expect_success 'bisect log: only skip commits left' ' @@ -1031,21 +1031,21 @@ test_expect_success 'bisect start with one new and old' ' git bisect new $HASH4 && git bisect new && git bisect new >bisect_result && - grep "$HASH2 is the first new commit" bisect_result && + grep "$HASH2 is the first '\''new'\'' commit" bisect_result && git bisect log >log_to_replay.txt && git bisect reset ' test_expect_success 'bisect replay with old and new' ' git bisect replay log_to_replay.txt >bisect_result && - grep "$HASH2 is the first new commit" bisect_result && + grep "$HASH2 is the first '\''new'\'' commit" bisect_result && git bisect reset ' test_expect_success 'bisect replay with CRLF log' ' append_cr log_to_replay_crlf.txt && git bisect replay log_to_replay_crlf.txt >bisect_result_crlf && - grep "$HASH2 is the first new commit" bisect_result_crlf && + grep "$HASH2 is the first '\''new'\'' commit" bisect_result_crlf && git bisect reset ' @@ -1077,12 +1077,14 @@ test_expect_success 'bisect terms shows good/bad after start' ' test_expect_success 'bisect start with one term1 and term2' ' git bisect reset && - git bisect start --term-old term2 --term-new term1 && - git bisect term2 $HASH1 && + git bisect start --term-old term2 --term-new term1 >bisect_result && + test_grep "status: waiting for both '\''term2'\'' and '\''term1'\'' commits" bisect_result && + git bisect term2 $HASH1 >bisect_result && + test_grep "status: waiting for '\''term1'\'' commit, 1 '\''term2'\'' commit known" bisect_result && git bisect term1 $HASH4 && git bisect term1 && git bisect term1 >bisect_result && - grep "$HASH2 is the first term1 commit" bisect_result && + test_grep "$HASH2 is the first '\''term1'\'' commit" bisect_result && git bisect log >log_to_replay.txt && git bisect reset ' @@ -1099,7 +1101,17 @@ test_expect_success 'bogus command does not start bisect' ' test_expect_success 'bisect replay with term1 and term2' ' git bisect replay log_to_replay.txt >bisect_result && - grep "$HASH2 is the first term1 commit" bisect_result && + grep "$HASH2 is the first '\''term1'\'' commit" bisect_result && + git bisect reset +' + +test_expect_success 'bisect run term1 term2' ' + git bisect reset && + git bisect start --term-new term1 --term-old term2 $HASH4 $HASH1 && + git bisect term1 && + git bisect run false >bisect_result && + test_grep "bisect found first '\''term1'\'' commit" bisect_result && + git bisect log >log_to_replay.txt && git bisect reset ' @@ -1108,7 +1120,7 @@ test_expect_success 'bisect start term1 term2' ' git bisect start --term-new term1 --term-old term2 $HASH4 $HASH1 && git bisect term1 && git bisect term1 >bisect_result && - grep "$HASH2 is the first term1 commit" bisect_result && + grep "$HASH2 is the first '\''term1'\'' commit" bisect_result && git bisect log >log_to_replay.txt && git bisect reset ' @@ -1142,8 +1154,8 @@ test_expect_success 'bisect start --term-* does store terms' ' git bisect start --term-bad=one --term-good=two && git bisect terms >actual && cat <<-EOF >expected && - Your current terms are two for the old state - and one for the new state. + Your current terms are '\''two'\'' for the old state + and '\''one'\'' for the new state. EOF test_cmp expected actual && git bisect terms --term-bad >actual && @@ -1200,7 +1212,7 @@ test_expect_success 'bisect handles annotated tags' ' git bisect good tag-one && git bisect bad tag-two >output && bad=$(git rev-parse --verify tag-two^{commit}) && - grep "$bad is the first bad commit" output + grep "$bad is the first '\''bad'\'' commit" output ' test_expect_success 'bisect run fails with exit code equals or greater than 128' ' @@ -1224,29 +1236,29 @@ test_expect_success 'bisect visualize with a filename with dash and space' ' test_expect_success 'bisect state output with multiple good commits' ' git bisect reset && git bisect start >output && - grep "waiting for both good and bad commits" output && + grep "waiting for both '\''good'\'' and '\''bad'\'' commits" output && git bisect log >output && - grep "waiting for both good and bad commits" output && + grep "waiting for both '\''good'\'' and '\''bad'\'' commits" output && git bisect good "$HASH1" >output && - grep "waiting for bad commit, 1 good commit known" output && + grep "waiting for '\''bad'\'' commit, 1 '\''good'\'' commit known" output && git bisect log >output && - grep "waiting for bad commit, 1 good commit known" output && + grep "waiting for '\''bad'\'' commit, 1 '\''good'\'' commit known" output && git bisect good "$HASH2" >output && - grep "waiting for bad commit, 2 good commits known" output && + grep "waiting for '\''bad'\'' commit, 2 '\''good'\'' commits known" output && git bisect log >output && - grep "waiting for bad commit, 2 good commits known" output + grep "waiting for '\''bad'\'' commit, 2 '\''good'\'' commits known" output ' test_expect_success 'bisect state output with bad commit' ' git bisect reset && git bisect start >output && - grep "waiting for both good and bad commits" output && + grep "waiting for both '\''good'\'' and '\''bad'\'' commits" output && git bisect log >output && - grep "waiting for both good and bad commits" output && + grep "waiting for both '\''good'\'' and '\''bad'\'' commits" output && git bisect bad "$HASH4" >output && - grep -F "waiting for good commit(s), bad commit known" output && + grep -F "waiting for '\''good'\'' commit(s), '\''bad'\'' commit known" output && git bisect log >output && - grep -F "waiting for good commit(s), bad commit known" output + grep -F "waiting for '\''good'\'' commit(s), '\''bad'\'' commit known" output ' test_expect_success 'verify correct error message' ' diff --git a/t/t6600-test-reach.sh b/t/t6600-test-reach.sh index 51c23b76833126..b5b314e57068f9 100755 --- a/t/t6600-test-reach.sh +++ b/t/t6600-test-reach.sh @@ -612,6 +612,51 @@ test_expect_success 'for-each-ref merged:none' ' --format="%(refname)" --stdin ' +test_expect_success 'for-each-ref merged:duplicate, all reachable' ' + git branch dup-a commit-3-3 && + git branch dup-b commit-3-3 && + cat >input <<-\EOF && + refs/heads/commit-1-1 + refs/heads/dup-a + refs/heads/dup-b + EOF + cat >expect <<-\EOF && + refs/heads/commit-1-1 + refs/heads/dup-a + refs/heads/dup-b + EOF + run_all_modes git for-each-ref --merged=commit-5-5 \ + --format="%(refname)" --stdin +' + +test_expect_success 'for-each-ref merged:duplicate, none reachable' ' + cat >input <<-\EOF && + refs/heads/dup-a + refs/heads/dup-b + refs/heads/commit-9-9 + EOF + >expect && + run_all_modes git for-each-ref --merged=commit-2-2 \ + --format="%(refname)" --stdin +' + +test_expect_success 'for-each-ref merged:duplicate at min generation' ' + git branch dup-c commit-1-1 && + git branch dup-d commit-1-1 && + cat >input <<-\EOF && + refs/heads/dup-c + refs/heads/dup-d + refs/heads/commit-5-5 + EOF + cat >expect <<-\EOF && + refs/heads/commit-5-5 + refs/heads/dup-c + refs/heads/dup-d + EOF + run_all_modes git for-each-ref --merged=commit-5-5 \ + --format="%(refname)" --stdin +' + # For get_branch_base_for_tip, we only care about # first-parent history. Here is the test graph with # second parents removed: diff --git a/t/t7527-builtin-fsmonitor.sh b/t/t7527-builtin-fsmonitor.sh index e7b40654694c9e..b63c162f9bac3f 100755 --- a/t/t7527-builtin-fsmonitor.sh +++ b/t/t7527-builtin-fsmonitor.sh @@ -10,9 +10,57 @@ then test_done fi +# Verify that the filesystem delivers events to the daemon. +# On some configurations (e.g., overlayfs with older kernels), +# inotify watches succeed but events are never delivered. The +# cookie wait will time out and the daemon logs a trace message. +# +# Use "timeout" (if available) to guard each step against hangs. +maybe_timeout () { + if type timeout >/dev/null 2>&1 + then + timeout "$@" + else + shift + "$@" + fi +} +verify_fsmonitor_works () { + git init test_fsmonitor_smoke || return 1 + + GIT_TRACE_FSMONITOR="$PWD/smoke.trace" && + export GIT_TRACE_FSMONITOR && + maybe_timeout 30 \ + git -C test_fsmonitor_smoke fsmonitor--daemon start \ + --start-timeout=10 + ret=$? + unset GIT_TRACE_FSMONITOR + if test $ret -ne 0 + then + rm -rf test_fsmonitor_smoke smoke.trace + return 1 + fi + + maybe_timeout 10 \ + test-tool -C test_fsmonitor_smoke fsmonitor-client query \ + --token 0 >/dev/null 2>&1 + maybe_timeout 5 \ + git -C test_fsmonitor_smoke fsmonitor--daemon stop 2>/dev/null + ! grep -q "cookie_wait timed out" "$PWD/smoke.trace" 2>/dev/null + ret=$? + rm -rf test_fsmonitor_smoke smoke.trace + return $ret +} + +if ! verify_fsmonitor_works +then + skip_all="filesystem does not deliver fsmonitor events (container/overlayfs?)" + test_done +fi + stop_daemon_delete_repo () { r=$1 && - test_might_fail git -C $r fsmonitor--daemon stop && + { maybe_timeout 30 git -C $r fsmonitor--daemon stop 2>/dev/null || :; } && rm -rf $1 } @@ -67,7 +115,7 @@ start_daemon () { export GIT_TEST_FSMONITOR_TOKEN fi && - git $r fsmonitor--daemon start && + git $r fsmonitor--daemon start --start-timeout=10 && git $r fsmonitor--daemon status ) } @@ -523,6 +571,28 @@ test_expect_success 'directory changes to a file' ' retry_grep "^event: dir1$" .git/trace ' +test_expect_success 'rapid nested directory creation' ' + test_when_finished "git fsmonitor--daemon stop; rm -rf rapid" && + + start_daemon --tf "$PWD/.git/trace" && + + # Rapidly create nested directories to exercise race conditions + # where directory watches may be added concurrently during + # event processing and recursive scanning. + for i in $(test_seq 1 20) + do + mkdir -p "rapid/nested/dir$i/subdir/deep" || return 1 + done && + + # Give the daemon time to process all events + sleep 1 && + + test-tool fsmonitor-client query --token 0 && + + # Verify daemon is still running (did not crash) + git fsmonitor--daemon status +' + # The next few test cases exercise the token-resync code. When filesystem # drops events (because of filesystem velocity or because the daemon isn't # polling fast enough), we need to discard the cached data (relative to the @@ -913,7 +983,10 @@ test_expect_success "submodule absorbgitdirs implicitly starts daemon" ' start_git_in_background () { git "$@" & git_pid=$! - git_pgid=$(ps -o pgid= -p $git_pid) + git_pgid=$(ps -o pgid= -p $git_pid 2>/dev/null || + awk '{print $5}' /proc/$git_pid/stat 2>/dev/null) && + git_pgid="${git_pgid## }" && + git_pgid="${git_pgid%% }" nr_tries_left=10 while true do @@ -924,15 +997,16 @@ start_git_in_background () { fi sleep 1 nr_tries_left=$(($nr_tries_left - 1)) - done >/dev/null 2>&1 & + done >/dev/null 2>&1 3>&- 4>&- 5>&- 6>&- 7>&- & watchdog_pid=$! wait $git_pid } stop_git () { - while kill -0 -- -$git_pgid + test -n "$git_pgid" || return 0 + while kill -0 -- -$git_pgid 2>/dev/null do - kill -- -$git_pgid + kill -- -$git_pgid 2>/dev/null sleep 1 done } @@ -947,7 +1021,7 @@ stop_watchdog () { test_expect_success !MINGW "submodule implicitly starts daemon by pull" ' test_atexit "stop_watchdog" && - test_when_finished "stop_git; rm -rf cloned super sub" && + test_when_finished "set +m; stop_git; rm -rf cloned super sub" && create_super super && create_sub sub && diff --git a/t/t9210-scalar.sh b/t/t9210-scalar.sh index 009437a5f3168f..f2a6df77ceeb01 100755 --- a/t/t9210-scalar.sh +++ b/t/t9210-scalar.sh @@ -152,6 +152,10 @@ test_expect_success 'set up repository to clone' ' ' test_expect_success 'scalar clone' ' + # index.skipHash (Scalar default) and GIT_TEST_SPLIT_INDEX are + # incompatible: the shared index gets a null OID and fails to + # load on re-read. + sane_unset GIT_TEST_SPLIT_INDEX && second=$(git rev-parse --verify second:second.t) && scalar clone "file://$(pwd)" cloned --single-branch && ( @@ -182,6 +186,7 @@ test_expect_success 'scalar clone' ' ' test_expect_success 'scalar clone --no-... opts' ' + sane_unset GIT_TEST_SPLIT_INDEX && # Note: redirect stderr always to avoid having a verbose test # run result in a difference in the --[no-]progress option. GIT_TRACE2_EVENT="$(pwd)/no-opt-trace" scalar clone \ @@ -307,6 +312,7 @@ test_expect_success '`scalar [...] ` errors out when dir is missing' ' SQ="'" test_expect_success UNZIP 'scalar diagnose' ' + sane_unset GIT_TEST_SPLIT_INDEX && scalar clone "file://$(pwd)" cloned --single-branch && git repack && echo "$(pwd)/.git/objects/" >>cloned/src/.git/objects/info/alternates && diff --git a/t/t9211-scalar-clone.sh b/t/t9211-scalar-clone.sh index bfbf22a4621843..2043f48a1acdf7 100755 --- a/t/t9211-scalar-clone.sh +++ b/t/t9211-scalar-clone.sh @@ -8,6 +8,11 @@ test_description='test the `scalar clone` subcommand' GIT_TEST_MAINT_SCHEDULER="crontab:test-tool crontab cron.txt,launchctl:true,schtasks:true" export GIT_TEST_MAINT_SCHEDULER +# index.skipHash (Scalar default) and GIT_TEST_SPLIT_INDEX are +# incompatible: the shared index gets a null OID and fails to +# load on re-read. Every test here uses scalar clone. +sane_unset GIT_TEST_SPLIT_INDEX + test_expect_success 'set up repository to clone' ' rm -rf .git && git init to-clone && diff --git a/tools/generate-configlist.sh b/tools/generate-configlist.sh index e28054f9e0e9ba..d1d2ba4bb7fef9 100755 --- a/tools/generate-configlist.sh +++ b/tools/generate-configlist.sh @@ -42,9 +42,12 @@ if test -n "$DEPFILE" then QUOTED_OUTPUT="$(printf '%s\n' "$OUTPUT" | sed 's,[&/\],\\&,g')" { + printf '%s' "$QUOTED_OUTPUT: " printf '%s\n' "$SOURCE_DIR"/Documentation/*config.adoc \ "$SOURCE_DIR"/Documentation/config/*.adoc | - sed -e 's/[# ]/\\&/g' -e "s/^/$QUOTED_OUTPUT: /" + sed -e 's/[# ]/\\&/g' | + tr '\n' ' ' + printf '\n' printf '%s:\n' "$SOURCE_DIR"/Documentation/*config.adoc \ "$SOURCE_DIR"/Documentation/config/*.adoc | sed -e 's/[# ]/\\&/g'