diff options
102 files changed, 4275 insertions, 1022 deletions
diff --git a/Documentation/RelNotes/2.31.0.txt b/Documentation/RelNotes/2.31.0.txt new file mode 100644 index 0000000000..60232a5645 --- /dev/null +++ b/Documentation/RelNotes/2.31.0.txt @@ -0,0 +1,61 @@ +Git 2.31 Release Notes +====================== + +Updates since v2.30 +------------------- + +UI, Workflows & Features + + * The "--format=%(trailers)" mechanism gets enhanced to make it + easier to design output for machine consumption. + + * When a user does not tell "git pull" to use rebase or merge, the + command gives a loud message telling a user to choose between + rebase or merge but creates a merge anyway, forcing users who would + want to rebase to redo the operation. Fix an early part of this + problem by tightening the condition to give the message---there is + no reason to stop or force the user to choose between rebase or + merge if the history fast-forwards. + + * The configuration variable 'core.abbrev' can be set to 'no' to + force no abbreviation regardless of the hash algorithm. + + * "git rev-parse" can be explicitly told to give output as absolute + or relative path with the `--path-format=(absolute|relative)` option. + + * Bash completion (in contrib/) update to make it easier for + end-users to add completion for their custom "git" subcommands. + + +Performance, Internal Implementation, Development Support etc. + + * A 3-year old test that was not testing anything useful has been + corrected. + + * Retire more names with "sha1" in it. + + * The topological walk codepath is covered by new trace2 stats. + + +Fixes since v2.30 +----------------- + + * Diagnose command line error of "git rebase" early. + (merge ca5120c339 rs/rebase-commit-validation later to maint). + + * Clean up option descriptions in "git cmd --help". + (merge e73fe3dd02 zh/arg-help-format later to maint). + + * "git stash" did not work well in a sparsely checked out working + tree. + (merge ba359fd507 en/stash-apply-sparse-checkout later to maint). + + * Other code cleanup, docfix, build fix, etc. + (merge 505a276596 pk/subsub-fetch-fix-take-2 later to maint). + (merge 33fc56253b fc/t6030-bisect-reset-removes-auxiliary-files later to maint). + (merge 7efc378205 ta/doc-typofix later to maint). + (merge 1f4e9319c7 pb/doc-modules-git-work-tree-typofix later to maint). + (merge 04f6b0a192 ma/t1300-cleanup later to maint). + (merge 7b77f5a13e ma/doc-pack-format-varint-for-sizes later to maint). + (merge cc2d43be2b nk/perf-fsmonitor-cleanup later to maint). + (merge c8302c6c00 ar/t6016-modernise later to maint). diff --git a/Documentation/SubmittingPatches b/Documentation/SubmittingPatches index d12094bac5..0452db2e67 100644 --- a/Documentation/SubmittingPatches +++ b/Documentation/SubmittingPatches @@ -307,7 +307,7 @@ wrote the patch or have the right to pass it on under the same license as ours, by "signing off" your patch. Without sign-off, we cannot accept your patches. -If you can certify the below D-C-O: +If (and only if) you certify the below D-C-O: [[dco]] .Developer's Certificate of Origin 1.1 diff --git a/Documentation/config/core.txt b/Documentation/config/core.txt index 160aacad84..c04f62a54a 100644 --- a/Documentation/config/core.txt +++ b/Documentation/config/core.txt @@ -625,4 +625,6 @@ core.abbrev:: computed based on the approximate number of packed objects in your repository, which hopefully is enough for abbreviated object names to stay unique for some time. + If set to "no", no abbreviation is made and the object names + are shown in their full length. The minimum length is 4. diff --git a/Documentation/config/http.txt b/Documentation/config/http.txt index 3968fbb697..7003661c0d 100644 --- a/Documentation/config/http.txt +++ b/Documentation/config/http.txt @@ -42,12 +42,12 @@ http.proxySSLKey:: http.proxySSLCertPasswordProtected:: Enable Git's password prompt for the proxy SSL certificate. Otherwise OpenSSL will prompt the user, possibly many times, if the certificate or private key - is encrypted. Can be overriden by the `GIT_PROXY_SSL_CERT_PASSWORD_PROTECTED` + is encrypted. Can be overridden by the `GIT_PROXY_SSL_CERT_PASSWORD_PROTECTED` environment variable. http.proxySSLCAInfo:: Pathname to the file containing the certificate bundle that should be used to - verify the proxy with when using an HTTPS proxy. Can be overriden by the + verify the proxy with when using an HTTPS proxy. Can be overridden by the `GIT_PROXY_SSL_CAINFO` environment variable. http.emptyAuth:: diff --git a/Documentation/config/trace2.txt b/Documentation/config/trace2.txt index 01d3afd8a8..fe1642f0d4 100644 --- a/Documentation/config/trace2.txt +++ b/Documentation/config/trace2.txt @@ -54,7 +54,7 @@ trace2.envVars:: `GIT_HTTP_USER_AGENT,GIT_CONFIG` would cause the trace2 output to contain events listing the overrides for HTTP user agent and the location of the Git configuration file (assuming any are set). May be - overriden by the `GIT_TRACE2_ENV_VARS` environment variable. Unset by + overridden by the `GIT_TRACE2_ENV_VARS` environment variable. Unset by default. trace2.destinationDebug:: diff --git a/Documentation/git-clone.txt b/Documentation/git-clone.txt index 876aedcd47..02d9c19cec 100644 --- a/Documentation/git-clone.txt +++ b/Documentation/git-clone.txt @@ -57,6 +57,10 @@ repository is specified as a URL, then this flag is ignored (and we never use the local optimizations). Specifying `--no-local` will override the default when `/path/to/repo` is given, using the regular Git transport instead. ++ +*NOTE*: this operation can race with concurrent modification to the +source repository, similar to running `cp -r src dst` while modifying +`src`. --no-hardlinks:: Force the cloning process from a repository on a local diff --git a/Documentation/git-ls-files.txt b/Documentation/git-ls-files.txt index cbcf5263dd..0a3b5265b3 100644 --- a/Documentation/git-ls-files.txt +++ b/Documentation/git-ls-files.txt @@ -23,9 +23,8 @@ SYNOPSIS DESCRIPTION ----------- -This merges the file listing in the directory cache index with the -actual working directory list, and shows different combinations of the -two. +This merges the file listing in the index with the actual working +directory list, and shows different combinations of the two. One or more of the options below may be used to determine the files shown: diff --git a/Documentation/git-maintenance.txt b/Documentation/git-maintenance.txt index d1f9b5172d..3b432171d6 100644 --- a/Documentation/git-maintenance.txt +++ b/Documentation/git-maintenance.txt @@ -38,7 +38,7 @@ register:: for running in the background without disrupting foreground processes. + -The `register` subcomand will also set the `maintenance.strategy` config +The `register` subcommand will also set the `maintenance.strategy` config value to `incremental`, if this value is not previously set. The `incremental` strategy uses the following schedule for each maintenance task: @@ -218,6 +218,122 @@ Further, the `git gc` command should not be combined with but does not take the lock in the same way as `git maintenance run`. If possible, use `git maintenance run --task=gc` instead of `git gc`. +The following sections describe the mechanisms put in place to run +background maintenance by `git maintenance start` and how to customize +them. + +BACKGROUND MAINTENANCE ON POSIX SYSTEMS +--------------------------------------- + +The standard mechanism for scheduling background tasks on POSIX systems +is cron(8). This tool executes commands based on a given schedule. The +current list of user-scheduled tasks can be found by running `crontab -l`. +The schedule written by `git maintenance start` is similar to this: + +----------------------------------------------------------------------- +# BEGIN GIT MAINTENANCE SCHEDULE +# The following schedule was created by Git +# Any edits made in this region might be +# replaced in the future by a Git command. + +0 1-23 * * * "/<path>/git" --exec-path="/<path>" for-each-repo --config=maintenance.repo maintenance run --schedule=hourly +0 0 * * 1-6 "/<path>/git" --exec-path="/<path>" for-each-repo --config=maintenance.repo maintenance run --schedule=daily +0 0 * * 0 "/<path>/git" --exec-path="/<path>" for-each-repo --config=maintenance.repo maintenance run --schedule=weekly + +# END GIT MAINTENANCE SCHEDULE +----------------------------------------------------------------------- + +The comments are used as a region to mark the schedule as written by Git. +Any modifications within this region will be completely deleted by +`git maintenance stop` or overwritten by `git maintenance start`. + +The `crontab` entry specifies the full path of the `git` executable to +ensure that the executed `git` command is the same one with which +`git maintenance start` was issued independent of `PATH`. If the same user +runs `git maintenance start` with multiple Git executables, then only the +latest executable is used. + +These commands use `git for-each-repo --config=maintenance.repo` to run +`git maintenance run --schedule=<frequency>` on each repository listed in +the multi-valued `maintenance.repo` config option. These are typically +loaded from the user-specific global config. The `git maintenance` process +then determines which maintenance tasks are configured to run on each +repository with each `<frequency>` using the `maintenance.<task>.schedule` +config options. These values are loaded from the global or repository +config values. + +If the config values are insufficient to achieve your desired background +maintenance schedule, then you can create your own schedule. If you run +`crontab -e`, then an editor will load with your user-specific `cron` +schedule. In that editor, you can add your own schedule lines. You could +start by adapting the default schedule listed earlier, or you could read +the crontab(5) documentation for advanced scheduling techniques. Please +do use the full path and `--exec-path` techniques from the default +schedule to ensure you are executing the correct binaries in your +schedule. + + +BACKGROUND MAINTENANCE ON MACOS SYSTEMS +--------------------------------------- + +While macOS technically supports `cron`, using `crontab -e` requires +elevated privileges and the executed process does not have a full user +context. Without a full user context, Git and its credential helpers +cannot access stored credentials, so some maintenance tasks are not +functional. + +Instead, `git maintenance start` interacts with the `launchctl` tool, +which is the recommended way to schedule timed jobs in macOS. Scheduling +maintenance through `git maintenance (start|stop)` requires some +`launchctl` features available only in macOS 10.11 or later. + +Your user-specific scheduled tasks are stored as XML-formatted `.plist` +files in `~/Library/LaunchAgents/`. You can see the currently-registered +tasks using the following command: + +----------------------------------------------------------------------- +$ ls ~/Library/LaunchAgents/org.git-scm.git* +org.git-scm.git.daily.plist +org.git-scm.git.hourly.plist +org.git-scm.git.weekly.plist +----------------------------------------------------------------------- + +One task is registered for each `--schedule=<frequency>` option. To +inspect how the XML format describes each schedule, open one of these +`.plist` files in an editor and inspect the `<array>` element following +the `<key>StartCalendarInterval</key>` element. + +`git maintenance start` will overwrite these files and register the +tasks again with `launchctl`, so any customizations should be done by +creating your own `.plist` files with distinct names. Similarly, the +`git maintenance stop` command will unregister the tasks with `launchctl` +and delete the `.plist` files. + +To create more advanced customizations to your background tasks, see +launchctl.plist(5) for more information. + + +BACKGROUND MAINTENANCE ON WINDOWS SYSTEMS +----------------------------------------- + +Windows does not support `cron` and instead has its own system for +scheduling background tasks. The `git maintenance start` command uses +the `schtasks` command to submit tasks to this system. You can inspect +all background tasks using the Task Scheduler application. The tasks +added by Git have names of the form `Git Maintenance (<frequency>)`. +The Task Scheduler GUI has ways to inspect these tasks, but you can also +export the tasks to XML files and view the details there. + +Note that since Git is a console application, these background tasks +create a console window visible to the current user. This can be changed +manually by selecting the "Run whether user is logged in or not" option +in Task Scheduler. This change requires a password input, which is why +`git maintenance start` does not select it by default. + +If you want to customize the background tasks, please rename the tasks +so future calls to `git maintenance (start|stop)` do not overwrite your +custom tasks. + GIT --- diff --git a/Documentation/git-p4.txt b/Documentation/git-p4.txt index ec233ac0c1..f89e68b424 100644 --- a/Documentation/git-p4.txt +++ b/Documentation/git-p4.txt @@ -397,7 +397,7 @@ changelist text. Exiting with a non-zero status from the script will abort the process. The purpose of the hook is to edit the message file in place, -and it is not supressed by the `--no-verify` option. This hook +and it is not suppressed by the `--no-verify` option. This hook is called even if `--prepare-p4-only` is set. p4-changelist diff --git a/Documentation/git-rev-parse.txt b/Documentation/git-rev-parse.txt index 5013daa6ef..6b8ca085aa 100644 --- a/Documentation/git-rev-parse.txt +++ b/Documentation/git-rev-parse.txt @@ -212,6 +212,18 @@ Options for Files Only the names of the variables are listed, not their value, even if they are set. +--path-format=(absolute|relative):: + Controls the behavior of certain other options. If specified as absolute, the + paths printed by those options will be absolute and canonical. If specified as + relative, the paths will be relative to the current working directory if that + is possible. The default is option specific. ++ +This option may be specified multiple times and affects only the arguments that +follow it on the command line, either to the end of the command line or the next +instance of this option. + +The following options are modified by `--path-format`: + --git-dir:: Show `$GIT_DIR` if defined. Otherwise show the path to the .git directory. The path shown, when relative, is @@ -221,27 +233,9 @@ If `$GIT_DIR` is not defined and the current directory is not detected to lie in a Git repository or work tree print a message to stderr and exit with nonzero status. ---absolute-git-dir:: - Like `--git-dir`, but its output is always the canonicalized - absolute path. - --git-common-dir:: Show `$GIT_COMMON_DIR` if defined, else `$GIT_DIR`. ---is-inside-git-dir:: - When the current working directory is below the repository - directory print "true", otherwise "false". - ---is-inside-work-tree:: - When the current working directory is inside the work tree of the - repository print "true", otherwise "false". - ---is-bare-repository:: - When the repository is bare print "true", otherwise "false". - ---is-shallow-repository:: - When the repository is shallow print "true", otherwise "false". - --resolve-git-dir <path>:: Check if <path> is a valid repository or a gitfile that points at a valid repository, and print the location of the @@ -255,19 +249,9 @@ print a message to stderr and exit with nonzero status. $GIT_OBJECT_DIRECTORY is set to /foo/bar then "git rev-parse --git-path objects/abc" returns /foo/bar/abc. ---show-cdup:: - When the command is invoked from a subdirectory, show the - path of the top-level directory relative to the current - directory (typically a sequence of "../", or an empty string). - ---show-prefix:: - When the command is invoked from a subdirectory, show the - path of the current directory relative to the top-level - directory. - --show-toplevel:: - Show the absolute path of the top-level directory of the working - tree. If there is no working tree, report an error. + Show the (by default, absolute) path of the top-level directory + of the working tree. If there is no working tree, report an error. --show-superproject-working-tree:: Show the absolute path of the root of the superproject's @@ -279,6 +263,36 @@ print a message to stderr and exit with nonzero status. Show the path to the shared index file in split index mode, or empty if not in split-index mode. +The following options are unaffected by `--path-format`: + +--absolute-git-dir:: + Like `--git-dir`, but its output is always the canonicalized + absolute path. + +--is-inside-git-dir:: + When the current working directory is below the repository + directory print "true", otherwise "false". + +--is-inside-work-tree:: + When the current working directory is inside the work tree of the + repository print "true", otherwise "false". + +--is-bare-repository:: + When the repository is bare print "true", otherwise "false". + +--is-shallow-repository:: + When the repository is shallow print "true", otherwise "false". + +--show-cdup:: + When the command is invoked from a subdirectory, show the + path of the top-level directory relative to the current + directory (typically a sequence of "../", or an empty string). + +--show-prefix:: + When the command is invoked from a subdirectory, show the + path of the current directory relative to the top-level + directory. + --show-object-format[=(storage|input|output)]:: Show the object format (hash algorithm) used for the repository for storage inside the `.git` directory, input, or output. For diff --git a/Documentation/git-send-email.txt b/Documentation/git-send-email.txt index b7bbbeadef..93708aefea 100644 --- a/Documentation/git-send-email.txt +++ b/Documentation/git-send-email.txt @@ -494,10 +494,14 @@ edit ~/.gitconfig to specify your account settings: smtpServerPort = 587 ---- -If you have multifactor authentication setup on your gmail account, you will +If you have multi-factor authentication set up on your Gmail account, you will need to generate an app-specific password for use with 'git send-email'. Visit https://security.google.com/settings/security/apppasswords to create it. +If you do not have multi-factor authentication set up on your Gmail account, +you will need to allow less secure app access. Visit +https://myaccount.google.com/lesssecureapps to enable it. + Once your commits are ready to be sent to the mailing list, run the following commands: diff --git a/Documentation/git-status.txt b/Documentation/git-status.txt index 7731b45f07..c0764e850a 100644 --- a/Documentation/git-status.txt +++ b/Documentation/git-status.txt @@ -184,11 +184,26 @@ characters, that field will be quoted in the manner of a C string literal: surrounded by ASCII double quote (34) characters, and with interior special characters backslash-escaped. -For paths with merge conflicts, `X` and `Y` show the modification -states of each side of the merge. For paths that do not have merge -conflicts, `X` shows the status of the index, and `Y` shows the status -of the work tree. For untracked paths, `XY` are `??`. Other status -codes can be interpreted as follows: +There are three different types of states that are shown using this format, and +each one uses the `XY` syntax differently: + +* When a merge is occurring and the merge was successful, or outside of a merge + situation, `X` shows the status of the index and `Y` shows the status of the + working tree. +* When a merge conflict has occurred and has not yet been resolved, `X` and `Y` + show the state introduced by each head of the merge, relative to the common + ancestor. These paths are said to be _unmerged_. +* When a path is untracked, `X` and `Y` are always the same, since they are + unknown to the index. `??` is used for untracked paths. Ignored files are + not listed unless `--ignored` is used; if it is, ignored files are indicated + by `!!`. + +Note that the term _merge_ here also includes rebases using the default +`--merge` strategy, cherry-picks, and anything else using the merge machinery. + +In the following table, these three classes are shown in separate sections, and +these characters are used for `X` and `Y` fields for the first two sections that +show tracked paths: * ' ' = unmodified * 'M' = modified @@ -198,9 +213,6 @@ codes can be interpreted as follows: * 'C' = copied * 'U' = updated but unmerged -Ignored files are not listed, unless `--ignored` option is in effect, -in which case `XY` are `!!`. - .... X Y Meaning ------------------------------------------------- diff --git a/Documentation/git-update-index.txt b/Documentation/git-update-index.txt index 1489cb09a0..2853f168d9 100644 --- a/Documentation/git-update-index.txt +++ b/Documentation/git-update-index.txt @@ -30,9 +30,8 @@ SYNOPSIS DESCRIPTION ----------- -Modifies the index or directory cache. Each file mentioned is updated -into the index and any 'unmerged' or 'needs updating' state is -cleared. +Modifies the index. Each file mentioned is updated into the index and +any 'unmerged' or 'needs updating' state is cleared. See also linkgit:git-add[1] for a more user-friendly way to do some of the most common operations on the index. diff --git a/Documentation/git-worktree.txt b/Documentation/git-worktree.txt index af06128cc9..02a706c4c0 100644 --- a/Documentation/git-worktree.txt +++ b/Documentation/git-worktree.txt @@ -143,6 +143,11 @@ locate it. Running `repair` within the recently-moved working tree will reestablish the connection. If multiple linked working trees are moved, running `repair` from any working tree with each tree's new `<path>` as an argument, will reestablish the connection to all the specified paths. ++ +If both the main working tree and linked working trees have been moved +manually, then running `repair` in the main working tree and specifying the +new `<path>` of each linked working tree will reestablish all connections +in both directions. unlock:: diff --git a/Documentation/githooks.txt b/Documentation/githooks.txt index ffccfc7760..1f3b57d04d 100644 --- a/Documentation/githooks.txt +++ b/Documentation/githooks.txt @@ -644,7 +644,7 @@ changelist text. Exiting with a non-zero status from the script will abort the process. The purpose of the hook is to edit the message file in place, -and it is not supressed by the `--no-verify` option. This hook +and it is not suppressed by the `--no-verify` option. This hook is called even if `--prepare-p4-only` is set. Run `git-p4 submit --help` for details. diff --git a/Documentation/gitmodules.txt b/Documentation/gitmodules.txt index 539b4e1997..8e333dde1b 100644 --- a/Documentation/gitmodules.txt +++ b/Documentation/gitmodules.txt @@ -7,7 +7,7 @@ gitmodules - Defining submodule properties SYNOPSIS -------- -$GIT_WORK_DIR/.gitmodules +$GIT_WORK_TREE/.gitmodules DESCRIPTION @@ -27,19 +27,19 @@ submodule.<name>.path:: Defines the path, relative to the top-level directory of the Git working tree, where the submodule is expected to be checked out. The path name must not end with a `/`. All submodule paths must - be unique within the .gitmodules file. + be unique within the `.gitmodules` file. submodule.<name>.url:: Defines a URL from which the submodule repository can be cloned. This may be either an absolute URL ready to be passed to - linkgit:git-clone[1] or (if it begins with ./ or ../) a location + linkgit:git-clone[1] or (if it begins with `./` or `../`) a location relative to the superproject's origin repository. In addition, there are a number of optional keys: submodule.<name>.update:: Defines the default update procedure for the named submodule, - i.e. how the submodule is updated by "git submodule update" + i.e. how the submodule is updated by the `git submodule update` command in the superproject. This is only used by `git submodule init` to initialize the configuration variable of the same name. Allowed values here are 'checkout', 'rebase', @@ -49,7 +49,7 @@ submodule.<name>.update:: submodule.<name>.branch:: A remote branch name for tracking updates in the upstream submodule. - If the option is not specified, it defaults to the remote 'HEAD'. + If the option is not specified, it defaults to the remote `HEAD`. A special value of `.` is used to indicate that the name of the branch in the submodule should be the same name as the current branch in the current repository. See the `--remote` documentation in @@ -57,14 +57,14 @@ submodule.<name>.branch:: submodule.<name>.fetchRecurseSubmodules:: This option can be used to control recursive fetching of this - submodule. If this option is also present in the submodules entry in - .git/config of the superproject, the setting there will override the - one found in .gitmodules. + submodule. If this option is also present in the submodule's entry in + `.git/config` of the superproject, the setting there will override the + one found in `.gitmodules`. Both settings can be overridden on the command line by using the - "--[no-]recurse-submodules" option to "git fetch" and "git pull". + `--[no-]recurse-submodules` option to `git fetch` and `git pull`. submodule.<name>.ignore:: - Defines under what circumstances "git status" and the diff family show + Defines under what circumstances `git status` and the diff family show a submodule as modified. The following values are supported: + -- @@ -73,7 +73,7 @@ submodule.<name>.ignore:: been staged). dirty;; All changes to the submodule's work tree will be ignored, only - committed differences between the HEAD of the submodule and its + committed differences between the `HEAD` of the submodule and its recorded state in the superproject are taken into account. untracked;; Only untracked files in submodules will be ignored. @@ -84,12 +84,12 @@ submodule.<name>.ignore:: differences, and modifications to tracked and untracked files are shown. This is the default option. -If this option is also present in the submodules entry in .git/config +If this option is also present in the submodule's entry in `.git/config` of the superproject, the setting there will override the one found in -.gitmodules. +`.gitmodules`. Both settings can be overridden on the command line by using the -"--ignore-submodules" option. The 'git submodule' commands are not +`--ignore-submodules` option. The `git submodule` commands are not affected by this setting. -- @@ -102,7 +102,7 @@ submodule.<name>.shallow:: EXAMPLES -------- -Consider the following .gitmodules file: +Consider the following `.gitmodules` file: ---- [submodule "libfoo"] diff --git a/Documentation/pretty-formats.txt b/Documentation/pretty-formats.txt index 84bbc7439a..6b59e28d44 100644 --- a/Documentation/pretty-formats.txt +++ b/Documentation/pretty-formats.txt @@ -252,7 +252,15 @@ endif::git-rev-list[] interpreted by linkgit:git-interpret-trailers[1]. The `trailers` string may be followed by a colon - and zero or more comma-separated options: + and zero or more comma-separated options. + If any option is provided multiple times the + last occurance wins. ++ +The boolean options accept an optional value `[=<BOOL>]`. The values +`true`, `false`, `on`, `off` etc. are all accepted. See the "boolean" +sub-section in "EXAMPLES" in linkgit:git-config[1]. If a boolean +option is given with no value, it's enabled. ++ ** 'key=<K>': only show trailers with specified key. Matching is done case-insensitively and trailing colon is optional. If option is given multiple times trailer lines matching any of the keys are @@ -261,27 +269,25 @@ endif::git-rev-list[] desired it can be disabled with `only=false`. E.g., `%(trailers:key=Reviewed-by)` shows trailer lines with key `Reviewed-by`. -** 'only[=val]': select whether non-trailer lines from the trailer - block should be included. The `only` keyword may optionally be - followed by an equal sign and one of `true`, `on`, `yes` to omit or - `false`, `off`, `no` to show the non-trailer lines. If option is - given without value it is enabled. If given multiple times the last - value is used. +** 'only[=<BOOL>]': select whether non-trailer lines from the trailer + block should be included. ** 'separator=<SEP>': specify a separator inserted between trailer lines. When this option is not given each trailer line is terminated with a line feed character. The string SEP may contain the literal formatting codes described above. To use comma as separator one must use `%x2C` as it would otherwise be parsed as - next option. If separator option is given multiple times only the - last one is used. E.g., `%(trailers:key=Ticket,separator=%x2C )` + next option. E.g., `%(trailers:key=Ticket,separator=%x2C )` shows all trailer lines whose key is "Ticket" separated by a comma and a space. -** 'unfold[=val]': make it behave as if interpret-trailer's `--unfold` - option was given. In same way as to for `only` it can be followed - by an equal sign and explicit value. E.g., +** 'unfold[=<BOOL>]': make it behave as if interpret-trailer's `--unfold` + option was given. E.g., `%(trailers:only,unfold=true)` unfolds and shows all trailer lines. -** 'valueonly[=val]': skip over the key part of the trailer line and only - show the value part. Also this optionally allows explicit value. +** 'keyonly[=<BOOL>]': only show the key part of the trailer. +** 'valueonly[=<BOOL>]': only show the value part of the trailer. +** 'key_value_separator=<SEP>': specify a separator inserted between + trailer lines. When this option is not given each trailer key-value + pair is separated by ": ". Otherwise it shares the same semantics + as 'separator=<SEP>' above. NOTE: Some placeholders may depend on other options given to the revision traversal engine. For example, the `%g*` reflog options will diff --git a/Documentation/technical/pack-format.txt b/Documentation/technical/pack-format.txt index f96b2e605f..96d2fc589f 100644 --- a/Documentation/technical/pack-format.txt +++ b/Documentation/technical/pack-format.txt @@ -55,6 +55,18 @@ Valid object types are: Type 5 is reserved for future expansion. Type 0 is invalid. +=== Size encoding + +This document uses the following "size encoding" of non-negative +integers: From each byte, the seven least significant bits are +used to form the resulting integer. As long as the most significant +bit is 1, this process continues; the byte with MSB 0 provides the +last seven bits. The seven-bit chunks are concatenated. Later +values are more significant. + +This size encoding should not be confused with the "offset encoding", +which is also used in this document. + === Deltified representation Conceptually there are only four object types: commit, tree, tag and @@ -73,7 +85,10 @@ Ref-delta can also refer to an object outside the pack (i.e. the so-called "thin pack"). When stored on disk however, the pack should be self contained to avoid cyclic dependency. -The delta data is a sequence of instructions to reconstruct an object +The delta data starts with the size of the base object and the +size of the object to be reconstructed. These sizes are +encoded using the size encoding from above. The remainder of +the delta data is a sequence of instructions to reconstruct the object from the base object. If the base object is deltified, it must be converted to canonical form first. Each instruction appends more and more data to the target object until it's complete. There are two diff --git a/Documentation/technical/reftable.txt b/Documentation/technical/reftable.txt index 2951840e9c..8095ab2590 100644 --- a/Documentation/technical/reftable.txt +++ b/Documentation/technical/reftable.txt @@ -446,7 +446,7 @@ especially if readers will not use the object name to ref mapping. Object blocks use unique, abbreviated 2-32 object name keys, mapping to ref blocks containing references pointing to that object directly, or as the peeled value of an annotated tag. Like ref blocks, object blocks use -the file's standard block size. The abbrevation length is available in +the file's standard block size. The abbreviation length is available in the footer as `obj_id_len`. To save space in small files, object blocks may be omitted if the ref diff --git a/GIT-VERSION-GEN b/GIT-VERSION-GEN index 5085ef1390..0327733794 100755 --- a/GIT-VERSION-GEN +++ b/GIT-VERSION-GEN @@ -1,7 +1,7 @@ #!/bin/sh GVF=GIT-VERSION-FILE -DEF_VER=v2.30.0 +DEF_VER=v2.30.GIT LF=' ' @@ -901,6 +901,7 @@ LIB_OBJS += gettext.o LIB_OBJS += gpg-interface.o LIB_OBJS += graph.o LIB_OBJS += grep.o +LIB_OBJS += hash-lookup.o LIB_OBJS += hashmap.o LIB_OBJS += help.o LIB_OBJS += hex.o @@ -937,6 +938,8 @@ LIB_OBJS += notes-cache.o LIB_OBJS += notes-merge.o LIB_OBJS += notes-utils.o LIB_OBJS += notes.o +LIB_OBJS += object-file.o +LIB_OBJS += object-name.o LIB_OBJS += object.o LIB_OBJS += oid-array.o LIB_OBJS += oidmap.o @@ -993,9 +996,6 @@ LIB_OBJS += sequencer.o LIB_OBJS += serve.o LIB_OBJS += server-info.o LIB_OBJS += setup.o -LIB_OBJS += sha1-file.o -LIB_OBJS += sha1-lookup.o -LIB_OBJS += sha1-name.o LIB_OBJS += shallow.o LIB_OBJS += sideband.o LIB_OBJS += sigchain.o @@ -1554,9 +1554,6 @@ endif ifdef NO_SYMLINK_HEAD BASIC_CFLAGS += -DNO_SYMLINK_HEAD endif -ifdef GETTEXT_POISON -$(warning The GETTEXT_POISON option has been removed in favor of runtime GIT_TEST_GETTEXT_POISON. See t/README!) -endif ifdef NO_GETTEXT BASIC_CFLAGS += -DNO_GETTEXT USE_GETTEXT_SCHEME ?= fallthrough @@ -1 +1 @@ -Documentation/RelNotes/2.30.0.txt
\ No newline at end of file +Documentation/RelNotes/2.31.0.txt
\ No newline at end of file @@ -67,19 +67,15 @@ static void get_root_part(struct strbuf *resolved, struct strbuf *remaining) #endif /* - * Return the real path (i.e., absolute path, with symlinks resolved - * and extra slashes removed) equivalent to the specified path. (If - * you want an absolute path but don't mind links, use - * absolute_path().) Places the resolved realpath in the provided strbuf. - * - * The directory part of path (i.e., everything up to the last - * dir_sep) must denote a valid, existing directory, but the last - * component need not exist. If die_on_error is set, then die with an - * informative error message if there is a problem. Otherwise, return - * NULL on errors (without generating any output). + * If set, any number of trailing components may be missing; otherwise, only one + * may be. */ -char *strbuf_realpath(struct strbuf *resolved, const char *path, - int die_on_error) +#define REALPATH_MANY_MISSING (1 << 0) +/* Should we die if there's an error? */ +#define REALPATH_DIE_ON_ERROR (1 << 1) + +static char *strbuf_realpath_1(struct strbuf *resolved, const char *path, + int flags) { struct strbuf remaining = STRBUF_INIT; struct strbuf next = STRBUF_INIT; @@ -89,7 +85,7 @@ char *strbuf_realpath(struct strbuf *resolved, const char *path, struct stat st; if (!*path) { - if (die_on_error) + if (flags & REALPATH_DIE_ON_ERROR) die("The empty string is not a valid path"); else goto error_out; @@ -101,7 +97,7 @@ char *strbuf_realpath(struct strbuf *resolved, const char *path, if (!resolved->len) { /* relative path; can use CWD as the initial resolved path */ if (strbuf_getcwd(resolved)) { - if (die_on_error) + if (flags & REALPATH_DIE_ON_ERROR) die_errno("unable to get current working directory"); else goto error_out; @@ -129,8 +125,9 @@ char *strbuf_realpath(struct strbuf *resolved, const char *path, if (lstat(resolved->buf, &st)) { /* error out unless this was the last component */ - if (errno != ENOENT || remaining.len) { - if (die_on_error) + if (errno != ENOENT || + (!(flags & REALPATH_MANY_MISSING) && remaining.len)) { + if (flags & REALPATH_DIE_ON_ERROR) die_errno("Invalid path '%s'", resolved->buf); else @@ -143,7 +140,7 @@ char *strbuf_realpath(struct strbuf *resolved, const char *path, if (num_symlinks++ > MAXSYMLINKS) { errno = ELOOP; - if (die_on_error) + if (flags & REALPATH_DIE_ON_ERROR) die("More than %d nested symlinks " "on path '%s'", MAXSYMLINKS, path); else @@ -153,7 +150,7 @@ char *strbuf_realpath(struct strbuf *resolved, const char *path, len = strbuf_readlink(&symlink, resolved->buf, st.st_size); if (len < 0) { - if (die_on_error) + if (flags & REALPATH_DIE_ON_ERROR) die_errno("Invalid symlink '%s'", resolved->buf); else @@ -202,6 +199,37 @@ error_out: return retval; } +/* + * Return the real path (i.e., absolute path, with symlinks resolved + * and extra slashes removed) equivalent to the specified path. (If + * you want an absolute path but don't mind links, use + * absolute_path().) Places the resolved realpath in the provided strbuf. + * + * The directory part of path (i.e., everything up to the last + * dir_sep) must denote a valid, existing directory, but the last + * component need not exist. If die_on_error is set, then die with an + * informative error message if there is a problem. Otherwise, return + * NULL on errors (without generating any output). + */ +char *strbuf_realpath(struct strbuf *resolved, const char *path, + int die_on_error) +{ + return strbuf_realpath_1(resolved, path, + die_on_error ? REALPATH_DIE_ON_ERROR : 0); +} + +/* + * Just like strbuf_realpath, but allows an arbitrary number of path + * components to be missing. + */ +char *strbuf_realpath_forgiving(struct strbuf *resolved, const char *path, + int die_on_error) +{ + return strbuf_realpath_1(resolved, path, + ((die_on_error ? REALPATH_DIE_ON_ERROR : 0) | + REALPATH_MANY_MISSING)); +} + char *real_pathdup(const char *path, int die_on_error) { struct strbuf realpath = STRBUF_INIT; @@ -6,7 +6,7 @@ #include "refs.h" #include "list-objects.h" #include "quote.h" -#include "sha1-lookup.h" +#include "hash-lookup.h" #include "run-command.h" #include "log-tree.h" #include "bisect.h" diff --git a/builtin/am.c b/builtin/am.c index f22c73a05b..8355e3566f 100644 --- a/builtin/am.c +++ b/builtin/am.c @@ -2284,10 +2284,10 @@ int cmd_am(int argc, const char **argv, const char *prefix) N_("skip the current patch"), RESUME_SKIP), OPT_CMDMODE(0, "abort", &resume.mode, - N_("restore the original branch and abort the patching operation."), + N_("restore the original branch and abort the patching operation"), RESUME_ABORT), OPT_CMDMODE(0, "quit", &resume.mode, - N_("abort the patching operation but keep HEAD where it is."), + N_("abort the patching operation but keep HEAD where it is"), RESUME_QUIT), { OPTION_CALLBACK, 0, "show-current-patch", &resume.mode, "(diff|raw)", diff --git a/builtin/blame.c b/builtin/blame.c index 6f7e32411a..2c1c02c455 100644 --- a/builtin/blame.c +++ b/builtin/blame.c @@ -866,33 +866,33 @@ int cmd_blame(int argc, const char **argv, const char *prefix) const char *revs_file = NULL; const char *contents_from = NULL; const struct option options[] = { - OPT_BOOL(0, "incremental", &incremental, N_("Show blame entries as we find them, incrementally")), - OPT_BOOL('b', NULL, &blank_boundary, N_("Do not show object names of boundary commits (Default: off)")), - OPT_BOOL(0, "root", &show_root, N_("Do not treat root commits as boundaries (Default: off)")), - OPT_BOOL(0, "show-stats", &show_stats, N_("Show work cost statistics")), - OPT_BOOL(0, "progress", &show_progress, N_("Force progress reporting")), - OPT_BIT(0, "score-debug", &output_option, N_("Show output score for blame entries"), OUTPUT_SHOW_SCORE), - OPT_BIT('f', "show-name", &output_option, N_("Show original filename (Default: auto)"), OUTPUT_SHOW_NAME), - OPT_BIT('n', "show-number", &output_option, N_("Show original linenumber (Default: off)"), OUTPUT_SHOW_NUMBER), - OPT_BIT('p', "porcelain", &output_option, N_("Show in a format designed for machine consumption"), OUTPUT_PORCELAIN), - OPT_BIT(0, "line-porcelain", &output_option, N_("Show porcelain format with per-line commit information"), OUTPUT_PORCELAIN|OUTPUT_LINE_PORCELAIN), - OPT_BIT('c', NULL, &output_option, N_("Use the same output mode as git-annotate (Default: off)"), OUTPUT_ANNOTATE_COMPAT), - OPT_BIT('t', NULL, &output_option, N_("Show raw timestamp (Default: off)"), OUTPUT_RAW_TIMESTAMP), - OPT_BIT('l', NULL, &output_option, N_("Show long commit SHA1 (Default: off)"), OUTPUT_LONG_OBJECT_NAME), - 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_BOOL(0, "incremental", &incremental, N_("show blame entries as we find them, incrementally")), + OPT_BOOL('b', NULL, &blank_boundary, N_("do not show object names of boundary commits (Default: off)")), + OPT_BOOL(0, "root", &show_root, N_("do not treat root commits as boundaries (Default: off)")), + OPT_BOOL(0, "show-stats", &show_stats, N_("show work cost statistics")), + OPT_BOOL(0, "progress", &show_progress, N_("force progress reporting")), + OPT_BIT(0, "score-debug", &output_option, N_("show output score for blame entries"), OUTPUT_SHOW_SCORE), + OPT_BIT('f', "show-name", &output_option, N_("show original filename (Default: auto)"), OUTPUT_SHOW_NAME), + OPT_BIT('n', "show-number", &output_option, N_("show original linenumber (Default: off)"), OUTPUT_SHOW_NUMBER), + OPT_BIT('p', "porcelain", &output_option, N_("show in a format designed for machine consumption"), OUTPUT_PORCELAIN), + OPT_BIT(0, "line-porcelain", &output_option, N_("show porcelain format with per-line commit information"), OUTPUT_PORCELAIN|OUTPUT_LINE_PORCELAIN), + OPT_BIT('c', NULL, &output_option, N_("use the same output mode as git-annotate (Default: off)"), OUTPUT_ANNOTATE_COMPAT), + OPT_BIT('t', NULL, &output_option, N_("show raw timestamp (Default: off)"), OUTPUT_RAW_TIMESTAMP), + OPT_BIT('l', NULL, &output_option, N_("show long commit SHA1 (Default: off)"), OUTPUT_LONG_OBJECT_NAME), + 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), - OPT_BIT(0, "minimal", &xdl_opts, N_("Spend extra cycles to find better match"), XDF_NEED_MINIMAL), - OPT_STRING('S', NULL, &revs_file, N_("file"), N_("Use revisions from <file> instead of calling git-rev-list")), - OPT_STRING(0, "contents", &contents_from, N_("file"), N_("Use <file>'s contents as the final image")), - OPT_CALLBACK_F('C', NULL, &opt, N_("score"), N_("Find line copies within and across files"), PARSE_OPT_OPTARG, blame_copy_callback), - OPT_CALLBACK_F('M', NULL, &opt, N_("score"), N_("Find line movements within and across files"), PARSE_OPT_OPTARG, blame_move_callback), + OPT_BIT(0, "minimal", &xdl_opts, N_("spend extra cycles to find better match"), XDF_NEED_MINIMAL), + OPT_STRING('S', NULL, &revs_file, N_("file"), N_("use revisions from <file> instead of calling git-rev-list")), + OPT_STRING(0, "contents", &contents_from, N_("file"), N_("use <file>'s contents as the final image")), + OPT_CALLBACK_F('C', NULL, &opt, N_("score"), N_("find line copies within and across files"), PARSE_OPT_OPTARG, blame_copy_callback), + OPT_CALLBACK_F('M', NULL, &opt, N_("score"), N_("find line movements within and across files"), PARSE_OPT_OPTARG, blame_move_callback), OPT_STRING_LIST('L', NULL, &range_list, N_("range"), - N_("Process only line range <start>,<end> or function :<funcname>")), + N_("process only line range <start>,<end> or function :<funcname>")), OPT__ABBREV(&abbrev), OPT_END() }; diff --git a/builtin/commit-graph.c b/builtin/commit-graph.c index 78fa08f43a..cd86315221 100644 --- a/builtin/commit-graph.c +++ b/builtin/commit-graph.c @@ -78,7 +78,7 @@ static int graph_verify(int argc, const char **argv) 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")), + 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_BOOL(0, "progress", &opts.progress, N_("force progress reporting")), @@ -208,7 +208,7 @@ static int graph_write(int argc, const char **argv) static struct option builtin_commit_graph_write_options[] = { OPT_STRING(0, "object-dir", &opts.obj_dir, N_("dir"), - N_("The object directory to store the graph")), + N_("the object directory to store the graph")), OPT_BOOL(0, "reachable", &opts.reachable, N_("start walk at all refs")), OPT_BOOL(0, "stdin-packs", &opts.stdin_packs, @@ -314,7 +314,7 @@ int cmd_commit_graph(int argc, const char **argv, const char *prefix) static struct option builtin_commit_graph_options[] = { OPT_STRING(0, "object-dir", &opts.obj_dir, N_("dir"), - N_("The object directory to store the graph")), + N_("the object directory to store the graph")), OPT_END(), }; diff --git a/builtin/fast-export.c b/builtin/fast-export.c index 0a60356b06..85a76e0ef8 100644 --- a/builtin/fast-export.c +++ b/builtin/fast-export.c @@ -1205,32 +1205,32 @@ int cmd_fast_export(int argc, const char **argv, const char *prefix) N_("select handling of commit messages in an alternate encoding"), parse_opt_reencode_mode), OPT_STRING(0, "export-marks", &export_filename, N_("file"), - N_("Dump marks to this file")), + N_("dump marks to this file")), OPT_STRING(0, "import-marks", &import_filename, N_("file"), - N_("Import marks from this file")), + N_("import marks from this file")), OPT_STRING(0, "import-marks-if-exists", &import_filename_if_exists, N_("file"), - N_("Import marks from this file if it exists")), + N_("import marks from this file if it exists")), OPT_BOOL(0, "fake-missing-tagger", &fake_missing_tagger, - N_("Fake a tagger when tags lack one")), + N_("fake a tagger when tags lack one")), OPT_BOOL(0, "full-tree", &full_tree, - N_("Output full tree for each commit")), + N_("output full tree for each commit")), OPT_BOOL(0, "use-done-feature", &use_done_feature, - N_("Use the done feature to terminate the stream")), - OPT_BOOL(0, "no-data", &no_data, N_("Skip output of blob data")), + N_("use the done feature to terminate the stream")), + OPT_BOOL(0, "no-data", &no_data, N_("skip output of blob data")), OPT_STRING_LIST(0, "refspec", &refspecs_list, N_("refspec"), - N_("Apply refspec to exported refs")), + N_("apply refspec to exported refs")), OPT_BOOL(0, "anonymize", &anonymize, N_("anonymize output")), OPT_CALLBACK_F(0, "anonymize-map", &anonymized_seeds, N_("from:to"), N_("convert <from> to <to> in anonymized output"), PARSE_OPT_NONEG, parse_opt_anonymize_map), OPT_BOOL(0, "reference-excluded-parents", - &reference_excluded_commits, N_("Reference parents which are not in fast-export stream by object id")), + &reference_excluded_commits, N_("reference parents which are not in fast-export stream by object id")), OPT_BOOL(0, "show-original-ids", &show_original_ids, - N_("Show original object ids of blobs/commits")), + N_("show original object ids of blobs/commits")), OPT_BOOL(0, "mark-tags", &mark_tags, - N_("Label tags with mark ids")), + N_("label tags with mark ids")), OPT_END() }; diff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c index 5bba623ff1..52be64a437 100644 --- a/builtin/for-each-repo.c +++ b/builtin/for-each-repo.c @@ -51,6 +51,13 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix) values = repo_config_get_value_multi(the_repository, config_key); + /* + * Do nothing on an empty list, which is equivalent to the case + * where the config variable does not exist at all. + */ + if (!values) + return 0; + for (i = 0; !result && i < values->nr; i++) result = run_command_on_repo(values->items[i].string, &args); diff --git a/builtin/gc.c b/builtin/gc.c index 4c24f41852..b315b2ad58 100644 --- a/builtin/gc.c +++ b/builtin/gc.c @@ -1493,38 +1493,368 @@ static int maintenance_unregister(void) return run_command(&config_unset); } +static const char *get_frequency(enum schedule_priority schedule) +{ + switch (schedule) { + case SCHEDULE_HOURLY: + return "hourly"; + case SCHEDULE_DAILY: + return "daily"; + case SCHEDULE_WEEKLY: + return "weekly"; + default: + BUG("invalid schedule %d", schedule); + } +} + +static char *launchctl_service_name(const char *frequency) +{ + struct strbuf label = STRBUF_INIT; + strbuf_addf(&label, "org.git-scm.git.%s", frequency); + return strbuf_detach(&label, NULL); +} + +static char *launchctl_service_filename(const char *name) +{ + char *expanded; + struct strbuf filename = STRBUF_INIT; + strbuf_addf(&filename, "~/Library/LaunchAgents/%s.plist", name); + + expanded = expand_user_path(filename.buf, 1); + if (!expanded) + die(_("failed to expand path '%s'"), filename.buf); + + strbuf_release(&filename); + return expanded; +} + +static char *launchctl_get_uid(void) +{ + return xstrfmt("gui/%d", getuid()); +} + +static int launchctl_boot_plist(int enable, const char *filename, const char *cmd) +{ + int result; + struct child_process child = CHILD_PROCESS_INIT; + char *uid = launchctl_get_uid(); + + strvec_split(&child.args, cmd); + if (enable) + strvec_push(&child.args, "bootstrap"); + else + strvec_push(&child.args, "bootout"); + strvec_push(&child.args, uid); + strvec_push(&child.args, filename); + + child.no_stderr = 1; + child.no_stdout = 1; + + if (start_command(&child)) + die(_("failed to start launchctl")); + + result = finish_command(&child); + + free(uid); + return result; +} + +static int launchctl_remove_plist(enum schedule_priority schedule, const char *cmd) +{ + const char *frequency = get_frequency(schedule); + char *name = launchctl_service_name(frequency); + char *filename = launchctl_service_filename(name); + int result = launchctl_boot_plist(0, filename, cmd); + unlink(filename); + free(filename); + free(name); + return result; +} + +static int launchctl_remove_plists(const char *cmd) +{ + return launchctl_remove_plist(SCHEDULE_HOURLY, cmd) || + launchctl_remove_plist(SCHEDULE_DAILY, cmd) || + launchctl_remove_plist(SCHEDULE_WEEKLY, cmd); +} + +static int launchctl_schedule_plist(const char *exec_path, enum schedule_priority schedule, const char *cmd) +{ + FILE *plist; + int i; + const char *preamble, *repeat; + const char *frequency = get_frequency(schedule); + char *name = launchctl_service_name(frequency); + char *filename = launchctl_service_filename(name); + + if (safe_create_leading_directories(filename)) + die(_("failed to create directories for '%s'"), filename); + plist = xfopen(filename, "w"); + + preamble = "<?xml version=\"1.0\"?>\n" + "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n" + "<plist version=\"1.0\">" + "<dict>\n" + "<key>Label</key><string>%s</string>\n" + "<key>ProgramArguments</key>\n" + "<array>\n" + "<string>%s/git</string>\n" + "<string>--exec-path=%s</string>\n" + "<string>for-each-repo</string>\n" + "<string>--config=maintenance.repo</string>\n" + "<string>maintenance</string>\n" + "<string>run</string>\n" + "<string>--schedule=%s</string>\n" + "</array>\n" + "<key>StartCalendarInterval</key>\n" + "<array>\n"; + fprintf(plist, preamble, name, exec_path, exec_path, frequency); + + switch (schedule) { + case SCHEDULE_HOURLY: + repeat = "<dict>\n" + "<key>Hour</key><integer>%d</integer>\n" + "<key>Minute</key><integer>0</integer>\n" + "</dict>\n"; + for (i = 1; i <= 23; i++) + fprintf(plist, repeat, i); + break; + + case SCHEDULE_DAILY: + repeat = "<dict>\n" + "<key>Day</key><integer>%d</integer>\n" + "<key>Hour</key><integer>0</integer>\n" + "<key>Minute</key><integer>0</integer>\n" + "</dict>\n"; + for (i = 1; i <= 6; i++) + fprintf(plist, repeat, i); + break; + + case SCHEDULE_WEEKLY: + fprintf(plist, + "<dict>\n" + "<key>Day</key><integer>0</integer>\n" + "<key>Hour</key><integer>0</integer>\n" + "<key>Minute</key><integer>0</integer>\n" + "</dict>\n"); + break; + + default: + /* unreachable */ + break; + } + fprintf(plist, "</array>\n</dict>\n</plist>\n"); + fclose(plist); + + /* bootout might fail if not already running, so ignore */ + launchctl_boot_plist(0, filename, cmd); + if (launchctl_boot_plist(1, filename, cmd)) + die(_("failed to bootstrap service %s"), filename); + + free(filename); + free(name); + return 0; +} + +static int launchctl_add_plists(const char *cmd) +{ + const char *exec_path = git_exec_path(); + + return launchctl_schedule_plist(exec_path, SCHEDULE_HOURLY, cmd) || + launchctl_schedule_plist(exec_path, SCHEDULE_DAILY, cmd) || + launchctl_schedule_plist(exec_path, SCHEDULE_WEEKLY, cmd); +} + +static int launchctl_update_schedule(int run_maintenance, int fd, const char *cmd) +{ + if (run_maintenance) + return launchctl_add_plists(cmd); + else + return launchctl_remove_plists(cmd); +} + +static char *schtasks_task_name(const char *frequency) +{ + struct strbuf label = STRBUF_INIT; + strbuf_addf(&label, "Git Maintenance (%s)", frequency); + return strbuf_detach(&label, NULL); +} + +static int schtasks_remove_task(enum schedule_priority schedule, const char *cmd) +{ + int result; + struct strvec args = STRVEC_INIT; + const char *frequency = get_frequency(schedule); + char *name = schtasks_task_name(frequency); + + strvec_split(&args, cmd); + strvec_pushl(&args, "/delete", "/tn", name, "/f", NULL); + + result = run_command_v_opt(args.v, 0); + + strvec_clear(&args); + free(name); + return result; +} + +static int schtasks_remove_tasks(const char *cmd) +{ + return schtasks_remove_task(SCHEDULE_HOURLY, cmd) || + schtasks_remove_task(SCHEDULE_DAILY, cmd) || + schtasks_remove_task(SCHEDULE_WEEKLY, cmd); +} + +static int schtasks_schedule_task(const char *exec_path, enum schedule_priority schedule, const char *cmd) +{ + int result; + struct child_process child = CHILD_PROCESS_INIT; + const char *xml; + struct tempfile *tfile; + const char *frequency = get_frequency(schedule); + char *name = schtasks_task_name(frequency); + struct strbuf tfilename = STRBUF_INIT; + + strbuf_addf(&tfilename, "%s/schedule_%s_XXXXXX", + get_git_common_dir(), frequency); + tfile = xmks_tempfile(tfilename.buf); + strbuf_release(&tfilename); + + if (!fdopen_tempfile(tfile, "w")) + die(_("failed to create temp xml file")); + + xml = "<?xml version=\"1.0\" ?>\n" + "<Task version=\"1.4\" xmlns=\"http://schemas.microsoft.com/windows/2004/02/mit/task\">\n" + "<Triggers>\n" + "<CalendarTrigger>\n"; + fputs(xml, tfile->fp); + + switch (schedule) { + case SCHEDULE_HOURLY: + fprintf(tfile->fp, + "<StartBoundary>2020-01-01T01:00:00</StartBoundary>\n" + "<Enabled>true</Enabled>\n" + "<ScheduleByDay>\n" + "<DaysInterval>1</DaysInterval>\n" + "</ScheduleByDay>\n" + "<Repetition>\n" + "<Interval>PT1H</Interval>\n" + "<Duration>PT23H</Duration>\n" + "<StopAtDurationEnd>false</StopAtDurationEnd>\n" + "</Repetition>\n"); + break; + + case SCHEDULE_DAILY: + fprintf(tfile->fp, + "<StartBoundary>2020-01-01T00:00:00</StartBoundary>\n" + "<Enabled>true</Enabled>\n" + "<ScheduleByWeek>\n" + "<DaysOfWeek>\n" + "<Monday />\n" + "<Tuesday />\n" + "<Wednesday />\n" + "<Thursday />\n" + "<Friday />\n" + "<Saturday />\n" + "</DaysOfWeek>\n" + "<WeeksInterval>1</WeeksInterval>\n" + "</ScheduleByWeek>\n"); + break; + + case SCHEDULE_WEEKLY: + fprintf(tfile->fp, + "<StartBoundary>2020-01-01T00:00:00</StartBoundary>\n" + "<Enabled>true</Enabled>\n" + "<ScheduleByWeek>\n" + "<DaysOfWeek>\n" + "<Sunday />\n" + "</DaysOfWeek>\n" + "<WeeksInterval>1</WeeksInterval>\n" + "</ScheduleByWeek>\n"); + break; + + default: + break; + } + + xml = "</CalendarTrigger>\n" + "</Triggers>\n" + "<Principals>\n" + "<Principal id=\"Author\">\n" + "<LogonType>InteractiveToken</LogonType>\n" + "<RunLevel>LeastPrivilege</RunLevel>\n" + "</Principal>\n" + "</Principals>\n" + "<Settings>\n" + "<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>\n" + "<Enabled>true</Enabled>\n" + "<Hidden>true</Hidden>\n" + "<UseUnifiedSchedulingEngine>true</UseUnifiedSchedulingEngine>\n" + "<WakeToRun>false</WakeToRun>\n" + "<ExecutionTimeLimit>PT72H</ExecutionTimeLimit>\n" + "<Priority>7</Priority>\n" + "</Settings>\n" + "<Actions Context=\"Author\">\n" + "<Exec>\n" + "<Command>\"%s\\git.exe\"</Command>\n" + "<Arguments>--exec-path=\"%s\" for-each-repo --config=maintenance.repo maintenance run --schedule=%s</Arguments>\n" + "</Exec>\n" + "</Actions>\n" + "</Task>\n"; + fprintf(tfile->fp, xml, exec_path, exec_path, frequency); + strvec_split(&child.args, cmd); + strvec_pushl(&child.args, "/create", "/tn", name, "/f", "/xml", + get_tempfile_path(tfile), NULL); + close_tempfile_gently(tfile); + + child.no_stdout = 1; + child.no_stderr = 1; + + if (start_command(&child)) + die(_("failed to start schtasks")); + result = finish_command(&child); + + delete_tempfile(&tfile); + free(name); + return result; +} + +static int schtasks_schedule_tasks(const char *cmd) +{ + const char *exec_path = git_exec_path(); + + return schtasks_schedule_task(exec_path, SCHEDULE_HOURLY, cmd) || + schtasks_schedule_task(exec_path, SCHEDULE_DAILY, cmd) || + schtasks_schedule_task(exec_path, SCHEDULE_WEEKLY, cmd); +} + +static int schtasks_update_schedule(int run_maintenance, int fd, const char *cmd) +{ + if (run_maintenance) + return schtasks_schedule_tasks(cmd); + else + return schtasks_remove_tasks(cmd); +} + #define BEGIN_LINE "# BEGIN GIT MAINTENANCE SCHEDULE" #define END_LINE "# END GIT MAINTENANCE SCHEDULE" -static int update_background_schedule(int run_maintenance) +static int crontab_update_schedule(int run_maintenance, int fd, const char *cmd) { int result = 0; int in_old_region = 0; struct child_process crontab_list = CHILD_PROCESS_INIT; struct child_process crontab_edit = CHILD_PROCESS_INIT; FILE *cron_list, *cron_in; - const char *crontab_name; struct strbuf line = STRBUF_INIT; - struct lock_file lk; - char *lock_path = xstrfmt("%s/schedule", the_repository->objects->odb->path); - if (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0) - return error(_("another process is scheduling background maintenance")); - - crontab_name = getenv("GIT_TEST_CRONTAB"); - if (!crontab_name) - crontab_name = "crontab"; - - strvec_split(&crontab_list.args, crontab_name); + strvec_split(&crontab_list.args, cmd); strvec_push(&crontab_list.args, "-l"); crontab_list.in = -1; - crontab_list.out = dup(lk.tempfile->fd); + crontab_list.out = dup(fd); crontab_list.git_cmd = 0; - if (start_command(&crontab_list)) { - result = error(_("failed to run 'crontab -l'; your system might not support 'cron'")); - goto cleanup; - } + if (start_command(&crontab_list)) + return error(_("failed to run 'crontab -l'; your system might not support 'cron'")); /* Ignore exit code, as an empty crontab will return error. */ finish_command(&crontab_list); @@ -1533,17 +1863,15 @@ static int update_background_schedule(int run_maintenance) * Read from the .lock file, filtering out the old * schedule while appending the new schedule. */ - cron_list = fdopen(lk.tempfile->fd, "r"); + cron_list = fdopen(fd, "r"); rewind(cron_list); - strvec_split(&crontab_edit.args, crontab_name); + strvec_split(&crontab_edit.args, cmd); crontab_edit.in = -1; crontab_edit.git_cmd = 0; - if (start_command(&crontab_edit)) { - result = error(_("failed to run 'crontab'; your system might not support 'cron'")); - goto cleanup; - } + if (start_command(&crontab_edit)) + return error(_("failed to run 'crontab'; your system might not support 'cron'")); cron_in = fdopen(crontab_edit.in, "w"); if (!cron_in) { @@ -1587,14 +1915,54 @@ static int update_background_schedule(int run_maintenance) close(crontab_edit.in); done_editing: - if (finish_command(&crontab_edit)) { + if (finish_command(&crontab_edit)) result = error(_("'crontab' died")); - goto cleanup; + else + fclose(cron_list); + return result; +} + +#if defined(__APPLE__) +static const char platform_scheduler[] = "launchctl"; +#elif defined(GIT_WINDOWS_NATIVE) +static const char platform_scheduler[] = "schtasks"; +#else +static const char platform_scheduler[] = "crontab"; +#endif + +static int update_background_schedule(int enable) +{ + int result; + const char *scheduler = platform_scheduler; + const char *cmd = scheduler; + char *testing; + struct lock_file lk; + char *lock_path = xstrfmt("%s/schedule", the_repository->objects->odb->path); + + testing = xstrdup_or_null(getenv("GIT_TEST_MAINT_SCHEDULER")); + if (testing) { + char *sep = strchr(testing, ':'); + if (!sep) + die("GIT_TEST_MAINT_SCHEDULER unparseable: %s", testing); + *sep = '\0'; + scheduler = testing; + cmd = sep + 1; } - fclose(cron_list); -cleanup: + if (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0) + return error(_("another process is scheduling background maintenance")); + + if (!strcmp(scheduler, "launchctl")) + result = launchctl_update_schedule(enable, lk.tempfile->fd, cmd); + else if (!strcmp(scheduler, "schtasks")) + result = schtasks_update_schedule(enable, lk.tempfile->fd, cmd); + else if (!strcmp(scheduler, "crontab")) + result = crontab_update_schedule(enable, lk.tempfile->fd, cmd); + else + die("unknown background scheduler: %s", scheduler); + rollback_lock_file(&lk); + free(testing); return result; } diff --git a/builtin/index-pack.c b/builtin/index-pack.c index 4b8d86e0ad..557bd2f348 100644 --- a/builtin/index-pack.c +++ b/builtin/index-pack.c @@ -1641,7 +1641,7 @@ static void read_idx_option(struct pack_idx_option *opts, const char *pack_name) /* * Get rid of the idx file as we do not need it anymore. * NEEDSWORK: extract this bit from free_pack_by_name() in - * sha1-file.c, perhaps? It shouldn't matter very much as we + * object-file.c, perhaps? It shouldn't matter very much as we * know we haven't installed this pack (hence we never have * read anything from it). */ diff --git a/builtin/log.c b/builtin/log.c index bd6ff4f9f9..f23ccdbec3 100644 --- a/builtin/log.c +++ b/builtin/log.c @@ -177,7 +177,7 @@ static void cmd_log_init_finish(int argc, const char **argv, const char *prefix, const struct option builtin_log_options[] = { OPT__QUIET(&quiet, N_("suppress diff output")), OPT_BOOL(0, "source", &source, N_("show source")), - OPT_BOOL(0, "use-mailmap", &mailmap, N_("Use mail map file")), + OPT_BOOL(0, "use-mailmap", &mailmap, N_("use mail map file")), OPT_ALIAS(0, "mailmap", "use-mailmap"), OPT_STRING_LIST(0, "decorate-refs", &decorate_refs_include, N_("pattern"), N_("only decorate refs that match <pattern>")), @@ -186,7 +186,7 @@ static void cmd_log_init_finish(int argc, const char **argv, const char *prefix, OPT_CALLBACK_F(0, "decorate", NULL, NULL, N_("decorate options"), PARSE_OPT_OPTARG, decorate_callback), OPT_CALLBACK('L', NULL, &line_cb, "range:file", - N_("Trace the evolution of line range <start>,<end> or function :<funcname> in <file>"), + N_("trace the evolution of line range <start>,<end> or function :<funcname> in <file>"), log_line_range_callback), OPT_END() }; @@ -1757,13 +1757,13 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix) OPT_INTEGER(0, "filename-max-length", &fmt_patch_name_max, N_("max length of output filename")), OPT_CALLBACK_F(0, "rfc", &rev, NULL, - N_("Use [RFC PATCH] instead of [PATCH]"), + N_("use [RFC PATCH] instead of [PATCH]"), PARSE_OPT_NOARG | PARSE_OPT_NONEG, rfc_callback), OPT_STRING(0, "cover-from-description", &cover_from_description_arg, N_("cover-from-description-mode"), N_("generate parts of a cover letter based on a branch's description")), OPT_CALLBACK_F(0, "subject-prefix", &rev, N_("prefix"), - N_("Use [<prefix>] instead of [PATCH]"), + N_("use [<prefix>] instead of [PATCH]"), PARSE_OPT_NONEG, subject_prefix_callback), OPT_CALLBACK_F('o', "output-directory", &output_directory, N_("dir"), N_("store resulting files in <dir>"), diff --git a/builtin/name-rev.c b/builtin/name-rev.c index 725dd04519..3fe71a8c01 100644 --- a/builtin/name-rev.c +++ b/builtin/name-rev.c @@ -7,7 +7,7 @@ #include "refs.h" #include "parse-options.h" #include "prio-queue.h" -#include "sha1-lookup.h" +#include "hash-lookup.h" #include "commit-slab.h" /* @@ -408,7 +408,7 @@ static const char *get_exact_ref_match(const struct object *o) tip_table.sorted = 1; } - found = sha1_pos(o->oid.hash, tip_table.table, tip_table.nr, + found = hash_pos(o->oid.hash, tip_table.table, tip_table.nr, nth_tip_table_ent); if (0 <= found) return tip_table.table[found].refname; diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c index 5617c01b5a..2a00358f34 100644 --- a/builtin/pack-objects.c +++ b/builtin/pack-objects.c @@ -1104,7 +1104,6 @@ static void write_pack_file(void) stop_progress(&progress_state); bitmap_writer_show_progress(progress); - bitmap_writer_reuse_bitmaps(&to_pack); bitmap_writer_select_commits(indexed_commits, indexed_commits_nr, -1); bitmap_writer_build(&to_pack); bitmap_writer_finish(written_list, nr_written, diff --git a/builtin/pull.c b/builtin/pull.c index aa56ebcdd0..e8927fc2ff 100644 --- a/builtin/pull.c +++ b/builtin/pull.c @@ -324,7 +324,7 @@ static const char *config_get_ff(void) * looks for the value of "pull.rebase". If both configuration keys do not * exist, returns REBASE_FALSE. */ -static enum rebase_type config_get_rebase(void) +static enum rebase_type config_get_rebase(int *rebase_unspecified) { struct branch *curr_branch = branch_get("HEAD"); const char *value; @@ -344,20 +344,7 @@ static enum rebase_type config_get_rebase(void) if (!git_config_get_value("pull.rebase", &value)) return parse_config_rebase("pull.rebase", value, 1); - if (opt_verbosity >= 0 && !opt_ff) { - advise(_("Pulling without specifying how to reconcile divergent branches is\n" - "discouraged. You can squelch this message by running one of the following\n" - "commands sometime before your next pull:\n" - "\n" - " git config pull.rebase false # merge (the default strategy)\n" - " git config pull.rebase true # rebase\n" - " git config pull.ff only # fast-forward only\n" - "\n" - "You can replace \"git config\" with \"git config --global\" to set a default\n" - "preference for all repositories. You can also pass --rebase, --no-rebase,\n" - "or --ff-only on the command line to override the configured default per\n" - "invocation.\n")); - } + *rebase_unspecified = 1; return REBASE_FALSE; } @@ -924,6 +911,36 @@ static int run_rebase(const struct object_id *newbase, return ret; } +static int get_can_ff(struct object_id *orig_head, struct object_id *orig_merge_head) +{ + int ret; + struct commit_list *list = NULL; + struct commit *merge_head, *head; + + head = lookup_commit_reference(the_repository, orig_head); + commit_list_insert(head, &list); + merge_head = lookup_commit_reference(the_repository, orig_merge_head); + ret = repo_is_descendant_of(the_repository, merge_head, list); + free_commit_list(list); + return ret; +} + +static void show_advice_pull_non_ff(void) +{ + advise(_("Pulling without specifying how to reconcile divergent branches is\n" + "discouraged. You can squelch this message by running one of the following\n" + "commands sometime before your next pull:\n" + "\n" + " git config pull.rebase false # merge (the default strategy)\n" + " git config pull.rebase true # rebase\n" + " git config pull.ff only # fast-forward only\n" + "\n" + "You can replace \"git config\" with \"git config --global\" to set a default\n" + "preference for all repositories. You can also pass --rebase, --no-rebase,\n" + "or --ff-only on the command line to override the configured default per\n" + "invocation.\n")); +} + int cmd_pull(int argc, const char **argv, const char *prefix) { const char *repo, **refspecs; @@ -931,6 +948,8 @@ int cmd_pull(int argc, const char **argv, const char *prefix) struct object_id orig_head, curr_head; struct object_id rebase_fork_point; int autostash; + int rebase_unspecified = 0; + int can_ff; if (!getenv("GIT_REFLOG_ACTION")) set_reflog_message(argc, argv); @@ -952,7 +971,7 @@ int cmd_pull(int argc, const char **argv, const char *prefix) opt_ff = xstrdup_or_null(config_get_ff()); if (opt_rebase < 0) - opt_rebase = config_get_rebase(); + opt_rebase = config_get_rebase(&rebase_unspecified); if (read_cache_unmerged()) die_resolve_conflict("pull"); @@ -1026,6 +1045,13 @@ int cmd_pull(int argc, const char **argv, const char *prefix) if (opt_rebase && merge_heads.nr > 1) die(_("Cannot rebase onto multiple branches.")); + can_ff = get_can_ff(&orig_head, &merge_heads.oid[0]); + + if (rebase_unspecified && !opt_ff && !can_ff) { + if (opt_verbosity >= 0) + show_advice_pull_non_ff(); + } + if (opt_rebase) { int ret = 0; int ran_ff = 0; @@ -1040,22 +1066,12 @@ int cmd_pull(int argc, const char **argv, const char *prefix) submodule_touches_in_range(the_repository, &upstream, &curr_head)) die(_("cannot rebase with locally recorded submodule modifications")); if (!autostash) { - struct commit_list *list = NULL; - struct commit *merge_head, *head; - - head = lookup_commit_reference(the_repository, - &orig_head); - commit_list_insert(head, &list); - merge_head = lookup_commit_reference(the_repository, - &merge_heads.oid[0]); - if (repo_is_descendant_of(the_repository, - merge_head, list)) { + if (can_ff) { /* we can fast-forward this without invoking rebase */ opt_ff = "--ff-only"; ran_ff = 1; ret = run_merge(); } - free_commit_list(list); } if (!ran_ff) ret = run_rebase(&newbase, &upstream); diff --git a/builtin/rebase.c b/builtin/rebase.c index 19c7b377aa..840dbd7eb7 100644 --- a/builtin/rebase.c +++ b/builtin/rebase.c @@ -1917,7 +1917,9 @@ int cmd_rebase(int argc, const char **argv, const char *prefix) die_if_checked_out(buf.buf, 1); options.head_name = xstrdup(buf.buf); /* If not is it a valid ref (branch or commit)? */ - } else if (!get_oid(branch_name, &options.orig_head)) + } else if (!get_oid(branch_name, &options.orig_head) && + lookup_commit_reference(the_repository, + &options.orig_head)) options.head_name = NULL; else die(_("fatal: no such branch/commit '%s'"), diff --git a/builtin/rev-parse.c b/builtin/rev-parse.c index 69ba7326cf..85bad9052e 100644 --- a/builtin/rev-parse.c +++ b/builtin/rev-parse.c @@ -583,6 +583,75 @@ static void handle_ref_opt(const char *pattern, const char *prefix) clear_ref_exclusion(&ref_excludes); } +enum format_type { + /* We would like a relative path. */ + FORMAT_RELATIVE, + /* We would like a canonical absolute path. */ + FORMAT_CANONICAL, + /* We would like the default behavior. */ + FORMAT_DEFAULT, +}; + +enum default_type { + /* Our default is a relative path. */ + DEFAULT_RELATIVE, + /* Our default is a relative path if there's a shared root. */ + DEFAULT_RELATIVE_IF_SHARED, + /* Our default is a canonical absolute path. */ + DEFAULT_CANONICAL, + /* Our default is not to modify the item. */ + DEFAULT_UNMODIFIED, +}; + +static void print_path(const char *path, const char *prefix, enum format_type format, enum default_type def) +{ + char *cwd = NULL; + /* + * We don't ever produce a relative path if prefix is NULL, so set the + * prefix to the current directory so that we can produce a relative + * path whenever possible. If we're using RELATIVE_IF_SHARED mode, then + * we want an absolute path unless the two share a common prefix, so don't + * set it in that case, since doing so causes a relative path to always + * be produced if possible. + */ + if (!prefix && (format != FORMAT_DEFAULT || def != DEFAULT_RELATIVE_IF_SHARED)) + prefix = cwd = xgetcwd(); + if (format == FORMAT_DEFAULT && def == DEFAULT_UNMODIFIED) { + puts(path); + } else if (format == FORMAT_RELATIVE || + (format == FORMAT_DEFAULT && def == DEFAULT_RELATIVE)) { + /* + * In order for relative_path to work as expected, we need to + * make sure that both paths are absolute paths. If we don't, + * we can end up with an unexpected absolute path that the user + * didn't want. + */ + struct strbuf buf = STRBUF_INIT, realbuf = STRBUF_INIT, prefixbuf = STRBUF_INIT; + if (!is_absolute_path(path)) { + strbuf_realpath_forgiving(&realbuf, path, 1); + path = realbuf.buf; + } + if (!is_absolute_path(prefix)) { + strbuf_realpath_forgiving(&prefixbuf, prefix, 1); + prefix = prefixbuf.buf; + } + puts(relative_path(path, prefix, &buf)); + strbuf_release(&buf); + strbuf_release(&realbuf); + strbuf_release(&prefixbuf); + } else if (format == FORMAT_DEFAULT && def == DEFAULT_RELATIVE_IF_SHARED) { + struct strbuf buf = STRBUF_INIT; + puts(relative_path(path, prefix, &buf)); + strbuf_release(&buf); + } else { + struct strbuf buf = STRBUF_INIT; + strbuf_realpath_forgiving(&buf, path, 1); + puts(buf.buf); + strbuf_release(&buf); + } + free(cwd); +} + int cmd_rev_parse(int argc, const char **argv, const char *prefix) { int i, as_is = 0, verify = 0, quiet = 0, revs_count = 0, type = 0; @@ -596,6 +665,7 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix) struct strbuf buf = STRBUF_INIT; const int hexsz = the_hash_algo->hexsz; int seen_end_of_options = 0; + enum format_type format = FORMAT_DEFAULT; if (argc > 1 && !strcmp("--parseopt", argv[1])) return cmd_parseopt(argc - 1, argv + 1, prefix); @@ -668,8 +738,9 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix) if (!argv[i + 1]) die("--git-path requires an argument"); strbuf_reset(&buf); - puts(relative_path(git_path("%s", argv[i + 1]), - prefix, &buf)); + print_path(git_path("%s", argv[i + 1]), prefix, + format, + DEFAULT_RELATIVE_IF_SHARED); i++; continue; } @@ -687,6 +758,16 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix) show(arg); continue; } + if (opt_with_value(arg, "--path-format", &arg)) { + if (!strcmp(arg, "absolute")) { + format = FORMAT_CANONICAL; + } else if (!strcmp(arg, "relative")) { + format = FORMAT_RELATIVE; + } else { + die("unknown argument to --path-format: %s", arg); + } + continue; + } if (!strcmp(arg, "--default")) { def = argv[++i]; if (!def) @@ -807,7 +888,7 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix) if (!strcmp(arg, "--show-toplevel")) { const char *work_tree = get_git_work_tree(); if (work_tree) - puts(work_tree); + print_path(work_tree, prefix, format, DEFAULT_UNMODIFIED); else die("this operation must be run in a work tree"); continue; @@ -815,7 +896,7 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix) if (!strcmp(arg, "--show-superproject-working-tree")) { struct strbuf superproject = STRBUF_INIT; if (get_superproject_working_tree(&superproject)) - puts(superproject.buf); + print_path(superproject.buf, prefix, format, DEFAULT_UNMODIFIED); strbuf_release(&superproject); continue; } @@ -850,16 +931,18 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix) const char *gitdir = getenv(GIT_DIR_ENVIRONMENT); char *cwd; int len; + enum format_type wanted = format; if (arg[2] == 'g') { /* --git-dir */ if (gitdir) { - puts(gitdir); + print_path(gitdir, prefix, format, DEFAULT_UNMODIFIED); continue; } if (!prefix) { - puts(".git"); + print_path(".git", prefix, format, DEFAULT_UNMODIFIED); continue; } } else { /* --absolute-git-dir */ + wanted = FORMAT_CANONICAL; if (!gitdir && !prefix) gitdir = ".git"; if (gitdir) { @@ -872,14 +955,14 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix) } cwd = xgetcwd(); len = strlen(cwd); - printf("%s%s.git\n", cwd, len && cwd[len-1] != '/' ? "/" : ""); + strbuf_reset(&buf); + strbuf_addf(&buf, "%s%s.git", cwd, len && cwd[len-1] != '/' ? "/" : ""); free(cwd); + print_path(buf.buf, prefix, wanted, DEFAULT_CANONICAL); continue; } if (!strcmp(arg, "--git-common-dir")) { - strbuf_reset(&buf); - puts(relative_path(get_git_common_dir(), - prefix, &buf)); + print_path(get_git_common_dir(), prefix, format, DEFAULT_RELATIVE_IF_SHARED); continue; } if (!strcmp(arg, "--is-inside-git-dir")) { @@ -909,8 +992,7 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix) if (the_index.split_index) { const struct object_id *oid = &the_index.split_index->base_oid; const char *path = git_path("sharedindex.%s", oid_to_hex(oid)); - strbuf_reset(&buf); - puts(relative_path(path, prefix, &buf)); + print_path(path, prefix, format, DEFAULT_RELATIVE); } continue; } diff --git a/builtin/shortlog.c b/builtin/shortlog.c index c52e4ccd19..1c0b3a9b05 100644 --- a/builtin/shortlog.c +++ b/builtin/shortlog.c @@ -360,19 +360,19 @@ int cmd_shortlog(int argc, const char **argv, const char *prefix) const struct option options[] = { OPT_BIT('c', "committer", &log.groups, - N_("Group by committer rather than author"), + N_("group by committer rather than author"), SHORTLOG_GROUP_COMMITTER), OPT_BOOL('n', "numbered", &log.sort_by_number, N_("sort output according to the number of commits per author")), OPT_BOOL('s', "summary", &log.summary, - N_("Suppress commit descriptions, only provides commit count")), + N_("suppress commit descriptions, only provides commit count")), OPT_BOOL('e', "email", &log.email, - N_("Show the email address of each author")), + N_("show the email address of each author")), OPT_CALLBACK_F('w', NULL, &log, N_("<w>[,<i1>[,<i2>]]"), - N_("Linewrap output"), PARSE_OPT_OPTARG, + N_("linewrap output"), PARSE_OPT_OPTARG, &parse_wrap_args), OPT_CALLBACK(0, "group", &log, N_("field"), - N_("Group by field"), parse_group_option), + N_("group by field"), parse_group_option), OPT_END(), }; diff --git a/builtin/stash.c b/builtin/stash.c index e1f8235fdd..9bc85f91cd 100644 --- a/builtin/stash.c +++ b/builtin/stash.c @@ -325,35 +325,6 @@ static void add_diff_to_buf(struct diff_queue_struct *q, } } -static int get_newly_staged(struct strbuf *out, struct object_id *c_tree) -{ - struct child_process cp = CHILD_PROCESS_INIT; - const char *c_tree_hex = oid_to_hex(c_tree); - - /* - * diff-index is very similar to diff-tree above, and should be - * converted together with update_index. - */ - cp.git_cmd = 1; - strvec_pushl(&cp.args, "diff-index", "--cached", "--name-only", - "--diff-filter=A", NULL); - strvec_push(&cp.args, c_tree_hex); - return pipe_command(&cp, NULL, 0, out, 0, NULL, 0); -} - -static int update_index(struct strbuf *out) -{ - struct child_process cp = CHILD_PROCESS_INIT; - - /* - * Update-index is very complicated and may need to have a public - * function exposed in order to remove this forking. - */ - cp.git_cmd = 1; - strvec_pushl(&cp.args, "update-index", "--add", "--stdin", NULL); - return pipe_command(&cp, out->buf, out->len, NULL, 0, NULL, 0); -} - static int restore_untracked(struct object_id *u_tree) { int res; @@ -385,6 +356,121 @@ static int restore_untracked(struct object_id *u_tree) return res; } +static void unstage_changes_unless_new(struct object_id *orig_tree) +{ + /* + * When we enter this function, there has been a clean merge of + * relevant trees, and the merge logic always stages whatever merges + * cleanly. We want to unstage those changes, unless it corresponds + * to a file that didn't exist as of orig_tree. + * + * However, if any SKIP_WORKTREE path is modified relative to + * orig_tree, then we want to clear the SKIP_WORKTREE bit and write + * it to the worktree before unstaging. + */ + + struct checkout state = CHECKOUT_INIT; + struct diff_options diff_opts; + struct lock_file lock = LOCK_INIT; + int i; + + /* If any entries have skip_worktree set, we'll have to check 'em out */ + state.force = 1; + state.quiet = 1; + state.refresh_cache = 1; + state.istate = &the_index; + + /* + * Step 1: get a difference between orig_tree (which corresponding + * to the index before a merge was run) and the current index + * (reflecting the changes brought in by the merge). + */ + diff_setup(&diff_opts); + diff_opts.flags.recursive = 1; + diff_opts.detect_rename = 0; + diff_opts.output_format = DIFF_FORMAT_NO_OUTPUT; + diff_setup_done(&diff_opts); + + do_diff_cache(orig_tree, &diff_opts); + diffcore_std(&diff_opts); + + /* Iterate over the paths that changed due to the merge... */ + for (i = 0; i < diff_queued_diff.nr; i++) { + struct diff_filepair *p; + struct cache_entry *ce; + int pos; + + /* Look up the path's position in the current index. */ + p = diff_queued_diff.queue[i]; + pos = index_name_pos(&the_index, p->two->path, + strlen(p->two->path)); + + /* + * Step 2: Place changes in the working tree + * + * Stash is about restoring changes *to the working tree*. + * So if the merge successfully got a new version of some + * path, but left it out of the working tree, then clear the + * SKIP_WORKTREE bit and write it to the working tree. + */ + if (pos >= 0 && ce_skip_worktree(active_cache[pos])) { + struct stat st; + + ce = active_cache[pos]; + if (!lstat(ce->name, &st)) { + /* Conflicting path present; relocate it */ + struct strbuf new_path = STRBUF_INIT; + int fd; + + strbuf_addf(&new_path, + "%s.stash.XXXXXX", ce->name); + fd = xmkstemp(new_path.buf); + close(fd); + printf(_("WARNING: Untracked file in way of " + "tracked file! Renaming\n " + " %s -> %s\n" + " to make room.\n"), + ce->name, new_path.buf); + if (rename(ce->name, new_path.buf)) + die("Failed to move %s to %s\n", + ce->name, new_path.buf); + strbuf_release(&new_path); + } + checkout_entry(ce, &state, NULL, NULL); + ce->ce_flags &= ~CE_SKIP_WORKTREE; + } + + /* + * Step 3: "unstage" changes, as long as they are still tracked + */ + if (p->one->oid_valid) { + /* + * Path existed in orig_tree; restore index entry + * from that tree in order to "unstage" the changes. + */ + int option = ADD_CACHE_OK_TO_REPLACE; + if (pos < 0) + option = ADD_CACHE_OK_TO_ADD; + + ce = make_cache_entry(&the_index, + p->one->mode, + &p->one->oid, + p->one->path, + 0, 0); + add_index_entry(&the_index, ce, option); + } + } + diff_flush(&diff_opts); + + /* + * Step 4: write the new index to disk + */ + repo_hold_locked_index(the_repository, &lock, LOCK_DIE_ON_ERROR); + if (write_locked_index(&the_index, &lock, + COMMIT_LOCK | SKIP_IF_UNCHANGED)) + die(_("Unable to write index.")); +} + static int do_apply_stash(const char *prefix, struct stash_info *info, int index, int quiet) { @@ -467,26 +553,7 @@ static int do_apply_stash(const char *prefix, struct stash_info *info, if (reset_tree(&index_tree, 0, 0)) return -1; } else { - struct strbuf out = STRBUF_INIT; - - if (get_newly_staged(&out, &c_tree)) { - strbuf_release(&out); - return -1; - } - - if (reset_tree(&c_tree, 0, 1)) { - strbuf_release(&out); - return -1; - } - - ret = update_index(&out); - strbuf_release(&out); - if (ret) - return -1; - - /* read back the result of update_index() back from the disk */ - discard_cache(); - read_cache(); + unstage_changes_unless_new(&c_tree); } if (!quiet) { diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c index c30896c897..c2bd882d17 100644 --- a/builtin/submodule--helper.c +++ b/builtin/submodule--helper.c @@ -562,9 +562,9 @@ static int module_foreach(int argc, const char **argv, const char *prefix) struct module_list list = MODULE_LIST_INIT; struct option module_foreach_options[] = { - OPT__QUIET(&info.quiet, N_("Suppress output of entering each submodule command")), + OPT__QUIET(&info.quiet, N_("suppress output of entering each submodule command")), OPT_BOOL(0, "recursive", &info.recursive, - N_("Recurse into nested submodules")), + N_("recurse into nested submodules")), OPT_END() }; @@ -706,7 +706,7 @@ static int module_init(int argc, const char **argv, const char *prefix) int quiet = 0; struct option module_init_options[] = { - OPT__QUIET(&quiet, N_("Suppress output for initializing a submodule")), + OPT__QUIET(&quiet, N_("suppress output for initializing a submodule")), OPT_END() }; @@ -883,8 +883,8 @@ static int module_status(int argc, const char **argv, const char *prefix) int quiet = 0; struct option module_status_options[] = { - OPT__QUIET(&quiet, N_("Suppress submodule status output")), - OPT_BIT(0, "cached", &info.flags, N_("Use commit stored in the index instead of the one stored in the submodule HEAD"), OPT_CACHED), + OPT__QUIET(&quiet, N_("suppress submodule status output")), + OPT_BIT(0, "cached", &info.flags, N_("use commit stored in the index instead of the one stored in the submodule HEAD"), OPT_CACHED), OPT_BIT(0, "recursive", &info.flags, N_("recurse into nested submodules"), OPT_RECURSIVE), OPT_END() }; @@ -1482,9 +1482,9 @@ static int module_sync(int argc, const char **argv, const char *prefix) int recursive = 0; struct option module_sync_options[] = { - OPT__QUIET(&quiet, N_("Suppress output of synchronizing submodule url")), + OPT__QUIET(&quiet, N_("suppress output of synchronizing submodule url")), OPT_BOOL(0, "recursive", &recursive, - N_("Recurse into nested submodules")), + N_("recurse into nested submodules")), OPT_END() }; @@ -1620,9 +1620,9 @@ static int module_deinit(int argc, const char **argv, const char *prefix) int all = 0; struct option module_deinit_options[] = { - OPT__QUIET(&quiet, N_("Suppress submodule status output")), - OPT__FORCE(&force, N_("Remove submodule working trees even if they contain local changes"), 0), - OPT_BOOL(0, "all", &all, N_("Unregister all submodules")), + OPT__QUIET(&quiet, N_("suppress submodule status output")), + OPT__FORCE(&force, N_("remove submodule working trees even if they contain local changes"), 0), + OPT_BOOL(0, "all", &all, N_("unregister all submodules")), OPT_END() }; @@ -2337,7 +2337,7 @@ static int update_clone(int argc, const char **argv, const char *prefix) OPT_BOOL(0, "dissociate", &suc.dissociate, N_("use --reference only while cloning")), OPT_STRING(0, "depth", &suc.depth, "<depth>", - N_("Create a shallow clone truncated to the " + N_("create a shallow clone truncated to the " "specified number of revisions")), OPT_INTEGER('j', "jobs", &suc.max_jobs, N_("parallel jobs")), @@ -2678,7 +2678,7 @@ static int module_set_url(int argc, const char **argv, const char *prefix) char *config_name; struct option options[] = { - OPT__QUIET(&quiet, N_("Suppress output for setting url of a submodule")), + OPT__QUIET(&quiet, N_("suppress output for setting url of a submodule")), OPT_END() }; const char *const usage[] = { diff --git a/builtin/worktree.c b/builtin/worktree.c index 197fd24a55..71287b2da6 100644 --- a/builtin/worktree.c +++ b/builtin/worktree.c @@ -1052,10 +1052,10 @@ static int repair(int ac, const char **av, const char *prefix) int rc = 0; ac = parse_options(ac, av, prefix, options, worktree_usage, 0); - repair_worktrees(report_repair, &rc); p = ac > 0 ? av : self; for (; *p; p++) repair_worktree_at_path(*p, report_repair, &rc); + repair_worktrees(report_repair, &rc); return rc; } @@ -1231,6 +1231,8 @@ static inline int is_absolute_path(const char *path) int is_directory(const char *); char *strbuf_realpath(struct strbuf *resolved, const char *path, int die_on_error); +char *strbuf_realpath_forgiving(struct strbuf *resolved, const char *path, + int die_on_error); char *real_pathdup(const char *path, int die_on_error); const char *absolute_path(const char *path); char *absolute_pathdup(const char *path); diff --git a/commit-graph.c b/commit-graph.c index 06f8dc1d89..e9124d4a41 100644 --- a/commit-graph.c +++ b/commit-graph.c @@ -7,7 +7,7 @@ #include "object.h" #include "refs.h" #include "revision.h" -#include "sha1-lookup.h" +#include "hash-lookup.h" #include "commit-graph.h" #include "object-store.h" #include "alloc.h" @@ -1043,7 +1043,7 @@ static int write_graph_chunk_data(struct hashfile *f, if (!parent) edge_value = GRAPH_PARENT_NONE; else { - edge_value = sha1_pos(parent->item->object.oid.hash, + edge_value = hash_pos(parent->item->object.oid.hash, ctx->commits.list, ctx->commits.nr, commit_to_sha1); @@ -1074,7 +1074,7 @@ static int write_graph_chunk_data(struct hashfile *f, else if (parent->next) edge_value = GRAPH_EXTRA_EDGES_NEEDED | num_extra_edges; else { - edge_value = sha1_pos(parent->item->object.oid.hash, + edge_value = hash_pos(parent->item->object.oid.hash, ctx->commits.list, ctx->commits.nr, commit_to_sha1); @@ -1143,7 +1143,7 @@ static int write_graph_chunk_extra_edges(struct hashfile *f, /* Since num_parents > 2, this initializer is safe. */ for (parent = (*list)->parents->next; parent; parent = parent->next) { - int edge_value = sha1_pos(parent->item->object.oid.hash, + int edge_value = hash_pos(parent->item->object.oid.hash, ctx->commits.list, ctx->commits.nr, commit_to_sha1); @@ -14,7 +14,7 @@ #include "mergesort.h" #include "commit-slab.h" #include "prio-queue.h" -#include "sha1-lookup.h" +#include "hash-lookup.h" #include "wt-status.h" #include "advice.h" #include "refs.h" @@ -113,7 +113,7 @@ static const unsigned char *commit_graft_sha1_access(size_t index, void *table) int commit_graft_pos(struct repository *r, const unsigned char *sha1) { - return sha1_pos(sha1, r->parsed_objects->grafts, + return hash_pos(sha1, r->parsed_objects->grafts, r->parsed_objects->grafts_nr, commit_graft_sha1_access); } @@ -544,6 +544,17 @@ struct commit_list *commit_list_insert(struct commit *item, struct commit_list * return new_list; } +int commit_list_contains(struct commit *item, struct commit_list *list) +{ + while (list) { + if (list->item == item) + return 1; + list = list->next; + } + + return 0; +} + unsigned commit_list_count(const struct commit_list *l) { unsigned c = 0; @@ -563,6 +574,17 @@ struct commit_list *copy_commit_list(struct commit_list *list) return head; } +struct commit_list *reverse_commit_list(struct commit_list *list) +{ + struct commit_list *next = NULL, *current, *backup; + for (current = list; current; current = backup) { + backup = current->next; + current->next = next; + next = current; + } + return next; +} + void free_commit_list(struct commit_list *list) { while (list) @@ -167,6 +167,8 @@ int find_commit_subject(const char *commit_buffer, const char **subject); struct commit_list *commit_list_insert(struct commit *item, struct commit_list **list); +int commit_list_contains(struct commit *item, + struct commit_list *list); struct commit_list **commit_list_append(struct commit *commit, struct commit_list **next); unsigned commit_list_count(const struct commit_list *l); @@ -177,6 +179,9 @@ void commit_list_sort_by_date(struct commit_list **list); /* Shallow copy of the input list */ struct commit_list *copy_commit_list(struct commit_list *list); +/* Modify list in-place to reverse it, returning new head; list will be tail */ +struct commit_list *reverse_commit_list(struct commit_list *list); + void free_commit_list(struct commit_list *list); struct rev_info; /* in revision.h, it circularly uses enum cmit_fmt */ @@ -1217,6 +1217,8 @@ static int git_default_core_config(const char *var, const char *value, void *cb) return config_error_nonbool(var); if (!strcasecmp(value, "auto")) default_abbrev = -1; + else if (!git_parse_maybe_bool_text(value)) + default_abbrev = the_hash_algo->hexsz; else { int abbrev = git_config_int(var, value); if (abbrev < minimum_abbrev || abbrev > the_hash_algo->hexsz) diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash index 463a3124da..4b1f4264a6 100644 --- a/contrib/completion/git-completion.bash +++ b/contrib/completion/git-completion.bash @@ -29,6 +29,15 @@ # tell the completion to use commit completion. This also works with aliases # of form "!sh -c '...'". For example, "!sh -c ': git commit ; ... '". # +# If you have a command that is not part of git, but you would still +# like completion, you can use __git_complete: +# +# __git_complete gl git_log +# +# Or if it's a main command (i.e. git or gitk): +# +# __git_complete gk gitk +# # Compatible with bash 3.2.57. # # You can set the following environment variables to influence the behavior of @@ -3358,15 +3367,19 @@ __git_support_parseopt_helper () { esac } +__git_have_func () { + declare -f -- "$1" >/dev/null 2>&1 +} + __git_complete_command () { local command="$1" local completion_func="_git_${command//-/_}" - if ! declare -f $completion_func >/dev/null 2>/dev/null && - declare -f _completion_loader >/dev/null 2>/dev/null + if ! __git_have_func $completion_func && + __git_have_func _completion_loader then _completion_loader "git-$command" fi - if declare -f $completion_func >/dev/null 2>/dev/null + if __git_have_func $completion_func then $completion_func return 0 @@ -3493,10 +3506,7 @@ __git_func_wrap () $1 } -# Setup completion for certain functions defined above by setting common -# variables and workarounds. -# This is NOT a public function; use at your own risk. -__git_complete () +___git_complete () { local wrapper="__git_wrap${2}" eval "$wrapper () { __git_func_wrap $2 ; }" @@ -3504,13 +3514,33 @@ __git_complete () || complete -o default -o nospace -F $wrapper $1 } -__git_complete git __git_main -__git_complete gitk __gitk_main +# Setup the completion for git commands +# 1: command or alias +# 2: function to call (e.g. `git`, `gitk`, `git_fetch`) +__git_complete () +{ + local func + + if __git_have_func $2; then + func=$2 + elif __git_have_func __$2_main; then + func=__$2_main + elif __git_have_func _$2; then + func=_$2 + else + echo "ERROR: could not find function '$2'" 1>&2 + return 1 + fi + ___git_complete $1 $func +} + +___git_complete git __git_main +___git_complete gitk __gitk_main # The following are necessary only for Cygwin, and only are needed # when the user has tab-completed the executable name and consequently # included the '.exe' suffix. # if [ "$OSTYPE" = cygwin ]; then - __git_complete git.exe __git_main + ___git_complete git.exe __git_main fi diff --git a/ewah/bitmap.c b/ewah/bitmap.c index d8cec585af..0d31cdc866 100644 --- a/ewah/bitmap.c +++ b/ewah/bitmap.c @@ -35,18 +35,26 @@ struct bitmap *bitmap_new(void) return bitmap_word_alloc(32); } +struct bitmap *bitmap_dup(const struct bitmap *src) +{ + struct bitmap *dst = bitmap_word_alloc(src->word_alloc); + COPY_ARRAY(dst->words, src->words, src->word_alloc); + return dst; +} + +static void bitmap_grow(struct bitmap *self, size_t word_alloc) +{ + size_t old_size = self->word_alloc; + ALLOC_GROW(self->words, word_alloc, self->word_alloc); + memset(self->words + old_size, 0x0, + (self->word_alloc - old_size) * sizeof(eword_t)); +} + void bitmap_set(struct bitmap *self, size_t pos) { size_t block = EWAH_BLOCK(pos); - if (block >= self->word_alloc) { - size_t old_size = self->word_alloc; - self->word_alloc = block ? block * 2 : 1; - REALLOC_ARRAY(self->words, self->word_alloc); - memset(self->words + old_size, 0x0, - (self->word_alloc - old_size) * sizeof(eword_t)); - } - + bitmap_grow(self, block + 1); self->words[block] |= EWAH_MASK(pos); } @@ -121,6 +129,15 @@ void bitmap_and_not(struct bitmap *self, struct bitmap *other) self->words[i] &= ~other->words[i]; } +void bitmap_or(struct bitmap *self, const struct bitmap *other) +{ + size_t i; + + bitmap_grow(self, other->word_alloc); + for (i = 0; i < other->word_alloc; i++) + self->words[i] |= other->words[i]; +} + void bitmap_or_ewah(struct bitmap *self, struct ewah_bitmap *other) { size_t original_size = self->word_alloc; @@ -178,6 +195,27 @@ int bitmap_equals(struct bitmap *self, struct bitmap *other) return 1; } +int bitmap_is_subset(struct bitmap *self, struct bitmap *other) +{ + size_t common_size, i; + + if (self->word_alloc < other->word_alloc) + common_size = self->word_alloc; + else { + common_size = other->word_alloc; + for (i = common_size; i < self->word_alloc; i++) { + if (self->words[i]) + return 1; + } + } + + for (i = 0; i < common_size; i++) { + if (self->words[i] & ~other->words[i]) + return 1; + } + return 0; +} + void bitmap_reset(struct bitmap *bitmap) { memset(bitmap->words, 0x0, bitmap->word_alloc * sizeof(eword_t)); diff --git a/ewah/ewah_bitmap.c b/ewah/ewah_bitmap.c index d59b1afe3d..2a8c7c5c33 100644 --- a/ewah/ewah_bitmap.c +++ b/ewah/ewah_bitmap.c @@ -19,6 +19,7 @@ #include "git-compat-util.h" #include "ewok.h" #include "ewok_rlw.h" +#include "cache.h" static inline size_t min_size(size_t a, size_t b) { @@ -33,20 +34,13 @@ static inline size_t max_size(size_t a, size_t b) static inline void buffer_grow(struct ewah_bitmap *self, size_t new_size) { size_t rlw_offset = (uint8_t *)self->rlw - (uint8_t *)self->buffer; - - if (self->alloc_size >= new_size) - return; - - self->alloc_size = new_size; - REALLOC_ARRAY(self->buffer, self->alloc_size); + ALLOC_GROW(self->buffer, new_size, self->alloc_size); self->rlw = self->buffer + (rlw_offset / sizeof(eword_t)); } static inline void buffer_push(struct ewah_bitmap *self, eword_t value) { - if (self->buffer_size + 1 >= self->alloc_size) - buffer_grow(self, self->buffer_size * 3 / 2); - + buffer_grow(self, self->buffer_size + 1); self->buffer[self->buffer_size++] = value; } @@ -137,8 +131,7 @@ void ewah_add_dirty_words( rlw_set_literal_words(self->rlw, literals + can_add); - if (self->buffer_size + can_add >= self->alloc_size) - buffer_grow(self, (self->buffer_size + can_add) * 3 / 2); + buffer_grow(self, self->buffer_size + can_add); if (negate) { size_t i; diff --git a/ewah/ewok.h b/ewah/ewok.h index 011852bef1..66920965da 100644 --- a/ewah/ewok.h +++ b/ewah/ewok.h @@ -173,13 +173,14 @@ struct bitmap { struct bitmap *bitmap_new(void); struct bitmap *bitmap_word_alloc(size_t word_alloc); +struct bitmap *bitmap_dup(const struct bitmap *src); void bitmap_set(struct bitmap *self, size_t pos); void bitmap_unset(struct bitmap *self, size_t pos); int bitmap_get(struct bitmap *self, size_t pos); void bitmap_reset(struct bitmap *self); void bitmap_free(struct bitmap *self); int bitmap_equals(struct bitmap *self, struct bitmap *other); -int bitmap_is_subset(struct bitmap *self, struct bitmap *super); +int bitmap_is_subset(struct bitmap *self, struct bitmap *other); struct ewah_bitmap * bitmap_to_ewah(struct bitmap *bitmap); struct bitmap *ewah_to_bitmap(struct ewah_bitmap *ewah); @@ -87,88 +87,24 @@ static int test_vsnprintf(const char *fmt, ...) static void init_gettext_charset(const char *domain) { - /* - This trick arranges for messages to be emitted in the user's - requested encoding, but avoids setting LC_CTYPE from the - environment for the whole program. - - This primarily done to avoid a bug in vsnprintf in the GNU C - Library [1]. which triggered a "your vsnprintf is broken" error - on Git's own repository when inspecting v0.99.6~1 under a UTF-8 - locale. - - That commit contains a ISO-8859-1 encoded author name, which - the locale aware vsnprintf(3) won't interpolate in the format - argument, due to mismatch between the data encoding and the - locale. - - Even if it wasn't for that bug we wouldn't want to use LC_CTYPE at - this point, because it'd require auditing all the code that uses C - functions whose semantics are modified by LC_CTYPE. - - But only setting LC_MESSAGES as we do creates a problem, since - we declare the encoding of our PO files[2] the gettext - implementation will try to recode it to the user's locale, but - without LC_CTYPE it'll emit something like this on 'git init' - under the Icelandic locale: - - Bj? til t?ma Git lind ? /hlagh/.git/ - - Gettext knows about the encoding of our PO file, but we haven't - told it about the user's encoding, so all the non-US-ASCII - characters get encoded to question marks. - - But we're in luck! We can set LC_CTYPE from the environment - only while we call nl_langinfo and - bind_textdomain_codeset. That suffices to tell gettext what - encoding it should emit in, so it'll now say: - - Bjó til tóma Git lind à /hlagh/.git/ - - And the equivalent ISO-8859-1 string will be emitted under a - ISO-8859-1 locale. - - With this change way we get the advantages of setting LC_CTYPE - (talk to the user in his language/encoding), without the major - drawbacks (changed semantics for C functions we rely on). - - However foreign functions using other message catalogs that - aren't using our neat trick will still have a problem, e.g. if - we have to call perror(3): - - #include <stdio.h> - #include <locale.h> - #include <errno.h> - - int main(void) - { - setlocale(LC_MESSAGES, ""); - setlocale(LC_CTYPE, "C"); - errno = ENODEV; - perror("test"); - return 0; - } - - Running that will give you a message with question marks: - - $ LANGUAGE= LANG=de_DE.utf8 ./test - test: Kein passendes Ger?t gefunden - - The vsnprintf bug has been fixed since glibc 2.17. - - Then we could simply set LC_CTYPE from the environment, which would - make things like the external perror(3) messages work. - - See t/t0203-gettext-setlocale-sanity.sh's "gettext.c" tests for - regression tests. - - 1. http://sourceware.org/bugzilla/show_bug.cgi?id=6530 - 2. E.g. "Content-Type: text/plain; charset=UTF-8\n" in po/is.po - */ setlocale(LC_CTYPE, ""); charset = locale_charset(); bind_textdomain_codeset(domain, charset); - /* the string is taken from v0.99.6~1 */ + + /* + * Work around an old bug fixed in glibc 2.17 (released on + * 2012-12-24), at the cost of potentially making translated + * messages from external functions like perror() emitted in + * the wrong encoding. + * + * The bug affected e.g. git.git's own 7eb93c89651 ([PATCH] + * Simplify git script, 2005-09-07), which is the origin of + * the "David_K\345gedal" test string. + * + * See a much longer comment added to this file in 5e9637c6297 + * (i18n: add infrastructure for translating Git with gettext, + * 2011-11-18) for more details. + */ if (test_vsnprintf("%.*s", 13, "David_K\345gedal") < 0) setlocale(LC_CTYPE, "C"); } diff --git a/git-mergetool--lib.sh b/git-mergetool--lib.sh index 7225abd811..78f3647ed9 100644 --- a/git-mergetool--lib.sh +++ b/git-mergetool--lib.sh @@ -46,9 +46,11 @@ show_tool_names () { while read scriptname do setup_tool "$scriptname" 2>/dev/null - variants="$variants$(list_tool_variants)\n" + # We need an actual line feed here + variants="$variants +$(list_tool_variants)" done - variants="$(echo "$variants" | sort | uniq)" + variants="$(echo "$variants" | sort -u)" for toolname in $variants do @@ -3031,7 +3031,7 @@ class P4Sync(Command, P4UserMap): regexp = re.compile(pattern, re.VERBOSE) text = ''.join(decode_text_stream(c) for c in contents) text = regexp.sub(r'$\1$', text) - contents = [ text ] + contents = [ encode_text_stream(text) ] if self.largeFileSystem: (git_mode, contents) = self.largeFileSystem.processContent(git_mode, relPath, contents) diff --git a/sha1-lookup.c b/hash-lookup.c index 29185844ec..1191856a32 100644 --- a/sha1-lookup.c +++ b/hash-lookup.c @@ -1,9 +1,9 @@ #include "cache.h" -#include "sha1-lookup.h" +#include "hash-lookup.h" -static uint32_t take2(const unsigned char *sha1) +static uint32_t take2(const unsigned char *hash) { - return ((sha1[0] << 8) | sha1[1]); + return ((hash[0] << 8) | hash[1]); } /* @@ -47,11 +47,11 @@ static uint32_t take2(const unsigned char *sha1) */ /* * The table should contain "nr" elements. - * The sha1 of element i (between 0 and nr - 1) should be returned + * The hash of element i (between 0 and nr - 1) should be returned * by "fn(i, table)". */ -int sha1_pos(const unsigned char *hash, void *table, size_t nr, - sha1_access_fn fn) +int hash_pos(const unsigned char *hash, void *table, size_t nr, + hash_access_fn fn) { size_t hi = nr; size_t lo = 0; @@ -74,7 +74,7 @@ int sha1_pos(const unsigned char *hash, void *table, size_t nr, if (lov != hiv) { /* * At this point miv could be equal - * to hiv (but sha1 could still be higher); + * to hiv (but hash could still be higher); * the invariant of (mi < hi) should be * kept. */ @@ -100,17 +100,17 @@ int sha1_pos(const unsigned char *hash, void *table, size_t nr, return index_pos_to_insert_pos(lo); } -int bsearch_hash(const unsigned char *sha1, const uint32_t *fanout_nbo, +int bsearch_hash(const unsigned char *hash, const uint32_t *fanout_nbo, const unsigned char *table, size_t stride, uint32_t *result) { uint32_t hi, lo; - hi = ntohl(fanout_nbo[*sha1]); - lo = ((*sha1 == 0x0) ? 0 : ntohl(fanout_nbo[*sha1 - 1])); + hi = ntohl(fanout_nbo[*hash]); + lo = ((*hash == 0x0) ? 0 : ntohl(fanout_nbo[*hash - 1])); while (lo < hi) { unsigned mi = lo + (hi - lo) / 2; - int cmp = hashcmp(table + mi * stride, sha1); + int cmp = hashcmp(table + mi * stride, hash); if (!cmp) { if (result) diff --git a/sha1-lookup.h b/hash-lookup.h index 5afcd011c6..5d476dec72 100644 --- a/sha1-lookup.h +++ b/hash-lookup.h @@ -1,20 +1,20 @@ -#ifndef SHA1_LOOKUP_H -#define SHA1_LOOKUP_H +#ifndef HASH_LOOKUP_H +#define HASH_LOOKUP_H -typedef const unsigned char *sha1_access_fn(size_t index, void *table); +typedef const unsigned char *hash_access_fn(size_t index, void *table); -int sha1_pos(const unsigned char *sha1, +int hash_pos(const unsigned char *hash, void *table, size_t nr, - sha1_access_fn fn); + hash_access_fn fn); /* - * Searches for sha1 in table, using the given fanout table to determine the + * Searches for hash in table, using the given fanout table to determine the * interval to search, then using binary search. Returns 1 if found, 0 if not. * * Takes the following parameters: * - * - sha1: the hash to search for + * - hash: the hash to search for * - fanout_nbo: a 256-element array of NETWORK-order 32-bit integers; the * integer at position i represents the number of elements in table whose * first byte is less than or equal to i @@ -23,10 +23,10 @@ int sha1_pos(const unsigned char *sha1, * GIT_MAX_RAWSZ or greater) * - result: if not NULL, this function stores the element index of the * position found (if the search is successful) or the index of the least - * element that is greater than sha1 (if the search is not successful) + * element that is greater than hash (if the search is not successful) * * This function does not verify the validity of the fanout table. */ -int bsearch_hash(const unsigned char *sha1, const uint32_t *fanout_nbo, +int bsearch_hash(const unsigned char *hash, const uint32_t *fanout_nbo, const unsigned char *table, size_t stride, uint32_t *result); #endif diff --git a/list-objects-filter.c b/list-objects-filter.c index 0a3ef3cab3..4ec0041cfb 100644 --- a/list-objects-filter.c +++ b/list-objects-filter.c @@ -21,7 +21,7 @@ * in the traversal (until we mark it SEEN). This is a way to * let us silently de-dup calls to show() in the caller. This * is subtly different from the "revision.h:SHOWN" and the - * "sha1-name.c:ONELINE_SEEN" bits. And also different from + * "object-name.c:ONELINE_SEEN" bits. And also different from * the non-de-dup usage in pack-bitmap.c */ #define FILTER_SHOWN_BUT_REVISIT (1<<21) diff --git a/merge-ort.c b/merge-ort.c index b487901d3e..31103d2140 100644 --- a/merge-ort.c +++ b/merge-ort.c @@ -17,20 +17,1535 @@ #include "cache.h" #include "merge-ort.h" +#include "alloc.h" +#include "blob.h" +#include "cache-tree.h" +#include "commit.h" +#include "commit-reach.h" +#include "diff.h" +#include "diffcore.h" +#include "dir.h" +#include "object-store.h" +#include "strmap.h" +#include "tree.h" +#include "unpack-trees.h" +#include "xdiff-interface.h" + +/* + * We have many arrays of size 3. Whenever we have such an array, the + * indices refer to one of the sides of the three-way merge. This is so + * pervasive that the constants 0, 1, and 2 are used in many places in the + * code (especially in arithmetic operations to find the other side's index + * or to compute a relevant mask), but sometimes these enum names are used + * to aid code clarity. + * + * See also 'filemask' and 'dirmask' in struct conflict_info; the "ith side" + * referred to there is one of these three sides. + */ +enum merge_side { + MERGE_BASE = 0, + MERGE_SIDE1 = 1, + MERGE_SIDE2 = 2 +}; + +struct merge_options_internal { + /* + * paths: primary data structure in all of merge ort. + * + * The keys of paths: + * * are full relative paths from the toplevel of the repository + * (e.g. "drivers/firmware/raspberrypi.c"). + * * store all relevant paths in the repo, both directories and + * files (e.g. drivers, drivers/firmware would also be included) + * * these keys serve to intern all the path strings, which allows + * us to do pointer comparison on directory names instead of + * strcmp; we just have to be careful to use the interned strings. + * (Technically paths_to_free may track some strings that were + * removed from froms paths.) + * + * The values of paths: + * * either a pointer to a merged_info, or a conflict_info struct + * * merged_info contains all relevant information for a + * non-conflicted entry. + * * conflict_info contains a merged_info, plus any additional + * information about a conflict such as the higher orders stages + * involved and the names of the paths those came from (handy + * once renames get involved). + * * a path may start "conflicted" (i.e. point to a conflict_info) + * and then a later step (e.g. three-way content merge) determines + * it can be cleanly merged, at which point it'll be marked clean + * and the algorithm will ignore any data outside the contained + * merged_info for that entry + * * If an entry remains conflicted, the merged_info portion of a + * conflict_info will later be filled with whatever version of + * the file should be placed in the working directory (e.g. an + * as-merged-as-possible variation that contains conflict markers). + */ + struct strmap paths; + + /* + * conflicted: a subset of keys->values from "paths" + * + * conflicted is basically an optimization between process_entries() + * and record_conflicted_index_entries(); the latter could loop over + * ALL the entries in paths AGAIN and look for the ones that are + * still conflicted, but since process_entries() has to loop over + * all of them, it saves the ones it couldn't resolve in this strmap + * so that record_conflicted_index_entries() can iterate just the + * relevant entries. + */ + struct strmap conflicted; + + /* + * paths_to_free: additional list of strings to free + * + * If keys are removed from "paths", they are added to paths_to_free + * to ensure they are later freed. We avoid free'ing immediately since + * other places (e.g. conflict_info.pathnames[]) may still be + * referencing these paths. + */ + struct string_list paths_to_free; + + /* + * output: special messages and conflict notices for various paths + * + * This is a map of pathnames (a subset of the keys in "paths" above) + * to strbufs. It gathers various warning/conflict/notice messages + * for later processing. + */ + struct strmap output; + + /* + * current_dir_name: temporary var used in collect_merge_info_callback() + * + * Used to set merged_info.directory_name; see documentation for that + * variable and the requirements placed on that field. + */ + const char *current_dir_name; + + /* call_depth: recursion level counter for merging merge bases */ + int call_depth; +}; + +struct version_info { + struct object_id oid; + unsigned short mode; +}; + +struct merged_info { + /* if is_null, ignore result. otherwise result has oid & mode */ + struct version_info result; + unsigned is_null:1; + + /* + * clean: whether the path in question is cleanly merged. + * + * see conflict_info.merged for more details. + */ + unsigned clean:1; + + /* + * basename_offset: offset of basename of path. + * + * perf optimization to avoid recomputing offset of final '/' + * character in pathname (0 if no '/' in pathname). + */ + size_t basename_offset; + + /* + * directory_name: containing directory name. + * + * Note that we assume directory_name is constructed such that + * strcmp(dir1_name, dir2_name) == 0 iff dir1_name == dir2_name, + * i.e. string equality is equivalent to pointer equality. For this + * to hold, we have to be careful setting directory_name. + */ + const char *directory_name; +}; + +struct conflict_info { + /* + * merged: the version of the path that will be written to working tree + * + * WARNING: It is critical to check merged.clean and ensure it is 0 + * before reading any conflict_info fields outside of merged. + * Allocated merge_info structs will always have clean set to 1. + * Allocated conflict_info structs will have merged.clean set to 0 + * initially. The merged.clean field is how we know if it is safe + * to access other parts of conflict_info besides merged; if a + * conflict_info's merged.clean is changed to 1, the rest of the + * algorithm is not allowed to look at anything outside of the + * merged member anymore. + */ + struct merged_info merged; + + /* oids & modes from each of the three trees for this path */ + struct version_info stages[3]; + + /* pathnames for each stage; may differ due to rename detection */ + const char *pathnames[3]; + + /* Whether this path is/was involved in a directory/file conflict */ + unsigned df_conflict:1; + + /* + * Whether this path is/was involved in a non-content conflict other + * than a directory/file conflict (e.g. rename/rename, rename/delete, + * file location based on possible directory rename). + */ + unsigned path_conflict:1; + + /* + * For filemask and dirmask, the ith bit corresponds to whether the + * ith entry is a file (filemask) or a directory (dirmask). Thus, + * filemask & dirmask is always zero, and filemask | dirmask is at + * most 7 but can be less when a path does not appear as either a + * file or a directory on at least one side of history. + * + * Note that these masks are related to enum merge_side, as the ith + * entry corresponds to side i. + * + * These values come from a traverse_trees() call; more info may be + * found looking at tree-walk.h's struct traverse_info, + * particularly the documentation above the "fn" member (note that + * filemask = mask & ~dirmask from that documentation). + */ + unsigned filemask:3; + unsigned dirmask:3; + + /* + * Optimization to track which stages match, to avoid the need to + * recompute it in multiple steps. Either 0 or at least 2 bits are + * set; if at least 2 bits are set, their corresponding stages match. + */ + unsigned match_mask:3; +}; + +/*** Function Grouping: various utility functions ***/ + +/* + * For the next three macros, see warning for conflict_info.merged. + * + * In each of the below, mi is a struct merged_info*, and ci was defined + * as a struct conflict_info* (but we need to verify ci isn't actually + * pointed at a struct merged_info*). + * + * INITIALIZE_CI: Assign ci to mi but only if it's safe; set to NULL otherwise. + * VERIFY_CI: Ensure that something we assigned to a conflict_info* is one. + * ASSIGN_AND_VERIFY_CI: Similar to VERIFY_CI but do assignment first. + */ +#define INITIALIZE_CI(ci, mi) do { \ + (ci) = (!(mi) || (mi)->clean) ? NULL : (struct conflict_info *)(mi); \ +} while (0) +#define VERIFY_CI(ci) assert(ci && !ci->merged.clean); +#define ASSIGN_AND_VERIFY_CI(ci, mi) do { \ + (ci) = (struct conflict_info *)(mi); \ + assert((ci) && !(mi)->clean); \ +} while (0) + +static void free_strmap_strings(struct strmap *map) +{ + struct hashmap_iter iter; + struct strmap_entry *entry; + + strmap_for_each_entry(map, &iter, entry) { + free((char*)entry->key); + } +} + +static void clear_or_reinit_internal_opts(struct merge_options_internal *opti, + int reinitialize) +{ + void (*strmap_func)(struct strmap *, int) = + reinitialize ? strmap_partial_clear : strmap_clear; + + /* + * We marked opti->paths with strdup_strings = 0, so that we + * wouldn't have to make another copy of the fullpath created by + * make_traverse_path from setup_path_info(). But, now that we've + * used it and have no other references to these strings, it is time + * to deallocate them. + */ + free_strmap_strings(&opti->paths); + strmap_func(&opti->paths, 1); + + /* + * All keys and values in opti->conflicted are a subset of those in + * opti->paths. We don't want to deallocate anything twice, so we + * don't free the keys and we pass 0 for free_values. + */ + strmap_func(&opti->conflicted, 0); + + /* + * opti->paths_to_free is similar to opti->paths; we created it with + * strdup_strings = 0 to avoid making _another_ copy of the fullpath + * but now that we've used it and have no other references to these + * strings, it is time to deallocate them. We do so by temporarily + * setting strdup_strings to 1. + */ + opti->paths_to_free.strdup_strings = 1; + string_list_clear(&opti->paths_to_free, 0); + opti->paths_to_free.strdup_strings = 0; + + if (!reinitialize) { + struct hashmap_iter iter; + struct strmap_entry *e; + + /* Release and free each strbuf found in output */ + strmap_for_each_entry(&opti->output, &iter, e) { + struct strbuf *sb = e->value; + strbuf_release(sb); + /* + * While strictly speaking we don't need to free(sb) + * here because we could pass free_values=1 when + * calling strmap_clear() on opti->output, that would + * require strmap_clear to do another + * strmap_for_each_entry() loop, so we just free it + * while we're iterating anyway. + */ + free(sb); + } + strmap_clear(&opti->output, 0); + } +} + +static int err(struct merge_options *opt, const char *err, ...) +{ + va_list params; + struct strbuf sb = STRBUF_INIT; + + strbuf_addstr(&sb, "error: "); + va_start(params, err); + strbuf_vaddf(&sb, err, params); + va_end(params); + + error("%s", sb.buf); + strbuf_release(&sb); + + return -1; +} + +__attribute__((format (printf, 4, 5))) +static void path_msg(struct merge_options *opt, + const char *path, + int omittable_hint, /* skippable under --remerge-diff */ + const char *fmt, ...) +{ + va_list ap; + struct strbuf *sb = strmap_get(&opt->priv->output, path); + if (!sb) { + sb = xmalloc(sizeof(*sb)); + strbuf_init(sb, 0); + strmap_put(&opt->priv->output, path, sb); + } + + va_start(ap, fmt); + strbuf_vaddf(sb, fmt, ap); + va_end(ap); + + strbuf_addch(sb, '\n'); +} + +/*** Function Grouping: functions related to collect_merge_info() ***/ + +static void setup_path_info(struct merge_options *opt, + struct string_list_item *result, + const char *current_dir_name, + int current_dir_name_len, + char *fullpath, /* we'll take over ownership */ + struct name_entry *names, + struct name_entry *merged_version, + unsigned is_null, /* boolean */ + unsigned df_conflict, /* boolean */ + unsigned filemask, + unsigned dirmask, + int resolved /* boolean */) +{ + /* result->util is void*, so mi is a convenience typed variable */ + struct merged_info *mi; + + assert(!is_null || resolved); + assert(!df_conflict || !resolved); /* df_conflict implies !resolved */ + assert(resolved == (merged_version != NULL)); + + mi = xcalloc(1, resolved ? sizeof(struct merged_info) : + sizeof(struct conflict_info)); + mi->directory_name = current_dir_name; + mi->basename_offset = current_dir_name_len; + mi->clean = !!resolved; + if (resolved) { + mi->result.mode = merged_version->mode; + oidcpy(&mi->result.oid, &merged_version->oid); + mi->is_null = !!is_null; + } else { + int i; + struct conflict_info *ci; + + ASSIGN_AND_VERIFY_CI(ci, mi); + for (i = MERGE_BASE; i <= MERGE_SIDE2; i++) { + ci->pathnames[i] = fullpath; + ci->stages[i].mode = names[i].mode; + oidcpy(&ci->stages[i].oid, &names[i].oid); + } + ci->filemask = filemask; + ci->dirmask = dirmask; + ci->df_conflict = !!df_conflict; + if (dirmask) + /* + * Assume is_null for now, but if we have entries + * under the directory then when it is complete in + * write_completed_directory() it'll update this. + * Also, for D/F conflicts, we have to handle the + * directory first, then clear this bit and process + * the file to see how it is handled -- that occurs + * near the top of process_entry(). + */ + mi->is_null = 1; + } + strmap_put(&opt->priv->paths, fullpath, mi); + result->string = fullpath; + result->util = mi; +} + +static int collect_merge_info_callback(int n, + unsigned long mask, + unsigned long dirmask, + struct name_entry *names, + struct traverse_info *info) +{ + /* + * n is 3. Always. + * common ancestor (mbase) has mask 1, and stored in index 0 of names + * head of side 1 (side1) has mask 2, and stored in index 1 of names + * head of side 2 (side2) has mask 4, and stored in index 2 of names + */ + struct merge_options *opt = info->data; + struct merge_options_internal *opti = opt->priv; + struct string_list_item pi; /* Path Info */ + struct conflict_info *ci; /* typed alias to pi.util (which is void*) */ + struct name_entry *p; + size_t len; + char *fullpath; + const char *dirname = opti->current_dir_name; + unsigned filemask = mask & ~dirmask; + unsigned match_mask = 0; /* will be updated below */ + unsigned mbase_null = !(mask & 1); + unsigned side1_null = !(mask & 2); + unsigned side2_null = !(mask & 4); + unsigned side1_matches_mbase = (!side1_null && !mbase_null && + names[0].mode == names[1].mode && + oideq(&names[0].oid, &names[1].oid)); + unsigned side2_matches_mbase = (!side2_null && !mbase_null && + names[0].mode == names[2].mode && + oideq(&names[0].oid, &names[2].oid)); + unsigned sides_match = (!side1_null && !side2_null && + names[1].mode == names[2].mode && + oideq(&names[1].oid, &names[2].oid)); + + /* + * Note: When a path is a file on one side of history and a directory + * in another, we have a directory/file conflict. In such cases, if + * the conflict doesn't resolve from renames and deletions, then we + * always leave directories where they are and move files out of the + * way. Thus, while struct conflict_info has a df_conflict field to + * track such conflicts, we ignore that field for any directories at + * a path and only pay attention to it for files at the given path. + * The fact that we leave directories were they are also means that + * we do not need to worry about getting additional df_conflict + * information propagated from parent directories down to children + * (unlike, say traverse_trees_recursive() in unpack-trees.c, which + * sets a newinfo.df_conflicts field specifically to propagate it). + */ + unsigned df_conflict = (filemask != 0) && (dirmask != 0); + + /* n = 3 is a fundamental assumption. */ + if (n != 3) + BUG("Called collect_merge_info_callback wrong"); + + /* + * A bunch of sanity checks verifying that traverse_trees() calls + * us the way I expect. Could just remove these at some point, + * though maybe they are helpful to future code readers. + */ + assert(mbase_null == is_null_oid(&names[0].oid)); + assert(side1_null == is_null_oid(&names[1].oid)); + assert(side2_null == is_null_oid(&names[2].oid)); + assert(!mbase_null || !side1_null || !side2_null); + assert(mask > 0 && mask < 8); + + /* Determine match_mask */ + if (side1_matches_mbase) + match_mask = (side2_matches_mbase ? 7 : 3); + else if (side2_matches_mbase) + match_mask = 5; + else if (sides_match) + match_mask = 6; + + /* + * Get the name of the relevant filepath, which we'll pass to + * setup_path_info() for tracking. + */ + p = names; + while (!p->mode) + p++; + len = traverse_path_len(info, p->pathlen); + + /* +1 in both of the following lines to include the NUL byte */ + fullpath = xmalloc(len + 1); + make_traverse_path(fullpath, len + 1, info, p->path, p->pathlen); + + /* + * If mbase, side1, and side2 all match, we can resolve early. Even + * if these are trees, there will be no renames or anything + * underneath. + */ + if (side1_matches_mbase && side2_matches_mbase) { + /* mbase, side1, & side2 all match; use mbase as resolution */ + setup_path_info(opt, &pi, dirname, info->pathlen, fullpath, + names, names+0, mbase_null, 0, + filemask, dirmask, 1); + return mask; + } + + /* + * Record information about the path so we can resolve later in + * process_entries. + */ + setup_path_info(opt, &pi, dirname, info->pathlen, fullpath, + names, NULL, 0, df_conflict, filemask, dirmask, 0); + + ci = pi.util; + VERIFY_CI(ci); + ci->match_mask = match_mask; + + /* If dirmask, recurse into subdirectories */ + if (dirmask) { + struct traverse_info newinfo; + struct tree_desc t[3]; + void *buf[3] = {NULL, NULL, NULL}; + const char *original_dir_name; + int i, ret; + + ci->match_mask &= filemask; + newinfo = *info; + newinfo.prev = info; + newinfo.name = p->path; + newinfo.namelen = p->pathlen; + newinfo.pathlen = st_add3(newinfo.pathlen, p->pathlen, 1); + /* + * If this directory we are about to recurse into cared about + * its parent directory (the current directory) having a D/F + * conflict, then we'd propagate the masks in this way: + * newinfo.df_conflicts |= (mask & ~dirmask); + * But we don't worry about propagating D/F conflicts. (See + * comment near setting of local df_conflict variable near + * the beginning of this function). + */ + + for (i = MERGE_BASE; i <= MERGE_SIDE2; i++) { + if (i == 1 && side1_matches_mbase) + t[1] = t[0]; + else if (i == 2 && side2_matches_mbase) + t[2] = t[0]; + else if (i == 2 && sides_match) + t[2] = t[1]; + else { + const struct object_id *oid = NULL; + if (dirmask & 1) + oid = &names[i].oid; + buf[i] = fill_tree_descriptor(opt->repo, + t + i, oid); + } + dirmask >>= 1; + } + + original_dir_name = opti->current_dir_name; + opti->current_dir_name = pi.string; + ret = traverse_trees(NULL, 3, t, &newinfo); + opti->current_dir_name = original_dir_name; + + for (i = MERGE_BASE; i <= MERGE_SIDE2; i++) + free(buf[i]); + + if (ret < 0) + return -1; + } + + return mask; +} + +static int collect_merge_info(struct merge_options *opt, + struct tree *merge_base, + struct tree *side1, + struct tree *side2) +{ + int ret; + struct tree_desc t[3]; + struct traverse_info info; + const char *toplevel_dir_placeholder = ""; + + opt->priv->current_dir_name = toplevel_dir_placeholder; + setup_traverse_info(&info, toplevel_dir_placeholder); + info.fn = collect_merge_info_callback; + info.data = opt; + info.show_all_errors = 1; + + parse_tree(merge_base); + parse_tree(side1); + parse_tree(side2); + init_tree_desc(t + 0, merge_base->buffer, merge_base->size); + init_tree_desc(t + 1, side1->buffer, side1->size); + init_tree_desc(t + 2, side2->buffer, side2->size); + + ret = traverse_trees(NULL, 3, t, &info); + + return ret; +} + +/*** Function Grouping: functions related to threeway content merges ***/ + +static int handle_content_merge(struct merge_options *opt, + const char *path, + const struct version_info *o, + const struct version_info *a, + const struct version_info *b, + const char *pathnames[3], + const int extra_marker_size, + struct version_info *result) +{ + die("Not yet implemented"); +} + +/*** Function Grouping: functions related to detect_and_process_renames(), *** + *** which are split into directory and regular rename detection sections. ***/ + +/*** Function Grouping: functions related to directory rename detection ***/ + +/*** Function Grouping: functions related to regular rename detection ***/ + +static int detect_and_process_renames(struct merge_options *opt, + struct tree *merge_base, + struct tree *side1, + struct tree *side2) +{ + int clean = 1; + + /* + * Rename detection works by detecting file similarity. Here we use + * a really easy-to-implement scheme: files are similar IFF they have + * the same filename. Therefore, by this scheme, there are no renames. + * + * TODO: Actually implement a real rename detection scheme. + */ + return clean; +} + +/*** Function Grouping: functions related to process_entries() ***/ + +static int string_list_df_name_compare(const char *one, const char *two) +{ + int onelen = strlen(one); + int twolen = strlen(two); + /* + * Here we only care that entries for D/F conflicts are + * adjacent, in particular with the file of the D/F conflict + * appearing before files below the corresponding directory. + * The order of the rest of the list is irrelevant for us. + * + * To achieve this, we sort with df_name_compare and provide + * the mode S_IFDIR so that D/F conflicts will sort correctly. + * We use the mode S_IFDIR for everything else for simplicity, + * since in other cases any changes in their order due to + * sorting cause no problems for us. + */ + int cmp = df_name_compare(one, onelen, S_IFDIR, + two, twolen, S_IFDIR); + /* + * Now that 'foo' and 'foo/bar' compare equal, we have to make sure + * that 'foo' comes before 'foo/bar'. + */ + if (cmp) + return cmp; + return onelen - twolen; +} + +struct directory_versions { + /* + * versions: list of (basename -> version_info) + * + * The basenames are in reverse lexicographic order of full pathnames, + * as processed in process_entries(). This puts all entries within + * a directory together, and covers the directory itself after + * everything within it, allowing us to write subtrees before needing + * to record information for the tree itself. + */ + struct string_list versions; + + /* + * offsets: list of (full relative path directories -> integer offsets) + * + * Since versions contains basenames from files in multiple different + * directories, we need to know which entries in versions correspond + * to which directories. Values of e.g. + * "" 0 + * src 2 + * src/moduleA 5 + * Would mean that entries 0-1 of versions are files in the toplevel + * directory, entries 2-4 are files under src/, and the remaining + * entries starting at index 5 are files under src/moduleA/. + */ + struct string_list offsets; + + /* + * last_directory: directory that previously processed file found in + * + * last_directory starts NULL, but records the directory in which the + * previous file was found within. As soon as + * directory(current_file) != last_directory + * then we need to start updating accounting in versions & offsets. + * Note that last_directory is always the last path in "offsets" (or + * NULL if "offsets" is empty) so this exists just for quick access. + */ + const char *last_directory; + + /* last_directory_len: cached computation of strlen(last_directory) */ + unsigned last_directory_len; +}; + +static int tree_entry_order(const void *a_, const void *b_) +{ + const struct string_list_item *a = a_; + const struct string_list_item *b = b_; + + const struct merged_info *ami = a->util; + const struct merged_info *bmi = b->util; + return base_name_compare(a->string, strlen(a->string), ami->result.mode, + b->string, strlen(b->string), bmi->result.mode); +} + +static void write_tree(struct object_id *result_oid, + struct string_list *versions, + unsigned int offset, + size_t hash_size) +{ + size_t maxlen = 0, extra; + unsigned int nr = versions->nr - offset; + struct strbuf buf = STRBUF_INIT; + struct string_list relevant_entries = STRING_LIST_INIT_NODUP; + int i; + + /* + * We want to sort the last (versions->nr-offset) entries in versions. + * Do so by abusing the string_list API a bit: make another string_list + * that contains just those entries and then sort them. + * + * We won't use relevant_entries again and will let it just pop off the + * stack, so there won't be allocation worries or anything. + */ + relevant_entries.items = versions->items + offset; + relevant_entries.nr = versions->nr - offset; + QSORT(relevant_entries.items, relevant_entries.nr, tree_entry_order); + + /* Pre-allocate some space in buf */ + extra = hash_size + 8; /* 8: 6 for mode, 1 for space, 1 for NUL char */ + for (i = 0; i < nr; i++) { + maxlen += strlen(versions->items[offset+i].string) + extra; + } + strbuf_grow(&buf, maxlen); + + /* Write each entry out to buf */ + for (i = 0; i < nr; i++) { + struct merged_info *mi = versions->items[offset+i].util; + struct version_info *ri = &mi->result; + strbuf_addf(&buf, "%o %s%c", + ri->mode, + versions->items[offset+i].string, '\0'); + strbuf_add(&buf, ri->oid.hash, hash_size); + } + + /* Write this object file out, and record in result_oid */ + write_object_file(buf.buf, buf.len, tree_type, result_oid); + strbuf_release(&buf); +} + +static void record_entry_for_tree(struct directory_versions *dir_metadata, + const char *path, + struct merged_info *mi) +{ + const char *basename; + + if (mi->is_null) + /* nothing to record */ + return; + + basename = path + mi->basename_offset; + assert(strchr(basename, '/') == NULL); + string_list_append(&dir_metadata->versions, + basename)->util = &mi->result; +} + +static void write_completed_directory(struct merge_options *opt, + const char *new_directory_name, + struct directory_versions *info) +{ + const char *prev_dir; + struct merged_info *dir_info = NULL; + unsigned int offset; + + /* + * Some explanation of info->versions and info->offsets... + * + * process_entries() iterates over all relevant files AND + * directories in reverse lexicographic order, and calls this + * function. Thus, an example of the paths that process_entries() + * could operate on (along with the directories for those paths + * being shown) is: + * + * xtract.c "" + * tokens.txt "" + * src/moduleB/umm.c src/moduleB + * src/moduleB/stuff.h src/moduleB + * src/moduleB/baz.c src/moduleB + * src/moduleB src + * src/moduleA/foo.c src/moduleA + * src/moduleA/bar.c src/moduleA + * src/moduleA src + * src "" + * Makefile "" + * + * info->versions: + * + * always contains the unprocessed entries and their + * version_info information. For example, after the first five + * entries above, info->versions would be: + * + * xtract.c <xtract.c's version_info> + * token.txt <token.txt's version_info> + * umm.c <src/moduleB/umm.c's version_info> + * stuff.h <src/moduleB/stuff.h's version_info> + * baz.c <src/moduleB/baz.c's version_info> + * + * Once a subdirectory is completed we remove the entries in + * that subdirectory from info->versions, writing it as a tree + * (write_tree()). Thus, as soon as we get to src/moduleB, + * info->versions would be updated to + * + * xtract.c <xtract.c's version_info> + * token.txt <token.txt's version_info> + * moduleB <src/moduleB's version_info> + * + * info->offsets: + * + * helps us track which entries in info->versions correspond to + * which directories. When we are N directories deep (e.g. 4 + * for src/modA/submod/subdir/), we have up to N+1 unprocessed + * directories (+1 because of toplevel dir). Corresponding to + * the info->versions example above, after processing five entries + * info->offsets will be: + * + * "" 0 + * src/moduleB 2 + * + * which is used to know that xtract.c & token.txt are from the + * toplevel dirctory, while umm.c & stuff.h & baz.c are from the + * src/moduleB directory. Again, following the example above, + * once we need to process src/moduleB, then info->offsets is + * updated to + * + * "" 0 + * src 2 + * + * which says that moduleB (and only moduleB so far) is in the + * src directory. + * + * One unique thing to note about info->offsets here is that + * "src" was not added to info->offsets until there was a path + * (a file OR directory) immediately below src/ that got + * processed. + * + * Since process_entry() just appends new entries to info->versions, + * write_completed_directory() only needs to do work if the next path + * is in a directory that is different than the last directory found + * in info->offsets. + */ + + /* + * If we are working with the same directory as the last entry, there + * is no work to do. (See comments above the directory_name member of + * struct merged_info for why we can use pointer comparison instead of + * strcmp here.) + */ + if (new_directory_name == info->last_directory) + return; + + /* + * If we are just starting (last_directory is NULL), or last_directory + * is a prefix of the current directory, then we can just update + * info->offsets to record the offset where we started this directory + * and update last_directory to have quick access to it. + */ + if (info->last_directory == NULL || + !strncmp(new_directory_name, info->last_directory, + info->last_directory_len)) { + uintptr_t offset = info->versions.nr; + + info->last_directory = new_directory_name; + info->last_directory_len = strlen(info->last_directory); + /* + * Record the offset into info->versions where we will + * start recording basenames of paths found within + * new_directory_name. + */ + string_list_append(&info->offsets, + info->last_directory)->util = (void*)offset; + return; + } + + /* + * The next entry that will be processed will be within + * new_directory_name. Since at this point we know that + * new_directory_name is within a different directory than + * info->last_directory, we have all entries for info->last_directory + * in info->versions and we need to create a tree object for them. + */ + dir_info = strmap_get(&opt->priv->paths, info->last_directory); + assert(dir_info); + offset = (uintptr_t)info->offsets.items[info->offsets.nr-1].util; + if (offset == info->versions.nr) { + /* + * Actually, we don't need to create a tree object in this + * case. Whenever all files within a directory disappear + * during the merge (e.g. unmodified on one side and + * deleted on the other, or files were renamed elsewhere), + * then we get here and the directory itself needs to be + * omitted from its parent tree as well. + */ + dir_info->is_null = 1; + } else { + /* + * Write out the tree to the git object directory, and also + * record the mode and oid in dir_info->result. + */ + dir_info->is_null = 0; + dir_info->result.mode = S_IFDIR; + write_tree(&dir_info->result.oid, &info->versions, offset, + opt->repo->hash_algo->rawsz); + } + + /* + * We've now used several entries from info->versions and one entry + * from info->offsets, so we get rid of those values. + */ + info->offsets.nr--; + info->versions.nr = offset; + + /* + * Now we've taken care of the completed directory, but we need to + * prepare things since future entries will be in + * new_directory_name. (In particular, process_entry() will be + * appending new entries to info->versions.) So, we need to make + * sure new_directory_name is the last entry in info->offsets. + */ + prev_dir = info->offsets.nr == 0 ? NULL : + info->offsets.items[info->offsets.nr-1].string; + if (new_directory_name != prev_dir) { + uintptr_t c = info->versions.nr; + string_list_append(&info->offsets, + new_directory_name)->util = (void*)c; + } + + /* And, of course, we need to update last_directory to match. */ + info->last_directory = new_directory_name; + info->last_directory_len = strlen(info->last_directory); +} + +/* Per entry merge function */ +static void process_entry(struct merge_options *opt, + const char *path, + struct conflict_info *ci, + struct directory_versions *dir_metadata) +{ + VERIFY_CI(ci); + assert(ci->filemask >= 0 && ci->filemask <= 7); + /* ci->match_mask == 7 was handled in collect_merge_info_callback() */ + assert(ci->match_mask == 0 || ci->match_mask == 3 || + ci->match_mask == 5 || ci->match_mask == 6); + + if (ci->dirmask) { + record_entry_for_tree(dir_metadata, path, &ci->merged); + if (ci->filemask == 0) + /* nothing else to handle */ + return; + assert(ci->df_conflict); + } + + if (ci->df_conflict) { + die("Not yet implemented."); + } + + /* + * NOTE: Below there is a long switch-like if-elseif-elseif... block + * which the code goes through even for the df_conflict cases + * above. Well, it will once we don't die-not-implemented above. + */ + if (ci->match_mask) { + ci->merged.clean = 1; + if (ci->match_mask == 6) { + /* stages[1] == stages[2] */ + ci->merged.result.mode = ci->stages[1].mode; + oidcpy(&ci->merged.result.oid, &ci->stages[1].oid); + } else { + /* determine the mask of the side that didn't match */ + unsigned int othermask = 7 & ~ci->match_mask; + int side = (othermask == 4) ? 2 : 1; + + ci->merged.result.mode = ci->stages[side].mode; + ci->merged.is_null = !ci->merged.result.mode; + oidcpy(&ci->merged.result.oid, &ci->stages[side].oid); + + assert(othermask == 2 || othermask == 4); + assert(ci->merged.is_null == + (ci->filemask == ci->match_mask)); + } + } else if (ci->filemask >= 6 && + (S_IFMT & ci->stages[1].mode) != + (S_IFMT & ci->stages[2].mode)) { + /* + * Two different items from (file/submodule/symlink) + */ + die("Not yet implemented."); + } else if (ci->filemask >= 6) { + /* + * TODO: Needs a two-way or three-way content merge, but we're + * just being lazy and copying the version from HEAD and + * leaving it as conflicted. + */ + ci->merged.clean = 0; + ci->merged.result.mode = ci->stages[1].mode; + oidcpy(&ci->merged.result.oid, &ci->stages[1].oid); + /* When we fix above, we'll call handle_content_merge() */ + (void)handle_content_merge; + } else if (ci->filemask == 3 || ci->filemask == 5) { + /* Modify/delete */ + const char *modify_branch, *delete_branch; + int side = (ci->filemask == 5) ? 2 : 1; + int index = opt->priv->call_depth ? 0 : side; + + ci->merged.result.mode = ci->stages[index].mode; + oidcpy(&ci->merged.result.oid, &ci->stages[index].oid); + ci->merged.clean = 0; + + modify_branch = (side == 1) ? opt->branch1 : opt->branch2; + delete_branch = (side == 1) ? opt->branch2 : opt->branch1; + + path_msg(opt, path, 0, + _("CONFLICT (modify/delete): %s deleted in %s " + "and modified in %s. Version %s of %s left " + "in tree."), + path, delete_branch, modify_branch, + modify_branch, path); + } else if (ci->filemask == 2 || ci->filemask == 4) { + /* Added on one side */ + int side = (ci->filemask == 4) ? 2 : 1; + ci->merged.result.mode = ci->stages[side].mode; + oidcpy(&ci->merged.result.oid, &ci->stages[side].oid); + ci->merged.clean = !ci->df_conflict; + } else if (ci->filemask == 1) { + /* Deleted on both sides */ + ci->merged.is_null = 1; + ci->merged.result.mode = 0; + oidcpy(&ci->merged.result.oid, &null_oid); + ci->merged.clean = 1; + } + + /* + * If still conflicted, record it separately. This allows us to later + * iterate over just conflicted entries when updating the index instead + * of iterating over all entries. + */ + if (!ci->merged.clean) + strmap_put(&opt->priv->conflicted, path, ci); + record_entry_for_tree(dir_metadata, path, &ci->merged); +} + +static void process_entries(struct merge_options *opt, + struct object_id *result_oid) +{ + struct hashmap_iter iter; + struct strmap_entry *e; + struct string_list plist = STRING_LIST_INIT_NODUP; + struct string_list_item *entry; + struct directory_versions dir_metadata = { STRING_LIST_INIT_NODUP, + STRING_LIST_INIT_NODUP, + NULL, 0 }; + + if (strmap_empty(&opt->priv->paths)) { + oidcpy(result_oid, opt->repo->hash_algo->empty_tree); + return; + } + + /* Hack to pre-allocate plist to the desired size */ + ALLOC_GROW(plist.items, strmap_get_size(&opt->priv->paths), plist.alloc); + + /* Put every entry from paths into plist, then sort */ + strmap_for_each_entry(&opt->priv->paths, &iter, e) { + string_list_append(&plist, e->key)->util = e->value; + } + plist.cmp = string_list_df_name_compare; + string_list_sort(&plist); + + /* + * Iterate over the items in reverse order, so we can handle paths + * below a directory before needing to handle the directory itself. + * + * This allows us to write subtrees before we need to write trees, + * and it also enables sane handling of directory/file conflicts + * (because it allows us to know whether the directory is still in + * the way when it is time to process the file at the same path). + */ + for (entry = &plist.items[plist.nr-1]; entry >= plist.items; --entry) { + char *path = entry->string; + /* + * NOTE: mi may actually be a pointer to a conflict_info, but + * we have to check mi->clean first to see if it's safe to + * reassign to such a pointer type. + */ + struct merged_info *mi = entry->util; + + write_completed_directory(opt, mi->directory_name, + &dir_metadata); + if (mi->clean) + record_entry_for_tree(&dir_metadata, path, mi); + else { + struct conflict_info *ci = (struct conflict_info *)mi; + process_entry(opt, path, ci, &dir_metadata); + } + } + + if (dir_metadata.offsets.nr != 1 || + (uintptr_t)dir_metadata.offsets.items[0].util != 0) { + printf("dir_metadata.offsets.nr = %d (should be 1)\n", + dir_metadata.offsets.nr); + printf("dir_metadata.offsets.items[0].util = %u (should be 0)\n", + (unsigned)(uintptr_t)dir_metadata.offsets.items[0].util); + fflush(stdout); + BUG("dir_metadata accounting completely off; shouldn't happen"); + } + write_tree(result_oid, &dir_metadata.versions, 0, + opt->repo->hash_algo->rawsz); + string_list_clear(&plist, 0); + string_list_clear(&dir_metadata.versions, 0); + string_list_clear(&dir_metadata.offsets, 0); +} + +/*** Function Grouping: functions related to merge_switch_to_result() ***/ + +static int checkout(struct merge_options *opt, + struct tree *prev, + struct tree *next) +{ + /* Switch the index/working copy from old to new */ + int ret; + struct tree_desc trees[2]; + struct unpack_trees_options unpack_opts; + + memset(&unpack_opts, 0, sizeof(unpack_opts)); + unpack_opts.head_idx = -1; + unpack_opts.src_index = opt->repo->index; + unpack_opts.dst_index = opt->repo->index; + + setup_unpack_trees_porcelain(&unpack_opts, "merge"); + + /* + * NOTE: if this were just "git checkout" code, we would probably + * read or refresh the cache and check for a conflicted index, but + * builtin/merge.c or sequencer.c really needs to read the index + * and check for conflicted entries before starting merging for a + * good user experience (no sense waiting for merges/rebases before + * erroring out), so there's no reason to duplicate that work here. + */ + + /* 2-way merge to the new branch */ + unpack_opts.update = 1; + unpack_opts.merge = 1; + unpack_opts.quiet = 0; /* FIXME: sequencer might want quiet? */ + unpack_opts.verbose_update = (opt->verbosity > 2); + unpack_opts.fn = twoway_merge; + if (1/* FIXME: opts->overwrite_ignore*/) { + unpack_opts.dir = xcalloc(1, sizeof(*unpack_opts.dir)); + unpack_opts.dir->flags |= DIR_SHOW_IGNORED; + setup_standard_excludes(unpack_opts.dir); + } + parse_tree(prev); + init_tree_desc(&trees[0], prev->buffer, prev->size); + parse_tree(next); + init_tree_desc(&trees[1], next->buffer, next->size); + + ret = unpack_trees(2, trees, &unpack_opts); + clear_unpack_trees_porcelain(&unpack_opts); + dir_clear(unpack_opts.dir); + FREE_AND_NULL(unpack_opts.dir); + return ret; +} + +static int record_conflicted_index_entries(struct merge_options *opt, + struct index_state *index, + struct strmap *paths, + struct strmap *conflicted) +{ + struct hashmap_iter iter; + struct strmap_entry *e; + int errs = 0; + int original_cache_nr; + + if (strmap_empty(conflicted)) + return 0; + + original_cache_nr = index->cache_nr; + + /* Put every entry from paths into plist, then sort */ + strmap_for_each_entry(conflicted, &iter, e) { + const char *path = e->key; + struct conflict_info *ci = e->value; + int pos; + struct cache_entry *ce; + int i; + + VERIFY_CI(ci); + + /* + * The index will already have a stage=0 entry for this path, + * because we created an as-merged-as-possible version of the + * file and checkout() moved the working copy and index over + * to that version. + * + * However, previous iterations through this loop will have + * added unstaged entries to the end of the cache which + * ignore the standard alphabetical ordering of cache + * entries and break invariants needed for index_name_pos() + * to work. However, we know the entry we want is before + * those appended cache entries, so do a temporary swap on + * cache_nr to only look through entries of interest. + */ + SWAP(index->cache_nr, original_cache_nr); + pos = index_name_pos(index, path, strlen(path)); + SWAP(index->cache_nr, original_cache_nr); + if (pos < 0) { + if (ci->filemask != 1) + BUG("Conflicted %s but nothing in basic working tree or index; this shouldn't happen", path); + cache_tree_invalidate_path(index, path); + } else { + ce = index->cache[pos]; + + /* + * Clean paths with CE_SKIP_WORKTREE set will not be + * written to the working tree by the unpack_trees() + * call in checkout(). Our conflicted entries would + * have appeared clean to that code since we ignored + * the higher order stages. Thus, we need override + * the CE_SKIP_WORKTREE bit and manually write those + * files to the working disk here. + * + * TODO: Implement this CE_SKIP_WORKTREE fixup. + */ + + /* + * Mark this cache entry for removal and instead add + * new stage>0 entries corresponding to the + * conflicts. If there are many conflicted entries, we + * want to avoid memmove'ing O(NM) entries by + * inserting the new entries one at a time. So, + * instead, we just add the new cache entries to the + * end (ignoring normal index requirements on sort + * order) and sort the index once we're all done. + */ + ce->ce_flags |= CE_REMOVE; + } + + for (i = MERGE_BASE; i <= MERGE_SIDE2; i++) { + struct version_info *vi; + if (!(ci->filemask & (1ul << i))) + continue; + vi = &ci->stages[i]; + ce = make_cache_entry(index, vi->mode, &vi->oid, + path, i+1, 0); + add_index_entry(index, ce, ADD_CACHE_JUST_APPEND); + } + } + + /* + * Remove the unused cache entries (and invalidate the relevant + * cache-trees), then sort the index entries to get the conflicted + * entries we added to the end into their right locations. + */ + remove_marked_cache_entries(index, 1); + QSORT(index->cache, index->cache_nr, cmp_cache_name_compare); + + return errs; +} + void merge_switch_to_result(struct merge_options *opt, struct tree *head, struct merge_result *result, int update_worktree_and_index, int display_update_msgs) { - die("Not yet implemented"); + assert(opt->priv == NULL); + if (result->clean >= 0 && update_worktree_and_index) { + struct merge_options_internal *opti = result->priv; + + if (checkout(opt, head, result->tree)) { + /* failure to function */ + result->clean = -1; + return; + } + + if (record_conflicted_index_entries(opt, opt->repo->index, + &opti->paths, + &opti->conflicted)) { + /* failure to function */ + result->clean = -1; + return; + } + } + + if (display_update_msgs) { + struct merge_options_internal *opti = result->priv; + struct hashmap_iter iter; + struct strmap_entry *e; + struct string_list olist = STRING_LIST_INIT_NODUP; + int i; + + /* Hack to pre-allocate olist to the desired size */ + ALLOC_GROW(olist.items, strmap_get_size(&opti->output), + olist.alloc); + + /* Put every entry from output into olist, then sort */ + strmap_for_each_entry(&opti->output, &iter, e) { + string_list_append(&olist, e->key)->util = e->value; + } + string_list_sort(&olist); + + /* Iterate over the items, printing them */ + for (i = 0; i < olist.nr; ++i) { + struct strbuf *sb = olist.items[i].util; + + printf("%s", sb->buf); + } + string_list_clear(&olist, 0); + } + merge_finalize(opt, result); } void merge_finalize(struct merge_options *opt, struct merge_result *result) { - die("Not yet implemented"); + struct merge_options_internal *opti = result->priv; + + assert(opt->priv == NULL); + + clear_or_reinit_internal_opts(opti, 0); + FREE_AND_NULL(opti); +} + +/*** Function Grouping: helper functions for merge_incore_*() ***/ + +static inline void set_commit_tree(struct commit *c, struct tree *t) +{ + c->maybe_tree = t; +} + +static struct commit *make_virtual_commit(struct repository *repo, + struct tree *tree, + const char *comment) +{ + struct commit *commit = alloc_commit_node(repo); + + set_merge_remote_desc(commit, comment, (struct object *)commit); + set_commit_tree(commit, tree); + commit->object.parsed = 1; + return commit; +} + +static void merge_start(struct merge_options *opt, struct merge_result *result) +{ + /* Sanity checks on opt */ + assert(opt->repo); + + assert(opt->branch1 && opt->branch2); + + assert(opt->detect_directory_renames >= MERGE_DIRECTORY_RENAMES_NONE && + opt->detect_directory_renames <= MERGE_DIRECTORY_RENAMES_TRUE); + assert(opt->rename_limit >= -1); + assert(opt->rename_score >= 0 && opt->rename_score <= MAX_SCORE); + assert(opt->show_rename_progress >= 0 && opt->show_rename_progress <= 1); + + assert(opt->xdl_opts >= 0); + assert(opt->recursive_variant >= MERGE_VARIANT_NORMAL && + opt->recursive_variant <= MERGE_VARIANT_THEIRS); + + /* + * detect_renames, verbosity, buffer_output, and obuf are ignored + * fields that were used by "recursive" rather than "ort" -- but + * sanity check them anyway. + */ + assert(opt->detect_renames >= -1 && + opt->detect_renames <= DIFF_DETECT_COPY); + assert(opt->verbosity >= 0 && opt->verbosity <= 5); + assert(opt->buffer_output <= 2); + assert(opt->obuf.len == 0); + + assert(opt->priv == NULL); + + /* Default to histogram diff. Actually, just hardcode it...for now. */ + opt->xdl_opts = DIFF_WITH_ALG(opt, HISTOGRAM_DIFF); + + /* Initialization of opt->priv, our internal merge data */ + opt->priv = xcalloc(1, sizeof(*opt->priv)); + + /* + * Although we initialize opt->priv->paths with strdup_strings=0, + * that's just to avoid making yet another copy of an allocated + * string. Putting the entry into paths means we are taking + * ownership, so we will later free it. paths_to_free is similar. + * + * In contrast, conflicted just has a subset of keys from paths, so + * we don't want to free those (it'd be a duplicate free). + */ + strmap_init_with_options(&opt->priv->paths, NULL, 0); + strmap_init_with_options(&opt->priv->conflicted, NULL, 0); + string_list_init(&opt->priv->paths_to_free, 0); + + /* + * keys & strbufs in output will sometimes need to outlive "paths", + * so it will have a copy of relevant keys. It's probably a small + * subset of the overall paths that have special output. + */ + strmap_init(&opt->priv->output); +} + +/*** Function Grouping: merge_incore_*() and their internal variants ***/ + +/* + * Originally from merge_trees_internal(); heavily adapted, though. + */ +static void merge_ort_nonrecursive_internal(struct merge_options *opt, + struct tree *merge_base, + struct tree *side1, + struct tree *side2, + struct merge_result *result) +{ + struct object_id working_tree_oid; + + if (collect_merge_info(opt, merge_base, side1, side2) != 0) { + /* + * TRANSLATORS: The %s arguments are: 1) tree hash of a merge + * base, and 2-3) the trees for the two trees we're merging. + */ + err(opt, _("collecting merge info failed for trees %s, %s, %s"), + oid_to_hex(&merge_base->object.oid), + oid_to_hex(&side1->object.oid), + oid_to_hex(&side2->object.oid)); + result->clean = -1; + return; + } + + result->clean = detect_and_process_renames(opt, merge_base, + side1, side2); + process_entries(opt, &working_tree_oid); + + /* Set return values */ + result->tree = parse_tree_indirect(&working_tree_oid); + /* existence of conflicted entries implies unclean */ + result->clean &= strmap_empty(&opt->priv->conflicted); + if (!opt->priv->call_depth) { + result->priv = opt->priv; + opt->priv = NULL; + } +} + +/* + * Originally from merge_recursive_internal(); somewhat adapted, though. + */ +static void merge_ort_internal(struct merge_options *opt, + struct commit_list *merge_bases, + struct commit *h1, + struct commit *h2, + struct merge_result *result) +{ + struct commit_list *iter; + struct commit *merged_merge_bases; + const char *ancestor_name; + struct strbuf merge_base_abbrev = STRBUF_INIT; + + if (!merge_bases) { + merge_bases = get_merge_bases(h1, h2); + /* See merge-ort.h:merge_incore_recursive() declaration NOTE */ + merge_bases = reverse_commit_list(merge_bases); + } + + merged_merge_bases = pop_commit(&merge_bases); + if (merged_merge_bases == NULL) { + /* if there is no common ancestor, use an empty tree */ + struct tree *tree; + + tree = lookup_tree(opt->repo, opt->repo->hash_algo->empty_tree); + merged_merge_bases = make_virtual_commit(opt->repo, tree, + "ancestor"); + ancestor_name = "empty tree"; + } else if (merge_bases) { + ancestor_name = "merged common ancestors"; + } else { + strbuf_add_unique_abbrev(&merge_base_abbrev, + &merged_merge_bases->object.oid, + DEFAULT_ABBREV); + ancestor_name = merge_base_abbrev.buf; + } + + for (iter = merge_bases; iter; iter = iter->next) { + const char *saved_b1, *saved_b2; + struct commit *prev = merged_merge_bases; + + opt->priv->call_depth++; + /* + * When the merge fails, the result contains files + * with conflict markers. The cleanness flag is + * ignored (unless indicating an error), it was never + * actually used, as result of merge_trees has always + * overwritten it: the committed "conflicts" were + * already resolved. + */ + saved_b1 = opt->branch1; + saved_b2 = opt->branch2; + opt->branch1 = "Temporary merge branch 1"; + opt->branch2 = "Temporary merge branch 2"; + merge_ort_internal(opt, NULL, prev, iter->item, result); + if (result->clean < 0) + return; + opt->branch1 = saved_b1; + opt->branch2 = saved_b2; + opt->priv->call_depth--; + + merged_merge_bases = make_virtual_commit(opt->repo, + result->tree, + "merged tree"); + commit_list_insert(prev, &merged_merge_bases->parents); + commit_list_insert(iter->item, + &merged_merge_bases->parents->next); + + clear_or_reinit_internal_opts(opt->priv, 1); + } + + opt->ancestor = ancestor_name; + merge_ort_nonrecursive_internal(opt, + repo_get_commit_tree(opt->repo, + merged_merge_bases), + repo_get_commit_tree(opt->repo, h1), + repo_get_commit_tree(opt->repo, h2), + result); + strbuf_release(&merge_base_abbrev); + opt->ancestor = NULL; /* avoid accidental re-use of opt->ancestor */ } void merge_incore_nonrecursive(struct merge_options *opt, @@ -39,7 +1554,9 @@ void merge_incore_nonrecursive(struct merge_options *opt, struct tree *side2, struct merge_result *result) { - die("Not yet implemented"); + assert(opt->ancestor != NULL); + merge_start(opt, result); + merge_ort_nonrecursive_internal(opt, merge_base, side1, side2, result); } void merge_incore_recursive(struct merge_options *opt, @@ -48,5 +1565,9 @@ void merge_incore_recursive(struct merge_options *opt, struct commit *side2, struct merge_result *result) { - die("Not yet implemented"); + /* We set the ancestor label based on the merge_bases */ + assert(opt->ancestor == NULL); + + merge_start(opt, result); + merge_ort_internal(opt, merge_bases, side1, side2, result); } diff --git a/merge-ort.h b/merge-ort.h index 74adccad16..d53a0a339f 100644 --- a/merge-ort.h +++ b/merge-ort.h @@ -7,7 +7,14 @@ struct commit; struct tree; struct merge_result { - /* Whether the merge is clean */ + /* + * Whether the merge is clean; possible values: + * 1: clean + * 0: not clean (merge conflicts) + * <0: operation aborted prematurely. (object database + * unreadable, disk full, etc.) Worktree may be left in an + * inconsistent state if operation failed near the end. + */ int clean; /* @@ -27,6 +34,16 @@ struct merge_result { /* * rename-detecting three-way merge with recursive ancestor consolidation. * working tree and index are untouched. + * + * merge_bases will be consumed (emptied) so make a copy if you need it. + * + * NOTE: empirically, the recursive algorithm will perform better if you + * pass the merge_bases in the order of oldest commit to the + * newest[1][2]. + * + * [1] https://lore.kernel.org/git/nycvar.QRO.7.76.6.1907252055500.21907@tvgsbejvaqbjf.bet/ + * [2] commit 8918b0c9c2 ("merge-recur: try to merge older merge bases + * first", 2006-08-09) */ void merge_incore_recursive(struct merge_options *opt, struct commit_list *merge_bases, diff --git a/merge-recursive.c b/merge-recursive.c index f736a0f632..b052974f19 100644 --- a/merge-recursive.c +++ b/merge-recursive.c @@ -3517,17 +3517,6 @@ static int merge_trees_internal(struct merge_options *opt, return clean; } -static struct commit_list *reverse_commit_list(struct commit_list *list) -{ - struct commit_list *next = NULL, *current, *backup; - for (current = list; current; current = backup) { - backup = current->next; - current->next = next; - next = current; - } - return next; -} - /* * Merge the commits h1 and h2, returning a flag (int) indicating the * cleanness of the merge. Also, if opt->priv->call_depth, create a @@ -5,7 +5,7 @@ #include "lockfile.h" #include "packfile.h" #include "object-store.h" -#include "sha1-lookup.h" +#include "hash-lookup.h" #include "midx.h" #include "progress.h" #include "trace2.h" diff --git a/sha1-file.c b/object-file.c index c3c49d2fa5..5bcfde8471 100644 --- a/sha1-file.c +++ b/object-file.c @@ -3,7 +3,7 @@ * * Copyright (C) Linus Torvalds, 2005 * - * This handles basic git sha1 object files - packing, unpacking, + * This handles basic git object files - packing, unpacking, * creation etc. */ #include "cache.h" @@ -20,7 +20,7 @@ #include "tree-walk.h" #include "refs.h" #include "pack-revindex.h" -#include "sha1-lookup.h" +#include "hash-lookup.h" #include "bulk-checkin.h" #include "repository.h" #include "replace-object.h" @@ -508,9 +508,9 @@ static int alt_odb_usable(struct raw_object_store *o, * LF separated. Its base points at a statically allocated buffer that * contains "/the/directory/corresponding/to/.git/objects/...", while * its name points just after the slash at the end of ".git/objects/" - * in the example above, and has enough space to hold 40-byte hex - * SHA1, an extra slash for the first level indirection, and the - * terminating NUL. + * in the example above, and has enough space to hold all hex characters + * of the object ID, an extra slash for the first level indirection, and + * the terminating NUL. */ static void read_info_alternates(struct repository *r, const char *relative_base, diff --git a/sha1-name.c b/object-name.c index 0b23b86ceb..64202de60b 100644 --- a/sha1-name.c +++ b/object-name.c @@ -85,7 +85,7 @@ static void update_candidates(struct disambiguate_state *ds, const struct object /* otherwise, current can be discarded and candidate is still good */ } -static int match_sha(unsigned, const unsigned char *, const unsigned char *); +static int match_hash(unsigned, const unsigned char *, const unsigned char *); static void find_short_object_filename(struct disambiguate_state *ds) { @@ -102,7 +102,7 @@ static void find_short_object_filename(struct disambiguate_state *ds) while (!ds->ambiguous && pos < loose_objects->nr) { const struct object_id *oid; oid = loose_objects->oid + pos; - if (!match_sha(ds->len, ds->bin_pfx.hash, oid->hash)) + if (!match_hash(ds->len, ds->bin_pfx.hash, oid->hash)) break; update_candidates(ds, oid); pos++; @@ -110,7 +110,7 @@ static void find_short_object_filename(struct disambiguate_state *ds) } } -static int match_sha(unsigned len, const unsigned char *a, const unsigned char *b) +static int match_hash(unsigned len, const unsigned char *a, const unsigned char *b) { do { if (*a != *b) @@ -145,7 +145,7 @@ static void unique_in_midx(struct multi_pack_index *m, for (i = first; i < num && !ds->ambiguous; i++) { struct object_id oid; current = nth_midxed_object_oid(&oid, m, i); - if (!match_sha(ds->len, ds->bin_pfx.hash, current->hash)) + if (!match_hash(ds->len, ds->bin_pfx.hash, current->hash)) break; update_candidates(ds, current); } @@ -173,7 +173,7 @@ static void unique_in_pack(struct packed_git *p, for (i = first; i < num && !ds->ambiguous; i++) { struct object_id oid; nth_packed_object_id(&oid, p, i); - if (!match_sha(ds->len, ds->bin_pfx.hash, oid.hash)) + if (!match_hash(ds->len, ds->bin_pfx.hash, oid.hash)) break; update_candidates(ds, &oid); } @@ -483,7 +483,7 @@ static enum get_oid_result get_short_oid(struct repository *r, if (!quietly && (status == SHORT_NAME_AMBIGUOUS)) { struct oid_array collect = OID_ARRAY_INIT; - error(_("short SHA1 %s is ambiguous"), ds.hex_pfx); + error(_("short object ID %s is ambiguous"), ds.hex_pfx); /* * We may still have ambiguity if we simply saw a series of @@ -1811,7 +1811,7 @@ static enum get_oid_result get_oid_with_context_1(struct repository *repo, if (!ret) return ret; /* - * sha1:path --> object name of path in ent sha1 + * tree:path --> object name of path in tree * :path -> object name of absolute path in index * :./path -> object name of path relative to cwd in index * :[0-3]:path -> object name of path in index at stage @@ -1949,6 +1949,6 @@ enum get_oid_result get_oid_with_context(struct repository *repo, struct object_context *oc) { if (flags & GET_OID_FOLLOW_SYMLINKS && flags & GET_OID_ONLY_TO_DIE) - BUG("incompatible flags for get_sha1_with_context"); + BUG("incompatible flags for get_oid_with_context"); return get_oid_with_context_1(repo, str, flags, NULL, oid, oc); } diff --git a/oid-array.c b/oid-array.c index 8e1bcedc0c..889b311f22 100644 --- a/oid-array.c +++ b/oid-array.c @@ -1,6 +1,6 @@ #include "cache.h" #include "oid-array.h" -#include "sha1-lookup.h" +#include "hash-lookup.h" void oid_array_append(struct oid_array *array, const struct object_id *oid) { @@ -31,7 +31,7 @@ static const unsigned char *sha1_access(size_t index, void *table) int oid_array_lookup(struct oid_array *array, const struct object_id *oid) { oid_array_sort(array); - return sha1_pos(oid->hash, array->oid, array->nr, sha1_access); + return hash_pos(oid->hash, array->oid, array->nr, sha1_access); } void oid_array_clear(struct oid_array *array) diff --git a/pack-bitmap-write.c b/pack-bitmap-write.c index 5e998bdaa7..92460a6126 100644 --- a/pack-bitmap-write.c +++ b/pack-bitmap-write.c @@ -9,9 +9,10 @@ #include "pack-revindex.h" #include "pack.h" #include "pack-bitmap.h" -#include "sha1-lookup.h" +#include "hash-lookup.h" #include "pack-objects.h" #include "commit-reach.h" +#include "prio-queue.h" struct bitmapped_commit { struct commit *commit; @@ -29,7 +30,6 @@ struct bitmap_writer { struct ewah_bitmap *tags; kh_oid_map_t *bitmaps; - kh_oid_map_t *reused; struct packing_data *to_pack; struct bitmapped_commit *selected; @@ -110,10 +110,8 @@ void bitmap_writer_build_type_index(struct packing_data *to_pack, /** * Compute the actual bitmaps */ -static struct object **seen_objects; -static unsigned int seen_objects_nr, seen_objects_alloc; -static inline void push_bitmapped_commit(struct commit *commit, struct ewah_bitmap *reused) +static inline void push_bitmapped_commit(struct commit *commit) { if (writer.selected_nr >= writer.selected_alloc) { writer.selected_alloc = (writer.selected_alloc + 32) * 2; @@ -121,27 +119,12 @@ static inline void push_bitmapped_commit(struct commit *commit, struct ewah_bitm } writer.selected[writer.selected_nr].commit = commit; - writer.selected[writer.selected_nr].bitmap = reused; + writer.selected[writer.selected_nr].bitmap = NULL; writer.selected[writer.selected_nr].flags = 0; writer.selected_nr++; } -static inline void mark_as_seen(struct object *object) -{ - ALLOC_GROW(seen_objects, seen_objects_nr + 1, seen_objects_alloc); - seen_objects[seen_objects_nr++] = object; -} - -static inline void reset_all_seen(void) -{ - unsigned int i; - for (i = 0; i < seen_objects_nr; ++i) { - seen_objects[i]->flags &= ~(SEEN | ADDED | SHOWN); - } - seen_objects_nr = 0; -} - static uint32_t find_object_pos(const struct object_id *oid) { struct object_entry *entry = packlist_find(writer.to_pack, oid); @@ -154,60 +137,6 @@ static uint32_t find_object_pos(const struct object_id *oid) return oe_in_pack_pos(writer.to_pack, entry); } -static void show_object(struct object *object, const char *name, void *data) -{ - struct bitmap *base = data; - bitmap_set(base, find_object_pos(&object->oid)); - mark_as_seen(object); -} - -static void show_commit(struct commit *commit, void *data) -{ - mark_as_seen((struct object *)commit); -} - -static int -add_to_include_set(struct bitmap *base, struct commit *commit) -{ - khiter_t hash_pos; - uint32_t bitmap_pos = find_object_pos(&commit->object.oid); - - if (bitmap_get(base, bitmap_pos)) - return 0; - - hash_pos = kh_get_oid_map(writer.bitmaps, commit->object.oid); - if (hash_pos < kh_end(writer.bitmaps)) { - struct bitmapped_commit *bc = kh_value(writer.bitmaps, hash_pos); - bitmap_or_ewah(base, bc->bitmap); - return 0; - } - - bitmap_set(base, bitmap_pos); - return 1; -} - -static int -should_include(struct commit *commit, void *_data) -{ - struct bitmap *base = _data; - - if (!add_to_include_set(base, commit)) { - struct commit_list *parent = commit->parents; - - mark_as_seen((struct object *)commit); - - while (parent) { - parent->item->object.flags |= SEEN; - mark_as_seen((struct object *)parent->item); - parent = parent->next; - } - - return 0; - } - - return 1; -} - static void compute_xor_offsets(void) { static const int MAX_XOR_OFFSET_SEARCH = 10; @@ -248,79 +177,326 @@ static void compute_xor_offsets(void) } } -void bitmap_writer_build(struct packing_data *to_pack) -{ - static const double REUSE_BITMAP_THRESHOLD = 0.2; +struct bb_commit { + struct commit_list *reverse_edges; + struct bitmap *commit_mask; + struct bitmap *bitmap; + unsigned selected:1, + maximal:1; + unsigned idx; /* within selected array */ +}; - int i, reuse_after, need_reset; - struct bitmap *base = bitmap_new(); - struct rev_info revs; +define_commit_slab(bb_data, struct bb_commit); - writer.bitmaps = kh_init_oid_map(); - writer.to_pack = to_pack; +struct bitmap_builder { + struct bb_data data; + struct commit **commits; + size_t commits_nr, commits_alloc; +}; - if (writer.show_progress) - writer.progress = start_progress("Building bitmaps", writer.selected_nr); +static void bitmap_builder_init(struct bitmap_builder *bb, + struct bitmap_writer *writer, + struct bitmap_index *old_bitmap) +{ + struct rev_info revs; + struct commit *commit; + struct commit_list *reusable = NULL; + struct commit_list *r; + unsigned int i, num_maximal = 0; - repo_init_revisions(to_pack->repo, &revs, NULL); - revs.tag_objects = 1; - revs.tree_objects = 1; - revs.blob_objects = 1; - revs.no_walk = 0; + memset(bb, 0, sizeof(*bb)); + init_bb_data(&bb->data); - revs.include_check = should_include; reset_revision_walk(); + repo_init_revisions(writer->to_pack->repo, &revs, NULL); + revs.topo_order = 1; + revs.first_parent_only = 1; + + for (i = 0; i < writer->selected_nr; i++) { + struct commit *c = writer->selected[i].commit; + struct bb_commit *ent = bb_data_at(&bb->data, c); + + ent->selected = 1; + ent->maximal = 1; + ent->idx = i; + + ent->commit_mask = bitmap_new(); + bitmap_set(ent->commit_mask, i); + + add_pending_object(&revs, &c->object, ""); + } + + if (prepare_revision_walk(&revs)) + die("revision walk setup failed"); + + while ((commit = get_revision(&revs))) { + struct commit_list *p = commit->parents; + struct bb_commit *c_ent; + + parse_commit_or_die(commit); + + c_ent = bb_data_at(&bb->data, commit); + + /* + * If there is no commit_mask, there is no reason to iterate + * over this commit; it is not selected (if it were, it would + * not have a blank commit mask) and all its children have + * existing bitmaps (see the comment starting with "This commit + * has an existing bitmap" below), so it does not contribute + * anything to the final bitmap file or its descendants. + */ + if (!c_ent->commit_mask) + continue; + + if (old_bitmap && bitmap_for_commit(old_bitmap, commit)) { + /* + * This commit has an existing bitmap, so we can + * get its bits immediately without an object + * walk. That is, it is reusable as-is and there is no + * need to continue walking beyond it. + * + * Mark it as such and add it to bb->commits separately + * to avoid allocating a position in the commit mask. + */ + commit_list_insert(commit, &reusable); + goto next; + } + + if (c_ent->maximal) { + num_maximal++; + ALLOC_GROW(bb->commits, bb->commits_nr + 1, bb->commits_alloc); + bb->commits[bb->commits_nr++] = commit; + } + + if (p) { + struct bb_commit *p_ent = bb_data_at(&bb->data, p->item); + int c_not_p, p_not_c; + + if (!p_ent->commit_mask) { + p_ent->commit_mask = bitmap_new(); + c_not_p = 1; + p_not_c = 0; + } else { + c_not_p = bitmap_is_subset(c_ent->commit_mask, p_ent->commit_mask); + p_not_c = bitmap_is_subset(p_ent->commit_mask, c_ent->commit_mask); + } + + if (!c_not_p) + continue; + + bitmap_or(p_ent->commit_mask, c_ent->commit_mask); + + if (p_not_c) + p_ent->maximal = 1; + else { + p_ent->maximal = 0; + free_commit_list(p_ent->reverse_edges); + p_ent->reverse_edges = NULL; + } - reuse_after = writer.selected_nr * REUSE_BITMAP_THRESHOLD; - need_reset = 0; + if (c_ent->maximal) { + commit_list_insert(commit, &p_ent->reverse_edges); + } else { + struct commit_list *cc = c_ent->reverse_edges; + + for (; cc; cc = cc->next) { + if (!commit_list_contains(cc->item, p_ent->reverse_edges)) + commit_list_insert(cc->item, &p_ent->reverse_edges); + } + } + } + +next: + bitmap_free(c_ent->commit_mask); + c_ent->commit_mask = NULL; + } + + for (r = reusable; r; r = r->next) { + ALLOC_GROW(bb->commits, bb->commits_nr + 1, bb->commits_alloc); + bb->commits[bb->commits_nr++] = r->item; + } + + trace2_data_intmax("pack-bitmap-write", the_repository, + "num_selected_commits", writer->selected_nr); + trace2_data_intmax("pack-bitmap-write", the_repository, + "num_maximal_commits", num_maximal); + + free_commit_list(reusable); +} + +static void bitmap_builder_clear(struct bitmap_builder *bb) +{ + clear_bb_data(&bb->data); + free(bb->commits); + bb->commits_nr = bb->commits_alloc = 0; +} + +static void fill_bitmap_tree(struct bitmap *bitmap, + struct tree *tree) +{ + uint32_t pos; + struct tree_desc desc; + struct name_entry entry; + + /* + * If our bit is already set, then there is nothing to do. Both this + * tree and all of its children will be set. + */ + pos = find_object_pos(&tree->object.oid); + if (bitmap_get(bitmap, pos)) + return; + bitmap_set(bitmap, pos); - for (i = writer.selected_nr - 1; i >= 0; --i) { - struct bitmapped_commit *stored; - struct object *object; + if (parse_tree(tree) < 0) + die("unable to load tree object %s", + oid_to_hex(&tree->object.oid)); + init_tree_desc(&desc, tree->buffer, tree->size); - khiter_t hash_pos; - int hash_ret; + while (tree_entry(&desc, &entry)) { + switch (object_type(entry.mode)) { + case OBJ_TREE: + fill_bitmap_tree(bitmap, + lookup_tree(the_repository, &entry.oid)); + break; + case OBJ_BLOB: + bitmap_set(bitmap, find_object_pos(&entry.oid)); + break; + default: + /* Gitlink, etc; not reachable */ + break; + } + } - stored = &writer.selected[i]; - object = (struct object *)stored->commit; + free_tree_buffer(tree); +} - if (stored->bitmap == NULL) { - if (i < writer.selected_nr - 1 && - (need_reset || - !in_merge_bases(writer.selected[i + 1].commit, - stored->commit))) { - bitmap_reset(base); - reset_all_seen(); +static void fill_bitmap_commit(struct bb_commit *ent, + struct commit *commit, + struct prio_queue *queue, + struct prio_queue *tree_queue, + struct bitmap_index *old_bitmap, + const uint32_t *mapping) +{ + if (!ent->bitmap) + ent->bitmap = bitmap_new(); + + prio_queue_put(queue, commit); + + while (queue->nr) { + struct commit_list *p; + struct commit *c = prio_queue_get(queue); + + if (old_bitmap && mapping) { + struct ewah_bitmap *old = bitmap_for_commit(old_bitmap, c); + /* + * If this commit has an old bitmap, then translate that + * bitmap and add its bits to this one. No need to walk + * parents or the tree for this commit. + */ + if (old && !rebuild_bitmap(mapping, old, ent->bitmap)) + continue; + } + + /* + * Mark ourselves and queue our tree. The commit + * walk ensures we cover all parents. + */ + bitmap_set(ent->bitmap, find_object_pos(&c->object.oid)); + prio_queue_put(tree_queue, get_commit_tree(c)); + + for (p = c->parents; p; p = p->next) { + int pos = find_object_pos(&p->item->object.oid); + if (!bitmap_get(ent->bitmap, pos)) { + bitmap_set(ent->bitmap, pos); + prio_queue_put(queue, p->item); } + } + } - add_pending_object(&revs, object, ""); - revs.include_check_data = base; + while (tree_queue->nr) + fill_bitmap_tree(ent->bitmap, prio_queue_get(tree_queue)); +} - if (prepare_revision_walk(&revs)) - die("revision walk setup failed"); +static void store_selected(struct bb_commit *ent, struct commit *commit) +{ + struct bitmapped_commit *stored = &writer.selected[ent->idx]; + khiter_t hash_pos; + int hash_ret; - traverse_commit_list(&revs, show_commit, show_object, base); + stored->bitmap = bitmap_to_ewah(ent->bitmap); - object_array_clear(&revs.pending); + hash_pos = kh_put_oid_map(writer.bitmaps, commit->object.oid, &hash_ret); + if (hash_ret == 0) + die("Duplicate entry when writing index: %s", + oid_to_hex(&commit->object.oid)); + kh_value(writer.bitmaps, hash_pos) = stored; +} - stored->bitmap = bitmap_to_ewah(base); - need_reset = 0; - } else - need_reset = 1; +void bitmap_writer_build(struct packing_data *to_pack) +{ + struct bitmap_builder bb; + size_t i; + int nr_stored = 0; /* for progress */ + struct prio_queue queue = { compare_commits_by_gen_then_commit_date }; + struct prio_queue tree_queue = { NULL }; + struct bitmap_index *old_bitmap; + uint32_t *mapping; - if (i >= reuse_after) - stored->flags |= BITMAP_FLAG_REUSE; + writer.bitmaps = kh_init_oid_map(); + writer.to_pack = to_pack; - hash_pos = kh_put_oid_map(writer.bitmaps, object->oid, &hash_ret); - if (hash_ret == 0) - die("Duplicate entry when writing index: %s", - oid_to_hex(&object->oid)); + if (writer.show_progress) + writer.progress = start_progress("Building bitmaps", writer.selected_nr); + trace2_region_enter("pack-bitmap-write", "building_bitmaps_total", + the_repository); + + old_bitmap = prepare_bitmap_git(to_pack->repo); + if (old_bitmap) + mapping = create_bitmap_mapping(old_bitmap, to_pack); + else + mapping = NULL; + + bitmap_builder_init(&bb, &writer, old_bitmap); + for (i = bb.commits_nr; i > 0; i--) { + struct commit *commit = bb.commits[i-1]; + struct bb_commit *ent = bb_data_at(&bb.data, commit); + struct commit *child; + int reused = 0; + + fill_bitmap_commit(ent, commit, &queue, &tree_queue, + old_bitmap, mapping); + + if (ent->selected) { + store_selected(ent, commit); + nr_stored++; + display_progress(writer.progress, nr_stored); + } - kh_value(writer.bitmaps, hash_pos) = stored; - display_progress(writer.progress, writer.selected_nr - i); + while ((child = pop_commit(&ent->reverse_edges))) { + struct bb_commit *child_ent = + bb_data_at(&bb.data, child); + + if (child_ent->bitmap) + bitmap_or(child_ent->bitmap, ent->bitmap); + else if (reused) + child_ent->bitmap = bitmap_dup(ent->bitmap); + else { + child_ent->bitmap = ent->bitmap; + reused = 1; + } + } + if (!reused) + bitmap_free(ent->bitmap); + ent->bitmap = NULL; } + clear_prio_queue(&queue); + clear_prio_queue(&tree_queue); + bitmap_builder_clear(&bb); + free(mapping); + + trace2_region_leave("pack-bitmap-write", "building_bitmaps_total", + the_repository); - bitmap_free(base); stop_progress(&writer.progress); compute_xor_offsets(); @@ -360,35 +536,6 @@ static int date_compare(const void *_a, const void *_b) return (long)b->date - (long)a->date; } -void bitmap_writer_reuse_bitmaps(struct packing_data *to_pack) -{ - struct bitmap_index *bitmap_git; - if (!(bitmap_git = prepare_bitmap_git(to_pack->repo))) - return; - - writer.reused = kh_init_oid_map(); - rebuild_existing_bitmaps(bitmap_git, to_pack, writer.reused, - writer.show_progress); - /* - * NEEDSWORK: rebuild_existing_bitmaps() makes writer.reused reference - * some bitmaps in bitmap_git, so we can't free the latter. - */ -} - -static struct ewah_bitmap *find_reused_bitmap(const struct object_id *oid) -{ - khiter_t hash_pos; - - if (!writer.reused) - return NULL; - - hash_pos = kh_get_oid_map(writer.reused, *oid); - if (hash_pos >= kh_end(writer.reused)) - return NULL; - - return kh_value(writer.reused, hash_pos); -} - void bitmap_writer_select_commits(struct commit **indexed_commits, unsigned int indexed_commits_nr, int max_bitmaps) @@ -402,12 +549,11 @@ void bitmap_writer_select_commits(struct commit **indexed_commits, if (indexed_commits_nr < 100) { for (i = 0; i < indexed_commits_nr; ++i) - push_bitmapped_commit(indexed_commits[i], NULL); + push_bitmapped_commit(indexed_commits[i]); return; } for (;;) { - struct ewah_bitmap *reused_bitmap = NULL; struct commit *chosen = NULL; next = next_commit_index(i); @@ -422,15 +568,13 @@ void bitmap_writer_select_commits(struct commit **indexed_commits, if (next == 0) { chosen = indexed_commits[i]; - reused_bitmap = find_reused_bitmap(&chosen->object.oid); } else { chosen = indexed_commits[i + next]; for (j = 0; j <= next; ++j) { struct commit *cm = indexed_commits[i + j]; - reused_bitmap = find_reused_bitmap(&cm->object.oid); - if (reused_bitmap || (cm->object.flags & NEEDS_BITMAP) != 0) { + if ((cm->object.flags & NEEDS_BITMAP) != 0) { chosen = cm; break; } @@ -440,7 +584,7 @@ void bitmap_writer_select_commits(struct commit **indexed_commits, } } - push_bitmapped_commit(chosen, reused_bitmap); + push_bitmapped_commit(chosen); i += next + 1; display_progress(writer.progress, i); @@ -482,7 +626,7 @@ static void write_selected_commits_v1(struct hashfile *f, struct bitmapped_commit *stored = &writer.selected[i]; int commit_pos = - sha1_pos(stored->commit->object.oid.hash, index, index_nr, sha1_access); + hash_pos(stored->commit->object.oid.hash, index, index_nr, sha1_access); if (commit_pos < 0) BUG("trying to write commit not in index"); diff --git a/pack-bitmap.c b/pack-bitmap.c index 4077e731e8..d88745fb02 100644 --- a/pack-bitmap.c +++ b/pack-bitmap.c @@ -138,9 +138,10 @@ static struct ewah_bitmap *read_bitmap_1(struct bitmap_index *index) static int load_bitmap_header(struct bitmap_index *index) { struct bitmap_disk_header *header = (void *)index->map; + size_t header_size = sizeof(*header) - GIT_MAX_RAWSZ + the_hash_algo->rawsz; - if (index->map_size < sizeof(*header) + the_hash_algo->rawsz) - return error("Corrupted bitmap index (missing header data)"); + if (index->map_size < header_size + the_hash_algo->rawsz) + return error("Corrupted bitmap index (too small)"); if (memcmp(header->magic, BITMAP_IDX_SIGNATURE, sizeof(BITMAP_IDX_SIGNATURE)) != 0) return error("Corrupted bitmap index file (wrong header)"); @@ -152,19 +153,23 @@ static int load_bitmap_header(struct bitmap_index *index) /* Parse known bitmap format options */ { uint32_t flags = ntohs(header->options); + size_t cache_size = st_mult(index->pack->num_objects, sizeof(uint32_t)); + unsigned char *index_end = index->map + index->map_size - the_hash_algo->rawsz; if ((flags & BITMAP_OPT_FULL_DAG) == 0) return error("Unsupported options for bitmap index file " "(Git requires BITMAP_OPT_FULL_DAG)"); if (flags & BITMAP_OPT_HASH_CACHE) { - unsigned char *end = index->map + index->map_size - the_hash_algo->rawsz; - index->hashes = ((uint32_t *)end) - index->pack->num_objects; + if (cache_size > index_end - index->map - header_size) + return error("corrupted bitmap index file (too short to fit hash cache)"); + index->hashes = (void *)(index_end - cache_size); + index_end -= cache_size; } } index->entry_count = ntohl(header->entry_count); - index->map_pos += sizeof(*header) - GIT_MAX_RAWSZ + the_hash_algo->rawsz; + index->map_pos += header_size; return 0; } @@ -224,11 +229,16 @@ static int load_bitmap_entries_v1(struct bitmap_index *index) uint32_t commit_idx_pos; struct object_id oid; + if (index->map_size - index->map_pos < 6) + return error("corrupt ewah bitmap: truncated header for entry %d", i); + commit_idx_pos = read_be32(index->map, &index->map_pos); xor_offset = read_u8(index->map, &index->map_pos); flags = read_u8(index->map, &index->map_pos); - nth_packed_object_id(&oid, index->pack, commit_idx_pos); + if (nth_packed_object_id(&oid, index->pack, commit_idx_pos) < 0) + return error("corrupt ewah bitmap: commit index %u out of range", + (unsigned)commit_idx_pos); bitmap = read_bitmap_1(index); if (!bitmap) @@ -370,6 +380,16 @@ struct include_data { struct bitmap *seen; }; +struct ewah_bitmap *bitmap_for_commit(struct bitmap_index *bitmap_git, + struct commit *commit) +{ + khiter_t hash_pos = kh_get_oid_map(bitmap_git->bitmaps, + commit->object.oid); + if (hash_pos >= kh_end(bitmap_git->bitmaps)) + return NULL; + return lookup_stored_bitmap(kh_value(bitmap_git->bitmaps, hash_pos)); +} + static inline int bitmap_position_extended(struct bitmap_index *bitmap_git, const struct object_id *oid) { @@ -455,10 +475,10 @@ static void show_commit(struct commit *commit, void *data) static int add_to_include_set(struct bitmap_index *bitmap_git, struct include_data *data, - const struct object_id *oid, + struct commit *commit, int bitmap_pos) { - khiter_t hash_pos; + struct ewah_bitmap *partial; if (data->seen && bitmap_get(data->seen, bitmap_pos)) return 0; @@ -466,10 +486,9 @@ static int add_to_include_set(struct bitmap_index *bitmap_git, if (bitmap_get(data->base, bitmap_pos)) return 0; - hash_pos = kh_get_oid_map(bitmap_git->bitmaps, *oid); - if (hash_pos < kh_end(bitmap_git->bitmaps)) { - struct stored_bitmap *st = kh_value(bitmap_git->bitmaps, hash_pos); - bitmap_or_ewah(data->base, lookup_stored_bitmap(st)); + partial = bitmap_for_commit(bitmap_git, commit); + if (partial) { + bitmap_or_ewah(data->base, partial); return 0; } @@ -488,8 +507,7 @@ static int should_include(struct commit *commit, void *_data) (struct object *)commit, NULL); - if (!add_to_include_set(data->bitmap_git, data, &commit->object.oid, - bitmap_pos)) { + if (!add_to_include_set(data->bitmap_git, data, commit, bitmap_pos)) { struct commit_list *parent = commit->parents; while (parent) { @@ -503,6 +521,23 @@ static int should_include(struct commit *commit, void *_data) return 1; } +static int add_commit_to_bitmap(struct bitmap_index *bitmap_git, + struct bitmap **base, + struct commit *commit) +{ + struct ewah_bitmap *or_with = bitmap_for_commit(bitmap_git, commit); + + if (!or_with) + return 0; + + if (*base == NULL) + *base = ewah_to_bitmap(or_with); + else + bitmap_or_ewah(*base, or_with); + + return 1; +} + static struct bitmap *find_objects(struct bitmap_index *bitmap_git, struct rev_info *revs, struct object_list *roots, @@ -526,21 +561,10 @@ static struct bitmap *find_objects(struct bitmap_index *bitmap_git, struct object *object = roots->item; roots = roots->next; - if (object->type == OBJ_COMMIT) { - khiter_t pos = kh_get_oid_map(bitmap_git->bitmaps, object->oid); - - if (pos < kh_end(bitmap_git->bitmaps)) { - struct stored_bitmap *st = kh_value(bitmap_git->bitmaps, pos); - struct ewah_bitmap *or_with = lookup_stored_bitmap(st); - - if (base == NULL) - base = ewah_to_bitmap(or_with); - else - bitmap_or_ewah(base, or_with); - - object->flags |= SEEN; - continue; - } + if (object->type == OBJ_COMMIT && + add_commit_to_bitmap(bitmap_git, &base, (struct commit *)object)) { + object->flags |= SEEN; + continue; } object_list_insert(object, ¬_mapped); @@ -1272,10 +1296,10 @@ void test_bitmap_walk(struct rev_info *revs) { struct object *root; struct bitmap *result = NULL; - khiter_t pos; size_t result_popcnt; struct bitmap_test_data tdata; struct bitmap_index *bitmap_git; + struct ewah_bitmap *bm; if (!(bitmap_git = prepare_bitmap_git(revs->repo))) die("failed to load bitmap indexes"); @@ -1287,12 +1311,9 @@ void test_bitmap_walk(struct rev_info *revs) bitmap_git->version, bitmap_git->entry_count); root = revs->pending.objects[0].item; - pos = kh_get_oid_map(bitmap_git->bitmaps, root->oid); - - if (pos < kh_end(bitmap_git->bitmaps)) { - struct stored_bitmap *st = kh_value(bitmap_git->bitmaps, pos); - struct ewah_bitmap *bm = lookup_stored_bitmap(st); + bm = bitmap_for_commit(bitmap_git, (struct commit *)root); + if (bm) { fprintf(stderr, "Found bitmap for %s. %d bits / %08x checksum\n", oid_to_hex(&root->oid), (int)bm->bit_size, ewah_checksum(bm)); @@ -1323,14 +1344,14 @@ void test_bitmap_walk(struct rev_info *revs) if (bitmap_equals(result, tdata.base)) fprintf(stderr, "OK!\n"); else - fprintf(stderr, "Mismatch!\n"); + die("mismatch in bitmap results"); free_bitmap_index(bitmap_git); } -static int rebuild_bitmap(uint32_t *reposition, - struct ewah_bitmap *source, - struct bitmap *dest) +int rebuild_bitmap(const uint32_t *reposition, + struct ewah_bitmap *source, + struct bitmap *dest) { uint32_t pos = 0; struct ewah_iterator it; @@ -1359,19 +1380,11 @@ static int rebuild_bitmap(uint32_t *reposition, return 0; } -int rebuild_existing_bitmaps(struct bitmap_index *bitmap_git, - struct packing_data *mapping, - kh_oid_map_t *reused_bitmaps, - int show_progress) +uint32_t *create_bitmap_mapping(struct bitmap_index *bitmap_git, + struct packing_data *mapping) { uint32_t i, num_objects; uint32_t *reposition; - struct bitmap *rebuild; - struct stored_bitmap *stored; - struct progress *progress = NULL; - - khiter_t hash_pos; - int hash_ret; num_objects = bitmap_git->pack->num_objects; reposition = xcalloc(num_objects, sizeof(uint32_t)); @@ -1389,33 +1402,7 @@ int rebuild_existing_bitmaps(struct bitmap_index *bitmap_git, reposition[i] = oe_in_pack_pos(mapping, oe) + 1; } - rebuild = bitmap_new(); - i = 0; - - if (show_progress) - progress = start_progress("Reusing bitmaps", 0); - - kh_foreach_value(bitmap_git->bitmaps, stored, { - if (stored->flags & BITMAP_FLAG_REUSE) { - if (!rebuild_bitmap(reposition, - lookup_stored_bitmap(stored), - rebuild)) { - hash_pos = kh_put_oid_map(reused_bitmaps, - stored->oid, - &hash_ret); - kh_value(reused_bitmaps, hash_pos) = - bitmap_to_ewah(rebuild); - } - bitmap_reset(rebuild); - display_progress(progress, ++i); - } - }); - - stop_progress(&progress); - - free(reposition); - bitmap_free(rebuild); - return 0; + return reposition; } void free_bitmap_index(struct bitmap_index *b) diff --git a/pack-bitmap.h b/pack-bitmap.h index 1203120c43..25dfcf5615 100644 --- a/pack-bitmap.h +++ b/pack-bitmap.h @@ -73,7 +73,13 @@ void bitmap_writer_set_checksum(unsigned char *sha1); void bitmap_writer_build_type_index(struct packing_data *to_pack, struct pack_idx_entry **index, uint32_t index_nr); -void bitmap_writer_reuse_bitmaps(struct packing_data *to_pack); +uint32_t *create_bitmap_mapping(struct bitmap_index *bitmap_git, + struct packing_data *mapping); +int rebuild_bitmap(const uint32_t *reposition, + struct ewah_bitmap *source, + struct bitmap *dest); +struct ewah_bitmap *bitmap_for_commit(struct bitmap_index *bitmap_git, + struct commit *commit); void bitmap_writer_select_commits(struct commit **indexed_commits, unsigned int indexed_commits_nr, int max_bitmaps); void bitmap_writer_build(struct packing_data *to_pack); diff --git a/packfile.c b/packfile.c index 86f5c8dbf6..62d92e0c7c 100644 --- a/packfile.c +++ b/packfile.c @@ -7,7 +7,7 @@ #include "packfile.h" #include "delta.h" #include "streaming.h" -#include "sha1-lookup.h" +#include "hash-lookup.h" #include "commit.h" #include "object.h" #include "tag.h" diff --git a/parse-options.h b/parse-options.h index 7030d8f3da..ff6506a504 100644 --- a/parse-options.h +++ b/parse-options.h @@ -82,9 +82,9 @@ typedef enum parse_opt_result parse_opt_ll_cb(struct parse_opt_ctx_t *ctx, * stores pointers to the values to be filled. * * `argh`:: - * token to explain the kind of argument this option wants. Keep it - * homogeneous across the repository. Should be wrapped by N_() for - * translation. + * token to explain the kind of argument this option wants. Does not + * begin in capital letter, and does not end with a full stop. + * Should be wrapped by N_() for translation. * * `help`:: * the short help associated to what the option does. diff --git a/patch-ids.c b/patch-ids.c index 21973e4933..cf5e8045b7 100644 --- a/patch-ids.c +++ b/patch-ids.c @@ -1,7 +1,7 @@ #include "cache.h" #include "diff.h" #include "commit.h" -#include "sha1-lookup.h" +#include "hash-lookup.h" #include "patch-ids.h" static int patch_id_defined(struct commit *commit) @@ -1418,6 +1418,7 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */ struct process_trailer_options opts = PROCESS_TRAILER_OPTIONS_INIT; struct string_list filter_list = STRING_LIST_INIT_NODUP; struct strbuf sepbuf = STRBUF_INIT; + struct strbuf kvsepbuf = STRBUF_INIT; size_t ret = 0; opts.no_divider = 1; @@ -1449,8 +1450,17 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */ strbuf_expand(&sepbuf, fmt, strbuf_expand_literal_cb, NULL); free(fmt); opts.separator = &sepbuf; + } else if (match_placeholder_arg_value(arg, "key_value_separator", &arg, &argval, &arglen)) { + char *fmt; + + strbuf_reset(&kvsepbuf); + fmt = xstrndup(argval, arglen); + strbuf_expand(&kvsepbuf, fmt, strbuf_expand_literal_cb, NULL); + free(fmt); + opts.key_value_separator = &kvsepbuf; } else if (!match_placeholder_bool_arg(arg, "only", &arg, &opts.only_trailers) && !match_placeholder_bool_arg(arg, "unfold", &arg, &opts.unfold) && + !match_placeholder_bool_arg(arg, "keyonly", &arg, &opts.key_only) && !match_placeholder_bool_arg(arg, "valueonly", &arg, &opts.value_only)) break; } @@ -10,7 +10,7 @@ #include "attr.h" #include "pathspec.h" #include "object-store.h" -#include "sha1-lookup.h" +#include "hash-lookup.h" #define RESOLVED 0 #define PUNTED 1 @@ -147,7 +147,7 @@ static struct rerere_dir *find_rerere_dir(const char *hex) if (get_sha1_hex(hex, hash)) return NULL; /* BUG */ - pos = sha1_pos(hash, rerere_dir, rerere_dir_nr, rerere_dir_hash); + pos = hash_pos(hash, rerere_dir, rerere_dir_nr, rerere_dir_hash); if (pos < 0) { rr_dir = xmalloc(sizeof(*rr_dir)); hashcpy(rr_dir->hash, hash); diff --git a/revision.c b/revision.c index 9dff845bed..1bb590ece7 100644 --- a/revision.c +++ b/revision.c @@ -3308,6 +3308,26 @@ struct topo_walk_info { struct author_date_slab author_date; }; +static int topo_walk_atexit_registered; +static unsigned int count_explore_walked; +static unsigned int count_indegree_walked; +static unsigned int count_topo_walked; + +static void trace2_topo_walk_statistics_atexit(void) +{ + struct json_writer jw = JSON_WRITER_INIT; + + jw_object_begin(&jw, 0); + jw_object_intmax(&jw, "count_explore_walked", count_explore_walked); + jw_object_intmax(&jw, "count_indegree_walked", count_indegree_walked); + jw_object_intmax(&jw, "count_topo_walked", count_topo_walked); + jw_end(&jw); + + trace2_data_json("topo_walk", the_repository, "statistics", &jw); + + jw_release(&jw); +} + static inline void test_flag_and_insert(struct prio_queue *q, struct commit *c, int flag) { if (c->object.flags & flag) @@ -3329,6 +3349,8 @@ static void explore_walk_step(struct rev_info *revs) if (repo_parse_commit_gently(revs->repo, c, 1) < 0) return; + count_explore_walked++; + if (revs->sort_order == REV_SORT_BY_AUTHOR_DATE) record_author_date(&info->author_date, c); @@ -3367,6 +3389,8 @@ static void indegree_walk_step(struct rev_info *revs) if (repo_parse_commit_gently(revs->repo, c, 1) < 0) return; + count_indegree_walked++; + explore_to_depth(revs, commit_graph_generation(c)); for (p = c->parents; p; p = p->next) { @@ -3476,6 +3500,11 @@ static void init_topo_walk(struct rev_info *revs) */ if (revs->sort_order == REV_SORT_IN_GRAPH_ORDER) prio_queue_reverse(&info->topo_queue); + + if (trace2_is_enabled() && !topo_walk_atexit_registered) { + atexit(trace2_topo_walk_statistics_atexit); + topo_walk_atexit_registered = 1; + } } static struct commit *next_topo_commit(struct rev_info *revs) @@ -3502,6 +3531,8 @@ static void expand_topo_walk(struct rev_info *revs, struct commit *commit) oid_to_hex(&commit->object.oid)); } + count_topo_walked++; + for (p = commit->parents; p; p = p->next) { struct commit *parent = p->item; int *pi; diff --git a/submodule.c b/submodule.c index b3bb59f066..b561445329 100644 --- a/submodule.c +++ b/submodule.c @@ -1477,6 +1477,7 @@ static int get_next_submodule(struct child_process *cp, strbuf_release(&submodule_prefix); return 1; } else { + struct strbuf empty_submodule_path = STRBUF_INIT; fetch_task_release(task); free(task); @@ -1485,13 +1486,17 @@ static int get_next_submodule(struct child_process *cp, * An empty directory is normal, * the submodule is not initialized */ + strbuf_addf(&empty_submodule_path, "%s/%s/", + spf->r->worktree, + ce->name); if (S_ISGITLINK(ce->ce_mode) && - !is_empty_dir(ce->name)) { + !is_empty_dir(empty_submodule_path.buf)) { spf->result = 1; strbuf_addf(err, _("Could not access submodule '%s'\n"), ce->name); } + strbuf_release(&empty_submodule_path); } } diff --git a/t/lib-submodule-update.sh b/t/lib-submodule-update.sh index bd3fa3c6da..4b714e9308 100644 --- a/t/lib-submodule-update.sh +++ b/t/lib-submodule-update.sh @@ -316,14 +316,7 @@ test_submodule_switch_common () { command="$1" ######################### Appearing submodule ######################### # Switching to a commit letting a submodule appear creates empty dir ... - if test "$KNOWN_FAILURE_STASH_DOES_IGNORE_SUBMODULE_CHANGES" = 1 - then - # Restoring stash fails to restore submodule index entry - RESULT="failure" - else - RESULT="success" - fi - test_expect_$RESULT "$command: added submodule creates empty directory" ' + test_expect_success "$command: added submodule creates empty directory" ' prolog && reset_work_tree_to no_submodule && ( @@ -337,6 +330,13 @@ test_submodule_switch_common () { ) ' # ... and doesn't care if it already exists. + if test "$KNOWN_FAILURE_STASH_DOES_IGNORE_SUBMODULE_CHANGES" = 1 + then + # Restoring stash fails to restore submodule index entry + RESULT="failure" + else + RESULT="success" + fi test_expect_$RESULT "$command: added submodule leaves existing empty directory alone" ' prolog && reset_work_tree_to no_submodule && diff --git a/t/oid-info/README b/t/oid-info/README index 27f843fc00..ca56a74b1e 100644 --- a/t/oid-info/README +++ b/t/oid-info/README @@ -5,7 +5,7 @@ starting with `#` are ignored. The key and value are separated by whitespace (specifically, those whitespace in the default `$IFS`). The key consists only of shell identifier characters, and the value consists of a hash algorithm, colon, and value. The hash algorithm also consists only of shell identifier -characters; it should match the value in sha1-file.c. +characters; it should match the value in object-file.c. For example, the following lines map the key "rawsz" to "20" if SHA-1 is in use and to "32" if SHA-256 is in use: diff --git a/t/perf/p7519-fsmonitor.sh b/t/perf/p7519-fsmonitor.sh index 9b43342806..1e20a184c7 100755 --- a/t/perf/p7519-fsmonitor.sh +++ b/t/perf/p7519-fsmonitor.sh @@ -129,7 +129,12 @@ setup_for_fsmonitor() { git config core.fsmonitor "$INTEGRATION_SCRIPT" && git update-index --fsmonitor 2>error && - test_must_be_empty error # ensure no silent error + if test_have_prereq WATCHMAN + then + test_must_be_empty error # ensure no silent error + else + grep "Empty last update token" error + fi } test_perf_w_drop_caches () { diff --git a/t/perf/perf-lib.sh b/t/perf/perf-lib.sh index 22d727cef8..e385c6896f 100644 --- a/t/perf/perf-lib.sh +++ b/t/perf/perf-lib.sh @@ -147,10 +147,7 @@ test_run_perf_ () { "$GTIME" -f "%E %U %S" -o test_time.$i "$SHELL" -c ' . '"$TEST_DIRECTORY"/test-lib-functions.sh' test_export () { - [ $# != 0 ] || return 0 - test_export_="$test_export_ $1" - shift - test_export "$@" + test_export_="$test_export_ $*" } '"$1"' ret=$? diff --git a/t/t0068-for-each-repo.sh b/t/t0068-for-each-repo.sh index 136b4ec839..4675e85251 100755 --- a/t/t0068-for-each-repo.sh +++ b/t/t0068-for-each-repo.sh @@ -27,4 +27,10 @@ test_expect_success 'run based on configured value' ' grep again message ' +test_expect_success 'do nothing on empty config' ' + # the whole thing would fail if for-each-ref iterated even + # once, because "git help --no-such-option" would fail + git for-each-repo --config=bogus.config -- help --no-such-option +' + test_done diff --git a/t/t1300-config.sh b/t/t1300-config.sh index 97a04c6cc2..1a4156c704 100755 --- a/t/t1300-config.sh +++ b/t/t1300-config.sh @@ -12,75 +12,75 @@ test_expect_success 'clear default config' ' ' cat > expect << EOF -[core] +[section] penguin = little blue EOF test_expect_success 'initial' ' - git config core.penguin "little blue" && + git config section.penguin "little blue" && test_cmp expect .git/config ' cat > expect << EOF -[core] +[section] penguin = little blue Movie = BadPhysics EOF test_expect_success 'mixed case' ' - git config Core.Movie BadPhysics && + git config Section.Movie BadPhysics && test_cmp expect .git/config ' cat > expect << EOF -[core] +[section] penguin = little blue Movie = BadPhysics -[Cores] +[Sections] WhatEver = Second EOF test_expect_success 'similar section' ' - git config Cores.WhatEver Second && + git config Sections.WhatEver Second && test_cmp expect .git/config ' cat > expect << EOF -[core] +[section] penguin = little blue Movie = BadPhysics UPPERCASE = true -[Cores] +[Sections] WhatEver = Second EOF test_expect_success 'uppercase section' ' - git config CORE.UPPERCASE true && + git config SECTION.UPPERCASE true && test_cmp expect .git/config ' test_expect_success 'replace with non-match' ' - git config core.penguin kingpin !blue + git config section.penguin kingpin !blue ' test_expect_success 'replace with non-match (actually matching)' ' - git config core.penguin "very blue" !kingpin + git config section.penguin "very blue" !kingpin ' cat > expect << EOF -[core] +[section] penguin = very blue Movie = BadPhysics UPPERCASE = true penguin = kingpin -[Cores] +[Sections] WhatEver = Second EOF test_expect_success 'non-match result' 'test_cmp expect .git/config' test_expect_success 'find mixed-case key by canonical name' ' - test_cmp_config Second cores.whatever + test_cmp_config Second sections.whatever ' test_expect_success 'find mixed-case key by non-canonical name' ' - test_cmp_config Second CoReS.WhAtEvEr + test_cmp_config Second SeCtIoNs.WhAtEvEr ' test_expect_success 'subsections are not canonicalized by git-config' ' @@ -469,7 +469,8 @@ test_expect_success 'new variable inserts into proper section' ' ' test_expect_success 'alternative --file (non-existing file should fail)' ' - test_must_fail git config --file non-existing-config -l + test_must_fail git config --file non-existing-config -l && + test_must_fail git config --file non-existing-config test.xyzzy ' cat > other-config << EOF @@ -506,10 +507,6 @@ test_expect_success 'editing stdin is an error' ' test_expect_success 'refer config from subdirectory' ' mkdir x && - test_cmp_config -C x strasse --get --file ../other-config ein.bahn -' - -test_expect_success 'refer config from subdirectory via --file' ' test_cmp_config -C x strasse --file=../other-config --get ein.bahn ' @@ -1036,11 +1033,6 @@ test_expect_success SYMLINKS 'symlinked configuration' ' test_cmp expect actual ' -test_expect_success 'nonexistent configuration' ' - test_must_fail git config --file=doesnotexist --list && - test_must_fail git config --file=doesnotexist test.xyzzy -' - test_expect_success SYMLINKS 'symlink to nonexistent configuration' ' ln -s doesnotexist linktonada && ln -s linktonada linktolinktonada && @@ -1065,12 +1057,12 @@ test_expect_success 'git -c "key=value" support' ' true EOF { - git -c core.name=value config core.name && + git -c section.name=value config section.name && git -c foo.CamelCase=value config foo.camelcase && git -c foo.flag config --bool foo.flag } >actual && test_cmp expect actual && - test_must_fail git -c name=value config core.name + test_must_fail git -c name=value config section.name ' # We just need a type-specifier here that cares about the @@ -1115,7 +1107,7 @@ test_expect_success 'aliases can be CamelCased' ' test_expect_success 'git -c does not split values on equals' ' echo "value with = in it" >expect && - git -c core.foo="value with = in it" config core.foo >actual && + git -c section.foo="value with = in it" config section.foo >actual && test_cmp expect actual ' @@ -1846,53 +1838,53 @@ do done cat >.git/config <<-\EOF && -[core] +[section] foo = true number = 10 big = 1M EOF test_expect_success 'identical modern --type specifiers are allowed' ' - test_cmp_config 1048576 --type=int --type=int core.big + test_cmp_config 1048576 --type=int --type=int section.big ' test_expect_success 'identical legacy --type specifiers are allowed' ' - test_cmp_config 1048576 --int --int core.big + test_cmp_config 1048576 --int --int section.big ' test_expect_success 'identical mixed --type specifiers are allowed' ' - test_cmp_config 1048576 --int --type=int core.big + test_cmp_config 1048576 --int --type=int section.big ' test_expect_success 'non-identical modern --type specifiers are not allowed' ' - test_must_fail git config --type=int --type=bool core.big 2>error && + test_must_fail git config --type=int --type=bool section.big 2>error && test_i18ngrep "only one type at a time" error ' test_expect_success 'non-identical legacy --type specifiers are not allowed' ' - test_must_fail git config --int --bool core.big 2>error && + test_must_fail git config --int --bool section.big 2>error && test_i18ngrep "only one type at a time" error ' test_expect_success 'non-identical mixed --type specifiers are not allowed' ' - test_must_fail git config --type=int --bool core.big 2>error && + test_must_fail git config --type=int --bool section.big 2>error && test_i18ngrep "only one type at a time" error ' test_expect_success '--type allows valid type specifiers' ' - test_cmp_config true --type=bool core.foo + test_cmp_config true --type=bool section.foo ' test_expect_success '--no-type unsets type specifiers' ' - test_cmp_config 10 --type=bool --no-type core.number + test_cmp_config 10 --type=bool --no-type section.number ' test_expect_success 'unset type specifiers may be reset to conflicting ones' ' - test_cmp_config 1048576 --type=bool --no-type --type=int core.big + test_cmp_config 1048576 --type=bool --no-type --type=int section.big ' test_expect_success '--type rejects unknown specifiers' ' - test_must_fail git config --type=nonsense core.foo 2>error && + test_must_fail git config --type=nonsense section.foo 2>error && test_i18ngrep "unrecognized --type argument" error ' diff --git a/t/t1500-rev-parse.sh b/t/t1500-rev-parse.sh index 408b97d5af..51d7d40ec1 100755 --- a/t/t1500-rev-parse.sh +++ b/t/t1500-rev-parse.sh @@ -3,6 +3,16 @@ test_description='test git rev-parse' . ./test-lib.sh +test_one () { + dir="$1" && + expect="$2" && + shift && + shift && + echo "$expect" >expect && + git -C "$dir" rev-parse "$@" >actual && + test_cmp expect actual +} + # usage: [options] label is-bare is-inside-git is-inside-work prefix git-dir absolute-git-dir test_rev_parse () { d= @@ -60,7 +70,13 @@ ROOT=$(pwd) test_expect_success 'setup' ' mkdir -p sub/dir work && - cp -R .git repo.git + cp -R .git repo.git && + git checkout -B main && + test_commit abc && + git checkout -b side && + test_commit def && + git checkout main && + git worktree add worktree side ' test_rev_parse toplevel false false true '' .git "$ROOT/.git" @@ -88,6 +104,45 @@ test_rev_parse -C work -g ../repo.git -b t 'GIT_DIR=../repo.git, core.bare = tru test_rev_parse -C work -g ../repo.git -b u 'GIT_DIR=../repo.git, core.bare undefined' false false true '' +test_expect_success 'rev-parse --path-format=absolute' ' + test_one "." "$ROOT/.git" --path-format=absolute --git-dir && + test_one "." "$ROOT/.git" --path-format=absolute --git-common-dir && + test_one "sub/dir" "$ROOT/.git" --path-format=absolute --git-dir && + test_one "sub/dir" "$ROOT/.git" --path-format=absolute --git-common-dir && + test_one "worktree" "$ROOT/.git/worktrees/worktree" --path-format=absolute --git-dir && + test_one "worktree" "$ROOT/.git" --path-format=absolute --git-common-dir && + test_one "." "$ROOT" --path-format=absolute --show-toplevel && + test_one "." "$ROOT/.git/objects" --path-format=absolute --git-path objects && + test_one "." "$ROOT/.git/objects/foo/bar/baz" --path-format=absolute --git-path objects/foo/bar/baz +' + +test_expect_success 'rev-parse --path-format=relative' ' + test_one "." ".git" --path-format=relative --git-dir && + test_one "." ".git" --path-format=relative --git-common-dir && + test_one "sub/dir" "../../.git" --path-format=relative --git-dir && + test_one "sub/dir" "../../.git" --path-format=relative --git-common-dir && + test_one "worktree" "../.git/worktrees/worktree" --path-format=relative --git-dir && + test_one "worktree" "../.git" --path-format=relative --git-common-dir && + test_one "." "./" --path-format=relative --show-toplevel && + test_one "." ".git/objects" --path-format=relative --git-path objects && + test_one "." ".git/objects/foo/bar/baz" --path-format=relative --git-path objects/foo/bar/baz +' + +test_expect_success '--path-format=relative does not affect --absolute-git-dir' ' + git rev-parse --path-format=relative --absolute-git-dir >actual && + echo "$ROOT/.git" >expect && + test_cmp expect actual +' + +test_expect_success '--path-format can change in the middle of the command line' ' + git rev-parse --path-format=absolute --git-dir --path-format=relative --git-path objects/foo/bar >actual && + cat >expect <<-EOF && + $ROOT/.git + .git/objects/foo/bar + EOF + test_cmp expect actual +' + test_expect_success 'git-common-dir from worktree root' ' echo .git >expect && git rev-parse --git-common-dir >actual && diff --git a/t/t1512-rev-parse-disambiguation.sh b/t/t1512-rev-parse-disambiguation.sh index 18fa6cf40d..cc889d7a84 100755 --- a/t/t1512-rev-parse-disambiguation.sh +++ b/t/t1512-rev-parse-disambiguation.sh @@ -48,7 +48,7 @@ test_expect_success 'blob and tree' ' test_expect_success 'warn ambiguity when no candidate matches type hint' ' test_must_fail git rev-parse --verify 000000000^{commit} 2>actual && - test_i18ngrep "short SHA1 000000000 is ambiguous" actual + test_i18ngrep "short object ID 000000000 is ambiguous" actual ' test_expect_success 'disambiguate tree-ish' ' diff --git a/t/t2406-worktree-repair.sh b/t/t2406-worktree-repair.sh index 1fe468bfe8..f73741886b 100755 --- a/t/t2406-worktree-repair.sh +++ b/t/t2406-worktree-repair.sh @@ -104,6 +104,16 @@ test_expect_success 'repo not found; .git not file' ' test_i18ngrep ".git is not a file" err ' +test_expect_success 'repo not found; .git not referencing repo' ' + test_when_finished "rm -rf side not-a-repo && git worktree prune" && + git worktree add --detach side && + sed s,\.git/worktrees/side$,not-a-repo, side/.git >side/.newgit && + mv side/.newgit side/.git && + mkdir not-a-repo && + test_must_fail git worktree repair side 2>err && + test_i18ngrep ".git file does not reference a repository" err +' + test_expect_success 'repo not found; .git file broken' ' test_when_finished "rm -rf orig moved && git worktree prune" && git worktree add --detach orig && @@ -176,4 +186,20 @@ test_expect_success 'repair multiple gitdir files' ' test_must_be_empty err ' +test_expect_success 'repair moved main and linked worktrees' ' + test_when_finished "rm -rf main side mainmoved sidemoved" && + test_create_repo main && + test_commit -C main init && + git -C main worktree add --detach ../side && + sed "s,side/\.git$,sidemoved/.git," \ + main/.git/worktrees/side/gitdir >expect-gitdir && + sed "s,main/.git/worktrees/side$,mainmoved/.git/worktrees/side," \ + side/.git >expect-gitfile && + mv main mainmoved && + mv side sidemoved && + git -C mainmoved worktree repair ../sidemoved && + test_cmp expect-gitdir mainmoved/.git/worktrees/side/gitdir && + test_cmp expect-gitfile sidemoved/.git +' + test_done diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh index 3ec3e1d730..0af3b85d17 100755 --- a/t/t3200-branch.sh +++ b/t/t3200-branch.sh @@ -306,7 +306,9 @@ test_expect_success 'git branch --list -v with --abbrev' ' git branch -v --list --no-abbrev t >actual.noabbrev && git branch -v --list --abbrev=0 t >actual.0abbrev && + git -c core.abbrev=no branch -v --list t >actual.noabbrev-conf && test_cmp actual.noabbrev actual.0abbrev && + test_cmp actual.noabbrev actual.noabbrev-conf && git branch -v --list --abbrev=36 t >actual.36abbrev && # how many hexdigits are used? diff --git a/t/t4129-apply-samemode.sh b/t/t4129-apply-samemode.sh index 41818d8315..576632f868 100755 --- a/t/t4129-apply-samemode.sh +++ b/t/t4129-apply-samemode.sh @@ -78,7 +78,7 @@ test_expect_success POSIXPERM 'do not use core.sharedRepository for working tree test_config core.sharedRepository 0666 && ( # Remove a default ACL if possible. - (setfacl -k newdir 2>/dev/null || true) && + (setfacl -k . 2>/dev/null || true) && umask 0077 && # Test both files (f1) and leading dirs (d) diff --git a/t/t4205-log-pretty-formats.sh b/t/t4205-log-pretty-formats.sh index 204c149d5a..749bc1431a 100755 --- a/t/t4205-log-pretty-formats.sh +++ b/t/t4205-log-pretty-formats.sh @@ -605,6 +605,12 @@ test_expect_success 'pretty format %(trailers) shows trailers' ' test_cmp expect actual ' +test_expect_success 'pretty format %(trailers:) enables no options' ' + git log --no-walk --pretty="%(trailers:)" >actual && + # "expect" the same as the test above + test_cmp expect actual +' + test_expect_success '%(trailers:only) shows only "key: value" trailers' ' git log --no-walk --pretty="%(trailers:only)" >actual && { @@ -709,19 +715,101 @@ test_expect_success '%(trailers:key) without value is error' ' test_cmp expect actual ' +test_expect_success '%(trailers:keyonly) shows only keys' ' + git log --no-walk --pretty="format:%(trailers:keyonly)" >actual && + test_write_lines \ + "Signed-off-by" \ + "Acked-by" \ + "[ v2 updated patch description ]" \ + "Signed-off-by" >expect && + test_cmp expect actual +' + +test_expect_success '%(trailers:key=foo,keyonly) shows only key' ' + git log --no-walk --pretty="format:%(trailers:key=Acked-by,keyonly)" >actual && + echo "Acked-by" >expect && + test_cmp expect actual +' + test_expect_success '%(trailers:key=foo,valueonly) shows only value' ' git log --no-walk --pretty="format:%(trailers:key=Acked-by,valueonly)" >actual && echo "A U Thor <author@example.com>" >expect && test_cmp expect actual ' +test_expect_success '%(trailers:valueonly) shows only values' ' + git log --no-walk --pretty="format:%(trailers:valueonly)" >actual && + test_write_lines \ + "A U Thor <author@example.com>" \ + "A U Thor <author@example.com>" \ + "[ v2 updated patch description ]" \ + "A U Thor" \ + " <author@example.com>" >expect && + test_cmp expect actual +' + +test_expect_success '%(trailers:key=foo,keyonly,valueonly) shows nothing' ' + git log --no-walk --pretty="format:%(trailers:key=Acked-by,keyonly,valueonly)" >actual && + echo >expect && + test_cmp expect actual +' + test_expect_success 'pretty format %(trailers:separator) changes separator' ' + git log --no-walk --pretty=format:"X%(trailers:separator=%x00)X" >actual && + ( + printf "XSigned-off-by: A U Thor <author@example.com>\0" && + printf "Acked-by: A U Thor <author@example.com>\0" && + printf "[ v2 updated patch description ]\0" && + printf "Signed-off-by: A U Thor\n <author@example.com>X" + ) >expect && + test_cmp expect actual +' + +test_expect_success 'pretty format %(trailers:separator=X,unfold) changes separator' ' git log --no-walk --pretty=format:"X%(trailers:separator=%x00,unfold)X" >actual && - printf "XSigned-off-by: A U Thor <author@example.com>\0Acked-by: A U Thor <author@example.com>\0[ v2 updated patch description ]\0Signed-off-by: A U Thor <author@example.com>X" >expect && + ( + printf "XSigned-off-by: A U Thor <author@example.com>\0" && + printf "Acked-by: A U Thor <author@example.com>\0" && + printf "[ v2 updated patch description ]\0" && + printf "Signed-off-by: A U Thor <author@example.com>X" + ) >expect && + test_cmp expect actual +' + +test_expect_success 'pretty format %(trailers:key_value_separator) changes key-value separator' ' + git log --no-walk --pretty=format:"X%(trailers:key_value_separator=%x00)X" >actual && + ( + printf "XSigned-off-by\0A U Thor <author@example.com>\n" && + printf "Acked-by\0A U Thor <author@example.com>\n" && + printf "[ v2 updated patch description ]\n" && + printf "Signed-off-by\0A U Thor\n <author@example.com>\nX" + ) >expect && + test_cmp expect actual +' + +test_expect_success 'pretty format %(trailers:key_value_separator,unfold) changes key-value separator' ' + git log --no-walk --pretty=format:"X%(trailers:key_value_separator=%x00,unfold)X" >actual && + ( + printf "XSigned-off-by\0A U Thor <author@example.com>\n" && + printf "Acked-by\0A U Thor <author@example.com>\n" && + printf "[ v2 updated patch description ]\n" && + printf "Signed-off-by\0A U Thor <author@example.com>\nX" + ) >expect && + test_cmp expect actual +' + +test_expect_success 'pretty format %(trailers:separator,key_value_separator) changes both separators' ' + git log --no-walk --pretty=format:"%(trailers:separator=%x00,key_value_separator=%x00%x00,unfold)" >actual && + ( + printf "Signed-off-by\0\0A U Thor <author@example.com>\0" && + printf "Acked-by\0\0A U Thor <author@example.com>\0" && + printf "[ v2 updated patch description ]\0" && + printf "Signed-off-by\0\0A U Thor <author@example.com>" + ) >expect && test_cmp expect actual ' -test_expect_success 'pretty format %(trailers) combining separator/key/valueonly' ' +test_expect_success 'pretty format %(trailers) combining separator/key/keyonly/valueonly' ' git commit --allow-empty -F - <<-\EOF && Important fix @@ -748,6 +836,13 @@ test_expect_success 'pretty format %(trailers) combining separator/key/valueonly "Does not close any tickets" \ "Another fix #567, #890" \ "Important fix #1234" >expect && + test_cmp expect actual && + + git log --pretty="%s% (trailers:separator=%x2c%x20,key=Closes,keyonly)" HEAD~3.. >actual && + test_write_lines \ + "Does not close any tickets" \ + "Another fix Closes, Closes" \ + "Important fix Closes" >expect && test_cmp expect actual ' diff --git a/t/t5310-pack-bitmaps.sh b/t/t5310-pack-bitmaps.sh index 1d40fcad39..3a2c9d2d8e 100755 --- a/t/t5310-pack-bitmaps.sh +++ b/t/t5310-pack-bitmaps.sh @@ -20,84 +20,172 @@ has_any () { grep -Ff "$1" "$2" } +# To ensure the logic for "maximal commits" is exercised, make +# the repository a bit more complicated. +# +# other second +# * * +# (99 commits) (99 commits) +# * * +# |\ /| +# | * octo-other octo-second * | +# |/|\_________ ____________/|\| +# | \ \/ __________/ | +# | | ________/\ / | +# * |/ * merge-right * +# | _|__________/ \____________ | +# |/ | \| +# (l1) * * merge-left * (r1) +# | / \________________________ | +# |/ \| +# (l2) * * (r2) +# \___________________________ | +# \| +# * (base) +# +# We only push bits down the first-parent history, which +# makes some of these commits unimportant! +# +# The important part for the maximal commit algorithm is how +# the bitmasks are extended. Assuming starting bit positions +# for second (bit 0) and other (bit 1), the bitmasks at the +# end should be: +# +# second: 1 (maximal, selected) +# other: 01 (maximal, selected) +# (base): 11 (maximal) +# +# This complicated history was important for a previous +# version of the walk that guarantees never walking a +# commit multiple times. That goal might be important +# again, so preserve this complicated case. For now, this +# test will guarantee that the bitmaps are computed +# correctly, even with the repeat calculations. + test_expect_success 'setup repo with moderate-sized history' ' - test_commit_bulk --id=file 100 && + test_commit_bulk --id=file 10 && + git branch -M second && git checkout -b other HEAD~5 && test_commit_bulk --id=side 10 && - git checkout master && - bitmaptip=$(git rev-parse master) && + + # add complicated history setup, including merges and + # ambiguous merge-bases + + git checkout -b merge-left other~2 && + git merge second~2 -m "merge-left" && + + git checkout -b merge-right second~1 && + git merge other~1 -m "merge-right" && + + git checkout -b octo-second second && + git merge merge-left merge-right -m "octopus-second" && + + git checkout -b octo-other other && + git merge merge-left merge-right -m "octopus-other" && + + git checkout other && + git merge octo-other -m "pull octopus" && + + git checkout second && + git merge octo-second -m "pull octopus" && + + # Remove these branches so they are not selected + # as bitmap tips + git branch -D merge-left && + git branch -D merge-right && + git branch -D octo-other && + git branch -D octo-second && + + # add padding to make these merges less interesting + # and avoid having them selected for bitmaps + test_commit_bulk --id=file 100 && + git checkout other && + test_commit_bulk --id=side 100 && + git checkout second && + + bitmaptip=$(git rev-parse second) && blob=$(echo tagged-blob | git hash-object -w --stdin) && git tag tagged-blob $blob && git config repack.writebitmaps true ' test_expect_success 'full repack creates bitmaps' ' - git repack -ad && + GIT_TRACE2_EVENT_NESTING=4 GIT_TRACE2_EVENT="$(pwd)/trace" \ + git repack -ad && ls .git/objects/pack/ | grep bitmap >output && - test_line_count = 1 output + test_line_count = 1 output && + grep "\"key\":\"num_selected_commits\",\"value\":\"106\"" trace && + grep "\"key\":\"num_maximal_commits\",\"value\":\"107\"" trace ' test_expect_success 'rev-list --test-bitmap verifies bitmaps' ' git rev-list --test-bitmap HEAD ' -rev_list_tests() { - state=$1 - - test_expect_success "counting commits via bitmap ($state)" ' - git rev-list --count HEAD >expect && - git rev-list --use-bitmap-index --count HEAD >actual && +rev_list_tests_head () { + test_expect_success "counting commits via bitmap ($state, $branch)" ' + git rev-list --count $branch >expect && + git rev-list --use-bitmap-index --count $branch >actual && test_cmp expect actual ' - test_expect_success "counting partial commits via bitmap ($state)" ' - git rev-list --count HEAD~5..HEAD >expect && - git rev-list --use-bitmap-index --count HEAD~5..HEAD >actual && + test_expect_success "counting partial commits via bitmap ($state, $branch)" ' + git rev-list --count $branch~5..$branch >expect && + git rev-list --use-bitmap-index --count $branch~5..$branch >actual && test_cmp expect actual ' - test_expect_success "counting commits with limit ($state)" ' - git rev-list --count -n 1 HEAD >expect && - git rev-list --use-bitmap-index --count -n 1 HEAD >actual && + test_expect_success "counting commits with limit ($state, $branch)" ' + git rev-list --count -n 1 $branch >expect && + git rev-list --use-bitmap-index --count -n 1 $branch >actual && test_cmp expect actual ' - test_expect_success "counting non-linear history ($state)" ' - git rev-list --count other...master >expect && - git rev-list --use-bitmap-index --count other...master >actual && + test_expect_success "counting non-linear history ($state, $branch)" ' + git rev-list --count other...second >expect && + git rev-list --use-bitmap-index --count other...second >actual && test_cmp expect actual ' - test_expect_success "counting commits with limiting ($state)" ' - git rev-list --count HEAD -- 1.t >expect && - git rev-list --use-bitmap-index --count HEAD -- 1.t >actual && + test_expect_success "counting commits with limiting ($state, $branch)" ' + git rev-list --count $branch -- 1.t >expect && + git rev-list --use-bitmap-index --count $branch -- 1.t >actual && test_cmp expect actual ' - test_expect_success "counting objects via bitmap ($state)" ' - git rev-list --count --objects HEAD >expect && - git rev-list --use-bitmap-index --count --objects HEAD >actual && + test_expect_success "counting objects via bitmap ($state, $branch)" ' + git rev-list --count --objects $branch >expect && + git rev-list --use-bitmap-index --count --objects $branch >actual && test_cmp expect actual ' - test_expect_success "enumerate commits ($state)" ' - git rev-list --use-bitmap-index HEAD >actual && - git rev-list HEAD >expect && + test_expect_success "enumerate commits ($state, $branch)" ' + git rev-list --use-bitmap-index $branch >actual && + git rev-list $branch >expect && test_bitmap_traversal --no-confirm-bitmaps expect actual ' - test_expect_success "enumerate --objects ($state)" ' - git rev-list --objects --use-bitmap-index HEAD >actual && - git rev-list --objects HEAD >expect && + test_expect_success "enumerate --objects ($state, $branch)" ' + git rev-list --objects --use-bitmap-index $branch >actual && + git rev-list --objects $branch >expect && test_bitmap_traversal expect actual ' - test_expect_success "bitmap --objects handles non-commit objects ($state)" ' - git rev-list --objects --use-bitmap-index HEAD tagged-blob >actual && + test_expect_success "bitmap --objects handles non-commit objects ($state, $branch)" ' + git rev-list --objects --use-bitmap-index $branch tagged-blob >actual && grep $blob actual ' } +rev_list_tests () { + state=$1 + + for branch in "second" "other" + do + rev_list_tests_head + done +} + rev_list_tests 'full bitmap' test_expect_success 'clone from bitmapped repository' ' @@ -128,7 +216,7 @@ test_expect_success 'setup further non-bitmapped commits' ' rev_list_tests 'partial bitmap' test_expect_success 'fetch (partial bitmap)' ' - git --git-dir=clone.git fetch origin master:master && + git --git-dir=clone.git fetch origin second:second && git rev-parse HEAD >expect && git --git-dir=clone.git rev-parse HEAD >actual && test_cmp expect actual @@ -230,7 +318,7 @@ test_expect_success 'full repack, reusing previous bitmaps' ' ' test_expect_success 'fetch (full bitmap)' ' - git --git-dir=clone.git fetch origin master:master && + git --git-dir=clone.git fetch origin second:second && git rev-parse HEAD >expect && git --git-dir=clone.git rev-parse HEAD >actual && test_cmp expect actual @@ -343,7 +431,20 @@ test_expect_success 'pack reuse respects --incremental' ' test_must_be_empty actual ' -test_expect_success 'truncated bitmap fails gracefully' ' +test_expect_success 'truncated bitmap fails gracefully (ewah)' ' + test_config pack.writebitmaphashcache false && + git repack -ad && + git rev-list --use-bitmap-index --count --all >expect && + bitmap=$(ls .git/objects/pack/*.bitmap) && + test_when_finished "rm -f $bitmap" && + test_copy_bytes 256 <$bitmap >$bitmap.tmp && + mv -f $bitmap.tmp $bitmap && + git rev-list --use-bitmap-index --count --all >actual 2>stderr && + test_cmp expect actual && + test_i18ngrep corrupt.ewah.bitmap stderr +' + +test_expect_success 'truncated bitmap fails gracefully (cache)' ' git repack -ad && git rev-list --use-bitmap-index --count --all >expect && bitmap=$(ls .git/objects/pack/*.bitmap) && @@ -352,7 +453,7 @@ test_expect_success 'truncated bitmap fails gracefully' ' mv -f $bitmap.tmp $bitmap && git rev-list --use-bitmap-index --count --all >actual 2>stderr && test_cmp expect actual && - test_i18ngrep corrupt stderr + test_i18ngrep corrupted.bitmap.index stderr ' # have_delta <obj> <expected_base> diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh index 01004ff680..3ed121d0ce 100755 --- a/t/t5516-fetch-push.sh +++ b/t/t5516-fetch-push.sh @@ -1260,7 +1260,9 @@ do git cat-file commit $SHA1_2 && test_must_fail env GIT_TEST_PROTOCOL_VERSION=0 \ git fetch ../testrepo/.git $SHA1_3 2>err && - test_i18ngrep "remote error:.*not our ref.*$SHA1_3\$" err + # ideally we would insist this be on a "remote error:" + # line, but it is racy; see the commit message + test_i18ngrep "not our ref.*$SHA1_3\$" err ) ' done diff --git a/t/t5526-fetch-submodules.sh b/t/t5526-fetch-submodules.sh index a877dd145e..53d7b8ed75 100755 --- a/t/t5526-fetch-submodules.sh +++ b/t/t5526-fetch-submodules.sh @@ -722,4 +722,121 @@ test_expect_success 'fetch new submodule commit intermittently referenced by sup ) ' +add_commit_push () { + dir="$1" && + msg="$2" && + shift 2 && + git -C "$dir" add "$@" && + git -C "$dir" commit -a -m "$msg" && + git -C "$dir" push +} + +compare_refs_in_dir () { + fail= && + if test "x$1" = 'x!' + then + fail='!' && + shift + fi && + git -C "$1" rev-parse --verify "$2" >expect && + git -C "$3" rev-parse --verify "$4" >actual && + eval $fail test_cmp expect actual +} + + +test_expect_success 'setup nested submodule fetch test' ' + # does not depend on any previous test setups + + for repo in outer middle inner + do + git init --bare $repo && + git clone $repo ${repo}_content && + echo "$repo" >"${repo}_content/file" && + add_commit_push ${repo}_content "initial" file || + return 1 + done && + + git clone outer A && + git -C A submodule add "$pwd/middle" && + git -C A/middle/ submodule add "$pwd/inner" && + add_commit_push A/middle/ "adding inner sub" .gitmodules inner && + add_commit_push A/ "adding middle sub" .gitmodules middle && + + git clone outer B && + git -C B/ submodule update --init middle && + + compare_refs_in_dir A HEAD B HEAD && + compare_refs_in_dir A/middle HEAD B/middle HEAD && + test_path_is_file B/file && + test_path_is_file B/middle/file && + test_path_is_missing B/middle/inner/file && + + echo "change on inner repo of A" >"A/middle/inner/file" && + add_commit_push A/middle/inner "change on inner" file && + add_commit_push A/middle "change on inner" inner && + add_commit_push A "change on inner" middle +' + +test_expect_success 'fetching a superproject containing an uninitialized sub/sub project' ' + # depends on previous test for setup + + git -C B/ fetch && + compare_refs_in_dir A origin/HEAD B origin/HEAD +' + +fetch_with_recursion_abort () { + # In a regression the following git call will run into infinite recursion. + # To handle that, we connect the sed command to the git call by a pipe + # so that sed can kill the infinite recursion when detected. + # The recursion creates git output like: + # Fetching submodule sub + # Fetching submodule sub/sub <-- [1] + # Fetching submodule sub/sub/sub + # ... + # [1] sed will stop reading and cause git to eventually stop and die + + git -C "$1" fetch --recurse-submodules 2>&1 | + sed "/Fetching submodule $2[^$]/q" >out && + ! grep "Fetching submodule $2[^$]" out +} + +test_expect_success 'setup recursive fetch with uninit submodule' ' + # does not depend on any previous test setups + + test_create_repo super && + test_commit -C super initial && + test_create_repo sub && + test_commit -C sub initial && + git -C sub rev-parse HEAD >expect && + + git -C super submodule add ../sub && + git -C super commit -m "add sub" && + + git clone super superclone && + git -C superclone submodule status >out && + sed -e "s/^-//" -e "s/ sub.*$//" out >actual && + test_cmp expect actual +' + +test_expect_success 'recursive fetch with uninit submodule' ' + # depends on previous test for setup + + fetch_with_recursion_abort superclone sub && + git -C superclone submodule status >out && + sed -e "s/^-//" -e "s/ sub$//" out >actual && + test_cmp expect actual +' + +test_expect_success 'recursive fetch after deinit a submodule' ' + # depends on previous test for setup + + git -C superclone submodule update --init sub && + git -C superclone submodule deinit -f sub && + + fetch_with_recursion_abort superclone sub && + git -C superclone submodule status >out && + sed -e "s/^-//" -e "s/ sub$//" out >actual && + test_cmp expect actual +' + test_done diff --git a/t/t6016-rev-list-graph-simplify-history.sh b/t/t6016-rev-list-graph-simplify-history.sh index f5e6e92f5b..f79df8b6d1 100755 --- a/t/t6016-rev-list-graph-simplify-history.sh +++ b/t/t6016-rev-list-graph-simplify-history.sh @@ -8,6 +8,12 @@ test_description='--graph and simplified history' . ./test-lib.sh +. "$TEST_DIRECTORY"/lib-log-graph.sh + +check_graph () { + cat >expect && + lib_test_cmp_graph --format=%s "$@" +} test_expect_success 'set up rev-list --graph test' ' # 3 commits on branch A @@ -28,7 +34,7 @@ test_expect_success 'set up rev-list --graph test' ' # Octopus merge B and C into branch A git checkout A && - git merge B C && + git merge B C -m A4 && git tag A4 && test_commit A5 bar.txt && @@ -38,81 +44,64 @@ test_expect_success 'set up rev-list --graph test' ' test_commit C3 foo.txt && test_commit C4 bar.txt && git checkout A && - git merge -s ours C && + git merge -s ours C -m A6 && git tag A6 && - test_commit A7 bar.txt && - - # Store commit names in variables for later use - A1=$(git rev-parse --verify A1) && - A2=$(git rev-parse --verify A2) && - A3=$(git rev-parse --verify A3) && - A4=$(git rev-parse --verify A4) && - A5=$(git rev-parse --verify A5) && - A6=$(git rev-parse --verify A6) && - A7=$(git rev-parse --verify A7) && - B1=$(git rev-parse --verify B1) && - B2=$(git rev-parse --verify B2) && - C1=$(git rev-parse --verify C1) && - C2=$(git rev-parse --verify C2) && - C3=$(git rev-parse --verify C3) && - C4=$(git rev-parse --verify C4) - ' + test_commit A7 bar.txt +' test_expect_success '--graph --all' ' - rm -f expected && - echo "* $A7" >> expected && - echo "* $A6" >> expected && - echo "|\\ " >> expected && - echo "| * $C4" >> expected && - echo "| * $C3" >> expected && - echo "* | $A5" >> expected && - echo "| | " >> expected && - echo "| \\ " >> expected && - echo "*-. | $A4" >> expected && - echo "|\\ \\| " >> expected && - echo "| | * $C2" >> expected && - echo "| | * $C1" >> expected && - echo "| * | $B2" >> expected && - echo "| * | $B1" >> expected && - echo "* | | $A3" >> expected && - echo "| |/ " >> expected && - echo "|/| " >> expected && - echo "* | $A2" >> expected && - echo "|/ " >> expected && - echo "* $A1" >> expected && - git rev-list --graph --all > actual && - test_cmp expected actual - ' + check_graph --all <<-\EOF + * A7 + * A6 + |\ + | * C4 + | * C3 + * | A5 + | | + | \ + *-. | A4 + |\ \| + | | * C2 + | | * C1 + | * | B2 + | * | B1 + * | | A3 + | |/ + |/| + * | A2 + |/ + * A1 + EOF +' # Make sure the graph_is_interesting() code still realizes # that undecorated merges are interesting, even with --simplify-by-decoration test_expect_success '--graph --simplify-by-decoration' ' - rm -f expected && git tag -d A4 && - echo "* $A7" >> expected && - echo "* $A6" >> expected && - echo "|\\ " >> expected && - echo "| * $C4" >> expected && - echo "| * $C3" >> expected && - echo "* | $A5" >> expected && - echo "| | " >> expected && - echo "| \\ " >> expected && - echo "*-. | $A4" >> expected && - echo "|\\ \\| " >> expected && - echo "| | * $C2" >> expected && - echo "| | * $C1" >> expected && - echo "| * | $B2" >> expected && - echo "| * | $B1" >> expected && - echo "* | | $A3" >> expected && - echo "| |/ " >> expected && - echo "|/| " >> expected && - echo "* | $A2" >> expected && - echo "|/ " >> expected && - echo "* $A1" >> expected && - git rev-list --graph --all --simplify-by-decoration > actual && - test_cmp expected actual - ' + check_graph --all --simplify-by-decoration <<-\EOF + * A7 + * A6 + |\ + | * C4 + | * C3 + * | A5 + | | + | \ + *-. | A4 + |\ \| + | | * C2 + | | * C1 + | * | B2 + | * | B1 + * | | A3 + | |/ + |/| + * | A2 + |/ + * A1 + EOF +' test_expect_success 'setup: get rid of decorations on B' ' git tag -d B2 && @@ -122,142 +111,133 @@ test_expect_success 'setup: get rid of decorations on B' ' # Graph with branch B simplified away test_expect_success '--graph --simplify-by-decoration prune branch B' ' - rm -f expected && - echo "* $A7" >> expected && - echo "* $A6" >> expected && - echo "|\\ " >> expected && - echo "| * $C4" >> expected && - echo "| * $C3" >> expected && - echo "* | $A5" >> expected && - echo "* | $A4" >> expected && - echo "|\\| " >> expected && - echo "| * $C2" >> expected && - echo "| * $C1" >> expected && - echo "* | $A3" >> expected && - echo "|/ " >> expected && - echo "* $A2" >> expected && - echo "* $A1" >> expected && - git rev-list --graph --simplify-by-decoration --all > actual && - test_cmp expected actual - ' + check_graph --simplify-by-decoration --all <<-\EOF + * A7 + * A6 + |\ + | * C4 + | * C3 + * | A5 + * | A4 + |\| + | * C2 + | * C1 + * | A3 + |/ + * A2 + * A1 + EOF +' test_expect_success '--graph --full-history -- bar.txt' ' - rm -f expected && - echo "* $A7" >> expected && - echo "* $A6" >> expected && - echo "|\\ " >> expected && - echo "| * $C4" >> expected && - echo "* | $A5" >> expected && - echo "* | $A4" >> expected && - echo "|\\| " >> expected && - echo "* | $A3" >> expected && - echo "|/ " >> expected && - echo "* $A2" >> expected && - git rev-list --graph --full-history --all -- bar.txt > actual && - test_cmp expected actual - ' + check_graph --full-history --all -- bar.txt <<-\EOF + * A7 + * A6 + |\ + | * C4 + * | A5 + * | A4 + |\| + * | A3 + |/ + * A2 + EOF +' test_expect_success '--graph --full-history --simplify-merges -- bar.txt' ' - rm -f expected && - echo "* $A7" >> expected && - echo "* $A6" >> expected && - echo "|\\ " >> expected && - echo "| * $C4" >> expected && - echo "* | $A5" >> expected && - echo "* | $A3" >> expected && - echo "|/ " >> expected && - echo "* $A2" >> expected && - git rev-list --graph --full-history --simplify-merges --all \ - -- bar.txt > actual && - test_cmp expected actual - ' + check_graph --full-history --simplify-merges --all -- bar.txt <<-\EOF + * A7 + * A6 + |\ + | * C4 + * | A5 + * | A3 + |/ + * A2 + EOF +' test_expect_success '--graph -- bar.txt' ' - rm -f expected && - echo "* $A7" >> expected && - echo "* $A5" >> expected && - echo "* $A3" >> expected && - echo "| * $C4" >> expected && - echo "|/ " >> expected && - echo "* $A2" >> expected && - git rev-list --graph --all -- bar.txt > actual && - test_cmp expected actual - ' + check_graph --all -- bar.txt <<-\EOF + * A7 + * A5 + * A3 + | * C4 + |/ + * A2 + EOF +' test_expect_success '--graph --sparse -- bar.txt' ' - rm -f expected && - echo "* $A7" >> expected && - echo "* $A6" >> expected && - echo "* $A5" >> expected && - echo "* $A4" >> expected && - echo "* $A3" >> expected && - echo "| * $C4" >> expected && - echo "| * $C3" >> expected && - echo "| * $C2" >> expected && - echo "| * $C1" >> expected && - echo "|/ " >> expected && - echo "* $A2" >> expected && - echo "* $A1" >> expected && - git rev-list --graph --sparse --all -- bar.txt > actual && - test_cmp expected actual - ' + check_graph --sparse --all -- bar.txt <<-\EOF + * A7 + * A6 + * A5 + * A4 + * A3 + | * C4 + | * C3 + | * C2 + | * C1 + |/ + * A2 + * A1 + EOF +' test_expect_success '--graph ^C4' ' - rm -f expected && - echo "* $A7" >> expected && - echo "* $A6" >> expected && - echo "* $A5" >> expected && - echo "* $A4" >> expected && - echo "|\\ " >> expected && - echo "| * $B2" >> expected && - echo "| * $B1" >> expected && - echo "* $A3" >> expected && - git rev-list --graph --all ^C4 > actual && - test_cmp expected actual - ' + check_graph --all ^C4 <<-\EOF + * A7 + * A6 + * A5 + * A4 + |\ + | * B2 + | * B1 + * A3 + EOF +' test_expect_success '--graph ^C3' ' - rm -f expected && - echo "* $A7" >> expected && - echo "* $A6" >> expected && - echo "|\\ " >> expected && - echo "| * $C4" >> expected && - echo "* $A5" >> expected && - echo "* $A4" >> expected && - echo "|\\ " >> expected && - echo "| * $B2" >> expected && - echo "| * $B1" >> expected && - echo "* $A3" >> expected && - git rev-list --graph --all ^C3 > actual && - test_cmp expected actual - ' + check_graph --all ^C3 <<-\EOF + * A7 + * A6 + |\ + | * C4 + * A5 + * A4 + |\ + | * B2 + | * B1 + * A3 + EOF +' # I don't think the ordering of the boundary commits is really # that important, but this test depends on it. If the ordering ever changes # in the code, we'll need to update this test. test_expect_success '--graph --boundary ^C3' ' - rm -f expected && - echo "* $A7" >> expected && - echo "* $A6" >> expected && - echo "|\\ " >> expected && - echo "| * $C4" >> expected && - echo "* | $A5" >> expected && - echo "| | " >> expected && - echo "| \\ " >> expected && - echo "*-. \\ $A4" >> expected && - echo "|\\ \\ \\ " >> expected && - echo "| * | | $B2" >> expected && - echo "| * | | $B1" >> expected && - echo "* | | | $A3" >> expected && - echo "o | | | $A2" >> expected && - echo "|/ / / " >> expected && - echo "o / / $A1" >> expected && - echo " / / " >> expected && - echo "| o $C3" >> expected && - echo "|/ " >> expected && - echo "o $C2" >> expected && - git rev-list --graph --boundary --all ^C3 > actual && - test_cmp expected actual - ' + check_graph --boundary --all ^C3 <<-\EOF + * A7 + * A6 + |\ + | * C4 + * | A5 + | | + | \ + *-. \ A4 + |\ \ \ + | * | | B2 + | * | | B1 + * | | | A3 + o | | | A2 + |/ / / + o / / A1 + / / + | o C3 + |/ + o C2 + EOF +' test_done diff --git a/t/t6030-bisect-porcelain.sh b/t/t6030-bisect-porcelain.sh index aa226381be..52614eefc7 100755 --- a/t/t6030-bisect-porcelain.sh +++ b/t/t6030-bisect-porcelain.sh @@ -926,14 +926,14 @@ test_expect_success 'git bisect reset cleans bisection state properly' ' git bisect bad $HASH4 && git bisect reset && test -z "$(git for-each-ref "refs/bisect/*")" && - test_path_is_missing "$GIT_DIR/BISECT_EXPECTED_REV" && - test_path_is_missing "$GIT_DIR/BISECT_ANCESTORS_OK" && - test_path_is_missing "$GIT_DIR/BISECT_LOG" && - test_path_is_missing "$GIT_DIR/BISECT_RUN" && - test_path_is_missing "$GIT_DIR/BISECT_TERMS" && - test_path_is_missing "$GIT_DIR/head-name" && - test_path_is_missing "$GIT_DIR/BISECT_HEAD" && - test_path_is_missing "$GIT_DIR/BISECT_START" + test_path_is_missing ".git/BISECT_EXPECTED_REV" && + test_path_is_missing ".git/BISECT_ANCESTORS_OK" && + test_path_is_missing ".git/BISECT_LOG" && + test_path_is_missing ".git/BISECT_RUN" && + test_path_is_missing ".git/BISECT_TERMS" && + test_path_is_missing ".git/head-name" && + test_path_is_missing ".git/BISECT_HEAD" && + test_path_is_missing ".git/BISECT_START" ' test_done diff --git a/t/t7012-skip-worktree-writing.sh b/t/t7012-skip-worktree-writing.sh index 7476781979..e5c6a038fb 100755 --- a/t/t7012-skip-worktree-writing.sh +++ b/t/t7012-skip-worktree-writing.sh @@ -149,6 +149,94 @@ test_expect_success '--ignore-skip-worktree-entries leaves worktree alone' ' --diff-filter=D -- keep-me.t ' +test_expect_success 'stash restore in sparse checkout' ' + test_create_repo stash-restore && + ( + cd stash-restore && + + mkdir subdir && + echo A >subdir/A && + echo untouched >untouched && + echo removeme >removeme && + echo modified >modified && + git add . && + git commit -m Initial && + + echo AA >>subdir/A && + echo addme >addme && + echo tweaked >>modified && + rm removeme && + git add addme && + + git stash push && + + git sparse-checkout set subdir && + + # Ensure after sparse-checkout we only have expected files + cat >expect <<-EOF && + S modified + S removeme + H subdir/A + S untouched + EOF + git ls-files -t >actual && + test_cmp expect actual && + + test_path_is_missing addme && + test_path_is_missing modified && + test_path_is_missing removeme && + test_path_is_file subdir/A && + test_path_is_missing untouched && + + # Put a file in the working directory in the way + echo in the way >modified && + git stash apply && + + # Ensure stash vivifies modifies paths... + cat >expect <<-EOF && + H addme + H modified + H removeme + H subdir/A + S untouched + EOF + git ls-files -t >actual && + test_cmp expect actual && + + # ...and that the paths show up in status as changed... + cat >expect <<-EOF && + A addme + M modified + D removeme + M subdir/A + ?? actual + ?? expect + ?? modified.stash.XXXXXX + EOF + git status --porcelain | \ + sed -e s/stash......./stash.XXXXXX/ >actual && + test_cmp expect actual && + + # ...and that working directory reflects the files correctly + test_path_is_file addme && + test_path_is_file modified && + test_path_is_missing removeme && + test_path_is_file subdir/A && + test_path_is_missing untouched && + + # ...including that we have the expected "modified" file... + cat >expect <<-EOF && + modified + tweaked + EOF + test_cmp expect modified && + + # ...and that the other "modified" file is still present... + echo in the way >expect && + test_cmp expect modified.stash.* + ) +' + #TODO test_expect_failure 'git-apply adds file' false #TODO test_expect_failure 'git-apply updates file' false #TODO test_expect_failure 'git-apply removes file' false diff --git a/t/t7601-merge-pull-config.sh b/t/t7601-merge-pull-config.sh index 6774e9d86f..52e8ccc933 100755 --- a/t/t7601-merge-pull-config.sh +++ b/t/t7601-merge-pull-config.sh @@ -29,11 +29,8 @@ test_expect_success 'setup' ' test_expect_success 'pull.rebase not set' ' git reset --hard c0 && - git -c color.advice=always pull . c1 2>err && - test_decode_color <err >decoded && - test_i18ngrep "<YELLOW>hint: " decoded && - test_i18ngrep "Pulling without specifying how to reconcile" decoded - + git pull . c1 2>err && + test_i18ngrep ! "Pulling without specifying how to reconcile" err ' test_expect_success 'pull.rebase not set and pull.ff=true' ' @@ -87,6 +84,65 @@ test_expect_success 'pull.rebase not set and --ff-only given' ' test_i18ngrep ! "Pulling without specifying how to reconcile" err ' +test_expect_success 'pull.rebase not set (not-fast-forward)' ' + git reset --hard c2 && + git -c color.advice=always pull . c1 2>err && + test_decode_color <err >decoded && + test_i18ngrep "<YELLOW>hint: " decoded && + test_i18ngrep "Pulling without specifying how to reconcile" decoded +' + +test_expect_success 'pull.rebase not set and pull.ff=true (not-fast-forward)' ' + git reset --hard c2 && + test_config pull.ff true && + git pull . c1 2>err && + test_i18ngrep ! "Pulling without specifying how to reconcile" err +' + +test_expect_success 'pull.rebase not set and pull.ff=false (not-fast-forward)' ' + git reset --hard c2 && + test_config pull.ff false && + git pull . c1 2>err && + test_i18ngrep ! "Pulling without specifying how to reconcile" err +' + +test_expect_success 'pull.rebase not set and pull.ff=only (not-fast-forward)' ' + git reset --hard c2 && + test_config pull.ff only && + test_must_fail git pull . c1 2>err && + test_i18ngrep ! "Pulling without specifying how to reconcile" err +' + +test_expect_success 'pull.rebase not set and --rebase given (not-fast-forward)' ' + git reset --hard c2 && + git pull --rebase . c1 2>err && + test_i18ngrep ! "Pulling without specifying how to reconcile" err +' + +test_expect_success 'pull.rebase not set and --no-rebase given (not-fast-forward)' ' + git reset --hard c2 && + git pull --no-rebase . c1 2>err && + test_i18ngrep ! "Pulling without specifying how to reconcile" err +' + +test_expect_success 'pull.rebase not set and --ff given (not-fast-forward)' ' + git reset --hard c2 && + git pull --ff . c1 2>err && + test_i18ngrep ! "Pulling without specifying how to reconcile" err +' + +test_expect_success 'pull.rebase not set and --no-ff given (not-fast-forward)' ' + git reset --hard c2 && + git pull --no-ff . c1 2>err && + test_i18ngrep ! "Pulling without specifying how to reconcile" err +' + +test_expect_success 'pull.rebase not set and --ff-only given (not-fast-forward)' ' + git reset --hard c2 && + test_must_fail git pull --ff-only . c1 2>err && + test_i18ngrep ! "Pulling without specifying how to reconcile" err +' + test_expect_success 'merge c1 with c2' ' git reset --hard c1 && test -f c0.c && diff --git a/t/t7610-mergetool.sh b/t/t7610-mergetool.sh index 70afdd06fa..6ac75b5d4c 100755 --- a/t/t7610-mergetool.sh +++ b/t/t7610-mergetool.sh @@ -828,4 +828,15 @@ test_expect_success 'mergetool -Oorder-file is honored' ' test_cmp expect actual ' +test_expect_success 'mergetool --tool-help shows recognized tools' ' + # Check a few known tools are correctly shown + git mergetool --tool-help >mergetools && + grep vimdiff mergetools && + grep vimdiff3 mergetools && + grep gvimdiff2 mergetools && + grep araxis mergetools && + grep xxdiff mergetools && + grep meld mergetools +' + test_done diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh index 99bf0c7582..1074009cc0 100755 --- a/t/t7900-maintenance.sh +++ b/t/t7900-maintenance.sh @@ -7,6 +7,19 @@ test_description='git maintenance builtin' GIT_TEST_COMMIT_GRAPH=0 GIT_TEST_MULTI_PACK_INDEX=0 +test_lazy_prereq XMLLINT ' + xmllint --version +' + +test_xmllint () { + if test_have_prereq XMLLINT + then + xmllint --noout "$@" + else + true + fi +} + test_expect_success 'help text' ' test_expect_code 129 git maintenance -h 2>err && test_i18ngrep "usage: git maintenance <subcommand>" err && @@ -419,7 +432,7 @@ test_expect_success !MINGW 'register and unregister with regex metacharacters' ' ' test_expect_success 'start from empty cron table' ' - GIT_TEST_CRONTAB="test-tool crontab cron.txt" git maintenance start && + GIT_TEST_MAINT_SCHEDULER="crontab:test-tool crontab cron.txt" git maintenance start && # start registers the repo git config --get --global --fixed-value maintenance.repo "$(pwd)" && @@ -430,19 +443,19 @@ test_expect_success 'start from empty cron table' ' ' test_expect_success 'stop from existing schedule' ' - GIT_TEST_CRONTAB="test-tool crontab cron.txt" git maintenance stop && + GIT_TEST_MAINT_SCHEDULER="crontab:test-tool crontab cron.txt" git maintenance stop && # stop does not unregister the repo git config --get --global --fixed-value maintenance.repo "$(pwd)" && # Operation is idempotent - GIT_TEST_CRONTAB="test-tool crontab cron.txt" git maintenance stop && + GIT_TEST_MAINT_SCHEDULER="crontab:test-tool crontab cron.txt" git maintenance stop && test_must_be_empty cron.txt ' test_expect_success 'start preserves existing schedule' ' echo "Important information!" >cron.txt && - GIT_TEST_CRONTAB="test-tool crontab cron.txt" git maintenance start && + GIT_TEST_MAINT_SCHEDULER="crontab:test-tool crontab cron.txt" git maintenance start && grep "Important information!" cron.txt ' @@ -457,11 +470,94 @@ test_expect_success 'magic markers are correct' ' test_expect_success 'stop preserves surrounding schedule' ' echo "Crucial information!" >>cron.txt && - GIT_TEST_CRONTAB="test-tool crontab cron.txt" git maintenance stop && + GIT_TEST_MAINT_SCHEDULER="crontab:test-tool crontab cron.txt" git maintenance stop && grep "Important information!" cron.txt && grep "Crucial information!" cron.txt ' +test_expect_success 'start and stop macOS maintenance' ' + # ensure $HOME can be compared against hook arguments on all platforms + pfx=$(cd "$HOME" && pwd) && + + write_script print-args <<-\EOF && + echo $* | sed "s:gui/[0-9][0-9]*:gui/[UID]:" >>args + EOF + + rm -f args && + GIT_TEST_MAINT_SCHEDULER=launchctl:./print-args git maintenance start && + + # start registers the repo + git config --get --global --fixed-value maintenance.repo "$(pwd)" && + + ls "$HOME/Library/LaunchAgents" >actual && + cat >expect <<-\EOF && + org.git-scm.git.daily.plist + org.git-scm.git.hourly.plist + org.git-scm.git.weekly.plist + EOF + test_cmp expect actual && + + rm -f expect && + for frequency in hourly daily weekly + do + PLIST="$pfx/Library/LaunchAgents/org.git-scm.git.$frequency.plist" && + test_xmllint "$PLIST" && + grep schedule=$frequency "$PLIST" && + echo "bootout gui/[UID] $PLIST" >>expect && + echo "bootstrap gui/[UID] $PLIST" >>expect || return 1 + done && + test_cmp expect args && + + rm -f args && + GIT_TEST_MAINT_SCHEDULER=launchctl:./print-args git maintenance stop && + + # stop does not unregister the repo + git config --get --global --fixed-value maintenance.repo "$(pwd)" && + + printf "bootout gui/[UID] $pfx/Library/LaunchAgents/org.git-scm.git.%s.plist\n" \ + hourly daily weekly >expect && + test_cmp expect args && + ls "$HOME/Library/LaunchAgents" >actual && + test_line_count = 0 actual +' + +test_expect_success 'start and stop Windows maintenance' ' + write_script print-args <<-\EOF && + echo $* >>args + while test $# -gt 0 + do + case "$1" in + /xml) shift; xmlfile=$1; break ;; + *) shift ;; + esac + done + test -z "$xmlfile" || cp "$xmlfile" "$xmlfile.xml" + EOF + + rm -f args && + GIT_TEST_MAINT_SCHEDULER="schtasks:./print-args" git maintenance start && + + # start registers the repo + git config --get --global --fixed-value maintenance.repo "$(pwd)" && + + for frequency in hourly daily weekly + do + grep "/create /tn Git Maintenance ($frequency) /f /xml" args && + file=$(ls .git/schedule_${frequency}*.xml) && + test_xmllint "$file" || return 1 + done && + + rm -f args && + GIT_TEST_MAINT_SCHEDULER="schtasks:./print-args" git maintenance stop && + + # stop does not unregister the repo + git config --get --global --fixed-value maintenance.repo "$(pwd)" && + + printf "/delete /tn Git Maintenance (%s) /f\n" \ + hourly daily weekly >expect && + test_cmp expect args +' + test_expect_success 'register preserves existing strategy' ' git config maintenance.strategy none && git maintenance register && diff --git a/t/t9902-completion.sh b/t/t9902-completion.sh index a1c4f1f6d4..c4a7758409 100755 --- a/t/t9902-completion.sh +++ b/t/t9902-completion.sh @@ -2380,4 +2380,24 @@ test_expect_success 'sourcing the completion script clears cached --options' ' verbose test -z "$__gitcomp_builtin_notes_edit" ' +test_expect_success '__git_complete' ' + unset -f __git_wrap__git_main && + + __git_complete foo __git_main && + __git_have_func __git_wrap__git_main && + unset -f __git_wrap__git_main && + + __git_complete gf _git_fetch && + __git_have_func __git_wrap_git_fetch && + + __git_complete foo git && + __git_have_func __git_wrap__git_main && + unset -f __git_wrap__git_main && + + __git_complete gd git_diff && + __git_have_func __git_wrap_git_diff && + + test_must_fail __git_complete ga missing +' + test_done diff --git a/t/test-lib-functions.sh b/t/test-lib-functions.sh index 999982fe4a..2f08ce7cba 100644 --- a/t/test-lib-functions.sh +++ b/t/test-lib-functions.sh @@ -367,9 +367,14 @@ test_chmod () { git update-index --add "--chmod=$@" } -# Get the modebits from a file or directory. +# Get the modebits from a file or directory, ignoring the setgid bit (g+s). +# This bit is inherited by subdirectories at their creation. So we remove it +# from the returning string to prevent callers from having to worry about the +# state of the bit in the test directory. +# test_modebits () { - ls -ld "$1" | sed -e 's|^\(..........\).*|\1|' + ls -ld "$1" | sed -e 's|^\(..........\).*|\1|' \ + -e 's|^\(......\)S|\1-|' -e 's|^\(......\)s|\1x|' } # Unset a configuration variable, but don't fail if it doesn't exist. diff --git a/t/test-lib.sh b/t/test-lib.sh index 9fa7c1d0f6..03c1c0836f 100644 --- a/t/test-lib.sh +++ b/t/test-lib.sh @@ -1713,7 +1713,8 @@ test_lazy_prereq REBASE_P ' ' # Ensure that no test accidentally triggers a Git command -# that runs 'crontab', affecting a user's cron schedule. -# Tests that verify the cron integration must set this locally +# that runs the actual maintenance scheduler, affecting a user's +# system permanently. +# Tests that verify the scheduler integration must set this locally # to avoid errors. -GIT_TEST_CRONTAB="exit 1" +GIT_TEST_MAINT_SCHEDULER="none:exit 1" @@ -1131,7 +1131,9 @@ static void format_trailer_info(struct strbuf *out, size_t i; /* If we want the whole block untouched, we can take the fast path. */ - if (!opts->only_trailers && !opts->unfold && !opts->filter && !opts->separator) { + if (!opts->only_trailers && !opts->unfold && !opts->filter && + !opts->separator && !opts->key_only && !opts->value_only && + !opts->key_value_separator) { strbuf_add(out, info->trailer_start, info->trailer_end - info->trailer_start); return; @@ -1153,8 +1155,15 @@ static void format_trailer_info(struct strbuf *out, if (opts->separator && out->len != origlen) strbuf_addbuf(out, opts->separator); if (!opts->value_only) - strbuf_addf(out, "%s: ", tok.buf); - strbuf_addbuf(out, &val); + strbuf_addbuf(out, &tok); + if (!opts->key_only && !opts->value_only) { + if (opts->key_value_separator) + strbuf_addbuf(out, opts->key_value_separator); + else + strbuf_addstr(out, ": "); + } + if (!opts->key_only) + strbuf_addbuf(out, &val); if (!opts->separator) strbuf_addch(out, '\n'); } @@ -71,8 +71,10 @@ struct process_trailer_options { int only_input; int unfold; int no_divider; + int key_only; int value_only; const struct strbuf *separator; + const struct strbuf *key_value_separator; int (*filter)(const struct strbuf *, void *); void *filter_data; }; @@ -144,7 +144,7 @@ int read_tree_recursive(struct repository *r, return ret; } -static int cmp_cache_name_compare(const void *a_, const void *b_) +int cmp_cache_name_compare(const void *a_, const void *b_) { const struct cache_entry *ce1, *ce2; @@ -28,6 +28,8 @@ void free_tree_buffer(struct tree *tree); /* Parses and returns the tree in the given ent, chasing tags and commits. */ struct tree *parse_tree_indirect(const struct object_id *oid); +int cmp_cache_name_compare(const void *a_, const void *b_); + #define READ_TREE_RECURSIVE 1 typedef int (*read_tree_fn_t)(const struct object_id *, struct strbuf *, const char *, unsigned int, int, void *); diff --git a/worktree.c b/worktree.c index f84ceae87d..821b233479 100644 --- a/worktree.c +++ b/worktree.c @@ -645,6 +645,42 @@ static int is_main_worktree_path(const char *path) } /* + * If both the main worktree and linked worktree have been moved, then the + * gitfile /path/to/worktree/.git won't point into the repository, thus we + * won't know which <repo>/worktrees/<id>/gitdir to repair. However, we may + * be able to infer the gitdir by manually reading /path/to/worktree/.git, + * extracting the <id>, and checking if <repo>/worktrees/<id> exists. + */ +static char *infer_backlink(const char *gitfile) +{ + struct strbuf actual = STRBUF_INIT; + struct strbuf inferred = STRBUF_INIT; + const char *id; + + if (strbuf_read_file(&actual, gitfile, 0) < 0) + goto error; + if (!starts_with(actual.buf, "gitdir:")) + goto error; + if (!(id = find_last_dir_sep(actual.buf))) + goto error; + strbuf_trim(&actual); + id++; /* advance past '/' to point at <id> */ + if (!*id) + goto error; + strbuf_git_common_path(&inferred, the_repository, "worktrees/%s", id); + if (!is_directory(inferred.buf)) + goto error; + + strbuf_release(&actual); + return strbuf_detach(&inferred, NULL); + +error: + strbuf_release(&actual); + strbuf_release(&inferred); + return NULL; +} + +/* * Repair <repo>/worktrees/<id>/gitdir if missing, corrupt, or not pointing at * the worktree's path. */ @@ -675,6 +711,11 @@ void repair_worktree_at_path(const char *path, if (err == READ_GITFILE_ERR_NOT_A_FILE) { fn(1, realdotgit.buf, _("unable to locate repository; .git is not a file"), cb_data); goto done; + } else if (err == READ_GITFILE_ERR_NOT_A_REPO) { + if (!(backlink = infer_backlink(realdotgit.buf))) { + fn(1, realdotgit.buf, _("unable to locate repository; .git file does not reference a repository"), cb_data); + goto done; + } } else if (err) { fn(1, realdotgit.buf, _("unable to locate repository; .git file broken"), cb_data); goto done; |