diff options
115 files changed, 3316 insertions, 938 deletions
diff --git a/.cirrus.yml b/.cirrus.yml index 4860bebd32..b6280692d2 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -19,4 +19,4 @@ freebsd_12_task: build_script: - su git -c gmake test_script: - - su git -c 'gmake test' + - su git -c 'gmake DEFAULT_UNIT_TEST_TARGET=unit-tests-prove test unit-tests' diff --git a/.gitignore b/.gitignore index 5e56e471b3..612c0f6a0f 100644 --- a/.gitignore +++ b/.gitignore @@ -135,6 +135,7 @@ /git-remote-ext /git-repack /git-replace +/git-replay /git-request-pull /git-rerere /git-reset diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000000..cd98bcb18a --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,53 @@ +default: + timeout: 2h + +workflow: + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + - if: $CI_COMMIT_TAG + - if: $CI_COMMIT_REF_PROTECTED == "true" + +test: + image: $image + before_script: + - ./ci/install-docker-dependencies.sh + script: + - useradd builder --create-home + - chown -R builder "${CI_PROJECT_DIR}" + - sudo --preserve-env --set-home --user=builder ./ci/run-build-and-tests.sh + after_script: + - | + if test "$CI_JOB_STATUS" != 'success' + then + sudo --preserve-env --set-home --user=builder ./ci/print-test-failures.sh + fi + parallel: + matrix: + - jobname: linux-sha256 + image: ubuntu:latest + CC: clang + - jobname: linux-gcc + image: ubuntu:20.04 + CC: gcc + CC_PACKAGE: gcc-8 + - jobname: linux-TEST-vars + image: ubuntu:20.04 + CC: gcc + CC_PACKAGE: gcc-8 + - jobname: linux-gcc-default + image: ubuntu:latest + CC: gcc + - jobname: linux-leaks + image: ubuntu:latest + CC: gcc + - jobname: linux-asan-ubsan + image: ubuntu:latest + CC: clang + - jobname: pedantic + image: fedora:latest + - jobname: linux-musl + image: alpine:latest + artifacts: + paths: + - t/failed-test-artifacts + when: on_failure diff --git a/Documentation/CodingGuidelines b/Documentation/CodingGuidelines index 8d3a467c01..39b9b7260f 100644 --- a/Documentation/CodingGuidelines +++ b/Documentation/CodingGuidelines @@ -490,7 +490,7 @@ For Perl programs: - Most of the C guidelines above apply. - - We try to support Perl 5.8 and later ("use Perl 5.008"). + - We try to support Perl 5.8.1 and later ("use Perl 5.008001"). - use strict and use warnings are strongly preferred. diff --git a/Documentation/Makefile b/Documentation/Makefile index b629176d7d..3f2383a12c 100644 --- a/Documentation/Makefile +++ b/Documentation/Makefile @@ -122,6 +122,7 @@ TECH_DOCS += technical/scalar TECH_DOCS += technical/send-pack-pipeline TECH_DOCS += technical/shallow TECH_DOCS += technical/trivial-merge +TECH_DOCS += technical/unit-tests SP_ARTICLES += $(TECH_DOCS) SP_ARTICLES += technical/api-index diff --git a/Documentation/RelNotes/2.44.0.txt b/Documentation/RelNotes/2.44.0.txt new file mode 100644 index 0000000000..6a2bd93c91 --- /dev/null +++ b/Documentation/RelNotes/2.44.0.txt @@ -0,0 +1,43 @@ +Git v2.44 Release Notes +======================= + +UI, Workflows & Features + + * "git add" and "git stash" learned to support the ":(attr:...)" + magic pathspec. + + * "git rebase --autosquash" is now enabled for non-interactive rebase, + but it is still incompatible with the apply backend. + + +Performance, Internal Implementation, Development Support etc. + + * Process to add some form of low-level unit tests has started. + + * Add support for GitLab CI. + + * "git for-each-ref --no-sort" still sorted the refs alphabetically + which paid non-trivial cost. It has been redefined to show output + in an unspecified order, to allow certain optimizations to take + advantage of. + + +Fixes since v2.43 +----------------- + + * The way CI testing used "prove" could lead to running the test + suite twice needlessly, which has been corrected. + (merge e7e03ef995 js/ci-discard-prove-state later to maint). + + * Update ref-related tests. + + * "git format-patch --encode-email-headers" ignored the option when + preparing the cover letter, which has been corrected. + + * Newer versions of Getopt::Long started giving warnings against our + (ab)use of it in "git send-email". Bump the minimum version + requirement for Perl to 5.8.1 (from September 2002) to allow + simplifying our implementation. + (merge 6ff658cc78 tz/send-email-negatable-options later to maint). + + * Other code cleanup, docfix, build fix, etc. diff --git a/Documentation/config/rebase.txt b/Documentation/config/rebase.txt index 9c248accec..d59576dbb2 100644 --- a/Documentation/config/rebase.txt +++ b/Documentation/config/rebase.txt @@ -9,7 +9,9 @@ rebase.stat:: rebase. False by default. rebase.autoSquash:: - If set to true enable `--autosquash` option by default. + If set to true, enable the `--autosquash` option of + linkgit:git-rebase[1] by default for interactive mode. + This can be overridden with the `--no-autosquash` option. rebase.autoStash:: When set to true, automatically create a temporary stash entry diff --git a/Documentation/git-for-each-ref.txt b/Documentation/git-for-each-ref.txt index e86d5700dd..be9543f684 100644 --- a/Documentation/git-for-each-ref.txt +++ b/Documentation/git-for-each-ref.txt @@ -51,17 +51,14 @@ OPTIONS key. --format=<format>:: - A string that interpolates `%(fieldname)` from a ref being shown - and the object it points at. If `fieldname` - is prefixed with an asterisk (`*`) and the ref points - at a tag object, use the value for the field in the object - which the tag object refers to (instead of the field in the tag object). - When unspecified, `<format>` defaults to - `%(objectname) SPC %(objecttype) TAB %(refname)`. - It also interpolates `%%` to `%`, and `%xx` where `xx` - are hex digits interpolates to character with hex code - `xx`; for example `%00` interpolates to `\0` (NUL), - `%09` to `\t` (TAB) and `%0a` to `\n` (LF). + A string that interpolates `%(fieldname)` from a ref being shown and + the object it points at. In addition, the string literal `%%` + renders as `%` and `%xx` - where `xx` are hex digits - renders as + the character with hex code `xx`. For example, `%00` interpolates to + `\0` (NUL), `%09` to `\t` (TAB), and `%0a` to `\n` (LF). ++ +When unspecified, `<format>` defaults to `%(objectname) SPC %(objecttype) +TAB %(refname)`. --color[=<when>]:: Respect any colors specified in the `--format` option. The @@ -298,6 +295,10 @@ fields will correspond to the appropriate date or name-email-date tuple from the `committer` or `tagger` fields depending on the object type. These are intended for working on a mix of annotated and lightweight tags. +For tag objects, a `fieldname` prefixed with an asterisk (`*`) expands to +the `fieldname` value of the peeled object, rather than that of the tag +object itself. + Fields that have name-email-date tuple as its value (`author`, `committer`, and `tagger`) can be suffixed with `name`, `email`, and `date` to extract the named component. For email fields (`authoremail`, diff --git a/Documentation/git-rebase.txt b/Documentation/git-rebase.txt index b4526ca246..1dd6555f66 100644 --- a/Documentation/git-rebase.txt +++ b/Documentation/git-rebase.txt @@ -589,21 +589,27 @@ See also INCOMPATIBLE OPTIONS below. --autosquash:: --no-autosquash:: - When the commit log message begins with "squash! ..." or "fixup! ..." - or "amend! ...", and there is already a commit in the todo list that - matches the same `...`, automatically modify the todo list of - `rebase -i`, so that the commit marked for squashing comes right after - the commit to be modified, and change the action of the moved commit - from `pick` to `squash` or `fixup` or `fixup -C` respectively. A commit - matches the `...` if the commit subject matches, or if the `...` refers - to the commit's hash. As a fall-back, partial matches of the commit - subject work, too. The recommended way to create fixup/amend/squash - commits is by using the `--fixup`, `--fixup=amend:` or `--fixup=reword:` - and `--squash` options respectively of linkgit:git-commit[1]. + Automatically squash commits with specially formatted messages into + previous commits being rebased. If a commit message starts with + "squash! ", "fixup! " or "amend! ", the remainder of the subject line + is taken as a commit specifier, which matches a previous commit if it + matches the subject line or the hash of that commit. If no commit + matches fully, matches of the specifier with the start of commit + subjects are considered. + -If the `--autosquash` option is enabled by default using the -configuration variable `rebase.autoSquash`, this option can be -used to override and disable this setting. +In the rebase todo list, the actions of squash, fixup and amend commits are +changed from `pick` to `squash`, `fixup` or `fixup -C`, respectively, and they +are moved right after the commit they modify. The `--interactive` option can +be used to review and edit the todo list before proceeding. ++ +The recommended way to create commits with squash markers is by using the +`--squash`, `--fixup`, `--fixup=amend:` or `--fixup=reword:` options of +linkgit:git-commit[1], which take the target commit as an argument and +automatically fill in the subject line of the new commit from that. ++ +Settting configuration variable `rebase.autoSquash` to true enables +auto-squashing by default for interactive rebase. The `--no-autosquash` +option can be used to override that setting. + See also INCOMPATIBLE OPTIONS below. diff --git a/Documentation/git-replay.txt b/Documentation/git-replay.txt new file mode 100644 index 0000000000..f6c269c62d --- /dev/null +++ b/Documentation/git-replay.txt @@ -0,0 +1,127 @@ +git-replay(1) +============= + +NAME +---- +git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos too + + +SYNOPSIS +-------- +[verse] +(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) <revision-range>... + +DESCRIPTION +----------- + +Takes ranges of commits and replays them onto a new location. Leaves +the working tree and the index untouched, and updates no references. +The output of this command is meant to be used as input to +`git update-ref --stdin`, which would update the relevant branches +(see the OUTPUT section below). + +THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE. + +OPTIONS +------- + +--onto <newbase>:: + Starting point at which to create the new commits. May be any + valid commit, and not just an existing branch name. ++ +When `--onto` is specified, the update-ref command(s) in the output will +update the branch(es) in the revision range to point at the new +commits, similar to the way how `git rebase --update-refs` updates +multiple branches in the affected range. + +--advance <branch>:: + Starting point at which to create the new commits; must be a + branch name. ++ +When `--advance` is specified, the update-ref command(s) in the output +will update the branch passed as an argument to `--advance` to point at +the new commits (in other words, this mimics a cherry-pick operation). + +<revision-range>:: + Range of commits to replay. More than one <revision-range> can + be passed, but in `--advance <branch>` mode, they should have + a single tip, so that it's clear where <branch> should point + to. See "Specifying Ranges" in linkgit:git-rev-parse and the + "Commit Limiting" options below. + +include::rev-list-options.txt[] + +OUTPUT +------ + +When there are no conflicts, the output of this command is usable as +input to `git update-ref --stdin`. It is of the form: + + update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH} + update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH} + update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH} + +where the number of refs updated depends on the arguments passed and +the shape of the history being replayed. When using `--advance`, the +number of refs updated is always one, but for `--onto`, it can be one +or more (rebasing multiple branches simultaneously is supported). + +EXIT STATUS +----------- + +For a successful, non-conflicted replay, the exit status is 0. When +the replay has conflicts, the exit status is 1. If the replay is not +able to complete (or start) due to some kind of error, the exit status +is something other than 0 or 1. + +EXAMPLES +-------- + +To simply rebase `mybranch` onto `target`: + +------------ +$ git replay --onto target origin/main..mybranch +update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH} +------------ + +To cherry-pick the commits from mybranch onto target: + +------------ +$ git replay --advance target origin/main..mybranch +update refs/heads/target ${NEW_target_HASH} ${OLD_target_HASH} +------------ + +Note that the first two examples replay the exact same commits and on +top of the exact same new base, they only differ in that the first +provides instructions to make mybranch point at the new commits and +the second provides instructions to make target point at them. + +What if you have a stack of branches, one depending upon another, and +you'd really like to rebase the whole set? + +------------ +$ git replay --contained --onto origin/main origin/main..tipbranch +update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH} +update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH} +update refs/heads/tipbranch ${NEW_tipbranch_HASH} ${OLD_tipbranch_HASH} +------------ + +When calling `git replay`, one does not need to specify a range of +commits to replay using the syntax `A..B`; any range expression will +do: + +------------ +$ git replay --onto origin/main ^base branch1 branch2 branch3 +update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH} +update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH} +update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH} +------------ + +This will simultaneously rebase `branch1`, `branch2`, and `branch3`, +all commits they have since `base`, playing them on top of +`origin/main`. These three branches may have commits on top of `base` +that they have in common, but that does not need to be the case. + +GIT +--- +Part of the linkgit:git[1] suite diff --git a/Documentation/technical/unit-tests.txt b/Documentation/technical/unit-tests.txt new file mode 100644 index 0000000000..206037ffb1 --- /dev/null +++ b/Documentation/technical/unit-tests.txt @@ -0,0 +1,240 @@ += Unit Testing + +In our current testing environment, we spend a significant amount of effort +crafting end-to-end tests for error conditions that could easily be captured by +unit tests (or we simply forgo some hard-to-setup and rare error conditions). +Unit tests additionally provide stability to the codebase and can simplify +debugging through isolation. Writing unit tests in pure C, rather than with our +current shell/test-tool helper setup, simplifies test setup, simplifies passing +data around (no shell-isms required), and reduces testing runtime by not +spawning a separate process for every test invocation. + +We believe that a large body of unit tests, living alongside the existing test +suite, will improve code quality for the Git project. + +== Definitions + +For the purposes of this document, we'll use *test framework* to refer to +projects that support writing test cases and running tests within the context +of a single executable. *Test harness* will refer to projects that manage +running multiple executables (each of which may contain multiple test cases) and +aggregating their results. + +In reality, these terms are not strictly defined, and many of the projects +discussed below contain features from both categories. + +For now, we will evaluate projects solely on their framework features. Since we +are relying on having TAP output (see below), we can assume that any framework +can be made to work with a harness that we can choose later. + + +== Summary + +We believe the best way forward is to implement a custom TAP framework for the +Git project. We use a version of the framework originally proposed in +https://lore.kernel.org/git/c902a166-98ce-afba-93f2-ea6027557176@gmail.com/[1]. + +See the <<framework-selection,Framework Selection>> section below for the +rationale behind this decision. + + +== Choosing a test harness + +During upstream discussion, it was occasionally noted that `prove` provides many +convenient features, such as scheduling slower tests first, or re-running +previously failed tests. + +While we already support the use of `prove` as a test harness for the shell +tests, it is not strictly required. The t/Makefile allows running shell tests +directly (though with interleaved output if parallelism is enabled). Git +developers who wish to use `prove` as a more advanced harness can do so by +setting DEFAULT_TEST_TARGET=prove in their config.mak. + +We will follow a similar approach for unit tests: by default the test +executables will be run directly from the t/Makefile, but `prove` can be +configured with DEFAULT_UNIT_TEST_TARGET=prove. + + +[[framework-selection]] +== Framework selection + +There are a variety of features we can use to rank the candidate frameworks, and +those features have different priorities: + +* Critical features: we probably won't consider a framework without these +** Can we legally / easily use the project? +*** <<license,License>> +*** <<vendorable-or-ubiquitous,Vendorable or ubiquitous>> +*** <<maintainable-extensible,Maintainable / extensible>> +*** <<major-platform-support,Major platform support>> +** Does the project support our bare-minimum needs? +*** <<tap-support,TAP support>> +*** <<diagnostic-output,Diagnostic output>> +*** <<runtime-skippable-tests,Runtime-skippable tests>> +* Nice-to-have features: +** <<parallel-execution,Parallel execution>> +** <<mock-support,Mock support>> +** <<signal-error-handling,Signal & error-handling>> +* Tie-breaker stats +** <<project-kloc,Project KLOC>> +** <<adoption,Adoption>> + +[[license]] +=== License + +We must be able to legally use the framework in connection with Git. As Git is +licensed only under GPLv2, we must eliminate any LGPLv3, GPLv3, or Apache 2.0 +projects. + +[[vendorable-or-ubiquitous]] +=== Vendorable or ubiquitous + +We want to avoid forcing Git developers to install new tools just to run unit +tests. Any prospective frameworks and harnesses must either be vendorable +(meaning, we can copy their source directly into Git's repository), or so +ubiquitous that it is reasonable to expect that most developers will have the +tools installed already. + +[[maintainable-extensible]] +=== Maintainable / extensible + +It is unlikely that any pre-existing project perfectly fits our needs, so any +project we select will need to be actively maintained and open to accepting +changes. Alternatively, assuming we are vendoring the source into our repo, it +must be simple enough that Git developers can feel comfortable making changes as +needed to our version. + +In the comparison table below, "True" means that the framework seems to have +active developers, that it is simple enough that Git developers can make changes +to it, and that the project seems open to accepting external contributions (or +that it is vendorable). "Partial" means that at least one of the above +conditions holds. + +[[major-platform-support]] +=== Major platform support + +At a bare minimum, unit-testing must work on Linux, MacOS, and Windows. + +In the comparison table below, "True" means that it works on all three major +platforms with no issues. "Partial" means that there may be annoyances on one or +more platforms, but it is still usable in principle. + +[[tap-support]] +=== TAP support + +The https://testanything.org/[Test Anything Protocol] is a text-based interface +that allows tests to communicate with a test harness. It is already used by +Git's integration test suite. Supporting TAP output is a mandatory feature for +any prospective test framework. + +In the comparison table below, "True" means this is natively supported. +"Partial" means TAP output must be generated by post-processing the native +output. + +Frameworks that do not have at least Partial support will not be evaluated +further. + +[[diagnostic-output]] +=== Diagnostic output + +When a test case fails, the framework must generate enough diagnostic output to +help developers find the appropriate test case in source code in order to debug +the failure. + +[[runtime-skippable-tests]] +=== Runtime-skippable tests + +Test authors may wish to skip certain test cases based on runtime circumstances, +so the framework should support this. + +[[parallel-execution]] +=== Parallel execution + +Ideally, we will build up a significant collection of unit test cases, most +likely split across multiple executables. It will be necessary to run these +tests in parallel to enable fast develop-test-debug cycles. + +In the comparison table below, "True" means that individual test cases within a +single test executable can be run in parallel. We assume that executable-level +parallelism can be handled by the test harness. + +[[mock-support]] +=== Mock support + +Unit test authors may wish to test code that interacts with objects that may be +inconvenient to handle in a test (e.g. interacting with a network service). +Mocking allows test authors to provide a fake implementation of these objects +for more convenient tests. + +[[signal-error-handling]] +=== Signal & error handling + +The test framework should fail gracefully when test cases are themselves buggy +or when they are interrupted by signals during runtime. + +[[project-kloc]] +=== Project KLOC + +The size of the project, in thousands of lines of code as measured by +https://dwheeler.com/sloccount/[sloccount] (rounded up to the next multiple of +1,000). As a tie-breaker, we probably prefer a project with fewer LOC. + +[[adoption]] +=== Adoption + +As a tie-breaker, we prefer a more widely-used project. We use the number of +GitHub / GitLab stars to estimate this. + + +=== Comparison + +:true: [lime-background]#True# +:false: [red-background]#False# +:partial: [yellow-background]#Partial# + +:gpl: [lime-background]#GPL v2# +:isc: [lime-background]#ISC# +:mit: [lime-background]#MIT# +:expat: [lime-background]#Expat# +:lgpl: [lime-background]#LGPL v2.1# + +:custom-impl: https://lore.kernel.org/git/c902a166-98ce-afba-93f2-ea6027557176@gmail.com/[Custom Git impl.] +:greatest: https://github.com/silentbicycle/greatest[Greatest] +:criterion: https://github.com/Snaipe/Criterion[Criterion] +:c-tap: https://github.com/rra/c-tap-harness/[C TAP] +:check: https://libcheck.github.io/check/[Check] + +[format="csv",options="header",width="33%",subs="specialcharacters,attributes,quotes,macros"] +|===== +Framework,"<<license,License>>","<<vendorable-or-ubiquitous,Vendorable or ubiquitous>>","<<maintainable-extensible,Maintainable / extensible>>","<<major-platform-support,Major platform support>>","<<tap-support,TAP support>>","<<diagnostic-output,Diagnostic output>>","<<runtime--skippable-tests,Runtime- skippable tests>>","<<parallel-execution,Parallel execution>>","<<mock-support,Mock support>>","<<signal-error-handling,Signal & error handling>>","<<project-kloc,Project KLOC>>","<<adoption,Adoption>>" +{custom-impl},{gpl},{true},{true},{true},{true},{true},{true},{false},{false},{false},1,0 +{greatest},{isc},{true},{partial},{true},{partial},{true},{true},{false},{false},{false},3,1400 +{criterion},{mit},{false},{partial},{true},{true},{true},{true},{true},{false},{true},19,1800 +{c-tap},{expat},{true},{partial},{partial},{true},{false},{true},{false},{false},{false},4,33 +{check},{lgpl},{false},{partial},{true},{true},{true},{false},{false},{false},{true},17,973 +|===== + +=== Additional framework candidates + +Several suggested frameworks have been eliminated from consideration: + +* Incompatible licenses: +** https://github.com/zorgnax/libtap[libtap] (LGPL v3) +** https://cmocka.org/[cmocka] (Apache 2.0) +* Missing source: https://www.kindahl.net/mytap/doc/index.html[MyTap] +* No TAP support: +** https://nemequ.github.io/munit/[µnit] +** https://github.com/google/cmockery[cmockery] +** https://github.com/lpabon/cmockery2[cmockery2] +** https://github.com/ThrowTheSwitch/Unity[Unity] +** https://github.com/siu/minunit[minunit] +** https://cunit.sourceforge.net/[CUnit] + + +== Milestones + +* Add useful tests of library-like code +* Integrate with + https://lore.kernel.org/git/20230502211454.1673000-1-calvinwan@google.com/[stdlib + work] +* Run alongside regular `make test` target diff --git a/GIT-VERSION-GEN b/GIT-VERSION-GEN index bc3e656eb8..0901e1bb23 100755 --- a/GIT-VERSION-GEN +++ b/GIT-VERSION-GEN @@ -1,7 +1,7 @@ #!/bin/sh GVF=GIT-VERSION-FILE -DEF_VER=v2.43.0 +DEF_VER=v2.43.GIT LF=' ' @@ -11,7 +11,7 @@ LF=' if test -f version then VN=$(cat version) || VN="$DEF_VER" -elif test -d ${GIT_DIR:-.git} -o -f .git && +elif { test -d "${GIT_DIR:-.git}" || test -f .git; } && VN=$(git describe --match "v[0-9]*" HEAD 2>/dev/null) && case "$VN" in *$LF*) (exit 1) ;; @@ -119,7 +119,7 @@ Issues of note: - A POSIX-compliant shell is required to run some scripts needed for everyday use (e.g. "bisect", "request-pull"). - - "Perl" version 5.8 or later is needed to use some of the + - "Perl" version 5.8.1 or later is needed to use some of the features (e.g. sending patches using "git send-email", interacting with svn repositories with "git svn"). If you can live without these, use NO_PERL. Note that recent releases of @@ -682,6 +682,9 @@ TEST_BUILTINS_OBJS = TEST_OBJS = TEST_PROGRAMS_NEED_X = THIRD_PARTY_SOURCES = +UNIT_TEST_PROGRAMS = +UNIT_TEST_DIR = t/unit-tests +UNIT_TEST_BIN = $(UNIT_TEST_DIR)/bin # Having this variable in your environment would break pipelines because # you cause "cd" to echo its destination to stdout. It can also take @@ -750,6 +753,7 @@ SCRIPTS = $(SCRIPT_SH_GEN) \ ETAGS_TARGET = TAGS FUZZ_OBJS += oss-fuzz/fuzz-commit-graph.o +FUZZ_OBJS += oss-fuzz/fuzz-date.o FUZZ_OBJS += oss-fuzz/fuzz-pack-headers.o FUZZ_OBJS += oss-fuzz/fuzz-pack-idx.o .PHONY: fuzz-objs @@ -799,7 +803,6 @@ TEST_BUILTINS_OBJS += test-dump-split-index.o TEST_BUILTINS_OBJS += test-dump-untracked-cache.o TEST_BUILTINS_OBJS += test-env-helper.o TEST_BUILTINS_OBJS += test-example-decorate.o -TEST_BUILTINS_OBJS += test-fast-rebase.o TEST_BUILTINS_OBJS += test-find-pack.o TEST_BUILTINS_OBJS += test-fsmonitor-client.o TEST_BUILTINS_OBJS += test-genrandom.o @@ -1290,6 +1293,7 @@ BUILTIN_OBJS += builtin/remote-fd.o BUILTIN_OBJS += builtin/remote.o BUILTIN_OBJS += builtin/repack.o BUILTIN_OBJS += builtin/replace.o +BUILTIN_OBJS += builtin/replay.o BUILTIN_OBJS += builtin/rerere.o BUILTIN_OBJS += builtin/reset.o BUILTIN_OBJS += builtin/rev-list.o @@ -1335,6 +1339,12 @@ THIRD_PARTY_SOURCES += compat/regex/% THIRD_PARTY_SOURCES += sha1collisiondetection/% THIRD_PARTY_SOURCES += sha1dc/% +UNIT_TEST_PROGRAMS += t-basic +UNIT_TEST_PROGRAMS += t-strbuf +UNIT_TEST_PROGS = $(patsubst %,$(UNIT_TEST_BIN)/%$X,$(UNIT_TEST_PROGRAMS)) +UNIT_TEST_OBJS = $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(UNIT_TEST_PROGRAMS)) +UNIT_TEST_OBJS += $(UNIT_TEST_DIR)/test-lib.o + # xdiff and reftable libs may in turn depend on what is in libgit.a GITLIBS = common-main.o $(LIB_FILE) $(XDIFF_LIB) $(REFTABLE_LIB) $(LIB_FILE) EXTLIBS = @@ -2342,7 +2352,7 @@ profile-fast: profile-clean all:: $(ALL_COMMANDS_TO_INSTALL) $(SCRIPT_LIB) $(OTHER_PROGRAMS) GIT-BUILD-OPTIONS ifneq (,$X) - $(QUIET_BUILT_IN)$(foreach p,$(patsubst %$X,%,$(filter %$X,$(ALL_COMMANDS_TO_INSTALL) $(OTHER_PROGRAMS))), test -d '$p' -o '$p' -ef '$p$X' || $(RM) '$p';) + $(QUIET_BUILT_IN)$(foreach p,$(patsubst %$X,%,$(filter %$X,$(ALL_COMMANDS_TO_INSTALL) $(OTHER_PROGRAMS))), if test ! -d '$p' && test ! '$p' -ef '$p$X'; then $(RM) '$p'; fi;) endif all:: @@ -2676,6 +2686,7 @@ OBJECTS += $(TEST_OBJS) OBJECTS += $(XDIFF_OBJS) OBJECTS += $(FUZZ_OBJS) OBJECTS += $(REFTABLE_OBJS) $(REFTABLE_TEST_OBJS) +OBJECTS += $(UNIT_TEST_OBJS) ifndef NO_CURL OBJECTS += http.o http-walker.o remote-curl.o @@ -3178,7 +3189,7 @@ endif test_bindir_programs := $(patsubst %,bin-wrappers/%,$(BINDIR_PROGRAMS_NEED_X) $(BINDIR_PROGRAMS_NO_X) $(TEST_PROGRAMS_NEED_X)) -all:: $(TEST_PROGRAMS) $(test_bindir_programs) +all:: $(TEST_PROGRAMS) $(test_bindir_programs) $(UNIT_TEST_PROGS) bin-wrappers/%: wrap-for-bin.sh $(call mkdir_p_parent_template) @@ -3604,12 +3615,12 @@ rpm:: .PHONY: rpm ifneq ($(INCLUDE_DLLS_IN_ARTIFACTS),) -OTHER_PROGRAMS += $(shell echo *.dll t/helper/*.dll) +OTHER_PROGRAMS += $(shell echo *.dll t/helper/*.dll t/unit-tests/bin/*.dll) endif artifacts-tar:: $(ALL_COMMANDS_TO_INSTALL) $(SCRIPT_LIB) $(OTHER_PROGRAMS) \ GIT-BUILD-OPTIONS $(TEST_PROGRAMS) $(test_bindir_programs) \ - $(MOFILES) + $(UNIT_TEST_PROGS) $(MOFILES) $(QUIET_SUBDIR0)templates $(QUIET_SUBDIR1) \ SHELL_PATH='$(SHELL_PATH_SQ)' PERL_PATH='$(PERL_PATH_SQ)' test -n "$(ARTIFACTS_DIRECTORY)" @@ -3671,7 +3682,7 @@ clean: profile-clean coverage-clean cocciclean $(RM) headless-git.o $(RM) $(LIB_FILE) $(XDIFF_LIB) $(REFTABLE_LIB) $(REFTABLE_TEST_LIB) $(RM) $(ALL_PROGRAMS) $(SCRIPT_LIB) $(BUILT_INS) $(OTHER_PROGRAMS) - $(RM) $(TEST_PROGRAMS) + $(RM) $(TEST_PROGRAMS) $(UNIT_TEST_PROGS) $(RM) $(FUZZ_PROGRAMS) $(RM) $(SP_OBJ) $(RM) $(HCC) @@ -3850,3 +3861,15 @@ $(FUZZ_PROGRAMS): all $(XDIFF_OBJS) $(EXTLIBS) git.o $@.o $(LIB_FUZZING_ENGINE) -o $@ fuzz-all: $(FUZZ_PROGRAMS) + +$(UNIT_TEST_BIN): + @mkdir -p $(UNIT_TEST_BIN) + +$(UNIT_TEST_PROGS): $(UNIT_TEST_BIN)/%$X: $(UNIT_TEST_DIR)/%.o $(UNIT_TEST_DIR)/test-lib.o $(GITLIBS) GIT-LDFLAGS $(UNIT_TEST_BIN) + $(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) \ + $(filter %.o,$^) $(filter %.a,$^) $(LIBS) + +.PHONY: build-unit-tests unit-tests +build-unit-tests: $(UNIT_TEST_PROGS) +unit-tests: $(UNIT_TEST_PROGS) + $(MAKE) -C t/ unit-tests @@ -1 +1 @@ -Documentation/RelNotes/2.43.0.txt
\ No newline at end of file +Documentation/RelNotes/2.44.0.txt
\ No newline at end of file @@ -211,6 +211,7 @@ int cmd_remote(int argc, const char **argv, const char *prefix); int cmd_remote_ext(int argc, const char **argv, const char *prefix); int cmd_remote_fd(int argc, const char **argv, const char *prefix); int cmd_repack(int argc, const char **argv, const char *prefix); +int cmd_replay(int argc, const char **argv, const char *prefix); int cmd_rerere(int argc, const char **argv, const char *prefix); int cmd_reset(int argc, const char **argv, const char *prefix); int cmd_restore(int argc, const char **argv, const char *prefix); diff --git a/builtin/add.c b/builtin/add.c index 5126d2ede3..d46e4d10e9 100644 --- a/builtin/add.c +++ b/builtin/add.c @@ -424,7 +424,7 @@ int cmd_add(int argc, const char **argv, const char *prefix) * Check the "pathspec '%s' did not match any files" block * below before enabling new magic. */ - parse_pathspec(&pathspec, PATHSPEC_ATTR, + parse_pathspec(&pathspec, 0, PATHSPEC_PREFER_FULL | PATHSPEC_SYMLINK_LEADING_PATH, prefix, argv); @@ -433,7 +433,7 @@ int cmd_add(int argc, const char **argv, const char *prefix) if (pathspec.nr) die(_("'%s' and pathspec arguments cannot be used together"), "--pathspec-from-file"); - parse_pathspec_file(&pathspec, PATHSPEC_ATTR, + parse_pathspec_file(&pathspec, 0, PATHSPEC_PREFER_FULL | PATHSPEC_SYMLINK_LEADING_PATH, prefix, pathspec_from_file, pathspec_file_nul); @@ -504,7 +504,8 @@ int cmd_add(int argc, const char **argv, const char *prefix) PATHSPEC_LITERAL | PATHSPEC_GLOB | PATHSPEC_ICASE | - PATHSPEC_EXCLUDE); + PATHSPEC_EXCLUDE | + PATHSPEC_ATTR); for (i = 0; i < pathspec.nr; i++) { const char *path = pathspec.items[i].match; diff --git a/builtin/branch.c b/builtin/branch.c index e7ee9bd0f1..2ed59f16f1 100644 --- a/builtin/branch.c +++ b/builtin/branch.c @@ -45,7 +45,6 @@ static const char *head; static struct object_id head_oid; static int recurse_submodules = 0; static int submodule_propagate_branches = 0; -static int omit_empty = 0; static int branch_use_color = -1; static char branch_colors[][COLOR_MAXLEN] = { @@ -438,8 +437,6 @@ static void print_ref_list(struct ref_filter *filter, struct ref_sorting *sortin { int i; struct ref_array array; - struct strbuf out = STRBUF_INIT; - struct strbuf err = STRBUF_INIT; int maxwidth = 0; const char *remote_prefix = ""; char *to_free = NULL; @@ -469,24 +466,27 @@ static void print_ref_list(struct ref_filter *filter, struct ref_sorting *sortin filter_ahead_behind(the_repository, format, &array); ref_array_sort(sorting, &array); - for (i = 0; i < array.nr; i++) { - strbuf_reset(&err); - strbuf_reset(&out); - if (format_ref_array_item(array.items[i], format, &out, &err)) - die("%s", err.buf); - if (column_active(colopts)) { - assert(!filter->verbose && "--column and --verbose are incompatible"); - /* format to a string_list to let print_columns() do its job */ + if (column_active(colopts)) { + struct strbuf out = STRBUF_INIT, err = STRBUF_INIT; + + assert(!filter->verbose && "--column and --verbose are incompatible"); + + for (i = 0; i < array.nr; i++) { + strbuf_reset(&err); + strbuf_reset(&out); + if (format_ref_array_item(array.items[i], format, &out, &err)) + die("%s", err.buf); + + /* format to a string_list to let print_columns() do its job */ string_list_append(output, out.buf); - } else { - fwrite(out.buf, 1, out.len, stdout); - if (out.len || !omit_empty) - putchar('\n'); } + + strbuf_release(&err); + strbuf_release(&out); + } else { + print_formatted_ref_array(&array, format); } - strbuf_release(&err); - strbuf_release(&out); ref_array_clear(&array); free(to_free); } @@ -737,7 +737,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix) OPT_BIT('D', NULL, &delete, N_("delete branch (even if not merged)"), 2), OPT_BIT('m', "move", &rename, N_("move/rename a branch and its reflog"), 1), OPT_BIT('M', NULL, &rename, N_("move/rename a branch, even if target exists"), 2), - OPT_BOOL(0, "omit-empty", &omit_empty, + OPT_BOOL(0, "omit-empty", &format.array_opts.omit_empty, N_("do not output a newline after empty formatted refs")), OPT_BIT('c', "copy", ©, N_("copy a branch and its reflog"), 1), OPT_BIT('C', NULL, ©, N_("copy a branch, even if target exists"), 2), @@ -767,7 +767,13 @@ 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); + /* + * Try to set sort keys from config. If config does not set any, + * fall back on default (refname) sorting. + */ git_config(git_branch_config, &sorting_options); + if (!sorting_options.nr) + string_list_append(&sorting_options, "refname"); track = git_branch_track; diff --git a/builtin/for-each-ref.c b/builtin/for-each-ref.c index 350bfa6e81..1c19cd5bd3 100644 --- a/builtin/for-each-ref.c +++ b/builtin/for-each-ref.c @@ -19,15 +19,11 @@ 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; struct string_list sorting_options = STRING_LIST_INIT_DUP; - int maxcount = 0, icase = 0, omit_empty = 0; - struct ref_array array; + int icase = 0; struct ref_filter filter = REF_FILTER_INIT; struct ref_format format = REF_FORMAT_INIT; - struct strbuf output = STRBUF_INIT; - struct strbuf err = STRBUF_INIT; int from_stdin = 0; struct strvec vec = STRVEC_INIT; @@ -40,11 +36,11 @@ int cmd_for_each_ref(int argc, const char **argv, const char *prefix) N_("quote placeholders suitably for python"), QUOTE_PYTHON), OPT_BIT(0 , "tcl", &format.quote_style, N_("quote placeholders suitably for Tcl"), QUOTE_TCL), - OPT_BOOL(0, "omit-empty", &omit_empty, + OPT_BOOL(0, "omit-empty", &format.array_opts.omit_empty, N_("do not output a newline after empty formatted refs")), OPT_GROUP(""), - OPT_INTEGER( 0 , "count", &maxcount, N_("show only <n> matched refs")), + OPT_INTEGER( 0 , "count", &format.array_opts.max_count, 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_FILTER_EXCLUDE(&filter), @@ -61,15 +57,16 @@ int cmd_for_each_ref(int argc, const char **argv, const char *prefix) OPT_END(), }; - memset(&array, 0, sizeof(array)); - format.format = "%(objectname) %(objecttype)\t%(refname)"; git_config(git_default_config, NULL); + /* Set default (refname) sorting */ + string_list_append(&sorting_options, "refname"); + parse_options(argc, argv, prefix, opts, for_each_ref_usage, 0); - if (maxcount < 0) { - error("invalid --count argument: `%d'", maxcount); + if (format.array_opts.max_count < 0) { + error("invalid --count argument: `%d'", format.array_opts.max_count); usage_with_options(for_each_ref_usage, opts); } if (HAS_MULTI_BITS(format.quote_style)) { @@ -101,26 +98,8 @@ int cmd_for_each_ref(int argc, const char **argv, const char *prefix) } filter.match_as_path = 1; - filter_refs(&array, &filter, FILTER_REFS_ALL); - filter_ahead_behind(the_repository, &format, &array); - - ref_array_sort(sorting, &array); - - if (!maxcount || array.nr < maxcount) - maxcount = array.nr; - for (i = 0; i < maxcount; i++) { - strbuf_reset(&err); - strbuf_reset(&output); - if (format_ref_array_item(array.items[i], &format, &output, &err)) - die("%s", err.buf); - fwrite(output.buf, 1, output.len, stdout); - if (output.len || !omit_empty) - putchar('\n'); - } + filter_and_format_refs(&filter, FILTER_REFS_ALL, sorting, &format); - strbuf_release(&err); - strbuf_release(&output); - ref_array_clear(&array); ref_filter_clear(&filter); ref_sorting_release(sorting); strvec_clear(&vec); diff --git a/builtin/log.c b/builtin/log.c index ba775d7b5c..87fd1c8560 100644 --- a/builtin/log.c +++ b/builtin/log.c @@ -1364,6 +1364,7 @@ static void make_cover_letter(struct rev_info *rev, int use_separate_file, pp.date_mode.type = DATE_RFC2822; pp.rev = rev; pp.print_email_subject = 1; + pp.encode_email_headers = rev->encode_email_headers; pp_user_info(&pp, NULL, &sb, committer, encoding); prepare_cover_text(&pp, description_file, branch_name, &sb, encoding, need_8bit_cte); diff --git a/builtin/ls-remote.c b/builtin/ls-remote.c index fc76575430..b416602b4d 100644 --- a/builtin/ls-remote.c +++ b/builtin/ls-remote.c @@ -58,6 +58,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; + struct ref_sorting *sorting; struct string_list sorting_options = STRING_LIST_INIT_DUP; struct option options[] = { @@ -141,13 +142,8 @@ int cmd_ls_remote(int argc, const char **argv, const char *prefix) item->symref = xstrdup_or_null(ref->symref); } - if (sorting_options.nr) { - struct ref_sorting *sorting; - - sorting = ref_sorting_options(&sorting_options); - ref_array_sort(sorting, &ref_array); - ref_sorting_release(sorting); - } + sorting = ref_sorting_options(&sorting_options); + ref_array_sort(sorting, &ref_array); for (i = 0; i < ref_array.nr; i++) { const struct ref_array_item *ref = ref_array.items[i]; @@ -157,6 +153,7 @@ int cmd_ls_remote(int argc, const char **argv, const char *prefix) status = 0; /* we found something */ } + ref_sorting_release(sorting); ref_array_clear(&ref_array); if (transport_disconnect(transport)) status = 1; diff --git a/builtin/rebase.c b/builtin/rebase.c index 043c65dccd..9f8192e0a5 100644 --- a/builtin/rebase.c +++ b/builtin/rebase.c @@ -149,7 +149,6 @@ struct rebase_options { .reapply_cherry_picks = -1, \ .allow_empty_message = 1, \ .autosquash = -1, \ - .config_autosquash = -1, \ .rebase_merges = -1, \ .config_rebase_merges = -1, \ .update_refs = -1, \ @@ -711,10 +710,8 @@ static int run_specific_rebase(struct rebase_options *opts) if (opts->type == REBASE_MERGE) { /* Run sequencer-based rebase */ setenv("GIT_CHERRY_PICK_HELP", resolvemsg, 1); - if (!(opts->flags & REBASE_INTERACTIVE_EXPLICIT)) { + if (!(opts->flags & REBASE_INTERACTIVE_EXPLICIT)) setenv("GIT_SEQUENCE_EDITOR", ":", 1); - opts->autosquash = 0; - } if (opts->gpg_sign_opt) { /* remove the leading "-S" */ char *tmp = xstrdup(opts->gpg_sign_opt + 2); @@ -1405,7 +1402,6 @@ int cmd_rebase(int argc, const char **argv, const char *prefix) if ((options.flags & REBASE_INTERACTIVE_EXPLICIT) || (options.action != ACTION_NONE) || (options.exec.nr > 0) || - (options.autosquash == -1 && options.config_autosquash == 1) || options.autosquash == 1) { allow_preemptive_ff = 0; } @@ -1508,8 +1504,6 @@ int cmd_rebase(int argc, const char **argv, const char *prefix) if (is_merge(&options)) die(_("apply options and merge options " "cannot be used together")); - else if (options.autosquash == -1 && options.config_autosquash == 1) - die(_("apply options are incompatible with rebase.autoSquash. Consider adding --no-autosquash")); else if (options.rebase_merges == -1 && options.config_rebase_merges == 1) die(_("apply options are incompatible with rebase.rebaseMerges. Consider adding --no-rebase-merges")); else if (options.update_refs == -1 && options.config_update_refs == 1) @@ -1529,10 +1523,13 @@ int cmd_rebase(int argc, const char **argv, const char *prefix) options.rebase_merges = (options.rebase_merges >= 0) ? options.rebase_merges : ((options.config_rebase_merges >= 0) ? options.config_rebase_merges : 0); - if (options.autosquash == 1) + if (options.autosquash == 1) { imply_merge(&options, "--autosquash"); - options.autosquash = (options.autosquash >= 0) ? options.autosquash : - ((options.config_autosquash >= 0) ? options.config_autosquash : 0); + } else if (options.autosquash == -1) { + options.autosquash = + options.config_autosquash && + (options.flags & REBASE_INTERACTIVE_EXPLICIT); + } if (options.type == REBASE_UNSPECIFIED) { if (!strcmp(options.default_backend, "merge")) diff --git a/builtin/replay.c b/builtin/replay.c new file mode 100644 index 0000000000..6bc4b47f09 --- /dev/null +++ b/builtin/replay.c @@ -0,0 +1,446 @@ +/* + * "git replay" builtin command + */ + +#define USE_THE_INDEX_VARIABLE +#include "git-compat-util.h" + +#include "builtin.h" +#include "environment.h" +#include "hex.h" +#include "lockfile.h" +#include "merge-ort.h" +#include "object-name.h" +#include "parse-options.h" +#include "refs.h" +#include "revision.h" +#include "strmap.h" +#include <oidset.h> +#include <tree.h> + +static const char *short_commit_name(struct commit *commit) +{ + return repo_find_unique_abbrev(the_repository, &commit->object.oid, + DEFAULT_ABBREV); +} + +static struct commit *peel_committish(const char *name) +{ + struct object *obj; + struct object_id oid; + + if (repo_get_oid(the_repository, name, &oid)) + return NULL; + obj = parse_object(the_repository, &oid); + return (struct commit *)repo_peel_to_type(the_repository, name, 0, obj, + OBJ_COMMIT); +} + +static char *get_author(const char *message) +{ + size_t len; + const char *a; + + a = find_commit_header(message, "author", &len); + if (a) + return xmemdupz(a, len); + + return NULL; +} + +static struct commit *create_commit(struct tree *tree, + struct commit *based_on, + struct commit *parent) +{ + struct object_id ret; + struct object *obj; + struct commit_list *parents = NULL; + char *author; + char *sign_commit = NULL; /* FIXME: cli users might want to sign again */ + struct commit_extra_header *extra; + struct strbuf msg = STRBUF_INIT; + const char *out_enc = get_commit_output_encoding(); + const char *message = repo_logmsg_reencode(the_repository, based_on, + NULL, out_enc); + const char *orig_message = NULL; + const char *exclude_gpgsig[] = { "gpgsig", NULL }; + + commit_list_insert(parent, &parents); + extra = read_commit_extra_headers(based_on, exclude_gpgsig); + find_commit_subject(message, &orig_message); + strbuf_addstr(&msg, orig_message); + author = get_author(message); + reset_ident_date(); + if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents, + &ret, author, NULL, sign_commit, extra)) { + error(_("failed to write commit object")); + return NULL; + } + free(author); + strbuf_release(&msg); + + obj = parse_object(the_repository, &ret); + return (struct commit *)obj; +} + +struct ref_info { + struct commit *onto; + struct strset positive_refs; + struct strset negative_refs; + int positive_refexprs; + int negative_refexprs; +}; + +static void get_ref_information(struct rev_cmdline_info *cmd_info, + struct ref_info *ref_info) +{ + int i; + + ref_info->onto = NULL; + strset_init(&ref_info->positive_refs); + strset_init(&ref_info->negative_refs); + ref_info->positive_refexprs = 0; + ref_info->negative_refexprs = 0; + + /* + * When the user specifies e.g. + * git replay origin/main..mybranch + * git replay ^origin/next mybranch1 mybranch2 + * we want to be able to determine where to replay the commits. In + * these examples, the branches are probably based on an old version + * of either origin/main or origin/next, so we want to replay on the + * newest version of that branch. In contrast we would want to error + * out if they ran + * git replay ^origin/master ^origin/next mybranch + * git replay mybranch~2..mybranch + * the first of those because there's no unique base to choose, and + * the second because they'd likely just be replaying commits on top + * of the same commit and not making any difference. + */ + for (i = 0; i < cmd_info->nr; i++) { + struct rev_cmdline_entry *e = cmd_info->rev + i; + struct object_id oid; + const char *refexpr = e->name; + char *fullname = NULL; + int can_uniquely_dwim = 1; + + if (*refexpr == '^') + refexpr++; + if (repo_dwim_ref(the_repository, refexpr, strlen(refexpr), &oid, &fullname, 0) != 1) + can_uniquely_dwim = 0; + + if (e->flags & BOTTOM) { + if (can_uniquely_dwim) + strset_add(&ref_info->negative_refs, fullname); + if (!ref_info->negative_refexprs) + ref_info->onto = lookup_commit_reference_gently(the_repository, + &e->item->oid, 1); + ref_info->negative_refexprs++; + } else { + if (can_uniquely_dwim) + strset_add(&ref_info->positive_refs, fullname); + ref_info->positive_refexprs++; + } + + free(fullname); + } +} + +static void determine_replay_mode(struct rev_cmdline_info *cmd_info, + const char *onto_name, + const char **advance_name, + struct commit **onto, + struct strset **update_refs) +{ + struct ref_info rinfo; + + get_ref_information(cmd_info, &rinfo); + if (!rinfo.positive_refexprs) + die(_("need some commits to replay")); + if (onto_name && *advance_name) + die(_("--onto and --advance are incompatible")); + else if (onto_name) { + *onto = peel_committish(onto_name); + if (rinfo.positive_refexprs < + strset_get_size(&rinfo.positive_refs)) + die(_("all positive revisions given must be references")); + } else if (*advance_name) { + struct object_id oid; + char *fullname = NULL; + + *onto = peel_committish(*advance_name); + if (repo_dwim_ref(the_repository, *advance_name, strlen(*advance_name), + &oid, &fullname, 0) == 1) { + *advance_name = fullname; + } else { + die(_("argument to --advance must be a reference")); + } + if (rinfo.positive_refexprs > 1) + die(_("cannot advance target with multiple sources because ordering would be ill-defined")); + } else { + int positive_refs_complete = ( + rinfo.positive_refexprs == + strset_get_size(&rinfo.positive_refs)); + int negative_refs_complete = ( + rinfo.negative_refexprs == + strset_get_size(&rinfo.negative_refs)); + /* + * We need either positive_refs_complete or + * negative_refs_complete, but not both. + */ + if (rinfo.negative_refexprs > 0 && + positive_refs_complete == negative_refs_complete) + die(_("cannot implicitly determine whether this is an --advance or --onto operation")); + if (negative_refs_complete) { + struct hashmap_iter iter; + struct strmap_entry *entry; + + if (rinfo.negative_refexprs == 0) + die(_("all positive revisions given must be references")); + else if (rinfo.negative_refexprs > 1) + die(_("cannot implicitly determine whether this is an --advance or --onto operation")); + else if (rinfo.positive_refexprs > 1) + die(_("cannot advance target with multiple source branches because ordering would be ill-defined")); + + /* Only one entry, but we have to loop to get it */ + strset_for_each_entry(&rinfo.negative_refs, + &iter, entry) { + *advance_name = entry->key; + } + } else { /* positive_refs_complete */ + if (rinfo.negative_refexprs > 1) + die(_("cannot implicitly determine correct base for --onto")); + if (rinfo.negative_refexprs == 1) + *onto = rinfo.onto; + } + } + if (!*advance_name) { + *update_refs = xcalloc(1, sizeof(**update_refs)); + **update_refs = rinfo.positive_refs; + memset(&rinfo.positive_refs, 0, sizeof(**update_refs)); + } + strset_clear(&rinfo.negative_refs); + strset_clear(&rinfo.positive_refs); +} + +static struct commit *mapped_commit(kh_oid_map_t *replayed_commits, + struct commit *commit, + struct commit *fallback) +{ + khint_t pos = kh_get_oid_map(replayed_commits, commit->object.oid); + if (pos == kh_end(replayed_commits)) + return fallback; + return kh_value(replayed_commits, pos); +} + +static struct commit *pick_regular_commit(struct commit *pickme, + kh_oid_map_t *replayed_commits, + struct commit *onto, + struct merge_options *merge_opt, + struct merge_result *result) +{ + struct commit *base, *replayed_base; + struct tree *pickme_tree, *base_tree; + + base = pickme->parents->item; + replayed_base = mapped_commit(replayed_commits, base, onto); + + result->tree = repo_get_commit_tree(the_repository, replayed_base); + pickme_tree = repo_get_commit_tree(the_repository, pickme); + base_tree = repo_get_commit_tree(the_repository, base); + + merge_opt->branch1 = short_commit_name(replayed_base); + merge_opt->branch2 = short_commit_name(pickme); + merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2); + + merge_incore_nonrecursive(merge_opt, + base_tree, + result->tree, + pickme_tree, + result); + + free((char*)merge_opt->ancestor); + merge_opt->ancestor = NULL; + if (!result->clean) + return NULL; + return create_commit(result->tree, pickme, replayed_base); +} + +int cmd_replay(int argc, const char **argv, const char *prefix) +{ + const char *advance_name = NULL; + struct commit *onto = NULL; + const char *onto_name = NULL; + int contained = 0; + + struct rev_info revs; + struct commit *last_commit = NULL; + struct commit *commit; + struct merge_options merge_opt; + struct merge_result result; + struct strset *update_refs = NULL; + kh_oid_map_t *replayed_commits; + int ret = 0; + + const char * const replay_usage[] = { + N_("(EXPERIMENTAL!) git replay " + "([--contained] --onto <newbase> | --advance <branch>) " + "<revision-range>..."), + NULL + }; + struct option replay_options[] = { + OPT_STRING(0, "advance", &advance_name, + N_("branch"), + N_("make replay advance given branch")), + OPT_STRING(0, "onto", &onto_name, + N_("revision"), + N_("replay onto given commit")), + OPT_BOOL(0, "contained", &contained, + N_("advance all branches contained in revision-range")), + OPT_END() + }; + + argc = parse_options(argc, argv, prefix, replay_options, replay_usage, + PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT); + + if (!onto_name && !advance_name) { + error(_("option --onto or --advance is mandatory")); + usage_with_options(replay_usage, replay_options); + } + + if (advance_name && contained) + die(_("options '%s' and '%s' cannot be used together"), + "--advance", "--contained"); + + repo_init_revisions(the_repository, &revs, prefix); + + /* + * Set desired values for rev walking options here. If they + * are changed by some user specified option in setup_revisions() + * below, we will detect that below and then warn. + * + * TODO: In the future we might want to either die(), or allow + * some options changing these values if we think they could + * be useful. + */ + revs.reverse = 1; + revs.sort_order = REV_SORT_IN_GRAPH_ORDER; + revs.topo_order = 1; + revs.simplify_history = 0; + + argc = setup_revisions(argc, argv, &revs, NULL); + if (argc > 1) { + ret = error(_("unrecognized argument: %s"), argv[1]); + goto cleanup; + } + + /* + * Detect and warn if we override some user specified rev + * walking options. + */ + if (revs.reverse != 1) { + warning(_("some rev walking options will be overridden as " + "'%s' bit in 'struct rev_info' will be forced"), + "reverse"); + revs.reverse = 1; + } + if (revs.sort_order != REV_SORT_IN_GRAPH_ORDER) { + warning(_("some rev walking options will be overridden as " + "'%s' bit in 'struct rev_info' will be forced"), + "sort_order"); + revs.sort_order = REV_SORT_IN_GRAPH_ORDER; + } + if (revs.topo_order != 1) { + warning(_("some rev walking options will be overridden as " + "'%s' bit in 'struct rev_info' will be forced"), + "topo_order"); + revs.topo_order = 1; + } + if (revs.simplify_history != 0) { + warning(_("some rev walking options will be overridden as " + "'%s' bit in 'struct rev_info' will be forced"), + "simplify_history"); + revs.simplify_history = 0; + } + + determine_replay_mode(&revs.cmdline, onto_name, &advance_name, + &onto, &update_refs); + + if (!onto) /* FIXME: Should handle replaying down to root commit */ + die("Replaying down to root commit is not supported yet!"); + + if (prepare_revision_walk(&revs) < 0) { + ret = error(_("error preparing revisions")); + goto cleanup; + } + + init_merge_options(&merge_opt, the_repository); + memset(&result, 0, sizeof(result)); + merge_opt.show_rename_progress = 0; + last_commit = onto; + replayed_commits = kh_init_oid_map(); + while ((commit = get_revision(&revs))) { + const struct name_decoration *decoration; + khint_t pos; + int hr; + + if (!commit->parents) + die(_("replaying down to root commit is not supported yet!")); + if (commit->parents->next) + die(_("replaying merge commits is not supported yet!")); + + last_commit = pick_regular_commit(commit, replayed_commits, onto, + &merge_opt, &result); + if (!last_commit) + break; + + /* Record commit -> last_commit mapping */ + pos = kh_put_oid_map(replayed_commits, commit->object.oid, &hr); + if (hr == 0) + BUG("Duplicate rewritten commit: %s\n", + oid_to_hex(&commit->object.oid)); + kh_value(replayed_commits, pos) = last_commit; + + /* Update any necessary branches */ + if (advance_name) + continue; + decoration = get_name_decoration(&commit->object); + if (!decoration) + continue; + while (decoration) { + if (decoration->type == DECORATION_REF_LOCAL && + (contained || strset_contains(update_refs, + decoration->name))) { + printf("update %s %s %s\n", + decoration->name, + oid_to_hex(&last_commit->object.oid), + oid_to_hex(&commit->object.oid)); + } + decoration = decoration->next; + } + } + + /* In --advance mode, advance the target ref */ + if (result.clean == 1 && advance_name) { + printf("update %s %s %s\n", + advance_name, + oid_to_hex(&last_commit->object.oid), + oid_to_hex(&onto->object.oid)); + } + + merge_finalize(&merge_opt, &result); + kh_destroy_oid_map(replayed_commits); + if (update_refs) { + strset_clear(update_refs); + free(update_refs); + } + ret = result.clean; + +cleanup: + release_revisions(&revs); + + /* Return */ + if (ret < 0) + exit(128); + return ret ? 0 : 1; +} diff --git a/builtin/tag.c b/builtin/tag.c index 3918eacbb5..2528d499dd 100644 --- a/builtin/tag.c +++ b/builtin/tag.c @@ -44,18 +44,11 @@ static const char * const git_tag_usage[] = { static unsigned int colopts; static int force_sign_annotate; static int config_sign_tag = -1; /* unspecified */ -static int omit_empty = 0; static int list_tags(struct ref_filter *filter, struct ref_sorting *sorting, struct ref_format *format) { - struct ref_array array; - struct strbuf output = STRBUF_INIT; - struct strbuf err = STRBUF_INIT; char *to_free = NULL; - int i; - - memset(&array, 0, sizeof(array)); if (filter->lines == -1) filter->lines = 0; @@ -73,23 +66,8 @@ static int list_tags(struct ref_filter *filter, struct ref_sorting *sorting, if (verify_ref_format(format)) die(_("unable to parse format string")); filter->with_commit_tag_algo = 1; - filter_refs(&array, filter, FILTER_REFS_TAGS); - filter_ahead_behind(the_repository, format, &array); - ref_array_sort(sorting, &array); - - for (i = 0; i < array.nr; i++) { - strbuf_reset(&output); - strbuf_reset(&err); - if (format_ref_array_item(array.items[i], format, &output, &err)) - die("%s", err.buf); - fwrite(output.buf, 1, output.len, stdout); - if (output.len || !omit_empty) - putchar('\n'); - } + filter_and_format_refs(filter, FILTER_REFS_TAGS, sorting, format); - strbuf_release(&err); - strbuf_release(&output); - ref_array_clear(&array); free(to_free); return 0; @@ -481,7 +459,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_BOOL(0, "omit-empty", &omit_empty, + OPT_BOOL(0, "omit-empty", &format.array_opts.omit_empty, N_("do not output a newline after empty formatted refs")), OPT_REF_SORT(&sorting_options), { @@ -501,7 +479,13 @@ int cmd_tag(int argc, const char **argv, const char *prefix) setup_ref_filter_porcelain_msg(); + /* + * Try to set sort keys from config. If config does not set any, + * fall back on default (refname) sorting. + */ git_config(git_tag_config, &sorting_options); + if (!sorting_options.nr) + string_list_append(&sorting_options, "refname"); memset(&opt, 0, sizeof(opt)); filter.lines = -1; diff --git a/ci/install-docker-dependencies.sh b/ci/install-docker-dependencies.sh index 78b7e326da..48c43f0f90 100755 --- a/ci/install-docker-dependencies.sh +++ b/ci/install-docker-dependencies.sh @@ -3,6 +3,10 @@ # Install dependencies required to build and test Git inside container # +. ${0%/*}/lib.sh + +begin_group "Install dependencies" + case "$jobname" in linux32) linux32 --32bit i386 sh -c ' @@ -12,11 +16,26 @@ linux32) ' ;; linux-musl) - apk add --update build-base curl-dev openssl-dev expat-dev gettext \ - pcre2-dev python3 musl-libintl perl-utils ncurses >/dev/null + apk add --update shadow sudo build-base curl-dev openssl-dev expat-dev gettext \ + pcre2-dev python3 musl-libintl perl-utils ncurses \ + apache2 apache2-http2 apache2-proxy apache2-ssl apache2-webdav apr-util-dbd_sqlite3 \ + bash cvs gnupg perl-cgi perl-dbd-sqlite >/dev/null + ;; +linux-*) + # Required so that apt doesn't wait for user input on certain packages. + export DEBIAN_FRONTEND=noninteractive + + apt update -q && + apt install -q -y sudo git make language-pack-is libsvn-perl apache2 libssl-dev \ + libcurl4-openssl-dev libexpat-dev tcl tk gettext zlib1g-dev \ + perl-modules liberror-perl libauthen-sasl-perl libemail-valid-perl \ + libdbd-sqlite3-perl libio-socket-ssl-perl libnet-smtp-ssl-perl ${CC_PACKAGE:-${CC:-gcc}} \ + apache2 cvs cvsps gnupg libcgi-pm-perl subversion ;; pedantic) dnf -yq update >/dev/null && dnf -yq install make gcc findutils diffutils perl python3 gettext zlib-devel expat-devel openssl-devel curl-devel pcre2-devel >/dev/null ;; esac + +end_group "Install dependencies" @@ -1,16 +1,7 @@ # Library of functions shared by all CI scripts -if test true != "$GITHUB_ACTIONS" +if test true = "$GITHUB_ACTIONS" then - begin_group () { :; } - end_group () { :; } - - group () { - shift - "$@" - } - set -x -else begin_group () { need_to_end_group=t echo "::group::$1" >&2 @@ -23,27 +14,50 @@ else need_to_end_group= echo '::endgroup::' >&2 } - trap end_group EXIT +elif test true = "$GITLAB_CI" +then + begin_group () { + need_to_end_group=t + printf "\e[0Ksection_start:$(date +%s):$(echo "$1" | tr ' ' _)\r\e[0K$1\n" + trap "end_group '$1'" EXIT + set -x + } - group () { + end_group () { + test -n "$need_to_end_group" || return 0 set +x - begin_group "$1" - shift - # work around `dash` not supporting `set -o pipefail` - ( - "$@" 2>&1 - echo $? >exit.status - ) | - sed 's/^\(\([^ ]*\):\([0-9]*\):\([0-9]*:\) \)\(error\|warning\): /::\5 file=\2,line=\3::\1/' - res=$(cat exit.status) - rm exit.status - end_group - return $res + need_to_end_group= + printf "\e[0Ksection_end:$(date +%s):$(echo "$1" | tr ' ' _)\r\e[0K\n" + trap - EXIT } +else + begin_group () { :; } + end_group () { :; } - begin_group "CI setup" + set -x fi +group () { + group="$1" + shift + begin_group "$group" + + # work around `dash` not supporting `set -o pipefail` + ( + "$@" 2>&1 + echo $? >exit.status + ) | + sed 's/^\(\([^ ]*\):\([0-9]*\):\([0-9]*:\) \)\(error\|warning\): /::\5 file=\2,line=\3::\1/' + res=$(cat exit.status) + rm exit.status + + end_group "$group" + return $res +} + +begin_group "CI setup" +trap "end_group 'CI setup'" EXIT + # Set 'exit on error' for all CI scripts to let the caller know that # something went wrong. # @@ -71,10 +85,32 @@ skip_branch_tip_with_tag () { fi } +# Check whether we can use the path passed via the first argument as Git +# repository. +is_usable_git_repository () { + # We require Git in our PATH, otherwise we cannot access repositories + # at all. + if ! command -v git >/dev/null + then + return 1 + fi + + # And the target directory needs to be a proper Git repository. + if ! git -C "$1" rev-parse 2>/dev/null + then + return 1 + fi +} + # Save some info about the current commit's tree, so we can skip the build # job if we encounter the same tree again and can provide a useful info # message. save_good_tree () { + if ! is_usable_git_repository . + then + return + fi + echo "$(git rev-parse $CI_COMMIT^{tree}) $CI_COMMIT $CI_JOB_NUMBER $CI_JOB_ID" >>"$good_trees_file" # limit the file size tail -1000 "$good_trees_file" >"$good_trees_file".tmp @@ -90,6 +126,11 @@ skip_good_tree () { return fi + if ! is_usable_git_repository . + then + return + fi + if ! good_tree_info="$(grep "^$(git rev-parse $CI_COMMIT^{tree}) " "$good_trees_file")" then # Haven't seen this tree yet, or no cached good trees file yet. @@ -121,6 +162,11 @@ skip_good_tree () { } check_unignored_build_artifacts () { + if ! is_usable_git_repository . + then + return + fi + ! git ls-files --other --exclude-standard --error-unmatch \ -- ':/*' 2>/dev/null || { @@ -133,6 +179,26 @@ handle_failed_tests () { return 1 } +create_failed_test_artifacts () { + mkdir -p t/failed-test-artifacts + + for test_exit in t/test-results/*.exit + do + test 0 != "$(cat "$test_exit")" || continue + + test_name="${test_exit%.exit}" + test_name="${test_name##*/}" + printf "\\e[33m\\e[1m=== Failed test: ${test_name} ===\\e[m\\n" + echo "The full logs are in the 'print test failures' step below." + echo "See also the 'failed-tests-*' artifacts attached to this run." + cat "t/test-results/$test_name.markup" + + trash_dir="t/trash directory.$test_name" + cp "t/test-results/$test_name.out" t/failed-test-artifacts/ + tar czf t/failed-test-artifacts/"$test_name".trash.tar.gz "$trash_dir" + done +} + # GitHub Action doesn't set TERM, which is required by tput export TERM=${TERM:-dumb} @@ -156,11 +222,8 @@ then # among *all* phases) cache_dir="$HOME/test-cache/$SYSTEM_PHASENAME" - 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" - test windows_nt != "$CI_OS_NAME" || - GIT_TEST_OPTS="--no-chain-lint --no-bin-wrappers $GIT_TEST_OPTS" + GIT_TEST_OPTS="--write-junit-xml" + JOBS=10 elif test true = "$GITHUB_ACTIONS" then CI_TYPE=github-actions @@ -173,40 +236,63 @@ then CC="${CC_PACKAGE:-${CC:-gcc}}" DONT_SKIP_TAGS=t handle_failed_tests () { - mkdir -p t/failed-test-artifacts echo "FAILED_TEST_ARTIFACTS=t/failed-test-artifacts" >>$GITHUB_ENV + create_failed_test_artifacts + return 1 + } - for test_exit in t/test-results/*.exit - do - test 0 != "$(cat "$test_exit")" || continue - - test_name="${test_exit%.exit}" - test_name="${test_name##*/}" - printf "\\e[33m\\e[1m=== Failed test: ${test_name} ===\\e[m\\n" - echo "The full logs are in the 'print test failures' step below." - echo "See also the 'failed-tests-*' artifacts attached to this run." - cat "t/test-results/$test_name.markup" - - trash_dir="t/trash directory.$test_name" - cp "t/test-results/$test_name.out" t/failed-test-artifacts/ - tar czf t/failed-test-artifacts/"$test_name".trash.tar.gz "$trash_dir" - done + cache_dir="$HOME/none" + + GIT_TEST_OPTS="--github-workflow-markup" + JOBS=10 +elif test true = "$GITLAB_CI" +then + CI_TYPE=gitlab-ci + CI_BRANCH="$CI_COMMIT_REF_NAME" + CI_COMMIT="$CI_COMMIT_SHA" + case "$CI_JOB_IMAGE" in + macos-*) + CI_OS_NAME=osx;; + alpine:*|fedora:*|ubuntu:*) + CI_OS_NAME=linux;; + *) + echo "Could not identify OS image" >&2 + env >&2 + exit 1 + ;; + esac + CI_REPO_SLUG="$CI_PROJECT_PATH" + CI_JOB_ID="$CI_JOB_ID" + CC="${CC_PACKAGE:-${CC:-gcc}}" + DONT_SKIP_TAGS=t + handle_failed_tests () { + create_failed_test_artifacts return 1 } cache_dir="$HOME/none" - export GIT_PROVE_OPTS="--timer --jobs 10" - export GIT_TEST_OPTS="--verbose-log -x --github-workflow-markup" - MAKEFLAGS="$MAKEFLAGS --jobs=10" - test windows != "$CI_OS_NAME" || - GIT_TEST_OPTS="--no-chain-lint --no-bin-wrappers $GIT_TEST_OPTS" + runs_on_pool=$(echo "$CI_JOB_IMAGE" | tr : -) + JOBS=$(nproc) else echo "Could not identify CI type" >&2 env >&2 exit 1 fi +MAKEFLAGS="$MAKEFLAGS --jobs=$JOBS" +GIT_PROVE_OPTS="--timer --jobs $JOBS" + +GIT_TEST_OPTS="$GIT_TEST_OPTS --verbose-log -x" +case "$CI_OS_NAME" in +windows|windows_nt) + GIT_TEST_OPTS="$GIT_TEST_OPTS --no-chain-lint --no-bin-wrappers" + ;; +esac + +export GIT_TEST_OPTS +export GIT_PROVE_OPTS + good_trees_file="$cache_dir/good-trees" mkdir -p "$cache_dir" @@ -285,5 +371,5 @@ esac MAKEFLAGS="$MAKEFLAGS CC=${CC:-cc}" -end_group +end_group "CI setup" set -x diff --git a/ci/print-test-failures.sh b/ci/print-test-failures.sh index 57277eefcd..c33ad4e3a2 100755 --- a/ci/print-test-failures.sh +++ b/ci/print-test-failures.sh @@ -51,6 +51,12 @@ do tar czf failed-test-artifacts/"$test_name".trash.tar.gz "$trash_dir" continue ;; + gitlab-ci) + mkdir -p failed-test-artifacts + cp "${TEST_EXIT%.exit}.out" failed-test-artifacts/ + tar czf failed-test-artifacts/"$test_name".trash.tar.gz "$trash_dir" + continue + ;; *) echo "Unhandled CI type: $CI_TYPE" >&2 exit 1 diff --git a/ci/run-build-and-tests.sh b/ci/run-build-and-tests.sh index 2528f25e31..7a1466b868 100755 --- a/ci/run-build-and-tests.sh +++ b/ci/run-build-and-tests.sh @@ -50,6 +50,8 @@ if test -n "$run_tests" then group "Run tests" make test || handle_failed_tests + group "Run unit tests" \ + make DEFAULT_UNIT_TEST_TARGET=unit-tests-prove unit-tests fi check_unignored_build_artifacts diff --git a/ci/run-test-slice.sh b/ci/run-test-slice.sh index a3c67956a8..ae8094382f 100755 --- a/ci/run-test-slice.sh +++ b/ci/run-test-slice.sh @@ -15,4 +15,9 @@ group "Run tests" make --quiet -C t T="$(cd t && tr '\n' ' ')" || handle_failed_tests +# We only have one unit test at the moment, so run it in the first slice +if [ "$1" == "0" ] ; then + group "Run unit tests" make --quiet -C t unit-tests-prove +fi + check_unignored_build_artifacts diff --git a/command-list.txt b/command-list.txt index 54b2a50f5f..c4cd0f352b 100644 --- a/command-list.txt +++ b/command-list.txt @@ -160,6 +160,7 @@ git-reflog ancillarymanipulators complete git-remote ancillarymanipulators complete git-repack ancillarymanipulators complete git-replace ancillarymanipulators complete +git-replay plumbingmanipulators git-request-pull foreignscminterface complete git-rerere ancillaryinterrogators git-reset mainporcelain history diff --git a/commit-graph.c b/commit-graph.c index a712917356..6fad9d195d 100644 --- a/commit-graph.c +++ b/commit-graph.c @@ -275,59 +275,27 @@ struct commit_graph *load_commit_graph_one_fd_st(struct repository *r, return ret; } -static int verify_commit_graph_lite(struct commit_graph *g) +static int graph_read_oid_fanout(const unsigned char *chunk_start, + size_t chunk_size, void *data) { + struct commit_graph *g = data; int i; - /* - * Basic validation shared between parse_commit_graph() - * which'll be called every time the graph is used, and the - * much more expensive verify_commit_graph() used by - * "commit-graph verify". - * - * There should only be very basic checks here to ensure that - * we don't e.g. segfault in fill_commit_in_graph(), but - * because this is a very hot codepath nothing that e.g. loops - * over g->num_commits, or runs a checksum on the commit-graph - * itself. - */ - if (!g->chunk_oid_fanout) { - error("commit-graph is missing the OID Fanout chunk"); - return 1; - } - if (!g->chunk_oid_lookup) { - error("commit-graph is missing the OID Lookup chunk"); - return 1; - } - if (!g->chunk_commit_data) { - error("commit-graph is missing the Commit Data chunk"); - return 1; - } + if (chunk_size != 256 * sizeof(uint32_t)) + return error(_("commit-graph oid fanout chunk is wrong size")); + g->chunk_oid_fanout = (const uint32_t *)chunk_start; + g->num_commits = ntohl(g->chunk_oid_fanout[255]); for (i = 0; i < 255; i++) { uint32_t oid_fanout1 = ntohl(g->chunk_oid_fanout[i]); uint32_t oid_fanout2 = ntohl(g->chunk_oid_fanout[i + 1]); if (oid_fanout1 > oid_fanout2) { - error("commit-graph fanout values out of order"); + error(_("commit-graph fanout values out of order")); return 1; } } - if (ntohl(g->chunk_oid_fanout[255]) != g->num_commits) { - error("commit-graph oid table and fanout disagree on size"); - return 1; - } - - return 0; -} -static int graph_read_oid_fanout(const unsigned char *chunk_start, - size_t chunk_size, void *data) -{ - struct commit_graph *g = data; - if (chunk_size != 256 * sizeof(uint32_t)) - return error("commit-graph oid fanout chunk is wrong size"); - g->chunk_oid_fanout = (const uint32_t *)chunk_start; return 0; } @@ -336,7 +304,8 @@ static int graph_read_oid_lookup(const unsigned char *chunk_start, { struct commit_graph *g = data; g->chunk_oid_lookup = chunk_start; - g->num_commits = chunk_size / g->hash_len; + if (chunk_size / g->hash_len != g->num_commits) + return error(_("commit-graph OID lookup chunk is the wrong size")); return 0; } @@ -344,8 +313,8 @@ static int graph_read_commit_data(const unsigned char *chunk_start, size_t chunk_size, void *data) { struct commit_graph *g = data; - if (chunk_size != g->num_commits * GRAPH_DATA_WIDTH) - return error("commit-graph commit data chunk is wrong size"); + if (chunk_size / GRAPH_DATA_WIDTH != g->num_commits) + return error(_("commit-graph commit data chunk is wrong size")); g->chunk_commit_data = chunk_start; return 0; } @@ -354,8 +323,8 @@ static int graph_read_generation_data(const unsigned char *chunk_start, size_t chunk_size, void *data) { struct commit_graph *g = data; - if (chunk_size != g->num_commits * sizeof(uint32_t)) - return error("commit-graph generations chunk is wrong size"); + if (chunk_size / sizeof(uint32_t) != g->num_commits) + return error(_("commit-graph generations chunk is wrong size")); g->chunk_generation_data = chunk_start; return 0; } @@ -364,8 +333,8 @@ static int graph_read_bloom_index(const unsigned char *chunk_start, size_t chunk_size, void *data) { struct commit_graph *g = data; - if (chunk_size != g->num_commits * 4) { - warning("commit-graph changed-path index chunk is too small"); + if (chunk_size / 4 != g->num_commits) { + warning(_("commit-graph changed-path index chunk is too small")); return -1; } g->chunk_bloom_indexes = chunk_start; @@ -379,8 +348,8 @@ static int graph_read_bloom_data(const unsigned char *chunk_start, uint32_t hash_version; if (chunk_size < BLOOMDATA_CHUNK_HEADER_SIZE) { - warning("ignoring too-small changed-path chunk" - " (%"PRIuMAX" < %"PRIuMAX") in commit-graph file", + warning(_("ignoring too-small changed-path chunk" + " (%"PRIuMAX" < %"PRIuMAX") in commit-graph file"), (uintmax_t)chunk_size, (uintmax_t)BLOOMDATA_CHUNK_HEADER_SIZE); return -1; @@ -462,9 +431,19 @@ struct commit_graph *parse_commit_graph(struct repo_settings *s, GRAPH_HEADER_SIZE, graph->num_chunks, 1)) goto free_and_return; - read_chunk(cf, GRAPH_CHUNKID_OIDFANOUT, graph_read_oid_fanout, graph); - read_chunk(cf, GRAPH_CHUNKID_OIDLOOKUP, graph_read_oid_lookup, graph); - read_chunk(cf, GRAPH_CHUNKID_DATA, graph_read_commit_data, graph); + if (read_chunk(cf, GRAPH_CHUNKID_OIDFANOUT, graph_read_oid_fanout, graph)) { + error(_("commit-graph required OID fanout chunk missing or corrupted")); + goto free_and_return; + } + if (read_chunk(cf, GRAPH_CHUNKID_OIDLOOKUP, graph_read_oid_lookup, graph)) { + error(_("commit-graph required OID lookup chunk missing or corrupted")); + goto free_and_return; + } + if (read_chunk(cf, GRAPH_CHUNKID_DATA, graph_read_commit_data, graph)) { + error(_("commit-graph required commit data chunk missing or corrupted")); + goto free_and_return; + } + pair_chunk(cf, GRAPH_CHUNKID_EXTRAEDGES, &graph->chunk_extra_edges, &graph->chunk_extra_edges_size); pair_chunk(cf, GRAPH_CHUNKID_BASE, &graph->chunk_base_graphs, @@ -499,9 +478,6 @@ struct commit_graph *parse_commit_graph(struct repo_settings *s, oidread(&graph->oid, graph->data + graph->data_len - graph->hash_len); - if (verify_commit_graph_lite(graph)) - goto free_and_return; - free_chunkfile(cf); return graph; @@ -629,7 +605,7 @@ int open_commit_graph_chain(const char *chain_file, /* treat empty files the same as missing */ errno = ENOENT; } else { - warning("commit-graph chain file too small"); + warning(_("commit-graph chain file too small")); errno = EINVAL; } return 0; @@ -970,7 +946,7 @@ static int fill_commit_in_graph(struct repository *r, parent_data_pos = edge_value & GRAPH_EDGE_LAST_MASK; do { if (g->chunk_extra_edges_size / sizeof(uint32_t) <= parent_data_pos) { - error("commit-graph extra-edges pointer out of bounds"); + error(_("commit-graph extra-edges pointer out of bounds")); free_commit_list(item->parents); item->parents = NULL; item->object.parsed = 0; @@ -2690,10 +2666,6 @@ static int verify_one_commit_graph(struct repository *r, struct commit *seen_gen_zero = NULL; struct commit *seen_gen_non_zero = NULL; - verify_commit_graph_error = verify_commit_graph_lite(g); - if (verify_commit_graph_error) - return verify_commit_graph_error; - if (!commit_graph_checksum_valid(g)) { graph_report(_("the commit-graph file has incorrect checksum and is likely corrupt")); verify_commit_graph_error = VERIFY_COMMIT_GRAPH_ERROR_HASH; diff --git a/configure.ac b/configure.ac index 276593cd9d..d1a96da14e 100644 --- a/configure.ac +++ b/configure.ac @@ -94,7 +94,7 @@ AC_DEFUN([GIT_PARSE_WITH_SET_MAKE_VAR], [AC_ARG_WITH([$1], [AS_HELP_STRING([--with-$1=VALUE], $3)], if test -n "$withval"; then - if test "$withval" = "yes" -o "$withval" = "no"; then + if test "$withval" = "yes" || test "$withval" = "no"; then AC_MSG_WARN([You likely do not want either 'yes' or 'no' as] [a value for $1 ($2). Maybe you do...?]) fi diff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt index 6b819e2fbd..804629c525 100644 --- a/contrib/buildsystems/CMakeLists.txt +++ b/contrib/buildsystems/CMakeLists.txt @@ -974,6 +974,35 @@ target_link_libraries(test-fake-ssh common-main) parse_makefile_for_sources(test-reftable_SOURCES "REFTABLE_TEST_OBJS") list(TRANSFORM test-reftable_SOURCES PREPEND "${CMAKE_SOURCE_DIR}/") +#unit-tests +add_library(unit-test-lib OBJECT ${CMAKE_SOURCE_DIR}/t/unit-tests/test-lib.c) + +parse_makefile_for_scripts(unit_test_PROGRAMS "UNIT_TEST_PROGRAMS" "") +foreach(unit_test ${unit_test_PROGRAMS}) + add_executable("${unit_test}" "${CMAKE_SOURCE_DIR}/t/unit-tests/${unit_test}.c") + target_link_libraries("${unit_test}" unit-test-lib common-main) + set_target_properties("${unit_test}" + PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/t/unit-tests/bin) + if(MSVC) + set_target_properties("${unit_test}" + PROPERTIES RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_BINARY_DIR}/t/unit-tests/bin) + set_target_properties("${unit_test}" + PROPERTIES RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_BINARY_DIR}/t/unit-tests/bin) + endif() + list(APPEND PROGRAMS_BUILT "${unit_test}") + + # t-basic intentionally fails tests, to validate the unit-test infrastructure. + # Therefore, it should only be run as part of t0080, which verifies that it + # fails only in the expected ways. + # + # All other unit tests should be run. + if(NOT ${unit_test} STREQUAL "t-basic") + add_test(NAME "t.unit-tests.${unit_test}" + COMMAND "./${unit_test}" + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/t/unit-tests/bin) + endif() +endforeach() + #test-tool parse_makefile_for_sources(test-tool_SOURCES "TEST_BUILTINS_OBJS") @@ -1093,17 +1122,18 @@ if(NOT ${CMAKE_BINARY_DIR}/CMakeCache.txt STREQUAL ${CACHE_PATH}) file(COPY ${CMAKE_SOURCE_DIR}/contrib/completion/git-completion.bash DESTINATION ${CMAKE_BINARY_DIR}/contrib/completion/) endif() -file(GLOB test_scipts "${CMAKE_SOURCE_DIR}/t/t[0-9]*.sh") +file(GLOB test_scripts "${CMAKE_SOURCE_DIR}/t/t[0-9]*.sh") #test -foreach(tsh ${test_scipts}) - add_test(NAME ${tsh} +foreach(tsh ${test_scripts}) + string(REGEX REPLACE ".*/(.*)\\.sh" "\\1" test_name ${tsh}) + add_test(NAME "t.suite.${test_name}" COMMAND ${SH_EXE} ${tsh} --no-bin-wrappers --no-chain-lint -vx WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/t) endforeach() # This test script takes an extremely long time and is known to time out even # on fast machines because it requires in excess of one hour to run -set_tests_properties("${CMAKE_SOURCE_DIR}/t/t7112-reset-submodule.sh" PROPERTIES TIMEOUT 4000) +set_tests_properties("t.suite.t7112-reset-submodule" PROPERTIES TIMEOUT 4000) endif()#BUILD_TESTING diff --git a/contrib/diff-highlight/DiffHighlight.pm b/contrib/diff-highlight/DiffHighlight.pm index 376f577737..636add6968 100644 --- a/contrib/diff-highlight/DiffHighlight.pm +++ b/contrib/diff-highlight/DiffHighlight.pm @@ -1,6 +1,6 @@ package DiffHighlight; -use 5.008; +use 5.008001; use warnings FATAL => 'all'; use strict; diff --git a/contrib/mw-to-git/Git/Mediawiki.pm b/contrib/mw-to-git/Git/Mediawiki.pm index 917d9e2d32..ff7811225e 100644 --- a/contrib/mw-to-git/Git/Mediawiki.pm +++ b/contrib/mw-to-git/Git/Mediawiki.pm @@ -1,6 +1,6 @@ package Git::Mediawiki; -use 5.008; +use 5.008001; use strict; use POSIX; use Git; diff --git a/contrib/subtree/git-subtree.sh b/contrib/subtree/git-subtree.sh index e0c5d3b0de..3028029ac2 100755 --- a/contrib/subtree/git-subtree.sh +++ b/contrib/subtree/git-subtree.sh @@ -373,7 +373,8 @@ try_remove_previous () { # Usage: process_subtree_split_trailer SPLIT_HASH MAIN_HASH [REPOSITORY] process_subtree_split_trailer () { - assert test $# = 2 -o $# = 3 + assert test $# -ge 2 + assert test $# -le 3 b="$1" sq="$2" repository="" @@ -402,7 +403,8 @@ process_subtree_split_trailer () { # Usage: find_latest_squash DIR [REPOSITORY] find_latest_squash () { - assert test $# = 1 -o $# = 2 + assert test $# -ge 1 + assert test $# -le 2 dir="$1" repository="" if test "$#" = 2 @@ -455,7 +457,8 @@ find_latest_squash () { # Usage: find_existing_splits DIR REV [REPOSITORY] find_existing_splits () { - assert test $# = 2 -o $# = 3 + assert test $# -ge 2 + assert test $# -le 3 debug "Looking for prior splits..." local indent=$(($indent + 1)) @@ -489,13 +492,13 @@ find_existing_splits () { ;; END) debug "Main is: '$main'" - if test -z "$main" -a -n "$sub" + if test -z "$main" && test -n "$sub" then # squash commits refer to a subtree debug " Squash: $sq from $sub" cache_set "$sq" "$sub" fi - if test -n "$main" -a -n "$sub" + if test -n "$main" && test -n "$sub" then debug " Prior: $main -> $sub" cache_set $main $sub @@ -638,10 +641,16 @@ subtree_for_commit () { while read mode type tree name do assert test "$name" = "$dir" - assert test "$type" = "tree" -o "$type" = "commit" - test "$type" = "commit" && continue # ignore submodules - echo $tree - break + + case "$type" in + commit) + continue;; # ignore submodules + tree) + echo $tree + break;; + *) + die "fatal: tree entry is of type ${type}, expected tree or commit";; + esac done || exit $? } @@ -916,7 +925,7 @@ cmd_split () { if test $# -eq 0 then rev=$(git rev-parse HEAD) - elif test $# -eq 1 -o $# -eq 2 + elif test $# -eq 1 || test $# -eq 2 then rev=$(git rev-parse -q --verify "$1^{commit}") || die "fatal: '$1' does not refer to a commit" @@ -1006,8 +1015,11 @@ cmd_split () { # Usage: cmd_merge REV [REPOSITORY] cmd_merge () { - test $# -eq 1 -o $# -eq 2 || + if test $# -lt 1 || test $# -gt 2 + then die "fatal: you must provide exactly one revision, and optionally a repository. Got: '$*'" + fi + rev=$(git rev-parse -q --verify "$1^{commit}") || die "fatal: '$1' does not refer to a commit" repository="" @@ -2179,7 +2179,8 @@ static int exclude_matches_pathspec(const char *path, int pathlen, PATHSPEC_LITERAL | PATHSPEC_GLOB | PATHSPEC_ICASE | - PATHSPEC_EXCLUDE); + PATHSPEC_EXCLUDE | + PATHSPEC_ATTR); for (i = 0; i < pathspec->nr; i++) { const struct pathspec_item *item = &pathspec->items[i]; diff --git a/git-archimport.perl b/git-archimport.perl index b7c173c345..f5a317b899 100755 --- a/git-archimport.perl +++ b/git-archimport.perl @@ -54,7 +54,7 @@ and can contain multiple, unrelated branches. =cut -use 5.008; +use 5.008001; use strict; use warnings; use Getopt::Std; diff --git a/git-cvsexportcommit.perl b/git-cvsexportcommit.perl index 289d4bc684..1e03ba94d1 100755 --- a/git-cvsexportcommit.perl +++ b/git-cvsexportcommit.perl @@ -1,6 +1,6 @@ #!/usr/bin/perl -use 5.008; +use 5.008001; use strict; use warnings; use Getopt::Std; diff --git a/git-cvsimport.perl b/git-cvsimport.perl index 7bf3c12d67..07ea3443f7 100755 --- a/git-cvsimport.perl +++ b/git-cvsimport.perl @@ -13,7 +13,7 @@ # The head revision is on branch "origin" by default. # You can change that with the '-o' option. -use 5.008; +use 5.008001; use strict; use warnings; use Getopt::Long; diff --git a/git-cvsserver.perl b/git-cvsserver.perl index 7b757360e2..124f598bdc 100755 --- a/git-cvsserver.perl +++ b/git-cvsserver.perl @@ -15,7 +15,7 @@ #### #### -use 5.008; +use 5.008001; use strict; use warnings; use bytes; diff --git a/git-send-email.perl b/git-send-email.perl index d24e981d61..821b2b3a13 100755 --- a/git-send-email.perl +++ b/git-send-email.perl @@ -16,7 +16,7 @@ # and second line is the subject of the message. # -use 5.008; +use 5.008001; use strict; use warnings $ENV{GIT_PERL_FATAL_WARNINGS} ? qw(FATAL all) : (); use Getopt::Long; @@ -119,13 +119,16 @@ sub completion_helper { foreach my $key (keys %$original_opts) { unless (exists $not_for_completion{$key}) { - $key =~ s/!$//; + my $negatable = ($key =~ s/!$//); if ($key =~ /[:=][si]$/) { $key =~ s/[:=][si]$//; push (@send_email_opts, "--$_=") foreach (split (/\|/, $key)); } else { push (@send_email_opts, "--$_") foreach (split (/\|/, $key)); + if ($negatable) { + push (@send_email_opts, "--no-$_") foreach (split (/\|/, $key)); + } } } } @@ -228,7 +231,7 @@ sub system_or_msg { my @sprintf_args = ($cmd_name ? $cmd_name : $args->[0], $exit_code); if (defined $msg) { # Quiet the 'redundant' warning category, except we - # need to support down to Perl 5.8, so we can't do a + # need to support down to Perl 5.8.1, so we can't do a # "no warnings 'redundant'", since that category was # introduced in perl 5.22, and asking for it will die # on older perls. @@ -491,7 +494,6 @@ my %options = ( "bcc=s" => \@getopt_bcc, "no-bcc" => \$no_bcc, "chain-reply-to!" => \$chain_reply_to, - "no-chain-reply-to" => sub {$chain_reply_to = 0}, "sendmail-cmd=s" => \$sendmail_cmd, "smtp-server=s" => \$smtp_server, "smtp-server-option=s" => \@smtp_server_options, @@ -506,36 +508,27 @@ my %options = ( "smtp-auth=s" => \$smtp_auth, "no-smtp-auth" => sub {$smtp_auth = 'none'}, "annotate!" => \$annotate, - "no-annotate" => sub {$annotate = 0}, "compose" => \$compose, "quiet" => \$quiet, "cc-cmd=s" => \$cc_cmd, "header-cmd=s" => \$header_cmd, "no-header-cmd" => \$no_header_cmd, "suppress-from!" => \$suppress_from, - "no-suppress-from" => sub {$suppress_from = 0}, "suppress-cc=s" => \@suppress_cc, "signed-off-cc|signed-off-by-cc!" => \$signed_off_by_cc, - "no-signed-off-cc|no-signed-off-by-cc" => sub {$signed_off_by_cc = 0}, - "cc-cover|cc-cover!" => \$cover_cc, - "no-cc-cover" => sub {$cover_cc = 0}, - "to-cover|to-cover!" => \$cover_to, - "no-to-cover" => sub {$cover_to = 0}, + "cc-cover!" => \$cover_cc, + "to-cover!" => \$cover_to, "confirm=s" => \$confirm, "dry-run" => \$dry_run, "envelope-sender=s" => \$envelope_sender, "thread!" => \$thread, - "no-thread" => sub {$thread = 0}, "validate!" => \$validate, - "no-validate" => sub {$validate = 0}, "transfer-encoding=s" => \$target_xfer_encoding, "format-patch!" => \$format_patch, - "no-format-patch" => sub {$format_patch = 0}, "8bit-encoding=s" => \$auto_8bit_encoding, "compose-encoding=s" => \$compose_encoding, "force" => \$force, "xmailer!" => \$use_xmailer, - "no-xmailer" => sub {$use_xmailer = 0}, "batch-size=i" => \$batch_size, "relogin-delay=i" => \$relogin_delay, "git-completion-helper" => \$git_completion_helper, diff --git a/git-svn.perl b/git-svn.perl index 4e8878f035..b0d0a50984 100755 --- a/git-svn.perl +++ b/git-svn.perl @@ -1,7 +1,7 @@ #!/usr/bin/perl # Copyright (C) 2006, Eric Wong <normalperson@yhbt.net> # License: GPL v2 or later -use 5.008; +use 5.008001; use warnings $ENV{GIT_PERL_FATAL_WARNINGS} ? qw(FATAL all) : (); use strict; use vars qw/ $AUTHOR $VERSION @@ -594,6 +594,7 @@ static struct cmd_struct commands[] = { { "remote-fd", cmd_remote_fd, NO_PARSEOPT }, { "repack", cmd_repack, RUN_SETUP }, { "replace", cmd_replace, RUN_SETUP }, + { "replay", cmd_replay, RUN_SETUP }, { "rerere", cmd_rerere, RUN_SETUP }, { "reset", cmd_reset, RUN_SETUP }, { "restore", cmd_restore, RUN_SETUP | NEED_WORK_TREE }, diff --git a/gitweb/INSTALL b/gitweb/INSTALL index a58e6b3c44..dadc6efa81 100644 --- a/gitweb/INSTALL +++ b/gitweb/INSTALL @@ -29,7 +29,7 @@ Requirements ------------ - Core git tools - - Perl 5.8 + - Perl 5.8.1 - Perl modules: CGI, Encode, Fcntl, File::Find, File::Basename. - web server diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index e66eb3d9ba..55e7c6567e 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -7,7 +7,7 @@ # # This program is licensed under the GPLv2 -use 5.008; +use 5.008001; use strict; use warnings; # handle ACL in file access tests @@ -64,6 +64,7 @@ void get_midx_rev_filename(struct strbuf *out, struct multi_pack_index *m) static int midx_read_oid_fanout(const unsigned char *chunk_start, size_t chunk_size, void *data) { + int i; struct multi_pack_index *m = data; m->chunk_oid_fanout = (uint32_t *)chunk_start; @@ -71,6 +72,16 @@ static int midx_read_oid_fanout(const unsigned char *chunk_start, error(_("multi-pack-index OID fanout is of the wrong size")); return 1; } + for (i = 0; i < 255; i++) { + uint32_t oid_fanout1 = ntohl(m->chunk_oid_fanout[i]); + uint32_t oid_fanout2 = ntohl(m->chunk_oid_fanout[i+1]); + + if (oid_fanout1 > oid_fanout2) { + error(_("oid fanout out of order: fanout[%d] = %"PRIx32" > %"PRIx32" = fanout[%d]"), + i, oid_fanout1, oid_fanout2, i + 1); + return 1; + } + } m->num_objects = ntohl(m->chunk_oid_fanout[255]); return 0; } @@ -1782,15 +1793,6 @@ int verify_midx_file(struct repository *r, const char *object_dir, unsigned flag } stop_progress(&progress); - for (i = 0; i < 255; i++) { - uint32_t oid_fanout1 = ntohl(m->chunk_oid_fanout[i]); - uint32_t oid_fanout2 = ntohl(m->chunk_oid_fanout[i + 1]); - - if (oid_fanout1 > oid_fanout2) - midx_report(_("oid fanout out of order: fanout[%d] = %"PRIx32" > %"PRIx32" = fanout[%d]"), - i, oid_fanout1, oid_fanout2, i + 1); - } - if (m->num_objects == 0) { midx_report(_("the midx contains no oid")); /* diff --git a/oss-fuzz/.gitignore b/oss-fuzz/.gitignore index 9acb74412e..5b95408825 100644 --- a/oss-fuzz/.gitignore +++ b/oss-fuzz/.gitignore @@ -1,3 +1,4 @@ fuzz-commit-graph +fuzz-date fuzz-pack-headers fuzz-pack-idx diff --git a/oss-fuzz/fuzz-date.c b/oss-fuzz/fuzz-date.c new file mode 100644 index 0000000000..036378b946 --- /dev/null +++ b/oss-fuzz/fuzz-date.c @@ -0,0 +1,49 @@ +#include "git-compat-util.h" +#include "date.h" + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size); + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) +{ + int local; + int num; + char *str; + int16_t tz; + timestamp_t ts; + enum date_mode_type dmtype; + struct date_mode *dm; + + if (size <= 4) + /* + * we use the first byte to fuzz dmtype and the + * second byte to fuzz local, then the next two + * bytes to fuzz tz offset. The remainder + * (at least one byte) is fed as input to + * approxidate_careful(). + */ + return 0; + + local = !!(*data++ & 0x10); + num = *data++ % DATE_UNIX; + if (num >= DATE_STRFTIME) + num++; + dmtype = (enum date_mode_type)num; + size -= 2; + + tz = *data++; + tz = (tz << 8) | *data++; + size -= 2; + + str = xmemdupz(data, size); + + ts = approxidate_careful(str, &num); + free(str); + + dm = date_mode_from_type(dmtype); + dm->local = local; + show_date(ts, (int)tz, dm); + + date_mode_release(dm); + + return 0; +} diff --git a/packfile.h b/packfile.h index c3692308b8..28c8fd3e39 100644 --- a/packfile.h +++ b/packfile.h @@ -54,7 +54,7 @@ const char *pack_basename(struct packed_git *p); struct packed_git *parse_pack_index(unsigned char *sha1, const char *idx_path); typedef void each_file_in_pack_dir_fn(const char *full_path, size_t full_path_len, - const char *file_pach, void *data); + const char *file_name, void *data); void for_each_file_in_pack_dir(const char *objdir, each_file_in_pack_dir_fn fn, void *data); diff --git a/pathspec.c b/pathspec.c index bb1efe1f39..2133b9fe60 100644 --- a/pathspec.c +++ b/pathspec.c @@ -109,16 +109,37 @@ static struct pathspec_magic { { PATHSPEC_ATTR, '\0', "attr" }, }; -static void prefix_magic(struct strbuf *sb, int prefixlen, unsigned magic) +static void prefix_magic(struct strbuf *sb, int prefixlen, + unsigned magic, const char *element) { - int i; - strbuf_addstr(sb, ":("); - for (i = 0; i < ARRAY_SIZE(pathspec_magic); i++) - if (magic & pathspec_magic[i].bit) { - if (sb->buf[sb->len - 1] != '(') - strbuf_addch(sb, ','); - strbuf_addstr(sb, pathspec_magic[i].name); + /* No magic was found in element, just add prefix magic */ + if (!magic) { + strbuf_addf(sb, ":(prefix:%d)", prefixlen); + return; + } + + /* + * At this point, we know that parse_element_magic() was able + * to extract some pathspec magic from element. So we know + * element is correctly formatted in either shorthand or + * longhand form + */ + if (element[1] != '(') { + /* Process an element in shorthand form (e.g. ":!/<match>") */ + strbuf_addstr(sb, ":("); + for (int i = 0; i < ARRAY_SIZE(pathspec_magic); i++) { + if ((magic & pathspec_magic[i].bit) && + pathspec_magic[i].mnemonic) { + if (sb->buf[sb->len - 1] != '(') + strbuf_addch(sb, ','); + strbuf_addstr(sb, pathspec_magic[i].name); + } } + } else { + /* For the longhand form, we copy everything up to the final ')' */ + size_t len = strchr(element, ')') - element; + strbuf_add(sb, element, len); + } strbuf_addf(sb, ",prefix:%d)", prefixlen); } @@ -493,7 +514,7 @@ static void init_pathspec_item(struct pathspec_item *item, unsigned flags, struct strbuf sb = STRBUF_INIT; /* Preserve the actual prefix length of each pattern */ - prefix_magic(&sb, prefixlen, element_magic); + prefix_magic(&sb, prefixlen, element_magic, elt); strbuf_addstr(&sb, match); item->original = strbuf_detach(&sb, NULL); diff --git a/perl/Git.pm b/perl/Git.pm index 117765dc73..03bf570bf4 100644 --- a/perl/Git.pm +++ b/perl/Git.pm @@ -7,7 +7,7 @@ Git - Perl interface to the Git version control system package Git; -use 5.008; +use 5.008001; use strict; use warnings $ENV{GIT_PERL_FATAL_WARNINGS} ? qw(FATAL all) : (); diff --git a/perl/Git/I18N.pm b/perl/Git/I18N.pm index 895e759c57..5454c3a6d2 100644 --- a/perl/Git/I18N.pm +++ b/perl/Git/I18N.pm @@ -1,5 +1,5 @@ package Git::I18N; -use 5.008; +use 5.008001; use strict; use warnings $ENV{GIT_PERL_FATAL_WARNINGS} ? qw(FATAL all) : (); BEGIN { diff --git a/perl/Git/LoadCPAN.pm b/perl/Git/LoadCPAN.pm index 0c360bc799..8c7fa805f9 100644 --- a/perl/Git/LoadCPAN.pm +++ b/perl/Git/LoadCPAN.pm @@ -1,5 +1,5 @@ package Git::LoadCPAN; -use 5.008; +use 5.008001; use strict; use warnings $ENV{GIT_PERL_FATAL_WARNINGS} ? qw(FATAL all) : (); diff --git a/perl/Git/LoadCPAN/Error.pm b/perl/Git/LoadCPAN/Error.pm index 5d84c20288..5cecb0fcd6 100644 --- a/perl/Git/LoadCPAN/Error.pm +++ b/perl/Git/LoadCPAN/Error.pm @@ -1,5 +1,5 @@ package Git::LoadCPAN::Error; -use 5.008; +use 5.008001; use strict; use warnings $ENV{GIT_PERL_FATAL_WARNINGS} ? qw(FATAL all) : (); use Git::LoadCPAN ( diff --git a/perl/Git/LoadCPAN/Mail/Address.pm b/perl/Git/LoadCPAN/Mail/Address.pm index 340e88a7a5..9f808090a6 100644 --- a/perl/Git/LoadCPAN/Mail/Address.pm +++ b/perl/Git/LoadCPAN/Mail/Address.pm @@ -1,5 +1,5 @@ package Git::LoadCPAN::Mail::Address; -use 5.008; +use 5.008001; use strict; use warnings $ENV{GIT_PERL_FATAL_WARNINGS} ? qw(FATAL all) : (); use Git::LoadCPAN ( diff --git a/perl/Git/Packet.pm b/perl/Git/Packet.pm index d144f5168f..d896e69523 100644 --- a/perl/Git/Packet.pm +++ b/perl/Git/Packet.pm @@ -1,5 +1,5 @@ package Git::Packet; -use 5.008; +use 5.008001; use strict; use warnings $ENV{GIT_PERL_FATAL_WARNINGS} ? qw(FATAL all) : (); BEGIN { diff --git a/ref-filter.c b/ref-filter.c index e4d3510e28..fdaabb5bb4 100644 --- a/ref-filter.c +++ b/ref-filter.c @@ -2508,17 +2508,12 @@ static int populate_value(struct ref_array_item *ref, struct strbuf *err) return 0; /* - * If it is a tag object, see if we use a value that derefs - * the object, and if we do grab the object it refers to. + * If it is a tag object, see if we use the peeled value. If we do, + * grab the peeled OID. */ - oi_deref.oid = *get_tagged_oid((struct tag *)obj); + if (need_tagged && peel_iterated_oid(&obj->oid, &oi_deref.oid)) + die("bad tag"); - /* - * NEEDSWORK: This derefs tag only once, which - * is good to deal with chains of trust, but - * is not consistent with what deref_tag() does - * which peels the onion to the core. - */ return get_object(ref, 1, &obj, &oi_deref, err); } @@ -2716,15 +2711,18 @@ static struct ref_array_item *new_ref_array_item(const char *refname, return ref; } +static void ref_array_append(struct ref_array *array, struct ref_array_item *ref) +{ + ALLOC_GROW(array->items, array->nr + 1, array->alloc); + array->items[array->nr++] = ref; +} + struct ref_array_item *ref_array_push(struct ref_array *array, const char *refname, const struct object_id *oid) { struct ref_array_item *ref = new_ref_array_item(refname, oid); - - ALLOC_GROW(array->items, array->nr + 1, array->alloc); - array->items[array->nr++] = ref; - + ref_array_append(array, ref); return ref; } @@ -2761,48 +2759,36 @@ static int filter_ref_kind(struct ref_filter *filter, const char *refname) return ref_kind_from_refname(refname); } -struct ref_filter_cbdata { - struct ref_array *array; - struct ref_filter *filter; - struct contains_cache contains_cache; - struct contains_cache no_contains_cache; -}; - -/* - * A call-back given to for_each_ref(). Filter refs and keep them for - * later object processing. - */ -static int ref_filter_handler(const char *refname, const struct object_id *oid, int flag, void *cb_data) +static struct ref_array_item *apply_ref_filter(const char *refname, const struct object_id *oid, + int flag, struct ref_filter *filter) { - struct ref_filter_cbdata *ref_cbdata = cb_data; - struct ref_filter *filter = ref_cbdata->filter; struct ref_array_item *ref; struct commit *commit = NULL; unsigned int kind; if (flag & REF_BAD_NAME) { warning(_("ignoring ref with broken name %s"), refname); - return 0; + return NULL; } if (flag & REF_ISBROKEN) { warning(_("ignoring broken ref %s"), refname); - return 0; + return NULL; } /* Obtain the current ref kind from filter_ref_kind() and ignore unwanted refs. */ kind = filter_ref_kind(filter, refname); if (!(kind & filter->kind)) - return 0; + return NULL; if (!filter_pattern_match(filter, refname)) - return 0; + return NULL; if (filter_exclude_match(filter, refname)) - return 0; + return NULL; if (filter->points_at.nr && !match_points_at(&filter->points_at, oid, refname)) - return 0; + return NULL; /* * A merge filter is applied on refs pointing to commits. Hence @@ -2813,15 +2799,15 @@ static int ref_filter_handler(const char *refname, const struct object_id *oid, filter->with_commit || filter->no_commit || filter->verbose) { commit = lookup_commit_reference_gently(the_repository, oid, 1); if (!commit) - return 0; + return NULL; /* We perform the filtering for the '--contains' option... */ if (filter->with_commit && - !commit_contains(filter, commit, filter->with_commit, &ref_cbdata->contains_cache)) - return 0; + !commit_contains(filter, commit, filter->with_commit, &filter->internal.contains_cache)) + return NULL; /* ...or for the `--no-contains' option */ if (filter->no_commit && - commit_contains(filter, commit, filter->no_commit, &ref_cbdata->no_contains_cache)) - return 0; + commit_contains(filter, commit, filter->no_commit, &filter->internal.no_contains_cache)) + return NULL; } /* @@ -2829,11 +2815,32 @@ static int ref_filter_handler(const char *refname, const struct object_id *oid, * to do its job and the resulting list may yet to be pruned * by maxcount logic. */ - ref = ref_array_push(ref_cbdata->array, refname, oid); + ref = new_ref_array_item(refname, oid); ref->commit = commit; ref->flag = flag; ref->kind = kind; + return ref; +} + +struct ref_filter_cbdata { + struct ref_array *array; + struct ref_filter *filter; +}; + +/* + * A call-back given to for_each_ref(). Filter refs and keep them for + * later object processing. + */ +static int filter_one(const char *refname, const struct object_id *oid, int flag, void *cb_data) +{ + struct ref_filter_cbdata *ref_cbdata = cb_data; + struct ref_array_item *ref; + + ref = apply_ref_filter(refname, oid, flag, ref_cbdata->filter); + if (ref) + ref_array_append(ref_cbdata->array, ref); + return 0; } @@ -2851,6 +2858,49 @@ static void free_array_item(struct ref_array_item *item) free(item); } +struct ref_filter_and_format_cbdata { + struct ref_filter *filter; + struct ref_format *format; + + struct ref_filter_and_format_internal { + int count; + } internal; +}; + +static int filter_and_format_one(const char *refname, const struct object_id *oid, int flag, void *cb_data) +{ + struct ref_filter_and_format_cbdata *ref_cbdata = cb_data; + struct ref_array_item *ref; + struct strbuf output = STRBUF_INIT, err = STRBUF_INIT; + + ref = apply_ref_filter(refname, oid, flag, ref_cbdata->filter); + if (!ref) + return 0; + + if (format_ref_array_item(ref, ref_cbdata->format, &output, &err)) + die("%s", err.buf); + + if (output.len || !ref_cbdata->format->array_opts.omit_empty) { + fwrite(output.buf, 1, output.len, stdout); + putchar('\n'); + } + + strbuf_release(&output); + strbuf_release(&err); + free_array_item(ref); + + /* + * Increment the running count of refs that match the filter. If + * max_count is set and we've reached the max, stop the ref + * iteration by returning a nonzero value. + */ + if (ref_cbdata->format->array_opts.max_count && + ++ref_cbdata->internal.count >= ref_cbdata->format->array_opts.max_count) + return 1; + + return 0; +} + /* Free all memory allocated for ref_array */ void ref_array_clear(struct ref_array *array) { @@ -2969,28 +3019,14 @@ void filter_ahead_behind(struct repository *r, free(commits); } -/* - * API for filtering a set of refs. Based on the type of refs the user - * has requested, we iterate through those refs and apply filters - * as per the given ref_filter structure and finally store the - * filtered refs in the ref_array structure. - */ -int filter_refs(struct ref_array *array, struct ref_filter *filter, unsigned int type) +static int do_filter_refs(struct ref_filter *filter, unsigned int type, each_ref_fn fn, void *cb_data) { - struct ref_filter_cbdata ref_cbdata; - int save_commit_buffer_orig; int ret = 0; - ref_cbdata.array = array; - ref_cbdata.filter = filter; - filter->kind = type & FILTER_REFS_KIND_MASK; - save_commit_buffer_orig = save_commit_buffer; - save_commit_buffer = 0; - - init_contains_cache(&ref_cbdata.contains_cache); - init_contains_cache(&ref_cbdata.no_contains_cache); + init_contains_cache(&filter->internal.contains_cache); + init_contains_cache(&filter->internal.no_contains_cache); /* Simple per-ref filtering */ if (!filter->kind) @@ -3003,19 +3039,42 @@ int filter_refs(struct ref_array *array, struct ref_filter *filter, unsigned int * of filter_ref_kind(). */ if (filter->kind == FILTER_REFS_BRANCHES) - ret = for_each_fullref_in("refs/heads/", ref_filter_handler, &ref_cbdata); + ret = for_each_fullref_in("refs/heads/", fn, cb_data); else if (filter->kind == FILTER_REFS_REMOTES) - ret = for_each_fullref_in("refs/remotes/", ref_filter_handler, &ref_cbdata); + ret = for_each_fullref_in("refs/remotes/", fn, cb_data); else if (filter->kind == FILTER_REFS_TAGS) - ret = for_each_fullref_in("refs/tags/", ref_filter_handler, &ref_cbdata); + ret = for_each_fullref_in("refs/tags/", fn, cb_data); else if (filter->kind & FILTER_REFS_ALL) - ret = for_each_fullref_in_pattern(filter, ref_filter_handler, &ref_cbdata); + ret = for_each_fullref_in_pattern(filter, fn, cb_data); if (!ret && (filter->kind & FILTER_REFS_DETACHED_HEAD)) - head_ref(ref_filter_handler, &ref_cbdata); + head_ref(fn, cb_data); } - clear_contains_cache(&ref_cbdata.contains_cache); - clear_contains_cache(&ref_cbdata.no_contains_cache); + clear_contains_cache(&filter->internal.contains_cache); + clear_contains_cache(&filter->internal.no_contains_cache); + + return ret; +} + +/* + * API for filtering a set of refs. Based on the type of refs the user + * has requested, we iterate through those refs and apply filters + * as per the given ref_filter structure and finally store the + * filtered refs in the ref_array structure. + */ +int filter_refs(struct ref_array *array, struct ref_filter *filter, unsigned int type) +{ + struct ref_filter_cbdata ref_cbdata; + int save_commit_buffer_orig; + int ret = 0; + + ref_cbdata.array = array; + ref_cbdata.filter = filter; + + save_commit_buffer_orig = save_commit_buffer; + save_commit_buffer = 0; + + ret = do_filter_refs(filter, type, filter_one, &ref_cbdata); /* Filters that need revision walking */ reach_filter(array, &filter->reachable_from, INCLUDE_REACHED); @@ -3025,6 +3084,51 @@ int filter_refs(struct ref_array *array, struct ref_filter *filter, unsigned int return ret; } +static inline int can_do_iterative_format(struct ref_filter *filter, + struct ref_sorting *sorting, + struct ref_format *format) +{ + /* + * Filtering & formatting results within a single ref iteration + * callback is not compatible with options that require + * post-processing a filtered ref_array. These include: + * - filtering on reachability + * - sorting the filtered results + * - including ahead-behind information in the formatted output + */ + return !(filter->reachable_from || + filter->unreachable_from || + sorting || + format->bases.nr); +} + +void filter_and_format_refs(struct ref_filter *filter, unsigned int type, + struct ref_sorting *sorting, + struct ref_format *format) +{ + if (can_do_iterative_format(filter, sorting, format)) { + int save_commit_buffer_orig; + struct ref_filter_and_format_cbdata ref_cbdata = { + .filter = filter, + .format = format, + }; + + save_commit_buffer_orig = save_commit_buffer; + save_commit_buffer = 0; + + do_filter_refs(filter, type, filter_and_format_one, &ref_cbdata); + + save_commit_buffer = save_commit_buffer_orig; + } else { + struct ref_array array = { 0 }; + filter_refs(&array, filter, type); + filter_ahead_behind(the_repository, format, &array); + ref_array_sort(sorting, &array); + print_formatted_ref_array(&array, format); + ref_array_clear(&array); + } +} + static int compare_detached_head(struct ref_array_item *a, struct ref_array_item *b) { if (!(a->kind ^ b->kind)) @@ -3142,7 +3246,8 @@ void ref_sorting_set_sort_flags_all(struct ref_sorting *sorting, void ref_array_sort(struct ref_sorting *sorting, struct ref_array *array) { - QSORT_S(array->items, array->nr, compare_refs, sorting); + if (sorting) + QSORT_S(array->items, array->nr, compare_refs, sorting); } static void append_literal(const char *cp, const char *ep, struct ref_formatting_state *state) @@ -3213,6 +3318,29 @@ int format_ref_array_item(struct ref_array_item *info, return 0; } +void print_formatted_ref_array(struct ref_array *array, struct ref_format *format) +{ + int total; + struct strbuf output = STRBUF_INIT, err = STRBUF_INIT; + + total = format->array_opts.max_count; + if (!total || array->nr < total) + total = array->nr; + for (int i = 0; i < total; i++) { + strbuf_reset(&err); + strbuf_reset(&output); + if (format_ref_array_item(array->items[i], format, &output, &err)) + die("%s", err.buf); + if (output.len || !format->array_opts.omit_empty) { + fwrite(output.buf, 1, output.len, stdout); + putchar('\n'); + } + } + + strbuf_release(&err); + strbuf_release(&output); +} + void pretty_print_ref(const char *name, const struct object_id *oid, struct ref_format *format) { @@ -3248,18 +3376,6 @@ static int parse_sorting_atom(const char *atom) return res; } -/* If no sorting option is given, use refname to sort as default */ -static struct ref_sorting *ref_default_sorting(void) -{ - static const char cstr_name[] = "refname"; - - struct ref_sorting *sorting = xcalloc(1, sizeof(*sorting)); - - sorting->next = NULL; - sorting->atom = parse_sorting_atom(cstr_name); - return sorting; -} - static void parse_ref_sorting(struct ref_sorting **sorting_tail, const char *arg) { struct ref_sorting *s; @@ -3283,9 +3399,7 @@ 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 { + if (options->nr) { for_each_string_list_item(item, options) parse_ref_sorting(tail, item->string); } diff --git a/ref-filter.h b/ref-filter.h index 1524bc463a..0ce5af58ab 100644 --- a/ref-filter.h +++ b/ref-filter.h @@ -7,6 +7,7 @@ #include "commit.h" #include "string-list.h" #include "strvec.h" +#include "commit-reach.h" /* Quoting styles */ #define QUOTE_NONE 0 @@ -75,6 +76,11 @@ struct ref_filter { lines; int abbrev, verbose; + + struct { + struct contains_cache contains_cache; + struct contains_cache no_contains_cache; + } internal; }; struct ref_format { @@ -92,6 +98,11 @@ struct ref_format { /* List of bases for ahead-behind counts. */ struct string_list bases; + + struct { + int max_count; + int omit_empty; + } array_opts; }; #define REF_FILTER_INIT { \ @@ -126,6 +137,14 @@ struct ref_format { * filtered refs in the ref_array structure. */ int filter_refs(struct ref_array *array, struct ref_filter *filter, unsigned int type); +/* + * Filter refs using the given ref_filter and type, sort the contents + * according to the given ref_sorting, format the filtered refs with the + * given ref_format, and print them to stdout. + */ +void filter_and_format_refs(struct ref_filter *filter, unsigned int type, + struct ref_sorting *sorting, + struct ref_format *format); /* Clear all memory allocated to ref_array */ void ref_array_clear(struct ref_array *array); /* Used to verify if the given format is correct and to parse out the used atoms */ @@ -151,6 +170,12 @@ char *get_head_description(void); void setup_ref_filter_porcelain_msg(void); /* + * Print up to maxcount ref_array elements to stdout using the given + * ref_format. + */ +void print_formatted_ref_array(struct ref_array *array, struct ref_format *format); + +/* * Print a single ref, outside of any ref-filter. Note that the * name must be a fully qualified refname. */ @@ -2599,13 +2599,55 @@ void ref_transaction_for_each_queued_update(struct ref_transaction *transaction, int refs_delete_refs(struct ref_store *refs, const char *logmsg, struct string_list *refnames, unsigned int flags) { + struct ref_transaction *transaction; + struct strbuf err = STRBUF_INIT; + struct string_list_item *item; + int ret = 0, failures = 0; char *msg; - int retval; + + if (!refnames->nr) + return 0; msg = normalize_reflog_message(logmsg); - retval = refs->be->delete_refs(refs, msg, refnames, flags); + + /* + * Since we don't check the references' old_oids, the + * individual updates can't fail, so we can pack all of the + * updates into a single transaction. + */ + transaction = ref_store_transaction_begin(refs, &err); + if (!transaction) { + ret = error("%s", err.buf); + goto out; + } + + for_each_string_list_item(item, refnames) { + ret = ref_transaction_delete(transaction, item->string, + NULL, flags, msg, &err); + if (ret) { + warning(_("could not delete reference %s: %s"), + item->string, err.buf); + strbuf_reset(&err); + failures = 1; + } + } + + ret = ref_transaction_commit(transaction, &err); + if (ret) { + if (refnames->nr == 1) + error(_("could not delete reference %s: %s"), + refnames->items[0].string, err.buf); + else + error(_("could not delete references: %s"), err.buf); + } + +out: + if (!ret && failures) + ret = -1; + ref_transaction_free(transaction); + strbuf_release(&err); free(msg); - return retval; + return ret; } int delete_refs(const char *msg, struct string_list *refnames, diff --git a/refs/debug.c b/refs/debug.c index b7ffc4ce67..83b7a0ba65 100644 --- a/refs/debug.c +++ b/refs/debug.c @@ -143,20 +143,6 @@ static int debug_create_symref(struct ref_store *ref_store, return res; } -static int debug_delete_refs(struct ref_store *ref_store, const char *msg, - struct string_list *refnames, unsigned int flags) -{ - struct debug_ref_store *drefs = (struct debug_ref_store *)ref_store; - int res = - drefs->refs->be->delete_refs(drefs->refs, msg, refnames, flags); - int i; - trace_printf_key(&trace_refs, "delete_refs {\n"); - for (i = 0; i < refnames->nr; i++) - trace_printf_key(&trace_refs, "%s\n", refnames->items[i].string); - trace_printf_key(&trace_refs, "}: %d\n", res); - return res; -} - static int debug_rename_ref(struct ref_store *ref_store, const char *oldref, const char *newref, const char *logmsg) { @@ -458,7 +444,6 @@ struct ref_storage_be refs_be_debug = { .pack_refs = debug_pack_refs, .create_symref = debug_create_symref, - .delete_refs = debug_delete_refs, .rename_ref = debug_rename_ref, .copy_ref = debug_copy_ref, diff --git a/refs/files-backend.c b/refs/files-backend.c index db5c0c7a72..ad8b1d143f 100644 --- a/refs/files-backend.c +++ b/refs/files-backend.c @@ -1265,54 +1265,6 @@ static int files_pack_refs(struct ref_store *ref_store, return 0; } -static int files_delete_refs(struct ref_store *ref_store, const char *msg, - struct string_list *refnames, unsigned int flags) -{ - struct files_ref_store *refs = - files_downcast(ref_store, REF_STORE_WRITE, "delete_refs"); - struct strbuf err = STRBUF_INIT; - int i, result = 0; - - if (!refnames->nr) - return 0; - - if (packed_refs_lock(refs->packed_ref_store, 0, &err)) - goto error; - - if (refs_delete_refs(refs->packed_ref_store, msg, refnames, flags)) { - packed_refs_unlock(refs->packed_ref_store); - goto error; - } - - packed_refs_unlock(refs->packed_ref_store); - - for (i = 0; i < refnames->nr; i++) { - const char *refname = refnames->items[i].string; - - if (refs_delete_ref(&refs->base, msg, refname, NULL, flags)) - result |= error(_("could not remove reference %s"), refname); - } - - strbuf_release(&err); - return result; - -error: - /* - * If we failed to rewrite the packed-refs file, then it is - * unsafe to try to remove loose refs, because doing so might - * expose an obsolete packed value for a reference that might - * even point at an object that has been garbage collected. - */ - if (refnames->nr == 1) - error(_("could not delete reference %s: %s"), - refnames->items[0].string, err.buf); - else - error(_("could not delete references: %s"), err.buf); - - strbuf_release(&err); - return -1; -} - /* * People using contrib's git-new-workdir have .git/logs/refs -> * /some/other/path/.git/logs/refs, and that may live on another device. @@ -3300,7 +3252,6 @@ struct ref_storage_be refs_be_files = { .pack_refs = files_pack_refs, .create_symref = files_create_symref, - .delete_refs = files_delete_refs, .rename_ref = files_rename_ref, .copy_ref = files_copy_ref, diff --git a/refs/packed-backend.c b/refs/packed-backend.c index 59c78d7941..b9fa097a29 100644 --- a/refs/packed-backend.c +++ b/refs/packed-backend.c @@ -1688,55 +1688,6 @@ static int packed_initial_transaction_commit(struct ref_store *ref_store UNUSED, return ref_transaction_commit(transaction, err); } -static int packed_delete_refs(struct ref_store *ref_store, const char *msg, - struct string_list *refnames, unsigned int flags) -{ - struct packed_ref_store *refs = - packed_downcast(ref_store, REF_STORE_WRITE, "delete_refs"); - struct strbuf err = STRBUF_INIT; - struct ref_transaction *transaction; - struct string_list_item *item; - int ret; - - (void)refs; /* We need the check above, but don't use the variable */ - - if (!refnames->nr) - return 0; - - /* - * Since we don't check the references' old_oids, the - * individual updates can't fail, so we can pack all of the - * updates into a single transaction. - */ - - transaction = ref_store_transaction_begin(ref_store, &err); - if (!transaction) - return -1; - - for_each_string_list_item(item, refnames) { - if (ref_transaction_delete(transaction, item->string, NULL, - flags, msg, &err)) { - warning(_("could not delete reference %s: %s"), - item->string, err.buf); - strbuf_reset(&err); - } - } - - ret = ref_transaction_commit(transaction, &err); - - if (ret) { - if (refnames->nr == 1) - error(_("could not delete reference %s: %s"), - refnames->items[0].string, err.buf); - else - error(_("could not delete references: %s"), err.buf); - } - - ref_transaction_free(transaction); - strbuf_release(&err); - return ret; -} - static int packed_pack_refs(struct ref_store *ref_store UNUSED, struct pack_refs_opts *pack_opts UNUSED) { @@ -1765,7 +1716,6 @@ struct ref_storage_be refs_be_packed = { .pack_refs = packed_pack_refs, .create_symref = NULL, - .delete_refs = packed_delete_refs, .rename_ref = NULL, .copy_ref = NULL, diff --git a/refs/refs-internal.h b/refs/refs-internal.h index 9db8aec4da..4af83bf9a5 100644 --- a/refs/refs-internal.h +++ b/refs/refs-internal.h @@ -553,8 +553,6 @@ typedef int create_symref_fn(struct ref_store *ref_store, const char *ref_target, const char *refs_heads_master, const char *logmsg); -typedef int delete_refs_fn(struct ref_store *ref_store, const char *msg, - struct string_list *refnames, unsigned int flags); typedef int rename_ref_fn(struct ref_store *ref_store, const char *oldref, const char *newref, const char *logmsg); @@ -677,7 +675,6 @@ struct ref_storage_be { pack_refs_fn *pack_refs; create_symref_fn *create_symref; - delete_refs_fn *delete_refs; rename_ref_fn *rename_ref; copy_ref_fn *copy_ref; diff --git a/t/Makefile b/t/Makefile index 3e00cdd801..225aaf78ed 100644 --- a/t/Makefile +++ b/t/Makefile @@ -17,6 +17,7 @@ TAR ?= $(TAR) RM ?= rm -f PROVE ?= prove DEFAULT_TEST_TARGET ?= test +DEFAULT_UNIT_TEST_TARGET ?= unit-tests-raw TEST_LINT ?= test-lint ifdef TEST_OUTPUT_DIRECTORY @@ -41,6 +42,7 @@ TPERF = $(sort $(wildcard perf/p[0-9][0-9][0-9][0-9]-*.sh)) TINTEROP = $(sort $(wildcard interop/i[0-9][0-9][0-9][0-9]-*.sh)) CHAINLINTTESTS = $(sort $(patsubst chainlint/%.test,%,$(wildcard chainlint/*.test))) CHAINLINT = '$(PERL_PATH_SQ)' chainlint.pl +UNIT_TESTS = $(sort $(filter-out %.pdb unit-tests/bin/t-basic%,$(wildcard unit-tests/bin/t-*))) # `test-chainlint` (which is a dependency of `test-lint`, `test` and `prove`) # checks all tests in all scripts via a single invocation, so tell individual @@ -65,6 +67,17 @@ prove: pre-clean check-chainlint $(TEST_LINT) $(T): @echo "*** $@ ***"; '$(TEST_SHELL_PATH_SQ)' $@ $(GIT_TEST_OPTS) +$(UNIT_TESTS): + @echo "*** $@ ***"; $@ + +.PHONY: unit-tests unit-tests-raw unit-tests-prove +unit-tests: $(DEFAULT_UNIT_TEST_TARGET) + +unit-tests-raw: $(UNIT_TESTS) + +unit-tests-prove: + @echo "*** prove - unit tests ***"; $(PROVE) $(GIT_PROVE_OPTS) $(UNIT_TESTS) + pre-clean: $(RM) -r '$(TEST_RESULTS_DIRECTORY_SQ)' @@ -149,4 +162,4 @@ perf: $(MAKE) -C perf/ all .PHONY: pre-clean $(T) aggregate-results clean valgrind perf \ - check-chainlint clean-chainlint test-chainlint + check-chainlint clean-chainlint test-chainlint $(UNIT_TESTS) diff --git a/t/helper/test-fast-rebase.c b/t/helper/test-fast-rebase.c deleted file mode 100644 index cac20a72b3..0000000000 --- a/t/helper/test-fast-rebase.c +++ /dev/null @@ -1,241 +0,0 @@ -/* - * "git fast-rebase" builtin command - * - * FAST: Forking Any Subprocesses (is) Taboo - * - * This is meant SOLELY as a demo of what is possible. sequencer.c and - * rebase.c should be refactored to use the ideas here, rather than attempting - * to extend this file to replace those (unless Phillip or Dscho say that - * refactoring is too hard and we need a clean slate, but I'm guessing that - * refactoring is the better route). - */ - -#define USE_THE_INDEX_VARIABLE -#include "test-tool.h" -#include "cache-tree.h" -#include "commit.h" -#include "environment.h" -#include "gettext.h" -#include "hash.h" -#include "hex.h" -#include "lockfile.h" -#include "merge-ort.h" -#include "object-name.h" -#include "read-cache-ll.h" -#include "refs.h" -#include "revision.h" -#include "sequencer.h" -#include "setup.h" -#include "strvec.h" -#include "tree.h" - -static const char *short_commit_name(struct commit *commit) -{ - return repo_find_unique_abbrev(the_repository, &commit->object.oid, - DEFAULT_ABBREV); -} - -static struct commit *peel_committish(const char *name) -{ - struct object *obj; - struct object_id oid; - - if (repo_get_oid(the_repository, name, &oid)) - return NULL; - obj = parse_object(the_repository, &oid); - return (struct commit *)repo_peel_to_type(the_repository, name, 0, obj, - OBJ_COMMIT); -} - -static char *get_author(const char *message) -{ - size_t len; - const char *a; - - a = find_commit_header(message, "author", &len); - if (a) - return xmemdupz(a, len); - - return NULL; -} - -static struct commit *create_commit(struct tree *tree, - struct commit *based_on, - struct commit *parent) -{ - struct object_id ret; - struct object *obj; - struct commit_list *parents = NULL; - char *author; - char *sign_commit = NULL; - struct commit_extra_header *extra; - struct strbuf msg = STRBUF_INIT; - const char *out_enc = get_commit_output_encoding(); - const char *message = repo_logmsg_reencode(the_repository, based_on, - NULL, out_enc); - const char *orig_message = NULL; - const char *exclude_gpgsig[] = { "gpgsig", NULL }; - - commit_list_insert(parent, &parents); - extra = read_commit_extra_headers(based_on, exclude_gpgsig); - find_commit_subject(message, &orig_message); - strbuf_addstr(&msg, orig_message); - author = get_author(message); - reset_ident_date(); - if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents, - &ret, author, NULL, sign_commit, extra)) { - error(_("failed to write commit object")); - return NULL; - } - free(author); - strbuf_release(&msg); - - obj = parse_object(the_repository, &ret); - return (struct commit *)obj; -} - -int cmd__fast_rebase(int argc, const char **argv) -{ - struct commit *onto; - struct commit *last_commit = NULL, *last_picked_commit = NULL; - struct object_id head; - struct lock_file lock = LOCK_INIT; - struct strvec rev_walk_args = STRVEC_INIT; - struct rev_info revs; - struct commit *commit; - struct merge_options merge_opt; - struct tree *next_tree, *base_tree, *head_tree; - struct merge_result result; - struct strbuf reflog_msg = STRBUF_INIT; - struct strbuf branch_name = STRBUF_INIT; - int ret = 0; - - /* - * test-tool stuff doesn't set up the git directory by default; need to - * do that manually. - */ - setup_git_directory(); - - if (argc == 2 && !strcmp(argv[1], "-h")) { - printf("Sorry, I am not a psychiatrist; I can not give you the help you need. Oh, you meant usage...\n"); - exit(129); - } - - if (argc != 5 || strcmp(argv[1], "--onto")) - die("usage: read the code, figure out how to use it, then do so"); - - onto = peel_committish(argv[2]); - strbuf_addf(&branch_name, "refs/heads/%s", argv[4]); - - /* Sanity check */ - if (repo_get_oid(the_repository, "HEAD", &head)) - die(_("Cannot read HEAD")); - assert(oideq(&onto->object.oid, &head)); - - repo_hold_locked_index(the_repository, &lock, LOCK_DIE_ON_ERROR); - if (repo_read_index(the_repository) < 0) - BUG("Could not read index"); - - repo_init_revisions(the_repository, &revs, NULL); - revs.verbose_header = 1; - revs.max_parents = 1; - revs.cherry_mark = 1; - revs.limited = 1; - revs.reverse = 1; - revs.right_only = 1; - revs.sort_order = REV_SORT_IN_GRAPH_ORDER; - revs.topo_order = 1; - strvec_pushl(&rev_walk_args, "", argv[4], "--not", argv[3], NULL); - - if (setup_revisions(rev_walk_args.nr, rev_walk_args.v, &revs, NULL) > 1) { - ret = error(_("unhandled options")); - goto cleanup; - } - - strvec_clear(&rev_walk_args); - - if (prepare_revision_walk(&revs) < 0) { - ret = error(_("error preparing revisions")); - goto cleanup; - } - - init_merge_options(&merge_opt, the_repository); - memset(&result, 0, sizeof(result)); - merge_opt.show_rename_progress = 1; - merge_opt.branch1 = "HEAD"; - head_tree = repo_get_commit_tree(the_repository, onto); - result.tree = head_tree; - last_commit = onto; - while ((commit = get_revision(&revs))) { - struct commit *base; - - fprintf(stderr, "Rebasing %s...\r", - oid_to_hex(&commit->object.oid)); - assert(commit->parents && !commit->parents->next); - base = commit->parents->item; - - next_tree = repo_get_commit_tree(the_repository, commit); - base_tree = repo_get_commit_tree(the_repository, base); - - merge_opt.branch2 = short_commit_name(commit); - merge_opt.ancestor = xstrfmt("parent of %s", merge_opt.branch2); - - merge_incore_nonrecursive(&merge_opt, - base_tree, - result.tree, - next_tree, - &result); - - free((char*)merge_opt.ancestor); - merge_opt.ancestor = NULL; - if (!result.clean) - break; - last_picked_commit = commit; - last_commit = create_commit(result.tree, commit, last_commit); - } - - merge_switch_to_result(&merge_opt, head_tree, &result, 1, !result.clean); - - if (result.clean < 0) - exit(128); - - if (result.clean) { - fprintf(stderr, "\nDone.\n"); - strbuf_addf(&reflog_msg, "finish rebase %s onto %s", - oid_to_hex(&last_picked_commit->object.oid), - oid_to_hex(&last_commit->object.oid)); - if (update_ref(reflog_msg.buf, branch_name.buf, - &last_commit->object.oid, - &last_picked_commit->object.oid, - REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR)) { - error(_("could not update %s"), argv[4]); - die("Failed to update %s", argv[4]); - } - if (create_symref("HEAD", branch_name.buf, reflog_msg.buf) < 0) - die(_("unable to update HEAD")); - - prime_cache_tree(the_repository, the_repository->index, - result.tree); - } else { - fprintf(stderr, "\nAborting: Hit a conflict.\n"); - strbuf_addf(&reflog_msg, "rebase progress up to %s", - oid_to_hex(&last_picked_commit->object.oid)); - if (update_ref(reflog_msg.buf, "HEAD", - &last_commit->object.oid, - &head, - REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR)) { - error(_("could not update %s"), argv[4]); - die("Failed to update %s", argv[4]); - } - } - if (write_locked_index(&the_index, &lock, - COMMIT_LOCK | SKIP_IF_UNCHANGED)) - die(_("unable to write %s"), get_index_file()); - - ret = (result.clean == 0); -cleanup: - strbuf_release(&reflog_msg); - strbuf_release(&branch_name); - release_revisions(&revs); - return ret; -} diff --git a/t/helper/test-ref-store.c b/t/helper/test-ref-store.c index 48552e6a9e..702ec1f128 100644 --- a/t/helper/test-ref-store.c +++ b/t/helper/test-ref-store.c @@ -298,16 +298,19 @@ static int cmd_update_ref(struct ref_store *refs, const char **argv) 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", transaction_flags); - struct object_id old_oid; + struct object_id old_oid, *old_oid_ptr = NULL; struct object_id new_oid; - if (get_oid_hex(old_sha1_buf, &old_oid)) - die("cannot parse %s as %s", old_sha1_buf, the_hash_algo->name); + if (*old_sha1_buf) { + if (get_oid_hex(old_sha1_buf, &old_oid)) + die("cannot parse %s as %s", old_sha1_buf, the_hash_algo->name); + old_oid_ptr = &old_oid; + } 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, + &new_oid, old_oid_ptr, flags, UPDATE_REFS_DIE_ON_ERR); } diff --git a/t/helper/test-tool.c b/t/helper/test-tool.c index 876cd2dc31..37ba996539 100644 --- a/t/helper/test-tool.c +++ b/t/helper/test-tool.c @@ -30,7 +30,6 @@ static struct test_cmd cmds[] = { { "dump-untracked-cache", cmd__dump_untracked_cache }, { "env-helper", cmd__env_helper }, { "example-decorate", cmd__example_decorate }, - { "fast-rebase", cmd__fast_rebase }, { "find-pack", cmd__find_pack }, { "fsmonitor-client", cmd__fsmonitor_client }, { "genrandom", cmd__genrandom }, diff --git a/t/helper/test-tool.h b/t/helper/test-tool.h index 70dd4eba11..8a1a7c63da 100644 --- a/t/helper/test-tool.h +++ b/t/helper/test-tool.h @@ -24,7 +24,6 @@ int cmd__dump_untracked_cache(int argc, const char **argv); int cmd__dump_reftable(int argc, const char **argv); int cmd__env_helper(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__find_pack(int argc, const char **argv); int cmd__fsmonitor_client(int argc, const char **argv); int cmd__genrandom(int argc, const char **argv); diff --git a/t/lib-httpd.sh b/t/lib-httpd.sh index 5fe3c8ab69..d83bafeab3 100644 --- a/t/lib-httpd.sh +++ b/t/lib-httpd.sh @@ -55,21 +55,31 @@ fi HTTPD_PARA="" -for DEFAULT_HTTPD_PATH in '/usr/sbin/httpd' '/usr/sbin/apache2' +for DEFAULT_HTTPD_PATH in '/usr/sbin/httpd' \ + '/usr/sbin/apache2' \ + "$(command -v httpd)" \ + "$(command -v apache2)" do - if test -x "$DEFAULT_HTTPD_PATH" + if test -n "$DEFAULT_HTTPD_PATH" && test -x "$DEFAULT_HTTPD_PATH" then break fi done +if test -x "$DEFAULT_HTTPD_PATH" +then + DETECTED_HTTPD_ROOT="$("$DEFAULT_HTTPD_PATH" -V 2>/dev/null | sed -n 's/^ -D HTTPD_ROOT="\(.*\)"$/\1/p')" +fi + for DEFAULT_HTTPD_MODULE_PATH in '/usr/libexec/apache2' \ '/usr/lib/apache2/modules' \ '/usr/lib64/httpd/modules' \ '/usr/lib/httpd/modules' \ - '/usr/libexec/httpd' + '/usr/libexec/httpd' \ + '/usr/lib/apache2' \ + "${DETECTED_HTTPD_ROOT:+${DETECTED_HTTPD_ROOT}/modules}" do - if test -d "$DEFAULT_HTTPD_MODULE_PATH" + if test -n "$DEFAULT_HTTPD_MODULE_PATH" && test -d "$DEFAULT_HTTPD_MODULE_PATH" then break fi @@ -127,6 +137,20 @@ else "Could not identify web server at '$LIB_HTTPD_PATH'" fi +if test -n "$LIB_HTTPD_DAV" && test -f /etc/os-release +then + case "$(grep "^ID=" /etc/os-release | cut -d= -f2-)" in + alpine) + # The WebDAV module in Alpine Linux is broken at least up to + # Alpine v3.16 as the default DBM driver is missing. + # + # https://gitlab.alpinelinux.org/alpine/aports/-/issues/13112 + test_skip_or_die GIT_TEST_HTTPD \ + "Apache WebDAV module does not have default DBM backend driver" + ;; + esac +fi + install_script () { write_script "$HTTPD_ROOT_PATH/$1" <"$TEST_PATH/$1" } diff --git a/t/lib-httpd/passwd b/t/lib-httpd/passwd index 99a34d6487..d9c122f348 100644 --- a/t/lib-httpd/passwd +++ b/t/lib-httpd/passwd @@ -1 +1 @@ -user@host:xb4E8pqD81KQs +user@host:$apr1$LGPmCZWj$9vxEwj5Z5GzQLBMxp3mCx1 diff --git a/t/lib-httpd/proxy-passwd b/t/lib-httpd/proxy-passwd index 77c25138e0..2ad7705d9a 100644 --- a/t/lib-httpd/proxy-passwd +++ b/t/lib-httpd/proxy-passwd @@ -1 +1 @@ -proxuser:2x7tAukjAED5M +proxuser:$apr1$RxS6MLkD$DYsqQdflheq4GPNxzJpx5. diff --git a/t/perf/p6300-for-each-ref.sh b/t/perf/p6300-for-each-ref.sh new file mode 100755 index 0000000000..fa7289c752 --- /dev/null +++ b/t/perf/p6300-for-each-ref.sh @@ -0,0 +1,87 @@ +#!/bin/sh + +test_description='performance of for-each-ref' +. ./perf-lib.sh + +test_perf_fresh_repo + +ref_count_per_type=10000 +test_iteration_count=10 + +test_expect_success "setup" ' + test_commit_bulk $(( 1 + $ref_count_per_type )) && + + # Create refs + test_seq $ref_count_per_type | + sed "s,.*,update refs/heads/branch_& HEAD~&\nupdate refs/custom/special_& HEAD~&," | + git update-ref --stdin && + + # Create annotated tags + for i in $(test_seq $ref_count_per_type) + do + # Base tags + echo "tag tag_$i" && + echo "mark :$i" && + echo "from HEAD~$i" && + printf "tagger %s <%s> %s\n" \ + "$GIT_COMMITTER_NAME" \ + "$GIT_COMMITTER_EMAIL" \ + "$GIT_COMMITTER_DATE" && + echo "data <<EOF" && + echo "tag $i" && + echo "EOF" && + + # Nested tags + echo "tag nested_$i" && + echo "from :$i" && + printf "tagger %s <%s> %s\n" \ + "$GIT_COMMITTER_NAME" \ + "$GIT_COMMITTER_EMAIL" \ + "$GIT_COMMITTER_DATE" && + echo "data <<EOF" && + echo "nested tag $i" && + echo "EOF" || return 1 + done | git fast-import +' + +test_for_each_ref () { + title="for-each-ref" + if test $# -gt 0; then + title="$title ($1)" + shift + fi + args="$@" + + test_perf "$title" " + for i in \$(test_seq $test_iteration_count); do + git for-each-ref $args >/dev/null + done + " +} + +run_tests () { + test_for_each_ref "$1" + test_for_each_ref "$1, no sort" --no-sort + test_for_each_ref "$1, --count=1" --count=1 + test_for_each_ref "$1, --count=1, no sort" --no-sort --count=1 + test_for_each_ref "$1, tags" refs/tags/ + test_for_each_ref "$1, tags, no sort" --no-sort refs/tags/ + test_for_each_ref "$1, tags, dereferenced" '--format="%(refname) %(objectname) %(*objectname)"' refs/tags/ + test_for_each_ref "$1, tags, dereferenced, no sort" --no-sort '--format="%(refname) %(objectname) %(*objectname)"' refs/tags/ + + test_perf "for-each-ref ($1, tags) + cat-file --batch-check (dereferenced)" " + for i in \$(test_seq $test_iteration_count); do + git for-each-ref --format='%(objectname)^{} %(refname) %(objectname)' refs/tags/ | \ + git cat-file --batch-check='%(objectname) %(rest)' >/dev/null + done + " +} + +run_tests "loose" + +test_expect_success 'pack refs' ' + git pack-refs --all +' +run_tests "packed" + +test_done diff --git a/t/perf/perf-lib.sh b/t/perf/perf-lib.sh index e7786775a9..b952e5024b 100644 --- a/t/perf/perf-lib.sh +++ b/t/perf/perf-lib.sh @@ -31,7 +31,7 @@ unset GIT_CONFIG_NOSYSTEM GIT_CONFIG_SYSTEM="$TEST_DIRECTORY/perf/config" export GIT_CONFIG_SYSTEM -if test -n "$GIT_TEST_INSTALLED" -a -z "$PERF_SET_GIT_TEST_INSTALLED" +if test -n "$GIT_TEST_INSTALLED" && test -z "$PERF_SET_GIT_TEST_INSTALLED" then error "Do not use GIT_TEST_INSTALLED with the perf tests. diff --git a/t/perf/run b/t/perf/run index 34115edec3..486ead2198 100755 --- a/t/perf/run +++ b/t/perf/run @@ -91,10 +91,10 @@ set_git_test_installed () { run_dirs_helper () { mydir=${1%/} shift - while test $# -gt 0 -a "$1" != -- -a ! -f "$1"; do + while test $# -gt 0 && test "$1" != -- && test ! -f "$1"; do shift done - if test $# -gt 0 -a "$1" = --; then + if test $# -gt 0 && test "$1" = --; then shift fi @@ -124,7 +124,7 @@ run_dirs_helper () { } run_dirs () { - while test $# -gt 0 -a "$1" != -- -a ! -f "$1"; do + while test $# -gt 0 && test "$1" != -- && test ! -f "$1"; do run_dirs_helper "$@" shift done @@ -180,7 +180,8 @@ run_subsection () { GIT_PERF_AGGREGATING_LATER=t export GIT_PERF_AGGREGATING_LATER - if test $# = 0 -o "$1" = -- -o -f "$1"; then + if test $# = 0 || test "$1" = -- || test -f "$1" + then set -- . "$@" fi diff --git a/t/t0080-unit-test-output.sh b/t/t0080-unit-test-output.sh new file mode 100755 index 0000000000..961b54b06c --- /dev/null +++ b/t/t0080-unit-test-output.sh @@ -0,0 +1,58 @@ +#!/bin/sh + +test_description='Test the output of the unit test framework' + +. ./test-lib.sh + +test_expect_success 'TAP output from unit tests' ' + cat >expect <<-EOF && + ok 1 - passing test + ok 2 - passing test and assertion return 1 + # check "1 == 2" failed at t/unit-tests/t-basic.c:76 + # left: 1 + # right: 2 + not ok 3 - failing test + ok 4 - failing test and assertion return 0 + not ok 5 - passing TEST_TODO() # TODO + ok 6 - passing TEST_TODO() returns 1 + # todo check ${SQ}check(x)${SQ} succeeded at t/unit-tests/t-basic.c:25 + not ok 7 - failing TEST_TODO() + ok 8 - failing TEST_TODO() returns 0 + # check "0" failed at t/unit-tests/t-basic.c:30 + # skipping test - missing prerequisite + # skipping check ${SQ}1${SQ} at t/unit-tests/t-basic.c:32 + ok 9 - test_skip() # SKIP + ok 10 - skipped test returns 1 + # skipping test - missing prerequisite + ok 11 - test_skip() inside TEST_TODO() # SKIP + ok 12 - test_skip() inside TEST_TODO() returns 1 + # check "0" failed at t/unit-tests/t-basic.c:48 + not ok 13 - TEST_TODO() after failing check + ok 14 - TEST_TODO() after failing check returns 0 + # check "0" failed at t/unit-tests/t-basic.c:56 + not ok 15 - failing check after TEST_TODO() + ok 16 - failing check after TEST_TODO() returns 0 + # check "!strcmp("\thello\\\\", "there\"\n")" failed at t/unit-tests/t-basic.c:61 + # left: "\011hello\\\\" + # right: "there\"\012" + # check "!strcmp("NULL", NULL)" failed at t/unit-tests/t-basic.c:62 + # left: "NULL" + # right: NULL + # check "${SQ}a${SQ} == ${SQ}\n${SQ}" failed at t/unit-tests/t-basic.c:63 + # left: ${SQ}a${SQ} + # right: ${SQ}\012${SQ} + # check "${SQ}\\\\${SQ} == ${SQ}\\${SQ}${SQ}" failed at t/unit-tests/t-basic.c:64 + # left: ${SQ}\\\\${SQ} + # right: ${SQ}\\${SQ}${SQ} + not ok 17 - messages from failing string and char comparison + # BUG: test has no checks at t/unit-tests/t-basic.c:91 + not ok 18 - test with no checks + ok 19 - test with no checks returns 0 + 1..19 + EOF + + ! "$GIT_BUILD_DIR"/t/unit-tests/bin/t-basic >actual && + test_cmp expect actual +' + +test_done diff --git a/t/t0202/test.pl b/t/t0202/test.pl index 2cbf7b9590..47d96a2a13 100755 --- a/t/t0202/test.pl +++ b/t/t0202/test.pl @@ -1,5 +1,5 @@ #!/usr/bin/perl -use 5.008; +use 5.008001; use lib (split(/:/, $ENV{GITPERLLIB})); use strict; use warnings; diff --git a/t/t1400-update-ref.sh b/t/t1400-update-ref.sh index 9ac4b7036b..6d1810c358 100755 --- a/t/t1400-update-ref.sh +++ b/t/t1400-update-ref.sh @@ -9,8 +9,6 @@ test_description='Test git update-ref and basic ref logging' Z=$ZERO_OID m=refs/heads/main -n_dir=refs/heads/gu -n=$n_dir/fixes outside=refs/foo bare=bare-repo @@ -62,10 +60,10 @@ test_expect_success "delete $m without oldvalue verification" ' test_must_fail git show-ref --verify -q $m ' -test_expect_success "fail to create $n" ' - test_when_finished "rm -f .git/$n_dir" && - touch .git/$n_dir && - test_must_fail git update-ref $n $A +test_expect_success "fail to create $n due to file/directory conflict" ' + test_when_finished "git update-ref -d refs/heads/gu" && + git update-ref refs/heads/gu $A && + test_must_fail git update-ref refs/heads/gu/fixes $A ' test_expect_success "create $m (by HEAD)" ' @@ -92,7 +90,8 @@ test_expect_success "deleting current branch adds message to HEAD's log" ' git symbolic-ref HEAD $m && git update-ref -m delete-$m -d $m && test_must_fail git show-ref --verify -q $m && - grep "delete-$m$" .git/logs/HEAD + test-tool ref-store main for-each-reflog-ent HEAD >actual && + grep "delete-$m$" actual ' test_expect_success "deleting by HEAD adds message to HEAD's log" ' @@ -101,7 +100,8 @@ test_expect_success "deleting by HEAD adds message to HEAD's log" ' git symbolic-ref HEAD $m && git update-ref -m delete-by-head -d HEAD && test_must_fail git show-ref --verify -q $m && - grep "delete-by-head$" .git/logs/HEAD + test-tool ref-store main for-each-reflog-ent HEAD >actual && + grep "delete-by-head$" actual ' test_expect_success 'update-ref does not create reflogs by default' ' @@ -132,7 +132,7 @@ test_expect_success 'creates no reflog in bare repository' ' test_expect_success 'core.logAllRefUpdates=true creates reflog in bare repository' ' test_when_finished "git -C $bare config --unset core.logAllRefUpdates && \ - rm $bare/logs/$m" && + test-tool ref-store main delete-reflog $m" && git -C $bare config core.logAllRefUpdates true && git -C $bare update-ref $m $bareB && git -C $bare rev-parse $bareB >expect && @@ -221,27 +221,27 @@ test_expect_success 'delete symref without dereference when the referred ref is ' test_expect_success 'update-ref -d is not confused by self-reference' ' + test_when_finished "test-tool ref-store main delete-refs REF_NO_DEREF refs/heads/self" && git symbolic-ref refs/heads/self refs/heads/self && - test_when_finished "rm -f .git/refs/heads/self" && - test_path_is_file .git/refs/heads/self && + git symbolic-ref --no-recurse refs/heads/self && test_must_fail git update-ref -d refs/heads/self && - test_path_is_file .git/refs/heads/self + git symbolic-ref --no-recurse refs/heads/self ' test_expect_success 'update-ref --no-deref -d can delete self-reference' ' + test_when_finished "test-tool ref-store main delete-refs REF_NO_DEREF refs/heads/self" && git symbolic-ref refs/heads/self refs/heads/self && - test_when_finished "rm -f .git/refs/heads/self" && - test_path_is_file .git/refs/heads/self && + git symbolic-ref --no-recurse refs/heads/self && git update-ref --no-deref -d refs/heads/self && test_must_fail git show-ref --verify -q refs/heads/self ' -test_expect_success 'update-ref --no-deref -d can delete reference to bad ref' ' +test_expect_success REFFILES 'update-ref --no-deref -d can delete reference to bad ref' ' >.git/refs/heads/bad && test_when_finished "rm -f .git/refs/heads/bad" && git symbolic-ref refs/heads/ref-to-bad refs/heads/bad && test_when_finished "git update-ref -d refs/heads/ref-to-bad" && - test_path_is_file .git/refs/heads/ref-to-bad && + git symbolic-ref --no-recurse refs/heads/ref-to-bad && git update-ref --no-deref -d refs/heads/ref-to-bad && test_must_fail git show-ref --verify -q refs/heads/ref-to-bad ' @@ -265,7 +265,10 @@ test_expect_success "(not) changed .git/$m" ' ! test $B = $(git show-ref -s --verify $m) ' -rm -f .git/logs/refs/heads/main +test_expect_success "clean up reflog" ' + test-tool ref-store main delete-reflog $m +' + test_expect_success "create $m (logged by touch)" ' test_config core.logAllRefUpdates false && GIT_COMMITTER_DATE="2005-05-26 23:30" \ @@ -285,7 +288,7 @@ test_expect_success "set $m (logged by touch)" ' test $A = $(git show-ref -s --verify $m) ' -test_expect_success 'empty directory removal' ' +test_expect_success REFFILES 'empty directory removal' ' git branch d1/d2/r1 HEAD && git branch d1/r2 HEAD && test_path_is_file .git/refs/heads/d1/d2/r1 && @@ -297,7 +300,7 @@ test_expect_success 'empty directory removal' ' test_path_is_file .git/logs/refs/heads/d1/r2 ' -test_expect_success 'symref empty directory removal' ' +test_expect_success REFFILES 'symref empty directory removal' ' git branch e1/e2/r1 HEAD && git branch e1/r2 HEAD && git checkout e1/e2/r1 && @@ -318,7 +321,7 @@ $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 "git update-ref -d $m && rm -rf .git/logs actual expect" && + test_when_finished "git update-ref -d $m && git reflog expire --expire=all --all && rm -rf actual expect" && test-tool ref-store main for-each-reflog-ent $m >actual && test_cmp actual expect ' @@ -348,7 +351,7 @@ $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 "git update-ref -d $m && rm -rf .git/logs actual expect" && + test_when_finished "git update-ref -d $m && git reflog expire --expire=all --all && rm -rf actual expect" && test-tool ref-store main for-each-reflog-ent $m >actual && test_cmp actual expect ' @@ -434,7 +437,8 @@ test_expect_success 'Query "main@{2005-05-28}" (past end of history)' ' test_grep -F "warning: log for ref $m unexpectedly ended on $ld" e ' -rm -f .git/$m .git/logs/$m expect +rm -f expect +git update-ref -d $m test_expect_success 'creating initial files' ' test_when_finished rm -f M && @@ -1635,7 +1639,7 @@ test_expect_success PIPE 'transaction flushes status updates' ' test_cmp expected actual ' -test_expect_success 'directory not created deleting packed ref' ' +test_expect_success REFFILES 'directory not created deleting packed ref' ' git branch d1/d2/r1 HEAD && git pack-refs --all && test_path_is_missing .git/refs/heads/d1/d2 && diff --git a/t/t1430-bad-ref-name.sh b/t/t1430-bad-ref-name.sh index 68cc9e73d0..0c00118c2b 100755 --- a/t/t1430-bad-ref-name.sh +++ b/t/t1430-bad-ref-name.sh @@ -164,9 +164,9 @@ test_expect_success 'rev-parse skips symref pointing to broken name' ' test_expect_success 'for-each-ref emits warnings for broken names' ' 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-tool ref-store main create-symref refs/heads/badname refs/heads/broken...ref && 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-tool ref-store main create-symref refs/heads/broken...symref refs/heads/main && 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 && @@ -257,7 +257,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-tool ref-store main create-symref refs/heads/broken...symref refs/heads/main && test_when_finished "test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...symref" && test_ref_exists refs/heads/broken...symref && git update-ref --no-deref -d refs/heads/broken...symref >output 2>error && @@ -267,7 +267,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-tool ref-store main create-symref refs/heads/broken...symref refs/heads/main && test_when_finished "test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...symref" && test_ref_exists refs/heads/broken...symref && git branch -d broken...symref >output 2>error && @@ -277,7 +277,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-tool ref-store main create-symref refs/heads/broken...symref refs/heads/idonotexist && test_when_finished "test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...symref" && test_ref_exists refs/heads/broken...symref && git update-ref --no-deref -d refs/heads/broken...symref >output 2>error && @@ -287,7 +287,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-tool ref-store main create-symref refs/heads/broken...symref refs/heads/idonotexist && test_when_finished "test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...symref" && test_ref_exists refs/heads/broken...symref && git branch -d broken...symref >output 2>error && diff --git a/t/t1450-fsck.sh b/t/t1450-fsck.sh index 0e3e87d37a..8a456b1142 100755 --- a/t/t1450-fsck.sh +++ b/t/t1450-fsck.sh @@ -15,6 +15,7 @@ test_expect_success setup ' git config --unset i18n.commitencoding && git checkout HEAD^0 && test_commit B fileB two && + orig_head=$(git rev-parse HEAD) && git tag -d A B && git reflog expire --expire=now --all ' @@ -115,15 +116,15 @@ test_expect_success 'zlib corrupt loose object output ' ' ' test_expect_success 'branch pointing to non-commit' ' - git rev-parse HEAD^{tree} >.git/refs/heads/invalid && + tree_oid=$(git rev-parse --verify HEAD^{tree}) && test_when_finished "git update-ref -d refs/heads/invalid" && + test-tool ref-store main update-ref msg refs/heads/invalid $tree_oid $ZERO_OID REF_SKIP_OID_VERIFICATION && test_must_fail git fsck 2>out && test_grep "not a commit" out ' -test_expect_success 'HEAD link pointing at a funny object' ' - test_when_finished "mv .git/SAVED_HEAD .git/HEAD" && - mv .git/HEAD .git/SAVED_HEAD && +test_expect_success REFFILES 'HEAD link pointing at a funny object' ' + test_when_finished "git update-ref HEAD $orig_head" && echo $ZERO_OID >.git/HEAD && # avoid corrupt/broken HEAD from interfering with repo discovery test_must_fail env GIT_DIR=.git git fsck 2>out && @@ -131,27 +132,25 @@ test_expect_success 'HEAD link pointing at a funny object' ' ' test_expect_success 'HEAD link pointing at a funny place' ' - test_when_finished "mv .git/SAVED_HEAD .git/HEAD" && - mv .git/HEAD .git/SAVED_HEAD && - echo "ref: refs/funny/place" >.git/HEAD && + test_when_finished "git update-ref --no-deref HEAD $orig_head" && + test-tool ref-store main create-symref HEAD refs/funny/place && # avoid corrupt/broken HEAD from interfering with repo discovery test_must_fail env GIT_DIR=.git git fsck 2>out && test_grep "HEAD points to something strange" out ' -test_expect_success 'HEAD link pointing at a funny object (from different wt)' ' - test_when_finished "mv .git/SAVED_HEAD .git/HEAD" && - test_when_finished "rm -rf .git/worktrees wt" && +test_expect_success REFFILES 'HEAD link pointing at a funny object (from different wt)' ' + test_when_finished "git update-ref HEAD $orig_head" && + test_when_finished "git worktree remove -f wt" && git worktree add wt && - mv .git/HEAD .git/SAVED_HEAD && echo $ZERO_OID >.git/HEAD && # avoid corrupt/broken HEAD from interfering with repo discovery test_must_fail git -C wt fsck 2>out && test_grep "main-worktree/HEAD: detached HEAD points" out ' -test_expect_success 'other worktree HEAD link pointing at a funny object' ' - test_when_finished "rm -rf .git/worktrees other" && +test_expect_success REFFILES 'other worktree HEAD link pointing at a funny object' ' + test_when_finished "git worktree remove -f other" && git worktree add other && echo $ZERO_OID >.git/worktrees/other/HEAD && test_must_fail git fsck 2>out && @@ -159,17 +158,18 @@ test_expect_success 'other worktree HEAD link pointing at a funny object' ' ' test_expect_success 'other worktree HEAD link pointing at missing object' ' - test_when_finished "rm -rf .git/worktrees other" && + test_when_finished "git worktree remove -f other" && git worktree add other && - echo "Contents missing from repo" | git hash-object --stdin >.git/worktrees/other/HEAD && + object_id=$(echo "Contents missing from repo" | git hash-object --stdin) && + test-tool -C other ref-store main update-ref msg HEAD $object_id "" REF_NO_DEREF,REF_SKIP_OID_VERIFICATION && test_must_fail git fsck 2>out && test_grep "worktrees/other/HEAD: invalid sha1 pointer" out ' test_expect_success 'other worktree HEAD link pointing at a funny place' ' - test_when_finished "rm -rf .git/worktrees other" && + test_when_finished "git worktree remove -f other" && git worktree add other && - echo "ref: refs/funny/place" >.git/worktrees/other/HEAD && + git -C other symbolic-ref HEAD refs/funny/place && test_must_fail git fsck 2>out && test_grep "worktrees/other/HEAD points to something strange" out ' @@ -391,7 +391,7 @@ test_expect_success 'tag pointing to nonexistent' ' tag=$(git hash-object -t tag -w --stdin <invalid-tag) && test_when_finished "remove_object $tag" && - echo $tag >.git/refs/tags/invalid && + git update-ref refs/tags/invalid $tag && test_when_finished "git update-ref -d refs/tags/invalid" && test_must_fail git fsck --tags >out && test_grep "broken link" out @@ -411,7 +411,7 @@ test_expect_success 'tag pointing to something else than its type' ' tag=$(git hash-object -t tag -w --stdin <wrong-tag) && test_when_finished "remove_object $tag" && - echo $tag >.git/refs/tags/wrong && + git update-ref refs/tags/wrong $tag && test_when_finished "git update-ref -d refs/tags/wrong" && test_must_fail git fsck --tags ' @@ -428,7 +428,7 @@ test_expect_success 'tag with incorrect tag name & missing tagger' ' tag=$(git hash-object --literally -t tag -w --stdin <wrong-tag) && test_when_finished "remove_object $tag" && - echo $tag >.git/refs/tags/wrong && + git update-ref refs/tags/wrong $tag && test_when_finished "git update-ref -d refs/tags/wrong" && git fsck --tags 2>out && @@ -452,7 +452,7 @@ test_expect_success 'tag with bad tagger' ' tag=$(git hash-object --literally -t tag -w --stdin <wrong-tag) && test_when_finished "remove_object $tag" && - echo $tag >.git/refs/tags/wrong && + git update-ref refs/tags/wrong $tag && test_when_finished "git update-ref -d refs/tags/wrong" && test_must_fail git fsck --tags 2>out && test_grep "error in tag .*: invalid author/committer" out @@ -471,7 +471,7 @@ test_expect_success 'tag with NUL in header' ' tag=$(git hash-object --literally -t tag -w --stdin <tag-NUL-header) && test_when_finished "remove_object $tag" && - echo $tag >.git/refs/tags/wrong && + git update-ref refs/tags/wrong $tag && test_when_finished "git update-ref -d refs/tags/wrong" && test_must_fail git fsck --tags 2>out && test_grep "error in tag $tag.*unterminated header: NUL at offset" out diff --git a/t/t2011-checkout-invalid-head.sh b/t/t2011-checkout-invalid-head.sh index d9997e7b6b..3c8135831b 100755 --- a/t/t2011-checkout-invalid-head.sh +++ b/t/t2011-checkout-invalid-head.sh @@ -18,18 +18,18 @@ test_expect_success 'checkout should not start branch from a tree' ' test_must_fail git checkout -b newbranch main^{tree} ' -test_expect_success 'checkout main from invalid HEAD' ' +test_expect_success REFFILES 'checkout main from invalid HEAD' ' echo $ZERO_OID >.git/HEAD && git checkout main -- ' -test_expect_success 'checkout notices failure to lock HEAD' ' +test_expect_success REFFILES 'checkout notices failure to lock HEAD' ' test_when_finished "rm -f .git/HEAD.lock" && >.git/HEAD.lock && test_must_fail git checkout -b other ' -test_expect_success 'create ref directory/file conflict scenario' ' +test_expect_success REFFILES 'create ref directory/file conflict scenario' ' git update-ref refs/heads/outer/inner main && # do not rely on symbolic-ref to get a known state, @@ -39,26 +39,26 @@ test_expect_success 'create ref directory/file conflict scenario' ' } ' -test_expect_success 'checkout away from d/f HEAD (unpacked, to branch)' ' +test_expect_success REFFILES 'checkout away from d/f HEAD (unpacked, to branch)' ' reset_to_df && git checkout main ' -test_expect_success 'checkout away from d/f HEAD (unpacked, to detached)' ' +test_expect_success REFFILES 'checkout away from d/f HEAD (unpacked, to detached)' ' reset_to_df && git checkout --detach main ' -test_expect_success 'pack refs' ' +test_expect_success REFFILES 'pack refs' ' git pack-refs --all --prune ' -test_expect_success 'checkout away from d/f HEAD (packed, to branch)' ' +test_expect_success REFFILES 'checkout away from d/f HEAD (packed, to branch)' ' reset_to_df && git checkout main ' -test_expect_success 'checkout away from d/f HEAD (packed, to detached)' ' +test_expect_success REFFILES 'checkout away from d/f HEAD (packed, to detached)' ' reset_to_df && git checkout --detach main ' diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh index c54fd9ea06..6a316f081e 100755 --- a/t/t3200-branch.sh +++ b/t/t3200-branch.sh @@ -28,7 +28,7 @@ test_expect_success 'git branch --help should not have created a bogus branch' ' test_ref_missing refs/heads/--help ' -test_expect_success 'branch -h in broken repository' ' +test_expect_success REFFILES 'branch -h in broken repository' ' mkdir broken && ( cd broken && @@ -76,14 +76,14 @@ test_expect_success 'git branch HEAD should fail' ' ' cat >expect <<EOF -$ZERO_OID $HEAD $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150200 +0000 branch: Created from main +$HEAD refs/heads/d/e/f@{0}: branch: Created from main EOF test_expect_success 'git branch --create-reflog d/e/f should create a branch and a log' ' GIT_COMMITTER_DATE="2005-05-26 23:30" \ git -c core.logallrefupdates=false branch --create-reflog d/e/f && test_ref_exists refs/heads/d/e/f && - test_path_is_file .git/logs/refs/heads/d/e/f && - test_cmp expect .git/logs/refs/heads/d/e/f + git reflog show --no-abbrev-commit refs/heads/d/e/f >actual && + test_cmp expect actual ' test_expect_success 'git branch -d d/e/f should delete a branch and a log' ' @@ -203,10 +203,9 @@ test_expect_success 'git branch -M baz bam should succeed when baz is checked ou test $(git rev-parse --abbrev-ref HEAD) = bam ' -test_expect_success 'git branch -M baz bam should add entries to .git/logs/HEAD' ' - msg="Branch: renamed refs/heads/baz to refs/heads/bam" && - grep " $ZERO_OID.*$msg$" .git/logs/HEAD && - grep "^$ZERO_OID.*$msg$" .git/logs/HEAD +test_expect_success 'git branch -M baz bam should add entries to HEAD reflog' ' + git reflog show HEAD >actual && + grep "HEAD@{0}: Branch: renamed refs/heads/baz to refs/heads/bam" actual ' test_expect_success 'git branch -M should leave orphaned HEAD alone' ' @@ -215,17 +214,20 @@ test_expect_success 'git branch -M should leave orphaned HEAD alone' ' cd orphan && test_commit initial && git checkout --orphan lonely && - grep lonely .git/HEAD && + git symbolic-ref HEAD >expect && + echo refs/heads/lonely >actual && + test_cmp expect actual && test_ref_missing refs/head/lonely && git branch -M main mistress && - grep lonely .git/HEAD + git symbolic-ref HEAD >expect && + test_cmp expect actual ) ' test_expect_success 'resulting reflog can be shown by log -g' ' oid=$(git rev-parse HEAD) && cat >expect <<-EOF && - HEAD@{0} $oid $msg + HEAD@{0} $oid Branch: renamed refs/heads/baz to refs/heads/bam HEAD@{2} $oid checkout: moving from foo to baz EOF git log -g --format="%gd %H %gs" -2 HEAD >actual && @@ -243,7 +245,7 @@ test_expect_success 'git branch -M baz bam should succeed when baz is checked ou git worktree prune ' -test_expect_success 'git branch -M fails if updating any linked working tree fails' ' +test_expect_success REFFILES 'git branch -M fails if updating any linked working tree fails' ' git worktree add -b baz bazdir1 && git worktree add -f bazdir2 baz && touch .git/worktrees/bazdir1/HEAD.lock && @@ -699,7 +701,8 @@ test_expect_success 'git branch -C c1 c2 should succeed when c1 is checked out' test_expect_success 'git branch -C c1 c2 should never touch HEAD' ' msg="Branch: copied refs/heads/c1 to refs/heads/c2" && - ! grep "$msg$" .git/logs/HEAD + git reflog HEAD >actual && + ! grep "$msg$" actual ' test_expect_success 'git branch -C main should work when main is checked out' ' @@ -809,7 +812,7 @@ test_expect_success 'deleting a symref' ' test_expect_success 'deleting a dangling symref' ' git symbolic-ref refs/heads/dangling-symref nowhere && - test_path_is_file .git/refs/heads/dangling-symref && + git symbolic-ref --no-recurse refs/heads/dangling-symref && echo "Deleted branch dangling-symref (was nowhere)." >expect && git branch -d dangling-symref >actual && test_ref_missing refs/heads/dangling-symref && @@ -833,14 +836,14 @@ test_expect_success 'renaming a symref is not allowed' ' test_ref_missing refs/heads/new-topic ' -test_expect_success SYMLINKS 'git branch -m u v should fail when the reflog for u is a symlink' ' +test_expect_success SYMLINKS,REFFILES 'git branch -m u v should fail when the reflog for u is a symlink' ' git branch --create-reflog u && mv .git/logs/refs/heads/u real-u && ln -s real-u .git/logs/refs/heads/u && test_must_fail git branch -m u v ' -test_expect_success SYMLINKS 'git branch -m with symlinked .git/refs' ' +test_expect_success SYMLINKS,REFFILES 'git branch -m with symlinked .git/refs' ' test_when_finished "rm -rf subdir" && git init --bare subdir && @@ -1140,14 +1143,14 @@ test_expect_success '--set-upstream-to notices an error to set branch as own ups # Keep this test last, as it changes the current branch cat >expect <<EOF -$ZERO_OID $HEAD $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150200 +0000 branch: Created from main +$HEAD refs/heads/g/h/i@{0}: branch: Created from main EOF test_expect_success 'git checkout -b g/h/i -l should create a branch and a log' ' GIT_COMMITTER_DATE="2005-05-26 23:30" \ git checkout -b g/h/i -l main && test_ref_exists refs/heads/g/h/i && - test_path_is_file .git/logs/refs/heads/g/h/i && - test_cmp expect .git/logs/refs/heads/g/h/i + git reflog show --no-abbrev-commit refs/heads/g/h/i >actual && + test_cmp expect actual ' test_expect_success 'checkout -b makes reflog by default' ' @@ -1573,9 +1576,10 @@ test_expect_success 'tracking with unexpected .fetch refspec' ' test_expect_success 'configured committerdate sort' ' git init -b main sort && + test_config -C sort branch.sort "committerdate" && + ( cd sort && - git config branch.sort committerdate && test_commit initial && git checkout -b a && test_commit a && @@ -1595,9 +1599,10 @@ test_expect_success 'configured committerdate sort' ' ' test_expect_success 'option override configured sort' ' + test_config -C sort branch.sort "committerdate" && + ( cd sort && - git config branch.sort committerdate && git branch --sort=refname >actual && cat >expect <<-\EOF && a @@ -1609,10 +1614,70 @@ test_expect_success 'option override configured sort' ' ) ' +test_expect_success '--no-sort cancels config sort keys' ' + test_config -C sort branch.sort "-refname" && + + ( + cd sort && + + # objecttype is identical for all of them, so sort falls back on + # default (ascending refname) + git branch \ + --no-sort \ + --sort="objecttype" >actual && + cat >expect <<-\EOF && + a + * b + c + main + EOF + test_cmp expect actual + ) + +' + +test_expect_success '--no-sort cancels command line sort keys' ' + ( + cd sort && + + # objecttype is identical for all of them, so sort falls back on + # default (ascending refname) + git branch \ + --sort="-refname" \ + --no-sort \ + --sort="objecttype" >actual && + cat >expect <<-\EOF && + a + * b + c + main + EOF + test_cmp expect actual + ) +' + +test_expect_success '--no-sort without subsequent --sort prints expected branches' ' + ( + cd sort && + + # Sort the results with `sort` for a consistent comparison + # against expected + git branch --no-sort | sort >actual && + cat >expect <<-\EOF && + a + c + main + * b + EOF + test_cmp expect actual + ) +' + test_expect_success 'invalid sort parameter in configuration' ' + test_config -C sort branch.sort "v:notvalid" && + ( cd sort && - git config branch.sort "v:notvalid" && # this works in the "listing" mode, so bad sort key # is a dying offence. diff --git a/t/t3400-rebase.sh b/t/t3400-rebase.sh index 24a539c662..57f1392926 100755 --- a/t/t3400-rebase.sh +++ b/t/t3400-rebase.sh @@ -424,7 +424,7 @@ test_expect_success 'refuse to switch to branch checked out elsewhere' ' test_grep "already used by worktree at" err ' -test_expect_success MINGW,SYMLINKS_WINDOWS 'rebase when .git/logs is a symlink' ' +test_expect_success REFFILES,MINGW,SYMLINKS_WINDOWS 'rebase when .git/logs is a symlink' ' git checkout main && mv .git/logs actual_logs && cmd //c "mklink /D .git\logs ..\actual_logs" && diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh index c5f30554c6..64b641002e 100755 --- a/t/t3404-rebase-interactive.sh +++ b/t/t3404-rebase-interactive.sh @@ -2160,7 +2160,7 @@ test_expect_success '--update-refs: check failed ref update' ' # recorded in the update-refs file. We will force-update the # "second" ref, but "git branch -f" will not work because of # the lock in the update-refs file. - git rev-parse third >.git/refs/heads/second && + git update-ref refs/heads/second third && test_must_fail git rebase --continue 2>err && grep "update_ref failed for ref '\''refs/heads/second'\''" err && diff --git a/t/t3415-rebase-autosquash.sh b/t/t3415-rebase-autosquash.sh index a364530d76..fcc40d6fe1 100755 --- a/t/t3415-rebase-autosquash.sh +++ b/t/t3415-rebase-autosquash.sh @@ -43,7 +43,7 @@ test_auto_fixup () { git tag $1 && test_tick && - git rebase $2 -i HEAD^^^ && + git rebase $2 HEAD^^^ && git log --oneline >actual && if test -n "$no_squash" then @@ -61,15 +61,24 @@ test_auto_fixup () { } test_expect_success 'auto fixup (option)' ' - test_auto_fixup final-fixup-option --autosquash + test_auto_fixup fixup-option --autosquash && + test_auto_fixup fixup-option-i "--autosquash -i" ' -test_expect_success 'auto fixup (config)' ' +test_expect_success 'auto fixup (config true)' ' git config rebase.autosquash true && - test_auto_fixup final-fixup-config-true && + test_auto_fixup ! fixup-config-true && + test_auto_fixup fixup-config-true-i -i && test_auto_fixup ! fixup-config-true-no --no-autosquash && + test_auto_fixup ! fixup-config-true-i-no "-i --no-autosquash" +' + +test_expect_success 'auto fixup (config false)' ' git config rebase.autosquash false && - test_auto_fixup ! final-fixup-config-false + test_auto_fixup ! fixup-config-false && + test_auto_fixup ! fixup-config-false-i -i && + test_auto_fixup fixup-config-false-yes --autosquash && + test_auto_fixup fixup-config-false-i-yes "-i --autosquash" ' test_auto_squash () { @@ -87,7 +96,7 @@ test_auto_squash () { git commit -m "squash! first" -m "extra para for first" && git tag $1 && test_tick && - git rebase $2 -i HEAD^^^ && + git rebase $2 HEAD^^^ && git log --oneline >actual && if test -n "$no_squash" then @@ -105,15 +114,24 @@ test_auto_squash () { } test_expect_success 'auto squash (option)' ' - test_auto_squash final-squash --autosquash + test_auto_squash squash-option --autosquash && + test_auto_squash squash-option-i "--autosquash -i" ' -test_expect_success 'auto squash (config)' ' +test_expect_success 'auto squash (config true)' ' git config rebase.autosquash true && - test_auto_squash final-squash-config-true && + test_auto_squash ! squash-config-true && + test_auto_squash squash-config-true-i -i && test_auto_squash ! squash-config-true-no --no-autosquash && + test_auto_squash ! squash-config-true-i-no "-i --no-autosquash" +' + +test_expect_success 'auto squash (config false)' ' git config rebase.autosquash false && - test_auto_squash ! final-squash-config-false + test_auto_squash ! squash-config-false && + test_auto_squash ! squash-config-false-i -i && + test_auto_squash squash-config-false-yes --autosquash && + test_auto_squash squash-config-false-i-yes "-i --autosquash" ' test_expect_success 'misspelled auto squash' ' diff --git a/t/t3422-rebase-incompatible-options.sh b/t/t3422-rebase-incompatible-options.sh index 2eba00bdf5..b40f26250b 100755 --- a/t/t3422-rebase-incompatible-options.sh +++ b/t/t3422-rebase-incompatible-options.sh @@ -100,12 +100,6 @@ test_rebase_am_only () { test_must_fail git rebase $opt --root A " - test_expect_success "$opt incompatible with rebase.autosquash" " - git checkout B^0 && - test_must_fail git -c rebase.autosquash=true rebase $opt A 2>err && - grep -e --no-autosquash err - " - test_expect_success "$opt incompatible with rebase.rebaseMerges" " git checkout B^0 && test_must_fail git -c rebase.rebaseMerges=true rebase $opt A 2>err && @@ -118,12 +112,6 @@ test_rebase_am_only () { grep -e --no-update-refs err " - test_expect_success "$opt okay with overridden rebase.autosquash" " - test_when_finished \"git reset --hard B^0\" && - git checkout B^0 && - git -c rebase.autosquash=true rebase --no-autosquash $opt A - " - test_expect_success "$opt okay with overridden rebase.rebaseMerges" " test_when_finished \"git reset --hard B^0\" && git checkout B^0 && diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh new file mode 100755 index 0000000000..389670262e --- /dev/null +++ b/t/t3650-replay-basics.sh @@ -0,0 +1,198 @@ +#!/bin/sh + +test_description='basic git replay tests' + +GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main +export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME + +. ./test-lib.sh + +GIT_AUTHOR_NAME=author@name +GIT_AUTHOR_EMAIL=bogus@email@address +export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL + +test_expect_success 'setup' ' + test_commit A && + test_commit B && + + git switch -c topic1 && + test_commit C && + git switch -c topic2 && + test_commit D && + test_commit E && + git switch topic1 && + test_commit F && + git switch -c topic3 && + test_commit G && + test_commit H && + git switch -c topic4 main && + test_commit I && + test_commit J && + + git switch -c next main && + test_commit K && + git merge -m "Merge topic1" topic1 && + git merge -m "Merge topic2" topic2 && + git merge -m "Merge topic3" topic3 && + >evil && + git add evil && + git commit --amend && + git merge -m "Merge topic4" topic4 && + + git switch main && + test_commit L && + test_commit M && + + git switch -c conflict B && + test_commit C.conflict C.t conflict +' + +test_expect_success 'setup bare' ' + git clone --bare . bare +' + +test_expect_success 'using replay to rebase two branches, one on top of other' ' + git replay --onto main topic1..topic2 >result && + + test_line_count = 1 result && + + git log --format=%s $(cut -f 3 -d " " result) >actual && + test_write_lines E D M L B A >expect && + test_cmp expect actual && + + printf "update refs/heads/topic2 " >expect && + printf "%s " $(cut -f 3 -d " " result) >>expect && + git rev-parse topic2 >>expect && + + test_cmp expect result +' + +test_expect_success 'using replay on bare repo to rebase two branches, one on top of other' ' + git -C bare replay --onto main topic1..topic2 >result-bare && + test_cmp expect result-bare +' + +test_expect_success 'using replay to rebase with a conflict' ' + test_expect_code 1 git replay --onto topic1 B..conflict +' + +test_expect_success 'using replay on bare repo to rebase with a conflict' ' + test_expect_code 1 git -C bare replay --onto topic1 B..conflict +' + +test_expect_success 'using replay to perform basic cherry-pick' ' + # The differences between this test and previous ones are: + # --advance vs --onto + # 2nd field of result is refs/heads/main vs. refs/heads/topic2 + # 4th field of result is hash for main instead of hash for topic2 + + git replay --advance main topic1..topic2 >result && + + test_line_count = 1 result && + + git log --format=%s $(cut -f 3 -d " " result) >actual && + test_write_lines E D M L B A >expect && + test_cmp expect actual && + + printf "update refs/heads/main " >expect && + printf "%s " $(cut -f 3 -d " " result) >>expect && + git rev-parse main >>expect && + + test_cmp expect result +' + +test_expect_success 'using replay on bare repo to perform basic cherry-pick' ' + git -C bare replay --advance main topic1..topic2 >result-bare && + test_cmp expect result-bare +' + +test_expect_success 'replay on bare repo fails with both --advance and --onto' ' + test_must_fail git -C bare replay --advance main --onto main topic1..topic2 >result-bare +' + +test_expect_success 'replay fails when both --advance and --onto are omitted' ' + test_must_fail git replay topic1..topic2 >result +' + +test_expect_success 'using replay to also rebase a contained branch' ' + git replay --contained --onto main main..topic3 >result && + + test_line_count = 2 result && + cut -f 3 -d " " result >new-branch-tips && + + git log --format=%s $(head -n 1 new-branch-tips) >actual && + test_write_lines F C M L B A >expect && + test_cmp expect actual && + + git log --format=%s $(tail -n 1 new-branch-tips) >actual && + test_write_lines H G F C M L B A >expect && + test_cmp expect actual && + + printf "update refs/heads/topic1 " >expect && + printf "%s " $(head -n 1 new-branch-tips) >>expect && + git rev-parse topic1 >>expect && + printf "update refs/heads/topic3 " >>expect && + printf "%s " $(tail -n 1 new-branch-tips) >>expect && + git rev-parse topic3 >>expect && + + test_cmp expect result +' + +test_expect_success 'using replay on bare repo to also rebase a contained branch' ' + git -C bare replay --contained --onto main main..topic3 >result-bare && + test_cmp expect result-bare +' + +test_expect_success 'using replay to rebase multiple divergent branches' ' + git replay --onto main ^topic1 topic2 topic4 >result && + + test_line_count = 2 result && + cut -f 3 -d " " result >new-branch-tips && + + git log --format=%s $(head -n 1 new-branch-tips) >actual && + test_write_lines E D M L B A >expect && + test_cmp expect actual && + + git log --format=%s $(tail -n 1 new-branch-tips) >actual && + test_write_lines J I M L B A >expect && + test_cmp expect actual && + + printf "update refs/heads/topic2 " >expect && + printf "%s " $(head -n 1 new-branch-tips) >>expect && + git rev-parse topic2 >>expect && + printf "update refs/heads/topic4 " >>expect && + printf "%s " $(tail -n 1 new-branch-tips) >>expect && + git rev-parse topic4 >>expect && + + test_cmp expect result +' + +test_expect_success 'using replay on bare repo to rebase multiple divergent branches, including contained ones' ' + git -C bare replay --contained --onto main ^main topic2 topic3 topic4 >result && + + test_line_count = 4 result && + cut -f 3 -d " " result >new-branch-tips && + + >expect && + for i in 2 1 3 4 + do + printf "update refs/heads/topic$i " >>expect && + printf "%s " $(grep topic$i result | cut -f 3 -d " ") >>expect && + git -C bare rev-parse topic$i >>expect || return 1 + done && + + test_cmp expect result && + + test_write_lines F C M L B A >expect1 && + test_write_lines E D C M L B A >expect2 && + test_write_lines H G F C M L B A >expect3 && + test_write_lines J I M L B A >expect4 && + + for i in 1 2 3 4 + do + git -C bare log --format=%s $(grep topic$i result | cut -f 3 -d " ") >actual && + test_cmp expect$i actual || return 1 + done +' + +test_done diff --git a/t/t4013-diff-various.sh b/t/t4013-diff-various.sh index 5cc17c2e0d..c4d508e00a 100755 --- a/t/t4013-diff-various.sh +++ b/t/t4013-diff-various.sh @@ -522,7 +522,7 @@ test_expect_success 'log -S requires an argument' ' ' test_expect_success 'diff --cached on unborn branch' ' - echo ref: refs/heads/unborn >.git/HEAD && + git symbolic-ref HEAD refs/heads/unborn && git diff --cached >result && process_diffs result >actual && process_diffs "$TEST_DIRECTORY/t4013/diff.diff_--cached" >expected && diff --git a/t/t4014-format-patch.sh b/t/t4014-format-patch.sh index 5ced27ed45..e37a1411ee 100755 --- a/t/t4014-format-patch.sh +++ b/t/t4014-format-patch.sh @@ -1906,6 +1906,16 @@ body" && grep "^body$" actual ' +test_expect_success 'cover letter with --cover-from-description subject (UTF-8 subject line)' ' + test_config branch.rebuild-1.description "Café? + +body" && + git checkout rebuild-1 && + git format-patch --stdout --cover-letter --cover-from-description subject --encode-email-headers main >actual && + grep "^Subject: \[PATCH 0/2\] =?UTF-8?q?Caf=C3=A9=3F?=$" actual && + ! grep "Café" actual +' + test_expect_success 'cover letter with format.coverFromDescription = auto (short subject line)' ' test_config branch.rebuild-1.description "config subject diff --git a/t/t4202-log.sh b/t/t4202-log.sh index 708636671a..ddd205f98a 100755 --- a/t/t4202-log.sh +++ b/t/t4202-log.sh @@ -2265,7 +2265,7 @@ test_expect_success REFFILES 'log diagnoses bogus HEAD hash' ' test_expect_success REFFILES 'log diagnoses bogus HEAD symref' ' git init empty && - echo "ref: refs/heads/invalid.lock" > empty/.git/HEAD && + test-tool -C empty ref-store main create-symref HEAD refs/heads/invalid.lock && test_must_fail git -C empty log 2>stderr && test_grep broken stderr && test_must_fail git -C empty log --default totally-bogus 2>stderr && diff --git a/t/t4207-log-decoration-colors.sh b/t/t4207-log-decoration-colors.sh index 21986a866d..73ea9e5155 100755 --- a/t/t4207-log-decoration-colors.sh +++ b/t/t4207-log-decoration-colors.sh @@ -70,8 +70,14 @@ ${c_tag}tag: ${c_reset}${c_tag}A${c_reset}${c_commit})${c_reset} A cmp_filtered_decorations ' +remove_replace_refs () { + git for-each-ref 'refs/replace*/**' --format='delete %(refname)' >in && + git update-ref --stdin <in && + rm in +} + test_expect_success 'test coloring with replace-objects' ' - test_when_finished rm -rf .git/refs/replace* && + test_when_finished remove_replace_refs && test_commit C && test_commit D && @@ -99,7 +105,7 @@ EOF ' test_expect_success 'test coloring with grafted commit' ' - test_when_finished rm -rf .git/refs/replace* && + test_when_finished remove_replace_refs && git replace --graft HEAD HEAD~2 && diff --git a/t/t5318-commit-graph.sh b/t/t5318-commit-graph.sh index 4c751a6871..a2b4442660 100755 --- a/t/t5318-commit-graph.sh +++ b/t/t5318-commit-graph.sh @@ -540,17 +540,17 @@ test_expect_success 'detect low chunk count' ' test_expect_success 'detect missing OID fanout chunk' ' corrupt_graph_and_verify $GRAPH_BYTE_OID_FANOUT_ID "\0" \ - "missing the OID Fanout chunk" + "commit-graph required OID fanout chunk missing or corrupted" ' test_expect_success 'detect missing OID lookup chunk' ' corrupt_graph_and_verify $GRAPH_BYTE_OID_LOOKUP_ID "\0" \ - "missing the OID Lookup chunk" + "commit-graph required OID lookup chunk missing or corrupted" ' test_expect_success 'detect missing commit data chunk' ' corrupt_graph_and_verify $GRAPH_BYTE_COMMIT_DATA_ID "\0" \ - "missing the Commit Data chunk" + "commit-graph required commit data chunk missing or corrupted" ' test_expect_success 'detect incorrect fanout' ' @@ -560,7 +560,7 @@ test_expect_success 'detect incorrect fanout' ' test_expect_success 'detect incorrect fanout final value' ' corrupt_graph_and_verify $GRAPH_BYTE_FANOUT2 "\01" \ - "oid table and fanout disagree on size" + "OID lookup chunk is the wrong size" ' test_expect_success 'detect incorrect OID order' ' @@ -842,7 +842,7 @@ test_expect_success 'reader notices too-small oid fanout chunk' ' check_corrupt_chunk OIDF clear $(printf "000000%02x" $(test_seq 250)) && cat >expect.err <<-\EOF && error: commit-graph oid fanout chunk is wrong size - error: commit-graph is missing the OID Fanout chunk + error: commit-graph required OID fanout chunk missing or corrupted EOF test_cmp expect.err err ' @@ -850,7 +850,8 @@ test_expect_success 'reader notices too-small oid fanout chunk' ' test_expect_success 'reader notices fanout/lookup table mismatch' ' check_corrupt_chunk OIDF 1020 "FFFFFFFF" && cat >expect.err <<-\EOF && - error: commit-graph oid table and fanout disagree on size + error: commit-graph OID lookup chunk is the wrong size + error: commit-graph required OID lookup chunk missing or corrupted EOF test_cmp expect.err err ' @@ -866,6 +867,7 @@ test_expect_success 'reader notices out-of-bounds fanout' ' check_corrupt_chunk OIDF 0 $(printf "%02x000000" $(test_seq 0 254)) && cat >expect.err <<-\EOF && error: commit-graph fanout values out of order + error: commit-graph required OID fanout chunk missing or corrupted EOF test_cmp expect.err err ' @@ -874,7 +876,7 @@ test_expect_success 'reader notices too-small commit data chunk' ' check_corrupt_chunk CDAT clear 00000000 && cat >expect.err <<-\EOF && error: commit-graph commit data chunk is wrong size - error: commit-graph is missing the Commit Data chunk + error: commit-graph required commit data chunk missing or corrupted EOF test_cmp expect.err err ' diff --git a/t/t5319-multi-pack-index.sh b/t/t5319-multi-pack-index.sh index c4c6060cee..c20aafe99a 100755 --- a/t/t5319-multi-pack-index.sh +++ b/t/t5319-multi-pack-index.sh @@ -1157,4 +1157,18 @@ test_expect_success 'reader notices too-small revindex chunk' ' test_cmp expect.err err ' +test_expect_success 'reader notices out-of-bounds fanout' ' + # This is similar to the out-of-bounds fanout test in t5318. The values + # in adjacent entries should be large but not identical (they + # are used as hi/lo starts for a binary search, which would then abort + # immediately). + corrupt_chunk OIDF 0 $(printf "%02x000000" $(test_seq 0 254)) && + test_must_fail git log 2>err && + cat >expect <<-\EOF && + error: oid fanout out of order: fanout[254] = fe000000 > 5c = fanout[255] + fatal: multi-pack-index required OID fanout chunk missing or corrupted + EOF + test_cmp expect err +' + test_done diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh index dcadd56d3a..79592a3b0a 100755 --- a/t/t5510-fetch.sh +++ b/t/t5510-fetch.sh @@ -169,6 +169,7 @@ test_expect_success REFFILES 'fetch --prune fails to delete branches' ' git clone . prune-fail && cd prune-fail && git update-ref refs/remotes/origin/extrabranch main && + git pack-refs --all && : this will prevent --prune from locking packed-refs for deleting refs, but adding loose refs still succeeds && >.git/packed-refs.new && diff --git a/t/t5526-fetch-submodules.sh b/t/t5526-fetch-submodules.sh index 26e933f93a..7ab220fa31 100755 --- a/t/t5526-fetch-submodules.sh +++ b/t/t5526-fetch-submodules.sh @@ -771,7 +771,7 @@ test_expect_success 'fetching submodule into a broken repository' ' git -C dst fetch --recurse-submodules && # Break the receiving submodule - rm -f dst/sub/.git/HEAD && + test-tool -C dst/sub ref-store main delete-refs REF_NO_DEREF msg HEAD && # NOTE: without the fix the following tests will recurse forever! # They should terminate with an error. diff --git a/t/t5562/invoke-with-content-length.pl b/t/t5562/invoke-with-content-length.pl index 718dd9b49d..9babb9a375 100644 --- a/t/t5562/invoke-with-content-length.pl +++ b/t/t5562/invoke-with-content-length.pl @@ -1,4 +1,4 @@ -use 5.008; +use 5.008001; use strict; use warnings; diff --git a/t/t5605-clone-local.sh b/t/t5605-clone-local.sh index 946c575188..a3055869bc 100755 --- a/t/t5605-clone-local.sh +++ b/t/t5605-clone-local.sh @@ -65,7 +65,7 @@ test_expect_success 'Even without -l, local will make a hardlink' ' ' test_expect_success 'local clone of repo with nonexistent ref in HEAD' ' - echo "ref: refs/heads/nonexistent" > a.git/HEAD && + git -C a.git symbolic-ref HEAD refs/heads/nonexistent && git clone a d && (cd d && git fetch && @@ -157,7 +157,7 @@ test_expect_success 'cloning locally respects "-u" for fetching refs' ' test_must_fail git clone --bare -u false a should_not_work.git ' -test_expect_success 'local clone from repo with corrupt refs fails gracefully' ' +test_expect_success REFFILES 'local clone from repo with corrupt refs fails gracefully' ' git init corrupt && test_commit -C corrupt one && echo a >corrupt/.git/refs/heads/topic && diff --git a/t/t5702-protocol-v2.sh b/t/t5702-protocol-v2.sh index 3c0c6047d5..6ef4971845 100755 --- a/t/t5702-protocol-v2.sh +++ b/t/t5702-protocol-v2.sh @@ -221,7 +221,9 @@ test_expect_success 'clone of empty repo propagates name of default branch' ' GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME= \ git -c init.defaultBranch=main -c protocol.version=2 \ clone "file://$(pwd)/file_empty_parent" file_empty_child && - grep "refs/heads/mydefaultbranch" file_empty_child/.git/HEAD + echo refs/heads/mydefaultbranch >expect && + git -C file_empty_child symbolic-ref HEAD >actual && + test_cmp expect actual ' test_expect_success '...but not if explicitly forbidden by config' ' @@ -234,7 +236,9 @@ test_expect_success '...but not if explicitly forbidden by config' ' GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME= \ git -c init.defaultBranch=main -c protocol.version=2 \ clone "file://$(pwd)/file_empty_parent" file_empty_child && - ! grep "refs/heads/mydefaultbranch" file_empty_child/.git/HEAD + echo refs/heads/main >expect && + git -C file_empty_child symbolic-ref HEAD >actual && + test_cmp expect actual ' test_expect_success 'bare clone propagates empty default branch' ' @@ -247,7 +251,9 @@ test_expect_success 'bare clone propagates empty default branch' ' git -c init.defaultBranch=main -c protocol.version=2 \ clone --bare \ "file://$(pwd)/file_empty_parent" file_empty_child.git && - grep "refs/heads/mydefaultbranch" file_empty_child.git/HEAD + echo "refs/heads/mydefaultbranch" >expect && + git -C file_empty_child.git symbolic-ref HEAD >actual && + test_cmp expect actual ' test_expect_success 'clone propagates unborn HEAD from non-empty repo' ' @@ -265,7 +271,9 @@ test_expect_success 'clone propagates unborn HEAD from non-empty repo' ' git -c init.defaultBranch=main -c protocol.version=2 \ clone "file://$(pwd)/file_unborn_parent" \ file_unborn_child 2>stderr && - grep "refs/heads/mydefaultbranch" file_unborn_child/.git/HEAD && + echo "refs/heads/mydefaultbranch" >expect && + git -C file_unborn_child symbolic-ref HEAD >actual && + test_cmp expect actual && grep "warning: remote HEAD refers to nonexistent ref" stderr ' @@ -295,7 +303,9 @@ test_expect_success 'bare clone propagates unborn HEAD from non-empty repo' ' git -c init.defaultBranch=main -c protocol.version=2 \ clone --bare "file://$(pwd)/file_unborn_parent" \ file_unborn_child.git 2>stderr && - grep "refs/heads/mydefaultbranch" file_unborn_child.git/HEAD && + echo "refs/heads/mydefaultbranch" >expect && + git -C file_unborn_child.git symbolic-ref HEAD >actual && + test_cmp expect actual && ! grep "warning:" stderr ' @@ -315,7 +325,9 @@ test_expect_success 'defaulted HEAD uses remote branch if available' ' git -c init.defaultBranch=branchwithstuff -c protocol.version=2 \ clone "file://$(pwd)/file_unborn_parent" \ file_unborn_child 2>stderr && - grep "refs/heads/branchwithstuff" file_unborn_child/.git/HEAD && + echo "refs/heads/branchwithstuff" >expect && + git -C file_unborn_child symbolic-ref HEAD >actual && + test_cmp expect actual && test_path_is_file file_unborn_child/stuff.t && ! grep "warning:" stderr ' diff --git a/t/t6135-pathspec-with-attrs.sh b/t/t6135-pathspec-with-attrs.sh index a9c1e4e0ec..00b84ecba6 100755 --- a/t/t6135-pathspec-with-attrs.sh +++ b/t/t6135-pathspec-with-attrs.sh @@ -64,12 +64,24 @@ test_expect_success 'setup .gitattributes' ' fileSetLabel label fileValue label=foo fileWrongLabel label☺ + newFileA* labelA + newFileB* labelB EOF echo fileSetLabel label1 >sub/.gitattributes && git add .gitattributes sub/.gitattributes && git commit -m "add attributes" ' +test_expect_success 'setup .gitignore' ' + cat <<-\EOF >.gitignore && + actual + expect + pathspec_file + EOF + git add .gitignore && + git commit -m "add gitignore" +' + test_expect_success 'check specific set attr' ' cat <<-\EOF >expect && fileSetLabel @@ -150,6 +162,7 @@ test_expect_success 'check specific value attr (2)' ' test_expect_success 'check unspecified attr' ' cat <<-\EOF >expect && .gitattributes + .gitignore fileA fileAB fileAC @@ -175,6 +188,7 @@ test_expect_success 'check unspecified attr' ' test_expect_success 'check unspecified attr (2)' ' cat <<-\EOF >expect && HEAD:.gitattributes + HEAD:.gitignore HEAD:fileA HEAD:fileAB HEAD:fileAC @@ -200,6 +214,7 @@ test_expect_success 'check unspecified attr (2)' ' test_expect_success 'check multiple unspecified attr' ' cat <<-\EOF >expect && .gitattributes + .gitignore fileC fileNoLabel fileWrongLabel @@ -239,16 +254,99 @@ test_expect_success 'fail on multiple attr specifiers in one pathspec item' ' test_grep "Only one" actual ' -test_expect_success 'fail if attr magic is used places not implemented' ' +test_expect_success 'fail if attr magic is used in places not implemented' ' # The main purpose of this test is to check that we actually fail # when you attempt to use attr magic in commands that do not implement - # attr magic. This test does not advocate git-add to stay that way, - # though, but git-add is convenient as it has its own internal pathspec - # parsing. - test_must_fail git add ":(attr:labelB)" 2>actual && + # attr magic. This test does not advocate check-ignore to stay that way. + # When you teach the command to grok the pathspec, you need to find + # another command to replace it for the test. + test_must_fail git check-ignore ":(attr:labelB)" 2>actual && test_grep "magic not supported" actual ' +test_expect_success 'check that attr magic works for git stash push' ' + cat <<-\EOF >expect && + A sub/newFileA-foo + EOF + >sub/newFileA-foo && + >sub/newFileB-foo && + git stash push --include-untracked -- ":(exclude,attr:labelB)" && + git stash show --include-untracked --name-status >actual && + test_cmp expect actual +' + +test_expect_success 'check that attr magic works for git add --all' ' + cat <<-\EOF >expect && + sub/newFileA-foo + EOF + >sub/newFileA-foo && + >sub/newFileB-foo && + git add --all ":(exclude,attr:labelB)" && + git diff --name-only --cached >actual && + git restore -W -S . && + test_cmp expect actual +' + +test_expect_success 'check that attr magic works for git add -u' ' + cat <<-\EOF >expect && + sub/fileA + EOF + >sub/newFileA-foo && + >sub/newFileB-foo && + >sub/fileA && + >sub/fileB && + git add -u ":(exclude,attr:labelB)" && + git diff --name-only --cached >actual && + git restore -S -W . && rm sub/new* && + test_cmp expect actual +' + +test_expect_success 'check that attr magic works for git add <path>' ' + cat <<-\EOF >expect && + fileA + fileB + sub/fileA + EOF + >fileA && + >fileB && + >sub/fileA && + >sub/fileB && + git add ":(exclude,attr:labelB)sub/*" && + git diff --name-only --cached >actual && + git restore -S -W . && + test_cmp expect actual +' + +test_expect_success 'check that attr magic works for git -add .' ' + cat <<-\EOF >expect && + sub/fileA + EOF + >fileA && + >fileB && + >sub/fileA && + >sub/fileB && + cd sub && + git add . ":(exclude,attr:labelB)" && + cd .. && + git diff --name-only --cached >actual && + git restore -S -W . && + test_cmp expect actual +' + +test_expect_success 'check that attr magic works for git add --pathspec-from-file' ' + cat <<-\EOF >pathspec_file && + :(exclude,attr:labelB) + EOF + cat <<-\EOF >expect && + sub/newFileA-foo + EOF + >sub/newFileA-foo && + >sub/newFileB-foo && + git add --all --pathspec-from-file=pathspec_file && + git diff --name-only --cached >actual && + test_cmp expect actual +' + test_expect_success 'abort on giving invalid label on the command line' ' test_must_fail git ls-files . ":(attr:☺)" ' diff --git a/t/t6300-for-each-ref.sh b/t/t6300-for-each-ref.sh index 00a060df0b..54e2281259 100755 --- a/t/t6300-for-each-ref.sh +++ b/t/t6300-for-each-ref.sh @@ -1335,6 +1335,27 @@ test_expect_success '--no-sort cancels the previous sort keys' ' test_cmp expected actual ' +test_expect_success '--no-sort without subsequent --sort prints expected refs' ' + cat >expected <<-\EOF && + refs/tags/multi-ref1-100000-user1 + refs/tags/multi-ref1-100000-user2 + refs/tags/multi-ref1-200000-user1 + refs/tags/multi-ref1-200000-user2 + refs/tags/multi-ref2-100000-user1 + refs/tags/multi-ref2-100000-user2 + refs/tags/multi-ref2-200000-user1 + refs/tags/multi-ref2-200000-user2 + EOF + + # Sort the results with `sort` for a consistent comparison against + # expected + git for-each-ref \ + --format="%(refname)" \ + --no-sort \ + "refs/tags/multi-*" | sort >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 && @@ -1818,6 +1839,28 @@ test_expect_success 'git for-each-ref with non-existing refs' ' test_must_be_empty actual ' +test_expect_success 'git for-each-ref with nested tags' ' + git tag -am "Normal tag" nested/base HEAD && + git tag -am "Nested tag" nested/nest1 refs/tags/nested/base && + git tag -am "Double nested tag" nested/nest2 refs/tags/nested/nest1 && + + head_oid="$(git rev-parse HEAD)" && + base_tag_oid="$(git rev-parse refs/tags/nested/base)" && + nest1_tag_oid="$(git rev-parse refs/tags/nested/nest1)" && + nest2_tag_oid="$(git rev-parse refs/tags/nested/nest2)" && + + cat >expect <<-EOF && + refs/tags/nested/base $base_tag_oid tag $head_oid commit + refs/tags/nested/nest1 $nest1_tag_oid tag $head_oid commit + refs/tags/nested/nest2 $nest2_tag_oid tag $head_oid commit + EOF + + git for-each-ref \ + --format="%(refname) %(objectname) %(objecttype) %(*objectname) %(*objecttype)" \ + refs/tags/nested/ >actual && + test_cmp expect actual +' + GRADE_FORMAT="%(signature:grade)%0a%(signature:key)%0a%(signature:signer)%0a%(signature:fingerprint)%0a%(signature:primarykeyfingerprint)" TRUSTLEVEL_FORMAT="%(signature:trustlevel)%0a%(signature:key)%0a%(signature:signer)%0a%(signature:fingerprint)%0a%(signature:primarykeyfingerprint)" diff --git a/t/t6302-for-each-ref-filter.sh b/t/t6302-for-each-ref-filter.sh index af223e44d6..82f3d1ea0f 100755 --- a/t/t6302-for-each-ref-filter.sh +++ b/t/t6302-for-each-ref-filter.sh @@ -45,8 +45,8 @@ test_expect_success 'check signed tags with --points-at' ' sed -e "s/Z$//" >expect <<-\EOF && refs/heads/side Z refs/tags/annotated-tag four - refs/tags/doubly-annotated-tag An annotated tag - refs/tags/doubly-signed-tag A signed tag + refs/tags/doubly-annotated-tag four + refs/tags/doubly-signed-tag four refs/tags/four Z refs/tags/signed-tag four EOF diff --git a/t/t6429-merge-sequence-rename-caching.sh b/t/t6429-merge-sequence-rename-caching.sh index d02fa16614..0f39ed0d08 100755 --- a/t/t6429-merge-sequence-rename-caching.sh +++ b/t/t6429-merge-sequence-rename-caching.sh @@ -71,8 +71,9 @@ test_expect_success 'caching renames does not preclude finding new ones' ' git switch upstream && - test-tool fast-rebase --onto HEAD upstream~1 topic && - #git cherry-pick upstream~1..topic + git replay --onto HEAD upstream~1..topic >out && + git update-ref --stdin <out && + git checkout topic && git ls-files >tracked-files && test_line_count = 2 tracked-files && @@ -140,8 +141,9 @@ test_expect_success 'cherry-pick both a commit and its immediate revert' ' GIT_TRACE2_PERF="$(pwd)/trace.output" && export GIT_TRACE2_PERF && - test-tool fast-rebase --onto HEAD upstream~1 topic && - #git cherry-pick upstream~1..topic && + git replay --onto HEAD upstream~1..topic >out && + git update-ref --stdin <out && + git checkout topic && grep region_enter.*diffcore_rename trace.output >calls && test_line_count = 1 calls @@ -199,8 +201,9 @@ test_expect_success 'rename same file identically, then reintroduce it' ' GIT_TRACE2_PERF="$(pwd)/trace.output" && export GIT_TRACE2_PERF && - test-tool fast-rebase --onto HEAD upstream~1 topic && - #git cherry-pick upstream~1..topic && + git replay --onto HEAD upstream~1..topic >out && + git update-ref --stdin <out && + git checkout topic && git ls-files >tracked && test_line_count = 2 tracked && @@ -276,8 +279,9 @@ test_expect_success 'rename same file identically, then add file to old dir' ' GIT_TRACE2_PERF="$(pwd)/trace.output" && export GIT_TRACE2_PERF && - test-tool fast-rebase --onto HEAD upstream~1 topic && - #git cherry-pick upstream~1..topic && + git replay --onto HEAD upstream~1..topic >out && + git update-ref --stdin <out && + git checkout topic && git ls-files >tracked && test_line_count = 4 tracked && @@ -353,10 +357,7 @@ test_expect_success 'cached dir rename does not prevent noticing later conflict' GIT_TRACE2_PERF="$(pwd)/trace.output" && export GIT_TRACE2_PERF && - test_must_fail test-tool fast-rebase --onto HEAD upstream~1 topic >output && - #git cherry-pick upstream..topic && - - grep CONFLICT..rename/rename output && + test_must_fail git replay --onto HEAD upstream~1..topic >output && grep region_enter.*diffcore_rename trace.output >calls && test_line_count = 2 calls @@ -455,8 +456,9 @@ test_expect_success 'dir rename unneeded, then add new file to old dir' ' GIT_TRACE2_PERF="$(pwd)/trace.output" && export GIT_TRACE2_PERF && - test-tool fast-rebase --onto HEAD upstream~1 topic && - #git cherry-pick upstream..topic && + git replay --onto HEAD upstream~1..topic >out && + git update-ref --stdin <out && + git checkout topic && grep region_enter.*diffcore_rename trace.output >calls && test_line_count = 2 calls && @@ -521,8 +523,9 @@ test_expect_success 'dir rename unneeded, then rename existing file into old dir GIT_TRACE2_PERF="$(pwd)/trace.output" && export GIT_TRACE2_PERF && - test-tool fast-rebase --onto HEAD upstream~1 topic && - #git cherry-pick upstream..topic && + git replay --onto HEAD upstream~1..topic >out && + git update-ref --stdin <out && + git checkout topic && grep region_enter.*diffcore_rename trace.output >calls && test_line_count = 3 calls && @@ -623,8 +626,9 @@ test_expect_success 'caching renames only on upstream side, part 1' ' GIT_TRACE2_PERF="$(pwd)/trace.output" && export GIT_TRACE2_PERF && - test-tool fast-rebase --onto HEAD upstream~1 topic && - #git cherry-pick upstream..topic && + git replay --onto HEAD upstream~1..topic >out && + git update-ref --stdin <out && + git checkout topic && grep region_enter.*diffcore_rename trace.output >calls && test_line_count = 1 calls && @@ -681,8 +685,9 @@ test_expect_success 'caching renames only on upstream side, part 2' ' GIT_TRACE2_PERF="$(pwd)/trace.output" && export GIT_TRACE2_PERF && - test-tool fast-rebase --onto HEAD upstream~1 topic && - #git cherry-pick upstream..topic && + git replay --onto HEAD upstream~1..topic >out && + git update-ref --stdin <out && + git checkout topic && grep region_enter.*diffcore_rename trace.output >calls && test_line_count = 2 calls && diff --git a/t/t7004-tag.sh b/t/t7004-tag.sh index e689db4292..b41a47eb94 100755 --- a/t/t7004-tag.sh +++ b/t/t7004-tag.sh @@ -1862,6 +1862,51 @@ test_expect_success 'option override configured sort' ' test_cmp expect actual ' +test_expect_success '--no-sort cancels config sort keys' ' + test_config tag.sort "-refname" && + + # objecttype is identical for all of them, so sort falls back on + # default (ascending refname) + git tag -l \ + --no-sort \ + --sort="objecttype" \ + "foo*" >actual && + cat >expect <<-\EOF && + foo1.10 + foo1.3 + foo1.6 + EOF + test_cmp expect actual +' + +test_expect_success '--no-sort cancels command line sort keys' ' + # objecttype is identical for all of them, so sort falls back on + # default (ascending refname) + git tag -l \ + --sort="-refname" \ + --no-sort \ + --sort="objecttype" \ + "foo*" >actual && + cat >expect <<-\EOF && + foo1.10 + foo1.3 + foo1.6 + EOF + test_cmp expect actual +' + +test_expect_success '--no-sort without subsequent --sort prints expected tags' ' + # Sort the results with `sort` for a consistent comparison against + # expected + git tag -l --no-sort "foo*" | sort >actual && + cat >expect <<-\EOF && + foo1.10 + foo1.3 + foo1.6 + EOF + test_cmp expect actual +' + test_expect_success 'invalid sort parameter on command line' ' test_must_fail git tag -l --sort=notvalid "foo*" >actual ' diff --git a/t/t7300-clean.sh b/t/t7300-clean.sh index 1a310a45fd..611b3dd3ae 100755 --- a/t/t7300-clean.sh +++ b/t/t7300-clean.sh @@ -517,8 +517,12 @@ test_expect_success 'nested (empty) git should be kept' ' git init empty_repo && mkdir to_clean && >to_clean/should_clean.this && + # Note that we put the expect file in the .git directory so that it + # does not get cleaned. + find empty_repo | sort >.git/expect && git clean -f -d && - test_path_is_file empty_repo/.git/HEAD && + find empty_repo | sort >actual && + test_cmp .git/expect actual && test_path_is_missing to_clean ' @@ -559,10 +563,10 @@ test_expect_success 'giving path in nested git work tree will NOT remove it' ' mkdir -p bar/baz && test_commit msg bar/baz/hello.world ) && + find repo | sort >expect && git clean -f -d repo/bar/baz && - test_path_is_file repo/.git/HEAD && - test_path_is_dir repo/bar/ && - test_path_is_file repo/bar/baz/hello.world + find repo | sort >actual && + test_cmp expect actual ' test_expect_success 'giving path to nested .git will not remove it' ' @@ -573,10 +577,10 @@ test_expect_success 'giving path to nested .git will not remove it' ' git init && test_commit msg hello.world ) && + find repo | sort >expect && git clean -f -d repo/.git && - test_path_is_file repo/.git/HEAD && - test_path_is_dir repo/.git/refs && - test_path_is_dir repo/.git/objects && + find repo | sort >actual && + test_cmp expect actual && test_path_is_dir untracked/ ' @@ -588,9 +592,10 @@ test_expect_success 'giving path to nested .git/ will NOT remove contents' ' git init && test_commit msg hello.world ) && + find repo | sort >expect && git clean -f -d repo/.git/ && - test_path_is_dir repo/.git && - test_path_is_file repo/.git/HEAD && + find repo | sort >actual && + test_cmp expect actual && test_path_is_dir untracked/ ' diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh index 935df6a1db..00d29871e6 100755 --- a/t/t7900-maintenance.sh +++ b/t/t7900-maintenance.sh @@ -157,7 +157,8 @@ test_expect_success 'prefetch multiple remotes' ' fetchargs="--prefetch --prune --no-tags --no-write-fetch-head --recurse-submodules=no --quiet" && test_subcommand git fetch remote1 $fetchargs <run-prefetch.txt && test_subcommand git fetch remote2 $fetchargs <run-prefetch.txt && - test_path_is_missing .git/refs/remotes && + git for-each-ref refs/remotes >actual && + test_must_be_empty actual && git log prefetch/remotes/remote1/one && git log prefetch/remotes/remote2/two && git fetch --all && diff --git a/t/t9133-git-svn-nested-git-repo.sh b/t/t9133-git-svn-nested-git-repo.sh index d8d536269c..8ca24670ac 100755 --- a/t/t9133-git-svn-nested-git-repo.sh +++ b/t/t9133-git-svn-nested-git-repo.sh @@ -11,7 +11,7 @@ test_expect_success 'setup repo with a git repo inside it' ' ( cd s && git init && - test -f .git/HEAD && + git symbolic-ref HEAD && > .git/a && echo a > a && svn_cmd add .git a && diff --git a/t/t9164-git-svn-dcommit-concurrent.sh b/t/t9164-git-svn-dcommit-concurrent.sh index c8e6c0733f..d1dec89c3b 100755 --- a/t/t9164-git-svn-dcommit-concurrent.sh +++ b/t/t9164-git-svn-dcommit-concurrent.sh @@ -46,6 +46,14 @@ setup_hook() "passed to setup_hook" >&2 ; return 1; } echo "cnt=$skip_revs" > "$hook_type-counter" rm -f "$rawsvnrepo/hooks/"*-commit # drop previous hooks + + # Subversion hooks run with an empty environment by default. We thus + # need to propagate PATH so that we can find executables. + cat >"$rawsvnrepo/conf/hooks-env" <<-EOF + [default] + PATH = ${PATH} + EOF + hook="$rawsvnrepo/hooks/$hook_type" cat > "$hook" <<- 'EOF1' #!/bin/sh @@ -63,7 +71,6 @@ EOF1 if [ "$hook_type" = "pre-commit" ]; then echo "echo 'commit disallowed' >&2; exit 1" >>"$hook" else - echo "PATH=\"$PATH\"; export PATH" >>"$hook" echo "svnconf=\"$svnconf\"" >>"$hook" cat >>"$hook" <<- 'EOF2' cd work-auto-commits.svn diff --git a/t/t9700/test.pl b/t/t9700/test.pl index 6d753708d2..d8e85482ab 100755 --- a/t/t9700/test.pl +++ b/t/t9700/test.pl @@ -1,7 +1,7 @@ #!/usr/bin/perl use lib (split(/:/, $ENV{GITPERLLIB})); -use 5.008; +use 5.008001; use warnings; use strict; diff --git a/t/test-terminal.perl b/t/test-terminal.perl index 1bcf01a9a4..3810e9bb43 100755 --- a/t/test-terminal.perl +++ b/t/test-terminal.perl @@ -1,5 +1,5 @@ #!/usr/bin/perl -use 5.008; +use 5.008001; use strict; use warnings; use IO::Pty; diff --git a/t/unit-tests/.gitignore b/t/unit-tests/.gitignore new file mode 100644 index 0000000000..5e56e040ec --- /dev/null +++ b/t/unit-tests/.gitignore @@ -0,0 +1 @@ +/bin diff --git a/t/unit-tests/t-basic.c b/t/unit-tests/t-basic.c new file mode 100644 index 0000000000..fda1ae59a6 --- /dev/null +++ b/t/unit-tests/t-basic.c @@ -0,0 +1,95 @@ +#include "test-lib.h" + +/* + * The purpose of this "unit test" is to verify a few invariants of the unit + * test framework itself, as well as to provide examples of output from actually + * failing tests. As such, it is intended that this test fails, and thus it + * should not be run as part of `make unit-tests`. Instead, we verify it behaves + * as expected in the integration test t0080-unit-test-output.sh + */ + +/* Used to store the return value of check_int(). */ +static int check_res; + +/* Used to store the return value of TEST(). */ +static int test_res; + +static void t_res(int expect) +{ + check_int(check_res, ==, expect); + check_int(test_res, ==, expect); +} + +static void t_todo(int x) +{ + check_res = TEST_TODO(check(x)); +} + +static void t_skip(void) +{ + check(0); + test_skip("missing prerequisite"); + check(1); +} + +static int do_skip(void) +{ + test_skip("missing prerequisite"); + return 1; +} + +static void t_skip_todo(void) +{ + check_res = TEST_TODO(do_skip()); +} + +static void t_todo_after_fail(void) +{ + check(0); + TEST_TODO(check(0)); +} + +static void t_fail_after_todo(void) +{ + check(1); + TEST_TODO(check(0)); + check(0); +} + +static void t_messages(void) +{ + check_str("\thello\\", "there\"\n"); + check_str("NULL", NULL); + check_char('a', ==, '\n'); + check_char('\\', ==, '\''); +} + +static void t_empty(void) +{ + ; /* empty */ +} + +int cmd_main(int argc, const char **argv) +{ + test_res = TEST(check_res = check_int(1, ==, 1), "passing test"); + TEST(t_res(1), "passing test and assertion return 1"); + test_res = TEST(check_res = check_int(1, ==, 2), "failing test"); + TEST(t_res(0), "failing test and assertion return 0"); + test_res = TEST(t_todo(0), "passing TEST_TODO()"); + TEST(t_res(1), "passing TEST_TODO() returns 1"); + test_res = TEST(t_todo(1), "failing TEST_TODO()"); + TEST(t_res(0), "failing TEST_TODO() returns 0"); + test_res = TEST(t_skip(), "test_skip()"); + TEST(check_int(test_res, ==, 1), "skipped test returns 1"); + test_res = TEST(t_skip_todo(), "test_skip() inside TEST_TODO()"); + TEST(t_res(1), "test_skip() inside TEST_TODO() returns 1"); + test_res = TEST(t_todo_after_fail(), "TEST_TODO() after failing check"); + TEST(check_int(test_res, ==, 0), "TEST_TODO() after failing check returns 0"); + test_res = TEST(t_fail_after_todo(), "failing check after TEST_TODO()"); + TEST(check_int(test_res, ==, 0), "failing check after TEST_TODO() returns 0"); + TEST(t_messages(), "messages from failing string and char comparison"); + test_res = TEST(t_empty(), "test with no checks"); + TEST(check_int(test_res, ==, 0), "test with no checks returns 0"); + + return test_done(); +} diff --git a/t/unit-tests/t-strbuf.c b/t/unit-tests/t-strbuf.c new file mode 100644 index 0000000000..de434a4441 --- /dev/null +++ b/t/unit-tests/t-strbuf.c @@ -0,0 +1,120 @@ +#include "test-lib.h" +#include "strbuf.h" + +/* wrapper that supplies tests with an empty, initialized strbuf */ +static void setup(void (*f)(struct strbuf*, void*), void *data) +{ + struct strbuf buf = STRBUF_INIT; + + f(&buf, data); + strbuf_release(&buf); + check_uint(buf.len, ==, 0); + check_uint(buf.alloc, ==, 0); +} + +/* wrapper that supplies tests with a populated, initialized strbuf */ +static void setup_populated(void (*f)(struct strbuf*, void*), char *init_str, void *data) +{ + struct strbuf buf = STRBUF_INIT; + + strbuf_addstr(&buf, init_str); + check_uint(buf.len, ==, strlen(init_str)); + f(&buf, data); + strbuf_release(&buf); + check_uint(buf.len, ==, 0); + check_uint(buf.alloc, ==, 0); +} + +static int assert_sane_strbuf(struct strbuf *buf) +{ + /* Initialized strbufs should always have a non-NULL buffer */ + if (!check(!!buf->buf)) + return 0; + /* Buffers should always be NUL-terminated */ + if (!check_char(buf->buf[buf->len], ==, '\0')) + return 0; + /* + * Freshly-initialized strbufs may not have a dynamically allocated + * buffer + */ + if (buf->len == 0 && buf->alloc == 0) + return 1; + /* alloc must be at least one byte larger than len */ + return check_uint(buf->len, <, buf->alloc); +} + +static void t_static_init(void) +{ + struct strbuf buf = STRBUF_INIT; + + check_uint(buf.len, ==, 0); + check_uint(buf.alloc, ==, 0); + check_char(buf.buf[0], ==, '\0'); +} + +static void t_dynamic_init(void) +{ + struct strbuf buf; + + strbuf_init(&buf, 1024); + check(assert_sane_strbuf(&buf)); + check_uint(buf.len, ==, 0); + check_uint(buf.alloc, >=, 1024); + check_char(buf.buf[0], ==, '\0'); + strbuf_release(&buf); +} + +static void t_addch(struct strbuf *buf, void *data) +{ + const char *p_ch = data; + const char ch = *p_ch; + size_t orig_alloc = buf->alloc; + size_t orig_len = buf->len; + + if (!check(assert_sane_strbuf(buf))) + return; + strbuf_addch(buf, ch); + if (!check(assert_sane_strbuf(buf))) + return; + if (!(check_uint(buf->len, ==, orig_len + 1) && + check_uint(buf->alloc, >=, orig_alloc))) + return; /* avoid de-referencing buf->buf */ + check_char(buf->buf[buf->len - 1], ==, ch); + check_char(buf->buf[buf->len], ==, '\0'); +} + +static void t_addstr(struct strbuf *buf, void *data) +{ + const char *text = data; + size_t len = strlen(text); + size_t orig_alloc = buf->alloc; + size_t orig_len = buf->len; + + if (!check(assert_sane_strbuf(buf))) + return; + strbuf_addstr(buf, text); + if (!check(assert_sane_strbuf(buf))) + return; + if (!(check_uint(buf->len, ==, orig_len + len) && + check_uint(buf->alloc, >=, orig_alloc) && + check_uint(buf->alloc, >, orig_len + len) && + check_char(buf->buf[orig_len + len], ==, '\0'))) + return; + check_str(buf->buf + orig_len, text); +} + +int cmd_main(int argc, const char **argv) +{ + if (!TEST(t_static_init(), "static initialization works")) + test_skip_all("STRBUF_INIT is broken"); + TEST(t_dynamic_init(), "dynamic initialization works"); + TEST(setup(t_addch, "a"), "strbuf_addch adds char"); + TEST(setup(t_addch, ""), "strbuf_addch adds NUL char"); + TEST(setup_populated(t_addch, "initial value", "a"), + "strbuf_addch appends to initial value"); + TEST(setup(t_addstr, "hello there"), "strbuf_addstr adds string"); + TEST(setup_populated(t_addstr, "initial value", "hello there"), + "strbuf_addstr appends string to initial value"); + + return test_done(); +} diff --git a/t/unit-tests/test-lib.c b/t/unit-tests/test-lib.c new file mode 100644 index 0000000000..7bf9dfdb95 --- /dev/null +++ b/t/unit-tests/test-lib.c @@ -0,0 +1,374 @@ +#include "test-lib.h" + +enum result { + RESULT_NONE, + RESULT_FAILURE, + RESULT_SKIP, + RESULT_SUCCESS, + RESULT_TODO +}; + +static struct { + enum result result; + int count; + unsigned failed :1; + unsigned lazy_plan :1; + unsigned running :1; + unsigned skip_all :1; + unsigned todo :1; +} ctx = { + .lazy_plan = 1, + .result = RESULT_NONE, +}; + +#ifndef _MSC_VER +#define make_relative(location) location +#else +/* + * Visual C interpolates the absolute Windows path for `__FILE__`, + * but we want to see relative paths, as verified by t0080. + */ +#include "dir.h" + +static const char *make_relative(const char *location) +{ + static char prefix[] = __FILE__, buf[PATH_MAX], *p; + static size_t prefix_len; + + if (!prefix_len) { + size_t len = strlen(prefix); + const char *needle = "\\t\\unit-tests\\test-lib.c"; + size_t needle_len = strlen(needle); + + if (len < needle_len || strcmp(needle, prefix + len - needle_len)) + die("unexpected suffix of '%s'", prefix); + + /* let it end in a directory separator */ + prefix_len = len - needle_len + 1; + } + + /* Does it not start with the expected prefix? */ + if (fspathncmp(location, prefix, prefix_len)) + return location; + + strlcpy(buf, location + prefix_len, sizeof(buf)); + /* convert backslashes to forward slashes */ + for (p = buf; *p; p++) + if (*p == '\\') + *p = '/'; + + return buf; +} +#endif + +static void msg_with_prefix(const char *prefix, const char *format, va_list ap) +{ + fflush(stderr); + if (prefix) + fprintf(stdout, "%s", prefix); + vprintf(format, ap); /* TODO: handle newlines */ + putc('\n', stdout); + fflush(stdout); +} + +void test_msg(const char *format, ...) +{ + va_list ap; + + va_start(ap, format); + msg_with_prefix("# ", format, ap); + va_end(ap); +} + +void test_plan(int count) +{ + assert(!ctx.running); + + fflush(stderr); + printf("1..%d\n", count); + fflush(stdout); + ctx.lazy_plan = 0; +} + +int test_done(void) +{ + assert(!ctx.running); + + if (ctx.lazy_plan) + test_plan(ctx.count); + + return ctx.failed; +} + +void test_skip(const char *format, ...) +{ + va_list ap; + + assert(ctx.running); + + ctx.result = RESULT_SKIP; + va_start(ap, format); + if (format) + msg_with_prefix("# skipping test - ", format, ap); + va_end(ap); +} + +void test_skip_all(const char *format, ...) +{ + va_list ap; + const char *prefix; + + if (!ctx.count && ctx.lazy_plan) { + /* We have not printed a test plan yet */ + prefix = "1..0 # SKIP "; + ctx.lazy_plan = 0; + } else { + /* We have already printed a test plan */ + prefix = "Bail out! # "; + ctx.failed = 1; + } + ctx.skip_all = 1; + ctx.result = RESULT_SKIP; + va_start(ap, format); + msg_with_prefix(prefix, format, ap); + va_end(ap); +} + +int test__run_begin(void) +{ + assert(!ctx.running); + + ctx.count++; + ctx.result = RESULT_NONE; + ctx.running = 1; + + return ctx.skip_all; +} + +static void print_description(const char *format, va_list ap) +{ + if (format) { + fputs(" - ", stdout); + vprintf(format, ap); + } +} + +int test__run_end(int was_run UNUSED, const char *location, const char *format, ...) +{ + va_list ap; + + assert(ctx.running); + assert(!ctx.todo); + + fflush(stderr); + va_start(ap, format); + if (!ctx.skip_all) { + switch (ctx.result) { + case RESULT_SUCCESS: + printf("ok %d", ctx.count); + print_description(format, ap); + break; + + case RESULT_FAILURE: + printf("not ok %d", ctx.count); + print_description(format, ap); + break; + + case RESULT_TODO: + printf("not ok %d", ctx.count); + print_description(format, ap); + printf(" # TODO"); + break; + + case RESULT_SKIP: + printf("ok %d", ctx.count); + print_description(format, ap); + printf(" # SKIP"); + break; + + case RESULT_NONE: + test_msg("BUG: test has no checks at %s", + make_relative(location)); + printf("not ok %d", ctx.count); + print_description(format, ap); + ctx.result = RESULT_FAILURE; + break; + } + } + va_end(ap); + ctx.running = 0; + if (ctx.skip_all) + return 1; + putc('\n', stdout); + fflush(stdout); + ctx.failed |= ctx.result == RESULT_FAILURE; + + return ctx.result != RESULT_FAILURE; +} + +static void test_fail(void) +{ + assert(ctx.result != RESULT_SKIP); + + ctx.result = RESULT_FAILURE; +} + +static void test_pass(void) +{ + assert(ctx.result != RESULT_SKIP); + + if (ctx.result == RESULT_NONE) + ctx.result = RESULT_SUCCESS; +} + +static void test_todo(void) +{ + assert(ctx.result != RESULT_SKIP); + + if (ctx.result != RESULT_FAILURE) + ctx.result = RESULT_TODO; +} + +int test_assert(const char *location, const char *check, int ok) +{ + assert(ctx.running); + + if (ctx.result == RESULT_SKIP) { + test_msg("skipping check '%s' at %s", check, + make_relative(location)); + return 1; + } + if (!ctx.todo) { + if (ok) { + test_pass(); + } else { + test_msg("check \"%s\" failed at %s", check, + make_relative(location)); + test_fail(); + } + } + + return !!ok; +} + +void test__todo_begin(void) +{ + assert(ctx.running); + assert(!ctx.todo); + + ctx.todo = 1; +} + +int test__todo_end(const char *location, const char *check, int res) +{ + assert(ctx.running); + assert(ctx.todo); + + ctx.todo = 0; + if (ctx.result == RESULT_SKIP) + return 1; + if (res) { + test_msg("todo check '%s' succeeded at %s", check, + make_relative(location)); + test_fail(); + } else { + test_todo(); + } + + return !res; +} + +int check_bool_loc(const char *loc, const char *check, int ok) +{ + return test_assert(loc, check, ok); +} + +union test__tmp test__tmp[2]; + +int check_int_loc(const char *loc, const char *check, int ok, + intmax_t a, intmax_t b) +{ + int ret = test_assert(loc, check, ok); + + if (!ret) { + test_msg(" left: %"PRIdMAX, a); + test_msg(" right: %"PRIdMAX, b); + } + + return ret; +} + +int check_uint_loc(const char *loc, const char *check, int ok, + uintmax_t a, uintmax_t b) +{ + int ret = test_assert(loc, check, ok); + + if (!ret) { + test_msg(" left: %"PRIuMAX, a); + test_msg(" right: %"PRIuMAX, b); + } + + return ret; +} + +static void print_one_char(char ch, char quote) +{ + if ((unsigned char)ch < 0x20u || ch == 0x7f) { + /* TODO: improve handling of \a, \b, \f ... */ + printf("\\%03o", (unsigned char)ch); + } else { + if (ch == '\\' || ch == quote) + putc('\\', stdout); + putc(ch, stdout); + } +} + +static void print_char(const char *prefix, char ch) +{ + printf("# %s: '", prefix); + print_one_char(ch, '\''); + fputs("'\n", stdout); +} + +int check_char_loc(const char *loc, const char *check, int ok, char a, char b) +{ + int ret = test_assert(loc, check, ok); + + if (!ret) { + fflush(stderr); + print_char(" left", a); + print_char(" right", b); + fflush(stdout); + } + + return ret; +} + +static void print_str(const char *prefix, const char *str) +{ + printf("# %s: ", prefix); + if (!str) { + fputs("NULL\n", stdout); + } else { + putc('"', stdout); + while (*str) + print_one_char(*str++, '"'); + fputs("\"\n", stdout); + } +} + +int check_str_loc(const char *loc, const char *check, + const char *a, const char *b) +{ + int ok = (!a && !b) || (a && b && !strcmp(a, b)); + int ret = test_assert(loc, check, ok); + + if (!ret) { + fflush(stderr); + print_str(" left", a); + print_str(" right", b); + fflush(stdout); + } + + return ret; +} diff --git a/t/unit-tests/test-lib.h b/t/unit-tests/test-lib.h new file mode 100644 index 0000000000..a8f07ae0b7 --- /dev/null +++ b/t/unit-tests/test-lib.h @@ -0,0 +1,149 @@ +#ifndef TEST_LIB_H +#define TEST_LIB_H + +#include "git-compat-util.h" + +/* + * Run a test function, returns 1 if the test succeeds, 0 if it + * fails. If test_skip_all() has been called then the test will not be + * run. The description for each test should be unique. For example: + * + * TEST(test_something(arg1, arg2), "something %d %d", arg1, arg2) + */ +#define TEST(t, ...) \ + test__run_end(test__run_begin() ? 0 : (t, 1), \ + TEST_LOCATION(), __VA_ARGS__) + +/* + * Print a test plan, should be called before any tests. If the number + * of tests is not known in advance test_done() will automatically + * print a plan at the end of the test program. + */ +void test_plan(int count); + +/* + * test_done() must be called at the end of main(). It will print the + * plan if plan() was not called at the beginning of the test program + * and returns the exit code for the test program. + */ +int test_done(void); + +/* Skip the current test. */ +__attribute__((format (printf, 1, 2))) +void test_skip(const char *format, ...); + +/* Skip all remaining tests. */ +__attribute__((format (printf, 1, 2))) +void test_skip_all(const char *format, ...); + +/* Print a diagnostic message to stdout. */ +__attribute__((format (printf, 1, 2))) +void test_msg(const char *format, ...); + +/* + * Test checks are built around test_assert(). checks return 1 on + * success, 0 on failure. If any check fails then the test will fail. To + * create a custom check define a function that wraps test_assert() and + * a macro to wrap that function to provide a source location and + * stringified arguments. Custom checks that take pointer arguments + * should be careful to check that they are non-NULL before + * dereferencing them. For example: + * + * static int check_oid_loc(const char *loc, const char *check, + * struct object_id *a, struct object_id *b) + * { + * int res = test_assert(loc, check, a && b && oideq(a, b)); + * + * if (!res) { + * test_msg(" left: %s", a ? oid_to_hex(a) : "NULL"; + * test_msg(" right: %s", b ? oid_to_hex(a) : "NULL"; + * + * } + * return res; + * } + * + * #define check_oid(a, b) \ + * check_oid_loc(TEST_LOCATION(), "oideq("#a", "#b")", a, b) + */ +int test_assert(const char *location, const char *check, int ok); + +/* Helper macro to pass the location to checks */ +#define TEST_LOCATION() TEST__MAKE_LOCATION(__LINE__) + +/* Check a boolean condition. */ +#define check(x) \ + check_bool_loc(TEST_LOCATION(), #x, x) +int check_bool_loc(const char *loc, const char *check, int ok); + +/* + * Compare two integers. Prints a message with the two values if the + * comparison fails. NB this is not thread safe. + */ +#define check_int(a, op, b) \ + (test__tmp[0].i = (a), test__tmp[1].i = (b), \ + check_int_loc(TEST_LOCATION(), #a" "#op" "#b, \ + test__tmp[0].i op test__tmp[1].i, \ + test__tmp[0].i, test__tmp[1].i)) +int check_int_loc(const char *loc, const char *check, int ok, + intmax_t a, intmax_t b); + +/* + * Compare two unsigned integers. Prints a message with the two values + * if the comparison fails. NB this is not thread safe. + */ +#define check_uint(a, op, b) \ + (test__tmp[0].u = (a), test__tmp[1].u = (b), \ + check_uint_loc(TEST_LOCATION(), #a" "#op" "#b, \ + test__tmp[0].u op test__tmp[1].u, \ + test__tmp[0].u, test__tmp[1].u)) +int check_uint_loc(const char *loc, const char *check, int ok, + uintmax_t a, uintmax_t b); + +/* + * Compare two chars. Prints a message with the two values if the + * comparison fails. NB this is not thread safe. + */ +#define check_char(a, op, b) \ + (test__tmp[0].c = (a), test__tmp[1].c = (b), \ + check_char_loc(TEST_LOCATION(), #a" "#op" "#b, \ + test__tmp[0].c op test__tmp[1].c, \ + test__tmp[0].c, test__tmp[1].c)) +int check_char_loc(const char *loc, const char *check, int ok, + char a, char b); + +/* Check whether two strings are equal. */ +#define check_str(a, b) \ + check_str_loc(TEST_LOCATION(), "!strcmp("#a", "#b")", a, b) +int check_str_loc(const char *loc, const char *check, + const char *a, const char *b); + +/* + * Wrap a check that is known to fail. If the check succeeds then the + * test will fail. Returns 1 if the check fails, 0 if it + * succeeds. For example: + * + * TEST_TODO(check(0)); + */ +#define TEST_TODO(check) \ + (test__todo_begin(), test__todo_end(TEST_LOCATION(), #check, check)) + +/* Private helpers */ + +#define TEST__STR(x) #x +#define TEST__MAKE_LOCATION(line) __FILE__ ":" TEST__STR(line) + +union test__tmp { + intmax_t i; + uintmax_t u; + char c; +}; + +extern union test__tmp test__tmp[2]; + +int test__run_begin(void); +__attribute__((format (printf, 3, 4))) +int test__run_end(int, const char *, const char *, ...); +void test__todo_begin(void); +int test__todo_end(const char *, const char *, int); + +#endif /* TEST_LIB_H */ diff --git a/t/valgrind/valgrind.sh b/t/valgrind/valgrind.sh index 669ebaf68b..3c8ee19975 100755 --- a/t/valgrind/valgrind.sh +++ b/t/valgrind/valgrind.sh @@ -23,7 +23,7 @@ memcheck) VALGRIND_MAJOR=$(expr "$VALGRIND_VERSION" : '[^0-9]*\([0-9]*\)') VALGRIND_MINOR=$(expr "$VALGRIND_VERSION" : '[^0-9]*[0-9]*\.\([0-9]*\)') test 3 -gt "$VALGRIND_MAJOR" || - test 3 -eq "$VALGRIND_MAJOR" -a 4 -gt "$VALGRIND_MINOR" || + { test 3 -eq "$VALGRIND_MAJOR" && test 4 -gt "$VALGRIND_MINOR"; } || TOOL_OPTIONS="$TOOL_OPTIONS --track-origins=yes" ;; *) |
