diff options
735 files changed, 20320 insertions, 3823 deletions
diff --git a/.github/workflows/check-whitespace.yml b/.github/workflows/check-whitespace.yml index 8c4358d805..ad3466ad16 100644 --- a/.github/workflows/check-whitespace.yml +++ b/.github/workflows/check-whitespace.yml @@ -1,8 +1,9 @@ name: check-whitespace -# Get the repo with the commits(+1) in the series. +# Get the repository with all commits to ensure that we can analyze +# all of the commits contributed via the Pull Request. # Process `git log --check` output to extract just the check errors. -# Add a comment to the pull request with the check errors. +# Exit with failure upon white-space issues. on: pull_request: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6ed6a9e807..c35200defb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,4 +1,4 @@ -name: CI/PR +name: CI on: [push, pull_request] @@ -7,6 +7,7 @@ env: jobs: ci-config: + name: config runs-on: ubuntu-latest outputs: enabled: ${{ steps.check-ref.outputs.enabled }}${{ steps.skip-if-redundant.outputs.enabled }} @@ -77,6 +78,7 @@ jobs: } windows-build: + name: win build needs: ci-config if: needs.ci-config.outputs.enabled == 'yes' runs-on: windows-latest @@ -97,6 +99,7 @@ jobs: name: windows-artifacts path: artifacts windows-test: + name: win test runs-on: windows-latest needs: [windows-build] strategy: @@ -127,6 +130,7 @@ jobs: name: failed-tests-windows path: ${{env.FAILED_TEST_ARTIFACTS}} vs-build: + name: win+VS build needs: ci-config if: needs.ci-config.outputs.enabled == 'yes' env: @@ -178,6 +182,7 @@ jobs: name: vs-artifacts path: artifacts vs-test: + name: win+VS test runs-on: windows-latest needs: vs-build strategy: @@ -210,6 +215,7 @@ jobs: name: failed-tests-windows path: ${{env.FAILED_TEST_ARTIFACTS}} regular: + name: ${{matrix.vector.jobname}} (${{matrix.vector.pool}}) needs: ci-config if: needs.ci-config.outputs.enabled == 'yes' strategy: @@ -219,14 +225,25 @@ jobs: - jobname: linux-clang cc: clang pool: ubuntu-latest + - jobname: linux-sha256 + cc: clang + os: ubuntu + pool: ubuntu-latest - jobname: linux-gcc cc: gcc + cc_package: gcc-8 + pool: ubuntu-latest + - jobname: linux-TEST-vars + cc: gcc + os: ubuntu + cc_package: gcc-8 pool: ubuntu-latest - jobname: osx-clang cc: clang pool: macos-latest - jobname: osx-gcc cc: gcc + cc_package: gcc-9 pool: macos-latest - jobname: linux-gcc-default cc: gcc @@ -236,7 +253,9 @@ jobs: pool: ubuntu-latest env: CC: ${{matrix.vector.cc}} + CC_PACKAGE: ${{matrix.vector.cc_package}} jobname: ${{matrix.vector.jobname}} + runs_on_pool: ${{matrix.vector.pool}} runs-on: ${{matrix.vector.pool}} steps: - uses: actions/checkout@v2 @@ -251,6 +270,7 @@ jobs: name: failed-tests-${{matrix.vector.jobname}} path: ${{env.FAILED_TEST_ARTIFACTS}} dockerized: + name: ${{matrix.vector.jobname}} (${{matrix.vector.image}}) needs: ci-config if: needs.ci-config.outputs.enabled == 'yes' strategy: @@ -259,7 +279,8 @@ jobs: vector: - jobname: linux-musl image: alpine - - jobname: Linux32 + - jobname: linux32 + os: ubuntu32 image: daald/ubuntu32:xenial - jobname: pedantic image: fedora @@ -289,6 +310,7 @@ jobs: - uses: actions/checkout@v2 - run: ci/install-dependencies.sh - run: ci/run-static-analysis.sh + - run: ci/check-directional-formatting.bash sparse: needs: ci-config if: needs.ci-config.outputs.enabled == 'yes' @@ -310,6 +332,7 @@ jobs: run: ci/install-dependencies.sh - run: make sparse documentation: + name: documentation needs: ci-config if: needs.ci-config.outputs.enabled == 'yes' env: diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 908330a0a3..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,60 +0,0 @@ -language: c - -cache: - directories: - - $HOME/travis-cache - -os: - - linux - - osx - -osx_image: xcode10.1 - -compiler: - - clang - - gcc - -matrix: - include: - - env: jobname=linux-gcc-default - os: linux - compiler: - addons: - before_install: - - env: jobname=linux-gcc-4.8 - os: linux - dist: trusty - compiler: - - env: jobname=Linux32 - os: linux - compiler: - addons: - services: - - docker - before_install: - script: ci/run-docker.sh - - env: jobname=linux-musl - os: linux - compiler: - addons: - services: - - docker - before_install: - script: ci/run-docker.sh - - env: jobname=StaticAnalysis - os: linux - compiler: - script: ci/run-static-analysis.sh - after_failure: - - env: jobname=Documentation - os: linux - compiler: - script: ci/test-documentation.sh - after_failure: - -before_install: ci/install-dependencies.sh -script: ci/run-build-and-tests.sh -after_failure: ci/print-test-failures.sh - -notifications: - email: false diff --git a/Documentation/CodingGuidelines b/Documentation/CodingGuidelines index 711cb9171e..0e27b5395d 100644 --- a/Documentation/CodingGuidelines +++ b/Documentation/CodingGuidelines @@ -499,6 +499,33 @@ For Python scripts: - Where required libraries do not restrict us to Python 2, we try to also be compatible with Python 3.1 and later. + +Program Output + + We make a distinction between a Git command's primary output and + output which is merely chatty feedback (for instance, status + messages, running transcript, or progress display), as well as error + messages. Roughly speaking, a Git command's primary output is that + which one might want to capture to a file or send down a pipe; its + chatty output should not interfere with these use-cases. + + As such, primary output should be sent to the standard output stream + (stdout), and chatty output should be sent to the standard error + stream (stderr). Examples of commands which produce primary output + include `git log`, `git show`, and `git branch --list` which generate + output on the stdout stream. + + Not all Git commands have primary output; this is often true of + commands whose main function is to perform an action. Some action + commands are silent, whereas others are chatty. An example of a + chatty action commands is `git clone` with its "Cloning into + '<path>'..." and "Checking connectivity..." status messages which it + sends to the stderr stream. + + Error messages from Git commands should always be sent to the stderr + stream. + + Error Messages - Do not end error messages with a full stop. diff --git a/Documentation/MyFirstContribution.txt b/Documentation/MyFirstContribution.txt index b20bc8e914..63a2ef5449 100644 --- a/Documentation/MyFirstContribution.txt +++ b/Documentation/MyFirstContribution.txt @@ -905,19 +905,34 @@ Sending emails with Git is a two-part process; before you can prepare the emails themselves, you'll need to prepare the patches. Luckily, this is pretty simple: ---- -$ git format-patch --cover-letter -o psuh/ master..psuh ----- - -The `--cover-letter` parameter tells `format-patch` to create a cover letter -template for you. You will need to fill in the template before you're ready -to send - but for now, the template will be next to your other patches. - -The `-o psuh/` parameter tells `format-patch` to place the patch files into a -directory. This is useful because `git send-email` can take a directory and -send out all the patches from there. - -`master..psuh` tells `format-patch` to generate patches for the difference -between `master` and `psuh`. It will make one patch file per commit. After you +$ git format-patch --cover-letter -o psuh/ --base=auto psuh@{u}..psuh +---- + + . The `--cover-letter` option tells `format-patch` to create a + cover letter template for you. You will need to fill in the + template before you're ready to send - but for now, the template + will be next to your other patches. + + . The `-o psuh/` option tells `format-patch` to place the patch + files into a directory. This is useful because `git send-email` + can take a directory and send out all the patches from there. + + . The `--base=auto` option tells the command to record the "base + commit", on which the recipient is expected to apply the patch + series. The `auto` value will cause `format-patch` to compute + the base commit automatically, which is the merge base of tip + commit of the remote-tracking branch and the specified revision + range. + + . The `psuh@{u}..psuh` option tells `format-patch` to generate + patches for the commits you created on the `psuh` branch since it + forked from its upstream (which is `origin/master` if you + followed the example in the "Set up your workspace" section). If + you are already on the `psuh` branch, you can just say `@{u}`, + which means "commits on the current branch since it forked from + its upstream", which is the same thing. + +The command will make one patch file per commit. After you run, you can go have a look at each of the patches with your favorite text editor and make sure everything looks alright; however, it's not recommended to make code fixups via the patch file. It's a better idea to make the change the diff --git a/Documentation/MyFirstObjectWalk.txt b/Documentation/MyFirstObjectWalk.txt index 45eb84d8b4..ca267941f3 100644 --- a/Documentation/MyFirstObjectWalk.txt +++ b/Documentation/MyFirstObjectWalk.txt @@ -58,14 +58,19 @@ running, enable trace output by setting the environment variable `GIT_TRACE`. Add usage text and `-h` handling, like all subcommands should consistently do (our test suite will notice and complain if you fail to do so). +We'll need to include the `parse-options.h` header. ---- +#include "parse-options.h" + +... + int cmd_walken(int argc, const char **argv, const char *prefix) { const char * const walken_usage[] = { N_("git walken"), NULL, - } + }; struct option options[] = { OPT_END() }; @@ -195,9 +200,14 @@ Similarly to the default values, we don't have anything to do here yet ourselves; however, we should call `git_default_config()` if we aren't calling any other existing config callbacks. -Add a new function to `builtin/walken.c`: +Add a new function to `builtin/walken.c`. +We'll also need to include the `config.h` header: ---- +#include "config.h" + +... + static int git_walken_config(const char *var, const char *value, void *cb) { /* @@ -229,8 +239,14 @@ typically done by calling `repo_init_revisions()` with the repository you intend to target, as well as the `prefix` argument of `cmd_walken` and your `rev_info` struct. -Add the `struct rev_info` and the `repo_init_revisions()` call: +Add the `struct rev_info` and the `repo_init_revisions()` call. +We'll also need to include the `revision.h` header: + ---- +#include "revision.h" + +... + int cmd_walken(int argc, const char **argv, const char *prefix) { /* This can go wherever you like in your declarations.*/ @@ -624,9 +640,14 @@ static void walken_object_walk(struct rev_info *rev) ---- Let's start by calling just the unfiltered walk and reporting our counts. -Complete your implementation of `walken_object_walk()`: +Complete your implementation of `walken_object_walk()`. +We'll also need to include the `list-objects.h` header. ---- +#include "list-objects.h" + +... + traverse_commit_list(rev, walken_show_commit, walken_show_object, NULL); printf("commits %d\nblobs %d\ntags %d\ntrees %d\n", commit_count, @@ -697,7 +718,7 @@ First, we'll need to `#include "list-objects-filter-options.h"` and set up the ---- static void walken_object_walk(struct rev_info *rev) { - struct list_objects_filter_options filter_options = {}; + struct list_objects_filter_options filter_options = { 0 }; ... ---- diff --git a/Documentation/RelNotes/2.35.0.txt b/Documentation/RelNotes/2.35.0.txt new file mode 100644 index 0000000000..ff90dd4776 --- /dev/null +++ b/Documentation/RelNotes/2.35.0.txt @@ -0,0 +1,334 @@ +Git 2.35 Release Notes +====================== + +Updates since Git 2.34 +---------------------- + +Backward compatibility warts + + * "_" is now treated as any other URL-valid characters in an URL when + matching the per-URL configuration variable names. + + +UI, Workflows & Features + + * "git status --porcelain=v2" now show the number of stash entries + with --show-stash like the normal output does. + + * "git stash" learned the "--staged" option to stash away what has + been added to the index (and nothing else). + + * "git var GIT_DEFAULT_BRANCH" is a way to see what name is used for + the newly created branch if "git init" is run. + + * Various operating modes of "git reset" have been made to work + better with the sparse index. + + * "git submodule deinit" for a submodule whose .git metadata + directory is embedded in its working tree refused to work, until + the submodule gets converted to use the "absorbed" form where the + metadata directory is stored in superproject, and a gitfile at the + top-level of the working tree of the submodule points at it. The + command is taught to convert such submodules to the absorbed form + as needed. + + * The completion script (in contrib/) learns that the "--date" + option of commands from the "git log" family takes "human" and + "auto" as valid values. + + * "Zealous diff3" style of merge conflict presentation has been added. + + * The "git log --format=%(describe)" placeholder has been extended to + allow passing selected command-line options to the underlying "git + describe" command. + + * "default" and "reset" have been added to our color palette. + + * The cryptographic signing using ssh keys can specify literal keys + for keytypes whose name do not begin with the "ssh-" prefix by + using the "key::" prefix mechanism (e.g. "key::ecdsa-sha2-nistp256"). + + * "git fetch" without the "--update-head-ok" option ought to protect + a checked out branch from getting updated, to prevent the working + tree that checks it out to go out of sync. The code was written + before the use of "git worktree" got widespread, and only checked + the branch that was checked out in the current worktree, which has + been updated. + + * "git name-rev" has been tweaked to give output that is shorter and + easier to understand. + + * "git apply" has been taught to ignore a message without a patch + with the "--allow-empty" option. It also learned to honor the + "--quiet" option given from the command line. + + * The "init" and "set" subcommands in "git sparse-checkout" have been + unified for a better user experience and performance. + + * Many git commands that deal with working tree files try to remove a + directory that becomes empty (i.e. "git switch" from a branch that + has the directory to another branch that does not would attempt + remove all files in the directory and the directory itself). This + drops users into an unfamiliar situation if the command was run in + a subdirectory that becomes subject to removal due to the command. + The commands have been taught to keep an empty directory if it is + the directory they were started in to avoid surprising users. + + * "git am" learns "--empty=(stop|drop|keep)" option to tweak what is + done to a piece of e-mail without a patch in it. + + * The default merge message prepared by "git merge" records the name + of the current branch; the name can be overridden with a new option + to allow users to pretend a merge is made on a different branch. + +Performance, Internal Implementation, Development Support etc. + + * The use of errno as a means to carry the nature of error in the ref + API implementation has been reworked and reduced. + + * Teach and encourage first-time contributors to this project to + state the base commit when they submit their topic. + + * The command line complation for "git send-email" options have been + tweaked to make it easier to keep it in sync with the command itself. + + * Ensure that the sparseness of the in-core index matches the + index.sparse configuration specified by the repository immediately + after the on-disk index file is read. + + * Code clean-up to eventually allow information on remotes defined + for an arbitrary repository to be read. + + * Build optimization. + + * Tighten code for testing pack-bitmap. + + * Weather balloon to break people with compilers that do not support + C99. + + * The "reftable" backend for the refs API, without integrating into + the refs subsystem, has been added. + + * More tests are marked as leak-free. + + * The test framework learns to list unsatisfied test prerequisites, + and optionally error out when prerequisites that are expected to be + satisfied are not. + + * The default setting for trace2 event nesting was too low to cause + test failures, which is worked around by bumping it up in the test + framework. + + * Drop support for TravisCI and update test workflows at GitHub. + + * Many tests that used to need GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME + mechanism to force "git" to use 'master' as the default name for + the initial branch no longer need it; the use of the mechanism from + them have been removed. + + * Allow running our tests while disabling fsync. + + * Document the parameters given to the reflog entry iterator callback + functions. + (merge e6e94f34b2 jc/reflog-iterator-callback-doc later to maint). + + * The test helper for refs subsystem learned to write bogus and/or + nonexistent object name to refs to simulate error situations we + want to test Git in. + + * "diff --histogram" optimization. + + * Weather balloon to find compilers that do not grok variable + declaration in the for() loop. + + * diff and blame commands have been taught to work better with sparse + index. + + * The chainlint test script linter in the test suite has been updated. + + * The DEVELOPER=yes build uses -std=gnu99 now. + + * "git format-patch" uses a single rev_info instance and then exits. + Mark the structure with UNLEAK() macro to squelch leak sanitizer. + + * New interface into the tmp-objdir API to help in-core use of the + quarantine feature. + + * Broken &&-chains in the test scripts have been corrected. + + * The RCS keyword substitution in "git p4" used to be done assuming + that the contents are UTF-8 text, which can trigger decoding + errors. We now treat the contents as a bytestring for robustness + and correctness. + + * The conditions to choose different definitions of the FLEX_ARRAY + macro for vendor compilers has been simplified to make it easier to + maintain. + + * Correctness and performance update to "diff --color-moved" feature. + + * "git upload-pack" (the other side of "git fetch") used a 8kB buffer + but most of its payload came on 64kB "packets". The buffer size + has been enlarged so that such a packet fits. + + +Fixes since v2.34 +----------------- + + * "git grep" looking in a blob that has non-UTF8 payload was + completely broken when linked with certain versions of PCREv2 + library in the latest release. + + * Other code cleanup, docfix, build fix, etc. + + * "git pull" with any strategy when the other side is behind us + should succeed as it is a no-op, but doesn't. + + * An earlier change in 2.34.0 caused JGit application (that abused + GIT_EDITOR mechanism when invoking "git config") to get stuck with + a SIGTTOU signal; it has been reverted. + + * An earlier change that broke .gitignore matching has been reverted. + + * Things like "git -c branch.sort=bogus branch new HEAD", i.e. the + operation modes of the "git branch" command that do not need the + sort key information, no longer errors out by seeing a bogus sort + key. + (merge 98e7ab6d42 jc/fix-ref-sorting-parse later to maint). + + * The compatibility implementation for unsetenv(3) were written to + mimic ancient, non-POSIX, variant seen in an old glibc; it has been + changed to return an integer to match the more modern era. + (merge a38989bd5b jc/unsetenv-returns-an-int later to maint). + + * The clean/smudge conversion code path has been prepared to better + work on platforms where ulong is narrower than size_t. + (merge 596b5e77c9 mc/clean-smudge-with-llp64 later to maint). + + * Redact the path part of packfile URI that appears in the trace output. + (merge 0ba558ffb1 if/redact-packfile-uri later to maint). + + * CI has been taught to catch some Unicode directional formatting + sequence that can be used in certain mischief. + (merge 0e7696c64d js/ci-no-directional-formatting later to maint). + + * The "--date=format:<strftime>" gained a workaround for the lack of + system support for a non-local timezone to handle "%s" placeholder. + (merge 9b591b9403 jk/strbuf-addftime-seconds-since-epoch later to maint). + + * The "merge" subcommand of "git jump" (in contrib/) silently ignored + pathspec and other parameters. + (merge 67ba13e5a4 jk/jump-merge-with-pathspec later to maint). + + * The code to decode the length of packed object size has been + corrected. + (merge 34de5b8eac jt/pack-header-lshift-overflow later to maint). + + * The advice message given by "git pull" when the user hasn't made a + choice between merge and rebase still said that the merge is the + default, which no longer is the case. This has been corrected. + (merge 71076d0edd ah/advice-pull-has-no-preference-between-rebase-and-merge later to maint). + + * "git fetch", when received a bad packfile, can fail with SIGPIPE. + This wasn't wrong per-se, but we now detect the situation and fail + in a more predictable way. + (merge 2a4aed42ec jk/fetch-pack-avoid-sigpipe-to-index-pack later to maint). + + * The function to cull a child process and determine the exit status + had two separate code paths for normal callers and callers in a + signal handler, and the latter did not yield correct value when the + child has caught a signal. The handling of the exit status has + been unified for these two code paths. An existing test with + flakiness has also been corrected. + (merge 5263e22cba jk/t7006-sigpipe-tests-fix later to maint). + + * When a non-existent program is given as the pager, we tried to + reuse an uninitialized child_process structure and crashed, which + has been fixed. + (merge f917f57f40 em/missing-pager later to maint). + + * The single-key-input mode in "git add -p" had some code to handle + keys that generate a sequence of input via ReadKey(), which did not + handle end-of-file correctly, which has been fixed. + (merge fc8a8126df cb/add-p-single-key-fix later to maint). + + * "git rebase -x" added an unnecessary 'exec' instructions before + 'noop', which has been corrected. + (merge cc9dcdee61 en/rebase-x-fix later to maint). + + * When the "git push" command is killed while the receiving end is + trying to report what happened to the ref update proposals, the + latter used to die, due to SIGPIPE. The code now ignores SIGPIPE + to increase our chances to run the post-receive hook after it + happens. + (merge d34182b9e3 rj/receive-pack-avoid-sigpipe-during-status-reporting later to maint). + + * "git worktree add" showed "Preparing worktree" message to the + standard output stream, but when it failed, the message from die() + went to the standard error stream. Depending on the order the + stdio streams are flushed at the program end, this resulted in + confusing output. It has been corrected by sending all the chatty + messages to the standard error stream. + (merge b50252484f es/worktree-chatty-to-stderr later to maint). + + * Coding guideline document has been updated to clarify what goes to + standard error in our system. + (merge e258eb4800 es/doc-stdout-vs-stderr later to maint). + + * The sparse-index/sparse-checkout feature had a bug in its use of + the matching code to determine which path is in or outside the + sparse checkout patterns. + (merge 8c5de0d265 ds/sparse-deep-pattern-checkout-fix later to maint). + + * "git rebase -x" by mistake started exporting the GIT_DIR and + GIT_WORK_TREE environment variables when the command was rewritten + in C, which has been corrected. + (merge 434e0636db en/rebase-x-wo-git-dir-env later to maint). + + * When "git log" implicitly enabled the "decoration" processing + without being explicitly asked with "--decorate" option, it failed + to read and honor the settings given by the "--decorate-refs" + option. + + * "git fetch --set-upstream" did not check if there is a current + branch, leading to a segfault when it is run on a detached HEAD, + which has been corrected. + (merge 17baeaf82d ab/fetch-set-upstream-while-detached later to maint). + + * Among some code paths that ask an yes/no question, only one place + gave a prompt that looked different from the others, which has been + updated to match what the others create. + (merge 0fc8ed154c km/help-prompt-fix later to maint). + + * "git log --invert-grep --author=<name>" used to exclude commits + written by the given author, but now "--invert-grep" only affects + the matches made by the "--grep=<pattern>" option. + (merge 794c000267 rs/log-invert-grep-with-headers later to maint). + + * "git grep --perl-regexp" failed to match UTF-8 characters with + wildcard when the pattern consists only of ASCII letters, which has + been corrected. + (merge 32e3e8bc55 rs/pcre2-utf later to maint). + + * Other code cleanup, docfix, build fix, etc. + (merge 74db416c9c cw/protocol-v2-doc-fix later to maint). + (merge f9b2b6684d ja/doc-cleanup later to maint). + (merge 7d1b866778 jc/fix-first-object-walk later to maint). + (merge 538ac74604 js/trace2-avoid-recursive-errors later to maint). + (merge 152923b132 jk/t5319-midx-corruption-test-deflake later to maint). + (merge 9081a421a6 ab/checkout-branch-info-leakfix later to maint). + (merge 42c456ff81 rs/mergesort later to maint). + (merge ad506e6780 tl/midx-docfix later to maint). + (merge bf5b83fd8a hk/ci-checkwhitespace-commentfix later to maint). + (merge 49f1eb3b34 jk/refs-g11-workaround later to maint). + (merge 7d3fc7df70 jt/midx-doc-fix later to maint). + (merge 7b089120d9 hn/create-reflog-simplify later to maint). + (merge 9e12400da8 cb/mingw-gmtime-r later to maint). + (merge 0bf0de6cc7 tb/pack-revindex-on-disk-cleanup later to maint). + (merge 2c68f577fc ew/cbtree-remove-unused-and-broken-cb-unlink later to maint). + (merge eafd6e7e55 ab/die-with-bug later to maint). + (merge 91028f7659 jc/grep-patterntype-default-doc later to maint). + (merge 47ca93d071 ds/repack-fixlets later to maint). + (merge e6a9bc0c60 rs/t4202-invert-grep-test-fix later to maint). + (merge deb5407a42 gh/gpg-doc-markup-fix later to maint). + (merge 999bba3e0b rs/daemon-plug-leak later to maint). diff --git a/Documentation/config.txt b/Documentation/config.txt index 1167e88e34..b168f02dc3 100644 --- a/Documentation/config.txt +++ b/Documentation/config.txt @@ -262,11 +262,19 @@ color:: colors (at most two, one for foreground and one for background) and attributes (as many as you want), separated by spaces. + -The basic colors accepted are `normal`, `black`, `red`, `green`, `yellow`, -`blue`, `magenta`, `cyan` and `white`. The first color given is the -foreground; the second is the background. All the basic colors except -`normal` have a bright variant that can be specified by prefixing the -color with `bright`, like `brightred`. +The basic colors accepted are `normal`, `black`, `red`, `green`, +`yellow`, `blue`, `magenta`, `cyan`, `white` and `default`. The first +color given is the foreground; the second is the background. All the +basic colors except `normal` and `default` have a bright variant that can +be specified by prefixing the color with `bright`, like `brightred`. ++ +The color `normal` makes no change to the color. It is the same as an +empty string, but can be used as the foreground color when specifying a +background color alone (for example, "normal red"). ++ +The color `default` explicitly resets the color to the terminal default, +for example to specify a cleared background. Although it varies between +terminals, this is usually not the same as setting to "white black". + Colors may also be given as numbers between 0 and 255; these use ANSI 256-color mode (but note that not all terminals may support this). If @@ -280,6 +288,11 @@ The position of any attributes with respect to the colors be turned off by prefixing them with `no` or `no-` (e.g., `noreverse`, `no-ul`, etc). + +The pseudo-attribute `reset` resets all colors and attributes before +applying the specified coloring. For example, `reset green` will result +in a green foreground and default background without any active +attributes. ++ An empty color string produces no color effect at all. This can be used to avoid coloring specific elements without disabling color entirely. + diff --git a/Documentation/config/gpg.txt b/Documentation/config/gpg.txt index 4f30c7dbdd..0cb189a077 100644 --- a/Documentation/config/gpg.txt +++ b/Documentation/config/gpg.txt @@ -34,7 +34,7 @@ gpg.minTrustLevel:: * `fully` * `ultimate` -gpg.ssh.defaultKeyCommand: +gpg.ssh.defaultKeyCommand:: This command that will be run when user.signingkey is not set and a ssh signature is requested. On successful exit a valid ssh public key is expected in the first line of its output. To automatically use the first @@ -44,7 +44,7 @@ gpg.ssh.allowedSignersFile:: A file containing ssh public keys which you are willing to trust. The file consists of one or more lines of principals followed by an ssh public key. - e.g.: user1@example.com,user2@example.com ssh-rsa AAAAX1... + e.g.: `user1@example.com,user2@example.com ssh-rsa AAAAX1...` See ssh-keygen(1) "ALLOWED SIGNERS" for details. The principal is only used to identify the key and is available when verifying a signature. @@ -64,6 +64,11 @@ A repository that only allows signed commits can store the file in the repository itself using a path relative to the top-level of the working tree. This way only committers with an already valid key can add or change keys in the keyring. + +Since OpensSSH 8.8 this file allows specifying a key lifetime using valid-after & +valid-before options. Git will mark signatures as valid if the signing key was +valid at the time of the signatures creation. This allows users to change a +signing key without invalidating all previously made signatures. ++ Using a SSH CA key with the cert-authority option (see ssh-keygen(1) "CERTIFICATES") is also valid. diff --git a/Documentation/config/grep.txt b/Documentation/config/grep.txt index 44abe45a7c..182edd813a 100644 --- a/Documentation/config/grep.txt +++ b/Documentation/config/grep.txt @@ -8,7 +8,8 @@ grep.patternType:: Set the default matching behavior. Using a value of 'basic', 'extended', 'fixed', or 'perl' will enable the `--basic-regexp`, `--extended-regexp`, `--fixed-strings`, or `--perl-regexp` option accordingly, while the - value 'default' will return to the default matching behavior. + value 'default' will use the `grep.extendedRegexp` option to choose + between 'basic' and 'extended'. grep.extendedRegexp:: If set to true, enable `--extended-regexp` option by default. This diff --git a/Documentation/config/merge.txt b/Documentation/config/merge.txt index e27cc63944..99e83dd36e 100644 --- a/Documentation/config/merge.txt +++ b/Documentation/config/merge.txt @@ -4,7 +4,14 @@ merge.conflictStyle:: shows a `<<<<<<<` conflict marker, changes made by one side, a `=======` marker, changes made by the other side, and then a `>>>>>>>` marker. An alternate style, "diff3", adds a `|||||||` - marker and the original text before the `=======` marker. + marker and the original text before the `=======` marker. The + "merge" style tends to produce smaller conflict regions than diff3, + both because of the exclusion of the original text, and because + when a subset of lines match on the two sides they are just pulled + out of the conflict region. Another alternate style, "zdiff3", is + similar to diff3 but removes matching lines on the two sides from + the conflict region when those matching lines appear near either + the beginning or end of a conflict region. merge.defaultToUpstream:: If merge is called without any commit argument, merge the upstream diff --git a/Documentation/config/user.txt b/Documentation/config/user.txt index ad78dce9ec..ec9233b060 100644 --- a/Documentation/config/user.txt +++ b/Documentation/config/user.txt @@ -36,10 +36,13 @@ user.signingKey:: commit, you can override the default selection with this variable. This option is passed unchanged to gpg's --local-user parameter, so you may specify a key using any method that gpg supports. - If gpg.format is set to "ssh" this can contain the literal ssh public - key (e.g.: "ssh-rsa XXXXXX identifier") or a file which contains it and - corresponds to the private key used for signing. The private key - needs to be available via ssh-agent. Alternatively it can be set to - a file containing a private key directly. If not set git will call - gpg.ssh.defaultKeyCommand (e.g.: "ssh-add -L") and try to use the first - key available. + If gpg.format is set to `ssh` this can contain the path to either + your private ssh key or the public key when ssh-agent is used. + Alternatively it can contain a public key prefixed with `key::` + directly (e.g.: "key::ssh-rsa XXXXXX identifier"). The private key + needs to be available via ssh-agent. If not set git will call + gpg.ssh.defaultKeyCommand (e.g.: "ssh-add -L") and try to use the + first key available. For backward compatibility, a raw key which + begins with "ssh-", such as "ssh-rsa XXXXXX identifier", is treated + as "key::ssh-rsa XXXXXX identifier", but this form is deprecated; + use the `key::` form instead. diff --git a/Documentation/date-formats.txt b/Documentation/date-formats.txt index 99c455f51c..67645cae64 100644 --- a/Documentation/date-formats.txt +++ b/Documentation/date-formats.txt @@ -5,9 +5,9 @@ The `GIT_AUTHOR_DATE` and `GIT_COMMITTER_DATE` environment variables support the following date formats: Git internal format:: - It is `<unix timestamp> <time zone offset>`, where `<unix - timestamp>` is the number of seconds since the UNIX epoch. - `<time zone offset>` is a positive or negative offset from UTC. + It is `<unix-timestamp> <time-zone-offset>`, where + `<unix-timestamp>` is the number of seconds since the UNIX epoch. + `<time-zone-offset>` is a positive or negative offset from UTC. For example CET (which is 1 hour ahead of UTC) is `+0100`. RFC 2822:: diff --git a/Documentation/git-am.txt b/Documentation/git-am.txt index 0a4a984dfd..09107fb106 100644 --- a/Documentation/git-am.txt +++ b/Documentation/git-am.txt @@ -16,8 +16,9 @@ SYNOPSIS [--exclude=<path>] [--include=<path>] [--reject] [-q | --quiet] [--[no-]scissors] [-S[<keyid>]] [--patch-format=<format>] [--quoted-cr=<action>] + [--empty=(stop|drop|keep)] [(<mbox> | <Maildir>)...] -'git am' (--continue | --skip | --abort | --quit | --show-current-patch[=(diff|raw)]) +'git am' (--continue | --skip | --abort | --quit | --show-current-patch[=(diff|raw)] | --allow-empty) DESCRIPTION ----------- @@ -63,6 +64,14 @@ OPTIONS --quoted-cr=<action>:: This flag will be passed down to 'git mailinfo' (see linkgit:git-mailinfo[1]). +--empty=(stop|drop|keep):: + By default, or when the option is set to 'stop', the command + errors out on an input e-mail message lacking a patch + and stops into the middle of the current am session. When this + option is set to 'drop', skip such an e-mail message instead. + When this option is set to 'keep', create an empty commit, + recording the contents of the e-mail message as its log. + -m:: --message-id:: Pass the `-m` flag to 'git mailinfo' (see linkgit:git-mailinfo[1]), @@ -191,6 +200,11 @@ default. You can use `--no-utf8` to override this. the e-mail message; if `diff`, show the diff portion only. Defaults to `raw`. +--allow-empty:: + After a patch failure on an input e-mail message lacking a patch, + create an empty commit with the contents of the e-mail message + as its log message. + DISCUSSION ---------- diff --git a/Documentation/git-apply.txt b/Documentation/git-apply.txt index aa1ae56a25..b6d77f4206 100644 --- a/Documentation/git-apply.txt +++ b/Documentation/git-apply.txt @@ -16,7 +16,7 @@ SYNOPSIS [--ignore-space-change | --ignore-whitespace] [--whitespace=(nowarn|warn|fix|error|error-all)] [--exclude=<path>] [--include=<path>] [--directory=<root>] - [--verbose] [--unsafe-paths] [<patch>...] + [--verbose | --quiet] [--unsafe-paths] [--allow-empty] [<patch>...] DESCRIPTION ----------- @@ -228,6 +228,11 @@ behavior: current patch being applied will be printed. This option will cause additional information to be reported. +-q:: +--quiet:: + Suppress stderr output. Messages about patch status and progress + will not be printed. + --recount:: Do not trust the line counts in the hunk headers, but infer them by inspecting the patch (e.g. after editing the patch without @@ -251,6 +256,10 @@ When `git apply` is used as a "better GNU patch", the user can pass the `--unsafe-paths` option to override this safety check. This option has no effect when `--index` or `--cached` is in use. +--allow-empty:: + Don't return error for patches containing no diff. This includes + empty patches and patches with commit text only. + CONFIGURATION ------------- diff --git a/Documentation/git-archimport.txt b/Documentation/git-archimport.txt index a595a0ffee..847777fd17 100644 --- a/Documentation/git-archimport.txt +++ b/Documentation/git-archimport.txt @@ -9,14 +9,14 @@ git-archimport - Import a GNU Arch repository into Git SYNOPSIS -------- [verse] -'git archimport' [-h] [-v] [-o] [-a] [-f] [-T] [-D depth] [-t tempdir] - <archive/branch>[:<git-branch>] ... +'git archimport' [-h] [-v] [-o] [-a] [-f] [-T] [-D <depth>] [-t <tempdir>] + <archive>/<branch>[:<git-branch>]... DESCRIPTION ----------- Imports a project from one or more GNU Arch repositories. It will follow branches -and repositories within the namespaces defined by the <archive/branch> +and repositories within the namespaces defined by the <archive>/<branch> parameters supplied. If it cannot find the remote branch a merge comes from it will just import it as a regular commit. If it can find it, it will mark it as a merge whenever possible (see discussion below). @@ -27,7 +27,7 @@ import new branches within the provided roots. It expects to be dealing with one project only. If it sees branches that have different roots, it will refuse to run. In that case, -edit your <archive/branch> parameters to define clearly the scope of the +edit your <archive>/<branch> parameters to define clearly the scope of the import. 'git archimport' uses `tla` extensively in the background to access the @@ -42,7 +42,7 @@ incremental imports. While 'git archimport' will try to create sensible branch names for the archives that it imports, it is also possible to specify Git branch names -manually. To do so, write a Git branch name after each <archive/branch> +manually. To do so, write a Git branch name after each <archive>/<branch> parameter, separated by a colon. This way, you can shorten the Arch branch names and convert Arch jargon to Git jargon, for example mapping a "PROJECT{litdd}devo{litdd}VERSION" branch to "master". @@ -104,8 +104,8 @@ OPTIONS Override the default tempdir. -<archive/branch>:: - Archive/branch identifier in a format that `tla log` understands. +<archive>/<branch>:: + <archive>/<branch> identifier in a format that `tla log` understands. GIT diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt index d473c9bf38..c497db7eae 100644 --- a/Documentation/git-checkout.txt +++ b/Documentation/git-checkout.txt @@ -11,7 +11,7 @@ SYNOPSIS 'git checkout' [-q] [-f] [-m] [<branch>] 'git checkout' [-q] [-f] [-m] --detach [<branch>] 'git checkout' [-q] [-f] [-m] [--detach] <commit> -'git checkout' [-q] [-f] [-m] [[-b|-B|--orphan] <new_branch>] [<start_point>] +'git checkout' [-q] [-f] [-m] [[-b|-B|--orphan] <new-branch>] [<start-point>] 'git checkout' [-f|--ours|--theirs|-m|--conflict=<style>] [<tree-ish>] [--] <pathspec>... 'git checkout' [-f|--ours|--theirs|-m|--conflict=<style>] [<tree-ish>] --pathspec-from-file=<file> [--pathspec-file-nul] 'git checkout' (-p|--patch) [<tree-ish>] [--] [<pathspec>...] @@ -43,7 +43,7 @@ You could omit `<branch>`, in which case the command degenerates to rather expensive side-effects to show only the tracking information, if exists, for the current branch. -'git checkout' -b|-B <new_branch> [<start point>]:: +'git checkout' -b|-B <new-branch> [<start-point>]:: Specifying `-b` causes a new branch to be created as if linkgit:git-branch[1] were called and then checked out. In @@ -52,11 +52,11 @@ if exists, for the current branch. `--track` without `-b` implies branch creation; see the description of `--track` below. + -If `-B` is given, `<new_branch>` is created if it doesn't exist; otherwise, it +If `-B` is given, `<new-branch>` is created if it doesn't exist; otherwise, it is reset. This is the transactional equivalent of + ------------ -$ git branch -f <branch> [<start point>] +$ git branch -f <branch> [<start-point>] $ git checkout <branch> ------------ + @@ -145,13 +145,13 @@ as `ours` (i.e. "our shared canonical history"), while what you did on your side branch as `theirs` (i.e. "one contributor's work on top of it"). --b <new_branch>:: - Create a new branch named `<new_branch>` and start it at - `<start_point>`; see linkgit:git-branch[1] for details. +-b <new-branch>:: + Create a new branch named `<new-branch>` and start it at + `<start-point>`; see linkgit:git-branch[1] for details. --B <new_branch>:: - Creates the branch `<new_branch>` and start it at `<start_point>`; - if it already exists, then reset it to `<start_point>`. This is +-B <new-branch>:: + Creates the branch `<new-branch>` and start it at `<start-point>`; + if it already exists, then reset it to `<start-point>`. This is equivalent to running "git branch" with "-f"; see linkgit:git-branch[1] for details. @@ -210,16 +210,16 @@ variable. `<commit>` is not a branch name. See the "DETACHED HEAD" section below for details. ---orphan <new_branch>:: - Create a new 'orphan' branch, named `<new_branch>`, started from - `<start_point>` and switch to it. The first commit made on this +--orphan <new-branch>:: + Create a new 'orphan' branch, named `<new-branch>`, started from + `<start-point>` and switch to it. The first commit made on this new branch will have no parents and it will be the root of a new history totally disconnected from all the other branches and commits. + The index and the working tree are adjusted as if you had previously run -`git checkout <start_point>`. This allows you to start a new history -that records a set of paths similar to `<start_point>` by easily running +`git checkout <start-point>`. This allows you to start a new history +that records a set of paths similar to `<start-point>` by easily running `git commit -a` to make the root commit. + This can be useful when you want to publish the tree from a commit @@ -229,7 +229,7 @@ whose full history contains proprietary or otherwise encumbered bits of code. + If you want to start a disconnected history that records a set of paths -that is totally different from the one of `<start_point>`, then you should +that is totally different from the one of `<start-point>`, then you should clear the index and the working tree right after creating the orphan branch by running `git rm -rf .` from the top level of the working tree. Afterwards you will be ready to prepare your new files, repopulating the @@ -266,8 +266,7 @@ When switching branches with `--merge`, staged changes may be lost. The same as `--merge` option above, but changes the way the conflicting hunks are presented, overriding the `merge.conflictStyle` configuration variable. Possible values are - "merge" (default) and "diff3" (in addition to what is shown by - "merge" style, shows the original contents). + "merge" (default), "diff3", and "zdiff3". -p:: --patch:: @@ -341,10 +340,10 @@ As a special case, you may use `A...B` as a shortcut for the merge base of `A` and `B` if there is exactly one merge base. You can leave out at most one of `A` and `B`, in which case it defaults to `HEAD`. -<new_branch>:: +<new-branch>:: Name for the new branch. -<start_point>:: +<start-point>:: The name of a commit at which to start the new branch; see linkgit:git-branch[1] for details. Defaults to `HEAD`. + diff --git a/Documentation/git-cherry-pick.txt b/Documentation/git-cherry-pick.txt index 5d750314b2..78dcc9171f 100644 --- a/Documentation/git-cherry-pick.txt +++ b/Documentation/git-cherry-pick.txt @@ -8,7 +8,7 @@ git-cherry-pick - Apply the changes introduced by some existing commits SYNOPSIS -------- [verse] -'git cherry-pick' [--edit] [-n] [-m parent-number] [-s] [-x] [--ff] +'git cherry-pick' [--edit] [-n] [-m <parent-number>] [-s] [-x] [--ff] [-S[<keyid>]] <commit>... 'git cherry-pick' (--continue | --skip | --abort | --quit) @@ -81,8 +81,8 @@ OPTIONS described above, and `-r` was to disable it. Now the default is not to do `-x` so this option is a no-op. --m parent-number:: ---mainline parent-number:: +-m <parent-number>:: +--mainline <parent-number>:: Usually you cannot cherry-pick a merge because you do not know which side of the merge should be considered the mainline. This option specifies the parent number (starting from 1) of diff --git a/Documentation/git-clone.txt b/Documentation/git-clone.txt index 3fe3810f1c..984d194934 100644 --- a/Documentation/git-clone.txt +++ b/Documentation/git-clone.txt @@ -9,10 +9,10 @@ git-clone - Clone a repository into a new directory SYNOPSIS -------- [verse] -'git clone' [--template=<template_directory>] +'git clone' [--template=<template-directory>] [-l] [-s] [--no-hardlinks] [-q] [-n] [--bare] [--mirror] [-o <name>] [-b <name>] [-u <upload-pack>] [--reference <repository>] - [--dissociate] [--separate-git-dir <git dir>] + [--dissociate] [--separate-git-dir <git-dir>] [--depth <depth>] [--[no-]single-branch] [--no-tags] [--recurse-submodules[=<pathspec>]] [--[no-]shallow-submodules] [--[no-]remote-submodules] [--jobs <n>] [--sparse] [--[no-]reject-shallow] @@ -167,10 +167,10 @@ objects from the source repository into a pack in the cloned repository. configuration variables are created. --sparse:: - Initialize the sparse-checkout file so the working - directory starts with only the files in the root - of the repository. The sparse-checkout file can be - modified to grow the working directory as needed. + Employ a sparse-checkout, with only files in the toplevel + directory initially being present. The + linkgit:git-sparse-checkout[1] command can be used to grow the + working directory as needed. --filter=<filter-spec>:: Use the partial clone feature and request that the server sends @@ -211,7 +211,7 @@ objects from the source repository into a pack in the cloned repository. via ssh, this specifies a non-default path for the command run on the other end. ---template=<template_directory>:: +--template=<template-directory>:: Specify the directory from which templates will be used; (See the "TEMPLATE DIRECTORY" section of linkgit:git-init[1].) @@ -294,7 +294,7 @@ or `--mirror` is given) superproject's recorded SHA-1. Equivalent to passing `--remote` to `git submodule update`. ---separate-git-dir=<git dir>:: +--separate-git-dir=<git-dir>:: Instead of placing the cloned repository where it is supposed to be, place the cloned repository at the specified directory, then make a filesystem-agnostic Git symbolic link to there. diff --git a/Documentation/git-config.txt b/Documentation/git-config.txt index 992225f612..2285effb36 100644 --- a/Documentation/git-config.txt +++ b/Documentation/git-config.txt @@ -9,20 +9,20 @@ git-config - Get and set repository or global options SYNOPSIS -------- [verse] -'git config' [<file-option>] [--type=<type>] [--fixed-value] [--show-origin] [--show-scope] [-z|--null] name [value [value-pattern]] -'git config' [<file-option>] [--type=<type>] --add name value -'git config' [<file-option>] [--type=<type>] [--fixed-value] --replace-all name value [value-pattern] -'git config' [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] --get name [value-pattern] -'git config' [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] --get-all name [value-pattern] -'git config' [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] [--name-only] --get-regexp name_regex [value-pattern] -'git config' [<file-option>] [--type=<type>] [-z|--null] --get-urlmatch name URL -'git config' [<file-option>] [--fixed-value] --unset name [value-pattern] -'git config' [<file-option>] [--fixed-value] --unset-all name [value-pattern] -'git config' [<file-option>] --rename-section old_name new_name -'git config' [<file-option>] --remove-section name +'git config' [<file-option>] [--type=<type>] [--fixed-value] [--show-origin] [--show-scope] [-z|--null] <name> [<value> [<value-pattern>]] +'git config' [<file-option>] [--type=<type>] --add <name> <value> +'git config' [<file-option>] [--type=<type>] [--fixed-value] --replace-all <name> <value> [<value-pattern>] +'git config' [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] --get <name> [<value-pattern>] +'git config' [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] --get-all <name> [<value-pattern>] +'git config' [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] [--name-only] --get-regexp <name-regex> [<value-pattern>] +'git config' [<file-option>] [--type=<type>] [-z|--null] --get-urlmatch <name> <URL> +'git config' [<file-option>] [--fixed-value] --unset <name> [<value-pattern>] +'git config' [<file-option>] [--fixed-value] --unset-all <name> [<value-pattern>] +'git config' [<file-option>] --rename-section <old-name> <new-name> +'git config' [<file-option>] --remove-section <name> 'git config' [<file-option>] [--show-origin] [--show-scope] [-z|--null] [--name-only] -l | --list -'git config' [<file-option>] --get-color name [default] -'git config' [<file-option>] --get-colorbool name [stdout-is-tty] +'git config' [<file-option>] --get-color <name> [<default>] +'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>] 'git config' [<file-option>] -e | --edit DESCRIPTION @@ -102,9 +102,9 @@ OPTIONS in which section and variable names are lowercased, but subsection names are not. ---get-urlmatch name URL:: +--get-urlmatch <name> <URL>:: When given a two-part name section.key, the value for - section.<url>.key whose <url> part matches the best to the + section.<URL>.key whose <URL> part matches the best to the given URL is returned (if no such key exists, the value for section.key is used as a fallback). When given just the section as name, do so for all the keys in the section and @@ -145,8 +145,8 @@ See also <<FILES>>. read from or written to if `extensions.worktreeConfig` is present. If not it's the same as `--local`. --f config-file:: ---file config-file:: +-f <config-file>:: +--file <config-file>:: For writing options: write to the specified file rather than the repository `.git/config`. + @@ -155,7 +155,7 @@ available files. + See also <<FILES>>. ---blob blob:: +--blob <blob>:: Similar to `--file` but use the given blob instead of a file. E.g. you can use 'master:.gitmodules' to read values from the file '.gitmodules' in the master branch. See "SPECIFYING REVISIONS" @@ -246,18 +246,18 @@ Valid `<type>`'s include: all queried config options with the scope of that value (local, global, system, command). ---get-colorbool name [stdout-is-tty]:: +--get-colorbool <name> [<stdout-is-tty>]:: - Find the color setting for `name` (e.g. `color.diff`) and output - "true" or "false". `stdout-is-tty` should be either "true" or + Find the color setting for `<name>` (e.g. `color.diff`) and output + "true" or "false". `<stdout-is-tty>` should be either "true" or "false", and is taken into account when configuration says - "auto". If `stdout-is-tty` is missing, then checks the standard + "auto". If `<stdout-is-tty>` is missing, then checks the standard output of the command itself, and exits with status 0 if color is to be used, or exits with status 1 otherwise. When the color setting for `name` is undefined, the command uses `color.ui` as fallback. ---get-color name [default]:: +--get-color <name> [<default>]:: Find the color configured for `name` (e.g. `color.diff.new`) and output it as the ANSI color escape sequence to the standard diff --git a/Documentation/git-credential.txt b/Documentation/git-credential.txt index 206e3c5f40..f18673017f 100644 --- a/Documentation/git-credential.txt +++ b/Documentation/git-credential.txt @@ -8,7 +8,7 @@ git-credential - Retrieve and store user credentials SYNOPSIS -------- ------------------ -git credential <fill|approve|reject> +'git credential' (fill|approve|reject) ------------------ DESCRIPTION diff --git a/Documentation/git-cvsexportcommit.txt b/Documentation/git-cvsexportcommit.txt index 00154b6c85..41c8a8a05c 100644 --- a/Documentation/git-cvsexportcommit.txt +++ b/Documentation/git-cvsexportcommit.txt @@ -9,8 +9,8 @@ git-cvsexportcommit - Export a single commit to a CVS checkout SYNOPSIS -------- [verse] -'git cvsexportcommit' [-h] [-u] [-v] [-c] [-P] [-p] [-a] [-d cvsroot] - [-w cvsworkdir] [-W] [-f] [-m msgprefix] [PARENTCOMMIT] COMMITID +'git cvsexportcommit' [-h] [-u] [-v] [-c] [-P] [-p] [-a] [-d <cvsroot>] + [-w <cvs-workdir>] [-W] [-f] [-m <msgprefix>] [<parent-commit>] <commit-id> DESCRIPTION diff --git a/Documentation/git-cvsimport.txt b/Documentation/git-cvsimport.txt index de1ebed67d..b3f27671a0 100644 --- a/Documentation/git-cvsimport.txt +++ b/Documentation/git-cvsimport.txt @@ -11,9 +11,9 @@ SYNOPSIS [verse] 'git cvsimport' [-o <branch-for-HEAD>] [-h] [-v] [-d <CVSROOT>] [-A <author-conv-file>] [-p <options-for-cvsps>] [-P <file>] - [-C <git_repository>] [-z <fuzz>] [-i] [-k] [-u] [-s <subst>] - [-a] [-m] [-M <regex>] [-S <regex>] [-L <commitlimit>] - [-r <remote>] [-R] [<CVS_module>] + [-C <git-repository>] [-z <fuzz>] [-i] [-k] [-u] [-s <subst>] + [-a] [-m] [-M <regex>] [-S <regex>] [-L <commit-limit>] + [-r <remote>] [-R] [<CVS-module>] DESCRIPTION @@ -59,7 +59,7 @@ OPTIONS from `CVS/Root`. If no such file exists, it checks for the `CVSROOT` environment variable. -<CVS_module>:: +<CVS-module>:: The CVS module you want to import. Relative to <CVSROOT>. If not given, 'git cvsimport' tries to read it from `CVS/Repository`. diff --git a/Documentation/git-diff-files.txt b/Documentation/git-diff-files.txt index 906774f0f7..bf1febb9ae 100644 --- a/Documentation/git-diff-files.txt +++ b/Documentation/git-diff-files.txt @@ -9,7 +9,7 @@ git-diff-files - Compares files in the working tree and the index SYNOPSIS -------- [verse] -'git diff-files' [-q] [-0|-1|-2|-3|-c|--cc] [<common diff options>] [<path>...] +'git diff-files' [-q] [-0|-1|-2|-3|-c|--cc] [<common-diff-options>] [<path>...] DESCRIPTION ----------- diff --git a/Documentation/git-diff-index.txt b/Documentation/git-diff-index.txt index 27acb31cbf..679cae27d9 100644 --- a/Documentation/git-diff-index.txt +++ b/Documentation/git-diff-index.txt @@ -9,7 +9,7 @@ git-diff-index - Compare a tree to the working tree or index SYNOPSIS -------- [verse] -'git diff-index' [-m] [--cached] [--merge-base] [<common diff options>] <tree-ish> [<path>...] +'git diff-index' [-m] [--cached] [--merge-base] [<common-diff-options>] <tree-ish> [<path>...] DESCRIPTION ----------- diff --git a/Documentation/git-diff-tree.txt b/Documentation/git-diff-tree.txt index 2fc24c542f..274d5eaba9 100644 --- a/Documentation/git-diff-tree.txt +++ b/Documentation/git-diff-tree.txt @@ -11,7 +11,7 @@ SYNOPSIS [verse] 'git diff-tree' [--stdin] [-m] [-s] [-v] [--no-commit-id] [--pretty] [-t] [-r] [-c | --cc] [--combined-all-paths] [--root] [--merge-base] - [<common diff options>] <tree-ish> [<tree-ish>] [<path>...] + [<common-diff-options>] <tree-ish> [<tree-ish>] [<path>...] DESCRIPTION ----------- diff --git a/Documentation/git-fmt-merge-msg.txt b/Documentation/git-fmt-merge-msg.txt index 6793d8fc05..6f28812f38 100644 --- a/Documentation/git-fmt-merge-msg.txt +++ b/Documentation/git-fmt-merge-msg.txt @@ -9,7 +9,7 @@ git-fmt-merge-msg - Produce a merge commit message SYNOPSIS -------- [verse] -'git fmt-merge-msg' [-m <message>] [--log[=<n>] | --no-log] +'git fmt-merge-msg' [-m <message>] [--into-name <branch>] [--log[=<n>] | --no-log] 'git fmt-merge-msg' [-m <message>] [--log[=<n>] | --no-log] -F <file> DESCRIPTION @@ -44,6 +44,10 @@ OPTIONS Use <message> instead of the branch names for the first line of the log message. For use with `--log`. +--into-name <branch>:: + Prepare the merge message as if merging to the branch `<branch>`, + instead of the name of the real branch to which the merge is made. + -F <file>:: --file <file>:: Take the list of merged objects from <file> instead of diff --git a/Documentation/git-format-patch.txt b/Documentation/git-format-patch.txt index 113eabc107..be797d7a28 100644 --- a/Documentation/git-format-patch.txt +++ b/Documentation/git-format-patch.txt @@ -18,7 +18,7 @@ SYNOPSIS [-n | --numbered | -N | --no-numbered] [--start-number <n>] [--numbered-files] [--in-reply-to=<message id>] [--suffix=.<sfx>] - [--ignore-if-in-upstream] + [--ignore-if-in-upstream] [--always] [--cover-from-description=<mode>] [--rfc] [--subject-prefix=<subject prefix>] [(--reroll-count|-v) <n>] @@ -192,6 +192,10 @@ will want to ensure that threading is disabled for `git send-email`. patches being generated, and any patch that matches is ignored. +--always:: + Include patches for commits that do not introduce any change, + which are omitted by default. + --cover-from-description=<mode>:: Controls which parts of the cover letter will be automatically populated using the branch's description. diff --git a/Documentation/git-fsck.txt b/Documentation/git-fsck.txt index bd596619c0..5088783dcc 100644 --- a/Documentation/git-fsck.txt +++ b/Documentation/git-fsck.txt @@ -12,7 +12,7 @@ SYNOPSIS 'git fsck' [--tags] [--root] [--unreachable] [--cache] [--no-reflogs] [--[no-]full] [--strict] [--verbose] [--lost-found] [--[no-]dangling] [--[no-]progress] [--connectivity-only] - [--[no-]name-objects] [<object>*] + [--[no-]name-objects] [<object>...] DESCRIPTION ----------- diff --git a/Documentation/git-gui.txt b/Documentation/git-gui.txt index c9d7e96214..e8f3ccb433 100644 --- a/Documentation/git-gui.txt +++ b/Documentation/git-gui.txt @@ -8,7 +8,7 @@ git-gui - A portable graphical interface to Git SYNOPSIS -------- [verse] -'git gui' [<command>] [arguments] +'git gui' [<command>] [<arguments>] DESCRIPTION ----------- diff --git a/Documentation/git-help.txt b/Documentation/git-help.txt index 96d5f598b4..44ea63cc6d 100644 --- a/Documentation/git-help.txt +++ b/Documentation/git-help.txt @@ -9,14 +9,14 @@ SYNOPSIS -------- [verse] 'git help' [-a|--all [--[no-]verbose]] - [[-i|--info] [-m|--man] [-w|--web]] [COMMAND|GUIDE] + [[-i|--info] [-m|--man] [-w|--web]] [<command>|<guide>] 'git help' [-g|--guides] 'git help' [-c|--config] DESCRIPTION ----------- -With no options and no COMMAND or GUIDE given, the synopsis of the 'git' +With no options and no '<command>' or '<guide>' given, the synopsis of the 'git' command and a list of the most commonly used Git commands are printed on the standard output. @@ -33,7 +33,7 @@ variables. If an alias is given, git shows the definition of the alias on standard output. To get the manual page for the aliased command, use -`git COMMAND --help`. +`git <command> --help`. Note that `git --help ...` is identical to `git help ...` because the former is internally converted into the latter. diff --git a/Documentation/git-http-fetch.txt b/Documentation/git-http-fetch.txt index 9fa17b60e4..319062c021 100644 --- a/Documentation/git-http-fetch.txt +++ b/Documentation/git-http-fetch.txt @@ -9,7 +9,7 @@ git-http-fetch - Download from a remote Git repository via HTTP SYNOPSIS -------- [verse] -'git http-fetch' [-c] [-t] [-a] [-d] [-v] [-w filename] [--recover] [--stdin | --packfile=<hash> | <commit>] <url> +'git http-fetch' [-c] [-t] [-a] [-d] [-v] [-w <filename>] [--recover] [--stdin | --packfile=<hash> | <commit>] <URL> DESCRIPTION ----------- diff --git a/Documentation/git-http-push.txt b/Documentation/git-http-push.txt index ea03a4eeb0..7c6a6dd7f6 100644 --- a/Documentation/git-http-push.txt +++ b/Documentation/git-http-push.txt @@ -9,7 +9,7 @@ git-http-push - Push objects over HTTP/DAV to another repository SYNOPSIS -------- [verse] -'git http-push' [--all] [--dry-run] [--force] [--verbose] <url> <ref> [<ref>...] +'git http-push' [--all] [--dry-run] [--force] [--verbose] <URL> <ref> [<ref>...] DESCRIPTION ----------- @@ -63,16 +63,15 @@ of such patterns separated by a colon ":" (this means that a ref name cannot have a colon in it). A single pattern '<name>' is just a shorthand for '<name>:<name>'. -Each pattern pair consists of the source side (before the colon) -and the destination side (after the colon). The ref to be -pushed is determined by finding a match that matches the source -side, and where it is pushed is determined by using the -destination side. +Each pattern pair '<src>:<dst>' consists of the source side (before +the colon) and the destination side (after the colon). The ref to be +pushed is determined by finding a match that matches the source side, +and where it is pushed is determined by using the destination side. - - It is an error if <src> does not match exactly one of the + - It is an error if '<src>' does not match exactly one of the local refs. - - If <dst> does not match any remote ref, either + - If '<dst>' does not match any remote ref, either * it has to start with "refs/"; <dst> is used as the destination literally in this case. diff --git a/Documentation/git-init-db.txt b/Documentation/git-init-db.txt index 648a6cd78a..18bf1a3c8c 100644 --- a/Documentation/git-init-db.txt +++ b/Documentation/git-init-db.txt @@ -9,7 +9,7 @@ git-init-db - Creates an empty Git repository SYNOPSIS -------- [verse] -'git init-db' [-q | --quiet] [--bare] [--template=<template_directory>] [--separate-git-dir <git dir>] [--shared[=<permissions>]] +'git init-db' [-q | --quiet] [--bare] [--template=<template-directory>] [--separate-git-dir <git-dir>] [--shared[=<permissions>]] DESCRIPTION diff --git a/Documentation/git-init.txt b/Documentation/git-init.txt index b611d80697..ad921fe782 100644 --- a/Documentation/git-init.txt +++ b/Documentation/git-init.txt @@ -9,10 +9,10 @@ git-init - Create an empty Git repository or reinitialize an existing one SYNOPSIS -------- [verse] -'git init' [-q | --quiet] [--bare] [--template=<template_directory>] - [--separate-git-dir <git dir>] [--object-format=<format>] +'git init' [-q | --quiet] [--bare] [--template=<template-directory>] + [--separate-git-dir <git-dir>] [--object-format=<format>] [-b <branch-name> | --initial-branch=<branch-name>] - [--shared[=<permissions>]] [directory] + [--shared[=<permissions>]] [<directory>] DESCRIPTION @@ -57,12 +57,12 @@ values are 'sha1' and (if enabled) 'sha256'. 'sha1' is the default. + include::object-format-disclaimer.txt[] ---template=<template_directory>:: +--template=<template-directory>:: Specify the directory from which templates will be used. (See the "TEMPLATE DIRECTORY" section below.) ---separate-git-dir=<git dir>:: +--separate-git-dir=<git-dir>:: Instead of initializing the repository as a directory to either `$GIT_DIR` or `./.git/`, create a text file there containing the path to the actual @@ -79,7 +79,7 @@ repository. If not specified, fall back to the default name (currently `master`, but this is subject to change in the future; the name can be customized via the `init.defaultBranch` configuration variable). ---shared[=(false|true|umask|group|all|world|everybody|0xxx)]:: +--shared[=(false|true|umask|group|all|world|everybody|<perm>)]:: Specify that the Git repository is to be shared amongst several users. This allows users belonging to the same group to push into that @@ -110,13 +110,16 @@ the repository permissions. Same as 'group', but make the repository readable by all users. -'0xxx':: +'<perm>':: -'0xxx' is an octal number and each file will have mode '0xxx'. '0xxx' will -override users' umask(2) value (and not only loosen permissions as 'group' and -'all' does). '0640' will create a repository which is group-readable, but not -group-writable or accessible to others. '0660' will create a repo that is -readable and writable to the current user and group, but inaccessible to others. +'<perm>' is a 3-digit octal number prefixed with `0` and each file +will have mode '<perm>'. '<perm>' will override users' umask(2) +value (and not only loosen permissions as 'group' and 'all' +does). '0640' will create a repository which is group-readable, but +not group-writable or accessible to others. '0660' will create a repo +that is readable and writable to the current user and group, but +inaccessible to others (directories and executable files get their +`x` bit from the `r` bit for corresponding classes of users). -- By default, the configuration flag `receive.denyNonFastForwards` is enabled diff --git a/Documentation/git-log.txt b/Documentation/git-log.txt index 0498e7bacb..20e87cecf4 100644 --- a/Documentation/git-log.txt +++ b/Documentation/git-log.txt @@ -9,7 +9,7 @@ git-log - Show commit logs SYNOPSIS -------- [verse] -'git log' [<options>] [<revision range>] [[--] <path>...] +'git log' [<options>] [<revision-range>] [[--] <path>...] DESCRIPTION ----------- @@ -81,13 +81,13 @@ produced by `--stat`, etc. include::line-range-options.txt[] -<revision range>:: +<revision-range>:: Show only commits in the specified revision range. When no - <revision range> is specified, it defaults to `HEAD` (i.e. the + <revision-range> is specified, it defaults to `HEAD` (i.e. the whole history leading to the current commit). `origin..HEAD` specifies all the commits reachable from the current commit (i.e. `HEAD`), but not from `origin`. For a complete list of - ways to spell <revision range>, see the 'Specifying Ranges' + ways to spell <revision-range>, see the 'Specifying Ranges' section of linkgit:gitrevisions[7]. [--] <path>...:: diff --git a/Documentation/git-ls-files.txt b/Documentation/git-ls-files.txt index 6d11ab506b..48cc7c0b6f 100644 --- a/Documentation/git-ls-files.txt +++ b/Documentation/git-ls-files.txt @@ -10,9 +10,9 @@ SYNOPSIS -------- [verse] 'git ls-files' [-z] [-t] [-v] [-f] - (--[cached|deleted|others|ignored|stage|unmerged|killed|modified])* - (-[c|d|o|i|s|u|k|m])* - [--eol] + [-c|--cached] [-d|--deleted] [-o|--others] [-i|--|ignored] + [-s|--stage] [-u|--unmerged] [-k|--|killed] [-m|--modified] + [--directory [--no-empty-directory]] [--eol] [--deduplicate] [-x <pattern>|--exclude=<pattern>] [-X <file>|--exclude-from=<file>] @@ -187,6 +187,11 @@ Both the <eolinfo> in the index ("i/<eolinfo>") and in the working tree ("w/<eolinfo>") are shown for regular files, followed by the ("attr/<eolattr>"). +--sparse:: + If the index is sparse, show the sparse directories without expanding + to the contained files. Sparse directories will be shown with a + trailing slash, such as "x/" for a sparse directory "x". + \--:: Do not interpret any more arguments as options. diff --git a/Documentation/git-merge-file.txt b/Documentation/git-merge-file.txt index f856032613..7e9093fab6 100644 --- a/Documentation/git-merge-file.txt +++ b/Documentation/git-merge-file.txt @@ -70,6 +70,9 @@ OPTIONS --diff3:: Show conflicts in "diff3" style. +--zdiff3:: + Show conflicts in "zdiff3" style. + --ours:: --theirs:: --union:: diff --git a/Documentation/git-merge-index.txt b/Documentation/git-merge-index.txt index 2ab84a91e5..eea56b3154 100644 --- a/Documentation/git-merge-index.txt +++ b/Documentation/git-merge-index.txt @@ -9,7 +9,7 @@ git-merge-index - Run a merge for files needing merging SYNOPSIS -------- [verse] -'git merge-index' [-o] [-q] <merge-program> (-a | [--] <file>*) +'git merge-index' [-o] [-q] <merge-program> (-a | ( [--] <file>...) ) DESCRIPTION ----------- diff --git a/Documentation/git-merge.txt b/Documentation/git-merge.txt index e4f3352eb5..3125473cc1 100644 --- a/Documentation/git-merge.txt +++ b/Documentation/git-merge.txt @@ -12,7 +12,8 @@ SYNOPSIS 'git merge' [-n] [--stat] [--no-commit] [--squash] [--[no-]edit] [--no-verify] [-s <strategy>] [-X <strategy-option>] [-S[<keyid>]] [--[no-]allow-unrelated-histories] - [--[no-]rerere-autoupdate] [-m <msg>] [-F <file>] [<commit>...] + [--[no-]rerere-autoupdate] [-m <msg>] [-F <file>] + [--into-name <branch>] [<commit>...] 'git merge' (--continue | --abort | --quit) DESCRIPTION @@ -76,6 +77,11 @@ The 'git fmt-merge-msg' command can be used to give a good default for automated 'git merge' invocations. The automated message can include the branch description. +--into-name <branch>:: + Prepare the default merge message as if merging to the branch + `<branch>`, instead of the name of the real branch to which + the merge is made. + -F <file>:: --file=<file>:: Read the commit message to be used for the merge commit (in @@ -240,7 +246,8 @@ from the RCS suite to present such a conflicted hunk, like this: ------------ Here are lines that are either unchanged from the common -ancestor, or cleanly resolved because only one side changed. +ancestor, or cleanly resolved because only one side changed, +or cleanly resolved because both sides changed the same way. <<<<<<< yours:sample.txt Conflict resolution is hard; let's go shopping. @@ -261,16 +268,37 @@ side wants to say it is hard and you'd prefer to go shopping, while the other side wants to claim it is easy. An alternative style can be used by setting the "merge.conflictStyle" -configuration variable to "diff3". In "diff3" style, the above conflict -may look like this: +configuration variable to either "diff3" or "zdiff3". In "diff3" +style, the above conflict may look like this: + +------------ +Here are lines that are either unchanged from the common +ancestor, or cleanly resolved because only one side changed, +<<<<<<< yours:sample.txt +or cleanly resolved because both sides changed the same way. +Conflict resolution is hard; +let's go shopping. +||||||| base:sample.txt +or cleanly resolved because both sides changed identically. +Conflict resolution is hard. +======= +or cleanly resolved because both sides changed the same way. +Git makes conflict resolution easy. +>>>>>>> theirs:sample.txt +And here is another line that is cleanly resolved or unmodified. +------------ + +while in "zdiff3" style, it may look like this: ------------ Here are lines that are either unchanged from the common -ancestor, or cleanly resolved because only one side changed. +ancestor, or cleanly resolved because only one side changed, +or cleanly resolved because both sides changed the same way. <<<<<<< yours:sample.txt Conflict resolution is hard; let's go shopping. -||||||| +||||||| base:sample.txt +or cleanly resolved because both sides changed identically. Conflict resolution is hard. ======= Git makes conflict resolution easy. diff --git a/Documentation/git-p4.txt b/Documentation/git-p4.txt index 38e5257b2a..e21fcd8f71 100644 --- a/Documentation/git-p4.txt +++ b/Documentation/git-p4.txt @@ -9,10 +9,10 @@ git-p4 - Import from and submit to Perforce repositories SYNOPSIS -------- [verse] -'git p4 clone' [<sync options>] [<clone options>] <p4 depot path>... -'git p4 sync' [<sync options>] [<p4 depot path>...] +'git p4 clone' [<sync-options>] [<clone-options>] <p4-depot-path>... +'git p4 sync' [<sync-options>] [<p4-depot-path>...] 'git p4 rebase' -'git p4 submit' [<submit options>] [<master branch name>] +'git p4 submit' [<submit-options>] [<master-branch-name>] DESCRIPTION @@ -361,7 +361,7 @@ These options can be used to modify 'git p4 submit' behavior. p4/master. See the "Sync options" section above for more information. ---commit <sha1>|<sha1..sha1>:: +--commit (<sha1>|<sha1>..<sha1>):: Submit only the specified commit or range of commits, instead of the full list of changes that are in the current Git branch. diff --git a/Documentation/git-pack-objects.txt b/Documentation/git-pack-objects.txt index dbfd1f9017..f8344e1e5b 100644 --- a/Documentation/git-pack-objects.txt +++ b/Documentation/git-pack-objects.txt @@ -13,8 +13,8 @@ SYNOPSIS [--no-reuse-delta] [--delta-base-offset] [--non-empty] [--local] [--incremental] [--window=<n>] [--depth=<n>] [--revs [--unpacked | --all]] [--keep-pack=<pack-name>] - [--stdout [--filter=<filter-spec>] | base-name] - [--shallow] [--keep-true-parents] [--[no-]sparse] < object-list + [--stdout [--filter=<filter-spec>] | <base-name>] + [--shallow] [--keep-true-parents] [--[no-]sparse] < <object-list> DESCRIPTION diff --git a/Documentation/git-pack-redundant.txt b/Documentation/git-pack-redundant.txt index f2869da572..ee7034b5e5 100644 --- a/Documentation/git-pack-redundant.txt +++ b/Documentation/git-pack-redundant.txt @@ -9,7 +9,7 @@ git-pack-redundant - Find redundant pack files SYNOPSIS -------- [verse] -'git pack-redundant' [ --verbose ] [ --alt-odb ] < --all | .pack filename ... > +'git pack-redundant' [ --verbose ] [ --alt-odb ] ( --all | <pack-filename>... ) DESCRIPTION ----------- diff --git a/Documentation/git-rebase.txt b/Documentation/git-rebase.txt index a1af21fcef..9da4647061 100644 --- a/Documentation/git-rebase.txt +++ b/Documentation/git-rebase.txt @@ -714,9 +714,9 @@ information about the rebased commits and their parents (and instead generates new fake commits based off limited information in the generated patches), those commits cannot be identified; instead it has to fall back to a commit summary. Also, when merge.conflictStyle is -set to diff3, the apply backend will use "constructed merge base" to -label the content from the merge base, and thus provide no information -about the merge base commit whatsoever. +set to diff3 or zdiff3, the apply backend will use "constructed merge +base" to label the content from the merge base, and thus provide no +information about the merge base commit whatsoever. The merge backend works with the full commits on both sides of history and thus has no such limitations. diff --git a/Documentation/git-reflog.txt b/Documentation/git-reflog.txt index ff487ff77d..5ced7ad4f8 100644 --- a/Documentation/git-reflog.txt +++ b/Documentation/git-reflog.txt @@ -17,12 +17,12 @@ The command takes various subcommands, and different options depending on the subcommand: [verse] -'git reflog' ['show'] [log-options] [<ref>] +'git reflog' ['show'] [<log-options>] [<ref>] 'git reflog expire' [--expire=<time>] [--expire-unreachable=<time>] [--rewrite] [--updateref] [--stale-fix] [--dry-run | -n] [--verbose] [--all [--single-worktree] | <refs>...] 'git reflog delete' [--rewrite] [--updateref] - [--dry-run | -n] [--verbose] ref@\{specifier\}... + [--dry-run | -n] [--verbose] <ref>@\{<specifier>\}... 'git reflog exists' <ref> Reference logs, or "reflogs", record when the tips of branches and diff --git a/Documentation/git-remote.txt b/Documentation/git-remote.txt index 31c29c9b31..2bebc32566 100644 --- a/Documentation/git-remote.txt +++ b/Documentation/git-remote.txt @@ -10,7 +10,7 @@ SYNOPSIS -------- [verse] 'git remote' [-v | --verbose] -'git remote add' [-t <branch>] [-m <master>] [-f] [--[no-]tags] [--mirror=(fetch|push)] <name> <url> +'git remote add' [-t <branch>] [-m <master>] [-f] [--[no-]tags] [--mirror=(fetch|push)] <name> <URL> 'git remote rename' <old> <new> 'git remote remove' <name> 'git remote set-head' <name> (-a | --auto | -d | --delete | <branch>) @@ -18,7 +18,7 @@ SYNOPSIS 'git remote get-url' [--push] [--all] <name> 'git remote set-url' [--push] <name> <newurl> [<oldurl>] 'git remote set-url --add' [--push] <name> <newurl> -'git remote set-url --delete' [--push] <name> <url> +'git remote set-url --delete' [--push] <name> <URL> 'git remote' [-v | --verbose] 'show' [-n] <name>... 'git remote prune' [-n | --dry-run] <name>... 'git remote' [-v | --verbose] 'update' [-p | --prune] [(<group> | <remote>)...] @@ -47,7 +47,7 @@ subcommands are available to perform operations on the remotes. 'add':: Add a remote named <name> for the repository at -<url>. The command `git fetch <name>` can then be used to create and +<URL>. The command `git fetch <name>` can then be used to create and update remote-tracking branches <name>/<branch>. + With `-f` option, `git fetch <name>` is run immediately after @@ -152,7 +152,7 @@ With `--push`, push URLs are manipulated instead of fetch URLs. With `--add`, instead of changing existing URLs, new URL is added. + With `--delete`, instead of changing existing URLs, all URLs matching -regex <url> are deleted for remote <name>. Trying to delete all +regex <URL> are deleted for remote <name>. Trying to delete all non-push URLs is an error. + Note that the push URL and the fetch URL, even though they can diff --git a/Documentation/git-repack.txt b/Documentation/git-repack.txt index 7183fb498f..ee30edc178 100644 --- a/Documentation/git-repack.txt +++ b/Documentation/git-repack.txt @@ -76,8 +76,9 @@ to the new separate pack will be written. linkgit:git-pack-objects[1]. -q:: - Pass the `-q` option to 'git pack-objects'. See - linkgit:git-pack-objects[1]. +--quiet:: + Show no progress over the standard error stream and pass the `-q` + option to 'git pack-objects'. See linkgit:git-pack-objects[1]. -n:: Do not update the server information with diff --git a/Documentation/git-request-pull.txt b/Documentation/git-request-pull.txt index 4d4392d0f8..fa5a426709 100644 --- a/Documentation/git-request-pull.txt +++ b/Documentation/git-request-pull.txt @@ -8,7 +8,7 @@ git-request-pull - Generates a summary of pending changes SYNOPSIS -------- [verse] -'git request-pull' [-p] <start> <url> [<end>] +'git request-pull' [-p] <start> <URL> [<end>] DESCRIPTION ----------- @@ -21,7 +21,7 @@ the changes and indicates from where they can be pulled. The upstream project is expected to have the commit named by `<start>` and the output asks it to integrate the changes you made since that commit, up to the commit named by `<end>`, by visiting -the repository named by `<url>`. +the repository named by `<URL>`. OPTIONS @@ -33,14 +33,14 @@ OPTIONS Commit to start at. This names a commit that is already in the upstream history. -<url>:: +<URL>:: The repository URL to be pulled from. <end>:: Commit to end at (defaults to HEAD). This names the commit at the tip of the history you are asking to be pulled. + -When the repository named by `<url>` has the commit at a tip of a +When the repository named by `<URL>` has the commit at a tip of a ref that is different from the ref you have locally, you can use the `<local>:<remote>` syntax, to have its local name, a colon `:`, and its remote name. diff --git a/Documentation/git-restore.txt b/Documentation/git-restore.txt index 55bde91ef9..5964810caa 100644 --- a/Documentation/git-restore.txt +++ b/Documentation/git-restore.txt @@ -92,8 +92,7 @@ in linkgit:git-checkout[1] for details. The same as `--merge` option above, but changes the way the conflicting hunks are presented, overriding the `merge.conflictStyle` configuration variable. Possible values - are "merge" (default) and "diff3" (in addition to what is - shown by "merge" style, shows the original contents). + are "merge" (default), "diff3", and "zdiff3". --ignore-unmerged:: When restoring files on the working tree from the index, do diff --git a/Documentation/git-send-email.txt b/Documentation/git-send-email.txt index 3db4eab4ba..41cd8cb424 100644 --- a/Documentation/git-send-email.txt +++ b/Documentation/git-send-email.txt @@ -9,7 +9,8 @@ git-send-email - Send a collection of patches as emails SYNOPSIS -------- [verse] -'git send-email' [<options>] <file|directory|rev-list options>... +'git send-email' [<options>] <file|directory>... +'git send-email' [<options>] <format-patch options> 'git send-email' --dump-aliases @@ -19,7 +20,8 @@ Takes the patches given on the command line and emails them out. Patches can be specified as files, directories (which will send all files in the directory), or directly as a revision list. In the last case, any format accepted by linkgit:git-format-patch[1] can -be passed to git send-email. +be passed to git send-email, as well as options understood by +linkgit:git-format-patch[1]. The header of the email is configurable via command-line options. If not specified on the command line, the user will be prompted with a ReadLine diff --git a/Documentation/git-shortlog.txt b/Documentation/git-shortlog.txt index c9c7f3065c..f64e77047b 100644 --- a/Documentation/git-shortlog.txt +++ b/Documentation/git-shortlog.txt @@ -8,7 +8,7 @@ git-shortlog - Summarize 'git log' output SYNOPSIS -------- [verse] -'git shortlog' [<options>] [<revision range>] [[--] <path>...] +'git shortlog' [<options>] [<revision-range>] [[--] <path>...] git log --pretty=short | 'git shortlog' [<options>] DESCRIPTION @@ -89,13 +89,13 @@ counts both authors and co-authors. If width is `0` (zero) then indent the lines of the output without wrapping them. -<revision range>:: +<revision-range>:: Show only commits in the specified revision range. When no - <revision range> is specified, it defaults to `HEAD` (i.e. the + <revision-range> is specified, it defaults to `HEAD` (i.e. the whole history leading to the current commit). `origin..HEAD` specifies all the commits reachable from the current commit (i.e. `HEAD`), but not from `origin`. For a complete list of - ways to spell <revision range>, see the "Specifying Ranges" + ways to spell <revision-range>, see the "Specifying Ranges" section of linkgit:gitrevisions[7]. [--] <path>...:: diff --git a/Documentation/git-sparse-checkout.txt b/Documentation/git-sparse-checkout.txt index 42056ee9ff..b81dbe0654 100644 --- a/Documentation/git-sparse-checkout.txt +++ b/Documentation/git-sparse-checkout.txt @@ -11,7 +11,7 @@ given by a list of patterns. SYNOPSIS -------- [verse] -'git sparse-checkout <subcommand> [options]' +'git sparse-checkout <subcommand> [<options>]' DESCRIPTION @@ -30,28 +30,36 @@ COMMANDS 'list':: Describe the patterns in the sparse-checkout file. -'init':: - Enable the `core.sparseCheckout` setting. If the - sparse-checkout file does not exist, then populate it with - patterns that match every file in the root directory and - no other directories, then will remove all directories tracked - by Git. Add patterns to the sparse-checkout file to - repopulate the working directory. +'set':: + Enable the necessary config settings + (extensions.worktreeConfig, core.sparseCheckout, + core.sparseCheckoutCone) if they are not already enabled, and + write a set of patterns to the sparse-checkout file from the + list of arguments following the 'set' subcommand. Update the + working directory to match the new patterns. + -To avoid interfering with other worktrees, it first enables the -`extensions.worktreeConfig` setting and makes sure to set the -`core.sparseCheckout` setting in the worktree-specific config file. +When the `--stdin` option is provided, the patterns are read from +standard in as a newline-delimited list instead of from the arguments. + -When `--cone` is provided, the `core.sparseCheckoutCone` setting is -also set, allowing for better performance with a limited set of -patterns (see 'CONE PATTERN SET' below). +When `--cone` is passed or `core.sparseCheckoutCone` is enabled, the +input list is considered a list of directories instead of +sparse-checkout patterns. This allows for better performance with a +limited set of patterns (see 'CONE PATTERN SET' below). Note that the +set command will write patterns to the sparse-checkout file to include +all files contained in those directories (recursively) as well as +files that are siblings of ancestor directories. The input format +matches the output of `git ls-tree --name-only`. This includes +interpreting pathnames that begin with a double quote (") as C-style +quoted strings. This may become the default in the future; --no-cone +can be passed to request non-cone mode. + -Use the `--[no-]sparse-index` option to toggle the use of the sparse -index format. This reduces the size of the index to be more closely -aligned with your sparse-checkout definition. This can have significant -performance advantages for commands such as `git status` or `git add`. -This feature is still experimental. Some commands might be slower with -a sparse index until they are properly integrated with the feature. +Use the `--[no-]sparse-index` option to use a sparse index (the +default is to not use it). A sparse index reduces the size of the +index to be more closely aligned with your sparse-checkout +definition. This can have significant performance advantages for +commands such as `git status` or `git add`. This feature is still +experimental. Some commands might be slower with a sparse index until +they are properly integrated with the feature. + **WARNING:** Using a sparse index requires modifying the index in a way that is not completely understood by external tools. If you have trouble @@ -60,23 +68,6 @@ to rewrite your index to not be sparse. Older versions of Git will not understand the sparse directory entries index extension and may fail to interact with your repository until it is disabled. -'set':: - Write a set of patterns to the sparse-checkout file, as given as - a list of arguments following the 'set' subcommand. Update the - working directory to match the new patterns. Enable the - core.sparseCheckout config setting if it is not already enabled. -+ -When the `--stdin` option is provided, the patterns are read from -standard in as a newline-delimited list instead of from the arguments. -+ -When `core.sparseCheckoutCone` is enabled, the input list is considered a -list of directories instead of sparse-checkout patterns. The command writes -patterns to the sparse-checkout file to include all files contained in those -directories (recursively) as well as files that are siblings of ancestor -directories. The input format matches the output of `git ls-tree --name-only`. -This includes interpreting pathnames that begin with a double quote (") as -C-style quoted strings. - 'add':: Update the sparse-checkout file to include additional patterns. By default, these patterns are read from the command-line arguments, @@ -93,12 +84,35 @@ C-style quoted strings. cases, it can make sense to run `git sparse-checkout reapply` later after cleaning up affected paths (e.g. resolving conflicts, undoing or committing changes, etc.). ++ +The `reapply` command can also take `--[no-]cone` and `--[no-]sparse-index` +flags, with the same meaning as the flags from the `set` command, in order +to change which sparsity mode you are using without needing to also respecify +all sparsity paths. 'disable':: Disable the `core.sparseCheckout` config setting, and restore the - working directory to include all files. Leaves the sparse-checkout - file intact so a later 'git sparse-checkout init' command may - return the working directory to the same state. + working directory to include all files. + +'init':: + Deprecated command that behaves like `set` with no specified paths. + May be removed in the future. ++ +Historically, `set` did not handle all the necessary config settings, +which meant that both `init` and `set` had to be called. Invoking +both meant the `init` step would first remove nearly all tracked files +(and in cone mode, ignored files too), then the `set` step would add +many of the tracked files (but not ignored files) back. In addition +to the lost files, the performance and UI of this combination was +poor. ++ +Also, historically, `init` would not actually initialize the +sparse-checkout file if it already existed. This meant it was +possible to return to a sparse-checkout without remembering which +paths to pass to a subsequent 'set' or 'add' command. However, +`--cone` and `--sparse-index` options would not be remembered across +the disable command, so the easy restore of calling a plain `init` +decreased in utility. SPARSE CHECKOUT --------------- @@ -107,7 +121,7 @@ SPARSE CHECKOUT It uses the skip-worktree bit (see linkgit:git-update-index[1]) to tell Git whether a file in the working directory is worth looking at. If the skip-worktree bit is set, then the file is ignored in the working -directory. Git will not populate the contents of those files, which +directory. Git will avoid populating the contents of those files, which makes a sparse checkout helpful when working in a repository with many files, but only a few are important to the current user. @@ -117,10 +131,8 @@ directory, it updates the skip-worktree bits in the index based on this file. The files matching the patterns in the file will appear in the working directory, and the rest will not. -To enable the sparse-checkout feature, run `git sparse-checkout init` to -initialize a simple sparse-checkout file and enable the `core.sparseCheckout` -config setting. Then, run `git sparse-checkout set` to modify the patterns in -the sparse-checkout file. +To enable the sparse-checkout feature, run `git sparse-checkout set` to +set the patterns you want to use. To repopulate the working directory with all files, use the `git sparse-checkout disable` command. diff --git a/Documentation/git-stage.txt b/Documentation/git-stage.txt index 25bcda936d..2f6aaa75b9 100644 --- a/Documentation/git-stage.txt +++ b/Documentation/git-stage.txt @@ -9,7 +9,7 @@ git-stage - Add file contents to the staging area SYNOPSIS -------- [verse] -'git stage' args... +'git stage' <arg>... DESCRIPTION diff --git a/Documentation/git-stash.txt b/Documentation/git-stash.txt index be6084ccef..6e15f47525 100644 --- a/Documentation/git-stash.txt +++ b/Documentation/git-stash.txt @@ -13,7 +13,7 @@ SYNOPSIS 'git stash' drop [-q|--quiet] [<stash>] 'git stash' ( pop | apply ) [--index] [-q|--quiet] [<stash>] 'git stash' branch <branchname> [<stash>] -'git stash' [push [-p|--patch] [-k|--[no-]keep-index] [-q|--quiet] +'git stash' [push [-p|--patch] [-S|--staged] [-k|--[no-]keep-index] [-q|--quiet] [-u|--include-untracked] [-a|--all] [-m|--message <message>] [--pathspec-from-file=<file> [--pathspec-file-nul]] [--] [<pathspec>...]] @@ -47,7 +47,7 @@ stash index (e.g. the integer `n` is equivalent to `stash@{n}`). COMMANDS -------- -push [-p|--patch] [-k|--[no-]keep-index] [-u|--include-untracked] [-a|--all] [-q|--quiet] [-m|--message <message>] [--pathspec-from-file=<file> [--pathspec-file-nul]] [--] [<pathspec>...]:: +push [-p|--patch] [-S|--staged] [-k|--[no-]keep-index] [-u|--include-untracked] [-a|--all] [-q|--quiet] [-m|--message <message>] [--pathspec-from-file=<file> [--pathspec-file-nul]] [--] [<pathspec>...]:: Save your local modifications to a new 'stash entry' and roll them back to HEAD (in the working tree and in the index). @@ -60,7 +60,7 @@ subcommand from making an unwanted stash entry. The two exceptions to this are `stash -p` which acts as alias for `stash push -p` and pathspec elements, which are allowed after a double hyphen `--` for disambiguation. -save [-p|--patch] [-k|--[no-]keep-index] [-u|--include-untracked] [-a|--all] [-q|--quiet] [<message>]:: +save [-p|--patch] [-S|--staged] [-k|--[no-]keep-index] [-u|--include-untracked] [-a|--all] [-q|--quiet] [<message>]:: This option is deprecated in favour of 'git stash push'. It differs from "stash push" in that it cannot take pathspec. @@ -205,6 +205,16 @@ to learn how to operate the `--patch` mode. The `--patch` option implies `--keep-index`. You can use `--no-keep-index` to override this. +-S:: +--staged:: + This option is only valid for `push` and `save` commands. ++ +Stash only the changes that are currently staged. This is similar to +basic `git commit` except the state is committed to the stash instead +of current branch. ++ +The `--patch` option has priority over this one. + --pathspec-from-file=<file>:: This option is only valid for `push` command. + @@ -341,6 +351,24 @@ $ edit/build/test remaining parts $ git commit foo -m 'Remaining parts' ---------------------------------------------------------------- +Saving unrelated changes for future use:: + +When you are in the middle of massive changes and you find some +unrelated issue that you don't want to forget to fix, you can do the +change(s), stage them, and use `git stash push --staged` to stash them +out for future use. This is similar to committing the staged changes, +only the commit ends-up being in the stash and not on the current branch. ++ +---------------------------------------------------------------- +# ... hack hack hack ... +$ git add --patch foo # add unrelated changes to the index +$ git stash push --staged # save these changes to the stash +# ... hack hack hack, finish curent changes ... +$ git commit -m 'Massive' # commit fully tested changes +$ git switch fixup-branch # switch to another branch +$ git stash pop # to finish work on the saved changes +---------------------------------------------------------------- + Recovering stash entries that were cleared/dropped erroneously:: If you mistakenly drop or clear stash entries, they cannot be recovered diff --git a/Documentation/git-status.txt b/Documentation/git-status.txt index 4a2c3e0408..54a4b29b47 100644 --- a/Documentation/git-status.txt +++ b/Documentation/git-status.txt @@ -314,6 +314,14 @@ Line Notes ------------------------------------------------------------ .... +Stash Information +^^^^^^^^^^^^^^^^^ + +If `--show-stash` is given, one line is printed showing the number of stash +entries if non-zero: + + # stash <N> + Changed Tracked Entries ^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/Documentation/git-svn.txt b/Documentation/git-svn.txt index 222b556d7a..4e92308e85 100644 --- a/Documentation/git-svn.txt +++ b/Documentation/git-svn.txt @@ -575,7 +575,7 @@ OPTIONS ------- --shared[=(false|true|umask|group|all|world|everybody)]:: ---template=<template_directory>:: +--template=<template-directory>:: Only used with the 'init' command. These are passed directly to 'git init'. diff --git a/Documentation/git-switch.txt b/Documentation/git-switch.txt index 5c438cd505..5c90f76fbe 100644 --- a/Documentation/git-switch.txt +++ b/Documentation/git-switch.txt @@ -137,8 +137,7 @@ should result in deletion of the path). The same as `--merge` option above, but changes the way the conflicting hunks are presented, overriding the `merge.conflictStyle` configuration variable. Possible values are - "merge" (default) and "diff3" (in addition to what is shown by - "merge" style, shows the original contents). + "merge" (default), "diff3", and "zdiff3". -q:: --quiet:: diff --git a/Documentation/git-var.txt b/Documentation/git-var.txt index 6072f936ab..387cc1b914 100644 --- a/Documentation/git-var.txt +++ b/Documentation/git-var.txt @@ -59,6 +59,9 @@ ifdef::git-default-pager[] The build you are using chose '{git-default-pager}' as the default. endif::git-default-pager[] +GIT_DEFAULT_BRANCH:: + The name of the first branch created in newly initialized repositories. + SEE ALSO -------- linkgit:git-commit-tree[1] diff --git a/Documentation/git-web--browse.txt b/Documentation/git-web--browse.txt index 8d162b56c5..f2f996cbe1 100644 --- a/Documentation/git-web--browse.txt +++ b/Documentation/git-web--browse.txt @@ -8,7 +8,7 @@ git-web--browse - Git helper script to launch a web browser SYNOPSIS -------- [verse] -'git web{litdd}browse' [<options>] <url|file>... +'git web{litdd}browse' [<options>] (<URL>|<file>)... DESCRIPTION ----------- diff --git a/Documentation/git-worktree.txt b/Documentation/git-worktree.txt index 8a7cbdd19c..9e862fbcf7 100644 --- a/Documentation/git-worktree.txt +++ b/Documentation/git-worktree.txt @@ -10,7 +10,7 @@ SYNOPSIS -------- [verse] 'git worktree add' [-f] [--detach] [--checkout] [--lock [--reason <string>]] [-b <new-branch>] <path> [<commit-ish>] -'git worktree list' [--porcelain] +'git worktree list' [-v | --porcelain] 'git worktree lock' [--reason <string>] <worktree> 'git worktree move' <worktree> <new-path> 'git worktree prune' [-n] [-v] [--expire <expire>] diff --git a/Documentation/git.txt b/Documentation/git.txt index 281c5f8cae..13f83a2a3a 100644 --- a/Documentation/git.txt +++ b/Documentation/git.txt @@ -832,8 +832,9 @@ for full details. `GIT_TRACE_REDACT`:: By default, when tracing is activated, Git redacts the values of - cookies, the "Authorization:" header, and the "Proxy-Authorization:" - header. Set this variable to `0` to prevent this redaction. + cookies, the "Authorization:" header, the "Proxy-Authorization:" + header and packfile URIs. Set this variable to `0` to prevent this + redaction. `GIT_LITERAL_PATHSPECS`:: Setting this variable to `1` will cause Git to treat all diff --git a/Documentation/gitcredentials.txt b/Documentation/gitcredentials.txt index 758bf39ba3..80517b4eb2 100644 --- a/Documentation/gitcredentials.txt +++ b/Documentation/gitcredentials.txt @@ -132,7 +132,7 @@ because the hostnames differ. Nor would it match `foo.example.com`; Git compares hostnames exactly, without considering whether two hosts are part of the same domain. Likewise, a config entry for `http://example.com` would not match: Git compares the protocols exactly. However, you may use wildcards in -the domain name and other pattern matching techniques as with the `http.<url>.*` +the domain name and other pattern matching techniques as with the `http.<URL>.*` options. If the "pattern" URL does include a path component, then this too must match @@ -147,7 +147,7 @@ CONFIGURATION OPTIONS Options for a credential context can be configured either in `credential.*` (which applies to all credentials), or -`credential.<url>.*`, where <url> matches the context as described +`credential.<URL>.*`, where <URL> matches the context as described above. The following options are available in either location: diff --git a/Documentation/gitsubmodules.txt b/Documentation/gitsubmodules.txt index 891c8da4fd..941858a6ec 100644 --- a/Documentation/gitsubmodules.txt +++ b/Documentation/gitsubmodules.txt @@ -226,7 +226,7 @@ Workflow for a third party library ---------------------------------- # Add a submodule - git submodule add <url> <path> + git submodule add <URL> <path> # Occasionally update the submodule to a new version: git -C <path> checkout <new version> diff --git a/Documentation/gitworkflows.txt b/Documentation/gitworkflows.txt index 47cf97f9be..59305265c5 100644 --- a/Documentation/gitworkflows.txt +++ b/Documentation/gitworkflows.txt @@ -394,7 +394,7 @@ request to do so by mail. Such a request looks like ------------------------------------- Please pull from - <url> <branch> + <URL> <branch> ------------------------------------- In that case, 'git pull' can do the fetch and merge in one go, as @@ -403,7 +403,7 @@ follows. .Push/pull: Merging remote topics [caption="Recipe: "] ===================================== -`git pull <url> <branch>` +`git pull <URL> <branch>` ===================================== Occasionally, the maintainer may get merge conflicts when they try to @@ -440,7 +440,7 @@ merge because you cannot format-patch merges): .format-patch/am: Keeping topics up to date [caption="Recipe: "] ===================================== -`git pull --rebase <url> <branch>` +`git pull --rebase <URL> <branch>` ===================================== You can then fix the conflicts during the rebase. Presumably you have diff --git a/Documentation/pretty-formats.txt b/Documentation/pretty-formats.txt index ef6bd420ae..0b4c1c8d98 100644 --- a/Documentation/pretty-formats.txt +++ b/Documentation/pretty-formats.txt @@ -20,7 +20,7 @@ built-in formats: * 'oneline' - <hash> <title line> + <hash> <title-line> + This is designed to be as compact as possible. @@ -29,17 +29,17 @@ This is designed to be as compact as possible. commit <hash> Author: <author> - <title line> + <title-line> * 'medium' commit <hash> Author: <author> - Date: <author date> + Date: <author-date> - <title line> + <title-line> - <full commit message> + <full-commit-message> * 'full' @@ -47,25 +47,25 @@ This is designed to be as compact as possible. Author: <author> Commit: <committer> - <title line> + <title-line> - <full commit message> + <full-commit-message> * 'fuller' commit <hash> Author: <author> - AuthorDate: <author date> + AuthorDate: <author-date> Commit: <committer> - CommitDate: <committer date> + CommitDate: <committer-date> - <title line> + <title-line> - <full commit message> + <full-commit-message> * 'reference' - <abbrev hash> (<title line>, <short author date>) + <abbrev-hash> (<title-line>, <short-author-date>) + This format is used to refer to another commit in a commit message and is the same as `--pretty='format:%C(auto)%h (%s, %ad)'`. By default, @@ -78,10 +78,10 @@ placeholders, its output is not affected by other options like From <hash> <date> From: <author> - Date: <author date> - Subject: [PATCH] <title line> + Date: <author-date> + Subject: [PATCH] <title-line> - <full commit message> + <full-commit-message> * 'mboxrd' + @@ -101,9 +101,9 @@ commits are displayed, but not the way the diff is shown e.g. with `git log --raw`. To get full object names in a raw diff format, use `--no-abbrev`. -* 'format:<string>' +* 'format:<format-string>' + -The 'format:<string>' format allows you to specify which information +The 'format:<format-string>' format allows you to specify which information you want to show. It works a little bit like printf format, with the notable exception that you get a newline with '%n' instead of '\n'. @@ -220,6 +220,12 @@ The placeholders are: inconsistent when tags are added or removed at the same time. + +** 'tags[=<bool-value>]': Instead of only considering annotated tags, + consider lightweight tags as well. +** 'abbrev=<number>': Instead of using the default number of hexadecimal digits + (which will vary according to the number of objects in the repository with a + default of 7) of the abbreviated object name, use <number> digits, or as many + digits as needed to form a unique object name. ** 'match=<pattern>': Only consider tags matching the given `glob(7)` pattern, excluding the "refs/tags/" prefix. ** 'exclude=<pattern>': Do not consider tags matching the given @@ -273,12 +279,7 @@ endif::git-rev-list[] If any option is provided multiple times the last occurrence wins. + -The boolean options accept an optional value `[=<BOOL>]`. The values -`true`, `false`, `on`, `off` etc. are all accepted. See the "boolean" -sub-section in "EXAMPLES" in linkgit:git-config[1]. If a boolean -option is given with no value, it's enabled. -+ -** 'key=<K>': only show trailers with specified key. Matching is done +** 'key=<key>': only show trailers with specified <key>. Matching is done case-insensitively and trailing colon is optional. If option is given multiple times trailer lines matching any of the keys are shown. This option automatically enables the `only` option so that @@ -286,25 +287,25 @@ option is given with no value, it's enabled. desired it can be disabled with `only=false`. E.g., `%(trailers:key=Reviewed-by)` shows trailer lines with key `Reviewed-by`. -** 'only[=<BOOL>]': select whether non-trailer lines from the trailer +** 'only[=<bool>]': select whether non-trailer lines from the trailer block should be included. -** 'separator=<SEP>': specify a separator inserted between trailer +** 'separator=<sep>': specify a separator inserted between trailer lines. When this option is not given each trailer line is - terminated with a line feed character. The string SEP may contain + terminated with a line feed character. The string <sep> may contain the literal formatting codes described above. To use comma as separator one must use `%x2C` as it would otherwise be parsed as next option. E.g., `%(trailers:key=Ticket,separator=%x2C )` shows all trailer lines whose key is "Ticket" separated by a comma and a space. -** 'unfold[=<BOOL>]': make it behave as if interpret-trailer's `--unfold` +** 'unfold[=<bool>]': make it behave as if interpret-trailer's `--unfold` option was given. E.g., `%(trailers:only,unfold=true)` unfolds and shows all trailer lines. -** 'keyonly[=<BOOL>]': only show the key part of the trailer. -** 'valueonly[=<BOOL>]': only show the value part of the trailer. -** 'key_value_separator=<SEP>': specify a separator inserted between +** 'keyonly[=<bool>]': only show the key part of the trailer. +** 'valueonly[=<bool>]': only show the value part of the trailer. +** 'key_value_separator=<sep>': specify a separator inserted between trailer lines. When this option is not given each trailer key-value pair is separated by ": ". Otherwise it shares the same semantics - as 'separator=<SEP>' above. + as 'separator=<sep>' above. NOTE: Some placeholders may depend on other options given to the revision traversal engine. For example, the `%g*` reflog options will @@ -313,6 +314,11 @@ insert an empty string unless we are traversing reflog entries (e.g., by decoration format if `--decorate` was not already provided on the command line. +The boolean options accept an optional value `[=<bool-value>]`. The values +`true`, `false`, `on`, `off` etc. are all accepted. See the "boolean" +sub-section in "EXAMPLES" in linkgit:git-config[1]. If a boolean +option is given with no value, it's enabled. + If you add a `+` (plus sign) after '%' of a placeholder, a line-feed is inserted immediately before the expansion if and only if the placeholder expands to a non-empty string. diff --git a/Documentation/rev-list-options.txt b/Documentation/rev-list-options.txt index 24569b06d1..43a86fa562 100644 --- a/Documentation/rev-list-options.txt +++ b/Documentation/rev-list-options.txt @@ -1047,7 +1047,7 @@ omitted. has no effect. `--date=format:...` feeds the format `...` to your system `strftime`, -except for %z and %Z, which are handled internally. +except for %s, %z, and %Z, which are handled internally. Use `--date=format:%c` to show the date in your system locale's preferred format. See the `strftime` manual for a complete list of format placeholders. When using `-local`, the correct syntax is diff --git a/Documentation/technical/multi-pack-index.txt b/Documentation/technical/multi-pack-index.txt index 86f40f2490..b39c69da8c 100644 --- a/Documentation/technical/multi-pack-index.txt +++ b/Documentation/technical/multi-pack-index.txt @@ -17,12 +17,12 @@ is not feasible due to storage space or excessive repack times. The multi-pack-index (MIDX for short) stores a list of objects and their offsets into multiple packfiles. It contains: -- A list of packfile names. -- A sorted list of object IDs. -- A list of metadata for the ith object ID including: - - A value j referring to the jth packfile. - - An offset within the jth packfile for the object. -- If large offsets are required, we use another list of large +* A list of packfile names. +* A sorted list of object IDs. +* A list of metadata for the ith object ID including: +** A value j referring to the jth packfile. +** An offset within the jth packfile for the object. +* If large offsets are required, we use another list of large offsets similar to version 2 pack-indexes. Thus, we can provide O(log N) lookup time for any number @@ -87,11 +87,6 @@ Future Work helpful to organize packfiles by object type (commit, tree, blob, etc.) and use this metadata to help that maintenance. -- The partial clone feature records special "promisor" packs that - may point to objects that are not stored locally, but available - on request to a server. The multi-pack-index does not currently - track these promisor packs. - Related Links ------------- [0] https://bugs.chromium.org/p/git/issues/detail?id=6 diff --git a/Documentation/technical/protocol-v2.txt b/Documentation/technical/protocol-v2.txt index 21e8258ccf..8a877d27e2 100644 --- a/Documentation/technical/protocol-v2.txt +++ b/Documentation/technical/protocol-v2.txt @@ -125,11 +125,11 @@ command can be requested at a time. empty-request = flush-pkt command-request = command capability-list - [command-args] + delim-pkt + command-args flush-pkt command = PKT-LINE("command=" key LF) - command-args = delim-pkt - *command-specific-arg + command-args = *command-specific-arg command-specific-args are packet line framed arguments defined by each individual command. diff --git a/Documentation/technical/rerere.txt b/Documentation/technical/rerere.txt index af5f9fc24f..35d4541433 100644 --- a/Documentation/technical/rerere.txt +++ b/Documentation/technical/rerere.txt @@ -14,9 +14,9 @@ conflicts before writing them to the rerere database. Different conflict styles and branch names are normalized by stripping the labels from the conflict markers, and removing the common ancestor -version from the `diff3` conflict style. Branches that are merged -in different order are normalized by sorting the conflict hunks. More -on each of those steps in the following sections. +version from the `diff3` or `zdiff3` conflict styles. Branches that +are merged in different order are normalized by sorting the conflict +hunks. More on each of those steps in the following sections. Once these two normalization operations are applied, a conflict ID is calculated based on the normalized conflict, which is later used by @@ -42,8 +42,8 @@ get a conflict like the following: >>>>>>> AC Doing the analogous with AC2 (forking a branch ABAC2 off of branch AB -and then merging branch AC2 into it), using the diff3 conflict style, -we get a conflict like the following: +and then merging branch AC2 into it), using the diff3 or zdiff3 +conflict style, we get a conflict like the following: <<<<<<< HEAD B diff --git a/Documentation/urls-remotes.txt b/Documentation/urls-remotes.txt index bd184cd653..86d0008f94 100644 --- a/Documentation/urls-remotes.txt +++ b/Documentation/urls-remotes.txt @@ -26,14 +26,14 @@ config file would appear like this: ------------ [remote "<name>"] - url = <url> + url = <URL> pushurl = <pushurl> push = <refspec> fetch = <refspec> ------------ The `<pushurl>` is used for pushes only. It is optional and defaults -to `<url>`. +to `<URL>`. Named file in `$GIT_DIR/remotes` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -67,10 +67,10 @@ This file should have the following format: ------------ - <url>#<head> + <URL>#<head> ------------ -`<url>` is required; `#<head>` is optional. +`<URL>` is required; `#<head>` is optional. Depending on the operation, git will use one of the following refspecs, if you don't provide one on the command line. diff --git a/GIT-VERSION-GEN b/GIT-VERSION-GEN index 991308dfaf..8d2297c33b 100755 --- a/GIT-VERSION-GEN +++ b/GIT-VERSION-GEN @@ -1,7 +1,7 @@ #!/bin/sh GVF=GIT-VERSION-FILE -DEF_VER=v2.34.1 +DEF_VER=v2.34.GIT LF=' ' @@ -256,6 +256,8 @@ all:: # # Define NO_DEFLATE_BOUND if your zlib does not have deflateBound. # +# Define NO_UNCOMPRESS2 if your zlib does not have uncompress2. +# # Define NO_NORETURN if using buggy versions of gcc 4.6+ and profile feedback, # as the compiler can crash (http://gcc.gnu.org/bugzilla/show_bug.cgi?id=49299) # @@ -305,9 +307,6 @@ all:: # # Define NO_TCLTK if you do not want Tcl/Tk GUI. # -# Define SANE_TEXT_GREP to "-a" if you use recent versions of GNU grep -# and egrep that are pickier when their input contains non-ASCII data. -# # The TCL_PATH variable governs the location of the Tcl interpreter # used to optimize git-gui for your system. Only used if NO_TCLTK # is not set. Defaults to the bare 'tclsh'. @@ -735,6 +734,7 @@ TEST_BUILTINS_OBJS += test-read-cache.o TEST_BUILTINS_OBJS += test-read-graph.o TEST_BUILTINS_OBJS += test-read-midx.o TEST_BUILTINS_OBJS += test-ref-store.o +TEST_BUILTINS_OBJS += test-reftable.o TEST_BUILTINS_OBJS += test-regex.o TEST_BUILTINS_OBJS += test-repository.o TEST_BUILTINS_OBJS += test-revision-walking.o @@ -813,6 +813,8 @@ TEST_SHELL_PATH = $(SHELL_PATH) LIB_FILE = libgit.a XDIFF_LIB = xdiff/lib.a +REFTABLE_LIB = reftable/libreftable.a +REFTABLE_TEST_LIB = reftable/libreftable_test.a GENERATED_H += command-list.h GENERATED_H += config-list.h @@ -1192,7 +1194,7 @@ THIRD_PARTY_SOURCES += compat/regex/% THIRD_PARTY_SOURCES += sha1collisiondetection/% THIRD_PARTY_SOURCES += sha1dc/% -GITLIBS = common-main.o $(LIB_FILE) $(XDIFF_LIB) +GITLIBS = common-main.o $(LIB_FILE) $(XDIFF_LIB) $(REFTABLE_LIB) EXTLIBS = GIT_USER_AGENT = git/$(GIT_VERSION) @@ -1204,6 +1206,7 @@ endif # Set CFLAGS, LDFLAGS and other *FLAGS variables. These might be # tweaked by config.* below as well as the command-line, both of # which'll override these defaults. +# Older versions of GCC may require adding "-std=gnu99" at the end. CFLAGS = -g -O2 -Wall LDFLAGS = CC_LD_DYNPATH = -Wl,-rpath, @@ -1215,7 +1218,7 @@ ARFLAGS = rcs PTHREAD_CFLAGS = # For the 'sparse' target -SPARSE_FLAGS ?= +SPARSE_FLAGS ?= -std=gnu99 SP_EXTRA_FLAGS = -Wno-universal-initializer # For informing GIT-BUILD-OPTIONS of the SANITIZE=leak target @@ -1723,6 +1726,11 @@ ifdef NO_DEFLATE_BOUND BASIC_CFLAGS += -DNO_DEFLATE_BOUND endif +ifdef NO_UNCOMPRESS2 + BASIC_CFLAGS += -DNO_UNCOMPRESS2 + REFTABLE_OBJS += compat/zlib-uncompress2.o +endif + ifdef NO_POSIX_GOODIES BASIC_CFLAGS += -DNO_POSIX_GOODIES endif @@ -1873,7 +1881,7 @@ ifdef GIT_TEST_CMP_USE_COPIED_CONTEXT endif ifndef NO_MSGFMT_EXTENDED_OPTIONS - MSGFMT += --check --statistics + MSGFMT += --check endif ifdef HAVE_CLOCK_GETTIME @@ -2104,11 +2112,6 @@ ifdef DEFAULT_HELP_FORMAT BASIC_CFLAGS += -DDEFAULT_HELP_FORMAT='"$(DEFAULT_HELP_FORMAT)"' endif -PAGER_ENV_SQ = $(subst ','\'',$(PAGER_ENV)) -PAGER_ENV_CQ = "$(subst ",\",$(subst \,\\,$(PAGER_ENV)))" -PAGER_ENV_CQ_SQ = $(subst ','\'',$(PAGER_ENV_CQ)) -BASIC_CFLAGS += -DPAGER_ENV='$(PAGER_ENV_CQ_SQ)' - ALL_CFLAGS += $(BASIC_CFLAGS) ALL_LDFLAGS += $(BASIC_LDFLAGS) @@ -2215,14 +2218,20 @@ git$X: git.o GIT-LDFLAGS $(BUILTIN_OBJS) $(GITLIBS) $(filter %.o,$^) $(LIBS) help.sp help.s help.o: command-list.h -hook.sp hook.s hook.o: hook-list.h +builtin/bugreport.sp builtin/bugreport.s builtin/bugreport.o: hook-list.h -builtin/help.sp builtin/help.s builtin/help.o: config-list.h hook-list.h GIT-PREFIX +builtin/help.sp builtin/help.s builtin/help.o: config-list.h GIT-PREFIX builtin/help.sp builtin/help.s builtin/help.o: EXTRA_CPPFLAGS = \ '-DGIT_HTML_PATH="$(htmldir_relative_SQ)"' \ '-DGIT_MAN_PATH="$(mandir_relative_SQ)"' \ '-DGIT_INFO_PATH="$(infodir_relative_SQ)"' +PAGER_ENV_SQ = $(subst ','\'',$(PAGER_ENV)) +PAGER_ENV_CQ = "$(subst ",\",$(subst \,\\,$(PAGER_ENV)))" +PAGER_ENV_CQ_SQ = $(subst ','\'',$(PAGER_ENV_CQ)) +pager.sp pager.s pager.o: EXTRA_CPPFLAGS = \ + -DPAGER_ENV='$(PAGER_ENV_CQ_SQ)' + version.sp version.s version.o: GIT-VERSION-FILE GIT-USER-AGENT version.sp version.s version.o: EXTRA_CPPFLAGS = \ '-DGIT_VERSION="$(GIT_VERSION)"' \ @@ -2252,33 +2261,30 @@ command-list.h: $(wildcard Documentation/git*.txt) hook-list.h: generate-hooklist.sh Documentation/githooks.txt $(QUIET_GEN)$(SHELL_PATH) ./generate-hooklist.sh >$@ -SCRIPT_DEFINES = $(SHELL_PATH_SQ):$(DIFF_SQ):$(GIT_VERSION):\ - $(localedir_SQ):$(NO_CURL):$(USE_GETTEXT_SCHEME):$(SANE_TOOL_PATH_SQ):\ - $(gitwebdir_SQ):$(PERL_PATH_SQ):$(SANE_TEXT_GREP):$(PAGER_ENV):\ +SCRIPT_DEFINES = $(SHELL_PATH_SQ):$(DIFF_SQ):\ + $(localedir_SQ):$(USE_GETTEXT_SCHEME):$(SANE_TOOL_PATH_SQ):\ + $(gitwebdir_SQ):$(PERL_PATH_SQ):$(PAGER_ENV):\ $(perllibdir_SQ) +GIT-SCRIPT-DEFINES: FORCE + @FLAGS='$(SCRIPT_DEFINES)'; \ + if test x"$$FLAGS" != x"`cat $@ 2>/dev/null`" ; then \ + echo >&2 " * new script parameters"; \ + echo "$$FLAGS" >$@; \ + fi + define cmd_munge_script sed -e '1s|#!.*/sh|#!$(SHELL_PATH_SQ)|' \ -e 's|@SHELL_PATH@|$(SHELL_PATH_SQ)|' \ -e 's|@@DIFF@@|$(DIFF_SQ)|' \ -e 's|@@LOCALEDIR@@|$(localedir_SQ)|g' \ - -e 's/@@NO_CURL@@/$(NO_CURL)/g' \ -e 's/@@USE_GETTEXT_SCHEME@@/$(USE_GETTEXT_SCHEME)/g' \ -e $(BROKEN_PATH_FIX) \ -e 's|@@GITWEBDIR@@|$(gitwebdir_SQ)|g' \ -e 's|@@PERL@@|$(PERL_PATH_SQ)|g' \ - -e 's|@@SANE_TEXT_GREP@@|$(SANE_TEXT_GREP)|g' \ -e 's|@@PAGER_ENV@@|$(PAGER_ENV_SQ)|g' \ $@.sh >$@+ endef -GIT-SCRIPT-DEFINES: FORCE - @FLAGS='$(SCRIPT_DEFINES)'; \ - if test x"$$FLAGS" != x"`cat $@ 2>/dev/null`" ; then \ - echo >&2 " * new script parameters"; \ - echo "$$FLAGS" >$@; \ - fi - - $(SCRIPT_SH_GEN) : % : %.sh GIT-SCRIPT-DEFINES $(QUIET_GEN)$(cmd_munge_script) && \ chmod +x $@+ && \ @@ -2437,7 +2443,36 @@ XDIFF_OBJS += xdiff/xutils.o .PHONY: xdiff-objs xdiff-objs: $(XDIFF_OBJS) +REFTABLE_OBJS += reftable/basics.o +REFTABLE_OBJS += reftable/error.o +REFTABLE_OBJS += reftable/block.o +REFTABLE_OBJS += reftable/blocksource.o +REFTABLE_OBJS += reftable/iter.o +REFTABLE_OBJS += reftable/publicbasics.o +REFTABLE_OBJS += reftable/merged.o +REFTABLE_OBJS += reftable/pq.o +REFTABLE_OBJS += reftable/reader.o +REFTABLE_OBJS += reftable/record.o +REFTABLE_OBJS += reftable/refname.o +REFTABLE_OBJS += reftable/generic.o +REFTABLE_OBJS += reftable/stack.o +REFTABLE_OBJS += reftable/tree.o +REFTABLE_OBJS += reftable/writer.o + +REFTABLE_TEST_OBJS += reftable/basics_test.o +REFTABLE_TEST_OBJS += reftable/block_test.o +REFTABLE_TEST_OBJS += reftable/dump.o +REFTABLE_TEST_OBJS += reftable/merged_test.o +REFTABLE_TEST_OBJS += reftable/pq_test.o +REFTABLE_TEST_OBJS += reftable/record_test.o +REFTABLE_TEST_OBJS += reftable/readwrite_test.o +REFTABLE_TEST_OBJS += reftable/refname_test.o +REFTABLE_TEST_OBJS += reftable/stack_test.o +REFTABLE_TEST_OBJS += reftable/test_framework.o +REFTABLE_TEST_OBJS += reftable/tree_test.o + TEST_OBJS := $(patsubst %$X,%.o,$(TEST_PROGRAMS)) $(patsubst %,t/helper/%,$(TEST_BUILTINS_OBJS)) + .PHONY: test-objs test-objs: $(TEST_OBJS) @@ -2453,9 +2488,16 @@ OBJECTS += $(PROGRAM_OBJS) OBJECTS += $(TEST_OBJS) OBJECTS += $(XDIFF_OBJS) OBJECTS += $(FUZZ_OBJS) +OBJECTS += $(REFTABLE_OBJS) $(REFTABLE_TEST_OBJS) + ifndef NO_CURL OBJECTS += http.o http-walker.o remote-curl.o endif + +SCALAR_SOURCES := contrib/scalar/scalar.c +SCALAR_OBJECTS := $(SCALAR_SOURCES:c=o) +OBJECTS += $(SCALAR_OBJECTS) + .PHONY: objects objects: $(OBJECTS) @@ -2589,12 +2631,22 @@ $(REMOTE_CURL_PRIMARY): remote-curl.o http.o http-walker.o GIT-LDFLAGS $(GITLIBS $(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) \ $(CURL_LIBCURL) $(EXPAT_LIBEXPAT) $(LIBS) +contrib/scalar/scalar$X: $(SCALAR_OBJECTS) GIT-LDFLAGS $(GITLIBS) + $(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) \ + $(filter %.o,$^) $(LIBS) + $(LIB_FILE): $(LIB_OBJS) $(QUIET_AR)$(RM) $@ && $(AR) $(ARFLAGS) $@ $^ $(XDIFF_LIB): $(XDIFF_OBJS) $(QUIET_AR)$(RM) $@ && $(AR) $(ARFLAGS) $@ $^ +$(REFTABLE_LIB): $(REFTABLE_OBJS) + $(QUIET_AR)$(RM) $@ && $(AR) $(ARFLAGS) $@ $^ + +$(REFTABLE_TEST_LIB): $(REFTABLE_TEST_OBJS) + $(QUIET_AR)$(RM) $@ && $(AR) $(ARFLAGS) $@ $^ + export DEFAULT_EDITOR DEFAULT_PAGER Documentation/GIT-EXCLUDED-PROGRAMS: FORCE @@ -2893,7 +2945,7 @@ perf: all t/helper/test-tool$X: $(patsubst %,t/helper/%,$(TEST_BUILTINS_OBJS)) -t/helper/test-%$X: t/helper/test-%.o GIT-LDFLAGS $(GITLIBS) +t/helper/test-%$X: t/helper/test-%.o GIT-LDFLAGS $(GITLIBS) $(REFTABLE_TEST_LIB) $(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) $(filter %.a,$^) $(LIBS) check-sha1:: t/helper/test-tool$X @@ -3231,7 +3283,7 @@ cocciclean: clean: profile-clean coverage-clean cocciclean $(RM) *.res $(RM) $(OBJECTS) - $(RM) $(LIB_FILE) $(XDIFF_LIB) + $(RM) $(LIB_FILE) $(XDIFF_LIB) $(REFTABLE_LIB) $(REFTABLE_TEST_LIB) $(RM) $(ALL_PROGRAMS) $(SCRIPT_LIB) $(BUILT_INS) git$X $(RM) $(TEST_PROGRAMS) $(RM) $(FUZZ_PROGRAMS) @@ -1,4 +1,4 @@ -[![Build status](https://github.com/git/git/workflows/CI/PR/badge.svg)](https://github.com/git/git/actions?query=branch%3Amaster+event%3Apush) +[![Build status](https://github.com/git/git/workflows/CI/badge.svg)](https://github.com/git/git/actions?query=branch%3Amaster+event%3Apush) Git - fast, scalable, distributed revision control system ========================================================= @@ -1 +1 @@ -Documentation/RelNotes/2.34.1.txt
\ No newline at end of file +Documentation/RelNotes/2.35.0.txt
\ No newline at end of file diff --git a/add-patch.c b/add-patch.c index 8c41cdfe39..573eef0cc4 100644 --- a/add-patch.c +++ b/add-patch.c @@ -413,7 +413,7 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps) strvec_push(&args, ps->items[i].original); setup_child_process(s, &cp, NULL); - cp.argv = args.v; + strvec_pushv(&cp.args, args.v); res = capture_command(&cp, plain, 0); if (res) { strvec_clear(&args); @@ -431,7 +431,7 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps) setup_child_process(s, &colored_cp, NULL); xsnprintf((char *)args.v[color_arg_index], 8, "--color"); - colored_cp.argv = args.v; + strvec_pushv(&colored_cp.args, args.v); colored = &s->colored; res = capture_command(&colored_cp, colored, 0); strvec_clear(&args); @@ -4752,8 +4752,10 @@ static int apply_patch(struct apply_state *state, } if (!list && !skipped_patch) { - error(_("unrecognized input")); - res = -128; + if (!state->allow_empty) { + error(_("No valid patches in input (allow with \"--allow-empty\")")); + res = -128; + } goto end; } @@ -5071,7 +5073,7 @@ int apply_parse_options(int argc, const char **argv, N_("leave the rejected hunks in corresponding *.rej files")), OPT_BOOL(0, "allow-overlap", &state->allow_overlap, N_("allow overlapping hunks")), - OPT__VERBOSE(&state->apply_verbosity, N_("be verbose")), + OPT__VERBOSITY(&state->apply_verbosity), OPT_BIT(0, "inaccurate-eof", options, N_("tolerate incorrectly detected missing new-line at the end of file"), APPLY_OPT_INACCURATE_EOF), @@ -5081,6 +5083,8 @@ int apply_parse_options(int argc, const char **argv, OPT_CALLBACK(0, "directory", state, N_("root"), N_("prepend <root> to all filenames"), apply_option_parse_directory), + OPT_BOOL(0, "allow-empty", &state->allow_empty, + N_("don't return error for empty patches")), OPT_END() }; @@ -66,6 +66,7 @@ struct apply_state { int threeway; int unidiff_zero; int unsafe_paths; + int allow_empty; /* Other non boolean parameters */ struct repository *repo; diff --git a/archive-tar.c b/archive-tar.c index 05d2455870..3c74db1746 100644 --- a/archive-tar.c +++ b/archive-tar.c @@ -430,7 +430,6 @@ static int write_tar_filter_archive(const struct archiver *ar, { struct strbuf cmd = STRBUF_INIT; struct child_process filter = CHILD_PROCESS_INIT; - const char *argv[2]; int r; if (!ar->data) @@ -440,14 +439,12 @@ static int write_tar_filter_archive(const struct archiver *ar, if (args->compression_level >= 0) strbuf_addf(&cmd, " -%d", args->compression_level); - argv[0] = cmd.buf; - argv[1] = NULL; - filter.argv = argv; + strvec_push(&filter.args, cmd.buf); filter.use_shell = 1; filter.in = -1; if (start_command(&filter) < 0) - die_errno(_("unable to start '%s' filter"), argv[0]); + die_errno(_("unable to start '%s' filter"), cmd.buf); close(1); if (dup2(filter.in, 1) < 0) die_errno(_("unable to redirect descriptor")); @@ -457,7 +454,7 @@ static int write_tar_filter_archive(const struct archiver *ar, close(1); if (finish_command(&filter) != 0) - die(_("'%s' filter reported error"), argv[0]); + die(_("'%s' filter reported error"), cmd.buf); strbuf_release(&cmd); return r; @@ -64,7 +64,7 @@ int install_branch_config(int flag, const char *local, const char *origin, const if (skip_prefix(remote, "refs/heads/", &shortname) && !strcmp(local, shortname) && !origin) { - warning(_("Not setting branch %s as its own upstream."), + warning(_("not setting branch %s as its own upstream"), local); return 0; } @@ -116,7 +116,7 @@ int install_branch_config(int flag, const char *local, const char *origin, const out_err: strbuf_release(&key); - error(_("Unable to write upstream branch configuration")); + error(_("unable to write upstream branch configuration")); advise(_(tracking_advice), origin ? origin : "", @@ -153,7 +153,7 @@ static void setup_tracking(const char *new_ref, const char *orig_ref, } if (tracking.matches > 1) - die(_("Not tracking: ambiguous information for ref %s"), + die(_("not tracking: ambiguous information for ref %s"), orig_ref); if (install_branch_config(config_flags, new_ref, tracking.remote, @@ -186,7 +186,7 @@ int read_branch_desc(struct strbuf *buf, const char *branch_name) int validate_branchname(const char *name, struct strbuf *ref) { if (strbuf_check_branch_ref(ref, name)) - die(_("'%s' is not a valid branch name."), name); + die(_("'%s' is not a valid branch name"), name); return ref_exists(ref->buf); } @@ -199,18 +199,23 @@ int validate_branchname(const char *name, struct strbuf *ref) */ int validate_new_branchname(const char *name, struct strbuf *ref, int force) { - const char *head; + struct worktree **worktrees; + const struct worktree *wt; if (!validate_branchname(name, ref)) return 0; if (!force) - die(_("A branch named '%s' already exists."), + die(_("a branch named '%s' already exists"), ref->buf + strlen("refs/heads/")); - head = resolve_ref_unsafe("HEAD", 0, NULL, NULL); - if (!is_bare_repository() && head && !strcmp(head, ref->buf)) - die(_("Cannot force update the current branch.")); + worktrees = get_worktrees(); + wt = find_shared_symref(worktrees, "HEAD", ref->buf); + if (wt && !wt->is_bare) + die(_("cannot force update the branch '%s'" + "checked out at '%s'"), + ref->buf + strlen("refs/heads/"), wt->path); + free_worktrees(worktrees); return 1; } @@ -230,7 +235,7 @@ static int validate_remote_tracking_branch(char *ref) } static const char upstream_not_branch[] = -N_("Cannot setup tracking information; starting point '%s' is not a branch."); +N_("cannot set up tracking information; starting point '%s' is not a branch"); static const char upstream_missing[] = N_("the requested upstream branch '%s' does not exist"); static const char upstream_advice[] = @@ -278,7 +283,7 @@ void create_branch(struct repository *r, } die(_(upstream_missing), start_name); } - die(_("Not a valid object name: '%s'."), start_name); + die(_("not a valid object name: '%s'"), start_name); } switch (dwim_ref(start_name, strlen(start_name), &oid, &real_ref, 0)) { @@ -298,12 +303,12 @@ void create_branch(struct repository *r, } break; default: - die(_("Ambiguous object name: '%s'."), start_name); + die(_("ambiguous object name: '%s'"), start_name); break; } if ((commit = lookup_commit_reference(r, &oid)) == NULL) - die(_("Not a valid branch point: '%s'."), start_name); + die(_("not a valid branch point: '%s'"), start_name); oidcpy(&oid, &commit->object.oid); if (reflog) @@ -357,14 +362,16 @@ void remove_branch_state(struct repository *r, int verbose) void die_if_checked_out(const char *branch, int ignore_current_worktree) { + struct worktree **worktrees = get_worktrees(); const struct worktree *wt; - wt = find_shared_symref("HEAD", branch); - if (!wt || (ignore_current_worktree && wt->is_current)) - return; - skip_prefix(branch, "refs/heads/", &branch); - die(_("'%s' is already checked out at '%s'"), - branch, wt->path); + wt = find_shared_symref(worktrees, "HEAD", branch); + if (wt && (!ignore_current_worktree || !wt->is_current)) { + skip_prefix(branch, "refs/heads/", &branch); + die(_("'%s' is already checked out at '%s'"), branch, wt->path); + } + + free_worktrees(worktrees); } int replace_each_worktree_head_symref(const char *oldref, const char *newref, diff --git a/builtin/add.c b/builtin/add.c index ef6b619c45..a010b2c325 100644 --- a/builtin/add.c +++ b/builtin/add.c @@ -302,15 +302,11 @@ int interactive_add(const char **argv, const char *prefix, int patch) static int edit_patch(int argc, const char **argv, const char *prefix) { char *file = git_pathdup("ADD_EDIT.patch"); - const char *apply_argv[] = { "apply", "--recount", "--cached", - NULL, NULL }; struct child_process child = CHILD_PROCESS_INIT; struct rev_info rev; int out; struct stat st; - apply_argv[3] = file; - git_config(git_diff_basic_config, NULL); /* no "diff" UI options */ if (read_cache() < 0) @@ -338,7 +334,8 @@ static int edit_patch(int argc, const char **argv, const char *prefix) die(_("Empty patch. Aborted.")); child.git_cmd = 1; - child.argv = apply_argv; + strvec_pushl(&child.args, "apply", "--recount", "--cached", file, + NULL); if (run_command(&child)) die(_("Could not apply '%s'"), file); diff --git a/builtin/am.c b/builtin/am.c index 8677ea2348..f2e7e53388 100644 --- a/builtin/am.c +++ b/builtin/am.c @@ -87,6 +87,12 @@ enum show_patch_type { SHOW_PATCH_DIFF = 1, }; +enum empty_action { + STOP_ON_EMPTY_COMMIT = 0, /* output errors and stop in the middle of an am session */ + DROP_EMPTY_COMMIT, /* skip with a notice message, unless "--quiet" has been passed */ + KEEP_EMPTY_COMMIT, /* keep recording as empty commits */ +}; + struct am_state { /* state directory path */ char *dir; @@ -118,6 +124,7 @@ struct am_state { int message_id; int scissors; /* enum scissors_type */ int quoted_cr; /* enum quoted_cr_action */ + int empty_type; /* enum empty_action */ struct strvec git_apply_opts; const char *resolvemsg; int committer_date_is_author_date; @@ -178,6 +185,25 @@ static int am_option_parse_quoted_cr(const struct option *opt, return 0; } +static int am_option_parse_empty(const struct option *opt, + const char *arg, int unset) +{ + int *opt_value = opt->value; + + BUG_ON_OPT_NEG(unset); + + if (!strcmp(arg, "stop")) + *opt_value = STOP_ON_EMPTY_COMMIT; + else if (!strcmp(arg, "drop")) + *opt_value = DROP_EMPTY_COMMIT; + else if (!strcmp(arg, "keep")) + *opt_value = KEEP_EMPTY_COMMIT; + else + return error(_("Invalid value for --empty: %s"), arg); + + return 0; +} + /** * Returns path relative to the am_state directory. */ @@ -1126,6 +1152,12 @@ static void NORETURN die_user_resolve(const struct am_state *state) printf_ln(_("When you have resolved this problem, run \"%s --continue\"."), cmdline); printf_ln(_("If you prefer to skip this patch, run \"%s --skip\" instead."), cmdline); + + if (advice_enabled(ADVICE_AM_WORK_DIR) && + is_empty_or_missing_file(am_path(state, "patch")) && + !repo_index_has_changes(the_repository, NULL, NULL)) + printf_ln(_("To record the empty patch as an empty commit, run \"%s --allow-empty\"."), cmdline); + printf_ln(_("To restore the original branch and stop patching, run \"%s --abort\"."), cmdline); } @@ -1248,11 +1280,6 @@ static int parse_mail(struct am_state *state, const char *mail) goto finish; } - if (is_empty_or_missing_file(am_path(state, "patch"))) { - printf_ln(_("Patch is empty.")); - die_user_resolve(state); - } - strbuf_addstr(&msg, "\n\n"); strbuf_addbuf(&msg, &mi.log_message); strbuf_stripspace(&msg, 0); @@ -1763,6 +1790,7 @@ static void am_run(struct am_state *state, int resume) while (state->cur <= state->last) { const char *mail = am_path(state, msgnum(state)); int apply_status; + int to_keep; reset_ident_date(); @@ -1792,8 +1820,29 @@ static void am_run(struct am_state *state, int resume) if (state->interactive && do_interactive(state)) goto next; + to_keep = 0; + if (is_empty_or_missing_file(am_path(state, "patch"))) { + switch (state->empty_type) { + case DROP_EMPTY_COMMIT: + say(state, stdout, _("Skipping: %.*s"), linelen(state->msg), state->msg); + goto next; + break; + case KEEP_EMPTY_COMMIT: + to_keep = 1; + say(state, stdout, _("Creating an empty commit: %.*s"), + linelen(state->msg), state->msg); + break; + case STOP_ON_EMPTY_COMMIT: + printf_ln(_("Patch is empty.")); + die_user_resolve(state); + break; + } + } + if (run_applypatch_msg_hook(state)) exit(1); + if (to_keep) + goto commit; say(state, stdout, _("Applying: %.*s"), linelen(state->msg), state->msg); @@ -1827,6 +1876,7 @@ static void am_run(struct am_state *state, int resume) die_user_resolve(state); } +commit: do_commit(state); next: @@ -1856,19 +1906,24 @@ next: /** * Resume the current am session after patch application failure. The user did * all the hard work, and we do not have to do any patch application. Just - * trust and commit what the user has in the index and working tree. + * trust and commit what the user has in the index and working tree. If `allow_empty` + * is true, commit as an empty commit when index has not changed and lacking a patch. */ -static void am_resolve(struct am_state *state) +static void am_resolve(struct am_state *state, int allow_empty) { validate_resume_state(state); say(state, stdout, _("Applying: %.*s"), linelen(state->msg), state->msg); if (!repo_index_has_changes(the_repository, NULL, NULL)) { - printf_ln(_("No changes - did you forget to use 'git add'?\n" - "If there is nothing left to stage, chances are that something else\n" - "already introduced the same changes; you might want to skip this patch.")); - die_user_resolve(state); + if (allow_empty && is_empty_or_missing_file(am_path(state, "patch"))) { + printf_ln(_("No changes - recorded it as an empty commit.")); + } else { + printf_ln(_("No changes - did you forget to use 'git add'?\n" + "If there is nothing left to stage, chances are that something else\n" + "already introduced the same changes; you might want to skip this patch.")); + die_user_resolve(state); + } } if (unmerged_cache()) { @@ -2195,7 +2250,8 @@ enum resume_type { RESUME_SKIP, RESUME_ABORT, RESUME_QUIT, - RESUME_SHOW_PATCH + RESUME_SHOW_PATCH, + RESUME_ALLOW_EMPTY, }; struct resume_mode { @@ -2348,6 +2404,9 @@ int cmd_am(int argc, const char **argv, const char *prefix) N_("show the patch being applied"), PARSE_OPT_CMDMODE | PARSE_OPT_OPTARG | PARSE_OPT_NONEG | PARSE_OPT_LITERAL_ARGHELP, parse_opt_show_current_patch, RESUME_SHOW_PATCH }, + OPT_CMDMODE(0, "allow-empty", &resume.mode, + N_("record the empty patch as an empty commit"), + RESUME_ALLOW_EMPTY), OPT_BOOL(0, "committer-date-is-author-date", &state.committer_date_is_author_date, N_("lie about committer date")), @@ -2357,6 +2416,9 @@ int cmd_am(int argc, const char **argv, const char *prefix) { OPTION_STRING, 'S', "gpg-sign", &state.sign_commit, N_("key-id"), N_("GPG-sign commits"), PARSE_OPT_OPTARG, NULL, (intptr_t) "" }, + OPT_CALLBACK_F(STOP_ON_EMPTY_COMMIT, "empty", &state.empty_type, "{stop,drop,keep}", + N_("how to handle empty patches"), + PARSE_OPT_NONEG, am_option_parse_empty), OPT_HIDDEN_BOOL(0, "rebasing", &state.rebasing, N_("(internal use for git-rebase)")), OPT_END() @@ -2453,7 +2515,8 @@ int cmd_am(int argc, const char **argv, const char *prefix) am_run(&state, 1); break; case RESUME_RESOLVED: - am_resolve(&state); + case RESUME_ALLOW_EMPTY: + am_resolve(&state, resume.mode == RESUME_ALLOW_EMPTY ? 1 : 0); break; case RESUME_SKIP: am_skip(&state); diff --git a/builtin/blame.c b/builtin/blame.c index f9ee3f8c68..7fafeac408 100644 --- a/builtin/blame.c +++ b/builtin/blame.c @@ -939,6 +939,9 @@ parse_done: revs.diffopt.flags.follow_renames = 0; argc = parse_options_end(&ctx); + prepare_repo_settings(the_repository); + the_repository->settings.command_requires_full_index = 0; + if (incremental || (output_option & OUTPUT_PORCELAIN)) { if (show_progress > 0) die(_("--progress can't be used with --incremental or porcelain formats")); diff --git a/builtin/branch.c b/builtin/branch.c index 7a1d1eeb07..6c8b0fcc11 100644 --- a/builtin/branch.c +++ b/builtin/branch.c @@ -77,12 +77,11 @@ define_list_config_array(color_branch_slots); static int git_branch_config(const char *var, const char *value, void *cb) { const char *slot_name; - struct ref_sorting **sorting_tail = (struct ref_sorting **)cb; if (!strcmp(var, "branch.sort")) { if (!value) return config_error_nonbool(var); - parse_ref_sorting(sorting_tail, value); + string_list_append(cb, value); return 0; } @@ -193,6 +192,7 @@ static void delete_branch_config(const char *branchname) static int delete_branches(int argc, const char **argv, int force, int kinds, int quiet) { + struct worktree **worktrees; struct commit *head_rev = NULL; struct object_id oid; char *name = NULL; @@ -229,6 +229,9 @@ static int delete_branches(int argc, const char **argv, int force, int kinds, if (!head_rev) die(_("Couldn't look up commit object for HEAD")); } + + worktrees = get_worktrees(); + for (i = 0; i < argc; i++, strbuf_reset(&bname)) { char *target = NULL; int flags = 0; @@ -239,7 +242,7 @@ static int delete_branches(int argc, const char **argv, int force, int kinds, if (kinds == FILTER_REFS_BRANCHES) { const struct worktree *wt = - find_shared_symref("HEAD", name); + find_shared_symref(worktrees, "HEAD", name); if (wt) { error(_("Cannot delete branch '%s' " "checked out at '%s'"), @@ -300,6 +303,7 @@ static int delete_branches(int argc, const char **argv, int force, int kinds, free(name); strbuf_release(&bname); + free_worktrees(worktrees); return ret; } @@ -625,7 +629,8 @@ int cmd_branch(int argc, const char **argv, const char *prefix) enum branch_track track; struct ref_filter filter; int icase = 0; - static struct ref_sorting *sorting = NULL, **sorting_tail = &sorting; + static struct ref_sorting *sorting; + struct string_list sorting_options = STRING_LIST_INIT_DUP; struct ref_format format = REF_FORMAT_INIT; struct option options[] = { @@ -666,7 +671,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix) OPT_MERGED(&filter, N_("print only branches that are merged")), OPT_NO_MERGED(&filter, N_("print only branches that are not merged")), OPT_COLUMN(0, "column", &colopts, N_("list branches in columns")), - OPT_REF_SORT(sorting_tail), + OPT_REF_SORT(&sorting_options), OPT_CALLBACK(0, "points-at", &filter.points_at, N_("object"), N_("print only branches of the object"), parse_opt_object_name), OPT_BOOL('i', "ignore-case", &icase, N_("sorting and filtering are case insensitive")), @@ -683,7 +688,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix) if (argc == 2 && !strcmp(argv[1], "-h")) usage_with_options(builtin_branch_usage, options); - git_config(git_branch_config, sorting_tail); + git_config(git_branch_config, &sorting_options); track = git_branch_track; @@ -749,8 +754,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix) * local branches 'refs/heads/...' and finally remote-tracking * branches 'refs/remotes/...'. */ - if (!sorting) - sorting = ref_default_sorting(); + sorting = ref_sorting_options(&sorting_options); ref_sorting_set_sort_flags_all(sorting, REF_SORTING_ICASE, icase); ref_sorting_set_sort_flags_all( sorting, REF_SORTING_DETACHED_HEAD_FIRST, 1); diff --git a/builtin/checkout.c b/builtin/checkout.c index cbf73b8c9f..72beeb49aa 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -91,8 +91,8 @@ struct checkout_opts { }; struct branch_info { - const char *name; /* The short name used */ - const char *path; /* The full name of a real branch */ + char *name; /* The short name used */ + char *path; /* The full name of a real branch */ struct commit *commit; /* The named commit */ char *refname; /* The full name of the ref being checked out. */ struct object_id oid; /* The object ID of the commit being checked out. */ @@ -103,6 +103,14 @@ struct branch_info { char *checkout; }; +static void branch_info_release(struct branch_info *info) +{ + free(info->name); + free(info->path); + free(info->refname); + free(info->checkout); +} + static int post_checkout_hook(struct commit *old_commit, struct commit *new_commit, int changed) { @@ -688,9 +696,12 @@ static void setup_branch_path(struct branch_info *branch) repo_get_oid_committish(the_repository, branch->name, &branch->oid); strbuf_branchname(&buf, branch->name, INTERPRET_BRANCH_LOCAL); - if (strcmp(buf.buf, branch->name)) + if (strcmp(buf.buf, branch->name)) { + free(branch->name); branch->name = xstrdup(buf.buf); + } strbuf_splice(&buf, 0, 0, "refs/heads/", 11); + free(branch->path); branch->path = strbuf_detach(&buf, NULL); } @@ -874,7 +885,7 @@ static void update_refs_for_switch(const struct checkout_opts *opts, int ret; struct strbuf err = STRBUF_INIT; - ret = safe_create_reflog(refname, 1, &err); + ret = safe_create_reflog(refname, &err); if (ret) { fprintf(stderr, _("Can not do reflog for '%s': %s\n"), opts->new_orphan_branch, err.buf); @@ -894,7 +905,9 @@ static void update_refs_for_switch(const struct checkout_opts *opts, opts->new_branch_log, opts->quiet, opts->track); - new_branch_info->name = opts->new_branch; + free(new_branch_info->name); + free(new_branch_info->refname); + new_branch_info->name = xstrdup(opts->new_branch); setup_branch_path(new_branch_info); } @@ -1062,8 +1075,7 @@ static int switch_branches(const struct checkout_opts *opts, struct branch_info *new_branch_info) { int ret = 0; - struct branch_info old_branch_info; - void *path_to_free; + struct branch_info old_branch_info = { 0 }; struct object_id rev; int flag, writeout_error = 0; int do_merge = 1; @@ -1071,25 +1083,32 @@ static int switch_branches(const struct checkout_opts *opts, trace2_cmd_mode("branch"); memset(&old_branch_info, 0, sizeof(old_branch_info)); - old_branch_info.path = path_to_free = resolve_refdup("HEAD", 0, &rev, &flag); + old_branch_info.path = resolve_refdup("HEAD", 0, &rev, &flag); if (old_branch_info.path) old_branch_info.commit = lookup_commit_reference_gently(the_repository, &rev, 1); if (!(flag & REF_ISSYMREF)) - old_branch_info.path = NULL; + FREE_AND_NULL(old_branch_info.path); - if (old_branch_info.path) - skip_prefix(old_branch_info.path, "refs/heads/", &old_branch_info.name); + if (old_branch_info.path) { + const char *const prefix = "refs/heads/"; + const char *p; + if (skip_prefix(old_branch_info.path, prefix, &p)) + old_branch_info.name = xstrdup(p); + else + BUG("should be able to skip past '%s' in '%s'!", + prefix, old_branch_info.path); + } if (opts->new_orphan_branch && opts->orphan_from_empty_tree) { if (new_branch_info->name) BUG("'switch --orphan' should never accept a commit as starting point"); new_branch_info->commit = NULL; - new_branch_info->name = "(empty)"; + new_branch_info->name = xstrdup("(empty)"); do_merge = 1; } if (!new_branch_info->name) { - new_branch_info->name = "HEAD"; + new_branch_info->name = xstrdup("HEAD"); new_branch_info->commit = old_branch_info.commit; if (!new_branch_info->commit) die(_("You are on a branch yet to be born")); @@ -1102,7 +1121,7 @@ static int switch_branches(const struct checkout_opts *opts, if (do_merge) { ret = merge_working_tree(opts, &old_branch_info, new_branch_info, &writeout_error); if (ret) { - free(path_to_free); + branch_info_release(&old_branch_info); return ret; } } @@ -1113,7 +1132,8 @@ static int switch_branches(const struct checkout_opts *opts, update_refs_for_switch(opts, &old_branch_info, new_branch_info); ret = post_checkout_hook(old_branch_info.commit, new_branch_info->commit, 1); - free(path_to_free); + branch_info_release(&old_branch_info); + return ret || writeout_error; } @@ -1145,16 +1165,15 @@ static void setup_new_branch_info_and_source_tree( struct tree **source_tree = &opts->source_tree; struct object_id branch_rev; - new_branch_info->name = arg; + new_branch_info->name = xstrdup(arg); setup_branch_path(new_branch_info); if (!check_refname_format(new_branch_info->path, 0) && !read_ref(new_branch_info->path, &branch_rev)) oidcpy(rev, &branch_rev); - else { - free((char *)new_branch_info->path); - new_branch_info->path = NULL; /* not an existing branch */ - } + else + /* not an existing branch */ + FREE_AND_NULL(new_branch_info->path); new_branch_info->commit = lookup_commit_reference_gently(the_repository, rev, 1); if (!new_branch_info->commit) { @@ -1517,7 +1536,7 @@ static struct option *add_common_options(struct checkout_opts *opts, OPT_BOOL(0, "progress", &opts->show_progress, N_("force progress reporting")), OPT_BOOL('m', "merge", &opts->merge, N_("perform a 3-way merge with the new branch")), OPT_STRING(0, "conflict", &opts->conflict_style, N_("style"), - N_("conflict style (merge or diff3)")), + N_("conflict style (merge, diff3, or zdiff3)")), OPT_END() }; struct option *newopts = parse_options_concat(prevopts, options); @@ -1574,12 +1593,11 @@ static char cb_option = 'b'; static int checkout_main(int argc, const char **argv, const char *prefix, struct checkout_opts *opts, struct option *options, - const char * const usagestr[]) + const char * const usagestr[], + struct branch_info *new_branch_info) { - struct branch_info new_branch_info; int parseopt_flags = 0; - memset(&new_branch_info, 0, sizeof(new_branch_info)); opts->overwrite_ignore = 1; opts->prefix = prefix; opts->show_progress = -1; @@ -1688,7 +1706,7 @@ static int checkout_main(int argc, const char **argv, const char *prefix, opts->track == BRANCH_TRACK_UNSPECIFIED && !opts->new_branch; int n = parse_branchname_arg(argc, argv, dwim_ok, - &new_branch_info, opts, &rev); + new_branch_info, opts, &rev); argv += n; argc -= n; } else if (!opts->accept_ref && opts->from_treeish) { @@ -1697,7 +1715,7 @@ static int checkout_main(int argc, const char **argv, const char *prefix, if (get_oid_mb(opts->from_treeish, &rev)) die(_("could not resolve %s"), opts->from_treeish); - setup_new_branch_info_and_source_tree(&new_branch_info, + setup_new_branch_info_and_source_tree(new_branch_info, opts, &rev, opts->from_treeish); @@ -1717,7 +1735,7 @@ static int checkout_main(int argc, const char **argv, const char *prefix, * Try to give more helpful suggestion. * new_branch && argc > 1 will be caught later. */ - if (opts->new_branch && argc == 1 && !new_branch_info.commit) + if (opts->new_branch && argc == 1 && !new_branch_info->commit) die(_("'%s' is not a commit and a branch '%s' cannot be created from it"), argv[0], opts->new_branch); @@ -1766,11 +1784,10 @@ static int checkout_main(int argc, const char **argv, const char *prefix, strbuf_release(&buf); } - UNLEAK(opts); if (opts->patch_mode || opts->pathspec.nr) - return checkout_paths(opts, &new_branch_info); + return checkout_paths(opts, new_branch_info); else - return checkout_branch(opts, &new_branch_info); + return checkout_branch(opts, new_branch_info); } int cmd_checkout(int argc, const char **argv, const char *prefix) @@ -1789,6 +1806,7 @@ int cmd_checkout(int argc, const char **argv, const char *prefix) OPT_END() }; int ret; + struct branch_info new_branch_info = { 0 }; memset(&opts, 0, sizeof(opts)); opts.dwim_new_local_branch = 1; @@ -1819,7 +1837,9 @@ int cmd_checkout(int argc, const char **argv, const char *prefix) options = add_checkout_path_options(&opts, options); ret = checkout_main(argc, argv, prefix, &opts, - options, checkout_usage); + options, checkout_usage, &new_branch_info); + branch_info_release(&new_branch_info); + clear_pathspec(&opts.pathspec); FREE_AND_NULL(options); return ret; } @@ -1840,6 +1860,7 @@ int cmd_switch(int argc, const char **argv, const char *prefix) OPT_END() }; int ret; + struct branch_info new_branch_info = { 0 }; memset(&opts, 0, sizeof(opts)); opts.dwim_new_local_branch = 1; @@ -1859,7 +1880,8 @@ int cmd_switch(int argc, const char **argv, const char *prefix) cb_option = 'c'; ret = checkout_main(argc, argv, prefix, &opts, - options, switch_branch_usage); + options, switch_branch_usage, &new_branch_info); + branch_info_release(&new_branch_info); FREE_AND_NULL(options); return ret; } @@ -1881,6 +1903,7 @@ int cmd_restore(int argc, const char **argv, const char *prefix) OPT_END() }; int ret; + struct branch_info new_branch_info = { 0 }; memset(&opts, 0, sizeof(opts)); opts.accept_ref = 0; @@ -1896,7 +1919,8 @@ int cmd_restore(int argc, const char **argv, const char *prefix) options = add_checkout_path_options(&opts, options); ret = checkout_main(argc, argv, prefix, &opts, - options, restore_usage); + options, restore_usage, &new_branch_info); + branch_info_release(&new_branch_info); FREE_AND_NULL(options); return ret; } diff --git a/builtin/clean.c b/builtin/clean.c index 98a2860409..3ff02bbbff 100644 --- a/builtin/clean.c +++ b/builtin/clean.c @@ -36,6 +36,8 @@ static const char *msg_skip_git_dir = N_("Skipping repository %s\n"); static const char *msg_would_skip_git_dir = N_("Would skip repository %s\n"); static const char *msg_warn_remove_failed = N_("failed to remove %s"); static const char *msg_warn_lstat_failed = N_("could not lstat %s\n"); +static const char *msg_skip_cwd = N_("Refusing to remove current working directory\n"); +static const char *msg_would_skip_cwd = N_("Would refuse to remove current working directory\n"); enum color_clean { CLEAN_COLOR_RESET = 0, @@ -153,6 +155,8 @@ static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag, { DIR *dir; struct strbuf quoted = STRBUF_INIT; + struct strbuf realpath = STRBUF_INIT; + struct strbuf real_ocwd = STRBUF_INIT; struct dirent *e; int res = 0, ret = 0, gone = 1, original_len = path->len, len; struct string_list dels = STRING_LIST_INIT_DUP; @@ -231,16 +235,36 @@ static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag, strbuf_setlen(path, original_len); if (*dir_gone) { - res = dry_run ? 0 : rmdir(path->buf); - if (!res) - *dir_gone = 1; - else { - int saved_errno = errno; - quote_path(path->buf, prefix, "ed, 0); - errno = saved_errno; - warning_errno(_(msg_warn_remove_failed), quoted.buf); + /* + * Normalize path components in path->buf, e.g. change '\' to + * '/' on Windows. + */ + strbuf_realpath(&realpath, path->buf, 1); + + /* + * path and realpath are absolute; for comparison, we would + * like to transform startup_info->original_cwd to an absolute + * path too. + */ + if (startup_info->original_cwd) + strbuf_realpath(&real_ocwd, + startup_info->original_cwd, 1); + + if (!strbuf_cmp(&realpath, &real_ocwd)) { + printf("%s", dry_run ? _(msg_would_skip_cwd) : _(msg_skip_cwd)); *dir_gone = 0; - ret = 1; + } else { + res = dry_run ? 0 : rmdir(path->buf); + if (!res) + *dir_gone = 1; + else { + int saved_errno = errno; + quote_path(path->buf, prefix, "ed, 0); + errno = saved_errno; + warning_errno(_(msg_warn_remove_failed), quoted.buf); + *dir_gone = 0; + ret = 1; + } } } @@ -250,6 +274,8 @@ static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag, printf(dry_run ? _(msg_would_remove) : _(msg_remove), dels.items[i].string); } out: + strbuf_release(&realpath); + strbuf_release(&real_ocwd); strbuf_release("ed); string_list_clear(&dels, 0); return ret; diff --git a/builtin/clone.c b/builtin/clone.c index fb377b2765..5bed37f8b5 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -633,7 +633,7 @@ static int git_sparse_checkout_init(const char *repo) { struct strvec argv = STRVEC_INIT; int result = 0; - strvec_pushl(&argv, "-C", repo, "sparse-checkout", "init", NULL); + strvec_pushl(&argv, "-C", repo, "sparse-checkout", "set", NULL); /* * We must apply the setting in the current process diff --git a/builtin/credential.c b/builtin/credential.c index d75dcdc64a..d7b304fa08 100644 --- a/builtin/credential.c +++ b/builtin/credential.c @@ -4,7 +4,7 @@ #include "config.h" static const char usage_msg[] = - "git credential [fill|approve|reject]"; + "git credential (fill|approve|reject)"; int cmd_credential(int argc, const char **argv, const char *prefix) { diff --git a/builtin/diff.c b/builtin/diff.c index dd8ce688ba..fa4683377e 100644 --- a/builtin/diff.c +++ b/builtin/diff.c @@ -437,6 +437,11 @@ int cmd_diff(int argc, const char **argv, const char *prefix) prefix = setup_git_directory_gently(&nongit); + if (!nongit) { + prepare_repo_settings(the_repository); + the_repository->settings.command_requires_full_index = 0; + } + if (!no_index) { /* * Treat git diff with at least one path outside of the diff --git a/builtin/difftool.c b/builtin/difftool.c index 4931c10845..4ee40fe3a0 100644 --- a/builtin/difftool.c +++ b/builtin/difftool.c @@ -202,15 +202,10 @@ static void changed_files(struct hashmap *result, const char *index_path, { struct child_process update_index = CHILD_PROCESS_INIT; struct child_process diff_files = CHILD_PROCESS_INIT; - struct strbuf index_env = STRBUF_INIT, buf = STRBUF_INIT; - const char *git_dir = absolute_path(get_git_dir()), *env[] = { - NULL, NULL - }; + struct strbuf buf = STRBUF_INIT; + const char *git_dir = absolute_path(get_git_dir()); FILE *fp; - strbuf_addf(&index_env, "GIT_INDEX_FILE=%s", index_path); - env[0] = index_env.buf; - strvec_pushl(&update_index.args, "--git-dir", git_dir, "--work-tree", workdir, "update-index", "--really-refresh", "-q", @@ -222,7 +217,7 @@ static void changed_files(struct hashmap *result, const char *index_path, update_index.use_shell = 0; update_index.clean_on_exit = 1; update_index.dir = workdir; - update_index.env = env; + strvec_pushf(&update_index.env_array, "GIT_INDEX_FILE=%s", index_path); /* Ignore any errors of update-index */ run_command(&update_index); @@ -235,7 +230,7 @@ static void changed_files(struct hashmap *result, const char *index_path, diff_files.clean_on_exit = 1; diff_files.out = -1; diff_files.dir = workdir; - diff_files.env = env; + strvec_pushf(&diff_files.env_array, "GIT_INDEX_FILE=%s", index_path); if (start_command(&diff_files)) die("could not obtain raw diff"); fp = xfdopen(diff_files.out, "r"); @@ -248,7 +243,6 @@ static void changed_files(struct hashmap *result, const char *index_path, fclose(fp); if (finish_command(&diff_files)) die("diff-files did not exit properly"); - strbuf_release(&index_env); strbuf_release(&buf); } diff --git a/builtin/fast-export.c b/builtin/fast-export.c index 8e2caf7281..cb1d6473f1 100644 --- a/builtin/fast-export.c +++ b/builtin/fast-export.c @@ -107,18 +107,6 @@ static int parse_opt_reencode_mode(const struct option *opt, static struct decoration idnums; static uint32_t last_idnum; - -static int has_unshown_parent(struct commit *commit) -{ - struct commit_list *parent; - - for (parent = commit->parents; parent; parent = parent->next) - if (!(parent->item->object.flags & SHOWN) && - !(parent->item->object.flags & UNINTERESTING)) - return 1; - return 0; -} - struct anonymized_entry { struct hashmap_entry hash; const char *anon; @@ -752,20 +740,6 @@ static char *anonymize_tag(void *data) return strbuf_detach(&out, NULL); } -static void handle_tail(struct object_array *commits, struct rev_info *revs, - struct string_list *paths_of_changed_objects) -{ - struct commit *commit; - while (commits->nr) { - commit = (struct commit *)object_array_pop(commits); - if (has_unshown_parent(commit)) { - /* Queue again, to be handled later */ - add_object_array(&commit->object, NULL, commits); - return; - } - handle_commit(commit, revs, paths_of_changed_objects); - } -} static void handle_tag(const char *name, struct tag *tag) { @@ -1185,7 +1159,6 @@ static int parse_opt_anonymize_map(const struct option *opt, int cmd_fast_export(int argc, const char **argv, const char *prefix) { struct rev_info revs; - struct object_array commits = OBJECT_ARRAY_INIT; struct commit *commit; char *export_filename = NULL, *import_filename = NULL, @@ -1283,18 +1256,13 @@ int cmd_fast_export(int argc, const char **argv, const char *prefix) if (prepare_revision_walk(&revs)) die("revision walk setup failed"); + + revs.reverse = 1; revs.diffopt.format_callback = show_filemodify; revs.diffopt.format_callback_data = &paths_of_changed_objects; revs.diffopt.flags.recursive = 1; - while ((commit = get_revision(&revs))) { - if (has_unshown_parent(commit)) { - add_object_array(&commit->object, NULL, &commits); - } - else { - handle_commit(commit, &revs, &paths_of_changed_objects); - handle_tail(&commits, &revs, &paths_of_changed_objects); - } - } + while ((commit = get_revision(&revs))) + handle_commit(commit, &revs, &paths_of_changed_objects); handle_tags_and_duplicates(&extra_refs); handle_tags_and_duplicates(&tag_refs); diff --git a/builtin/fetch.c b/builtin/fetch.c index f7abbc31ff..e33fda4174 100644 --- a/builtin/fetch.c +++ b/builtin/fetch.c @@ -28,6 +28,7 @@ #include "promisor-remote.h" #include "commit-graph.h" #include "shallow.h" +#include "worktree.h" #define FORCED_UPDATES_DELAY_WARNING_IN_MS (10 * 1000) @@ -552,7 +553,7 @@ static struct ref *get_ref_map(struct remote *remote, for (i = 0; i < fetch_refspec->nr; i++) get_fetch_map(ref_map, &fetch_refspec->items[i], &oref_tail, 1); } else if (refmap.nr) { - die("--refmap option is only meaningful with command-line refspec(s)."); + die("--refmap option is only meaningful with command-line refspec(s)"); } else { /* Use the defaults */ struct branch *branch = branch_get(NULL); @@ -583,7 +584,7 @@ static struct ref *get_ref_map(struct remote *remote, } else if (!prefetch) { ref_map = get_remote_ref(remote_refs, "HEAD"); if (!ref_map) - die(_("Couldn't find remote ref HEAD")); + die(_("couldn't find remote ref HEAD")); ref_map->fetch_head_status = FETCH_HEAD_MERGE; tail = &ref_map->next; } @@ -848,13 +849,12 @@ static void format_display(struct strbuf *display, char code, static int update_local_ref(struct ref *ref, struct ref_transaction *transaction, - const char *remote, - const struct ref *remote_ref, - struct strbuf *display, - int summary_width) + const char *remote, const struct ref *remote_ref, + struct strbuf *display, int summary_width, + struct worktree **worktrees) { struct commit *current = NULL, *updated; - struct branch *current_branch = branch_get(NULL); + const struct worktree *wt; const char *pretty_ref = prettify_refname(ref->name); int fast_forward = 0; @@ -868,16 +868,17 @@ static int update_local_ref(struct ref *ref, return 0; } - if (current_branch && - !strcmp(ref->name, current_branch->name) && - !(update_head_ok || is_bare_repository()) && - !is_null_oid(&ref->old_oid)) { + if (!update_head_ok && + (wt = find_shared_symref(worktrees, "HEAD", ref->name)) && + !wt->is_bare && !is_null_oid(&ref->old_oid)) { /* * If this is the head, and it's not okay to update * the head, and the old value of the head isn't empty... */ format_display(display, '!', _("[rejected]"), - _("can't fetch in current branch"), + wt->is_current ? + _("can't fetch in current branch") : + _("checked out in another worktree"), remote, pretty_ref, summary_width); return 1; } @@ -1067,16 +1068,17 @@ static void close_fetch_head(struct fetch_head *fetch_head) } static const char warn_show_forced_updates[] = -N_("Fetch normally indicates which branches had a forced update,\n" - "but that check has been disabled. To re-enable, use '--show-forced-updates'\n" - "flag or run 'git config fetch.showForcedUpdates true'."); +N_("fetch normally indicates which branches had a forced update,\n" + "but that check has been disabled; to re-enable, use '--show-forced-updates'\n" + "flag or run 'git config fetch.showForcedUpdates true'"); static const char warn_time_show_forced_updates[] = -N_("It took %.2f seconds to check forced updates. You can use\n" +N_("it took %.2f seconds to check forced updates; you can use\n" "'--no-show-forced-updates' or run 'git config fetch.showForcedUpdates false'\n" - " to avoid this check.\n"); + "to avoid this check\n"); static int store_updated_refs(const char *raw_url, const char *remote_name, - int connectivity_checked, struct ref *ref_map) + int connectivity_checked, struct ref *ref_map, + struct worktree **worktrees) { struct fetch_head fetch_head; int url_len, i, rc = 0; @@ -1205,7 +1207,8 @@ static int store_updated_refs(const char *raw_url, const char *remote_name, strbuf_reset(¬e); if (ref) { rc |= update_local_ref(ref, transaction, what, - rm, ¬e, summary_width); + rm, ¬e, summary_width, + worktrees); free(ref); } else if (write_fetch_head || dry_run) { /* @@ -1298,7 +1301,9 @@ static int check_exist_and_connected(struct ref *ref_map) return check_connected(iterate_ref_map, &rm, &opt); } -static int fetch_and_consume_refs(struct transport *transport, struct ref *ref_map) +static int fetch_and_consume_refs(struct transport *transport, + struct ref *ref_map, + struct worktree **worktrees) { int connectivity_checked = 1; int ret; @@ -1319,10 +1324,8 @@ static int fetch_and_consume_refs(struct transport *transport, struct ref *ref_m } trace2_region_enter("fetch", "consume_refs", the_repository); - ret = store_updated_refs(transport->url, - transport->remote->name, - connectivity_checked, - ref_map); + ret = store_updated_refs(transport->url, transport->remote->name, + connectivity_checked, ref_map, worktrees); trace2_region_leave("fetch", "consume_refs", the_repository); out: @@ -1385,18 +1388,18 @@ static int prune_refs(struct refspec *rs, struct ref *ref_map, return result; } -static void check_not_current_branch(struct ref *ref_map) +static void check_not_current_branch(struct ref *ref_map, + struct worktree **worktrees) { - struct branch *current_branch = branch_get(NULL); - - if (is_bare_repository() || !current_branch) - return; - + const struct worktree *wt; for (; ref_map; ref_map = ref_map->next) - if (ref_map->peer_ref && !strcmp(current_branch->refname, - ref_map->peer_ref->name)) - die(_("Refusing to fetch into current branch %s " - "of non-bare repository"), current_branch->refname); + if (ref_map->peer_ref && + (wt = find_shared_symref(worktrees, "HEAD", + ref_map->peer_ref->name)) && + !wt->is_bare) + die(_("refusing to fetch into branch '%s' " + "checked out at '%s'"), + ref_map->peer_ref->name, wt->path); } static int truncate_fetch_head(void) @@ -1414,10 +1417,10 @@ static void set_option(struct transport *transport, const char *name, const char { int r = transport_set_option(transport, name, value); if (r < 0) - die(_("Option \"%s\" value \"%s\" is not valid for %s"), + die(_("option \"%s\" value \"%s\" is not valid for %s"), name, value, transport->url); if (r > 0) - warning(_("Option \"%s\" is ignored for %s\n"), + warning(_("option \"%s\" is ignored for %s\n"), name, transport->url); } @@ -1451,7 +1454,7 @@ static void add_negotiation_tips(struct git_transport_options *smart_options) old_nr = oids->nr; for_each_glob_ref(add_oid, s, oids); if (old_nr == oids->nr) - warning("Ignoring --negotiation-tip=%s because it does not match any refs", + warning("ignoring --negotiation-tip=%s because it does not match any refs", s); } smart_options->negotiation_tips = oids; @@ -1489,12 +1492,13 @@ static struct transport *prepare_transport(struct remote *remote, int deepen) if (transport->smart_options) add_negotiation_tips(transport->smart_options); else - warning("Ignoring --negotiation-tip because the protocol does not support it."); + warning("ignoring --negotiation-tip because the protocol does not support it"); } return transport; } -static void backfill_tags(struct transport *transport, struct ref *ref_map) +static void backfill_tags(struct transport *transport, struct ref *ref_map, + struct worktree **worktrees) { int cannot_reuse; @@ -1515,7 +1519,7 @@ static void backfill_tags(struct transport *transport, struct ref *ref_map) transport_set_option(transport, TRANS_OPT_FOLLOWTAGS, NULL); transport_set_option(transport, TRANS_OPT_DEPTH, "0"); transport_set_option(transport, TRANS_OPT_DEEPEN_RELATIVE, NULL); - fetch_and_consume_refs(transport, ref_map); + fetch_and_consume_refs(transport, ref_map, worktrees); if (gsecondary) { transport_disconnect(gsecondary); @@ -1533,6 +1537,7 @@ static int do_fetch(struct transport *transport, struct transport_ls_refs_options transport_ls_refs_options = TRANSPORT_LS_REFS_OPTIONS_INIT; int must_list_refs = 1; + struct worktree **worktrees = get_worktrees(); if (tags == TAGS_DEFAULT) { if (transport->remote->fetch_tags == 2) @@ -1588,7 +1593,7 @@ static int do_fetch(struct transport *transport, ref_map = get_ref_map(transport->remote, remote_refs, rs, tags, &autotags); if (!update_head_ok) - check_not_current_branch(ref_map); + check_not_current_branch(ref_map, worktrees); if (tags == TAGS_DEFAULT && autotags) transport_set_option(transport, TRANS_OPT_FOLLOWTAGS, "1"); @@ -1606,7 +1611,7 @@ static int do_fetch(struct transport *transport, transport->url); } } - if (fetch_and_consume_refs(transport, ref_map)) { + if (fetch_and_consume_refs(transport, ref_map, worktrees)) { free_refs(ref_map); retcode = 1; goto cleanup; @@ -1638,6 +1643,16 @@ static int do_fetch(struct transport *transport, } } if (source_ref) { + if (!branch) { + const char *shortname = source_ref->name; + skip_prefix(shortname, "refs/heads/", &shortname); + + warning(_("could not set upstream of HEAD to '%s' from '%s' when " + "it does not point to any branch."), + shortname, transport->remote->name); + goto skip; + } + if (!strcmp(source_ref->name, "HEAD") || starts_with(source_ref->name, "refs/heads/")) install_branch_config(0, @@ -1651,11 +1666,11 @@ static int do_fetch(struct transport *transport, else warning(_("unknown branch type")); } else { - warning(_("no source branch found.\n" - "you need to specify exactly one branch with the --set-upstream option.")); + warning(_("no source branch found;\n" + "you need to specify exactly one branch with the --set-upstream option")); } } - skip: +skip: free_refs(ref_map); /* if neither --no-tags nor --tags was specified, do automated tag @@ -1665,11 +1680,12 @@ static int do_fetch(struct transport *transport, ref_map = NULL; find_non_local_tags(remote_refs, &ref_map, &tail); if (ref_map) - backfill_tags(transport, ref_map); + backfill_tags(transport, ref_map, worktrees); free_refs(ref_map); } - cleanup: +cleanup: + free_worktrees(worktrees); return retcode; } @@ -1790,7 +1806,7 @@ static int fetch_failed_to_start(struct strbuf *out, void *cb, void *task_cb) struct parallel_fetch_state *state = cb; const char *remote = task_cb; - state->result = error(_("Could not fetch %s"), remote); + state->result = error(_("could not fetch %s"), remote); return 0; } @@ -1845,7 +1861,7 @@ static int fetch_multiple(struct string_list *list, int max_children) if (verbosity >= 0) printf(_("Fetching %s\n"), name); if (run_command_v_opt(argv.v, RUN_GIT_CMD)) { - error(_("Could not fetch %s"), name); + error(_("could not fetch %s"), name); result = 1; } strvec_pop(&argv); @@ -1906,8 +1922,8 @@ static int fetch_one(struct remote *remote, int argc, const char **argv, int remote_via_config = remote_is_configured(remote, 0); if (!remote) - die(_("No remote repository specified. Please, specify either a URL or a\n" - "remote name from which new revisions should be fetched.")); + die(_("no remote repository specified; please specify either a URL or a\n" + "remote name from which new revisions should be fetched")); gtransport = prepare_transport(remote, 1); @@ -1942,7 +1958,7 @@ static int fetch_one(struct remote *remote, int argc, const char **argv, if (!strcmp(argv[i], "tag")) { i++; if (i >= argc) - die(_("You need to specify a tag name.")); + die(_("you need to specify a tag name")); refspec_appendf(&rs, "refs/tags/%s:refs/tags/%s", argv[i], argv[i]); @@ -1993,6 +2009,8 @@ int cmd_fetch(int argc, const char **argv, const char *prefix) } git_config(git_fetch_config, NULL); + prepare_repo_settings(the_repository); + the_repository->settings.command_requires_full_index = 0; argc = parse_options(argc, argv, prefix, builtin_fetch_options, builtin_fetch_usage, 0); @@ -2010,7 +2028,7 @@ int cmd_fetch(int argc, const char **argv, const char *prefix) if (deepen_relative) { if (deepen_relative < 0) - die(_("Negative depth in --deepen is not supported")); + die(_("negative depth in --deepen is not supported")); if (depth) die(_("--deepen and --depth are mutually exclusive")); depth = xstrfmt("%d", deepen_relative); @@ -2047,14 +2065,15 @@ int cmd_fetch(int argc, const char **argv, const char *prefix) /* All arguments are assumed to be remotes or groups */ for (i = 0; i < argc; i++) if (!add_remote_or_group(argv[i], &list)) - die(_("No such remote or remote group: %s"), argv[i]); + die(_("no such remote or remote group: %s"), + argv[i]); } else { /* Single remote or group */ (void) add_remote_or_group(argv[0], &list); if (list.nr > 1) { /* More than one remote */ if (argc > 1) - die(_("Fetching a group and specifying refspecs does not make sense")); + die(_("fetching a group and specifying refspecs does not make sense")); } else { /* Zero or one remotes */ remote = remote_get(argv[0]); @@ -2075,7 +2094,7 @@ int cmd_fetch(int argc, const char **argv, const char *prefix) if (gtransport->smart_options) { gtransport->smart_options->acked_commits = &acked_commits; } else { - warning(_("Protocol does not support --negotiate-only, exiting.")); + warning(_("protocol does not support --negotiate-only, exiting")); return 1; } if (server_options.nr) diff --git a/builtin/fmt-merge-msg.c b/builtin/fmt-merge-msg.c index 48a8699de7..8d8fd393f8 100644 --- a/builtin/fmt-merge-msg.c +++ b/builtin/fmt-merge-msg.c @@ -12,6 +12,7 @@ int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix) { const char *inpath = NULL; const char *message = NULL; + char *into_name = NULL; int shortlog_len = -1; struct option options[] = { { OPTION_INTEGER, 0, "log", &shortlog_len, N_("n"), @@ -23,6 +24,8 @@ int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix) DEFAULT_MERGE_LOG_LEN }, OPT_STRING('m', "message", &message, N_("text"), N_("use <text> as start of message")), + OPT_STRING(0, "into-name", &into_name, N_("name"), + N_("use <name> instead of the real target branch")), OPT_FILENAME('F', "file", &inpath, N_("file to read from")), OPT_END() }; @@ -56,6 +59,7 @@ int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix) opts.add_title = !message; opts.credit_people = 1; opts.shortlog_len = shortlog_len; + opts.into_name = into_name; ret = fmt_merge_msg(&input, &output, &opts); if (ret) diff --git a/builtin/for-each-ref.c b/builtin/for-each-ref.c index 16a2c7d57c..6f62f40d12 100644 --- a/builtin/for-each-ref.c +++ b/builtin/for-each-ref.c @@ -17,7 +17,8 @@ static char const * const for_each_ref_usage[] = { int cmd_for_each_ref(int argc, const char **argv, const char *prefix) { int i; - struct ref_sorting *sorting = NULL, **sorting_tail = &sorting; + struct ref_sorting *sorting; + struct string_list sorting_options = STRING_LIST_INIT_DUP; int maxcount = 0, icase = 0; struct ref_array array; struct ref_filter filter; @@ -39,7 +40,7 @@ int cmd_for_each_ref(int argc, const char **argv, const char *prefix) OPT_INTEGER( 0 , "count", &maxcount, N_("show only <n> matched refs")), OPT_STRING( 0 , "format", &format.format, N_("format"), N_("format to use for the output")), OPT__COLOR(&format.use_color, N_("respect format colors")), - OPT_REF_SORT(sorting_tail), + OPT_REF_SORT(&sorting_options), OPT_CALLBACK(0, "points-at", &filter.points_at, N_("object"), N_("print only refs which points at the given object"), parse_opt_object_name), @@ -70,8 +71,7 @@ int cmd_for_each_ref(int argc, const char **argv, const char *prefix) if (verify_ref_format(&format)) usage_with_options(for_each_ref_usage, opts); - if (!sorting) - sorting = ref_default_sorting(); + sorting = ref_sorting_options(&sorting_options); ref_sorting_set_sort_flags_all(sorting, REF_SORTING_ICASE, icase); filter.ignore_case = icase; diff --git a/builtin/fsck.c b/builtin/fsck.c index 27b9e78094..9e54892311 100644 --- a/builtin/fsck.c +++ b/builtin/fsck.c @@ -944,15 +944,13 @@ int cmd_fsck(int argc, const char **argv, const char *prefix) if (the_repository->settings.core_commit_graph) { struct child_process commit_graph_verify = CHILD_PROCESS_INIT; - const char *verify_argv[] = { "commit-graph", "verify", NULL, NULL, NULL }; prepare_alt_odb(the_repository); for (odb = the_repository->objects->odb; odb; odb = odb->next) { child_process_init(&commit_graph_verify); - commit_graph_verify.argv = verify_argv; commit_graph_verify.git_cmd = 1; - verify_argv[2] = "--object-dir"; - verify_argv[3] = odb->path; + strvec_pushl(&commit_graph_verify.args, "commit-graph", + "verify", "--object-dir", odb->path, NULL); if (run_command(&commit_graph_verify)) errors_found |= ERROR_COMMIT_GRAPH; } @@ -960,15 +958,13 @@ int cmd_fsck(int argc, const char **argv, const char *prefix) if (the_repository->settings.core_multi_pack_index) { struct child_process midx_verify = CHILD_PROCESS_INIT; - const char *midx_argv[] = { "multi-pack-index", "verify", NULL, NULL, NULL }; prepare_alt_odb(the_repository); for (odb = the_repository->objects->odb; odb; odb = odb->next) { child_process_init(&midx_verify); - midx_verify.argv = midx_argv; midx_verify.git_cmd = 1; - midx_argv[2] = "--object-dir"; - midx_argv[3] = odb->path; + strvec_pushl(&midx_verify.args, "multi-pack-index", + "verify", "--object-dir", odb->path, NULL); if (run_command(&midx_verify)) errors_found |= ERROR_MULTI_PACK_INDEX; } diff --git a/builtin/help.c b/builtin/help.c index 75cd2fb407..d387131dd8 100644 --- a/builtin/help.c +++ b/builtin/help.c @@ -212,11 +212,10 @@ static int check_emacsclient_version(void) { struct strbuf buffer = STRBUF_INIT; struct child_process ec_process = CHILD_PROCESS_INIT; - const char *argv_ec[] = { "emacsclient", "--version", NULL }; int version; /* emacsclient prints its version number on stderr */ - ec_process.argv = argv_ec; + strvec_pushl(&ec_process.args, "emacsclient", "--version", NULL); ec_process.err = -1; ec_process.stdout_to_stderr = 1; if (start_command(&ec_process)) diff --git a/builtin/log.c b/builtin/log.c index f75d87e8d7..942c07ece6 100644 --- a/builtin/log.c +++ b/builtin/log.c @@ -245,10 +245,24 @@ static void cmd_log_init_finish(int argc, const char **argv, const char *prefix, rev->abbrev_commit = 0; } - if (rev->commit_format == CMIT_FMT_USERFORMAT && !w.decorate) - decoration_style = 0; + if (rev->commit_format == CMIT_FMT_USERFORMAT) { + if (!w.decorate) { + /* + * Disable decoration loading if the format will not + * show them anyway. + */ + decoration_style = 0; + } else if (!decoration_style) { + /* + * If we are going to show them, make sure we do load + * them here, but taking care not to override a + * specific style set by config or --decorate. + */ + decoration_style = DECORATE_SHORT_REFS; + } + } - if (decoration_style) { + if (decoration_style || rev->simplify_by_decoration) { const struct string_list *config_exclude = repo_config_get_value_multi(the_repository, "log.excludeDecoration"); @@ -260,7 +274,8 @@ static void cmd_log_init_finish(int argc, const char **argv, const char *prefix, item->string); } - rev->show_decorations = 1; + if (decoration_style) + rev->show_decorations = 1; load_ref_decorations(&decoration_filter, decoration_style); } @@ -2241,6 +2256,7 @@ done: strbuf_release(&rdiff1); strbuf_release(&rdiff2); strbuf_release(&rdiff_title); + UNLEAK(rev); return 0; } diff --git a/builtin/ls-files.c b/builtin/ls-files.c index 031fef1bca..c151eb1fb7 100644 --- a/builtin/ls-files.c +++ b/builtin/ls-files.c @@ -37,6 +37,7 @@ static int debug_mode; static int show_eol; static int recurse_submodules; static int skipping_duplicates; +static int show_sparse_dirs; static const char *prefix; static int max_prefix_len; @@ -315,8 +316,10 @@ static void show_files(struct repository *repo, struct dir_struct *dir) if (!(show_cached || show_stage || show_deleted || show_modified)) return; - /* TODO: audit for interaction with sparse-index. */ - ensure_full_index(repo->index); + + if (!show_sparse_dirs) + ensure_full_index(repo->index); + for (i = 0; i < repo->index->cache_nr; i++) { const struct cache_entry *ce = repo->index->cache[i]; struct stat st; @@ -670,6 +673,8 @@ int cmd_ls_files(int argc, const char **argv, const char *cmd_prefix) OPT_BOOL(0, "debug", &debug_mode, N_("show debugging data")), OPT_BOOL(0, "deduplicate", &skipping_duplicates, N_("suppress duplicate entries")), + OPT_BOOL(0, "sparse", &show_sparse_dirs, + N_("show sparse directories in the presence of a sparse index")), OPT_END() }; int ret = 0; @@ -677,6 +682,9 @@ int cmd_ls_files(int argc, const char **argv, const char *cmd_prefix) if (argc == 2 && !strcmp(argv[1], "-h")) usage_with_options(ls_files_usage, builtin_ls_files_options); + prepare_repo_settings(the_repository); + the_repository->settings.command_requires_full_index = 0; + prefix = cmd_prefix; if (prefix) prefix_len = strlen(prefix); diff --git a/builtin/ls-remote.c b/builtin/ls-remote.c index 318949c3d7..44448fa61d 100644 --- a/builtin/ls-remote.c +++ b/builtin/ls-remote.c @@ -54,7 +54,7 @@ int cmd_ls_remote(int argc, const char **argv, const char *prefix) struct transport *transport; const struct ref *ref; struct ref_array ref_array; - static struct ref_sorting *sorting = NULL, **sorting_tail = &sorting; + struct string_list sorting_options = STRING_LIST_INIT_DUP; struct option options[] = { OPT__QUIET(&quiet, N_("do not print remote URL")), @@ -68,7 +68,7 @@ int cmd_ls_remote(int argc, const char **argv, const char *prefix) OPT_BIT(0, "refs", &flags, N_("do not show peeled tags"), REF_NORMAL), OPT_BOOL(0, "get-url", &get_url, N_("take url.<base>.insteadOf into account")), - OPT_REF_SORT(sorting_tail), + OPT_REF_SORT(&sorting_options), OPT_SET_INT_F(0, "exit-code", &status, N_("exit with exit code 2 if no matching refs are found"), 2, PARSE_OPT_NOCOMPLETE), @@ -86,8 +86,6 @@ int cmd_ls_remote(int argc, const char **argv, const char *prefix) packet_trace_identity("ls-remote"); - UNLEAK(sorting); - if (argc > 1) { int i; CALLOC_ARRAY(pattern, argc); @@ -139,8 +137,13 @@ int cmd_ls_remote(int argc, const char **argv, const char *prefix) item->symref = xstrdup_or_null(ref->symref); } - if (sorting) + if (sorting_options.nr) { + struct ref_sorting *sorting; + + sorting = ref_sorting_options(&sorting_options); ref_array_sort(sorting, &ref_array); + ref_sorting_release(sorting); + } for (i = 0; i < ref_array.nr; i++) { const struct ref_array_item *ref = ref_array.items[i]; diff --git a/builtin/merge-file.c b/builtin/merge-file.c index 06a2f90c48..e695867ee5 100644 --- a/builtin/merge-file.c +++ b/builtin/merge-file.c @@ -34,6 +34,8 @@ int cmd_merge_file(int argc, const char **argv, const char *prefix) struct option options[] = { OPT_BOOL('p', "stdout", &to_stdout, N_("send results to standard output")), OPT_SET_INT(0, "diff3", &xmp.style, N_("use a diff3 based merge"), XDL_MERGE_DIFF3), + OPT_SET_INT(0, "zdiff3", &xmp.style, N_("use a zealous diff3 based merge"), + XDL_MERGE_ZEALOUS_DIFF3), OPT_SET_INT(0, "ours", &xmp.favor, N_("for conflicts, use our version"), XDL_MERGE_FAVOR_OURS), OPT_SET_INT(0, "theirs", &xmp.favor, N_("for conflicts, use their version"), diff --git a/builtin/merge.c b/builtin/merge.c index ea3112e0c0..991de01bb4 100644 --- a/builtin/merge.c +++ b/builtin/merge.c @@ -87,6 +87,7 @@ static int signoff; static const char *sign_commit; static int autostash; static int no_verify; +static char *into_name; static struct strategy all_strategy[] = { { "recursive", NO_TRIVIAL }, @@ -286,6 +287,8 @@ static struct option builtin_merge_options[] = { { OPTION_LOWLEVEL_CALLBACK, 'F', "file", &merge_msg, N_("path"), N_("read message from file"), PARSE_OPT_NONEG, NULL, 0, option_read_message }, + OPT_STRING(0, "into-name", &into_name, N_("name"), + N_("use <name> instead of the real target")), OPT__VERBOSITY(&verbosity), OPT_BOOL(0, "abort", &abort_current_merge, N_("abort the current in-progress merge")), @@ -310,10 +313,9 @@ static int save_state(struct object_id *stash) int len; struct child_process cp = CHILD_PROCESS_INIT; struct strbuf buffer = STRBUF_INIT; - const char *argv[] = {"stash", "create", NULL}; int rc = -1; - cp.argv = argv; + strvec_pushl(&cp.args, "stash", "create", NULL); cp.out = -1; cp.git_cmd = 1; @@ -1122,6 +1124,7 @@ static void prepare_merge_message(struct strbuf *merge_names, struct strbuf *mer opts.add_title = !have_message; opts.shortlog_len = shortlog_len; opts.credit_people = (0 < option_edit); + opts.into_name = into_name; fmt_merge_msg(merge_names, merge_msg, &opts); if (merge_msg->len) diff --git a/builtin/multi-pack-index.c b/builtin/multi-pack-index.c index 075d15d706..4480ba3982 100644 --- a/builtin/multi-pack-index.c +++ b/builtin/multi-pack-index.c @@ -167,6 +167,8 @@ static int cmd_multi_pack_index_verify(int argc, const char **argv) usage_with_options(builtin_multi_pack_index_verify_usage, options); + FREE_AND_NULL(options); + return verify_midx_file(the_repository, opts.object_dir, opts.flags); } @@ -191,6 +193,8 @@ static int cmd_multi_pack_index_expire(int argc, const char **argv) usage_with_options(builtin_multi_pack_index_expire_usage, options); + FREE_AND_NULL(options); + return expire_midx_packs(the_repository, opts.object_dir, opts.flags); } diff --git a/builtin/name-rev.c b/builtin/name-rev.c index b221d30014..27f60153a6 100644 --- a/builtin/name-rev.c +++ b/builtin/name-rev.c @@ -44,11 +44,20 @@ static struct rev_name *get_commit_rev_name(const struct commit *commit) return is_valid_rev_name(name) ? name : NULL; } +static int effective_distance(int distance, int generation) +{ + return distance + (generation > 0 ? MERGE_TRAVERSAL_WEIGHT : 0); +} + static int is_better_name(struct rev_name *name, timestamp_t taggerdate, + int generation, int distance, int from_tag) { + 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. @@ -56,7 +65,7 @@ static int is_better_name(struct rev_name *name, if (from_tag && name->from_tag) return (name->taggerdate > taggerdate || (name->taggerdate == taggerdate && - name->distance > distance)); + name_distance > new_distance)); /* * We know that at least one of them is a non-tag at this point. @@ -69,8 +78,8 @@ static int is_better_name(struct rev_name *name, * We are now looking at two non-tags. Tiebreak to favor * shorter hops. */ - if (name->distance != distance) - return name->distance > distance; + if (name_distance != new_distance) + return name_distance > new_distance; /* ... or tiebreak to favor older date */ if (name->taggerdate != taggerdate) @@ -88,7 +97,7 @@ static struct rev_name *create_or_update_name(struct commit *commit, struct rev_name *name = commit_rev_name_at(&rev_names, commit); if (is_valid_rev_name(name)) { - if (!is_better_name(name, taggerdate, distance, from_tag)) + if (!is_better_name(name, taggerdate, generation, distance, from_tag)) return NULL; /* diff --git a/builtin/notes.c b/builtin/notes.c index 71c59583a1..118db9e455 100644 --- a/builtin/notes.c +++ b/builtin/notes.c @@ -134,14 +134,13 @@ static void copy_obj_to_fd(int fd, const struct object_id *oid) static void write_commented_object(int fd, const struct object_id *object) { - const char *show_args[5] = - {"show", "--stat", "--no-notes", oid_to_hex(object), NULL}; struct child_process show = CHILD_PROCESS_INIT; struct strbuf buf = STRBUF_INIT; struct strbuf cbuf = STRBUF_INIT; /* Invoke "git show --stat --no-notes $object" */ - show.argv = show_args; + strvec_pushl(&show.args, "show", "--stat", "--no-notes", + oid_to_hex(object), NULL); show.no_stdin = 1; show.out = -1; show.err = 0; @@ -861,15 +860,19 @@ static int merge(int argc, const char **argv, const char *prefix) update_ref(msg.buf, default_notes_ref(), &result_oid, NULL, 0, UPDATE_REFS_DIE_ON_ERR); else { /* Merge has unresolved conflicts */ + struct worktree **worktrees; const struct worktree *wt; /* Update .git/NOTES_MERGE_PARTIAL with partial merge result */ update_ref(msg.buf, "NOTES_MERGE_PARTIAL", &result_oid, NULL, 0, UPDATE_REFS_DIE_ON_ERR); /* Store ref-to-be-updated into .git/NOTES_MERGE_REF */ - wt = find_shared_symref("NOTES_MERGE_REF", default_notes_ref()); + worktrees = get_worktrees(); + wt = find_shared_symref(worktrees, "NOTES_MERGE_REF", + default_notes_ref()); if (wt) die(_("a notes merge into %s is already in-progress at %s"), default_notes_ref(), wt->path); + free_worktrees(worktrees); if (create_symref("NOTES_MERGE_REF", default_notes_ref(), NULL)) die(_("failed to store link to current notes ref (%s)"), default_notes_ref()); diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c index 1a3dd445f8..b36ed828d8 100644 --- a/builtin/pack-objects.c +++ b/builtin/pack-objects.c @@ -3397,7 +3397,7 @@ static void read_object_list_from_stdin(void) if (feof(stdin)) break; if (!ferror(stdin)) - die("BUG: fgets returned NULL, not EOF, not error!"); + BUG("fgets returned NULL, not EOF, not error!"); if (errno != EINTR) die_errno("fgets"); clearerr(stdin); @@ -4148,11 +4148,10 @@ int cmd_pack_objects(int argc, const char **argv, const char *prefix) read_packs_list_from_stdin(); if (rev_list_unpacked) add_unreachable_loose_objects(); - } else if (!use_internal_rev_list) + } else if (!use_internal_rev_list) { read_object_list_from_stdin(); - else { + } else { get_object_list(rp.nr, rp.v); - strvec_clear(&rp); } cleanup_preferred_base(); if (include_tag && nr_result) @@ -4162,7 +4161,7 @@ int cmd_pack_objects(int argc, const char **argv, const char *prefix) the_repository); if (non_empty && !nr_result) - return 0; + goto cleanup; if (nr_result) { trace2_region_enter("pack-objects", "prepare-pack", the_repository); @@ -4183,5 +4182,9 @@ int cmd_pack_objects(int argc, const char **argv, const char *prefix) " pack-reused %"PRIu32), written, written_delta, reused, reused_delta, reuse_packfile_objects); + +cleanup: + strvec_clear(&rp); + return 0; } diff --git a/builtin/prune.c b/builtin/prune.c index 485c9a3c56..c2bcdc07db 100644 --- a/builtin/prune.c +++ b/builtin/prune.c @@ -26,10 +26,22 @@ static int prune_tmp_file(const char *fullpath) return error("Could not stat '%s'", fullpath); if (st.st_mtime > expire) return 0; - if (show_only || verbose) - printf("Removing stale temporary file %s\n", fullpath); - if (!show_only) - unlink_or_warn(fullpath); + if (S_ISDIR(st.st_mode)) { + if (show_only || verbose) + printf("Removing stale temporary directory %s\n", fullpath); + if (!show_only) { + struct strbuf remove_dir_buf = STRBUF_INIT; + + strbuf_addstr(&remove_dir_buf, fullpath); + remove_dir_recursively(&remove_dir_buf, 0); + strbuf_release(&remove_dir_buf); + } + } else { + if (show_only || verbose) + printf("Removing stale temporary file %s\n", fullpath); + if (!show_only) + unlink_or_warn(fullpath); + } return 0; } diff --git a/builtin/pull.c b/builtin/pull.c index 1cfaf9f343..100cbf9fb8 100644 --- a/builtin/pull.c +++ b/builtin/pull.c @@ -970,7 +970,7 @@ static void show_advice_pull_non_ff(void) "You can do so by running one of the following commands sometime before\n" "your next pull:\n" "\n" - " git config pull.rebase false # merge (the default strategy)\n" + " git config pull.rebase false # merge\n" " git config pull.rebase true # rebase\n" " git config pull.ff only # fast-forward only\n" "\n" @@ -994,6 +994,8 @@ int cmd_pull(int argc, const char **argv, const char *prefix) set_reflog_message(argc, argv); git_config(git_pull_config, NULL); + prepare_repo_settings(the_repository); + the_repository->settings.command_requires_full_index = 0; argc = parse_options(argc, argv, prefix, pull_options, pull_usage, 0); diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c index 49b846d960..9f4a0b816c 100644 --- a/builtin/receive-pack.c +++ b/builtin/receive-pack.c @@ -175,7 +175,7 @@ static int receive_pack_config(const char *var, const char *value, void *cb) strbuf_addf(&fsck_msg_types, "%c%s=%s", fsck_msg_types.len ? ',' : '=', var, value); else - warning("Skipping unknown msg id '%s'", var); + warning("skipping unknown msg id '%s'", var); return 0; } @@ -769,8 +769,10 @@ static void prepare_push_cert_sha1(struct child_process *proc) memset(&sigcheck, '\0', sizeof(sigcheck)); bogs = parse_signed_buffer(push_cert.buf, push_cert.len); - check_signature(push_cert.buf, bogs, push_cert.buf + bogs, - push_cert.len - bogs, &sigcheck); + sigcheck.payload = xmemdupz(push_cert.buf, bogs); + sigcheck.payload_len = bogs; + check_signature(&sigcheck, push_cert.buf + bogs, + push_cert.len - bogs); nonce_status = check_nonce(push_cert.buf, bogs); } @@ -812,16 +814,13 @@ static int run_and_feed_hook(const char *hook_name, feed_fn feed, { struct child_process proc = CHILD_PROCESS_INIT; struct async muxer; - const char *argv[2]; int code; + const char *hook_path = find_hook(hook_name); - argv[0] = find_hook(hook_name); - if (!argv[0]) + if (!hook_path) return 0; - argv[1] = NULL; - - proc.argv = argv; + strvec_push(&proc.args, hook_path); proc.in = -1; proc.stdout_to_stderr = 1; proc.trace2_hook_name = hook_name; @@ -943,23 +942,21 @@ static int run_receive_hook(struct command *commands, static int run_update_hook(struct command *cmd) { - const char *argv[5]; struct child_process proc = CHILD_PROCESS_INIT; int code; + const char *hook_path = find_hook("update"); - argv[0] = find_hook("update"); - if (!argv[0]) + if (!hook_path) return 0; - argv[1] = cmd->ref_name; - argv[2] = oid_to_hex(&cmd->old_oid); - argv[3] = oid_to_hex(&cmd->new_oid); - argv[4] = NULL; + strvec_push(&proc.args, hook_path); + strvec_push(&proc.args, cmd->ref_name); + strvec_push(&proc.args, oid_to_hex(&cmd->old_oid)); + strvec_push(&proc.args, oid_to_hex(&cmd->new_oid)); proc.no_stdin = 1; proc.stdout_to_stderr = 1; proc.err = use_sideband ? -1 : 0; - proc.argv = argv; proc.trace2_hook_name = "update"; code = start_command(&proc); @@ -1117,22 +1114,20 @@ static int run_proc_receive_hook(struct command *commands, struct child_process proc = CHILD_PROCESS_INIT; struct async muxer; struct command *cmd; - const char *argv[2]; struct packet_reader reader; struct strbuf cap = STRBUF_INIT; struct strbuf errmsg = STRBUF_INIT; int hook_use_push_options = 0; int version = 0; int code; + const char *hook_path = find_hook("proc-receive"); - argv[0] = find_hook("proc-receive"); - if (!argv[0]) { + if (!hook_path) { rp_error("cannot find hook 'proc-receive'"); return -1; } - argv[1] = NULL; - proc.argv = argv; + strvec_push(&proc.args, hook_path); proc.in = -1; proc.out = -1; proc.trace2_hook_name = "proc-receive"; @@ -1370,23 +1365,11 @@ static const char *push_to_deploy(unsigned char *sha1, struct strvec *env, const char *work_tree) { - const char *update_refresh[] = { - "update-index", "-q", "--ignore-submodules", "--refresh", NULL - }; - const char *diff_files[] = { - "diff-files", "--quiet", "--ignore-submodules", "--", NULL - }; - const char *diff_index[] = { - "diff-index", "--quiet", "--cached", "--ignore-submodules", - NULL, "--", NULL - }; - const char *read_tree[] = { - "read-tree", "-u", "-m", NULL, NULL - }; struct child_process child = CHILD_PROCESS_INIT; - child.argv = update_refresh; - child.env = env->v; + strvec_pushl(&child.args, "update-index", "-q", "--ignore-submodules", + "--refresh", NULL); + strvec_pushv(&child.env_array, env->v); child.dir = work_tree; child.no_stdin = 1; child.stdout_to_stderr = 1; @@ -1396,8 +1379,9 @@ static const char *push_to_deploy(unsigned char *sha1, /* run_command() does not clean up completely; reinitialize */ child_process_init(&child); - child.argv = diff_files; - child.env = env->v; + strvec_pushl(&child.args, "diff-files", "--quiet", + "--ignore-submodules", "--", NULL); + strvec_pushv(&child.env_array, env->v); child.dir = work_tree; child.no_stdin = 1; child.stdout_to_stderr = 1; @@ -1405,12 +1389,13 @@ static const char *push_to_deploy(unsigned char *sha1, if (run_command(&child)) return "Working directory has unstaged changes"; - /* diff-index with either HEAD or an empty tree */ - diff_index[4] = head_has_history() ? "HEAD" : empty_tree_oid_hex(); - child_process_init(&child); - child.argv = diff_index; - child.env = env->v; + strvec_pushl(&child.args, "diff-index", "--quiet", "--cached", + "--ignore-submodules", + /* diff-index with either HEAD or an empty tree */ + head_has_history() ? "HEAD" : empty_tree_oid_hex(), + "--", NULL); + strvec_pushv(&child.env_array, env->v); child.no_stdin = 1; child.no_stdout = 1; child.stdout_to_stderr = 0; @@ -1418,10 +1403,10 @@ static const char *push_to_deploy(unsigned char *sha1, if (run_command(&child)) return "Working directory has staged changes"; - read_tree[3] = hash_to_hex(sha1); child_process_init(&child); - child.argv = read_tree; - child.env = env->v; + strvec_pushl(&child.args, "read-tree", "-u", "-m", hash_to_hex(sha1), + NULL); + strvec_pushv(&child.env_array, env->v); child.dir = work_tree; child.no_stdin = 1; child.no_stdout = 1; @@ -1449,29 +1434,22 @@ static const char *push_to_checkout(unsigned char *hash, static const char *update_worktree(unsigned char *sha1, const struct worktree *worktree) { - const char *retval, *work_tree, *git_dir = NULL; + const char *retval, *git_dir; struct strvec env = STRVEC_INIT; - if (worktree && worktree->path) - work_tree = worktree->path; - else if (git_work_tree_cfg) - work_tree = git_work_tree_cfg; - else - work_tree = ".."; + if (!worktree || !worktree->path) + BUG("worktree->path must be non-NULL"); - if (is_bare_repository()) + if (worktree->is_bare) return "denyCurrentBranch = updateInstead needs a worktree"; - if (worktree) - git_dir = get_worktree_git_dir(worktree); - if (!git_dir) - git_dir = get_git_dir(); + git_dir = get_worktree_git_dir(worktree); strvec_pushf(&env, "GIT_DIR=%s", absolute_path(git_dir)); if (!hook_exists(push_to_checkout_hook)) - retval = push_to_deploy(sha1, &env, work_tree); + retval = push_to_deploy(sha1, &env, worktree->path); else - retval = push_to_checkout(sha1, &env, work_tree); + retval = push_to_checkout(sha1, &env, worktree->path); strvec_clear(&env); return retval; @@ -1486,19 +1464,22 @@ static const char *update(struct command *cmd, struct shallow_info *si) struct object_id *old_oid = &cmd->old_oid; struct object_id *new_oid = &cmd->new_oid; int do_update_worktree = 0; - const struct worktree *worktree = is_bare_repository() ? NULL : find_shared_symref("HEAD", name); + struct worktree **worktrees = get_worktrees(); + const struct worktree *worktree = + find_shared_symref(worktrees, "HEAD", name); /* only refs/... are allowed */ if (!starts_with(name, "refs/") || check_refname_format(name + 5, 0)) { rp_error("refusing to create funny ref '%s' remotely", name); - return "funny refname"; + ret = "funny refname"; + goto out; } strbuf_addf(&namespaced_name_buf, "%s%s", get_git_namespace(), name); free(namespaced_name); namespaced_name = strbuf_detach(&namespaced_name_buf, NULL); - if (worktree) { + if (worktree && !worktree->is_bare) { switch (deny_current_branch) { case DENY_IGNORE: break; @@ -1510,7 +1491,8 @@ static const char *update(struct command *cmd, struct shallow_info *si) rp_error("refusing to update checked out branch: %s", name); if (deny_current_branch == DENY_UNCONFIGURED) refuse_unconfigured_deny(); - return "branch is currently checked out"; + ret = "branch is currently checked out"; + goto out; case DENY_UPDATE_INSTEAD: /* pass -- let other checks intervene first */ do_update_worktree = 1; @@ -1521,13 +1503,15 @@ static const char *update(struct command *cmd, struct shallow_info *si) if (!is_null_oid(new_oid) && !has_object_file(new_oid)) { error("unpack should have generated %s, " "but I can't find it!", oid_to_hex(new_oid)); - return "bad pack"; + ret = "bad pack"; + goto out; } if (!is_null_oid(old_oid) && is_null_oid(new_oid)) { if (deny_deletes && starts_with(name, "refs/heads/")) { rp_error("denying ref deletion for %s", name); - return "deletion prohibited"; + ret = "deletion prohibited"; + goto out; } if (worktree || (head_name && !strcmp(namespaced_name, head_name))) { @@ -1543,9 +1527,11 @@ static const char *update(struct command *cmd, struct shallow_info *si) if (deny_delete_current == DENY_UNCONFIGURED) refuse_unconfigured_deny_delete_current(); rp_error("refusing to delete the current branch: %s", name); - return "deletion of the current branch prohibited"; + ret = "deletion of the current branch prohibited"; + goto out; default: - return "Invalid denyDeleteCurrent setting"; + ret = "Invalid denyDeleteCurrent setting"; + goto out; } } } @@ -1563,25 +1549,28 @@ static const char *update(struct command *cmd, struct shallow_info *si) old_object->type != OBJ_COMMIT || new_object->type != OBJ_COMMIT) { error("bad sha1 objects for %s", name); - return "bad ref"; + ret = "bad ref"; + goto out; } old_commit = (struct commit *)old_object; new_commit = (struct commit *)new_object; if (!in_merge_bases(old_commit, new_commit)) { rp_error("denying non-fast-forward %s" " (you should pull first)", name); - return "non-fast-forward"; + ret = "non-fast-forward"; + goto out; } } if (run_update_hook(cmd)) { rp_error("hook declined to update %s", name); - return "hook declined"; + ret = "hook declined"; + goto out; } if (do_update_worktree) { - ret = update_worktree(new_oid->hash, find_shared_symref("HEAD", name)); + ret = update_worktree(new_oid->hash, worktree); if (ret) - return ret; + goto out; } if (is_null_oid(new_oid)) { @@ -1589,9 +1578,9 @@ static const char *update(struct command *cmd, struct shallow_info *si) if (!parse_object(the_repository, old_oid)) { old_oid = NULL; if (ref_exists(name)) { - rp_warning("Allowing deletion of corrupt ref."); + rp_warning("allowing deletion of corrupt ref"); } else { - rp_warning("Deleting a non-existent ref."); + rp_warning("deleting a non-existent ref"); cmd->did_not_exist = 1; } } @@ -1600,17 +1589,19 @@ static const char *update(struct command *cmd, struct shallow_info *si) old_oid, 0, "push", &err)) { rp_error("%s", err.buf); - strbuf_release(&err); - return "failed to delete"; + ret = "failed to delete"; + } else { + ret = NULL; /* good */ } strbuf_release(&err); - return NULL; /* good */ } else { struct strbuf err = STRBUF_INIT; if (shallow_update && si->shallow_ref[cmd->index] && - update_shallow_ref(cmd, si)) - return "shallow error"; + update_shallow_ref(cmd, si)) { + ret = "shallow error"; + goto out; + } if (ref_transaction_update(transaction, namespaced_name, @@ -1618,14 +1609,16 @@ static const char *update(struct command *cmd, struct shallow_info *si) 0, "push", &err)) { rp_error("%s", err.buf); - strbuf_release(&err); - - return "failed to update ref"; + ret = "failed to update ref"; + } else { + ret = NULL; /* good */ } strbuf_release(&err); - - return NULL; /* good */ } + +out: + free_worktrees(worktrees); + return ret; } static void run_update_post_hook(struct command *commands) @@ -2213,13 +2206,14 @@ static const char *unpack(int err_fd, struct shallow_info *si) strvec_push(&child.args, alt_shallow_file); } - tmp_objdir = tmp_objdir_create(); + tmp_objdir = tmp_objdir_create("incoming"); if (!tmp_objdir) { if (err_fd > 0) close(err_fd); return "unable to create temporary object directory"; } - child.env = tmp_objdir_env(tmp_objdir); + if (tmp_objdir) + strvec_pushv(&child.env_array, tmp_objdir_env(tmp_objdir)); /* * Normally we just pass the tmp_objdir environment to the child @@ -2490,9 +2484,9 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix) argc = parse_options(argc, argv, prefix, options, receive_pack_usage, 0); if (argc > 1) - usage_msg_opt(_("Too many arguments."), receive_pack_usage, options); + usage_msg_opt(_("too many arguments"), receive_pack_usage, options); if (argc == 0) - usage_msg_opt(_("You must specify a directory."), receive_pack_usage, options); + usage_msg_opt(_("you must specify a directory"), receive_pack_usage, options); service_dir = argv[0]; @@ -2566,25 +2560,25 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix) &push_options); if (pack_lockfile) unlink_or_warn(pack_lockfile); + sigchain_push(SIGPIPE, SIG_IGN); if (report_status_v2) report_v2(commands, unpack_status); else if (report_status) report(commands, unpack_status); + sigchain_pop(SIGPIPE); run_receive_hook(commands, "post-receive", 1, &push_options); run_update_post_hook(commands); string_list_clear(&push_options, 0); if (auto_gc) { - const char *argv_gc_auto[] = { - "gc", "--auto", "--quiet", NULL, - }; struct child_process proc = CHILD_PROCESS_INIT; proc.no_stdin = 1; proc.stdout_to_stderr = 1; proc.err = use_sideband ? -1 : 0; proc.git_cmd = proc.close_object_store = 1; - proc.argv = argv_gc_auto; + strvec_pushl(&proc.args, "gc", "--auto", "--quiet", + NULL); if (!start_command(&proc)) { if (use_sideband) diff --git a/builtin/repack.c b/builtin/repack.c index 0b2d1e5d82..c393a5db77 100644 --- a/builtin/repack.c +++ b/builtin/repack.c @@ -258,9 +258,11 @@ static void repack_promisor_objects(const struct pack_objects_args *args, for_each_packed_object(write_oid, &cmd, FOR_EACH_OBJECT_PROMISOR_ONLY); - if (cmd.in == -1) + if (cmd.in == -1) { /* No packed objects; cmd was never started */ + child_process_clear(&cmd); return; + } close(cmd.in); @@ -610,7 +612,7 @@ int cmd_repack(int argc, const char **argv, const char *prefix) struct tempfile *refs_snapshot = NULL; int i, ext, ret; FILE *out; - int show_progress = isatty(2); + int show_progress; /* variables to be filled by option parsing */ int pack_everything = 0; @@ -691,7 +693,7 @@ int cmd_repack(int argc, const char **argv, const char *prefix) write_bitmaps = 0; } if (pack_kept_objects < 0) - pack_kept_objects = write_bitmaps > 0; + pack_kept_objects = write_bitmaps > 0 && !write_midx; if (write_bitmaps && !(pack_everything & ALL_INTO_ONE) && !write_midx) die(_(incremental_bitmap_conflict_error)); @@ -723,6 +725,8 @@ int cmd_repack(int argc, const char **argv, const char *prefix) prepare_pack_objects(&cmd, &po_args); + show_progress = !po_args.quiet && isatty(2); + strvec_push(&cmd.args, "--keep-true-parents"); if (!pack_kept_objects) strvec_push(&cmd.args, "--honor-pack-keep"); @@ -842,7 +846,7 @@ int cmd_repack(int argc, const char **argv, const char *prefix) fname_old = mkpathdup("%s-%s%s", packtmp, item->string, exts[ext].name); - if (((uintptr_t)item->util) & (1 << ext)) { + if (((uintptr_t)item->util) & ((uintptr_t)1 << ext)) { struct stat statbuffer; if (!stat(fname_old, &statbuffer)) { statbuffer.st_mode &= ~(S_IWUSR | S_IWGRP | S_IWOTH); @@ -924,7 +928,7 @@ int cmd_repack(int argc, const char **argv, const char *prefix) } strbuf_release(&buf); } - if (!po_args.quiet && show_progress) + if (show_progress) opts |= PRUNE_PACKED_VERBOSE; prune_packed_objects(opts); diff --git a/builtin/replace.c b/builtin/replace.c index 946938d011..6ff1734d58 100644 --- a/builtin/replace.c +++ b/builtin/replace.c @@ -258,11 +258,10 @@ static int import_object(struct object_id *oid, enum object_type type, return error_errno(_("unable to open %s for reading"), filename); if (!raw && type == OBJ_TREE) { - const char *argv[] = { "mktree", NULL }; struct child_process cmd = CHILD_PROCESS_INIT; struct strbuf result = STRBUF_INIT; - cmd.argv = argv; + strvec_push(&cmd.args, "mktree"); cmd.git_cmd = 1; cmd.in = fd; cmd.out = -1; diff --git a/builtin/reset.c b/builtin/reset.c index 7393595349..b1ff699b43 100644 --- a/builtin/reset.c +++ b/builtin/reset.c @@ -25,6 +25,7 @@ #include "cache-tree.h" #include "submodule.h" #include "submodule-config.h" +#include "dir.h" #define REFRESH_INDEX_DELAY_WARNING_IN_MS (2 * 1000) @@ -136,21 +137,36 @@ static void update_index_from_diff(struct diff_queue_struct *q, int intent_to_add = *(int *)data; for (i = 0; i < q->nr; i++) { + int pos; struct diff_filespec *one = q->queue[i]->one; - int is_missing = !(one->mode && !is_null_oid(&one->oid)); + int is_in_reset_tree = one->mode && !is_null_oid(&one->oid); struct cache_entry *ce; - if (is_missing && !intent_to_add) { + if (!is_in_reset_tree && !intent_to_add) { remove_file_from_cache(one->path); continue; } ce = make_cache_entry(&the_index, one->mode, &one->oid, one->path, 0, 0); + + /* + * If the file 1) corresponds to an existing index entry with + * skip-worktree set, or 2) does not exist in the index but is + * outside the sparse checkout definition, add a skip-worktree bit + * to the new index entry. Note that a sparse index will be expanded + * if this entry is outside the sparse cone - this is necessary + * to properly construct the reset sparse directory. + */ + pos = cache_name_pos(one->path, strlen(one->path)); + if ((pos >= 0 && ce_skip_worktree(active_cache[pos])) || + (pos < 0 && !path_in_sparse_checkout(one->path, &the_index))) + ce->ce_flags |= CE_SKIP_WORKTREE; + if (!ce) die(_("make_cache_entry failed for path '%s'"), one->path); - if (is_missing) { + if (!is_in_reset_tree) { ce->ce_flags |= CE_INTENT_TO_ADD; set_object_name_for_intent_to_add_entry(ce); } @@ -158,6 +174,82 @@ static void update_index_from_diff(struct diff_queue_struct *q, } } +static int pathspec_needs_expanded_index(const struct pathspec *pathspec) +{ + unsigned int i, pos; + int res = 0; + char *skip_worktree_seen = NULL; + + /* + * When using a magic pathspec, assume for the sake of simplicity that + * the index needs to be expanded to match all matchable files. + */ + if (pathspec->magic) + return 1; + + for (i = 0; i < pathspec->nr; i++) { + struct pathspec_item item = pathspec->items[i]; + + /* + * If the pathspec item has a wildcard, the index should be expanded + * if the pathspec has the possibility of matching a subset of entries inside + * of a sparse directory (but not the entire directory). + * + * If the pathspec item is a literal path, the index only needs to be expanded + * if a) the pathspec isn't in the sparse checkout cone (to make sure we don't + * expand for in-cone files) and b) it doesn't match any sparse directories + * (since we can reset whole sparse directories without expanding them). + */ + if (item.nowildcard_len < item.len) { + /* + * Special case: if the pattern is a path inside the cone + * followed by only wildcards, the pattern cannot match + * partial sparse directories, so we don't expand the index. + */ + if (path_in_cone_mode_sparse_checkout(item.original, &the_index) && + strspn(item.original + item.nowildcard_len, "*") == item.len - item.nowildcard_len) + continue; + + for (pos = 0; pos < active_nr; pos++) { + struct cache_entry *ce = active_cache[pos]; + + if (!S_ISSPARSEDIR(ce->ce_mode)) + continue; + + /* + * If the pre-wildcard length is longer than the sparse + * directory name and the sparse directory is the first + * component of the pathspec, need to expand the index. + */ + if (item.nowildcard_len > ce_namelen(ce) && + !strncmp(item.original, ce->name, ce_namelen(ce))) { + res = 1; + break; + } + + /* + * If the pre-wildcard length is shorter than the sparse + * directory and the pathspec does not match the whole + * directory, need to expand the index. + */ + if (!strncmp(item.original, ce->name, item.nowildcard_len) && + wildmatch(item.original, ce->name, 0)) { + res = 1; + break; + } + } + } else if (!path_in_cone_mode_sparse_checkout(item.original, &the_index) && + !matches_skip_worktree(pathspec, i, &skip_worktree_seen)) + res = 1; + + if (res > 0) + break; + } + + free(skip_worktree_seen); + return res; +} + static int read_from_tree(const struct pathspec *pathspec, struct object_id *tree_oid, int intent_to_add) @@ -170,7 +262,13 @@ static int read_from_tree(const struct pathspec *pathspec, opt.format_callback = update_index_from_diff; opt.format_callback_data = &intent_to_add; opt.flags.override_submodule_config = 1; + opt.flags.recursive = 1; opt.repo = the_repository; + opt.change = diff_change; + opt.add_remove = diff_addremove; + + if (pathspec->nr && the_index.sparse_index && pathspec_needs_expanded_index(pathspec)) + ensure_full_index(&the_index); if (do_diff_cache(tree_oid, &opt)) return 1; @@ -249,9 +347,6 @@ static void parse_args(struct pathspec *pathspec, } *rev_ret = rev; - if (read_cache() < 0) - die(_("index file corrupt")); - parse_pathspec(pathspec, 0, PATHSPEC_PREFER_FULL | (patch_mode ? PATHSPEC_PREFIX_ORIGIN : 0), @@ -397,6 +492,12 @@ int cmd_reset(int argc, const char **argv, const char *prefix) if (intent_to_add && reset_type != MIXED) die(_("-N can only be used with --mixed")); + prepare_repo_settings(the_repository); + the_repository->settings.command_requires_full_index = 0; + + if (read_cache() < 0) + die(_("index file corrupt")); + /* Soft reset does not touch the index file nor the working tree * at all, but requires them in a good order. Other resets reset * the index file to the tree object we are switching to. */ diff --git a/builtin/rm.c b/builtin/rm.c index 3d0967cdc1..b4132e5d8e 100644 --- a/builtin/rm.c +++ b/builtin/rm.c @@ -399,12 +399,13 @@ int cmd_rm(int argc, const char **argv, const char *prefix) if (!index_only) { int removed = 0, gitmodules_modified = 0; struct strbuf buf = STRBUF_INIT; + int flag = force ? REMOVE_DIR_PURGE_ORIGINAL_CWD : 0; for (i = 0; i < list.nr; i++) { const char *path = list.entry[i].name; if (list.entry[i].is_submodule) { strbuf_reset(&buf); strbuf_addstr(&buf, path); - if (remove_dir_recursively(&buf, 0)) + if (remove_dir_recursively(&buf, flag)) die(_("could not remove '%s'"), path); removed = 1; diff --git a/builtin/show-branch.c b/builtin/show-branch.c index 082449293b..f1e8318592 100644 --- a/builtin/show-branch.c +++ b/builtin/show-branch.c @@ -761,6 +761,7 @@ int cmd_show_branch(int ac, const char **av, const char *prefix) char *logmsg; char *nth_desc; const char *msg; + char *end; timestamp_t timestamp; int tz; @@ -770,11 +771,12 @@ int cmd_show_branch(int ac, const char **av, const char *prefix) reflog = i; break; } - msg = strchr(logmsg, '\t'); - if (!msg) - msg = "(none)"; - else - msg++; + + end = strchr(logmsg, '\n'); + if (end) + *end = '\0'; + + msg = (*logmsg == '\0') ? "(none)" : logmsg; reflog_msg[i] = xstrfmt("(%s) %s", show_date(timestamp, tz, DATE_MODE(RELATIVE)), diff --git a/builtin/sparse-checkout.c b/builtin/sparse-checkout.c index d0f5c4702b..679c107036 100644 --- a/builtin/sparse-checkout.c +++ b/builtin/sparse-checkout.c @@ -56,6 +56,9 @@ static int sparse_checkout_list(int argc, const char **argv) char *sparse_filename; int res; + if (!core_apply_sparse_checkout) + die(_("this worktree is not sparse")); + argc = parse_options(argc, argv, NULL, builtin_sparse_checkout_list_options, builtin_sparse_checkout_list_usage, 0); @@ -380,6 +383,41 @@ static int set_config(enum sparse_checkout_mode mode) return 0; } +static int update_modes(int *cone_mode, int *sparse_index) +{ + int mode, record_mode; + + /* Determine if we need to record the mode; ensure sparse checkout on */ + record_mode = (*cone_mode != -1) || !core_apply_sparse_checkout; + + /* If not specified, use previous definition of cone mode */ + if (*cone_mode == -1 && core_apply_sparse_checkout) + *cone_mode = core_sparse_checkout_cone; + + /* Set cone/non-cone mode appropriately */ + core_apply_sparse_checkout = 1; + if (*cone_mode == 1) { + mode = MODE_CONE_PATTERNS; + core_sparse_checkout_cone = 1; + } else { + mode = MODE_ALL_PATTERNS; + } + if (record_mode && set_config(mode)) + return 1; + + /* Set sparse-index/non-sparse-index mode if specified */ + if (*sparse_index >= 0) { + if (set_sparse_index_config(the_repository, *sparse_index) < 0) + die(_("failed to modify sparse-index config")); + + /* force an index rewrite */ + repo_read_index(the_repository); + the_repository->index->updated_workdir = 1; + } + + return 0; +} + static char const * const builtin_sparse_checkout_init_usage[] = { N_("git sparse-checkout init [--cone] [--[no-]sparse-index]"), NULL @@ -396,7 +434,6 @@ static int sparse_checkout_init(int argc, const char **argv) char *sparse_filename; int res; struct object_id oid; - int mode; struct strbuf pattern = STRBUF_INIT; static struct option builtin_sparse_checkout_init_options[] = { @@ -409,19 +446,14 @@ static int sparse_checkout_init(int argc, const char **argv) repo_read_index(the_repository); + init_opts.cone_mode = -1; init_opts.sparse_index = -1; argc = parse_options(argc, argv, NULL, builtin_sparse_checkout_init_options, builtin_sparse_checkout_init_usage, 0); - if (init_opts.cone_mode) { - mode = MODE_CONE_PATTERNS; - core_sparse_checkout_cone = 1; - } else - mode = MODE_ALL_PATTERNS; - - if (set_config(mode)) + if (update_modes(&init_opts.cone_mode, &init_opts.sparse_index)) return 1; memset(&pl, 0, sizeof(pl)); @@ -429,17 +461,6 @@ static int sparse_checkout_init(int argc, const char **argv) sparse_filename = get_sparse_checkout_filename(); res = add_patterns_from_file_to_list(sparse_filename, "", 0, &pl, NULL, 0); - if (init_opts.sparse_index >= 0) { - if (set_sparse_index_config(the_repository, init_opts.sparse_index) < 0) - die(_("failed to modify sparse-index config")); - - /* force an index rewrite */ - repo_read_index(the_repository); - the_repository->index->updated_workdir = 1; - } - - core_apply_sparse_checkout = 1; - /* If we already have a sparse-checkout file, use it. */ if (res >= 0) { free(sparse_filename); @@ -483,7 +504,7 @@ static void insert_recursive_pattern(struct pattern_list *pl, struct strbuf *pat char *oldpattern = e->pattern; size_t newlen; - if (slash == e->pattern) + if (!slash || slash == e->pattern) break; newlen = slash - e->pattern; @@ -515,17 +536,9 @@ static void strbuf_to_cone_pattern(struct strbuf *line, struct pattern_list *pl) insert_recursive_pattern(pl, line); } -static char const * const builtin_sparse_checkout_set_usage[] = { - N_("git sparse-checkout (set|add) (--stdin | <patterns>)"), - NULL -}; - -static struct sparse_checkout_set_opts { - int use_stdin; -} set_opts; - static void add_patterns_from_input(struct pattern_list *pl, - int argc, const char **argv) + int argc, const char **argv, + int use_stdin) { int i; if (core_sparse_checkout_cone) { @@ -535,7 +548,7 @@ static void add_patterns_from_input(struct pattern_list *pl, hashmap_init(&pl->parent_hashmap, pl_hashmap_cmp, NULL, 0); pl->use_cone_patterns = 1; - if (set_opts.use_stdin) { + if (use_stdin) { struct strbuf unquoted = STRBUF_INIT; while (!strbuf_getline(&line, stdin)) { if (line.buf[0] == '"') { @@ -559,7 +572,7 @@ static void add_patterns_from_input(struct pattern_list *pl, } } } else { - if (set_opts.use_stdin) { + if (use_stdin) { struct strbuf line = STRBUF_INIT; while (!strbuf_getline(&line, stdin)) { @@ -580,7 +593,8 @@ enum modify_type { }; static void add_patterns_cone_mode(int argc, const char **argv, - struct pattern_list *pl) + struct pattern_list *pl, + int use_stdin) { struct strbuf buffer = STRBUF_INIT; struct pattern_entry *pe; @@ -588,7 +602,7 @@ static void add_patterns_cone_mode(int argc, const char **argv, struct pattern_list existing; char *sparse_filename = get_sparse_checkout_filename(); - add_patterns_from_input(pl, argc, argv); + add_patterns_from_input(pl, argc, argv, use_stdin); memset(&existing, 0, sizeof(existing)); existing.use_cone_patterns = core_sparse_checkout_cone; @@ -598,6 +612,9 @@ static void add_patterns_cone_mode(int argc, const char **argv, die(_("unable to load existing sparse-checkout patterns")); free(sparse_filename); + if (!existing.use_cone_patterns) + die(_("existing sparse-checkout patterns do not use cone mode")); + hashmap_for_each_entry(&existing.recursive_hashmap, &iter, pe, ent) { if (!hashmap_contains_parent(&pl->recursive_hashmap, pe->pattern, &buffer) || @@ -614,17 +631,19 @@ static void add_patterns_cone_mode(int argc, const char **argv, } static void add_patterns_literal(int argc, const char **argv, - struct pattern_list *pl) + struct pattern_list *pl, + int use_stdin) { char *sparse_filename = get_sparse_checkout_filename(); if (add_patterns_from_file_to_list(sparse_filename, "", 0, pl, NULL, 0)) die(_("unable to load existing sparse-checkout patterns")); free(sparse_filename); - add_patterns_from_input(pl, argc, argv); + add_patterns_from_input(pl, argc, argv, use_stdin); } -static int modify_pattern_list(int argc, const char **argv, enum modify_type m) +static int modify_pattern_list(int argc, const char **argv, int use_stdin, + enum modify_type m) { int result; int changed_config = 0; @@ -633,13 +652,13 @@ static int modify_pattern_list(int argc, const char **argv, enum modify_type m) switch (m) { case ADD: if (core_sparse_checkout_cone) - add_patterns_cone_mode(argc, argv, pl); + add_patterns_cone_mode(argc, argv, pl, use_stdin); else - add_patterns_literal(argc, argv, pl); + add_patterns_literal(argc, argv, pl, use_stdin); break; case REPLACE: - add_patterns_from_input(pl, argc, argv); + add_patterns_from_input(pl, argc, argv, use_stdin); break; } @@ -659,41 +678,124 @@ static int modify_pattern_list(int argc, const char **argv, enum modify_type m) return result; } -static int sparse_checkout_set(int argc, const char **argv, const char *prefix, - enum modify_type m) +static char const * const builtin_sparse_checkout_add_usage[] = { + N_("git sparse-checkout add (--stdin | <patterns>)"), + NULL +}; + +static struct sparse_checkout_add_opts { + int use_stdin; +} add_opts; + +static int sparse_checkout_add(int argc, const char **argv, const char *prefix) { - static struct option builtin_sparse_checkout_set_options[] = { - OPT_BOOL(0, "stdin", &set_opts.use_stdin, + static struct option builtin_sparse_checkout_add_options[] = { + OPT_BOOL(0, "stdin", &add_opts.use_stdin, N_("read patterns from standard in")), OPT_END(), }; + if (!core_apply_sparse_checkout) + die(_("no sparse-checkout to add to")); + repo_read_index(the_repository); argc = parse_options(argc, argv, prefix, + builtin_sparse_checkout_add_options, + builtin_sparse_checkout_add_usage, + PARSE_OPT_KEEP_UNKNOWN); + + return modify_pattern_list(argc, argv, add_opts.use_stdin, ADD); +} + +static char const * const builtin_sparse_checkout_set_usage[] = { + N_("git sparse-checkout set [--[no-]cone] [--[no-]sparse-index] (--stdin | <patterns>)"), + NULL +}; + +static struct sparse_checkout_set_opts { + int cone_mode; + int sparse_index; + int use_stdin; +} set_opts; + +static int sparse_checkout_set(int argc, const char **argv, const char *prefix) +{ + int default_patterns_nr = 2; + const char *default_patterns[] = {"/*", "!/*/", NULL}; + + static struct option builtin_sparse_checkout_set_options[] = { + OPT_BOOL(0, "cone", &set_opts.cone_mode, + N_("initialize the sparse-checkout in cone mode")), + OPT_BOOL(0, "sparse-index", &set_opts.sparse_index, + N_("toggle the use of a sparse index")), + OPT_BOOL_F(0, "stdin", &set_opts.use_stdin, + N_("read patterns from standard in"), + PARSE_OPT_NONEG), + OPT_END(), + }; + + repo_read_index(the_repository); + + set_opts.cone_mode = -1; + set_opts.sparse_index = -1; + + argc = parse_options(argc, argv, prefix, builtin_sparse_checkout_set_options, builtin_sparse_checkout_set_usage, PARSE_OPT_KEEP_UNKNOWN); - return modify_pattern_list(argc, argv, m); + if (update_modes(&set_opts.cone_mode, &set_opts.sparse_index)) + return 1; + + /* + * Cone mode automatically specifies the toplevel directory. For + * non-cone mode, if nothing is specified, manually select just the + * top-level directory (much as 'init' would do). + */ + if (!core_sparse_checkout_cone && argc == 0) { + argv = default_patterns; + argc = default_patterns_nr; + } + + return modify_pattern_list(argc, argv, set_opts.use_stdin, REPLACE); } static char const * const builtin_sparse_checkout_reapply_usage[] = { - N_("git sparse-checkout reapply"), + N_("git sparse-checkout reapply [--[no-]cone] [--[no-]sparse-index]"), NULL }; +static struct sparse_checkout_reapply_opts { + int cone_mode; + int sparse_index; +} reapply_opts; + static int sparse_checkout_reapply(int argc, const char **argv) { static struct option builtin_sparse_checkout_reapply_options[] = { + OPT_BOOL(0, "cone", &reapply_opts.cone_mode, + N_("initialize the sparse-checkout in cone mode")), + OPT_BOOL(0, "sparse-index", &reapply_opts.sparse_index, + N_("toggle the use of a sparse index")), OPT_END(), }; + if (!core_apply_sparse_checkout) + die(_("must be in a sparse-checkout to reapply sparsity patterns")); + argc = parse_options(argc, argv, NULL, builtin_sparse_checkout_reapply_options, builtin_sparse_checkout_reapply_usage, 0); repo_read_index(the_repository); + + reapply_opts.cone_mode = -1; + reapply_opts.sparse_index = -1; + + if (update_modes(&reapply_opts.cone_mode, &reapply_opts.sparse_index)) + return 1; + return update_working_directory(NULL); } @@ -710,6 +812,17 @@ static int sparse_checkout_disable(int argc, const char **argv) struct pattern_list pl; struct strbuf match_all = STRBUF_INIT; + /* + * We do not exit early if !core_apply_sparse_checkout; due to the + * ability for users to manually muck things up between + * direct editing of .git/info/sparse-checkout + * running read-tree -m u HEAD or update-index --skip-worktree + * direct toggling of config options + * users might end up with an index with SKIP_WORKTREE bit set on + * some files and not know how to undo it. So, here we just + * forcibly return to a dense checkout regardless of initial state. + */ + argc = parse_options(argc, argv, NULL, builtin_sparse_checkout_disable_options, builtin_sparse_checkout_disable_usage, 0); @@ -758,9 +871,9 @@ int cmd_sparse_checkout(int argc, const char **argv, const char *prefix) if (!strcmp(argv[0], "init")) return sparse_checkout_init(argc, argv); if (!strcmp(argv[0], "set")) - return sparse_checkout_set(argc, argv, prefix, REPLACE); + return sparse_checkout_set(argc, argv, prefix); if (!strcmp(argv[0], "add")) - return sparse_checkout_set(argc, argv, prefix, ADD); + return sparse_checkout_add(argc, argv, prefix); if (!strcmp(argv[0], "reapply")) return sparse_checkout_reapply(argc, argv); if (!strcmp(argv[0], "disable")) diff --git a/builtin/stash.c b/builtin/stash.c index 26ebefaa68..6da736d718 100644 --- a/builtin/stash.c +++ b/builtin/stash.c @@ -27,11 +27,11 @@ static const char * const git_stash_usage[] = { N_("git stash ( pop | apply ) [--index] [-q|--quiet] [<stash>]"), N_("git stash branch <branchname> [<stash>]"), "git stash clear", - N_("git stash [push [-p|--patch] [-k|--[no-]keep-index] [-q|--quiet]\n" + N_("git stash [push [-p|--patch] [-S|--staged] [-k|--[no-]keep-index] [-q|--quiet]\n" " [-u|--include-untracked] [-a|--all] [-m|--message <message>]\n" " [--pathspec-from-file=<file> [--pathspec-file-nul]]\n" " [--] [<pathspec>...]]"), - N_("git stash save [-p|--patch] [-k|--[no-]keep-index] [-q|--quiet]\n" + N_("git stash save [-p|--patch] [-S|--staged] [-k|--[no-]keep-index] [-q|--quiet]\n" " [-u|--include-untracked] [-a|--all] [<message>]"), NULL }; @@ -1132,6 +1132,38 @@ done: return ret; } +static int stash_staged(struct stash_info *info, struct strbuf *out_patch, + int quiet) +{ + int ret = 0; + struct child_process cp_diff_tree = CHILD_PROCESS_INIT; + struct index_state istate = { NULL }; + + if (write_index_as_tree(&info->w_tree, &istate, the_repository->index_file, + 0, NULL)) { + ret = -1; + goto done; + } + + cp_diff_tree.git_cmd = 1; + strvec_pushl(&cp_diff_tree.args, "diff-tree", "-p", "-U1", "HEAD", + oid_to_hex(&info->w_tree), "--", NULL); + if (pipe_command(&cp_diff_tree, NULL, 0, out_patch, 0, NULL, 0)) { + ret = -1; + goto done; + } + + if (!out_patch->len) { + if (!quiet) + fprintf_ln(stderr, _("No staged changes")); + ret = 1; + } + +done: + discard_index(&istate); + return ret; +} + static int stash_patch(struct stash_info *info, const struct pathspec *ps, struct strbuf *out_patch, int quiet) { @@ -1258,7 +1290,7 @@ done: } static int do_create_stash(const struct pathspec *ps, struct strbuf *stash_msg_buf, - int include_untracked, int patch_mode, + int include_untracked, int patch_mode, int only_staged, struct stash_info *info, struct strbuf *patch, int quiet) { @@ -1337,6 +1369,16 @@ static int do_create_stash(const struct pathspec *ps, struct strbuf *stash_msg_b } else if (ret > 0) { goto done; } + } else if (only_staged) { + ret = stash_staged(info, patch, quiet); + if (ret < 0) { + if (!quiet) + fprintf_ln(stderr, _("Cannot save the current " + "staged state")); + goto done; + } else if (ret > 0) { + goto done; + } } else { if (stash_working_tree(info, ps)) { if (!quiet) @@ -1395,7 +1437,7 @@ static int create_stash(int argc, const char **argv, const char *prefix) if (!check_changes_tracked_files(&ps)) return 0; - ret = do_create_stash(&ps, &stash_msg_buf, 0, 0, &info, + ret = do_create_stash(&ps, &stash_msg_buf, 0, 0, 0, &info, NULL, 0); if (!ret) printf_ln("%s", oid_to_hex(&info.w_commit)); @@ -1405,7 +1447,7 @@ static int create_stash(int argc, const char **argv, const char *prefix) } static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int quiet, - int keep_index, int patch_mode, int include_untracked) + int keep_index, int patch_mode, int include_untracked, int only_staged) { int ret = 0; struct stash_info info; @@ -1423,6 +1465,17 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q goto done; } + /* --patch overrides --staged */ + if (patch_mode) + only_staged = 0; + + if (only_staged && include_untracked) { + fprintf_ln(stderr, _("Can't use --staged and --include-untracked" + " or --all at the same time")); + ret = -1; + goto done; + } + read_cache_preload(NULL); if (!include_untracked && ps->nr) { int i; @@ -1463,7 +1516,7 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q if (stash_msg) strbuf_addstr(&stash_msg_buf, stash_msg); - if (do_create_stash(ps, &stash_msg_buf, include_untracked, patch_mode, + if (do_create_stash(ps, &stash_msg_buf, include_untracked, patch_mode, only_staged, &info, &patch, quiet)) { ret = -1; goto done; @@ -1480,13 +1533,15 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q printf_ln(_("Saved working directory and index state %s"), stash_msg_buf.buf); - if (!patch_mode) { + if (!(patch_mode || only_staged)) { if (include_untracked && !ps->nr) { struct child_process cp = CHILD_PROCESS_INIT; cp.git_cmd = 1; + if (startup_info->original_cwd) + cp.dir = startup_info->original_cwd; strvec_pushl(&cp.args, "clean", "--force", - "--quiet", "-d", NULL); + "--quiet", "-d", ":/", NULL); if (include_untracked == INCLUDE_ALL_FILES) strvec_push(&cp.args, "-x"); if (run_command(&cp)) { @@ -1598,6 +1653,7 @@ static int push_stash(int argc, const char **argv, const char *prefix, { int force_assume = 0; int keep_index = -1; + int only_staged = 0; int patch_mode = 0; int include_untracked = 0; int quiet = 0; @@ -1608,6 +1664,8 @@ static int push_stash(int argc, const char **argv, const char *prefix, struct option options[] = { OPT_BOOL('k', "keep-index", &keep_index, N_("keep index")), + OPT_BOOL('S', "staged", &only_staged, + N_("stash staged changes only")), OPT_BOOL('p', "patch", &patch_mode, N_("stash in patch mode")), OPT__QUIET(&quiet, N_("quiet mode")), @@ -1647,6 +1705,9 @@ static int push_stash(int argc, const char **argv, const char *prefix, if (patch_mode) die(_("--pathspec-from-file is incompatible with --patch")); + if (only_staged) + die(_("--pathspec-from-file is incompatible with --staged")); + if (ps.nr) die(_("--pathspec-from-file is incompatible with pathspec arguments")); @@ -1658,12 +1719,13 @@ static int push_stash(int argc, const char **argv, const char *prefix, } return do_push_stash(&ps, stash_msg, quiet, keep_index, patch_mode, - include_untracked); + include_untracked, only_staged); } static int save_stash(int argc, const char **argv, const char *prefix) { int keep_index = -1; + int only_staged = 0; int patch_mode = 0; int include_untracked = 0; int quiet = 0; @@ -1674,6 +1736,8 @@ static int save_stash(int argc, const char **argv, const char *prefix) struct option options[] = { OPT_BOOL('k', "keep-index", &keep_index, N_("keep index")), + OPT_BOOL('S', "staged", &only_staged, + N_("stash staged changes only")), OPT_BOOL('p', "patch", &patch_mode, N_("stash in patch mode")), OPT__QUIET(&quiet, N_("quiet mode")), @@ -1695,7 +1759,7 @@ static int save_stash(int argc, const char **argv, const char *prefix) memset(&ps, 0, sizeof(ps)); ret = do_push_stash(&ps, stash_msg, quiet, keep_index, - patch_mode, include_untracked); + patch_mode, include_untracked, only_staged); strbuf_release(&stash_msg_buf); return ret; diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c index e630f0c730..9b25a508e6 100644 --- a/builtin/submodule--helper.c +++ b/builtin/submodule--helper.c @@ -1503,16 +1503,17 @@ static void deinit_submodule(const char *path, const char *prefix, struct strbuf sb_rm = STRBUF_INIT; const char *format; - /* - * protect submodules containing a .git directory - * NEEDSWORK: instead of dying, automatically call - * absorbgitdirs and (possibly) warn. - */ - if (is_directory(sub_git_dir)) - die(_("Submodule work tree '%s' contains a .git " - "directory (use 'rm -rf' if you really want " - "to remove it including all of its history)"), - displaypath); + if (is_directory(sub_git_dir)) { + if (!(flags & OPT_QUIET)) + warning(_("Submodule work tree '%s' contains a .git " + "directory. This will be replaced with a " + ".git file by using absorbgitdirs."), + displaypath); + + absorb_git_dir_into_superproject(path, + ABSORB_GITDIR_RECURSE_SUBMODULES); + + } if (!(flags & OPT_FORCE)) { struct child_process cp_rm = CHILD_PROCESS_INIT; diff --git a/builtin/tag.c b/builtin/tag.c index 6fe646710d..41863c5ab7 100644 --- a/builtin/tag.c +++ b/builtin/tag.c @@ -178,7 +178,6 @@ static const char tag_template_nocleanup[] = static int git_tag_config(const char *var, const char *value, void *cb) { int status; - struct ref_sorting **sorting_tail = (struct ref_sorting **)cb; if (!strcmp(var, "tag.gpgsign")) { config_sign_tag = git_config_bool(var, value); @@ -188,7 +187,7 @@ static int git_tag_config(const char *var, const char *value, void *cb) if (!strcmp(var, "tag.sort")) { if (!value) return config_error_nonbool(var); - parse_ref_sorting(sorting_tail, value); + string_list_append(cb, value); return 0; } @@ -436,7 +435,8 @@ int cmd_tag(int argc, const char **argv, const char *prefix) struct ref_transaction *transaction; struct strbuf err = STRBUF_INIT; struct ref_filter filter; - static struct ref_sorting *sorting = NULL, **sorting_tail = &sorting; + struct ref_sorting *sorting; + struct string_list sorting_options = STRING_LIST_INIT_DUP; struct ref_format format = REF_FORMAT_INIT; int icase = 0; int edit_flag = 0; @@ -470,7 +470,7 @@ int cmd_tag(int argc, const char **argv, const char *prefix) OPT_WITHOUT(&filter.no_commit, N_("print only tags that don't contain the commit")), OPT_MERGED(&filter, N_("print only tags that are merged")), OPT_NO_MERGED(&filter, N_("print only tags that are not merged")), - OPT_REF_SORT(sorting_tail), + OPT_REF_SORT(&sorting_options), { OPTION_CALLBACK, 0, "points-at", &filter.points_at, N_("object"), N_("print only tags of the object"), PARSE_OPT_LASTARG_DEFAULT, @@ -486,7 +486,7 @@ int cmd_tag(int argc, const char **argv, const char *prefix) setup_ref_filter_porcelain_msg(); - git_config(git_tag_config, sorting_tail); + git_config(git_tag_config, &sorting_options); memset(&opt, 0, sizeof(opt)); memset(&filter, 0, sizeof(filter)); @@ -525,8 +525,7 @@ int cmd_tag(int argc, const char **argv, const char *prefix) die(_("--column and -n are incompatible")); colopts = 0; } - if (!sorting) - sorting = ref_default_sorting(); + sorting = ref_sorting_options(&sorting_options); ref_sorting_set_sort_flags_all(sorting, REF_SORTING_ICASE, icase); filter.ignore_case = icase; if (cmdmode == 'l') { diff --git a/builtin/upload-archive.c b/builtin/upload-archive.c index 24654b4c9b..98d028dae6 100644 --- a/builtin/upload-archive.c +++ b/builtin/upload-archive.c @@ -77,7 +77,7 @@ static ssize_t process_input(int child_fd, int band) int cmd_upload_archive(int argc, const char **argv, const char *prefix) { - struct child_process writer = { argv }; + struct child_process writer = CHILD_PROCESS_INIT; if (argc == 2 && !strcmp(argv[1], "-h")) usage(upload_archive_usage); @@ -89,9 +89,10 @@ int cmd_upload_archive(int argc, const char **argv, const char *prefix) * multiplexed out to our fd#1. If the child dies, we tell the other * end over channel #3. */ - argv[0] = "upload-archive--writer"; writer.out = writer.err = -1; writer.git_cmd = 1; + strvec_push(&writer.args, "upload-archive--writer"); + strvec_pushv(&writer.args, argv + 1); if (start_command(&writer)) { int err = errno; packet_write_fmt(1, "NACK unable to spawn subprocess\n"); diff --git a/builtin/var.c b/builtin/var.c index 6c6f46b4ae..491db27429 100644 --- a/builtin/var.c +++ b/builtin/var.c @@ -5,6 +5,7 @@ */ #include "builtin.h" #include "config.h" +#include "refs.h" static const char var_usage[] = "git var (-l | <variable>)"; @@ -27,6 +28,11 @@ static const char *pager(int flag) return pgm; } +static const char *default_branch(int flag) +{ + return git_default_branch_name(1); +} + struct git_var { const char *name; const char *(*read)(int); @@ -36,6 +42,7 @@ static struct git_var git_vars[] = { { "GIT_AUTHOR_IDENT", git_author_info }, { "GIT_EDITOR", editor }, { "GIT_PAGER", pager }, + { "GIT_DEFAULT_BRANCH", default_branch }, { "", NULL }, }; diff --git a/builtin/worktree.c b/builtin/worktree.c index d22ece93e1..a396cfdc64 100644 --- a/builtin/worktree.c +++ b/builtin/worktree.c @@ -72,7 +72,7 @@ static void delete_worktrees_dir_if_empty(void) static void prune_worktree(const char *id, const char *reason) { if (show_only || verbose) - printf_ln(_("Removing %s/%s: %s"), "worktrees", id, reason); + fprintf_ln(stderr, _("Removing %s/%s: %s"), "worktrees", id, reason); if (!show_only) delete_git_dir(id); } @@ -349,18 +349,18 @@ static int add_worktree(const char *path, const char *refname, strvec_push(&cp.args, "--quiet"); } - cp.env = child_env.v; + strvec_pushv(&cp.env_array, child_env.v); ret = run_command(&cp); if (ret) goto done; if (opts->checkout) { - cp.argv = NULL; - strvec_clear(&cp.args); + struct child_process cp = CHILD_PROCESS_INIT; + cp.git_cmd = 1; strvec_pushl(&cp.args, "reset", "--hard", "--no-recurse-submodules", NULL); if (opts->quiet) strvec_push(&cp.args, "--quiet"); - cp.env = child_env.v; + strvec_pushv(&cp.env_array, child_env.v); ret = run_command(&cp); if (ret) goto done; @@ -385,12 +385,11 @@ done: const char *hook = find_hook("post-checkout"); if (hook) { const char *env[] = { "GIT_DIR", "GIT_WORK_TREE", NULL }; - cp.git_cmd = 0; + struct child_process cp = CHILD_PROCESS_INIT; cp.no_stdin = 1; cp.stdout_to_stderr = 1; cp.dir = path; - cp.env = env; - cp.argv = NULL; + strvec_pushv(&cp.env_array, env); cp.trace2_hook_name = "post-checkout"; strvec_pushl(&cp.args, absolute_path(hook), oid_to_hex(null_oid()), @@ -418,24 +417,24 @@ static void print_preparing_worktree_line(int detach, if (force_new_branch) { struct commit *commit = lookup_commit_reference_by_name(new_branch); if (!commit) - printf_ln(_("Preparing worktree (new branch '%s')"), new_branch); + fprintf_ln(stderr, _("Preparing worktree (new branch '%s')"), new_branch); else - printf_ln(_("Preparing worktree (resetting branch '%s'; was at %s)"), + fprintf_ln(stderr, _("Preparing worktree (resetting branch '%s'; was at %s)"), new_branch, find_unique_abbrev(&commit->object.oid, DEFAULT_ABBREV)); } else if (new_branch) { - printf_ln(_("Preparing worktree (new branch '%s')"), new_branch); + fprintf_ln(stderr, _("Preparing worktree (new branch '%s')"), new_branch); } else { struct strbuf s = STRBUF_INIT; if (!detach && !strbuf_check_branch_ref(&s, branch) && ref_exists(s.buf)) - printf_ln(_("Preparing worktree (checking out '%s')"), + fprintf_ln(stderr, _("Preparing worktree (checking out '%s')"), branch); else { struct commit *commit = lookup_commit_reference_by_name(branch); if (!commit) die(_("invalid reference: %s"), branch); - printf_ln(_("Preparing worktree (detached HEAD %s)"), + fprintf_ln(stderr, _("Preparing worktree (detached HEAD %s)"), find_unique_abbrev(&commit->object.oid, DEFAULT_ABBREV)); } strbuf_release(&s); @@ -1006,7 +1005,7 @@ static int remove_worktree(int ac, const char **av, const char *prefix) static void report_repair(int iserr, const char *path, const char *msg, void *cb_data) { if (!iserr) { - printf_ln(_("repair: %s: %s"), msg, path); + fprintf_ln(stderr, _("repair: %s: %s"), msg, path); } else { int *exit_status = (int *)cb_data; fprintf_ln(stderr, _("error: %s: %s"), msg, path); diff --git a/cache-tree.c b/cache-tree.c index 79d168192d..65ca993361 100644 --- a/cache-tree.c +++ b/cache-tree.c @@ -741,15 +741,26 @@ out: return ret; } +static void prime_cache_tree_sparse_dir(struct cache_tree *it, + struct tree *tree) +{ + + oidcpy(&it->oid, &tree->object.oid); + it->entry_count = 1; +} + static void prime_cache_tree_rec(struct repository *r, struct cache_tree *it, - struct tree *tree) + struct tree *tree, + struct strbuf *tree_path) { struct tree_desc desc; struct name_entry entry; int cnt; + int base_path_len = tree_path->len; oidcpy(&it->oid, &tree->object.oid); + init_tree_desc(&desc, tree->buffer, tree->size); cnt = 0; while (tree_entry(&desc, &entry)) { @@ -758,14 +769,40 @@ static void prime_cache_tree_rec(struct repository *r, else { struct cache_tree_sub *sub; struct tree *subtree = lookup_tree(r, &entry.oid); + if (!subtree->object.parsed) parse_tree(subtree); sub = cache_tree_sub(it, entry.path); sub->cache_tree = cache_tree(); - prime_cache_tree_rec(r, sub->cache_tree, subtree); + + /* + * Recursively-constructed subtree path is only needed when working + * in a sparse index (where it's used to determine whether the + * subtree is a sparse directory in the index). + */ + if (r->index->sparse_index) { + strbuf_setlen(tree_path, base_path_len); + strbuf_grow(tree_path, base_path_len + entry.pathlen + 1); + strbuf_add(tree_path, entry.path, entry.pathlen); + strbuf_addch(tree_path, '/'); + } + + /* + * If a sparse index is in use, the directory being processed may be + * sparse. To confirm that, we can check whether an entry with that + * exact name exists in the index. If it does, the created subtree + * should be sparse. Otherwise, cache tree expansion should continue + * as normal. + */ + if (r->index->sparse_index && + index_entry_exists(r->index, tree_path->buf, tree_path->len)) + prime_cache_tree_sparse_dir(sub->cache_tree, subtree); + else + prime_cache_tree_rec(r, sub->cache_tree, subtree, tree_path); cnt += sub->cache_tree->entry_count; } } + it->entry_count = cnt; } @@ -773,11 +810,14 @@ void prime_cache_tree(struct repository *r, struct index_state *istate, struct tree *tree) { + struct strbuf tree_path = STRBUF_INIT; + trace2_region_enter("cache-tree", "prime_cache_tree", the_repository); cache_tree_free(&istate->cache_tree); istate->cache_tree = cache_tree(); - prime_cache_tree_rec(r, istate->cache_tree, tree); + prime_cache_tree_rec(r, istate->cache_tree, tree, &tree_path); + strbuf_release(&tree_path); istate->cache_changed |= CACHE_TREE_CHANGED; trace2_region_leave("cache-tree", "prime_cache_tree", the_repository); } @@ -817,6 +817,16 @@ struct cache_entry *index_file_exists(struct index_state *istate, const char *na int index_name_pos(struct index_state *, const char *name, int namelen); /* + * Determines whether an entry with the given name exists within the + * given index. The return value is 1 if an exact match is found, otherwise + * it is 0. Note that, unlike index_name_pos, this function does not expand + * the index if it is sparse. If an item exists within the full index but it + * is contained within a sparse directory (and not in the sparse index), 0 is + * returned. + */ +int index_entry_exists(struct index_state *, const char *name, int namelen); + +/* * Some functions return the negative complement of an insert position when a * precise match was not found but a position was found where the entry would * need to be inserted. This helper protects that logic from any integer @@ -986,6 +996,7 @@ extern int read_replace_refs; extern char *git_replace_ref_base; extern int fsync_object_files; +extern int use_fsync; extern int core_preload_index; extern int precomposed_unicode; extern int protect_hfs; @@ -1588,6 +1599,7 @@ timestamp_t approxidate_careful(const char *, int *); timestamp_t approxidate_relative(const char *date); void parse_date_format(const char *format, struct date_mode *mode); int date_overflows(timestamp_t date); +time_t tm_to_time_t(const struct tm *tm); #define IDENT_STRICT 1 #define IDENT_NO_DATE 2 @@ -1834,8 +1846,10 @@ void overlay_tree_on_index(struct index_state *istate, struct startup_info { int have_repository; const char *prefix; + const char *original_cwd; }; extern struct startup_info *startup_info; +extern const char *tmp_original_cwd; /* merge.c */ struct commit_list; @@ -95,38 +95,6 @@ struct cb_node *cb_lookup(struct cb_tree *t, const uint8_t *k, size_t klen) return p && !memcmp(p->k, k, klen) ? p : NULL; } -struct cb_node *cb_unlink(struct cb_tree *t, const uint8_t *k, size_t klen) -{ - struct cb_node **wherep = &t->root; - struct cb_node **whereq = NULL; - struct cb_node *q = NULL; - size_t direction = 0; - uint8_t c; - struct cb_node *p = t->root; - - if (!p) return NULL; /* empty tree, nothing to delete */ - - /* traverse to find best match, keeping link to parent */ - while (1 & (uintptr_t)p) { - whereq = wherep; - q = cb_node_of(p); - c = q->byte < klen ? k[q->byte] : 0; - direction = (1 + (q->otherbits | c)) >> 8; - wherep = q->child + direction; - p = *wherep; - } - - if (memcmp(p->k, k, klen)) - return NULL; /* no match, nothing unlinked */ - - /* found an exact match */ - if (whereq) /* update parent */ - *whereq = q->child[1 - direction]; - else - t->root = NULL; - return p; -} - static enum cb_next cb_descend(struct cb_node *p, cb_iter fn, void *arg) { if (1 & (uintptr_t)p) { @@ -47,7 +47,6 @@ static inline void cb_init(struct cb_tree *t) struct cb_node *cb_lookup(struct cb_tree *, const uint8_t *k, size_t klen); struct cb_node *cb_insert(struct cb_tree *, struct cb_node *, size_t klen); -struct cb_node *cb_unlink(struct cb_tree *t, const uint8_t *k, size_t klen); typedef enum cb_next (*cb_iter)(struct cb_node *, void *arg); diff --git a/ci/check-directional-formatting.bash b/ci/check-directional-formatting.bash new file mode 100755 index 0000000000..e6211b141a --- /dev/null +++ b/ci/check-directional-formatting.bash @@ -0,0 +1,27 @@ +#!/bin/bash + +# This script verifies that the non-binary files tracked in the Git index do +# not contain any Unicode directional formatting: such formatting could be used +# to deceive reviewers into interpreting code differently from the compiler. +# This is intended to run on an Ubuntu agent in a GitHub workflow. +# +# To allow translated messages to introduce such directional formatting in the +# future, we exclude the `.po` files from this validation. +# +# Neither GNU grep nor `git grep` (not even with `-P`) handle `\u` as a way to +# specify UTF-8. +# +# To work around that, we use `printf` to produce the pattern as a byte +# sequence, and then feed that to `git grep` as a byte sequence (setting +# `LC_CTYPE` to make sure that the arguments are interpreted as intended). +# +# Note: we need to use Bash here because its `printf` interprets `\uNNNN` as +# UTF-8 code points, as desired. Running this script through Ubuntu's `dash`, +# for example, would use a `printf` that does not understand that syntax. + +# U+202a..U+2a2e: LRE, RLE, PDF, LRO and RLO +# U+2066..U+2069: LRI, RLI, FSI and PDI +regex='(\u202a|\u202b|\u202c|\u202d|\u202e|\u2066|\u2067|\u2068|\u2069)' + +! LC_CTYPE=C git grep -El "$(LC_CTYPE=C.UTF-8 printf "$regex")" \ + -- ':(exclude,attr:binary)' ':(exclude)*.po' diff --git a/ci/install-dependencies.sh b/ci/install-dependencies.sh index 1d0e48f451..dbcebad2fb 100755 --- a/ci/install-dependencies.sh +++ b/ci/install-dependencies.sh @@ -11,18 +11,11 @@ UBUNTU_COMMON_PKGS="make libssl-dev libcurl4-openssl-dev libexpat-dev tcl tk gettext zlib1g-dev perl-modules liberror-perl libauthen-sasl-perl libemail-valid-perl libio-socket-ssl-perl libnet-smtp-ssl-perl" -case "$jobname" in -linux-clang|linux-gcc|linux-leaks) - sudo apt-add-repository -y "ppa:ubuntu-toolchain-r/test" +case "$runs_on_pool" in +ubuntu-latest) sudo apt-get -q update sudo apt-get -q -y install language-pack-is libsvn-perl apache2 \ - $UBUNTU_COMMON_PKGS - case "$jobname" in - linux-gcc) - sudo apt-get -q -y install gcc-8 - ;; - esac - + $UBUNTU_COMMON_PKGS $CC_PACKAGE mkdir --parents "$P4_PATH" pushd "$P4_PATH" wget --quiet "$P4WHENCE/bin.linux26x86_64/p4d" @@ -37,7 +30,7 @@ linux-clang|linux-gcc|linux-leaks) cp git-lfs-$LINUX_GIT_LFS_VERSION/git-lfs . popd ;; -osx-clang|osx-gcc) +macos-latest) export HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 # Uncomment this if you want to run perf tests: # brew install gnu-time @@ -51,15 +44,17 @@ osx-clang|osx-gcc) brew install --cask --no-quarantine perforce } || brew install homebrew/cask/perforce - case "$jobname" in - osx-gcc) - brew install gcc@9 - # Just in case the image is updated to contain gcc@9 - # pre-installed but not linked. - brew link gcc@9 - ;; - esac + + if test -n "$CC_PACKAGE" + then + BREW_PACKAGE=${CC_PACKAGE/-/@} + brew install "$BREW_PACKAGE" + brew link "$BREW_PACKAGE" + fi ;; +esac + +case "$jobname" in StaticAnalysis) sudo apt-get -q update sudo apt-get -q -y install coccinelle libcurl4-openssl-dev libssl-dev \ @@ -77,7 +72,7 @@ Documentation) test -n "$ALREADY_HAVE_ASCIIDOCTOR" || sudo gem install --version 1.5.8 asciidoctor ;; -linux-gcc-default|linux-gcc-4.8) +linux-gcc-default) sudo apt-get -q update sudo apt-get -q -y install $UBUNTU_COMMON_PKGS ;; diff --git a/ci/install-docker-dependencies.sh b/ci/install-docker-dependencies.sh index 07a8c6b199..78b7e326da 100755 --- a/ci/install-docker-dependencies.sh +++ b/ci/install-docker-dependencies.sh @@ -4,7 +4,7 @@ # case "$jobname" in -Linux32) +linux32) linux32 --32bit i386 sh -c ' apt update >/dev/null && apt install -y build-essential libcurl4-openssl-dev \ @@ -34,7 +34,7 @@ save_good_tree () { # successfully before (e.g. because the branch got rebased, changing only # the commit messages). skip_good_tree () { - if test "$TRAVIS_DEBUG_MODE" = true || test true = "$GITHUB_ACTIONS" + if test true = "$GITHUB_ACTIONS" then return fi @@ -60,7 +60,7 @@ skip_good_tree () { cat <<-EOF $(tput setaf 2)Skipping build job for commit $CI_COMMIT.$(tput sgr0) This commit's tree has already been built and tested successfully in build job $prev_good_job_number for commit $prev_good_commit. - The log of that build job is available at $(url_for_job_id $prev_good_job_id) + The log of that build job is available at $SYSTEM_TASKDEFINITIONSURI$SYSTEM_TEAMPROJECT/_build/results?buildId=$prev_good_job_id To force a re-build delete the branch's cache and then hit 'Restart job'. EOF fi @@ -91,29 +91,7 @@ export MAKEFLAGS= # and installing dependencies. set -ex -if test true = "$TRAVIS" -then - CI_TYPE=travis - # When building a PR, TRAVIS_BRANCH refers to the *target* branch. Not - # what we want here. We want the source branch instead. - CI_BRANCH="${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH}" - CI_COMMIT="$TRAVIS_COMMIT" - CI_JOB_ID="$TRAVIS_JOB_ID" - CI_JOB_NUMBER="$TRAVIS_JOB_NUMBER" - CI_OS_NAME="$TRAVIS_OS_NAME" - CI_REPO_SLUG="$TRAVIS_REPO_SLUG" - - cache_dir="$HOME/travis-cache" - - url_for_job_id () { - echo "https://travis-ci.org/$CI_REPO_SLUG/jobs/$1" - } - - BREW_INSTALL_PACKAGES="git-lfs gettext" - export GIT_PROVE_OPTS="--timer --jobs 3 --state=failed,slow,save" - export GIT_TEST_OPTS="--verbose-log -x --immediate" - MAKEFLAGS="$MAKEFLAGS --jobs=2" -elif test -n "$SYSTEM_COLLECTIONURI" || test -n "$SYSTEM_TASKDEFINITIONSURI" +if test -n "$SYSTEM_COLLECTIONURI" || test -n "$SYSTEM_TASKDEFINITIONSURI" then CI_TYPE=azure-pipelines # We are running in Azure Pipelines @@ -130,10 +108,6 @@ then # among *all* phases) cache_dir="$HOME/test-cache/$SYSTEM_PHASENAME" - url_for_job_id () { - echo "$SYSTEM_TASKDEFINITIONSURI$SYSTEM_TEAMPROJECT/_build/results?buildId=$1" - } - export GIT_PROVE_OPTS="--timer --jobs 10 --state=failed,slow,save" export GIT_TEST_OPTS="--verbose-log -x --write-junit-xml" MAKEFLAGS="$MAKEFLAGS --jobs=10" @@ -182,11 +156,15 @@ export DEFAULT_TEST_TARGET=prove export GIT_TEST_CLONE_2GB=true export SKIP_DASHED_BUILT_INS=YesPlease -case "$jobname" in -linux-clang|linux-gcc|linux-leaks) +case "$runs_on_pool" in +ubuntu-latest) + if test "$jobname" = "linux-gcc-default" + then + break + fi + if [ "$jobname" = linux-gcc ] then - export CC=gcc-8 MAKEFLAGS="$MAKEFLAGS PYTHON_PATH=/usr/bin/python3" else MAKEFLAGS="$MAKEFLAGS PYTHON_PATH=/usr/bin/python2" @@ -206,24 +184,20 @@ linux-clang|linux-gcc|linux-leaks) GIT_LFS_PATH="$HOME/custom/git-lfs" export PATH="$GIT_LFS_PATH:$P4_PATH:$PATH" ;; -osx-clang|osx-gcc) +macos-latest) if [ "$jobname" = osx-gcc ] then - export CC=gcc-9 MAKEFLAGS="$MAKEFLAGS PYTHON_PATH=$(which python3)" else MAKEFLAGS="$MAKEFLAGS PYTHON_PATH=$(which python2)" fi - - # t9810 occasionally fails on Travis CI OS X - # t9816 occasionally fails with "TAP out of sequence errors" on - # Travis CI OS X - export GIT_SKIP_TESTS="t9810 t9816" - ;; -linux-gcc-default) ;; -Linux32) +esac + +case "$jobname" in +linux32) CC=gcc + MAKEFLAGS="$MAKEFLAGS NO_UNCOMPRESS2=1" ;; linux-musl) CC=gcc @@ -231,9 +205,6 @@ linux-musl) MAKEFLAGS="$MAKEFLAGS NO_REGEX=Yes ICONV_OMITS_BOM=Yes" MAKEFLAGS="$MAKEFLAGS GIT_TEST_UTF8_LOCALE=C.UTF-8" ;; -esac - -case "$jobname" in linux-leaks) export SANITIZE=leak export GIT_TEST_PASSING_SANITIZE_LEAK=true diff --git a/ci/print-test-failures.sh b/ci/print-test-failures.sh index c70d6cdbf2..57277eefcd 100755 --- a/ci/print-test-failures.sh +++ b/ci/print-test-failures.sh @@ -39,8 +39,6 @@ do test_name="${test_name##*/}" trash_dir="trash directory.$test_name" case "$CI_TYPE" in - travis) - ;; azure-pipelines) mkdir -p failed-test-artifacts mv "$trash_dir" failed-test-artifacts @@ -88,11 +86,3 @@ do fi fi done - -if [ $combined_trash_size -gt 0 ] -then - echo "------------------------------------------------------------------------" - echo "Trash directories embedded in this log can be extracted by running:" - echo - echo " curl https://api.travis-ci.org/v3/job/$TRAVIS_JOB_ID/log.txt |./ci/util/extract-trash-dirs.sh" -fi diff --git a/ci/run-build-and-tests.sh b/ci/run-build-and-tests.sh index cc62616d80..280dda7d28 100755 --- a/ci/run-build-and-tests.sh +++ b/ci/run-build-and-tests.sh @@ -10,16 +10,13 @@ windows*) cmd //c mklink //j t\\.prove "$(cygpath -aw "$cache_dir/.prove")";; *) ln -s "$cache_dir/.prove" t/.prove;; esac -if test "$jobname" = "pedantic" -then - export DEVOPTS=pedantic -fi +export MAKE_TARGETS="all test" -make case "$jobname" in linux-gcc) export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main - make test + ;; +linux-TEST-vars) export GIT_TEST_SPLIT_INDEX=yes export GIT_TEST_MERGE_ALGORITHM=recursive export GIT_TEST_FULL_IN_PACK_ARRAY=true @@ -33,23 +30,25 @@ linux-gcc) export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=master export GIT_TEST_WRITE_REV_INDEX=1 export GIT_TEST_CHECKOUT_WORKERS=2 - make test ;; linux-clang) export GIT_TEST_DEFAULT_HASH=sha1 - make test + ;; +linux-sha256) export GIT_TEST_DEFAULT_HASH=sha256 - make test ;; -linux-gcc-4.8|pedantic) +pedantic) # Don't run the tests; we only care about whether Git can be - # built with GCC 4.8 or with pedantic - ;; -*) - make test + # built. + export DEVOPTS=pedantic + export MAKE_TARGETS=all ;; esac +# Any new "test" targets should not go after this "make", but should +# adjust $MAKE_TARGETS. Otherwise compilation-only targets above will +# start running tests. +make $MAKE_TARGETS check_unignored_build_artifacts save_good_tree diff --git a/ci/run-docker-build.sh b/ci/run-docker-build.sh index 8d47a5fda3..6cd832efb9 100755 --- a/ci/run-docker-build.sh +++ b/ci/run-docker-build.sh @@ -15,7 +15,7 @@ then fi case "$jobname" in -Linux32) +linux32) switch_cmd="linux32 --32bit i386" ;; linux-musl) @@ -47,15 +47,6 @@ else else useradd -u $HOST_UID $CI_USER fi - - # Due to a bug the test suite was run as root in the past, so - # a prove state file created back then is only accessible by - # root. Now that bug is fixed, the test suite is run as a - # regular user, but the prove state file coming from Travis - # CI's cache might still be owned by root. - # Make sure that this user has rights to any cached files, - # including an existing prove state file. - test -n "$cache_dir" && chown -R $HOST_UID:$HOST_UID "$cache_dir" fi # Build and test diff --git a/ci/run-docker.sh b/ci/run-docker.sh index 37fa372052..af89d1624a 100755 --- a/ci/run-docker.sh +++ b/ci/run-docker.sh @@ -6,7 +6,7 @@ . ${0%/*}/lib.sh case "$jobname" in -Linux32) +linux32) CI_CONTAINER="daald/ubuntu32:xenial" ;; linux-musl) @@ -25,7 +25,7 @@ docker pull "$CI_CONTAINER" # root@container:/# export jobname=<jobname> # root@container:/# /usr/src/git/ci/run-docker-build.sh <host-user-id> -container_cache_dir=/tmp/travis-cache +container_cache_dir=/tmp/container-cache docker run \ --interactive \ @@ -40,7 +40,7 @@ struct color { enum { COLOR_UNSPECIFIED = 0, COLOR_NORMAL, - COLOR_ANSI, /* basic 0-7 ANSI colors */ + COLOR_ANSI, /* basic 0-7 ANSI colors + "default" (value = 9) */ COLOR_256, COLOR_RGB } type; @@ -83,6 +83,27 @@ static int parse_ansi_color(struct color *out, const char *name, int len) int i; int color_offset = COLOR_FOREGROUND_ANSI; + if (match_word(name, len, "default")) { + /* + * Restores to the terminal's default color, which may not be + * the same as explicitly setting "white" or "black". + * + * ECMA-48 - Control Functions \ + * for Coded Character Sets, 5th edition (June 1991): + * > 39 default display colour (implementation-defined) + * > 49 default background colour (implementation-defined) + * + * Although not supported /everywhere/--according to terminfo, + * some terminals define "op" (original pair) as a blunt + * "set to white on black", or even "send full SGR reset"-- + * it's standard and well-supported enough that if a user + * asks for it in their config this will do the right thing. + */ + out->type = COLOR_ANSI; + out->value = 9 + color_offset; + return 0; + } + if (strncasecmp(name, "bright", 6) == 0) { color_offset = COLOR_FOREGROUND_BRIGHT_ANSI; name += 6; @@ -234,6 +255,7 @@ int color_parse_mem(const char *value, int value_len, char *dst) const char *ptr = value; int len = value_len; char *end = dst + COLOR_MAXLEN; + unsigned int has_reset = 0; unsigned int attr = 0; struct color fg = { COLOR_UNSPECIFIED }; struct color bg = { COLOR_UNSPECIFIED }; @@ -248,12 +270,7 @@ int color_parse_mem(const char *value, int value_len, char *dst) return 0; } - if (!strncasecmp(ptr, "reset", len)) { - xsnprintf(dst, end - dst, GIT_COLOR_RESET); - return 0; - } - - /* [fg [bg]] [attr]... */ + /* [reset] [fg [bg]] [attr]... */ while (len > 0) { const char *word = ptr; struct color c = { COLOR_UNSPECIFIED }; @@ -270,6 +287,11 @@ int color_parse_mem(const char *value, int value_len, char *dst) len--; } + if (match_word(word, wordlen, "reset")) { + has_reset = 1; + continue; + } + if (!parse_color(&c, word, wordlen)) { if (fg.type == COLOR_UNSPECIFIED) { fg = c; @@ -295,13 +317,16 @@ int color_parse_mem(const char *value, int value_len, char *dst) *dst++ = (x); \ } while(0) - if (attr || !color_empty(&fg) || !color_empty(&bg)) { + if (has_reset || attr || !color_empty(&fg) || !color_empty(&bg)) { int sep = 0; int i; OUT('\033'); OUT('['); + if (has_reset) + sep++; + for (i = 0; attr; i++) { unsigned bit = (1 << i); if (!(attr & bit)) @@ -6,6 +6,7 @@ struct strbuf; /* * The maximum length of ANSI color sequence we would generate: * - leading ESC '[' 2 + * - reset ';' .................1 * - attr + ';' 2 * num_attr (e.g. "1;") * - no-attr + ';' 3 * num_attr (e.g. "22;") * - fg color + ';' 17 (e.g. "38;2;255;255;255;") @@ -24,30 +25,42 @@ struct strbuf; #define GIT_COLOR_NORMAL "" #define GIT_COLOR_RESET "\033[m" #define GIT_COLOR_BOLD "\033[1m" +#define GIT_COLOR_BLACK "\033[30m" #define GIT_COLOR_RED "\033[31m" #define GIT_COLOR_GREEN "\033[32m" #define GIT_COLOR_YELLOW "\033[33m" #define GIT_COLOR_BLUE "\033[34m" #define GIT_COLOR_MAGENTA "\033[35m" #define GIT_COLOR_CYAN "\033[36m" +#define GIT_COLOR_WHITE "\033[37m" +#define GIT_COLOR_DEFAULT "\033[39m" +#define GIT_COLOR_BOLD_BLACK "\033[1;30m" #define GIT_COLOR_BOLD_RED "\033[1;31m" #define GIT_COLOR_BOLD_GREEN "\033[1;32m" #define GIT_COLOR_BOLD_YELLOW "\033[1;33m" #define GIT_COLOR_BOLD_BLUE "\033[1;34m" #define GIT_COLOR_BOLD_MAGENTA "\033[1;35m" #define GIT_COLOR_BOLD_CYAN "\033[1;36m" +#define GIT_COLOR_BOLD_WHITE "\033[1;37m" +#define GIT_COLOR_BOLD_DEFAULT "\033[1;39m" +#define GIT_COLOR_FAINT_BLACK "\033[2;30m" #define GIT_COLOR_FAINT_RED "\033[2;31m" #define GIT_COLOR_FAINT_GREEN "\033[2;32m" #define GIT_COLOR_FAINT_YELLOW "\033[2;33m" #define GIT_COLOR_FAINT_BLUE "\033[2;34m" #define GIT_COLOR_FAINT_MAGENTA "\033[2;35m" #define GIT_COLOR_FAINT_CYAN "\033[2;36m" +#define GIT_COLOR_FAINT_WHITE "\033[2;37m" +#define GIT_COLOR_FAINT_DEFAULT "\033[2;39m" +#define GIT_COLOR_BG_BLACK "\033[40m" #define GIT_COLOR_BG_RED "\033[41m" #define GIT_COLOR_BG_GREEN "\033[42m" #define GIT_COLOR_BG_YELLOW "\033[43m" #define GIT_COLOR_BG_BLUE "\033[44m" #define GIT_COLOR_BG_MAGENTA "\033[45m" #define GIT_COLOR_BG_CYAN "\033[46m" +#define GIT_COLOR_BG_WHITE "\033[47m" +#define GIT_COLOR_BG_DEFAULT "\033[49m" #define GIT_COLOR_FAINT "\033[2m" #define GIT_COLOR_FAINT_ITALIC "\033[2;3m" #define GIT_COLOR_REVERSE "\033[7m" diff --git a/command-list.txt b/command-list.txt index eb9cee8dee..675c28f0bd 100644 --- a/command-list.txt +++ b/command-list.txt @@ -43,7 +43,7 @@ # specified here, which can only have "guide" attribute and nothing # else. # -### command list (do not change this line, also do not change alignment) +### command list (do not change this line) # command name category [category] [category] git-add mainporcelain worktree git-am mainporcelain @@ -60,9 +60,9 @@ git-cat-file plumbinginterrogators git-check-attr purehelpers git-check-ignore purehelpers git-check-mailmap purehelpers +git-check-ref-format purehelpers git-checkout mainporcelain git-checkout-index plumbingmanipulators -git-check-ref-format purehelpers git-cherry plumbinginterrogators complete git-cherry-pick mainporcelain git-citool mainporcelain @@ -111,7 +111,6 @@ git-index-pack plumbingmanipulators git-init mainporcelain init git-instaweb ancillaryinterrogators complete git-interpret-trailers purehelpers -gitk mainporcelain git-log mainporcelain info git-ls-files plumbinginterrogators git-ls-remote plumbinginterrogators @@ -124,11 +123,11 @@ git-merge-base plumbinginterrogators git-merge-file plumbingmanipulators git-merge-index plumbingmanipulators git-merge-one-file purehelpers -git-mergetool ancillarymanipulators complete git-merge-tree ancillaryinterrogators -git-multi-pack-index plumbingmanipulators +git-mergetool ancillarymanipulators complete git-mktag plumbingmanipulators git-mktree plumbingmanipulators +git-multi-pack-index plumbingmanipulators git-mv mainporcelain worktree git-name-rev plumbinginterrogators git-notes mainporcelain @@ -154,23 +153,23 @@ git-request-pull foreignscminterface complete git-rerere ancillaryinterrogators git-reset mainporcelain history git-restore mainporcelain worktree -git-revert mainporcelain git-rev-list plumbinginterrogators git-rev-parse plumbinginterrogators +git-revert mainporcelain git-rm mainporcelain worktree git-send-email foreignscminterface complete git-send-pack synchingrepositories +git-sh-i18n purehelpers +git-sh-setup purehelpers git-shell synchelpers git-shortlog mainporcelain git-show mainporcelain info git-show-branch ancillaryinterrogators complete git-show-index plumbinginterrogators git-show-ref plumbinginterrogators -git-sh-i18n purehelpers -git-sh-setup purehelpers git-sparse-checkout mainporcelain -git-stash mainporcelain git-stage complete +git-stash mainporcelain git-status mainporcelain info git-stripspace purehelpers git-submodule mainporcelain @@ -189,7 +188,6 @@ git-var plumbinginterrogators git-verify-commit ancillaryinterrogators git-verify-pack plumbinginterrogators git-verify-tag ancillaryinterrogators -gitweb ancillaryinterrogators git-whatchanged ancillaryinterrogators complete git-worktree mainporcelain git-write-tree plumbingmanipulators @@ -204,6 +202,7 @@ gitfaq guide gitglossary guide githooks guide gitignore guide +gitk mainporcelain gitmailmap guide gitmodules guide gitnamespaces guide @@ -211,6 +210,7 @@ gitremote-helpers guide gitrepository-layout guide gitrevisions guide gitsubmodules guide -gittutorial-2 guide gittutorial guide +gittutorial-2 guide +gitweb ancillaryinterrogators gitworkflows guide diff --git a/commit-graph.c b/commit-graph.c index 2706683acf..265c010122 100644 --- a/commit-graph.c +++ b/commit-graph.c @@ -632,10 +632,13 @@ static int prepare_commit_graph(struct repository *r) struct object_directory *odb; /* + * Early return if there is no git dir or if the commit graph is + * disabled. + * * This must come before the "already attempted?" check below, because * we want to disable even an already-loaded graph file. */ - if (r->commit_graph_disabled) + if (!r->gitdir || r->commit_graph_disabled) return 0; if (r->objects->commit_graph_attempted) @@ -1212,8 +1212,10 @@ int check_commit_signature(const struct commit *commit, struct signature_check * if (parse_signed_commit(commit, &payload, &signature, the_hash_algo) <= 0) goto out; - ret = check_signature(payload.buf, payload.len, signature.buf, - signature.len, sigc); + + sigc->payload_type = SIGNATURE_PAYLOAD_COMMIT; + sigc->payload = strbuf_detach(&payload, &sigc->payload_len); + ret = check_signature(sigc, signature.buf, signature.len); out: strbuf_release(&payload); diff --git a/common-main.c b/common-main.c index 71e21dd20a..29fb7452f8 100644 --- a/common-main.c +++ b/common-main.c @@ -26,6 +26,7 @@ static void restore_sigpipe_to_default(void) int main(int argc, const char **argv) { int result; + struct strbuf tmp = STRBUF_INIT; trace2_initialize_clock(); @@ -49,9 +50,15 @@ int main(int argc, const char **argv) trace2_cmd_start(argv); trace2_collect_process_info(TRACE2_PROCESS_INFO_STARTUP); - result = cmd_main(argc, argv); + if (!strbuf_getcwd(&tmp)) + tmp_original_cwd = strbuf_detach(&tmp, NULL); - trace2_cmd_exit(result); + result = cmd_main(argc, argv); - return result; + /* + * We define exit() to call trace2_cmd_exit_fl() in + * git-compat-util.h. Whether we reach this or exit() + * elsewhere we'll always run our trace2 exit handler. + */ + exit(result); } diff --git a/compat/.gitattributes b/compat/.gitattributes new file mode 100644 index 0000000000..40dbfb170d --- /dev/null +++ b/compat/.gitattributes @@ -0,0 +1 @@ +/zlib-uncompress2.c whitespace=-indent-with-non-tab,-trailing-space diff --git a/compat/mingw.c b/compat/mingw.c index 9e0cd1e097..e14f2d5f77 100644 --- a/compat/mingw.c +++ b/compat/mingw.c @@ -1083,6 +1083,7 @@ int pipe(int filedes[2]) return 0; } +#ifndef __MINGW64__ struct tm *gmtime_r(const time_t *timep, struct tm *result) { if (gmtime_s(result, timep) == 0) @@ -1096,6 +1097,7 @@ struct tm *localtime_r(const time_t *timep, struct tm *result) return result; return NULL; } +#endif char *mingw_getcwd(char *pointer, int len) { diff --git a/compat/unsetenv.c b/compat/unsetenv.c index bf5fd7063b..b9d34af613 100644 --- a/compat/unsetenv.c +++ b/compat/unsetenv.c @@ -1,6 +1,6 @@ #include "../git-compat-util.h" -void gitunsetenv (const char *name) +int gitunsetenv(const char *name) { #if !defined(__MINGW32__) extern char **environ; @@ -24,4 +24,6 @@ void gitunsetenv (const char *name) ++dst; } environ[dst] = NULL; + + return 0; } diff --git a/compat/zlib-uncompress2.c b/compat/zlib-uncompress2.c new file mode 100644 index 0000000000..722610b971 --- /dev/null +++ b/compat/zlib-uncompress2.c @@ -0,0 +1,95 @@ +/* taken from zlib's uncompr.c + + commit cacf7f1d4e3d44d871b605da3b647f07d718623f + Author: Mark Adler <madler@alumni.caltech.edu> + Date: Sun Jan 15 09:18:46 2017 -0800 + + zlib 1.2.11 + +*/ + +#include "../reftable/system.h" +#define z_const + +/* + * Copyright (C) 1995-2003, 2010, 2014, 2016 Jean-loup Gailly, Mark Adler + * For conditions of distribution and use, see copyright notice in zlib.h + */ + +#include <zlib.h> + +/* clang-format off */ + +/* =========================================================================== + Decompresses the source buffer into the destination buffer. *sourceLen is + the byte length of the source buffer. Upon entry, *destLen is the total size + of the destination buffer, which must be large enough to hold the entire + uncompressed data. (The size of the uncompressed data must have been saved + previously by the compressor and transmitted to the decompressor by some + mechanism outside the scope of this compression library.) Upon exit, + *destLen is the size of the decompressed data and *sourceLen is the number + of source bytes consumed. Upon return, source + *sourceLen points to the + first unused input byte. + + uncompress returns Z_OK if success, Z_MEM_ERROR if there was not enough + memory, Z_BUF_ERROR if there was not enough room in the output buffer, or + Z_DATA_ERROR if the input data was corrupted, including if the input data is + an incomplete zlib stream. +*/ +int ZEXPORT uncompress2 ( + Bytef *dest, + uLongf *destLen, + const Bytef *source, + uLong *sourceLen) { + z_stream stream; + int err; + const uInt max = (uInt)-1; + uLong len, left; + Byte buf[1]; /* for detection of incomplete stream when *destLen == 0 */ + + len = *sourceLen; + if (*destLen) { + left = *destLen; + *destLen = 0; + } + else { + left = 1; + dest = buf; + } + + stream.next_in = (z_const Bytef *)source; + stream.avail_in = 0; + stream.zalloc = (alloc_func)0; + stream.zfree = (free_func)0; + stream.opaque = (voidpf)0; + + err = inflateInit(&stream); + if (err != Z_OK) return err; + + stream.next_out = dest; + stream.avail_out = 0; + + do { + if (stream.avail_out == 0) { + stream.avail_out = left > (uLong)max ? max : (uInt)left; + left -= stream.avail_out; + } + if (stream.avail_in == 0) { + stream.avail_in = len > (uLong)max ? max : (uInt)len; + len -= stream.avail_in; + } + err = inflate(&stream, Z_NO_FLUSH); + } while (err == Z_OK); + + *sourceLen -= len + stream.avail_in; + if (dest != buf) + *destLen = stream.total_out; + else if (stream.total_out && err == Z_BUF_ERROR) + left = 1; + + inflateEnd(&stream); + return err == Z_STREAM_END ? Z_OK : + err == Z_NEED_DICT ? Z_DATA_ERROR : + err == Z_BUF_ERROR && left + stream.avail_out ? Z_DATA_ERROR : + err; +} diff --git a/config.mak.dev b/config.mak.dev index 7673fed114..d4afac6b51 100644 --- a/config.mak.dev +++ b/config.mak.dev @@ -19,6 +19,11 @@ endif endif endif endif + +ifneq ($(or $(filter gcc6,$(COMPILER_FEATURES)),$(filter clang7,$(COMPILER_FEATURES))),) +DEVELOPER_CFLAGS += -std=gnu99 +endif + DEVELOPER_CFLAGS += -Wdeclaration-after-statement DEVELOPER_CFLAGS += -Wformat-security DEVELOPER_CFLAGS += -Wold-style-definition diff --git a/config.mak.uname b/config.mak.uname index 3236a4918a..a3a779327f 100644 --- a/config.mak.uname +++ b/config.mak.uname @@ -58,7 +58,6 @@ ifeq ($(uname_S),Linux) # -lrt is needed for clock_gettime on glibc <= 2.16 NEEDS_LIBRT = YesPlease HAVE_GETDELIM = YesPlease - SANE_TEXT_GREP=-a FREAD_READS_DIRECTORIES = UnfortunatelyYes BASIC_CFLAGS += -DHAVE_SYSINFO PROCFS_EXECUTABLE_PATH = /proc/self/exe @@ -262,6 +261,10 @@ ifeq ($(uname_S),FreeBSD) FILENO_IS_A_MACRO = UnfortunatelyYes endif ifeq ($(uname_S),OpenBSD) + # Versions < 7.0 need compatibility layer + ifeq ($(shell expr "$(uname_R)" : "[1-6]\."),2) + NO_UNCOMPRESS2 = UnfortunatelyYes + endif NO_STRCASESTR = YesPlease NO_MEMMEM = YesPlease USE_ST_TIMESPEC = YesPlease @@ -517,6 +520,7 @@ ifeq ($(uname_S),Interix) endif endif ifeq ($(uname_S),Minix) + NO_UNCOMPRESS2 = YesPlease NO_IPV6 = YesPlease NO_ST_BLOCKS_IN_STRUCT_STAT = YesPlease NO_NSEC = YesPlease diff --git a/configure.ac b/configure.ac index 031e8d3fee..d60d494ee4 100644 --- a/configure.ac +++ b/configure.ac @@ -507,14 +507,6 @@ if test -n "$ASCIIDOC"; then esac fi -if grep -a ascii configure.ac >/dev/null; then - AC_MSG_RESULT([Using 'grep -a' for sane_grep]) - SANE_TEXT_GREP=-a -else - SANE_TEXT_GREP= -fi -GIT_CONF_SUBST([SANE_TEXT_GREP]) - ## Checks for libraries. AC_MSG_NOTICE([CHECKS for libraries]) # @@ -672,9 +664,22 @@ AC_LINK_IFELSE([ZLIBTEST_SRC], NO_DEFLATE_BOUND=yes]) LIBS="$old_LIBS" +AC_DEFUN([ZLIBTEST_UNCOMPRESS2_SRC], [ +AC_LANG_PROGRAM([#include <zlib.h>], + [uncompress2(NULL,NULL,NULL,NULL);])]) +AC_MSG_CHECKING([for uncompress2 in -lz]) +old_LIBS="$LIBS" +LIBS="$LIBS -lz" +AC_LINK_IFELSE([ZLIBTEST_UNCOMPRESS2_SRC], + [AC_MSG_RESULT([yes])], + [AC_MSG_RESULT([no]) + NO_UNCOMPRESS2=yes]) +LIBS="$old_LIBS" + GIT_UNSTASH_FLAGS($ZLIB_PATH) GIT_CONF_SUBST([NO_DEFLATE_BOUND]) +GIT_CONF_SUBST([NO_UNCOMPRESS2]) # # Define NEEDS_SOCKET if linking with libc is not enough (SunOS, diff --git a/connected.c b/connected.c index 35bd4a2638..ed3025e7a2 100644 --- a/connected.c +++ b/connected.c @@ -109,7 +109,8 @@ no_promisor_pack_found: _("Checking connectivity")); rev_list.git_cmd = 1; - rev_list.env = opt->env; + if (opt->env) + strvec_pushv(&rev_list.env_array, opt->env); rev_list.in = -1; rev_list.no_stdout = 1; if (opt->err_fd) diff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt index fd1399c440..5100f56bb3 100644 --- a/contrib/buildsystems/CMakeLists.txt +++ b/contrib/buildsystems/CMakeLists.txt @@ -208,7 +208,7 @@ endif() if(CMAKE_C_COMPILER_ID STREQUAL "MSVC") set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_BINARY_DIR}) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_BINARY_DIR}) - add_compile_options(/MP) + add_compile_options(/MP /std:c11) endif() #default behaviour @@ -647,6 +647,12 @@ parse_makefile_for_sources(libxdiff_SOURCES "XDIFF_OBJS") list(TRANSFORM libxdiff_SOURCES PREPEND "${CMAKE_SOURCE_DIR}/") add_library(xdiff STATIC ${libxdiff_SOURCES}) +#reftable +parse_makefile_for_sources(reftable_SOURCES "REFTABLE_OBJS") + +list(TRANSFORM reftable_SOURCES PREPEND "${CMAKE_SOURCE_DIR}/") +add_library(reftable STATIC ${reftable_SOURCES}) + if(WIN32) if(NOT MSVC)#use windres when compiling with gcc and clang add_custom_command(OUTPUT ${CMAKE_BINARY_DIR}/git.res @@ -669,7 +675,7 @@ endif() #link all required libraries to common-main add_library(common-main OBJECT ${CMAKE_SOURCE_DIR}/common-main.c) -target_link_libraries(common-main libgit xdiff ${ZLIB_LIBRARIES}) +target_link_libraries(common-main libgit xdiff reftable ${ZLIB_LIBRARIES}) if(Intl_FOUND) target_link_libraries(common-main ${Intl_LIBRARIES}) endif() @@ -781,7 +787,6 @@ foreach(script ${git_shell_scripts}) string(REPLACE "@@USE_GETTEXT_SCHEME@@" "" content "${content}") string(REPLACE "# @@BROKEN_PATH_FIX@@" "" content "${content}") string(REPLACE "@@PERL@@" "${PERL_PATH}" content "${content}") - string(REPLACE "@@SANE_TEXT_GREP@@" "-a" content "${content}") string(REPLACE "@@PAGER_ENV@@" "LESS=FRX LV=-c" content "${content}") file(WRITE ${CMAKE_BINARY_DIR}/${script} ${content}) endforeach() @@ -909,11 +914,15 @@ if(BUILD_TESTING) add_executable(test-fake-ssh ${CMAKE_SOURCE_DIR}/t/helper/test-fake-ssh.c) target_link_libraries(test-fake-ssh common-main) +#reftable-tests +parse_makefile_for_sources(test-reftable_SOURCES "REFTABLE_TEST_OBJS") +list(TRANSFORM test-reftable_SOURCES PREPEND "${CMAKE_SOURCE_DIR}/") + #test-tool parse_makefile_for_sources(test-tool_SOURCES "TEST_BUILTINS_OBJS") list(TRANSFORM test-tool_SOURCES PREPEND "${CMAKE_SOURCE_DIR}/t/helper/") -add_executable(test-tool ${CMAKE_SOURCE_DIR}/t/helper/test-tool.c ${test-tool_SOURCES}) +add_executable(test-tool ${CMAKE_SOURCE_DIR}/t/helper/test-tool.c ${test-tool_SOURCES} ${test-reftable_SOURCES}) target_link_libraries(test-tool common-main) set_target_properties(test-fake-ssh test-tool diff --git a/contrib/buildsystems/Generators/Vcxproj.pm b/contrib/buildsystems/Generators/Vcxproj.pm index d2584450ba..1a25789d28 100644 --- a/contrib/buildsystems/Generators/Vcxproj.pm +++ b/contrib/buildsystems/Generators/Vcxproj.pm @@ -77,7 +77,7 @@ sub createProject { my $libs_release = "\n "; my $libs_debug = "\n "; if (!$static_library) { - $libs_release = join(";", sort(grep /^(?!libgit\.lib|xdiff\/lib\.lib|vcs-svn\/lib\.lib)/, @{$$build_structure{"$prefix${name}_LIBS"}})); + $libs_release = join(";", sort(grep /^(?!libgit\.lib|xdiff\/lib\.lib|vcs-svn\/lib\.lib|reftable\/libreftable\.lib)/, @{$$build_structure{"$prefix${name}_LIBS"}})); $libs_debug = $libs_release; $libs_debug =~ s/zlib\.lib/zlibd\.lib/g; $libs_debug =~ s/libexpat\.lib/libexpatd\.lib/g; @@ -232,6 +232,7 @@ EOM EOM if (!$static_library || $target =~ 'vcs-svn' || $target =~ 'xdiff') { my $uuid_libgit = $$build_structure{"LIBS_libgit_GUID"}; + my $uuid_libreftable = $$build_structure{"LIBS_reftable/libreftable_GUID"}; my $uuid_xdiff_lib = $$build_structure{"LIBS_xdiff/lib_GUID"}; print F << "EOM"; @@ -241,6 +242,14 @@ EOM <ReferenceOutputAssembly>false</ReferenceOutputAssembly> </ProjectReference> EOM + if (!($name =~ /xdiff|libreftable/)) { + print F << "EOM"; + <ProjectReference Include="$cdup\\reftable\\libreftable\\libreftable.vcxproj"> + <Project>$uuid_libreftable</Project> + <ReferenceOutputAssembly>false</ReferenceOutputAssembly> + </ProjectReference> +EOM + } if (!($name =~ 'xdiff')) { print F << "EOM"; <ProjectReference Include="$cdup\\xdiff\\lib\\xdiff_lib.vcxproj"> diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash index 7c3a75373a..377d6c5494 100644 --- a/contrib/completion/git-completion.bash +++ b/contrib/completion/git-completion.bash @@ -1566,7 +1566,7 @@ _git_checkout () case "$cur" in --conflict=*) - __gitcomp "diff3 merge" "" "${cur##--conflict=}" + __gitcomp "diff3 merge zdiff3" "" "${cur##--conflict=}" ;; --*) __gitcomp_builtin checkout @@ -2001,7 +2001,7 @@ __git_log_shortlog_options=" " __git_log_pretty_formats="oneline short medium full fuller reference email raw format: tformat: mboxrd" -__git_log_date_formats="relative iso8601 iso8601-strict rfc2822 short local default raw unix format:" +__git_log_date_formats="relative iso8601 iso8601-strict rfc2822 short local default human raw unix auto: format:" _git_log () { @@ -2359,16 +2359,7 @@ _git_send_email () return ;; --*) - __gitcomp_builtin send-email "--annotate --bcc --cc --cc-cmd --chain-reply-to - --compose --confirm= --dry-run --envelope-sender - --from --identity - --in-reply-to --no-chain-reply-to --no-signed-off-by-cc - --no-suppress-from --no-thread --quiet --reply-to - --signed-off-by-cc --smtp-pass --smtp-server - --smtp-server-port --smtp-encryption= --smtp-user - --subject --suppress-cc= --suppress-from --thread --to - --validate --no-validate - $__git_format_patch_extra_options" + __gitcomp_builtin send-email "$__git_format_patch_extra_options" return ;; esac @@ -2446,7 +2437,7 @@ _git_switch () case "$cur" in --conflict=*) - __gitcomp "diff3 merge" "" "${cur##--conflict=}" + __gitcomp "diff3 merge zdiff3" "" "${cur##--conflict=}" ;; --*) __gitcomp_builtin switch @@ -2886,7 +2877,7 @@ _git_restore () case "$cur" in --conflict=*) - __gitcomp "diff3 merge" "" "${cur##--conflict=}" + __gitcomp "diff3 merge zdiff3" "" "${cur##--conflict=}" ;; --source=*) __git_complete_refs --cur="${cur##--source=}" diff --git a/contrib/git-jump/README b/contrib/git-jump/README index 2f618a7f97..8bcace29d2 100644 --- a/contrib/git-jump/README +++ b/contrib/git-jump/README @@ -65,6 +65,9 @@ git jump diff --cached # jump to merge conflicts git jump merge +# documentation conflicts are hard; skip past them for now +git jump merge :^Documentation + # jump to all instances of foo_bar git jump grep foo_bar diff --git a/contrib/git-jump/git-jump b/contrib/git-jump/git-jump index 931b0fe3a9..92dbd4cde1 100755 --- a/contrib/git-jump/git-jump +++ b/contrib/git-jump/git-jump @@ -39,7 +39,7 @@ mode_diff() { } mode_merge() { - git ls-files -u | + git ls-files -u "$@" | perl -pe 's/^.*?\t//' | sort -u | while IFS= read fn; do diff --git a/contrib/mw-to-git/t/t9365-continuing-queries.sh b/contrib/mw-to-git/t/t9365-continuing-queries.sh index 016454749f..d3e7312659 100755 --- a/contrib/mw-to-git/t/t9365-continuing-queries.sh +++ b/contrib/mw-to-git/t/t9365-continuing-queries.sh @@ -12,7 +12,7 @@ test_expect_success 'creating page w/ >500 revisions' ' for i in $(test_seq 501) do echo "creating revision $i" && - wiki_editpage foo "revision $i<br/>" true + wiki_editpage foo "revision $i<br/>" true || return 1 done ' diff --git a/contrib/scalar/.gitignore b/contrib/scalar/.gitignore new file mode 100644 index 0000000000..ff3d47e84d --- /dev/null +++ b/contrib/scalar/.gitignore @@ -0,0 +1,2 @@ +/*.exe +/scalar diff --git a/contrib/scalar/Makefile b/contrib/scalar/Makefile new file mode 100644 index 0000000000..231b1ee179 --- /dev/null +++ b/contrib/scalar/Makefile @@ -0,0 +1,45 @@ +QUIET_SUBDIR0 = +$(MAKE) -C # space to separate -C and subdir +QUIET_SUBDIR1 = + +ifneq ($(findstring s,$(MAKEFLAGS)),s) +ifndef V + QUIET_GEN = @echo ' ' GEN $@; + QUIET_SUBDIR0 = +@subdir= + QUIET_SUBDIR1 = ;$(NO_SUBDIR) echo ' ' SUBDIR $$subdir; \ + $(MAKE) $(PRINT_DIR) -C $$subdir +else + export V +endif +endif + +all: + +include ../../config.mak.uname +-include ../../config.mak.autogen +-include ../../config.mak + +TARGETS = scalar$(X) scalar.o +GITLIBS = ../../common-main.o ../../libgit.a ../../xdiff/lib.a + +all: scalar$(X) ../../bin-wrappers/scalar + +$(GITLIBS): + $(QUIET_SUBDIR0)../.. $(QUIET_SUBDIR1) $(subst ../../,,$@) + +$(TARGETS): $(GITLIBS) scalar.c + $(QUIET_SUBDIR0)../.. $(QUIET_SUBDIR1) $(patsubst %,contrib/scalar/%,$@) + +clean: + $(RM) $(TARGETS) ../../bin-wrappers/scalar + +../../bin-wrappers/scalar: ../../wrap-for-bin.sh Makefile + @mkdir -p ../../bin-wrappers + $(QUIET_GEN)sed -e '1s|#!.*/sh|#!$(SHELL_PATH_SQ)|' \ + -e 's|@@BUILD_DIR@@|$(shell cd ../.. && pwd)|' \ + -e 's|@@PROG@@|contrib/scalar/scalar$(X)|' < $< > $@ && \ + chmod +x $@ + +test: all + $(MAKE) -C t + +.PHONY: $(GITLIBS) all clean test FORCE diff --git a/contrib/scalar/README.md b/contrib/scalar/README.md new file mode 100644 index 0000000000..634b5771ed --- /dev/null +++ b/contrib/scalar/README.md @@ -0,0 +1,82 @@ +# Scalar - an opinionated repository management tool + +Scalar is an add-on to Git that helps users take advantage of advanced +performance features in Git. Originally implemented in C# using .NET Core, +based on the learnings from the VFS for Git project, most of the techniques +developed by the Scalar project have been integrated into core Git already: + +* partial clone, +* commit graphs, +* multi-pack index, +* sparse checkout (cone mode), +* scheduled background maintenance, +* etc + +This directory contains the remaining parts of Scalar that are not (yet) in +core Git. + +## Roadmap + +The idea is to populate this directory via incremental patch series and +eventually move to a top-level directory next to `gitk-git/` and to `git-gui/`. The +current plan involves the following patch series: + +- `scalar-the-beginning`: The initial patch series which sets up + `contrib/scalar/` and populates it with a minimal `scalar` command that + demonstrates the fundamental ideas. + +- `scalar-c-and-C`: The `scalar` command learns about two options that can be + specified before the command, `-c <key>=<value>` and `-C <directory>`. + +- `scalar-diagnose`: The `scalar` command is taught the `diagnose` subcommand. + +- `scalar-and-builtin-fsmonitor`: The built-in FSMonitor is enabled in `scalar + register` and in `scalar clone`, for an enormous performance boost when + working in large worktrees. This patch series necessarily depends on Jeff + Hostetler's FSMonitor patch series to be integrated into Git. + +- `scalar-gentler-config-locking`: Scalar enlistments are registered in the + user's Git config. This usually does not represent any problem because it is + rare for a user to register an enlistment. However, in Scalar's functional + tests, Scalar enlistments are created galore, and in parallel, which can lead + to lock contention. This patch series works around that problem by re-trying + to lock the config file in a gentle fashion. + +- `scalar-extra-docs`: Add some extensive documentation that has been written + in the original Scalar project (all subject to discussion, of course). + +- `optionally-install-scalar`: Now that Scalar is feature (and documentation) + complete and is verified in CI builds, let's offer to install it. + +- `move-scalar-to-toplevel`: Now that Scalar is complete, let's move it next to + `gitk-git/` and to `git-gui/`, making it a top-level command. + +The following two patch series exist in Microsoft's fork of Git and are +publicly available. There is no current plan to upstream them, not because I +want to withhold these patches, but because I don't think the Git community is +interested in these patches. + +There are some interesting ideas there, but the implementation is too specific +to Azure Repos and/or VFS for Git to be of much help in general (and also: my +colleagues tried to upstream some patches already and the enthusiasm for +integrating things related to Azure Repos and VFS for Git can be summarized in +very, very few words). + +These still exist mainly because the GVFS protocol is what Azure Repos has +instead of partial clone, while Git is focused on improving partial clone: + +- `scalar-with-gvfs`: The primary purpose of this patch series is to support + existing Scalar users whose repositories are hosted in Azure Repos (which + does not support Git's partial clones, but supports its predecessor, the GVFS + protocol, which is used by Scalar to emulate the partial clone). + + Since the GVFS protocol will never be supported by core Git, this patch + series will remain in Microsoft's fork of Git. + +- `run-scalar-functional-tests`: The Scalar project developed a quite + comprehensive set of integration tests (or, "Functional Tests"). They are the + sole remaining part of the original C#-based Scalar project, and this patch + adds a GitHub workflow that runs them all. + + Since the tests partially depend on features that are only provided in the + `scalar-with-gvfs` patch series, this patch cannot be upstreamed. diff --git a/contrib/scalar/scalar.c b/contrib/scalar/scalar.c new file mode 100644 index 0000000000..1ce9c2b00e --- /dev/null +++ b/contrib/scalar/scalar.c @@ -0,0 +1,826 @@ +/* + * The Scalar command-line interface. + */ + +#include "cache.h" +#include "gettext.h" +#include "parse-options.h" +#include "config.h" +#include "run-command.h" +#include "refs.h" +#include "dir.h" +#include "packfile.h" +#include "help.h" + +/* + * Remove the deepest subdirectory in the provided path string. Path must not + * include a trailing path separator. Returns 1 if parent directory found, + * otherwise 0. + */ +static int strbuf_parent_directory(struct strbuf *buf) +{ + size_t len = buf->len; + size_t offset = offset_1st_component(buf->buf); + char *path_sep = find_last_dir_sep(buf->buf + offset); + strbuf_setlen(buf, path_sep ? path_sep - buf->buf : offset); + + return buf->len < len; +} + +static void setup_enlistment_directory(int argc, const char **argv, + const char * const *usagestr, + const struct option *options, + struct strbuf *enlistment_root) +{ + struct strbuf path = STRBUF_INIT; + char *root; + int enlistment_found = 0; + + if (startup_info->have_repository) + BUG("gitdir already set up?!?"); + + if (argc > 1) + usage_with_options(usagestr, options); + + /* find the worktree, determine its corresponding root */ + if (argc == 1) + strbuf_add_absolute_path(&path, argv[0]); + else if (strbuf_getcwd(&path) < 0) + die(_("need a working directory")); + + strbuf_trim_trailing_dir_sep(&path); + do { + const size_t len = path.len; + + /* check if currently in enlistment root with src/ workdir */ + strbuf_addstr(&path, "/src"); + if (is_nonbare_repository_dir(&path)) { + if (enlistment_root) + strbuf_add(enlistment_root, path.buf, len); + + enlistment_found = 1; + break; + } + + /* reset to original path */ + strbuf_setlen(&path, len); + + /* check if currently in workdir */ + if (is_nonbare_repository_dir(&path)) { + if (enlistment_root) { + /* + * If the worktree's directory's name is `src`, the enlistment is the + * parent directory, otherwise it is identical to the worktree. + */ + root = strip_path_suffix(path.buf, "src"); + strbuf_addstr(enlistment_root, root ? root : path.buf); + free(root); + } + + enlistment_found = 1; + break; + } + } while (strbuf_parent_directory(&path)); + + if (!enlistment_found) + die(_("could not find enlistment root")); + + if (chdir(path.buf) < 0) + die_errno(_("could not switch to '%s'"), path.buf); + + strbuf_release(&path); + setup_git_directory(); +} + +static int run_git(const char *arg, ...) +{ + struct strvec argv = STRVEC_INIT; + va_list args; + const char *p; + int res; + + va_start(args, arg); + strvec_push(&argv, arg); + while ((p = va_arg(args, const char *))) + strvec_push(&argv, p); + va_end(args); + + res = run_command_v_opt(argv.v, RUN_GIT_CMD); + + strvec_clear(&argv); + return res; +} + +static int set_recommended_config(int reconfigure) +{ + struct { + const char *key; + const char *value; + int overwrite_on_reconfigure; + } config[] = { + /* Required */ + { "am.keepCR", "true", 1 }, + { "core.FSCache", "true", 1 }, + { "core.multiPackIndex", "true", 1 }, + { "core.preloadIndex", "true", 1 }, +#ifndef WIN32 + { "core.untrackedCache", "true", 1 }, +#else + /* + * Unfortunately, Scalar's Functional Tests demonstrated + * that the untracked cache feature is unreliable on Windows + * (which is a bummer because that platform would benefit the + * most from it). For some reason, freshly created files seem + * not to update the directory's `lastModified` time + * immediately, but the untracked cache would need to rely on + * that. + * + * Therefore, with a sad heart, we disable this very useful + * feature on Windows. + */ + { "core.untrackedCache", "false", 1 }, +#endif + { "core.logAllRefUpdates", "true", 1 }, + { "credential.https://dev.azure.com.useHttpPath", "true", 1 }, + { "credential.validate", "false", 1 }, /* GCM4W-only */ + { "gc.auto", "0", 1 }, + { "gui.GCWarning", "false", 1 }, + { "index.threads", "true", 1 }, + { "index.version", "4", 1 }, + { "merge.stat", "false", 1 }, + { "merge.renames", "true", 1 }, + { "pack.useBitmaps", "false", 1 }, + { "pack.useSparse", "true", 1 }, + { "receive.autoGC", "false", 1 }, + { "reset.quiet", "true", 1 }, + { "feature.manyFiles", "false", 1 }, + { "feature.experimental", "false", 1 }, + { "fetch.unpackLimit", "1", 1 }, + { "fetch.writeCommitGraph", "false", 1 }, +#ifdef WIN32 + { "http.sslBackend", "schannel", 1 }, +#endif + /* Optional */ + { "status.aheadBehind", "false" }, + { "commitGraph.generationVersion", "1" }, + { "core.autoCRLF", "false" }, + { "core.safeCRLF", "false" }, + { "fetch.showForcedUpdates", "false" }, + { NULL, NULL }, + }; + int i; + char *value; + + for (i = 0; config[i].key; i++) { + if ((reconfigure && config[i].overwrite_on_reconfigure) || + git_config_get_string(config[i].key, &value)) { + trace2_data_string("scalar", the_repository, config[i].key, "created"); + if (git_config_set_gently(config[i].key, + config[i].value) < 0) + return error(_("could not configure %s=%s"), + config[i].key, config[i].value); + } else { + trace2_data_string("scalar", the_repository, config[i].key, "exists"); + free(value); + } + } + + /* + * The `log.excludeDecoration` setting is special because it allows + * for multiple values. + */ + if (git_config_get_string("log.excludeDecoration", &value)) { + trace2_data_string("scalar", the_repository, + "log.excludeDecoration", "created"); + if (git_config_set_multivar_gently("log.excludeDecoration", + "refs/prefetch/*", + CONFIG_REGEX_NONE, 0)) + return error(_("could not configure " + "log.excludeDecoration")); + } else { + trace2_data_string("scalar", the_repository, + "log.excludeDecoration", "exists"); + free(value); + } + + return 0; +} + +static int toggle_maintenance(int enable) +{ + return run_git("maintenance", enable ? "start" : "unregister", NULL); +} + +static int add_or_remove_enlistment(int add) +{ + int res; + + if (!the_repository->worktree) + die(_("Scalar enlistments require a worktree")); + + res = run_git("config", "--global", "--get", "--fixed-value", + "scalar.repo", the_repository->worktree, NULL); + + /* + * If we want to add and the setting is already there, then do nothing. + * If we want to remove and the setting is not there, then do nothing. + */ + if ((add && !res) || (!add && res)) + return 0; + + return run_git("config", "--global", add ? "--add" : "--unset", + add ? "--no-fixed-value" : "--fixed-value", + "scalar.repo", the_repository->worktree, NULL); +} + +static int register_dir(void) +{ + int res = add_or_remove_enlistment(1); + + if (!res) + res = set_recommended_config(0); + + if (!res) + res = toggle_maintenance(1); + + return res; +} + +static int unregister_dir(void) +{ + int res = 0; + + if (toggle_maintenance(0) < 0) + res = -1; + + if (add_or_remove_enlistment(0) < 0) + res = -1; + + return res; +} + +/* printf-style interface, expects `<key>=<value>` argument */ +static int set_config(const char *fmt, ...) +{ + struct strbuf buf = STRBUF_INIT; + char *value; + int res; + va_list args; + + va_start(args, fmt); + strbuf_vaddf(&buf, fmt, args); + va_end(args); + + value = strchr(buf.buf, '='); + if (value) + *(value++) = '\0'; + res = git_config_set_gently(buf.buf, value); + strbuf_release(&buf); + + return res; +} + +static char *remote_default_branch(const char *url) +{ + struct child_process cp = CHILD_PROCESS_INIT; + struct strbuf out = STRBUF_INIT; + + cp.git_cmd = 1; + strvec_pushl(&cp.args, "ls-remote", "--symref", url, "HEAD", NULL); + if (!pipe_command(&cp, NULL, 0, &out, 0, NULL, 0)) { + const char *line = out.buf; + + while (*line) { + const char *eol = strchrnul(line, '\n'), *p; + size_t len = eol - line; + char *branch; + + if (!skip_prefix(line, "ref: ", &p) || + !strip_suffix_mem(line, &len, "\tHEAD")) { + line = eol + (*eol == '\n'); + continue; + } + + eol = line + len; + if (skip_prefix(p, "refs/heads/", &p)) { + branch = xstrndup(p, eol - p); + strbuf_release(&out); + return branch; + } + + error(_("remote HEAD is not a branch: '%.*s'"), + (int)(eol - p), p); + strbuf_release(&out); + return NULL; + } + } + warning(_("failed to get default branch name from remote; " + "using local default")); + strbuf_reset(&out); + + child_process_init(&cp); + cp.git_cmd = 1; + strvec_pushl(&cp.args, "symbolic-ref", "--short", "HEAD", NULL); + if (!pipe_command(&cp, NULL, 0, &out, 0, NULL, 0)) { + strbuf_trim(&out); + return strbuf_detach(&out, NULL); + } + + strbuf_release(&out); + error(_("failed to get default branch name")); + return NULL; +} + +static int delete_enlistment(struct strbuf *enlistment) +{ +#ifdef WIN32 + struct strbuf parent = STRBUF_INIT; +#endif + + if (unregister_dir()) + die(_("failed to unregister repository")); + +#ifdef WIN32 + /* + * Change the current directory to one outside of the enlistment so + * that we may delete everything underneath it. + */ + strbuf_addbuf(&parent, enlistment); + strbuf_parent_directory(&parent); + if (chdir(parent.buf) < 0) + die_errno(_("could not switch to '%s'"), parent.buf); + strbuf_release(&parent); +#endif + + if (remove_dir_recursively(enlistment, 0)) + die(_("failed to delete enlistment directory")); + + return 0; +} + +/* + * Dummy implementation; Using `get_version_info()` would cause a link error + * without this. + */ +void load_builtin_commands(const char *prefix, struct cmdnames *cmds) +{ + die("not implemented"); +} + +static int cmd_clone(int argc, const char **argv) +{ + const char *branch = NULL; + int full_clone = 0, single_branch = 0; + struct option clone_options[] = { + OPT_STRING('b', "branch", &branch, N_("<branch>"), + N_("branch to checkout after clone")), + OPT_BOOL(0, "full-clone", &full_clone, + N_("when cloning, create full working directory")), + OPT_BOOL(0, "single-branch", &single_branch, + N_("only download metadata for the branch that will " + "be checked out")), + OPT_END(), + }; + const char * const clone_usage[] = { + N_("scalar clone [<options>] [--] <repo> [<dir>]"), + NULL + }; + const char *url; + char *enlistment = NULL, *dir = NULL; + struct strbuf buf = STRBUF_INIT; + int res; + + argc = parse_options(argc, argv, NULL, clone_options, clone_usage, 0); + + if (argc == 2) { + url = argv[0]; + enlistment = xstrdup(argv[1]); + } else if (argc == 1) { + url = argv[0]; + + strbuf_addstr(&buf, url); + /* Strip trailing slashes, if any */ + while (buf.len > 0 && is_dir_sep(buf.buf[buf.len - 1])) + strbuf_setlen(&buf, buf.len - 1); + /* Strip suffix `.git`, if any */ + strbuf_strip_suffix(&buf, ".git"); + + enlistment = find_last_dir_sep(buf.buf); + if (!enlistment) { + die(_("cannot deduce worktree name from '%s'"), url); + } + enlistment = xstrdup(enlistment + 1); + } else { + usage_msg_opt(_("You must specify a repository to clone."), + clone_usage, clone_options); + } + + if (is_directory(enlistment)) + die(_("directory '%s' exists already"), enlistment); + + dir = xstrfmt("%s/src", enlistment); + + strbuf_reset(&buf); + if (branch) + strbuf_addf(&buf, "init.defaultBranch=%s", branch); + else { + char *b = repo_default_branch_name(the_repository, 1); + strbuf_addf(&buf, "init.defaultBranch=%s", b); + free(b); + } + + if ((res = run_git("-c", buf.buf, "init", "--", dir, NULL))) + goto cleanup; + + if (chdir(dir) < 0) { + res = error_errno(_("could not switch to '%s'"), dir); + goto cleanup; + } + + setup_git_directory(); + + /* common-main already logs `argv` */ + trace2_def_repo(the_repository); + + if (!branch && !(branch = remote_default_branch(url))) { + res = error(_("failed to get default branch for '%s'"), url); + goto cleanup; + } + + if (set_config("remote.origin.url=%s", url) || + set_config("remote.origin.fetch=" + "+refs/heads/%s:refs/remotes/origin/%s", + single_branch ? branch : "*", + single_branch ? branch : "*") || + set_config("remote.origin.promisor=true") || + set_config("remote.origin.partialCloneFilter=blob:none")) { + res = error(_("could not configure remote in '%s'"), dir); + goto cleanup; + } + + if (!full_clone && + (res = run_git("sparse-checkout", "init", "--cone", NULL))) + goto cleanup; + + if (set_recommended_config(0)) + return error(_("could not configure '%s'"), dir); + + if ((res = run_git("fetch", "--quiet", "origin", NULL))) { + warning(_("partial clone failed; attempting full clone")); + + if (set_config("remote.origin.promisor") || + set_config("remote.origin.partialCloneFilter")) { + res = error(_("could not configure for full clone")); + goto cleanup; + } + + if ((res = run_git("fetch", "--quiet", "origin", NULL))) + goto cleanup; + } + + if ((res = set_config("branch.%s.remote=origin", branch))) + goto cleanup; + if ((res = set_config("branch.%s.merge=refs/heads/%s", + branch, branch))) + goto cleanup; + + strbuf_reset(&buf); + strbuf_addf(&buf, "origin/%s", branch); + res = run_git("checkout", "-f", "-t", buf.buf, NULL); + if (res) + goto cleanup; + + res = register_dir(); + +cleanup: + free(enlistment); + free(dir); + strbuf_release(&buf); + return res; +} + +static int cmd_list(int argc, const char **argv) +{ + if (argc != 1) + die(_("`scalar list` does not take arguments")); + + if (run_git("config", "--global", "--get-all", "scalar.repo", NULL) < 0) + return -1; + return 0; +} + +static int cmd_register(int argc, const char **argv) +{ + struct option options[] = { + OPT_END(), + }; + const char * const usage[] = { + N_("scalar register [<enlistment>]"), + NULL + }; + + argc = parse_options(argc, argv, NULL, options, + usage, 0); + + setup_enlistment_directory(argc, argv, usage, options, NULL); + + return register_dir(); +} + +static int get_scalar_repos(const char *key, const char *value, void *data) +{ + struct string_list *list = data; + + if (!strcmp(key, "scalar.repo")) + string_list_append(list, value); + + return 0; +} + +static int cmd_reconfigure(int argc, const char **argv) +{ + int all = 0; + struct option options[] = { + OPT_BOOL('a', "all", &all, + N_("reconfigure all registered enlistments")), + OPT_END(), + }; + const char * const usage[] = { + N_("scalar reconfigure [--all | <enlistment>]"), + NULL + }; + struct string_list scalar_repos = STRING_LIST_INIT_DUP; + int i, res = 0; + struct repository r = { NULL }; + struct strbuf commondir = STRBUF_INIT, gitdir = STRBUF_INIT; + + argc = parse_options(argc, argv, NULL, options, + usage, 0); + + if (!all) { + setup_enlistment_directory(argc, argv, usage, options, NULL); + + return set_recommended_config(1); + } + + if (argc > 0) + usage_msg_opt(_("--all or <enlistment>, but not both"), + usage, options); + + git_config(get_scalar_repos, &scalar_repos); + + for (i = 0; i < scalar_repos.nr; i++) { + const char *dir = scalar_repos.items[i].string; + + strbuf_reset(&commondir); + strbuf_reset(&gitdir); + + if (chdir(dir) < 0) { + warning_errno(_("could not switch to '%s'"), dir); + res = -1; + } else if (discover_git_directory(&commondir, &gitdir) < 0) { + warning_errno(_("git repository gone in '%s'"), dir); + res = -1; + } else { + git_config_clear(); + + the_repository = &r; + r.commondir = commondir.buf; + r.gitdir = gitdir.buf; + + if (set_recommended_config(1) < 0) + res = -1; + } + } + + string_list_clear(&scalar_repos, 1); + strbuf_release(&commondir); + strbuf_release(&gitdir); + + return res; +} + +static int cmd_run(int argc, const char **argv) +{ + struct option options[] = { + OPT_END(), + }; + struct { + const char *arg, *task; + } tasks[] = { + { "config", NULL }, + { "commit-graph", "commit-graph" }, + { "fetch", "prefetch" }, + { "loose-objects", "loose-objects" }, + { "pack-files", "incremental-repack" }, + { NULL, NULL } + }; + struct strbuf buf = STRBUF_INIT; + const char *usagestr[] = { NULL, NULL }; + int i; + + strbuf_addstr(&buf, N_("scalar run <task> [<enlistment>]\nTasks:\n")); + for (i = 0; tasks[i].arg; i++) + strbuf_addf(&buf, "\t%s\n", tasks[i].arg); + usagestr[0] = buf.buf; + + argc = parse_options(argc, argv, NULL, options, + usagestr, 0); + + if (!argc) + usage_with_options(usagestr, options); + + if (!strcmp("all", argv[0])) { + i = -1; + } else { + for (i = 0; tasks[i].arg && strcmp(tasks[i].arg, argv[0]); i++) + ; /* keep looking for the task */ + + if (i > 0 && !tasks[i].arg) { + error(_("no such task: '%s'"), argv[0]); + usage_with_options(usagestr, options); + } + } + + argc--; + argv++; + setup_enlistment_directory(argc, argv, usagestr, options, NULL); + strbuf_release(&buf); + + if (i == 0) + return register_dir(); + + if (i > 0) + return run_git("maintenance", "run", + "--task", tasks[i].task, NULL); + + if (register_dir()) + return -1; + for (i = 1; tasks[i].arg; i++) + if (run_git("maintenance", "run", + "--task", tasks[i].task, NULL)) + return -1; + return 0; +} + +static int remove_deleted_enlistment(struct strbuf *path) +{ + int res = 0; + strbuf_realpath_forgiving(path, path->buf, 1); + + if (run_git("config", "--global", + "--unset", "--fixed-value", + "scalar.repo", path->buf, NULL) < 0) + res = -1; + + if (run_git("config", "--global", + "--unset", "--fixed-value", + "maintenance.repo", path->buf, NULL) < 0) + res = -1; + + return res; +} + +static int cmd_unregister(int argc, const char **argv) +{ + struct option options[] = { + OPT_END(), + }; + const char * const usage[] = { + N_("scalar unregister [<enlistment>]"), + NULL + }; + + argc = parse_options(argc, argv, NULL, options, + usage, 0); + + /* + * Be forgiving when the enlistment or worktree does not even exist any + * longer; This can be the case if a user deleted the worktree by + * mistake and _still_ wants to unregister the thing. + */ + if (argc == 1) { + struct strbuf src_path = STRBUF_INIT, workdir_path = STRBUF_INIT; + + strbuf_addf(&src_path, "%s/src/.git", argv[0]); + strbuf_addf(&workdir_path, "%s/.git", argv[0]); + if (!is_directory(src_path.buf) && !is_directory(workdir_path.buf)) { + /* remove possible matching registrations */ + int res = -1; + + strbuf_strip_suffix(&src_path, "/.git"); + res = remove_deleted_enlistment(&src_path) && res; + + strbuf_strip_suffix(&workdir_path, "/.git"); + res = remove_deleted_enlistment(&workdir_path) && res; + + strbuf_release(&src_path); + strbuf_release(&workdir_path); + return res; + } + strbuf_release(&src_path); + strbuf_release(&workdir_path); + } + + setup_enlistment_directory(argc, argv, usage, options, NULL); + + return unregister_dir(); +} + +static int cmd_delete(int argc, const char **argv) +{ + char *cwd = xgetcwd(); + struct option options[] = { + OPT_END(), + }; + const char * const usage[] = { + N_("scalar delete <enlistment>"), + NULL + }; + struct strbuf enlistment = STRBUF_INIT; + int res = 0; + + argc = parse_options(argc, argv, NULL, options, + usage, 0); + + if (argc != 1) + usage_with_options(usage, options); + + setup_enlistment_directory(argc, argv, usage, options, &enlistment); + + if (dir_inside_of(cwd, enlistment.buf) >= 0) + res = error(_("refusing to delete current working directory")); + else { + close_object_store(the_repository->objects); + res = delete_enlistment(&enlistment); + } + strbuf_release(&enlistment); + free(cwd); + + return res; +} + +static int cmd_version(int argc, const char **argv) +{ + int verbose = 0, build_options = 0; + struct option options[] = { + OPT__VERBOSE(&verbose, N_("include Git version")), + OPT_BOOL(0, "build-options", &build_options, + N_("include Git's build options")), + OPT_END(), + }; + const char * const usage[] = { + N_("scalar verbose [-v | --verbose] [--build-options]"), + NULL + }; + struct strbuf buf = STRBUF_INIT; + + argc = parse_options(argc, argv, NULL, options, + usage, 0); + + if (argc != 0) + usage_with_options(usage, options); + + get_version_info(&buf, build_options); + fprintf(stderr, "%s\n", buf.buf); + strbuf_release(&buf); + + return 0; +} + +static struct { + const char *name; + int (*fn)(int, const char **); +} builtins[] = { + { "clone", cmd_clone }, + { "list", cmd_list }, + { "register", cmd_register }, + { "unregister", cmd_unregister }, + { "run", cmd_run }, + { "reconfigure", cmd_reconfigure }, + { "delete", cmd_delete }, + { "version", cmd_version }, + { NULL, NULL}, +}; + +int cmd_main(int argc, const char **argv) +{ + struct strbuf scalar_usage = STRBUF_INIT; + int i; + + if (argc > 1) { + argv++; + argc--; + + for (i = 0; builtins[i].name; i++) + if (!strcmp(builtins[i].name, argv[0])) + return !!builtins[i].fn(argc, argv); + } + + strbuf_addstr(&scalar_usage, + N_("scalar <command> [<options>]\n\nCommands:\n")); + for (i = 0; builtins[i].name; i++) + strbuf_addf(&scalar_usage, "\t%s\n", builtins[i].name); + + usage(scalar_usage.buf); +} diff --git a/contrib/scalar/scalar.txt b/contrib/scalar/scalar.txt new file mode 100644 index 0000000000..f416d63728 --- /dev/null +++ b/contrib/scalar/scalar.txt @@ -0,0 +1,145 @@ +scalar(1) +========= + +NAME +---- +scalar - an opinionated repository management tool + +SYNOPSIS +-------- +[verse] +scalar clone [--single-branch] [--branch <main-branch>] [--full-clone] <url> [<enlistment>] +scalar list +scalar register [<enlistment>] +scalar unregister [<enlistment>] +scalar run ( all | config | commit-graph | fetch | loose-objects | pack-files ) [<enlistment>] +scalar reconfigure [ --all | <enlistment> ] +scalar delete <enlistment> + +DESCRIPTION +----------- + +Scalar is an opinionated repository management tool. By creating new +repositories or registering existing repositories with Scalar, your Git +experience will speed up. Scalar sets advanced Git config settings, +maintains your repositories in the background, and helps reduce data sent +across the network. + +An important Scalar concept is the enlistment: this is the top-level directory +of the project. It usually contains the subdirectory `src/` which is a Git +worktree. This encourages the separation between tracked files (inside `src/`) +and untracked files, such as build artifacts (outside `src/`). When registering +an existing Git worktree with Scalar whose name is not `src`, the enlistment +will be identical to the worktree. + +The `scalar` command implements various subcommands, and different options +depending on the subcommand. With the exception of `clone`, `list` and +`reconfigure --all`, all subcommands expect to be run in an enlistment. + +COMMANDS +-------- + +Clone +~~~~~ + +clone [<options>] <url> [<enlistment>]:: + Clones the specified repository, similar to linkgit:git-clone[1]. By + default, only commit and tree objects are cloned. Once finished, the + worktree is located at `<enlistment>/src`. ++ +The sparse-checkout feature is enabled (except when run with `--full-clone`) +and the only files present are those in the top-level directory. Use +`git sparse-checkout set` to expand the set of directories you want to see, +or `git sparse-checkout disable` to expand to all files (see +linkgit:git-sparse-checkout[1] for more details). You can explore the +subdirectories outside your sparse-checkout by using `git ls-tree +HEAD[:<directory>]`. + +-b <name>:: +--branch <name>:: + Instead of checking out the branch pointed to by the cloned + repository's HEAD, check out the `<name>` branch instead. + +--[no-]single-branch:: + Clone only the history leading to the tip of a single branch, either + specified by the `--branch` option or the primary branch remote's + `HEAD` points at. ++ +Further fetches into the resulting repository will only update the +remote-tracking branch for the branch this option was used for the initial +cloning. If the HEAD at the remote did not point at any branch when +`--single-branch` clone was made, no remote-tracking branch is created. + +--[no-]full-clone:: + A sparse-checkout is initialized by default. This behavior can be + turned off via `--full-clone`. + +List +~~~~ + +list:: + List enlistments that are currently registered by Scalar. This + subcommand does not need to be run inside an enlistment. + +Register +~~~~~~~~ + +register [<enlistment>]:: + Adds the enlistment's repository to the list of registered repositories + and starts background maintenance. If `<enlistment>` is not provided, + then the enlistment associated with the current working directory is + registered. ++ +Note: when this subcommand is called in a worktree that is called `src/`, its +parent directory is considered to be the Scalar enlistment. If the worktree is +_not_ called `src/`, it itself will be considered to be the Scalar enlistment. + +Unregister +~~~~~~~~~~ + +unregister [<enlistment>]:: + Remove the specified repository from the list of repositories + registered with Scalar and stop the scheduled background maintenance. + +Run +~~~ + +scalar run ( all | config | commit-graph | fetch | loose-objects | pack-files ) [<enlistment>]:: + Run the given maintenance task (or all tasks, if `all` was specified). + Except for `all` and `config`, this subcommand simply hands off to + linkgit:git-maintenance[1] (mapping `fetch` to `prefetch` and + `pack-files` to `incremental-repack`). ++ +These tasks are run automatically as part of the scheduled maintenance, +as soon as the repository is registered with Scalar. It should therefore +not be necessary to run this subcommand manually. ++ +The `config` task is specific to Scalar and configures all those +opinionated default settings that make Git work more efficiently with +large repositories. As this task is run as part of `scalar clone` +automatically, explicit invocations of this task are rarely needed. + +Reconfigure +~~~~~~~~~~~ + +After a Scalar upgrade, or when the configuration of a Scalar enlistment +was somehow corrupted or changed by mistake, this subcommand allows to +reconfigure the enlistment. + +With the `--all` option, all enlistments currently registered with Scalar +will be reconfigured. Use this option after each Scalar upgrade. + +Delete +~~~~~~ + +delete <enlistment>:: + This subcommand lets you delete an existing Scalar enlistment from your + local file system, unregistering the repository. + +SEE ALSO +-------- +linkgit:git-clone[1], linkgit:git-maintenance[1]. + +Scalar +--- +Associated with the linkgit:git[1] suite diff --git a/contrib/scalar/t/Makefile b/contrib/scalar/t/Makefile new file mode 100644 index 0000000000..6170672bb3 --- /dev/null +++ b/contrib/scalar/t/Makefile @@ -0,0 +1,78 @@ +# Run scalar tests +# +# Copyright (c) 2005,2021 Junio C Hamano, Johannes Schindelin +# + +-include ../../../config.mak.autogen +-include ../../../config.mak + +SHELL_PATH ?= $(SHELL) +PERL_PATH ?= /usr/bin/perl +RM ?= rm -f +PROVE ?= prove +DEFAULT_TEST_TARGET ?= test +TEST_LINT ?= test-lint + +ifdef TEST_OUTPUT_DIRECTORY +TEST_RESULTS_DIRECTORY = $(TEST_OUTPUT_DIRECTORY)/test-results +else +TEST_RESULTS_DIRECTORY = ../../../t/test-results +endif + +# Shell quote; +SHELL_PATH_SQ = $(subst ','\'',$(SHELL_PATH)) +PERL_PATH_SQ = $(subst ','\'',$(PERL_PATH)) +TEST_RESULTS_DIRECTORY_SQ = $(subst ','\'',$(TEST_RESULTS_DIRECTORY)) + +T = $(sort $(wildcard t[0-9][0-9][0-9][0-9]-*.sh)) + +all: $(DEFAULT_TEST_TARGET) + +test: $(TEST_LINT) + $(MAKE) aggregate-results-and-cleanup + +prove: $(TEST_LINT) + @echo "*** prove ***"; GIT_CONFIG=.git/config $(PROVE) --exec '$(SHELL_PATH_SQ)' $(GIT_PROVE_OPTS) $(T) :: $(GIT_TEST_OPTS) + $(MAKE) clean-except-prove-cache + +$(T): + @echo "*** $@ ***"; GIT_CONFIG=.git/config '$(SHELL_PATH_SQ)' $@ $(GIT_TEST_OPTS) + +clean-except-prove-cache: + $(RM) -r 'trash directory'.* '$(TEST_RESULTS_DIRECTORY_SQ)' + $(RM) -r valgrind/bin + +clean: clean-except-prove-cache + $(RM) .prove + +test-lint: test-lint-duplicates test-lint-executable test-lint-shell-syntax + +test-lint-duplicates: + @dups=`echo $(T) | tr ' ' '\n' | sed 's/-.*//' | sort | uniq -d` && \ + test -z "$$dups" || { \ + echo >&2 "duplicate test numbers:" $$dups; exit 1; } + +test-lint-executable: + @bad=`for i in $(T); do test -x "$$i" || echo $$i; done` && \ + test -z "$$bad" || { \ + echo >&2 "non-executable tests:" $$bad; exit 1; } + +test-lint-shell-syntax: + @'$(PERL_PATH_SQ)' ../../../t/check-non-portable-shell.pl $(T) + +aggregate-results-and-cleanup: $(T) + $(MAKE) aggregate-results + $(MAKE) clean + +aggregate-results: + for f in '$(TEST_RESULTS_DIRECTORY_SQ)'/t*-*.counts; do \ + echo "$$f"; \ + done | '$(SHELL_PATH_SQ)' ../../../t/aggregate-results.sh + +valgrind: + $(MAKE) GIT_TEST_OPTS="$(GIT_TEST_OPTS) --valgrind" + +test-results: + mkdir -p test-results + +.PHONY: $(T) aggregate-results clean valgrind diff --git a/contrib/scalar/t/t9099-scalar.sh b/contrib/scalar/t/t9099-scalar.sh new file mode 100755 index 0000000000..2e1502ad45 --- /dev/null +++ b/contrib/scalar/t/t9099-scalar.sh @@ -0,0 +1,88 @@ +#!/bin/sh + +test_description='test the `scalar` command' + +TEST_DIRECTORY=$PWD/../../../t +export TEST_DIRECTORY + +# Make it work with --no-bin-wrappers +PATH=$PWD/..:$PATH + +. ../../../t/test-lib.sh + +GIT_TEST_MAINT_SCHEDULER="crontab:test-tool crontab ../cron.txt,launchctl:true,schtasks:true" +export GIT_TEST_MAINT_SCHEDULER + +test_expect_success 'scalar shows a usage' ' + test_expect_code 129 scalar -h +' + +test_expect_success 'scalar unregister' ' + git init vanish/src && + scalar register vanish/src && + git config --get --global --fixed-value \ + maintenance.repo "$(pwd)/vanish/src" && + scalar list >scalar.repos && + grep -F "$(pwd)/vanish/src" scalar.repos && + rm -rf vanish/src/.git && + scalar unregister vanish && + test_must_fail git config --get --global --fixed-value \ + maintenance.repo "$(pwd)/vanish/src" && + scalar list >scalar.repos && + ! grep -F "$(pwd)/vanish/src" scalar.repos +' + +test_expect_success 'set up repository to clone' ' + test_commit first && + test_commit second && + test_commit third && + git switch -c parallel first && + mkdir -p 1/2 && + test_commit 1/2/3 && + git config uploadPack.allowFilter true && + git config uploadPack.allowAnySHA1InWant true +' + +test_expect_success 'scalar clone' ' + second=$(git rev-parse --verify second:second.t) && + scalar clone "file://$(pwd)" cloned --single-branch && + ( + cd cloned/src && + + git config --get --global --fixed-value maintenance.repo \ + "$(pwd)" && + + git for-each-ref --format="%(refname)" refs/remotes/origin/ >actual && + echo "refs/remotes/origin/parallel" >expect && + test_cmp expect actual && + + test_path_is_missing 1/2 && + test_must_fail git rev-list --missing=print $second && + git rev-list $second && + git cat-file blob $second >actual && + echo "second" >expect && + test_cmp expect actual + ) +' + +test_expect_success 'scalar reconfigure' ' + git init one/src && + scalar register one && + git -C one/src config core.preloadIndex false && + scalar reconfigure one && + test true = "$(git -C one/src config core.preloadIndex)" && + git -C one/src config core.preloadIndex false && + scalar reconfigure -a && + test true = "$(git -C one/src config core.preloadIndex)" +' + +test_expect_success 'scalar delete without enlistment shows a usage' ' + test_expect_code 129 scalar delete +' + +test_expect_success 'scalar delete with enlistment' ' + scalar delete cloned && + test_path_is_missing cloned +' + +test_done diff --git a/contrib/subtree/t/t7900-subtree.sh b/contrib/subtree/t/t7900-subtree.sh index 4153b65321..1c1f76f04a 100755 --- a/contrib/subtree/t/t7900-subtree.sh +++ b/contrib/subtree/t/t7900-subtree.sh @@ -1445,7 +1445,7 @@ test_expect_success 'subtree descendant check' ' ) && test_create_commit "$test_count" folder_subtree/0 && test_create_commit "$test_count" folder_subtree/b && - cherry=$(cd "$test_count"; git rev-parse HEAD) && + cherry=$(cd "$test_count" && git rev-parse HEAD) && ( cd "$test_count" && git checkout branch @@ -613,7 +613,7 @@ static int crlf_to_worktree(const char *src, size_t len, struct strbuf *buf, struct filter_params { const char *src; - unsigned long size; + size_t size; int fd; const char *cmd; const char *path; @@ -232,13 +232,13 @@ static const char *path_ok(const char *directory, struct hostinfo *hi) rlen = strlcpy(interp_path, expanded_path.buf, sizeof(interp_path)); + strbuf_release(&expanded_path); if (rlen >= sizeof(interp_path)) { logerror("interpolated path too large: %s", interp_path); return NULL; } - strbuf_release(&expanded_path); loginfo("Interpolated dir '%s'", interp_path); dir = interp_path; @@ -326,22 +326,18 @@ static int run_access_hook(struct daemon_service *service, const char *dir, { struct child_process child = CHILD_PROCESS_INIT; struct strbuf buf = STRBUF_INIT; - const char *argv[8]; - const char **arg = argv; char *eol; int seen_errors = 0; - *arg++ = access_hook; - *arg++ = service->name; - *arg++ = path; - *arg++ = hi->hostname.buf; - *arg++ = get_canon_hostname(hi); - *arg++ = get_ip_address(hi); - *arg++ = hi->tcp_port.buf; - *arg = NULL; + strvec_push(&child.args, access_hook); + strvec_push(&child.args, service->name); + strvec_push(&child.args, path); + strvec_push(&child.args, hi->hostname.buf); + strvec_push(&child.args, get_canon_hostname(hi)); + strvec_push(&child.args, get_ip_address(hi)); + strvec_push(&child.args, hi->tcp_port.buf); child.use_shell = 1; - child.argv = argv; child.no_stdin = 1; child.no_stderr = 1; child.out = -1; @@ -922,7 +918,7 @@ static void handle(int incoming, struct sockaddr *addr, socklen_t addrlen) #endif } - cld.argv = cld_argv.v; + strvec_pushv(&cld.args, cld_argv.v); cld.in = incoming; cld.out = dup(incoming); @@ -9,7 +9,7 @@ /* * This is like mktime, but without normalization of tm_wday and tm_yday. */ -static time_t tm_to_time_t(const struct tm *tm) +time_t tm_to_time_t(const struct tm *tm) { static const int mdays[] = { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 @@ -90,15 +90,15 @@ static inline unsigned long get_delta_hdr_size(const unsigned char **datap, const unsigned char *top) { const unsigned char *data = *datap; - unsigned long cmd, size = 0; + size_t cmd, size = 0; int i = 0; do { cmd = *data++; - size |= (cmd & 0x7f) << i; + size |= st_left_shift(cmd & 0x7f, i); i += 7; } while (cmd & 0x80 && data < top); *datap = data; - return size; + return cast_size_t_to_ulong(size); } #endif @@ -18,6 +18,7 @@ #include "submodule-config.h" #include "submodule.h" #include "hashmap.h" +#include "mem-pool.h" #include "ll-merge.h" #include "string-list.h" #include "strvec.h" @@ -773,6 +774,7 @@ struct emitted_diff_symbol { int flags; int indent_off; /* Offset to first non-whitespace character */ int indent_width; /* The visual width of the indentation */ + unsigned id; enum diff_symbol s; }; #define EMITTED_DIFF_SYMBOL_INIT { 0 } @@ -798,9 +800,9 @@ static void append_emitted_diff_symbol(struct diff_options *o, } struct moved_entry { - struct hashmap_entry ent; const struct emitted_diff_symbol *es; struct moved_entry *next_line; + struct moved_entry *next_match; }; struct moved_block { @@ -808,11 +810,6 @@ struct moved_block { int wsd; /* The whitespace delta of this block */ }; -static void moved_block_clear(struct moved_block *b) -{ - memset(b, 0, sizeof(*b)); -} - #define INDENT_BLANKLINE INT_MIN static void fill_es_indent_data(struct emitted_diff_symbol *es) @@ -856,79 +853,41 @@ static void fill_es_indent_data(struct emitted_diff_symbol *es) } static int compute_ws_delta(const struct emitted_diff_symbol *a, - const struct emitted_diff_symbol *b, - int *out) -{ - int a_len = a->len, - b_len = b->len, - a_off = a->indent_off, - a_width = a->indent_width, - b_off = b->indent_off, + const struct emitted_diff_symbol *b) +{ + int a_width = a->indent_width, b_width = b->indent_width; - int delta; - if (a_width == INDENT_BLANKLINE && b_width == INDENT_BLANKLINE) { - *out = INDENT_BLANKLINE; - return 1; - } - - if (a->s == DIFF_SYMBOL_PLUS) - delta = a_width - b_width; - else - delta = b_width - a_width; - - if (a_len - a_off != b_len - b_off || - memcmp(a->line + a_off, b->line + b_off, a_len - a_off)) - return 0; + if (a_width == INDENT_BLANKLINE && b_width == INDENT_BLANKLINE) + return INDENT_BLANKLINE; - *out = delta; - - return 1; + return a_width - b_width; } -static int cmp_in_block_with_wsd(const struct diff_options *o, - const struct moved_entry *cur, - const struct moved_entry *match, - struct moved_block *pmb, - int n) -{ - struct emitted_diff_symbol *l = &o->emitted_symbols->buf[n]; - int al = cur->es->len, bl = match->es->len, cl = l->len; - const char *a = cur->es->line, - *b = match->es->line, - *c = l->line; - int a_off = cur->es->indent_off, - a_width = cur->es->indent_width, - c_off = l->indent_off, - c_width = l->indent_width; +static int cmp_in_block_with_wsd(const struct moved_entry *cur, + const struct emitted_diff_symbol *l, + struct moved_block *pmb) +{ + int a_width = cur->es->indent_width, b_width = l->indent_width; int delta; - /* - * We need to check if 'cur' is equal to 'match'. As those - * are from the same (+/-) side, we do not need to adjust for - * indent changes. However these were found using fuzzy - * matching so we do have to check if they are equal. Here we - * just check the lengths. We delay calling memcmp() to check - * the contents until later as if the length comparison for a - * and c fails we can avoid the call all together. - */ - if (al != bl) + /* The text of each line must match */ + if (cur->es->id != l->id) return 1; - /* If 'l' and 'cur' are both blank then they match. */ - if (a_width == INDENT_BLANKLINE && c_width == INDENT_BLANKLINE) + /* + * If 'l' and 'cur' are both blank then we don't need to check the + * indent. We only need to check cur as we know the strings match. + * */ + if (a_width == INDENT_BLANKLINE) return 0; /* * The indent changes of the block are known and stored in pmb->wsd; * however we need to check if the indent changes of the current line - * match those of the current block and that the text of 'l' and 'cur' - * after the indentation match. + * match those of the current block. */ - if (cur->es->s == DIFF_SYMBOL_PLUS) - delta = a_width - c_width; - else - delta = c_width - a_width; + delta = b_width - a_width; /* * If the previous lines of this block were all blank then set its @@ -937,166 +896,165 @@ static int cmp_in_block_with_wsd(const struct diff_options *o, if (pmb->wsd == INDENT_BLANKLINE) pmb->wsd = delta; - return !(delta == pmb->wsd && al - a_off == cl - c_off && - !memcmp(a, b, al) && ! - memcmp(a + a_off, c + c_off, al - a_off)); + return delta != pmb->wsd; } -static int moved_entry_cmp(const void *hashmap_cmp_fn_data, - const struct hashmap_entry *eptr, - const struct hashmap_entry *entry_or_key, - const void *keydata) +struct interned_diff_symbol { + struct hashmap_entry ent; + struct emitted_diff_symbol *es; +}; + +static int interned_diff_symbol_cmp(const void *hashmap_cmp_fn_data, + const struct hashmap_entry *eptr, + const struct hashmap_entry *entry_or_key, + const void *keydata) { const struct diff_options *diffopt = hashmap_cmp_fn_data; - const struct moved_entry *a, *b; + const struct emitted_diff_symbol *a, *b; unsigned flags = diffopt->color_moved_ws_handling & XDF_WHITESPACE_FLAGS; - a = container_of(eptr, const struct moved_entry, ent); - b = container_of(entry_or_key, const struct moved_entry, ent); - - if (diffopt->color_moved_ws_handling & - COLOR_MOVED_WS_ALLOW_INDENTATION_CHANGE) - /* - * As there is not specific white space config given, - * we'd need to check for a new block, so ignore all - * white space. The setup of the white space - * configuration for the next block is done else where - */ - flags |= XDF_IGNORE_WHITESPACE; + a = container_of(eptr, const struct interned_diff_symbol, ent)->es; + b = container_of(entry_or_key, const struct interned_diff_symbol, ent)->es; - return !xdiff_compare_lines(a->es->line, a->es->len, - b->es->line, b->es->len, - flags); + return !xdiff_compare_lines(a->line + a->indent_off, + a->len - a->indent_off, + b->line + b->indent_off, + b->len - b->indent_off, flags); } -static struct moved_entry *prepare_entry(struct diff_options *o, - int line_no) +static void prepare_entry(struct diff_options *o, struct emitted_diff_symbol *l, + struct interned_diff_symbol *s) { - struct moved_entry *ret = xmalloc(sizeof(*ret)); - struct emitted_diff_symbol *l = &o->emitted_symbols->buf[line_no]; unsigned flags = o->color_moved_ws_handling & XDF_WHITESPACE_FLAGS; - unsigned int hash = xdiff_hash_string(l->line, l->len, flags); - - hashmap_entry_init(&ret->ent, hash); - ret->es = l; - ret->next_line = NULL; + unsigned int hash = xdiff_hash_string(l->line + l->indent_off, + l->len - l->indent_off, flags); - return ret; + hashmap_entry_init(&s->ent, hash); + s->es = l; } -static void add_lines_to_move_detection(struct diff_options *o, - struct hashmap *add_lines, - struct hashmap *del_lines) +struct moved_entry_list { + struct moved_entry *add, *del; +}; + +static struct moved_entry_list *add_lines_to_move_detection(struct diff_options *o, + struct mem_pool *entry_mem_pool) { struct moved_entry *prev_line = NULL; - + struct mem_pool interned_pool; + struct hashmap interned_map; + struct moved_entry_list *entry_list = NULL; + size_t entry_list_alloc = 0; + unsigned id = 0; int n; + + hashmap_init(&interned_map, interned_diff_symbol_cmp, o, 8096); + mem_pool_init(&interned_pool, 1024 * 1024); + for (n = 0; n < o->emitted_symbols->nr; n++) { - struct hashmap *hm; - struct moved_entry *key; + struct interned_diff_symbol key; + struct emitted_diff_symbol *l = &o->emitted_symbols->buf[n]; + struct interned_diff_symbol *s; + struct moved_entry *entry; - switch (o->emitted_symbols->buf[n].s) { - case DIFF_SYMBOL_PLUS: - hm = add_lines; - break; - case DIFF_SYMBOL_MINUS: - hm = del_lines; - break; - default: + if (l->s != DIFF_SYMBOL_PLUS && l->s != DIFF_SYMBOL_MINUS) { prev_line = NULL; continue; } if (o->color_moved_ws_handling & COLOR_MOVED_WS_ALLOW_INDENTATION_CHANGE) - fill_es_indent_data(&o->emitted_symbols->buf[n]); - key = prepare_entry(o, n); - if (prev_line && prev_line->es->s == o->emitted_symbols->buf[n].s) - prev_line->next_line = key; + fill_es_indent_data(l); - hashmap_add(hm, &key->ent); - prev_line = key; + prepare_entry(o, l, &key); + s = hashmap_get_entry(&interned_map, &key, ent, &key.ent); + if (s) { + l->id = s->es->id; + } else { + l->id = id; + ALLOC_GROW_BY(entry_list, id, 1, entry_list_alloc); + hashmap_add(&interned_map, + memcpy(mem_pool_alloc(&interned_pool, + sizeof(key)), + &key, sizeof(key))); + } + entry = mem_pool_alloc(entry_mem_pool, sizeof(*entry)); + entry->es = l; + entry->next_line = NULL; + if (prev_line && prev_line->es->s == l->s) + prev_line->next_line = entry; + prev_line = entry; + if (l->s == DIFF_SYMBOL_PLUS) { + entry->next_match = entry_list[l->id].add; + entry_list[l->id].add = entry; + } else { + entry->next_match = entry_list[l->id].del; + entry_list[l->id].del = entry; + } } + + hashmap_clear(&interned_map); + mem_pool_discard(&interned_pool, 0); + + return entry_list; } static void pmb_advance_or_null(struct diff_options *o, - struct moved_entry *match, - struct hashmap *hm, + struct emitted_diff_symbol *l, struct moved_block *pmb, - int pmb_nr) + int *pmb_nr) { - int i; - for (i = 0; i < pmb_nr; i++) { + int i, j; + + for (i = 0, j = 0; i < *pmb_nr; i++) { + int match; struct moved_entry *prev = pmb[i].match; struct moved_entry *cur = (prev && prev->next_line) ? prev->next_line : NULL; - if (cur && !hm->cmpfn(o, &cur->ent, &match->ent, NULL)) { - pmb[i].match = cur; - } else { - pmb[i].match = NULL; - } - } -} -static void pmb_advance_or_null_multi_match(struct diff_options *o, - struct moved_entry *match, - struct hashmap *hm, - struct moved_block *pmb, - int pmb_nr, int n) -{ - int i; - char *got_match = xcalloc(1, pmb_nr); - - hashmap_for_each_entry_from(hm, match, ent) { - for (i = 0; i < pmb_nr; i++) { - struct moved_entry *prev = pmb[i].match; - struct moved_entry *cur = (prev && prev->next_line) ? - prev->next_line : NULL; - if (!cur) - continue; - if (!cmp_in_block_with_wsd(o, cur, match, &pmb[i], n)) - got_match[i] |= 1; - } - } + if (o->color_moved_ws_handling & + COLOR_MOVED_WS_ALLOW_INDENTATION_CHANGE) + match = cur && + !cmp_in_block_with_wsd(cur, l, &pmb[i]); + else + match = cur && cur->es->id == l->id; - for (i = 0; i < pmb_nr; i++) { - if (got_match[i]) { - /* Advance to the next line */ - pmb[i].match = pmb[i].match->next_line; - } else { - moved_block_clear(&pmb[i]); + if (match) { + pmb[j] = pmb[i]; + pmb[j++].match = cur; } } - - free(got_match); + *pmb_nr = j; } -static int shrink_potential_moved_blocks(struct moved_block *pmb, - int pmb_nr) -{ - int lp, rp; - - /* Shrink the set of potential block to the remaining running */ - for (lp = 0, rp = pmb_nr - 1; lp <= rp;) { - while (lp < pmb_nr && pmb[lp].match) - lp++; - /* lp points at the first NULL now */ +static void fill_potential_moved_blocks(struct diff_options *o, + struct moved_entry *match, + struct emitted_diff_symbol *l, + struct moved_block **pmb_p, + int *pmb_alloc_p, int *pmb_nr_p) - while (rp > -1 && !pmb[rp].match) - rp--; - /* rp points at the last non-NULL */ +{ + struct moved_block *pmb = *pmb_p; + int pmb_alloc = *pmb_alloc_p, pmb_nr = *pmb_nr_p; - if (lp < pmb_nr && rp > -1 && lp < rp) { - pmb[lp] = pmb[rp]; - memset(&pmb[rp], 0, sizeof(pmb[rp])); - rp--; - lp++; - } + /* + * The current line is the start of a new block. + * Setup the set of potential blocks. + */ + for (; match; match = match->next_match) { + ALLOC_GROW(pmb, pmb_nr + 1, pmb_alloc); + if (o->color_moved_ws_handling & + COLOR_MOVED_WS_ALLOW_INDENTATION_CHANGE) + pmb[pmb_nr].wsd = compute_ws_delta(l, match->es); + else + pmb[pmb_nr].wsd = 0; + pmb[pmb_nr++].match = match; } - /* Remember the number of running sets */ - return rp + 1; + *pmb_p = pmb; + *pmb_alloc_p = pmb_alloc; + *pmb_nr_p = pmb_nr; } /* @@ -1115,6 +1073,8 @@ static int shrink_potential_moved_blocks(struct moved_block *pmb, * NEEDSWORK: This uses the same heuristic as blame_entry_score() in blame.c. * Think of a way to unify them. */ +#define DIFF_SYMBOL_MOVED_LINE_ZEBRA_MASK \ + (DIFF_SYMBOL_MOVED_LINE | DIFF_SYMBOL_MOVED_LINE_ALT) static int adjust_last_block(struct diff_options *o, int n, int block_length) { int i, alnum_count = 0; @@ -1131,95 +1091,85 @@ static int adjust_last_block(struct diff_options *o, int n, int block_length) } } for (i = 1; i < block_length + 1; i++) - o->emitted_symbols->buf[n - i].flags &= ~DIFF_SYMBOL_MOVED_LINE; + o->emitted_symbols->buf[n - i].flags &= ~DIFF_SYMBOL_MOVED_LINE_ZEBRA_MASK; return 0; } /* Find blocks of moved code, delegate actual coloring decision to helper */ static void mark_color_as_moved(struct diff_options *o, - struct hashmap *add_lines, - struct hashmap *del_lines) + struct moved_entry_list *entry_list) { struct moved_block *pmb = NULL; /* potentially moved blocks */ int pmb_nr = 0, pmb_alloc = 0; int n, flipped_block = 0, block_length = 0; + enum diff_symbol moved_symbol = DIFF_SYMBOL_BINARY_DIFF_HEADER; for (n = 0; n < o->emitted_symbols->nr; n++) { - struct hashmap *hm = NULL; - struct moved_entry *key; struct moved_entry *match = NULL; struct emitted_diff_symbol *l = &o->emitted_symbols->buf[n]; - enum diff_symbol last_symbol = 0; switch (l->s) { case DIFF_SYMBOL_PLUS: - hm = del_lines; - key = prepare_entry(o, n); - match = hashmap_get_entry(hm, key, ent, NULL); - free(key); + match = entry_list[l->id].del; break; case DIFF_SYMBOL_MINUS: - hm = add_lines; - key = prepare_entry(o, n); - match = hashmap_get_entry(hm, key, ent, NULL); - free(key); + match = entry_list[l->id].add; break; default: flipped_block = 0; } - if (!match) { - int i; - - adjust_last_block(o, n, block_length); - for(i = 0; i < pmb_nr; i++) - moved_block_clear(&pmb[i]); + if (pmb_nr && (!match || l->s != moved_symbol)) { + if (!adjust_last_block(o, n, block_length) && + block_length > 1) { + /* + * Rewind in case there is another match + * starting at the second line of the block + */ + match = NULL; + n -= block_length; + } pmb_nr = 0; block_length = 0; flipped_block = 0; - last_symbol = l->s; + } + if (!match) { + moved_symbol = DIFF_SYMBOL_BINARY_DIFF_HEADER; continue; } if (o->color_moved == COLOR_MOVED_PLAIN) { - last_symbol = l->s; l->flags |= DIFF_SYMBOL_MOVED_LINE; continue; } - if (o->color_moved_ws_handling & - COLOR_MOVED_WS_ALLOW_INDENTATION_CHANGE) - pmb_advance_or_null_multi_match(o, match, hm, pmb, pmb_nr, n); - else - pmb_advance_or_null(o, match, hm, pmb, pmb_nr); - - pmb_nr = shrink_potential_moved_blocks(pmb, pmb_nr); + pmb_advance_or_null(o, l, pmb, &pmb_nr); if (pmb_nr == 0) { - /* - * The current line is the start of a new block. - * Setup the set of potential blocks. - */ - hashmap_for_each_entry_from(hm, match, ent) { - ALLOC_GROW(pmb, pmb_nr + 1, pmb_alloc); - if (o->color_moved_ws_handling & - COLOR_MOVED_WS_ALLOW_INDENTATION_CHANGE) { - if (compute_ws_delta(l, match->es, - &pmb[pmb_nr].wsd)) - pmb[pmb_nr++].match = match; - } else { - pmb[pmb_nr].wsd = 0; - pmb[pmb_nr++].match = match; - } - } + int contiguous = adjust_last_block(o, n, block_length); + + if (!contiguous && block_length > 1) + /* + * Rewind in case there is another match + * starting at the second line of the block + */ + n -= block_length; + else + fill_potential_moved_blocks(o, match, l, + &pmb, &pmb_alloc, + &pmb_nr); - if (adjust_last_block(o, n, block_length) && - pmb_nr && last_symbol != l->s) + if (contiguous && pmb_nr && moved_symbol == l->s) flipped_block = (flipped_block + 1) % 2; else flipped_block = 0; + if (pmb_nr) + moved_symbol = l->s; + else + moved_symbol = DIFF_SYMBOL_BINARY_DIFF_HEADER; + block_length = 0; } @@ -1229,17 +1179,12 @@ static void mark_color_as_moved(struct diff_options *o, if (flipped_block && o->color_moved != COLOR_MOVED_BLOCKS) l->flags |= DIFF_SYMBOL_MOVED_LINE_ALT; } - last_symbol = l->s; } adjust_last_block(o, n, block_length); - for(n = 0; n < pmb_nr; n++) - moved_block_clear(&pmb[n]); free(pmb); } -#define DIFF_SYMBOL_MOVED_LINE_ZEBRA_MASK \ - (DIFF_SYMBOL_MOVED_LINE | DIFF_SYMBOL_MOVED_LINE_ALT) static void dim_moved_lines(struct diff_options *o) { int n; @@ -1573,7 +1518,9 @@ static void emit_diff_symbol_from_struct(struct diff_options *o, static void emit_diff_symbol(struct diff_options *o, enum diff_symbol s, const char *line, int len, unsigned flags) { - struct emitted_diff_symbol e = {line, len, flags, 0, 0, s}; + struct emitted_diff_symbol e = { + .line = line, .len = len, .flags = flags, .s = s + }; if (o->emitted_symbols) append_emitted_diff_symbol(o, &e); @@ -6345,24 +6292,18 @@ static void diff_flush_patch_all_file_pairs(struct diff_options *o) if (o->emitted_symbols) { if (o->color_moved) { - struct hashmap add_lines, del_lines; - - if (o->color_moved_ws_handling & - COLOR_MOVED_WS_ALLOW_INDENTATION_CHANGE) - o->color_moved_ws_handling |= XDF_IGNORE_WHITESPACE; - - hashmap_init(&del_lines, moved_entry_cmp, o, 0); - hashmap_init(&add_lines, moved_entry_cmp, o, 0); + struct mem_pool entry_pool; + struct moved_entry_list *entry_list; - add_lines_to_move_detection(o, &add_lines, &del_lines); - mark_color_as_moved(o, &add_lines, &del_lines); + mem_pool_init(&entry_pool, 1024 * 1024); + entry_list = add_lines_to_move_detection(o, + &entry_pool); + mark_color_as_moved(o, entry_list); if (o->color_moved == COLOR_MOVED_ZEBRA_DIM) dim_moved_lines(o); - hashmap_clear_and_free(&add_lines, struct moved_entry, - ent); - hashmap_clear_and_free(&del_lines, struct moved_entry, - ent); + mem_pool_discard(&entry_pool, 0); + free(entry_list); } for (i = 0; i < esm.nr; i++) @@ -6921,19 +6862,15 @@ static char *run_textconv(struct repository *r, size_t *outsize) { struct diff_tempfile *temp; - const char *argv[3]; - const char **arg = argv; struct child_process child = CHILD_PROCESS_INIT; struct strbuf buf = STRBUF_INIT; int err = 0; temp = prepare_temp_file(r, spec->path, spec); - *arg++ = pgm; - *arg++ = temp->name; - *arg = NULL; + strvec_push(&child.args, pgm); + strvec_push(&child.args, temp->name); child.use_shell = 1; - child.argv = argv; child.out = -1; if (start_command(&child)) { remove_tempfile(); diff --git a/diffcore-delta.c b/diffcore-delta.c index 5668ace60d..18d8f766d7 100644 --- a/diffcore-delta.c +++ b/diffcore-delta.c @@ -133,10 +133,10 @@ static struct spanhash_top *hash_chars(struct repository *r, i = INITIAL_HASH_SIZE; hash = xmalloc(st_add(sizeof(*hash), - st_mult(sizeof(struct spanhash), 1<<i))); + st_mult(sizeof(struct spanhash), (size_t)1 << i))); hash->alloc_log2 = i; hash->free = INITIAL_FREE(i); - memset(hash->data, 0, sizeof(struct spanhash) * (1<<i)); + memset(hash->data, 0, sizeof(struct spanhash) * ((size_t)1 << i)); n = 0; accum1 = accum2 = 0; @@ -159,7 +159,7 @@ static struct spanhash_top *hash_chars(struct repository *r, n = 0; accum1 = accum2 = 0; } - QSORT(hash->data, 1ul << hash->alloc_log2, spanhash_cmp); + QSORT(hash->data, (size_t)1ul << hash->alloc_log2, spanhash_cmp); return hash; } @@ -727,7 +727,7 @@ static void add_pattern_to_hashsets(struct pattern_list *pl, struct path_pattern } if (given->patternlen < 2 || - *given->pattern == '*' || + *given->pattern != '/' || strstr(given->pattern, "**")) { /* Not a cone pattern. */ warning(_("unrecognized pattern: '%s'"), given->pattern); @@ -819,9 +819,7 @@ static void add_pattern_to_hashsets(struct pattern_list *pl, struct path_pattern /* we already included this at the parent level */ warning(_("your sparse-checkout file may have issues: pattern '%s' is repeated"), given->pattern); - hashmap_remove(&pl->parent_hashmap, &translated->ent, &data); - free(data); - free(translated); + goto clear_hashmaps; } return; @@ -3160,6 +3158,7 @@ static int remove_dir_recurse(struct strbuf *path, int flag, int *kept_up) int ret = 0, original_len = path->len, len, kept_down = 0; int only_empty = (flag & REMOVE_DIR_EMPTY_ONLY); int keep_toplevel = (flag & REMOVE_DIR_KEEP_TOPLEVEL); + int purge_original_cwd = (flag & REMOVE_DIR_PURGE_ORIGINAL_CWD); struct object_id submodule_head; if ((flag & REMOVE_DIR_KEEP_NESTED_GIT) && @@ -3215,9 +3214,14 @@ static int remove_dir_recurse(struct strbuf *path, int flag, int *kept_up) closedir(dir); strbuf_setlen(path, original_len); - if (!ret && !keep_toplevel && !kept_down) - ret = (!rmdir(path->buf) || errno == ENOENT) ? 0 : -1; - else if (kept_up) + if (!ret && !keep_toplevel && !kept_down) { + if (!purge_original_cwd && + startup_info->original_cwd && + !strcmp(startup_info->original_cwd, path->buf)) + ret = -1; /* Do not remove current working directory */ + else + ret = (!rmdir(path->buf) || errno == ENOENT) ? 0 : -1; + } else if (kept_up) /* * report the uplevel that it is not an error that we * did not rmdir() our directory. @@ -3283,6 +3287,9 @@ int remove_path(const char *name) slash = dirs + (slash - name); do { *slash = '\0'; + if (startup_info->original_cwd && + !strcmp(startup_info->original_cwd, dirs)) + break; } while (rmdir(dirs) == 0 && (slash = strrchr(dirs, '/'))); free(dirs); } @@ -495,6 +495,9 @@ int get_sparse_checkout_patterns(struct pattern_list *pl); /* Remove the contents of path, but leave path itself. */ #define REMOVE_DIR_KEEP_TOPLEVEL 04 +/* Remove the_original_cwd too */ +#define REMOVE_DIR_PURGE_ORIGINAL_CWD 0x08 + /* * Remove path and its contents, recursively. flags is a combination * of the above REMOVE_DIR_* constants. Return 0 on success. @@ -504,7 +507,11 @@ int get_sparse_checkout_patterns(struct pattern_list *pl); */ int remove_dir_recursively(struct strbuf *path, int flag); -/* tries to remove the path with empty directories along it, ignores ENOENT */ +/* + * Tries to remove the path, along with leading empty directories so long as + * those empty directories are not startup_info->original_cwd. Ignores + * ENOENT. + */ int remove_path(const char *path); int fspathcmp(const char *a, const char *b); @@ -1,6 +1,7 @@ #include "cache.h" #include "config.h" #include "strbuf.h" +#include "strvec.h" #include "run-command.h" #include "sigchain.h" @@ -55,7 +56,6 @@ static int launch_specified_editor(const char *editor, const char *path, if (strcmp(editor, ":")) { struct strbuf realpath = STRBUF_INIT; - const char *args[] = { editor, NULL, NULL }; struct child_process p = CHILD_PROCESS_INIT; int ret, sig; int print_waiting_for_editor = advice_enabled(ADVICE_WAITING_FOR_EDITOR) && isatty(2); @@ -77,10 +77,10 @@ static int launch_specified_editor(const char *editor, const char *path, } strbuf_realpath(&realpath, path, 1); - args[1] = realpath.buf; - p.argv = args; - p.env = env; + strvec_pushl(&p.args, editor, realpath.buf, NULL); + if (env) + strvec_pushv(&p.env_array, (const char **)env); p.use_shell = 1; p.trace2_child_class = "editor"; if (start_command(&p) < 0) { @@ -82,11 +82,13 @@ static int create_file(const char *path, unsigned int mode) return open(path, O_WRONLY | O_CREAT | O_EXCL, mode); } -void *read_blob_entry(const struct cache_entry *ce, unsigned long *size) +void *read_blob_entry(const struct cache_entry *ce, size_t *size) { enum object_type type; - void *blob_data = read_object_file(&ce->oid, &type, size); + unsigned long ul; + void *blob_data = read_object_file(&ce->oid, &type, &ul); + *size = ul; if (blob_data) { if (type == OBJ_BLOB) return blob_data; @@ -271,7 +273,7 @@ static int write_entry(struct cache_entry *ce, char *path, struct conv_attrs *ca int fd, ret, fstat_done = 0; char *new_blob; struct strbuf buf = STRBUF_INIT; - unsigned long size; + size_t size; ssize_t wrote; size_t newsize = 0; struct stat st; @@ -52,7 +52,7 @@ int finish_delayed_checkout(struct checkout *state, int *nr_checkouts, */ void unlink_entry(const struct cache_entry *ce); -void *read_blob_entry(const struct cache_entry *ce, unsigned long *size); +void *read_blob_entry(const struct cache_entry *ce, size_t *size); int fstat_checkout_output(int fd, const struct checkout *state, struct stat *st); void update_ce_after_write(const struct checkout *state, struct cache_entry *ce, struct stat *st); diff --git a/environment.c b/environment.c index 9da7f3c1a1..fd0501e77a 100644 --- a/environment.c +++ b/environment.c @@ -17,6 +17,7 @@ #include "commit.h" #include "strvec.h" #include "object-store.h" +#include "tmp-objdir.h" #include "chdir-notify.h" #include "shallow.h" @@ -42,6 +43,7 @@ const char *git_hooks_path; int zlib_compression_level = Z_BEST_SPEED; int pack_compression_level = Z_DEFAULT_COMPRESSION; int fsync_object_files; +int use_fsync = -1; size_t packed_git_window_size = DEFAULT_PACKED_GIT_WINDOW_SIZE; size_t packed_git_limit = DEFAULT_PACKED_GIT_LIMIT; size_t delta_base_cache_limit = 96 * 1024 * 1024; @@ -168,6 +170,10 @@ void setup_git_env(const char *git_dir) args.graft_file = getenv_safe(&to_free, GRAFT_ENVIRONMENT); args.index_file = getenv_safe(&to_free, INDEX_ENVIRONMENT); args.alternate_db = getenv_safe(&to_free, ALTERNATE_DB_ENVIRONMENT); + if (getenv(GIT_QUARANTINE_ENVIRONMENT)) { + args.disable_ref_updates = 1; + } + repo_set_gitdir(the_repository, git_dir, &args); strvec_clear(&to_free); @@ -331,10 +337,14 @@ static void update_relative_gitdir(const char *name, void *data) { char *path = reparent_relative_path(old_cwd, new_cwd, get_git_dir()); + struct tmp_objdir *tmp_objdir = tmp_objdir_unapply_primary_odb(); + trace_printf_key(&trace_setup_key, "setup: move $GIT_DIR to '%s'", path); set_git_dir_1(path); + if (tmp_objdir) + tmp_objdir_reapply_primary_odb(tmp_objdir, old_cwd, new_cwd); free(path); } diff --git a/fetch-pack.c b/fetch-pack.c index a9604f35a3..34987a2c30 100644 --- a/fetch-pack.c +++ b/fetch-pack.c @@ -25,6 +25,7 @@ #include "shallow.h" #include "commit-reach.h" #include "commit-graph.h" +#include "sigchain.h" static int transfer_unpack_limit = -1; static int fetch_unpack_limit = -1; @@ -956,6 +957,8 @@ static int get_pack(struct fetch_pack_args *args, strvec_push(index_pack_args, cmd.args.v[i]); } + sigchain_push(SIGPIPE, SIG_IGN); + cmd.in = demux.out; cmd.git_cmd = 1; if (start_command(&cmd)) @@ -986,6 +989,8 @@ static int get_pack(struct fetch_pack_args *args, if (use_sideband && finish_async(&demux)) die(_("error in sideband demultiplexer")); + sigchain_pop(SIGPIPE); + /* * Now that index-pack has succeeded, write the promisor file using the * obtained .keep filename if necessary @@ -1653,8 +1658,13 @@ static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args, receive_wanted_refs(&reader, sought, nr_sought); /* get the pack(s) */ + if (git_env_bool("GIT_TRACE_REDACT", 1)) + reader.options |= PACKET_READ_REDACT_URI_PATH; if (process_section_header(&reader, "packfile-uris", 1)) receive_packfile_uris(&reader, &packfile_uris); + /* We don't expect more URIs. Reset to avoid expensive URI check. */ + reader.options &= ~PACKET_READ_REDACT_URI_PATH; + process_section_header(&reader, "packfile", 0); /* diff --git a/fmt-merge-msg.c b/fmt-merge-msg.c index 5216191488..e5c0aff2bf 100644 --- a/fmt-merge-msg.c +++ b/fmt-merge-msg.c @@ -533,8 +533,9 @@ static void fmt_merge_msg_sigs(struct strbuf *out) else { buf = payload.buf; len = payload.len; - if (check_signature(payload.buf, payload.len, sig.buf, - sig.len, &sigc) && + sigc.payload_type = SIGNATURE_PAYLOAD_TAG; + sigc.payload = strbuf_detach(&payload, &sigc.payload_len); + if (check_signature(&sigc, sig.buf, sig.len) && !sigc.output) strbuf_addstr(&sig, "gpg verification failed.\n"); else @@ -649,12 +650,15 @@ int fmt_merge_msg(struct strbuf *in, struct strbuf *out, memset(&merge_parents, 0, sizeof(merge_parents)); - /* get current branch */ + /* learn the commit that we merge into and the current branch name */ current_branch = current_branch_to_free = resolve_refdup("HEAD", RESOLVE_REF_READING, &head_oid, NULL); if (!current_branch) die("No current branch"); - if (starts_with(current_branch, "refs/heads/")) + + if (opts->into_name) + current_branch = opts->into_name; + else if (starts_with(current_branch, "refs/heads/")) current_branch += 11; find_merge_parents(&merge_parents, in, &head_oid); diff --git a/fmt-merge-msg.h b/fmt-merge-msg.h index f2ab0e0085..99054042dc 100644 --- a/fmt-merge-msg.h +++ b/fmt-merge-msg.h @@ -9,6 +9,7 @@ struct fmt_merge_msg_opts { unsigned add_title:1, credit_people:1; int shortlog_len; + const char *into_name; }; extern int merge_log_config; diff --git a/generate-cmdlist.sh b/generate-cmdlist.sh index 9dbbb08e70..205541e0f7 100755 --- a/generate-cmdlist.sh +++ b/generate-cmdlist.sh @@ -6,37 +6,38 @@ die () { } command_list () { - eval "grep -ve '^#' $exclude_programs" <"$1" -} - -get_categories () { - tr ' ' '\012'| - grep -v '^$' | - sort | - uniq + while read cmd rest + do + case "$cmd" in + "#"* | '') + # Ignore comments and allow empty lines + continue + ;; + *) + case "$exclude_programs" in + *":$cmd:"*) + ;; + *) + echo "$cmd $rest" + ;; + esac + esac + done <"$1" } category_list () { - command_list "$1" | - cut -c 40- | - get_categories -} - -get_synopsis () { - sed -n ' - /^NAME/,/'"$1"'/H - ${ - x - s/.*'"$1"' - \(.*\)/N_("\1")/ - p - }' "Documentation/$1.txt" + echo "$1" | + cut -d' ' -f2- | + tr ' ' '\012' | + grep -v '^$' | + LC_ALL=C sort -u } define_categories () { echo echo "/* Command categories */" bit=0 - category_list "$1" | + echo "$1" | while read cat do echo "#define CAT_$cat (1UL << $bit)" @@ -50,7 +51,7 @@ define_category_names () { echo "/* Category names */" echo "static const char *category_names[] = {" bit=0 - category_list "$1" | + echo "$1" | while read cat do echo " \"$cat\", /* (1UL << $bit) */" @@ -63,27 +64,38 @@ define_category_names () { print_command_list () { echo "static struct cmdname_help command_list[] = {" - command_list "$1" | + echo "$1" | while read cmd rest do - printf " { \"$cmd\", $(get_synopsis $cmd), 0" - for cat in $(echo "$rest" | get_categories) + synopsis= + while read line do - printf " | CAT_$cat" - done + case "$line" in + "$cmd - "*) + synopsis=${line#$cmd - } + break + ;; + esac + done <"Documentation/$cmd.txt" + + printf '\t{ "%s", N_("%s"), 0' "$cmd" "$synopsis" + printf " | CAT_%s" $rest echo " }," done echo "};" } -exclude_programs= +exclude_programs=: while test "--exclude-program" = "$1" do shift - exclude_programs="$exclude_programs -e \"^$1 \"" + exclude_programs="$exclude_programs$1:" shift done +commands="$(command_list "$1")" +categories="$(category_list "$commands")" + echo "/* Automatically generated by generate-cmdlist.sh */ struct cmdname_help { const char *name; @@ -91,8 +103,8 @@ struct cmdname_help { uint32_t category; }; " -define_categories "$1" +define_categories "$categories" echo -define_category_names "$1" +define_category_names "$categories" echo -print_command_list "$1" +print_command_list "$commands" diff --git a/git-add--interactive.perl b/git-add--interactive.perl index bc3a1e8eff..95887fd8e5 100755 --- a/git-add--interactive.perl +++ b/git-add--interactive.perl @@ -1175,15 +1175,17 @@ sub prompt_single_character { ReadMode 'cbreak'; my $key = ReadKey 0; ReadMode 'restore'; - if ($use_termcap and $key eq "\e") { - while (!defined $term_escapes{$key}) { - my $next = ReadKey 0.5; - last if (!defined $next); - $key .= $next; + 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/^[/; } - $key =~ s/\e/^[/; + print "$key"; } - print "$key" if defined $key; print "\n"; return $key; } else { diff --git a/git-compat-util.h b/git-compat-util.h index d70ce14286..b7caf23666 100644 --- a/git-compat-util.h +++ b/git-compat-util.h @@ -1,6 +1,19 @@ #ifndef GIT_COMPAT_UTIL_H #define GIT_COMPAT_UTIL_H +#if __STDC_VERSION__ - 0 < 199901L +/* + * Git is in a testing period for mandatory C99 support in the compiler. If + * your compiler is reasonably recent, you can try to enable C99 support (or, + * for MSVC, C11 support). If you encounter a problem and can't enable C99 + * support with your compiler (such as with "-std=gnu99") and don't have access + * to one with this support, such as GCC or Clang, you can remove this #if + * directive, but please report the details of your system to + * git@vger.kernel.org. + */ +#error "Required C99 support is in a test phase. Please see git-compat-util.h for more details." +#endif + #ifdef USE_MSVC_CRTDBG /* * For these to work they must appear very early in each @@ -33,14 +46,23 @@ /* * See if our compiler is known to support flexible array members. */ -#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) && (!defined(__SUNPRO_C) || (__SUNPRO_C > 0x580)) -# define FLEX_ARRAY /* empty */ + +/* + * Check vendor specific quirks first, before checking the + * __STDC_VERSION__, as vendor compilers can lie and we need to be + * able to work them around. Note that by not defining FLEX_ARRAY + * here, we can fall back to use the "safer but a bit wasteful" one + * later. + */ +#if defined(__SUNPRO_C) && (__SUNPRO_C <= 0x580) #elif defined(__GNUC__) # if (__GNUC__ >= 3) # define FLEX_ARRAY /* empty */ # else # define FLEX_ARRAY 0 /* older GNU extension */ # endif +#elif defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) +# define FLEX_ARRAY /* empty */ #endif /* @@ -113,6 +135,14 @@ #define unsigned_mult_overflows(a, b) \ ((a) && (b) > maximum_unsigned_value_of_type(a) / (a)) +/* + * Returns true if the left shift of "a" by "shift" bits will + * overflow. The type of "a" must be unsigned. + */ +#define unsigned_left_shift_overflows(a, shift) \ + ((shift) < bitsizeof(a) && \ + (a) > maximum_unsigned_value_of_type(a) >> (shift)) + #ifdef __GNUC__ #define TYPEOF(x) (__typeof__(x)) #else @@ -127,7 +157,9 @@ /* Approximation of the length of the decimal representation of this type. */ #define decimal_length(x) ((int)(sizeof(x) * 2.56 + 0.5) + 1) -#if defined(__sun__) +#ifdef __MINGW64__ +#define _POSIX_C_SOURCE 1 +#elif defined(__sun__) /* * On Solaris, when _XOPEN_EXTENDED is set, its header file * forces the programs to be XPG4v2, defeating any _XOPEN_SOURCE @@ -729,7 +761,7 @@ char *gitmkdtemp(char *); #ifdef NO_UNSETENV #define unsetenv gitunsetenv -void gitunsetenv(const char *); +int gitunsetenv(const char *); #endif #ifdef NO_STRCASESTR @@ -862,6 +894,23 @@ static inline size_t st_sub(size_t a, size_t b) return a - b; } +static inline size_t st_left_shift(size_t a, unsigned shift) +{ + if (unsigned_left_shift_overflows(a, shift)) + die("size_t overflow: %"PRIuMAX" << %u", + (uintmax_t)a, shift); + return a << shift; +} + +static inline unsigned long cast_size_t_to_ulong(size_t a) +{ + if (a != (unsigned long)a) + die("object too large to read on this platform: %" + PRIuMAX" is cut off to %lu", + (uintmax_t)a, (unsigned long)a); + return (unsigned long)a; +} + #ifdef HAVE_ALLOCA_H # include <alloca.h> # define xalloca(size) (alloca(size)) diff --git a/git-cvsserver.perl b/git-cvsserver.perl index 64319bed43..4c8118010a 100755 --- a/git-cvsserver.perl +++ b/git-cvsserver.perl @@ -3607,6 +3607,22 @@ package GITCVS::updater; use strict; use warnings; use DBI; +our $_use_fsync; + +# n.b. consider using Git.pm +sub use_fsync { + if (!defined($_use_fsync)) { + my $x = $ENV{GIT_TEST_FSYNC}; + if (defined $x) { + local $ENV{GIT_CONFIG}; + delete $ENV{GIT_CONFIG}; + my $v = ::safe_pipe_capture('git', '-c', "test.fsync=$x", + qw(config --type=bool test.fsync)); + $_use_fsync = defined($v) ? ($v eq "true\n") : 1; + } + } + $_use_fsync; +} =head1 METHODS @@ -3676,6 +3692,9 @@ sub new $self->{dbuser}, $self->{dbpass}); die "Error connecting to database\n" unless defined $self->{dbh}; + if ($self->{dbdriver} eq 'SQLite' && !use_fsync()) { + $self->{dbh}->do('PRAGMA synchronous = OFF'); + } $self->{tables} = {}; foreach my $table ( keys %{$self->{dbh}->table_info(undef,undef,undef,'TABLE')->fetchall_hashref('TABLE_NAME')} ) diff --git a/git-filter-branch.sh b/git-filter-branch.sh index cb89372813..3a51d4507c 100755 --- a/git-filter-branch.sh +++ b/git-filter-branch.sh @@ -579,7 +579,7 @@ if [ "$filter_tag_name" ]; then git hash-object -t tag -w --stdin) || die "Could not create new tag object for $ref" if git cat-file tag "$ref" | \ - sane_grep '^-----BEGIN PGP SIGNATURE-----' >/dev/null 2>&1 + grep '^-----BEGIN PGP SIGNATURE-----' >/dev/null 2>&1 then warn "gpg signature stripped from tag object $sha1t" fi diff --git a/git-instaweb.sh b/git-instaweb.sh index 7c55229773..4349566c89 100755 --- a/git-instaweb.sh +++ b/git-instaweb.sh @@ -49,7 +49,7 @@ resolve_full_httpd () { *apache2*|*lighttpd*|*httpd*) # yes, *httpd* covers *lighttpd* above, but it is there for clarity # ensure that the apache2/lighttpd command ends with "-f" - if ! echo "$httpd" | sane_grep -- '-f *$' >/dev/null 2>&1 + if ! echo "$httpd" | grep -- '-f *$' >/dev/null 2>&1 then httpd="$httpd -f" fi @@ -380,10 +380,7 @@ TypesConfig "$fqgitdir/mime.types" DirectoryIndex gitweb.cgi EOF - # check to see if Dennis Stosberg's mod_perl compatibility patch - # (<20060621130708.Gcbc6e5c@leonov.stosberg.net>) has been applied - if test -f "$module_path/mod_perl.so" && - sane_grep 'MOD_PERL' "$root/gitweb.cgi" >/dev/null + if test -f "$module_path/mod_perl.so" then # favor mod_perl if available cat >> "$conf" <<EOF @@ -402,7 +399,7 @@ EOF # plain-old CGI resolve_full_httpd list_mods=$(echo "$full_httpd" | sed 's/-f$/-l/') - $list_mods | sane_grep 'mod_cgi\.c' >/dev/null 2>&1 || \ + $list_mods | grep 'mod_cgi\.c' >/dev/null 2>&1 || \ if test -f "$module_path/mod_cgi.so" then echo "LoadModule cgi_module $module_path/mod_cgi.so" >> "$conf" @@ -56,6 +56,21 @@ defaultBlockSize = 1<<20 p4_access_checked = False +re_ko_keywords = re.compile(br'\$(Id|Header)(:[^$\n]+)?\$') +re_k_keywords = re.compile(br'\$(Id|Header|Author|Date|DateTime|Change|File|Revision)(:[^$\n]+)?\$') + +def format_size_human_readable(num): + """ Returns a number of units (typically bytes) formatted as a human-readable + string. + """ + if num < 1024: + return '{:d} B'.format(num) + for unit in ["Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"]: + num /= 1024.0 + if num < 1024.0: + return "{:3.1f} {}B".format(num, unit) + return "{:.1f} YiB".format(num) + def p4_build_cmd(cmd): """Build a suitable p4 command line. @@ -337,17 +352,19 @@ def p4_read_pipe(c, ignore_error=False, raw=False): real_cmd = p4_build_cmd(c) return read_pipe(real_cmd, ignore_error, raw=raw) -def read_pipe_lines(c): +def read_pipe_lines(c, raw=False): if verbose: sys.stderr.write('Reading pipe: %s\n' % str(c)) expand = not isinstance(c, list) p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand) pipe = p.stdout - val = [decode_text_stream(line) for line in pipe.readlines()] + lines = pipe.readlines() + if not raw: + lines = [decode_text_stream(line) for line in lines] if pipe.close() or p.wait(): die('Command failed: %s' % str(c)) - return val + return lines def p4_read_pipe_lines(c): """Specifically invoke p4 on the command supplied. """ @@ -577,20 +594,12 @@ def p4_type(f): # def p4_keywords_regexp_for_type(base, type_mods): if base in ("text", "unicode", "binary"): - kwords = None if "ko" in type_mods: - kwords = 'Id|Header' + return re_ko_keywords elif "k" in type_mods: - kwords = 'Id|Header|Author|Date|DateTime|Change|File|Revision' + return re_k_keywords else: return None - pattern = r""" - \$ # Starts with a dollar, followed by... - (%s) # one of the keywords, followed by... - (:[^$\n]+)? # possibly an old expansion, followed by... - \$ # another dollar - """ % kwords - return pattern else: return None @@ -1532,80 +1541,6 @@ class P4UserMap: except IOError: self.getUserMapFromPerforceServer() -class P4Debug(Command): - def __init__(self): - Command.__init__(self) - self.options = [] - self.description = "A tool to debug the output of p4 -G." - self.needsGit = False - - def run(self, args): - j = 0 - for output in p4CmdList(args): - print('Element: %d' % j) - j += 1 - print(output) - return True - -class P4RollBack(Command): - def __init__(self): - Command.__init__(self) - self.options = [ - optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true") - ] - self.description = "A tool to debug the multi-branch import. Don't use :)" - self.rollbackLocalBranches = False - - def run(self, args): - if len(args) != 1: - return False - maxChange = int(args[0]) - - if "p4ExitCode" in p4Cmd("changes -m 1"): - die("Problems executing p4"); - - if self.rollbackLocalBranches: - refPrefix = "refs/heads/" - lines = read_pipe_lines("git rev-parse --symbolic --branches") - else: - refPrefix = "refs/remotes/" - lines = read_pipe_lines("git rev-parse --symbolic --remotes") - - for line in lines: - if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"): - line = line.strip() - ref = refPrefix + line - log = extractLogMessageFromGitCommit(ref) - settings = extractSettingsGitLog(log) - - depotPaths = settings['depot-paths'] - change = settings['change'] - - changed = False - - if len(p4Cmd("changes -m 1 " + ' '.join (['%s...@%s' % (p, maxChange) - for p in depotPaths]))) == 0: - print("Branch %s did not exist at change %s, deleting." % (ref, maxChange)) - system("git update-ref -d %s `git rev-parse %s`" % (ref, ref)) - continue - - while change and int(change) > maxChange: - changed = True - if self.verbose: - print("%s is at %s ; rewinding towards %s" % (ref, change, maxChange)) - system("git update-ref %s \"%s^\"" % (ref, ref)) - log = extractLogMessageFromGitCommit(ref) - settings = extractSettingsGitLog(log) - - - depotPaths = settings['depot-paths'] - change = settings['change'] - - if changed: - print("%s rewound to %s" % (ref, change)) - - return True - class P4Submit(Command, P4UserMap): conflict_behavior_choices = ("ask", "skip", "quit") @@ -1753,18 +1688,13 @@ class P4Submit(Command, P4UserMap): return result - def patchRCSKeywords(self, file, pattern): - # Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern + def patchRCSKeywords(self, file, regexp): + # Attempt to zap the RCS keywords in a p4 controlled file matching the given regex (handle, outFileName) = tempfile.mkstemp(dir='.') try: - outFile = os.fdopen(handle, "w+") - inFile = open(file, "r") - regexp = re.compile(pattern, re.VERBOSE) - for line in inFile.readlines(): - line = regexp.sub(r'$\1$', line) - outFile.write(line) - inFile.close() - outFile.close() + with os.fdopen(handle, "wb") as outFile, open(file, "rb") as inFile: + for line in inFile.readlines(): + outFile.write(regexp.sub(br'$\1$', line)) # Forcibly overwrite the original file os.unlink(file) shutil.move(outFileName, file) @@ -2091,25 +2021,24 @@ class P4Submit(Command, P4UserMap): # the patch to see if that's possible. if gitConfigBool("git-p4.attemptRCSCleanup"): file = None - pattern = None kwfiles = {} for file in editedFiles | filesToDelete: # did this file's delta contain RCS keywords? - pattern = p4_keywords_regexp_for_file(file) - - if pattern: + regexp = p4_keywords_regexp_for_file(file) + if regexp: # this file is a possibility...look for RCS keywords. - regexp = re.compile(pattern, re.VERBOSE) - for line in read_pipe_lines(["git", "diff", "%s^..%s" % (id, id), file]): + for line in read_pipe_lines( + ["git", "diff", "%s^..%s" % (id, id), file], + raw=True): if regexp.search(line): if verbose: - print("got keyword match on %s in %s in %s" % (pattern, line, file)) - kwfiles[file] = pattern + print("got keyword match on %s in %s in %s" % (regex.pattern, line, file)) + kwfiles[file] = regexp break - for file in kwfiles: + for file, regexp in kwfiles.items(): if verbose: - print("zapping %s with %s" % (line,pattern)) + print("zapping %s with %s" % (line, regexp.pattern)) # File is being deleted, so not open in p4. Must # disable the read-only bit on windows. if self.isWindows and file not in editedFiles: @@ -2966,7 +2895,8 @@ class P4Sync(Command, P4UserMap): size = int(self.stream_file['fileSize']) else: size = 0 # deleted files don't get a fileSize apparently - sys.stdout.write('\r%s --> %s (%i MB)\n' % (file_path, relPath, size/1024/1024)) + sys.stdout.write('\r%s --> %s (%s)\n' % ( + file_path, relPath, format_size_human_readable(size))) sys.stdout.flush() (type_base, type_mods) = split_p4_type(file["type"]) @@ -3029,12 +2959,9 @@ class P4Sync(Command, P4UserMap): # Note that we do not try to de-mangle keywords on utf16 files, # even though in theory somebody may want that. - pattern = p4_keywords_regexp_for_type(type_base, type_mods) - if pattern: - regexp = re.compile(pattern, re.VERBOSE) - text = ''.join(decode_text_stream(c) for c in contents) - text = regexp.sub(r'$\1$', text) - contents = [ encode_text_stream(text) ] + regexp = p4_keywords_regexp_for_type(type_base, type_mods) + if regexp: + contents = [regexp.sub(br'$\1$', c) for c in contents] if self.largeFileSystem: (git_mode, contents) = self.largeFileSystem.processContent(git_mode, relPath, contents) @@ -3064,9 +2991,8 @@ class P4Sync(Command, P4UserMap): if not err and 'fileSize' in self.stream_file: required_bytes = int((4 * int(self.stream_file["fileSize"])) - calcDiskFree()) if required_bytes > 0: - err = 'Not enough space left on %s! Free at least %i MB.' % ( - os.getcwd(), required_bytes/1024/1024 - ) + err = 'Not enough space left on %s! Free at least %s.' % ( + os.getcwd(), format_size_human_readable(required_bytes)) if err: f = None @@ -3110,7 +3036,9 @@ class P4Sync(Command, P4UserMap): size = int(self.stream_file["fileSize"]) if size > 0: progress = 100*self.stream_file['streamContentSize']/size - sys.stdout.write('\r%s %d%% (%i MB)' % (self.stream_file['depotFile'], progress, int(size/1024/1024))) + sys.stdout.write('\r%s %d%% (%s)' % ( + self.stream_file['depotFile'], progress, + format_size_human_readable(size))) sys.stdout.flush() self.stream_have_file_info = True @@ -3623,7 +3551,8 @@ class P4Sync(Command, P4UserMap): self.updateOptionDict(description) if not self.silent: - sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes))) + sys.stdout.write("\rImporting revision %s (%d%%)" % ( + change, (cnt * 100) // len(changes))) sys.stdout.flush() cnt = cnt + 1 @@ -4363,13 +4292,11 @@ def printUsage(commands): print("") commands = { - "debug" : P4Debug, "submit" : P4Submit, "commit" : P4Submit, "sync" : P4Sync, "rebase" : P4Rebase, "clone" : P4Clone, - "rollback" : P4RollBack, "branches" : P4Branches, "unshelve" : P4Unshelve, } diff --git a/git-send-email.perl b/git-send-email.perl index 5262d88ee3..04087221aa 100755 --- a/git-send-email.perl +++ b/git-send-email.perl @@ -40,7 +40,8 @@ package main; sub usage { print <<EOT; -git send-email [options] <file | directory | rev-list options > +git send-email' [<options>] <file|directory> +git send-email' [<options>] <format-patch options> git send-email --dump-aliases Composing: @@ -113,9 +114,38 @@ EOT exit(1); } +sub uniq { + my %seen; + grep !$seen{$_}++, @_; +} + sub completion_helper { - print Git::command('format-patch', '--git-completion-helper'); - exit(0); + my ($original_opts) = @_; + my %not_for_completion = ( + "git-completion-helper" => undef, + "h" => undef, + ); + my @send_email_opts = (); + + foreach my $key (keys %$original_opts) { + unless (exists $not_for_completion{$key}) { + $key =~ s/!$//; + + if ($key =~ /[:=][si]$/) { + $key =~ s/[:=][si]$//; + push (@send_email_opts, "--$_=") foreach (split (/\|/, $key)); + } else { + push (@send_email_opts, "--$_") foreach (split (/\|/, $key)); + } + } + } + + my @format_patch_opts = split(/ /, Git::command('format-patch', '--git-completion-helper')); + my @opts = (@send_email_opts, @format_patch_opts); + @opts = uniq (grep !/^$/, @opts); + # There's an implicit '\n' here already, no need to add an explicit one. + print "@opts"; + exit(0); } # most mail servers generate the Date: header, but not all... @@ -425,10 +455,11 @@ my %known_config_keys; my $key = "sendemail.identity"; $identity = Git::config(@repo, $key) if exists $known_config_keys{$key}; } -my $rc = GetOptions( +my %identity_options = ( "identity=s" => \$identity, "no-identity" => \$no_identity, ); +my $rc = GetOptions(%identity_options); usage() unless $rc; undef $identity if $no_identity; @@ -444,14 +475,17 @@ undef $identity if $no_identity; my $help; my $git_completion_helper; -$rc = GetOptions("h" => \$help, - "dump-aliases" => \$dump_aliases); +my %dump_aliases_options = ( + "h" => \$help, + "dump-aliases" => \$dump_aliases, +); +$rc = GetOptions(%dump_aliases_options); usage() unless $rc; die __("--dump-aliases incompatible with other options\n") if !$help and $dump_aliases and @ARGV; -$rc = GetOptions( +my %options = ( "sender|from=s" => \$sender, - "in-reply-to=s" => \$initial_in_reply_to, + "in-reply-to=s" => \$initial_in_reply_to, "reply-to=s" => \$reply_to, "subject=s" => \$initial_subject, "to=s" => \@getopt_to, @@ -508,7 +542,8 @@ $rc = GetOptions( "batch-size=i" => \$batch_size, "relogin-delay=i" => \$relogin_delay, "git-completion-helper" => \$git_completion_helper, - ); +); +$rc = GetOptions(%options); # Munge any "either config or getopt, not both" variables my @initial_to = @getopt_to ? @getopt_to : ($no_to ? () : @config_to); @@ -516,7 +551,8 @@ my @initial_cc = @getopt_cc ? @getopt_cc : ($no_cc ? () : @config_cc); my @initial_bcc = @getopt_bcc ? @getopt_bcc : ($no_bcc ? () : @config_bcc); usage() if $help; -completion_helper() if $git_completion_helper; +my %all_options = (%options, %dump_aliases_options, %identity_options); +completion_helper(\%all_options) if $git_completion_helper; unless ($rc) { usage(); } diff --git a/git-sh-setup.sh b/git-sh-setup.sh index 960982f9d5..b93f39288c 100644 --- a/git-sh-setup.sh +++ b/git-sh-setup.sh @@ -173,14 +173,6 @@ git_pager() { eval "$GIT_PAGER" '"$@"' } -sane_grep () { - GREP_OPTIONS= LC_ALL=C grep @@SANE_TEXT_GREP@@ "$@" -} - -sane_egrep () { - GREP_OPTIONS= LC_ALL=C egrep @@SANE_TEXT_GREP@@ "$@" -} - is_bare_repository () { git rev-parse --is-bare-repository } @@ -421,27 +421,30 @@ static int run_builtin(struct cmd_struct *p, int argc, const char **argv) int status, help; struct stat st; const char *prefix; + int run_setup = (p->option & (RUN_SETUP | RUN_SETUP_GENTLY)); - prefix = NULL; help = argc == 2 && !strcmp(argv[1], "-h"); - if (!help) { - if (p->option & RUN_SETUP) - prefix = setup_git_directory(); - else if (p->option & RUN_SETUP_GENTLY) { - int nongit_ok; - prefix = setup_git_directory_gently(&nongit_ok); - } - precompose_argv_prefix(argc, argv, NULL); - if (use_pager == -1 && p->option & (RUN_SETUP | RUN_SETUP_GENTLY) && - !(p->option & DELAY_PAGER_CONFIG)) - use_pager = check_pager_config(p->cmd); - if (use_pager == -1 && p->option & USE_PAGER) - use_pager = 1; - - if ((p->option & (RUN_SETUP | RUN_SETUP_GENTLY)) && - startup_info->have_repository) /* get_git_dir() may set up repo, avoid that */ - trace_repo_setup(prefix); + if (help && (run_setup & RUN_SETUP)) + /* demote to GENTLY to allow 'git cmd -h' outside repo */ + run_setup = RUN_SETUP_GENTLY; + + if (run_setup & RUN_SETUP) { + prefix = setup_git_directory(); + } else if (run_setup & RUN_SETUP_GENTLY) { + int nongit_ok; + prefix = setup_git_directory_gently(&nongit_ok); + } else { + prefix = NULL; } + precompose_argv_prefix(argc, argv, NULL); + if (use_pager == -1 && run_setup && + !(p->option & DELAY_PAGER_CONFIG)) + use_pager = check_pager_config(p->cmd); + if (use_pager == -1 && p->option & USE_PAGER) + use_pager = 1; + if (run_setup && startup_info->have_repository) + /* get_git_dir() may set up repo, avoid that */ + trace_repo_setup(prefix); commit_pager_choice(); if (!help && get_super_prefix()) { diff --git a/gpg-interface.c b/gpg-interface.c index 3e7255a2a9..b52eb0e2e0 100644 --- a/gpg-interface.c +++ b/gpg-interface.c @@ -19,8 +19,8 @@ struct gpg_format { const char **verify_args; const char **sigs; int (*verify_signed_buffer)(struct signature_check *sigc, - struct gpg_format *fmt, const char *payload, - size_t payload_size, const char *signature, + struct gpg_format *fmt, + const char *signature, size_t signature_size); int (*sign_buffer)(struct strbuf *buffer, struct strbuf *signature, const char *signing_key); @@ -53,12 +53,12 @@ static const char *ssh_sigs[] = { }; static int verify_gpg_signed_buffer(struct signature_check *sigc, - struct gpg_format *fmt, const char *payload, - size_t payload_size, const char *signature, + struct gpg_format *fmt, + const char *signature, size_t signature_size); static int verify_ssh_signed_buffer(struct signature_check *sigc, - struct gpg_format *fmt, const char *payload, - size_t payload_size, const char *signature, + struct gpg_format *fmt, + const char *signature, size_t signature_size); static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature, const char *signing_key); @@ -314,8 +314,8 @@ error: } static int verify_gpg_signed_buffer(struct signature_check *sigc, - struct gpg_format *fmt, const char *payload, - size_t payload_size, const char *signature, + struct gpg_format *fmt, + const char *signature, size_t signature_size) { struct child_process gpg = CHILD_PROCESS_INIT; @@ -343,14 +343,13 @@ static int verify_gpg_signed_buffer(struct signature_check *sigc, NULL); sigchain_push(SIGPIPE, SIG_IGN); - ret = pipe_command(&gpg, payload, payload_size, &gpg_stdout, 0, + ret = pipe_command(&gpg, sigc->payload, sigc->payload_len, &gpg_stdout, 0, &gpg_stderr, 0); sigchain_pop(SIGPIPE); delete_tempfile(&temp); ret |= !strstr(gpg_stdout.buf, "\n[GNUPG:] GOODSIG "); - sigc->payload = xmemdupz(payload, payload_size); sigc->output = strbuf_detach(&gpg_stderr, NULL); sigc->gpg_status = strbuf_detach(&gpg_stdout, NULL); @@ -426,8 +425,8 @@ cleanup: } static int verify_ssh_signed_buffer(struct signature_check *sigc, - struct gpg_format *fmt, const char *payload, - size_t payload_size, const char *signature, + struct gpg_format *fmt, + const char *signature, size_t signature_size) { struct child_process ssh_keygen = CHILD_PROCESS_INIT; @@ -440,6 +439,13 @@ static int verify_ssh_signed_buffer(struct signature_check *sigc, struct strbuf ssh_principals_err = STRBUF_INIT; struct strbuf ssh_keygen_out = STRBUF_INIT; struct strbuf ssh_keygen_err = STRBUF_INIT; + struct strbuf verify_time = STRBUF_INIT; + const struct date_mode verify_date_mode = { + .type = DATE_STRFTIME, + .strftime_fmt = "%Y%m%d%H%M%S", + /* SSH signing key validity has no timezone information - Use the local timezone */ + .local = 1, + }; if (!ssh_allowed_signers) { error(_("gpg.ssh.allowedSignersFile needs to be configured and exist for ssh signature verification")); @@ -457,11 +463,16 @@ static int verify_ssh_signed_buffer(struct signature_check *sigc, return -1; } + if (sigc->payload_timestamp) + strbuf_addf(&verify_time, "-Overify-time=%s", + show_date(sigc->payload_timestamp, 0, &verify_date_mode)); + /* Find the principal from the signers */ strvec_pushl(&ssh_keygen.args, fmt->program, "-Y", "find-principals", "-f", ssh_allowed_signers, "-s", buffer_file->filename.buf, + verify_time.buf, NULL); ret = pipe_command(&ssh_keygen, NULL, 0, &ssh_principals_out, 0, &ssh_principals_err, 0); @@ -479,8 +490,9 @@ static int verify_ssh_signed_buffer(struct signature_check *sigc, "-Y", "check-novalidate", "-n", "git", "-s", buffer_file->filename.buf, + verify_time.buf, NULL); - pipe_command(&ssh_keygen, payload, payload_size, + pipe_command(&ssh_keygen, sigc->payload, sigc->payload_len, &ssh_keygen_out, 0, &ssh_keygen_err, 0); /* @@ -513,6 +525,7 @@ static int verify_ssh_signed_buffer(struct signature_check *sigc, "-f", ssh_allowed_signers, "-I", principal, "-s", buffer_file->filename.buf, + verify_time.buf, NULL); if (ssh_revocation_file) { @@ -526,7 +539,7 @@ static int verify_ssh_signed_buffer(struct signature_check *sigc, } sigchain_push(SIGPIPE, SIG_IGN); - ret = pipe_command(&ssh_keygen, payload, payload_size, + ret = pipe_command(&ssh_keygen, sigc->payload, sigc->payload_len, &ssh_keygen_out, 0, &ssh_keygen_err, 0); sigchain_pop(SIGPIPE); @@ -540,7 +553,6 @@ static int verify_ssh_signed_buffer(struct signature_check *sigc, } } - sigc->payload = xmemdupz(payload, payload_size); strbuf_stripspace(&ssh_keygen_out, 0); strbuf_stripspace(&ssh_keygen_err, 0); /* Add stderr outputs to show the user actual ssh-keygen errors */ @@ -558,12 +570,48 @@ out: strbuf_release(&ssh_principals_err); strbuf_release(&ssh_keygen_out); strbuf_release(&ssh_keygen_err); + strbuf_release(&verify_time); return ret; } -int check_signature(const char *payload, size_t plen, const char *signature, - size_t slen, struct signature_check *sigc) +static int parse_payload_metadata(struct signature_check *sigc) +{ + const char *ident_line = NULL; + size_t ident_len; + struct ident_split ident; + const char *signer_header; + + switch (sigc->payload_type) { + case SIGNATURE_PAYLOAD_COMMIT: + signer_header = "committer"; + break; + case SIGNATURE_PAYLOAD_TAG: + signer_header = "tagger"; + break; + case SIGNATURE_PAYLOAD_UNDEFINED: + case SIGNATURE_PAYLOAD_PUSH_CERT: + /* Ignore payloads we don't want to parse */ + return 0; + default: + BUG("invalid value for sigc->payload_type"); + } + + ident_line = find_commit_header(sigc->payload, signer_header, &ident_len); + if (!ident_line || !ident_len) + return 1; + + if (split_ident_line(&ident, ident_line, ident_len)) + return 1; + + if (!sigc->payload_timestamp && ident.date_begin && ident.date_end) + sigc->payload_timestamp = parse_timestamp(ident.date_begin, NULL, 10); + + return 0; +} + +int check_signature(struct signature_check *sigc, + const char *signature, size_t slen) { struct gpg_format *fmt; int status; @@ -575,8 +623,10 @@ int check_signature(const char *payload, size_t plen, const char *signature, if (!fmt) die(_("bad/incompatible signature '%s'"), signature); - status = fmt->verify_signed_buffer(sigc, fmt, payload, plen, signature, - slen); + if (parse_payload_metadata(sigc)) + return 1; + + status = fmt->verify_signed_buffer(sigc, fmt, signature, slen); if (status && !sigc->output) return !!status; @@ -593,7 +643,7 @@ void print_signature_buffer(const struct signature_check *sigc, unsigned flags) sigc->output; if (flags & GPG_VERIFY_VERBOSE && sigc->payload) - fputs(sigc->payload, stdout); + fwrite(sigc->payload, 1, sigc->payload_len, stdout); if (output) fputs(output, stderr); @@ -707,6 +757,21 @@ int git_gpg_config(const char *var, const char *value, void *cb) return 0; } +/* + * Returns 1 if `string` contains a literal ssh key, 0 otherwise + * `key` will be set to the start of the actual key if a prefix is present. + */ +static int is_literal_ssh_key(const char *string, const char **key) +{ + if (skip_prefix(string, "key::", key)) + return 1; + if (starts_with(string, "ssh-")) { + *key = string; + return 1; + } + return 0; +} + static char *get_ssh_key_fingerprint(const char *signing_key) { struct child_process ssh_keygen = CHILD_PROCESS_INIT; @@ -714,15 +779,16 @@ static char *get_ssh_key_fingerprint(const char *signing_key) struct strbuf fingerprint_stdout = STRBUF_INIT; struct strbuf **fingerprint; char *fingerprint_ret; + const char *literal_key = NULL; /* * With SSH Signing this can contain a filename or a public key * For textual representation we usually want a fingerprint */ - if (starts_with(signing_key, "ssh-")) { + if (is_literal_ssh_key(signing_key, &literal_key)) { strvec_pushl(&ssh_keygen.args, "ssh-keygen", "-lf", "-", NULL); - ret = pipe_command(&ssh_keygen, signing_key, - strlen(signing_key), &fingerprint_stdout, 0, + ret = pipe_command(&ssh_keygen, literal_key, + strlen(literal_key), &fingerprint_stdout, 0, NULL, 0); } else { strvec_pushl(&ssh_keygen.args, "ssh-keygen", "-lf", @@ -757,6 +823,7 @@ static const char *get_default_ssh_signing_key(void) const char **argv; int n; char *default_key = NULL; + const char *literal_key = NULL; if (!ssh_default_key_command) die(_("either user.signingkey or gpg.ssh.defaultKeyCommand needs to be configured")); @@ -774,7 +841,11 @@ static const char *get_default_ssh_signing_key(void) if (!ret) { keys = strbuf_split_max(&key_stdout, '\n', 2); - if (keys[0] && starts_with(keys[0]->buf, "ssh-")) { + if (keys[0] && is_literal_ssh_key(keys[0]->buf, &literal_key)) { + /* + * We only use `is_literal_ssh_key` here to check validity + * The prefix will be stripped when the key is used. + */ default_key = strbuf_detach(keys[0], NULL); } else { warning(_("gpg.ssh.defaultKeyCommand succeeded but returned no keys: %s %s"), @@ -889,19 +960,20 @@ static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature, struct tempfile *key_file = NULL, *buffer_file = NULL; char *ssh_signing_key_file = NULL; struct strbuf ssh_signature_filename = STRBUF_INIT; + const char *literal_key = NULL; if (!signing_key || signing_key[0] == '\0') return error( _("user.signingkey needs to be set for ssh signing")); - if (starts_with(signing_key, "ssh-")) { + if (is_literal_ssh_key(signing_key, &literal_key)) { /* A literal ssh key */ key_file = mks_tempfile_t(".git_signing_key_tmpXXXXXX"); if (!key_file) return error_errno( _("could not create temporary file")); - keylen = strlen(signing_key); - if (write_in_full(key_file->fd, signing_key, keylen) < 0 || + keylen = strlen(literal_key); + if (write_in_full(key_file->fd, literal_key, keylen) < 0 || close_tempfile_gently(key_file) < 0) { error_errno(_("failed writing ssh signing key to '%s'"), key_file->filename.buf); diff --git a/gpg-interface.h b/gpg-interface.h index beefacbb1e..b30cbdcd3d 100644 --- a/gpg-interface.h +++ b/gpg-interface.h @@ -15,8 +15,18 @@ enum signature_trust_level { TRUST_ULTIMATE, }; +enum payload_type { + SIGNATURE_PAYLOAD_UNDEFINED, + SIGNATURE_PAYLOAD_COMMIT, + SIGNATURE_PAYLOAD_TAG, + SIGNATURE_PAYLOAD_PUSH_CERT, +}; + struct signature_check { char *payload; + size_t payload_len; + enum payload_type payload_type; + timestamp_t payload_timestamp; char *output; char *gpg_status; @@ -70,9 +80,8 @@ const char *get_signing_key(void); * Either a GPG KeyID or a SSH Key Fingerprint */ const char *get_signing_key_id(void); -int check_signature(const char *payload, size_t plen, - const char *signature, size_t slen, - struct signature_check *sigc); +int check_signature(struct signature_check *sigc, + const char *signature, size_t slen); void print_signature_buffer(const struct signature_check *sigc, unsigned flags); @@ -362,6 +362,7 @@ static void compile_pcre2_pattern(struct grep_pat *p, const struct grep_opt *opt int jitret; int patinforet; size_t jitsizearg; + int literal = !opt->ignore_case && (p->fixed || p->is_fixed); /* * Call pcre2_general_context_create() before calling any @@ -382,8 +383,7 @@ static void compile_pcre2_pattern(struct grep_pat *p, const struct grep_opt *opt } options |= PCRE2_CASELESS; } - if (!opt->ignore_locale && is_utf8_locale() && has_non_ascii(p->pattern) && - !(!opt->ignore_case && (p->fixed || p->is_fixed))) + if (!opt->ignore_locale && is_utf8_locale() && !literal) options |= (PCRE2_UTF | PCRE2_MATCH_INVALID_UTF); #ifdef GIT_PCRE2_VERSION_10_36_OR_HIGHER @@ -699,6 +699,14 @@ static struct grep_expr *compile_pattern_expr(struct grep_pat **list) return compile_pattern_or(list); } +static struct grep_expr *grep_not_expr(struct grep_expr *expr) +{ + struct grep_expr *z = xcalloc(1, sizeof(*z)); + z->node = GREP_NODE_NOT; + z->u.unary = expr; + return z; +} + static struct grep_expr *grep_true_expr(void) { struct grep_expr *z = xcalloc(1, sizeof(*z)); @@ -797,7 +805,7 @@ void compile_grep_patterns(struct grep_opt *opt) } } - if (opt->all_match || header_expr) + if (opt->all_match || opt->no_body_match || header_expr) opt->extended = 1; else if (!opt->extended) return; @@ -808,6 +816,9 @@ void compile_grep_patterns(struct grep_opt *opt) if (p) die("incomplete pattern expression: %s", p->pattern); + if (opt->no_body_match && opt->pattern_expression) + opt->pattern_expression = grep_not_expr(opt->pattern_expression); + if (!header_expr) return; @@ -1057,6 +1068,8 @@ static int match_expr_eval(struct grep_opt *opt, struct grep_expr *x, if (h && (*col < 0 || tmp.rm_so < *col)) *col = tmp.rm_so; } + if (x->u.atom->token == GREP_PATTERN_BODY) + opt->body_hit |= h; break; case GREP_NODE_NOT: /* @@ -1825,16 +1838,19 @@ int grep_source(struct grep_opt *opt, struct grep_source *gs) * we do not have to do the two-pass grep when we do not check * buffer-wide "all-match". */ - if (!opt->all_match) + if (!opt->all_match && !opt->no_body_match) return grep_source_1(opt, gs, 0); /* Otherwise the toplevel "or" terms hit a bit differently. * We first clear hit markers from them. */ clr_hit_marker(opt->pattern_expression); + opt->body_hit = 0; grep_source_1(opt, gs, 1); - if (!chk_hit_marker(opt->pattern_expression)) + if (opt->all_match && !chk_hit_marker(opt->pattern_expression)) + return 0; + if (opt->no_body_match && opt->body_hit) return 0; return grep_source_1(opt, gs, 0); @@ -148,6 +148,8 @@ struct grep_opt { int word_regexp; int fixed; int all_match; + int no_body_match; + int body_hit; #define GREP_BINARY_DEFAULT 0 #define GREP_BINARY_NOMATCH 1 #define GREP_BINARY_TEXT 2 @@ -95,12 +95,18 @@ static inline void git_SHA256_Clone(git_SHA256_CTX *dst, const git_SHA256_CTX *s /* Number of algorithms supported (including unknown). */ #define GIT_HASH_NALGOS (GIT_HASH_SHA256 + 1) +/* "sha1", big-endian */ +#define GIT_SHA1_FORMAT_ID 0x73686131 + /* The length in bytes and in hex digits of an object name (SHA-1 value). */ #define GIT_SHA1_RAWSZ 20 #define GIT_SHA1_HEXSZ (2 * GIT_SHA1_RAWSZ) /* The block size of SHA-1. */ #define GIT_SHA1_BLKSZ 64 +/* "s256", big-endian */ +#define GIT_SHA256_FORMAT_ID 0x73323536 + /* The length in bytes and in hex digits of an object name (SHA-256 value). */ #define GIT_SHA256_RAWSZ 32 #define GIT_SHA256_HEXSZ (2 * GIT_SHA256_RAWSZ) @@ -643,7 +643,7 @@ const char *help_unknown_cmd(const char *cmd) else if (autocorrect == AUTOCORRECT_PROMPT) { char *answer; struct strbuf msg = STRBUF_INIT; - strbuf_addf(&msg, _("Run '%s' instead? (y/N)"), assumed); + strbuf_addf(&msg, _("Run '%s' instead [y/N]? "), assumed); answer = git_prompt(msg.buf, PROMPT_ECHO); strbuf_release(&msg); if (!(starts_with(answer, "y") || diff --git a/http-backend.c b/http-backend.c index 3d6e2ff17f..4dd4d939f8 100644 --- a/http-backend.c +++ b/http-backend.c @@ -480,7 +480,7 @@ static void run_service(const char **argv, int buffer_input) strvec_pushf(&cld.env_array, "GIT_COMMITTER_EMAIL=%s@http.%s", user, host); - cld.argv = argv; + strvec_pushv(&cld.args, argv); if (buffer_input || gzipped_request || req_len >= 0) cld.in = -1; cld.git_cmd = 1; diff --git a/http-fetch.c b/http-fetch.c index fa642462a9..c7c7d391ac 100644 --- a/http-fetch.c +++ b/http-fetch.c @@ -4,6 +4,7 @@ #include "http.h" #include "walker.h" #include "strvec.h" +#include "urlmatch.h" static const char http_fetch_usage[] = "git http-fetch " "[-c] [-t] [-a] [-v] [--recover] [-w ref] [--stdin | --packfile=hash | commit-id] url"; @@ -63,8 +64,17 @@ static void fetch_single_packfile(struct object_id *packfile_hash, if (start_active_slot(preq->slot)) { run_active_slot(preq->slot); if (results.curl_result != CURLE_OK) { - die("Unable to get pack file %s\n%s", preq->url, - curl_errorstr); + struct url_info url; + char *nurl = url_normalize(preq->url, &url); + if (!nurl || !git_env_bool("GIT_TRACE_REDACT", 1)) { + die("unable to get pack file '%s'\n%s", preq->url, + curl_errorstr); + } else { + die("failed to get '%.*s' url from '%.*s' " + "(full URL redacted due to GIT_TRACE_REDACT setting)\n%s", + (int)url.scheme_len, url.url, + (int)url.host_len, &url.url[url.host_off], curl_errorstr); + } } } else { die("Unable to start request"); @@ -2126,8 +2126,9 @@ int finish_http_pack_request(struct http_pack_request *preq) ip.git_cmd = 1; ip.in = tmpfile_fd; - ip.argv = preq->index_pack_args ? preq->index_pack_args - : default_index_pack_args; + strvec_pushv(&ip.args, preq->index_pack_args ? + preq->index_pack_args : + default_index_pack_args); if (preq->preserve_index_pack_stdout) ip.out = 0; diff --git a/log-tree.c b/log-tree.c index 644893fd8c..d3e7a40b64 100644 --- a/log-tree.c +++ b/log-tree.c @@ -513,8 +513,9 @@ static void show_signature(struct rev_info *opt, struct commit *commit) if (parse_signed_commit(commit, &payload, &signature, the_hash_algo) <= 0) goto out; - status = check_signature(payload.buf, payload.len, signature.buf, - signature.len, &sigc); + sigc.payload_type = SIGNATURE_PAYLOAD_COMMIT; + sigc.payload = strbuf_detach(&payload, &sigc.payload_len); + status = check_signature(&sigc, signature.buf, signature.len); if (status && !sigc.output) show_sig_lines(opt, status, "No signature\n"); else @@ -583,8 +584,9 @@ static int show_one_mergetag(struct commit *commit, status = -1; if (parse_signature(extra->value, extra->len, &payload, &signature)) { /* could have a good signature */ - status = check_signature(payload.buf, payload.len, - signature.buf, signature.len, &sigc); + sigc.payload_type = SIGNATURE_PAYLOAD_TAG; + sigc.payload = strbuf_detach(&payload, &sigc.payload_len); + status = check_signature(&sigc, signature.buf, signature.len); if (sigc.output) strbuf_addstr(&verify_message, sigc.output); else diff --git a/mergesort.c b/mergesort.c index 6216835566..bd9c6ef8ee 100644 --- a/mergesort.c +++ b/mergesort.c @@ -63,7 +63,7 @@ void *llist_mergesort(void *list, void *next = get_next_fn(list); if (next) set_next_fn(list, NULL); - for (i = 0; n & (1 << i); i++) + for (i = 0; n & ((size_t)1 << i); i++) list = llist_merge(ranks[i], list, get_next_fn, set_next_fn, compare_fn); n++; @@ -57,15 +57,15 @@ const unsigned char *get_midx_checksum(struct multi_pack_index *m) return m->data + m->data_len - the_hash_algo->rawsz; } -char *get_midx_filename(const char *object_dir) +void get_midx_filename(struct strbuf *out, const char *object_dir) { - return xstrfmt("%s/pack/multi-pack-index", object_dir); + strbuf_addf(out, "%s/pack/multi-pack-index", object_dir); } -char *get_midx_rev_filename(struct multi_pack_index *m) +void get_midx_rev_filename(struct strbuf *out, struct multi_pack_index *m) { - return xstrfmt("%s/pack/multi-pack-index-%s.rev", - m->object_dir, hash_to_hex(get_midx_checksum(m))); + get_midx_filename(out, m->object_dir); + strbuf_addf(out, "-%s.rev", hash_to_hex(get_midx_checksum(m))); } static int midx_read_oid_fanout(const unsigned char *chunk_start, @@ -89,28 +89,30 @@ struct multi_pack_index *load_multi_pack_index(const char *object_dir, int local size_t midx_size; void *midx_map = NULL; uint32_t hash_version; - char *midx_name = get_midx_filename(object_dir); + struct strbuf midx_name = STRBUF_INIT; uint32_t i; const char *cur_pack_name; struct chunkfile *cf = NULL; - fd = git_open(midx_name); + get_midx_filename(&midx_name, object_dir); + + fd = git_open(midx_name.buf); if (fd < 0) goto cleanup_fail; if (fstat(fd, &st)) { - error_errno(_("failed to read %s"), midx_name); + error_errno(_("failed to read %s"), midx_name.buf); goto cleanup_fail; } midx_size = xsize_t(st.st_size); if (midx_size < MIDX_MIN_SIZE) { - error(_("multi-pack-index file %s is too small"), midx_name); + error(_("multi-pack-index file %s is too small"), midx_name.buf); goto cleanup_fail; } - FREE_AND_NULL(midx_name); + strbuf_release(&midx_name); midx_map = xmmap(NULL, midx_size, PROT_READ, MAP_PRIVATE, fd, 0); close(fd); @@ -179,12 +181,13 @@ struct multi_pack_index *load_multi_pack_index(const char *object_dir, int local trace2_data_intmax("midx", the_repository, "load/num_packs", m->num_packs); trace2_data_intmax("midx", the_repository, "load/num_objects", m->num_objects); + free_chunkfile(cf); return m; cleanup_fail: free(m); - free(midx_name); - free(cf); + strbuf_release(&midx_name); + free_chunkfile(cf); if (midx_map) munmap(midx_map, midx_size); if (0 <= fd) @@ -1130,7 +1133,7 @@ static int write_midx_internal(const char *object_dir, const char *refs_snapshot, unsigned flags) { - char *midx_name; + struct strbuf midx_name = STRBUF_INIT; unsigned char midx_hash[GIT_MAX_RAWSZ]; uint32_t i; struct hashfile *f = NULL; @@ -1141,10 +1144,10 @@ static int write_midx_internal(const char *object_dir, int result = 0; struct chunkfile *cf; - midx_name = get_midx_filename(object_dir); - if (safe_create_leading_directories(midx_name)) + get_midx_filename(&midx_name, object_dir); + if (safe_create_leading_directories(midx_name.buf)) die_errno(_("unable to create leading directories of %s"), - midx_name); + midx_name.buf); if (!packs_to_include) { /* @@ -1373,7 +1376,7 @@ static int write_midx_internal(const char *object_dir, pack_name_concat_len += MIDX_CHUNK_ALIGNMENT - (pack_name_concat_len % MIDX_CHUNK_ALIGNMENT); - hold_lock_file_for_update(&lk, midx_name, LOCK_DIE_ON_ERROR); + hold_lock_file_for_update(&lk, midx_name.buf, LOCK_DIE_ON_ERROR); f = hashfd(get_lock_file_fd(&lk), get_lock_file_path(&lk)); if (ctx.nr - dropped_packs == 0) { @@ -1410,9 +1413,9 @@ static int write_midx_internal(const char *object_dir, ctx.pack_order = midx_pack_order(&ctx); if (flags & MIDX_WRITE_REV_INDEX) - write_midx_reverse_index(midx_name, midx_hash, &ctx); + write_midx_reverse_index(midx_name.buf, midx_hash, &ctx); if (flags & MIDX_WRITE_BITMAP) { - if (write_midx_bitmap(midx_name, midx_hash, &ctx, + if (write_midx_bitmap(midx_name.buf, midx_hash, &ctx, refs_snapshot, flags) < 0) { error(_("could not write multi-pack bitmap")); result = 1; @@ -1442,7 +1445,7 @@ cleanup: free(ctx.entries); free(ctx.pack_perm); free(ctx.pack_order); - free(midx_name); + strbuf_release(&midx_name); return result; } @@ -1506,20 +1509,22 @@ static void clear_midx_files_ext(const char *object_dir, const char *ext, void clear_midx_file(struct repository *r) { - char *midx = get_midx_filename(r->objects->odb->path); + struct strbuf midx = STRBUF_INIT; + + get_midx_filename(&midx, r->objects->odb->path); if (r->objects && r->objects->multi_pack_index) { close_midx(r->objects->multi_pack_index); r->objects->multi_pack_index = NULL; } - if (remove_path(midx)) - die(_("failed to clear multi-pack-index at %s"), midx); + if (remove_path(midx.buf)) + die(_("failed to clear multi-pack-index at %s"), midx.buf); clear_midx_files_ext(r->objects->odb->path, ".bitmap", NULL); clear_midx_files_ext(r->objects->odb->path, ".rev", NULL); - free(midx); + strbuf_release(&midx); } static int verify_midx_error; @@ -1572,12 +1577,15 @@ int verify_midx_file(struct repository *r, const char *object_dir, unsigned flag if (!m) { int result = 0; struct stat sb; - char *filename = get_midx_filename(object_dir); - if (!stat(filename, &sb)) { + struct strbuf filename = STRBUF_INIT; + + get_midx_filename(&filename, object_dir); + + if (!stat(filename.buf, &sb)) { error(_("multi-pack-index file exists, but failed to parse")); result = 1; } - free(filename); + strbuf_release(&filename); return result; } @@ -1610,7 +1618,7 @@ int verify_midx_file(struct repository *r, const char *object_dir, unsigned flag * Remaining tests assume that we have objects, so we can * return here. */ - return verify_midx_error; + goto cleanup; } if (flags & MIDX_PROGRESS) @@ -1688,7 +1696,9 @@ int verify_midx_file(struct repository *r, const char *object_dir, unsigned flag } stop_progress(&progress); +cleanup: free(pairs); + close_midx(m); return verify_midx_error; } @@ -48,8 +48,8 @@ struct multi_pack_index { #define MIDX_WRITE_BITMAP_HASH_CACHE (1 << 3) const unsigned char *get_midx_checksum(struct multi_pack_index *m); -char *get_midx_filename(const char *object_dir); -char *get_midx_rev_filename(struct multi_pack_index *m); +void get_midx_filename(struct strbuf *out, const char *object_dir); +void get_midx_rev_filename(struct strbuf *out, struct multi_pack_index *m); struct multi_pack_index *load_multi_pack_index(const char *object_dir, int local); int prepare_midx_pack(struct repository *r, struct multi_pack_index *m, uint32_t pack_int_id); diff --git a/object-file.c b/object-file.c index c3d866a287..8be57f48de 100644 --- a/object-file.c +++ b/object-file.c @@ -165,7 +165,6 @@ static void git_hash_unknown_final_oid(struct object_id *oid, git_hash_ctx *ctx) BUG("trying to finalize unknown hash"); } - const struct git_hash_algo hash_algos[GIT_HASH_NALGOS] = { { NULL, @@ -184,8 +183,7 @@ const struct git_hash_algo hash_algos[GIT_HASH_NALGOS] = { }, { "sha1", - /* "sha1", big-endian */ - 0x73686131, + GIT_SHA1_FORMAT_ID, GIT_SHA1_RAWSZ, GIT_SHA1_HEXSZ, GIT_SHA1_BLKSZ, @@ -200,8 +198,7 @@ const struct git_hash_algo hash_algos[GIT_HASH_NALGOS] = { }, { "sha256", - /* "s256", big-endian */ - 0x73323536, + GIT_SHA256_FORMAT_ID, GIT_SHA256_RAWSZ, GIT_SHA256_HEXSZ, GIT_SHA256_BLKSZ, @@ -683,6 +680,49 @@ void add_to_alternates_memory(const char *reference) '\n', NULL, 0); } +struct object_directory *set_temporary_primary_odb(const char *dir, int will_destroy) +{ + struct object_directory *new_odb; + + /* + * Make sure alternates are initialized, or else our entry may be + * overwritten when they are. + */ + prepare_alt_odb(the_repository); + + /* + * Make a new primary odb and link the old primary ODB in as an + * alternate + */ + new_odb = xcalloc(1, sizeof(*new_odb)); + new_odb->path = xstrdup(dir); + + /* + * Disable ref updates while a temporary odb is active, since + * the objects in the database may roll back. + */ + new_odb->disable_ref_updates = 1; + new_odb->will_destroy = will_destroy; + new_odb->next = the_repository->objects->odb; + the_repository->objects->odb = new_odb; + return new_odb->next; +} + +void restore_primary_odb(struct object_directory *restore_odb, const char *old_path) +{ + struct object_directory *cur_odb = the_repository->objects->odb; + + if (strcmp(old_path, cur_odb->path)) + BUG("expected %s as primary object store; found %s", + old_path, cur_odb->path); + + if (cur_odb->next != restore_odb) + BUG("we expect the old primary object store to be the first alternate"); + + the_repository->objects->odb = restore_odb; + free_object_directory(cur_odb); +} + /* * Compute the exact path an alternate is at and returns it. In case of * error NULL is returned and the human readable error is added to `err` @@ -797,7 +837,7 @@ static void fill_alternate_refs_command(struct child_process *cmd, } } - cmd->env = local_repo_env; + strvec_pushv(&cmd->env_array, (const char **)local_repo_env); cmd->out = -1; } @@ -1306,7 +1346,7 @@ static void *unpack_loose_rest(git_zstream *stream, int parse_loose_header(const char *hdr, struct object_info *oi) { const char *type_buf = hdr; - unsigned long size; + size_t size; int type, type_len = 0; /* @@ -1341,12 +1381,12 @@ int parse_loose_header(const char *hdr, struct object_info *oi) if (c > 9) break; hdr++; - size = size * 10 + c; + size = st_add(st_mult(size, 10), c); } } if (oi->sizep) - *oi->sizep = size; + *oi->sizep = cast_size_t_to_ulong(size); /* * The length must be followed by a zero byte @@ -1809,8 +1849,11 @@ int hash_object_file(const struct git_hash_algo *algo, const void *buf, /* Finalize a file on disk, and close it. */ static void close_loose_object(int fd) { - if (fsync_object_files) - fsync_or_die(fd, "loose object file"); + if (!the_repository->objects->odb->will_destroy) { + if (fsync_object_files) + fsync_or_die(fd, "loose object file"); + } + if (close(fd) != 0) die_errno(_("error when closing loose object file")); } @@ -2425,7 +2468,7 @@ struct oidtree *odb_loose_cache(struct object_directory *odb, struct strbuf buf = STRBUF_INIT; size_t word_bits = bitsizeof(odb->loose_objects_subdir_seen[0]); size_t word_index = subdir_nr / word_bits; - size_t mask = 1u << (subdir_nr % word_bits); + size_t mask = (size_t)1u << (subdir_nr % word_bits); uint32_t *bitmap; if (subdir_nr < 0 || diff --git a/object-store.h b/object-store.h index 952efb6a4b..6f89482df0 100644 --- a/object-store.h +++ b/object-store.h @@ -28,6 +28,18 @@ struct object_directory { struct oidtree *loose_objects_cache; /* + * This is a temporary object store created by the tmp_objdir + * facility. Disable ref updates since the objects in the store + * might be discarded on rollback. + */ + int disable_ref_updates; + + /* + * This object store is ephemeral, so there is no need to fsync. + */ + int will_destroy; + + /* * Path to the alternative object store. If this is a relative path, * it is relative to the current working directory. */ @@ -59,6 +71,17 @@ void add_to_alternates_file(const char *dir); void add_to_alternates_memory(const char *dir); /* + * Replace the current writable object directory with the specified temporary + * object directory; returns the former primary object directory. + */ +struct object_directory *set_temporary_primary_odb(const char *dir, int will_destroy); + +/* + * Restore a previous ODB replaced by set_temporary_main_odb. + */ +void restore_primary_odb(struct object_directory *restore_odb, const char *old_path); + +/* * Populate and return the loose object cache array corresponding to the * given object ID. */ @@ -68,6 +91,9 @@ struct oidtree *odb_loose_cache(struct object_directory *odb, /* Empty the loose object cache for the specified object directory. */ void odb_clear_loose_cache(struct object_directory *odb); +/* Clear and free the specified object directory */ +void free_object_directory(struct object_directory *odb); + struct packed_git { struct hashmap_entry packmap_ent; struct packed_git *next; @@ -199,7 +199,7 @@ struct object *lookup_object_by_type(struct repository *r, case OBJ_BLOB: return (struct object *)lookup_blob(r, oid); default: - die("BUG: unknown object type %d", type); + BUG("unknown object type %d", type); } } @@ -513,7 +513,7 @@ struct raw_object_store *raw_object_store_new(void) return o; } -static void free_object_directory(struct object_directory *odb) +void free_object_directory(struct object_directory *odb) { free(odb->path); odb_clear_loose_cache(odb); diff --git a/pack-bitmap.c b/pack-bitmap.c index f47a0a7db4..f772d3cb7f 100644 --- a/pack-bitmap.c +++ b/pack-bitmap.c @@ -292,9 +292,12 @@ static int load_bitmap_entries_v1(struct bitmap_index *index) char *midx_bitmap_filename(struct multi_pack_index *midx) { - return xstrfmt("%s-%s.bitmap", - get_midx_filename(midx->object_dir), - hash_to_hex(get_midx_checksum(midx))); + struct strbuf buf = STRBUF_INIT; + + get_midx_filename(&buf, midx->object_dir); + strbuf_addf(&buf, "-%s.bitmap", hash_to_hex(get_midx_checksum(midx))); + + return strbuf_detach(&buf, NULL); } char *pack_bitmap_filename(struct packed_git *p) @@ -324,10 +327,12 @@ static int open_midx_bitmap_1(struct bitmap_index *bitmap_git, } if (bitmap_git->pack || bitmap_git->midx) { + struct strbuf buf = STRBUF_INIT; + get_midx_filename(&buf, midx->object_dir); /* ignore extra bitmap file; we can only handle one */ - warning("ignoring extra bitmap file: %s", - get_midx_filename(midx->object_dir)); + warning("ignoring extra bitmap file: %s", buf.buf); close(fd); + strbuf_release(&buf); return -1; } @@ -1721,6 +1726,12 @@ void test_bitmap_walk(struct rev_info *revs) else die("mismatch in bitmap results"); + bitmap_free(result); + bitmap_free(tdata.base); + bitmap_free(tdata.commits); + bitmap_free(tdata.trees); + bitmap_free(tdata.blobs); + bitmap_free(tdata.tags); free_bitmap_index(bitmap_git); } @@ -1748,7 +1759,7 @@ int test_bitmap_hashes(struct repository *r) struct object_id oid; uint32_t i, index_pos; - if (!bitmap_git->hashes) + if (!bitmap_git || !bitmap_git->hashes) goto cleanup; for (i = 0; i < bitmap_num_objects(bitmap_git); i++) { @@ -1848,9 +1859,17 @@ void free_bitmap_index(struct bitmap_index *b) ewah_pool_free(b->trees); ewah_pool_free(b->blobs); ewah_pool_free(b->tags); + if (b->bitmaps) { + struct stored_bitmap *sb; + kh_foreach_value(b->bitmaps, sb, { + ewah_pool_free(sb->root); + free(sb); + }); + } kh_destroy_oid_map(b->bitmaps); free(b->ext_index.objects); free(b->ext_index.hashes); + kh_destroy_oid_pos(b->ext_index.positions); bitmap_free(b->result); bitmap_free(b->haves); if (bitmap_is_midx(b)) { diff --git a/pack-revindex.c b/pack-revindex.c index 0e4a31d9db..70d0fbafcb 100644 --- a/pack-revindex.c +++ b/pack-revindex.c @@ -296,14 +296,14 @@ int load_pack_revindex(struct packed_git *p) int load_midx_revindex(struct multi_pack_index *m) { - char *revindex_name; + struct strbuf revindex_name = STRBUF_INIT; int ret; if (m->revindex_data) return 0; - revindex_name = get_midx_rev_filename(m); + get_midx_rev_filename(&revindex_name, m); - ret = load_revindex_from_disk(revindex_name, + ret = load_revindex_from_disk(revindex_name.buf, m->num_objects, &m->revindex_map, &m->revindex_len); @@ -313,7 +313,7 @@ int load_midx_revindex(struct multi_pack_index *m) m->revindex_data = (const uint32_t *)((const char *)m->revindex_map + RIDX_HEADER_SIZE); cleanup: - free(revindex_name); + strbuf_release(&revindex_name); return ret; } diff --git a/packfile.c b/packfile.c index 89402cfc69..11bb262482 100644 --- a/packfile.c +++ b/packfile.c @@ -324,7 +324,8 @@ void close_pack_index(struct packed_git *p) } } -void close_pack_revindex(struct packed_git *p) { +static void close_pack_revindex(struct packed_git *p) +{ if (!p->revindex_map) return; @@ -1060,7 +1061,7 @@ unsigned long unpack_object_header_buffer(const unsigned char *buf, unsigned long len, enum object_type *type, unsigned long *sizep) { unsigned shift; - unsigned long size, c; + size_t size, c; unsigned long used = 0; c = buf[used++]; @@ -1068,16 +1069,16 @@ unsigned long unpack_object_header_buffer(const unsigned char *buf, size = c & 15; shift = 4; while (c & 0x80) { - if (len <= used || bitsizeof(long) <= shift) { + if (len <= used || (bitsizeof(long) - 7) <= shift) { error("bad object header"); size = used = 0; break; } c = buf[used++]; - size += (c & 0x7f) << shift; + size = st_add(size, st_left_shift(c & 0x7f, shift)); shift += 7; } - *sizep = size; + *sizep = cast_size_t_to_ulong(size); return used; } diff --git a/packfile.h b/packfile.h index 186146779d..a3f6723857 100644 --- a/packfile.h +++ b/packfile.h @@ -90,7 +90,6 @@ uint32_t get_pack_fanout(struct packed_git *p, uint32_t value); unsigned char *use_pack(struct packed_git *, struct pack_window **, off_t, unsigned long *); void close_pack_windows(struct packed_git *); -void close_pack_revindex(struct packed_git *); void close_pack(struct packed_git *); void close_object_store(struct raw_object_store *o); void unuse_pack(struct pack_window **); @@ -8,7 +8,7 @@ #define DEFAULT_PAGER "less" #endif -static struct child_process pager_process = CHILD_PROCESS_INIT; +static struct child_process pager_process; static const char *pager_program; /* Is the value coming back from term_columns() just a guess? */ @@ -124,6 +124,8 @@ void setup_pager(void) setenv("GIT_PAGER_IN_USE", "true", 1); + child_process_init(&pager_process); + /* spawn the pager */ prepare_pager_args(&pager_process, pager); pager_process.in = -1; diff --git a/parallel-checkout.c b/parallel-checkout.c index ed9c999520..8dd7e7bad4 100644 --- a/parallel-checkout.c +++ b/parallel-checkout.c @@ -261,7 +261,7 @@ static int write_pc_item_to_fd(struct parallel_checkout_item *pc_item, int fd, struct stream_filter *filter; struct strbuf buf = STRBUF_INIT; char *blob; - unsigned long size; + size_t size; ssize_t wrote; /* Sanity check */ diff --git a/parse-options.c b/parse-options.c index fc5b43ff0b..629e796349 100644 --- a/parse-options.c +++ b/parse-options.c @@ -404,8 +404,9 @@ is_abbreviated: return PARSE_OPT_UNKNOWN; } -static int parse_nodash_opt(struct parse_opt_ctx_t *p, const char *arg, - const struct option *options) +static enum parse_opt_result parse_nodash_opt(struct parse_opt_ctx_t *p, + const char *arg, + const struct option *options) { const struct option *all_opts = options; @@ -415,7 +416,7 @@ static int parse_nodash_opt(struct parse_opt_ctx_t *p, const char *arg, if (options->short_name == arg[0] && arg[1] == '\0') return get_value(p, options, all_opts, OPT_SHORT); } - return -2; + return PARSE_OPT_ERROR; } static void check_typos(const char *arg, const struct option *options) diff --git a/pathspec.h b/pathspec.h index 2341dc9901..402ebb8080 100644 --- a/pathspec.h +++ b/pathspec.h @@ -58,8 +58,7 @@ struct pathspec { #define GUARD_PATHSPEC(ps, mask) \ do { \ if ((ps)->magic & ~(mask)) \ - die("BUG:%s:%d: unsupported magic %x", \ - __FILE__, __LINE__, (ps)->magic & ~(mask)); \ + BUG("unsupported magic %x", (ps)->magic & ~(mask)); \ } while (0) /* parse_pathspec flags */ diff --git a/perl/Git/SVN.pm b/perl/Git/SVN.pm index 35ff5a6896..6ce2e283c8 100644 --- a/perl/Git/SVN.pm +++ b/perl/Git/SVN.pm @@ -6,7 +6,7 @@ use constant rev_map_fmt => 'NH*'; use vars qw/$_no_metadata $_repack $_repack_flags $_use_svm_props $_head $_use_svnsync_props $no_reuse_existing - $_use_log_author $_add_author_from $_localtime/; + $_use_log_author $_add_author_from $_localtime $_use_fsync/; use Carp qw/croak/; use File::Path qw/mkpath/; use IPC::Open3; @@ -2269,6 +2269,19 @@ sub mkfile { } } +# TODO: move this to Git.pm? +sub use_fsync { + if (!defined($_use_fsync)) { + my $x = $ENV{GIT_TEST_FSYNC}; + if (defined $x) { + my $v = command_oneline('-c', "test.fsync=$x", + qw(config --type=bool test.fsync)); + $_use_fsync = defined($v) ? ($v eq "true\n") : 1; + } + } + $_use_fsync; +} + sub rev_map_set { my ($self, $rev, $commit, $update_ref, $uuid) = @_; defined $commit or die "missing arg3\n"; @@ -2290,7 +2303,7 @@ sub rev_map_set { my $sync; # both of these options make our .rev_db file very, very important # and we can't afford to lose it because rebuild() won't work - if ($self->use_svm_props || $self->no_metadata) { + if (($self->use_svm_props || $self->no_metadata) && use_fsync()) { require File::Copy; $sync = 1; File::Copy::copy($db, $db_lock) or die "rev_map_set(@_): ", diff --git a/pkt-line.c b/pkt-line.c index 2dc8ac274b..8e43c2def4 100644 --- a/pkt-line.c +++ b/pkt-line.c @@ -370,6 +370,32 @@ int packet_length(const char lenbuf_hex[4]) return (val < 0) ? val : (val << 8) | hex2chr(lenbuf_hex + 2); } +static char *find_packfile_uri_path(const char *buffer) +{ + const char *URI_MARK = "://"; + char *path; + int len; + + /* First char is sideband mark */ + buffer += 1; + + len = strspn(buffer, "0123456789abcdefABCDEF"); + /* size of SHA1 and SHA256 hash */ + if (!(len == 40 || len == 64) || buffer[len] != ' ') + return NULL; /* required "<hash>SP" not seen */ + + path = strstr(buffer + len + 1, URI_MARK); + if (!path) + return NULL; + + path = strchr(path + strlen(URI_MARK), '/'); + if (!path || !*(path + 1)) + return NULL; + + /* position after '/' */ + return ++path; +} + enum packet_read_status packet_read_with_status(int fd, char **src_buffer, size_t *src_len, char *buffer, unsigned size, int *pktlen, @@ -377,6 +403,7 @@ enum packet_read_status packet_read_with_status(int fd, char **src_buffer, { int len; char linelen[4]; + char *uri_path_start; if (get_packet_data(fd, src_buffer, src_len, linelen, 4, options) < 0) { *pktlen = -1; @@ -427,7 +454,18 @@ enum packet_read_status packet_read_with_status(int fd, char **src_buffer, len--; buffer[len] = 0; - packet_trace(buffer, len, 0); + if (options & PACKET_READ_REDACT_URI_PATH && + (uri_path_start = find_packfile_uri_path(buffer))) { + const char *redacted = "<redacted>"; + struct strbuf tracebuf = STRBUF_INIT; + strbuf_insert(&tracebuf, 0, buffer, len); + strbuf_splice(&tracebuf, uri_path_start - buffer, + strlen(uri_path_start), redacted, strlen(redacted)); + packet_trace(tracebuf.buf, tracebuf.len, 0); + strbuf_release(&tracebuf); + } else { + packet_trace(buffer, len, 0); + } if ((options & PACKET_READ_DIE_ON_ERR_PACKET) && starts_with(buffer, "ERR ")) diff --git a/pkt-line.h b/pkt-line.h index 467ae01357..6d2a63db23 100644 --- a/pkt-line.h +++ b/pkt-line.h @@ -87,6 +87,7 @@ void packet_fflush(FILE *f); #define PACKET_READ_CHOMP_NEWLINE (1u<<1) #define PACKET_READ_DIE_ON_ERR_PACKET (1u<<2) #define PACKET_READ_GENTLE_ON_READ_ERROR (1u<<3) +#define PACKET_READ_REDACT_URI_PATH (1u<<4) int packet_read(int fd, char *buffer, unsigned size, int options); /* diff --git a/po/README.md b/po/README.md index dcd8436c25..19fabb4acf 100644 --- a/po/README.md +++ b/po/README.md @@ -221,6 +221,10 @@ General advice: - Adjust the strings so that they're easy to translate. Most of the advice in `info '(gettext)Preparing Strings'` applies here. +- Strings referencing numbers of items may need to be split into singular and + plural forms; see the Q\_() wrapper in the C sub-section below for an + example. + - If something is unclear or ambiguous you can use a "TRANSLATORS" comment to tell the translators what to make of it. These will be extracted by xgettext(1) and put in the "po/\*.po" files, e.g. from @@ -1275,28 +1275,66 @@ int format_set_trailers_options(struct process_trailer_options *opts, static size_t parse_describe_args(const char *start, struct strvec *args) { - const char *options[] = { "match", "exclude" }; + struct { + char *name; + enum { + DESCRIBE_ARG_BOOL, + DESCRIBE_ARG_INTEGER, + DESCRIBE_ARG_STRING, + } type; + } option[] = { + { "tags", DESCRIBE_ARG_BOOL}, + { "abbrev", DESCRIBE_ARG_INTEGER }, + { "exclude", DESCRIBE_ARG_STRING }, + { "match", DESCRIBE_ARG_STRING }, + }; const char *arg = start; for (;;) { - const char *matched = NULL; + int found = 0; const char *argval; size_t arglen = 0; + int optval = 0; int i; - for (i = 0; i < ARRAY_SIZE(options); i++) { - if (match_placeholder_arg_value(arg, options[i], &arg, - &argval, &arglen)) { - matched = options[i]; + for (i = 0; !found && i < ARRAY_SIZE(option); i++) { + switch (option[i].type) { + case DESCRIBE_ARG_BOOL: + if (match_placeholder_bool_arg(arg, option[i].name, &arg, &optval)) { + if (optval) + strvec_pushf(args, "--%s", option[i].name); + else + strvec_pushf(args, "--no-%s", option[i].name); + found = 1; + } + break; + case DESCRIBE_ARG_INTEGER: + if (match_placeholder_arg_value(arg, option[i].name, &arg, + &argval, &arglen)) { + char *endptr; + if (!arglen) + return 0; + strtol(argval, &endptr, 10); + if (endptr - argval != arglen) + return 0; + strvec_pushf(args, "--%s=%.*s", option[i].name, (int)arglen, argval); + found = 1; + } + break; + case DESCRIBE_ARG_STRING: + if (match_placeholder_arg_value(arg, option[i].name, &arg, + &argval, &arglen)) { + if (!arglen) + return 0; + strvec_pushf(args, "--%s=%.*s", option[i].name, (int)arglen, argval); + found = 1; + } break; } } - if (!matched) + if (!found) break; - if (!arglen) - return 0; - strvec_pushf(args, "--%s=%.*s", matched, (int)arglen, argval); } return arg - start; } @@ -8,15 +8,12 @@ static char *do_askpass(const char *cmd, const char *prompt) { struct child_process pass = CHILD_PROCESS_INIT; - const char *args[3]; static struct strbuf buffer = STRBUF_INIT; int err = 0; - args[0] = cmd; - args[1] = prompt; - args[2] = NULL; + strvec_push(&pass.args, cmd); + strvec_push(&pass.args, prompt); - pass.argv = args; pass.out = -1; if (start_command(&pass)) diff --git a/read-cache.c b/read-cache.c index f398659662..cbe73f14e5 100644 --- a/read-cache.c +++ b/read-cache.c @@ -68,6 +68,11 @@ */ #define CACHE_ENTRY_PATH_LENGTH 80 +enum index_search_mode { + NO_EXPAND_SPARSE = 0, + EXPAND_SPARSE = 1 +}; + static inline struct cache_entry *mem_pool__ce_alloc(struct mem_pool *mem_pool, size_t len) { struct cache_entry *ce; @@ -551,7 +556,10 @@ int cache_name_stage_compare(const char *name1, int len1, int stage1, const char return 0; } -static int index_name_stage_pos(struct index_state *istate, const char *name, int namelen, int stage) +static int index_name_stage_pos(struct index_state *istate, + const char *name, int namelen, + int stage, + enum index_search_mode search_mode) { int first, last; @@ -570,7 +578,7 @@ static int index_name_stage_pos(struct index_state *istate, const char *name, in first = next+1; } - if (istate->sparse_index && + if (search_mode == EXPAND_SPARSE && istate->sparse_index && first > 0) { /* Note: first <= istate->cache_nr */ struct cache_entry *ce = istate->cache[first - 1]; @@ -586,7 +594,7 @@ static int index_name_stage_pos(struct index_state *istate, const char *name, in ce_namelen(ce) < namelen && !strncmp(name, ce->name, ce_namelen(ce))) { ensure_full_index(istate); - return index_name_stage_pos(istate, name, namelen, stage); + return index_name_stage_pos(istate, name, namelen, stage, search_mode); } } @@ -595,7 +603,12 @@ static int index_name_stage_pos(struct index_state *istate, const char *name, in int index_name_pos(struct index_state *istate, const char *name, int namelen) { - return index_name_stage_pos(istate, name, namelen, 0); + return index_name_stage_pos(istate, name, namelen, 0, EXPAND_SPARSE); +} + +int index_entry_exists(struct index_state *istate, const char *name, int namelen) +{ + return index_name_stage_pos(istate, name, namelen, 0, NO_EXPAND_SPARSE) >= 0; } int remove_index_entry_at(struct index_state *istate, int pos) @@ -1237,7 +1250,7 @@ static int has_dir_name(struct index_state *istate, */ } - pos = index_name_stage_pos(istate, name, len, stage); + pos = index_name_stage_pos(istate, name, len, stage, EXPAND_SPARSE); if (pos >= 0) { /* * Found one, but not so fast. This could @@ -1337,7 +1350,7 @@ static int add_index_entry_with_check(struct index_state *istate, struct cache_e strcmp(ce->name, istate->cache[istate->cache_nr - 1]->name) > 0) pos = index_pos_to_insert_pos(istate->cache_nr); else - pos = index_name_stage_pos(istate, ce->name, ce_namelen(ce), ce_stage(ce)); + pos = index_name_stage_pos(istate, ce->name, ce_namelen(ce), ce_stage(ce), EXPAND_SPARSE); /* existing match? Just replace it. */ if (pos >= 0) { @@ -1372,7 +1385,7 @@ static int add_index_entry_with_check(struct index_state *istate, struct cache_e if (!ok_to_replace) return error(_("'%s' appears as both a file and as a directory"), ce->name); - pos = index_name_stage_pos(istate, ce->name, ce_namelen(ce), ce_stage(ce)); + pos = index_name_stage_pos(istate, ce->name, ce_namelen(ce), ce_stage(ce), EXPAND_SPARSE); pos = -pos-1; } return pos + 1; @@ -2352,9 +2365,17 @@ int do_read_index(struct index_state *istate, const char *path, int must_exist) if (!istate->repo) istate->repo = the_repository; + + /* + * If the command explicitly requires a full index, force it + * to be full. Otherwise, correct the sparsity based on repository + * settings and other properties of the index (if necessary). + */ prepare_repo_settings(istate->repo); if (istate->repo->settings.command_requires_full_index) ensure_full_index(istate); + else + ensure_correct_sparsity(istate); return istate->cache_nr; diff --git a/ref-filter.c b/ref-filter.c index 08a3f839c9..7260fce31d 100644 --- a/ref-filter.c +++ b/ref-filter.c @@ -2470,6 +2470,12 @@ static int memcasecmp(const void *vs1, const void *vs2, size_t n) return 0; } +struct ref_sorting { + struct ref_sorting *next; + int atom; /* index into used_atom array (internal) */ + enum ref_sorting_order sort_flags; +}; + static int cmp_ref_sorting(struct ref_sorting *s, struct ref_array_item *a, struct ref_array_item *b) { struct atom_value *va, *vb; @@ -2663,7 +2669,7 @@ static int parse_sorting_atom(const char *atom) } /* If no sorting option is given, use refname to sort as default */ -struct ref_sorting *ref_default_sorting(void) +static struct ref_sorting *ref_default_sorting(void) { static const char cstr_name[] = "refname"; @@ -2674,7 +2680,7 @@ struct ref_sorting *ref_default_sorting(void) return sorting; } -void parse_ref_sorting(struct ref_sorting **sorting_tail, const char *arg) +static void parse_ref_sorting(struct ref_sorting **sorting_tail, const char *arg) { struct ref_sorting *s; @@ -2692,17 +2698,25 @@ void parse_ref_sorting(struct ref_sorting **sorting_tail, const char *arg) s->atom = parse_sorting_atom(arg); } -int parse_opt_ref_sorting(const struct option *opt, const char *arg, int unset) +struct ref_sorting *ref_sorting_options(struct string_list *options) { + struct string_list_item *item; + struct ref_sorting *sorting = NULL, **tail = &sorting; + + if (!options->nr) { + sorting = ref_default_sorting(); + } else { + for_each_string_list_item(item, options) + parse_ref_sorting(tail, item->string); + } + /* - * NEEDSWORK: We should probably clear the list in this case, but we've - * already munged the global used_atoms list, which would need to be - * undone. + * From here on, the ref_sorting list should be used to talk + * about the sort order used for the output. The caller + * should not touch the string form anymore. */ - BUG_ON_OPT_NEG(unset); - - parse_ref_sorting(opt->value, arg); - return 0; + string_list_clear(options, 0); + return sorting; } void ref_sorting_release(struct ref_sorting *sorting) diff --git a/ref-filter.h b/ref-filter.h index 6228458d30..aa0eea4ecf 100644 --- a/ref-filter.h +++ b/ref-filter.h @@ -23,16 +23,13 @@ #define FILTER_REFS_KIND_MASK (FILTER_REFS_ALL | FILTER_REFS_DETACHED_HEAD) struct atom_value; +struct ref_sorting; -struct ref_sorting { - struct ref_sorting *next; - int atom; /* index into used_atom array (internal) */ - enum { - REF_SORTING_REVERSE = 1<<0, - REF_SORTING_ICASE = 1<<1, - REF_SORTING_VERSION = 1<<2, - REF_SORTING_DETACHED_HEAD_FIRST = 1<<3, - } sort_flags; +enum ref_sorting_order { + REF_SORTING_REVERSE = 1<<0, + REF_SORTING_ICASE = 1<<1, + REF_SORTING_VERSION = 1<<2, + REF_SORTING_DETACHED_HEAD_FIRST = 1<<3, }; struct ref_array_item { @@ -97,9 +94,8 @@ struct ref_format { #define OPT_NO_MERGED(f, h) _OPT_MERGED_NO_MERGED("no-merged", f, h) #define OPT_REF_SORT(var) \ - OPT_CALLBACK_F(0, "sort", (var), \ - N_("key"), N_("field name to sort on"), \ - PARSE_OPT_NONEG, parse_opt_ref_sorting) + OPT_STRING_LIST(0, "sort", (var), \ + N_("key"), N_("field name to sort on")) /* * API for filtering a set of refs. Based on the type of refs the user @@ -121,14 +117,10 @@ int format_ref_array_item(struct ref_array_item *info, struct ref_format *format, struct strbuf *final_buf, struct strbuf *error_buf); -/* Parse a single sort specifier and add it to the list */ -void parse_ref_sorting(struct ref_sorting **sorting_tail, const char *atom); -/* Callback function for parsing the sort option */ -int parse_opt_ref_sorting(const struct option *opt, const char *arg, int unset); -/* Default sort option based on refname */ -struct ref_sorting *ref_default_sorting(void); /* Release a "struct ref_sorting" */ void ref_sorting_release(struct ref_sorting *); +/* Convert list of sort options into ref_sorting */ +struct ref_sorting *ref_sorting_options(struct string_list *); /* Function to parse --merged and --no-merged options */ int parse_opt_merge_filter(const struct option *opt, const char *arg, int unset); /* Get the current HEAD's description */ @@ -269,9 +269,10 @@ char *refs_resolve_refdup(struct ref_store *refs, struct object_id *oid, int *flags) { const char *result; + int ignore_errno; result = refs_resolve_ref_unsafe(refs, refname, resolve_flags, - oid, flags); + oid, flags, &ignore_errno); return xstrdup_or_null(result); } @@ -291,20 +292,17 @@ struct ref_filter { void *cb_data; }; -int refs_read_ref_full(struct ref_store *refs, const char *refname, - int resolve_flags, struct object_id *oid, int *flags) +int read_ref_full(const char *refname, int resolve_flags, struct object_id *oid, int *flags) { - if (refs_resolve_ref_unsafe(refs, refname, resolve_flags, oid, flags)) + int ignore_errno; + struct ref_store *refs = get_main_ref_store(the_repository); + + if (refs_resolve_ref_unsafe(refs, refname, resolve_flags, + oid, flags, &ignore_errno)) return 0; return -1; } -int read_ref_full(const char *refname, int resolve_flags, struct object_id *oid, int *flags) -{ - return refs_read_ref_full(get_main_ref_store(the_repository), refname, - resolve_flags, oid, flags); -} - int read_ref(const char *refname, struct object_id *oid) { return read_ref_full(refname, RESOLVE_REF_READING, oid, NULL); @@ -312,7 +310,9 @@ int read_ref(const char *refname, struct object_id *oid) int refs_ref_exists(struct ref_store *refs, const char *refname) { - return !!refs_resolve_ref_unsafe(refs, refname, RESOLVE_REF_READING, NULL, NULL); + int ignore_errno; + return !!refs_resolve_ref_unsafe(refs, refname, RESOLVE_REF_READING, + NULL, NULL, &ignore_errno); } int ref_exists(const char *refname) @@ -655,13 +655,16 @@ int expand_ref(struct repository *repo, const char *str, int len, struct object_id oid_from_ref; struct object_id *this_result; int flag; + struct ref_store *refs = get_main_ref_store(repo); + int ignore_errno; this_result = refs_found ? &oid_from_ref : oid; strbuf_reset(&fullref); strbuf_addf(&fullref, *p, len, str); - r = refs_resolve_ref_unsafe(get_main_ref_store(repo), - fullref.buf, RESOLVE_REF_READING, - this_result, &flag); + r = refs_resolve_ref_unsafe(refs, fullref.buf, + RESOLVE_REF_READING, + this_result, &flag, + &ignore_errno); if (r) { if (!refs_found++) *ref = xstrdup(r); @@ -690,12 +693,14 @@ int repo_dwim_log(struct repository *r, const char *str, int len, for (p = ref_rev_parse_rules; *p; p++) { struct object_id hash; const char *ref, *it; + int ignore_errno; strbuf_reset(&path); strbuf_addf(&path, *p, len, str); ref = refs_resolve_ref_unsafe(refs, path.buf, RESOLVE_REF_READING, - oid ? &hash : NULL, NULL); + oid ? &hash : NULL, NULL, + &ignore_errno); if (!ref) continue; if (refs_reflog_exists(refs, path.buf)) @@ -1078,9 +1083,10 @@ int ref_transaction_update(struct ref_transaction *transaction, { assert(err); - if ((new_oid && !is_null_oid(new_oid)) ? - check_refname_format(refname, REFNAME_ALLOW_ONELEVEL) : - !refname_is_safe(refname)) { + if (!(flags & REF_SKIP_REFNAME_VERIFICATION) && + ((new_oid && !is_null_oid(new_oid)) ? + check_refname_format(refname, REFNAME_ALLOW_ONELEVEL) : + !refname_is_safe(refname))) { strbuf_addf(err, _("refusing to update ref with bad name '%s'"), refname); return -1; @@ -1089,6 +1095,13 @@ int ref_transaction_update(struct ref_transaction *transaction, if (flags & ~REF_TRANSACTION_UPDATE_ALLOWED_FLAGS) BUG("illegal flags 0x%x passed to ref_transaction_update()", flags); + /* + * Clear flags outside the allowed set; this should be a noop because + * of the BUG() check above, but it works around a -Wnonnull warning + * with some versions of "gcc -O3". + */ + flags &= REF_TRANSACTION_UPDATE_ALLOWED_FLAGS; + flags |= (new_oid ? REF_HAVE_NEW : 0) | (old_oid ? REF_HAVE_OLD : 0); ref_transaction_add_update(transaction, refname, flags, @@ -1373,32 +1386,14 @@ const char *find_descendant_ref(const char *dirname, return NULL; } -int refs_rename_ref_available(struct ref_store *refs, - const char *old_refname, - const char *new_refname) -{ - struct string_list skip = STRING_LIST_INIT_NODUP; - struct strbuf err = STRBUF_INIT; - int ok; - - string_list_insert(&skip, old_refname); - ok = !refs_verify_refname_available(refs, new_refname, - NULL, &skip, &err); - if (!ok) - error("%s", err.buf); - - string_list_clear(&skip, 0); - strbuf_release(&err); - return ok; -} - int refs_head_ref(struct ref_store *refs, each_ref_fn fn, void *cb_data) { struct object_id oid; int flag; + int ignore_errno; - if (!refs_read_ref_full(refs, "HEAD", RESOLVE_REF_READING, - &oid, &flag)) + if (refs_resolve_ref_unsafe(refs, "HEAD", RESOLVE_REF_READING, + &oid, &flag, &ignore_errno)) return fn("HEAD", &oid, flag, cb_data); return 0; @@ -1649,7 +1644,8 @@ int for_each_fullref_in_prefixes(const char *namespace, static int refs_read_special_head(struct ref_store *ref_store, const char *refname, struct object_id *oid, - struct strbuf *referent, unsigned int *type) + struct strbuf *referent, unsigned int *type, + int *failure_errno) { struct strbuf full_path = STRBUF_INIT; struct strbuf content = STRBUF_INIT; @@ -1659,7 +1655,8 @@ static int refs_read_special_head(struct ref_store *ref_store, if (strbuf_read_file(&content, full_path.buf, 0) < 0) goto done; - result = parse_loose_ref_contents(content.buf, oid, referent, type); + result = parse_loose_ref_contents(content.buf, oid, referent, type, + failure_errno); done: strbuf_release(&full_path); @@ -1667,30 +1664,33 @@ done: return result; } -int refs_read_raw_ref(struct ref_store *ref_store, - const char *refname, struct object_id *oid, - struct strbuf *referent, unsigned int *type) +int refs_read_raw_ref(struct ref_store *ref_store, const char *refname, + struct object_id *oid, struct strbuf *referent, + unsigned int *type, int *failure_errno) { + assert(failure_errno); if (!strcmp(refname, "FETCH_HEAD") || !strcmp(refname, "MERGE_HEAD")) { return refs_read_special_head(ref_store, refname, oid, referent, - type); + type, failure_errno); } return ref_store->be->read_raw_ref(ref_store, refname, oid, referent, - type, &errno); + type, failure_errno); } -/* This function needs to return a meaningful errno on failure */ const char *refs_resolve_ref_unsafe(struct ref_store *refs, const char *refname, int resolve_flags, - struct object_id *oid, int *flags) + struct object_id *oid, + int *flags, int *failure_errno) { static struct strbuf sb_refname = STRBUF_INIT; struct object_id unused_oid; int unused_flags; int symref_count; + assert(failure_errno); + if (!oid) oid = &unused_oid; if (!flags) @@ -1701,7 +1701,7 @@ const char *refs_resolve_ref_unsafe(struct ref_store *refs, if (check_refname_format(refname, REFNAME_ALLOW_ONELEVEL)) { if (!(resolve_flags & RESOLVE_REF_ALLOW_BAD_NAME) || !refname_is_safe(refname)) { - errno = EINVAL; + *failure_errno = EINVAL; return NULL; } @@ -1719,9 +1719,11 @@ const char *refs_resolve_ref_unsafe(struct ref_store *refs, for (symref_count = 0; symref_count < SYMREF_MAXDEPTH; symref_count++) { unsigned int read_flags = 0; - if (refs_read_raw_ref(refs, refname, - oid, &sb_refname, &read_flags)) { + if (refs_read_raw_ref(refs, refname, oid, &sb_refname, + &read_flags, failure_errno)) { *flags |= read_flags; + if (errno) + *failure_errno = errno; /* In reading mode, refs must eventually resolve */ if (resolve_flags & RESOLVE_REF_READING) @@ -1732,9 +1734,9 @@ const char *refs_resolve_ref_unsafe(struct ref_store *refs, * may show errors besides ENOENT if there are * similarly-named refs. */ - if (errno != ENOENT && - errno != EISDIR && - errno != ENOTDIR) + if (*failure_errno != ENOENT && + *failure_errno != EISDIR && + *failure_errno != ENOTDIR) return NULL; oidclr(oid); @@ -1761,7 +1763,7 @@ const char *refs_resolve_ref_unsafe(struct ref_store *refs, if (check_refname_format(refname, REFNAME_ALLOW_ONELEVEL)) { if (!(resolve_flags & RESOLVE_REF_ALLOW_BAD_NAME) || !refname_is_safe(refname)) { - errno = EINVAL; + *failure_errno = EINVAL; return NULL; } @@ -1769,7 +1771,7 @@ const char *refs_resolve_ref_unsafe(struct ref_store *refs, } } - errno = ELOOP; + *failure_errno = ELOOP; return NULL; } @@ -1784,8 +1786,10 @@ int refs_init_db(struct strbuf *err) const char *resolve_ref_unsafe(const char *refname, int resolve_flags, struct object_id *oid, int *flags) { + int ignore_errno; + return refs_resolve_ref_unsafe(get_main_ref_store(the_repository), refname, - resolve_flags, oid, flags); + resolve_flags, oid, flags, &ignore_errno); } int resolve_gitlink_ref(const char *submodule, const char *refname, @@ -1793,14 +1797,15 @@ int resolve_gitlink_ref(const char *submodule, const char *refname, { struct ref_store *refs; int flags; + int ignore_errno; refs = get_submodule_ref_store(submodule); if (!refs) return -1; - if (!refs_resolve_ref_unsafe(refs, refname, 0, oid, &flags) || - is_null_oid(oid)) + if (!refs_resolve_ref_unsafe(refs, refname, 0, oid, &flags, + &ignore_errno) || is_null_oid(oid)) return -1; return 0; } @@ -2002,10 +2007,12 @@ struct ref_store *get_worktree_ref_store(const struct worktree *wt) return refs; } -void base_ref_store_init(struct ref_store *refs, - const struct ref_storage_be *be) +void base_ref_store_init(struct ref_store *refs, struct repository *repo, + const char *path, const struct ref_storage_be *be) { refs->be = be; + refs->repo = repo; + refs->gitdir = xstrdup(path); } /* backend functions */ @@ -2102,8 +2109,11 @@ static int run_transaction_hook(struct ref_transaction *transaction, update->refname); if (write_in_full(proc.in, buf.buf, buf.len) < 0) { - if (errno != EPIPE) + if (errno != EPIPE) { + /* Don't leak errno outside this API */ + errno = 0; ret = -1; + } break; } } @@ -2137,7 +2147,7 @@ int ref_transaction_prepare(struct ref_transaction *transaction, break; } - if (getenv(GIT_QUARANTINE_ENVIRONMENT)) { + if (refs->repo->objects->odb->disable_ref_updates) { strbuf_addstr(err, _("ref updates forbidden inside quarantine environment")); return -1; @@ -2238,6 +2248,13 @@ int refs_verify_refname_available(struct ref_store *refs, strbuf_grow(&dirname, strlen(refname) + 1); for (slash = strchr(refname, '/'); slash; slash = strchr(slash + 1, '/')) { + /* + * Just saying "Is a directory" when we e.g. can't + * lock some multi-level ref isn't very informative, + * the user won't be told *what* is a directory, so + * let's not use strerror() below. + */ + int ignore_errno; /* Expand dirname to the new prefix, not including the trailing slash: */ strbuf_add(&dirname, refname + dirname.len, slash - refname - dirname.len); @@ -2249,7 +2266,8 @@ int refs_verify_refname_available(struct ref_store *refs, if (skip && string_list_has_string(skip, dirname.buf)) continue; - if (!refs_read_raw_ref(refs, dirname.buf, &oid, &referent, &type)) { + if (!refs_read_raw_ref(refs, dirname.buf, &oid, &referent, + &type, &ignore_errno)) { strbuf_addf(err, _("'%s' exists; cannot create '%s'"), dirname.buf, refname); goto cleanup; @@ -2358,16 +2376,15 @@ int reflog_exists(const char *refname) } int refs_create_reflog(struct ref_store *refs, const char *refname, - int force_create, struct strbuf *err) + struct strbuf *err) { - return refs->be->create_reflog(refs, refname, force_create, err); + return refs->be->create_reflog(refs, refname, err); } -int safe_create_reflog(const char *refname, int force_create, - struct strbuf *err) +int safe_create_reflog(const char *refname, struct strbuf *err) { return refs_create_reflog(get_main_ref_store(the_repository), refname, - force_create, err); + err); } int refs_delete_reflog(struct ref_store *refs, const char *refname) @@ -58,6 +58,11 @@ struct worktree; * resolved. The function returns NULL for such ref names. * Caps and underscores refers to the special refs, such as HEAD, * FETCH_HEAD and friends, that all live outside of the refs/ directory. + * + * Callers should not inspect "errno" on failure, but rather pass in a + * "failure_errno" parameter, on failure the "errno" will indicate the + * type of failure encountered, but not necessarily one that came from + * a syscall. We might have faked it up. */ #define RESOLVE_REF_READING 0x01 #define RESOLVE_REF_NO_RECURSE 0x02 @@ -67,7 +72,8 @@ const char *refs_resolve_ref_unsafe(struct ref_store *refs, const char *refname, int resolve_flags, struct object_id *oid, - int *flags); + int *flags, int *failure_errno); + const char *resolve_ref_unsafe(const char *refname, int resolve_flags, struct object_id *oid, int *flags); @@ -77,8 +83,6 @@ char *refs_resolve_refdup(struct ref_store *refs, char *resolve_refdup(const char *refname, int resolve_flags, struct object_id *oid, int *flags); -int refs_read_ref_full(struct ref_store *refs, const char *refname, - int resolve_flags, struct object_id *oid, int *flags); int read_ref_full(const char *refname, int resolve_flags, struct object_id *oid, int *flags); int read_ref(const char *refname, struct object_id *oid); @@ -413,8 +417,8 @@ int refs_pack_refs(struct ref_store *refs, unsigned int flags); * Setup reflog before using. Fill in err and return -1 on failure. */ int refs_create_reflog(struct ref_store *refs, const char *refname, - int force_create, struct strbuf *err); -int safe_create_reflog(const char *refname, int force_create, struct strbuf *err); + struct strbuf *err); +int safe_create_reflog(const char *refname, struct strbuf *err); /** Reads log for the value of ref during at_time. **/ int read_ref_at(struct ref_store *refs, @@ -459,7 +463,29 @@ int delete_reflog(const char *refname); /* * Callback to process a reflog entry found by the iteration functions (see - * below) + * below). + * + * The committer parameter is a single string, in the form + * "$GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>" (without double quotes). + * + * The timestamp parameter gives the time when entry was created as the number + * of seconds since the UNIX epoch. + * + * The tz parameter gives the timezone offset for the user who created + * the reflog entry, and its value gives a positive or negative offset + * from UTC. Its absolute value is formed by multiplying the hour + * part by 100 and adding the minute part. For example, 1 hour ahead + * of UTC, CET == "+0100", is represented as positive one hundred (not + * postiive sixty). + * + * The msg parameter is a single complete line; a reflog message given + * to refs_delete_ref, refs_update_ref, etc. is returned to the + * callback normalized---each run of whitespaces are squashed into a + * single whitespace, trailing whitespace, if exists, is trimmed, and + * then a single LF is added at the end. + * + * The cb_data is a caller-supplied pointer given to the iterator + * functions. */ typedef int each_reflog_ent_fn( struct object_id *old_oid, struct object_id *new_oid, @@ -612,11 +638,23 @@ struct ref_transaction *ref_transaction_begin(struct strbuf *err); #define REF_FORCE_CREATE_REFLOG (1 << 1) /* + * Blindly write an object_id. This is useful for testing data corruption + * scenarios. + */ +#define REF_SKIP_OID_VERIFICATION (1 << 10) + +/* + * Skip verifying refname. This is useful for testing data corruption scenarios. + */ +#define REF_SKIP_REFNAME_VERIFICATION (1 << 11) + +/* * Bitmask of all of the flags that are allowed to be passed in to * ref_transaction_update() and friends: */ -#define REF_TRANSACTION_UPDATE_ALLOWED_FLAGS \ - (REF_NO_DEREF | REF_FORCE_CREATE_REFLOG) +#define REF_TRANSACTION_UPDATE_ALLOWED_FLAGS \ + (REF_NO_DEREF | REF_FORCE_CREATE_REFLOG | REF_SKIP_OID_VERIFICATION | \ + REF_SKIP_REFNAME_VERIFICATION) /* * Add a reference update to transaction. `new_oid` is the value that diff --git a/refs/debug.c b/refs/debug.c index 8667c64023..2b0771ca53 100644 --- a/refs/debug.c +++ b/refs/debug.c @@ -26,7 +26,8 @@ struct ref_store *maybe_debug_wrap_ref_store(const char *gitdir, struct ref_stor be_copy->name = store->be->name; trace_printf_key(&trace_refs, "ref_store for %s\n", gitdir); res->refs = store; - base_ref_store_init((struct ref_store *)res, be_copy); + base_ref_store_init((struct ref_store *)res, store->repo, gitdir, + be_copy); return (struct ref_store *)res; } @@ -47,7 +48,8 @@ static int debug_transaction_prepare(struct ref_store *refs, transaction->ref_store = drefs->refs; res = drefs->refs->be->transaction_prepare(drefs->refs, transaction, err); - trace_printf_key(&trace_refs, "transaction_prepare: %d\n", res); + trace_printf_key(&trace_refs, "transaction_prepare: %d \"%s\"\n", res, + err->buf); return res; } @@ -284,6 +286,7 @@ static int debug_print_reflog_ent(struct object_id *old_oid, int ret; char o[GIT_MAX_HEXSZ + 1] = "null"; char n[GIT_MAX_HEXSZ + 1] = "null"; + char *msgend = strchrnul(msg, '\n'); if (old_oid) oid_to_hex_r(o, old_oid); if (new_oid) @@ -291,8 +294,10 @@ static int debug_print_reflog_ent(struct object_id *old_oid, ret = dbg->fn(old_oid, new_oid, committer, timestamp, tz, msg, dbg->cb_data); - trace_printf_key(&trace_refs, "reflog_ent %s (ret %d): %s -> %s, %s %ld \"%s\"\n", - dbg->refname, ret, o, n, committer, (long int)timestamp, msg); + trace_printf_key(&trace_refs, + "reflog_ent %s (ret %d): %s -> %s, %s %ld \"%.*s\"\n", + dbg->refname, ret, o, n, committer, + (long int)timestamp, (int)(msgend - msg), msg); return ret; } @@ -339,11 +344,10 @@ static int debug_reflog_exists(struct ref_store *ref_store, const char *refname) } static int debug_create_reflog(struct ref_store *ref_store, const char *refname, - int force_create, struct strbuf *err) + struct strbuf *err) { struct debug_ref_store *drefs = (struct debug_ref_store *)ref_store; - int res = drefs->refs->be->create_reflog(drefs->refs, refname, - force_create, err); + int res = drefs->refs->be->create_reflog(drefs->refs, refname, err); trace_printf_key(&trace_refs, "create_reflog: %s: %d\n", refname, res); return res; } diff --git a/refs/files-backend.c b/refs/files-backend.c index 151b0056fe..5c6d49a267 100644 --- a/refs/files-backend.c +++ b/refs/files-backend.c @@ -16,8 +16,7 @@ * This backend uses the following flags in `ref_update::flags` for * internal bookkeeping purposes. Their numerical values must not * conflict with REF_NO_DEREF, REF_FORCE_CREATE_REFLOG, REF_HAVE_NEW, - * REF_HAVE_OLD, or REF_IS_PRUNING, which are also stored in - * `ref_update::flags`. + * or REF_HAVE_OLD, which are also stored in `ref_update::flags`. */ /* @@ -87,16 +86,12 @@ static struct ref_store *files_ref_store_create(struct repository *repo, struct ref_store *ref_store = (struct ref_store *)refs; struct strbuf sb = STRBUF_INIT; - ref_store->repo = repo; - ref_store->gitdir = xstrdup(gitdir); - base_ref_store_init(ref_store, &refs_be_files); + base_ref_store_init(ref_store, repo, gitdir, &refs_be_files); refs->store_flags = flags; - get_common_dir_noenv(&sb, gitdir); refs->gitcommondir = strbuf_detach(&sb, NULL); - strbuf_addf(&sb, "%s/packed-refs", refs->gitcommondir); - refs->packed_ref_store = packed_ref_store_create(repo, sb.buf, flags); - strbuf_release(&sb); + refs->packed_ref_store = + packed_ref_store_create(repo, refs->gitcommondir, flags); chdir_notify_reparent("files-backend $GIT_DIR", &refs->base.gitdir); chdir_notify_reparent("files-backend $GIT_COMMONDIR", @@ -282,10 +277,11 @@ static void loose_fill_ref_dir(struct ref_store *ref_store, create_dir_entry(dir->cache, refname.buf, refname.len)); } else { + int ignore_errno; if (!refs_resolve_ref_unsafe(&refs->base, refname.buf, RESOLVE_REF_READING, - &oid, &flag)) { + &oid, &flag, &ignore_errno)) { oidclr(&oid); flag |= REF_ISBROKEN; } else if (is_null_oid(&oid)) { @@ -357,6 +353,7 @@ static int files_read_raw_ref(struct ref_store *ref_store, const char *refname, int fd; int ret = -1; int remaining_retries = 3; + int myerr = 0; *type = 0; strbuf_reset(&sb_path); @@ -383,11 +380,14 @@ stat_ref: goto out; if (lstat(path, &st) < 0) { - if (errno != ENOENT) + int ignore_errno; + myerr = errno; + errno = 0; + if (myerr != ENOENT) goto out; - if (refs_read_raw_ref(refs->packed_ref_store, refname, - oid, referent, type)) { - errno = ENOENT; + if (refs_read_raw_ref(refs->packed_ref_store, refname, oid, + referent, type, &ignore_errno)) { + myerr = ENOENT; goto out; } ret = 0; @@ -398,7 +398,9 @@ stat_ref: if (S_ISLNK(st.st_mode)) { strbuf_reset(&sb_contents); if (strbuf_readlink(&sb_contents, path, st.st_size) < 0) { - if (errno == ENOENT || errno == EINVAL) + myerr = errno; + errno = 0; + if (myerr == ENOENT || myerr == EINVAL) /* inconsistent with lstat; retry */ goto stat_ref; else @@ -420,14 +422,15 @@ stat_ref: /* Is it a directory? */ if (S_ISDIR(st.st_mode)) { + int ignore_errno; /* * Even though there is a directory where the loose * ref is supposed to be, there could still be a * packed ref: */ - if (refs_read_raw_ref(refs->packed_ref_store, refname, - oid, referent, type)) { - errno = EISDIR; + if (refs_read_raw_ref(refs->packed_ref_store, refname, oid, + referent, type, &ignore_errno)) { + myerr = EISDIR; goto out; } ret = 0; @@ -440,7 +443,8 @@ stat_ref: */ fd = open(path, O_RDONLY); if (fd < 0) { - if (errno == ENOENT && !S_ISLNK(st.st_mode)) + myerr = errno; + if (myerr == ENOENT && !S_ISLNK(st.st_mode)) /* inconsistent with lstat; retry */ goto stat_ref; else @@ -448,26 +452,29 @@ stat_ref: } strbuf_reset(&sb_contents); if (strbuf_read(&sb_contents, fd, 256) < 0) { - int save_errno = errno; + myerr = errno; close(fd); - errno = save_errno; goto out; } close(fd); strbuf_rtrim(&sb_contents); buf = sb_contents.buf; - ret = parse_loose_ref_contents(buf, oid, referent, type); + ret = parse_loose_ref_contents(buf, oid, referent, type, &myerr); out: - *failure_errno = errno; + if (ret && !myerr) + BUG("returning non-zero %d, should have set myerr!", ret); + *failure_errno = myerr; + strbuf_release(&sb_path); strbuf_release(&sb_contents); return ret; } int parse_loose_ref_contents(const char *buf, struct object_id *oid, - struct strbuf *referent, unsigned int *type) + struct strbuf *referent, unsigned int *type, + int *failure_errno) { const char *p; if (skip_prefix(buf, "ref:", &buf)) { @@ -486,7 +493,7 @@ int parse_loose_ref_contents(const char *buf, struct object_id *oid, if (parse_oid_hex(buf, oid, &p) || (*p != '\0' && !isspace(*p))) { *type |= REF_ISBROKEN; - errno = EINVAL; + *failure_errno = EINVAL; return -1; } return 0; @@ -995,11 +1002,12 @@ static int create_reflock(const char *path, void *cb) * Locks a ref returning the lock on success and NULL on failure. */ static struct ref_lock *lock_ref_oid_basic(struct files_ref_store *refs, - const char *refname, int *type, + const char *refname, struct strbuf *err) { struct strbuf ref_file = STRBUF_INIT; struct ref_lock *lock; + int ignore_errno; files_assert_main_repository(refs, "lock_ref_oid_basic"); assert(err); @@ -1007,16 +1015,6 @@ static struct ref_lock *lock_ref_oid_basic(struct files_ref_store *refs, CALLOC_ARRAY(lock, 1); files_ref_path(refs, &ref_file, refname); - if (!refs_resolve_ref_unsafe(&refs->base, refname, - RESOLVE_REF_NO_RECURSE, - &lock->old_oid, type)) { - if (!refs_verify_refname_available(&refs->base, refname, - NULL, NULL, err)) - strbuf_addf(err, "unable to resolve reference '%s': %s", - refname, strerror(errno)); - - goto error_return; - } /* * If the ref did not exist and we are creating it, make sure @@ -1036,9 +1034,8 @@ static struct ref_lock *lock_ref_oid_basic(struct files_ref_store *refs, goto error_return; } - if (refs_read_ref_full(&refs->base, lock->ref_name, - 0, - &lock->old_oid, NULL)) + if (!refs_resolve_ref_unsafe(&refs->base, lock->ref_name, 0, + &lock->old_oid, NULL, &ignore_errno)) oidclr(&lock->old_oid); goto out; @@ -1352,12 +1349,42 @@ static int rename_tmp_log(struct files_ref_store *refs, const char *newrefname) } static int write_ref_to_lockfile(struct ref_lock *lock, - const struct object_id *oid, struct strbuf *err); + const struct object_id *oid, + int skip_oid_verification, struct strbuf *err); static int commit_ref_update(struct files_ref_store *refs, struct ref_lock *lock, const struct object_id *oid, const char *logmsg, struct strbuf *err); +/* + * Emit a better error message than lockfile.c's + * unable_to_lock_message() would in case there is a D/F conflict with + * another existing reference. If there would be a conflict, emit an error + * message and return false; otherwise, return true. + * + * Note that this function is not safe against all races with other + * processes, and that's not its job. We'll emit a more verbose error on D/f + * conflicts if we get past it into lock_ref_oid_basic(). + */ +static int refs_rename_ref_available(struct ref_store *refs, + const char *old_refname, + const char *new_refname) +{ + struct string_list skip = STRING_LIST_INIT_NODUP; + struct strbuf err = STRBUF_INIT; + int ok; + + string_list_insert(&skip, old_refname); + ok = !refs_verify_refname_available(refs, new_refname, + NULL, &skip, &err); + if (!ok) + error("%s", err.buf); + + string_list_clear(&skip, 0); + strbuf_release(&err); + return ok; +} + static int files_copy_or_rename_ref(struct ref_store *ref_store, const char *oldrefname, const char *newrefname, const char *logmsg, int copy) @@ -1373,6 +1400,7 @@ static int files_copy_or_rename_ref(struct ref_store *ref_store, struct strbuf tmp_renamed_log = STRBUF_INIT; int log, ret; struct strbuf err = STRBUF_INIT; + int ignore_errno; files_reflog_path(refs, &sb_oldref, oldrefname); files_reflog_path(refs, &sb_newref, newrefname); @@ -1386,7 +1414,7 @@ static int files_copy_or_rename_ref(struct ref_store *ref_store, if (!refs_resolve_ref_unsafe(&refs->base, oldrefname, RESOLVE_REF_READING | RESOLVE_REF_NO_RECURSE, - &orig_oid, &flag)) { + &orig_oid, &flag, &ignore_errno)) { ret = error("refname %s not found", oldrefname); goto out; } @@ -1430,9 +1458,9 @@ static int files_copy_or_rename_ref(struct ref_store *ref_store, * the safety anyway; we want to delete the reference whatever * its current value. */ - if (!copy && !refs_read_ref_full(&refs->base, newrefname, - RESOLVE_REF_READING | RESOLVE_REF_NO_RECURSE, - NULL, NULL) && + if (!copy && refs_resolve_ref_unsafe(&refs->base, newrefname, + RESOLVE_REF_READING | RESOLVE_REF_NO_RECURSE, + NULL, NULL, &ignore_errno) && refs_delete_ref(&refs->base, NULL, newrefname, NULL, REF_NO_DEREF)) { if (errno == EISDIR) { @@ -1458,7 +1486,7 @@ static int files_copy_or_rename_ref(struct ref_store *ref_store, logmoved = log; - lock = lock_ref_oid_basic(refs, newrefname, NULL, &err); + lock = lock_ref_oid_basic(refs, newrefname, &err); if (!lock) { if (copy) error("unable to copy '%s' to '%s': %s", oldrefname, newrefname, err.buf); @@ -1469,7 +1497,7 @@ static int files_copy_or_rename_ref(struct ref_store *ref_store, } oidcpy(&lock->old_oid, &orig_oid); - if (write_ref_to_lockfile(lock, &orig_oid, &err) || + if (write_ref_to_lockfile(lock, &orig_oid, 0, &err) || commit_ref_update(refs, lock, &orig_oid, logmsg, &err)) { error("unable to write current sha1 into %s: %s", newrefname, err.buf); strbuf_release(&err); @@ -1480,7 +1508,7 @@ static int files_copy_or_rename_ref(struct ref_store *ref_store, goto out; rollback: - lock = lock_ref_oid_basic(refs, oldrefname, NULL, &err); + lock = lock_ref_oid_basic(refs, oldrefname, &err); if (!lock) { error("unable to lock %s for rollback: %s", oldrefname, err.buf); strbuf_release(&err); @@ -1489,7 +1517,7 @@ static int files_copy_or_rename_ref(struct ref_store *ref_store, flag = log_all_ref_updates; log_all_ref_updates = LOG_REFS_NONE; - if (write_ref_to_lockfile(lock, &orig_oid, &err) || + if (write_ref_to_lockfile(lock, &orig_oid, 0, &err) || commit_ref_update(refs, lock, &orig_oid, NULL, &err)) { error("unable to write current sha1 into %s: %s", oldrefname, err.buf); strbuf_release(&err); @@ -1639,15 +1667,14 @@ error: return -1; } -static int files_create_reflog(struct ref_store *ref_store, - const char *refname, int force_create, +static int files_create_reflog(struct ref_store *ref_store, const char *refname, struct strbuf *err) { struct files_ref_store *refs = files_downcast(ref_store, REF_STORE_WRITE, "create_reflog"); int fd; - if (log_ref_setup(refs, refname, force_create, &fd, err)) + if (log_ref_setup(refs, refname, 1, &fd, err)) return -1; if (fd >= 0) @@ -1725,26 +1752,31 @@ static int files_log_ref_write(struct files_ref_store *refs, * errors, rollback the lockfile, fill in *err and return -1. */ static int write_ref_to_lockfile(struct ref_lock *lock, - const struct object_id *oid, struct strbuf *err) + const struct object_id *oid, + int skip_oid_verification, struct strbuf *err) { static char term = '\n'; struct object *o; int fd; - o = parse_object(the_repository, oid); - if (!o) { - strbuf_addf(err, - "trying to write ref '%s' with nonexistent object %s", - lock->ref_name, oid_to_hex(oid)); - unlock_ref(lock); - return -1; - } - if (o->type != OBJ_COMMIT && is_branch(lock->ref_name)) { - strbuf_addf(err, - "trying to write non-commit object %s to branch '%s'", - oid_to_hex(oid), lock->ref_name); - unlock_ref(lock); - return -1; + if (!skip_oid_verification) { + o = parse_object(the_repository, oid); + if (!o) { + strbuf_addf( + err, + "trying to write ref '%s' with nonexistent object %s", + lock->ref_name, oid_to_hex(oid)); + unlock_ref(lock); + return -1; + } + if (o->type != OBJ_COMMIT && is_branch(lock->ref_name)) { + strbuf_addf( + err, + "trying to write non-commit object %s to branch '%s'", + oid_to_hex(oid), lock->ref_name); + unlock_ref(lock); + return -1; + } } fd = get_lock_file_fd(&lock->lk); if (write_in_full(fd, oid_to_hex(oid), the_hash_algo->hexsz) < 0 || @@ -1797,10 +1829,12 @@ static int commit_ref_update(struct files_ref_store *refs, */ int head_flag; const char *head_ref; + int ignore_errno; head_ref = refs_resolve_ref_unsafe(&refs->base, "HEAD", RESOLVE_REF_READING, - NULL, &head_flag); + NULL, &head_flag, + &ignore_errno); if (head_ref && (head_flag & REF_ISSYMREF) && !strcmp(head_ref, lock->ref_name)) { struct strbuf log_err = STRBUF_INIT; @@ -1844,9 +1878,12 @@ static void update_symref_reflog(struct files_ref_store *refs, { struct strbuf err = STRBUF_INIT; struct object_id new_oid; + int ignore_errno; + if (logmsg && - !refs_read_ref_full(&refs->base, target, - RESOLVE_REF_READING, &new_oid, NULL) && + refs_resolve_ref_unsafe(&refs->base, target, + RESOLVE_REF_READING, &new_oid, NULL, + &ignore_errno) && files_log_ref_write(refs, refname, &lock->old_oid, &new_oid, logmsg, 0, &err)) { error("%s", err.buf); @@ -1887,7 +1924,7 @@ static int files_create_symref(struct ref_store *ref_store, struct ref_lock *lock; int ret; - lock = lock_ref_oid_basic(refs, refname, NULL, &err); + lock = lock_ref_oid_basic(refs, refname, &err); if (!lock) { error("%s", err.buf); strbuf_release(&err); @@ -2120,6 +2157,7 @@ static int files_reflog_iterator_advance(struct ref_iterator *ref_iterator) (struct files_reflog_iterator *)ref_iterator; struct dir_iterator *diter = iter->dir_iterator; int ok; + int ignore_errno; while ((ok = dir_iterator_advance(diter)) == ITER_OK) { int flags; @@ -2131,9 +2169,10 @@ static int files_reflog_iterator_advance(struct ref_iterator *ref_iterator) if (ends_with(diter->basename, ".lock")) continue; - if (refs_read_ref_full(iter->ref_store, - diter->relative_path, 0, - &iter->oid, &flags)) { + if (!refs_resolve_ref_unsafe(iter->ref_store, + diter->relative_path, 0, + &iter->oid, &flags, + &ignore_errno)) { error("bad ref for %s", diter->path.buf); continue; } @@ -2151,7 +2190,7 @@ static int files_reflog_iterator_advance(struct ref_iterator *ref_iterator) } static int files_reflog_iterator_peel(struct ref_iterator *ref_iterator, - struct object_id *peeled) + struct object_id *peeled) { BUG("ref_iterator_peel() called for reflog_iterator"); } @@ -2477,9 +2516,11 @@ static int lock_ref_for_update(struct files_ref_store *refs, * the transaction, so we have to read it here * to record and possibly check old_oid: */ - if (refs_read_ref_full(&refs->base, - referent.buf, 0, - &lock->old_oid, NULL)) { + int ignore_errno; + if (!refs_resolve_ref_unsafe(&refs->base, + referent.buf, 0, + &lock->old_oid, NULL, + &ignore_errno)) { if (update->flags & REF_HAVE_OLD) { strbuf_addf(err, "cannot lock ref '%s': " "error reading reference", @@ -2535,8 +2576,10 @@ static int lock_ref_for_update(struct files_ref_store *refs, * The reference already has the desired * value, so we don't need to write it. */ - } else if (write_ref_to_lockfile(lock, &update->new_oid, - err)) { + } else if (write_ref_to_lockfile( + lock, &update->new_oid, + update->flags & REF_SKIP_OID_VERIFICATION, + err)) { char *write_err = strbuf_detach(err, NULL); /* @@ -3091,7 +3134,6 @@ static int files_reflog_expire(struct ref_store *ref_store, struct strbuf log_file_sb = STRBUF_INIT; char *log_file; int status = 0; - int type; struct strbuf err = STRBUF_INIT; const struct object_id *oid; @@ -3105,7 +3147,7 @@ static int files_reflog_expire(struct ref_store *ref_store, * reference itself, plus we might need to update the * reference if --updateref was specified: */ - lock = lock_ref_oid_basic(refs, refname, &type, &err); + lock = lock_ref_oid_basic(refs, refname, &err); if (!lock) { error("cannot lock ref '%s': %s", refname, err.buf); strbuf_release(&err); @@ -3167,9 +3209,20 @@ static int files_reflog_expire(struct ref_store *ref_store, * a reference if there are no remaining reflog * entries. */ - int update = (flags & EXPIRE_REFLOGS_UPDATE_REF) && - !(type & REF_ISSYMREF) && - !is_null_oid(&cb.last_kept_oid); + int update = 0; + + if ((flags & EXPIRE_REFLOGS_UPDATE_REF) && + !is_null_oid(&cb.last_kept_oid)) { + int ignore_errno; + int type; + const char *ref; + + ref = refs_resolve_ref_unsafe(&refs->base, refname, + RESOLVE_REF_NO_RECURSE, + NULL, &type, + &ignore_errno); + update = !!(ref && !(type & REF_ISSYMREF)); + } if (close_lock_file_gently(&reflog_lock)) { status |= error("couldn't write %s: %s", log_file, diff --git a/refs/packed-backend.c b/refs/packed-backend.c index 1c5211b03e..d91a2018f6 100644 --- a/refs/packed-backend.c +++ b/refs/packed-backend.c @@ -194,20 +194,19 @@ static int release_snapshot(struct snapshot *snapshot) } struct ref_store *packed_ref_store_create(struct repository *repo, - const char *path, + const char *gitdir, unsigned int store_flags) { struct packed_ref_store *refs = xcalloc(1, sizeof(*refs)); struct ref_store *ref_store = (struct ref_store *)refs; + struct strbuf sb = STRBUF_INIT; - base_ref_store_init(ref_store, &refs_be_packed); - ref_store->repo = repo; - ref_store->gitdir = xstrdup(path); + base_ref_store_init(ref_store, repo, gitdir, &refs_be_packed); refs->store_flags = store_flags; - refs->path = xstrdup(path); + strbuf_addf(&sb, "%s/packed-refs", gitdir); + refs->path = strbuf_detach(&sb, NULL); chdir_notify_reparent("packed-refs", &refs->path); - return ref_store; } @@ -1354,6 +1353,7 @@ int is_packed_transaction_needed(struct ref_store *ref_store, ret = 0; for (i = 0; i < transaction->nr; i++) { struct ref_update *update = transaction->updates[i]; + int failure_errno; unsigned int type; struct object_id oid; @@ -1364,9 +1364,9 @@ int is_packed_transaction_needed(struct ref_store *ref_store, */ continue; - if (!refs_read_raw_ref(ref_store, update->refname, - &oid, &referent, &type) || - errno != ENOENT) { + if (!refs_read_raw_ref(ref_store, update->refname, &oid, + &referent, &type, &failure_errno) || + failure_errno != ENOENT) { /* * We have to actually delete that reference * -> this transaction is needed. @@ -1628,8 +1628,7 @@ static int packed_reflog_exists(struct ref_store *ref_store, } static int packed_create_reflog(struct ref_store *ref_store, - const char *refname, int force_create, - struct strbuf *err) + const char *refname, struct strbuf *err) { BUG("packed reference store does not support reflogs"); } diff --git a/refs/packed-backend.h b/refs/packed-backend.h index f61a73ec25..9dd8a344c3 100644 --- a/refs/packed-backend.h +++ b/refs/packed-backend.h @@ -14,7 +14,7 @@ struct ref_transaction; */ struct ref_store *packed_ref_store_create(struct repository *repo, - const char *path, + const char *gitdir, unsigned int store_flags); /* diff --git a/refs/refs-internal.h b/refs/refs-internal.h index 12224742ed..7ff6fba4f0 100644 --- a/refs/refs-internal.h +++ b/refs/refs-internal.h @@ -150,9 +150,9 @@ struct ref_update { const char refname[FLEX_ARRAY]; }; -int refs_read_raw_ref(struct ref_store *ref_store, - const char *refname, struct object_id *oid, - struct strbuf *referent, unsigned int *type); +int refs_read_raw_ref(struct ref_store *ref_store, const char *refname, + struct object_id *oid, struct strbuf *referent, + unsigned int *type, int *failure_errno); /* * Write an error to `err` and return a nonzero value iff the same @@ -229,20 +229,6 @@ const char *find_descendant_ref(const char *dirname, const struct string_list *extras, const struct string_list *skip); -/* - * Check whether an attempt to rename old_refname to new_refname would - * cause a D/F conflict with any existing reference (other than - * possibly old_refname). If there would be a conflict, emit an error - * message and return false; otherwise, return true. - * - * Note that this function is not safe against all races with other - * processes (though rename_ref() catches some races that might get by - * this check). - */ -int refs_rename_ref_available(struct ref_store *refs, - const char *old_refname, - const char *new_refname); - /* We allow "recursive" symbolic refs. Only within reason, though */ #define SYMREF_MAXDEPTH 5 @@ -606,7 +592,7 @@ typedef int for_each_reflog_ent_reverse_fn(struct ref_store *ref_store, void *cb_data); typedef int reflog_exists_fn(struct ref_store *ref_store, const char *refname); typedef int create_reflog_fn(struct ref_store *ref_store, const char *refname, - int force_create, struct strbuf *err); + struct strbuf *err); typedef int delete_reflog_fn(struct ref_store *ref_store, const char *refname); typedef int reflog_expire_fn(struct ref_store *ref_store, const char *refname, @@ -713,17 +699,19 @@ struct ref_store { }; /* - * Parse contents of a loose ref file. + * Parse contents of a loose ref file. *failure_errno maybe be set to EINVAL for + * invalid contents. */ int parse_loose_ref_contents(const char *buf, struct object_id *oid, - struct strbuf *referent, unsigned int *type); + struct strbuf *referent, unsigned int *type, + int *failure_errno); /* * Fill in the generic part of refs and add it to our collection of * reference stores. */ -void base_ref_store_init(struct ref_store *refs, - const struct ref_storage_be *be); +void base_ref_store_init(struct ref_store *refs, struct repository *repo, + const char *path, const struct ref_storage_be *be); /* * Support GIT_TRACE_REFS by optionally wrapping the given ref_store instance. diff --git a/reftable/LICENSE b/reftable/LICENSE new file mode 100644 index 0000000000..402e0f9356 --- /dev/null +++ b/reftable/LICENSE @@ -0,0 +1,31 @@ +BSD License + +Copyright (c) 2020, Google LLC +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +* Neither the name of Google LLC nor the names of its contributors may +be used to endorse or promote products derived from this software +without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/reftable/basics.c b/reftable/basics.c new file mode 100644 index 0000000000..f761e48028 --- /dev/null +++ b/reftable/basics.c @@ -0,0 +1,128 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#include "basics.h" + +void put_be24(uint8_t *out, uint32_t i) +{ + out[0] = (uint8_t)((i >> 16) & 0xff); + out[1] = (uint8_t)((i >> 8) & 0xff); + out[2] = (uint8_t)(i & 0xff); +} + +uint32_t get_be24(uint8_t *in) +{ + return (uint32_t)(in[0]) << 16 | (uint32_t)(in[1]) << 8 | + (uint32_t)(in[2]); +} + +void put_be16(uint8_t *out, uint16_t i) +{ + out[0] = (uint8_t)((i >> 8) & 0xff); + out[1] = (uint8_t)(i & 0xff); +} + +int binsearch(size_t sz, int (*f)(size_t k, void *args), void *args) +{ + size_t lo = 0; + size_t hi = sz; + + /* Invariants: + * + * (hi == sz) || f(hi) == true + * (lo == 0 && f(0) == true) || fi(lo) == false + */ + while (hi - lo > 1) { + size_t mid = lo + (hi - lo) / 2; + + if (f(mid, args)) + hi = mid; + else + lo = mid; + } + + if (lo) + return hi; + + return f(0, args) ? 0 : 1; +} + +void free_names(char **a) +{ + char **p; + if (!a) { + return; + } + for (p = a; *p; p++) { + reftable_free(*p); + } + reftable_free(a); +} + +int names_length(char **names) +{ + char **p = names; + for (; *p; p++) { + /* empty */ + } + return p - names; +} + +void parse_names(char *buf, int size, char ***namesp) +{ + char **names = NULL; + size_t names_cap = 0; + size_t names_len = 0; + + char *p = buf; + char *end = buf + size; + while (p < end) { + char *next = strchr(p, '\n'); + if (next && next < end) { + *next = 0; + } else { + next = end; + } + if (p < next) { + if (names_len == names_cap) { + names_cap = 2 * names_cap + 1; + names = reftable_realloc( + names, names_cap * sizeof(*names)); + } + names[names_len++] = xstrdup(p); + } + p = next + 1; + } + + names = reftable_realloc(names, (names_len + 1) * sizeof(*names)); + names[names_len] = NULL; + *namesp = names; +} + +int names_equal(char **a, char **b) +{ + int i = 0; + for (; a[i] && b[i]; i++) { + if (strcmp(a[i], b[i])) { + return 0; + } + } + + return a[i] == b[i]; +} + +int common_prefix_size(struct strbuf *a, struct strbuf *b) +{ + int p = 0; + for (; p < a->len && p < b->len; p++) { + if (a->buf[p] != b->buf[p]) + break; + } + + return p; +} diff --git a/reftable/basics.h b/reftable/basics.h new file mode 100644 index 0000000000..096b36862b --- /dev/null +++ b/reftable/basics.h @@ -0,0 +1,60 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#ifndef BASICS_H +#define BASICS_H + +/* + * miscellaneous utilities that are not provided by Git. + */ + +#include "system.h" + +/* Bigendian en/decoding of integers */ + +void put_be24(uint8_t *out, uint32_t i); +uint32_t get_be24(uint8_t *in); +void put_be16(uint8_t *out, uint16_t i); + +/* + * find smallest index i in [0, sz) at which f(i) is true, assuming + * that f is ascending. Return sz if f(i) is false for all indices. + * + * Contrary to bsearch(3), this returns something useful if the argument is not + * found. + */ +int binsearch(size_t sz, int (*f)(size_t k, void *args), void *args); + +/* + * Frees a NULL terminated array of malloced strings. The array itself is also + * freed. + */ +void free_names(char **a); + +/* parse a newline separated list of names. `size` is the length of the buffer, + * without terminating '\0'. Empty names are discarded. */ +void parse_names(char *buf, int size, char ***namesp); + +/* compares two NULL-terminated arrays of strings. */ +int names_equal(char **a, char **b); + +/* returns the array size of a NULL-terminated array of strings. */ +int names_length(char **names); + +/* Allocation routines; they invoke the functions set through + * reftable_set_alloc() */ +void *reftable_malloc(size_t sz); +void *reftable_realloc(void *p, size_t sz); +void reftable_free(void *p); +void *reftable_calloc(size_t sz); + +/* Find the longest shared prefix size of `a` and `b` */ +struct strbuf; +int common_prefix_size(struct strbuf *a, struct strbuf *b); + +#endif diff --git a/reftable/basics_test.c b/reftable/basics_test.c new file mode 100644 index 0000000000..1fcd229725 --- /dev/null +++ b/reftable/basics_test.c @@ -0,0 +1,98 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#include "system.h" + +#include "basics.h" +#include "test_framework.h" +#include "reftable-tests.h" + +struct binsearch_args { + int key; + int *arr; +}; + +static int binsearch_func(size_t i, void *void_args) +{ + struct binsearch_args *args = void_args; + + return args->key < args->arr[i]; +} + +static void test_binsearch(void) +{ + int arr[] = { 2, 4, 6, 8, 10 }; + size_t sz = ARRAY_SIZE(arr); + struct binsearch_args args = { + .arr = arr, + }; + + int i = 0; + for (i = 1; i < 11; i++) { + int res; + args.key = i; + res = binsearch(sz, &binsearch_func, &args); + + if (res < sz) { + EXPECT(args.key < arr[res]); + if (res > 0) { + EXPECT(args.key >= arr[res - 1]); + } + } else { + EXPECT(args.key == 10 || args.key == 11); + } + } +} + +static void test_names_length(void) +{ + char *a[] = { "a", "b", NULL }; + EXPECT(names_length(a) == 2); +} + +static void test_parse_names_normal(void) +{ + char in[] = "a\nb\n"; + char **out = NULL; + parse_names(in, strlen(in), &out); + EXPECT(!strcmp(out[0], "a")); + EXPECT(!strcmp(out[1], "b")); + EXPECT(!out[2]); + free_names(out); +} + +static void test_parse_names_drop_empty(void) +{ + char in[] = "a\n\n"; + char **out = NULL; + parse_names(in, strlen(in), &out); + EXPECT(!strcmp(out[0], "a")); + EXPECT(!out[1]); + free_names(out); +} + +static void test_common_prefix(void) +{ + struct strbuf s1 = STRBUF_INIT; + struct strbuf s2 = STRBUF_INIT; + strbuf_addstr(&s1, "abcdef"); + strbuf_addstr(&s2, "abc"); + EXPECT(common_prefix_size(&s1, &s2) == 3); + strbuf_release(&s1); + strbuf_release(&s2); +} + +int basics_test_main(int argc, const char *argv[]) +{ + RUN_TEST(test_common_prefix); + RUN_TEST(test_parse_names_normal); + RUN_TEST(test_parse_names_drop_empty); + RUN_TEST(test_binsearch); + RUN_TEST(test_names_length); + return 0; +} diff --git a/reftable/block.c b/reftable/block.c new file mode 100644 index 0000000000..855e3f5c94 --- /dev/null +++ b/reftable/block.c @@ -0,0 +1,437 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#include "block.h" + +#include "blocksource.h" +#include "constants.h" +#include "record.h" +#include "reftable-error.h" +#include "system.h" +#include <zlib.h> + +int header_size(int version) +{ + switch (version) { + case 1: + return 24; + case 2: + return 28; + } + abort(); +} + +int footer_size(int version) +{ + switch (version) { + case 1: + return 68; + case 2: + return 72; + } + abort(); +} + +static int block_writer_register_restart(struct block_writer *w, int n, + int is_restart, struct strbuf *key) +{ + int rlen = w->restart_len; + if (rlen >= MAX_RESTARTS) { + is_restart = 0; + } + + if (is_restart) { + rlen++; + } + if (2 + 3 * rlen + n > w->block_size - w->next) + return -1; + if (is_restart) { + if (w->restart_len == w->restart_cap) { + w->restart_cap = w->restart_cap * 2 + 1; + w->restarts = reftable_realloc( + w->restarts, sizeof(uint32_t) * w->restart_cap); + } + + w->restarts[w->restart_len++] = w->next; + } + + w->next += n; + + strbuf_reset(&w->last_key); + strbuf_addbuf(&w->last_key, key); + w->entries++; + return 0; +} + +void block_writer_init(struct block_writer *bw, uint8_t typ, uint8_t *buf, + uint32_t block_size, uint32_t header_off, int hash_size) +{ + bw->buf = buf; + bw->hash_size = hash_size; + bw->block_size = block_size; + bw->header_off = header_off; + bw->buf[header_off] = typ; + bw->next = header_off + 4; + bw->restart_interval = 16; + bw->entries = 0; + bw->restart_len = 0; + bw->last_key.len = 0; +} + +uint8_t block_writer_type(struct block_writer *bw) +{ + return bw->buf[bw->header_off]; +} + +/* adds the reftable_record to the block. Returns -1 if it does not fit, 0 on + success */ +int block_writer_add(struct block_writer *w, struct reftable_record *rec) +{ + struct strbuf empty = STRBUF_INIT; + struct strbuf last = + w->entries % w->restart_interval == 0 ? empty : w->last_key; + struct string_view out = { + .buf = w->buf + w->next, + .len = w->block_size - w->next, + }; + + struct string_view start = out; + + int is_restart = 0; + struct strbuf key = STRBUF_INIT; + int n = 0; + + reftable_record_key(rec, &key); + n = reftable_encode_key(&is_restart, out, last, key, + reftable_record_val_type(rec)); + if (n < 0) + goto done; + string_view_consume(&out, n); + + n = reftable_record_encode(rec, out, w->hash_size); + if (n < 0) + goto done; + string_view_consume(&out, n); + + if (block_writer_register_restart(w, start.len - out.len, is_restart, + &key) < 0) + goto done; + + strbuf_release(&key); + return 0; + +done: + strbuf_release(&key); + return -1; +} + +int block_writer_finish(struct block_writer *w) +{ + int i; + for (i = 0; i < w->restart_len; i++) { + put_be24(w->buf + w->next, w->restarts[i]); + w->next += 3; + } + + put_be16(w->buf + w->next, w->restart_len); + w->next += 2; + put_be24(w->buf + 1 + w->header_off, w->next); + + if (block_writer_type(w) == BLOCK_TYPE_LOG) { + int block_header_skip = 4 + w->header_off; + uLongf src_len = w->next - block_header_skip; + uLongf dest_cap = src_len * 1.001 + 12; + + uint8_t *compressed = reftable_malloc(dest_cap); + while (1) { + uLongf out_dest_len = dest_cap; + int zresult = compress2(compressed, &out_dest_len, + w->buf + block_header_skip, + src_len, 9); + if (zresult == Z_BUF_ERROR && dest_cap < LONG_MAX) { + dest_cap *= 2; + compressed = + reftable_realloc(compressed, dest_cap); + if (compressed) + continue; + } + + if (Z_OK != zresult) { + reftable_free(compressed); + return REFTABLE_ZLIB_ERROR; + } + + memcpy(w->buf + block_header_skip, compressed, + out_dest_len); + w->next = out_dest_len + block_header_skip; + reftable_free(compressed); + break; + } + } + return w->next; +} + +uint8_t block_reader_type(struct block_reader *r) +{ + return r->block.data[r->header_off]; +} + +int block_reader_init(struct block_reader *br, struct reftable_block *block, + uint32_t header_off, uint32_t table_block_size, + int hash_size) +{ + uint32_t full_block_size = table_block_size; + uint8_t typ = block->data[header_off]; + uint32_t sz = get_be24(block->data + header_off + 1); + + uint16_t restart_count = 0; + uint32_t restart_start = 0; + uint8_t *restart_bytes = NULL; + + if (!reftable_is_block_type(typ)) + return REFTABLE_FORMAT_ERROR; + + if (typ == BLOCK_TYPE_LOG) { + int block_header_skip = 4 + header_off; + uLongf dst_len = sz - block_header_skip; /* total size of dest + buffer. */ + uLongf src_len = block->len - block_header_skip; + /* Log blocks specify the *uncompressed* size in their header. + */ + uint8_t *uncompressed = reftable_malloc(sz); + + /* Copy over the block header verbatim. It's not compressed. */ + memcpy(uncompressed, block->data, block_header_skip); + + /* Uncompress */ + if (Z_OK != + uncompress2(uncompressed + block_header_skip, &dst_len, + block->data + block_header_skip, &src_len)) { + reftable_free(uncompressed); + return REFTABLE_ZLIB_ERROR; + } + + if (dst_len + block_header_skip != sz) + return REFTABLE_FORMAT_ERROR; + + /* We're done with the input data. */ + reftable_block_done(block); + block->data = uncompressed; + block->len = sz; + block->source = malloc_block_source(); + full_block_size = src_len + block_header_skip; + } else if (full_block_size == 0) { + full_block_size = sz; + } else if (sz < full_block_size && sz < block->len && + block->data[sz] != 0) { + /* If the block is smaller than the full block size, it is + padded (data followed by '\0') or the next block is + unaligned. */ + full_block_size = sz; + } + + restart_count = get_be16(block->data + sz - 2); + restart_start = sz - 2 - 3 * restart_count; + restart_bytes = block->data + restart_start; + + /* transfer ownership. */ + br->block = *block; + block->data = NULL; + block->len = 0; + + br->hash_size = hash_size; + br->block_len = restart_start; + br->full_block_size = full_block_size; + br->header_off = header_off; + br->restart_count = restart_count; + br->restart_bytes = restart_bytes; + + return 0; +} + +static uint32_t block_reader_restart_offset(struct block_reader *br, int i) +{ + return get_be24(br->restart_bytes + 3 * i); +} + +void block_reader_start(struct block_reader *br, struct block_iter *it) +{ + it->br = br; + strbuf_reset(&it->last_key); + it->next_off = br->header_off + 4; +} + +struct restart_find_args { + int error; + struct strbuf key; + struct block_reader *r; +}; + +static int restart_key_less(size_t idx, void *args) +{ + struct restart_find_args *a = args; + uint32_t off = block_reader_restart_offset(a->r, idx); + struct string_view in = { + .buf = a->r->block.data + off, + .len = a->r->block_len - off, + }; + + /* the restart key is verbatim in the block, so this could avoid the + alloc for decoding the key */ + struct strbuf rkey = STRBUF_INIT; + struct strbuf last_key = STRBUF_INIT; + uint8_t unused_extra; + int n = reftable_decode_key(&rkey, &unused_extra, last_key, in); + int result; + if (n < 0) { + a->error = 1; + return -1; + } + + result = strbuf_cmp(&a->key, &rkey); + strbuf_release(&rkey); + return result; +} + +void block_iter_copy_from(struct block_iter *dest, struct block_iter *src) +{ + dest->br = src->br; + dest->next_off = src->next_off; + strbuf_reset(&dest->last_key); + strbuf_addbuf(&dest->last_key, &src->last_key); +} + +int block_iter_next(struct block_iter *it, struct reftable_record *rec) +{ + struct string_view in = { + .buf = it->br->block.data + it->next_off, + .len = it->br->block_len - it->next_off, + }; + struct string_view start = in; + struct strbuf key = STRBUF_INIT; + uint8_t extra = 0; + int n = 0; + + if (it->next_off >= it->br->block_len) + return 1; + + n = reftable_decode_key(&key, &extra, it->last_key, in); + if (n < 0) + return -1; + + string_view_consume(&in, n); + n = reftable_record_decode(rec, key, extra, in, it->br->hash_size); + if (n < 0) + return -1; + string_view_consume(&in, n); + + strbuf_reset(&it->last_key); + strbuf_addbuf(&it->last_key, &key); + it->next_off += start.len - in.len; + strbuf_release(&key); + return 0; +} + +int block_reader_first_key(struct block_reader *br, struct strbuf *key) +{ + struct strbuf empty = STRBUF_INIT; + int off = br->header_off + 4; + struct string_view in = { + .buf = br->block.data + off, + .len = br->block_len - off, + }; + + uint8_t extra = 0; + int n = reftable_decode_key(key, &extra, empty, in); + if (n < 0) + return n; + + return 0; +} + +int block_iter_seek(struct block_iter *it, struct strbuf *want) +{ + return block_reader_seek(it->br, it, want); +} + +void block_iter_close(struct block_iter *it) +{ + strbuf_release(&it->last_key); +} + +int block_reader_seek(struct block_reader *br, struct block_iter *it, + struct strbuf *want) +{ + struct restart_find_args args = { + .key = *want, + .r = br, + }; + struct reftable_record rec = reftable_new_record(block_reader_type(br)); + struct strbuf key = STRBUF_INIT; + int err = 0; + struct block_iter next = { + .last_key = STRBUF_INIT, + }; + + int i = binsearch(br->restart_count, &restart_key_less, &args); + if (args.error) { + err = REFTABLE_FORMAT_ERROR; + goto done; + } + + it->br = br; + if (i > 0) { + i--; + it->next_off = block_reader_restart_offset(br, i); + } else { + it->next_off = br->header_off + 4; + } + + /* We're looking for the last entry less/equal than the wanted key, so + we have to go one entry too far and then back up. + */ + while (1) { + block_iter_copy_from(&next, it); + err = block_iter_next(&next, &rec); + if (err < 0) + goto done; + + reftable_record_key(&rec, &key); + if (err > 0 || strbuf_cmp(&key, want) >= 0) { + err = 0; + goto done; + } + + block_iter_copy_from(it, &next); + } + +done: + strbuf_release(&key); + strbuf_release(&next.last_key); + reftable_record_destroy(&rec); + + return err; +} + +void block_writer_release(struct block_writer *bw) +{ + FREE_AND_NULL(bw->restarts); + strbuf_release(&bw->last_key); + /* the block is not owned. */ +} + +void reftable_block_done(struct reftable_block *blockp) +{ + struct reftable_block_source source = blockp->source; + if (blockp && source.ops) + source.ops->return_block(source.arg, blockp); + blockp->data = NULL; + blockp->len = 0; + blockp->source.ops = NULL; + blockp->source.arg = NULL; +} diff --git a/reftable/block.h b/reftable/block.h new file mode 100644 index 0000000000..e207706a64 --- /dev/null +++ b/reftable/block.h @@ -0,0 +1,127 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#ifndef BLOCK_H +#define BLOCK_H + +#include "basics.h" +#include "record.h" +#include "reftable-blocksource.h" + +/* + * Writes reftable blocks. The block_writer is reused across blocks to minimize + * allocation overhead. + */ +struct block_writer { + uint8_t *buf; + uint32_t block_size; + + /* Offset ofof the global header. Nonzero in the first block only. */ + uint32_t header_off; + + /* How often to restart keys. */ + int restart_interval; + int hash_size; + + /* Offset of next uint8_t to write. */ + uint32_t next; + uint32_t *restarts; + uint32_t restart_len; + uint32_t restart_cap; + + struct strbuf last_key; + int entries; +}; + +/* + * initializes the blockwriter to write `typ` entries, using `buf` as temporary + * storage. `buf` is not owned by the block_writer. */ +void block_writer_init(struct block_writer *bw, uint8_t typ, uint8_t *buf, + uint32_t block_size, uint32_t header_off, int hash_size); + +/* returns the block type (eg. 'r' for ref records. */ +uint8_t block_writer_type(struct block_writer *bw); + +/* appends the record, or -1 if it doesn't fit. */ +int block_writer_add(struct block_writer *w, struct reftable_record *rec); + +/* appends the key restarts, and compress the block if necessary. */ +int block_writer_finish(struct block_writer *w); + +/* clears out internally allocated block_writer members. */ +void block_writer_release(struct block_writer *bw); + +/* Read a block. */ +struct block_reader { + /* offset of the block header; nonzero for the first block in a + * reftable. */ + uint32_t header_off; + + /* the memory block */ + struct reftable_block block; + int hash_size; + + /* size of the data, excluding restart data. */ + uint32_t block_len; + uint8_t *restart_bytes; + uint16_t restart_count; + + /* size of the data in the file. For log blocks, this is the compressed + * size. */ + uint32_t full_block_size; +}; + +/* Iterate over entries in a block */ +struct block_iter { + /* offset within the block of the next entry to read. */ + uint32_t next_off; + struct block_reader *br; + + /* key for last entry we read. */ + struct strbuf last_key; +}; + +/* initializes a block reader. */ +int block_reader_init(struct block_reader *br, struct reftable_block *bl, + uint32_t header_off, uint32_t table_block_size, + int hash_size); + +/* Position `it` at start of the block */ +void block_reader_start(struct block_reader *br, struct block_iter *it); + +/* Position `it` to the `want` key in the block */ +int block_reader_seek(struct block_reader *br, struct block_iter *it, + struct strbuf *want); + +/* Returns the block type (eg. 'r' for refs) */ +uint8_t block_reader_type(struct block_reader *r); + +/* Decodes the first key in the block */ +int block_reader_first_key(struct block_reader *br, struct strbuf *key); + +void block_iter_copy_from(struct block_iter *dest, struct block_iter *src); + +/* return < 0 for error, 0 for OK, > 0 for EOF. */ +int block_iter_next(struct block_iter *it, struct reftable_record *rec); + +/* Seek to `want` with in the block pointed to by `it` */ +int block_iter_seek(struct block_iter *it, struct strbuf *want); + +/* deallocate memory for `it`. The block reader and its block is left intact. */ +void block_iter_close(struct block_iter *it); + +/* size of file header, depending on format version */ +int header_size(int version); + +/* size of file footer, depending on format version */ +int footer_size(int version); + +/* returns a block to its source. */ +void reftable_block_done(struct reftable_block *ret); + +#endif diff --git a/reftable/block_test.c b/reftable/block_test.c new file mode 100644 index 0000000000..4b3ea262dc --- /dev/null +++ b/reftable/block_test.c @@ -0,0 +1,120 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#include "block.h" + +#include "system.h" +#include "blocksource.h" +#include "basics.h" +#include "constants.h" +#include "record.h" +#include "test_framework.h" +#include "reftable-tests.h" + +static void test_block_read_write(void) +{ + const int header_off = 21; /* random */ + char *names[30]; + const int N = ARRAY_SIZE(names); + const int block_size = 1024; + struct reftable_block block = { NULL }; + struct block_writer bw = { + .last_key = STRBUF_INIT, + }; + struct reftable_ref_record ref = { NULL }; + struct reftable_record rec = { NULL }; + int i = 0; + int n; + struct block_reader br = { 0 }; + struct block_iter it = { .last_key = STRBUF_INIT }; + int j = 0; + struct strbuf want = STRBUF_INIT; + + block.data = reftable_calloc(block_size); + block.len = block_size; + block.source = malloc_block_source(); + block_writer_init(&bw, BLOCK_TYPE_REF, block.data, block_size, + header_off, hash_size(GIT_SHA1_FORMAT_ID)); + reftable_record_from_ref(&rec, &ref); + + for (i = 0; i < N; i++) { + char name[100]; + uint8_t hash[GIT_SHA1_RAWSZ]; + snprintf(name, sizeof(name), "branch%02d", i); + memset(hash, i, sizeof(hash)); + + ref.refname = name; + ref.value_type = REFTABLE_REF_VAL1; + ref.value.val1 = hash; + + names[i] = xstrdup(name); + n = block_writer_add(&bw, &rec); + ref.refname = NULL; + ref.value_type = REFTABLE_REF_DELETION; + EXPECT(n == 0); + } + + n = block_writer_finish(&bw); + EXPECT(n > 0); + + block_writer_release(&bw); + + block_reader_init(&br, &block, header_off, block_size, GIT_SHA1_RAWSZ); + + block_reader_start(&br, &it); + + while (1) { + int r = block_iter_next(&it, &rec); + EXPECT(r >= 0); + if (r > 0) { + break; + } + EXPECT_STREQ(names[j], ref.refname); + j++; + } + + reftable_record_release(&rec); + block_iter_close(&it); + + for (i = 0; i < N; i++) { + struct block_iter it = { .last_key = STRBUF_INIT }; + strbuf_reset(&want); + strbuf_addstr(&want, names[i]); + + n = block_reader_seek(&br, &it, &want); + EXPECT(n == 0); + + n = block_iter_next(&it, &rec); + EXPECT(n == 0); + + EXPECT_STREQ(names[i], ref.refname); + + want.len--; + n = block_reader_seek(&br, &it, &want); + EXPECT(n == 0); + + n = block_iter_next(&it, &rec); + EXPECT(n == 0); + EXPECT_STREQ(names[10 * (i / 10)], ref.refname); + + block_iter_close(&it); + } + + reftable_record_release(&rec); + reftable_block_done(&br.block); + strbuf_release(&want); + for (i = 0; i < N; i++) { + reftable_free(names[i]); + } +} + +int block_test_main(int argc, const char *argv[]) +{ + RUN_TEST(test_block_read_write); + return 0; +} diff --git a/reftable/blocksource.c b/reftable/blocksource.c new file mode 100644 index 0000000000..0044eecd9a --- /dev/null +++ b/reftable/blocksource.c @@ -0,0 +1,148 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#include "system.h" + +#include "basics.h" +#include "blocksource.h" +#include "reftable-blocksource.h" +#include "reftable-error.h" + +static void strbuf_return_block(void *b, struct reftable_block *dest) +{ + memset(dest->data, 0xff, dest->len); + reftable_free(dest->data); +} + +static void strbuf_close(void *b) +{ +} + +static int strbuf_read_block(void *v, struct reftable_block *dest, uint64_t off, + uint32_t size) +{ + struct strbuf *b = v; + assert(off + size <= b->len); + dest->data = reftable_calloc(size); + memcpy(dest->data, b->buf + off, size); + dest->len = size; + return size; +} + +static uint64_t strbuf_size(void *b) +{ + return ((struct strbuf *)b)->len; +} + +static struct reftable_block_source_vtable strbuf_vtable = { + .size = &strbuf_size, + .read_block = &strbuf_read_block, + .return_block = &strbuf_return_block, + .close = &strbuf_close, +}; + +void block_source_from_strbuf(struct reftable_block_source *bs, + struct strbuf *buf) +{ + assert(!bs->ops); + bs->ops = &strbuf_vtable; + bs->arg = buf; +} + +static void malloc_return_block(void *b, struct reftable_block *dest) +{ + memset(dest->data, 0xff, dest->len); + reftable_free(dest->data); +} + +static struct reftable_block_source_vtable malloc_vtable = { + .return_block = &malloc_return_block, +}; + +static struct reftable_block_source malloc_block_source_instance = { + .ops = &malloc_vtable, +}; + +struct reftable_block_source malloc_block_source(void) +{ + return malloc_block_source_instance; +} + +struct file_block_source { + int fd; + uint64_t size; +}; + +static uint64_t file_size(void *b) +{ + return ((struct file_block_source *)b)->size; +} + +static void file_return_block(void *b, struct reftable_block *dest) +{ + memset(dest->data, 0xff, dest->len); + reftable_free(dest->data); +} + +static void file_close(void *b) +{ + int fd = ((struct file_block_source *)b)->fd; + if (fd > 0) { + close(fd); + ((struct file_block_source *)b)->fd = 0; + } + + reftable_free(b); +} + +static int file_read_block(void *v, struct reftable_block *dest, uint64_t off, + uint32_t size) +{ + struct file_block_source *b = v; + assert(off + size <= b->size); + dest->data = reftable_malloc(size); + if (pread(b->fd, dest->data, size, off) != size) + return -1; + dest->len = size; + return size; +} + +static struct reftable_block_source_vtable file_vtable = { + .size = &file_size, + .read_block = &file_read_block, + .return_block = &file_return_block, + .close = &file_close, +}; + +int reftable_block_source_from_file(struct reftable_block_source *bs, + const char *name) +{ + struct stat st = { 0 }; + int err = 0; + int fd = open(name, O_RDONLY); + struct file_block_source *p = NULL; + if (fd < 0) { + if (errno == ENOENT) { + return REFTABLE_NOT_EXIST_ERROR; + } + return -1; + } + + err = fstat(fd, &st); + if (err < 0) + return -1; + + p = reftable_calloc(sizeof(struct file_block_source)); + p->size = st.st_size; + p->fd = fd; + + assert(!bs->ops); + bs->ops = &file_vtable; + bs->arg = p; + return 0; +} diff --git a/reftable/blocksource.h b/reftable/blocksource.h new file mode 100644 index 0000000000..072e2727ad --- /dev/null +++ b/reftable/blocksource.h @@ -0,0 +1,22 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#ifndef BLOCKSOURCE_H +#define BLOCKSOURCE_H + +#include "system.h" + +struct reftable_block_source; + +/* Create an in-memory block source for reading reftables */ +void block_source_from_strbuf(struct reftable_block_source *bs, + struct strbuf *buf); + +struct reftable_block_source malloc_block_source(void); + +#endif diff --git a/reftable/constants.h b/reftable/constants.h new file mode 100644 index 0000000000..5eee72c4c1 --- /dev/null +++ b/reftable/constants.h @@ -0,0 +1,21 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#ifndef CONSTANTS_H +#define CONSTANTS_H + +#define BLOCK_TYPE_LOG 'g' +#define BLOCK_TYPE_INDEX 'i' +#define BLOCK_TYPE_REF 'r' +#define BLOCK_TYPE_OBJ 'o' +#define BLOCK_TYPE_ANY 0 + +#define MAX_RESTARTS ((1 << 16) - 1) +#define DEFAULT_BLOCK_SIZE 4096 + +#endif diff --git a/reftable/dump.c b/reftable/dump.c new file mode 100644 index 0000000000..155953d1b8 --- /dev/null +++ b/reftable/dump.c @@ -0,0 +1,107 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#include "git-compat-util.h" +#include "hash.h" + +#include "reftable-blocksource.h" +#include "reftable-error.h" +#include "reftable-merged.h" +#include "reftable-record.h" +#include "reftable-tests.h" +#include "reftable-writer.h" +#include "reftable-iterator.h" +#include "reftable-reader.h" +#include "reftable-stack.h" +#include "reftable-generic.h" + +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include <string.h> + +static int compact_stack(const char *stackdir) +{ + struct reftable_stack *stack = NULL; + struct reftable_write_options cfg = { 0 }; + + int err = reftable_new_stack(&stack, stackdir, cfg); + if (err < 0) + goto done; + + err = reftable_stack_compact_all(stack, NULL); + if (err < 0) + goto done; +done: + if (stack) { + reftable_stack_destroy(stack); + } + return err; +} + +static void print_help(void) +{ + printf("usage: dump [-cst] arg\n\n" + "options: \n" + " -c compact\n" + " -t dump table\n" + " -s dump stack\n" + " -6 sha256 hash format\n" + " -h this help\n" + "\n"); +} + +int reftable_dump_main(int argc, char *const *argv) +{ + int err = 0; + int opt_dump_table = 0; + int opt_dump_stack = 0; + int opt_compact = 0; + uint32_t opt_hash_id = GIT_SHA1_FORMAT_ID; + const char *arg = NULL, *argv0 = argv[0]; + + for (; argc > 1; argv++, argc--) + if (*argv[1] != '-') + break; + else if (!strcmp("-t", argv[1])) + opt_dump_table = 1; + else if (!strcmp("-6", argv[1])) + opt_hash_id = GIT_SHA256_FORMAT_ID; + else if (!strcmp("-s", argv[1])) + opt_dump_stack = 1; + else if (!strcmp("-c", argv[1])) + opt_compact = 1; + else if (!strcmp("-?", argv[1]) || !strcmp("-h", argv[1])) { + print_help(); + return 2; + } + + if (argc != 2) { + fprintf(stderr, "need argument\n"); + print_help(); + return 2; + } + + arg = argv[1]; + + if (opt_dump_table) { + err = reftable_reader_print_file(arg); + } else if (opt_dump_stack) { + err = reftable_stack_print_directory(arg, opt_hash_id); + } else if (opt_compact) { + err = compact_stack(arg); + } + + if (err < 0) { + fprintf(stderr, "%s: %s: %s\n", argv0, arg, + reftable_error_str(err)); + return 1; + } + return 0; +} diff --git a/reftable/error.c b/reftable/error.c new file mode 100644 index 0000000000..f6f16def92 --- /dev/null +++ b/reftable/error.c @@ -0,0 +1,41 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#include "reftable-error.h" + +#include <stdio.h> + +const char *reftable_error_str(int err) +{ + static char buf[250]; + switch (err) { + case REFTABLE_IO_ERROR: + return "I/O error"; + case REFTABLE_FORMAT_ERROR: + return "corrupt reftable file"; + case REFTABLE_NOT_EXIST_ERROR: + return "file does not exist"; + case REFTABLE_LOCK_ERROR: + return "data is outdated"; + case REFTABLE_API_ERROR: + return "misuse of the reftable API"; + case REFTABLE_ZLIB_ERROR: + return "zlib failure"; + case REFTABLE_NAME_CONFLICT: + return "file/directory conflict"; + case REFTABLE_EMPTY_TABLE_ERROR: + return "wrote empty table"; + case REFTABLE_REFNAME_ERROR: + return "invalid refname"; + case -1: + return "general error"; + default: + snprintf(buf, sizeof(buf), "unknown error code %d", err); + return buf; + } +} diff --git a/reftable/generic.c b/reftable/generic.c new file mode 100644 index 0000000000..7a8a738d86 --- /dev/null +++ b/reftable/generic.c @@ -0,0 +1,169 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#include "basics.h" +#include "record.h" +#include "generic.h" +#include "reftable-iterator.h" +#include "reftable-generic.h" + +int reftable_table_seek_ref(struct reftable_table *tab, + struct reftable_iterator *it, const char *name) +{ + struct reftable_ref_record ref = { + .refname = (char *)name, + }; + struct reftable_record rec = { NULL }; + reftable_record_from_ref(&rec, &ref); + return tab->ops->seek_record(tab->table_arg, it, &rec); +} + +int reftable_table_seek_log(struct reftable_table *tab, + struct reftable_iterator *it, const char *name) +{ + struct reftable_log_record log = { + .refname = (char *)name, + .update_index = ~((uint64_t)0), + }; + struct reftable_record rec = { NULL }; + reftable_record_from_log(&rec, &log); + return tab->ops->seek_record(tab->table_arg, it, &rec); +} + +int reftable_table_read_ref(struct reftable_table *tab, const char *name, + struct reftable_ref_record *ref) +{ + struct reftable_iterator it = { NULL }; + int err = reftable_table_seek_ref(tab, &it, name); + if (err) + goto done; + + err = reftable_iterator_next_ref(&it, ref); + if (err) + goto done; + + if (strcmp(ref->refname, name) || + reftable_ref_record_is_deletion(ref)) { + reftable_ref_record_release(ref); + err = 1; + goto done; + } + +done: + reftable_iterator_destroy(&it); + return err; +} + +int reftable_table_print(struct reftable_table *tab) { + struct reftable_iterator it = { NULL }; + struct reftable_ref_record ref = { NULL }; + struct reftable_log_record log = { NULL }; + uint32_t hash_id = reftable_table_hash_id(tab); + int err = reftable_table_seek_ref(tab, &it, ""); + if (err < 0) { + return err; + } + + while (1) { + err = reftable_iterator_next_ref(&it, &ref); + if (err > 0) { + break; + } + if (err < 0) { + return err; + } + reftable_ref_record_print(&ref, hash_id); + } + reftable_iterator_destroy(&it); + reftable_ref_record_release(&ref); + + err = reftable_table_seek_log(tab, &it, ""); + if (err < 0) { + return err; + } + while (1) { + err = reftable_iterator_next_log(&it, &log); + if (err > 0) { + break; + } + if (err < 0) { + return err; + } + reftable_log_record_print(&log, hash_id); + } + reftable_iterator_destroy(&it); + reftable_log_record_release(&log); + return 0; +} + +uint64_t reftable_table_max_update_index(struct reftable_table *tab) +{ + return tab->ops->max_update_index(tab->table_arg); +} + +uint64_t reftable_table_min_update_index(struct reftable_table *tab) +{ + return tab->ops->min_update_index(tab->table_arg); +} + +uint32_t reftable_table_hash_id(struct reftable_table *tab) +{ + return tab->ops->hash_id(tab->table_arg); +} + +void reftable_iterator_destroy(struct reftable_iterator *it) +{ + if (!it->ops) { + return; + } + it->ops->close(it->iter_arg); + it->ops = NULL; + FREE_AND_NULL(it->iter_arg); +} + +int reftable_iterator_next_ref(struct reftable_iterator *it, + struct reftable_ref_record *ref) +{ + struct reftable_record rec = { NULL }; + reftable_record_from_ref(&rec, ref); + return iterator_next(it, &rec); +} + +int reftable_iterator_next_log(struct reftable_iterator *it, + struct reftable_log_record *log) +{ + struct reftable_record rec = { NULL }; + reftable_record_from_log(&rec, log); + return iterator_next(it, &rec); +} + +int iterator_next(struct reftable_iterator *it, struct reftable_record *rec) +{ + return it->ops->next(it->iter_arg, rec); +} + +static int empty_iterator_next(void *arg, struct reftable_record *rec) +{ + return 1; +} + +static void empty_iterator_close(void *arg) +{ +} + +static struct reftable_iterator_vtable empty_vtable = { + .next = &empty_iterator_next, + .close = &empty_iterator_close, +}; + +void iterator_set_empty(struct reftable_iterator *it) +{ + assert(!it->ops); + it->iter_arg = NULL; + it->ops = &empty_vtable; +} diff --git a/reftable/generic.h b/reftable/generic.h new file mode 100644 index 0000000000..98886a0640 --- /dev/null +++ b/reftable/generic.h @@ -0,0 +1,32 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#ifndef GENERIC_H +#define GENERIC_H + +#include "record.h" +#include "reftable-generic.h" + +/* generic interface to reftables */ +struct reftable_table_vtable { + int (*seek_record)(void *tab, struct reftable_iterator *it, + struct reftable_record *); + uint32_t (*hash_id)(void *tab); + uint64_t (*min_update_index)(void *tab); + uint64_t (*max_update_index)(void *tab); +}; + +struct reftable_iterator_vtable { + int (*next)(void *iter_arg, struct reftable_record *rec); + void (*close)(void *iter_arg); +}; + +void iterator_set_empty(struct reftable_iterator *it); +int iterator_next(struct reftable_iterator *it, struct reftable_record *rec); + +#endif diff --git a/reftable/iter.c b/reftable/iter.c new file mode 100644 index 0000000000..93d04f735b --- /dev/null +++ b/reftable/iter.c @@ -0,0 +1,194 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#include "iter.h" + +#include "system.h" + +#include "block.h" +#include "generic.h" +#include "constants.h" +#include "reader.h" +#include "reftable-error.h" + +int iterator_is_null(struct reftable_iterator *it) +{ + return !it->ops; +} + +static void filtering_ref_iterator_close(void *iter_arg) +{ + struct filtering_ref_iterator *fri = iter_arg; + strbuf_release(&fri->oid); + reftable_iterator_destroy(&fri->it); +} + +static int filtering_ref_iterator_next(void *iter_arg, + struct reftable_record *rec) +{ + struct filtering_ref_iterator *fri = iter_arg; + struct reftable_ref_record *ref = rec->data; + int err = 0; + while (1) { + err = reftable_iterator_next_ref(&fri->it, ref); + if (err != 0) { + break; + } + + if (fri->double_check) { + struct reftable_iterator it = { NULL }; + + err = reftable_table_seek_ref(&fri->tab, &it, + ref->refname); + if (err == 0) { + err = reftable_iterator_next_ref(&it, ref); + } + + reftable_iterator_destroy(&it); + + if (err < 0) { + break; + } + + if (err > 0) { + continue; + } + } + + if (ref->value_type == REFTABLE_REF_VAL2 && + (!memcmp(fri->oid.buf, ref->value.val2.target_value, + fri->oid.len) || + !memcmp(fri->oid.buf, ref->value.val2.value, + fri->oid.len))) + return 0; + + if (ref->value_type == REFTABLE_REF_VAL1 && + !memcmp(fri->oid.buf, ref->value.val1, fri->oid.len)) { + return 0; + } + } + + reftable_ref_record_release(ref); + return err; +} + +static struct reftable_iterator_vtable filtering_ref_iterator_vtable = { + .next = &filtering_ref_iterator_next, + .close = &filtering_ref_iterator_close, +}; + +void iterator_from_filtering_ref_iterator(struct reftable_iterator *it, + struct filtering_ref_iterator *fri) +{ + assert(!it->ops); + it->iter_arg = fri; + it->ops = &filtering_ref_iterator_vtable; +} + +static void indexed_table_ref_iter_close(void *p) +{ + struct indexed_table_ref_iter *it = p; + block_iter_close(&it->cur); + reftable_block_done(&it->block_reader.block); + reftable_free(it->offsets); + strbuf_release(&it->oid); +} + +static int indexed_table_ref_iter_next_block(struct indexed_table_ref_iter *it) +{ + uint64_t off; + int err = 0; + if (it->offset_idx == it->offset_len) { + it->is_finished = 1; + return 1; + } + + reftable_block_done(&it->block_reader.block); + + off = it->offsets[it->offset_idx++]; + err = reader_init_block_reader(it->r, &it->block_reader, off, + BLOCK_TYPE_REF); + if (err < 0) { + return err; + } + if (err > 0) { + /* indexed block does not exist. */ + return REFTABLE_FORMAT_ERROR; + } + block_reader_start(&it->block_reader, &it->cur); + return 0; +} + +static int indexed_table_ref_iter_next(void *p, struct reftable_record *rec) +{ + struct indexed_table_ref_iter *it = p; + struct reftable_ref_record *ref = rec->data; + + while (1) { + int err = block_iter_next(&it->cur, rec); + if (err < 0) { + return err; + } + + if (err > 0) { + err = indexed_table_ref_iter_next_block(it); + if (err < 0) { + return err; + } + + if (it->is_finished) { + return 1; + } + continue; + } + /* BUG */ + if (!memcmp(it->oid.buf, ref->value.val2.target_value, + it->oid.len) || + !memcmp(it->oid.buf, ref->value.val2.value, it->oid.len)) { + return 0; + } + } +} + +int new_indexed_table_ref_iter(struct indexed_table_ref_iter **dest, + struct reftable_reader *r, uint8_t *oid, + int oid_len, uint64_t *offsets, int offset_len) +{ + struct indexed_table_ref_iter empty = INDEXED_TABLE_REF_ITER_INIT; + struct indexed_table_ref_iter *itr = + reftable_calloc(sizeof(struct indexed_table_ref_iter)); + int err = 0; + + *itr = empty; + itr->r = r; + strbuf_add(&itr->oid, oid, oid_len); + + itr->offsets = offsets; + itr->offset_len = offset_len; + + err = indexed_table_ref_iter_next_block(itr); + if (err < 0) { + reftable_free(itr); + } else { + *dest = itr; + } + return err; +} + +static struct reftable_iterator_vtable indexed_table_ref_iter_vtable = { + .next = &indexed_table_ref_iter_next, + .close = &indexed_table_ref_iter_close, +}; + +void iterator_from_indexed_table_ref_iter(struct reftable_iterator *it, + struct indexed_table_ref_iter *itr) +{ + assert(!it->ops); + it->iter_arg = itr; + it->ops = &indexed_table_ref_iter_vtable; +} diff --git a/reftable/iter.h b/reftable/iter.h new file mode 100644 index 0000000000..09eb0cbfa5 --- /dev/null +++ b/reftable/iter.h @@ -0,0 +1,69 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#ifndef ITER_H +#define ITER_H + +#include "system.h" +#include "block.h" +#include "record.h" + +#include "reftable-iterator.h" +#include "reftable-generic.h" + +/* Returns true for a zeroed out iterator, such as the one returned from + * iterator_destroy. */ +int iterator_is_null(struct reftable_iterator *it); + +/* iterator that produces only ref records that point to `oid` */ +struct filtering_ref_iterator { + int double_check; + struct reftable_table tab; + struct strbuf oid; + struct reftable_iterator it; +}; +#define FILTERING_REF_ITERATOR_INIT \ + { \ + .oid = STRBUF_INIT \ + } + +void iterator_from_filtering_ref_iterator(struct reftable_iterator *, + struct filtering_ref_iterator *); + +/* iterator that produces only ref records that point to `oid`, + * but using the object index. + */ +struct indexed_table_ref_iter { + struct reftable_reader *r; + struct strbuf oid; + + /* mutable */ + uint64_t *offsets; + + /* Points to the next offset to read. */ + int offset_idx; + int offset_len; + struct block_reader block_reader; + struct block_iter cur; + int is_finished; +}; + +#define INDEXED_TABLE_REF_ITER_INIT \ + { \ + .cur = { .last_key = STRBUF_INIT }, .oid = STRBUF_INIT, \ + } + +void iterator_from_indexed_table_ref_iter(struct reftable_iterator *it, + struct indexed_table_ref_iter *itr); + +/* Takes ownership of `offsets` */ +int new_indexed_table_ref_iter(struct indexed_table_ref_iter **dest, + struct reftable_reader *r, uint8_t *oid, + int oid_len, uint64_t *offsets, int offset_len); + +#endif diff --git a/reftable/merged.c b/reftable/merged.c new file mode 100644 index 0000000000..e5b53da6db --- /dev/null +++ b/reftable/merged.c @@ -0,0 +1,362 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#include "merged.h" + +#include "constants.h" +#include "iter.h" +#include "pq.h" +#include "reader.h" +#include "record.h" +#include "generic.h" +#include "reftable-merged.h" +#include "reftable-error.h" +#include "system.h" + +static int merged_iter_init(struct merged_iter *mi) +{ + int i = 0; + for (i = 0; i < mi->stack_len; i++) { + struct reftable_record rec = reftable_new_record(mi->typ); + int err = iterator_next(&mi->stack[i], &rec); + if (err < 0) { + return err; + } + + if (err > 0) { + reftable_iterator_destroy(&mi->stack[i]); + reftable_record_destroy(&rec); + } else { + struct pq_entry e = { + .rec = rec, + .index = i, + }; + merged_iter_pqueue_add(&mi->pq, e); + } + } + + return 0; +} + +static void merged_iter_close(void *p) +{ + struct merged_iter *mi = p; + int i = 0; + merged_iter_pqueue_release(&mi->pq); + for (i = 0; i < mi->stack_len; i++) { + reftable_iterator_destroy(&mi->stack[i]); + } + reftable_free(mi->stack); +} + +static int merged_iter_advance_nonnull_subiter(struct merged_iter *mi, + size_t idx) +{ + struct reftable_record rec = reftable_new_record(mi->typ); + struct pq_entry e = { + .rec = rec, + .index = idx, + }; + int err = iterator_next(&mi->stack[idx], &rec); + if (err < 0) + return err; + + if (err > 0) { + reftable_iterator_destroy(&mi->stack[idx]); + reftable_record_destroy(&rec); + return 0; + } + + merged_iter_pqueue_add(&mi->pq, e); + return 0; +} + +static int merged_iter_advance_subiter(struct merged_iter *mi, size_t idx) +{ + if (iterator_is_null(&mi->stack[idx])) + return 0; + return merged_iter_advance_nonnull_subiter(mi, idx); +} + +static int merged_iter_next_entry(struct merged_iter *mi, + struct reftable_record *rec) +{ + struct strbuf entry_key = STRBUF_INIT; + struct pq_entry entry = { 0 }; + int err = 0; + + if (merged_iter_pqueue_is_empty(mi->pq)) + return 1; + + entry = merged_iter_pqueue_remove(&mi->pq); + err = merged_iter_advance_subiter(mi, entry.index); + if (err < 0) + return err; + + /* + One can also use reftable as datacenter-local storage, where the ref + database is maintained in globally consistent database (eg. + CockroachDB or Spanner). In this scenario, replication delays together + with compaction may cause newer tables to contain older entries. In + such a deployment, the loop below must be changed to collect all + entries for the same key, and return new the newest one. + */ + reftable_record_key(&entry.rec, &entry_key); + while (!merged_iter_pqueue_is_empty(mi->pq)) { + struct pq_entry top = merged_iter_pqueue_top(mi->pq); + struct strbuf k = STRBUF_INIT; + int err = 0, cmp = 0; + + reftable_record_key(&top.rec, &k); + + cmp = strbuf_cmp(&k, &entry_key); + strbuf_release(&k); + + if (cmp > 0) { + break; + } + + merged_iter_pqueue_remove(&mi->pq); + err = merged_iter_advance_subiter(mi, top.index); + if (err < 0) { + return err; + } + reftable_record_destroy(&top.rec); + } + + reftable_record_copy_from(rec, &entry.rec, hash_size(mi->hash_id)); + reftable_record_destroy(&entry.rec); + strbuf_release(&entry_key); + return 0; +} + +static int merged_iter_next(struct merged_iter *mi, struct reftable_record *rec) +{ + while (1) { + int err = merged_iter_next_entry(mi, rec); + if (err == 0 && mi->suppress_deletions && + reftable_record_is_deletion(rec)) { + continue; + } + + return err; + } +} + +static int merged_iter_next_void(void *p, struct reftable_record *rec) +{ + struct merged_iter *mi = p; + if (merged_iter_pqueue_is_empty(mi->pq)) + return 1; + + return merged_iter_next(mi, rec); +} + +static struct reftable_iterator_vtable merged_iter_vtable = { + .next = &merged_iter_next_void, + .close = &merged_iter_close, +}; + +static void iterator_from_merged_iter(struct reftable_iterator *it, + struct merged_iter *mi) +{ + assert(!it->ops); + it->iter_arg = mi; + it->ops = &merged_iter_vtable; +} + +int reftable_new_merged_table(struct reftable_merged_table **dest, + struct reftable_table *stack, int n, + uint32_t hash_id) +{ + struct reftable_merged_table *m = NULL; + uint64_t last_max = 0; + uint64_t first_min = 0; + int i = 0; + for (i = 0; i < n; i++) { + uint64_t min = reftable_table_min_update_index(&stack[i]); + uint64_t max = reftable_table_max_update_index(&stack[i]); + + if (reftable_table_hash_id(&stack[i]) != hash_id) { + return REFTABLE_FORMAT_ERROR; + } + if (i == 0 || min < first_min) { + first_min = min; + } + if (i == 0 || max > last_max) { + last_max = max; + } + } + + m = reftable_calloc(sizeof(struct reftable_merged_table)); + m->stack = stack; + m->stack_len = n; + m->min = first_min; + m->max = last_max; + m->hash_id = hash_id; + *dest = m; + return 0; +} + +/* clears the list of subtable, without affecting the readers themselves. */ +void merged_table_release(struct reftable_merged_table *mt) +{ + FREE_AND_NULL(mt->stack); + mt->stack_len = 0; +} + +void reftable_merged_table_free(struct reftable_merged_table *mt) +{ + if (!mt) { + return; + } + merged_table_release(mt); + reftable_free(mt); +} + +uint64_t +reftable_merged_table_max_update_index(struct reftable_merged_table *mt) +{ + return mt->max; +} + +uint64_t +reftable_merged_table_min_update_index(struct reftable_merged_table *mt) +{ + return mt->min; +} + +static int reftable_table_seek_record(struct reftable_table *tab, + struct reftable_iterator *it, + struct reftable_record *rec) +{ + return tab->ops->seek_record(tab->table_arg, it, rec); +} + +static int merged_table_seek_record(struct reftable_merged_table *mt, + struct reftable_iterator *it, + struct reftable_record *rec) +{ + struct reftable_iterator *iters = reftable_calloc( + sizeof(struct reftable_iterator) * mt->stack_len); + struct merged_iter merged = { + .stack = iters, + .typ = reftable_record_type(rec), + .hash_id = mt->hash_id, + .suppress_deletions = mt->suppress_deletions, + }; + int n = 0; + int err = 0; + int i = 0; + for (i = 0; i < mt->stack_len && err == 0; i++) { + int e = reftable_table_seek_record(&mt->stack[i], &iters[n], + rec); + if (e < 0) { + err = e; + } + if (e == 0) { + n++; + } + } + if (err < 0) { + int i = 0; + for (i = 0; i < n; i++) { + reftable_iterator_destroy(&iters[i]); + } + reftable_free(iters); + return err; + } + + merged.stack_len = n; + err = merged_iter_init(&merged); + if (err < 0) { + merged_iter_close(&merged); + return err; + } else { + struct merged_iter *p = + reftable_malloc(sizeof(struct merged_iter)); + *p = merged; + iterator_from_merged_iter(it, p); + } + return 0; +} + +int reftable_merged_table_seek_ref(struct reftable_merged_table *mt, + struct reftable_iterator *it, + const char *name) +{ + struct reftable_ref_record ref = { + .refname = (char *)name, + }; + struct reftable_record rec = { NULL }; + reftable_record_from_ref(&rec, &ref); + return merged_table_seek_record(mt, it, &rec); +} + +int reftable_merged_table_seek_log_at(struct reftable_merged_table *mt, + struct reftable_iterator *it, + const char *name, uint64_t update_index) +{ + struct reftable_log_record log = { + .refname = (char *)name, + .update_index = update_index, + }; + struct reftable_record rec = { NULL }; + reftable_record_from_log(&rec, &log); + return merged_table_seek_record(mt, it, &rec); +} + +int reftable_merged_table_seek_log(struct reftable_merged_table *mt, + struct reftable_iterator *it, + const char *name) +{ + uint64_t max = ~((uint64_t)0); + return reftable_merged_table_seek_log_at(mt, it, name, max); +} + +uint32_t reftable_merged_table_hash_id(struct reftable_merged_table *mt) +{ + return mt->hash_id; +} + +static int reftable_merged_table_seek_void(void *tab, + struct reftable_iterator *it, + struct reftable_record *rec) +{ + return merged_table_seek_record(tab, it, rec); +} + +static uint32_t reftable_merged_table_hash_id_void(void *tab) +{ + return reftable_merged_table_hash_id(tab); +} + +static uint64_t reftable_merged_table_min_update_index_void(void *tab) +{ + return reftable_merged_table_min_update_index(tab); +} + +static uint64_t reftable_merged_table_max_update_index_void(void *tab) +{ + return reftable_merged_table_max_update_index(tab); +} + +static struct reftable_table_vtable merged_table_vtable = { + .seek_record = reftable_merged_table_seek_void, + .hash_id = reftable_merged_table_hash_id_void, + .min_update_index = reftable_merged_table_min_update_index_void, + .max_update_index = reftable_merged_table_max_update_index_void, +}; + +void reftable_table_from_merged_table(struct reftable_table *tab, + struct reftable_merged_table *merged) +{ + assert(!tab->ops); + tab->ops = &merged_table_vtable; + tab->table_arg = merged; +} diff --git a/reftable/merged.h b/reftable/merged.h new file mode 100644 index 0000000000..7d9f95d27e --- /dev/null +++ b/reftable/merged.h @@ -0,0 +1,38 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#ifndef MERGED_H +#define MERGED_H + +#include "pq.h" + +struct reftable_merged_table { + struct reftable_table *stack; + size_t stack_len; + uint32_t hash_id; + + /* If unset, produce deletions. This is useful for compaction. For the + * full stack, deletions should be produced. */ + int suppress_deletions; + + uint64_t min; + uint64_t max; +}; + +struct merged_iter { + struct reftable_iterator *stack; + uint32_t hash_id; + size_t stack_len; + uint8_t typ; + int suppress_deletions; + struct merged_iter_pqueue pq; +}; + +void merged_table_release(struct reftable_merged_table *mt); + +#endif diff --git a/reftable/merged_test.c b/reftable/merged_test.c new file mode 100644 index 0000000000..24461e8a80 --- /dev/null +++ b/reftable/merged_test.c @@ -0,0 +1,468 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#include "merged.h" + +#include "system.h" + +#include "basics.h" +#include "blocksource.h" +#include "constants.h" +#include "reader.h" +#include "record.h" +#include "test_framework.h" +#include "reftable-merged.h" +#include "reftable-tests.h" +#include "reftable-generic.h" +#include "reftable-writer.h" + +static void write_test_table(struct strbuf *buf, + struct reftable_ref_record refs[], int n) +{ + int min = 0xffffffff; + int max = 0; + int i = 0; + int err; + + struct reftable_write_options opts = { + .block_size = 256, + }; + struct reftable_writer *w = NULL; + for (i = 0; i < n; i++) { + uint64_t ui = refs[i].update_index; + if (ui > max) { + max = ui; + } + if (ui < min) { + min = ui; + } + } + + w = reftable_new_writer(&strbuf_add_void, buf, &opts); + reftable_writer_set_limits(w, min, max); + + for (i = 0; i < n; i++) { + uint64_t before = refs[i].update_index; + int n = reftable_writer_add_ref(w, &refs[i]); + EXPECT(n == 0); + EXPECT(before == refs[i].update_index); + } + + err = reftable_writer_close(w); + EXPECT_ERR(err); + + reftable_writer_free(w); +} + +static void write_test_log_table(struct strbuf *buf, + struct reftable_log_record logs[], int n, + uint64_t update_index) +{ + int i = 0; + int err; + + struct reftable_write_options opts = { + .block_size = 256, + .exact_log_message = 1, + }; + struct reftable_writer *w = NULL; + w = reftable_new_writer(&strbuf_add_void, buf, &opts); + reftable_writer_set_limits(w, update_index, update_index); + + for (i = 0; i < n; i++) { + int err = reftable_writer_add_log(w, &logs[i]); + EXPECT_ERR(err); + } + + err = reftable_writer_close(w); + EXPECT_ERR(err); + + reftable_writer_free(w); +} + +static struct reftable_merged_table * +merged_table_from_records(struct reftable_ref_record **refs, + struct reftable_block_source **source, + struct reftable_reader ***readers, int *sizes, + struct strbuf *buf, int n) +{ + int i = 0; + struct reftable_merged_table *mt = NULL; + int err; + struct reftable_table *tabs = + reftable_calloc(n * sizeof(struct reftable_table)); + *readers = reftable_calloc(n * sizeof(struct reftable_reader *)); + *source = reftable_calloc(n * sizeof(**source)); + for (i = 0; i < n; i++) { + write_test_table(&buf[i], refs[i], sizes[i]); + block_source_from_strbuf(&(*source)[i], &buf[i]); + + err = reftable_new_reader(&(*readers)[i], &(*source)[i], + "name"); + EXPECT_ERR(err); + reftable_table_from_reader(&tabs[i], (*readers)[i]); + } + + err = reftable_new_merged_table(&mt, tabs, n, GIT_SHA1_FORMAT_ID); + EXPECT_ERR(err); + return mt; +} + +static void readers_destroy(struct reftable_reader **readers, size_t n) +{ + int i = 0; + for (; i < n; i++) + reftable_reader_free(readers[i]); + reftable_free(readers); +} + +static void test_merged_between(void) +{ + uint8_t hash1[GIT_SHA1_RAWSZ] = { 1, 2, 3, 0 }; + + struct reftable_ref_record r1[] = { { + .refname = "b", + .update_index = 1, + .value_type = REFTABLE_REF_VAL1, + .value.val1 = hash1, + } }; + struct reftable_ref_record r2[] = { { + .refname = "a", + .update_index = 2, + .value_type = REFTABLE_REF_DELETION, + } }; + + struct reftable_ref_record *refs[] = { r1, r2 }; + int sizes[] = { 1, 1 }; + struct strbuf bufs[2] = { STRBUF_INIT, STRBUF_INIT }; + struct reftable_block_source *bs = NULL; + struct reftable_reader **readers = NULL; + struct reftable_merged_table *mt = + merged_table_from_records(refs, &bs, &readers, sizes, bufs, 2); + int i; + struct reftable_ref_record ref = { NULL }; + struct reftable_iterator it = { NULL }; + int err = reftable_merged_table_seek_ref(mt, &it, "a"); + EXPECT_ERR(err); + + err = reftable_iterator_next_ref(&it, &ref); + EXPECT_ERR(err); + EXPECT(ref.update_index == 2); + reftable_ref_record_release(&ref); + reftable_iterator_destroy(&it); + readers_destroy(readers, 2); + reftable_merged_table_free(mt); + for (i = 0; i < ARRAY_SIZE(bufs); i++) { + strbuf_release(&bufs[i]); + } + reftable_free(bs); +} + +static void test_merged(void) +{ + uint8_t hash1[GIT_SHA1_RAWSZ] = { 1 }; + uint8_t hash2[GIT_SHA1_RAWSZ] = { 2 }; + struct reftable_ref_record r1[] = { + { + .refname = "a", + .update_index = 1, + .value_type = REFTABLE_REF_VAL1, + .value.val1 = hash1, + }, + { + .refname = "b", + .update_index = 1, + .value_type = REFTABLE_REF_VAL1, + .value.val1 = hash1, + }, + { + .refname = "c", + .update_index = 1, + .value_type = REFTABLE_REF_VAL1, + .value.val1 = hash1, + } + }; + struct reftable_ref_record r2[] = { { + .refname = "a", + .update_index = 2, + .value_type = REFTABLE_REF_DELETION, + } }; + struct reftable_ref_record r3[] = { + { + .refname = "c", + .update_index = 3, + .value_type = REFTABLE_REF_VAL1, + .value.val1 = hash2, + }, + { + .refname = "d", + .update_index = 3, + .value_type = REFTABLE_REF_VAL1, + .value.val1 = hash1, + }, + }; + + struct reftable_ref_record want[] = { + r2[0], + r1[1], + r3[0], + r3[1], + }; + + struct reftable_ref_record *refs[] = { r1, r2, r3 }; + int sizes[3] = { 3, 1, 2 }; + struct strbuf bufs[3] = { STRBUF_INIT, STRBUF_INIT, STRBUF_INIT }; + struct reftable_block_source *bs = NULL; + struct reftable_reader **readers = NULL; + struct reftable_merged_table *mt = + merged_table_from_records(refs, &bs, &readers, sizes, bufs, 3); + + struct reftable_iterator it = { NULL }; + int err = reftable_merged_table_seek_ref(mt, &it, "a"); + struct reftable_ref_record *out = NULL; + size_t len = 0; + size_t cap = 0; + int i = 0; + + EXPECT_ERR(err); + EXPECT(reftable_merged_table_hash_id(mt) == GIT_SHA1_FORMAT_ID); + EXPECT(reftable_merged_table_min_update_index(mt) == 1); + + while (len < 100) { /* cap loops/recursion. */ + struct reftable_ref_record ref = { NULL }; + int err = reftable_iterator_next_ref(&it, &ref); + if (err > 0) { + break; + } + if (len == cap) { + cap = 2 * cap + 1; + out = reftable_realloc( + out, sizeof(struct reftable_ref_record) * cap); + } + out[len++] = ref; + } + reftable_iterator_destroy(&it); + + EXPECT(ARRAY_SIZE(want) == len); + for (i = 0; i < len; i++) { + EXPECT(reftable_ref_record_equal(&want[i], &out[i], + GIT_SHA1_RAWSZ)); + } + for (i = 0; i < len; i++) { + reftable_ref_record_release(&out[i]); + } + reftable_free(out); + + for (i = 0; i < 3; i++) { + strbuf_release(&bufs[i]); + } + readers_destroy(readers, 3); + reftable_merged_table_free(mt); + reftable_free(bs); +} + +static struct reftable_merged_table * +merged_table_from_log_records(struct reftable_log_record **logs, + struct reftable_block_source **source, + struct reftable_reader ***readers, int *sizes, + struct strbuf *buf, int n) +{ + int i = 0; + struct reftable_merged_table *mt = NULL; + int err; + struct reftable_table *tabs = + reftable_calloc(n * sizeof(struct reftable_table)); + *readers = reftable_calloc(n * sizeof(struct reftable_reader *)); + *source = reftable_calloc(n * sizeof(**source)); + for (i = 0; i < n; i++) { + write_test_log_table(&buf[i], logs[i], sizes[i], i + 1); + block_source_from_strbuf(&(*source)[i], &buf[i]); + + err = reftable_new_reader(&(*readers)[i], &(*source)[i], + "name"); + EXPECT_ERR(err); + reftable_table_from_reader(&tabs[i], (*readers)[i]); + } + + err = reftable_new_merged_table(&mt, tabs, n, GIT_SHA1_FORMAT_ID); + EXPECT_ERR(err); + return mt; +} + +static void test_merged_logs(void) +{ + uint8_t hash1[GIT_SHA1_RAWSZ] = { 1 }; + uint8_t hash2[GIT_SHA1_RAWSZ] = { 2 }; + uint8_t hash3[GIT_SHA1_RAWSZ] = { 3 }; + struct reftable_log_record r1[] = { + { + .refname = "a", + .update_index = 2, + .value_type = REFTABLE_LOG_UPDATE, + .value.update = { + .old_hash = hash2, + /* deletion */ + .name = "jane doe", + .email = "jane@invalid", + .message = "message2", + } + }, + { + .refname = "a", + .update_index = 1, + .value_type = REFTABLE_LOG_UPDATE, + .value.update = { + .old_hash = hash1, + .new_hash = hash2, + .name = "jane doe", + .email = "jane@invalid", + .message = "message1", + } + }, + }; + struct reftable_log_record r2[] = { + { + .refname = "a", + .update_index = 3, + .value_type = REFTABLE_LOG_UPDATE, + .value.update = { + .new_hash = hash3, + .name = "jane doe", + .email = "jane@invalid", + .message = "message3", + } + }, + }; + struct reftable_log_record r3[] = { + { + .refname = "a", + .update_index = 2, + .value_type = REFTABLE_LOG_DELETION, + }, + }; + struct reftable_log_record want[] = { + r2[0], + r3[0], + r1[1], + }; + + struct reftable_log_record *logs[] = { r1, r2, r3 }; + int sizes[3] = { 2, 1, 1 }; + struct strbuf bufs[3] = { STRBUF_INIT, STRBUF_INIT, STRBUF_INIT }; + struct reftable_block_source *bs = NULL; + struct reftable_reader **readers = NULL; + struct reftable_merged_table *mt = merged_table_from_log_records( + logs, &bs, &readers, sizes, bufs, 3); + + struct reftable_iterator it = { NULL }; + int err = reftable_merged_table_seek_log(mt, &it, "a"); + struct reftable_log_record *out = NULL; + size_t len = 0; + size_t cap = 0; + int i = 0; + + EXPECT_ERR(err); + EXPECT(reftable_merged_table_hash_id(mt) == GIT_SHA1_FORMAT_ID); + EXPECT(reftable_merged_table_min_update_index(mt) == 1); + + while (len < 100) { /* cap loops/recursion. */ + struct reftable_log_record log = { NULL }; + int err = reftable_iterator_next_log(&it, &log); + if (err > 0) { + break; + } + if (len == cap) { + cap = 2 * cap + 1; + out = reftable_realloc( + out, sizeof(struct reftable_log_record) * cap); + } + out[len++] = log; + } + reftable_iterator_destroy(&it); + + EXPECT(ARRAY_SIZE(want) == len); + for (i = 0; i < len; i++) { + EXPECT(reftable_log_record_equal(&want[i], &out[i], + GIT_SHA1_RAWSZ)); + } + + err = reftable_merged_table_seek_log_at(mt, &it, "a", 2); + EXPECT_ERR(err); + reftable_log_record_release(&out[0]); + err = reftable_iterator_next_log(&it, &out[0]); + EXPECT_ERR(err); + EXPECT(reftable_log_record_equal(&out[0], &r3[0], GIT_SHA1_RAWSZ)); + reftable_iterator_destroy(&it); + + for (i = 0; i < len; i++) { + reftable_log_record_release(&out[i]); + } + reftable_free(out); + + for (i = 0; i < 3; i++) { + strbuf_release(&bufs[i]); + } + readers_destroy(readers, 3); + reftable_merged_table_free(mt); + reftable_free(bs); +} + +static void test_default_write_opts(void) +{ + struct reftable_write_options opts = { 0 }; + struct strbuf buf = STRBUF_INIT; + struct reftable_writer *w = + reftable_new_writer(&strbuf_add_void, &buf, &opts); + + struct reftable_ref_record rec = { + .refname = "master", + .update_index = 1, + }; + int err; + struct reftable_block_source source = { NULL }; + struct reftable_table *tab = reftable_calloc(sizeof(*tab) * 1); + uint32_t hash_id; + struct reftable_reader *rd = NULL; + struct reftable_merged_table *merged = NULL; + + reftable_writer_set_limits(w, 1, 1); + + err = reftable_writer_add_ref(w, &rec); + EXPECT_ERR(err); + + err = reftable_writer_close(w); + EXPECT_ERR(err); + reftable_writer_free(w); + + block_source_from_strbuf(&source, &buf); + + err = reftable_new_reader(&rd, &source, "filename"); + EXPECT_ERR(err); + + hash_id = reftable_reader_hash_id(rd); + EXPECT(hash_id == GIT_SHA1_FORMAT_ID); + + reftable_table_from_reader(&tab[0], rd); + err = reftable_new_merged_table(&merged, tab, 1, GIT_SHA1_FORMAT_ID); + EXPECT_ERR(err); + + reftable_reader_free(rd); + reftable_merged_table_free(merged); + strbuf_release(&buf); +} + +/* XXX test refs_for(oid) */ + +int merged_test_main(int argc, const char *argv[]) +{ + RUN_TEST(test_merged_logs); + RUN_TEST(test_merged_between); + RUN_TEST(test_merged); + RUN_TEST(test_default_write_opts); + return 0; +} diff --git a/reftable/pq.c b/reftable/pq.c new file mode 100644 index 0000000000..efc474017a --- /dev/null +++ b/reftable/pq.c @@ -0,0 +1,105 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#include "pq.h" + +#include "reftable-record.h" +#include "system.h" +#include "basics.h" + +int pq_less(struct pq_entry *a, struct pq_entry *b) +{ + struct strbuf ak = STRBUF_INIT; + struct strbuf bk = STRBUF_INIT; + int cmp = 0; + reftable_record_key(&a->rec, &ak); + reftable_record_key(&b->rec, &bk); + + cmp = strbuf_cmp(&ak, &bk); + + strbuf_release(&ak); + strbuf_release(&bk); + + if (cmp == 0) + return a->index > b->index; + + return cmp < 0; +} + +struct pq_entry merged_iter_pqueue_top(struct merged_iter_pqueue pq) +{ + return pq.heap[0]; +} + +int merged_iter_pqueue_is_empty(struct merged_iter_pqueue pq) +{ + return pq.len == 0; +} + +struct pq_entry merged_iter_pqueue_remove(struct merged_iter_pqueue *pq) +{ + int i = 0; + struct pq_entry e = pq->heap[0]; + pq->heap[0] = pq->heap[pq->len - 1]; + pq->len--; + + i = 0; + while (i < pq->len) { + int min = i; + int j = 2 * i + 1; + int k = 2 * i + 2; + if (j < pq->len && pq_less(&pq->heap[j], &pq->heap[i])) { + min = j; + } + if (k < pq->len && pq_less(&pq->heap[k], &pq->heap[min])) { + min = k; + } + + if (min == i) { + break; + } + + SWAP(pq->heap[i], pq->heap[min]); + i = min; + } + + return e; +} + +void merged_iter_pqueue_add(struct merged_iter_pqueue *pq, struct pq_entry e) +{ + int i = 0; + if (pq->len == pq->cap) { + pq->cap = 2 * pq->cap + 1; + pq->heap = reftable_realloc(pq->heap, + pq->cap * sizeof(struct pq_entry)); + } + + pq->heap[pq->len++] = e; + i = pq->len - 1; + while (i > 0) { + int j = (i - 1) / 2; + if (pq_less(&pq->heap[j], &pq->heap[i])) { + break; + } + + SWAP(pq->heap[j], pq->heap[i]); + + i = j; + } +} + +void merged_iter_pqueue_release(struct merged_iter_pqueue *pq) +{ + int i = 0; + for (i = 0; i < pq->len; i++) { + reftable_record_destroy(&pq->heap[i].rec); + } + FREE_AND_NULL(pq->heap); + pq->len = pq->cap = 0; +} diff --git a/reftable/pq.h b/reftable/pq.h new file mode 100644 index 0000000000..56fc1b6d87 --- /dev/null +++ b/reftable/pq.h @@ -0,0 +1,33 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#ifndef PQ_H +#define PQ_H + +#include "record.h" + +struct pq_entry { + int index; + struct reftable_record rec; +}; + +struct merged_iter_pqueue { + struct pq_entry *heap; + size_t len; + size_t cap; +}; + +struct pq_entry merged_iter_pqueue_top(struct merged_iter_pqueue pq); +int merged_iter_pqueue_is_empty(struct merged_iter_pqueue pq); +void merged_iter_pqueue_check(struct merged_iter_pqueue pq); +struct pq_entry merged_iter_pqueue_remove(struct merged_iter_pqueue *pq); +void merged_iter_pqueue_add(struct merged_iter_pqueue *pq, struct pq_entry e); +void merged_iter_pqueue_release(struct merged_iter_pqueue *pq); +int pq_less(struct pq_entry *a, struct pq_entry *b); + +#endif diff --git a/reftable/pq_test.c b/reftable/pq_test.c new file mode 100644 index 0000000000..c9bb05e37b --- /dev/null +++ b/reftable/pq_test.c @@ -0,0 +1,82 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#include "system.h" + +#include "basics.h" +#include "constants.h" +#include "pq.h" +#include "record.h" +#include "reftable-tests.h" +#include "test_framework.h" + +void merged_iter_pqueue_check(struct merged_iter_pqueue pq) +{ + int i; + for (i = 1; i < pq.len; i++) { + int parent = (i - 1) / 2; + + EXPECT(pq_less(&pq.heap[parent], &pq.heap[i])); + } +} + +static void test_pq(void) +{ + char *names[54] = { NULL }; + int N = ARRAY_SIZE(names) - 1; + + struct merged_iter_pqueue pq = { NULL }; + const char *last = NULL; + + int i = 0; + for (i = 0; i < N; i++) { + char name[100]; + snprintf(name, sizeof(name), "%02d", i); + names[i] = xstrdup(name); + } + + i = 1; + do { + struct reftable_record rec = + reftable_new_record(BLOCK_TYPE_REF); + struct pq_entry e = { 0 }; + + reftable_record_as_ref(&rec)->refname = names[i]; + e.rec = rec; + merged_iter_pqueue_add(&pq, e); + merged_iter_pqueue_check(pq); + i = (i * 7) % N; + } while (i != 1); + + while (!merged_iter_pqueue_is_empty(pq)) { + struct pq_entry e = merged_iter_pqueue_remove(&pq); + struct reftable_ref_record *ref = + reftable_record_as_ref(&e.rec); + + merged_iter_pqueue_check(pq); + + if (last) { + EXPECT(strcmp(last, ref->refname) < 0); + } + last = ref->refname; + ref->refname = NULL; + reftable_free(ref); + } + + for (i = 0; i < N; i++) { + reftable_free(names[i]); + } + + merged_iter_pqueue_release(&pq); +} + +int pq_test_main(int argc, const char *argv[]) +{ + RUN_TEST(test_pq); + return 0; +} diff --git a/reftable/publicbasics.c b/reftable/publicbasics.c new file mode 100644 index 0000000000..0ad7d5c0ff --- /dev/null +++ b/reftable/publicbasics.c @@ -0,0 +1,65 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#include "reftable-malloc.h" + +#include "basics.h" +#include "system.h" + +static void *(*reftable_malloc_ptr)(size_t sz); +static void *(*reftable_realloc_ptr)(void *, size_t); +static void (*reftable_free_ptr)(void *); + +void *reftable_malloc(size_t sz) +{ + if (reftable_malloc_ptr) + return (*reftable_malloc_ptr)(sz); + return malloc(sz); +} + +void *reftable_realloc(void *p, size_t sz) +{ + if (reftable_realloc_ptr) + return (*reftable_realloc_ptr)(p, sz); + return realloc(p, sz); +} + +void reftable_free(void *p) +{ + if (reftable_free_ptr) + reftable_free_ptr(p); + else + free(p); +} + +void *reftable_calloc(size_t sz) +{ + void *p = reftable_malloc(sz); + memset(p, 0, sz); + return p; +} + +void reftable_set_alloc(void *(*malloc)(size_t), + void *(*realloc)(void *, size_t), void (*free)(void *)) +{ + reftable_malloc_ptr = malloc; + reftable_realloc_ptr = realloc; + reftable_free_ptr = free; +} + +int hash_size(uint32_t id) +{ + switch (id) { + case 0: + case GIT_SHA1_FORMAT_ID: + return GIT_SHA1_RAWSZ; + case GIT_SHA256_FORMAT_ID: + return GIT_SHA256_RAWSZ; + } + abort(); +} diff --git a/reftable/reader.c b/reftable/reader.c new file mode 100644 index 0000000000..006709a645 --- /dev/null +++ b/reftable/reader.c @@ -0,0 +1,801 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#include "reader.h" + +#include "system.h" +#include "block.h" +#include "constants.h" +#include "generic.h" +#include "iter.h" +#include "record.h" +#include "reftable-error.h" +#include "reftable-generic.h" +#include "tree.h" + +uint64_t block_source_size(struct reftable_block_source *source) +{ + return source->ops->size(source->arg); +} + +int block_source_read_block(struct reftable_block_source *source, + struct reftable_block *dest, uint64_t off, + uint32_t size) +{ + int result = source->ops->read_block(source->arg, dest, off, size); + dest->source = *source; + return result; +} + +void block_source_close(struct reftable_block_source *source) +{ + if (!source->ops) { + return; + } + + source->ops->close(source->arg); + source->ops = NULL; +} + +static struct reftable_reader_offsets * +reader_offsets_for(struct reftable_reader *r, uint8_t typ) +{ + switch (typ) { + case BLOCK_TYPE_REF: + return &r->ref_offsets; + case BLOCK_TYPE_LOG: + return &r->log_offsets; + case BLOCK_TYPE_OBJ: + return &r->obj_offsets; + } + abort(); +} + +static int reader_get_block(struct reftable_reader *r, + struct reftable_block *dest, uint64_t off, + uint32_t sz) +{ + if (off >= r->size) + return 0; + + if (off + sz > r->size) { + sz = r->size - off; + } + + return block_source_read_block(&r->source, dest, off, sz); +} + +uint32_t reftable_reader_hash_id(struct reftable_reader *r) +{ + return r->hash_id; +} + +const char *reader_name(struct reftable_reader *r) +{ + return r->name; +} + +static int parse_footer(struct reftable_reader *r, uint8_t *footer, + uint8_t *header) +{ + uint8_t *f = footer; + uint8_t first_block_typ; + int err = 0; + uint32_t computed_crc; + uint32_t file_crc; + + if (memcmp(f, "REFT", 4)) { + err = REFTABLE_FORMAT_ERROR; + goto done; + } + f += 4; + + if (memcmp(footer, header, header_size(r->version))) { + err = REFTABLE_FORMAT_ERROR; + goto done; + } + + f++; + r->block_size = get_be24(f); + + f += 3; + r->min_update_index = get_be64(f); + f += 8; + r->max_update_index = get_be64(f); + f += 8; + + if (r->version == 1) { + r->hash_id = GIT_SHA1_FORMAT_ID; + } else { + r->hash_id = get_be32(f); + switch (r->hash_id) { + case GIT_SHA1_FORMAT_ID: + break; + case GIT_SHA256_FORMAT_ID: + break; + default: + err = REFTABLE_FORMAT_ERROR; + goto done; + } + f += 4; + } + + r->ref_offsets.index_offset = get_be64(f); + f += 8; + + r->obj_offsets.offset = get_be64(f); + f += 8; + + r->object_id_len = r->obj_offsets.offset & ((1 << 5) - 1); + r->obj_offsets.offset >>= 5; + + r->obj_offsets.index_offset = get_be64(f); + f += 8; + r->log_offsets.offset = get_be64(f); + f += 8; + r->log_offsets.index_offset = get_be64(f); + f += 8; + + computed_crc = crc32(0, footer, f - footer); + file_crc = get_be32(f); + f += 4; + if (computed_crc != file_crc) { + err = REFTABLE_FORMAT_ERROR; + goto done; + } + + first_block_typ = header[header_size(r->version)]; + r->ref_offsets.is_present = (first_block_typ == BLOCK_TYPE_REF); + r->ref_offsets.offset = 0; + r->log_offsets.is_present = (first_block_typ == BLOCK_TYPE_LOG || + r->log_offsets.offset > 0); + r->obj_offsets.is_present = r->obj_offsets.offset > 0; + err = 0; +done: + return err; +} + +int init_reader(struct reftable_reader *r, struct reftable_block_source *source, + const char *name) +{ + struct reftable_block footer = { NULL }; + struct reftable_block header = { NULL }; + int err = 0; + uint64_t file_size = block_source_size(source); + + /* Need +1 to read type of first block. */ + uint32_t read_size = header_size(2) + 1; /* read v2 because it's larger. */ + memset(r, 0, sizeof(struct reftable_reader)); + + if (read_size > file_size) { + err = REFTABLE_FORMAT_ERROR; + goto done; + } + + err = block_source_read_block(source, &header, 0, read_size); + if (err != read_size) { + err = REFTABLE_IO_ERROR; + goto done; + } + + if (memcmp(header.data, "REFT", 4)) { + err = REFTABLE_FORMAT_ERROR; + goto done; + } + r->version = header.data[4]; + if (r->version != 1 && r->version != 2) { + err = REFTABLE_FORMAT_ERROR; + goto done; + } + + r->size = file_size - footer_size(r->version); + r->source = *source; + r->name = xstrdup(name); + r->hash_id = 0; + + err = block_source_read_block(source, &footer, r->size, + footer_size(r->version)); + if (err != footer_size(r->version)) { + err = REFTABLE_IO_ERROR; + goto done; + } + + err = parse_footer(r, footer.data, header.data); +done: + reftable_block_done(&footer); + reftable_block_done(&header); + return err; +} + +struct table_iter { + struct reftable_reader *r; + uint8_t typ; + uint64_t block_off; + struct block_iter bi; + int is_finished; +}; +#define TABLE_ITER_INIT \ + { \ + .bi = {.last_key = STRBUF_INIT } \ + } + +static void table_iter_copy_from(struct table_iter *dest, + struct table_iter *src) +{ + dest->r = src->r; + dest->typ = src->typ; + dest->block_off = src->block_off; + dest->is_finished = src->is_finished; + block_iter_copy_from(&dest->bi, &src->bi); +} + +static int table_iter_next_in_block(struct table_iter *ti, + struct reftable_record *rec) +{ + int res = block_iter_next(&ti->bi, rec); + if (res == 0 && reftable_record_type(rec) == BLOCK_TYPE_REF) { + ((struct reftable_ref_record *)rec->data)->update_index += + ti->r->min_update_index; + } + + return res; +} + +static void table_iter_block_done(struct table_iter *ti) +{ + if (!ti->bi.br) { + return; + } + reftable_block_done(&ti->bi.br->block); + FREE_AND_NULL(ti->bi.br); + + ti->bi.last_key.len = 0; + ti->bi.next_off = 0; +} + +static int32_t extract_block_size(uint8_t *data, uint8_t *typ, uint64_t off, + int version) +{ + int32_t result = 0; + + if (off == 0) { + data += header_size(version); + } + + *typ = data[0]; + if (reftable_is_block_type(*typ)) { + result = get_be24(data + 1); + } + return result; +} + +int reader_init_block_reader(struct reftable_reader *r, struct block_reader *br, + uint64_t next_off, uint8_t want_typ) +{ + int32_t guess_block_size = r->block_size ? r->block_size : + DEFAULT_BLOCK_SIZE; + struct reftable_block block = { NULL }; + uint8_t block_typ = 0; + int err = 0; + uint32_t header_off = next_off ? 0 : header_size(r->version); + int32_t block_size = 0; + + if (next_off >= r->size) + return 1; + + err = reader_get_block(r, &block, next_off, guess_block_size); + if (err < 0) + return err; + + block_size = extract_block_size(block.data, &block_typ, next_off, + r->version); + if (block_size < 0) + return block_size; + + if (want_typ != BLOCK_TYPE_ANY && block_typ != want_typ) { + reftable_block_done(&block); + return 1; + } + + if (block_size > guess_block_size) { + reftable_block_done(&block); + err = reader_get_block(r, &block, next_off, block_size); + if (err < 0) { + return err; + } + } + + return block_reader_init(br, &block, header_off, r->block_size, + hash_size(r->hash_id)); +} + +static int table_iter_next_block(struct table_iter *dest, + struct table_iter *src) +{ + uint64_t next_block_off = src->block_off + src->bi.br->full_block_size; + struct block_reader br = { 0 }; + int err = 0; + + dest->r = src->r; + dest->typ = src->typ; + dest->block_off = next_block_off; + + err = reader_init_block_reader(src->r, &br, next_block_off, src->typ); + if (err > 0) { + dest->is_finished = 1; + return 1; + } + if (err != 0) + return err; + else { + struct block_reader *brp = + reftable_malloc(sizeof(struct block_reader)); + *brp = br; + + dest->is_finished = 0; + block_reader_start(brp, &dest->bi); + } + return 0; +} + +static int table_iter_next(struct table_iter *ti, struct reftable_record *rec) +{ + if (reftable_record_type(rec) != ti->typ) + return REFTABLE_API_ERROR; + + while (1) { + struct table_iter next = TABLE_ITER_INIT; + int err = 0; + if (ti->is_finished) { + return 1; + } + + err = table_iter_next_in_block(ti, rec); + if (err <= 0) { + return err; + } + + err = table_iter_next_block(&next, ti); + if (err != 0) { + ti->is_finished = 1; + } + table_iter_block_done(ti); + if (err != 0) { + return err; + } + table_iter_copy_from(ti, &next); + block_iter_close(&next.bi); + } +} + +static int table_iter_next_void(void *ti, struct reftable_record *rec) +{ + return table_iter_next(ti, rec); +} + +static void table_iter_close(void *p) +{ + struct table_iter *ti = p; + table_iter_block_done(ti); + block_iter_close(&ti->bi); +} + +static struct reftable_iterator_vtable table_iter_vtable = { + .next = &table_iter_next_void, + .close = &table_iter_close, +}; + +static void iterator_from_table_iter(struct reftable_iterator *it, + struct table_iter *ti) +{ + assert(!it->ops); + it->iter_arg = ti; + it->ops = &table_iter_vtable; +} + +static int reader_table_iter_at(struct reftable_reader *r, + struct table_iter *ti, uint64_t off, + uint8_t typ) +{ + struct block_reader br = { 0 }; + struct block_reader *brp = NULL; + + int err = reader_init_block_reader(r, &br, off, typ); + if (err != 0) + return err; + + brp = reftable_malloc(sizeof(struct block_reader)); + *brp = br; + ti->r = r; + ti->typ = block_reader_type(brp); + ti->block_off = off; + block_reader_start(brp, &ti->bi); + return 0; +} + +static int reader_start(struct reftable_reader *r, struct table_iter *ti, + uint8_t typ, int index) +{ + struct reftable_reader_offsets *offs = reader_offsets_for(r, typ); + uint64_t off = offs->offset; + if (index) { + off = offs->index_offset; + if (off == 0) { + return 1; + } + typ = BLOCK_TYPE_INDEX; + } + + return reader_table_iter_at(r, ti, off, typ); +} + +static int reader_seek_linear(struct reftable_reader *r, struct table_iter *ti, + struct reftable_record *want) +{ + struct reftable_record rec = + reftable_new_record(reftable_record_type(want)); + struct strbuf want_key = STRBUF_INIT; + struct strbuf got_key = STRBUF_INIT; + struct table_iter next = TABLE_ITER_INIT; + int err = -1; + + reftable_record_key(want, &want_key); + + while (1) { + err = table_iter_next_block(&next, ti); + if (err < 0) + goto done; + + if (err > 0) { + break; + } + + err = block_reader_first_key(next.bi.br, &got_key); + if (err < 0) + goto done; + + if (strbuf_cmp(&got_key, &want_key) > 0) { + table_iter_block_done(&next); + break; + } + + table_iter_block_done(ti); + table_iter_copy_from(ti, &next); + } + + err = block_iter_seek(&ti->bi, &want_key); + if (err < 0) + goto done; + err = 0; + +done: + block_iter_close(&next.bi); + reftable_record_destroy(&rec); + strbuf_release(&want_key); + strbuf_release(&got_key); + return err; +} + +static int reader_seek_indexed(struct reftable_reader *r, + struct reftable_iterator *it, + struct reftable_record *rec) +{ + struct reftable_index_record want_index = { .last_key = STRBUF_INIT }; + struct reftable_record want_index_rec = { NULL }; + struct reftable_index_record index_result = { .last_key = STRBUF_INIT }; + struct reftable_record index_result_rec = { NULL }; + struct table_iter index_iter = TABLE_ITER_INIT; + struct table_iter next = TABLE_ITER_INIT; + int err = 0; + + reftable_record_key(rec, &want_index.last_key); + reftable_record_from_index(&want_index_rec, &want_index); + reftable_record_from_index(&index_result_rec, &index_result); + + err = reader_start(r, &index_iter, reftable_record_type(rec), 1); + if (err < 0) + goto done; + + err = reader_seek_linear(r, &index_iter, &want_index_rec); + while (1) { + err = table_iter_next(&index_iter, &index_result_rec); + table_iter_block_done(&index_iter); + if (err != 0) + goto done; + + err = reader_table_iter_at(r, &next, index_result.offset, 0); + if (err != 0) + goto done; + + err = block_iter_seek(&next.bi, &want_index.last_key); + if (err < 0) + goto done; + + if (next.typ == reftable_record_type(rec)) { + err = 0; + break; + } + + if (next.typ != BLOCK_TYPE_INDEX) { + err = REFTABLE_FORMAT_ERROR; + break; + } + + table_iter_copy_from(&index_iter, &next); + } + + if (err == 0) { + struct table_iter empty = TABLE_ITER_INIT; + struct table_iter *malloced = + reftable_calloc(sizeof(struct table_iter)); + *malloced = empty; + table_iter_copy_from(malloced, &next); + iterator_from_table_iter(it, malloced); + } +done: + block_iter_close(&next.bi); + table_iter_close(&index_iter); + reftable_record_release(&want_index_rec); + reftable_record_release(&index_result_rec); + return err; +} + +static int reader_seek_internal(struct reftable_reader *r, + struct reftable_iterator *it, + struct reftable_record *rec) +{ + struct reftable_reader_offsets *offs = + reader_offsets_for(r, reftable_record_type(rec)); + uint64_t idx = offs->index_offset; + struct table_iter ti = TABLE_ITER_INIT; + int err = 0; + if (idx > 0) + return reader_seek_indexed(r, it, rec); + + err = reader_start(r, &ti, reftable_record_type(rec), 0); + if (err < 0) + return err; + err = reader_seek_linear(r, &ti, rec); + if (err < 0) + return err; + else { + struct table_iter *p = + reftable_malloc(sizeof(struct table_iter)); + *p = ti; + iterator_from_table_iter(it, p); + } + + return 0; +} + +static int reader_seek(struct reftable_reader *r, struct reftable_iterator *it, + struct reftable_record *rec) +{ + uint8_t typ = reftable_record_type(rec); + + struct reftable_reader_offsets *offs = reader_offsets_for(r, typ); + if (!offs->is_present) { + iterator_set_empty(it); + return 0; + } + + return reader_seek_internal(r, it, rec); +} + +int reftable_reader_seek_ref(struct reftable_reader *r, + struct reftable_iterator *it, const char *name) +{ + struct reftable_ref_record ref = { + .refname = (char *)name, + }; + struct reftable_record rec = { NULL }; + reftable_record_from_ref(&rec, &ref); + return reader_seek(r, it, &rec); +} + +int reftable_reader_seek_log_at(struct reftable_reader *r, + struct reftable_iterator *it, const char *name, + uint64_t update_index) +{ + struct reftable_log_record log = { + .refname = (char *)name, + .update_index = update_index, + }; + struct reftable_record rec = { NULL }; + reftable_record_from_log(&rec, &log); + return reader_seek(r, it, &rec); +} + +int reftable_reader_seek_log(struct reftable_reader *r, + struct reftable_iterator *it, const char *name) +{ + uint64_t max = ~((uint64_t)0); + return reftable_reader_seek_log_at(r, it, name, max); +} + +void reader_close(struct reftable_reader *r) +{ + block_source_close(&r->source); + FREE_AND_NULL(r->name); +} + +int reftable_new_reader(struct reftable_reader **p, + struct reftable_block_source *src, char const *name) +{ + struct reftable_reader *rd = + reftable_calloc(sizeof(struct reftable_reader)); + int err = init_reader(rd, src, name); + if (err == 0) { + *p = rd; + } else { + block_source_close(src); + reftable_free(rd); + } + return err; +} + +void reftable_reader_free(struct reftable_reader *r) +{ + reader_close(r); + reftable_free(r); +} + +static int reftable_reader_refs_for_indexed(struct reftable_reader *r, + struct reftable_iterator *it, + uint8_t *oid) +{ + struct reftable_obj_record want = { + .hash_prefix = oid, + .hash_prefix_len = r->object_id_len, + }; + struct reftable_record want_rec = { NULL }; + struct reftable_iterator oit = { NULL }; + struct reftable_obj_record got = { NULL }; + struct reftable_record got_rec = { NULL }; + int err = 0; + struct indexed_table_ref_iter *itr = NULL; + + /* Look through the reverse index. */ + reftable_record_from_obj(&want_rec, &want); + err = reader_seek(r, &oit, &want_rec); + if (err != 0) + goto done; + + /* read out the reftable_obj_record */ + reftable_record_from_obj(&got_rec, &got); + err = iterator_next(&oit, &got_rec); + if (err < 0) + goto done; + + if (err > 0 || + memcmp(want.hash_prefix, got.hash_prefix, r->object_id_len)) { + /* didn't find it; return empty iterator */ + iterator_set_empty(it); + err = 0; + goto done; + } + + err = new_indexed_table_ref_iter(&itr, r, oid, hash_size(r->hash_id), + got.offsets, got.offset_len); + if (err < 0) + goto done; + got.offsets = NULL; + iterator_from_indexed_table_ref_iter(it, itr); + +done: + reftable_iterator_destroy(&oit); + reftable_record_release(&got_rec); + return err; +} + +static int reftable_reader_refs_for_unindexed(struct reftable_reader *r, + struct reftable_iterator *it, + uint8_t *oid) +{ + struct table_iter ti_empty = TABLE_ITER_INIT; + struct table_iter *ti = reftable_calloc(sizeof(struct table_iter)); + struct filtering_ref_iterator *filter = NULL; + struct filtering_ref_iterator empty = FILTERING_REF_ITERATOR_INIT; + int oid_len = hash_size(r->hash_id); + int err; + + *ti = ti_empty; + err = reader_start(r, ti, BLOCK_TYPE_REF, 0); + if (err < 0) { + reftable_free(ti); + return err; + } + + filter = reftable_malloc(sizeof(struct filtering_ref_iterator)); + *filter = empty; + + strbuf_add(&filter->oid, oid, oid_len); + reftable_table_from_reader(&filter->tab, r); + filter->double_check = 0; + iterator_from_table_iter(&filter->it, ti); + + iterator_from_filtering_ref_iterator(it, filter); + return 0; +} + +int reftable_reader_refs_for(struct reftable_reader *r, + struct reftable_iterator *it, uint8_t *oid) +{ + if (r->obj_offsets.is_present) + return reftable_reader_refs_for_indexed(r, it, oid); + return reftable_reader_refs_for_unindexed(r, it, oid); +} + +uint64_t reftable_reader_max_update_index(struct reftable_reader *r) +{ + return r->max_update_index; +} + +uint64_t reftable_reader_min_update_index(struct reftable_reader *r) +{ + return r->min_update_index; +} + +/* generic table interface. */ + +static int reftable_reader_seek_void(void *tab, struct reftable_iterator *it, + struct reftable_record *rec) +{ + return reader_seek(tab, it, rec); +} + +static uint32_t reftable_reader_hash_id_void(void *tab) +{ + return reftable_reader_hash_id(tab); +} + +static uint64_t reftable_reader_min_update_index_void(void *tab) +{ + return reftable_reader_min_update_index(tab); +} + +static uint64_t reftable_reader_max_update_index_void(void *tab) +{ + return reftable_reader_max_update_index(tab); +} + +static struct reftable_table_vtable reader_vtable = { + .seek_record = reftable_reader_seek_void, + .hash_id = reftable_reader_hash_id_void, + .min_update_index = reftable_reader_min_update_index_void, + .max_update_index = reftable_reader_max_update_index_void, +}; + +void reftable_table_from_reader(struct reftable_table *tab, + struct reftable_reader *reader) +{ + assert(!tab->ops); + tab->ops = &reader_vtable; + tab->table_arg = reader; +} + + +int reftable_reader_print_file(const char *tablename) +{ + struct reftable_block_source src = { NULL }; + int err = reftable_block_source_from_file(&src, tablename); + struct reftable_reader *r = NULL; + struct reftable_table tab = { NULL }; + if (err < 0) + goto done; + + err = reftable_new_reader(&r, &src, tablename); + if (err < 0) + goto done; + + reftable_table_from_reader(&tab, r); + err = reftable_table_print(&tab); +done: + reftable_reader_free(r); + return err; +} diff --git a/reftable/reader.h b/reftable/reader.h new file mode 100644 index 0000000000..e869165f23 --- /dev/null +++ b/reftable/reader.h @@ -0,0 +1,64 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#ifndef READER_H +#define READER_H + +#include "block.h" +#include "record.h" +#include "reftable-iterator.h" +#include "reftable-reader.h" + +uint64_t block_source_size(struct reftable_block_source *source); + +int block_source_read_block(struct reftable_block_source *source, + struct reftable_block *dest, uint64_t off, + uint32_t size); +void block_source_close(struct reftable_block_source *source); + +/* metadata for a block type */ +struct reftable_reader_offsets { + int is_present; + uint64_t offset; + uint64_t index_offset; +}; + +/* The state for reading a reftable file. */ +struct reftable_reader { + /* for convience, associate a name with the instance. */ + char *name; + struct reftable_block_source source; + + /* Size of the file, excluding the footer. */ + uint64_t size; + + /* 'sha1' for SHA1, 's256' for SHA-256 */ + uint32_t hash_id; + + uint32_t block_size; + uint64_t min_update_index; + uint64_t max_update_index; + /* Length of the OID keys in the 'o' section */ + int object_id_len; + int version; + + struct reftable_reader_offsets ref_offsets; + struct reftable_reader_offsets obj_offsets; + struct reftable_reader_offsets log_offsets; +}; + +int init_reader(struct reftable_reader *r, struct reftable_block_source *source, + const char *name); +void reader_close(struct reftable_reader *r); +const char *reader_name(struct reftable_reader *r); + +/* initialize a block reader to read from `r` */ +int reader_init_block_reader(struct reftable_reader *r, struct block_reader *br, + uint64_t next_off, uint8_t want_typ); + +#endif diff --git a/reftable/readwrite_test.c b/reftable/readwrite_test.c new file mode 100644 index 0000000000..5f6bcc2f77 --- /dev/null +++ b/reftable/readwrite_test.c @@ -0,0 +1,652 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#include "system.h" + +#include "basics.h" +#include "block.h" +#include "blocksource.h" +#include "constants.h" +#include "reader.h" +#include "record.h" +#include "test_framework.h" +#include "reftable-tests.h" +#include "reftable-writer.h" + +static const int update_index = 5; + +static void test_buffer(void) +{ + struct strbuf buf = STRBUF_INIT; + struct reftable_block_source source = { NULL }; + struct reftable_block out = { NULL }; + int n; + uint8_t in[] = "hello"; + strbuf_add(&buf, in, sizeof(in)); + block_source_from_strbuf(&source, &buf); + EXPECT(block_source_size(&source) == 6); + n = block_source_read_block(&source, &out, 0, sizeof(in)); + EXPECT(n == sizeof(in)); + EXPECT(!memcmp(in, out.data, n)); + reftable_block_done(&out); + + n = block_source_read_block(&source, &out, 1, 2); + EXPECT(n == 2); + EXPECT(!memcmp(out.data, "el", 2)); + + reftable_block_done(&out); + block_source_close(&source); + strbuf_release(&buf); +} + +static void write_table(char ***names, struct strbuf *buf, int N, + int block_size, uint32_t hash_id) +{ + struct reftable_write_options opts = { + .block_size = block_size, + .hash_id = hash_id, + }; + struct reftable_writer *w = + reftable_new_writer(&strbuf_add_void, buf, &opts); + struct reftable_ref_record ref = { NULL }; + int i = 0, n; + struct reftable_log_record log = { NULL }; + const struct reftable_stats *stats = NULL; + *names = reftable_calloc(sizeof(char *) * (N + 1)); + reftable_writer_set_limits(w, update_index, update_index); + for (i = 0; i < N; i++) { + uint8_t hash[GIT_SHA256_RAWSZ] = { 0 }; + char name[100]; + int n; + + set_test_hash(hash, i); + + snprintf(name, sizeof(name), "refs/heads/branch%02d", i); + + ref.refname = name; + ref.update_index = update_index; + ref.value_type = REFTABLE_REF_VAL1; + ref.value.val1 = hash; + (*names)[i] = xstrdup(name); + + n = reftable_writer_add_ref(w, &ref); + EXPECT(n == 0); + } + + for (i = 0; i < N; i++) { + uint8_t hash[GIT_SHA256_RAWSZ] = { 0 }; + char name[100]; + int n; + + set_test_hash(hash, i); + + snprintf(name, sizeof(name), "refs/heads/branch%02d", i); + + log.refname = name; + log.update_index = update_index; + log.value_type = REFTABLE_LOG_UPDATE; + log.value.update.new_hash = hash; + log.value.update.message = "message"; + + n = reftable_writer_add_log(w, &log); + EXPECT(n == 0); + } + + n = reftable_writer_close(w); + EXPECT(n == 0); + + stats = writer_stats(w); + for (i = 0; i < stats->ref_stats.blocks; i++) { + int off = i * opts.block_size; + if (off == 0) { + off = header_size( + (hash_id == GIT_SHA256_FORMAT_ID) ? 2 : 1); + } + EXPECT(buf->buf[off] == 'r'); + } + + EXPECT(stats->log_stats.blocks > 0); + reftable_writer_free(w); +} + +static void test_log_buffer_size(void) +{ + struct strbuf buf = STRBUF_INIT; + struct reftable_write_options opts = { + .block_size = 4096, + }; + int err; + int i; + struct reftable_log_record + log = { .refname = "refs/heads/master", + .update_index = 0xa, + .value_type = REFTABLE_LOG_UPDATE, + .value = { .update = { + .name = "Han-Wen Nienhuys", + .email = "hanwen@google.com", + .tz_offset = 100, + .time = 0x5e430672, + .message = "commit: 9\n", + } } }; + struct reftable_writer *w = + reftable_new_writer(&strbuf_add_void, &buf, &opts); + + /* This tests buffer extension for log compression. Must use a random + hash, to ensure that the compressed part is larger than the original. + */ + uint8_t hash1[GIT_SHA1_RAWSZ], hash2[GIT_SHA1_RAWSZ]; + for (i = 0; i < GIT_SHA1_RAWSZ; i++) { + hash1[i] = (uint8_t)(rand() % 256); + hash2[i] = (uint8_t)(rand() % 256); + } + log.value.update.old_hash = hash1; + log.value.update.new_hash = hash2; + reftable_writer_set_limits(w, update_index, update_index); + err = reftable_writer_add_log(w, &log); + EXPECT_ERR(err); + err = reftable_writer_close(w); + EXPECT_ERR(err); + reftable_writer_free(w); + strbuf_release(&buf); +} + +static void test_log_write_read(void) +{ + int N = 2; + char **names = reftable_calloc(sizeof(char *) * (N + 1)); + int err; + struct reftable_write_options opts = { + .block_size = 256, + }; + struct reftable_ref_record ref = { NULL }; + int i = 0; + struct reftable_log_record log = { NULL }; + int n; + struct reftable_iterator it = { NULL }; + struct reftable_reader rd = { NULL }; + struct reftable_block_source source = { NULL }; + struct strbuf buf = STRBUF_INIT; + struct reftable_writer *w = + reftable_new_writer(&strbuf_add_void, &buf, &opts); + const struct reftable_stats *stats = NULL; + reftable_writer_set_limits(w, 0, N); + for (i = 0; i < N; i++) { + char name[256]; + struct reftable_ref_record ref = { NULL }; + snprintf(name, sizeof(name), "b%02d%0*d", i, 130, 7); + names[i] = xstrdup(name); + ref.refname = name; + ref.update_index = i; + + err = reftable_writer_add_ref(w, &ref); + EXPECT_ERR(err); + } + for (i = 0; i < N; i++) { + uint8_t hash1[GIT_SHA1_RAWSZ], hash2[GIT_SHA1_RAWSZ]; + struct reftable_log_record log = { NULL }; + set_test_hash(hash1, i); + set_test_hash(hash2, i + 1); + + log.refname = names[i]; + log.update_index = i; + log.value_type = REFTABLE_LOG_UPDATE; + log.value.update.old_hash = hash1; + log.value.update.new_hash = hash2; + + err = reftable_writer_add_log(w, &log); + EXPECT_ERR(err); + } + + n = reftable_writer_close(w); + EXPECT(n == 0); + + stats = writer_stats(w); + EXPECT(stats->log_stats.blocks > 0); + reftable_writer_free(w); + w = NULL; + + block_source_from_strbuf(&source, &buf); + + err = init_reader(&rd, &source, "file.log"); + EXPECT_ERR(err); + + err = reftable_reader_seek_ref(&rd, &it, names[N - 1]); + EXPECT_ERR(err); + + err = reftable_iterator_next_ref(&it, &ref); + EXPECT_ERR(err); + + /* end of iteration. */ + err = reftable_iterator_next_ref(&it, &ref); + EXPECT(0 < err); + + reftable_iterator_destroy(&it); + reftable_ref_record_release(&ref); + + err = reftable_reader_seek_log(&rd, &it, ""); + EXPECT_ERR(err); + + i = 0; + while (1) { + int err = reftable_iterator_next_log(&it, &log); + if (err > 0) { + break; + } + + EXPECT_ERR(err); + EXPECT_STREQ(names[i], log.refname); + EXPECT(i == log.update_index); + i++; + reftable_log_record_release(&log); + } + + EXPECT(i == N); + reftable_iterator_destroy(&it); + + /* cleanup. */ + strbuf_release(&buf); + free_names(names); + reader_close(&rd); +} + +static void test_table_read_write_sequential(void) +{ + char **names; + struct strbuf buf = STRBUF_INIT; + int N = 50; + struct reftable_iterator it = { NULL }; + struct reftable_block_source source = { NULL }; + struct reftable_reader rd = { NULL }; + int err = 0; + int j = 0; + + write_table(&names, &buf, N, 256, GIT_SHA1_FORMAT_ID); + + block_source_from_strbuf(&source, &buf); + + err = init_reader(&rd, &source, "file.ref"); + EXPECT_ERR(err); + + err = reftable_reader_seek_ref(&rd, &it, ""); + EXPECT_ERR(err); + + while (1) { + struct reftable_ref_record ref = { NULL }; + int r = reftable_iterator_next_ref(&it, &ref); + EXPECT(r >= 0); + if (r > 0) { + break; + } + EXPECT(0 == strcmp(names[j], ref.refname)); + EXPECT(update_index == ref.update_index); + + j++; + reftable_ref_record_release(&ref); + } + EXPECT(j == N); + reftable_iterator_destroy(&it); + strbuf_release(&buf); + free_names(names); + + reader_close(&rd); +} + +static void test_table_write_small_table(void) +{ + char **names; + struct strbuf buf = STRBUF_INIT; + int N = 1; + write_table(&names, &buf, N, 4096, GIT_SHA1_FORMAT_ID); + EXPECT(buf.len < 200); + strbuf_release(&buf); + free_names(names); +} + +static void test_table_read_api(void) +{ + char **names; + struct strbuf buf = STRBUF_INIT; + int N = 50; + struct reftable_reader rd = { NULL }; + struct reftable_block_source source = { NULL }; + int err; + int i; + struct reftable_log_record log = { NULL }; + struct reftable_iterator it = { NULL }; + + write_table(&names, &buf, N, 256, GIT_SHA1_FORMAT_ID); + + block_source_from_strbuf(&source, &buf); + + err = init_reader(&rd, &source, "file.ref"); + EXPECT_ERR(err); + + err = reftable_reader_seek_ref(&rd, &it, names[0]); + EXPECT_ERR(err); + + err = reftable_iterator_next_log(&it, &log); + EXPECT(err == REFTABLE_API_ERROR); + + strbuf_release(&buf); + for (i = 0; i < N; i++) { + reftable_free(names[i]); + } + reftable_iterator_destroy(&it); + reftable_free(names); + reader_close(&rd); + strbuf_release(&buf); +} + +static void test_table_read_write_seek(int index, int hash_id) +{ + char **names; + struct strbuf buf = STRBUF_INIT; + int N = 50; + struct reftable_reader rd = { NULL }; + struct reftable_block_source source = { NULL }; + int err; + int i = 0; + + struct reftable_iterator it = { NULL }; + struct strbuf pastLast = STRBUF_INIT; + struct reftable_ref_record ref = { NULL }; + + write_table(&names, &buf, N, 256, hash_id); + + block_source_from_strbuf(&source, &buf); + + err = init_reader(&rd, &source, "file.ref"); + EXPECT_ERR(err); + EXPECT(hash_id == reftable_reader_hash_id(&rd)); + + if (!index) { + rd.ref_offsets.index_offset = 0; + } else { + EXPECT(rd.ref_offsets.index_offset > 0); + } + + for (i = 1; i < N; i++) { + int err = reftable_reader_seek_ref(&rd, &it, names[i]); + EXPECT_ERR(err); + err = reftable_iterator_next_ref(&it, &ref); + EXPECT_ERR(err); + EXPECT(0 == strcmp(names[i], ref.refname)); + EXPECT(REFTABLE_REF_VAL1 == ref.value_type); + EXPECT(i == ref.value.val1[0]); + + reftable_ref_record_release(&ref); + reftable_iterator_destroy(&it); + } + + strbuf_addstr(&pastLast, names[N - 1]); + strbuf_addstr(&pastLast, "/"); + + err = reftable_reader_seek_ref(&rd, &it, pastLast.buf); + if (err == 0) { + struct reftable_ref_record ref = { NULL }; + int err = reftable_iterator_next_ref(&it, &ref); + EXPECT(err > 0); + } else { + EXPECT(err > 0); + } + + strbuf_release(&pastLast); + reftable_iterator_destroy(&it); + + strbuf_release(&buf); + for (i = 0; i < N; i++) { + reftable_free(names[i]); + } + reftable_free(names); + reader_close(&rd); +} + +static void test_table_read_write_seek_linear(void) +{ + test_table_read_write_seek(0, GIT_SHA1_FORMAT_ID); +} + +static void test_table_read_write_seek_linear_sha256(void) +{ + test_table_read_write_seek(0, GIT_SHA256_FORMAT_ID); +} + +static void test_table_read_write_seek_index(void) +{ + test_table_read_write_seek(1, GIT_SHA1_FORMAT_ID); +} + +static void test_table_refs_for(int indexed) +{ + int N = 50; + char **want_names = reftable_calloc(sizeof(char *) * (N + 1)); + int want_names_len = 0; + uint8_t want_hash[GIT_SHA1_RAWSZ]; + + struct reftable_write_options opts = { + .block_size = 256, + }; + struct reftable_ref_record ref = { NULL }; + int i = 0; + int n; + int err; + struct reftable_reader rd; + struct reftable_block_source source = { NULL }; + + struct strbuf buf = STRBUF_INIT; + struct reftable_writer *w = + reftable_new_writer(&strbuf_add_void, &buf, &opts); + + struct reftable_iterator it = { NULL }; + int j; + + set_test_hash(want_hash, 4); + + for (i = 0; i < N; i++) { + uint8_t hash[GIT_SHA1_RAWSZ]; + char fill[51] = { 0 }; + char name[100]; + uint8_t hash1[GIT_SHA1_RAWSZ]; + uint8_t hash2[GIT_SHA1_RAWSZ]; + struct reftable_ref_record ref = { NULL }; + + memset(hash, i, sizeof(hash)); + memset(fill, 'x', 50); + /* Put the variable part in the start */ + snprintf(name, sizeof(name), "br%02d%s", i, fill); + name[40] = 0; + ref.refname = name; + + set_test_hash(hash1, i / 4); + set_test_hash(hash2, 3 + i / 4); + ref.value_type = REFTABLE_REF_VAL2; + ref.value.val2.value = hash1; + ref.value.val2.target_value = hash2; + + /* 80 bytes / entry, so 3 entries per block. Yields 17 + */ + /* blocks. */ + n = reftable_writer_add_ref(w, &ref); + EXPECT(n == 0); + + if (!memcmp(hash1, want_hash, GIT_SHA1_RAWSZ) || + !memcmp(hash2, want_hash, GIT_SHA1_RAWSZ)) { + want_names[want_names_len++] = xstrdup(name); + } + } + + n = reftable_writer_close(w); + EXPECT(n == 0); + + reftable_writer_free(w); + w = NULL; + + block_source_from_strbuf(&source, &buf); + + err = init_reader(&rd, &source, "file.ref"); + EXPECT_ERR(err); + if (!indexed) { + rd.obj_offsets.is_present = 0; + } + + err = reftable_reader_seek_ref(&rd, &it, ""); + EXPECT_ERR(err); + reftable_iterator_destroy(&it); + + err = reftable_reader_refs_for(&rd, &it, want_hash); + EXPECT_ERR(err); + + j = 0; + while (1) { + int err = reftable_iterator_next_ref(&it, &ref); + EXPECT(err >= 0); + if (err > 0) { + break; + } + + EXPECT(j < want_names_len); + EXPECT(0 == strcmp(ref.refname, want_names[j])); + j++; + reftable_ref_record_release(&ref); + } + EXPECT(j == want_names_len); + + strbuf_release(&buf); + free_names(want_names); + reftable_iterator_destroy(&it); + reader_close(&rd); +} + +static void test_table_refs_for_no_index(void) +{ + test_table_refs_for(0); +} + +static void test_table_refs_for_obj_index(void) +{ + test_table_refs_for(1); +} + +static void test_write_empty_table(void) +{ + struct reftable_write_options opts = { 0 }; + struct strbuf buf = STRBUF_INIT; + struct reftable_writer *w = + reftable_new_writer(&strbuf_add_void, &buf, &opts); + struct reftable_block_source source = { NULL }; + struct reftable_reader *rd = NULL; + struct reftable_ref_record rec = { NULL }; + struct reftable_iterator it = { NULL }; + int err; + + reftable_writer_set_limits(w, 1, 1); + + err = reftable_writer_close(w); + EXPECT(err == REFTABLE_EMPTY_TABLE_ERROR); + reftable_writer_free(w); + + EXPECT(buf.len == header_size(1) + footer_size(1)); + + block_source_from_strbuf(&source, &buf); + + err = reftable_new_reader(&rd, &source, "filename"); + EXPECT_ERR(err); + + err = reftable_reader_seek_ref(rd, &it, ""); + EXPECT_ERR(err); + + err = reftable_iterator_next_ref(&it, &rec); + EXPECT(err > 0); + + reftable_iterator_destroy(&it); + reftable_reader_free(rd); + strbuf_release(&buf); +} + +static void test_write_key_order(void) +{ + struct reftable_write_options opts = { 0 }; + struct strbuf buf = STRBUF_INIT; + struct reftable_writer *w = + reftable_new_writer(&strbuf_add_void, &buf, &opts); + struct reftable_ref_record refs[2] = { + { + .refname = "b", + .update_index = 1, + .value_type = REFTABLE_REF_SYMREF, + .value = { + .symref = "target", + }, + }, { + .refname = "a", + .update_index = 1, + .value_type = REFTABLE_REF_SYMREF, + .value = { + .symref = "target", + }, + } + }; + int err; + + reftable_writer_set_limits(w, 1, 1); + err = reftable_writer_add_ref(w, &refs[0]); + EXPECT_ERR(err); + err = reftable_writer_add_ref(w, &refs[1]); + printf("%d\n", err); + EXPECT(err == REFTABLE_API_ERROR); + reftable_writer_close(w); + reftable_writer_free(w); + strbuf_release(&buf); +} + +static void test_corrupt_table_empty(void) +{ + struct strbuf buf = STRBUF_INIT; + struct reftable_block_source source = { NULL }; + struct reftable_reader rd = { NULL }; + int err; + + block_source_from_strbuf(&source, &buf); + err = init_reader(&rd, &source, "file.log"); + EXPECT(err == REFTABLE_FORMAT_ERROR); +} + +static void test_corrupt_table(void) +{ + uint8_t zeros[1024] = { 0 }; + struct strbuf buf = STRBUF_INIT; + struct reftable_block_source source = { NULL }; + struct reftable_reader rd = { NULL }; + int err; + strbuf_add(&buf, zeros, sizeof(zeros)); + + block_source_from_strbuf(&source, &buf); + err = init_reader(&rd, &source, "file.log"); + EXPECT(err == REFTABLE_FORMAT_ERROR); + strbuf_release(&buf); +} + +int readwrite_test_main(int argc, const char *argv[]) +{ + RUN_TEST(test_corrupt_table); + RUN_TEST(test_corrupt_table_empty); + RUN_TEST(test_log_write_read); + RUN_TEST(test_write_key_order); + RUN_TEST(test_table_read_write_seek_linear_sha256); + RUN_TEST(test_log_buffer_size); + RUN_TEST(test_table_write_small_table); + RUN_TEST(test_buffer); + RUN_TEST(test_table_read_api); + RUN_TEST(test_table_read_write_sequential); + RUN_TEST(test_table_read_write_seek_linear); + RUN_TEST(test_table_read_write_seek_index); + RUN_TEST(test_table_refs_for_no_index); + RUN_TEST(test_table_refs_for_obj_index); + RUN_TEST(test_write_empty_table); + return 0; +} diff --git a/reftable/record.c b/reftable/record.c new file mode 100644 index 0000000000..6a5dac32dc --- /dev/null +++ b/reftable/record.c @@ -0,0 +1,1212 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +/* record.c - methods for different types of records. */ + +#include "record.h" + +#include "system.h" +#include "constants.h" +#include "reftable-error.h" +#include "basics.h" + +int get_var_int(uint64_t *dest, struct string_view *in) +{ + int ptr = 0; + uint64_t val; + + if (in->len == 0) + return -1; + val = in->buf[ptr] & 0x7f; + + while (in->buf[ptr] & 0x80) { + ptr++; + if (ptr > in->len) { + return -1; + } + val = (val + 1) << 7 | (uint64_t)(in->buf[ptr] & 0x7f); + } + + *dest = val; + return ptr + 1; +} + +int put_var_int(struct string_view *dest, uint64_t val) +{ + uint8_t buf[10] = { 0 }; + int i = 9; + int n = 0; + buf[i] = (uint8_t)(val & 0x7f); + i--; + while (1) { + val >>= 7; + if (!val) { + break; + } + val--; + buf[i] = 0x80 | (uint8_t)(val & 0x7f); + i--; + } + + n = sizeof(buf) - i - 1; + if (dest->len < n) + return -1; + memcpy(dest->buf, &buf[i + 1], n); + return n; +} + +int reftable_is_block_type(uint8_t typ) +{ + switch (typ) { + case BLOCK_TYPE_REF: + case BLOCK_TYPE_LOG: + case BLOCK_TYPE_OBJ: + case BLOCK_TYPE_INDEX: + return 1; + } + return 0; +} + +uint8_t *reftable_ref_record_val1(struct reftable_ref_record *rec) +{ + switch (rec->value_type) { + case REFTABLE_REF_VAL1: + return rec->value.val1; + case REFTABLE_REF_VAL2: + return rec->value.val2.value; + default: + return NULL; + } +} + +uint8_t *reftable_ref_record_val2(struct reftable_ref_record *rec) +{ + switch (rec->value_type) { + case REFTABLE_REF_VAL2: + return rec->value.val2.target_value; + default: + return NULL; + } +} + +static int decode_string(struct strbuf *dest, struct string_view in) +{ + int start_len = in.len; + uint64_t tsize = 0; + int n = get_var_int(&tsize, &in); + if (n <= 0) + return -1; + string_view_consume(&in, n); + if (in.len < tsize) + return -1; + + strbuf_reset(dest); + strbuf_add(dest, in.buf, tsize); + string_view_consume(&in, tsize); + + return start_len - in.len; +} + +static int encode_string(char *str, struct string_view s) +{ + struct string_view start = s; + int l = strlen(str); + int n = put_var_int(&s, l); + if (n < 0) + return -1; + string_view_consume(&s, n); + if (s.len < l) + return -1; + memcpy(s.buf, str, l); + string_view_consume(&s, l); + + return start.len - s.len; +} + +int reftable_encode_key(int *restart, struct string_view dest, + struct strbuf prev_key, struct strbuf key, + uint8_t extra) +{ + struct string_view start = dest; + int prefix_len = common_prefix_size(&prev_key, &key); + uint64_t suffix_len = key.len - prefix_len; + int n = put_var_int(&dest, (uint64_t)prefix_len); + if (n < 0) + return -1; + string_view_consume(&dest, n); + + *restart = (prefix_len == 0); + + n = put_var_int(&dest, suffix_len << 3 | (uint64_t)extra); + if (n < 0) + return -1; + string_view_consume(&dest, n); + + if (dest.len < suffix_len) + return -1; + memcpy(dest.buf, key.buf + prefix_len, suffix_len); + string_view_consume(&dest, suffix_len); + + return start.len - dest.len; +} + +int reftable_decode_key(struct strbuf *key, uint8_t *extra, + struct strbuf last_key, struct string_view in) +{ + int start_len = in.len; + uint64_t prefix_len = 0; + uint64_t suffix_len = 0; + int n = get_var_int(&prefix_len, &in); + if (n < 0) + return -1; + string_view_consume(&in, n); + + if (prefix_len > last_key.len) + return -1; + + n = get_var_int(&suffix_len, &in); + if (n <= 0) + return -1; + string_view_consume(&in, n); + + *extra = (uint8_t)(suffix_len & 0x7); + suffix_len >>= 3; + + if (in.len < suffix_len) + return -1; + + strbuf_reset(key); + strbuf_add(key, last_key.buf, prefix_len); + strbuf_add(key, in.buf, suffix_len); + string_view_consume(&in, suffix_len); + + return start_len - in.len; +} + +static void reftable_ref_record_key(const void *r, struct strbuf *dest) +{ + const struct reftable_ref_record *rec = + (const struct reftable_ref_record *)r; + strbuf_reset(dest); + strbuf_addstr(dest, rec->refname); +} + +static void reftable_ref_record_copy_from(void *rec, const void *src_rec, + int hash_size) +{ + struct reftable_ref_record *ref = rec; + const struct reftable_ref_record *src = src_rec; + assert(hash_size > 0); + + /* This is simple and correct, but we could probably reuse the hash + * fields. */ + reftable_ref_record_release(ref); + if (src->refname) { + ref->refname = xstrdup(src->refname); + } + ref->update_index = src->update_index; + ref->value_type = src->value_type; + switch (src->value_type) { + case REFTABLE_REF_DELETION: + break; + case REFTABLE_REF_VAL1: + ref->value.val1 = reftable_malloc(hash_size); + memcpy(ref->value.val1, src->value.val1, hash_size); + break; + case REFTABLE_REF_VAL2: + ref->value.val2.value = reftable_malloc(hash_size); + memcpy(ref->value.val2.value, src->value.val2.value, hash_size); + ref->value.val2.target_value = reftable_malloc(hash_size); + memcpy(ref->value.val2.target_value, + src->value.val2.target_value, hash_size); + break; + case REFTABLE_REF_SYMREF: + ref->value.symref = xstrdup(src->value.symref); + break; + } +} + +static char hexdigit(int c) +{ + if (c <= 9) + return '0' + c; + return 'a' + (c - 10); +} + +static void hex_format(char *dest, uint8_t *src, int hash_size) +{ + assert(hash_size > 0); + if (src) { + int i = 0; + for (i = 0; i < hash_size; i++) { + dest[2 * i] = hexdigit(src[i] >> 4); + dest[2 * i + 1] = hexdigit(src[i] & 0xf); + } + dest[2 * hash_size] = 0; + } +} + +void reftable_ref_record_print(struct reftable_ref_record *ref, + uint32_t hash_id) +{ + char hex[2 * GIT_SHA256_RAWSZ + 1] = { 0 }; /* BUG */ + printf("ref{%s(%" PRIu64 ") ", ref->refname, ref->update_index); + switch (ref->value_type) { + case REFTABLE_REF_SYMREF: + printf("=> %s", ref->value.symref); + break; + case REFTABLE_REF_VAL2: + hex_format(hex, ref->value.val2.value, hash_size(hash_id)); + printf("val 2 %s", hex); + hex_format(hex, ref->value.val2.target_value, + hash_size(hash_id)); + printf("(T %s)", hex); + break; + case REFTABLE_REF_VAL1: + hex_format(hex, ref->value.val1, hash_size(hash_id)); + printf("val 1 %s", hex); + break; + case REFTABLE_REF_DELETION: + printf("delete"); + break; + } + printf("}\n"); +} + +static void reftable_ref_record_release_void(void *rec) +{ + reftable_ref_record_release(rec); +} + +void reftable_ref_record_release(struct reftable_ref_record *ref) +{ + switch (ref->value_type) { + case REFTABLE_REF_SYMREF: + reftable_free(ref->value.symref); + break; + case REFTABLE_REF_VAL2: + reftable_free(ref->value.val2.target_value); + reftable_free(ref->value.val2.value); + break; + case REFTABLE_REF_VAL1: + reftable_free(ref->value.val1); + break; + case REFTABLE_REF_DELETION: + break; + default: + abort(); + } + + reftable_free(ref->refname); + memset(ref, 0, sizeof(struct reftable_ref_record)); +} + +static uint8_t reftable_ref_record_val_type(const void *rec) +{ + const struct reftable_ref_record *r = + (const struct reftable_ref_record *)rec; + return r->value_type; +} + +static int reftable_ref_record_encode(const void *rec, struct string_view s, + int hash_size) +{ + const struct reftable_ref_record *r = + (const struct reftable_ref_record *)rec; + struct string_view start = s; + int n = put_var_int(&s, r->update_index); + assert(hash_size > 0); + if (n < 0) + return -1; + string_view_consume(&s, n); + + switch (r->value_type) { + case REFTABLE_REF_SYMREF: + n = encode_string(r->value.symref, s); + if (n < 0) { + return -1; + } + string_view_consume(&s, n); + break; + case REFTABLE_REF_VAL2: + if (s.len < 2 * hash_size) { + return -1; + } + memcpy(s.buf, r->value.val2.value, hash_size); + string_view_consume(&s, hash_size); + memcpy(s.buf, r->value.val2.target_value, hash_size); + string_view_consume(&s, hash_size); + break; + case REFTABLE_REF_VAL1: + if (s.len < hash_size) { + return -1; + } + memcpy(s.buf, r->value.val1, hash_size); + string_view_consume(&s, hash_size); + break; + case REFTABLE_REF_DELETION: + break; + default: + abort(); + } + + return start.len - s.len; +} + +static int reftable_ref_record_decode(void *rec, struct strbuf key, + uint8_t val_type, struct string_view in, + int hash_size) +{ + struct reftable_ref_record *r = rec; + struct string_view start = in; + uint64_t update_index = 0; + int n = get_var_int(&update_index, &in); + if (n < 0) + return n; + string_view_consume(&in, n); + + reftable_ref_record_release(r); + + assert(hash_size > 0); + + r->refname = reftable_realloc(r->refname, key.len + 1); + memcpy(r->refname, key.buf, key.len); + r->update_index = update_index; + r->refname[key.len] = 0; + r->value_type = val_type; + switch (val_type) { + case REFTABLE_REF_VAL1: + if (in.len < hash_size) { + return -1; + } + + r->value.val1 = reftable_malloc(hash_size); + memcpy(r->value.val1, in.buf, hash_size); + string_view_consume(&in, hash_size); + break; + + case REFTABLE_REF_VAL2: + if (in.len < 2 * hash_size) { + return -1; + } + + r->value.val2.value = reftable_malloc(hash_size); + memcpy(r->value.val2.value, in.buf, hash_size); + string_view_consume(&in, hash_size); + + r->value.val2.target_value = reftable_malloc(hash_size); + memcpy(r->value.val2.target_value, in.buf, hash_size); + string_view_consume(&in, hash_size); + break; + + case REFTABLE_REF_SYMREF: { + struct strbuf dest = STRBUF_INIT; + int n = decode_string(&dest, in); + if (n < 0) { + return -1; + } + string_view_consume(&in, n); + r->value.symref = dest.buf; + } break; + + case REFTABLE_REF_DELETION: + break; + default: + abort(); + break; + } + + return start.len - in.len; +} + +static int reftable_ref_record_is_deletion_void(const void *p) +{ + return reftable_ref_record_is_deletion( + (const struct reftable_ref_record *)p); +} + +static struct reftable_record_vtable reftable_ref_record_vtable = { + .key = &reftable_ref_record_key, + .type = BLOCK_TYPE_REF, + .copy_from = &reftable_ref_record_copy_from, + .val_type = &reftable_ref_record_val_type, + .encode = &reftable_ref_record_encode, + .decode = &reftable_ref_record_decode, + .release = &reftable_ref_record_release_void, + .is_deletion = &reftable_ref_record_is_deletion_void, +}; + +static void reftable_obj_record_key(const void *r, struct strbuf *dest) +{ + const struct reftable_obj_record *rec = + (const struct reftable_obj_record *)r; + strbuf_reset(dest); + strbuf_add(dest, rec->hash_prefix, rec->hash_prefix_len); +} + +static void reftable_obj_record_release(void *rec) +{ + struct reftable_obj_record *obj = rec; + FREE_AND_NULL(obj->hash_prefix); + FREE_AND_NULL(obj->offsets); + memset(obj, 0, sizeof(struct reftable_obj_record)); +} + +static void reftable_obj_record_copy_from(void *rec, const void *src_rec, + int hash_size) +{ + struct reftable_obj_record *obj = rec; + const struct reftable_obj_record *src = + (const struct reftable_obj_record *)src_rec; + + reftable_obj_record_release(obj); + *obj = *src; + obj->hash_prefix = reftable_malloc(obj->hash_prefix_len); + memcpy(obj->hash_prefix, src->hash_prefix, obj->hash_prefix_len); + + obj->offsets = reftable_malloc(obj->offset_len * sizeof(uint64_t)); + COPY_ARRAY(obj->offsets, src->offsets, obj->offset_len); +} + +static uint8_t reftable_obj_record_val_type(const void *rec) +{ + const struct reftable_obj_record *r = rec; + if (r->offset_len > 0 && r->offset_len < 8) + return r->offset_len; + return 0; +} + +static int reftable_obj_record_encode(const void *rec, struct string_view s, + int hash_size) +{ + const struct reftable_obj_record *r = rec; + struct string_view start = s; + int i = 0; + int n = 0; + uint64_t last = 0; + if (r->offset_len == 0 || r->offset_len >= 8) { + n = put_var_int(&s, r->offset_len); + if (n < 0) { + return -1; + } + string_view_consume(&s, n); + } + if (r->offset_len == 0) + return start.len - s.len; + n = put_var_int(&s, r->offsets[0]); + if (n < 0) + return -1; + string_view_consume(&s, n); + + last = r->offsets[0]; + for (i = 1; i < r->offset_len; i++) { + int n = put_var_int(&s, r->offsets[i] - last); + if (n < 0) { + return -1; + } + string_view_consume(&s, n); + last = r->offsets[i]; + } + return start.len - s.len; +} + +static int reftable_obj_record_decode(void *rec, struct strbuf key, + uint8_t val_type, struct string_view in, + int hash_size) +{ + struct string_view start = in; + struct reftable_obj_record *r = rec; + uint64_t count = val_type; + int n = 0; + uint64_t last; + int j; + r->hash_prefix = reftable_malloc(key.len); + memcpy(r->hash_prefix, key.buf, key.len); + r->hash_prefix_len = key.len; + + if (val_type == 0) { + n = get_var_int(&count, &in); + if (n < 0) { + return n; + } + + string_view_consume(&in, n); + } + + r->offsets = NULL; + r->offset_len = 0; + if (count == 0) + return start.len - in.len; + + r->offsets = reftable_malloc(count * sizeof(uint64_t)); + r->offset_len = count; + + n = get_var_int(&r->offsets[0], &in); + if (n < 0) + return n; + string_view_consume(&in, n); + + last = r->offsets[0]; + j = 1; + while (j < count) { + uint64_t delta = 0; + int n = get_var_int(&delta, &in); + if (n < 0) { + return n; + } + string_view_consume(&in, n); + + last = r->offsets[j] = (delta + last); + j++; + } + return start.len - in.len; +} + +static int not_a_deletion(const void *p) +{ + return 0; +} + +static struct reftable_record_vtable reftable_obj_record_vtable = { + .key = &reftable_obj_record_key, + .type = BLOCK_TYPE_OBJ, + .copy_from = &reftable_obj_record_copy_from, + .val_type = &reftable_obj_record_val_type, + .encode = &reftable_obj_record_encode, + .decode = &reftable_obj_record_decode, + .release = &reftable_obj_record_release, + .is_deletion = not_a_deletion, +}; + +void reftable_log_record_print(struct reftable_log_record *log, + uint32_t hash_id) +{ + char hex[GIT_SHA256_RAWSZ + 1] = { 0 }; + + switch (log->value_type) { + case REFTABLE_LOG_DELETION: + printf("log{%s(%" PRIu64 ") delete", log->refname, + log->update_index); + break; + case REFTABLE_LOG_UPDATE: + printf("log{%s(%" PRIu64 ") %s <%s> %" PRIu64 " %04d\n", + log->refname, log->update_index, log->value.update.name, + log->value.update.email, log->value.update.time, + log->value.update.tz_offset); + hex_format(hex, log->value.update.old_hash, hash_size(hash_id)); + printf("%s => ", hex); + hex_format(hex, log->value.update.new_hash, hash_size(hash_id)); + printf("%s\n\n%s\n}\n", hex, log->value.update.message); + break; + } +} + +static void reftable_log_record_key(const void *r, struct strbuf *dest) +{ + const struct reftable_log_record *rec = + (const struct reftable_log_record *)r; + int len = strlen(rec->refname); + uint8_t i64[8]; + uint64_t ts = 0; + strbuf_reset(dest); + strbuf_add(dest, (uint8_t *)rec->refname, len + 1); + + ts = (~ts) - rec->update_index; + put_be64(&i64[0], ts); + strbuf_add(dest, i64, sizeof(i64)); +} + +static void reftable_log_record_copy_from(void *rec, const void *src_rec, + int hash_size) +{ + struct reftable_log_record *dst = rec; + const struct reftable_log_record *src = + (const struct reftable_log_record *)src_rec; + + reftable_log_record_release(dst); + *dst = *src; + if (dst->refname) { + dst->refname = xstrdup(dst->refname); + } + switch (dst->value_type) { + case REFTABLE_LOG_DELETION: + break; + case REFTABLE_LOG_UPDATE: + if (dst->value.update.email) { + dst->value.update.email = + xstrdup(dst->value.update.email); + } + if (dst->value.update.name) { + dst->value.update.name = + xstrdup(dst->value.update.name); + } + if (dst->value.update.message) { + dst->value.update.message = + xstrdup(dst->value.update.message); + } + + if (dst->value.update.new_hash) { + dst->value.update.new_hash = reftable_malloc(hash_size); + memcpy(dst->value.update.new_hash, + src->value.update.new_hash, hash_size); + } + if (dst->value.update.old_hash) { + dst->value.update.old_hash = reftable_malloc(hash_size); + memcpy(dst->value.update.old_hash, + src->value.update.old_hash, hash_size); + } + break; + } +} + +static void reftable_log_record_release_void(void *rec) +{ + struct reftable_log_record *r = rec; + reftable_log_record_release(r); +} + +void reftable_log_record_release(struct reftable_log_record *r) +{ + reftable_free(r->refname); + switch (r->value_type) { + case REFTABLE_LOG_DELETION: + break; + case REFTABLE_LOG_UPDATE: + reftable_free(r->value.update.new_hash); + reftable_free(r->value.update.old_hash); + reftable_free(r->value.update.name); + reftable_free(r->value.update.email); + reftable_free(r->value.update.message); + break; + } + memset(r, 0, sizeof(struct reftable_log_record)); +} + +static uint8_t reftable_log_record_val_type(const void *rec) +{ + const struct reftable_log_record *log = + (const struct reftable_log_record *)rec; + + return reftable_log_record_is_deletion(log) ? 0 : 1; +} + +static uint8_t zero[GIT_SHA256_RAWSZ] = { 0 }; + +static int reftable_log_record_encode(const void *rec, struct string_view s, + int hash_size) +{ + const struct reftable_log_record *r = rec; + struct string_view start = s; + int n = 0; + uint8_t *oldh = NULL; + uint8_t *newh = NULL; + if (reftable_log_record_is_deletion(r)) + return 0; + + oldh = r->value.update.old_hash; + newh = r->value.update.new_hash; + if (!oldh) { + oldh = zero; + } + if (!newh) { + newh = zero; + } + + if (s.len < 2 * hash_size) + return -1; + + memcpy(s.buf, oldh, hash_size); + memcpy(s.buf + hash_size, newh, hash_size); + string_view_consume(&s, 2 * hash_size); + + n = encode_string(r->value.update.name ? r->value.update.name : "", s); + if (n < 0) + return -1; + string_view_consume(&s, n); + + n = encode_string(r->value.update.email ? r->value.update.email : "", + s); + if (n < 0) + return -1; + string_view_consume(&s, n); + + n = put_var_int(&s, r->value.update.time); + if (n < 0) + return -1; + string_view_consume(&s, n); + + if (s.len < 2) + return -1; + + put_be16(s.buf, r->value.update.tz_offset); + string_view_consume(&s, 2); + + n = encode_string( + r->value.update.message ? r->value.update.message : "", s); + if (n < 0) + return -1; + string_view_consume(&s, n); + + return start.len - s.len; +} + +static int reftable_log_record_decode(void *rec, struct strbuf key, + uint8_t val_type, struct string_view in, + int hash_size) +{ + struct string_view start = in; + struct reftable_log_record *r = rec; + uint64_t max = 0; + uint64_t ts = 0; + struct strbuf dest = STRBUF_INIT; + int n; + + if (key.len <= 9 || key.buf[key.len - 9] != 0) + return REFTABLE_FORMAT_ERROR; + + r->refname = reftable_realloc(r->refname, key.len - 8); + memcpy(r->refname, key.buf, key.len - 8); + ts = get_be64(key.buf + key.len - 8); + + r->update_index = (~max) - ts; + + if (val_type != r->value_type) { + switch (r->value_type) { + case REFTABLE_LOG_UPDATE: + FREE_AND_NULL(r->value.update.old_hash); + FREE_AND_NULL(r->value.update.new_hash); + FREE_AND_NULL(r->value.update.message); + FREE_AND_NULL(r->value.update.email); + FREE_AND_NULL(r->value.update.name); + break; + case REFTABLE_LOG_DELETION: + break; + } + } + + r->value_type = val_type; + if (val_type == REFTABLE_LOG_DELETION) + return 0; + + if (in.len < 2 * hash_size) + return REFTABLE_FORMAT_ERROR; + + r->value.update.old_hash = + reftable_realloc(r->value.update.old_hash, hash_size); + r->value.update.new_hash = + reftable_realloc(r->value.update.new_hash, hash_size); + + memcpy(r->value.update.old_hash, in.buf, hash_size); + memcpy(r->value.update.new_hash, in.buf + hash_size, hash_size); + + string_view_consume(&in, 2 * hash_size); + + n = decode_string(&dest, in); + if (n < 0) + goto done; + string_view_consume(&in, n); + + r->value.update.name = + reftable_realloc(r->value.update.name, dest.len + 1); + memcpy(r->value.update.name, dest.buf, dest.len); + r->value.update.name[dest.len] = 0; + + strbuf_reset(&dest); + n = decode_string(&dest, in); + if (n < 0) + goto done; + string_view_consume(&in, n); + + r->value.update.email = + reftable_realloc(r->value.update.email, dest.len + 1); + memcpy(r->value.update.email, dest.buf, dest.len); + r->value.update.email[dest.len] = 0; + + ts = 0; + n = get_var_int(&ts, &in); + if (n < 0) + goto done; + string_view_consume(&in, n); + r->value.update.time = ts; + if (in.len < 2) + goto done; + + r->value.update.tz_offset = get_be16(in.buf); + string_view_consume(&in, 2); + + strbuf_reset(&dest); + n = decode_string(&dest, in); + if (n < 0) + goto done; + string_view_consume(&in, n); + + r->value.update.message = + reftable_realloc(r->value.update.message, dest.len + 1); + memcpy(r->value.update.message, dest.buf, dest.len); + r->value.update.message[dest.len] = 0; + + strbuf_release(&dest); + return start.len - in.len; + +done: + strbuf_release(&dest); + return REFTABLE_FORMAT_ERROR; +} + +static int null_streq(char *a, char *b) +{ + char *empty = ""; + if (!a) + a = empty; + + if (!b) + b = empty; + + return 0 == strcmp(a, b); +} + +static int zero_hash_eq(uint8_t *a, uint8_t *b, int sz) +{ + if (!a) + a = zero; + + if (!b) + b = zero; + + return !memcmp(a, b, sz); +} + +int reftable_log_record_equal(struct reftable_log_record *a, + struct reftable_log_record *b, int hash_size) +{ + if (!(null_streq(a->refname, b->refname) && + a->update_index == b->update_index && + a->value_type == b->value_type)) + return 0; + + switch (a->value_type) { + case REFTABLE_LOG_DELETION: + return 1; + case REFTABLE_LOG_UPDATE: + return null_streq(a->value.update.name, b->value.update.name) && + a->value.update.time == b->value.update.time && + a->value.update.tz_offset == b->value.update.tz_offset && + null_streq(a->value.update.email, + b->value.update.email) && + null_streq(a->value.update.message, + b->value.update.message) && + zero_hash_eq(a->value.update.old_hash, + b->value.update.old_hash, hash_size) && + zero_hash_eq(a->value.update.new_hash, + b->value.update.new_hash, hash_size); + } + + abort(); +} + +static int reftable_log_record_is_deletion_void(const void *p) +{ + return reftable_log_record_is_deletion( + (const struct reftable_log_record *)p); +} + +static struct reftable_record_vtable reftable_log_record_vtable = { + .key = &reftable_log_record_key, + .type = BLOCK_TYPE_LOG, + .copy_from = &reftable_log_record_copy_from, + .val_type = &reftable_log_record_val_type, + .encode = &reftable_log_record_encode, + .decode = &reftable_log_record_decode, + .release = &reftable_log_record_release_void, + .is_deletion = &reftable_log_record_is_deletion_void, +}; + +struct reftable_record reftable_new_record(uint8_t typ) +{ + struct reftable_record rec = { NULL }; + switch (typ) { + case BLOCK_TYPE_REF: { + struct reftable_ref_record *r = + reftable_calloc(sizeof(struct reftable_ref_record)); + reftable_record_from_ref(&rec, r); + return rec; + } + + case BLOCK_TYPE_OBJ: { + struct reftable_obj_record *r = + reftable_calloc(sizeof(struct reftable_obj_record)); + reftable_record_from_obj(&rec, r); + return rec; + } + case BLOCK_TYPE_LOG: { + struct reftable_log_record *r = + reftable_calloc(sizeof(struct reftable_log_record)); + reftable_record_from_log(&rec, r); + return rec; + } + case BLOCK_TYPE_INDEX: { + struct reftable_index_record empty = { .last_key = + STRBUF_INIT }; + struct reftable_index_record *r = + reftable_calloc(sizeof(struct reftable_index_record)); + *r = empty; + reftable_record_from_index(&rec, r); + return rec; + } + } + abort(); + return rec; +} + +/* clear out the record, yielding the reftable_record data that was + * encapsulated. */ +static void *reftable_record_yield(struct reftable_record *rec) +{ + void *p = rec->data; + rec->data = NULL; + return p; +} + +void reftable_record_destroy(struct reftable_record *rec) +{ + reftable_record_release(rec); + reftable_free(reftable_record_yield(rec)); +} + +static void reftable_index_record_key(const void *r, struct strbuf *dest) +{ + const struct reftable_index_record *rec = r; + strbuf_reset(dest); + strbuf_addbuf(dest, &rec->last_key); +} + +static void reftable_index_record_copy_from(void *rec, const void *src_rec, + int hash_size) +{ + struct reftable_index_record *dst = rec; + const struct reftable_index_record *src = src_rec; + + strbuf_reset(&dst->last_key); + strbuf_addbuf(&dst->last_key, &src->last_key); + dst->offset = src->offset; +} + +static void reftable_index_record_release(void *rec) +{ + struct reftable_index_record *idx = rec; + strbuf_release(&idx->last_key); +} + +static uint8_t reftable_index_record_val_type(const void *rec) +{ + return 0; +} + +static int reftable_index_record_encode(const void *rec, struct string_view out, + int hash_size) +{ + const struct reftable_index_record *r = + (const struct reftable_index_record *)rec; + struct string_view start = out; + + int n = put_var_int(&out, r->offset); + if (n < 0) + return n; + + string_view_consume(&out, n); + + return start.len - out.len; +} + +static int reftable_index_record_decode(void *rec, struct strbuf key, + uint8_t val_type, struct string_view in, + int hash_size) +{ + struct string_view start = in; + struct reftable_index_record *r = rec; + int n = 0; + + strbuf_reset(&r->last_key); + strbuf_addbuf(&r->last_key, &key); + + n = get_var_int(&r->offset, &in); + if (n < 0) + return n; + + string_view_consume(&in, n); + return start.len - in.len; +} + +static struct reftable_record_vtable reftable_index_record_vtable = { + .key = &reftable_index_record_key, + .type = BLOCK_TYPE_INDEX, + .copy_from = &reftable_index_record_copy_from, + .val_type = &reftable_index_record_val_type, + .encode = &reftable_index_record_encode, + .decode = &reftable_index_record_decode, + .release = &reftable_index_record_release, + .is_deletion = ¬_a_deletion, +}; + +void reftable_record_key(struct reftable_record *rec, struct strbuf *dest) +{ + rec->ops->key(rec->data, dest); +} + +uint8_t reftable_record_type(struct reftable_record *rec) +{ + return rec->ops->type; +} + +int reftable_record_encode(struct reftable_record *rec, struct string_view dest, + int hash_size) +{ + return rec->ops->encode(rec->data, dest, hash_size); +} + +void reftable_record_copy_from(struct reftable_record *rec, + struct reftable_record *src, int hash_size) +{ + assert(src->ops->type == rec->ops->type); + + rec->ops->copy_from(rec->data, src->data, hash_size); +} + +uint8_t reftable_record_val_type(struct reftable_record *rec) +{ + return rec->ops->val_type(rec->data); +} + +int reftable_record_decode(struct reftable_record *rec, struct strbuf key, + uint8_t extra, struct string_view src, int hash_size) +{ + return rec->ops->decode(rec->data, key, extra, src, hash_size); +} + +void reftable_record_release(struct reftable_record *rec) +{ + rec->ops->release(rec->data); +} + +int reftable_record_is_deletion(struct reftable_record *rec) +{ + return rec->ops->is_deletion(rec->data); +} + +void reftable_record_from_ref(struct reftable_record *rec, + struct reftable_ref_record *ref_rec) +{ + assert(!rec->ops); + rec->data = ref_rec; + rec->ops = &reftable_ref_record_vtable; +} + +void reftable_record_from_obj(struct reftable_record *rec, + struct reftable_obj_record *obj_rec) +{ + assert(!rec->ops); + rec->data = obj_rec; + rec->ops = &reftable_obj_record_vtable; +} + +void reftable_record_from_index(struct reftable_record *rec, + struct reftable_index_record *index_rec) +{ + assert(!rec->ops); + rec->data = index_rec; + rec->ops = &reftable_index_record_vtable; +} + +void reftable_record_from_log(struct reftable_record *rec, + struct reftable_log_record *log_rec) +{ + assert(!rec->ops); + rec->data = log_rec; + rec->ops = &reftable_log_record_vtable; +} + +struct reftable_ref_record *reftable_record_as_ref(struct reftable_record *rec) +{ + assert(reftable_record_type(rec) == BLOCK_TYPE_REF); + return rec->data; +} + +struct reftable_log_record *reftable_record_as_log(struct reftable_record *rec) +{ + assert(reftable_record_type(rec) == BLOCK_TYPE_LOG); + return rec->data; +} + +static int hash_equal(uint8_t *a, uint8_t *b, int hash_size) +{ + if (a && b) + return !memcmp(a, b, hash_size); + + return a == b; +} + +int reftable_ref_record_equal(struct reftable_ref_record *a, + struct reftable_ref_record *b, int hash_size) +{ + assert(hash_size > 0); + if (!(0 == strcmp(a->refname, b->refname) && + a->update_index == b->update_index && + a->value_type == b->value_type)) + return 0; + + switch (a->value_type) { + case REFTABLE_REF_SYMREF: + return !strcmp(a->value.symref, b->value.symref); + case REFTABLE_REF_VAL2: + return hash_equal(a->value.val2.value, b->value.val2.value, + hash_size) && + hash_equal(a->value.val2.target_value, + b->value.val2.target_value, hash_size); + case REFTABLE_REF_VAL1: + return hash_equal(a->value.val1, b->value.val1, hash_size); + case REFTABLE_REF_DELETION: + return 1; + default: + abort(); + } +} + +int reftable_ref_record_compare_name(const void *a, const void *b) +{ + return strcmp(((struct reftable_ref_record *)a)->refname, + ((struct reftable_ref_record *)b)->refname); +} + +int reftable_ref_record_is_deletion(const struct reftable_ref_record *ref) +{ + return ref->value_type == REFTABLE_REF_DELETION; +} + +int reftable_log_record_compare_key(const void *a, const void *b) +{ + const struct reftable_log_record *la = a; + const struct reftable_log_record *lb = b; + + int cmp = strcmp(la->refname, lb->refname); + if (cmp) + return cmp; + if (la->update_index > lb->update_index) + return -1; + return (la->update_index < lb->update_index) ? 1 : 0; +} + +int reftable_log_record_is_deletion(const struct reftable_log_record *log) +{ + return (log->value_type == REFTABLE_LOG_DELETION); +} + +void string_view_consume(struct string_view *s, int n) +{ + s->buf += n; + s->len -= n; +} diff --git a/reftable/record.h b/reftable/record.h new file mode 100644 index 0000000000..498e8c50bf --- /dev/null +++ b/reftable/record.h @@ -0,0 +1,139 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#ifndef RECORD_H +#define RECORD_H + +#include "system.h" + +#include <stdint.h> + +#include "reftable-record.h" + +/* + * A substring of existing string data. This structure takes no responsibility + * for the lifetime of the data it points to. + */ +struct string_view { + uint8_t *buf; + size_t len; +}; + +/* Advance `s.buf` by `n`, and decrease length. */ +void string_view_consume(struct string_view *s, int n); + +/* utilities for de/encoding varints */ + +int get_var_int(uint64_t *dest, struct string_view *in); +int put_var_int(struct string_view *dest, uint64_t val); + +/* Methods for records. */ +struct reftable_record_vtable { + /* encode the key of to a uint8_t strbuf. */ + void (*key)(const void *rec, struct strbuf *dest); + + /* The record type of ('r' for ref). */ + uint8_t type; + + void (*copy_from)(void *dest, const void *src, int hash_size); + + /* a value of [0..7], indicating record subvariants (eg. ref vs. symref + * vs ref deletion) */ + uint8_t (*val_type)(const void *rec); + + /* encodes rec into dest, returning how much space was used. */ + int (*encode)(const void *rec, struct string_view dest, int hash_size); + + /* decode data from `src` into the record. */ + int (*decode)(void *rec, struct strbuf key, uint8_t extra, + struct string_view src, int hash_size); + + /* deallocate and null the record. */ + void (*release)(void *rec); + + /* is this a tombstone? */ + int (*is_deletion)(const void *rec); +}; + +/* record is a generic wrapper for different types of records. */ +struct reftable_record { + void *data; + struct reftable_record_vtable *ops; +}; + +/* returns true for recognized block types. Block start with the block type. */ +int reftable_is_block_type(uint8_t typ); + +/* creates a malloced record of the given type. Dispose with record_destroy */ +struct reftable_record reftable_new_record(uint8_t typ); + +/* Encode `key` into `dest`. Sets `is_restart` to indicate a restart. Returns + * number of bytes written. */ +int reftable_encode_key(int *is_restart, struct string_view dest, + struct strbuf prev_key, struct strbuf key, + uint8_t extra); + +/* Decode into `key` and `extra` from `in` */ +int reftable_decode_key(struct strbuf *key, uint8_t *extra, + struct strbuf last_key, struct string_view in); + +/* reftable_index_record are used internally to speed up lookups. */ +struct reftable_index_record { + uint64_t offset; /* Offset of block */ + struct strbuf last_key; /* Last key of the block. */ +}; + +/* reftable_obj_record stores an object ID => ref mapping. */ +struct reftable_obj_record { + uint8_t *hash_prefix; /* leading bytes of the object ID */ + int hash_prefix_len; /* number of leading bytes. Constant + * across a single table. */ + uint64_t *offsets; /* a vector of file offsets. */ + int offset_len; +}; + +/* see struct record_vtable */ + +void reftable_record_key(struct reftable_record *rec, struct strbuf *dest); +uint8_t reftable_record_type(struct reftable_record *rec); +void reftable_record_copy_from(struct reftable_record *rec, + struct reftable_record *src, int hash_size); +uint8_t reftable_record_val_type(struct reftable_record *rec); +int reftable_record_encode(struct reftable_record *rec, struct string_view dest, + int hash_size); +int reftable_record_decode(struct reftable_record *rec, struct strbuf key, + uint8_t extra, struct string_view src, + int hash_size); +int reftable_record_is_deletion(struct reftable_record *rec); + +/* zeroes out the embedded record */ +void reftable_record_release(struct reftable_record *rec); + +/* clear and deallocate embedded record, and zero `rec`. */ +void reftable_record_destroy(struct reftable_record *rec); + +/* initialize generic records from concrete records. The generic record should + * be zeroed out. */ +void reftable_record_from_obj(struct reftable_record *rec, + struct reftable_obj_record *objrec); +void reftable_record_from_index(struct reftable_record *rec, + struct reftable_index_record *idxrec); +void reftable_record_from_ref(struct reftable_record *rec, + struct reftable_ref_record *refrec); +void reftable_record_from_log(struct reftable_record *rec, + struct reftable_log_record *logrec); +struct reftable_ref_record *reftable_record_as_ref(struct reftable_record *ref); +struct reftable_log_record *reftable_record_as_log(struct reftable_record *ref); + +/* for qsort. */ +int reftable_ref_record_compare_name(const void *a, const void *b); + +/* for qsort. */ +int reftable_log_record_compare_key(const void *a, const void *b); + +#endif diff --git a/reftable/record_test.c b/reftable/record_test.c new file mode 100644 index 0000000000..f4ad7cace4 --- /dev/null +++ b/reftable/record_test.c @@ -0,0 +1,412 @@ +/* + Copyright 2020 Google LLC + + Use of this source code is governed by a BSD-style + license that can be found in the LICENSE file or at + https://developers.google.com/open-source/licenses/bsd +*/ + +#include "record.h" + +#include "system.h" +#include "basics.h" +#include "constants.h" +#include "test_framework.h" +#include "reftable-tests.h" + +static void test_copy(struct reftable_record *rec) +{ + struct reftable_record copy = + reftable_new_record(reftable_record_type(rec)); + reftable_record_copy_from(©, rec, GIT_SHA1_RAWSZ); + /* do it twice to catch memory leaks */ + reftable_record_copy_from(©, rec, GIT_SHA1_RAWSZ); + switch (reftable_record_type(©)) { + case BLOCK_TYPE_REF: + EXPECT(reftable_ref_record_equal(reftable_record_as_ref(©), + reftable_record_as_ref(rec), + GIT_SHA1_RAWSZ)); + break; + case BLOCK_TYPE_LOG: + EXPECT(reftable_log_record_equal(reftable_record_as_log(©), + reftable_record_as_log(rec), + GIT_SHA1_RAWSZ)); + break; + } + reftable_record_destroy(©); +} + +static void test_varint_roundtrip(void) +{ + uint64_t inputs[] = { 0, + 1, + 27, + 127, + 128, + 257, + 4096, + ((uint64_t)1 << 63), + ((uint64_t)1 << 63) + ((uint64_t)1 << 63) - 1 }; + int i = 0; + for (i = 0; i < ARRAY_SIZE(inputs); i++) { + uint8_t dest[10]; + + struct string_view out = { + .buf = dest, + .len = sizeof(dest), + }; + uint64_t in = inputs[i]; + int n = put_var_int(&out, in); + uint64_t got = 0; + + EXPECT(n > 0); + out.len = n; + n = get_var_int(&got, &out); + EXPECT(n > 0); + + EXPECT(got == in); + } +} + +static void test_common_prefix(void) +{ + struct { + const char *a, *b; + int want; + } cases[] = { + { "abc", "ab", 2 }, + { "", "abc", 0 }, + { "abc", "abd", 2 }, + { "abc", "pqr", 0 }, + }; + + int i = 0; + for (i = 0; i < ARRAY_SIZE(cases); i++) { + struct strbuf a = STRBUF_INIT; + struct strbuf b = STRBUF_INIT; + strbuf_addstr(&a, cases[i].a); + strbuf_addstr(&b, cases[i].b); + EXPECT(common_prefix_size(&a, &b) == cases[i].want); + + strbuf_release(&a); + strbuf_release(&b); + } +} + +static void set_hash(uint8_t *h, int j) +{ + int i = 0; + for (i = 0; i < hash_size(GIT_SHA1_FORMAT_ID); i++) { + h[i] = (j >> i) & 0xff; + } +} + +static void test_reftable_ref_record_roundtrip(void) +{ + int i = 0; + + for (i = REFTABLE_REF_DELETION; i < REFTABLE_NR_REF_VALUETYPES; i++) { + struct reftable_ref_record in = { NULL }; + struct reftable_ref_record out = { NULL }; + struct reftable_record rec_out = { NULL }; + struct strbuf key = STRBUF_INIT; + struct reftable_record rec = { NULL }; + uint8_t buffer[1024] = { 0 }; + struct string_view dest = { + .buf = buffer, + .len = sizeof(buffer), + }; + + int n, m; + + in.value_type = i; + switch (i) { + case REFTABLE_REF_DELETION: + break; + case REFTABLE_REF_VAL1: + in.value.val1 = reftable_malloc(GIT_SHA1_RAWSZ); + set_hash(in.value.val1, 1); + break; + case REFTABLE_REF_VAL2: + in.value.val2.value = reftable_malloc(GIT_SHA1_RAWSZ); + set_hash(in.value.val2.value, 1); + in.value.val2.target_value = + reftable_malloc(GIT_SHA1_RAWSZ); + set_hash(in.value.val2.target_value, 2); + break; + case REFTABLE_REF_SYMREF: + in.value.symref = xstrdup("target"); + break; + } + in.refname = xstrdup("refs/heads/master"); + + reftable_record_from_ref(&rec, &in); + test_copy(&rec); + + EXPECT(reftable_record_val_type(&rec) == i); + + reftable_record_key(&rec, &key); + n = reftable_record_encode(&rec, dest, GIT_SHA1_RAWSZ); + EXPECT(n > 0); + + /* decode into a non-zero reftable_record to test for leaks. */ + + reftable_record_from_ref(&rec_out, &out); + m = reftable_record_decode(&rec_out, key, i, dest, + GIT_SHA1_RAWSZ); + EXPECT(n == m); + + EXPECT(reftable_ref_record_equal(&in, &out, GIT_SHA1_RAWSZ)); + reftable_record_release(&rec_out); + + strbuf_release(&key); + reftable_ref_record_release(&in); + } +} + +static void test_reftable_log_record_equal(void) +{ + struct reftable_log_record in[2] = { + { + .refname = xstrdup("refs/heads/master"), + .update_index = 42, + }, + { + .refname = xstrdup("refs/heads/master"), + .update_index = 22, + } + }; + + EXPECT(!reftable_log_record_equal(&in[0], &in[1], GIT_SHA1_RAWSZ)); + in[1].update_index = in[0].update_index; + EXPECT(reftable_log_record_equal(&in[0], &in[1], GIT_SHA1_RAWSZ)); + reftable_log_record_release(&in[0]); + reftable_log_record_release(&in[1]); +} + +static void test_reftable_log_record_roundtrip(void) +{ + int i; + struct reftable_log_record in[2] = { + { + .refname = xstrdup("refs/heads/master"), + .update_index = 42, + .value_type = REFTABLE_LOG_UPDATE, + .value = { + .update = { + .old_hash = reftable_malloc(GIT_SHA1_RAWSZ), + .new_hash = reftable_malloc(GIT_SHA1_RAWSZ), + .name = xstrdup("han-wen"), + .email = xstrdup("hanwen@google.com"), + .message = xstrdup("test"), + .time = 1577123507, + .tz_offset = 100, + }, + } + }, + { + .refname = xstrdup("refs/heads/master"), + .update_index = 22, + .value_type = REFTABLE_LOG_DELETION, + } + }; + set_test_hash(in[0].value.update.new_hash, 1); + set_test_hash(in[0].value.update.old_hash, 2); + for (i = 0; i < ARRAY_SIZE(in); i++) { + struct reftable_record rec = { NULL }; + struct strbuf key = STRBUF_INIT; + uint8_t buffer[1024] = { 0 }; + struct string_view dest = { + .buf = buffer, + .len = sizeof(buffer), + }; + /* populate out, to check for leaks. */ + struct reftable_log_record out = { + .refname = xstrdup("old name"), + .value_type = REFTABLE_LOG_UPDATE, + .value = { + .update = { + .new_hash = reftable_calloc(GIT_SHA1_RAWSZ), + .old_hash = reftable_calloc(GIT_SHA1_RAWSZ), + .name = xstrdup("old name"), + .email = xstrdup("old@email"), + .message = xstrdup("old message"), + }, + }, + }; + struct reftable_record rec_out = { NULL }; + int n, m, valtype; + + reftable_record_from_log(&rec, &in[i]); + + test_copy(&rec); + + reftable_record_key(&rec, &key); + + n = reftable_record_encode(&rec, dest, GIT_SHA1_RAWSZ); + EXPECT(n >= 0); + reftable_record_from_log(&rec_out, &out); + valtype = reftable_record_val_type(&rec); + m = reftable_record_decode(&rec_out, key, valtype, dest, + GIT_SHA1_RAWSZ); + EXPECT(n == m); + + EXPECT(reftable_log_record_equal(&in[i], &out, GIT_SHA1_RAWSZ)); + reftable_log_record_release(&in[i]); + strbuf_release(&key); + reftable_record_release(&rec_out); + } +} + +static void test_u24_roundtrip(void) +{ + uint32_t in = 0x112233; + uint8_t dest[3]; + uint32_t out; + put_be24(dest, in); + out = get_be24(dest); + EXPECT(in == out); +} + +static void test_key_roundtrip(void) +{ + uint8_t buffer[1024] = { 0 }; + struct string_view dest = { + .buf = buffer, + .len = sizeof(buffer), + }; + struct strbuf last_key = STRBUF_INIT; + struct strbuf key = STRBUF_INIT; + struct strbuf roundtrip = STRBUF_INIT; + int restart; + uint8_t extra; + int n, m; + uint8_t rt_extra; + + strbuf_addstr(&last_key, "refs/heads/master"); + strbuf_addstr(&key, "refs/tags/bla"); + extra = 6; + n = reftable_encode_key(&restart, dest, last_key, key, extra); + EXPECT(!restart); + EXPECT(n > 0); + + m = reftable_decode_key(&roundtrip, &rt_extra, last_key, dest); + EXPECT(n == m); + EXPECT(0 == strbuf_cmp(&key, &roundtrip)); + EXPECT(rt_extra == extra); + + strbuf_release(&last_key); + strbuf_release(&key); + strbuf_release(&roundtrip); +} + +static void test_reftable_obj_record_roundtrip(void) +{ + uint8_t testHash1[GIT_SHA1_RAWSZ] = { 1, 2, 3, 4, 0 }; + uint64_t till9[] = { 1, 2, 3, 4, 500, 600, 700, 800, 9000 }; + struct reftable_obj_record recs[3] = { { + .hash_prefix = testHash1, + .hash_prefix_len = 5, + .offsets = till9, + .offset_len = 3, + }, + { + .hash_prefix = testHash1, + .hash_prefix_len = 5, + .offsets = till9, + .offset_len = 9, + }, + { + .hash_prefix = testHash1, + .hash_prefix_len = 5, + } }; + int i = 0; + for (i = 0; i < ARRAY_SIZE(recs); i++) { + struct reftable_obj_record in = recs[i]; + uint8_t buffer[1024] = { 0 }; + struct string_view dest = { + .buf = buffer, + .len = sizeof(buffer), + }; + struct reftable_record rec = { NULL }; + struct strbuf key = STRBUF_INIT; + struct reftable_obj_record out = { NULL }; + struct reftable_record rec_out = { NULL }; + int n, m; + uint8_t extra; + + reftable_record_from_obj(&rec, &in); + test_copy(&rec); + reftable_record_key(&rec, &key); + n = reftable_record_encode(&rec, dest, GIT_SHA1_RAWSZ); + EXPECT(n > 0); + extra = reftable_record_val_type(&rec); + reftable_record_from_obj(&rec_out, &out); + m = reftable_record_decode(&rec_out, key, extra, dest, + GIT_SHA1_RAWSZ); + EXPECT(n == m); + + EXPECT(in.hash_prefix_len == out.hash_prefix_len); + EXPECT(in.offset_len == out.offset_len); + + EXPECT(!memcmp(in.hash_prefix, out.hash_prefix, + in.hash_prefix_len)); + EXPECT(0 == memcmp(in.offsets, out.offsets, + sizeof(uint64_t) * in.offset_len)); + strbuf_release(&key); + reftable_record_release(&rec_out); + } +} + +static void test_reftable_index_record_roundtrip(void) +{ + struct reftable_index_record in = { + .offset = 42, + .last_key = STRBUF_INIT, + }; + uint8_t buffer[1024] = { 0 }; + struct string_view dest = { + .buf = buffer, + .len = sizeof(buffer), + }; + struct strbuf key = STRBUF_INIT; + struct reftable_record rec = { NULL }; + struct reftable_index_record out = { .last_key = STRBUF_INIT }; + struct reftable_record out_rec = { NULL }; + int n, m; + uint8_t extra; + + strbuf_addstr(&in.last_key, "refs/heads/master"); + reftable_record_from_index(&rec, &in); + reftable_record_key(&rec, &key); + test_copy(&rec); + + EXPECT(0 == strbuf_cmp(&key, &in.last_key)); + n = reftable_record_encode(&rec, dest, GIT_SHA1_RAWSZ); + EXPECT(n > 0); + + extra = reftable_record_val_type(&rec); + reftable_record_from_index(&out_rec, &out); + m = reftable_record_decode(&out_rec, key, extra, dest, GIT_SHA1_RAWSZ); + EXPECT(m == n); + + EXPECT(in.offset == out.offset); + + reftable_record_release(&out_rec); + strbuf_release(&key); + strbuf_release(&in.last_key); +} + +int record_test_main(int argc, const char *argv[]) +{ + RUN_TEST(test_reftable_log_record_equal); + RUN_TEST(test_reftable_log_record_roundtrip); + RUN_TEST(test_reftable_ref_record_roundtrip); + RUN_TEST(test_varint_roundtrip); + RUN_TEST(test_key_roundtrip); + RUN_TEST(test_common_prefix); + RUN_TEST(test_reftable_obj_record_roundtrip); + RUN_TEST(test_reftable_index_record_roundtrip); + RUN_TEST(test_u24_roundtrip); + return 0; +} diff --git a/reftable/refname.c b/reftable/refname.c new file mode 100644 index 0000000000..9573496932 --- /dev/null +++ b/reftable/refname.c @@ -0,0 +1,209 @@ +/* + Copyright 2020 Google LLC + + Use of this source code is governed by a BSD-style + license that can be found in the LICENSE file or at + https://developers.google.com/open-source/licenses/bsd +*/ + +#include "system.h" +#include "reftable-error.h" +#include "basics.h" +#include "refname.h" +#include "reftable-iterator.h" + +struct find_arg { + char **names; + const char *want; +}; + +static int find_name(size_t k, void *arg) +{ + struct find_arg *f_arg = arg; + return strcmp(f_arg->names[k], f_arg->want) >= 0; +} + +static int modification_has_ref(struct modification *mod, const char *name) +{ + struct reftable_ref_record ref = { NULL }; + int err = 0; + + if (mod->add_len > 0) { + struct find_arg arg = { + .names = mod->add, + .want = name, + }; + int idx = binsearch(mod->add_len, find_name, &arg); + if (idx < mod->add_len && !strcmp(mod->add[idx], name)) { + return 0; + } + } + + if (mod->del_len > 0) { + struct find_arg arg = { + .names = mod->del, + .want = name, + }; + int idx = binsearch(mod->del_len, find_name, &arg); + if (idx < mod->del_len && !strcmp(mod->del[idx], name)) { + return 1; + } + } + + err = reftable_table_read_ref(&mod->tab, name, &ref); + reftable_ref_record_release(&ref); + return err; +} + +static void modification_release(struct modification *mod) +{ + /* don't delete the strings themselves; they're owned by ref records. + */ + FREE_AND_NULL(mod->add); + FREE_AND_NULL(mod->del); + mod->add_len = 0; + mod->del_len = 0; +} + +static int modification_has_ref_with_prefix(struct modification *mod, + const char *prefix) +{ + struct reftable_iterator it = { NULL }; + struct reftable_ref_record ref = { NULL }; + int err = 0; + + if (mod->add_len > 0) { + struct find_arg arg = { + .names = mod->add, + .want = prefix, + }; + int idx = binsearch(mod->add_len, find_name, &arg); + if (idx < mod->add_len && + !strncmp(prefix, mod->add[idx], strlen(prefix))) + goto done; + } + err = reftable_table_seek_ref(&mod->tab, &it, prefix); + if (err) + goto done; + + while (1) { + err = reftable_iterator_next_ref(&it, &ref); + if (err) + goto done; + + if (mod->del_len > 0) { + struct find_arg arg = { + .names = mod->del, + .want = ref.refname, + }; + int idx = binsearch(mod->del_len, find_name, &arg); + if (idx < mod->del_len && + !strcmp(ref.refname, mod->del[idx])) { + continue; + } + } + + if (strncmp(ref.refname, prefix, strlen(prefix))) { + err = 1; + goto done; + } + err = 0; + goto done; + } + +done: + reftable_ref_record_release(&ref); + reftable_iterator_destroy(&it); + return err; +} + +static int validate_refname(const char *name) +{ + while (1) { + char *next = strchr(name, '/'); + if (!*name) { + return REFTABLE_REFNAME_ERROR; + } + if (!next) { + return 0; + } + if (next - name == 0 || (next - name == 1 && *name == '.') || + (next - name == 2 && name[0] == '.' && name[1] == '.')) + return REFTABLE_REFNAME_ERROR; + name = next + 1; + } + return 0; +} + +int validate_ref_record_addition(struct reftable_table tab, + struct reftable_ref_record *recs, size_t sz) +{ + struct modification mod = { + .tab = tab, + .add = reftable_calloc(sizeof(char *) * sz), + .del = reftable_calloc(sizeof(char *) * sz), + }; + int i = 0; + int err = 0; + for (; i < sz; i++) { + if (reftable_ref_record_is_deletion(&recs[i])) { + mod.del[mod.del_len++] = recs[i].refname; + } else { + mod.add[mod.add_len++] = recs[i].refname; + } + } + + err = modification_validate(&mod); + modification_release(&mod); + return err; +} + +static void strbuf_trim_component(struct strbuf *sl) +{ + while (sl->len > 0) { + int is_slash = (sl->buf[sl->len - 1] == '/'); + strbuf_setlen(sl, sl->len - 1); + if (is_slash) + break; + } +} + +int modification_validate(struct modification *mod) +{ + struct strbuf slashed = STRBUF_INIT; + int err = 0; + int i = 0; + for (; i < mod->add_len; i++) { + err = validate_refname(mod->add[i]); + if (err) + goto done; + strbuf_reset(&slashed); + strbuf_addstr(&slashed, mod->add[i]); + strbuf_addstr(&slashed, "/"); + + err = modification_has_ref_with_prefix(mod, slashed.buf); + if (err == 0) { + err = REFTABLE_NAME_CONFLICT; + goto done; + } + if (err < 0) + goto done; + + strbuf_reset(&slashed); + strbuf_addstr(&slashed, mod->add[i]); + while (slashed.len) { + strbuf_trim_component(&slashed); + err = modification_has_ref(mod, slashed.buf); + if (err == 0) { + err = REFTABLE_NAME_CONFLICT; + goto done; + } + if (err < 0) + goto done; + } + } + err = 0; +done: + strbuf_release(&slashed); + return err; +} diff --git a/reftable/refname.h b/reftable/refname.h new file mode 100644 index 0000000000..a24b40fcb4 --- /dev/null +++ b/reftable/refname.h @@ -0,0 +1,29 @@ +/* + Copyright 2020 Google LLC + + Use of this source code is governed by a BSD-style + license that can be found in the LICENSE file or at + https://developers.google.com/open-source/licenses/bsd +*/ +#ifndef REFNAME_H +#define REFNAME_H + +#include "reftable-record.h" +#include "reftable-generic.h" + +struct modification { + struct reftable_table tab; + + char **add; + size_t add_len; + + char **del; + size_t del_len; +}; + +int validate_ref_record_addition(struct reftable_table tab, + struct reftable_ref_record *recs, size_t sz); + +int modification_validate(struct modification *mod); + +#endif diff --git a/reftable/refname_test.c b/reftable/refname_test.c new file mode 100644 index 0000000000..8645cd93bb --- /dev/null +++ b/reftable/refname_test.c @@ -0,0 +1,102 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#include "basics.h" +#include "block.h" +#include "blocksource.h" +#include "constants.h" +#include "reader.h" +#include "record.h" +#include "refname.h" +#include "reftable-error.h" +#include "reftable-writer.h" +#include "system.h" + +#include "test_framework.h" +#include "reftable-tests.h" + +struct testcase { + char *add; + char *del; + int error_code; +}; + +static void test_conflict(void) +{ + struct reftable_write_options opts = { 0 }; + struct strbuf buf = STRBUF_INIT; + struct reftable_writer *w = + reftable_new_writer(&strbuf_add_void, &buf, &opts); + struct reftable_ref_record rec = { + .refname = "a/b", + .value_type = REFTABLE_REF_SYMREF, + .value.symref = "destination", /* make sure it's not a symref. + */ + .update_index = 1, + }; + int err; + int i; + struct reftable_block_source source = { NULL }; + struct reftable_reader *rd = NULL; + struct reftable_table tab = { NULL }; + struct testcase cases[] = { + { "a/b/c", NULL, REFTABLE_NAME_CONFLICT }, + { "b", NULL, 0 }, + { "a", NULL, REFTABLE_NAME_CONFLICT }, + { "a", "a/b", 0 }, + + { "p/", NULL, REFTABLE_REFNAME_ERROR }, + { "p//q", NULL, REFTABLE_REFNAME_ERROR }, + { "p/./q", NULL, REFTABLE_REFNAME_ERROR }, + { "p/../q", NULL, REFTABLE_REFNAME_ERROR }, + + { "a/b/c", "a/b", 0 }, + { NULL, "a//b", 0 }, + }; + reftable_writer_set_limits(w, 1, 1); + + err = reftable_writer_add_ref(w, &rec); + EXPECT_ERR(err); + + err = reftable_writer_close(w); + EXPECT_ERR(err); + reftable_writer_free(w); + + block_source_from_strbuf(&source, &buf); + err = reftable_new_reader(&rd, &source, "filename"); + EXPECT_ERR(err); + + reftable_table_from_reader(&tab, rd); + + for (i = 0; i < ARRAY_SIZE(cases); i++) { + struct modification mod = { + .tab = tab, + }; + + if (cases[i].add) { + mod.add = &cases[i].add; + mod.add_len = 1; + } + if (cases[i].del) { + mod.del = &cases[i].del; + mod.del_len = 1; + } + + err = modification_validate(&mod); + EXPECT(err == cases[i].error_code); + } + + reftable_reader_free(rd); + strbuf_release(&buf); +} + +int refname_test_main(int argc, const char *argv[]) +{ + RUN_TEST(test_conflict); + return 0; +} diff --git a/reftable/reftable-blocksource.h b/reftable/reftable-blocksource.h new file mode 100644 index 0000000000..5aa3990a57 --- /dev/null +++ b/reftable/reftable-blocksource.h @@ -0,0 +1,49 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#ifndef REFTABLE_BLOCKSOURCE_H +#define REFTABLE_BLOCKSOURCE_H + +#include <stdint.h> + +/* block_source is a generic wrapper for a seekable readable file. + */ +struct reftable_block_source { + struct reftable_block_source_vtable *ops; + void *arg; +}; + +/* a contiguous segment of bytes. It keeps track of its generating block_source + * so it can return itself into the pool. */ +struct reftable_block { + uint8_t *data; + int len; + struct reftable_block_source source; +}; + +/* block_source_vtable are the operations that make up block_source */ +struct reftable_block_source_vtable { + /* returns the size of a block source */ + uint64_t (*size)(void *source); + + /* reads a segment from the block source. It is an error to read + beyond the end of the block */ + int (*read_block)(void *source, struct reftable_block *dest, + uint64_t off, uint32_t size); + /* mark the block as read; may return the data back to malloc */ + void (*return_block)(void *source, struct reftable_block *blockp); + + /* release all resources associated with the block source */ + void (*close)(void *source); +}; + +/* opens a file on the file system as a block_source */ +int reftable_block_source_from_file(struct reftable_block_source *block_src, + const char *name); + +#endif diff --git a/reftable/reftable-error.h b/reftable/reftable-error.h new file mode 100644 index 0000000000..6f89bedf1a --- /dev/null +++ b/reftable/reftable-error.h @@ -0,0 +1,62 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#ifndef REFTABLE_ERROR_H +#define REFTABLE_ERROR_H + +/* + * Errors in reftable calls are signaled with negative integer return values. 0 + * means success. + */ +enum reftable_error { + /* Unexpected file system behavior */ + REFTABLE_IO_ERROR = -2, + + /* Format inconsistency on reading data */ + REFTABLE_FORMAT_ERROR = -3, + + /* File does not exist. Returned from block_source_from_file(), because + * it needs special handling in stack. + */ + REFTABLE_NOT_EXIST_ERROR = -4, + + /* Trying to write out-of-date data. */ + REFTABLE_LOCK_ERROR = -5, + + /* Misuse of the API: + * - on writing a record with NULL refname. + * - on writing a reftable_ref_record outside the table limits + * - on writing a ref or log record before the stack's + * next_update_inde*x + * - on writing a log record with multiline message with + * exact_log_message unset + * - on reading a reftable_ref_record from log iterator, or vice versa. + * + * When a call misuses the API, the internal state of the library is + * kept unchanged. + */ + REFTABLE_API_ERROR = -6, + + /* Decompression error */ + REFTABLE_ZLIB_ERROR = -7, + + /* Wrote a table without blocks. */ + REFTABLE_EMPTY_TABLE_ERROR = -8, + + /* Dir/file conflict. */ + REFTABLE_NAME_CONFLICT = -9, + + /* Invalid ref name. */ + REFTABLE_REFNAME_ERROR = -10, +}; + +/* convert the numeric error code to a string. The string should not be + * deallocated. */ +const char *reftable_error_str(int err); + +#endif diff --git a/reftable/reftable-generic.h b/reftable/reftable-generic.h new file mode 100644 index 0000000000..d239751a77 --- /dev/null +++ b/reftable/reftable-generic.h @@ -0,0 +1,47 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#ifndef REFTABLE_GENERIC_H +#define REFTABLE_GENERIC_H + +#include "reftable-iterator.h" + +struct reftable_table_vtable; + +/* + * Provides a unified API for reading tables, either merged tables, or single + * readers. */ +struct reftable_table { + struct reftable_table_vtable *ops; + void *table_arg; +}; + +int reftable_table_seek_log(struct reftable_table *tab, + struct reftable_iterator *it, const char *name); + +int reftable_table_seek_ref(struct reftable_table *tab, + struct reftable_iterator *it, const char *name); + +/* returns the hash ID from a generic reftable_table */ +uint32_t reftable_table_hash_id(struct reftable_table *tab); + +/* returns the max update_index covered by this table. */ +uint64_t reftable_table_max_update_index(struct reftable_table *tab); + +/* returns the min update_index covered by this table. */ +uint64_t reftable_table_min_update_index(struct reftable_table *tab); + +/* convenience function to read a single ref. Returns < 0 for error, 0 + for success, and 1 if ref not found. */ +int reftable_table_read_ref(struct reftable_table *tab, const char *name, + struct reftable_ref_record *ref); + +/* dump table contents onto stdout for debugging */ +int reftable_table_print(struct reftable_table *tab); + +#endif diff --git a/reftable/reftable-iterator.h b/reftable/reftable-iterator.h new file mode 100644 index 0000000000..d3eee7af35 --- /dev/null +++ b/reftable/reftable-iterator.h @@ -0,0 +1,39 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#ifndef REFTABLE_ITERATOR_H +#define REFTABLE_ITERATOR_H + +#include "reftable-record.h" + +struct reftable_iterator_vtable; + +/* iterator is the generic interface for walking over data stored in a + * reftable. + */ +struct reftable_iterator { + struct reftable_iterator_vtable *ops; + void *iter_arg; +}; + +/* reads the next reftable_ref_record. Returns < 0 for error, 0 for OK and > 0: + * end of iteration. + */ +int reftable_iterator_next_ref(struct reftable_iterator *it, + struct reftable_ref_record *ref); + +/* reads the next reftable_log_record. Returns < 0 for error, 0 for OK and > 0: + * end of iteration. + */ +int reftable_iterator_next_log(struct reftable_iterator *it, + struct reftable_log_record *log); + +/* releases resources associated with an iterator. */ +void reftable_iterator_destroy(struct reftable_iterator *it); + +#endif diff --git a/reftable/reftable-malloc.h b/reftable/reftable-malloc.h new file mode 100644 index 0000000000..5f2185f1f3 --- /dev/null +++ b/reftable/reftable-malloc.h @@ -0,0 +1,18 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#ifndef REFTABLE_H +#define REFTABLE_H + +#include <stddef.h> + +/* Overrides the functions to use for memory management. */ +void reftable_set_alloc(void *(*malloc)(size_t), + void *(*realloc)(void *, size_t), void (*free)(void *)); + +#endif diff --git a/reftable/reftable-merged.h b/reftable/reftable-merged.h new file mode 100644 index 0000000000..1a6d16915a --- /dev/null +++ b/reftable/reftable-merged.h @@ -0,0 +1,72 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#ifndef REFTABLE_MERGED_H +#define REFTABLE_MERGED_H + +#include "reftable-iterator.h" + +/* + * Merged tables + * + * A ref database kept in a sequence of table files. The merged_table presents a + * unified view to reading (seeking, iterating) a sequence of immutable tables. + * + * The merged tables are on purpose kept disconnected from their actual storage + * (eg. files on disk), because it is useful to merge tables aren't files. For + * example, the per-workspace and global ref namespace can be implemented as a + * merged table of two stacks of file-backed reftables. + */ + +/* A merged table is implements seeking/iterating over a stack of tables. */ +struct reftable_merged_table; + +/* A generic reftable; see below. */ +struct reftable_table; + +/* reftable_new_merged_table creates a new merged table. It takes ownership of + the stack array. +*/ +int reftable_new_merged_table(struct reftable_merged_table **dest, + struct reftable_table *stack, int n, + uint32_t hash_id); + +/* returns an iterator positioned just before 'name' */ +int reftable_merged_table_seek_ref(struct reftable_merged_table *mt, + struct reftable_iterator *it, + const char *name); + +/* returns an iterator for log entry, at given update_index */ +int reftable_merged_table_seek_log_at(struct reftable_merged_table *mt, + struct reftable_iterator *it, + const char *name, uint64_t update_index); + +/* like reftable_merged_table_seek_log_at but look for the newest entry. */ +int reftable_merged_table_seek_log(struct reftable_merged_table *mt, + struct reftable_iterator *it, + const char *name); + +/* returns the max update_index covered by this merged table. */ +uint64_t +reftable_merged_table_max_update_index(struct reftable_merged_table *mt); + +/* returns the min update_index covered by this merged table. */ +uint64_t +reftable_merged_table_min_update_index(struct reftable_merged_table *mt); + +/* releases memory for the merged_table */ +void reftable_merged_table_free(struct reftable_merged_table *m); + +/* return the hash ID of the merged table. */ +uint32_t reftable_merged_table_hash_id(struct reftable_merged_table *m); + +/* create a generic table from reftable_merged_table */ +void reftable_table_from_merged_table(struct reftable_table *tab, + struct reftable_merged_table *table); + +#endif diff --git a/reftable/reftable-reader.h b/reftable/reftable-reader.h new file mode 100644 index 0000000000..4a4bc2fdf8 --- /dev/null +++ b/reftable/reftable-reader.h @@ -0,0 +1,101 @@ +/* + Copyright 2020 Google LLC + + Use of this source code is governed by a BSD-style + license that can be found in the LICENSE file or at + https://developers.google.com/open-source/licenses/bsd +*/ + +#ifndef REFTABLE_READER_H +#define REFTABLE_READER_H + +#include "reftable-iterator.h" +#include "reftable-blocksource.h" + +/* + * Reading single tables + * + * The follow routines are for reading single files. For an + * application-level interface, skip ahead to struct + * reftable_merged_table and struct reftable_stack. + */ + +/* The reader struct is a handle to an open reftable file. */ +struct reftable_reader; + +/* Generic table. */ +struct reftable_table; + +/* reftable_new_reader opens a reftable for reading. If successful, + * returns 0 code and sets pp. The name is used for creating a + * stack. Typically, it is the basename of the file. The block source + * `src` is owned by the reader, and is closed on calling + * reftable_reader_destroy(). On error, the block source `src` is + * closed as well. + */ +int reftable_new_reader(struct reftable_reader **pp, + struct reftable_block_source *src, const char *name); + +/* reftable_reader_seek_ref returns an iterator where 'name' would be inserted + in the table. To seek to the start of the table, use name = "". + + example: + + struct reftable_reader *r = NULL; + int err = reftable_new_reader(&r, &src, "filename"); + if (err < 0) { ... } + struct reftable_iterator it = {0}; + err = reftable_reader_seek_ref(r, &it, "refs/heads/master"); + if (err < 0) { ... } + struct reftable_ref_record ref = {0}; + while (1) { + err = reftable_iterator_next_ref(&it, &ref); + if (err > 0) { + break; + } + if (err < 0) { + ..error handling.. + } + ..found.. + } + reftable_iterator_destroy(&it); + reftable_ref_record_release(&ref); +*/ +int reftable_reader_seek_ref(struct reftable_reader *r, + struct reftable_iterator *it, const char *name); + +/* returns the hash ID used in this table. */ +uint32_t reftable_reader_hash_id(struct reftable_reader *r); + +/* seek to logs for the given name, older than update_index. To seek to the + start of the table, use name = "". +*/ +int reftable_reader_seek_log_at(struct reftable_reader *r, + struct reftable_iterator *it, const char *name, + uint64_t update_index); + +/* seek to newest log entry for given name. */ +int reftable_reader_seek_log(struct reftable_reader *r, + struct reftable_iterator *it, const char *name); + +/* closes and deallocates a reader. */ +void reftable_reader_free(struct reftable_reader *); + +/* return an iterator for the refs pointing to `oid`. */ +int reftable_reader_refs_for(struct reftable_reader *r, + struct reftable_iterator *it, uint8_t *oid); + +/* return the max_update_index for a table */ +uint64_t reftable_reader_max_update_index(struct reftable_reader *r); + +/* return the min_update_index for a table */ +uint64_t reftable_reader_min_update_index(struct reftable_reader *r); + +/* creates a generic table from a file reader. */ +void reftable_table_from_reader(struct reftable_table *tab, + struct reftable_reader *reader); + +/* print table onto stdout for debugging. */ +int reftable_reader_print_file(const char *tablename); + +#endif diff --git a/reftable/reftable-record.h b/reftable/reftable-record.h new file mode 100644 index 0000000000..5370d2288c --- /dev/null +++ b/reftable/reftable-record.h @@ -0,0 +1,114 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#ifndef REFTABLE_RECORD_H +#define REFTABLE_RECORD_H + +#include <stdint.h> + +/* + * Basic data types + * + * Reftables store the state of each ref in struct reftable_ref_record, and they + * store a sequence of reflog updates in struct reftable_log_record. + */ + +/* reftable_ref_record holds a ref database entry target_value */ +struct reftable_ref_record { + char *refname; /* Name of the ref, malloced. */ + uint64_t update_index; /* Logical timestamp at which this value is + * written */ + + enum { + /* tombstone to hide deletions from earlier tables */ + REFTABLE_REF_DELETION = 0x0, + + /* a simple ref */ + REFTABLE_REF_VAL1 = 0x1, + /* a tag, plus its peeled hash */ + REFTABLE_REF_VAL2 = 0x2, + + /* a symbolic reference */ + REFTABLE_REF_SYMREF = 0x3, +#define REFTABLE_NR_REF_VALUETYPES 4 + } value_type; + union { + uint8_t *val1; /* malloced hash. */ + struct { + uint8_t *value; /* first value, malloced hash */ + uint8_t *target_value; /* second value, malloced hash */ + } val2; + char *symref; /* referent, malloced 0-terminated string */ + } value; +}; + +/* Returns the first hash, or NULL if `rec` is not of type + * REFTABLE_REF_VAL1 or REFTABLE_REF_VAL2. */ +uint8_t *reftable_ref_record_val1(struct reftable_ref_record *rec); + +/* Returns the second hash, or NULL if `rec` is not of type + * REFTABLE_REF_VAL2. */ +uint8_t *reftable_ref_record_val2(struct reftable_ref_record *rec); + +/* returns whether 'ref' represents a deletion */ +int reftable_ref_record_is_deletion(const struct reftable_ref_record *ref); + +/* prints a reftable_ref_record onto stdout. Useful for debugging. */ +void reftable_ref_record_print(struct reftable_ref_record *ref, + uint32_t hash_id); + +/* frees and nulls all pointer values inside `ref`. */ +void reftable_ref_record_release(struct reftable_ref_record *ref); + +/* returns whether two reftable_ref_records are the same. Useful for testing. */ +int reftable_ref_record_equal(struct reftable_ref_record *a, + struct reftable_ref_record *b, int hash_size); + +/* reftable_log_record holds a reflog entry */ +struct reftable_log_record { + char *refname; + uint64_t update_index; /* logical timestamp of a transactional update. + */ + + enum { + /* tombstone to hide deletions from earlier tables */ + REFTABLE_LOG_DELETION = 0x0, + + /* a simple update */ + REFTABLE_LOG_UPDATE = 0x1, +#define REFTABLE_NR_LOG_VALUETYPES 2 + } value_type; + + union { + struct { + uint8_t *new_hash; + uint8_t *old_hash; + char *name; + char *email; + uint64_t time; + int16_t tz_offset; + char *message; + } update; + } value; +}; + +/* returns whether 'ref' represents the deletion of a log record. */ +int reftable_log_record_is_deletion(const struct reftable_log_record *log); + +/* frees and nulls all pointer values. */ +void reftable_log_record_release(struct reftable_log_record *log); + +/* returns whether two records are equal. Useful for testing. */ +int reftable_log_record_equal(struct reftable_log_record *a, + struct reftable_log_record *b, int hash_size); + +/* dumps a reftable_log_record on stdout, for debugging/testing. */ +void reftable_log_record_print(struct reftable_log_record *log, + uint32_t hash_id); + +#endif diff --git a/reftable/reftable-stack.h b/reftable/reftable-stack.h new file mode 100644 index 0000000000..1b602dda58 --- /dev/null +++ b/reftable/reftable-stack.h @@ -0,0 +1,128 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#ifndef REFTABLE_STACK_H +#define REFTABLE_STACK_H + +#include "reftable-writer.h" + +/* + * The stack presents an interface to a mutable sequence of reftables. + + * A stack can be mutated by pushing a table to the top of the stack. + + * The reftable_stack automatically compacts files on disk to ensure good + * amortized performance. + * + * For windows and other platforms that cannot have open files as rename + * destinations, concurrent access from multiple processes needs the rand() + * random seed to be randomized. + */ +struct reftable_stack; + +/* open a new reftable stack. The tables along with the table list will be + * stored in 'dir'. Typically, this should be .git/reftables. + */ +int reftable_new_stack(struct reftable_stack **dest, const char *dir, + struct reftable_write_options config); + +/* returns the update_index at which a next table should be written. */ +uint64_t reftable_stack_next_update_index(struct reftable_stack *st); + +/* holds a transaction to add tables at the top of a stack. */ +struct reftable_addition; + +/* + * returns a new transaction to add reftables to the given stack. As a side + * effect, the ref database is locked. + */ +int reftable_stack_new_addition(struct reftable_addition **dest, + struct reftable_stack *st); + +/* Adds a reftable to transaction. */ +int reftable_addition_add(struct reftable_addition *add, + int (*write_table)(struct reftable_writer *wr, + void *arg), + void *arg); + +/* Commits the transaction, releasing the lock. After calling this, + * reftable_addition_destroy should still be called. + */ +int reftable_addition_commit(struct reftable_addition *add); + +/* Release all non-committed data from the transaction, and deallocate the + * transaction. Releases the lock if held. */ +void reftable_addition_destroy(struct reftable_addition *add); + +/* add a new table to the stack. The write_table function must call + * reftable_writer_set_limits, add refs and return an error value. */ +int reftable_stack_add(struct reftable_stack *st, + int (*write_table)(struct reftable_writer *wr, + void *write_arg), + void *write_arg); + +/* returns the merged_table for seeking. This table is valid until the + * next write or reload, and should not be closed or deleted. + */ +struct reftable_merged_table * +reftable_stack_merged_table(struct reftable_stack *st); + +/* frees all resources associated with the stack. */ +void reftable_stack_destroy(struct reftable_stack *st); + +/* Reloads the stack if necessary. This is very cheap to run if the stack was up + * to date */ +int reftable_stack_reload(struct reftable_stack *st); + +/* Policy for expiring reflog entries. */ +struct reftable_log_expiry_config { + /* Drop entries older than this timestamp */ + uint64_t time; + + /* Drop older entries */ + uint64_t min_update_index; +}; + +/* compacts all reftables into a giant table. Expire reflog entries if config is + * non-NULL */ +int reftable_stack_compact_all(struct reftable_stack *st, + struct reftable_log_expiry_config *config); + +/* heuristically compact unbalanced table stack. */ +int reftable_stack_auto_compact(struct reftable_stack *st); + +/* delete stale .ref tables. */ +int reftable_stack_clean(struct reftable_stack *st); + +/* convenience function to read a single ref. Returns < 0 for error, 0 for + * success, and 1 if ref not found. */ +int reftable_stack_read_ref(struct reftable_stack *st, const char *refname, + struct reftable_ref_record *ref); + +/* convenience function to read a single log. Returns < 0 for error, 0 for + * success, and 1 if ref not found. */ +int reftable_stack_read_log(struct reftable_stack *st, const char *refname, + struct reftable_log_record *log); + +/* statistics on past compactions. */ +struct reftable_compaction_stats { + uint64_t bytes; /* total number of bytes written */ + uint64_t entries_written; /* total number of entries written, including + failures. */ + int attempts; /* how often we tried to compact */ + int failures; /* failures happen on concurrent updates */ +}; + +/* return statistics for compaction up till now. */ +struct reftable_compaction_stats * +reftable_stack_compaction_stats(struct reftable_stack *st); + +/* print the entire stack represented by the directory */ +int reftable_stack_print_directory(const char *stackdir, uint32_t hash_id); + +#endif diff --git a/reftable/reftable-tests.h b/reftable/reftable-tests.h new file mode 100644 index 0000000000..0019cbcfa4 --- /dev/null +++ b/reftable/reftable-tests.h @@ -0,0 +1,23 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#ifndef REFTABLE_TESTS_H +#define REFTABLE_TESTS_H + +int basics_test_main(int argc, const char **argv); +int block_test_main(int argc, const char **argv); +int merged_test_main(int argc, const char **argv); +int pq_test_main(int argc, const char **argv); +int record_test_main(int argc, const char **argv); +int refname_test_main(int argc, const char **argv); +int readwrite_test_main(int argc, const char **argv); +int stack_test_main(int argc, const char **argv); +int tree_test_main(int argc, const char **argv); +int reftable_dump_main(int argc, char *const *argv); + +#endif diff --git a/reftable/reftable-writer.h b/reftable/reftable-writer.h new file mode 100644 index 0000000000..af36462ced --- /dev/null +++ b/reftable/reftable-writer.h @@ -0,0 +1,148 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#ifndef REFTABLE_WRITER_H +#define REFTABLE_WRITER_H + +#include "reftable-record.h" + +#include <stdint.h> +#include <unistd.h> /* ssize_t */ + +/* Writing single reftables */ + +/* reftable_write_options sets options for writing a single reftable. */ +struct reftable_write_options { + /* boolean: do not pad out blocks to block size. */ + unsigned unpadded : 1; + + /* the blocksize. Should be less than 2^24. */ + uint32_t block_size; + + /* boolean: do not generate a SHA1 => ref index. */ + unsigned skip_index_objects : 1; + + /* how often to write complete keys in each block. */ + int restart_interval; + + /* 4-byte identifier ("sha1", "s256") of the hash. + * Defaults to SHA1 if unset + */ + uint32_t hash_id; + + /* boolean: do not check ref names for validity or dir/file conflicts. + */ + unsigned skip_name_check : 1; + + /* boolean: copy log messages exactly. If unset, check that the message + * is a single line, and add '\n' if missing. + */ + unsigned exact_log_message : 1; +}; + +/* reftable_block_stats holds statistics for a single block type */ +struct reftable_block_stats { + /* total number of entries written */ + int entries; + /* total number of key restarts */ + int restarts; + /* total number of blocks */ + int blocks; + /* total number of index blocks */ + int index_blocks; + /* depth of the index */ + int max_index_level; + + /* offset of the first block for this type */ + uint64_t offset; + /* offset of the top level index block for this type, or 0 if not + * present */ + uint64_t index_offset; +}; + +/* stats holds overall statistics for a single reftable */ +struct reftable_stats { + /* total number of blocks written. */ + int blocks; + /* stats for ref data */ + struct reftable_block_stats ref_stats; + /* stats for the SHA1 to ref map. */ + struct reftable_block_stats obj_stats; + /* stats for index blocks */ + struct reftable_block_stats idx_stats; + /* stats for log blocks */ + struct reftable_block_stats log_stats; + + /* disambiguation length of shortened object IDs. */ + int object_id_len; +}; + +/* reftable_new_writer creates a new writer */ +struct reftable_writer * +reftable_new_writer(ssize_t (*writer_func)(void *, const void *, size_t), + void *writer_arg, struct reftable_write_options *opts); + +/* Set the range of update indices for the records we will add. When writing a + table into a stack, the min should be at least + reftable_stack_next_update_index(), or REFTABLE_API_ERROR is returned. + + For transactional updates to a stack, typically min==max, and the + update_index can be obtained by inspeciting the stack. When converting an + existing ref database into a single reftable, this would be a range of + update-index timestamps. + */ +void reftable_writer_set_limits(struct reftable_writer *w, uint64_t min, + uint64_t max); + +/* + Add a reftable_ref_record. The record should have names that come after + already added records. + + The update_index must be within the limits set by + reftable_writer_set_limits(), or REFTABLE_API_ERROR is returned. It is an + REFTABLE_API_ERROR error to write a ref record after a log record. +*/ +int reftable_writer_add_ref(struct reftable_writer *w, + struct reftable_ref_record *ref); + +/* + Convenience function to add multiple reftable_ref_records; the function sorts + the records before adding them, reordering the records array passed in. +*/ +int reftable_writer_add_refs(struct reftable_writer *w, + struct reftable_ref_record *refs, int n); + +/* + adds reftable_log_records. Log records are keyed by (refname, decreasing + update_index). The key for the record added must come after the already added + log records. +*/ +int reftable_writer_add_log(struct reftable_writer *w, + struct reftable_log_record *log); + +/* + Convenience function to add multiple reftable_log_records; the function sorts + the records before adding them, reordering records array passed in. +*/ +int reftable_writer_add_logs(struct reftable_writer *w, + struct reftable_log_record *logs, int n); + +/* reftable_writer_close finalizes the reftable. The writer is retained so + * statistics can be inspected. */ +int reftable_writer_close(struct reftable_writer *w); + +/* writer_stats returns the statistics on the reftable being written. + + This struct becomes invalid when the writer is freed. + */ +const struct reftable_stats *writer_stats(struct reftable_writer *w); + +/* reftable_writer_free deallocates memory for the writer */ +void reftable_writer_free(struct reftable_writer *w); + +#endif diff --git a/reftable/reftable.c b/reftable/reftable.c new file mode 100644 index 0000000000..0e4607a7cd --- /dev/null +++ b/reftable/reftable.c @@ -0,0 +1,115 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#include "basics.h" +#include "record.h" +#include "generic.h" +#include "reftable-iterator.h" +#include "reftable-generic.h" + +int reftable_table_seek_ref(struct reftable_table *tab, + struct reftable_iterator *it, const char *name) +{ + struct reftable_ref_record ref = { + .refname = (char *)name, + }; + struct reftable_record rec = { NULL }; + reftable_record_from_ref(&rec, &ref); + return tab->ops->seek_record(tab->table_arg, it, &rec); +} + +int reftable_table_read_ref(struct reftable_table *tab, const char *name, + struct reftable_ref_record *ref) +{ + struct reftable_iterator it = { NULL }; + int err = reftable_table_seek_ref(tab, &it, name); + if (err) + goto done; + + err = reftable_iterator_next_ref(&it, ref); + if (err) + goto done; + + if (strcmp(ref->refname, name) || + reftable_ref_record_is_deletion(ref)) { + reftable_ref_record_release(ref); + err = 1; + goto done; + } + +done: + reftable_iterator_destroy(&it); + return err; +} + +uint64_t reftable_table_max_update_index(struct reftable_table *tab) +{ + return tab->ops->max_update_index(tab->table_arg); +} + +uint64_t reftable_table_min_update_index(struct reftable_table *tab) +{ + return tab->ops->min_update_index(tab->table_arg); +} + +uint32_t reftable_table_hash_id(struct reftable_table *tab) +{ + return tab->ops->hash_id(tab->table_arg); +} + +void reftable_iterator_destroy(struct reftable_iterator *it) +{ + if (!it->ops) { + return; + } + it->ops->close(it->iter_arg); + it->ops = NULL; + FREE_AND_NULL(it->iter_arg); +} + +int reftable_iterator_next_ref(struct reftable_iterator *it, + struct reftable_ref_record *ref) +{ + struct reftable_record rec = { NULL }; + reftable_record_from_ref(&rec, ref); + return iterator_next(it, &rec); +} + +int reftable_iterator_next_log(struct reftable_iterator *it, + struct reftable_log_record *log) +{ + struct reftable_record rec = { NULL }; + reftable_record_from_log(&rec, log); + return iterator_next(it, &rec); +} + +int iterator_next(struct reftable_iterator *it, struct reftable_record *rec) +{ + return it->ops->next(it->iter_arg, rec); +} + +static int empty_iterator_next(void *arg, struct reftable_record *rec) +{ + return 1; +} + +static void empty_iterator_close(void *arg) +{ +} + +static struct reftable_iterator_vtable empty_vtable = { + .next = &empty_iterator_next, + .close = &empty_iterator_close, +}; + +void iterator_set_empty(struct reftable_iterator *it) +{ + assert(!it->ops); + it->iter_arg = NULL; + it->ops = &empty_vtable; +} diff --git a/reftable/stack.c b/reftable/stack.c new file mode 100644 index 0000000000..df5021ebf0 --- /dev/null +++ b/reftable/stack.c @@ -0,0 +1,1396 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#include "stack.h" + +#include "system.h" +#include "merged.h" +#include "reader.h" +#include "refname.h" +#include "reftable-error.h" +#include "reftable-record.h" +#include "reftable-merged.h" +#include "writer.h" + +static int stack_try_add(struct reftable_stack *st, + int (*write_table)(struct reftable_writer *wr, + void *arg), + void *arg); +static int stack_write_compact(struct reftable_stack *st, + struct reftable_writer *wr, int first, int last, + struct reftable_log_expiry_config *config); +static int stack_check_addition(struct reftable_stack *st, + const char *new_tab_name); +static void reftable_addition_close(struct reftable_addition *add); +static int reftable_stack_reload_maybe_reuse(struct reftable_stack *st, + int reuse_open); + +static void stack_filename(struct strbuf *dest, struct reftable_stack *st, + const char *name) +{ + strbuf_reset(dest); + strbuf_addstr(dest, st->reftable_dir); + strbuf_addstr(dest, "/"); + strbuf_addstr(dest, name); +} + +static ssize_t reftable_fd_write(void *arg, const void *data, size_t sz) +{ + int *fdp = (int *)arg; + return write(*fdp, data, sz); +} + +int reftable_new_stack(struct reftable_stack **dest, const char *dir, + struct reftable_write_options config) +{ + struct reftable_stack *p = + reftable_calloc(sizeof(struct reftable_stack)); + struct strbuf list_file_name = STRBUF_INIT; + int err = 0; + + if (config.hash_id == 0) { + config.hash_id = GIT_SHA1_FORMAT_ID; + } + + *dest = NULL; + + strbuf_reset(&list_file_name); + strbuf_addstr(&list_file_name, dir); + strbuf_addstr(&list_file_name, "/tables.list"); + + p->list_file = strbuf_detach(&list_file_name, NULL); + p->reftable_dir = xstrdup(dir); + p->config = config; + + err = reftable_stack_reload_maybe_reuse(p, 1); + if (err < 0) { + reftable_stack_destroy(p); + } else { + *dest = p; + } + return err; +} + +static int fd_read_lines(int fd, char ***namesp) +{ + off_t size = lseek(fd, 0, SEEK_END); + char *buf = NULL; + int err = 0; + if (size < 0) { + err = REFTABLE_IO_ERROR; + goto done; + } + err = lseek(fd, 0, SEEK_SET); + if (err < 0) { + err = REFTABLE_IO_ERROR; + goto done; + } + + buf = reftable_malloc(size + 1); + if (read(fd, buf, size) != size) { + err = REFTABLE_IO_ERROR; + goto done; + } + buf[size] = 0; + + parse_names(buf, size, namesp); + +done: + reftable_free(buf); + return err; +} + +int read_lines(const char *filename, char ***namesp) +{ + int fd = open(filename, O_RDONLY); + int err = 0; + if (fd < 0) { + if (errno == ENOENT) { + *namesp = reftable_calloc(sizeof(char *)); + return 0; + } + + return REFTABLE_IO_ERROR; + } + err = fd_read_lines(fd, namesp); + close(fd); + return err; +} + +struct reftable_merged_table * +reftable_stack_merged_table(struct reftable_stack *st) +{ + return st->merged; +} + +static int has_name(char **names, const char *name) +{ + while (*names) { + if (!strcmp(*names, name)) + return 1; + names++; + } + return 0; +} + +/* Close and free the stack */ +void reftable_stack_destroy(struct reftable_stack *st) +{ + char **names = NULL; + int err = 0; + if (st->merged) { + reftable_merged_table_free(st->merged); + st->merged = NULL; + } + + err = read_lines(st->list_file, &names); + if (err < 0) { + FREE_AND_NULL(names); + } + + if (st->readers) { + int i = 0; + struct strbuf filename = STRBUF_INIT; + for (i = 0; i < st->readers_len; i++) { + const char *name = reader_name(st->readers[i]); + strbuf_reset(&filename); + if (names && !has_name(names, name)) { + stack_filename(&filename, st, name); + } + reftable_reader_free(st->readers[i]); + + if (filename.len) { + /* On Windows, can only unlink after closing. */ + unlink(filename.buf); + } + } + strbuf_release(&filename); + st->readers_len = 0; + FREE_AND_NULL(st->readers); + } + FREE_AND_NULL(st->list_file); + FREE_AND_NULL(st->reftable_dir); + reftable_free(st); + free_names(names); +} + +static struct reftable_reader **stack_copy_readers(struct reftable_stack *st, + int cur_len) +{ + struct reftable_reader **cur = + reftable_calloc(sizeof(struct reftable_reader *) * cur_len); + int i = 0; + for (i = 0; i < cur_len; i++) { + cur[i] = st->readers[i]; + } + return cur; +} + +static int reftable_stack_reload_once(struct reftable_stack *st, char **names, + int reuse_open) +{ + int cur_len = !st->merged ? 0 : st->merged->stack_len; + struct reftable_reader **cur = stack_copy_readers(st, cur_len); + int err = 0; + int names_len = names_length(names); + struct reftable_reader **new_readers = + reftable_calloc(sizeof(struct reftable_reader *) * names_len); + struct reftable_table *new_tables = + reftable_calloc(sizeof(struct reftable_table) * names_len); + int new_readers_len = 0; + struct reftable_merged_table *new_merged = NULL; + int i; + + while (*names) { + struct reftable_reader *rd = NULL; + char *name = *names++; + + /* this is linear; we assume compaction keeps the number of + tables under control so this is not quadratic. */ + int j = 0; + for (j = 0; reuse_open && j < cur_len; j++) { + if (cur[j] && 0 == strcmp(cur[j]->name, name)) { + rd = cur[j]; + cur[j] = NULL; + break; + } + } + + if (!rd) { + struct reftable_block_source src = { NULL }; + struct strbuf table_path = STRBUF_INIT; + stack_filename(&table_path, st, name); + + err = reftable_block_source_from_file(&src, + table_path.buf); + strbuf_release(&table_path); + + if (err < 0) + goto done; + + err = reftable_new_reader(&rd, &src, name); + if (err < 0) + goto done; + } + + new_readers[new_readers_len] = rd; + reftable_table_from_reader(&new_tables[new_readers_len], rd); + new_readers_len++; + } + + /* success! */ + err = reftable_new_merged_table(&new_merged, new_tables, + new_readers_len, st->config.hash_id); + if (err < 0) + goto done; + + new_tables = NULL; + st->readers_len = new_readers_len; + if (st->merged) { + merged_table_release(st->merged); + reftable_merged_table_free(st->merged); + } + if (st->readers) { + reftable_free(st->readers); + } + st->readers = new_readers; + new_readers = NULL; + new_readers_len = 0; + + new_merged->suppress_deletions = 1; + st->merged = new_merged; + for (i = 0; i < cur_len; i++) { + if (cur[i]) { + const char *name = reader_name(cur[i]); + struct strbuf filename = STRBUF_INIT; + stack_filename(&filename, st, name); + + reader_close(cur[i]); + reftable_reader_free(cur[i]); + + /* On Windows, can only unlink after closing. */ + unlink(filename.buf); + + strbuf_release(&filename); + } + } + +done: + for (i = 0; i < new_readers_len; i++) { + reader_close(new_readers[i]); + reftable_reader_free(new_readers[i]); + } + reftable_free(new_readers); + reftable_free(new_tables); + reftable_free(cur); + return err; +} + +/* return negative if a before b. */ +static int tv_cmp(struct timeval *a, struct timeval *b) +{ + time_t diff = a->tv_sec - b->tv_sec; + int udiff = a->tv_usec - b->tv_usec; + + if (diff != 0) + return diff; + + return udiff; +} + +static int reftable_stack_reload_maybe_reuse(struct reftable_stack *st, + int reuse_open) +{ + struct timeval deadline = { 0 }; + int err = gettimeofday(&deadline, NULL); + int64_t delay = 0; + int tries = 0; + if (err < 0) + return err; + + deadline.tv_sec += 3; + while (1) { + char **names = NULL; + char **names_after = NULL; + struct timeval now = { 0 }; + int err = gettimeofday(&now, NULL); + int err2 = 0; + if (err < 0) { + return err; + } + + /* Only look at deadlines after the first few times. This + simplifies debugging in GDB */ + tries++; + if (tries > 3 && tv_cmp(&now, &deadline) >= 0) { + break; + } + + err = read_lines(st->list_file, &names); + if (err < 0) { + free_names(names); + return err; + } + err = reftable_stack_reload_once(st, names, reuse_open); + if (err == 0) { + free_names(names); + break; + } + if (err != REFTABLE_NOT_EXIST_ERROR) { + free_names(names); + return err; + } + + /* err == REFTABLE_NOT_EXIST_ERROR can be caused by a concurrent + writer. Check if there was one by checking if the name list + changed. + */ + err2 = read_lines(st->list_file, &names_after); + if (err2 < 0) { + free_names(names); + return err2; + } + + if (names_equal(names_after, names)) { + free_names(names); + free_names(names_after); + return err; + } + free_names(names); + free_names(names_after); + + delay = delay + (delay * rand()) / RAND_MAX + 1; + sleep_millisec(delay); + } + + return 0; +} + +/* -1 = error + 0 = up to date + 1 = changed. */ +static int stack_uptodate(struct reftable_stack *st) +{ + char **names = NULL; + int err = read_lines(st->list_file, &names); + int i = 0; + if (err < 0) + return err; + + for (i = 0; i < st->readers_len; i++) { + if (!names[i]) { + err = 1; + goto done; + } + + if (strcmp(st->readers[i]->name, names[i])) { + err = 1; + goto done; + } + } + + if (names[st->merged->stack_len]) { + err = 1; + goto done; + } + +done: + free_names(names); + return err; +} + +int reftable_stack_reload(struct reftable_stack *st) +{ + int err = stack_uptodate(st); + if (err > 0) + return reftable_stack_reload_maybe_reuse(st, 1); + return err; +} + +int reftable_stack_add(struct reftable_stack *st, + int (*write)(struct reftable_writer *wr, void *arg), + void *arg) +{ + int err = stack_try_add(st, write, arg); + if (err < 0) { + if (err == REFTABLE_LOCK_ERROR) { + /* Ignore error return, we want to propagate + REFTABLE_LOCK_ERROR. + */ + reftable_stack_reload(st); + } + return err; + } + + if (!st->disable_auto_compact) + return reftable_stack_auto_compact(st); + + return 0; +} + +static void format_name(struct strbuf *dest, uint64_t min, uint64_t max) +{ + char buf[100]; + uint32_t rnd = (uint32_t)rand(); + snprintf(buf, sizeof(buf), "0x%012" PRIx64 "-0x%012" PRIx64 "-%08x", + min, max, rnd); + strbuf_reset(dest); + strbuf_addstr(dest, buf); +} + +struct reftable_addition { + int lock_file_fd; + struct strbuf lock_file_name; + struct reftable_stack *stack; + + char **new_tables; + int new_tables_len; + uint64_t next_update_index; +}; + +#define REFTABLE_ADDITION_INIT \ + { \ + .lock_file_name = STRBUF_INIT \ + } + +static int reftable_stack_init_addition(struct reftable_addition *add, + struct reftable_stack *st) +{ + int err = 0; + add->stack = st; + + strbuf_reset(&add->lock_file_name); + strbuf_addstr(&add->lock_file_name, st->list_file); + strbuf_addstr(&add->lock_file_name, ".lock"); + + add->lock_file_fd = open(add->lock_file_name.buf, + O_EXCL | O_CREAT | O_WRONLY, 0644); + if (add->lock_file_fd < 0) { + if (errno == EEXIST) { + err = REFTABLE_LOCK_ERROR; + } else { + err = REFTABLE_IO_ERROR; + } + goto done; + } + err = stack_uptodate(st); + if (err < 0) + goto done; + + if (err > 1) { + err = REFTABLE_LOCK_ERROR; + goto done; + } + + add->next_update_index = reftable_stack_next_update_index(st); +done: + if (err) { + reftable_addition_close(add); + } + return err; +} + +static void reftable_addition_close(struct reftable_addition *add) +{ + int i = 0; + struct strbuf nm = STRBUF_INIT; + for (i = 0; i < add->new_tables_len; i++) { + stack_filename(&nm, add->stack, add->new_tables[i]); + unlink(nm.buf); + reftable_free(add->new_tables[i]); + add->new_tables[i] = NULL; + } + reftable_free(add->new_tables); + add->new_tables = NULL; + add->new_tables_len = 0; + + if (add->lock_file_fd > 0) { + close(add->lock_file_fd); + add->lock_file_fd = 0; + } + if (add->lock_file_name.len > 0) { + unlink(add->lock_file_name.buf); + strbuf_release(&add->lock_file_name); + } + + strbuf_release(&nm); +} + +void reftable_addition_destroy(struct reftable_addition *add) +{ + if (!add) { + return; + } + reftable_addition_close(add); + reftable_free(add); +} + +int reftable_addition_commit(struct reftable_addition *add) +{ + struct strbuf table_list = STRBUF_INIT; + int i = 0; + int err = 0; + if (add->new_tables_len == 0) + goto done; + + for (i = 0; i < add->stack->merged->stack_len; i++) { + strbuf_addstr(&table_list, add->stack->readers[i]->name); + strbuf_addstr(&table_list, "\n"); + } + for (i = 0; i < add->new_tables_len; i++) { + strbuf_addstr(&table_list, add->new_tables[i]); + strbuf_addstr(&table_list, "\n"); + } + + err = write(add->lock_file_fd, table_list.buf, table_list.len); + strbuf_release(&table_list); + if (err < 0) { + err = REFTABLE_IO_ERROR; + goto done; + } + + err = close(add->lock_file_fd); + add->lock_file_fd = 0; + if (err < 0) { + err = REFTABLE_IO_ERROR; + goto done; + } + + err = rename(add->lock_file_name.buf, add->stack->list_file); + if (err < 0) { + err = REFTABLE_IO_ERROR; + goto done; + } + + /* success, no more state to clean up. */ + strbuf_release(&add->lock_file_name); + for (i = 0; i < add->new_tables_len; i++) { + reftable_free(add->new_tables[i]); + } + reftable_free(add->new_tables); + add->new_tables = NULL; + add->new_tables_len = 0; + + err = reftable_stack_reload(add->stack); +done: + reftable_addition_close(add); + return err; +} + +int reftable_stack_new_addition(struct reftable_addition **dest, + struct reftable_stack *st) +{ + int err = 0; + struct reftable_addition empty = REFTABLE_ADDITION_INIT; + *dest = reftable_calloc(sizeof(**dest)); + **dest = empty; + err = reftable_stack_init_addition(*dest, st); + if (err) { + reftable_free(*dest); + *dest = NULL; + } + return err; +} + +static int stack_try_add(struct reftable_stack *st, + int (*write_table)(struct reftable_writer *wr, + void *arg), + void *arg) +{ + struct reftable_addition add = REFTABLE_ADDITION_INIT; + int err = reftable_stack_init_addition(&add, st); + if (err < 0) + goto done; + if (err > 0) { + err = REFTABLE_LOCK_ERROR; + goto done; + } + + err = reftable_addition_add(&add, write_table, arg); + if (err < 0) + goto done; + + err = reftable_addition_commit(&add); +done: + reftable_addition_close(&add); + return err; +} + +int reftable_addition_add(struct reftable_addition *add, + int (*write_table)(struct reftable_writer *wr, + void *arg), + void *arg) +{ + struct strbuf temp_tab_file_name = STRBUF_INIT; + struct strbuf tab_file_name = STRBUF_INIT; + struct strbuf next_name = STRBUF_INIT; + struct reftable_writer *wr = NULL; + int err = 0; + int tab_fd = 0; + + strbuf_reset(&next_name); + format_name(&next_name, add->next_update_index, add->next_update_index); + + stack_filename(&temp_tab_file_name, add->stack, next_name.buf); + strbuf_addstr(&temp_tab_file_name, ".temp.XXXXXX"); + + tab_fd = mkstemp(temp_tab_file_name.buf); + if (tab_fd < 0) { + err = REFTABLE_IO_ERROR; + goto done; + } + + wr = reftable_new_writer(reftable_fd_write, &tab_fd, + &add->stack->config); + err = write_table(wr, arg); + if (err < 0) + goto done; + + err = reftable_writer_close(wr); + if (err == REFTABLE_EMPTY_TABLE_ERROR) { + err = 0; + goto done; + } + if (err < 0) + goto done; + + err = close(tab_fd); + tab_fd = 0; + if (err < 0) { + err = REFTABLE_IO_ERROR; + goto done; + } + + err = stack_check_addition(add->stack, temp_tab_file_name.buf); + if (err < 0) + goto done; + + if (wr->min_update_index < add->next_update_index) { + err = REFTABLE_API_ERROR; + goto done; + } + + format_name(&next_name, wr->min_update_index, wr->max_update_index); + strbuf_addstr(&next_name, ".ref"); + + stack_filename(&tab_file_name, add->stack, next_name.buf); + + /* + On windows, this relies on rand() picking a unique destination name. + Maybe we should do retry loop as well? + */ + err = rename(temp_tab_file_name.buf, tab_file_name.buf); + if (err < 0) { + err = REFTABLE_IO_ERROR; + goto done; + } + + add->new_tables = reftable_realloc(add->new_tables, + sizeof(*add->new_tables) * + (add->new_tables_len + 1)); + add->new_tables[add->new_tables_len] = strbuf_detach(&next_name, NULL); + add->new_tables_len++; +done: + if (tab_fd > 0) { + close(tab_fd); + tab_fd = 0; + } + if (temp_tab_file_name.len > 0) { + unlink(temp_tab_file_name.buf); + } + + strbuf_release(&temp_tab_file_name); + strbuf_release(&tab_file_name); + strbuf_release(&next_name); + reftable_writer_free(wr); + return err; +} + +uint64_t reftable_stack_next_update_index(struct reftable_stack *st) +{ + int sz = st->merged->stack_len; + if (sz > 0) + return reftable_reader_max_update_index(st->readers[sz - 1]) + + 1; + return 1; +} + +static int stack_compact_locked(struct reftable_stack *st, int first, int last, + struct strbuf *temp_tab, + struct reftable_log_expiry_config *config) +{ + struct strbuf next_name = STRBUF_INIT; + int tab_fd = -1; + struct reftable_writer *wr = NULL; + int err = 0; + + format_name(&next_name, + reftable_reader_min_update_index(st->readers[first]), + reftable_reader_max_update_index(st->readers[last])); + + stack_filename(temp_tab, st, next_name.buf); + strbuf_addstr(temp_tab, ".temp.XXXXXX"); + + tab_fd = mkstemp(temp_tab->buf); + wr = reftable_new_writer(reftable_fd_write, &tab_fd, &st->config); + + err = stack_write_compact(st, wr, first, last, config); + if (err < 0) + goto done; + err = reftable_writer_close(wr); + if (err < 0) + goto done; + + err = close(tab_fd); + tab_fd = 0; + +done: + reftable_writer_free(wr); + if (tab_fd > 0) { + close(tab_fd); + tab_fd = 0; + } + if (err != 0 && temp_tab->len > 0) { + unlink(temp_tab->buf); + strbuf_release(temp_tab); + } + strbuf_release(&next_name); + return err; +} + +static int stack_write_compact(struct reftable_stack *st, + struct reftable_writer *wr, int first, int last, + struct reftable_log_expiry_config *config) +{ + int subtabs_len = last - first + 1; + struct reftable_table *subtabs = reftable_calloc( + sizeof(struct reftable_table) * (last - first + 1)); + struct reftable_merged_table *mt = NULL; + int err = 0; + struct reftable_iterator it = { NULL }; + struct reftable_ref_record ref = { NULL }; + struct reftable_log_record log = { NULL }; + + uint64_t entries = 0; + + int i = 0, j = 0; + for (i = first, j = 0; i <= last; i++) { + struct reftable_reader *t = st->readers[i]; + reftable_table_from_reader(&subtabs[j++], t); + st->stats.bytes += t->size; + } + reftable_writer_set_limits(wr, st->readers[first]->min_update_index, + st->readers[last]->max_update_index); + + err = reftable_new_merged_table(&mt, subtabs, subtabs_len, + st->config.hash_id); + if (err < 0) { + reftable_free(subtabs); + goto done; + } + + err = reftable_merged_table_seek_ref(mt, &it, ""); + if (err < 0) + goto done; + + while (1) { + err = reftable_iterator_next_ref(&it, &ref); + if (err > 0) { + err = 0; + break; + } + if (err < 0) { + break; + } + + if (first == 0 && reftable_ref_record_is_deletion(&ref)) { + continue; + } + + err = reftable_writer_add_ref(wr, &ref); + if (err < 0) { + break; + } + entries++; + } + reftable_iterator_destroy(&it); + + err = reftable_merged_table_seek_log(mt, &it, ""); + if (err < 0) + goto done; + + while (1) { + err = reftable_iterator_next_log(&it, &log); + if (err > 0) { + err = 0; + break; + } + if (err < 0) { + break; + } + if (first == 0 && reftable_log_record_is_deletion(&log)) { + continue; + } + + if (config && config->min_update_index > 0 && + log.update_index < config->min_update_index) { + continue; + } + + if (config && config->time > 0 && + log.value.update.time < config->time) { + continue; + } + + err = reftable_writer_add_log(wr, &log); + if (err < 0) { + break; + } + entries++; + } + +done: + reftable_iterator_destroy(&it); + if (mt) { + merged_table_release(mt); + reftable_merged_table_free(mt); + } + reftable_ref_record_release(&ref); + reftable_log_record_release(&log); + st->stats.entries_written += entries; + return err; +} + +/* < 0: error. 0 == OK, > 0 attempt failed; could retry. */ +static int stack_compact_range(struct reftable_stack *st, int first, int last, + struct reftable_log_expiry_config *expiry) +{ + struct strbuf temp_tab_file_name = STRBUF_INIT; + struct strbuf new_table_name = STRBUF_INIT; + struct strbuf lock_file_name = STRBUF_INIT; + struct strbuf ref_list_contents = STRBUF_INIT; + struct strbuf new_table_path = STRBUF_INIT; + int err = 0; + int have_lock = 0; + int lock_file_fd = 0; + int compact_count = last - first + 1; + char **listp = NULL; + char **delete_on_success = + reftable_calloc(sizeof(char *) * (compact_count + 1)); + char **subtable_locks = + reftable_calloc(sizeof(char *) * (compact_count + 1)); + int i = 0; + int j = 0; + int is_empty_table = 0; + + if (first > last || (!expiry && first == last)) { + err = 0; + goto done; + } + + st->stats.attempts++; + + strbuf_reset(&lock_file_name); + strbuf_addstr(&lock_file_name, st->list_file); + strbuf_addstr(&lock_file_name, ".lock"); + + lock_file_fd = + open(lock_file_name.buf, O_EXCL | O_CREAT | O_WRONLY, 0644); + if (lock_file_fd < 0) { + if (errno == EEXIST) { + err = 1; + } else { + err = REFTABLE_IO_ERROR; + } + goto done; + } + /* Don't want to write to the lock for now. */ + close(lock_file_fd); + lock_file_fd = 0; + + have_lock = 1; + err = stack_uptodate(st); + if (err != 0) + goto done; + + for (i = first, j = 0; i <= last; i++) { + struct strbuf subtab_file_name = STRBUF_INIT; + struct strbuf subtab_lock = STRBUF_INIT; + int sublock_file_fd = -1; + + stack_filename(&subtab_file_name, st, + reader_name(st->readers[i])); + + strbuf_reset(&subtab_lock); + strbuf_addbuf(&subtab_lock, &subtab_file_name); + strbuf_addstr(&subtab_lock, ".lock"); + + sublock_file_fd = open(subtab_lock.buf, + O_EXCL | O_CREAT | O_WRONLY, 0644); + if (sublock_file_fd > 0) { + close(sublock_file_fd); + } else if (sublock_file_fd < 0) { + if (errno == EEXIST) { + err = 1; + } else { + err = REFTABLE_IO_ERROR; + } + } + + subtable_locks[j] = subtab_lock.buf; + delete_on_success[j] = subtab_file_name.buf; + j++; + + if (err != 0) + goto done; + } + + err = unlink(lock_file_name.buf); + if (err < 0) + goto done; + have_lock = 0; + + err = stack_compact_locked(st, first, last, &temp_tab_file_name, + expiry); + /* Compaction + tombstones can create an empty table out of non-empty + * tables. */ + is_empty_table = (err == REFTABLE_EMPTY_TABLE_ERROR); + if (is_empty_table) { + err = 0; + } + if (err < 0) + goto done; + + lock_file_fd = + open(lock_file_name.buf, O_EXCL | O_CREAT | O_WRONLY, 0644); + if (lock_file_fd < 0) { + if (errno == EEXIST) { + err = 1; + } else { + err = REFTABLE_IO_ERROR; + } + goto done; + } + have_lock = 1; + + format_name(&new_table_name, st->readers[first]->min_update_index, + st->readers[last]->max_update_index); + strbuf_addstr(&new_table_name, ".ref"); + + stack_filename(&new_table_path, st, new_table_name.buf); + + if (!is_empty_table) { + /* retry? */ + err = rename(temp_tab_file_name.buf, new_table_path.buf); + if (err < 0) { + err = REFTABLE_IO_ERROR; + goto done; + } + } + + for (i = 0; i < first; i++) { + strbuf_addstr(&ref_list_contents, st->readers[i]->name); + strbuf_addstr(&ref_list_contents, "\n"); + } + if (!is_empty_table) { + strbuf_addbuf(&ref_list_contents, &new_table_name); + strbuf_addstr(&ref_list_contents, "\n"); + } + for (i = last + 1; i < st->merged->stack_len; i++) { + strbuf_addstr(&ref_list_contents, st->readers[i]->name); + strbuf_addstr(&ref_list_contents, "\n"); + } + + err = write(lock_file_fd, ref_list_contents.buf, ref_list_contents.len); + if (err < 0) { + err = REFTABLE_IO_ERROR; + unlink(new_table_path.buf); + goto done; + } + err = close(lock_file_fd); + lock_file_fd = 0; + if (err < 0) { + err = REFTABLE_IO_ERROR; + unlink(new_table_path.buf); + goto done; + } + + err = rename(lock_file_name.buf, st->list_file); + if (err < 0) { + err = REFTABLE_IO_ERROR; + unlink(new_table_path.buf); + goto done; + } + have_lock = 0; + + /* Reload the stack before deleting. On windows, we can only delete the + files after we closed them. + */ + err = reftable_stack_reload_maybe_reuse(st, first < last); + + listp = delete_on_success; + while (*listp) { + if (strcmp(*listp, new_table_path.buf)) { + unlink(*listp); + } + listp++; + } + +done: + free_names(delete_on_success); + + listp = subtable_locks; + while (*listp) { + unlink(*listp); + listp++; + } + free_names(subtable_locks); + if (lock_file_fd > 0) { + close(lock_file_fd); + lock_file_fd = 0; + } + if (have_lock) { + unlink(lock_file_name.buf); + } + strbuf_release(&new_table_name); + strbuf_release(&new_table_path); + strbuf_release(&ref_list_contents); + strbuf_release(&temp_tab_file_name); + strbuf_release(&lock_file_name); + return err; +} + +int reftable_stack_compact_all(struct reftable_stack *st, + struct reftable_log_expiry_config *config) +{ + return stack_compact_range(st, 0, st->merged->stack_len - 1, config); +} + +static int stack_compact_range_stats(struct reftable_stack *st, int first, + int last, + struct reftable_log_expiry_config *config) +{ + int err = stack_compact_range(st, first, last, config); + if (err > 0) { + st->stats.failures++; + } + return err; +} + +static int segment_size(struct segment *s) +{ + return s->end - s->start; +} + +int fastlog2(uint64_t sz) +{ + int l = 0; + if (sz == 0) + return 0; + for (; sz; sz /= 2) { + l++; + } + return l - 1; +} + +struct segment *sizes_to_segments(int *seglen, uint64_t *sizes, int n) +{ + struct segment *segs = reftable_calloc(sizeof(struct segment) * n); + int next = 0; + struct segment cur = { 0 }; + int i = 0; + + if (n == 0) { + *seglen = 0; + return segs; + } + for (i = 0; i < n; i++) { + int log = fastlog2(sizes[i]); + if (cur.log != log && cur.bytes > 0) { + struct segment fresh = { + .start = i, + }; + + segs[next++] = cur; + cur = fresh; + } + + cur.log = log; + cur.end = i + 1; + cur.bytes += sizes[i]; + } + segs[next++] = cur; + *seglen = next; + return segs; +} + +struct segment suggest_compaction_segment(uint64_t *sizes, int n) +{ + int seglen = 0; + struct segment *segs = sizes_to_segments(&seglen, sizes, n); + struct segment min_seg = { + .log = 64, + }; + int i = 0; + for (i = 0; i < seglen; i++) { + if (segment_size(&segs[i]) == 1) { + continue; + } + + if (segs[i].log < min_seg.log) { + min_seg = segs[i]; + } + } + + while (min_seg.start > 0) { + int prev = min_seg.start - 1; + if (fastlog2(min_seg.bytes) < fastlog2(sizes[prev])) { + break; + } + + min_seg.start = prev; + min_seg.bytes += sizes[prev]; + } + + reftable_free(segs); + return min_seg; +} + +static uint64_t *stack_table_sizes_for_compaction(struct reftable_stack *st) +{ + uint64_t *sizes = + reftable_calloc(sizeof(uint64_t) * st->merged->stack_len); + int version = (st->config.hash_id == GIT_SHA1_FORMAT_ID) ? 1 : 2; + int overhead = header_size(version) - 1; + int i = 0; + for (i = 0; i < st->merged->stack_len; i++) { + sizes[i] = st->readers[i]->size - overhead; + } + return sizes; +} + +int reftable_stack_auto_compact(struct reftable_stack *st) +{ + uint64_t *sizes = stack_table_sizes_for_compaction(st); + struct segment seg = + suggest_compaction_segment(sizes, st->merged->stack_len); + reftable_free(sizes); + if (segment_size(&seg) > 0) + return stack_compact_range_stats(st, seg.start, seg.end - 1, + NULL); + + return 0; +} + +struct reftable_compaction_stats * +reftable_stack_compaction_stats(struct reftable_stack *st) +{ + return &st->stats; +} + +int reftable_stack_read_ref(struct reftable_stack *st, const char *refname, + struct reftable_ref_record *ref) +{ + struct reftable_table tab = { NULL }; + reftable_table_from_merged_table(&tab, reftable_stack_merged_table(st)); + return reftable_table_read_ref(&tab, refname, ref); +} + +int reftable_stack_read_log(struct reftable_stack *st, const char *refname, + struct reftable_log_record *log) +{ + struct reftable_iterator it = { NULL }; + struct reftable_merged_table *mt = reftable_stack_merged_table(st); + int err = reftable_merged_table_seek_log(mt, &it, refname); + if (err) + goto done; + + err = reftable_iterator_next_log(&it, log); + if (err) + goto done; + + if (strcmp(log->refname, refname) || + reftable_log_record_is_deletion(log)) { + err = 1; + goto done; + } + +done: + if (err) { + reftable_log_record_release(log); + } + reftable_iterator_destroy(&it); + return err; +} + +static int stack_check_addition(struct reftable_stack *st, + const char *new_tab_name) +{ + int err = 0; + struct reftable_block_source src = { NULL }; + struct reftable_reader *rd = NULL; + struct reftable_table tab = { NULL }; + struct reftable_ref_record *refs = NULL; + struct reftable_iterator it = { NULL }; + int cap = 0; + int len = 0; + int i = 0; + + if (st->config.skip_name_check) + return 0; + + err = reftable_block_source_from_file(&src, new_tab_name); + if (err < 0) + goto done; + + err = reftable_new_reader(&rd, &src, new_tab_name); + if (err < 0) + goto done; + + err = reftable_reader_seek_ref(rd, &it, ""); + if (err > 0) { + err = 0; + goto done; + } + if (err < 0) + goto done; + + while (1) { + struct reftable_ref_record ref = { NULL }; + err = reftable_iterator_next_ref(&it, &ref); + if (err > 0) { + break; + } + if (err < 0) + goto done; + + if (len >= cap) { + cap = 2 * cap + 1; + refs = reftable_realloc(refs, cap * sizeof(refs[0])); + } + + refs[len++] = ref; + } + + reftable_table_from_merged_table(&tab, reftable_stack_merged_table(st)); + + err = validate_ref_record_addition(tab, refs, len); + +done: + for (i = 0; i < len; i++) { + reftable_ref_record_release(&refs[i]); + } + + free(refs); + reftable_iterator_destroy(&it); + reftable_reader_free(rd); + return err; +} + +static int is_table_name(const char *s) +{ + const char *dot = strrchr(s, '.'); + return dot && !strcmp(dot, ".ref"); +} + +static void remove_maybe_stale_table(struct reftable_stack *st, uint64_t max, + const char *name) +{ + int err = 0; + uint64_t update_idx = 0; + struct reftable_block_source src = { NULL }; + struct reftable_reader *rd = NULL; + struct strbuf table_path = STRBUF_INIT; + stack_filename(&table_path, st, name); + + err = reftable_block_source_from_file(&src, table_path.buf); + if (err < 0) + goto done; + + err = reftable_new_reader(&rd, &src, name); + if (err < 0) + goto done; + + update_idx = reftable_reader_max_update_index(rd); + reftable_reader_free(rd); + + if (update_idx <= max) { + unlink(table_path.buf); + } +done: + strbuf_release(&table_path); +} + +static int reftable_stack_clean_locked(struct reftable_stack *st) +{ + uint64_t max = reftable_merged_table_max_update_index( + reftable_stack_merged_table(st)); + DIR *dir = opendir(st->reftable_dir); + struct dirent *d = NULL; + if (!dir) { + return REFTABLE_IO_ERROR; + } + + while ((d = readdir(dir))) { + int i = 0; + int found = 0; + if (!is_table_name(d->d_name)) + continue; + + for (i = 0; !found && i < st->readers_len; i++) { + found = !strcmp(reader_name(st->readers[i]), d->d_name); + } + if (found) + continue; + + remove_maybe_stale_table(st, max, d->d_name); + } + + closedir(dir); + return 0; +} + +int reftable_stack_clean(struct reftable_stack *st) +{ + struct reftable_addition *add = NULL; + int err = reftable_stack_new_addition(&add, st); + if (err < 0) { + goto done; + } + + err = reftable_stack_reload(st); + if (err < 0) { + goto done; + } + + err = reftable_stack_clean_locked(st); + +done: + reftable_addition_destroy(add); + return err; +} + +int reftable_stack_print_directory(const char *stackdir, uint32_t hash_id) +{ + struct reftable_stack *stack = NULL; + struct reftable_write_options cfg = { .hash_id = hash_id }; + struct reftable_merged_table *merged = NULL; + struct reftable_table table = { NULL }; + + int err = reftable_new_stack(&stack, stackdir, cfg); + if (err < 0) + goto done; + + merged = reftable_stack_merged_table(stack); + reftable_table_from_merged_table(&table, merged); + err = reftable_table_print(&table); +done: + if (stack) + reftable_stack_destroy(stack); + return err; +} diff --git a/reftable/stack.h b/reftable/stack.h new file mode 100644 index 0000000000..f57005846e --- /dev/null +++ b/reftable/stack.h @@ -0,0 +1,41 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#ifndef STACK_H +#define STACK_H + +#include "system.h" +#include "reftable-writer.h" +#include "reftable-stack.h" + +struct reftable_stack { + char *list_file; + char *reftable_dir; + int disable_auto_compact; + + struct reftable_write_options config; + + struct reftable_reader **readers; + size_t readers_len; + struct reftable_merged_table *merged; + struct reftable_compaction_stats stats; +}; + +int read_lines(const char *filename, char ***lines); + +struct segment { + int start, end; + int log; + uint64_t bytes; +}; + +int fastlog2(uint64_t sz); +struct segment *sizes_to_segments(int *seglen, uint64_t *sizes, int n); +struct segment suggest_compaction_segment(uint64_t *sizes, int n); + +#endif diff --git a/reftable/stack_test.c b/reftable/stack_test.c new file mode 100644 index 0000000000..eb0b7228b0 --- /dev/null +++ b/reftable/stack_test.c @@ -0,0 +1,953 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#include "stack.h" + +#include "system.h" + +#include "reftable-reader.h" +#include "merged.h" +#include "basics.h" +#include "constants.h" +#include "record.h" +#include "test_framework.h" +#include "reftable-tests.h" + +#include <sys/types.h> +#include <dirent.h> + +static void clear_dir(const char *dirname) +{ + struct strbuf path = STRBUF_INIT; + strbuf_addstr(&path, dirname); + remove_dir_recursively(&path, 0); + strbuf_release(&path); +} + +static int count_dir_entries(const char *dirname) +{ + DIR *dir = opendir(dirname); + int len = 0; + struct dirent *d; + if (dir == NULL) + return 0; + + while ((d = readdir(dir))) { + if (!strcmp(d->d_name, "..") || !strcmp(d->d_name, ".")) + continue; + len++; + } + closedir(dir); + return len; +} + +/* + * Work linenumber into the tempdir, so we can see which tests forget to + * cleanup. + */ +static char *get_tmp_template(int linenumber) +{ + const char *tmp = getenv("TMPDIR"); + static char template[1024]; + snprintf(template, sizeof(template) - 1, "%s/stack_test-%d.XXXXXX", + tmp ? tmp : "/tmp", linenumber); + return template; +} + +static char *get_tmp_dir(int linenumber) +{ + char *dir = get_tmp_template(linenumber); + EXPECT(mkdtemp(dir)); + return dir; +} + +static void test_read_file(void) +{ + char *fn = get_tmp_template(__LINE__); + int fd = mkstemp(fn); + char out[1024] = "line1\n\nline2\nline3"; + int n, err; + char **names = NULL; + char *want[] = { "line1", "line2", "line3" }; + int i = 0; + + EXPECT(fd > 0); + n = write(fd, out, strlen(out)); + EXPECT(n == strlen(out)); + err = close(fd); + EXPECT(err >= 0); + + err = read_lines(fn, &names); + EXPECT_ERR(err); + + for (i = 0; names[i]; i++) { + EXPECT(0 == strcmp(want[i], names[i])); + } + free_names(names); + remove(fn); +} + +static void test_parse_names(void) +{ + char buf[] = "line\n"; + char **names = NULL; + parse_names(buf, strlen(buf), &names); + + EXPECT(NULL != names[0]); + EXPECT(0 == strcmp(names[0], "line")); + EXPECT(NULL == names[1]); + free_names(names); +} + +static void test_names_equal(void) +{ + char *a[] = { "a", "b", "c", NULL }; + char *b[] = { "a", "b", "d", NULL }; + char *c[] = { "a", "b", NULL }; + + EXPECT(names_equal(a, a)); + EXPECT(!names_equal(a, b)); + EXPECT(!names_equal(a, c)); +} + +static int write_test_ref(struct reftable_writer *wr, void *arg) +{ + struct reftable_ref_record *ref = arg; + reftable_writer_set_limits(wr, ref->update_index, ref->update_index); + return reftable_writer_add_ref(wr, ref); +} + +struct write_log_arg { + struct reftable_log_record *log; + uint64_t update_index; +}; + +static int write_test_log(struct reftable_writer *wr, void *arg) +{ + struct write_log_arg *wla = arg; + + reftable_writer_set_limits(wr, wla->update_index, wla->update_index); + return reftable_writer_add_log(wr, wla->log); +} + +static void test_reftable_stack_add_one(void) +{ + char *dir = get_tmp_dir(__LINE__); + + struct reftable_write_options cfg = { 0 }; + struct reftable_stack *st = NULL; + int err; + struct reftable_ref_record ref = { + .refname = "HEAD", + .update_index = 1, + .value_type = REFTABLE_REF_SYMREF, + .value.symref = "master", + }; + struct reftable_ref_record dest = { NULL }; + + + err = reftable_new_stack(&st, dir, cfg); + EXPECT_ERR(err); + + err = reftable_stack_add(st, &write_test_ref, &ref); + EXPECT_ERR(err); + + err = reftable_stack_read_ref(st, ref.refname, &dest); + EXPECT_ERR(err); + EXPECT(0 == strcmp("master", dest.value.symref)); + + printf("testing print functionality:\n"); + err = reftable_stack_print_directory(dir, GIT_SHA1_FORMAT_ID); + EXPECT_ERR(err); + + err = reftable_stack_print_directory(dir, GIT_SHA256_FORMAT_ID); + EXPECT(err == REFTABLE_FORMAT_ERROR); + + reftable_ref_record_release(&dest); + reftable_stack_destroy(st); + clear_dir(dir); +} + +static void test_reftable_stack_uptodate(void) +{ + struct reftable_write_options cfg = { 0 }; + struct reftable_stack *st1 = NULL; + struct reftable_stack *st2 = NULL; + char *dir = get_tmp_dir(__LINE__); + + int err; + struct reftable_ref_record ref1 = { + .refname = "HEAD", + .update_index = 1, + .value_type = REFTABLE_REF_SYMREF, + .value.symref = "master", + }; + struct reftable_ref_record ref2 = { + .refname = "branch2", + .update_index = 2, + .value_type = REFTABLE_REF_SYMREF, + .value.symref = "master", + }; + + + /* simulate multi-process access to the same stack + by creating two stacks for the same directory. + */ + err = reftable_new_stack(&st1, dir, cfg); + EXPECT_ERR(err); + + err = reftable_new_stack(&st2, dir, cfg); + EXPECT_ERR(err); + + err = reftable_stack_add(st1, &write_test_ref, &ref1); + EXPECT_ERR(err); + + err = reftable_stack_add(st2, &write_test_ref, &ref2); + EXPECT(err == REFTABLE_LOCK_ERROR); + + err = reftable_stack_reload(st2); + EXPECT_ERR(err); + + err = reftable_stack_add(st2, &write_test_ref, &ref2); + EXPECT_ERR(err); + reftable_stack_destroy(st1); + reftable_stack_destroy(st2); + clear_dir(dir); +} + +static void test_reftable_stack_transaction_api(void) +{ + char *dir = get_tmp_dir(__LINE__); + + struct reftable_write_options cfg = { 0 }; + struct reftable_stack *st = NULL; + int err; + struct reftable_addition *add = NULL; + + struct reftable_ref_record ref = { + .refname = "HEAD", + .update_index = 1, + .value_type = REFTABLE_REF_SYMREF, + .value.symref = "master", + }; + struct reftable_ref_record dest = { NULL }; + + + err = reftable_new_stack(&st, dir, cfg); + EXPECT_ERR(err); + + reftable_addition_destroy(add); + + err = reftable_stack_new_addition(&add, st); + EXPECT_ERR(err); + + err = reftable_addition_add(add, &write_test_ref, &ref); + EXPECT_ERR(err); + + err = reftable_addition_commit(add); + EXPECT_ERR(err); + + reftable_addition_destroy(add); + + err = reftable_stack_read_ref(st, ref.refname, &dest); + EXPECT_ERR(err); + EXPECT(REFTABLE_REF_SYMREF == dest.value_type); + EXPECT(0 == strcmp("master", dest.value.symref)); + + reftable_ref_record_release(&dest); + reftable_stack_destroy(st); + clear_dir(dir); +} + +static void test_reftable_stack_validate_refname(void) +{ + struct reftable_write_options cfg = { 0 }; + struct reftable_stack *st = NULL; + int err; + char *dir = get_tmp_dir(__LINE__); + + int i; + struct reftable_ref_record ref = { + .refname = "a/b", + .update_index = 1, + .value_type = REFTABLE_REF_SYMREF, + .value.symref = "master", + }; + char *additions[] = { "a", "a/b/c" }; + + err = reftable_new_stack(&st, dir, cfg); + EXPECT_ERR(err); + + err = reftable_stack_add(st, &write_test_ref, &ref); + EXPECT_ERR(err); + + for (i = 0; i < ARRAY_SIZE(additions); i++) { + struct reftable_ref_record ref = { + .refname = additions[i], + .update_index = 1, + .value_type = REFTABLE_REF_SYMREF, + .value.symref = "master", + }; + + err = reftable_stack_add(st, &write_test_ref, &ref); + EXPECT(err == REFTABLE_NAME_CONFLICT); + } + + reftable_stack_destroy(st); + clear_dir(dir); +} + +static int write_error(struct reftable_writer *wr, void *arg) +{ + return *((int *)arg); +} + +static void test_reftable_stack_update_index_check(void) +{ + char *dir = get_tmp_dir(__LINE__); + + struct reftable_write_options cfg = { 0 }; + struct reftable_stack *st = NULL; + int err; + struct reftable_ref_record ref1 = { + .refname = "name1", + .update_index = 1, + .value_type = REFTABLE_REF_SYMREF, + .value.symref = "master", + }; + struct reftable_ref_record ref2 = { + .refname = "name2", + .update_index = 1, + .value_type = REFTABLE_REF_SYMREF, + .value.symref = "master", + }; + + err = reftable_new_stack(&st, dir, cfg); + EXPECT_ERR(err); + + err = reftable_stack_add(st, &write_test_ref, &ref1); + EXPECT_ERR(err); + + err = reftable_stack_add(st, &write_test_ref, &ref2); + EXPECT(err == REFTABLE_API_ERROR); + reftable_stack_destroy(st); + clear_dir(dir); +} + +static void test_reftable_stack_lock_failure(void) +{ + char *dir = get_tmp_dir(__LINE__); + + struct reftable_write_options cfg = { 0 }; + struct reftable_stack *st = NULL; + int err, i; + + err = reftable_new_stack(&st, dir, cfg); + EXPECT_ERR(err); + for (i = -1; i != REFTABLE_EMPTY_TABLE_ERROR; i--) { + err = reftable_stack_add(st, &write_error, &i); + EXPECT(err == i); + } + + reftable_stack_destroy(st); + clear_dir(dir); +} + +static void test_reftable_stack_add(void) +{ + int i = 0; + int err = 0; + struct reftable_write_options cfg = { + .exact_log_message = 1, + }; + struct reftable_stack *st = NULL; + char *dir = get_tmp_dir(__LINE__); + + struct reftable_ref_record refs[2] = { { NULL } }; + struct reftable_log_record logs[2] = { { NULL } }; + int N = ARRAY_SIZE(refs); + + + err = reftable_new_stack(&st, dir, cfg); + EXPECT_ERR(err); + st->disable_auto_compact = 1; + + for (i = 0; i < N; i++) { + char buf[256]; + snprintf(buf, sizeof(buf), "branch%02d", i); + refs[i].refname = xstrdup(buf); + refs[i].update_index = i + 1; + refs[i].value_type = REFTABLE_REF_VAL1; + refs[i].value.val1 = reftable_malloc(GIT_SHA1_RAWSZ); + set_test_hash(refs[i].value.val1, i); + + logs[i].refname = xstrdup(buf); + logs[i].update_index = N + i + 1; + logs[i].value_type = REFTABLE_LOG_UPDATE; + + logs[i].value.update.new_hash = reftable_malloc(GIT_SHA1_RAWSZ); + logs[i].value.update.email = xstrdup("identity@invalid"); + set_test_hash(logs[i].value.update.new_hash, i); + } + + for (i = 0; i < N; i++) { + int err = reftable_stack_add(st, &write_test_ref, &refs[i]); + EXPECT_ERR(err); + } + + for (i = 0; i < N; i++) { + struct write_log_arg arg = { + .log = &logs[i], + .update_index = reftable_stack_next_update_index(st), + }; + int err = reftable_stack_add(st, &write_test_log, &arg); + EXPECT_ERR(err); + } + + err = reftable_stack_compact_all(st, NULL); + EXPECT_ERR(err); + + for (i = 0; i < N; i++) { + struct reftable_ref_record dest = { NULL }; + + int err = reftable_stack_read_ref(st, refs[i].refname, &dest); + EXPECT_ERR(err); + EXPECT(reftable_ref_record_equal(&dest, refs + i, + GIT_SHA1_RAWSZ)); + reftable_ref_record_release(&dest); + } + + for (i = 0; i < N; i++) { + struct reftable_log_record dest = { NULL }; + int err = reftable_stack_read_log(st, refs[i].refname, &dest); + EXPECT_ERR(err); + EXPECT(reftable_log_record_equal(&dest, logs + i, + GIT_SHA1_RAWSZ)); + reftable_log_record_release(&dest); + } + + /* cleanup */ + reftable_stack_destroy(st); + for (i = 0; i < N; i++) { + reftable_ref_record_release(&refs[i]); + reftable_log_record_release(&logs[i]); + } + clear_dir(dir); +} + +static void test_reftable_stack_log_normalize(void) +{ + int err = 0; + struct reftable_write_options cfg = { + 0, + }; + struct reftable_stack *st = NULL; + char *dir = get_tmp_dir(__LINE__); + + uint8_t h1[GIT_SHA1_RAWSZ] = { 0x01 }, h2[GIT_SHA1_RAWSZ] = { 0x02 }; + + struct reftable_log_record input = { .refname = "branch", + .update_index = 1, + .value_type = REFTABLE_LOG_UPDATE, + .value = { .update = { + .new_hash = h1, + .old_hash = h2, + } } }; + struct reftable_log_record dest = { + .update_index = 0, + }; + struct write_log_arg arg = { + .log = &input, + .update_index = 1, + }; + + err = reftable_new_stack(&st, dir, cfg); + EXPECT_ERR(err); + + input.value.update.message = "one\ntwo"; + err = reftable_stack_add(st, &write_test_log, &arg); + EXPECT(err == REFTABLE_API_ERROR); + + input.value.update.message = "one"; + err = reftable_stack_add(st, &write_test_log, &arg); + EXPECT_ERR(err); + + err = reftable_stack_read_log(st, input.refname, &dest); + EXPECT_ERR(err); + EXPECT(0 == strcmp(dest.value.update.message, "one\n")); + + input.value.update.message = "two\n"; + arg.update_index = 2; + err = reftable_stack_add(st, &write_test_log, &arg); + EXPECT_ERR(err); + err = reftable_stack_read_log(st, input.refname, &dest); + EXPECT_ERR(err); + EXPECT(0 == strcmp(dest.value.update.message, "two\n")); + + /* cleanup */ + reftable_stack_destroy(st); + reftable_log_record_release(&dest); + clear_dir(dir); +} + +static void test_reftable_stack_tombstone(void) +{ + int i = 0; + char *dir = get_tmp_dir(__LINE__); + + struct reftable_write_options cfg = { 0 }; + struct reftable_stack *st = NULL; + int err; + struct reftable_ref_record refs[2] = { { NULL } }; + struct reftable_log_record logs[2] = { { NULL } }; + int N = ARRAY_SIZE(refs); + struct reftable_ref_record dest = { NULL }; + struct reftable_log_record log_dest = { NULL }; + + + err = reftable_new_stack(&st, dir, cfg); + EXPECT_ERR(err); + + /* even entries add the refs, odd entries delete them. */ + for (i = 0; i < N; i++) { + const char *buf = "branch"; + refs[i].refname = xstrdup(buf); + refs[i].update_index = i + 1; + if (i % 2 == 0) { + refs[i].value_type = REFTABLE_REF_VAL1; + refs[i].value.val1 = reftable_malloc(GIT_SHA1_RAWSZ); + set_test_hash(refs[i].value.val1, i); + } + + logs[i].refname = xstrdup(buf); + /* update_index is part of the key. */ + logs[i].update_index = 42; + if (i % 2 == 0) { + logs[i].value_type = REFTABLE_LOG_UPDATE; + logs[i].value.update.new_hash = + reftable_malloc(GIT_SHA1_RAWSZ); + set_test_hash(logs[i].value.update.new_hash, i); + logs[i].value.update.email = + xstrdup("identity@invalid"); + } + } + for (i = 0; i < N; i++) { + int err = reftable_stack_add(st, &write_test_ref, &refs[i]); + EXPECT_ERR(err); + } + + for (i = 0; i < N; i++) { + struct write_log_arg arg = { + .log = &logs[i], + .update_index = reftable_stack_next_update_index(st), + }; + int err = reftable_stack_add(st, &write_test_log, &arg); + EXPECT_ERR(err); + } + + err = reftable_stack_read_ref(st, "branch", &dest); + EXPECT(err == 1); + reftable_ref_record_release(&dest); + + err = reftable_stack_read_log(st, "branch", &log_dest); + EXPECT(err == 1); + reftable_log_record_release(&log_dest); + + err = reftable_stack_compact_all(st, NULL); + EXPECT_ERR(err); + + err = reftable_stack_read_ref(st, "branch", &dest); + EXPECT(err == 1); + + err = reftable_stack_read_log(st, "branch", &log_dest); + EXPECT(err == 1); + reftable_ref_record_release(&dest); + reftable_log_record_release(&log_dest); + + /* cleanup */ + reftable_stack_destroy(st); + for (i = 0; i < N; i++) { + reftable_ref_record_release(&refs[i]); + reftable_log_record_release(&logs[i]); + } + clear_dir(dir); +} + +static void test_reftable_stack_hash_id(void) +{ + char *dir = get_tmp_dir(__LINE__); + + struct reftable_write_options cfg = { 0 }; + struct reftable_stack *st = NULL; + int err; + + struct reftable_ref_record ref = { + .refname = "master", + .value_type = REFTABLE_REF_SYMREF, + .value.symref = "target", + .update_index = 1, + }; + struct reftable_write_options cfg32 = { .hash_id = GIT_SHA256_FORMAT_ID }; + struct reftable_stack *st32 = NULL; + struct reftable_write_options cfg_default = { 0 }; + struct reftable_stack *st_default = NULL; + struct reftable_ref_record dest = { NULL }; + + err = reftable_new_stack(&st, dir, cfg); + EXPECT_ERR(err); + + err = reftable_stack_add(st, &write_test_ref, &ref); + EXPECT_ERR(err); + + /* can't read it with the wrong hash ID. */ + err = reftable_new_stack(&st32, dir, cfg32); + EXPECT(err == REFTABLE_FORMAT_ERROR); + + /* check that we can read it back with default config too. */ + err = reftable_new_stack(&st_default, dir, cfg_default); + EXPECT_ERR(err); + + err = reftable_stack_read_ref(st_default, "master", &dest); + EXPECT_ERR(err); + + EXPECT(reftable_ref_record_equal(&ref, &dest, GIT_SHA1_RAWSZ)); + reftable_ref_record_release(&dest); + reftable_stack_destroy(st); + reftable_stack_destroy(st_default); + clear_dir(dir); +} + +static void test_log2(void) +{ + EXPECT(1 == fastlog2(3)); + EXPECT(2 == fastlog2(4)); + EXPECT(2 == fastlog2(5)); +} + +static void test_sizes_to_segments(void) +{ + uint64_t sizes[] = { 2, 3, 4, 5, 7, 9 }; + /* .................0 1 2 3 4 5 */ + + int seglen = 0; + struct segment *segs = + sizes_to_segments(&seglen, sizes, ARRAY_SIZE(sizes)); + EXPECT(segs[2].log == 3); + EXPECT(segs[2].start == 5); + EXPECT(segs[2].end == 6); + + EXPECT(segs[1].log == 2); + EXPECT(segs[1].start == 2); + EXPECT(segs[1].end == 5); + reftable_free(segs); +} + +static void test_sizes_to_segments_empty(void) +{ + int seglen = 0; + struct segment *segs = sizes_to_segments(&seglen, NULL, 0); + EXPECT(seglen == 0); + reftable_free(segs); +} + +static void test_sizes_to_segments_all_equal(void) +{ + uint64_t sizes[] = { 5, 5 }; + + int seglen = 0; + struct segment *segs = + sizes_to_segments(&seglen, sizes, ARRAY_SIZE(sizes)); + EXPECT(seglen == 1); + EXPECT(segs[0].start == 0); + EXPECT(segs[0].end == 2); + reftable_free(segs); +} + +static void test_suggest_compaction_segment(void) +{ + uint64_t sizes[] = { 128, 64, 17, 16, 9, 9, 9, 16, 16 }; + /* .................0 1 2 3 4 5 6 */ + struct segment min = + suggest_compaction_segment(sizes, ARRAY_SIZE(sizes)); + EXPECT(min.start == 2); + EXPECT(min.end == 7); +} + +static void test_suggest_compaction_segment_nothing(void) +{ + uint64_t sizes[] = { 64, 32, 16, 8, 4, 2 }; + struct segment result = + suggest_compaction_segment(sizes, ARRAY_SIZE(sizes)); + EXPECT(result.start == result.end); +} + +static void test_reflog_expire(void) +{ + char *dir = get_tmp_dir(__LINE__); + + struct reftable_write_options cfg = { 0 }; + struct reftable_stack *st = NULL; + struct reftable_log_record logs[20] = { { NULL } }; + int N = ARRAY_SIZE(logs) - 1; + int i = 0; + int err; + struct reftable_log_expiry_config expiry = { + .time = 10, + }; + struct reftable_log_record log = { NULL }; + + + err = reftable_new_stack(&st, dir, cfg); + EXPECT_ERR(err); + + for (i = 1; i <= N; i++) { + char buf[256]; + snprintf(buf, sizeof(buf), "branch%02d", i); + + logs[i].refname = xstrdup(buf); + logs[i].update_index = i; + logs[i].value_type = REFTABLE_LOG_UPDATE; + logs[i].value.update.time = i; + logs[i].value.update.new_hash = reftable_malloc(GIT_SHA1_RAWSZ); + logs[i].value.update.email = xstrdup("identity@invalid"); + set_test_hash(logs[i].value.update.new_hash, i); + } + + for (i = 1; i <= N; i++) { + struct write_log_arg arg = { + .log = &logs[i], + .update_index = reftable_stack_next_update_index(st), + }; + int err = reftable_stack_add(st, &write_test_log, &arg); + EXPECT_ERR(err); + } + + err = reftable_stack_compact_all(st, NULL); + EXPECT_ERR(err); + + err = reftable_stack_compact_all(st, &expiry); + EXPECT_ERR(err); + + err = reftable_stack_read_log(st, logs[9].refname, &log); + EXPECT(err == 1); + + err = reftable_stack_read_log(st, logs[11].refname, &log); + EXPECT_ERR(err); + + expiry.min_update_index = 15; + err = reftable_stack_compact_all(st, &expiry); + EXPECT_ERR(err); + + err = reftable_stack_read_log(st, logs[14].refname, &log); + EXPECT(err == 1); + + err = reftable_stack_read_log(st, logs[16].refname, &log); + EXPECT_ERR(err); + + /* cleanup */ + reftable_stack_destroy(st); + for (i = 0; i <= N; i++) { + reftable_log_record_release(&logs[i]); + } + clear_dir(dir); + reftable_log_record_release(&log); +} + +static int write_nothing(struct reftable_writer *wr, void *arg) +{ + reftable_writer_set_limits(wr, 1, 1); + return 0; +} + +static void test_empty_add(void) +{ + struct reftable_write_options cfg = { 0 }; + struct reftable_stack *st = NULL; + int err; + char *dir = get_tmp_dir(__LINE__); + + struct reftable_stack *st2 = NULL; + + + err = reftable_new_stack(&st, dir, cfg); + EXPECT_ERR(err); + + err = reftable_stack_add(st, &write_nothing, NULL); + EXPECT_ERR(err); + + err = reftable_new_stack(&st2, dir, cfg); + EXPECT_ERR(err); + clear_dir(dir); + reftable_stack_destroy(st); + reftable_stack_destroy(st2); +} + +static void test_reftable_stack_auto_compaction(void) +{ + struct reftable_write_options cfg = { 0 }; + struct reftable_stack *st = NULL; + char *dir = get_tmp_dir(__LINE__); + + int err, i; + int N = 100; + + err = reftable_new_stack(&st, dir, cfg); + EXPECT_ERR(err); + + st->disable_auto_compact = 1; /* call manually below for coverage. */ + for (i = 0; i < N; i++) { + char name[100]; + struct reftable_ref_record ref = { + .refname = name, + .update_index = reftable_stack_next_update_index(st), + .value_type = REFTABLE_REF_SYMREF, + .value.symref = "master", + }; + snprintf(name, sizeof(name), "branch%04d", i); + + err = reftable_stack_add(st, &write_test_ref, &ref); + EXPECT_ERR(err); + + err = reftable_stack_auto_compact(st); + EXPECT(i < 3 || st->merged->stack_len < 2 * fastlog2(i)); + } + + EXPECT(reftable_stack_compaction_stats(st)->entries_written < + (uint64_t)(N * fastlog2(N))); + + reftable_stack_destroy(st); + clear_dir(dir); +} + +static void test_reftable_stack_compaction_concurrent(void) +{ + struct reftable_write_options cfg = { 0 }; + struct reftable_stack *st1 = NULL, *st2 = NULL; + char *dir = get_tmp_dir(__LINE__); + + int err, i; + int N = 3; + + err = reftable_new_stack(&st1, dir, cfg); + EXPECT_ERR(err); + + for (i = 0; i < N; i++) { + char name[100]; + struct reftable_ref_record ref = { + .refname = name, + .update_index = reftable_stack_next_update_index(st1), + .value_type = REFTABLE_REF_SYMREF, + .value.symref = "master", + }; + snprintf(name, sizeof(name), "branch%04d", i); + + err = reftable_stack_add(st1, &write_test_ref, &ref); + EXPECT_ERR(err); + } + + err = reftable_new_stack(&st2, dir, cfg); + EXPECT_ERR(err); + + err = reftable_stack_compact_all(st1, NULL); + EXPECT_ERR(err); + + reftable_stack_destroy(st1); + reftable_stack_destroy(st2); + + EXPECT(count_dir_entries(dir) == 2); + clear_dir(dir); +} + +static void unclean_stack_close(struct reftable_stack *st) +{ + /* break abstraction boundary to simulate unclean shutdown. */ + int i = 0; + for (; i < st->readers_len; i++) { + reftable_reader_free(st->readers[i]); + } + st->readers_len = 0; + FREE_AND_NULL(st->readers); +} + +static void test_reftable_stack_compaction_concurrent_clean(void) +{ + struct reftable_write_options cfg = { 0 }; + struct reftable_stack *st1 = NULL, *st2 = NULL, *st3 = NULL; + char *dir = get_tmp_dir(__LINE__); + + int err, i; + int N = 3; + + err = reftable_new_stack(&st1, dir, cfg); + EXPECT_ERR(err); + + for (i = 0; i < N; i++) { + char name[100]; + struct reftable_ref_record ref = { + .refname = name, + .update_index = reftable_stack_next_update_index(st1), + .value_type = REFTABLE_REF_SYMREF, + .value.symref = "master", + }; + snprintf(name, sizeof(name), "branch%04d", i); + + err = reftable_stack_add(st1, &write_test_ref, &ref); + EXPECT_ERR(err); + } + + err = reftable_new_stack(&st2, dir, cfg); + EXPECT_ERR(err); + + err = reftable_stack_compact_all(st1, NULL); + EXPECT_ERR(err); + + unclean_stack_close(st1); + unclean_stack_close(st2); + + err = reftable_new_stack(&st3, dir, cfg); + EXPECT_ERR(err); + + err = reftable_stack_clean(st3); + EXPECT_ERR(err); + EXPECT(count_dir_entries(dir) == 2); + + reftable_stack_destroy(st1); + reftable_stack_destroy(st2); + reftable_stack_destroy(st3); + + clear_dir(dir); +} + +int stack_test_main(int argc, const char *argv[]) +{ + RUN_TEST(test_empty_add); + RUN_TEST(test_log2); + RUN_TEST(test_names_equal); + RUN_TEST(test_parse_names); + RUN_TEST(test_read_file); + RUN_TEST(test_reflog_expire); + RUN_TEST(test_reftable_stack_add); + RUN_TEST(test_reftable_stack_add_one); + RUN_TEST(test_reftable_stack_auto_compaction); + RUN_TEST(test_reftable_stack_compaction_concurrent); + RUN_TEST(test_reftable_stack_compaction_concurrent_clean); + RUN_TEST(test_reftable_stack_hash_id); + RUN_TEST(test_reftable_stack_lock_failure); + RUN_TEST(test_reftable_stack_log_normalize); + RUN_TEST(test_reftable_stack_tombstone); + RUN_TEST(test_reftable_stack_transaction_api); + RUN_TEST(test_reftable_stack_update_index_check); + RUN_TEST(test_reftable_stack_uptodate); + RUN_TEST(test_reftable_stack_validate_refname); + RUN_TEST(test_sizes_to_segments); + RUN_TEST(test_sizes_to_segments_all_equal); + RUN_TEST(test_sizes_to_segments_empty); + RUN_TEST(test_suggest_compaction_segment); + RUN_TEST(test_suggest_compaction_segment_nothing); + return 0; +} diff --git a/reftable/system.h b/reftable/system.h new file mode 100644 index 0000000000..4907306c0c --- /dev/null +++ b/reftable/system.h @@ -0,0 +1,32 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#ifndef SYSTEM_H +#define SYSTEM_H + +/* This header glues the reftable library to the rest of Git */ + +#include "git-compat-util.h" +#include "strbuf.h" +#include "hash.h" /* hash ID, sizes.*/ +#include "dir.h" /* remove_dir_recursively, for tests.*/ + +#include <zlib.h> + +#ifdef NO_UNCOMPRESS2 +/* + * This is uncompress2, which is only available in zlib >= 1.2.9 + * (released as of early 2017) + */ +int uncompress2(Bytef *dest, uLongf *destLen, const Bytef *source, + uLong *sourceLen); +#endif + +int hash_size(uint32_t id); + +#endif diff --git a/reftable/test_framework.c b/reftable/test_framework.c new file mode 100644 index 0000000000..84ac972cad --- /dev/null +++ b/reftable/test_framework.c @@ -0,0 +1,23 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#include "system.h" +#include "test_framework.h" + +#include "basics.h" + +void set_test_hash(uint8_t *p, int i) +{ + memset(p, (uint8_t)i, hash_size(GIT_SHA1_FORMAT_ID)); +} + +ssize_t strbuf_add_void(void *b, const void *data, size_t sz) +{ + strbuf_add(b, data, sz); + return sz; +} diff --git a/reftable/test_framework.h b/reftable/test_framework.h new file mode 100644 index 0000000000..774cb275bf --- /dev/null +++ b/reftable/test_framework.h @@ -0,0 +1,53 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#ifndef TEST_FRAMEWORK_H +#define TEST_FRAMEWORK_H + +#include "system.h" +#include "reftable-error.h" + +#define EXPECT_ERR(c) \ + if (c != 0) { \ + fflush(stderr); \ + fflush(stdout); \ + fprintf(stderr, "%s: %d: error == %d (%s), want 0\n", \ + __FILE__, __LINE__, c, reftable_error_str(c)); \ + abort(); \ + } + +#define EXPECT_STREQ(a, b) \ + if (strcmp(a, b)) { \ + fflush(stderr); \ + fflush(stdout); \ + fprintf(stderr, "%s:%d: %s (%s) != %s (%s)\n", __FILE__, \ + __LINE__, #a, a, #b, b); \ + abort(); \ + } + +#define EXPECT(c) \ + if (!(c)) { \ + fflush(stderr); \ + fflush(stdout); \ + fprintf(stderr, "%s: %d: failed assertion %s\n", __FILE__, \ + __LINE__, #c); \ + abort(); \ + } + +#define RUN_TEST(f) \ + fprintf(stderr, "running %s\n", #f); \ + fflush(stderr); \ + f(); + +void set_test_hash(uint8_t *p, int i); + +/* Like strbuf_add, but suitable for passing to reftable_new_writer + */ +ssize_t strbuf_add_void(void *b, const void *data, size_t sz); + +#endif diff --git a/reftable/tree.c b/reftable/tree.c new file mode 100644 index 0000000000..82db7995dd --- /dev/null +++ b/reftable/tree.c @@ -0,0 +1,63 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#include "tree.h" + +#include "basics.h" +#include "system.h" + +struct tree_node *tree_search(void *key, struct tree_node **rootp, + int (*compare)(const void *, const void *), + int insert) +{ + int res; + if (*rootp == NULL) { + if (!insert) { + return NULL; + } else { + struct tree_node *n = + reftable_calloc(sizeof(struct tree_node)); + n->key = key; + *rootp = n; + return *rootp; + } + } + + res = compare(key, (*rootp)->key); + if (res < 0) + return tree_search(key, &(*rootp)->left, compare, insert); + else if (res > 0) + return tree_search(key, &(*rootp)->right, compare, insert); + return *rootp; +} + +void infix_walk(struct tree_node *t, void (*action)(void *arg, void *key), + void *arg) +{ + if (t->left) { + infix_walk(t->left, action, arg); + } + action(arg, t->key); + if (t->right) { + infix_walk(t->right, action, arg); + } +} + +void tree_free(struct tree_node *t) +{ + if (t == NULL) { + return; + } + if (t->left) { + tree_free(t->left); + } + if (t->right) { + tree_free(t->right); + } + reftable_free(t); +} diff --git a/reftable/tree.h b/reftable/tree.h new file mode 100644 index 0000000000..fbdd002e23 --- /dev/null +++ b/reftable/tree.h @@ -0,0 +1,34 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#ifndef TREE_H +#define TREE_H + +/* tree_node is a generic binary search tree. */ +struct tree_node { + void *key; + struct tree_node *left, *right; +}; + +/* looks for `key` in `rootp` using `compare` as comparison function. If insert + * is set, insert the key if it's not found. Else, return NULL. + */ +struct tree_node *tree_search(void *key, struct tree_node **rootp, + int (*compare)(const void *, const void *), + int insert); + +/* performs an infix walk of the tree. */ +void infix_walk(struct tree_node *t, void (*action)(void *arg, void *key), + void *arg); + +/* + * deallocates the tree nodes recursively. Keys should be deallocated separately + * by walking over the tree. */ +void tree_free(struct tree_node *t); + +#endif diff --git a/reftable/tree_test.c b/reftable/tree_test.c new file mode 100644 index 0000000000..cbff125588 --- /dev/null +++ b/reftable/tree_test.c @@ -0,0 +1,61 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#include "tree.h" + +#include "basics.h" +#include "record.h" +#include "test_framework.h" +#include "reftable-tests.h" + +static int test_compare(const void *a, const void *b) +{ + return (char *)a - (char *)b; +} + +struct curry { + void *last; +}; + +static void check_increasing(void *arg, void *key) +{ + struct curry *c = arg; + if (c->last) { + EXPECT(test_compare(c->last, key) < 0); + } + c->last = key; +} + +static void test_tree(void) +{ + struct tree_node *root = NULL; + + void *values[11] = { NULL }; + struct tree_node *nodes[11] = { NULL }; + int i = 1; + struct curry c = { NULL }; + do { + nodes[i] = tree_search(values + i, &root, &test_compare, 1); + i = (i * 7) % 11; + } while (i != 1); + + for (i = 1; i < ARRAY_SIZE(nodes); i++) { + EXPECT(values + i == nodes[i]->key); + EXPECT(nodes[i] == + tree_search(values + i, &root, &test_compare, 0)); + } + + infix_walk(root, check_increasing, &c); + tree_free(root); +} + +int tree_test_main(int argc, const char *argv[]) +{ + RUN_TEST(test_tree); + return 0; +} diff --git a/reftable/writer.c b/reftable/writer.c new file mode 100644 index 0000000000..3ca721e9f6 --- /dev/null +++ b/reftable/writer.c @@ -0,0 +1,690 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#include "writer.h" + +#include "system.h" + +#include "block.h" +#include "constants.h" +#include "record.h" +#include "tree.h" +#include "reftable-error.h" + +/* finishes a block, and writes it to storage */ +static int writer_flush_block(struct reftable_writer *w); + +/* deallocates memory related to the index */ +static void writer_clear_index(struct reftable_writer *w); + +/* finishes writing a 'r' (refs) or 'g' (reflogs) section */ +static int writer_finish_public_section(struct reftable_writer *w); + +static struct reftable_block_stats * +writer_reftable_block_stats(struct reftable_writer *w, uint8_t typ) +{ + switch (typ) { + case 'r': + return &w->stats.ref_stats; + case 'o': + return &w->stats.obj_stats; + case 'i': + return &w->stats.idx_stats; + case 'g': + return &w->stats.log_stats; + } + abort(); + return NULL; +} + +/* write data, queuing the padding for the next write. Returns negative for + * error. */ +static int padded_write(struct reftable_writer *w, uint8_t *data, size_t len, + int padding) +{ + int n = 0; + if (w->pending_padding > 0) { + uint8_t *zeroed = reftable_calloc(w->pending_padding); + int n = w->write(w->write_arg, zeroed, w->pending_padding); + if (n < 0) + return n; + + w->pending_padding = 0; + reftable_free(zeroed); + } + + w->pending_padding = padding; + n = w->write(w->write_arg, data, len); + if (n < 0) + return n; + n += padding; + return 0; +} + +static void options_set_defaults(struct reftable_write_options *opts) +{ + if (opts->restart_interval == 0) { + opts->restart_interval = 16; + } + + if (opts->hash_id == 0) { + opts->hash_id = GIT_SHA1_FORMAT_ID; + } + if (opts->block_size == 0) { + opts->block_size = DEFAULT_BLOCK_SIZE; + } +} + +static int writer_version(struct reftable_writer *w) +{ + return (w->opts.hash_id == 0 || w->opts.hash_id == GIT_SHA1_FORMAT_ID) ? + 1 : + 2; +} + +static int writer_write_header(struct reftable_writer *w, uint8_t *dest) +{ + memcpy(dest, "REFT", 4); + + dest[4] = writer_version(w); + + put_be24(dest + 5, w->opts.block_size); + put_be64(dest + 8, w->min_update_index); + put_be64(dest + 16, w->max_update_index); + if (writer_version(w) == 2) { + put_be32(dest + 24, w->opts.hash_id); + } + return header_size(writer_version(w)); +} + +static void writer_reinit_block_writer(struct reftable_writer *w, uint8_t typ) +{ + int block_start = 0; + if (w->next == 0) { + block_start = header_size(writer_version(w)); + } + + strbuf_release(&w->last_key); + block_writer_init(&w->block_writer_data, typ, w->block, + w->opts.block_size, block_start, + hash_size(w->opts.hash_id)); + w->block_writer = &w->block_writer_data; + w->block_writer->restart_interval = w->opts.restart_interval; +} + +static struct strbuf reftable_empty_strbuf = STRBUF_INIT; + +struct reftable_writer * +reftable_new_writer(ssize_t (*writer_func)(void *, const void *, size_t), + void *writer_arg, struct reftable_write_options *opts) +{ + struct reftable_writer *wp = + reftable_calloc(sizeof(struct reftable_writer)); + strbuf_init(&wp->block_writer_data.last_key, 0); + options_set_defaults(opts); + if (opts->block_size >= (1 << 24)) { + /* TODO - error return? */ + abort(); + } + wp->last_key = reftable_empty_strbuf; + wp->block = reftable_calloc(opts->block_size); + wp->write = writer_func; + wp->write_arg = writer_arg; + wp->opts = *opts; + writer_reinit_block_writer(wp, BLOCK_TYPE_REF); + + return wp; +} + +void reftable_writer_set_limits(struct reftable_writer *w, uint64_t min, + uint64_t max) +{ + w->min_update_index = min; + w->max_update_index = max; +} + +void reftable_writer_free(struct reftable_writer *w) +{ + reftable_free(w->block); + reftable_free(w); +} + +struct obj_index_tree_node { + struct strbuf hash; + uint64_t *offsets; + size_t offset_len; + size_t offset_cap; +}; + +#define OBJ_INDEX_TREE_NODE_INIT \ + { \ + .hash = STRBUF_INIT \ + } + +static int obj_index_tree_node_compare(const void *a, const void *b) +{ + return strbuf_cmp(&((const struct obj_index_tree_node *)a)->hash, + &((const struct obj_index_tree_node *)b)->hash); +} + +static void writer_index_hash(struct reftable_writer *w, struct strbuf *hash) +{ + uint64_t off = w->next; + + struct obj_index_tree_node want = { .hash = *hash }; + + struct tree_node *node = tree_search(&want, &w->obj_index_tree, + &obj_index_tree_node_compare, 0); + struct obj_index_tree_node *key = NULL; + if (node == NULL) { + struct obj_index_tree_node empty = OBJ_INDEX_TREE_NODE_INIT; + key = reftable_malloc(sizeof(struct obj_index_tree_node)); + *key = empty; + + strbuf_reset(&key->hash); + strbuf_addbuf(&key->hash, hash); + tree_search((void *)key, &w->obj_index_tree, + &obj_index_tree_node_compare, 1); + } else { + key = node->key; + } + + if (key->offset_len > 0 && key->offsets[key->offset_len - 1] == off) { + return; + } + + if (key->offset_len == key->offset_cap) { + key->offset_cap = 2 * key->offset_cap + 1; + key->offsets = reftable_realloc( + key->offsets, sizeof(uint64_t) * key->offset_cap); + } + + key->offsets[key->offset_len++] = off; +} + +static int writer_add_record(struct reftable_writer *w, + struct reftable_record *rec) +{ + struct strbuf key = STRBUF_INIT; + int err = -1; + reftable_record_key(rec, &key); + if (strbuf_cmp(&w->last_key, &key) >= 0) { + err = REFTABLE_API_ERROR; + goto done; + } + + strbuf_reset(&w->last_key); + strbuf_addbuf(&w->last_key, &key); + if (w->block_writer == NULL) { + writer_reinit_block_writer(w, reftable_record_type(rec)); + } + + assert(block_writer_type(w->block_writer) == reftable_record_type(rec)); + + if (block_writer_add(w->block_writer, rec) == 0) { + err = 0; + goto done; + } + + err = writer_flush_block(w); + if (err < 0) { + goto done; + } + + writer_reinit_block_writer(w, reftable_record_type(rec)); + err = block_writer_add(w->block_writer, rec); + if (err < 0) { + goto done; + } + + err = 0; +done: + strbuf_release(&key); + return err; +} + +int reftable_writer_add_ref(struct reftable_writer *w, + struct reftable_ref_record *ref) +{ + struct reftable_record rec = { NULL }; + struct reftable_ref_record copy = *ref; + int err = 0; + + if (ref->refname == NULL) + return REFTABLE_API_ERROR; + if (ref->update_index < w->min_update_index || + ref->update_index > w->max_update_index) + return REFTABLE_API_ERROR; + + reftable_record_from_ref(&rec, ©); + copy.update_index -= w->min_update_index; + + err = writer_add_record(w, &rec); + if (err < 0) + return err; + + if (!w->opts.skip_index_objects && reftable_ref_record_val1(ref)) { + struct strbuf h = STRBUF_INIT; + strbuf_add(&h, (char *)reftable_ref_record_val1(ref), + hash_size(w->opts.hash_id)); + writer_index_hash(w, &h); + strbuf_release(&h); + } + + if (!w->opts.skip_index_objects && reftable_ref_record_val2(ref)) { + struct strbuf h = STRBUF_INIT; + strbuf_add(&h, reftable_ref_record_val2(ref), + hash_size(w->opts.hash_id)); + writer_index_hash(w, &h); + strbuf_release(&h); + } + return 0; +} + +int reftable_writer_add_refs(struct reftable_writer *w, + struct reftable_ref_record *refs, int n) +{ + int err = 0; + int i = 0; + QSORT(refs, n, reftable_ref_record_compare_name); + for (i = 0; err == 0 && i < n; i++) { + err = reftable_writer_add_ref(w, &refs[i]); + } + return err; +} + +static int reftable_writer_add_log_verbatim(struct reftable_writer *w, + struct reftable_log_record *log) +{ + struct reftable_record rec = { NULL }; + if (w->block_writer && + block_writer_type(w->block_writer) == BLOCK_TYPE_REF) { + int err = writer_finish_public_section(w); + if (err < 0) + return err; + } + + w->next -= w->pending_padding; + w->pending_padding = 0; + + reftable_record_from_log(&rec, log); + return writer_add_record(w, &rec); +} + +int reftable_writer_add_log(struct reftable_writer *w, + struct reftable_log_record *log) +{ + char *input_log_message = NULL; + struct strbuf cleaned_message = STRBUF_INIT; + int err = 0; + + if (log->value_type == REFTABLE_LOG_DELETION) + return reftable_writer_add_log_verbatim(w, log); + + if (log->refname == NULL) + return REFTABLE_API_ERROR; + + input_log_message = log->value.update.message; + if (!w->opts.exact_log_message && log->value.update.message) { + strbuf_addstr(&cleaned_message, log->value.update.message); + while (cleaned_message.len && + cleaned_message.buf[cleaned_message.len - 1] == '\n') + strbuf_setlen(&cleaned_message, + cleaned_message.len - 1); + if (strchr(cleaned_message.buf, '\n')) { + /* multiple lines not allowed. */ + err = REFTABLE_API_ERROR; + goto done; + } + strbuf_addstr(&cleaned_message, "\n"); + log->value.update.message = cleaned_message.buf; + } + + err = reftable_writer_add_log_verbatim(w, log); + log->value.update.message = input_log_message; +done: + strbuf_release(&cleaned_message); + return err; +} + +int reftable_writer_add_logs(struct reftable_writer *w, + struct reftable_log_record *logs, int n) +{ + int err = 0; + int i = 0; + QSORT(logs, n, reftable_log_record_compare_key); + + for (i = 0; err == 0 && i < n; i++) { + err = reftable_writer_add_log(w, &logs[i]); + } + return err; +} + +static int writer_finish_section(struct reftable_writer *w) +{ + uint8_t typ = block_writer_type(w->block_writer); + uint64_t index_start = 0; + int max_level = 0; + int threshold = w->opts.unpadded ? 1 : 3; + int before_blocks = w->stats.idx_stats.blocks; + int err = writer_flush_block(w); + int i = 0; + struct reftable_block_stats *bstats = NULL; + if (err < 0) + return err; + + while (w->index_len > threshold) { + struct reftable_index_record *idx = NULL; + int idx_len = 0; + + max_level++; + index_start = w->next; + writer_reinit_block_writer(w, BLOCK_TYPE_INDEX); + + idx = w->index; + idx_len = w->index_len; + + w->index = NULL; + w->index_len = 0; + w->index_cap = 0; + for (i = 0; i < idx_len; i++) { + struct reftable_record rec = { NULL }; + reftable_record_from_index(&rec, idx + i); + if (block_writer_add(w->block_writer, &rec) == 0) { + continue; + } + + err = writer_flush_block(w); + if (err < 0) + return err; + + writer_reinit_block_writer(w, BLOCK_TYPE_INDEX); + + err = block_writer_add(w->block_writer, &rec); + if (err != 0) { + /* write into fresh block should always succeed + */ + abort(); + } + } + for (i = 0; i < idx_len; i++) { + strbuf_release(&idx[i].last_key); + } + reftable_free(idx); + } + + writer_clear_index(w); + + err = writer_flush_block(w); + if (err < 0) + return err; + + bstats = writer_reftable_block_stats(w, typ); + bstats->index_blocks = w->stats.idx_stats.blocks - before_blocks; + bstats->index_offset = index_start; + bstats->max_index_level = max_level; + + /* Reinit lastKey, as the next section can start with any key. */ + w->last_key.len = 0; + + return 0; +} + +struct common_prefix_arg { + struct strbuf *last; + int max; +}; + +static void update_common(void *void_arg, void *key) +{ + struct common_prefix_arg *arg = void_arg; + struct obj_index_tree_node *entry = key; + if (arg->last) { + int n = common_prefix_size(&entry->hash, arg->last); + if (n > arg->max) { + arg->max = n; + } + } + arg->last = &entry->hash; +} + +struct write_record_arg { + struct reftable_writer *w; + int err; +}; + +static void write_object_record(void *void_arg, void *key) +{ + struct write_record_arg *arg = void_arg; + struct obj_index_tree_node *entry = key; + struct reftable_obj_record obj_rec = { + .hash_prefix = (uint8_t *)entry->hash.buf, + .hash_prefix_len = arg->w->stats.object_id_len, + .offsets = entry->offsets, + .offset_len = entry->offset_len, + }; + struct reftable_record rec = { NULL }; + if (arg->err < 0) + goto done; + + reftable_record_from_obj(&rec, &obj_rec); + arg->err = block_writer_add(arg->w->block_writer, &rec); + if (arg->err == 0) + goto done; + + arg->err = writer_flush_block(arg->w); + if (arg->err < 0) + goto done; + + writer_reinit_block_writer(arg->w, BLOCK_TYPE_OBJ); + arg->err = block_writer_add(arg->w->block_writer, &rec); + if (arg->err == 0) + goto done; + obj_rec.offset_len = 0; + arg->err = block_writer_add(arg->w->block_writer, &rec); + + /* Should be able to write into a fresh block. */ + assert(arg->err == 0); + +done:; +} + +static void object_record_free(void *void_arg, void *key) +{ + struct obj_index_tree_node *entry = key; + + FREE_AND_NULL(entry->offsets); + strbuf_release(&entry->hash); + reftable_free(entry); +} + +static int writer_dump_object_index(struct reftable_writer *w) +{ + struct write_record_arg closure = { .w = w }; + struct common_prefix_arg common = { NULL }; + if (w->obj_index_tree) { + infix_walk(w->obj_index_tree, &update_common, &common); + } + w->stats.object_id_len = common.max + 1; + + writer_reinit_block_writer(w, BLOCK_TYPE_OBJ); + + if (w->obj_index_tree) { + infix_walk(w->obj_index_tree, &write_object_record, &closure); + } + + if (closure.err < 0) + return closure.err; + return writer_finish_section(w); +} + +static int writer_finish_public_section(struct reftable_writer *w) +{ + uint8_t typ = 0; + int err = 0; + + if (w->block_writer == NULL) + return 0; + + typ = block_writer_type(w->block_writer); + err = writer_finish_section(w); + if (err < 0) + return err; + if (typ == BLOCK_TYPE_REF && !w->opts.skip_index_objects && + w->stats.ref_stats.index_blocks > 0) { + err = writer_dump_object_index(w); + if (err < 0) + return err; + } + + if (w->obj_index_tree) { + infix_walk(w->obj_index_tree, &object_record_free, NULL); + tree_free(w->obj_index_tree); + w->obj_index_tree = NULL; + } + + w->block_writer = NULL; + return 0; +} + +int reftable_writer_close(struct reftable_writer *w) +{ + uint8_t footer[72]; + uint8_t *p = footer; + int err = writer_finish_public_section(w); + int empty_table = w->next == 0; + if (err != 0) + goto done; + w->pending_padding = 0; + if (empty_table) { + /* Empty tables need a header anyway. */ + uint8_t header[28]; + int n = writer_write_header(w, header); + err = padded_write(w, header, n, 0); + if (err < 0) + goto done; + } + + p += writer_write_header(w, footer); + put_be64(p, w->stats.ref_stats.index_offset); + p += 8; + put_be64(p, (w->stats.obj_stats.offset) << 5 | w->stats.object_id_len); + p += 8; + put_be64(p, w->stats.obj_stats.index_offset); + p += 8; + + put_be64(p, w->stats.log_stats.offset); + p += 8; + put_be64(p, w->stats.log_stats.index_offset); + p += 8; + + put_be32(p, crc32(0, footer, p - footer)); + p += 4; + + err = padded_write(w, footer, footer_size(writer_version(w)), 0); + if (err < 0) + goto done; + + if (empty_table) { + err = REFTABLE_EMPTY_TABLE_ERROR; + goto done; + } + +done: + /* free up memory. */ + block_writer_release(&w->block_writer_data); + writer_clear_index(w); + strbuf_release(&w->last_key); + return err; +} + +static void writer_clear_index(struct reftable_writer *w) +{ + int i = 0; + for (i = 0; i < w->index_len; i++) { + strbuf_release(&w->index[i].last_key); + } + + FREE_AND_NULL(w->index); + w->index_len = 0; + w->index_cap = 0; +} + +static const int debug = 0; + +static int writer_flush_nonempty_block(struct reftable_writer *w) +{ + uint8_t typ = block_writer_type(w->block_writer); + struct reftable_block_stats *bstats = + writer_reftable_block_stats(w, typ); + uint64_t block_typ_off = (bstats->blocks == 0) ? w->next : 0; + int raw_bytes = block_writer_finish(w->block_writer); + int padding = 0; + int err = 0; + struct reftable_index_record ir = { .last_key = STRBUF_INIT }; + if (raw_bytes < 0) + return raw_bytes; + + if (!w->opts.unpadded && typ != BLOCK_TYPE_LOG) { + padding = w->opts.block_size - raw_bytes; + } + + if (block_typ_off > 0) { + bstats->offset = block_typ_off; + } + + bstats->entries += w->block_writer->entries; + bstats->restarts += w->block_writer->restart_len; + bstats->blocks++; + w->stats.blocks++; + + if (debug) { + fprintf(stderr, "block %c off %" PRIu64 " sz %d (%d)\n", typ, + w->next, raw_bytes, + get_be24(w->block + w->block_writer->header_off + 1)); + } + + if (w->next == 0) { + writer_write_header(w, w->block); + } + + err = padded_write(w, w->block, raw_bytes, padding); + if (err < 0) + return err; + + if (w->index_cap == w->index_len) { + w->index_cap = 2 * w->index_cap + 1; + w->index = reftable_realloc( + w->index, + sizeof(struct reftable_index_record) * w->index_cap); + } + + ir.offset = w->next; + strbuf_reset(&ir.last_key); + strbuf_addbuf(&ir.last_key, &w->block_writer->last_key); + w->index[w->index_len] = ir; + + w->index_len++; + w->next += padding + raw_bytes; + w->block_writer = NULL; + return 0; +} + +static int writer_flush_block(struct reftable_writer *w) +{ + if (w->block_writer == NULL) + return 0; + if (w->block_writer->entries == 0) + return 0; + return writer_flush_nonempty_block(w); +} + +const struct reftable_stats *writer_stats(struct reftable_writer *w) +{ + return &w->stats; +} diff --git a/reftable/writer.h b/reftable/writer.h new file mode 100644 index 0000000000..09b88673d9 --- /dev/null +++ b/reftable/writer.h @@ -0,0 +1,50 @@ +/* +Copyright 2020 Google LLC + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +#ifndef WRITER_H +#define WRITER_H + +#include "basics.h" +#include "block.h" +#include "tree.h" +#include "reftable-writer.h" + +struct reftable_writer { + ssize_t (*write)(void *, const void *, size_t); + void *write_arg; + int pending_padding; + struct strbuf last_key; + + /* offset of next block to write. */ + uint64_t next; + uint64_t min_update_index, max_update_index; + struct reftable_write_options opts; + + /* memory buffer for writing */ + uint8_t *block; + + /* writer for the current section. NULL or points to + * block_writer_data */ + struct block_writer *block_writer; + + struct block_writer block_writer_data; + + /* pending index records for the current section */ + struct reftable_index_record *index; + size_t index_len; + size_t index_cap; + + /* + * tree for use with tsearch; used to populate the 'o' inverse OID + * map */ + struct tree_node *obj_index_tree; + + struct reftable_stats stats; +}; + +#endif diff --git a/remote-curl.c b/remote-curl.c index d69156312b..0dabef2dd7 100644 --- a/remote-curl.c +++ b/remote-curl.c @@ -1061,7 +1061,7 @@ static int rpc_service(struct rpc_state *rpc, struct discovery *heads, client.in = -1; client.out = -1; client.git_cmd = 1; - client.argv = client_argv; + strvec_pushv(&client.args, client_argv); if (start_command(&client)) exit(1); write_or_die(client.in, preamble->buf, preamble->len); @@ -21,33 +21,6 @@ struct counted_string { size_t len; const char *s; }; -struct rewrite { - const char *base; - size_t baselen; - struct counted_string *instead_of; - int instead_of_nr; - int instead_of_alloc; -}; -struct rewrites { - struct rewrite **rewrite; - int rewrite_alloc; - int rewrite_nr; -}; - -static struct remote **remotes; -static int remotes_alloc; -static int remotes_nr; -static struct hashmap remotes_hash; - -static struct branch **branches; -static int branches_alloc; -static int branches_nr; - -static struct branch *current_branch; -static const char *pushremote_name; - -static struct rewrites rewrites; -static struct rewrites rewrites_push; static int valid_remote(const struct remote *remote) { @@ -92,17 +65,19 @@ static void add_pushurl(struct remote *remote, const char *pushurl) remote->pushurl[remote->pushurl_nr++] = pushurl; } -static void add_pushurl_alias(struct remote *remote, const char *url) +static void add_pushurl_alias(struct remote_state *remote_state, + struct remote *remote, const char *url) { - const char *pushurl = alias_url(url, &rewrites_push); + const char *pushurl = alias_url(url, &remote_state->rewrites_push); if (pushurl != url) add_pushurl(remote, pushurl); } -static void add_url_alias(struct remote *remote, const char *url) +static void add_url_alias(struct remote_state *remote_state, + struct remote *remote, const char *url) { - add_url(remote, alias_url(url, &rewrites)); - add_pushurl_alias(remote, url); + add_url(remote, alias_url(url, &remote_state->rewrites)); + add_pushurl_alias(remote_state, remote, url); } struct remotes_hash_key { @@ -127,13 +102,8 @@ static int remotes_hash_cmp(const void *unused_cmp_data, return strcmp(a->name, b->name); } -static inline void init_remotes_hash(void) -{ - if (!remotes_hash.cmpfn) - hashmap_init(&remotes_hash, remotes_hash_cmp, NULL, 0); -} - -static struct remote *make_remote(const char *name, int len) +static struct remote *make_remote(struct remote_state *remote_state, + const char *name, int len) { struct remote *ret; struct remotes_hash_key lookup; @@ -142,12 +112,11 @@ static struct remote *make_remote(const char *name, int len) if (!len) len = strlen(name); - init_remotes_hash(); lookup.str = name; lookup.len = len; hashmap_entry_init(&lookup_entry, memhash(name, len)); - e = hashmap_get(&remotes_hash, &lookup_entry, &lookup); + e = hashmap_get(&remote_state->remotes_hash, &lookup_entry, &lookup); if (e) return container_of(e, struct remote, ent); @@ -158,15 +127,38 @@ static struct remote *make_remote(const char *name, int len) refspec_init(&ret->push, REFSPEC_PUSH); refspec_init(&ret->fetch, REFSPEC_FETCH); - ALLOC_GROW(remotes, remotes_nr + 1, remotes_alloc); - remotes[remotes_nr++] = ret; + ALLOC_GROW(remote_state->remotes, remote_state->remotes_nr + 1, + remote_state->remotes_alloc); + remote_state->remotes[remote_state->remotes_nr++] = ret; hashmap_entry_init(&ret->ent, lookup_entry.hash); - if (hashmap_put_entry(&remotes_hash, ret, ent)) + if (hashmap_put_entry(&remote_state->remotes_hash, ret, ent)) BUG("hashmap_put overwrote entry after hashmap_get returned NULL"); return ret; } +static void remote_clear(struct remote *remote) +{ + int i; + + free((char *)remote->name); + free((char *)remote->foreign_vcs); + + for (i = 0; i < remote->url_nr; i++) { + free((char *)remote->url[i]); + } + FREE_AND_NULL(remote->pushurl); + + for (i = 0; i < remote->pushurl_nr; i++) { + free((char *)remote->pushurl[i]); + } + FREE_AND_NULL(remote->pushurl); + free((char *)remote->receivepack); + free((char *)remote->uploadpack); + FREE_AND_NULL(remote->http_proxy); + FREE_AND_NULL(remote->http_proxy_authmethod); +} + static void add_merge(struct branch *branch, const char *name) { ALLOC_GROW(branch->merge_name, branch->merge_nr + 1, @@ -174,23 +166,74 @@ static void add_merge(struct branch *branch, const char *name) branch->merge_name[branch->merge_nr++] = name; } -static struct branch *make_branch(const char *name, size_t len) +struct branches_hash_key { + const char *str; + int len; +}; + +static int branches_hash_cmp(const void *unused_cmp_data, + const struct hashmap_entry *eptr, + const struct hashmap_entry *entry_or_key, + const void *keydata) +{ + const struct branch *a, *b; + const struct branches_hash_key *key = keydata; + + a = container_of(eptr, const struct branch, ent); + b = container_of(entry_or_key, const struct branch, ent); + + if (key) + return strncmp(a->name, key->str, key->len) || + a->name[key->len]; + else + return strcmp(a->name, b->name); +} + +static struct branch *find_branch(struct remote_state *remote_state, + const char *name, size_t len) +{ + struct branches_hash_key lookup; + struct hashmap_entry lookup_entry, *e; + + if (!len) + len = strlen(name); + + lookup.str = name; + lookup.len = len; + hashmap_entry_init(&lookup_entry, memhash(name, len)); + + e = hashmap_get(&remote_state->branches_hash, &lookup_entry, &lookup); + if (e) + return container_of(e, struct branch, ent); + + return NULL; +} + +static void die_on_missing_branch(struct repository *repo, + struct branch *branch) +{ + /* branch == NULL is always valid because it represents detached HEAD. */ + if (branch && + branch != find_branch(repo->remote_state, branch->name, 0)) + die("branch %s was not found in the repository", branch->name); +} + +static struct branch *make_branch(struct remote_state *remote_state, + const char *name, size_t len) { struct branch *ret; - int i; - for (i = 0; i < branches_nr; i++) { - if (!strncmp(name, branches[i]->name, len) && - !branches[i]->name[len]) - return branches[i]; - } + ret = find_branch(remote_state, name, len); + if (ret) + return ret; - ALLOC_GROW(branches, branches_nr + 1, branches_alloc); CALLOC_ARRAY(ret, 1); - branches[branches_nr++] = ret; ret->name = xstrndup(name, len); ret->refname = xstrfmt("refs/heads/%s", ret->name); + hashmap_entry_init(&ret->ent, memhash(name, len)); + if (hashmap_put_entry(&remote_state->branches_hash, ret, ent)) + BUG("hashmap_put overwrote entry after hashmap_get returned NULL"); return ret; } @@ -229,7 +272,8 @@ static const char *skip_spaces(const char *s) return s; } -static void read_remotes_file(struct remote *remote) +static void read_remotes_file(struct remote_state *remote_state, + struct remote *remote) { struct strbuf buf = STRBUF_INIT; FILE *f = fopen_or_warn(git_path("remotes/%s", remote->name), "r"); @@ -244,7 +288,8 @@ static void read_remotes_file(struct remote *remote) strbuf_rtrim(&buf); if (skip_prefix(buf.buf, "URL:", &v)) - add_url_alias(remote, xstrdup(skip_spaces(v))); + add_url_alias(remote_state, remote, + xstrdup(skip_spaces(v))); else if (skip_prefix(buf.buf, "Push:", &v)) refspec_append(&remote->push, skip_spaces(v)); else if (skip_prefix(buf.buf, "Pull:", &v)) @@ -254,7 +299,8 @@ static void read_remotes_file(struct remote *remote) fclose(f); } -static void read_branches_file(struct remote *remote) +static void read_branches_file(struct remote_state *remote_state, + struct remote *remote) { char *frag; struct strbuf buf = STRBUF_INIT; @@ -286,7 +332,7 @@ static void read_branches_file(struct remote *remote) else frag = (char *)git_default_branch_name(0); - add_url_alias(remote, strbuf_detach(&buf, NULL)); + add_url_alias(remote_state, remote, strbuf_detach(&buf, NULL)); refspec_appendf(&remote->fetch, "refs/heads/%s:refs/heads/%s", frag, remote->name); @@ -305,10 +351,12 @@ static int handle_config(const char *key, const char *value, void *cb) const char *subkey; struct remote *remote; struct branch *branch; + struct remote_state *remote_state = cb; + if (parse_config_key(key, "branch", &name, &namelen, &subkey) >= 0) { if (!name) return 0; - branch = make_branch(name, namelen); + branch = make_branch(remote_state, name, namelen); if (!strcmp(subkey, "remote")) { return git_config_string(&branch->remote_name, key, value); } else if (!strcmp(subkey, "pushremote")) { @@ -327,12 +375,14 @@ static int handle_config(const char *key, const char *value, void *cb) if (!strcmp(subkey, "insteadof")) { if (!value) return config_error_nonbool(key); - rewrite = make_rewrite(&rewrites, name, namelen); + rewrite = make_rewrite(&remote_state->rewrites, name, + namelen); add_instead_of(rewrite, xstrdup(value)); } else if (!strcmp(subkey, "pushinsteadof")) { if (!value) return config_error_nonbool(key); - rewrite = make_rewrite(&rewrites_push, name, namelen); + rewrite = make_rewrite(&remote_state->rewrites_push, + name, namelen); add_instead_of(rewrite, xstrdup(value)); } } @@ -342,7 +392,8 @@ static int handle_config(const char *key, const char *value, void *cb) /* Handle remote.* variables */ if (!name && !strcmp(subkey, "pushdefault")) - return git_config_string(&pushremote_name, key, value); + return git_config_string(&remote_state->pushremote_name, key, + value); if (!name) return 0; @@ -352,7 +403,7 @@ static int handle_config(const char *key, const char *value, void *cb) name); return 0; } - remote = make_remote(name, namelen); + remote = make_remote(remote_state, name, namelen); remote->origin = REMOTE_CONFIG; if (current_config_scope() == CONFIG_SCOPE_LOCAL || current_config_scope() == CONFIG_SCOPE_WORKTREE) @@ -422,44 +473,52 @@ static int handle_config(const char *key, const char *value, void *cb) return 0; } -static void alias_all_urls(void) +static void alias_all_urls(struct remote_state *remote_state) { int i, j; - for (i = 0; i < remotes_nr; i++) { + for (i = 0; i < remote_state->remotes_nr; i++) { int add_pushurl_aliases; - if (!remotes[i]) + if (!remote_state->remotes[i]) continue; - for (j = 0; j < remotes[i]->pushurl_nr; j++) { - remotes[i]->pushurl[j] = alias_url(remotes[i]->pushurl[j], &rewrites); + for (j = 0; j < remote_state->remotes[i]->pushurl_nr; j++) { + remote_state->remotes[i]->pushurl[j] = + alias_url(remote_state->remotes[i]->pushurl[j], + &remote_state->rewrites); } - add_pushurl_aliases = remotes[i]->pushurl_nr == 0; - for (j = 0; j < remotes[i]->url_nr; j++) { + add_pushurl_aliases = remote_state->remotes[i]->pushurl_nr == 0; + for (j = 0; j < remote_state->remotes[i]->url_nr; j++) { if (add_pushurl_aliases) - add_pushurl_alias(remotes[i], remotes[i]->url[j]); - remotes[i]->url[j] = alias_url(remotes[i]->url[j], &rewrites); + add_pushurl_alias( + remote_state, remote_state->remotes[i], + remote_state->remotes[i]->url[j]); + remote_state->remotes[i]->url[j] = + alias_url(remote_state->remotes[i]->url[j], + &remote_state->rewrites); } } } -static void read_config(void) +static void read_config(struct repository *repo) { - static int loaded; int flag; - if (loaded) + if (repo->remote_state->initialized) return; - loaded = 1; + repo->remote_state->initialized = 1; - current_branch = NULL; + repo->remote_state->current_branch = NULL; if (startup_info->have_repository) { - const char *head_ref = resolve_ref_unsafe("HEAD", 0, NULL, &flag); + int ignore_errno; + const char *head_ref = refs_resolve_ref_unsafe( + get_main_ref_store(repo), "HEAD", 0, NULL, &flag, &ignore_errno); if (head_ref && (flag & REF_ISSYMREF) && skip_prefix(head_ref, "refs/heads/", &head_ref)) { - current_branch = make_branch(head_ref, strlen(head_ref)); + repo->remote_state->current_branch = make_branch( + repo->remote_state, head_ref, strlen(head_ref)); } } - git_config(handle_config, NULL); - alias_all_urls(); + repo_config(repo, handle_config, repo->remote_state); + alias_all_urls(repo->remote_state); } static int valid_remote_nick(const char *name) @@ -474,7 +533,9 @@ static int valid_remote_nick(const char *name) return 1; } -const char *remote_for_branch(struct branch *branch, int *explicit) +static const char *remotes_remote_for_branch(struct remote_state *remote_state, + struct branch *branch, + int *explicit) { if (branch && branch->remote_name) { if (explicit) @@ -486,32 +547,61 @@ const char *remote_for_branch(struct branch *branch, int *explicit) return "origin"; } -const char *pushremote_for_branch(struct branch *branch, int *explicit) +const char *remote_for_branch(struct branch *branch, int *explicit) +{ + read_config(the_repository); + die_on_missing_branch(the_repository, branch); + + return remotes_remote_for_branch(the_repository->remote_state, branch, + explicit); +} + +static const char * +remotes_pushremote_for_branch(struct remote_state *remote_state, + struct branch *branch, int *explicit) { if (branch && branch->pushremote_name) { if (explicit) *explicit = 1; return branch->pushremote_name; } - if (pushremote_name) { + if (remote_state->pushremote_name) { if (explicit) *explicit = 1; - return pushremote_name; + return remote_state->pushremote_name; } - return remote_for_branch(branch, explicit); + return remotes_remote_for_branch(remote_state, branch, explicit); +} + +const char *pushremote_for_branch(struct branch *branch, int *explicit) +{ + read_config(the_repository); + die_on_missing_branch(the_repository, branch); + + return remotes_pushremote_for_branch(the_repository->remote_state, + branch, explicit); } +static struct remote *remotes_remote_get(struct remote_state *remote_state, + const char *name); + const char *remote_ref_for_branch(struct branch *branch, int for_push) { + read_config(the_repository); + die_on_missing_branch(the_repository, branch); + if (branch) { if (!for_push) { if (branch->merge_nr) { return branch->merge_name[0]; } } else { - const char *dst, *remote_name = - pushremote_for_branch(branch, NULL); - struct remote *remote = remote_get(remote_name); + const char *dst, + *remote_name = remotes_pushremote_for_branch( + the_repository->remote_state, branch, + NULL); + struct remote *remote = remotes_remote_get( + the_repository->remote_state, remote_name); if (remote && remote->push.nr && (dst = apply_refspecs(&remote->push, @@ -523,41 +613,58 @@ const char *remote_ref_for_branch(struct branch *branch, int for_push) return NULL; } -static struct remote *remote_get_1(const char *name, - const char *(*get_default)(struct branch *, int *)) +static struct remote * +remotes_remote_get_1(struct remote_state *remote_state, const char *name, + const char *(*get_default)(struct remote_state *, + struct branch *, int *)) { struct remote *ret; int name_given = 0; - read_config(); - if (name) name_given = 1; else - name = get_default(current_branch, &name_given); + name = get_default(remote_state, remote_state->current_branch, + &name_given); - ret = make_remote(name, 0); + ret = make_remote(remote_state, name, 0); if (valid_remote_nick(name) && have_git_dir()) { if (!valid_remote(ret)) - read_remotes_file(ret); + read_remotes_file(remote_state, ret); if (!valid_remote(ret)) - read_branches_file(ret); + read_branches_file(remote_state, ret); } if (name_given && !valid_remote(ret)) - add_url_alias(ret, name); + add_url_alias(remote_state, ret, name); if (!valid_remote(ret)) return NULL; return ret; } +static inline struct remote * +remotes_remote_get(struct remote_state *remote_state, const char *name) +{ + return remotes_remote_get_1(remote_state, name, + remotes_remote_for_branch); +} + struct remote *remote_get(const char *name) { - return remote_get_1(name, remote_for_branch); + read_config(the_repository); + return remotes_remote_get(the_repository->remote_state, name); +} + +static inline struct remote * +remotes_pushremote_get(struct remote_state *remote_state, const char *name) +{ + return remotes_remote_get_1(remote_state, name, + remotes_pushremote_for_branch); } struct remote *pushremote_get(const char *name) { - return remote_get_1(name, pushremote_for_branch); + read_config(the_repository); + return remotes_pushremote_get(the_repository->remote_state, name); } int remote_is_configured(struct remote *remote, int in_repo) @@ -572,12 +679,14 @@ int remote_is_configured(struct remote *remote, int in_repo) int for_each_remote(each_remote_fn fn, void *priv) { int i, result = 0; - read_config(); - for (i = 0; i < remotes_nr && !result; i++) { - struct remote *r = remotes[i]; - if (!r) + read_config(the_repository); + for (i = 0; i < the_repository->remote_state->remotes_nr && !result; + i++) { + struct remote *remote = + the_repository->remote_state->remotes[i]; + if (!remote) continue; - result = fn(r, priv); + result = fn(remote, priv); } return result; } @@ -1642,7 +1751,7 @@ void set_ref_status_for_push(struct ref *remote_refs, int send_mirror, } } -static void set_merge(struct branch *ret) +static void set_merge(struct remote_state *remote_state, struct branch *ret) { struct remote *remote; char *ref; @@ -1662,7 +1771,7 @@ static void set_merge(struct branch *ret) return; } - remote = remote_get(ret->remote_name); + remote = remotes_remote_get(remote_state, ret->remote_name); CALLOC_ARRAY(ret->merge, ret->merge_nr); for (i = 0; i < ret->merge_nr; i++) { @@ -1683,12 +1792,13 @@ struct branch *branch_get(const char *name) { struct branch *ret; - read_config(); + read_config(the_repository); if (!name || !*name || !strcmp(name, "HEAD")) - ret = current_branch; + ret = the_repository->remote_state->current_branch; else - ret = make_branch(name, strlen(name)); - set_merge(ret); + ret = make_branch(the_repository->remote_state, name, + strlen(name)); + set_merge(the_repository->remote_state, ret); return ret; } @@ -1759,11 +1869,14 @@ static const char *tracking_for_push_dest(struct remote *remote, return ret; } -static const char *branch_get_push_1(struct branch *branch, struct strbuf *err) +static const char *branch_get_push_1(struct remote_state *remote_state, + struct branch *branch, struct strbuf *err) { struct remote *remote; - remote = remote_get(pushremote_for_branch(branch, NULL)); + remote = remotes_remote_get( + remote_state, + remotes_pushremote_for_branch(remote_state, branch, NULL)); if (!remote) return error_buf(err, _("branch '%s' has no remote for pushing"), @@ -1821,11 +1934,15 @@ static const char *branch_get_push_1(struct branch *branch, struct strbuf *err) const char *branch_get_push(struct branch *branch, struct strbuf *err) { + read_config(the_repository); + die_on_missing_branch(the_repository, branch); + if (!branch) return error_buf(err, _("HEAD does not point to a branch")); if (!branch->push_tracking_ref) - branch->push_tracking_ref = branch_get_push_1(branch, err); + branch->push_tracking_ref = branch_get_push_1( + the_repository->remote_state, branch, err); return branch->push_tracking_ref; } @@ -2585,3 +2702,29 @@ void apply_push_cas(struct push_cas_option *cas, check_if_includes_upstream(ref); } } + +struct remote_state *remote_state_new(void) +{ + struct remote_state *r = xmalloc(sizeof(*r)); + + memset(r, 0, sizeof(*r)); + + hashmap_init(&r->remotes_hash, remotes_hash_cmp, NULL, 0); + hashmap_init(&r->branches_hash, branches_hash_cmp, NULL, 0); + return r; +} + +void remote_state_clear(struct remote_state *remote_state) +{ + int i; + + for (i = 0; i < remote_state->remotes_nr; i++) { + remote_clear(remote_state->remotes[i]); + } + FREE_AND_NULL(remote_state->remotes); + remote_state->remotes_alloc = 0; + remote_state->remotes_nr = 0; + + hashmap_clear_and_free(&remote_state->remotes_hash, struct remote, ent); + hashmap_clear_and_free(&remote_state->branches_hash, struct remote, ent); +} @@ -23,6 +23,40 @@ enum { REMOTE_BRANCHES }; +struct rewrite { + const char *base; + size_t baselen; + struct counted_string *instead_of; + int instead_of_nr; + int instead_of_alloc; +}; + +struct rewrites { + struct rewrite **rewrite; + int rewrite_alloc; + int rewrite_nr; +}; + +struct remote_state { + struct remote **remotes; + int remotes_alloc; + int remotes_nr; + struct hashmap remotes_hash; + + struct hashmap branches_hash; + + struct branch *current_branch; + const char *pushremote_name; + + struct rewrites rewrites; + struct rewrites rewrites_push; + + int initialized; +}; + +void remote_state_clear(struct remote_state *remote_state); +struct remote_state *remote_state_new(void); + struct remote { struct hashmap_entry ent; @@ -256,6 +290,7 @@ int remote_find_tracking(struct remote *remote, struct refspec_item *refspec); * branch_get(name) for "refs/heads/{name}", or with branch_get(NULL) for HEAD. */ struct branch { + struct hashmap_entry ent; /* The short name of the branch. */ const char *name; diff --git a/repo-settings.c b/repo-settings.c index b93e91a212..00ca5571a1 100644 --- a/repo-settings.c +++ b/repo-settings.c @@ -17,6 +17,9 @@ void prepare_repo_settings(struct repository *r) char *strval; int manyfiles; + if (!r->gitdir) + BUG("Cannot add settings for uninitialized repository"); + if (r->settings.initialized++) return; diff --git a/repository.c b/repository.c index c5b90ba93e..34610c5a33 100644 --- a/repository.c +++ b/repository.c @@ -9,6 +9,7 @@ #include "config.h" #include "object.h" #include "lockfile.h" +#include "remote.h" #include "submodule-config.h" #include "sparse-index.h" #include "promisor-remote.h" @@ -24,6 +25,7 @@ void initialize_the_repository(void) the_repo.index = &the_index; the_repo.objects = raw_object_store_new(); + the_repo.remote_state = remote_state_new(); the_repo.parsed_objects = parsed_object_pool_new(); repo_set_hash_algo(&the_repo, GIT_HASH_SHA1); @@ -80,6 +82,8 @@ void repo_set_gitdir(struct repository *repo, expand_base_dir(&repo->objects->odb->path, o->object_dir, repo->commondir, "objects"); + repo->objects->odb->disable_ref_updates = o->disable_ref_updates; + free(repo->objects->alternate_db); repo->objects->alternate_db = xstrdup_or_null(o->alternate_db); expand_base_dir(&repo->graft_file, o->graft_file, @@ -164,6 +168,7 @@ int repo_init(struct repository *repo, repo->objects = raw_object_store_new(); repo->parsed_objects = parsed_object_pool_new(); + repo->remote_state = remote_state_new(); if (repo_init_gitdir(repo, gitdir)) goto error; @@ -270,6 +275,11 @@ void repo_clear(struct repository *repo) promisor_remote_clear(repo->promisor_remote_config); FREE_AND_NULL(repo->promisor_remote_config); } + + if (repo->remote_state) { + remote_state_clear(repo->remote_state); + FREE_AND_NULL(repo->remote_state); + } } int repo_read_index(struct repository *repo) diff --git a/repository.h b/repository.h index a057653981..2b5cf97f31 100644 --- a/repository.h +++ b/repository.h @@ -11,6 +11,7 @@ struct pathspec; struct raw_object_store; struct submodule_cache; struct promisor_remote_config; +struct remote_state; enum untracked_cache_setting { UNTRACKED_CACHE_KEEP, @@ -127,6 +128,9 @@ struct repository { */ struct index_state *index; + /* Repository's remotes and associated structures. */ + struct remote_state *remote_state; + /* Repository's current hash algorithm, as serialized on disk. */ const struct git_hash_algo *hash_algo; @@ -158,6 +162,7 @@ struct set_gitdir_args { const char *graft_file; const char *index_file; const char *alternate_db; + int disable_ref_updates; }; void repo_set_gitdir(struct repository *repo, const char *root, diff --git a/revision.c b/revision.c index 1981a0859f..250f61e8cf 100644 --- a/revision.c +++ b/revision.c @@ -44,10 +44,15 @@ static inline int want_ancestry(const struct rev_info *revs); void show_object_with_name(FILE *out, struct object *obj, const char *name) { - const char *p; - fprintf(out, "%s ", oid_to_hex(&obj->oid)); - for (p = name; *p && *p != '\n'; p++) + /* + * This "for (const char *p = ..." is made as a first step towards + * making use of such declarations elsewhere in our codebase. If + * it causes compilation problems on your platform, please report + * it to the Git mailing list at git@vger.kernel.org. In the meantime, + * adding -std=gnu99 to CFLAGS may help if you are with older GCC. + */ + for (const char *p = name; *p && *p != '\n'; p++) fputc(*p, out); fputc('\n', out); } @@ -2493,7 +2498,7 @@ static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg } else if (!strcmp(arg, "--all-match")) { revs->grep_filter.all_match = 1; } else if (!strcmp(arg, "--invert-grep")) { - revs->invert_grep = 1; + revs->grep_filter.no_body_match = 1; } else if ((argcount = parse_long_opt("encoding", argv, &optarg))) { if (strcmp(optarg, "none")) git_log_output_encoding = xstrdup(optarg); @@ -3778,7 +3783,7 @@ static int commit_match(struct commit *commit, struct rev_info *opt) (char *)message, strlen(message)); strbuf_release(&buf); unuse_commit_buffer(commit, message); - return opt->invert_grep ? !retval : retval; + return retval; } static inline int want_ancestry(const struct rev_info *revs) diff --git a/revision.h b/revision.h index 5578bb4720..3f66147bfd 100644 --- a/revision.h +++ b/revision.h @@ -246,8 +246,6 @@ struct rev_info { /* Filter by commit log message */ struct grep_opt grep_filter; - /* Negate the match of grep_filter */ - int invert_grep; /* Display history graph */ struct git_graph *graph; diff --git a/run-command.c b/run-command.c index f40df01c77..3ea2c2fc10 100644 --- a/run-command.c +++ b/run-command.c @@ -380,7 +380,7 @@ static void child_err_spew(struct child_process *cmd, struct child_err *cerr) switch (cerr->err) { case CHILD_ERR_CHDIR: error_errno("exec '%s': cd to '%s' failed", - cmd->argv[0], cmd->dir); + cmd->args.v[0], cmd->dir); break; case CHILD_ERR_DUP2: error_errno("dup2() in child failed"); @@ -392,12 +392,12 @@ static void child_err_spew(struct child_process *cmd, struct child_err *cerr) error_errno("sigprocmask failed restoring signals"); break; case CHILD_ERR_ENOENT: - error_errno("cannot run %s", cmd->argv[0]); + error_errno("cannot run %s", cmd->args.v[0]); break; case CHILD_ERR_SILENT: break; case CHILD_ERR_ERRNO: - error_errno("cannot exec '%s'", cmd->argv[0]); + error_errno("cannot exec '%s'", cmd->args.v[0]); break; } set_error_routine(old_errfn); @@ -405,7 +405,7 @@ static void child_err_spew(struct child_process *cmd, struct child_err *cerr) static int prepare_cmd(struct strvec *out, const struct child_process *cmd) { - if (!cmd->argv[0]) + if (!cmd->args.v[0]) BUG("command is empty"); /* @@ -415,11 +415,11 @@ static int prepare_cmd(struct strvec *out, const struct child_process *cmd) strvec_push(out, SHELL_PATH); if (cmd->git_cmd) { - prepare_git_cmd(out, cmd->argv); + prepare_git_cmd(out, cmd->args.v); } else if (cmd->use_shell) { - prepare_shell_cmd(out, cmd->argv); + prepare_shell_cmd(out, cmd->args.v); } else { - strvec_pushv(out, cmd->argv); + strvec_pushv(out, cmd->args.v); } /* @@ -552,20 +552,17 @@ static int wait_or_whine(pid_t pid, const char *argv0, int in_signal) while ((waiting = waitpid(pid, &status, 0)) < 0 && errno == EINTR) ; /* nothing */ - if (in_signal) { - if (WIFEXITED(status)) - code = WEXITSTATUS(status); - return code; - } if (waiting < 0) { failed_errno = errno; - error_errno("waitpid for %s failed", argv0); + if (!in_signal) + error_errno("waitpid for %s failed", argv0); } else if (waiting != pid) { - error("waitpid is confused (%s)", argv0); + if (!in_signal) + error("waitpid is confused (%s)", argv0); } else if (WIFSIGNALED(status)) { code = WTERMSIG(status); - if (code != SIGINT && code != SIGQUIT && code != SIGPIPE) + if (!in_signal && code != SIGINT && code != SIGQUIT && code != SIGPIPE) error("%s died of signal %d", argv0, code); /* * This return value is chosen so that code & 0xff @@ -576,10 +573,12 @@ static int wait_or_whine(pid_t pid, const char *argv0, int in_signal) } else if (WIFEXITED(status)) { code = WEXITSTATUS(status); } else { - error("waitpid is confused (%s)", argv0); + if (!in_signal) + error("waitpid is confused (%s)", argv0); } - clear_child_for_cleanup(pid); + if (!in_signal) + clear_child_for_cleanup(pid); errno = failed_errno; return code; @@ -655,15 +654,10 @@ static void trace_run_command(const struct child_process *cp) sq_quote_buf_pretty(&buf, cp->dir); strbuf_addch(&buf, ';'); } - /* - * The caller is responsible for initializing cp->env from - * cp->env_array if needed. We only check one place. - */ - if (cp->env) - trace_add_env(&buf, cp->env); + trace_add_env(&buf, cp->env_array.v); if (cp->git_cmd) strbuf_addstr(&buf, " git"); - sq_quote_argv_pretty(&buf, cp->argv); + sq_quote_argv_pretty(&buf, cp->args.v); trace_printf("%s", buf.buf); strbuf_release(&buf); @@ -676,11 +670,6 @@ int start_command(struct child_process *cmd) int failed_errno; char *str; - if (!cmd->argv) - cmd->argv = cmd->args.v; - if (!cmd->env) - cmd->env = cmd->env_array.v; - /* * In case of errors we must keep the promise to close FDs * that have been passed in via ->in and ->out. @@ -729,7 +718,7 @@ int start_command(struct child_process *cmd) str = "standard error"; fail_pipe: error("cannot create %s pipe for %s: %s", - str, cmd->argv[0], strerror(failed_errno)); + str, cmd->args.v[0], strerror(failed_errno)); child_process_clear(cmd); errno = failed_errno; return -1; @@ -758,7 +747,7 @@ fail_pipe: failed_errno = errno; cmd->pid = -1; if (!cmd->silent_exec_failure) - error_errno("cannot run %s", cmd->argv[0]); + error_errno("cannot run %s", cmd->args.v[0]); goto end_of_spawn; } @@ -770,7 +759,7 @@ fail_pipe: set_cloexec(null_fd); } - childenv = prep_childenv(cmd->env); + childenv = prep_childenv(cmd->env_array.v); atfork_prepare(&as); /* @@ -868,7 +857,7 @@ fail_pipe: } atfork_parent(&as); if (cmd->pid < 0) - error_errno("cannot fork() for %s", cmd->argv[0]); + error_errno("cannot fork() for %s", cmd->args.v[0]); else if (cmd->clean_on_exit) mark_child_for_cleanup(cmd->pid, cmd); @@ -885,7 +874,7 @@ fail_pipe: * At this point we know that fork() succeeded, but exec() * failed. Errors have been reported to our stderr. */ - wait_or_whine(cmd->pid, cmd->argv[0], 0); + wait_or_whine(cmd->pid, cmd->args.v[0], 0); child_err_spew(cmd, &cerr); failed_errno = errno; cmd->pid = -1; @@ -902,7 +891,7 @@ end_of_spawn: #else { int fhin = 0, fhout = 1, fherr = 2; - const char **sargv = cmd->argv; + const char **sargv = cmd->args.v; struct strvec nargv = STRVEC_INIT; if (cmd->no_stdin) @@ -929,20 +918,20 @@ end_of_spawn: fhout = dup(cmd->out); if (cmd->git_cmd) - cmd->argv = prepare_git_cmd(&nargv, cmd->argv); + cmd->args.v = prepare_git_cmd(&nargv, sargv); else if (cmd->use_shell) - cmd->argv = prepare_shell_cmd(&nargv, cmd->argv); + cmd->args.v = prepare_shell_cmd(&nargv, sargv); - cmd->pid = mingw_spawnvpe(cmd->argv[0], cmd->argv, (char**) cmd->env, + cmd->pid = mingw_spawnvpe(cmd->args.v[0], cmd->args.v, (char**) cmd->env_array.v, cmd->dir, fhin, fhout, fherr); failed_errno = errno; if (cmd->pid < 0 && (!cmd->silent_exec_failure || errno != ENOENT)) - error_errno("cannot spawn %s", cmd->argv[0]); + error_errno("cannot spawn %s", cmd->args.v[0]); if (cmd->clean_on_exit && cmd->pid >= 0) mark_child_for_cleanup(cmd->pid, cmd); strvec_clear(&nargv); - cmd->argv = sargv; + cmd->args.v = sargv; if (fhin != 0) close(fhin); if (fhout != 1) @@ -992,7 +981,7 @@ end_of_spawn: int finish_command(struct child_process *cmd) { - int ret = wait_or_whine(cmd->pid, cmd->argv[0], 0); + int ret = wait_or_whine(cmd->pid, cmd->args.v[0], 0); trace2_child_exit(cmd, ret); child_process_clear(cmd); invalidate_lstat_cache(); @@ -1001,7 +990,7 @@ int finish_command(struct child_process *cmd) int finish_command_in_signal(struct child_process *cmd) { - int ret = wait_or_whine(cmd->pid, cmd->argv[0], 1); + int ret = wait_or_whine(cmd->pid, cmd->args.v[0], 1); trace2_child_exit(cmd, ret); return ret; } @@ -1039,7 +1028,7 @@ int run_command_v_opt_cd_env_tr2(const char **argv, int opt, const char *dir, const char *const *env, const char *tr2_class) { struct child_process cmd = CHILD_PROCESS_INIT; - cmd.argv = argv; + strvec_pushv(&cmd.args, argv); cmd.no_stdin = opt & RUN_COMMAND_NO_STDIN ? 1 : 0; cmd.git_cmd = opt & RUN_GIT_CMD ? 1 : 0; cmd.stdout_to_stderr = opt & RUN_COMMAND_STDOUT_TO_STDERR ? 1 : 0; @@ -1049,7 +1038,8 @@ int run_command_v_opt_cd_env_tr2(const char **argv, int opt, const char *dir, cmd.wait_after_clean = opt & RUN_WAIT_AFTER_CLEAN ? 1 : 0; cmd.close_object_store = opt & RUN_CLOSE_OBJECT_STORE ? 1 : 0; cmd.dir = dir; - cmd.env = env; + if (env) + strvec_pushv(&cmd.env_array, (const char **)env); cmd.trace2_child_class = tr2_class; return run_command(&cmd); } @@ -1335,7 +1325,8 @@ int run_hook_ve(const char *const *env, const char *name, va_list args) strvec_push(&hook.args, p); while ((p = va_arg(args, const char *))) strvec_push(&hook.args, p); - hook.env = env; + if (env) + strvec_pushv(&hook.env_array, (const char **)env); hook.no_stdin = 1; hook.stdout_to_stderr = 1; hook.trace2_hook_name = name; diff --git a/run-command.h b/run-command.h index 4987826258..2be5f5d642 100644 --- a/run-command.h +++ b/run-command.h @@ -44,22 +44,35 @@ struct child_process { /** - * The .argv member is set up as an array of string pointers (NULL - * terminated), of which .argv[0] is the program name to run (usually - * without a path). If the command to run is a git command, set argv[0] to - * the command name without the 'git-' prefix and set .git_cmd = 1. + * The .args is a `struct strvec', use that API to manipulate + * it, e.g. strvec_pushv() to add an existing "const char **" + * vector. * - * Note that the ownership of the memory pointed to by .argv stays with the - * caller, but it should survive until `finish_command` completes. If the - * .argv member is NULL, `start_command` will point it at the .args - * `strvec` (so you may use one or the other, but you must use exactly - * one). The memory in .args will be cleaned up automatically during - * `finish_command` (or during `start_command` when it is unsuccessful). + * If the command to run is a git command, set the first + * element in the strvec to the command name without the + * 'git-' prefix and set .git_cmd = 1. * + * The memory in .args will be cleaned up automatically during + * `finish_command` (or during `start_command` when it is unsuccessful). */ - const char **argv; - struct strvec args; + + /** + * Like .args the .env_array is a `struct strvec'. + * + * To modify the environment of the sub-process, specify an array of + * environment settings. Each string in the array manipulates the + * environment. + * + * - If the string is of the form "VAR=value", i.e. it contains '=' + * the variable is added to the child process's environment. + * + * - If the string does not contain '=', it names an environment + * variable that will be removed from the child process's environment. + * + * The memory in .env_array will be cleaned up automatically during + * `finish_command` (or during `start_command` when it is unsuccessful). + */ struct strvec env_array; pid_t pid; @@ -96,23 +109,6 @@ struct child_process { */ const char *dir; - /** - * To modify the environment of the sub-process, specify an array of - * string pointers (NULL terminated) in .env: - * - * - If the string is of the form "VAR=value", i.e. it contains '=' - * the variable is added to the child process's environment. - * - * - If the string does not contain '=', it names an environment - * variable that will be removed from the child process's environment. - * - * If the .env member is NULL, `start_command` will point it at the - * .env_array `strvec` (so you may use one or the other, but not both). - * The memory in .env_array will be cleaned up automatically during - * `finish_command` (or during `start_command` when it is unsuccessful). - */ - const char *const *env; - unsigned no_stdin:1; unsigned no_stdout:1; unsigned no_stderr:1; diff --git a/sequencer.c b/sequencer.c index ea96837cde..6abd72160c 100644 --- a/sequencer.c +++ b/sequencer.c @@ -1164,18 +1164,14 @@ static int run_rewrite_hook(const struct object_id *oldoid, const struct object_id *newoid) { struct child_process proc = CHILD_PROCESS_INIT; - const char *argv[3]; int code; struct strbuf sb = STRBUF_INIT; + const char *hook_path = find_hook("post-rewrite"); - argv[0] = find_hook("post-rewrite"); - if (!argv[0]) + if (!hook_path) return 0; - argv[1] = "amend"; - argv[2] = NULL; - - proc.argv = argv; + strvec_pushl(&proc.args, hook_path, "amend", NULL); proc.in = -1; proc.stdout_to_stderr = 1; proc.trace2_hook_name = "post-rewrite"; @@ -1284,6 +1280,8 @@ void print_commit_summary(struct repository *r, struct pretty_print_context pctx = {0}; struct strbuf author_ident = STRBUF_INIT; struct strbuf committer_ident = STRBUF_INIT; + struct ref_store *refs; + int resolve_errno; commit = lookup_commit(r, oid); if (!commit) @@ -1333,9 +1331,13 @@ void print_commit_summary(struct repository *r, rev.diffopt.break_opt = 0; diff_setup_done(&rev.diffopt); - head = resolve_ref_unsafe("HEAD", 0, NULL, NULL); - if (!head) + refs = get_main_ref_store(the_repository); + head = refs_resolve_ref_unsafe(refs, "HEAD", 0, NULL, NULL, + &resolve_errno); + if (!head) { + errno = resolve_errno; die_errno(_("unable to resolve HEAD after creating commit")); + } if (!strcmp(head, "HEAD")) head = _("detached HEAD"); else @@ -3495,17 +3497,12 @@ static int error_failed_squash(struct repository *r, static int do_exec(struct repository *r, const char *command_line) { - struct strvec child_env = STRVEC_INIT; const char *child_argv[] = { NULL, NULL }; int dirty, status; fprintf(stderr, _("Executing: %s\n"), command_line); child_argv[0] = command_line; - strvec_pushf(&child_env, "GIT_DIR=%s", absolute_path(get_git_dir())); - strvec_pushf(&child_env, "GIT_WORK_TREE=%s", - absolute_path(get_git_work_tree())); - status = run_command_v_opt_cd_env(child_argv, RUN_USING_SHELL, NULL, - child_env.v); + status = run_command_v_opt(child_argv, RUN_USING_SHELL); /* force re-reading of the cache */ if (discard_index(r->index) < 0 || repo_read_index(r) < 0) @@ -3535,8 +3532,6 @@ static int do_exec(struct repository *r, const char *command_line) status = 1; } - strvec_clear(&child_env); - return status; } @@ -4228,6 +4223,8 @@ static int run_git_checkout(struct repository *r, struct replay_opts *opts, cmd.git_cmd = 1; + if (startup_info->original_cwd) + cmd.dir = startup_info->original_cwd; strvec_push(&cmd.args, "checkout"); strvec_push(&cmd.args, commit); strvec_pushf(&cmd.env_array, GIT_REFLOG_ACTION "=%s", action); @@ -5496,7 +5493,7 @@ static void todo_list_add_exec_commands(struct todo_list *todo_list, } /* insert or append final <commands> */ - if (insert || nr == todo_list->nr) { + if (insert) { ALLOC_GROW(items, nr + commands->nr, alloc); COPY_ARRAY(items + nr, base_items, commands->nr); nr += commands->nr; @@ -12,6 +12,7 @@ static int work_tree_config_is_bogus; static struct startup_info the_startup_info; struct startup_info *startup_info = &the_startup_info; +const char *tmp_original_cwd; /* * The input parameter must contain an absolute path, and it must already be @@ -432,6 +433,69 @@ void setup_work_tree(void) initialized = 1; } +static void setup_original_cwd(void) +{ + struct strbuf tmp = STRBUF_INIT; + const char *worktree = NULL; + int offset = -1; + + if (!tmp_original_cwd) + return; + + /* + * startup_info->original_cwd points to the current working + * directory we inherited from our parent process, which is a + * directory we want to avoid removing. + * + * For convience, we would like to have the path relative to the + * worktree instead of an absolute path. + * + * Yes, startup_info->original_cwd is usually the same as 'prefix', + * but differs in two ways: + * - prefix has a trailing '/' + * - if the user passes '-C' to git, that modifies the prefix but + * not startup_info->original_cwd. + */ + + /* Normalize the directory */ + strbuf_realpath(&tmp, tmp_original_cwd, 1); + free((char*)tmp_original_cwd); + tmp_original_cwd = NULL; + startup_info->original_cwd = strbuf_detach(&tmp, NULL); + + /* + * Get our worktree; we only protect the current working directory + * if it's in the worktree. + */ + worktree = get_git_work_tree(); + if (!worktree) + goto no_prevention_needed; + + offset = dir_inside_of(startup_info->original_cwd, worktree); + if (offset >= 0) { + /* + * If startup_info->original_cwd == worktree, that is already + * protected and we don't need original_cwd as a secondary + * protection measure. + */ + if (!*(startup_info->original_cwd + offset)) + goto no_prevention_needed; + + /* + * original_cwd was inside worktree; precompose it just as + * we do prefix so that built up paths will match + */ + startup_info->original_cwd = \ + precompose_string_if_needed(startup_info->original_cwd + + offset); + return; + } + +no_prevention_needed: + free((char*)startup_info->original_cwd); + startup_info->original_cwd = NULL; +} + static int read_worktree_config(const char *var, const char *value, void *vdata) { struct repository_format *data = vdata; @@ -1330,6 +1394,7 @@ const char *setup_git_directory_gently(int *nongit_ok) setenv(GIT_PREFIX_ENVIRONMENT, "", 1); } + setup_original_cwd(); strbuf_release(&dir); strbuf_release(&gitdir); diff --git a/sparse-index.c b/sparse-index.c index 7b7ff79e04..a1d505d50e 100644 --- a/sparse-index.c +++ b/sparse-index.c @@ -122,17 +122,17 @@ static int index_has_unmerged_entries(struct index_state *istate) return 0; } -int convert_to_sparse(struct index_state *istate, int flags) +static int is_sparse_index_allowed(struct index_state *istate, int flags) { - int test_env; - if (istate->sparse_index || !istate->cache_nr || - !core_apply_sparse_checkout || !core_sparse_checkout_cone) + if (!core_apply_sparse_checkout || !core_sparse_checkout_cone) return 0; if (!istate->repo) istate->repo = the_repository; if (!(flags & SPARSE_INDEX_MEMORY_ONLY)) { + int test_env; + /* * The sparse index is not (yet) integrated with a split index. */ @@ -168,25 +168,41 @@ int convert_to_sparse(struct index_state *istate, int flags) if (!istate->sparse_checkout_patterns->use_cone_patterns) return 0; + return 1; +} + +int convert_to_sparse(struct index_state *istate, int flags) +{ /* - * NEEDSWORK: If we have unmerged entries, then stay full. - * Unmerged entries prevent the cache-tree extension from working. + * If the index is already sparse, empty, or otherwise + * cannot be converted to sparse, do not convert. */ - if (index_has_unmerged_entries(istate)) + if (istate->sparse_index || !istate->cache_nr || + !is_sparse_index_allowed(istate, flags)) return 0; - /* Clear and recompute the cache-tree */ - cache_tree_free(&istate->cache_tree); /* - * Silently return if there is a problem with the cache tree update, - * which might just be due to a conflict state in some entry. - * - * This might create new tree objects, so be sure to use - * WRITE_TREE_MISSING_OK. + * NEEDSWORK: If we have unmerged entries, then stay full. + * Unmerged entries prevent the cache-tree extension from working. */ - if (cache_tree_update(istate, WRITE_TREE_MISSING_OK)) + if (index_has_unmerged_entries(istate)) return 0; + if (!cache_tree_fully_valid(istate->cache_tree)) { + /* Clear and recompute the cache-tree */ + cache_tree_free(&istate->cache_tree); + + /* + * Silently return if there is a problem with the cache tree update, + * which might just be due to a conflict state in some entry. + * + * This might create new tree objects, so be sure to use + * WRITE_TREE_MISSING_OK. + */ + if (cache_tree_update(istate, WRITE_TREE_MISSING_OK)) + return 0; + } + remove_fsmonitor(istate); trace2_region_enter("index", "convert_to_sparse", istate->repo); @@ -313,6 +329,18 @@ void ensure_full_index(struct index_state *istate) trace2_region_leave("index", "ensure_full_index", istate->repo); } +void ensure_correct_sparsity(struct index_state *istate) +{ + /* + * If the index can be sparse, make it sparse. Otherwise, + * ensure the index is full. + */ + if (is_sparse_index_allowed(istate, 0)) + convert_to_sparse(istate, 0); + else + ensure_full_index(istate); +} + /* * This static global helps avoid infinite recursion between * expand_to_path() and index_file_exists(). diff --git a/sparse-index.h b/sparse-index.h index 9f3d7bc7fa..656bd835b2 100644 --- a/sparse-index.h +++ b/sparse-index.h @@ -4,6 +4,7 @@ struct index_state; #define SPARSE_INDEX_MEMORY_ONLY (1 << 0) int convert_to_sparse(struct index_state *istate, int flags); +void ensure_correct_sparsity(struct index_state *istate); /* * Some places in the codebase expect to search for a specific path. @@ -1006,7 +1006,12 @@ void strbuf_addftime(struct strbuf *sb, const char *fmt, const struct tm *tm, /* * There is no portable way to pass timezone information to - * strftime, so we handle %z and %Z here. + * strftime, so we handle %z and %Z here. Likewise '%s', because + * going back to an epoch time requires knowing the zone. + * + * Note that tz_offset is in the "[-+]HHMM" decimal form; this is what + * we want for %z, but the computation for %s has to convert to number + * of seconds. */ for (;;) { const char *percent = strchrnul(fmt, '%'); @@ -1019,6 +1024,13 @@ void strbuf_addftime(struct strbuf *sb, const char *fmt, const struct tm *tm, strbuf_addstr(&munged_fmt, "%%"); fmt++; break; + case 's': + strbuf_addf(&munged_fmt, "%"PRItime, + (timestamp_t)tm_to_time_t(tm) - + 3600 * (tz_offset / 100) - + 60 * (tz_offset % 100)); + fmt++; + break; case 'z': strbuf_addf(&munged_fmt, "%+05d", tz_offset); fmt++; @@ -160,7 +160,7 @@ void strbuf_grow(struct strbuf *sb, size_t amount); static inline void strbuf_setlen(struct strbuf *sb, size_t len) { if (len > (sb->alloc ? sb->alloc - 1 : 0)) - die("BUG: strbuf_setlen() beyond buffer"); + BUG("strbuf_setlen() beyond buffer"); sb->len = len; if (sb->buf != strbuf_slopbuf) sb->buf[len] = '\0'; diff --git a/sub-process.c b/sub-process.c index dfa790d3ff..cae56ae6b8 100644 --- a/sub-process.c +++ b/sub-process.c @@ -187,7 +187,7 @@ static int handshake_capabilities(struct child_process *process, *supported_capabilities |= capabilities[i].flag; } else { die("subprocess '%s' requested unsupported capability '%s'", - process->argv[0], p); + process->args.v[0], p); } } diff --git a/symlinks.c b/symlinks.c index 5232d02020..c667baa949 100644 --- a/symlinks.c +++ b/symlinks.c @@ -279,7 +279,9 @@ static void do_remove_scheduled_dirs(int new_len) { while (removal.len > new_len) { removal.buf[removal.len] = '\0'; - if (rmdir(removal.buf)) + if ((startup_info->original_cwd && + !strcmp(removal.buf, startup_info->original_cwd)) || + rmdir(removal.buf)) break; do { removal.len--; @@ -293,6 +295,10 @@ void schedule_dir_for_removal(const char *name, int len) { int match_len, last_slash, i, previous_slash; + if (startup_info->original_cwd && + !strcmp(name, startup_info->original_cwd)) + return; /* Do not remove the current working directory */ + match_len = last_slash = i = longest_path_match(name, len, removal.buf, removal.len, &previous_slash); diff --git a/t/Makefile b/t/Makefile index 882d26eee3..46cd5fc527 100644 --- a/t/Makefile +++ b/t/Makefile @@ -71,12 +71,10 @@ clean-chainlint: check-chainlint: @mkdir -p '$(CHAINLINTTMP_SQ)' && \ - err=0 && \ - for i in $(CHAINLINTTESTS); do \ - $(CHAINLINT) <chainlint/$$i.test | \ - sed -e '/^# LINT: /d' >'$(CHAINLINTTMP_SQ)'/$$i.actual && \ - diff -u chainlint/$$i.expect '$(CHAINLINTTMP_SQ)'/$$i.actual || err=1; \ - done && exit $$err + sed -e '/^# LINT: /d' $(patsubst %,chainlint/%.test,$(CHAINLINTTESTS)) >'$(CHAINLINTTMP_SQ)'/tests && \ + sed -e '/^[ ]*$$/d' $(patsubst %,chainlint/%.expect,$(CHAINLINTTESTS)) >'$(CHAINLINTTMP_SQ)'/expect && \ + $(CHAINLINT) '$(CHAINLINTTMP_SQ)'/tests | grep -v '^[ ]*$$' >'$(CHAINLINTTMP_SQ)'/actual && \ + diff -u '$(CHAINLINTTMP_SQ)'/expect '$(CHAINLINTTMP_SQ)'/actual test-lint: test-lint-duplicates test-lint-executable test-lint-shell-syntax \ test-lint-filenames @@ -466,6 +466,12 @@ explicitly providing repositories when accessing submodule objects is complete or needs to be abandoned for whatever reason (in which case the migrated codepaths still retain their performance benefits). +GIT_TEST_REQUIRE_PREREQ=<list> allows specifying a space speparated list of +prereqs that are required to succeed. If a prereq in this list is triggered by +a test and then fails then the whole test run will abort. This can help to make +sure the expected tests are executed and not silently skipped when their +dependency breaks or is simply not present in a new environment. + Naming Tests ------------ diff --git a/t/aggregate-results.sh b/t/aggregate-results.sh index 7913e206ed..7f2b83bdc8 100755 --- a/t/aggregate-results.sh +++ b/t/aggregate-results.sh @@ -6,6 +6,7 @@ success=0 failed=0 broken=0 total=0 +missing_prereq= while read file do @@ -30,10 +31,26 @@ do broken=$(($broken + $value)) ;; total) total=$(($total + $value)) ;; + missing_prereq) + missing_prereq="$missing_prereq,$value" ;; esac done <"$file" done +if test -n "$missing_prereq" +then + unique_missing_prereq=$( + echo $missing_prereq | + tr -s "," "\n" | + grep -v '^$' | + sort -u | + paste -s -d ' ') + if test -n "$unique_missing_prereq" + then + printf "\nmissing prereq: $unique_missing_prereq\n\n" + fi +fi + if test -n "$failed_tests" then printf "\nfailed test(s):$failed_tests\n\n" diff --git a/t/annotate-tests.sh b/t/annotate-tests.sh index d3b299e75c..09e86f9ba0 100644 --- a/t/annotate-tests.sh +++ b/t/annotate-tests.sh @@ -161,7 +161,7 @@ test_expect_success 'blame huge graft' ' GIT_AUTHOR_NAME=$i$j GIT_AUTHOR_EMAIL=$i$j@test.git \ git commit -a -m "$i$j" && commit=$(git rev-parse --verify HEAD) && - graft="$graft$commit " + graft="$graft$commit " || return 1 done done && printf "%s " $graft >.git/info/grafts && diff --git a/t/chainlint.sed b/t/chainlint.sed index 8a25c5b855..dc4ce37cb5 100644 --- a/t/chainlint.sed +++ b/t/chainlint.sed @@ -24,9 +24,9 @@ # in order to avoid misinterpreting the ")" in constructs such as "x=$(...)" # and "case $x in *)" as ending the subshell. # -# Lines missing a final "&&" are flagged with "?!AMP?!", and lines which chain -# commands with ";" internally rather than "&&" are flagged "?!SEMI?!". A line -# may be flagged for both violations. +# Lines missing a final "&&" are flagged with "?!AMP?!", as are lines which +# chain commands with ";" internally rather than "&&". A line may be flagged +# for both violations. # # Detection of a missing &&-link in a multi-line subshell is complicated by the # fact that the last statement before the closing ")" must not end with "&&". @@ -47,8 +47,8 @@ # "?!AMP?!" violation is removed from the "bar" line (retrieved from the "hold" # area) since the final statement of a subshell must not end with "&&". The # final line of a subshell may still break the &&-chain by using ";" internally -# to chain commands together rather than "&&", so "?!SEMI?!" is never removed -# from a line (even though "?!AMP?!" might be). +# to chain commands together rather than "&&", but an internal "?!AMP?!" is +# never removed from a line even though a line-ending "?!AMP?!" might be. # # Care is taken to recognize the last _statement_ of a multi-line subshell, not # necessarily the last textual _line_ within the subshell, since &&-chaining @@ -62,26 +62,20 @@ # receives similar treatment. # # Swallowing here-docs with arbitrary tags requires a bit of finesse. When a -# line such as "cat <<EOF >out" is seen, the here-doc tag is moved to the front -# of the line enclosed in angle brackets as a sentinel, giving "<EOF>cat >out". +# line such as "cat <<EOF" is seen, the here-doc tag is copied to the front of +# the line enclosed in angle brackets as a sentinel, giving "<EOF>cat <<EOF". # As each subsequent line is read, it is appended to the target line and a # (whitespace-loose) back-reference match /^<(.*)>\n\1$/ is attempted to see if # the content inside "<...>" matches the entirety of the newly-read line. For # instance, if the next line read is "some data", when concatenated with the -# target line, it becomes "<EOF>cat >out\nsome data", and a match is attempted +# target line, it becomes "<EOF>cat <<EOF\nsome data", and a match is attempted # to see if "EOF" matches "some data". Since it doesn't, the next line is # attempted. When a line consisting of only "EOF" (and possible whitespace) is -# encountered, it is appended to the target line giving "<EOF>cat >out\nEOF", +# encountered, it is appended to the target line giving "<EOF>cat <<EOF\nEOF", # in which case the "EOF" inside "<...>" does match the text following the # newline, thus the closing here-doc tag has been found. The closing tag line # and the "<...>" prefix on the target line are then discarded, leaving just -# the target line "cat >out". -# -# To facilitate regression testing (and manual debugging), a ">" annotation is -# applied to the line containing ")" which closes a subshell, ">>" to a line -# closing a nested subshell, and ">>>" to a line closing both at once. This -# makes it easy to detect whether the heuristics correctly identify -# end-of-subshell. +# the target line "cat <<EOF". #------------------------------------------------------------------------------ # incomplete line -- slurp up next line @@ -94,9 +88,9 @@ # here-doc -- swallow it to avoid false hits within its body (but keep the # command to which it was attached) -/<<[ ]*[-\\'"]*[A-Za-z0-9_]/ { - s/^\(.*\)<<[ ]*[-\\'"]*\([A-Za-z0-9_][A-Za-z0-9_]*\)['"]*/<\2>\1<</ - s/[ ]*<<// +/<<-*[ ]*[\\'"]*[A-Za-z0-9_]/ { + /"[^"]*<<[^"]*"/bnotdoc + s/^\(.*<<-*[ ]*\)[\\'"]*\([A-Za-z0-9_][A-Za-z0-9_]*\)['"]*/<\2>\1\2/ :hered N /^<\([^>]*\)>.*\n[ ]*\1[ ]*$/!{ @@ -106,6 +100,7 @@ s/^<[^>]*>// s/\n.*$// } +:notdoc # one-liner "(...) &&" /^[ ]*!*[ ]*(..*)[ ]*&&[ ]*$/boneline @@ -126,7 +121,7 @@ b # "&&" (but not ";" in a string) :oneline /;/{ - /"[^"]*;[^"]*"/!s/^/?!SEMI?!/ + /"[^"]*;[^"]*"/!s/;/; ?!AMP?!/ } b @@ -136,11 +131,15 @@ b h bnextln } -# "(..." line -- split off and stash "(", then process "..." as its own line +# "(..." line -- "(" opening subshell cuddled with command; temporarily replace +# "(" with sentinel "^" and process the line as if "(" had been seen solo on +# the preceding line; this temporary replacement prevents several rules from +# accidentally thinking "(" introduces a nested subshell; "^" is changed back +# to "(" at output time x -s/.*/(/ +s/.*// x -s/(// +s/(/^/ bslurp :nextln @@ -157,8 +156,10 @@ s/.*\n// /"[^'"]*'[^'"]*"/!bsqstr } :folded -# here-doc -- swallow it -/<<[ ]*[-\\'"]*[A-Za-z0-9_]/bheredoc +# here-doc -- swallow it (but not "<<" in a string) +/<<-*[ ]*[\\'"]*[A-Za-z0-9_]/{ + /"[^"]*<<[^"]*"/!bheredoc +} # comment or empty line -- discard since final non-comment, non-empty line # before closing ")", "done", "elsif", "else", or "fi" will need to be # re-visited to drop "suspect" marking since final line of those constructs @@ -171,12 +172,12 @@ s/.*\n// /"[^"]*#[^"]*"/!s/[ ]#.*$// } # one-liner "case ... esac" -/^[ ]*case[ ]*..*esac/bchkchn +/^[ ^]*case[ ]*..*esac/bchkchn # multi-line "case ... esac" -/^[ ]*case[ ]..*[ ]in/bcase +/^[ ^]*case[ ]..*[ ]in/bcase # multi-line "for ... done" or "while ... done" -/^[ ]*for[ ]..*[ ]in/bcont -/^[ ]*while[ ]/bcont +/^[ ^]*for[ ]..*[ ]in/bcont +/^[ ^]*while[ ]/bcont /^[ ]*do[ ]/bcont /^[ ]*do[ ]*$/bcont /;[ ]*do/bcont @@ -187,7 +188,7 @@ s/.*\n// /||[ ]*exit[ ]/bcont /||[ ]*exit[ ]*$/bcont # multi-line "if...elsif...else...fi" -/^[ ]*if[ ]/bcont +/^[ ^]*if[ ]/bcont /^[ ]*then[ ]/bcont /^[ ]*then[ ]*$/bcont /;[ ]*then/bcont @@ -200,15 +201,15 @@ s/.*\n// /^[ ]*fi[ ]*[<>|]/bdone /^[ ]*fi[ ]*)/bdone # nested one-liner "(...) &&" -/^[ ]*(.*)[ ]*&&[ ]*$/bchkchn +/^[ ^]*(.*)[ ]*&&[ ]*$/bchkchn # nested one-liner "(...)" -/^[ ]*(.*)[ ]*$/bchkchn +/^[ ^]*(.*)[ ]*$/bchkchn # nested one-liner "(...) >x" (or "2>x" or "<x" or "|x") -/^[ ]*(.*)[ ]*[0-9]*[<>|]/bchkchn +/^[ ^]*(.*)[ ]*[0-9]*[<>|]/bchkchn # nested multi-line "(...\n...)" -/^[ ]*(/bnest +/^[ ^]*(/bnest # multi-line "{...\n...}" -/^[ ]*{/bblock +/^[ ^]*{/bblock # closing ")" on own line -- exit subshell /^[ ]*)/bclssolo # "$((...))" -- arithmetic expansion; not closing ")" @@ -230,16 +231,18 @@ s/.*\n// # string and not ";;" in one-liner "case...esac") /;/{ /;;/!{ - /"[^"]*;[^"]*"/!s/^/?!SEMI?!/ + /"[^"]*;[^"]*"/!s/;/; ?!AMP?!/ } } # line ends with pipe "...|" -- valid; not missing "&&" /|[ ]*$/bcont # missing end-of-line "&&" -- mark suspect -/&&[ ]*$/!s/^/?!AMP?!/ +/&&[ ]*$/!s/$/ ?!AMP?!/ :cont # retrieve and print previous line x +s/^\([ ]*\)^/\1(/ +s/?!HERE?!/<</g n bslurp @@ -280,8 +283,7 @@ bfolded # found here-doc -- swallow it to avoid false hits within its body (but keep # the command to which it was attached) :heredoc -s/^\(.*\)<<[ ]*[-\\'"]*\([A-Za-z0-9_][A-Za-z0-9_]*\)['"]*/<\2>\1<</ -s/[ ]*<<// +s/^\(.*\)<<\(-*[ ]*\)[\\'"]*\([A-Za-z0-9_][A-Za-z0-9_]*\)['"]*/<\3>\1?!HERE?!\2\3/ :hdocsub N /^<\([^>]*\)>.*\n[ ]*\1[ ]*$/!{ @@ -295,7 +297,15 @@ bfolded # found "case ... in" -- pass through untouched :case x +s/^\([ ]*\)^/\1(/ +s/?!HERE?!/<</g n +:cascom +/^[ ]*#/{ + N + s/.*\n// + bcascom +} /^[ ]*esac/bslurp bcase @@ -303,7 +313,7 @@ bcase # that line legitimately lacks "&&" :else x -s/?!AMP?!// +s/\( ?!AMP?!\)* ?!AMP?!$// x bcont @@ -311,7 +321,7 @@ bcont # "suspect" from final contained line since that line legitimately lacks "&&" :done x -s/?!AMP?!// +s/\( ?!AMP?!\)* ?!AMP?!$// x # is 'done' or 'fi' cuddled with ")" to close subshell? /done.*)/bclose @@ -322,11 +332,18 @@ bchkchn :nest x :nstslrp +s/^\([ ]*\)^/\1(/ +s/?!HERE?!/<</g n +:nstcom +# comment -- not closing ")" if in comment +/^[ ]*#/{ + N + s/.*\n// + bnstcom +} # closing ")" on own line -- stop nested slurp /^[ ]*)/bnstcl -# comment -- not closing ")" if in comment -/^[ ]*#/bnstcnt # "$((...))" -- arithmetic expansion; not closing ")" /\$(([^)][^)]*))[^)]*$/bnstcnt # "$(...)" -- command substitution; not closing ")" @@ -337,7 +354,6 @@ n x bnstslrp :nstcl -s/^/>>/ # is it "))" which closes nested and parent subshells? /)[ ]*)/bslurp bchkchn @@ -345,7 +361,15 @@ bchkchn # found multi-line "{...\n...}" block -- pass through untouched :block x +s/^\([ ]*\)^/\1(/ +s/?!HERE?!/<</g n +:blkcom +/^[ ]*#/{ + N + s/.*\n// + bblkcom +} # closing "}" -- stop block slurp /}/bchkchn bblock @@ -354,16 +378,22 @@ bblock # since that line legitimately lacks "&&" and exit subshell loop :clssolo x -s/?!AMP?!// +s/\( ?!AMP?!\)* ?!AMP?!$// +s/^\([ ]*\)^/\1(/ +s/?!HERE?!/<</g p x -s/^/>/ +s/^\([ ]*\)^/\1(/ +s/?!HERE?!/<</g b # found closing "...)" -- exit subshell loop :close x +s/^\([ ]*\)^/\1(/ +s/?!HERE?!/<</g p x -s/^/>/ +s/^\([ ]*\)^/\1(/ +s/?!HERE?!/<</g b diff --git a/t/chainlint/arithmetic-expansion.expect b/t/chainlint/arithmetic-expansion.expect index 09457d3196..46ee1046af 100644 --- a/t/chainlint/arithmetic-expansion.expect +++ b/t/chainlint/arithmetic-expansion.expect @@ -2,8 +2,8 @@ foo && bar=$((42 + 1)) && baz ->) && +) && ( -?!AMP?! bar=$((42 + 1)) + bar=$((42 + 1)) ?!AMP?! baz ->) +) diff --git a/t/chainlint/bash-array.expect b/t/chainlint/bash-array.expect index c4a830d1c1..4c34eaee45 100644 --- a/t/chainlint/bash-array.expect +++ b/t/chainlint/bash-array.expect @@ -2,9 +2,9 @@ foo && bar=(gumbo stumbo wumbo) && baz ->) && +) && ( foo && bar=${#bar[@]} && baz ->) +) diff --git a/t/chainlint/blank-line.expect b/t/chainlint/blank-line.expect index 3be939ed38..f76fde1ffb 100644 --- a/t/chainlint/blank-line.expect +++ b/t/chainlint/blank-line.expect @@ -1,4 +1,4 @@ ( nothing && something ->) +) diff --git a/t/chainlint/blank-line.test b/t/chainlint/blank-line.test index f6dd14302b..0fdf15b3e1 100644 --- a/t/chainlint/blank-line.test +++ b/t/chainlint/blank-line.test @@ -3,7 +3,7 @@ nothing && something -# LINT: swallow blank lines since final _statement_ before subshell end is +# LINT: ignore blank lines since final _statement_ before subshell end is # LINT: significant to "&&"-check, not final _line_ (which might be blank) diff --git a/t/chainlint/block-comment.expect b/t/chainlint/block-comment.expect new file mode 100644 index 0000000000..d10b2eeaf2 --- /dev/null +++ b/t/chainlint/block-comment.expect @@ -0,0 +1,6 @@ +( + { + echo a && + echo b + } +) diff --git a/t/chainlint/block-comment.test b/t/chainlint/block-comment.test new file mode 100644 index 0000000000..df2beea888 --- /dev/null +++ b/t/chainlint/block-comment.test @@ -0,0 +1,8 @@ +( + { + # show a + echo a && + # show b + echo b + } +) diff --git a/t/chainlint/block.expect b/t/chainlint/block.expect index fed7e89ae8..da60257ebc 100644 --- a/t/chainlint/block.expect +++ b/t/chainlint/block.expect @@ -7,6 +7,6 @@ bar && { echo c -?!AMP?! } + } ?!AMP?! baz ->) +) diff --git a/t/chainlint/block.test b/t/chainlint/block.test index d859151af1..0a82fd579f 100644 --- a/t/chainlint/block.test +++ b/t/chainlint/block.test @@ -1,6 +1,5 @@ ( -# LINT: missing "&&" in block not currently detected (for consistency with -# LINT: --chain-lint at top level and to provide escape hatch if needed) +# LINT: missing "&&" after first "echo" foo && { echo a diff --git a/t/chainlint/broken-chain.expect b/t/chainlint/broken-chain.expect index 55b0f42a53..cfb58fb6b9 100644 --- a/t/chainlint/broken-chain.expect +++ b/t/chainlint/broken-chain.expect @@ -1,6 +1,6 @@ ( foo && -?!AMP?! bar + bar ?!AMP?! baz && wop ->) +) diff --git a/t/chainlint/broken-chain.test b/t/chainlint/broken-chain.test index 3cc67b65d0..2a44aa73b7 100644 --- a/t/chainlint/broken-chain.test +++ b/t/chainlint/broken-chain.test @@ -1,6 +1,6 @@ ( foo && -# LINT: missing "&&" from 'bar' +# LINT: missing "&&" from "bar" bar baz && # LINT: final statement before closing ")" legitimately lacks "&&" diff --git a/t/chainlint/case-comment.expect b/t/chainlint/case-comment.expect new file mode 100644 index 0000000000..1e4b054bda --- /dev/null +++ b/t/chainlint/case-comment.expect @@ -0,0 +1,8 @@ +( + case "$x" in + x) foo ;; + *) + bar + ;; + esac +) diff --git a/t/chainlint/case-comment.test b/t/chainlint/case-comment.test new file mode 100644 index 0000000000..641c157b98 --- /dev/null +++ b/t/chainlint/case-comment.test @@ -0,0 +1,11 @@ +( + case "$x" in + # found foo + x) foo ;; + # found other + *) + # treat it as bar + bar + ;; + esac +) diff --git a/t/chainlint/case.expect b/t/chainlint/case.expect index 41f121fbbf..31f280d8ce 100644 --- a/t/chainlint/case.expect +++ b/t/chainlint/case.expect @@ -4,16 +4,16 @@ *) bar ;; esac && foobar ->) && +) && ( case "$x" in x) foo ;; *) bar ;; -?!AMP?! esac + esac ?!AMP?! foobar ->) && +) && ( case "$x" in 1) true;; esac && -?!AMP?! case "$y" in 2) false;; esac + case "$y" in 2) false;; esac ?!AMP?! foobar ->) +) diff --git a/t/chainlint/case.test b/t/chainlint/case.test index 5ef6ff7db5..4cb086bf87 100644 --- a/t/chainlint/case.test +++ b/t/chainlint/case.test @@ -1,5 +1,5 @@ ( -# LINT: "...)" arms in 'case' not misinterpreted as subshell-closing ")" +# LINT: "...)" arms in "case" not misinterpreted as subshell-closing ")" case "$x" in x) foo ;; *) bar ;; @@ -7,7 +7,7 @@ foobar ) && ( -# LINT: missing "&&" on 'esac' +# LINT: missing "&&" on "esac" case "$x" in x) foo ;; *) bar ;; @@ -15,7 +15,7 @@ foobar ) && ( -# LINT: "...)" arm in one-liner 'case' not misinterpreted as closing ")" +# LINT: "...)" arm in one-liner "case" not misinterpreted as closing ")" case "$x" in 1) true;; esac && # LINT: same but missing "&&" case "$y" in 2) false;; esac diff --git a/t/chainlint/close-nested-and-parent-together.expect b/t/chainlint/close-nested-and-parent-together.expect index 2a910f9d66..72d482f76d 100644 --- a/t/chainlint/close-nested-and-parent-together.expect +++ b/t/chainlint/close-nested-and-parent-together.expect @@ -1,4 +1,3 @@ -( -cd foo && +(cd foo && (bar && ->>> baz)) + baz)) diff --git a/t/chainlint/close-subshell.expect b/t/chainlint/close-subshell.expect index 184688718a..0f87db9ae6 100644 --- a/t/chainlint/close-subshell.expect +++ b/t/chainlint/close-subshell.expect @@ -1,25 +1,25 @@ ( foo ->) && +) && ( bar ->) >out && +) >out && ( baz ->) 2>err && +) 2>err && ( boo ->) <input && +) <input && ( bip ->) | wuzzle && +) | wuzzle && ( bop ->) | fazz fozz && +) | fazz fozz && ( bup ->) | +) | fuzzle && ( yop ->) +) diff --git a/t/chainlint/command-substitution.expect b/t/chainlint/command-substitution.expect index ad4118e537..c72e4df9e7 100644 --- a/t/chainlint/command-substitution.expect +++ b/t/chainlint/command-substitution.expect @@ -2,8 +2,8 @@ foo && bar=$(gobble) && baz ->) && +) && ( -?!AMP?! bar=$(gobble blocks) + bar=$(gobble blocks) ?!AMP?! baz ->) +) diff --git a/t/chainlint/comment.expect b/t/chainlint/comment.expect index 3be939ed38..f76fde1ffb 100644 --- a/t/chainlint/comment.expect +++ b/t/chainlint/comment.expect @@ -1,4 +1,4 @@ ( nothing && something ->) +) diff --git a/t/chainlint/complex-if-in-cuddled-loop.expect b/t/chainlint/complex-if-in-cuddled-loop.expect index 9674b88cf2..2fca183409 100644 --- a/t/chainlint/complex-if-in-cuddled-loop.expect +++ b/t/chainlint/complex-if-in-cuddled-loop.expect @@ -1,10 +1,9 @@ -( -for i in a b c; do +(for i in a b c; do if test "$(echo $(waffle bat))" = "eleventeen" && test "$x" = "$y"; then : else echo >file fi -> done) && + done) && test ! -f file diff --git a/t/chainlint/complex-if-in-cuddled-loop.test b/t/chainlint/complex-if-in-cuddled-loop.test index 571bbd85cd..5efeda58b2 100644 --- a/t/chainlint/complex-if-in-cuddled-loop.test +++ b/t/chainlint/complex-if-in-cuddled-loop.test @@ -1,4 +1,4 @@ -# LINT: 'for' loop cuddled with "(" and ")" and nested 'if' with complex +# LINT: "for" loop cuddled with "(" and ")" and nested "if" with complex # LINT: multi-line condition; indented with spaces, not tabs (for i in a b c; do if test "$(echo $(waffle bat))" = "eleventeen" && diff --git a/t/chainlint/cuddled-if-then-else.expect b/t/chainlint/cuddled-if-then-else.expect index ab2a026fbc..1d8ed58c49 100644 --- a/t/chainlint/cuddled-if-then-else.expect +++ b/t/chainlint/cuddled-if-then-else.expect @@ -1,7 +1,6 @@ -( -if test -z ""; then +(if test -z ""; then echo empty else echo bizzy -> fi) && + fi) && echo foobar diff --git a/t/chainlint/cuddled-if-then-else.test b/t/chainlint/cuddled-if-then-else.test index eed774a9d6..7c53f4efe3 100644 --- a/t/chainlint/cuddled-if-then-else.test +++ b/t/chainlint/cuddled-if-then-else.test @@ -1,4 +1,4 @@ -# LINT: 'if' cuddled with "(" and ")"; indented with spaces, not tabs +# LINT: "if" cuddled with "(" and ")"; indented with spaces, not tabs (if test -z ""; then echo empty else diff --git a/t/chainlint/cuddled-loop.expect b/t/chainlint/cuddled-loop.expect index 8c0260d7f1..9cf260708e 100644 --- a/t/chainlint/cuddled-loop.expect +++ b/t/chainlint/cuddled-loop.expect @@ -1,5 +1,4 @@ -( - while read x +( while read x do foobar bop || exit 1 -> done <file ) && + done <file ) && outside subshell diff --git a/t/chainlint/cuddled-loop.test b/t/chainlint/cuddled-loop.test index a841d781f0..3c2a62f751 100644 --- a/t/chainlint/cuddled-loop.test +++ b/t/chainlint/cuddled-loop.test @@ -1,4 +1,4 @@ -# LINT: 'while' loop cuddled with "(" and ")", with embedded (allowed) +# LINT: "while" loop cuddled with "(" and ")", with embedded (allowed) # LINT: "|| exit {n}" to exit loop early, and using redirection "<" to feed # LINT: loop; indented with spaces, not tabs ( while read x diff --git a/t/chainlint/cuddled.expect b/t/chainlint/cuddled.expect index b506d46221..c3e0be4047 100644 --- a/t/chainlint/cuddled.expect +++ b/t/chainlint/cuddled.expect @@ -1,21 +1,17 @@ -( -cd foo && +(cd foo && bar ->) && +) && -( -?!AMP?!cd foo +(cd foo ?!AMP?! bar ->) && +) && ( cd foo && -> bar) && + bar) && -( -cd foo && -> bar) && +(cd foo && + bar) && -( -?!AMP?!cd foo -> bar) +(cd foo ?!AMP?! + bar) diff --git a/t/chainlint/cuddled.test b/t/chainlint/cuddled.test index 0499fa4180..257b5b5eed 100644 --- a/t/chainlint/cuddled.test +++ b/t/chainlint/cuddled.test @@ -1,5 +1,4 @@ -# LINT: first subshell statement cuddled with opening "("; for implementation -# LINT: simplicity, "(..." is split into two lines, "(" and "..." +# LINT: first subshell statement cuddled with opening "(" (cd foo && bar ) && diff --git a/t/chainlint/exit-loop.expect b/t/chainlint/exit-loop.expect index 84d8bdebc0..f76aa60466 100644 --- a/t/chainlint/exit-loop.expect +++ b/t/chainlint/exit-loop.expect @@ -5,7 +5,7 @@ bar && baz done ->) && +) && ( while true do @@ -13,7 +13,7 @@ bar && baz done ->) && +) && ( i=0 && while test $i -lt 10 @@ -21,4 +21,4 @@ echo $i || exit i=$(($i + 1)) done ->) +) diff --git a/t/chainlint/exit-subshell.expect b/t/chainlint/exit-subshell.expect index bf78454f74..da80339f78 100644 --- a/t/chainlint/exit-subshell.expect +++ b/t/chainlint/exit-subshell.expect @@ -2,4 +2,4 @@ foo || exit 1 bar && baz ->) +) diff --git a/t/chainlint/for-loop.expect b/t/chainlint/for-loop.expect index c33cf56ee7..6671b8cd84 100644 --- a/t/chainlint/for-loop.expect +++ b/t/chainlint/for-loop.expect @@ -1,11 +1,11 @@ ( for i in a b c do -?!AMP?! echo $i - cat -?!AMP?! done + echo $i ?!AMP?! + cat <<-EOF + done ?!AMP?! for i in a b c; do echo $i && cat $i done ->) +) diff --git a/t/chainlint/for-loop.test b/t/chainlint/for-loop.test index 7db76262bc..6cb3428158 100644 --- a/t/chainlint/for-loop.test +++ b/t/chainlint/for-loop.test @@ -1,17 +1,17 @@ ( -# LINT: 'for', 'do', 'done' do not need "&&" +# LINT: "for", "do", "done" do not need "&&" for i in a b c do -# LINT: missing "&&" on 'echo' +# LINT: missing "&&" on "echo" echo $i # LINT: last statement of while does not need "&&" cat <<-\EOF bar EOF -# LINT: missing "&&" on 'done' +# LINT: missing "&&" on "done" done -# LINT: 'do' on same line as 'for' +# LINT: "do" on same line as "for" for i in a b c; do echo $i && cat $i diff --git a/t/chainlint/here-doc-close-subshell.expect b/t/chainlint/here-doc-close-subshell.expect index f011e335e5..2af9ced71c 100644 --- a/t/chainlint/here-doc-close-subshell.expect +++ b/t/chainlint/here-doc-close-subshell.expect @@ -1,2 +1,2 @@ ( -> cat) + cat <<-INPUT) diff --git a/t/chainlint/here-doc-multi-line-command-subst.expect b/t/chainlint/here-doc-multi-line-command-subst.expect index e5fb752d2f..f8b3aa73c4 100644 --- a/t/chainlint/here-doc-multi-line-command-subst.expect +++ b/t/chainlint/here-doc-multi-line-command-subst.expect @@ -1,5 +1,5 @@ ( - x=$(bobble && -?!AMP?!>> wiffle) + x=$(bobble <<-END && + wiffle) ?!AMP?! echo $x ->) +) diff --git a/t/chainlint/here-doc-multi-line-string.expect b/t/chainlint/here-doc-multi-line-string.expect index 32038a070c..2578191ca8 100644 --- a/t/chainlint/here-doc-multi-line-string.expect +++ b/t/chainlint/here-doc-multi-line-string.expect @@ -1,4 +1,4 @@ ( -?!AMP?! cat && echo "multi-line string" + cat <<-TXT && echo "multi-line string" ?!AMP?! bap ->) +) diff --git a/t/chainlint/here-doc.expect b/t/chainlint/here-doc.expect index 534b065e38..110059ba58 100644 --- a/t/chainlint/here-doc.expect +++ b/t/chainlint/here-doc.expect @@ -1,9 +1,7 @@ -boodle wobba gorgo snoot wafta snurb && +boodle wobba gorgo snoot wafta snurb <<EOF && -cat >foo && +cat <<-Arbitrary_Tag_42 >foo && -cat >bar && +cat <<zump >boo && -cat >boo && - -horticulture +horticulture <<EOF diff --git a/t/chainlint/here-doc.test b/t/chainlint/here-doc.test index ad4ce8afd9..3f5f92cad3 100644 --- a/t/chainlint/here-doc.test +++ b/t/chainlint/here-doc.test @@ -14,13 +14,6 @@ boz woz Arbitrary_Tag_42 -# LINT: swallow 'quoted' here-doc -cat <<'FUMP' >bar && -snoz -boz -woz -FUMP - # LINT: swallow "quoted" here-doc cat <<"zump" >boo && snoz diff --git a/t/chainlint/if-in-loop.expect b/t/chainlint/if-in-loop.expect index 03d3ceb22d..03b82a3e58 100644 --- a/t/chainlint/if-in-loop.expect +++ b/t/chainlint/if-in-loop.expect @@ -3,10 +3,10 @@ do if false then -?!AMP?! echo "err" + echo "err" ?!AMP?! exit 1 -?!AMP?! fi + fi ?!AMP?! foo -?!AMP?! done + done ?!AMP?! bar ->) +) diff --git a/t/chainlint/if-in-loop.test b/t/chainlint/if-in-loop.test index daf22da164..f0cf19cfad 100644 --- a/t/chainlint/if-in-loop.test +++ b/t/chainlint/if-in-loop.test @@ -3,13 +3,13 @@ do if false then -# LINT: missing "&&" on 'echo' +# LINT: missing "&&" on "echo" echo "err" exit 1 -# LINT: missing "&&" on 'fi' +# LINT: missing "&&" on "fi" fi foo -# LINT: missing "&&" on 'done' +# LINT: missing "&&" on "done" done bar ) diff --git a/t/chainlint/if-then-else.expect b/t/chainlint/if-then-else.expect index 5953c7bfbc..44d86c3597 100644 --- a/t/chainlint/if-then-else.expect +++ b/t/chainlint/if-then-else.expect @@ -1,19 +1,20 @@ ( if test -n "" then -?!AMP?! echo very + echo very ?!AMP?! echo empty elif test -z "" + then echo foo else echo foo && - cat -?!AMP?! fi + cat <<-EOF + fi ?!AMP?! echo poodle ->) && +) && ( if test -n ""; then echo very && -?!AMP?! echo empty - if ->) + echo empty + fi +) diff --git a/t/chainlint/if-then-else.test b/t/chainlint/if-then-else.test index 9bd8e9a4c6..2055336c2b 100644 --- a/t/chainlint/if-then-else.test +++ b/t/chainlint/if-then-else.test @@ -1,28 +1,29 @@ ( -# LINT: 'if', 'then', 'elif', 'else', 'fi' do not need "&&" +# LINT: "if", "then", "elif", "else", "fi" do not need "&&" if test -n "" then -# LINT: missing "&&" on 'echo' +# LINT: missing "&&" on "echo" echo very -# LINT: last statement before 'elif' does not need "&&" +# LINT: last statement before "elif" does not need "&&" echo empty elif test -z "" -# LINT: last statement before 'else' does not need "&&" + then +# LINT: last statement before "else" does not need "&&" echo foo else echo foo && -# LINT: last statement before 'fi' does not need "&&" +# LINT: last statement before "fi" does not need "&&" cat <<-\EOF bar EOF -# LINT: missing "&&" on 'fi' +# LINT: missing "&&" on "fi" fi echo poodle ) && ( -# LINT: 'then' on same line as 'if' +# LINT: "then" on same line as "if" if test -n ""; then echo very && echo empty - if + fi ) diff --git a/t/chainlint/incomplete-line.expect b/t/chainlint/incomplete-line.expect index 2f3ebabdc2..ffac8f9018 100644 --- a/t/chainlint/incomplete-line.expect +++ b/t/chainlint/incomplete-line.expect @@ -1,4 +1,4 @@ line 1 line 2 line 3 line 4 && ( line 5 line 6 line 7 line 8 ->) +) diff --git a/t/chainlint/inline-comment.expect b/t/chainlint/inline-comment.expect index fc9f250ac4..dd0dace077 100644 --- a/t/chainlint/inline-comment.expect +++ b/t/chainlint/inline-comment.expect @@ -1,9 +1,8 @@ ( foobar && -?!AMP?! barfoo + barfoo ?!AMP?! flibble "not a # comment" ->) && +) && -( -cd foo && -> flibble "not a # comment") +(cd foo && + flibble "not a # comment") diff --git a/t/chainlint/loop-in-if.expect b/t/chainlint/loop-in-if.expect index 088e622c31..e1be42376c 100644 --- a/t/chainlint/loop-in-if.expect +++ b/t/chainlint/loop-in-if.expect @@ -3,10 +3,10 @@ then while true do -?!AMP?! echo "pop" + echo "pop" ?!AMP?! echo "glup" -?!AMP?! done + done ?!AMP?! foo -?!AMP?! fi + fi ?!AMP?! bar ->) +) diff --git a/t/chainlint/loop-in-if.test b/t/chainlint/loop-in-if.test index 93e8ba8e4d..dfcc3f98fb 100644 --- a/t/chainlint/loop-in-if.test +++ b/t/chainlint/loop-in-if.test @@ -3,13 +3,13 @@ then while true do -# LINT: missing "&&" on 'echo' +# LINT: missing "&&" on "echo" echo "pop" echo "glup" -# LINT: missing "&&" on 'done' +# LINT: missing "&&" on "done" done foo -# LINT: missing "&&" on 'fi' +# LINT: missing "&&" on "fi" fi bar ) diff --git a/t/chainlint/multi-line-nested-command-substitution.expect b/t/chainlint/multi-line-nested-command-substitution.expect index 59b6c8b850..300058341b 100644 --- a/t/chainlint/multi-line-nested-command-substitution.expect +++ b/t/chainlint/multi-line-nested-command-substitution.expect @@ -3,16 +3,16 @@ x=$( echo bar | cat ->> ) && + ) && echo ok ->) | +) | sort && ( bar && x=$(echo bar | cat ->> ) && + ) && y=$(echo baz | ->> fip) && + fip) && echo fail ->) +) diff --git a/t/chainlint/multi-line-string.expect b/t/chainlint/multi-line-string.expect index 170cb59993..ab0dadf748 100644 --- a/t/chainlint/multi-line-string.expect +++ b/t/chainlint/multi-line-string.expect @@ -1,15 +1,9 @@ ( x="line 1 line 2 line 3" && -?!AMP?! y='line 1 line2' + y="line 1 line2" ?!AMP?! foobar ->) && -( - echo "there's nothing to see here" && - exit ->) && +) && ( echo "xyz" "abc def ghi" && - echo 'xyz' 'abc def ghi' && - echo 'xyz' "abc def ghi" && barfoo ->) +) diff --git a/t/chainlint/multi-line-string.test b/t/chainlint/multi-line-string.test index 287ab89705..4a0af2107d 100644 --- a/t/chainlint/multi-line-string.test +++ b/t/chainlint/multi-line-string.test @@ -3,25 +3,13 @@ line 2 line 3" && # LINT: missing "&&" on assignment - y='line 1 - line2' + y="line 1 + line2" foobar ) && ( -# LINT: apostrophe (in a contraction) within string not misinterpreted as -# LINT: starting multi-line single-quoted string - echo "there's nothing to see here" && - exit -) && -( echo "xyz" "abc def ghi" && - echo 'xyz' 'abc - def - ghi' && - echo 'xyz' "abc - def - ghi" && barfoo ) diff --git a/t/chainlint/negated-one-liner.expect b/t/chainlint/negated-one-liner.expect index cf18429d03..ad4c2d949e 100644 --- a/t/chainlint/negated-one-liner.expect +++ b/t/chainlint/negated-one-liner.expect @@ -1,5 +1,5 @@ ! (foo && bar) && ! (foo && bar) >baz && -?!SEMI?!! (foo; bar) && -?!SEMI?!! (foo; bar) >baz +! (foo; ?!AMP?! bar) && +! (foo; ?!AMP?! bar) >baz diff --git a/t/chainlint/nested-cuddled-subshell.expect b/t/chainlint/nested-cuddled-subshell.expect index c2a59ffc33..2a86885ee6 100644 --- a/t/chainlint/nested-cuddled-subshell.expect +++ b/t/chainlint/nested-cuddled-subshell.expect @@ -1,19 +1,19 @@ ( (cd foo && bar ->> ) && + ) && (cd foo && bar -?!AMP?!>> ) + ) ?!AMP?! ( cd foo && ->> bar) && + bar) && ( cd foo && -?!AMP?!>> bar) + bar) ?!AMP?! (cd foo && ->> bar) && + bar) && (cd foo && -?!AMP?!>> bar) + bar) ?!AMP?! foobar ->) +) diff --git a/t/chainlint/nested-here-doc.expect b/t/chainlint/nested-here-doc.expect index 0c9ef1cfc6..e3bef63f75 100644 --- a/t/chainlint/nested-here-doc.expect +++ b/t/chainlint/nested-here-doc.expect @@ -1,7 +1,7 @@ -cat >foop && +cat <<ARBITRARY >foop && ( - cat && -?!AMP?! cat + cat <<-INPUT_END && + cat <<-EOT ?!AMP?! foobar ->) +) diff --git a/t/chainlint/nested-subshell-comment.expect b/t/chainlint/nested-subshell-comment.expect index 15b68d4373..be4b27a305 100644 --- a/t/chainlint/nested-subshell-comment.expect +++ b/t/chainlint/nested-subshell-comment.expect @@ -2,10 +2,8 @@ foo && ( bar && - # bottles wobble while fiddles gobble - # minor numbers of cows (or do they?) baz && snaff -?!AMP?!>> ) + ) ?!AMP?! fuzzy ->) +) diff --git a/t/chainlint/nested-subshell-comment.test b/t/chainlint/nested-subshell-comment.test index 0ff136ab3c..0215cdb192 100644 --- a/t/chainlint/nested-subshell-comment.test +++ b/t/chainlint/nested-subshell-comment.test @@ -7,7 +7,7 @@ # minor numbers of cows (or do they?) baz && snaff -# LINT: missing "&&" on ')' +# LINT: missing "&&" on ")" ) fuzzy ) diff --git a/t/chainlint/nested-subshell.expect b/t/chainlint/nested-subshell.expect index c8165ad19e..41a48adaa2 100644 --- a/t/chainlint/nested-subshell.expect +++ b/t/chainlint/nested-subshell.expect @@ -3,10 +3,10 @@ ( echo a && echo b ->> ) >file && + ) >file && cd foo && ( echo a echo b ->> ) >file ->) + ) >file +) diff --git a/t/chainlint/nested-subshell.test b/t/chainlint/nested-subshell.test index 998b05a47d..440ee9992d 100644 --- a/t/chainlint/nested-subshell.test +++ b/t/chainlint/nested-subshell.test @@ -7,7 +7,6 @@ cd foo && ( -# LINT: nested multi-line subshell not presently checked for missing "&&" echo a echo b ) >file diff --git a/t/chainlint/not-heredoc.expect b/t/chainlint/not-heredoc.expect new file mode 100644 index 0000000000..2e9bb135fe --- /dev/null +++ b/t/chainlint/not-heredoc.expect @@ -0,0 +1,14 @@ +echo "<<<<<<< ours" && +echo ourside && +echo "=======" && +echo theirside && +echo ">>>>>>> theirs" && + +( + echo "<<<<<<< ours" && + echo ourside && + echo "=======" && + echo theirside && + echo ">>>>>>> theirs" ?!AMP?! + poodle +) >merged diff --git a/t/chainlint/not-heredoc.test b/t/chainlint/not-heredoc.test new file mode 100644 index 0000000000..9aa57346cd --- /dev/null +++ b/t/chainlint/not-heredoc.test @@ -0,0 +1,16 @@ +# LINT: "<< ours" inside string is not here-doc +echo "<<<<<<< ours" && +echo ourside && +echo "=======" && +echo theirside && +echo ">>>>>>> theirs" && + +( +# LINT: "<< ours" inside string is not here-doc + echo "<<<<<<< ours" && + echo ourside && + echo "=======" && + echo theirside && + echo ">>>>>>> theirs" + poodle +) >merged diff --git a/t/chainlint/one-liner.expect b/t/chainlint/one-liner.expect index 237f227349..57a7a444c1 100644 --- a/t/chainlint/one-liner.expect +++ b/t/chainlint/one-liner.expect @@ -2,8 +2,8 @@ (foo && bar) | (foo && bar) >baz && -?!SEMI?!(foo; bar) && -?!SEMI?!(foo; bar) | -?!SEMI?!(foo; bar) >baz +(foo; ?!AMP?! bar) && +(foo; ?!AMP?! bar) | +(foo; ?!AMP?! bar) >baz && (foo "bar; baz") diff --git a/t/chainlint/one-liner.test b/t/chainlint/one-liner.test index ec9acb9825..be9858fa29 100644 --- a/t/chainlint/one-liner.test +++ b/t/chainlint/one-liner.test @@ -3,10 +3,10 @@ (foo && bar) | (foo && bar) >baz && -# LINT: top-level one-liner subshell missing internal "&&" +# LINT: top-level one-liner subshell missing internal "&&" and broken &&-chain (foo; bar) && (foo; bar) | -(foo; bar) >baz +(foo; bar) >baz && # LINT: ";" in string not misinterpreted as broken &&-chain (foo "bar; baz") diff --git a/t/chainlint/p4-filespec.expect b/t/chainlint/p4-filespec.expect index 98b3d881fd..1290fd1ff2 100644 --- a/t/chainlint/p4-filespec.expect +++ b/t/chainlint/p4-filespec.expect @@ -1,4 +1,4 @@ ( p4 print -1 //depot/fiddle#42 >file && foobar ->) +) diff --git a/t/chainlint/pipe.expect b/t/chainlint/pipe.expect index 211b901dbc..2cfc028297 100644 --- a/t/chainlint/pipe.expect +++ b/t/chainlint/pipe.expect @@ -3,6 +3,6 @@ bar | baz && fish | -?!AMP?! cow + cow ?!AMP?! sunder ->) +) diff --git a/t/chainlint/pipe.test b/t/chainlint/pipe.test index e6af4de916..dd82534c66 100644 --- a/t/chainlint/pipe.test +++ b/t/chainlint/pipe.test @@ -4,7 +4,7 @@ bar | baz && -# LINT: final line of pipe sequence ('cow') lacking "&&" +# LINT: final line of pipe sequence ("cow") lacking "&&" fish | cow diff --git a/t/chainlint/semicolon.expect b/t/chainlint/semicolon.expect index 1d79384606..ed0b3707ae 100644 --- a/t/chainlint/semicolon.expect +++ b/t/chainlint/semicolon.expect @@ -1,20 +1,19 @@ ( -?!AMP?!?!SEMI?! cat foo ; echo bar -?!SEMI?! cat foo ; echo bar ->) && + cat foo ; ?!AMP?! echo bar ?!AMP?! + cat foo ; ?!AMP?! echo bar +) && ( -?!SEMI?! cat foo ; echo bar && -?!SEMI?! cat foo ; echo bar ->) && + cat foo ; ?!AMP?! echo bar && + cat foo ; ?!AMP?! echo bar +) && ( echo "foo; bar" && -?!SEMI?! cat foo; echo bar ->) && + cat foo; ?!AMP?! echo bar +) && ( -?!SEMI?! foo; ->) && -( -cd foo && + foo; +) && +(cd foo && for i in a b c; do -?!SEMI?! echo; -> done) + echo; + done) diff --git a/t/chainlint/semicolon.test b/t/chainlint/semicolon.test index d82c8ebbc0..67e1192c50 100644 --- a/t/chainlint/semicolon.test +++ b/t/chainlint/semicolon.test @@ -15,11 +15,11 @@ cat foo; echo bar ) && ( -# LINT: unnecessary terminating semicolon +# LINT: semicolon unnecessary but legitimate foo; ) && (cd foo && for i in a b c; do -# LINT: unnecessary terminating semicolon +# LINT: semicolon unnecessary but legitimate echo; done) diff --git a/t/chainlint/subshell-here-doc.expect b/t/chainlint/subshell-here-doc.expect index 74723e7340..029d129299 100644 --- a/t/chainlint/subshell-here-doc.expect +++ b/t/chainlint/subshell-here-doc.expect @@ -1,11 +1,10 @@ ( - echo wobba gorgo snoot wafta snurb && -?!AMP?! cat >bip - echo >bop ->) && + echo wobba gorgo snoot wafta snurb <<-EOF && + cat <<EOF >bip ?!AMP?! + echo <<-EOF >bop +) && ( - cat >bup && - cat >bup2 && - cat >bup3 && + cat <<-ARBITRARY >bup && + cat <<-ARBITRARY3 >bup3 && meep ->) +) diff --git a/t/chainlint/subshell-here-doc.test b/t/chainlint/subshell-here-doc.test index f6b3ba4214..d40eb65583 100644 --- a/t/chainlint/subshell-here-doc.test +++ b/t/chainlint/subshell-here-doc.test @@ -8,10 +8,10 @@ nevermore... EOF -# LINT: missing "&&" on 'cat' +# LINT: missing "&&" on "cat" cat <<EOF >bip fish fly high - EOF +EOF # LINT: swallow here-doc (EOF is last line of subshell) echo <<-\EOF >bop @@ -27,10 +27,6 @@ glink FIZZ ARBITRARY - cat <<-'ARBITRARY2' >bup2 && - glink - FIZZ - ARBITRARY2 cat <<-"ARBITRARY3" >bup3 && glink FIZZ diff --git a/t/chainlint/subshell-one-liner.expect b/t/chainlint/subshell-one-liner.expect index 51162821d7..b7015361bf 100644 --- a/t/chainlint/subshell-one-liner.expect +++ b/t/chainlint/subshell-one-liner.expect @@ -2,13 +2,13 @@ (foo && bar) && (foo && bar) | (foo && bar) >baz && -?!SEMI?! (foo; bar) && -?!SEMI?! (foo; bar) | -?!SEMI?! (foo; bar) >baz && + (foo; ?!AMP?! bar) && + (foo; ?!AMP?! bar) | + (foo; ?!AMP?! bar) >baz && (foo || exit 1) && (foo || exit 1) | (foo || exit 1) >baz && -?!AMP?! (foo && bar) -?!AMP?!?!SEMI?! (foo && bar; baz) + (foo && bar) ?!AMP?! + (foo && bar; ?!AMP?! baz) ?!AMP?! foobar ->) +) diff --git a/t/chainlint/t7900-subtree.expect b/t/chainlint/t7900-subtree.expect index c9913429e6..1cccc7bf7e 100644 --- a/t/chainlint/t7900-subtree.expect +++ b/t/chainlint/t7900-subtree.expect @@ -1,10 +1,10 @@ ( chks="sub1sub2sub3sub4" && - chks_sub=$(cat | sed 's,^,sub dir/,' ->>) && + chks_sub=$(cat <<TXT | sed "s,^,sub dir/," +) && chkms="main-sub1main-sub2main-sub3main-sub4" && - chkms_sub=$(cat | sed 's,^,sub dir/,' ->>) && + chkms_sub=$(cat <<TXT | sed "s,^,sub dir/," +) && subfiles=$(git ls-files) && check_equal "$subfiles" "$chkms$chks" ->) +) diff --git a/t/chainlint/t7900-subtree.test b/t/chainlint/t7900-subtree.test index 277d8358df..02f3129232 100644 --- a/t/chainlint/t7900-subtree.test +++ b/t/chainlint/t7900-subtree.test @@ -3,7 +3,7 @@ sub2 sub3 sub4" && - chks_sub=$(cat <<TXT | sed 's,^,sub dir/,' + chks_sub=$(cat <<TXT | sed "s,^,sub dir/," $chks TXT ) && @@ -11,7 +11,7 @@ TXT main-sub2 main-sub3 main-sub4" && - chkms_sub=$(cat <<TXT | sed 's,^,sub dir/,' + chkms_sub=$(cat <<TXT | sed "s,^,sub dir/," $chkms TXT ) && diff --git a/t/chainlint/while-loop.expect b/t/chainlint/while-loop.expect index 13cff2c0a5..0d3a9b3d12 100644 --- a/t/chainlint/while-loop.expect +++ b/t/chainlint/while-loop.expect @@ -1,11 +1,11 @@ ( while true do -?!AMP?! echo foo - cat -?!AMP?! done + echo foo ?!AMP?! + cat <<-EOF + done ?!AMP?! while true; do echo foo && cat bar done ->) +) diff --git a/t/chainlint/while-loop.test b/t/chainlint/while-loop.test index f1df085bf0..d09fb016e4 100644 --- a/t/chainlint/while-loop.test +++ b/t/chainlint/while-loop.test @@ -1,17 +1,17 @@ ( -# LINT: 'while, 'do', 'done' do not need "&&" +# LINT: "while", "do", "done" do not need "&&" while true do -# LINT: missing "&&" on 'echo' +# LINT: missing "&&" on "echo" echo foo # LINT: last statement of while does not need "&&" cat <<-\EOF bar EOF -# LINT: missing "&&" on 'done' +# LINT: missing "&&" on "done" done -# LINT: 'do' on same line as 'while' +# LINT: "do" on same line as "while" while true; do echo foo && cat bar diff --git a/t/helper/test-genzeros.c b/t/helper/test-genzeros.c index 9532f5bac9..8ca988d621 100644 --- a/t/helper/test-genzeros.c +++ b/t/helper/test-genzeros.c @@ -3,18 +3,31 @@ int cmd__genzeros(int argc, const char **argv) { - long count; + /* static, so that it is NUL-initialized */ + static const char zeros[256 * 1024]; + intmax_t count; + ssize_t n; if (argc > 2) { fprintf(stderr, "usage: %s [<count>]\n", argv[0]); return 1; } - count = argc > 1 ? strtol(argv[1], NULL, 0) : -1L; + count = argc > 1 ? strtoimax(argv[1], NULL, 0) : -1; - while (count < 0 || count--) { - if (putchar(0) == EOF) + /* Writing out individual NUL bytes is slow... */ + while (count < 0) + if (write(1, zeros, ARRAY_SIZE(zeros)) < 0) return -1; + + while (count > 0) { + n = write(1, zeros, count < ARRAY_SIZE(zeros) ? + count : ARRAY_SIZE(zeros)); + + if (n < 0) + return -1; + + count -= n; } return 0; diff --git a/t/helper/test-read-cache.c b/t/helper/test-read-cache.c index b52c174acc..b736ef1642 100644 --- a/t/helper/test-read-cache.c +++ b/t/helper/test-read-cache.c @@ -1,82 +1,39 @@ #include "test-tool.h" #include "cache.h" #include "config.h" -#include "blob.h" -#include "commit.h" -#include "tree.h" -#include "sparse-index.h" - -static void print_cache_entry(struct cache_entry *ce) -{ - const char *type; - printf("%06o ", ce->ce_mode & 0177777); - - if (S_ISSPARSEDIR(ce->ce_mode)) - type = tree_type; - else if (S_ISGITLINK(ce->ce_mode)) - type = commit_type; - else - type = blob_type; - - printf("%s %s\t%s\n", - type, - oid_to_hex(&ce->oid), - ce->name); -} - -static void print_cache(struct index_state *istate) -{ - int i; - for (i = 0; i < istate->cache_nr; i++) - print_cache_entry(istate->cache[i]); -} int cmd__read_cache(int argc, const char **argv) { - struct repository *r = the_repository; int i, cnt = 1; const char *name = NULL; - int table = 0, expand = 0; initialize_the_repository(); - prepare_repo_settings(r); - r->settings.command_requires_full_index = 0; - for (++argv, --argc; *argv && starts_with(*argv, "--"); ++argv, --argc) { - if (skip_prefix(*argv, "--print-and-refresh=", &name)) - continue; - if (!strcmp(*argv, "--table")) - table = 1; - else if (!strcmp(*argv, "--expand")) - expand = 1; + if (argc > 1 && skip_prefix(argv[1], "--print-and-refresh=", &name)) { + argc--; + argv++; } - if (argc == 1) - cnt = strtol(argv[0], NULL, 0); + if (argc == 2) + cnt = strtol(argv[1], NULL, 0); setup_git_directory(); git_config(git_default_config, NULL); for (i = 0; i < cnt; i++) { - repo_read_index(r); - - if (expand) - ensure_full_index(r->index); - + read_cache(); if (name) { int pos; - refresh_index(r->index, REFRESH_QUIET, + refresh_index(&the_index, REFRESH_QUIET, NULL, NULL, NULL); - pos = index_name_pos(r->index, name, strlen(name)); + pos = index_name_pos(&the_index, name, strlen(name)); if (pos < 0) die("%s not in index", name); printf("%s is%s up to date\n", name, - ce_uptodate(r->index->cache[pos]) ? "" : " not"); + ce_uptodate(the_index.cache[pos]) ? "" : " not"); write_file(name, "%d\n", i); } - if (table) - print_cache(r->index); - discard_index(r->index); + discard_cache(); } return 0; } diff --git a/t/helper/test-read-midx.c b/t/helper/test-read-midx.c index 9d6fa7a377..27072ba94d 100644 --- a/t/helper/test-read-midx.c +++ b/t/helper/test-read-midx.c @@ -55,9 +55,10 @@ static int read_midx_file(const char *object_dir, int show_objects) printf("%s %"PRIu64"\t%s\n", oid_to_hex(&oid), e.offset, e.p->pack_name); } - return 0; } + close_midx(m); + return 0; } diff --git a/t/helper/test-ref-store.c b/t/helper/test-ref-store.c index b314b81a45..3e4ddaee70 100644 --- a/t/helper/test-ref-store.c +++ b/t/helper/test-ref-store.c @@ -5,6 +5,48 @@ #include "object-store.h" #include "repository.h" +struct flag_definition { + const char *name; + uint64_t mask; +}; + +#define FLAG_DEF(x) \ + { \ +#x, (x) \ + } + +static unsigned int parse_flags(const char *str, struct flag_definition *defs) +{ + struct string_list masks = STRING_LIST_INIT_DUP; + int i = 0; + unsigned int result = 0; + + if (!strcmp(str, "0")) + return 0; + + string_list_split(&masks, str, ',', 64); + for (; i < masks.nr; i++) { + const char *name = masks.items[i].string; + struct flag_definition *def = defs; + int found = 0; + while (def->name) { + if (!strcmp(def->name, name)) { + result |= def->mask; + found = 1; + break; + } + def++; + } + if (!found) + die("unknown flag \"%s\"", name); + } + + string_list_clear(&masks, 0); + return result; +} + +static struct flag_definition empty_flags[] = { { NULL, 0 } }; + static const char *notnull(const char *arg, const char *name) { if (!arg) @@ -12,9 +54,10 @@ static const char *notnull(const char *arg, const char *name) return arg; } -static unsigned int arg_flags(const char *arg, const char *name) +static unsigned int arg_flags(const char *arg, const char *name, + struct flag_definition *defs) { - return atoi(notnull(arg, name)); + return parse_flags(notnull(arg, name), defs); } static const char **get_store(const char **argv, struct ref_store **refs) @@ -64,10 +107,13 @@ static const char **get_store(const char **argv, struct ref_store **refs) return argv + 1; } +static struct flag_definition pack_flags[] = { FLAG_DEF(PACK_REFS_PRUNE), + FLAG_DEF(PACK_REFS_ALL), + { NULL, 0 } }; static int cmd_pack_refs(struct ref_store *refs, const char **argv) { - unsigned int flags = arg_flags(*argv++, "flags"); + unsigned int flags = arg_flags(*argv++, "flags", pack_flags); return refs_pack_refs(refs, flags); } @@ -81,16 +127,27 @@ static int cmd_create_symref(struct ref_store *refs, const char **argv) return refs_create_symref(refs, refname, target, logmsg); } +static struct flag_definition transaction_flags[] = { + FLAG_DEF(REF_NO_DEREF), + FLAG_DEF(REF_FORCE_CREATE_REFLOG), + FLAG_DEF(REF_SKIP_OID_VERIFICATION), + FLAG_DEF(REF_SKIP_REFNAME_VERIFICATION), + { NULL, 0 } +}; + static int cmd_delete_refs(struct ref_store *refs, const char **argv) { - unsigned int flags = arg_flags(*argv++, "flags"); + unsigned int flags = arg_flags(*argv++, "flags", transaction_flags); const char *msg = *argv++; struct string_list refnames = STRING_LIST_INIT_NODUP; + int result; while (*argv) string_list_append(&refnames, *argv++); - return refs_delete_refs(refs, msg, &refnames, flags); + result = refs_delete_refs(refs, msg, &refnames, flags); + string_list_clear(&refnames, 0); + return result; } static int cmd_rename_ref(struct ref_store *refs, const char **argv) @@ -120,12 +177,13 @@ static int cmd_resolve_ref(struct ref_store *refs, const char **argv) { struct object_id oid = *null_oid(); const char *refname = notnull(*argv++, "refname"); - int resolve_flags = arg_flags(*argv++, "resolve-flags"); + int resolve_flags = arg_flags(*argv++, "resolve-flags", empty_flags); int flags; const char *ref; + int ignore_errno; ref = refs_resolve_ref_unsafe(refs, refname, resolve_flags, - &oid, &flags); + &oid, &flags, &ignore_errno); printf("%s %s 0x%x\n", oid_to_hex(&oid), ref ? ref : "(null)", flags); return ref ? 0 : 1; } @@ -151,9 +209,9 @@ static int each_reflog(struct object_id *old_oid, struct object_id *new_oid, const char *committer, timestamp_t timestamp, int tz, const char *msg, void *cb_data) { - printf("%s %s %s %"PRItime" %d %s\n", - oid_to_hex(old_oid), oid_to_hex(new_oid), - committer, timestamp, tz, msg); + printf("%s %s %s %" PRItime " %+05d%s%s", oid_to_hex(old_oid), + oid_to_hex(new_oid), committer, timestamp, tz, + *msg == '\n' ? "" : "\t", msg); return 0; } @@ -181,11 +239,10 @@ static int cmd_reflog_exists(struct ref_store *refs, const char **argv) static int cmd_create_reflog(struct ref_store *refs, const char **argv) { const char *refname = notnull(*argv++, "refname"); - int force_create = arg_flags(*argv++, "force-create"); struct strbuf err = STRBUF_INIT; int ret; - ret = refs_create_reflog(refs, refname, force_create, &err); + ret = refs_create_reflog(refs, refname, &err); if (err.len) puts(err.buf); return ret; @@ -208,11 +265,11 @@ static int cmd_delete_ref(struct ref_store *refs, const char **argv) const char *msg = notnull(*argv++, "msg"); const char *refname = notnull(*argv++, "refname"); const char *sha1_buf = notnull(*argv++, "old-sha1"); - unsigned int flags = arg_flags(*argv++, "flags"); + unsigned int flags = arg_flags(*argv++, "flags", transaction_flags); struct object_id old_oid; if (get_oid_hex(sha1_buf, &old_oid)) - die("not sha-1"); + die("cannot parse %s as %s", sha1_buf, the_hash_algo->name); return refs_delete_ref(refs, msg, refname, &old_oid, flags); } @@ -223,13 +280,14 @@ static int cmd_update_ref(struct ref_store *refs, const char **argv) const char *refname = notnull(*argv++, "refname"); const char *new_sha1_buf = notnull(*argv++, "new-sha1"); const char *old_sha1_buf = notnull(*argv++, "old-sha1"); - unsigned int flags = arg_flags(*argv++, "flags"); + unsigned int flags = arg_flags(*argv++, "flags", transaction_flags); struct object_id old_oid; struct object_id new_oid; - if (get_oid_hex(old_sha1_buf, &old_oid) || - get_oid_hex(new_sha1_buf, &new_oid)) - die("not sha-1"); + if (get_oid_hex(old_sha1_buf, &old_oid)) + die("cannot parse %s as %s", old_sha1_buf, the_hash_algo->name); + if (get_oid_hex(new_sha1_buf, &new_oid)) + die("cannot parse %s as %s", new_sha1_buf, the_hash_algo->name); return refs_update_ref(refs, msg, refname, &new_oid, &old_oid, diff --git a/t/helper/test-reftable.c b/t/helper/test-reftable.c new file mode 100644 index 0000000000..26b03d7b78 --- /dev/null +++ b/t/helper/test-reftable.c @@ -0,0 +1,21 @@ +#include "reftable/reftable-tests.h" +#include "test-tool.h" + +int cmd__reftable(int argc, const char **argv) +{ + basics_test_main(argc, argv); + block_test_main(argc, argv); + merged_test_main(argc, argv); + pq_test_main(argc, argv); + record_test_main(argc, argv); + refname_test_main(argc, argv); + readwrite_test_main(argc, argv); + stack_test_main(argc, argv); + tree_test_main(argc, argv); + return 0; +} + +int cmd__dump_reftable(int argc, const char **argv) +{ + return reftable_dump_main(argc, (char *const *)argv); +} diff --git a/t/helper/test-run-command.c b/t/helper/test-run-command.c index 3c4fb86223..913775a14b 100644 --- a/t/helper/test-run-command.c +++ b/t/helper/test-run-command.c @@ -31,7 +31,7 @@ static int parallel_next(struct child_process *cp, if (number_callbacks >= 4) return 0; - strvec_pushv(&cp->args, d->argv); + strvec_pushv(&cp->args, d->args.v); strbuf_addstr(err, "preloaded output of a child\n"); number_callbacks++; return 1; @@ -274,7 +274,7 @@ static int quote_stress_test(int argc, const char **argv) if (i < skip) continue; - cp.argv = args.v; + strvec_pushv(&cp.args, args.v); strbuf_reset(&out); if (pipe_command(&cp, NULL, 0, &out, 0, NULL, 0) < 0) return error("Failed to spawn child process"); @@ -396,7 +396,7 @@ int cmd__run_command(int argc, const char **argv) } if (argc < 3) return 1; - proc.argv = (const char **)argv + 2; + strvec_pushv(&proc.args, (const char **)argv + 2); if (!strcmp(argv[1], "start-command-ENOENT")) { if (start_command(&proc) < 0 && errno == ENOENT) @@ -408,7 +408,8 @@ int cmd__run_command(int argc, const char **argv) exit(run_command(&proc)); jobs = atoi(argv[2]); - proc.argv = (const char **)argv + 3; + strvec_clear(&proc.args); + strvec_pushv(&proc.args, (const char **)argv + 3); if (!strcmp(argv[1], "run-command-parallel")) exit(run_processes_parallel(jobs, parallel_next, diff --git a/t/helper/test-subprocess.c b/t/helper/test-subprocess.c index 92b69de635..ff22f2fa2c 100644 --- a/t/helper/test-subprocess.c +++ b/t/helper/test-subprocess.c @@ -15,6 +15,6 @@ int cmd__subprocess(int argc, const char **argv) argv++; } cp.git_cmd = 1; - cp.argv = (const char **)argv + 1; + strvec_pushv(&cp.args, (const char **)argv + 1); return run_command(&cp); } diff --git a/t/helper/test-tool.c b/t/helper/test-tool.c index 3ce5585e53..338a57b104 100644 --- a/t/helper/test-tool.c +++ b/t/helper/test-tool.c @@ -53,13 +53,15 @@ static struct test_cmd cmds[] = { { "pcre2-config", cmd__pcre2_config }, { "pkt-line", cmd__pkt_line }, { "prio-queue", cmd__prio_queue }, - { "proc-receive", cmd__proc_receive}, + { "proc-receive", cmd__proc_receive }, { "progress", cmd__progress }, { "reach", cmd__reach }, { "read-cache", cmd__read_cache }, { "read-graph", cmd__read_graph }, { "read-midx", cmd__read_midx }, { "ref-store", cmd__ref_store }, + { "reftable", cmd__reftable }, + { "dump-reftable", cmd__dump_reftable }, { "regex", cmd__regex }, { "repository", cmd__repository }, { "revision-walking", cmd__revision_walking }, diff --git a/t/helper/test-tool.h b/t/helper/test-tool.h index 9f0f522850..48cee1f4a2 100644 --- a/t/helper/test-tool.h +++ b/t/helper/test-tool.h @@ -19,6 +19,7 @@ int cmd__dump_cache_tree(int argc, const char **argv); int cmd__dump_fsmonitor(int argc, const char **argv); int cmd__dump_split_index(int argc, const char **argv); int cmd__dump_untracked_cache(int argc, const char **argv); +int cmd__dump_reftable(int argc, const char **argv); int cmd__example_decorate(int argc, const char **argv); int cmd__fast_rebase(int argc, const char **argv); int cmd__genrandom(int argc, const char **argv); @@ -49,6 +50,7 @@ int cmd__read_cache(int argc, const char **argv); int cmd__read_graph(int argc, const char **argv); int cmd__read_midx(int argc, const char **argv); int cmd__ref_store(int argc, const char **argv); +int cmd__reftable(int argc, const char **argv); int cmd__regex(int argc, const char **argv); int cmd__repository(int argc, const char **argv); int cmd__revision_walking(int argc, const char **argv); diff --git a/t/helper/test-trace2.c b/t/helper/test-trace2.c index f93633f895..59b124bb5f 100644 --- a/t/helper/test-trace2.c +++ b/t/helper/test-trace2.c @@ -262,8 +262,9 @@ static int print_usage(void) * [] the "cmd_name" event has been generated. * [] this writes various "def_param" events for interesting config values. * - * We further assume that if we return (rather than exit()), trace2_cmd_exit() - * will be called by test-tool.c:cmd_main(). + * We return from here and let test-tool.c::cmd_main() pass the exit + * code to common-main.c::main(), which will use it to call + * trace2_cmd_exit(). */ int cmd__trace2(int argc, const char **argv) { diff --git a/t/lib-gpg.sh b/t/lib-gpg.sh index a3f285f515..3e7ee1386a 100644 --- a/t/lib-gpg.sh +++ b/t/lib-gpg.sh @@ -90,7 +90,12 @@ test_lazy_prereq RFC1991 ' GPGSSH_KEY_PRIMARY="${GNUPGHOME}/ed25519_ssh_signing_key" GPGSSH_KEY_SECONDARY="${GNUPGHOME}/rsa_2048_ssh_signing_key" GPGSSH_KEY_UNTRUSTED="${GNUPGHOME}/untrusted_ssh_signing_key" +GPGSSH_KEY_EXPIRED="${GNUPGHOME}/expired_ssh_signing_key" +GPGSSH_KEY_NOTYETVALID="${GNUPGHOME}/notyetvalid_ssh_signing_key" +GPGSSH_KEY_TIMEBOXEDVALID="${GNUPGHOME}/timeboxed_valid_ssh_signing_key" +GPGSSH_KEY_TIMEBOXEDINVALID="${GNUPGHOME}/timeboxed_invalid_ssh_signing_key" GPGSSH_KEY_WITH_PASSPHRASE="${GNUPGHOME}/protected_ssh_signing_key" +GPGSSH_KEY_ECDSA="${GNUPGHOME}/ecdsa_ssh_signing_key" GPGSSH_KEY_PASSPHRASE="super_secret" GPGSSH_ALLOWED_SIGNERS="${GNUPGHOME}/ssh.all_valid.allowedSignersFile" @@ -105,21 +110,63 @@ test_lazy_prereq GPGSSH ' echo $ssh_version | grep -q "find-principals:missing signature file" test $? = 0 || exit 1; - # some broken versions of ssh-keygen segfault on find-principals; - # avoid testing with them. - ssh-keygen -Y find-principals -f /dev/null -s /dev/null - test $? = 139 && exit 1 - + # Setup some keys and an allowed signers file mkdir -p "${GNUPGHOME}" && chmod 0700 "${GNUPGHOME}" && (setfacl -k "${GNUPGHOME}" 2>/dev/null || true) && ssh-keygen -t ed25519 -N "" -C "git ed25519 key" -f "${GPGSSH_KEY_PRIMARY}" >/dev/null && - echo "\"principal with number 1\" $(cat "${GPGSSH_KEY_PRIMARY}.pub")" >> "${GPGSSH_ALLOWED_SIGNERS}" && ssh-keygen -t rsa -b 2048 -N "" -C "git rsa2048 key" -f "${GPGSSH_KEY_SECONDARY}" >/dev/null && - echo "\"principal with number 2\" $(cat "${GPGSSH_KEY_SECONDARY}.pub")" >> "${GPGSSH_ALLOWED_SIGNERS}" && ssh-keygen -t ed25519 -N "${GPGSSH_KEY_PASSPHRASE}" -C "git ed25519 encrypted key" -f "${GPGSSH_KEY_WITH_PASSPHRASE}" >/dev/null && - echo "\"principal with number 3\" $(cat "${GPGSSH_KEY_WITH_PASSPHRASE}.pub")" >> "${GPGSSH_ALLOWED_SIGNERS}" && - ssh-keygen -t ed25519 -N "" -f "${GPGSSH_KEY_UNTRUSTED}" >/dev/null + ssh-keygen -t ecdsa -N "" -f "${GPGSSH_KEY_ECDSA}" >/dev/null && + ssh-keygen -t ed25519 -N "" -C "git ed25519 key" -f "${GPGSSH_KEY_UNTRUSTED}" >/dev/null && + + cat >"${GPGSSH_ALLOWED_SIGNERS}" <<-EOF && + "principal with number 1" $(cat "${GPGSSH_KEY_PRIMARY}.pub")" + "principal with number 2" $(cat "${GPGSSH_KEY_SECONDARY}.pub")" + "principal with number 3" $(cat "${GPGSSH_KEY_WITH_PASSPHRASE}.pub")" + "principal with number 4" $(cat "${GPGSSH_KEY_ECDSA}.pub")" + EOF + + # Verify if at least one key and ssh-keygen works as expected + echo "testpayload" | + ssh-keygen -Y sign -n "git" -f "${GPGSSH_KEY_PRIMARY}" >gpgssh_prereq.sig && + ssh-keygen -Y find-principals -f "${GPGSSH_ALLOWED_SIGNERS}" -s gpgssh_prereq.sig && + echo "testpayload" | + ssh-keygen -Y verify -n "git" -f "${GPGSSH_ALLOWED_SIGNERS}" -I "principal with number 1" -s gpgssh_prereq.sig +' + +test_lazy_prereq GPGSSH_VERIFYTIME ' + # Check if ssh-keygen has a verify-time option by passing an invalid date to it + ssh-keygen -Overify-time=INVALID -Y check-novalidate -s doesnotmatter 2>&1 | grep -q -F "Invalid \"verify-time\"" && + + # Set up keys with key lifetimes + ssh-keygen -t ed25519 -N "" -C "timeboxed valid key" -f "${GPGSSH_KEY_TIMEBOXEDVALID}" >/dev/null && + key_valid=$(cat "${GPGSSH_KEY_TIMEBOXEDVALID}.pub") && + ssh-keygen -t ed25519 -N "" -C "timeboxed invalid key" -f "${GPGSSH_KEY_TIMEBOXEDINVALID}" >/dev/null && + key_invalid=$(cat "${GPGSSH_KEY_TIMEBOXEDINVALID}.pub") && + ssh-keygen -t ed25519 -N "" -C "expired key" -f "${GPGSSH_KEY_EXPIRED}" >/dev/null && + key_expired=$(cat "${GPGSSH_KEY_EXPIRED}.pub") && + ssh-keygen -t ed25519 -N "" -C "not yet valid key" -f "${GPGSSH_KEY_NOTYETVALID}" >/dev/null && + key_notyetvalid=$(cat "${GPGSSH_KEY_NOTYETVALID}.pub") && + + # Timestamps outside of test_tick span + ts2005a=20050401000000 ts2005b=200504020000 && + # Timestamps within test_tick span + ts2005c=20050407000000 ts2005d=200504100000 && + # Definitely not yet valid / expired timestamps + ts2000=20000101000000 ts2999=29990101000000 && + + cat >>"${GPGSSH_ALLOWED_SIGNERS}" <<-EOF && + "timeboxed valid key" valid-after="$ts2005c",valid-before="$ts2005d" $key_valid" + "timeboxed invalid key" valid-after="$ts2005a",valid-before="$ts2005b" $key_invalid" + "principal with expired key" valid-before="$ts2000" $key_expired" + "principal with not yet valid key" valid-after="$ts2999" $key_notyetvalid" + EOF + + # and verify ssh-keygen verifies the key lifetime + echo "testpayload" | + ssh-keygen -Y sign -n "git" -f "${GPGSSH_KEY_EXPIRED}" >gpgssh_verifytime_prereq.sig && + ! (ssh-keygen -Y verify -n "git" -f "${GPGSSH_ALLOWED_SIGNERS}" -I "principal with expired key" -s gpgssh_verifytime_prereq.sig) ' sanitize_pgp() { diff --git a/t/lib-pager.sh b/t/lib-pager.sh index 3aa7a3ffd8..e5eb28df4e 100644 --- a/t/lib-pager.sh +++ b/t/lib-pager.sh @@ -3,7 +3,7 @@ test_expect_success 'determine default pager' ' test_might_fail git config --unset core.pager && less=$( - unset PAGER GIT_PAGER; + sane_unset PAGER GIT_PAGER && git var GIT_PAGER ) && test -n "$less" diff --git a/t/perf/p0005-status.sh b/t/perf/p0005-status.sh index 0b0aa9858f..ca58d6c9b5 100755 --- a/t/perf/p0005-status.sh +++ b/t/perf/p0005-status.sh @@ -24,17 +24,17 @@ test_perf_default_repo test_expect_success "setup repo" ' if git rev-parse --verify refs/heads/p0006-ballast^{commit} then - echo Assuming synthetic repo from many-files.sh - git branch br_base master - git branch br_ballast p0006-ballast - git config --local core.sparsecheckout 1 + echo Assuming synthetic repo from many-files.sh && + git branch br_base master && + git branch br_ballast p0006-ballast && + git config --local core.sparsecheckout 1 && cat >.git/info/sparse-checkout <<-EOF /* !ballast/* EOF else - echo Assuming non-synthetic repo... - git branch br_base $(git rev-list HEAD | tail -n 1) + echo Assuming non-synthetic repo... && + git branch br_base $(git rev-list HEAD | tail -n 1) && git branch br_ballast HEAD fi && git checkout -q br_ballast && diff --git a/t/perf/p0006-read-tree-checkout.sh b/t/perf/p0006-read-tree-checkout.sh index 78cc23fe2f..900b385c4b 100755 --- a/t/perf/p0006-read-tree-checkout.sh +++ b/t/perf/p0006-read-tree-checkout.sh @@ -24,21 +24,21 @@ test_perf_default_repo test_expect_success "setup repo" ' if git rev-parse --verify refs/heads/p0006-ballast^{commit} then - echo Assuming synthetic repo from many-files.sh - git branch br_base master - git branch br_ballast p0006-ballast^ - git branch br_ballast_alias p0006-ballast^ - git branch br_ballast_plus_1 p0006-ballast - git config --local core.sparsecheckout 1 + echo Assuming synthetic repo from many-files.sh && + git branch br_base master && + git branch br_ballast p0006-ballast^ && + git branch br_ballast_alias p0006-ballast^ && + git branch br_ballast_plus_1 p0006-ballast && + git config --local core.sparsecheckout 1 && cat >.git/info/sparse-checkout <<-EOF /* !ballast/* EOF else - echo Assuming non-synthetic repo... - git branch br_base $(git rev-list HEAD | tail -n 1) - git branch br_ballast HEAD^ || error "no ancestor commit from current head" - git branch br_ballast_alias HEAD^ + echo Assuming non-synthetic repo... && + git branch br_base $(git rev-list HEAD | tail -n 1) && + git branch br_ballast HEAD^ || error "no ancestor commit from current head" && + git branch br_ballast_alias HEAD^ && git branch br_ballast_plus_1 HEAD fi && git checkout -q br_ballast && diff --git a/t/perf/p0007-write-cache.sh b/t/perf/p0007-write-cache.sh index 09595264f0..25d8ff7443 100755 --- a/t/perf/p0007-write-cache.sh +++ b/t/perf/p0007-write-cache.sh @@ -9,8 +9,8 @@ test_perf_default_repo test_expect_success "setup repo" ' if git rev-parse --verify refs/heads/p0006-ballast^{commit} then - echo Assuming synthetic repo from many-files.sh - git config --local core.sparsecheckout 1 + echo Assuming synthetic repo from many-files.sh && + git config --local core.sparsecheckout 1 && cat >.git/info/sparse-checkout <<-EOF /* !ballast/* diff --git a/t/perf/p0100-globbing.sh b/t/perf/p0100-globbing.sh index dd18a9ce2b..439e9c8e3c 100755 --- a/t/perf/p0100-globbing.sh +++ b/t/perf/p0100-globbing.sh @@ -19,9 +19,9 @@ test_expect_success 'setup' ' printf "a" >>refname && for j in $(test_seq 1 $i) do - printf "a*" >>refglob.$i + printf "a*" >>refglob.$i || return 1 done && - echo b >>refglob.$i + echo b >>refglob.$i || return 1 done && test_commit test $(cat refname).t "" $(cat refname).t ' diff --git a/t/perf/p1400-update-ref.sh b/t/perf/p1400-update-ref.sh index dda8a74866..a75969cbb1 100755 --- a/t/perf/p1400-update-ref.sh +++ b/t/perf/p1400-update-ref.sh @@ -13,7 +13,7 @@ test_expect_success "setup" ' do printf "start\ncreate refs/heads/%d PRE\ncommit\n" $i && printf "start\nupdate refs/heads/%d POST PRE\ncommit\n" $i && - printf "start\ndelete refs/heads/%d POST\ncommit\n" $i + printf "start\ndelete refs/heads/%d POST\ncommit\n" $i || return 1 done >instructions ' @@ -22,7 +22,7 @@ test_perf "update-ref" ' do git update-ref refs/heads/branch PRE && git update-ref refs/heads/branch POST PRE && - git update-ref -d refs/heads/branch + git update-ref -d refs/heads/branch || return 1 done ' diff --git a/t/perf/p1451-fsck-skip-list.sh b/t/perf/p1451-fsck-skip-list.sh index c2b97d2487..f767d834f2 100755 --- a/t/perf/p1451-fsck-skip-list.sh +++ b/t/perf/p1451-fsck-skip-list.sh @@ -15,7 +15,7 @@ test_expect_success "setup $n bad commits" ' echo "committer C <c@example.com> 1234567890 +0000" && echo "data <<EOF" && echo "$i.Q." && - echo "EOF" + echo "EOF" || return 1 done | q_to_nul | git fast-import ' diff --git a/t/perf/p2000-sparse-operations.sh b/t/perf/p2000-sparse-operations.sh index 597626276f..cb777c74a2 100755 --- a/t/perf/p2000-sparse-operations.sh +++ b/t/perf/p2000-sparse-operations.sh @@ -110,5 +110,12 @@ test_perf_on_all git add -A test_perf_on_all git add . test_perf_on_all git commit -a -m A test_perf_on_all git checkout -f - +test_perf_on_all git reset +test_perf_on_all git reset --hard +test_perf_on_all git reset -- does-not-exist +test_perf_on_all git diff +test_perf_on_all git diff --cached +test_perf_on_all git blame $SPARSE_CONE/a +test_perf_on_all git blame $SPARSE_CONE/f3/a test_done diff --git a/t/perf/p3400-rebase.sh b/t/perf/p3400-rebase.sh index 43d5a34e8c..e6b0277729 100755 --- a/t/perf/p3400-rebase.sh +++ b/t/perf/p3400-rebase.sh @@ -22,7 +22,7 @@ test_expect_success 'setup rebasing on top of a lot of changes' ' git add unrelated-file$i && test_tick && git commit -m commit$i-reverse unrelated-file$i || - break + return 1 done && git checkout to-rebase && test_commit our-patch interesting-file diff --git a/t/perf/p4002-diff-color-moved.sh b/t/perf/p4002-diff-color-moved.sh new file mode 100755 index 0000000000..ab2af931c0 --- /dev/null +++ b/t/perf/p4002-diff-color-moved.sh @@ -0,0 +1,57 @@ +#!/bin/sh + +test_description='Tests diff --color-moved performance' +. ./perf-lib.sh + +test_perf_default_repo + +# The endpoints of the diff can be customized by setting TEST_REV_A +# and TEST_REV_B in the environment when running this test. + +rev="${TEST_REV_A:-v2.28.0}" +if ! rev_a="$(git rev-parse --quiet --verify "$rev")" +then + skip_all="skipping because '$rev' was not found. \ + Use TEST_REV_A and TEST_REV_B to set the revs to use" + test_done +fi +rev="${TEST_REV_B:-v2.29.0}" +if ! rev_b="$(git rev-parse --quiet --verify "$rev")" +then + skip_all="skipping because '$rev' was not found. \ + Use TEST_REV_A and TEST_REV_B to set the revs to use" + test_done +fi + +GIT_PAGER_IN_USE=1 +test_export GIT_PAGER_IN_USE rev_a rev_b + +test_perf 'diff --no-color-moved --no-color-moved-ws large change' ' + git diff --no-color-moved --no-color-moved-ws $rev_a $rev_b +' + +test_perf 'diff --color-moved --no-color-moved-ws large change' ' + git diff --color-moved=zebra --no-color-moved-ws $rev_a $rev_b +' + +test_perf 'diff --color-moved-ws=allow-indentation-change large change' ' + git diff --color-moved=zebra --color-moved-ws=allow-indentation-change \ + $rev_a $rev_b +' + +test_perf 'log --no-color-moved --no-color-moved-ws' ' + git log --no-color-moved --no-color-moved-ws --no-merges --patch \ + -n1000 $rev_b +' + +test_perf 'log --color-moved --no-color-moved-ws' ' + git log --color-moved=zebra --no-color-moved-ws --no-merges --patch \ + -n1000 $rev_b +' + +test_perf 'log --color-moved-ws=allow-indentation-change' ' + git log --color-moved=zebra --color-moved-ws=allow-indentation-change \ + --no-merges --patch -n1000 $rev_b +' + +test_done diff --git a/t/perf/p5302-pack-index.sh b/t/perf/p5302-pack-index.sh index 228593d9ad..c16f6a3ff6 100755 --- a/t/perf/p5302-pack-index.sh +++ b/t/perf/p5302-pack-index.sh @@ -21,8 +21,8 @@ test_expect_success 'set up thread-counting tests' ' threads= && while test $t -gt 0 do - threads="$t $threads" - t=$((t / 2)) + threads="$t $threads" && + t=$((t / 2)) || return 1 done ' diff --git a/t/perf/p5303-many-packs.sh b/t/perf/p5303-many-packs.sh index 35c0cbdf49..af173a7b73 100755 --- a/t/perf/p5303-many-packs.sh +++ b/t/perf/p5303-many-packs.sh @@ -126,11 +126,11 @@ done # Measure pack loading with 10,000 packs. test_expect_success 'generate lots of packs' ' for i in $(test_seq 10000); do - echo "blob" - echo "data <<EOF" - echo "blob $i" - echo "EOF" - echo "checkpoint" + echo "blob" && + echo "data <<EOF" && + echo "blob $i" && + echo "EOF" && + echo "checkpoint" || return 1 done | git -c fastimport.unpackLimit=0 fast-import ' diff --git a/t/perf/p7519-fsmonitor.sh b/t/perf/p7519-fsmonitor.sh index 5eb5044a10..c8be58f3c7 100755 --- a/t/perf/p7519-fsmonitor.sh +++ b/t/perf/p7519-fsmonitor.sh @@ -119,10 +119,10 @@ test_expect_success "one time repo setup" ' fi && mkdir 1_file 10_files 100_files 1000_files 10000_files && - for i in $(test_seq 1 10); do touch 10_files/$i; done && - for i in $(test_seq 1 100); do touch 100_files/$i; done && - for i in $(test_seq 1 1000); do touch 1000_files/$i; done && - for i in $(test_seq 1 10000); do touch 10000_files/$i; done && + for i in $(test_seq 1 10); do touch 10_files/$i || return 1; done && + for i in $(test_seq 1 100); do touch 100_files/$i || return 1; done && + for i in $(test_seq 1 1000); do touch 1000_files/$i || return 1; done && + for i in $(test_seq 1 10000); do touch 10000_files/$i || return 1; done && git add 1_file 10_files 100_files 1000_files 10000_files && git commit -qm "Add files" && diff --git a/t/perf/perf-lib.sh b/t/perf/perf-lib.sh index 780a7402d5..407252bac7 100644 --- a/t/perf/perf-lib.sh +++ b/t/perf/perf-lib.sh @@ -161,7 +161,7 @@ test_run_perf_ () { test_cleanup=: test_export_="test_cleanup" export test_cleanup test_export_ - "$GTIME" -f "%E %U %S" -o test_time.$i "$SHELL" -c ' + "$GTIME" -f "%E %U %S" -o test_time.$i "$TEST_SHELL_PATH" -c ' . '"$TEST_DIRECTORY"/test-lib-functions.sh' test_export () { test_export_="$test_export_ $*" diff --git a/t/t0005-signals.sh b/t/t0005-signals.sh index a5ec6a0315..eba75a2490 100755 --- a/t/t0005-signals.sh +++ b/t/t0005-signals.sh @@ -48,7 +48,7 @@ test_expect_success !MINGW 'a constipated git dies with SIGPIPE' ' ' test_expect_success !MINGW 'a constipated git dies with SIGPIPE even if parent ignores it' ' - OUT=$( ((trap "" PIPE; large_git; echo $? 1>&3) | :) 3>&1 ) && + OUT=$( ((trap "" PIPE && large_git; echo $? 1>&3) | :) 3>&1 ) && test_match_signal 13 "$OUT" ' diff --git a/t/t0006-date.sh b/t/t0006-date.sh index 6b757d7169..794186961e 100755 --- a/t/t0006-date.sh +++ b/t/t0006-date.sh @@ -63,6 +63,10 @@ check_show 'format-local:%%z' "$TIME" '%z' check_show 'format:%Y-%m-%d %H:%M:%S' "$TIME" '2016-06-15 16:13:20' check_show 'format-local:%Y-%m-%d %H:%M:%S' "$TIME" '2016-06-15 09:13:20' '' EST5 +check_show 'format:%s' '123456789 +1234' 123456789 +check_show 'format:%s' '123456789 -1234' 123456789 +check_show 'format-local:%s' '123456789 -1234' 123456789 + # arbitrary time absurdly far in the future FUTURE="5758122296 -0400" check_show iso "$FUTURE" "2152-06-19 18:24:56 -0400" TIME_IS_64BIT,TIME_T_IS_64BIT diff --git a/t/t0007-git-var.sh b/t/t0007-git-var.sh index 53af92d571..e56f4b9ac5 100755 --- a/t/t0007-git-var.sh +++ b/t/t0007-git-var.sh @@ -27,6 +27,26 @@ test_expect_success !FAIL_PREREQS,!AUTOIDENT 'requested identities are strict' ' ) ' +test_expect_success 'get GIT_DEFAULT_BRANCH without configuration' ' + ( + sane_unset GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME && + git init defbranch && + git -C defbranch symbolic-ref --short HEAD >expect && + git var GIT_DEFAULT_BRANCH >actual && + test_cmp expect actual + ) +' + +test_expect_success 'get GIT_DEFAULT_BRANCH with configuration' ' + test_config init.defaultbranch foo && + ( + sane_unset GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME && + echo foo >expect && + git var GIT_DEFAULT_BRANCH >actual && + test_cmp expect actual + ) +' + # For git var -l, we check only a representative variable; # testing the whole output would make our test too brittle with # respect to unrelated changes in the test suite's environment. diff --git a/t/t0008-ignores.sh b/t/t0008-ignores.sh index 42d2314804..5575dade8e 100755 --- a/t/t0008-ignores.sh +++ b/t/t0008-ignores.sh @@ -200,7 +200,7 @@ test_expect_success 'setup' ' do : >$dir/not-ignored && : >$dir/ignored-and-untracked && - : >$dir/ignored-but-in-index + : >$dir/ignored-but-in-index || return 1 done && git add -f ignored-but-in-index a/ignored-but-in-index && cat <<-\EOF >a/.gitignore && diff --git a/t/t0011-hashmap.sh b/t/t0011-hashmap.sh index e094975b13..1cb6aa6824 100755 --- a/t/t0011-hashmap.sh +++ b/t/t0011-hashmap.sh @@ -220,7 +220,7 @@ test_expect_success 'grow / shrink' ' for n in $(test_seq 51) do echo put key$n value$n >> in && - echo NULL >> expect + echo NULL >> expect || return 1 done && echo size >> in && echo 64 51 >> expect && @@ -231,7 +231,7 @@ test_expect_success 'grow / shrink' ' for n in $(test_seq 12) do echo remove key$n >> in && - echo value$n >> expect + echo value$n >> expect || return 1 done && echo size >> in && echo 256 40 >> expect && diff --git a/t/t0020-crlf.sh b/t/t0020-crlf.sh index f25ae8b5e1..35cc8c3b39 100755 --- a/t/t0020-crlf.sh +++ b/t/t0020-crlf.sh @@ -5,6 +5,7 @@ test_description='CRLF conversion' GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh has_cr() { @@ -22,10 +23,10 @@ test_expect_success setup ' git config core.autocrlf false && - for w in Hello world how are you; do echo $w; done >one && + test_write_lines Hello world how are you >one && mkdir dir && - for w in I am very very fine thank you; do echo $w; done >dir/two && - for w in Oh here is NULQin text here; do echo $w; done | q_to_nul >three && + test_write_lines I am very very fine thank you >dir/two && + test_write_lines Oh here is NULQin text here | q_to_nul >three && git add . && git commit -m initial && @@ -35,7 +36,7 @@ test_expect_success setup ' two=$(git rev-parse HEAD:dir/two) && three=$(git rev-parse HEAD:three) && - for w in Some extra lines here; do echo $w; done >>one && + test_write_lines Some extra lines here >>one && git diff >patch.file && patched=$(git hash-object --stdin <one) && git read-tree --reset -u HEAD @@ -46,7 +47,7 @@ test_expect_success 'safecrlf: autocrlf=input, all CRLF' ' git config core.autocrlf input && git config core.safecrlf true && - for w in I am all CRLF; do echo $w; done | append_cr >allcrlf && + test_write_lines I am all CRLF | append_cr >allcrlf && test_must_fail git add allcrlf ' @@ -55,7 +56,7 @@ test_expect_success 'safecrlf: autocrlf=input, mixed LF/CRLF' ' git config core.autocrlf input && git config core.safecrlf true && - for w in Oh here is CRLFQ in text; do echo $w; done | q_to_cr >mixed && + test_write_lines Oh here is CRLFQ in text | q_to_cr >mixed && test_must_fail git add mixed ' @@ -64,7 +65,7 @@ test_expect_success 'safecrlf: autocrlf=true, all LF' ' git config core.autocrlf true && git config core.safecrlf true && - for w in I am all LF; do echo $w; done >alllf && + test_write_lines I am all LF >alllf && test_must_fail git add alllf ' @@ -73,7 +74,7 @@ test_expect_success 'safecrlf: autocrlf=true mixed LF/CRLF' ' git config core.autocrlf true && git config core.safecrlf true && - for w in Oh here is CRLFQ in text; do echo $w; done | q_to_cr >mixed && + test_write_lines Oh here is CRLFQ in text | q_to_cr >mixed && test_must_fail git add mixed ' @@ -82,10 +83,10 @@ test_expect_success 'safecrlf: print warning only once' ' git config core.autocrlf input && git config core.safecrlf warn && - for w in I am all LF; do echo $w; done >doublewarn && + test_write_lines I am all LF >doublewarn && git add doublewarn && git commit -m "nowarn" && - for w in Oh here is CRLFQ in text; do echo $w; done | q_to_cr >doublewarn && + test_write_lines Oh here is CRLFQ in text | q_to_cr >doublewarn && git add doublewarn 2>err && grep "CRLF will be replaced by LF" err >err.warnings && test_line_count = 1 err.warnings @@ -103,7 +104,7 @@ test_expect_success 'safecrlf: no warning with safecrlf=false' ' git config core.autocrlf input && git config core.safecrlf false && - for w in I am all CRLF; do echo $w; done | append_cr >allcrlf && + test_write_lines I am all CRLF | append_cr >allcrlf && git add allcrlf 2>err && test_must_be_empty err ' @@ -351,9 +352,9 @@ test_expect_success 'setting up for new autocrlf tests' ' git config core.autocrlf false && git config core.safecrlf false && rm -rf .????* * && - for w in I am all LF; do echo $w; done >alllf && - for w in Oh here is CRLFQ in text; do echo $w; done | q_to_cr >mixed && - for w in I am all CRLF; do echo $w; done | append_cr >allcrlf && + test_write_lines I am all LF >alllf && + test_write_lines Oh here is CRLFQ in text | q_to_cr >mixed && + test_write_lines I am all CRLF | append_cr >allcrlf && git add -A . && git commit -m "alllf, allcrlf and mixed only" && git tag -a -m "message" autocrlf-checkpoint diff --git a/t/t0021-conversion.sh b/t/t0021-conversion.sh index 33dfc9cd56..bad37abad2 100755 --- a/t/t0021-conversion.sh +++ b/t/t0021-conversion.sh @@ -76,13 +76,13 @@ test_expect_success setup ' git config filter.rot13.clean ./rot13.sh && { - echo "*.t filter=rot13" + echo "*.t filter=rot13" && echo "*.i ident" } >.gitattributes && { - echo a b c d e f g h i j k l m - echo n o p q r s t u v w x y z + echo a b c d e f g h i j k l m && + echo n o p q r s t u v w x y z && echo '\''$Id$'\'' } >test && cat test >test.t && @@ -118,17 +118,17 @@ test_expect_success check ' # If an expanded ident ever gets into the repository, we want to make sure that # it is collapsed before being expanded again on checkout test_expect_success expanded_in_repo ' - { - echo "File with expanded keywords" - echo "\$Id\$" - echo "\$Id:\$" - echo "\$Id: 0000000000000000000000000000000000000000 \$" - echo "\$Id: NoSpaceAtEnd\$" - echo "\$Id:NoSpaceAtFront \$" - echo "\$Id:NoSpaceAtEitherEnd\$" - echo "\$Id: NoTerminatingSymbol" - echo "\$Id: Foreign Commit With Spaces \$" - } >expanded-keywords.0 && + cat >expanded-keywords.0 <<-\EOF && + File with expanded keywords + $Id$ + $Id:$ + $Id: 0000000000000000000000000000000000000000 $ + $Id: NoSpaceAtEnd$ + $Id:NoSpaceAtFront $ + $Id:NoSpaceAtEitherEnd$ + $Id: NoTerminatingSymbol + $Id: Foreign Commit With Spaces $ + EOF { cat expanded-keywords.0 && @@ -139,17 +139,17 @@ test_expect_success expanded_in_repo ' git commit -m "File with keywords expanded" && id=$(git rev-parse --verify :expanded-keywords) && - { - echo "File with expanded keywords" - echo "\$Id: $id \$" - echo "\$Id: $id \$" - echo "\$Id: $id \$" - echo "\$Id: $id \$" - echo "\$Id: $id \$" - echo "\$Id: $id \$" - echo "\$Id: NoTerminatingSymbol" - echo "\$Id: Foreign Commit With Spaces \$" - } >expected-output.0 && + cat >expected-output.0 <<-EOF && + File with expanded keywords + \$Id: $id \$ + \$Id: $id \$ + \$Id: $id \$ + \$Id: $id \$ + \$Id: $id \$ + \$Id: $id \$ + \$Id: NoTerminatingSymbol + \$Id: Foreign Commit With Spaces \$ + EOF { cat expected-output.0 && printf "\$Id: NoTerminatingSymbolAtEOF" @@ -159,7 +159,7 @@ test_expect_success expanded_in_repo ' printf "\$Id: NoTerminatingSymbolAtEOF" } >expected-output-crlf && { - echo "expanded-keywords ident" + echo "expanded-keywords ident" && echo "expanded-keywords-crlf ident text eol=crlf" } >>.gitattributes && @@ -285,7 +285,7 @@ test_expect_success 'required filter with absent smudge field' ' test_expect_success 'filtering large input to small output should use little memory' ' test_config filter.devnull.clean "cat >/dev/null" && test_config filter.devnull.required true && - for i in $(test_seq 1 30); do printf "%1048576d" 1; done >30MB && + for i in $(test_seq 1 30); do printf "%1048576d" 1 || return 1; done >30MB && echo "30MB filter=devnull" >.gitattributes && GIT_MMAP_LIMIT=1m GIT_ALLOC_LIMIT=1m git add 30MB ' @@ -303,7 +303,7 @@ test_expect_success 'filter that does not read is fine' ' test_expect_success EXPENSIVE 'filter large file' ' test_config filter.largefile.smudge cat && test_config filter.largefile.clean cat && - for i in $(test_seq 1 2048); do printf "%1048576d" 1; done >2GB && + for i in $(test_seq 1 2048); do printf "%1048576d" 1 || return 1; done >2GB && echo "2GB filter=largefile" >.gitattributes && git add 2GB 2>err && test_must_be_empty err && @@ -643,7 +643,7 @@ test_expect_success PERL 'required process filter should process multiple packet for FILE in "$TEST_ROOT"/*.file do cp "$FILE" . && - rot13.sh <"$FILE" >"$FILE.rot13" + rot13.sh <"$FILE" >"$FILE.rot13" || return 1 done && echo "*.file filter=protocol" >.gitattributes && @@ -682,7 +682,7 @@ test_expect_success PERL 'required process filter should process multiple packet for FILE in *.file do - test_cmp_committed_rot13 "$TEST_ROOT/$FILE" $FILE + test_cmp_committed_rot13 "$TEST_ROOT/$FILE" $FILE || return 1 done ) ' diff --git a/t/t0026-eol-config.sh b/t/t0026-eol-config.sh index cdcafcdff7..f426a185bb 100755 --- a/t/t0026-eol-config.sh +++ b/t/t0026-eol-config.sh @@ -15,8 +15,8 @@ test_expect_success setup ' echo "one text" > .gitattributes && - for w in Hello world how are you; do echo $w; done >one && - for w in I am very very fine thank you; do echo $w; done >two && + test_write_lines Hello world how are you >one && + test_write_lines I am very very fine thank you >two && git add . && git commit -m initial && diff --git a/t/t0032-reftable-unittest.sh b/t/t0032-reftable-unittest.sh new file mode 100755 index 0000000000..0ed14971a5 --- /dev/null +++ b/t/t0032-reftable-unittest.sh @@ -0,0 +1,15 @@ +#!/bin/sh +# +# Copyright (c) 2020 Google LLC +# + +test_description='reftable unittests' + +. ./test-lib.sh + +test_expect_success 'unittests' ' + TMPDIR=$(pwd) && export TMPDIR && + test-tool reftable +' + +test_done diff --git a/t/t0060-path-utils.sh b/t/t0060-path-utils.sh index 34d1061f32..71a5d370cc 100755 --- a/t/t0060-path-utils.sh +++ b/t/t0060-path-utils.sh @@ -216,7 +216,7 @@ test_expect_success SYMLINKS 'real path works on symlinks' ' mkdir second && ln -s ../first second/other && mkdir third && - dir="$(cd .git; pwd -P)" && + dir="$(cd .git && pwd -P)" && dir2=third/../second/other/.git && test "$dir" = "$(test-tool path-utils real_path $dir2)" && file="$dir"/index && @@ -224,7 +224,7 @@ test_expect_success SYMLINKS 'real path works on symlinks' ' basename=blub && test "$dir/$basename" = "$(cd .git && test-tool path-utils real_path "$basename")" && ln -s ../first/file .git/syml && - sym="$(cd first; pwd -P)"/file && + sym="$(cd first && pwd -P)"/file && test "$sym" = "$(test-tool path-utils real_path "$dir2/syml")" ' diff --git a/t/t0069-oidtree.sh b/t/t0069-oidtree.sh index 74cc59bf8a..889db50818 100755 --- a/t/t0069-oidtree.sh +++ b/t/t0069-oidtree.sh @@ -28,7 +28,7 @@ test_expect_success 'oidtree insert and contains' ' EOF { echoid insert 444 1 2 3 4 5 a b c d e && - echoid contains 44 441 440 444 4440 4444 + echoid contains 44 441 440 444 4440 4444 && echo clear } | test-tool oidtree >actual && test_cmp expect actual @@ -37,11 +37,11 @@ test_expect_success 'oidtree insert and contains' ' test_expect_success 'oidtree each' ' echoid "" 123 321 321 >expect && { - echoid insert f 9 8 123 321 a b c d e - echo each 12300 - echo each 3211 - echo each 3210 - echo each 32100 + echoid insert f 9 8 123 321 a b c d e && + echo each 12300 && + echo each 3211 && + echo each 3210 && + echo each 32100 && echo clear } | test-tool oidtree >actual && test_cmp expect actual diff --git a/t/t0071-sort.sh b/t/t0071-sort.sh index a8ab174879..6f9a501c72 100755 --- a/t/t0071-sort.sh +++ b/t/t0071-sort.sh @@ -2,6 +2,7 @@ test_description='verify sort functions' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success 'llist_mergesort()' ' diff --git a/t/t0095-bloom.sh b/t/t0095-bloom.sh index 7e4ab1795f..5945973552 100755 --- a/t/t0095-bloom.sh +++ b/t/t0095-bloom.sh @@ -84,7 +84,7 @@ test_expect_success 'get bloom filter for commit with 10 changes' ' mkdir smallDir && for i in $(test_seq 0 9) do - echo $i >smallDir/$i + echo $i >smallDir/$i || return 1 done && git add smallDir && git commit -m "commit with 10 changes" && @@ -102,7 +102,7 @@ test_expect_success EXPENSIVE 'get bloom filter for commit with 513 changes' ' mkdir bigDir && for i in $(test_seq 0 511) do - echo $i >bigDir/$i + echo $i >bigDir/$i || return 1 done && git add bigDir && git commit -m "commit with 513 changes" && diff --git a/t/t0110-urlmatch-normalization.sh b/t/t0110-urlmatch-normalization.sh index f99529d838..4dc9fecf72 100755 --- a/t/t0110-urlmatch-normalization.sh +++ b/t/t0110-urlmatch-normalization.sh @@ -47,7 +47,7 @@ test_expect_success 'url authority' ' test-tool urlmatch-normalization "scheme://@host" && test-tool urlmatch-normalization "scheme://%00@host" && ! test-tool urlmatch-normalization "scheme://%%@host" && - ! test-tool urlmatch-normalization "scheme://host_" && + test-tool urlmatch-normalization "scheme://host_" && test-tool urlmatch-normalization "scheme://user:pass@host/" && test-tool urlmatch-normalization "scheme://@host/" && test-tool urlmatch-normalization "scheme://host/" && diff --git a/t/t0200-gettext-basic.sh b/t/t0200-gettext-basic.sh index 8853d8afb9..522fb2ae69 100755 --- a/t/t0200-gettext-basic.sh +++ b/t/t0200-gettext-basic.sh @@ -5,6 +5,7 @@ test_description='Gettext support for Git' +TEST_PASSES_SANITIZE_LEAK=true . ./lib-gettext.sh test_expect_success "sanity: \$GIT_INTERNAL_GETTEXT_SH_SCHEME is set (to $GIT_INTERNAL_GETTEXT_SH_SCHEME)" ' diff --git a/t/t0201-gettext-fallbacks.sh b/t/t0201-gettext-fallbacks.sh index 6c74df0dc6..8724ce1052 100755 --- a/t/t0201-gettext-fallbacks.sh +++ b/t/t0201-gettext-fallbacks.sh @@ -8,6 +8,7 @@ test_description='Gettext Shell fallbacks' GIT_INTERNAL_GETTEXT_TEST_FALLBACKS=YesPlease export GIT_INTERNAL_GETTEXT_TEST_FALLBACKS +TEST_PASSES_SANITIZE_LEAK=true . ./lib-gettext.sh test_expect_success "sanity: \$GIT_INTERNAL_GETTEXT_SH_SCHEME is set (to $GIT_INTERNAL_GETTEXT_SH_SCHEME)" ' diff --git a/t/t0202-gettext-perl.sh b/t/t0202-gettext-perl.sh index a29d166e00..df2ea34932 100755 --- a/t/t0202-gettext-perl.sh +++ b/t/t0202-gettext-perl.sh @@ -5,6 +5,7 @@ test_description='Perl gettext interface (Git::I18N)' +TEST_PASSES_SANITIZE_LEAK=true . ./lib-gettext.sh if ! test_have_prereq PERL; then diff --git a/t/t0204-gettext-reencode-sanity.sh b/t/t0204-gettext-reencode-sanity.sh index 8437e51eb5..4f2e0dcb02 100755 --- a/t/t0204-gettext-reencode-sanity.sh +++ b/t/t0204-gettext-reencode-sanity.sh @@ -5,6 +5,7 @@ test_description="Gettext reencoding of our *.po/*.mo files works" +TEST_PASSES_SANITIZE_LEAK=true . ./lib-gettext.sh # The constants used in a tricky observation for undefined behaviour diff --git a/t/t0410-partial-clone.sh b/t/t0410-partial-clone.sh index c76485b1b6..f17abd298c 100755 --- a/t/t0410-partial-clone.sh +++ b/t/t0410-partial-clone.sh @@ -469,7 +469,7 @@ test_expect_success 'rev-list dies for missing objects on cmd line' ' git -C repo rev-list --ignore-missing --objects \ --exclude-promisor-objects "$OBJ" && git -C repo rev-list --ignore-missing --objects-edge-aggressive \ - --exclude-promisor-objects "$OBJ" + --exclude-promisor-objects "$OBJ" || return 1 done ' diff --git a/t/t1002-read-tree-m-u-2way.sh b/t/t1002-read-tree-m-u-2way.sh index 9c05f5e1f5..ca5c5510c7 100755 --- a/t/t1002-read-tree-m-u-2way.sh +++ b/t/t1002-read-tree-m-u-2way.sh @@ -8,6 +8,8 @@ test_description='Two way merge with read-tree -m -u $H $M This is identical to t1001, but uses -u to update the work tree as well. ' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh . "$TEST_DIRECTORY"/lib-read-tree.sh diff --git a/t/t1005-read-tree-reset.sh b/t/t1005-read-tree-reset.sh index 83b09e1310..12e30d77d0 100755 --- a/t/t1005-read-tree-reset.sh +++ b/t/t1005-read-tree-reset.sh @@ -2,6 +2,7 @@ test_description='read-tree -u --reset' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh . "$TEST_DIRECTORY"/lib-read-tree.sh diff --git a/t/t1006-cat-file.sh b/t/t1006-cat-file.sh index 658628375c..39382fa195 100755 --- a/t/t1006-cat-file.sh +++ b/t/t1006-cat-file.sh @@ -211,14 +211,14 @@ done test_expect_success "--batch-check for a non-existent named object" ' test "foobar42 missing foobar84 missing" = \ - "$( ( echo foobar42; echo_without_newline foobar84; ) | git cat-file --batch-check)" + "$( ( echo foobar42 && echo_without_newline foobar84 ) | git cat-file --batch-check)" ' test_expect_success "--batch-check for a non-existent hash" ' test "0000000000000000000000000000000000000042 missing 0000000000000000000000000000000000000084 missing" = \ - "$( ( echo 0000000000000000000000000000000000000042; - echo_without_newline 0000000000000000000000000000000000000084; ) | + "$( ( echo 0000000000000000000000000000000000000042 && + echo_without_newline 0000000000000000000000000000000000000084 ) | git cat-file --batch-check)" ' @@ -226,8 +226,8 @@ test_expect_success "--batch for an existent and a non-existent hash" ' test "$tag_sha1 tag $tag_size $tag_content 0000000000000000000000000000000000000000 missing" = \ - "$( ( echo $tag_sha1; - echo_without_newline 0000000000000000000000000000000000000000; ) | + "$( ( echo $tag_sha1 && + echo_without_newline 0000000000000000000000000000000000000000 ) | git cat-file --batch)" ' @@ -283,7 +283,7 @@ test_expect_success "--batch-check with multiple sha1s gives correct format" ' test_expect_success 'setup blobs which are likely to delta' ' test-tool genrandom foo 10240 >foo && - { cat foo; echo plus; } >foo-plus && + { cat foo && echo plus; } >foo-plus && git add foo foo-plus && git commit -m foo && cat >blobs <<-\EOF @@ -452,9 +452,8 @@ test_expect_success 'the --allow-unknown-type option does not consider replaceme # Create it manually, as "git replace" will die on bogus # types. head=$(git rev-parse --verify HEAD) && - test_when_finished "rm -rf .git/refs/replace" && - mkdir -p .git/refs/replace && - echo $head >.git/refs/replace/$bogus_short_sha1 && + test_when_finished "test-tool ref-store main delete-refs 0 msg refs/replace/$bogus_short_sha1" && + test-tool ref-store main update-ref msg "refs/replace/$bogus_short_sha1" $head $ZERO_OID REF_SKIP_OID_VERIFICATION && cat >expect <<-EOF && commit diff --git a/t/t1008-read-tree-overlay.sh b/t/t1008-read-tree-overlay.sh index 4512fb0b6e..ad5936e54d 100755 --- a/t/t1008-read-tree-overlay.sh +++ b/t/t1008-read-tree-overlay.sh @@ -5,6 +5,7 @@ test_description='test multi-tree read-tree without merging' 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-read-tree.sh diff --git a/t/t1010-mktree.sh b/t/t1010-mktree.sh index 48bfad07ab..3c08194526 100755 --- a/t/t1010-mktree.sh +++ b/t/t1010-mktree.sh @@ -6,10 +6,10 @@ TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success setup ' - for d in a a. a0 + for d in a a- a0 do mkdir "$d" && echo "$d/one" >"$d/one" && - git add "$d" + git add "$d" || return 1 done && echo zero >one && git update-index --add --info-only one && diff --git a/t/t1020-subdirectory.sh b/t/t1020-subdirectory.sh index c2df75e495..9fdbb2af80 100755 --- a/t/t1020-subdirectory.sh +++ b/t/t1020-subdirectory.sh @@ -11,9 +11,9 @@ test_description='Try various core-level commands in subdirectory. test_expect_success setup ' long="a b c d e f g h i j k l m n o p q r s t u v w x y z" && - for c in $long; do echo $c; done >one && + test_write_lines $long >one && mkdir dir && - for c in x y z $long a b c; do echo $c; done >dir/two && + test_write_lines x y z $long a b c >dir/two && cp one original.one && cp dir/two original.two ' @@ -22,7 +22,7 @@ test_expect_success 'update-index and ls-files' ' git update-index --add one && case "$(git ls-files)" in one) echo pass one ;; - *) echo bad one; exit 1 ;; + *) echo bad one; return 1 ;; esac && ( cd dir && @@ -34,7 +34,7 @@ test_expect_success 'update-index and ls-files' ' ) && case "$(git ls-files)" in dir/two"$LF"one) echo pass both ;; - *) echo bad; exit 1 ;; + *) echo bad; return 1 ;; esac ' @@ -57,7 +57,7 @@ test_expect_success 'diff-files' ' echo d >>dir/two && case "$(git diff-files --name-only)" in dir/two"$LF"one) echo pass top ;; - *) echo bad top; exit 1 ;; + *) echo bad top; return 1 ;; esac && # diff should not omit leading paths ( diff --git a/t/t1022-read-tree-partial-clone.sh b/t/t1022-read-tree-partial-clone.sh index a763e27c7d..a9953b6a71 100755 --- a/t/t1022-read-tree-partial-clone.sh +++ b/t/t1022-read-tree-partial-clone.sh @@ -4,9 +4,6 @@ test_description='git read-tree in partial clones' TEST_NO_CREATE_REPO=1 -GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main -export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME - . ./test-lib.sh test_expect_success 'read-tree in partial clone prefetches in one batch' ' diff --git a/t/t1050-large.sh b/t/t1050-large.sh index 6bc1d76fb1..4f3aa17c99 100755 --- a/t/t1050-large.sh +++ b/t/t1050-large.sh @@ -51,42 +51,32 @@ EOF test_expect_success 'add a large file or two' ' git add large1 huge large2 && # make sure we got a single packfile and no loose objects - bad= count=0 idx= && + count=0 idx= && for p in .git/objects/pack/pack-*.pack do - count=$(( $count + 1 )) - if test_path_is_file "$p" && - idx=${p%.pack}.idx && test_path_is_file "$idx" - then - continue - fi - bad=t + count=$(( $count + 1 )) && + test_path_is_file "$p" && + idx=${p%.pack}.idx && + test_path_is_file "$idx" || return 1 done && - test -z "$bad" && test $count = 1 && cnt=$(git show-index <"$idx" | wc -l) && test $cnt = 2 && for l in .git/objects/$OIDPATH_REGEX do - test_path_is_file "$l" || continue - bad=t + test_path_is_missing "$l" || return 1 done && - test -z "$bad" && # attempt to add another copy of the same git add large3 && bad= count=0 && for p in .git/objects/pack/pack-*.pack do - count=$(( $count + 1 )) - if test_path_is_file "$p" && - idx=${p%.pack}.idx && test_path_is_file "$idx" - then - continue - fi - bad=t + count=$(( $count + 1 )) && + test_path_is_file "$p" && + idx=${p%.pack}.idx && + test_path_is_file "$idx" || return 1 done && - test -z "$bad" && test $count = 1 ' @@ -115,7 +105,7 @@ test_expect_success 'packsize limit' ' count=0 && for pi in .git/objects/pack/pack-*.idx do - test_path_is_file "$pi" && count=$(( $count + 1 )) + test_path_is_file "$pi" && count=$(( $count + 1 )) || return 1 done && test $count = 2 && @@ -128,7 +118,7 @@ test_expect_success 'packsize limit' ' for pi in .git/objects/pack/pack-*.idx do - git show-index <"$pi" + git show-index <"$pi" || return 1 done | sed -e "s/^[0-9]* \([0-9a-f]*\) .*/\1/" | sort >actual && diff --git a/t/t1051-large-conversion.sh b/t/t1051-large-conversion.sh index 8b7640b3ba..042b0e4429 100755 --- a/t/t1051-large-conversion.sh +++ b/t/t1051-large-conversion.sh @@ -83,4 +83,30 @@ test_expect_success 'ident converts on output' ' test_cmp small.clean large.clean ' +# This smudge filter prepends 5GB of zeros to the file it checks out. This +# ensures that smudging doesn't mangle large files on 64-bit Windows. +test_expect_success EXPENSIVE,SIZE_T_IS_64BIT,!LONG_IS_64BIT \ + 'files over 4GB convert on output' ' + test_commit test small "a small file" && + small_size=$(test_file_size small) && + test_config filter.makelarge.smudge \ + "test-tool genzeros $((5*1024*1024*1024)) && cat" && + echo "small filter=makelarge" >.gitattributes && + rm small && + git checkout -- small && + size=$(test_file_size small) && + test "$size" -eq $((5 * 1024 * 1024 * 1024 + $small_size)) +' + +# This clean filter writes down the size of input it receives. By checking against +# the actual size, we ensure that cleaning doesn't mangle large files on 64-bit Windows. +test_expect_success EXPENSIVE,SIZE_T_IS_64BIT,!LONG_IS_64BIT \ + 'files over 4GB convert on input' ' + test-tool genzeros $((5*1024*1024*1024)) >big && + test_config filter.checklarge.clean "wc -c >big.size" && + echo "big filter=checklarge" >.gitattributes && + git add big && + test $(test_file_size big) -eq $(cat big.size) +' + test_done diff --git a/t/t1091-sparse-checkout-builtin.sh b/t/t1091-sparse-checkout-builtin.sh index 272ba1b566..42776984fe 100755 --- a/t/t1091-sparse-checkout-builtin.sh +++ b/t/t1091-sparse-checkout-builtin.sh @@ -41,7 +41,15 @@ test_expect_success 'setup' ' ) ' -test_expect_success 'git sparse-checkout list (empty)' ' +test_expect_success 'git sparse-checkout list (not sparse)' ' + test_must_fail git -C repo sparse-checkout list >list 2>err && + test_must_be_empty list && + test_i18ngrep "this worktree is not sparse" err +' + +test_expect_success 'git sparse-checkout list (not sparse)' ' + git -C repo sparse-checkout set && + rm repo/.git/info/sparse-checkout && git -C repo sparse-checkout list >list 2>err && test_must_be_empty list && test_i18ngrep "this worktree is not sparse (sparse-checkout file may not exist)" err @@ -103,6 +111,18 @@ test_expect_success 'clone --sparse' ' check_files clone a ' +test_expect_success 'switching to cone mode with non-cone mode patterns' ' + git init bad-patterns && + ( + cd bad-patterns && + git sparse-checkout init && + git sparse-checkout add dir && + git config core.sparseCheckoutCone true && + test_must_fail git sparse-checkout add dir 2>err && + grep "existing sparse-checkout patterns do not use cone mode" err + ) +' + test_expect_success 'interaction with clone --no-checkout (unborn index)' ' git clone --no-checkout "file://$(pwd)/repo" clone_no_checkout && git -C clone_no_checkout sparse-checkout init --cone && @@ -165,12 +185,14 @@ test_expect_success 'set sparse-checkout using --stdin' ' ' test_expect_success 'add to sparse-checkout' ' - cat repo/.git/info/sparse-checkout >expect && + cat repo/.git/info/sparse-checkout >old && + test_when_finished cp old repo/.git/info/sparse-checkout && cat >add <<-\EOF && pattern1 /folder1/ pattern2 EOF + cat old >expect && cat add >>expect && git -C repo sparse-checkout add --stdin <add && git -C repo sparse-checkout list >actual && @@ -212,12 +234,27 @@ test_expect_success 'sparse-index enabled and disabled' ' git -C repo sparse-checkout init --cone --sparse-index && test_cmp_config -C repo true index.sparse && - test-tool -C repo read-cache --table >cache && - grep " tree " cache && - + git -C repo ls-files --sparse >sparse && git -C repo sparse-checkout disable && - test-tool -C repo read-cache --table >cache && - ! grep " tree " cache && + git -C repo ls-files --sparse >full && + + cat >expect <<-\EOF && + @@ -1,4 +1,7 @@ + a + -deep/ + -folder1/ + -folder2/ + +deep/a + +deep/deeper1/a + +deep/deeper1/deepest/a + +deep/deeper2/a + +folder1/a + +folder2/a + EOF + + diff -u sparse full | tail -n +3 >actual && + test_cmp expect actual && + git -C repo config --list >config && ! grep index.sparse config ) @@ -586,7 +623,7 @@ test_expect_success 'pattern-checks: contained glob characters' ' !/*/ something$c-else/ EOF - check_read_tree_errors repo "a" "disabling cone pattern matching" + check_read_tree_errors repo "a" "disabling cone pattern matching" || return 1 done ' @@ -708,4 +745,25 @@ test_expect_success 'cone mode clears ignored subdirectories' ' test_cmp expect out ' +test_expect_success 'malformed cone-mode patterns' ' + git -C repo sparse-checkout init --cone && + mkdir -p repo/foo/bar && + touch repo/foo/bar/x repo/foo/y && + cat >repo/.git/info/sparse-checkout <<-\EOF && + /* + !/*/ + /foo/ + !/foo/*/ + /foo/\*/ + EOF + + # Listing the patterns will notice the duplicate pattern and + # emit a warning. It will list the patterns directly instead + # of using the cone-mode translation to a set of directories. + git -C repo sparse-checkout list >actual 2>err && + test_cmp repo/.git/info/sparse-checkout actual && + grep "warning: your sparse-checkout file may have issues: pattern .* is repeated" err && + grep "warning: disabling cone pattern matching" err +' + test_done diff --git a/t/t1092-sparse-checkout-compatibility.sh b/t/t1092-sparse-checkout-compatibility.sh index 16fbd2c6db..4ba1617752 100755 --- a/t/t1092-sparse-checkout-compatibility.sh +++ b/t/t1092-sparse-checkout-compatibility.sh @@ -19,6 +19,8 @@ test_expect_success 'setup' ' mkdir folder1 folder2 deep x && mkdir deep/deeper1 deep/deeper2 deep/before deep/later && mkdir deep/deeper1/deepest && + mkdir deep/deeper1/deepest2 && + mkdir deep/deeper1/deepest3 && echo "after deeper1" >deep/e && echo "after deepest" >deep/deeper1/e && cp a folder1 && @@ -30,7 +32,9 @@ test_expect_success 'setup' ' cp a deep/deeper2 && cp a deep/later && cp a deep/deeper1/deepest && - cp -r deep/deeper1/deepest deep/deeper2 && + cp a deep/deeper1/deepest2 && + cp a deep/deeper1/deepest3 && + cp -r deep/deeper1/ deep/deeper2 && mkdir deep/deeper1/0 && mkdir deep/deeper1/0/0 && touch deep/deeper1/0/1 && @@ -126,6 +130,8 @@ test_expect_success 'setup' ' git checkout -b deepest base && echo "updated deepest" >deep/deeper1/deepest/a && + echo "updated deepest2" >deep/deeper1/deepest2/a && + echo "updated deepest3" >deep/deeper1/deepest3/a && git commit -a -m "update deepest" && git checkout -f base && @@ -200,45 +206,42 @@ test_sparse_unstaged () { test_expect_success 'sparse-index contents' ' init_repos && - test-tool -C sparse-index read-cache --table >cache && + git -C sparse-index ls-files --sparse --stage >cache && for dir in folder1 folder2 x do TREE=$(git -C sparse-index rev-parse HEAD:$dir) && - grep "040000 tree $TREE $dir/" cache \ + grep "040000 $TREE 0 $dir/" cache \ || return 1 done && git -C sparse-index sparse-checkout set folder1 && - test-tool -C sparse-index read-cache --table >cache && + git -C sparse-index ls-files --sparse --stage >cache && for dir in deep folder2 x do TREE=$(git -C sparse-index rev-parse HEAD:$dir) && - grep "040000 tree $TREE $dir/" cache \ + grep "040000 $TREE 0 $dir/" cache \ || return 1 done && git -C sparse-index sparse-checkout set deep/deeper1 && - test-tool -C sparse-index read-cache --table >cache && + git -C sparse-index ls-files --sparse --stage >cache && for dir in deep/deeper2 folder1 folder2 x do TREE=$(git -C sparse-index rev-parse HEAD:$dir) && - grep "040000 tree $TREE $dir/" cache \ + grep "040000 $TREE 0 $dir/" cache \ || return 1 done && - # Disabling the sparse-index removes tree entries with full ones + # Disabling the sparse-index replaces tree entries with full ones git -C sparse-index sparse-checkout init --no-sparse-index && - - test-tool -C sparse-index read-cache --table >cache && - ! grep "040000 tree" cache && - test_sparse_match test-tool read-cache --table + test_sparse_match git ls-files --stage --sparse ' test_expect_success 'expanded in-memory index matches full index' ' init_repos && - test_sparse_match test-tool read-cache --expand --table + test_sparse_match git ls-files --stage ' test_expect_success 'status with options' ' @@ -301,6 +304,14 @@ test_expect_success 'add, commit, checkout' ' test_all_match git checkout - ' +test_expect_success 'deep changes during checkout' ' + init_repos && + + test_sparse_match git sparse-checkout set deep/deeper1/deepest && + test_all_match git checkout deepest && + test_all_match git checkout base +' + test_expect_success 'add outside sparse cone' ' init_repos && @@ -401,7 +412,7 @@ test_expect_success 'checkout and reset --hard' ' test_all_match git reset --hard update-folder2 ' -test_expect_success 'diff --staged' ' +test_expect_success 'diff --cached' ' init_repos && write_script edit-contents <<-\EOF && @@ -410,10 +421,10 @@ test_expect_success 'diff --staged' ' run_on_all ../edit-contents && test_all_match git diff && - test_all_match git diff --staged && + test_all_match git diff --cached && test_all_match git add README.md && test_all_match git diff && - test_all_match git diff --staged + test_all_match git diff --cached ' # NEEDSWORK: sparse-checkout behaves differently from full-checkout when @@ -430,8 +441,8 @@ test_expect_success 'diff with renames and conflicts' ' test_all_match git checkout rename-base && test_all_match git checkout $branch -- . && test_all_match git status --porcelain=v2 && - test_all_match git diff --staged --no-renames && - test_all_match git diff --staged --find-renames || return 1 + test_all_match git diff --cached --no-renames && + test_all_match git diff --cached --find-renames || return 1 done ' @@ -450,8 +461,8 @@ test_expect_success 'diff with directory/file conflicts' ' test_all_match git checkout $branch && test_all_match git checkout rename-base -- . && test_all_match git status --porcelain=v2 && - test_all_match git diff --staged --no-renames && - test_all_match git diff --staged --find-renames || return 1 + test_all_match git diff --cached --no-renames && + test_all_match git diff --cached --find-renames || return 1 done ' @@ -472,43 +483,150 @@ test_expect_success 'log with pathspec outside sparse definition' ' test_expect_success 'blame with pathspec inside sparse definition' ' init_repos && - test_all_match git blame a && - test_all_match git blame deep/a && - test_all_match git blame deep/deeper1/a && - test_all_match git blame deep/deeper1/deepest/a + for file in a \ + deep/a \ + deep/deeper1/a \ + deep/deeper1/deepest/a + do + test_all_match git blame $file + done ' -# TODO: blame currently does not support blaming files outside of the -# sparse definition. It complains that the file doesn't exist locally. -test_expect_failure 'blame with pathspec outside sparse definition' ' +# Without a revision specified, blame will error if passed any file that +# is not present in the working directory (even if the file is tracked). +# Here we just verify that this is also true with sparse checkouts. +test_expect_success 'blame with pathspec outside sparse definition' ' init_repos && + test_sparse_match git sparse-checkout set && - test_all_match git blame folder1/a && - test_all_match git blame folder2/a && - test_all_match git blame deep/deeper2/a && - test_all_match git blame deep/deeper2/deepest/a + for file in a \ + deep/a \ + deep/deeper1/a \ + deep/deeper1/deepest/a + do + test_sparse_match test_must_fail git blame $file && + cat >expect <<-EOF && + fatal: Cannot lstat '"'"'$file'"'"': No such file or directory + EOF + # We compare sparse-checkout-err and sparse-index-err in + # `test_sparse_match`. Given we know they are the same, we + # only check the content of sparse-index-err here. + test_cmp expect sparse-index-err + done ' -# NEEDSWORK: a sparse-checkout behaves differently from a full checkout -# in this scenario, but it shouldn't. -test_expect_failure 'checkout and reset (mixed)' ' +test_expect_success 'checkout and reset (mixed)' ' init_repos && test_all_match git checkout -b reset-test update-deep && test_all_match git reset deepest && - test_all_match git reset update-folder1 && - test_all_match git reset update-folder2 + + # Because skip-worktree is preserved, resetting to update-folder1 + # will show worktree changes for folder1/a in full-checkout, but not + # in sparse-checkout or sparse-index. + git -C full-checkout reset update-folder1 >full-checkout-out && + test_sparse_match git reset update-folder1 && + grep "M folder1/a" full-checkout-out && + ! grep "M folder1/a" sparse-checkout-out && + run_on_sparse test_path_is_missing folder1 ' -# NEEDSWORK: a sparse-checkout behaves differently from a full checkout -# in this scenario, but it shouldn't. -test_expect_success 'checkout and reset (mixed) [sparse]' ' +test_expect_success 'checkout and reset (merge)' ' init_repos && - test_sparse_match git checkout -b reset-test update-deep && - test_sparse_match git reset deepest && - test_sparse_match git reset update-folder1 && - test_sparse_match git reset update-folder2 + write_script edit-contents <<-\EOF && + echo text >>$1 + EOF + + test_all_match git checkout -b reset-test update-deep && + run_on_all ../edit-contents a && + test_all_match git reset --merge deepest && + test_all_match git status --porcelain=v2 && + + test_all_match git reset --hard update-deep && + run_on_all ../edit-contents deep/a && + test_all_match test_must_fail git reset --merge deepest +' + +test_expect_success 'checkout and reset (keep)' ' + init_repos && + + write_script edit-contents <<-\EOF && + echo text >>$1 + EOF + + test_all_match git checkout -b reset-test update-deep && + run_on_all ../edit-contents a && + test_all_match git reset --keep deepest && + test_all_match git status --porcelain=v2 && + + test_all_match git reset --hard update-deep && + run_on_all ../edit-contents deep/a && + test_all_match test_must_fail git reset --keep deepest +' + +test_expect_success 'reset with pathspecs inside sparse definition' ' + init_repos && + + write_script edit-contents <<-\EOF && + echo text >>$1 + EOF + + test_all_match git checkout -b reset-test update-deep && + run_on_all ../edit-contents deep/a && + + test_all_match git reset base -- deep/a && + test_all_match git status --porcelain=v2 && + + test_all_match git reset base -- nonexistent-file && + test_all_match git status --porcelain=v2 && + + test_all_match git reset deepest -- deep && + test_all_match git status --porcelain=v2 +' + +# Although the working tree differs between full and sparse checkouts after +# reset, the state of the index is the same. +test_expect_success 'reset with pathspecs outside sparse definition' ' + init_repos && + test_all_match git checkout -b reset-test base && + + test_sparse_match git reset update-folder1 -- folder1 && + git -C full-checkout reset update-folder1 -- folder1 && + test_sparse_match git status --porcelain=v2 && + test_all_match git rev-parse HEAD:folder1 && + + test_sparse_match git reset update-folder2 -- folder2/a && + git -C full-checkout reset update-folder2 -- folder2/a && + test_sparse_match git status --porcelain=v2 && + test_all_match git rev-parse HEAD:folder2/a +' + +test_expect_success 'reset with wildcard pathspec' ' + init_repos && + + test_all_match git reset update-deep -- deep\* && + test_all_match git ls-files -s -- deep && + + test_all_match git reset deepest -- deep\*\*\* && + test_all_match git ls-files -s -- deep && + + # The following `git reset`s result in updating the index on files with + # `skip-worktree` enabled. To avoid failing due to discrepencies in reported + # "modified" files, `test_sparse_match` reset is performed separately from + # "full-checkout" reset, then the index contents of all repos are verified. + + test_sparse_match git reset update-folder1 -- \*/a && + git -C full-checkout reset update-folder1 -- \*/a && + test_all_match git ls-files -s -- deep/a folder1/a && + + test_sparse_match git reset update-folder2 -- folder\* && + git -C full-checkout reset update-folder2 -- folder\* && + test_all_match git ls-files -s -- folder10 folder1 folder2 && + + test_sparse_match git reset base -- folder1/\* && + git -C full-checkout reset base -- folder1/\* && + test_all_match git ls-files -s -- folder1 ' test_expect_success 'merge, cherry-pick, and rebase' ' @@ -680,20 +798,61 @@ test_expect_success 'submodule handling' ' # having a submodule prevents "modules" from collapse test_sparse_match git sparse-checkout set deep/deeper1 && - test-tool -C sparse-index read-cache --table >cache && - grep "100644 blob .* modules/a" cache && - grep "160000 commit $(git -C initial-repo rev-parse HEAD) modules/sub" cache + git -C sparse-index ls-files --sparse --stage >cache && + grep "100644 .* modules/a" cache && + grep "160000 $(git -C initial-repo rev-parse HEAD) 0 modules/sub" cache ' +# When working with a sparse index, some commands will need to expand the +# index to operate properly. If those commands also write the index back +# to disk, they need to convert the index to sparse before writing. +# This test verifies that both of these events are logged in trace2 logs. test_expect_success 'sparse-index is expanded and converted back' ' init_repos && - GIT_TRACE2_EVENT="$(pwd)/trace2.txt" GIT_TRACE2_EVENT_NESTING=10 \ - git -C sparse-index -c core.fsmonitor="" reset --hard && + GIT_TRACE2_EVENT="$(pwd)/trace2.txt" \ + git -C sparse-index reset -- folder1/a && test_region index convert_to_sparse trace2.txt && + test_region index ensure_full_index trace2.txt && + + # ls-files expands on read, but does not write. + rm trace2.txt && + GIT_TRACE2_EVENT="$(pwd)/trace2.txt" GIT_TRACE2_EVENT_NESTING=10 \ + git -C sparse-index ls-files && test_region index ensure_full_index trace2.txt ' +test_expect_success 'index.sparse disabled inline uses full index' ' + init_repos && + + # When index.sparse is disabled inline with `git status`, the + # index is expanded at the beginning of the execution then never + # converted back to sparse. It is then written to disk as a full index. + rm -f trace2.txt && + GIT_TRACE2_EVENT="$(pwd)/trace2.txt" GIT_TRACE2_EVENT_NESTING=10 \ + git -C sparse-index -c index.sparse=false status && + ! test_region index convert_to_sparse trace2.txt && + test_region index ensure_full_index trace2.txt && + + # Since index.sparse is set to true at a repo level, the index + # is converted from full to sparse when read, then never expanded + # over the course of `git status`. It is written to disk as a sparse + # index. + rm -f trace2.txt && + GIT_TRACE2_EVENT="$(pwd)/trace2.txt" GIT_TRACE2_EVENT_NESTING=10 \ + git -C sparse-index status && + test_region index convert_to_sparse trace2.txt && + ! test_region index ensure_full_index trace2.txt && + + # Now that the index has been written to disk as sparse, it is not + # converted to sparse (or expanded to full) when read by `git status`. + rm -f trace2.txt && + GIT_TRACE2_EVENT="$(pwd)/trace2.txt" GIT_TRACE2_EVENT_NESTING=10 \ + git -C sparse-index status && + ! test_region index convert_to_sparse trace2.txt && + ! test_region index ensure_full_index trace2.txt +' + ensure_not_expanded () { rm -f trace2.txt && echo >>sparse-index/untracked.txt && @@ -702,10 +861,10 @@ ensure_not_expanded () { then shift && test_must_fail env \ - GIT_TRACE2_EVENT="$(pwd)/trace2.txt" GIT_TRACE2_EVENT_NESTING=10 \ + GIT_TRACE2_EVENT="$(pwd)/trace2.txt" \ git -C sparse-index "$@" || return 1 else - GIT_TRACE2_EVENT="$(pwd)/trace2.txt" GIT_TRACE2_EVENT_NESTING=10 \ + GIT_TRACE2_EVENT="$(pwd)/trace2.txt" \ git -C sparse-index "$@" || return 1 fi && test_region ! index ensure_full_index trace2.txt @@ -715,6 +874,7 @@ test_expect_success 'sparse-index is not expanded' ' init_repos && ensure_not_expanded status && + ensure_not_expanded ls-files --sparse && ensure_not_expanded commit --allow-empty -m empty && echo >>sparse-index/a && ensure_not_expanded commit -a -m a && @@ -726,9 +886,9 @@ test_expect_success 'sparse-index is not expanded' ' ensure_not_expanded checkout - && ensure_not_expanded switch rename-out-to-out && ensure_not_expanded switch - && - git -C sparse-index reset --hard && + ensure_not_expanded reset --hard && ensure_not_expanded checkout rename-out-to-out -- deep/deeper1 && - git -C sparse-index reset --hard && + ensure_not_expanded reset --hard && ensure_not_expanded restore -s rename-out-to-out -- deep/deeper1 && echo >>sparse-index/README.md && @@ -738,6 +898,34 @@ test_expect_success 'sparse-index is not expanded' ' echo >>sparse-index/untracked.txt && ensure_not_expanded add . && + for ref in update-deep update-folder1 update-folder2 update-deep + do + echo >>sparse-index/README.md && + ensure_not_expanded reset --hard $ref || return 1 + done && + + ensure_not_expanded reset --mixed base && + ensure_not_expanded reset --hard update-deep && + ensure_not_expanded reset --keep base && + ensure_not_expanded reset --merge update-deep && + ensure_not_expanded reset --hard && + + ensure_not_expanded reset base -- deep/a && + ensure_not_expanded reset base -- nonexistent-file && + ensure_not_expanded reset deepest -- deep && + + # Although folder1 is outside the sparse definition, it exists as a + # directory entry in the index, so the pathspec will not force the + # index to be expanded. + ensure_not_expanded reset deepest -- folder1 && + ensure_not_expanded reset deepest -- folder1/ && + + # Wildcard identifies only in-cone files, no index expansion + ensure_not_expanded reset deepest -- deep/\* && + + # Wildcard identifies only full sparse directories, no index expansion + ensure_not_expanded reset deepest -- folder\* && + ensure_not_expanded checkout -f update-deep && test_config -C sparse-index pull.twohead ort && ( @@ -767,6 +955,158 @@ test_expect_success 'sparse-index is not expanded: merge conflict in cone' ' ) ' +test_expect_success 'sparse index is not expanded: diff' ' + init_repos && + + write_script edit-contents <<-\EOF && + echo text >>$1 + EOF + + # Add file within cone + test_sparse_match git sparse-checkout set deep && + run_on_all ../edit-contents deep/testfile && + test_all_match git add deep/testfile && + run_on_all ../edit-contents deep/testfile && + + test_all_match git diff && + test_all_match git diff --cached && + ensure_not_expanded diff && + ensure_not_expanded diff --cached && + + # Add file outside cone + test_all_match git reset --hard && + run_on_all mkdir newdirectory && + run_on_all ../edit-contents newdirectory/testfile && + test_sparse_match git sparse-checkout set newdirectory && + test_all_match git add newdirectory/testfile && + run_on_all ../edit-contents newdirectory/testfile && + test_sparse_match git sparse-checkout set && + + test_all_match git diff && + test_all_match git diff --cached && + ensure_not_expanded diff && + ensure_not_expanded diff --cached && + + # Merge conflict outside cone + # The sparse checkout will report a warning that is not in the + # full checkout, so we use `run_on_all` instead of + # `test_all_match` + run_on_all git reset --hard && + test_all_match git checkout merge-left && + test_all_match test_must_fail git merge merge-right && + + test_all_match git diff && + test_all_match git diff --cached && + ensure_not_expanded diff && + ensure_not_expanded diff --cached +' + +test_expect_success 'sparse index is not expanded: blame' ' + init_repos && + + for file in a \ + deep/a \ + deep/deeper1/a \ + deep/deeper1/deepest/a + do + ensure_not_expanded blame $file + done +' + +test_expect_success 'sparse index is not expanded: fetch/pull' ' + init_repos && + + git -C sparse-index remote add full "file://$(pwd)/full-checkout" && + ensure_not_expanded fetch full && + git -C full-checkout commit --allow-empty -m "for pull merge" && + git -C sparse-index commit --allow-empty -m "for pull merge" && + ensure_not_expanded pull full base +' + +test_expect_success 'ls-files' ' + init_repos && + + # Use a smaller sparse-checkout for reduced output + test_sparse_match git sparse-checkout set && + + # Behavior agrees by default. Sparse index is expanded. + test_all_match git ls-files && + + # With --sparse, the sparse index data changes behavior. + git -C sparse-index ls-files --sparse >actual && + + cat >expect <<-\EOF && + a + deep/ + e + folder1- + folder1.x + folder1/ + folder10 + folder2/ + g + x/ + z + EOF + + test_cmp expect actual && + + # With --sparse and no sparse index, nothing changes. + git -C sparse-checkout ls-files >dense && + git -C sparse-checkout ls-files --sparse >sparse && + test_cmp dense sparse && + + # Set up a strange condition of having a file edit + # outside of the sparse-checkout cone. This is just + # to verify that sparse-checkout and sparse-index + # behave the same in this case. + write_script edit-content <<-\EOF && + mkdir folder1 && + echo content >>folder1/a + EOF + run_on_sparse ../edit-content && + + # ls-files does not currently notice modified files whose + # cache entries are marked SKIP_WORKTREE. This may change + # in the future, but here we test that sparse index does + # not accidentally create a change of behavior. + test_sparse_match git ls-files --modified && + test_must_be_empty sparse-checkout-out && + test_must_be_empty sparse-index-out && + + git -C sparse-index ls-files --sparse --modified >sparse-index-out && + test_must_be_empty sparse-index-out && + + # Add folder1 to the sparse-checkout cone and + # check that ls-files shows the expanded files. + test_sparse_match git sparse-checkout add folder1 && + test_sparse_match git ls-files --modified && + + test_all_match git ls-files && + git -C sparse-index ls-files --sparse >actual && + + cat >expect <<-\EOF && + a + deep/ + e + folder1- + folder1.x + folder1/0/0/0 + folder1/0/1 + folder1/a + folder10 + folder2/ + g + x/ + z + EOF + + test_cmp expect actual && + + # Double-check index expansion is avoided + ensure_not_expanded ls-files --sparse +' + # NEEDSWORK: a sparse-checkout behaves differently from a full checkout # in this scenario, but it shouldn't. test_expect_success 'reset mixed and checkout orphan' ' @@ -782,13 +1122,13 @@ test_expect_success 'reset mixed and checkout orphan' ' # the sparse checkouts skip "adding" the other side of # the conflict. test_sparse_match git reset --mixed HEAD~1 && - test_sparse_match test-tool read-cache --table --expand && + test_sparse_match git ls-files --stage && test_sparse_match git status --porcelain=v2 && # At this point, sparse-checkouts behave differently # from the full-checkout. test_sparse_match git checkout --orphan new-branch && - test_sparse_match test-tool read-cache --table --expand && + test_sparse_match git ls-files --stage && test_sparse_match git status --porcelain=v2 ' diff --git a/t/t1300-config.sh b/t/t1300-config.sh index 9ff46f3b04..78359f1f4a 100755 --- a/t/t1300-config.sh +++ b/t/t1300-config.sh @@ -8,6 +8,7 @@ test_description='Test git config in different settings' 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 'clear default config' ' @@ -717,8 +718,8 @@ test_expect_success bool ' rm -f result && for i in 1 2 3 4 do - git config --bool --get bool.true$i >>result - git config --bool --get bool.false$i >>result + git config --bool --get bool.true$i >>result && + git config --bool --get bool.false$i >>result || return 1 done && test_cmp expect result' @@ -901,7 +902,7 @@ test_expect_success 'get --expiry-date' ' EOF : "work around heredoc parsing bug fixed in dash 0.5.7 (in ec2c84d)" && { - echo "$rel_out $(git config --expiry-date date.valid1)" + echo "$rel_out $(git config --expiry-date date.valid1)" && git config --expiry-date date.valid2 && git config --expiry-date date.valid3 && git config --expiry-date date.valid4 && diff --git a/t/t1303-wacky-config.sh b/t/t1303-wacky-config.sh index 0000e664e7..0506f3d6bb 100755 --- a/t/t1303-wacky-config.sh +++ b/t/t1303-wacky-config.sh @@ -1,6 +1,8 @@ #!/bin/sh test_description='Test wacky input to git config' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh # Leaving off the newline is intentional! diff --git a/t/t1307-config-blob.sh b/t/t1307-config-blob.sh index 930dce06f0..0a7099d6f5 100755 --- a/t/t1307-config-blob.sh +++ b/t/t1307-config-blob.sh @@ -1,6 +1,8 @@ #!/bin/sh test_description='support for reading config from a blob' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success 'create config blob' ' diff --git a/t/t1308-config-set.sh b/t/t1308-config-set.sh index 88b119a0a3..b38e158d3b 100755 --- a/t/t1308-config-set.sh +++ b/t/t1308-config-set.sh @@ -2,6 +2,7 @@ test_description='Test git config-set API in different settings' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh # 'check_config get_* section.key value' verifies that the entry for diff --git a/t/t1309-early-config.sh b/t/t1309-early-config.sh index b4a9158307..537435b90a 100755 --- a/t/t1309-early-config.sh +++ b/t/t1309-early-config.sh @@ -2,6 +2,7 @@ test_description='Test read_early_config()' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success 'read early config' ' diff --git a/t/t1310-config-default.sh b/t/t1310-config-default.sh index 6049d91708..09b10c144b 100755 --- a/t/t1310-config-default.sh +++ b/t/t1310-config-default.sh @@ -2,6 +2,7 @@ test_description='Test git config in different settings (with --default)' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success 'uses --default when entry missing' ' diff --git a/t/t1400-update-ref.sh b/t/t1400-update-ref.sh index 0d4f73acaa..cf58cf025c 100755 --- a/t/t1400-update-ref.sh +++ b/t/t1400-update-ref.sh @@ -4,9 +4,6 @@ # test_description='Test git update-ref and basic ref logging' -GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main -export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME - . ./test-lib.sh Z=$ZERO_OID @@ -321,8 +318,9 @@ $A $B $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150260 +0000 Switch $B $A $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150860 +0000 EOF test_expect_success "verifying $m's log (logged by touch)" ' - test_when_finished "rm -rf .git/$m .git/logs expect" && - test_cmp expect .git/logs/$m + test_when_finished "git update-ref -d $m && rm -rf .git/logs actual expect" && + test-tool ref-store main for-each-reflog-ent $m >actual && + test_cmp actual expect ' test_expect_success "create $m (logged by config)" ' @@ -350,8 +348,9 @@ $A $B $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150380 +0000 Switch $B $A $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150980 +0000 EOF test_expect_success "verifying $m's log (logged by config)" ' - test_when_finished "rm -f .git/$m .git/logs/$m expect" && - test_cmp expect .git/logs/$m + test_when_finished "git update-ref -d $m && rm -rf .git/logs actual expect" && + test-tool ref-store main for-each-reflog-ent $m >actual && + test_cmp actual expect ' test_expect_success 'set up for querying the reflog' ' @@ -467,7 +466,8 @@ $h_OTHER $h_FIXED $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117151040 +0000 co $h_FIXED $h_MERGED $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117151100 +0000 commit (merge): Merged initial commit and a later commit. EOF test_expect_success 'git commit logged updates' ' - test_cmp expect .git/logs/$m + test-tool ref-store main for-each-reflog-ent $m >actual && + test_cmp expect actual ' unset h_TEST h_OTHER h_FIXED h_MERGED @@ -1368,7 +1368,7 @@ test_expect_success ULIMIT_FILE_DESCRIPTORS 'large transaction creating branches ( for i in $(test_seq 33) do - echo "create refs/heads/$i HEAD" + echo "create refs/heads/$i HEAD" || exit 1 done >large_input && run_with_limited_open_files git update-ref --stdin <large_input && git rev-parse --verify -q refs/heads/33 @@ -1379,7 +1379,7 @@ test_expect_success ULIMIT_FILE_DESCRIPTORS 'large transaction deleting branches ( for i in $(test_seq 33) do - echo "delete refs/heads/$i HEAD" + echo "delete refs/heads/$i HEAD" || exit 1 done >large_input && run_with_limited_open_files git update-ref --stdin <large_input && test_must_fail git rev-parse --verify -q refs/heads/33 diff --git a/t/t1403-show-ref.sh b/t/t1403-show-ref.sh index 17d3cc1405..9252a581ab 100755 --- a/t/t1403-show-ref.sh +++ b/t/t1403-show-ref.sh @@ -4,6 +4,7 @@ test_description='show-ref' 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 ' @@ -78,7 +79,7 @@ test_expect_success 'show-ref --verify -q' ' test_expect_success 'show-ref -d' ' { echo $(git rev-parse refs/tags/A) refs/tags/A && - echo $(git rev-parse refs/tags/A^0) "refs/tags/A^{}" + echo $(git rev-parse refs/tags/A^0) "refs/tags/A^{}" && echo $(git rev-parse refs/tags/C) refs/tags/C } >expect && git show-ref -d A C >actual && @@ -123,14 +124,14 @@ test_expect_success 'show-ref -d' ' test_expect_success 'show-ref --heads, --tags, --head, pattern' ' for branch in B main side do - echo $(git rev-parse refs/heads/$branch) refs/heads/$branch + echo $(git rev-parse refs/heads/$branch) refs/heads/$branch || return 1 done >expect.heads && git show-ref --heads >actual && test_cmp expect.heads actual && for tag in A B C do - echo $(git rev-parse refs/tags/$tag) refs/tags/$tag + echo $(git rev-parse refs/tags/$tag) refs/tags/$tag || return 1 done >expect.tags && git show-ref --tags >actual && test_cmp expect.tags actual && @@ -148,7 +149,7 @@ test_expect_success 'show-ref --heads, --tags, --head, pattern' ' { echo $(git rev-parse HEAD) HEAD && - echo $(git rev-parse refs/heads/B) refs/heads/B + echo $(git rev-parse refs/heads/B) refs/heads/B && echo $(git rev-parse refs/tags/B) refs/tags/B } >expect && git show-ref --head B >actual && @@ -156,8 +157,8 @@ test_expect_success 'show-ref --heads, --tags, --head, pattern' ' { echo $(git rev-parse HEAD) HEAD && - echo $(git rev-parse refs/heads/B) refs/heads/B - echo $(git rev-parse refs/tags/B) refs/tags/B + echo $(git rev-parse refs/heads/B) refs/heads/B && + echo $(git rev-parse refs/tags/B) refs/tags/B && echo $(git rev-parse refs/tags/B^0) "refs/tags/B^{}" } >expect && git show-ref --head -d B >actual && diff --git a/t/t1404-update-ref-errors.sh b/t/t1404-update-ref-errors.sh index b729c1f480..13c2b43bba 100755 --- a/t/t1404-update-ref-errors.sh +++ b/t/t1404-update-ref-errors.sh @@ -261,69 +261,69 @@ test_expect_success REFFILES 'empty directory should not fool 1-arg delete' ' git update-ref --stdin ' -test_expect_success 'D/F conflict prevents add long + delete short' ' +test_expect_success REFFILES 'D/F conflict prevents add long + delete short' ' df_test refs/df-al-ds --add-del foo/bar foo ' -test_expect_success 'D/F conflict prevents add short + delete long' ' +test_expect_success REFFILES 'D/F conflict prevents add short + delete long' ' df_test refs/df-as-dl --add-del foo foo/bar ' -test_expect_success 'D/F conflict prevents delete long + add short' ' +test_expect_success REFFILES 'D/F conflict prevents delete long + add short' ' df_test refs/df-dl-as --del-add foo/bar foo ' -test_expect_success 'D/F conflict prevents delete short + add long' ' +test_expect_success REFFILES 'D/F conflict prevents delete short + add long' ' df_test refs/df-ds-al --del-add foo foo/bar ' -test_expect_success 'D/F conflict prevents add long + delete short packed' ' +test_expect_success REFFILES 'D/F conflict prevents add long + delete short packed' ' df_test refs/df-al-dsp --pack --add-del foo/bar foo ' -test_expect_success 'D/F conflict prevents add short + delete long packed' ' +test_expect_success REFFILES 'D/F conflict prevents add short + delete long packed' ' df_test refs/df-as-dlp --pack --add-del foo foo/bar ' -test_expect_success 'D/F conflict prevents delete long packed + add short' ' +test_expect_success REFFILES 'D/F conflict prevents delete long packed + add short' ' df_test refs/df-dlp-as --pack --del-add foo/bar foo ' -test_expect_success 'D/F conflict prevents delete short packed + add long' ' +test_expect_success REFFILES 'D/F conflict prevents delete short packed + add long' ' df_test refs/df-dsp-al --pack --del-add foo foo/bar ' # Try some combinations involving symbolic refs... -test_expect_success 'D/F conflict prevents indirect add long + delete short' ' +test_expect_success REFFILES 'D/F conflict prevents indirect add long + delete short' ' df_test refs/df-ial-ds --sym-add --add-del foo/bar foo ' -test_expect_success 'D/F conflict prevents indirect add long + indirect delete short' ' +test_expect_success REFFILES 'D/F conflict prevents indirect add long + indirect delete short' ' df_test refs/df-ial-ids --sym-add --sym-del --add-del foo/bar foo ' -test_expect_success 'D/F conflict prevents indirect add short + indirect delete long' ' +test_expect_success REFFILES 'D/F conflict prevents indirect add short + indirect delete long' ' df_test refs/df-ias-idl --sym-add --sym-del --add-del foo foo/bar ' -test_expect_success 'D/F conflict prevents indirect delete long + indirect add short' ' +test_expect_success REFFILES 'D/F conflict prevents indirect delete long + indirect add short' ' df_test refs/df-idl-ias --sym-add --sym-del --del-add foo/bar foo ' -test_expect_success 'D/F conflict prevents indirect add long + delete short packed' ' +test_expect_success REFFILES 'D/F conflict prevents indirect add long + delete short packed' ' df_test refs/df-ial-dsp --sym-add --pack --add-del foo/bar foo ' -test_expect_success 'D/F conflict prevents indirect add long + indirect delete short packed' ' +test_expect_success REFFILES 'D/F conflict prevents indirect add long + indirect delete short packed' ' df_test refs/df-ial-idsp --sym-add --sym-del --pack --add-del foo/bar foo ' -test_expect_success 'D/F conflict prevents add long + indirect delete short packed' ' +test_expect_success REFFILES 'D/F conflict prevents add long + indirect delete short packed' ' df_test refs/df-al-idsp --sym-del --pack --add-del foo/bar foo ' -test_expect_success 'D/F conflict prevents indirect delete long packed + indirect add short' ' +test_expect_success REFFILES 'D/F conflict prevents indirect delete long packed + indirect add short' ' df_test refs/df-idlp-ias --sym-add --sym-del --pack --del-add foo/bar foo ' diff --git a/t/t1405-main-ref-store.sh b/t/t1405-main-ref-store.sh index 49718b7ea7..1a3ee8845d 100755 --- a/t/t1405-main-ref-store.sh +++ b/t/t1405-main-ref-store.sh @@ -17,8 +17,7 @@ test_expect_success 'setup' ' test_expect_success REFFILES 'pack_refs(PACK_REFS_ALL | PACK_REFS_PRUNE)' ' N=`find .git/refs -type f | wc -l` && test "$N" != 0 && - ALL_OR_PRUNE_FLAG=3 && - $RUN pack-refs ${ALL_OR_PRUNE_FLAG} && + $RUN pack-refs PACK_REFS_PRUNE,PACK_REFS_ALL && N=`find .git/refs -type f` && test -z "$N" ' @@ -35,8 +34,7 @@ test_expect_success 'delete_refs(FOO, refs/tags/new-tag)' ' git rev-parse FOO -- && git rev-parse refs/tags/new-tag -- && m=$(git rev-parse main) && - REF_NO_DEREF=1 && - $RUN delete-refs $REF_NO_DEREF nothing FOO refs/tags/new-tag && + $RUN delete-refs REF_NO_DEREF nothing FOO refs/tags/new-tag && test_must_fail git rev-parse --symbolic-full-name FOO && test_must_fail git rev-parse FOO -- && test_must_fail git rev-parse refs/tags/new-tag -- @@ -89,13 +87,13 @@ test_expect_success 'for_each_reflog()' ' test_expect_success 'for_each_reflog_ent()' ' $RUN for-each-reflog-ent HEAD >actual && head -n1 actual | grep one && - tail -n2 actual | head -n1 | grep recreate-main + tail -n1 actual | grep recreate-main ' test_expect_success 'for_each_reflog_ent_reverse()' ' $RUN for-each-reflog-ent-reverse HEAD >actual && head -n1 actual | grep recreate-main && - tail -n2 actual | head -n1 | grep one + tail -n1 actual | grep one ' test_expect_success 'reflog_exists(HEAD)' ' @@ -108,7 +106,7 @@ test_expect_success 'delete_reflog(HEAD)' ' ' test_expect_success 'create-reflog(HEAD)' ' - $RUN create-reflog HEAD 1 && + $RUN create-reflog HEAD && git reflog exists HEAD ' diff --git a/t/t1406-submodule-ref-store.sh b/t/t1406-submodule-ref-store.sh index 0a87058971..e6a7f7334b 100755 --- a/t/t1406-submodule-ref-store.sh +++ b/t/t1406-submodule-ref-store.sh @@ -5,6 +5,7 @@ test_description='test submodule ref store api' GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh RUN="test-tool ref-store submodule:sub" @@ -74,13 +75,13 @@ test_expect_success 'for_each_reflog()' ' test_expect_success 'for_each_reflog_ent()' ' $RUN for-each-reflog-ent HEAD >actual && head -n1 actual | grep first && - tail -n2 actual | head -n1 | grep main.to.new + tail -n1 actual | grep main.to.new ' test_expect_success 'for_each_reflog_ent_reverse()' ' $RUN for-each-reflog-ent-reverse HEAD >actual && head -n1 actual | grep main.to.new && - tail -n2 actual | head -n1 | grep first + tail -n1 actual | grep first ' test_expect_success 'reflog_exists(HEAD)' ' @@ -92,7 +93,7 @@ test_expect_success 'delete_reflog() not allowed' ' ' test_expect_success 'create-reflog() not allowed' ' - test_must_fail $RUN create-reflog HEAD 1 + test_must_fail $RUN create-reflog HEAD ' test_done diff --git a/t/t1410-reflog.sh b/t/t1410-reflog.sh index d42f067ff8..d7ddf7612d 100755 --- a/t/t1410-reflog.sh +++ b/t/t1410-reflog.sh @@ -349,12 +349,12 @@ test_expect_success SHA1 'parsing reverse reflogs at BUFSIZ boundaries' ' printf "$zf%02d $zf%02d %s\t" $i $(($i+1)) "$ident" && if test $i = 75; then for j in $(test_seq 1 89); do - printf X + printf X || return 1 done else printf X fi && - printf "\n" + printf "\n" || return 1 done >.git/logs/refs/heads/reflogskip && git rev-parse reflogskip@{73} >actual && echo ${zf}03 >expect && diff --git a/t/t1417-reflog-updateref.sh b/t/t1417-reflog-updateref.sh new file mode 100755 index 0000000000..14f13b57c6 --- /dev/null +++ b/t/t1417-reflog-updateref.sh @@ -0,0 +1,65 @@ +#!/bin/sh + +test_description='git reflog --updateref' + +TEST_PASSES_SANITIZE_LEAK=true +. ./test-lib.sh + +test_expect_success 'setup' ' + git init -b main repo && + ( + cd repo && + + test_commit A && + test_commit B && + test_commit C && + + cp .git/logs/HEAD HEAD.old && + git reset --hard HEAD~ && + cp HEAD.old .git/logs/HEAD + ) +' + +test_reflog_updateref () { + exp=$1 + shift + args="$@" + + test_expect_success REFFILES "get '$exp' with '$args'" ' + test_when_finished "rm -rf copy" && + cp -R repo copy && + + ( + cd copy && + + $args && + git rev-parse $exp >expect && + git rev-parse HEAD >actual && + + test_cmp expect actual + ) + ' +} + +test_reflog_updateref B git reflog delete --updateref HEAD@{0} +test_reflog_updateref B git reflog delete --updateref HEAD@{1} +test_reflog_updateref C git reflog delete --updateref main@{0} +test_reflog_updateref B git reflog delete --updateref main@{1} +test_reflog_updateref B git reflog delete --updateref --rewrite HEAD@{0} +test_reflog_updateref B git reflog delete --updateref --rewrite HEAD@{1} +test_reflog_updateref C git reflog delete --updateref --rewrite main@{0} +test_reflog_updateref B git reflog delete --updateref --rewrite main@{1} +test_reflog_updateref B test_must_fail git reflog expire HEAD@{0} +test_reflog_updateref B test_must_fail git reflog expire HEAD@{1} +test_reflog_updateref B test_must_fail git reflog expire main@{0} +test_reflog_updateref B test_must_fail git reflog expire main@{1} +test_reflog_updateref B test_must_fail git reflog expire --updateref HEAD@{0} +test_reflog_updateref B test_must_fail git reflog expire --updateref HEAD@{1} +test_reflog_updateref B test_must_fail git reflog expire --updateref main@{0} +test_reflog_updateref B test_must_fail git reflog expire --updateref main@{1} +test_reflog_updateref B test_must_fail git reflog expire --updateref --rewrite HEAD@{0} +test_reflog_updateref B test_must_fail git reflog expire --updateref --rewrite HEAD@{1} +test_reflog_updateref B test_must_fail git reflog expire --updateref --rewrite main@{0} +test_reflog_updateref B test_must_fail git reflog expire --updateref --rewrite main@{1} + +test_done diff --git a/t/t1420-lost-found.sh b/t/t1420-lost-found.sh index dc9e402c55..dbe15a0be1 100755 --- a/t/t1420-lost-found.sh +++ b/t/t1420-lost-found.sh @@ -4,6 +4,8 @@ # test_description='Test fsck --lost-found' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success setup ' diff --git a/t/t1430-bad-ref-name.sh b/t/t1430-bad-ref-name.sh index 4c77cf89a6..ff1c967d55 100755 --- a/t/t1430-bad-ref-name.sh +++ b/t/t1430-bad-ref-name.sh @@ -9,7 +9,8 @@ TEST_PASSES_SANITIZE_LEAK=true test_expect_success setup ' test_commit one && - test_commit two + test_commit two && + main_sha1=$(git rev-parse refs/heads/main) ' test_expect_success 'fast-import: fail on invalid branch name ".badbranchname"' ' @@ -43,16 +44,16 @@ test_expect_success 'fast-import: fail on invalid branch name "bad[branch]name"' ' test_expect_success 'git branch shows badly named ref as warning' ' - cp .git/refs/heads/main .git/refs/heads/broken...ref && - test_when_finished "rm -f .git/refs/heads/broken...ref" && + test-tool ref-store main update-ref msg "refs/heads/broken...ref" $main_sha1 $ZERO_OID REF_SKIP_REFNAME_VERIFICATION && + test_when_finished "test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...ref" && git branch >output 2>error && test_i18ngrep -e "ignoring ref with broken name refs/heads/broken\.\.\.ref" error && ! grep -e "broken\.\.\.ref" output ' test_expect_success 'branch -d can delete badly named ref' ' - cp .git/refs/heads/main .git/refs/heads/broken...ref && - test_when_finished "rm -f .git/refs/heads/broken...ref" && + test-tool ref-store main update-ref msg "refs/heads/broken...ref" $main_sha1 $ZERO_OID REF_SKIP_REFNAME_VERIFICATION && + test_when_finished "test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...ref" && git branch -d broken...ref && git branch >output 2>error && ! grep -e "broken\.\.\.ref" error && @@ -60,8 +61,8 @@ test_expect_success 'branch -d can delete badly named ref' ' ' test_expect_success 'branch -D can delete badly named ref' ' - cp .git/refs/heads/main .git/refs/heads/broken...ref && - test_when_finished "rm -f .git/refs/heads/broken...ref" && + test-tool ref-store main update-ref msg "refs/heads/broken...ref" $main_sha1 $ZERO_OID REF_SKIP_REFNAME_VERIFICATION && + test_when_finished "test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...ref" && git branch -D broken...ref && git branch >output 2>error && ! grep -e "broken\.\.\.ref" error && @@ -90,7 +91,7 @@ test_expect_success 'branch -D cannot delete absolute path' ' ' test_expect_success 'git branch cannot create a badly named ref' ' - test_when_finished "rm -f .git/refs/heads/broken...ref" && + test_when_finished "test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...ref" && test_must_fail git branch broken...ref && git branch >output 2>error && ! grep -e "broken\.\.\.ref" error && @@ -98,7 +99,7 @@ test_expect_success 'git branch cannot create a badly named ref' ' ' test_expect_success 'branch -m cannot rename to a bad ref name' ' - test_when_finished "rm -f .git/refs/heads/broken...ref" && + test_when_finished "test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...ref" && test_might_fail git branch -D goodref && git branch goodref && test_must_fail git branch -m goodref broken...ref && @@ -109,8 +110,9 @@ test_expect_success 'branch -m cannot rename to a bad ref name' ' ' test_expect_failure 'branch -m can rename from a bad ref name' ' - cp .git/refs/heads/main .git/refs/heads/broken...ref && - test_when_finished "rm -f .git/refs/heads/broken...ref" && + test-tool ref-store main update-ref msg "refs/heads/broken...ref" $main_sha1 $ZERO_OID REF_SKIP_REFNAME_VERIFICATION && + + test_when_finished "test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...ref" && git branch -m broken...ref renamed && test_cmp_rev main renamed && git branch >output 2>error && @@ -119,7 +121,7 @@ test_expect_failure 'branch -m can rename from a bad ref name' ' ' test_expect_success 'push cannot create a badly named ref' ' - test_when_finished "rm -f .git/refs/heads/broken...ref" && + test_when_finished "test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...ref" && test_must_fail git push "file://$(pwd)" HEAD:refs/heads/broken...ref && git branch >output 2>error && ! grep -e "broken\.\.\.ref" error && @@ -139,7 +141,7 @@ test_expect_failure 'push --mirror can delete badly named ref' ' cd dest && test_commit two && git checkout --detach && - cp .git/refs/heads/main .git/refs/heads/broken...ref + test-tool ref-store main update-ref msg "refs/heads/broken...ref" $main_sha1 $ZERO_OID REF_SKIP_REFNAME_VERIFICATION ) && git -C src push --mirror "file://$top/dest" && git -C dest branch >output 2>error && @@ -148,11 +150,11 @@ test_expect_failure 'push --mirror can delete badly named ref' ' ' test_expect_success 'rev-parse skips symref pointing to broken name' ' - test_when_finished "rm -f .git/refs/heads/broken...ref" && + test_when_finished "test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...ref" && git branch shadow one && - cp .git/refs/heads/main .git/refs/heads/broken...ref && - printf "ref: refs/heads/broken...ref\n" >.git/refs/tags/shadow && - test_when_finished "rm -f .git/refs/tags/shadow" && + test-tool ref-store main update-ref msg "refs/heads/broken...ref" $main_sha1 $ZERO_OID REF_SKIP_REFNAME_VERIFICATION && + test-tool ref-store main create-symref refs/tags/shadow refs/heads/broken...ref msg && + test_when_finished "test-tool ref-store main delete-refs REF_NO_DEREF msg refs/tags/shadow" && git rev-parse --verify one >expect && git rev-parse --verify shadow >actual 2>err && test_cmp expect actual && @@ -160,12 +162,12 @@ test_expect_success 'rev-parse skips symref pointing to broken name' ' ' test_expect_success 'for-each-ref emits warnings for broken names' ' - cp .git/refs/heads/main .git/refs/heads/broken...ref && - test_when_finished "rm -f .git/refs/heads/broken...ref" && + test-tool ref-store main update-ref msg "refs/heads/broken...ref" $main_sha1 $ZERO_OID REF_SKIP_REFNAME_VERIFICATION && + test_when_finished "test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...ref" && printf "ref: refs/heads/broken...ref\n" >.git/refs/heads/badname && - test_when_finished "rm -f .git/refs/heads/badname" && + test_when_finished "test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/badname" && printf "ref: refs/heads/main\n" >.git/refs/heads/broken...symref && - test_when_finished "rm -f .git/refs/heads/broken...symref" && + test_when_finished "test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...symref" && git for-each-ref >output 2>error && ! grep -e "broken\.\.\.ref" output && ! grep -e "badname" output && @@ -176,8 +178,8 @@ test_expect_success 'for-each-ref emits warnings for broken names' ' ' test_expect_success 'update-ref -d can delete broken name' ' - cp .git/refs/heads/main .git/refs/heads/broken...ref && - test_when_finished "rm -f .git/refs/heads/broken...ref" && + test-tool ref-store main update-ref msg "refs/heads/broken...ref" $main_sha1 $ZERO_OID REF_SKIP_REFNAME_VERIFICATION && + test_when_finished "test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...ref" && git update-ref -d refs/heads/broken...ref >output 2>error && test_must_be_empty output && test_must_be_empty error && @@ -187,8 +189,8 @@ test_expect_success 'update-ref -d can delete broken name' ' ' test_expect_success 'branch -d can delete broken name' ' - cp .git/refs/heads/main .git/refs/heads/broken...ref && - test_when_finished "rm -f .git/refs/heads/broken...ref" && + test-tool ref-store main update-ref msg "refs/heads/broken...ref" $main_sha1 $ZERO_OID REF_SKIP_REFNAME_VERIFICATION && + test_when_finished "test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...ref" && git branch -d broken...ref >output 2>error && test_i18ngrep "Deleted branch broken...ref (was broken)" output && test_must_be_empty error && @@ -198,10 +200,11 @@ test_expect_success 'branch -d can delete broken name' ' ' test_expect_success 'update-ref --no-deref -d can delete symref to broken name' ' - cp .git/refs/heads/main .git/refs/heads/broken...ref && - test_when_finished "rm -f .git/refs/heads/broken...ref" && - printf "ref: refs/heads/broken...ref\n" >.git/refs/heads/badname && - test_when_finished "rm -f .git/refs/heads/badname" && + test-tool ref-store main update-ref msg "refs/heads/broken...ref" $main_sha1 $ZERO_OID REF_SKIP_REFNAME_VERIFICATION && + + test_when_finished "test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...ref" && + test-tool ref-store main create-symref refs/heads/badname refs/heads/broken...ref msg && + test_when_finished "test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/badname" && git update-ref --no-deref -d refs/heads/badname >output 2>error && test_path_is_missing .git/refs/heads/badname && test_must_be_empty output && @@ -209,10 +212,10 @@ test_expect_success 'update-ref --no-deref -d can delete symref to broken name' ' test_expect_success 'branch -d can delete symref to broken name' ' - cp .git/refs/heads/main .git/refs/heads/broken...ref && - test_when_finished "rm -f .git/refs/heads/broken...ref" && - printf "ref: refs/heads/broken...ref\n" >.git/refs/heads/badname && - test_when_finished "rm -f .git/refs/heads/badname" && + test-tool ref-store main update-ref msg "refs/heads/broken...ref" $main_sha1 $ZERO_OID REF_SKIP_REFNAME_VERIFICATION && + test_when_finished "test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...ref" && + test-tool ref-store main create-symref refs/heads/badname refs/heads/broken...ref msg && + test_when_finished "test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/badname" && git branch -d badname >output 2>error && test_path_is_missing .git/refs/heads/badname && test_i18ngrep "Deleted branch badname (was refs/heads/broken\.\.\.ref)" output && @@ -220,8 +223,8 @@ test_expect_success 'branch -d can delete symref to broken name' ' ' test_expect_success 'update-ref --no-deref -d can delete dangling symref to broken name' ' - printf "ref: refs/heads/broken...ref\n" >.git/refs/heads/badname && - test_when_finished "rm -f .git/refs/heads/badname" && + test-tool ref-store main create-symref refs/heads/badname refs/heads/broken...ref msg && + test_when_finished "test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/badname" && git update-ref --no-deref -d refs/heads/badname >output 2>error && test_path_is_missing .git/refs/heads/badname && test_must_be_empty output && @@ -229,8 +232,8 @@ test_expect_success 'update-ref --no-deref -d can delete dangling symref to brok ' test_expect_success 'branch -d can delete dangling symref to broken name' ' - printf "ref: refs/heads/broken...ref\n" >.git/refs/heads/badname && - test_when_finished "rm -f .git/refs/heads/badname" && + test-tool ref-store main create-symref refs/heads/badname refs/heads/broken...ref msg && + test_when_finished "test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/badname" && git branch -d badname >output 2>error && test_path_is_missing .git/refs/heads/badname && test_i18ngrep "Deleted branch badname (was refs/heads/broken\.\.\.ref)" output && @@ -238,10 +241,10 @@ test_expect_success 'branch -d can delete dangling symref to broken name' ' ' test_expect_success 'update-ref -d can delete broken name through symref' ' - cp .git/refs/heads/main .git/refs/heads/broken...ref && - test_when_finished "rm -f .git/refs/heads/broken...ref" && - printf "ref: refs/heads/broken...ref\n" >.git/refs/heads/badname && - test_when_finished "rm -f .git/refs/heads/badname" && + test-tool ref-store main update-ref msg "refs/heads/broken...ref" $main_sha1 $ZERO_OID REF_SKIP_REFNAME_VERIFICATION && + test_when_finished "test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...ref" && + test-tool ref-store main create-symref refs/heads/badname refs/heads/broken...ref msg && + test_when_finished "test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/badname" && git update-ref -d refs/heads/badname >output 2>error && test_path_is_missing .git/refs/heads/broken...ref && test_must_be_empty output && @@ -250,7 +253,7 @@ test_expect_success 'update-ref -d can delete broken name through symref' ' test_expect_success 'update-ref --no-deref -d can delete symref with broken name' ' printf "ref: refs/heads/main\n" >.git/refs/heads/broken...symref && - test_when_finished "rm -f .git/refs/heads/broken...symref" && + test_when_finished "test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...symref" && git update-ref --no-deref -d refs/heads/broken...symref >output 2>error && test_path_is_missing .git/refs/heads/broken...symref && test_must_be_empty output && @@ -259,7 +262,7 @@ test_expect_success 'update-ref --no-deref -d can delete symref with broken name test_expect_success 'branch -d can delete symref with broken name' ' printf "ref: refs/heads/main\n" >.git/refs/heads/broken...symref && - test_when_finished "rm -f .git/refs/heads/broken...symref" && + test_when_finished "test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...symref" && git branch -d broken...symref >output 2>error && test_path_is_missing .git/refs/heads/broken...symref && test_i18ngrep "Deleted branch broken...symref (was refs/heads/main)" output && @@ -268,7 +271,7 @@ test_expect_success 'branch -d can delete symref with broken name' ' test_expect_success 'update-ref --no-deref -d can delete dangling symref with broken name' ' printf "ref: refs/heads/idonotexist\n" >.git/refs/heads/broken...symref && - test_when_finished "rm -f .git/refs/heads/broken...symref" && + test_when_finished "test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...symref" && git update-ref --no-deref -d refs/heads/broken...symref >output 2>error && test_path_is_missing .git/refs/heads/broken...symref && test_must_be_empty output && @@ -277,7 +280,7 @@ test_expect_success 'update-ref --no-deref -d can delete dangling symref with br test_expect_success 'branch -d can delete dangling symref with broken name' ' printf "ref: refs/heads/idonotexist\n" >.git/refs/heads/broken...symref && - test_when_finished "rm -f .git/refs/heads/broken...symref" && + test_when_finished "test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...symref" && git branch -d broken...symref >output 2>error && test_path_is_missing .git/refs/heads/broken...symref && test_i18ngrep "Deleted branch broken...symref (was refs/heads/idonotexist)" output && diff --git a/t/t1503-rev-parse-verify.sh b/t/t1503-rev-parse-verify.sh index 40958615eb..94fe413ee3 100755 --- a/t/t1503-rev-parse-verify.sh +++ b/t/t1503-rev-parse-verify.sh @@ -9,6 +9,7 @@ exec </dev/null GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh add_line_into_file() diff --git a/t/t1505-rev-parse-last.sh b/t/t1505-rev-parse-last.sh index 2803ca9489..4a5758f08a 100755 --- a/t/t1505-rev-parse-last.sh +++ b/t/t1505-rev-parse-last.sh @@ -5,6 +5,7 @@ test_description='test @{-N} syntax' GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh diff --git a/t/t1506-rev-parse-diagnosis.sh b/t/t1506-rev-parse-diagnosis.sh index 65a154a8a2..18688cae17 100755 --- a/t/t1506-rev-parse-diagnosis.sh +++ b/t/t1506-rev-parse-diagnosis.sh @@ -7,6 +7,7 @@ exec </dev/null GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_did_you_mean () diff --git a/t/t1512-rev-parse-disambiguation.sh b/t/t1512-rev-parse-disambiguation.sh index 7891a6becf..b0119bf8bc 100755 --- a/t/t1512-rev-parse-disambiguation.sh +++ b/t/t1512-rev-parse-disambiguation.sh @@ -34,10 +34,7 @@ fi test_expect_success 'blob and tree' ' test_tick && ( - for i in 0 1 2 3 4 5 6 7 8 9 - do - echo $i - done && + test_write_lines 0 1 2 3 4 5 6 7 8 9 && echo && echo b1rwzyc3 ) >a0blgqsjc && @@ -204,10 +201,7 @@ test_expect_success 'more history' ' git checkout v1.0.0^0 && git mv a0blgqsjc f5518nwu && - for i in h62xsjeu j08bekfvt kg7xflhm - do - echo $i - done >>f5518nwu && + test_write_lines h62xsjeu j08bekfvt kg7xflhm >>f5518nwu && git add f5518nwu && test_tick && @@ -387,7 +381,7 @@ test_expect_success 'ambiguous commits are printed by type first, then hash orde do grep $type objects >$type.objects && sort $type.objects >$type.objects.sorted && - test_cmp $type.objects.sorted $type.objects + test_cmp $type.objects.sorted $type.objects || return 1 done ' diff --git a/t/t1513-rev-parse-prefix.sh b/t/t1513-rev-parse-prefix.sh index 5f437be8c9..ba43387bf1 100755 --- a/t/t1513-rev-parse-prefix.sh +++ b/t/t1513-rev-parse-prefix.sh @@ -5,6 +5,7 @@ test_description='Tests for rev-parse --prefix' 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/t1515-rev-parse-outside-repo.sh b/t/t1515-rev-parse-outside-repo.sh index 3ec2971ee5..cdb26a30d7 100755 --- a/t/t1515-rev-parse-outside-repo.sh +++ b/t/t1515-rev-parse-outside-repo.sh @@ -1,6 +1,8 @@ #!/bin/sh test_description='check that certain rev-parse options work outside repo' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success 'set up non-repo directory' ' diff --git a/t/t1600-index.sh b/t/t1600-index.sh index 46329c488b..010989f90e 100755 --- a/t/t1600-index.sh +++ b/t/t1600-index.sh @@ -2,6 +2,7 @@ test_description='index file specific tests' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh sane_unset GIT_TEST_SPLIT_INDEX diff --git a/t/t1700-split-index.sh b/t/t1700-split-index.sh index decd2527ed..b4ab166369 100755 --- a/t/t1700-split-index.sh +++ b/t/t1700-split-index.sh @@ -48,10 +48,10 @@ test_expect_success 'enable split index' ' # NEEDSWORK: Stop hard-coding checksums. if test "$indexversion" = "4" then - own=$(test_oid own_v4) + own=$(test_oid own_v4) && base=$(test_oid base_v4) else - own=$(test_oid own_v3) + own=$(test_oid own_v3) && base=$(test_oid base_v3) fi && diff --git a/t/t2000-conflict-when-checking-files-out.sh b/t/t2000-conflict-when-checking-files-out.sh index f18616ad2b..79fc97f1d7 100755 --- a/t/t2000-conflict-when-checking-files-out.sh +++ b/t/t2000-conflict-when-checking-files-out.sh @@ -21,6 +21,7 @@ test_description='git conflicts when checking files out test.' # path1 is occupied by a non-directory. With "-f" flag, it should remove # the conflicting paths and succeed. +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh show_files() { diff --git a/t/t2004-checkout-cache-temp.sh b/t/t2004-checkout-cache-temp.sh index 9bb503a975..b16d69ca4a 100755 --- a/t/t2004-checkout-cache-temp.sh +++ b/t/t2004-checkout-cache-temp.sh @@ -57,7 +57,7 @@ test_expect_success 'checkout all stage 0 to temporary files' ' test $(grep $f actual | cut "-d " -f2) = $f && p=$(grep $f actual | cut "-d " -f1) && test -f $p && - test $(cat $p) = tree1$f + test $(cat $p) = tree1$f || return 1 done ' @@ -85,7 +85,7 @@ test_expect_success 'checkout all stage 2 to temporary files' ' test $(grep $f actual | cut "-d " -f2) = $f && p=$(grep $f actual | cut "-d " -f1) && test -f $p && - test $(cat $p) = tree2$f + test $(cat $p) = tree2$f || return 1 done ' diff --git a/t/t2007-checkout-symlink.sh b/t/t2007-checkout-symlink.sh index 6f0b90ce12..bd9e9e7530 100755 --- a/t/t2007-checkout-symlink.sh +++ b/t/t2007-checkout-symlink.sh @@ -7,6 +7,7 @@ test_description='git checkout to switch between branches with symlink<->dir' 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/t2008-checkout-subdir.sh b/t/t2008-checkout-subdir.sh index eadb9434ae..8a518a44ea 100755 --- a/t/t2008-checkout-subdir.sh +++ b/t/t2008-checkout-subdir.sh @@ -4,6 +4,7 @@ test_description='git checkout from subdirectories' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success setup ' diff --git a/t/t2009-checkout-statinfo.sh b/t/t2009-checkout-statinfo.sh index b0540636ae..71195dd28f 100755 --- a/t/t2009-checkout-statinfo.sh +++ b/t/t2009-checkout-statinfo.sh @@ -5,6 +5,7 @@ test_description='checkout should leave clean stat info' 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/t2010-checkout-ambiguous.sh b/t/t2010-checkout-ambiguous.sh index 6e8757387d..9d4b37526a 100755 --- a/t/t2010-checkout-ambiguous.sh +++ b/t/t2010-checkout-ambiguous.sh @@ -5,6 +5,7 @@ test_description='checkout and pathspecs/refspecs ambiguities' 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/t2011-checkout-invalid-head.sh b/t/t2011-checkout-invalid-head.sh index e52022e152..d9997e7b6b 100755 --- a/t/t2011-checkout-invalid-head.sh +++ b/t/t2011-checkout-invalid-head.sh @@ -5,6 +5,7 @@ test_description='checkout switching away from an invalid branch' 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/t2012-checkout-last.sh b/t/t2012-checkout-last.sh index 0e7d47ab31..42601d5a31 100755 --- a/t/t2012-checkout-last.sh +++ b/t/t2012-checkout-last.sh @@ -49,14 +49,14 @@ test_expect_success '"checkout -" detaches again' ' test_expect_success 'more switches' ' for i in 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 do - git checkout -b branch$i + git checkout -b branch$i || return 1 done ' more_switches () { for i in 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 do - git checkout branch$i + git checkout branch$i || return 1 done } diff --git a/t/t2014-checkout-switch.sh b/t/t2014-checkout-switch.sh index ccfb147113..c138bdde4f 100755 --- a/t/t2014-checkout-switch.sh +++ b/t/t2014-checkout-switch.sh @@ -1,6 +1,8 @@ #!/bin/sh test_description='Peter MacMillan' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success setup ' diff --git a/t/t2017-checkout-orphan.sh b/t/t2017-checkout-orphan.sh index 88d6992a5e..f3371e2646 100755 --- a/t/t2017-checkout-orphan.sh +++ b/t/t2017-checkout-orphan.sh @@ -10,6 +10,7 @@ Main Tests for --orphan functionality.' GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh TEST_FILE=foo diff --git a/t/t2018-checkout-branch.sh b/t/t2018-checkout-branch.sh index 93be1c0eae..3e93506c04 100755 --- a/t/t2018-checkout-branch.sh +++ b/t/t2018-checkout-branch.sh @@ -148,7 +148,7 @@ test_expect_success 'checkout -b to an existing branch fails' ' test_expect_success 'checkout -b to @{-1} fails with the right branch name' ' git checkout branch1 && git checkout branch2 && - echo >expect "fatal: A branch named '\''branch1'\'' already exists." && + echo >expect "fatal: a branch named '\''branch1'\'' already exists" && test_must_fail git checkout -b @{-1} 2>actual && test_cmp expect actual ' diff --git a/t/t2019-checkout-ambiguous-ref.sh b/t/t2019-checkout-ambiguous-ref.sh index b99d5192a9..2c8c926b4d 100755 --- a/t/t2019-checkout-ambiguous-ref.sh +++ b/t/t2019-checkout-ambiguous-ref.sh @@ -1,6 +1,8 @@ #!/bin/sh test_description='checkout handling of ambiguous (branch/tag) refs' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success 'setup ambiguous refs' ' diff --git a/t/t2021-checkout-overwrite.sh b/t/t2021-checkout-overwrite.sh index 660132ff8d..713c3fa603 100755 --- a/t/t2021-checkout-overwrite.sh +++ b/t/t2021-checkout-overwrite.sh @@ -1,6 +1,8 @@ #!/bin/sh test_description='checkout must not overwrite an untracked objects' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success 'setup' ' diff --git a/t/t2022-checkout-paths.sh b/t/t2022-checkout-paths.sh index c49ba7f9bd..f1b709d58b 100755 --- a/t/t2022-checkout-paths.sh +++ b/t/t2022-checkout-paths.sh @@ -4,6 +4,7 @@ test_description='checkout $tree -- $paths' 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/t2026-checkout-pathspec-file.sh b/t/t2026-checkout-pathspec-file.sh index 43d31d7948..9db11f86dd 100755 --- a/t/t2026-checkout-pathspec-file.sh +++ b/t/t2026-checkout-pathspec-file.sh @@ -2,6 +2,7 @@ test_description='checkout --pathspec-from-file' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_tick diff --git a/t/t2100-update-cache-badpath.sh b/t/t2100-update-cache-badpath.sh index 2df3fdde8b..7915e7b821 100755 --- a/t/t2100-update-cache-badpath.sh +++ b/t/t2100-update-cache-badpath.sh @@ -22,6 +22,7 @@ and tries to git update-index --add the following: All of the attempts should fail. ' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh mkdir path2 path3 diff --git a/t/t2101-update-index-reupdate.sh b/t/t2101-update-index-reupdate.sh index 6c32d42c8c..e3c7acdbf9 100755 --- a/t/t2101-update-index-reupdate.sh +++ b/t/t2101-update-index-reupdate.sh @@ -6,6 +6,7 @@ test_description='git update-index --again test. ' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success 'update-index --add' ' diff --git a/t/t2102-update-index-symlinks.sh b/t/t2102-update-index-symlinks.sh index 22f2c730ae..c49cdfb6e5 100755 --- a/t/t2102-update-index-symlinks.sh +++ b/t/t2102-update-index-symlinks.sh @@ -8,6 +8,7 @@ test_description='git update-index on filesystem w/o symlinks test. This tests that git update-index keeps the symbolic link property even if a plain file is in the working tree if core.symlinks is false.' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success \ @@ -25,7 +26,7 @@ test_expect_success \ 'the index entry must still be a symbolic link' ' case "$(git ls-files --stage --cached symlink)" in 120000" "*symlink) echo pass;; -*) echo fail; git ls-files --stage --cached symlink; (exit 1);; +*) echo fail; git ls-files --stage --cached symlink; false;; esac' test_done diff --git a/t/t2103-update-index-ignore-missing.sh b/t/t2103-update-index-ignore-missing.sh index 0114f05228..e9451cd567 100755 --- a/t/t2103-update-index-ignore-missing.sh +++ b/t/t2103-update-index-ignore-missing.sh @@ -2,6 +2,7 @@ test_description='update-index with options' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success basics ' @@ -23,7 +24,7 @@ test_expect_success basics ' test_cmp expect actual && git update-index --add one two three && - for i in one three two; do echo $i; done >expect && + test_write_lines one three two >expect && git ls-files >actual && test_cmp expect actual && diff --git a/t/t2104-update-index-skip-worktree.sh b/t/t2104-update-index-skip-worktree.sh index 30666fc70d..b8686aabd3 100755 --- a/t/t2104-update-index-skip-worktree.sh +++ b/t/t2104-update-index-skip-worktree.sh @@ -5,6 +5,7 @@ test_description='skip-worktree bit test' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh sane_unset GIT_TEST_SPLIT_INDEX diff --git a/t/t2105-update-index-gitfile.sh b/t/t2105-update-index-gitfile.sh index a7f3d47aec..963ebe77eb 100755 --- a/t/t2105-update-index-gitfile.sh +++ b/t/t2105-update-index-gitfile.sh @@ -6,6 +6,7 @@ test_description='git update-index for gitlink to .git file. ' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success 'submodule with absolute .git file' ' diff --git a/t/t2106-update-index-assume-unchanged.sh b/t/t2106-update-index-assume-unchanged.sh index 2d450daf5c..d943ddf47e 100755 --- a/t/t2106-update-index-assume-unchanged.sh +++ b/t/t2106-update-index-assume-unchanged.sh @@ -3,6 +3,7 @@ test_description='git update-index --assume-unchanged test. ' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success 'setup' ' diff --git a/t/t2200-add-update.sh b/t/t2200-add-update.sh index 94c4cb0672..acd3650d3c 100755 --- a/t/t2200-add-update.sh +++ b/t/t2200-add-update.sh @@ -14,6 +14,7 @@ only the updates to dir/sub. Also tested are "git add -u" without limiting, and "git add -u" without contents changes, and other conditions' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success setup ' @@ -150,13 +151,13 @@ test_expect_success 'add -u resolves unmerged paths' ' { for path in path1 path2 do - echo "100644 $one 1 $path" - echo "100644 $two 2 $path" - echo "100644 $three 3 $path" - done - echo "100644 $one 1 path3" - echo "100644 $one 1 path4" - echo "100644 $one 3 path5" + echo "100644 $one 1 $path" && + echo "100644 $two 2 $path" && + echo "100644 $three 3 $path" || return 1 + done && + echo "100644 $one 1 path3" && + echo "100644 $one 1 path4" && + echo "100644 $one 3 path5" && echo "100644 $one 3 path6" } | git update-index --index-info && @@ -173,8 +174,8 @@ test_expect_success 'add -u resolves unmerged paths' ' git add -u && git ls-files -s path1 path2 path3 path4 path5 path6 >actual && { - echo "100644 $three 0 path1" - echo "100644 $two 0 path3" + echo "100644 $three 0 path1" && + echo "100644 $two 0 path3" && echo "100644 $two 0 path5" } >expect && test_cmp expect actual diff --git a/t/t2201-add-update-typechange.sh b/t/t2201-add-update-typechange.sh index a4eec0a346..dba62d69c6 100755 --- a/t/t2201-add-update-typechange.sh +++ b/t/t2201-add-update-typechange.sh @@ -2,6 +2,7 @@ test_description='more git add -u' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success setup ' @@ -97,17 +98,17 @@ test_expect_success modify ' " } >expect && { - cat expect - echo ":100644 160000 $_empty $ZERO_OID T yonk" + cat expect && + echo ":100644 160000 $_empty $ZERO_OID T yonk" && echo ":100644 000000 $_empty $ZERO_OID D zifmia" } >expect-files && { - cat expect + cat expect && echo ":000000 160000 $ZERO_OID $ZERO_OID A yonk" } >expect-index && { - echo "100644 $_empty 0 nitfol" - echo "160000 $yomin 0 yomin" + echo "100644 $_empty 0 nitfol" && + echo "160000 $yomin 0 yomin" && echo "160000 $yonk 0 yonk" } >expect-final ' diff --git a/t/t2202-add-addremove.sh b/t/t2202-add-addremove.sh index 9ee659098c..24c60bfd79 100755 --- a/t/t2202-add-addremove.sh +++ b/t/t2202-add-addremove.sh @@ -2,6 +2,7 @@ test_description='git add --all' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success setup ' diff --git a/t/t2203-add-intent.sh b/t/t2203-add-intent.sh index cf0175ad6e..db7ca55998 100755 --- a/t/t2203-add-intent.sh +++ b/t/t2203-add-intent.sh @@ -116,7 +116,7 @@ test_expect_success 'cache-tree does not ignore dir that has i-t-a entries' ' mkdir 2 && for f in 1 2/1 2/2 3 do - echo "$f" >"$f" + echo "$f" >"$f" || return 1 done && git add 1 2/2 3 && git add -N 2/1 && diff --git a/t/t2204-add-ignored.sh b/t/t2204-add-ignored.sh index 2e07365bbb..89424abccd 100755 --- a/t/t2204-add-ignored.sh +++ b/t/t2204-add-ignored.sh @@ -2,6 +2,7 @@ test_description='giving ignored paths to git add' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success setup ' diff --git a/t/t2401-worktree-prune.sh b/t/t2401-worktree-prune.sh index a615d3b483..3d28c7f06b 100755 --- a/t/t2401-worktree-prune.sh +++ b/t/t2401-worktree-prune.sh @@ -19,7 +19,7 @@ test_expect_success 'worktree prune on normal repo' ' test_expect_success 'prune files inside $GIT_DIR/worktrees' ' mkdir .git/worktrees && : >.git/worktrees/abc && - git worktree prune --verbose >actual && + git worktree prune --verbose 2>actual && cat >expect <<EOF && Removing worktrees/abc: not a valid directory EOF @@ -34,7 +34,7 @@ test_expect_success 'prune directories without gitdir' ' cat >expect <<EOF && Removing worktrees/def: gitdir file does not exist EOF - git worktree prune --verbose >actual && + git worktree prune --verbose 2>actual && test_cmp expect actual && ! test -d .git/worktrees/def && ! test -d .git/worktrees @@ -45,7 +45,7 @@ test_expect_success SANITY 'prune directories with unreadable gitdir' ' : >.git/worktrees/def/def && : >.git/worktrees/def/gitdir && chmod u-r .git/worktrees/def/gitdir && - git worktree prune --verbose >actual && + git worktree prune --verbose 2>actual && test_i18ngrep "Removing worktrees/def: unable to read gitdir file" actual && ! test -d .git/worktrees/def && ! test -d .git/worktrees @@ -55,7 +55,7 @@ test_expect_success 'prune directories with invalid gitdir' ' mkdir -p .git/worktrees/def/abc && : >.git/worktrees/def/def && : >.git/worktrees/def/gitdir && - git worktree prune --verbose >actual && + git worktree prune --verbose 2>actual && test_i18ngrep "Removing worktrees/def: invalid gitdir file" actual && ! test -d .git/worktrees/def && ! test -d .git/worktrees @@ -65,7 +65,7 @@ test_expect_success 'prune directories with gitdir pointing to nowhere' ' mkdir -p .git/worktrees/def/abc && : >.git/worktrees/def/def && echo "$(pwd)"/nowhere >.git/worktrees/def/gitdir && - git worktree prune --verbose >actual && + git worktree prune --verbose 2>actual && test_i18ngrep "Removing worktrees/def: gitdir file points to non-existent location" actual && ! test -d .git/worktrees/def && ! test -d .git/worktrees @@ -101,7 +101,7 @@ test_expect_success 'prune duplicate (linked/linked)' ' git worktree add --detach w2 && sed "s/w2/w1/" .git/worktrees/w2/gitdir >.git/worktrees/w2/gitdir.new && mv .git/worktrees/w2/gitdir.new .git/worktrees/w2/gitdir && - git worktree prune --verbose >actual && + git worktree prune --verbose 2>actual && test_i18ngrep "duplicate entry" actual && test -d .git/worktrees/w1 && ! test -d .git/worktrees/w2 @@ -114,7 +114,7 @@ test_expect_success 'prune duplicate (main/linked)' ' git -C repo worktree add --detach ../wt && rm -fr wt && mv repo wt && - git -C wt worktree prune --verbose >actual && + git -C wt worktree prune --verbose 2>actual && test_i18ngrep "duplicate entry" actual && ! test -d .git/worktrees/wt ' diff --git a/t/t2402-worktree-list.sh b/t/t2402-worktree-list.sh index 4012bd67b0..c8a5a0aac6 100755 --- a/t/t2402-worktree-list.sh +++ b/t/t2402-worktree-list.sh @@ -134,7 +134,7 @@ test_expect_success '"list" all worktrees with prunable consistent with "prune"' git worktree list >out && grep "/prunable *[0-9a-f].* prunable$" out && ! grep "/unprunable *[0-9a-f].* unprunable$" out && - git worktree prune --verbose >out && + git worktree prune --verbose 2>out && test_i18ngrep "^Removing worktrees/prunable" out && test_i18ngrep ! "^Removing worktrees/unprunable" out ' diff --git a/t/t2404-worktree-config.sh b/t/t2404-worktree-config.sh index 9536d10919..842937bfb9 100755 --- a/t/t2404-worktree-config.sh +++ b/t/t2404-worktree-config.sh @@ -2,6 +2,7 @@ test_description="config file in multi worktree" +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success 'setup' ' diff --git a/t/t2406-worktree-repair.sh b/t/t2406-worktree-repair.sh index f73741886b..5c44453e1c 100755 --- a/t/t2406-worktree-repair.sh +++ b/t/t2406-worktree-repair.sh @@ -45,9 +45,8 @@ test_corrupt_gitfile () { git worktree add --detach corrupt && git -C corrupt rev-parse --absolute-git-dir >expect && eval "$butcher" && - git -C "$repairdir" worktree repair >out 2>err && - test_i18ngrep "$problem" out && - test_must_be_empty err && + git -C "$repairdir" worktree repair 2>err && + test_i18ngrep "$problem" err && git -C corrupt rev-parse --absolute-git-dir >actual && test_cmp expect actual } @@ -130,10 +129,9 @@ test_expect_success 'repair broken gitdir' ' sed s,orig/\.git$,moved/.git, .git/worktrees/orig/gitdir >expect && rm .git/worktrees/orig/gitdir && mv orig moved && - git worktree repair moved >out 2>err && + git worktree repair moved 2>err && test_cmp expect .git/worktrees/orig/gitdir && - test_i18ngrep "gitdir unreadable" out && - test_must_be_empty err + test_i18ngrep "gitdir unreadable" err ' test_expect_success 'repair incorrect gitdir' ' @@ -141,10 +139,9 @@ test_expect_success 'repair incorrect gitdir' ' git worktree add --detach orig && sed s,orig/\.git$,moved/.git, .git/worktrees/orig/gitdir >expect && mv orig moved && - git worktree repair moved >out 2>err && + git worktree repair moved 2>err && test_cmp expect .git/worktrees/orig/gitdir && - test_i18ngrep "gitdir incorrect" out && - test_must_be_empty err + test_i18ngrep "gitdir incorrect" err ' test_expect_success 'repair gitdir (implicit) from linked worktree' ' @@ -152,10 +149,9 @@ test_expect_success 'repair gitdir (implicit) from linked worktree' ' git worktree add --detach orig && sed s,orig/\.git$,moved/.git, .git/worktrees/orig/gitdir >expect && mv orig moved && - git -C moved worktree repair >out 2>err && + git -C moved worktree repair 2>err && test_cmp expect .git/worktrees/orig/gitdir && - test_i18ngrep "gitdir incorrect" out && - test_must_be_empty err + test_i18ngrep "gitdir incorrect" err ' test_expect_success 'unable to repair gitdir (implicit) from main worktree' ' @@ -163,9 +159,8 @@ test_expect_success 'unable to repair gitdir (implicit) from main worktree' ' git worktree add --detach orig && cat .git/worktrees/orig/gitdir >expect && mv orig moved && - git worktree repair >out 2>err && + git worktree repair 2>err && test_cmp expect .git/worktrees/orig/gitdir && - test_must_be_empty out && test_must_be_empty err ' @@ -178,12 +173,11 @@ test_expect_success 'repair multiple gitdir files' ' sed s,orig2/\.git$,moved2/.git, .git/worktrees/orig2/gitdir >expect2 && mv orig1 moved1 && mv orig2 moved2 && - git worktree repair moved1 moved2 >out 2>err && + git worktree repair moved1 moved2 2>err && test_cmp expect1 .git/worktrees/orig1/gitdir && test_cmp expect2 .git/worktrees/orig2/gitdir && - test_i18ngrep "gitdir incorrect:.*orig1/gitdir$" out && - test_i18ngrep "gitdir incorrect:.*orig2/gitdir$" out && - test_must_be_empty err + test_i18ngrep "gitdir incorrect:.*orig1/gitdir$" err && + test_i18ngrep "gitdir incorrect:.*orig2/gitdir$" err ' test_expect_success 'repair moved main and linked worktrees' ' diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh new file mode 100755 index 0000000000..f6d8d7d03d --- /dev/null +++ b/t/t2501-cwd-empty.sh @@ -0,0 +1,277 @@ +#!/bin/sh + +test_description='Test handling of the current working directory becoming empty' + +. ./test-lib.sh + +test_expect_success setup ' + test_commit init && + + git branch fd_conflict && + + mkdir -p foo/bar && + test_commit foo/bar/baz && + + git revert HEAD && + git tag reverted && + + git checkout fd_conflict && + mkdir dirORfile && + test_commit dirORfile/foo && + + git rm -r dirORfile && + echo not-a-directory >dirORfile && + git add dirORfile && + git commit -m dirORfile && + + git switch -c df_conflict HEAD~1 && + test_commit random_file && + + git switch -c undo_fd_conflict fd_conflict && + git revert HEAD +' + +test_incidental_dir_removal () { + test_when_finished "git reset --hard" && + + git checkout foo/bar/baz^{commit} && + test_path_is_dir foo/bar && + + ( + cd foo && + "$@" && + + # Make sure foo still exists, and commands needing it work + test-tool getcwd && + git status --porcelain + ) && + test_path_is_missing foo/bar/baz && + test_path_is_missing foo/bar && + + test_path_is_dir foo +} + +test_required_dir_removal () { + git checkout df_conflict^{commit} && + test_when_finished "git clean -fdx" && + + ( + cd dirORfile && + + # Ensure command refuses to run + test_must_fail "$@" 2>../error && + grep "Refusing to remove.*current working directory" ../error && + + # ...and that the index and working tree are left clean + git diff --exit-code HEAD && + + # Ensure that getcwd and git status do not error out (which + # they might if the current working directory had been removed) + test-tool getcwd && + git status --porcelain + ) && + + test_path_is_dir dirORfile +} + +test_expect_success 'checkout does not clean cwd incidentally' ' + test_incidental_dir_removal git checkout init +' + +test_expect_success 'checkout fails if cwd needs to be removed' ' + test_required_dir_removal git checkout fd_conflict +' + +test_expect_success 'reset --hard does not clean cwd incidentally' ' + test_incidental_dir_removal git reset --hard init +' + +test_expect_success 'reset --hard fails if cwd needs to be removed' ' + test_required_dir_removal git reset --hard fd_conflict +' + +test_expect_success 'merge does not clean cwd incidentally' ' + test_incidental_dir_removal git merge reverted +' + +# This file uses some simple merges where +# Base: 'dirORfile/' exists +# Side1: random other file changed +# Side2: 'dirORfile/' removed, 'dirORfile' added +# this should resolve cleanly, but merge-recursive throws merge conflicts +# because it's dumb. Add a special test for checking merge-recursive (and +# merge-ort), then after this just hard require ort for all remaining tests. +# +test_expect_success 'merge fails if cwd needs to be removed; recursive friendly' ' + git checkout foo/bar/baz && + test_when_finished "git clean -fdx" && + + mkdir dirORfile && + ( + cd dirORfile && + + test_must_fail git merge fd_conflict 2>../error + ) && + + test_path_is_dir dirORfile && + grep "Refusing to remove the current working directory" error +' + +GIT_TEST_MERGE_ALGORITHM=ort + +test_expect_success 'merge fails if cwd needs to be removed' ' + test_required_dir_removal git merge fd_conflict +' + +test_expect_success 'cherry-pick does not clean cwd incidentally' ' + test_incidental_dir_removal git cherry-pick reverted +' + +test_expect_success 'cherry-pick fails if cwd needs to be removed' ' + test_required_dir_removal git cherry-pick fd_conflict +' + +test_expect_success 'rebase does not clean cwd incidentally' ' + test_incidental_dir_removal git rebase reverted +' + +test_expect_success 'rebase fails if cwd needs to be removed' ' + test_required_dir_removal git rebase fd_conflict +' + +test_expect_success 'revert does not clean cwd incidentally' ' + test_incidental_dir_removal git revert HEAD +' + +test_expect_success 'revert fails if cwd needs to be removed' ' + test_required_dir_removal git revert undo_fd_conflict +' + +test_expect_success 'rm does not clean cwd incidentally' ' + test_incidental_dir_removal git rm bar/baz.t +' + +test_expect_success 'apply does not remove cwd incidentally' ' + git diff HEAD HEAD~1 >patch && + test_incidental_dir_removal git apply ../patch +' + +test_incidental_untracked_dir_removal () { + test_when_finished "git reset --hard" && + + git checkout foo/bar/baz^{commit} && + mkdir -p untracked && + mkdir empty + >untracked/random && + + ( + cd untracked && + "$@" && + + # Make sure untracked still exists, and commands needing it work + test-tool getcwd && + git status --porcelain + ) && + test_path_is_missing empty && + test_path_is_missing untracked/random && + + test_path_is_dir untracked +} + +test_expect_success 'clean does not remove cwd incidentally' ' + test_incidental_untracked_dir_removal \ + git -C .. clean -fd -e warnings . >warnings && + grep "Refusing to remove current working directory" warnings +' + +test_expect_success 'stash does not remove cwd incidentally' ' + test_incidental_untracked_dir_removal \ + git stash --include-untracked +' + +test_expect_success '`rm -rf dir` only removes a subset of dir' ' + test_when_finished "rm -rf a/" && + + mkdir -p a/b/c && + >a/b/c/untracked && + >a/b/c/tracked && + git add a/b/c/tracked && + + ( + cd a/b && + git rm -rf ../b + ) && + + test_path_is_dir a/b && + test_path_is_missing a/b/c/tracked && + test_path_is_file a/b/c/untracked +' + +test_expect_success '`rm -rf dir` even with only tracked files will remove something else' ' + test_when_finished "rm -rf a/" && + + mkdir -p a/b/c && + >a/b/c/tracked && + git add a/b/c/tracked && + + ( + cd a/b && + git rm -rf ../b + ) && + + test_path_is_missing a/b/c/tracked && + test_path_is_missing a/b/c && + test_path_is_dir a/b +' + +test_expect_success 'git version continues working from a deleted dir' ' + mkdir tmp && + ( + cd tmp && + rm -rf ../tmp && + git version + ) +' + +test_submodule_removal () { + path_status=$1 && + shift && + + test_status= + test "$path_status" = dir && test_status=test_must_fail + + test_when_finished "git reset --hard HEAD~1" && + test_when_finished "rm -rf .git/modules/my_submodule" && + + git checkout foo/bar/baz && + + git init my_submodule && + touch my_submodule/file && + git -C my_submodule add file && + git -C my_submodule commit -m "initial commit" && + git submodule add ./my_submodule && + git commit -m "Add the submodule" && + + ( + cd my_submodule && + $test_status "$@" + ) && + + test_path_is_${path_status} my_submodule +} + +test_expect_success 'rm -r with -C leaves submodule if cwd inside' ' + test_submodule_removal dir git -C .. rm -r my_submodule/ +' + +test_expect_success 'rm -r leaves submodule if cwd inside' ' + test_submodule_removal dir \ + git --git-dir=../.git --work-tree=.. rm -r ../my_submodule/ +' + +test_expect_success 'rm -rf removes submodule even if cwd inside' ' + test_submodule_removal missing \ + git --git-dir=../.git --work-tree=.. rm -rf ../my_submodule/ +' + +test_done diff --git a/t/t3005-ls-files-relative.sh b/t/t3005-ls-files-relative.sh index 6ba8b589cd..fbfa210a50 100755 --- a/t/t3005-ls-files-relative.sh +++ b/t/t3005-ls-files-relative.sh @@ -39,10 +39,7 @@ test_expect_success 'ls-files with mixed levels' ' test_expect_success 'ls-files -c' ' ( cd top/sub && - for f in ../y* - do - echo "error: pathspec $SQ$f$SQ did not match any file(s) known to git" - done >expect.err && + printf "error: pathspec $SQ%s$SQ did not match any file(s) known to git\n" ../y* >expect.err && echo "Did you forget to ${SQ}git add${SQ}?" >>expect.err && ls ../x* >expect.out && test_must_fail git ls-files -c --error-unmatch ../[xy]* >actual.out 2>actual.err && @@ -54,10 +51,7 @@ test_expect_success 'ls-files -c' ' test_expect_success 'ls-files -o' ' ( cd top/sub && - for f in ../x* - do - echo "error: pathspec $SQ$f$SQ did not match any file(s) known to git" - done >expect.err && + printf "error: pathspec $SQ%s$SQ did not match any file(s) known to git\n" ../x* >expect.err && echo "Did you forget to ${SQ}git add${SQ}?" >>expect.err && ls ../y* >expect.out && test_must_fail git ls-files -o --error-unmatch ../[xy]* >actual.out 2>actual.err && diff --git a/t/t3040-subprojects-basic.sh b/t/t3040-subprojects-basic.sh index 6abdcbbc94..bd65dfcffc 100755 --- a/t/t3040-subprojects-basic.sh +++ b/t/t3040-subprojects-basic.sh @@ -1,6 +1,8 @@ #!/bin/sh test_description='Basic subproject functionality' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success 'setup: create superproject' ' diff --git a/t/t3070-wildmatch.sh b/t/t3070-wildmatch.sh index 72d5b014d8..f9539968e4 100755 --- a/t/t3070-wildmatch.sh +++ b/t/t3070-wildmatch.sh @@ -193,7 +193,7 @@ match() { file=$(cat .git/expected_test_file) && if should_create_test_file "$file" then - dirs=${file%/*} + dirs=${file%/*} && if test "$file" != "$dirs" then mkdir -p -- "$dirs" && diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh index e575ffb4ff..8a619d785e 100755 --- a/t/t3200-branch.sh +++ b/t/t3200-branch.sh @@ -168,6 +168,13 @@ test_expect_success 'git branch -M foo bar should fail when bar is checked out' test_must_fail git branch -M bar foo ' +test_expect_success 'git branch -M foo bar should fail when bar is checked out in worktree' ' + git branch -f bar && + test_when_finished "git worktree remove wt && git branch -D wt" && + git worktree add wt && + test_must_fail git branch -M bar wt +' + test_expect_success 'git branch -M baz bam should succeed when baz is checked out' ' git checkout -b baz && git branch bam && @@ -731,6 +738,28 @@ test_expect_success SYMLINKS 'git branch -m u v should fail when the reflog for test_must_fail git branch -m u v ' +test_expect_success SYMLINKS 'git branch -m with symlinked .git/refs' ' + test_when_finished "rm -rf subdir" && + git init --bare subdir && + + rm -rfv subdir/refs subdir/objects subdir/packed-refs && + ln -s ../.git/refs subdir/refs && + ln -s ../.git/objects subdir/objects && + ln -s ../.git/packed-refs subdir/packed-refs && + + git -C subdir rev-parse --absolute-git-dir >subdir.dir && + git rev-parse --absolute-git-dir >our.dir && + ! test_cmp subdir.dir our.dir && + + git -C subdir log && + git -C subdir branch rename-src && + git rev-parse rename-src >expect && + git -C subdir branch -m rename-src rename-dest && + git rev-parse rename-dest >actual && + test_cmp expect actual && + git branch -D rename-dest +' + test_expect_success 'test tracking setup via --track' ' git config remote.local.url . && git config remote.local.fetch refs/heads/*:refs/remotes/local/* && @@ -866,7 +895,7 @@ test_expect_success '--set-upstream-to fails on a missing src branch' ' ' test_expect_success '--set-upstream-to fails on a non-ref' ' - echo "fatal: Cannot setup tracking information; starting point '"'"'HEAD^{}'"'"' is not a branch." >expect && + echo "fatal: cannot set up tracking information; starting point '"'"'HEAD^{}'"'"' is not a branch" >expect && test_must_fail git branch --set-upstream-to HEAD^{} 2>err && test_cmp expect err ' @@ -953,7 +982,7 @@ test_expect_success 'disabled option --set-upstream fails' ' test_expect_success '--set-upstream-to notices an error to set branch as own upstream' ' git branch --set-upstream-to refs/heads/my13 my13 2>actual && cat >expect <<-\EOF && - warning: Not setting branch my13 as its own upstream. + warning: not setting branch my13 as its own upstream EOF test_expect_code 1 git config branch.my13.remote && test_expect_code 1 git config branch.my13.merge && @@ -1418,7 +1447,17 @@ test_expect_success 'invalid sort parameter in configuration' ' ( cd sort && git config branch.sort "v:notvalid" && - test_must_fail git branch + + # this works in the "listing" mode, so bad sort key + # is a dying offence. + test_must_fail git branch && + + # these do not need to use sorting, and should all + # succeed + git branch newone main && + git branch -c newone newerone && + git branch -m newone newestone && + git branch -d newerone newestone ) ' diff --git a/t/t3201-branch-contains.sh b/t/t3201-branch-contains.sh index 349a810cee..800fc33165 100755 --- a/t/t3201-branch-contains.sh +++ b/t/t3201-branch-contains.sh @@ -2,9 +2,6 @@ test_description='branch --contains <commit>, --no-contains <commit> --merged, and --no-merged' -GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main -export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME - . ./test-lib.sh test_expect_success setup ' diff --git a/t/t3202-show-branch.sh b/t/t3202-show-branch.sh index ad9902a06b..7a1be73ce8 100755 --- a/t/t3202-show-branch.sh +++ b/t/t3202-show-branch.sh @@ -4,12 +4,15 @@ test_description='test show-branch' . ./test-lib.sh +# arbitrary reference time: 2009-08-30 19:20:00 +GIT_TEST_DATE_NOW=1251660000; export GIT_TEST_DATE_NOW + test_expect_success 'setup' ' test_commit initial && for i in $(test_seq 1 10) do git checkout -b branch$i initial && - test_commit --no-tag branch$i + test_commit --no-tag branch$i || return 1 done && git for-each-ref \ --sort=version:refname \ @@ -49,7 +52,7 @@ test_expect_success 'show-branch with more than 8 branches' ' test_expect_success 'show-branch with showbranch.default' ' for branch in $(cat branches.sorted) do - test_config showbranch.default $branch --add + test_config showbranch.default $branch --add || return 1 done && git show-branch >actual && test_cmp expect actual @@ -124,7 +127,7 @@ test_expect_success 'show branch --merge-base with one argument' ' do git rev-parse $branch >expect && git show-branch --merge-base $branch >actual && - test_cmp expect actual + test_cmp expect actual || return 1 done ' @@ -133,7 +136,7 @@ test_expect_success 'show branch --merge-base with two arguments' ' do git rev-parse initial >expect && git show-branch --merge-base initial $branch >actual && - test_cmp expect actual + test_cmp expect actual || return 1 done ' @@ -146,4 +149,16 @@ test_expect_success 'show branch --merge-base with N arguments' ' test_cmp expect actual ' +test_expect_success 'show branch --reflog=2' ' + sed "s/^> //" >expect <<-\EOF && + > ! [refs/heads/branch10@{0}] (4 years, 5 months ago) commit: branch10 + > ! [refs/heads/branch10@{1}] (4 years, 5 months ago) commit: branch10 + > -- + > + [refs/heads/branch10@{0}] branch10 + > ++ [refs/heads/branch10@{1}] initial + EOF + git show-branch --reflog=2 >actual && + test_cmp actual expect +' + test_done diff --git a/t/t3203-branch-output.sh b/t/t3203-branch-output.sh index 6e94c6db7b..d34d77f893 100755 --- a/t/t3203-branch-output.sh +++ b/t/t3203-branch-output.sh @@ -1,9 +1,6 @@ #!/bin/sh test_description='git branch display tests' -GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main -export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME - . ./test-lib.sh . "$TEST_DIRECTORY"/lib-terminal.sh diff --git a/t/t3205-branch-color.sh b/t/t3205-branch-color.sh index 6a521c1a3e..0b61da92b3 100755 --- a/t/t3205-branch-color.sh +++ b/t/t3205-branch-color.sh @@ -1,9 +1,6 @@ #!/bin/sh test_description='basic branch output coloring' -GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main -export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME - TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh diff --git a/t/t3302-notes-index-expensive.sh b/t/t3302-notes-index-expensive.sh index ef8b63952e..bc9d8ee1e6 100755 --- a/t/t3302-notes-index-expensive.sh +++ b/t/t3302-notes-index-expensive.sh @@ -8,6 +8,7 @@ test_description='Test commit notes index (expensive!)' GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh create_repo () { diff --git a/t/t3303-notes-subtrees.sh b/t/t3303-notes-subtrees.sh index d47ce00f69..7e0a8960af 100755 --- a/t/t3303-notes-subtrees.sh +++ b/t/t3303-notes-subtrees.sh @@ -5,6 +5,7 @@ test_description='Test commit notes organized in subtrees' GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh number_of_commits=100 @@ -30,7 +31,7 @@ verify_notes () { while [ $i -gt 0 ]; do echo " commit #$i" && echo " note for commit #$i" && - i=$(($i-1)); + i=$(($i-1)) || return 1 done > expect && test_cmp expect output } @@ -42,7 +43,7 @@ test_expect_success "setup: create $number_of_commits commits" ' while [ $nr -lt $number_of_commits ]; do nr=$(($nr+1)) && test_tick && - cat <<INPUT_END + cat <<INPUT_END || return 1 commit refs/heads/main committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE data <<COMMIT @@ -178,7 +179,7 @@ verify_concatenated_notes () { echo " first note for commit #$i" && echo " " && echo " second note for commit #$i" && - i=$(($i-1)); + i=$(($i-1)) || return 1 done > expect && test_cmp expect output } diff --git a/t/t3305-notes-fanout.sh b/t/t3305-notes-fanout.sh index 94c1b02251..1f5964865a 100755 --- a/t/t3305-notes-fanout.sh +++ b/t/t3305-notes-fanout.sh @@ -2,6 +2,7 @@ test_description='Test that adding/removing many notes triggers automatic fanout restructuring' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh path_has_fanout() { @@ -57,7 +58,7 @@ test_expect_success 'many notes created correctly with git-notes' ' do echo " commit #$i" && echo " note #$i" && - i=$(($i - 1)); + i=$(($i - 1)) || return 1 done > expect && test_cmp expect output ' @@ -106,7 +107,7 @@ test_expect_success 'most notes deleted correctly with git-notes' ' do echo " commit #$i" && echo " note #$i" && - i=$(($i - 1)); + i=$(($i - 1)) || return 1 done > expect && test_cmp expect output ' diff --git a/t/t3320-notes-merge-worktrees.sh b/t/t3320-notes-merge-worktrees.sh index 6b2d507f3e..bff0aea550 100755 --- a/t/t3320-notes-merge-worktrees.sh +++ b/t/t3320-notes-merge-worktrees.sh @@ -8,6 +8,7 @@ test_description='Test merging of notes trees in multiple worktrees' 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 commit' ' diff --git a/t/t3402-rebase-merge.sh b/t/t3402-rebase-merge.sh index cfde68f193..7e46f4ca85 100755 --- a/t/t3402-rebase-merge.sh +++ b/t/t3402-rebase-merge.sh @@ -68,7 +68,7 @@ test_expect_success 'merge and rebase should match' ' if test -s difference then cat difference - (exit 1) + false else echo happy fi @@ -102,7 +102,7 @@ test_expect_success 'merge and rebase should match' ' if test -s difference then cat difference - (exit 1) + false else echo happy fi @@ -117,7 +117,7 @@ test_expect_success 'picking rebase' ' echo happy else git show-branch - (exit 1) + false fi && f=$(git diff-tree --name-only HEAD^ HEAD) && g=$(git diff-tree --name-only HEAD^^ HEAD^) && @@ -127,7 +127,7 @@ test_expect_success 'picking rebase' ' *) echo "$f" echo "$g" - (exit 1) + false esac ' diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh index 12eb226957..a38f2da769 100755 --- a/t/t3404-rebase-interactive.sh +++ b/t/t3404-rebase-interactive.sh @@ -25,8 +25,6 @@ Initial setup: where A, B, D and G all touch file1, and one, two, three, four all touch file "conflict". ' -GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=master -export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME . ./test-lib.sh @@ -826,7 +824,7 @@ test_expect_success 'always cherry-pick with --no-ff' ' do test ! $(git rev-parse HEAD~$p) = $(git rev-parse original-no-ff-branch~$p) && git diff HEAD~$p original-no-ff-branch~$p > out && - test_must_be_empty out + test_must_be_empty out || return 1 done && test_cmp_rev HEAD~3 original-no-ff-branch~3 && git diff HEAD~3 original-no-ff-branch~3 > out && @@ -1341,7 +1339,7 @@ test_expect_success 'rebase --continue removes CHERRY_PICK_HEAD' ' test_seq 5 | sed "s/$double/&&/" >seq && git add seq && test_tick && - git commit -m seq-$double + git commit -m seq-$double || return 1 done && git tag seq-onto && git reset --hard HEAD~2 && diff --git a/t/t3409-rebase-environ.sh b/t/t3409-rebase-environ.sh new file mode 100755 index 0000000000..83ffb39d9f --- /dev/null +++ b/t/t3409-rebase-environ.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +test_description='git rebase interactive environment' + +. ./test-lib.sh + +test_expect_success 'setup' ' + test_commit one && + test_commit two && + test_commit three +' + +test_expect_success 'rebase --exec does not muck with GIT_DIR' ' + git rebase --exec "printf %s \$GIT_DIR >environ" HEAD~1 && + test_must_be_empty environ +' + +test_expect_success 'rebase --exec does not muck with GIT_WORK_TREE' ' + git rebase --exec "printf %s \$GIT_WORK_TREE >environ" HEAD~1 && + test_must_be_empty environ +' + +test_done diff --git a/t/t3417-rebase-whitespace-fix.sh b/t/t3417-rebase-whitespace-fix.sh index 946e92f8da..96f2cf22fa 100755 --- a/t/t3417-rebase-whitespace-fix.sh +++ b/t/t3417-rebase-whitespace-fix.sh @@ -115,9 +115,7 @@ test_expect_success 'at beginning of file' ' git config core.whitespace "blank-at-eol" && cp beginning file && git commit -m beginning file && - for i in 1 2 3 4 5; do - echo $i - done >> file && + test_write_lines 1 2 3 4 5 >>file && git commit -m more file && git rebase --whitespace=fix HEAD^^ && test_cmp expect-beginning file diff --git a/t/t3422-rebase-incompatible-options.sh b/t/t3422-rebase-incompatible-options.sh index eb0a3d9d48..6dabb05a2a 100755 --- a/t/t3422-rebase-incompatible-options.sh +++ b/t/t3422-rebase-incompatible-options.sh @@ -1,6 +1,8 @@ #!/bin/sh test_description='test if rebase detects and aborts on incompatible options' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success 'setup' ' diff --git a/t/t3429-rebase-edit-todo.sh b/t/t3429-rebase-edit-todo.sh index 7024d49ae7..abd66f3602 100755 --- a/t/t3429-rebase-edit-todo.sh +++ b/t/t3429-rebase-edit-todo.sh @@ -13,10 +13,15 @@ test_expect_success 'setup' ' test_expect_success 'rebase exec modifies rebase-todo' ' todo=.git/rebase-merge/git-rebase-todo && - git rebase HEAD -x "echo exec touch F >>$todo" && + git rebase HEAD~1 -x "echo exec touch F >>$todo" && test -e F ' +test_expect_success 'rebase exec with an empty list does not exec anything' ' + git rebase HEAD -x "true" 2>output && + ! grep "Executing: true" output +' + test_expect_success 'loose object cache vs re-reading todo list' ' GIT_REBASE_TODO=.git/rebase-merge/git-rebase-todo && export GIT_REBASE_TODO && diff --git a/t/t3501-revert-cherry-pick.sh b/t/t3501-revert-cherry-pick.sh index 4b5b607673..8617efaaf1 100755 --- a/t/t3501-revert-cherry-pick.sh +++ b/t/t3501-revert-cherry-pick.sh @@ -19,7 +19,7 @@ test_expect_success setup ' for l in a b c d e f g h i j k l m n o do - echo $l$l$l$l$l$l$l$l$l + echo $l$l$l$l$l$l$l$l$l || return 1 done >oops && test_tick && diff --git a/t/t3508-cherry-pick-many-commits.sh b/t/t3508-cherry-pick-many-commits.sh index e8375d1c97..2d53ce754c 100755 --- a/t/t3508-cherry-pick-many-commits.sh +++ b/t/t3508-cherry-pick-many-commits.sh @@ -29,7 +29,7 @@ test_expect_success setup ' git add file1 && test_tick && git commit -m "$val" && - git tag $val + git tag $val || return 1 done ' diff --git a/t/t3600-rm.sh b/t/t3600-rm.sh index bb9ef35dac..e74a318ac3 100755 --- a/t/t3600-rm.sh +++ b/t/t3600-rm.sh @@ -265,7 +265,7 @@ test_expect_success 'choking "git rm" should not let it die with cruft (induce S test_expect_success !MINGW 'choking "git rm" should not let it die with cruft (induce and check SIGPIPE)' ' choke_git_rm_setup && - OUT=$( ((trap "" PIPE; git rm -n "some-file-*"; echo $? 1>&3) | :) 3>&1 ) && + OUT=$( ((trap "" PIPE && git rm -n "some-file-*"; echo $? 1>&3) | :) 3>&1 ) && test_match_signal 13 "$OUT" && test_path_is_missing .git/index.lock ' @@ -274,10 +274,7 @@ test_expect_success 'Resolving by removal is not a warning-worthy event' ' git reset -q --hard && test_when_finished "rm -f .git/index.lock msg && git reset -q --hard" && blob=$(echo blob | git hash-object -w --stdin) && - for stage in 1 2 3 - do - echo "100644 $blob $stage blob" - done | git update-index --index-info && + printf "100644 $blob %d\tblob\n" 1 2 3 | git update-index --index-info && git rm blob >msg 2>&1 && test_i18ngrep ! "needs merge" msg && test_must_fail git ls-files -s --error-unmatch blob diff --git a/t/t3700-add.sh b/t/t3700-add.sh index 283a66955d..b1f90ba325 100755 --- a/t/t3700-add.sh +++ b/t/t3700-add.sh @@ -141,9 +141,9 @@ test_expect_success 'check correct prefix detection' ' test_expect_success 'git add with filemode=0, symlinks=0, and unmerged entries' ' for s in 1 2 3 do - echo $s > stage$s - echo "100755 $(git hash-object -w stage$s) $s file" - echo "120000 $(printf $s | git hash-object -w -t blob --stdin) $s symlink" + echo $s > stage$s && + echo "100755 $(git hash-object -w stage$s) $s file" && + echo "120000 $(printf $s | git hash-object -w -t blob --stdin) $s symlink" || return 1 done | git update-index --index-info && git config core.filemode 0 && git config core.symlinks 0 && @@ -177,7 +177,7 @@ test_expect_success 'git add --refresh' ' git read-tree HEAD && case "$(git diff-index HEAD -- foo)" in :100644" "*"M foo") echo pass;; - *) echo fail; (exit 1);; + *) echo fail; false;; esac && git add --refresh -- foo && test -z "$(git diff-index HEAD -- foo)" diff --git a/t/t3702-add-edit.sh b/t/t3702-add-edit.sh index 6c676645d8..a1801a8cbd 100755 --- a/t/t3702-add-edit.sh +++ b/t/t3702-add-edit.sh @@ -4,6 +4,8 @@ # test_description='add -e basic tests' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh diff --git a/t/t3703-add-magic-pathspec.sh b/t/t3703-add-magic-pathspec.sh index 3ef525a559..d84071038e 100755 --- a/t/t3703-add-magic-pathspec.sh +++ b/t/t3703-add-magic-pathspec.sh @@ -2,6 +2,7 @@ test_description='magic pathspec tests using git-add' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success 'setup' ' diff --git a/t/t3704-add-pathspec-file.sh b/t/t3704-add-pathspec-file.sh index 9e35c1fbca..5d5164d1fc 100755 --- a/t/t3704-add-pathspec-file.sh +++ b/t/t3704-add-pathspec-file.sh @@ -2,6 +2,7 @@ test_description='add --pathspec-from-file' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_tick diff --git a/t/t3705-add-sparse-checkout.sh b/t/t3705-add-sparse-checkout.sh index f3143c9290..81f3384eee 100755 --- a/t/t3705-add-sparse-checkout.sh +++ b/t/t3705-add-sparse-checkout.sh @@ -181,13 +181,13 @@ test_expect_success 'git add fails outside of sparse-checkout definition' ' # Avoid munging CRLFs to avoid an error message git -c core.autocrlf=input add --sparse sparse_entry 2>stderr && test_must_be_empty stderr && - test-tool read-cache --table >actual && - grep "^100644 blob.*sparse_entry\$" actual && + git ls-files --stage >actual && + grep "^100644 .*sparse_entry\$" actual && git add --sparse --chmod=+x sparse_entry 2>stderr && test_must_be_empty stderr && - test-tool read-cache --table >actual && - grep "^100755 blob.*sparse_entry\$" actual && + git ls-files --stage >actual && + grep "^100755 .*sparse_entry\$" actual && git reset && diff --git a/t/t3800-mktag.sh b/t/t3800-mktag.sh index 0544d58a6e..e3cf0ffbe5 100755 --- a/t/t3800-mktag.sh +++ b/t/t3800-mktag.sh @@ -72,7 +72,8 @@ check_verify_failure () { # Manually create the broken, we cannot do it with # update-ref - echo "$bad_tag" >"bad-tag/$tag_ref" && + test-tool -C bad-tag ref-store main delete-refs 0 msg "$tag_ref" && + test-tool -C bad-tag ref-store main update-ref msg "$tag_ref" $bad_tag $ZERO_OID REF_SKIP_OID_VERIFICATION && # Unlike fsck-ing unreachable content above, this # will always fail. @@ -83,7 +84,8 @@ check_verify_failure () { # Make sure the earlier test created it for us git rev-parse "$bad_tag" && - echo "$bad_tag" >"bad-tag/$tag_ref" && + test-tool -C bad-tag ref-store main delete-refs 0 msg "$tag_ref" && + test-tool -C bad-tag ref-store main update-ref msg "$tag_ref" $bad_tag $ZERO_OID REF_SKIP_OID_VERIFICATION && printf "%s tag\t%s\n" "$bad_tag" "$tag_ref" >expected && git -C bad-tag for-each-ref "$tag_ref" >actual && diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh index 711f02ff6b..b17c52d880 100755 --- a/t/t3903-stash.sh +++ b/t/t3903-stash.sh @@ -307,6 +307,17 @@ test_expect_success 'stash --no-keep-index' ' test bar,bar2 = $(cat file),$(cat file2) ' +test_expect_success 'stash --staged' ' + echo bar3 >file && + echo bar4 >file2 && + git add file2 && + git stash --staged && + test bar3,bar2 = $(cat file),$(cat file2) && + git reset --hard && + git stash pop && + test bar,bar4 = $(cat file),$(cat file2) +' + test_expect_success 'dont assume push with non-option args' ' test_must_fail git stash -q drop 2>err && test_i18ngrep -e "subcommand wasn'\''t specified; '\''push'\'' can'\''t be assumed due to unexpected token '\''drop'\''" err diff --git a/t/t3908-stash-in-worktree.sh b/t/t3908-stash-in-worktree.sh index 2b2b366ef9..347a89b030 100755 --- a/t/t3908-stash-in-worktree.sh +++ b/t/t3908-stash-in-worktree.sh @@ -5,6 +5,7 @@ test_description='Test git stash in a worktree' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success 'setup' ' diff --git a/t/t3920-crlf-messages.sh b/t/t3920-crlf-messages.sh index a8ad5462d9..0276edbe3d 100755 --- a/t/t3920-crlf-messages.sh +++ b/t/t3920-crlf-messages.sh @@ -70,7 +70,7 @@ test_crlf_subject_body_and_contents() { for ref in ${LIB_CRLF_BRANCHES} do cat .crlf-${file}-\"\${ref}\".txt >>expect && - printf \"\n\" >>expect + printf \"\n\" >>expect || return 1 done && git $command_and_args --format=\"%${atom}\" >actual && test_cmp expect actual @@ -90,7 +90,7 @@ test_expect_success 'branch: --verbose works with messages using CRLF' ' do printf " " >>expect && cat .crlf-subject-${branch}.txt >>expect && - printf "\n" >>expect + printf "\n" >>expect || return 1 done && git branch -v >tmp && # Remove first two columns, and the line for the currently checked out branch diff --git a/t/t4000-diff-format.sh b/t/t4000-diff-format.sh index cce334981e..bfcaae390f 100755 --- a/t/t4000-diff-format.sh +++ b/t/t4000-diff-format.sh @@ -6,6 +6,8 @@ test_description='Test built-in diff output engine. ' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh . "$TEST_DIRECTORY"/lib-diff.sh diff --git a/t/t4001-diff-rename.sh b/t/t4001-diff-rename.sh index 68f2ebca58..3dc9047044 100755 --- a/t/t4001-diff-rename.sh +++ b/t/t4001-diff-rename.sh @@ -174,7 +174,7 @@ test_expect_success 'setup for many rename source candidates' ' do for j in 0 1 2 3 4 5 6 7 8 9; do - echo "$i$j" >"path$i$j" + echo "$i$j" >"path$i$j" || return 1 done done && git add "path??" && diff --git a/t/t4003-diff-rename-1.sh b/t/t4003-diff-rename-1.sh index f4485a87c6..181e9683a7 100755 --- a/t/t4003-diff-rename-1.sh +++ b/t/t4003-diff-rename-1.sh @@ -6,6 +6,8 @@ test_description='More rename detection ' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh . "$TEST_DIRECTORY"/lib-diff.sh ;# test-lib chdir's into trash diff --git a/t/t4004-diff-rename-symlink.sh b/t/t4004-diff-rename-symlink.sh index 3d495e37bb..8def4d4aee 100755 --- a/t/t4004-diff-rename-symlink.sh +++ b/t/t4004-diff-rename-symlink.sh @@ -9,6 +9,8 @@ The rename detection logic should be able to detect pure rename or copy of symbolic links, but should not produce rename/copy followed by an edit for them. ' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh . "$TEST_DIRECTORY"/lib-diff.sh diff --git a/t/t4005-diff-rename-2.sh b/t/t4005-diff-rename-2.sh index 6f1b323f97..5c756dc243 100755 --- a/t/t4005-diff-rename-2.sh +++ b/t/t4005-diff-rename-2.sh @@ -5,6 +5,8 @@ test_description='Same rename detection as t4003 but testing diff-raw.' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh . "$TEST_DIRECTORY"/lib-diff.sh ;# test-lib chdir's into trash diff --git a/t/t4006-diff-mode.sh b/t/t4006-diff-mode.sh index 6cdee2a216..dbd4c0da21 100755 --- a/t/t4006-diff-mode.sh +++ b/t/t4006-diff-mode.sh @@ -6,6 +6,8 @@ test_description='Test mode change diffs. ' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh sed_script='s/\(:100644 100755\) \('"$OID_REGEX"'\) \2 /\1 X X /' diff --git a/t/t4007-rename-3.sh b/t/t4007-rename-3.sh index c634653b5b..b86165cbac 100755 --- a/t/t4007-rename-3.sh +++ b/t/t4007-rename-3.sh @@ -6,6 +6,8 @@ test_description='Rename interaction with pathspec. ' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh . "$TEST_DIRECTORY"/lib-diff.sh ;# test-lib chdir's into trash diff --git a/t/t4009-diff-rename-4.sh b/t/t4009-diff-rename-4.sh index 59b7f44f05..3480781dab 100755 --- a/t/t4009-diff-rename-4.sh +++ b/t/t4009-diff-rename-4.sh @@ -6,6 +6,8 @@ test_description='Same rename detection as t4003 but testing diff-raw -z. ' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh . "$TEST_DIRECTORY"/lib-diff.sh ;# test-lib chdir's into trash diff --git a/t/t4010-diff-pathspec.sh b/t/t4010-diff-pathspec.sh index 1bbced79ec..9d9650eba7 100755 --- a/t/t4010-diff-pathspec.sh +++ b/t/t4010-diff-pathspec.sh @@ -9,6 +9,8 @@ Prepare: file0 path1/file1 ' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh . "$TEST_DIRECTORY"/lib-diff.sh ;# test-lib chdir's into trash diff --git a/t/t4011-diff-symlink.sh b/t/t4011-diff-symlink.sh index 5a25c259fe..d7a5f7ae78 100755 --- a/t/t4011-diff-symlink.sh +++ b/t/t4011-diff-symlink.sh @@ -6,6 +6,8 @@ test_description='Test diff of symlinks. ' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh . "$TEST_DIRECTORY"/lib-diff.sh diff --git a/t/t4012-diff-binary.sh b/t/t4012-diff-binary.sh index 33ff588ebc..c509143c81 100755 --- a/t/t4012-diff-binary.sh +++ b/t/t4012-diff-binary.sh @@ -6,6 +6,7 @@ test_description='Binary diff and apply ' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh cat >expect.binary-numstat <<\EOF @@ -122,7 +123,7 @@ test_expect_success 'diff --stat with binary files and big change count' ' i=0 && while test $i -lt 10000; do echo $i && - i=$(($i + 1)) + i=$(($i + 1)) || return 1 done >textfile && git add textfile && git diff --cached --stat binfile textfile >output && diff --git a/t/t4013-diff-various.sh b/t/t4013-diff-various.sh index 28683d059d..750aee17ea 100755 --- a/t/t4013-diff-various.sh +++ b/t/t4013-diff-various.sh @@ -19,8 +19,8 @@ test_expect_success setup ' mkdir dir && mkdir dir2 && - for i in 1 2 3; do echo $i; done >file0 && - for i in A B; do echo $i; done >dir/sub && + test_write_lines 1 2 3 >file0 && + test_write_lines A B >dir/sub && cat file0 >file2 && git add file0 file2 dir/sub && git commit -m Initial && @@ -32,8 +32,8 @@ test_expect_success setup ' GIT_COMMITTER_DATE="2006-06-26 00:01:00 +0000" && export GIT_AUTHOR_DATE GIT_COMMITTER_DATE && - for i in 4 5 6; do echo $i; done >>file0 && - for i in C D; do echo $i; done >>dir/sub && + test_write_lines 4 5 6 >>file0 && + test_write_lines C D >>dir/sub && rm -f file2 && git update-index --remove file0 file2 dir/sub && git commit -m "Second${LF}${LF}This is the second commit." && @@ -42,9 +42,9 @@ test_expect_success setup ' GIT_COMMITTER_DATE="2006-06-26 00:02:00 +0000" && export GIT_AUTHOR_DATE GIT_COMMITTER_DATE && - for i in A B C; do echo $i; done >file1 && + test_write_lines A B C >file1 && git add file1 && - for i in E F; do echo $i; done >>dir/sub && + test_write_lines E F >>dir/sub && git update-index dir/sub && git commit -m Third && @@ -53,8 +53,8 @@ test_expect_success setup ' export GIT_AUTHOR_DATE GIT_COMMITTER_DATE && git checkout side && - for i in A B C; do echo $i; done >>file0 && - for i in 1 2; do echo $i; done >>dir/sub && + test_write_lines A B C >>file0 && + test_write_lines 1 2 >>dir/sub && cat dir/sub >file3 && git add file3 && git update-index file0 dir/sub && @@ -71,8 +71,8 @@ test_expect_success setup ' GIT_COMMITTER_DATE="2006-06-26 00:05:00 +0000" && export GIT_AUTHOR_DATE GIT_COMMITTER_DATE && - for i in A B C; do echo $i; done >>file0 && - for i in 1 2; do echo $i; done >>dir/sub && + test_write_lines A B C >>file0 && + test_write_lines 1 2 >>dir/sub && git update-index file0 dir/sub && mkdir dir3 && @@ -86,7 +86,7 @@ test_expect_success setup ' GIT_COMMITTER_DATE="2006-06-26 00:06:00 +0000" && export GIT_AUTHOR_DATE GIT_COMMITTER_DATE && git checkout -b rearrange initial && - for i in B A; do echo $i; done >dir/sub && + test_write_lines B A >dir/sub && git add dir/sub && git commit -m "Rearranged lines in dir/sub" && git checkout master && diff --git a/t/t4014-format-patch.sh b/t/t4014-format-patch.sh index 712d4b5ddf..7dc5a5c736 100755 --- a/t/t4014-format-patch.sh +++ b/t/t4014-format-patch.sh @@ -12,25 +12,25 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME . "$TEST_DIRECTORY"/lib-terminal.sh test_expect_success setup ' - for i in 1 2 3 4 5 6 7 8 9 10; do echo "$i"; done >file && + test_write_lines 1 2 3 4 5 6 7 8 9 10 >file && cat file >elif && git add file elif && test_tick && git commit -m Initial && git checkout -b side && - for i in 1 2 5 6 A B C 7 8 9 10; do echo "$i"; done >file && + test_write_lines 1 2 5 6 A B C 7 8 9 10 >file && test_chmod +x elif && test_tick && git commit -m "Side changes #1" && - for i in D E F; do echo "$i"; done >>file && + test_write_lines D E F >>file && git update-index file && test_tick && git commit -m "Side changes #2" && git tag C2 && - for i in 5 6 1 2 3 A 4 B C 7 8 9 10 D E F; do echo "$i"; done >file && + test_write_lines 5 6 1 2 3 A 4 B C 7 8 9 10 D E F >file && git update-index file && test_tick && git commit -m "Side changes #3 with \\n backslash-n in it." && @@ -43,18 +43,18 @@ test_expect_success setup ' git checkout side && git checkout -b patchid && - for i in 5 6 1 2 3 A 4 B C 7 8 9 10 D E F; do echo "$i"; done >file2 && - for i in 1 2 3 A 4 B C 7 8 9 10 D E F 5 6; do echo "$i"; done >file3 && - for i in 8 9 10; do echo "$i"; done >file && + test_write_lines 5 6 1 2 3 A 4 B C 7 8 9 10 D E F >file2 && + test_write_lines 1 2 3 A 4 B C 7 8 9 10 D E F 5 6 >file3 && + test_write_lines 8 9 10 >file && git add file file2 file3 && test_tick && git commit -m "patchid 1" && - for i in 4 A B 7 8 9 10; do echo "$i"; done >file2 && - for i in 8 9 10 5 6; do echo "$i"; done >file3 && + test_write_lines 4 A B 7 8 9 10 >file2 && + test_write_lines 8 9 10 5 6 >file3 && git add file2 file3 && test_tick && git commit -m "patchid 2" && - for i in 10 5 6; do echo "$i"; done >file && + test_write_lines 10 5 6 >file && git add file && test_tick && git commit -m "patchid 3" && @@ -325,7 +325,7 @@ test_expect_success 'filename length limit' ' max=$( for patch in 000[1-9]-*.patch do - echo "$patch" | wc -c + echo "$patch" | wc -c || exit 1 done | sort -nr | head -n 1 @@ -343,7 +343,7 @@ test_expect_success 'filename length limit from config' ' max=$( for patch in 000[1-9]-*.patch do - echo "$patch" | wc -c + echo "$patch" | wc -c || exit 1 done | sort -nr | head -n 1 @@ -361,7 +361,7 @@ test_expect_success 'filename limit applies only to basename' ' max=$( for patch in patches/000[1-9]-*.patch do - echo "${patch#patches/}" | wc -c + echo "${patch#patches/}" | wc -c || exit 1 done | sort -nr | head -n 1 @@ -653,7 +653,7 @@ test_expect_success 'excessive subject' ' git checkout side && before=$(git hash-object file) && before=$(git rev-parse --short $before) && - for i in 5 6 1 2 3 A 4 B C 7 8 9 10 D E F; do echo "$i"; done >>file && + test_write_lines 5 6 1 2 3 A 4 B C 7 8 9 10 D E F >>file && after=$(git hash-object file) && after=$(git rev-parse --short $after) && git update-index file && @@ -1086,7 +1086,7 @@ test_expect_success TTY 'format-patch --stdout paginates' ' test_expect_success 'format-patch handles multi-line subjects' ' rm -rf patches/ && echo content >>file && - for i in one two three; do echo $i; done >msg && + test_write_lines one two three >msg && git add file && git commit -F msg && git format-patch -o patches -1 && @@ -1098,7 +1098,7 @@ test_expect_success 'format-patch handles multi-line subjects' ' test_expect_success 'format-patch handles multi-line encoded subjects' ' rm -rf patches/ && echo content >>file && - for i in en tvÃ¥ tre; do echo $i; done >msg && + test_write_lines en tvÃ¥ tre >msg && git add file && git commit -F msg && git format-patch -o patches -1 && diff --git a/t/t4015-diff-whitespace.sh b/t/t4015-diff-whitespace.sh index 2c13b62d3c..9babf13bc9 100755 --- a/t/t4015-diff-whitespace.sh +++ b/t/t4015-diff-whitespace.sh @@ -843,7 +843,7 @@ test_expect_success 'whitespace changes with modification reported (diffstat)' ' test_expect_success 'whitespace-only changes reported across renames (diffstat)' ' git reset --hard && - for i in 1 2 3 4 5 6 7 8 9; do echo "$i$i$i$i$i$i"; done >x && + for i in 1 2 3 4 5 6 7 8 9; do echo "$i$i$i$i$i$i" || return 1; done >x && git add x && git commit -m "base" && sed -e "5s/^/ /" x >z && @@ -859,7 +859,7 @@ test_expect_success 'whitespace-only changes reported across renames (diffstat)' test_expect_success 'whitespace-only changes reported across renames' ' git reset --hard HEAD~1 && - for i in 1 2 3 4 5 6 7 8 9; do echo "$i$i$i$i$i$i"; done >x && + for i in 1 2 3 4 5 6 7 8 9; do echo "$i$i$i$i$i$i" || return 1; done >x && git add x && hash_x=$(git hash-object x) && before=$(git rev-parse --short "$hash_x") && @@ -1442,6 +1442,143 @@ test_expect_success 'detect permutations inside moved code -- dimmed-zebra' ' test_cmp expected actual ' +test_expect_success 'zebra alternate color is only used when necessary' ' + cat >old.txt <<-\EOF && + line 1A should be marked as oldMoved newMovedAlternate + line 1B should be marked as oldMoved newMovedAlternate + unchanged + line 2A should be marked as oldMoved newMovedAlternate + line 2B should be marked as oldMoved newMovedAlternate + line 3A should be marked as oldMovedAlternate newMoved + line 3B should be marked as oldMovedAlternate newMoved + unchanged + line 4A should be marked as oldMoved newMovedAlternate + line 4B should be marked as oldMoved newMovedAlternate + line 5A should be marked as oldMovedAlternate newMoved + line 5B should be marked as oldMovedAlternate newMoved + line 6A should be marked as oldMoved newMoved + line 6B should be marked as oldMoved newMoved + EOF + cat >new.txt <<-\EOF && + line 1A should be marked as oldMoved newMovedAlternate + line 1B should be marked as oldMoved newMovedAlternate + unchanged + line 3A should be marked as oldMovedAlternate newMoved + line 3B should be marked as oldMovedAlternate newMoved + line 2A should be marked as oldMoved newMovedAlternate + line 2B should be marked as oldMoved newMovedAlternate + unchanged + line 6A should be marked as oldMoved newMoved + line 6B should be marked as oldMoved newMoved + line 4A should be marked as oldMoved newMovedAlternate + line 4B should be marked as oldMoved newMovedAlternate + line 5A should be marked as oldMovedAlternate newMoved + line 5B should be marked as oldMovedAlternate newMoved + EOF + test_expect_code 1 git diff --no-index --color --color-moved=zebra \ + --color-moved-ws=allow-indentation-change \ + old.txt new.txt >output && + grep -v index output | test_decode_color >actual && + cat >expected <<-\EOF && + <BOLD>diff --git a/old.txt b/new.txt<RESET> + <BOLD>--- a/old.txt<RESET> + <BOLD>+++ b/new.txt<RESET> + <CYAN>@@ -1,14 +1,14 @@<RESET> + <BOLD;MAGENTA>-line 1A should be marked as oldMoved newMovedAlternate<RESET> + <BOLD;MAGENTA>-line 1B should be marked as oldMoved newMovedAlternate<RESET> + <BOLD;CYAN>+<RESET><BOLD;CYAN> line 1A should be marked as oldMoved newMovedAlternate<RESET> + <BOLD;CYAN>+<RESET><BOLD;CYAN> line 1B should be marked as oldMoved newMovedAlternate<RESET> + unchanged<RESET> + <BOLD;MAGENTA>-line 2A should be marked as oldMoved newMovedAlternate<RESET> + <BOLD;MAGENTA>-line 2B should be marked as oldMoved newMovedAlternate<RESET> + <BOLD;BLUE>-line 3A should be marked as oldMovedAlternate newMoved<RESET> + <BOLD;BLUE>-line 3B should be marked as oldMovedAlternate newMoved<RESET> + <BOLD;CYAN>+<RESET><BOLD;CYAN> line 3A should be marked as oldMovedAlternate newMoved<RESET> + <BOLD;CYAN>+<RESET><BOLD;CYAN> line 3B should be marked as oldMovedAlternate newMoved<RESET> + <BOLD;YELLOW>+<RESET><BOLD;YELLOW> line 2A should be marked as oldMoved newMovedAlternate<RESET> + <BOLD;YELLOW>+<RESET><BOLD;YELLOW> line 2B should be marked as oldMoved newMovedAlternate<RESET> + unchanged<RESET> + <BOLD;MAGENTA>-line 4A should be marked as oldMoved newMovedAlternate<RESET> + <BOLD;MAGENTA>-line 4B should be marked as oldMoved newMovedAlternate<RESET> + <BOLD;BLUE>-line 5A should be marked as oldMovedAlternate newMoved<RESET> + <BOLD;BLUE>-line 5B should be marked as oldMovedAlternate newMoved<RESET> + <BOLD;MAGENTA>-line 6A should be marked as oldMoved newMoved<RESET> + <BOLD;MAGENTA>-line 6B should be marked as oldMoved newMoved<RESET> + <BOLD;CYAN>+<RESET><BOLD;CYAN> line 6A should be marked as oldMoved newMoved<RESET> + <BOLD;CYAN>+<RESET><BOLD;CYAN> line 6B should be marked as oldMoved newMoved<RESET> + <BOLD;YELLOW>+<RESET><BOLD;YELLOW> line 4A should be marked as oldMoved newMovedAlternate<RESET> + <BOLD;YELLOW>+<RESET><BOLD;YELLOW> line 4B should be marked as oldMoved newMovedAlternate<RESET> + <BOLD;CYAN>+<RESET><BOLD;CYAN> line 5A should be marked as oldMovedAlternate newMoved<RESET> + <BOLD;CYAN>+<RESET><BOLD;CYAN> line 5B should be marked as oldMovedAlternate newMoved<RESET> + EOF + test_cmp expected actual +' + +test_expect_success 'short lines of opposite sign do not get marked as moved' ' + cat >old.txt <<-\EOF && + this line should be marked as moved + unchanged + unchanged + unchanged + unchanged + too short + this line should be marked as oldMoved newMoved + this line should be marked as oldMovedAlternate newMoved + unchanged 1 + unchanged 2 + unchanged 3 + unchanged 4 + this line should be marked as oldMoved newMoved/newMovedAlternate + EOF + cat >new.txt <<-\EOF && + too short + unchanged + unchanged + this line should be marked as moved + too short + unchanged + unchanged + this line should be marked as oldMoved newMoved/newMovedAlternate + unchanged 1 + unchanged 2 + this line should be marked as oldMovedAlternate newMoved + this line should be marked as oldMoved newMoved/newMovedAlternate + unchanged 3 + this line should be marked as oldMoved newMoved + unchanged 4 + EOF + test_expect_code 1 git diff --no-index --color --color-moved=zebra \ + old.txt new.txt >output && cat output && + grep -v index output | test_decode_color >actual && + cat >expect <<-\EOF && + <BOLD>diff --git a/old.txt b/new.txt<RESET> + <BOLD>--- a/old.txt<RESET> + <BOLD>+++ b/new.txt<RESET> + <CYAN>@@ -1,13 +1,15 @@<RESET> + <BOLD;MAGENTA>-this line should be marked as moved<RESET> + <GREEN>+<RESET><GREEN>too short<RESET> + unchanged<RESET> + unchanged<RESET> + <BOLD;CYAN>+<RESET><BOLD;CYAN>this line should be marked as moved<RESET> + <GREEN>+<RESET><GREEN>too short<RESET> + unchanged<RESET> + unchanged<RESET> + <RED>-too short<RESET> + <BOLD;MAGENTA>-this line should be marked as oldMoved newMoved<RESET> + <BOLD;BLUE>-this line should be marked as oldMovedAlternate newMoved<RESET> + <BOLD;CYAN>+<RESET><BOLD;CYAN>this line should be marked as oldMoved newMoved/newMovedAlternate<RESET> + unchanged 1<RESET> + unchanged 2<RESET> + <BOLD;CYAN>+<RESET><BOLD;CYAN>this line should be marked as oldMovedAlternate newMoved<RESET> + <BOLD;YELLOW>+<RESET><BOLD;YELLOW>this line should be marked as oldMoved newMoved/newMovedAlternate<RESET> + unchanged 3<RESET> + <BOLD;CYAN>+<RESET><BOLD;CYAN>this line should be marked as oldMoved newMoved<RESET> + unchanged 4<RESET> + <BOLD;MAGENTA>-this line should be marked as oldMoved newMoved/newMovedAlternate<RESET> + EOF + test_cmp expect actual +' + test_expect_success 'cmd option assumes configured colored-moved' ' test_config color.diff.oldMoved "magenta" && test_config color.diff.newMoved "cyan" && @@ -1833,6 +1970,52 @@ test_expect_success '--color-moved treats adjacent blocks as separate for MIN_AL test_cmp expected actual ' +test_expect_success '--color-moved rewinds for MIN_ALNUM_COUNT' ' + git reset --hard && + test_write_lines >file \ + A B C one two three four five six seven D E F G H I J && + git add file && + test_write_lines >file \ + one two A B C D E F G H I J two three four five six seven && + git diff --color-moved=zebra -- file && + + git diff --color-moved=zebra --color -- file >actual.raw && + grep -v "index" actual.raw | test_decode_color >actual && + cat >expected <<-\EOF && + <BOLD>diff --git a/file b/file<RESET> + <BOLD>--- a/file<RESET> + <BOLD>+++ b/file<RESET> + <CYAN>@@ -1,13 +1,8 @@<RESET> + <GREEN>+<RESET><GREEN>one<RESET> + <GREEN>+<RESET><GREEN>two<RESET> + A<RESET> + B<RESET> + C<RESET> + <RED>-one<RESET> + <BOLD;MAGENTA>-two<RESET> + <BOLD;MAGENTA>-three<RESET> + <BOLD;MAGENTA>-four<RESET> + <BOLD;MAGENTA>-five<RESET> + <BOLD;MAGENTA>-six<RESET> + <BOLD;MAGENTA>-seven<RESET> + D<RESET> + E<RESET> + F<RESET> + <CYAN>@@ -15,3 +10,9 @@<RESET> <RESET>G<RESET> + H<RESET> + I<RESET> + J<RESET> + <BOLD;CYAN>+<RESET><BOLD;CYAN>two<RESET> + <BOLD;CYAN>+<RESET><BOLD;CYAN>three<RESET> + <BOLD;CYAN>+<RESET><BOLD;CYAN>four<RESET> + <BOLD;CYAN>+<RESET><BOLD;CYAN>five<RESET> + <BOLD;CYAN>+<RESET><BOLD;CYAN>six<RESET> + <BOLD;CYAN>+<RESET><BOLD;CYAN>seven<RESET> + EOF + + test_cmp expected actual +' + test_expect_success 'move detection with submodules' ' test_create_repo bananas && echo ripe >bananas/recipe && @@ -2023,10 +2206,10 @@ EMPTY='' test_expect_success 'compare mixed whitespace delta across moved blocks' ' git reset --hard && - tr Q_ "\t " <<-EOF >text.txt && - ${EMPTY} - ____too short without - ${EMPTY} + tr "^|Q_" "\f\v\t " <<-EOF >text.txt && + ^__ + |____too short without + ^ ___being grouped across blank line ${EMPTY} context @@ -2045,7 +2228,7 @@ test_expect_success 'compare mixed whitespace delta across moved blocks' ' git add text.txt && git commit -m "add text.txt" && - tr Q_ "\t " <<-EOF >text.txt && + tr "^|Q_" "\f\v\t " <<-EOF >text.txt && context lines to @@ -2056,7 +2239,7 @@ test_expect_success 'compare mixed whitespace delta across moved blocks' ' ${EMPTY} QQtoo short without ${EMPTY} - Q_______being grouped across blank line + ^Q_______being grouped across blank line ${EMPTY} Q_QThese two lines have had their indentation reduced by four spaces @@ -2068,16 +2251,16 @@ test_expect_success 'compare mixed whitespace delta across moved blocks' ' -c core.whitespace=space-before-tab \ diff --color --color-moved --ws-error-highlight=all \ --color-moved-ws=allow-indentation-change >actual.raw && - grep -v "index" actual.raw | test_decode_color >actual && + grep -v "index" actual.raw | tr "\f\v" "^|" | test_decode_color >actual && cat <<-\EOF >expected && <BOLD>diff --git a/text.txt b/text.txt<RESET> <BOLD>--- a/text.txt<RESET> <BOLD>+++ b/text.txt<RESET> <CYAN>@@ -1,16 +1,16 @@<RESET> - <BOLD;MAGENTA>-<RESET> - <BOLD;MAGENTA>-<RESET><BOLD;MAGENTA> too short without<RESET> - <BOLD;MAGENTA>-<RESET> + <BOLD;MAGENTA>-<RESET><BOLD;MAGENTA>^<RESET><BRED> <RESET> + <BOLD;MAGENTA>-<RESET><BOLD;MAGENTA>| too short without<RESET> + <BOLD;MAGENTA>-<RESET><BOLD;MAGENTA>^<RESET> <BOLD;MAGENTA>-<RESET><BOLD;MAGENTA> being grouped across blank line<RESET> <BOLD;MAGENTA>-<RESET> <RESET>context<RESET> @@ -2097,7 +2280,7 @@ test_expect_success 'compare mixed whitespace delta across moved blocks' ' <BOLD;YELLOW>+<RESET> <BOLD;YELLOW>+<RESET> <BOLD;YELLOW>too short without<RESET> <BOLD;YELLOW>+<RESET> - <BOLD;YELLOW>+<RESET> <BOLD;YELLOW> being grouped across blank line<RESET> + <BOLD;YELLOW>+<RESET><BOLD;YELLOW>^ being grouped across blank line<RESET> <BOLD;YELLOW>+<RESET> <BOLD;CYAN>+<RESET> <BRED> <RESET> <BOLD;CYAN>These two lines have had their<RESET> <BOLD;CYAN>+<RESET><BOLD;CYAN>indentation reduced by four spaces<RESET> diff --git a/t/t4018-diff-funcname.sh b/t/t4018-diff-funcname.sh index 740696c8f7..42a2b9a13b 100755 --- a/t/t4018-diff-funcname.sh +++ b/t/t4018-diff-funcname.sh @@ -75,7 +75,7 @@ test_expect_success 'last regexp must not be negated' ' test_expect_success 'setup hunk header tests' ' for i in $diffpatterns do - echo "$i-* diff=$i" + echo "$i-* diff=$i" || return 1 done > .gitattributes && # add all test files to the index diff --git a/t/t4019-diff-wserror.sh b/t/t4019-diff-wserror.sh index c68729ac09..d2b3109c2d 100755 --- a/t/t4019-diff-wserror.sh +++ b/t/t4019-diff-wserror.sh @@ -287,9 +287,9 @@ test_expect_success 'do not color trailing cr in context' ' ' test_expect_success 'color new trailing blank lines' ' - { echo a; echo b; echo; echo; } >x && + test_write_lines a b "" "" >x && git add x && - { echo a; echo; echo; echo; echo c; echo; echo; echo; echo; } >x && + test_write_lines a "" "" "" c "" "" "" "" >x && git diff --color x >output && cnt=$($grep_a "${blue_grep}" output | wc -l) && test $cnt = 2 diff --git a/t/t4020-diff-external.sh b/t/t4020-diff-external.sh index e009826fcb..54bb8ef27e 100755 --- a/t/t4020-diff-external.sh +++ b/t/t4020-diff-external.sh @@ -2,6 +2,7 @@ test_description='external diff interface test' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success setup ' diff --git a/t/t4023-diff-rename-typechange.sh b/t/t4023-diff-rename-typechange.sh index 47d6f35dcc..7cb9909293 100755 --- a/t/t4023-diff-rename-typechange.sh +++ b/t/t4023-diff-rename-typechange.sh @@ -55,7 +55,7 @@ test_expect_success 'cross renames to be detected for regular files' ' git diff-tree five six -r --name-status -B -M | sort >actual && { - echo "R100 foo bar" + echo "R100 foo bar" && echo "R100 bar foo" } | sort >expect && test_cmp expect actual @@ -66,7 +66,7 @@ test_expect_success 'cross renames to be detected for typechange' ' git diff-tree one two -r --name-status -B -M | sort >actual && { - echo "R100 foo bar" + echo "R100 foo bar" && echo "R100 bar foo" } | sort >expect && test_cmp expect actual @@ -78,7 +78,7 @@ test_expect_success 'moves and renames' ' git diff-tree three four -r --name-status -B -M | sort >actual && { # see -B -M (#6) in t4008 - echo "C100 foo bar" + echo "C100 foo bar" && echo "T100 foo" } | sort >expect && test_cmp expect actual diff --git a/t/t4024-diff-optimize-common.sh b/t/t4024-diff-optimize-common.sh index 6b44ce1493..e2f0eca4af 100755 --- a/t/t4024-diff-optimize-common.sh +++ b/t/t4024-diff-optimize-common.sh @@ -2,6 +2,7 @@ test_description='common tail optimization' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh z=zzzzzzzz ;# 8 @@ -148,7 +149,7 @@ test_expect_success 'diff -U0' ' for n in $sample do - git diff -U0 file-?$n + git diff -U0 file-?$n || return 1 done | zc >actual && test_cmp expect actual diff --git a/t/t4025-hunk-header.sh b/t/t4025-hunk-header.sh index 6356961de4..5397cb7d42 100755 --- a/t/t4025-hunk-header.sh +++ b/t/t4025-hunk-header.sh @@ -14,15 +14,9 @@ test_expect_success setup ' ( echo "A $NS" && - for c in B C D E F G H I J K - do - echo " $c" - done && + printf " %s\n" B C D E F G H I J K && echo "L $NS" && - for c in M N O P Q R S T U V - do - echo " $c" - done + printf " %s\n" M N O P Q R S T U V ) >file && git add file && diff --git a/t/t4026-color.sh b/t/t4026-color.sh index cc73161b46..cc3f60d468 100755 --- a/t/t4026-color.sh +++ b/t/t4026-color.sh @@ -60,6 +60,10 @@ test_expect_success 'fg bg attr...' ' color "blue bold dim ul blink reverse" "[1;2;4;5;7;34m" ' +test_expect_success 'reset fg bg attr...' ' + color "reset blue bold dim ul blink reverse" "[;1;2;4;5;7;34m" +' + # note that nobold and nodim are the same code (22) test_expect_success 'attr negation' ' color "nobold nodim noul noblink noreverse" "[22;24;25;27m" @@ -96,6 +100,18 @@ test_expect_success '24-bit colors' ' color "#ff00ff black" "[38;2;255;0;255;40m" ' +test_expect_success '"default" foreground' ' + color "default" "[39m" +' + +test_expect_success '"normal default" to clear background' ' + color "normal default" "[49m" +' + +test_expect_success '"default" can be combined with attributes' ' + color "default default no-reverse bold" "[1;27;39;49m" +' + test_expect_success '"normal" yields no color at all"' ' color "normal black" "[40m" ' diff --git a/t/t4027-diff-submodule.sh b/t/t4027-diff-submodule.sh index 94ef77e1df..6cef0da982 100755 --- a/t/t4027-diff-submodule.sh +++ b/t/t4027-diff-submodule.sh @@ -2,6 +2,7 @@ test_description='difference in submodules' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh . "$TEST_DIRECTORY"/lib-diff.sh diff --git a/t/t4029-diff-trailing-space.sh b/t/t4029-diff-trailing-space.sh index 32b6e9a4e7..5f8ffef74b 100755 --- a/t/t4029-diff-trailing-space.sh +++ b/t/t4029-diff-trailing-space.sh @@ -4,6 +4,7 @@ # test_description='diff honors config option, diff.suppressBlankEmpty' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh cat <<\EOF >expected || diff --git a/t/t4032-diff-inter-hunk-context.sh b/t/t4032-diff-inter-hunk-context.sh index bada0cbd32..7db92d0d9f 100755 --- a/t/t4032-diff-inter-hunk-context.sh +++ b/t/t4032-diff-inter-hunk-context.sh @@ -2,6 +2,7 @@ test_description='diff hunk fusing' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh f() { diff --git a/t/t4033-diff-patience.sh b/t/t4033-diff-patience.sh index 113304dc59..f7be7f5ef0 100755 --- a/t/t4033-diff-patience.sh +++ b/t/t4033-diff-patience.sh @@ -2,6 +2,7 @@ test_description='patience diff algorithm' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh . "$TEST_DIRECTORY"/lib-diff-alternative.sh diff --git a/t/t4034-diff-words.sh b/t/t4034-diff-words.sh index 561c582d16..d5abcf4b4c 100755 --- a/t/t4034-diff-words.sh +++ b/t/t4034-diff-words.sh @@ -2,6 +2,7 @@ test_description='word diff colors' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh . "$TEST_DIRECTORY"/lib-diff.sh diff --git a/t/t4035-diff-quiet.sh b/t/t4035-diff-quiet.sh index 0352bf81a9..76f8034c60 100755 --- a/t/t4035-diff-quiet.sh +++ b/t/t4035-diff-quiet.sh @@ -2,6 +2,7 @@ test_description='Return value of diffs' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success 'setup' ' diff --git a/t/t4037-diff-r-t-dirs.sh b/t/t4037-diff-r-t-dirs.sh index f5ce3b29a2..b5f96fe23b 100755 --- a/t/t4037-diff-r-t-dirs.sh +++ b/t/t4037-diff-r-t-dirs.sh @@ -2,6 +2,7 @@ test_description='diff -r -t shows directory additions and deletions' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success setup ' diff --git a/t/t4038-diff-combined.sh b/t/t4038-diff-combined.sh index aeac203c42..9a292bac70 100755 --- a/t/t4038-diff-combined.sh +++ b/t/t4038-diff-combined.sh @@ -100,7 +100,7 @@ test_expect_success 'setup for --cc --raw' ' for i in $(test_seq 1 40) do blob=$(echo file$i | git hash-object --stdin -w) && - trees="$trees$(echo "100644 blob $blob file" | git mktree)$LF" + trees="$trees$(echo "100644 blob $blob file" | git mktree)$LF" || return 1 done ' diff --git a/t/t4040-whitespace-status.sh b/t/t4040-whitespace-status.sh index 3c728a3ebf..e70e020ae9 100755 --- a/t/t4040-whitespace-status.sh +++ b/t/t4040-whitespace-status.sh @@ -1,6 +1,8 @@ #!/bin/sh test_description='diff --exit-code with whitespace' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success setup ' diff --git a/t/t4046-diff-unmerged.sh b/t/t4046-diff-unmerged.sh index ff7cfd884a..0ae0cd3a52 100755 --- a/t/t4046-diff-unmerged.sh +++ b/t/t4046-diff-unmerged.sh @@ -1,6 +1,8 @@ #!/bin/sh test_description='diff with unmerged index entries' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success setup ' @@ -18,7 +20,7 @@ test_expect_success setup ' for t in o x do path="$b$o$t" && - case "$path" in ooo) continue ;; esac + case "$path" in ooo) continue ;; esac && paths="$paths$path " && p=" $path" && case "$b" in x) echo "$m1$p" ;; esac && @@ -37,7 +39,7 @@ test_expect_success 'diff-files -0' ' for path in $paths do >"$path" && - echo ":000000 100644 $ZERO_OID $ZERO_OID U $path" + echo ":000000 100644 $ZERO_OID $ZERO_OID U $path" || return 1 done >diff-files-0.expect && git diff-files -0 >diff-files-0.actual && test_cmp diff-files-0.expect diff-files-0.actual @@ -50,7 +52,7 @@ test_expect_success 'diff-files -1' ' echo ":000000 100644 $ZERO_OID $ZERO_OID U $path" && case "$path" in x??) echo ":100644 100644 $blob1 $ZERO_OID M $path" - esac + esac || return 1 done >diff-files-1.expect && git diff-files -1 >diff-files-1.actual && test_cmp diff-files-1.expect diff-files-1.actual @@ -63,7 +65,7 @@ test_expect_success 'diff-files -2' ' echo ":000000 100644 $ZERO_OID $ZERO_OID U $path" && case "$path" in ?x?) echo ":100644 100644 $blob2 $ZERO_OID M $path" - esac + esac || return 1 done >diff-files-2.expect && git diff-files -2 >diff-files-2.actual && test_cmp diff-files-2.expect diff-files-2.actual && @@ -78,7 +80,7 @@ test_expect_success 'diff-files -3' ' echo ":000000 100644 $ZERO_OID $ZERO_OID U $path" && case "$path" in ??x) echo ":100644 100644 $blob3 $ZERO_OID M $path" - esac + esac || return 1 done >diff-files-3.expect && git diff-files -3 >diff-files-3.actual && test_cmp diff-files-3.expect diff-files-3.actual diff --git a/t/t4049-diff-stat-count.sh b/t/t4049-diff-stat-count.sh index 53061b104e..0a4fc735d4 100755 --- a/t/t4049-diff-stat-count.sh +++ b/t/t4049-diff-stat-count.sh @@ -2,6 +2,8 @@ # Copyright (c) 2011, Google Inc. test_description='diff --stat-count' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success 'setup' ' @@ -51,7 +53,7 @@ test_expect_success 'exclude unmerged entries from total file count' ' git rm -f d && for stage in 1 2 3 do - sed -e "s/ 0 a/ $stage d/" x + sed -e "s/ 0 a/ $stage d/" x || return 1 done | git update-index --index-info && echo d >d && diff --git a/t/t4050-diff-histogram.sh b/t/t4050-diff-histogram.sh index fd3e86a74f..c61b30f96d 100755 --- a/t/t4050-diff-histogram.sh +++ b/t/t4050-diff-histogram.sh @@ -2,6 +2,7 @@ test_description='histogram diff algorithm' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh . "$TEST_DIRECTORY"/lib-diff-alternative.sh diff --git a/t/t4052-stat-output.sh b/t/t4052-stat-output.sh index 9eba436211..b5c281edaa 100755 --- a/t/t4052-stat-output.sh +++ b/t/t4052-stat-output.sh @@ -101,7 +101,7 @@ test_expect_success 'preparation for big change tests' ' i=0 && while test $i -lt 1000 do - echo $i && i=$(($i + 1)) + echo $i && i=$(($i + 1)) || return 1 done >abcd && git commit -m message abcd ' diff --git a/t/t4054-diff-bogus-tree.sh b/t/t4054-diff-bogus-tree.sh index 8c95f152b2..294fb55313 100755 --- a/t/t4054-diff-bogus-tree.sh +++ b/t/t4054-diff-bogus-tree.sh @@ -1,6 +1,8 @@ #!/bin/sh test_description='test diff with a bogus tree containing the null sha1' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success 'create bogus tree' ' diff --git a/t/t4057-diff-combined-paths.sh b/t/t4057-diff-combined-paths.sh index 7e5b74f72e..04b8a1542a 100755 --- a/t/t4057-diff-combined-paths.sh +++ b/t/t4057-diff-combined-paths.sh @@ -18,13 +18,13 @@ test_expect_success 'trivial merge - combine-diff empty' ' for i in $(test_seq 1 9) do echo $i >$i.txt && - git add $i.txt + git add $i.txt || return 1 done && git commit -m "init" && git checkout -b side && for i in $(test_seq 2 9) do - echo $i/2 >>$i.txt + echo $i/2 >>$i.txt || return 1 done && git commit -a -m "side 2-9" && git checkout main && @@ -40,14 +40,14 @@ test_expect_success 'only one truly conflicting path' ' git checkout side && for i in $(test_seq 2 9) do - echo $i/3 >>$i.txt + echo $i/3 >>$i.txt || return 1 done && echo "4side" >>4.txt && git commit -a -m "side 2-9 +4" && git checkout main && for i in $(test_seq 1 9) do - echo $i/3 >>$i.txt + echo $i/3 >>$i.txt || return 1 done && echo "4main" >>4.txt && git commit -a -m "main 1-9 +4" && @@ -69,13 +69,13 @@ test_expect_success 'merge introduces new file' ' git checkout side && for i in $(test_seq 5 9) do - echo $i/4 >>$i.txt + echo $i/4 >>$i.txt || return 1 done && git commit -a -m "side 5-9" && git checkout main && for i in $(test_seq 1 3) do - echo $i/4 >>$i.txt + echo $i/4 >>$i.txt || return 1 done && git commit -a -m "main 1-3 +4hello" && git merge side && @@ -90,13 +90,13 @@ test_expect_success 'merge removed a file' ' git checkout side && for i in $(test_seq 5 9) do - echo $i/5 >>$i.txt + echo $i/5 >>$i.txt || return 1 done && git commit -a -m "side 5-9" && git checkout main && for i in $(test_seq 1 3) do - echo $i/4 >>$i.txt + echo $i/4 >>$i.txt || return 1 done && git commit -a -m "main 1-3" && git merge side && diff --git a/t/t4062-diff-pickaxe.sh b/t/t4062-diff-pickaxe.sh index 1130c8019b..9aaa068ed9 100755 --- a/t/t4062-diff-pickaxe.sh +++ b/t/t4062-diff-pickaxe.sh @@ -5,6 +5,7 @@ test_description='Pickaxe options' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success setup ' diff --git a/t/t4063-diff-blobs.sh b/t/t4063-diff-blobs.sh index bc69e26c52..7e6c9d6384 100755 --- a/t/t4063-diff-blobs.sh +++ b/t/t4063-diff-blobs.sh @@ -1,6 +1,8 @@ #!/bin/sh test_description='test direct comparison of blobs via git-diff' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh run_diff () { diff --git a/t/t4100-apply-stat.sh b/t/t4100-apply-stat.sh index 9b433de836..d503547732 100755 --- a/t/t4100-apply-stat.sh +++ b/t/t4100-apply-stat.sh @@ -6,6 +6,8 @@ test_description='git apply --stat --summary test, with --recount ' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh UNC='s/^\(@@ -[1-9][0-9]*\),[0-9]* \(+[1-9][0-9]*\),[0-9]* @@/\1,999 \2,999 @@/' diff --git a/t/t4101-apply-nonl.sh b/t/t4101-apply-nonl.sh index e3443d004d..b1169193ef 100755 --- a/t/t4101-apply-nonl.sh +++ b/t/t4101-apply-nonl.sh @@ -6,6 +6,8 @@ test_description='git apply should handle files with incomplete lines. ' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh # setup diff --git a/t/t4102-apply-rename.sh b/t/t4102-apply-rename.sh index fae305979a..d1e06fc1ac 100755 --- a/t/t4102-apply-rename.sh +++ b/t/t4102-apply-rename.sh @@ -6,6 +6,8 @@ test_description='git apply handling copy/rename patch. ' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh # setup diff --git a/t/t4105-apply-fuzz.sh b/t/t4105-apply-fuzz.sh index 3266e39400..ed814a839e 100755 --- a/t/t4105-apply-fuzz.sh +++ b/t/t4105-apply-fuzz.sh @@ -2,6 +2,8 @@ test_description='apply with fuzz and offset' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh dotest () { @@ -15,15 +17,9 @@ dotest () { test_expect_success setup ' - for i in 1 2 3 4 5 6 7 8 9 10 11 12 - do - echo $i - done >file && + test_write_lines 1 2 3 4 5 6 7 8 9 10 11 12 >file && git update-index --add file && - for i in 1 2 3 4 5 6 7 a b c d e 8 9 10 11 12 - do - echo $i - done >file && + test_write_lines 1 2 3 4 5 6 7 a b c d e 8 9 10 11 12 >file && cat file >expect && git diff >O0.diff && diff --git a/t/t4106-apply-stdin.sh b/t/t4106-apply-stdin.sh index 72467a1e8e..5c150f3b0b 100755 --- a/t/t4106-apply-stdin.sh +++ b/t/t4106-apply-stdin.sh @@ -2,6 +2,8 @@ test_description='git apply --numstat - <patch' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success setup ' @@ -18,7 +20,10 @@ test_expect_success 'git apply --numstat - < patch' ' ' test_expect_success 'git apply --numstat - < patch patch' ' - for i in 1 2; do echo "1 1 text"; done >expect && + cat >expect <<-\EOF && + 1 1 text + 1 1 text + EOF git apply --numstat - < patch patch >actual && test_cmp expect actual ' diff --git a/t/t4109-apply-multifrag.sh b/t/t4109-apply-multifrag.sh index ac58083fe2..4dc6d8e7d3 100755 --- a/t/t4109-apply-multifrag.sh +++ b/t/t4109-apply-multifrag.sh @@ -6,6 +6,8 @@ test_description='git apply test patches with multiple fragments.' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh cp "$TEST_DIRECTORY/t4109/patch1.patch" . diff --git a/t/t4110-apply-scan.sh b/t/t4110-apply-scan.sh index 09f58112e0..266302a182 100755 --- a/t/t4110-apply-scan.sh +++ b/t/t4110-apply-scan.sh @@ -7,6 +7,8 @@ test_description='git apply test for patches which require scanning forwards and backwards. ' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success 'git apply scan' ' diff --git a/t/t4112-apply-renames.sh b/t/t4112-apply-renames.sh index f9ad183758..d53aa4222e 100755 --- a/t/t4112-apply-renames.sh +++ b/t/t4112-apply-renames.sh @@ -7,6 +7,8 @@ test_description='git apply should not get confused with rename/copy. ' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh # setup diff --git a/t/t4115-apply-symlink.sh b/t/t4115-apply-symlink.sh index 872fcda6cb..d0f3edef54 100755 --- a/t/t4115-apply-symlink.sh +++ b/t/t4115-apply-symlink.sh @@ -7,6 +7,7 @@ test_description='git apply symlinks and partial files ' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success setup ' diff --git a/t/t4116-apply-reverse.sh b/t/t4116-apply-reverse.sh index b99e65c086..a9f4ddda6c 100755 --- a/t/t4116-apply-reverse.sh +++ b/t/t4116-apply-reverse.sh @@ -7,18 +7,20 @@ test_description='git apply in reverse ' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success setup ' - for i in a b c d e f g h i j k l m n; do echo $i; done >file1 && + test_write_lines a b c d e f g h i j k l m n >file1 && perl -pe "y/ijk/\\000\\001\\002/" <file1 >file2 && git add file1 file2 && git commit -m initial && git tag initial && - for i in a b c g h i J K L m o n p q; do echo $i; done >file1 && + test_write_lines a b c g h i J K L m o n p q >file1 && perl -pe "y/mon/\\000\\001\\002/" <file1 >file2 && git commit -a -m second && diff --git a/t/t4117-apply-reject.sh b/t/t4117-apply-reject.sh index 0ee93fe845..c86d05a96f 100755 --- a/t/t4117-apply-reject.sh +++ b/t/t4117-apply-reject.sh @@ -10,25 +10,16 @@ test_description='git apply with rejects . ./test-lib.sh test_expect_success setup ' - for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 - do - echo $i - done >file1 && + test_write_lines 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 >file1 && cat file1 >saved.file1 && git update-index --add file1 && git commit -m initial && - for i in 1 2 A B 4 5 6 7 8 9 10 11 12 C 13 14 15 16 17 18 19 20 D 21 - do - echo $i - done >file1 && + test_write_lines 1 2 A B 4 5 6 7 8 9 10 11 12 C 13 14 15 16 17 18 19 20 D 21 >file1 && git diff >patch.1 && cat file1 >clean && - for i in 1 E 2 3 4 5 6 7 8 9 10 11 12 C 13 14 15 16 17 18 19 20 F 21 - do - echo $i - done >expected && + test_write_lines 1 E 2 3 4 5 6 7 8 9 10 11 12 C 13 14 15 16 17 18 19 20 F 21 >expected && mv file1 file2 && git update-index --add --remove file1 file2 && @@ -38,10 +29,7 @@ test_expect_success setup ' mv saved.file1 file1 && git update-index --add --remove file1 file2 && - for i in 1 E 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 F 21 - do - echo $i - done >file1 && + test_write_lines 1 E 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 F 21 >file1 && cat file1 >saved.file1 ' diff --git a/t/t4118-apply-empty-context.sh b/t/t4118-apply-empty-context.sh index 65f2e4c3ef..69c9c48e72 100755 --- a/t/t4118-apply-empty-context.sh +++ b/t/t4118-apply-empty-context.sh @@ -7,14 +7,12 @@ test_description='git apply with new style GNU diff with empty context ' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success setup ' - { - echo; echo; - echo A; echo B; echo C; - echo; - } >file1 && + test_write_lines "" "" A B C "" >file1 && cat file1 >file1.orig && { cat file1 && diff --git a/t/t4119-apply-config.sh b/t/t4119-apply-config.sh index a9a0583811..208c961d37 100755 --- a/t/t4119-apply-config.sh +++ b/t/t4119-apply-config.sh @@ -7,6 +7,8 @@ test_description='git apply --whitespace=strip and configuration file. ' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success setup ' diff --git a/t/t4121-apply-diffs.sh b/t/t4121-apply-diffs.sh index b45454aaf4..a80cec9d11 100755 --- a/t/t4121-apply-diffs.sh +++ b/t/t4121-apply-diffs.sh @@ -4,6 +4,7 @@ test_description='git apply for contextually independent diffs' GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh echo '1 diff --git a/t/t4123-apply-shrink.sh b/t/t4123-apply-shrink.sh index 984157f03b..dfa053ff28 100755 --- a/t/t4123-apply-shrink.sh +++ b/t/t4123-apply-shrink.sh @@ -2,6 +2,8 @@ test_description='apply a patch that is larger than the preimage' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh cat >F <<\EOF @@ -45,8 +47,8 @@ test_expect_success 'apply should fail gracefully' ' echo Oops, should not have succeeded false else - status=$? - echo "Status was $status" + status=$? && + echo "Status was $status" && if test -f .git/index.lock then echo Oops, should not have crashed diff --git a/t/t4124-apply-ws-rule.sh b/t/t4124-apply-ws-rule.sh index 0ca29821ec..485c7d2d12 100755 --- a/t/t4124-apply-ws-rule.sh +++ b/t/t4124-apply-ws-rule.sh @@ -230,10 +230,10 @@ test_expect_success 'blank at EOF with --whitespace=fix (1)' ' test_might_fail git config --unset core.whitespace && rm -f .gitattributes && - { echo a; echo b; echo c; } >one && + test_write_lines a b c >one && git add one && - { echo a; echo b; echo c; } >expect && - { cat expect; echo; } >one && + test_write_lines a b c >expect && + { cat expect && echo; } >one && git diff -- one >patch && git checkout one && @@ -242,10 +242,10 @@ test_expect_success 'blank at EOF with --whitespace=fix (1)' ' ' test_expect_success 'blank at EOF with --whitespace=fix (2)' ' - { echo a; echo b; echo c; } >one && + test_write_lines a b c >one && git add one && - { echo a; echo c; } >expect && - { cat expect; echo; echo; } >one && + test_write_lines a b >expect && + { cat expect && test_write_lines "" ""; } >one && git diff -- one >patch && git checkout one && @@ -254,10 +254,10 @@ test_expect_success 'blank at EOF with --whitespace=fix (2)' ' ' test_expect_success 'blank at EOF with --whitespace=fix (3)' ' - { echo a; echo b; echo; } >one && + test_write_lines a b "" >one && git add one && - { echo a; echo c; echo; } >expect && - { cat expect; echo; echo; } >one && + test_write_lines a c "" >expect && + { cat expect && test_write_lines "" ""; } >one && git diff -- one >patch && git checkout one && @@ -266,9 +266,9 @@ test_expect_success 'blank at EOF with --whitespace=fix (3)' ' ' test_expect_success 'blank at end of hunk, not at EOF with --whitespace=fix' ' - { echo a; echo b; echo; echo; echo; echo; echo; echo d; } >one && + test_write_lines a b "" "" "" "" "" d >one && git add one && - { echo a; echo c; echo; echo; echo; echo; echo; echo; echo d; } >expect && + test_write_lines a b "" "" "" "" "" "" d >expect && cp expect one && git diff -- one >patch && @@ -278,7 +278,7 @@ test_expect_success 'blank at end of hunk, not at EOF with --whitespace=fix' ' ' test_expect_success 'blank at EOF with --whitespace=warn' ' - { echo a; echo b; echo c; } >one && + test_write_lines a b c >one && git add one && echo >>one && cat one >expect && @@ -291,7 +291,7 @@ test_expect_success 'blank at EOF with --whitespace=warn' ' ' test_expect_success 'blank at EOF with --whitespace=error' ' - { echo a; echo b; echo c; } >one && + test_write_lines a b c >one && git add one && cat one >expect && echo >>one && @@ -304,7 +304,7 @@ test_expect_success 'blank at EOF with --whitespace=error' ' ' test_expect_success 'blank but not empty at EOF' ' - { echo a; echo b; echo c; } >one && + test_write_lines a b c >one && git add one && echo " " >>one && cat one >expect && @@ -317,13 +317,13 @@ test_expect_success 'blank but not empty at EOF' ' ' test_expect_success 'applying beyond EOF requires one non-blank context line' ' - { echo; echo; echo; echo; } >one && + test_write_lines "" "" "" "" >one && git add one && - { echo b; } >>one && + echo b >>one && git diff -- one >patch && git checkout one && - { echo a; echo; } >one && + test_write_lines a "" >one && cp one expect && test_must_fail git apply --whitespace=fix patch && test_cmp expect one && @@ -333,7 +333,7 @@ test_expect_success 'applying beyond EOF requires one non-blank context line' ' test_expect_success 'tons of blanks at EOF should not apply' ' for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16; do - echo; echo; echo; echo; + test_write_lines "" "" "" "" || return 1 done >one && git add one && echo a >>one && @@ -362,9 +362,9 @@ test_expect_success 'missing blank line at end with --whitespace=fix' ' ' test_expect_success 'two missing blank lines at end with --whitespace=fix' ' - { echo a; echo; echo b; echo c; } >one && + test_write_lines a "" b c >one && cp one no-blank-lines && - { echo; echo; } >>one && + test_write_lines "" "" >>one && git add one && echo d >>one && cp one expect && @@ -381,9 +381,9 @@ test_expect_success 'two missing blank lines at end with --whitespace=fix' ' ' test_expect_success 'missing blank line at end, insert before end, --whitespace=fix' ' - { echo a; echo; } >one && + test_write_lines a "" >one && git add one && - { echo b; echo a; echo; } >one && + test_write_lines b a "" >one && cp one expect && git diff -- one >patch && echo a >one && @@ -393,10 +393,10 @@ test_expect_success 'missing blank line at end, insert before end, --whitespace= ' test_expect_success 'shrink file with tons of missing blanks at end of file' ' - { echo a; echo b; echo c; } >one && + test_write_lines a b c >one && cp one no-blank-lines && for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16; do - echo; echo; echo; echo; + test_write_lines "" "" "" "" || return 1 done >>one && git add one && echo a >one && @@ -412,9 +412,9 @@ test_expect_success 'shrink file with tons of missing blanks at end of file' ' ' test_expect_success 'missing blanks at EOF must only match blank lines' ' - { echo a; echo b; } >one && + test_write_lines a b >one && git add one && - { echo c; echo d; } >>one && + test_write_lines c d >>one && git diff -- one >patch && echo a >one && @@ -434,9 +434,9 @@ test_expect_success 'missing blank line should match context line with spaces' ' git add one && echo d >>one && git diff -- one >patch && - { echo a; echo b; echo c; } >one && + test_write_lines a b c >one && cp one expect && - { echo; echo d; } >>expect && + test_write_lines "" d >>expect && git add one && git apply --whitespace=fix patch && @@ -455,7 +455,7 @@ test_expect_success 'same, but with the --ignore-space-option' ' echo d >>one && cp one expect && git diff -- one >patch && - { echo a; echo b; echo c; } >one && + test_write_lines a b c >one && git add one && git checkout-index -f one && diff --git a/t/t4125-apply-ws-fuzz.sh b/t/t4125-apply-ws-fuzz.sh index 9671de7999..090987c89b 100755 --- a/t/t4125-apply-ws-fuzz.sh +++ b/t/t4125-apply-ws-fuzz.sh @@ -10,10 +10,7 @@ test_expect_success setup ' git add file && # file-0 is full of whitespace breakages - for l in a bb c d eeee f ggg h - do - echo "$l " - done >file-0 && + printf "%s \n" a bb c d eeee f ggg h >file-0 && # patch-0 creates a whitespace broken file cat file-0 >file && diff --git a/t/t4126-apply-empty.sh b/t/t4126-apply-empty.sh index ceb6a79fe0..33860d3829 100755 --- a/t/t4126-apply-empty.sh +++ b/t/t4126-apply-empty.sh @@ -2,6 +2,8 @@ test_description='apply empty' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success setup ' @@ -9,10 +11,9 @@ test_expect_success setup ' git add empty && test_tick && git commit -m initial && - for i in a b c d e - do - echo $i - done >empty && + git commit --allow-empty -m "empty commit" && + git format-patch --always HEAD~ >empty.patch && + test_write_lines a b c d e >empty && cat empty >expect && git diff | sed -e "/^diff --git/d" \ @@ -25,30 +26,42 @@ test_expect_success setup ' ' test_expect_success 'apply empty' ' - git reset --hard && rm -f missing && + test_when_finished "git reset --hard" && git apply patch0 && test_cmp expect empty ' +test_expect_success 'apply empty patch fails' ' + test_when_finished "git reset --hard" && + test_must_fail git apply empty.patch && + test_must_fail git apply - </dev/null +' + +test_expect_success 'apply with --allow-empty succeeds' ' + test_when_finished "git reset --hard" && + git apply --allow-empty empty.patch && + git apply --allow-empty - </dev/null +' + test_expect_success 'apply --index empty' ' - git reset --hard && rm -f missing && + test_when_finished "git reset --hard" && git apply --index patch0 && test_cmp expect empty && git diff --exit-code ' test_expect_success 'apply create' ' - git reset --hard && rm -f missing && + test_when_finished "git reset --hard" && git apply patch1 && test_cmp expect missing ' test_expect_success 'apply --index create' ' - git reset --hard && rm -f missing && + test_when_finished "git reset --hard" && git apply --index patch1 && test_cmp expect missing && git diff --exit-code diff --git a/t/t4127-apply-same-fn.sh b/t/t4127-apply-same-fn.sh index 305b7e649e..aa5cfae2b6 100755 --- a/t/t4127-apply-same-fn.sh +++ b/t/t4127-apply-same-fn.sh @@ -2,6 +2,8 @@ test_description='apply same filename' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh modify () { @@ -10,10 +12,7 @@ modify () { } test_expect_success setup ' - for i in a b c d e f g h i j k l m - do - echo $i - done >same_fn && + test_write_lines a b c d e f g h i j k l m >same_fn && cp same_fn other_fn && git add same_fn other_fn && git commit -m initial diff --git a/t/t4128-apply-root.sh b/t/t4128-apply-root.sh index 6cc741a634..cb3181e8b7 100755 --- a/t/t4128-apply-root.sh +++ b/t/t4128-apply-root.sh @@ -2,6 +2,8 @@ test_description='apply same filename' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success 'setup' ' diff --git a/t/t4129-apply-samemode.sh b/t/t4129-apply-samemode.sh index 576632f868..a1c7686519 100755 --- a/t/t4129-apply-samemode.sh +++ b/t/t4129-apply-samemode.sh @@ -2,6 +2,8 @@ test_description='applying patch with mode bits' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success setup ' diff --git a/t/t4130-apply-criss-cross-rename.sh b/t/t4130-apply-criss-cross-rename.sh index f8a313bcb9..f3ea632742 100755 --- a/t/t4130-apply-criss-cross-rename.sh +++ b/t/t4130-apply-criss-cross-rename.sh @@ -1,6 +1,8 @@ #!/bin/sh test_description='git apply handling criss-cross rename patch.' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh create_file() { diff --git a/t/t4132-apply-removal.sh b/t/t4132-apply-removal.sh index fec1d6fa51..c1e3049c04 100755 --- a/t/t4132-apply-removal.sh +++ b/t/t4132-apply-removal.sh @@ -4,6 +4,8 @@ test_description='git-apply notices removal patches generated by GNU diff' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success setup ' diff --git a/t/t4133-apply-filenames.sh b/t/t4133-apply-filenames.sh index c5ed3b17c4..35f1060bc8 100755 --- a/t/t4133-apply-filenames.sh +++ b/t/t4133-apply-filenames.sh @@ -5,6 +5,8 @@ test_description='git apply filename consistency check' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success setup ' diff --git a/t/t4134-apply-submodule.sh b/t/t4134-apply-submodule.sh index d1c16ba33c..aceb4c42b0 100755 --- a/t/t4134-apply-submodule.sh +++ b/t/t4134-apply-submodule.sh @@ -5,6 +5,8 @@ test_description='git apply submodule tests' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success setup ' diff --git a/t/t4136-apply-check.sh b/t/t4136-apply-check.sh index 4c3f264a63..dfec1c5f0f 100755 --- a/t/t4136-apply-check.sh +++ b/t/t4136-apply-check.sh @@ -2,6 +2,8 @@ test_description='git apply should exit non-zero with unrecognized input.' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success 'setup' ' diff --git a/t/t4138-apply-ws-expansion.sh b/t/t4138-apply-ws-expansion.sh index b19faeb67a..8bbf8260fa 100755 --- a/t/t4138-apply-ws-expansion.sh +++ b/t/t4138-apply-ws-expansion.sh @@ -29,8 +29,8 @@ test_expect_success setup ' x=1 && while test $x -lt $n do - printf "%63s%d\n" "" $x >>after - x=$(( $x + 1 )) + printf "%63s%d\n" "" $x >>after && + x=$(( $x + 1 )) || return 1 done && printf "\t%s\n" d e f >>after && test_expect_code 1 git diff --no-index before after >patch2.patch.raw && @@ -40,8 +40,8 @@ test_expect_success setup ' x=1 && while test $x -lt $n do - printf "%63s%d\n" "" $x >>expect-2 - x=$(( $x + 1 )) + printf "%63s%d\n" "" $x >>expect-2 && + x=$(( $x + 1 )) || return 1 done && printf "%64s\n" d e f >>expect-2 && @@ -52,8 +52,8 @@ test_expect_success setup ' x=0 && while test $x -lt $n do - printf "%63s%02d\n" "" $x >>after - x=$(( $x + 1 )) + printf "%63s%02d\n" "" $x >>after && + x=$(( $x + 1 )) || return 1 done && printf "\t%s\n" d e f >>after && test_expect_code 1 git diff --no-index before after >patch3.patch.raw && @@ -63,8 +63,8 @@ test_expect_success setup ' x=0 && while test $x -lt $n do - printf "%63s%02d\n" "" $x >>expect-3 - x=$(( $x + 1 )) + printf "%63s%02d\n" "" $x >>expect-3 && + x=$(( $x + 1 )) || return 1 done && printf "%64s\n" d e f >>expect-3 && @@ -73,16 +73,16 @@ test_expect_success setup ' x=0 && while test $x -lt 50 do - printf "\t%02d\n" $x >>before - x=$(( $x + 1 )) + printf "\t%02d\n" $x >>before && + x=$(( $x + 1 )) || return 1 done && cat before >after && printf "%64s\n" a b c >>after && while test $x -lt 100 do - printf "\t%02d\n" $x >>before - printf "\t%02d\n" $x >>after - x=$(( $x + 1 )) + printf "\t%02d\n" $x >>before && + printf "\t%02d\n" $x >>after && + x=$(( $x + 1 )) || return 1 done && test_expect_code 1 git diff --no-index before after >patch4.patch.raw && sed -e "s/before/test-4/" -e "s/after/test-4/" patch4.patch.raw >patch4.patch && @@ -90,16 +90,16 @@ test_expect_success setup ' x=0 && while test $x -lt 50 do - printf "%63s%02d\n" "" $x >>test-4 - x=$(( $x + 1 )) + printf "%63s%02d\n" "" $x >>test-4 && + x=$(( $x + 1 )) || return 1 done && cat test-4 >expect-4 && printf "%64s\n" a b c >>expect-4 && while test $x -lt 100 do - printf "%63s%02d\n" "" $x >>test-4 - printf "%63s%02d\n" "" $x >>expect-4 - x=$(( $x + 1 )) + printf "%63s%02d\n" "" $x >>test-4 && + printf "%63s%02d\n" "" $x >>expect-4 && + x=$(( $x + 1 )) || return 1 done && git config core.whitespace tab-in-indent,tabwidth=63 && diff --git a/t/t4139-apply-escape.sh b/t/t4139-apply-escape.sh index 45b5660a47..e5c7439df1 100755 --- a/t/t4139-apply-escape.sh +++ b/t/t4139-apply-escape.sh @@ -1,6 +1,8 @@ #!/bin/sh test_description='paths written by git-apply cannot escape the working tree' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh # tests will try to write to ../foo, and we do not diff --git a/t/t4150-am.sh b/t/t4150-am.sh index 2aaaa0d7de..6caff0ca39 100755 --- a/t/t4150-am.sh +++ b/t/t4150-am.sh @@ -116,7 +116,7 @@ test_expect_success setup ' git format-patch --stdout first | sed -e "1d" } | append_cr >patch1-crlf.eml && { - printf "%255s\\n" "" + printf "%255s\\n" "" && echo "X-Fake-Field: Line One" && echo "X-Fake-Field: Line Two" && echo "X-Fake-Field: Line Three" && @@ -196,6 +196,12 @@ test_expect_success setup ' git format-patch -M --stdout lorem^ >rename-add.patch && + git checkout -b empty-commit && + git commit -m "empty commit" --allow-empty && + + : >empty.patch && + git format-patch --always --stdout empty-commit^ >empty-commit.patch && + # reset time sane_unset test_tick && test_tick @@ -1152,4 +1158,105 @@ test_expect_success 'apply binary blob in partial clone' ' git -C client am ../patch ' +test_expect_success 'an empty input file is error regardless of --empty option' ' + test_when_finished "git am --abort || :" && + test_must_fail git am --empty=drop empty.patch 2>actual && + echo "Patch format detection failed." >expected && + test_cmp expected actual +' + +test_expect_success 'invalid when passing the --empty option alone' ' + test_when_finished "git am --abort || :" && + git checkout empty-commit^ && + test_must_fail git am --empty empty-commit.patch 2>err && + echo "error: Invalid value for --empty: empty-commit.patch" >expected && + test_cmp expected err +' + +test_expect_success 'a message without a patch is an error (default)' ' + test_when_finished "git am --abort || :" && + test_must_fail git am empty-commit.patch >err && + grep "Patch is empty" err +' + +test_expect_success 'a message without a patch is an error where an explicit "--empty=stop" is given' ' + test_when_finished "git am --abort || :" && + test_must_fail git am --empty=stop empty-commit.patch >err && + grep "Patch is empty." err +' + +test_expect_success 'a message without a patch will be skipped when "--empty=drop" is given' ' + git am --empty=drop empty-commit.patch >output && + git rev-parse empty-commit^ >expected && + git rev-parse HEAD >actual && + test_cmp expected actual && + grep "Skipping: empty commit" output +' + +test_expect_success 'record as an empty commit when meeting e-mail message that lacks a patch' ' + git am --empty=keep empty-commit.patch >output && + test_path_is_missing .git/rebase-apply && + git show empty-commit --format="%B" >expected && + git show HEAD --format="%B" >actual && + grep -f actual expected && + grep "Creating an empty commit: empty commit" output +' + +test_expect_success 'skip an empty patch in the middle of an am session' ' + git checkout empty-commit^ && + test_must_fail git am empty-commit.patch >err && + grep "Patch is empty." err && + grep "To record the empty patch as an empty commit, run \"git am --allow-empty\"." err && + git am --skip && + test_path_is_missing .git/rebase-apply && + git rev-parse empty-commit^ >expected && + git rev-parse HEAD >actual && + test_cmp expected actual +' + +test_expect_success 'record an empty patch as an empty commit in the middle of an am session' ' + git checkout empty-commit^ && + test_must_fail git am empty-commit.patch >err && + grep "Patch is empty." err && + grep "To record the empty patch as an empty commit, run \"git am --allow-empty\"." err && + git am --allow-empty >output && + grep "No changes - recorded it as an empty commit." output && + test_path_is_missing .git/rebase-apply && + git show empty-commit --format="%B" >expected && + git show HEAD --format="%B" >actual && + grep -f actual expected +' + +test_expect_success 'create an non-empty commit when the index IS changed though "--allow-empty" is given' ' + git checkout empty-commit^ && + test_must_fail git am empty-commit.patch >err && + : >empty-file && + git add empty-file && + git am --allow-empty && + git show empty-commit --format="%B" >expected && + git show HEAD --format="%B" >actual && + grep -f actual expected && + git diff HEAD^..HEAD --name-only +' + +test_expect_success 'cannot create empty commits when there is a clean index due to merge conflicts' ' + test_when_finished "git am --abort || :" && + git rev-parse HEAD >expected && + test_must_fail git am seq.patch && + test_must_fail git am --allow-empty >err && + ! grep "To record the empty patch as an empty commit, run \"git am --allow-empty\"." err && + git rev-parse HEAD >actual && + test_cmp actual expected +' + +test_expect_success 'cannot create empty commits when there is unmerged index due to merge conflicts' ' + test_when_finished "git am --abort || :" && + git rev-parse HEAD >expected && + test_must_fail git am -3 seq.patch && + test_must_fail git am --allow-empty >err && + ! grep "To record the empty patch as an empty commit, run \"git am --allow-empty\"." err && + git rev-parse HEAD >actual && + test_cmp actual expected +' + test_done diff --git a/t/t4151-am-abort.sh b/t/t4151-am-abort.sh index 2374151662..5ed7e22827 100755 --- a/t/t4151-am-abort.sh +++ b/t/t4151-am-abort.sh @@ -5,10 +5,7 @@ test_description='am --abort' . ./test-lib.sh test_expect_success setup ' - for i in a b c d e f g - do - echo $i - done >file-1 && + test_write_lines a b c d e f g >file-1 && cp file-1 file-2 && test_tick && git add file-1 file-2 && @@ -43,10 +40,7 @@ do test_must_fail git am$with3 000[1245]-*.patch && git log --pretty=tformat:%s >actual && - for i in 3 2 initial - do - echo $i - done >expect && + test_write_lines 3 2 initial >expect && test_cmp expect actual ' diff --git a/t/t4202-log.sh b/t/t4202-log.sh index 7884e3d46b..5049559861 100755 --- a/t/t4202-log.sh +++ b/t/t4202-log.sh @@ -120,48 +120,48 @@ test_expect_success 'diff-filter=A' ' test_expect_success 'diff-filter=M' ' - actual=$(git log --pretty="format:%s" --diff-filter=M HEAD) && - expect=$(echo second) && - verbose test "$actual" = "$expect" + git log --pretty="format:%s" --diff-filter=M HEAD >actual && + printf "second" >expect && + test_cmp expect actual ' test_expect_success 'diff-filter=D' ' - actual=$(git log --no-renames --pretty="format:%s" --diff-filter=D HEAD) && - expect=$(echo sixth ; echo third) && - verbose test "$actual" = "$expect" + git log --no-renames --pretty="format:%s" --diff-filter=D HEAD >actual && + printf "sixth\nthird" >expect && + test_cmp expect actual ' test_expect_success 'diff-filter=R' ' - actual=$(git log -M --pretty="format:%s" --diff-filter=R HEAD) && - expect=$(echo third) && - verbose test "$actual" = "$expect" + git log -M --pretty="format:%s" --diff-filter=R HEAD >actual && + printf "third" >expect && + test_cmp expect actual ' test_expect_success 'diff-filter=C' ' - actual=$(git log -C -C --pretty="format:%s" --diff-filter=C HEAD) && - expect=$(echo fourth) && - verbose test "$actual" = "$expect" + git log -C -C --pretty="format:%s" --diff-filter=C HEAD >actual && + printf "fourth" >expect && + test_cmp expect actual ' test_expect_success 'git log --follow' ' - actual=$(git log --follow --pretty="format:%s" ichi) && - expect=$(echo third ; echo second ; echo initial) && - verbose test "$actual" = "$expect" + git log --follow --pretty="format:%s" ichi >actual && + printf "third\nsecond\ninitial" >expect && + test_cmp expect actual ' test_expect_success 'git config log.follow works like --follow' ' test_config log.follow true && - actual=$(git log --pretty="format:%s" ichi) && - expect=$(echo third ; echo second ; echo initial) && - verbose test "$actual" = "$expect" + git log --pretty="format:%s" ichi >actual && + printf "third\nsecond\ninitial" >expect && + test_cmp expect actual ' test_expect_success 'git config log.follow does not die with multiple paths' ' @@ -176,9 +176,9 @@ test_expect_success 'git config log.follow does not die with no paths' ' test_expect_success 'git config log.follow is overridden by --no-follow' ' test_config log.follow true && - actual=$(git log --no-follow --pretty="format:%s" ichi) && - expect="third" && - verbose test "$actual" = "$expect" + git log --no-follow --pretty="format:%s" ichi >actual && + printf "third" >expect && + test_cmp expect actual ' # Note that these commits are intentionally listed out of order. @@ -250,7 +250,7 @@ test_expect_success 'log --invert-grep --grep' ' test_cmp expect actual && # POSIX extended - git -c grep.patternType=basic log --pretty="tformat:%s" --invert-grep --grep=t[h] --grep=S[e]c >actual && + git -c grep.patternType=extended log --pretty="tformat:%s" --invert-grep --grep=t[h] --grep=S[e]c >actual && test_cmp expect actual && # PCRE @@ -952,6 +952,43 @@ test_expect_success 'decorate-refs-exclude and simplify-by-decoration' ' test_cmp expect.decorate actual ' +test_expect_success 'decorate-refs with implied decorate from format' ' + cat >expect <<-\EOF && + side-2 (tag: side-2) + side-1 + EOF + git log --no-walk --format="%s%d" \ + --decorate-refs="*side-2" side-1 side-2 \ + >actual && + test_cmp expect actual +' + +test_expect_success 'implied decorate does not override option' ' + cat >expect <<-\EOF && + side-2 (tag: refs/tags/side-2, refs/heads/side) + side-1 (tag: refs/tags/side-1) + EOF + git log --no-walk --format="%s%d" \ + --decorate=full side-1 side-2 \ + >actual && + test_cmp expect actual +' + +test_expect_success 'decorate-refs and simplify-by-decoration without output' ' + cat >expect <<-\EOF && + side-2 + initial + EOF + # Do not just use a --format without %d here; we want to + # make sure that we did not accidentally turn on displaying + # the decorations, too. And that requires one of the regular + # formats. + git log --decorate-refs="*side-2" --oneline \ + --simplify-by-decoration >actual.raw && + sed "s/^[0-9a-f]* //" <actual.raw >actual && + test_cmp expect actual +' + test_expect_success 'log.decorate config parsing' ' git log --oneline --decorate=full >expect.full && git log --oneline --decorate=short >expect.short && @@ -1677,6 +1714,24 @@ test_expect_success GPGSSH 'setup sshkey signed branch' ' git commit -S -m signed_commit ' +test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'create signed commits with keys having defined lifetimes' ' + test_config gpg.format ssh && + touch file && + git add file && + + echo expired >file && test_tick && git commit -a -m expired -S"${GPGSSH_KEY_EXPIRED}" && + git tag expired-signed && + + echo notyetvalid >file && test_tick && git commit -a -m notyetvalid -S"${GPGSSH_KEY_NOTYETVALID}" && + git tag notyetvalid-signed && + + echo timeboxedvalid >file && test_tick && git commit -a -m timeboxedvalid -S"${GPGSSH_KEY_TIMEBOXEDVALID}" && + git tag timeboxedvalid-signed && + + echo timeboxedinvalid >file && test_tick && git commit -a -m timeboxedinvalid -S"${GPGSSH_KEY_TIMEBOXEDINVALID}" && + git tag timeboxedinvalid-signed +' + test_expect_success GPGSM 'log x509 fingerprint' ' echo "F8BF62E0693D0694816377099909C779FA23FD65 | " >expect && git log -n1 --format="%GF | %GP" signed-x509 >actual && @@ -1714,6 +1769,31 @@ test_expect_success GPGSSH 'log --graph --show-signature ssh' ' grep "${GOOD_SIGNATURE_TRUSTED}" actual ' +test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'log shows failure on expired signature key' ' + test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && + git log --graph --show-signature -n1 expired-signed >actual && + ! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual +' + +test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'log shows failure on not yet valid signature key' ' + test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && + git log --graph --show-signature -n1 notyetvalid-signed >actual && + ! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual +' + +test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'log show success with commit date and key validity matching' ' + test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && + git log --graph --show-signature -n1 timeboxedvalid-signed >actual && + grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual && + ! grep "${GPGSSH_BAD_SIGNATURE}" actual +' + +test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'log shows failure with commit date outside of key validity' ' + test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && + git log --graph --show-signature -n1 timeboxedinvalid-signed >actual && + ! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual +' + test_expect_success GPG 'log --graph --show-signature for merged tag' ' test_when_finished "git reset --hard && git checkout main" && git checkout -b plain main && @@ -2010,4 +2090,23 @@ test_expect_success 'log --end-of-options' ' test_cmp expect actual ' +test_expect_success 'set up commits with different authors' ' + git checkout --orphan authors && + test_commit --author "Jim <jim@example.com>" jim_1 && + test_commit --author "Val <val@example.com>" val_1 && + test_commit --author "Val <val@example.com>" val_2 && + test_commit --author "Jim <jim@example.com>" jim_2 && + test_commit --author "Val <val@example.com>" val_3 && + test_commit --author "Jim <jim@example.com>" jim_3 +' + +test_expect_success 'log --invert-grep --grep --author' ' + cat >expect <<-\EOF && + val_3 + val_1 + EOF + git log --format=%s --author=Val --grep 2 --invert-grep >actual && + test_cmp expect actual +' + test_done diff --git a/t/t4204-patch-id.sh b/t/t4204-patch-id.sh index f120857c20..80f4a65b28 100755 --- a/t/t4204-patch-id.sh +++ b/t/t4204-patch-id.sh @@ -27,7 +27,8 @@ test_expect_success 'setup' ' ' test_expect_success 'patch-id output is well-formed' ' - git log -p -1 | git patch-id >output && + git log -p -1 >log.output && + git patch-id <log.output >output && grep "^$OID_REGEX $(git rev-parse HEAD)$" output ' @@ -35,8 +36,8 @@ test_expect_success 'patch-id output is well-formed' ' calc_patch_id () { patch_name="$1" shift - git patch-id "$@" | - sed "s/ .*//" >patch-id_"$patch_name" && + git patch-id "$@" >patch-id.output && + sed "s/ .*//" patch-id.output >patch-id_"$patch_name" && test_line_count -gt 0 patch-id_"$patch_name" } @@ -45,7 +46,8 @@ get_top_diff () { } get_patch_id () { - get_top_diff "$1" | calc_patch_id "$@" + get_top_diff "$1" >top-diff.output && + calc_patch_id <top-diff.output "$@" } test_expect_success 'patch-id detects equality' ' @@ -63,16 +65,18 @@ test_expect_success 'patch-id detects inequality' ' test_expect_success 'patch-id supports git-format-patch output' ' get_patch_id main && git checkout same && - git format-patch -1 --stdout | calc_patch_id same && + git format-patch -1 --stdout >format-patch.output && + calc_patch_id same <format-patch.output && test_cmp patch-id_main patch-id_same && - set $(git format-patch -1 --stdout | git patch-id) && + set $(git patch-id <format-patch.output) && test "$2" = $(git rev-parse HEAD) ' test_expect_success 'whitespace is irrelevant in footer' ' get_patch_id main && git checkout same && - git format-patch -1 --stdout | sed "s/ \$//" | calc_patch_id same && + git format-patch -1 --stdout >format-patch.output && + sed "s/ \$//" format-patch.output | calc_patch_id same && test_cmp patch-id_main patch-id_same ' @@ -91,10 +95,11 @@ test_patch_id_file_order () { shift name="order-${1}-$relevant" shift - get_top_diff "main" | calc_patch_id "$name" "$@" && + get_top_diff "main" >top-diff.output && + calc_patch_id <top-diff.output "$name" "$@" && git checkout same && - git format-patch -1 --stdout -O foo-then-bar | - calc_patch_id "ordered-$name" "$@" && + git format-patch -1 --stdout -O foo-then-bar >format-patch.output && + calc_patch_id <format-patch.output "ordered-$name" "$@" && cmp_patch_id $relevant "$name" "ordered-$name" } @@ -142,7 +147,8 @@ test_expect_success '--stable overrides patchid.stable = false' ' test_expect_success 'patch-id supports git-format-patch MIME output' ' get_patch_id main && git checkout same && - git format-patch -1 --attach --stdout | calc_patch_id same && + git format-patch -1 --attach --stdout >format-patch.output && + calc_patch_id <format-patch.output same && test_cmp patch-id_main patch-id_same ' diff --git a/t/t4205-log-pretty-formats.sh b/t/t4205-log-pretty-formats.sh index 5865daa8f8..e448ef2928 100755 --- a/t/t4205-log-pretty-formats.sh +++ b/t/t4205-log-pretty-formats.sh @@ -976,7 +976,7 @@ test_expect_success '%(describe) vs git describe' ' else : >expect-contains-bad fi && - echo "$hash $desc" + echo "$hash $desc" || return 1 done >expect && test_path_exists expect-contains-good && test_path_exists expect-contains-bad && @@ -1002,4 +1002,20 @@ test_expect_success '%(describe:exclude=...) vs git describe --exclude ...' ' test_cmp expect actual ' +test_expect_success '%(describe:tags) vs git describe --tags' ' + test_when_finished "git tag -d tagname" && + git tag tagname && + git describe --tags >expect && + git log -1 --format="%(describe:tags)" >actual && + test_cmp expect actual +' + +test_expect_success '%(describe:abbrev=...) vs git describe --abbrev=...' ' + test_when_finished "git tag -d tagname" && + git tag -a -m tagged tagname && + git describe --abbrev=15 >expect && + git log -1 --format="%(describe:abbrev=15)" >actual && + test_cmp expect actual +' + test_done diff --git a/t/t4211-line-log.sh b/t/t4211-line-log.sh index 560127cc07..ac9e4d0928 100755 --- a/t/t4211-line-log.sh +++ b/t/t4211-line-log.sh @@ -137,7 +137,7 @@ test_expect_success 'range_set_union' ' test_seq 1000 > c.c && git add c.c && git commit -m "modify many lines" && - git log $(for x in $(test_seq 200); do echo -L $((2*x)),+1:c.c; done) + git log $(for x in $(test_seq 200); do echo -L $((2*x)),+1:c.c || return 1; done) ' test_expect_success '-s shows only line-log commits' ' diff --git a/t/t4212-log-corrupt.sh b/t/t4212-log-corrupt.sh index 03b952c90d..0244888a5a 100755 --- a/t/t4212-log-corrupt.sh +++ b/t/t4212-log-corrupt.sh @@ -20,10 +20,10 @@ test_expect_success 'fsck notices broken commit' ' test_expect_success 'git log with broken author email' ' { - echo commit $(cat broken_email.hash) - echo "Author: A U Thor <author@example.com>" - echo "Date: Thu Apr 7 15:13:13 2005 -0700" - echo + echo commit $(cat broken_email.hash) && + echo "Author: A U Thor <author@example.com>" && + echo "Date: Thu Apr 7 15:13:13 2005 -0700" && + echo && echo " foo" } >expect.out && diff --git a/t/t4216-log-bloom.sh b/t/t4216-log-bloom.sh index 50f206db55..cc3cebf672 100755 --- a/t/t4216-log-bloom.sh +++ b/t/t4216-log-bloom.sh @@ -175,13 +175,11 @@ test_expect_success 'persist filter settings' ' test_when_finished rm -rf .git/objects/info/commit-graph* && rm -rf .git/objects/info/commit-graph* && GIT_TRACE2_EVENT="$(pwd)/trace2.txt" \ - GIT_TRACE2_EVENT_NESTING=5 \ GIT_TEST_BLOOM_SETTINGS_NUM_HASHES=9 \ GIT_TEST_BLOOM_SETTINGS_BITS_PER_ENTRY=15 \ git commit-graph write --reachable --changed-paths && grep "{\"hash_version\":1,\"num_hashes\":9,\"bits_per_entry\":15,\"max_changed_paths\":512" trace2.txt && GIT_TRACE2_EVENT="$(pwd)/trace2-auto.txt" \ - GIT_TRACE2_EVENT_NESTING=5 \ git commit-graph write --reachable --changed-paths && grep "{\"hash_version\":1,\"num_hashes\":9,\"bits_per_entry\":15,\"max_changed_paths\":512" trace2-auto.txt ' @@ -376,7 +374,7 @@ test_expect_success 'Bloom generation backfills empty commits' ' cd empty && for i in $(test_seq 1 6) do - git commit --allow-empty -m "$i" + git commit --allow-empty -m "$i" || return 1 done && # Generate Bloom filters for empty commits 1-6, two at a time. @@ -389,7 +387,7 @@ test_expect_success 'Bloom generation backfills empty commits' ' test_filter_computed 2 trace.event && test_filter_not_computed 4 trace.event && test_filter_trunc_empty 2 trace.event && - test_filter_trunc_large 0 trace.event + test_filter_trunc_large 0 trace.event || return 1 done && # Finally, make sure that once all commits have filters, that diff --git a/t/t5000-tar-tree.sh b/t/t5000-tar-tree.sh index 2c88d1c159..7f8d2ab0a7 100755 --- a/t/t5000-tar-tree.sh +++ b/t/t5000-tar-tree.sh @@ -77,7 +77,7 @@ check_tar() { path=$(get_pax_header $header path) && if test -n "$path" then - mv "$data" "$path" + mv "$data" "$path" || exit 1 fi fi done @@ -133,7 +133,7 @@ test_expect_success 'populate workdir' ' for depth in 1 2 3 4 5 do mkdir $p && - cd $p + cd $p || exit 1 done && echo text >file_with_long_path ) && diff --git a/t/t5002-archive-attr-pattern.sh b/t/t5002-archive-attr-pattern.sh index bda6d7d7e9..a66b5ba27e 100755 --- a/t/t5002-archive-attr-pattern.sh +++ b/t/t5002-archive-attr-pattern.sh @@ -2,6 +2,7 @@ test_description='git archive attribute pattern tests' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_exists() { diff --git a/t/t5003-archive-zip.sh b/t/t5003-archive-zip.sh index 1e6d18b140..d726964307 100755 --- a/t/t5003-archive-zip.sh +++ b/t/t5003-archive-zip.sh @@ -106,7 +106,7 @@ test_expect_success \ printf "A\$Format:%s\$O" "$SUBSTFORMAT" >a/substfile1 && printf "A not substituted O" >a/substfile2 && (p=long_path_to_a_file && cd a && - for depth in 1 2 3 4 5; do mkdir $p && cd $p; done && + for depth in 1 2 3 4 5; do mkdir $p && cd $p || exit 1; done && echo text >file_with_long_path) ' diff --git a/t/t5004-archive-corner-cases.sh b/t/t5004-archive-corner-cases.sh index 2d32d0ed12..ae508e2162 100755 --- a/t/t5004-archive-corner-cases.sh +++ b/t/t5004-archive-corner-cases.sh @@ -131,7 +131,7 @@ test_expect_success ZIPINFO 'zip archive with many entries' ' do for b in 0 1 2 3 4 5 6 7 8 9 a b c d e f do - : >00/$a$b + : >00/$a$b || return 1 done done && git add 00 && @@ -143,7 +143,7 @@ test_expect_success ZIPINFO 'zip archive with many entries' ' do for d in 0 1 2 3 4 5 6 7 8 9 a b c d e f do - echo "040000 tree $subtree $c$d" + echo "040000 tree $subtree $c$d" || return 1 done done >tree && tree=$(git mktree <tree) && @@ -171,7 +171,7 @@ test_expect_success EXPENSIVE,UNZIP,UNZIP_ZIP64_SUPPORT \ # create tree containing 65500 entries of that blob for i in $(test_seq 1 65500) do - echo "100644 blob $blob $i" + echo "100644 blob $blob $i" || return 1 done >tree && tree=$(git mktree <tree) && diff --git a/t/t5100-mailinfo.sh b/t/t5100-mailinfo.sh index 141b29f031..cebad1048c 100755 --- a/t/t5100-mailinfo.sh +++ b/t/t5100-mailinfo.sh @@ -122,7 +122,7 @@ test_expect_success 'mailinfo unescapes with --mboxrd' ' do git mailinfo mboxrd/msg mboxrd/patch \ <mboxrd/$i >mboxrd/out && - test_cmp "$DATA/${i}mboxrd" mboxrd/msg + test_cmp "$DATA/${i}mboxrd" mboxrd/msg || return 1 done && sp=" " && echo "From " >expect && diff --git a/t/t5200-update-server-info.sh b/t/t5200-update-server-info.sh index 21a58eecb9..ed9dfd624c 100755 --- a/t/t5200-update-server-info.sh +++ b/t/t5200-update-server-info.sh @@ -2,6 +2,7 @@ test_description='Test git update-server-info' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success 'setup' 'test_commit file' diff --git a/t/t5300-pack-object.sh b/t/t5300-pack-object.sh index e13a884207..2fd845187e 100755 --- a/t/t5300-pack-object.sh +++ b/t/t5300-pack-object.sh @@ -347,7 +347,7 @@ test_expect_success 'unpacking with --strict' ' for i in 0 1 2 3 4 5 6 7 8 9 do o=$(echo $j$i | git hash-object -w --stdin) && - echo "100644 $o 0 $j$i" + echo "100644 $o 0 $j$i" || return 1 done done >LIST && rm -f .git/index && @@ -361,11 +361,7 @@ test_expect_success 'unpacking with --strict' ' ST=$(git write-tree) && git rev-list --objects "$LIST" "$LI" "$ST" >actual && PACK5=$( git pack-objects test-5 <actual ) && - PACK6=$( ( - echo "$LIST" - echo "$LI" - echo "$ST" - ) | git pack-objects test-6 ) && + PACK6=$( test_write_lines "$LIST" "$LI" "$ST" | git pack-objects test-6 ) && test_create_repo test-5 && ( cd test-5 && @@ -394,7 +390,7 @@ test_expect_success 'index-pack with --strict' ' for i in 0 1 2 3 4 5 6 7 8 9 do o=$(echo $j$i | git hash-object -w --stdin) && - echo "100644 $o 0 $j$i" + echo "100644 $o 0 $j$i" || return 1 done done >LIST && rm -f .git/index && @@ -408,11 +404,7 @@ test_expect_success 'index-pack with --strict' ' ST=$(git write-tree) && git rev-list --objects "$LIST" "$LI" "$ST" >actual && PACK5=$( git pack-objects test-5 <actual ) && - PACK6=$( ( - echo "$LIST" - echo "$LI" - echo "$ST" - ) | git pack-objects test-6 ) && + PACK6=$( test_write_lines "$LIST" "$LI" "$ST" | git pack-objects test-6 ) && test_create_repo test-7 && ( cd test-7 && @@ -594,7 +586,7 @@ test_expect_success 'setup for --stdin-packs tests' ' for id in A B C do git pack-objects .git/objects/pack/pack-$id \ - --incremental --revs <<-EOF + --incremental --revs <<-EOF || exit 1 refs/tags/$id EOF done && diff --git a/t/t5302-pack-index.sh b/t/t5302-pack-index.sh index 7c9d687367..8ee67df38f 100755 --- a/t/t5302-pack-index.sh +++ b/t/t5302-pack-index.sh @@ -14,7 +14,7 @@ test_expect_success 'setup' ' i=1 && while test $i -le 100 do - iii=$(printf "%03i" $i) + iii=$(printf "%03i" $i) && test-tool genrandom "bar" 200 > wide_delta_$iii && test-tool genrandom "baz $iii" 50 >> wide_delta_$iii && test-tool genrandom "foo"$i 100 > deep_delta_$iii && diff --git a/t/t5306-pack-nobase.sh b/t/t5306-pack-nobase.sh index f4931c0c2a..51973f4a51 100755 --- a/t/t5306-pack-nobase.sh +++ b/t/t5306-pack-nobase.sh @@ -12,7 +12,7 @@ test_description='git-pack-object with missing base # test_expect_success \ 'setup base' \ - 'for a in a b c d e f g h i; do echo $a >>text; done && + 'test_write_lines a b c d e f g h i >text && echo side >side && git update-index --add text side && A=$(echo A | git commit-tree $(git write-tree)) && diff --git a/t/t5307-pack-missing-commit.sh b/t/t5307-pack-missing-commit.sh index f4338abb78..1e02c305c4 100755 --- a/t/t5307-pack-missing-commit.sh +++ b/t/t5307-pack-missing-commit.sh @@ -2,6 +2,7 @@ test_description='pack should notice missing commit objects' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success setup ' @@ -11,7 +12,7 @@ test_expect_success setup ' git add "file$i" && test_tick && git commit -m "$i" && - git tag "tag$i" + git tag "tag$i" || return 1 done && obj=$(git rev-parse --verify tag3) && fanout=$(expr "$obj" : "\(..\)") && diff --git a/t/t5310-pack-bitmaps.sh b/t/t5310-pack-bitmaps.sh index dcf03d324a..d05ab716f6 100755 --- a/t/t5310-pack-bitmaps.sh +++ b/t/t5310-pack-bitmaps.sh @@ -1,8 +1,6 @@ #!/bin/sh test_description='exercise basic bitmap functionality' -GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=master -export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME . ./test-lib.sh . "$TEST_DIRECTORY"/lib-bitmap.sh @@ -35,7 +33,7 @@ test_expect_success 'setup writing bitmaps during repack' ' ' test_expect_success 'full repack creates bitmaps' ' - GIT_TRACE2_EVENT_NESTING=4 GIT_TRACE2_EVENT="$(pwd)/trace" \ + GIT_TRACE2_EVENT="$(pwd)/trace" \ git repack -ad && ls .git/objects/pack/ | grep bitmap >output && test_line_count = 1 output && @@ -230,7 +228,7 @@ test_expect_success 'pack reuse respects --honor-pack-keep' ' test_when_finished "rm -f .git/objects/pack/*.keep" && for i in .git/objects/pack/*.pack do - >${i%.pack}.keep + >${i%.pack}.keep || return 1 done && reusable_pack --honor-pack-keep >empty.pack && git index-pack empty.pack && diff --git a/t/t5316-pack-delta-depth.sh b/t/t5316-pack-delta-depth.sh index 759169d074..df524f7b6d 100755 --- a/t/t5316-pack-delta-depth.sh +++ b/t/t5316-pack-delta-depth.sh @@ -57,8 +57,11 @@ test_expect_success 'create series of packs' ' git commit -m $i && cur=$(git rev-parse HEAD^{tree}) && { - test -n "$prev" && echo "-$prev" - echo $cur + if test -n "$prev" + then + echo "-$prev" + fi && + echo $cur && echo "$(git rev-parse :file) file" } | git pack-objects --stdout >tmp && git index-pack --stdin --fix-thin <tmp || return 1 diff --git a/t/t5317-pack-objects-filter-objects.sh b/t/t5317-pack-objects-filter-objects.sh index 13ed3eb136..33b740ce62 100755 --- a/t/t5317-pack-objects-filter-objects.sh +++ b/t/t5317-pack-objects-filter-objects.sh @@ -16,9 +16,9 @@ test_expect_success 'setup r1' ' git init r1 && for n in 1 2 3 4 5 do - echo "This is file: $n" > r1/file.$n - git -C r1 add file.$n - git -C r1 commit -m "$n" + echo "This is file: $n" > r1/file.$n && + git -C r1 add file.$n && + git -C r1 commit -m "$n" || return 1 done ' @@ -116,9 +116,9 @@ test_expect_success 'setup r2' ' git init r2 && for n in 1000 10000 do - printf "%"$n"s" X > r2/large.$n - git -C r2 add large.$n - git -C r2 commit -m "$n" + printf "%"$n"s" X > r2/large.$n && + git -C r2 add large.$n && + git -C r2 commit -m "$n" || return 1 done ' @@ -278,10 +278,10 @@ test_expect_success 'setup r3' ' mkdir r3/dir1 && for n in sparse1 sparse2 do - echo "This is file: $n" > r3/$n - git -C r3 add $n - echo "This is file: dir1/$n" > r3/dir1/$n - git -C r3 add dir1/$n + echo "This is file: $n" > r3/$n && + git -C r3 add $n && + echo "This is file: dir1/$n" > r3/dir1/$n && + git -C r3 add dir1/$n || return 1 done && git -C r3 commit -m "sparse" && echo dir1/ >pattern1 && @@ -331,10 +331,10 @@ test_expect_success 'setup r4' ' mkdir r4/dir1 && for n in sparse1 sparse2 do - echo "This is file: $n" > r4/$n - git -C r4 add $n - echo "This is file: dir1/$n" > r4/dir1/$n - git -C r4 add dir1/$n + echo "This is file: $n" > r4/$n && + git -C r4 add $n && + echo "This is file: dir1/$n" > r4/dir1/$n && + git -C r4 add dir1/$n || return 1 done && echo dir1/ >r4/pattern && git -C r4 add pattern && @@ -409,7 +409,7 @@ test_expect_success 'setup r1 - delete loose blobs' ' for id in `cat expected | sed "s|..|&/|"` do - rm r1/.git/objects/$id + rm r1/.git/objects/$id || return 1 done ' diff --git a/t/t5318-commit-graph.sh b/t/t5318-commit-graph.sh index f516fda7cc..edb728f77c 100755 --- a/t/t5318-commit-graph.sh +++ b/t/t5318-commit-graph.sh @@ -64,7 +64,7 @@ test_expect_success 'create commits and repack' ' for i in $(test_seq 3) do test_commit $i && - git branch commits/$i + git branch commits/$i || return 1 done && git repack ' @@ -147,13 +147,13 @@ test_expect_success 'Add more commits' ' for i in $(test_seq 4 5) do test_commit $i && - git branch commits/$i + git branch commits/$i || return 1 done && git reset --hard commits/2 && for i in $(test_seq 6 7) do test_commit $i && - git branch commits/$i + git branch commits/$i || return 1 done && git reset --hard commits/2 && git merge commits/4 && diff --git a/t/t5319-multi-pack-index.sh b/t/t5319-multi-pack-index.sh index 3f69e43178..afbe93f162 100755 --- a/t/t5319-multi-pack-index.sh +++ b/t/t5319-multi-pack-index.sh @@ -93,7 +93,7 @@ test_expect_success 'create objects' ' test_commit initial && for i in $(test_seq 1 5) do - generate_objects $i + generate_objects $i || return 1 done && commit_and_list_objects ' @@ -155,7 +155,7 @@ test_expect_success 'corrupt idx reports errors' ' test_expect_success 'add more objects' ' for i in $(test_seq 6 10) do - generate_objects $i + generate_objects $i || return 1 done && commit_and_list_objects ' @@ -203,7 +203,7 @@ test_expect_success 'add more packs' ' do generate_objects $j && commit_and_list_objects && - git pack-objects --index-version=2 $objdir/pack/test-pack <obj-list + git pack-objects --index-version=2 $objdir/pack/test-pack <obj-list || return 1 done ' @@ -482,8 +482,10 @@ test_expect_success 'corrupt MIDX is not reused' ' ' test_expect_success 'verify incorrect checksum' ' - pos=$(($(wc -c <$objdir/pack/multi-pack-index) - 1)) && - corrupt_midx_and_verify $pos "\377" $objdir "incorrect checksum" + pos=$(($(wc -c <$objdir/pack/multi-pack-index) - 10)) && + corrupt_midx_and_verify $pos \ + "\377\377\377\377\377\377\377\377\377\377" \ + $objdir "incorrect checksum" ' test_expect_success 'repack progress off for redirected stderr' ' @@ -594,7 +596,7 @@ test_expect_success 'force some 64-bit offsets with pack-objects' ' mkdir objects64/pack && for i in $(test_seq 1 11) do - generate_objects 11 + generate_objects 11 || return 1 done && commit_and_list_objects && pack64=$(git pack-objects --index-version=2,0x40 objects64/pack/test-64 <obj-list) && @@ -638,7 +640,7 @@ test_expect_success 'setup expire tests' ' git update-index --add large_file.txt && for i in $(test_seq 1 20) do - test_commit $i + test_commit $i || exit 1 done && git branch A HEAD && git branch B HEAD~8 && diff --git a/t/t5322-pack-objects-sparse.sh b/t/t5322-pack-objects-sparse.sh index 61cb907a90..d39958c066 100755 --- a/t/t5322-pack-objects-sparse.sh +++ b/t/t5322-pack-objects-sparse.sh @@ -14,7 +14,7 @@ test_expect_success 'setup repo' ' for j in $(test_seq 1 3) do mkdir f$i/f$j && - echo $j >f$i/f$j/data.txt + echo $j >f$i/f$j/data.txt || return 1 done done && git add . && @@ -23,7 +23,7 @@ test_expect_success 'setup repo' ' do git checkout -b topic$i main && echo change-$i >f$i/f$i/data.txt && - git commit -a -m "Changed f$i/f$i/data.txt" + git commit -a -m "Changed f$i/f$i/data.txt" || return 1 done && cat >packinput.txt <<-EOF && topic1 diff --git a/t/t5325-reverse-index.sh b/t/t5325-reverse-index.sh index da453f68d6..d042d26f2b 100755 --- a/t/t5325-reverse-index.sh +++ b/t/t5325-reverse-index.sh @@ -46,7 +46,7 @@ test_expect_success 'index-pack with --[no-]rev-index' ' test_path_exists $rev && test_index_pack "$conf" --no-rev-index && - test_path_is_missing $rev + test_path_is_missing $rev || return 1 done ' diff --git a/t/t5410-receive-pack-alternates.sh b/t/t5410-receive-pack-alternates.sh index 0b28e4e452..7a45d4c311 100755 --- a/t/t5410-receive-pack-alternates.sh +++ b/t/t5410-receive-pack-alternates.sh @@ -5,6 +5,7 @@ test_description='git receive-pack with alternate ref filtering' 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/t5500-fetch-pack.sh b/t/t5500-fetch-pack.sh index 8a5d3492c7..f0dc4e6968 100755 --- a/t/t5500-fetch-pack.sh +++ b/t/t5500-fetch-pack.sh @@ -95,7 +95,7 @@ test_expect_success 'setup' ' while [ $cur -le 10 ]; do add A$cur $(eval echo \$A$prev) && prev=$cur && - cur=$(($cur+1)) + cur=$(($cur+1)) || return 1 done && add B1 $A1 && git update-ref refs/heads/A "$ATIP" && @@ -112,7 +112,7 @@ test_expect_success 'post 1st pull setup' ' while [ $cur -le 65 ]; do add B$cur $(eval echo \$B$prev) && prev=$cur && - cur=$(($cur+1)) + cur=$(($cur+1)) || return 1 done ' @@ -464,11 +464,11 @@ test_expect_success 'fetch creating new shallow root' ' test_expect_success 'setup tests for the --stdin parameter' ' for head in C D E F do - add $head + add $head || return 1 done && for head in A B C D E F do - git tag $head $head + git tag $head $head || return 1 done && cat >input <<-\EOF && refs/heads/C diff --git a/t/t5502-quickfetch.sh b/t/t5502-quickfetch.sh index 8c05c7715b..b160f8b7fb 100755 --- a/t/t5502-quickfetch.sh +++ b/t/t5502-quickfetch.sh @@ -130,7 +130,7 @@ test_expect_success 'quickfetch should handle ~1000 refs (on Windows)' ' for i in 0 1 2 3 4 5 6 7 8 9; do for j in 0 1 2 3 4 5 6 7 8 9; do for k in 0 1 2 3 4 5 6 7 8 9; do - echo "$branchprefix$i$j$k" >> .git/packed-refs + echo "$branchprefix$i$j$k" >> .git/packed-refs || return 1 done done done && diff --git a/t/t5504-fetch-receive-strict.sh b/t/t5504-fetch-receive-strict.sh index 6e5a9c20e7..b0b795aca9 100755 --- a/t/t5504-fetch-receive-strict.sh +++ b/t/t5504-fetch-receive-strict.sh @@ -292,7 +292,7 @@ test_expect_success 'push with receive.fsck.missingEmail=warn' ' receive.fsck.missingEmail warn && git push --porcelain dst bogus >act 2>&1 && grep "missingEmail" act && - test_i18ngrep "Skipping unknown msg id.*whatever" act && + test_i18ngrep "skipping unknown msg id.*whatever" act && git --git-dir=dst/.git branch -D bogus && git --git-dir=dst/.git config --add \ receive.fsck.missingEmail ignore && diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh index e6e3c8f552..9ab315424c 100755 --- a/t/t5505-remote.sh +++ b/t/t5505-remote.sh @@ -2,9 +2,6 @@ test_description='git remote porcelain-ish' -GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main -export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME - . ./test-lib.sh setup_repository () { @@ -1332,7 +1329,6 @@ test_expect_success 'unqualified <dst> refspec DWIM and advice' ' ( cd test && git tag -a -m "Some tag" some-tag main && - exit_with=true && for type in commit tag tree blob do if test "$type" = "blob" @@ -1348,9 +1344,8 @@ test_expect_success 'unqualified <dst> refspec DWIM and advice' ' push origin $oid:dst 2>err && test_i18ngrep "error: The destination you" err && test_i18ngrep ! "hint: Did you mean" err || - exit_with=false - done && - $exit_with + exit 1 + done ) ' diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh index a0faf0dd94..20f7110ec1 100755 --- a/t/t5510-fetch.sh +++ b/t/t5510-fetch.sh @@ -5,9 +5,6 @@ test_description='Per branch config variables affects "git fetch". ' -GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main -export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME - . ./test-lib.sh . "$TEST_DIRECTORY"/lib-bundle.sh @@ -40,11 +37,11 @@ test_expect_success "clone and setup child repos" ' git config branch.main.remote two && git config branch.main.merge refs/heads/one && mkdir -p .git/remotes && - { - echo "URL: ../two/.git/" - echo "Pull: refs/heads/main:refs/heads/two" - echo "Pull: refs/heads/one:refs/heads/one" - } >.git/remotes/two + cat >.git/remotes/two <<-\EOF + URL: ../two/.git/ + Pull: refs/heads/main:refs/heads/two + Pull: refs/heads/one:refs/heads/one + EOF ) && git clone . bundle && git clone . seven @@ -71,7 +68,7 @@ test_expect_success "fetch test for-merge" ' main_in_two=$(cd ../two && git rev-parse main) && one_in_two=$(cd ../two && git rev-parse one) && { - echo "$one_in_two " + echo "$one_in_two " && echo "$main_in_two not-for-merge" } >expected && cut -f -2 .git/FETCH_HEAD >actual && @@ -550,7 +547,7 @@ test_expect_success 'bundle should record HEAD correctly' ' git bundle list-heads bundle5 >actual && for h in HEAD refs/heads/main do - echo "$(git rev-parse --verify $h) $h" + echo "$(git rev-parse --verify $h) $h" || return 1 done >expect && test_cmp expect actual diff --git a/t/t5515-fetch-merge-logic.sh b/t/t5515-fetch-merge-logic.sh index 50f14101c5..320d26796d 100755 --- a/t/t5515-fetch-merge-logic.sh +++ b/t/t5515-fetch-merge-logic.sh @@ -105,19 +105,19 @@ test_expect_success setup ' remotes="$remotes config-glob" && mkdir -p .git/remotes && - { - echo "URL: ../.git/" - echo "Pull: refs/heads/main:remotes/rem/main" - echo "Pull: refs/heads/one:remotes/rem/one" - echo "Pull: two:remotes/rem/two" - echo "Pull: refs/heads/three:remotes/rem/three" - } >.git/remotes/remote-explicit && + cat >.git/remotes/remote-explicit <<-\EOF && + URL: ../.git/ + Pull: refs/heads/main:remotes/rem/main + Pull: refs/heads/one:remotes/rem/one + Pull: two:remotes/rem/two + Pull: refs/heads/three:remotes/rem/three + EOF remotes="$remotes remote-explicit" && - { - echo "URL: ../.git/" - echo "Pull: refs/heads/*:refs/remotes/rem/*" - } >.git/remotes/remote-glob && + cat >.git/remotes/remote-glob <<-\EOF && + URL: ../.git/ + Pull: refs/heads/*:refs/remotes/rem/* + EOF remotes="$remotes remote-glob" && mkdir -p .git/branches && @@ -133,7 +133,7 @@ test_expect_success setup ' git config branch.br-$remote-merge.merge refs/heads/three && git config branch.br-$remote-octopus.remote $remote && git config branch.br-$remote-octopus.merge refs/heads/one && - git config --add branch.br-$remote-octopus.merge two + git config --add branch.br-$remote-octopus.merge two || return 1 done && build_script sed_script ' @@ -191,17 +191,17 @@ do cp "$expect_r" expect_r && convert_expected expect_r sed_script && { - echo "# $cmd" - set x $cmd; shift - git symbolic-ref HEAD refs/heads/$1 ; shift - rm -f .git/FETCH_HEAD + echo "# $cmd" && + set x $cmd && shift && + git symbolic-ref HEAD refs/heads/$1 && shift && + rm -f .git/FETCH_HEAD && git for-each-ref \ refs/heads refs/remotes/rem refs/tags | while read val type refname do - git update-ref -d "$refname" "$val" - done - git fetch "$@" >/dev/null + git update-ref -d "$refname" "$val" || return 1 + done && + git fetch "$@" >/dev/null && cat .git/FETCH_HEAD } >"$actual_f" && git show-ref >"$actual_r" && diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh index 8212ca56dc..2f04cf9a1c 100755 --- a/t/t5516-fetch-push.sh +++ b/t/t5516-fetch-push.sh @@ -541,6 +541,15 @@ do done +test_expect_success "push to remote with no explicit refspec and config remote.*.push = src:dest" ' + mk_test testrepo heads/main && + git checkout $the_first_commit && + test_config remote.there.url testrepo && + test_config remote.there.push refs/heads/main:refs/heads/main && + git push there && + check_push_result testrepo $the_commit heads/main +' + test_expect_success 'push with remote.pushdefault' ' mk_test up_repo heads/main && mk_test down_repo heads/main && @@ -1316,10 +1325,7 @@ test_expect_success 'fetch follows tags by default' ' git pull ../testrepo main && git tag -m "annotated" tag && git for-each-ref >tmp1 && - ( - cat tmp1 - sed -n "s|refs/heads/main$|refs/remotes/origin/main|p" tmp1 - ) | + sed -n "p; s|refs/heads/main$|refs/remotes/origin/main|p" tmp1 | sort -k 3 >../expect ) && git init dst && @@ -1769,6 +1775,38 @@ test_expect_success 'denyCurrentBranch and worktrees' ' test_must_fail git -C cloned push origin HEAD:new-wt && test_config receive.denyCurrentBranch updateInstead && git -C cloned push origin HEAD:new-wt && + test_path_exists new-wt/first.t && test_must_fail git -C cloned push --delete origin new-wt ' + +test_expect_success 'denyCurrentBranch and bare repository worktrees' ' + test_when_finished "rm -fr bare.git" && + git clone --bare . bare.git && + git -C bare.git worktree add wt && + test_commit grape && + git -C bare.git config receive.denyCurrentBranch refuse && + test_must_fail git push bare.git HEAD:wt && + git -C bare.git config receive.denyCurrentBranch updateInstead && + git push bare.git HEAD:wt && + test_path_exists bare.git/wt/grape.t && + test_must_fail git push --delete bare.git wt +' + +test_expect_success 'refuse fetch to current branch of worktree' ' + test_when_finished "git worktree remove --force wt && git branch -D wt" && + git worktree add wt && + test_commit apple && + test_must_fail git fetch . HEAD:wt && + git fetch -u . HEAD:wt +' + +test_expect_success 'refuse fetch to current branch of bare repository worktree' ' + test_when_finished "rm -fr bare.git" && + git clone --bare . bare.git && + git -C bare.git worktree add wt && + test_commit banana && + test_must_fail git -C bare.git fetch .. HEAD:wt && + git -C bare.git fetch -u .. HEAD:wt +' + test_done diff --git a/t/t5526-fetch-submodules.sh b/t/t5526-fetch-submodules.sh index 2dc75b80db..840c89cc8b 100755 --- a/t/t5526-fetch-submodules.sh +++ b/t/t5526-fetch-submodules.sh @@ -3,9 +3,6 @@ test_description='Recursive "git fetch" for submodules' -GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=master -export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME - GIT_TEST_FATAL_REGISTER_SUBMODULE_ODB=1 export GIT_TEST_FATAL_REGISTER_SUBMODULE_ODB diff --git a/t/t5540-http-push-webdav.sh b/t/t5540-http-push-webdav.sh index 8b68bb38a4..b0dbacf0b9 100755 --- a/t/t5540-http-push-webdav.sh +++ b/t/t5540-http-push-webdav.sh @@ -18,6 +18,12 @@ then test_done fi +if test_have_prereq !REFFILES +then + skip_all='skipping test; dumb HTTP protocol not supported with reftable.' + test_done +fi + LIB_HTTPD_DAV=t . "$TEST_DIRECTORY"/lib-httpd.sh ROOT_PATH="$PWD" diff --git a/t/t5550-http-fetch-dumb.sh b/t/t5550-http-fetch-dumb.sh index 6d9142afc3..259203926a 100755 --- a/t/t5550-http-fetch-dumb.sh +++ b/t/t5550-http-fetch-dumb.sh @@ -5,6 +5,13 @@ GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME . ./test-lib.sh + +if test_have_prereq !REFFILES +then + skip_all='skipping test; dumb HTTP protocol not supported with reftable.' + test_done +fi + . "$TEST_DIRECTORY"/lib-httpd.sh start_httpd diff --git a/t/t5552-skipping-fetch-negotiator.sh b/t/t5552-skipping-fetch-negotiator.sh index 7b9fb4ff02..165427d57e 100755 --- a/t/t5552-skipping-fetch-negotiator.sh +++ b/t/t5552-skipping-fetch-negotiator.sh @@ -48,7 +48,7 @@ test_expect_success 'commits with no parents are sent regardless of skip distanc git init client && for i in $(test_seq 7) do - test_commit -C client c$i + test_commit -C client c$i || return 1 done && # We send: "c7" (skip 1) "c5" (skip 2) "c2" (skip 4). After that, since @@ -68,7 +68,7 @@ test_expect_success 'when two skips collide, favor the larger one' ' git init client && for i in $(test_seq 11) do - test_commit -C client c$i + test_commit -C client c$i || return 1 done && git -C client checkout c5 && test_commit -C client c5side && @@ -155,14 +155,14 @@ test_expect_success 'do not send "have" with ancestors of commits that server AC for i in $(test_seq 8) do git -C client checkout --orphan b$i && - test_commit -C client b$i.c0 + test_commit -C client b$i.c0 || return 1 done && for j in $(test_seq 19) do for i in $(test_seq 8) do git -C client checkout b$i && - test_commit -C client b$i.c$j + test_commit -C client b$i.c$j || return 1 done done && @@ -201,7 +201,7 @@ test_expect_success 'do not send "have" with ancestors of commits that server AC # should still send the others (in this test, just check b2). for i in $(test_seq 0 8) do - have_not_sent b1.c$i + have_not_sent b1.c$i || return 1 done && have_sent b2.c1 b2.c0 ' diff --git a/t/t5553-set-upstream.sh b/t/t5553-set-upstream.sh index 9c12c0f8c3..48050162c2 100755 --- a/t/t5553-set-upstream.sh +++ b/t/t5553-set-upstream.sh @@ -91,6 +91,17 @@ test_expect_success 'fetch --set-upstream with valid URL sets upstream to URL' ' check_config_missing other2 ' +test_expect_success 'fetch --set-upstream with a detached HEAD' ' + git checkout HEAD^0 && + test_when_finished "git checkout -" && + cat >expect <<-\EOF && + warning: could not set upstream of HEAD to '"'"'main'"'"' from '"'"'upstream'"'"' when it does not point to any branch. + EOF + git fetch --set-upstream upstream main 2>actual.raw && + grep ^warning: actual.raw >actual && + test_cmp expect actual +' + # tests for pull --set-upstream test_expect_success 'setup bare parent pull' ' @@ -178,4 +189,15 @@ test_expect_success 'pull --set-upstream with valid URL and branch sets branch' check_config_missing other2 ' +test_expect_success 'pull --set-upstream with a detached HEAD' ' + git checkout HEAD^0 && + test_when_finished "git checkout -" && + cat >expect <<-\EOF && + warning: could not set upstream of HEAD to '"'"'main'"'"' from '"'"'upstream'"'"' when it does not point to any branch. + EOF + git pull --no-rebase --set-upstream upstream main 2>actual.raw && + grep ^warning: actual.raw >actual && + test_cmp expect actual +' + test_done diff --git a/t/t5555-http-smart-common.sh b/t/t5555-http-smart-common.sh index 49faf5e283..b1cfe8b7db 100755 --- a/t/t5555-http-smart-common.sh +++ b/t/t5555-http-smart-common.sh @@ -2,6 +2,7 @@ test_description='test functionality common to smart fetch & push' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success 'setup' ' diff --git a/t/t5562-http-backend-content-length.sh b/t/t5562-http-backend-content-length.sh index 05a58069b0..b68ec22d3f 100755 --- a/t/t5562-http-backend-content-length.sh +++ b/t/t5562-http-backend-content-length.sh @@ -63,7 +63,7 @@ test_expect_success 'setup' ' hash_next=$(git commit-tree -p HEAD -m next HEAD^{tree}) && { printf "%s %s refs/heads/newbranch\\0report-status object-format=%s\\n" \ - "$ZERO_OID" "$hash_next" "$(test_oid algo)" | packetize_raw + "$ZERO_OID" "$hash_next" "$(test_oid algo)" | packetize_raw && printf 0000 && echo "$hash_next" | git pack-objects --stdout } >push_body && diff --git a/t/t5570-git-daemon.sh b/t/t5570-git-daemon.sh index b87ca06a58..1131503b76 100755 --- a/t/t5570-git-daemon.sh +++ b/t/t5570-git-daemon.sh @@ -194,7 +194,7 @@ test_expect_success 'hostname cannot break out of directory' ' test_expect_success FAKENC 'hostname interpolation works after LF-stripping' ' { - printf "git-upload-pack /interp.git\n\0host=localhost" | packetize_raw + printf "git-upload-pack /interp.git\n\0host=localhost" | packetize_raw && printf "0000" } >input && fake_nc "$GIT_DAEMON_HOST_PORT" <input >output && diff --git a/t/t5571-pre-push-hook.sh b/t/t5571-pre-push-hook.sh index ad8d5804f7..660f876eec 100755 --- a/t/t5571-pre-push-hook.sh +++ b/t/t5571-pre-push-hook.sh @@ -114,11 +114,11 @@ test_expect_success 'push to URL' ' test_expect_success 'set up many-ref tests' ' { - nr=1000 + nr=1000 && while test $nr -lt 2000 do - nr=$(( $nr + 1 )) - echo "create refs/heads/b/$nr $COMMIT3" + nr=$(( $nr + 1 )) && + echo "create refs/heads/b/$nr $COMMIT3" || return 1 done } | git update-ref --stdin ' diff --git a/t/t5602-clone-remote-exec.sh b/t/t5602-clone-remote-exec.sh index cbcceab9d5..56329aa160 100755 --- a/t/t5602-clone-remote-exec.sh +++ b/t/t5602-clone-remote-exec.sh @@ -2,6 +2,7 @@ test_description=clone +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success setup ' diff --git a/t/t5603-clone-dirname.sh b/t/t5603-clone-dirname.sh index 13b5e5eb9b..8ca1f09423 100755 --- a/t/t5603-clone-dirname.sh +++ b/t/t5603-clone-dirname.sh @@ -1,6 +1,8 @@ #!/bin/sh test_description='check output directory names used by git-clone' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh # we use a fake ssh wrapper that ignores the arguments diff --git a/t/t5609-clone-branch.sh b/t/t5609-clone-branch.sh index f86a674a03..252e1f7c20 100755 --- a/t/t5609-clone-branch.sh +++ b/t/t5609-clone-branch.sh @@ -4,6 +4,7 @@ test_description='clone --branch option' GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh check_HEAD() { diff --git a/t/t5611-clone-config.sh b/t/t5611-clone-config.sh index f8625f9158..4b3877216e 100755 --- a/t/t5611-clone-config.sh +++ b/t/t5611-clone-config.sh @@ -17,7 +17,7 @@ test_expect_success 'clone -c sets config in cloned repo' ' test_expect_success 'clone -c can set multi-keys' ' rm -rf child && git clone -c core.foo=bar -c core.foo=baz . child && - { echo bar; echo baz; } >expect && + test_write_lines bar baz >expect && git --git-dir=child/.git config --get-all core.foo >actual && test_cmp expect actual ' diff --git a/t/t5616-partial-clone.sh b/t/t5616-partial-clone.sh index cf3e82bdf5..34469b6ac1 100755 --- a/t/t5616-partial-clone.sh +++ b/t/t5616-partial-clone.sh @@ -16,10 +16,10 @@ test_expect_success 'setup normal src repo' ' git init src && for n in 1 2 3 4 do - echo "This is file: $n" > src/file.$n.txt - git -C src add file.$n.txt - git -C src commit -m "file $n" - git -C src ls-files -s file.$n.txt >>temp + echo "This is file: $n" > src/file.$n.txt && + git -C src add file.$n.txt && + git -C src commit -m "file $n" && + git -C src ls-files -s file.$n.txt >>temp || return 1 done && awk -f print_2.awk <temp | sort >expect_1.oids && test_line_count = 4 expect_1.oids @@ -72,9 +72,9 @@ test_expect_success 'push new commits to server' ' git -C src remote add srv "file://$(pwd)/srv.bare" && for x in a b c d e do - echo "Mod file.1.txt $x" >>src/file.1.txt - git -C src add file.1.txt - git -C src commit -m "mod $x" + echo "Mod file.1.txt $x" >>src/file.1.txt && + git -C src add file.1.txt && + git -C src commit -m "mod $x" || return 1 done && git -C src blame main -- file.1.txt >expect.blame && git -C src push -u srv main @@ -114,9 +114,9 @@ test_expect_success 'verify blame causes dynamic object fetch' ' test_expect_success 'push new commits to server for file.2.txt' ' for x in a b c d e f do - echo "Mod file.2.txt $x" >>src/file.2.txt - git -C src add file.2.txt - git -C src commit -m "mod $x" + echo "Mod file.2.txt $x" >>src/file.2.txt && + git -C src add file.2.txt && + git -C src commit -m "mod $x" || return 1 done && git -C src push -u srv main ' @@ -135,9 +135,9 @@ test_expect_success 'override inherited filter-spec using --no-filter' ' test_expect_success 'push new commits to server for file.3.txt' ' for x in a b c d e f do - echo "Mod file.3.txt $x" >>src/file.3.txt - git -C src add file.3.txt - git -C src commit -m "mod $x" + echo "Mod file.3.txt $x" >>src/file.3.txt && + git -C src add file.3.txt && + git -C src commit -m "mod $x" || return 1 done && git -C src push -u srv main ' @@ -385,7 +385,7 @@ setup_triangle () { for i in $(test_seq 1 100) do echo "make the tree big" >server/file$i && - git -C server add file$i + git -C server add file$i || return 1 done && git -C server commit -m "initial" && git clone --bare --filter=tree:0 "file://$(pwd)/server" client && @@ -669,7 +669,7 @@ test_expect_success 'tolerate server sending REF_DELTA against missing promisor for i in $(test_seq 10) do echo "this is a line" >>"$SERVER/foo.txt" && - echo "this is another line" >>"$SERVER/have.txt" + echo "this is another line" >>"$SERVER/have.txt" || return 1 done && git -C "$SERVER" add foo.txt have.txt && git -C "$SERVER" commit -m bar && diff --git a/t/t5701-git-serve.sh b/t/t5701-git-serve.sh index aa1827d841..1896f671cb 100755 --- a/t/t5701-git-serve.sh +++ b/t/t5701-git-serve.sh @@ -5,6 +5,7 @@ test_description='test protocol v2 server commands' 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 'test capability advertisement' ' diff --git a/t/t5702-protocol-v2.sh b/t/t5702-protocol-v2.sh index d527cf6c49..710f33e2aa 100755 --- a/t/t5702-protocol-v2.sh +++ b/t/t5702-protocol-v2.sh @@ -747,7 +747,7 @@ test_expect_success 'clone big repository with http:// using protocol v2' ' echo "data 0" && echo "M 644 inline bla.txt" && echo "data 4" && - echo "bla" + echo "bla" || return 1 done | git -C "$HTTPD_DOCUMENT_ROOT_PATH/big" fast-import && GIT_TRACE_PACKET="$(pwd)/log" GIT_TRACE_CURL="$(pwd)/log" git \ @@ -942,7 +942,7 @@ test_expect_success 'part of packfile response provided as URI' ' then >h2found fi - fi + fi || return 1 done && test -f hfound && test -f h2found && @@ -1107,6 +1107,57 @@ test_expect_success 'packfile-uri with transfer.fsckobjects fails when .gitmodul test_i18ngrep "disallowed submodule name" err ' +test_expect_success 'packfile-uri path redacted in trace' ' + P="$HTTPD_DOCUMENT_ROOT_PATH/http_parent" && + rm -rf "$P" http_child log && + + git init "$P" && + git -C "$P" config "uploadpack.allowsidebandall" "true" && + + echo my-blob >"$P/my-blob" && + git -C "$P" add my-blob && + git -C "$P" commit -m x && + + git -C "$P" hash-object my-blob >objh && + git -C "$P" pack-objects "$HTTPD_DOCUMENT_ROOT_PATH/mypack" <objh >packh && + git -C "$P" config --add \ + "uploadpack.blobpackfileuri" \ + "$(cat objh) $(cat packh) $HTTPD_URL/dumb/mypack-$(cat packh).pack" && + + GIT_TRACE_PACKET="$(pwd)/log" \ + git -c protocol.version=2 \ + -c fetch.uriprotocols=http,https \ + clone "$HTTPD_URL/smart/http_parent" http_child && + + grep -F "clone< \\1$(cat packh) $HTTPD_URL/<redacted>" log +' + +test_expect_success 'packfile-uri path not redacted in trace when GIT_TRACE_REDACT=0' ' + P="$HTTPD_DOCUMENT_ROOT_PATH/http_parent" && + rm -rf "$P" http_child log && + + git init "$P" && + git -C "$P" config "uploadpack.allowsidebandall" "true" && + + echo my-blob >"$P/my-blob" && + git -C "$P" add my-blob && + git -C "$P" commit -m x && + + git -C "$P" hash-object my-blob >objh && + git -C "$P" pack-objects "$HTTPD_DOCUMENT_ROOT_PATH/mypack" <objh >packh && + git -C "$P" config --add \ + "uploadpack.blobpackfileuri" \ + "$(cat objh) $(cat packh) $HTTPD_URL/dumb/mypack-$(cat packh).pack" && + + GIT_TRACE_PACKET="$(pwd)/log" \ + GIT_TRACE_REDACT=0 \ + git -c protocol.version=2 \ + -c fetch.uriprotocols=http,https \ + clone "$HTTPD_URL/smart/http_parent" http_child && + + grep -F "clone< \\1$(cat packh) $HTTPD_URL/dumb/mypack-$(cat packh).pack" log +' + test_expect_success 'http:// --negotiate-only' ' SERVER="$HTTPD_DOCUMENT_ROOT_PATH/server" && URI="$HTTPD_URL/smart/server" && diff --git a/t/t5703-upload-pack-ref-in-want.sh b/t/t5703-upload-pack-ref-in-want.sh index 220098523a..9d6cd7d986 100755 --- a/t/t5703-upload-pack-ref-in-want.sh +++ b/t/t5703-upload-pack-ref-in-want.sh @@ -2,9 +2,6 @@ test_description='upload-pack ref-in-want' -GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main -export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME - . ./test-lib.sh get_actual_refs () { diff --git a/t/t5704-protocol-violations.sh b/t/t5704-protocol-violations.sh index bc393d7c31..ae1a00afb0 100755 --- a/t/t5704-protocol-violations.sh +++ b/t/t5704-protocol-violations.sh @@ -4,6 +4,8 @@ test_description='Test responses to violations of the network protocol. In most of these cases it will generally be acceptable for one side to break off communications if the other side says something unexpected. We are mostly making sure that we do not segfault or otherwise behave badly.' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success 'extra delim packet in v2 ls-refs args' ' diff --git a/t/t5705-session-id-in-capabilities.sh b/t/t5705-session-id-in-capabilities.sh index eb8c79aafd..ed38c76c29 100755 --- a/t/t5705-session-id-in-capabilities.sh +++ b/t/t5705-session-id-in-capabilities.sh @@ -32,7 +32,6 @@ do test_when_finished "git -C local push --delete origin new-branch" && cp -r "$LOCAL_PRISTINE" local && git -C local pull --no-rebase origin && - GIT_TRACE2_EVENT_NESTING=5 \ GIT_TRACE2_EVENT="$(pwd)/tr2-client-events" \ git -c protocol.version=$PROTO -C local push \ --receive-pack "GIT_TRACE2_EVENT=\"$(pwd)/tr2-server-events\" git-receive-pack" \ @@ -65,7 +64,6 @@ do test_when_finished "git -C local push --delete origin new-branch" && cp -r "$LOCAL_PRISTINE" local && git -C local pull --no-rebase origin && - GIT_TRACE2_EVENT_NESTING=5 \ GIT_TRACE2_EVENT="$(pwd)/tr2-client-events" \ git -c protocol.version=$PROTO -C local push \ --receive-pack "GIT_TRACE2_EVENT=\"$(pwd)/tr2-server-events\" git-receive-pack" \ diff --git a/t/t6005-rev-list-count.sh b/t/t6005-rev-list-count.sh index 0b64822bf6..86542c650e 100755 --- a/t/t6005-rev-list-count.sh +++ b/t/t6005-rev-list-count.sh @@ -2,13 +2,14 @@ test_description='git rev-list --max-count and --skip test' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success 'setup' ' - for n in 1 2 3 4 5 ; do \ - echo $n > a ; \ - git add a ; \ - git commit -m "$n" ; \ + for n in 1 2 3 4 5 ; do + echo $n > a && + git add a && + git commit -m "$n" || return 1 done ' diff --git a/t/t6009-rev-list-parent.sh b/t/t6009-rev-list-parent.sh index 63fa7c8313..5a67bbc760 100755 --- a/t/t6009-rev-list-parent.sh +++ b/t/t6009-rev-list-parent.sh @@ -124,7 +124,7 @@ test_expect_success 'dodecapus' ' git checkout -b root$i five && test_commit $i && roots="$roots root$i" || - return + return 1 done && git checkout main && test_tick && @@ -142,8 +142,8 @@ test_expect_success 'ancestors with the same commit time' ' test_tick_keep=$test_tick && for i in 1 2 3 4 5 6 7 8; do - test_tick=$test_tick_keep - test_commit t$i + test_tick=$test_tick_keep && + test_commit t$i || return 1 done && git rev-list t1^! --not t$i >result && test_must_be_empty result diff --git a/t/t6019-rev-list-ancestry-path.sh b/t/t6019-rev-list-ancestry-path.sh index 20adbece65..af57a04b7f 100755 --- a/t/t6019-rev-list-ancestry-path.sh +++ b/t/t6019-rev-list-ancestry-path.sh @@ -51,7 +51,7 @@ test_expect_success setup ' ' test_expect_success 'rev-list D..M' ' - for c in E F G H I J K L M; do echo $c; done >expect && + test_write_lines E F G H I J K L M >expect && git rev-list --format=%s D..M | sed -e "/^commit /d" | sort >actual && @@ -59,7 +59,7 @@ test_expect_success 'rev-list D..M' ' ' test_expect_success 'rev-list --ancestry-path D..M' ' - for c in E F H I J L M; do echo $c; done >expect && + test_write_lines E F H I J L M >expect && git rev-list --ancestry-path --format=%s D..M | sed -e "/^commit /d" | sort >actual && @@ -81,7 +81,7 @@ test_expect_success 'rev-list --ancestry-path D..M -- M.t' ' ' test_expect_success 'rev-list F...I' ' - for c in F G H I; do echo $c; done >expect && + test_write_lines F G H I >expect && git rev-list --format=%s F...I | sed -e "/^commit /d" | sort >actual && @@ -89,7 +89,7 @@ test_expect_success 'rev-list F...I' ' ' test_expect_success 'rev-list --ancestry-path F...I' ' - for c in F H I; do echo $c; done >expect && + test_write_lines F H I >expect && git rev-list --ancestry-path --format=%s F...I | sed -e "/^commit /d" | sort >actual && @@ -111,7 +111,7 @@ test_expect_success 'rev-list --ancestry-path G..M -- G.t' ' ' test_expect_success 'rev-list --ancestry-path --simplify-merges G^..M -- G.t' ' - for c in G L; do echo $c; done >expect && + test_write_lines G L >expect && git rev-list --ancestry-path --simplify-merges --format=%s G^..M -- G.t | sed -e "/^commit /d" | sort >actual && diff --git a/t/t6060-merge-index.sh b/t/t6060-merge-index.sh index ddf34f0115..ed449abe55 100755 --- a/t/t6060-merge-index.sh +++ b/t/t6060-merge-index.sh @@ -4,9 +4,7 @@ test_description='basic git merge-index / git-merge-one-file tests' . ./test-lib.sh test_expect_success 'setup diverging branches' ' - for i in 1 2 3 4 5 6 7 8 9 10; do - echo $i - done >file && + test_write_lines 1 2 3 4 5 6 7 8 9 10 >file && git add file && git commit -m base && git tag base && diff --git a/t/t6101-rev-parse-parents.sh b/t/t6101-rev-parse-parents.sh index 78b5851780..c571fa5179 100755 --- a/t/t6101-rev-parse-parents.sh +++ b/t/t6101-rev-parse-parents.sh @@ -32,7 +32,7 @@ test_expect_success 'setup' ' test_tick && git commit --allow-empty -m "$i" && commit=$(git rev-parse --verify HEAD) && - printf "$commit " >>.git/info/grafts + printf "$commit " >>.git/info/grafts || return 1 done ' diff --git a/t/t6102-rev-list-unexpected-objects.sh b/t/t6102-rev-list-unexpected-objects.sh index 52cde097dd..6f0902b863 100755 --- a/t/t6102-rev-list-unexpected-objects.sh +++ b/t/t6102-rev-list-unexpected-objects.sh @@ -2,6 +2,7 @@ test_description='git rev-list should handle unexpected object types' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success 'setup well-formed objects' ' diff --git a/t/t6112-rev-list-filters-objects.sh b/t/t6112-rev-list-filters-objects.sh index 4ade105db3..8d9d6604f0 100755 --- a/t/t6112-rev-list-filters-objects.sh +++ b/t/t6112-rev-list-filters-objects.sh @@ -16,9 +16,9 @@ test_expect_success 'setup r1' ' git init r1 && for n in 1 2 3 4 5 do - echo "This is file: $n" > r1/file.$n - git -C r1 add file.$n - git -C r1 commit -m "$n" + echo "This is file: $n" > r1/file.$n && + git -C r1 add file.$n && + git -C r1 commit -m "$n" || return 1 done ' @@ -73,9 +73,9 @@ test_expect_success 'setup r2' ' git init r2 && for n in 1000 10000 do - printf "%"$n"s" X > r2/large.$n - git -C r2 add large.$n - git -C r2 commit -m "$n" + printf "%"$n"s" X > r2/large.$n && + git -C r2 add large.$n && + git -C r2 commit -m "$n" || return 1 done ' @@ -245,10 +245,10 @@ test_expect_success 'setup r3' ' mkdir r3/dir1 && for n in sparse1 sparse2 do - echo "This is file: $n" > r3/$n - git -C r3 add $n - echo "This is file: dir1/$n" > r3/dir1/$n - git -C r3 add dir1/$n + echo "This is file: $n" > r3/$n && + git -C r3 add $n && + echo "This is file: dir1/$n" > r3/dir1/$n && + git -C r3 add dir1/$n || return 1 done && git -C r3 commit -m "sparse" && echo dir1/ >pattern1 && @@ -672,7 +672,7 @@ test_expect_success 'rev-list W/ --missing=print' ' for id in `cat expected | sed "s|..|&/|"` do - rm r1/.git/objects/$id + rm r1/.git/objects/$id || return 1 done && git -C r1 rev-list --quiet --missing=print --objects HEAD >revs && diff --git a/t/t6120-describe.sh b/t/t6120-describe.sh index bae2419150..d8af2bb9d2 100755 --- a/t/t6120-describe.sh +++ b/t/t6120-describe.sh @@ -262,7 +262,7 @@ test_expect_success 'name-rev --all' ' >expect.unsorted && for rev in $(git rev-list --all) do - git name-rev $rev >>expect.unsorted + git name-rev $rev >>expect.unsorted || return 1 done && sort <expect.unsorted >expect && git name-rev --all >actual.unsorted && @@ -275,7 +275,7 @@ test_expect_success 'name-rev --stdin' ' for rev in $(git rev-list --all) do name=$(git name-rev --name-only $rev) && - echo "$rev ($name)" >>expect.unsorted + echo "$rev ($name)" >>expect.unsorted || return 1 done && sort <expect.unsorted >expect && git rev-list --all | git name-rev --stdin >actual.unsorted && @@ -390,9 +390,12 @@ test_expect_success ULIMIT_STACK_SIZE 'name-rev works in a deep repo' ' committer A U Thor <author@example.com> $((1000000000 + $i * 100)) +0200 data <<EOF commit #$i -EOF" - test $i = 1 && echo "from refs/heads/main^0" - i=$(($i + 1)) +EOF" && + if test $i = 1 + then + echo "from refs/heads/main^0" + fi && + i=$(($i + 1)) || return 1 done | git fast-import && git checkout main && git tag far-far-away HEAD^ && diff --git a/t/t6132-pathspec-exclude.sh b/t/t6132-pathspec-exclude.sh index 30328b87f0..8ff1d76f79 100755 --- a/t/t6132-pathspec-exclude.sh +++ b/t/t6132-pathspec-exclude.sh @@ -11,7 +11,7 @@ test_expect_success 'setup' ' fi && : >$p && git add $p && - git commit -m $p + git commit -m $p || return 1 done && git log --oneline --format=%s >actual && cat <<EOF >expect && diff --git a/t/t6136-pathspec-in-bare.sh b/t/t6136-pathspec-in-bare.sh index b117251366..ae8b5379e2 100755 --- a/t/t6136-pathspec-in-bare.sh +++ b/t/t6136-pathspec-in-bare.sh @@ -2,6 +2,7 @@ test_description='diagnosing out-of-scope pathspec' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success 'setup a bare and non-bare repository' ' diff --git a/t/t6200-fmt-merge-msg.sh b/t/t6200-fmt-merge-msg.sh index 06c5fb5615..7544245f90 100755 --- a/t/t6200-fmt-merge-msg.sh +++ b/t/t6200-fmt-merge-msg.sh @@ -91,6 +91,26 @@ test_expect_success GPGSSH 'created ssh signed commit and tag' ' git tag -s -u"${GPGSSH_KEY_UNTRUSTED}" -m signed-ssh-tag-msg-untrusted signed-untrusted-ssh-tag left ' +test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'create signed tags with keys having defined lifetimes' ' + test_when_finished "test_unconfig commit.gpgsign" && + test_config gpg.format ssh && + git checkout -b signed-expiry-ssh && + touch file && + git add file && + + echo expired >file && test_tick && git commit -a -m expired -S"${GPGSSH_KEY_EXPIRED}" && + git tag -s -u "${GPGSSH_KEY_EXPIRED}" -m expired-signed expired-signed && + + echo notyetvalid >file && test_tick && git commit -a -m notyetvalid -S"${GPGSSH_KEY_NOTYETVALID}" && + git tag -s -u "${GPGSSH_KEY_NOTYETVALID}" -m notyetvalid-signed notyetvalid-signed && + + echo timeboxedvalid >file && test_tick && git commit -a -m timeboxedvalid -S"${GPGSSH_KEY_TIMEBOXEDVALID}" && + git tag -s -u "${GPGSSH_KEY_TIMEBOXEDVALID}" -m timeboxedvalid-signed timeboxedvalid-signed && + + echo timeboxedinvalid >file && test_tick && git commit -a -m timeboxedinvalid -S"${GPGSSH_KEY_TIMEBOXEDINVALID}" && + git tag -s -u "${GPGSSH_KEY_TIMEBOXEDINVALID}" -m timeboxedinvalid-signed timeboxedinvalid-signed +' + test_expect_success 'message for merging local branch' ' echo "Merge branch ${apos}left${apos}" >expected && @@ -104,7 +124,7 @@ test_expect_success 'message for merging local branch' ' test_expect_success GPG 'message for merging local tag signed by good key' ' git checkout main && git fetch . signed-good-tag && - git fmt-merge-msg <.git/FETCH_HEAD >actual 2>&1 && + git fmt-merge-msg <.git/FETCH_HEAD >actual && grep "^Merge tag ${apos}signed-good-tag${apos}" actual && grep "^# gpg: Signature made" actual && grep "^# gpg: Good signature from" actual @@ -113,7 +133,7 @@ test_expect_success GPG 'message for merging local tag signed by good key' ' test_expect_success GPG 'message for merging local tag signed by unknown key' ' git checkout main && git fetch . signed-good-tag && - GNUPGHOME=. git fmt-merge-msg <.git/FETCH_HEAD >actual 2>&1 && + GNUPGHOME=. git fmt-merge-msg <.git/FETCH_HEAD >actual && grep "^Merge tag ${apos}signed-good-tag${apos}" actual && grep "^# gpg: Signature made" actual && grep -E "^# gpg: Can${apos}t check signature: (public key not found|No public key)" actual @@ -123,7 +143,8 @@ test_expect_success GPGSSH 'message for merging local tag signed by good ssh key test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && git checkout main && git fetch . signed-good-ssh-tag && - git fmt-merge-msg <.git/FETCH_HEAD >actual 2>&1 && + git fmt-merge-msg <.git/FETCH_HEAD >actual && + grep "^Merge tag ${apos}signed-good-ssh-tag${apos}" actual && grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual && ! grep "${GPGSSH_BAD_SIGNATURE}" actual ' @@ -132,11 +153,50 @@ test_expect_success GPGSSH 'message for merging local tag signed by unknown ssh test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && git checkout main && git fetch . signed-untrusted-ssh-tag && - git fmt-merge-msg <.git/FETCH_HEAD >actual 2>&1 && + git fmt-merge-msg <.git/FETCH_HEAD >actual && + grep "^Merge tag ${apos}signed-untrusted-ssh-tag${apos}" actual && grep "${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}" actual && ! grep "${GPGSSH_BAD_SIGNATURE}" actual && grep "${GPGSSH_KEY_NOT_TRUSTED}" actual ' + +test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'message for merging local tag signed by expired ssh key' ' + test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && + git checkout main && + git fetch . expired-signed && + git fmt-merge-msg <.git/FETCH_HEAD >actual && + grep "^Merge tag ${apos}expired-signed${apos}" actual && + ! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual +' + +test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'message for merging local tag signed by not yet valid ssh key' ' + test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && + git checkout main && + git fetch . notyetvalid-signed && + git fmt-merge-msg <.git/FETCH_HEAD >actual && + grep "^Merge tag ${apos}notyetvalid-signed${apos}" actual && + ! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual +' + +test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'message for merging local tag signed by valid timeboxed ssh key' ' + test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && + git checkout main && + git fetch . timeboxedvalid-signed && + git fmt-merge-msg <.git/FETCH_HEAD >actual && + grep "^Merge tag ${apos}timeboxedvalid-signed${apos}" actual && + grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual && + ! grep "${GPGSSH_BAD_SIGNATURE}" actual +' + +test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'message for merging local tag signed by invalid timeboxed ssh key' ' + test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && + git checkout main && + git fetch . timeboxedinvalid-signed && + git fmt-merge-msg <.git/FETCH_HEAD >actual && + grep "^Merge tag ${apos}timeboxedinvalid-signed${apos}" actual && + ! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual +' + test_expect_success 'message for merging external branch' ' echo "Merge branch ${apos}left${apos} of $(pwd)" >expected && @@ -519,7 +579,7 @@ test_expect_success 'merge-msg lots of commits' ' while test $i -gt 9 do echo " $i" && - i=$(($i-1)) + i=$(($i-1)) || return 1 done && echo " ..." } >expected && @@ -573,7 +633,35 @@ test_expect_success 'merge-msg with "merging" an annotated tag' ' test_cmp expected .git/MERGE_MSG ' +test_expect_success 'merge --into-name=<name>' ' + test_when_finished "git checkout main" && + git checkout -B side main && + git commit --allow-empty -m "One step ahead" && + + git checkout --detach main && + git merge --no-ff side && + git show -s --format="%s" >full.0 && + head -n1 full.0 >actual && + # expect that HEAD is shown as-is + grep -e "Merge branch .side. into HEAD$" actual && + + git reset --hard main && + git merge --no-ff --into-name=main side && + git show -s --format="%s" >full.1 && + head -n1 full.1 >actual && + # expect that we pretend to be merging to main, that is suppressed + grep -e "Merge branch .side.$" actual && + + git checkout -b throwaway main && + git merge --no-ff --into-name=main side && + git show -s --format="%s" >full.2 && + head -n1 full.2 >actual && + # expect that we pretend to be merging to main, that is suppressed + grep -e "Merge branch .side.$" actual +' + test_expect_success 'merge.suppressDest configuration' ' + test_when_finished "git checkout main" && git checkout -B side main && git commit --allow-empty -m "One step ahead" && git checkout main && @@ -590,7 +678,19 @@ test_expect_success 'merge.suppressDest configuration' ' git -c merge.suppressDest="ma?*[rn]" fmt-merge-msg <.git/FETCH_HEAD >full.3 && head -n1 full.3 >actual && grep -e "Merge branch .side." actual && - ! grep -e " into main$" actual + ! grep -e " into main$" actual && + + git checkout --detach HEAD && + git -c merge.suppressDest="main" fmt-merge-msg <.git/FETCH_HEAD >full.4 && + head -n1 full.4 >actual && + grep -e "Merge branch .side. into HEAD$" actual && + + git -c merge.suppressDest="main" fmt-merge-msg \ + --into-name=main <.git/FETCH_HEAD >full.5 && + head -n1 full.5 >actual && + grep -e "Merge branch .side." actual && + ! grep -e " into main$" actual && + ! grep -e " into HEAD$" actual ' test_done diff --git a/t/t6300-for-each-ref.sh b/t/t6300-for-each-ref.sh index 80679d5e12..dcaab7265f 100755 --- a/t/t6300-for-each-ref.sh +++ b/t/t6300-for-each-ref.sh @@ -5,9 +5,6 @@ test_description='for-each-ref test' -GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=master -export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME - . ./test-lib.sh . "$TEST_DIRECTORY"/lib-gpg.sh . "$TEST_DIRECTORY"/lib-terminal.sh @@ -419,6 +416,11 @@ test_expect_success 'Verify descending sort' ' test_cmp expected actual ' +test_expect_success 'Give help even with invalid sort atoms' ' + test_expect_code 129 git for-each-ref --sort=bogus -h >actual 2>&1 && + grep "^usage: git for-each-ref" actual +' + cat >expected <<\EOF refs/tags/testtag refs/tags/testtag-2 @@ -950,10 +952,7 @@ test_expect_success '%(raw) with --shell and --sort=raw must fail' ' ' test_expect_success '%(raw:size) with --shell' ' - git for-each-ref --format="%(raw:size)" | while read line - do - echo "'\''$line'\''" >>expect - done && + git for-each-ref --format="%(raw:size)" | sed "s/^/$SQ/;s/$/$SQ/" >expect && git for-each-ref --format="%(raw:size)" --shell >actual && test_cmp expect actual ' @@ -1019,6 +1018,27 @@ test_expect_success 'equivalent sorts fall back on refname' ' test_cmp expected actual ' +test_expect_success '--no-sort cancels the previous sort keys' ' + cat >expected <<-\EOF && + 100000 <user1@example.com> refs/tags/multi-ref1-100000-user1 + 100000 <user2@example.com> refs/tags/multi-ref1-100000-user2 + 100000 <user1@example.com> refs/tags/multi-ref2-100000-user1 + 100000 <user2@example.com> refs/tags/multi-ref2-100000-user2 + 200000 <user1@example.com> refs/tags/multi-ref1-200000-user1 + 200000 <user2@example.com> refs/tags/multi-ref1-200000-user2 + 200000 <user1@example.com> refs/tags/multi-ref2-200000-user1 + 200000 <user2@example.com> refs/tags/multi-ref2-200000-user2 + EOF + git for-each-ref \ + --format="%(taggerdate:unix) %(taggeremail) %(refname)" \ + --sort=-refname \ + --sort=taggeremail \ + --no-sort \ + --sort=taggerdate \ + "refs/tags/multi-*" >actual && + test_cmp expected actual +' + test_expect_success 'do not dereference NULL upon %(HEAD) on unborn branch' ' test_when_finished "git checkout main" && git for-each-ref --format="%(HEAD) %(refname:short)" refs/heads/ >actual && @@ -1315,7 +1335,7 @@ test_expect_success ':remotename and :remoteref' ' echo "${pair#*=}" >expect && git for-each-ref --format="${pair%=*}" \ refs/heads/main >actual && - test_cmp expect actual + test_cmp expect actual || exit 1 done && git branch push-simple && git config branch.push-simple.pushRemote from && diff --git a/t/t6302-for-each-ref-filter.sh b/t/t6302-for-each-ref-filter.sh index 1537aa2179..1ce5f490e9 100755 --- a/t/t6302-for-each-ref-filter.sh +++ b/t/t6302-for-each-ref-filter.sh @@ -2,9 +2,6 @@ test_description='test for-each-refs usage of ref-filter APIs' -GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main -export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME - . ./test-lib.sh . "$TEST_DIRECTORY"/lib-gpg.sh diff --git a/t/t6406-merge-attr.sh b/t/t6406-merge-attr.sh index 8494645837..57e6af5eaa 100755 --- a/t/t6406-merge-attr.sh +++ b/t/t6406-merge-attr.sh @@ -62,10 +62,10 @@ test_expect_success setup ' test_expect_success merge ' - { - echo "binary -merge" - echo "union merge=union" - } >.gitattributes && + cat >.gitattributes <<-\EOF && + binary -merge + union merge=union + EOF if git merge main then diff --git a/t/t6407-merge-binary.sh b/t/t6407-merge-binary.sh index d4273f2575..8e6241f92e 100755 --- a/t/t6407-merge-binary.sh +++ b/t/t6407-merge-binary.sh @@ -5,6 +5,7 @@ test_description='ask merge-recursive to merge binary files' 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 ' @@ -47,7 +48,7 @@ test_expect_success resolve ' echo Oops, should not have succeeded false else - git ls-files -s >current + git ls-files -s >current && test_cmp expect current fi ' @@ -62,7 +63,7 @@ test_expect_success recursive ' echo Oops, should not have succeeded false else - git ls-files -s >current + git ls-files -s >current && test_cmp expect current fi ' diff --git a/t/t6409-merge-subtree.sh b/t/t6409-merge-subtree.sh index ba7890ec52..e9ba6f1690 100755 --- a/t/t6409-merge-subtree.sh +++ b/t/t6409-merge-subtree.sh @@ -10,7 +10,7 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME test_expect_success setup ' s="1 2 3 4 5 6 7 8" && - for i in $s; do echo $i; done >hello && + test_write_lines $s >hello && git add hello && git commit -m initial && git checkout -b side && @@ -18,7 +18,7 @@ test_expect_success setup ' git add hello && git commit -m second && git checkout main && - for i in mundo $s; do echo $i; done >hello && + test_write_lines mundo $s >hello && git add hello && git commit -m main @@ -27,7 +27,7 @@ test_expect_success setup ' test_expect_success 'subtree available and works like recursive' ' git merge -s subtree side && - for i in mundo $s world; do echo $i; done >expect && + test_write_lines mundo $s world >expect && test_cmp expect hello ' diff --git a/t/t6411-merge-filemode.sh b/t/t6411-merge-filemode.sh index f54c915d6a..6ae2489286 100755 --- a/t/t6411-merge-filemode.sh +++ b/t/t6411-merge-filemode.sh @@ -51,10 +51,10 @@ test_expect_success 'set up mode change in both branches' ' : >file2 && git add file2 && git commit -m b2 && - { - echo "100755 $H 2 file2" - echo "100644 $H 3 file2" - } >expect + cat >expect <<-EOF + 100755 $H 2 file2 + 100644 $H 3 file2 + EOF ' do_both_modes () { diff --git a/t/t6412-merge-large-rename.sh b/t/t6412-merge-large-rename.sh index c50d315722..ca018d11f5 100755 --- a/t/t6412-merge-large-rename.sh +++ b/t/t6412-merge-large-rename.sh @@ -37,18 +37,18 @@ test_rename() { test_might_fail git branch -D test$n && git reset --hard initial && for i in $(count $n); do - make_text $i initial initial >$i + make_text $i initial initial >$i || return 1 done && git add . && git commit -m add=$n && for i in $(count $n); do - make_text $i changed initial >$i + make_text $i changed initial >$i || return 1 done && git commit -a -m change=$n && git checkout -b test$n HEAD^ && for i in $(count $n); do - git rm $i - make_text $i initial changed >$i.moved + git rm $i && + make_text $i initial changed >$i.moved || return 1 done && git add . && git commit -m change+rename=$n && @@ -79,7 +79,7 @@ test_expect_success 'setup large simple rename' ' git reset --hard initial && for i in $(count 200); do - make_text foo bar baz >$i + make_text foo bar baz >$i || return 1 done && git add . && git commit -m create-files && diff --git a/t/t6414-merge-rename-nocruft.sh b/t/t6414-merge-rename-nocruft.sh index d7e3c1fa6e..69fc1c9e69 100755 --- a/t/t6414-merge-rename-nocruft.sh +++ b/t/t6414-merge-rename-nocruft.sh @@ -4,6 +4,7 @@ test_description='Merge-recursive merging 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/t6416-recursive-corner-cases.sh b/t/t6416-recursive-corner-cases.sh index 84f5082366..690c8482b1 100755 --- a/t/t6416-recursive-corner-cases.sh +++ b/t/t6416-recursive-corner-cases.sh @@ -24,14 +24,8 @@ test_expect_success 'setup basic criss-cross + rename with no modifications' ' cd basic-rename && ten="0 1 2 3 4 5 6 7 8 9" && - for i in $ten - do - echo line $i in a sample file - done >one && - for i in $ten - do - echo line $i in another sample file - done >two && + printf "line %d in a sample file\n" $ten >one && + printf "line %d in another sample file\n" $ten >two && git add one two && test_tick && git commit -m initial && @@ -96,14 +90,8 @@ test_expect_success 'setup criss-cross + rename merges with basic modification' cd rename-modify && ten="0 1 2 3 4 5 6 7 8 9" && - for i in $ten - do - echo line $i in a sample file - done >one && - for i in $ten - do - echo line $i in another sample file - done >two && + printf "line %d in a sample file\n" $ten >one && + printf "line %d in another sample file\n" $ten >two && git add one two && test_tick && git commit -m initial && @@ -1588,10 +1576,7 @@ test_expect_success 'setup nested conflicts' ' cd nested_conflicts && # Create some related files now - for i in $(test_seq 1 10) - do - echo Random base content line $i - done >initial && + printf "Random base content line %d\n" $(test_seq 1 10) >initial && cp initial b_L1 && cp initial b_R1 && @@ -1777,10 +1762,7 @@ test_expect_success 'setup virtual merge base with nested conflicts' ' cd virtual_merge_base_has_nested_conflicts && # Create some related files now - for i in $(test_seq 1 10) - do - echo Random base content line $i - done >content && + printf "Random base content line %d\n" $(test_seq 1 10) >content && # Setup original commit git add content && diff --git a/t/t6417-merge-ours-theirs.sh b/t/t6417-merge-ours-theirs.sh index ec065d6a65..62d1406119 100755 --- a/t/t6417-merge-ours-theirs.sh +++ b/t/t6417-merge-ours-theirs.sh @@ -7,10 +7,7 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME . ./test-lib.sh test_expect_success setup ' - for i in 1 2 3 4 5 6 7 8 9 - do - echo "$i" - done >file && + test_write_lines 1 2 3 4 5 6 7 8 9 >file && git add file && cp file elif && git commit -m initial && diff --git a/t/t6427-diff3-conflict-markers.sh b/t/t6427-diff3-conflict-markers.sh index 25c4b720e7..a9ee4cb207 100755 --- a/t/t6427-diff3-conflict-markers.sh +++ b/t/t6427-diff3-conflict-markers.sh @@ -211,4 +211,94 @@ test_expect_success 'rebase --apply describes fake ancestor base' ' ) ' +test_setup_zdiff3 () { + test_create_repo zdiff3 && + ( + cd zdiff3 && + + test_write_lines 1 2 3 4 5 6 7 8 9 >basic && + test_write_lines 1 2 3 AA 4 5 BB 6 7 8 >middle-common && + test_write_lines 1 2 3 4 5 6 7 8 9 >interesting && + test_write_lines 1 2 3 4 5 6 7 8 9 >evil && + + git add basic middle-common interesting evil && + git commit -m base && + + git branch left && + git branch right && + + git checkout left && + test_write_lines 1 2 3 4 A B C D E 7 8 9 >basic && + test_write_lines 1 2 3 CC 4 5 DD 6 7 8 >middle-common && + test_write_lines 1 2 3 4 A B C D E F G H I J 7 8 9 >interesting && + test_write_lines 1 2 3 4 X A B C 7 8 9 >evil && + git add -u && + git commit -m letters && + + git checkout right && + test_write_lines 1 2 3 4 A X C Y E 7 8 9 >basic && + test_write_lines 1 2 3 EE 4 5 FF 6 7 8 >middle-common && + test_write_lines 1 2 3 4 A B C 5 6 G H I J 7 8 9 >interesting && + test_write_lines 1 2 3 4 Y A B C B C 7 8 9 >evil && + git add -u && + git commit -m permuted + ) +} + +test_expect_success 'check zdiff3 markers' ' + test_setup_zdiff3 && + ( + cd zdiff3 && + + git checkout left^0 && + + base=$(git rev-parse --short HEAD^1) && + test_must_fail git -c merge.conflictstyle=zdiff3 merge -s recursive right^0 && + + test_write_lines 1 2 3 4 A \ + "<<<<<<< HEAD" B C D \ + "||||||| $base" 5 6 \ + ======= X C Y \ + ">>>>>>> right^0" \ + E 7 8 9 \ + >expect && + test_cmp expect basic && + + test_write_lines 1 2 3 \ + "<<<<<<< HEAD" CC \ + "||||||| $base" AA \ + ======= EE \ + ">>>>>>> right^0" \ + 4 5 \ + "<<<<<<< HEAD" DD \ + "||||||| $base" BB \ + ======= FF \ + ">>>>>>> right^0" \ + 6 7 8 \ + >expect && + test_cmp expect middle-common && + + test_write_lines 1 2 3 4 A B C \ + "<<<<<<< HEAD" D E F \ + "||||||| $base" 5 6 \ + ======= 5 6 \ + ">>>>>>> right^0" \ + G H I J 7 8 9 \ + >expect && + test_cmp expect interesting && + + # Not passing this one yet; the common "B C" lines is still + # being left in the conflict blocks on the left and right + # sides. + test_write_lines 1 2 3 4 \ + "<<<<<<< HEAD" X A \ + "||||||| $base" 5 6 \ + ======= Y A B C \ + ">>>>>>> right^0" \ + B C 7 8 9 \ + >expect && + test_cmp expect evil + ) +' + test_done diff --git a/t/t6430-merge-recursive.sh b/t/t6430-merge-recursive.sh index a0efe7cb6d..07067bb347 100755 --- a/t/t6430-merge-recursive.sh +++ b/t/t6430-merge-recursive.sh @@ -706,7 +706,7 @@ test_expect_success 'merge-recursive remembers the names of all base trees' ' # more trees than static slots used by oid_to_hex() for commit in $c0 $c2 $c4 $c5 $c6 $c7 do - git rev-parse "$commit^{tree}" + git rev-parse "$commit^{tree}" || return 1 done >trees && # ignore the return code; it only fails because the input is weird... diff --git a/t/t6600-test-reach.sh b/t/t6600-test-reach.sh index 3d7a62ddab..338a9c46a2 100755 --- a/t/t6600-test-reach.sh +++ b/t/t6600-test-reach.sh @@ -32,7 +32,7 @@ test_expect_success 'setup' ' do test_commit "1-$i" && git branch -f commit-1-$i && - git tag -a -m "1-$i" tag-1-$i commit-1-$i + git tag -a -m "1-$i" tag-1-$i commit-1-$i || return 1 done && for j in $(test_seq 1 9) do @@ -46,7 +46,7 @@ test_expect_success 'setup' ' do git merge commit-$j-$i -m "$x-$i" && git branch -f commit-$x-$i && - git tag -a -m "$x-$i" tag-$x-$i commit-$x-$i + git tag -a -m "$x-$i" tag-$x-$i commit-$x-$i || return 1 done done && git commit-graph write --reachable && diff --git a/t/t7004-tag.sh b/t/t7004-tag.sh index 082be85dff..9aa1660651 100755 --- a/t/t7004-tag.sh +++ b/t/t7004-tag.sh @@ -94,10 +94,10 @@ test_expect_success 'creating a tag with --create-reflog should create reflog' ' git log -1 \ --format="format:tag: tagging %h (%s, %cd)%n" \ --date=format:%Y-%m-%d >expected && - test_when_finished "git tag -d tag_with_reflog" && - git tag --create-reflog tag_with_reflog && - git reflog exists refs/tags/tag_with_reflog && - sed -e "s/^.* //" .git/logs/refs/tags/tag_with_reflog >actual && + test_when_finished "git tag -d tag_with_reflog1" && + git tag --create-reflog tag_with_reflog1 && + git reflog exists refs/tags/tag_with_reflog1 && + test-tool ref-store main for-each-reflog-ent refs/tags/tag_with_reflog1 | sed -e "s/^.* //" >actual && test_cmp expected actual ' @@ -105,10 +105,10 @@ test_expect_success 'annotated tag with --create-reflog has correct message' ' git log -1 \ --format="format:tag: tagging %h (%s, %cd)%n" \ --date=format:%Y-%m-%d >expected && - test_when_finished "git tag -d tag_with_reflog" && - git tag -m "annotated tag" --create-reflog tag_with_reflog && - git reflog exists refs/tags/tag_with_reflog && - sed -e "s/^.* //" .git/logs/refs/tags/tag_with_reflog >actual && + test_when_finished "git tag -d tag_with_reflog2" && + git tag -m "annotated tag" --create-reflog tag_with_reflog2 && + git reflog exists refs/tags/tag_with_reflog2 && + test-tool ref-store main for-each-reflog-ent refs/tags/tag_with_reflog2 | sed -e "s/^.* //" >actual && test_cmp expected actual ' @@ -118,10 +118,10 @@ test_expect_success '--create-reflog does not create reflog on failure' ' ' test_expect_success 'option core.logAllRefUpdates=always creates reflog' ' - test_when_finished "git tag -d tag_with_reflog" && + test_when_finished "git tag -d tag_with_reflog3" && test_config core.logAllRefUpdates always && - git tag tag_with_reflog && - git reflog exists refs/tags/tag_with_reflog + git tag tag_with_reflog3 && + git reflog exists refs/tags/tag_with_reflog3 ' test_expect_success 'listing all tags if one exists should succeed' ' @@ -1976,9 +1976,12 @@ test_expect_success ULIMIT_STACK_SIZE '--contains and --no-contains work in a de committer A U Thor <author@example.com> $((1000000000 + $i * 100)) +0200 data <<EOF commit #$i -EOF" - test $i = 1 && echo "from refs/heads/main^0" - i=$(($i + 1)) +EOF" && + if test $i = 1 + then + echo "from refs/heads/main^0" + fi && + i=$(($i + 1)) || return 1 done | git fast-import && git checkout main && git tag far-far-away HEAD^ && diff --git a/t/t7006-pager.sh b/t/t7006-pager.sh index 0e7cf75435..e56ca5b0fa 100755 --- a/t/t7006-pager.sh +++ b/t/t7006-pager.sh @@ -661,6 +661,13 @@ test_expect_success 'setup trace2' ' export GIT_TRACE2_BRIEF ' +test_expect_success 'setup large log output' ' + perl -e " + print \"this is a long commit message\" x 50000 + " >commit-msg && + git commit --allow-empty -F commit-msg +' + test_expect_success TTY 'git returns SIGPIPE on early pager exit' ' test_when_finished "rm pager-used trace.normal" && test_config core.pager ">pager-used; head -n 1; exit 0" && @@ -670,7 +677,7 @@ test_expect_success TTY 'git returns SIGPIPE on early pager exit' ' if test_have_prereq !MINGW then - OUT=$( ((test_terminal git log; echo $? 1>&3) | :) 3>&1 ) && + { test_terminal git log >/dev/null; OUT=$?; } && test_match_signal 13 "$OUT" else test_terminal git log @@ -691,7 +698,7 @@ test_expect_success TTY 'git returns SIGPIPE on early pager non-zero exit' ' if test_have_prereq !MINGW then - OUT=$( ((test_terminal git log; echo $? 1>&3) | :) 3>&1 ) && + { test_terminal git log >/dev/null; OUT=$?; } && test_match_signal 13 "$OUT" else test_terminal git log @@ -710,13 +717,7 @@ test_expect_success TTY 'git discards pager non-zero exit without SIGPIPE' ' export GIT_TRACE2 && test_when_finished "unset GIT_TRACE2" && - if test_have_prereq !MINGW - then - OUT=$( ((test_terminal git log; echo $? 1>&3) | :) 3>&1 ) && - test "$OUT" -eq 0 - else - test_terminal git log - fi && + test_terminal git log && grep child_exit trace.normal >child-exits && test_line_count = 1 child-exits && @@ -724,41 +725,14 @@ test_expect_success TTY 'git discards pager non-zero exit without SIGPIPE' ' test_path_is_file pager-used ' -test_expect_success TTY 'git discards nonexisting pager without SIGPIPE' ' - test_when_finished "rm pager-used trace.normal" && - test_config core.pager "wc >pager-used; does-not-exist" && - GIT_TRACE2="$(pwd)/trace.normal" && - export GIT_TRACE2 && - test_when_finished "unset GIT_TRACE2" && - - if test_have_prereq !MINGW - then - OUT=$( ((test_terminal git log; echo $? 1>&3) | :) 3>&1 ) && - test "$OUT" -eq 0 - else - test_terminal git log - fi && - - grep child_exit trace.normal >child-exits && - test_line_count = 1 child-exits && - grep " code:127 " child-exits && - test_path_is_file pager-used -' - -test_expect_success TTY 'git attempts to page to nonexisting pager command, gets SIGPIPE' ' +test_expect_success TTY 'git skips paging nonexisting command' ' test_when_finished "rm trace.normal" && test_config core.pager "does-not-exist" && GIT_TRACE2="$(pwd)/trace.normal" && export GIT_TRACE2 && test_when_finished "unset GIT_TRACE2" && - if test_have_prereq !MINGW - then - OUT=$( ((test_terminal git log; echo $? 1>&3) | :) 3>&1 ) && - test_match_signal 13 "$OUT" - else - test_terminal git log - fi && + test_terminal git log && grep child_exit trace.normal >child-exits && test_line_count = 1 child-exits && @@ -767,14 +741,14 @@ test_expect_success TTY 'git attempts to page to nonexisting pager command, gets test_expect_success TTY 'git returns SIGPIPE on propagated signals from pager' ' test_when_finished "rm pager-used trace.normal" && - test_config core.pager ">pager-used; test-tool sigchain" && + test_config core.pager ">pager-used; exec test-tool sigchain" && GIT_TRACE2="$(pwd)/trace.normal" && export GIT_TRACE2 && test_when_finished "unset GIT_TRACE2" && if test_have_prereq !MINGW then - OUT=$( ((test_terminal git log; echo $? 1>&3) | :) 3>&1 ) && + { test_terminal git log >/dev/null; OUT=$?; } && test_match_signal 13 "$OUT" else test_terminal git log @@ -786,4 +760,9 @@ test_expect_success TTY 'git returns SIGPIPE on propagated signals from pager' ' test_path_is_file pager-used ' +test_expect_success TTY 'non-existent pager doesnt cause crash' ' + test_config pager.show invalid-pager && + test_terminal git show +' + test_done diff --git a/t/t7010-setup.sh b/t/t7010-setup.sh index 0335a9a158..520f96d09f 100755 --- a/t/t7010-setup.sh +++ b/t/t7010-setup.sh @@ -137,7 +137,7 @@ test_expect_success 'setup deeper work tree' ' test_expect_success 'add a directory outside the work tree' '( cd tester && - d1="$(cd .. ; pwd)" && + d1="$(cd .. && pwd)" && test_must_fail git add "$d1" )' diff --git a/t/t7031-verify-tag-signed-ssh.sh b/t/t7031-verify-tag-signed-ssh.sh index 06c9dd6c93..1cb36b9ab8 100755 --- a/t/t7031-verify-tag-signed-ssh.sh +++ b/t/t7031-verify-tag-signed-ssh.sh @@ -48,6 +48,23 @@ test_expect_success GPGSSH 'create signed tags ssh' ' git tag -u"${GPGSSH_KEY_UNTRUSTED}" -m eighth eighth-signed-alt ' +test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'create signed tags with keys having defined lifetimes' ' + test_when_finished "test_unconfig commit.gpgsign" && + test_config gpg.format ssh && + + echo expired >file && test_tick && git commit -a -m expired -S"${GPGSSH_KEY_EXPIRED}" && + git tag -s -u "${GPGSSH_KEY_EXPIRED}" -m expired-signed expired-signed && + + echo notyetvalid >file && test_tick && git commit -a -m notyetvalid -S"${GPGSSH_KEY_NOTYETVALID}" && + git tag -s -u "${GPGSSH_KEY_NOTYETVALID}" -m notyetvalid-signed notyetvalid-signed && + + echo timeboxedvalid >file && test_tick && git commit -a -m timeboxedvalid -S"${GPGSSH_KEY_TIMEBOXEDVALID}" && + git tag -s -u "${GPGSSH_KEY_TIMEBOXEDVALID}" -m timeboxedvalid-signed timeboxedvalid-signed && + + echo timeboxedinvalid >file && test_tick && git commit -a -m timeboxedinvalid -S"${GPGSSH_KEY_TIMEBOXEDINVALID}" && + git tag -s -u "${GPGSSH_KEY_TIMEBOXEDINVALID}" -m timeboxedinvalid-signed timeboxedinvalid-signed +' + test_expect_success GPGSSH 'verify and show ssh signatures' ' test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && ( @@ -80,6 +97,31 @@ test_expect_success GPGSSH 'verify and show ssh signatures' ' ) ' +test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'verify-tag exits failure on expired signature key' ' + test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && + test_must_fail git verify-tag expired-signed 2>actual && + ! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual +' + +test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'verify-tag exits failure on not yet valid signature key' ' + test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && + test_must_fail git verify-tag notyetvalid-signed 2>actual && + ! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual +' + +test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'verify-tag succeeds with tag date and key validity matching' ' + test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && + git verify-tag timeboxedvalid-signed 2>actual && + grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual && + ! grep "${GPGSSH_BAD_SIGNATURE}" actual +' + +test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'verify-tag failes with tag date outside of key validity' ' + test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && + test_must_fail git verify-tag timeboxedinvalid-signed 2>actual && + ! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual +' + test_expect_success GPGSSH 'detect fudged ssh signature' ' test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && git cat-file tag seventh-signed >raw && diff --git a/t/t7064-wtstatus-pv2.sh b/t/t7064-wtstatus-pv2.sh index eeb0534163..20a0d2afc2 100755 --- a/t/t7064-wtstatus-pv2.sh +++ b/t/t7064-wtstatus-pv2.sh @@ -4,10 +4,6 @@ test_description='git status --porcelain=v2 This test exercises porcelain V2 output for git status.' - -GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=master -export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME - . ./test-lib.sh @@ -113,6 +109,21 @@ test_expect_success 'after first commit, create unstaged changes' ' test_cmp expect actual ' +test_expect_success 'after first commit, stash existing changes' ' + cat >expect <<-EOF && + # branch.oid $H0 + # branch.head initial-branch + # stash 2 + EOF + + test_when_finished "git stash pop && git stash pop" && + + git stash -- file_x && + git stash && + git status --porcelain=v2 --branch --show-stash --untracked-files=no >actual && + test_cmp expect actual +' + test_expect_success 'after first commit but omit untracked files and branch' ' cat >expect <<-EOF && 1 .M N... 100644 100644 100644 $OID_X $OID_X file_x diff --git a/t/t7101-reset-empty-subdirs.sh b/t/t7101-reset-empty-subdirs.sh index 5530651eea..638bb04e21 100755 --- a/t/t7101-reset-empty-subdirs.sh +++ b/t/t7101-reset-empty-subdirs.sh @@ -4,6 +4,8 @@ # test_description='git reset should cull empty subdirs' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh . "$TEST_DIRECTORY"/lib-diff-data.sh diff --git a/t/t7102-reset.sh b/t/t7102-reset.sh index 601b2bf97f..d05426062e 100755 --- a/t/t7102-reset.sh +++ b/t/t7102-reset.sh @@ -472,6 +472,23 @@ test_expect_success '--mixed refreshes the index' ' test_cmp expect output ' +test_expect_success '--mixed preserves skip-worktree' ' + echo 123 >>file2 && + git add file2 && + git update-index --skip-worktree file2 && + git reset --mixed HEAD >output && + test_must_be_empty output && + + cat >expect <<-\EOF && + Unstaged changes after reset: + M file2 + EOF + git update-index --no-skip-worktree file2 && + git add file2 && + git reset --mixed HEAD >output && + test_cmp expect output +' + test_expect_success 'resetting specific path that is unmerged' ' git rm --cached file2 && F1=$(git rev-parse HEAD:file1) && diff --git a/t/t7103-reset-bare.sh b/t/t7103-reset-bare.sh index afe36a533c..0de83e3619 100755 --- a/t/t7103-reset-bare.sh +++ b/t/t7103-reset-bare.sh @@ -1,6 +1,8 @@ #!/bin/sh test_description='git reset in a bare repository' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success 'setup non-bare' ' diff --git a/t/t7110-reset-merge.sh b/t/t7110-reset-merge.sh index a82a07a04a..3d62e10b53 100755 --- a/t/t7110-reset-merge.sh +++ b/t/t7110-reset-merge.sh @@ -8,7 +8,7 @@ test_description='Tests for "git reset" with "--merge" and "--keep" options' . ./test-lib.sh test_expect_success setup ' - for i in 1 2 3; do echo line $i; done >file1 && + printf "line %d\n" 1 2 3 >file1 && cat file1 >file2 && git add file1 file2 && test_tick && diff --git a/t/t7113-post-index-change-hook.sh b/t/t7113-post-index-change-hook.sh index 688fa995c9..a21781d68a 100755 --- a/t/t7113-post-index-change-hook.sh +++ b/t/t7113-post-index-change-hook.sh @@ -5,6 +5,7 @@ test_description='post index change hook' 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/t7400-submodule-basic.sh b/t/t7400-submodule-basic.sh index cb1b8e35db..e7cec2e457 100755 --- a/t/t7400-submodule-basic.sh +++ b/t/t7400-submodule-basic.sh @@ -1182,18 +1182,17 @@ test_expect_success 'submodule deinit is silent when used on an uninitialized su rmdir init example2 ' -test_expect_success 'submodule deinit fails when submodule has a .git directory even when forced' ' +test_expect_success 'submodule deinit absorbs .git directory if .git is a directory' ' git submodule update --init && ( cd init && rm .git && - cp -R ../.git/modules/example .git && + mv ../.git/modules/example .git && GIT_WORK_TREE=. git config --unset core.worktree ) && - test_must_fail git submodule deinit init && - test_must_fail git submodule deinit -f init && - test -d init/.git && - test -n "$(git config --get-regexp "submodule\.example\.")" + git submodule deinit init && + test_path_is_missing init/.git && + test -z "$(git config --get-regexp "submodule\.example\.")" ' test_expect_success 'submodule with UTF-8 name' ' diff --git a/t/t7501-commit-basic-functionality.sh b/t/t7501-commit-basic-functionality.sh index 512ae2781f..fb5417d5e7 100755 --- a/t/t7501-commit-basic-functionality.sh +++ b/t/t7501-commit-basic-functionality.sh @@ -667,10 +667,7 @@ test_expect_success 'amend can copy notes' ' test_expect_success 'commit a file whose name is a dash' ' git reset --hard && - for i in 1 2 3 4 5 - do - echo $i - done >./- && + test_write_lines 1 2 3 4 5 >./- && git add ./- && test_tick && git commit -m "add dash" >output </dev/null && diff --git a/t/t7505-prepare-commit-msg-hook.sh b/t/t7505-prepare-commit-msg-hook.sh index 2a07c70867..e39c809ca4 100755 --- a/t/t7505-prepare-commit-msg-hook.sh +++ b/t/t7505-prepare-commit-msg-hook.sh @@ -16,7 +16,7 @@ test_expect_success 'set up commits for rebasing' ' test_commit rebase-b b bb && for i in $(test_seq 1 13) do - test_commit rebase-$i c $i + test_commit rebase-$i c $i || return 1 done && git checkout main && diff --git a/t/t7509-commit-authorship.sh b/t/t7509-commit-authorship.sh index d568593382..21c668f75e 100755 --- a/t/t7509-commit-authorship.sh +++ b/t/t7509-commit-authorship.sh @@ -5,6 +5,7 @@ test_description='commit tests of various authorhip options. ' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh author_header () { diff --git a/t/t7510-signed-commit.sh b/t/t7510-signed-commit.sh index d65a0171f2..9882b69ae2 100755 --- a/t/t7510-signed-commit.sh +++ b/t/t7510-signed-commit.sh @@ -228,7 +228,7 @@ test_expect_success GPG 'detect fudged signature with NUL' ' ' test_expect_success GPG 'amending already signed commit' ' - git checkout fourth-signed^0 && + git checkout -f fourth-signed^0 && git commit --amend -S --no-edit && git verify-commit HEAD && git show -s --show-signature HEAD >actual && diff --git a/t/t7511-status-index.sh b/t/t7511-status-index.sh index b5fdc048a5..4ffa45a7bf 100755 --- a/t/t7511-status-index.sh +++ b/t/t7511-status-index.sh @@ -2,6 +2,7 @@ test_description='git status with certain file name lengths' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh files="0 1 2 3 4 5 6 7 8 9 a b c d e f g h i j k l m n o p q r s t u v w x y z" diff --git a/t/t7512-status-help.sh b/t/t7512-status-help.sh index 7f2956d77a..2f16d5787e 100755 --- a/t/t7512-status-help.sh +++ b/t/t7512-status-help.sh @@ -659,6 +659,7 @@ On branch am_empty You are in the middle of an am session. The current patch is empty. (use "git am --skip" to skip this patch) + (use "git am --allow-empty" to record this patch as an empty commit) (use "git am --abort" to restore the original branch) nothing to commit (use -u to show untracked files) diff --git a/t/t7513-interpret-trailers.sh b/t/t7513-interpret-trailers.sh index 04885d0a5e..97f10905d2 100755 --- a/t/t7513-interpret-trailers.sh +++ b/t/t7513-interpret-trailers.sh @@ -156,7 +156,7 @@ test_expect_success 'with config option on the command line' ' Acked-by: Johan Reviewed-by: Peff EOF - { echo; echo "Acked-by: Johan"; } | + { echo && echo "Acked-by: Johan"; } | git -c "trailer.Acked-by.ifexists=addifdifferent" interpret-trailers \ --trailer "Reviewed-by: Peff" --trailer "Acked-by: Johan" >actual && test_cmp expected actual diff --git a/t/t7515-status-symlinks.sh b/t/t7515-status-symlinks.sh index 9f989be01b..e3d6bb67bf 100755 --- a/t/t7515-status-symlinks.sh +++ b/t/t7515-status-symlinks.sh @@ -2,6 +2,7 @@ test_description='git status and symlinks' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success 'setup' ' diff --git a/t/t7519-status-fsmonitor.sh b/t/t7519-status-fsmonitor.sh index f488d930df..a6308acf00 100755 --- a/t/t7519-status-fsmonitor.sh +++ b/t/t7519-status-fsmonitor.sh @@ -248,7 +248,7 @@ do git config core.preloadIndex $preload_val && if test $preload_val = true then - GIT_TEST_PRELOAD_INDEX=$preload_val; export GIT_TEST_PRELOAD_INDEX + GIT_TEST_PRELOAD_INDEX=$preload_val && export GIT_TEST_PRELOAD_INDEX else sane_unset GIT_TEST_PRELOAD_INDEX fi @@ -390,7 +390,7 @@ test_expect_success 'status succeeds after staging/unstaging' ' # during a call to 'git status'. Otherwise, we verify that we _do_ call it. check_sparse_index_behavior () { git -C full status --porcelain=v2 >expect && - GIT_TRACE2_EVENT="$(pwd)/trace2.txt" GIT_TRACE2_EVENT_NESTING=10 \ + GIT_TRACE2_EVENT="$(pwd)/trace2.txt" \ git -C sparse status --porcelain=v2 >actual && test_region $1 index ensure_full_index trace2.txt && test_region fsm_hook query trace2.txt && diff --git a/t/t7525-status-rename.sh b/t/t7525-status-rename.sh index a62736dce0..22bf5c7e5d 100755 --- a/t/t7525-status-rename.sh +++ b/t/t7525-status-rename.sh @@ -2,6 +2,7 @@ test_description='git status rename detection options' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success 'setup' ' diff --git a/t/t7526-commit-pathspec-file.sh b/t/t7526-commit-pathspec-file.sh index 5fbe47ebcd..dca62fc48e 100755 --- a/t/t7526-commit-pathspec-file.sh +++ b/t/t7526-commit-pathspec-file.sh @@ -2,6 +2,7 @@ test_description='commit --pathspec-from-file' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_tick diff --git a/t/t7528-signed-commit-ssh.sh b/t/t7528-signed-commit-ssh.sh index badf3ed320..f47e995179 100755 --- a/t/t7528-signed-commit-ssh.sh +++ b/t/t7528-signed-commit-ssh.sh @@ -73,7 +73,46 @@ test_expect_success GPGSSH 'create signed commits' ' git tag eleventh-signed $(cat oid) && echo 12 | git commit-tree --gpg-sign="${GPGSSH_KEY_UNTRUSTED}" HEAD^{tree} >oid && test_line_count = 1 oid && - git tag twelfth-signed-alt $(cat oid) + git tag twelfth-signed-alt $(cat oid) && + + echo 13>file && test_tick && git commit -a -m thirteenth -S"${GPGSSH_KEY_ECDSA}" && + git tag thirteenth-signed-ecdsa +' + +test_expect_success GPGSSH 'sign commits using literal public keys with ssh-agent' ' + test_when_finished "test_unconfig commit.gpgsign" && + test_config gpg.format ssh && + eval $(ssh-agent) && + test_when_finished "kill ${SSH_AGENT_PID}" && + ssh-add "${GPGSSH_KEY_PRIMARY}" && + echo 1 >file && git add file && + git commit -a -m rsa-inline -S"$(cat "${GPGSSH_KEY_PRIMARY}.pub")" && + echo 2 >file && + test_config user.signingkey "$(cat "${GPGSSH_KEY_PRIMARY}.pub")" && + git commit -a -m rsa-config -S && + ssh-add "${GPGSSH_KEY_ECDSA}" && + echo 3 >file && + git commit -a -m ecdsa-inline -S"key::$(cat "${GPGSSH_KEY_ECDSA}.pub")" && + echo 4 >file && + test_config user.signingkey "key::$(cat "${GPGSSH_KEY_ECDSA}.pub")" && + git commit -a -m ecdsa-config -S +' + +test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'create signed commits with keys having defined lifetimes' ' + test_when_finished "test_unconfig commit.gpgsign" && + test_config gpg.format ssh && + + echo expired >file && test_tick && git commit -a -m expired -S"${GPGSSH_KEY_EXPIRED}" && + git tag expired-signed && + + echo notyetvalid >file && test_tick && git commit -a -m notyetvalid -S"${GPGSSH_KEY_NOTYETVALID}" && + git tag notyetvalid-signed && + + echo timeboxedvalid >file && test_tick && git commit -a -m timeboxedvalid -S"${GPGSSH_KEY_TIMEBOXEDVALID}" && + git tag timeboxedvalid-signed && + + echo timeboxedinvalid >file && test_tick && git commit -a -m timeboxedinvalid -S"${GPGSSH_KEY_TIMEBOXEDINVALID}" && + git tag timeboxedinvalid-signed ' test_expect_success GPGSSH 'verify and show signatures' ' @@ -122,6 +161,31 @@ test_expect_success GPGSSH 'verify-commit exits failure on untrusted signature' grep "${GPGSSH_KEY_NOT_TRUSTED}" actual ' +test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'verify-commit exits failure on expired signature key' ' + test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && + test_must_fail git verify-commit expired-signed 2>actual && + ! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual +' + +test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'verify-commit exits failure on not yet valid signature key' ' + test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && + test_must_fail git verify-commit notyetvalid-signed 2>actual && + ! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual +' + +test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'verify-commit succeeds with commit date and key validity matching' ' + test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && + git verify-commit timeboxedvalid-signed 2>actual && + grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual && + ! grep "${GPGSSH_BAD_SIGNATURE}" actual +' + +test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'verify-commit exits failure with commit date outside of key validity' ' + test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && + test_must_fail git verify-commit timeboxedinvalid-signed 2>actual && + ! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual +' + test_expect_success GPGSSH 'verify-commit exits success with matching minTrustLevel' ' test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && test_config gpg.minTrustLevel fully && @@ -217,7 +281,7 @@ test_expect_success GPGSSH 'amending already signed commit' ' test_config gpg.format ssh && test_config user.signingkey "${GPGSSH_KEY_PRIMARY}" && test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && - git checkout fourth-signed^0 && + git checkout -f fourth-signed^0 && git commit --amend -S --no-edit && git verify-commit HEAD && git show -s --show-signature HEAD >actual && diff --git a/t/t7600-merge.sh b/t/t7600-merge.sh index c773e30b3f..f0f6fda150 100755 --- a/t/t7600-merge.sh +++ b/t/t7600-merge.sh @@ -967,7 +967,7 @@ test_expect_success 'set up mod-256 conflict scenario' ' # 256 near-identical stanzas... for i in $(test_seq 1 256); do for j in 1 2 3 4 5; do - echo $i-$j + echo $i-$j || return 1 done done >file && git add file && diff --git a/t/t7602-merge-octopus-many.sh b/t/t7602-merge-octopus-many.sh index a9c816b47f..ff085b086c 100755 --- a/t/t7602-merge-octopus-many.sh +++ b/t/t7602-merge-octopus-many.sh @@ -29,8 +29,8 @@ test_expect_success 'merge c1 with c2, c3, c4, ... c29' ' refs="" && while test $i -le 30 do - refs="$refs c$i" - i=$(expr $i + 1) + refs="$refs c$i" && + i=$(expr $i + 1) || return 1 done && git merge $refs && test "$(git rev-parse c1)" != "$(git rev-parse HEAD)" && diff --git a/t/t7603-merge-reduce-heads.sh b/t/t7603-merge-reduce-heads.sh index 27cd94ad6f..4887ca705b 100755 --- a/t/t7603-merge-reduce-heads.sh +++ b/t/t7603-merge-reduce-heads.sh @@ -95,7 +95,7 @@ test_expect_success 'setup' ' echo $i > $i.c && git add $i.c && git commit -m $i && - git tag $i + git tag $i || return 1 done && git reset --hard A && for i in F G H I @@ -103,7 +103,7 @@ test_expect_success 'setup' ' echo $i > $i.c && git add $i.c && git commit -m $i && - git tag $i + git tag $i || return 1 done ' diff --git a/t/t7700-repack.sh b/t/t7700-repack.sh index 0260ad6f0e..e489869dd9 100755 --- a/t/t7700-repack.sh +++ b/t/t7700-repack.sh @@ -5,6 +5,7 @@ test_description='git repack works correctly' . ./test-lib.sh . "${TEST_DIRECTORY}/lib-bitmap.sh" . "${TEST_DIRECTORY}/lib-midx.sh" +. "${TEST_DIRECTORY}/lib-terminal.sh" commit_and_pack () { test_commit "$@" 1>&2 && @@ -117,7 +118,7 @@ test_expect_success 'packed obs in alternate ODB kept pack are repacked' ' rm alt_objects/pack/$base_name.keep else touch alt_objects/pack/$base_name.keep - fi + fi || return 1 done && git repack -a -d && test_no_missing_in_packs @@ -372,4 +373,16 @@ test_expect_success '--write-midx with preferred bitmap tips' ' ) ' +test_expect_success '--write-midx -b packs non-kept objects' ' + GIT_TRACE2_EVENT="$(pwd)/trace.txt" \ + git repack --write-midx -a -b && + test_subcommand_inexact git pack-objects --honor-pack-keep <trace.txt +' + +test_expect_success TTY '--quiet disables progress' ' + test_terminal env GIT_PROGRESS_DELAY=0 \ + git -C midx repack -ad --quiet --write-midx 2>stderr && + test_must_be_empty stderr +' + test_done diff --git a/t/t7810-grep.sh b/t/t7810-grep.sh index 6b6423a07c..424c31c328 100755 --- a/t/t7810-grep.sh +++ b/t/t7810-grep.sh @@ -31,28 +31,28 @@ int main(int argc, const char **argv) EOF test_expect_success setup ' - { - echo foo mmap bar - echo foo_mmap bar - echo foo_mmap bar mmap - echo foo mmap bar_mmap - echo foo_mmap bar mmap baz - } >file && - { - echo Hello world - echo HeLLo world - echo Hello_world - echo HeLLo_world - } >hello_world && - { - echo "a+b*c" - echo "a+bc" - echo "abc" - } >ab && - { - echo d && - echo 0 - } >d0 && + cat >file <<-\EOF && + foo mmap bar + foo_mmap bar + foo_mmap bar mmap + foo mmap bar_mmap + foo_mmap bar mmap baz + EOF + cat >hello_world <<-\EOF && + Hello world + HeLLo world + Hello_world + HeLLo_world + EOF + cat >ab <<-\EOF && + a+b*c + a+bc + abc + EOF + cat >d0 <<-\EOF && + d + 0 + EOF echo vvv >v && echo ww w >w && echo x x xx x >x && @@ -63,13 +63,13 @@ test_expect_success setup ' echo vvv >t/v && mkdir t/a && echo vvv >t/a/v && - { - echo "line without leading space1" - echo " line with leading space1" - echo " line with leading space2" - echo " line with leading space3" - echo "line without leading space2" - } >space && + qz_to_tab_space >space <<-\EOF && + line without leading space1 + Zline with leading space1 + Zline with leading space2 + Zline with leading space3 + line without leading space2 + EOF cat >hello.ps1 <<-\EOF && # No-op. function dummy() {} @@ -106,129 +106,129 @@ do esac test_expect_success "grep -w $L" ' - { - echo ${HC}file:1:foo mmap bar - echo ${HC}file:3:foo_mmap bar mmap - echo ${HC}file:4:foo mmap bar_mmap - echo ${HC}file:5:foo_mmap bar mmap baz - } >expected && + cat >expected <<-EOF && + ${HC}file:1:foo mmap bar + ${HC}file:3:foo_mmap bar mmap + ${HC}file:4:foo mmap bar_mmap + ${HC}file:5:foo_mmap bar mmap baz + EOF git -c grep.linenumber=false grep -n -w -e mmap $H >actual && test_cmp expected actual ' test_expect_success "grep -w $L (with --column)" ' - { - echo ${HC}file:5:foo mmap bar - echo ${HC}file:14:foo_mmap bar mmap - echo ${HC}file:5:foo mmap bar_mmap - echo ${HC}file:14:foo_mmap bar mmap baz - } >expected && + cat >expected <<-EOF && + ${HC}file:5:foo mmap bar + ${HC}file:14:foo_mmap bar mmap + ${HC}file:5:foo mmap bar_mmap + ${HC}file:14:foo_mmap bar mmap baz + EOF git grep --column -w -e mmap $H >actual && test_cmp expected actual ' test_expect_success "grep -w $L (with --column, extended OR)" ' - { - echo ${HC}file:14:foo_mmap bar mmap - echo ${HC}file:19:foo_mmap bar mmap baz - } >expected && + cat >expected <<-EOF && + ${HC}file:14:foo_mmap bar mmap + ${HC}file:19:foo_mmap bar mmap baz + EOF git grep --column -w -e mmap$ --or -e baz $H >actual && test_cmp expected actual ' test_expect_success "grep -w $L (with --column, --invert-match)" ' - { - echo ${HC}file:1:foo mmap bar - echo ${HC}file:1:foo_mmap bar - echo ${HC}file:1:foo_mmap bar mmap - echo ${HC}file:1:foo mmap bar_mmap - } >expected && + cat >expected <<-EOF && + ${HC}file:1:foo mmap bar + ${HC}file:1:foo_mmap bar + ${HC}file:1:foo_mmap bar mmap + ${HC}file:1:foo mmap bar_mmap + EOF git grep --column --invert-match -w -e baz $H -- file >actual && test_cmp expected actual ' test_expect_success "grep $L (with --column, --invert-match, extended OR)" ' - { - echo ${HC}hello_world:6:HeLLo_world - } >expected && + cat >expected <<-EOF && + ${HC}hello_world:6:HeLLo_world + EOF git grep --column --invert-match -e ll --or --not -e _ $H -- hello_world \ >actual && test_cmp expected actual ' test_expect_success "grep $L (with --column, --invert-match, extended AND)" ' - { - echo ${HC}hello_world:3:Hello world - echo ${HC}hello_world:3:Hello_world - echo ${HC}hello_world:6:HeLLo_world - } >expected && + cat >expected <<-EOF && + ${HC}hello_world:3:Hello world + ${HC}hello_world:3:Hello_world + ${HC}hello_world:6:HeLLo_world + EOF git grep --column --invert-match --not -e _ --and --not -e ll $H -- hello_world \ >actual && test_cmp expected actual ' test_expect_success "grep $L (with --column, double-negation)" ' - { - echo ${HC}file:1:foo_mmap bar mmap baz - } >expected && + cat >expected <<-EOF && + ${HC}file:1:foo_mmap bar mmap baz + EOF git grep --column --not \( --not -e foo --or --not -e baz \) $H -- file \ >actual && test_cmp expected actual ' test_expect_success "grep -w $L (with --column, -C)" ' - { - echo ${HC}file:5:foo mmap bar - echo ${HC}file-foo_mmap bar - echo ${HC}file:14:foo_mmap bar mmap - echo ${HC}file:5:foo mmap bar_mmap - echo ${HC}file:14:foo_mmap bar mmap baz - } >expected && + cat >expected <<-EOF && + ${HC}file:5:foo mmap bar + ${HC}file-foo_mmap bar + ${HC}file:14:foo_mmap bar mmap + ${HC}file:5:foo mmap bar_mmap + ${HC}file:14:foo_mmap bar mmap baz + EOF git grep --column -w -C1 -e mmap $H >actual && test_cmp expected actual ' test_expect_success "grep -w $L (with --line-number, --column)" ' - { - echo ${HC}file:1:5:foo mmap bar - echo ${HC}file:3:14:foo_mmap bar mmap - echo ${HC}file:4:5:foo mmap bar_mmap - echo ${HC}file:5:14:foo_mmap bar mmap baz - } >expected && + cat >expected <<-EOF && + ${HC}file:1:5:foo mmap bar + ${HC}file:3:14:foo_mmap bar mmap + ${HC}file:4:5:foo mmap bar_mmap + ${HC}file:5:14:foo_mmap bar mmap baz + EOF git grep -n --column -w -e mmap $H >actual && test_cmp expected actual ' test_expect_success "grep -w $L (with non-extended patterns, --column)" ' - { - echo ${HC}file:5:foo mmap bar - echo ${HC}file:10:foo_mmap bar - echo ${HC}file:10:foo_mmap bar mmap - echo ${HC}file:5:foo mmap bar_mmap - echo ${HC}file:10:foo_mmap bar mmap baz - } >expected && + cat >expected <<-EOF && + ${HC}file:5:foo mmap bar + ${HC}file:10:foo_mmap bar + ${HC}file:10:foo_mmap bar mmap + ${HC}file:5:foo mmap bar_mmap + ${HC}file:10:foo_mmap bar mmap baz + EOF git grep --column -w -e bar -e mmap $H >actual && test_cmp expected actual ' test_expect_success "grep -w $L" ' - { - echo ${HC}file:1:foo mmap bar - echo ${HC}file:3:foo_mmap bar mmap - echo ${HC}file:4:foo mmap bar_mmap - echo ${HC}file:5:foo_mmap bar mmap baz - } >expected && + cat >expected <<-EOF && + ${HC}file:1:foo mmap bar + ${HC}file:3:foo_mmap bar mmap + ${HC}file:4:foo mmap bar_mmap + ${HC}file:5:foo_mmap bar mmap baz + EOF git -c grep.linenumber=true grep -w -e mmap $H >actual && test_cmp expected actual ' test_expect_success "grep -w $L" ' - { - echo ${HC}file:foo mmap bar - echo ${HC}file:foo_mmap bar mmap - echo ${HC}file:foo mmap bar_mmap - echo ${HC}file:foo_mmap bar mmap baz - } >expected && + cat >expected <<-EOF && + ${HC}file:foo mmap bar + ${HC}file:foo_mmap bar mmap + ${HC}file:foo mmap bar_mmap + ${HC}file:foo_mmap bar mmap baz + EOF git -c grep.linenumber=true grep --no-line-number -w -e mmap $H >actual && test_cmp expected actual ' @@ -239,17 +239,17 @@ do ' test_expect_success "grep -w $L (x)" ' - { - echo ${HC}x:1:x x xx x - } >expected && + cat >expected <<-EOF && + ${HC}x:1:x x xx x + EOF git grep -n -w -e "x xx* x" $H >actual && test_cmp expected actual ' test_expect_success "grep -w $L (y-1)" ' - { - echo ${HC}y:1:y yy - } >expected && + cat >expected <<-EOF && + ${HC}y:1:y yy + EOF git grep -n -w -e "^y" $H >actual && test_cmp expected actual ' @@ -277,16 +277,16 @@ do ' test_expect_success "grep $L (with --column, --only-matching)" ' - { - echo ${HC}file:1:5:mmap - echo ${HC}file:2:5:mmap - echo ${HC}file:3:5:mmap - echo ${HC}file:3:13:mmap - echo ${HC}file:4:5:mmap - echo ${HC}file:4:13:mmap - echo ${HC}file:5:5:mmap - echo ${HC}file:5:13:mmap - } >expected && + cat >expected <<-EOF && + ${HC}file:1:5:mmap + ${HC}file:2:5:mmap + ${HC}file:3:5:mmap + ${HC}file:3:13:mmap + ${HC}file:4:5:mmap + ${HC}file:4:13:mmap + ${HC}file:5:5:mmap + ${HC}file:5:13:mmap + EOF git grep --column -n -o -e mmap $H >actual && test_cmp expected actual ' @@ -320,11 +320,11 @@ do ' test_expect_success "grep --max-depth -1 $L" ' - { - echo ${HC}t/a/v:1:vvv - echo ${HC}t/v:1:vvv - echo ${HC}v:1:vvv - } >expected && + cat >expected <<-EOF && + ${HC}t/a/v:1:vvv + ${HC}t/v:1:vvv + ${HC}v:1:vvv + EOF git grep --max-depth -1 -n -e vvv $H >actual && test_cmp expected actual && git grep --recursive -n -e vvv $H >actual && @@ -332,9 +332,9 @@ do ' test_expect_success "grep --max-depth 0 $L" ' - { - echo ${HC}v:1:vvv - } >expected && + cat >expected <<-EOF && + ${HC}v:1:vvv + EOF git grep --max-depth 0 -n -e vvv $H >actual && test_cmp expected actual && git grep --no-recursive -n -e vvv $H >actual && @@ -342,11 +342,11 @@ do ' test_expect_success "grep --max-depth 0 -- '*' $L" ' - { - echo ${HC}t/a/v:1:vvv - echo ${HC}t/v:1:vvv - echo ${HC}v:1:vvv - } >expected && + cat >expected <<-EOF && + ${HC}t/a/v:1:vvv + ${HC}t/v:1:vvv + ${HC}v:1:vvv + EOF git grep --max-depth 0 -n -e vvv $H -- "*" >actual && test_cmp expected actual && git grep --no-recursive -n -e vvv $H -- "*" >actual && @@ -354,18 +354,18 @@ do ' test_expect_success "grep --max-depth 1 $L" ' - { - echo ${HC}t/v:1:vvv - echo ${HC}v:1:vvv - } >expected && + cat >expected <<-EOF && + ${HC}t/v:1:vvv + ${HC}v:1:vvv + EOF git grep --max-depth 1 -n -e vvv $H >actual && test_cmp expected actual ' test_expect_success "grep --max-depth 0 -- t $L" ' - { - echo ${HC}t/v:1:vvv - } >expected && + cat >expected <<-EOF && + ${HC}t/v:1:vvv + EOF git grep --max-depth 0 -n -e vvv $H -- t >actual && test_cmp expected actual && git grep --no-recursive -n -e vvv $H -- t >actual && @@ -373,10 +373,10 @@ do ' test_expect_success "grep --max-depth 0 -- . t $L" ' - { - echo ${HC}t/v:1:vvv - echo ${HC}v:1:vvv - } >expected && + cat >expected <<-EOF && + ${HC}t/v:1:vvv + ${HC}v:1:vvv + EOF git grep --max-depth 0 -n -e vvv $H -- . t >actual && test_cmp expected actual && git grep --no-recursive -n -e vvv $H -- . t >actual && @@ -384,10 +384,10 @@ do ' test_expect_success "grep --max-depth 0 -- t . $L" ' - { - echo ${HC}t/v:1:vvv - echo ${HC}v:1:vvv - } >expected && + cat >expected <<-EOF && + ${HC}t/v:1:vvv + ${HC}v:1:vvv + EOF git grep --max-depth 0 -n -e vvv $H -- t . >actual && test_cmp expected actual && git grep --no-recursive -n -e vvv $H -- t . >actual && @@ -1314,10 +1314,10 @@ test_expect_success PCRE 'grep -P pattern with grep.extendedRegexp=true' ' ' test_expect_success PCRE 'grep -P -v pattern' ' - { - echo "ab:a+b*c" - echo "ab:a+bc" - } >expected && + cat >expected <<-\EOF && + ab:a+b*c + ab:a+bc + EOF git grep -P -v "abc" ab >actual && test_cmp expected actual ' @@ -1331,10 +1331,10 @@ test_expect_success PCRE 'grep -P -i pattern' ' ' test_expect_success PCRE 'grep -P -w pattern' ' - { - echo "hello_world:Hello world" - echo "hello_world:HeLLo world" - } >expected && + cat >expected <<-\EOF && + hello_world:Hello world + hello_world:HeLLo world + EOF git grep -P -w "He((?i)ll)o" hello_world >actual && test_cmp expected actual ' @@ -1469,10 +1469,10 @@ test_expect_success 'grep -F pattern with grep.patternType=basic' ' ' test_expect_success 'grep -G pattern with grep.patternType=fixed' ' - { - echo "ab:a+b*c" - echo "ab:a+bc" - } >expected && + cat >expected <<-\EOF && + ab:a+b*c + ab:a+bc + EOF git \ -c grep.patterntype=fixed \ grep -G "a+b" ab >actual && @@ -1480,11 +1480,11 @@ test_expect_success 'grep -G pattern with grep.patternType=fixed' ' ' test_expect_success 'grep -E pattern with grep.patternType=fixed' ' - { - echo "ab:a+b*c" - echo "ab:a+bc" - echo "ab:abc" - } >expected && + cat >expected <<-\EOF && + ab:a+b*c + ab:a+bc + ab:abc + EOF git \ -c grep.patterntype=fixed \ grep -E "a+" ab >actual && diff --git a/t/t7812-grep-icase-non-ascii.sh b/t/t7812-grep-icase-non-ascii.sh index e5d1e4ea68..ca3f24f807 100755 --- a/t/t7812-grep-icase-non-ascii.sh +++ b/t/t7812-grep-icase-non-ascii.sh @@ -123,4 +123,10 @@ test_expect_success GETTEXT_LOCALE,LIBPCRE2,PCRE2_MATCH_INVALID_UTF 'PCRE v2: gr test_cmp invalid-0xe5 actual ' +test_expect_success GETTEXT_LOCALE,LIBPCRE2 'PCRE v2: grep non-literal ASCII from UTF-8' ' + git grep --perl-regexp -h -o -e ll. file >actual && + echo "lló" >expected && + test_cmp expected actual +' + test_done diff --git a/t/t7815-grep-binary.sh b/t/t7815-grep-binary.sh index 90ebb64f46..ac871287c0 100755 --- a/t/t7815-grep-binary.sh +++ b/t/t7815-grep-binary.sh @@ -2,6 +2,7 @@ test_description='git grep in binary files' +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success 'setup' " diff --git a/t/t8002-blame.sh b/t/t8002-blame.sh index 5bb302b1ba..ee4fdd8f18 100755 --- a/t/t8002-blame.sh +++ b/t/t8002-blame.sh @@ -97,7 +97,7 @@ test_expect_success 'set up abbrev tests' ' test_commit abbrev && sha1=$(git rev-parse --verify HEAD) && check_abbrev () { - expect=$1; shift + expect=$1 && shift && echo $sha1 | cut -c 1-$expect >expect && git blame "$@" abbrev.t >actual && perl -lne "/[0-9a-f]+/ and print \$&" <actual >actual.sha && diff --git a/t/t8003-blame-corner-cases.sh b/t/t8003-blame-corner-cases.sh index da80f815ce..d751d48b7d 100755 --- a/t/t8003-blame-corner-cases.sh +++ b/t/t8003-blame-corner-cases.sh @@ -13,14 +13,8 @@ test_expect_success setup ' echo B B B B B >two && echo C C C C C >tres && echo ABC >mouse && - for i in 1 2 3 4 5 6 7 8 9 - do - echo $i - done >nine_lines && - for i in 1 2 3 4 5 6 7 8 9 a - do - echo $i - done >ten_lines && + test_write_lines 1 2 3 4 5 6 7 8 9 >nine_lines && + test_write_lines 1 2 3 4 5 6 7 8 9 a >ten_lines && git add one two tres mouse nine_lines ten_lines && test_tick && GIT_AUTHOR_NAME=Initial git commit -m Initial && diff --git a/t/t8014-blame-ignore-fuzzy.sh b/t/t8014-blame-ignore-fuzzy.sh index e68e6115a6..0bd0341301 100755 --- a/t/t8014-blame-ignore-fuzzy.sh +++ b/t/t8014-blame-ignore-fuzzy.sh @@ -310,7 +310,7 @@ test_expect_success setup ' echo "$line" >>"$i" && git add "$i" && test_tick && - GIT_AUTHOR_NAME="$line_count" git commit -m "$line_count" + GIT_AUTHOR_NAME="$line_count" git commit -m "$line_count" || return 1 done <"a$i" done && @@ -318,7 +318,7 @@ test_expect_success setup ' do # Overwrite the files with the final content. cp b$i $i && - git add $i + git add $i || return 1 done && test_tick && diff --git a/t/t9102-git-svn-deep-rmdir.sh b/t/t9102-git-svn-deep-rmdir.sh index 66cd51102c..7b2049caa0 100755 --- a/t/t9102-git-svn-deep-rmdir.sh +++ b/t/t9102-git-svn-deep-rmdir.sh @@ -1,5 +1,7 @@ #!/bin/sh test_description='git svn rmdir' + +TEST_PASSES_SANITIZE_LEAK=true . ./lib-git-svn.sh test_expect_success 'initialize repo' ' diff --git a/t/t9104-git-svn-follow-parent.sh b/t/t9104-git-svn-follow-parent.sh index 67eed2fefc..c7d8e0bf00 100755 --- a/t/t9104-git-svn-follow-parent.sh +++ b/t/t9104-git-svn-follow-parent.sh @@ -117,7 +117,7 @@ test_expect_success 'follow-parent avoids deleting relevant info' ' mkdir -p import/trunk/subversion/bindings/swig/perl/t && for i in a b c ; do \ echo $i > import/trunk/subversion/bindings/swig/perl/$i.pm && - echo _$i > import/trunk/subversion/bindings/swig/perl/t/$i.t; \ + echo _$i > import/trunk/subversion/bindings/swig/perl/t/$i.t || return 1 done && echo "bad delete test" > \ import/trunk/subversion/bindings/swig/perl/t/larger-parent && @@ -134,7 +134,7 @@ test_expect_success 'follow-parent avoids deleting relevant info' ' svn mv t native/t && for i in a b c do - svn mv $i.pm native/$i.pm + svn mv $i.pm native/$i.pm || return 1 done && echo z >>native/t/c.t && poke native/t/c.t && diff --git a/t/t9107-git-svn-migrate.sh b/t/t9107-git-svn-migrate.sh index ceaa5bad10..aa908bbc2f 100755 --- a/t/t9107-git-svn-migrate.sh +++ b/t/t9107-git-svn-migrate.sh @@ -98,10 +98,10 @@ test_expect_success 'migrate --minimize on old inited layout' ' rm -rf "$GIT_DIR"/svn && for i in $(cat fetch.out) do - path=$(expr $i : "\([^:]*\):.*$") - ref=$(expr $i : "[^:]*:\(refs/remotes/.*\)$") - if test -z "$ref"; then continue; fi - if test -n "$path"; then path="/$path"; fi + path=${i%%:*} && + ref=${i#*:} && + if test "$ref" = "${ref#refs/remotes/}"; then continue; fi && + if test -n "$path"; then path="/$path"; fi && mkdir -p "$GIT_DIR"/svn/$ref/info/ && echo "$svnrepo"$path >"$GIT_DIR"/svn/$ref/info/url || return 1 diff --git a/t/t9123-git-svn-rebuild-with-rewriteroot.sh b/t/t9123-git-svn-rebuild-with-rewriteroot.sh index ead404589e..3320b1f39c 100755 --- a/t/t9123-git-svn-rebuild-with-rewriteroot.sh +++ b/t/t9123-git-svn-rebuild-with-rewriteroot.sh @@ -5,6 +5,7 @@ test_description='git svn respects rewriteRoot during rebuild' +TEST_PASSES_SANITIZE_LEAK=true . ./lib-git-svn.sh mkdir import diff --git a/t/t9128-git-svn-cmd-branch.sh b/t/t9128-git-svn-cmd-branch.sh index 4e95f791db..9871f5abc9 100755 --- a/t/t9128-git-svn-cmd-branch.sh +++ b/t/t9128-git-svn-cmd-branch.sh @@ -4,6 +4,8 @@ # test_description='git svn partial-rebuild tests' + +TEST_PASSES_SANITIZE_LEAK=true . ./lib-git-svn.sh test_expect_success 'initialize svnrepo' ' diff --git a/t/t9130-git-svn-authors-file.sh b/t/t9130-git-svn-authors-file.sh index cb764bcadc..90325db909 100755 --- a/t/t9130-git-svn-authors-file.sh +++ b/t/t9130-git-svn-authors-file.sh @@ -15,7 +15,7 @@ EOF test_expect_success 'setup svnrepo' ' for i in aa bb cc dd do - svn_cmd mkdir -m $i --username $i "$svnrepo"/$i + svn_cmd mkdir -m $i --username $i "$svnrepo"/$i || return 1 done ' @@ -59,8 +59,8 @@ test_expect_success 'authors-file against globs' ' git svn clone --authors-file=svn-authors -s "$svnrepo"/aa aa-work && for i in bb ee cc do - branch="aa/branches/$i" - svn_cmd mkdir -m "$branch" --username $i "$svnrepo/$branch" + branch="aa/branches/$i" && + svn_cmd mkdir -m "$branch" --username $i "$svnrepo/$branch" || return 1 done ' diff --git a/t/t9134-git-svn-ignore-paths.sh b/t/t9134-git-svn-ignore-paths.sh index fff49c4100..4a77eb9f60 100755 --- a/t/t9134-git-svn-ignore-paths.sh +++ b/t/t9134-git-svn-ignore-paths.sh @@ -27,7 +27,7 @@ test_expect_success 'setup test repository' ' test_expect_success 'clone an SVN repository with ignored www directory' ' git svn clone --ignore-paths="^www" "$svnrepo" g && echo test_qqq > expect && - for i in g/*/*.txt; do cat $i >> expect2; done && + for i in g/*/*.txt; do cat $i >> expect2 || return 1; done && test_cmp expect expect2 ' @@ -36,7 +36,7 @@ test_expect_success 'init+fetch an SVN repository with ignored www directory' ' ( cd c && git svn fetch --ignore-paths="^www" ) && rm expect2 && echo test_qqq > expect && - for i in c/*/*.txt; do cat $i >> expect2; done && + for i in c/*/*.txt; do cat $i >> expect2 || return 1; done && test_cmp expect expect2 ' @@ -62,7 +62,7 @@ test_expect_success 'update git svn-cloned repo (config ignore)' ' cd g && git svn rebase && printf "test_qqq\nb\n" > expect && - for i in */*.txt; do cat $i >> expect2; done && + for i in */*.txt; do cat $i >> expect2 || exit 1; done && test_cmp expect2 expect && rm expect expect2 ) @@ -73,7 +73,7 @@ test_expect_success 'update git svn-cloned repo (option ignore)' ' cd c && git svn rebase --ignore-paths="^www" && printf "test_qqq\nb\n" > expect && - for i in */*.txt; do cat $i >> expect2; done && + for i in */*.txt; do cat $i >> expect2 || exit 1; done && test_cmp expect2 expect && rm expect expect2 ) @@ -94,7 +94,7 @@ test_expect_success 'update git svn-cloned repo (config ignore)' ' cd g && git svn rebase && printf "test_qqq\nb\n" > expect && - for i in */*.txt; do cat $i >> expect2; done && + for i in */*.txt; do cat $i >> expect2 || exit 1; done && test_cmp expect2 expect && rm expect expect2 ) @@ -105,7 +105,7 @@ test_expect_success 'update git svn-cloned repo (option ignore)' ' cd c && git svn rebase --ignore-paths="^www" && printf "test_qqq\nb\n" > expect && - for i in */*.txt; do cat $i >> expect2; done && + for i in */*.txt; do cat $i >> expect2 || exit 1; done && test_cmp expect2 expect && rm expect expect2 ) @@ -127,7 +127,7 @@ test_expect_success 'update git svn-cloned repo again (config ignore)' ' cd g && git svn rebase && printf "test_qqq\nb\nygg\n" > expect && - for i in */*.txt; do cat $i >> expect2; done && + for i in */*.txt; do cat $i >> expect2 || exit 1; done && test_cmp expect2 expect && rm expect expect2 ) @@ -138,7 +138,7 @@ test_expect_success 'update git svn-cloned repo again (option ignore)' ' cd c && git svn rebase --ignore-paths="^www" && printf "test_qqq\nb\nygg\n" > expect && - for i in */*.txt; do cat $i >> expect2; done && + for i in */*.txt; do cat $i >> expect2 || exit 1; done && test_cmp expect2 expect && rm expect expect2 ) diff --git a/t/t9138-git-svn-authors-prog.sh b/t/t9138-git-svn-authors-prog.sh index 027b416720..784ec7fc2d 100755 --- a/t/t9138-git-svn-authors-prog.sh +++ b/t/t9138-git-svn-authors-prog.sh @@ -27,7 +27,7 @@ test_expect_success 'svn-authors setup' ' test_expect_success 'setup svnrepo' ' for i in aa bb cc-sub dd-sub ee-foo ff do - svn mkdir -m $i --username $i "$svnrepo"/$i + svn mkdir -m $i --username $i "$svnrepo"/$i || return 1 done ' diff --git a/t/t9146-git-svn-empty-dirs.sh b/t/t9146-git-svn-empty-dirs.sh index 5f91c0d68b..80cb55fee7 100755 --- a/t/t9146-git-svn-empty-dirs.sh +++ b/t/t9146-git-svn-empty-dirs.sh @@ -8,7 +8,7 @@ test_description='git svn creates empty directories' test_expect_success 'initialize repo' ' for i in a b c d d/e d/e/f "weird file name" do - svn_cmd mkdir -m "mkdir $i" "$svnrepo"/"$i" + svn_cmd mkdir -m "mkdir $i" "$svnrepo"/"$i" || return 1 done ' @@ -102,7 +102,7 @@ test_expect_success 'git svn mkdirs -r works' ' test_expect_success 'initialize trunk' ' for i in trunk trunk/a trunk/"weird file name" do - svn_cmd mkdir -m "mkdir $i" "$svnrepo"/"$i" + svn_cmd mkdir -m "mkdir $i" "$svnrepo"/"$i" || return 1 done ' diff --git a/t/t9147-git-svn-include-paths.sh b/t/t9147-git-svn-include-paths.sh index d292bf9f55..257fc8f2f8 100755 --- a/t/t9147-git-svn-include-paths.sh +++ b/t/t9147-git-svn-include-paths.sh @@ -28,7 +28,7 @@ test_expect_success 'setup test repository' ' test_expect_success 'clone an SVN repository with filter to include qqq directory' ' git svn clone --include-paths="qqq" "$svnrepo" g && echo test_qqq > expect && - for i in g/*/*.txt; do cat $i >> expect2; done && + for i in g/*/*.txt; do cat $i >> expect2 || return 1; done && test_cmp expect expect2 ' @@ -38,7 +38,7 @@ test_expect_success 'init+fetch an SVN repository with included qqq directory' ' ( cd c && git svn fetch --include-paths="qqq" ) && rm expect2 && echo test_qqq > expect && - for i in c/*/*.txt; do cat $i >> expect2; done && + for i in c/*/*.txt; do cat $i >> expect2 || return 1; done && test_cmp expect expect2 ' @@ -64,7 +64,7 @@ test_expect_success 'update git svn-cloned repo (config include)' ' cd g && git svn rebase && printf "test_qqq\nb\n" > expect && - for i in */*.txt; do cat $i >> expect2; done && + for i in */*.txt; do cat $i >> expect2 || exit 1; done && test_cmp expect2 expect && rm expect expect2 ) @@ -75,7 +75,7 @@ test_expect_success 'update git svn-cloned repo (option include)' ' cd c && git svn rebase --include-paths="qqq" && printf "test_qqq\nb\n" > expect && - for i in */*.txt; do cat $i >> expect2; done && + for i in */*.txt; do cat $i >> expect2 || exit 1; done && test_cmp expect2 expect && rm expect expect2 ) @@ -96,7 +96,7 @@ test_expect_success 'update git svn-cloned repo (config include)' ' cd g && git svn rebase && printf "test_qqq\nb\n" > expect && - for i in */*.txt; do cat $i >> expect2; done && + for i in */*.txt; do cat $i >> expect2 || exit 1; done && test_cmp expect2 expect && rm expect expect2 ) @@ -107,7 +107,7 @@ test_expect_success 'update git svn-cloned repo (option include)' ' cd c && git svn rebase --include-paths="qqq" && printf "test_qqq\nb\n" > expect && - for i in */*.txt; do cat $i >> expect2; done && + for i in */*.txt; do cat $i >> expect2 || exit 1; done && test_cmp expect2 expect && rm expect expect2 ) @@ -129,7 +129,7 @@ test_expect_success 'update git svn-cloned repo again (config include)' ' cd g && git svn rebase && printf "test_qqq\nb\nygg\n" > expect && - for i in */*.txt; do cat $i >> expect2; done && + for i in */*.txt; do cat $i >> expect2 || exit 1; done && test_cmp expect2 expect && rm expect expect2 ) @@ -140,7 +140,7 @@ test_expect_success 'update git svn-cloned repo again (option include)' ' cd c && git svn rebase --include-paths="qqq" && printf "test_qqq\nb\nygg\n" > expect && - for i in */*.txt; do cat $i >> expect2; done && + for i in */*.txt; do cat $i >> expect2 || exit 1; done && test_cmp expect2 expect && rm expect expect2 ) diff --git a/t/t9151-svn-mergeinfo.sh b/t/t9151-svn-mergeinfo.sh index 1fbe84feb1..c93a5beab2 100755 --- a/t/t9151-svn-mergeinfo.sh +++ b/t/t9151-svn-mergeinfo.sh @@ -5,9 +5,6 @@ test_description='git-svn svn mergeinfo properties' -GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main -export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME - . ./lib-git-svn.sh test_expect_success 'load svn dump' " diff --git a/t/t9152-svn-empty-dirs-after-gc.sh b/t/t9152-svn-empty-dirs-after-gc.sh index 89f285d082..a597c42f77 100755 --- a/t/t9152-svn-empty-dirs-after-gc.sh +++ b/t/t9152-svn-empty-dirs-after-gc.sh @@ -8,7 +8,7 @@ test_description='git svn creates empty directories, calls git gc, makes sure th test_expect_success 'initialize repo' ' for i in a b c d d/e d/e/f "weird file name" do - svn_cmd mkdir -m "mkdir $i" "$svnrepo"/"$i" + svn_cmd mkdir -m "mkdir $i" "$svnrepo"/"$i" || return 1 done ' diff --git a/t/t9167-git-svn-cmd-branch-subproject.sh b/t/t9167-git-svn-cmd-branch-subproject.sh index ba35fc06fc..d9fd111c10 100755 --- a/t/t9167-git-svn-cmd-branch-subproject.sh +++ b/t/t9167-git-svn-cmd-branch-subproject.sh @@ -4,6 +4,8 @@ # test_description='git svn branch for subproject clones' + +TEST_PASSES_SANITIZE_LEAK=true . ./lib-git-svn.sh test_expect_success 'initialize svnrepo' ' diff --git a/t/t9302-fast-import-unpack-limit.sh b/t/t9302-fast-import-unpack-limit.sh index f519e4f1bf..d8b1f9442e 100755 --- a/t/t9302-fast-import-unpack-limit.sh +++ b/t/t9302-fast-import-unpack-limit.sh @@ -1,5 +1,7 @@ #!/bin/sh test_description='test git fast-import unpack limit' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh test_expect_success 'create loose objects on import' ' diff --git a/t/t9303-fast-import-compression.sh b/t/t9303-fast-import-compression.sh index 57d916524e..4f5bf40587 100755 --- a/t/t9303-fast-import-compression.sh +++ b/t/t9303-fast-import-compression.sh @@ -1,6 +1,8 @@ #!/bin/sh test_description='compression setting of fast-import utility' + +TEST_PASSES_SANITIZE_LEAK=true . ./test-lib.sh import_large () { diff --git a/t/t9304-fast-import-marks.sh b/t/t9304-fast-import-marks.sh index d4359dba21..bed01c99ea 100755 --- a/t/t9304-fast-import-marks.sh +++ b/t/t9304-fast-import-marks.sh @@ -16,7 +16,7 @@ test_expect_success 'setup large marks file' ' blob=$(git rev-parse HEAD:one.t) && for i in $(test_seq 1024 16384) do - echo ":$i $blob" + echo ":$i $blob" || return 1 done >>marks ' diff --git a/t/t9350-fast-export.sh b/t/t9350-fast-export.sh index 409b48e244..7b7a18dd2c 100755 --- a/t/t9350-fast-export.sh +++ b/t/t9350-fast-export.sh @@ -750,4 +750,36 @@ test_expect_success 'merge commit gets exported with --import-marks' ' ) ' + +test_expect_success 'fast-export --first-parent outputs all revisions output by revision walk' ' + git init first-parent && + ( + cd first-parent && + test_commit A && + git checkout -b topic1 && + test_commit B && + git checkout main && + git merge --no-ff topic1 && + + git checkout -b topic2 && + test_commit C && + git checkout main && + git merge --no-ff topic2 && + + test_commit D && + + git fast-export main -- --first-parent >first-parent-export && + git fast-export main -- --first-parent --reverse >first-parent-reverse-export && + test_cmp first-parent-export first-parent-reverse-export && + + git init import && + git -C import fast-import <first-parent-export && + + git log --format="%ad %s" --first-parent main >expected && + git -C import log --format="%ad %s" --all >actual && + test_cmp expected actual && + test_line_count = 4 actual + ) +' + test_done diff --git a/t/t9400-git-cvsserver-server.sh b/t/t9400-git-cvsserver-server.sh index 17f988edd2..210ddf09e3 100755 --- a/t/t9400-git-cvsserver-server.sh +++ b/t/t9400-git-cvsserver-server.sh @@ -338,7 +338,7 @@ test_expect_success 'cvs update (subdirectories)' \ '(for dir in A A/B A/B/C A/D E; do mkdir $dir && echo "test file in $dir" >"$dir/file_in_$(echo $dir|sed -e "s#/# #g")" && - git add $dir + git add $dir || exit 1 done) && git commit -q -m "deep sub directory structure" && git push gitcvs.git >/dev/null && @@ -350,10 +350,9 @@ test_expect_success 'cvs update (subdirectories)' \ test_cmp "$dir/$filename" "../$dir/$filename"; then : else - echo >failure + exit 1 fi - done) && - test ! -f failure' + done)' cd "$WORKDIR" test_expect_success 'cvs update (delete file)' \ @@ -382,7 +381,7 @@ test_expect_success 'cvs update (merge)' \ for i in 1 2 3 4 5 6 7 do echo Line $i >>merge && - echo Line $i >>expected + echo Line $i >>expected || return 1 done && echo Line 8 >>expected && git add merge && @@ -592,7 +591,7 @@ test_expect_success 'cvs annotate' ' cd cvswork && GIT_CONFIG="$git_config" cvs annotate merge >../out && sed -e "s/ .*//" ../out >../actual && - for i in 3 1 1 1 1 1 1 1 2 4; do echo 1.$i; done >../expect && + printf "1.%d\n" 3 1 1 1 1 1 1 1 2 4 >../expect && test_cmp ../expect ../actual ' diff --git a/t/t9603-cvsimport-patchsets.sh b/t/t9603-cvsimport-patchsets.sh index 0e9daa5768..19f38f78f2 100755 --- a/t/t9603-cvsimport-patchsets.sh +++ b/t/t9603-cvsimport-patchsets.sh @@ -12,9 +12,6 @@ # bug. test_description='git cvsimport testing for correct patchset estimation' -GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main -export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME - . ./lib-cvs.sh setup_cvs_test_repository t9603 diff --git a/t/t9800-git-p4-basic.sh b/t/t9800-git-p4-basic.sh index 81bc8e8da1..806005a793 100755 --- a/t/t9800-git-p4-basic.sh +++ b/t/t9800-git-p4-basic.sh @@ -171,7 +171,7 @@ test_expect_success 'clone using non-numeric revision ranges' ' cd "$git" && git ls-files >lines && test_line_count = 8 lines - ) + ) || return 1 done ' diff --git a/t/t9810-git-p4-rcs.sh b/t/t9810-git-p4-rcs.sh index e3836888ec..5fe83315ec 100755 --- a/t/t9810-git-p4-rcs.sh +++ b/t/t9810-git-p4-rcs.sh @@ -4,6 +4,8 @@ test_description='git p4 rcs keywords' . ./lib-git-p4.sh +CP1252="\223\224" + test_expect_success 'start p4d' ' start_p4d ' @@ -32,6 +34,9 @@ test_expect_success 'init depot' ' p4 submit -d "filek" && p4 add -t text+ko fileko && p4 submit -d "fileko" && + printf "$CP1252" >fileko_cp1252 && + p4 add -t text+ko fileko_cp1252 && + p4 submit -d "fileko_cp1252" && p4 add -t text file_text && p4 submit -d "file_text" ) @@ -359,4 +364,14 @@ test_expect_failure 'Add keywords in git which do not match the default p4 value ) ' +test_expect_success 'check cp1252 smart quote are preserved through RCS keyword processing' ' + test_when_finished cleanup_git && + git p4 clone --dest="$git" //depot && + ( + cd "$git" && + printf "$CP1252" >expect && + test_cmp_bin expect fileko_cp1252 + ) +' + test_done diff --git a/t/t9818-git-p4-block.sh b/t/t9818-git-p4-block.sh index 0db7ab9918..de591d875c 100755 --- a/t/t9818-git-p4-block.sh +++ b/t/t9818-git-p4-block.sh @@ -92,11 +92,11 @@ test_expect_success 'Add some more files' ' for i in $(test_seq 0 10) do p4_add_file "included/x$i" && - p4_add_file "excluded/x$i" + p4_add_file "excluded/x$i" || return 1 done && for i in $(test_seq 0 10) do - p4_add_file "excluded/y$i" + p4_add_file "excluded/y$i" || return 1 done ' @@ -123,7 +123,7 @@ test_expect_success 'Create a repo with multiple depot paths' ' do for i in $(test_seq 1 10) do - p4_add_file "$p/file$p$i" + p4_add_file "$p/file$p$i" || return 1 done done ' diff --git a/t/t9902-completion.sh b/t/t9902-completion.sh index 5decc3b269..98c6280632 100755 --- a/t/t9902-completion.sh +++ b/t/t9902-completion.sh @@ -5,9 +5,6 @@ test_description='test bash completion' -GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=master -export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME - . ./lib-bash.sh complete () @@ -879,7 +876,7 @@ test_expect_success '__git_refs - unique remote branches for git checkout DWIMer refs/remotes/remote/branch-in-remote do git update-ref $remote_ref main && - test_when_finished "git update-ref -d $remote_ref" + test_when_finished "git update-ref -d $remote_ref" || return 1 done && ( cur= && @@ -1052,7 +1049,7 @@ test_expect_success '__git_refs - only matching refs - checkout DWIMery' ' refs/remotes/remote/branch-in-remote do git update-ref $remote_ref main && - test_when_finished "git update-ref -d $remote_ref" + test_when_finished "git update-ref -d $remote_ref" || return 1 done && ( cur=mat && @@ -2148,6 +2145,9 @@ test_expect_success PERL 'send-email' ' --cover-from-description=Z --cover-letter Z EOF + test_completion "git send-email --val" <<-\EOF && + --validate Z + EOF test_completion "git send-email ma" "main " ' diff --git a/t/test-lib-functions.sh b/t/test-lib-functions.sh index eef2262a36..c3d38aaccb 100644 --- a/t/test-lib-functions.sh +++ b/t/test-lib-functions.sh @@ -680,6 +680,17 @@ test_have_prereq () { # Keep a list of missing prerequisites; restore # the negative marker if necessary. prerequisite=${negative_prereq:+!}$prerequisite + + # Abort if this prereq was marked as required + if test -n "$GIT_TEST_REQUIRE_PREREQ" + then + case " $GIT_TEST_REQUIRE_PREREQ " in + *" $prerequisite "*) + BAIL_OUT "required prereq $prerequisite failed" + ;; + esac + fi + if test -z "$missing_prereq" then missing_prereq=$prerequisite @@ -1749,6 +1760,40 @@ test_subcommand () { } # Check that the given command was invoked as part of the +# trace2-format trace on stdin, but without an exact set of +# arguments. +# +# test_subcommand [!] <command> <args>... < <trace> +# +# For example, to look for an invocation of "git pack-objects" +# with the "--honor-pack-keep" argument, use +# +# GIT_TRACE2_EVENT=event.log git repack ... && +# test_subcommand git pack-objects --honor-pack-keep <event.log +# +# If the first parameter passed is !, this instead checks that +# the given command was not called. +# +test_subcommand_inexact () { + local negate= + if test "$1" = "!" + then + negate=t + shift + fi + + local expr=$(printf '"%s".*' "$@") + expr="${expr%,}" + + if test -n "$negate" + then + ! grep "\"event\":\"child_start\".*\[$expr\]" + else + grep "\"event\":\"child_start\".*\[$expr\]" + fi +} + +# Check that the given command was invoked as part of the # trace2-format trace on stdin. # # test_region [!] <category> <label> git <command> <args>... diff --git a/t/test-lib.sh b/t/test-lib.sh index 2679a7596a..0f7a137c7d 100644 --- a/t/test-lib.sh +++ b/t/test-lib.sh @@ -476,6 +476,13 @@ export GIT_TEST_MERGE_ALGORITHM GIT_TRACE_BARE=1 export GIT_TRACE_BARE +# Some tests scan the GIT_TRACE2_EVENT feed for events, but the +# default depth is 2, which frequently causes issues when the +# events are wrapped in new regions. Set it to a sufficiently +# large depth to avoid custom changes in the test suite. +GIT_TRACE2_EVENT_NESTING=100 +export GIT_TRACE2_EVENT_NESTING + # Use specific version of the index file format if test -n "${GIT_TEST_INDEX_VERSION:+isset}" then @@ -489,6 +496,13 @@ then export GIT_PERL_FATAL_WARNINGS fi +case $GIT_TEST_FSYNC in +'') + GIT_TEST_FSYNC=0 + export GIT_TEST_FSYNC + ;; +esac + # Add libc MALLOC and MALLOC_PERTURB test # only if we are not executing the test with valgrind if test -n "$valgrind" || @@ -589,6 +603,15 @@ USER_TERM="$TERM" TERM=dumb export TERM USER_TERM +# What is written by tests to stdout and stderr is sent to different places +# depending on the test mode (e.g. /dev/null in non-verbose mode, piped to tee +# with --tee option, etc.). We save the original stdin to FD #6 and stdout and +# stderr to #5 and #7, so that the test framework can use them (e.g. for +# printing errors within the test framework) independently of the test mode. +exec 5>&1 +exec 6<&0 +exec 7>&2 + _error_exit () { finalize_junit_xml GIT_EXIT_OK=t @@ -612,7 +635,7 @@ BAIL_OUT () { local bail_out="Bail out! " local message="$1" - say_color error $bail_out "$message" + say_color >&5 error $bail_out "$message" _error_exit } @@ -637,9 +660,6 @@ then exit 0 fi -exec 5>&1 -exec 6<&0 -exec 7>&2 if test "$verbose_log" = "t" then exec 3>>"$GIT_TEST_TEE_OUTPUT_FILE" 4>&3 @@ -669,6 +689,8 @@ test_fixed=0 test_broken=0 test_success=0 +test_missing_prereq= + test_external_has_tap=0 die () { @@ -1069,6 +1091,14 @@ test_skip () { of_prereq=" of $test_prereq" fi skipped_reason="missing $missing_prereq${of_prereq}" + + # Keep a list of all the missing prereq for result aggregation + if test -z "$missing_prereq" + then + test_missing_prereq=$missing_prereq + else + test_missing_prereq="$test_missing_prereq,$missing_prereq" + fi fi case "$to_skip" in @@ -1175,6 +1205,7 @@ test_done () { fixed $test_fixed broken $test_broken failed $test_failure + missing_prereq $test_missing_prereq EOF fi @@ -1734,6 +1765,10 @@ build_option () { sed -ne "s/^$1: //p" } +test_lazy_prereq SIZE_T_IS_64BIT ' + test 8 -eq "$(build_option sizeof-size_t)" +' + test_lazy_prereq LONG_IS_64BIT ' test 8 -le "$(build_option sizeof-long)" ' @@ -25,8 +25,9 @@ static int run_gpg_verify(const char *buf, unsigned long size, unsigned flags) return error("no signature found"); } - ret = check_signature(payload.buf, payload.len, signature.buf, - signature.len, &sigc); + sigc.payload_type = SIGNATURE_PAYLOAD_TAG; + sigc.payload = strbuf_detach(&payload, &sigc.payload_len); + ret = check_signature(&sigc, signature.buf, signature.len); if (!(flags & GPG_VERIFY_OMIT_STATUS)) print_signature_buffer(&sigc, flags); diff --git a/tmp-objdir.c b/tmp-objdir.c index b8d880e362..3d38eeab66 100644 --- a/tmp-objdir.c +++ b/tmp-objdir.c @@ -1,5 +1,6 @@ #include "cache.h" #include "tmp-objdir.h" +#include "chdir-notify.h" #include "dir.h" #include "sigchain.h" #include "string-list.h" @@ -11,6 +12,8 @@ struct tmp_objdir { struct strbuf path; struct strvec env; + struct object_directory *prev_odb; + int will_destroy; }; /* @@ -38,6 +41,9 @@ static int tmp_objdir_destroy_1(struct tmp_objdir *t, int on_signal) if (t == the_tmp_objdir) the_tmp_objdir = NULL; + if (!on_signal && t->prev_odb) + restore_primary_odb(t->prev_odb, t->path.buf); + /* * This may use malloc via strbuf_grow(), but we should * have pre-grown t->path sufficiently so that this @@ -52,6 +58,7 @@ static int tmp_objdir_destroy_1(struct tmp_objdir *t, int on_signal) */ if (!on_signal) tmp_objdir_free(t); + return err; } @@ -121,7 +128,7 @@ static int setup_tmp_objdir(const char *root) return ret; } -struct tmp_objdir *tmp_objdir_create(void) +struct tmp_objdir *tmp_objdir_create(const char *prefix) { static int installed_handlers; struct tmp_objdir *t; @@ -129,11 +136,16 @@ struct tmp_objdir *tmp_objdir_create(void) if (the_tmp_objdir) BUG("only one tmp_objdir can be used at a time"); - t = xmalloc(sizeof(*t)); + t = xcalloc(1, sizeof(*t)); strbuf_init(&t->path, 0); strvec_init(&t->env); - strbuf_addf(&t->path, "%s/incoming-XXXXXX", get_object_directory()); + /* + * Use a string starting with tmp_ so that the builtin/prune.c code + * can recognize any stale objdirs left behind by a crash and delete + * them. + */ + strbuf_addf(&t->path, "%s/tmp_objdir-%s-XXXXXX", get_object_directory(), prefix); /* * Grow the strbuf beyond any filename we expect to be placed in it. @@ -269,6 +281,13 @@ int tmp_objdir_migrate(struct tmp_objdir *t) if (!t) return 0; + if (t->prev_odb) { + if (the_repository->objects->odb->will_destroy) + BUG("migrating an ODB that was marked for destruction"); + restore_primary_odb(t->prev_odb, t->path.buf); + t->prev_odb = NULL; + } + strbuf_addbuf(&src, &t->path); strbuf_addstr(&dst, get_object_directory()); @@ -292,3 +311,33 @@ void tmp_objdir_add_as_alternate(const struct tmp_objdir *t) { add_to_alternates_memory(t->path.buf); } + +void tmp_objdir_replace_primary_odb(struct tmp_objdir *t, int will_destroy) +{ + if (t->prev_odb) + BUG("the primary object database is already replaced"); + t->prev_odb = set_temporary_primary_odb(t->path.buf, will_destroy); + t->will_destroy = will_destroy; +} + +struct tmp_objdir *tmp_objdir_unapply_primary_odb(void) +{ + if (!the_tmp_objdir || !the_tmp_objdir->prev_odb) + return NULL; + + restore_primary_odb(the_tmp_objdir->prev_odb, the_tmp_objdir->path.buf); + the_tmp_objdir->prev_odb = NULL; + return the_tmp_objdir; +} + +void tmp_objdir_reapply_primary_odb(struct tmp_objdir *t, const char *old_cwd, + const char *new_cwd) +{ + char *path; + + path = reparent_relative_path(old_cwd, new_cwd, t->path.buf); + strbuf_reset(&t->path); + strbuf_addstr(&t->path, path); + free(path); + tmp_objdir_replace_primary_odb(t, t->will_destroy); +} diff --git a/tmp-objdir.h b/tmp-objdir.h index b1e45b4c75..cda5ec7677 100644 --- a/tmp-objdir.h +++ b/tmp-objdir.h @@ -10,7 +10,7 @@ * * Example: * - * struct tmp_objdir *t = tmp_objdir_create(); + * struct tmp_objdir *t = tmp_objdir_create("incoming"); * if (!run_command_v_opt_cd_env(cmd, 0, NULL, tmp_objdir_env(t)) && * !tmp_objdir_migrate(t)) * printf("success!\n"); @@ -22,9 +22,10 @@ struct tmp_objdir; /* - * Create a new temporary object directory; returns NULL on failure. + * Create a new temporary object directory with the specified prefix; + * returns NULL on failure. */ -struct tmp_objdir *tmp_objdir_create(void); +struct tmp_objdir *tmp_objdir_create(const char *prefix); /* * Return a list of environment strings, suitable for use with @@ -51,4 +52,26 @@ int tmp_objdir_destroy(struct tmp_objdir *); */ void tmp_objdir_add_as_alternate(const struct tmp_objdir *); +/* + * Replaces the writable object store in the current process with the temporary + * object directory and makes the former main object store an alternate. + * If will_destroy is nonzero, the object directory may not be migrated. + */ +void tmp_objdir_replace_primary_odb(struct tmp_objdir *, int will_destroy); + +/* + * If the primary object database was replaced by a temporary object directory, + * restore it to its original value while keeping the directory contents around. + * Returns NULL if the primary object database was not replaced. + */ +struct tmp_objdir *tmp_objdir_unapply_primary_odb(void); + +/* + * Reapplies the former primary temporary object database, after potentially + * changing its relative path. + */ +void tmp_objdir_reapply_primary_odb(struct tmp_objdir *, const char *old_cwd, + const char *new_cwd); + + #endif /* TMP_OBJDIR_H */ diff --git a/trace2/tr2_dst.c b/trace2/tr2_dst.c index bda283e7f4..8a21dd2972 100644 --- a/trace2/tr2_dst.c +++ b/trace2/tr2_dst.c @@ -1,4 +1,5 @@ #include "cache.h" +#include "sigchain.h" #include "trace2/tr2_dst.h" #include "trace2/tr2_sid.h" #include "trace2/tr2_sysenv.h" @@ -360,6 +361,7 @@ int tr2_dst_trace_want(struct tr2_dst *dst) void tr2_dst_write_line(struct tr2_dst *dst, struct strbuf *buf_line) { int fd = tr2_dst_get_trace_fd(dst); + ssize_t bytes; strbuf_complete_line(buf_line); /* ensure final NL on buffer */ @@ -378,12 +380,15 @@ void tr2_dst_write_line(struct tr2_dst *dst, struct strbuf *buf_line) * * If we get an IO error, just close the trace dst. */ - if (write(fd, buf_line->buf, buf_line->len) >= 0) + sigchain_push(SIGPIPE, SIG_IGN); + bytes = write(fd, buf_line->buf, buf_line->len); + sigchain_pop(SIGPIPE); + if (bytes >= 0) return; + tr2_dst_trace_disable(dst); if (tr2_dst_want_warning()) warning("unable to write trace to '%s': %s", tr2_sysenv_display_name(dst->sysenv_var), strerror(errno)); - tr2_dst_trace_disable(dst); } diff --git a/trace2/tr2_tgt_event.c b/trace2/tr2_tgt_event.c index 3a0014417c..bd17ecdc32 100644 --- a/trace2/tr2_tgt_event.c +++ b/trace2/tr2_tgt_event.c @@ -354,7 +354,7 @@ static void fn_child_start_fl(const char *file, int line, jw_object_inline_begin_array(&jw, "argv"); if (cmd->git_cmd) jw_array_string(&jw, "git"); - jw_array_argv(&jw, cmd->argv); + jw_array_argv(&jw, cmd->args.v); jw_end(&jw); jw_end(&jw); diff --git a/trace2/tr2_tgt_normal.c b/trace2/tr2_tgt_normal.c index 58d9e430f0..6e429a3fb9 100644 --- a/trace2/tr2_tgt_normal.c +++ b/trace2/tr2_tgt_normal.c @@ -232,7 +232,7 @@ static void fn_child_start_fl(const char *file, int line, strbuf_addch(&buf_payload, ' '); if (cmd->git_cmd) strbuf_addstr(&buf_payload, "git "); - sq_append_quote_argv_pretty(&buf_payload, cmd->argv); + sq_append_quote_argv_pretty(&buf_payload, cmd->args.v); normal_io_write_fl(file, line, &buf_payload); strbuf_release(&buf_payload); diff --git a/trace2/tr2_tgt_perf.c b/trace2/tr2_tgt_perf.c index e4acca13d6..2ff9cf7083 100644 --- a/trace2/tr2_tgt_perf.c +++ b/trace2/tr2_tgt_perf.c @@ -335,10 +335,10 @@ static void fn_child_start_fl(const char *file, int line, strbuf_addstr(&buf_payload, " argv:["); if (cmd->git_cmd) { strbuf_addstr(&buf_payload, "git"); - if (cmd->argv[0]) + if (cmd->args.nr) strbuf_addch(&buf_payload, ' '); } - sq_append_quote_argv_pretty(&buf_payload, cmd->argv); + sq_append_quote_argv_pretty(&buf_payload, cmd->args.v); strbuf_addch(&buf_payload, ']'); perf_io_write_fl(file, line, event_name, NULL, &us_elapsed_absolute, @@ -236,7 +236,7 @@ static char *apply_command(struct conf_info *conf, const char *arg) strbuf_replace(&cmd, TRAILER_ARG_STRING, arg); strvec_push(&cp.args, cmd.buf); } - cp.env = local_repo_env; + strvec_pushv(&cp.env_array, (const char **)local_repo_env); cp.no_stdin = 1; cp.use_shell = 1; diff --git a/transport.c b/transport.c index e4f1decae2..92ab9a3fa6 100644 --- a/transport.c +++ b/transport.c @@ -1204,16 +1204,15 @@ static int run_pre_push_hook(struct transport *transport, struct ref *r; struct child_process proc = CHILD_PROCESS_INIT; struct strbuf buf; - const char *argv[4]; + const char *hook_path = find_hook("pre-push"); - if (!(argv[0] = find_hook("pre-push"))) + if (!hook_path) return 0; - argv[1] = transport->remote->name; - argv[2] = transport->url; - argv[3] = NULL; + strvec_push(&proc.args, hook_path); + strvec_push(&proc.args, transport->remote->name); + strvec_push(&proc.args, transport->url); - proc.argv = argv; proc.in = -1; proc.trace2_hook_name = "pre-push"; diff --git a/tree-diff.c b/tree-diff.c index 437c98a70e..69031d7cba 100644 --- a/tree-diff.c +++ b/tree-diff.c @@ -603,8 +603,7 @@ static void try_to_follow_renames(const struct object_id *old_oid, * about dry-run mode and returns wildcard info. */ if (opt->pathspec.has_wildcard) - die("BUG:%s:%d: wildcards are not supported", - __FILE__, __LINE__); + BUG("wildcards are not supported"); #endif /* Remove the file creation entry from the diff queue, and remember it */ diff --git a/unpack-trees.c b/unpack-trees.c index 89ca95ce90..360844bda3 100644 --- a/unpack-trees.c +++ b/unpack-trees.c @@ -36,6 +36,9 @@ static const char *unpack_plumbing_errors[NB_UNPACK_TREES_WARNING_TYPES] = { /* ERROR_NOT_UPTODATE_DIR */ "Updating '%s' would lose untracked files in it", + /* ERROR_CWD_IN_THE_WAY */ + "Refusing to remove '%s' since it is the current working directory.", + /* ERROR_WOULD_LOSE_UNTRACKED_OVERWRITTEN */ "Untracked working tree file '%s' would be overwritten by merge.", @@ -131,6 +134,9 @@ void setup_unpack_trees_porcelain(struct unpack_trees_options *opts, msgs[ERROR_NOT_UPTODATE_DIR] = _("Updating the following directories would lose untracked files in them:\n%s"); + msgs[ERROR_CWD_IN_THE_WAY] = + _("Refusing to remove the current working directory:\n%s"); + if (!strcmp(cmd, "checkout")) msg = advice_enabled(ADVICE_COMMIT_BEFORE_MERGE) ? _("The following untracked working tree files would be removed by checkout:\n%%s" @@ -645,17 +651,24 @@ static void mark_ce_used_same_name(struct cache_entry *ce, } } -static struct cache_entry *next_cache_entry(struct unpack_trees_options *o) +static struct cache_entry *next_cache_entry(struct unpack_trees_options *o, int *hint) { const struct index_state *index = o->src_index; int pos = o->cache_bottom; + if (*hint > pos) + pos = *hint; + while (pos < index->cache_nr) { struct cache_entry *ce = index->cache[pos]; - if (!(ce->ce_flags & CE_UNPACKED)) + if (!(ce->ce_flags & CE_UNPACKED)) { + *hint = pos + 1; return ce; + } pos++; } + + *hint = pos; return NULL; } @@ -1231,7 +1244,9 @@ static int find_cache_pos(struct traverse_info *info, /* * Given a sparse directory entry 'ce', compare ce->name to - * info->name + '/' + p->path + '/' if info->name is non-empty. + * info->traverse_path + p->path + '/' if info->traverse_path + * is non-empty. + * * Compare ce->name to p->path + '/' otherwise. Note that * ce->name must end in a trailing '/' because it is a sparse * directory entry. @@ -1243,11 +1258,11 @@ static int sparse_dir_matches_path(const struct cache_entry *ce, assert(S_ISSPARSEDIR(ce->ce_mode)); assert(ce->name[ce->ce_namelen - 1] == '/'); - if (info->namelen) - return ce->ce_namelen == info->namelen + p->pathlen + 2 && - ce->name[info->namelen] == '/' && - !strncmp(ce->name, info->name, info->namelen) && - !strncmp(ce->name + info->namelen + 1, p->path, p->pathlen); + if (info->pathlen) + return ce->ce_namelen == info->pathlen + p->pathlen + 1 && + ce->name[info->pathlen - 1] == '/' && + !strncmp(ce->name, info->traverse_path, info->pathlen) && + !strncmp(ce->name + info->pathlen, p->path, p->pathlen); return ce->ce_namelen == p->pathlen + 1 && !strncmp(ce->name, p->path, p->pathlen); } @@ -1365,12 +1380,13 @@ static int unpack_callback(int n, unsigned long mask, unsigned long dirmask, str /* Are we supposed to look at the index too? */ if (o->merge) { + int hint = -1; while (1) { int cmp; struct cache_entry *ce; if (o->diff_index_cached) - ce = next_cache_entry(o); + ce = next_cache_entry(o, &hint); else ce = find_cache_entry(info, p); @@ -1690,7 +1706,7 @@ static int verify_absent(const struct cache_entry *, int unpack_trees(unsigned len, struct tree_desc *t, struct unpack_trees_options *o) { struct repository *repo = the_repository; - int i, ret; + int i, hint, ret; static struct cache_entry *dfc; struct pattern_list pl; int free_pattern_list = 0; @@ -1779,13 +1795,15 @@ int unpack_trees(unsigned len, struct tree_desc *t, struct unpack_trees_options info.pathspec = o->pathspec; if (o->prefix) { + hint = -1; + /* * Unpack existing index entries that sort before the * prefix the tree is spliced into. Note that o->merge * is always true in this case. */ while (1) { - struct cache_entry *ce = next_cache_entry(o); + struct cache_entry *ce = next_cache_entry(o, &hint); if (!ce) break; if (ce_in_traverse_path(ce, &info)) @@ -1806,8 +1824,9 @@ int unpack_trees(unsigned len, struct tree_desc *t, struct unpack_trees_options /* Any left-over entries in the index? */ if (o->merge) { + hint = -1; while (1) { - struct cache_entry *ce = next_cache_entry(o); + struct cache_entry *ce = next_cache_entry(o, &hint); if (!ce) break; if (unpack_index_entry(ce, o) < 0) @@ -2146,10 +2165,7 @@ static int verify_clean_subdirectory(const struct cache_entry *ce, cnt++; } - /* - * Then we need to make sure that we do not lose a locally - * present file that is not ignored. - */ + /* Do not lose a locally present file that is not ignored. */ pathbuf = xstrfmt("%.*s/", namelen, ce->name); memset(&d, 0, sizeof(d)); @@ -2160,6 +2176,12 @@ static int verify_clean_subdirectory(const struct cache_entry *ce, free(pathbuf); if (i) return add_rejected_path(o, ERROR_NOT_UPTODATE_DIR, ce->name); + + /* Do not lose startup_info->original_cwd */ + if (startup_info->original_cwd && + !strcmp(startup_info->original_cwd, ce->name)) + return add_rejected_path(o, ERROR_CWD_IN_THE_WAY, ce->name); + return cnt; } @@ -2252,10 +2274,19 @@ static int verify_absent_1(const struct cache_entry *ce, int len; struct stat st; - if (o->index_only || !o->update || - o->reset == UNPACK_RESET_OVERWRITE_UNTRACKED) + if (o->index_only || !o->update) return 0; + if (o->reset == UNPACK_RESET_OVERWRITE_UNTRACKED) { + /* Avoid nuking startup_info->original_cwd... */ + if (startup_info->original_cwd && + !strcmp(startup_info->original_cwd, ce->name)) + return add_rejected_path(o, ERROR_CWD_IN_THE_WAY, + ce->name); + /* ...but nuke anything else. */ + return 0; + } + len = check_leading_path(ce->name, ce_namelen(ce), 0); if (!len) return 0; diff --git a/unpack-trees.h b/unpack-trees.h index 71ffb7eeb0..efb9edfbb2 100644 --- a/unpack-trees.h +++ b/unpack-trees.h @@ -19,6 +19,7 @@ enum unpack_trees_error_types { ERROR_WOULD_OVERWRITE = 0, ERROR_NOT_UPTODATE_FILE, ERROR_NOT_UPTODATE_DIR, + ERROR_CWD_IN_THE_WAY, ERROR_WOULD_LOSE_UNTRACKED_OVERWRITTEN, ERROR_WOULD_LOSE_UNTRACKED_REMOVED, ERROR_BIND_OVERLAP, diff --git a/upload-pack.c b/upload-pack.c index c78d55bc67..8acc98741b 100644 --- a/upload-pack.c +++ b/upload-pack.c @@ -194,7 +194,13 @@ static int write_one_shallow(const struct commit_graft *graft, void *cb_data) } struct output_state { - char buffer[8193]; + /* + * We do writes no bigger than LARGE_PACKET_DATA_MAX - 1, because with + * sideband-64k the band designator takes up 1 byte of space. Because + * relay_pack_data keeps the last byte to itself, we make the buffer 1 + * byte bigger than the intended maximum write size. + */ + char buffer[(LARGE_PACKET_DATA_MAX - 1) + 1]; int used; unsigned packfile_uris_started : 1; unsigned packfile_started : 1; @@ -269,7 +275,7 @@ static void create_pack_file(struct upload_pack_data *pack_data, const struct string_list *uri_protocols) { struct child_process pack_objects = CHILD_PROCESS_INIT; - struct output_state output_state = { { 0 } }; + struct output_state *output_state = xcalloc(1, sizeof(struct output_state)); char progress[128]; char abort_msg[] = "aborting due to possible repository " "corruption on the remote side."; @@ -404,7 +410,7 @@ static void create_pack_file(struct upload_pack_data *pack_data, } if (0 <= pu && (pfd[pu].revents & (POLLIN|POLLHUP))) { int result = relay_pack_data(pack_objects.out, - &output_state, + output_state, pack_data->use_sideband, !!uri_protocols); @@ -438,11 +444,12 @@ static void create_pack_file(struct upload_pack_data *pack_data, } /* flush the data */ - if (output_state.used > 0) { - send_client_data(1, output_state.buffer, output_state.used, + if (output_state->used > 0) { + send_client_data(1, output_state->buffer, output_state->used, pack_data->use_sideband); fprintf(stderr, "flushed.\n"); } + free(output_state); if (pack_data->use_sideband) packet_flush(1); return; @@ -596,14 +603,11 @@ static int do_reachable_revlist(struct child_process *cmd, struct object_array *reachable, enum allow_uor allow_uor) { - static const char *argv[] = { - "rev-list", "--stdin", NULL, - }; struct object *o; FILE *cmd_in = NULL; int i; - cmd->argv = argv; + strvec_pushl(&cmd->args, "rev-list", "--stdin", NULL); cmd->git_cmd = 1; cmd->no_stderr = 1; cmd->in = -1; diff --git a/urlmatch.c b/urlmatch.c index 33a2ccd306..03ad3f30a9 100644 --- a/urlmatch.c +++ b/urlmatch.c @@ -5,7 +5,7 @@ #define URL_DIGIT "0123456789" #define URL_ALPHADIGIT URL_ALPHA URL_DIGIT #define URL_SCHEME_CHARS URL_ALPHADIGIT "+.-" -#define URL_HOST_CHARS URL_ALPHADIGIT ".-[:]" /* IPv6 literals need [:] */ +#define URL_HOST_CHARS URL_ALPHADIGIT ".-_[:]" /* IPv6 literals need [:] */ #define URL_UNSAFE_CHARS " <>\"%{}|\\^`" /* plus 0x00-0x1F,0x7F-0xFF */ #define URL_GEN_RESERVED ":/?#[]@" #define URL_SUB_RESERVED "!$&'()*+,;=" diff --git a/worktree.c b/worktree.c index 092a4f92ad..6f598dcfcd 100644 --- a/worktree.c +++ b/worktree.c @@ -28,11 +28,13 @@ static void add_head_info(struct worktree *wt) { int flags; const char *target; + int ignore_errno; target = refs_resolve_ref_unsafe(get_worktree_ref_store(wt), "HEAD", 0, - &wt->head_oid, &flags); + &wt->head_oid, &flags, + &ignore_errno); if (!target) return; @@ -402,22 +404,19 @@ int is_worktree_being_bisected(const struct worktree *wt, * bisect). New commands that do similar things should update this * function as well. */ -const struct worktree *find_shared_symref(const char *symref, +const struct worktree *find_shared_symref(struct worktree **worktrees, + const char *symref, const char *target) { const struct worktree *existing = NULL; - static struct worktree **worktrees; int i = 0; - if (worktrees) - free_worktrees(worktrees); - worktrees = get_worktrees(); - for (i = 0; worktrees[i]; i++) { struct worktree *wt = worktrees[i]; const char *symref_target; struct ref_store *refs; int flags; + int ignore_errno; if (wt->is_bare) continue; @@ -435,7 +434,8 @@ const struct worktree *find_shared_symref(const char *symref, refs = get_worktree_ref_store(wt); symref_target = refs_resolve_ref_unsafe(refs, symref, 0, - NULL, &flags); + NULL, &flags, + &ignore_errno); if ((flags & REF_ISSYMREF) && symref_target && !strcmp(symref_target, target)) { existing = wt; @@ -563,16 +563,17 @@ int other_head_refs(each_ref_fn fn, void *cb_data) struct worktree *wt = *p; struct object_id oid; int flag; + int ignore_errno; if (wt->is_current) continue; strbuf_reset(&refname); strbuf_worktree_ref(wt, &refname, "HEAD"); - if (!refs_read_ref_full(get_main_ref_store(the_repository), - refname.buf, - RESOLVE_REF_READING, - &oid, &flag)) + if (refs_resolve_ref_unsafe(get_main_ref_store(the_repository), + refname.buf, + RESOLVE_REF_READING, + &oid, &flag, &ignore_errno)) ret = fn(refname.buf, &oid, flag, cb_data); if (ret) break; diff --git a/worktree.h b/worktree.h index 8b7c408132..9e06fcbdf3 100644 --- a/worktree.h +++ b/worktree.h @@ -143,9 +143,10 @@ void free_worktrees(struct worktree **); /* * Check if a per-worktree symref points to a ref in the main worktree * or any linked worktree, and return the worktree that holds the ref, - * or NULL otherwise. The result may be destroyed by the next call. + * or NULL otherwise. */ -const struct worktree *find_shared_symref(const char *symref, +const struct worktree *find_shared_symref(struct worktree **worktrees, + const char *symref, const char *target); /* diff --git a/write-or-die.c b/write-or-die.c index 0b1ec8190b..a3d5784cec 100644 --- a/write-or-die.c +++ b/write-or-die.c @@ -1,4 +1,5 @@ #include "cache.h" +#include "config.h" #include "run-command.h" /* @@ -57,6 +58,10 @@ void fprintf_or_die(FILE *f, const char *fmt, ...) void fsync_or_die(int fd, const char *msg) { + if (use_fsync < 0) + use_fsync = git_env_bool("GIT_TEST_FSYNC", 1); + if (!use_fsync) + return; while (fsync(fd) < 0) { if (errno != EINTR) die_errno("fsync error on '%s'", msg); diff --git a/wt-status.c b/wt-status.c index e4f29b2b4c..335e723a71 100644 --- a/wt-status.c +++ b/wt-status.c @@ -948,11 +948,17 @@ static int stash_count_refs(struct object_id *ooid, struct object_id *noid, return 0; } +static int count_stash_entries(void) +{ + int n = 0; + for_each_reflog_ent("refs/stash", stash_count_refs, &n); + return n; +} + static void wt_longstatus_print_stash_summary(struct wt_status *s) { - int stash_count = 0; + int stash_count = count_stash_entries(); - for_each_reflog_ent("refs/stash", stash_count_refs, &stash_count); if (stash_count > 0) status_printf_ln(s, GIT_COLOR_NORMAL, Q_("Your stash currently has %d entry", @@ -1212,17 +1218,23 @@ static void show_merge_in_progress(struct wt_status *s, static void show_am_in_progress(struct wt_status *s, const char *color) { + int am_empty_patch; + status_printf_ln(s, color, _("You are in the middle of an am session.")); if (s->state.am_empty_patch) status_printf_ln(s, color, _("The current patch is empty.")); if (s->hints) { - if (!s->state.am_empty_patch) + am_empty_patch = s->state.am_empty_patch; + if (!am_empty_patch) status_printf_ln(s, color, _(" (fix conflicts and then run \"git am --continue\")")); status_printf_ln(s, color, _(" (use \"git am --skip\" to skip this patch)")); + if (am_empty_patch) + status_printf_ln(s, color, + _(" (use \"git am --allow-empty\" to record this patch as an empty commit)")); status_printf_ln(s, color, _(" (use \"git am --abort\" to restore the original branch)")); } @@ -2177,6 +2189,18 @@ static void wt_porcelain_v2_print_tracking(struct wt_status *s) } /* + * Print the stash count in a porcelain-friendly format + */ +static void wt_porcelain_v2_print_stash(struct wt_status *s) +{ + int stash_count = count_stash_entries(); + char eol = s->null_termination ? '\0' : '\n'; + + if (stash_count > 0) + fprintf(s->fp, "# stash %d%c", stash_count, eol); +} + +/* * Convert various submodule status values into a * fixed-length string of characters in the buffer provided. */ @@ -2437,6 +2461,9 @@ static void wt_porcelain_v2_print(struct wt_status *s) if (s->show_branch) wt_porcelain_v2_print_tracking(s); + if (s->show_stash) + wt_porcelain_v2_print_stash(s); + for (i = 0; i < s->change.nr; i++) { it = &(s->change.items[i]); d = it->util; diff --git a/xdiff-interface.c b/xdiff-interface.c index 75b32aef51..2e3a5a2943 100644 --- a/xdiff-interface.c +++ b/xdiff-interface.c @@ -313,6 +313,8 @@ int git_xmerge_config(const char *var, const char *value, void *cb) die("'%s' is not a boolean", var); if (!strcmp(value, "diff3")) git_xmerge_style = XDL_MERGE_DIFF3; + else if (!strcmp(value, "zdiff3")) + git_xmerge_style = XDL_MERGE_ZEALOUS_DIFF3; else if (!strcmp(value, "merge")) git_xmerge_style = 0; /* diff --git a/xdiff/xdiff.h b/xdiff/xdiff.h index b29deca5de..72e25a9ffa 100644 --- a/xdiff/xdiff.h +++ b/xdiff/xdiff.h @@ -66,6 +66,7 @@ extern "C" { /* merge output styles */ #define XDL_MERGE_DIFF3 1 +#define XDL_MERGE_ZEALOUS_DIFF3 2 typedef struct s_mmfile { char *ptr; diff --git a/xdiff/xdiffi.c b/xdiff/xdiffi.c index a4542c05b6..69689fab24 100644 --- a/xdiff/xdiffi.c +++ b/xdiff/xdiffi.c @@ -390,12 +390,9 @@ static xdchange_t *xdl_add_change(xdchange_t *xscr, long i1, long i2, long chg1, } -static int recs_match(xrecord_t *rec1, xrecord_t *rec2, long flags) +static int recs_match(xrecord_t *rec1, xrecord_t *rec2) { - return (rec1->ha == rec2->ha && - xdl_recmatch(rec1->ptr, rec1->size, - rec2->ptr, rec2->size, - flags)); + return (rec1->ha == rec2->ha); } /* @@ -759,10 +756,10 @@ static inline int group_previous(xdfile_t *xdf, struct xdlgroup *g) * following group, expand this group to include it. Return 0 on success or -1 * if g cannot be slid down. */ -static int group_slide_down(xdfile_t *xdf, struct xdlgroup *g, long flags) +static int group_slide_down(xdfile_t *xdf, struct xdlgroup *g) { if (g->end < xdf->nrec && - recs_match(xdf->recs[g->start], xdf->recs[g->end], flags)) { + recs_match(xdf->recs[g->start], xdf->recs[g->end])) { xdf->rchg[g->start++] = 0; xdf->rchg[g->end++] = 1; @@ -780,10 +777,10 @@ static int group_slide_down(xdfile_t *xdf, struct xdlgroup *g, long flags) * into a previous group, expand this group to include it. Return 0 on success * or -1 if g cannot be slid up. */ -static int group_slide_up(xdfile_t *xdf, struct xdlgroup *g, long flags) +static int group_slide_up(xdfile_t *xdf, struct xdlgroup *g) { if (g->start > 0 && - recs_match(xdf->recs[g->start - 1], xdf->recs[g->end - 1], flags)) { + recs_match(xdf->recs[g->start - 1], xdf->recs[g->end - 1])) { xdf->rchg[--g->start] = 1; xdf->rchg[--g->end] = 0; @@ -833,7 +830,7 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) { end_matching_other = -1; /* Shift the group backward as much as possible: */ - while (!group_slide_up(xdf, &g, flags)) + while (!group_slide_up(xdf, &g)) if (group_previous(xdfo, &go)) BUG("group sync broken sliding up"); @@ -848,7 +845,7 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) { /* Now shift the group forward as far as possible: */ while (1) { - if (group_slide_down(xdf, &g, flags)) + if (group_slide_down(xdf, &g)) break; if (group_next(xdfo, &go)) BUG("group sync broken sliding down"); @@ -875,7 +872,7 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) { * other file that it can align with. */ while (go.end == go.start) { - if (group_slide_up(xdf, &g, flags)) + if (group_slide_up(xdf, &g)) BUG("match disappeared"); if (group_previous(xdfo, &go)) BUG("group sync broken sliding to match"); @@ -918,7 +915,7 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) { } while (g.end > best_shift) { - if (group_slide_up(xdf, &g, flags)) + if (group_slide_up(xdf, &g)) BUG("best shift unreached"); if (group_previous(xdfo, &go)) BUG("group sync broken sliding to blank line"); diff --git a/xdiff/xhistogram.c b/xdiff/xhistogram.c index e694bfd9e3..80794748b0 100644 --- a/xdiff/xhistogram.c +++ b/xdiff/xhistogram.c @@ -88,19 +88,14 @@ struct region { #define REC(env, s, l) \ (env->xdf##s.recs[l - 1]) -static int cmp_recs(xpparam_t const *xpp, - xrecord_t *r1, xrecord_t *r2) +static int cmp_recs(xrecord_t *r1, xrecord_t *r2) { - return r1->ha == r2->ha && - xdl_recmatch(r1->ptr, r1->size, r2->ptr, r2->size, - xpp->flags); -} + return r1->ha == r2->ha; -#define CMP_ENV(xpp, env, s1, l1, s2, l2) \ - (cmp_recs(xpp, REC(env, s1, l1), REC(env, s2, l2))) +} #define CMP(i, s1, l1, s2, l2) \ - (cmp_recs(i->xpp, REC(i->env, s1, l1), REC(i->env, s2, l2))) + (cmp_recs(REC(i->env, s1, l1), REC(i->env, s2, l2))) #define TABLE_HASH(index, side, line) \ XDL_HASHLONG((REC(index->env, side, line))->ha, index->table_bits) diff --git a/xdiff/xmerge.c b/xdiff/xmerge.c index 1659edb453..fff0b594f9 100644 --- a/xdiff/xmerge.c +++ b/xdiff/xmerge.c @@ -230,7 +230,7 @@ static int fill_conflict_hunk(xdfenv_t *xe1, const char *name1, size += xdl_recs_copy(xe1, m->i1, m->chg1, needs_cr, 1, dest ? dest + size : NULL); - if (style == XDL_MERGE_DIFF3) { + if (style == XDL_MERGE_DIFF3 || style == XDL_MERGE_ZEALOUS_DIFF3) { /* Shared preimage */ if (!dest) { size += marker_size + 1 + needs_cr + marker3_size; @@ -322,6 +322,40 @@ static int xdl_fill_merge_buffer(xdfenv_t *xe1, const char *name1, return size; } +static int recmatch(xrecord_t *rec1, xrecord_t *rec2, unsigned long flags) +{ + return xdl_recmatch(rec1->ptr, rec1->size, + rec2->ptr, rec2->size, flags); +} + +/* + * Remove any common lines from the beginning and end of the conflicted region. + */ +static void xdl_refine_zdiff3_conflicts(xdfenv_t *xe1, xdfenv_t *xe2, xdmerge_t *m, + xpparam_t const *xpp) +{ + xrecord_t **rec1 = xe1->xdf2.recs, **rec2 = xe2->xdf2.recs; + for (; m; m = m->next) { + /* let's handle just the conflicts */ + if (m->mode) + continue; + + while(m->chg1 && m->chg2 && + recmatch(rec1[m->i1], rec2[m->i2], xpp->flags)) { + m->chg1--; + m->chg2--; + m->i1++; + m->i2++; + } + while (m->chg1 && m->chg2 && + recmatch(rec1[m->i1 + m->chg1 - 1], + rec2[m->i2 + m->chg2 - 1], xpp->flags)) { + m->chg1--; + m->chg2--; + } + } +} + /* * Sometimes, changes are not quite identical, but differ in only a few * lines. Try hard to show only these few lines as conflicting. @@ -482,7 +516,22 @@ static int xdl_do_merge(xdfenv_t *xe1, xdchange_t *xscr1, int style = xmp->style; int favor = xmp->favor; - if (style == XDL_MERGE_DIFF3) { + /* + * XDL_MERGE_DIFF3 does not attempt to refine conflicts by looking + * at common areas of sides 1 & 2, because the base (side 0) does + * not match and is being shown. Similarly, simplification of + * non-conflicts is also skipped due to the skipping of conflict + * refinement. + * + * XDL_MERGE_ZEALOUS_DIFF3, on the other hand, will attempt to + * refine conflicts looking for common areas of sides 1 & 2. + * However, since the base is being shown and does not match, + * it will only look for common areas at the beginning or end + * of the conflict block. Since XDL_MERGE_ZEALOUS_DIFF3's + * conflict refinement is much more limited in this fashion, the + * conflict simplification will be skipped. + */ + if (style == XDL_MERGE_DIFF3 || style == XDL_MERGE_ZEALOUS_DIFF3) { /* * "diff3 -m" output does not make sense for anything * more aggressive than XDL_MERGE_EAGER. @@ -603,10 +652,12 @@ static int xdl_do_merge(xdfenv_t *xe1, xdchange_t *xscr1, if (!changes) changes = c; /* refine conflicts */ - if (XDL_MERGE_ZEALOUS <= level && - (xdl_refine_conflicts(xe1, xe2, changes, xpp) < 0 || - xdl_simplify_non_conflicts(xe1, changes, - XDL_MERGE_ZEALOUS < level) < 0)) { + if (style == XDL_MERGE_ZEALOUS_DIFF3) { + xdl_refine_zdiff3_conflicts(xe1, xe2, changes, xpp); + } else if (XDL_MERGE_ZEALOUS <= level && + (xdl_refine_conflicts(xe1, xe2, changes, xpp) < 0 || + xdl_simplify_non_conflicts(xe1, changes, + XDL_MERGE_ZEALOUS < level) < 0)) { xdl_cleanup_merge(changes); return -1; } diff --git a/xdiff/xprepare.c b/xdiff/xprepare.c index abeb8fb84e..4527a4a07c 100644 --- a/xdiff/xprepare.c +++ b/xdiff/xprepare.c @@ -181,15 +181,11 @@ static int xdl_prepare_ctx(unsigned int pass, mmfile_t *mf, long narec, xpparam_ if (!(recs = (xrecord_t **) xdl_malloc(narec * sizeof(xrecord_t *)))) goto abort; - if (XDF_DIFF_ALG(xpp->flags) == XDF_HISTOGRAM_DIFF) - hbits = hsize = 0; - else { - hbits = xdl_hashbits((unsigned int) narec); - hsize = 1 << hbits; - if (!(rhash = (xrecord_t **) xdl_malloc(hsize * sizeof(xrecord_t *)))) - goto abort; - memset(rhash, 0, hsize * sizeof(xrecord_t *)); - } + hbits = xdl_hashbits((unsigned int) narec); + hsize = 1 << hbits; + if (!(rhash = (xrecord_t **) xdl_malloc(hsize * sizeof(xrecord_t *)))) + goto abort; + memset(rhash, 0, hsize * sizeof(xrecord_t *)); nrec = 0; if ((cur = blk = xdl_mmfile_first(mf, &bsize)) != NULL) { @@ -208,9 +204,7 @@ static int xdl_prepare_ctx(unsigned int pass, mmfile_t *mf, long narec, xpparam_ crec->size = (long) (cur - prev); crec->ha = hav; recs[nrec++] = crec; - - if ((XDF_DIFF_ALG(xpp->flags) != XDF_HISTOGRAM_DIFF) && - xdl_classify_record(pass, cf, rhash, hbits, crec) < 0) + if (xdl_classify_record(pass, cf, rhash, hbits, crec) < 0) goto abort; } } @@ -219,10 +213,13 @@ static int xdl_prepare_ctx(unsigned int pass, mmfile_t *mf, long narec, xpparam_ goto abort; memset(rchg, 0, (nrec + 2) * sizeof(char)); - if (!(rindex = (long *) xdl_malloc((nrec + 1) * sizeof(long)))) - goto abort; - if (!(ha = (unsigned long *) xdl_malloc((nrec + 1) * sizeof(unsigned long)))) - goto abort; + if ((XDF_DIFF_ALG(xpp->flags) != XDF_PATIENCE_DIFF) && + (XDF_DIFF_ALG(xpp->flags) != XDF_HISTOGRAM_DIFF)) { + if (!(rindex = xdl_malloc((nrec + 1) * sizeof(*rindex)))) + goto abort; + if (!(ha = xdl_malloc((nrec + 1) * sizeof(*ha)))) + goto abort; + } xdf->nrec = nrec; xdf->recs = recs; @@ -279,8 +276,7 @@ int xdl_prepare_env(mmfile_t *mf1, mmfile_t *mf2, xpparam_t const *xpp, enl1 = xdl_guess_lines(mf1, sample) + 1; enl2 = xdl_guess_lines(mf2, sample) + 1; - if (XDF_DIFF_ALG(xpp->flags) != XDF_HISTOGRAM_DIFF && - xdl_init_classifier(&cf, enl1 + enl2 + 1, xpp->flags) < 0) + if (xdl_init_classifier(&cf, enl1 + enl2 + 1, xpp->flags) < 0) return -1; if (xdl_prepare_ctx(1, mf1, enl1, xpp, &cf, &xe->xdf1) < 0) { @@ -305,8 +301,7 @@ int xdl_prepare_env(mmfile_t *mf1, mmfile_t *mf2, xpparam_t const *xpp, return -1; } - if (XDF_DIFF_ALG(xpp->flags) != XDF_HISTOGRAM_DIFF) - xdl_free_classifier(&cf); + xdl_free_classifier(&cf); return 0; } |