Merge branch 'ac/log-use-mailmap-by-default-transition'
authorJunio C Hamano <gitster@pobox.com>
Thu, 25 Jul 2019 20:59:22 +0000 (13:59 -0700)
committerJunio C Hamano <gitster@pobox.com>
Thu, 25 Jul 2019 20:59:22 +0000 (13:59 -0700)
The "git log" command learns to issue a warning when log.mailmap
configuration is not set and --[no-]mailmap option is not used, to
prepare users for future versions of Git that uses the mailmap by
default.

* ac/log-use-mailmap-by-default-transition:
tests: defang pager tests by explicitly disabling the log.mailmap warning
documentation: mention --no-use-mailmap and log.mailmap false setting
log: add warning for unspecified log.mailmap setting

129 files changed:
.gitignore
Documentation/RelNotes/2.23.0.txt
Documentation/blame-options.txt
Documentation/config/advice.txt
Documentation/config/blame.txt
Documentation/config/gpg.txt
Documentation/git-blame.txt
Documentation/git-cherry-pick.txt
Documentation/git-commit-graph.txt
Documentation/git-multi-pack-index.txt
Documentation/git-revert.txt
Documentation/rev-list-options.txt
Documentation/sequencer.txt
Documentation/technical/api-trace2.txt
Documentation/technical/commit-graph-format.txt
Documentation/technical/commit-graph.txt
Makefile
advice.c
advice.h
apply.c
apply.h
archive.c
blame.c
blame.h
builtin.h
builtin/blame.c
builtin/cat-file.c
builtin/commit-graph.c
builtin/commit.c
builtin/env--helper.c [new file with mode: 0644]
builtin/gc.c
builtin/grep.c
builtin/merge-tree.c
builtin/merge.c
builtin/multi-pack-index.c
builtin/pack-objects.c
builtin/rebase.c
builtin/receive-pack.c
builtin/repack.c
builtin/reset.c
builtin/revert.c
builtin/rm.c
builtin/update-index.c
cache.h
ci/lib.sh
commit-graph.c
commit-graph.h
compat/mingw.c
config.c
connected.c
contrib/completion/git-prompt.sh
diff.c
diff.h
fast-import.c
fsck.c
gettext.c
git-sh-i18n.sh
git.c
line-log.c
match-trees.c
merge-recursive.c
midx.c
midx.h
notes.c
object-store.h
oidmap.c
oidset.c
oidset.h
packfile.c
packfile.h
po/README
range-diff.c
ref-filter.c
revision.c
sequencer.c
sequencer.h
sha1-file.c
sha1-name.c
shallow.c
t/README
t/helper/test-hashmap.c
t/helper/test-match-trees.c
t/helper/test-oidmap.c [new file with mode: 0644]
t/helper/test-tool.c
t/helper/test-tool.h
t/lib-git-daemon.sh
t/lib-git-svn.sh
t/lib-httpd.sh
t/perf/p5600-clone-reference.sh [new file with mode: 0755]
t/t0000-basic.sh
t/t0011-hashmap.sh
t/t0016-oidmap.sh [new file with mode: 0755]
t/t0017-env-helper.sh [new file with mode: 0755]
t/t0205-gettext-poison.sh
t/t1305-config-include.sh
t/t3206-range-diff.sh
t/t3206/history.export
t/t3420-rebase-autostash.sh
t/t3510-cherry-pick-sequence.sh
t/t5318-commit-graph.sh
t/t5319-multi-pack-index.sh
t/t5324-split-commit-graph.sh [new file with mode: 0755]
t/t5504-fetch-receive-strict.sh
t/t5512-ls-remote.sh
t/t5541-http-push-smart.sh
t/t5618-alternate-refs.sh [new file with mode: 0755]
t/t6040-tracking-info.sh
t/t6300-for-each-ref.sh
t/t7060-wtstatus.sh
t/t7201-co.sh
t/t7508-status.sh
t/t7512-status-help.sh
t/t7700-repack.sh
t/t7814-grep-recurse-submodules.sh
t/t8003-blame-corner-cases.sh
t/t8013-blame-ignore-revs.sh [new file with mode: 0755]
t/t8014-blame-ignore-fuzzy.sh [new file with mode: 0755]
t/t9902-completion.sh
t/t9903-bash-prompt.sh
t/test-lib-functions.sh
t/test-lib.sh
transport-helper.c
transport.c
transport.h
tree-diff.c
tree-walk.c
tree-walk.h
unpack-trees.c
wt-status.c
index e096e0a51c19966eb743454fab50d232441008ca..069c190ba078a7fe4ac3b42db67c6d1221939249 100644 (file)
@@ -58,6 +58,7 @@
 /git-difftool
 /git-difftool--helper
 /git-describe
+/git-env--helper
 /git-fast-export
 /git-fast-import
 /git-fetch
index a63204ffe8a040479654c3e44db6c170feca2a58..82683553696e8b93b960fd0bd30df0caa9c02615 100644 (file)
@@ -75,6 +75,22 @@ UI, Workflows & Features
  * A new tag.gpgSign configuration variable turns "git tag -a" into
    "git tag -s".
 
+ * "git multi-pack-index" learned expire and repack subcommands.
+
+ * "git blame" learned to "ignore" commits in the history, whose
+   effects (as well as their presence) get ignored.
+
+ * "git cherry-pick/revert" learned a new "--skip" action.
+
+ * The tips of refs from the alternate object store can be used as
+   starting point for reachability computation now.
+
+ * Extra blank lines in "git status" output have been reduced.
+
+ * The commits in a repository can be described by multiple
+   commit-graph files now, which allows the commit-graph files to be
+   updated incrementally.
+
 
 Performance, Internal Implementation, Development Support etc.
 
@@ -114,6 +130,19 @@ Performance, Internal Implementation, Development Support etc.
 
  * A handful of Windows build patches have been upstreamed.
 
+ * The code to read state files used by the sequencer machinery for
+   "git status" has been made more robust against a corrupt or stale
+   state files.
+
+ * "git for-each-ref" with multiple patterns have been optimized.
+
+ * The tree-walk API learned to pass an in-core repository
+   instance throughout more codepaths.
+
+ * When one step in multi step cherry-pick or revert is reset or
+   committed, the command line prompt script failed to notice the
+   current status, which has been improved.
+
 
 Fixes since v2.22
 -----------------
@@ -293,6 +322,10 @@ Fixes since v2.22
    forms to make them more recognisable.
    (merge bfc8c84ed5 qn/clone-doc-use-long-form later to maint).
 
+ * Generation of pack bitmaps are now disabled when .keep files exist,
+   as these are mutually exclusive features.
+   (merge 7328482253 ew/repack-with-bitmaps-by-default later to maint).
+
  * Other code cleanup, docfix, build fix, etc.
    (merge f547101b26 es/git-debugger-doc later to maint).
    (merge 7877ac3d7b js/bisect-helper-check-get-oid-return-value later to maint).
@@ -312,3 +345,5 @@ Fixes since v2.22
    (merge 8d45ad8c29 jt/t5551-test-chunked later to maint).
    (merge 1a64e07d23 sg/git-C-empty-doc later to maint).
    (merge 37a2e35395 sg/ci-brew-gcc-workaround later to maint).
+   (merge 24df0d49c4 js/trace2-signo-typofix later to maint).
+   (merge fbec05c210 cc/test-oidmap later to maint).
index dc41957afab25d08cf6fc530cde97b91bed8e06e..5d122db6e9e6863fcf1e69ebc14feb1393501e0b 100644 (file)
@@ -110,5 +110,24 @@ commit. And the default value is 40. If there are more than one
 `-C` options given, the <num> argument of the last `-C` will
 take effect.
 
+--ignore-rev <rev>::
+       Ignore changes made by the revision when assigning blame, as if the
+       change never happened.  Lines that were changed or added by an ignored
+       commit will be blamed on the previous commit that changed that line or
+       nearby lines.  This option may be specified multiple times to ignore
+       more than one revision.  If the `blame.markIgnoredLines` config option
+       is set, then lines that were changed by an ignored commit and attributed to
+       another commit will be marked with a `?` in the blame output.  If the
+       `blame.markUnblamableLines` config option is set, then those lines touched
+       by an ignored commit that we could not attribute to another revision are
+       marked with a '*'.
+
+--ignore-revs-file <file>::
+       Ignore revisions listed in `file`, which must be in the same format as an
+       `fsck.skipList`.  This option may be repeated, and these files will be
+       processed after any files specified with the `blame.ignoreRevsFile` config
+       option.  An empty file name, `""`, will clear the list of revs from
+       previously processed files.
+
 -h::
        Show help message.
index ee85c536cec83c9097ae586da404580573448431..6aaa36020298f54f60cab1973b91641d684b9b18 100644 (file)
@@ -68,6 +68,8 @@ advice.*::
        resolveConflict::
                Advice shown by various commands when conflicts
                prevent the operation from being performed.
+       sequencerInUse::
+               Advice shown when a sequencer command is already in progress.
        implicitIdentity::
                Advice on how to set your identity configuration when
                your information is guessed from the system username and
index 67b5c1d1e02a4458f5fefef2ed7e25df32343e2b..9468e8599c0c16d3bd54ec67ba65351be9de1a69 100644 (file)
@@ -19,3 +19,19 @@ blame.showEmail::
 blame.showRoot::
        Do not treat root commits as boundaries in linkgit:git-blame[1].
        This option defaults to false.
+
+blame.ignoreRevsFile::
+       Ignore revisions listed in the file, one unabbreviated object name per
+       line, in linkgit:git-blame[1].  Whitespace and comments beginning with
+       `#` are ignored.  This option may be repeated multiple times.  Empty
+       file names will reset the list of ignored revisions.  This option will
+       be handled before the command line option `--ignore-revs-file`.
+
+blame.markUnblamables::
+       Mark lines that were changed by an ignored revision that we could not
+       attribute to another commit with a '*' in the output of
+       linkgit:git-blame[1].
+
+blame.markIgnoredLines::
+       Mark lines that were changed by an ignored revision that we attributed to
+       another commit with a '?' in the output of linkgit:git-blame[1].
index f999f8ea491d0238abaca2037535c0374f28bc01..cce2c8924598c240fcebfadea8be5b4b28ce378e 100644 (file)
@@ -2,7 +2,7 @@ gpg.program::
        Use this custom program instead of "`gpg`" found on `$PATH` when
        making or verifying a PGP signature. The program must support the
        same command-line interface as GPG, namely, to verify a detached
-       signature, "`gpg --verify $file - <$signature`" is run, and the
+       signature, "`gpg --verify $signature - <$file`" is run, and the
        program is expected to signal a good signature by exiting with
        code 0, and to generate an ASCII-armored detached signature, the
        standard input of "`gpg -bsau $key`" is fed with the contents to be
index 16323eb80e3108794067c4dbfcbfe25e46938498..7e81541996359cf4b7a4abce35e8cae5c2ce29fb 100644 (file)
@@ -10,6 +10,7 @@ SYNOPSIS
 [verse]
 'git blame' [-c] [-b] [-l] [--root] [-t] [-f] [-n] [-s] [-e] [-p] [-w] [--incremental]
            [-L <range>] [-S <revs-file>] [-M] [-C] [-C] [-C] [--since=<date>]
+           [--ignore-rev <rev>] [--ignore-revs-file <file>]
            [--progress] [--abbrev=<n>] [<rev> | --contents <file> | --reverse <rev>..<rev>]
            [--] <file>
 
index 754b16ce0c9da6a137c9f58de86eb44841ae64d6..83ce51aedfea54fd5150ef142aca24f8c1df95c9 100644 (file)
@@ -10,9 +10,7 @@ SYNOPSIS
 [verse]
 'git cherry-pick' [--edit] [-n] [-m parent-number] [-s] [-x] [--ff]
                  [-S[<keyid>]] <commit>...
-'git cherry-pick' --continue
-'git cherry-pick' --quit
-'git cherry-pick' --abort
+'git cherry-pick' (--continue | --skip | --abort | --quit)
 
 DESCRIPTION
 -----------
index 624470e198202a47e91e89274bb32b2bc3d441d6..eb5e7865f0ef787e1410a95702990e38567b68fe 100644 (file)
@@ -10,7 +10,7 @@ SYNOPSIS
 --------
 [verse]
 'git commit-graph read' [--object-dir <dir>]
-'git commit-graph verify' [--object-dir <dir>]
+'git commit-graph verify' [--object-dir <dir>] [--shallow]
 'git commit-graph write' <options> [--object-dir <dir>]
 
 
@@ -26,7 +26,7 @@ OPTIONS
        Use given directory for the location of packfiles and commit-graph
        file. This parameter exists to specify the location of an alternate
        that only has the objects directory, not a full `.git` directory. The
-       commit-graph file is expected to be at `<dir>/info/commit-graph` and
+       commit-graph file is expected to be in the `<dir>/info` directory and
        the packfiles are expected to be in `<dir>/pack`.
 
 
@@ -51,6 +51,25 @@ or `--stdin-packs`.)
 +
 With the `--append` option, include all commits that are present in the
 existing commit-graph file.
++
+With the `--split` option, write the commit-graph as a chain of multiple
+commit-graph files stored in `<dir>/info/commit-graphs`. The new commits
+not already in the commit-graph are added in a new "tip" file. This file
+is merged with the existing file if the following merge conditions are
+met:
++
+* If `--size-multiple=<X>` is not specified, let `X` equal 2. If the new
+tip file would have `N` commits and the previous tip has `M` commits and
+`X` times `N` is greater than  `M`, instead merge the two files into a
+single file.
++
+* If `--max-commits=<M>` is specified with `M` a positive integer, and the
+new tip file would have more than `M` commits, then instead merge the new
+tip with the previous tip.
++
+Finally, if `--expire-time=<datetime>` is not specified, let `datetime`
+be the current time. After writing the split commit-graph, delete all
+unused commit-graph whose modified times are older than `datetime`.
 
 'read'::
 
@@ -61,6 +80,9 @@ Used for debugging purposes.
 
 Read the commit-graph file and verify its contents against the object
 database. Used to check for corrupted data.
++
+With the `--shallow` option, only check the tip commit-graph file in
+a chain of split commit-graphs.
 
 
 EXAMPLES
index f7778a2c85c1aaa1295dbf3b9e4f74f3683def23..233b2b786271cc695268d2f7c0139d02228bd3c2 100644 (file)
@@ -9,7 +9,7 @@ git-multi-pack-index - Write and verify multi-pack-indexes
 SYNOPSIS
 --------
 [verse]
-'git multi-pack-index' [--object-dir=<dir>] <verb>
+'git multi-pack-index' [--object-dir=<dir>] <subcommand>
 
 DESCRIPTION
 -----------
@@ -23,13 +23,35 @@ OPTIONS
        `<dir>/packs/multi-pack-index` for the current MIDX file, and
        `<dir>/packs` for the pack-files to index.
 
+The following subcommands are available:
+
 write::
-       When given as the verb, write a new MIDX file to
-       `<dir>/packs/multi-pack-index`.
+       Write a new MIDX file.
 
 verify::
-       When given as the verb, verify the contents of the MIDX file
-       at `<dir>/packs/multi-pack-index`.
+       Verify the contents of the MIDX file.
+
+expire::
+       Delete the pack-files that are tracked  by the MIDX file, but
+       have no objects referenced by the MIDX. Rewrite the MIDX file
+       afterward to remove all references to these pack-files.
+
+repack::
+       Create a new pack-file containing objects in small pack-files
+       referenced by the multi-pack-index. If the size given by the
+       `--batch-size=<size>` argument is zero, then create a pack
+       containing all objects referenced by the multi-pack-index. For
+       a non-zero batch size, Select the pack-files by examining packs
+       from oldest-to-newest, computing the "expected size" by counting
+       the number of objects in the pack referenced by the
+       multi-pack-index, then divide by the total number of objects in
+       the pack and multiply by the pack size. We select packs with
+       expected size below the batch size until the set of packs have
+       total expected size at least the batch size. If the total size
+       does not reach the batch size, then do nothing. If a new pack-
+       file is created, rewrite the multi-pack-index to reference the
+       new pack-file. A later run of 'git multi-pack-index expire' will
+       delete the pack-files that were part of this batch.
 
 
 EXAMPLES
index fae4d66547fb900d79d2bafad52cf0aba9a51158..9d22270757c9b5d402f680a3f5933989678cb6f0 100644 (file)
@@ -9,9 +9,7 @@ SYNOPSIS
 --------
 [verse]
 'git revert' [--[no-]edit] [-n] [-m parent-number] [-s] [-S[<keyid>]] <commit>...
-'git revert' --continue
-'git revert' --quit
-'git revert' --abort
+'git revert' (--continue | --skip | --abort | --quit)
 
 DESCRIPTION
 -----------
index 286fc163f14256f4f7ff4be7fba57cef963d2f75..bb1251c0364dc71880f6e63db7c6116ed859b90f 100644 (file)
@@ -182,6 +182,14 @@ explicitly.
        Pretend as if all objects mentioned by reflogs are listed on the
        command line as `<commit>`.
 
+--alternate-refs::
+       Pretend as if all objects mentioned as ref tips of alternate
+       repositories were listed on the command line. An alternate
+       repository is any repository whose object directory is specified
+       in `objects/info/alternates`.  The set of included objects may
+       be modified by `core.alternateRefsCommand`, etc. See
+       linkgit:git-config[1].
+
 --single-worktree::
        By default, all working trees will be examined by the
        following options when there are more than one (see
index 5a57c4a4077f0bba7bb0186932a166f2dc7666ce..3bceb564741158dfcebbfabf6f01a9b2f444494f 100644 (file)
@@ -3,6 +3,10 @@
        `.git/sequencer`.  Can be used to continue after resolving
        conflicts in a failed cherry-pick or revert.
 
+--skip::
+       Skip the current commit and continue with the rest of the
+       sequence.
+
 --quit::
        Forget about the current operation in progress.  Can be used
        to clear the sequencer state after a failed cherry-pick or
index f7ffe7d5998c6ec6f7ed4ec420c938d7f129c94f..71eb081fed25a7314a822087056a6c5b9a72d944 100644 (file)
@@ -668,7 +668,7 @@ completed.)
        "event":"signal",
        ...
        "t_abs":0.001227,  # elapsed time in seconds
-       "signal":13        # SIGTERM, SIGINT, etc.
+       "signo":13         # SIGTERM, SIGINT, etc.
 }
 ------------
 
index 16452a0504c8fa5b9b1b62cb907b96315db43942..a4f17441aed30f14c036a4bed6a911c86cf31ce5 100644 (file)
@@ -44,8 +44,9 @@ HEADER:
 
   1-byte number (C) of "chunks"
 
-  1-byte (reserved for later use)
-     Current clients should ignore this value.
+  1-byte number (B) of base commit-graphs
+      We infer the length (H*B) of the Base Graphs chunk
+      from this value.
 
 CHUNK LOOKUP:
 
@@ -92,6 +93,12 @@ CHUNK DATA:
       positions for the parents until reaching a value with the most-significant
       bit on. The other bits correspond to the position of the last parent.
 
+  Base Graphs List (ID: {'B', 'A', 'S', 'E'}) [Optional]
+      This list of H-byte hashes describe a set of B commit-graph files that
+      form a commit-graph chain. The graph position for the ith commit in this
+      file's OID Lookup chunk is equal to i plus the number of commits in all
+      base graphs.  If B is non-zero, this chunk must exist.
+
 TRAILER:
 
        H-byte HASH-checksum of all of the above.
index fb53341d5ee3116361d599b5aca2e5ea3a228589..729fbcb32f8793d06da3a985bb6d8a299b3a15dd 100644 (file)
@@ -127,6 +127,197 @@ Design Details
   helpful for these clones, anyway. The commit-graph will not be read or
   written when shallow commits are present.
 
+Commit Graphs Chains
+--------------------
+
+Typically, repos grow with near-constant velocity (commits per day). Over time,
+the number of commits added by a fetch operation is much smaller than the
+number of commits in the full history. By creating a "chain" of commit-graphs,
+we enable fast writes of new commit data without rewriting the entire commit
+history -- at least, most of the time.
+
+## File Layout
+
+A commit-graph chain uses multiple files, and we use a fixed naming convention
+to organize these files. Each commit-graph file has a name
+`$OBJDIR/info/commit-graphs/graph-{hash}.graph` where `{hash}` is the hex-
+valued hash stored in the footer of that file (which is a hash of the file's
+contents before that hash). For a chain of commit-graph files, a plain-text
+file at `$OBJDIR/info/commit-graphs/commit-graph-chain` contains the
+hashes for the files in order from "lowest" to "highest".
+
+For example, if the `commit-graph-chain` file contains the lines
+
+```
+       {hash0}
+       {hash1}
+       {hash2}
+```
+
+then the commit-graph chain looks like the following diagram:
+
+ +-----------------------+
+ |  graph-{hash2}.graph  |
+ +-----------------------+
+         |
+ +-----------------------+
+ |                       |
+ |  graph-{hash1}.graph  |
+ |                       |
+ +-----------------------+
+         |
+ +-----------------------+
+ |                       |
+ |                       |
+ |                       |
+ |  graph-{hash0}.graph  |
+ |                       |
+ |                       |
+ |                       |
+ +-----------------------+
+
+Let X0 be the number of commits in `graph-{hash0}.graph`, X1 be the number of
+commits in `graph-{hash1}.graph`, and X2 be the number of commits in
+`graph-{hash2}.graph`. If a commit appears in position i in `graph-{hash2}.graph`,
+then we interpret this as being the commit in position (X0 + X1 + i), and that
+will be used as its "graph position". The commits in `graph-{hash2}.graph` use these
+positions to refer to their parents, which may be in `graph-{hash1}.graph` or
+`graph-{hash0}.graph`. We can navigate to an arbitrary commit in position j by checking
+its containment in the intervals [0, X0), [X0, X0 + X1), [X0 + X1, X0 + X1 +
+X2).
+
+Each commit-graph file (except the base, `graph-{hash0}.graph`) contains data
+specifying the hashes of all files in the lower layers. In the above example,
+`graph-{hash1}.graph` contains `{hash0}` while `graph-{hash2}.graph` contains
+`{hash0}` and `{hash1}`.
+
+## Merging commit-graph files
+
+If we only added a new commit-graph file on every write, we would run into a
+linear search problem through many commit-graph files.  Instead, we use a merge
+strategy to decide when the stack should collapse some number of levels.
+
+The diagram below shows such a collapse. As a set of new commits are added, it
+is determined by the merge strategy that the files should collapse to
+`graph-{hash1}`. Thus, the new commits, the commits in `graph-{hash2}` and
+the commits in `graph-{hash1}` should be combined into a new `graph-{hash3}`
+file.
+
+                           +---------------------+
+                           |                     |
+                           |    (new commits)    |
+                           |                     |
+                           +---------------------+
+                           |                     |
+ +-----------------------+  +---------------------+
+ |  graph-{hash2} |->|                     |
+ +-----------------------+  +---------------------+
+         |                 |                     |
+ +-----------------------+  +---------------------+
+ |                       |  |                     |
+ |  graph-{hash1} |->|                     |
+ |                       |  |                     |
+ +-----------------------+  +---------------------+
+         |                  tmp_graphXXX
+ +-----------------------+
+ |                       |
+ |                       |
+ |                       |
+ |  graph-{hash0} |
+ |                       |
+ |                       |
+ |                       |
+ +-----------------------+
+
+During this process, the commits to write are combined, sorted and we write the
+contents to a temporary file, all while holding a `commit-graph-chain.lock`
+lock-file.  When the file is flushed, we rename it to `graph-{hash3}`
+according to the computed `{hash3}`. Finally, we write the new chain data to
+`commit-graph-chain.lock`:
+
+```
+       {hash3}
+       {hash0}
+```
+
+We then close the lock-file.
+
+## Merge Strategy
+
+When writing a set of commits that do not exist in the commit-graph stack of
+height N, we default to creating a new file at level N + 1. We then decide to
+merge with the Nth level if one of two conditions hold:
+
+  1. `--size-multiple=<X>` is specified or X = 2, and the number of commits in
+     level N is less than X times the number of commits in level N + 1.
+
+  2. `--max-commits=<C>` is specified with non-zero C and the number of commits
+     in level N + 1 is more than C commits.
+
+This decision cascades down the levels: when we merge a level we create a new
+set of commits that then compares to the next level.
+
+The first condition bounds the number of levels to be logarithmic in the total
+number of commits.  The second condition bounds the total number of commits in
+a `graph-{hashN}` file and not in the `commit-graph` file, preventing
+significant performance issues when the stack merges and another process only
+partially reads the previous stack.
+
+The merge strategy values (2 for the size multiple, 64,000 for the maximum
+number of commits) could be extracted into config settings for full
+flexibility.
+
+## Deleting graph-{hash} files
+
+After a new tip file is written, some `graph-{hash}` files may no longer
+be part of a chain. It is important to remove these files from disk, eventually.
+The main reason to delay removal is that another process could read the
+`commit-graph-chain` file before it is rewritten, but then look for the
+`graph-{hash}` files after they are deleted.
+
+To allow holding old split commit-graphs for a while after they are unreferenced,
+we update the modified times of the files when they become unreferenced. Then,
+we scan the `$OBJDIR/info/commit-graphs/` directory for `graph-{hash}`
+files whose modified times are older than a given expiry window. This window
+defaults to zero, but can be changed using command-line arguments or a config
+setting.
+
+## Chains across multiple object directories
+
+In a repo with alternates, we look for the `commit-graph-chain` file starting
+in the local object directory and then in each alternate. The first file that
+exists defines our chain. As we look for the `graph-{hash}` files for
+each `{hash}` in the chain file, we follow the same pattern for the host
+directories.
+
+This allows commit-graphs to be split across multiple forks in a fork network.
+The typical case is a large "base" repo with many smaller forks.
+
+As the base repo advances, it will likely update and merge its commit-graph
+chain more frequently than the forks. If a fork updates their commit-graph after
+the base repo, then it should "reparent" the commit-graph chain onto the new
+chain in the base repo. When reading each `graph-{hash}` file, we track
+the object directory containing it. During a write of a new commit-graph file,
+we check for any changes in the source object directory and read the
+`commit-graph-chain` file for that source and create a new file based on those
+files. During this "reparent" operation, we necessarily need to collapse all
+levels in the fork, as all of the files are invalid against the new base file.
+
+It is crucial to be careful when cleaning up "unreferenced" `graph-{hash}.graph`
+files in this scenario. It falls to the user to define the proper settings for
+their custom environment:
+
+ 1. When merging levels in the base repo, the unreferenced files may still be
+    referenced by chains from fork repos.
+
+ 2. The expiry time should be set to a length of time such that every fork has
+    time to recompute their commit-graph chain to "reparent" onto the new base
+    file(s).
+
+ 3. If the commit-graph chain is updated in the base, the fork will not have
+    access to the new chain until its chain is updated to reference those files.
+    (This may change in the future [5].)
+
 Related Links
 -------------
 [0] https://bugs.chromium.org/p/git/issues/detail?id=8
@@ -153,3 +344,7 @@ Related Links
 
 [4] https://public-inbox.org/git/20180108154822.54829-1-git@jeffhostetler.com/T/#u
     A patch to remove the ahead-behind calculation from 'status'.
+
+[5] https://public-inbox.org/git/f27db281-abad-5043-6d71-cbb083b1c877@gmail.com/
+    A discussion of a "two-dimensional graph position" that can allow reading
+    multiple commit-graph chains at the same time.
index 98a0588416c4f3f7a5b48ad72a6c2fd54ff94120..b11cdd4fe79234b57211d236930b0798416b8e6c 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -721,6 +721,7 @@ TEST_BUILTINS_OBJS += test-lazy-init-name-hash.o
 TEST_BUILTINS_OBJS += test-match-trees.o
 TEST_BUILTINS_OBJS += test-mergesort.o
 TEST_BUILTINS_OBJS += test-mktemp.o
+TEST_BUILTINS_OBJS += test-oidmap.o
 TEST_BUILTINS_OBJS += test-online-cpus.o
 TEST_BUILTINS_OBJS += test-parse-options.o
 TEST_BUILTINS_OBJS += test-path-utils.o
@@ -1061,6 +1062,7 @@ BUILTIN_OBJS += builtin/diff-index.o
 BUILTIN_OBJS += builtin/diff-tree.o
 BUILTIN_OBJS += builtin/diff.o
 BUILTIN_OBJS += builtin/difftool.o
+BUILTIN_OBJS += builtin/env--helper.o
 BUILTIN_OBJS += builtin/fast-export.o
 BUILTIN_OBJS += builtin/fetch-pack.o
 BUILTIN_OBJS += builtin/fetch.o
index 67de6dece4284a52fef27f3e7ef662d68eaa8109..3ee0ee2d8fbb04dabcfc0152dc741c7d1f28b220 100644 (file)
--- a/advice.c
+++ b/advice.c
@@ -17,6 +17,7 @@ int advice_status_ahead_behind_warning = 1;
 int advice_commit_before_merge = 1;
 int advice_reset_quiet_warning = 1;
 int advice_resolve_conflict = 1;
+int advice_sequencer_in_use = 1;
 int advice_implicit_identity = 1;
 int advice_detached_head = 1;
 int advice_set_upstream_failure = 1;
@@ -75,6 +76,7 @@ static struct {
        { "commitBeforeMerge", &advice_commit_before_merge },
        { "resetQuiet", &advice_reset_quiet_warning },
        { "resolveConflict", &advice_resolve_conflict },
+       { "sequencerInUse", &advice_sequencer_in_use },
        { "implicitIdentity", &advice_implicit_identity },
        { "detachedHead", &advice_detached_head },
        { "setupStreamFailure", &advice_set_upstream_failure },
index 940c4c253e25644cd85362418ea965dd0bb51a3d..d0154048431c773b9bf0ec450478a93d5ede06c0 100644 (file)
--- a/advice.h
+++ b/advice.h
@@ -17,6 +17,7 @@ extern int advice_status_ahead_behind_warning;
 extern int advice_commit_before_merge;
 extern int advice_reset_quiet_warning;
 extern int advice_resolve_conflict;
+extern int advice_sequencer_in_use;
 extern int advice_implicit_identity;
 extern int advice_detached_head;
 extern int advice_set_upstream_failure;
diff --git a/apply.c b/apply.c
index 4992eca416c9c2b5d21de42460cafa839df25f4e..cde95369bb3f3a9c763108fd91e1e48e0e455e29 100644 (file)
--- a/apply.c
+++ b/apply.c
 #include "rerere.h"
 #include "apply.h"
 
+struct gitdiff_data {
+       struct strbuf *root;
+       int linenr;
+       int p_value;
+};
+
 static void git_apply_config(void)
 {
        git_config_get_string_const("apply.whitespace", &apply_default_whitespace);
@@ -201,40 +207,6 @@ struct fragment {
 #define BINARY_DELTA_DEFLATED  1
 #define BINARY_LITERAL_DEFLATED 2
 
-/*
- * This represents a "patch" to a file, both metainfo changes
- * such as creation/deletion, filemode and content changes represented
- * as a series of fragments.
- */
-struct patch {
-       char *new_name, *old_name, *def_name;
-       unsigned int old_mode, new_mode;
-       int is_new, is_delete;  /* -1 = unknown, 0 = false, 1 = true */
-       int rejected;
-       unsigned ws_rule;
-       int lines_added, lines_deleted;
-       int score;
-       int extension_linenr; /* first line specifying delete/new/rename/copy */
-       unsigned int is_toplevel_relative:1;
-       unsigned int inaccurate_eof:1;
-       unsigned int is_binary:1;
-       unsigned int is_copy:1;
-       unsigned int is_rename:1;
-       unsigned int recount:1;
-       unsigned int conflicted_threeway:1;
-       unsigned int direct_to_threeway:1;
-       unsigned int crlf_in_old:1;
-       struct fragment *fragments;
-       char *result;
-       size_t resultsize;
-       char old_oid_prefix[GIT_MAX_HEXSZ + 1];
-       char new_oid_prefix[GIT_MAX_HEXSZ + 1];
-       struct patch *next;
-
-       /* three-way fallback result */
-       struct object_id threeway_stage[3];
-};
-
 static void free_fragment_list(struct fragment *list)
 {
        while (list) {
@@ -469,7 +441,7 @@ static char *squash_slash(char *name)
        return name;
 }
 
-static char *find_name_gnu(struct apply_state *state,
+static char *find_name_gnu(struct strbuf *root,
                           const char *line,
                           int p_value)
 {
@@ -478,7 +450,7 @@ static char *find_name_gnu(struct apply_state *state,
 
        /*
         * Proposed "new-style" GNU patch/diff format; see
-        * http://marc.info/?l=git&m=112927316408690&w=2
+        * https://public-inbox.org/git/7vll0wvb2a.fsf@assigned-by-dhcp.cox.net/
         */
        if (unquote_c_style(&name, line, NULL)) {
                strbuf_release(&name);
@@ -495,8 +467,8 @@ static char *find_name_gnu(struct apply_state *state,
        }
 
        strbuf_remove(&name, 0, cp - name.buf);
-       if (state->root.len)
-               strbuf_insert(&name, 0, state->root.buf, state->root.len);
+       if (root->len)
+               strbuf_insert(&name, 0, root->buf, root->len);
        return squash_slash(strbuf_detach(&name, NULL));
 }
 
@@ -659,7 +631,7 @@ static size_t diff_timestamp_len(const char *line, size_t len)
        return line + len - end;
 }
 
-static char *find_name_common(struct apply_state *state,
+static char *find_name_common(struct strbuf *root,
                              const char *line,
                              const char *def,
                              int p_value,
@@ -702,30 +674,30 @@ static char *find_name_common(struct apply_state *state,
                        return squash_slash(xstrdup(def));
        }
 
-       if (state->root.len) {
-               char *ret = xstrfmt("%s%.*s", state->root.buf, len, start);
+       if (root->len) {
+               char *ret = xstrfmt("%s%.*s", root->buf, len, start);
                return squash_slash(ret);
        }
 
        return squash_slash(xmemdupz(start, len));
 }
 
-static char *find_name(struct apply_state *state,
+static char *find_name(struct strbuf *root,
                       const char *line,
                       char *def,
                       int p_value,
                       int terminate)
 {
        if (*line == '"') {
-               char *name = find_name_gnu(state, line, p_value);
+               char *name = find_name_gnu(root, line, p_value);
                if (name)
                        return name;
        }
 
-       return find_name_common(state, line, def, p_value, NULL, terminate);
+       return find_name_common(root, line, def, p_value, NULL, terminate);
 }
 
-static char *find_name_traditional(struct apply_state *state,
+static char *find_name_traditional(struct strbuf *root,
                                   const char *line,
                                   char *def,
                                   int p_value)
@@ -734,7 +706,7 @@ static char *find_name_traditional(struct apply_state *state,
        size_t date_len;
 
        if (*line == '"') {
-               char *name = find_name_gnu(state, line, p_value);
+               char *name = find_name_gnu(root, line, p_value);
                if (name)
                        return name;
        }
@@ -742,10 +714,10 @@ static char *find_name_traditional(struct apply_state *state,
        len = strchrnul(line, '\n') - line;
        date_len = diff_timestamp_len(line, len);
        if (!date_len)
-               return find_name_common(state, line, def, p_value, NULL, TERM_TAB);
+               return find_name_common(root, line, def, p_value, NULL, TERM_TAB);
        len -= date_len;
 
-       return find_name_common(state, line, def, p_value, line + len, 0);
+       return find_name_common(root, line, def, p_value, line + len, 0);
 }
 
 /*
@@ -759,7 +731,7 @@ static int guess_p_value(struct apply_state *state, const char *nameline)
 
        if (is_dev_null(nameline))
                return -1;
-       name = find_name_traditional(state, nameline, NULL, 0);
+       name = find_name_traditional(&state->root, nameline, NULL, 0);
        if (!name)
                return -1;
        cp = strchr(name, '/');
@@ -883,17 +855,17 @@ static int parse_traditional_patch(struct apply_state *state,
        if (is_dev_null(first)) {
                patch->is_new = 1;
                patch->is_delete = 0;
-               name = find_name_traditional(state, second, NULL, state->p_value);
+               name = find_name_traditional(&state->root, second, NULL, state->p_value);
                patch->new_name = name;
        } else if (is_dev_null(second)) {
                patch->is_new = 0;
                patch->is_delete = 1;
-               name = find_name_traditional(state, first, NULL, state->p_value);
+               name = find_name_traditional(&state->root, first, NULL, state->p_value);
                patch->old_name = name;
        } else {
                char *first_name;
-               first_name = find_name_traditional(state, first, NULL, state->p_value);
-               name = find_name_traditional(state, second, first_name, state->p_value);
+               first_name = find_name_traditional(&state->root, first, NULL, state->p_value);
+               name = find_name_traditional(&state->root, second, first_name, state->p_value);
                free(first_name);
                if (has_epoch_timestamp(first)) {
                        patch->is_new = 1;
@@ -914,7 +886,7 @@ static int parse_traditional_patch(struct apply_state *state,
        return 0;
 }
 
-static int gitdiff_hdrend(struct apply_state *state,
+static int gitdiff_hdrend(struct gitdiff_data *state,
                          const char *line,
                          struct patch *patch)
 {
@@ -933,14 +905,14 @@ static int gitdiff_hdrend(struct apply_state *state,
 #define DIFF_OLD_NAME 0
 #define DIFF_NEW_NAME 1
 
-static int gitdiff_verify_name(struct apply_state *state,
+static int gitdiff_verify_name(struct gitdiff_data *state,
                               const char *line,
                               int isnull,
                               char **name,
                               int side)
 {
        if (!*name && !isnull) {
-               *name = find_name(state, line, NULL, state->p_value, TERM_TAB);
+               *name = find_name(state->root, line, NULL, state->p_value, TERM_TAB);
                return 0;
        }
 
@@ -949,7 +921,7 @@ static int gitdiff_verify_name(struct apply_state *state,
                if (isnull)
                        return error(_("git apply: bad git-diff - expected /dev/null, got %s on line %d"),
                                     *name, state->linenr);
-               another = find_name(state, line, NULL, state->p_value, TERM_TAB);
+               another = find_name(state->root, line, NULL, state->p_value, TERM_TAB);
                if (!another || strcmp(another, *name)) {
                        free(another);
                        return error((side == DIFF_NEW_NAME) ?
@@ -965,7 +937,7 @@ static int gitdiff_verify_name(struct apply_state *state,
        return 0;
 }
 
-static int gitdiff_oldname(struct apply_state *state,
+static int gitdiff_oldname(struct gitdiff_data *state,
                           const char *line,
                           struct patch *patch)
 {
@@ -974,7 +946,7 @@ static int gitdiff_oldname(struct apply_state *state,
                                   DIFF_OLD_NAME);
 }
 
-static int gitdiff_newname(struct apply_state *state,
+static int gitdiff_newname(struct gitdiff_data *state,
                           const char *line,
                           struct patch *patch)
 {
@@ -992,21 +964,21 @@ static int parse_mode_line(const char *line, int linenr, unsigned int *mode)
        return 0;
 }
 
-static int gitdiff_oldmode(struct apply_state *state,
+static int gitdiff_oldmode(struct gitdiff_data *state,
                           const char *line,
                           struct patch *patch)
 {
        return parse_mode_line(line, state->linenr, &patch->old_mode);
 }
 
-static int gitdiff_newmode(struct apply_state *state,
+static int gitdiff_newmode(struct gitdiff_data *state,
                           const char *line,
                           struct patch *patch)
 {
        return parse_mode_line(line, state->linenr, &patch->new_mode);
 }
 
-static int gitdiff_delete(struct apply_state *state,
+static int gitdiff_delete(struct gitdiff_data *state,
                          const char *line,
                          struct patch *patch)
 {
@@ -1016,7 +988,7 @@ static int gitdiff_delete(struct apply_state *state,
        return gitdiff_oldmode(state, line, patch);
 }
 
-static int gitdiff_newfile(struct apply_state *state,
+static int gitdiff_newfile(struct gitdiff_data *state,
                           const char *line,
                           struct patch *patch)
 {
@@ -1026,47 +998,47 @@ static int gitdiff_newfile(struct apply_state *state,
        return gitdiff_newmode(state, line, patch);
 }
 
-static int gitdiff_copysrc(struct apply_state *state,
+static int gitdiff_copysrc(struct gitdiff_data *state,
                           const char *line,
                           struct patch *patch)
 {
        patch->is_copy = 1;
        free(patch->old_name);
-       patch->old_name = find_name(state, line, NULL, state->p_value ? state->p_value - 1 : 0, 0);
+       patch->old_name = find_name(state->root, line, NULL, state->p_value ? state->p_value - 1 : 0, 0);
        return 0;
 }
 
-static int gitdiff_copydst(struct apply_state *state,
+static int gitdiff_copydst(struct gitdiff_data *state,
                           const char *line,
                           struct patch *patch)
 {
        patch->is_copy = 1;
        free(patch->new_name);
-       patch->new_name = find_name(state, line, NULL, state->p_value ? state->p_value - 1 : 0, 0);
+       patch->new_name = find_name(state->root, line, NULL, state->p_value ? state->p_value - 1 : 0, 0);
        return 0;
 }
 
-static int gitdiff_renamesrc(struct apply_state *state,
+static int gitdiff_renamesrc(struct gitdiff_data *state,
                             const char *line,
                             struct patch *patch)
 {
        patch->is_rename = 1;
        free(patch->old_name);
-       patch->old_name = find_name(state, line, NULL, state->p_value ? state->p_value - 1 : 0, 0);
+       patch->old_name = find_name(state->root, line, NULL, state->p_value ? state->p_value - 1 : 0, 0);
        return 0;
 }
 
-static int gitdiff_renamedst(struct apply_state *state,
+static int gitdiff_renamedst(struct gitdiff_data *state,
                             const char *line,
                             struct patch *patch)
 {
        patch->is_rename = 1;
        free(patch->new_name);
-       patch->new_name = find_name(state, line, NULL, state->p_value ? state->p_value - 1 : 0, 0);
+       patch->new_name = find_name(state->root, line, NULL, state->p_value ? state->p_value - 1 : 0, 0);
        return 0;
 }
 
-static int gitdiff_similarity(struct apply_state *state,
+static int gitdiff_similarity(struct gitdiff_data *state,
                              const char *line,
                              struct patch *patch)
 {
@@ -1076,7 +1048,7 @@ static int gitdiff_similarity(struct apply_state *state,
        return 0;
 }
 
-static int gitdiff_dissimilarity(struct apply_state *state,
+static int gitdiff_dissimilarity(struct gitdiff_data *state,
                                 const char *line,
                                 struct patch *patch)
 {
@@ -1086,7 +1058,7 @@ static int gitdiff_dissimilarity(struct apply_state *state,
        return 0;
 }
 
-static int gitdiff_index(struct apply_state *state,
+static int gitdiff_index(struct gitdiff_data *state,
                         const char *line,
                         struct patch *patch)
 {
@@ -1126,7 +1098,7 @@ static int gitdiff_index(struct apply_state *state,
  * This is normal for a diff that doesn't change anything: we'll fall through
  * into the next diff. Tell the parser to break out.
  */
-static int gitdiff_unrecognized(struct apply_state *state,
+static int gitdiff_unrecognized(struct gitdiff_data *state,
                                const char *line,
                                struct patch *patch)
 {
@@ -1137,17 +1109,17 @@ static int gitdiff_unrecognized(struct apply_state *state,
  * Skip p_value leading components from "line"; as we do not accept
  * absolute paths, return NULL in that case.
  */
-static const char *skip_tree_prefix(struct apply_state *state,
+static const char *skip_tree_prefix(int p_value,
                                    const char *line,
                                    int llen)
 {
        int nslash;
        int i;
 
-       if (!state->p_value)
+       if (!p_value)
                return (llen && line[0] == '/') ? NULL : line;
 
-       nslash = state->p_value;
+       nslash = p_value;
        for (i = 0; i < llen; i++) {
                int ch = line[i];
                if (ch == '/' && --nslash <= 0)
@@ -1164,7 +1136,7 @@ static const char *skip_tree_prefix(struct apply_state *state,
  * creation or deletion of an empty file.  In any of these cases,
  * both sides are the same name under a/ and b/ respectively.
  */
-static char *git_header_name(struct apply_state *state,
+static char *git_header_name(int p_value,
                             const char *line,
                             int llen)
 {
@@ -1184,7 +1156,7 @@ static char *git_header_name(struct apply_state *state,
                        goto free_and_fail1;
 
                /* strip the a/b prefix including trailing slash */
-               cp = skip_tree_prefix(state, first.buf, first.len);
+               cp = skip_tree_prefix(p_value, first.buf, first.len);
                if (!cp)
                        goto free_and_fail1;
                strbuf_remove(&first, 0, cp - first.buf);
@@ -1201,7 +1173,7 @@ static char *git_header_name(struct apply_state *state,
                if (*second == '"') {
                        if (unquote_c_style(&sp, second, NULL))
                                goto free_and_fail1;
-                       cp = skip_tree_prefix(state, sp.buf, sp.len);
+                       cp = skip_tree_prefix(p_value, sp.buf, sp.len);
                        if (!cp)
                                goto free_and_fail1;
                        /* They must match, otherwise ignore */
@@ -1212,7 +1184,7 @@ static char *git_header_name(struct apply_state *state,
                }
 
                /* unquoted second */
-               cp = skip_tree_prefix(state, second, line + llen - second);
+               cp = skip_tree_prefix(p_value, second, line + llen - second);
                if (!cp)
                        goto free_and_fail1;
                if (line + llen - cp != first.len ||
@@ -1227,7 +1199,7 @@ static char *git_header_name(struct apply_state *state,
        }
 
        /* unquoted first name */
-       name = skip_tree_prefix(state, line, llen);
+       name = skip_tree_prefix(p_value, line, llen);
        if (!name)
                return NULL;
 
@@ -1243,7 +1215,7 @@ static char *git_header_name(struct apply_state *state,
                        if (unquote_c_style(&sp, second, NULL))
                                goto free_and_fail2;
 
-                       np = skip_tree_prefix(state, sp.buf, sp.len);
+                       np = skip_tree_prefix(p_value, sp.buf, sp.len);
                        if (!np)
                                goto free_and_fail2;
 
@@ -1287,7 +1259,7 @@ static char *git_header_name(struct apply_state *state,
                         */
                        if (!name[len + 1])
                                return NULL; /* no postimage name */
-                       second = skip_tree_prefix(state, name + len + 1,
+                       second = skip_tree_prefix(p_value, name + len + 1,
                                                  line_len - (len + 1));
                        if (!second)
                                return NULL;
@@ -1302,26 +1274,28 @@ static char *git_header_name(struct apply_state *state,
        }
 }
 
-static int check_header_line(struct apply_state *state, struct patch *patch)
+static int check_header_line(int linenr, struct patch *patch)
 {
        int extensions = (patch->is_delete == 1) + (patch->is_new == 1) +
                         (patch->is_rename == 1) + (patch->is_copy == 1);
        if (extensions > 1)
                return error(_("inconsistent header lines %d and %d"),
-                            patch->extension_linenr, state->linenr);
+                            patch->extension_linenr, linenr);
        if (extensions && !patch->extension_linenr)
-               patch->extension_linenr = state->linenr;
+               patch->extension_linenr = linenr;
        return 0;
 }
 
-/* Verify that we recognize the lines following a git header */
-static int parse_git_header(struct apply_state *state,
-                           const char *line,
-                           int len,
-                           unsigned int size,
-                           struct patch *patch)
+int parse_git_diff_header(struct strbuf *root,
+                         int *linenr,
+                         int p_value,
+                         const char *line,
+                         int len,
+                         unsigned int size,
+                         struct patch *patch)
 {
        unsigned long offset;
+       struct gitdiff_data parse_hdr_state;
 
        /* A git diff has explicit new/delete information, so we don't guess */
        patch->is_new = 0;
@@ -1333,20 +1307,24 @@ static int parse_git_header(struct apply_state *state,
         * or removing or adding empty files), so we get
         * the default name from the header.
         */
-       patch->def_name = git_header_name(state, line, len);
-       if (patch->def_name && state->root.len) {
-               char *s = xstrfmt("%s%s", state->root.buf, patch->def_name);
+       patch->def_name = git_header_name(p_value, line, len);
+       if (patch->def_name && root->len) {
+               char *s = xstrfmt("%s%s", root->buf, patch->def_name);
                free(patch->def_name);
                patch->def_name = s;
        }
 
        line += len;
        size -= len;
-       state->linenr++;
-       for (offset = len ; size > 0 ; offset += len, size -= len, line += len, state->linenr++) {
+       (*linenr)++;
+       parse_hdr_state.root = root;
+       parse_hdr_state.linenr = *linenr;
+       parse_hdr_state.p_value = p_value;
+
+       for (offset = len ; size > 0 ; offset += len, size -= len, line += len, (*linenr)++) {
                static const struct opentry {
                        const char *str;
-                       int (*fn)(struct apply_state *, const char *, struct patch *);
+                       int (*fn)(struct gitdiff_data *, const char *, struct patch *);
                } optable[] = {
                        { "@@ -", gitdiff_hdrend },
                        { "--- ", gitdiff_oldname },
@@ -1377,10 +1355,10 @@ static int parse_git_header(struct apply_state *state,
                        int res;
                        if (len < oplen || memcmp(p->str, line, oplen))
                                continue;
-                       res = p->fn(state, line + oplen, patch);
+                       res = p->fn(&parse_hdr_state, line + oplen, patch);
                        if (res < 0)
                                return -1;
-                       if (check_header_line(state, patch))
+                       if (check_header_line(*linenr, patch))
                                return -1;
                        if (res > 0)
                                return offset;
@@ -1561,7 +1539,9 @@ static int find_header(struct apply_state *state,
                 * or mode change, so we handle that specially
                 */
                if (!memcmp("diff --git ", line, 11)) {
-                       int git_hdr_len = parse_git_header(state, line, len, size, patch);
+                       int git_hdr_len = parse_git_diff_header(&state->root, &state->linenr,
+                                                               state->p_value, line, len,
+                                                               size, patch);
                        if (git_hdr_len < 0)
                                return -128;
                        if (git_hdr_len <= len)
diff --git a/apply.h b/apply.h
index 59483481330c61033d56e363f70dff08f4d689af..a7951934352cacf9869221a651504973bd4e7f9e 100644 (file)
--- a/apply.h
+++ b/apply.h
@@ -117,6 +117,40 @@ struct apply_state {
        int applied_after_fixing_ws;
 };
 
+/*
+ * This represents a "patch" to a file, both metainfo changes
+ * such as creation/deletion, filemode and content changes represented
+ * as a series of fragments.
+ */
+struct patch {
+       char *new_name, *old_name, *def_name;
+       unsigned int old_mode, new_mode;
+       int is_new, is_delete;  /* -1 = unknown, 0 = false, 1 = true */
+       int rejected;
+       unsigned ws_rule;
+       int lines_added, lines_deleted;
+       int score;
+       int extension_linenr; /* first line specifying delete/new/rename/copy */
+       unsigned int is_toplevel_relative:1;
+       unsigned int inaccurate_eof:1;
+       unsigned int is_binary:1;
+       unsigned int is_copy:1;
+       unsigned int is_rename:1;
+       unsigned int recount:1;
+       unsigned int conflicted_threeway:1;
+       unsigned int direct_to_threeway:1;
+       unsigned int crlf_in_old:1;
+       struct fragment *fragments;
+       char *result;
+       size_t resultsize;
+       char old_oid_prefix[GIT_MAX_HEXSZ + 1];
+       char new_oid_prefix[GIT_MAX_HEXSZ + 1];
+       struct patch *next;
+
+       /* three-way fallback result */
+       struct object_id threeway_stage[3];
+};
+
 int apply_parse_options(int argc, const char **argv,
                        struct apply_state *state,
                        int *force_apply, int *options,
@@ -127,6 +161,20 @@ int init_apply_state(struct apply_state *state,
 void clear_apply_state(struct apply_state *state);
 int check_apply_state(struct apply_state *state, int force_apply);
 
+/*
+ * Parse a git diff header, starting at line.  Fills the relevant
+ * metadata information in 'struct patch'.
+ *
+ * Returns -1 on failure, the length of the parsed header otherwise.
+ */
+int parse_git_diff_header(struct strbuf *root,
+                         int *linenr,
+                         int p_value,
+                         const char *line,
+                         int len,
+                         unsigned int size,
+                         struct patch *patch);
+
 /*
  * Some aspects of the apply behavior are controlled by the following
  * bits in the "options" parameter passed to apply_all_patches().
index 53141c1f0ee12b4ce14efef1e4026d7ba7d2b665..a8da0fcc4f0cc47e585b3b378109fbd84496ed15 100644 (file)
--- a/archive.c
+++ b/archive.c
@@ -418,7 +418,9 @@ static void parse_treeish_arg(const char **argv,
                unsigned short mode;
                int err;
 
-               err = get_tree_entry(&tree->object.oid, prefix, &tree_oid,
+               err = get_tree_entry(ar_args->repo,
+                                    &tree->object.oid,
+                                    prefix, &tree_oid,
                                     &mode);
                if (err || !S_ISDIR(mode))
                        die(_("current working directory is untracked"));
diff --git a/blame.c b/blame.c
index 145eaf2faf9cf56977da61572c93783ea702b0f9..36a2e7ef119d7bea691babe15f861a0600028196 100644 (file)
--- a/blame.c
+++ b/blame.c
@@ -101,7 +101,7 @@ static void verify_working_tree_path(struct repository *r,
                struct object_id blob_oid;
                unsigned short mode;
 
-               if (!get_tree_entry(commit_oid, path, &blob_oid, &mode) &&
+               if (!get_tree_entry(r, commit_oid, path, &blob_oid, &mode) &&
                    oid_object_info(r, &blob_oid, NULL) == OBJ_BLOB)
                        return;
        }
@@ -311,12 +311,707 @@ static int diff_hunks(mmfile_t *file_a, mmfile_t *file_b,
        return xdi_diff(file_a, file_b, &xpp, &xecfg, &ecb);
 }
 
+static const char *get_next_line(const char *start, const char *end)
+{
+       const char *nl = memchr(start, '\n', end - start);
+
+       return nl ? nl + 1 : end;
+}
+
+static int find_line_starts(int **line_starts, const char *buf,
+                           unsigned long len)
+{
+       const char *end = buf + len;
+       const char *p;
+       int *lineno;
+       int num = 0;
+
+       for (p = buf; p < end; p = get_next_line(p, end))
+               num++;
+
+       ALLOC_ARRAY(*line_starts, num + 1);
+       lineno = *line_starts;
+
+       for (p = buf; p < end; p = get_next_line(p, end))
+               *lineno++ = p - buf;
+
+       *lineno = len;
+
+       return num;
+}
+
+struct fingerprint_entry;
+
+/* A fingerprint is intended to loosely represent a string, such that two
+ * fingerprints can be quickly compared to give an indication of the similarity
+ * of the strings that they represent.
+ *
+ * A fingerprint is represented as a multiset of the lower-cased byte pairs in
+ * the string that it represents. Whitespace is added at each end of the
+ * string. Whitespace pairs are ignored. Whitespace is converted to '\0'.
+ * For example, the string "Darth   Radar" will be converted to the following
+ * fingerprint:
+ * {"\0d", "da", "da", "ar", "ar", "rt", "th", "h\0", "\0r", "ra", "ad", "r\0"}
+ *
+ * The similarity between two fingerprints is the size of the intersection of
+ * their multisets, including repeated elements. See fingerprint_similarity for
+ * examples.
+ *
+ * For ease of implementation, the fingerprint is implemented as a map
+ * of byte pairs to the count of that byte pair in the string, instead of
+ * allowing repeated elements in a set.
+ */
+struct fingerprint {
+       struct hashmap map;
+       /* As we know the maximum number of entries in advance, it's
+        * convenient to store the entries in a single array instead of having
+        * the hashmap manage the memory.
+        */
+       struct fingerprint_entry *entries;
+};
+
+/* A byte pair in a fingerprint. Stores the number of times the byte pair
+ * occurs in the string that the fingerprint represents.
+ */
+struct fingerprint_entry {
+       /* The hashmap entry - the hash represents the byte pair in its
+        * entirety so we don't need to store the byte pair separately.
+        */
+       struct hashmap_entry entry;
+       /* The number of times the byte pair occurs in the string that the
+        * fingerprint represents.
+        */
+       int count;
+};
+
+/* See `struct fingerprint` for an explanation of what a fingerprint is.
+ * \param result the fingerprint of the string is stored here. This must be
+ *              freed later using free_fingerprint.
+ * \param line_begin the start of the string
+ * \param line_end the end of the string
+ */
+static void get_fingerprint(struct fingerprint *result,
+                           const char *line_begin,
+                           const char *line_end)
+{
+       unsigned int hash, c0 = 0, c1;
+       const char *p;
+       int max_map_entry_count = 1 + line_end - line_begin;
+       struct fingerprint_entry *entry = xcalloc(max_map_entry_count,
+               sizeof(struct fingerprint_entry));
+       struct fingerprint_entry *found_entry;
+
+       hashmap_init(&result->map, NULL, NULL, max_map_entry_count);
+       result->entries = entry;
+       for (p = line_begin; p <= line_end; ++p, c0 = c1) {
+               /* Always terminate the string with whitespace.
+                * Normalise whitespace to 0, and normalise letters to
+                * lower case. This won't work for multibyte characters but at
+                * worst will match some unrelated characters.
+                */
+               if ((p == line_end) || isspace(*p))
+                       c1 = 0;
+               else
+                       c1 = tolower(*p);
+               hash = c0 | (c1 << 8);
+               /* Ignore whitespace pairs */
+               if (hash == 0)
+                       continue;
+               hashmap_entry_init(entry, hash);
+
+               found_entry = hashmap_get(&result->map, entry, NULL);
+               if (found_entry) {
+                       found_entry->count += 1;
+               } else {
+                       entry->count = 1;
+                       hashmap_add(&result->map, entry);
+                       ++entry;
+               }
+       }
+}
+
+static void free_fingerprint(struct fingerprint *f)
+{
+       hashmap_free(&f->map, 0);
+       free(f->entries);
+}
+
+/* Calculates the similarity between two fingerprints as the size of the
+ * intersection of their multisets, including repeated elements. See
+ * `struct fingerprint` for an explanation of the fingerprint representation.
+ * The similarity between "cat mat" and "father rather" is 2 because "at" is
+ * present twice in both strings while the similarity between "tim" and "mit"
+ * is 0.
+ */
+static int fingerprint_similarity(struct fingerprint *a, struct fingerprint *b)
+{
+       int intersection = 0;
+       struct hashmap_iter iter;
+       const struct fingerprint_entry *entry_a, *entry_b;
+
+       hashmap_iter_init(&b->map, &iter);
+
+       while ((entry_b = hashmap_iter_next(&iter))) {
+               if ((entry_a = hashmap_get(&a->map, entry_b, NULL))) {
+                       intersection += entry_a->count < entry_b->count ?
+                                       entry_a->count : entry_b->count;
+               }
+       }
+       return intersection;
+}
+
+/* Subtracts byte-pair elements in B from A, modifying A in place.
+ */
+static void fingerprint_subtract(struct fingerprint *a, struct fingerprint *b)
+{
+       struct hashmap_iter iter;
+       struct fingerprint_entry *entry_a;
+       const struct fingerprint_entry *entry_b;
+
+       hashmap_iter_init(&b->map, &iter);
+
+       while ((entry_b = hashmap_iter_next(&iter))) {
+               if ((entry_a = hashmap_get(&a->map, entry_b, NULL))) {
+                       if (entry_a->count <= entry_b->count)
+                               hashmap_remove(&a->map, entry_b, NULL);
+                       else
+                               entry_a->count -= entry_b->count;
+               }
+       }
+}
+
+/* Calculate fingerprints for a series of lines.
+ * Puts the fingerprints in the fingerprints array, which must have been
+ * preallocated to allow storing line_count elements.
+ */
+static void get_line_fingerprints(struct fingerprint *fingerprints,
+                                 const char *content, const int *line_starts,
+                                 long first_line, long line_count)
+{
+       int i;
+       const char *linestart, *lineend;
+
+       line_starts += first_line;
+       for (i = 0; i < line_count; ++i) {
+               linestart = content + line_starts[i];
+               lineend = content + line_starts[i + 1];
+               get_fingerprint(fingerprints + i, linestart, lineend);
+       }
+}
+
+static void free_line_fingerprints(struct fingerprint *fingerprints,
+                                  int nr_fingerprints)
+{
+       int i;
+
+       for (i = 0; i < nr_fingerprints; i++)
+               free_fingerprint(&fingerprints[i]);
+}
+
+/* This contains the data necessary to linearly map a line number in one half
+ * of a diff chunk to the line in the other half of the diff chunk that is
+ * closest in terms of its position as a fraction of the length of the chunk.
+ */
+struct line_number_mapping {
+       int destination_start, destination_length,
+               source_start, source_length;
+};
+
+/* Given a line number in one range, offset and scale it to map it onto the
+ * other range.
+ * Essentially this mapping is a simple linear equation but the calculation is
+ * more complicated to allow performing it with integer operations.
+ * Another complication is that if a line could map onto many lines in the
+ * destination range then we want to choose the line at the center of those
+ * possibilities.
+ * Example: if the chunk is 2 lines long in A and 10 lines long in B then the
+ * first 5 lines in B will map onto the first line in the A chunk, while the
+ * last 5 lines will all map onto the second line in the A chunk.
+ * Example: if the chunk is 10 lines long in A and 2 lines long in B then line
+ * 0 in B will map onto line 2 in A, and line 1 in B will map onto line 7 in A.
+ */
+static int map_line_number(int line_number,
+       const struct line_number_mapping *mapping)
+{
+       return ((line_number - mapping->source_start) * 2 + 1) *
+              mapping->destination_length /
+              (mapping->source_length * 2) +
+              mapping->destination_start;
+}
+
+/* Get a pointer to the element storing the similarity between a line in A
+ * and a line in B.
+ *
+ * The similarities are stored in a 2-dimensional array. Each "row" in the
+ * array contains the similarities for a line in B. The similarities stored in
+ * a row are the similarities between the line in B and the nearby lines in A.
+ * To keep the length of each row the same, it is padded out with values of -1
+ * where the search range extends beyond the lines in A.
+ * For example, if max_search_distance_a is 2 and the two sides of a diff chunk
+ * look like this:
+ * a | m
+ * b | n
+ * c | o
+ * d | p
+ * e | q
+ * Then the similarity array will contain:
+ * [-1, -1, am, bm, cm,
+ *  -1, an, bn, cn, dn,
+ *  ao, bo, co, do, eo,
+ *  bp, cp, dp, ep, -1,
+ *  cq, dq, eq, -1, -1]
+ * Where similarities are denoted either by -1 for invalid, or the
+ * concatenation of the two lines in the diff being compared.
+ *
+ * \param similarities array of similarities between lines in A and B
+ * \param line_a the index of the line in A, in the same frame of reference as
+ *     closest_line_a.
+ * \param local_line_b the index of the line in B, relative to the first line
+ *                    in B that similarities represents.
+ * \param closest_line_a the index of the line in A that is deemed to be
+ *                      closest to local_line_b. This must be in the same
+ *                      frame of reference as line_a. This value defines
+ *                      where similarities is centered for the line in B.
+ * \param max_search_distance_a maximum distance in lines from the closest line
+ *                             in A for other lines in A for which
+ *                             similarities may be calculated.
+ */
+static int *get_similarity(int *similarities,
+                          int line_a, int local_line_b,
+                          int closest_line_a, int max_search_distance_a)
+{
+       assert(abs(line_a - closest_line_a) <=
+              max_search_distance_a);
+       return similarities + line_a - closest_line_a +
+              max_search_distance_a +
+              local_line_b * (max_search_distance_a * 2 + 1);
+}
+
+#define CERTAIN_NOTHING_MATCHES -2
+#define CERTAINTY_NOT_CALCULATED -1
+
+/* Given a line in B, first calculate its similarities with nearby lines in A
+ * if not already calculated, then identify the most similar and second most
+ * similar lines. The "certainty" is calculated based on those two
+ * similarities.
+ *
+ * \param start_a the index of the first line of the chunk in A
+ * \param length_a the length in lines of the chunk in A
+ * \param local_line_b the index of the line in B, relative to the first line
+ *                    in the chunk.
+ * \param fingerprints_a array of fingerprints for the chunk in A
+ * \param fingerprints_b array of fingerprints for the chunk in B
+ * \param similarities 2-dimensional array of similarities between lines in A
+ *                    and B. See get_similarity() for more details.
+ * \param certainties array of values indicating how strongly a line in B is
+ *                   matched with some line in A.
+ * \param second_best_result array of absolute indices in A for the second
+ *                          closest match of a line in B.
+ * \param result array of absolute indices in A for the closest match of a line
+ *              in B.
+ * \param max_search_distance_a maximum distance in lines from the closest line
+ *                             in A for other lines in A for which
+ *                             similarities may be calculated.
+ * \param map_line_number_in_b_to_a parameter to map_line_number().
+ */
+static void find_best_line_matches(
+       int start_a,
+       int length_a,
+       int start_b,
+       int local_line_b,
+       struct fingerprint *fingerprints_a,
+       struct fingerprint *fingerprints_b,
+       int *similarities,
+       int *certainties,
+       int *second_best_result,
+       int *result,
+       const int max_search_distance_a,
+       const struct line_number_mapping *map_line_number_in_b_to_a)
+{
+
+       int i, search_start, search_end, closest_local_line_a, *similarity,
+               best_similarity = 0, second_best_similarity = 0,
+               best_similarity_index = 0, second_best_similarity_index = 0;
+
+       /* certainty has already been calculated so no need to redo the work */
+       if (certainties[local_line_b] != CERTAINTY_NOT_CALCULATED)
+               return;
+
+       closest_local_line_a = map_line_number(
+               local_line_b + start_b, map_line_number_in_b_to_a) - start_a;
+
+       search_start = closest_local_line_a - max_search_distance_a;
+       if (search_start < 0)
+               search_start = 0;
+
+       search_end = closest_local_line_a + max_search_distance_a + 1;
+       if (search_end > length_a)
+               search_end = length_a;
+
+       for (i = search_start; i < search_end; ++i) {
+               similarity = get_similarity(similarities,
+                                           i, local_line_b,
+                                           closest_local_line_a,
+                                           max_search_distance_a);
+               if (*similarity == -1) {
+                       /* This value will never exceed 10 but assert just in
+                        * case
+                        */
+                       assert(abs(i - closest_local_line_a) < 1000);
+                       /* scale the similarity by (1000 - distance from
+                        * closest line) to act as a tie break between lines
+                        * that otherwise are equally similar.
+                        */
+                       *similarity = fingerprint_similarity(
+                               fingerprints_b + local_line_b,
+                               fingerprints_a + i) *
+                               (1000 - abs(i - closest_local_line_a));
+               }
+               if (*similarity > best_similarity) {
+                       second_best_similarity = best_similarity;
+                       second_best_similarity_index = best_similarity_index;
+                       best_similarity = *similarity;
+                       best_similarity_index = i;
+               } else if (*similarity > second_best_similarity) {
+                       second_best_similarity = *similarity;
+                       second_best_similarity_index = i;
+               }
+       }
+
+       if (best_similarity == 0) {
+               /* this line definitely doesn't match with anything. Mark it
+                * with this special value so it doesn't get invalidated and
+                * won't be recalculated.
+                */
+               certainties[local_line_b] = CERTAIN_NOTHING_MATCHES;
+               result[local_line_b] = -1;
+       } else {
+               /* Calculate the certainty with which this line matches.
+                * If the line matches well with two lines then that reduces
+                * the certainty. However we still want to prioritise matching
+                * a line that matches very well with two lines over matching a
+                * line that matches poorly with one line, hence doubling
+                * best_similarity.
+                * This means that if we have
+                * line X that matches only one line with a score of 3,
+                * line Y that matches two lines equally with a score of 5,
+                * and line Z that matches only one line with a score or 2,
+                * then the lines in order of certainty are X, Y, Z.
+                */
+               certainties[local_line_b] = best_similarity * 2 -
+                       second_best_similarity;
+
+               /* We keep both the best and second best results to allow us to
+                * check at a later stage of the matching process whether the
+                * result needs to be invalidated.
+                */
+               result[local_line_b] = start_a + best_similarity_index;
+               second_best_result[local_line_b] =
+                       start_a + second_best_similarity_index;
+       }
+}
+
+/*
+ * This finds the line that we can match with the most confidence, and
+ * uses it as a partition. It then calls itself on the lines on either side of
+ * that partition. In this way we avoid lines appearing out of order, and
+ * retain a sensible line ordering.
+ * \param start_a index of the first line in A with which lines in B may be
+ *               compared.
+ * \param start_b index of the first line in B for which matching should be
+ *               done.
+ * \param length_a number of lines in A with which lines in B may be compared.
+ * \param length_b number of lines in B for which matching should be done.
+ * \param fingerprints_a mutable array of fingerprints in A. The first element
+ *                      corresponds to the line at start_a.
+ * \param fingerprints_b array of fingerprints in B. The first element
+ *                      corresponds to the line at start_b.
+ * \param similarities 2-dimensional array of similarities between lines in A
+ *                    and B. See get_similarity() for more details.
+ * \param certainties array of values indicating how strongly a line in B is
+ *                   matched with some line in A.
+ * \param second_best_result array of absolute indices in A for the second
+ *                          closest match of a line in B.
+ * \param result array of absolute indices in A for the closest match of a line
+ *              in B.
+ * \param max_search_distance_a maximum distance in lines from the closest line
+ *                           in A for other lines in A for which
+ *                           similarities may be calculated.
+ * \param max_search_distance_b an upper bound on the greatest possible
+ *                           distance between lines in B such that they will
+ *                              both be compared with the same line in A
+ *                           according to max_search_distance_a.
+ * \param map_line_number_in_b_to_a parameter to map_line_number().
+ */
+static void fuzzy_find_matching_lines_recurse(
+       int start_a, int start_b,
+       int length_a, int length_b,
+       struct fingerprint *fingerprints_a,
+       struct fingerprint *fingerprints_b,
+       int *similarities,
+       int *certainties,
+       int *second_best_result,
+       int *result,
+       int max_search_distance_a,
+       int max_search_distance_b,
+       const struct line_number_mapping *map_line_number_in_b_to_a)
+{
+       int i, invalidate_min, invalidate_max, offset_b,
+               second_half_start_a, second_half_start_b,
+               second_half_length_a, second_half_length_b,
+               most_certain_line_a, most_certain_local_line_b = -1,
+               most_certain_line_certainty = -1,
+               closest_local_line_a;
+
+       for (i = 0; i < length_b; ++i) {
+               find_best_line_matches(start_a,
+                                      length_a,
+                                      start_b,
+                                      i,
+                                      fingerprints_a,
+                                      fingerprints_b,
+                                      similarities,
+                                      certainties,
+                                      second_best_result,
+                                      result,
+                                      max_search_distance_a,
+                                      map_line_number_in_b_to_a);
+
+               if (certainties[i] > most_certain_line_certainty) {
+                       most_certain_line_certainty = certainties[i];
+                       most_certain_local_line_b = i;
+               }
+       }
+
+       /* No matches. */
+       if (most_certain_local_line_b == -1)
+               return;
+
+       most_certain_line_a = result[most_certain_local_line_b];
+
+       /*
+        * Subtract the most certain line's fingerprint in B from the matched
+        * fingerprint in A. This means that other lines in B can't also match
+        * the same parts of the line in A.
+        */
+       fingerprint_subtract(fingerprints_a + most_certain_line_a - start_a,
+                            fingerprints_b + most_certain_local_line_b);
+
+       /* Invalidate results that may be affected by the choice of most
+        * certain line.
+        */
+       invalidate_min = most_certain_local_line_b - max_search_distance_b;
+       invalidate_max = most_certain_local_line_b + max_search_distance_b + 1;
+       if (invalidate_min < 0)
+               invalidate_min = 0;
+       if (invalidate_max > length_b)
+               invalidate_max = length_b;
+
+       /* As the fingerprint in A has changed, discard previously calculated
+        * similarity values with that fingerprint.
+        */
+       for (i = invalidate_min; i < invalidate_max; ++i) {
+               closest_local_line_a = map_line_number(
+                       i + start_b, map_line_number_in_b_to_a) - start_a;
+
+               /* Check that the lines in A and B are close enough that there
+                * is a similarity value for them.
+                */
+               if (abs(most_certain_line_a - start_a - closest_local_line_a) >
+                       max_search_distance_a) {
+                       continue;
+               }
+
+               *get_similarity(similarities, most_certain_line_a - start_a,
+                               i, closest_local_line_a,
+                               max_search_distance_a) = -1;
+       }
+
+       /* More invalidating of results that may be affected by the choice of
+        * most certain line.
+        * Discard the matches for lines in B that are currently matched with a
+        * line in A such that their ordering contradicts the ordering imposed
+        * by the choice of most certain line.
+        */
+       for (i = most_certain_local_line_b - 1; i >= invalidate_min; --i) {
+               /* In this loop we discard results for lines in B that are
+                * before most-certain-line-B but are matched with a line in A
+                * that is after most-certain-line-A.
+                */
+               if (certainties[i] >= 0 &&
+                   (result[i] >= most_certain_line_a ||
+                    second_best_result[i] >= most_certain_line_a)) {
+                       certainties[i] = CERTAINTY_NOT_CALCULATED;
+               }
+       }
+       for (i = most_certain_local_line_b + 1; i < invalidate_max; ++i) {
+               /* In this loop we discard results for lines in B that are
+                * after most-certain-line-B but are matched with a line in A
+                * that is before most-certain-line-A.
+                */
+               if (certainties[i] >= 0 &&
+                   (result[i] <= most_certain_line_a ||
+                    second_best_result[i] <= most_certain_line_a)) {
+                       certainties[i] = CERTAINTY_NOT_CALCULATED;
+               }
+       }
+
+       /* Repeat the matching process for lines before the most certain line.
+        */
+       if (most_certain_local_line_b > 0) {
+               fuzzy_find_matching_lines_recurse(
+                       start_a, start_b,
+                       most_certain_line_a + 1 - start_a,
+                       most_certain_local_line_b,
+                       fingerprints_a, fingerprints_b, similarities,
+                       certainties, second_best_result, result,
+                       max_search_distance_a,
+                       max_search_distance_b,
+                       map_line_number_in_b_to_a);
+       }
+       /* Repeat the matching process for lines after the most certain line.
+        */
+       if (most_certain_local_line_b + 1 < length_b) {
+               second_half_start_a = most_certain_line_a;
+               offset_b = most_certain_local_line_b + 1;
+               second_half_start_b = start_b + offset_b;
+               second_half_length_a =
+                       length_a + start_a - second_half_start_a;
+               second_half_length_b =
+                       length_b + start_b - second_half_start_b;
+               fuzzy_find_matching_lines_recurse(
+                       second_half_start_a, second_half_start_b,
+                       second_half_length_a, second_half_length_b,
+                       fingerprints_a + second_half_start_a - start_a,
+                       fingerprints_b + offset_b,
+                       similarities +
+                               offset_b * (max_search_distance_a * 2 + 1),
+                       certainties + offset_b,
+                       second_best_result + offset_b, result + offset_b,
+                       max_search_distance_a,
+                       max_search_distance_b,
+                       map_line_number_in_b_to_a);
+       }
+}
+
+/* Find the lines in the parent line range that most closely match the lines in
+ * the target line range. This is accomplished by matching fingerprints in each
+ * blame_origin, and choosing the best matches that preserve the line ordering.
+ * See struct fingerprint for details of fingerprint matching, and
+ * fuzzy_find_matching_lines_recurse for details of preserving line ordering.
+ *
+ * The performance is believed to be O(n log n) in the typical case and O(n^2)
+ * in a pathological case, where n is the number of lines in the target range.
+ */
+static int *fuzzy_find_matching_lines(struct blame_origin *parent,
+                                     struct blame_origin *target,
+                                     int tlno, int parent_slno, int same,
+                                     int parent_len)
+{
+       /* We use the terminology "A" for the left hand side of the diff AKA
+        * parent, and "B" for the right hand side of the diff AKA target. */
+       int start_a = parent_slno;
+       int length_a = parent_len;
+       int start_b = tlno;
+       int length_b = same - tlno;
+
+       struct line_number_mapping map_line_number_in_b_to_a = {
+               start_a, length_a, start_b, length_b
+       };
+
+       struct fingerprint *fingerprints_a = parent->fingerprints;
+       struct fingerprint *fingerprints_b = target->fingerprints;
+
+       int i, *result, *second_best_result,
+               *certainties, *similarities, similarity_count;
+
+       /*
+        * max_search_distance_a means that given a line in B, compare it to
+        * the line in A that is closest to its position, and the lines in A
+        * that are no greater than max_search_distance_a lines away from the
+        * closest line in A.
+        *
+        * max_search_distance_b is an upper bound on the greatest possible
+        * distance between lines in B such that they will both be compared
+        * with the same line in A according to max_search_distance_a.
+        */
+       int max_search_distance_a = 10, max_search_distance_b;
+
+       if (length_a <= 0)
+               return NULL;
+
+       if (max_search_distance_a >= length_a)
+               max_search_distance_a = length_a ? length_a - 1 : 0;
+
+       max_search_distance_b = ((2 * max_search_distance_a + 1) * length_b
+                                - 1) / length_a;
+
+       result = xcalloc(sizeof(int), length_b);
+       second_best_result = xcalloc(sizeof(int), length_b);
+       certainties = xcalloc(sizeof(int), length_b);
+
+       /* See get_similarity() for details of similarities. */
+       similarity_count = length_b * (max_search_distance_a * 2 + 1);
+       similarities = xcalloc(sizeof(int), similarity_count);
+
+       for (i = 0; i < length_b; ++i) {
+               result[i] = -1;
+               second_best_result[i] = -1;
+               certainties[i] = CERTAINTY_NOT_CALCULATED;
+       }
+
+       for (i = 0; i < similarity_count; ++i)
+               similarities[i] = -1;
+
+       fuzzy_find_matching_lines_recurse(start_a, start_b,
+                                         length_a, length_b,
+                                         fingerprints_a + start_a,
+                                         fingerprints_b + start_b,
+                                         similarities,
+                                         certainties,
+                                         second_best_result,
+                                         result,
+                                         max_search_distance_a,
+                                         max_search_distance_b,
+                                         &map_line_number_in_b_to_a);
+
+       free(similarities);
+       free(certainties);
+       free(second_best_result);
+
+       return result;
+}
+
+static void fill_origin_fingerprints(struct blame_origin *o)
+{
+       int *line_starts;
+
+       if (o->fingerprints)
+               return;
+       o->num_lines = find_line_starts(&line_starts, o->file.ptr,
+                                       o->file.size);
+       o->fingerprints = xcalloc(sizeof(struct fingerprint), o->num_lines);
+       get_line_fingerprints(o->fingerprints, o->file.ptr, line_starts,
+                             0, o->num_lines);
+       free(line_starts);
+}
+
+static void drop_origin_fingerprints(struct blame_origin *o)
+{
+       if (o->fingerprints) {
+               free_line_fingerprints(o->fingerprints, o->num_lines);
+               o->num_lines = 0;
+               FREE_AND_NULL(o->fingerprints);
+       }
+}
+
 /*
  * Given an origin, prepare mmfile_t structure to be used by the
  * diff machinery
  */
 static void fill_origin_blob(struct diff_options *opt,
-                            struct blame_origin *o, mmfile_t *file, int *num_read_blob)
+                            struct blame_origin *o, mmfile_t *file,
+                            int *num_read_blob, int fill_fingerprints)
 {
        if (!o->file.ptr) {
                enum object_type type;
@@ -340,11 +1035,14 @@ static void fill_origin_blob(struct diff_options *opt,
        }
        else
                *file = o->file;
+       if (fill_fingerprints)
+               fill_origin_fingerprints(o);
 }
 
 static void drop_origin_blob(struct blame_origin *o)
 {
        FREE_AND_NULL(o->file.ptr);
+       drop_origin_fingerprints(o);
 }
 
 /*
@@ -480,7 +1178,9 @@ void blame_coalesce(struct blame_scoreboard *sb)
 
        for (ent = sb->ent; ent && (next = ent->next); ent = next) {
                if (ent->suspect == next->suspect &&
-                   ent->s_lno + ent->num_lines == next->s_lno) {
+                   ent->s_lno + ent->num_lines == next->s_lno &&
+                   ent->ignored == next->ignored &&
+                   ent->unblamable == next->unblamable) {
                        ent->num_lines += next->num_lines;
                        ent->next = next->next;
                        blame_origin_decref(next->suspect);
@@ -532,7 +1232,7 @@ static int fill_blob_sha1_and_mode(struct repository *r,
 {
        if (!is_null_oid(&origin->blob_oid))
                return 0;
-       if (get_tree_entry(&origin->commit->object.oid, origin->path, &origin->blob_oid, &origin->mode))
+       if (get_tree_entry(r, &origin->commit->object.oid, origin->path, &origin->blob_oid, &origin->mode))
                goto error_out;
        if (oid_object_info(r, &origin->blob_oid, NULL) != OBJ_BLOB)
                goto error_out;
@@ -730,8 +1430,14 @@ static void split_overlap(struct blame_entry *split,
                          struct blame_origin *parent)
 {
        int chunk_end_lno;
+       int i;
        memset(split, 0, sizeof(struct blame_entry [3]));
 
+       for (i = 0; i < 3; i++) {
+               split[i].ignored = e->ignored;
+               split[i].unblamable = e->unblamable;
+       }
+
        if (e->s_lno < tlno) {
                /* there is a pre-chunk part not blamed on parent */
                split[0].suspect = blame_origin_incref(e->suspect);
@@ -839,6 +1545,164 @@ static struct blame_entry *reverse_blame(struct blame_entry *head,
        return tail;
 }
 
+/*
+ * Splits a blame entry into two entries at 'len' lines.  The original 'e'
+ * consists of len lines, i.e. [e->lno, e->lno + len), and the second part,
+ * which is returned, consists of the remainder: [e->lno + len, e->lno +
+ * e->num_lines).  The caller needs to sort out the reference counting for the
+ * new entry's suspect.
+ */
+static struct blame_entry *split_blame_at(struct blame_entry *e, int len,
+                                         struct blame_origin *new_suspect)
+{
+       struct blame_entry *n = xcalloc(1, sizeof(struct blame_entry));
+
+       n->suspect = new_suspect;
+       n->ignored = e->ignored;
+       n->unblamable = e->unblamable;
+       n->lno = e->lno + len;
+       n->s_lno = e->s_lno + len;
+       n->num_lines = e->num_lines - len;
+       e->num_lines = len;
+       e->score = 0;
+       return n;
+}
+
+struct blame_line_tracker {
+       int is_parent;
+       int s_lno;
+};
+
+static int are_lines_adjacent(struct blame_line_tracker *first,
+                             struct blame_line_tracker *second)
+{
+       return first->is_parent == second->is_parent &&
+              first->s_lno + 1 == second->s_lno;
+}
+
+static int scan_parent_range(struct fingerprint *p_fps,
+                            struct fingerprint *t_fps, int t_idx,
+                            int from, int nr_lines)
+{
+       int sim, p_idx;
+       #define FINGERPRINT_FILE_THRESHOLD      10
+       int best_sim_val = FINGERPRINT_FILE_THRESHOLD;
+       int best_sim_idx = -1;
+
+       for (p_idx = from; p_idx < from + nr_lines; p_idx++) {
+               sim = fingerprint_similarity(&t_fps[t_idx], &p_fps[p_idx]);
+               if (sim < best_sim_val)
+                       continue;
+               /* Break ties with the closest-to-target line number */
+               if (sim == best_sim_val && best_sim_idx != -1 &&
+                   abs(best_sim_idx - t_idx) < abs(p_idx - t_idx))
+                       continue;
+               best_sim_val = sim;
+               best_sim_idx = p_idx;
+       }
+       return best_sim_idx;
+}
+
+/*
+ * The first pass checks the blame entry (from the target) against the parent's
+ * diff chunk.  If that fails for a line, the second pass tries to match that
+ * line to any part of parent file.  That catches cases where a change was
+ * broken into two chunks by 'context.'
+ */
+static void guess_line_blames(struct blame_origin *parent,
+                             struct blame_origin *target,
+                             int tlno, int offset, int same, int parent_len,
+                             struct blame_line_tracker *line_blames)
+{
+       int i, best_idx, target_idx;
+       int parent_slno = tlno + offset;
+       int *fuzzy_matches;
+
+       fuzzy_matches = fuzzy_find_matching_lines(parent, target,
+                                                 tlno, parent_slno, same,
+                                                 parent_len);
+       for (i = 0; i < same - tlno; i++) {
+               target_idx = tlno + i;
+               if (fuzzy_matches && fuzzy_matches[i] >= 0) {
+                       best_idx = fuzzy_matches[i];
+               } else {
+                       best_idx = scan_parent_range(parent->fingerprints,
+                                                    target->fingerprints,
+                                                    target_idx, 0,
+                                                    parent->num_lines);
+               }
+               if (best_idx >= 0) {
+                       line_blames[i].is_parent = 1;
+                       line_blames[i].s_lno = best_idx;
+               } else {
+                       line_blames[i].is_parent = 0;
+                       line_blames[i].s_lno = target_idx;
+               }
+       }
+       free(fuzzy_matches);
+}
+
+/*
+ * This decides which parts of a blame entry go to the parent (added to the
+ * ignoredp list) and which stay with the target (added to the diffp list).  The
+ * actual decision was made in a separate heuristic function, and those answers
+ * for the lines in 'e' are in line_blames.  This consumes e, essentially
+ * putting it on a list.
+ *
+ * Note that the blame entries on the ignoredp list are not necessarily sorted
+ * with respect to the parent's line numbers yet.
+ */
+static void ignore_blame_entry(struct blame_entry *e,
+                              struct blame_origin *parent,
+                              struct blame_entry **diffp,
+                              struct blame_entry **ignoredp,
+                              struct blame_line_tracker *line_blames)
+{
+       int entry_len, nr_lines, i;
+
+       /*
+        * We carve new entries off the front of e.  Each entry comes from a
+        * contiguous chunk of lines: adjacent lines from the same origin
+        * (either the parent or the target).
+        */
+       entry_len = 1;
+       nr_lines = e->num_lines;        /* e changes in the loop */
+       for (i = 0; i < nr_lines; i++) {
+               struct blame_entry *next = NULL;
+
+               /*
+                * We are often adjacent to the next line - only split the blame
+                * entry when we have to.
+                */
+               if (i + 1 < nr_lines) {
+                       if (are_lines_adjacent(&line_blames[i],
+                                              &line_blames[i + 1])) {
+                               entry_len++;
+                               continue;
+                       }
+                       next = split_blame_at(e, entry_len,
+                                             blame_origin_incref(e->suspect));
+               }
+               if (line_blames[i].is_parent) {
+                       e->ignored = 1;
+                       blame_origin_decref(e->suspect);
+                       e->suspect = blame_origin_incref(parent);
+                       e->s_lno = line_blames[i - entry_len + 1].s_lno;
+                       e->next = *ignoredp;
+                       *ignoredp = e;
+               } else {
+                       e->unblamable = 1;
+                       /* e->s_lno is already in the target's address space. */
+                       e->next = *diffp;
+                       *diffp = e;
+               }
+               assert(e->num_lines == entry_len);
+               e = next;
+               entry_len = 1;
+       }
+       assert(!e);
+}
+
 /*
  * Process one hunk from the patch between the current suspect for
  * blame_entry e and its parent.  This first blames any unfinished
@@ -848,13 +1712,20 @@ static struct blame_entry *reverse_blame(struct blame_entry *head,
  * -C options may lead to overlapping/duplicate source line number
  * ranges, all we can rely on from sorting/merging is the order of the
  * first suspect line number.
+ *
+ * tlno: line number in the target where this chunk begins
+ * same: line number in the target where this chunk ends
+ * offset: add to tlno to get the chunk starting point in the parent
+ * parent_len: number of lines in the parent chunk
  */
 static void blame_chunk(struct blame_entry ***dstq, struct blame_entry ***srcq,
-                       int tlno, int offset, int same,
-                       struct blame_origin *parent)
+                       int tlno, int offset, int same, int parent_len,
+                       struct blame_origin *parent,
+                       struct blame_origin *target, int ignore_diffs)
 {
        struct blame_entry *e = **srcq;
-       struct blame_entry *samep = NULL, *diffp = NULL;
+       struct blame_entry *samep = NULL, *diffp = NULL, *ignoredp = NULL;
+       struct blame_line_tracker *line_blames = NULL;
 
        while (e && e->s_lno < tlno) {
                struct blame_entry *next = e->next;
@@ -865,14 +1736,9 @@ static void blame_chunk(struct blame_entry ***dstq, struct blame_entry ***srcq,
                 */
                if (e->s_lno + e->num_lines > tlno) {
                        /* Move second half to a new record */
-                       int len = tlno - e->s_lno;
-                       struct blame_entry *n = xcalloc(1, sizeof (struct blame_entry));
-                       n->suspect = e->suspect;
-                       n->lno = e->lno + len;
-                       n->s_lno = e->s_lno + len;
-                       n->num_lines = e->num_lines - len;
-                       e->num_lines = len;
-                       e->score = 0;
+                       struct blame_entry *n;
+
+                       n = split_blame_at(e, tlno - e->s_lno, e->suspect);
                        /* Push new record to diffp */
                        n->next = diffp;
                        diffp = n;
@@ -908,6 +1774,14 @@ static void blame_chunk(struct blame_entry ***dstq, struct blame_entry ***srcq,
         */
        samep = NULL;
        diffp = NULL;
+
+       if (ignore_diffs && same - tlno > 0) {
+               line_blames = xcalloc(sizeof(struct blame_line_tracker),
+                                     same - tlno);
+               guess_line_blames(parent, target, tlno, offset, same,
+                                 parent_len, line_blames);
+       }
+
        while (e && e->s_lno < same) {
                struct blame_entry *next = e->next;
 
@@ -919,22 +1793,37 @@ static void blame_chunk(struct blame_entry ***dstq, struct blame_entry ***srcq,
                         * Move second half to a new record to be
                         * processed by later chunks
                         */
-                       int len = same - e->s_lno;
-                       struct blame_entry *n = xcalloc(1, sizeof (struct blame_entry));
-                       n->suspect = blame_origin_incref(e->suspect);
-                       n->lno = e->lno + len;
-                       n->s_lno = e->s_lno + len;
-                       n->num_lines = e->num_lines - len;
-                       e->num_lines = len;
-                       e->score = 0;
+                       struct blame_entry *n;
+
+                       n = split_blame_at(e, same - e->s_lno,
+                                          blame_origin_incref(e->suspect));
                        /* Push new record to samep */
                        n->next = samep;
                        samep = n;
                }
-               e->next = diffp;
-               diffp = e;
+               if (ignore_diffs) {
+                       ignore_blame_entry(e, parent, &diffp, &ignoredp,
+                                          line_blames + e->s_lno - tlno);
+               } else {
+                       e->next = diffp;
+                       diffp = e;
+               }
                e = next;
        }
+       free(line_blames);
+       if (ignoredp) {
+               /*
+                * Note ignoredp is not sorted yet, and thus neither is dstq.
+                * That list must be sorted before we queue_blames().  We defer
+                * sorting until after all diff hunks are processed, so that
+                * guess_line_blames() can pick *any* line in the parent.  The
+                * slight drawback is that we end up sorting all blame entries
+                * passed to the parent, including those that are unrelated to
+                * changes made by the ignored commit.
+                */
+               **dstq = reverse_blame(ignoredp, **dstq);
+               *dstq = &ignoredp->next;
+       }
        **srcq = reverse_blame(diffp, reverse_blame(samep, e));
        /* Move across elements that are in the unblamable portion */
        if (diffp)
@@ -943,7 +1832,9 @@ static void blame_chunk(struct blame_entry ***dstq, struct blame_entry ***srcq,
 
 struct blame_chunk_cb_data {
        struct blame_origin *parent;
+       struct blame_origin *target;
        long offset;
+       int ignore_diffs;
        struct blame_entry **dstq;
        struct blame_entry **srcq;
 };
@@ -956,7 +1847,8 @@ static int blame_chunk_cb(long start_a, long count_a,
        if (start_a - start_b != d->offset)
                die("internal error in blame::blame_chunk_cb");
        blame_chunk(&d->dstq, &d->srcq, start_b, start_a - start_b,
-                   start_b + count_b, d->parent);
+                   start_b + count_b, count_a, d->parent, d->target,
+                   d->ignore_diffs);
        d->offset = start_a + count_a - (start_b + count_b);
        return 0;
 }
@@ -968,7 +1860,7 @@ static int blame_chunk_cb(long start_a, long count_a,
  */
 static void pass_blame_to_parent(struct blame_scoreboard *sb,
                                 struct blame_origin *target,
-                                struct blame_origin *parent)
+                                struct blame_origin *parent, int ignore_diffs)
 {
        mmfile_t file_p, file_o;
        struct blame_chunk_cb_data d;
@@ -978,11 +1870,15 @@ static void pass_blame_to_parent(struct blame_scoreboard *sb,
                return; /* nothing remains for this target */
 
        d.parent = parent;
+       d.target = target;
        d.offset = 0;
+       d.ignore_diffs = ignore_diffs;
        d.dstq = &newdest; d.srcq = &target->suspects;
 
-       fill_origin_blob(&sb->revs->diffopt, parent, &file_p, &sb->num_read_blob);
-       fill_origin_blob(&sb->revs->diffopt, target, &file_o, &sb->num_read_blob);
+       fill_origin_blob(&sb->revs->diffopt, parent, &file_p,
+                        &sb->num_read_blob, ignore_diffs);
+       fill_origin_blob(&sb->revs->diffopt, target, &file_o,
+                        &sb->num_read_blob, ignore_diffs);
        sb->num_get_patch++;
 
        if (diff_hunks(&file_p, &file_o, blame_chunk_cb, &d, sb->xdl_opts))
@@ -990,8 +1886,13 @@ static void pass_blame_to_parent(struct blame_scoreboard *sb,
                    oid_to_hex(&parent->commit->object.oid),
                    oid_to_hex(&target->commit->object.oid));
        /* The rest are the same as the parent */
-       blame_chunk(&d.dstq, &d.srcq, INT_MAX, d.offset, INT_MAX, parent);
+       blame_chunk(&d.dstq, &d.srcq, INT_MAX, d.offset, INT_MAX, 0,
+                   parent, target, 0);
        *d.dstq = NULL;
+       if (ignore_diffs)
+               newdest = llist_mergesort(newdest, get_next_blame,
+                                         set_next_blame,
+                                         compare_blame_suspect);
        queue_blames(sb, parent, newdest);
 
        return;
@@ -1188,7 +2089,8 @@ static void find_move_in_parent(struct blame_scoreboard *sb,
        if (!unblamed)
                return; /* nothing remains for this target */
 
-       fill_origin_blob(&sb->revs->diffopt, parent, &file_p, &sb->num_read_blob);
+       fill_origin_blob(&sb->revs->diffopt, parent, &file_p,
+                        &sb->num_read_blob, 0);
        if (!file_p.ptr)
                return;
 
@@ -1317,7 +2219,8 @@ static void find_copy_in_parent(struct blame_scoreboard *sb,
                        norigin = get_origin(parent, p->one->path);
                        oidcpy(&norigin->blob_oid, &p->one->oid);
                        norigin->mode = p->one->mode;
-                       fill_origin_blob(&sb->revs->diffopt, norigin, &file_p, &sb->num_read_blob);
+                       fill_origin_blob(&sb->revs->diffopt, norigin, &file_p,
+                                        &sb->num_read_blob, 0);
                        if (!file_p.ptr)
                                continue;
 
@@ -1495,11 +2398,34 @@ static void pass_blame(struct blame_scoreboard *sb, struct blame_origin *origin,
                        blame_origin_incref(porigin);
                        origin->previous = porigin;
                }
-               pass_blame_to_parent(sb, origin, porigin);
+               pass_blame_to_parent(sb, origin, porigin, 0);
                if (!origin->suspects)
                        goto finish;
        }
 
+       /*
+        * Pass remaining suspects for ignored commits to their parents.
+        */
+       if (oidset_contains(&sb->ignore_list, &commit->object.oid)) {
+               for (i = 0, sg = first_scapegoat(revs, commit, sb->reverse);
+                    i < num_sg && sg;
+                    sg = sg->next, i++) {
+                       struct blame_origin *porigin = sg_origin[i];
+
+                       if (!porigin)
+                               continue;
+                       pass_blame_to_parent(sb, origin, porigin, 1);
+                       /*
+                        * Preemptively drop porigin so we can refresh the
+                        * fingerprints if we use the parent again, which can
+                        * occur if you ignore back-to-back commits.
+                        */
+                       drop_origin_blob(porigin);
+                       if (!origin->suspects)
+                               goto finish;
+               }
+       }
+
        /*
         * Optionally find moves in parents' files.
         */
@@ -1640,37 +2566,14 @@ void assign_blame(struct blame_scoreboard *sb, int opt)
        }
 }
 
-static const char *get_next_line(const char *start, const char *end)
-{
-       const char *nl = memchr(start, '\n', end - start);
-       return nl ? nl + 1 : end;
-}
-
 /*
  * To allow quick access to the contents of nth line in the
  * final image, prepare an index in the scoreboard.
  */
 static int prepare_lines(struct blame_scoreboard *sb)
 {
-       const char *buf = sb->final_buf;
-       unsigned long len = sb->final_buf_size;
-       const char *end = buf + len;
-       const char *p;
-       int *lineno;
-       int num = 0;
-
-       for (p = buf; p < end; p = get_next_line(p, end))
-               num++;
-
-       ALLOC_ARRAY(sb->lineno, num + 1);
-       lineno = sb->lineno;
-
-       for (p = buf; p < end; p = get_next_line(p, end))
-               *lineno++ = p - buf;
-
-       *lineno = len;
-
-       sb->num_lines = num;
+       sb->num_lines = find_line_starts(&sb->lineno, sb->final_buf,
+                                        sb->final_buf_size);
        return sb->num_lines;
 }
 
diff --git a/blame.h b/blame.h
index d62f80fa74c44011f8cdaea7a4baec3d8cae03e6..4a9e1270b036465c23fab5a0e536b9638ca3ce1b 100644 (file)
--- a/blame.h
+++ b/blame.h
@@ -51,6 +51,8 @@ struct blame_origin {
         */
        struct blame_entry *suspects;
        mmfile_t file;
+       int num_lines;
+       void *fingerprints;
        struct object_id blob_oid;
        unsigned short mode;
        /* guilty gets set when shipping any suspects to the final
@@ -92,6 +94,8 @@ struct blame_entry {
         * scanning the lines over and over.
         */
        unsigned score;
+       int ignored;
+       int unblamable;
 };
 
 /*
@@ -117,6 +121,8 @@ struct blame_scoreboard {
        /* linked list of blames */
        struct blame_entry *ent;
 
+       struct oidset ignore_list;
+
        /* look-up a line in the final buffer */
        int num_lines;
        int *lineno;
index 3d449a021002540f1183166c3cfb1dfce765b75b..5cf5df69f72fd5a660f4dfc8981387f1bcc9c7cc 100644 (file)
--- a/builtin.h
+++ b/builtin.h
@@ -160,6 +160,7 @@ int cmd_diff_index(int argc, const char **argv, const char *prefix);
 int cmd_diff(int argc, const char **argv, const char *prefix);
 int cmd_diff_tree(int argc, const char **argv, const char *prefix);
 int cmd_difftool(int argc, const char **argv, const char *prefix);
+int cmd_env__helper(int argc, const char **argv, const char *prefix);
 int cmd_fast_export(int argc, const char **argv, const char *prefix);
 int cmd_fetch(int argc, const char **argv, const char *prefix);
 int cmd_fetch_pack(int argc, const char **argv, const char *prefix);
index 50e3d4a2656140b406975947d066283a2ef14460..b6534d4dea9ad81a34eaf099f7cf9a0a1e56f410 100644 (file)
@@ -53,6 +53,9 @@ static int no_whole_file_rename;
 static int show_progress;
 static char repeated_meta_color[COLOR_MAXLEN];
 static int coloring_mode;
+static struct string_list ignore_revs_file_list = STRING_LIST_INIT_NODUP;
+static int mark_unblamable_lines;
+static int mark_ignored_lines;
 
 static struct date_mode blame_date_mode = { DATE_ISO8601 };
 static size_t blame_date_width;
@@ -480,6 +483,14 @@ static void emit_other(struct blame_scoreboard *sb, struct blame_entry *ent, int
                        }
                }
 
+               if (mark_unblamable_lines && ent->unblamable) {
+                       length--;
+                       putchar('*');
+               }
+               if (mark_ignored_lines && ent->ignored) {
+                       length--;
+                       putchar('?');
+               }
                printf("%.*s", length, hex);
                if (opt & OUTPUT_ANNOTATE_COMPAT) {
                        const char *name;
@@ -696,6 +707,24 @@ static int git_blame_config(const char *var, const char *value, void *cb)
                parse_date_format(value, &blame_date_mode);
                return 0;
        }
+       if (!strcmp(var, "blame.ignorerevsfile")) {
+               const char *str;
+               int ret;
+
+               ret = git_config_pathname(&str, var, value);
+               if (ret)
+                       return ret;
+               string_list_insert(&ignore_revs_file_list, str);
+               return 0;
+       }
+       if (!strcmp(var, "blame.markunblamablelines")) {
+               mark_unblamable_lines = git_config_bool(var, value);
+               return 0;
+       }
+       if (!strcmp(var, "blame.markignoredlines")) {
+               mark_ignored_lines = git_config_bool(var, value);
+               return 0;
+       }
        if (!strcmp(var, "color.blame.repeatedlines")) {
                if (color_parse_mem(value, strlen(value), repeated_meta_color))
                        warning(_("invalid color '%s' in color.blame.repeatedLines"),
@@ -775,6 +804,27 @@ static int is_a_rev(const char *name)
        return OBJ_NONE < oid_object_info(the_repository, &oid, NULL);
 }
 
+static void build_ignorelist(struct blame_scoreboard *sb,
+                            struct string_list *ignore_revs_file_list,
+                            struct string_list *ignore_rev_list)
+{
+       struct string_list_item *i;
+       struct object_id oid;
+
+       oidset_init(&sb->ignore_list, 0);
+       for_each_string_list_item(i, ignore_revs_file_list) {
+               if (!strcmp(i->string, ""))
+                       oidset_clear(&sb->ignore_list);
+               else
+                       oidset_parse_file(&sb->ignore_list, i->string);
+       }
+       for_each_string_list_item(i, ignore_rev_list) {
+               if (get_oid_committish(i->string, &oid))
+                       die(_("cannot find revision %s to ignore"), i->string);
+               oidset_insert(&sb->ignore_list, &oid);
+       }
+}
+
 int cmd_blame(int argc, const char **argv, const char *prefix)
 {
        struct rev_info revs;
@@ -786,6 +836,7 @@ int cmd_blame(int argc, const char **argv, const char *prefix)
        struct progress_info pi = { NULL, 0 };
 
        struct string_list range_list = STRING_LIST_INIT_NODUP;
+       struct string_list ignore_rev_list = STRING_LIST_INIT_NODUP;
        int output_option = 0, opt = 0;
        int show_stats = 0;
        const char *revs_file = NULL;
@@ -807,6 +858,8 @@ int cmd_blame(int argc, const char **argv, const char *prefix)
                OPT_BIT('s', NULL, &output_option, N_("Suppress author name and timestamp (Default: off)"), OUTPUT_NO_AUTHOR),
                OPT_BIT('e', "show-email", &output_option, N_("Show author email instead of name (Default: off)"), OUTPUT_SHOW_EMAIL),
                OPT_BIT('w', NULL, &xdl_opts, N_("Ignore whitespace differences"), XDF_IGNORE_WHITESPACE),
+               OPT_STRING_LIST(0, "ignore-rev", &ignore_rev_list, N_("rev"), N_("Ignore <rev> when blaming")),
+               OPT_STRING_LIST(0, "ignore-revs-file", &ignore_revs_file_list, N_("file"), N_("Ignore revisions from <file>")),
                OPT_BIT(0, "color-lines", &output_option, N_("color redundant metadata from previous line differently"), OUTPUT_COLOR_LINE),
                OPT_BIT(0, "color-by-age", &output_option, N_("color lines by age"), OUTPUT_SHOW_AGE_WITH_COLOR),
 
@@ -1012,6 +1065,9 @@ int cmd_blame(int argc, const char **argv, const char *prefix)
        sb.contents_from = contents_from;
        sb.reverse = reverse;
        sb.repo = the_repository;
+       build_ignorelist(&sb, &ignore_revs_file_list, &ignore_rev_list);
+       string_list_clear(&ignore_revs_file_list, 0);
+       string_list_clear(&ignore_rev_list, 0);
        setup_scoreboard(&sb, path, &o);
        lno = sb.num_lines;
 
index 0f092382e175cf7ebe43d2f53ef1c5c79c338568..995d47c85aad24a645786ed6480bd659c755997e 100644 (file)
@@ -172,7 +172,8 @@ static int cat_one_file(int opt, const char *exp_type, const char *obj_name,
                         * fall-back to the usual case.
                         */
                }
-               buf = read_object_with_reference(&oid, exp_type, &size, NULL);
+               buf = read_object_with_reference(the_repository,
+                                                &oid, exp_type, &size, NULL);
                break;
 
        default:
index d8efa5bab276a816bef48e9cb3891ccc0cb81aad..38027b83d9d8329a1dc2e47b236a985e4ce71060 100644 (file)
@@ -5,17 +5,18 @@
 #include "parse-options.h"
 #include "repository.h"
 #include "commit-graph.h"
+#include "object-store.h"
 
 static char const * const builtin_commit_graph_usage[] = {
        N_("git commit-graph [--object-dir <objdir>]"),
        N_("git commit-graph read [--object-dir <objdir>]"),
-       N_("git commit-graph verify [--object-dir <objdir>]"),
-       N_("git commit-graph write [--object-dir <objdir>] [--append] [--reachable|--stdin-packs|--stdin-commits]"),
+       N_("git commit-graph verify [--object-dir <objdir>] [--shallow]"),
+       N_("git commit-graph write [--object-dir <objdir>] [--append|--split] [--reachable|--stdin-packs|--stdin-commits] <split options>"),
        NULL
 };
 
 static const char * const builtin_commit_graph_verify_usage[] = {
-       N_("git commit-graph verify [--object-dir <objdir>]"),
+       N_("git commit-graph verify [--object-dir <objdir>] [--shallow]"),
        NULL
 };
 
@@ -25,7 +26,7 @@ static const char * const builtin_commit_graph_read_usage[] = {
 };
 
 static const char * const builtin_commit_graph_write_usage[] = {
-       N_("git commit-graph write [--object-dir <objdir>] [--append] [--reachable|--stdin-packs|--stdin-commits]"),
+       N_("git commit-graph write [--object-dir <objdir>] [--append|--split] [--reachable|--stdin-packs|--stdin-commits] <split options>"),
        NULL
 };
 
@@ -35,9 +36,10 @@ static struct opts_commit_graph {
        int stdin_packs;
        int stdin_commits;
        int append;
+       int split;
+       int shallow;
 } opts;
 
-
 static int graph_verify(int argc, const char **argv)
 {
        struct commit_graph *graph = NULL;
@@ -45,11 +47,14 @@ static int graph_verify(int argc, const char **argv)
        int open_ok;
        int fd;
        struct stat st;
+       int flags = 0;
 
        static struct option builtin_commit_graph_verify_options[] = {
                OPT_STRING(0, "object-dir", &opts.obj_dir,
                           N_("dir"),
                           N_("The object directory to store the graph")),
+               OPT_BOOL(0, "shallow", &opts.shallow,
+                        N_("if the commit-graph is split, only verify the tip file")),
                OPT_END(),
        };
 
@@ -59,21 +64,27 @@ static int graph_verify(int argc, const char **argv)
 
        if (!opts.obj_dir)
                opts.obj_dir = get_object_directory();
+       if (opts.shallow)
+               flags |= COMMIT_GRAPH_VERIFY_SHALLOW;
 
        graph_name = get_commit_graph_filename(opts.obj_dir);
        open_ok = open_commit_graph(graph_name, &fd, &st);
-       if (!open_ok && errno == ENOENT)
-               return 0;
-       if (!open_ok)
+       if (!open_ok && errno != ENOENT)
                die_errno(_("Could not open commit-graph '%s'"), graph_name);
-       graph = load_commit_graph_one_fd_st(fd, &st);
+
        FREE_AND_NULL(graph_name);
 
+       if (open_ok)
+               graph = load_commit_graph_one_fd_st(fd, &st);
+        else
+               graph = read_commit_graph_one(the_repository, opts.obj_dir);
+
+       /* Return failure if open_ok predicted success */
        if (!graph)
-               return 1;
+               return !!open_ok;
 
        UNLEAK(graph);
-       return verify_commit_graph(the_repository, graph);
+       return verify_commit_graph(the_repository, graph, flags);
 }
 
 static int graph_read(int argc, const char **argv)
@@ -135,6 +146,7 @@ static int graph_read(int argc, const char **argv)
 }
 
 extern int read_replace_refs;
+static struct split_commit_graph_opts split_opts;
 
 static int graph_write(int argc, const char **argv)
 {
@@ -156,9 +168,21 @@ static int graph_write(int argc, const char **argv)
                        N_("start walk at commits listed by stdin")),
                OPT_BOOL(0, "append", &opts.append,
                        N_("include all commits already in the commit-graph file")),
+               OPT_BOOL(0, "split", &opts.split,
+                       N_("allow writing an incremental commit-graph file")),
+               OPT_INTEGER(0, "max-commits", &split_opts.max_commits,
+                       N_("maximum number of commits in a non-base split commit-graph")),
+               OPT_INTEGER(0, "size-multiple", &split_opts.size_multiple,
+                       N_("maximum ratio between two levels of a split commit-graph")),
+               OPT_EXPIRY_DATE(0, "expire-time", &split_opts.expire_time,
+                       N_("maximum number of commits in a non-base split commit-graph")),
                OPT_END(),
        };
 
+       split_opts.size_multiple = 2;
+       split_opts.max_commits = 0;
+       split_opts.expire_time = 0;
+
        argc = parse_options(argc, argv, NULL,
                             builtin_commit_graph_write_options,
                             builtin_commit_graph_write_usage, 0);
@@ -169,11 +193,16 @@ static int graph_write(int argc, const char **argv)
                opts.obj_dir = get_object_directory();
        if (opts.append)
                flags |= COMMIT_GRAPH_APPEND;
+       if (opts.split)
+               flags |= COMMIT_GRAPH_SPLIT;
 
        read_replace_refs = 0;
 
-       if (opts.reachable)
-               return write_commit_graph_reachable(opts.obj_dir, flags);
+       if (opts.reachable) {
+               if (write_commit_graph_reachable(opts.obj_dir, flags, &split_opts))
+                       return 1;
+               return 0;
+       }
 
        string_list_init(&lines, 0);
        if (opts.stdin_packs || opts.stdin_commits) {
@@ -193,7 +222,8 @@ static int graph_write(int argc, const char **argv)
        if (write_commit_graph(opts.obj_dir,
                               pack_indexes,
                               commit_hex,
-                              flags))
+                              flags,
+                              &split_opts))
                result = 1;
 
        UNLEAK(lines);
index 3b561c2a75b5075ca688868ab965c7b96a87a9e8..ae7aaf6dc6835888e4fb55de8a135331ab05316e 100644 (file)
@@ -60,15 +60,18 @@ N_("The previous cherry-pick is now empty, possibly due to conflict resolution.\
 "\n");
 
 static const char empty_cherry_pick_advice_single[] =
-N_("Otherwise, please use 'git reset'\n");
+N_("Otherwise, please use 'git cherry-pick --skip'\n");
 
 static const char empty_cherry_pick_advice_multi[] =
-N_("If you wish to skip this commit, use:\n"
+N_("and then use:\n"
 "\n"
-"    git reset\n"
+"    git cherry-pick --continue\n"
 "\n"
-"Then \"git cherry-pick --continue\" will resume cherry-picking\n"
-"the remaining commits.\n");
+"to resume cherry-picking the remaining commits.\n"
+"If you wish to skip this commit, use:\n"
+"\n"
+"    git cherry-pick --skip\n"
+"\n");
 
 static const char *color_status_slots[] = {
        [WT_STATUS_HEADER]        = "header",
@@ -1687,7 +1690,7 @@ int cmd_commit(int argc, const char **argv, const char *prefix)
                      "not exceeded, and then \"git restore --staged :/\" to recover."));
 
        if (git_env_bool(GIT_TEST_COMMIT_GRAPH, 0) &&
-           write_commit_graph_reachable(get_object_directory(), 0))
+           write_commit_graph_reachable(get_object_directory(), 0, NULL))
                return 1;
 
        repo_rerere(the_repository, 0);
diff --git a/builtin/env--helper.c b/builtin/env--helper.c
new file mode 100644 (file)
index 0000000..23c214f
--- /dev/null
@@ -0,0 +1,95 @@
+#include "builtin.h"
+#include "config.h"
+#include "parse-options.h"
+
+static char const * const env__helper_usage[] = {
+       N_("git env--helper --type=[bool|ulong] <options> <env-var>"),
+       NULL
+};
+
+static enum {
+       ENV_HELPER_TYPE_BOOL = 1,
+       ENV_HELPER_TYPE_ULONG
+} cmdmode = 0;
+
+static int option_parse_type(const struct option *opt, const char *arg,
+                            int unset)
+{
+       if (!strcmp(arg, "bool"))
+               cmdmode = ENV_HELPER_TYPE_BOOL;
+       else if (!strcmp(arg, "ulong"))
+               cmdmode = ENV_HELPER_TYPE_ULONG;
+       else
+               die(_("unrecognized --type argument, %s"), arg);
+
+       return 0;
+}
+
+int cmd_env__helper(int argc, const char **argv, const char *prefix)
+{
+       int exit_code = 0;
+       const char *env_variable = NULL;
+       const char *env_default = NULL;
+       int ret;
+       int ret_int, default_int;
+       unsigned long ret_ulong, default_ulong;
+       struct option opts[] = {
+               OPT_CALLBACK_F(0, "type", &cmdmode, N_("type"),
+                              N_("value is given this type"), PARSE_OPT_NONEG,
+                              option_parse_type),
+               OPT_STRING(0, "default", &env_default, N_("value"),
+                          N_("default for git_env_*(...) to fall back on")),
+               OPT_BOOL(0, "exit-code", &exit_code,
+                        N_("be quiet only use git_env_*() value as exit code")),
+               OPT_END(),
+       };
+
+       argc = parse_options(argc, argv, prefix, opts, env__helper_usage,
+                            PARSE_OPT_KEEP_UNKNOWN);
+       if (env_default && !*env_default)
+               usage_with_options(env__helper_usage, opts);
+       if (!cmdmode)
+               usage_with_options(env__helper_usage, opts);
+       if (argc != 1)
+               usage_with_options(env__helper_usage, opts);
+       env_variable = argv[0];
+
+       switch (cmdmode) {
+       case ENV_HELPER_TYPE_BOOL:
+               if (env_default) {
+                       default_int = git_parse_maybe_bool(env_default);
+                       if (default_int == -1) {
+                               error(_("option `--default' expects a boolean value with `--type=bool`, not `%s`"),
+                                     env_default);
+                               usage_with_options(env__helper_usage, opts);
+                       }
+               } else {
+                       default_int = 0;
+               }
+               ret_int = git_env_bool(env_variable, default_int);
+               if (!exit_code)
+                       puts(ret_int ? "true" : "false");
+               ret = ret_int;
+               break;
+       case ENV_HELPER_TYPE_ULONG:
+               if (env_default) {
+                       if (!git_parse_ulong(env_default, &default_ulong)) {
+                               error(_("option `--default' expects an unsigned long value with `--type=ulong`, not `%s`"),
+                                     env_default);
+                               usage_with_options(env__helper_usage, opts);
+                       }
+               } else {
+                       default_ulong = 0;
+               }
+               ret_ulong = git_env_ulong(env_variable, default_ulong);
+               if (!exit_code)
+                       printf("%lu\n", ret_ulong);
+               ret = ret_ulong;
+               break;
+       default:
+               BUG("unknown <type> value");
+               break;
+       }
+
+       return !ret;
+}
index be8e0bfcbe0a428f72c5762ff8a066baaefd2533..c18efadda53e54f0e80dbd16737e2d40f47fa16f 100644 (file)
@@ -687,7 +687,8 @@ int cmd_gc(int argc, const char **argv, const char *prefix)
 
        if (gc_write_commit_graph &&
            write_commit_graph_reachable(get_object_directory(),
-                                        !quiet && !daemonized ? COMMIT_GRAPH_PROGRESS : 0))
+                                        !quiet && !daemonized ? COMMIT_GRAPH_PROGRESS : 0,
+                                        NULL))
                return 1;
 
        if (auto_gc && too_many_loose_objects())
index 580fd38f41704b6d534c04f2dee1bf85eee67c3c..560051784ef7c222046d51ba8b8bf7f84223a26e 100644 (file)
@@ -458,7 +458,8 @@ static int grep_submodule(struct grep_opt *opt,
                object = parse_object_or_die(oid, oid_to_hex(oid));
 
                grep_read_lock();
-               data = read_object_with_reference(&object->oid, tree_type,
+               data = read_object_with_reference(&subrepo,
+                                                 &object->oid, tree_type,
                                                  &size, NULL);
                grep_read_unlock();
 
@@ -623,7 +624,8 @@ static int grep_object(struct grep_opt *opt, const struct pathspec *pathspec,
                int hit, len;
 
                grep_read_lock();
-               data = read_object_with_reference(&obj->oid, tree_type,
+               data = read_object_with_reference(opt->repo,
+                                                 &obj->oid, tree_type,
                                                  &size, NULL);
                grep_read_unlock();
 
index 34ca0258b12ae4e4a04c495c244fb9c18268e7d8..97b54caeb90085e0fb4a88898ac87c7a5cb45eed 100644 (file)
@@ -205,6 +205,7 @@ static void resolve(const struct traverse_info *info, struct name_entry *ours, s
 static void unresolved_directory(const struct traverse_info *info,
                                 struct name_entry n[3])
 {
+       struct repository *r = the_repository;
        char *newbase;
        struct name_entry *p;
        struct tree_desc t[3];
@@ -220,9 +221,9 @@ static void unresolved_directory(const struct traverse_info *info,
        newbase = traverse_path(info, p);
 
 #define ENTRY_OID(e) (((e)->mode && S_ISDIR((e)->mode)) ? &(e)->oid : NULL)
-       buf0 = fill_tree_descriptor(t + 0, ENTRY_OID(n + 0));
-       buf1 = fill_tree_descriptor(t + 1, ENTRY_OID(n + 1));
-       buf2 = fill_tree_descriptor(t + 2, ENTRY_OID(n + 2));
+       buf0 = fill_tree_descriptor(r, t + 0, ENTRY_OID(n + 0));
+       buf1 = fill_tree_descriptor(r, t + 1, ENTRY_OID(n + 1));
+       buf2 = fill_tree_descriptor(r, t + 2, ENTRY_OID(n + 2));
 #undef ENTRY_OID
 
        merge_trees(t, newbase);
@@ -351,14 +352,16 @@ static void merge_trees(struct tree_desc t[3], const char *base)
        traverse_trees(&the_index, 3, t, &info);
 }
 
-static void *get_tree_descriptor(struct tree_desc *desc, const char *rev)
+static void *get_tree_descriptor(struct repository *r,
+                                struct tree_desc *desc,
+                                const char *rev)
 {
        struct object_id oid;
        void *buf;
 
-       if (get_oid(rev, &oid))
+       if (repo_get_oid(r, rev, &oid))
                die("unknown rev %s", rev);
-       buf = fill_tree_descriptor(desc, &oid);
+       buf = fill_tree_descriptor(r, desc, &oid);
        if (!buf)
                die("%s is not a tree", rev);
        return buf;
@@ -366,15 +369,16 @@ static void *get_tree_descriptor(struct tree_desc *desc, const char *rev)
 
 int cmd_merge_tree(int argc, const char **argv, const char *prefix)
 {
+       struct repository *r = the_repository;
        struct tree_desc t[3];
        void *buf1, *buf2, *buf3;
 
        if (argc != 4)
                usage(merge_tree_usage);
 
-       buf1 = get_tree_descriptor(t+0, argv[1]);
-       buf2 = get_tree_descriptor(t+1, argv[2]);
-       buf3 = get_tree_descriptor(t+2, argv[3]);
+       buf1 = get_tree_descriptor(r, t+0, argv[1]);
+       buf2 = get_tree_descriptor(r, t+1, argv[2]);
+       buf3 = get_tree_descriptor(r, t+2, argv[3]);
        merge_trees(t, "");
        free(buf1);
        free(buf2);
index aad5a9504c8546db0adfd36c59cb1ad1918a56e9..e2ccbc44e204173b09f5ad4b704a31a9e8643bb6 100644 (file)
@@ -892,6 +892,7 @@ static int finish_automerge(struct commit *head,
        struct strbuf buf = STRBUF_INIT;
        struct object_id result_commit;
 
+       write_tree_trivial(result_tree);
        free_commit_list(common);
        parents = remoteheads;
        if (!head_subsumed || fast_forward == FF_NO)
@@ -1586,8 +1587,8 @@ int cmd_merge(int argc, const char **argv, const char *prefix)
            save_state(&stash))
                oidclr(&stash);
 
-       for (i = 0; i < use_strategies_nr; i++) {
-               int ret;
+       for (i = 0; !merge_was_ok && i < use_strategies_nr; i++) {
+               int ret, cnt;
                if (i) {
                        printf(_("Rewinding the tree to pristine...\n"));
                        restore_state(&head_commit->object.oid, &stash);
@@ -1604,40 +1605,26 @@ int cmd_merge(int argc, const char **argv, const char *prefix)
                ret = try_merge_strategy(use_strategies[i]->name,
                                         common, remoteheads,
                                         head_commit);
-               if (!option_commit && !ret) {
-                       merge_was_ok = 1;
-                       /*
-                        * This is necessary here just to avoid writing
-                        * the tree, but later we will *not* exit with
-                        * status code 1 because merge_was_ok is set.
-                        */
-                       ret = 1;
-               }
-
-               if (ret) {
-                       /*
-                        * The backend exits with 1 when conflicts are
-                        * left to be resolved, with 2 when it does not
-                        * handle the given merge at all.
-                        */
-                       if (ret == 1) {
-                               int cnt = evaluate_result();
-
-                               if (best_cnt <= 0 || cnt <= best_cnt) {
-                                       best_strategy = use_strategies[i]->name;
-                                       best_cnt = cnt;
+               /*
+                * The backend exits with 1 when conflicts are
+                * left to be resolved, with 2 when it does not
+                * handle the given merge at all.
+                */
+               if (ret < 2) {
+                       if (!ret) {
+                               if (option_commit) {
+                                       /* Automerge succeeded. */
+                                       automerge_was_ok = 1;
+                                       break;
                                }
+                               merge_was_ok = 1;
+                       }
+                       cnt = evaluate_result();
+                       if (best_cnt <= 0 || cnt <= best_cnt) {
+                               best_strategy = use_strategies[i]->name;
+                               best_cnt = cnt;
                        }
-                       if (merge_was_ok)
-                               break;
-                       else
-                               continue;
                }
-
-               /* Automerge succeeded. */
-               write_tree_trivial(&result_tree);
-               automerge_was_ok = 1;
-               break;
        }
 
        /*
index 72dfd3dadc7bf8037d4bd11d24aabca6a56a5fa8..b1ea1a6aa17724915a529a882831640aec7ea7b8 100644 (file)
@@ -6,12 +6,13 @@
 #include "trace2.h"
 
 static char const * const builtin_multi_pack_index_usage[] = {
-       N_("git multi-pack-index [--object-dir=<dir>] (write|verify)"),
+       N_("git multi-pack-index [--object-dir=<dir>] (write|verify|expire|repack --batch-size=<size>)"),
        NULL
 };
 
 static struct opts_multi_pack_index {
        const char *object_dir;
+       unsigned long batch_size;
 } opts;
 
 int cmd_multi_pack_index(int argc, const char **argv,
@@ -20,6 +21,8 @@ int cmd_multi_pack_index(int argc, const char **argv,
        static struct option builtin_multi_pack_index_options[] = {
                OPT_FILENAME(0, "object-dir", &opts.object_dir,
                  N_("object directory containing set of packfile and pack-index pairs")),
+               OPT_MAGNITUDE(0, "batch-size", &opts.batch_size,
+                 N_("during repack, collect pack-files of smaller size into a batch that is larger than this size")),
                OPT_END(),
        };
 
@@ -43,10 +46,17 @@ int cmd_multi_pack_index(int argc, const char **argv,
 
        trace2_cmd_mode(argv[0]);
 
+       if (!strcmp(argv[0], "repack"))
+               return midx_repack(the_repository, opts.object_dir, (size_t)opts.batch_size);
+       if (opts.batch_size)
+               die(_("--batch-size option is only for 'repack' subcommand"));
+
        if (!strcmp(argv[0], "write"))
                return write_midx_file(opts.object_dir);
        if (!strcmp(argv[0], "verify"))
                return verify_midx_file(the_repository, opts.object_dir);
+       if (!strcmp(argv[0], "expire"))
+               return expire_midx_packs(the_repository, opts.object_dir);
 
-       die(_("unrecognized verb: %s"), argv[0]);
+       die(_("unrecognized subcommand: %s"), argv[0]);
 }
index 000dc4b872b23d555d87f475511ca60d601bc4a1..267c562b1f81dad2c42e2392a76ff0a269d38f65 100644 (file)
@@ -1428,7 +1428,8 @@ static void add_preferred_base(struct object_id *oid)
        if (window <= num_preferred_base++)
                return;
 
-       data = read_object_with_reference(oid, tree_type, &size, &tree_oid);
+       data = read_object_with_reference(the_repository, oid,
+                                         tree_type, &size, &tree_oid);
        if (!data)
                return;
 
index 89fc4b8153e4ec0e0f9be8786caa1ab5f67fc49e..95d34223e93774edbede0c83d781587777d647f3 100644 (file)
@@ -850,13 +850,13 @@ static int reset_head(struct object_id *oid, const char *action,
                goto leave_reset_head;
        }
 
-       if (!reset_hard && !fill_tree_descriptor(&desc[nr++], &head_oid)) {
+       if (!reset_hard && !fill_tree_descriptor(the_repository, &desc[nr++], &head_oid)) {
                ret = error(_("failed to find tree of %s"),
                            oid_to_hex(&head_oid));
                goto leave_reset_head;
        }
 
-       if (!fill_tree_descriptor(&desc[nr++], oid)) {
+       if (!fill_tree_descriptor(the_repository, &desc[nr++], oid)) {
                ret = error(_("failed to find tree of %s"), oid_to_hex(oid));
                goto leave_reset_head;
        }
index 610eadf5f092a651fe3d04b07528f40f6adddafe..dcf385511f07dcd8efb5079077823b9df9d0d850 100644 (file)
@@ -12,7 +12,6 @@
 #include "object.h"
 #include "remote.h"
 #include "connect.h"
-#include "transport.h"
 #include "string-list.h"
 #include "sha1-array.h"
 #include "connected.h"
index f834b5551b1ffe003b943c18cd35397e9196e730..30982ed2a2aa750b68f1613e8f14b049708b70c4 100644 (file)
@@ -89,6 +89,17 @@ static void remove_pack_on_signal(int signo)
        raise(signo);
 }
 
+static int has_pack_keep_file(void)
+{
+       struct packed_git *p;
+
+       for (p = get_all_packs(the_repository); p; p = p->next) {
+               if (p->pack_keep)
+                       return 1;
+       }
+       return 0;
+}
+
 /*
  * Adds all packs hex strings to the fname list, which do not
  * have a corresponding .keep file. These packs are not to
@@ -129,19 +140,9 @@ static void get_non_kept_pack_filenames(struct string_list *fname_list,
 
 static void remove_redundant_pack(const char *dir_name, const char *base_name)
 {
-       const char *exts[] = {".pack", ".idx", ".keep", ".bitmap", ".promisor"};
-       int i;
        struct strbuf buf = STRBUF_INIT;
-       size_t plen;
-
-       strbuf_addf(&buf, "%s/%s", dir_name, base_name);
-       plen = buf.len;
-
-       for (i = 0; i < ARRAY_SIZE(exts); i++) {
-               strbuf_setlen(&buf, plen);
-               strbuf_addstr(&buf, exts[i]);
-               unlink(buf.buf);
-       }
+       strbuf_addf(&buf, "%s/%s.pack", dir_name, base_name);
+       unlink_pack_path(buf.buf, 1);
        strbuf_release(&buf);
 }
 
@@ -343,9 +344,12 @@ int cmd_repack(int argc, const char **argv, const char *prefix)
            (unpack_unreachable || (pack_everything & LOOSEN_UNREACHABLE)))
                die(_("--keep-unreachable and -A are incompatible"));
 
-       if (write_bitmaps < 0)
+       if (write_bitmaps < 0) {
                write_bitmaps = (pack_everything & ALL_INTO_ONE) &&
-                                is_bare_repository();
+                                is_bare_repository() &&
+                                keep_pack_list.nr == 0 &&
+                                !has_pack_keep_file();
+       }
        if (pack_kept_objects < 0)
                pack_kept_objects = write_bitmaps;
 
index c2bb35a4b7048c94f79057ed3db4cbdd30a28504..fdd572168b51cc388a098008c3636ffa60856233 100644 (file)
@@ -79,13 +79,13 @@ static int reset_index(const struct object_id *oid, int reset_type, int quiet)
                struct object_id head_oid;
                if (get_oid("HEAD", &head_oid))
                        return error(_("You do not have a valid HEAD."));
-               if (!fill_tree_descriptor(desc + nr, &head_oid))
+               if (!fill_tree_descriptor(the_repository, desc + nr, &head_oid))
                        return error(_("Failed to find tree of HEAD."));
                nr++;
                opts.fn = twoway_merge;
        }
 
-       if (!fill_tree_descriptor(desc + nr, oid)) {
+       if (!fill_tree_descriptor(the_repository, desc + nr, oid)) {
                error(_("Failed to find tree of %s."), oid_to_hex(oid));
                goto out;
        }
index 4e71b2f2aa292ffc3187aac61001e7fd529255b3..f61cc5d82cf2697583b5851893ba4076f96643a5 100644 (file)
@@ -102,6 +102,7 @@ static int run_sequencer(int argc, const char **argv, struct replay_opts *opts)
                OPT_CMDMODE(0, "quit", &cmd, N_("end revert or cherry-pick sequence"), 'q'),
                OPT_CMDMODE(0, "continue", &cmd, N_("resume revert or cherry-pick sequence"), 'c'),
                OPT_CMDMODE(0, "abort", &cmd, N_("cancel revert or cherry-pick sequence"), 'a'),
+               OPT_CMDMODE(0, "skip", &cmd, N_("skip current commit and continue"), 's'),
                OPT_CLEANUP(&cleanup_arg),
                OPT_BOOL('n', "no-commit", &opts->no_commit, N_("don't automatically commit")),
                OPT_BOOL('e', "edit", &opts->edit, N_("edit the commit message")),
@@ -151,6 +152,8 @@ static int run_sequencer(int argc, const char **argv, struct replay_opts *opts)
                        this_operation = "--quit";
                else if (cmd == 'c')
                        this_operation = "--continue";
+               else if (cmd == 's')
+                       this_operation = "--skip";
                else {
                        assert(cmd == 'a');
                        this_operation = "--abort";
@@ -210,6 +213,8 @@ static int run_sequencer(int argc, const char **argv, struct replay_opts *opts)
                return sequencer_continue(the_repository, opts);
        if (cmd == 'a')
                return sequencer_rollback(the_repository, opts);
+       if (cmd == 's')
+               return sequencer_skip(the_repository, opts);
        return sequencer_pick_revisions(the_repository, opts);
 }
 
index be8edc6d1e1185fa0224b516c0e4f73ceb590aa3..2eacda42b4a8fff746e846891fce0107d80efbe1 100644 (file)
@@ -179,7 +179,7 @@ static int check_local_mod(struct object_id *head, int index_only)
                 * way as changed from the HEAD.
                 */
                if (no_head
-                    || get_tree_entry(head, name, &oid, &mode)
+                    || get_tree_entry(the_repository, head, name, &oid, &mode)
                     || ce->ce_mode != create_ce_mode(mode)
                     || !oideq(&ce->oid, &oid))
                        staged_changes = 1;
index 3f8cc6ccb47c2f8927e91076a8844d2cfd8a0bb2..dff2f4b837208deebeb34f4af687c18df7dbfe76 100644 (file)
@@ -601,7 +601,7 @@ static struct cache_entry *read_one_ent(const char *which,
        struct object_id oid;
        struct cache_entry *ce;
 
-       if (get_tree_entry(ent, path, &oid, &mode)) {
+       if (get_tree_entry(the_repository, ent, path, &oid, &mode)) {
                if (which)
                        error("%s: not in %s branch.", path, which);
                return NULL;
diff --git a/cache.h b/cache.h
index 3167585cabda5f91f4c501d9b5bf924b4f5a3e12..b1da1ab08faad3da19657a9a5dcf5f2592c2127c 100644 (file)
--- a/cache.h
+++ b/cache.h
@@ -1476,7 +1476,8 @@ int df_name_compare(const char *name1, int len1, int mode1, const char *name2, i
 int name_compare(const char *name1, size_t len1, const char *name2, size_t len2);
 int cache_name_stage_compare(const char *name1, int len1, int stage1, const char *name2, int len2, int stage2);
 
-void *read_object_with_reference(const struct object_id *oid,
+void *read_object_with_reference(struct repository *r,
+                                const struct object_id *oid,
                                 const char *required_type,
                                 unsigned long *size,
                                 struct object_id *oid_ret);
@@ -1762,8 +1763,8 @@ int add_files_to_cache(const char *prefix, const struct pathspec *pathspec, int
 extern int diff_auto_refresh_index;
 
 /* match-trees.c */
-void shift_tree(const struct object_id *, const struct object_id *, struct object_id *, int);
-void shift_tree_by(const struct object_id *, const struct object_id *, struct object_id *, const char *);
+void shift_tree(struct repository *, const struct object_id *, const struct object_id *, struct object_id *, int);
+void shift_tree_by(struct repository *, const struct object_id *, const struct object_id *, struct object_id *, const char *);
 
 /*
  * whitespace rules.
index 0c7171a17354fd6dba5017e443b3d1b936f73e34..44db2d5cbb2241c7bd3ffd69d15a73c1fd4357ae 100755 (executable)
--- a/ci/lib.sh
+++ b/ci/lib.sh
@@ -186,7 +186,7 @@ osx-clang|osx-gcc)
        export GIT_SKIP_TESTS="t9810 t9816"
        ;;
 GIT_TEST_GETTEXT_POISON)
-       export GIT_TEST_GETTEXT_POISON=YesPlease
+       export GIT_TEST_GETTEXT_POISON=true
        ;;
 esac
 
index 8cc1d1d6c3aff0842c42ecda5cc545c9eb085802..b3c4de79b6da4502726dbec5e21b11734d76e28e 100644 (file)
@@ -22,6 +22,7 @@
 #define GRAPH_CHUNKID_OIDLOOKUP 0x4f49444c /* "OIDL" */
 #define GRAPH_CHUNKID_DATA 0x43444154 /* "CDAT" */
 #define GRAPH_CHUNKID_EXTRAEDGES 0x45444745 /* "EDGE" */
+#define GRAPH_CHUNKID_BASE 0x42415345 /* "BASE" */
 
 #define GRAPH_DATA_WIDTH (the_hash_algo->rawsz + 16)
 
 
 char *get_commit_graph_filename(const char *obj_dir)
 {
-       return xstrfmt("%s/info/commit-graph", obj_dir);
+       char *filename = xstrfmt("%s/info/commit-graph", obj_dir);
+       char *normalized = xmalloc(strlen(filename) + 1);
+       normalize_path_copy(normalized, filename);
+       free(filename);
+       return normalized;
+}
+
+static char *get_split_graph_filename(const char *obj_dir,
+                                     const char *oid_hex)
+{
+       char *filename = xstrfmt("%s/info/commit-graphs/graph-%s.graph",
+                                obj_dir,
+                                oid_hex);
+       char *normalized = xmalloc(strlen(filename) + 1);
+       normalize_path_copy(normalized, filename);
+       free(filename);
+       return normalized;
+}
+
+static char *get_chain_filename(const char *obj_dir)
+{
+       return xstrfmt("%s/info/commit-graphs/commit-graph-chain", obj_dir);
 }
 
 static uint8_t oid_version(void)
@@ -249,6 +271,12 @@ struct commit_graph *parse_commit_graph(void *graph_map, int fd,
                        else
                                graph->chunk_extra_edges = data + chunk_offset;
                        break;
+
+               case GRAPH_CHUNKID_BASE:
+                       if (graph->chunk_base_graphs)
+                               chunk_repeated = 1;
+                       else
+                               graph->chunk_base_graphs = data + chunk_offset;
                }
 
                if (chunk_repeated) {
@@ -267,6 +295,8 @@ struct commit_graph *parse_commit_graph(void *graph_map, int fd,
                last_chunk_offset = chunk_offset;
        }
 
+       hashcpy(graph->oid.hash, graph->data + graph->data_len - graph->hash_len);
+
        if (verify_commit_graph_lite(graph)) {
                free(graph);
                return NULL;
@@ -280,26 +310,151 @@ static struct commit_graph *load_commit_graph_one(const char *graph_file)
 
        struct stat st;
        int fd;
+       struct commit_graph *g;
        int open_ok = open_commit_graph(graph_file, &fd, &st);
 
        if (!open_ok)
                return NULL;
 
-       return load_commit_graph_one_fd_st(fd, &st);
+       g = load_commit_graph_one_fd_st(fd, &st);
+
+       if (g)
+               g->filename = xstrdup(graph_file);
+
+       return g;
+}
+
+static struct commit_graph *load_commit_graph_v1(struct repository *r, const char *obj_dir)
+{
+       char *graph_name = get_commit_graph_filename(obj_dir);
+       struct commit_graph *g = load_commit_graph_one(graph_name);
+       free(graph_name);
+
+       if (g)
+               g->obj_dir = obj_dir;
+
+       return g;
+}
+
+static int add_graph_to_chain(struct commit_graph *g,
+                             struct commit_graph *chain,
+                             struct object_id *oids,
+                             int n)
+{
+       struct commit_graph *cur_g = chain;
+
+       if (n && !g->chunk_base_graphs) {
+               warning(_("commit-graph has no base graphs chunk"));
+               return 0;
+       }
+
+       while (n) {
+               n--;
+
+               if (!cur_g ||
+                   !oideq(&oids[n], &cur_g->oid) ||
+                   !hasheq(oids[n].hash, g->chunk_base_graphs + g->hash_len * n)) {
+                       warning(_("commit-graph chain does not match"));
+                       return 0;
+               }
+
+               cur_g = cur_g->base_graph;
+       }
+
+       g->base_graph = chain;
+
+       if (chain)
+               g->num_commits_in_base = chain->num_commits + chain->num_commits_in_base;
+
+       return 1;
+}
+
+static struct commit_graph *load_commit_graph_chain(struct repository *r, const char *obj_dir)
+{
+       struct commit_graph *graph_chain = NULL;
+       struct strbuf line = STRBUF_INIT;
+       struct stat st;
+       struct object_id *oids;
+       int i = 0, valid = 1, count;
+       char *chain_name = get_chain_filename(obj_dir);
+       FILE *fp;
+       int stat_res;
+
+       fp = fopen(chain_name, "r");
+       stat_res = stat(chain_name, &st);
+       free(chain_name);
+
+       if (!fp ||
+           stat_res ||
+           st.st_size <= the_hash_algo->hexsz)
+               return NULL;
+
+       count = st.st_size / (the_hash_algo->hexsz + 1);
+       oids = xcalloc(count, sizeof(struct object_id));
+
+       prepare_alt_odb(r);
+
+       for (i = 0; i < count; i++) {
+               struct object_directory *odb;
+
+               if (strbuf_getline_lf(&line, fp) == EOF)
+                       break;
+
+               if (get_oid_hex(line.buf, &oids[i])) {
+                       warning(_("invalid commit-graph chain: line '%s' not a hash"),
+                               line.buf);
+                       valid = 0;
+                       break;
+               }
+
+               valid = 0;
+               for (odb = r->objects->odb; odb; odb = odb->next) {
+                       char *graph_name = get_split_graph_filename(odb->path, line.buf);
+                       struct commit_graph *g = load_commit_graph_one(graph_name);
+
+                       free(graph_name);
+
+                       if (g) {
+                               g->obj_dir = odb->path;
+
+                               if (add_graph_to_chain(g, graph_chain, oids, i)) {
+                                       graph_chain = g;
+                                       valid = 1;
+                               }
+
+                               break;
+                       }
+               }
+
+               if (!valid) {
+                       warning(_("unable to find all commit-graph files"));
+                       break;
+               }
+       }
+
+       free(oids);
+       fclose(fp);
+
+       return graph_chain;
+}
+
+struct commit_graph *read_commit_graph_one(struct repository *r, const char *obj_dir)
+{
+       struct commit_graph *g = load_commit_graph_v1(r, obj_dir);
+
+       if (!g)
+               g = load_commit_graph_chain(r, obj_dir);
+
+       return g;
 }
 
 static void prepare_commit_graph_one(struct repository *r, const char *obj_dir)
 {
-       char *graph_name;
 
        if (r->objects->commit_graph)
                return;
 
-       graph_name = get_commit_graph_filename(obj_dir);
-       r->objects->commit_graph =
-               load_commit_graph_one(graph_name);
-
-       FREE_AND_NULL(graph_name);
+       r->objects->commit_graph = read_commit_graph_one(r, obj_dir);
 }
 
 /*
@@ -361,9 +516,18 @@ int generation_numbers_enabled(struct repository *r)
        return !!first_generation;
 }
 
+static void close_commit_graph_one(struct commit_graph *g)
+{
+       if (!g)
+               return;
+
+       close_commit_graph_one(g->base_graph);
+       free_commit_graph(g);
+}
+
 void close_commit_graph(struct raw_object_store *o)
 {
-       free_commit_graph(o->commit_graph);
+       close_commit_graph_one(o->commit_graph);
        o->commit_graph = NULL;
 }
 
@@ -373,18 +537,38 @@ static int bsearch_graph(struct commit_graph *g, struct object_id *oid, uint32_t
                            g->chunk_oid_lookup, g->hash_len, pos);
 }
 
+static void load_oid_from_graph(struct commit_graph *g,
+                               uint32_t pos,
+                               struct object_id *oid)
+{
+       uint32_t lex_index;
+
+       while (g && pos < g->num_commits_in_base)
+               g = g->base_graph;
+
+       if (!g)
+               BUG("NULL commit-graph");
+
+       if (pos >= g->num_commits + g->num_commits_in_base)
+               die(_("invalid commit position. commit-graph is likely corrupt"));
+
+       lex_index = pos - g->num_commits_in_base;
+
+       hashcpy(oid->hash, g->chunk_oid_lookup + g->hash_len * lex_index);
+}
+
 static struct commit_list **insert_parent_or_die(struct repository *r,
                                                 struct commit_graph *g,
-                                                uint64_t pos,
+                                                uint32_t pos,
                                                 struct commit_list **pptr)
 {
        struct commit *c;
        struct object_id oid;
 
-       if (pos >= g->num_commits)
-               die("invalid parent position %"PRIu64, pos);
+       if (pos >= g->num_commits + g->num_commits_in_base)
+               die("invalid parent position %"PRIu32, pos);
 
-       hashcpy(oid.hash, g->chunk_oid_lookup + g->hash_len * pos);
+       load_oid_from_graph(g, pos, &oid);
        c = lookup_commit(r, &oid);
        if (!c)
                die(_("could not find commit %s"), oid_to_hex(&oid));
@@ -394,7 +578,14 @@ static struct commit_list **insert_parent_or_die(struct repository *r,
 
 static void fill_commit_graph_info(struct commit *item, struct commit_graph *g, uint32_t pos)
 {
-       const unsigned char *commit_data = g->chunk_commit_data + GRAPH_DATA_WIDTH * pos;
+       const unsigned char *commit_data;
+       uint32_t lex_index;
+
+       while (pos < g->num_commits_in_base)
+               g = g->base_graph;
+
+       lex_index = pos - g->num_commits_in_base;
+       commit_data = g->chunk_commit_data + GRAPH_DATA_WIDTH * lex_index;
        item->graph_pos = pos;
        item->generation = get_be32(commit_data + g->hash_len + 8) >> 2;
 }
@@ -412,10 +603,25 @@ static int fill_commit_in_graph(struct repository *r,
        uint32_t *parent_data_ptr;
        uint64_t date_low, date_high;
        struct commit_list **pptr;
-       const unsigned char *commit_data = g->chunk_commit_data + (g->hash_len + 16) * pos;
+       const unsigned char *commit_data;
+       uint32_t lex_index;
 
-       item->object.parsed = 1;
+       while (pos < g->num_commits_in_base)
+               g = g->base_graph;
+
+       if (pos >= g->num_commits + g->num_commits_in_base)
+               die(_("invalid commit position. commit-graph is likely corrupt"));
+
+       /*
+        * Store the "full" position, but then use the
+        * "local" position for the rest of the calculation.
+        */
        item->graph_pos = pos;
+       lex_index = pos - g->num_commits_in_base;
+
+       commit_data = g->chunk_commit_data + (g->hash_len + 16) * lex_index;
+
+       item->object.parsed = 1;
 
        set_commit_tree(item, NULL);
 
@@ -459,7 +665,18 @@ static int find_commit_in_graph(struct commit *item, struct commit_graph *g, uin
                *pos = item->graph_pos;
                return 1;
        } else {
-               return bsearch_graph(g, &(item->object.oid), pos);
+               struct commit_graph *cur_g = g;
+               uint32_t lex_index;
+
+               while (cur_g && !bsearch_graph(cur_g, &(item->object.oid), &lex_index))
+                       cur_g = cur_g->base_graph;
+
+               if (cur_g) {
+                       *pos = lex_index + cur_g->num_commits_in_base;
+                       return 1;
+               }
+
+               return 0;
        }
 }
 
@@ -499,8 +716,13 @@ static struct tree *load_tree_for_commit(struct repository *r,
                                         struct commit *c)
 {
        struct object_id oid;
-       const unsigned char *commit_data = g->chunk_commit_data +
-                                          GRAPH_DATA_WIDTH * (c->graph_pos);
+       const unsigned char *commit_data;
+
+       while (c->graph_pos < g->num_commits_in_base)
+               g = g->base_graph;
+
+       commit_data = g->chunk_commit_data +
+                       GRAPH_DATA_WIDTH * (c->graph_pos - g->num_commits_in_base);
 
        hashcpy(oid.hash, commit_data);
        set_commit_tree(c, lookup_tree(r, &oid));
@@ -539,7 +761,7 @@ struct packed_oid_list {
 
 struct write_commit_graph_context {
        struct repository *r;
-       const char *obj_dir;
+       char *obj_dir;
        char *graph_name;
        struct packed_oid_list oids;
        struct packed_commit_list commits;
@@ -548,8 +770,21 @@ struct write_commit_graph_context {
        struct progress *progress;
        int progress_done;
        uint64_t progress_cnt;
+
+       char *base_graph_name;
+       int num_commit_graphs_before;
+       int num_commit_graphs_after;
+       char **commit_graph_filenames_before;
+       char **commit_graph_filenames_after;
+       char **commit_graph_hash_after;
+       uint32_t new_num_commits_in_base;
+       struct commit_graph *new_base_graph;
+
        unsigned append:1,
-                report_progress:1;
+                report_progress:1,
+                split:1;
+
+       const struct split_commit_graph_opts *split_opts;
 };
 
 static void write_graph_chunk_fanout(struct hashfile *f,
@@ -619,6 +854,16 @@ static void write_graph_chunk_data(struct hashfile *f, int hash_len,
                                              ctx->commits.nr,
                                              commit_to_sha1);
 
+                       if (edge_value >= 0)
+                               edge_value += ctx->new_num_commits_in_base;
+                       else {
+                               uint32_t pos;
+                               if (find_commit_in_graph(parent->item,
+                                                        ctx->new_base_graph,
+                                                        &pos))
+                                       edge_value = pos;
+                       }
+
                        if (edge_value < 0)
                                BUG("missing parent %s for commit %s",
                                    oid_to_hex(&parent->item->object.oid),
@@ -639,6 +884,17 @@ static void write_graph_chunk_data(struct hashfile *f, int hash_len,
                                              ctx->commits.list,
                                              ctx->commits.nr,
                                              commit_to_sha1);
+
+                       if (edge_value >= 0)
+                               edge_value += ctx->new_num_commits_in_base;
+                       else {
+                               uint32_t pos;
+                               if (find_commit_in_graph(parent->item,
+                                                        ctx->new_base_graph,
+                                                        &pos))
+                                       edge_value = pos;
+                       }
+
                        if (edge_value < 0)
                                BUG("missing parent %s for commit %s",
                                    oid_to_hex(&parent->item->object.oid),
@@ -696,6 +952,16 @@ static void write_graph_chunk_extra_edges(struct hashfile *f,
                                                  ctx->commits.nr,
                                                  commit_to_sha1);
 
+                       if (edge_value >= 0)
+                               edge_value += ctx->new_num_commits_in_base;
+                       else {
+                               uint32_t pos;
+                               if (find_commit_in_graph(parent->item,
+                                                        ctx->new_base_graph,
+                                                        &pos))
+                                       edge_value = pos;
+                       }
+
                        if (edge_value < 0)
                                BUG("missing parent %s for commit %s",
                                    oid_to_hex(&parent->item->object.oid),
@@ -710,7 +976,7 @@ static void write_graph_chunk_extra_edges(struct hashfile *f,
        }
 }
 
-static int commit_compare(const void *_a, const void *_b)
+static int oid_compare(const void *_a, const void *_b)
 {
        const struct object_id *a = (const struct object_id *)_a;
        const struct object_id *b = (const struct object_id *)_b;
@@ -787,7 +1053,13 @@ static void close_reachable(struct write_commit_graph_context *ctx)
                display_progress(ctx->progress, i + 1);
                commit = lookup_commit(ctx->r, &ctx->oids.list[i]);
 
-               if (commit && !parse_commit_no_graph(commit))
+               if (!commit)
+                       continue;
+               if (ctx->split) {
+                       if (!parse_commit(commit) &&
+                           commit->graph_pos == COMMIT_NOT_FROM_GRAPH)
+                               add_missing_parents(ctx, commit);
+               } else if (!parse_commit_no_graph(commit))
                        add_missing_parents(ctx, commit);
        }
        stop_progress(&ctx->progress);
@@ -861,14 +1133,15 @@ static int add_ref_to_list(const char *refname,
        return 0;
 }
 
-int write_commit_graph_reachable(const char *obj_dir, unsigned int flags)
+int write_commit_graph_reachable(const char *obj_dir, unsigned int flags,
+                                const struct split_commit_graph_opts *split_opts)
 {
        struct string_list list = STRING_LIST_INIT_DUP;
        int result;
 
        for_each_ref(add_ref_to_list, &list);
        result = write_commit_graph(obj_dir, NULL, &list,
-                                   flags);
+                                   flags, split_opts);
 
        string_list_clear(&list, 0);
        return result;
@@ -979,12 +1252,20 @@ static uint32_t count_distinct_commits(struct write_commit_graph_context *ctx)
                        _("Counting distinct commits in commit graph"),
                        ctx->oids.nr);
        display_progress(ctx->progress, 0); /* TODO: Measure QSORT() progress */
-       QSORT(ctx->oids.list, ctx->oids.nr, commit_compare);
+       QSORT(ctx->oids.list, ctx->oids.nr, oid_compare);
 
        for (i = 1; i < ctx->oids.nr; i++) {
                display_progress(ctx->progress, i + 1);
-               if (!oideq(&ctx->oids.list[i - 1], &ctx->oids.list[i]))
+               if (!oideq(&ctx->oids.list[i - 1], &ctx->oids.list[i])) {
+                       if (ctx->split) {
+                               struct commit *c = lookup_commit(ctx->r, &ctx->oids.list[i]);
+
+                               if (!c || c->graph_pos != COMMIT_NOT_FROM_GRAPH)
+                                       continue;
+                       }
+
                        count_distinct++;
+               }
        }
        stop_progress(&ctx->progress);
 
@@ -1007,7 +1288,13 @@ static void copy_oids_to_commits(struct write_commit_graph_context *ctx)
                if (i > 0 && oideq(&ctx->oids.list[i - 1], &ctx->oids.list[i]))
                        continue;
 
+               ALLOC_GROW(ctx->commits.list, ctx->commits.nr + 1, ctx->commits.alloc);
                ctx->commits.list[ctx->commits.nr] = lookup_commit(ctx->r, &ctx->oids.list[i]);
+
+               if (ctx->split &&
+                   ctx->commits.list[ctx->commits.nr]->graph_pos != COMMIT_NOT_FROM_GRAPH)
+                       continue;
+
                parse_commit_no_graph(ctx->commits.list[ctx->commits.nr]);
 
                for (parent = ctx->commits.list[ctx->commits.nr]->parents;
@@ -1022,18 +1309,56 @@ static void copy_oids_to_commits(struct write_commit_graph_context *ctx)
        stop_progress(&ctx->progress);
 }
 
+static int write_graph_chunk_base_1(struct hashfile *f,
+                                   struct commit_graph *g)
+{
+       int num = 0;
+
+       if (!g)
+               return 0;
+
+       num = write_graph_chunk_base_1(f, g->base_graph);
+       hashwrite(f, g->oid.hash, the_hash_algo->rawsz);
+       return num + 1;
+}
+
+static int write_graph_chunk_base(struct hashfile *f,
+                                 struct write_commit_graph_context *ctx)
+{
+       int num = write_graph_chunk_base_1(f, ctx->new_base_graph);
+
+       if (num != ctx->num_commit_graphs_after - 1) {
+               error(_("failed to write correct number of base graph ids"));
+               return -1;
+       }
+
+       return 0;
+}
+
 static int write_commit_graph_file(struct write_commit_graph_context *ctx)
 {
        uint32_t i;
+       int fd;
        struct hashfile *f;
        struct lock_file lk = LOCK_INIT;
-       uint32_t chunk_ids[5];
-       uint64_t chunk_offsets[5];
+       uint32_t chunk_ids[6];
+       uint64_t chunk_offsets[6];
        const unsigned hashsz = the_hash_algo->rawsz;
        struct strbuf progress_title = STRBUF_INIT;
-       int num_chunks = ctx->num_extra_edges ? 4 : 3;
+       int num_chunks = 3;
+       struct object_id file_hash;
+
+       if (ctx->split) {
+               struct strbuf tmp_file = STRBUF_INIT;
+
+               strbuf_addf(&tmp_file,
+                           "%s/info/commit-graphs/tmp_graph_XXXXXX",
+                           ctx->obj_dir);
+               ctx->graph_name = strbuf_detach(&tmp_file, NULL);
+       } else {
+               ctx->graph_name = get_commit_graph_filename(ctx->obj_dir);
+       }
 
-       ctx->graph_name = get_commit_graph_filename(ctx->obj_dir);
        if (safe_create_leading_directories(ctx->graph_name)) {
                UNLEAK(ctx->graph_name);
                error(_("unable to create leading directories of %s"),
@@ -1041,30 +1366,61 @@ static int write_commit_graph_file(struct write_commit_graph_context *ctx)
                return -1;
        }
 
-       hold_lock_file_for_update(&lk, ctx->graph_name, LOCK_DIE_ON_ERROR);
-       f = hashfd(lk.tempfile->fd, lk.tempfile->filename.buf);
+       if (ctx->split) {
+               char *lock_name = get_chain_filename(ctx->obj_dir);
 
-       hashwrite_be32(f, GRAPH_SIGNATURE);
+               hold_lock_file_for_update(&lk, lock_name, LOCK_DIE_ON_ERROR);
 
-       hashwrite_u8(f, GRAPH_VERSION);
-       hashwrite_u8(f, oid_version());
-       hashwrite_u8(f, num_chunks);
-       hashwrite_u8(f, 0); /* unused padding byte */
+               fd = git_mkstemp_mode(ctx->graph_name, 0444);
+               if (fd < 0) {
+                       error(_("unable to create '%s'"), ctx->graph_name);
+                       return -1;
+               }
+
+               f = hashfd(fd, ctx->graph_name);
+       } else {
+               hold_lock_file_for_update(&lk, ctx->graph_name, LOCK_DIE_ON_ERROR);
+               fd = lk.tempfile->fd;
+               f = hashfd(lk.tempfile->fd, lk.tempfile->filename.buf);
+       }
 
        chunk_ids[0] = GRAPH_CHUNKID_OIDFANOUT;
        chunk_ids[1] = GRAPH_CHUNKID_OIDLOOKUP;
        chunk_ids[2] = GRAPH_CHUNKID_DATA;
-       if (ctx->num_extra_edges)
-               chunk_ids[3] = GRAPH_CHUNKID_EXTRAEDGES;
-       else
-               chunk_ids[3] = 0;
-       chunk_ids[4] = 0;
+       if (ctx->num_extra_edges) {
+               chunk_ids[num_chunks] = GRAPH_CHUNKID_EXTRAEDGES;
+               num_chunks++;
+       }
+       if (ctx->num_commit_graphs_after > 1) {
+               chunk_ids[num_chunks] = GRAPH_CHUNKID_BASE;
+               num_chunks++;
+       }
+
+       chunk_ids[num_chunks] = 0;
 
        chunk_offsets[0] = 8 + (num_chunks + 1) * GRAPH_CHUNKLOOKUP_WIDTH;
        chunk_offsets[1] = chunk_offsets[0] + GRAPH_FANOUT_SIZE;
        chunk_offsets[2] = chunk_offsets[1] + hashsz * ctx->commits.nr;
        chunk_offsets[3] = chunk_offsets[2] + (hashsz + 16) * ctx->commits.nr;
-       chunk_offsets[4] = chunk_offsets[3] + 4 * ctx->num_extra_edges;
+
+       num_chunks = 3;
+       if (ctx->num_extra_edges) {
+               chunk_offsets[num_chunks + 1] = chunk_offsets[num_chunks] +
+                                               4 * ctx->num_extra_edges;
+               num_chunks++;
+       }
+       if (ctx->num_commit_graphs_after > 1) {
+               chunk_offsets[num_chunks + 1] = chunk_offsets[num_chunks] +
+                                               hashsz * (ctx->num_commit_graphs_after - 1);
+               num_chunks++;
+       }
+
+       hashwrite_be32(f, GRAPH_SIGNATURE);
+
+       hashwrite_u8(f, GRAPH_VERSION);
+       hashwrite_u8(f, oid_version());
+       hashwrite_u8(f, num_chunks);
+       hashwrite_u8(f, ctx->num_commit_graphs_after - 1);
 
        for (i = 0; i <= num_chunks; i++) {
                uint32_t chunk_write[3];
@@ -1090,23 +1446,316 @@ static int write_commit_graph_file(struct write_commit_graph_context *ctx)
        write_graph_chunk_data(f, hashsz, ctx);
        if (ctx->num_extra_edges)
                write_graph_chunk_extra_edges(f, ctx);
+       if (ctx->num_commit_graphs_after > 1 &&
+           write_graph_chunk_base(f, ctx)) {
+               return -1;
+       }
        stop_progress(&ctx->progress);
        strbuf_release(&progress_title);
 
+       if (ctx->split && ctx->base_graph_name && ctx->num_commit_graphs_after > 1) {
+               char *new_base_hash = xstrdup(oid_to_hex(&ctx->new_base_graph->oid));
+               char *new_base_name = get_split_graph_filename(ctx->new_base_graph->obj_dir, new_base_hash);
+
+               free(ctx->commit_graph_filenames_after[ctx->num_commit_graphs_after - 2]);
+               free(ctx->commit_graph_hash_after[ctx->num_commit_graphs_after - 2]);
+               ctx->commit_graph_filenames_after[ctx->num_commit_graphs_after - 2] = new_base_name;
+               ctx->commit_graph_hash_after[ctx->num_commit_graphs_after - 2] = new_base_hash;
+       }
+
        close_commit_graph(ctx->r->objects);
-       finalize_hashfile(f, NULL, CSUM_HASH_IN_STREAM | CSUM_FSYNC);
+       finalize_hashfile(f, file_hash.hash, CSUM_HASH_IN_STREAM | CSUM_FSYNC);
+
+       if (ctx->split) {
+               FILE *chainf = fdopen_lock_file(&lk, "w");
+               char *final_graph_name;
+               int result;
+
+               close(fd);
+
+               if (!chainf) {
+                       error(_("unable to open commit-graph chain file"));
+                       return -1;
+               }
+
+               if (ctx->base_graph_name) {
+                       const char *dest = ctx->commit_graph_filenames_after[
+                                               ctx->num_commit_graphs_after - 2];
+
+                       if (strcmp(ctx->base_graph_name, dest)) {
+                               result = rename(ctx->base_graph_name, dest);
+
+                               if (result) {
+                                       error(_("failed to rename base commit-graph file"));
+                                       return -1;
+                               }
+                       }
+               } else {
+                       char *graph_name = get_commit_graph_filename(ctx->obj_dir);
+                       unlink(graph_name);
+               }
+
+               ctx->commit_graph_hash_after[ctx->num_commit_graphs_after - 1] = xstrdup(oid_to_hex(&file_hash));
+               final_graph_name = get_split_graph_filename(ctx->obj_dir,
+                                       ctx->commit_graph_hash_after[ctx->num_commit_graphs_after - 1]);
+               ctx->commit_graph_filenames_after[ctx->num_commit_graphs_after - 1] = final_graph_name;
+
+               result = rename(ctx->graph_name, final_graph_name);
+
+               for (i = 0; i < ctx->num_commit_graphs_after; i++)
+                       fprintf(lk.tempfile->fp, "%s\n", ctx->commit_graph_hash_after[i]);
+
+               if (result) {
+                       error(_("failed to rename temporary commit-graph file"));
+                       return -1;
+               }
+       }
+
        commit_lock_file(&lk);
 
        return 0;
 }
 
+static void split_graph_merge_strategy(struct write_commit_graph_context *ctx)
+{
+       struct commit_graph *g = ctx->r->objects->commit_graph;
+       uint32_t num_commits = ctx->commits.nr;
+       uint32_t i;
+
+       int max_commits = 0;
+       int size_mult = 2;
+
+       if (ctx->split_opts) {
+               max_commits = ctx->split_opts->max_commits;
+               size_mult = ctx->split_opts->size_multiple;
+       }
+
+       g = ctx->r->objects->commit_graph;
+       ctx->num_commit_graphs_after = ctx->num_commit_graphs_before + 1;
+
+       while (g && (g->num_commits <= size_mult * num_commits ||
+                   (max_commits && num_commits > max_commits))) {
+               if (strcmp(g->obj_dir, ctx->obj_dir))
+                       break;
+
+               num_commits += g->num_commits;
+               g = g->base_graph;
+
+               ctx->num_commit_graphs_after--;
+       }
+
+       ctx->new_base_graph = g;
+
+       if (ctx->num_commit_graphs_after == 2) {
+               char *old_graph_name = get_commit_graph_filename(g->obj_dir);
+
+               if (!strcmp(g->filename, old_graph_name) &&
+                   strcmp(g->obj_dir, ctx->obj_dir)) {
+                       ctx->num_commit_graphs_after = 1;
+                       ctx->new_base_graph = NULL;
+               }
+
+               free(old_graph_name);
+       }
+
+       ALLOC_ARRAY(ctx->commit_graph_filenames_after, ctx->num_commit_graphs_after);
+       ALLOC_ARRAY(ctx->commit_graph_hash_after, ctx->num_commit_graphs_after);
+
+       for (i = 0; i < ctx->num_commit_graphs_after &&
+                   i < ctx->num_commit_graphs_before; i++)
+               ctx->commit_graph_filenames_after[i] = xstrdup(ctx->commit_graph_filenames_before[i]);
+
+       i = ctx->num_commit_graphs_before - 1;
+       g = ctx->r->objects->commit_graph;
+
+       while (g) {
+               if (i < ctx->num_commit_graphs_after)
+                       ctx->commit_graph_hash_after[i] = xstrdup(oid_to_hex(&g->oid));
+
+               i--;
+               g = g->base_graph;
+       }
+}
+
+static void merge_commit_graph(struct write_commit_graph_context *ctx,
+                              struct commit_graph *g)
+{
+       uint32_t i;
+       uint32_t offset = g->num_commits_in_base;
+
+       ALLOC_GROW(ctx->commits.list, ctx->commits.nr + g->num_commits, ctx->commits.alloc);
+
+       for (i = 0; i < g->num_commits; i++) {
+               struct object_id oid;
+               struct commit *result;
+
+               display_progress(ctx->progress, i + 1);
+
+               load_oid_from_graph(g, i + offset, &oid);
+
+               /* only add commits if they still exist in the repo */
+               result = lookup_commit_reference_gently(ctx->r, &oid, 1);
+
+               if (result) {
+                       ctx->commits.list[ctx->commits.nr] = result;
+                       ctx->commits.nr++;
+               }
+       }
+}
+
+static int commit_compare(const void *_a, const void *_b)
+{
+       const struct commit *a = *(const struct commit **)_a;
+       const struct commit *b = *(const struct commit **)_b;
+       return oidcmp(&a->object.oid, &b->object.oid);
+}
+
+static void sort_and_scan_merged_commits(struct write_commit_graph_context *ctx)
+{
+       uint32_t i, num_parents;
+       struct commit_list *parent;
+
+       if (ctx->report_progress)
+               ctx->progress = start_delayed_progress(
+                                       _("Scanning merged commits"),
+                                       ctx->commits.nr);
+
+       QSORT(ctx->commits.list, ctx->commits.nr, commit_compare);
+
+       ctx->num_extra_edges = 0;
+       for (i = 0; i < ctx->commits.nr; i++) {
+               display_progress(ctx->progress, i);
+
+               if (i && oideq(&ctx->commits.list[i - 1]->object.oid,
+                         &ctx->commits.list[i]->object.oid)) {
+                       die(_("unexpected duplicate commit id %s"),
+                           oid_to_hex(&ctx->commits.list[i]->object.oid));
+               } else {
+                       num_parents = 0;
+                       for (parent = ctx->commits.list[i]->parents; parent; parent = parent->next)
+                               num_parents++;
+
+                       if (num_parents > 2)
+                               ctx->num_extra_edges += num_parents - 2;
+               }
+       }
+
+       stop_progress(&ctx->progress);
+}
+
+static void merge_commit_graphs(struct write_commit_graph_context *ctx)
+{
+       struct commit_graph *g = ctx->r->objects->commit_graph;
+       uint32_t current_graph_number = ctx->num_commit_graphs_before;
+       struct strbuf progress_title = STRBUF_INIT;
+
+       while (g && current_graph_number >= ctx->num_commit_graphs_after) {
+               current_graph_number--;
+
+               if (ctx->report_progress) {
+                       strbuf_addstr(&progress_title, _("Merging commit-graph"));
+                       ctx->progress = start_delayed_progress(progress_title.buf, 0);
+               }
+
+               merge_commit_graph(ctx, g);
+               stop_progress(&ctx->progress);
+               strbuf_release(&progress_title);
+
+               g = g->base_graph;
+       }
+
+       if (g) {
+               ctx->new_base_graph = g;
+               ctx->new_num_commits_in_base = g->num_commits + g->num_commits_in_base;
+       }
+
+       if (ctx->new_base_graph)
+               ctx->base_graph_name = xstrdup(ctx->new_base_graph->filename);
+
+       sort_and_scan_merged_commits(ctx);
+}
+
+static void mark_commit_graphs(struct write_commit_graph_context *ctx)
+{
+       uint32_t i;
+       time_t now = time(NULL);
+
+       for (i = ctx->num_commit_graphs_after - 1; i < ctx->num_commit_graphs_before; i++) {
+               struct stat st;
+               struct utimbuf updated_time;
+
+               stat(ctx->commit_graph_filenames_before[i], &st);
+
+               updated_time.actime = st.st_atime;
+               updated_time.modtime = now;
+               utime(ctx->commit_graph_filenames_before[i], &updated_time);
+       }
+}
+
+static void expire_commit_graphs(struct write_commit_graph_context *ctx)
+{
+       struct strbuf path = STRBUF_INIT;
+       DIR *dir;
+       struct dirent *de;
+       size_t dirnamelen;
+       timestamp_t expire_time = time(NULL);
+
+       if (ctx->split_opts && ctx->split_opts->expire_time)
+               expire_time -= ctx->split_opts->expire_time;
+       if (!ctx->split) {
+               char *chain_file_name = get_chain_filename(ctx->obj_dir);
+               unlink(chain_file_name);
+               free(chain_file_name);
+               ctx->num_commit_graphs_after = 0;
+       }
+
+       strbuf_addstr(&path, ctx->obj_dir);
+       strbuf_addstr(&path, "/info/commit-graphs");
+       dir = opendir(path.buf);
+
+       if (!dir) {
+               strbuf_release(&path);
+               return;
+       }
+
+       strbuf_addch(&path, '/');
+       dirnamelen = path.len;
+       while ((de = readdir(dir)) != NULL) {
+               struct stat st;
+               uint32_t i, found = 0;
+
+               strbuf_setlen(&path, dirnamelen);
+               strbuf_addstr(&path, de->d_name);
+
+               stat(path.buf, &st);
+
+               if (st.st_mtime > expire_time)
+                       continue;
+               if (path.len < 6 || strcmp(path.buf + path.len - 6, ".graph"))
+                       continue;
+
+               for (i = 0; i < ctx->num_commit_graphs_after; i++) {
+                       if (!strcmp(ctx->commit_graph_filenames_after[i],
+                                   path.buf)) {
+                               found = 1;
+                               break;
+                       }
+               }
+
+               if (!found)
+                       unlink(path.buf);
+       }
+}
+
 int write_commit_graph(const char *obj_dir,
                       struct string_list *pack_indexes,
                       struct string_list *commit_hex,
-                      unsigned int flags)
+                      unsigned int flags,
+                      const struct split_commit_graph_opts *split_opts)
 {
        struct write_commit_graph_context *ctx;
        uint32_t i, count_distinct = 0;
+       size_t len;
        int res = 0;
 
        if (!commit_graph_compatible(the_repository))
@@ -1114,13 +1763,48 @@ int write_commit_graph(const char *obj_dir,
 
        ctx = xcalloc(1, sizeof(struct write_commit_graph_context));
        ctx->r = the_repository;
-       ctx->obj_dir = obj_dir;
+
+       /* normalize object dir with no trailing slash */
+       ctx->obj_dir = xmallocz(strlen(obj_dir) + 1);
+       normalize_path_copy(ctx->obj_dir, obj_dir);
+       len = strlen(ctx->obj_dir);
+       if (len && ctx->obj_dir[len - 1] == '/')
+               ctx->obj_dir[len - 1] = 0;
+
        ctx->append = flags & COMMIT_GRAPH_APPEND ? 1 : 0;
        ctx->report_progress = flags & COMMIT_GRAPH_PROGRESS ? 1 : 0;
+       ctx->split = flags & COMMIT_GRAPH_SPLIT ? 1 : 0;
+       ctx->split_opts = split_opts;
+
+       if (ctx->split) {
+               struct commit_graph *g;
+               prepare_commit_graph(ctx->r);
+
+               g = ctx->r->objects->commit_graph;
+
+               while (g) {
+                       ctx->num_commit_graphs_before++;
+                       g = g->base_graph;
+               }
+
+               if (ctx->num_commit_graphs_before) {
+                       ALLOC_ARRAY(ctx->commit_graph_filenames_before, ctx->num_commit_graphs_before);
+                       i = ctx->num_commit_graphs_before;
+                       g = ctx->r->objects->commit_graph;
+
+                       while (g) {
+                               ctx->commit_graph_filenames_before[--i] = xstrdup(g->filename);
+                               g = g->base_graph;
+                       }
+               }
+       }
 
        ctx->approx_nr_objects = approximate_object_count();
        ctx->oids.alloc = ctx->approx_nr_objects / 32;
 
+       if (ctx->split && split_opts && ctx->oids.alloc > split_opts->max_commits)
+               ctx->oids.alloc = split_opts->max_commits;
+
        if (ctx->append) {
                prepare_commit_graph_one(ctx->r, ctx->obj_dir);
                if (ctx->r->objects->commit_graph)
@@ -1171,14 +1855,45 @@ int write_commit_graph(const char *obj_dir,
                goto cleanup;
        }
 
+       if (!ctx->commits.nr)
+               goto cleanup;
+
+       if (ctx->split) {
+               split_graph_merge_strategy(ctx);
+
+               merge_commit_graphs(ctx);
+       } else
+               ctx->num_commit_graphs_after = 1;
+
        compute_generation_numbers(ctx);
 
        res = write_commit_graph_file(ctx);
 
+       if (ctx->split)
+               mark_commit_graphs(ctx);
+
+       expire_commit_graphs(ctx);
+
 cleanup:
        free(ctx->graph_name);
        free(ctx->commits.list);
        free(ctx->oids.list);
+       free(ctx->obj_dir);
+
+       if (ctx->commit_graph_filenames_after) {
+               for (i = 0; i < ctx->num_commit_graphs_after; i++) {
+                       free(ctx->commit_graph_filenames_after[i]);
+                       free(ctx->commit_graph_hash_after[i]);
+               }
+
+               for (i = 0; i < ctx->num_commit_graphs_before; i++)
+                       free(ctx->commit_graph_filenames_before[i]);
+
+               free(ctx->commit_graph_filenames_after);
+               free(ctx->commit_graph_filenames_before);
+               free(ctx->commit_graph_hash_after);
+       }
+
        free(ctx);
 
        return res;
@@ -1201,7 +1916,7 @@ static void graph_report(const char *fmt, ...)
 #define GENERATION_ZERO_EXISTS 1
 #define GENERATION_NUMBER_EXISTS 2
 
-int verify_commit_graph(struct repository *r, struct commit_graph *g)
+int verify_commit_graph(struct repository *r, struct commit_graph *g, int flags)
 {
        uint32_t i, cur_fanout_pos = 0;
        struct object_id prev_oid, cur_oid, checksum;
@@ -1209,6 +1924,7 @@ int verify_commit_graph(struct repository *r, struct commit_graph *g)
        struct hashfile *f;
        int devnull;
        struct progress *progress = NULL;
+       int local_error = 0;
 
        if (!g) {
                graph_report("no commit-graph file loaded");
@@ -1303,6 +2019,9 @@ int verify_commit_graph(struct repository *r, struct commit_graph *g)
                                break;
                        }
 
+                       /* parse parent in case it is in a base graph */
+                       parse_commit_in_graph_one(r, g, graph_parents->item);
+
                        if (!oideq(&graph_parents->item->object.oid, &odb_parents->item->object.oid))
                                graph_report(_("commit-graph parent for %s is %s != %s"),
                                             oid_to_hex(&cur_oid),
@@ -1354,7 +2073,12 @@ int verify_commit_graph(struct repository *r, struct commit_graph *g)
        }
        stop_progress(&progress);
 
-       return verify_commit_graph_error;
+       local_error = verify_commit_graph_error;
+
+       if (!(flags & COMMIT_GRAPH_VERIFY_SHALLOW) && g->base_graph)
+               local_error |= verify_commit_graph(r, g->base_graph, flags);
+
+       return local_error;
 }
 
 void free_commit_graph(struct commit_graph *g)
@@ -1366,5 +2090,6 @@ void free_commit_graph(struct commit_graph *g)
                g->data = NULL;
                close(g->graph_fd);
        }
+       free(g->filename);
        free(g);
 }
index 390c7f696104fbe772151b91cb04889d0f682401..df9a3b20e4abc7d388acab1cc85546aafa8345a3 100644 (file)
@@ -47,15 +47,21 @@ struct commit_graph {
        unsigned char num_chunks;
        uint32_t num_commits;
        struct object_id oid;
+       char *filename;
+       const char *obj_dir;
+
+       uint32_t num_commits_in_base;
+       struct commit_graph *base_graph;
 
        const uint32_t *chunk_oid_fanout;
        const unsigned char *chunk_oid_lookup;
        const unsigned char *chunk_commit_data;
        const unsigned char *chunk_extra_edges;
+       const unsigned char *chunk_base_graphs;
 };
 
 struct commit_graph *load_commit_graph_one_fd_st(int fd, struct stat *st);
-
+struct commit_graph *read_commit_graph_one(struct repository *r, const char *obj_dir);
 struct commit_graph *parse_commit_graph(void *graph_map, int fd,
                                        size_t graph_size);
 
@@ -67,6 +73,13 @@ int generation_numbers_enabled(struct repository *r);
 
 #define COMMIT_GRAPH_APPEND     (1 << 0)
 #define COMMIT_GRAPH_PROGRESS   (1 << 1)
+#define COMMIT_GRAPH_SPLIT      (1 << 2)
+
+struct split_commit_graph_opts {
+       int size_multiple;
+       int max_commits;
+       timestamp_t expire_time;
+};
 
 /*
  * The write_commit_graph* methods return zero on success
@@ -74,13 +87,17 @@ int generation_numbers_enabled(struct repository *r);
  * is not compatible with the commit-graph feature, then the
  * methods will return 0 without writing a commit-graph.
  */
-int write_commit_graph_reachable(const char *obj_dir, unsigned int flags);
+int write_commit_graph_reachable(const char *obj_dir, unsigned int flags,
+                                const struct split_commit_graph_opts *split_opts);
 int write_commit_graph(const char *obj_dir,
                       struct string_list *pack_indexes,
                       struct string_list *commit_hex,
-                      unsigned int flags);
+                      unsigned int flags,
+                      const struct split_commit_graph_opts *split_opts);
+
+#define COMMIT_GRAPH_VERIFY_SHALLOW    (1 << 0)
 
-int verify_commit_graph(struct repository *r, struct commit_graph *g);
+int verify_commit_graph(struct repository *r, struct commit_graph *g, int flags);
 
 void close_commit_graph(struct raw_object_store *);
 void free_commit_graph(struct commit_graph *);
index 48917897710169fff29d93e5f1b5313cea871808..d9913463be184fc508610c082f2ec1827898d12d 100644 (file)
@@ -2333,6 +2333,30 @@ static void setup_windows_environment(void)
        /* simulate TERM to enable auto-color (see color.c) */
        if (!getenv("TERM"))
                setenv("TERM", "cygwin", 1);
+
+       /* calculate HOME if not set */
+       if (!getenv("HOME")) {
+               /*
+                * try $HOMEDRIVE$HOMEPATH - the home share may be a network
+                * location, thus also check if the path exists (i.e. is not
+                * disconnected)
+                */
+               if ((tmp = getenv("HOMEDRIVE"))) {
+                       struct strbuf buf = STRBUF_INIT;
+                       strbuf_addstr(&buf, tmp);
+                       if ((tmp = getenv("HOMEPATH"))) {
+                               strbuf_addstr(&buf, tmp);
+                               if (is_directory(buf.buf))
+                                       setenv("HOME", buf.buf, 1);
+                               else
+                                       tmp = NULL; /* use $USERPROFILE */
+                       }
+                       strbuf_release(&buf);
+               }
+               /* use $USERPROFILE if the home share is not available */
+               if (!tmp && (tmp = getenv("USERPROFILE")))
+                       setenv("HOME", tmp, 1);
+       }
 }
 
 #if !defined(_MSC_VER)
index faa57e436cf4fc6d84580f3940bafdb4896c7658..ed7f58e0fcf221733165f3dd6a9d095d727a0a5e 100644 (file)
--- a/config.c
+++ b/config.c
@@ -973,34 +973,44 @@ int git_parse_ssize_t(const char *value, ssize_t *ret)
 NORETURN
 static void die_bad_number(const char *name, const char *value)
 {
-       const char * error_type = (errno == ERANGE)? _("out of range"):_("invalid unit");
+       const char *error_type = (errno == ERANGE) ?
+               N_("out of range") : N_("invalid unit");
+       const char *bad_numeric = N_("bad numeric config value '%s' for '%s': %s");
 
        if (!value)
                value = "";
 
+       if (!strcmp(name, "GIT_TEST_GETTEXT_POISON"))
+               /*
+                * We explicitly *don't* use _() here since it would
+                * cause an infinite loop with _() needing to call
+                * use_gettext_poison(). This is why marked up
+                * translations with N_() above.
+                */
+               die(bad_numeric, value, name, error_type);
+
        if (!(cf && cf->name))
-               die(_("bad numeric config value '%s' for '%s': %s"),
-                   value, name, error_type);
+               die(_(bad_numeric), value, name, _(error_type));
 
        switch (cf->origin_type) {
        case CONFIG_ORIGIN_BLOB:
                die(_("bad numeric config value '%s' for '%s' in blob %s: %s"),
-                   value, name, cf->name, error_type);
+                   value, name, cf->name, _(error_type));
        case CONFIG_ORIGIN_FILE:
                die(_("bad numeric config value '%s' for '%s' in file %s: %s"),
-                   value, name, cf->name, error_type);
+                   value, name, cf->name, _(error_type));
        case CONFIG_ORIGIN_STDIN:
                die(_("bad numeric config value '%s' for '%s' in standard input: %s"),
-                   value, name, error_type);
+                   value, name, _(error_type));
        case CONFIG_ORIGIN_SUBMODULE_BLOB:
                die(_("bad numeric config value '%s' for '%s' in submodule-blob %s: %s"),
-                   value, name, cf->name, error_type);
+                   value, name, cf->name, _(error_type));
        case CONFIG_ORIGIN_CMDLINE:
                die(_("bad numeric config value '%s' for '%s' in command line %s: %s"),
-                   value, name, cf->name, error_type);
+                   value, name, cf->name, _(error_type));
        default:
                die(_("bad numeric config value '%s' for '%s' in %s: %s"),
-                   value, name, cf->name, error_type);
+                   value, name, cf->name, _(error_type));
        }
 }
 
index 1ab481fed69b33b48bd95c282d2c0faff897123c..cd9b324afa5a33be7eced6a420061905d52c211f 100644 (file)
@@ -80,6 +80,7 @@ int check_connected(oid_iterate_fn fn, void *cb_data,
                argv_array_push(&rev_list.args, "--all");
        }
        argv_array_push(&rev_list.args, "--quiet");
+       argv_array_push(&rev_list.args, "--alternate-refs");
        if (opt->progress)
                argv_array_pushf(&rev_list.args, "--progress=%s",
                                 _("Checking connectivity"));
index 983e419d2b7eda8f191a878a395179dcc24eb949..1d510cd47bef49d918704a45e9592e0ffcb7ee0f 100644 (file)
@@ -286,6 +286,37 @@ __git_eread ()
        test -r "$1" && IFS=$'\r\n' read "$2" <"$1"
 }
 
+# see if a cherry-pick or revert is in progress, if the user has committed a
+# conflict resolution with 'git commit' in the middle of a sequence of picks or
+# reverts then CHERRY_PICK_HEAD/REVERT_HEAD will not exist so we have to read
+# the todo file.
+__git_sequencer_status ()
+{
+       local todo
+       if test -f "$g/CHERRY_PICK_HEAD"
+       then
+               r="|CHERRY-PICKING"
+               return 0;
+       elif test -f "$g/REVERT_HEAD"
+       then
+               r="|REVERTING"
+               return 0;
+       elif __git_eread "$g/sequencer/todo" todo
+       then
+               case "$todo" in
+               p[\ \   ]|pick[\ \      ]*)
+                       r="|CHERRY-PICKING"
+                       return 0
+               ;;
+               revert[\ \      ]*)
+                       r="|REVERTING"
+                       return 0
+               ;;
+               esac
+       fi
+       return 1
+}
+
 # __git_ps1 accepts 0 or 1 arguments (i.e., format string)
 # when called from PS1 using command substitution
 # in this mode it prints text to add to bash PS1 prompt (includes branch name)
@@ -417,10 +448,8 @@ __git_ps1 ()
                        fi
                elif [ -f "$g/MERGE_HEAD" ]; then
                        r="|MERGING"
-               elif [ -f "$g/CHERRY_PICK_HEAD" ]; then
-                       r="|CHERRY-PICKING"
-               elif [ -f "$g/REVERT_HEAD" ]; then
-                       r="|REVERTING"
+               elif __git_sequencer_status; then
+                       :
                elif [ -f "$g/BISECT_LOG" ]; then
                        r="|BISECTING"
                fi
diff --git a/diff.c b/diff.c
index 1ee04e321b1b5cc4e3f6bde37f49b7d3c0694aa6..efe42b341ae15ebceb1e216518ab3a542be361dc 100644 (file)
--- a/diff.c
+++ b/diff.c
@@ -1673,7 +1673,10 @@ static void emit_hunk_header(struct emit_callback *ecbdata,
        if (ecbdata->opt->flags.dual_color_diffed_diffs)
                strbuf_addstr(&msgbuf, reverse);
        strbuf_addstr(&msgbuf, frag);
-       strbuf_add(&msgbuf, line, ep - line);
+       if (ecbdata->opt->flags.suppress_hunk_header_line_count)
+               strbuf_add(&msgbuf, atat, sizeof(atat));
+       else
+               strbuf_add(&msgbuf, line, ep - line);
        strbuf_addstr(&msgbuf, reset);
 
        /*
@@ -4206,6 +4209,8 @@ static void run_external_diff(const char *pgm,
        argv_array_pushf(&env, "GIT_DIFF_PATH_COUNTER=%d", ++o->diff_path_counter);
        argv_array_pushf(&env, "GIT_DIFF_PATH_TOTAL=%d", q->nr);
 
+       diff_free_filespec_data(one);
+       diff_free_filespec_data(two);
        if (run_command_v_opt_cd_env(argv.argv, RUN_USING_SHELL, NULL, env.argv))
                die(_("external diff died, stopping at %s"), name);
 
diff --git a/diff.h b/diff.h
index b680b377b2f5871ecb3a27856f8d15fe2b1c8621..c2c30568101d4733d421a8e1742568bde38c7167 100644 (file)
--- a/diff.h
+++ b/diff.h
@@ -98,6 +98,7 @@ struct diff_flags {
        unsigned stat_with_summary;
        unsigned suppress_diff_headers;
        unsigned dual_color_diffed_diffs;
+       unsigned suppress_hunk_header_line_count;
 };
 
 static inline void diff_flags_or(struct diff_flags *a,
index 6dfdd6801c720f3b693ca6b09f8b7b3cf0f0c764..b44d6a467ef17f3d2f541475ab1f4ce968504a5e 100644 (file)
@@ -2410,7 +2410,8 @@ static void note_change_n(const char *p, struct branch *b, unsigned char *old_fa
                oidcpy(&commit_oid, &commit_oe->idx.oid);
        } else if (!get_oid(p, &commit_oid)) {
                unsigned long size;
-               char *buf = read_object_with_reference(&commit_oid,
+               char *buf = read_object_with_reference(the_repository,
+                                                      &commit_oid,
                                                       commit_type, &size,
                                                       &commit_oid);
                if (!buf || size < the_hash_algo->hexsz + 6)
@@ -2482,7 +2483,8 @@ static void parse_from_existing(struct branch *b)
                unsigned long size;
                char *buf;
 
-               buf = read_object_with_reference(&b->oid, commit_type, &size,
+               buf = read_object_with_reference(the_repository,
+                                                &b->oid, commit_type, &size,
                                                 &b->oid);
                parse_from_commit(b, buf, size);
                free(buf);
@@ -2560,7 +2562,8 @@ static struct hash_list *parse_merge(unsigned int *count)
                        oidcpy(&n->oid, &oe->idx.oid);
                } else if (!get_oid(from, &n->oid)) {
                        unsigned long size;
-                       char *buf = read_object_with_reference(&n->oid,
+                       char *buf = read_object_with_reference(the_repository,
+                                                              &n->oid,
                                                               commit_type,
                                                               &size, &n->oid);
                        if (!buf || size < the_hash_algo->hexsz + 6)
diff --git a/fsck.c b/fsck.c
index 117c4a978f410047b6ca4b939ca6c294d6163dfc..cdb7d8db03017e36811e2bf3803ff0a4ca032a3f 100644 (file)
--- a/fsck.c
+++ b/fsck.c
@@ -181,41 +181,6 @@ static int fsck_msg_type(enum fsck_msg_id msg_id,
        return msg_type;
 }
 
-static void init_skiplist(struct fsck_options *options, const char *path)
-{
-       FILE *fp;
-       struct strbuf sb = STRBUF_INIT;
-       struct object_id oid;
-
-       fp = fopen(path, "r");
-       if (!fp)
-               die("Could not open skip list: %s", path);
-       while (!strbuf_getline(&sb, fp)) {
-               const char *p;
-               const char *hash;
-
-               /*
-                * Allow trailing comments, leading whitespace
-                * (including before commits), and empty or whitespace
-                * only lines.
-                */
-               hash = strchr(sb.buf, '#');
-               if (hash)
-                       strbuf_setlen(&sb, hash - sb.buf);
-               strbuf_trim(&sb);
-               if (!sb.len)
-                       continue;
-
-               if (parse_oid_hex(sb.buf, &oid, &p) || *p != '\0')
-                       die("Invalid SHA-1: %s", sb.buf);
-               oidset_insert(&options->skiplist, &oid);
-       }
-       if (ferror(fp))
-               die_errno("Could not read '%s'", path);
-       fclose(fp);
-       strbuf_release(&sb);
-}
-
 static int parse_msg_type(const char *str)
 {
        if (!strcmp(str, "error"))
@@ -284,7 +249,7 @@ void fsck_set_msg_types(struct fsck_options *options, const char *values)
                if (!strcmp(buf, "skiplist")) {
                        if (equal == len)
                                die("skiplist requires a path");
-                       init_skiplist(options, buf + equal + 1);
+                       oidset_parse_file(&options->skiplist, buf + equal + 1);
                        buf += len + 1;
                        continue;
                }
index 3f2aca5c3b16d39375fab9b6203c67ae16209023..35d2c1218db2e27a6ac07fbf3c1244987e3e3021 100644 (file)
--- a/gettext.c
+++ b/gettext.c
@@ -68,10 +68,8 @@ const char *get_preferred_languages(void)
 int use_gettext_poison(void)
 {
        static int poison_requested = -1;
-       if (poison_requested == -1) {
-               const char *v = getenv("GIT_TEST_GETTEXT_POISON");
-               poison_requested = v && strlen(v) ? 1 : 0;
-       }
+       if (poison_requested == -1)
+               poison_requested = git_env_bool("GIT_TEST_GETTEXT_POISON", 0);
        return poison_requested;
 }
 
index e1d917fd2796d1f2d952c509cb79d8cafe0cf89e..8eef60b43fd47a4a679c5c77ae2f9c5ab5907951 100644 (file)
@@ -17,7 +17,9 @@ export TEXTDOMAINDIR
 
 # First decide what scheme to use...
 GIT_INTERNAL_GETTEXT_SH_SCHEME=fallthrough
-if test -n "$GIT_TEST_GETTEXT_POISON"
+if test -n "$GIT_TEST_GETTEXT_POISON" &&
+           git env--helper --type=bool --default=0 --exit-code \
+               GIT_TEST_GETTEXT_POISON
 then
        GIT_INTERNAL_GETTEXT_SH_SCHEME=poison
 elif test -n "@@USE_GETTEXT_SCHEME@@"
diff --git a/git.c b/git.c
index f4c0478f320fba5e919238c0af19890cb9185242..a9604768578f608ea5268365d2006b57c1f73d86 100644 (file)
--- a/git.c
+++ b/git.c
@@ -500,6 +500,7 @@ static struct cmd_struct commands[] = {
        { "diff-index", cmd_diff_index, RUN_SETUP | NO_PARSEOPT },
        { "diff-tree", cmd_diff_tree, RUN_SETUP | NO_PARSEOPT },
        { "difftool", cmd_difftool, RUN_SETUP_GENTLY },
+       { "env--helper", cmd_env__helper },
        { "fast-export", cmd_fast_export, RUN_SETUP },
        { "fetch", cmd_fetch, RUN_SETUP },
        { "fetch-pack", cmd_fetch_pack, RUN_SETUP | NO_PARSEOPT },
index 0a17b21187b6620c3c5c82e53271d56bc0c6deb8..3aff1849e7d5c7dba5c14b2fa34f7af64064a42d 100644 (file)
@@ -496,12 +496,13 @@ static struct commit *check_single_commit(struct rev_info *revs)
        return (struct commit *) commit;
 }
 
-static void fill_blob_sha1(struct commit *commit, struct diff_filespec *spec)
+static void fill_blob_sha1(struct repository *r, struct commit *commit,
+                          struct diff_filespec *spec)
 {
        unsigned short mode;
        struct object_id oid;
 
-       if (get_tree_entry(&commit->object.oid, spec->path, &oid, &mode))
+       if (get_tree_entry(r, &commit->object.oid, spec->path, &oid, &mode))
                die("There is no path %s in the commit", spec->path);
        fill_filespec(spec, &oid, 1, mode);
 
@@ -585,7 +586,7 @@ parse_lines(struct repository *r, struct commit *commit,
                                        name_part);
 
                spec = alloc_filespec(full_name);
-               fill_blob_sha1(commit, spec);
+               fill_blob_sha1(r, commit, spec);
                fill_line_ends(r, spec, &lines, &ends);
                cb_data.spec = spec;
                cb_data.lines = lines;
index 9d1ec8d6b01e13b20b34c836130cd9557fdd1d80..f6c194c1cca6bd64489b5dc1e90e5525da25d873 100644 (file)
@@ -248,7 +248,8 @@ static int splice_tree(const struct object_id *oid1, const char *prefix,
  * other hand, it could cover tree one and we might need to pick a
  * subtree of it.
  */
-void shift_tree(const struct object_id *hash1,
+void shift_tree(struct repository *r,
+               const struct object_id *hash1,
                const struct object_id *hash2,
                struct object_id *shifted,
                int depth_limit)
@@ -290,7 +291,7 @@ void shift_tree(const struct object_id *hash1,
                if (!*del_prefix)
                        return;
 
-               if (get_tree_entry(hash2, del_prefix, shifted, &mode))
+               if (get_tree_entry(r, hash2, del_prefix, shifted, &mode))
                        die("cannot find path %s in tree %s",
                            del_prefix, oid_to_hex(hash2));
                return;
@@ -307,7 +308,8 @@ void shift_tree(const struct object_id *hash1,
  * Unfortunately we cannot fundamentally tell which one to
  * be prefixed, as recursive merge can work in either direction.
  */
-void shift_tree_by(const struct object_id *hash1,
+void shift_tree_by(struct repository *r,
+                  const struct object_id *hash1,
                   const struct object_id *hash2,
                   struct object_id *shifted,
                   const char *shift_prefix)
@@ -317,12 +319,12 @@ void shift_tree_by(const struct object_id *hash1,
        unsigned candidate = 0;
 
        /* Can hash2 be a tree at shift_prefix in tree hash1? */
-       if (!get_tree_entry(hash1, shift_prefix, &sub1, &mode1) &&
+       if (!get_tree_entry(r, hash1, shift_prefix, &sub1, &mode1) &&
            S_ISDIR(mode1))
                candidate |= 1;
 
        /* Can hash1 be a tree at shift_prefix in tree hash2? */
-       if (!get_tree_entry(hash2, shift_prefix, &sub2, &mode2) &&
+       if (!get_tree_entry(r, hash2, shift_prefix, &sub2, &mode2) &&
            S_ISDIR(mode2))
                candidate |= 2;
 
index d2e380b7ed845e6fb2cd5616754a3f12a11c7b18..12300131fc12b15fbc9514e90ca45d76113d7267 100644 (file)
@@ -153,9 +153,9 @@ static struct tree *shift_tree_object(struct repository *repo,
        struct object_id shifted;
 
        if (!*subtree_shift) {
-               shift_tree(&one->object.oid, &two->object.oid, &shifted, 0);
+               shift_tree(repo, &one->object.oid, &two->object.oid, &shifted, 0);
        } else {
-               shift_tree_by(&one->object.oid, &two->object.oid, &shifted,
+               shift_tree_by(repo, &one->object.oid, &two->object.oid, &shifted,
                              subtree_shift);
        }
        if (oideq(&two->object.oid, &shifted))
@@ -465,17 +465,18 @@ static void get_files_dirs(struct merge_options *opt, struct tree *tree)
 {
        struct pathspec match_all;
        memset(&match_all, 0, sizeof(match_all));
-       read_tree_recursive(the_repository, tree, "", 0, 0,
+       read_tree_recursive(opt->repo, tree, "", 0, 0,
                            &match_all, save_files_dirs, opt);
 }
 
-static int get_tree_entry_if_blob(const struct object_id *tree,
+static int get_tree_entry_if_blob(struct repository *r,
+                                 const struct object_id *tree,
                                  const char *path,
                                  struct diff_filespec *dfs)
 {
        int ret;
 
-       ret = get_tree_entry(tree, path, &dfs->oid, &dfs->mode);
+       ret = get_tree_entry(r, tree, path, &dfs->oid, &dfs->mode);
        if (S_ISDIR(dfs->mode)) {
                oidcpy(&dfs->oid, &null_oid);
                dfs->mode = 0;
@@ -487,15 +488,16 @@ static int get_tree_entry_if_blob(const struct object_id *tree,
  * Returns an index_entry instance which doesn't have to correspond to
  * a real cache entry in Git's index.
  */
-static struct stage_data *insert_stage_data(const char *path,
+static struct stage_data *insert_stage_data(struct repository *r,
+               const char *path,
                struct tree *o, struct tree *a, struct tree *b,
                struct string_list *entries)
 {
        struct string_list_item *item;
        struct stage_data *e = xcalloc(1, sizeof(struct stage_data));
-       get_tree_entry_if_blob(&o->object.oid, path, &e->stages[1]);
-       get_tree_entry_if_blob(&a->object.oid, path, &e->stages[2]);
-       get_tree_entry_if_blob(&b->object.oid, path, &e->stages[3]);
+       get_tree_entry_if_blob(r, &o->object.oid, path, &e->stages[1]);
+       get_tree_entry_if_blob(r, &a->object.oid, path, &e->stages[2]);
+       get_tree_entry_if_blob(r, &b->object.oid, path, &e->stages[3]);
        item = string_list_insert(entries, path);
        item->util = e;
        return e;
@@ -1900,12 +1902,14 @@ static struct diff_queue_struct *get_diffpairs(struct merge_options *opt,
        return ret;
 }
 
-static int tree_has_path(struct tree *tree, const char *path)
+static int tree_has_path(struct repository *r, struct tree *tree,
+                        const char *path)
 {
        struct object_id hashy;
        unsigned short mode_o;
 
-       return !get_tree_entry(&tree->object.oid, path,
+       return !get_tree_entry(r,
+                              &tree->object.oid, path,
                               &hashy, &mode_o);
 }
 
@@ -2056,7 +2060,7 @@ static char *handle_path_level_conflicts(struct merge_options *opt,
         */
        if (collision_ent->reported_already) {
                clean = 0;
-       } else if (tree_has_path(tree, new_path)) {
+       } else if (tree_has_path(opt->repo, tree, new_path)) {
                collision_ent->reported_already = 1;
                strbuf_add_separated_string_list(&collision_paths, ", ",
                                                 &collision_ent->source_files);
@@ -2134,7 +2138,7 @@ static void handle_directory_level_conflicts(struct merge_options *opt,
                        string_list_append(&remove_from_merge,
                                           merge_ent->dir)->util = merge_ent;
                        strbuf_release(&merge_ent->new_dir);
-               } else if (tree_has_path(head, head_ent->dir)) {
+               } else if (tree_has_path(opt->repo, head, head_ent->dir)) {
                        /* 2. This wasn't a directory rename after all */
                        string_list_append(&remove_from_head,
                                           head_ent->dir)->util = head_ent;
@@ -2148,7 +2152,7 @@ static void handle_directory_level_conflicts(struct merge_options *opt,
        hashmap_iter_init(dir_re_merge, &iter);
        while ((merge_ent = hashmap_iter_next(&iter))) {
                head_ent = dir_rename_find_entry(dir_re_head, merge_ent->dir);
-               if (tree_has_path(merge, merge_ent->dir)) {
+               if (tree_has_path(opt->repo, merge, merge_ent->dir)) {
                        /* 2. This wasn't a directory rename after all */
                        string_list_append(&remove_from_merge,
                                           merge_ent->dir)->util = merge_ent;
@@ -2477,7 +2481,7 @@ static void apply_directory_rename_modifications(struct merge_options *opt,
                if (pair->status == 'R')
                        re->dst_entry->processed = 1;
 
-               re->dst_entry = insert_stage_data(new_path,
+               re->dst_entry = insert_stage_data(opt->repo, new_path,
                                                  o_tree, a_tree, b_tree,
                                                  entries);
                item = string_list_insert(entries, new_path);
@@ -2500,7 +2504,8 @@ static void apply_directory_rename_modifications(struct merge_options *opt,
         * the various handle_rename_*() functions update the index
         * explicitly rather than relying on unpack_trees() to have done it.
         */
-       get_tree_entry(&tree->object.oid,
+       get_tree_entry(opt->repo,
+                      &tree->object.oid,
                       pair->two->path,
                       &re->dst_entry->stages[stage].oid,
                       &re->dst_entry->stages[stage].mode);
@@ -2585,14 +2590,16 @@ static struct string_list *get_renames(struct merge_options *opt,
                re->dir_rename_original_dest = NULL;
                item = string_list_lookup(entries, re->pair->one->path);
                if (!item)
-                       re->src_entry = insert_stage_data(re->pair->one->path,
+                       re->src_entry = insert_stage_data(opt->repo,
+                                       re->pair->one->path,
                                        o_tree, a_tree, b_tree, entries);
                else
                        re->src_entry = item->util;
 
                item = string_list_lookup(entries, re->pair->two->path);
                if (!item)
-                       re->dst_entry = insert_stage_data(re->pair->two->path,
+                       re->dst_entry = insert_stage_data(opt->repo,
+                                       re->pair->two->path,
                                        o_tree, a_tree, b_tree, entries);
                else
                        re->dst_entry = item->util;
diff --git a/midx.c b/midx.c
index e7e1fe4d65ac3be54154e44ff07cd6122011405c..d6496444206aad68a88eb45583e452343c719764 100644 (file)
--- a/midx.c
+++ b/midx.c
@@ -9,6 +9,7 @@
 #include "midx.h"
 #include "progress.h"
 #include "trace2.h"
+#include "run-command.h"
 
 #define MIDX_SIGNATURE 0x4d494458 /* "MIDX" */
 #define MIDX_VERSION 1
@@ -34,6 +35,8 @@
 #define MIDX_CHUNK_LARGE_OFFSET_WIDTH (sizeof(uint64_t))
 #define MIDX_LARGE_OFFSET_NEEDED 0x80000000
 
+#define PACK_EXPIRED UINT_MAX
+
 static char *get_midx_filename(const char *object_dir)
 {
        return xstrfmt("%s/pack/multi-pack-index", object_dir);
@@ -427,13 +430,24 @@ static size_t write_midx_header(struct hashfile *f,
        return MIDX_HEADER_SIZE;
 }
 
+struct pack_info {
+       uint32_t orig_pack_int_id;
+       char *pack_name;
+       struct packed_git *p;
+       unsigned expired : 1;
+};
+
+static int pack_info_compare(const void *_a, const void *_b)
+{
+       struct pack_info *a = (struct pack_info *)_a;
+       struct pack_info *b = (struct pack_info *)_b;
+       return strcmp(a->pack_name, b->pack_name);
+}
+
 struct pack_list {
-       struct packed_git **list;
-       char **names;
+       struct pack_info *info;
        uint32_t nr;
-       uint32_t alloc_list;
-       uint32_t alloc_names;
-       size_t pack_name_concat_len;
+       uint32_t alloc;
        struct multi_pack_index *m;
 };
 
@@ -446,67 +460,33 @@ static void add_pack_to_midx(const char *full_path, size_t full_path_len,
                if (packs->m && midx_contains_pack(packs->m, file_name))
                        return;
 
-               ALLOC_GROW(packs->list, packs->nr + 1, packs->alloc_list);
-               ALLOC_GROW(packs->names, packs->nr + 1, packs->alloc_names);
+               ALLOC_GROW(packs->info, packs->nr + 1, packs->alloc);
 
-               packs->list[packs->nr] = add_packed_git(full_path,
-                                                       full_path_len,
-                                                       0);
+               packs->info[packs->nr].p = add_packed_git(full_path,
+                                                         full_path_len,
+                                                         0);
 
-               if (!packs->list[packs->nr]) {
+               if (!packs->info[packs->nr].p) {
                        warning(_("failed to add packfile '%s'"),
                                full_path);
                        return;
                }
 
-               if (open_pack_index(packs->list[packs->nr])) {
+               if (open_pack_index(packs->info[packs->nr].p)) {
                        warning(_("failed to open pack-index '%s'"),
                                full_path);
-                       close_pack(packs->list[packs->nr]);
-                       FREE_AND_NULL(packs->list[packs->nr]);
+                       close_pack(packs->info[packs->nr].p);
+                       FREE_AND_NULL(packs->info[packs->nr].p);
                        return;
                }
 
-               packs->names[packs->nr] = xstrdup(file_name);
-               packs->pack_name_concat_len += strlen(file_name) + 1;
+               packs->info[packs->nr].pack_name = xstrdup(file_name);
+               packs->info[packs->nr].orig_pack_int_id = packs->nr;
+               packs->info[packs->nr].expired = 0;
                packs->nr++;
        }
 }
 
-struct pack_pair {
-       uint32_t pack_int_id;
-       char *pack_name;
-};
-
-static int pack_pair_compare(const void *_a, const void *_b)
-{
-       struct pack_pair *a = (struct pack_pair *)_a;
-       struct pack_pair *b = (struct pack_pair *)_b;
-       return strcmp(a->pack_name, b->pack_name);
-}
-
-static void sort_packs_by_name(char **pack_names, uint32_t nr_packs, uint32_t *perm)
-{
-       uint32_t i;
-       struct pack_pair *pairs;
-
-       ALLOC_ARRAY(pairs, nr_packs);
-
-       for (i = 0; i < nr_packs; i++) {
-               pairs[i].pack_int_id = i;
-               pairs[i].pack_name = pack_names[i];
-       }
-
-       QSORT(pairs, nr_packs, pack_pair_compare);
-
-       for (i = 0; i < nr_packs; i++) {
-               pack_names[i] = pairs[i].pack_name;
-               perm[pairs[i].pack_int_id] = i;
-       }
-
-       free(pairs);
-}
-
 struct pack_midx_entry {
        struct object_id oid;
        uint32_t pack_int_id;
@@ -532,7 +512,6 @@ static int midx_oid_compare(const void *_a, const void *_b)
 }
 
 static int nth_midxed_pack_midx_entry(struct multi_pack_index *m,
-                                     uint32_t *pack_perm,
                                      struct pack_midx_entry *e,
                                      uint32_t pos)
 {
@@ -540,7 +519,7 @@ static int nth_midxed_pack_midx_entry(struct multi_pack_index *m,
                return 1;
 
        nth_midxed_object_oid(&e->oid, m, pos);
-       e->pack_int_id = pack_perm[nth_midxed_pack_int_id(m, pos)];
+       e->pack_int_id = nth_midxed_pack_int_id(m, pos);
        e->offset = nth_midxed_offset(m, pos);
 
        /* consider objects in midx to be from "old" packs */
@@ -574,8 +553,7 @@ static void fill_pack_entry(uint32_t pack_int_id,
  * of a packfile containing the object).
  */
 static struct pack_midx_entry *get_sorted_entries(struct multi_pack_index *m,
-                                                 struct packed_git **p,
-                                                 uint32_t *perm,
+                                                 struct pack_info *info,
                                                  uint32_t nr_packs,
                                                  uint32_t *nr_objects)
 {
@@ -586,7 +564,7 @@ static struct pack_midx_entry *get_sorted_entries(struct multi_pack_index *m,
        uint32_t start_pack = m ? m->num_packs : 0;
 
        for (cur_pack = start_pack; cur_pack < nr_packs; cur_pack++)
-               total_objects += p[cur_pack]->num_objects;
+               total_objects += info[cur_pack].p->num_objects;
 
        /*
         * As we de-duplicate by fanout value, we expect the fanout
@@ -611,7 +589,7 @@ static struct pack_midx_entry *get_sorted_entries(struct multi_pack_index *m,
 
                        for (cur_object = start; cur_object < end; cur_object++) {
                                ALLOC_GROW(entries_by_fanout, nr_fanout + 1, alloc_fanout);
-                               nth_midxed_pack_midx_entry(m, perm,
+                               nth_midxed_pack_midx_entry(m,
                                                           &entries_by_fanout[nr_fanout],
                                                           cur_object);
                                nr_fanout++;
@@ -622,12 +600,12 @@ static struct pack_midx_entry *get_sorted_entries(struct multi_pack_index *m,
                        uint32_t start = 0, end;
 
                        if (cur_fanout)
-                               start = get_pack_fanout(p[cur_pack], cur_fanout - 1);
-                       end = get_pack_fanout(p[cur_pack], cur_fanout);
+                               start = get_pack_fanout(info[cur_pack].p, cur_fanout - 1);
+                       end = get_pack_fanout(info[cur_pack].p, cur_fanout);
 
                        for (cur_object = start; cur_object < end; cur_object++) {
                                ALLOC_GROW(entries_by_fanout, nr_fanout + 1, alloc_fanout);
-                               fill_pack_entry(perm[cur_pack], p[cur_pack], cur_object, &entries_by_fanout[nr_fanout]);
+                               fill_pack_entry(cur_pack, info[cur_pack].p, cur_object, &entries_by_fanout[nr_fanout]);
                                nr_fanout++;
                        }
                }
@@ -656,7 +634,7 @@ static struct pack_midx_entry *get_sorted_entries(struct multi_pack_index *m,
 }
 
 static size_t write_midx_pack_names(struct hashfile *f,
-                                   char **pack_names,
+                                   struct pack_info *info,
                                    uint32_t num_packs)
 {
        uint32_t i;
@@ -664,14 +642,18 @@ static size_t write_midx_pack_names(struct hashfile *f,
        size_t written = 0;
 
        for (i = 0; i < num_packs; i++) {
-               size_t writelen = strlen(pack_names[i]) + 1;
+               size_t writelen;
+
+               if (info[i].expired)
+                       continue;
 
-               if (i && strcmp(pack_names[i], pack_names[i - 1]) <= 0)
+               if (i && strcmp(info[i].pack_name, info[i - 1].pack_name) <= 0)
                        BUG("incorrect pack-file order: %s before %s",
-                           pack_names[i - 1],
-                           pack_names[i]);
+                           info[i - 1].pack_name,
+                           info[i].pack_name);
 
-               hashwrite(f, pack_names[i], writelen);
+               writelen = strlen(info[i].pack_name) + 1;
+               hashwrite(f, info[i].pack_name, writelen);
                written += writelen;
        }
 
@@ -742,6 +724,7 @@ static size_t write_midx_oid_lookup(struct hashfile *f, unsigned char hash_len,
 }
 
 static size_t write_midx_object_offsets(struct hashfile *f, int large_offset_needed,
+                                       uint32_t *perm,
                                        struct pack_midx_entry *objects, uint32_t nr_objects)
 {
        struct pack_midx_entry *list = objects;
@@ -751,7 +734,12 @@ static size_t write_midx_object_offsets(struct hashfile *f, int large_offset_nee
        for (i = 0; i < nr_objects; i++) {
                struct pack_midx_entry *obj = list++;
 
-               hashwrite_be32(f, obj->pack_int_id);
+               if (perm[obj->pack_int_id] == PACK_EXPIRED)
+                       BUG("object %s is in an expired pack with int-id %d",
+                           oid_to_hex(&obj->oid),
+                           obj->pack_int_id);
+
+               hashwrite_be32(f, perm[obj->pack_int_id]);
 
                if (large_offset_needed && obj->offset >> 31)
                        hashwrite_be32(f, MIDX_LARGE_OFFSET_NEEDED | nr_large_offset++);
@@ -797,7 +785,8 @@ static size_t write_midx_large_offsets(struct hashfile *f, uint32_t nr_large_off
        return written;
 }
 
-int write_midx_file(const char *object_dir)
+static int write_midx_internal(const char *object_dir, struct multi_pack_index *m,
+                              struct string_list *packs_to_drop)
 {
        unsigned char cur_chunk, num_chunks = 0;
        char *midx_name;
@@ -812,6 +801,9 @@ int write_midx_file(const char *object_dir)
        uint32_t nr_entries, num_large_offsets = 0;
        struct pack_midx_entry *entries = NULL;
        int large_offsets_needed = 0;
+       int pack_name_concat_len = 0;
+       int dropped_packs = 0;
+       int result = 0;
 
        midx_name = get_midx_filename(object_dir);
        if (safe_create_leading_directories(midx_name)) {
@@ -820,42 +812,34 @@ int write_midx_file(const char *object_dir)
                          midx_name);
        }
 
-       packs.m = load_multi_pack_index(object_dir, 1);
+       if (m)
+               packs.m = m;
+       else
+               packs.m = load_multi_pack_index(object_dir, 1);
 
        packs.nr = 0;
-       packs.alloc_list = packs.m ? packs.m->num_packs : 16;
-       packs.alloc_names = packs.alloc_list;
-       packs.list = NULL;
-       packs.names = NULL;
-       packs.pack_name_concat_len = 0;
-       ALLOC_ARRAY(packs.list, packs.alloc_list);
-       ALLOC_ARRAY(packs.names, packs.alloc_names);
+       packs.alloc = packs.m ? packs.m->num_packs : 16;
+       packs.info = NULL;
+       ALLOC_ARRAY(packs.info, packs.alloc);
 
        if (packs.m) {
                for (i = 0; i < packs.m->num_packs; i++) {
-                       ALLOC_GROW(packs.list, packs.nr + 1, packs.alloc_list);
-                       ALLOC_GROW(packs.names, packs.nr + 1, packs.alloc_names);
+                       ALLOC_GROW(packs.info, packs.nr + 1, packs.alloc);
 
-                       packs.list[packs.nr] = NULL;
-                       packs.names[packs.nr] = xstrdup(packs.m->pack_names[i]);
-                       packs.pack_name_concat_len += strlen(packs.names[packs.nr]) + 1;
+                       packs.info[packs.nr].orig_pack_int_id = i;
+                       packs.info[packs.nr].pack_name = xstrdup(packs.m->pack_names[i]);
+                       packs.info[packs.nr].p = NULL;
+                       packs.info[packs.nr].expired = 0;
                        packs.nr++;
                }
        }
 
        for_each_file_in_pack_dir(object_dir, add_pack_to_midx, &packs);
 
-       if (packs.m && packs.nr == packs.m->num_packs)
+       if (packs.m && packs.nr == packs.m->num_packs && !packs_to_drop)
                goto cleanup;
 
-       if (packs.pack_name_concat_len % MIDX_CHUNK_ALIGNMENT)
-               packs.pack_name_concat_len += MIDX_CHUNK_ALIGNMENT -
-                                             (packs.pack_name_concat_len % MIDX_CHUNK_ALIGNMENT);
-
-       ALLOC_ARRAY(pack_perm, packs.nr);
-       sort_packs_by_name(packs.names, packs.nr, pack_perm);
-
-       entries = get_sorted_entries(packs.m, packs.list, pack_perm, packs.nr, &nr_entries);
+       entries = get_sorted_entries(packs.m, packs.info, packs.nr, &nr_entries);
 
        for (i = 0; i < nr_entries; i++) {
                if (entries[i].offset > 0x7fffffff)
@@ -864,6 +848,61 @@ int write_midx_file(const char *object_dir)
                        large_offsets_needed = 1;
        }
 
+       QSORT(packs.info, packs.nr, pack_info_compare);
+
+       if (packs_to_drop && packs_to_drop->nr) {
+               int drop_index = 0;
+               int missing_drops = 0;
+
+               for (i = 0; i < packs.nr && drop_index < packs_to_drop->nr; i++) {
+                       int cmp = strcmp(packs.info[i].pack_name,
+                                        packs_to_drop->items[drop_index].string);
+
+                       if (!cmp) {
+                               drop_index++;
+                               packs.info[i].expired = 1;
+                       } else if (cmp > 0) {
+                               error(_("did not see pack-file %s to drop"),
+                                     packs_to_drop->items[drop_index].string);
+                               drop_index++;
+                               missing_drops++;
+                               i--;
+                       } else {
+                               packs.info[i].expired = 0;
+                       }
+               }
+
+               if (missing_drops) {
+                       result = 1;
+                       goto cleanup;
+               }
+       }
+
+       /*
+        * pack_perm stores a permutation between pack-int-ids from the
+        * previous multi-pack-index to the new one we are writing:
+        *
+        * pack_perm[old_id] = new_id
+        */
+       ALLOC_ARRAY(pack_perm, packs.nr);
+       for (i = 0; i < packs.nr; i++) {
+               if (packs.info[i].expired) {
+                       dropped_packs++;
+                       pack_perm[packs.info[i].orig_pack_int_id] = PACK_EXPIRED;
+               } else {
+                       pack_perm[packs.info[i].orig_pack_int_id] = i - dropped_packs;
+               }
+       }
+
+       for (i = 0; i < packs.nr; i++) {
+               if (!packs.info[i].expired)
+                       pack_name_concat_len += strlen(packs.info[i].pack_name) + 1;
+       }
+
+       if (pack_name_concat_len % MIDX_CHUNK_ALIGNMENT)
+               pack_name_concat_len += MIDX_CHUNK_ALIGNMENT -
+                                       (pack_name_concat_len % MIDX_CHUNK_ALIGNMENT);
+
        hold_lock_file_for_update(&lk, midx_name, LOCK_DIE_ON_ERROR);
        f = hashfd(lk.tempfile->fd, lk.tempfile->filename.buf);
        FREE_AND_NULL(midx_name);
@@ -874,14 +913,14 @@ int write_midx_file(const char *object_dir)
        cur_chunk = 0;
        num_chunks = large_offsets_needed ? 5 : 4;
 
-       written = write_midx_header(f, num_chunks, packs.nr);
+       written = write_midx_header(f, num_chunks, packs.nr - dropped_packs);
 
        chunk_ids[cur_chunk] = MIDX_CHUNKID_PACKNAMES;
        chunk_offsets[cur_chunk] = written + (num_chunks + 1) * MIDX_CHUNKLOOKUP_WIDTH;
 
        cur_chunk++;
        chunk_ids[cur_chunk] = MIDX_CHUNKID_OIDFANOUT;
-       chunk_offsets[cur_chunk] = chunk_offsets[cur_chunk - 1] + packs.pack_name_concat_len;
+       chunk_offsets[cur_chunk] = chunk_offsets[cur_chunk - 1] + pack_name_concat_len;
 
        cur_chunk++;
        chunk_ids[cur_chunk] = MIDX_CHUNKID_OIDLOOKUP;
@@ -929,7 +968,7 @@ int write_midx_file(const char *object_dir)
 
                switch (chunk_ids[i]) {
                        case MIDX_CHUNKID_PACKNAMES:
-                               written += write_midx_pack_names(f, packs.names, packs.nr);
+                               written += write_midx_pack_names(f, packs.info, packs.nr);
                                break;
 
                        case MIDX_CHUNKID_OIDFANOUT:
@@ -941,7 +980,7 @@ int write_midx_file(const char *object_dir)
                                break;
 
                        case MIDX_CHUNKID_OBJECTOFFSETS:
-                               written += write_midx_object_offsets(f, large_offsets_needed, entries, nr_entries);
+                               written += write_midx_object_offsets(f, large_offsets_needed, pack_perm, entries, nr_entries);
                                break;
 
                        case MIDX_CHUNKID_LARGEOFFSETS:
@@ -964,19 +1003,23 @@ int write_midx_file(const char *object_dir)
 
 cleanup:
        for (i = 0; i < packs.nr; i++) {
-               if (packs.list[i]) {
-                       close_pack(packs.list[i]);
-                       free(packs.list[i]);
+               if (packs.info[i].p) {
+                       close_pack(packs.info[i].p);
+                       free(packs.info[i].p);
                }
-               free(packs.names[i]);
+               free(packs.info[i].pack_name);
        }
 
-       free(packs.list);
-       free(packs.names);
+       free(packs.info);
        free(entries);
        free(pack_perm);
        free(midx_name);
-       return 0;
+       return result;
+}
+
+int write_midx_file(const char *object_dir)
+{
+       return write_midx_internal(object_dir, NULL, NULL);
 }
 
 void clear_midx_file(struct repository *r)
@@ -1140,3 +1183,200 @@ int verify_midx_file(struct repository *r, const char *object_dir)
 
        return verify_midx_error;
 }
+
+int expire_midx_packs(struct repository *r, const char *object_dir)
+{
+       uint32_t i, *count, result = 0;
+       struct string_list packs_to_drop = STRING_LIST_INIT_DUP;
+       struct multi_pack_index *m = load_multi_pack_index(object_dir, 1);
+
+       if (!m)
+               return 0;
+
+       count = xcalloc(m->num_packs, sizeof(uint32_t));
+       for (i = 0; i < m->num_objects; i++) {
+               int pack_int_id = nth_midxed_pack_int_id(m, i);
+               count[pack_int_id]++;
+       }
+
+       for (i = 0; i < m->num_packs; i++) {
+               char *pack_name;
+
+               if (count[i])
+                       continue;
+
+               if (prepare_midx_pack(r, m, i))
+                       continue;
+
+               if (m->packs[i]->pack_keep)
+                       continue;
+
+               pack_name = xstrdup(m->packs[i]->pack_name);
+               close_pack(m->packs[i]);
+
+               string_list_insert(&packs_to_drop, m->pack_names[i]);
+               unlink_pack_path(pack_name, 0);
+               free(pack_name);
+       }
+
+       free(count);
+
+       if (packs_to_drop.nr)
+               result = write_midx_internal(object_dir, m, &packs_to_drop);
+
+       string_list_clear(&packs_to_drop, 0);
+       return result;
+}
+
+struct repack_info {
+       timestamp_t mtime;
+       uint32_t referenced_objects;
+       uint32_t pack_int_id;
+};
+
+static int compare_by_mtime(const void *a_, const void *b_)
+{
+       const struct repack_info *a, *b;
+
+       a = (const struct repack_info *)a_;
+       b = (const struct repack_info *)b_;
+
+       if (a->mtime < b->mtime)
+               return -1;
+       if (a->mtime > b->mtime)
+               return 1;
+       return 0;
+}
+
+static int fill_included_packs_all(struct multi_pack_index *m,
+                                  unsigned char *include_pack)
+{
+       uint32_t i;
+
+       for (i = 0; i < m->num_packs; i++)
+               include_pack[i] = 1;
+
+       return m->num_packs < 2;
+}
+
+static int fill_included_packs_batch(struct repository *r,
+                                    struct multi_pack_index *m,
+                                    unsigned char *include_pack,
+                                    size_t batch_size)
+{
+       uint32_t i, packs_to_repack;
+       size_t total_size;
+       struct repack_info *pack_info = xcalloc(m->num_packs, sizeof(struct repack_info));
+
+       for (i = 0; i < m->num_packs; i++) {
+               pack_info[i].pack_int_id = i;
+
+               if (prepare_midx_pack(r, m, i))
+                       continue;
+
+               pack_info[i].mtime = m->packs[i]->mtime;
+       }
+
+       for (i = 0; batch_size && i < m->num_objects; i++) {
+               uint32_t pack_int_id = nth_midxed_pack_int_id(m, i);
+               pack_info[pack_int_id].referenced_objects++;
+       }
+
+       QSORT(pack_info, m->num_packs, compare_by_mtime);
+
+       total_size = 0;
+       packs_to_repack = 0;
+       for (i = 0; total_size < batch_size && i < m->num_packs; i++) {
+               int pack_int_id = pack_info[i].pack_int_id;
+               struct packed_git *p = m->packs[pack_int_id];
+               size_t expected_size;
+
+               if (!p)
+                       continue;
+               if (open_pack_index(p) || !p->num_objects)
+                       continue;
+
+               expected_size = (size_t)(p->pack_size
+                                        * pack_info[i].referenced_objects);
+               expected_size /= p->num_objects;
+
+               if (expected_size >= batch_size)
+                       continue;
+
+               packs_to_repack++;
+               total_size += expected_size;
+               include_pack[pack_int_id] = 1;
+       }
+
+       free(pack_info);
+
+       if (total_size < batch_size || packs_to_repack < 2)
+               return 1;
+
+       return 0;
+}
+
+int midx_repack(struct repository *r, const char *object_dir, size_t batch_size)
+{
+       int result = 0;
+       uint32_t i;
+       unsigned char *include_pack;
+       struct child_process cmd = CHILD_PROCESS_INIT;
+       struct strbuf base_name = STRBUF_INIT;
+       struct multi_pack_index *m = load_multi_pack_index(object_dir, 1);
+
+       if (!m)
+               return 0;
+
+       include_pack = xcalloc(m->num_packs, sizeof(unsigned char));
+
+       if (batch_size) {
+               if (fill_included_packs_batch(r, m, include_pack, batch_size))
+                       goto cleanup;
+       } else if (fill_included_packs_all(m, include_pack))
+               goto cleanup;
+
+       argv_array_push(&cmd.args, "pack-objects");
+
+       strbuf_addstr(&base_name, object_dir);
+       strbuf_addstr(&base_name, "/pack/pack");
+       argv_array_push(&cmd.args, base_name.buf);
+       strbuf_release(&base_name);
+
+       cmd.git_cmd = 1;
+       cmd.in = cmd.out = -1;
+
+       if (start_command(&cmd)) {
+               error(_("could not start pack-objects"));
+               result = 1;
+               goto cleanup;
+       }
+
+       for (i = 0; i < m->num_objects; i++) {
+               struct object_id oid;
+               uint32_t pack_int_id = nth_midxed_pack_int_id(m, i);
+
+               if (!include_pack[pack_int_id])
+                       continue;
+
+               nth_midxed_object_oid(&oid, m, i);
+               xwrite(cmd.in, oid_to_hex(&oid), the_hash_algo->hexsz);
+               xwrite(cmd.in, "\n", 1);
+       }
+       close(cmd.in);
+
+       if (finish_command(&cmd)) {
+               error(_("could not finish pack-objects"));
+               result = 1;
+               goto cleanup;
+       }
+
+       result = write_midx_internal(object_dir, m, NULL);
+       m = NULL;
+
+cleanup:
+       if (m)
+               close_midx(m);
+       free(include_pack);
+       return result;
+}
diff --git a/midx.h b/midx.h
index 3eb29731f2b1e8e96a116a683fd8baad1020a46b..f0ae656b5d767644d60ef7b101350ca29cde7585 100644 (file)
--- a/midx.h
+++ b/midx.h
@@ -50,6 +50,8 @@ int prepare_multi_pack_index_one(struct repository *r, const char *object_dir, i
 int write_midx_file(const char *object_dir);
 void clear_midx_file(struct repository *r);
 int verify_midx_file(struct repository *r, const char *object_dir);
+int expire_midx_packs(struct repository *r, const char *object_dir);
+int midx_repack(struct repository *r, const char *object_dir, size_t batch_size);
 
 void close_midx(struct multi_pack_index *m);
 
diff --git a/notes.c b/notes.c
index 532ec37865768d05a31606f495b4f0c1645ea757..75c028b3005a8af8f39e9bceb7ab6e4b3b5dff83 100644 (file)
--- a/notes.c
+++ b/notes.c
@@ -397,7 +397,7 @@ static void load_subtree(struct notes_tree *t, struct leaf_node *subtree,
        struct name_entry entry;
        const unsigned hashsz = the_hash_algo->rawsz;
 
-       buf = fill_tree_descriptor(&desc, &subtree->val_oid);
+       buf = fill_tree_descriptor(the_repository, &desc, &subtree->val_oid);
        if (!buf)
                die("Could not read %s for notes-index",
                     oid_to_hex(&subtree->val_oid));
@@ -1015,7 +1015,7 @@ void init_notes(struct notes_tree *t, const char *notes_ref,
                return;
        if (flags & NOTES_INIT_WRITABLE && read_ref(notes_ref, &object_oid))
                die("Cannot use notes ref %s", notes_ref);
-       if (get_tree_entry(&object_oid, "", &oid, &mode))
+       if (get_tree_entry(the_repository, &object_oid, "", &oid, &mode))
                die("Failed to read notes tree referenced by %s (%s)",
                    notes_ref, oid_to_hex(&object_oid));
 
index 49f56ab8d9608919808ecebe71b9deb30c53980b..7f7b3cdd806b756eefb86c7d116ba9a9a6c62558 100644 (file)
@@ -33,6 +33,8 @@ void prepare_alt_odb(struct repository *r);
 char *compute_alternate_path(const char *path, struct strbuf *err);
 typedef int alt_odb_fn(struct object_directory *, void *);
 int foreach_alt_odb(alt_odb_fn, void*);
+typedef void alternate_ref_fn(const struct object_id *oid, void *);
+void for_each_alternate_ref(alternate_ref_fn, void *);
 
 /*
  * Add the directory to the on-disk alternates file; the new entry will also
index b0841a0f5870bafdcaf3482ed725e43ddaf5d461..6d6e840d037657721a5aa975e2567c8415547d93 100644 (file)
--- a/oidmap.c
+++ b/oidmap.c
@@ -12,13 +12,6 @@ static int oidmap_neq(const void *hashmap_cmp_fn_data,
                      &((const struct oidmap_entry *) entry_or_key)->oid);
 }
 
-static int hash(const struct object_id *oid)
-{
-       int hash;
-       memcpy(&hash, oid->hash, sizeof(hash));
-       return hash;
-}
-
 void oidmap_init(struct oidmap *map, size_t initial_size)
 {
        hashmap_init(&map->map, oidmap_neq, NULL, initial_size);
@@ -36,7 +29,7 @@ void *oidmap_get(const struct oidmap *map, const struct object_id *key)
        if (!map->map.cmpfn)
                return NULL;
 
-       return hashmap_get_from_hash(&map->map, hash(key), key);
+       return hashmap_get_from_hash(&map->map, oidhash(key), key);
 }
 
 void *oidmap_remove(struct oidmap *map, const struct object_id *key)
@@ -46,7 +39,7 @@ void *oidmap_remove(struct oidmap *map, const struct object_id *key)
        if (!map->map.cmpfn)
                oidmap_init(map, 0);
 
-       hashmap_entry_init(&entry, hash(key));
+       hashmap_entry_init(&entry, oidhash(key));
        return hashmap_remove(&map->map, &entry, key);
 }
 
@@ -57,6 +50,6 @@ void *oidmap_put(struct oidmap *map, void *entry)
        if (!map->map.cmpfn)
                oidmap_init(map, 0);
 
-       hashmap_entry_init(&to_put->internal_entry, hash(&to_put->oid));
+       hashmap_entry_init(&to_put->internal_entry, oidhash(&to_put->oid));
        return hashmap_put(&map->map, to_put);
 }
index 8bdecb13de1e0d0f24bfccb27dd46cdf64b27cff..f63ce818f67766378485ab16075dd11e87c00ca0 100644 (file)
--- a/oidset.c
+++ b/oidset.c
@@ -35,3 +35,38 @@ void oidset_clear(struct oidset *set)
        kh_release_oid_set(&set->set);
        oidset_init(set, 0);
 }
+
+void oidset_parse_file(struct oidset *set, const char *path)
+{
+       FILE *fp;
+       struct strbuf sb = STRBUF_INIT;
+       struct object_id oid;
+
+       fp = fopen(path, "r");
+       if (!fp)
+               die("could not open object name list: %s", path);
+       while (!strbuf_getline(&sb, fp)) {
+               const char *p;
+               const char *name;
+
+               /*
+                * Allow trailing comments, leading whitespace
+                * (including before commits), and empty or whitespace
+                * only lines.
+                */
+               name = strchr(sb.buf, '#');
+               if (name)
+                       strbuf_setlen(&sb, name - sb.buf);
+               strbuf_trim(&sb);
+               if (!sb.len)
+                       continue;
+
+               if (parse_oid_hex(sb.buf, &oid, &p) || *p != '\0')
+                       die("invalid object name: %s", sb.buf);
+               oidset_insert(set, &oid);
+       }
+       if (ferror(fp))
+               die_errno("Could not read '%s'", path);
+       fclose(fp);
+       strbuf_release(&sb);
+}
index 505fad578bec1691fde9f28342d4c476c60dd50c..5346563b0bccb602b8de745d1308793e8995a5e5 100644 (file)
--- a/oidset.h
+++ b/oidset.h
@@ -61,6 +61,14 @@ int oidset_remove(struct oidset *set, const struct object_id *oid);
  */
 void oidset_clear(struct oidset *set);
 
+/**
+ * Add the contents of the file 'path' to an initialized oidset.  Each line is
+ * an unabbreviated object name.  Comments begin with '#', and trailing comments
+ * are allowed.  Leading whitespace and empty or white-space only lines are
+ * ignored.
+ */
+void oidset_parse_file(struct oidset *set, const char *path);
+
 struct oidset_iter {
        kh_oid_set_t *set;
        khiter_t iter;
index c0d83fdfed973de8574224e46fb0afd4be0a98c9..fc43a6c52c75a32548c20bbc4a5aa7d0cc3ddd0d 100644 (file)
@@ -355,6 +355,34 @@ void close_object_store(struct raw_object_store *o)
        close_commit_graph(o);
 }
 
+void unlink_pack_path(const char *pack_name, int force_delete)
+{
+       static const char *exts[] = {".pack", ".idx", ".keep", ".bitmap", ".promisor"};
+       int i;
+       struct strbuf buf = STRBUF_INIT;
+       size_t plen;
+
+       strbuf_addstr(&buf, pack_name);
+       strip_suffix_mem(buf.buf, &buf.len, ".pack");
+       plen = buf.len;
+
+       if (!force_delete) {
+               strbuf_addstr(&buf, ".keep");
+               if (!access(buf.buf, F_OK)) {
+                       strbuf_release(&buf);
+                       return;
+               }
+       }
+
+       for (i = 0; i < ARRAY_SIZE(exts); i++) {
+               strbuf_setlen(&buf, plen);
+               strbuf_addstr(&buf, exts[i]);
+               unlink(buf.buf);
+       }
+
+       strbuf_release(&buf);
+}
+
 /*
  * The LRU pack is the one with the oldest MRU window, preferring packs
  * with no used windows, or the oldest mtime if it has no windows allocated.
index 81e868d55a9b1f1aeaafa4925a72ae5c53af86e9..3e98910bdd191f45d3dd86ff0360f40060944705 100644 (file)
@@ -95,6 +95,13 @@ void unuse_pack(struct pack_window **);
 void clear_delta_base_cache(void);
 struct packed_git *add_packed_git(const char *path, size_t path_len, int local);
 
+/*
+ * Unlink the .pack and associated extension files.
+ * Does not unlink if 'force_delete' is false and the pack-file is
+ * marked as ".keep".
+ */
+extern void unlink_pack_path(const char *pack_name, int force_delete);
+
 /*
  * Make sure that a pointer access into an mmap'd index file is within bounds,
  * and can provide at least 8 bytes of data.
index aa704ffcb7f70af2c42494a2f26eeef17c5eed67..07595d369b0ab3cd3a90cef74cae81661f9cb95f 100644 (file)
--- a/po/README
+++ b/po/README
@@ -293,7 +293,7 @@ To smoke out issues like these, Git tested with a translation mode that
 emits gibberish on every call to gettext. To use it run the test suite
 with it, e.g.:
 
-    cd t && GIT_TEST_GETTEXT_POISON=YesPlease prove -j 9 ./t[0-9]*.sh
+    cd t && GIT_TEST_GETTEXT_POISON=true prove -j 9 ./t[0-9]*.sh
 
 If tests break with it you should inspect them manually and see if
 what you're translating is sane, i.e. that you're not translating
index 48b0e1b4ce0ff69b9c7a430174db25b1ca6494f8..ba1e9a4265a6cb3e6ae77b2f32f9fd64bee04cc6 100644 (file)
@@ -10,6 +10,7 @@
 #include "commit.h"
 #include "pretty.h"
 #include "userdiff.h"
+#include "apply.h"
 
 struct patch_util {
        /* For the search for an exact match */
@@ -24,6 +25,17 @@ struct patch_util {
        struct object_id oid;
 };
 
+static size_t find_end_of_line(char *buffer, unsigned long size)
+{
+       char *eol = memchr(buffer, '\n', size);
+
+       if (!eol)
+               return size;
+
+       *eol = '\0';
+       return eol + 1 - buffer;
+}
+
 /*
  * Reads the patches into a string list, with the `util` field being populated
  * as struct object_id (will need to be free()d).
@@ -31,10 +43,12 @@ struct patch_util {
 static int read_patches(const char *range, struct string_list *list)
 {
        struct child_process cp = CHILD_PROCESS_INIT;
-       FILE *in;
-       struct strbuf buf = STRBUF_INIT, line = STRBUF_INIT;
+       struct strbuf buf = STRBUF_INIT, contents = STRBUF_INIT;
        struct patch_util *util = NULL;
        int in_header = 1;
+       char *line, *current_filename = NULL;
+       int offset, len;
+       size_t size;
 
        argv_array_pushl(&cp.args, "log", "--no-color", "-p", "--no-merges",
                        "--reverse", "--date-order", "--decorate=no",
@@ -54,17 +68,20 @@ static int read_patches(const char *range, struct string_list *list)
 
        if (start_command(&cp))
                return error_errno(_("could not start `log`"));
-       in = fdopen(cp.out, "r");
-       if (!in) {
+       if (strbuf_read(&contents, cp.out, 0) < 0) {
                error_errno(_("could not read `log` output"));
                finish_command(&cp);
                return -1;
        }
 
-       while (strbuf_getline(&line, in) != EOF) {
+       line = contents.buf;
+       size = contents.len;
+       for (offset = 0; size > 0; offset += len, size -= len, line += len) {
                const char *p;
 
-               if (skip_prefix(line.buf, "commit ", &p)) {
+               len = find_end_of_line(line, size);
+               line[len - 1] = '\0';
+               if (skip_prefix(line, "commit ", &p)) {
                        if (util) {
                                string_list_append(list, buf.buf)->util = util;
                                strbuf_reset(&buf);
@@ -75,8 +92,7 @@ static int read_patches(const char *range, struct string_list *list)
                                free(util);
                                string_list_clear(list, 1);
                                strbuf_release(&buf);
-                               strbuf_release(&line);
-                               fclose(in);
+                               strbuf_release(&contents);
                                finish_command(&cp);
                                return -1;
                        }
@@ -85,61 +101,95 @@ static int read_patches(const char *range, struct string_list *list)
                        continue;
                }
 
-               if (starts_with(line.buf, "diff --git")) {
+               if (starts_with(line, "diff --git")) {
+                       struct patch patch = { 0 };
+                       struct strbuf root = STRBUF_INIT;
+                       int linenr = 0;
+
                        in_header = 0;
                        strbuf_addch(&buf, '\n');
                        if (!util->diff_offset)
                                util->diff_offset = buf.len;
-                       strbuf_addch(&buf, ' ');
-                       strbuf_addbuf(&buf, &line);
+                       line[len - 1] = '\n';
+                       len = parse_git_diff_header(&root, &linenr, 1, line,
+                                                   len, size, &patch);
+                       if (len < 0)
+                               die(_("could not parse git header '%.*s'"), (int)len, line);
+                       strbuf_addstr(&buf, " ## ");
+                       if (patch.is_new > 0)
+                               strbuf_addf(&buf, "%s (new)", patch.new_name);
+                       else if (patch.is_delete > 0)
+                               strbuf_addf(&buf, "%s (deleted)", patch.old_name);
+                       else if (patch.is_rename)
+                               strbuf_addf(&buf, "%s => %s", patch.old_name, patch.new_name);
+                       else
+                               strbuf_addstr(&buf, patch.new_name);
+
+                       free(current_filename);
+                       if (patch.is_delete > 0)
+                               current_filename = xstrdup(patch.old_name);
+                       else
+                               current_filename = xstrdup(patch.new_name);
+
+                       if (patch.new_mode && patch.old_mode &&
+                           patch.old_mode != patch.new_mode)
+                               strbuf_addf(&buf, " (mode change %06o => %06o)",
+                                           patch.old_mode, patch.new_mode);
+
+                       strbuf_addstr(&buf, " ##");
                } else if (in_header) {
-                       if (starts_with(line.buf, "Author: ")) {
-                               strbuf_addbuf(&buf, &line);
+                       if (starts_with(line, "Author: ")) {
+                               strbuf_addstr(&buf, " ## Metadata ##\n");
+                               strbuf_addstr(&buf, line);
                                strbuf_addstr(&buf, "\n\n");
-                       } else if (starts_with(line.buf, "    ")) {
-                               strbuf_rtrim(&line);
-                               strbuf_addbuf(&buf, &line);
+                               strbuf_addstr(&buf, " ## Commit message ##\n");
+                       } else if (starts_with(line, "    ")) {
+                               p = line + len - 2;
+                               while (isspace(*p) && p >= line)
+                                       p--;
+                               strbuf_add(&buf, line, p - line + 1);
                                strbuf_addch(&buf, '\n');
                        }
                        continue;
-               } else if (starts_with(line.buf, "@@ "))
+               } else if (skip_prefix(line, "@@ ", &p)) {
+                       p = strstr(p, "@@");
                        strbuf_addstr(&buf, "@@");
-               else if (!line.buf[0] || starts_with(line.buf, "index "))
+                       if (current_filename && p[2])
+                               strbuf_addf(&buf, " %s:", current_filename);
+                       if (p)
+                               strbuf_addstr(&buf, p + 2);
+               } else if (!line[0])
                        /*
                         * A completely blank (not ' \n', which is context)
                         * line is not valid in a diff.  We skip it
                         * silently, because this neatly handles the blank
                         * separator line between commits in git-log
                         * output.
-                        *
-                        * We also want to ignore the diff's `index` lines
-                        * because they contain exact blob hashes in which
-                        * we are not interested.
                         */
                        continue;
-               else if (line.buf[0] == '>') {
+               else if (line[0] == '>') {
                        strbuf_addch(&buf, '+');
-                       strbuf_add(&buf, line.buf + 1, line.len - 1);
-               } else if (line.buf[0] == '<') {
+                       strbuf_addstr(&buf, line + 1);
+               } else if (line[0] == '<') {
                        strbuf_addch(&buf, '-');
-                       strbuf_add(&buf, line.buf + 1, line.len - 1);
-               } else if (line.buf[0] == '#') {
+                       strbuf_addstr(&buf, line + 1);
+               } else if (line[0] == '#') {
                        strbuf_addch(&buf, ' ');
-                       strbuf_add(&buf, line.buf + 1, line.len - 1);
+                       strbuf_addstr(&buf, line + 1);
                } else {
                        strbuf_addch(&buf, ' ');
-                       strbuf_addbuf(&buf, &line);
+                       strbuf_addstr(&buf, line);
                }
 
                strbuf_addch(&buf, '\n');
                util->diffsize++;
        }
-       fclose(in);
-       strbuf_release(&line);
+       strbuf_release(&contents);
 
        if (util)
                string_list_append(list, buf.buf)->util = util;
        strbuf_release(&buf);
+       free(current_filename);
 
        if (finish_command(&cp))
                return -1;
@@ -148,7 +198,7 @@ static int read_patches(const char *range, struct string_list *list)
 }
 
 static int patch_util_cmp(const void *dummy, const struct patch_util *a,
-                    const struct patch_util *b, const char *keydata)
+                         const struct patch_util *b, const char *keydata)
 {
        return strcmp(a->diff, keydata ? keydata : b->diff);
 }
@@ -354,8 +404,9 @@ static void output_pair_header(struct diff_options *diffopt,
        fwrite(buf->buf, buf->len, 1, diffopt->file);
 }
 
-static struct userdiff_driver no_func_name = {
-       .funcname = { "$^", 0 }
+static struct userdiff_driver section_headers = {
+       .funcname = { "^ ## (.*) ##$\n"
+                     "^.?@@ (.*)$", REG_EXTENDED }
 };
 
 static struct diff_filespec *get_filespec(const char *name, const char *p)
@@ -367,13 +418,13 @@ static struct diff_filespec *get_filespec(const char *name, const char *p)
        spec->size = strlen(p);
        spec->should_munmap = 0;
        spec->is_stdin = 1;
-       spec->driver = &no_func_name;
+       spec->driver = &section_headers;
 
        return spec;
 }
 
 static void patch_diff(const char *a, const char *b,
-                             struct diff_options *diffopt)
+                      struct diff_options *diffopt)
 {
        diff_queue(&diff_queued_diff,
                   get_filespec("a", a), get_filespec("b", b));
@@ -469,6 +520,7 @@ int show_range_diff(const char *range1, const char *range2,
                        opts.output_format = DIFF_FORMAT_PATCH;
                opts.flags.suppress_diff_headers = 1;
                opts.flags.dual_color_diffed_diffs = dual_color;
+               opts.flags.suppress_hunk_header_line_count = 1;
                opts.output_prefix = output_prefix_cb;
                strbuf_addstr(&indent, "    ");
                opts.output_prefix_data = &indent;
index 791f0648a6edc4ecc4629865393703d9b1e4a227..f27cfc8c3e358fa27d7aec78c0b3c44c816402b3 100644 (file)
@@ -22,6 +22,7 @@
 #include "commit-reach.h"
 #include "worktree.h"
 #include "hashmap.h"
+#include "argv-array.h"
 
 static struct ref_msg {
        const char *gone;
@@ -1863,21 +1864,62 @@ static int filter_pattern_match(struct ref_filter *filter, const char *refname)
        return match_pattern(filter, refname);
 }
 
-/*
- * Find the longest prefix of pattern we can pass to
- * `for_each_fullref_in()`, namely the part of pattern preceding the
- * first glob character. (Note that `for_each_fullref_in()` is
- * perfectly happy working with a prefix that doesn't end at a
- * pathname component boundary.)
- */
-static void find_longest_prefix(struct strbuf *out, const char *pattern)
+static int qsort_strcmp(const void *va, const void *vb)
+{
+       const char *a = *(const char **)va;
+       const char *b = *(const char **)vb;
+
+       return strcmp(a, b);
+}
+
+static void find_longest_prefixes_1(struct string_list *out,
+                                 struct strbuf *prefix,
+                                 const char **patterns, size_t nr)
 {
-       const char *p;
+       size_t i;
+
+       for (i = 0; i < nr; i++) {
+               char c = patterns[i][prefix->len];
+               if (!c || is_glob_special(c)) {
+                       string_list_append(out, prefix->buf);
+                       return;
+               }
+       }
+
+       i = 0;
+       while (i < nr) {
+               size_t end;
+
+               /*
+               * Set "end" to the index of the element _after_ the last one
+               * in our group.
+               */
+               for (end = i + 1; end < nr; end++) {
+                       if (patterns[i][prefix->len] != patterns[end][prefix->len])
+                               break;
+               }
+
+               strbuf_addch(prefix, patterns[i][prefix->len]);
+               find_longest_prefixes_1(out, prefix, patterns + i, end - i);
+               strbuf_setlen(prefix, prefix->len - 1);
+
+               i = end;
+       }
+}
+
+static void find_longest_prefixes(struct string_list *out,
+                                 const char **patterns)
+{
+       struct argv_array sorted = ARGV_ARRAY_INIT;
+       struct strbuf prefix = STRBUF_INIT;
 
-       for (p = pattern; *p && !is_glob_special(*p); p++)
-               ;
+       argv_array_pushv(&sorted, patterns);
+       QSORT(sorted.argv, sorted.argc, qsort_strcmp);
 
-       strbuf_add(out, pattern, p - pattern);
+       find_longest_prefixes_1(out, &prefix, sorted.argv, sorted.argc);
+
+       argv_array_clear(&sorted);
+       strbuf_release(&prefix);
 }
 
 /*
@@ -1890,7 +1932,8 @@ static int for_each_fullref_in_pattern(struct ref_filter *filter,
                                       void *cb_data,
                                       int broken)
 {
-       struct strbuf prefix = STRBUF_INIT;
+       struct string_list prefixes = STRING_LIST_INIT_DUP;
+       struct string_list_item *prefix;
        int ret;
 
        if (!filter->match_as_path) {
@@ -1916,21 +1959,15 @@ static int for_each_fullref_in_pattern(struct ref_filter *filter,
                return for_each_fullref_in("", cb, cb_data, broken);
        }
 
-       if (filter->name_patterns[1]) {
-               /*
-                * multiple patterns; in theory this could still work as long
-                * as the patterns are disjoint. We'd just make multiple calls
-                * to for_each_ref(). But if they're not disjoint, we'd end up
-                * reporting the same ref multiple times. So let's punt on that
-                * for now.
-                */
-               return for_each_fullref_in("", cb, cb_data, broken);
-       }
+       find_longest_prefixes(&prefixes, filter->name_patterns);
 
-       find_longest_prefix(&prefix, filter->name_patterns[0]);
+       for_each_string_list_item(prefix, &prefixes) {
+               ret = for_each_fullref_in(prefix->string, cb, cb_data, broken);
+               if (ret)
+                       break;
+       }
 
-       ret = for_each_fullref_in(prefix.buf, cb, cb_data, broken);
-       strbuf_release(&prefix);
+       string_list_clear(&prefixes, 0);
        return ret;
 }
 
@@ -2105,7 +2142,9 @@ static void free_array_item(struct ref_array_item *item)
 {
        free((char *)item->symref);
        if (item->value) {
-               free((char *)item->value->s);
+               int i;
+               for (i = 0; i < used_atom_cnt; i++)
+                       free((char *)item->value[i].s);
                free(item->value);
        }
        free(item);
@@ -2116,14 +2155,16 @@ void ref_array_clear(struct ref_array *array)
 {
        int i;
 
-       for (i = 0; i < used_atom_cnt; i++)
-               free((char *)used_atom[i].name);
-       FREE_AND_NULL(used_atom);
-       used_atom_cnt = 0;
        for (i = 0; i < array->nr; i++)
                free_array_item(array->items[i]);
        FREE_AND_NULL(array->items);
        array->nr = array->alloc = 0;
+
+       for (i = 0; i < used_atom_cnt; i++)
+               free((char *)used_atom[i].name);
+       FREE_AND_NULL(used_atom);
+       used_atom_cnt = 0;
+
        if (ref_to_worktree_map.worktrees) {
                hashmap_free(&(ref_to_worktree_map.map), 1);
                free_worktrees(ref_to_worktree_map.worktrees);
index 621feb9df716400f32d016e1d36fc368b6a884fb..07412297f0248aae886eeb77c3a1cab13c93039c 100644 (file)
@@ -1554,6 +1554,32 @@ void add_index_objects_to_pending(struct rev_info *revs, unsigned int flags)
        free_worktrees(worktrees);
 }
 
+struct add_alternate_refs_data {
+       struct rev_info *revs;
+       unsigned int flags;
+};
+
+static void add_one_alternate_ref(const struct object_id *oid,
+                                 void *vdata)
+{
+       const char *name = ".alternate";
+       struct add_alternate_refs_data *data = vdata;
+       struct object *obj;
+
+       obj = get_reference(data->revs, name, oid, data->flags);
+       add_rev_cmdline(data->revs, obj, name, REV_CMD_REV, data->flags);
+       add_pending_object(data->revs, obj, name);
+}
+
+static void add_alternate_refs_to_pending(struct rev_info *revs,
+                                         unsigned int flags)
+{
+       struct add_alternate_refs_data data;
+       data.revs = revs;
+       data.flags = flags;
+       for_each_alternate_ref(add_one_alternate_ref, &data);
+}
+
 static int add_parents_only(struct rev_info *revs, const char *arg_, int flags,
                            int exclude_parent)
 {
@@ -1956,6 +1982,7 @@ static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg
            !strcmp(arg, "--no-walk") || !strcmp(arg, "--do-walk") ||
            !strcmp(arg, "--bisect") || starts_with(arg, "--glob=") ||
            !strcmp(arg, "--indexed-objects") ||
+           !strcmp(arg, "--alternate-refs") ||
            starts_with(arg, "--exclude=") ||
            starts_with(arg, "--branches=") || starts_with(arg, "--tags=") ||
            starts_with(arg, "--remotes=") || starts_with(arg, "--no-walk="))
@@ -2442,6 +2469,8 @@ static int handle_revision_pseudo_opt(const char *submodule,
                add_reflogs_to_pending(revs, *flags);
        } else if (!strcmp(arg, "--indexed-objects")) {
                add_index_objects_to_pending(revs, *flags);
+       } else if (!strcmp(arg, "--alternate-refs")) {
+               add_alternate_refs_to_pending(revs, *flags);
        } else if (!strcmp(arg, "--not")) {
                *flags ^= UNINTERESTING | BOTTOM;
        } else if (!strcmp(arg, "--no-walk")) {
index cf262701e8e7df668b2c659bfb4806f0c7e90a9e..34ebf8ed94ad7d8df6773337d31ff7d9c2c84c4a 100644 (file)
@@ -2079,6 +2079,18 @@ const char *todo_item_get_arg(struct todo_list *todo_list,
        return todo_list->buf.buf + item->arg_offset;
 }
 
+static int is_command(enum todo_command command, const char **bol)
+{
+       const char *str = todo_command_info[command].str;
+       const char nick = todo_command_info[command].c;
+       const char *p = *bol + 1;
+
+       return skip_prefix(*bol, str, bol) ||
+               ((nick && **bol == nick) &&
+                (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || !*p) &&
+                (*bol = p));
+}
+
 static int parse_insn_line(struct repository *r, struct todo_item *item,
                           const char *buf, const char *bol, char *eol)
 {
@@ -2100,12 +2112,7 @@ static int parse_insn_line(struct repository *r, struct todo_item *item,
        }
 
        for (i = 0; i < TODO_COMMENT; i++)
-               if (skip_prefix(bol, todo_command_info[i].str, &bol)) {
-                       item->command = i;
-                       break;
-               } else if ((bol + 1 == eol || bol[1] == ' ') &&
-                          *bol == todo_command_info[i].c) {
-                       bol++;
+               if (is_command(i, &bol)) {
                        item->command = i;
                        break;
                }
@@ -2173,34 +2180,26 @@ static int parse_insn_line(struct repository *r, struct todo_item *item,
 
 int sequencer_get_last_command(struct repository *r, enum replay_action *action)
 {
-       struct todo_item item;
-       char *eol;
-       const char *todo_file;
+       const char *todo_file, *bol;
        struct strbuf buf = STRBUF_INIT;
-       int ret = -1;
+       int ret = 0;
 
        todo_file = git_path_todo_file();
        if (strbuf_read_file(&buf, todo_file, 0) < 0) {
-               if (errno == ENOENT)
+               if (errno == ENOENT || errno == ENOTDIR)
                        return -1;
                else
                        return error_errno("unable to open '%s'", todo_file);
        }
-       eol = strchrnul(buf.buf, '\n');
-       if (buf.buf != eol && eol[-1] == '\r')
-               eol--; /* strip Carriage Return */
-       if (parse_insn_line(r, &item, buf.buf, buf.buf, eol))
-               goto fail;
-       if (item.command == TODO_PICK)
+       bol = buf.buf + strspn(buf.buf, " \t\r\n");
+       if (is_command(TODO_PICK, &bol) && (*bol == ' ' || *bol == '\t'))
                *action = REPLAY_PICK;
-       else if (item.command == TODO_REVERT)
+       else if (is_command(TODO_REVERT, &bol) &&
+                (*bol == ' ' || *bol == '\t'))
                *action = REPLAY_REVERT;
        else
-               goto fail;
-
-       ret = 0;
+               ret = -1;
 
- fail:
        strbuf_release(&buf);
 
        return ret;
@@ -2655,15 +2654,41 @@ static int walk_revs_populate_todo(struct todo_list *todo_list,
        return 0;
 }
 
-static int create_seq_dir(void)
+static int create_seq_dir(struct repository *r)
 {
-       if (file_exists(git_path_seq_dir())) {
-               error(_("a cherry-pick or revert is already in progress"));
-               advise(_("try \"git cherry-pick (--continue | --quit | --abort)\""));
+       enum replay_action action;
+       const char *in_progress_error = NULL;
+       const char *in_progress_advice = NULL;
+       unsigned int advise_skip = file_exists(git_path_revert_head(r)) ||
+                               file_exists(git_path_cherry_pick_head(r));
+
+       if (!sequencer_get_last_command(r, &action)) {
+               switch (action) {
+               case REPLAY_REVERT:
+                       in_progress_error = _("revert is already in progress");
+                       in_progress_advice =
+                       _("try \"git revert (--continue | %s--abort | --quit)\"");
+                       break;
+               case REPLAY_PICK:
+                       in_progress_error = _("cherry-pick is already in progress");
+                       in_progress_advice =
+                       _("try \"git cherry-pick (--continue | %s--abort | --quit)\"");
+                       break;
+               default:
+                       BUG("unexpected action in create_seq_dir");
+               }
+       }
+       if (in_progress_error) {
+               error("%s", in_progress_error);
+               if (advice_sequencer_in_use)
+                       advise(in_progress_advice,
+                               advise_skip ? "--skip | " : "");
                return -1;
-       } else if (mkdir(git_path_seq_dir(), 0777) < 0)
+       }
+       if (mkdir(git_path_seq_dir(), 0777) < 0)
                return error_errno(_("could not create sequencer directory '%s'"),
                                   git_path_seq_dir());
+
        return 0;
 }
 
@@ -2714,15 +2739,20 @@ static int rollback_is_safe(void)
        return oideq(&actual_head, &expected_head);
 }
 
-static int reset_for_rollback(const struct object_id *oid)
+static int reset_merge(const struct object_id *oid)
 {
-       const char *argv[4];    /* reset --merge <arg> + NULL */
+       int ret;
+       struct argv_array argv = ARGV_ARRAY_INIT;
 
-       argv[0] = "reset";
-       argv[1] = "--merge";
-       argv[2] = oid_to_hex(oid);
-       argv[3] = NULL;
-       return run_command_v_opt(argv, RUN_GIT_CMD);
+       argv_array_pushl(&argv, "reset", "--merge", NULL);
+
+       if (!is_null_oid(oid))
+               argv_array_push(&argv, oid_to_hex(oid));
+
+       ret = run_command_v_opt(argv.argv, RUN_GIT_CMD);
+       argv_array_clear(&argv);
+
+       return ret;
 }
 
 static int rollback_single_pick(struct repository *r)
@@ -2736,7 +2766,16 @@ static int rollback_single_pick(struct repository *r)
                return error(_("cannot resolve HEAD"));
        if (is_null_oid(&head_oid))
                return error(_("cannot abort from a branch yet to be born"));
-       return reset_for_rollback(&head_oid);
+       return reset_merge(&head_oid);
+}
+
+static int skip_single_pick(void)
+{
+       struct object_id head;
+
+       if (read_ref_full("HEAD", 0, &head, NULL))
+               return error(_("cannot resolve HEAD"));
+       return reset_merge(&head);
 }
 
 int sequencer_rollback(struct repository *r, struct replay_opts *opts)
@@ -2779,7 +2818,7 @@ int sequencer_rollback(struct repository *r, struct replay_opts *opts)
                warning(_("You seem to have moved HEAD. "
                          "Not rewinding, check your HEAD!"));
        } else
-       if (reset_for_rollback(&oid))
+       if (reset_merge(&oid))
                goto fail;
        strbuf_release(&buf);
        return sequencer_remove_state(opts);
@@ -2788,6 +2827,70 @@ int sequencer_rollback(struct repository *r, struct replay_opts *opts)
        return -1;
 }
 
+int sequencer_skip(struct repository *r, struct replay_opts *opts)
+{
+       enum replay_action action = -1;
+       sequencer_get_last_command(r, &action);
+
+       /*
+        * Check whether the subcommand requested to skip the commit is actually
+        * in progress and that it's safe to skip the commit.
+        *
+        * opts->action tells us which subcommand requested to skip the commit.
+        * If the corresponding .git/<ACTION>_HEAD exists, we know that the
+        * action is in progress and we can skip the commit.
+        *
+        * Otherwise we check that the last instruction was related to the
+        * particular subcommand we're trying to execute and barf if that's not
+        * the case.
+        *
+        * Finally we check that the rollback is "safe", i.e., has the HEAD
+        * moved? In this case, it doesn't make sense to "reset the merge" and
+        * "skip the commit" as the user already handled this by committing. But
+        * we'd not want to barf here, instead give advice on how to proceed. We
+        * only need to check that when .git/<ACTION>_HEAD doesn't exist because
+        * it gets removed when the user commits, so if it still exists we're
+        * sure the user can't have committed before.
+        */
+       switch (opts->action) {
+       case REPLAY_REVERT:
+               if (!file_exists(git_path_revert_head(r))) {
+                       if (action != REPLAY_REVERT)
+                               return error(_("no revert in progress"));
+                       if (!rollback_is_safe())
+                               goto give_advice;
+               }
+               break;
+       case REPLAY_PICK:
+               if (!file_exists(git_path_cherry_pick_head(r))) {
+                       if (action != REPLAY_PICK)
+                               return error(_("no cherry-pick in progress"));
+                       if (!rollback_is_safe())
+                               goto give_advice;
+               }
+               break;
+       default:
+               BUG("unexpected action in sequencer_skip");
+       }
+
+       if (skip_single_pick())
+               return error(_("failed to skip the commit"));
+       if (!is_directory(git_path_seq_dir()))
+               return 0;
+
+       return sequencer_continue(r, opts);
+
+give_advice:
+       error(_("there is nothing to skip"));
+
+       if (advice_resolve_conflict) {
+               advise(_("have you committed already?\n"
+                        "try \"git %s --continue\""),
+                        action == REPLAY_REVERT ? "revert" : "cherry-pick");
+       }
+       return -1;
+}
+
 static int save_todo(struct todo_list *todo_list, struct replay_opts *opts)
 {
        struct lock_file todo_lock = LOCK_INIT;
@@ -3199,7 +3302,7 @@ static int do_reset(struct repository *r,
                return error_resolve_conflict(_(action_name(opts)));
        }
 
-       if (!fill_tree_descriptor(&desc, &oid)) {
+       if (!fill_tree_descriptor(r, &desc, &oid)) {
                error(_("failed to find tree of %s"), oid_to_hex(&oid));
                rollback_lock_file(&lock);
                free((void *)desc.buffer);
@@ -3738,7 +3841,7 @@ static int pick_commits(struct repository *r,
                        unlink(rebase_path_author_script());
                        unlink(rebase_path_stopped_sha());
                        unlink(rebase_path_amend());
-                       unlink(git_path_merge_head(the_repository));
+                       unlink(git_path_merge_head(r));
                        delete_ref(NULL, "REBASE_HEAD", NULL, REF_NO_DEREF);
 
                        if (item->command == TODO_BREAK) {
@@ -4123,7 +4226,7 @@ static int commit_staged_changes(struct repository *r,
                           opts, flags))
                return error(_("could not commit staged changes."));
        unlink(rebase_path_amend());
-       unlink(git_path_merge_head(the_repository));
+       unlink(git_path_merge_head(r));
        if (final_fixup) {
                unlink(rebase_path_fixup_msg());
                unlink(rebase_path_squash_msg());
@@ -4258,7 +4361,7 @@ int sequencer_pick_revisions(struct repository *r,
         */
 
        if (walk_revs_populate_todo(&todo_list, opts) ||
-                       create_seq_dir() < 0)
+                       create_seq_dir(r) < 0)
                return -1;
        if (get_oid("HEAD", &oid) && (opts->action == REPLAY_REVERT))
                return error(_("can't revert as initial commit"));
index 3d0b68c34ce1fed23899415660127c999fa999c6..6704acbb9c93a55cb7ec69d2e045d67850bc4049 100644 (file)
@@ -129,6 +129,7 @@ int sequencer_pick_revisions(struct repository *repo,
                             struct replay_opts *opts);
 int sequencer_continue(struct repository *repo, struct replay_opts *opts);
 int sequencer_rollback(struct repository *repo, struct replay_opts *opts);
+int sequencer_skip(struct repository *repo, struct replay_opts *opts);
 int sequencer_remove_state(struct replay_opts *opts);
 
 #define TODO_LIST_KEEP_EMPTY (1U << 0)
index 888b6024d5de050753765e37b65ea0521d91f9fd..84fd02f107602411f3ceb460e4fe5b41c212bdec 100644 (file)
@@ -743,6 +743,103 @@ char *compute_alternate_path(const char *path, struct strbuf *err)
        return ref_git;
 }
 
+static void fill_alternate_refs_command(struct child_process *cmd,
+                                       const char *repo_path)
+{
+       const char *value;
+
+       if (!git_config_get_value("core.alternateRefsCommand", &value)) {
+               cmd->use_shell = 1;
+
+               argv_array_push(&cmd->args, value);
+               argv_array_push(&cmd->args, repo_path);
+       } else {
+               cmd->git_cmd = 1;
+
+               argv_array_pushf(&cmd->args, "--git-dir=%s", repo_path);
+               argv_array_push(&cmd->args, "for-each-ref");
+               argv_array_push(&cmd->args, "--format=%(objectname)");
+
+               if (!git_config_get_value("core.alternateRefsPrefixes", &value)) {
+                       argv_array_push(&cmd->args, "--");
+                       argv_array_split(&cmd->args, value);
+               }
+       }
+
+       cmd->env = local_repo_env;
+       cmd->out = -1;
+}
+
+static void read_alternate_refs(const char *path,
+                               alternate_ref_fn *cb,
+                               void *data)
+{
+       struct child_process cmd = CHILD_PROCESS_INIT;
+       struct strbuf line = STRBUF_INIT;
+       FILE *fh;
+
+       fill_alternate_refs_command(&cmd, path);
+
+       if (start_command(&cmd))
+               return;
+
+       fh = xfdopen(cmd.out, "r");
+       while (strbuf_getline_lf(&line, fh) != EOF) {
+               struct object_id oid;
+               const char *p;
+
+               if (parse_oid_hex(line.buf, &oid, &p) || *p) {
+                       warning(_("invalid line while parsing alternate refs: %s"),
+                               line.buf);
+                       break;
+               }
+
+               cb(&oid, data);
+       }
+
+       fclose(fh);
+       finish_command(&cmd);
+}
+
+struct alternate_refs_data {
+       alternate_ref_fn *fn;
+       void *data;
+};
+
+static int refs_from_alternate_cb(struct object_directory *e,
+                                 void *data)
+{
+       struct strbuf path = STRBUF_INIT;
+       size_t base_len;
+       struct alternate_refs_data *cb = data;
+
+       if (!strbuf_realpath(&path, e->path, 0))
+               goto out;
+       if (!strbuf_strip_suffix(&path, "/objects"))
+               goto out;
+       base_len = path.len;
+
+       /* Is this a git repository with refs? */
+       strbuf_addstr(&path, "/refs");
+       if (!is_directory(path.buf))
+               goto out;
+       strbuf_setlen(&path, base_len);
+
+       read_alternate_refs(path.buf, cb->fn, cb->data);
+
+out:
+       strbuf_release(&path);
+       return 0;
+}
+
+void for_each_alternate_ref(alternate_ref_fn fn, void *data)
+{
+       struct alternate_refs_data cb;
+       cb.fn = fn;
+       cb.data = data;
+       foreach_alt_odb(refs_from_alternate_cb, &cb);
+}
+
 int foreach_alt_odb(alt_odb_fn fn, void *cb)
 {
        struct object_directory *ent;
@@ -1505,7 +1602,8 @@ void *read_object_file_extended(struct repository *r,
        return NULL;
 }
 
-void *read_object_with_reference(const struct object_id *oid,
+void *read_object_with_reference(struct repository *r,
+                                const struct object_id *oid,
                                 const char *required_type_name,
                                 unsigned long *size,
                                 struct object_id *actual_oid_return)
@@ -1521,7 +1619,7 @@ void *read_object_with_reference(const struct object_id *oid,
                int ref_length = -1;
                const char *ref_type = NULL;
 
-               buffer = read_object_file(&actual_oid, &type, &isize);
+               buffer = repo_read_object_file(r, &actual_oid, &type, &isize);
                if (!buffer)
                        return NULL;
                if (type == required_type) {
index 49855ad24f1bc81aa8924fc927e0e32c7a62bf0a..2989e27b717abdabd6623299d7737e9a51641990 100644 (file)
@@ -478,7 +478,7 @@ static enum get_oid_result get_short_oid(struct repository *r,
         * or migrated from loose to packed.
         */
        if (status == MISSING_OBJECT) {
-               reprepare_packed_git(the_repository);
+               reprepare_packed_git(r);
                find_short_object_filename(&ds);
                find_short_packed_object(&ds);
                status = finish_object_disambiguation(&ds, oid);
@@ -1389,9 +1389,7 @@ int repo_get_oid_mb(struct repository *r,
        two = lookup_commit_reference_gently(r, &oid_tmp, 0);
        if (!two)
                return -1;
-       if (r != the_repository)
-               BUG("sorry get_merge_bases() can't take struct repository yet");
-       mbs = get_merge_bases(one, two);
+       mbs = repo_get_merge_bases(r, one, two);
        if (!mbs || mbs->next)
                st = -1;
        else {
@@ -1677,7 +1675,8 @@ int repo_get_oid_blob(struct repository *r,
 }
 
 /* Must be called only when object_name:filename doesn't exist. */
-static void diagnose_invalid_oid_path(const char *prefix,
+static void diagnose_invalid_oid_path(struct repository *r,
+                                     const char *prefix,
                                      const char *filename,
                                      const struct object_id *tree_oid,
                                      const char *object_name,
@@ -1695,7 +1694,7 @@ static void diagnose_invalid_oid_path(const char *prefix,
        if (is_missing_file_error(errno)) {
                char *fullname = xstrfmt("%s%s", prefix, filename);
 
-               if (!get_tree_entry(tree_oid, fullname, &oid, &mode)) {
+               if (!get_tree_entry(r, tree_oid, fullname, &oid, &mode)) {
                        die("Path '%s' exists, but not '%s'.\n"
                            "Did you mean '%.*s:%s' aka '%.*s:./%s'?",
                            fullname,
@@ -1889,23 +1888,15 @@ static enum get_oid_result get_oid_with_context_1(struct repository *repo,
                        new_filename = resolve_relative_path(repo, filename);
                        if (new_filename)
                                filename = new_filename;
-                       /*
-                        * NEEDSWORK: Eventually get_tree_entry*() should
-                        * learn to take struct repository directly and we
-                        * would not need to inject submodule odb to the
-                        * in-core odb.
-                        */
-                       if (repo != the_repository)
-                               add_to_alternates_memory(repo->objects->odb->path);
                        if (flags & GET_OID_FOLLOW_SYMLINKS) {
-                               ret = get_tree_entry_follow_symlinks(&tree_oid,
+                               ret = get_tree_entry_follow_symlinks(repo, &tree_oid,
                                        filename, oid, &oc->symlink_path,
                                        &oc->mode);
                        } else {
-                               ret = get_tree_entry(&tree_oid, filename, oid,
+                               ret = get_tree_entry(repo, &tree_oid, filename, oid,
                                                     &oc->mode);
                                if (ret && only_to_die) {
-                                       diagnose_invalid_oid_path(prefix,
+                                       diagnose_invalid_oid_path(repo, prefix,
                                                                   filename,
                                                                   &tree_oid,
                                                                   name, len);
index ce45297940d417e3454b08d3f0c29f5cc6d93658..5fa2b15d3705439b0088ac0fa0800c81ffad7a41 100644 (file)
--- a/shallow.c
+++ b/shallow.c
@@ -248,7 +248,8 @@ static void check_shallow_file_for_update(struct repository *r)
        if (r->parsed_objects->is_shallow == -1)
                BUG("shallow must be initialized by now");
 
-       if (!stat_validity_check(r->parsed_objects->shallow_stat, git_path_shallow(the_repository)))
+       if (!stat_validity_check(r->parsed_objects->shallow_stat,
+                                git_path_shallow(r)))
                die("shallow file has changed since we read it");
 }
 
index 9747971d58e1a5efaa85a8ce00772c82d7c5aba7..60d5b77bccd952ffe8128a62827d7a15e728f614 100644 (file)
--- a/t/README
+++ b/t/README
@@ -334,7 +334,7 @@ that cannot be easily covered by a few specific test cases. These
 could be enabled by running the test suite with correct GIT_TEST_
 environment set.
 
-GIT_TEST_FAIL_PREREQS<non-empty?> fails all prerequisites. This is
+GIT_TEST_FAIL_PREREQS=<boolean> fails all prerequisites. This is
 useful for discovering issues with the tests where say a later test
 implicitly depends on an optional earlier test.
 
@@ -343,11 +343,11 @@ whether this mode is active, and e.g. skip some tests that are hard to
 refactor to deal with it. The "SYMLINKS" prerequisite is currently
 excluded as so much relies on it, but this might change in the future.
 
-GIT_TEST_GETTEXT_POISON=<non-empty?> turns all strings marked for
-translation into gibberish if non-empty (think "test -n"). Used for
-spotting those tests that need to be marked with a C_LOCALE_OUTPUT
-prerequisite when adding more strings for translation. See "Testing
-marked strings" in po/README for details.
+GIT_TEST_GETTEXT_POISON=<boolean> turns all strings marked for
+translation into gibberish if true. Used for spotting those tests that
+need to be marked with a C_LOCALE_OUTPUT prerequisite when adding more
+strings for translation. See "Testing marked strings" in po/README for
+details.
 
 GIT_TEST_SPLIT_INDEX=<boolean> forces split-index mode on the whole
 test suite. Accept any boolean values that are accepted by git-config.
index 23d2b172fe708f711a15613e906637cd948324ef..aaf17b0ddf9e8ddd1270f613f8c71841c6644eb9 100644 (file)
@@ -173,14 +173,7 @@ int cmd__hashmap(int argc, const char **argv)
                        p2 = strtok(NULL, DELIM);
                }
 
-               if (!strcmp("hash", cmd) && p1) {
-
-                       /* print results of different hash functions */
-                       printf("%u %u %u %u\n",
-                              strhash(p1), memhash(p1, strlen(p1)),
-                              strihash(p1), memihash(p1, strlen(p1)));
-
-               } else if (!strcmp("add", cmd) && p1 && p2) {
+               if (!strcmp("add", cmd) && p1 && p2) {
 
                        /* create entry with key = p1, value = p2 */
                        entry = alloc_test_entry(hash, p1, p2);
index 96857f26ac8540cf22e74aed72bbd30bc8147f00..b9fd427571e6265dbc3be8b6e93acbb6eba61c56 100644 (file)
@@ -20,7 +20,7 @@ int cmd__match_trees(int ac, const char **av)
        if (!two)
                die("not a tree-ish %s", av[2]);
 
-       shift_tree(&one->object.oid, &two->object.oid, &shifted, -1);
+       shift_tree(the_repository, &one->object.oid, &two->object.oid, &shifted, -1);
        printf("shifted: %s\n", oid_to_hex(&shifted));
 
        exit(0);
diff --git a/t/helper/test-oidmap.c b/t/helper/test-oidmap.c
new file mode 100644 (file)
index 0000000..0acf999
--- /dev/null
@@ -0,0 +1,112 @@
+#include "test-tool.h"
+#include "cache.h"
+#include "oidmap.h"
+#include "strbuf.h"
+
+/* key is an oid and value is a name (could be a refname for example) */
+struct test_entry {
+       struct oidmap_entry entry;
+       char name[FLEX_ARRAY];
+};
+
+#define DELIM " \t\r\n"
+
+/*
+ * Read stdin line by line and print result of commands to stdout:
+ *
+ * hash oidkey -> sha1hash(oidkey)
+ * put oidkey namevalue -> NULL / old namevalue
+ * get oidkey -> NULL / namevalue
+ * remove oidkey -> NULL / old namevalue
+ * iterate -> oidkey1 namevalue1\noidkey2 namevalue2\n...
+ *
+ */
+int cmd__oidmap(int argc, const char **argv)
+{
+       struct strbuf line = STRBUF_INIT;
+       struct oidmap map = OIDMAP_INIT;
+
+       setup_git_directory();
+
+       /* init oidmap */
+       oidmap_init(&map, 0);
+
+       /* process commands from stdin */
+       while (strbuf_getline(&line, stdin) != EOF) {
+               char *cmd, *p1 = NULL, *p2 = NULL;
+               struct test_entry *entry;
+               struct object_id oid;
+
+               /* break line into command and up to two parameters */
+               cmd = strtok(line.buf, DELIM);
+               /* ignore empty lines */
+               if (!cmd || *cmd == '#')
+                       continue;
+
+               p1 = strtok(NULL, DELIM);
+               if (p1)
+                       p2 = strtok(NULL, DELIM);
+
+               if (!strcmp("put", cmd) && p1 && p2) {
+
+                       if (get_oid(p1, &oid)) {
+                               printf("Unknown oid: %s\n", p1);
+                               continue;
+                       }
+
+                       /* create entry with oid_key = p1, name_value = p2 */
+                       FLEX_ALLOC_STR(entry, name, p2);
+                       oidcpy(&entry->entry.oid, &oid);
+
+                       /* add / replace entry */
+                       entry = oidmap_put(&map, entry);
+
+                       /* print and free replaced entry, if any */
+                       puts(entry ? entry->name : "NULL");
+                       free(entry);
+
+               } else if (!strcmp("get", cmd) && p1) {
+
+                       if (get_oid(p1, &oid)) {
+                               printf("Unknown oid: %s\n", p1);
+                               continue;
+                       }
+
+                       /* lookup entry in oidmap */
+                       entry = oidmap_get(&map, &oid);
+
+                       /* print result */
+                       puts(entry ? entry->name : "NULL");
+
+               } else if (!strcmp("remove", cmd) && p1) {
+
+                       if (get_oid(p1, &oid)) {
+                               printf("Unknown oid: %s\n", p1);
+                               continue;
+                       }
+
+                       /* remove entry from oidmap */
+                       entry = oidmap_remove(&map, &oid);
+
+                       /* print result and free entry*/
+                       puts(entry ? entry->name : "NULL");
+                       free(entry);
+
+               } else if (!strcmp("iterate", cmd)) {
+
+                       struct oidmap_iter iter;
+                       oidmap_iter_init(&map, &iter);
+                       while ((entry = oidmap_iter_next(&iter)))
+                               printf("%s %s\n", oid_to_hex(&entry->entry.oid), entry->name);
+
+               } else {
+
+                       printf("Unknown command %s\n", cmd);
+
+               }
+       }
+
+       strbuf_release(&line);
+       oidmap_free(&map, 1);
+       return 0;
+}
index 087a8c0cc9da64d7bc276c3870b2d0faba4c2627..1eac25233f7ce62ecb00b2d1e3d06d1423c3581f 100644 (file)
@@ -35,6 +35,7 @@ static struct test_cmd cmds[] = {
        { "match-trees", cmd__match_trees },
        { "mergesort", cmd__mergesort },
        { "mktemp", cmd__mktemp },
+       { "oidmap", cmd__oidmap },
        { "online-cpus", cmd__online_cpus },
        { "parse-options", cmd__parse_options },
        { "path-utils", cmd__path_utils },
index 7e703f3038ae433c7d8b4ef5af51d9781d6bfffb..c7a46dc320e93b3bb5aef5f7fce3697c4558f814 100644 (file)
@@ -25,6 +25,7 @@ int cmd__lazy_init_name_hash(int argc, const char **argv);
 int cmd__match_trees(int argc, const char **argv);
 int cmd__mergesort(int argc, const char **argv);
 int cmd__mktemp(int argc, const char **argv);
+int cmd__oidmap(int argc, const char **argv);
 int cmd__online_cpus(int argc, const char **argv);
 int cmd__parse_options(int argc, const char **argv);
 int cmd__path_utils(int argc, const char **argv);
index 7b3407134e1a776de7a030bce98f0248fe6cd42f..fb8f8870801eb56e1ada818fd1b2ccdf10a4600d 100644 (file)
@@ -15,8 +15,7 @@
 #
 #      test_done
 
-test_tristate GIT_TEST_GIT_DAEMON
-if test "$GIT_TEST_GIT_DAEMON" = false
+if ! git env--helper --type=bool --default=true --exit-code GIT_TEST_GIT_DAEMON
 then
        skip_all="git-daemon testing disabled (unset GIT_TEST_GIT_DAEMON to enable)"
        test_done
@@ -24,7 +23,7 @@ fi
 
 if test_have_prereq !PIPE
 then
-       test_skip_or_die $GIT_TEST_GIT_DAEMON "file system does not support FIFOs"
+       test_skip_or_die GIT_TEST_GIT_DAEMON "file system does not support FIFOs"
 fi
 
 test_set_port LIB_GIT_DAEMON_PORT
@@ -73,7 +72,7 @@ start_git_daemon() {
                kill "$GIT_DAEMON_PID"
                wait "$GIT_DAEMON_PID"
                unset GIT_DAEMON_PID
-               test_skip_or_die $GIT_TEST_GIT_DAEMON \
+               test_skip_or_die GIT_TEST_GIT_DAEMON \
                        "git daemon failed to start"
        fi
 }
index c1271d686372dfd46d640a84ebec3971933ed4f2..5d4ae629e14e668279a559527876685689cae61c 100644 (file)
@@ -69,14 +69,12 @@ svn_cmd () {
 maybe_start_httpd () {
        loc=${1-svn}
 
-       test_tristate GIT_SVN_TEST_HTTPD
-       case $GIT_SVN_TEST_HTTPD in
-       true)
+       if git env--helper --type=bool --default=false --exit-code GIT_TEST_HTTPD
+       then
                . "$TEST_DIRECTORY"/lib-httpd.sh
                LIB_HTTPD_SVN="$loc"
                start_httpd
-               ;;
-       esac
+       fi
 }
 
 convert_to_rev_db () {
@@ -106,8 +104,7 @@ EOF
 }
 
 require_svnserve () {
-       test_tristate GIT_TEST_SVNSERVE
-       if ! test "$GIT_TEST_SVNSERVE" = true
+       if ! git env--helper --type=bool --default=false --exit-code GIT_TEST_SVNSERVE
        then
                skip_all='skipping svnserve test. (set $GIT_TEST_SVNSERVE to enable)'
                test_done
index b3cc62bd36f26f776b307607a941b01c547c404d..0d985758c6dd85e801404860ee82c5e93f1b51d9 100644 (file)
@@ -41,15 +41,14 @@ then
        test_done
 fi
 
-test_tristate GIT_TEST_HTTPD
-if test "$GIT_TEST_HTTPD" = false
+if ! git env--helper --type=bool --default=true --exit-code GIT_TEST_HTTPD
 then
        skip_all="Network testing disabled (unset GIT_TEST_HTTPD to enable)"
        test_done
 fi
 
 if ! test_have_prereq NOT_ROOT; then
-       test_skip_or_die $GIT_TEST_HTTPD \
+       test_skip_or_die GIT_TEST_HTTPD \
                "Cannot run httpd tests as root"
 fi
 
@@ -95,7 +94,7 @@ GIT_TRACE=$GIT_TRACE; export GIT_TRACE
 
 if ! test -x "$LIB_HTTPD_PATH"
 then
-       test_skip_or_die $GIT_TEST_HTTPD "no web server found at '$LIB_HTTPD_PATH'"
+       test_skip_or_die GIT_TEST_HTTPD "no web server found at '$LIB_HTTPD_PATH'"
 fi
 
 HTTPD_VERSION=$($LIB_HTTPD_PATH -v | \
@@ -107,19 +106,19 @@ then
        then
                if ! test $HTTPD_VERSION -ge 2
                then
-                       test_skip_or_die $GIT_TEST_HTTPD \
+                       test_skip_or_die GIT_TEST_HTTPD \
                                "at least Apache version 2 is required"
                fi
                if ! test -d "$DEFAULT_HTTPD_MODULE_PATH"
                then
-                       test_skip_or_die $GIT_TEST_HTTPD \
+                       test_skip_or_die GIT_TEST_HTTPD \
                                "Apache module directory not found"
                fi
 
                LIB_HTTPD_MODULE_PATH="$DEFAULT_HTTPD_MODULE_PATH"
        fi
 else
-       test_skip_or_die $GIT_TEST_HTTPD \
+       test_skip_or_die GIT_TEST_HTTPD \
                "Could not identify web server at '$LIB_HTTPD_PATH'"
 fi
 
@@ -184,7 +183,7 @@ start_httpd() {
        if test $? -ne 0
        then
                cat "$HTTPD_ROOT_PATH"/error.log >&4 2>/dev/null
-               test_skip_or_die $GIT_TEST_HTTPD "web server setup failed"
+               test_skip_or_die GIT_TEST_HTTPD "web server setup failed"
        fi
 }
 
diff --git a/t/perf/p5600-clone-reference.sh b/t/perf/p5600-clone-reference.sh
new file mode 100755 (executable)
index 0000000..68fed66
--- /dev/null
@@ -0,0 +1,27 @@
+#!/bin/sh
+
+test_description='speed of clone --reference'
+. ./perf-lib.sh
+
+test_perf_default_repo
+
+test_expect_success 'create shareable repository' '
+       git clone --bare . shared.git
+'
+
+test_expect_success 'advance base repository' '
+       # Do not use test_commit here; its test_tick will
+       # use some ancient hard-coded date. The resulting clock
+       # skew will cause pack-objects to traverse in a very
+       # sub-optimal order, skewing the results.
+       echo content >new-file-that-does-not-exist &&
+       git add new-file-that-does-not-exist &&
+       git commit -m "new commit"
+'
+
+test_perf 'clone --reference' '
+       rm -rf dst.git &&
+       git clone --no-local --bare --reference shared.git . dst.git
+'
+
+test_done
index 31de7e90f3b38d304e952bee68119a0100bbbfb6..e89438e619b010d8eb36bcfa54de8d49cd373180 100755 (executable)
@@ -726,7 +726,7 @@ donthaveit=yes
 test_expect_success DONTHAVEIT 'unmet prerequisite causes test to be skipped' '
        donthaveit=no
 '
-if test -z "$GIT_TEST_FAIL_PREREQS" -a $haveit$donthaveit != yesyes
+if test -z "$GIT_TEST_FAIL_PREREQS_INTERNAL" -a $haveit$donthaveit != yesyes
 then
        say "bug in test framework: prerequisite tags do not work reliably"
        exit 1
@@ -747,7 +747,7 @@ donthaveiteither=yes
 test_expect_success DONTHAVEIT,HAVEIT 'unmet prerequisites causes test to be skipped' '
        donthaveiteither=no
 '
-if test -z "$GIT_TEST_FAIL_PREREQS" -a $haveit$donthaveit$donthaveiteither != yesyesyes
+if test -z "$GIT_TEST_FAIL_PREREQS_INTERNAL" -a $haveit$donthaveit$donthaveiteither != yesyesyes
 then
        say "bug in test framework: multiple prerequisite tags do not work reliably"
        exit 1
@@ -763,7 +763,7 @@ test_expect_success !LAZY_TRUE 'missing lazy prereqs skip tests' '
        donthavetrue=no
 '
 
-if test -z "$GIT_TEST_FAIL_PREREQS" -a "$havetrue$donthavetrue" != yesyes
+if test -z "$GIT_TEST_FAIL_PREREQS_INTERNAL" -a "$havetrue$donthavetrue" != yesyes
 then
        say 'bug in test framework: lazy prerequisites do not work'
        exit 1
@@ -779,7 +779,7 @@ test_expect_success LAZY_FALSE 'missing negative lazy prereqs will skip' '
        havefalse=no
 '
 
-if test -z "$GIT_TEST_FAIL_PREREQS" -a "$nothavefalse$havefalse" != yesyes
+if test -z "$GIT_TEST_FAIL_PREREQS_INTERNAL" -a "$nothavefalse$havefalse" != yesyes
 then
        say 'bug in test framework: negative lazy prerequisites do not work'
        exit 1
@@ -790,7 +790,7 @@ test_expect_success 'tests clean up after themselves' '
        test_when_finished clean=yes
 '
 
-if test -z "$GIT_TEST_FAIL_PREREQS" -a $clean != yes
+if test -z "$GIT_TEST_FAIL_PREREQS_INTERNAL" -a $clean != yes
 then
        say "bug in test framework: basic cleanup command does not work reliably"
        exit 1
index 3f1f505e8937f391666a1b7e6d9b972a5f146974..9c96b3e3b10a99c20a1e6f5d4d0349c4fbcb0a10 100755 (executable)
@@ -9,15 +9,6 @@ test_hashmap() {
        test_cmp expect actual
 }
 
-test_expect_success 'hash functions' '
-
-test_hashmap "hash key1" "2215982743 2215982743 116372151 116372151" &&
-test_hashmap "hash key2" "2215982740 2215982740 116372148 116372148" &&
-test_hashmap "hash fooBarFrotz" "1383912807 1383912807 3189766727 3189766727" &&
-test_hashmap "hash foobarfrotz" "2862305959 2862305959 3189766727 3189766727"
-
-'
-
 test_expect_success 'put' '
 
 test_hashmap "put key1 value1
diff --git a/t/t0016-oidmap.sh b/t/t0016-oidmap.sh
new file mode 100755 (executable)
index 0000000..bbe719e
--- /dev/null
@@ -0,0 +1,102 @@
+#!/bin/sh
+
+test_description='test oidmap'
+. ./test-lib.sh
+
+# This purposefully is very similar to t0011-hashmap.sh
+
+test_oidmap () {
+       echo "$1" | test-tool oidmap $3 >actual &&
+       echo "$2" >expect &&
+       test_cmp expect actual
+}
+
+
+test_expect_success 'setup' '
+
+       test_commit one &&
+       test_commit two &&
+       test_commit three &&
+       test_commit four
+
+'
+
+test_expect_success 'put' '
+
+test_oidmap "put one 1
+put two 2
+put invalidOid 4
+put three 3" "NULL
+NULL
+Unknown oid: invalidOid
+NULL"
+
+'
+
+test_expect_success 'replace' '
+
+test_oidmap "put one 1
+put two 2
+put three 3
+put invalidOid 4
+put two deux
+put one un" "NULL
+NULL
+NULL
+Unknown oid: invalidOid
+2
+1"
+
+'
+
+test_expect_success 'get' '
+
+test_oidmap "put one 1
+put two 2
+put three 3
+get two
+get four
+get invalidOid
+get one" "NULL
+NULL
+NULL
+2
+NULL
+Unknown oid: invalidOid
+1"
+
+'
+
+test_expect_success 'remove' '
+
+test_oidmap "put one 1
+put two 2
+put three 3
+remove one
+remove two
+remove invalidOid
+remove four" "NULL
+NULL
+NULL
+1
+2
+Unknown oid: invalidOid
+NULL"
+
+'
+
+test_expect_success 'iterate' '
+
+test_oidmap "put one 1
+put two 2
+put three 3
+iterate" "NULL
+NULL
+NULL
+$(git rev-parse two) 2
+$(git rev-parse one) 1
+$(git rev-parse three) 3"
+
+'
+
+test_done
diff --git a/t/t0017-env-helper.sh b/t/t0017-env-helper.sh
new file mode 100755 (executable)
index 0000000..c1ecf6a
--- /dev/null
@@ -0,0 +1,99 @@
+#!/bin/sh
+
+test_description='test env--helper'
+
+. ./test-lib.sh
+
+
+test_expect_success 'env--helper usage' '
+       test_must_fail git env--helper &&
+       test_must_fail git env--helper --type=bool &&
+       test_must_fail git env--helper --type=ulong &&
+       test_must_fail git env--helper --type=bool &&
+       test_must_fail git env--helper --type=bool --default &&
+       test_must_fail git env--helper --type=bool --default= &&
+       test_must_fail git env--helper --defaultxyz
+'
+
+test_expect_success 'env--helper bad default values' '
+       test_must_fail git env--helper --type=bool --default=1xyz MISSING &&
+       test_must_fail git env--helper --type=ulong --default=1xyz MISSING
+'
+
+test_expect_success 'env--helper --type=bool' '
+       # Test various --default bool values
+       echo true >expected &&
+       git env--helper --type=bool --default=1 MISSING >actual &&
+       test_cmp expected actual &&
+       git env--helper --type=bool --default=yes MISSING >actual &&
+       test_cmp expected actual &&
+       git env--helper --type=bool --default=true MISSING >actual &&
+       test_cmp expected actual &&
+       echo false >expected &&
+       test_must_fail git env--helper --type=bool --default=0 MISSING >actual &&
+       test_cmp expected actual &&
+       test_must_fail git env--helper --type=bool --default=no MISSING >actual &&
+       test_cmp expected actual &&
+       test_must_fail git env--helper --type=bool --default=false MISSING >actual &&
+       test_cmp expected actual &&
+
+       # No output with --exit-code
+       git env--helper --type=bool --default=true --exit-code MISSING >actual.out 2>actual.err &&
+       test_must_be_empty actual.out &&
+       test_must_be_empty actual.err &&
+       test_must_fail git env--helper --type=bool --default=false --exit-code MISSING >actual.out 2>actual.err &&
+       test_must_be_empty actual.out &&
+       test_must_be_empty actual.err &&
+
+       # Existing variable
+       EXISTS=true git env--helper --type=bool --default=false --exit-code EXISTS >actual.out 2>actual.err &&
+       test_must_be_empty actual.out &&
+       test_must_be_empty actual.err &&
+       test_must_fail \
+               env EXISTS=false \
+               git env--helper --type=bool --default=true --exit-code EXISTS >actual.out 2>actual.err &&
+       test_must_be_empty actual.out &&
+       test_must_be_empty actual.err
+'
+
+test_expect_success 'env--helper --type=ulong' '
+       echo 1234567890 >expected &&
+       git env--helper --type=ulong --default=1234567890 MISSING >actual.out 2>actual.err &&
+       test_cmp expected actual.out &&
+       test_must_be_empty actual.err &&
+
+       echo 0 >expected &&
+       test_must_fail git env--helper --type=ulong --default=0 MISSING >actual &&
+       test_cmp expected actual &&
+
+       git env--helper --type=ulong --default=1234567890 --exit-code MISSING >actual.out 2>actual.err &&
+       test_must_be_empty actual.out &&
+       test_must_be_empty actual.err &&
+
+       EXISTS=1234567890 git env--helper --type=ulong --default=0 EXISTS --exit-code >actual.out 2>actual.err &&
+       test_must_be_empty actual.out &&
+       test_must_be_empty actual.err &&
+
+       echo 1234567890 >expected &&
+       EXISTS=1234567890 git env--helper --type=ulong --default=0 EXISTS >actual.out 2>actual.err &&
+       test_cmp expected actual.out &&
+       test_must_be_empty actual.err
+'
+
+test_expect_success 'env--helper reads config thanks to trace2' '
+       mkdir home &&
+       git config -f home/.gitconfig include.path cycle &&
+       git config -f home/cycle include.path .gitconfig &&
+
+       test_must_fail \
+               env HOME="$(pwd)/home" GIT_TEST_GETTEXT_POISON=false \
+               git config -l 2>err &&
+       grep "exceeded maximum include depth" err &&
+
+       test_must_fail \
+               env HOME="$(pwd)/home" GIT_TEST_GETTEXT_POISON=true \
+               git -C cycle env--helper --type=bool --default=0 --exit-code GIT_TEST_GETTEXT_POISON 2>err &&
+       grep "# GETTEXT POISON #" err
+'
+
+test_done
index a06269f38ae555f952b83b6fbc38374a725293d1..f9fa16ad8363a9e1c8611f37b5945f43ee2053c9 100755 (executable)
@@ -5,7 +5,7 @@
 
 test_description='Gettext Shell poison'
 
-GIT_TEST_GETTEXT_POISON=YesPlease
+GIT_TEST_GETTEXT_POISON=true
 export GIT_TEST_GETTEXT_POISON
 . ./lib-gettext.sh
 
@@ -31,4 +31,9 @@ test_expect_success 'eval_gettext: our eval_gettext() fallback has poison semant
     test_cmp expect actual
 '
 
+test_expect_success "gettext: invalid GIT_TEST_GETTEXT_POISON value doesn't infinitely loop" "
+       test_must_fail env GIT_TEST_GETTEXT_POISON=xyz git version 2>error &&
+       grep \"fatal: bad numeric config value 'xyz' for 'GIT_TEST_GETTEXT_POISON': invalid unit\" error
+"
+
 test_done
index 9571e366f801ea347845eef9736c92197fbdba08..d20b4d150d42c9fd6c14eb5f36e01a442d045cee 100755 (executable)
@@ -349,20 +349,13 @@ test_expect_success 'conditional include, onbranch, implicit /** for /' '
 '
 
 test_expect_success 'include cycles are detected' '
-       cat >.gitconfig <<-\EOF &&
-       [test]value = gitconfig
-       [include]path = cycle
-       EOF
-       cat >cycle <<-\EOF &&
-       [test]value = cycle
-       [include]path = .gitconfig
-       EOF
-       cat >expect <<-\EOF &&
-       gitconfig
-       cycle
-       EOF
-       test_must_fail git config --get-all test.value 2>stderr &&
-       test_i18ngrep "exceeded maximum include depth" stderr
+       git init --bare cycle &&
+       git -C cycle config include.path cycle &&
+       git config -f cycle/cycle include.path config &&
+       test_must_fail \
+               env GIT_TEST_GETTEXT_POISON=false \
+               git -C cycle config --get-all test.value 2>stderr &&
+       grep "exceeded maximum include depth" stderr
 '
 
 test_done
index 048feaf6ddf845279b30b7f1ebb7056c60e01c6d..ec548654ce1cae8773ad642877e525896a6b54f4 100755 (executable)
@@ -99,7 +99,7 @@ test_expect_success 'changed commit' '
        1:  4de457d = 1:  a4b3333 s/5/A/
        2:  fccce22 = 2:  f51d370 s/4/A/
        3:  147e64e ! 3:  0559556 s/11/B/
-           @@ -10,7 +10,7 @@
+           @@ file: A
              9
              10
             -11
@@ -109,8 +109,8 @@ test_expect_success 'changed commit' '
              13
              14
        4:  a63e992 ! 4:  d966c5c s/12/B/
-           @@ -8,7 +8,7 @@
-            @@
+           @@ file
+            @@ file: A
              9
              10
            - B
@@ -158,7 +158,7 @@ test_expect_success 'changed commit with sm config' '
        1:  4de457d = 1:  a4b3333 s/5/A/
        2:  fccce22 = 2:  f51d370 s/4/A/
        3:  147e64e ! 3:  0559556 s/11/B/
-           @@ -10,7 +10,7 @@
+           @@ file: A
              9
              10
             -11
@@ -168,8 +168,8 @@ test_expect_success 'changed commit with sm config' '
              13
              14
        4:  a63e992 ! 4:  d966c5c s/12/B/
-           @@ -8,7 +8,7 @@
-            @@
+           @@ file
+            @@ file: A
              9
              10
            - B
@@ -181,6 +181,92 @@ test_expect_success 'changed commit with sm config' '
        test_cmp expected actual
 '
 
+test_expect_success 'renamed file' '
+       git range-diff --no-color --submodule=log topic...renamed-file >actual &&
+       sed s/Z/\ /g >expected <<-EOF &&
+       1:  4de457d = 1:  f258d75 s/5/A/
+       2:  fccce22 ! 2:  017b62d s/4/A/
+           @@ Metadata
+           ZAuthor: Thomas Rast <trast@inf.ethz.ch>
+           Z
+           Z ## Commit message ##
+           -    s/4/A/
+           +    s/4/A/ + rename file
+           Z
+           - ## file ##
+           + ## file => renamed-file ##
+           Z@@
+           Z 1
+           Z 2
+       3:  147e64e ! 3:  3ce7af6 s/11/B/
+           @@ Metadata
+           Z ## Commit message ##
+           Z    s/11/B/
+           Z
+           - ## file ##
+           -@@ file: A
+           + ## renamed-file ##
+           +@@ renamed-file: A
+           Z 8
+           Z 9
+           Z 10
+       4:  a63e992 ! 4:  1e6226b s/12/B/
+           @@ Metadata
+           Z ## Commit message ##
+           Z    s/12/B/
+           Z
+           - ## file ##
+           -@@ file: A
+           + ## renamed-file ##
+           +@@ renamed-file: A
+           Z 9
+           Z 10
+           Z B
+       EOF
+       test_cmp expected actual
+'
+
+test_expect_success 'file added and later removed' '
+       git range-diff --no-color --submodule=log topic...added-removed >actual &&
+       sed s/Z/\ /g >expected <<-EOF &&
+       1:  4de457d = 1:  096b1ba s/5/A/
+       2:  fccce22 ! 2:  d92e698 s/4/A/
+           @@ Metadata
+           ZAuthor: Thomas Rast <trast@inf.ethz.ch>
+           Z
+           Z ## Commit message ##
+           -    s/4/A/
+           +    s/4/A/ + new-file
+           Z
+           Z ## file ##
+           Z@@
+           @@ file
+           Z A
+           Z 6
+           Z 7
+           +
+           + ## new-file (new) ##
+       3:  147e64e ! 3:  9a1db4d s/11/B/
+           @@ Metadata
+           ZAuthor: Thomas Rast <trast@inf.ethz.ch>
+           Z
+           Z ## Commit message ##
+           -    s/11/B/
+           +    s/11/B/ + remove file
+           Z
+           Z ## file ##
+           Z@@ file: A
+           @@ file: A
+           Z 12
+           Z 13
+           Z 14
+           +
+           + ## new-file (deleted) ##
+       4:  a63e992 = 4:  fea3b5c s/12/B/
+       EOF
+       test_cmp expected actual
+'
+
 test_expect_success 'no commits on one side' '
        git commit --amend -m "new message" &&
        git range-diff master HEAD@{1} HEAD
@@ -191,15 +277,15 @@ test_expect_success 'changed message' '
        sed s/Z/\ /g >expected <<-EOF &&
        1:  4de457d = 1:  f686024 s/5/A/
        2:  fccce22 ! 2:  4ab067d s/4/A/
-           @@ -2,6 +2,8 @@
-           Z
+           @@ Metadata
+           Z ## Commit message ##
            Z    s/4/A/
            Z
            +    Also a silly comment here!
            +
-           Z diff --git a/file b/file
-           Z --- a/file
-           Z +++ b/file
+           Z ## file ##
+           Z@@
+           Z 1
        3:  147e64e = 3:  b9cb956 s/11/B/
        4:  a63e992 = 4:  8add5f1 s/12/B/
        EOF
@@ -210,17 +296,17 @@ test_expect_success 'dual-coloring' '
        sed -e "s|^:||" >expect <<-\EOF &&
        :<YELLOW>1:  a4b3333 = 1:  f686024 s/5/A/<RESET>
        :<RED>2:  f51d370 <RESET><YELLOW>!<RESET><GREEN> 2:  4ab067d<RESET><YELLOW> s/4/A/<RESET>
-       :    <REVERSE><CYAN>@@ -2,6 +2,8 @@<RESET>
-       :     <RESET>
+       :    <REVERSE><CYAN>@@<RESET> <RESET>Metadata<RESET>
+       :      ## Commit message ##<RESET>
        :         s/4/A/<RESET>
        :     <RESET>
        :    <REVERSE><GREEN>+<RESET><BOLD>    Also a silly comment here!<RESET>
        :    <REVERSE><GREEN>+<RESET>
-       :      diff --git a/file b/file<RESET>
-       :      --- a/file<RESET>
-       :      +++ b/file<RESET>
+       :      ## file ##<RESET>
+       :    <CYAN> @@<RESET>
+       :      1<RESET>
        :<RED>3:  0559556 <RESET><YELLOW>!<RESET><GREEN> 3:  b9cb956<RESET><YELLOW> s/11/B/<RESET>
-       :    <REVERSE><CYAN>@@ -10,7 +10,7 @@<RESET>
+       :    <REVERSE><CYAN>@@<RESET> <RESET>file: A<RESET>
        :      9<RESET>
        :      10<RESET>
        :    <RED> -11<RESET>
@@ -230,8 +316,8 @@ test_expect_success 'dual-coloring' '
        :      13<RESET>
        :      14<RESET>
        :<RED>4:  d966c5c <RESET><YELLOW>!<RESET><GREEN> 4:  8add5f1<RESET><YELLOW> s/12/B/<RESET>
-       :    <REVERSE><CYAN>@@ -8,7 +8,7 @@<RESET>
-       :    <CYAN> @@<RESET>
+       :    <REVERSE><CYAN>@@<RESET> <RESET>file<RESET>
+       :    <CYAN> @@ file: A<RESET>
        :      9<RESET>
        :      10<RESET>
        :    <REVERSE><RED>-<RESET><FAINT> BB<RESET>
index b8ffff0940d6f15da7b90400106a162af921c069..7bb38149622737ac65a05b5b8105360640e0aa6e 100644 (file)
@@ -22,8 +22,8 @@ data 51
 19
 20
 
-reset refs/heads/removed
-commit refs/heads/removed
+reset refs/heads/renamed-file
+commit refs/heads/renamed-file
 mark :2
 author Thomas Rast <trast@inf.ethz.ch> 1374424921 +0200
 committer Thomas Rast <trast@inf.ethz.ch> 1374484724 +0200
@@ -599,6 +599,82 @@ s/12/B/
 from :46
 M 100644 :28 file
 
-reset refs/heads/removed
-from :47
+commit refs/heads/added-removed
+mark :48
+author Thomas Rast <trast@inf.ethz.ch> 1374485014 +0200
+committer Thomas Gummerer <t.gummerer@gmail.com> 1556574151 +0100
+data 7
+s/5/A/
+from :2
+M 100644 :3 file
+
+blob
+mark :49
+data 0
+
+commit refs/heads/added-removed
+mark :50
+author Thomas Rast <trast@inf.ethz.ch> 1374485024 +0200
+committer Thomas Gummerer <t.gummerer@gmail.com> 1556574177 +0100
+data 18
+s/4/A/ + new-file
+from :48
+M 100644 :5 file
+M 100644 :49 new-file
+
+commit refs/heads/added-removed
+mark :51
+author Thomas Rast <trast@inf.ethz.ch> 1374485036 +0200
+committer Thomas Gummerer <t.gummerer@gmail.com> 1556574177 +0100
+data 22
+s/11/B/ + remove file
+from :50
+M 100644 :7 file
+D new-file
+
+commit refs/heads/added-removed
+mark :52
+author Thomas Rast <trast@inf.ethz.ch> 1374485044 +0200
+committer Thomas Gummerer <t.gummerer@gmail.com> 1556574177 +0100
+data 8
+s/12/B/
+from :51
+M 100644 :9 file
+
+commit refs/heads/renamed-file
+mark :53
+author Thomas Rast <trast@inf.ethz.ch> 1374485014 +0200
+committer Thomas Gummerer <t.gummerer@gmail.com> 1556574309 +0100
+data 7
+s/5/A/
+from :2
+M 100644 :3 file
+
+commit refs/heads/renamed-file
+mark :54
+author Thomas Rast <trast@inf.ethz.ch> 1374485024 +0200
+committer Thomas Gummerer <t.gummerer@gmail.com> 1556574312 +0100
+data 21
+s/4/A/ + rename file
+from :53
+D file
+M 100644 :5 renamed-file
+
+commit refs/heads/renamed-file
+mark :55
+author Thomas Rast <trast@inf.ethz.ch> 1374485036 +0200
+committer Thomas Gummerer <t.gummerer@gmail.com> 1556574319 +0100
+data 8
+s/11/B/
+from :54
+M 100644 :7 renamed-file
+
+commit refs/heads/renamed-file
+mark :56
+author Thomas Rast <trast@inf.ethz.ch> 1374485044 +0200
+committer Thomas Gummerer <t.gummerer@gmail.com> 1556574319 +0100
+data 8
+s/12/B/
+from :55
+M 100644 :9 renamed-file
 
index 9186e90127712feaf92fe94e5bdfce02528fcf5f..b8f4d034672378065a5fe08df6b8210fa3c71721 100755 (executable)
@@ -30,7 +30,8 @@ test_expect_success setup '
        echo conflicting-change >file2 &&
        git add . &&
        test_tick &&
-       git commit -m "related commit"
+       git commit -m "related commit" &&
+       remove_progress_re="$(printf "s/.*\\r//")"
 '
 
 create_expected_success_am () {
@@ -48,8 +49,8 @@ create_expected_success_interactive () {
        q_to_cr >expected <<-EOF
        $(grep "^Created autostash: [0-9a-f][0-9a-f]*\$" actual)
        HEAD is now at $(git rev-parse --short feature-branch) third commit
-       Rebasing (1/2)QRebasing (2/2)QApplied autostash.
-       Q                                                                                QSuccessfully rebased and updated refs/heads/rebased-feature-branch.
+       Applied autostash.
+       Successfully rebased and updated refs/heads/rebased-feature-branch.
        EOF
 }
 
@@ -67,13 +68,13 @@ create_expected_failure_am () {
 }
 
 create_expected_failure_interactive () {
-       q_to_cr >expected <<-EOF
+       cat >expected <<-EOF
        $(grep "^Created autostash: [0-9a-f][0-9a-f]*\$" actual)
        HEAD is now at $(git rev-parse --short feature-branch) third commit
-       Rebasing (1/2)QRebasing (2/2)QApplying autostash resulted in conflicts.
+       Applying autostash resulted in conflicts.
        Your changes are safe in the stash.
        You can run "git stash pop" or "git stash drop" at any time.
-       Q                                                                                QSuccessfully rebased and updated refs/heads/rebased-feature-branch.
+       Successfully rebased and updated refs/heads/rebased-feature-branch.
        EOF
 }
 
@@ -109,7 +110,8 @@ testrebase () {
                        suffix=interactive
                fi &&
                create_expected_success_$suffix &&
-               test_i18ncmp expected actual
+               sed "$remove_progress_re" <actual >actual2 &&
+               test_i18ncmp expected actual2
        '
 
        test_expect_success "rebase$type: dirty index, non-conflicting rebase" '
@@ -209,7 +211,8 @@ testrebase () {
                        suffix=interactive
                fi &&
                create_expected_failure_$suffix &&
-               test_i18ncmp expected actual
+               sed "$remove_progress_re" <actual >actual2 &&
+               test_i18ncmp expected actual2
        '
 }
 
index 941d5026da2adc857fa332f899ea3594876a550c..793bcc7fe3246e8fc375b8678bb99f7bc875a48c 100755 (executable)
@@ -93,6 +93,128 @@ test_expect_success 'cherry-pick cleans up sequencer state upon success' '
        test_path_is_missing .git/sequencer
 '
 
+test_expect_success 'cherry-pick --skip requires cherry-pick in progress' '
+       pristine_detach initial &&
+       test_must_fail git cherry-pick --skip
+'
+
+test_expect_success 'revert --skip requires revert in progress' '
+       pristine_detach initial &&
+       test_must_fail git revert --skip
+'
+
+test_expect_success 'cherry-pick --skip to skip commit' '
+       pristine_detach initial &&
+       test_must_fail git cherry-pick anotherpick &&
+       test_must_fail git revert --skip &&
+       git cherry-pick --skip &&
+       test_cmp_rev initial HEAD &&
+       test_path_is_missing .git/CHERRY_PICK_HEAD
+'
+
+test_expect_success 'revert --skip to skip commit' '
+       pristine_detach anotherpick &&
+       test_must_fail git revert anotherpick~1 &&
+       test_must_fail git cherry-pick --skip &&
+       git revert --skip &&
+       test_cmp_rev anotherpick HEAD
+'
+
+test_expect_success 'skip "empty" commit' '
+       pristine_detach picked &&
+       test_commit dummy foo d &&
+       test_must_fail git cherry-pick anotherpick &&
+       git cherry-pick --skip &&
+       test_cmp_rev dummy HEAD
+'
+
+test_expect_success 'skip a commit and check if rest of sequence is correct' '
+       pristine_detach initial &&
+       echo e >expect &&
+       cat >expect.log <<-EOF &&
+       OBJID
+       :100644 100644 OBJID OBJID M    foo
+       OBJID
+       :100644 100644 OBJID OBJID M    foo
+       OBJID
+       :100644 100644 OBJID OBJID M    unrelated
+       OBJID
+       :000000 100644 OBJID OBJID A    foo
+       :000000 100644 OBJID OBJID A    unrelated
+       EOF
+       test_must_fail git cherry-pick base..yetanotherpick &&
+       test_must_fail git cherry-pick --skip &&
+       echo d >foo &&
+       git add foo &&
+       git cherry-pick --continue &&
+       {
+               git rev-list HEAD |
+               git diff-tree --root --stdin |
+               sed "s/$OID_REGEX/OBJID/g"
+       } >actual.log &&
+       test_cmp expect foo &&
+       test_cmp expect.log actual.log
+'
+
+test_expect_success 'check advice when we move HEAD by committing' '
+       pristine_detach initial &&
+       cat >expect <<-EOF &&
+       error: there is nothing to skip
+       hint: have you committed already?
+       hint: try "git cherry-pick --continue"
+       fatal: cherry-pick failed
+       EOF
+       test_must_fail git cherry-pick base..yetanotherpick &&
+       echo c >foo &&
+       git commit -a &&
+       test_path_is_missing .git/CHERRY_PICK_HEAD &&
+       test_must_fail git cherry-pick --skip 2>advice &&
+       test_i18ncmp expect advice
+'
+
+test_expect_success 'selectively advise --skip while launching another sequence' '
+       pristine_detach initial &&
+       cat >expect <<-EOF &&
+       error: cherry-pick is already in progress
+       hint: try "git cherry-pick (--continue | --skip | --abort | --quit)"
+       fatal: cherry-pick failed
+       EOF
+       test_must_fail git cherry-pick picked..yetanotherpick &&
+       test_must_fail git cherry-pick picked..yetanotherpick 2>advice &&
+       test_i18ncmp expect advice &&
+       cat >expect <<-EOF &&
+       error: cherry-pick is already in progress
+       hint: try "git cherry-pick (--continue | --abort | --quit)"
+       fatal: cherry-pick failed
+       EOF
+       git reset --merge &&
+       test_must_fail git cherry-pick picked..yetanotherpick 2>advice &&
+       test_i18ncmp expect advice
+'
+
+test_expect_success 'allow skipping commit but not abort for a new history' '
+       pristine_detach initial &&
+       cat >expect <<-EOF &&
+       error: cannot abort from a branch yet to be born
+       fatal: cherry-pick failed
+       EOF
+       git checkout --orphan new_disconnected &&
+       git reset --hard &&
+       test_must_fail git cherry-pick anotherpick &&
+       test_must_fail git cherry-pick --abort 2>advice &&
+       git cherry-pick --skip &&
+       test_i18ncmp expect advice
+'
+
+test_expect_success 'allow skipping stopped cherry-pick because of untracked file modifications' '
+       pristine_detach initial &&
+       git rm --cached unrelated &&
+       git commit -m "untrack unrelated" &&
+       test_must_fail git cherry-pick initial base &&
+       test_path_is_missing .git/CHERRY_PICK_HEAD &&
+       git cherry-pick --skip
+'
+
 test_expect_success '--quit does not complain when no cherry-pick is in progress' '
        pristine_detach initial &&
        git cherry-pick --quit
index 5267c4be20e709bb1632c5b9786ffdb418bcb5c9..22cb9d66430410f726e821e906fa79587f43c3e8 100755 (executable)
@@ -20,7 +20,7 @@ test_expect_success 'verify graph with no graph file' '
 test_expect_success 'write graph with no packs' '
        cd "$TRASH_DIRECTORY/full" &&
        git commit-graph write --object-dir . &&
-       test_path_is_file info/commit-graph
+       test_path_is_missing info/commit-graph
 '
 
 test_expect_success 'close with correct error on bad input' '
index 1ebf19ec3cd559dbf9ae7205a225d6e49368a8d2..c72ca0439993bb25b3d0e25fa2ce5d399b49a2b7 100755 (executable)
@@ -363,4 +363,188 @@ test_expect_success 'verify incorrect 64-bit offset' '
                "incorrect object offset"
 '
 
+test_expect_success 'setup expire tests' '
+       mkdir dup &&
+       (
+               cd dup &&
+               git init &&
+               test-tool genrandom "data" 4096 >large_file.txt &&
+               git update-index --add large_file.txt &&
+               for i in $(test_seq 1 20)
+               do
+                       test_commit $i
+               done &&
+               git branch A HEAD &&
+               git branch B HEAD~8 &&
+               git branch C HEAD~13 &&
+               git branch D HEAD~16 &&
+               git branch E HEAD~18 &&
+               git pack-objects --revs .git/objects/pack/pack-A <<-EOF &&
+               refs/heads/A
+               ^refs/heads/B
+               EOF
+               git pack-objects --revs .git/objects/pack/pack-B <<-EOF &&
+               refs/heads/B
+               ^refs/heads/C
+               EOF
+               git pack-objects --revs .git/objects/pack/pack-C <<-EOF &&
+               refs/heads/C
+               ^refs/heads/D
+               EOF
+               git pack-objects --revs .git/objects/pack/pack-D <<-EOF &&
+               refs/heads/D
+               ^refs/heads/E
+               EOF
+               git pack-objects --revs .git/objects/pack/pack-E <<-EOF &&
+               refs/heads/E
+               EOF
+               git multi-pack-index write &&
+               cp -r .git/objects/pack .git/objects/pack-backup
+       )
+'
+
+test_expect_success 'expire does not remove any packs' '
+       (
+               cd dup &&
+               ls .git/objects/pack >expect &&
+               git multi-pack-index expire &&
+               ls .git/objects/pack >actual &&
+               test_cmp expect actual
+       )
+'
+
+test_expect_success 'expire removes unreferenced packs' '
+       (
+               cd dup &&
+               git pack-objects --revs .git/objects/pack/pack-combined <<-EOF &&
+               refs/heads/A
+               ^refs/heads/C
+               EOF
+               git multi-pack-index write &&
+               ls .git/objects/pack | grep -v -e pack-[AB] >expect &&
+               git multi-pack-index expire &&
+               ls .git/objects/pack >actual &&
+               test_cmp expect actual &&
+               ls .git/objects/pack/ | grep idx >expect-idx &&
+               test-tool read-midx .git/objects | grep idx >actual-midx &&
+               test_cmp expect-idx actual-midx &&
+               git multi-pack-index verify &&
+               git fsck
+       )
+'
+
+test_expect_success 'repack with minimum size does not alter existing packs' '
+       (
+               cd dup &&
+               rm -rf .git/objects/pack &&
+               mv .git/objects/pack-backup .git/objects/pack &&
+               touch -m -t 201901010000 .git/objects/pack/pack-D* &&
+               touch -m -t 201901010001 .git/objects/pack/pack-C* &&
+               touch -m -t 201901010002 .git/objects/pack/pack-B* &&
+               touch -m -t 201901010003 .git/objects/pack/pack-A* &&
+               ls .git/objects/pack >expect &&
+               MINSIZE=$(test-tool path-utils file-size .git/objects/pack/*pack | sort -n | head -n 1) &&
+               git multi-pack-index repack --batch-size=$MINSIZE &&
+               ls .git/objects/pack >actual &&
+               test_cmp expect actual
+       )
+'
+
+test_expect_success 'repack creates a new pack' '
+       (
+               cd dup &&
+               ls .git/objects/pack/*idx >idx-list &&
+               test_line_count = 5 idx-list &&
+               THIRD_SMALLEST_SIZE=$(test-tool path-utils file-size .git/objects/pack/*pack | sort -n | head -n 3 | tail -n 1) &&
+               BATCH_SIZE=$(($THIRD_SMALLEST_SIZE + 1)) &&
+               git multi-pack-index repack --batch-size=$BATCH_SIZE &&
+               ls .git/objects/pack/*idx >idx-list &&
+               test_line_count = 6 idx-list &&
+               test-tool read-midx .git/objects | grep idx >midx-list &&
+               test_line_count = 6 midx-list
+       )
+'
+
+test_expect_success 'expire removes repacked packs' '
+       (
+               cd dup &&
+               ls -al .git/objects/pack/*pack &&
+               ls -S .git/objects/pack/*pack | head -n 4 >expect &&
+               git multi-pack-index expire &&
+               ls -S .git/objects/pack/*pack >actual &&
+               test_cmp expect actual &&
+               test-tool read-midx .git/objects | grep idx >midx-list &&
+               test_line_count = 4 midx-list
+       )
+'
+
+test_expect_success 'expire works when adding new packs' '
+       (
+               cd dup &&
+               git pack-objects --revs .git/objects/pack/pack-combined <<-EOF &&
+               refs/heads/A
+               ^refs/heads/B
+               EOF
+               git pack-objects --revs .git/objects/pack/pack-combined <<-EOF &&
+               refs/heads/B
+               ^refs/heads/C
+               EOF
+               git pack-objects --revs .git/objects/pack/pack-combined <<-EOF &&
+               refs/heads/C
+               ^refs/heads/D
+               EOF
+               git multi-pack-index write &&
+               git pack-objects --revs .git/objects/pack/a-pack <<-EOF &&
+               refs/heads/D
+               ^refs/heads/E
+               EOF
+               git multi-pack-index write &&
+               git pack-objects --revs .git/objects/pack/z-pack <<-EOF &&
+               refs/heads/E
+               EOF
+               git multi-pack-index expire &&
+               ls .git/objects/pack/ | grep idx >expect &&
+               test-tool read-midx .git/objects | grep idx >actual &&
+               test_cmp expect actual &&
+               git multi-pack-index verify
+       )
+'
+
+test_expect_success 'expire respects .keep files' '
+       (
+               cd dup &&
+               git pack-objects --revs .git/objects/pack/pack-all <<-EOF &&
+               refs/heads/A
+               EOF
+               git multi-pack-index write &&
+               PACKA=$(ls .git/objects/pack/a-pack*\.pack | sed s/\.pack\$//) &&
+               touch $PACKA.keep &&
+               git multi-pack-index expire &&
+               ls -S .git/objects/pack/a-pack* | grep $PACKA >a-pack-files &&
+               test_line_count = 3 a-pack-files &&
+               test-tool read-midx .git/objects | grep idx >midx-list &&
+               test_line_count = 2 midx-list
+       )
+'
+
+test_expect_success 'repack --batch-size=0 repacks everything' '
+       (
+               cd dup &&
+               rm .git/objects/pack/*.keep &&
+               ls .git/objects/pack/*idx >idx-list &&
+               test_line_count = 2 idx-list &&
+               git multi-pack-index repack --batch-size=0 &&
+               ls .git/objects/pack/*idx >idx-list &&
+               test_line_count = 3 idx-list &&
+               test-tool read-midx .git/objects | grep idx >midx-list &&
+               test_line_count = 3 midx-list &&
+               git multi-pack-index expire &&
+               ls -al .git/objects/pack/*idx >idx-list &&
+               test_line_count = 1 idx-list &&
+               git multi-pack-index repack --batch-size=0 &&
+               ls -al .git/objects/pack/*idx >new-idx-list &&
+               test_cmp idx-list new-idx-list
+       )
+'
+
 test_done
diff --git a/t/t5324-split-commit-graph.sh b/t/t5324-split-commit-graph.sh
new file mode 100755 (executable)
index 0000000..03f45a1
--- /dev/null
@@ -0,0 +1,343 @@
+#!/bin/sh
+
+test_description='split commit graph'
+. ./test-lib.sh
+
+GIT_TEST_COMMIT_GRAPH=0
+
+test_expect_success 'setup repo' '
+       git init &&
+       git config core.commitGraph true &&
+       infodir=".git/objects/info" &&
+       graphdir="$infodir/commit-graphs" &&
+       test_oid_init
+'
+
+graph_read_expect() {
+       NUM_BASE=0
+       if test ! -z $2
+       then
+               NUM_BASE=$2
+       fi
+       cat >expect <<- EOF
+       header: 43475048 1 1 3 $NUM_BASE
+       num_commits: $1
+       chunks: oid_fanout oid_lookup commit_metadata
+       EOF
+       git commit-graph read >output &&
+       test_cmp expect output
+}
+
+test_expect_success 'create commits and write commit-graph' '
+       for i in $(test_seq 3)
+       do
+               test_commit $i &&
+               git branch commits/$i || return 1
+       done &&
+       git commit-graph write --reachable &&
+       test_path_is_file $infodir/commit-graph &&
+       graph_read_expect 3
+'
+
+graph_git_two_modes() {
+       git -c core.commitGraph=true $1 >output
+       git -c core.commitGraph=false $1 >expect
+       test_cmp expect output
+}
+
+graph_git_behavior() {
+       MSG=$1
+       BRANCH=$2
+       COMPARE=$3
+       test_expect_success "check normal git operations: $MSG" '
+               graph_git_two_modes "log --oneline $BRANCH" &&
+               graph_git_two_modes "log --topo-order $BRANCH" &&
+               graph_git_two_modes "log --graph $COMPARE..$BRANCH" &&
+               graph_git_two_modes "branch -vv" &&
+               graph_git_two_modes "merge-base -a $BRANCH $COMPARE"
+       '
+}
+
+graph_git_behavior 'graph exists' commits/3 commits/1
+
+verify_chain_files_exist() {
+       for hash in $(cat $1/commit-graph-chain)
+       do
+               test_path_is_file $1/graph-$hash.graph || return 1
+       done
+}
+
+test_expect_success 'add more commits, and write a new base graph' '
+       git reset --hard commits/1 &&
+       for i in $(test_seq 4 5)
+       do
+               test_commit $i &&
+               git branch commits/$i || return 1
+       done &&
+       git reset --hard commits/2 &&
+       for i in $(test_seq 6 10)
+       do
+               test_commit $i &&
+               git branch commits/$i || return 1
+       done &&
+       git reset --hard commits/2 &&
+       git merge commits/4 &&
+       git branch merge/1 &&
+       git reset --hard commits/4 &&
+       git merge commits/6 &&
+       git branch merge/2 &&
+       git commit-graph write --reachable &&
+       graph_read_expect 12
+'
+
+test_expect_success 'fork and fail to base a chain on a commit-graph file' '
+       test_when_finished rm -rf fork &&
+       git clone . fork &&
+       (
+               cd fork &&
+               rm .git/objects/info/commit-graph &&
+               echo "$(pwd)/../.git/objects" >.git/objects/info/alternates &&
+               test_commit new-commit &&
+               git commit-graph write --reachable --split &&
+               test_path_is_file $graphdir/commit-graph-chain &&
+               test_line_count = 1 $graphdir/commit-graph-chain &&
+               verify_chain_files_exist $graphdir
+       )
+'
+
+test_expect_success 'add three more commits, write a tip graph' '
+       git reset --hard commits/3 &&
+       git merge merge/1 &&
+       git merge commits/5 &&
+       git merge merge/2 &&
+       git branch merge/3 &&
+       git commit-graph write --reachable --split &&
+       test_path_is_missing $infodir/commit-graph &&
+       test_path_is_file $graphdir/commit-graph-chain &&
+       ls $graphdir/graph-*.graph >graph-files &&
+       test_line_count = 2 graph-files &&
+       verify_chain_files_exist $graphdir
+'
+
+graph_git_behavior 'split commit-graph: merge 3 vs 2' merge/3 merge/2
+
+test_expect_success 'add one commit, write a tip graph' '
+       test_commit 11 &&
+       git branch commits/11 &&
+       git commit-graph write --reachable --split &&
+       test_path_is_missing $infodir/commit-graph &&
+       test_path_is_file $graphdir/commit-graph-chain &&
+       ls $graphdir/graph-*.graph >graph-files &&
+       test_line_count = 3 graph-files &&
+       verify_chain_files_exist $graphdir
+'
+
+graph_git_behavior 'three-layer commit-graph: commit 11 vs 6' commits/11 commits/6
+
+test_expect_success 'add one commit, write a merged graph' '
+       test_commit 12 &&
+       git branch commits/12 &&
+       git commit-graph write --reachable --split &&
+       test_path_is_file $graphdir/commit-graph-chain &&
+       test_line_count = 2 $graphdir/commit-graph-chain &&
+       ls $graphdir/graph-*.graph >graph-files &&
+       test_line_count = 2 graph-files &&
+       verify_chain_files_exist $graphdir
+'
+
+graph_git_behavior 'merged commit-graph: commit 12 vs 6' commits/12 commits/6
+
+test_expect_success 'create fork and chain across alternate' '
+       git clone . fork &&
+       (
+               cd fork &&
+               git config core.commitGraph true &&
+               rm -rf $graphdir &&
+               echo "$(pwd)/../.git/objects" >.git/objects/info/alternates &&
+               test_commit 13 &&
+               git branch commits/13 &&
+               git commit-graph write --reachable --split &&
+               test_path_is_file $graphdir/commit-graph-chain &&
+               test_line_count = 3 $graphdir/commit-graph-chain &&
+               ls $graphdir/graph-*.graph >graph-files &&
+               test_line_count = 1 graph-files &&
+               git -c core.commitGraph=true  rev-list HEAD >expect &&
+               git -c core.commitGraph=false rev-list HEAD >actual &&
+               test_cmp expect actual &&
+               test_commit 14 &&
+               git commit-graph write --reachable --split --object-dir=.git/objects/ &&
+               test_line_count = 3 $graphdir/commit-graph-chain &&
+               ls $graphdir/graph-*.graph >graph-files &&
+               test_line_count = 1 graph-files
+       )
+'
+
+graph_git_behavior 'alternate: commit 13 vs 6' commits/13 commits/6
+
+test_expect_success 'test merge stragety constants' '
+       git clone . merge-2 &&
+       (
+               cd merge-2 &&
+               git config core.commitGraph true &&
+               test_line_count = 2 $graphdir/commit-graph-chain &&
+               test_commit 14 &&
+               git commit-graph write --reachable --split --size-multiple=2 &&
+               test_line_count = 3 $graphdir/commit-graph-chain
+
+       ) &&
+       git clone . merge-10 &&
+       (
+               cd merge-10 &&
+               git config core.commitGraph true &&
+               test_line_count = 2 $graphdir/commit-graph-chain &&
+               test_commit 14 &&
+               git commit-graph write --reachable --split --size-multiple=10 &&
+               test_line_count = 1 $graphdir/commit-graph-chain &&
+               ls $graphdir/graph-*.graph >graph-files &&
+               test_line_count = 1 graph-files
+       ) &&
+       git clone . merge-10-expire &&
+       (
+               cd merge-10-expire &&
+               git config core.commitGraph true &&
+               test_line_count = 2 $graphdir/commit-graph-chain &&
+               test_commit 15 &&
+               git commit-graph write --reachable --split --size-multiple=10 --expire-time=1980-01-01 &&
+               test_line_count = 1 $graphdir/commit-graph-chain &&
+               ls $graphdir/graph-*.graph >graph-files &&
+               test_line_count = 3 graph-files
+       ) &&
+       git clone --no-hardlinks . max-commits &&
+       (
+               cd max-commits &&
+               git config core.commitGraph true &&
+               test_line_count = 2 $graphdir/commit-graph-chain &&
+               test_commit 16 &&
+               test_commit 17 &&
+               git commit-graph write --reachable --split --max-commits=1 &&
+               test_line_count = 1 $graphdir/commit-graph-chain &&
+               ls $graphdir/graph-*.graph >graph-files &&
+               test_line_count = 1 graph-files
+       )
+'
+
+test_expect_success 'remove commit-graph-chain file after flattening' '
+       git clone . flatten &&
+       (
+               cd flatten &&
+               test_line_count = 2 $graphdir/commit-graph-chain &&
+               git commit-graph write --reachable &&
+               test_path_is_missing $graphdir/commit-graph-chain &&
+               ls $graphdir >graph-files &&
+               test_line_count = 0 graph-files
+       )
+'
+
+corrupt_file() {
+       file=$1
+       pos=$2
+       data="${3:-\0}"
+       chmod a+w "$file" &&
+       printf "$data" | dd of="$file" bs=1 seek="$pos" conv=notrunc
+}
+
+test_expect_success 'verify hashes along chain, even in shallow' '
+       git clone --no-hardlinks . verify &&
+       (
+               cd verify &&
+               git commit-graph verify &&
+               base_file=$graphdir/graph-$(head -n 1 $graphdir/commit-graph-chain).graph &&
+               corrupt_file "$base_file" 1760 "\01" &&
+               test_must_fail git commit-graph verify --shallow 2>test_err &&
+               grep -v "^+" test_err >err &&
+               test_i18ngrep "incorrect checksum" err
+       )
+'
+
+test_expect_success 'verify --shallow does not check base contents' '
+       git clone --no-hardlinks . verify-shallow &&
+       (
+               cd verify-shallow &&
+               git commit-graph verify &&
+               base_file=$graphdir/graph-$(head -n 1 $graphdir/commit-graph-chain).graph &&
+               corrupt_file "$base_file" 1000 "\01" &&
+               git commit-graph verify --shallow &&
+               test_must_fail git commit-graph verify 2>test_err &&
+               grep -v "^+" test_err >err &&
+               test_i18ngrep "incorrect checksum" err
+       )
+'
+
+test_expect_success 'warn on base graph chunk incorrect' '
+       git clone --no-hardlinks . base-chunk &&
+       (
+               cd base-chunk &&
+               git commit-graph verify &&
+               base_file=$graphdir/graph-$(tail -n 1 $graphdir/commit-graph-chain).graph &&
+               corrupt_file "$base_file" 1376 "\01" &&
+               git commit-graph verify --shallow 2>test_err &&
+               grep -v "^+" test_err >err &&
+               test_i18ngrep "commit-graph chain does not match" err
+       )
+'
+
+test_expect_success 'verify after commit-graph-chain corruption' '
+       git clone --no-hardlinks . verify-chain &&
+       (
+               cd verify-chain &&
+               corrupt_file "$graphdir/commit-graph-chain" 60 "G" &&
+               git commit-graph verify 2>test_err &&
+               grep -v "^+" test_err >err &&
+               test_i18ngrep "invalid commit-graph chain" err &&
+               corrupt_file "$graphdir/commit-graph-chain" 60 "A" &&
+               git commit-graph verify 2>test_err &&
+               grep -v "^+" test_err >err &&
+               test_i18ngrep "unable to find all commit-graph files" err
+       )
+'
+
+test_expect_success 'verify across alternates' '
+       git clone --no-hardlinks . verify-alt &&
+       (
+               cd verify-alt &&
+               rm -rf $graphdir &&
+               altdir="$(pwd)/../.git/objects" &&
+               echo "$altdir" >.git/objects/info/alternates &&
+               git commit-graph verify --object-dir="$altdir/" &&
+               test_commit extra &&
+               git commit-graph write --reachable --split &&
+               tip_file=$graphdir/graph-$(tail -n 1 $graphdir/commit-graph-chain).graph &&
+               corrupt_file "$tip_file" 100 "\01" &&
+               test_must_fail git commit-graph verify --shallow 2>test_err &&
+               grep -v "^+" test_err >err &&
+               test_i18ngrep "commit-graph has incorrect fanout value" err
+       )
+'
+
+test_expect_success 'add octopus merge' '
+       git reset --hard commits/10 &&
+       git merge commits/3 commits/4 &&
+       git branch merge/octopus &&
+       git commit-graph write --reachable --split &&
+       git commit-graph verify &&
+       test_line_count = 3 $graphdir/commit-graph-chain
+'
+
+graph_git_behavior 'graph exists' merge/octopus commits/12
+
+test_expect_success 'split across alternate where alternate is not split' '
+       git commit-graph write --reachable &&
+       test_path_is_file .git/objects/info/commit-graph &&
+       cp .git/objects/info/commit-graph . &&
+       git clone --no-hardlinks . alt-split &&
+       (
+               cd alt-split &&
+               echo "$(pwd)"/../.git/objects >.git/objects/info/alternates &&
+               test_commit 18 &&
+               git commit-graph write --reachable --split &&
+               test_line_count = 1 $graphdir/commit-graph-chain
+       ) &&
+       test_cmp commit-graph .git/objects/info/commit-graph
+'
+
+test_done
index 7bc706873c5b2341f6a0922cf2e4f79d34eee97c..fdfe179b11885be7fdc49ed3732d0dfe5d3537bc 100755 (executable)
@@ -164,9 +164,9 @@ test_expect_success 'fsck with unsorted skipList' '
 test_expect_success 'fsck with invalid or bogus skipList input' '
        git -c fsck.skipList=/dev/null -c fsck.missingEmail=ignore fsck &&
        test_must_fail git -c fsck.skipList=does-not-exist -c fsck.missingEmail=ignore fsck 2>err &&
-       test_i18ngrep "Could not open skip list: does-not-exist" err &&
+       test_i18ngrep "could not open.*: does-not-exist" err &&
        test_must_fail git -c fsck.skipList=.git/config -c fsck.missingEmail=ignore fsck 2>err &&
-       test_i18ngrep "Invalid SHA-1: \[core\]" err
+       test_i18ngrep "invalid object name: \[core\]" err
 '
 
 test_expect_success 'fsck with other accepted skipList input (comments & empty lines)' '
@@ -193,7 +193,7 @@ test_expect_success 'fsck no garbage output from comments & empty lines errors'
 test_expect_success 'fsck with invalid abbreviated skipList input' '
        echo $commit | test_copy_bytes 20 >SKIP.abbreviated &&
        test_must_fail git -c fsck.skipList=SKIP.abbreviated fsck 2>err-abbreviated &&
-       test_i18ngrep "^fatal: Invalid SHA-1: " err-abbreviated
+       test_i18ngrep "^fatal: invalid object name: " err-abbreviated
 '
 
 test_expect_success 'fsck with exhaustive accepted skipList input (various types of comments etc.)' '
@@ -226,10 +226,10 @@ test_expect_success 'push with receive.fsck.skipList' '
        test_must_fail git push --porcelain dst bogus &&
        git --git-dir=dst/.git config receive.fsck.skipList does-not-exist &&
        test_must_fail git push --porcelain dst bogus 2>err &&
-       test_i18ngrep "Could not open skip list: does-not-exist" err &&
+       test_i18ngrep "could not open.*: does-not-exist" err &&
        git --git-dir=dst/.git config receive.fsck.skipList config &&
        test_must_fail git push --porcelain dst bogus 2>err &&
-       test_i18ngrep "Invalid SHA-1: \[core\]" err &&
+       test_i18ngrep "invalid object name: \[core\]" err &&
 
        git --git-dir=dst/.git config receive.fsck.skipList SKIP &&
        git push --porcelain dst bogus
@@ -255,10 +255,10 @@ test_expect_success 'fetch with fetch.fsck.skipList' '
        test_must_fail git --git-dir=dst/.git fetch "file://$(pwd)" $refspec &&
        git --git-dir=dst/.git config fetch.fsck.skipList does-not-exist &&
        test_must_fail git --git-dir=dst/.git fetch "file://$(pwd)" $refspec 2>err &&
-       test_i18ngrep "Could not open skip list: does-not-exist" err &&
+       test_i18ngrep "could not open.*: does-not-exist" err &&
        git --git-dir=dst/.git config fetch.fsck.skipList dst/.git/config &&
        test_must_fail git --git-dir=dst/.git fetch "file://$(pwd)" $refspec 2>err &&
-       test_i18ngrep "Invalid SHA-1: \[core\]" err &&
+       test_i18ngrep "invalid object name: \[core\]" err &&
 
        git --git-dir=dst/.git config fetch.fsck.skipList dst/.git/SKIP &&
        git --git-dir=dst/.git fetch "file://$(pwd)" $refspec
index e3c4a48c8536e46e0aa36b4d862b3876617335a4..43e1d8d4d2a45c5cff87d64b32ccc2814f5579e3 100755 (executable)
@@ -267,8 +267,7 @@ test_expect_success 'ls-remote --symref omits filtered-out matches' '
 '
 
 test_lazy_prereq GIT_DAEMON '
-       test_tristate GIT_TEST_GIT_DAEMON &&
-       test "$GIT_TEST_GIT_DAEMON" != false
+       git env--helper --type=bool --default=true --exit-code GIT_TEST_GIT_DAEMON
 '
 
 # This test spawns a daemon, so run it only if the user would be OK with
index 2e4802e2063b87bc6c36d2e728195036e47d8edc..b86ddb60f2ea6ec1c177b8be5d056d064a1d7697 100755 (executable)
@@ -177,6 +177,55 @@ test_expect_success 'push (chunked)' '
         test $HEAD = $(git rev-parse --verify HEAD))
 '
 
+test_expect_success 'push --atomic also prevents branch creation, reports collateral' '
+       # Setup upstream repo - empty for now
+       d=$HTTPD_DOCUMENT_ROOT_PATH/atomic-branches.git &&
+       git init --bare "$d" &&
+       test_config -C "$d" http.receivepack true &&
+       up="$HTTPD_URL"/smart/atomic-branches.git &&
+
+       # Tell "$up" about two branches for now
+       test_commit atomic1 &&
+       test_commit atomic2 &&
+       git branch collateral &&
+       git push "$up" master collateral &&
+
+       # collateral is a valid push, but should be failed by atomic push
+       git checkout collateral &&
+       test_commit collateral1 &&
+
+       # Make master incompatible with upstream to provoke atomic
+       git checkout master &&
+       git reset --hard HEAD^ &&
+
+       # Add a new branch which should be failed by atomic push. This is a
+       # regression case.
+       git branch atomic &&
+
+       # --atomic should cause entire push to be rejected
+       test_must_fail git push --atomic "$up" master atomic collateral 2>output &&
+
+       # the new branch should not have been created upstream
+       test_must_fail git -C "$d" show-ref --verify refs/heads/atomic &&
+
+       # upstream should still reflect atomic2, the last thing we pushed
+       # successfully
+       git rev-parse atomic2 >expected &&
+       # on master...
+       git -C "$d" rev-parse refs/heads/master >actual &&
+       test_cmp expected actual &&
+       # ...and collateral.
+       git -C "$d" rev-parse refs/heads/collateral >actual &&
+       test_cmp expected actual &&
+
+       # the failed refs should be indicated to the user
+       grep "^ ! .*rejected.* master -> master" output &&
+
+       # the collateral failure refs should be indicated to the user
+       grep "^ ! .*rejected.* atomic -> atomic .*atomic push failed" output &&
+       grep "^ ! .*rejected.* collateral -> collateral .*atomic push failed" output
+'
+
 test_expect_success 'push --all can push to empty repo' '
        d=$HTTPD_DOCUMENT_ROOT_PATH/empty-all.git &&
        git init --bare "$d" &&
diff --git a/t/t5618-alternate-refs.sh b/t/t5618-alternate-refs.sh
new file mode 100755 (executable)
index 0000000..3353216
--- /dev/null
@@ -0,0 +1,60 @@
+#!/bin/sh
+
+test_description='test handling of --alternate-refs traversal'
+. ./test-lib.sh
+
+# Avoid test_commit because we want a specific and known set of refs:
+#
+#  base -- one
+#      \      \
+#       two -- merged
+#
+# where "one" and "two" are on separate refs, and "merged" is available only in
+# the dependent child repository.
+test_expect_success 'set up local refs' '
+       git checkout -b one &&
+       test_tick &&
+       git commit --allow-empty -m base &&
+       test_tick &&
+       git commit --allow-empty -m one &&
+       git checkout -b two HEAD^ &&
+       test_tick &&
+       git commit --allow-empty -m two
+'
+
+# We'll enter the child repository after it's set up since that's where
+# all of the subsequent tests will want to run (and it's easy to forget a
+# "-C child" and get nonsense results).
+test_expect_success 'set up shared clone' '
+       git clone -s . child &&
+       cd child &&
+       git merge origin/one
+'
+
+test_expect_success 'rev-list --alternate-refs' '
+       git rev-list --remotes=origin >expect &&
+       git rev-list --alternate-refs >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'rev-list --not --alternate-refs' '
+       git rev-parse HEAD >expect &&
+       git rev-list HEAD --not --alternate-refs >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'limiting with alternateRefsPrefixes' '
+       test_config core.alternateRefsPrefixes refs/heads/one &&
+       git rev-list origin/one >expect &&
+       git rev-list --alternate-refs >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'log --source shows .alternate marker' '
+       git log --oneline --source --remotes=origin >expect.orig &&
+       sed "s/origin.* /.alternate /" <expect.orig >expect &&
+       git log --oneline --source --alternate-refs >actual &&
+       test_cmp expect actual
+'
+
+test_done
index febf63f28a54cd6a1e54e7322830e3ddfbfa9af3..ad1922b999b18d8437cc48c139a862ab5f59425d 100755 (executable)
@@ -38,7 +38,7 @@ test_expect_success setup '
        advance h
 '
 
-script='s/^..\(b.\) *[0-9a-f]* \(.*\)$/\1 \2/p'
+t6040_script='s/^..\(b.\) *[0-9a-f]* \(.*\)$/\1 \2/p'
 cat >expect <<\EOF
 b1 [ahead 1, behind 1] d
 b2 [ahead 1, behind 1] d
@@ -53,7 +53,7 @@ test_expect_success 'branch -v' '
                cd test &&
                git branch -v
        ) |
-       sed -n -e "$script" >actual &&
+       sed -n -e "$t6040_script" >actual &&
        test_i18ncmp expect actual
 '
 
@@ -71,7 +71,7 @@ test_expect_success 'branch -vv' '
                cd test &&
                git branch -vv
        ) |
-       sed -n -e "$script" >actual &&
+       sed -n -e "$t6040_script" >actual &&
        test_i18ncmp expect actual
 '
 
index d9235217fcc72912574c80cea684aa17c8b736d2..ab69aa176d14bf2d8dd88be1d7cbae0560609be8 100755 (executable)
@@ -345,6 +345,32 @@ test_expect_success 'Verify descending sort' '
        test_cmp expected actual
 '
 
+cat >expected <<\EOF
+refs/tags/testtag
+refs/tags/testtag-2
+EOF
+
+test_expect_success 'exercise patterns with prefixes' '
+       git tag testtag-2 &&
+       test_when_finished "git tag -d testtag-2" &&
+       git for-each-ref --format="%(refname)" \
+               refs/tags/testtag refs/tags/testtag-2 >actual &&
+       test_cmp expected actual
+'
+
+cat >expected <<\EOF
+refs/tags/testtag
+refs/tags/testtag-2
+EOF
+
+test_expect_success 'exercise glob patterns with prefixes' '
+       git tag testtag-2 &&
+       test_when_finished "git tag -d testtag-2" &&
+       git for-each-ref --format="%(refname)" \
+               refs/tags/testtag "refs/tags/testtag-*" >actual &&
+       test_cmp expected actual
+'
+
 cat >expected <<\EOF
 'refs/heads/master'
 'refs/remotes/origin/master'
index 53cf42fac19c83f2da7de1cd5b6c4ca0cf6e5ccf..d5218743e963bd7e68788946c686e0a45c7d8a2f 100755 (executable)
@@ -38,7 +38,6 @@ You have unmerged paths.
 
 Unmerged paths:
   (use "git add/rm <file>..." as appropriate to mark resolution)
-
        deleted by us:   foo
 
 no changes added to commit (use "git add" and/or "git commit -a")
@@ -143,7 +142,6 @@ You have unmerged paths.
 
 Unmerged paths:
   (use "git add/rm <file>..." as appropriate to mark resolution)
-
        both added:      conflict.txt
        deleted by them: main.txt
 
@@ -177,7 +175,6 @@ You have unmerged paths.
 
 Unmerged paths:
   (use "git add/rm <file>..." as appropriate to mark resolution)
-
        both deleted:    main.txt
        added by them:   sub_master.txt
        added by us:     sub_second.txt
@@ -201,12 +198,10 @@ You have unmerged paths.
   (use "git merge --abort" to abort the merge)
 
 Changes to be committed:
-
        new file:   sub_master.txt
 
 Unmerged paths:
   (use "git rm <file>..." to mark resolution)
-
        both deleted:    main.txt
 
 Untracked files not listed (use -u option to show untracked files)
index 5990299fc9555d54a59a5de462864a7a98e83e45..b696bae5f534e82609f8dd0d27e6f9abb85cd7cc 100755 (executable)
@@ -249,7 +249,7 @@ test_expect_success 'checkout to detach HEAD (with advice declined)' '
 test_expect_success 'checkout to detach HEAD' '
        git config advice.detachedHead true &&
        git checkout -f renamer && git clean -f &&
-       GIT_TEST_GETTEXT_POISON= git checkout renamer^ 2>messages &&
+       GIT_TEST_GETTEXT_POISON=false git checkout renamer^ 2>messages &&
        grep "HEAD is now at 7329388" messages &&
        test_line_count -gt 1 messages &&
        H=$(git rev-parse --verify HEAD) &&
index 681bc314b483d61c145b450c195da7449bd413cc..4e676cdce8d621c86b4f758bf9535efb17735f46 100755 (executable)
@@ -95,18 +95,15 @@ test_expect_success 'status --column' '
 #
 # Changes to be committed:
 #   (use "git restore --staged <file>..." to unstage)
-#
 #      new file:   dir2/added
 #
 # Changes not staged for commit:
 #   (use "git add <file>..." to update what will be committed)
 #   (use "git restore <file>..." to discard changes in working directory)
-#
 #      modified:   dir1/modified
 #
 # Untracked files:
 #   (use "git add <file>..." to include in what will be committed)
-#
 #      dir1/untracked dir2/untracked
 #      dir2/modified  untracked
 #
@@ -129,18 +126,15 @@ cat >expect <<\EOF
 #
 # Changes to be committed:
 #   (use "git restore --staged <file>..." to unstage)
-#
 #      new file:   dir2/added
 #
 # Changes not staged for commit:
 #   (use "git add <file>..." to update what will be committed)
 #   (use "git restore <file>..." to discard changes in working directory)
-#
 #      modified:   dir1/modified
 #
 # Untracked files:
 #   (use "git add <file>..." to include in what will be committed)
-#
 #      dir1/untracked
 #      dir2/modified
 #      dir2/untracked
@@ -279,23 +273,19 @@ and have 1 and 2 different commits each, respectively.
 
 Changes to be committed:
   (use "git restore --staged <file>..." to unstage)
-
        new file:   dir2/added
 
 Changes not staged for commit:
   (use "git add <file>..." to update what will be committed)
   (use "git restore <file>..." to discard changes in working directory)
-
        modified:   dir1/modified
 
 Untracked files:
   (use "git add <file>..." to include in what will be committed)
-
        dir2/modified
 
 Ignored files:
   (use "git add -f <file>..." to include in what will be committed)
-
        .gitignore
        dir1/untracked
        dir2/untracked
@@ -348,18 +338,15 @@ and have 1 and 2 different commits each, respectively.
 
 Changes to be committed:
   (use "git restore --staged <file>..." to unstage)
-
        new file:   dir2/added
 
 Changes not staged for commit:
   (use "git add <file>..." to update what will be committed)
   (use "git restore <file>..." to discard changes in working directory)
-
        modified:   dir1/modified
 
 Ignored files:
   (use "git add -f <file>..." to include in what will be committed)
-
        .gitignore
        dir1/untracked
        dir2/modified
@@ -421,13 +408,11 @@ and have 1 and 2 different commits each, respectively.
 
 Changes to be committed:
   (use "git restore --staged <file>..." to unstage)
-
        new file:   dir2/added
 
 Changes not staged for commit:
   (use "git add <file>..." to update what will be committed)
   (use "git restore <file>..." to discard changes in working directory)
-
        modified:   dir1/modified
 
 Untracked files not listed (use -u option to show untracked files)
@@ -485,18 +470,15 @@ and have 1 and 2 different commits each, respectively.
 
 Changes to be committed:
   (use "git restore --staged <file>..." to unstage)
-
        new file:   dir2/added
 
 Changes not staged for commit:
   (use "git add <file>..." to update what will be committed)
   (use "git restore <file>..." to discard changes in working directory)
-
        modified:   dir1/modified
 
 Untracked files:
   (use "git add <file>..." to include in what will be committed)
-
        dir1/untracked
        dir2/modified
        dir2/untracked
@@ -543,18 +525,15 @@ and have 1 and 2 different commits each, respectively.
 
 Changes to be committed:
   (use "git restore --staged <file>..." to unstage)
-
        new file:   dir2/added
 
 Changes not staged for commit:
   (use "git add <file>..." to update what will be committed)
   (use "git restore <file>..." to discard changes in working directory)
-
        modified:   dir1/modified
 
 Untracked files:
   (use "git add <file>..." to include in what will be committed)
-
        dir1/untracked
        dir2/modified
        dir2/untracked
@@ -606,18 +585,15 @@ and have 1 and 2 different commits each, respectively.
 
 Changes to be committed:
   (use "git restore --staged <file>..." to unstage)
-
        new file:   ../dir2/added
 
 Changes not staged for commit:
   (use "git add <file>..." to update what will be committed)
   (use "git restore <file>..." to discard changes in working directory)
-
        modified:   modified
 
 Untracked files:
   (use "git add <file>..." to include in what will be committed)
-
        untracked
        ../dir2/modified
        ../dir2/untracked
@@ -677,18 +653,15 @@ and have 1 and 2 different commits each, respectively.
 
 Changes to be committed:
   (use "git restore --staged <file>..." to unstage)
-
        <GREEN>new file:   dir2/added<RESET>
 
 Changes not staged for commit:
   (use "git add <file>..." to update what will be committed)
   (use "git restore <file>..." to discard changes in working directory)
-
        <RED>modified:   dir1/modified<RESET>
 
 Untracked files:
   (use "git add <file>..." to include in what will be committed)
-
        <BLUE>dir1/untracked<RESET>
        <BLUE>dir2/modified<RESET>
        <BLUE>dir2/untracked<RESET>
@@ -803,18 +776,15 @@ and have 1 and 2 different commits each, respectively.
 
 Changes to be committed:
   (use "git restore --staged <file>..." to unstage)
-
        new file:   dir2/added
 
 Changes not staged for commit:
   (use "git add <file>..." to update what will be committed)
   (use "git restore <file>..." to discard changes in working directory)
-
        modified:   dir1/modified
 
 Untracked files:
   (use "git add <file>..." to include in what will be committed)
-
        dir1/untracked
        dir2/modified
        dir2/untracked
@@ -853,12 +823,10 @@ and have 1 and 2 different commits each, respectively.
 
 Changes to be committed:
   (use "git restore --staged <file>..." to unstage)
-
        modified:   dir1/modified
 
 Untracked files:
   (use "git add <file>..." to include in what will be committed)
-
        dir1/untracked
        dir2/
        untracked
@@ -897,19 +865,16 @@ and have 1 and 2 different commits each, respectively.
 
 Changes to be committed:
   (use "git restore --staged <file>..." to unstage)
-
        new file:   dir2/added
        new file:   sm
 
 Changes not staged for commit:
   (use "git add <file>..." to update what will be committed)
   (use "git restore <file>..." to discard changes in working directory)
-
        modified:   dir1/modified
 
 Untracked files:
   (use "git add <file>..." to include in what will be committed)
-
        dir1/untracked
        dir2/modified
        dir2/untracked
@@ -957,14 +922,12 @@ and have 1 and 2 different commits each, respectively.
 
 Changes to be committed:
   (use "git restore --staged <file>..." to unstage)
-
        new file:   dir2/added
        new file:   sm
 
 Changes not staged for commit:
   (use "git add <file>..." to update what will be committed)
   (use "git restore <file>..." to discard changes in working directory)
-
        modified:   dir1/modified
 
 Submodule changes to be committed:
@@ -974,7 +937,6 @@ Submodule changes to be committed:
 
 Untracked files:
   (use "git add <file>..." to include in what will be committed)
-
        dir1/untracked
        dir2/modified
        dir2/untracked
@@ -1020,12 +982,10 @@ and have 2 and 2 different commits each, respectively.
 Changes not staged for commit:
   (use "git add <file>..." to update what will be committed)
   (use "git restore <file>..." to discard changes in working directory)
-
        modified:   dir1/modified
 
 Untracked files:
   (use "git add <file>..." to include in what will be committed)
-
        dir1/untracked
        dir2/modified
        dir2/untracked
@@ -1069,14 +1029,12 @@ and have 2 and 2 different commits each, respectively.
 
 Changes to be committed:
   (use "git restore --source=HEAD^1 --staged <file>..." to unstage)
-
        new file:   dir2/added
        new file:   sm
 
 Changes not staged for commit:
   (use "git add <file>..." to update what will be committed)
   (use "git restore <file>..." to discard changes in working directory)
-
        modified:   dir1/modified
 
 Submodule changes to be committed:
@@ -1086,7 +1044,6 @@ Submodule changes to be committed:
 
 Untracked files:
   (use "git add <file>..." to include in what will be committed)
-
        dir1/untracked
        dir2/modified
        dir2/untracked
@@ -1124,13 +1081,11 @@ and have 2 and 2 different commits each, respectively.
 
 Changes to be committed:
   (use "git restore --staged <file>..." to unstage)
-
        modified:   sm
 
 Changes not staged for commit:
   (use "git add <file>..." to update what will be committed)
   (use "git restore <file>..." to discard changes in working directory)
-
        modified:   dir1/modified
 
 Submodule changes to be committed:
@@ -1140,7 +1095,6 @@ Submodule changes to be committed:
 
 Untracked files:
   (use "git add <file>..." to include in what will be committed)
-
        .gitmodules
        dir1/untracked
        dir2/modified
@@ -1236,14 +1190,12 @@ and have 2 and 2 different commits each, respectively.
 
 Changes to be committed:
   (use "git restore --staged <file>..." to unstage)
-
        modified:   sm
 
 Changes not staged for commit:
   (use "git add <file>..." to update what will be committed)
   (use "git restore <file>..." to discard changes in working directory)
   (commit or discard the untracked or modified content in submodules)
-
        modified:   dir1/modified
        modified:   sm (modified content)
 
@@ -1254,7 +1206,6 @@ Submodule changes to be committed:
 
 Untracked files:
   (use "git add <file>..." to include in what will be committed)
-
        .gitmodules
        dir1/untracked
        dir2/modified
@@ -1296,13 +1247,11 @@ and have 2 and 2 different commits each, respectively.
 
 Changes to be committed:
   (use "git restore --staged <file>..." to unstage)
-
        modified:   sm
 
 Changes not staged for commit:
   (use "git add <file>..." to update what will be committed)
   (use "git restore <file>..." to discard changes in working directory)
-
        modified:   dir1/modified
        modified:   sm (new commits)
 
@@ -1318,7 +1267,6 @@ Submodules changed but not updated:
 
 Untracked files:
   (use "git add <file>..." to include in what will be committed)
-
        .gitmodules
        dir1/untracked
        dir2/modified
@@ -1380,13 +1328,11 @@ cat > expect << EOF
 ;
 ; Changes to be committed:
 ;   (use "git restore --staged <file>..." to unstage)
-;
 ;      modified:   sm
 ;
 ; Changes not staged for commit:
 ;   (use "git add <file>..." to update what will be committed)
 ;   (use "git restore <file>..." to discard changes in working directory)
-;
 ;      modified:   dir1/modified
 ;      modified:   sm (new commits)
 ;
@@ -1402,7 +1348,6 @@ cat > expect << EOF
 ;
 ; Untracked files:
 ;   (use "git add <file>..." to include in what will be committed)
-;
 ;      .gitmodules
 ;      dir1/untracked
 ;      dir2/modified
@@ -1432,12 +1377,10 @@ and have 2 and 2 different commits each, respectively.
 Changes not staged for commit:
   (use "git add <file>..." to update what will be committed)
   (use "git restore <file>..." to discard changes in working directory)
-
        modified:   dir1/modified
 
 Untracked files:
   (use "git add <file>..." to include in what will be committed)
-
        .gitmodules
        dir1/untracked
        dir2/modified
@@ -1459,18 +1402,15 @@ and have 2 and 2 different commits each, respectively.
 
 Changes to be committed:
   (use "git restore --staged <file>..." to unstage)
-
        modified:   sm
 
 Changes not staged for commit:
   (use "git add <file>..." to update what will be committed)
   (use "git restore <file>..." to discard changes in working directory)
-
        modified:   dir1/modified
 
 Untracked files:
   (use "git add <file>..." to include in what will be committed)
-
        .gitmodules
        dir1/untracked
        dir2/modified
@@ -1582,13 +1522,11 @@ and have 2 and 2 different commits each, respectively.
 
 Changes to be committed:
   (use "git restore --staged <file>..." to unstage)
-
        modified:   sm
 
 Changes not staged for commit:
   (use "git add <file>..." to update what will be committed)
   (use "git restore <file>..." to discard changes in working directory)
-
        modified:   dir1/modified
 
 Untracked files not listed (use -u option to show untracked files)
index b9f5d73423cfec8e61e10b6b42e7f8dc237a37b2..e01c285cbf8fdaa511240858f9f43165d149bc7e 100755 (executable)
@@ -33,7 +33,6 @@ You have unmerged paths.
 
 Unmerged paths:
   (use "git add <file>..." to mark resolution)
-
        both modified:   main.txt
 
 no changes added to commit (use "git add" and/or "git commit -a")
@@ -54,7 +53,6 @@ All conflicts fixed but you are still merging.
   (use "git commit" to conclude merge)
 
 Changes to be committed:
-
        modified:   main.txt
 
 Untracked files not listed (use -u option to show untracked files)
@@ -87,7 +85,6 @@ You are currently rebasing branch '\''rebase_conflicts'\'' on '\''$ONTO'\''.
 Unmerged paths:
   (use "git restore --staged <file>..." to unstage)
   (use "git add <file>..." to mark resolution)
-
        both modified:   main.txt
 
 no changes added to commit (use "git add" and/or "git commit -a")
@@ -111,7 +108,6 @@ You are currently rebasing branch '\''rebase_conflicts'\'' on '\''$ONTO'\''.
 
 Changes to be committed:
   (use "git restore --staged <file>..." to unstage)
-
        modified:   main.txt
 
 Untracked files not listed (use -u option to show untracked files)
@@ -150,7 +146,6 @@ You are currently rebasing branch '\''rebase_i_conflicts_second'\'' on '\''$ONTO
 Unmerged paths:
   (use "git restore --staged <file>..." to unstage)
   (use "git add <file>..." to mark resolution)
-
        both modified:   main.txt
 
 no changes added to commit (use "git add" and/or "git commit -a")
@@ -177,7 +172,6 @@ You are currently rebasing branch '\''rebase_i_conflicts_second'\'' on '\''$ONTO
 
 Changes to be committed:
   (use "git restore --staged <file>..." to unstage)
-
        modified:   main.txt
 
 Untracked files not listed (use -u option to show untracked files)
@@ -247,7 +241,6 @@ You are currently splitting a commit while rebasing branch '\''split_commit'\''
 Changes not staged for commit:
   (use "git add <file>..." to update what will be committed)
   (use "git restore <file>..." to discard changes in working directory)
-
        modified:   main.txt
 
 no changes added to commit (use "git add" and/or "git commit -a")
@@ -355,7 +348,6 @@ You are currently splitting a commit while rebasing branch '\''several_edits'\''
 Changes not staged for commit:
   (use "git add <file>..." to update what will be committed)
   (use "git restore <file>..." to discard changes in working directory)
-
        modified:   main.txt
 
 no changes added to commit (use "git add" and/or "git commit -a")
@@ -454,7 +446,6 @@ You are currently splitting a commit while rebasing branch '\''several_edits'\''
 Changes not staged for commit:
   (use "git add <file>..." to update what will be committed)
   (use "git restore <file>..." to discard changes in working directory)
-
        modified:   main.txt
 
 no changes added to commit (use "git add" and/or "git commit -a")
@@ -558,7 +549,6 @@ You are currently splitting a commit while rebasing branch '\''several_edits'\''
 Changes not staged for commit:
   (use "git add <file>..." to update what will be committed)
   (use "git restore <file>..." to discard changes in working directory)
-
        modified:   main.txt
 
 no changes added to commit (use "git add" and/or "git commit -a")
@@ -747,7 +737,6 @@ You are currently cherry-picking commit $TO_CHERRY_PICK.
 
 Unmerged paths:
   (use "git add <file>..." to mark resolution)
-
        both modified:   main.txt
 
 no changes added to commit (use "git add" and/or "git commit -a")
@@ -771,7 +760,6 @@ You are currently cherry-picking commit $TO_CHERRY_PICK.
   (use "git cherry-pick --abort" to cancel the cherry-pick operation)
 
 Changes to be committed:
-
        modified:   main.txt
 
 Untracked files not listed (use -u option to show untracked files)
@@ -798,6 +786,22 @@ EOF
        test_i18ncmp expected actual
 '
 
+test_expect_success 'status shows cherry-pick with invalid oid' '
+       mkdir .git/sequencer &&
+       test_write_lines "pick invalid-oid" >.git/sequencer/todo &&
+       git status --untracked-files=no >actual 2>err &&
+       git cherry-pick --quit &&
+       test_must_be_empty err &&
+       test_i18ncmp expected actual
+'
+
+test_expect_success 'status does not show error if .git/sequencer is a file' '
+       test_when_finished "rm .git/sequencer" &&
+       test_write_lines hello >.git/sequencer &&
+       git status --untracked-files=no 2>err &&
+       test_must_be_empty err
+'
+
 test_expect_success 'status showing detached at and from a tag' '
        test_commit atag tagging &&
        git checkout atag &&
@@ -836,7 +840,6 @@ You are currently reverting commit $TO_REVERT.
 Unmerged paths:
   (use "git restore --staged <file>..." to unstage)
   (use "git add <file>..." to mark resolution)
-
        both modified:   to-revert.txt
 
 no changes added to commit (use "git add" and/or "git commit -a")
@@ -856,7 +859,6 @@ You are currently reverting commit $TO_REVERT.
 
 Changes to be committed:
   (use "git restore --staged <file>..." to unstage)
-
        modified:   to-revert.txt
 
 Untracked files not listed (use -u option to show untracked files)
index 86d05160a3589cefd7ad21dbf64c655658f26342..0e9af832c9790f3e329fc59d61b8bc177c151b48 100755 (executable)
@@ -239,4 +239,14 @@ test_expect_success 'bitmaps can be disabled on bare repos' '
        test -z "$bitmap"
 '
 
+test_expect_success 'no bitmaps created if .keep files present' '
+       pack=$(ls bare.git/objects/pack/*.pack) &&
+       test_path_is_file "$pack" &&
+       keep=${pack%.pack}.keep &&
+       >"$keep" &&
+       git -C bare.git repack -ad &&
+       find bare.git/objects/pack/ -type f -name "*.bitmap" >actual &&
+       test_must_be_empty actual
+'
+
 test_done
index 134a694516c924c5930b9c227da45e896297a338..a11366b4cee44da8206852f52fe749816443b34f 100755 (executable)
@@ -14,12 +14,14 @@ test_expect_success 'setup directory structure and submodule' '
        echo "(3|4)" >b/b &&
        git add a b &&
        git commit -m "add a and b" &&
+       test_tick &&
        git init submodule &&
        echo "(1|2)d(3|4)" >submodule/a &&
        git -C submodule add a &&
        git -C submodule commit -m "add a" &&
        git submodule add ./submodule &&
-       git commit -m "added submodule"
+       git commit -m "added submodule" &&
+       test_tick
 '
 
 test_expect_success 'grep correctly finds patterns in a submodule' '
@@ -65,11 +67,14 @@ test_expect_success 'grep and nested submodules' '
        echo "(1|2)d(3|4)" >submodule/sub/a &&
        git -C submodule/sub add a &&
        git -C submodule/sub commit -m "add a" &&
+       test_tick &&
        git -C submodule submodule add ./sub &&
        git -C submodule add sub &&
        git -C submodule commit -m "added sub" &&
+       test_tick &&
        git add submodule &&
        git commit -m "updated submodule" &&
+       test_tick &&
 
        cat >expect <<-\EOF &&
        a:(1|2)d(3|4)
@@ -179,15 +184,18 @@ test_expect_success !MINGW 'grep recurse submodule colon in name' '
        echo "(1|2)d(3|4)" >"parent/fi:le" &&
        git -C parent add "fi:le" &&
        git -C parent commit -m "add fi:le" &&
+       test_tick &&
 
        git init "su:b" &&
        test_when_finished "rm -rf su:b" &&
        echo "(1|2)d(3|4)" >"su:b/fi:le" &&
        git -C "su:b" add "fi:le" &&
        git -C "su:b" commit -m "add fi:le" &&
+       test_tick &&
 
        git -C parent submodule add "../su:b" "su:b" &&
        git -C parent commit -m "add submodule" &&
+       test_tick &&
 
        cat >expect <<-\EOF &&
        fi:le:(1|2)d(3|4)
@@ -210,15 +218,18 @@ test_expect_success 'grep history with moved submoules' '
        echo "(1|2)d(3|4)" >parent/file &&
        git -C parent add file &&
        git -C parent commit -m "add file" &&
+       test_tick &&
 
        git init sub &&
        test_when_finished "rm -rf sub" &&
        echo "(1|2)d(3|4)" >sub/file &&
        git -C sub add file &&
        git -C sub commit -m "add file" &&
+       test_tick &&
 
        git -C parent submodule add ../sub dir/sub &&
        git -C parent commit -m "add submodule" &&
+       test_tick &&
 
        cat >expect <<-\EOF &&
        dir/sub/file:(1|2)d(3|4)
@@ -229,6 +240,7 @@ test_expect_success 'grep history with moved submoules' '
 
        git -C parent mv dir/sub sub-moved &&
        git -C parent commit -m "moved submodule" &&
+       test_tick &&
 
        cat >expect <<-\EOF &&
        file:(1|2)d(3|4)
@@ -251,6 +263,7 @@ test_expect_success 'grep using relative path' '
        echo "(1|2)d(3|4)" >sub/file &&
        git -C sub add file &&
        git -C sub commit -m "add file" &&
+       test_tick &&
 
        git init parent &&
        echo "(1|2)d(3|4)" >parent/file &&
@@ -260,6 +273,7 @@ test_expect_success 'grep using relative path' '
        git -C parent add src/file2 &&
        git -C parent submodule add ../sub &&
        git -C parent commit -m "add files and submodule" &&
+       test_tick &&
 
        # From top works
        cat >expect <<-\EOF &&
@@ -293,6 +307,7 @@ test_expect_success 'grep from a subdir' '
        echo "(1|2)d(3|4)" >sub/file &&
        git -C sub add file &&
        git -C sub commit -m "add file" &&
+       test_tick &&
 
        git init parent &&
        mkdir parent/src &&
@@ -301,6 +316,7 @@ test_expect_success 'grep from a subdir' '
        git -C parent submodule add ../sub src/sub &&
        git -C parent submodule add ../sub sub &&
        git -C parent commit -m "add files and submodules" &&
+       test_tick &&
 
        # Verify grep from root works
        cat >expect <<-\EOF &&
index c92a47b6d5b11ab537ce9892ca21feddc19db7d9..1c5fb1d1f8c9cd9062ae44c6069fc530259762d4 100755 (executable)
@@ -275,4 +275,40 @@ test_expect_success 'blame file with CRLF core.autocrlf=true' '
        grep "A U Thor" actual
 '
 
+# Tests the splitting and merging of blame entries in blame_coalesce().
+# The output of blame is the same, regardless of whether blame_coalesce() runs
+# or not, so we'd likely only notice a problem if blame crashes or assigned
+# blame to the "splitting" commit ('SPLIT' below).
+test_expect_success 'blame coalesce' '
+       cat >giraffe <<-\EOF &&
+       ABC
+       DEF
+       EOF
+       git add giraffe &&
+       git commit -m "original file" &&
+       oid=$(git rev-parse HEAD) &&
+
+       cat >giraffe <<-\EOF &&
+       ABC
+       SPLIT
+       DEF
+       EOF
+       git add giraffe &&
+       git commit -m "interior SPLIT line" &&
+
+       cat >giraffe <<-\EOF &&
+       ABC
+       DEF
+       EOF
+       git add giraffe &&
+       git commit -m "same contents as original" &&
+
+       cat >expect <<-EOF &&
+       $oid 1) ABC
+       $oid 2) DEF
+       EOF
+       git -c core.abbrev=40 blame -s giraffe >actual &&
+       test_cmp expect actual
+'
+
 test_done
diff --git a/t/t8013-blame-ignore-revs.sh b/t/t8013-blame-ignore-revs.sh
new file mode 100755 (executable)
index 0000000..36dc31e
--- /dev/null
@@ -0,0 +1,274 @@
+#!/bin/sh
+
+test_description='ignore revisions when blaming'
+. ./test-lib.sh
+
+# Creates:
+#      A--B--X
+# A added line 1 and B added line 2.  X makes changes to those lines.  Sanity
+# check that X is blamed for both lines.
+test_expect_success setup '
+       test_commit A file line1 &&
+
+       echo line2 >>file &&
+       git add file &&
+       test_tick &&
+       git commit -m B &&
+       git tag B &&
+
+       test_write_lines line-one line-two >file &&
+       git add file &&
+       test_tick &&
+       git commit -m X &&
+       git tag X &&
+
+       git blame --line-porcelain file >blame_raw &&
+
+       grep -E "^[0-9a-f]+ [0-9]+ 1" blame_raw | sed -e "s/ .*//" >actual &&
+       git rev-parse X >expect &&
+       test_cmp expect actual &&
+
+       grep -E "^[0-9a-f]+ [0-9]+ 2" blame_raw | sed -e "s/ .*//" >actual &&
+       git rev-parse X >expect &&
+       test_cmp expect actual
+       '
+
+# Ignore X, make sure A is blamed for line 1 and B for line 2.
+test_expect_success ignore_rev_changing_lines '
+       git blame --line-porcelain --ignore-rev X file >blame_raw &&
+
+       grep -E "^[0-9a-f]+ [0-9]+ 1" blame_raw | sed -e "s/ .*//" >actual &&
+       git rev-parse A >expect &&
+       test_cmp expect actual &&
+
+       grep -E "^[0-9a-f]+ [0-9]+ 2" blame_raw | sed -e "s/ .*//" >actual &&
+       git rev-parse B >expect &&
+       test_cmp expect actual
+       '
+
+# For ignored revs that have added 'unblamable' lines, attribute those to the
+# ignored commit.
+#      A--B--X--Y
+# Where Y changes lines 1 and 2, and adds lines 3 and 4.  The added lines ought
+# to have nothing in common with "line-one" or "line-two", to keep any
+# heuristics from matching them with any lines in the parent.
+test_expect_success ignore_rev_adding_unblamable_lines '
+       test_write_lines line-one-change line-two-changed y3 y4 >file &&
+       git add file &&
+       test_tick &&
+       git commit -m Y &&
+       git tag Y &&
+
+       git rev-parse Y >expect &&
+       git blame --line-porcelain file --ignore-rev Y >blame_raw &&
+
+       grep -E "^[0-9a-f]+ [0-9]+ 3" blame_raw | sed -e "s/ .*//" >actual &&
+       test_cmp expect actual &&
+
+       grep -E "^[0-9a-f]+ [0-9]+ 4" blame_raw | sed -e "s/ .*//" >actual &&
+       test_cmp expect actual
+       '
+
+# Ignore X and Y, both in separate files.  Lines 1 == A, 2 == B.
+test_expect_success ignore_revs_from_files '
+       git rev-parse X >ignore_x &&
+       git rev-parse Y >ignore_y &&
+       git blame --line-porcelain file --ignore-revs-file ignore_x --ignore-revs-file ignore_y >blame_raw &&
+
+       grep -E "^[0-9a-f]+ [0-9]+ 1" blame_raw | sed -e "s/ .*//" >actual &&
+       git rev-parse A >expect &&
+       test_cmp expect actual &&
+
+       grep -E "^[0-9a-f]+ [0-9]+ 2" blame_raw | sed -e "s/ .*//" >actual &&
+       git rev-parse B >expect &&
+       test_cmp expect actual
+       '
+
+# Ignore X from the config option, Y from a file.
+test_expect_success ignore_revs_from_configs_and_files '
+       git config --add blame.ignoreRevsFile ignore_x &&
+       git blame --line-porcelain file --ignore-revs-file ignore_y >blame_raw &&
+
+       grep -E "^[0-9a-f]+ [0-9]+ 1" blame_raw | sed -e "s/ .*//" >actual &&
+       git rev-parse A >expect &&
+       test_cmp expect actual &&
+
+       grep -E "^[0-9a-f]+ [0-9]+ 2" blame_raw | sed -e "s/ .*//" >actual &&
+       git rev-parse B >expect &&
+       test_cmp expect actual
+       '
+
+# Override blame.ignoreRevsFile (ignore_x) with an empty string.  X should be
+# blamed now for lines 1 and 2, since we are no longer ignoring X.
+test_expect_success override_ignore_revs_file '
+       git blame --line-porcelain file --ignore-revs-file "" --ignore-revs-file ignore_y >blame_raw &&
+       git rev-parse X >expect &&
+
+       grep -E "^[0-9a-f]+ [0-9]+ 1" blame_raw | sed -e "s/ .*//" >actual &&
+       test_cmp expect actual &&
+
+       grep -E "^[0-9a-f]+ [0-9]+ 2" blame_raw | sed -e "s/ .*//" >actual &&
+       test_cmp expect actual
+       '
+test_expect_success bad_files_and_revs '
+       test_must_fail git blame file --ignore-rev NOREV 2>err &&
+       test_i18ngrep "cannot find revision NOREV to ignore" err &&
+
+       test_must_fail git blame file --ignore-revs-file NOFILE 2>err &&
+       test_i18ngrep "could not open.*: NOFILE" err &&
+
+       echo NOREV >ignore_norev &&
+       test_must_fail git blame file --ignore-revs-file ignore_norev 2>err &&
+       test_i18ngrep "invalid object name: NOREV" err
+       '
+
+# For ignored revs that have added 'unblamable' lines, mark those lines with a
+# '*'
+#      A--B--X--Y
+# Lines 3 and 4 are from Y and unblamable.  This was set up in
+# ignore_rev_adding_unblamable_lines.
+test_expect_success mark_unblamable_lines '
+       git config --add blame.markUnblamableLines true &&
+
+       git blame --ignore-rev Y file >blame_raw &&
+       echo "*" >expect &&
+
+       sed -n "3p" blame_raw | cut -c1 >actual &&
+       test_cmp expect actual &&
+
+       sed -n "4p" blame_raw | cut -c1 >actual &&
+       test_cmp expect actual
+       '
+
+# Commit Z will touch the first two lines.  Y touched all four.
+#      A--B--X--Y--Z
+# The blame output when ignoring Z should be:
+# ?Y ... 1)
+# ?Y ... 2)
+# Y  ... 3)
+# Y  ... 4)
+# We're checking only the first character
+test_expect_success mark_ignored_lines '
+       git config --add blame.markIgnoredLines true &&
+
+       test_write_lines line-one-Z line-two-Z y3 y4 >file &&
+       git add file &&
+       test_tick &&
+       git commit -m Z &&
+       git tag Z &&
+
+       git blame --ignore-rev Z file >blame_raw &&
+       echo "?" >expect &&
+
+       sed -n "1p" blame_raw | cut -c1 >actual &&
+       test_cmp expect actual &&
+
+       sed -n "2p" blame_raw | cut -c1 >actual &&
+       test_cmp expect actual &&
+
+       sed -n "3p" blame_raw | cut -c1 >actual &&
+       ! test_cmp expect actual &&
+
+       sed -n "4p" blame_raw | cut -c1 >actual &&
+       ! test_cmp expect actual
+       '
+
+# For ignored revs that added 'unblamable' lines and more recent commits changed
+# the blamable lines, mark the unblamable lines with a
+# '*'
+#      A--B--X--Y--Z
+# Lines 3 and 4 are from Y and unblamable, as set up in
+# ignore_rev_adding_unblamable_lines.  Z changed lines 1 and 2.
+test_expect_success mark_unblamable_lines_intermediate '
+       git config --add blame.markUnblamableLines true &&
+
+       git blame --ignore-rev Y file >blame_raw 2>stderr &&
+       echo "*" >expect &&
+
+       sed -n "3p" blame_raw | cut -c1 >actual &&
+       test_cmp expect actual &&
+
+       sed -n "4p" blame_raw | cut -c1 >actual &&
+       test_cmp expect actual
+       '
+
+# The heuristic called by guess_line_blames() tries to find the size of a
+# blame_entry 'e' in the parent's address space.  Those calculations need to
+# check for negative or zero values for when a blame entry is completely outside
+# the window of the parent's version of a file.
+#
+# This happens when one commit adds several lines (commit B below).  A later
+# commit (C) changes one line in the middle of B's change.  Commit C gets blamed
+# for its change, and that breaks up B's change into multiple blame entries.
+# When processing B, one of the blame_entries is outside A's window (which was
+# zero - it had no lines added on its side of the diff).
+#
+# A--B--C, ignore B to test the ignore heuristic's boundary checks.
+test_expect_success ignored_chunk_negative_parent_size '
+       rm -rf .git/ &&
+       git init &&
+
+       test_write_lines L1 L2 L7 L8 L9 >file &&
+       git add file &&
+       test_tick &&
+       git commit -m A &&
+       git tag A &&
+
+       test_write_lines L1 L2 L3 L4 L5 L6 L7 L8 L9 >file &&
+       git add file &&
+       test_tick &&
+       git commit -m B &&
+       git tag B &&
+
+       test_write_lines L1 L2 L3 L4 xxx L6 L7 L8 L9 >file &&
+       git add file &&
+       test_tick &&
+       git commit -m C &&
+       git tag C &&
+
+       git blame file --ignore-rev B >blame_raw
+       '
+
+# Resetting the repo and creating:
+#
+# A--B--M
+#  \   /
+#   C-+
+#
+# 'A' creates a file.  B changes line 1, and C changes line 9.  M merges.
+test_expect_success ignore_merge '
+       rm -rf .git/ &&
+       git init &&
+
+       test_write_lines L1 L2 L3 L4 L5 L6 L7 L8 L9 >file &&
+       git add file &&
+       test_tick &&
+       git commit -m A &&
+       git tag A &&
+
+       test_write_lines BB L2 L3 L4 L5 L6 L7 L8 L9 >file &&
+       git add file &&
+       test_tick &&
+       git commit -m B &&
+       git tag B &&
+
+       git reset --hard A &&
+       test_write_lines L1 L2 L3 L4 L5 L6 L7 L8 CC >file &&
+       git add file &&
+       test_tick &&
+       git commit -m C &&
+       git tag C &&
+
+       test_merge M B &&
+       git blame --line-porcelain file --ignore-rev M >blame_raw &&
+
+       grep -E "^[0-9a-f]+ [0-9]+ 1" blame_raw | sed -e "s/ .*//" >actual &&
+       git rev-parse B >expect &&
+       test_cmp expect actual &&
+
+       grep -E "^[0-9a-f]+ [0-9]+ 9" blame_raw | sed -e "s/ .*//" >actual &&
+       git rev-parse C >expect &&
+       test_cmp expect actual
+       '
+
+test_done
diff --git a/t/t8014-blame-ignore-fuzzy.sh b/t/t8014-blame-ignore-fuzzy.sh
new file mode 100755 (executable)
index 0000000..6e61882
--- /dev/null
@@ -0,0 +1,437 @@
+#!/bin/sh
+
+test_description='git blame ignore fuzzy heuristic'
+. ./test-lib.sh
+
+pick_author='s/^[0-9a-f^]* *(\([^ ]*\) .*/\1/'
+
+# Each test is composed of 4 variables:
+# titleN - the test name
+# aN - the initial content
+# bN - the final content
+# expectedN - the line numbers from aN that we expect git blame
+#             on bN to identify, or "Final" if bN itself should
+#             be identified as the origin of that line.
+
+# We start at test 2 because setup will show as test 1
+title2="Regression test for partially overlapping search ranges"
+cat <<EOF >a2
+1
+2
+3
+abcdef
+5
+6
+7
+ijkl
+9
+10
+11
+pqrs
+13
+14
+15
+wxyz
+17
+18
+19
+EOF
+cat <<EOF >b2
+abcde
+ijk
+pqr
+wxy
+EOF
+cat <<EOF >expected2
+4
+8
+12
+16
+EOF
+
+title3="Combine 3 lines into 2"
+cat <<EOF >a3
+if ((maxgrow==0) ||
+       ( single_line_field && (field->dcols < maxgrow)) ||
+       (!single_line_field && (field->drows < maxgrow)))
+EOF
+cat <<EOF >b3
+if ((maxgrow == 0) || (single_line_field && (field->dcols < maxgrow)) ||
+       (!single_line_field && (field->drows < maxgrow))) {
+EOF
+cat <<EOF >expected3
+2
+3
+EOF
+
+title4="Add curly brackets"
+cat <<EOF >a4
+       if (rows) *rows = field->rows;
+       if (cols) *cols = field->cols;
+       if (frow) *frow = field->frow;
+       if (fcol) *fcol = field->fcol;
+EOF
+cat <<EOF >b4
+       if (rows) {
+               *rows = field->rows;
+       }
+       if (cols) {
+               *cols = field->cols;
+       }
+       if (frow) {
+               *frow = field->frow;
+       }
+       if (fcol) {
+               *fcol = field->fcol;
+       }
+EOF
+cat <<EOF >expected4
+1
+1
+Final
+2
+2
+Final
+3
+3
+Final
+4
+4
+Final
+EOF
+
+
+title5="Combine many lines and change case"
+cat <<EOF >a5
+for(row=0,pBuffer=field->buf;
+       row<height;
+       row++,pBuffer+=width )
+{
+       if ((len = (int)( After_End_Of_Data( pBuffer, width ) - pBuffer )) > 0)
+       {
+               wmove( win, row, 0 );
+               waddnstr( win, pBuffer, len );
+EOF
+cat <<EOF >b5
+for (Row = 0, PBuffer = field->buf; Row < Height; Row++, PBuffer += Width) {
+       if ((Len = (int)(afterEndOfData(PBuffer, Width) - PBuffer)) > 0) {
+               wmove(win, Row, 0);
+               waddnstr(win, PBuffer, Len);
+EOF
+cat <<EOF >expected5
+1
+5
+7
+8
+EOF
+
+title6="Rename and combine lines"
+cat <<EOF >a6
+bool need_visual_update = ((form != (FORM *)0)      &&
+       (form->status & _POSTED) &&
+       (form->current==field));
+
+if (need_visual_update)
+       Synchronize_Buffer(form);
+
+if (single_line_field)
+{
+       growth = field->cols * amount;
+       if (field->maxgrow)
+               growth = Minimum(field->maxgrow - field->dcols,growth);
+       field->dcols += growth;
+       if (field->dcols == field->maxgrow)
+EOF
+cat <<EOF >b6
+bool NeedVisualUpdate = ((Form != (FORM *)0) && (Form->status & _POSTED) &&
+       (Form->current == field));
+
+if (NeedVisualUpdate) {
+       synchronizeBuffer(Form);
+}
+
+if (SingleLineField) {
+       Growth = field->cols * amount;
+       if (field->maxgrow) {
+               Growth = Minimum(field->maxgrow - field->dcols, Growth);
+       }
+       field->dcols += Growth;
+       if (field->dcols == field->maxgrow) {
+EOF
+cat <<EOF >expected6
+1
+3
+4
+5
+6
+Final
+7
+8
+10
+11
+12
+Final
+13
+14
+EOF
+
+# Both lines match identically so position must be used to tie-break.
+title7="Same line twice"
+cat <<EOF >a7
+abc
+abc
+EOF
+cat <<EOF >b7
+abcd
+abcd
+EOF
+cat <<EOF >expected7
+1
+2
+EOF
+
+title8="Enforce line order"
+cat <<EOF >a8
+abcdef
+ghijkl
+ab
+EOF
+cat <<EOF >b8
+ghijk
+abcd
+EOF
+cat <<EOF >expected8
+2
+3
+EOF
+
+title9="Expand lines and rename variables"
+cat <<EOF >a9
+int myFunction(int ArgumentOne, Thing *ArgTwo, Blah XuglyBug) {
+       Squiggle FabulousResult = squargle(ArgumentOne, *ArgTwo,
+               XuglyBug) + EwwwGlobalWithAReallyLongNameYepTooLong;
+       return FabulousResult * 42;
+}
+EOF
+cat <<EOF >b9
+int myFunction(int argument_one, Thing *arg_asdfgh,
+       Blah xugly_bug) {
+       Squiggle fabulous_result = squargle(argument_one,
+               *arg_asdfgh, xugly_bug)
+               + g_ewww_global_with_a_really_long_name_yep_too_long;
+       return fabulous_result * 42;
+}
+EOF
+cat <<EOF >expected9
+1
+1
+2
+3
+3
+4
+5
+EOF
+
+title10="Two close matches versus one less close match"
+cat <<EOF >a10
+abcdef
+abcdef
+ghijkl
+EOF
+cat <<EOF >b10
+gh
+abcdefx
+EOF
+cat <<EOF >expected10
+Final
+2
+EOF
+
+# The first line of b matches best with the last line of a, but the overall
+# match is better if we match it with the the first line of a.
+title11="Piggy in the middle"
+cat <<EOF >a11
+abcdefg
+ijklmn
+abcdefgh
+EOF
+cat <<EOF >b11
+abcdefghx
+ijklm
+EOF
+cat <<EOF >expected11
+1
+2
+EOF
+
+title12="No trailing newline"
+printf "abc\ndef" >a12
+printf "abx\nstu" >b12
+cat <<EOF >expected12
+1
+Final
+EOF
+
+title13="Reorder includes"
+cat <<EOF >a13
+#include "c.h"
+#include "b.h"
+#include "a.h"
+#include "e.h"
+#include "d.h"
+EOF
+cat <<EOF >b13
+#include "a.h"
+#include "b.h"
+#include "c.h"
+#include "d.h"
+#include "e.h"
+EOF
+cat <<EOF >expected13
+3
+2
+1
+5
+4
+EOF
+
+last_test=13
+
+test_expect_success setup '
+       for i in $(test_seq 2 $last_test)
+       do
+               # Append each line in a separate commit to make it easy to
+               # check which original line the blame output relates to.
+
+               line_count=0 &&
+               while IFS= read line
+               do
+                       line_count=$((line_count+1)) &&
+                       echo "$line" >>"$i" &&
+                       git add "$i" &&
+                       test_tick &&
+                       GIT_AUTHOR_NAME="$line_count" git commit -m "$line_count"
+               done <"a$i"
+       done &&
+
+       for i in $(test_seq 2 $last_test)
+       do
+               # Overwrite the files with the final content.
+               cp b$i $i &&
+               git add $i
+       done &&
+       test_tick &&
+
+       # Commit the final content all at once so it can all be
+       # referred to with the same commit ID.
+       GIT_AUTHOR_NAME=Final git commit -m Final &&
+
+       IGNOREME=$(git rev-parse HEAD)
+'
+
+for i in $(test_seq 2 $last_test); do
+       eval title="\$title$i"
+       test_expect_success "$title" \
+       "git blame -M9 --ignore-rev $IGNOREME $i >output &&
+       sed -e \"$pick_author\" output >actual &&
+       test_cmp expected$i actual"
+done
+
+# This invoked a null pointer dereference when the chunk callback was called
+# with a zero length parent chunk and there were no more suspects.
+test_expect_success 'Diff chunks with no suspects' '
+       test_write_lines xy1 A B C xy1 >file &&
+       git add file &&
+       test_tick &&
+       GIT_AUTHOR_NAME=1 git commit -m 1 &&
+
+       test_write_lines xy2 A B xy2 C xy2 >file &&
+       git add file &&
+       test_tick &&
+       GIT_AUTHOR_NAME=2 git commit -m 2 &&
+       REV_2=$(git rev-parse HEAD) &&
+
+       test_write_lines xy3 A >file &&
+       git add file &&
+       test_tick &&
+       GIT_AUTHOR_NAME=3 git commit -m 3 &&
+       REV_3=$(git rev-parse HEAD) &&
+
+       test_write_lines 1 1 >expected &&
+
+       git blame --ignore-rev $REV_2 --ignore-rev $REV_3 file >output &&
+       sed -e "$pick_author" output >actual &&
+
+       test_cmp expected actual
+       '
+
+test_expect_success 'position matching' '
+       test_write_lines abc def >file2 &&
+       git add file2 &&
+       test_tick &&
+       GIT_AUTHOR_NAME=1 git commit -m 1 &&
+
+       test_write_lines abc def abc def >file2 &&
+       git add file2 &&
+       test_tick &&
+       GIT_AUTHOR_NAME=2 git commit -m 2 &&
+
+       test_write_lines abcx defx abcx defx >file2 &&
+       git add file2 &&
+       test_tick &&
+       GIT_AUTHOR_NAME=3 git commit -m 3 &&
+       REV_3=$(git rev-parse HEAD) &&
+
+       test_write_lines abcy defy abcx defx >file2 &&
+       git add file2 &&
+       test_tick &&
+       GIT_AUTHOR_NAME=4 git commit -m 4 &&
+       REV_4=$(git rev-parse HEAD) &&
+
+       test_write_lines 1 1 2 2 >expected &&
+
+       git blame --ignore-rev $REV_3 --ignore-rev $REV_4 file2 >output &&
+       sed -e "$pick_author" output >actual &&
+
+       test_cmp expected actual
+       '
+
+# This fails if each blame entry is processed independently instead of
+# processing each diff change in full.
+test_expect_success 'preserve order' '
+       test_write_lines bcde >file3 &&
+       git add file3 &&
+       test_tick &&
+       GIT_AUTHOR_NAME=1 git commit -m 1 &&
+
+       test_write_lines bcde fghij >file3 &&
+       git add file3 &&
+       test_tick &&
+       GIT_AUTHOR_NAME=2 git commit -m 2 &&
+
+       test_write_lines bcde fghij abcd >file3 &&
+       git add file3 &&
+       test_tick &&
+       GIT_AUTHOR_NAME=3 git commit -m 3 &&
+
+       test_write_lines abcdx fghijx bcdex >file3 &&
+       git add file3 &&
+       test_tick &&
+       GIT_AUTHOR_NAME=4 git commit -m 4 &&
+       REV_4=$(git rev-parse HEAD) &&
+
+       test_write_lines abcdx fghijy bcdex >file3 &&
+       git add file3 &&
+       test_tick &&
+       GIT_AUTHOR_NAME=5 git commit -m 5 &&
+       REV_5=$(git rev-parse HEAD) &&
+
+       test_write_lines 1 2 3 >expected &&
+
+       git blame --ignore-rev $REV_4 --ignore-rev $REV_5 file3 >output &&
+       sed -e "$pick_author" output >actual &&
+
+       test_cmp expected actual
+       '
+
+test_done
index 43cf313a1c09ad8fb223ba775d5548c3f00c339c..75512c340366f3034c58effb62c8796d0b1463a8 100755 (executable)
@@ -1706,7 +1706,7 @@ test_expect_success 'sourcing the completion script clears cached commands' '
 '
 
 test_expect_success 'sourcing the completion script clears cached merge strategies' '
-       GIT_TEST_GETTEXT_POISON= &&
+       GIT_TEST_GETTEXT_POISON=false &&
        __git_compute_merge_strategies &&
        verbose test -n "$__git_merge_strategies" &&
        . "$GIT_BUILD_DIR/contrib/completion/git-completion.bash" &&
index 5cadedb2a9bc6eb66b9ead3ccae037e62a655f2c..88bc733ad6910ae0a2b11bb5e239f010cb2cdb72 100755 (executable)
@@ -211,8 +211,24 @@ test_expect_success 'prompt - merge' '
 
 test_expect_success 'prompt - cherry-pick' '
        printf " (master|CHERRY-PICKING)" >expected &&
-       test_must_fail git cherry-pick b1 &&
-       test_when_finished "git reset --hard" &&
+       test_must_fail git cherry-pick b1 b1^ &&
+       test_when_finished "git cherry-pick --abort" &&
+       __git_ps1 >"$actual" &&
+       test_cmp expected "$actual" &&
+       git reset --merge &&
+       test_must_fail git rev-parse CHERRY_PICK_HEAD &&
+       __git_ps1 >"$actual" &&
+       test_cmp expected "$actual"
+'
+
+test_expect_success 'prompt - revert' '
+       printf " (master|REVERTING)" >expected &&
+       test_must_fail git revert b1^ b1 &&
+       test_when_finished "git revert --abort" &&
+       __git_ps1 >"$actual" &&
+       test_cmp expected "$actual" &&
+       git reset --merge &&
+       test_must_fail git rev-parse REVERT_HEAD &&
        __git_ps1 >"$actual" &&
        test_cmp expected "$actual"
 '
index 7308f679229044030336922515a4e2870e6451d1..27b81276fc22569249bb5e83dcf4ba95e9823a57 100644 (file)
@@ -309,7 +309,7 @@ test_unset_prereq () {
 }
 
 test_set_prereq () {
-       if test -n "$GIT_TEST_FAIL_PREREQS"
+       if test -n "$GIT_TEST_FAIL_PREREQS_INTERNAL"
        then
                case "$1" in
                # The "!" case is handled below with
@@ -1050,62 +1050,20 @@ perl () {
        command "$PERL_PATH" "$@" 2>&7
 } 7>&2 2>&4
 
-# Is the value one of the various ways to spell a boolean true/false?
-test_normalize_bool () {
-       git -c magic.variable="$1" config --bool magic.variable 2>/dev/null
-}
-
-# Given a variable $1, normalize the value of it to one of "true",
-# "false", or "auto" and store the result to it.
-#
-#     test_tristate GIT_TEST_HTTPD
-#
-# A variable set to an empty string is set to 'false'.
-# A variable set to 'false' or 'auto' keeps its value.
-# Anything else is set to 'true'.
-# An unset variable defaults to 'auto'.
-#
-# The last rule is to allow people to set the variable to an empty
-# string and export it to decline testing the particular feature
-# for versions both before and after this change.  We used to treat
-# both unset and empty variable as a signal for "do not test" and
-# took any non-empty string as "please test".
-
-test_tristate () {
-       if eval "test x\"\${$1+isset}\" = xisset"
-       then
-               # explicitly set
-               eval "
-                       case \"\$$1\" in
-                       '')     $1=false ;;
-                       auto)   ;;
-                       *)      $1=\$(test_normalize_bool \$$1 || echo true) ;;
-                       esac
-               "
-       else
-               eval "$1=auto"
-       fi
-}
-
 # Exit the test suite, either by skipping all remaining tests or by
-# exiting with an error. If "$1" is "auto", we then we assume we were
-# opportunistically trying to set up some tests and we skip. If it is
-# "true", then we report a failure.
+# exiting with an error. If our prerequisite variable $1 falls back
+# on a default assume we were opportunistically trying to set up some
+# tests and we skip. If it is explicitly "true", then we report a failure.
 #
 # The error/skip message should be given by $2.
 #
 test_skip_or_die () {
-       case "$1" in
-       auto)
+       if ! git env--helper --type=bool --default=false --exit-code $1
+       then
                skip_all=$2
                test_done
-               ;;
-       true)
-               error "$2"
-               ;;
-       *)
-               error "BUG: test tristate is '$1' (real error: $2)"
-       esac
+       fi
+       error "$2"
 }
 
 # The following mingw_* functions obey POSIX shell syntax, but are actually
index d1ba33745a24c92411baca54604c853373b9bc39..30b07e310f59493616f766ea6cf7c45a335c9258 100644 (file)
@@ -1388,6 +1388,25 @@ yes () {
        done
 }
 
+# The GIT_TEST_FAIL_PREREQS code hooks into test_set_prereq(), and
+# thus needs to be set up really early, and set an internal variable
+# for convenience so the hot test_set_prereq() codepath doesn't need
+# to call "git env--helper". Only do that work if needed by seeing if
+# GIT_TEST_FAIL_PREREQS is set at all.
+GIT_TEST_FAIL_PREREQS_INTERNAL=
+if test -n "$GIT_TEST_FAIL_PREREQS"
+then
+       if git env--helper --type=bool --default=0 --exit-code GIT_TEST_FAIL_PREREQS
+       then
+               GIT_TEST_FAIL_PREREQS_INTERNAL=true
+               test_set_prereq FAIL_PREREQS
+       fi
+else
+       test_lazy_prereq FAIL_PREREQS '
+               git env--helper --type=bool --default=0 --exit-code GIT_TEST_FAIL_PREREQS
+       '
+fi
+
 # Fix some commands on Windows
 uname_s=$(uname -s)
 case $uname_s in
@@ -1442,11 +1461,9 @@ then
        unset GIT_TEST_GETTEXT_POISON_ORIG
 fi
 
-# Can we rely on git's output in the C locale?
-if test -z "$GIT_TEST_GETTEXT_POISON"
-then
-       test_set_prereq C_LOCALE_OUTPUT
-fi
+test_lazy_prereq C_LOCALE_OUTPUT '
+       ! git env--helper --type=bool --default=0 --exit-code GIT_TEST_GETTEXT_POISON
+'
 
 if test -z "$GIT_TEST_CHECK_CACHE_TREE"
 then
@@ -1606,7 +1623,3 @@ test_lazy_prereq SHA1 '
 test_lazy_prereq REBASE_P '
        test -z "$GIT_TEST_SKIP_REBASE_P"
 '
-
-test_lazy_prereq FAIL_PREREQS '
-       test -n "$GIT_TEST_FAIL_PREREQS"
-'
index c7e17ec9cb61e6782bc255e6b90381054255c269..6b05a88faf59ee74840b0f9fa340a5d9397a0f5d 100644 (file)
@@ -853,6 +853,7 @@ static int push_refs_with_push(struct transport *transport,
 {
        int force_all = flags & TRANSPORT_PUSH_FORCE;
        int mirror = flags & TRANSPORT_PUSH_MIRROR;
+       int atomic = flags & TRANSPORT_PUSH_ATOMIC;
        struct helper_data *data = transport->data;
        struct strbuf buf = STRBUF_INIT;
        struct ref *ref;
@@ -872,6 +873,11 @@ static int push_refs_with_push(struct transport *transport,
                case REF_STATUS_REJECT_NONFASTFORWARD:
                case REF_STATUS_REJECT_STALE:
                case REF_STATUS_REJECT_ALREADY_EXISTS:
+                       if (atomic) {
+                               string_list_clear(&cas_options, 0);
+                               return 0;
+                       } else
+                               continue;
                case REF_STATUS_UPTODATE:
                        continue;
                default:
index f1fcd2c4b006dc2ece2019ac91f73a2f42bbf6bd..778c60bf573a714134435e438da5d390fa490484 100644 (file)
@@ -1226,6 +1226,20 @@ int transport_push(struct repository *r,
                err = push_had_errors(remote_refs);
                ret = push_ret | err;
 
+               if ((flags & TRANSPORT_PUSH_ATOMIC) && err) {
+                       struct ref *it;
+                       for (it = remote_refs; it; it = it->next)
+                               switch (it->status) {
+                               case REF_STATUS_NONE:
+                               case REF_STATUS_UPTODATE:
+                               case REF_STATUS_OK:
+                                       it->status = REF_STATUS_ATOMIC_PUSH_FAILED;
+                                       break;
+                               default:
+                                       break;
+                               }
+               }
+
                if (!quiet || err)
                        transport_print_push_status(transport->url, remote_refs,
                                        verbose | porcelain, porcelain,
@@ -1380,100 +1394,3 @@ char *transport_anonymize_url(const char *url)
 literal_copy:
        return xstrdup(url);
 }
-
-static void fill_alternate_refs_command(struct child_process *cmd,
-                                       const char *repo_path)
-{
-       const char *value;
-
-       if (!git_config_get_value("core.alternateRefsCommand", &value)) {
-               cmd->use_shell = 1;
-
-               argv_array_push(&cmd->args, value);
-               argv_array_push(&cmd->args, repo_path);
-       } else {
-               cmd->git_cmd = 1;
-
-               argv_array_pushf(&cmd->args, "--git-dir=%s", repo_path);
-               argv_array_push(&cmd->args, "for-each-ref");
-               argv_array_push(&cmd->args, "--format=%(objectname)");
-
-               if (!git_config_get_value("core.alternateRefsPrefixes", &value)) {
-                       argv_array_push(&cmd->args, "--");
-                       argv_array_split(&cmd->args, value);
-               }
-       }
-
-       cmd->env = local_repo_env;
-       cmd->out = -1;
-}
-
-static void read_alternate_refs(const char *path,
-                               alternate_ref_fn *cb,
-                               void *data)
-{
-       struct child_process cmd = CHILD_PROCESS_INIT;
-       struct strbuf line = STRBUF_INIT;
-       FILE *fh;
-
-       fill_alternate_refs_command(&cmd, path);
-
-       if (start_command(&cmd))
-               return;
-
-       fh = xfdopen(cmd.out, "r");
-       while (strbuf_getline_lf(&line, fh) != EOF) {
-               struct object_id oid;
-               const char *p;
-
-               if (parse_oid_hex(line.buf, &oid, &p) || *p) {
-                       warning(_("invalid line while parsing alternate refs: %s"),
-                               line.buf);
-                       break;
-               }
-
-               cb(&oid, data);
-       }
-
-       fclose(fh);
-       finish_command(&cmd);
-}
-
-struct alternate_refs_data {
-       alternate_ref_fn *fn;
-       void *data;
-};
-
-static int refs_from_alternate_cb(struct object_directory *e,
-                                 void *data)
-{
-       struct strbuf path = STRBUF_INIT;
-       size_t base_len;
-       struct alternate_refs_data *cb = data;
-
-       if (!strbuf_realpath(&path, e->path, 0))
-               goto out;
-       if (!strbuf_strip_suffix(&path, "/objects"))
-               goto out;
-       base_len = path.len;
-
-       /* Is this a git repository with refs? */
-       strbuf_addstr(&path, "/refs");
-       if (!is_directory(path.buf))
-               goto out;
-       strbuf_setlen(&path, base_len);
-
-       read_alternate_refs(path.buf, cb->fn, cb->data);
-
-out:
-       strbuf_release(&path);
-       return 0;
-}
-
-void for_each_alternate_ref(alternate_ref_fn fn, void *data)
-{
-       struct alternate_refs_data cb;
-       cb.fn = fn;
-       cb.data = data;
-       foreach_alt_odb(refs_from_alternate_cb, &cb);
-}
index 06e06d3d8937bc553ef42cd895ee7132c6e5639c..0b5f7806f625d888175e1c633ec2b59d8d07803b 100644 (file)
@@ -262,6 +262,4 @@ int transport_refs_pushed(struct ref *ref);
 void transport_print_push_status(const char *dest, struct ref *refs,
                  int verbose, int porcelain, unsigned int *reject_reasons);
 
-typedef void alternate_ref_fn(const struct object_id *oid, void *);
-void for_each_alternate_ref(alternate_ref_fn, void *);
 #endif
index f1f641eb6a64abc2dbb6f0ef70f7375101874812..33ded7f8b3e71069f7a75bf7524443f3d81b7532 100644 (file)
@@ -422,8 +422,8 @@ static struct combine_diff_path *ll_diff_tree_paths(
         *   diff_tree_oid(parent, commit) )
         */
        for (i = 0; i < nparent; ++i)
-               tptree[i] = fill_tree_descriptor(&tp[i], parents_oid[i]);
-       ttree = fill_tree_descriptor(&t, oid);
+               tptree[i] = fill_tree_descriptor(opt->repo, &tp[i], parents_oid[i]);
+       ttree = fill_tree_descriptor(opt->repo, &t, oid);
 
        /* Enable recursion indefinitely */
        opt->pathspec.recursive = opt->flags.recursive;
index ec32a47b2e7664365f771f3955747794001d3f28..c20b62f49e4709253d71e18eea0a2472809d8e9d 100644 (file)
@@ -81,13 +81,15 @@ int init_tree_desc_gently(struct tree_desc *desc, const void *buffer, unsigned l
        return result;
 }
 
-void *fill_tree_descriptor(struct tree_desc *desc, const struct object_id *oid)
+void *fill_tree_descriptor(struct repository *r,
+                          struct tree_desc *desc,
+                          const struct object_id *oid)
 {
        unsigned long size = 0;
        void *buf = NULL;
 
        if (oid) {
-               buf = read_object_with_reference(oid, tree_type, &size, NULL);
+               buf = read_object_with_reference(r, oid, tree_type, &size, NULL);
                if (!buf)
                        die("unable to read tree %s", oid_to_hex(oid));
        }
@@ -500,7 +502,9 @@ struct dir_state {
        struct object_id oid;
 };
 
-static int find_tree_entry(struct tree_desc *t, const char *name, struct object_id *result, unsigned short *mode)
+static int find_tree_entry(struct repository *r, struct tree_desc *t,
+                          const char *name, struct object_id *result,
+                          unsigned short *mode)
 {
        int namelen = strlen(name);
        while (t->size) {
@@ -530,19 +534,23 @@ static int find_tree_entry(struct tree_desc *t, const char *name, struct object_
                        oidcpy(result, &oid);
                        return 0;
                }
-               return get_tree_entry(&oid, name + entrylen, result, mode);
+               return get_tree_entry(r, &oid, name + entrylen, result, mode);
        }
        return -1;
 }
 
-int get_tree_entry(const struct object_id *tree_oid, const char *name, struct object_id *oid, unsigned short *mode)
+int get_tree_entry(struct repository *r,
+                  const struct object_id *tree_oid,
+                  const char *name,
+                  struct object_id *oid,
+                  unsigned short *mode)
 {
        int retval;
        void *tree;
        unsigned long size;
        struct object_id root;
 
-       tree = read_object_with_reference(tree_oid, tree_type, &size, &root);
+       tree = read_object_with_reference(r, tree_oid, tree_type, &size, &root);
        if (!tree)
                return -1;
 
@@ -557,7 +565,7 @@ int get_tree_entry(const struct object_id *tree_oid, const char *name, struct ob
        } else {
                struct tree_desc t;
                init_tree_desc(&t, tree, size);
-               retval = find_tree_entry(&t, name, oid, mode);
+               retval = find_tree_entry(r, &t, name, oid, mode);
        }
        free(tree);
        return retval;
@@ -585,7 +593,10 @@ int get_tree_entry(const struct object_id *tree_oid, const char *name, struct ob
  * See the code for enum get_oid_result for a description of
  * the return values.
  */
-enum get_oid_result get_tree_entry_follow_symlinks(struct object_id *tree_oid, const char *name, struct object_id *result, struct strbuf *result_path, unsigned short *mode)
+enum get_oid_result get_tree_entry_follow_symlinks(struct repository *r,
+               struct object_id *tree_oid, const char *name,
+               struct object_id *result, struct strbuf *result_path,
+               unsigned short *mode)
 {
        int retval = MISSING_OBJECT;
        struct dir_state *parents = NULL;
@@ -609,7 +620,8 @@ enum get_oid_result get_tree_entry_follow_symlinks(struct object_id *tree_oid, c
                        void *tree;
                        struct object_id root;
                        unsigned long size;
-                       tree = read_object_with_reference(&current_tree_oid,
+                       tree = read_object_with_reference(r,
+                                                         &current_tree_oid,
                                                          tree_type, &size,
                                                          &root);
                        if (!tree)
@@ -678,7 +690,7 @@ enum get_oid_result get_tree_entry_follow_symlinks(struct object_id *tree_oid, c
                }
 
                /* Look up the first (or only) path component in the tree. */
-               find_result = find_tree_entry(&t, namebuf.buf,
+               find_result = find_tree_entry(r, &t, namebuf.buf,
                                              &current_tree_oid, mode);
                if (find_result) {
                        goto done;
@@ -722,7 +734,8 @@ enum get_oid_result get_tree_entry_follow_symlinks(struct object_id *tree_oid, c
                         */
                        retval = DANGLING_SYMLINK;
 
-                       contents = read_object_file(&current_tree_oid, &type,
+                       contents = repo_read_object_file(r,
+                                                   &current_tree_oid, &type,
                                                    &link_len);
 
                        if (!contents)
index 161e2400f443460fc6606e97d4e356c1f0b478ed..2a5db29e8f196f535c75fbf84dcae8f45ca5c4a3 100644 (file)
@@ -45,13 +45,15 @@ int init_tree_desc_gently(struct tree_desc *desc, const void *buf, unsigned long
 int tree_entry(struct tree_desc *, struct name_entry *);
 int tree_entry_gently(struct tree_desc *, struct name_entry *);
 
-void *fill_tree_descriptor(struct tree_desc *desc, const struct object_id *oid);
+void *fill_tree_descriptor(struct repository *r,
+                          struct tree_desc *desc,
+                          const struct object_id *oid);
 
 struct traverse_info;
 typedef int (*traverse_callback_t)(int n, unsigned long mask, unsigned long dirmask, struct name_entry *entry, struct traverse_info *);
 int traverse_trees(struct index_state *istate, int n, struct tree_desc *t, struct traverse_info *info);
 
-enum get_oid_result get_tree_entry_follow_symlinks(struct object_id *tree_oid, const char *name, struct object_id *result, struct strbuf *result_path, unsigned short *mode);
+enum get_oid_result get_tree_entry_follow_symlinks(struct repository *r, struct object_id *tree_oid, const char *name, struct object_id *result, struct strbuf *result_path, unsigned short *mode);
 
 struct traverse_info {
        const char *traverse_path;
@@ -66,7 +68,7 @@ struct traverse_info {
        int show_all_errors;
 };
 
-int get_tree_entry(const struct object_id *, const char *, struct object_id *, unsigned short *);
+int get_tree_entry(struct repository *, const struct object_id *, const char *, struct object_id *, unsigned short *);
 char *make_traverse_path(char *path, const struct traverse_info *info, const struct name_entry *n);
 void setup_traverse_info(struct traverse_info *info, const char *base);
 
index dab713203e15a83e452119d0140bb4c28c8a7bf0..62276d4fef5f67e7147377f16e24e30abca0906a 100644 (file)
@@ -840,7 +840,7 @@ static int traverse_trees_recursive(int n, unsigned long dirmask,
                        const struct object_id *oid = NULL;
                        if (dirmask & 1)
                                oid = &names[i].oid;
-                       buf[nr_buf++] = fill_tree_descriptor(t + i, oid);
+                       buf[nr_buf++] = fill_tree_descriptor(the_repository, t + i, oid);
                }
        }
 
index 7d776f8a9756e9defa3d2b7a2c15217524204e07..9f6c65a5809754717f8c51f809eae78f435bcd12 100644 (file)
@@ -202,7 +202,6 @@ static void wt_longstatus_print_unmerged_header(struct wt_status *s)
        } else {
                status_printf_ln(s, c, _("  (use \"git add/rm <file>...\" as appropriate to mark resolution)"));
        }
-       status_printf_ln(s, c, "%s", "");
 }
 
 static void wt_longstatus_print_cached_header(struct wt_status *s)
@@ -224,7 +223,6 @@ static void wt_longstatus_print_cached_header(struct wt_status *s)
                                         s->reference);
        } else
                status_printf_ln(s, c, _("  (use \"git rm --cached <file>...\" to unstage)"));
-       status_printf_ln(s, c, "%s", "");
 }
 
 static void wt_longstatus_print_dirty_header(struct wt_status *s,
@@ -243,7 +241,6 @@ static void wt_longstatus_print_dirty_header(struct wt_status *s,
        status_printf_ln(s, c, _("  (use \"git restore <file>...\" to discard changes in working directory)"));
        if (has_dirty_submodules)
                status_printf_ln(s, c, _("  (commit or discard the untracked or modified content in submodules)"));
-       status_printf_ln(s, c, "%s", "");
 }
 
 static void wt_longstatus_print_other_header(struct wt_status *s,
@@ -255,7 +252,6 @@ static void wt_longstatus_print_other_header(struct wt_status *s,
        if (!s->hints)
                return;
        status_printf_ln(s, c, _("  (use \"git %s <file>...\" to include in what will be committed)"), how);
-       status_printf_ln(s, c, "%s", "");
 }
 
 static void wt_longstatus_print_trailer(struct wt_status *s)