diff options
76 files changed, 1524 insertions, 277 deletions
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3428773b09..8922bd73bd 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -284,8 +284,7 @@ jobs: cc: clang pool: macos-13 - jobname: osx-gcc - cc: gcc - cc_package: gcc-13 + cc: gcc-13 pool: macos-13 - jobname: linux-gcc-default cc: gcc diff --git a/Documentation/RelNotes/2.39.4.txt b/Documentation/RelNotes/2.39.4.txt new file mode 100644 index 0000000000..7f54521fea --- /dev/null +++ b/Documentation/RelNotes/2.39.4.txt @@ -0,0 +1,79 @@ +Git v2.39.4 Release Notes +========================= + +This addresses the security issues CVE-2024-32002, CVE-2024-32004, +CVE-2024-32020 and CVE-2024-32021. + +This release also backports fixes necessary to let the CI builds pass +successfully. + +Fixes since v2.39.3 +------------------- + + * CVE-2024-32002: + + Recursive clones on case-insensitive filesystems that support symbolic + links are susceptible to case confusion that can be exploited to + execute just-cloned code during the clone operation. + + * CVE-2024-32004: + + Repositories can be configured to execute arbitrary code during local + clones. To address this, the ownership checks introduced in v2.30.3 + are now extended to cover cloning local repositories. + + * CVE-2024-32020: + + Local clones may end up hardlinking files into the target repository's + object database when source and target repository reside on the same + disk. If the source repository is owned by a different user, then + those hardlinked files may be rewritten at any point in time by the + untrusted user. + + * CVE-2024-32021: + + When cloning a local source repository that contains symlinks via the + filesystem, Git may create hardlinks to arbitrary user-readable files + on the same filesystem as the target repository in the objects/ + directory. + + * CVE-2024-32465: + + It is supposed to be safe to clone untrusted repositories, even those + unpacked from zip archives or tarballs originating from untrusted + sources, but Git can be tricked to run arbitrary code as part of the + clone. + + * Defense-in-depth: submodule: require the submodule path to contain + directories only. + + * Defense-in-depth: clone: when symbolic links collide with directories, keep + the latter. + + * Defense-in-depth: clone: prevent hooks from running during a clone. + + * Defense-in-depth: core.hooksPath: add some protection while cloning. + + * Defense-in-depth: fsck: warn about symlink pointing inside a gitdir. + + * Various fix-ups on HTTP tests. + + * Test update. + + * HTTP Header redaction code has been adjusted for a newer version of + cURL library that shows its traces differently from earlier + versions. + + * Fix was added to work around a regression in libcURL 8.7.0 (which has + already been fixed in their tip of the tree). + + * Replace macos-12 used at GitHub CI with macos-13. + + * ci(linux-asan/linux-ubsan): let's save some time + + * Tests with LSan from time to time seem to emit harmless message that makes + our tests unnecessarily flakey; we work it around by filtering the + uninteresting output. + + * Update GitHub Actions jobs to avoid warnings against using deprecated + version of Node.js. diff --git a/Documentation/RelNotes/2.39.5.txt b/Documentation/RelNotes/2.39.5.txt new file mode 100644 index 0000000000..97c0185de4 --- /dev/null +++ b/Documentation/RelNotes/2.39.5.txt @@ -0,0 +1,26 @@ +Git v2.39.5 Release Notes +========================= + +In preparing security fixes for four CVEs, we made overly aggressive +"defense in depth" changes that broke legitimate use cases like 'git +lfs' and 'git annex.' This release is to revert these misguided, if +well-intentioned, changes that were shipped in 2.39.4 and were not +direct security fixes. + +Jeff King (5): + send-email: drop FakeTerm hack + send-email: avoid creating more than one Term::ReadLine object + ci: drop mention of BREW_INSTALL_PACKAGES variable + ci: avoid bare "gcc" for osx-gcc job + ci: stop installing "gcc-13" for osx-gcc + +Johannes Schindelin (6): + hook: plug a new memory leak + init: use the correct path of the templates directory again + Revert "core.hooksPath: add some protection while cloning" + tests: verify that `clone -c core.hooksPath=/dev/null` works again + clone: drop the protections where hooks aren't run + Revert "Add a helper function to compare file contents" + +Junio C Hamano (1): + Revert "fsck: warn about symlink pointing inside a gitdir" diff --git a/Documentation/RelNotes/2.40.2.txt b/Documentation/RelNotes/2.40.2.txt new file mode 100644 index 0000000000..646a2cc3eb --- /dev/null +++ b/Documentation/RelNotes/2.40.2.txt @@ -0,0 +1,7 @@ +Git v2.40.2 Release Notes +========================= + +This release merges up the fix that appears in v2.39.4 to address +the security issues CVE-2024-32002, CVE-2024-32004, CVE-2024-32020, +CVE-2024-32021 and CVE-2024-32465; see the release notes for that +version for details. diff --git a/Documentation/RelNotes/2.40.3.txt b/Documentation/RelNotes/2.40.3.txt new file mode 100644 index 0000000000..6ca088ec86 --- /dev/null +++ b/Documentation/RelNotes/2.40.3.txt @@ -0,0 +1,26 @@ +Git v2.40.3 Release Notes +========================= + +In preparing security fixes for four CVEs, we made overly aggressive +"defense in depth" changes that broke legitimate use cases like 'git +lfs' and 'git annex.' This release is to revert these misguided, if +well-intentioned, changes that were shipped in 2.40.2 and were not +direct security fixes. + +Jeff King (5): + send-email: drop FakeTerm hack + send-email: avoid creating more than one Term::ReadLine object + ci: drop mention of BREW_INSTALL_PACKAGES variable + ci: avoid bare "gcc" for osx-gcc job + ci: stop installing "gcc-13" for osx-gcc + +Johannes Schindelin (6): + hook: plug a new memory leak + init: use the correct path of the templates directory again + Revert "core.hooksPath: add some protection while cloning" + tests: verify that `clone -c core.hooksPath=/dev/null` works again + clone: drop the protections where hooks aren't run + Revert "Add a helper function to compare file contents" + +Junio C Hamano (1): + Revert "fsck: warn about symlink pointing inside a gitdir" diff --git a/Documentation/RelNotes/2.41.1.txt b/Documentation/RelNotes/2.41.1.txt new file mode 100644 index 0000000000..9fb4c218b2 --- /dev/null +++ b/Documentation/RelNotes/2.41.1.txt @@ -0,0 +1,7 @@ +Git v2.41.1 Release Notes +========================= + +This release merges up the fix that appears in v2.39.4 and v2.40.2 +to address the security issues CVE-2024-32002, CVE-2024-32004, +CVE-2024-32020, CVE-2024-32021 and CVE-2024-32465; see the release +notes for these versions for details. diff --git a/Documentation/RelNotes/2.41.2.txt b/Documentation/RelNotes/2.41.2.txt new file mode 100644 index 0000000000..f94afde8c2 --- /dev/null +++ b/Documentation/RelNotes/2.41.2.txt @@ -0,0 +1,26 @@ +Git v2.41.2 Release Notes +========================= + +In preparing security fixes for four CVEs, we made overly aggressive +"defense in depth" changes that broke legitimate use cases like 'git +lfs' and 'git annex.' This release is to revert these misguided, if +well-intentioned, changes that were shipped in 2.41.1 and were not +direct security fixes. + +Jeff King (5): + send-email: drop FakeTerm hack + send-email: avoid creating more than one Term::ReadLine object + ci: drop mention of BREW_INSTALL_PACKAGES variable + ci: avoid bare "gcc" for osx-gcc job + ci: stop installing "gcc-13" for osx-gcc + +Johannes Schindelin (6): + hook: plug a new memory leak + init: use the correct path of the templates directory again + Revert "core.hooksPath: add some protection while cloning" + tests: verify that `clone -c core.hooksPath=/dev/null` works again + clone: drop the protections where hooks aren't run + Revert "Add a helper function to compare file contents" + +Junio C Hamano (1): + Revert "fsck: warn about symlink pointing inside a gitdir" diff --git a/Documentation/RelNotes/2.42.2.txt b/Documentation/RelNotes/2.42.2.txt new file mode 100644 index 0000000000..dbf761a01d --- /dev/null +++ b/Documentation/RelNotes/2.42.2.txt @@ -0,0 +1,7 @@ +Git v2.42.2 Release Notes +========================= + +This release merges up the fix that appears in v2.39.4, v2.40.2 +and v2.41.1 to address the security issues CVE-2024-32002, +CVE-2024-32004, CVE-2024-32020, CVE-2024-32021 and CVE-2024-32465; +see the release notes for these versions for details. diff --git a/Documentation/RelNotes/2.42.3.txt b/Documentation/RelNotes/2.42.3.txt new file mode 100644 index 0000000000..bfe3ba5629 --- /dev/null +++ b/Documentation/RelNotes/2.42.3.txt @@ -0,0 +1,26 @@ +Git v2.42.3 Release Notes +========================= + +In preparing security fixes for four CVEs, we made overly aggressive +"defense in depth" changes that broke legitimate use cases like 'git +lfs' and 'git annex.' This release is to revert these misguided, if +well-intentioned, changes that were shipped in 2.42.2 and were not +direct security fixes. + +Jeff King (5): + send-email: drop FakeTerm hack + send-email: avoid creating more than one Term::ReadLine object + ci: drop mention of BREW_INSTALL_PACKAGES variable + ci: avoid bare "gcc" for osx-gcc job + ci: stop installing "gcc-13" for osx-gcc + +Johannes Schindelin (6): + hook: plug a new memory leak + init: use the correct path of the templates directory again + Revert "core.hooksPath: add some protection while cloning" + tests: verify that `clone -c core.hooksPath=/dev/null` works again + clone: drop the protections where hooks aren't run + Revert "Add a helper function to compare file contents" + +Junio C Hamano (1): + Revert "fsck: warn about symlink pointing inside a gitdir" diff --git a/Documentation/RelNotes/2.43.4.txt b/Documentation/RelNotes/2.43.4.txt new file mode 100644 index 0000000000..0a842515ff --- /dev/null +++ b/Documentation/RelNotes/2.43.4.txt @@ -0,0 +1,7 @@ +Git v2.43.4 Release Notes +========================= + +This release merges up the fix that appears in v2.39.4, v2.40.2, +v2.41.1 and v2.42.2 to address the security issues CVE-2024-32002, +CVE-2024-32004, CVE-2024-32020, CVE-2024-32021 and CVE-2024-32465; +see the release notes for these versions for details. diff --git a/Documentation/RelNotes/2.43.5.txt b/Documentation/RelNotes/2.43.5.txt new file mode 100644 index 0000000000..236b234b06 --- /dev/null +++ b/Documentation/RelNotes/2.43.5.txt @@ -0,0 +1,26 @@ +Git v2.43.5 Release Notes +========================= + +In preparing security fixes for four CVEs, we made overly aggressive +"defense in depth" changes that broke legitimate use cases like 'git +lfs' and 'git annex.' This release is to revert these misguided, if +well-intentioned, changes that were shipped in 2.43.4 and were not +direct security fixes. + +Jeff King (5): + send-email: drop FakeTerm hack + send-email: avoid creating more than one Term::ReadLine object + ci: drop mention of BREW_INSTALL_PACKAGES variable + ci: avoid bare "gcc" for osx-gcc job + ci: stop installing "gcc-13" for osx-gcc + +Johannes Schindelin (6): + hook: plug a new memory leak + init: use the correct path of the templates directory again + Revert "core.hooksPath: add some protection while cloning" + tests: verify that `clone -c core.hooksPath=/dev/null` works again + clone: drop the protections where hooks aren't run + Revert "Add a helper function to compare file contents" + +Junio C Hamano (1): + Revert "fsck: warn about symlink pointing inside a gitdir" diff --git a/Documentation/RelNotes/2.44.1.txt b/Documentation/RelNotes/2.44.1.txt new file mode 100644 index 0000000000..b5135c3281 --- /dev/null +++ b/Documentation/RelNotes/2.44.1.txt @@ -0,0 +1,8 @@ +Git v2.44.1 Release Notes +========================= + +This release merges up the fix that appears in v2.39.4, v2.40.2, +v2.41.1, v2.42.2 and v2.43.4 to address the security issues +CVE-2024-32002, CVE-2024-32004, CVE-2024-32020, CVE-2024-32021 +and CVE-2024-32465; see the release notes for these versions +for details. diff --git a/Documentation/RelNotes/2.44.2.txt b/Documentation/RelNotes/2.44.2.txt new file mode 100644 index 0000000000..76700f0b73 --- /dev/null +++ b/Documentation/RelNotes/2.44.2.txt @@ -0,0 +1,26 @@ +Git v2.44.2 Release Notes +========================= + +In preparing security fixes for four CVEs, we made overly aggressive +"defense in depth" changes that broke legitimate use cases like 'git +lfs' and 'git annex.' This release is to revert these misguided, if +well-intentioned, changes that were shipped in 2.44.1 and were not +direct security fixes. + +Jeff King (5): + send-email: drop FakeTerm hack + send-email: avoid creating more than one Term::ReadLine object + ci: drop mention of BREW_INSTALL_PACKAGES variable + ci: avoid bare "gcc" for osx-gcc job + ci: stop installing "gcc-13" for osx-gcc + +Johannes Schindelin (6): + hook: plug a new memory leak + init: use the correct path of the templates directory again + Revert "core.hooksPath: add some protection while cloning" + tests: verify that `clone -c core.hooksPath=/dev/null` works again + clone: drop the protections where hooks aren't run + Revert "Add a helper function to compare file contents" + +Junio C Hamano (1): + Revert "fsck: warn about symlink pointing inside a gitdir" diff --git a/Documentation/RelNotes/2.45.1.txt b/Documentation/RelNotes/2.45.1.txt new file mode 100644 index 0000000000..3b0d60cfa3 --- /dev/null +++ b/Documentation/RelNotes/2.45.1.txt @@ -0,0 +1,8 @@ +Git v2.45.1 Release Notes +========================= + +This release merges up the fix that appears in v2.39.4, +v2.40.2, v2.41.1, v2.42.2, v2.43.4 and v2.44.1 to address the +security issues CVE-2024-32002, CVE-2024-32004, CVE-2024-32020, +CVE-2024-32021 and CVE-2024-32465; see the release notes for +these versions for details. diff --git a/Documentation/RelNotes/2.45.2.txt b/Documentation/RelNotes/2.45.2.txt new file mode 100644 index 0000000000..13429e6491 --- /dev/null +++ b/Documentation/RelNotes/2.45.2.txt @@ -0,0 +1,26 @@ +Git v2.45.2 Release Notes +========================= + +In preparing security fixes for four CVEs, we made overly aggressive +"defense in depth" changes that broke legitimate use cases like 'git +lfs' and 'git annex.' This release is to revert these misguided, if +well-intentioned, changes that were shipped in 2.45.1 and were not +direct security fixes. + +Jeff King (5): + send-email: drop FakeTerm hack + send-email: avoid creating more than one Term::ReadLine object + ci: drop mention of BREW_INSTALL_PACKAGES variable + ci: avoid bare "gcc" for osx-gcc job + ci: stop installing "gcc-13" for osx-gcc + +Johannes Schindelin (6): + hook: plug a new memory leak + init: use the correct path of the templates directory again + Revert "core.hooksPath: add some protection while cloning" + tests: verify that `clone -c core.hooksPath=/dev/null` works again + clone: drop the protections where hooks aren't run + Revert "Add a helper function to compare file contents" + +Junio C Hamano (1): + Revert "fsck: warn about symlink pointing inside a gitdir" diff --git a/Documentation/SubmittingPatches b/Documentation/SubmittingPatches index c647c7e1b4..625bdd0bb6 100644 --- a/Documentation/SubmittingPatches +++ b/Documentation/SubmittingPatches @@ -415,10 +415,12 @@ e-mail tools, so that they may comment on specific portions of your code. For this reason, each patch should be submitted "inline" in a separate message. -Multiple related patches should be grouped into their own e-mail -thread to help readers find all parts of the series. To that end, -send them as replies to either an additional "cover letter" message -(see below), the first patch, or the respective preceding patch. +All subsequent versions of a patch series and other related patches should be +grouped into their own e-mail thread to help readers find all parts of the +series. To that end, send them as replies to either an additional "cover +letter" message (see below), the first patch, or the respective preceding patch. +Here is a link:MyFirstContribution.html#v2-git-send-email[step-by-step guide] on +how to submit updated versions of a patch series. If your log message (including your name on the `Signed-off-by` trailer) is not writable in ASCII, make sure that @@ -531,9 +533,9 @@ patch, and "cc:" them when sending such a final version for inclusion. Some parts of the system have dedicated maintainers with their own repositories. -- `git-gui/` comes from git-gui project, maintained by Pratyush Yadav: +- `git-gui/` comes from git-gui project, maintained by Johannes Sixt: - https://github.com/prati0100/git-gui.git + https://github.com/j6t/git-gui - `gitk-git/` comes from Paul Mackerras's gitk project: @@ -548,6 +550,13 @@ repositories. Patches to these parts should be based on their trees. +- The "Git documentation translations" project, led by Jean-Noël + Avila, translates our documentation pages. Their work products are + maintained separately from this project, not as part of our tree: + + https://github.com/jnavila/git-manpages-l10n/ + + [[patch-flow]] == An ideal patch flow diff --git a/Documentation/git-for-each-repo.txt b/Documentation/git-for-each-repo.txt index 94bd19da26..abe3527aac 100644 --- a/Documentation/git-for-each-repo.txt +++ b/Documentation/git-for-each-repo.txt @@ -42,6 +42,15 @@ These config values are loaded from system, global, and local Git config, as available. If `git for-each-repo` is run in a directory that is not a Git repository, then only the system and global config is used. +--keep-going:: + Continue with the remaining repositories if the command failed + on a repository. The exit code will still indicate that the + overall operation was not successful. ++ +Note that the exact exit code of the failing command is not passed +through as the exit code of the `for-each-repo` command: If the command +failed in any of the specified repositories, the overall exit code will +be 1. SUBPROCESS BEHAVIOR ------------------- diff --git a/Documentation/git-gui.txt b/Documentation/git-gui.txt index e8f3ccb433..f5b02ef114 100644 --- a/Documentation/git-gui.txt +++ b/Documentation/git-gui.txt @@ -114,7 +114,7 @@ of end users. The official repository of the 'git gui' project can be found at: - https://github.com/prati0100/git-gui.git/ + https://github.com/j6t/git-gui GIT --- diff --git a/Documentation/git-upload-pack.txt b/Documentation/git-upload-pack.txt index 7ad60bc348..516d1639d9 100644 --- a/Documentation/git-upload-pack.txt +++ b/Documentation/git-upload-pack.txt @@ -55,6 +55,37 @@ ENVIRONMENT admins may need to configure some transports to allow this variable to be passed. See the discussion in linkgit:git[1]. +`GIT_NO_LAZY_FETCH`:: + When cloning or fetching from a partial repository (i.e., one + itself cloned with `--filter`), the server-side `upload-pack` + may need to fetch extra objects from its upstream in order to + complete the request. By default, `upload-pack` will refuse to + perform such a lazy fetch, because `git fetch` may run arbitrary + commands specified in configuration and hooks of the source + repository (and `upload-pack` tries to be safe to run even in + untrusted `.git` directories). ++ +This is implemented by having `upload-pack` internally set the +`GIT_NO_LAZY_FETCH` variable to `1`. If you want to override it +(because you are fetching from a partial clone, and you are sure +you trust it), you can explicitly set `GIT_NO_LAZY_FETCH` to +`0`. + +SECURITY +-------- + +Most Git commands should not be run in an untrusted `.git` directory +(see the section `SECURITY` in linkgit:git[1]). `upload-pack` tries to +avoid any dangerous configuration options or hooks from the repository +it's serving, making it safe to clone an untrusted directory and run +commands on the resulting clone. + +For an extra level of safety, you may be able to run `upload-pack` as an +alternate user. The details will be platform dependent, but on many +systems you can run: + + git clone --no-local --upload-pack='sudo -u nobody git-upload-pack' ... + SEE ALSO -------- linkgit:gitnamespaces[7] diff --git a/Documentation/git.txt b/Documentation/git.txt index 7a1b112a3e..024a01df6c 100644 --- a/Documentation/git.txt +++ b/Documentation/git.txt @@ -1067,6 +1067,37 @@ The index is also capable of storing multiple entries (called "stages") for a given pathname. These stages are used to hold the various unmerged version of a file when a merge is in progress. +SECURITY +-------- + +Some configuration options and hook files may cause Git to run arbitrary +shell commands. Because configuration and hooks are not copied using +`git clone`, it is generally safe to clone remote repositories with +untrusted content, inspect them with `git log`, and so on. + +However, it is not safe to run Git commands in a `.git` directory (or +the working tree that surrounds it) when that `.git` directory itself +comes from an untrusted source. The commands in its config and hooks +are executed in the usual way. + +By default, Git will refuse to run when the repository is owned by +someone other than the user running the command. See the entry for +`safe.directory` in linkgit:git-config[1]. While this can help protect +you in a multi-user environment, note that you can also acquire +untrusted repositories that are owned by you (for example, if you +extract a zip file or tarball from an untrusted source). In such cases, +you'd need to "sanitize" the untrusted repository first. + +If you have an untrusted `.git` directory, you should first clone it +with `git clone --no-local` to obtain a clean copy. Git does restrict +the set of options and hooks that will be run by `upload-pack`, which +handles the server side of a clone or fetch, but beware that the +surface area for attack against `upload-pack` is large, so this does +carry some risk. The safest thing is to serve the repository as an +unprivileged user (either via linkgit:git-daemon[1], ssh, or using +other tools to change user ids). See the discussion in the `SECURITY` +section of linkgit:git-upload-pack[1]. + FURTHER DOCUMENTATION --------------------- diff --git a/GIT-VERSION-GEN b/GIT-VERSION-GEN index 433cf41e59..eb53b0ee01 100755 --- a/GIT-VERSION-GEN +++ b/GIT-VERSION-GEN @@ -1,7 +1,7 @@ #!/bin/sh GVF=GIT-VERSION-FILE -DEF_VER=v2.45.0 +DEF_VER=v2.45.2 LF=' ' @@ -1 +1 @@ -Documentation/RelNotes/2.45.0.txt
\ No newline at end of file +Documentation/RelNotes/2.45.2.txt
\ No newline at end of file @@ -765,8 +765,8 @@ static struct attr_stack *read_attr_from_file(const char *path, unsigned flags) return res; } -static struct attr_stack *read_attr_from_buf(char *buf, const char *path, - unsigned flags) +static struct attr_stack *read_attr_from_buf(char *buf, size_t length, + const char *path, unsigned flags) { struct attr_stack *res; char *sp; @@ -774,6 +774,11 @@ static struct attr_stack *read_attr_from_buf(char *buf, const char *path, if (!buf) return NULL; + if (length >= ATTR_MAX_FILE_SIZE) { + warning(_("ignoring overly large gitattributes blob '%s'"), path); + free(buf); + return NULL; + } CALLOC_ARRAY(res, 1); for (sp = buf; *sp;) { @@ -813,7 +818,7 @@ static struct attr_stack *read_attr_from_blob(struct index_state *istate, return NULL; } - return read_attr_from_buf(buf, path, flags); + return read_attr_from_buf(buf, sz, path, flags); } static struct attr_stack *read_attr_from_index(struct index_state *istate, @@ -860,13 +865,7 @@ static struct attr_stack *read_attr_from_index(struct index_state *istate, stack = read_attr_from_blob(istate, &istate->cache[sparse_dir_pos]->oid, relative_path, flags); } else { buf = read_blob_data_from_index(istate, path, &size); - if (!buf) - return NULL; - if (size >= ATTR_MAX_FILE_SIZE) { - warning(_("ignoring overly large gitattributes blob '%s'"), path); - return NULL; - } - stack = read_attr_from_buf(buf, path, flags); + stack = read_attr_from_buf(buf, size, path, flags); } return stack; } @@ -1223,13 +1222,6 @@ static void compute_default_attr_source(struct object_id *attr_source) ignore_bad_attr_tree = 1; } - if (!default_attr_source_tree_object_name && - startup_info->have_repository && - is_bare_repository()) { - default_attr_source_tree_object_name = "HEAD"; - ignore_bad_attr_tree = 1; - } - if (!default_attr_source_tree_object_name || !is_null_oid(attr_source)) return; diff --git a/builtin/clone.c b/builtin/clone.c index 74ec14542e..5fbe39f898 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -329,7 +329,20 @@ static void copy_or_link_directory(struct strbuf *src, struct strbuf *dest, int src_len, dest_len; struct dir_iterator *iter; int iter_status; - struct strbuf realpath = STRBUF_INIT; + + /* + * Refuse copying directories by default which aren't owned by us. The + * code that performs either the copying or hardlinking is not prepared + * to handle various edge cases where an adversary may for example + * racily swap out files for symlinks. This can cause us to + * inadvertently use the wrong source file. + * + * Furthermore, even if we were prepared to handle such races safely, + * creating hardlinks across user boundaries is an inherently unsafe + * operation as the hardlinked files can be rewritten at will by the + * potentially-untrusted user. We thus refuse to do so by default. + */ + die_upon_dubious_ownership(NULL, NULL, src_repo); mkdir_if_missing(dest->buf, 0777); @@ -377,9 +390,27 @@ static void copy_or_link_directory(struct strbuf *src, struct strbuf *dest, if (unlink(dest->buf) && errno != ENOENT) die_errno(_("failed to unlink '%s'"), dest->buf); if (!option_no_hardlinks) { - strbuf_realpath(&realpath, src->buf, 1); - if (!link(realpath.buf, dest->buf)) + if (!link(src->buf, dest->buf)) { + struct stat st; + + /* + * Sanity-check whether the created hardlink + * actually links to the expected file now. This + * catches time-of-check-time-of-use bugs in + * case the source file was meanwhile swapped. + */ + if (lstat(dest->buf, &st)) + die(_("hardlink cannot be checked at '%s'"), dest->buf); + if (st.st_mode != iter->st.st_mode || + st.st_ino != iter->st.st_ino || + st.st_dev != iter->st.st_dev || + st.st_size != iter->st.st_size || + st.st_uid != iter->st.st_uid || + st.st_gid != iter->st.st_gid) + die(_("hardlink different from source at '%s'"), dest->buf); + continue; + } if (option_local > 0) die_errno(_("failed to create link '%s'"), dest->buf); option_no_hardlinks = 1; @@ -392,8 +423,6 @@ static void copy_or_link_directory(struct strbuf *src, struct strbuf *dest, strbuf_setlen(src, src_len); die(_("failed to iterate over '%s'"), src->buf); } - - strbuf_release(&realpath); } static void clone_local(const char *src_repo, const char *dest_repo) @@ -1507,6 +1536,7 @@ int cmd_clone(int argc, const char **argv, const char *prefix) free(dir); free(path); free(repo_to_free); + UNLEAK(repo); junk_mode = JUNK_LEAVE_ALL; transport_ls_refs_options_release(&transport_ls_refs_options); diff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c index 28186b30f5..c4fa41fda9 100644 --- a/builtin/for-each-repo.c +++ b/builtin/for-each-repo.c @@ -32,6 +32,7 @@ static int run_command_on_repo(const char *path, int argc, const char ** argv) int cmd_for_each_repo(int argc, const char **argv, const char *prefix) { static const char *config_key = NULL; + int keep_going = 0; int i, result = 0; const struct string_list *values; int err; @@ -39,6 +40,8 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix) const struct option options[] = { OPT_STRING(0, "config", &config_key, N_("config"), N_("config key storing a list of repository paths")), + OPT_BOOL(0, "keep-going", &keep_going, + N_("keep going even if command fails in a repository")), OPT_END() }; @@ -55,8 +58,14 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix) else if (err) return 0; - for (i = 0; !result && i < values->nr; i++) - result = run_command_on_repo(values->items[i].string, argc, argv); + for (i = 0; i < values->nr; i++) { + int ret = run_command_on_repo(values->items[i].string, argc, argv); + if (ret) { + if (!keep_going) + return ret; + result = 1; + } + } return result; } diff --git a/builtin/gc.c b/builtin/gc.c index d187cec1ea..d3b5ca9bb1 100644 --- a/builtin/gc.c +++ b/builtin/gc.c @@ -1870,6 +1870,7 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit "<string>%s/git</string>\n" "<string>--exec-path=%s</string>\n" "<string>for-each-repo</string>\n" + "<string>--keep-going</string>\n" "<string>--config=maintenance.repo</string>\n" "<string>maintenance</string>\n" "<string>run</string>\n" @@ -2112,7 +2113,7 @@ static int schtasks_schedule_task(const char *exec_path, enum schedule_priority "<Actions Context=\"Author\">\n" "<Exec>\n" "<Command>\"%s\\headless-git.exe\"</Command>\n" - "<Arguments>--exec-path=\"%s\" for-each-repo --config=maintenance.repo maintenance run --schedule=%s</Arguments>\n" + "<Arguments>--exec-path=\"%s\" for-each-repo --keep-going --config=maintenance.repo maintenance run --schedule=%s</Arguments>\n" "</Exec>\n" "</Actions>\n" "</Task>\n"; @@ -2257,7 +2258,7 @@ static int crontab_update_schedule(int run_maintenance, int fd) "# replaced in the future by a Git command.\n\n"); strbuf_addf(&line_format, - "%%d %%s * * %%s \"%s/git\" --exec-path=\"%s\" for-each-repo --config=maintenance.repo maintenance run --schedule=%%s\n", + "%%d %%s * * %%s \"%s/git\" --exec-path=\"%s\" for-each-repo --keep-going --config=maintenance.repo maintenance run --schedule=%%s\n", exec_path, exec_path); fprintf(cron_in, line_format.buf, minute, "1-23", "*", "hourly"); fprintf(cron_in, line_format.buf, minute, "0", "1-6", "daily"); @@ -2458,7 +2459,7 @@ static int systemd_timer_write_service_template(const char *exec_path) "\n" "[Service]\n" "Type=oneshot\n" - "ExecStart=\"%s/git\" --exec-path=\"%s\" for-each-repo --config=maintenance.repo maintenance run --schedule=%%i\n" + "ExecStart=\"%s/git\" --exec-path=\"%s\" for-each-repo --keep-going --config=maintenance.repo maintenance run --schedule=%%i\n" "LockPersonality=yes\n" "MemoryDenyWriteExecute=yes\n" "NoNewPrivileges=yes\n" diff --git a/builtin/multi-pack-index.c b/builtin/multi-pack-index.c index a72aebecaa..8360932d2e 100644 --- a/builtin/multi-pack-index.c +++ b/builtin/multi-pack-index.c @@ -8,6 +8,7 @@ #include "strbuf.h" #include "trace2.h" #include "object-store-ll.h" +#include "replace-object.h" #define BUILTIN_MIDX_WRITE_USAGE \ N_("git multi-pack-index [<options>] write [--preferred-pack=<pack>]" \ @@ -273,6 +274,8 @@ int cmd_multi_pack_index(int argc, const char **argv, }; struct option *options = parse_options_concat(builtin_multi_pack_index_options, common_opts); + disable_replace_refs(); + git_config(git_default_config, NULL); if (the_repository && diff --git a/builtin/stash.c b/builtin/stash.c index 062be1fbc0..7751bca868 100644 --- a/builtin/stash.c +++ b/builtin/stash.c @@ -1205,8 +1205,8 @@ static int stash_staged(struct stash_info *info, struct strbuf *out_patch, } cp_diff_tree.git_cmd = 1; - strvec_pushl(&cp_diff_tree.args, "diff-tree", "-p", "-U1", "HEAD", - oid_to_hex(&info->w_tree), "--", NULL); + strvec_pushl(&cp_diff_tree.args, "diff-tree", "-p", "--binary", + "-U1", "HEAD", oid_to_hex(&info->w_tree), "--", NULL); if (pipe_command(&cp_diff_tree, NULL, 0, out_patch, 0, NULL, 0)) { ret = -1; goto done; diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c index e4e18adb57..5a71b1ee7e 100644 --- a/builtin/submodule--helper.c +++ b/builtin/submodule--helper.c @@ -303,6 +303,9 @@ static void runcommand_in_submodule_cb(const struct cache_entry *list_item, struct child_process cp = CHILD_PROCESS_INIT; char *displaypath; + if (validate_submodule_path(path) < 0) + exit(128); + displaypath = get_submodule_displaypath(path, info->prefix, info->super_prefix); @@ -634,6 +637,9 @@ static void status_submodule(const char *path, const struct object_id *ce_oid, .free_removed_argv_elements = 1, }; + if (validate_submodule_path(path) < 0) + exit(128); + if (!submodule_from_path(the_repository, null_oid(), path)) die(_("no submodule mapping found in .gitmodules for path '%s'"), path); @@ -1238,6 +1244,9 @@ static void sync_submodule(const char *path, const char *prefix, if (!is_submodule_active(the_repository, path)) return; + if (validate_submodule_path(path) < 0) + exit(128); + sub = submodule_from_path(the_repository, null_oid(), path); if (sub && sub->url) { @@ -1381,6 +1390,9 @@ static void deinit_submodule(const char *path, const char *prefix, struct strbuf sb_config = STRBUF_INIT; char *sub_git_dir = xstrfmt("%s/.git", path); + if (validate_submodule_path(path) < 0) + exit(128); + sub = submodule_from_path(the_repository, null_oid(), path); if (!sub || !sub->name) @@ -1662,16 +1674,42 @@ static char *clone_submodule_sm_gitdir(const char *name) return sm_gitdir; } +static int dir_contains_only_dotgit(const char *path) +{ + DIR *dir = opendir(path); + struct dirent *e; + int ret = 1; + + if (!dir) + return 0; + + e = readdir_skip_dot_and_dotdot(dir); + if (!e) + ret = 0; + else if (strcmp(DEFAULT_GIT_DIR_ENVIRONMENT, e->d_name) || + (e = readdir_skip_dot_and_dotdot(dir))) { + error("unexpected item '%s' in '%s'", e->d_name, path); + ret = 0; + } + + closedir(dir); + return ret; +} + static int clone_submodule(const struct module_clone_data *clone_data, struct string_list *reference) { char *p; char *sm_gitdir = clone_submodule_sm_gitdir(clone_data->name); char *sm_alternate = NULL, *error_strategy = NULL; + struct stat st; struct child_process cp = CHILD_PROCESS_INIT; const char *clone_data_path = clone_data->path; char *to_free = NULL; + if (validate_submodule_path(clone_data_path) < 0) + exit(128); + if (!is_absolute_path(clone_data->path)) clone_data_path = to_free = xstrfmt("%s/%s", get_git_work_tree(), clone_data->path); @@ -1681,6 +1719,10 @@ static int clone_submodule(const struct module_clone_data *clone_data, "git dir"), sm_gitdir); if (!file_exists(sm_gitdir)) { + if (clone_data->require_init && !stat(clone_data_path, &st) && + !is_empty_dir(clone_data_path)) + die(_("directory not empty: '%s'"), clone_data_path); + if (safe_create_leading_directories_const(sm_gitdir) < 0) die(_("could not create directory '%s'"), sm_gitdir); @@ -1725,10 +1767,18 @@ static int clone_submodule(const struct module_clone_data *clone_data, if(run_command(&cp)) die(_("clone of '%s' into submodule path '%s' failed"), clone_data->url, clone_data_path); + + if (clone_data->require_init && !stat(clone_data_path, &st) && + !dir_contains_only_dotgit(clone_data_path)) { + char *dot_git = xstrfmt("%s/.git", clone_data_path); + unlink(dot_git); + free(dot_git); + die(_("directory not empty: '%s'"), clone_data_path); + } } else { char *path; - if (clone_data->require_init && !access(clone_data_path, X_OK) && + if (clone_data->require_init && !stat(clone_data_path, &st) && !is_empty_dir(clone_data_path)) die(_("directory not empty: '%s'"), clone_data_path); if (safe_create_leading_directories_const(clone_data_path) < 0) @@ -1738,6 +1788,23 @@ static int clone_submodule(const struct module_clone_data *clone_data, free(path); } + /* + * We already performed this check at the beginning of this function, + * before cloning the objects. This tries to detect racy behavior e.g. + * in parallel clones, where another process could easily have made the + * gitdir nested _after_ it was created. + * + * To prevent further harm coming from this unintentionally-nested + * gitdir, let's disable it by deleting the `HEAD` file. + */ + if (validate_submodule_git_dir(sm_gitdir, clone_data->name) < 0) { + char *head = xstrfmt("%s/HEAD", sm_gitdir); + unlink(head); + free(head); + die(_("refusing to create/use '%s' in another submodule's " + "git dir"), sm_gitdir); + } + connect_work_tree_and_git_dir(clone_data_path, sm_gitdir, 0); p = git_pathdup_submodule(clone_data_path, "config"); @@ -2517,6 +2584,9 @@ static int update_submodule(struct update_data *update_data) { int ret; + if (validate_submodule_path(update_data->sm_path) < 0) + return -1; + ret = determine_submodule_update_strategy(the_repository, update_data->just_cloned, update_data->sm_path, @@ -2624,12 +2694,21 @@ static int update_submodules(struct update_data *update_data) for (i = 0; i < suc.update_clone_nr; i++) { struct update_clone_data ucd = suc.update_clone[i]; - int code; + int code = 128; oidcpy(&update_data->oid, &ucd.oid); update_data->just_cloned = ucd.just_cloned; update_data->sm_path = ucd.sub->path; + /* + * Verify that the submodule path does not contain any + * symlinks; if it does, it might have been tampered with. + * TODO: allow exempting it via + * `safe.submodule.path` or something + */ + if (validate_submodule_path(update_data->sm_path) < 0) + goto fail; + code = ensure_core_worktree(update_data->sm_path); if (code) goto fail; @@ -3356,6 +3435,9 @@ static int module_add(int argc, const char **argv, const char *prefix) normalize_path_copy(add_data.sm_path, add_data.sm_path); strip_dir_trailing_slashes(add_data.sm_path); + if (validate_submodule_path(add_data.sm_path) < 0) + exit(128); + die_on_index_match(add_data.sm_path, force); die_on_repo_without_commits(add_data.sm_path); diff --git a/builtin/upload-pack.c b/builtin/upload-pack.c index 15afb97260..46d93278d9 100644 --- a/builtin/upload-pack.c +++ b/builtin/upload-pack.c @@ -9,6 +9,7 @@ #include "upload-pack.h" #include "serve.h" #include "commit.h" +#include "environment.h" static const char * const upload_pack_usage[] = { N_("git-upload-pack [--[no-]strict] [--timeout=<n>] [--stateless-rpc]\n" @@ -39,6 +40,7 @@ int cmd_upload_pack(int argc, const char **argv, const char *prefix) packet_trace_identity("upload-pack"); disable_replace_refs(); save_commit_buffer = 0; + xsetenv(NO_LAZY_FETCH_ENVIRONMENT, "1", 0); argc = parse_options(argc, argv, prefix, options, upload_pack_usage, 0); diff --git a/ci/install-dependencies.sh b/ci/install-dependencies.sh index b4e22de3cb..fa0c00c606 100755 --- a/ci/install-dependencies.sh +++ b/ci/install-dependencies.sh @@ -34,8 +34,6 @@ macos-*) export HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 # Uncomment this if you want to run perf tests: # brew install gnu-time - test -z "$BREW_INSTALL_PACKAGES" || - brew install $BREW_INSTALL_PACKAGES brew link --force gettext mkdir -p "$P4_PATH" @@ -325,9 +325,13 @@ ubuntu-*) break fi - PYTHON_PACKAGE=python2 - if test "$jobname" = linux-gcc + # Python 2 is end of life, and Ubuntu 23.04 and newer don't actually + # have it anymore. We thus only test with Python 2 on older LTS + # releases. + if "$distro" = "ubuntu-20.04" then + PYTHON_PACKAGE=python2 + else PYTHON_PACKAGE=python3 fi MAKEFLAGS="$MAKEFLAGS PYTHON_PATH=/usr/bin/$PYTHON_PACKAGE" diff --git a/compat/mingw.c b/compat/mingw.c index 4876344b5b..6b06ea540f 100644 --- a/compat/mingw.c +++ b/compat/mingw.c @@ -3159,6 +3159,7 @@ int uname(struct utsname *buf) return 0; } +#ifndef NO_UNIX_SOCKETS int mingw_have_unix_sockets(void) { SC_HANDLE scm, srvc; @@ -3177,3 +3178,4 @@ int mingw_have_unix_sockets(void) } return ret; } +#endif diff --git a/compat/regex/regcomp.c b/compat/regex/regcomp.c index d1bc09e49b..2bc0f1187a 100644 --- a/compat/regex/regcomp.c +++ b/compat/regex/regcomp.c @@ -868,7 +868,7 @@ init_dfa (re_dfa_t *dfa, size_t pat_len) if (table_size > pat_len) break; - dfa->state_table = calloc (sizeof (struct re_state_table_entry), table_size); + dfa->state_table = calloc (table_size, sizeof (struct re_state_table_entry)); dfa->state_hash_mask = table_size - 1; dfa->mb_cur_max = MB_CUR_MAX; @@ -936,7 +936,7 @@ init_dfa (re_dfa_t *dfa, size_t pat_len) { int i, j, ch; - dfa->sb_char = (re_bitset_ptr_t) calloc (sizeof (bitset_t), 1); + dfa->sb_char = (re_bitset_ptr_t) calloc (1, sizeof (bitset_t)); if (BE (dfa->sb_char == NULL, 0)) return REG_ESPACE; @@ -3079,9 +3079,9 @@ parse_bracket_exp (re_string_t *regexp, re_dfa_t *dfa, re_token_t *token, _NL_COLLATE_SYMB_EXTRAMB); } #endif - sbcset = (re_bitset_ptr_t) calloc (sizeof (bitset_t), 1); + sbcset = (re_bitset_ptr_t) calloc (1, sizeof (bitset_t)); #ifdef RE_ENABLE_I18N - mbcset = (re_charset_t *) calloc (sizeof (re_charset_t), 1); + mbcset = (re_charset_t *) calloc (1, sizeof (re_charset_t)); #endif /* RE_ENABLE_I18N */ #ifdef RE_ENABLE_I18N if (BE (sbcset == NULL || mbcset == NULL, 0)) @@ -3626,9 +3626,9 @@ build_charclass_op (re_dfa_t *dfa, RE_TRANSLATE_TYPE trans, re_token_t br_token; bin_tree_t *tree; - sbcset = (re_bitset_ptr_t) calloc (sizeof (bitset_t), 1); + sbcset = (re_bitset_ptr_t) calloc (1, sizeof (bitset_t)); #ifdef RE_ENABLE_I18N - mbcset = (re_charset_t *) calloc (sizeof (re_charset_t), 1); + mbcset = (re_charset_t *) calloc (1, sizeof (re_charset_t)); #endif /* RE_ENABLE_I18N */ #ifdef RE_ENABLE_I18N diff --git a/compat/regex/regex_internal.c b/compat/regex/regex_internal.c index ec51cf3446..ec5cc5d2dd 100644 --- a/compat/regex/regex_internal.c +++ b/compat/regex/regex_internal.c @@ -1628,7 +1628,7 @@ create_ci_newstate (const re_dfa_t *dfa, const re_node_set *nodes, reg_errcode_t err; re_dfastate_t *newstate; - newstate = (re_dfastate_t *) calloc (sizeof (re_dfastate_t), 1); + newstate = (re_dfastate_t *) calloc (1, sizeof (re_dfastate_t)); if (BE (newstate == NULL, 0)) return NULL; err = re_node_set_init_copy (&newstate->nodes, nodes); @@ -1678,7 +1678,7 @@ create_cd_newstate (const re_dfa_t *dfa, const re_node_set *nodes, reg_errcode_t err; re_dfastate_t *newstate; - newstate = (re_dfastate_t *) calloc (sizeof (re_dfastate_t), 1); + newstate = (re_dfastate_t *) calloc (1, sizeof (re_dfastate_t)); if (BE (newstate == NULL, 0)) return NULL; err = re_node_set_init_copy (&newstate->nodes, nodes); diff --git a/compat/regex/regexec.c b/compat/regex/regexec.c index 49358ae475..e92be5741d 100644 --- a/compat/regex/regexec.c +++ b/compat/regex/regexec.c @@ -2796,8 +2796,8 @@ get_subexp (re_match_context_t *mctx, int bkref_node, int bkref_str_idx) continue; /* No. */ if (sub_top->path == NULL) { - sub_top->path = calloc (sizeof (state_array_t), - sl_str - sub_top->str_idx + 1); + sub_top->path = calloc (sl_str - sub_top->str_idx + 1, + sizeof (state_array_t)); if (sub_top->path == NULL) return REG_ESPACE; } @@ -3361,7 +3361,7 @@ build_trtable (const re_dfa_t *dfa, re_dfastate_t *state) if (ndests == 0) { state->trtable = (re_dfastate_t **) - calloc (sizeof (re_dfastate_t *), SBC_MAX); + calloc (SBC_MAX, sizeof (re_dfastate_t *)); return 1; } return 0; @@ -3457,7 +3457,7 @@ out_free: discern by looking at the character code: allocate a 256-entry transition table. */ trtable = state->trtable = - (re_dfastate_t **) calloc (sizeof (re_dfastate_t *), SBC_MAX); + (re_dfastate_t **) calloc (SBC_MAX, sizeof (re_dfastate_t *)); if (BE (trtable == NULL, 0)) goto out_free; @@ -3488,7 +3488,7 @@ out_free: transition tables, one starting at trtable[0] and one starting at trtable[SBC_MAX]. */ trtable = state->word_trtable = - (re_dfastate_t **) calloc (sizeof (re_dfastate_t *), 2 * SBC_MAX); + (re_dfastate_t **) calloc (2 * SBC_MAX, sizeof (re_dfastate_t *)); if (BE (trtable == NULL, 0)) goto out_free; diff --git a/contrib/completion/git-completion.zsh b/contrib/completion/git-completion.zsh index cac6f61881..f5877bd7a1 100644 --- a/contrib/completion/git-completion.zsh +++ b/contrib/completion/git-completion.zsh @@ -272,6 +272,7 @@ _git () { local _ret=1 local cur cword prev + local __git_repo_path cur=${words[CURRENT]} prev=${words[CURRENT-1]} @@ -1,6 +1,9 @@ #include "git-compat-util.h" #include "copy.h" #include "path.h" +#include "gettext.h" +#include "strbuf.h" +#include "abspath.h" int copy_fd(int ifd, int ofd) { diff --git a/diff-lib.c b/diff-lib.c index 683f11e509..12b1541478 100644 --- a/diff-lib.c +++ b/diff-lib.c @@ -660,7 +660,6 @@ int do_diff_cache(const struct object_id *tree_oid, struct diff_options *opt) repo_init_revisions(opt->repo, &revs, NULL); copy_pathspec(&revs.prune_data, &opt->pathspec); - diff_setup_done(&revs.diffopt); revs.diffopt = *opt; if (diff_cache(&revs, tree_oid, NULL, 1)) @@ -100,6 +100,18 @@ int fspathncmp(const char *a, const char *b, size_t count) return ignore_case ? strncasecmp(a, b, count) : strncmp(a, b, count); } +int paths_collide(const char *a, const char *b) +{ + size_t len_a = strlen(a), len_b = strlen(b); + + if (len_a == len_b) + return fspatheq(a, b); + + if (len_a < len_b) + return is_dir_sep(b[len_a]) && !fspathncmp(a, b, len_a); + return is_dir_sep(a[len_b]) && !fspathncmp(a, b, len_b); +} + unsigned int fspathhash(const char *str) { return ignore_case ? strihash(str) : strhash(str); @@ -549,6 +549,13 @@ int fspathncmp(const char *a, const char *b, size_t count); unsigned int fspathhash(const char *str); /* + * Reports whether paths collide. This may be because the paths differ only in + * case on a case-sensitive filesystem, or that one path refers to a symlink + * that collides with one of the parent directories of the other. + */ +int paths_collide(const char *a, const char *b); + +/* * The prefix part of pattern must not contains wildcards. */ struct pathspec_item; @@ -460,7 +460,7 @@ static void mark_colliding_entries(const struct checkout *state, continue; if ((trust_ino && !match_stat_data(&dup->ce_stat_data, st)) || - (!trust_ino && !fspathcmp(ce->name, dup->name))) { + paths_collide(ce->name, dup->name)) { dup->ce_flags |= CE_MATCHED; break; } @@ -547,6 +547,20 @@ int checkout_entry_ca(struct cache_entry *ce, struct conv_attrs *ca, /* If it is a gitlink, leave it alone! */ if (S_ISGITLINK(ce->ce_mode)) return 0; + /* + * We must avoid replacing submodules' leading + * directories with symbolic links, lest recursive + * clones can write into arbitrary locations. + * + * Technically, this logic is not limited + * to recursive clones, or for that matter to + * submodules' paths colliding with symbolic links' + * paths. Yet it strikes a balance in favor of + * simplicity, and if paths are colliding, we might + * just as well keep the directories during a clone. + */ + if (state->clone && S_ISLNK(ce->ce_mode)) + return 0; remove_subtree(&path); } else if (unlink(path.buf)) return error_errno("unable to unlink old '%s'", path.buf); @@ -3253,17 +3253,19 @@ class P4Sync(Command, P4UserMap): if self.stream_have_file_info: if "depotFile" in self.stream_file: f = self.stream_file["depotFile"] - # force a failure in fast-import, else an empty - # commit will be made - self.gitStream.write("\n") - self.gitStream.write("die-now\n") - self.gitStream.close() - # ignore errors, but make sure it exits first - self.importProcess.wait() - if f: - die("Error from p4 print for %s: %s" % (f, err)) - else: - die("Error from p4 print: %s" % err) + try: + # force a failure in fast-import, else an empty + # commit will be made + self.gitStream.write("\n") + self.gitStream.write("die-now\n") + self.gitStream.close() + # ignore errors, but make sure it exits first + self.importProcess.wait() + finally: + if f: + die("Error from p4 print for %s: %s" % (f, err)) + else: + die("Error from p4 print: %s" % err) if 'depotFile' in marshalled and self.stream_have_file_info: # start of a new file - output the old one first @@ -7,25 +7,31 @@ #include "run-command.h" #include "config.h" #include "strbuf.h" +#include "environment.h" +#include "setup.h" const char *find_hook(const char *name) { static struct strbuf path = STRBUF_INIT; + int found_hook; + strbuf_reset(&path); strbuf_git_path(&path, "hooks/%s", name); - if (access(path.buf, X_OK) < 0) { + found_hook = access(path.buf, X_OK) >= 0; +#ifdef STRIP_EXTENSION + if (!found_hook) { int err = errno; -#ifdef STRIP_EXTENSION strbuf_addstr(&path, STRIP_EXTENSION); - if (access(path.buf, X_OK) >= 0) - return path.buf; - if (errno == EACCES) - err = errno; + found_hook = access(path.buf, X_OK) >= 0; + if (!found_hook) + errno = err; + } #endif - if (err == EACCES && advice_enabled(ADVICE_IGNORED_HOOK)) { + if (!found_hook) { + if (errno == EACCES && advice_enabled(ADVICE_IGNORED_HOOK)) { static struct string_list advise_given = STRING_LIST_INIT_DUP; if (!string_list_lookup(&advise_given, name)) { diff --git a/mergetools/vimdiff b/mergetools/vimdiff index 97e376329b..734d15a03b 100644 --- a/mergetools/vimdiff +++ b/mergetools/vimdiff @@ -72,7 +72,6 @@ gen_cmd_aux () { nested=0 nested_min=100 - # Step 1: # # Increase/decrease "start"/"end" indices respectively to get rid of @@ -87,7 +86,7 @@ gen_cmd_aux () { IFS=# for c in $(echo "$LAYOUT" | sed 's:.:&#:g') do - if test "$c" = " " + if test -z "$c" || test "$c" = " " then continue fi @@ -829,6 +829,7 @@ const char *enter_repo(const char *path, int strict) if (!suffix[i]) return NULL; gitfile = read_gitfile(used_path.buf); + die_upon_dubious_ownership(gitfile, NULL, used_path.buf); if (gitfile) { strbuf_reset(&used_path); strbuf_addstr(&used_path, gitfile); @@ -839,6 +840,7 @@ const char *enter_repo(const char *path, int strict) } else { const char *gitfile = read_gitfile(path); + die_upon_dubious_ownership(gitfile, NULL, path); if (gitfile) path = gitfile; if (chdir(path)) diff --git a/promisor-remote.c b/promisor-remote.c index ac3aa1e365..b414922c44 100644 --- a/promisor-remote.c +++ b/promisor-remote.c @@ -8,6 +8,7 @@ #include "transport.h" #include "strvec.h" #include "packfile.h" +#include "environment.h" struct promisor_remote_config { struct promisor_remote *promisors; @@ -23,6 +24,15 @@ static int fetch_objects(struct repository *repo, int i; FILE *child_in; + if (git_env_bool(NO_LAZY_FETCH_ENVIRONMENT, 0)) { + static int warning_shown; + if (!warning_shown) { + warning_shown = 1; + warning(_("lazy fetching disabled; some objects may not be available")); + } + return -1; + } + child.git_cmd = 1; child.in = -1; if (repo != the_repository) diff --git a/read-cache.c b/read-cache.c index e1723ad796..a6db25a16d 100644 --- a/read-cache.c +++ b/read-cache.c @@ -1116,19 +1116,32 @@ static int has_dir_name(struct index_state *istate, istate->cache[istate->cache_nr - 1]->name, &len_eq_last); if (cmp_last > 0) { - if (len_eq_last == 0) { + if (name[len_eq_last] != '/') { /* * The entry sorts AFTER the last one in the - * index and their paths have no common prefix, - * so there cannot be a F/D conflict. + * index. + * + * If there were a conflict with "file", then our + * name would start with "file/" and the last index + * entry would start with "file" but not "file/". + * + * The next character after common prefix is + * not '/', so there can be no conflict. */ return retval; } else { /* * The entry sorts AFTER the last one in the - * index, but has a common prefix. Fall through - * to the loop below to disect the entry's path - * and see where the difference is. + * index, and the next character after common + * prefix is '/'. + * + * Either the last index entry is a file in + * conflict with this entry, or it has a name + * which sorts between this entry and the + * potential conflicting file. + * + * In both cases, we fall through to the loop + * below and let the regular search code handle it. */ } } else if (cmp_last == 0) { @@ -1152,53 +1165,6 @@ static int has_dir_name(struct index_state *istate, } len = slash - name; - if (cmp_last > 0) { - /* - * (len + 1) is a directory boundary (including - * the trailing slash). And since the loop is - * decrementing "slash", the first iteration is - * the longest directory prefix; subsequent - * iterations consider parent directories. - */ - - if (len + 1 <= len_eq_last) { - /* - * The directory prefix (including the trailing - * slash) also appears as a prefix in the last - * entry, so the remainder cannot collide (because - * strcmp said the whole path was greater). - * - * EQ: last: xxx/A - * this: xxx/B - * - * LT: last: xxx/file_A - * this: xxx/file_B - */ - return retval; - } - - if (len > len_eq_last) { - /* - * This part of the directory prefix (excluding - * the trailing slash) is longer than the known - * equal portions, so this sub-directory cannot - * collide with a file. - * - * GT: last: xxxA - * this: xxxB/file - */ - return retval; - } - - /* - * This is a possible collision. Fall through and - * let the regular search code handle it. - * - * last: xxx - * this: xxx/file - */ - } - pos = index_name_stage_pos(istate, name, len, stage, EXPAND_SPARSE); if (pos >= 0) { /* diff --git a/repository.c b/repository.c index e15b416944..da42ff550e 100644 --- a/repository.c +++ b/repository.c @@ -295,6 +295,8 @@ void repo_clear(struct repository *repo) parsed_object_pool_clear(repo->parsed_objects); FREE_AND_NULL(repo->parsed_objects); + FREE_AND_NULL(repo->settings.fsmonitor); + if (repo->config) { git_configset_clear(repo->config); FREE_AND_NULL(repo->config); diff --git a/sequencer.c b/sequencer.c index 2c19846385..88de4dc20f 100644 --- a/sequencer.c +++ b/sequencer.c @@ -207,6 +207,46 @@ static GIT_PATH_FUNC(rebase_path_no_reschedule_failed_exec, "rebase-merge/no-res static GIT_PATH_FUNC(rebase_path_drop_redundant_commits, "rebase-merge/drop_redundant_commits") static GIT_PATH_FUNC(rebase_path_keep_redundant_commits, "rebase-merge/keep_redundant_commits") +/* + * A 'struct replay_ctx' represents the private state of the sequencer. + */ +struct replay_ctx { + /* + * The commit message that will be used except at the end of a + * chain of fixup and squash commands. + */ + struct strbuf message; + /* + * The list of completed fixup and squash commands in the + * current chain. + */ + struct strbuf current_fixups; + /* + * Stores the reflog message that will be used when creating a + * commit. Points to a static buffer and should not be free()'d. + */ + const char *reflog_message; + /* + * The number of completed fixup and squash commands in the + * current chain. + */ + int current_fixup_count; + /* + * Whether message contains a commit message. + */ + unsigned have_message :1; +}; + +struct replay_ctx* replay_ctx_new(void) +{ + struct replay_ctx *ctx = xcalloc(1, sizeof(*ctx)); + + strbuf_init(&ctx->current_fixups, 0); + strbuf_init(&ctx->message, 0); + + return ctx; +} + /** * A 'struct update_refs_record' represents a value in the update-refs * list. We use a string_list to map refs to these (before, after) pairs. @@ -366,17 +406,26 @@ static const char *gpg_sign_opt_quoted(struct replay_opts *opts) return buf.buf; } +static void replay_ctx_release(struct replay_ctx *ctx) +{ + strbuf_release(&ctx->current_fixups); + strbuf_release(&ctx->message); +} + void replay_opts_release(struct replay_opts *opts) { + struct replay_ctx *ctx = opts->ctx; + free(opts->gpg_sign); free(opts->reflog_action); free(opts->default_strategy); free(opts->strategy); strvec_clear (&opts->xopts); - strbuf_release(&opts->current_fixups); if (opts->revs) release_revisions(opts->revs); free(opts->revs); + replay_ctx_release(ctx); + free(opts->ctx); } int sequencer_remove_state(struct replay_opts *opts) @@ -1084,6 +1133,7 @@ static int run_git_commit(const char *defmsg, struct replay_opts *opts, unsigned int flags) { + struct replay_ctx *ctx = opts->ctx; struct child_process cmd = CHILD_PROCESS_INIT; if ((flags & CLEANUP_MSG) && (flags & VERBATIM_MSG)) @@ -1101,7 +1151,7 @@ static int run_git_commit(const char *defmsg, gpg_opt, gpg_opt); } - strvec_pushf(&cmd.env, GIT_REFLOG_ACTION "=%s", opts->reflog_message); + strvec_pushf(&cmd.env, GIT_REFLOG_ACTION "=%s", ctx->reflog_message); if (opts->committer_date_is_author_date) strvec_pushf(&cmd.env, "GIT_COMMITTER_DATE=%s", @@ -1487,6 +1537,7 @@ static int try_to_commit(struct repository *r, struct replay_opts *opts, unsigned int flags, struct object_id *oid) { + struct replay_ctx *ctx = opts->ctx; struct object_id tree; struct commit *current_head = NULL; struct commit_list *parents = NULL; @@ -1648,7 +1699,7 @@ static int try_to_commit(struct repository *r, goto out; } - if (update_head_with_reflog(current_head, oid, opts->reflog_message, + if (update_head_with_reflog(current_head, oid, ctx->reflog_message, msg, &err)) { res = error("%s", err.buf); goto out; @@ -1878,10 +1929,10 @@ static void add_commented_lines(struct strbuf *buf, const void *str, size_t len) } /* Does the current fixup chain contain a squash command? */ -static int seen_squash(struct replay_opts *opts) +static int seen_squash(struct replay_ctx *ctx) { - return starts_with(opts->current_fixups.buf, "squash") || - strstr(opts->current_fixups.buf, "\nsquash"); + return starts_with(ctx->current_fixups.buf, "squash") || + strstr(ctx->current_fixups.buf, "\nsquash"); } static void update_comment_bufs(struct strbuf *buf1, struct strbuf *buf2, int n) @@ -1957,6 +2008,7 @@ static int append_squash_message(struct strbuf *buf, const char *body, enum todo_command command, struct replay_opts *opts, unsigned flag) { + struct replay_ctx *ctx = opts->ctx; const char *fixup_msg; size_t commented_len = 0, fixup_off; /* @@ -1965,13 +2017,13 @@ static int append_squash_message(struct strbuf *buf, const char *body, * squashing commit messages. */ if (starts_with(body, "amend!") || - ((command == TODO_SQUASH || seen_squash(opts)) && + ((command == TODO_SQUASH || seen_squash(ctx)) && (starts_with(body, "squash!") || starts_with(body, "fixup!")))) commented_len = commit_subject_length(body); strbuf_addf(buf, "\n%s ", comment_line_str); strbuf_addf(buf, _(nth_commit_msg_fmt), - ++opts->current_fixup_count + 1); + ++ctx->current_fixup_count + 1); strbuf_addstr(buf, "\n\n"); strbuf_add_commented_lines(buf, body, commented_len, comment_line_str); /* buf->buf may be reallocated so store an offset into the buffer */ @@ -1979,7 +2031,7 @@ static int append_squash_message(struct strbuf *buf, const char *body, strbuf_addstr(buf, body + commented_len); /* fixup -C after squash behaves like squash */ - if (is_fixup_flag(command, flag) && !seen_squash(opts)) { + if (is_fixup_flag(command, flag) && !seen_squash(ctx)) { /* * We're replacing the commit message so we need to * append the Signed-off-by: trailer if the user @@ -2013,12 +2065,13 @@ static int update_squash_messages(struct repository *r, struct replay_opts *opts, unsigned flag) { + struct replay_ctx *ctx = opts->ctx; struct strbuf buf = STRBUF_INIT; int res = 0; const char *message, *body; const char *encoding = get_commit_output_encoding(); - if (opts->current_fixup_count > 0) { + if (ctx->current_fixup_count > 0) { struct strbuf header = STRBUF_INIT; char *eol; @@ -2031,10 +2084,10 @@ static int update_squash_messages(struct repository *r, strbuf_addf(&header, "%s ", comment_line_str); strbuf_addf(&header, _(combined_commit_msg_fmt), - opts->current_fixup_count + 2); + ctx->current_fixup_count + 2); strbuf_splice(&buf, 0, eol - buf.buf, header.buf, header.len); strbuf_release(&header); - if (is_fixup_flag(command, flag) && !seen_squash(opts)) + if (is_fixup_flag(command, flag) && !seen_squash(ctx)) update_squash_message_for_fixup(&buf); } else { struct object_id head; @@ -2081,7 +2134,7 @@ static int update_squash_messages(struct repository *r, } else if (command == TODO_FIXUP) { strbuf_addf(&buf, "\n%s ", comment_line_str); strbuf_addf(&buf, _(skip_nth_commit_msg_fmt), - ++opts->current_fixup_count + 1); + ++ctx->current_fixup_count + 1); strbuf_addstr(&buf, "\n\n"); strbuf_add_commented_lines(&buf, body, strlen(body), comment_line_str); @@ -2095,12 +2148,12 @@ static int update_squash_messages(struct repository *r, strbuf_release(&buf); if (!res) { - strbuf_addf(&opts->current_fixups, "%s%s %s", - opts->current_fixups.len ? "\n" : "", + strbuf_addf(&ctx->current_fixups, "%s%s %s", + ctx->current_fixups.len ? "\n" : "", command_to_string(command), oid_to_hex(&commit->object.oid)); - res = write_message(opts->current_fixups.buf, - opts->current_fixups.len, + res = write_message(ctx->current_fixups.buf, + ctx->current_fixups.len, rebase_path_current_fixups(), 0); } @@ -2178,6 +2231,7 @@ static int do_pick_commit(struct repository *r, struct replay_opts *opts, int final_fixup, int *check_todo) { + struct replay_ctx *ctx = opts->ctx; unsigned int flags = should_edit(opts) ? EDIT_MSG : 0; const char *msg_file = should_edit(opts) ? NULL : git_path_merge_msg(r); struct object_id head; @@ -2185,7 +2239,6 @@ static int do_pick_commit(struct repository *r, const char *base_label, *next_label; char *author = NULL; struct commit_message msg = { NULL, NULL, NULL, NULL }; - struct strbuf msgbuf = STRBUF_INIT; int res, unborn = 0, reword = 0, allow, drop_commit; enum todo_command command = item->command; struct commit *commit = item->commit; @@ -2284,7 +2337,7 @@ static int do_pick_commit(struct repository *r, next = parent; next_label = msg.parent_label; if (opts->commit_use_reference) { - strbuf_addstr(&msgbuf, + strbuf_addstr(&ctx->message, "# *** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***"); } else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) && /* @@ -2293,21 +2346,21 @@ static int do_pick_commit(struct repository *r, * thus requiring excessive complexity to deal with. */ !starts_with(orig_subject, "Revert \"")) { - strbuf_addstr(&msgbuf, "Reapply \""); - strbuf_addstr(&msgbuf, orig_subject); + strbuf_addstr(&ctx->message, "Reapply \""); + strbuf_addstr(&ctx->message, orig_subject); } else { - strbuf_addstr(&msgbuf, "Revert \""); - strbuf_addstr(&msgbuf, msg.subject); - strbuf_addstr(&msgbuf, "\""); + strbuf_addstr(&ctx->message, "Revert \""); + strbuf_addstr(&ctx->message, msg.subject); + strbuf_addstr(&ctx->message, "\""); } - strbuf_addstr(&msgbuf, "\n\nThis reverts commit "); - refer_to_commit(opts, &msgbuf, commit); + strbuf_addstr(&ctx->message, "\n\nThis reverts commit "); + refer_to_commit(opts, &ctx->message, commit); if (commit->parents && commit->parents->next) { - strbuf_addstr(&msgbuf, ", reversing\nchanges made to "); - refer_to_commit(opts, &msgbuf, parent); + strbuf_addstr(&ctx->message, ", reversing\nchanges made to "); + refer_to_commit(opts, &ctx->message, parent); } - strbuf_addstr(&msgbuf, ".\n"); + strbuf_addstr(&ctx->message, ".\n"); } else { const char *p; @@ -2316,21 +2369,22 @@ static int do_pick_commit(struct repository *r, next = commit; next_label = msg.label; - /* Append the commit log message to msgbuf. */ + /* Append the commit log message to ctx->message. */ if (find_commit_subject(msg.message, &p)) - strbuf_addstr(&msgbuf, p); + strbuf_addstr(&ctx->message, p); if (opts->record_origin) { - strbuf_complete_line(&msgbuf); - if (!has_conforming_footer(&msgbuf, NULL, 0)) - strbuf_addch(&msgbuf, '\n'); - strbuf_addstr(&msgbuf, cherry_picked_prefix); - strbuf_addstr(&msgbuf, oid_to_hex(&commit->object.oid)); - strbuf_addstr(&msgbuf, ")\n"); + strbuf_complete_line(&ctx->message); + if (!has_conforming_footer(&ctx->message, NULL, 0)) + strbuf_addch(&ctx->message, '\n'); + strbuf_addstr(&ctx->message, cherry_picked_prefix); + strbuf_addstr(&ctx->message, oid_to_hex(&commit->object.oid)); + strbuf_addstr(&ctx->message, ")\n"); } if (!is_fixup(command)) author = get_author(msg.message); } + ctx->have_message = 1; if (command == TODO_REWORD) reword = 1; @@ -2361,7 +2415,7 @@ static int do_pick_commit(struct repository *r, } if (opts->signoff && !is_fixup(command)) - append_signoff(&msgbuf, 0, 0); + append_signoff(&ctx->message, 0, 0); if (is_rebase_i(opts) && write_author_script(msg.message) < 0) res = -1; @@ -2370,17 +2424,17 @@ static int do_pick_commit(struct repository *r, !strcmp(opts->strategy, "ort") || command == TODO_REVERT) { res = do_recursive_merge(r, base, next, base_label, next_label, - &head, &msgbuf, opts); + &head, &ctx->message, opts); if (res < 0) goto leave; - res |= write_message(msgbuf.buf, msgbuf.len, + res |= write_message(ctx->message.buf, ctx->message.len, git_path_merge_msg(r), 0); } else { struct commit_list *common = NULL; struct commit_list *remotes = NULL; - res = write_message(msgbuf.buf, msgbuf.len, + res = write_message(ctx->message.buf, ctx->message.len, git_path_merge_msg(r), 0); commit_list_insert(base, &common); @@ -2458,14 +2512,13 @@ fast_forward_edit: unlink(rebase_path_fixup_msg()); unlink(rebase_path_squash_msg()); unlink(rebase_path_current_fixups()); - strbuf_reset(&opts->current_fixups); - opts->current_fixup_count = 0; + strbuf_reset(&ctx->current_fixups); + ctx->current_fixup_count = 0; } leave: free_message(commit, &msg); free(author); - strbuf_release(&msgbuf); update_abort_safety_file(); return res; @@ -2846,12 +2899,14 @@ void sequencer_post_commit_cleanup(struct repository *r, int verbose) NULL, REF_NO_DEREF); if (!need_cleanup) - return; + goto out; if (!have_finished_the_last_pick()) - return; + goto out; sequencer_remove_state(&opts); +out: + replay_opts_release(&opts); } static void todo_list_write_total_nr(struct todo_list *todo_list) @@ -3022,6 +3077,8 @@ static void read_strategy_opts(struct replay_opts *opts, struct strbuf *buf) static int read_populate_opts(struct replay_opts *opts) { + struct replay_ctx *ctx = opts->ctx; + if (is_rebase_i(opts)) { struct strbuf buf = STRBUF_INIT; int ret = 0; @@ -3081,13 +3138,13 @@ static int read_populate_opts(struct replay_opts *opts) read_strategy_opts(opts, &buf); strbuf_reset(&buf); - if (read_oneliner(&opts->current_fixups, + if (read_oneliner(&ctx->current_fixups, rebase_path_current_fixups(), READ_ONELINER_SKIP_IF_EMPTY)) { - const char *p = opts->current_fixups.buf; - opts->current_fixup_count = 1; + const char *p = ctx->current_fixups.buf; + ctx->current_fixup_count = 1; while ((p = strchr(p, '\n'))) { - opts->current_fixup_count++; + ctx->current_fixup_count++; p++; } } @@ -3608,13 +3665,24 @@ static int error_with_patch(struct repository *r, struct replay_opts *opts, int exit_code, int to_amend) { - if (commit) { - if (make_patch(r, commit, opts)) + struct replay_ctx *ctx = opts->ctx; + + /* + * Write the commit message to be used by "git rebase + * --continue". If a "fixup" or "squash" command has conflicts + * then we will have already written rebase_path_message() in + * error_failed_squash(). If an "edit" command was + * fast-forwarded then we don't have a message in ctx->message + * and rely on make_patch() to write rebase_path_message() + * instead. + */ + if (ctx->have_message && !file_exists(rebase_path_message()) && + write_message(ctx->message.buf, ctx->message.len, + rebase_path_message(), 0)) + return error(_("could not write commit message file")); + + if (commit && make_patch(r, commit, opts)) return -1; - } else if (copy_file(rebase_path_message(), - git_path_merge_msg(r), 0666)) - return error(_("unable to copy '%s' to '%s'"), - git_path_merge_msg(r), rebase_path_message()); if (to_amend) { if (intend_to_amend()) @@ -3936,6 +4004,7 @@ static int do_merge(struct repository *r, const char *arg, int arg_len, int flags, int *check_todo, struct replay_opts *opts) { + struct replay_ctx *ctx = opts->ctx; int run_commit_flags = 0; struct strbuf ref_name = STRBUF_INIT; struct commit *head_commit, *merge_commit, *i; @@ -4064,40 +4133,31 @@ static int do_merge(struct repository *r, write_author_script(message); find_commit_subject(message, &body); len = strlen(body); - ret = write_message(body, len, git_path_merge_msg(r), 0); + strbuf_add(&ctx->message, body, len); repo_unuse_commit_buffer(r, commit, message); - if (ret) { - error_errno(_("could not write '%s'"), - git_path_merge_msg(r)); - goto leave_merge; - } } else { struct strbuf buf = STRBUF_INIT; - int len; strbuf_addf(&buf, "author %s", git_author_info(0)); write_author_script(buf.buf); - strbuf_reset(&buf); + strbuf_release(&buf); if (oneline_offset < arg_len) { - p = arg + oneline_offset; - len = arg_len - oneline_offset; + strbuf_add(&ctx->message, arg + oneline_offset, + arg_len - oneline_offset); } else { - strbuf_addf(&buf, "Merge %s '%.*s'", + strbuf_addf(&ctx->message, "Merge %s '%.*s'", to_merge->next ? "branches" : "branch", merge_arg_len, arg); - p = buf.buf; - len = buf.len; - } - - ret = write_message(p, len, git_path_merge_msg(r), 0); - strbuf_release(&buf); - if (ret) { - error_errno(_("could not write '%s'"), - git_path_merge_msg(r)); - goto leave_merge; } } + ctx->have_message = 1; + if (write_message(ctx->message.buf, ctx->message.len, + git_path_merge_msg(r), 0)) { + ret = error_errno(_("could not write '%s'"), + git_path_merge_msg(r)); + goto leave_merge; + } if (strategy || to_merge->next) { /* Octopus merge */ @@ -4758,11 +4818,12 @@ static int pick_one_commit(struct repository *r, struct replay_opts *opts, int *check_todo, int* reschedule) { + struct replay_ctx *ctx = opts->ctx; int res; struct todo_item *item = todo_list->items + todo_list->current; const char *arg = todo_item_get_arg(todo_list, item); if (is_rebase_i(opts)) - opts->reflog_message = reflog_message( + ctx->reflog_message = reflog_message( opts, command_to_string(item->command), NULL); res = do_pick_commit(r, item, opts, is_final_fixup(todo_list), @@ -4819,9 +4880,10 @@ static int pick_commits(struct repository *r, struct todo_list *todo_list, struct replay_opts *opts) { + struct replay_ctx *ctx = opts->ctx; int res = 0, reschedule = 0; - opts->reflog_message = sequencer_reflog_action(opts); + ctx->reflog_message = sequencer_reflog_action(opts); if (opts->allow_ff) assert(!(opts->signoff || opts->no_commit || opts->record_origin || should_edit(opts) || @@ -4871,6 +4933,8 @@ static int pick_commits(struct repository *r, return stopped_at_head(r); } } + strbuf_reset(&ctx->message); + ctx->have_message = 0; if (item->command <= TODO_SQUASH) { res = pick_one_commit(r, todo_list, opts, &check_todo, &reschedule); @@ -5076,6 +5140,7 @@ static int commit_staged_changes(struct repository *r, struct replay_opts *opts, struct todo_list *todo_list) { + struct replay_ctx *ctx = opts->ctx; unsigned int flags = ALLOW_EMPTY | EDIT_MSG; unsigned int final_fixup = 0, is_clean; @@ -5112,7 +5177,7 @@ static int commit_staged_changes(struct repository *r, * the commit message and if there was a squash, let the user * edit it. */ - if (!is_clean || !opts->current_fixup_count) + if (!is_clean || !ctx->current_fixup_count) ; /* this is not the final fixup */ else if (!oideq(&head, &to_amend) || !file_exists(rebase_path_stopped_sha())) { @@ -5121,20 +5186,20 @@ static int commit_staged_changes(struct repository *r, unlink(rebase_path_fixup_msg()); unlink(rebase_path_squash_msg()); unlink(rebase_path_current_fixups()); - strbuf_reset(&opts->current_fixups); - opts->current_fixup_count = 0; + strbuf_reset(&ctx->current_fixups); + ctx->current_fixup_count = 0; } } else { /* we are in a fixup/squash chain */ - const char *p = opts->current_fixups.buf; - int len = opts->current_fixups.len; + const char *p = ctx->current_fixups.buf; + int len = ctx->current_fixups.len; - opts->current_fixup_count--; + ctx->current_fixup_count--; if (!len) BUG("Incorrect current_fixups:\n%s", p); while (len && p[len - 1] != '\n') len--; - strbuf_setlen(&opts->current_fixups, len); + strbuf_setlen(&ctx->current_fixups, len); if (write_message(p, len, rebase_path_current_fixups(), 0) < 0) return error(_("could not write file: '%s'"), @@ -5151,7 +5216,7 @@ static int commit_staged_changes(struct repository *r, * actually need to re-commit with a cleaned up commit * message. */ - if (opts->current_fixup_count > 0 && + if (ctx->current_fixup_count > 0 && !is_fixup(peek_command(todo_list, 0))) { final_fixup = 1; /* @@ -5224,20 +5289,21 @@ static int commit_staged_changes(struct repository *r, unlink(rebase_path_fixup_msg()); unlink(rebase_path_squash_msg()); } - if (opts->current_fixup_count > 0) { + if (ctx->current_fixup_count > 0) { /* * Whether final fixup or not, we just cleaned up the commit * message... */ unlink(rebase_path_current_fixups()); - strbuf_reset(&opts->current_fixups); - opts->current_fixup_count = 0; + strbuf_reset(&ctx->current_fixups); + ctx->current_fixup_count = 0; } return 0; } int sequencer_continue(struct repository *r, struct replay_opts *opts) { + struct replay_ctx *ctx = opts->ctx; struct todo_list todo_list = TODO_LIST_INIT; int res; @@ -5257,7 +5323,7 @@ int sequencer_continue(struct repository *r, struct replay_opts *opts) unlink(rebase_path_dropped()); } - opts->reflog_message = reflog_message(opts, "continue", NULL); + ctx->reflog_message = reflog_message(opts, "continue", NULL); if (commit_staged_changes(r, opts, &todo_list)) { res = -1; goto release_todo_list; @@ -5309,7 +5375,7 @@ static int single_pick(struct repository *r, TODO_PICK : TODO_REVERT; item.commit = cmit; - opts->reflog_message = sequencer_reflog_action(opts); + opts->ctx->reflog_message = sequencer_reflog_action(opts); return do_pick_commit(r, &item, opts, 0, &check_todo); } diff --git a/sequencer.h b/sequencer.h index 437eabd38a..a309ddd712 100644 --- a/sequencer.h +++ b/sequencer.h @@ -31,6 +31,9 @@ enum commit_msg_cleanup_mode { COMMIT_MSG_CLEANUP_ALL }; +struct replay_ctx; +struct replay_ctx* replay_ctx_new(void); + struct replay_opts { enum replay_action action; @@ -68,10 +71,6 @@ struct replay_opts { /* Reflog */ char *reflog_action; - /* Used by fixup/squash */ - struct strbuf current_fixups; - int current_fixup_count; - /* placeholder commit for -i --root */ struct object_id squash_onto; int have_squash_onto; @@ -80,13 +79,13 @@ struct replay_opts { struct rev_info *revs; /* Private use */ - const char *reflog_message; + struct replay_ctx *ctx; }; #define REPLAY_OPTS_INIT { \ .edit = -1, \ .action = -1, \ - .current_fixups = STRBUF_INIT, \ .xopts = STRVEC_INIT, \ + .ctx = replay_ctx_new(), \ } /* @@ -16,6 +16,7 @@ #include "quote.h" #include "trace2.h" #include "worktree.h" +#include "exec-cmd.h" static int inside_git_dir = -1; static int inside_work_tree = -1; @@ -1220,6 +1221,27 @@ static int ensure_valid_ownership(const char *gitfile, return data.is_safe; } +void die_upon_dubious_ownership(const char *gitfile, const char *worktree, + const char *gitdir) +{ + struct strbuf report = STRBUF_INIT, quoted = STRBUF_INIT; + const char *path; + + if (ensure_valid_ownership(gitfile, worktree, gitdir, &report)) + return; + + strbuf_complete(&report, '\n'); + path = gitfile ? gitfile : gitdir; + sq_quote_buf_pretty("ed, path); + + die(_("detected dubious ownership in repository at '%s'\n" + "%s" + "To add an exception for this directory, call:\n" + "\n" + "\tgit config --global --add safe.directory %s"), + path, report.buf, quoted.buf); +} + static int allowed_bare_repo_cb(const char *key, const char *value, const struct config_context *ctx UNUSED, void *d) @@ -1781,6 +1803,57 @@ int daemonize(void) #endif } +struct template_dir_cb_data { + char *path; + int initialized; +}; + +static int template_dir_cb(const char *key, const char *value, + const struct config_context *ctx, void *d) +{ + struct template_dir_cb_data *data = d; + + if (strcmp(key, "init.templatedir")) + return 0; + + if (!value) { + data->path = NULL; + } else { + char *path = NULL; + + FREE_AND_NULL(data->path); + if (!git_config_pathname((const char **)&path, key, value)) + data->path = path ? path : xstrdup(value); + } + + return 0; +} + +const char *get_template_dir(const char *option_template) +{ + const char *template_dir = option_template; + + if (!template_dir) + template_dir = getenv(TEMPLATE_DIR_ENVIRONMENT); + if (!template_dir) { + static struct template_dir_cb_data data; + + if (!data.initialized) { + git_protected_config(template_dir_cb, &data); + data.initialized = 1; + } + template_dir = data.path; + } + if (!template_dir) { + static char *dir; + + if (!dir) + dir = system_path(DEFAULT_GIT_TEMPLATE_DIR); + template_dir = dir; + } + return template_dir; +} + #ifdef NO_TRUSTABLE_FILEMODE #define TEST_FILEMODE 0 #else @@ -1856,8 +1929,9 @@ static void copy_templates_1(struct strbuf *path, struct strbuf *template_path, } } -static void copy_templates(const char *template_dir, const char *init_template_dir) +static void copy_templates(const char *option_template) { + const char *template_dir = get_template_dir(option_template); struct strbuf path = STRBUF_INIT; struct strbuf template_path = STRBUF_INIT; size_t template_len; @@ -1866,16 +1940,8 @@ static void copy_templates(const char *template_dir, const char *init_template_d DIR *dir; char *to_free = NULL; - if (!template_dir) - template_dir = getenv(TEMPLATE_DIR_ENVIRONMENT); - if (!template_dir) - template_dir = init_template_dir; - if (!template_dir) - template_dir = to_free = system_path(DEFAULT_GIT_TEMPLATE_DIR); - if (!template_dir[0]) { - free(to_free); + if (!template_dir || !*template_dir) return; - } strbuf_addstr(&template_path, template_dir); strbuf_complete(&template_path, '/'); @@ -2023,7 +2089,6 @@ static int create_default_files(const char *template_path, char *path; int reinit; int filemode; - const char *init_template_dir = NULL; const char *work_tree = get_git_work_tree(); /* @@ -2035,9 +2100,7 @@ static int create_default_files(const char *template_path, * values (since we've just potentially changed what's available on * disk). */ - git_config_get_pathname("init.templatedir", &init_template_dir); - copy_templates(template_path, init_template_dir); - free((char *)init_template_dir); + copy_templates(template_path); git_config_clear(); reset_shared_repository(); git_config(git_default_config, NULL); @@ -41,6 +41,18 @@ const char *read_gitfile_gently(const char *path, int *return_error_code); const char *resolve_gitdir_gently(const char *suspect, int *return_error_code); #define resolve_gitdir(path) resolve_gitdir_gently((path), NULL) +/* + * Check if a repository is safe and die if it is not, by verifying the + * ownership of the worktree (if any), the git directory, and the gitfile (if + * any). + * + * Exemptions for known-safe repositories can be added via `safe.directory` + * config settings; for non-bare repositories, their worktree needs to be + * added, for bare ones their git directory. + */ +void die_upon_dubious_ownership(const char *gitfile, const char *worktree, + const char *gitdir); + void setup_work_tree(void); /* @@ -172,6 +184,8 @@ int verify_repository_format(const struct repository_format *format, */ void check_repository_format(struct repository_format *fmt); +const char *get_template_dir(const char *option_template); + #define INIT_DB_QUIET (1 << 0) #define INIT_DB_EXIST_OK (1 << 1) #define INIT_DB_SKIP_REFDB (1 << 2) diff --git a/submodule.c b/submodule.c index ce2d032521..05d6db9d3d 100644 --- a/submodule.c +++ b/submodule.c @@ -1015,6 +1015,9 @@ static int submodule_has_commits(struct repository *r, .super_oid = super_oid }; + if (validate_submodule_path(path) < 0) + exit(128); + oid_array_for_each_unique(commits, check_has_commit, &has_commit); if (has_commit.result) { @@ -1137,6 +1140,9 @@ static int push_submodule(const char *path, const struct string_list *push_options, int dry_run) { + if (validate_submodule_path(path) < 0) + exit(128); + if (for_each_remote_ref_submodule(path, has_remote, NULL) > 0) { struct child_process cp = CHILD_PROCESS_INIT; strvec_push(&cp.args, "push"); @@ -1186,6 +1192,9 @@ static void submodule_push_check(const char *path, const char *head, struct child_process cp = CHILD_PROCESS_INIT; int i; + if (validate_submodule_path(path) < 0) + exit(128); + strvec_push(&cp.args, "submodule--helper"); strvec_push(&cp.args, "push-check"); strvec_push(&cp.args, head); @@ -1517,6 +1526,9 @@ static struct fetch_task *fetch_task_create(struct submodule_parallel_fetch *spf struct fetch_task *task = xmalloc(sizeof(*task)); memset(task, 0, sizeof(*task)); + if (validate_submodule_path(path) < 0) + exit(128); + task->sub = submodule_from_path(spf->r, treeish_name, path); if (!task->sub) { @@ -1878,6 +1890,9 @@ unsigned is_submodule_modified(const char *path, int ignore_untracked) const char *git_dir; int ignore_cp_exit_code = 0; + if (validate_submodule_path(path) < 0) + exit(128); + strbuf_addf(&buf, "%s/.git", path); git_dir = read_gitfile(buf.buf); if (!git_dir) @@ -1954,6 +1969,9 @@ int submodule_uses_gitfile(const char *path) struct strbuf buf = STRBUF_INIT; const char *git_dir; + if (validate_submodule_path(path) < 0) + exit(128); + strbuf_addf(&buf, "%s/.git", path); git_dir = read_gitfile(buf.buf); if (!git_dir) { @@ -1993,6 +2011,9 @@ int bad_to_remove_submodule(const char *path, unsigned flags) struct strbuf buf = STRBUF_INIT; int ret = 0; + if (validate_submodule_path(path) < 0) + exit(128); + if (!file_exists(path) || is_empty_dir(path)) return 0; @@ -2043,6 +2064,9 @@ void submodule_unset_core_worktree(const struct submodule *sub) { struct strbuf config_path = STRBUF_INIT; + if (validate_submodule_path(sub->path) < 0) + exit(128); + submodule_name_to_gitdir(&config_path, the_repository, sub->name); strbuf_addstr(&config_path, "/config"); @@ -2057,6 +2081,9 @@ static int submodule_has_dirty_index(const struct submodule *sub) { struct child_process cp = CHILD_PROCESS_INIT; + if (validate_submodule_path(sub->path) < 0) + exit(128); + prepare_submodule_repo_env(&cp.env); cp.git_cmd = 1; @@ -2074,6 +2101,10 @@ static int submodule_has_dirty_index(const struct submodule *sub) static void submodule_reset_index(const char *path, const char *super_prefix) { struct child_process cp = CHILD_PROCESS_INIT; + + if (validate_submodule_path(path) < 0) + exit(128); + prepare_submodule_repo_env(&cp.env); cp.git_cmd = 1; @@ -2137,10 +2168,27 @@ int submodule_move_head(const char *path, const char *super_prefix, if (!submodule_uses_gitfile(path)) absorb_git_dir_into_superproject(path, super_prefix); + else { + char *dotgit = xstrfmt("%s/.git", path); + char *git_dir = xstrdup(read_gitfile(dotgit)); + + free(dotgit); + if (validate_submodule_git_dir(git_dir, + sub->name) < 0) + die(_("refusing to create/use '%s' in " + "another submodule's git dir"), + git_dir); + free(git_dir); + } } else { struct strbuf gitdir = STRBUF_INIT; submodule_name_to_gitdir(&gitdir, the_repository, sub->name); + if (validate_submodule_git_dir(gitdir.buf, + sub->name) < 0) + die(_("refusing to create/use '%s' in another " + "submodule's git dir"), + gitdir.buf); connect_work_tree_and_git_dir(path, gitdir.buf, 0); strbuf_release(&gitdir); @@ -2261,6 +2309,34 @@ int validate_submodule_git_dir(char *git_dir, const char *submodule_name) return 0; } +int validate_submodule_path(const char *path) +{ + char *p = xstrdup(path); + struct stat st; + int i, ret = 0; + char sep; + + for (i = 0; !ret && p[i]; i++) { + if (!is_dir_sep(p[i])) + continue; + + sep = p[i]; + p[i] = '\0'; + /* allow missing components, but no symlinks */ + ret = lstat(p, &st) || !S_ISLNK(st.st_mode) ? 0 : -1; + p[i] = sep; + if (ret) + error(_("expected '%.*s' in submodule path '%s' not to " + "be a symbolic link"), i, p, p); + } + if (!lstat(p, &st) && S_ISLNK(st.st_mode)) + ret = error(_("expected submodule path '%s' not to be a " + "symbolic link"), p); + free(p); + return ret; +} + + /* * Embeds a single submodules git directory into the superprojects git dir, * non recursively. @@ -2272,6 +2348,9 @@ static void relocate_single_git_dir_into_superproject(const char *path, struct strbuf new_gitdir = STRBUF_INIT; const struct submodule *sub; + if (validate_submodule_path(path) < 0) + exit(128); + if (submodule_uses_worktrees(path)) die(_("relocate_gitdir for submodule '%s' with " "more than one worktree not supported"), path); @@ -2313,6 +2392,9 @@ static void absorb_git_dir_into_superproject_recurse(const char *path, struct child_process cp = CHILD_PROCESS_INIT; + if (validate_submodule_path(path) < 0) + exit(128); + cp.dir = path; cp.git_cmd = 1; cp.no_stdin = 1; @@ -2337,6 +2419,10 @@ void absorb_git_dir_into_superproject(const char *path, int err_code; const char *sub_git_dir; struct strbuf gitdir = STRBUF_INIT; + + if (validate_submodule_path(path) < 0) + exit(128); + strbuf_addf(&gitdir, "%s/.git", path); sub_git_dir = resolve_gitdir_gently(gitdir.buf, &err_code); @@ -2479,6 +2565,9 @@ int submodule_to_gitdir(struct strbuf *buf, const char *submodule) const char *git_dir; int ret = 0; + if (validate_submodule_path(submodule) < 0) + exit(128); + strbuf_reset(buf); strbuf_addstr(buf, submodule); strbuf_complete(buf, '/'); diff --git a/submodule.h b/submodule.h index c55a25ca37..b50d29eba4 100644 --- a/submodule.h +++ b/submodule.h @@ -148,6 +148,11 @@ void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r, */ int validate_submodule_git_dir(char *git_dir, const char *submodule_name); +/* + * Make sure that the given submodule path does not follow symlinks. + */ +int validate_submodule_path(const char *path); + #define SUBMODULE_MOVE_HEAD_DRY_RUN (1<<0) #define SUBMODULE_MOVE_HEAD_FORCE (1<<1) int submodule_move_head(const char *path, const char *super_prefix, diff --git a/t/helper/test-path-utils.c b/t/helper/test-path-utils.c index 70396fa384..bf0e23ed50 100644 --- a/t/helper/test-path-utils.c +++ b/t/helper/test-path-utils.c @@ -7,6 +7,7 @@ #include "string-list.h" #include "trace.h" #include "utf8.h" +#include "copy.h" /* * A "string_list_each_func_t" function that normalizes an entry from diff --git a/t/lib-chunk.sh b/t/lib-chunk.sh index a7cd9c3c6d..9f01df190b 100644 --- a/t/lib-chunk.sh +++ b/t/lib-chunk.sh @@ -13,5 +13,6 @@ corrupt_chunk_file () { fn=$1; shift perl "$TEST_DIRECTORY"/lib-chunk/corrupt-chunk-file.pl \ "$@" <"$fn" >"$fn.tmp" && - mv "$fn.tmp" "$fn" + # some vintages of macOS 'mv' fails to overwrite a read-only file. + mv -f "$fn.tmp" "$fn" } diff --git a/t/t0000-basic.sh b/t/t0000-basic.sh index 6e300be2ac..98b81e4d63 100755 --- a/t/t0000-basic.sh +++ b/t/t0000-basic.sh @@ -1201,6 +1201,34 @@ test_expect_success 'very long name in the index handled sanely' ' test $len = 4098 ' +# D/F conflict checking uses an optimization when adding to the end. +# make sure it does not get confused by `a-` sorting _between_ +# `a` and `a/`. +test_expect_success 'more update-index D/F conflicts' ' + # empty the index to make sure our entry is last + git read-tree --empty && + cacheinfo=100644,$(test_oid empty_blob) && + git update-index --add --cacheinfo $cacheinfo,path5/a && + + test_must_fail git update-index --add --cacheinfo $cacheinfo,path5/a/file && + test_must_fail git update-index --add --cacheinfo $cacheinfo,path5/a/b/file && + test_must_fail git update-index --add --cacheinfo $cacheinfo,path5/a/b/c/file && + + # "a-" sorts between "a" and "a/" + git update-index --add --cacheinfo $cacheinfo,path5/a- && + + test_must_fail git update-index --add --cacheinfo $cacheinfo,path5/a/file && + test_must_fail git update-index --add --cacheinfo $cacheinfo,path5/a/b/file && + test_must_fail git update-index --add --cacheinfo $cacheinfo,path5/a/b/c/file && + + cat >expected <<-\EOF && + path5/a + path5/a- + EOF + git ls-files >actual && + test_cmp expected actual +' + test_expect_success 'test_must_fail on a failing git command' ' test_must_fail git notacommand ' diff --git a/t/t0003-attributes.sh b/t/t0003-attributes.sh index 774b52c298..71f082836a 100755 --- a/t/t0003-attributes.sh +++ b/t/t0003-attributes.sh @@ -398,13 +398,19 @@ test_expect_success 'bad attr source defaults to reading .gitattributes file' ' ) ' -test_expect_success 'bare repo defaults to reading .gitattributes from HEAD' ' +test_expect_success 'bare repo no longer defaults to reading .gitattributes from HEAD' ' test_when_finished rm -rf test bare_with_gitattribute && git init test && test_commit -C test gitattributes .gitattributes "f/path test=val" && git clone --bare test bare_with_gitattribute && - echo "f/path: test: val" >expect && + + echo "f/path: test: unspecified" >expect && git -C bare_with_gitattribute check-attr test -- f/path >actual && + test_cmp expect actual && + + echo "f/path: test: val" >expect && + git -C bare_with_gitattribute -c attr.tree=HEAD \ + check-attr test -- f/path >actual && test_cmp expect actual ' @@ -572,6 +578,16 @@ test_expect_success EXPENSIVE 'large attributes file ignored in index' ' test_cmp expect err ' +test_expect_success EXPENSIVE 'large attributes blob ignored' ' + test_when_finished "git update-index --remove .gitattributes" && + blob=$(dd if=/dev/zero bs=1048576 count=101 2>/dev/null | git hash-object -w --stdin) && + git update-index --add --cacheinfo 100644,$blob,.gitattributes && + tree="$(git write-tree)" && + git check-attr --cached --all --source="$tree" path >/dev/null 2>err && + echo "warning: ignoring overly large gitattributes blob ${SQ}.gitattributes${SQ}" >expect && + test_cmp expect err +' + test_expect_success 'builtin object mode attributes work (dir and regular paths)' ' >normal && attr_check_object_mode normal 100644 && diff --git a/t/t0033-safe-directory.sh b/t/t0033-safe-directory.sh index dc3496897a..11c3e8f28e 100755 --- a/t/t0033-safe-directory.sh +++ b/t/t0033-safe-directory.sh @@ -80,4 +80,28 @@ test_expect_success 'safe.directory in included file' ' git status ' +test_expect_success 'local clone of unowned repo refused in unsafe directory' ' + test_when_finished "rm -rf source" && + git init source && + ( + sane_unset GIT_TEST_ASSUME_DIFFERENT_OWNER && + test_commit -C source initial + ) && + test_must_fail git clone --local source target && + test_path_is_missing target +' + +test_expect_success 'local clone of unowned repo accepted in safe directory' ' + test_when_finished "rm -rf source" && + git init source && + ( + sane_unset GIT_TEST_ASSUME_DIFFERENT_OWNER && + test_commit -C source initial + ) && + test_must_fail git clone --local source target && + git config --global --add safe.directory "$(pwd)/source/.git" && + git clone --local source target && + test_path_is_dir target +' + test_done diff --git a/t/t0068-for-each-repo.sh b/t/t0068-for-each-repo.sh index 4b90b74d5d..95019e01ed 100755 --- a/t/t0068-for-each-repo.sh +++ b/t/t0068-for-each-repo.sh @@ -59,4 +59,20 @@ test_expect_success 'error on NULL value for config keys' ' test_cmp expect actual ' +test_expect_success '--keep-going' ' + git config keep.going non-existing && + git config --add keep.going . && + + test_must_fail git for-each-repo --config=keep.going \ + -- branch >out 2>err && + test_grep "cannot change to .*non-existing" err && + test_must_be_empty out && + + test_must_fail git for-each-repo --config=keep.going --keep-going \ + -- branch >out 2>err && + test_grep "cannot change to .*non-existing" err && + git branch >expect && + test_cmp expect out +' + test_done diff --git a/t/t0411-clone-from-partial.sh b/t/t0411-clone-from-partial.sh new file mode 100755 index 0000000000..c98d501869 --- /dev/null +++ b/t/t0411-clone-from-partial.sh @@ -0,0 +1,78 @@ +#!/bin/sh + +test_description='check that local clone does not fetch from promisor remotes' + +. ./test-lib.sh + +test_expect_success 'create evil repo' ' + git init tmp && + test_commit -C tmp a && + git -C tmp config uploadpack.allowfilter 1 && + git clone --filter=blob:none --no-local --no-checkout tmp evil && + rm -rf tmp && + + git -C evil config remote.origin.uploadpack \"\$TRASH_DIRECTORY/fake-upload-pack\" && + write_script fake-upload-pack <<-\EOF && + echo >&2 "fake-upload-pack running" + >"$TRASH_DIRECTORY/script-executed" + exit 1 + EOF + export TRASH_DIRECTORY && + + # empty shallow file disables local clone optimization + >evil/.git/shallow +' + +test_expect_success 'local clone must not fetch from promisor remote and execute script' ' + rm -f script-executed && + test_must_fail git clone \ + --upload-pack="GIT_TEST_ASSUME_DIFFERENT_OWNER=true git-upload-pack" \ + evil clone1 2>err && + test_grep "detected dubious ownership" err && + test_grep ! "fake-upload-pack running" err && + test_path_is_missing script-executed +' + +test_expect_success 'clone from file://... must not fetch from promisor remote and execute script' ' + rm -f script-executed && + test_must_fail git clone \ + --upload-pack="GIT_TEST_ASSUME_DIFFERENT_OWNER=true git-upload-pack" \ + "file://$(pwd)/evil" clone2 2>err && + test_grep "detected dubious ownership" err && + test_grep ! "fake-upload-pack running" err && + test_path_is_missing script-executed +' + +test_expect_success 'fetch from file://... must not fetch from promisor remote and execute script' ' + rm -f script-executed && + test_must_fail git fetch \ + --upload-pack="GIT_TEST_ASSUME_DIFFERENT_OWNER=true git-upload-pack" \ + "file://$(pwd)/evil" 2>err && + test_grep "detected dubious ownership" err && + test_grep ! "fake-upload-pack running" err && + test_path_is_missing script-executed +' + +test_expect_success 'pack-objects should fetch from promisor remote and execute script' ' + rm -f script-executed && + echo "HEAD" | test_must_fail git -C evil pack-objects --revs --stdout >/dev/null 2>err && + test_grep "fake-upload-pack running" err && + test_path_is_file script-executed +' + +test_expect_success 'clone from promisor remote does not lazy-fetch by default' ' + rm -f script-executed && + test_must_fail git clone evil no-lazy 2>err && + test_grep "lazy fetching disabled" err && + test_path_is_missing script-executed +' + +test_expect_success 'promisor lazy-fetching can be re-enabled' ' + rm -f script-executed && + test_must_fail env GIT_NO_LAZY_FETCH=0 \ + git clone evil lazy-ok 2>err && + test_grep "fake-upload-pack running" err && + test_path_is_file script-executed +' + +test_done diff --git a/t/t1350-config-hooks-path.sh b/t/t1350-config-hooks-path.sh index f6dc83e2aa..45a0492917 100755 --- a/t/t1350-config-hooks-path.sh +++ b/t/t1350-config-hooks-path.sh @@ -41,4 +41,11 @@ test_expect_success 'git rev-parse --git-path hooks' ' test .git/custom-hooks/abc = "$(cat actual)" ' +test_expect_success 'core.hooksPath=/dev/null' ' + git clone -c core.hooksPath=/dev/null . no-templates && + value="$(git -C no-templates config --local core.hooksPath)" && + # The Bash used by Git for Windows rewrites `/dev/null` to `nul` + { test /dev/null = "$value" || test nul = "$value"; } +' + test_done diff --git a/t/t3428-rebase-signoff.sh b/t/t3428-rebase-signoff.sh index 1bebd1ce74..6f57aed9fa 100755 --- a/t/t3428-rebase-signoff.sh +++ b/t/t3428-rebase-signoff.sh @@ -5,12 +5,17 @@ test_description='git rebase --signoff This test runs git rebase --signoff and make sure that it works. ' -TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh +. "$TEST_DIRECTORY"/lib-rebase.sh test_expect_success 'setup' ' git commit --allow-empty -m "Initial empty commit" && test_commit first file a && + test_commit second file && + git checkout -b conflict-branch first && + test_commit file-2 file-2 && + test_commit conflict file && + test_commit third file && ident="$GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>" && @@ -28,6 +33,22 @@ test_expect_success 'setup' ' Signed-off-by: $ident EOF + # Expected commit message after conflict resolution for rebase --signoff + cat >expected-signed-conflict <<-EOF && + third + + Signed-off-by: $ident + + conflict + + Signed-off-by: $ident + + file-2 + + Signed-off-by: $ident + + EOF + # Expected commit message after rebase without --signoff (or with --no-signoff) cat >expected-unsigned <<-EOF && first @@ -39,8 +60,12 @@ test_expect_success 'setup' ' # We configure an alias to do the rebase --signoff so that # on the next subtest we can show that --no-signoff overrides the alias test_expect_success 'rebase --apply --signoff adds a sign-off line' ' - git rbs --apply HEAD^ && - test_commit_message HEAD expected-signed + test_must_fail git rbs --apply second third && + git checkout --theirs file && + git add file && + git rebase --continue && + git log --format=%B -n3 >actual && + test_cmp expected-signed-conflict actual ' test_expect_success 'rebase --no-signoff does not add a sign-off line' ' @@ -51,28 +76,65 @@ test_expect_success 'rebase --no-signoff does not add a sign-off line' ' test_expect_success 'rebase --exec --signoff adds a sign-off line' ' test_when_finished "rm exec" && - git commit --amend -m "first" && - git rebase --exec "touch exec" --signoff HEAD^ && + git rebase --exec "touch exec" --signoff first^ first && test_path_is_file exec && test_commit_message HEAD expected-signed ' test_expect_success 'rebase --root --signoff adds a sign-off line' ' - git commit --amend -m "first" && + git checkout first && git rebase --root --keep-empty --signoff && test_commit_message HEAD^ expected-initial-signed && test_commit_message HEAD expected-signed ' -test_expect_success 'rebase -i --signoff fails' ' - git commit --amend -m "first" && - git rebase -i --signoff HEAD^ && - test_commit_message HEAD expected-signed +test_expect_success 'rebase -m --signoff adds a sign-off line' ' + test_must_fail git rebase -m --signoff second third && + git checkout --theirs file && + git add file && + GIT_EDITOR="sed -n /Conflicts:/,/^\\\$/p >actual" \ + git rebase --continue && + cat >expect <<-\EOF && + # Conflicts: + # file + + EOF + test_cmp expect actual && + git log --format=%B -n3 >actual && + test_cmp expected-signed-conflict actual ' -test_expect_success 'rebase -m --signoff fails' ' - git commit --amend -m "first" && - git rebase -m --signoff HEAD^ && - test_commit_message HEAD expected-signed +test_expect_success 'rebase -i --signoff adds a sign-off line when editing commit' ' + ( + set_fake_editor && + FAKE_LINES="edit 1 edit 3 edit 2" \ + git rebase -i --signoff first third + ) && + echo a >a && + git add a && + test_must_fail git rebase --continue && + git checkout --ours file && + echo b >a && + git add a file && + git rebase --continue && + echo c >a && + git add a && + git log --format=%B -n3 >actual && + cat >expect <<-EOF && + conflict + + Signed-off-by: $ident + + third + + Signed-off-by: $ident + + file-2 + + Signed-off-by: $ident + + EOF + test_cmp expect actual ' + test_done diff --git a/t/t3434-rebase-i18n.sh b/t/t3434-rebase-i18n.sh index e6fef696bb..a4e482d2cd 100755 --- a/t/t3434-rebase-i18n.sh +++ b/t/t3434-rebase-i18n.sh @@ -71,7 +71,7 @@ test_rebase_continue_update_encode () { git config i18n.commitencoding $new && test_must_fail git rebase -m main && test -f .git/rebase-merge/message && - git stripspace <.git/rebase-merge/message >two.t && + git stripspace -s <.git/rebase-merge/message >two.t && git add two.t && git rebase --continue && compare_msg $msgfile $old $new && diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh index 00db82fb24..a7f71f8126 100755 --- a/t/t3903-stash.sh +++ b/t/t3903-stash.sh @@ -393,6 +393,15 @@ test_expect_success 'stash --staged' ' test bar,bar4 = $(cat file),$(cat file2) ' +test_expect_success 'stash --staged with binary file' ' + printf "\0" >file && + git add file && + git stash --staged && + git stash pop && + printf "\0" >expect && + test_cmp expect file +' + test_expect_success 'dont assume push with non-option args' ' test_must_fail git stash -q drop 2>err && test_grep -e "subcommand wasn'\''t specified; '\''push'\'' can'\''t be assumed due to unexpected token '\''drop'\''" err diff --git a/t/t4046-diff-unmerged.sh b/t/t4046-diff-unmerged.sh index ffaf69335f..fb8c51746e 100755 --- a/t/t4046-diff-unmerged.sh +++ b/t/t4046-diff-unmerged.sh @@ -20,13 +20,15 @@ test_expect_success setup ' for t in o x do path="$b$o$t" && - case "$path" in ooo) continue ;; esac && - paths="$paths$path " && - p=" $path" && - case "$b" in x) echo "$m1$p" ;; esac && - case "$o" in x) echo "$m2$p" ;; esac && - case "$t" in x) echo "$m3$p" ;; esac || - return 1 + if test "$path" != ooo + then + paths="$paths$path " && + p=" $path" && + case "$b" in x) echo "$m1$p" ;; esac && + case "$o" in x) echo "$m2$p" ;; esac && + case "$t" in x) echo "$m3$p" ;; esac || + return 1 + fi done done done >ls-files-s.expect && diff --git a/t/t5001-archive-attr.sh b/t/t5001-archive-attr.sh index eaf959d8f6..7310774af5 100755 --- a/t/t5001-archive-attr.sh +++ b/t/t5001-archive-attr.sh @@ -133,7 +133,8 @@ test_expect_success 'git archive vs. bare' ' ' test_expect_success 'git archive with worktree attributes, bare' ' - (cd bare && git archive --worktree-attributes HEAD) >bare-worktree.tar && + (cd bare && + git -c attr.tree=HEAD archive --worktree-attributes HEAD) >bare-worktree.tar && (mkdir bare-worktree && cd bare-worktree && "$TAR" xf -) <bare-worktree.tar ' diff --git a/t/t5326-multi-pack-bitmaps.sh b/t/t5326-multi-pack-bitmaps.sh index 5d7d321840..cc7220b6c0 100755 --- a/t/t5326-multi-pack-bitmaps.sh +++ b/t/t5326-multi-pack-bitmaps.sh @@ -434,6 +434,27 @@ test_expect_success 'tagged commits are selected for bitmapping' ' ) ' +test_expect_success 'do not follow replace objects for MIDX bitmap' ' + rm -fr repo && + git init repo && + test_when_finished "rm -fr repo" && + ( + cd repo && + + test_commit A && + test_commit B && + git checkout --orphan=orphan A && + test_commit orphan && + + git replace A HEAD && + git repack -ad --write-midx --write-bitmap-index && + + # generating reachability bitmaps with replace refs + # enabled will result in broken clones + git clone --no-local --bare . clone.git + ) +' + corrupt_file () { chmod a+w "$1" && printf "bogus" | dd of="$1" bs=1 seek="12" conv=notrunc diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh index 33d34d5ae9..9441793d06 100755 --- a/t/t5510-fetch.sh +++ b/t/t5510-fetch.sh @@ -1252,6 +1252,30 @@ EOF test_cmp fatal-expect fatal-actual ' +test_expect_success SYMLINKS 'clone does not get confused by a D/F conflict' ' + git init df-conflict && + ( + cd df-conflict && + ln -s .git a && + git add a && + test_tick && + git commit -m symlink && + test_commit a- && + rm a && + mkdir -p a/hooks && + write_script a/hooks/post-checkout <<-EOF && + echo WHOOPSIE >&2 + echo whoopsie >"$TRASH_DIRECTORY"/whoops + EOF + git add a/hooks/post-checkout && + test_tick && + git commit -m post-checkout + ) && + git clone df-conflict clone 2>err && + test_grep ! WHOOPS err && + test_path_is_missing whoops +' + . "$TEST_DIRECTORY"/lib-httpd.sh start_httpd diff --git a/t/t5601-clone.sh b/t/t5601-clone.sh index ca43185681..cc0b953f14 100755 --- a/t/t5601-clone.sh +++ b/t/t5601-clone.sh @@ -650,6 +650,21 @@ test_expect_success CASE_INSENSITIVE_FS 'colliding file detection' ' test_grep "the following paths have collided" icasefs/warning ' +test_expect_success CASE_INSENSITIVE_FS,SYMLINKS \ + 'colliding symlink/directory keeps directory' ' + git init icasefs-colliding-symlink && + ( + cd icasefs-colliding-symlink && + a=$(printf a | git hash-object -w --stdin) && + printf "100644 %s 0\tA/dir/b\n120000 %s 0\ta\n" $a $a >idx && + git update-index --index-info <idx && + test_tick && + git commit -m initial + ) && + git clone icasefs-colliding-symlink icasefs-colliding-symlink-clone && + test_file_not_empty icasefs-colliding-symlink-clone/A/dir/b +' + test_expect_success 'clone with GIT_DEFAULT_HASH' ' ( sane_unset GIT_DEFAULT_HASH && diff --git a/t/t7400-submodule-basic.sh b/t/t7400-submodule-basic.sh index 5c4a89df5c..981488885f 100755 --- a/t/t7400-submodule-basic.sh +++ b/t/t7400-submodule-basic.sh @@ -1451,4 +1451,35 @@ test_expect_success 'recursive clone respects -q' ' test_must_be_empty actual ' +test_expect_success '`submodule init` and `init.templateDir`' ' + mkdir -p tmpl/hooks && + write_script tmpl/hooks/post-checkout <<-EOF && + echo HOOK-RUN >&2 + echo I was here >hook.run + exit 1 + EOF + + test_config init.templateDir "$(pwd)/tmpl" && + test_when_finished \ + "git config --global --unset init.templateDir || true" && + ( + sane_unset GIT_TEMPLATE_DIR && + NO_SET_GIT_TEMPLATE_DIR=t && + export NO_SET_GIT_TEMPLATE_DIR && + + git config --global init.templateDir "$(pwd)/tmpl" && + test_must_fail git submodule \ + add "$submodurl" sub-global 2>err && + git config --global --unset init.templateDir && + test_grep HOOK-RUN err && + test_path_is_file sub-global/hook.run && + + git config init.templateDir "$(pwd)/tmpl" && + git submodule add "$submodurl" sub-local 2>err && + git config --unset init.templateDir && + test_grep ! HOOK-RUN err && + test_path_is_missing sub-local/hook.run + ) +' + test_done diff --git a/t/t7406-submodule-update.sh b/t/t7406-submodule-update.sh index 8491b8c58b..297c6c3b5c 100755 --- a/t/t7406-submodule-update.sh +++ b/t/t7406-submodule-update.sh @@ -1202,4 +1202,52 @@ test_expect_success 'commit with staged submodule change with ignoreSubmodules a add_submodule_commit_and_validate ' +test_expect_success CASE_INSENSITIVE_FS,SYMLINKS \ + 'submodule paths must not follow symlinks' ' + + # This is only needed because we want to run this in a self-contained + # test without having to spin up an HTTP server; However, it would not + # be needed in a real-world scenario where the submodule is simply + # hosted on a public site. + test_config_global protocol.file.allow always && + + # Make sure that Git tries to use symlinks on Windows + test_config_global core.symlinks true && + + tell_tale_path="$PWD/tell.tale" && + git init hook && + ( + cd hook && + mkdir -p y/hooks && + write_script y/hooks/post-checkout <<-EOF && + echo HOOK-RUN >&2 + echo hook-run >"$tell_tale_path" + EOF + git add y/hooks/post-checkout && + test_tick && + git commit -m post-checkout + ) && + + hook_repo_path="$(pwd)/hook" && + git init captain && + ( + cd captain && + git submodule add --name x/y "$hook_repo_path" A/modules/x && + test_tick && + git commit -m add-submodule && + + printf .git >dotgit.txt && + git hash-object -w --stdin <dotgit.txt >dot-git.hash && + printf "120000 %s 0\ta\n" "$(cat dot-git.hash)" >index.info && + git update-index --index-info <index.info && + test_tick && + git commit -m add-symlink + ) && + + test_path_is_missing "$tell_tale_path" && + git clone --recursive captain hooked 2>err && + test_grep ! HOOK-RUN err && + test_path_is_missing "$tell_tale_path" +' + test_done diff --git a/t/t7423-submodule-symlinks.sh b/t/t7423-submodule-symlinks.sh new file mode 100755 index 0000000000..3d3c7af3ce --- /dev/null +++ b/t/t7423-submodule-symlinks.sh @@ -0,0 +1,67 @@ +#!/bin/sh + +test_description='check that submodule operations do not follow symlinks' + +. ./test-lib.sh + +test_expect_success 'prepare' ' + git config --global protocol.file.allow always && + test_commit initial && + git init upstream && + test_commit -C upstream upstream submodule_file && + git submodule add ./upstream a/sm && + test_tick && + git commit -m submodule +' + +test_expect_success SYMLINKS 'git submodule update must not create submodule behind symlink' ' + rm -rf a b && + mkdir b && + ln -s b a && + test_path_is_missing b/sm && + test_must_fail git submodule update && + test_path_is_missing b/sm +' + +test_expect_success SYMLINKS,CASE_INSENSITIVE_FS 'git submodule update must not create submodule behind symlink on case insensitive fs' ' + rm -rf a b && + mkdir b && + ln -s b A && + test_must_fail git submodule update && + test_path_is_missing b/sm +' + +prepare_symlink_to_repo() { + rm -rf a && + mkdir a && + git init a/target && + git -C a/target fetch ../../upstream && + ln -s target a/sm +} + +test_expect_success SYMLINKS 'git restore --recurse-submodules must not be confused by a symlink' ' + prepare_symlink_to_repo && + test_must_fail git restore --recurse-submodules a/sm && + test_path_is_missing a/sm/submodule_file && + test_path_is_dir a/target/.git && + test_path_is_missing a/target/submodule_file +' + +test_expect_success SYMLINKS 'git restore --recurse-submodules must not migrate git dir of symlinked repo' ' + prepare_symlink_to_repo && + rm -rf .git/modules && + test_must_fail git restore --recurse-submodules a/sm && + test_path_is_dir a/target/.git && + test_path_is_missing .git/modules/a/sm && + test_path_is_missing a/target/submodule_file +' + +test_expect_success SYMLINKS 'git checkout -f --recurse-submodules must not migrate git dir of symlinked repo when removing submodule' ' + prepare_symlink_to_repo && + rm -rf .git/modules && + test_must_fail git checkout -f --recurse-submodules initial && + test_path_is_dir a/target/.git && + test_path_is_missing .git/modules/a/sm +' + +test_done diff --git a/t/t7450-bad-git-dotfiles.sh b/t/t7450-bad-git-dotfiles.sh index 46d4fb0354..4a9c22c9e2 100755 --- a/t/t7450-bad-git-dotfiles.sh +++ b/t/t7450-bad-git-dotfiles.sh @@ -320,7 +320,7 @@ test_expect_success WINDOWS 'prevent git~1 squatting on Windows' ' fi ' -test_expect_success 'git dirs of sibling submodules must not be nested' ' +test_expect_success 'setup submodules with nested git dirs' ' git init nested && test_commit -C nested nested && ( @@ -338,9 +338,39 @@ test_expect_success 'git dirs of sibling submodules must not be nested' ' git add .gitmodules thing1 thing2 && test_tick && git commit -m nested - ) && + ) +' + +test_expect_success 'git dirs of sibling submodules must not be nested' ' test_must_fail git clone --recurse-submodules nested clone 2>err && test_grep "is inside git dir" err ' +test_expect_success 'submodule git dir nesting detection must work with parallel cloning' ' + test_must_fail git clone --recurse-submodules --jobs=2 nested clone_parallel 2>err && + cat err && + grep -E "(already exists|is inside git dir|not a git repository)" err && + { + test_path_is_missing .git/modules/hippo/HEAD || + test_path_is_missing .git/modules/hippo/hooks/HEAD + } +' + +test_expect_success 'checkout -f --recurse-submodules must not use a nested gitdir' ' + git clone nested nested_checkout && + ( + cd nested_checkout && + git submodule init && + git submodule update thing1 && + mkdir -p .git/modules/hippo/hooks/refs && + mkdir -p .git/modules/hippo/hooks/objects/info && + echo "../../../../objects" >.git/modules/hippo/hooks/objects/info/alternates && + echo "ref: refs/heads/master" >.git/modules/hippo/hooks/HEAD + ) && + test_must_fail git -C nested_checkout checkout -f --recurse-submodules HEAD 2>err && + cat err && + grep "is inside git dir" err && + test_path_is_missing nested_checkout/thing2/.git +' + test_done diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh index 0943dfa18a..8595489ceb 100755 --- a/t/t7900-maintenance.sh +++ b/t/t7900-maintenance.sh @@ -639,9 +639,9 @@ test_expect_success 'start from empty cron table' ' # start registers the repo git config --get --global --fixed-value maintenance.repo "$(pwd)" && - grep "for-each-repo --config=maintenance.repo maintenance run --schedule=daily" cron.txt && - grep "for-each-repo --config=maintenance.repo maintenance run --schedule=hourly" cron.txt && - grep "for-each-repo --config=maintenance.repo maintenance run --schedule=weekly" cron.txt + grep "for-each-repo --keep-going --config=maintenance.repo maintenance run --schedule=daily" cron.txt && + grep "for-each-repo --keep-going --config=maintenance.repo maintenance run --schedule=hourly" cron.txt && + grep "for-each-repo --keep-going --config=maintenance.repo maintenance run --schedule=weekly" cron.txt ' test_expect_success 'stop from existing schedule' ' |
