diff options
102 files changed, 1889 insertions, 2214 deletions
diff --git a/.gitattributes b/.gitattributes index b0044cf272..158c3d45c4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,17 +1,17 @@ * whitespace=!indent,trail,space *.[ch] whitespace=indent,trail,space diff=cpp -*.sh whitespace=indent,trail,space eol=lf -*.perl eol=lf diff=perl -*.pl eof=lf diff=perl -*.pm eol=lf diff=perl -*.py eol=lf diff=python -*.bat eol=crlf +*.sh whitespace=indent,trail,space text eol=lf +*.perl text eol=lf diff=perl +*.pl text eof=lf diff=perl +*.pm text eol=lf diff=perl +*.py text eol=lf diff=python +*.bat text eol=crlf CODE_OF_CONDUCT.md -whitespace -/Documentation/**/*.txt eol=lf -/command-list.txt eol=lf -/GIT-VERSION-GEN eol=lf -/mergetools/* eol=lf -/t/oid-info/* eol=lf +/Documentation/**/*.txt text eol=lf +/command-list.txt text eol=lf +/GIT-VERSION-GEN text eol=lf +/mergetools/* text eol=lf +/t/oid-info/* text eol=lf /Documentation/git-merge.txt conflict-marker-size=32 /Documentation/gitk.txt conflict-marker-size=32 /Documentation/user-manual.txt conflict-marker-size=32 diff --git a/.gitignore b/.gitignore index 6782f3ceca..e875c59054 100644 --- a/.gitignore +++ b/.gitignore @@ -14,7 +14,6 @@ /bin-wrappers/ /git /git-add -/git-add--interactive /git-am /git-annotate /git-apply diff --git a/Documentation/RelNotes/2.30.8.txt b/Documentation/RelNotes/2.30.8.txt new file mode 100644 index 0000000000..5ed3efbd6a --- /dev/null +++ b/Documentation/RelNotes/2.30.8.txt @@ -0,0 +1,51 @@ +Git v2.30.8 Release Notes +========================= + +This release addresses the security issues CVE-2023-22490 and +CVE-2023-23946. + + +Fixes since v2.30.7 +------------------- + + * CVE-2023-22490: + + Using a specially-crafted repository, Git can be tricked into using + its local clone optimization even when using a non-local transport. + Though Git will abort local clones whose source $GIT_DIR/objects + directory contains symbolic links (c.f., CVE-2022-39253), the objects + directory itself may still be a symbolic link. + + These two may be combined to include arbitrary files based on known + paths on the victim's filesystem within the malicious repository's + working copy, allowing for data exfiltration in a similar manner as + CVE-2022-39253. + + * CVE-2023-23946: + + By feeding a crafted input to "git apply", a path outside the + working tree can be overwritten as the user who is running "git + apply". + + * A mismatched type in `attr.c::read_attr_from_index()` which could + cause Git to errantly reject attributes on Windows and 32-bit Linux + has been corrected. + +Credit for finding CVE-2023-22490 goes to yvvdwf, and the fix was +developed by Taylor Blau, with additional help from others on the +Git security mailing list. + +Credit for finding CVE-2023-23946 goes to Joern Schneeweisz, and the +fix was developed by Patrick Steinhardt. + + +Johannes Schindelin (1): + attr: adjust a mismatched data type + +Patrick Steinhardt (1): + apply: fix writing behind newly created symbolic links + +Taylor Blau (3): + t5619: demonstrate clone_local() with ambiguous transport + clone: delay picking a transport until after get_repo_path() + dir-iterator: prevent top-level symlinks without FOLLOW_SYMLINKS diff --git a/Documentation/RelNotes/2.31.7.txt b/Documentation/RelNotes/2.31.7.txt new file mode 100644 index 0000000000..dd44d5bc62 --- /dev/null +++ b/Documentation/RelNotes/2.31.7.txt @@ -0,0 +1,6 @@ +Git v2.31.7 Release Notes +========================= + +This release merges up the fixes that appear in v2.30.8 to +address the security issues CVE-2023-22490 and CVE-2023-23946; +see the release notes for that version for details. diff --git a/Documentation/RelNotes/2.32.6.txt b/Documentation/RelNotes/2.32.6.txt new file mode 100644 index 0000000000..fd659612e3 --- /dev/null +++ b/Documentation/RelNotes/2.32.6.txt @@ -0,0 +1,6 @@ +Git v2.32.6 Release Notes +========================= + +This release merges up the fixes that appear in v2.30.8 and v2.31.7 +to address the security issues CVE-2023-22490 and CVE-2023-23946; +see the release notes for these versions for details. diff --git a/Documentation/RelNotes/2.33.7.txt b/Documentation/RelNotes/2.33.7.txt new file mode 100644 index 0000000000..078a837cb4 --- /dev/null +++ b/Documentation/RelNotes/2.33.7.txt @@ -0,0 +1,7 @@ +Git v2.33.7 Release Notes +========================= + +This release merges up the fixes that appear in v2.30.8, v2.31.7 +and v2.32.6 to address the security issues CVE-2023-22490 and +CVE-2023-23946; see the release notes for these versions for +details. diff --git a/Documentation/RelNotes/2.34.7.txt b/Documentation/RelNotes/2.34.7.txt new file mode 100644 index 0000000000..88898adacc --- /dev/null +++ b/Documentation/RelNotes/2.34.7.txt @@ -0,0 +1,7 @@ +Git v2.34.7 Release Notes +========================= + +This release merges up the fixes that appear in v2.30.8, v2.31.7, +v2.32.6 and v2.33.7 to address the security issues CVE-2023-22490 +and CVE-2023-23946; see the release notes for these versions +for details. diff --git a/Documentation/RelNotes/2.35.7.txt b/Documentation/RelNotes/2.35.7.txt new file mode 100644 index 0000000000..42baabfc3b --- /dev/null +++ b/Documentation/RelNotes/2.35.7.txt @@ -0,0 +1,7 @@ +Git v2.35.7 Release Notes +========================= + +This release merges up the fixes that appear in v2.30.8, v2.31.7, +v2.32.6, v2.33.7 and v2.34.7 to address the security issues +CVE-2023-22490 and CVE-2023-23946; see the release notes for +these versions for details. diff --git a/Documentation/RelNotes/2.36.5.txt b/Documentation/RelNotes/2.36.5.txt new file mode 100644 index 0000000000..8a098c7916 --- /dev/null +++ b/Documentation/RelNotes/2.36.5.txt @@ -0,0 +1,7 @@ +Git v2.36.5 Release Notes +========================= + +This release merges up the fixes that appear in v2.30.8, v2.31.7, +v2.32.6, v2.33.7, v2.34.7 and v2.35.7 to address the security +issues CVE-2023-22490 and CVE-2023-23946; see the release notes +for these versions for details. diff --git a/Documentation/RelNotes/2.37.6.txt b/Documentation/RelNotes/2.37.6.txt new file mode 100644 index 0000000000..51dc149711 --- /dev/null +++ b/Documentation/RelNotes/2.37.6.txt @@ -0,0 +1,7 @@ +Git v2.37.6 Release Notes +========================= + +This release merges up the fixes that appear in v2.30.8, v2.31.7, +v2.32.6, v2.33.7, v2.34.7, v2.35.7 and v2.36.5 to address the +security issues CVE-2023-22490 and CVE-2023-23946; see the release +notes for these versions for details. diff --git a/Documentation/RelNotes/2.38.4.txt b/Documentation/RelNotes/2.38.4.txt new file mode 100644 index 0000000000..fdfde22022 --- /dev/null +++ b/Documentation/RelNotes/2.38.4.txt @@ -0,0 +1,7 @@ +Git v2.38.4 Release Notes +========================= + +This release merges up the fixes that appear in v2.30.8, v2.31.7, +v2.32.6, v2.33.7, v2.34.7, v2.35.7, v2.36.5 and v2.37.6 to +address the security issues CVE-2023-22490 and CVE-2023-23946; +see the release notes for these versions for details. diff --git a/Documentation/RelNotes/2.39.2.txt b/Documentation/RelNotes/2.39.2.txt new file mode 100644 index 0000000000..ebb9900bc5 --- /dev/null +++ b/Documentation/RelNotes/2.39.2.txt @@ -0,0 +1,7 @@ +Git v2.39.2 Release Notes +========================= + +This release merges up the fixes that appear in v2.30.8, v2.31.7, +v2.32.6, v2.33.7, v2.34.7, v2.35.7, v2.36.5, v2.37.6 and v2.38.4 +to address the security issues CVE-2023-22490 and CVE-2023-23946; +see the release notes for these versions for details. diff --git a/Documentation/RelNotes/2.39.3.txt b/Documentation/RelNotes/2.39.3.txt new file mode 100644 index 0000000000..dddff53627 --- /dev/null +++ b/Documentation/RelNotes/2.39.3.txt @@ -0,0 +1,58 @@ +Git v2.39.3 Release Notes +========================= + +This release is primarily to merge fixes accumulated on the 'master' +front to prepare for 2.40 release that are still relevant to 2.39.x +maintenance track. + +Fixes since v2.39.2 +------------------- + + * Stop running win+VS build by default. + + * CI updates. We probably want a clean-up to move the long shell + script embedded in yaml file into a separate file, but that can + come later. + + * Avoid unnecessary builds in CI, with settings configured in + ci-config. + + * Redefining system functions for a few functions did not follow our + usual "implement git_foo() and #define foo(args) git_foo(args)" + pattern, which has broken build for some folks. + + * Deal with a few deprecation warning from cURL library. + + * Newer regex library macOS stopped enabling GNU-like enhanced BRE, + where '\(A\|B\)' works as alternation, unless explicitly asked with + the REG_ENHANCED flag. "git grep" now can be compiled to do so, to + retain the old behaviour. + + * When given a pattern that matches an empty string at the end of a + line, the code to parse the "git diff" line-ranges fell into an + infinite loop, which has been corrected. + + * Fix the sequence to fsync $GIT_DIR/packed-refs file that forgot to + flush its output to the disk.. + + * "git diff --relative" did not mix well with "git diff --ext-diff", + which has been corrected. + + * The logic to see if we are using the "cone" mode by checking the + sparsity patterns has been tightened to avoid mistaking a pattern + that names a single file as specifying a cone. + + * Doc update for environment variables set when hooks are invoked. + + * Document ORIG_HEAD a bit more. + + * "git ls-tree --format='%(path) %(path)' $tree $path" showed the + path three times, which has been corrected. + + * Document that "branch -f <branch>" disables only the safety to + avoid recreating an existing branch. + + * Clarify how "checkout -b/-B" and "git branch [-f]" are similar but + different in the documentation. + +Also contains minor documentation updates and code clean-ups. diff --git a/Documentation/RelNotes/2.40.0.txt b/Documentation/RelNotes/2.40.0.txt index edbbe7d049..d1428f5fb8 100644 --- a/Documentation/RelNotes/2.40.0.txt +++ b/Documentation/RelNotes/2.40.0.txt @@ -49,6 +49,11 @@ UI, Workflows & Features * "scalar" warns but continues when its periodic maintenance feature cannot be enabled. + * The bundle-URI subsystem adds support for creation-token heuristics + to help incremental fetches. + + * Userdiff regexp update for Java language. + Performance, Internal Implementation, Development Support etc. @@ -69,12 +74,10 @@ Performance, Internal Implementation, Development Support etc. the submodule--helper. * Stop running win+VS build by default. - (merge a0da6deeec js/ci-disable-cmake-by-default later to maint). * CI updates. We probably want a clean-up to move the long shell script embedded in yaml file into a separate file, but that can come later. - (merge 4542582e59 cw/ci-whitespace later to maint). * Use `git diff --no-index` as a test_cmp on Windows. @@ -84,14 +87,21 @@ Performance, Internal Implementation, Development Support etc. * Avoid unnecessary builds in CI, with settings configured in ci-config. - (merge eb5b03a9c0 tb/ci-concurrency later to maint). + + * Plug leaks in sequencer subsystem and its users. + + * In-tree .gitattributes update to match the way we recommend our + users to mark a file as text. + (merge 1f34e0cd3d po/attributes-text later to maint). + + * Finally retire the scripted "git add -p/-i" implementation and have + everybody use the one reimplemented in C. Fixes since v2.39 ----------------- * Various leak fixes. - (merge ac95f5d36a ab/various-leak-fixes later to maint). * Fix a bug where `pack-objects` would not respect multiple `--filter` arguments when invoked directly. @@ -106,8 +116,6 @@ Fixes since v2.39 * Redefining system functions for a few functions did not follow our usual "implement git_foo() and #define foo(args) git_foo(args)" pattern, which has broken build for some folks. - (merge e1a95b78d8 jk/avoid-redef-system-functions-2.30 later to maint). - (merge 395bec6b39 jk/avoid-redef-system-functions later to maint). * The way the diff machinery prepares the options array for the parse_options API has been refactored to avoid resource leaks. @@ -137,36 +145,27 @@ Fixes since v2.39 * When given a pattern that matches an empty string at the end of a line, the code to parse the "git diff" line-ranges fell into an infinite loop, which has been corrected. - (merge 4e57c88e02 lk/line-range-parsing-fix later to maint). * Fix the sequence to fsync $GIT_DIR/packed-refs file that forgot to flush its output to the disk.. - (merge ce54672f9b ps/fsync-refs-fix later to maint). * Fix to a small regression in 2.38 days. - (merge 6d5e9e53aa ab/bundle-wo-args later to maint). * "git diff --relative" did not mix well with "git diff --ext-diff", which has been corrected. - (merge f034bb1cad jk/ext-diff-with-relative later to maint). * The logic to see if we are using the "cone" mode by checking the sparsity patterns has been tightened to avoid mistaking a pattern that names a single file as specifying a cone. - (merge 5842710dc2 ws/single-file-cone later to maint). * Deal with a few deprecation warning from cURL library. - (merge 6c065f72b8 jk/curl-avoid-deprecated-api later to maint). * Doc update for environment variables set when hooks are invoked. - (merge 772f8ff826 es/hooks-and-local-env later to maint). * Document ORIG_HEAD a bit more. - (merge f1c9243fc5 pb/doc-orig-head later to maint). * "git ls-tree --format='%(path) %(path)' $tree $path" showed the path three times, which has been corrected. - (merge c388fcda99 rs/ls-tree-path-expansion-fix later to maint). * Remove "git env--helper" and demote it to a test-tool subcommand. (merge 4a1baacd46 ab/test-env-helper later to maint). @@ -175,7 +174,6 @@ Fixes since v2.39 where '\(A\|B\)' works as alternation, unless explicitly asked with the REG_ENHANCED flag. "git grep" now can be compiled to do so, to retain the old behaviour. - (merge 54463d32ef rs/use-enhanced-bre-on-macos later to maint). * Pthread emulation on Win32 leaked thread handle when a thread is joined. @@ -188,7 +186,6 @@ Fixes since v2.39 * Document that "branch -f <branch>" disables only the safety to avoid recreating an existing branch. - (merge bf08abac56 jc/doc-branch-update-checked-out-branch later to maint). * "git fetch <group>", when "<group>" of remotes lists the same remote twice, unnecessarily failed when parallel fetching was @@ -197,7 +194,6 @@ Fixes since v2.39 * Clarify how "checkout -b/-B" and "git branch [-f]" are similar but different in the documentation. - (merge fedb8ea2df jc/doc-checkout-b later to maint). * "git hash-object" now checks that the resulting object is well formed with the same code as "git fsck". @@ -222,22 +218,12 @@ Fixes since v2.39 it is done using it, saving peak heap memory usage. (merge 647982bb71 ew/free-island-marks later to maint). + * In an environment where dynamically generated code is prohibited to + run (e.g. SELinux), failure to JIT pcre patterns is expected. Fall + back to interpreted execution in such a case. + (merge 50b6ad55b0 cb/grep-fallback-failing-jit later to maint). + * Other code cleanup, docfix, build fix, etc. - (merge 77e04b2ed4 rs/t4205-do-not-exit-in-test-script later to maint). - (merge faebba436e rs/plug-pattern-list-leak-in-lof later to maint). - (merge 243caa8982 ab/t5314-avoid-losing-exit-status later to maint). - (merge 4d81ce1b99 ab/t7600-avoid-losing-exit-status-of-git later to maint). - (merge 5f3bfdc4f3 ab/t4023-avoid-losing-exit-status-of-diff later to maint). - (merge 500317ae03 js/t3920-shell-and-or-fix later to maint). - (merge 86325d36e6 rs/t3920-crlf-eating-grep-fix later to maint). - (merge cfbd173ccb rj/branch-copy-and-rename later to maint). - (merge c25d9e529d jk/unused-post-2.39 later to maint). - (merge a31cfe3283 jk/server-supports-v2-cleanup later to maint). - (merge a658e881c1 rs/am-parse-options-cleanup later to maint). - (merge 4cb39fcf19 rs/clear-commit-marks-cleanup later to maint). - (merge b07a819c05 rs/reflog-expiry-cleanup later to maint). - (merge d422d06167 rs/clarify-error-in-write-loose-object later to maint). - (merge 92cb135855 sk/remove-duplicate-includes later to maint). (merge 4eb1ccecd4 dh/mingw-ownership-check-typofix later to maint). (merge f95526419b ar/typofix-gitattributes-doc later to maint). (merge 27875aeec9 km/doc-branch-start-point later to maint). @@ -258,3 +244,8 @@ Fixes since v2.39 (merge 4f542975d1 mh/doc-credential-cache-only-in-core later to maint). (merge 3a2ebaebc7 gc/index-format-doc later to maint). (merge b08edf709d jk/httpd-test-updates later to maint). + (merge d85e9448dd wl/new-command-doc later to maint). + (merge d912a603ed kf/t5000-modernise later to maint). + (merge e65b868d07 rs/size-t-fixes later to maint). + (merge 3eb1e1ca9a ab/config-h-remove-unused later to maint). + (merge d390e08076 cw/doc-pushurl-vs-url later to maint). diff --git a/Documentation/config/add.txt b/Documentation/config/add.txt index 3e859f3419..e0354ceaed 100644 --- a/Documentation/config/add.txt +++ b/Documentation/config/add.txt @@ -7,6 +7,7 @@ add.ignore-errors (deprecated):: variables. add.interactive.useBuiltin:: - Set to `false` to fall back to the original Perl implementation of - the interactive version of linkgit:git-add[1] instead of the built-in - version. Is `true` by default. + Unused configuration variable. Used in Git versions v2.25.0 to + v2.36.0 to enable the built-in version of linkgit:git-add[1]'s + interactive mode, which then became the default in Git + versions v2.37.0 to v2.39.0. diff --git a/Documentation/config/bundle.txt b/Documentation/config/bundle.txt index daa21eb674..3faae38685 100644 --- a/Documentation/config/bundle.txt +++ b/Documentation/config/bundle.txt @@ -15,6 +15,13 @@ bundle.mode:: complete understanding of the bundled information (`all`) or if any one of the listed bundle URIs is sufficient (`any`). +bundle.heuristic:: + If this string-valued key exists, then the bundle list is designed to + work well with incremental `git fetch` commands. The heuristic signals + that there are additional keys available for each bundle that help + determine which subset of bundles the client should download. The + only value currently understood is `creationToken`. + bundle.<id>.*:: The `bundle.<id>.*` keys are used to describe a single item in the bundle list, grouped under `<id>` for identification purposes. diff --git a/Documentation/config/fetch.txt b/Documentation/config/fetch.txt index cd65d236b4..568f0f75b3 100644 --- a/Documentation/config/fetch.txt +++ b/Documentation/config/fetch.txt @@ -96,3 +96,27 @@ fetch.writeCommitGraph:: merge and the write may take longer. Having an updated commit-graph file helps performance of many Git commands, including `git merge-base`, `git push -f`, and `git log --graph`. Defaults to false. + +fetch.bundleURI:: + This value stores a URI for downloading Git object data from a bundle + URI before performing an incremental fetch from the origin Git server. + This is similar to how the `--bundle-uri` option behaves in + linkgit:git-clone[1]. `git clone --bundle-uri` will set the + `fetch.bundleURI` value if the supplied bundle URI contains a bundle + list that is organized for incremental fetches. ++ +If you modify this value and your repository has a `fetch.bundleCreationToken` +value, then remove that `fetch.bundleCreationToken` value before fetching from +the new bundle URI. + +fetch.bundleCreationToken:: + When using `fetch.bundleURI` to fetch incrementally from a bundle + list that uses the "creationToken" heuristic, this config value + stores the maximum `creationToken` value of the downloaded bundles. + This value is used to prevent downloading bundles in the future + if the advertised `creationToken` is not strictly larger than this + value. ++ +The creation token values are chosen by the provider serving the specific +bundle URI. If you modify the URI at `fetch.bundleURI`, then be sure to +remove the value for the `fetch.bundleCreationToken` value before fetching. diff --git a/Documentation/git-add.txt b/Documentation/git-add.txt index a030d33c6e..ed44c1cb31 100644 --- a/Documentation/git-add.txt +++ b/Documentation/git-add.txt @@ -274,7 +274,7 @@ status:: ------------ staged unstaged path 1: binary nothing foo.png - 2: +403/-35 +1/-1 git-add--interactive.perl + 2: +403/-35 +1/-1 add-interactive.c ------------ + It shows that foo.png has differences from HEAD (but that is @@ -282,7 +282,7 @@ binary so line count cannot be shown) and there is no difference between indexed copy and the working tree version (if the working tree version were also different, 'binary' would have been shown in place of 'nothing'). The -other file, git-add{litdd}interactive.perl, has 403 lines added +other file, add-interactive.c, has 403 lines added and 35 lines deleted if you commit what is in the index, but working tree file has further modifications (one addition and one deletion). @@ -303,7 +303,7 @@ like this: ------------ staged unstaged path 1: binary nothing foo.png -* 2: +403/-35 +1/-1 git-add--interactive.perl +* 2: +403/-35 +1/-1 add-interactive.c ------------ + To remove selection, prefix the input with `-` diff --git a/Documentation/howto/new-command.txt b/Documentation/howto/new-command.txt index 15a4c8031f..880c51112b 100644 --- a/Documentation/howto/new-command.txt +++ b/Documentation/howto/new-command.txt @@ -1,13 +1,13 @@ From: Eric S. Raymond <esr@thyrsus.com> Abstract: This is how-to documentation for people who want to add extension - commands to Git. It should be read alongside api-builtin.txt. + commands to Git. It should be read alongside builtin.h. Content-type: text/asciidoc How to integrate new subcommands ================================ This is how-to documentation for people who want to add extension -commands to Git. It should be read alongside api-builtin.txt. +commands to Git. It should be read alongside builtin.h. Runtime environment ------------------- diff --git a/Documentation/technical/bundle-uri.txt b/Documentation/technical/bundle-uri.txt index b78d01d9ad..91d3a13e32 100644 --- a/Documentation/technical/bundle-uri.txt +++ b/Documentation/technical/bundle-uri.txt @@ -479,14 +479,14 @@ outline for submitting these features: (This choice is an opt-in via a config option and a command-line option.) -4. Allow the client to understand the `bundle.flag=forFetch` configuration +4. Allow the client to understand the `bundle.heuristic` configuration key and the `bundle.<id>.creationToken` heuristic. When `git clone` - discovers a bundle URI with `bundle.flag=forFetch`, it configures the - client repository to check that bundle URI during later `git fetch <remote>` + discovers a bundle URI with `bundle.heuristic`, it configures the client + repository to check that bundle URI during later `git fetch <remote>` commands. 5. Allow clients to discover bundle URIs during `git fetch` and configure - a bundle URI for later fetches if `bundle.flag=forFetch`. + a bundle URI for later fetches if `bundle.heuristic` is set. 6. Implement the "inspect headers" heuristic to reduce data downloads when the `bundle.<id>.creationToken` heuristic is not available. diff --git a/Documentation/urls-remotes.txt b/Documentation/urls-remotes.txt index 86d0008f94..e410912fe5 100644 --- a/Documentation/urls-remotes.txt +++ b/Documentation/urls-remotes.txt @@ -33,7 +33,9 @@ config file would appear like this: ------------ The `<pushurl>` is used for pushes only. It is optional and defaults -to `<URL>`. +to `<URL>`. Pushing to a remote affects all defined pushurls or to all +defined urls if no pushurls are defined. Fetch, however, will only +fetch from the first defined url if muliple urls are defined. Named file in `$GIT_DIR/remotes` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -120,7 +120,7 @@ Issues of note: for everyday use (e.g. "bisect", "request-pull"). - "Perl" version 5.8 or later is needed to use some of the - features (e.g. preparing a partial commit using "git add -i/-p", + features (e.g. sending patches using "git send-email", interacting with svn repositories with "git svn"). If you can live without these, use NO_PERL. Note that recent releases of Redhat/Fedora are reported to ship Perl binary package with some @@ -708,7 +708,6 @@ SCRIPT_LIB += git-mergetool--lib SCRIPT_LIB += git-sh-i18n SCRIPT_LIB += git-sh-setup -SCRIPT_PERL += git-add--interactive.perl SCRIPT_PERL += git-archimport.perl SCRIPT_PERL += git-cvsexportcommit.perl SCRIPT_PERL += git-cvsimport.perl @@ -4418,6 +4418,33 @@ static int create_one_file(struct apply_state *state, if (state->cached) return 0; + /* + * We already try to detect whether files are beyond a symlink in our + * up-front checks. But in the case where symlinks are created by any + * of the intermediate hunks it can happen that our up-front checks + * didn't yet see the symlink, but at the point of arriving here there + * in fact is one. We thus repeat the check for symlinks here. + * + * Note that this does not make the up-front check obsolete as the + * failure mode is different: + * + * - The up-front checks cause us to abort before we have written + * anything into the working directory. So when we exit this way the + * working directory remains clean. + * + * - The checks here happen in the middle of the action where we have + * already started to apply the patch. The end result will be a dirty + * working directory. + * + * Ideally, we should update the up-front checks to catch what would + * happen when we apply the patch before we damage the working tree. + * We have all the information necessary to do so. But for now, as a + * part of embargoed security work, having this check would serve as a + * reasonable first step. + */ + if (path_is_beyond_symlink(state, path)) + return error(_("affected file '%s' is beyond a symbolic link"), path); + res = try_create_file(state, path, mode, buf, size); if (res < 0) return -1; diff --git a/builtin/add.c b/builtin/add.c index 0c60402267..61dd386d10 100644 --- a/builtin/add.c +++ b/builtin/add.c @@ -238,58 +238,14 @@ static int refresh(int verbose, const struct pathspec *pathspec) return ret; } -int run_add_interactive(const char *revision, const char *patch_mode, - const struct pathspec *pathspec) -{ - int i; - struct child_process cmd = CHILD_PROCESS_INIT; - int use_builtin_add_i = - git_env_bool("GIT_TEST_ADD_I_USE_BUILTIN", -1); - - if (use_builtin_add_i < 0 && - git_config_get_bool("add.interactive.usebuiltin", - &use_builtin_add_i)) - use_builtin_add_i = 1; - - if (use_builtin_add_i != 0) { - enum add_p_mode mode; - - if (!patch_mode) - return !!run_add_i(the_repository, pathspec); - - if (!strcmp(patch_mode, "--patch")) - mode = ADD_P_ADD; - else if (!strcmp(patch_mode, "--patch=stash")) - mode = ADD_P_STASH; - else if (!strcmp(patch_mode, "--patch=reset")) - mode = ADD_P_RESET; - else if (!strcmp(patch_mode, "--patch=checkout")) - mode = ADD_P_CHECKOUT; - else if (!strcmp(patch_mode, "--patch=worktree")) - mode = ADD_P_WORKTREE; - else - die("'%s' not supported", patch_mode); - - return !!run_add_p(the_repository, mode, revision, pathspec); - } - - strvec_push(&cmd.args, "add--interactive"); - if (patch_mode) - strvec_push(&cmd.args, patch_mode); - if (revision) - strvec_push(&cmd.args, revision); - strvec_push(&cmd.args, "--"); - for (i = 0; i < pathspec->nr; i++) - /* pass original pathspec, to be re-parsed */ - strvec_push(&cmd.args, pathspec->items[i].original); - - cmd.git_cmd = 1; - return run_command(&cmd); -} - int interactive_add(const char **argv, const char *prefix, int patch) { struct pathspec pathspec; + int unused; + + if (!git_config_get_bool("add.interactive.usebuiltin", &unused)) + warning(_("the add.interactive.useBuiltin setting has been removed!\n" + "See its entry in 'git help config' for details.")); parse_pathspec(&pathspec, 0, PATHSPEC_PREFER_FULL | @@ -297,9 +253,10 @@ int interactive_add(const char **argv, const char *prefix, int patch) PATHSPEC_PREFIX_ORIGIN, prefix, argv); - return run_add_interactive(NULL, - patch ? "--patch" : NULL, - &pathspec); + if (patch) + return !!run_add_p(the_repository, ADD_P_ADD, NULL, &pathspec); + else + return !!run_add_i(the_repository, &pathspec); } static int edit_patch(int argc, const char **argv, const char *prefix) diff --git a/builtin/checkout.c b/builtin/checkout.c index 5963e1b74b..a5155cf55c 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -29,6 +29,7 @@ #include "xdiff-interface.h" #include "entry.h" #include "parallel-checkout.h" +#include "add-interactive.h" static const char * const checkout_usage[] = { N_("git checkout [<options>] <branch>"), @@ -499,7 +500,7 @@ static int checkout_paths(const struct checkout_opts *opts, "--merge", "--conflict", "--staged"); if (opts->patch_mode) { - const char *patch_mode; + enum add_p_mode patch_mode; const char *rev = new_branch_info->name; char rev_oid[GIT_MAX_HEXSZ + 1]; @@ -517,15 +518,16 @@ static int checkout_paths(const struct checkout_opts *opts, rev = oid_to_hex_r(rev_oid, &new_branch_info->commit->object.oid); if (opts->checkout_index && opts->checkout_worktree) - patch_mode = "--patch=checkout"; + patch_mode = ADD_P_CHECKOUT; else if (opts->checkout_index && !opts->checkout_worktree) - patch_mode = "--patch=reset"; + patch_mode = ADD_P_RESET; else if (!opts->checkout_index && opts->checkout_worktree) - patch_mode = "--patch=worktree"; + patch_mode = ADD_P_WORKTREE; else BUG("either flag must have been set, worktree=%d, index=%d", opts->checkout_worktree, opts->checkout_index); - return run_add_interactive(rev, patch_mode, &opts->pathspec); + return !!run_add_p(the_repository, patch_mode, rev, + &opts->pathspec); } repo_hold_locked_index(the_repository, &lock_file, LOCK_DIE_ON_ERROR); diff --git a/builtin/clean.c b/builtin/clean.c index b2701a2815..1e6d491d3b 100644 --- a/builtin/clean.c +++ b/builtin/clean.c @@ -560,7 +560,7 @@ static int parse_choice(struct menu_stuff *menu_stuff, /* * Implement a git-add-interactive compatible UI, which is borrowed - * from git-add--interactive.perl. + * from add-interactive.c. * * Return value: * diff --git a/builtin/clone.c b/builtin/clone.c index 5453ba5277..5691364ea1 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -1170,10 +1170,6 @@ int cmd_clone(int argc, const char **argv, const char *prefix) refspec_appendf(&remote->fetch, "+%s*:%s*", src_ref_prefix, branch_top.buf); - transport = transport_get(remote, remote->url[0]); - transport_set_verbosity(transport, option_verbosity, option_progress); - transport->family = family; - path = get_repo_path(remote->url[0], &is_bundle); is_local = option_local != 0 && path && !is_bundle; if (is_local) { @@ -1195,6 +1191,10 @@ int cmd_clone(int argc, const char **argv, const char *prefix) } if (option_local > 0 && !is_local) warning(_("--local is ignored")); + + transport = transport_get(remote, path ? path : remote->url[0]); + transport_set_verbosity(transport, option_verbosity, option_progress); + transport->family = family; transport->cloning = 1; if (is_bundle) { @@ -1248,12 +1248,16 @@ int cmd_clone(int argc, const char **argv, const char *prefix) * data from the --bundle-uri option. */ if (bundle_uri) { + int has_heuristic = 0; + /* At this point, we need the_repository to match the cloned repo. */ if (repo_init(the_repository, git_dir, work_tree)) warning(_("failed to initialize the repo, skipping bundle URI")); - else if (fetch_bundle_uri(the_repository, bundle_uri)) + else if (fetch_bundle_uri(the_repository, bundle_uri, &has_heuristic)) warning(_("failed to fetch objects from bundle URI '%s'"), bundle_uri); + else if (has_heuristic) + git_config_set_gently("fetch.bundleuri", bundle_uri); } strvec_push(&transport_ls_refs_options.ref_prefixes, "HEAD"); diff --git a/builtin/fetch.c b/builtin/fetch.c index 12978622d5..a21ce89312 100644 --- a/builtin/fetch.c +++ b/builtin/fetch.c @@ -29,6 +29,7 @@ #include "commit-graph.h" #include "shallow.h" #include "worktree.h" +#include "bundle-uri.h" #define FORCED_UPDATES_DELAY_WARNING_IN_MS (10 * 1000) @@ -2109,6 +2110,7 @@ static int fetch_one(struct remote *remote, int argc, const char **argv, int cmd_fetch(int argc, const char **argv, const char *prefix) { int i; + const char *bundle_uri; struct string_list list = STRING_LIST_INIT_DUP; struct remote *remote = NULL; int result = 0; @@ -2194,6 +2196,10 @@ int cmd_fetch(int argc, const char **argv, const char *prefix) if (dry_run) write_fetch_head = 0; + if (!git_config_get_string_tmp("fetch.bundleuri", &bundle_uri) && + fetch_bundle_uri(the_repository, bundle_uri, NULL)) + warning(_("failed to fetch bundles from '%s'"), bundle_uri); + if (all) { if (argc == 1) die(_("fetch --all does not take a repository argument")); diff --git a/builtin/name-rev.c b/builtin/name-rev.c index 15535e914a..0ebf06fad5 100644 --- a/builtin/name-rev.c +++ b/builtin/name-rev.c @@ -108,19 +108,11 @@ static int is_better_name(struct rev_name *name, int name_distance = effective_distance(name->distance, name->generation); int new_distance = effective_distance(distance, generation); - /* - * When comparing names based on tags, prefer names - * based on the older tag, even if it is farther away. - */ + /* If both are tags, we prefer the nearer one. */ if (from_tag && name->from_tag) - return (name->taggerdate > taggerdate || - (name->taggerdate == taggerdate && - name_distance > new_distance)); + return name_distance > new_distance; - /* - * We know that at least one of them is a non-tag at this point. - * favor a tag over a non-tag. - */ + /* Favor a tag over a non-tag. */ if (name->from_tag != from_tag) return from_tag; diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c index 3395f63aba..74a167a180 100644 --- a/builtin/pack-objects.c +++ b/builtin/pack-objects.c @@ -1710,17 +1710,14 @@ static void pbase_tree_put(struct pbase_tree_cache *cache) free(cache); } -static int name_cmp_len(const char *name) +static size_t name_cmp_len(const char *name) { - int i; - for (i = 0; name[i] && name[i] != '\n' && name[i] != '/'; i++) - ; - return i; + return strcspn(name, "\n/"); } static void add_pbase_object(struct tree_desc *tree, const char *name, - int cmplen, + size_t cmplen, const char *fullname) { struct name_entry entry; @@ -1745,7 +1742,7 @@ static void add_pbase_object(struct tree_desc *tree, struct tree_desc sub; struct pbase_tree_cache *tree; const char *down = name+cmplen+1; - int downlen = name_cmp_len(down); + size_t downlen = name_cmp_len(down); tree = pbase_tree_get(&entry.oid); if (!tree) @@ -1797,7 +1794,7 @@ static int check_pbase_path(unsigned hash) static void add_preferred_base_object(const char *name) { struct pbase_tree *it; - int cmplen; + size_t cmplen; unsigned hash = pack_name_hash(name); if (!num_preferred_base || check_pbase_path(hash)) diff --git a/builtin/rebase.c b/builtin/rebase.c index 7171be40ee..6635f10d52 100644 --- a/builtin/rebase.c +++ b/builtin/rebase.c @@ -254,7 +254,7 @@ static int init_basic_state(struct replay_opts *opts, const char *head_name, static int do_interactive_rebase(struct rebase_options *opts, unsigned flags) { - int ret; + int ret = -1; char *revisions = NULL, *shortrevisions = NULL; struct strvec make_script_args = STRVEC_INIT; struct todo_list todo_list = TODO_LIST_INIT; @@ -262,16 +262,12 @@ static int do_interactive_rebase(struct rebase_options *opts, unsigned flags) if (get_revision_ranges(opts->upstream, opts->onto, &opts->orig_head->object.oid, &revisions, &shortrevisions)) - return -1; + goto cleanup; if (init_basic_state(&replay, opts->head_name ? opts->head_name : "detached HEAD", - opts->onto, &opts->orig_head->object.oid)) { - free(revisions); - free(shortrevisions); - - return -1; - } + opts->onto, &opts->orig_head->object.oid)) + goto cleanup; if (!opts->upstream && opts->squash_onto) write_file(path_squash_onto(), "%s\n", @@ -300,6 +296,8 @@ static int do_interactive_rebase(struct rebase_options *opts, unsigned flags) opts->autosquash, opts->update_refs, &todo_list); } +cleanup: + replay_opts_release(&replay); free(revisions); free(shortrevisions); todo_list_release(&todo_list); @@ -341,6 +339,7 @@ static int run_sequencer_rebase(struct rebase_options *opts) struct replay_opts replay_opts = get_replay_opts(opts); ret = sequencer_continue(the_repository, &replay_opts); + replay_opts_release(&replay_opts); break; } case ACTION_EDIT_TODO: @@ -556,6 +555,7 @@ static int finish_rebase(struct rebase_options *opts) replay.action = REPLAY_INTERACTIVE_REBASE; ret = sequencer_remove_state(&replay); + replay_opts_release(&replay); } else { strbuf_addstr(&dir, opts->state_dir); if (remove_dir_recursively(&dir, 0)) @@ -1039,6 +1039,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix) struct string_list strategy_options = STRING_LIST_INIT_NODUP; struct object_id squash_onto; char *squash_onto_name = NULL; + char *keep_base_onto_name = NULL; int reschedule_failed_exec = -1; int allow_preemptive_ff = 1; int preserve_merges_selected = 0; @@ -1327,6 +1328,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix) replay.action = REPLAY_INTERACTIVE_REBASE; ret = sequencer_remove_state(&replay); + replay_opts_release(&replay); } else { strbuf_reset(&buf); strbuf_addstr(&buf, options.state_dir); @@ -1674,7 +1676,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix) strbuf_addstr(&buf, options.upstream_name); strbuf_addstr(&buf, "..."); strbuf_addstr(&buf, branch_name); - options.onto_name = xstrdup(buf.buf); + options.onto_name = keep_base_onto_name = xstrdup(buf.buf); } else if (!options.onto_name) options.onto_name = options.upstream_name; if (strstr(options.onto_name, "...")) { @@ -1848,8 +1850,10 @@ cleanup: free(options.gpg_sign_opt); string_list_clear(&options.exec, 0); free(options.strategy); + free(options.strategy_opts); strbuf_release(&options.git_format_patch_opt); free(squash_onto_name); + free(keep_base_onto_name); string_list_clear(&strategy_options, 0); return !!ret; } diff --git a/builtin/reset.c b/builtin/reset.c index fea20a9ba0..4b59aa9aea 100644 --- a/builtin/reset.c +++ b/builtin/reset.c @@ -26,6 +26,7 @@ #include "submodule.h" #include "submodule-config.h" #include "dir.h" +#include "add-interactive.h" #define REFRESH_INDEX_DELAY_WARNING_IN_MS (2 * 1000) @@ -390,7 +391,8 @@ int cmd_reset(int argc, const char **argv, const char *prefix) if (reset_type != NONE) die(_("options '%s' and '%s' cannot be used together"), "--patch", "--{hard,mixed,soft}"); trace2_cmd_mode("patch-interactive"); - return run_add_interactive(rev, "--patch=reset", &pathspec); + return !!run_add_p(the_repository, ADD_P_RESET, rev, + &pathspec); } /* git reset tree [--] paths... can be used to diff --git a/builtin/revert.c b/builtin/revert.c index f2d86d2a8f..77d2035616 100644 --- a/builtin/revert.c +++ b/builtin/revert.c @@ -248,9 +248,7 @@ int cmd_revert(int argc, const char **argv, const char *prefix) res = run_sequencer(argc, argv, &opts); if (res < 0) die(_("revert failed")); - if (opts.revs) - release_revisions(opts.revs); - free(opts.revs); + replay_opts_release(&opts); return res; } @@ -262,10 +260,8 @@ int cmd_cherry_pick(int argc, const char **argv, const char *prefix) opts.action = REPLAY_PICK; sequencer_init_config(&opts); res = run_sequencer(argc, argv, &opts); - if (opts.revs) - release_revisions(opts.revs); - free(opts.revs); if (res < 0) die(_("cherry-pick failed")); + replay_opts_release(&opts); return res; } diff --git a/builtin/stash.c b/builtin/stash.c index 78d69da8cf..f93d04f11e 100644 --- a/builtin/stash.c +++ b/builtin/stash.c @@ -18,6 +18,7 @@ #include "diffcore.h" #include "exec-cmd.h" #include "reflog.h" +#include "add-interactive.h" #define INCLUDE_ALL_FILES 2 @@ -1231,7 +1232,7 @@ static int stash_patch(struct stash_info *info, const struct pathspec *ps, old_index_env = xstrdup_or_null(getenv(INDEX_ENVIRONMENT)); setenv(INDEX_ENVIRONMENT, the_repository->index_file, 1); - ret = run_add_interactive(NULL, "--patch=stash", ps); + ret = !!run_add_p(the_repository, ADD_P_STASH, NULL, ps); the_repository->index_file = old_repo_index_file; if (old_index_env && *old_index_env) diff --git a/bundle-uri.c b/bundle-uri.c index 6462ab6deb..8a3c39ce57 100644 --- a/bundle-uri.c +++ b/bundle-uri.c @@ -9,6 +9,14 @@ #include "config.h" #include "remote.h" +static struct { + enum bundle_list_heuristic heuristic; + const char *name; +} heuristics[BUNDLE_HEURISTIC__COUNT] = { + { BUNDLE_HEURISTIC_NONE, ""}, + { BUNDLE_HEURISTIC_CREATIONTOKEN, "creationToken" }, +}; + static int compare_bundles(const void *hashmap_cmp_fn_data, const struct hashmap_entry *he1, const struct hashmap_entry *he2, @@ -75,6 +83,9 @@ static int summarize_bundle(struct remote_bundle_info *info, void *data) FILE *fp = data; fprintf(fp, "[bundle \"%s\"]\n", info->id); fprintf(fp, "\turi = %s\n", info->uri); + + if (info->creationToken) + fprintf(fp, "\tcreationToken = %"PRIu64"\n", info->creationToken); return 0; } @@ -100,6 +111,17 @@ void print_bundle_list(FILE *fp, struct bundle_list *list) fprintf(fp, "\tversion = %d\n", list->version); fprintf(fp, "\tmode = %s\n", mode); + if (list->heuristic) { + int i; + for (i = 0; i < BUNDLE_HEURISTIC__COUNT; i++) { + if (heuristics[i].heuristic == list->heuristic) { + printf("\theuristic = %s\n", + heuristics[list->heuristic].name); + break; + } + } + } + for_all_bundles_in_list(list, summarize_bundle, fp); } @@ -142,6 +164,21 @@ static int bundle_list_update(const char *key, const char *value, return 0; } + if (!strcmp(subkey, "heuristic")) { + int i; + for (i = 0; i < BUNDLE_HEURISTIC__COUNT; i++) { + if (heuristics[i].heuristic && + heuristics[i].name && + !strcmp(value, heuristics[i].name)) { + list->heuristic = heuristics[i].heuristic; + return 0; + } + } + + /* Ignore unknown heuristics. */ + return 0; + } + /* Ignore other unknown global keys. */ return 0; } @@ -169,6 +206,13 @@ static int bundle_list_update(const char *key, const char *value, return 0; } + if (!strcmp(subkey, "creationtoken")) { + if (sscanf(value, "%"PRIu64, &bundle->creationToken) != 1) + warning(_("could not parse bundle list key %s with value '%s'"), + "creationToken", value); + return 0; + } + /* * At this point, we ignore any information that we don't * understand, assuming it to be hints for a heuristic the client @@ -403,6 +447,183 @@ static int download_bundle_to_file(struct remote_bundle_info *bundle, void *data return 0; } +struct bundles_for_sorting { + struct remote_bundle_info **items; + size_t alloc; + size_t nr; +}; + +static int append_bundle(struct remote_bundle_info *bundle, void *data) +{ + struct bundles_for_sorting *list = data; + list->items[list->nr++] = bundle; + return 0; +} + +/** + * For use in QSORT() to get a list sorted by creationToken + * in decreasing order. + */ +static int compare_creation_token_decreasing(const void *va, const void *vb) +{ + const struct remote_bundle_info * const *a = va; + const struct remote_bundle_info * const *b = vb; + + if ((*a)->creationToken > (*b)->creationToken) + return -1; + if ((*a)->creationToken < (*b)->creationToken) + return 1; + return 0; +} + +static int fetch_bundles_by_token(struct repository *r, + struct bundle_list *list) +{ + int cur; + int move_direction = 0; + const char *creationTokenStr; + uint64_t maxCreationToken = 0, newMaxCreationToken = 0; + struct bundle_list_context ctx = { + .r = r, + .list = list, + .mode = list->mode, + }; + struct bundles_for_sorting bundles = { + .alloc = hashmap_get_size(&list->bundles), + }; + + ALLOC_ARRAY(bundles.items, bundles.alloc); + + for_all_bundles_in_list(list, append_bundle, &bundles); + + if (!bundles.nr) { + free(bundles.items); + return 0; + } + + QSORT(bundles.items, bundles.nr, compare_creation_token_decreasing); + + /* + * If fetch.bundleCreationToken exists, parses to a uint64t, and + * is not strictly smaller than the maximum creation token in the + * bundle list, then do not download any bundles. + */ + if (!repo_config_get_value(r, + "fetch.bundlecreationtoken", + &creationTokenStr) && + sscanf(creationTokenStr, "%"PRIu64, &maxCreationToken) == 1 && + bundles.items[0]->creationToken <= maxCreationToken) { + free(bundles.items); + return 0; + } + + /* + * Attempt to download and unbundle the minimum number of bundles by + * creationToken in decreasing order. If we fail to unbundle (after + * a successful download) then move to the next non-downloaded bundle + * and attempt downloading. Once we succeed in applying a bundle, + * move to the previous unapplied bundle and attempt to unbundle it + * again. + * + * In the case of a fresh clone, we will likely download all of the + * bundles before successfully unbundling the oldest one, then the + * rest of the bundles unbundle successfully in increasing order + * of creationToken. + * + * If there are existing objects, then this process may terminate + * early when all required commits from "new" bundles exist in the + * repo's object store. + */ + cur = 0; + while (cur >= 0 && cur < bundles.nr) { + struct remote_bundle_info *bundle = bundles.items[cur]; + + /* + * If we need to dig into bundles below the previous + * creation token value, then likely we are in an erroneous + * state due to missing or invalid bundles. Halt the process + * instead of continuing to download extra data. + */ + if (bundle->creationToken <= maxCreationToken) + break; + + if (!bundle->file) { + /* + * Not downloaded yet. Try downloading. + * + * Note that bundle->file is non-NULL if a download + * was attempted, even if it failed to download. + */ + if (fetch_bundle_uri_internal(ctx.r, bundle, ctx.depth + 1, ctx.list)) { + /* Mark as unbundled so we do not retry. */ + bundle->unbundled = 1; + + /* Try looking deeper in the list. */ + move_direction = 1; + goto move; + } + + /* We expect bundles when using creationTokens. */ + if (!is_bundle(bundle->file, 1)) { + warning(_("file downloaded from '%s' is not a bundle"), + bundle->uri); + break; + } + } + + if (bundle->file && !bundle->unbundled) { + /* + * This was downloaded, but not successfully + * unbundled. Try unbundling again. + */ + if (unbundle_from_file(ctx.r, bundle->file)) { + /* Try looking deeper in the list. */ + move_direction = 1; + } else { + /* + * Succeeded in unbundle. Retry bundles + * that previously failed to unbundle. + */ + move_direction = -1; + bundle->unbundled = 1; + + if (bundle->creationToken > newMaxCreationToken) + newMaxCreationToken = bundle->creationToken; + } + } + + /* + * Else case: downloaded and unbundled successfully. + * Skip this by moving in the same direction as the + * previous step. + */ + +move: + /* Move in the specified direction and repeat. */ + cur += move_direction; + } + + /* + * We succeed if the loop terminates because 'cur' drops below + * zero. The other case is that we terminate because 'cur' + * reaches the end of the list, so we have a failure no matter + * which bundles we apply from the list. + */ + if (cur < 0) { + struct strbuf value = STRBUF_INIT; + strbuf_addf(&value, "%"PRIu64"", newMaxCreationToken); + if (repo_config_set_multivar_gently(ctx.r, + "fetch.bundleCreationToken", + value.buf, NULL, 0)) + warning(_("failed to store maximum creation token")); + + strbuf_release(&value); + } + + free(bundles.items); + return cur >= 0; +} + static int download_bundle_list(struct repository *r, struct bundle_list *local_list, struct bundle_list *global_list, @@ -440,7 +661,15 @@ static int fetch_bundle_list_in_config_format(struct repository *r, goto cleanup; } - if ((result = download_bundle_list(r, &list_from_bundle, + /* + * If this list uses the creationToken heuristic, then the URIs + * it advertises are expected to be bundles, not nested lists. + * We can drop 'global_list' and 'depth'. + */ + if (list_from_bundle.heuristic == BUNDLE_HEURISTIC_CREATIONTOKEN) { + result = fetch_bundles_by_token(r, &list_from_bundle); + global_list->heuristic = BUNDLE_HEURISTIC_CREATIONTOKEN; + } else if ((result = download_bundle_list(r, &list_from_bundle, global_list, depth))) goto cleanup; @@ -551,7 +780,8 @@ static int unlink_bundle(struct remote_bundle_info *info, void *data) return 0; } -int fetch_bundle_uri(struct repository *r, const char *uri) +int fetch_bundle_uri(struct repository *r, const char *uri, + int *has_heuristic) { int result; struct bundle_list list; @@ -571,6 +801,8 @@ int fetch_bundle_uri(struct repository *r, const char *uri) result = unbundle_all_bundles(r, &list); cleanup: + if (has_heuristic) + *has_heuristic = (list.heuristic != BUNDLE_HEURISTIC_NONE); for_all_bundles_in_list(&list, unlink_bundle, NULL); clear_bundle_list(&list); clear_remote_bundle_info(&bundle, NULL); @@ -582,6 +814,14 @@ int fetch_bundle_list(struct repository *r, struct bundle_list *list) int result; struct bundle_list global_list; + /* + * If the creationToken heuristic is used, then the URIs + * advertised by 'list' are not nested lists and instead + * direct bundles. We do not need to use global_list. + */ + if (list->heuristic == BUNDLE_HEURISTIC_CREATIONTOKEN) + return fetch_bundles_by_token(r, list); + init_bundle_list(&global_list); /* If a bundle is added to this global list, then it is required. */ @@ -590,7 +830,10 @@ int fetch_bundle_list(struct repository *r, struct bundle_list *list) if ((result = download_bundle_list(r, list, &global_list, 0))) goto cleanup; - result = unbundle_all_bundles(r, &global_list); + if (list->heuristic == BUNDLE_HEURISTIC_CREATIONTOKEN) + result = fetch_bundles_by_token(r, list); + else + result = unbundle_all_bundles(r, &global_list); cleanup: for_all_bundles_in_list(&global_list, unlink_bundle, NULL); diff --git a/bundle-uri.h b/bundle-uri.h index d5e89f1671..6dbc780f66 100644 --- a/bundle-uri.h +++ b/bundle-uri.h @@ -42,6 +42,12 @@ struct remote_bundle_info { * this boolean is true. */ unsigned unbundled:1; + + /** + * If the bundle is part of a list with the creationToken + * heuristic, then we use this member for sorting the bundles. + */ + uint64_t creationToken; }; #define REMOTE_BUNDLE_INFO_INIT { 0 } @@ -52,6 +58,14 @@ enum bundle_list_mode { BUNDLE_MODE_ANY }; +enum bundle_list_heuristic { + BUNDLE_HEURISTIC_NONE = 0, + BUNDLE_HEURISTIC_CREATIONTOKEN, + + /* Must be last. */ + BUNDLE_HEURISTIC__COUNT +}; + /** * A bundle_list contains an unordered set of remote_bundle_info structs, * as well as information about the bundle listing, such as version and @@ -75,6 +89,12 @@ struct bundle_list { * advertised by the bundle list at that location. */ char *baseURI; + + /** + * A list can have a heuristic, which helps reduce the number of + * downloaded bundles. + */ + enum bundle_list_heuristic heuristic; }; void init_bundle_list(struct bundle_list *list); @@ -104,8 +124,14 @@ int bundle_uri_parse_config_format(const char *uri, * based on that information. * * Returns non-zero if no bundle information is found at the given 'uri'. + * + * If the pointer 'has_heuristic' is non-NULL, then the value it points to + * will be set to be non-zero if and only if the fetched list has a + * heuristic value. Such a value indicates that the list was designed for + * incremental fetches. */ -int fetch_bundle_uri(struct repository *r, const char *uri); +int fetch_bundle_uri(struct repository *r, const char *uri, + int *has_heuristic); /** * Given a bundle list that was already advertised (likely by the @@ -12,6 +12,7 @@ #include "refs.h" #include "strvec.h" #include "list-objects-filter-options.h" +#include "connected.h" static const char v2_bundle_signature[] = "# v2 git bundle\n"; static const char v3_bundle_signature[] = "# v3 git bundle\n"; @@ -187,6 +188,21 @@ static int list_refs(struct string_list *r, int argc, const char **argv) /* Remember to update object flag allocation in object.h */ #define PREREQ_MARK (1u<<16) +struct string_list_iterator { + struct string_list *list; + size_t cur; +}; + +static const struct object_id *iterate_ref_map(void *cb_data) +{ + struct string_list_iterator *iter = cb_data; + + if (iter->cur >= iter->list->nr) + return NULL; + + return iter->list->items[iter->cur++].util; +} + int verify_bundle(struct repository *r, struct bundle_header *header, enum verify_bundle_flags flags) @@ -196,26 +212,25 @@ int verify_bundle(struct repository *r, * to be verbose about the errors */ struct string_list *p = &header->prerequisites; - struct rev_info revs = REV_INFO_INIT; - const char *argv[] = {NULL, "--all", NULL}; - struct commit *commit; - int i, ret = 0, req_nr; + int i, ret = 0; const char *message = _("Repository lacks these prerequisite commits:"); + struct string_list_iterator iter = { + .list = p, + }; + struct check_connected_options opts = { + .quiet = 1, + }; if (!r || !r->objects || !r->objects->odb) return error(_("need a repository to verify a bundle")); - repo_init_revisions(r, &revs, NULL); for (i = 0; i < p->nr; i++) { struct string_list_item *e = p->items + i; const char *name = e->string; struct object_id *oid = e->util; struct object *o = parse_object(r, oid); - if (o) { - o->flags |= PREREQ_MARK; - add_pending_object(&revs, o, name); + if (o) continue; - } ret++; if (flags & VERIFY_BUNDLE_QUIET) continue; @@ -223,37 +238,14 @@ int verify_bundle(struct repository *r, error("%s", message); error("%s %s", oid_to_hex(oid), name); } - if (revs.pending.nr != p->nr) + if (ret) goto cleanup; - req_nr = revs.pending.nr; - setup_revisions(2, argv, &revs, NULL); - - list_objects_filter_copy(&revs.filter, &header->filter); - - if (prepare_revision_walk(&revs)) - die(_("revision walk setup failed")); - i = req_nr; - while (i && (commit = get_revision(&revs))) - if (commit->object.flags & PREREQ_MARK) - i--; - - for (i = 0; i < p->nr; i++) { - struct string_list_item *e = p->items + i; - const char *name = e->string; - const struct object_id *oid = e->util; - struct object *o = parse_object(r, oid); - assert(o); /* otherwise we'd have returned early */ - if (o->flags & SHOWN) - continue; - ret++; - if (flags & VERIFY_BUNDLE_QUIET) - continue; - if (ret == 1) - error("%s", message); - error("%s %s", oid_to_hex(oid), name); - } + if ((ret = check_connected(iterate_ref_map, &iter, &opts))) + error(_("some prerequisite commits exist in the object store, " + "but are not connected to the repository's history")); + /* TODO: preserve this verbose language. */ if (flags & VERIFY_BUNDLE_VERBOSE) { struct string_list *r; @@ -282,15 +274,6 @@ int verify_bundle(struct repository *r, list_objects_filter_spec(&header->filter)); } cleanup: - /* Clean up objects used, as they will be reused. */ - for (i = 0; i < p->nr; i++) { - struct string_list_item *e = p->items + i; - struct object_id *oid = e->util; - commit = lookup_commit_reference_gently(r, oid, 1); - if (commit) - clear_commit_marks(commit, ALL_REV_FLAGS | PREREQ_MARK); - } - release_revisions(&revs); return ret; } @@ -1613,8 +1613,10 @@ int repo_interpret_branch_name(struct repository *r, int validate_headref(const char *ref); -int base_name_compare(const char *name1, int len1, int mode1, const char *name2, int len2, int mode2); -int df_name_compare(const char *name1, int len1, int mode1, const char *name2, int len2, int mode2); +int base_name_compare(const char *name1, size_t len1, int mode1, + const char *name2, size_t len2, int mode2); +int df_name_compare(const char *name1, size_t len1, int mode1, + const char *name2, size_t len2, int mode2); int name_compare(const char *name1, size_t len1, const char *name2, size_t len2); int cache_name_stage_compare(const char *name1, int len1, int stage1, const char *name2, int len2, int stage2); diff --git a/ci/run-build-and-tests.sh b/ci/run-build-and-tests.sh index 8ebff42596..b098e10f52 100755 --- a/ci/run-build-and-tests.sh +++ b/ci/run-build-and-tests.sh @@ -26,7 +26,6 @@ linux-TEST-vars) export GIT_TEST_COMMIT_GRAPH_CHANGED_PATHS=1 export GIT_TEST_MULTI_PACK_INDEX=1 export GIT_TEST_MULTI_PACK_INDEX_WRITE_BITMAP=1 - export GIT_TEST_ADD_I_USE_BUILTIN=0 export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=master export GIT_TEST_WRITE_REV_INDEX=1 export GIT_TEST_CHECKOUT_WORKERS=2 @@ -1033,6 +1033,7 @@ struct commit *get_fork_point(const char *refname, struct commit *commit) ret = bases->item; cleanup_return: + free(revs.commit); free_commit_list(bases); free(full_refname); return ret; @@ -274,8 +274,6 @@ struct ref; int for_each_commit_graft(each_commit_graft_fn, void *); int interactive_add(const char **argv, const char *prefix, int patch); -int run_add_interactive(const char *revision, const char *patch_mode, - const struct pathspec *pathspec); struct commit_extra_header { struct commit_extra_header *next; @@ -448,15 +448,6 @@ void git_configset_init(struct config_set *cs); int git_configset_add_file(struct config_set *cs, const char *filename); /** - * Parses command line options and environment variables, and adds the - * variable-value pairs to the `config_set`. Returns 0 on success, or -1 - * if there is an error in parsing. The caller decides whether to free - * the incomplete configset or continue using it when the function - * returns -1. - */ -int git_configset_add_parameters(struct config_set *cs); - -/** * Finds and returns the value list, sorted in order of increasing priority * for the configuration variable `key` and config set `cs`. When the * configuration variable `key` is not found, returns NULL. The caller diff --git a/dir-iterator.c b/dir-iterator.c index b17e9f970a..3764dd81a1 100644 --- a/dir-iterator.c +++ b/dir-iterator.c @@ -203,7 +203,7 @@ struct dir_iterator *dir_iterator_begin(const char *path, unsigned int flags) { struct dir_iterator_int *iter = xcalloc(1, sizeof(*iter)); struct dir_iterator *dir_iterator = &iter->base; - int saved_errno; + int saved_errno, err; strbuf_init(&iter->base.path, PATH_MAX); strbuf_addstr(&iter->base.path, path); @@ -213,10 +213,15 @@ struct dir_iterator *dir_iterator_begin(const char *path, unsigned int flags) iter->flags = flags; /* - * Note: stat already checks for NULL or empty strings and - * inexistent paths. + * Note: stat/lstat already checks for NULL or empty strings and + * nonexistent paths. */ - if (stat(iter->base.path.buf, &iter->base.st) < 0) { + if (iter->flags & DIR_ITERATOR_FOLLOW_SYMLINKS) + err = stat(iter->base.path.buf, &iter->base.st); + else + err = lstat(iter->base.path.buf, &iter->base.st); + + if (err < 0) { saved_errno = errno; goto error_out; } diff --git a/dir-iterator.h b/dir-iterator.h index 08229157c6..e3b6ff2800 100644 --- a/dir-iterator.h +++ b/dir-iterator.h @@ -61,6 +61,11 @@ * not the symlinks themselves, which is the default behavior. Broken * symlinks are ignored. * + * Note: setting DIR_ITERATOR_FOLLOW_SYMLINKS affects resolving the + * starting path as well (e.g., attempting to iterate starting at a + * symbolic link pointing to a directory without FOLLOW_SYMLINKS will + * result in an error). + * * Warning: circular symlinks are also followed when * DIR_ITERATOR_FOLLOW_SYMLINKS is set. The iteration may end up with * an ELOOP if they happen and DIR_ITERATOR_PEDANTIC is set. diff --git a/git-add--interactive.perl b/git-add--interactive.perl deleted file mode 100755 index 95887fd8e5..0000000000 --- a/git-add--interactive.perl +++ /dev/null @@ -1,1920 +0,0 @@ -#!/usr/bin/perl - -use 5.008; -use strict; -use warnings; -use Git qw(unquote_path); -use Git::I18N; - -binmode(STDOUT, ":raw"); - -my $repo = Git->repository(); - -my $menu_use_color = $repo->get_colorbool('color.interactive'); -my ($prompt_color, $header_color, $help_color) = - $menu_use_color ? ( - $repo->get_color('color.interactive.prompt', 'bold blue'), - $repo->get_color('color.interactive.header', 'bold'), - $repo->get_color('color.interactive.help', 'red bold'), - ) : (); -my $error_color = (); -if ($menu_use_color) { - my $help_color_spec = ($repo->config('color.interactive.help') or - 'red bold'); - $error_color = $repo->get_color('color.interactive.error', - $help_color_spec); -} - -my $diff_use_color = $repo->get_colorbool('color.diff'); -my ($fraginfo_color) = - $diff_use_color ? ( - $repo->get_color('color.diff.frag', 'cyan'), - ) : (); -my ($diff_context_color) = - $diff_use_color ? ( - $repo->get_color($repo->config('color.diff.context') ? 'color.diff.context' : 'color.diff.plain', ''), - ) : (); -my ($diff_old_color) = - $diff_use_color ? ( - $repo->get_color('color.diff.old', 'red'), - ) : (); -my ($diff_new_color) = - $diff_use_color ? ( - $repo->get_color('color.diff.new', 'green'), - ) : (); - -my $normal_color = $repo->get_color("", "reset"); - -my $diff_algorithm = $repo->config('diff.algorithm'); -my $diff_filter = $repo->config('interactive.difffilter'); - -my $use_readkey = 0; -my $use_termcap = 0; -my %term_escapes; - -sub ReadMode; -sub ReadKey; -if ($repo->config_bool("interactive.singlekey")) { - eval { - require Term::ReadKey; - Term::ReadKey->import; - $use_readkey = 1; - }; - if (!$use_readkey) { - print STDERR "missing Term::ReadKey, disabling interactive.singlekey\n"; - } - eval { - require Term::Cap; - my $termcap = Term::Cap->Tgetent; - foreach (values %$termcap) { - $term_escapes{$_} = 1 if /^\e/; - } - $use_termcap = 1; - }; -} - -sub colored { - my $color = shift; - my $string = join("", @_); - - if (defined $color) { - # Put a color code at the beginning of each line, a reset at the end - # color after newlines that are not at the end of the string - $string =~ s/(\n+)(.)/$1$color$2/g; - # reset before newlines - $string =~ s/(\n+)/$normal_color$1/g; - # codes at beginning and end (if necessary): - $string =~ s/^/$color/; - $string =~ s/$/$normal_color/ unless $string =~ /\n$/; - } - return $string; -} - -# command line options -my $patch_mode_only; -my $patch_mode; -my $patch_mode_revision; - -sub apply_patch; -sub apply_patch_for_checkout_commit; -sub apply_patch_for_stash; - -my %patch_modes = ( - 'stage' => { - DIFF => 'diff-files -p', - APPLY => sub { apply_patch 'apply --cached', @_; }, - APPLY_CHECK => 'apply --cached', - FILTER => 'file-only', - IS_REVERSE => 0, - }, - 'stash' => { - DIFF => 'diff-index -p HEAD', - APPLY => sub { apply_patch 'apply --cached', @_; }, - APPLY_CHECK => 'apply --cached', - FILTER => undef, - IS_REVERSE => 0, - }, - 'reset_head' => { - DIFF => 'diff-index -p --cached', - APPLY => sub { apply_patch 'apply -R --cached', @_; }, - APPLY_CHECK => 'apply -R --cached', - FILTER => 'index-only', - IS_REVERSE => 1, - }, - 'reset_nothead' => { - DIFF => 'diff-index -R -p --cached', - APPLY => sub { apply_patch 'apply --cached', @_; }, - APPLY_CHECK => 'apply --cached', - FILTER => 'index-only', - IS_REVERSE => 0, - }, - 'checkout_index' => { - DIFF => 'diff-files -p', - APPLY => sub { apply_patch 'apply -R', @_; }, - APPLY_CHECK => 'apply -R', - FILTER => 'file-only', - IS_REVERSE => 1, - }, - 'checkout_head' => { - DIFF => 'diff-index -p', - APPLY => sub { apply_patch_for_checkout_commit '-R', @_ }, - APPLY_CHECK => 'apply -R', - FILTER => undef, - IS_REVERSE => 1, - }, - 'checkout_nothead' => { - DIFF => 'diff-index -R -p', - APPLY => sub { apply_patch_for_checkout_commit '', @_ }, - APPLY_CHECK => 'apply', - FILTER => undef, - IS_REVERSE => 0, - }, - 'worktree_head' => { - DIFF => 'diff-index -p', - APPLY => sub { apply_patch 'apply -R', @_ }, - APPLY_CHECK => 'apply -R', - FILTER => undef, - IS_REVERSE => 1, - }, - 'worktree_nothead' => { - DIFF => 'diff-index -R -p', - APPLY => sub { apply_patch 'apply', @_ }, - APPLY_CHECK => 'apply', - FILTER => undef, - IS_REVERSE => 0, - }, -); - -$patch_mode = 'stage'; -my %patch_mode_flavour = %{$patch_modes{$patch_mode}}; - -sub run_cmd_pipe { - if ($^O eq 'MSWin32') { - my @invalid = grep {m/[":*]/} @_; - die "$^O does not support: @invalid\n" if @invalid; - my @args = map { m/ /o ? "\"$_\"": $_ } @_; - return qx{@args}; - } else { - my $fh = undef; - open($fh, '-|', @_) or die; - my @out = <$fh>; - close $fh || die "Cannot close @_ ($!)"; - return @out; - } -} - -my ($GIT_DIR) = run_cmd_pipe(qw(git rev-parse --git-dir)); - -if (!defined $GIT_DIR) { - exit(1); # rev-parse would have already said "not a git repo" -} -chomp($GIT_DIR); - -sub refresh { - my $fh; - open $fh, 'git update-index --refresh |' - or die; - while (<$fh>) { - ;# ignore 'needs update' - } - close $fh; -} - -sub list_untracked { - map { - chomp $_; - unquote_path($_); - } - run_cmd_pipe(qw(git ls-files --others --exclude-standard --), @ARGV); -} - -# TRANSLATORS: you can adjust this to align "git add -i" status menu -my $status_fmt = __('%12s %12s %s'); -my $status_head = sprintf($status_fmt, __('staged'), __('unstaged'), __('path')); - -{ - my $initial; - sub is_initial_commit { - $initial = system('git rev-parse HEAD -- >/dev/null 2>&1') != 0 - unless defined $initial; - return $initial; - } -} - -{ - my $empty_tree; - sub get_empty_tree { - return $empty_tree if defined $empty_tree; - - ($empty_tree) = run_cmd_pipe(qw(git hash-object -t tree /dev/null)); - chomp $empty_tree; - return $empty_tree; - } -} - -sub get_diff_reference { - my $ref = shift; - if (defined $ref and $ref ne 'HEAD') { - return $ref; - } elsif (is_initial_commit()) { - return get_empty_tree(); - } else { - return 'HEAD'; - } -} - -# Returns list of hashes, contents of each of which are: -# VALUE: pathname -# BINARY: is a binary path -# INDEX: is index different from HEAD? -# FILE: is file different from index? -# INDEX_ADDDEL: is it add/delete between HEAD and index? -# FILE_ADDDEL: is it add/delete between index and file? -# UNMERGED: is the path unmerged - -sub list_modified { - my ($only) = @_; - my (%data, @return); - my ($add, $del, $adddel, $file); - - my $reference = get_diff_reference($patch_mode_revision); - for (run_cmd_pipe(qw(git diff-index --cached - --numstat --summary), $reference, - '--', @ARGV)) { - if (($add, $del, $file) = - /^([-\d]+) ([-\d]+) (.*)/) { - my ($change, $bin); - $file = unquote_path($file); - if ($add eq '-' && $del eq '-') { - $change = __('binary'); - $bin = 1; - } - else { - $change = "+$add/-$del"; - } - $data{$file} = { - INDEX => $change, - BINARY => $bin, - FILE => __('nothing'), - } - } - elsif (($adddel, $file) = - /^ (create|delete) mode [0-7]+ (.*)$/) { - $file = unquote_path($file); - $data{$file}{INDEX_ADDDEL} = $adddel; - } - } - - for (run_cmd_pipe(qw(git diff-files --ignore-submodules=dirty --numstat --summary --raw --), @ARGV)) { - if (($add, $del, $file) = - /^([-\d]+) ([-\d]+) (.*)/) { - $file = unquote_path($file); - my ($change, $bin); - if ($add eq '-' && $del eq '-') { - $change = __('binary'); - $bin = 1; - } - else { - $change = "+$add/-$del"; - } - $data{$file}{FILE} = $change; - if ($bin) { - $data{$file}{BINARY} = 1; - } - } - elsif (($adddel, $file) = - /^ (create|delete) mode [0-7]+ (.*)$/) { - $file = unquote_path($file); - $data{$file}{FILE_ADDDEL} = $adddel; - } - elsif (/^:[0-7]+ [0-7]+ [0-9a-f]+ [0-9a-f]+ (.) (.*)$/) { - $file = unquote_path($2); - if (!exists $data{$file}) { - $data{$file} = +{ - INDEX => __('unchanged'), - BINARY => 0, - }; - } - if ($1 eq 'U') { - $data{$file}{UNMERGED} = 1; - } - } - } - - for (sort keys %data) { - my $it = $data{$_}; - - if ($only) { - if ($only eq 'index-only') { - next if ($it->{INDEX} eq __('unchanged')); - } - if ($only eq 'file-only') { - next if ($it->{FILE} eq __('nothing')); - } - } - push @return, +{ - VALUE => $_, - %$it, - }; - } - return @return; -} - -sub find_unique { - my ($string, @stuff) = @_; - my $found = undef; - for (my $i = 0; $i < @stuff; $i++) { - my $it = $stuff[$i]; - my $hit = undef; - if (ref $it) { - if ((ref $it) eq 'ARRAY') { - $it = $it->[0]; - } - else { - $it = $it->{VALUE}; - } - } - eval { - if ($it =~ /^$string/) { - $hit = 1; - }; - }; - if (defined $hit && defined $found) { - return undef; - } - if ($hit) { - $found = $i + 1; - } - } - return $found; -} - -# inserts string into trie and updates count for each character -sub update_trie { - my ($trie, $string) = @_; - foreach (split //, $string) { - $trie = $trie->{$_} ||= {COUNT => 0}; - $trie->{COUNT}++; - } -} - -# returns an array of tuples (prefix, remainder) -sub find_unique_prefixes { - my @stuff = @_; - my @return = (); - - # any single prefix exceeding the soft limit is omitted - # if any prefix exceeds the hard limit all are omitted - # 0 indicates no limit - my $soft_limit = 0; - my $hard_limit = 3; - - # build a trie modelling all possible options - my %trie; - foreach my $print (@stuff) { - if ((ref $print) eq 'ARRAY') { - $print = $print->[0]; - } - elsif ((ref $print) eq 'HASH') { - $print = $print->{VALUE}; - } - update_trie(\%trie, $print); - push @return, $print; - } - - # use the trie to find the unique prefixes - for (my $i = 0; $i < @return; $i++) { - my $ret = $return[$i]; - my @letters = split //, $ret; - my %search = %trie; - my ($prefix, $remainder); - my $j; - for ($j = 0; $j < @letters; $j++) { - my $letter = $letters[$j]; - if ($search{$letter}{COUNT} == 1) { - $prefix = substr $ret, 0, $j + 1; - $remainder = substr $ret, $j + 1; - last; - } - else { - my $prefix = substr $ret, 0, $j; - return () - if ($hard_limit && $j + 1 > $hard_limit); - } - %search = %{$search{$letter}}; - } - if (ord($letters[0]) > 127 || - ($soft_limit && $j + 1 > $soft_limit)) { - $prefix = undef; - $remainder = $ret; - } - $return[$i] = [$prefix, $remainder]; - } - return @return; -} - -# filters out prefixes which have special meaning to list_and_choose() -sub is_valid_prefix { - my $prefix = shift; - return (defined $prefix) && - !($prefix =~ /[\s,]/) && # separators - !($prefix =~ /^-/) && # deselection - !($prefix =~ /^\d+/) && # selection - ($prefix ne '*') && # "all" wildcard - ($prefix ne '?'); # prompt help -} - -# given a prefix/remainder tuple return a string with the prefix highlighted -# for now use square brackets; later might use ANSI colors (underline, bold) -sub highlight_prefix { - my $prefix = shift; - my $remainder = shift; - - if (!defined $prefix) { - return $remainder; - } - - if (!is_valid_prefix($prefix)) { - return "$prefix$remainder"; - } - - if (!$menu_use_color) { - return "[$prefix]$remainder"; - } - - return "$prompt_color$prefix$normal_color$remainder"; -} - -sub error_msg { - print STDERR colored $error_color, @_; -} - -sub list_and_choose { - my ($opts, @stuff) = @_; - my (@chosen, @return); - if (!@stuff) { - return @return; - } - my $i; - my @prefixes = find_unique_prefixes(@stuff) unless $opts->{LIST_ONLY}; - - TOPLOOP: - while (1) { - my $last_lf = 0; - - if ($opts->{HEADER}) { - my $indent = $opts->{LIST_FLAT} ? "" : " "; - print colored $header_color, "$indent$opts->{HEADER}\n"; - } - for ($i = 0; $i < @stuff; $i++) { - my $chosen = $chosen[$i] ? '*' : ' '; - my $print = $stuff[$i]; - my $ref = ref $print; - my $highlighted = highlight_prefix(@{$prefixes[$i]}) - if @prefixes; - if ($ref eq 'ARRAY') { - $print = $highlighted || $print->[0]; - } - elsif ($ref eq 'HASH') { - my $value = $highlighted || $print->{VALUE}; - $print = sprintf($status_fmt, - $print->{INDEX}, - $print->{FILE}, - $value); - } - else { - $print = $highlighted || $print; - } - printf("%s%2d: %s", $chosen, $i+1, $print); - if (($opts->{LIST_FLAT}) && - (($i + 1) % ($opts->{LIST_FLAT}))) { - print "\t"; - $last_lf = 0; - } - else { - print "\n"; - $last_lf = 1; - } - } - if (!$last_lf) { - print "\n"; - } - - return if ($opts->{LIST_ONLY}); - - print colored $prompt_color, $opts->{PROMPT}; - if ($opts->{SINGLETON}) { - print "> "; - } - else { - print ">> "; - } - my $line = <STDIN>; - if (!$line) { - print "\n"; - $opts->{ON_EOF}->() if $opts->{ON_EOF}; - last; - } - chomp $line; - last if $line eq ''; - if ($line eq '?') { - $opts->{SINGLETON} ? - singleton_prompt_help_cmd() : - prompt_help_cmd(); - next TOPLOOP; - } - for my $choice (split(/[\s,]+/, $line)) { - my $choose = 1; - my ($bottom, $top); - - # Input that begins with '-'; unchoose - if ($choice =~ s/^-//) { - $choose = 0; - } - # A range can be specified like 5-7 or 5-. - if ($choice =~ /^(\d+)-(\d*)$/) { - ($bottom, $top) = ($1, length($2) ? $2 : 1 + @stuff); - } - elsif ($choice =~ /^\d+$/) { - $bottom = $top = $choice; - } - elsif ($choice eq '*') { - $bottom = 1; - $top = 1 + @stuff; - } - else { - $bottom = $top = find_unique($choice, @stuff); - if (!defined $bottom) { - error_msg sprintf(__("Huh (%s)?\n"), $choice); - next TOPLOOP; - } - } - if ($opts->{SINGLETON} && $bottom != $top) { - error_msg sprintf(__("Huh (%s)?\n"), $choice); - next TOPLOOP; - } - for ($i = $bottom-1; $i <= $top-1; $i++) { - next if (@stuff <= $i || $i < 0); - $chosen[$i] = $choose; - } - } - last if ($opts->{IMMEDIATE} || $line eq '*'); - } - for ($i = 0; $i < @stuff; $i++) { - if ($chosen[$i]) { - push @return, $stuff[$i]; - } - } - return @return; -} - -sub singleton_prompt_help_cmd { - print colored $help_color, __ <<'EOF' ; -Prompt help: -1 - select a numbered item -foo - select item based on unique prefix - - (empty) select nothing -EOF -} - -sub prompt_help_cmd { - print colored $help_color, __ <<'EOF' ; -Prompt help: -1 - select a single item -3-5 - select a range of items -2-3,6-9 - select multiple ranges -foo - select item based on unique prefix --... - unselect specified items -* - choose all items - - (empty) finish selecting -EOF -} - -sub status_cmd { - list_and_choose({ LIST_ONLY => 1, HEADER => $status_head }, - list_modified()); - print "\n"; -} - -sub say_n_paths { - my $did = shift @_; - my $cnt = scalar @_; - if ($did eq 'added') { - printf(__n("added %d path\n", "added %d paths\n", - $cnt), $cnt); - } elsif ($did eq 'updated') { - printf(__n("updated %d path\n", "updated %d paths\n", - $cnt), $cnt); - } elsif ($did eq 'reverted') { - printf(__n("reverted %d path\n", "reverted %d paths\n", - $cnt), $cnt); - } else { - printf(__n("touched %d path\n", "touched %d paths\n", - $cnt), $cnt); - } -} - -sub update_cmd { - my @mods = list_modified('file-only'); - return if (!@mods); - - my @update = list_and_choose({ PROMPT => __('Update'), - HEADER => $status_head, }, - @mods); - if (@update) { - system(qw(git update-index --add --remove --), - map { $_->{VALUE} } @update); - say_n_paths('updated', @update); - } - print "\n"; -} - -sub revert_cmd { - my @update = list_and_choose({ PROMPT => __('Revert'), - HEADER => $status_head, }, - list_modified()); - if (@update) { - if (is_initial_commit()) { - system(qw(git rm --cached), - map { $_->{VALUE} } @update); - } - else { - my @lines = run_cmd_pipe(qw(git ls-tree HEAD --), - map { $_->{VALUE} } @update); - my $fh; - open $fh, '| git update-index --index-info' - or die; - for (@lines) { - print $fh $_; - } - close($fh); - for (@update) { - if ($_->{INDEX_ADDDEL} && - $_->{INDEX_ADDDEL} eq 'create') { - system(qw(git update-index --force-remove --), - $_->{VALUE}); - printf(__("note: %s is untracked now.\n"), $_->{VALUE}); - } - } - } - refresh(); - say_n_paths('reverted', @update); - } - print "\n"; -} - -sub add_untracked_cmd { - my @add = list_and_choose({ PROMPT => __('Add untracked') }, - list_untracked()); - if (@add) { - system(qw(git update-index --add --), @add); - say_n_paths('added', @add); - } else { - print __("No untracked files.\n"); - } - print "\n"; -} - -sub run_git_apply { - my $cmd = shift; - my $fh; - open $fh, '| git ' . $cmd . " --allow-overlap"; - print $fh @_; - return close $fh; -} - -sub parse_diff { - my ($path) = @_; - my @diff_cmd = split(" ", $patch_mode_flavour{DIFF}); - if (defined $diff_algorithm) { - splice @diff_cmd, 1, 0, "--diff-algorithm=${diff_algorithm}"; - } - if (defined $patch_mode_revision) { - push @diff_cmd, get_diff_reference($patch_mode_revision); - } - my @diff = run_cmd_pipe("git", @diff_cmd, qw(--no-color --), $path); - my @colored = (); - if ($diff_use_color) { - my @display_cmd = ("git", @diff_cmd, qw(--color --), $path); - if (defined $diff_filter) { - # quotemeta is overkill, but sufficient for shell-quoting - my $diff = join(' ', map { quotemeta } @display_cmd); - @display_cmd = ("$diff | $diff_filter"); - } - - @colored = run_cmd_pipe(@display_cmd); - } - my (@hunk) = { TEXT => [], DISPLAY => [], TYPE => 'header' }; - - if (@colored && @colored != @diff) { - print STDERR - "fatal: mismatched output from interactive.diffFilter\n", - "hint: Your filter must maintain a one-to-one correspondence\n", - "hint: between its input and output lines.\n"; - exit 1; - } - - for (my $i = 0; $i < @diff; $i++) { - if ($diff[$i] =~ /^@@ /) { - push @hunk, { TEXT => [], DISPLAY => [], - TYPE => 'hunk' }; - } - push @{$hunk[-1]{TEXT}}, $diff[$i]; - push @{$hunk[-1]{DISPLAY}}, - (@colored ? $colored[$i] : $diff[$i]); - } - return @hunk; -} - -sub parse_diff_header { - my $src = shift; - - my $head = { TEXT => [], DISPLAY => [], TYPE => 'header' }; - my $mode = { TEXT => [], DISPLAY => [], TYPE => 'mode' }; - my $deletion = { TEXT => [], DISPLAY => [], TYPE => 'deletion' }; - my $addition; - - for (my $i = 0; $i < @{$src->{TEXT}}; $i++) { - if ($src->{TEXT}->[$i] =~ /^new file/) { - $addition = 1; - $head->{TYPE} = 'addition'; - } - my $dest = - $src->{TEXT}->[$i] =~ /^(old|new) mode (\d+)$/ ? $mode : - $src->{TEXT}->[$i] =~ /^deleted file/ ? $deletion : - $head; - push @{$dest->{TEXT}}, $src->{TEXT}->[$i]; - push @{$dest->{DISPLAY}}, $src->{DISPLAY}->[$i]; - } - return ($head, $mode, $deletion, $addition); -} - -sub hunk_splittable { - my ($text) = @_; - - my @s = split_hunk($text); - return (1 < @s); -} - -sub parse_hunk_header { - my ($line) = @_; - my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) = - $line =~ /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/; - $o_cnt = 1 unless defined $o_cnt; - $n_cnt = 1 unless defined $n_cnt; - return ($o_ofs, $o_cnt, $n_ofs, $n_cnt); -} - -sub format_hunk_header { - my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) = @_; - return ("@@ -$o_ofs" . - (($o_cnt != 1) ? ",$o_cnt" : '') . - " +$n_ofs" . - (($n_cnt != 1) ? ",$n_cnt" : '') . - " @@\n"); -} - -sub split_hunk { - my ($text, $display) = @_; - my @split = (); - if (!defined $display) { - $display = $text; - } - # If there are context lines in the middle of a hunk, - # it can be split, but we would need to take care of - # overlaps later. - - my ($o_ofs, undef, $n_ofs) = parse_hunk_header($text->[0]); - my $hunk_start = 1; - - OUTER: - while (1) { - my $next_hunk_start = undef; - my $i = $hunk_start - 1; - my $this = +{ - TEXT => [], - DISPLAY => [], - TYPE => 'hunk', - OLD => $o_ofs, - NEW => $n_ofs, - OCNT => 0, - NCNT => 0, - ADDDEL => 0, - POSTCTX => 0, - USE => undef, - }; - - while (++$i < @$text) { - my $line = $text->[$i]; - my $display = $display->[$i]; - if ($line =~ /^\\/) { - push @{$this->{TEXT}}, $line; - push @{$this->{DISPLAY}}, $display; - next; - } - if ($line =~ /^ /) { - if ($this->{ADDDEL} && - !defined $next_hunk_start) { - # We have seen leading context and - # adds/dels and then here is another - # context, which is trailing for this - # split hunk and leading for the next - # one. - $next_hunk_start = $i; - } - push @{$this->{TEXT}}, $line; - push @{$this->{DISPLAY}}, $display; - $this->{OCNT}++; - $this->{NCNT}++; - if (defined $next_hunk_start) { - $this->{POSTCTX}++; - } - next; - } - - # add/del - if (defined $next_hunk_start) { - # We are done with the current hunk and - # this is the first real change for the - # next split one. - $hunk_start = $next_hunk_start; - $o_ofs = $this->{OLD} + $this->{OCNT}; - $n_ofs = $this->{NEW} + $this->{NCNT}; - $o_ofs -= $this->{POSTCTX}; - $n_ofs -= $this->{POSTCTX}; - push @split, $this; - redo OUTER; - } - push @{$this->{TEXT}}, $line; - push @{$this->{DISPLAY}}, $display; - $this->{ADDDEL}++; - if ($line =~ /^-/) { - $this->{OCNT}++; - } - else { - $this->{NCNT}++; - } - } - - push @split, $this; - last; - } - - for my $hunk (@split) { - $o_ofs = $hunk->{OLD}; - $n_ofs = $hunk->{NEW}; - my $o_cnt = $hunk->{OCNT}; - my $n_cnt = $hunk->{NCNT}; - - my $head = format_hunk_header($o_ofs, $o_cnt, $n_ofs, $n_cnt); - my $display_head = $head; - unshift @{$hunk->{TEXT}}, $head; - if ($diff_use_color) { - $display_head = colored($fraginfo_color, $head); - } - unshift @{$hunk->{DISPLAY}}, $display_head; - } - return @split; -} - -sub find_last_o_ctx { - my ($it) = @_; - my $text = $it->{TEXT}; - my ($o_ofs, $o_cnt) = parse_hunk_header($text->[0]); - my $i = @{$text}; - my $last_o_ctx = $o_ofs + $o_cnt; - while (0 < --$i) { - my $line = $text->[$i]; - if ($line =~ /^ /) { - $last_o_ctx--; - next; - } - last; - } - return $last_o_ctx; -} - -sub merge_hunk { - my ($prev, $this) = @_; - my ($o0_ofs, $o0_cnt, $n0_ofs, $n0_cnt) = - parse_hunk_header($prev->{TEXT}[0]); - my ($o1_ofs, $o1_cnt, $n1_ofs, $n1_cnt) = - parse_hunk_header($this->{TEXT}[0]); - - my (@line, $i, $ofs, $o_cnt, $n_cnt); - $ofs = $o0_ofs; - $o_cnt = $n_cnt = 0; - for ($i = 1; $i < @{$prev->{TEXT}}; $i++) { - my $line = $prev->{TEXT}[$i]; - if ($line =~ /^\+/) { - $n_cnt++; - push @line, $line; - next; - } elsif ($line =~ /^\\/) { - push @line, $line; - next; - } - - last if ($o1_ofs <= $ofs); - - $o_cnt++; - $ofs++; - if ($line =~ /^ /) { - $n_cnt++; - } - push @line, $line; - } - - for ($i = 1; $i < @{$this->{TEXT}}; $i++) { - my $line = $this->{TEXT}[$i]; - if ($line =~ /^\+/) { - $n_cnt++; - push @line, $line; - next; - } elsif ($line =~ /^\\/) { - push @line, $line; - next; - } - $ofs++; - $o_cnt++; - if ($line =~ /^ /) { - $n_cnt++; - } - push @line, $line; - } - my $head = format_hunk_header($o0_ofs, $o_cnt, $n0_ofs, $n_cnt); - @{$prev->{TEXT}} = ($head, @line); -} - -sub coalesce_overlapping_hunks { - my (@in) = @_; - my @out = (); - - my ($last_o_ctx, $last_was_dirty); - my $ofs_delta = 0; - - for (@in) { - if ($_->{TYPE} ne 'hunk') { - push @out, $_; - next; - } - my $text = $_->{TEXT}; - my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) = - parse_hunk_header($text->[0]); - unless ($_->{USE}) { - $ofs_delta += $o_cnt - $n_cnt; - # If this hunk has been edited then subtract - # the delta that is due to the edit. - if ($_->{OFS_DELTA}) { - $ofs_delta -= $_->{OFS_DELTA}; - } - next; - } - if ($ofs_delta) { - if ($patch_mode_flavour{IS_REVERSE}) { - $o_ofs -= $ofs_delta; - } else { - $n_ofs += $ofs_delta; - } - $_->{TEXT}->[0] = format_hunk_header($o_ofs, $o_cnt, - $n_ofs, $n_cnt); - } - # If this hunk was edited then adjust the offset delta - # to reflect the edit. - if ($_->{OFS_DELTA}) { - $ofs_delta += $_->{OFS_DELTA}; - } - if (defined $last_o_ctx && - $o_ofs <= $last_o_ctx && - !$_->{DIRTY} && - !$last_was_dirty) { - merge_hunk($out[-1], $_); - } - else { - push @out, $_; - } - $last_o_ctx = find_last_o_ctx($out[-1]); - $last_was_dirty = $_->{DIRTY}; - } - return @out; -} - -sub reassemble_patch { - my $head = shift; - my @patch; - - # Include everything in the header except the beginning of the diff. - push @patch, (grep { !/^[-+]{3}/ } @$head); - - # Then include any headers from the hunk lines, which must - # come before any actual hunk. - while (@_ && $_[0] !~ /^@/) { - push @patch, shift; - } - - # Then begin the diff. - push @patch, grep { /^[-+]{3}/ } @$head; - - # And then the actual hunks. - push @patch, @_; - - return @patch; -} - -sub color_diff { - return map { - colored((/^@/ ? $fraginfo_color : - /^\+/ ? $diff_new_color : - /^-/ ? $diff_old_color : - $diff_context_color), - $_); - } @_; -} - -my %edit_hunk_manually_modes = ( - stage => N__( -"If the patch applies cleanly, the edited hunk will immediately be -marked for staging."), - stash => N__( -"If the patch applies cleanly, the edited hunk will immediately be -marked for stashing."), - reset_head => N__( -"If the patch applies cleanly, the edited hunk will immediately be -marked for unstaging."), - reset_nothead => N__( -"If the patch applies cleanly, the edited hunk will immediately be -marked for applying."), - checkout_index => N__( -"If the patch applies cleanly, the edited hunk will immediately be -marked for discarding."), - checkout_head => N__( -"If the patch applies cleanly, the edited hunk will immediately be -marked for discarding."), - checkout_nothead => N__( -"If the patch applies cleanly, the edited hunk will immediately be -marked for applying."), - worktree_head => N__( -"If the patch applies cleanly, the edited hunk will immediately be -marked for discarding."), - worktree_nothead => N__( -"If the patch applies cleanly, the edited hunk will immediately be -marked for applying."), -); - -sub recount_edited_hunk { - local $_; - my ($oldtext, $newtext) = @_; - my ($o_cnt, $n_cnt) = (0, 0); - for (@{$newtext}[1..$#{$newtext}]) { - my $mode = substr($_, 0, 1); - if ($mode eq '-') { - $o_cnt++; - } elsif ($mode eq '+') { - $n_cnt++; - } elsif ($mode eq ' ' or $mode eq "\n") { - $o_cnt++; - $n_cnt++; - } - } - my ($o_ofs, undef, $n_ofs, undef) = - parse_hunk_header($newtext->[0]); - $newtext->[0] = format_hunk_header($o_ofs, $o_cnt, $n_ofs, $n_cnt); - my (undef, $orig_o_cnt, undef, $orig_n_cnt) = - parse_hunk_header($oldtext->[0]); - # Return the change in the number of lines inserted by this hunk - return $orig_o_cnt - $orig_n_cnt - $o_cnt + $n_cnt; -} - -sub edit_hunk_manually { - my ($oldtext) = @_; - - my $hunkfile = $repo->repo_path . "/addp-hunk-edit.diff"; - my $fh; - open $fh, '>', $hunkfile - or die sprintf(__("failed to open hunk edit file for writing: %s"), $!); - print $fh Git::comment_lines __("Manual hunk edit mode -- see bottom for a quick guide.\n"); - print $fh @$oldtext; - my $is_reverse = $patch_mode_flavour{IS_REVERSE}; - my ($remove_plus, $remove_minus) = $is_reverse ? ('-', '+') : ('+', '-'); - my $comment_line_char = Git::get_comment_line_char; - print $fh Git::comment_lines sprintf(__ <<EOF, $remove_minus, $remove_plus, $comment_line_char), ---- -To remove '%s' lines, make them ' ' lines (context). -To remove '%s' lines, delete them. -Lines starting with %s will be removed. -EOF -__($edit_hunk_manually_modes{$patch_mode}), -# TRANSLATORS: 'it' refers to the patch mentioned in the previous messages. -__ <<EOF2 ; -If it does not apply cleanly, you will be given an opportunity to -edit again. If all lines of the hunk are removed, then the edit is -aborted and the hunk is left unchanged. -EOF2 - close $fh; - - chomp(my ($editor) = run_cmd_pipe(qw(git var GIT_EDITOR))); - system('sh', '-c', $editor.' "$@"', $editor, $hunkfile); - - if ($? != 0) { - return undef; - } - - open $fh, '<', $hunkfile - or die sprintf(__("failed to open hunk edit file for reading: %s"), $!); - my @newtext = grep { !/^\Q$comment_line_char\E/ } <$fh>; - close $fh; - unlink $hunkfile; - - # Abort if nothing remains - if (!grep { /\S/ } @newtext) { - return undef; - } - - # Reinsert the first hunk header if the user accidentally deleted it - if ($newtext[0] !~ /^@/) { - unshift @newtext, $oldtext->[0]; - } - return \@newtext; -} - -sub diff_applies { - return run_git_apply($patch_mode_flavour{APPLY_CHECK} . ' --check', - map { @{$_->{TEXT}} } @_); -} - -sub _restore_terminal_and_die { - ReadMode 'restore'; - print "\n"; - exit 1; -} - -sub prompt_single_character { - if ($use_readkey) { - local $SIG{TERM} = \&_restore_terminal_and_die; - local $SIG{INT} = \&_restore_terminal_and_die; - ReadMode 'cbreak'; - my $key = ReadKey 0; - ReadMode 'restore'; - if (defined $key) { - if ($use_termcap and $key eq "\e") { - while (!defined $term_escapes{$key}) { - my $next = ReadKey 0.5; - last if (!defined $next); - $key .= $next; - } - $key =~ s/\e/^[/; - } - print "$key"; - } - print "\n"; - return $key; - } else { - return <STDIN>; - } -} - -sub prompt_yesno { - my ($prompt) = @_; - while (1) { - print colored $prompt_color, $prompt; - my $line = prompt_single_character; - return undef unless defined $line; - return 0 if $line =~ /^n/i; - return 1 if $line =~ /^y/i; - } -} - -sub edit_hunk_loop { - my ($head, $hunks, $ix) = @_; - my $hunk = $hunks->[$ix]; - my $text = $hunk->{TEXT}; - - while (1) { - my $newtext = edit_hunk_manually($text); - if (!defined $newtext) { - return undef; - } - my $newhunk = { - TEXT => $newtext, - TYPE => $hunk->{TYPE}, - USE => 1, - DIRTY => 1, - }; - $newhunk->{OFS_DELTA} = recount_edited_hunk($text, $newtext); - # If this hunk has already been edited then add the - # offset delta of the previous edit to get the real - # delta from the original unedited hunk. - $hunk->{OFS_DELTA} and - $newhunk->{OFS_DELTA} += $hunk->{OFS_DELTA}; - if (diff_applies($head, - @{$hunks}[0..$ix-1], - $newhunk, - @{$hunks}[$ix+1..$#{$hunks}])) { - $newhunk->{DISPLAY} = [color_diff(@{$newtext})]; - return $newhunk; - } - else { - prompt_yesno( - # TRANSLATORS: do not translate [y/n] - # The program will only accept that input - # at this point. - # Consider translating (saying "no" discards!) as - # (saying "n" for "no" discards!) if the translation - # of the word "no" does not start with n. - __('Your edited hunk does not apply. Edit again ' - . '(saying "no" discards!) [y/n]? ') - ) or return undef; - } - } -} - -my %help_patch_modes = ( - stage => N__( -"y - stage this hunk -n - do not stage this hunk -q - quit; do not stage this hunk or any of the remaining ones -a - stage this hunk and all later hunks in the file -d - do not stage this hunk or any of the later hunks in the file"), - stash => N__( -"y - stash this hunk -n - do not stash this hunk -q - quit; do not stash this hunk or any of the remaining ones -a - stash this hunk and all later hunks in the file -d - do not stash this hunk or any of the later hunks in the file"), - reset_head => N__( -"y - unstage this hunk -n - do not unstage this hunk -q - quit; do not unstage this hunk or any of the remaining ones -a - unstage this hunk and all later hunks in the file -d - do not unstage this hunk or any of the later hunks in the file"), - reset_nothead => N__( -"y - apply this hunk to index -n - do not apply this hunk to index -q - quit; do not apply this hunk or any of the remaining ones -a - apply this hunk and all later hunks in the file -d - do not apply this hunk or any of the later hunks in the file"), - checkout_index => N__( -"y - discard this hunk from worktree -n - do not discard this hunk from worktree -q - quit; do not discard this hunk or any of the remaining ones -a - discard this hunk and all later hunks in the file -d - do not discard this hunk or any of the later hunks in the file"), - checkout_head => N__( -"y - discard this hunk from index and worktree -n - do not discard this hunk from index and worktree -q - quit; do not discard this hunk or any of the remaining ones -a - discard this hunk and all later hunks in the file -d - do not discard this hunk or any of the later hunks in the file"), - checkout_nothead => N__( -"y - apply this hunk to index and worktree -n - do not apply this hunk to index and worktree -q - quit; do not apply this hunk or any of the remaining ones -a - apply this hunk and all later hunks in the file -d - do not apply this hunk or any of the later hunks in the file"), - worktree_head => N__( -"y - discard this hunk from worktree -n - do not discard this hunk from worktree -q - quit; do not discard this hunk or any of the remaining ones -a - discard this hunk and all later hunks in the file -d - do not discard this hunk or any of the later hunks in the file"), - worktree_nothead => N__( -"y - apply this hunk to worktree -n - do not apply this hunk to worktree -q - quit; do not apply this hunk or any of the remaining ones -a - apply this hunk and all later hunks in the file -d - do not apply this hunk or any of the later hunks in the file"), -); - -sub help_patch_cmd { - local $_; - my $other = $_[0] . ",?"; - print colored $help_color, __($help_patch_modes{$patch_mode}), "\n", - map { "$_\n" } grep { - my $c = quotemeta(substr($_, 0, 1)); - $other =~ /,$c/ - } split "\n", __ <<EOF ; -g - select a hunk to go to -/ - search for a hunk matching the given regex -j - leave this hunk undecided, see next undecided hunk -J - leave this hunk undecided, see next hunk -k - leave this hunk undecided, see previous undecided hunk -K - leave this hunk undecided, see previous hunk -s - split the current hunk into smaller hunks -e - manually edit the current hunk -? - print help -EOF -} - -sub apply_patch { - my $cmd = shift; - my $ret = run_git_apply $cmd, @_; - if (!$ret) { - print STDERR @_; - } - return $ret; -} - -sub apply_patch_for_checkout_commit { - my $reverse = shift; - my $applies_index = run_git_apply 'apply '.$reverse.' --cached --check', @_; - my $applies_worktree = run_git_apply 'apply '.$reverse.' --check', @_; - - if ($applies_worktree && $applies_index) { - run_git_apply 'apply '.$reverse.' --cached', @_; - run_git_apply 'apply '.$reverse, @_; - return 1; - } elsif (!$applies_index) { - print colored $error_color, __("The selected hunks do not apply to the index!\n"); - if (prompt_yesno __("Apply them to the worktree anyway? ")) { - return run_git_apply 'apply '.$reverse, @_; - } else { - print colored $error_color, __("Nothing was applied.\n"); - return 0; - } - } else { - print STDERR @_; - return 0; - } -} - -sub patch_update_cmd { - my @all_mods = list_modified($patch_mode_flavour{FILTER}); - error_msg sprintf(__("ignoring unmerged: %s\n"), $_->{VALUE}) - for grep { $_->{UNMERGED} } @all_mods; - @all_mods = grep { !$_->{UNMERGED} } @all_mods; - - my @mods = grep { !($_->{BINARY}) } @all_mods; - my @them; - - if (!@mods) { - if (@all_mods) { - print STDERR __("Only binary files changed.\n"); - } else { - print STDERR __("No changes.\n"); - } - return 0; - } - if ($patch_mode_only) { - @them = @mods; - } - else { - @them = list_and_choose({ PROMPT => __('Patch update'), - HEADER => $status_head, }, - @mods); - } - for (@them) { - return 0 if patch_update_file($_->{VALUE}); - } -} - -# Generate a one line summary of a hunk. -sub summarize_hunk { - my $rhunk = shift; - my $summary = $rhunk->{TEXT}[0]; - - # Keep the line numbers, discard extra context. - $summary =~ s/@@(.*?)@@.*/$1 /s; - $summary .= " " x (20 - length $summary); - - # Add some user context. - for my $line (@{$rhunk->{TEXT}}) { - if ($line =~ m/^[+-].*\w/) { - $summary .= $line; - last; - } - } - - chomp $summary; - return substr($summary, 0, 80) . "\n"; -} - - -# Print a one-line summary of each hunk in the array ref in -# the first argument, starting with the index in the 2nd. -sub display_hunks { - my ($hunks, $i) = @_; - my $ctr = 0; - $i ||= 0; - for (; $i < @$hunks && $ctr < 20; $i++, $ctr++) { - my $status = " "; - if (defined $hunks->[$i]{USE}) { - $status = $hunks->[$i]{USE} ? "+" : "-"; - } - printf "%s%2d: %s", - $status, - $i + 1, - summarize_hunk($hunks->[$i]); - } - return $i; -} - -my %patch_update_prompt_modes = ( - stage => { - mode => N__("Stage mode change [y,n,q,a,d%s,?]? "), - deletion => N__("Stage deletion [y,n,q,a,d%s,?]? "), - addition => N__("Stage addition [y,n,q,a,d%s,?]? "), - hunk => N__("Stage this hunk [y,n,q,a,d%s,?]? "), - }, - stash => { - mode => N__("Stash mode change [y,n,q,a,d%s,?]? "), - deletion => N__("Stash deletion [y,n,q,a,d%s,?]? "), - addition => N__("Stash addition [y,n,q,a,d%s,?]? "), - hunk => N__("Stash this hunk [y,n,q,a,d%s,?]? "), - }, - reset_head => { - mode => N__("Unstage mode change [y,n,q,a,d%s,?]? "), - deletion => N__("Unstage deletion [y,n,q,a,d%s,?]? "), - addition => N__("Unstage addition [y,n,q,a,d%s,?]? "), - hunk => N__("Unstage this hunk [y,n,q,a,d%s,?]? "), - }, - reset_nothead => { - mode => N__("Apply mode change to index [y,n,q,a,d%s,?]? "), - deletion => N__("Apply deletion to index [y,n,q,a,d%s,?]? "), - addition => N__("Apply addition to index [y,n,q,a,d%s,?]? "), - hunk => N__("Apply this hunk to index [y,n,q,a,d%s,?]? "), - }, - checkout_index => { - mode => N__("Discard mode change from worktree [y,n,q,a,d%s,?]? "), - deletion => N__("Discard deletion from worktree [y,n,q,a,d%s,?]? "), - addition => N__("Discard addition from worktree [y,n,q,a,d%s,?]? "), - hunk => N__("Discard this hunk from worktree [y,n,q,a,d%s,?]? "), - }, - checkout_head => { - mode => N__("Discard mode change from index and worktree [y,n,q,a,d%s,?]? "), - deletion => N__("Discard deletion from index and worktree [y,n,q,a,d%s,?]? "), - addition => N__("Discard addition from index and worktree [y,n,q,a,d%s,?]? "), - hunk => N__("Discard this hunk from index and worktree [y,n,q,a,d%s,?]? "), - }, - checkout_nothead => { - mode => N__("Apply mode change to index and worktree [y,n,q,a,d%s,?]? "), - deletion => N__("Apply deletion to index and worktree [y,n,q,a,d%s,?]? "), - addition => N__("Apply addition to index and worktree [y,n,q,a,d%s,?]? "), - hunk => N__("Apply this hunk to index and worktree [y,n,q,a,d%s,?]? "), - }, - worktree_head => { - mode => N__("Discard mode change from worktree [y,n,q,a,d%s,?]? "), - deletion => N__("Discard deletion from worktree [y,n,q,a,d%s,?]? "), - addition => N__("Discard addition from worktree [y,n,q,a,d%s,?]? "), - hunk => N__("Discard this hunk from worktree [y,n,q,a,d%s,?]? "), - }, - worktree_nothead => { - mode => N__("Apply mode change to worktree [y,n,q,a,d%s,?]? "), - deletion => N__("Apply deletion to worktree [y,n,q,a,d%s,?]? "), - addition => N__("Apply addition to worktree [y,n,q,a,d%s,?]? "), - hunk => N__("Apply this hunk to worktree [y,n,q,a,d%s,?]? "), - }, -); - -sub patch_update_file { - my $quit = 0; - my ($ix, $num); - my $path = shift; - my ($head, @hunk) = parse_diff($path); - ($head, my $mode, my $deletion, my $addition) = parse_diff_header($head); - for (@{$head->{DISPLAY}}) { - print; - } - - if (@{$mode->{TEXT}}) { - unshift @hunk, $mode; - } - if (@{$deletion->{TEXT}}) { - foreach my $hunk (@hunk) { - push @{$deletion->{TEXT}}, @{$hunk->{TEXT}}; - push @{$deletion->{DISPLAY}}, @{$hunk->{DISPLAY}}; - } - @hunk = ($deletion); - } - - $num = scalar @hunk; - $ix = 0; - - while (1) { - my ($prev, $next, $other, $undecided, $i); - $other = ''; - - last if ($ix and !$num); - if ($num <= $ix) { - $ix = 0; - } - for ($i = 0; $i < $ix; $i++) { - if (!defined $hunk[$i]{USE}) { - $prev = 1; - $other .= ',k'; - last; - } - } - if ($ix) { - $other .= ',K'; - } - for ($i = $ix + 1; $i < $num; $i++) { - if (!defined $hunk[$i]{USE}) { - $next = 1; - $other .= ',j'; - last; - } - } - if ($ix < $num - 1) { - $other .= ',J'; - } - if ($num > 1) { - $other .= ',g,/'; - } - for ($i = 0; $i < $num; $i++) { - if (!defined $hunk[$i]{USE}) { - $undecided = 1; - last; - } - } - last if (!$undecided && ($num || !$addition)); - - if ($num) { - if ($hunk[$ix]{TYPE} eq 'hunk' && - hunk_splittable($hunk[$ix]{TEXT})) { - $other .= ',s'; - } - if ($hunk[$ix]{TYPE} eq 'hunk') { - $other .= ',e'; - } - for (@{$hunk[$ix]{DISPLAY}}) { - print; - } - } - my $type = $num ? $hunk[$ix]{TYPE} : $head->{TYPE}; - print colored $prompt_color, "(", ($ix+1), "/", ($num ? $num : 1), ") ", - sprintf(__($patch_update_prompt_modes{$patch_mode}{$type}), $other); - - my $line = prompt_single_character; - last unless defined $line; - if ($line) { - if ($line =~ /^y/i) { - if ($num) { - $hunk[$ix]{USE} = 1; - } else { - $head->{USE} = 1; - } - } - elsif ($line =~ /^n/i) { - if ($num) { - $hunk[$ix]{USE} = 0; - } else { - $head->{USE} = 0; - } - } - elsif ($line =~ /^a/i) { - if ($num) { - while ($ix < $num) { - if (!defined $hunk[$ix]{USE}) { - $hunk[$ix]{USE} = 1; - } - $ix++; - } - } else { - $head->{USE} = 1; - $ix++; - } - next; - } - elsif ($line =~ /^g(.*)/) { - my $response = $1; - unless ($other =~ /g/) { - error_msg __("No other hunks to goto\n"); - next; - } - my $no = $ix > 10 ? $ix - 10 : 0; - while ($response eq '') { - $no = display_hunks(\@hunk, $no); - if ($no < $num) { - print __("go to which hunk (<ret> to see more)? "); - } else { - print __("go to which hunk? "); - } - $response = <STDIN>; - if (!defined $response) { - $response = ''; - } - chomp $response; - } - if ($response !~ /^\s*\d+\s*$/) { - error_msg sprintf(__("Invalid number: '%s'\n"), - $response); - } elsif (0 < $response && $response <= $num) { - $ix = $response - 1; - } else { - error_msg sprintf(__n("Sorry, only %d hunk available.\n", - "Sorry, only %d hunks available.\n", $num), $num); - } - next; - } - elsif ($line =~ /^d/i) { - if ($num) { - while ($ix < $num) { - if (!defined $hunk[$ix]{USE}) { - $hunk[$ix]{USE} = 0; - } - $ix++; - } - } else { - $head->{USE} = 0; - $ix++; - } - next; - } - elsif ($line =~ /^q/i) { - if ($num) { - for ($i = 0; $i < $num; $i++) { - if (!defined $hunk[$i]{USE}) { - $hunk[$i]{USE} = 0; - } - } - } elsif (!defined $head->{USE}) { - $head->{USE} = 0; - } - $quit = 1; - last; - } - elsif ($line =~ m|^/(.*)|) { - my $regex = $1; - unless ($other =~ m|/|) { - error_msg __("No other hunks to search\n"); - next; - } - if ($regex eq "") { - print colored $prompt_color, __("search for regex? "); - $regex = <STDIN>; - if (defined $regex) { - chomp $regex; - } - } - my $search_string; - eval { - $search_string = qr{$regex}m; - }; - if ($@) { - my ($err,$exp) = ($@, $1); - $err =~ s/ at .*git-add--interactive line \d+, <STDIN> line \d+.*$//; - error_msg sprintf(__("Malformed search regexp %s: %s\n"), $exp, $err); - next; - } - my $iy = $ix; - while (1) { - my $text = join ("", @{$hunk[$iy]{TEXT}}); - last if ($text =~ $search_string); - $iy++; - $iy = 0 if ($iy >= $num); - if ($ix == $iy) { - error_msg __("No hunk matches the given pattern\n"); - last; - } - } - $ix = $iy; - next; - } - elsif ($line =~ /^K/) { - if ($other =~ /K/) { - $ix--; - } - else { - error_msg __("No previous hunk\n"); - } - next; - } - elsif ($line =~ /^J/) { - if ($other =~ /J/) { - $ix++; - } - else { - error_msg __("No next hunk\n"); - } - next; - } - elsif ($line =~ /^k/) { - if ($other =~ /k/) { - while (1) { - $ix--; - last if (!$ix || - !defined $hunk[$ix]{USE}); - } - } - else { - error_msg __("No previous hunk\n"); - } - next; - } - elsif ($line =~ /^j/) { - if ($other !~ /j/) { - error_msg __("No next hunk\n"); - next; - } - } - elsif ($line =~ /^s/) { - unless ($other =~ /s/) { - error_msg __("Sorry, cannot split this hunk\n"); - next; - } - my @split = split_hunk($hunk[$ix]{TEXT}, $hunk[$ix]{DISPLAY}); - if (1 < @split) { - print colored $header_color, sprintf( - __n("Split into %d hunk.\n", - "Split into %d hunks.\n", - scalar(@split)), scalar(@split)); - } - splice (@hunk, $ix, 1, @split); - $num = scalar @hunk; - next; - } - elsif ($line =~ /^e/) { - unless ($other =~ /e/) { - error_msg __("Sorry, cannot edit this hunk\n"); - next; - } - my $newhunk = edit_hunk_loop($head, \@hunk, $ix); - if (defined $newhunk) { - splice @hunk, $ix, 1, $newhunk; - } - } - else { - help_patch_cmd($other); - next; - } - # soft increment - while (1) { - $ix++; - last if ($ix >= $num || - !defined $hunk[$ix]{USE}); - } - } - } - - @hunk = coalesce_overlapping_hunks(@hunk) if ($num); - - my $n_lofs = 0; - my @result = (); - for (@hunk) { - if ($_->{USE}) { - push @result, @{$_->{TEXT}}; - } - } - - if (@result or $head->{USE}) { - my @patch = reassemble_patch($head->{TEXT}, @result); - my $apply_routine = $patch_mode_flavour{APPLY}; - &$apply_routine(@patch); - refresh(); - } - - print "\n"; - return $quit; -} - -sub diff_cmd { - my @mods = list_modified('index-only'); - @mods = grep { !($_->{BINARY}) } @mods; - return if (!@mods); - my (@them) = list_and_choose({ PROMPT => __('Review diff'), - IMMEDIATE => 1, - HEADER => $status_head, }, - @mods); - return if (!@them); - my $reference = (is_initial_commit()) ? get_empty_tree() : 'HEAD'; - system(qw(git diff -p --cached), $reference, '--', - map { $_->{VALUE} } @them); -} - -sub quit_cmd { - print __("Bye.\n"); - exit(0); -} - -sub help_cmd { -# TRANSLATORS: please do not translate the command names -# 'status', 'update', 'revert', etc. - print colored $help_color, __ <<'EOF' ; -status - show paths with changes -update - add working tree state to the staged set of changes -revert - revert staged set of changes back to the HEAD version -patch - pick hunks and update selectively -diff - view diff between HEAD and index -add untracked - add contents of untracked files to the staged set of changes -EOF -} - -sub process_args { - return unless @ARGV; - my $arg = shift @ARGV; - if ($arg =~ /--patch(?:=(.*))?/) { - if (defined $1) { - if ($1 eq 'reset') { - $patch_mode = 'reset_head'; - $patch_mode_revision = 'HEAD'; - $arg = shift @ARGV or die __("missing --"); - if ($arg ne '--') { - $patch_mode_revision = $arg; - - # NEEDSWORK: Instead of comparing to the literal "HEAD", - # compare the commit objects instead so that other ways of - # saying the same thing (such as "@") are also handled - # appropriately. - # - # This applies to the cases below too. - $patch_mode = ($arg eq 'HEAD' ? - 'reset_head' : 'reset_nothead'); - $arg = shift @ARGV or die __("missing --"); - } - } elsif ($1 eq 'checkout') { - $arg = shift @ARGV or die __("missing --"); - if ($arg eq '--') { - $patch_mode = 'checkout_index'; - } else { - $patch_mode_revision = $arg; - $patch_mode = ($arg eq 'HEAD' ? - 'checkout_head' : 'checkout_nothead'); - $arg = shift @ARGV or die __("missing --"); - } - } elsif ($1 eq 'worktree') { - $arg = shift @ARGV or die __("missing --"); - if ($arg eq '--') { - $patch_mode = 'checkout_index'; - } else { - $patch_mode_revision = $arg; - $patch_mode = ($arg eq 'HEAD' ? - 'worktree_head' : 'worktree_nothead'); - $arg = shift @ARGV or die __("missing --"); - } - } elsif ($1 eq 'stage' or $1 eq 'stash') { - $patch_mode = $1; - $arg = shift @ARGV or die __("missing --"); - } else { - die sprintf(__("unknown --patch mode: %s"), $1); - } - } else { - $patch_mode = 'stage'; - $arg = shift @ARGV or die __("missing --"); - } - die sprintf(__("invalid argument %s, expecting --"), - $arg) unless $arg eq "--"; - %patch_mode_flavour = %{$patch_modes{$patch_mode}}; - $patch_mode_only = 1; - } - elsif ($arg ne "--") { - die sprintf(__("invalid argument %s, expecting --"), $arg); - } -} - -sub main_loop { - my @cmd = ([ 'status', \&status_cmd, ], - [ 'update', \&update_cmd, ], - [ 'revert', \&revert_cmd, ], - [ 'add untracked', \&add_untracked_cmd, ], - [ 'patch', \&patch_update_cmd, ], - [ 'diff', \&diff_cmd, ], - [ 'quit', \&quit_cmd, ], - [ 'help', \&help_cmd, ], - ); - while (1) { - my ($it) = list_and_choose({ PROMPT => __('What now'), - SINGLETON => 1, - LIST_FLAT => 4, - HEADER => __('*** Commands ***'), - ON_EOF => \&quit_cmd, - IMMEDIATE => 1 }, @cmd); - if ($it) { - eval { - $it->[1]->(); - }; - if ($@) { - print "$@"; - } - } - } -} - -process_args(); -refresh(); -if ($patch_mode_only) { - patch_update_cmd(); -} -else { - status_cmd(); - main_loop(); -} @@ -262,6 +262,31 @@ static void pcre2_free(void *pointer, MAYBE_UNUSED void *memory_data) free(pointer); } +static int pcre2_jit_functional(void) +{ + static int jit_working = -1; + pcre2_code *code; + size_t off; + int err; + + if (jit_working != -1) + return jit_working; + + /* + * Try to JIT compile a simple pattern to probe if the JIT is + * working in general. It might fail for systems where creating + * memory mappings for runtime code generation is restricted. + */ + code = pcre2_compile((PCRE2_SPTR)".", 1, 0, &err, &off, NULL); + if (!code) + return 0; + + jit_working = pcre2_jit_compile(code, PCRE2_JIT_COMPLETE) == 0; + pcre2_code_free(code); + + return jit_working; +} + static void compile_pcre2_pattern(struct grep_pat *p, const struct grep_opt *opt) { int error; @@ -317,8 +342,29 @@ static void compile_pcre2_pattern(struct grep_pat *p, const struct grep_opt *opt pcre2_config(PCRE2_CONFIG_JIT, &p->pcre2_jit_on); if (p->pcre2_jit_on) { jitret = pcre2_jit_compile(p->pcre2_pattern, PCRE2_JIT_COMPLETE); - if (jitret) - die("Couldn't JIT the PCRE2 pattern '%s', got '%d'\n", p->pattern, jitret); + if (jitret == PCRE2_ERROR_NOMEMORY && !pcre2_jit_functional()) { + /* + * Even though pcre2_config(PCRE2_CONFIG_JIT, ...) + * indicated JIT support, the library might still + * fail to generate JIT code for various reasons, + * e.g. when SELinux's 'deny_execmem' or PaX's + * MPROTECT prevent creating W|X memory mappings. + * + * Instead of faling hard, fall back to interpreter + * mode, just as if the pattern was prefixed with + * '(*NO_JIT)'. + */ + p->pcre2_jit_on = 0; + return; + } else if (jitret) { + int need_clip = p->patternlen > 64; + int clip_len = need_clip ? 64 : p->patternlen; + die("Couldn't JIT the PCRE2 pattern '%.*s'%s, got '%d'%s", + clip_len, p->pattern, need_clip ? "..." : "", jitret, + pcre2_jit_functional() + ? "\nPerhaps prefix (*NO_JIT) to your pattern?" + : ""); + } /* * The pcre2_config(PCRE2_CONFIG_JIT, ...) call just diff --git a/pathspec.c b/pathspec.c index dbcfe7b321..ab70fcbe61 100644 --- a/pathspec.c +++ b/pathspec.c @@ -545,7 +545,7 @@ static void NORETURN unsupported_magic(const char *pattern, } /* * We may want to substitute "this command" with a command - * name. E.g. when add--interactive dies when running + * name. E.g. when "git add -p" or "git add -i" dies when running * "checkout -p" */ die(_("%s: pathspec magic not supported by this command: %s"), diff --git a/read-cache.c b/read-cache.c index 7bd12afb38..35e5657877 100644 --- a/read-cache.c +++ b/read-cache.c @@ -488,11 +488,11 @@ int ie_modified(struct index_state *istate, return 0; } -int base_name_compare(const char *name1, int len1, int mode1, - const char *name2, int len2, int mode2) +int base_name_compare(const char *name1, size_t len1, int mode1, + const char *name2, size_t len2, int mode2) { unsigned char c1, c2; - int len = len1 < len2 ? len1 : len2; + size_t len = len1 < len2 ? len1 : len2; int cmp; cmp = memcmp(name1, name2, len); @@ -517,11 +517,12 @@ int base_name_compare(const char *name1, int len1, int mode1, * This is used by routines that want to traverse the git namespace * but then handle conflicting entries together when possible. */ -int df_name_compare(const char *name1, int len1, int mode1, - const char *name2, int len2, int mode2) +int df_name_compare(const char *name1, size_t len1, int mode1, + const char *name2, size_t len2, int mode2) { - int len = len1 < len2 ? len1 : len2, cmp; unsigned char c1, c2; + size_t len = len1 < len2 ? len1 : len2; + int cmp; cmp = memcmp(name1, name2, len); if (cmp) diff --git a/sequencer.c b/sequencer.c index 3e4a197289..fb23f734ad 100644 --- a/sequencer.c +++ b/sequencer.c @@ -351,10 +351,25 @@ static const char *gpg_sign_opt_quoted(struct replay_opts *opts) return buf.buf; } +void replay_opts_release(struct replay_opts *opts) +{ + free(opts->gpg_sign); + free(opts->reflog_action); + free(opts->default_strategy); + free(opts->strategy); + for (size_t i = 0; i < opts->xopts_nr; i++) + free(opts->xopts[i]); + free(opts->xopts); + strbuf_release(&opts->current_fixups); + if (opts->revs) + release_revisions(opts->revs); + free(opts->revs); +} + int sequencer_remove_state(struct replay_opts *opts) { struct strbuf buf = STRBUF_INIT; - int i, ret = 0; + int ret = 0; if (is_rebase_i(opts) && strbuf_read_file(&buf, rebase_path_refs_to_delete(), 0) > 0) { @@ -373,15 +388,6 @@ int sequencer_remove_state(struct replay_opts *opts) } } - free(opts->gpg_sign); - free(opts->reflog_action); - free(opts->default_strategy); - free(opts->strategy); - for (i = 0; i < opts->xopts_nr; i++) - free(opts->xopts[i]); - free(opts->xopts); - strbuf_release(&opts->current_fixups); - strbuf_reset(&buf); strbuf_addstr(&buf, get_dir(opts)); if (remove_dir_recursively(&buf, 0)) @@ -2271,8 +2277,10 @@ static int do_pick_commit(struct repository *r, reword = 1; else if (is_fixup(command)) { if (update_squash_messages(r, command, commit, - opts, item->flags)) - return -1; + opts, item->flags)) { + res = -1; + goto leave; + } flags |= AMEND_MSG; if (!final_fixup) msg_file = rebase_path_squash_msg(); @@ -2282,9 +2290,11 @@ static int do_pick_commit(struct repository *r, } else { const char *dest = git_path_squash_msg(r); unlink(dest); - if (copy_file(dest, rebase_path_squash_msg(), 0666)) - return error(_("could not rename '%s' to '%s'"), - rebase_path_squash_msg(), dest); + if (copy_file(dest, rebase_path_squash_msg(), 0666)) { + res = error(_("could not rename '%s' to '%s'"), + rebase_path_squash_msg(), dest); + goto leave; + } unlink(git_path_merge_msg(r)); msg_file = dest; flags |= EDIT_MSG; @@ -2322,7 +2332,6 @@ static int do_pick_commit(struct repository *r, free_commit_list(common); free_commit_list(remotes); } - strbuf_release(&msgbuf); /* * If the merge was clean or if it failed due to conflict, we write @@ -2396,6 +2405,7 @@ fast_forward_edit: leave: free_message(commit, &msg); free(author); + strbuf_release(&msgbuf); update_abort_safety_file(); return res; diff --git a/sequencer.h b/sequencer.h index 888c18aad7..3bcdfa1b58 100644 --- a/sequencer.h +++ b/sequencer.h @@ -158,6 +158,7 @@ int sequencer_pick_revisions(struct repository *repo, int sequencer_continue(struct repository *repo, struct replay_opts *opts); int sequencer_rollback(struct repository *repo, struct replay_opts *opts); int sequencer_skip(struct repository *repo, struct replay_opts *opts); +void replay_opts_release(struct replay_opts *opts); int sequencer_remove_state(struct replay_opts *opts); #define TODO_LIST_KEEP_EMPTY (1U << 0) @@ -449,10 +449,6 @@ the --sparse command-line argument. GIT_TEST_PRELOAD_INDEX=<boolean> exercises the preload-index code path by overriding the minimum number of cache entries required per thread. -GIT_TEST_ADD_I_USE_BUILTIN=<boolean>, when false, disables the -built-in version of git add -i. See 'add.interactive.useBuiltin' in -git-config(1). - GIT_TEST_INDEX_THREADS=<n> enables exercising the multi-threaded loading of the index for the whole test suite by bypassing the default number of cache entries and thread minimums. Setting this to 1 will make the diff --git a/t/t0066-dir-iterator.sh b/t/t0066-dir-iterator.sh index 63a1a45cd3..04b811622b 100755 --- a/t/t0066-dir-iterator.sh +++ b/t/t0066-dir-iterator.sh @@ -110,7 +110,9 @@ test_expect_success SYMLINKS 'setup dirs with symlinks' ' mkdir -p dir5/a/c && ln -s ../c dir5/a/b/d && ln -s ../ dir5/a/b/e && - ln -s ../../ dir5/a/b/f + ln -s ../../ dir5/a/b/f && + + ln -s dir4 dir6 ' test_expect_success SYMLINKS 'dir-iterator should not follow symlinks by default' ' @@ -146,4 +148,27 @@ test_expect_success SYMLINKS 'dir-iterator should follow symlinks w/ follow flag test_cmp expected-follow-sorted-output actual-follow-sorted-output ' +test_expect_success SYMLINKS 'dir-iterator does not resolve top-level symlinks' ' + test_must_fail test-tool dir-iterator ./dir6 >out && + + grep "ENOTDIR" out +' + +test_expect_success SYMLINKS 'dir-iterator resolves top-level symlinks w/ follow flag' ' + cat >expected-follow-sorted-output <<-EOF && + [d] (a) [a] ./dir6/a + [d] (a/f) [f] ./dir6/a/f + [d] (a/f/c) [c] ./dir6/a/f/c + [d] (b) [b] ./dir6/b + [d] (b/c) [c] ./dir6/b/c + [f] (a/d) [d] ./dir6/a/d + [f] (a/e) [e] ./dir6/a/e + EOF + + test-tool dir-iterator --follow-symlinks ./dir6 >out && + sort out >actual-follow-sorted-output && + + test_cmp expected-follow-sorted-output actual-follow-sorted-output +' + test_done diff --git a/t/t2016-checkout-patch.sh b/t/t2016-checkout-patch.sh index a5822e41af..747eb5563e 100755 --- a/t/t2016-checkout-patch.sh +++ b/t/t2016-checkout-patch.sh @@ -4,12 +4,6 @@ test_description='git checkout --patch' . ./lib-patch-mode.sh -if ! test_have_prereq ADD_I_USE_BUILTIN && ! test_have_prereq PERL -then - skip_all='skipping interactive add tests, PERL not set' - test_done -fi - test_expect_success 'setup' ' mkdir dir && echo parent > dir/foo && diff --git a/t/t3405-rebase-malformed.sh b/t/t3405-rebase-malformed.sh index 2524331861..8979bc3407 100755 --- a/t/t3405-rebase-malformed.sh +++ b/t/t3405-rebase-malformed.sh @@ -5,6 +5,7 @@ test_description='rebase should handle arbitrary git message' GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh . "$TEST_DIRECTORY"/lib-rebase.sh diff --git a/t/t3412-rebase-root.sh b/t/t3412-rebase-root.sh index 58371d8a54..e75b3d0e07 100755 --- a/t/t3412-rebase-root.sh +++ b/t/t3412-rebase-root.sh @@ -7,6 +7,7 @@ Tests if git rebase --root --onto <newparent> can rebase the root commit. GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh log_with_names () { diff --git a/t/t3416-rebase-onto-threedots.sh b/t/t3416-rebase-onto-threedots.sh index ea501f2b42..f8c4ed78c9 100755 --- a/t/t3416-rebase-onto-threedots.sh +++ b/t/t3416-rebase-onto-threedots.sh @@ -5,6 +5,7 @@ test_description='git rebase --onto A...B' GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh . "$TEST_DIRECTORY/lib-rebase.sh" diff --git a/t/t3419-rebase-patch-id.sh b/t/t3419-rebase-patch-id.sh index 7181f176b8..6c61f240cf 100755 --- a/t/t3419-rebase-patch-id.sh +++ b/t/t3419-rebase-patch-id.sh @@ -5,6 +5,7 @@ test_description='git rebase - test patch id computation' GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh scramble () { diff --git a/t/t3423-rebase-reword.sh b/t/t3423-rebase-reword.sh index 4859bb8f72..2fab703d61 100755 --- a/t/t3423-rebase-reword.sh +++ b/t/t3423-rebase-reword.sh @@ -2,6 +2,7 @@ test_description='git rebase interactive with rewording' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh . "$TEST_DIRECTORY"/lib-rebase.sh diff --git a/t/t3425-rebase-topology-merges.sh b/t/t3425-rebase-topology-merges.sh index 63acc1ea4d..a16428bdf5 100755 --- a/t/t3425-rebase-topology-merges.sh +++ b/t/t3425-rebase-topology-merges.sh @@ -1,6 +1,8 @@ #!/bin/sh test_description='rebase topology tests with merges' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh . "$TEST_DIRECTORY"/lib-rebase.sh diff --git a/t/t3431-rebase-fork-point.sh b/t/t3431-rebase-fork-point.sh index 70e8136356..4bfc779bb8 100755 --- a/t/t3431-rebase-fork-point.sh +++ b/t/t3431-rebase-fork-point.sh @@ -8,6 +8,7 @@ test_description='git rebase --fork-point test' GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh # A---B---D---E (main) diff --git a/t/t3432-rebase-fast-forward.sh b/t/t3432-rebase-fast-forward.sh index 5086e14c02..7f1a5dd3de 100755 --- a/t/t3432-rebase-fast-forward.sh +++ b/t/t3432-rebase-fast-forward.sh @@ -8,6 +8,7 @@ test_description='ensure rebase fast-forwards commits when possible' GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success setup ' diff --git a/t/t3437-rebase-fixup-options.sh b/t/t3437-rebase-fixup-options.sh index c023fefd68..274699dadb 100755 --- a/t/t3437-rebase-fixup-options.sh +++ b/t/t3437-rebase-fixup-options.sh @@ -14,6 +14,7 @@ to the "fixup" command that works with "fixup!", "fixup -C" works with "amend!" upon --autosquash. ' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh . "$TEST_DIRECTORY"/lib-rebase.sh diff --git a/t/t3438-rebase-broken-files.sh b/t/t3438-rebase-broken-files.sh index b92a3ce46b..c614c4f2e4 100755 --- a/t/t3438-rebase-broken-files.sh +++ b/t/t3438-rebase-broken-files.sh @@ -1,6 +1,8 @@ #!/bin/sh test_description='rebase behavior when on-disk files are broken' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success 'set up conflicting branches' ' diff --git a/t/t3501-revert-cherry-pick.sh b/t/t3501-revert-cherry-pick.sh index 1f4cfc3744..2f3e3e2416 100755 --- a/t/t3501-revert-cherry-pick.sh +++ b/t/t3501-revert-cherry-pick.sh @@ -13,6 +13,7 @@ test_description='test cherry-pick and revert with renames GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success setup ' diff --git a/t/t3502-cherry-pick-merge.sh b/t/t3502-cherry-pick-merge.sh index 5495eacfec..1b2c0d6aca 100755 --- a/t/t3502-cherry-pick-merge.sh +++ b/t/t3502-cherry-pick-merge.sh @@ -11,6 +11,7 @@ test_description='cherry picking and reverting a merge GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success setup ' diff --git a/t/t3503-cherry-pick-root.sh b/t/t3503-cherry-pick-root.sh index 95fe4feaee..76d393dc8a 100755 --- a/t/t3503-cherry-pick-root.sh +++ b/t/t3503-cherry-pick-root.sh @@ -5,6 +5,7 @@ test_description='test cherry-picking (and reverting) a root commit' GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success setup ' diff --git a/t/t3506-cherry-pick-ff.sh b/t/t3506-cherry-pick-ff.sh index 7e11bd4a4c..b71bad17b8 100755 --- a/t/t3506-cherry-pick-ff.sh +++ b/t/t3506-cherry-pick-ff.sh @@ -5,6 +5,7 @@ test_description='test cherry-picking with --ff option' GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success setup ' diff --git a/t/t3511-cherry-pick-x.sh b/t/t3511-cherry-pick-x.sh index 84a587daf3..dd5d92ef30 100755 --- a/t/t3511-cherry-pick-x.sh +++ b/t/t3511-cherry-pick-x.sh @@ -2,6 +2,7 @@ test_description='Test cherry-pick -x and -s' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh pristine_detach () { diff --git a/t/t3701-add-interactive.sh b/t/t3701-add-interactive.sh index 5841f280fb..3a99837d9b 100755 --- a/t/t3701-add-interactive.sh +++ b/t/t3701-add-interactive.sh @@ -7,7 +7,7 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME . ./test-lib.sh . "$TEST_DIRECTORY"/lib-terminal.sh -if test_have_prereq !ADD_I_USE_BUILTIN,!PERL +if test_have_prereq !PERL then skip_all='skipping add -i (scripted) tests, perl not available' test_done @@ -46,6 +46,21 @@ force_color () { ) } +test_expect_success 'warn about add.interactive.useBuiltin' ' + cat >expect <<-\EOF && + warning: the add.interactive.useBuiltin setting has been removed! + See its entry in '\''git help config'\'' for details. + No changes. + EOF + + for v in = =true =false + do + git -c "add.interactive.useBuiltin$v" add -p >out 2>actual && + test_must_be_empty out && + test_cmp expect actual || return 1 + done +' + test_expect_success 'setup (initial)' ' echo content >file && git add file && @@ -547,15 +562,7 @@ test_expect_success 'split hunk "add -p (edit)"' ' ! grep "^+15" actual ' -test_expect_success 'setup ADD_I_USE_BUILTIN check' ' - result=success && - if ! test_have_prereq ADD_I_USE_BUILTIN - then - result=failure - fi -' - -test_expect_$result 'split hunk "add -p (no, yes, edit)"' ' +test_expect_success 'split hunk "add -p (no, yes, edit)"' ' test_write_lines 5 10 20 21 30 31 40 50 60 >test && git reset && # test sequence is s(plit), n(o), y(es), e(dit) @@ -579,7 +586,7 @@ test_expect_success 'split hunk with incomplete line at end' ' test_must_fail git grep --cached before ' -test_expect_$result 'edit, adding lines to the first hunk' ' +test_expect_success 'edit, adding lines to the first hunk' ' test_write_lines 10 11 20 30 40 50 51 60 >test && git reset && tr _ " " >patch <<-EOF && diff --git a/t/t4018/java-class-brace-on-separate-line b/t/t4018/java-class-brace-on-separate-line new file mode 100644 index 0000000000..8795acd4cf --- /dev/null +++ b/t/t4018/java-class-brace-on-separate-line @@ -0,0 +1,6 @@ +class RIGHT +{ + static int ONE; + static int TWO; + static int ChangeMe; +} diff --git a/t/t4018/java-class-space-before-type-parameters b/t/t4018/java-class-space-before-type-parameters new file mode 100644 index 0000000000..0bdef1dfbe --- /dev/null +++ b/t/t4018/java-class-space-before-type-parameters @@ -0,0 +1,6 @@ +class RIGHT <TYPE, PARAMS, AFTER, SPACE> { + static int ONE; + static int TWO; + static int THREE; + private A ChangeMe; +} diff --git a/t/t4018/java-class-type-parameters b/t/t4018/java-class-type-parameters new file mode 100644 index 0000000000..579aa7af21 --- /dev/null +++ b/t/t4018/java-class-type-parameters @@ -0,0 +1,6 @@ +class RIGHT<A, B> { + static int ONE; + static int TWO; + static int THREE; + private A ChangeMe; +} diff --git a/t/t4018/java-class-type-parameters-implements b/t/t4018/java-class-type-parameters-implements new file mode 100644 index 0000000000..b8038b1866 --- /dev/null +++ b/t/t4018/java-class-type-parameters-implements @@ -0,0 +1,6 @@ +class RIGHT<A, B> implements List<A> { + static int ONE; + static int TWO; + static int THREE; + private A ChangeMe; +} diff --git a/t/t4018/java-interface-type-parameters b/t/t4018/java-interface-type-parameters new file mode 100644 index 0000000000..a4baa1ae68 --- /dev/null +++ b/t/t4018/java-interface-type-parameters @@ -0,0 +1,6 @@ +interface RIGHT<A, B> { + static int ONE; + static int TWO; + static int THREE; + public B foo(A ChangeMe); +} diff --git a/t/t4018/java-interface-type-parameters-extends b/t/t4018/java-interface-type-parameters-extends new file mode 100644 index 0000000000..31d7fb3244 --- /dev/null +++ b/t/t4018/java-interface-type-parameters-extends @@ -0,0 +1,6 @@ +interface RIGHT<A, B> extends Function<A, B> { + static int ONE; + static int TWO; + static int THREE; + public B foo(A ChangeMe); +} diff --git a/t/t4018/java-non-sealed b/t/t4018/java-non-sealed new file mode 100644 index 0000000000..069087c1c6 --- /dev/null +++ b/t/t4018/java-non-sealed @@ -0,0 +1,8 @@ +public abstract sealed class SealedClass { + public static non-sealed class RIGHT extends SealedClass { + static int ONE; + static int TWO; + static int THREE; + private int ChangeMe; + } +} diff --git a/t/t4018/java-record b/t/t4018/java-record new file mode 100644 index 0000000000..97aa819dd8 --- /dev/null +++ b/t/t4018/java-record @@ -0,0 +1,6 @@ +public record RIGHT(int comp1, double comp2, String comp3) { + static int ONE; + static int TWO; + static int THREE; + static int ChangeMe; +} diff --git a/t/t4018/java-record-space-before-components b/t/t4018/java-record-space-before-components new file mode 100644 index 0000000000..9827f22583 --- /dev/null +++ b/t/t4018/java-record-space-before-components @@ -0,0 +1,6 @@ +public record RIGHT (String components, String after, String space) { + static int ONE; + static int TWO; + static int THREE; + static int ChangeMe; +} diff --git a/t/t4018/java-record-type-parameters b/t/t4018/java-record-type-parameters new file mode 100644 index 0000000000..f62a035cc8 --- /dev/null +++ b/t/t4018/java-record-type-parameters @@ -0,0 +1,6 @@ +public record RIGHT<A, N extends Number>(A comp1, N comp2, int comp3) { + static int ONE; + static int TWO; + static int THREE; + static int ChangeMe; +} diff --git a/t/t4018/java-sealed b/t/t4018/java-sealed new file mode 100644 index 0000000000..785fbc62bc --- /dev/null +++ b/t/t4018/java-sealed @@ -0,0 +1,7 @@ +public abstract sealed class Sealed { // RIGHT + static int ONE; + static int TWO; + static int THREE; + public final class ChangeMe extends Sealed { + } +} diff --git a/t/t4018/java-sealed-permits b/t/t4018/java-sealed-permits new file mode 100644 index 0000000000..18dd4894cf --- /dev/null +++ b/t/t4018/java-sealed-permits @@ -0,0 +1,6 @@ +public abstract sealed class RIGHT permits PermittedA, PermittedB { + static int ONE; + static int TWO; + static int THREE; + private int ChangeMe; +} diff --git a/t/t4018/java-sealed-type-parameters b/t/t4018/java-sealed-type-parameters new file mode 100644 index 0000000000..e6530c47c3 --- /dev/null +++ b/t/t4018/java-sealed-type-parameters @@ -0,0 +1,6 @@ +public abstract sealed class RIGHT<A, B> { + static int ONE; + static int TWO; + static int THREE; + private int ChangeMe; +} diff --git a/t/t4018/java-sealed-type-parameters-implements-permits b/t/t4018/java-sealed-type-parameters-implements-permits new file mode 100644 index 0000000000..bd6e6d3582 --- /dev/null +++ b/t/t4018/java-sealed-type-parameters-implements-permits @@ -0,0 +1,6 @@ +public abstract sealed class RIGHT<A, B> implements List<A> permits PermittedA, PermittedB { + static int ONE; + static int TWO; + static int THREE; + private int ChangeMe; +} diff --git a/t/t4018/java-sealed-type-parameters-permits b/t/t4018/java-sealed-type-parameters-permits new file mode 100644 index 0000000000..25a0da6442 --- /dev/null +++ b/t/t4018/java-sealed-type-parameters-permits @@ -0,0 +1,6 @@ +public abstract sealed class RIGHT<A, B> permits PermittedA, PermittedB { + static int ONE; + static int TWO; + static int THREE; + private int ChangeMe; +} diff --git a/t/t4115-apply-symlink.sh b/t/t4115-apply-symlink.sh index d0f3edef54..65ac7df2d7 100755 --- a/t/t4115-apply-symlink.sh +++ b/t/t4115-apply-symlink.sh @@ -45,4 +45,85 @@ test_expect_success 'apply --index symlink patch' ' ' +test_expect_success 'symlink setup' ' + ln -s .git symlink && + git add symlink && + git commit -m "add symlink" +' + +test_expect_success SYMLINKS 'symlink escape when creating new files' ' + test_when_finished "git reset --hard && git clean -dfx" && + + cat >patch <<-EOF && + diff --git a/symlink b/renamed-symlink + similarity index 100% + rename from symlink + rename to renamed-symlink + -- + diff --git /dev/null b/renamed-symlink/create-me + new file mode 100644 + index 0000000..039727e + --- /dev/null + +++ b/renamed-symlink/create-me + @@ -0,0 +1,1 @@ + +busted + EOF + + test_must_fail git apply patch 2>stderr && + cat >expected_stderr <<-EOF && + error: affected file ${SQ}renamed-symlink/create-me${SQ} is beyond a symbolic link + EOF + test_cmp expected_stderr stderr && + ! test_path_exists .git/create-me +' + +test_expect_success SYMLINKS 'symlink escape when modifying file' ' + test_when_finished "git reset --hard && git clean -dfx" && + touch .git/modify-me && + + cat >patch <<-EOF && + diff --git a/symlink b/renamed-symlink + similarity index 100% + rename from symlink + rename to renamed-symlink + -- + diff --git a/renamed-symlink/modify-me b/renamed-symlink/modify-me + index 1111111..2222222 100644 + --- a/renamed-symlink/modify-me + +++ b/renamed-symlink/modify-me + @@ -0,0 +1,1 @@ + +busted + EOF + + test_must_fail git apply patch 2>stderr && + cat >expected_stderr <<-EOF && + error: renamed-symlink/modify-me: No such file or directory + EOF + test_cmp expected_stderr stderr && + test_must_be_empty .git/modify-me +' + +test_expect_success SYMLINKS 'symlink escape when deleting file' ' + test_when_finished "git reset --hard && git clean -dfx && rm .git/delete-me" && + touch .git/delete-me && + + cat >patch <<-EOF && + diff --git a/symlink b/renamed-symlink + similarity index 100% + rename from symlink + rename to renamed-symlink + -- + diff --git a/renamed-symlink/delete-me b/renamed-symlink/delete-me + deleted file mode 100644 + index 1111111..0000000 100644 + EOF + + test_must_fail git apply patch 2>stderr && + cat >expected_stderr <<-EOF && + error: renamed-symlink/delete-me: No such file or directory + EOF + test_cmp expected_stderr stderr && + test_path_is_file .git/delete-me +' + test_done diff --git a/t/t5000-tar-tree.sh b/t/t5000-tar-tree.sh index d473048138..eb3214bc17 100755 --- a/t/t5000-tar-tree.sh +++ b/t/t5000-tar-tree.sh @@ -402,11 +402,11 @@ test_expect_success GZIP 'extract tgz file (external gzip)' ' test_expect_success 'archive and :(glob)' ' git archive -v HEAD -- ":(glob)**/sh" >/dev/null 2>actual && - cat >expect <<EOF && -a/ -a/bin/ -a/bin/sh -EOF + cat >expect <<-\EOF && + a/ + a/bin/ + a/bin/sh + EOF test_cmp expect actual ' diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh index c0b745e33b..34a1261520 100755 --- a/t/t5510-fetch.sh +++ b/t/t5510-fetch.sh @@ -806,6 +806,14 @@ test_expect_success 'fetch.writeCommitGraph with submodules' ' ) ' +# fetches from first configured url +test_expect_success 'fetch from multiple configured URLs in single remote' ' + git init url1 && + git remote add multipleurls url1 && + git remote set-url --add multipleurls url2 && + git fetch multipleurls +' + # configured prune tests set_config_tristate () { diff --git a/t/t5558-clone-bundle-uri.sh b/t/t5558-clone-bundle-uri.sh index 9155f31fa2..afd56926c5 100755 --- a/t/t5558-clone-bundle-uri.sh +++ b/t/t5558-clone-bundle-uri.sh @@ -285,6 +285,8 @@ test_expect_success 'clone HTTP bundle' ' ' test_expect_success 'clone bundle list (HTTP, no heuristic)' ' + test_when_finished rm -f trace*.txt && + cp clone-from/bundle-*.bundle "$HTTPD_DOCUMENT_ROOT_PATH/" && cat >"$HTTPD_DOCUMENT_ROOT_PATH/bundle-list" <<-EOF && [bundle] @@ -304,12 +306,26 @@ test_expect_success 'clone bundle list (HTTP, no heuristic)' ' uri = $HTTPD_URL/bundle-4.bundle EOF - git clone --bundle-uri="$HTTPD_URL/bundle-list" \ + GIT_TRACE2_EVENT="$(pwd)/trace-clone.txt" \ + git clone --bundle-uri="$HTTPD_URL/bundle-list" \ clone-from clone-list-http 2>err && ! grep "Repository lacks these prerequisite commits" err && git -C clone-from for-each-ref --format="%(objectname)" >oids && - git -C clone-list-http cat-file --batch-check <oids + git -C clone-list-http cat-file --batch-check <oids && + + cat >expect <<-EOF && + $HTTPD_URL/bundle-1.bundle + $HTTPD_URL/bundle-2.bundle + $HTTPD_URL/bundle-3.bundle + $HTTPD_URL/bundle-4.bundle + $HTTPD_URL/bundle-list + EOF + + # Sort the list, since the order is not well-defined + # without a heuristic. + test_remote_https_urls <trace-clone.txt | sort >actual && + test_cmp expect actual ' test_expect_success 'clone bundle list (HTTP, any mode)' ' @@ -350,6 +366,658 @@ test_expect_success 'clone bundle list (HTTP, any mode)' ' test_cmp expect actual ' +test_expect_success 'clone bundle list (http, creationToken)' ' + test_when_finished rm -f trace*.txt && + + cp clone-from/bundle-*.bundle "$HTTPD_DOCUMENT_ROOT_PATH/" && + cat >"$HTTPD_DOCUMENT_ROOT_PATH/bundle-list" <<-EOF && + [bundle] + version = 1 + mode = all + heuristic = creationToken + + [bundle "bundle-1"] + uri = bundle-1.bundle + creationToken = 1 + + [bundle "bundle-2"] + uri = bundle-2.bundle + creationToken = 2 + + [bundle "bundle-3"] + uri = bundle-3.bundle + creationToken = 3 + + [bundle "bundle-4"] + uri = bundle-4.bundle + creationToken = 4 + EOF + + GIT_TRACE2_EVENT="$(pwd)/trace-clone.txt" git \ + clone --bundle-uri="$HTTPD_URL/bundle-list" \ + "$HTTPD_URL/smart/fetch.git" clone-list-http-2 && + + git -C clone-from for-each-ref --format="%(objectname)" >oids && + git -C clone-list-http-2 cat-file --batch-check <oids && + + cat >expect <<-EOF && + $HTTPD_URL/bundle-list + $HTTPD_URL/bundle-4.bundle + $HTTPD_URL/bundle-3.bundle + $HTTPD_URL/bundle-2.bundle + $HTTPD_URL/bundle-1.bundle + EOF + + test_remote_https_urls <trace-clone.txt >actual && + test_cmp expect actual +' + +test_expect_success 'clone incomplete bundle list (http, creationToken)' ' + test_when_finished rm -f trace*.txt && + + cp clone-from/bundle-*.bundle "$HTTPD_DOCUMENT_ROOT_PATH/" && + cat >"$HTTPD_DOCUMENT_ROOT_PATH/bundle-list" <<-EOF && + [bundle] + version = 1 + mode = all + heuristic = creationToken + + [bundle "bundle-1"] + uri = bundle-1.bundle + creationToken = 1 + EOF + + GIT_TRACE2_EVENT=$(pwd)/trace-clone.txt \ + git clone --bundle-uri="$HTTPD_URL/bundle-list" \ + --single-branch --branch=base --no-tags \ + "$HTTPD_URL/smart/fetch.git" clone-token-http && + + test_cmp_config -C clone-token-http "$HTTPD_URL/bundle-list" fetch.bundleuri && + test_cmp_config -C clone-token-http 1 fetch.bundlecreationtoken && + + cat >expect <<-EOF && + $HTTPD_URL/bundle-list + $HTTPD_URL/bundle-1.bundle + EOF + + test_remote_https_urls <trace-clone.txt >actual && + test_cmp expect actual && + + # We now have only one bundle ref. + git -C clone-token-http for-each-ref --format="%(refname)" "refs/bundles/*" >refs && + cat >expect <<-\EOF && + refs/bundles/base + EOF + test_cmp expect refs && + + # Add remaining bundles, exercising the "deepening" strategy + # for downloading via the creationToken heurisitc. + cat >>"$HTTPD_DOCUMENT_ROOT_PATH/bundle-list" <<-EOF && + [bundle "bundle-2"] + uri = bundle-2.bundle + creationToken = 2 + + [bundle "bundle-3"] + uri = bundle-3.bundle + creationToken = 3 + + [bundle "bundle-4"] + uri = bundle-4.bundle + creationToken = 4 + EOF + + GIT_TRACE2_EVENT="$(pwd)/trace1.txt" \ + git -C clone-token-http fetch origin --no-tags \ + refs/heads/merge:refs/heads/merge && + test_cmp_config -C clone-token-http 4 fetch.bundlecreationtoken && + + cat >expect <<-EOF && + $HTTPD_URL/bundle-list + $HTTPD_URL/bundle-4.bundle + $HTTPD_URL/bundle-3.bundle + $HTTPD_URL/bundle-2.bundle + EOF + + test_remote_https_urls <trace1.txt >actual && + test_cmp expect actual && + + # We now have all bundle refs. + git -C clone-token-http for-each-ref --format="%(refname)" "refs/bundles/*" >refs && + + cat >expect <<-\EOF && + refs/bundles/base + refs/bundles/left + refs/bundles/merge + refs/bundles/right + EOF + test_cmp expect refs +' + +test_expect_success 'http clone with bundle.heuristic creates fetch.bundleURI' ' + test_when_finished rm -rf fetch-http-4 trace*.txt && + + cat >"$HTTPD_DOCUMENT_ROOT_PATH/bundle-list" <<-EOF && + [bundle] + version = 1 + mode = all + heuristic = creationToken + + [bundle "bundle-1"] + uri = bundle-1.bundle + creationToken = 1 + EOF + + GIT_TRACE2_EVENT="$(pwd)/trace-clone.txt" \ + git clone --single-branch --branch=base \ + --bundle-uri="$HTTPD_URL/bundle-list" \ + "$HTTPD_URL/smart/fetch.git" fetch-http-4 && + + test_cmp_config -C fetch-http-4 "$HTTPD_URL/bundle-list" fetch.bundleuri && + test_cmp_config -C fetch-http-4 1 fetch.bundlecreationtoken && + + cat >expect <<-EOF && + $HTTPD_URL/bundle-list + $HTTPD_URL/bundle-1.bundle + EOF + + test_remote_https_urls <trace-clone.txt >actual && + test_cmp expect actual && + + # only received base ref from bundle-1 + git -C fetch-http-4 for-each-ref --format="%(refname)" "refs/bundles/*" >refs && + cat >expect <<-\EOF && + refs/bundles/base + EOF + test_cmp expect refs && + + cat >>"$HTTPD_DOCUMENT_ROOT_PATH/bundle-list" <<-EOF && + [bundle "bundle-2"] + uri = bundle-2.bundle + creationToken = 2 + EOF + + # Fetch the objects for bundle-2 _and_ bundle-3. + GIT_TRACE2_EVENT="$(pwd)/trace1.txt" \ + git -C fetch-http-4 fetch origin --no-tags \ + refs/heads/left:refs/heads/left \ + refs/heads/right:refs/heads/right && + test_cmp_config -C fetch-http-4 2 fetch.bundlecreationtoken && + + cat >expect <<-EOF && + $HTTPD_URL/bundle-list + $HTTPD_URL/bundle-2.bundle + EOF + + test_remote_https_urls <trace1.txt >actual && + test_cmp expect actual && + + # received left from bundle-2 + git -C fetch-http-4 for-each-ref --format="%(refname)" "refs/bundles/*" >refs && + cat >expect <<-\EOF && + refs/bundles/base + refs/bundles/left + EOF + test_cmp expect refs && + + # No-op fetch + GIT_TRACE2_EVENT="$(pwd)/trace1b.txt" \ + git -C fetch-http-4 fetch origin --no-tags \ + refs/heads/left:refs/heads/left \ + refs/heads/right:refs/heads/right && + + cat >expect <<-EOF && + $HTTPD_URL/bundle-list + EOF + test_remote_https_urls <trace1b.txt >actual && + test_cmp expect actual && + + cat >>"$HTTPD_DOCUMENT_ROOT_PATH/bundle-list" <<-EOF && + [bundle "bundle-3"] + uri = bundle-3.bundle + creationToken = 3 + + [bundle "bundle-4"] + uri = bundle-4.bundle + creationToken = 4 + EOF + + # This fetch should skip bundle-3.bundle, since its objects are + # already local (we have the requisite commits for bundle-4.bundle). + GIT_TRACE2_EVENT="$(pwd)/trace2.txt" \ + git -C fetch-http-4 fetch origin --no-tags \ + refs/heads/merge:refs/heads/merge && + test_cmp_config -C fetch-http-4 4 fetch.bundlecreationtoken && + + cat >expect <<-EOF && + $HTTPD_URL/bundle-list + $HTTPD_URL/bundle-4.bundle + EOF + + test_remote_https_urls <trace2.txt >actual && + test_cmp expect actual && + + # received merge ref from bundle-4, but right is missing + # because we did not download bundle-3. + git -C fetch-http-4 for-each-ref --format="%(refname)" "refs/bundles/*" >refs && + + cat >expect <<-\EOF && + refs/bundles/base + refs/bundles/left + refs/bundles/merge + EOF + test_cmp expect refs && + + # No-op fetch + GIT_TRACE2_EVENT="$(pwd)/trace2b.txt" \ + git -C fetch-http-4 fetch origin && + + cat >expect <<-EOF && + $HTTPD_URL/bundle-list + EOF + test_remote_https_urls <trace2b.txt >actual && + test_cmp expect actual +' + +test_expect_success 'creationToken heuristic with failed downloads (clone)' ' + test_when_finished rm -rf download-* trace*.txt && + + # Case 1: base bundle does not exist, nothing can unbundle + cat >"$HTTPD_DOCUMENT_ROOT_PATH/bundle-list" <<-EOF && + [bundle] + version = 1 + mode = all + heuristic = creationToken + + [bundle "bundle-1"] + uri = fake.bundle + creationToken = 1 + + [bundle "bundle-2"] + uri = bundle-2.bundle + creationToken = 2 + + [bundle "bundle-3"] + uri = bundle-3.bundle + creationToken = 3 + + [bundle "bundle-4"] + uri = bundle-4.bundle + creationToken = 4 + EOF + + GIT_TRACE2_EVENT="$(pwd)/trace-clone-1.txt" \ + git clone --single-branch --branch=base \ + --bundle-uri="$HTTPD_URL/bundle-list" \ + "$HTTPD_URL/smart/fetch.git" download-1 && + + # Bundle failure does not set these configs. + test_must_fail git -C download-1 config fetch.bundleuri && + test_must_fail git -C download-1 config fetch.bundlecreationtoken && + + cat >expect <<-EOF && + $HTTPD_URL/bundle-list + $HTTPD_URL/bundle-4.bundle + $HTTPD_URL/bundle-3.bundle + $HTTPD_URL/bundle-2.bundle + $HTTPD_URL/fake.bundle + EOF + test_remote_https_urls <trace-clone-1.txt >actual && + test_cmp expect actual && + + # All bundles failed to unbundle + git -C download-1 for-each-ref --format="%(refname)" "refs/bundles/*" >refs && + test_must_be_empty refs && + + # Case 2: middle bundle does not exist, only two bundles can unbundle + cat >"$HTTPD_DOCUMENT_ROOT_PATH/bundle-list" <<-EOF && + [bundle] + version = 1 + mode = all + heuristic = creationToken + + [bundle "bundle-1"] + uri = bundle-1.bundle + creationToken = 1 + + [bundle "bundle-2"] + uri = fake.bundle + creationToken = 2 + + [bundle "bundle-3"] + uri = bundle-3.bundle + creationToken = 3 + + [bundle "bundle-4"] + uri = bundle-4.bundle + creationToken = 4 + EOF + + GIT_TRACE2_EVENT="$(pwd)/trace-clone-2.txt" \ + git clone --single-branch --branch=base \ + --bundle-uri="$HTTPD_URL/bundle-list" \ + "$HTTPD_URL/smart/fetch.git" download-2 && + + # Bundle failure does not set these configs. + test_must_fail git -C download-2 config fetch.bundleuri && + test_must_fail git -C download-2 config fetch.bundlecreationtoken && + + cat >expect <<-EOF && + $HTTPD_URL/bundle-list + $HTTPD_URL/bundle-4.bundle + $HTTPD_URL/bundle-3.bundle + $HTTPD_URL/fake.bundle + $HTTPD_URL/bundle-1.bundle + EOF + test_remote_https_urls <trace-clone-2.txt >actual && + test_cmp expect actual && + + # bundle-1 and bundle-3 could unbundle, but bundle-4 could not + git -C download-2 for-each-ref --format="%(refname)" "refs/bundles/*" >refs && + cat >expect <<-EOF && + refs/bundles/base + refs/bundles/right + EOF + test_cmp expect refs && + + # Case 3: top bundle does not exist, rest unbundle fine. + cat >"$HTTPD_DOCUMENT_ROOT_PATH/bundle-list" <<-EOF && + [bundle] + version = 1 + mode = all + heuristic = creationToken + + [bundle "bundle-1"] + uri = bundle-1.bundle + creationToken = 1 + + [bundle "bundle-2"] + uri = bundle-2.bundle + creationToken = 2 + + [bundle "bundle-3"] + uri = bundle-3.bundle + creationToken = 3 + + [bundle "bundle-4"] + uri = fake.bundle + creationToken = 4 + EOF + + GIT_TRACE2_EVENT="$(pwd)/trace-clone-3.txt" \ + git clone --single-branch --branch=base \ + --bundle-uri="$HTTPD_URL/bundle-list" \ + "$HTTPD_URL/smart/fetch.git" download-3 && + + # As long as we have continguous successful downloads, + # we _do_ set these configs. + test_cmp_config -C download-3 "$HTTPD_URL/bundle-list" fetch.bundleuri && + test_cmp_config -C download-3 3 fetch.bundlecreationtoken && + + cat >expect <<-EOF && + $HTTPD_URL/bundle-list + $HTTPD_URL/fake.bundle + $HTTPD_URL/bundle-3.bundle + $HTTPD_URL/bundle-2.bundle + $HTTPD_URL/bundle-1.bundle + EOF + test_remote_https_urls <trace-clone-3.txt >actual && + test_cmp expect actual && + + # fake.bundle did not unbundle, but the others did. + git -C download-3 for-each-ref --format="%(refname)" "refs/bundles/*" >refs && + cat >expect <<-EOF && + refs/bundles/base + refs/bundles/left + refs/bundles/right + EOF + test_cmp expect refs +' + +# Expand the bundle list to include other interesting shapes, specifically +# interesting for use when fetching from a previous state. +# +# ---------------- bundle-7 +# 7 +# _/|\_ +# ---/--|--\------ bundle-6 +# 5 | 6 +# --|---|---|----- bundle-4 +# | 4 | +# | / \ / +# --|-|---|/------ bundle-3 (the client will be caught up to this point.) +# \ | 3 +# ---\|---|------- bundle-2 +# 2 | +# ----|---|------- bundle-1 +# \ / +# 1 +# | +# (previous commits) +test_expect_success 'expand incremental bundle list' ' + ( + cd clone-from && + git checkout -b lefter left && + test_commit 5 && + git checkout -b righter right && + test_commit 6 && + git checkout -b top lefter && + git merge -m "7" merge righter && + + git bundle create bundle-6.bundle lefter righter --not left right && + git bundle create bundle-7.bundle top --not lefter merge righter && + + cp bundle-*.bundle "$HTTPD_DOCUMENT_ROOT_PATH/" + ) && + git -C "$HTTPD_DOCUMENT_ROOT_PATH/fetch.git" fetch origin +refs/heads/*:refs/heads/* +' + +test_expect_success 'creationToken heuristic with failed downloads (fetch)' ' + test_when_finished rm -rf download-* trace*.txt && + + cat >"$HTTPD_DOCUMENT_ROOT_PATH/bundle-list" <<-EOF && + [bundle] + version = 1 + mode = all + heuristic = creationToken + + [bundle "bundle-1"] + uri = bundle-1.bundle + creationToken = 1 + + [bundle "bundle-2"] + uri = bundle-2.bundle + creationToken = 2 + + [bundle "bundle-3"] + uri = bundle-3.bundle + creationToken = 3 + EOF + + git clone --single-branch --branch=left \ + --bundle-uri="$HTTPD_URL/bundle-list" \ + "$HTTPD_URL/smart/fetch.git" fetch-base && + test_cmp_config -C fetch-base "$HTTPD_URL/bundle-list" fetch.bundleURI && + test_cmp_config -C fetch-base 3 fetch.bundleCreationToken && + + # Case 1: all bundles exist: successful unbundling of all bundles + cat >"$HTTPD_DOCUMENT_ROOT_PATH/bundle-list" <<-EOF && + [bundle] + version = 1 + mode = all + heuristic = creationToken + + [bundle "bundle-1"] + uri = bundle-1.bundle + creationToken = 1 + + [bundle "bundle-2"] + uri = bundle-2.bundle + creationToken = 2 + + [bundle "bundle-3"] + uri = bundle-3.bundle + creationToken = 3 + + [bundle "bundle-4"] + uri = bundle-4.bundle + creationToken = 4 + + [bundle "bundle-6"] + uri = bundle-6.bundle + creationToken = 6 + + [bundle "bundle-7"] + uri = bundle-7.bundle + creationToken = 7 + EOF + + cp -r fetch-base fetch-1 && + GIT_TRACE2_EVENT="$(pwd)/trace-fetch-1.txt" \ + git -C fetch-1 fetch origin && + test_cmp_config -C fetch-1 7 fetch.bundlecreationtoken && + + cat >expect <<-EOF && + $HTTPD_URL/bundle-list + $HTTPD_URL/bundle-7.bundle + $HTTPD_URL/bundle-6.bundle + $HTTPD_URL/bundle-4.bundle + EOF + test_remote_https_urls <trace-fetch-1.txt >actual && + test_cmp expect actual && + + # Check which bundles have unbundled by refs + git -C fetch-1 for-each-ref --format="%(refname)" "refs/bundles/*" >refs && + cat >expect <<-EOF && + refs/bundles/base + refs/bundles/left + refs/bundles/lefter + refs/bundles/merge + refs/bundles/right + refs/bundles/righter + refs/bundles/top + EOF + test_cmp expect refs && + + # Case 2: middle bundle does not exist, only bundle-4 can unbundle + cat >"$HTTPD_DOCUMENT_ROOT_PATH/bundle-list" <<-EOF && + [bundle] + version = 1 + mode = all + heuristic = creationToken + + [bundle "bundle-1"] + uri = bundle-1.bundle + creationToken = 1 + + [bundle "bundle-2"] + uri = bundle-2.bundle + creationToken = 2 + + [bundle "bundle-3"] + uri = bundle-3.bundle + creationToken = 3 + + [bundle "bundle-4"] + uri = bundle-4.bundle + creationToken = 4 + + [bundle "bundle-6"] + uri = fake.bundle + creationToken = 6 + + [bundle "bundle-7"] + uri = bundle-7.bundle + creationToken = 7 + EOF + + cp -r fetch-base fetch-2 && + GIT_TRACE2_EVENT="$(pwd)/trace-fetch-2.txt" \ + git -C fetch-2 fetch origin && + + # Since bundle-7 fails to unbundle, do not update creation token. + test_cmp_config -C fetch-2 3 fetch.bundlecreationtoken && + + cat >expect <<-EOF && + $HTTPD_URL/bundle-list + $HTTPD_URL/bundle-7.bundle + $HTTPD_URL/fake.bundle + $HTTPD_URL/bundle-4.bundle + EOF + test_remote_https_urls <trace-fetch-2.txt >actual && + test_cmp expect actual && + + # Check which bundles have unbundled by refs + git -C fetch-2 for-each-ref --format="%(refname)" "refs/bundles/*" >refs && + cat >expect <<-EOF && + refs/bundles/base + refs/bundles/left + refs/bundles/merge + refs/bundles/right + EOF + test_cmp expect refs && + + # Case 3: top bundle does not exist, rest unbundle fine. + cat >"$HTTPD_DOCUMENT_ROOT_PATH/bundle-list" <<-EOF && + [bundle] + version = 1 + mode = all + heuristic = creationToken + + [bundle "bundle-1"] + uri = bundle-1.bundle + creationToken = 1 + + [bundle "bundle-2"] + uri = bundle-2.bundle + creationToken = 2 + + [bundle "bundle-3"] + uri = bundle-3.bundle + creationToken = 3 + + [bundle "bundle-4"] + uri = bundle-4.bundle + creationToken = 4 + + [bundle "bundle-6"] + uri = bundle-6.bundle + creationToken = 6 + + [bundle "bundle-7"] + uri = fake.bundle + creationToken = 7 + EOF + + cp -r fetch-base fetch-3 && + GIT_TRACE2_EVENT="$(pwd)/trace-fetch-3.txt" \ + git -C fetch-3 fetch origin && + + # As long as we have continguous successful downloads, + # we _do_ set the maximum creation token. + test_cmp_config -C fetch-3 6 fetch.bundlecreationtoken && + + # NOTE: the fetch skips bundle-4 since bundle-6 successfully + # unbundles itself and bundle-7 failed to download. + cat >expect <<-EOF && + $HTTPD_URL/bundle-list + $HTTPD_URL/fake.bundle + $HTTPD_URL/bundle-6.bundle + EOF + test_remote_https_urls <trace-fetch-3.txt >actual && + test_cmp expect actual && + + # Check which bundles have unbundled by refs + git -C fetch-3 for-each-ref --format="%(refname)" "refs/bundles/*" >refs && + cat >expect <<-EOF && + refs/bundles/base + refs/bundles/left + refs/bundles/lefter + refs/bundles/right + refs/bundles/righter + EOF + test_cmp expect refs +' + # Do not add tests here unless they use the HTTP server, as they will # not run unless the HTTP dependencies exist. diff --git a/t/t5601-clone.sh b/t/t5601-clone.sh index 1928ea1dd7..b7d5551262 100755 --- a/t/t5601-clone.sh +++ b/t/t5601-clone.sh @@ -831,6 +831,52 @@ test_expect_success 'auto-discover multiple bundles from HTTP clone' ' grep -f pattern trace.txt ' +test_expect_success 'auto-discover multiple bundles from HTTP clone: creationToken heuristic' ' + test_when_finished rm -rf "$HTTPD_DOCUMENT_ROOT_PATH/repo4.git" && + test_when_finished rm -rf clone-heuristic trace*.txt && + + test_commit -C src newest && + git -C src bundle create "$HTTPD_DOCUMENT_ROOT_PATH/newest.bundle" HEAD~1..HEAD && + git clone --bare --no-local src "$HTTPD_DOCUMENT_ROOT_PATH/repo4.git" && + + cat >>"$HTTPD_DOCUMENT_ROOT_PATH/repo4.git/config" <<-EOF && + [uploadPack] + advertiseBundleURIs = true + + [bundle] + version = 1 + mode = all + heuristic = creationToken + + [bundle "everything"] + uri = $HTTPD_URL/everything.bundle + creationtoken = 1 + + [bundle "new"] + uri = $HTTPD_URL/new.bundle + creationtoken = 2 + + [bundle "newest"] + uri = $HTTPD_URL/newest.bundle + creationtoken = 3 + EOF + + GIT_TRACE2_EVENT="$(pwd)/trace-clone.txt" \ + git -c protocol.version=2 \ + -c transfer.bundleURI=true clone \ + "$HTTPD_URL/smart/repo4.git" clone-heuristic && + + cat >expect <<-EOF && + $HTTPD_URL/newest.bundle + $HTTPD_URL/new.bundle + $HTTPD_URL/everything.bundle + EOF + + # We should fetch all bundles in the expected order. + test_remote_https_urls <trace-clone.txt >actual && + test_cmp expect actual +' + # DO NOT add non-httpd-specific tests here, because the last part of this # test script is only executed when httpd is available and enabled. diff --git a/t/t5604-clone-reference.sh b/t/t5604-clone-reference.sh index 2734e37e88..7ccebb40c3 100755 --- a/t/t5604-clone-reference.sh +++ b/t/t5604-clone-reference.sh @@ -344,4 +344,20 @@ test_expect_success SYMLINKS 'clone repo with symlinked or unknown files at obje test_must_be_empty T--shared.objects-symlinks.raw ' +test_expect_success SYMLINKS 'clone repo with symlinked objects directory' ' + test_when_finished "rm -fr sensitive malicious" && + + mkdir -p sensitive && + echo "secret" >sensitive/file && + + git init malicious && + rm -fr malicious/.git/objects && + ln -s "$(pwd)/sensitive" ./malicious/.git/objects && + + test_must_fail git clone --local malicious clone 2>err && + + test_path_is_missing clone && + grep "failed to start iterator over" err +' + test_done diff --git a/t/t5619-clone-local-ambiguous-transport.sh b/t/t5619-clone-local-ambiguous-transport.sh new file mode 100755 index 0000000000..cce62bf78d --- /dev/null +++ b/t/t5619-clone-local-ambiguous-transport.sh @@ -0,0 +1,70 @@ +#!/bin/sh + +test_description='test local clone with ambiguous transport' + +. ./test-lib.sh +. "$TEST_DIRECTORY/lib-httpd.sh" + +if ! test_have_prereq SYMLINKS +then + skip_all='skipping test, symlink support unavailable' + test_done +fi + +start_httpd + +REPO="$HTTPD_DOCUMENT_ROOT_PATH/sub.git" +URI="$HTTPD_URL/dumb/sub.git" + +test_expect_success 'setup' ' + mkdir -p sensitive && + echo "secret" >sensitive/secret && + + git init --bare "$REPO" && + test_commit_bulk -C "$REPO" --ref=main 1 && + + git -C "$REPO" update-ref HEAD main && + git -C "$REPO" update-server-info && + + git init malicious && + ( + cd malicious && + + git submodule add "$URI" && + + mkdir -p repo/refs && + touch repo/refs/.gitkeep && + printf "ref: refs/heads/a" >repo/HEAD && + ln -s "$(cd .. && pwd)/sensitive" repo/objects && + + mkdir -p "$HTTPD_URL/dumb" && + ln -s "../../../.git/modules/sub/../../../repo/" "$URI" && + + git add . && + git commit -m "initial commit" + ) && + + # Delete all of the references in our malicious submodule to + # avoid the client attempting to checkout any objects (which + # will be missing, and thus will cause the clone to fail before + # we can trigger the exploit). + git -C "$REPO" for-each-ref --format="delete %(refname)" >in && + git -C "$REPO" update-ref --stdin <in && + git -C "$REPO" update-server-info +' + +test_expect_success 'ambiguous transport does not lead to arbitrary file-inclusion' ' + git clone malicious clone && + test_must_fail git -C clone submodule update --init 2>err && + + test_path_is_missing clone/.git/modules/sub/objects/secret && + # We would actually expect "transport .file. not allowed" here, + # but due to quirks of the URL detection in Git, we mis-parse + # the absolute path as a bogus URL and die before that step. + # + # This works for now, and if we ever fix the URL detection, it + # is OK to change this to detect the transport error. + grep "protocol .* is not supported" err +' + +test_done diff --git a/t/t5750-bundle-uri-parse.sh b/t/t5750-bundle-uri-parse.sh index 7b4f930e53..81bdf58b94 100755 --- a/t/t5750-bundle-uri-parse.sh +++ b/t/t5750-bundle-uri-parse.sh @@ -250,4 +250,41 @@ test_expect_success 'parse config format edge cases: empty key or value' ' test_cmp_config_output expect actual ' +test_expect_success 'parse config format: creationToken heuristic' ' + cat >expect <<-\EOF && + [bundle] + version = 1 + mode = all + heuristic = creationToken + [bundle "one"] + uri = http://example.com/bundle.bdl + creationToken = 123456 + [bundle "two"] + uri = https://example.com/bundle.bdl + creationToken = 12345678901234567890 + [bundle "three"] + uri = file:///usr/share/git/bundle.bdl + creationToken = 1 + EOF + + test-tool bundle-uri parse-config expect >actual 2>err && + test_must_be_empty err && + test_cmp_config_output expect actual +' + +test_expect_success 'parse config format edge cases: creationToken heuristic' ' + cat >expect <<-\EOF && + [bundle] + version = 1 + mode = all + heuristic = creationToken + [bundle "one"] + uri = http://example.com/bundle.bdl + creationToken = bogus + EOF + + test-tool bundle-uri parse-config expect >actual 2>err && + grep "could not parse bundle list key creationToken with value '\''bogus'\''" err +' + test_done diff --git a/t/t6020-bundle-misc.sh b/t/t6020-bundle-misc.sh index 3a1cf30b1d..7d40994991 100755 --- a/t/t6020-bundle-misc.sh +++ b/t/t6020-bundle-misc.sh @@ -566,4 +566,44 @@ test_expect_success 'cloning from filtered bundle has useful error' ' grep "cannot clone from filtered bundle" err ' +test_expect_success 'verify catches unreachable, broken prerequisites' ' + test_when_finished rm -rf clone-from clone-to && + git init clone-from && + ( + cd clone-from && + git checkout -b base && + test_commit A && + git checkout -b tip && + git commit --allow-empty -m "will drop by shallow" && + git commit --allow-empty -m "will keep by shallow" && + git commit --allow-empty -m "for bundle, not clone" && + git bundle create tip.bundle tip~1..tip && + git reset --hard HEAD~1 && + git checkout base + ) && + BAD_OID=$(git -C clone-from rev-parse tip~1) && + TIP_OID=$(git -C clone-from rev-parse tip) && + git clone --depth=1 --no-single-branch \ + "file://$(pwd)/clone-from" clone-to && + ( + cd clone-to && + + # Set up broken history by removing shallow markers + git update-ref -d refs/remotes/origin/tip && + rm .git/shallow && + + # Verify should fail + test_must_fail git bundle verify \ + ../clone-from/tip.bundle 2>err && + grep "some prerequisite commits .* are not connected" err && + test_line_count = 1 err && + + # Unbundling should fail + test_must_fail git bundle unbundle \ + ../clone-from/tip.bundle 2>err && + grep "some prerequisite commits .* are not connected" err && + test_line_count = 1 err + ) +' + test_done diff --git a/t/t6120-describe.sh b/t/t6120-describe.sh index 9a35e783a7..c9afcef201 100755 --- a/t/t6120-describe.sh +++ b/t/t6120-describe.sh @@ -657,4 +657,10 @@ test_expect_success 'setup: describe commits with disjoint bases 2' ' check_describe -C disjoint2 "B-3-gHASH" HEAD +test_expect_success 'setup misleading taggerdates' ' + GIT_COMMITTER_DATE="2006-12-12 12:31" git tag -a -m "another tag" newer-tag-older-commit unique-file~1 +' + +check_describe newer-tag-older-commit~1 --contains unique-file~2 + test_done diff --git a/t/t6132-pathspec-exclude.sh b/t/t6132-pathspec-exclude.sh index cada952f9a..9fdafeb1e9 100755 --- a/t/t6132-pathspec-exclude.sh +++ b/t/t6132-pathspec-exclude.sh @@ -293,11 +293,7 @@ test_expect_success 'add with all negative' ' test_cmp expect actual ' -test_lazy_prereq ADD_I_USE_BUILTIN_OR_PERL ' - test_have_prereq ADD_I_USE_BUILTIN || test_have_prereq PERL -' - -test_expect_success ADD_I_USE_BUILTIN_OR_PERL 'add -p with all negative' ' +test_expect_success 'add -p with all negative' ' H=$(git rev-parse HEAD) && git reset --hard $H && git clean -f && diff --git a/t/t7402-submodule-rebase.sh b/t/t7402-submodule-rebase.sh index ebeca12a71..b19792b326 100755 --- a/t/t7402-submodule-rebase.sh +++ b/t/t7402-submodule-rebase.sh @@ -5,6 +5,7 @@ test_description='Test rebasing, stashing, etc. with submodules' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success setup ' diff --git a/t/t9106-git-svn-commit-diff-clobber.sh b/t/t9106-git-svn-commit-diff-clobber.sh index 3cab0b9720..bca496c40e 100755 --- a/t/t9106-git-svn-commit-diff-clobber.sh +++ b/t/t9106-git-svn-commit-diff-clobber.sh @@ -3,7 +3,6 @@ # Copyright (c) 2006 Eric Wong test_description='git svn commit-diff clobber' -TEST_FAILS_SANITIZE_LEAK=true . ./lib-git-svn.sh test_expect_success 'initialize repo' ' diff --git a/t/t9164-git-svn-dcommit-concurrent.sh b/t/t9164-git-svn-dcommit-concurrent.sh index 1465156072..c8e6c0733f 100755 --- a/t/t9164-git-svn-dcommit-concurrent.sh +++ b/t/t9164-git-svn-dcommit-concurrent.sh @@ -5,7 +5,6 @@ test_description='concurrent git svn dcommit' -TEST_FAILS_SANITIZE_LEAK=true . ./lib-git-svn.sh diff --git a/t/test-lib-functions.sh b/t/test-lib-functions.sh index 75b8ee95e7..58cfd2f1fd 100644 --- a/t/test-lib-functions.sh +++ b/t/test-lib-functions.sh @@ -1767,6 +1767,14 @@ test_region () { return 0 } +# Given a GIT_TRACE2_EVENT log over stdin, writes to stdout a list of URLs +# sent to git-remote-https child processes. +test_remote_https_urls() { + grep -e '"event":"child_start".*"argv":\["git-remote-https",".*"\]' | + sed -e 's/{"event":"child_start".*"argv":\["git-remote-https","//g' \ + -e 's/"\]}//g' +} + # Print the destination of symlink(s) provided as arguments. Basically # the same as the readlink command, but it's not available everywhere. test_readlink () { diff --git a/t/test-lib.sh b/t/test-lib.sh index 01e88781dd..d272cca008 100644 --- a/t/test-lib.sh +++ b/t/test-lib.sh @@ -1937,10 +1937,6 @@ test_lazy_prereq SHA1 ' esac ' -test_lazy_prereq ADD_I_USE_BUILTIN ' - test_bool_env GIT_TEST_ADD_I_USE_BUILTIN true -' - # Ensure that no test accidentally triggers a Git command # that runs the actual maintenance scheduler, affecting a user's # system permanently. diff --git a/userdiff.c b/userdiff.c index d71b82feb7..94cca1a2a8 100644 --- a/userdiff.c +++ b/userdiff.c @@ -170,8 +170,8 @@ PATTERNS("html", "[^<>= \t]+"), PATTERNS("java", "!^[ \t]*(catch|do|for|if|instanceof|new|return|switch|throw|while)\n" - /* Class, enum, and interface declarations */ - "^[ \t]*(([a-z]+[ \t]+)*(class|enum|interface)[ \t]+[A-Za-z][A-Za-z0-9_$]*[ \t]+.*)$\n" + /* Class, enum, interface, and record declarations */ + "^[ \t]*(([a-z-]+[ \t]+)*(class|enum|interface|record)[ \t]+.*)$\n" /* Method definitions; note that constructor signatures are not */ /* matched because they are indistinguishable from method calls. */ "^[ \t]*(([A-Za-z_<>&][][?&<>.,A-Za-z_0-9]*[ \t]+)+[A-Za-z_][A-Za-z_0-9]*[ \t]*\\([^;]*)$", |
