diff options
139 files changed, 5377 insertions, 1872 deletions
diff --git a/.gitignore b/.gitignore index b1020b875f..833ef3b0b7 100644 --- a/.gitignore +++ b/.gitignore @@ -114,6 +114,7 @@ /git-read-tree /git-rebase /git-rebase--am +/git-rebase--helper /git-rebase--interactive /git-rebase--merge /git-receive-pack @@ -177,6 +177,7 @@ Paolo Bonzini <bonzini@gnu.org> <paolo.bonzini@lu.unisi.ch> Pascal Obry <pascal@obry.net> <pascal.obry@gmail.com> Pascal Obry <pascal@obry.net> <pascal.obry@wanadoo.fr> Pat Notz <patnotz@gmail.com> <pknotz@sandia.gov> +Patrick Steinhardt <ps@pks.im> <patrick.steinhardt@elego.de> Paul Mackerras <paulus@samba.org> <paulus@dorrigo.(none)> Paul Mackerras <paulus@samba.org> <paulus@pogo.(none)> Peter Baumann <waste.manager@gmx.de> <Peter.B.Baumann@stud.informatik.uni-erlangen.de> diff --git a/Documentation/RelNotes/2.13.0.txt b/Documentation/RelNotes/2.13.0.txt new file mode 100644 index 0000000000..9d92ddb277 --- /dev/null +++ b/Documentation/RelNotes/2.13.0.txt @@ -0,0 +1,216 @@ +Git 2.13 Release Notes +====================== + +Backward compatibility notes. + + * Use of an empty string as a pathspec element that is used for + 'everything matches' is still warned and Git asks users to use a + more explicit '.' for that instead. The hope is that existing + users will not mind this change, and eventually the warning can be + turned into a hard error, upgrading the deprecation into removal of + this (mis)feature. That is not scheduled to happen in the upcoming + release (yet). + + * The historical argument order "git merge <msg> HEAD <commit>..." + has been deprecated for quite some time, and will be removed in a + future release. + + +Updates since v2.12 +------------------- + +UI, Workflows & Features + + * "git describe" and "git name-rev" have been taught to take more + than one refname patterns to restrict the set of refs to base their + naming output on, and also learned to take negative patterns to + name refs not to be used for naming via their "--exclude" option. + + * Deletion of a branch "foo/bar" could remove .git/refs/heads/foo + once there no longer is any other branch whose name begins with + "foo/", but we didn't do so so far. Now we do. + + * When "git merge" detects a path that is renamed in one history + while the other history deleted (or modified) it, it now reports + both paths to help the user understand what is going on in the two + histories being merged. + + * The <url> part in "http.<url>.<variable>" configuration variable + can now be spelled with '*' that serves as wildcard. + E.g. "http.https://*.example.com.proxy" can be used to specify the + proxy used for https://a.example.com, https://b.example.com, etc., + i.e. any host in the example.com domain. + + * "git tag" did not leave useful message when adding a new entry to + reflog; this was left unnoticed for a long time because refs/tags/* + doesn't keep reflog by default. + + * The "negative" pathspec feature was somewhat more cumbersome to use + than necessary in that its short-hand used "!" which needed to be + escaped from shells, and it required "exclude from what?" specified. + + * The command line options for ssh invocation needs to be tweaked for + some implementations of SSH (e.g. PuTTY plink wants "-P <port>" + while OpenSSH wants "-p <port>" to specify port to connect to), and + the variant was guessed when GIT_SSH environment variable is used + to specify it. The logic to guess now applies to the command + specified by the newer GIT_SSH_COMMAND and also core.sshcommand + configuration variable, and comes with an escape hatch for users to + deal with misdetected cases. + + * The "--git-path", "--git-common-dir", and "--shared-index-path" + options of "git rev-parse" did not produce usable output. They are + now updated to show the path to the correct file, relative to where + the caller is. + + * "git diff -W" has been taught to handle the case where a new + function is added at the end of the file better. + + * "git update-ref -d" and other operations to delete references did + not leave any entry in HEAD's reflog when the reference being + deleted was the current branch. This is not a problem in practice + because you do not want to delete the branch you are currently on, + but caused renaming of the current branch to something else not to + be logged in a useful way. + + * "Cc:" on the trailer part does not have to conform to RFC strictly, + unlike in the e-mail header. "git send-email" has been updated to + ignore anything after '>' when picking addresses, to allow non-address + cruft like " # stable 4.4" after the address. + (merge 9d3343961b jh/send-email-one-cc later to maint). + + * When "git submodule init" decides that the submodule in the working + tree is its upstream, it now gives a warning as it is not a very + common setup. + (merge d1b3b81aab sb/submodule-init-url-selection later to maint). + + * "git stash save" takes a pathspec so that the local changes can be + stashed away only partially. + (merge 9e140909f6 tg/stash-push later to maint). + + +Performance, Internal Implementation, Development Support etc. + + * The code to list branches in "git branch" has been consolidated + with the more generic ref-filter API. + + * Resource usage while enumerating refs from alternate object store + has been optimized to help receiving end of "push" that hosts a + repository with many "forks". + + * The gitattributes machinery is being taught to work better in a + multi-threaded environment. + + * "git rebase -i" starts using the recently updated "sequencer" code. + + * Code and design clean-up for the refs API. + + * The preload-index code has been taught not to bother with the index + entries that are paths that are not checked out by "sparse checkout". + + * Some warning() messages from "git clean" were updated to show the + errno from failed system calls. + + * The "parse_config_key()" API function has been cleaned up. + (merge ad8c7cdadd jk/parse-config-key-cleanup later to maint). + + * A test that creates a confusing branch whose name is HEAD has been + corrected not to do so. + (merge f0252ca23c jk/t6300-cleanup later to maint). + + * The code that parses header fields in the commit object has been + updated for (micro)performance and code hygiene. + (merge b072504ce1 rs/commit-parsing-optim later to maint). + + * An helper function to make it easier to append the result from + real_path() to a strbuf has been added. + (merge 33ad9ddd0b rs/strbuf-add-real-path later to maint). + + * Reduce authentication round-trip over HTTP when the server supports + just a single authentication method. This also improves the + behaviour when Git is misconfigured to enable http.emptyAuth + against a server that does not authenticate without a username + (i.e. not using Kerberos etc., which makes http.emptyAuth + pointless). + (merge 40a18fc77c jk/http-auth later to maint). + + +Also contains various documentation updates and code clean-ups. + + +Fixes since v2.12 +----------------- + +Unless otherwise noted, all the fixes since v2.9 in the maintenance +track are contained in this release (see the maintenance releases' +notes for details). + + * "git repack --depth=<n>" for a long time busted the specified depth + when reusing delta from existing packs. This has been corrected. + (merge 42b766d765 jk/delta-chain-limit later to maint). + + * The code to parse the command line "git grep <patterns>... <rev> + [[--] <pathspec>...]" has been cleaned up, and a handful of bugs + have been fixed (e.g. we used to check "--" if it is a rev). + (merge 131f3c96d2 jk/grep-no-index-fix later to maint). + + * "git ls-remote" and "git archive --remote" are designed to work + without being in a directory under Git's control. However, recent + updates revealed that we randomly look into a directory called + .git/ without actually doing necessary set-up when working in a + repository. Stop doing so. + (merge 4b0c3c7735 jn/remote-helpers-with-git-dir later to maint). + + * "git show-branch" expected there were only very short branch names + in the repository and used a fixed-length buffer to hold them + without checking for overflow. + (merge d3cc5f4c44 jk/show-branch-lift-name-len-limit later to maint). + + * A caller of tempfile API that uses stdio interface to write to + files may ignore errors while writing, which is detected when + tempfile is closed (with a call to ferror()). By that time, the + original errno that may have told us what went wrong is likely to + be long gone and was overwritten by an irrelevant value. + close_tempfile() now resets errno to EIO to make errno at least + predictable. + (merge 7e8c9355b7 jk/tempfile-ferror-fclose-confusion later to maint). + + * "git remote rm X", when a branch has remote X configured as the + value of its branch.*.remote, tried to remove branch.*.remote and + branch.*.merge and failed if either is unset. + (merge 20690b2139 rl/remote-allow-missing-branch-name-merge later to maint). + + * A "gc.log" file left by a backgrounded "gc --auto" disables further + automatic gc; it has been taught to run at least once a day (by + default) by ignoring a stale "gc.log" file that is too old. + (merge a831c06a2b dt/gc-ignore-old-gc-logs later to maint). + + * The code to parse "git -c VAR=VAL cmd" and set configuration + variable for the duration of cmd had two small bugs, which have + been fixed. + (merge 1274a155af jc/config-case-cmdline-take-2 later to maint). + + * user.email that consists of only cruft chars should consistently + error out, but didn't. + (merge 94425552f3 jk/ident-empty later to maint). + + * "git upload-pack", which is a counter-part of "git fetch", did not + report a request for a ref that was not advertised as invalid. + This is generally not a problem (because "git fetch" will stop + before making such a request), but is the right thing to do. + (merge bdb31eada7 jt/upload-pack-error-report later to maint). + + * A leak in a codepath to read from a packed object in (rare) cases + has been plugged. + (merge 886ddf4777 rs/sha1-file-plug-fallback-base-leak later to maint). + + * When a redirected http transport gets an error during the + redirected request, we ignored the error we got from the server, + and ended up giving a not-so-useful error message. + (merge 8e27391a5f jt/http-base-url-update-upon-redirect later to maint). + + * Other minor doc, test and build updates and code cleanups. + (merge 2cfa83574c mm/two-more-xstrfmt later to maint). + (merge b803ae4427 ps/docs-diffcore later to maint). + (merge bcd886d897 ew/markdown-url-in-readme later to maint). + (merge b2d593a779 rj/remove-unused-mktemp later to maint). diff --git a/Documentation/config.txt b/Documentation/config.txt index 015346c417..47603f5484 100644 --- a/Documentation/config.txt +++ b/Documentation/config.txt @@ -1402,6 +1402,12 @@ gc.autoDetach:: Make `git gc --auto` return immediately and run in background if the system supports it. Default is true. +gc.logExpiry:: + If the file gc.log exists, then `git gc --auto` won't run + unless that file is more than 'gc.logExpiry' old. Default is + "1.day". See `gc.pruneExpire` for more ways to specify its + value. + gc.packRefs:: Running `git pack-refs` in a repository renders it unclonable by Git versions prior to 1.5.1.2 over dumb @@ -1919,7 +1925,10 @@ http.<url>.*:: must match exactly between the config key and the URL. . Host/domain name (e.g., `example.com` in `https://example.com/`). - This field must match exactly between the config key and the URL. + This field must match between the config key and the URL. It is + possible to specify a `*` as part of the host name to match all subdomains + at this level. `https://*.example.com/` for example would match + `https://foo.example.com/`, but not `https://foo.bar.example.com/`. . Port number (e.g., `8080` in `http://example.com:8080/`). This field must match exactly between the config key and the URL. @@ -1954,6 +1963,17 @@ Environment variable settings always override any matches. The URLs that are matched against are those given directly to Git commands. This means any URLs visited as a result of a redirection do not participate in matching. +ssh.variant:: + Depending on the value of the environment variables `GIT_SSH` or + `GIT_SSH_COMMAND`, or the config setting `core.sshCommand`, Git + auto-detects whether to adjust its command-line parameters for use + with plink or tortoiseplink, as opposed to the default (OpenSSH). ++ +The config variable `ssh.variant` can be set to override this auto-detection; +valid values are `ssh`, `plink`, `putty` or `tortoiseplink`. Any other value +will be treated as normal ssh. This setting can be overridden via the +environment variable `GIT_SSH_VARIANT`. + i18n.commitEncoding:: Character encoding the commit messages are stored in; Git itself does not care per se, but this information is necessary e.g. when diff --git a/Documentation/git-branch.txt b/Documentation/git-branch.txt index 28d46cc03b..092f1bcf9f 100644 --- a/Documentation/git-branch.txt +++ b/Documentation/git-branch.txt @@ -12,7 +12,7 @@ SYNOPSIS [--list] [-v [--abbrev=<length> | --no-abbrev]] [--column[=<options>] | --no-column] [(--merged | --no-merged | --contains) [<commit>]] [--sort=<key>] - [--points-at <object>] [<pattern>...] + [--points-at <object>] [--format=<format>] [<pattern>...] 'git branch' [--set-upstream | --track | --no-track] [-l] [-f] <branchname> [<start-point>] 'git branch' (--set-upstream-to=<upstream> | -u <upstream>) [<branchname>] 'git branch' --unset-upstream [<branchname>] @@ -253,6 +253,11 @@ start-point is either a local or remote-tracking branch. --points-at <object>:: Only list branches of the given object. +--format <format>:: + A string that interpolates `%(fieldname)` from the object + pointed at by a ref being shown. The format is the same as + that of linkgit:git-for-each-ref[1]. + Examples -------- diff --git a/Documentation/git-describe.txt b/Documentation/git-describe.txt index e4ac448ff5..8755f3af7b 100644 --- a/Documentation/git-describe.txt +++ b/Documentation/git-describe.txt @@ -83,7 +83,20 @@ OPTIONS --match <pattern>:: Only consider tags matching the given `glob(7)` pattern, excluding the "refs/tags/" prefix. This can be used to avoid - leaking private tags from the repository. + leaking private tags from the repository. If given multiple times, a + list of patterns will be accumulated, and tags matching any of the + patterns will be considered. Use `--no-match` to clear and reset the + list of patterns. + +--exclude <pattern>:: + Do not consider tags matching the given `glob(7)` pattern, excluding + the "refs/tags/" prefix. This can be used to narrow the tag space and + find only tags matching some meaningful criteria. If given multiple + times, a list of patterns will be accumulated and tags matching any + of the patterns will be excluded. When combined with --match a tag will + be considered when it matches at least one --match pattern and does not + match any of the --exclude patterns. Use `--no-exclude` to clear and + reset the list of patterns. --always:: Show uniquely abbreviated commit object as fallback. diff --git a/Documentation/git-for-each-ref.txt b/Documentation/git-for-each-ref.txt index abe13f3bed..111e1be6f5 100644 --- a/Documentation/git-for-each-ref.txt +++ b/Documentation/git-for-each-ref.txt @@ -95,11 +95,20 @@ refname:: The name of the ref (the part after $GIT_DIR/). For a non-ambiguous short name of the ref append `:short`. The option core.warnAmbiguousRefs is used to select the strict - abbreviation mode. If `strip=<N>` is appended, strips `<N>` - slash-separated path components from the front of the refname - (e.g., `%(refname:strip=2)` turns `refs/tags/foo` into `foo`. - `<N>` must be a positive integer. If a displayed ref has fewer - components than `<N>`, the command aborts with an error. + abbreviation mode. If `lstrip=<N>` (`rstrip=<N>`) is appended, strips `<N>` + slash-separated path components from the front (back) of the refname + (e.g. `%(refname:lstrip=2)` turns `refs/tags/foo` into `foo` and + `%(refname:rstrip=2)` turns `refs/tags/foo` into `refs`). + If `<N>` is a negative number, strip as many path components as + necessary from the specified end to leave `-<N>` path components + (e.g. `%(refname:lstrip=-2)` turns + `refs/tags/foo` into `tags/foo` and `%(refname:rstrip=-1)` + turns `refs/tags/foo` into `refs`). When the ref does not have + enough components, the result becomes an empty string if + stripping with positive <N>, or it becomes the full refname if + stripping with negative <N>. Neither is an error. ++ +`strip` can be used as a synomym to `lstrip`. objecttype:: The type of the object (`blob`, `tree`, `commit`, `tag`). @@ -110,21 +119,31 @@ objectsize:: objectname:: The object name (aka SHA-1). For a non-ambiguous abbreviation of the object name append `:short`. + For an abbreviation of the object name with desired length append + `:short=<length>`, where the minimum length is MINIMUM_ABBREV. The + length may be exceeded to ensure unique object names. upstream:: The name of a local ref which can be considered ``upstream'' - from the displayed ref. Respects `:short` in the same way as - `refname` above. Additionally respects `:track` to show - "[ahead N, behind M]" and `:trackshort` to show the terse - version: ">" (ahead), "<" (behind), "<>" (ahead and behind), - or "=" (in sync). Has no effect if the ref does not have - tracking information associated with it. + from the displayed ref. Respects `:short`, `:lstrip` and + `:rstrip` in the same way as `refname` above. Additionally + respects `:track` to show "[ahead N, behind M]" and + `:trackshort` to show the terse version: ">" (ahead), "<" + (behind), "<>" (ahead and behind), or "=" (in sync). `:track` + also prints "[gone]" whenever unknown upstream ref is + encountered. Append `:track,nobracket` to show tracking + information without brackets (i.e "ahead N, behind M"). Has + no effect if the ref does not have tracking information + associated with it. All the options apart from `nobracket` + are mutually exclusive, but if used together the last option + is selected. push:: - The name of a local ref which represents the `@{push}` location - for the displayed ref. Respects `:short`, `:track`, and - `:trackshort` options as `upstream` does. Produces an empty - string if no `@{push}` ref is configured. + The name of a local ref which represents the `@{push}` + location for the displayed ref. Respects `:short`, `:lstrip`, + `:rstrip`, `:track`, and `:trackshort` options as `upstream` + does. Produces an empty string if no `@{push}` ref is + configured. HEAD:: '*' if HEAD matches current ref (the checked out branch), ' ' @@ -149,6 +168,25 @@ align:: quoted, but if nested then only the topmost level performs quoting. +if:: + Used as %(if)...%(then)...%(end) or + %(if)...%(then)...%(else)...%(end). If there is an atom with + value or string literal after the %(if) then everything after + the %(then) is printed, else if the %(else) atom is used, then + everything after %(else) is printed. We ignore space when + evaluating the string before %(then), this is useful when we + use the %(HEAD) atom which prints either "*" or " " and we + want to apply the 'if' condition only on the 'HEAD' ref. + Append ":equals=<string>" or ":notequals=<string>" to compare + the value between the %(if:...) and %(then) atoms with the + given string. + +symref:: + The ref which the given symbolic ref refers to. If not a + symbolic ref, nothing is printed. Respects the `:short`, + `:lstrip` and `:rstrip` options in the same way as `refname` + above. + In addition to the above, for commit and tag objects, the header field names (`tree`, `parent`, `object`, `type`, and `tag`) can be used to specify the value in the header field. @@ -186,6 +224,14 @@ As a special case for the date-type fields, you may specify a format for the date by adding `:` followed by date format name (see the values the `--date` option to linkgit:git-rev-list[1] takes). +Some atoms like %(align) and %(if) always require a matching %(end). +We call them "opening atoms" and sometimes denote them as %($open). + +When a scripting language specific quoting is in effect, everything +between a top-level opening atom and its matching %(end) is evaluated +according to the semantics of the opening atom and only its result +from the top-level is quoted. + EXAMPLES -------- @@ -273,6 +319,22 @@ eval=`git for-each-ref --shell --format="$fmt" \ eval "$eval" ------------ + +An example to show the usage of %(if)...%(then)...%(else)...%(end). +This prefixes the current branch with a star. + +------------ +git for-each-ref --format="%(if)%(HEAD)%(then)* %(else) %(end)%(refname:short)" refs/heads/ +------------ + + +An example to show the usage of %(if)...%(then)...%(end). +This prints the authorname, if present. + +------------ +git for-each-ref --format="%(refname)%(if)%(authorname)%(then) Authored by: %(authorname)%(end)" +------------ + SEE ALSO -------- linkgit:git-show-ref[1] diff --git a/Documentation/git-name-rev.txt b/Documentation/git-name-rev.txt index ca28fb8e2a..e8e68f528c 100644 --- a/Documentation/git-name-rev.txt +++ b/Documentation/git-name-rev.txt @@ -26,7 +26,18 @@ OPTIONS --refs=<pattern>:: Only use refs whose names match a given shell pattern. The pattern - can be one of branch name, tag name or fully qualified ref name. + can be one of branch name, tag name or fully qualified ref name. If + given multiple times, use refs whose names match any of the given shell + patterns. Use `--no-refs` to clear any previous ref patterns given. + +--exclude=<pattern>:: + Do not use any ref whose name matches a given shell pattern. The + pattern can be one of branch name, tag name or fully qualified ref + name. If given multiple times, a ref will be excluded when it matches + any of the given patterns. When used together with --refs, a ref will + be used as a match only when it matches at least one --refs pattern and + does not match any --exclude patterns. Use `--no-exclude` to clear the + list of exclude patterns. --all:: List all commits reachable from all refs diff --git a/Documentation/git-rev-parse.txt b/Documentation/git-rev-parse.txt index 7241e96893..91c02b8c85 100644 --- a/Documentation/git-rev-parse.txt +++ b/Documentation/git-rev-parse.txt @@ -217,6 +217,10 @@ 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`. diff --git a/Documentation/git-stash.txt b/Documentation/git-stash.txt index 2e9e344cd7..70191d06b6 100644 --- a/Documentation/git-stash.txt +++ b/Documentation/git-stash.txt @@ -13,8 +13,11 @@ SYNOPSIS 'git stash' drop [-q|--quiet] [<stash>] 'git stash' ( pop | apply ) [--index] [-q|--quiet] [<stash>] 'git stash' branch <branchname> [<stash>] -'git stash' [save [-p|--patch] [-k|--[no-]keep-index] [-q|--quiet] - [-u|--include-untracked] [-a|--all] [<message>]] +'git stash' save [-p|--patch] [-k|--[no-]keep-index] [-q|--quiet] + [-u|--include-untracked] [-a|--all] [<message>] +'git stash' [push [-p|--patch] [-k|--[no-]keep-index] [-q|--quiet] + [-u|--include-untracked] [-a|--all] [-m|--message <message>]] + [--] [<pathspec>...]] 'git stash' clear 'git stash' create [<message>] 'git stash' store [-m|--message <message>] [-q|--quiet] <commit> @@ -46,14 +49,24 @@ OPTIONS ------- save [-p|--patch] [-k|--[no-]keep-index] [-u|--include-untracked] [-a|--all] [-q|--quiet] [<message>]:: +push [-p|--patch] [-k|--[no-]keep-index] [-u|--include-untracked] [-a|--all] [-q|--quiet] [-m|--message <message>] [--] [<pathspec>...]:: Save your local modifications to a new 'stash' and roll them back to HEAD (in the working tree and in the index). The <message> part is optional and gives - the description along with the stashed state. For quickly making - a snapshot, you can omit _both_ "save" and <message>, but giving - only <message> does not trigger this action to prevent a misspelled - subcommand from making an unwanted stash. + the description along with the stashed state. ++ +For quickly making a snapshot, you can omit "push". In this mode, +non-option arguments are not allowed to prevent a misspelled +subcommand from making an unwanted stash. The two exceptions to this +are `stash -p` which acts as alias for `stash push -p` and pathspecs, +which are allowed after a double hyphen `--` for disambiguation. ++ +When pathspec is given to 'git stash push', the new stash records the +modified states only for the files that match the pathspec. The index +entries and working tree files are then rolled back to the state in +HEAD only for these files, too, leaving files that do not match the +pathspec intact. + If the `--keep-index` option is used, all changes already added to the index are left intact. diff --git a/Documentation/git-submodule.txt b/Documentation/git-submodule.txt index 8acc72ebb8..e05d0cddef 100644 --- a/Documentation/git-submodule.txt +++ b/Documentation/git-submodule.txt @@ -73,13 +73,17 @@ configuration entries unless `--name` is used to specify a logical name. + <repository> is the URL of the new submodule's origin repository. This may be either an absolute URL, or (if it begins with ./ -or ../), the location relative to the superproject's origin +or ../), the location relative to the superproject's default remote repository (Please note that to specify a repository 'foo.git' which is located right next to a superproject 'bar.git', you'll have to use '../foo.git' instead of './foo.git' - as one might expect when following the rules for relative URLs - because the evaluation of relative URLs in Git is identical to that of relative directories). -If the superproject doesn't have an origin configured ++ +The default remote is the remote of the remote tracking branch +of the current branch. If no such remote tracking branch exists or +the HEAD is detached, "origin" is assumed to be the default remote. +If the superproject doesn't have a default remote configured the superproject is its own authoritative upstream and the current working directory is used instead. + @@ -118,18 +122,24 @@ too (and can also report changes to a submodule's work tree). init [--] [<path>...]:: Initialize the submodules recorded in the index (which were - added and committed elsewhere) by copying submodule - names and urls from .gitmodules to .git/config. - Optional <path> arguments limit which submodules will be initialized. - It will also copy the value of `submodule.$name.update` into - .git/config. - The key used in .git/config is `submodule.$name.url`. - This command does not alter existing information in .git/config. - You can then customize the submodule clone URLs in .git/config - for your local setup and proceed to `git submodule update`; - you can also just use `git submodule update --init` without - the explicit 'init' step if you do not intend to customize - any submodule locations. + added and committed elsewhere) by setting `submodule.$name.url` + in .git/config. It uses the same setting from .gitmodules as + a template. If the URL is relative, it will be resolved using + the default remote. If there is no default remote, the current + repository will be assumed to be upstream. ++ +Optional <path> arguments limit which submodules will be initialized. +If no path is specified, all submodules are initialized. ++ +When present, it will also copy the value of `submodule.$name.update`. +This command does not alter existing information in .git/config. +You can then customize the submodule clone URLs in .git/config +for your local setup and proceed to `git submodule update`; +you can also just use `git submodule update --init` without +the explicit 'init' step if you do not intend to customize +any submodule locations. ++ +See the add subcommand for the defintion of default remote. deinit [-f|--force] (--all|[--] <path>...):: Unregister the given submodules, i.e. remove the whole diff --git a/Documentation/git.txt b/Documentation/git.txt index aa895da4a5..df0941d456 100644 --- a/Documentation/git.txt +++ b/Documentation/git.txt @@ -1025,6 +1025,12 @@ Usually it is easier to configure any desired options through your personal `.ssh/config` file. Please consult your ssh documentation for further details. +`GIT_SSH_VARIANT`:: + If this environment variable is set, it overrides Git's autodetection + whether `GIT_SSH`/`GIT_SSH_COMMAND`/`core.sshCommand` refer to OpenSSH, + plink or tortoiseplink. This variable overrides the config setting + `ssh.variant` that serves the same purpose. + `GIT_ASKPASS`:: If this environment variable is set, then Git commands which need to acquire passwords or passphrases (e.g. for HTTP or IMAP authentication) diff --git a/Documentation/gitattributes.txt b/Documentation/gitattributes.txt index e0b66c1220..a53d093ca1 100644 --- a/Documentation/gitattributes.txt +++ b/Documentation/gitattributes.txt @@ -21,9 +21,11 @@ Each line in `gitattributes` file is of form: pattern attr1 attr2 ... That is, a pattern followed by an attributes list, -separated by whitespaces. When the pattern matches the -path in question, the attributes listed on the line are given to -the path. +separated by whitespaces. Leading and trailing whitespaces are +ignored. Lines that begin with '#' are ignored. Patterns +that begin with a double quote are quoted in C style. +When the pattern matches the path in question, the attributes +listed on the line are given to the path. Each attribute can be in one of these states for a given path: @@ -86,7 +88,7 @@ is either not set or empty, $HOME/.config/git/attributes is used instead. Attributes for all users on a system should be placed in the `$(prefix)/etc/gitattributes` file. -Sometimes you would need to override an setting of an attribute +Sometimes you would need to override a setting of an attribute for a path to `Unspecified` state. This can be done by listing the name of the attribute prefixed with an exclamation point `!`. diff --git a/Documentation/gitdiffcore.txt b/Documentation/gitdiffcore.txt index 46bc6d077c..c0a60f3158 100644 --- a/Documentation/gitdiffcore.txt +++ b/Documentation/gitdiffcore.txt @@ -84,8 +84,8 @@ format sections of the manual for 'git diff-{asterisk}' commands) or diff-patch format. -diffcore-break: For Splitting Up "Complete Rewrites" ----------------------------------------------------- +diffcore-break: For Splitting Up Complete Rewrites +-------------------------------------------------- The second transformation in the chain is diffcore-break, and is controlled by the -B option to the 'git diff-{asterisk}' commands. This is @@ -119,7 +119,7 @@ the original is used), and can be customized by giving a number after "-B" option (e.g. "-B75" to tell it to use 75%). -diffcore-rename: For Detection Renames and Copies +diffcore-rename: For Detecting Renames and Copies ------------------------------------------------- This transformation is used to detect renames and copies, and is @@ -177,8 +177,8 @@ the expense of making it slower. Without `--find-copies-harder`, copied happened to have been modified in the same changeset. -diffcore-merge-broken: For Putting "Complete Rewrites" Back Together --------------------------------------------------------------------- +diffcore-merge-broken: For Putting Complete Rewrites Back Together +------------------------------------------------------------------ This transformation is used to merge filepairs broken by diffcore-break, and not transformed into rename/copy by diff --git a/Documentation/glossary-content.txt b/Documentation/glossary-content.txt index 8ad29e61a9..fc9320e59e 100644 --- a/Documentation/glossary-content.txt +++ b/Documentation/glossary-content.txt @@ -386,8 +386,10 @@ Glob magic is incompatible with literal magic. exclude;; After a path matches any non-exclude pathspec, it will be run - through all exclude pathspec (magic signature: `!`). If it - matches, the path is ignored. + through all exclude pathspec (magic signature: `!` or its + synonym `^`). If it matches, the path is ignored. When there + is no non-exclude pathspec, the exclusion is applied to the + result set as if invoked without any pathspec. -- [[def_parent]]parent:: diff --git a/Documentation/technical/api-gitattributes.txt b/Documentation/technical/api-gitattributes.txt index 2602668677..e7cbb7c13a 100644 --- a/Documentation/technical/api-gitattributes.txt +++ b/Documentation/technical/api-gitattributes.txt @@ -16,10 +16,15 @@ Data Structure of no interest to the calling programs. The name of the attribute can be retrieved by calling `git_attr_name()`. -`struct git_attr_check`:: +`struct attr_check_item`:: - This structure represents a set of attributes to check in a call - to `git_check_attr()` function, and receives the results. + This structure represents one attribute and its value. + +`struct attr_check`:: + + This structure represents a collection of `attr_check_item`. + It is passed to `git_check_attr()` function, specifying the + attributes to check, and receives their values. Attribute Values @@ -27,7 +32,7 @@ Attribute Values An attribute for a path can be in one of four states: Set, Unset, Unspecified or set to a string, and `.value` member of `struct -git_attr_check` records it. There are three macros to check these: +attr_check_item` records it. There are three macros to check these: `ATTR_TRUE()`:: @@ -48,49 +53,51 @@ value of the attribute for the path. Querying Specific Attributes ---------------------------- -* Prepare an array of `struct git_attr_check` to define the list of - attributes you would want to check. To populate this array, you would - need to define necessary attributes by calling `git_attr()` function. +* Prepare `struct attr_check` using attr_check_initl() + function, enumerating the names of attributes whose values you are + interested in, terminated with a NULL pointer. Alternatively, an + empty `struct attr_check` can be prepared by calling + `attr_check_alloc()` function and then attributes you want to + ask about can be added to it with `attr_check_append()` + function. * Call `git_check_attr()` to check the attributes for the path. -* Inspect `git_attr_check` structure to see how each of the attribute in - the array is defined for the path. +* Inspect `attr_check` structure to see how each of the + attribute in the array is defined for the path. Example ------- -To see how attributes "crlf" and "indent" are set for different paths. +To see how attributes "crlf" and "ident" are set for different paths. -. Prepare an array of `struct git_attr_check` with two elements (because - we are checking two attributes). Initialize their `attr` member with - pointers to `struct git_attr` obtained by calling `git_attr()`: +. Prepare a `struct attr_check` with two elements (because + we are checking two attributes): ------------ -static struct git_attr_check check[2]; +static struct attr_check *check; static void setup_check(void) { - if (check[0].attr) + if (check) return; /* already done */ - check[0].attr = git_attr("crlf"); - check[1].attr = git_attr("ident"); + check = attr_check_initl("crlf", "ident", NULL); } ------------ -. Call `git_check_attr()` with the prepared array of `struct git_attr_check`: +. Call `git_check_attr()` with the prepared `struct attr_check`: ------------ const char *path; setup_check(); - git_check_attr(path, ARRAY_SIZE(check), check); + git_check_attr(path, check); ------------ -. Act on `.value` member of the result, left in `check[]`: +. Act on `.value` member of the result, left in `check->items[]`: ------------ - const char *value = check[0].value; + const char *value = check->items[0].value; if (ATTR_TRUE(value)) { The attribute is Set, by listing only the name of the @@ -109,20 +116,39 @@ static void setup_check(void) } ------------ +To see how attributes in argv[] are set for different paths, only +the first step in the above would be different. + +------------ +static struct attr_check *check; +static void setup_check(const char **argv) +{ + check = attr_check_alloc(); + while (*argv) { + struct git_attr *attr = git_attr(*argv); + attr_check_append(check, attr); + argv++; + } +} +------------ + Querying All Attributes ----------------------- To get the values of all attributes associated with a file: -* Call `git_all_attrs()`, which returns an array of `git_attr_check` - structures. +* Prepare an empty `attr_check` structure by calling + `attr_check_alloc()`. + +* Call `git_all_attrs()`, which populates the `attr_check` + with the attributes attached to the path. -* Iterate over the `git_attr_check` array to examine the attribute - names and values. The name of the attribute described by a - `git_attr_check` object can be retrieved via - `git_attr_name(check[i].attr)`. (Please note that no items will be - returned for unset attributes, so `ATTR_UNSET()` will return false - for all returned `git_array_check` objects.) +* Iterate over the `attr_check.items[]` array to examine + the attribute names and values. The name of the attribute + described by a `attr_check.items[]` object can be retrieved via + `git_attr_name(check->items[i].attr)`. (Please note that no items + will be returned for unset attributes, so `ATTR_UNSET()` will return + false for all returned `attr_check.items[]` objects.) -* Free the `git_array_check` array. +* Free the `attr_check` struct by calling `attr_check_free()`. diff --git a/Documentation/technical/api-parse-options.txt b/Documentation/technical/api-parse-options.txt index 27bd701c0d..36768b479e 100644 --- a/Documentation/technical/api-parse-options.txt +++ b/Documentation/technical/api-parse-options.txt @@ -168,6 +168,11 @@ There are some macros to easily define options: Introduce an option with string argument. The string argument is put into `str_var`. +`OPT_STRING_LIST(short, long, &struct string_list, arg_str, description)`:: + Introduce an option with string argument. + The string argument is stored as an element in `string_list`. + Use of `--no-option` will clear the list of preceding values. + `OPT_INTEGER(short, long, &int_var, description)`:: Introduce an option with integer argument. The integer is put into `int_var`. diff --git a/GIT-VERSION-GEN b/GIT-VERSION-GEN index 6a208e92bf..817d1cf7ef 100755 --- a/GIT-VERSION-GEN +++ b/GIT-VERSION-GEN @@ -1,7 +1,7 @@ #!/bin/sh GVF=GIT-VERSION-FILE -DEF_VER=v2.12.0 +DEF_VER=v2.12.GIT LF=' ' @@ -102,8 +102,6 @@ all:: # # Define MKDIR_WO_TRAILING_SLASH if your mkdir() can't deal with trailing slash. # -# Define NO_MKSTEMPS if you don't have mkstemps in the C library. -# # Define NO_GECOS_IN_PWENT if you don't have pw_gecos in struct passwd # in the C library. # @@ -781,6 +779,7 @@ LIB_OBJS += notes-cache.o LIB_OBJS += notes-merge.o LIB_OBJS += notes-utils.o LIB_OBJS += object.o +LIB_OBJS += oidset.o LIB_OBJS += pack-bitmap.o LIB_OBJS += pack-bitmap-write.o LIB_OBJS += pack-check.o @@ -932,6 +931,7 @@ BUILTIN_OBJS += builtin/prune.o BUILTIN_OBJS += builtin/pull.o BUILTIN_OBJS += builtin/push.o BUILTIN_OBJS += builtin/read-tree.o +BUILTIN_OBJS += builtin/rebase--helper.o BUILTIN_OBJS += builtin/receive-pack.o BUILTIN_OBJS += builtin/reflog.o BUILTIN_OBJS += builtin/remote.o @@ -1280,9 +1280,6 @@ ifdef MKDIR_WO_TRAILING_SLASH COMPAT_CFLAGS += -DMKDIR_WO_TRAILING_SLASH COMPAT_OBJS += compat/mkdir.o endif -ifdef NO_MKSTEMPS - COMPAT_CFLAGS += -DNO_MKSTEMPS -endif ifdef NO_UNSETENV COMPAT_CFLAGS += -DNO_UNSETENV COMPAT_OBJS += compat/unsetenv.o @@ -12,7 +12,7 @@ Torvalds with help of a group of hackers around the net. Please read the file [INSTALL][] for installation instructions. -Many Git online resources are accessible from http://git-scm.com/ +Many Git online resources are accessible from <https://git-scm.com/> including full documentation and Git related tools. See [Documentation/gittutorial.txt][] to get started, then see @@ -33,8 +33,8 @@ requests, comments and patches to git@vger.kernel.org (read [Documentation/SubmittingPatches][] for instructions on patch submission). To subscribe to the list, send an email with just "subscribe git" in the body to majordomo@vger.kernel.org. The mailing list archives are -available at https://public-inbox.org/git, -http://marc.info/?l=git and other archival sites. +available at <https://public-inbox.org/git/>, +<http://marc.info/?l=git> and other archival sites. The maintainer frequently sends the "What's cooking" reports that list the current status of various development topics to the mailing @@ -1 +1 @@ -Documentation/RelNotes/2.12.0.txt
\ No newline at end of file +Documentation/RelNotes/2.13.0.txt
\ No newline at end of file @@ -87,19 +87,6 @@ void *sha1_file_to_archive(const struct archiver_args *args, return buffer; } -static void setup_archive_check(struct git_attr_check *check) -{ - static struct git_attr *attr_export_ignore; - static struct git_attr *attr_export_subst; - - if (!attr_export_ignore) { - attr_export_ignore = git_attr("export-ignore"); - attr_export_subst = git_attr("export-subst"); - } - check[0].attr = attr_export_ignore; - check[1].attr = attr_export_subst; -} - struct directory { struct directory *up; struct object_id oid; @@ -120,10 +107,10 @@ static int write_archive_entry(const unsigned char *sha1, const char *base, void *context) { static struct strbuf path = STRBUF_INIT; + static struct attr_check *check; struct archiver_context *c = context; struct archiver_args *args = c->args; write_archive_entry_fn_t write_entry = c->write_entry; - struct git_attr_check check[2]; const char *path_without_prefix; int err; @@ -137,11 +124,12 @@ static int write_archive_entry(const unsigned char *sha1, const char *base, strbuf_addch(&path, '/'); path_without_prefix = path.buf + args->baselen; - setup_archive_check(check); - if (!git_check_attr(path_without_prefix, ARRAY_SIZE(check), check)) { - if (ATTR_TRUE(check[0].value)) + if (!check) + check = attr_check_initl("export-ignore", "export-subst", NULL); + if (!git_check_attr(path_without_prefix, check)) { + if (ATTR_TRUE(check->items[0].value)) return 0; - args->convert = ATTR_TRUE(check[1].value); + args->convert = ATTR_TRUE(check->items[1].value); } if (S_ISDIR(mode) || S_ISGITLINK(mode)) { @@ -13,6 +13,8 @@ #include "attr.h" #include "dir.h" #include "utf8.h" +#include "quote.h" +#include "thread-utils.h" const char git_attr__true[] = "(builtin)true"; const char git_attr__false[] = "\0(builtin)false"; @@ -22,99 +24,234 @@ static const char git_attr__unknown[] = "(builtin)unknown"; #define ATTR__UNSET NULL #define ATTR__UNKNOWN git_attr__unknown -/* This is a randomly chosen prime. */ -#define HASHSIZE 257 - #ifndef DEBUG_ATTR #define DEBUG_ATTR 0 #endif struct git_attr { - struct git_attr *next; - unsigned h; - int attr_nr; - int maybe_macro; - int maybe_real; - char name[FLEX_ARRAY]; + int attr_nr; /* unique attribute number */ + char name[FLEX_ARRAY]; /* attribute name */ }; -static int attr_nr; -static int cannot_trust_maybe_real; - -static struct git_attr_check *check_all_attr; -static struct git_attr *(git_attr_hash[HASHSIZE]); -char *git_attr_name(struct git_attr *attr) +const char *git_attr_name(const struct git_attr *attr) { return attr->name; } -static unsigned hash_name(const char *name, int namelen) +struct attr_hashmap { + struct hashmap map; +#ifndef NO_PTHREADS + pthread_mutex_t mutex; +#endif +}; + +static inline void hashmap_lock(struct attr_hashmap *map) +{ +#ifndef NO_PTHREADS + pthread_mutex_lock(&map->mutex); +#endif +} + +static inline void hashmap_unlock(struct attr_hashmap *map) { - unsigned val = 0, c; +#ifndef NO_PTHREADS + pthread_mutex_unlock(&map->mutex); +#endif +} - while (namelen--) { - c = *name++; - val = ((val << 7) | (val >> 22)) ^ c; +/* + * The global dictionary of all interned attributes. This + * is a singleton object which is shared between threads. + * Access to this dictionary must be surrounded with a mutex. + */ +static struct attr_hashmap g_attr_hashmap; + +/* The container for objects stored in "struct attr_hashmap" */ +struct attr_hash_entry { + struct hashmap_entry ent; /* must be the first member! */ + const char *key; /* the key; memory should be owned by value */ + size_t keylen; /* length of the key */ + void *value; /* the stored value */ +}; + +/* attr_hashmap comparison function */ +static int attr_hash_entry_cmp(const struct attr_hash_entry *a, + const struct attr_hash_entry *b, + void *unused) +{ + return (a->keylen != b->keylen) || strncmp(a->key, b->key, a->keylen); +} + +/* Initialize an 'attr_hashmap' object */ +static void attr_hashmap_init(struct attr_hashmap *map) +{ + hashmap_init(&map->map, (hashmap_cmp_fn) attr_hash_entry_cmp, 0); +} + +/* + * Retrieve the 'value' stored in a hashmap given the provided 'key'. + * If there is no matching entry, return NULL. + */ +static void *attr_hashmap_get(struct attr_hashmap *map, + const char *key, size_t keylen) +{ + struct attr_hash_entry k; + struct attr_hash_entry *e; + + if (!map->map.tablesize) + attr_hashmap_init(map); + + hashmap_entry_init(&k, memhash(key, keylen)); + k.key = key; + k.keylen = keylen; + e = hashmap_get(&map->map, &k, NULL); + + return e ? e->value : NULL; +} + +/* Add 'value' to a hashmap based on the provided 'key'. */ +static void attr_hashmap_add(struct attr_hashmap *map, + const char *key, size_t keylen, + void *value) +{ + struct attr_hash_entry *e; + + if (!map->map.tablesize) + attr_hashmap_init(map); + + e = xmalloc(sizeof(struct attr_hash_entry)); + hashmap_entry_init(e, memhash(key, keylen)); + e->key = key; + e->keylen = keylen; + e->value = value; + + hashmap_add(&map->map, e); +} + +struct all_attrs_item { + const struct git_attr *attr; + const char *value; + /* + * If 'macro' is non-NULL, indicates that 'attr' is a macro based on + * the current attribute stack and contains a pointer to the match_attr + * definition of the macro + */ + const struct match_attr *macro; +}; + +/* + * Reallocate and reinitialize the array of all attributes (which is used in + * the attribute collection process) in 'check' based on the global dictionary + * of attributes. + */ +static void all_attrs_init(struct attr_hashmap *map, struct attr_check *check) +{ + int i; + + hashmap_lock(map); + + if (map->map.size < check->all_attrs_nr) + die("BUG: interned attributes shouldn't be deleted"); + + /* + * If the number of attributes in the global dictionary has increased + * (or this attr_check instance doesn't have an initialized all_attrs + * field), reallocate the provided attr_check instance's all_attrs + * field and fill each entry with its corresponding git_attr. + */ + if (map->map.size != check->all_attrs_nr) { + struct attr_hash_entry *e; + struct hashmap_iter iter; + hashmap_iter_init(&map->map, &iter); + + REALLOC_ARRAY(check->all_attrs, map->map.size); + check->all_attrs_nr = map->map.size; + + while ((e = hashmap_iter_next(&iter))) { + const struct git_attr *a = e->value; + check->all_attrs[a->attr_nr].attr = a; + } + } + + hashmap_unlock(map); + + /* + * Re-initialize every entry in check->all_attrs. + * This re-initialization can live outside of the locked region since + * the attribute dictionary is no longer being accessed. + */ + for (i = 0; i < check->all_attrs_nr; i++) { + check->all_attrs[i].value = ATTR__UNKNOWN; + check->all_attrs[i].macro = NULL; } - return val; } -static int invalid_attr_name(const char *name, int namelen) +static int attr_name_valid(const char *name, size_t namelen) { /* * Attribute name cannot begin with '-' and must consist of * characters from [-A-Za-z0-9_.]. */ if (namelen <= 0 || *name == '-') - return -1; + return 0; while (namelen--) { char ch = *name++; if (! (ch == '-' || ch == '.' || ch == '_' || ('0' <= ch && ch <= '9') || ('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z')) ) - return -1; + return 0; } - return 0; + return 1; } -static struct git_attr *git_attr_internal(const char *name, int len) +static void report_invalid_attr(const char *name, size_t len, + const char *src, int lineno) +{ + struct strbuf err = STRBUF_INIT; + strbuf_addf(&err, _("%.*s is not a valid attribute name"), + (int) len, name); + fprintf(stderr, "%s: %s:%d\n", err.buf, src, lineno); + strbuf_release(&err); +} + +/* + * Given a 'name', lookup and return the corresponding attribute in the global + * dictionary. If no entry is found, create a new attribute and store it in + * the dictionary. + */ +static const struct git_attr *git_attr_internal(const char *name, int namelen) { - unsigned hval = hash_name(name, len); - unsigned pos = hval % HASHSIZE; struct git_attr *a; - for (a = git_attr_hash[pos]; a; a = a->next) { - if (a->h == hval && - !memcmp(a->name, name, len) && !a->name[len]) - return a; + if (!attr_name_valid(name, namelen)) + return NULL; + + hashmap_lock(&g_attr_hashmap); + + a = attr_hashmap_get(&g_attr_hashmap, name, namelen); + + if (!a) { + FLEX_ALLOC_MEM(a, name, name, namelen); + a->attr_nr = g_attr_hashmap.map.size; + + attr_hashmap_add(&g_attr_hashmap, a->name, namelen, a); + assert(a->attr_nr == (g_attr_hashmap.map.size - 1)); } - if (invalid_attr_name(name, len)) - return NULL; + hashmap_unlock(&g_attr_hashmap); - FLEX_ALLOC_MEM(a, name, name, len); - a->h = hval; - a->next = git_attr_hash[pos]; - a->attr_nr = attr_nr++; - a->maybe_macro = 0; - a->maybe_real = 0; - git_attr_hash[pos] = a; - - REALLOC_ARRAY(check_all_attr, attr_nr); - check_all_attr[a->attr_nr].attr = a; - check_all_attr[a->attr_nr].value = ATTR__UNKNOWN; return a; } -struct git_attr *git_attr(const char *name) +const struct git_attr *git_attr(const char *name) { return git_attr_internal(name, strlen(name)); } /* What does a matched pattern decide? */ struct attr_state { - struct git_attr *attr; + const struct git_attr *attr; const char *setto; }; @@ -131,9 +268,8 @@ struct pattern { * If is_macro is true, then u.attr is a pointer to the git_attr being * defined. * - * If is_macro is false, then u.pattern points at the filename pattern - * to which the rule applies. (The memory pointed to is part of the - * memory block allocated for the match_attr instance.) + * If is_macro is false, then u.pat is the filename pattern to which the + * rule applies. * * In either case, num_attr is the number of attributes affected by * this rule, and state is an array listing them. The attributes are @@ -142,7 +278,7 @@ struct pattern { struct match_attr { union { struct pattern pat; - struct git_attr *attr; + const struct git_attr *attr; } u; char is_macro; unsigned num_attr; @@ -177,13 +313,17 @@ static const char *parse_attr(const char *src, int lineno, const char *cp, cp++; len--; } - if (invalid_attr_name(cp, len)) { - fprintf(stderr, - "%.*s is not a valid attribute name: %s:%d\n", - len, cp, src, lineno); + if (!attr_name_valid(cp, len)) { + report_invalid_attr(cp, len, src, lineno); return NULL; } } else { + /* + * As this function is always called twice, once with + * e == NULL in the first pass and then e != NULL in + * the second pass, no need for attr_name_valid() + * check here. + */ if (*cp == '-' || *cp == '!') { e->setto = (*cp == '-') ? ATTR__FALSE : ATTR__UNSET; cp++; @@ -207,41 +347,47 @@ static struct match_attr *parse_attr_line(const char *line, const char *src, const char *cp, *name, *states; struct match_attr *res = NULL; int is_macro; + struct strbuf pattern = STRBUF_INIT; cp = line + strspn(line, blank); if (!*cp || *cp == '#') return NULL; name = cp; - namelen = strcspn(name, blank); + + if (*cp == '"' && !unquote_c_style(&pattern, name, &states)) { + name = pattern.buf; + namelen = pattern.len; + } else { + namelen = strcspn(name, blank); + states = name + namelen; + } + if (strlen(ATTRIBUTE_MACRO_PREFIX) < namelen && starts_with(name, ATTRIBUTE_MACRO_PREFIX)) { if (!macro_ok) { fprintf(stderr, "%s not allowed: %s:%d\n", name, src, lineno); - return NULL; + goto fail_return; } is_macro = 1; name += strlen(ATTRIBUTE_MACRO_PREFIX); name += strspn(name, blank); namelen = strcspn(name, blank); - if (invalid_attr_name(name, namelen)) { - fprintf(stderr, - "%.*s is not a valid attribute name: %s:%d\n", - namelen, name, src, lineno); - return NULL; + if (!attr_name_valid(name, namelen)) { + report_invalid_attr(name, namelen, src, lineno); + goto fail_return; } } else is_macro = 0; - states = name + namelen; states += strspn(states, blank); /* First pass to count the attr_states */ for (cp = states, num_attr = 0; *cp; num_attr++) { cp = parse_attr(src, lineno, cp, NULL); if (!cp) - return NULL; + goto fail_return; } res = xcalloc(1, @@ -250,7 +396,6 @@ static struct match_attr *parse_attr_line(const char *line, const char *src, (is_macro ? 0 : namelen + 1)); if (is_macro) { res->u.attr = git_attr_internal(name, namelen); - res->u.attr->maybe_macro = 1; } else { char *p = (char *)&(res->state[num_attr]); memcpy(p, name, namelen); @@ -262,7 +407,7 @@ static struct match_attr *parse_attr_line(const char *line, const char *src, if (res->u.pat.flags & EXC_FLAG_NEGATIVE) { warning(_("Negative patterns are ignored in git attributes\n" "Use '\\!' for literal leading exclamation.")); - return NULL; + goto fail_return; } } res->is_macro = is_macro; @@ -271,13 +416,15 @@ static struct match_attr *parse_attr_line(const char *line, const char *src, /* Second pass to fill the attr_states */ for (cp = states, i = 0; *cp; i++) { cp = parse_attr(src, lineno, cp, &(res->state[i])); - if (!is_macro) - res->state[i].attr->maybe_real = 1; - if (res->state[i].attr->maybe_macro) - cannot_trust_maybe_real = 1; } + strbuf_release(&pattern); return res; + +fail_return: + strbuf_release(&pattern); + free(res); + return NULL; } /* @@ -295,19 +442,19 @@ static struct match_attr *parse_attr_line(const char *line, const char *src, * directory (again, reading the file from top to bottom) down to the * current directory, and then scan the list backwards to find the first match. * This is exactly the same as what is_excluded() does in dir.c to deal with - * .gitignore + * .gitignore file and info/excludes file as a fallback. */ -static struct attr_stack { +struct attr_stack { struct attr_stack *prev; char *origin; size_t originlen; unsigned num_matches; unsigned alloc; struct match_attr **attrs; -} *attr_stack; +}; -static void free_attr_elem(struct attr_stack *e) +static void attr_stack_free(struct attr_stack *e) { int i; free(e->origin); @@ -330,6 +477,173 @@ static void free_attr_elem(struct attr_stack *e) free(e); } +static void drop_attr_stack(struct attr_stack **stack) +{ + while (*stack) { + struct attr_stack *elem = *stack; + *stack = elem->prev; + attr_stack_free(elem); + } +} + +/* List of all attr_check structs; access should be surrounded by mutex */ +static struct check_vector { + size_t nr; + size_t alloc; + struct attr_check **checks; +#ifndef NO_PTHREADS + pthread_mutex_t mutex; +#endif +} check_vector; + +static inline void vector_lock(void) +{ +#ifndef NO_PTHREADS + pthread_mutex_lock(&check_vector.mutex); +#endif +} + +static inline void vector_unlock(void) +{ +#ifndef NO_PTHREADS + pthread_mutex_unlock(&check_vector.mutex); +#endif +} + +static void check_vector_add(struct attr_check *c) +{ + vector_lock(); + + ALLOC_GROW(check_vector.checks, + check_vector.nr + 1, + check_vector.alloc); + check_vector.checks[check_vector.nr++] = c; + + vector_unlock(); +} + +static void check_vector_remove(struct attr_check *check) +{ + int i; + + vector_lock(); + + /* Find entry */ + for (i = 0; i < check_vector.nr; i++) + if (check_vector.checks[i] == check) + break; + + if (i >= check_vector.nr) + die("BUG: no entry found"); + + /* shift entries over */ + for (; i < check_vector.nr - 1; i++) + check_vector.checks[i] = check_vector.checks[i + 1]; + + check_vector.nr--; + + vector_unlock(); +} + +/* Iterate through all attr_check instances and drop their stacks */ +static void drop_all_attr_stacks(void) +{ + int i; + + vector_lock(); + + for (i = 0; i < check_vector.nr; i++) { + drop_attr_stack(&check_vector.checks[i]->stack); + } + + vector_unlock(); +} + +struct attr_check *attr_check_alloc(void) +{ + struct attr_check *c = xcalloc(1, sizeof(struct attr_check)); + + /* save pointer to the check struct */ + check_vector_add(c); + + return c; +} + +struct attr_check *attr_check_initl(const char *one, ...) +{ + struct attr_check *check; + int cnt; + va_list params; + const char *param; + + va_start(params, one); + for (cnt = 1; (param = va_arg(params, const char *)) != NULL; cnt++) + ; + va_end(params); + + check = attr_check_alloc(); + check->nr = cnt; + check->alloc = cnt; + check->items = xcalloc(cnt, sizeof(struct attr_check_item)); + + check->items[0].attr = git_attr(one); + va_start(params, one); + for (cnt = 1; cnt < check->nr; cnt++) { + const struct git_attr *attr; + param = va_arg(params, const char *); + if (!param) + die("BUG: counted %d != ended at %d", + check->nr, cnt); + attr = git_attr(param); + if (!attr) + die("BUG: %s: not a valid attribute name", param); + check->items[cnt].attr = attr; + } + va_end(params); + return check; +} + +struct attr_check_item *attr_check_append(struct attr_check *check, + const struct git_attr *attr) +{ + struct attr_check_item *item; + + ALLOC_GROW(check->items, check->nr + 1, check->alloc); + item = &check->items[check->nr++]; + item->attr = attr; + return item; +} + +void attr_check_reset(struct attr_check *check) +{ + check->nr = 0; +} + +void attr_check_clear(struct attr_check *check) +{ + free(check->items); + check->items = NULL; + check->alloc = 0; + check->nr = 0; + + free(check->all_attrs); + check->all_attrs = NULL; + check->all_attrs_nr = 0; + + drop_attr_stack(&check->stack); +} + +void attr_check_free(struct attr_check *check) +{ + if (check) { + /* Remove check from the check vector */ + check_vector_remove(check); + + attr_check_clear(check); + free(check); + } +} + static const char *builtin_attr[] = { "[attr]binary -diff -merge -text", NULL, @@ -362,9 +676,31 @@ static struct attr_stack *read_attr_from_array(const char **list) return res; } +/* + * Callers into the attribute system assume there is a single, system-wide + * global state where attributes are read from and when the state is flipped by + * calling git_attr_set_direction(), the stack frames that have been + * constructed need to be discarded so so that subsequent calls into the + * attribute system will lazily read from the right place. Since changing + * direction causes a global paradigm shift, it should not ever be called while + * another thread could potentially be calling into the attribute system. + */ static enum git_attr_direction direction; static struct index_state *use_index; +void git_attr_set_direction(enum git_attr_direction new_direction, + struct index_state *istate) +{ + if (is_bare_repository() && new_direction != GIT_ATTR_INDEX) + die("BUG: non-INDEX attr direction in a bare repo"); + + if (new_direction != direction) + drop_all_attr_stacks(); + + direction = new_direction; + use_index = istate; +} + static struct attr_stack *read_attr_from_file(const char *path, int macro_ok) { FILE *fp = fopen(path, "r"); @@ -402,8 +738,8 @@ static struct attr_stack *read_attr_from_index(const char *path, int macro_ok) for (sp = buf; *sp; ) { char *ep; int more; - for (ep = sp; *ep && *ep != '\n'; ep++) - ; + + ep = strchrnul(sp, '\n'); more = (*ep == '\n'); *ep = '\0'; handle_attr_line(res, sp, path, ++lineno, macro_ok); @@ -415,25 +751,28 @@ static struct attr_stack *read_attr_from_index(const char *path, int macro_ok) static struct attr_stack *read_attr(const char *path, int macro_ok) { - struct attr_stack *res; + struct attr_stack *res = NULL; - if (direction == GIT_ATTR_CHECKOUT) { + if (direction == GIT_ATTR_INDEX) { res = read_attr_from_index(path, macro_ok); - if (!res) - res = read_attr_from_file(path, macro_ok); - } - else if (direction == GIT_ATTR_CHECKIN) { - res = read_attr_from_file(path, macro_ok); - if (!res) - /* - * There is no checked out .gitattributes file there, but - * we might have it in the index. We allow operation in a - * sparsely checked out work tree, so read from it. - */ + } else if (!is_bare_repository()) { + if (direction == GIT_ATTR_CHECKOUT) { res = read_attr_from_index(path, macro_ok); + if (!res) + res = read_attr_from_file(path, macro_ok); + } else if (direction == GIT_ATTR_CHECKIN) { + res = read_attr_from_file(path, macro_ok); + if (!res) + /* + * There is no checked out .gitattributes file + * there, but we might have it in the index. + * We allow operation in a sparsely checked out + * work tree, so read from it. + */ + res = read_attr_from_index(path, macro_ok); + } } - else - res = read_attr_from_index(path, macro_ok); + if (!res) res = xcalloc(1, sizeof(*res)); return res; @@ -464,16 +803,7 @@ static void debug_set(const char *what, const char *match, struct git_attr *attr #define debug_push(a) do { ; } while (0) #define debug_pop(a) do { ; } while (0) #define debug_set(a,b,c,d) do { ; } while (0) -#endif - -static void drop_attr_stack(void) -{ - while (attr_stack) { - struct attr_stack *elem = attr_stack; - attr_stack = elem->prev; - free_attr_elem(elem); - } -} +#endif /* DEBUG_ATTR */ static const char *git_etc_gitattributes(void) { @@ -483,6 +813,14 @@ static const char *git_etc_gitattributes(void) return system_wide; } +static const char *get_home_gitattributes(void) +{ + if (!git_attributes_file) + git_attributes_file = xdg_config_home("attributes"); + + return git_attributes_file; +} + static int git_attr_system(void) { return !git_env_bool("GIT_ATTR_NOSYSTEM", 0); @@ -490,64 +828,60 @@ static int git_attr_system(void) static GIT_PATH_FUNC(git_path_info_attributes, INFOATTRIBUTES_FILE) -static void bootstrap_attr_stack(void) +static void push_stack(struct attr_stack **attr_stack_p, + struct attr_stack *elem, char *origin, size_t originlen) { - struct attr_stack *elem; + if (elem) { + elem->origin = origin; + if (origin) + elem->originlen = originlen; + elem->prev = *attr_stack_p; + *attr_stack_p = elem; + } +} - if (attr_stack) +static void bootstrap_attr_stack(struct attr_stack **stack) +{ + struct attr_stack *e; + + if (*stack) return; - elem = read_attr_from_array(builtin_attr); - elem->origin = NULL; - elem->prev = attr_stack; - attr_stack = elem; + /* builtin frame */ + e = read_attr_from_array(builtin_attr); + push_stack(stack, e, NULL, 0); + /* system-wide frame */ if (git_attr_system()) { - elem = read_attr_from_file(git_etc_gitattributes(), 1); - if (elem) { - elem->origin = NULL; - elem->prev = attr_stack; - attr_stack = elem; - } + e = read_attr_from_file(git_etc_gitattributes(), 1); + push_stack(stack, e, NULL, 0); } - if (!git_attributes_file) - git_attributes_file = xdg_config_home("attributes"); - if (git_attributes_file) { - elem = read_attr_from_file(git_attributes_file, 1); - if (elem) { - elem->origin = NULL; - elem->prev = attr_stack; - attr_stack = elem; - } + /* home directory */ + if (get_home_gitattributes()) { + e = read_attr_from_file(get_home_gitattributes(), 1); + push_stack(stack, e, NULL, 0); } - if (!is_bare_repository() || direction == GIT_ATTR_INDEX) { - elem = read_attr(GITATTRIBUTES_FILE, 1); - elem->origin = xstrdup(""); - elem->originlen = 0; - elem->prev = attr_stack; - attr_stack = elem; - debug_push(elem); - } + /* root directory */ + e = read_attr(GITATTRIBUTES_FILE, 1); + push_stack(stack, e, xstrdup(""), 0); + /* info frame */ if (startup_info->have_repository) - elem = read_attr_from_file(git_path_info_attributes(), 1); + e = read_attr_from_file(git_path_info_attributes(), 1); else - elem = NULL; - - if (!elem) - elem = xcalloc(1, sizeof(*elem)); - elem->origin = NULL; - elem->prev = attr_stack; - attr_stack = elem; + e = NULL; + if (!e) + e = xcalloc(1, sizeof(struct attr_stack)); + push_stack(stack, e, NULL, 0); } -static void prepare_attr_stack(const char *path, int dirlen) +static void prepare_attr_stack(const char *path, int dirlen, + struct attr_stack **stack) { - struct attr_stack *elem, *info; - int len; - const char *cp; + struct attr_stack *info; + struct strbuf pathbuf = STRBUF_INIT; /* * At the bottom of the attribute stack is the built-in @@ -564,13 +898,13 @@ static void prepare_attr_stack(const char *path, int dirlen) * .gitattributes in deeper directories to shallower ones, * and finally use the built-in set as the default. */ - bootstrap_attr_stack(); + bootstrap_attr_stack(stack); /* * Pop the "info" one that is always at the top of the stack. */ - info = attr_stack; - attr_stack = info->prev; + info = *stack; + *stack = info->prev; /* * Pop the ones from directories that are not the prefix of @@ -578,59 +912,63 @@ static void prepare_attr_stack(const char *path, int dirlen) * the root one (whose origin is an empty string "") or the builtin * one (whose origin is NULL) without popping it. */ - while (attr_stack->origin) { - int namelen = strlen(attr_stack->origin); + while ((*stack)->origin) { + int namelen = (*stack)->originlen; + struct attr_stack *elem; - elem = attr_stack; + elem = *stack; if (namelen <= dirlen && !strncmp(elem->origin, path, namelen) && (!namelen || path[namelen] == '/')) break; debug_pop(elem); - attr_stack = elem->prev; - free_attr_elem(elem); + *stack = elem->prev; + attr_stack_free(elem); } /* - * Read from parent directories and push them down + * bootstrap_attr_stack() should have added, and the + * above loop should have stopped before popping, the + * root element whose attr_stack->origin is set to an + * empty string. */ - if (!is_bare_repository() || direction == GIT_ATTR_INDEX) { - /* - * bootstrap_attr_stack() should have added, and the - * above loop should have stopped before popping, the - * root element whose attr_stack->origin is set to an - * empty string. - */ - struct strbuf pathbuf = STRBUF_INIT; - - assert(attr_stack->origin); - while (1) { - len = strlen(attr_stack->origin); - if (dirlen <= len) - break; - cp = memchr(path + len + 1, '/', dirlen - len - 1); - if (!cp) - cp = path + dirlen; - strbuf_add(&pathbuf, path, cp - path); + assert((*stack)->origin); + + strbuf_addstr(&pathbuf, (*stack)->origin); + /* Build up to the directory 'path' is in */ + while (pathbuf.len < dirlen) { + size_t len = pathbuf.len; + struct attr_stack *next; + char *origin; + + /* Skip path-separator */ + if (len < dirlen && is_dir_sep(path[len])) + len++; + /* Find the end of the next component */ + while (len < dirlen && !is_dir_sep(path[len])) + len++; + + if (pathbuf.len > 0) strbuf_addch(&pathbuf, '/'); - strbuf_addstr(&pathbuf, GITATTRIBUTES_FILE); - elem = read_attr(pathbuf.buf, 0); - strbuf_setlen(&pathbuf, cp - path); - elem->origin = strbuf_detach(&pathbuf, &elem->originlen); - elem->prev = attr_stack; - attr_stack = elem; - debug_push(elem); - } + strbuf_add(&pathbuf, path + pathbuf.len, (len - pathbuf.len)); + strbuf_addf(&pathbuf, "/%s", GITATTRIBUTES_FILE); + + next = read_attr(pathbuf.buf, 0); - strbuf_release(&pathbuf); + /* reset the pathbuf to not include "/.gitattributes" */ + strbuf_setlen(&pathbuf, len); + + origin = xstrdup(pathbuf.buf); + push_stack(stack, next, origin, len); } /* * Finally push the "info" one at the top of the stack. */ - info->prev = attr_stack; - attr_stack = info; + push_stack(stack, info, NULL, 0); + + strbuf_release(&pathbuf); } static int path_matches(const char *pathname, int pathlen, @@ -656,16 +994,16 @@ static int path_matches(const char *pathname, int pathlen, pattern, prefix, pat->patternlen, pat->flags); } -static int macroexpand_one(int attr_nr, int rem); +static int macroexpand_one(struct all_attrs_item *all_attrs, int nr, int rem); -static int fill_one(const char *what, struct match_attr *a, int rem) +static int fill_one(const char *what, struct all_attrs_item *all_attrs, + const struct match_attr *a, int rem) { - struct git_attr_check *check = check_all_attr; int i; - for (i = a->num_attr - 1; 0 < rem && 0 <= i; i--) { - struct git_attr *attr = a->state[i].attr; - const char **n = &(check[attr->attr_nr].value); + for (i = a->num_attr - 1; rem > 0 && i >= 0; i--) { + const struct git_attr *attr = a->state[i].attr; + const char **n = &(all_attrs[attr->attr_nr].value); const char *v = a->state[i].setto; if (*n == ATTR__UNKNOWN) { @@ -674,64 +1012,72 @@ static int fill_one(const char *what, struct match_attr *a, int rem) attr, v); *n = v; rem--; - rem = macroexpand_one(attr->attr_nr, rem); + rem = macroexpand_one(all_attrs, attr->attr_nr, rem); } } return rem; } static int fill(const char *path, int pathlen, int basename_offset, - struct attr_stack *stk, int rem) + const struct attr_stack *stack, + struct all_attrs_item *all_attrs, int rem) { - int i; - const char *base = stk->origin ? stk->origin : ""; + for (; rem > 0 && stack; stack = stack->prev) { + int i; + const char *base = stack->origin ? stack->origin : ""; - for (i = stk->num_matches - 1; 0 < rem && 0 <= i; i--) { - struct match_attr *a = stk->attrs[i]; - if (a->is_macro) - continue; - if (path_matches(path, pathlen, basename_offset, - &a->u.pat, base, stk->originlen)) - rem = fill_one("fill", a, rem); + for (i = stack->num_matches - 1; 0 < rem && 0 <= i; i--) { + const struct match_attr *a = stack->attrs[i]; + if (a->is_macro) + continue; + if (path_matches(path, pathlen, basename_offset, + &a->u.pat, base, stack->originlen)) + rem = fill_one("fill", all_attrs, a, rem); + } } + return rem; } -static int macroexpand_one(int nr, int rem) +static int macroexpand_one(struct all_attrs_item *all_attrs, int nr, int rem) { - struct attr_stack *stk; - struct match_attr *a = NULL; - int i; + const struct all_attrs_item *item = &all_attrs[nr]; - if (check_all_attr[nr].value != ATTR__TRUE || - !check_all_attr[nr].attr->maybe_macro) + if (item->macro && item->value == ATTR__TRUE) + return fill_one("expand", all_attrs, item->macro, rem); + else return rem; +} - for (stk = attr_stack; !a && stk; stk = stk->prev) - for (i = stk->num_matches - 1; !a && 0 <= i; i--) { - struct match_attr *ma = stk->attrs[i]; - if (!ma->is_macro) - continue; - if (ma->u.attr->attr_nr == nr) - a = ma; +/* + * Marks the attributes which are macros based on the attribute stack. + * This prevents having to search through the attribute stack each time + * a macro needs to be expanded during the fill stage. + */ +static void determine_macros(struct all_attrs_item *all_attrs, + const struct attr_stack *stack) +{ + for (; stack; stack = stack->prev) { + int i; + for (i = stack->num_matches - 1; i >= 0; i--) { + const struct match_attr *ma = stack->attrs[i]; + if (ma->is_macro) { + int n = ma->u.attr->attr_nr; + if (!all_attrs[n].macro) { + all_attrs[n].macro = ma; + } + } } - - if (a) - rem = fill_one("expand", a, rem); - - return rem; + } } /* - * Collect attributes for path into the array pointed to by - * check_all_attr. If num is non-zero, only attributes in check[] are - * collected. Otherwise all attributes are collected. + * Collect attributes for path into the array pointed to by check->all_attrs. + * If check->check_nr is non-zero, only attributes in check[] are collected. + * Otherwise all attributes are collected. */ -static void collect_some_attrs(const char *path, int num, - struct git_attr_check *check) - +static void collect_some_attrs(const char *path, struct attr_check *check) { - struct attr_stack *stk; int i, pathlen, rem, dirlen; const char *cp, *last_slash = NULL; int basename_offset; @@ -749,81 +1095,67 @@ static void collect_some_attrs(const char *path, int num, dirlen = 0; } - prepare_attr_stack(path, dirlen); - for (i = 0; i < attr_nr; i++) - check_all_attr[i].value = ATTR__UNKNOWN; - if (num && !cannot_trust_maybe_real) { + prepare_attr_stack(path, dirlen, &check->stack); + all_attrs_init(&g_attr_hashmap, check); + determine_macros(check->all_attrs, check->stack); + + if (check->nr) { rem = 0; - for (i = 0; i < num; i++) { - if (!check[i].attr->maybe_real) { - struct git_attr_check *c; - c = check_all_attr + check[i].attr->attr_nr; - c->value = ATTR__UNSET; + for (i = 0; i < check->nr; i++) { + int n = check->items[i].attr->attr_nr; + struct all_attrs_item *item = &check->all_attrs[n]; + if (item->macro) { + item->value = ATTR__UNSET; rem++; } } - if (rem == num) + if (rem == check->nr) return; } - rem = attr_nr; - for (stk = attr_stack; 0 < rem && stk; stk = stk->prev) - rem = fill(path, pathlen, basename_offset, stk, rem); + rem = check->all_attrs_nr; + fill(path, pathlen, basename_offset, check->stack, check->all_attrs, rem); } -int git_check_attr(const char *path, int num, struct git_attr_check *check) +int git_check_attr(const char *path, struct attr_check *check) { int i; - collect_some_attrs(path, num, check); + collect_some_attrs(path, check); - for (i = 0; i < num; i++) { - const char *value = check_all_attr[check[i].attr->attr_nr].value; + for (i = 0; i < check->nr; i++) { + size_t n = check->items[i].attr->attr_nr; + const char *value = check->all_attrs[n].value; if (value == ATTR__UNKNOWN) value = ATTR__UNSET; - check[i].value = value; + check->items[i].value = value; } return 0; } -int git_all_attrs(const char *path, int *num, struct git_attr_check **check) +void git_all_attrs(const char *path, struct attr_check *check) { - int i, count, j; + int i; - collect_some_attrs(path, 0, NULL); + attr_check_reset(check); + collect_some_attrs(path, check); - /* Count the number of attributes that are set. */ - count = 0; - for (i = 0; i < attr_nr; i++) { - const char *value = check_all_attr[i].value; - if (value != ATTR__UNSET && value != ATTR__UNKNOWN) - ++count; - } - *num = count; - ALLOC_ARRAY(*check, count); - j = 0; - for (i = 0; i < attr_nr; i++) { - const char *value = check_all_attr[i].value; - if (value != ATTR__UNSET && value != ATTR__UNKNOWN) { - (*check)[j].attr = check_all_attr[i].attr; - (*check)[j].value = value; - ++j; - } + for (i = 0; i < check->all_attrs_nr; i++) { + const char *name = check->all_attrs[i].attr->name; + const char *value = check->all_attrs[i].value; + struct attr_check_item *item; + if (value == ATTR__UNSET || value == ATTR__UNKNOWN) + continue; + item = attr_check_append(check, git_attr(name)); + item->value = value; } - - return 0; } -void git_attr_set_direction(enum git_attr_direction new, struct index_state *istate) +void attr_start(void) { - enum git_attr_direction old = direction; - - if (is_bare_repository() && new != GIT_ATTR_INDEX) - die("BUG: non-INDEX attr direction in a bare repo"); - - direction = new; - if (new != old) - drop_attr_stack(); - use_index = istate; +#ifndef NO_PTHREADS + pthread_mutex_init(&g_attr_hashmap.mutex, NULL); + pthread_mutex_init(&check_vector.mutex, NULL); +#endif } @@ -4,11 +4,15 @@ /* An attribute is a pointer to this opaque structure */ struct git_attr; +/* opaque structures used internally for attribute collection */ +struct all_attrs_item; +struct attr_stack; + /* * Given a string, return the gitattribute object that * corresponds to it. */ -struct git_attr *git_attr(const char *); +const struct git_attr *git_attr(const char *); /* Internal use */ extern const char git_attr__true[]; @@ -20,38 +24,57 @@ extern const char git_attr__false[]; #define ATTR_UNSET(v) ((v) == NULL) /* - * Send one or more git_attr_check to git_check_attr(), and + * Send one or more git_attr_check to git_check_attrs(), and * each 'value' member tells what its value is. * Unset one is returned as NULL. */ -struct git_attr_check { - struct git_attr *attr; +struct attr_check_item { + const struct git_attr *attr; const char *value; }; +struct attr_check { + int nr; + int alloc; + struct attr_check_item *items; + int all_attrs_nr; + struct all_attrs_item *all_attrs; + struct attr_stack *stack; +}; + +extern struct attr_check *attr_check_alloc(void); +extern struct attr_check *attr_check_initl(const char *, ...); + +extern struct attr_check_item *attr_check_append(struct attr_check *check, + const struct git_attr *attr); + +extern void attr_check_reset(struct attr_check *check); +extern void attr_check_clear(struct attr_check *check); +extern void attr_check_free(struct attr_check *check); + /* * Return the name of the attribute represented by the argument. The * return value is a pointer to a null-delimited string that is part * of the internal data structure; it should not be modified or freed. */ -char *git_attr_name(struct git_attr *); +extern const char *git_attr_name(const struct git_attr *); -int git_check_attr(const char *path, int, struct git_attr_check *); +extern int git_check_attr(const char *path, struct attr_check *check); /* - * Retrieve all attributes that apply to the specified path. *num - * will be set to the number of attributes on the path; **check will - * be set to point at a newly-allocated array of git_attr_check - * objects describing the attributes and their values. *check must be - * free()ed by the caller. + * Retrieve all attributes that apply to the specified path. + * check holds the attributes and their values. */ -int git_all_attrs(const char *path, int *num, struct git_attr_check **check); +extern void git_all_attrs(const char *path, struct attr_check *check); enum git_attr_direction { GIT_ATTR_CHECKIN, GIT_ATTR_CHECKOUT, GIT_ATTR_INDEX }; -void git_attr_set_direction(enum git_attr_direction, struct index_state *); +void git_attr_set_direction(enum git_attr_direction new_direction, + struct index_state *istate); + +extern void attr_start(void); #endif /* ATTR_H */ @@ -940,7 +940,7 @@ int bisect_next_all(const char *prefix, int no_checkout) struct commit_list *tried; int reaches = 0, all = 0, nr, steps; const unsigned char *bisect_rev; - char steps_msg[32]; + char *steps_msg; read_bisect_terms(&term_bad, &term_good); if (read_bisect_refs()) @@ -990,14 +990,15 @@ int bisect_next_all(const char *prefix, int no_checkout) nr = all - reaches - 1; steps = estimate_bisect_steps(all); - xsnprintf(steps_msg, sizeof(steps_msg), - Q_("(roughly %d step)", "(roughly %d steps)", steps), - steps); + + steps_msg = xstrfmt(Q_("(roughly %d step)", "(roughly %d steps)", + steps), steps); /* TRANSLATORS: the last %s will be replaced with "(roughly %d steps)" translation */ printf(Q_("Bisecting: %d revision left to test after this %s\n", "Bisecting: %d revisions left to test after this %s\n", nr), nr, steps_msg); + free(steps_msg); return bisect_checkout(bisect_rev, no_checkout); } @@ -345,7 +345,8 @@ void die_if_checked_out(const char *branch, int ignore_current_worktree) branch, wt->path); } -int replace_each_worktree_head_symref(const char *oldref, const char *newref) +int replace_each_worktree_head_symref(const char *oldref, const char *newref, + const char *logmsg) { int ret = 0; struct worktree **worktrees = get_worktrees(0); @@ -358,7 +359,7 @@ int replace_each_worktree_head_symref(const char *oldref, const char *newref) continue; if (set_worktree_head_symref(get_worktree_git_dir(worktrees[i]), - newref)) { + newref, logmsg)) { ret = -1; error(_("HEAD of working tree %s is not updated"), worktrees[i]->path); @@ -71,6 +71,7 @@ extern void die_if_checked_out(const char *branch, int ignore_current_worktree); * This will be used when renaming a branch. Returns 0 if successful, non-zero * otherwise. */ -extern int replace_each_worktree_head_symref(const char *oldref, const char *newref); +extern int replace_each_worktree_head_symref(const char *oldref, const char *newref, + const char *logmsg); #endif @@ -103,6 +103,7 @@ extern int cmd_prune_packed(int argc, const char **argv, const char *prefix); extern int cmd_pull(int argc, const char **argv, const char *prefix); extern int cmd_push(int argc, const char **argv, const char *prefix); extern int cmd_read_tree(int argc, const char **argv, const char *prefix); +extern int cmd_rebase__helper(int argc, const char **argv, const char *prefix); extern int cmd_receive_pack(int argc, const char **argv, const char *prefix); extern int cmd_reflog(int argc, const char **argv, const char *prefix); extern int cmd_remote(int argc, const char **argv, const char *prefix); diff --git a/builtin/am.c b/builtin/am.c index 31fb60578f..f7a7a971fb 100644 --- a/builtin/am.c +++ b/builtin/am.c @@ -1049,7 +1049,7 @@ static void am_setup(struct am_state *state, enum patch_format patch_format, } else { write_state_text(state, "abort-safety", ""); if (!state->rebasing) - delete_ref("ORIG_HEAD", NULL, 0); + delete_ref(NULL, "ORIG_HEAD", NULL, 0); } /* @@ -2172,7 +2172,7 @@ static void am_abort(struct am_state *state) has_curr_head ? &curr_head : NULL, 0, UPDATE_REFS_DIE_ON_ERR); else if (curr_branch) - delete_ref(curr_branch, NULL, REF_NODEREF); + delete_ref(NULL, curr_branch, NULL, REF_NODEREF); free(curr_branch); am_destroy(state); diff --git a/builtin/branch.c b/builtin/branch.c index 9d30f55b0b..94f7de7fa5 100644 --- a/builtin/branch.c +++ b/builtin/branch.c @@ -28,6 +28,7 @@ static const char * const builtin_branch_usage[] = { N_("git branch [<options>] [-r] (-d | -D) <branch-name>..."), N_("git branch [<options>] (-m | -M) [<old-branch>] <new-branch>"), N_("git branch [<options>] [-r | -a] [--points-at]"), + N_("git branch [<options>] [-r | -a] [--format]"), NULL }; @@ -37,11 +38,11 @@ static unsigned char head_sha1[20]; static int branch_use_color = -1; static char branch_colors[][COLOR_MAXLEN] = { GIT_COLOR_RESET, - GIT_COLOR_NORMAL, /* PLAIN */ - GIT_COLOR_RED, /* REMOTE */ - GIT_COLOR_NORMAL, /* LOCAL */ - GIT_COLOR_GREEN, /* CURRENT */ - GIT_COLOR_BLUE, /* UPSTREAM */ + GIT_COLOR_NORMAL, /* PLAIN */ + GIT_COLOR_RED, /* REMOTE */ + GIT_COLOR_NORMAL, /* LOCAL */ + GIT_COLOR_GREEN, /* CURRENT */ + GIT_COLOR_BLUE, /* UPSTREAM */ }; enum color_branch { BRANCH_COLOR_RESET = 0, @@ -251,7 +252,7 @@ static int delete_branches(int argc, const char **argv, int force, int kinds, goto next; } - if (delete_ref(name, is_null_sha1(sha1) ? NULL : sha1, + if (delete_ref(NULL, name, is_null_sha1(sha1) ? NULL : sha1, REF_NODEREF)) { error(remote_branch ? _("Error deleting remote-tracking branch '%s'") @@ -280,221 +281,98 @@ static int delete_branches(int argc, const char **argv, int force, int kinds, return(ret); } -static void fill_tracking_info(struct strbuf *stat, const char *branch_name, - int show_upstream_ref) +static int calc_maxwidth(struct ref_array *refs, int remote_bonus) { - int ours, theirs; - char *ref = NULL; - struct branch *branch = branch_get(branch_name); - const char *upstream; - struct strbuf fancy = STRBUF_INIT; - int upstream_is_gone = 0; - int added_decoration = 1; - - if (stat_tracking_info(branch, &ours, &theirs, &upstream) < 0) { - if (!upstream) - return; - upstream_is_gone = 1; - } - - if (show_upstream_ref) { - ref = shorten_unambiguous_ref(upstream, 0); - if (want_color(branch_use_color)) - strbuf_addf(&fancy, "%s%s%s", - branch_get_color(BRANCH_COLOR_UPSTREAM), - ref, branch_get_color(BRANCH_COLOR_RESET)); - else - strbuf_addstr(&fancy, ref); - } + int i, max = 0; + for (i = 0; i < refs->nr; i++) { + struct ref_array_item *it = refs->items[i]; + const char *desc = it->refname; + int w; - if (upstream_is_gone) { - if (show_upstream_ref) - strbuf_addf(stat, _("[%s: gone]"), fancy.buf); - else - added_decoration = 0; - } else if (!ours && !theirs) { - if (show_upstream_ref) - strbuf_addf(stat, _("[%s]"), fancy.buf); - else - added_decoration = 0; - } else if (!ours) { - if (show_upstream_ref) - strbuf_addf(stat, _("[%s: behind %d]"), fancy.buf, theirs); - else - strbuf_addf(stat, _("[behind %d]"), theirs); + skip_prefix(it->refname, "refs/heads/", &desc); + skip_prefix(it->refname, "refs/remotes/", &desc); + if (it->kind == FILTER_REFS_DETACHED_HEAD) { + char *head_desc = get_head_description(); + w = utf8_strwidth(head_desc); + free(head_desc); + } else + w = utf8_strwidth(desc); - } else if (!theirs) { - if (show_upstream_ref) - strbuf_addf(stat, _("[%s: ahead %d]"), fancy.buf, ours); - else - strbuf_addf(stat, _("[ahead %d]"), ours); - } else { - if (show_upstream_ref) - strbuf_addf(stat, _("[%s: ahead %d, behind %d]"), - fancy.buf, ours, theirs); - else - strbuf_addf(stat, _("[ahead %d, behind %d]"), - ours, theirs); + if (it->kind == FILTER_REFS_REMOTES) + w += remote_bonus; + if (w > max) + max = w; } - strbuf_release(&fancy); - if (added_decoration) - strbuf_addch(stat, ' '); - free(ref); + return max; } -static void add_verbose_info(struct strbuf *out, struct ref_array_item *item, - struct ref_filter *filter, const char *refname) +static const char *quote_literal_for_format(const char *s) { - struct strbuf subject = STRBUF_INIT, stat = STRBUF_INIT; - const char *sub = _(" **** invalid ref ****"); - struct commit *commit = item->commit; + static struct strbuf buf = STRBUF_INIT; - if (!parse_commit(commit)) { - pp_commit_easy(CMIT_FMT_ONELINE, commit, &subject); - sub = subject.buf; + strbuf_reset(&buf); + while (*s) { + const char *ep = strchrnul(s, '%'); + if (s < ep) + strbuf_add(&buf, s, ep - s); + if (*ep == '%') { + strbuf_addstr(&buf, "%%"); + s = ep + 1; + } else { + s = ep; + } } - - if (item->kind == FILTER_REFS_BRANCHES) - fill_tracking_info(&stat, refname, filter->verbose > 1); - - strbuf_addf(out, " %s %s%s", - find_unique_abbrev(item->commit->object.oid.hash, filter->abbrev), - stat.buf, sub); - strbuf_release(&stat); - strbuf_release(&subject); + return buf.buf; } -static char *get_head_description(void) +static char *build_format(struct ref_filter *filter, int maxwidth, const char *remote_prefix) { - struct strbuf desc = STRBUF_INIT; - struct wt_status_state state; - memset(&state, 0, sizeof(state)); - wt_status_get_state(&state, 1); - if (state.rebase_in_progress || - state.rebase_interactive_in_progress) - strbuf_addf(&desc, _("(no branch, rebasing %s)"), - state.branch); - else if (state.bisect_in_progress) - strbuf_addf(&desc, _("(no branch, bisect started on %s)"), - state.branch); - else if (state.detached_from) { - if (state.detached_at) - /* TRANSLATORS: make sure this matches - "HEAD detached at " in wt-status.c */ - strbuf_addf(&desc, _("(HEAD detached at %s)"), - state.detached_from); - else - /* TRANSLATORS: make sure this matches - "HEAD detached from " in wt-status.c */ - strbuf_addf(&desc, _("(HEAD detached from %s)"), - state.detached_from); - } - else - strbuf_addstr(&desc, _("(no branch)")); - free(state.branch); - free(state.onto); - free(state.detached_from); - return strbuf_detach(&desc, NULL); -} + struct strbuf fmt = STRBUF_INIT; + struct strbuf local = STRBUF_INIT; + struct strbuf remote = STRBUF_INIT; -static void format_and_print_ref_item(struct ref_array_item *item, int maxwidth, - struct ref_filter *filter, const char *remote_prefix) -{ - char c; - int current = 0; - int color; - struct strbuf out = STRBUF_INIT, name = STRBUF_INIT; - const char *prefix_to_show = ""; - const char *prefix_to_skip = NULL; - const char *desc = item->refname; - char *to_free = NULL; + strbuf_addf(&fmt, "%%(if)%%(HEAD)%%(then)* %s%%(else) %%(end)", + branch_get_color(BRANCH_COLOR_CURRENT)); - switch (item->kind) { - case FILTER_REFS_BRANCHES: - prefix_to_skip = "refs/heads/"; - skip_prefix(desc, prefix_to_skip, &desc); - if (!filter->detached && !strcmp(desc, head)) - current = 1; + if (filter->verbose) { + strbuf_addf(&local, "%%(align:%d,left)%%(refname:lstrip=2)%%(end)", maxwidth); + strbuf_addf(&local, "%s", branch_get_color(BRANCH_COLOR_RESET)); + strbuf_addf(&local, " %%(objectname:short=7) "); + + if (filter->verbose > 1) + strbuf_addf(&local, "%%(if)%%(upstream)%%(then)[%s%%(upstream:short)%s%%(if)%%(upstream:track)" + "%%(then): %%(upstream:track,nobracket)%%(end)] %%(end)%%(contents:subject)", + branch_get_color(BRANCH_COLOR_UPSTREAM), branch_get_color(BRANCH_COLOR_RESET)); else - color = BRANCH_COLOR_LOCAL; - break; - case FILTER_REFS_REMOTES: - prefix_to_skip = "refs/remotes/"; - skip_prefix(desc, prefix_to_skip, &desc); - color = BRANCH_COLOR_REMOTE; - prefix_to_show = remote_prefix; - break; - case FILTER_REFS_DETACHED_HEAD: - desc = to_free = get_head_description(); - current = 1; - break; - default: - color = BRANCH_COLOR_PLAIN; - break; - } - - c = ' '; - if (current) { - c = '*'; - color = BRANCH_COLOR_CURRENT; - } + strbuf_addf(&local, "%%(if)%%(upstream:track)%%(then)%%(upstream:track) %%(end)%%(contents:subject)"); - strbuf_addf(&name, "%s%s", prefix_to_show, desc); - if (filter->verbose) { - int utf8_compensation = strlen(name.buf) - utf8_strwidth(name.buf); - strbuf_addf(&out, "%c %s%-*s%s", c, branch_get_color(color), - maxwidth + utf8_compensation, name.buf, + strbuf_addf(&remote, "%s%%(align:%d,left)%s%%(refname:lstrip=2)%%(end)%s%%(if)%%(symref)%%(then) -> %%(symref:short)" + "%%(else) %%(objectname:short=7) %%(contents:subject)%%(end)", + branch_get_color(BRANCH_COLOR_REMOTE), maxwidth, quote_literal_for_format(remote_prefix), branch_get_color(BRANCH_COLOR_RESET)); - } else - strbuf_addf(&out, "%c %s%s%s", c, branch_get_color(color), - name.buf, branch_get_color(BRANCH_COLOR_RESET)); - - if (item->symref) { - const char *symref = item->symref; - if (prefix_to_skip) - skip_prefix(symref, prefix_to_skip, &symref); - strbuf_addf(&out, " -> %s", symref); - } - else if (filter->verbose) - /* " f7c0c00 [ahead 58, behind 197] vcs-svn: drop obj_pool.h" */ - add_verbose_info(&out, item, filter, desc); - if (column_active(colopts)) { - assert(!filter->verbose && "--column and --verbose are incompatible"); - string_list_append(&output, out.buf); } else { - printf("%s\n", out.buf); + strbuf_addf(&local, "%%(refname:lstrip=2)%s%%(if)%%(symref)%%(then) -> %%(symref:short)%%(end)", + branch_get_color(BRANCH_COLOR_RESET)); + strbuf_addf(&remote, "%s%s%%(refname:lstrip=2)%s%%(if)%%(symref)%%(then) -> %%(symref:short)%%(end)", + branch_get_color(BRANCH_COLOR_REMOTE), quote_literal_for_format(remote_prefix), + branch_get_color(BRANCH_COLOR_RESET)); } - strbuf_release(&name); - strbuf_release(&out); - free(to_free); -} - -static int calc_maxwidth(struct ref_array *refs, int remote_bonus) -{ - int i, max = 0; - for (i = 0; i < refs->nr; i++) { - struct ref_array_item *it = refs->items[i]; - const char *desc = it->refname; - int w; - skip_prefix(it->refname, "refs/heads/", &desc); - skip_prefix(it->refname, "refs/remotes/", &desc); - w = utf8_strwidth(desc); + strbuf_addf(&fmt, "%%(if:notequals=refs/remotes)%%(refname:rstrip=-2)%%(then)%s%%(else)%s%%(end)", local.buf, remote.buf); - if (it->kind == FILTER_REFS_REMOTES) - w += remote_bonus; - if (w > max) - max = w; - } - return max; + strbuf_release(&local); + strbuf_release(&remote); + return strbuf_detach(&fmt, NULL); } -static void print_ref_list(struct ref_filter *filter, struct ref_sorting *sorting) +static void print_ref_list(struct ref_filter *filter, struct ref_sorting *sorting, const char *format) { int i; struct ref_array array; int maxwidth = 0; const char *remote_prefix = ""; + struct strbuf out = STRBUF_INIT; + char *to_free = NULL; /* * If we are listing more than just remote branches, @@ -506,18 +384,32 @@ static void print_ref_list(struct ref_filter *filter, struct ref_sorting *sortin memset(&array, 0, sizeof(array)); - verify_ref_format("%(refname)%(symref)"); filter_refs(&array, filter, filter->kind | FILTER_REFS_INCLUDE_BROKEN); if (filter->verbose) maxwidth = calc_maxwidth(&array, strlen(remote_prefix)); + if (!format) + format = to_free = build_format(filter, maxwidth, remote_prefix); + verify_ref_format(format); + ref_array_sort(sorting, &array); - for (i = 0; i < array.nr; i++) - format_and_print_ref_item(array.items[i], maxwidth, filter, remote_prefix); + for (i = 0; i < array.nr; i++) { + format_ref_array_item(array.items[i], format, 0, &out); + if (column_active(colopts)) { + assert(!filter->verbose && "--column and --verbose are incompatible"); + /* format to a string_list to let print_columns() do its job */ + string_list_append(&output, out.buf); + } else { + fwrite(out.buf, 1, out.len, stdout); + putchar('\n'); + } + strbuf_release(&out); + } ref_array_clear(&array); + free(to_free); } static void reject_rebase_or_bisect_branch(const char *target) @@ -579,14 +471,15 @@ static void rename_branch(const char *oldname, const char *newname, int force) if (rename_ref(oldref.buf, newref.buf, logmsg.buf)) die(_("Branch rename failed")); - strbuf_release(&logmsg); if (recovery) warning(_("Renamed a misnamed branch '%s' away"), oldref.buf + 11); - if (replace_each_worktree_head_symref(oldref.buf, newref.buf)) + if (replace_each_worktree_head_symref(oldref.buf, newref.buf, logmsg.buf)) die(_("Branch renamed to %s, but HEAD is not updated!"), newname); + strbuf_release(&logmsg); + strbuf_addf(&oldsection, "branch.%s", oldref.buf + 11); strbuf_release(&oldref); strbuf_addf(&newsection, "branch.%s", newref.buf + 11); @@ -638,6 +531,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix) struct ref_filter filter; int icase = 0; static struct ref_sorting *sorting = NULL, **sorting_tail = &sorting; + const char *format = NULL; struct option options[] = { OPT_GROUP(N_("Generic options")), @@ -679,9 +573,12 @@ int cmd_branch(int argc, const char **argv, const char *prefix) N_("print only branches of the object"), 0, parse_opt_object_name }, OPT_BOOL('i', "ignore-case", &icase, N_("sorting and filtering are case insensitive")), + OPT_STRING( 0 , "format", &format, N_("format"), N_("format to use for the output")), OPT_END(), }; + setup_ref_filter_porcelain_msg(); + memset(&filter, 0, sizeof(filter)); filter.kind = FILTER_REFS_BRANCHES; filter.abbrev = -1; @@ -749,7 +646,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix) if (!sorting) sorting = ref_default_sorting(); sorting->ignore_case = icase; - print_ref_list(&filter, sorting); + print_ref_list(&filter, sorting, format); print_columns(&output, colopts, NULL); string_list_clear(&output, 0); return 0; diff --git a/builtin/check-attr.c b/builtin/check-attr.c index 53a5a18c16..4d01ca0c8b 100644 --- a/builtin/check-attr.c +++ b/builtin/check-attr.c @@ -24,12 +24,13 @@ static const struct option check_attr_options[] = { OPT_END() }; -static void output_attr(int cnt, struct git_attr_check *check, - const char *file) +static void output_attr(struct attr_check *check, const char *file) { int j; + int cnt = check->nr; + for (j = 0; j < cnt; j++) { - const char *value = check[j].value; + const char *value = check->items[j].value; if (ATTR_TRUE(value)) value = "set"; @@ -42,35 +43,38 @@ static void output_attr(int cnt, struct git_attr_check *check, printf("%s%c" /* path */ "%s%c" /* attrname */ "%s%c" /* attrvalue */, - file, 0, git_attr_name(check[j].attr), 0, value, 0); + file, 0, + git_attr_name(check->items[j].attr), 0, value, 0); } else { quote_c_style(file, NULL, stdout, 0); - printf(": %s: %s\n", git_attr_name(check[j].attr), value); + printf(": %s: %s\n", + git_attr_name(check->items[j].attr), value); } - } } -static void check_attr(const char *prefix, int cnt, - struct git_attr_check *check, const char *file) +static void check_attr(const char *prefix, + struct attr_check *check, + int collect_all, + const char *file) { char *full_path = prefix_path(prefix, prefix ? strlen(prefix) : 0, file); - if (check != NULL) { - if (git_check_attr(full_path, cnt, check)) - die("git_check_attr died"); - output_attr(cnt, check, file); + + if (collect_all) { + git_all_attrs(full_path, check); } else { - if (git_all_attrs(full_path, &cnt, &check)) - die("git_all_attrs died"); - output_attr(cnt, check, file); - free(check); + if (git_check_attr(full_path, check)) + die("git_check_attr died"); } + output_attr(check, file); + free(full_path); } -static void check_attr_stdin_paths(const char *prefix, int cnt, - struct git_attr_check *check) +static void check_attr_stdin_paths(const char *prefix, + struct attr_check *check, + int collect_all) { struct strbuf buf = STRBUF_INIT; struct strbuf unquoted = STRBUF_INIT; @@ -84,7 +88,7 @@ static void check_attr_stdin_paths(const char *prefix, int cnt, die("line is badly quoted"); strbuf_swap(&buf, &unquoted); } - check_attr(prefix, cnt, check, buf.buf); + check_attr(prefix, check, collect_all, buf.buf); maybe_flush_or_die(stdout, "attribute to stdout"); } strbuf_release(&buf); @@ -99,7 +103,7 @@ static NORETURN void error_with_usage(const char *msg) int cmd_check_attr(int argc, const char **argv, const char *prefix) { - struct git_attr_check *check; + struct attr_check *check; int cnt, i, doubledash, filei; if (!is_bare_repository()) @@ -159,28 +163,26 @@ int cmd_check_attr(int argc, const char **argv, const char *prefix) error_with_usage("No file specified"); } - if (all_attrs) { - check = NULL; - } else { - check = xcalloc(cnt, sizeof(*check)); + check = attr_check_alloc(); + if (!all_attrs) { for (i = 0; i < cnt; i++) { - const char *name; - struct git_attr *a; - name = argv[i]; - a = git_attr(name); + const struct git_attr *a = git_attr(argv[i]); + if (!a) return error("%s: not a valid attribute name", - name); - check[i].attr = a; + argv[i]); + attr_check_append(check, a); } } if (stdin_paths) - check_attr_stdin_paths(prefix, cnt, check); + check_attr_stdin_paths(prefix, check, all_attrs); else { for (i = filei; i < argc; i++) - check_attr(prefix, cnt, check, argv[i]); + check_attr(prefix, check, all_attrs, argv[i]); maybe_flush_or_die(stdout, "attribute to stdout"); } + + attr_check_free(check); return 0; } diff --git a/builtin/clean.c b/builtin/clean.c index d6bc3aaaea..d861f836a2 100644 --- a/builtin/clean.c +++ b/builtin/clean.c @@ -174,8 +174,10 @@ static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag, /* an empty dir could be removed even if it is unreadble */ res = dry_run ? 0 : rmdir(path->buf); if (res) { + int saved_errno = errno; quote_path_relative(path->buf, prefix, "ed); - warning(_(msg_warn_remove_failed), quoted.buf); + errno = saved_errno; + warning_errno(_(msg_warn_remove_failed), quoted.buf); *dir_gone = 0; } return res; @@ -208,8 +210,10 @@ static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag, quote_path_relative(path->buf, prefix, "ed); string_list_append(&dels, quoted.buf); } else { + int saved_errno = errno; quote_path_relative(path->buf, prefix, "ed); - warning(_(msg_warn_remove_failed), quoted.buf); + errno = saved_errno; + warning_errno(_(msg_warn_remove_failed), quoted.buf); *dir_gone = 0; ret = 1; } @@ -230,8 +234,10 @@ static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag, if (!res) *dir_gone = 1; else { + int saved_errno = errno; quote_path_relative(path->buf, prefix, "ed); - warning(_(msg_warn_remove_failed), quoted.buf); + errno = saved_errno; + warning_errno(_(msg_warn_remove_failed), quoted.buf); *dir_gone = 0; ret = 1; } @@ -981,8 +987,10 @@ int cmd_clean(int argc, const char **argv, const char *prefix) } else { res = dry_run ? 0 : unlink(abs_path.buf); if (res) { + int saved_errno = errno; qname = quote_path_relative(item->string, NULL, &buf); - warning(_(msg_warn_remove_failed), qname); + errno = saved_errno; + warning_errno(_(msg_warn_remove_failed), qname); errors++; } else if (!quiet) { qname = quote_path_relative(item->string, NULL, &buf); diff --git a/builtin/describe.c b/builtin/describe.c index 01490a157e..6769446e1f 100644 --- a/builtin/describe.c +++ b/builtin/describe.c @@ -28,7 +28,8 @@ static int abbrev = -1; /* unspecified */ static int max_candidates = 10; static struct hashmap names; static int have_util; -static const char *pattern; +static struct string_list patterns = STRING_LIST_INIT_NODUP; +static struct string_list exclude_patterns = STRING_LIST_INIT_NODUP; static int always; static const char *dirty; @@ -129,9 +130,40 @@ static int get_name(const char *path, const struct object_id *oid, int flag, voi if (!all && !is_tag) return 0; - /* Accept only tags that match the pattern, if given */ - if (pattern && (!is_tag || wildmatch(pattern, path + 10, 0, NULL))) - return 0; + /* + * If we're given exclude patterns, first exclude any tag which match + * any of the exclude pattern. + */ + if (exclude_patterns.nr) { + struct string_list_item *item; + + if (!is_tag) + return 0; + + for_each_string_list_item(item, &exclude_patterns) { + if (!wildmatch(item->string, path + 10, 0, NULL)) + return 0; + } + } + + /* + * If we're given patterns, accept only tags which match at least one + * pattern. + */ + if (patterns.nr) { + struct string_list_item *item; + + if (!is_tag) + return 0; + + for_each_string_list_item(item, &patterns) { + if (!wildmatch(item->string, path + 10, 0, NULL)) + break; + + /* If we get here, no pattern matched. */ + return 0; + } + } /* Is it annotated? */ if (!peel_ref(path, peeled.hash)) { @@ -404,8 +436,10 @@ int cmd_describe(int argc, const char **argv, const char *prefix) N_("only output exact matches"), 0), OPT_INTEGER(0, "candidates", &max_candidates, N_("consider <n> most recent tags (default: 10)")), - OPT_STRING(0, "match", &pattern, N_("pattern"), + OPT_STRING_LIST(0, "match", &patterns, N_("pattern"), N_("only consider tags matching <pattern>")), + OPT_STRING_LIST(0, "exclude", &exclude_patterns, N_("pattern"), + N_("do not consider tags matching <pattern>")), OPT_BOOL(0, "always", &always, N_("show abbreviated commit object as fallback")), {OPTION_STRING, 0, "dirty", &dirty, N_("mark"), @@ -430,6 +464,7 @@ int cmd_describe(int argc, const char **argv, const char *prefix) die(_("--long is incompatible with --abbrev=0")); if (contains) { + struct string_list_item *item; struct argv_array args; argv_array_init(&args); @@ -440,8 +475,10 @@ int cmd_describe(int argc, const char **argv, const char *prefix) argv_array_push(&args, "--always"); if (!all) { argv_array_push(&args, "--tags"); - if (pattern) - argv_array_pushf(&args, "--refs=refs/tags/%s", pattern); + for_each_string_list_item(item, &patterns) + argv_array_pushf(&args, "--refs=refs/tags/%s", item->string); + for_each_string_list_item(item, &exclude_patterns) + argv_array_pushf(&args, "--exclude=refs/tags/%s", item->string); } if (argc) argv_array_pushv(&args, argv); diff --git a/builtin/gc.c b/builtin/gc.c index 331f219260..a2b9e8924e 100644 --- a/builtin/gc.c +++ b/builtin/gc.c @@ -33,6 +33,8 @@ static int aggressive_window = 250; static int gc_auto_threshold = 6700; static int gc_auto_pack_limit = 50; static int detach_auto = 1; +static unsigned long gc_log_expire_time; +static const char *gc_log_expire = "1.day.ago"; static const char *prune_expire = "2.weeks.ago"; static const char *prune_worktrees_expire = "3.months.ago"; @@ -76,10 +78,28 @@ static void git_config_date_string(const char *key, const char **output) static void process_log_file(void) { struct stat st; - if (!fstat(get_lock_file_fd(&log_lock), &st) && st.st_size) + if (fstat(get_lock_file_fd(&log_lock), &st)) { + /* + * Perhaps there was an i/o error or another + * unlikely situation. Try to make a note of + * this in gc.log along with any existing + * messages. + */ + int saved_errno = errno; + fprintf(stderr, _("Failed to fstat %s: %s"), + get_tempfile_path(&log_lock.tempfile), + strerror(saved_errno)); + fflush(stderr); commit_lock_file(&log_lock); - else + errno = saved_errno; + } else if (st.st_size) { + /* There was some error recorded in the lock file */ + commit_lock_file(&log_lock); + } else { + /* No error, clean up any old gc.log */ + unlink(git_path("gc.log")); rollback_lock_file(&log_lock); + } } static void process_log_file_at_exit(void) @@ -113,6 +133,8 @@ static void gc_config(void) git_config_get_bool("gc.autodetach", &detach_auto); git_config_date_string("gc.pruneexpire", &prune_expire); git_config_date_string("gc.worktreepruneexpire", &prune_worktrees_expire); + git_config_date_string("gc.logexpiry", &gc_log_expire); + git_config(git_default_config, NULL); } @@ -290,19 +312,34 @@ static const char *lock_repo_for_gc(int force, pid_t* ret_pid) static int report_last_gc_error(void) { struct strbuf sb = STRBUF_INIT; - int ret; + int ret = 0; + struct stat st; + char *gc_log_path = git_pathdup("gc.log"); - ret = strbuf_read_file(&sb, git_path("gc.log"), 0); + if (stat(gc_log_path, &st)) { + if (errno == ENOENT) + goto done; + + ret = error_errno(_("Can't stat %s"), gc_log_path); + goto done; + } + + if (st.st_mtime < gc_log_expire_time) + goto done; + + ret = strbuf_read_file(&sb, gc_log_path, 0); if (ret > 0) - return error(_("The last gc run reported the following. " + ret = error(_("The last gc run reported the following. " "Please correct the root cause\n" "and remove %s.\n" "Automatic cleanup will not be performed " "until the file is removed.\n\n" "%s"), - git_path("gc.log"), sb.buf); + gc_log_path, sb.buf); strbuf_release(&sb); - return 0; +done: + free(gc_log_path); + return ret; } static int gc_before_repack(void) @@ -349,7 +386,10 @@ int cmd_gc(int argc, const char **argv, const char *prefix) argv_array_pushl(&prune_worktrees, "worktree", "prune", "--expire", NULL); argv_array_pushl(&rerere, "rerere", "gc", NULL); + /* default expiry time, overwritten in gc_config */ gc_config(); + if (parse_expiry_date(gc_log_expire, &gc_log_expire_time)) + die(_("Failed to parse gc.logexpiry value %s"), gc_log_expire); if (pack_refs < 0) pack_refs = !is_bare_repository(); @@ -448,5 +488,8 @@ int cmd_gc(int argc, const char **argv, const char *prefix) warning(_("There are too many unreachable loose objects; " "run 'git prune' to remove them.")); + if (!daemonized) + unlink(git_path("gc.log")); + return 0; } diff --git a/builtin/grep.c b/builtin/grep.c index 2c727ef499..9304c33e75 100644 --- a/builtin/grep.c +++ b/builtin/grep.c @@ -967,6 +967,7 @@ int cmd_grep(int argc, const char **argv, const char *prefix) int dummy; int use_index = 1; int pattern_type_arg = GREP_PATTERN_TYPE_UNSPECIFIED; + int allow_revs; struct option options[] = { OPT_BOOL(0, "cached", &cached, @@ -1149,26 +1150,69 @@ int cmd_grep(int argc, const char **argv, const char *prefix) compile_grep_patterns(&opt); - /* Check revs and then paths */ + /* + * We have to find "--" in a separate pass, because its presence + * influences how we will parse arguments that come before it. + */ + for (i = 0; i < argc; i++) { + if (!strcmp(argv[i], "--")) { + seen_dashdash = 1; + break; + } + } + + /* + * Resolve any rev arguments. If we have a dashdash, then everything up + * to it must resolve as a rev. If not, then we stop at the first + * non-rev and assume everything else is a path. + */ + allow_revs = use_index && !untracked; for (i = 0; i < argc; i++) { const char *arg = argv[i]; unsigned char sha1[20]; struct object_context oc; - /* Is it a rev? */ - if (!get_sha1_with_context(arg, 0, sha1, &oc)) { - struct object *object = parse_object_or_die(sha1, arg); - if (!seen_dashdash) - verify_non_filename(prefix, arg); - add_object_array_with_path(object, arg, &list, oc.mode, oc.path); - continue; - } + struct object *object; + if (!strcmp(arg, "--")) { i++; - seen_dashdash = 1; + break; } - break; + + if (!allow_revs) { + if (seen_dashdash) + die(_("--no-index or --untracked cannot be used with revs")); + break; + } + + if (get_sha1_with_context(arg, 0, sha1, &oc)) { + if (seen_dashdash) + die(_("unable to resolve revision: %s"), arg); + break; + } + + object = parse_object_or_die(sha1, arg); + if (!seen_dashdash) + verify_non_filename(prefix, arg); + add_object_array_with_path(object, arg, &list, oc.mode, oc.path); } + /* + * Anything left over is presumed to be a path. But in the non-dashdash + * "do what I mean" case, we verify and complain when that isn't true. + */ + if (!seen_dashdash) { + int j; + for (j = i; j < argc; j++) + verify_filename(prefix, argv[j], j == i && allow_revs); + } + + parse_pathspec(&pathspec, 0, + PATHSPEC_PREFER_CWD | + (opt.max_depth != -1 ? PATHSPEC_MAXDEPTH_VALID : 0), + prefix, argv + i); + pathspec.max_depth = opt.max_depth; + pathspec.recursive = 1; + #ifndef NO_PTHREADS if (list.nr || cached || show_in_pager) num_threads = 0; @@ -1190,20 +1234,6 @@ int cmd_grep(int argc, const char **argv, const char *prefix) } #endif - /* The rest are paths */ - if (!seen_dashdash) { - int j; - for (j = i; j < argc; j++) - verify_filename(prefix, argv[j], j == i); - } - - parse_pathspec(&pathspec, 0, - PATHSPEC_PREFER_CWD | - (opt.max_depth != -1 ? PATHSPEC_MAXDEPTH_VALID : 0), - prefix, argv + i); - pathspec.max_depth = opt.max_depth; - pathspec.recursive = 1; - if (recurse_submodules) { gitmodules_config(); compile_submodule_options(&opt, &pathspec, cached, untracked, @@ -1245,8 +1275,6 @@ int cmd_grep(int argc, const char **argv, const char *prefix) if (!use_index || untracked) { int use_exclude = (opt_exclude < 0) ? use_index : !!opt_exclude; - if (list.nr) - die(_("--no-index or --untracked cannot be used with revs.")); hit = grep_directory(&opt, &pathspec, use_exclude, use_index); } else if (0 <= opt_exclude) { die(_("--[no-]exclude-standard cannot be used for tracked contents.")); diff --git a/builtin/log.c b/builtin/log.c index 55d20cc2d8..281af8c1ec 100644 --- a/builtin/log.c +++ b/builtin/log.c @@ -989,8 +989,7 @@ static void make_cover_letter(struct rev_info *rev, int use_stdout, open_next_file(NULL, rev->numbered_files ? NULL : "cover-letter", rev, quiet)) return; - log_write_email_headers(rev, head, &pp.subject, &pp.after_subject, - &need_8bit_cte); + log_write_email_headers(rev, head, &pp.after_subject, &need_8bit_cte); for (i = 0; !need_8bit_cte && i < nr; i++) { const char *buf = get_commit_buffer(list[i], NULL); @@ -1005,6 +1004,8 @@ static void make_cover_letter(struct rev_info *rev, int use_stdout, msg = body; pp.fmt = CMIT_FMT_EMAIL; pp.date_mode.type = DATE_RFC2822; + pp.rev = rev; + pp.print_email_subject = 1; pp_user_info(&pp, NULL, &sb, committer, encoding); pp_title_line(&pp, &msg, &sb, encoding, need_8bit_cte); pp_remainder(&pp, &msg, &sb, 0); diff --git a/builtin/name-rev.c b/builtin/name-rev.c index cd89d48b65..8bdc3eaa6f 100644 --- a/builtin/name-rev.c +++ b/builtin/name-rev.c @@ -108,7 +108,8 @@ static const char *name_ref_abbrev(const char *refname, int shorten_unambiguous) struct name_ref_data { int tags_only; int name_only; - const char *ref_filter; + struct string_list ref_filters; + struct string_list exclude_filters; }; static struct tip_table { @@ -150,18 +151,49 @@ static int name_ref(const char *path, const struct object_id *oid, int flags, vo if (data->tags_only && !starts_with(path, "refs/tags/")) return 0; - if (data->ref_filter) { - switch (subpath_matches(path, data->ref_filter)) { - case -1: /* did not match */ - return 0; - case 0: /* matched fully */ - break; - default: /* matched subpath */ - can_abbreviate_output = 1; - break; + if (data->exclude_filters.nr) { + struct string_list_item *item; + + for_each_string_list_item(item, &data->exclude_filters) { + if (subpath_matches(path, item->string) >= 0) + return 0; } } + if (data->ref_filters.nr) { + struct string_list_item *item; + int matched = 0; + + /* See if any of the patterns match. */ + for_each_string_list_item(item, &data->ref_filters) { + /* + * Check all patterns even after finding a match, so + * that we can see if a match with a subpath exists. + * When a user asked for 'refs/tags/v*' and 'v1.*', + * both of which match, the user is showing her + * willingness to accept a shortened output by having + * the 'v1.*' in the acceptable refnames, so we + * shouldn't stop when seeing 'refs/tags/v1.4' matches + * 'refs/tags/v*'. We should show it as 'v1.4'. + */ + switch (subpath_matches(path, item->string)) { + case -1: /* did not match */ + break; + case 0: /* matched fully */ + matched = 1; + break; + default: /* matched subpath */ + matched = 1; + can_abbreviate_output = 1; + break; + } + } + + /* If none of the patterns matched, stop now */ + if (!matched) + return 0; + } + add_to_tip_table(oid->hash, path, can_abbreviate_output); while (o && o->type == OBJ_TAG) { @@ -306,12 +338,14 @@ int cmd_name_rev(int argc, const char **argv, const char *prefix) { struct object_array revs = OBJECT_ARRAY_INIT; int all = 0, transform_stdin = 0, allow_undefined = 1, always = 0, peel_tag = 0; - struct name_ref_data data = { 0, 0, NULL }; + struct name_ref_data data = { 0, 0, STRING_LIST_INIT_NODUP, STRING_LIST_INIT_NODUP }; struct option opts[] = { OPT_BOOL(0, "name-only", &data.name_only, N_("print only names (no SHA-1)")), OPT_BOOL(0, "tags", &data.tags_only, N_("only use tags to name the commits")), - OPT_STRING(0, "refs", &data.ref_filter, N_("pattern"), + OPT_STRING_LIST(0, "refs", &data.ref_filters, N_("pattern"), N_("only use refs matching <pattern>")), + OPT_STRING_LIST(0, "exclude", &data.exclude_filters, N_("pattern"), + N_("ignore refs matching <pattern>")), OPT_GROUP(""), OPT_BOOL(0, "all", &all, N_("list all commits reachable from all refs")), OPT_BOOL(0, "stdin", &transform_stdin, N_("read from stdin")), diff --git a/builtin/notes.c b/builtin/notes.c index 5248a9bad8..4b492abd41 100644 --- a/builtin/notes.c +++ b/builtin/notes.c @@ -681,9 +681,9 @@ static int merge_abort(struct notes_merge_options *o) * notes_merge_abort() to remove .git/NOTES_MERGE_WORKTREE. */ - if (delete_ref("NOTES_MERGE_PARTIAL", NULL, 0)) + if (delete_ref(NULL, "NOTES_MERGE_PARTIAL", NULL, 0)) ret += error(_("failed to delete ref NOTES_MERGE_PARTIAL")); - if (delete_ref("NOTES_MERGE_REF", NULL, REF_NODEREF)) + if (delete_ref(NULL, "NOTES_MERGE_REF", NULL, REF_NODEREF)) ret += error(_("failed to delete ref NOTES_MERGE_REF")); if (notes_merge_abort(o)) ret += error(_("failed to remove 'git notes merge' worktree")); diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c index 8841f8b366..f294dcffa9 100644 --- a/builtin/pack-objects.c +++ b/builtin/pack-objects.c @@ -894,24 +894,15 @@ static void write_pack_file(void) written, nr_result); } -static void setup_delta_attr_check(struct git_attr_check *check) -{ - static struct git_attr *attr_delta; - - if (!attr_delta) - attr_delta = git_attr("delta"); - - check[0].attr = attr_delta; -} - static int no_try_delta(const char *path) { - struct git_attr_check check[1]; + static struct attr_check *check; - setup_delta_attr_check(check); - if (git_check_attr(path, ARRAY_SIZE(check), check)) + if (!check) + check = attr_check_initl("delta", NULL); + if (git_check_attr(path, check)) return 0; - if (ATTR_FALSE(check->value)) + if (ATTR_FALSE(check->items[0].value)) return 1; return 0; } @@ -1539,6 +1530,8 @@ static int pack_offset_sort(const void *_a, const void *_b) * 2. Updating our size/type to the non-delta representation. These were * either not recorded initially (size) or overwritten with the delta type * (type) when check_object() decided to reuse the delta. + * + * 3. Resetting our delta depth, as we are now a base object. */ static void drop_reused_delta(struct object_entry *entry) { @@ -1552,6 +1545,7 @@ static void drop_reused_delta(struct object_entry *entry) p = &(*p)->delta_sibling; } entry->delta = NULL; + entry->depth = 0; oi.sizep = &entry->size; oi.typep = &entry->type; @@ -1570,39 +1564,123 @@ static void drop_reused_delta(struct object_entry *entry) * Follow the chain of deltas from this entry onward, throwing away any links * that cause us to hit a cycle (as determined by the DFS state flags in * the entries). + * + * We also detect too-long reused chains that would violate our --depth + * limit. */ static void break_delta_chains(struct object_entry *entry) { - /* If it's not a delta, it can't be part of a cycle. */ - if (!entry->delta) { - entry->dfs_state = DFS_DONE; - return; - } + /* + * The actual depth of each object we will write is stored as an int, + * as it cannot exceed our int "depth" limit. But before we break + * changes based no that limit, we may potentially go as deep as the + * number of objects, which is elsewhere bounded to a uint32_t. + */ + uint32_t total_depth; + struct object_entry *cur, *next; + + for (cur = entry, total_depth = 0; + cur; + cur = cur->delta, total_depth++) { + if (cur->dfs_state == DFS_DONE) { + /* + * We've already seen this object and know it isn't + * part of a cycle. We do need to append its depth + * to our count. + */ + total_depth += cur->depth; + break; + } + + /* + * We break cycles before looping, so an ACTIVE state (or any + * other cruft which made its way into the state variable) + * is a bug. + */ + if (cur->dfs_state != DFS_NONE) + die("BUG: confusing delta dfs state in first pass: %d", + cur->dfs_state); - switch (entry->dfs_state) { - case DFS_NONE: /* - * This is the first time we've seen the object. We mark it as - * part of the active potential cycle and recurse. + * Now we know this is the first time we've seen the object. If + * it's not a delta, we're done traversing, but we'll mark it + * done to save time on future traversals. */ - entry->dfs_state = DFS_ACTIVE; - break_delta_chains(entry->delta); - entry->dfs_state = DFS_DONE; - break; + if (!cur->delta) { + cur->dfs_state = DFS_DONE; + break; + } - case DFS_DONE: - /* object already examined, and not part of a cycle */ - break; + /* + * Mark ourselves as active and see if the next step causes + * us to cycle to another active object. It's important to do + * this _before_ we loop, because it impacts where we make the + * cut, and thus how our total_depth counter works. + * E.g., We may see a partial loop like: + * + * A -> B -> C -> D -> B + * + * Cutting B->C breaks the cycle. But now the depth of A is + * only 1, and our total_depth counter is at 3. The size of the + * error is always one less than the size of the cycle we + * broke. Commits C and D were "lost" from A's chain. + * + * If we instead cut D->B, then the depth of A is correct at 3. + * We keep all commits in the chain that we examined. + */ + cur->dfs_state = DFS_ACTIVE; + if (cur->delta->dfs_state == DFS_ACTIVE) { + drop_reused_delta(cur); + cur->dfs_state = DFS_DONE; + break; + } + } + + /* + * And now that we've gone all the way to the bottom of the chain, we + * need to clear the active flags and set the depth fields as + * appropriate. Unlike the loop above, which can quit when it drops a + * delta, we need to keep going to look for more depth cuts. So we need + * an extra "next" pointer to keep going after we reset cur->delta. + */ + for (cur = entry; cur; cur = next) { + next = cur->delta; + + /* + * We should have a chain of zero or more ACTIVE states down to + * a final DONE. We can quit after the DONE, because either it + * has no bases, or we've already handled them in a previous + * call. + */ + if (cur->dfs_state == DFS_DONE) + break; + else if (cur->dfs_state != DFS_ACTIVE) + die("BUG: confusing delta dfs state in second pass: %d", + cur->dfs_state); - case DFS_ACTIVE: /* - * We found a cycle that needs broken. It would be correct to - * break any link in the chain, but it's convenient to - * break this one. + * If the total_depth is more than depth, then we need to snip + * the chain into two or more smaller chains that don't exceed + * the maximum depth. Most of the resulting chains will contain + * (depth + 1) entries (i.e., depth deltas plus one base), and + * the last chain (i.e., the one containing entry) will contain + * whatever entries are left over, namely + * (total_depth % (depth + 1)) of them. + * + * Since we are iterating towards decreasing depth, we need to + * decrement total_depth as we go, and we need to write to the + * entry what its final depth will be after all of the + * snipping. Since we're snipping into chains of length (depth + * + 1) entries, the final depth of an entry will be its + * original depth modulo (depth + 1). Any time we encounter an + * entry whose final depth is supposed to be zero, we snip it + * from its delta base, thereby making it so. */ - drop_reused_delta(entry); - entry->dfs_state = DFS_DONE; - break; + cur->depth = (total_depth--) % (depth + 1); + if (!cur->depth) + drop_reused_delta(cur); + + cur->dfs_state = DFS_DONE; } } diff --git a/builtin/rebase--helper.c b/builtin/rebase--helper.c new file mode 100644 index 0000000000..ca1ebb2fa1 --- /dev/null +++ b/builtin/rebase--helper.c @@ -0,0 +1,40 @@ +#include "builtin.h" +#include "cache.h" +#include "parse-options.h" +#include "sequencer.h" + +static const char * const builtin_rebase_helper_usage[] = { + N_("git rebase--helper [<options>]"), + NULL +}; + +int cmd_rebase__helper(int argc, const char **argv, const char *prefix) +{ + struct replay_opts opts = REPLAY_OPTS_INIT; + enum { + CONTINUE = 1, ABORT + } command = 0; + struct option options[] = { + OPT_BOOL(0, "ff", &opts.allow_ff, N_("allow fast-forward")), + OPT_CMDMODE(0, "continue", &command, N_("continue rebase"), + CONTINUE), + OPT_CMDMODE(0, "abort", &command, N_("abort rebase"), + ABORT), + OPT_END() + }; + + git_config(git_default_config, NULL); + + opts.action = REPLAY_INTERACTIVE_REBASE; + opts.allow_ff = 1; + opts.allow_empty = 1; + + argc = parse_options(argc, argv, NULL, options, + builtin_rebase_helper_usage, PARSE_OPT_KEEP_ARGV0); + + if (command == CONTINUE && argc == 1) + return !!sequencer_continue(&opts); + if (command == ABORT && argc == 1) + return !!sequencer_remove_state(&opts); + usage_with_options(builtin_rebase_helper_usage, options); +} diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c index 1dbb8a0692..9ed8fbbfad 100644 --- a/builtin/receive-pack.c +++ b/builtin/receive-pack.c @@ -21,6 +21,7 @@ #include "sigchain.h" #include "fsck.h" #include "tmp-objdir.h" +#include "oidset.h" static const char * const receive_pack_usage[] = { N_("git receive-pack <git-dir>"), @@ -250,8 +251,9 @@ static void show_ref(const char *path, const unsigned char *sha1) } static int show_ref_cb(const char *path_full, const struct object_id *oid, - int flag, void *unused) + int flag, void *data) { + struct oidset *seen = data; const char *path = strip_namespace(path_full); if (ref_is_hidden(path, path_full)) @@ -260,37 +262,38 @@ static int show_ref_cb(const char *path_full, const struct object_id *oid, /* * Advertise refs outside our current namespace as ".have" * refs, so that the client can use them to minimize data - * transfer but will otherwise ignore them. This happens to - * cover ".have" that are thrown in by add_one_alternate_ref() - * to mark histories that are complete in our alternates as - * well. + * transfer but will otherwise ignore them. */ - if (!path) + if (!path) { + if (oidset_insert(seen, oid)) + return 0; path = ".have"; + } else { + oidset_insert(seen, oid); + } show_ref(path, oid->hash); return 0; } -static int show_one_alternate_sha1(const unsigned char sha1[20], void *unused) +static void show_one_alternate_ref(const char *refname, + const struct object_id *oid, + void *data) { - show_ref(".have", sha1); - return 0; -} + struct oidset *seen = data; -static void collect_one_alternate_ref(const struct ref *ref, void *data) -{ - struct sha1_array *sa = data; - sha1_array_append(sa, ref->old_oid.hash); + if (oidset_insert(seen, oid)) + return; + + show_ref(".have", oid->hash); } static void write_head_info(void) { - struct sha1_array sa = SHA1_ARRAY_INIT; + static struct oidset seen = OIDSET_INIT; - for_each_alternate_ref(collect_one_alternate_ref, &sa); - sha1_array_for_each_unique(&sa, show_one_alternate_sha1, NULL); - sha1_array_clear(&sa); - for_each_ref(show_ref_cb, NULL); + for_each_ref(show_ref_cb, &seen); + for_each_alternate_ref(show_one_alternate_ref, &seen); + oidset_clear(&seen); if (!sent_capabilities) show_ref("capabilities^{}", null_sha1); diff --git a/builtin/remote.c b/builtin/remote.c index 5339ed6ad1..addf97ad29 100644 --- a/builtin/remote.c +++ b/builtin/remote.c @@ -691,7 +691,7 @@ static int mv(int argc, const char **argv) read_ref_full(item->string, RESOLVE_REF_READING, oid.hash, &flag); if (!(flag & REF_ISSYMREF)) continue; - if (delete_ref(item->string, NULL, REF_NODEREF)) + if (delete_ref(NULL, item->string, NULL, REF_NODEREF)) die(_("deleting '%s' failed"), item->string); } for (i = 0; i < remote_branches.nr; i++) { @@ -769,7 +769,9 @@ static int rm(int argc, const char **argv) strbuf_reset(&buf); strbuf_addf(&buf, "branch.%s.%s", item->string, *k); - git_config_set(buf.buf, NULL); + result = git_config_set_gently(buf.buf, NULL); + if (result && result != CONFIG_NOTHING_SET) + die(_("could not unset '%s'"), buf.buf); } } } @@ -1248,7 +1250,7 @@ static int set_head(int argc, const char **argv) head_name = xstrdup(states.heads.items[0].string); free_remote_ref_states(&states); } else if (opt_d && !opt_a && argc == 1) { - if (delete_ref(buf.buf, NULL, REF_NODEREF)) + if (delete_ref(NULL, buf.buf, NULL, REF_NODEREF)) result |= error(_("Could not delete %s"), buf.buf); } else usage_with_options(builtin_remote_sethead_usage, options); diff --git a/builtin/replace.c b/builtin/replace.c index b58c714cb8..226d0f9523 100644 --- a/builtin/replace.c +++ b/builtin/replace.c @@ -121,7 +121,7 @@ static int for_each_replace_name(const char **argv, each_replace_name_fn fn) static int delete_replace_ref(const char *name, const char *ref, const unsigned char *sha1) { - if (delete_ref(ref, sha1, 0)) + if (delete_ref(NULL, ref, sha1, 0)) return 1; printf("Deleted replace ref '%s'\n", name); return 0; diff --git a/builtin/reset.c b/builtin/reset.c index 8ab915bfcb..fc3b906c47 100644 --- a/builtin/reset.c +++ b/builtin/reset.c @@ -256,7 +256,7 @@ static int reset_refs(const char *rev, const struct object_id *oid) update_ref_oid(msg.buf, "ORIG_HEAD", orig, old_orig, 0, UPDATE_REFS_MSG_ON_ERR); } else if (old_orig) - delete_ref("ORIG_HEAD", old_orig->hash, 0); + delete_ref(NULL, "ORIG_HEAD", old_orig->hash, 0); set_reflog_message(&msg, "updating HEAD", rev); update_ref_status = update_ref_oid(msg.buf, "HEAD", oid, orig, 0, UPDATE_REFS_MSG_ON_ERR); diff --git a/builtin/rev-parse.c b/builtin/rev-parse.c index ff13e59e1d..e08677e559 100644 --- a/builtin/rev-parse.c +++ b/builtin/rev-parse.c @@ -545,6 +545,7 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix) unsigned int flags = 0; const char *name = NULL; struct object_context unused; + struct strbuf buf = STRBUF_INIT; if (argc > 1 && !strcmp("--parseopt", argv[1])) return cmd_parseopt(argc - 1, argv + 1, prefix); @@ -599,7 +600,9 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix) if (!strcmp(arg, "--git-path")) { if (!argv[i + 1]) die("--git-path requires an argument"); - puts(git_path("%s", argv[i + 1])); + strbuf_reset(&buf); + puts(relative_path(git_path("%s", argv[i + 1]), + prefix, &buf)); i++; continue; } @@ -802,17 +805,27 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix) putchar('\n'); continue; } - if (!strcmp(arg, "--git-dir")) { + if (!strcmp(arg, "--git-dir") || + !strcmp(arg, "--absolute-git-dir")) { const char *gitdir = getenv(GIT_DIR_ENVIRONMENT); char *cwd; int len; - if (gitdir) { - puts(gitdir); - continue; - } - if (!prefix) { - puts(".git"); - continue; + if (arg[2] == 'g') { /* --git-dir */ + if (gitdir) { + puts(gitdir); + continue; + } + if (!prefix) { + puts(".git"); + continue; + } + } else { /* --absolute-git-dir */ + if (!gitdir && !prefix) + gitdir = ".git"; + if (gitdir) { + puts(real_path(gitdir)); + continue; + } } cwd = xgetcwd(); len = strlen(cwd); @@ -821,8 +834,9 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix) continue; } if (!strcmp(arg, "--git-common-dir")) { - const char *pfx = prefix ? prefix : ""; - puts(prefix_filename(pfx, strlen(pfx), get_git_common_dir())); + strbuf_reset(&buf); + puts(relative_path(get_git_common_dir(), + prefix, &buf)); continue; } if (!strcmp(arg, "--is-inside-git-dir")) { @@ -845,7 +859,9 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix) die(_("Could not read the index")); if (the_index.split_index) { const unsigned char *sha1 = the_index.split_index->base_sha1; - puts(git_path("sharedindex.%s", sha1_to_hex(sha1))); + const char *path = git_path("sharedindex.%s", sha1_to_hex(sha1)); + strbuf_reset(&buf); + puts(relative_path(path, prefix, &buf)); } continue; } @@ -897,6 +913,7 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix) continue; verify_filename(prefix, arg, 1); } + strbuf_release(&buf); if (verify) { if (revs_count == 1) { show_rev(type, sha1, name); diff --git a/builtin/shortlog.c b/builtin/shortlog.c index c9585d475d..f78bb4818d 100644 --- a/builtin/shortlog.c +++ b/builtin/shortlog.c @@ -148,7 +148,7 @@ void shortlog_add_commit(struct shortlog *log, struct commit *commit) ctx.fmt = CMIT_FMT_USERFORMAT; ctx.abbrev = log->abbrev; - ctx.subject = ""; + ctx.print_email_subject = 1; ctx.after_subject = ""; ctx.date_mode.type = DATE_NORMAL; ctx.output_encoding = get_log_output_encoding(); diff --git a/builtin/show-branch.c b/builtin/show-branch.c index 974f3403ab..19756595d5 100644 --- a/builtin/show-branch.c +++ b/builtin/show-branch.c @@ -275,8 +275,7 @@ static void show_one_commit(struct commit *commit, int no_name) pp_commit_easy(CMIT_FMT_ONELINE, commit, &pretty); pretty_str = pretty.buf; } - if (starts_with(pretty_str, "[PATCH] ")) - pretty_str += 8; + skip_prefix(pretty_str, "[PATCH] ", &pretty_str); if (!no_name) { if (name && name->head_name) { @@ -470,18 +469,14 @@ static void snarf_refs(int head, int remotes) } } -static int rev_is_head(char *head, int headlen, char *name, +static int rev_is_head(const char *head, const char *name, unsigned char *head_sha1, unsigned char *sha1) { - if ((!head[0]) || - (head_sha1 && sha1 && hashcmp(head_sha1, sha1))) + if (!head || (head_sha1 && sha1 && hashcmp(head_sha1, sha1))) return 0; - if (starts_with(head, "refs/heads/")) - head += 11; - if (starts_with(name, "refs/heads/")) - name += 11; - else if (starts_with(name, "heads/")) - name += 6; + skip_prefix(head, "refs/heads/", &head); + if (!skip_prefix(name, "refs/heads/", &name)) + skip_prefix(name, "heads/", &name); return !strcmp(head, name); } @@ -620,9 +615,7 @@ int cmd_show_branch(int ac, const char **av, const char *prefix) int all_heads = 0, all_remotes = 0; int all_mask, all_revs; enum rev_sort_order sort_order = REV_SORT_IN_GRAPH_ORDER; - char head[128]; - const char *head_p; - int head_len; + char *head; struct object_id head_oid; int merge_base = 0; int independent = 0; @@ -787,32 +780,24 @@ int cmd_show_branch(int ac, const char **av, const char *prefix) snarf_refs(all_heads, all_remotes); } - head_p = resolve_ref_unsafe("HEAD", RESOLVE_REF_READING, - head_oid.hash, NULL); - if (head_p) { - head_len = strlen(head_p); - memcpy(head, head_p, head_len + 1); - } - else { - head_len = 0; - head[0] = 0; - } + head = resolve_refdup("HEAD", RESOLVE_REF_READING, + head_oid.hash, NULL); - if (with_current_branch && head_p) { + if (with_current_branch && head) { int has_head = 0; for (i = 0; !has_head && i < ref_name_cnt; i++) { /* We are only interested in adding the branch * HEAD points at. */ if (rev_is_head(head, - head_len, ref_name[i], head_oid.hash, NULL)) has_head++; } if (!has_head) { - int offset = starts_with(head, "refs/heads/") ? 11 : 0; - append_one_rev(head + offset); + const char *name = head; + skip_prefix(name, "refs/heads/", &name); + append_one_rev(name); } } @@ -866,7 +851,6 @@ int cmd_show_branch(int ac, const char **av, const char *prefix) for (i = 0; i < num_rev; i++) { int j; int is_head = rev_is_head(head, - head_len, ref_name[i], head_oid.hash, rev[i]->object.oid.hash); diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c index 899dc334e3..15a5430c00 100644 --- a/builtin/submodule--helper.c +++ b/builtin/submodule--helper.c @@ -356,12 +356,10 @@ static void init_submodule(const char *path, const char *prefix, int quiet) strbuf_addf(&remotesb, "remote.%s.url", remote); free(remote); - if (git_config_get_string(remotesb.buf, &remoteurl)) - /* - * The repository is its own - * authoritative upstream - */ + if (git_config_get_string(remotesb.buf, &remoteurl)) { + warning(_("could not lookup configuration '%s'. Assuming this repository is its own authoritative upstream."), remotesb.buf); remoteurl = xgetcwd(); + } relurl = relative_url(remoteurl, url, NULL); strbuf_release(&remotesb); free(remoteurl); diff --git a/builtin/symbolic-ref.c b/builtin/symbolic-ref.c index 96eed94468..70addef158 100644 --- a/builtin/symbolic-ref.c +++ b/builtin/symbolic-ref.c @@ -58,7 +58,7 @@ int cmd_symbolic_ref(int argc, const char **argv, const char *prefix) die("Cannot delete %s, not a symbolic ref", argv[0]); if (!strcmp(argv[0], "HEAD")) die("deleting '%s' is not allowed", argv[0]); - return delete_ref(argv[0], NULL, REF_NODEREF); + return delete_ref(NULL, argv[0], NULL, REF_NODEREF); } switch (argc) { diff --git a/builtin/tag.c b/builtin/tag.c index e40c4a9676..ad29be6923 100644 --- a/builtin/tag.c +++ b/builtin/tag.c @@ -45,11 +45,11 @@ static int list_tags(struct ref_filter *filter, struct ref_sorting *sorting, con if (!format) { if (filter->lines) { to_free = xstrfmt("%s %%(contents:lines=%d)", - "%(align:15)%(refname:strip=2)%(end)", + "%(align:15)%(refname:lstrip=2)%(end)", filter->lines); format = to_free; } else - format = "%(refname:strip=2)"; + format = "%(refname:lstrip=2)"; } verify_ref_format(format); @@ -97,7 +97,7 @@ static int for_each_tag_name(const char **argv, each_tag_name_fn fn, static int delete_tag(const char *name, const char *ref, const unsigned char *sha1, const void *cb_data) { - if (delete_ref(ref, sha1, 0)) + if (delete_ref(NULL, ref, sha1, 0)) return 1; printf(_("Deleted tag '%s' (was %s)\n"), name, find_unique_abbrev(sha1, DEFAULT_ABBREV)); return 0; @@ -302,6 +302,54 @@ static void create_tag(const unsigned char *object, const char *tag, } } +static void create_reflog_msg(const unsigned char *sha1, struct strbuf *sb) +{ + enum object_type type; + struct commit *c; + char *buf; + unsigned long size; + int subject_len = 0; + const char *subject_start; + + char *rla = getenv("GIT_REFLOG_ACTION"); + if (rla) { + strbuf_addstr(sb, rla); + } else { + strbuf_addstr(sb, _("tag: tagging ")); + strbuf_add_unique_abbrev(sb, sha1, DEFAULT_ABBREV); + } + + strbuf_addstr(sb, " ("); + type = sha1_object_info(sha1, NULL); + switch (type) { + default: + strbuf_addstr(sb, _("object of unknown type")); + break; + case OBJ_COMMIT: + if ((buf = read_sha1_file(sha1, &type, &size)) != NULL) { + subject_len = find_commit_subject(buf, &subject_start); + strbuf_insert(sb, sb->len, subject_start, subject_len); + } else { + strbuf_addstr(sb, _("commit object")); + } + free(buf); + + if ((c = lookup_commit_reference(sha1)) != NULL) + strbuf_addf(sb, ", %s", show_date(c->date, 0, DATE_MODE(SHORT))); + break; + case OBJ_TREE: + strbuf_addstr(sb, _("tree object")); + break; + case OBJ_BLOB: + strbuf_addstr(sb, _("blob object")); + break; + case OBJ_TAG: + strbuf_addstr(sb, _("other tag object")); + break; + } + strbuf_addch(sb, ')'); +} + struct msg_arg { int given; struct strbuf buf; @@ -335,6 +383,7 @@ int cmd_tag(int argc, const char **argv, const char *prefix) { struct strbuf buf = STRBUF_INIT; struct strbuf ref = STRBUF_INIT; + struct strbuf reflog_msg = STRBUF_INIT; unsigned char object[20], prev[20]; const char *object_ref, *tag; struct create_tag_options opt; @@ -389,6 +438,8 @@ int cmd_tag(int argc, const char **argv, const char *prefix) OPT_END() }; + setup_ref_filter_porcelain_msg(); + git_config(git_tag_config, sorting_tail); memset(&opt, 0, sizeof(opt)); @@ -494,6 +545,8 @@ int cmd_tag(int argc, const char **argv, const char *prefix) else die(_("Invalid cleanup mode %s"), cleanup_arg); + create_reflog_msg(object, &reflog_msg); + if (create_tag_object) { if (force_sign_annotate && !annotate) opt.sign = 1; @@ -504,7 +557,7 @@ int cmd_tag(int argc, const char **argv, const char *prefix) if (!transaction || ref_transaction_update(transaction, ref.buf, object, prev, create_reflog ? REF_FORCE_CREATE_REFLOG : 0, - NULL, &err) || + reflog_msg.buf, &err) || ref_transaction_commit(transaction, &err)) die("%s", err.buf); ref_transaction_free(transaction); @@ -514,5 +567,6 @@ int cmd_tag(int argc, const char **argv, const char *prefix) strbuf_release(&err); strbuf_release(&buf); strbuf_release(&ref); + strbuf_release(&reflog_msg); return 0; } diff --git a/builtin/update-ref.c b/builtin/update-ref.c index 7f30d3a76f..0b2ecf41ae 100644 --- a/builtin/update-ref.c +++ b/builtin/update-ref.c @@ -433,7 +433,7 @@ int cmd_update_ref(int argc, const char **argv, const char *prefix) * For purposes of backwards compatibility, we treat * NULL_SHA1 as "don't care" here: */ - return delete_ref(refname, + return delete_ref(msg, refname, (oldval && !is_null_sha1(oldsha1)) ? oldsha1 : NULL, flags); else @@ -1045,9 +1045,6 @@ static inline int is_empty_tree_oid(const struct object_id *oid) return !hashcmp(oid->hash, EMPTY_TREE_SHA1_BIN); } - -int git_mkstemp(char *path, size_t n, const char *template); - /* set default permissions by passing mode arguments to open(2) */ int git_mkstemps_mode(char *pattern, int suffix_len, int mode); int git_mkstemp_mode(char *pattern, int mode); @@ -1072,8 +1069,9 @@ int adjust_shared_perm(const char *path); /* * Create the directory containing the named path, using care to be - * somewhat safe against races. Return one of the scld_error values - * to indicate success/failure. + * somewhat safe against races. Return one of the scld_error values to + * indicate success/failure. On error, set errno to describe the + * problem. * * SCLD_VANISHED indicates that one of the ancestor directories of the * path existed at one point during the function call and then @@ -1097,6 +1095,49 @@ enum scld_error { enum scld_error safe_create_leading_directories(char *path); enum scld_error safe_create_leading_directories_const(const char *path); +/* + * Callback function for raceproof_create_file(). This function is + * expected to do something that makes dirname(path) permanent despite + * the fact that other processes might be cleaning up empty + * directories at the same time. Usually it will create a file named + * path, but alternatively it could create another file in that + * directory, or even chdir() into that directory. The function should + * return 0 if the action was completed successfully. On error, it + * should return a nonzero result and set errno. + * raceproof_create_file() treats two errno values specially: + * + * - ENOENT -- dirname(path) does not exist. In this case, + * raceproof_create_file() tries creating dirname(path) + * (and any parent directories, if necessary) and calls + * the function again. + * + * - EISDIR -- the file already exists and is a directory. In this + * case, raceproof_create_file() removes the directory if + * it is empty (and recursively any empty directories that + * it contains) and calls the function again. + * + * Any other errno causes raceproof_create_file() to fail with the + * callback's return value and errno. + * + * Obviously, this function should be OK with being called again if it + * fails with ENOENT or EISDIR. In other scenarios it will not be + * called again. + */ +typedef int create_file_fn(const char *path, void *cb); + +/* + * Create a file in dirname(path) by calling fn, creating leading + * directories if necessary. Retry a few times in case we are racing + * with another process that is trying to clean up the directory that + * contains path. See the documentation for create_file_fn for more + * details. + * + * Return the value and set the errno that resulted from the most + * recent call of fn. fn is always called at least once, and will be + * called more than once if it returns ENOENT or EISDIR. + */ +int raceproof_create_file(const char *path, create_file_fn fn, void *cb); + int mkdir_in_gitdir(const char *path); extern char *expand_user_path(const char *path); const char *enter_repo(const char *path, int strict); @@ -1819,8 +1860,11 @@ extern int git_config_include(const char *name, const char *value, void *data); * * (i.e., what gets handed to a config_fn_t). The caller provides the section; * we return -1 if it does not match, 0 otherwise. The subsection and key - * out-parameters are filled by the function (and subsection is NULL if it is + * out-parameters are filled by the function (and *subsection is NULL if it is * missing). + * + * If the subsection pointer-to-pointer passed in is NULL, returns 0 only if + * there is no subsection at all. */ extern int parse_config_key(const char *var, const char *section, @@ -415,8 +415,7 @@ int find_commit_subject(const char *commit_buffer, const char **subject) p++; if (*p) { p = skip_blank_lines(p + 2); - for (eol = p; *eol && *eol != '\n'; eol++) - ; /* do nothing */ + eol = strchrnul(p, '\n'); } else eol = p; @@ -1308,11 +1307,11 @@ void for_each_mergetag(each_mergetag_fn fn, struct commit *commit, void *data) static inline int standard_header_field(const char *field, size_t len) { - return ((len == 4 && !memcmp(field, "tree ", 5)) || - (len == 6 && !memcmp(field, "parent ", 7)) || - (len == 6 && !memcmp(field, "author ", 7)) || - (len == 9 && !memcmp(field, "committer ", 10)) || - (len == 8 && !memcmp(field, "encoding ", 9))); + return ((len == 4 && !memcmp(field, "tree", 4)) || + (len == 6 && !memcmp(field, "parent", 6)) || + (len == 6 && !memcmp(field, "author", 6)) || + (len == 9 && !memcmp(field, "committer", 9)) || + (len == 8 && !memcmp(field, "encoding", 8))); } static int excluded_header_field(const char *field, size_t len, const char **exclude) @@ -1322,8 +1321,7 @@ static int excluded_header_field(const char *field, size_t len, const char **exc while (*exclude) { size_t xlen = strlen(*exclude); - if (len == xlen && - !memcmp(field, *exclude, xlen) && field[xlen] == ' ') + if (len == xlen && !memcmp(field, *exclude, xlen)) return 1; exclude++; } @@ -1354,12 +1352,11 @@ static struct commit_extra_header *read_commit_extra_header_lines( strbuf_reset(&buf); it = NULL; - eof = strchr(line, ' '); - if (next <= eof) + eof = memchr(line, ' ', next - line); + if (!eof) eof = next; - - if (standard_header_field(line, eof - line) || - excluded_header_field(line, eof - line, exclude)) + else if (standard_header_field(line, eof - line) || + excluded_header_field(line, eof - line, exclude)) continue; it = xcalloc(1, sizeof(*it)); @@ -142,21 +142,24 @@ static inline int cmit_fmt_is_mail(enum cmit_fmt fmt) return (fmt == CMIT_FMT_EMAIL || fmt == CMIT_FMT_MBOXRD); } +struct rev_info; /* in revision.h, it circularly uses enum cmit_fmt */ + struct pretty_print_context { /* * Callers should tweak these to change the behavior of pp_* functions. */ enum cmit_fmt fmt; int abbrev; - const char *subject; const char *after_subject; int preserve_subject; struct date_mode date_mode; unsigned date_mode_explicit:1; + int print_email_subject; int expand_tabs_in_log; int need_8bit_cte; char *notes_message; struct reflog_walk_info *reflog_info; + struct rev_info *rev; const char *output_encoding; struct string_list *mailmap; int color; @@ -175,7 +178,6 @@ struct userformat_want { }; extern int has_non_ascii(const char *text); -struct rev_info; /* in revision.h, it circularly uses enum cmit_fmt */ extern const char *logmsg_reencode(const struct commit *commit, char **commit_encoding, const char *output_encoding); diff --git a/common-main.c b/common-main.c index c654f95551..6a689007e7 100644 --- a/common-main.c +++ b/common-main.c @@ -1,5 +1,6 @@ #include "cache.h" #include "exec_cmd.h" +#include "attr.h" /* * Many parts of Git have subprograms communicate via pipe, expect the @@ -33,6 +34,8 @@ int main(int argc, const char **argv) git_setup_gettext(); + attr_start(); + git_extract_argv0_path(argv[0]); restore_sigpipe_to_default(); @@ -201,11 +201,105 @@ void git_config_push_parameter(const char *text) strbuf_release(&env); } +static inline int iskeychar(int c) +{ + return isalnum(c) || c == '-'; +} + +/* + * Auxiliary function to sanity-check and split the key into the section + * identifier and variable name. + * + * Returns 0 on success, -1 when there is an invalid character in the key and + * -2 if there is no section name in the key. + * + * store_key - pointer to char* which will hold a copy of the key with + * lowercase section and variable name + * baselen - pointer to int which will hold the length of the + * section + subsection part, can be NULL + */ +static int git_config_parse_key_1(const char *key, char **store_key, int *baselen_, int quiet) +{ + int i, dot, baselen; + const char *last_dot = strrchr(key, '.'); + + /* + * Since "key" actually contains the section name and the real + * key name separated by a dot, we have to know where the dot is. + */ + + if (last_dot == NULL || last_dot == key) { + if (!quiet) + error("key does not contain a section: %s", key); + return -CONFIG_NO_SECTION_OR_NAME; + } + + if (!last_dot[1]) { + if (!quiet) + error("key does not contain variable name: %s", key); + return -CONFIG_NO_SECTION_OR_NAME; + } + + baselen = last_dot - key; + if (baselen_) + *baselen_ = baselen; + + /* + * Validate the key and while at it, lower case it for matching. + */ + if (store_key) + *store_key = xmallocz(strlen(key)); + + dot = 0; + for (i = 0; key[i]; i++) { + unsigned char c = key[i]; + if (c == '.') + dot = 1; + /* Leave the extended basename untouched.. */ + if (!dot || i > baselen) { + if (!iskeychar(c) || + (i == baselen + 1 && !isalpha(c))) { + if (!quiet) + error("invalid key: %s", key); + goto out_free_ret_1; + } + c = tolower(c); + } else if (c == '\n') { + if (!quiet) + error("invalid key (newline): %s", key); + goto out_free_ret_1; + } + if (store_key) + (*store_key)[i] = c; + } + + return 0; + +out_free_ret_1: + if (store_key) { + free(*store_key); + *store_key = NULL; + } + return -CONFIG_INVALID_KEY; +} + +int git_config_parse_key(const char *key, char **store_key, int *baselen) +{ + return git_config_parse_key_1(key, store_key, baselen, 0); +} + +int git_config_key_is_valid(const char *key) +{ + return !git_config_parse_key_1(key, NULL, NULL, 1); +} + int git_config_parse_parameter(const char *text, config_fn_t fn, void *data) { const char *value; + char *canonical_name; struct strbuf **pair; + int ret; pair = strbuf_split_str(text, '=', 2); if (!pair[0]) @@ -223,13 +317,15 @@ int git_config_parse_parameter(const char *text, strbuf_list_free(pair); return error("bogus config parameter: %s", text); } - strbuf_tolower(pair[0]); - if (fn(pair[0]->buf, value, data) < 0) { - strbuf_list_free(pair); - return -1; + + if (git_config_parse_key(pair[0]->buf, &canonical_name, NULL)) { + ret = -1; + } else { + ret = (fn(canonical_name, value, data) < 0) ? -1 : 0; + free(canonical_name); } strbuf_list_free(pair); - return 0; + return ret; } int git_config_from_parameters(config_fn_t fn, void *data) @@ -356,11 +452,6 @@ static char *parse_value(void) } } -static inline int iskeychar(int c) -{ - return isalnum(c) || c == '-'; -} - static int get_value(config_fn_t fn, void *data, struct strbuf *name) { int c; @@ -1990,93 +2081,6 @@ void git_config_set(const char *key, const char *value) } /* - * Auxiliary function to sanity-check and split the key into the section - * identifier and variable name. - * - * Returns 0 on success, -1 when there is an invalid character in the key and - * -2 if there is no section name in the key. - * - * store_key - pointer to char* which will hold a copy of the key with - * lowercase section and variable name - * baselen - pointer to int which will hold the length of the - * section + subsection part, can be NULL - */ -static int git_config_parse_key_1(const char *key, char **store_key, int *baselen_, int quiet) -{ - int i, dot, baselen; - const char *last_dot = strrchr(key, '.'); - - /* - * Since "key" actually contains the section name and the real - * key name separated by a dot, we have to know where the dot is. - */ - - if (last_dot == NULL || last_dot == key) { - if (!quiet) - error("key does not contain a section: %s", key); - return -CONFIG_NO_SECTION_OR_NAME; - } - - if (!last_dot[1]) { - if (!quiet) - error("key does not contain variable name: %s", key); - return -CONFIG_NO_SECTION_OR_NAME; - } - - baselen = last_dot - key; - if (baselen_) - *baselen_ = baselen; - - /* - * Validate the key and while at it, lower case it for matching. - */ - if (store_key) - *store_key = xmallocz(strlen(key)); - - dot = 0; - for (i = 0; key[i]; i++) { - unsigned char c = key[i]; - if (c == '.') - dot = 1; - /* Leave the extended basename untouched.. */ - if (!dot || i > baselen) { - if (!iskeychar(c) || - (i == baselen + 1 && !isalpha(c))) { - if (!quiet) - error("invalid key: %s", key); - goto out_free_ret_1; - } - c = tolower(c); - } else if (c == '\n') { - if (!quiet) - error("invalid key (newline): %s", key); - goto out_free_ret_1; - } - if (store_key) - (*store_key)[i] = c; - } - - return 0; - -out_free_ret_1: - if (store_key) { - free(*store_key); - *store_key = NULL; - } - return -CONFIG_INVALID_KEY; -} - -int git_config_parse_key(const char *key, char **store_key, int *baselen) -{ - return git_config_parse_key_1(key, store_key, baselen, 0); -} - -int git_config_key_is_valid(const char *key) -{ - return !git_config_parse_key_1(key, NULL, NULL, 1); -} - -/* * If value==NULL, unset in (remove from) config, * if value_regex!=NULL, disregard key/value pairs where value does not match. * if value_regex==CONFIG_REGEX_NONE, do not match any existing values @@ -2536,11 +2540,10 @@ int parse_config_key(const char *var, const char **subsection, int *subsection_len, const char **key) { - int section_len = strlen(section); const char *dot; /* Does it start with "section." ? */ - if (!starts_with(var, section) || var[section_len] != '.') + if (!skip_prefix(var, section, &var) || *var != '.') return -1; /* @@ -2552,12 +2555,16 @@ int parse_config_key(const char *var, *key = dot + 1; /* Did we have a subsection at all? */ - if (dot == var + section_len) { - *subsection = NULL; - *subsection_len = 0; + if (dot == var) { + if (subsection) { + *subsection = NULL; + *subsection_len = 0; + } } else { - *subsection = var + section_len + 1; + if (!subsection) + return -1; + *subsection = var + 1; *subsection_len = dot - *subsection; } diff --git a/config.mak.uname b/config.mak.uname index 447f36ac2e..399fe19271 100644 --- a/config.mak.uname +++ b/config.mak.uname @@ -27,7 +27,6 @@ endif ifeq ($(uname_S),Linux) HAVE_ALLOCA_H = YesPlease NO_STRLCPY = YesPlease - NO_MKSTEMPS = YesPlease HAVE_PATHS_H = YesPlease LIBC_CONTAINS_LIBINTL = YesPlease HAVE_DEV_TTY = YesPlease @@ -41,7 +40,6 @@ endif ifeq ($(uname_S),GNU/kFreeBSD) HAVE_ALLOCA_H = YesPlease NO_STRLCPY = YesPlease - NO_MKSTEMPS = YesPlease HAVE_PATHS_H = YesPlease DIR_HAS_BSD_GROUP_SEMANTICS = YesPlease LIBC_CONTAINS_LIBINTL = YesPlease @@ -55,7 +53,6 @@ ifeq ($(uname_S),UnixWare) SHELL_PATH = /usr/local/bin/bash NO_IPV6 = YesPlease NO_HSTRERROR = YesPlease - NO_MKSTEMPS = YesPlease BASIC_CFLAGS += -Kthread BASIC_CFLAGS += -I/usr/local/include BASIC_LDFLAGS += -L/usr/local/lib @@ -79,7 +76,6 @@ ifeq ($(uname_S),SCO_SV) SHELL_PATH = /usr/bin/bash NO_IPV6 = YesPlease NO_HSTRERROR = YesPlease - NO_MKSTEMPS = YesPlease BASIC_CFLAGS += -I/usr/local/include BASIC_LDFLAGS += -L/usr/local/lib NO_STRCASESTR = YesPlease @@ -122,7 +118,6 @@ ifeq ($(uname_S),SunOS) NO_STRCASESTR = YesPlease NO_MEMMEM = YesPlease NO_MKDTEMP = YesPlease - NO_MKSTEMPS = YesPlease NO_REGEX = YesPlease NO_MSGFMT_EXTENDED_OPTIONS = YesPlease HAVE_DEV_TTY = YesPlease @@ -168,7 +163,6 @@ ifeq ($(uname_O),Cygwin) NO_D_TYPE_IN_DIRENT = YesPlease NO_STRCASESTR = YesPlease NO_MEMMEM = YesPlease - NO_MKSTEMPS = YesPlease NO_SYMLINK_HEAD = YesPlease NO_IPV6 = YesPlease OLD_ICONV = UnfortunatelyYes @@ -233,7 +227,6 @@ ifeq ($(uname_S),NetBSD) BASIC_CFLAGS += -I/usr/pkg/include BASIC_LDFLAGS += -L/usr/pkg/lib $(CC_LD_DYNPATH)/usr/pkg/lib USE_ST_TIMESPEC = YesPlease - NO_MKSTEMPS = YesPlease HAVE_PATHS_H = YesPlease HAVE_BSD_SYSCTL = YesPlease endif @@ -242,7 +235,6 @@ ifeq ($(uname_S),AIX) NO_STRCASESTR = YesPlease NO_MEMMEM = YesPlease NO_MKDTEMP = YesPlease - NO_MKSTEMPS = YesPlease NO_STRLCPY = YesPlease NO_NSEC = YesPlease FREAD_READS_DIRECTORIES = UnfortunatelyYes @@ -263,7 +255,6 @@ ifeq ($(uname_S),GNU) # GNU/Hurd HAVE_ALLOCA_H = YesPlease NO_STRLCPY = YesPlease - NO_MKSTEMPS = YesPlease HAVE_PATHS_H = YesPlease LIBC_CONTAINS_LIBINTL = YesPlease endif @@ -272,7 +263,6 @@ ifeq ($(uname_S),IRIX) NO_UNSETENV = YesPlease NO_STRCASESTR = YesPlease NO_MEMMEM = YesPlease - NO_MKSTEMPS = YesPlease NO_MKDTEMP = YesPlease # When compiled with the MIPSpro 7.4.4m compiler, and without pthreads # (i.e. NO_PTHREADS is set), and _with_ MMAP (i.e. NO_MMAP is not set), @@ -291,7 +281,6 @@ ifeq ($(uname_S),IRIX64) NO_UNSETENV = YesPlease NO_STRCASESTR = YesPlease NO_MEMMEM = YesPlease - NO_MKSTEMPS = YesPlease NO_MKDTEMP = YesPlease # When compiled with the MIPSpro 7.4.4m compiler, and without pthreads # (i.e. NO_PTHREADS is set), and _with_ MMAP (i.e. NO_MMAP is not set), @@ -311,7 +300,6 @@ ifeq ($(uname_S),HP-UX) NO_SETENV = YesPlease NO_STRCASESTR = YesPlease NO_MEMMEM = YesPlease - NO_MKSTEMPS = YesPlease NO_STRLCPY = YesPlease NO_MKDTEMP = YesPlease NO_UNSETENV = YesPlease @@ -352,7 +340,6 @@ ifeq ($(uname_S),Windows) NO_ICONV = YesPlease NO_STRTOUMAX = YesPlease NO_MKDTEMP = YesPlease - NO_MKSTEMPS = YesPlease SNPRINTF_RETURNS_BOGUS = YesPlease NO_SVN_TESTS = YesPlease RUNTIME_PREFIX = YesPlease @@ -402,7 +389,6 @@ ifeq ($(uname_S),Interix) NO_MKDTEMP = YesPlease NO_STRTOUMAX = YesPlease NO_NSEC = YesPlease - NO_MKSTEMPS = YesPlease ifeq ($(uname_R),3.5) NO_INET_NTOP = YesPlease NO_INET_PTON = YesPlease @@ -461,7 +447,6 @@ ifeq ($(uname_S),NONSTOP_KERNEL) NO_SETENV = YesPlease NO_UNSETENV = YesPlease NO_MKDTEMP = YesPlease - NO_MKSTEMPS = YesPlease # Currently libiconv-1.9.1. OLD_ICONV = UnfortunatelyYes NO_REGEX = YesPlease @@ -503,7 +488,6 @@ ifneq (,$(findstring MINGW,$(uname_S))) NEEDS_LIBICONV = YesPlease NO_STRTOUMAX = YesPlease NO_MKDTEMP = YesPlease - NO_MKSTEMPS = YesPlease NO_SVN_TESTS = YesPlease NO_PERL_MAKEMAKER = YesPlease RUNTIME_PREFIX = YesPlease @@ -515,7 +499,6 @@ ifneq (,$(findstring MINGW,$(uname_S))) OBJECT_CREATION_USES_RENAMES = UnfortunatelyNeedsTo NO_REGEX = YesPlease NO_PYTHON = YesPlease - BLK_SHA1 = YesPlease ETAGS_TARGET = ETAGS NO_INET_PTON = YesPlease NO_INET_NTOP = YesPlease @@ -584,7 +567,6 @@ ifeq ($(uname_S),QNX) NO_ICONV = YesPlease NO_MEMMEM = YesPlease NO_MKDTEMP = YesPlease - NO_MKSTEMPS = YesPlease NO_NSEC = YesPlease NO_PTHREADS = YesPlease NO_R_TO_GCC_LINKER = YesPlease diff --git a/configure.ac b/configure.ac index 0b15f04b10..128165529f 100644 --- a/configure.ac +++ b/configure.ac @@ -1050,12 +1050,6 @@ GIT_CHECK_FUNC(mkdtemp, [NO_MKDTEMP=YesPlease]) GIT_CONF_SUBST([NO_MKDTEMP]) # -# Define NO_MKSTEMPS if you don't have mkstemps in the C library. -GIT_CHECK_FUNC(mkstemps, -[NO_MKSTEMPS=], -[NO_MKSTEMPS=YesPlease]) -GIT_CONF_SUBST([NO_MKSTEMPS]) -# # Define NO_INITGROUPS if you don't have initgroups in the C library. GIT_CHECK_FUNC(initgroups, [NO_INITGROUPS=], @@ -691,6 +691,68 @@ static const char *get_ssh_command(void) return NULL; } +static int override_ssh_variant(int *port_option, int *needs_batch) +{ + char *variant; + + variant = xstrdup_or_null(getenv("GIT_SSH_VARIANT")); + if (!variant && + git_config_get_string("ssh.variant", &variant)) + return 0; + + if (!strcmp(variant, "plink") || !strcmp(variant, "putty")) { + *port_option = 'P'; + *needs_batch = 0; + } else if (!strcmp(variant, "tortoiseplink")) { + *port_option = 'P'; + *needs_batch = 1; + } else { + *port_option = 'p'; + *needs_batch = 0; + } + free(variant); + return 1; +} + +static void handle_ssh_variant(const char *ssh_command, int is_cmdline, + int *port_option, int *needs_batch) +{ + const char *variant; + char *p = NULL; + + if (override_ssh_variant(port_option, needs_batch)) + return; + + if (!is_cmdline) { + p = xstrdup(ssh_command); + variant = basename(p); + } else { + const char **ssh_argv; + + p = xstrdup(ssh_command); + if (split_cmdline(p, &ssh_argv)) { + variant = basename((char *)ssh_argv[0]); + /* + * At this point, variant points into the buffer + * referenced by p, hence we do not need ssh_argv + * any longer. + */ + free(ssh_argv); + } else + return; + } + + if (!strcasecmp(variant, "plink") || + !strcasecmp(variant, "plink.exe")) + *port_option = 'P'; + else if (!strcasecmp(variant, "tortoiseplink") || + !strcasecmp(variant, "tortoiseplink.exe")) { + *port_option = 'P'; + *needs_batch = 1; + } + free(p); +} + /* * This returns a dummy child_process if the transport protocol does not * need fork(2), or a struct child_process object if it does. Once done, @@ -769,7 +831,8 @@ struct child_process *git_connect(int fd[2], const char *url, conn->in = conn->out = -1; if (protocol == PROTO_SSH) { const char *ssh; - int putty = 0, tortoiseplink = 0; + int needs_batch = 0; + int port_option = 'p'; char *ssh_host = hostandport; const char *port = NULL; transport_check_allowed("ssh"); @@ -792,10 +855,10 @@ struct child_process *git_connect(int fd[2], const char *url, } ssh = get_ssh_command(); - if (!ssh) { - const char *base; - char *ssh_dup; - + if (ssh) + handle_ssh_variant(ssh, 1, &port_option, + &needs_batch); + else { /* * GIT_SSH is the no-shell version of * GIT_SSH_COMMAND (and must remain so for @@ -806,17 +869,10 @@ struct child_process *git_connect(int fd[2], const char *url, ssh = getenv("GIT_SSH"); if (!ssh) ssh = "ssh"; - - ssh_dup = xstrdup(ssh); - base = basename(ssh_dup); - - tortoiseplink = !strcasecmp(base, "tortoiseplink") || - !strcasecmp(base, "tortoiseplink.exe"); - putty = tortoiseplink || - !strcasecmp(base, "plink") || - !strcasecmp(base, "plink.exe"); - - free(ssh_dup); + else + handle_ssh_variant(ssh, 0, + &port_option, + &needs_batch); } argv_array_push(&conn->args, ssh); @@ -824,11 +880,11 @@ struct child_process *git_connect(int fd[2], const char *url, argv_array_push(&conn->args, "-4"); else if (flags & CONNECT_IPV6) argv_array_push(&conn->args, "-6"); - if (tortoiseplink) + if (needs_batch) argv_array_push(&conn->args, "-batch"); if (port) { - /* P is for PuTTY, p is for OpenSSH */ - argv_array_push(&conn->args, putty ? "-P" : "-p"); + argv_array_pushf(&conn->args, + "-%c", port_option); argv_array_push(&conn->args, port); } argv_array_push(&conn->args, ssh_host); diff --git a/contrib/coccinelle/array.cocci b/contrib/coccinelle/array.cocci index 2d7f25d99f..4ba98b7eaf 100644 --- a/contrib/coccinelle/array.cocci +++ b/contrib/coccinelle/array.cocci @@ -24,3 +24,19 @@ expression n; @@ - memcpy(dst, src, n * sizeof(T)); + COPY_ARRAY(dst, src, n); + +@@ +type T; +T *ptr; +expression n; +@@ +- ptr = xmalloc(n * sizeof(*ptr)); ++ ALLOC_ARRAY(ptr, n); + +@@ +type T; +T *ptr; +expression n; +@@ +- ptr = xmalloc(n * sizeof(T)); ++ ALLOC_ARRAY(ptr, n); diff --git a/contrib/coccinelle/strbuf.cocci b/contrib/coccinelle/strbuf.cocci index 63995f22ff..1d580e49b0 100644 --- a/contrib/coccinelle/strbuf.cocci +++ b/contrib/coccinelle/strbuf.cocci @@ -38,3 +38,9 @@ expression E1, E2, E3; @@ - strbuf_addstr(E1, find_unique_abbrev(E2, E3)); + strbuf_add_unique_abbrev(E1, E2, E3); + +@@ +expression E1, E2; +@@ +- strbuf_addstr(E1, real_path(E2)); ++ strbuf_add_real_path(E1, E2); diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash index 41ee52991d..fc32286a43 100644 --- a/contrib/completion/git-completion.bash +++ b/contrib/completion/git-completion.bash @@ -34,21 +34,41 @@ case "$COMP_WORDBREAKS" in *) COMP_WORDBREAKS="$COMP_WORDBREAKS:" esac +# Discovers the path to the git repository taking any '--git-dir=<path>' and +# '-C <path>' options into account and stores it in the $__git_repo_path +# variable. +__git_find_repo_path () +{ + if [ -n "$__git_repo_path" ]; then + # we already know where it is + return + fi + + if [ -n "${__git_C_args-}" ]; then + __git_repo_path="$(git "${__git_C_args[@]}" \ + ${__git_dir:+--git-dir="$__git_dir"} \ + rev-parse --absolute-git-dir 2>/dev/null)" + elif [ -n "${__git_dir-}" ]; then + test -d "$__git_dir" && + __git_repo_path="$__git_dir" + elif [ -n "${GIT_DIR-}" ]; then + test -d "${GIT_DIR-}" && + __git_repo_path="$GIT_DIR" + elif [ -d .git ]; then + __git_repo_path=.git + else + __git_repo_path="$(git rev-parse --git-dir 2>/dev/null)" + fi +} + +# Deprecated: use __git_find_repo_path() and $__git_repo_path instead # __gitdir accepts 0 or 1 arguments (i.e., location) # returns location of .git repo __gitdir () { if [ -z "${1-}" ]; then - if [ -n "${__git_dir-}" ]; then - echo "$__git_dir" - elif [ -n "${GIT_DIR-}" ]; then - test -d "${GIT_DIR-}" || return 1 - echo "$GIT_DIR" - elif [ -d .git ]; then - echo .git - else - git rev-parse --git-dir 2>/dev/null - fi + __git_find_repo_path || return 1 + echo "$__git_repo_path" elif [ -d "$1/.git" ]; then echo "$1/.git" else @@ -56,6 +76,14 @@ __gitdir () fi } +# Runs git with all the options given as argument, respecting any +# '--git-dir=<path>' and '-C <path>' options present on the command line +__git () +{ + git ${__git_C_args:+"${__git_C_args[@]}"} \ + ${__git_dir:+--git-dir="$__git_dir"} "$@" 2>/dev/null +} + # The following function is based on code from: # # bash_completion - programmable completion functions for bash 3.2+ @@ -283,11 +311,11 @@ __gitcomp_file () __git_ls_files_helper () { if [ "$2" == "--committable" ]; then - git -C "$1" diff-index --name-only --relative HEAD + __git -C "$1" diff-index --name-only --relative HEAD else # NOTE: $2 is not quoted in order to support multiple options - git -C "$1" ls-files --exclude-standard $2 - fi 2>/dev/null + __git -C "$1" ls-files --exclude-standard $2 + fi } @@ -299,47 +327,61 @@ __git_ls_files_helper () # slash. __git_index_files () { - local dir="$(__gitdir)" root="${2-.}" file + local root="${2-.}" file - if [ -d "$dir" ]; then - __git_ls_files_helper "$root" "$1" | - while read -r file; do - case "$file" in - ?*/*) echo "${file%%/*}" ;; - *) echo "$file" ;; - esac - done | sort | uniq - fi + __git_ls_files_helper "$root" "$1" | + while read -r file; do + case "$file" in + ?*/*) echo "${file%%/*}" ;; + *) echo "$file" ;; + esac + done | sort | uniq } __git_heads () { - local dir="$(__gitdir)" - if [ -d "$dir" ]; then - git --git-dir="$dir" for-each-ref --format='%(refname:short)' \ - refs/heads - return - fi + __git for-each-ref --format='%(refname:short)' refs/heads } __git_tags () { - local dir="$(__gitdir)" - if [ -d "$dir" ]; then - git --git-dir="$dir" for-each-ref --format='%(refname:short)' \ - refs/tags - return - fi + __git for-each-ref --format='%(refname:short)' refs/tags } -# __git_refs accepts 0, 1 (to pass to __gitdir), or 2 arguments -# presence of 2nd argument means use the guess heuristic employed -# by checkout for tracking branches +# Lists refs from the local (by default) or from a remote repository. +# It accepts 0, 1 or 2 arguments: +# 1: The remote to list refs from (optional; ignored, if set but empty). +# Can be the name of a configured remote, a path, or a URL. +# 2: In addition to local refs, list unique branches from refs/remotes/ for +# 'git checkout's tracking DWIMery (optional; ignored, if set but empty). __git_refs () { - local i hash dir="$(__gitdir "${1-}")" track="${2-}" + local i hash dir track="${2-}" + local list_refs_from=path remote="${1-}" local format refs pfx - if [ -d "$dir" ]; then + + __git_find_repo_path + dir="$__git_repo_path" + + if [ -z "$remote" ]; then + if [ -z "$dir" ]; then + return + fi + else + if __git_is_configured_remote "$remote"; then + # configured remote takes precedence over a + # local directory with the same name + list_refs_from=remote + elif [ -d "$remote/.git" ]; then + dir="$remote/.git" + elif [ -d "$remote" ]; then + dir="$remote" + else + list_refs_from=url + fi + fi + + if [ "$list_refs_from" = path ]; then case "$cur" in refs|refs/*) format="refname" @@ -355,14 +397,14 @@ __git_refs () refs="refs/tags refs/heads refs/remotes" ;; esac - git --git-dir="$dir" for-each-ref --format="$pfx%($format)" \ + __git_dir="$dir" __git for-each-ref --format="$pfx%($format)" \ $refs if [ -n "$track" ]; then # employ the heuristic used by git checkout # Try to find a remote branch that matches the completion word # but only output if the branch name is unique local ref entry - git --git-dir="$dir" for-each-ref --shell --format="ref=%(refname:short)" \ + __git for-each-ref --shell --format="ref=%(refname:short)" \ "refs/remotes/" | \ while read -r entry; do eval "$entry" @@ -376,7 +418,7 @@ __git_refs () fi case "$cur" in refs|refs/*) - git ls-remote "$dir" "$cur*" 2>/dev/null | \ + __git ls-remote "$remote" "$cur*" | \ while read -r hash i; do case "$i" in *^{}) ;; @@ -385,9 +427,21 @@ __git_refs () done ;; *) - echo "HEAD" - git for-each-ref --format="%(refname:short)" -- \ - "refs/remotes/$dir/" 2>/dev/null | sed -e "s#^$dir/##" + if [ "$list_refs_from" = remote ]; then + echo "HEAD" + __git for-each-ref --format="%(refname:short)" \ + "refs/remotes/$remote/" | sed -e "s#^$remote/##" + else + __git ls-remote "$remote" HEAD \ + "refs/tags/*" "refs/heads/*" "refs/remotes/*" | + while read -r hash i; do + case "$i" in + *^{}) ;; + refs/*) echo "${i#refs/*/}" ;; + *) echo "$i" ;; # symbolic refs + esac + done + fi ;; esac } @@ -405,7 +459,7 @@ __git_refs2 () __git_refs_remotes () { local i hash - git ls-remote "$1" 'refs/heads/*' 2>/dev/null | \ + __git ls-remote "$1" 'refs/heads/*' | \ while read -r hash i; do echo "$i:refs/remotes/$1/${i#refs/heads/}" done @@ -413,9 +467,21 @@ __git_refs_remotes () __git_remotes () { - local d="$(__gitdir)" - test -d "$d/remotes" && ls -1 "$d/remotes" - git --git-dir="$d" remote + __git_find_repo_path + test -d "$__git_repo_path/remotes" && ls -1 "$__git_repo_path/remotes" + __git remote +} + +# Returns true if $1 matches the name of a configured remote, false otherwise. +__git_is_configured_remote () +{ + local remote + for remote in $(__git_remotes); do + if [ "$remote" = "$1" ]; then + return 0 + fi + done + return 1 } __git_list_merge_strategies () @@ -469,7 +535,7 @@ __git_complete_revlist_file () *) pfx="$ref:$pfx" ;; esac - __gitcomp_nl "$(git --git-dir="$(__gitdir)" ls-tree "$ls" 2>/dev/null \ + __gitcomp_nl "$(__git ls-tree "$ls" \ | sed '/^100... blob /{ s,^.* ,, s,$, , @@ -747,7 +813,7 @@ __git_compute_porcelain_commands () __git_get_config_variables () { local section="$1" i IFS=$'\n' - for i in $(git --git-dir="$(__gitdir)" config --name-only --get-regexp "^$section\..*" 2>/dev/null); do + for i in $(__git config --name-only --get-regexp "^$section\..*"); do echo "${i#$section.}" done } @@ -765,8 +831,7 @@ __git_aliases () # __git_aliased_command requires 1 argument __git_aliased_command () { - local word cmdline=$(git --git-dir="$(__gitdir)" \ - config --get "alias.$1") + local word cmdline=$(__git config --get "alias.$1") for word in $cmdline; do case "$word" in \!gitk|gitk) @@ -842,7 +907,7 @@ __git_get_option_value () done if [ -n "$config_key" ] && [ -z "$result" ]; then - result="$(git --git-dir="$(__gitdir)" config "$config_key")" + result="$(__git config "$config_key")" fi echo "$result" @@ -901,8 +966,8 @@ __git_whitespacelist="nowarn warn error error-all fix" _git_am () { - local dir="$(__gitdir)" - if [ -d "$dir"/rebase-apply ]; then + __git_find_repo_path + if [ -d "$__git_repo_path"/rebase-apply ]; then __gitcomp "--skip --continue --resolved --abort" return fi @@ -990,7 +1055,8 @@ _git_bisect () local subcommands="start bad good skip reset visualize replay log run" local subcommand="$(__git_find_on_cmdline "$subcommands")" if [ -z "$subcommand" ]; then - if [ -f "$(__gitdir)"/BISECT_START ]; then + __git_find_repo_path + if [ -f "$__git_repo_path"/BISECT_START ]; then __gitcomp "$subcommands" else __gitcomp "replay start" @@ -1096,8 +1162,8 @@ _git_cherry () _git_cherry_pick () { - local dir="$(__gitdir)" - if [ -f "$dir"/CHERRY_PICK_HEAD ]; then + __git_find_repo_path + if [ -f "$__git_repo_path"/CHERRY_PICK_HEAD ]; then __gitcomp "--continue --quit --abort" return fi @@ -1192,7 +1258,7 @@ _git_commit () return esac - if git rev-parse --verify --quiet HEAD >/dev/null; then + if __git rev-parse --verify --quiet HEAD >/dev/null; then __git_complete_index_file "--committable" else # This is the first commit @@ -1207,6 +1273,7 @@ _git_describe () __gitcomp " --all --tags --contains --abbrev= --candidates= --exact-match --debug --long --match --always --first-parent + --exclude " return esac @@ -1500,10 +1567,10 @@ __git_log_date_formats="relative iso8601 rfc2822 short local default raw" _git_log () { __git_has_doubledash && return + __git_find_repo_path - local g="$(git rev-parse --git-dir 2>/dev/null)" local merge="" - if [ -f "$g/MERGE_HEAD" ]; then + if [ -f "$__git_repo_path/MERGE_HEAD" ]; then merge="--merge" fi case "$cur" in @@ -1750,11 +1817,12 @@ _git_push () _git_rebase () { - local dir="$(__gitdir)" - if [ -f "$dir"/rebase-merge/interactive ]; then + __git_find_repo_path + if [ -f "$__git_repo_path"/rebase-merge/interactive ]; then __gitcomp "--continue --skip --abort --quit --edit-todo" return - elif [ -d "$dir"/rebase-apply ] || [ -d "$dir"/rebase-merge ]; then + elif [ -d "$__git_repo_path"/rebase-apply ] || \ + [ -d "$__git_repo_path"/rebase-merge ]; then __gitcomp "--continue --skip --abort --quit" return fi @@ -1802,9 +1870,7 @@ _git_send_email () { case "$prev" in --to|--cc|--bcc|--from) - __gitcomp " - $(git --git-dir="$(__gitdir)" send-email --dump-aliases 2>/dev/null) - " + __gitcomp "$(__git send-email --dump-aliases)" return ;; esac @@ -1834,9 +1900,7 @@ _git_send_email () return ;; --to=*|--cc=*|--bcc=*|--from=*) - __gitcomp " - $(git --git-dir="$(__gitdir)" send-email --dump-aliases 2>/dev/null) - " "" "${cur#--*=}" + __gitcomp "$(__git send-email --dump-aliases)" "" "${cur#--*=}" return ;; --*) @@ -1930,7 +1994,7 @@ __git_config_get_set_variables () c=$((--c)) done - git --git-dir="$(__gitdir)" config $config_file --name-only --list 2>/dev/null + __git config $config_file --name-only --list } _git_config () @@ -1965,9 +2029,8 @@ _git_config () remote.*.push) local remote="${prev#remote.}" remote="${remote%.push}" - __gitcomp_nl "$(git --git-dir="$(__gitdir)" \ - for-each-ref --format='%(refname):%(refname)' \ - refs/heads)" + __gitcomp_nl "$(__git for-each-ref \ + --format='%(refname):%(refname)' refs/heads)" return ;; pull.twohead|pull.octopus) @@ -2482,8 +2545,8 @@ _git_reset () _git_revert () { - local dir="$(__gitdir)" - if [ -f "$dir"/REVERT_HEAD ]; then + __git_find_repo_path + if [ -f "$__git_repo_path"/REVERT_HEAD ]; then __gitcomp "--continue --quit --abort" return fi @@ -2606,12 +2669,12 @@ _git_stash () if [ $cword -eq 3 ]; then __gitcomp_nl "$(__git_refs)"; else - __gitcomp_nl "$(git --git-dir="$(__gitdir)" stash list \ + __gitcomp_nl "$(__git stash list \ | sed -n -e 's/:.*//p')" fi ;; show,*|apply,*|drop,*|pop,*) - __gitcomp_nl "$(git --git-dir="$(__gitdir)" stash list \ + __gitcomp_nl "$(__git stash list \ | sed -n -e 's/:.*//p')" ;; *) @@ -2838,7 +2901,8 @@ _git_worktree () __git_main () { - local i c=1 command __git_dir + local i c=1 command __git_dir __git_repo_path + local __git_C_args C_args_count=0 while [ $c -lt $cword ]; do i="${words[c]}" @@ -2848,6 +2912,10 @@ __git_main () --bare) __git_dir="." ;; --help) command="help"; break ;; -c|--work-tree|--namespace) ((c++)) ;; + -C) __git_C_args[C_args_count++]=-C + ((c++)) + __git_C_args[C_args_count++]="${words[c]}" + ;; -*) ;; *) command="$i"; break ;; esac @@ -2855,6 +2923,17 @@ __git_main () done if [ -z "$command" ]; then + case "$prev" in + --git-dir|-C|--work-tree) + # these need a path argument, let's fall back to + # Bash filename completion + return + ;; + -c|--namespace) + # we don't support completing these options' arguments + return + ;; + esac case "$cur" in --*) __gitcomp " --paginate @@ -2880,13 +2959,13 @@ __git_main () fi local completion_func="_git_${command//-/_}" - declare -f $completion_func >/dev/null && $completion_func && return + declare -f $completion_func >/dev/null 2>/dev/null && $completion_func && return local expansion=$(__git_aliased_command "$command") if [ -n "$expansion" ]; then words[1]=$expansion completion_func="_git_${expansion//-/_}" - declare -f $completion_func >/dev/null && $completion_func + declare -f $completion_func >/dev/null 2>/dev/null && $completion_func fi } @@ -2894,9 +2973,11 @@ __gitk_main () { __git_has_doubledash && return - local g="$(__gitdir)" + local __git_repo_path + __git_find_repo_path + local merge="" - if [ -f "$g/MERGE_HEAD" ]; then + if [ -f "$__git_repo_path/MERGE_HEAD" ]; then merge="--merge" fi case "$cur" in @@ -1028,7 +1028,7 @@ static int ident_to_worktree(const char *path, const char *src, size_t len, return 1; } -static enum crlf_action git_path_check_crlf(struct git_attr_check *check) +static enum crlf_action git_path_check_crlf(struct attr_check_item *check) { const char *value = check->value; @@ -1045,7 +1045,7 @@ static enum crlf_action git_path_check_crlf(struct git_attr_check *check) return CRLF_UNDEFINED; } -static enum eol git_path_check_eol(struct git_attr_check *check) +static enum eol git_path_check_eol(struct attr_check_item *check) { const char *value = check->value; @@ -1058,7 +1058,7 @@ static enum eol git_path_check_eol(struct git_attr_check *check) return EOL_UNSET; } -static struct convert_driver *git_path_check_convert(struct git_attr_check *check) +static struct convert_driver *git_path_check_convert(struct attr_check_item *check) { const char *value = check->value; struct convert_driver *drv; @@ -1071,7 +1071,7 @@ static struct convert_driver *git_path_check_convert(struct git_attr_check *chec return NULL; } -static int git_path_check_ident(struct git_attr_check *check) +static int git_path_check_ident(struct attr_check_item *check) { const char *value = check->value; @@ -1085,24 +1085,19 @@ struct conv_attrs { int ident; }; -static const char *conv_attr_name[] = { - "crlf", "ident", "filter", "eol", "text", -}; -#define NUM_CONV_ATTRS ARRAY_SIZE(conv_attr_name) - static void convert_attrs(struct conv_attrs *ca, const char *path) { - int i; - static struct git_attr_check ccheck[NUM_CONV_ATTRS]; + static struct attr_check *check; - if (!ccheck[0].attr) { - for (i = 0; i < NUM_CONV_ATTRS; i++) - ccheck[i].attr = git_attr(conv_attr_name[i]); + if (!check) { + check = attr_check_initl("crlf", "ident", "filter", + "eol", "text", NULL); user_convert_tail = &user_convert; git_config(read_convert_config, NULL); } - if (!git_check_attr(path, NUM_CONV_ATTRS, ccheck)) { + if (!git_check_attr(path, check)) { + struct attr_check_item *ccheck = check->items; ca->crlf_action = git_path_check_crlf(ccheck + 4); if (ca->crlf_action == CRLF_UNDEFINED) ca->crlf_action = git_path_check_crlf(ccheck + 0); diff --git a/fast-import.c b/fast-import.c index 64fe602f0b..6c13472c42 100644 --- a/fast-import.c +++ b/fast-import.c @@ -1752,7 +1752,7 @@ static int update_branch(struct branch *b) if (is_null_sha1(b->sha1)) { if (b->delete) - delete_ref(b->name, NULL, 0); + delete_ref(NULL, b->name, NULL, 0); return 0; } if (read_ref(b->name, old_sha1)) diff --git a/fetch-pack.c b/fetch-pack.c index 601f0779a1..e0f5d5ce87 100644 --- a/fetch-pack.c +++ b/fetch-pack.c @@ -35,6 +35,7 @@ static const char *alternate_shallow_file; #define COMMON_REF (1U << 2) #define SEEN (1U << 3) #define POPPED (1U << 4) +#define ALTERNATE (1U << 5) static int marked; @@ -67,6 +68,41 @@ static inline void print_verbose(const struct fetch_pack_args *args, fputc('\n', stderr); } +struct alternate_object_cache { + struct object **items; + size_t nr, alloc; +}; + +static void cache_one_alternate(const char *refname, + const struct object_id *oid, + void *vcache) +{ + struct alternate_object_cache *cache = vcache; + struct object *obj = parse_object(oid->hash); + + if (!obj || (obj->flags & ALTERNATE)) + return; + + obj->flags |= ALTERNATE; + ALLOC_GROW(cache->items, cache->nr + 1, cache->alloc); + cache->items[cache->nr++] = obj; +} + +static void for_each_cached_alternate(void (*cb)(struct object *)) +{ + static int initialized; + static struct alternate_object_cache cache; + size_t i; + + if (!initialized) { + for_each_alternate_ref(cache_one_alternate, &cache); + initialized = 1; + } + + for (i = 0; i < cache.nr; i++) + cb(cache.items[i]); +} + static void rev_list_push(struct commit *commit, int mark) { if (!(commit->object.flags & mark)) { @@ -253,9 +289,9 @@ static void send_request(struct fetch_pack_args *args, write_or_die(fd, buf->buf, buf->len); } -static void insert_one_alternate_ref(const struct ref *ref, void *unused) +static void insert_one_alternate_object(struct object *obj) { - rev_list_insert_ref(NULL, ref->old_oid.hash); + rev_list_insert_ref(NULL, obj->oid.hash); } #define INITIAL_FLUSH 16 @@ -298,7 +334,7 @@ static int find_common(struct fetch_pack_args *args, marked = 1; for_each_ref(rev_list_insert_ref_oid, NULL); - for_each_alternate_ref(insert_one_alternate_ref, NULL); + for_each_cached_alternate(insert_one_alternate_object); fetching = 0; for ( ; refs ; refs = refs->next) { @@ -619,9 +655,9 @@ static void filter_refs(struct fetch_pack_args *args, *refs = newlist; } -static void mark_alternate_complete(const struct ref *ref, void *unused) +static void mark_alternate_complete(struct object *obj) { - mark_complete(ref->old_oid.hash); + mark_complete(obj->oid.hash); } static int everything_local(struct fetch_pack_args *args, @@ -657,7 +693,7 @@ static int everything_local(struct fetch_pack_args *args, if (!args->deepen) { for_each_ref(mark_complete_oid, NULL); - for_each_alternate_ref(mark_alternate_complete, NULL); + for_each_cached_alternate(mark_alternate_complete); commit_list_sort_by_date(&complete); if (cutoff) mark_recent_complete_commits(args, cutoff); diff --git a/git-compat-util.h b/git-compat-util.h index ef6d560e15..e626851fe9 100644 --- a/git-compat-util.h +++ b/git-compat-util.h @@ -639,11 +639,6 @@ extern int gitsetenv(const char *, const char *, int); extern char *gitmkdtemp(char *); #endif -#ifdef NO_MKSTEMPS -#define mkstemps gitmkstemps -extern int gitmkstemps(char *, int); -#endif - #ifdef NO_UNSETENV #define unsetenv gitunsetenv extern void gitunsetenv(const char *); diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh index 4734094a3f..2c9c0165b5 100644 --- a/git-rebase--interactive.sh +++ b/git-rebase--interactive.sh @@ -1069,6 +1069,10 @@ git_rebase__interactive () { case "$action" in continue) + if test ! -d "$rewritten" + then + exec git rebase--helper ${force_rebase:+--no-ff} --continue + fi # do we have anything to commit? if git diff-index --cached --quiet HEAD -- then @@ -1128,6 +1132,10 @@ first and then run 'git rebase --continue' again.")" skip) git rerere clear + if test ! -d "$rewritten" + then + exec git rebase--helper ${force_rebase:+--no-ff} --continue + fi do_rest return 0 ;; @@ -1314,6 +1322,11 @@ expand_todo_ids test -d "$rewritten" || test -n "$force_rebase" || skip_unnecessary_picks checkout_onto +if test -z "$rebase_root" && test ! -d "$rewritten" +then + require_clean_work_tree "rebase" + exec git rebase--helper ${force_rebase:+--no-ff} --continue +fi do_rest } diff --git a/git-send-email.perl b/git-send-email.perl index 068d60b3e6..eea0a517f7 100755 --- a/git-send-email.perl +++ b/git-send-email.perl @@ -1563,7 +1563,7 @@ foreach my $t (@files) { # Now parse the message body while(<$fh>) { $message .= $_; - if (/^(Signed-off-by|Cc): (.*)$/i) { + if (/^(Signed-off-by|Cc): ([^>]*>?)/i) { chomp; my ($what, $c) = ($1, $2); chomp $c; diff --git a/git-stash.sh b/git-stash.sh index 10c284d1aa..9c70662cc8 100755 --- a/git-stash.sh +++ b/git-stash.sh @@ -7,8 +7,11 @@ USAGE="list [<options>] or: $dashless drop [-q|--quiet] [<stash>] or: $dashless ( pop | apply ) [--index] [-q|--quiet] [<stash>] or: $dashless branch <branchname> [<stash>] - or: $dashless [save [--patch] [-k|--[no-]keep-index] [-q|--quiet] - [-u|--include-untracked] [-a|--all] [<message>]] + or: $dashless save [--patch] [-k|--[no-]keep-index] [-q|--quiet] + [-u|--include-untracked] [-a|--all] [<message>] + or: $dashless [push [--patch] [-k|--[no-]keep-index] [-q|--quiet] + [-u|--include-untracked] [-a|--all] [-m <message>] + [-- <pathspec>...]] or: $dashless clear" SUBDIRECTORY_OK=Yes @@ -33,15 +36,15 @@ else fi no_changes () { - git diff-index --quiet --cached HEAD --ignore-submodules -- && - git diff-files --quiet --ignore-submodules && + git diff-index --quiet --cached HEAD --ignore-submodules -- "$@" && + git diff-files --quiet --ignore-submodules -- "$@" && (test -z "$untracked" || test -z "$(untracked_files)") } untracked_files () { excl_opt=--exclude-standard test "$untracked" = "all" && excl_opt= - git ls-files -o -z $excl_opt + git ls-files -o -z $excl_opt -- "$@" } clear_stash () { @@ -56,11 +59,29 @@ clear_stash () { } create_stash () { - stash_msg="$1" - untracked="$2" + stash_msg= + untracked= + while test $# != 0 + do + case "$1" in + -m|--message) + shift + stash_msg=${1?"BUG: create_stash () -m requires an argument"} + ;; + -u|--include-untracked) + shift + untracked=${1?"BUG: create_stash () -u requires an argument"} + ;; + --) + shift + break + ;; + esac + shift + done git update-index -q --refresh - if no_changes + if no_changes "$@" then exit 0 fi @@ -92,7 +113,7 @@ create_stash () { # Untracked files are stored by themselves in a parentless commit, for # ease of unpacking later. u_commit=$( - untracked_files | ( + untracked_files "$@" | ( GIT_INDEX_FILE="$TMPindex" && export GIT_INDEX_FILE && rm -f "$TMPindex" && @@ -115,7 +136,7 @@ create_stash () { git read-tree --index-output="$TMPindex" -m $i_tree && GIT_INDEX_FILE="$TMPindex" && export GIT_INDEX_FILE && - git diff-index --name-only -z HEAD -- >"$TMP-stagenames" && + git diff-index --name-only -z HEAD -- "$@" >"$TMP-stagenames" && git update-index -z --add --remove --stdin <"$TMP-stagenames" && git write-tree && rm -f "$TMPindex" @@ -129,7 +150,7 @@ create_stash () { # find out what the user wants GIT_INDEX_FILE="$TMP-index" \ - git add--interactive --patch=stash -- && + git add--interactive --patch=stash -- "$@" && # state of the working tree w_tree=$(GIT_INDEX_FILE="$TMP-index" git write-tree) || @@ -189,10 +210,11 @@ store_stash () { return $ret } -save_stash () { +push_stash () { keep_index= patch_mode= untracked= + stash_msg= while test $# != 0 do case "$1" in @@ -216,6 +238,11 @@ save_stash () { -a|--all) untracked=all ;; + -m|--message) + shift + test -z ${1+x} && usage + stash_msg=$1 + ;; --help) show_help ;; @@ -251,29 +278,38 @@ save_stash () { die "$(gettext "Can't use --patch and --include-untracked or --all at the same time")" fi - stash_msg="$*" + test -n "$untracked" || git ls-files --error-unmatch -- "$@" >/dev/null || exit 1 git update-index -q --refresh - if no_changes + if no_changes "$@" then say "$(gettext "No local changes to save")" exit 0 fi + git reflog exists $ref_stash || clear_stash || die "$(gettext "Cannot initialize stash")" - create_stash "$stash_msg" $untracked + create_stash -m "$stash_msg" -u "$untracked" -- "$@" store_stash -m "$stash_msg" -q $w_commit || die "$(gettext "Cannot save the current status")" say "$(eval_gettext "Saved working directory and index state \$stash_msg")" if test -z "$patch_mode" then - git reset --hard ${GIT_QUIET:+-q} + if test $# != 0 + then + git reset ${GIT_QUIET:+-q} -- "$@" + git ls-files -z --modified -- "$@" | + git checkout-index -z --force --stdin + git clean --force ${GIT_QUIET:+-q} -d -- "$@" + else + git reset --hard ${GIT_QUIET:+-q} + fi test "$untracked" = "all" && CLEAN_X_OPTION=-x || CLEAN_X_OPTION= if test -n "$untracked" then - git clean --force --quiet -d $CLEAN_X_OPTION + git clean --force --quiet -d $CLEAN_X_OPTION -- "$@" fi if test "$keep_index" = "t" && test -n "$i_tree" @@ -291,6 +327,36 @@ save_stash () { fi } +save_stash () { + push_options= + while test $# != 0 + do + case "$1" in + --) + shift + break + ;; + -*) + # pass all options through to push_stash + push_options="$push_options $1" + ;; + *) + break + ;; + esac + shift + done + + stash_msg="$*" + + if test -z "$stash_msg" + then + push_stash $push_options + else + push_stash $push_options -m "$stash_msg" + fi +} + have_stash () { git rev-parse --verify --quiet $ref_stash >/dev/null } @@ -590,18 +656,21 @@ apply_to_branch () { } } +test "$1" = "-p" && set "push" "$@" + PARSE_CACHE='--not-parsed' -# The default command is "save" if nothing but options are given +# The default command is "push" if nothing but options are given seen_non_option= for opt do case "$opt" in + --) break ;; -*) ;; *) seen_non_option=t; break ;; esac done -test -n "$seen_non_option" || set "save" "$@" +test -n "$seen_non_option" || set "push" "$@" # Main command set case "$1" in @@ -617,6 +686,10 @@ save) shift save_stash "$@" ;; +push) + shift + push_stash "$@" + ;; apply) shift apply_stash "$@" @@ -627,7 +700,7 @@ clear) ;; create) shift - create_stash "$*" && echo "$w_commit" + create_stash -m "$*" && echo "$w_commit" ;; store) shift @@ -648,7 +721,7 @@ branch) *) case $# in 0) - save_stash && + push_stash && say "$(gettext "(To restore them type \"git stash apply\")")" ;; *) diff --git a/git-svn.perl b/git-svn.perl index fa42364785..d2404184ba 100755 --- a/git-svn.perl +++ b/git-svn.perl @@ -1175,10 +1175,10 @@ sub cmd_branch { ::_req_svn(); require SVN::Client; + my ($config, $baton, undef) = Git::SVN::Ra::prepare_config_once(); my $ctx = SVN::Client->new( - config => SVN::Core::config_get_config( - $Git::SVN::Ra::config_dir - ), + auth => $baton, + config => $config, log_msg => sub { ${ $_[0] } = defined $_message ? $_message @@ -473,6 +473,7 @@ static struct cmd_struct commands[] = { { "pull", cmd_pull, RUN_SETUP | NEED_WORK_TREE }, { "push", cmd_push, RUN_SETUP }, { "read-tree", cmd_read_tree, RUN_SETUP | SUPPORT_SUPER_PREFIX}, + { "rebase--helper", cmd_rebase__helper, RUN_SETUP | NEED_WORK_TREE }, { "receive-pack", cmd_receive_pack }, { "reflog", cmd_reflog, RUN_SETUP }, { "remote", cmd_remote, RUN_SETUP }, @@ -109,7 +109,7 @@ static int curl_save_cookies; struct credential http_auth = CREDENTIAL_INIT; static int http_proactive_auth; static const char *user_agent; -static int curl_empty_auth; +static int curl_empty_auth = -1; enum http_follow_config http_follow_config = HTTP_FOLLOW_INITIAL; @@ -125,6 +125,14 @@ static struct credential cert_auth = CREDENTIAL_INIT; static int ssl_cert_password_required; #ifdef LIBCURL_CAN_HANDLE_AUTH_ANY static unsigned long http_auth_methods = CURLAUTH_ANY; +static int http_auth_methods_restricted; +/* Modes for which empty_auth cannot actually help us. */ +static unsigned long empty_auth_useless = + CURLAUTH_BASIC +#ifdef CURLAUTH_DIGEST_IE + | CURLAUTH_DIGEST_IE +#endif + | CURLAUTH_DIGEST; #endif static struct curl_slist *pragma_header; @@ -333,7 +341,10 @@ static int http_options(const char *var, const char *value, void *cb) return git_config_string(&user_agent, var, value); if (!strcmp("http.emptyauth", var)) { - curl_empty_auth = git_config_bool(var, value); + if (value && !strcmp("auto", value)) + curl_empty_auth = -1; + else + curl_empty_auth = git_config_bool(var, value); return 0; } @@ -382,10 +393,37 @@ static int http_options(const char *var, const char *value, void *cb) return git_default_config(var, value, cb); } +static int curl_empty_auth_enabled(void) +{ + if (curl_empty_auth >= 0) + return curl_empty_auth; + +#ifndef LIBCURL_CAN_HANDLE_AUTH_ANY + /* + * Our libcurl is too old to do AUTH_ANY in the first place; + * just default to turning the feature off. + */ +#else + /* + * In the automatic case, kick in the empty-auth + * hack as long as we would potentially try some + * method more exotic than "Basic" or "Digest". + * + * But only do this when this is our second or + * subsequent request, as by then we know what + * methods are available. + */ + if (http_auth_methods_restricted && + (http_auth_methods & ~empty_auth_useless)) + return 1; +#endif + return 0; +} + static void init_curl_http_auth(CURL *result) { if (!http_auth.username || !*http_auth.username) { - if (curl_empty_auth) + if (curl_empty_auth_enabled()) curl_easy_setopt(result, CURLOPT_USERPWD, ":"); return; } @@ -1079,7 +1117,7 @@ struct active_request_slot *get_active_slot(void) #ifdef LIBCURL_CAN_HANDLE_AUTH_ANY curl_easy_setopt(slot->curl, CURLOPT_HTTPAUTH, http_auth_methods); #endif - if (http_auth.password || curl_empty_auth) + if (http_auth.password || curl_empty_auth_enabled()) init_curl_http_auth(slot->curl); return slot; @@ -1347,6 +1385,10 @@ static int handle_curl_result(struct slot_results *results) } else { #ifdef LIBCURL_CAN_HANDLE_AUTH_ANY http_auth_methods &= ~CURLAUTH_GSSNEGOTIATE; + if (results->auth_avail) { + http_auth_methods &= results->auth_avail; + http_auth_methods_restricted = 1; + } #endif return HTTP_REAUTH; } @@ -1727,6 +1769,9 @@ static int http_request_reauth(const char *url, { int ret = http_request(url, result, target, options); + if (ret != HTTP_OK && ret != HTTP_REAUTH) + return ret; + if (options && options->effective_url && options->base_url) { if (update_url_from_redirect(options->base_url, url, options->effective_url)) { @@ -153,7 +153,7 @@ static void copy_email(const struct passwd *pw, struct strbuf *email, const char *ident_default_name(void) { - if (!git_default_name.len) { + if (!(ident_config_given & IDENT_NAME_GIVEN) && !git_default_name.len) { copy_gecos(xgetpwuid_self(&default_name_is_bogus), &git_default_name); strbuf_trim(&git_default_name); } @@ -162,7 +162,7 @@ const char *ident_default_name(void) const char *ident_default_email(void) { - if (!git_default_email.len) { + if (!(ident_config_given & IDENT_MAIL_GIVEN) && !git_default_email.len) { const char *email = getenv("EMAIL"); if (email && email[0]) { @@ -203,6 +203,15 @@ static int crud(unsigned char c) c == '\''; } +static int has_non_crud(const char *str) +{ + for (; *str; str++) { + if (!crud(*str)) + return 1; + } + return 0; +} + /* * Copy over a string to the destination, but avoid special * characters ('\n', '<' and '>') and remove crud at the end @@ -351,19 +360,32 @@ const char *fmt_ident(const char *name, const char *email, int want_date = !(flag & IDENT_NO_DATE); int want_name = !(flag & IDENT_NO_NAME); + if (!email) { + if (strict && ident_use_config_only + && !(ident_config_given & IDENT_MAIL_GIVEN)) { + fputs(_(env_hint), stderr); + die(_("no email was given and auto-detection is disabled")); + } + email = ident_default_email(); + if (strict && default_email_is_bogus) { + fputs(_(env_hint), stderr); + die(_("unable to auto-detect email address (got '%s')"), email); + } + } + if (want_name) { int using_default = 0; if (!name) { if (strict && ident_use_config_only && !(ident_config_given & IDENT_NAME_GIVEN)) { fputs(_(env_hint), stderr); - die("no name was given and auto-detection is disabled"); + die(_("no name was given and auto-detection is disabled")); } name = ident_default_name(); using_default = 1; if (strict && default_name_is_bogus) { fputs(_(env_hint), stderr); - die("unable to auto-detect name (got '%s')", name); + die(_("unable to auto-detect name (got '%s')"), name); } } if (!*name) { @@ -371,24 +393,13 @@ const char *fmt_ident(const char *name, const char *email, if (strict) { if (using_default) fputs(_(env_hint), stderr); - die("empty ident name (for <%s>) not allowed", email); + die(_("empty ident name (for <%s>) not allowed"), email); } pw = xgetpwuid_self(NULL); name = pw->pw_name; } - } - - if (!email) { - if (strict && ident_use_config_only - && !(ident_config_given & IDENT_MAIL_GIVEN)) { - fputs(_(env_hint), stderr); - die("no email was given and auto-detection is disabled"); - } - email = ident_default_email(); - if (strict && default_email_is_bogus) { - fputs(_(env_hint), stderr); - die("unable to auto-detect email address (got '%s')", email); - } + if (strict && !has_non_crud(name)) + die(_("name consists only of disallowed characters: %s"), name); } strbuf_reset(&ident); @@ -403,7 +414,7 @@ const char *fmt_ident(const char *name, const char *email, strbuf_addch(&ident, ' '); if (date_str && date_str[0]) { if (parse_date(date_str, &ident) < 0) - die("invalid date format: %s", date_str); + die(_("invalid date format: %s"), date_str); } else strbuf_addstr(&ident, ident_default_date()); diff --git a/ll-merge.c b/ll-merge.c index ad8be42f91..ac0d4a5d78 100644 --- a/ll-merge.c +++ b/ll-merge.c @@ -336,15 +336,6 @@ static const struct ll_merge_driver *find_ll_merge_driver(const char *merge_attr return &ll_merge_drv[LL_TEXT_MERGE]; } -static int git_path_check_merge(const char *path, struct git_attr_check check[2]) -{ - if (!check[0].attr) { - check[0].attr = git_attr("merge"); - check[1].attr = git_attr("conflict-marker-size"); - } - return git_check_attr(path, 2, check); -} - static void normalize_file(mmfile_t *mm, const char *path) { struct strbuf strbuf = STRBUF_INIT; @@ -362,7 +353,7 @@ int ll_merge(mmbuffer_t *result_buf, mmfile_t *theirs, const char *their_label, const struct ll_merge_options *opts) { - static struct git_attr_check check[2]; + static struct attr_check *check; static const struct ll_merge_options default_opts; const char *ll_driver_name = NULL; int marker_size = DEFAULT_CONFLICT_MARKER_SIZE; @@ -376,10 +367,14 @@ int ll_merge(mmbuffer_t *result_buf, normalize_file(ours, path); normalize_file(theirs, path); } - if (!git_path_check_merge(path, check)) { - ll_driver_name = check[0].value; - if (check[1].value) { - marker_size = atoi(check[1].value); + + if (!check) + check = attr_check_initl("merge", "conflict-marker-size", NULL); + + if (!git_check_attr(path, check)) { + ll_driver_name = check->items[0].value; + if (check->items[1].value) { + marker_size = atoi(check->items[1].value); if (marker_size <= 0) marker_size = DEFAULT_CONFLICT_MARKER_SIZE; } @@ -398,13 +393,13 @@ int ll_merge(mmbuffer_t *result_buf, int ll_merge_marker_size(const char *path) { - static struct git_attr_check check; + static struct attr_check *check; int marker_size = DEFAULT_CONFLICT_MARKER_SIZE; - if (!check.attr) - check.attr = git_attr("conflict-marker-size"); - if (!git_check_attr(path, 1, &check) && check.value) { - marker_size = atoi(check.value); + if (!check) + check = attr_check_initl("conflict-marker-size", NULL); + if (!git_check_attr(path, check) && check->items[0].value) { + marker_size = atoi(check->items[0].value); if (marker_size <= 0) marker_size = DEFAULT_CONFLICT_MARKER_SIZE; } diff --git a/log-tree.c b/log-tree.c index 8c2415747a..4618dd04ca 100644 --- a/log-tree.c +++ b/log-tree.c @@ -332,35 +332,31 @@ void fmt_output_commit(struct strbuf *filename, strbuf_release(&subject); } +void fmt_output_email_subject(struct strbuf *sb, struct rev_info *opt) +{ + if (opt->total > 0) { + strbuf_addf(sb, "Subject: [%s%s%0*d/%d] ", + opt->subject_prefix, + *opt->subject_prefix ? " " : "", + digits_in_number(opt->total), + opt->nr, opt->total); + } else if (opt->total == 0 && opt->subject_prefix && *opt->subject_prefix) { + strbuf_addf(sb, "Subject: [%s] ", + opt->subject_prefix); + } else { + strbuf_addstr(sb, "Subject: "); + } +} + void log_write_email_headers(struct rev_info *opt, struct commit *commit, - const char **subject_p, const char **extra_headers_p, int *need_8bit_cte_p) { - const char *subject = NULL; const char *extra_headers = opt->extra_headers; const char *name = oid_to_hex(opt->zero_commit ? &null_oid : &commit->object.oid); *need_8bit_cte_p = 0; /* unknown */ - if (opt->total > 0) { - static char buffer[64]; - snprintf(buffer, sizeof(buffer), - "Subject: [%s%s%0*d/%d] ", - opt->subject_prefix, - *opt->subject_prefix ? " " : "", - digits_in_number(opt->total), - opt->nr, opt->total); - subject = buffer; - } else if (opt->total == 0 && opt->subject_prefix && *opt->subject_prefix) { - static char buffer[256]; - snprintf(buffer, sizeof(buffer), - "Subject: [%s] ", - opt->subject_prefix); - subject = buffer; - } else { - subject = "Subject: "; - } fprintf(opt->diffopt.file, "From %s Mon Sep 17 00:00:00 2001\n", name); graph_show_oneline(opt->graph); @@ -417,7 +413,6 @@ void log_write_email_headers(struct rev_info *opt, struct commit *commit, opt->diffopt.stat_sep = buffer; strbuf_release(&filename); } - *subject_p = subject; *extra_headers_p = extra_headers; } @@ -602,8 +597,10 @@ void show_log(struct rev_info *opt) */ if (cmit_fmt_is_mail(opt->commit_format)) { - log_write_email_headers(opt, commit, &ctx.subject, &extra_headers, + log_write_email_headers(opt, commit, &extra_headers, &ctx.need_8bit_cte); + ctx.rev = opt; + ctx.print_email_subject = 1; } else if (opt->commit_format != CMIT_FMT_USERFORMAT) { fputs(diff_get_color_opt(&opt->diffopt, DIFF_COMMIT), opt->diffopt.file); if (opt->commit_format != CMIT_FMT_ONELINE) diff --git a/log-tree.h b/log-tree.h index c8116e60cd..48f11fb740 100644 --- a/log-tree.h +++ b/log-tree.h @@ -22,7 +22,6 @@ void format_decorations_extended(struct strbuf *sb, const struct commit *commit, format_decorations_extended((strbuf), (commit), (color), " (", ", ", ")") void show_decorations(struct rev_info *opt, struct commit *commit); void log_write_email_headers(struct rev_info *opt, struct commit *commit, - const char **subject_p, const char **extra_headers_p, int *need_8bit_cte_p); void load_ref_decorations(int flags); @@ -30,5 +29,6 @@ void load_ref_decorations(int flags); #define FORMAT_PATCH_NAME_MAX 64 void fmt_output_commit(struct strbuf *, struct commit *, struct rev_info *); void fmt_output_subject(struct strbuf *, const char *subject, struct rev_info *); +void fmt_output_email_subject(struct strbuf *, struct rev_info *); #endif diff --git a/merge-recursive.c b/merge-recursive.c index b7ff1ada3c..62decd51cc 100644 --- a/merge-recursive.c +++ b/merge-recursive.c @@ -1061,16 +1061,20 @@ static int merge_file_one(struct merge_options *o, } static int handle_change_delete(struct merge_options *o, - const char *path, + const char *path, const char *old_path, const struct object_id *o_oid, int o_mode, - const struct object_id *a_oid, int a_mode, - const struct object_id *b_oid, int b_mode, + const struct object_id *changed_oid, + int changed_mode, + const char *change_branch, + const char *delete_branch, const char *change, const char *change_past) { - char *renamed = NULL; + char *alt_path = NULL; + const char *update_path = path; int ret = 0; + if (dir_in_way(path, !o->call_depth, 0)) { - renamed = unique_path(o, path, a_oid ? o->branch1 : o->branch2); + update_path = alt_path = unique_path(o, path, change_branch); } if (o->call_depth) { @@ -1081,43 +1085,43 @@ static int handle_change_delete(struct merge_options *o, */ ret = remove_file_from_cache(path); if (!ret) - ret = update_file(o, 0, o_oid, o_mode, - renamed ? renamed : path); - } else if (!a_oid) { - if (!renamed) { - output(o, 1, _("CONFLICT (%s/delete): %s deleted in %s " - "and %s in %s. Version %s of %s left in tree."), - change, path, o->branch1, change_past, - o->branch2, o->branch2, path); - ret = update_file(o, 0, b_oid, b_mode, path); - } else { - output(o, 1, _("CONFLICT (%s/delete): %s deleted in %s " - "and %s in %s. Version %s of %s left in tree at %s."), - change, path, o->branch1, change_past, - o->branch2, o->branch2, path, renamed); - ret = update_file(o, 0, b_oid, b_mode, renamed); - } + ret = update_file(o, 0, o_oid, o_mode, update_path); } else { - if (!renamed) { - output(o, 1, _("CONFLICT (%s/delete): %s deleted in %s " - "and %s in %s. Version %s of %s left in tree."), - change, path, o->branch2, change_past, - o->branch1, o->branch1, path); + if (!alt_path) { + if (!old_path) { + output(o, 1, _("CONFLICT (%s/delete): %s deleted in %s " + "and %s in %s. Version %s of %s left in tree."), + change, path, delete_branch, change_past, + change_branch, change_branch, path); + } else { + output(o, 1, _("CONFLICT (%s/delete): %s deleted in %s " + "and %s to %s in %s. Version %s of %s left in tree."), + change, old_path, delete_branch, change_past, path, + change_branch, change_branch, path); + } } else { - output(o, 1, _("CONFLICT (%s/delete): %s deleted in %s " - "and %s in %s. Version %s of %s left in tree at %s."), - change, path, o->branch2, change_past, - o->branch1, o->branch1, path, renamed); - ret = update_file(o, 0, a_oid, a_mode, renamed); + if (!old_path) { + output(o, 1, _("CONFLICT (%s/delete): %s deleted in %s " + "and %s in %s. Version %s of %s left in tree at %s."), + change, path, delete_branch, change_past, + change_branch, change_branch, path, alt_path); + } else { + output(o, 1, _("CONFLICT (%s/delete): %s deleted in %s " + "and %s to %s in %s. Version %s of %s left in tree at %s."), + change, old_path, delete_branch, change_past, path, + change_branch, change_branch, path, alt_path); + } } /* - * No need to call update_file() on path when !renamed, since - * that would needlessly touch path. We could call - * update_file_flags() with update_cache=0 and update_wd=0, - * but that's a no-op. + * No need to call update_file() on path when change_branch == + * o->branch1 && !alt_path, since that would needlessly touch + * path. We could call update_file_flags() with update_cache=0 + * and update_wd=0, but that's a no-op. */ + if (change_branch != o->branch1 || alt_path) + ret = update_file(o, 0, changed_oid, changed_mode, update_path); } - free(renamed); + free(alt_path); return ret; } @@ -1125,28 +1129,17 @@ static int handle_change_delete(struct merge_options *o, static int conflict_rename_delete(struct merge_options *o, struct diff_filepair *pair, const char *rename_branch, - const char *other_branch) + const char *delete_branch) { const struct diff_filespec *orig = pair->one; const struct diff_filespec *dest = pair->two; - const struct object_id *a_oid = NULL; - const struct object_id *b_oid = NULL; - int a_mode = 0; - int b_mode = 0; - - if (rename_branch == o->branch1) { - a_oid = &dest->oid; - a_mode = dest->mode; - } else { - b_oid = &dest->oid; - b_mode = dest->mode; - } if (handle_change_delete(o, o->call_depth ? orig->path : dest->path, + o->call_depth ? NULL : orig->path, &orig->oid, orig->mode, - a_oid, a_mode, - b_oid, b_mode, + &dest->oid, dest->mode, + rename_branch, delete_branch, _("rename"), _("renamed"))) return -1; @@ -1662,11 +1655,27 @@ static int handle_modify_delete(struct merge_options *o, struct object_id *a_oid, int a_mode, struct object_id *b_oid, int b_mode) { + const char *modify_branch, *delete_branch; + struct object_id *changed_oid; + int changed_mode; + + if (a_oid) { + modify_branch = o->branch1; + delete_branch = o->branch2; + changed_oid = a_oid; + changed_mode = a_mode; + } else { + modify_branch = o->branch2; + delete_branch = o->branch1; + changed_oid = b_oid; + changed_mode = b_mode; + } + return handle_change_delete(o, - path, + path, NULL, o_oid, o_mode, - a_oid, a_mode, - b_oid, b_mode, + changed_oid, changed_mode, + modify_branch, delete_branch, _("modify"), _("modified")); } @@ -29,7 +29,7 @@ struct object_array { /* * object flag allocation: * revision.h: 0---------10 26 - * fetch-pack.c: 0---4 + * fetch-pack.c: 0---5 * walker.c: 0-2 * upload-pack.c: 4 11----------------19 * builtin/blame.c: 12-13 diff --git a/oidset.c b/oidset.c new file mode 100644 index 0000000000..ac169f05d3 --- /dev/null +++ b/oidset.c @@ -0,0 +1,49 @@ +#include "cache.h" +#include "oidset.h" + +struct oidset_entry { + struct hashmap_entry hash; + struct object_id oid; +}; + +static int oidset_hashcmp(const void *va, const void *vb, + const void *vkey) +{ + const struct oidset_entry *a = va, *b = vb; + const struct object_id *key = vkey; + return oidcmp(&a->oid, key ? key : &b->oid); +} + +int oidset_contains(const struct oidset *set, const struct object_id *oid) +{ + struct hashmap_entry key; + + if (!set->map.cmpfn) + return 0; + + hashmap_entry_init(&key, sha1hash(oid->hash)); + return !!hashmap_get(&set->map, &key, oid); +} + +int oidset_insert(struct oidset *set, const struct object_id *oid) +{ + struct oidset_entry *entry; + + if (!set->map.cmpfn) + hashmap_init(&set->map, oidset_hashcmp, 0); + + if (oidset_contains(set, oid)) + return 1; + + entry = xmalloc(sizeof(*entry)); + hashmap_entry_init(&entry->hash, sha1hash(oid->hash)); + oidcpy(&entry->oid, oid); + + hashmap_add(&set->map, entry); + return 0; +} + +void oidset_clear(struct oidset *set) +{ + hashmap_free(&set->map, 1); +} diff --git a/oidset.h b/oidset.h new file mode 100644 index 0000000000..b7eaab5b88 --- /dev/null +++ b/oidset.h @@ -0,0 +1,45 @@ +#ifndef OIDSET_H +#define OIDSET_H + +/** + * This API is similar to sha1-array, in that it maintains a set of object ids + * in a memory-efficient way. The major differences are: + * + * 1. It uses a hash, so we can do online duplicate removal, rather than + * sort-and-uniq at the end. This can reduce memory footprint if you have + * a large list of oids with many duplicates. + * + * 2. The per-unique-oid memory footprint is slightly higher due to hash + * table overhead. + */ + +/** + * A single oidset; should be zero-initialized (or use OIDSET_INIT). + */ +struct oidset { + struct hashmap map; +}; + +#define OIDSET_INIT { { NULL } } + +/** + * Returns true iff `set` contains `oid`. + */ +int oidset_contains(const struct oidset *set, const struct object_id *oid); + +/** + * Insert the oid into the set; a copy is made, so "oid" does not need + * to persist after this function is called. + * + * Returns 1 if the oid was already in the set, 0 otherwise. This can be used + * to perform an efficient check-and-add. + */ +int oidset_insert(struct oidset *set, const struct object_id *oid); + +/** + * Remove all entries from the oidset, freeing any resources associated with + * it. + */ +void oidset_clear(struct oidset *set); + +#endif /* OIDSET_H */ diff --git a/pack-objects.h b/pack-objects.h index cc9b9a9b90..03f1191659 100644 --- a/pack-objects.h +++ b/pack-objects.h @@ -30,12 +30,16 @@ struct object_entry { /* * State flags for depth-first search used for analyzing delta cycles. + * + * The depth is measured in delta-links to the base (so if A is a delta + * against B, then A has a depth of 1, and B a depth of 0). */ enum { DFS_NONE = 0, DFS_ACTIVE, DFS_DONE } dfs_state; + int depth; }; struct packing_data { diff --git a/pathspec.c b/pathspec.c index 7ababb3159..b961f00c8c 100644 --- a/pathspec.c +++ b/pathspec.c @@ -224,6 +224,12 @@ static const char *parse_short_magic(unsigned *magic, const char *elem) char ch = *pos; int i; + /* Special case alias for '!' */ + if (ch == '^') { + *magic |= PATHSPEC_EXCLUDE; + continue; + } + if (!is_pathspec_magic(ch)) break; @@ -516,7 +522,7 @@ void parse_pathspec(struct pathspec *pathspec, } pathspec->nr = n; - ALLOC_ARRAY(pathspec->items, n); + ALLOC_ARRAY(pathspec->items, n + 1); item = pathspec->items; prefixlen = prefix ? strlen(prefix) : 0; @@ -540,10 +546,15 @@ void parse_pathspec(struct pathspec *pathspec, pathspec->magic |= item[i].magic; } - if (nr_exclude == n) - die(_("There is nothing to exclude from by :(exclude) patterns.\n" - "Perhaps you forgot to add either ':/' or '.' ?")); - + /* + * If everything is an exclude pattern, add one positive pattern + * that matches everyting. We allocated an extra one for this. + */ + if (nr_exclude == n) { + int plen = (!(flags & PATHSPEC_PREFER_CWD)) ? 0 : prefixlen; + init_pathspec_item(item + n, 0, prefix, plen, ""); + pathspec->nr++; + } if (pathspec->magic & PATHSPEC_MAXDEPTH) { if (flags & PATHSPEC_KEEP_ORDER) diff --git a/preload-index.c b/preload-index.c index c1fe3a3ef9..70a4c80878 100644 --- a/preload-index.c +++ b/preload-index.c @@ -53,6 +53,8 @@ static void *preload_thread(void *_data) continue; if (ce_uptodate(ce)) continue; + if (ce_skip_worktree(ce)) + continue; if (!ce_path_match(ce, &p->pathspec, NULL)) continue; if (threaded_has_symlink_leading_path(&cache, ce->name, ce_namelen(ce))) @@ -1607,8 +1607,9 @@ void pp_title_line(struct pretty_print_context *pp, pp->preserve_subject ? "\n" : " "); strbuf_grow(sb, title.len + 1024); - if (pp->subject) { - strbuf_addstr(sb, pp->subject); + if (pp->print_email_subject) { + if (pp->rev) + fmt_output_email_subject(sb, pp->rev); if (needs_rfc2047_encoding(title.buf, title.len, RFC2047_SUBJECT)) add_rfc2047(sb, title.buf, title.len, encoding, RFC2047_SUBJECT); @@ -1818,7 +1819,7 @@ void pretty_print_commit(struct pretty_print_context *pp, } pp_header(pp, encoding, commit, &msg, sb); - if (pp->fmt != CMIT_FMT_ONELINE && !pp->subject) { + if (pp->fmt != CMIT_FMT_ONELINE && !pp->print_email_subject) { strbuf_addch(sb, '\n'); } diff --git a/progress.c b/progress.c index 76a88c573f..29378caa05 100644 --- a/progress.c +++ b/progress.c @@ -243,21 +243,18 @@ void stop_progress_msg(struct progress **p_progress, const char *msg) *p_progress = NULL; if (progress->last_value != -1) { /* Force the last update */ - char buf[128], *bufp; - size_t len = strlen(msg) + 5; + char *buf; struct throughput *tp = progress->throughput; - bufp = (len < sizeof(buf)) ? buf : xmallocz(len); if (tp) { unsigned int rate = !tp->avg_misecs ? 0 : tp->avg_bytes / tp->avg_misecs; throughput_string(&tp->display, tp->curr_total, rate); } progress_update = 1; - xsnprintf(bufp, len + 1, ", %s.\n", msg); - display(progress, progress->last_value, bufp); - if (buf != bufp) - free(bufp); + buf = xstrfmt(", %s.\n", msg); + display(progress, progress->last_value, buf); + free(buf); } clear_progress_signal(); if (progress->throughput) diff --git a/ref-filter.c b/ref-filter.c index 3820b21cc7..1ec0fb8391 100644 --- a/ref-filter.c +++ b/ref-filter.c @@ -14,14 +14,50 @@ #include "git-compat-util.h" #include "version.h" #include "trailer.h" +#include "wt-status.h" + +static struct ref_msg { + const char *gone; + const char *ahead; + const char *behind; + const char *ahead_behind; +} msgs = { + /* Untranslated plumbing messages: */ + "gone", + "ahead %d", + "behind %d", + "ahead %d, behind %d" +}; + +void setup_ref_filter_porcelain_msg(void) +{ + msgs.gone = _("gone"); + msgs.ahead = _("ahead %d"); + msgs.behind = _("behind %d"); + msgs.ahead_behind = _("ahead %d, behind %d"); +} typedef enum { FIELD_STR, FIELD_ULONG, FIELD_TIME } cmp_type; +typedef enum { COMPARE_EQUAL, COMPARE_UNEQUAL, COMPARE_NONE } cmp_status; struct align { align_type position; unsigned int width; }; +struct if_then_else { + cmp_status cmp_status; + const char *str; + unsigned int then_atom_seen : 1, + else_atom_seen : 1, + condition_satisfied : 1; +}; + +struct refname_atom { + enum { R_NORMAL, R_SHORT, R_LSTRIP, R_RSTRIP } option; + int lstrip, rstrip; +}; + /* * An atom is a valid field atom listed below, possibly prefixed with * a "*" to denote deref_tag(). @@ -38,13 +74,24 @@ static struct used_atom { union { char color[COLOR_MAXLEN]; struct align align; - enum { RR_NORMAL, RR_SHORTEN, RR_TRACK, RR_TRACKSHORT } - remote_ref; + struct { + enum { RR_REF, RR_TRACK, RR_TRACKSHORT } option; + struct refname_atom refname; + unsigned int nobracket : 1; + } remote_ref; struct { enum { C_BARE, C_BODY, C_BODY_DEP, C_LINES, C_SIG, C_SUB, C_TRAILERS } option; unsigned int nlines; } contents; - enum { O_FULL, O_SHORT } objectname; + struct { + cmp_status cmp_status; + const char *str; + } if_then_else; + struct { + enum { O_FULL, O_LENGTH, O_SHORT } option; + unsigned int length; + } objectname; + struct refname_atom refname; } u; } *used_atom; static int used_atom_cnt, need_tagged, need_symref; @@ -58,18 +105,58 @@ static void color_atom_parser(struct used_atom *atom, const char *color_value) die(_("unrecognized color: %%(color:%s)"), color_value); } -static void remote_ref_atom_parser(struct used_atom *atom, const char *arg) +static void refname_atom_parser_internal(struct refname_atom *atom, + const char *arg, const char *name) { if (!arg) - atom->u.remote_ref = RR_NORMAL; + atom->option = R_NORMAL; else if (!strcmp(arg, "short")) - atom->u.remote_ref = RR_SHORTEN; - else if (!strcmp(arg, "track")) - atom->u.remote_ref = RR_TRACK; - else if (!strcmp(arg, "trackshort")) - atom->u.remote_ref = RR_TRACKSHORT; - else - die(_("unrecognized format: %%(%s)"), atom->name); + atom->option = R_SHORT; + else if (skip_prefix(arg, "lstrip=", &arg) || + skip_prefix(arg, "strip=", &arg)) { + atom->option = R_LSTRIP; + if (strtol_i(arg, 10, &atom->lstrip)) + die(_("Integer value expected refname:lstrip=%s"), arg); + } else if (skip_prefix(arg, "rstrip=", &arg)) { + atom->option = R_RSTRIP; + if (strtol_i(arg, 10, &atom->rstrip)) + die(_("Integer value expected refname:rstrip=%s"), arg); + } else + die(_("unrecognized %%(%s) argument: %s"), name, arg); +} + +static void remote_ref_atom_parser(struct used_atom *atom, const char *arg) +{ + struct string_list params = STRING_LIST_INIT_DUP; + int i; + + if (!arg) { + atom->u.remote_ref.option = RR_REF; + refname_atom_parser_internal(&atom->u.remote_ref.refname, + arg, atom->name); + return; + } + + atom->u.remote_ref.nobracket = 0; + string_list_split(¶ms, arg, ',', -1); + + for (i = 0; i < params.nr; i++) { + const char *s = params.items[i].string; + + if (!strcmp(s, "track")) + atom->u.remote_ref.option = RR_TRACK; + else if (!strcmp(s, "trackshort")) + atom->u.remote_ref.option = RR_TRACKSHORT; + else if (!strcmp(s, "nobracket")) + atom->u.remote_ref.nobracket = 1; + else { + atom->u.remote_ref.option = RR_REF; + refname_atom_parser_internal(&atom->u.remote_ref.refname, + arg, atom->name); + } + } + + string_list_clear(¶ms, 0); } static void body_atom_parser(struct used_atom *atom, const char *arg) @@ -116,13 +203,25 @@ static void contents_atom_parser(struct used_atom *atom, const char *arg) static void objectname_atom_parser(struct used_atom *atom, const char *arg) { if (!arg) - atom->u.objectname = O_FULL; + atom->u.objectname.option = O_FULL; else if (!strcmp(arg, "short")) - atom->u.objectname = O_SHORT; - else + atom->u.objectname.option = O_SHORT; + else if (skip_prefix(arg, "short=", &arg)) { + atom->u.objectname.option = O_LENGTH; + if (strtoul_ui(arg, 10, &atom->u.objectname.length) || + atom->u.objectname.length == 0) + die(_("positive value expected objectname:short=%s"), arg); + if (atom->u.objectname.length < MINIMUM_ABBREV) + atom->u.objectname.length = MINIMUM_ABBREV; + } else die(_("unrecognized %%(objectname) argument: %s"), arg); } +static void refname_atom_parser(struct used_atom *atom, const char *arg) +{ + return refname_atom_parser_internal(&atom->u.refname, arg, atom->name); +} + static align_type parse_align_position(const char *s) { if (!strcmp(s, "right")) @@ -173,12 +272,27 @@ static void align_atom_parser(struct used_atom *atom, const char *arg) string_list_clear(¶ms, 0); } +static void if_atom_parser(struct used_atom *atom, const char *arg) +{ + if (!arg) { + atom->u.if_then_else.cmp_status = COMPARE_NONE; + return; + } else if (skip_prefix(arg, "equals=", &atom->u.if_then_else.str)) { + atom->u.if_then_else.cmp_status = COMPARE_EQUAL; + } else if (skip_prefix(arg, "notequals=", &atom->u.if_then_else.str)) { + atom->u.if_then_else.cmp_status = COMPARE_UNEQUAL; + } else { + die(_("unrecognized %%(if) argument: %s"), arg); + } +} + + static struct { const char *name; cmp_type cmp_type; void (*parser)(struct used_atom *atom, const char *arg); } valid_atom[] = { - { "refname" }, + { "refname" , FIELD_STR, refname_atom_parser }, { "objecttype" }, { "objectsize", FIELD_ULONG }, { "objectname", FIELD_STR, objectname_atom_parser }, @@ -208,12 +322,15 @@ static struct { { "contents", FIELD_STR, contents_atom_parser }, { "upstream", FIELD_STR, remote_ref_atom_parser }, { "push", FIELD_STR, remote_ref_atom_parser }, - { "symref" }, + { "symref", FIELD_STR, refname_atom_parser }, { "flag" }, { "HEAD" }, { "color", FIELD_STR, color_atom_parser }, { "align", FIELD_STR, align_atom_parser }, { "end" }, + { "if", FIELD_STR, if_atom_parser }, + { "then" }, + { "else" }, }; #define REF_FORMATTING_STATE_INIT { 0, NULL } @@ -221,7 +338,7 @@ static struct { struct ref_formatting_stack { struct ref_formatting_stack *prev; struct strbuf output; - void (*at_end)(struct ref_formatting_stack *stack); + void (*at_end)(struct ref_formatting_stack **stack); void *at_end_data; }; @@ -232,11 +349,9 @@ struct ref_formatting_state { struct atom_value { const char *s; - union { - struct align align; - } u; void (*handler)(struct atom_value *atomv, struct ref_formatting_state *state); unsigned long ul; /* used for sorting when not FIELD_STR */ + struct used_atom *atom; }; /* @@ -293,7 +408,7 @@ int parse_ref_filter_atom(const char *atom, const char *ep) valid_atom[i].parser(&used_atom[at], arg); if (*atom == '*') need_tagged = 1; - if (!strcmp(used_atom[at].name, "symref")) + if (!strcmp(valid_atom[i].name, "symref")) need_symref = 1; return at; } @@ -354,13 +469,14 @@ static void pop_stack_element(struct ref_formatting_stack **stack) *stack = prev; } -static void end_align_handler(struct ref_formatting_stack *stack) +static void end_align_handler(struct ref_formatting_stack **stack) { - struct align *align = (struct align *)stack->at_end_data; + struct ref_formatting_stack *cur = *stack; + struct align *align = (struct align *)cur->at_end_data; struct strbuf s = STRBUF_INIT; - strbuf_utf8_align(&s, align->position, align->width, stack->output.buf); - strbuf_swap(&stack->output, &s); + strbuf_utf8_align(&s, align->position, align->width, cur->output.buf); + strbuf_swap(&cur->output, &s); strbuf_release(&s); } @@ -371,7 +487,115 @@ static void align_atom_handler(struct atom_value *atomv, struct ref_formatting_s push_stack_element(&state->stack); new = state->stack; new->at_end = end_align_handler; - new->at_end_data = &atomv->u.align; + new->at_end_data = &atomv->atom->u.align; +} + +static void if_then_else_handler(struct ref_formatting_stack **stack) +{ + struct ref_formatting_stack *cur = *stack; + struct ref_formatting_stack *prev = cur->prev; + struct if_then_else *if_then_else = (struct if_then_else *)cur->at_end_data; + + if (!if_then_else->then_atom_seen) + die(_("format: %%(if) atom used without a %%(then) atom")); + + if (if_then_else->else_atom_seen) { + /* + * There is an %(else) atom: we need to drop one state from the + * stack, either the %(else) branch if the condition is satisfied, or + * the %(then) branch if it isn't. + */ + if (if_then_else->condition_satisfied) { + strbuf_reset(&cur->output); + pop_stack_element(&cur); + } else { + strbuf_swap(&cur->output, &prev->output); + strbuf_reset(&cur->output); + pop_stack_element(&cur); + } + } else if (!if_then_else->condition_satisfied) { + /* + * No %(else) atom: just drop the %(then) branch if the + * condition is not satisfied. + */ + strbuf_reset(&cur->output); + } + + *stack = cur; + free(if_then_else); +} + +static void if_atom_handler(struct atom_value *atomv, struct ref_formatting_state *state) +{ + struct ref_formatting_stack *new; + struct if_then_else *if_then_else = xcalloc(sizeof(struct if_then_else), 1); + + if_then_else->str = atomv->atom->u.if_then_else.str; + if_then_else->cmp_status = atomv->atom->u.if_then_else.cmp_status; + + push_stack_element(&state->stack); + new = state->stack; + new->at_end = if_then_else_handler; + new->at_end_data = if_then_else; +} + +static int is_empty(const char *s) +{ + while (*s != '\0') { + if (!isspace(*s)) + return 0; + s++; + } + return 1; +} + +static void then_atom_handler(struct atom_value *atomv, struct ref_formatting_state *state) +{ + struct ref_formatting_stack *cur = state->stack; + struct if_then_else *if_then_else = NULL; + + if (cur->at_end == if_then_else_handler) + if_then_else = (struct if_then_else *)cur->at_end_data; + if (!if_then_else) + die(_("format: %%(then) atom used without an %%(if) atom")); + if (if_then_else->then_atom_seen) + die(_("format: %%(then) atom used more than once")); + if (if_then_else->else_atom_seen) + die(_("format: %%(then) atom used after %%(else)")); + if_then_else->then_atom_seen = 1; + /* + * If the 'equals' or 'notequals' attribute is used then + * perform the required comparison. If not, only non-empty + * strings satisfy the 'if' condition. + */ + if (if_then_else->cmp_status == COMPARE_EQUAL) { + if (!strcmp(if_then_else->str, cur->output.buf)) + if_then_else->condition_satisfied = 1; + } else if (if_then_else->cmp_status == COMPARE_UNEQUAL) { + if (strcmp(if_then_else->str, cur->output.buf)) + if_then_else->condition_satisfied = 1; + } else if (cur->output.len && !is_empty(cur->output.buf)) + if_then_else->condition_satisfied = 1; + strbuf_reset(&cur->output); +} + +static void else_atom_handler(struct atom_value *atomv, struct ref_formatting_state *state) +{ + struct ref_formatting_stack *prev = state->stack; + struct if_then_else *if_then_else = NULL; + + if (prev->at_end == if_then_else_handler) + if_then_else = (struct if_then_else *)prev->at_end_data; + if (!if_then_else) + die(_("format: %%(else) atom used without an %%(if) atom")); + if (!if_then_else->then_atom_seen) + die(_("format: %%(else) atom used without a %%(then) atom")); + if (if_then_else->else_atom_seen) + die(_("format: %%(else) atom used more than once")); + if_then_else->else_atom_seen = 1; + push_stack_element(&state->stack); + state->stack->at_end_data = prev->at_end_data; + state->stack->at_end = prev->at_end; } static void end_atom_handler(struct atom_value *atomv, struct ref_formatting_state *state) @@ -381,14 +605,17 @@ static void end_atom_handler(struct atom_value *atomv, struct ref_formatting_sta if (!current->at_end) die(_("format: %%(end) atom used without corresponding atom")); - current->at_end(current); + current->at_end(&state->stack); + + /* Stack may have been popped within at_end(), hence reset the current pointer */ + current = state->stack; /* * Perform quote formatting when the stack element is that of * a supporting atom. If nested then perform quote formatting * only on the topmost supporting atom. */ - if (!state->stack->prev->prev) { + if (!current->prev->prev) { quote_formatting(&s, current->output.buf, state->quote_style); strbuf_swap(¤t->output, &s); } @@ -465,12 +692,15 @@ static int grab_objectname(const char *name, const unsigned char *sha1, struct atom_value *v, struct used_atom *atom) { if (starts_with(name, "objectname")) { - if (atom->u.objectname == O_SHORT) { + if (atom->u.objectname.option == O_SHORT) { v->s = xstrdup(find_unique_abbrev(sha1, DEFAULT_ABBREV)); return 1; - } else if (atom->u.objectname == O_FULL) { + } else if (atom->u.objectname.option == O_FULL) { v->s = xstrdup(sha1_to_hex(sha1)); return 1; + } else if (atom->u.objectname.option == O_LENGTH) { + v->s = xstrdup(find_unique_abbrev(sha1, atom->u.objectname.length)); + return 1; } else die("BUG: unknown %%(objectname) option"); } @@ -887,50 +1117,108 @@ static inline char *copy_advance(char *dst, const char *src) return dst; } -static const char *strip_ref_components(const char *refname, const char *nr_arg) +static const char *lstrip_ref_components(const char *refname, int len) { - char *end; - long nr = strtol(nr_arg, &end, 10); - long remaining = nr; + long remaining = len; const char *start = refname; - if (nr < 1 || *end != '\0') - die(_(":strip= requires a positive integer argument")); + if (len < 0) { + int i; + const char *p = refname; + + /* Find total no of '/' separated path-components */ + for (i = 0; p[i]; p[i] == '/' ? i++ : *p++) + ; + /* + * The number of components we need to strip is now + * the total minus the components to be left (Plus one + * because we count the number of '/', but the number + * of components is one more than the no of '/'). + */ + remaining = i + len + 1; + } - while (remaining) { + while (remaining > 0) { switch (*start++) { case '\0': - die(_("ref '%s' does not have %ld components to :strip"), - refname, nr); + return ""; case '/': remaining--; break; } } + return start; } +static const char *rstrip_ref_components(const char *refname, int len) +{ + long remaining = len; + char *start = xstrdup(refname); + + if (len < 0) { + int i; + const char *p = refname; + + /* Find total no of '/' separated path-components */ + for (i = 0; p[i]; p[i] == '/' ? i++ : *p++) + ; + /* + * The number of components we need to strip is now + * the total minus the components to be left (Plus one + * because we count the number of '/', but the number + * of components is one more than the no of '/'). + */ + remaining = i + len + 1; + } + + while (remaining-- > 0) { + char *p = strrchr(start, '/'); + if (p == NULL) + return ""; + else + p[0] = '\0'; + } + return start; +} + +static const char *show_ref(struct refname_atom *atom, const char *refname) +{ + if (atom->option == R_SHORT) + return shorten_unambiguous_ref(refname, warn_ambiguous_refs); + else if (atom->option == R_LSTRIP) + return lstrip_ref_components(refname, atom->lstrip); + else if (atom->option == R_RSTRIP) + return rstrip_ref_components(refname, atom->rstrip); + else + return refname; +} + static void fill_remote_ref_details(struct used_atom *atom, const char *refname, struct branch *branch, const char **s) { int num_ours, num_theirs; - if (atom->u.remote_ref == RR_SHORTEN) - *s = shorten_unambiguous_ref(refname, warn_ambiguous_refs); - else if (atom->u.remote_ref == RR_TRACK) { + if (atom->u.remote_ref.option == RR_REF) + *s = show_ref(&atom->u.remote_ref.refname, refname); + else if (atom->u.remote_ref.option == RR_TRACK) { if (stat_tracking_info(branch, &num_ours, - &num_theirs, NULL)) - return; - - if (!num_ours && !num_theirs) + &num_theirs, NULL)) { + *s = xstrdup(msgs.gone); + } else if (!num_ours && !num_theirs) *s = ""; else if (!num_ours) - *s = xstrfmt("[behind %d]", num_theirs); + *s = xstrfmt(msgs.behind, num_theirs); else if (!num_theirs) - *s = xstrfmt("[ahead %d]", num_ours); + *s = xstrfmt(msgs.ahead, num_ours); else - *s = xstrfmt("[ahead %d, behind %d]", + *s = xstrfmt(msgs.ahead_behind, num_ours, num_theirs); - } else if (atom->u.remote_ref == RR_TRACKSHORT) { + if (!atom->u.remote_ref.nobracket && *s[0]) { + const char *to_free = *s; + *s = xstrfmt("[%s]", *s); + free((void *)to_free); + } + } else if (atom->u.remote_ref.option == RR_TRACKSHORT) { if (stat_tracking_info(branch, &num_ours, &num_theirs, NULL)) return; @@ -943,8 +1231,56 @@ static void fill_remote_ref_details(struct used_atom *atom, const char *refname, *s = ">"; else *s = "<>"; - } else /* RR_NORMAL */ - *s = refname; + } else + die("BUG: unhandled RR_* enum"); +} + +char *get_head_description(void) +{ + struct strbuf desc = STRBUF_INIT; + struct wt_status_state state; + memset(&state, 0, sizeof(state)); + wt_status_get_state(&state, 1); + if (state.rebase_in_progress || + state.rebase_interactive_in_progress) + strbuf_addf(&desc, _("(no branch, rebasing %s)"), + state.branch); + else if (state.bisect_in_progress) + strbuf_addf(&desc, _("(no branch, bisect started on %s)"), + state.branch); + else if (state.detached_from) { + if (state.detached_at) + /* TRANSLATORS: make sure this matches + "HEAD detached at " in wt-status.c */ + strbuf_addf(&desc, _("(HEAD detached at %s)"), + state.detached_from); + else + /* TRANSLATORS: make sure this matches + "HEAD detached from " in wt-status.c */ + strbuf_addf(&desc, _("(HEAD detached from %s)"), + state.detached_from); + } + else + strbuf_addstr(&desc, _("(no branch)")); + free(state.branch); + free(state.onto); + free(state.detached_from); + return strbuf_detach(&desc, NULL); +} + +static const char *get_symref(struct used_atom *atom, struct ref_array_item *ref) +{ + if (!ref->symref) + return ""; + else + return show_ref(&atom->u.refname, ref->symref); +} + +static const char *get_refname(struct used_atom *atom, struct ref_array_item *ref) +{ + if (ref->kind & FILTER_REFS_DETACHED_HEAD) + return get_head_description(); + return show_ref(&atom->u.refname, ref->refname); } /* @@ -975,10 +1311,10 @@ static void populate_value(struct ref_array_item *ref) struct atom_value *v = &ref->value[i]; int deref = 0; const char *refname; - const char *formatp; struct branch *branch = NULL; v->handler = append_atom; + v->atom = atom; if (*name == '*') { deref = 1; @@ -986,9 +1322,9 @@ static void populate_value(struct ref_array_item *ref) } if (starts_with(name, "refname")) - refname = ref->refname; + refname = get_refname(atom, ref); else if (starts_with(name, "symref")) - refname = ref->symref ? ref->symref : ""; + refname = get_symref(atom, ref); else if (starts_with(name, "upstream")) { const char *branch_name; /* only local branches may have an upstream */ @@ -1043,30 +1379,27 @@ static void populate_value(struct ref_array_item *ref) v->s = " "; continue; } else if (starts_with(name, "align")) { - v->u.align = atom->u.align; v->handler = align_atom_handler; continue; } else if (!strcmp(name, "end")) { v->handler = end_atom_handler; continue; + } else if (starts_with(name, "if")) { + const char *s; + + if (skip_prefix(name, "if:", &s)) + v->s = xstrdup(s); + v->handler = if_atom_handler; + continue; + } else if (!strcmp(name, "then")) { + v->handler = then_atom_handler; + continue; + } else if (!strcmp(name, "else")) { + v->handler = else_atom_handler; + continue; } else continue; - formatp = strchr(name, ':'); - if (formatp) { - const char *arg; - - formatp++; - if (!strcmp(formatp, "short")) - refname = shorten_unambiguous_ref(refname, - warn_ambiguous_refs); - else if (skip_prefix(formatp, "strip=", &arg)) - refname = strip_ref_components(refname, arg); - else - die(_("unknown %.*s format %s"), - (int)(formatp - name), name, formatp); - } - if (!deref) v->s = refname; else @@ -1635,10 +1968,10 @@ static void append_literal(const char *cp, const char *ep, struct ref_formatting } } -void show_ref_array_item(struct ref_array_item *info, const char *format, int quote_style) +void format_ref_array_item(struct ref_array_item *info, const char *format, + int quote_style, struct strbuf *final_buf) { const char *cp, *sp, *ep; - struct strbuf *final_buf; struct ref_formatting_state state = REF_FORMATTING_STATE_INIT; state.quote_style = quote_style; @@ -1668,9 +2001,17 @@ void show_ref_array_item(struct ref_array_item *info, const char *format, int qu } if (state.stack->prev) die(_("format: %%(end) atom missing")); - final_buf = &state.stack->output; - fwrite(final_buf->buf, 1, final_buf->len, stdout); + strbuf_addbuf(final_buf, &state.stack->output); pop_stack_element(&state.stack); +} + +void show_ref_array_item(struct ref_array_item *info, const char *format, int quote_style) +{ + struct strbuf final_buf = STRBUF_INIT; + + format_ref_array_item(info, format, quote_style, &final_buf); + fwrite(final_buf.buf, 1, final_buf.len, stdout); + strbuf_release(&final_buf); putchar('\n'); } diff --git a/ref-filter.h b/ref-filter.h index 7b05592baf..154e24c405 100644 --- a/ref-filter.h +++ b/ref-filter.h @@ -100,6 +100,9 @@ int parse_ref_filter_atom(const char *atom, const char *ep); int verify_ref_format(const char *format); /* Sort the given ref_array as per the ref_sorting provided */ void ref_array_sort(struct ref_sorting *sort, struct ref_array *array); +/* Based on the given format and quote_style, fill the strbuf */ +void format_ref_array_item(struct ref_array_item *info, const char *format, + int quote_style, struct strbuf *final_buf); /* Print the ref using the given format and quote_style */ void show_ref_array_item(struct ref_array_item *info, const char *format, int quote_style); /* Callback function for parsing the sort option */ @@ -108,6 +111,10 @@ int parse_opt_ref_sorting(const struct option *opt, const char *arg, int unset); struct ref_sorting *ref_default_sorting(void); /* Function to parse --merged and --no-merged options */ int parse_opt_merge_filter(const struct option *opt, const char *arg, int unset); +/* Get the current HEAD's description */ +char *get_head_description(void); +/* Set up translated strings in the output. */ +void setup_ref_filter_porcelain_msg(void); /* * Print a single ref, outside of any ref-filter. Note that the @@ -3,6 +3,7 @@ */ #include "cache.h" +#include "hashmap.h" #include "lockfile.h" #include "refs.h" #include "refs/refs-internal.h" @@ -591,8 +592,8 @@ static int delete_pseudoref(const char *pseudoref, const unsigned char *old_sha1 return 0; } -int delete_ref(const char *refname, const unsigned char *old_sha1, - unsigned int flags) +int delete_ref(const char *msg, const char *refname, + const unsigned char *old_sha1, unsigned int flags) { struct ref_transaction *transaction; struct strbuf err = STRBUF_INIT; @@ -603,7 +604,7 @@ int delete_ref(const char *refname, const unsigned char *old_sha1, transaction = ref_transaction_begin(&err); if (!transaction || ref_transaction_delete(transaction, refname, old_sha1, - flags, NULL, &err) || + flags, msg, &err) || ref_transaction_commit(transaction, &err)) { error("%s", err.buf); ref_transaction_free(transaction); @@ -1034,10 +1035,10 @@ static struct string_list *hide_refs; int parse_hide_refs_config(const char *var, const char *value, const char *section) { + const char *key; if (!strcmp("transfer.hiderefs", var) || - /* NEEDSWORK: use parse_config_key() once both are merged */ - (starts_with(var, section) && var[strlen(section)] == '.' && - !strcmp(var + strlen(section), ".hiderefs"))) { + (!parse_config_key(var, section, NULL, NULL, &key) && + !strcmp(key, "hiderefs"))) { char *ref; int len; @@ -1234,10 +1235,10 @@ int for_each_rawref(each_ref_fn fn, void *cb_data) } /* This function needs to return a meaningful errno on failure */ -static const char *resolve_ref_recursively(struct ref_store *refs, - const char *refname, - int resolve_flags, - unsigned char *sha1, int *flags) +const char *resolve_ref_recursively(struct ref_store *refs, + const char *refname, + int resolve_flags, + unsigned char *sha1, int *flags) { static struct strbuf sb_refname = STRBUF_INIT; int unused_flags; @@ -1357,62 +1358,102 @@ int resolve_gitlink_ref(const char *submodule, const char *refname, return 0; } +struct submodule_hash_entry +{ + struct hashmap_entry ent; /* must be the first member! */ + + struct ref_store *refs; + + /* NUL-terminated name of submodule: */ + char submodule[FLEX_ARRAY]; +}; + +static int submodule_hash_cmp(const void *entry, const void *entry_or_key, + const void *keydata) +{ + const struct submodule_hash_entry *e1 = entry, *e2 = entry_or_key; + const char *submodule = keydata ? keydata : e2->submodule; + + return strcmp(e1->submodule, submodule); +} + +static struct submodule_hash_entry *alloc_submodule_hash_entry( + const char *submodule, struct ref_store *refs) +{ + struct submodule_hash_entry *entry; + + FLEX_ALLOC_STR(entry, submodule, submodule); + hashmap_entry_init(entry, strhash(submodule)); + entry->refs = refs; + return entry; +} + /* A pointer to the ref_store for the main repository: */ static struct ref_store *main_ref_store; -/* A linked list of ref_stores for submodules: */ -static struct ref_store *submodule_ref_stores; +/* A hashmap of ref_stores, stored by submodule name: */ +static struct hashmap submodule_ref_stores; -void base_ref_store_init(struct ref_store *refs, - const struct ref_storage_be *be, - const char *submodule) +/* + * Return the ref_store instance for the specified submodule (or the + * main repository if submodule is NULL). If that ref_store hasn't + * been initialized yet, return NULL. + */ +static struct ref_store *lookup_ref_store(const char *submodule) +{ + struct submodule_hash_entry *entry; + + if (!submodule) + return main_ref_store; + + if (!submodule_ref_stores.tablesize) + /* It's initialized on demand in register_ref_store(). */ + return NULL; + + entry = hashmap_get_from_hash(&submodule_ref_stores, + strhash(submodule), submodule); + return entry ? entry->refs : NULL; +} + +/* + * Register the specified ref_store to be the one that should be used + * for submodule (or the main repository if submodule is NULL). It is + * a fatal error to call this function twice for the same submodule. + */ +static void register_ref_store(struct ref_store *refs, const char *submodule) { - refs->be = be; if (!submodule) { if (main_ref_store) die("BUG: main_ref_store initialized twice"); - refs->submodule = ""; - refs->next = NULL; main_ref_store = refs; } else { - if (lookup_ref_store(submodule)) + if (!submodule_ref_stores.tablesize) + hashmap_init(&submodule_ref_stores, submodule_hash_cmp, 0); + + if (hashmap_put(&submodule_ref_stores, + alloc_submodule_hash_entry(submodule, refs))) die("BUG: ref_store for submodule '%s' initialized twice", submodule); - - refs->submodule = xstrdup(submodule); - refs->next = submodule_ref_stores; - submodule_ref_stores = refs; } } -struct ref_store *ref_store_init(const char *submodule) +/* + * Create, record, and return a ref_store instance for the specified + * submodule (or the main repository if submodule is NULL). + */ +static struct ref_store *ref_store_init(const char *submodule) { const char *be_name = "files"; struct ref_storage_be *be = find_ref_storage_backend(be_name); + struct ref_store *refs; if (!be) die("BUG: reference backend %s is unknown", be_name); - if (!submodule || !*submodule) - return be->init(NULL); - else - return be->init(submodule); -} - -struct ref_store *lookup_ref_store(const char *submodule) -{ - struct ref_store *refs; - - if (!submodule || !*submodule) - return main_ref_store; - - for (refs = submodule_ref_stores; refs; refs = refs->next) { - if (!strcmp(submodule, refs->submodule)) - return refs; - } - - return NULL; + refs = be->init(submodule); + register_ref_store(refs, submodule); + return refs; } struct ref_store *get_ref_store(const char *submodule) @@ -1440,10 +1481,10 @@ struct ref_store *get_ref_store(const char *submodule) return refs; } -void assert_main_repository(struct ref_store *refs, const char *caller) +void base_ref_store_init(struct ref_store *refs, + const struct ref_storage_be *be) { - if (*refs->submodule) - die("BUG: %s called for a submodule", caller); + refs->be = be; } /* backend functions */ @@ -276,8 +276,8 @@ int reflog_exists(const char *refname); * exists, regardless of its old value. It is an error for old_sha1 to * be NULL_SHA1. flags is passed through to ref_transaction_delete(). */ -int delete_ref(const char *refname, const unsigned char *old_sha1, - unsigned int flags); +int delete_ref(const char *msg, const char *refname, + const unsigned char *old_sha1, unsigned int flags); /* * Delete the specified references. If there are any problems, emit @@ -334,7 +334,8 @@ int create_symref(const char *refname, const char *target, const char *logmsg); * $GIT_DIR points to. * Return 0 if successful, non-zero otherwise. * */ -int set_worktree_head_symref(const char *gitdir, const char *target); +int set_worktree_head_symref(const char *gitdir, const char *target, + const char *logmsg); enum action_on_err { UPDATE_REFS_MSG_ON_ERR, diff --git a/refs/files-backend.c b/refs/files-backend.c index c041d4ba21..b42df147c9 100644 --- a/refs/files-backend.c +++ b/refs/files-backend.c @@ -912,6 +912,14 @@ struct packed_ref_cache { */ struct files_ref_store { struct ref_store base; + + /* + * The name of the submodule represented by this object, or + * NULL if it represents the main repository's reference + * store: + */ + const char *submodule; + struct ref_entry *loose; struct packed_ref_cache *packed; }; @@ -972,12 +980,25 @@ static struct ref_store *files_ref_store_create(const char *submodule) struct files_ref_store *refs = xcalloc(1, sizeof(*refs)); struct ref_store *ref_store = (struct ref_store *)refs; - base_ref_store_init(ref_store, &refs_be_files, submodule); + base_ref_store_init(ref_store, &refs_be_files); + + refs->submodule = xstrdup_or_null(submodule); return ref_store; } /* + * Die if refs is for a submodule (i.e., not for the main repository). + * caller is used in any necessary error messages. + */ +static void files_assert_main_repository(struct files_ref_store *refs, + const char *caller) +{ + if (refs->submodule) + die("BUG: %s called for a submodule", caller); +} + +/* * Downcast ref_store to files_ref_store. Die if ref_store is not a * files_ref_store. If submodule_allowed is not true, then also die if * files_ref_store is for a submodule (i.e., not for the main @@ -987,14 +1008,18 @@ static struct files_ref_store *files_downcast( struct ref_store *ref_store, int submodule_allowed, const char *caller) { + struct files_ref_store *refs; + if (ref_store->be != &refs_be_files) die("BUG: ref_store is type \"%s\" not \"files\" in %s", ref_store->be->name, caller); + refs = (struct files_ref_store *)ref_store; + if (!submodule_allowed) - assert_main_repository(ref_store, caller); + files_assert_main_repository(refs, caller); - return (struct files_ref_store *)ref_store; + return refs; } /* The length of a peeled reference line in packed-refs, including EOL: */ @@ -1133,8 +1158,8 @@ static struct packed_ref_cache *get_packed_ref_cache(struct files_ref_store *ref { char *packed_refs_file; - if (*refs->base.submodule) - packed_refs_file = git_pathdup_submodule(refs->base.submodule, + if (refs->submodule) + packed_refs_file = git_pathdup_submodule(refs->submodule, "packed-refs"); else packed_refs_file = git_pathdup("packed-refs"); @@ -1203,8 +1228,8 @@ static void read_loose_refs(const char *dirname, struct ref_dir *dir) size_t path_baselen; int err = 0; - if (*refs->base.submodule) - err = strbuf_git_path_submodule(&path, refs->base.submodule, "%s", dirname); + if (refs->submodule) + err = strbuf_git_path_submodule(&path, refs->submodule, "%s", dirname); else strbuf_git_path(&path, "%s", dirname); path_baselen = path.len; @@ -1242,20 +1267,10 @@ static void read_loose_refs(const char *dirname, struct ref_dir *dir) create_dir_entry(refs, refname.buf, refname.len, 1)); } else { - int read_ok; - - if (*refs->base.submodule) { - hashclr(sha1); - flag = 0; - read_ok = !resolve_gitlink_ref(refs->base.submodule, - refname.buf, sha1); - } else { - read_ok = !read_ref_full(refname.buf, - RESOLVE_REF_READING, - sha1, &flag); - } - - if (!read_ok) { + if (!resolve_ref_recursively(&refs->base, + refname.buf, + RESOLVE_REF_READING, + sha1, &flag)) { hashclr(sha1); flag |= REF_ISBROKEN; } else if (is_null_sha1(sha1)) { @@ -1358,8 +1373,8 @@ static int files_read_raw_ref(struct ref_store *ref_store, *type = 0; strbuf_reset(&sb_path); - if (*refs->base.submodule) - strbuf_git_path_submodule(&sb_path, refs->base.submodule, "%s", refname); + if (refs->submodule) + strbuf_git_path_submodule(&sb_path, refs->submodule, "%s", refname); else strbuf_git_path(&sb_path, "%s", refname); @@ -1540,7 +1555,7 @@ static int lock_raw_ref(struct files_ref_store *refs, int ret = TRANSACTION_GENERIC_ERROR; assert(err); - assert_main_repository(&refs->base, "lock_raw_ref"); + files_assert_main_repository(refs, "lock_raw_ref"); *type = 0; @@ -1985,6 +2000,13 @@ static int remove_empty_directories(struct strbuf *path) return remove_dir_recursively(path, REMOVE_DIR_EMPTY_ONLY); } +static int create_reflock(const char *path, void *cb) +{ + struct lock_file *lk = cb; + + return hold_lock_file_for_update(lk, path, LOCK_NO_DEREF) < 0 ? -1 : 0; +} + /* * Locks a ref returning the lock on success and NULL on failure. * On failure errno is set to something meaningful. @@ -2000,13 +2022,11 @@ static struct ref_lock *lock_ref_sha1_basic(struct files_ref_store *refs, struct strbuf ref_file = STRBUF_INIT; struct ref_lock *lock; int last_errno = 0; - int lflags = LOCK_NO_DEREF; int mustexist = (old_sha1 && !is_null_sha1(old_sha1)); int resolve_flags = RESOLVE_REF_NO_RECURSE; - int attempts_remaining = 3; int resolved; - assert_main_repository(&refs->base, "lock_ref_sha1_basic"); + files_assert_main_repository(refs, "lock_ref_sha1_basic"); assert(err); lock = xcalloc(1, sizeof(struct ref_lock)); @@ -2068,35 +2088,12 @@ static struct ref_lock *lock_ref_sha1_basic(struct files_ref_store *refs, lock->ref_name = xstrdup(refname); - retry: - switch (safe_create_leading_directories_const(ref_file.buf)) { - case SCLD_OK: - break; /* success */ - case SCLD_VANISHED: - if (--attempts_remaining > 0) - goto retry; - /* fall through */ - default: + if (raceproof_create_file(ref_file.buf, create_reflock, lock->lk)) { last_errno = errno; - strbuf_addf(err, "unable to create directory for '%s'", - ref_file.buf); + unable_to_lock_message(ref_file.buf, errno, err); goto error_return; } - if (hold_lock_file_for_update(lock->lk, ref_file.buf, lflags) < 0) { - last_errno = errno; - if (errno == ENOENT && --attempts_remaining > 0) - /* - * Maybe somebody just deleted one of the - * directories leading to ref_file. Try - * again: - */ - goto retry; - else { - unable_to_lock_message(ref_file.buf, errno, err); - goto error_return; - } - } if (verify_lock(lock, old_sha1, mustexist, err)) { last_errno = errno; goto error_return; @@ -2152,7 +2149,7 @@ static int lock_packed_refs(struct files_ref_store *refs, int flags) static int timeout_value = 1000; struct packed_ref_cache *packed_ref_cache; - assert_main_repository(&refs->base, "lock_packed_refs"); + files_assert_main_repository(refs, "lock_packed_refs"); if (!timeout_configured) { git_config_get_int("core.packedrefstimeout", &timeout_value); @@ -2190,7 +2187,7 @@ static int commit_packed_refs(struct files_ref_store *refs) int save_errno = 0; FILE *out; - assert_main_repository(&refs->base, "commit_packed_refs"); + files_assert_main_repository(refs, "commit_packed_refs"); if (!packed_ref_cache->lock) die("internal error: packed-refs not locked"); @@ -2223,7 +2220,7 @@ static void rollback_packed_refs(struct files_ref_store *refs) struct packed_ref_cache *packed_ref_cache = get_packed_ref_cache(refs); - assert_main_repository(&refs->base, "rollback_packed_refs"); + files_assert_main_repository(refs, "rollback_packed_refs"); if (!packed_ref_cache->lock) die("internal error: packed-refs not locked"); @@ -2298,15 +2295,25 @@ static int pack_if_possible_fn(struct ref_entry *entry, void *cb_data) return 0; } +enum { + REMOVE_EMPTY_PARENTS_REF = 0x01, + REMOVE_EMPTY_PARENTS_REFLOG = 0x02 +}; + /* - * Remove empty parents, but spare refs/ and immediate subdirs. - * Note: munges *name. + * Remove empty parent directories associated with the specified + * reference and/or its reflog, but spare [logs/]refs/ and immediate + * subdirs. flags is a combination of REMOVE_EMPTY_PARENTS_REF and/or + * REMOVE_EMPTY_PARENTS_REFLOG. */ -static void try_remove_empty_parents(char *name) +static void try_remove_empty_parents(const char *refname, unsigned int flags) { + struct strbuf buf = STRBUF_INIT; char *p, *q; int i; - p = name; + + strbuf_addstr(&buf, refname); + p = buf.buf; for (i = 0; i < 2; i++) { /* refs/{heads,tags,...}/ */ while (*p && *p != '/') p++; @@ -2314,19 +2321,23 @@ static void try_remove_empty_parents(char *name) while (*p == '/') p++; } - for (q = p; *q; q++) - ; - while (1) { + q = buf.buf + buf.len; + while (flags & (REMOVE_EMPTY_PARENTS_REF | REMOVE_EMPTY_PARENTS_REFLOG)) { while (q > p && *q != '/') q--; while (q > p && *(q-1) == '/') q--; if (q == p) break; - *q = '\0'; - if (rmdir(git_path("%s", name))) - break; + strbuf_setlen(&buf, q - buf.buf); + if ((flags & REMOVE_EMPTY_PARENTS_REF) && + rmdir(git_path("%s", buf.buf))) + flags &= ~REMOVE_EMPTY_PARENTS_REF; + if ((flags & REMOVE_EMPTY_PARENTS_REFLOG) && + rmdir(git_path("logs/%s", buf.buf))) + flags &= ~REMOVE_EMPTY_PARENTS_REFLOG; } + strbuf_release(&buf); } /* make sure nobody touched the ref, and unlink */ @@ -2350,7 +2361,6 @@ static void prune_ref(struct ref_to_prune *r) } ref_transaction_free(transaction); strbuf_release(&err); - try_remove_empty_parents(r->name); } static void prune_refs(struct ref_to_prune *r) @@ -2397,7 +2407,7 @@ static int repack_without_refs(struct files_ref_store *refs, struct string_list_item *refname; int ret, needs_repacking = 0, removed = 0; - assert_main_repository(&refs->base, "repack_without_refs"); + files_assert_main_repository(refs, "repack_without_refs"); assert(err); /* Look for a packed ref */ @@ -2439,24 +2449,6 @@ static int repack_without_refs(struct files_ref_store *refs, return ret; } -static int delete_ref_loose(struct ref_lock *lock, int flag, struct strbuf *err) -{ - assert(err); - - if (!(flag & REF_ISPACKED) || flag & REF_ISSYMREF) { - /* - * loose. The loose file name is the same as the - * lockfile name, minus ".lock": - */ - char *loose_filename = get_locked_file_path(lock->lk); - int res = unlink_or_msg(loose_filename, err); - free(loose_filename); - if (res) - return 1; - } - return 0; -} - static int files_delete_refs(struct ref_store *ref_store, struct string_list *refnames, unsigned int flags) { @@ -2489,7 +2481,7 @@ static int files_delete_refs(struct ref_store *ref_store, for (i = 0; i < refnames->nr; i++) { const char *refname = refnames->items[i].string; - if (delete_ref(refname, NULL, flags)) + if (delete_ref(NULL, refname, NULL, flags)) result |= error(_("could not remove reference %s"), refname); } @@ -2507,55 +2499,43 @@ out: */ #define TMP_RENAMED_LOG "logs/refs/.tmp-renamed-log" -static int rename_tmp_log(const char *newrefname) +static int rename_tmp_log_callback(const char *path, void *cb) { - int attempts_remaining = 4; - struct strbuf path = STRBUF_INIT; - int ret = -1; + int *true_errno = cb; - retry: - strbuf_reset(&path); - strbuf_git_path(&path, "logs/%s", newrefname); - switch (safe_create_leading_directories_const(path.buf)) { - case SCLD_OK: - break; /* success */ - case SCLD_VANISHED: - if (--attempts_remaining > 0) - goto retry; - /* fall through */ - default: - error("unable to create directory for %s", newrefname); - goto out; + if (rename(git_path(TMP_RENAMED_LOG), path)) { + /* + * rename(a, b) when b is an existing directory ought + * to result in ISDIR, but Solaris 5.8 gives ENOTDIR. + * Sheesh. Record the true errno for error reporting, + * but report EISDIR to raceproof_create_file() so + * that it knows to retry. + */ + *true_errno = errno; + if (errno == ENOTDIR) + errno = EISDIR; + return -1; + } else { + return 0; } +} - if (rename(git_path(TMP_RENAMED_LOG), path.buf)) { - if ((errno==EISDIR || errno==ENOTDIR) && --attempts_remaining > 0) { - /* - * rename(a, b) when b is an existing - * directory ought to result in ISDIR, but - * Solaris 5.8 gives ENOTDIR. Sheesh. - */ - if (remove_empty_directories(&path)) { - error("Directory not empty: logs/%s", newrefname); - goto out; - } - goto retry; - } else if (errno == ENOENT && --attempts_remaining > 0) { - /* - * Maybe another process just deleted one of - * the directories in the path to newrefname. - * Try again from the beginning. - */ - goto retry; - } else { - error("unable to move logfile "TMP_RENAMED_LOG" to logs/%s: %s", - newrefname, strerror(errno)); - goto out; - } +static int rename_tmp_log(const char *newrefname) +{ + char *path = git_pathdup("logs/%s", newrefname); + int ret, true_errno; + + ret = raceproof_create_file(path, rename_tmp_log_callback, &true_errno); + if (ret) { + if (errno == EISDIR) + error("directory not empty: %s", path); + else + error("unable to move logfile %s to %s: %s", + git_path(TMP_RENAMED_LOG), path, + strerror(true_errno)); } - ret = 0; -out: - strbuf_release(&path); + + free(path); return ret; } @@ -2616,7 +2596,7 @@ static int files_rename_ref(struct ref_store *ref_store, return error("unable to move logfile logs/%s to "TMP_RENAMED_LOG": %s", oldrefname, strerror(errno)); - if (delete_ref(oldrefname, orig_sha1, REF_NODEREF)) { + if (delete_ref(logmsg, oldrefname, orig_sha1, REF_NODEREF)) { error("unable to delete old %s", oldrefname); goto rollback; } @@ -2630,8 +2610,8 @@ static int files_rename_ref(struct ref_store *ref_store, */ if (!read_ref_full(newrefname, RESOLVE_REF_READING | RESOLVE_REF_NO_RECURSE, sha1, NULL) && - delete_ref(newrefname, NULL, REF_NODEREF)) { - if (errno==EISDIR) { + delete_ref(NULL, newrefname, NULL, REF_NODEREF)) { + if (errno == EISDIR) { struct strbuf path = STRBUF_INIT; int result; @@ -2740,66 +2720,89 @@ static int commit_ref(struct ref_lock *lock) return 0; } +static int open_or_create_logfile(const char *path, void *cb) +{ + int *fd = cb; + + *fd = open(path, O_APPEND | O_WRONLY | O_CREAT, 0666); + return (*fd < 0) ? -1 : 0; +} + /* - * Create a reflog for a ref. If force_create = 0, the reflog will - * only be created for certain refs (those for which - * should_autocreate_reflog returns non-zero. Otherwise, create it - * regardless of the ref name. Fill in *err and return -1 on failure. + * Create a reflog for a ref. If force_create = 0, only create the + * reflog for certain refs (those for which should_autocreate_reflog + * returns non-zero). Otherwise, create it regardless of the reference + * name. If the logfile already existed or was created, return 0 and + * set *logfd to the file descriptor opened for appending to the file. + * If no logfile exists and we decided not to create one, return 0 and + * set *logfd to -1. On failure, fill in *err, set *logfd to -1, and + * return -1. */ -static int log_ref_setup(const char *refname, struct strbuf *logfile, struct strbuf *err, int force_create) +static int log_ref_setup(const char *refname, int force_create, + int *logfd, struct strbuf *err) { - int logfd, oflags = O_APPEND | O_WRONLY; + char *logfile = git_pathdup("logs/%s", refname); - strbuf_git_path(logfile, "logs/%s", refname); if (force_create || should_autocreate_reflog(refname)) { - if (safe_create_leading_directories(logfile->buf) < 0) { - strbuf_addf(err, "unable to create directory for '%s': " - "%s", logfile->buf, strerror(errno)); - return -1; - } - oflags |= O_CREAT; - } - - logfd = open(logfile->buf, oflags, 0666); - if (logfd < 0) { - if (!(oflags & O_CREAT) && (errno == ENOENT || errno == EISDIR)) - return 0; + if (raceproof_create_file(logfile, open_or_create_logfile, logfd)) { + if (errno == ENOENT) + strbuf_addf(err, "unable to create directory for '%s': " + "%s", logfile, strerror(errno)); + else if (errno == EISDIR) + strbuf_addf(err, "there are still logs under '%s'", + logfile); + else + strbuf_addf(err, "unable to append to '%s': %s", + logfile, strerror(errno)); - if (errno == EISDIR) { - if (remove_empty_directories(logfile)) { - strbuf_addf(err, "there are still logs under " - "'%s'", logfile->buf); - return -1; - } - logfd = open(logfile->buf, oflags, 0666); + goto error; } - - if (logfd < 0) { - strbuf_addf(err, "unable to append to '%s': %s", - logfile->buf, strerror(errno)); - return -1; + } else { + *logfd = open(logfile, O_APPEND | O_WRONLY, 0666); + if (*logfd < 0) { + if (errno == ENOENT || errno == EISDIR) { + /* + * The logfile doesn't already exist, + * but that is not an error; it only + * means that we won't write log + * entries to it. + */ + ; + } else { + strbuf_addf(err, "unable to append to '%s': %s", + logfile, strerror(errno)); + goto error; + } } } - adjust_shared_perm(logfile->buf); - close(logfd); + if (*logfd >= 0) + adjust_shared_perm(logfile); + + free(logfile); return 0; -} +error: + free(logfile); + return -1; +} static int files_create_reflog(struct ref_store *ref_store, const char *refname, int force_create, struct strbuf *err) { - int ret; - struct strbuf sb = STRBUF_INIT; + int fd; /* Check validity (but we don't need the result): */ files_downcast(ref_store, 0, "create_reflog"); - ret = log_ref_setup(refname, &sb, err, force_create); - strbuf_release(&sb); - return ret; + if (log_ref_setup(refname, force_create, &fd, err)) + return -1; + + if (fd >= 0) + close(fd); + + return 0; } static int log_ref_write_fd(int fd, const unsigned char *old_sha1, @@ -2828,59 +2831,43 @@ static int log_ref_write_fd(int fd, const unsigned char *old_sha1, return 0; } -static int log_ref_write_1(const char *refname, const unsigned char *old_sha1, - const unsigned char *new_sha1, const char *msg, - struct strbuf *logfile, int flags, - struct strbuf *err) +int files_log_ref_write(const char *refname, const unsigned char *old_sha1, + const unsigned char *new_sha1, const char *msg, + int flags, struct strbuf *err) { - int logfd, result, oflags = O_APPEND | O_WRONLY; + int logfd, result; if (log_all_ref_updates == LOG_REFS_UNSET) log_all_ref_updates = is_bare_repository() ? LOG_REFS_NONE : LOG_REFS_NORMAL; - result = log_ref_setup(refname, logfile, err, flags & REF_FORCE_CREATE_REFLOG); + result = log_ref_setup(refname, flags & REF_FORCE_CREATE_REFLOG, + &logfd, err); if (result) return result; - logfd = open(logfile->buf, oflags); if (logfd < 0) return 0; result = log_ref_write_fd(logfd, old_sha1, new_sha1, git_committer_info(0), msg); if (result) { - strbuf_addf(err, "unable to append to '%s': %s", logfile->buf, - strerror(errno)); + int save_errno = errno; + + strbuf_addf(err, "unable to append to '%s': %s", + git_path("logs/%s", refname), strerror(save_errno)); close(logfd); return -1; } if (close(logfd)) { - strbuf_addf(err, "unable to append to '%s': %s", logfile->buf, - strerror(errno)); + int save_errno = errno; + + strbuf_addf(err, "unable to append to '%s': %s", + git_path("logs/%s", refname), strerror(save_errno)); return -1; } return 0; } -static int log_ref_write(const char *refname, const unsigned char *old_sha1, - const unsigned char *new_sha1, const char *msg, - int flags, struct strbuf *err) -{ - return files_log_ref_write(refname, old_sha1, new_sha1, msg, flags, - err); -} - -int files_log_ref_write(const char *refname, const unsigned char *old_sha1, - const unsigned char *new_sha1, const char *msg, - int flags, struct strbuf *err) -{ - struct strbuf sb = STRBUF_INIT; - int ret = log_ref_write_1(refname, old_sha1, new_sha1, msg, &sb, flags, - err); - strbuf_release(&sb); - return ret; -} - /* * Write sha1 into the open lockfile, then close the lockfile. On * errors, rollback the lockfile, fill in *err and @@ -2930,10 +2917,11 @@ static int commit_ref_update(struct files_ref_store *refs, const unsigned char *sha1, const char *logmsg, struct strbuf *err) { - assert_main_repository(&refs->base, "commit_ref_update"); + files_assert_main_repository(refs, "commit_ref_update"); clear_loose_ref_cache(refs); - if (log_ref_write(lock->ref_name, lock->old_oid.hash, sha1, logmsg, 0, err)) { + if (files_log_ref_write(lock->ref_name, lock->old_oid.hash, sha1, + logmsg, 0, err)) { char *old_msg = strbuf_detach(err, NULL); strbuf_addf(err, "cannot update the ref '%s': %s", lock->ref_name, old_msg); @@ -2964,7 +2952,7 @@ static int commit_ref_update(struct files_ref_store *refs, if (head_ref && (head_flag & REF_ISSYMREF) && !strcmp(head_ref, lock->ref_name)) { struct strbuf log_err = STRBUF_INIT; - if (log_ref_write("HEAD", lock->old_oid.hash, sha1, + if (files_log_ref_write("HEAD", lock->old_oid.hash, sha1, logmsg, 0, &log_err)) { error("%s", log_err.buf); strbuf_release(&log_err); @@ -3003,7 +2991,8 @@ static void update_symref_reflog(struct ref_lock *lock, const char *refname, struct strbuf err = STRBUF_INIT; unsigned char new_sha1[20]; if (logmsg && !read_ref(target, new_sha1) && - log_ref_write(refname, lock->old_oid.hash, new_sha1, logmsg, 0, &err)) { + files_log_ref_write(refname, lock->old_oid.hash, new_sha1, + logmsg, 0, &err)) { error("%s", err.buf); strbuf_release(&err); } @@ -3055,7 +3044,7 @@ static int files_create_symref(struct ref_store *ref_store, return ret; } -int set_worktree_head_symref(const char *gitdir, const char *target) +int set_worktree_head_symref(const char *gitdir, const char *target, const char *logmsg) { static struct lock_file head_lock; struct ref_lock *lock; @@ -3083,7 +3072,7 @@ int set_worktree_head_symref(const char *gitdir, const char *target) lock->lk = &head_lock; lock->ref_name = xstrdup(head_rel); - ret = create_symref_locked(lock, head_rel, target, NULL); + ret = create_symref_locked(lock, head_rel, target, logmsg); unlock_ref(lock); /* will free lock */ strbuf_release(&head_path); @@ -3560,7 +3549,7 @@ static int lock_ref_for_update(struct files_ref_store *refs, int ret; struct ref_lock *lock; - assert_main_repository(&refs->base, "lock_ref_for_update"); + files_assert_main_repository(refs, "lock_ref_for_update"); if ((update->flags & REF_HAVE_NEW) && is_null_sha1(update->new_sha1)) update->flags |= REF_DELETING; @@ -3778,9 +3767,11 @@ static int files_transaction_commit(struct ref_store *ref_store, if (update->flags & REF_NEEDS_COMMIT || update->flags & REF_LOG_ONLY) { - if (log_ref_write(lock->ref_name, lock->old_oid.hash, - update->new_sha1, - update->msg, update->flags, err)) { + if (files_log_ref_write(lock->ref_name, + lock->old_oid.hash, + update->new_sha1, + update->msg, update->flags, + err)) { char *old_msg = strbuf_detach(err, NULL); strbuf_addf(err, "cannot update the ref '%s': %s", @@ -3810,9 +3801,14 @@ static int files_transaction_commit(struct ref_store *ref_store, if (update->flags & REF_DELETING && !(update->flags & REF_LOG_ONLY)) { - if (delete_ref_loose(lock, update->type, err)) { - ret = TRANSACTION_GENERIC_ERROR; - goto cleanup; + if (!(update->type & REF_ISPACKED) || + update->type & REF_ISSYMREF) { + /* It is a loose reference. */ + if (unlink_or_msg(git_path("%s", lock->ref_name), err)) { + ret = TRANSACTION_GENERIC_ERROR; + goto cleanup; + } + update->flags |= REF_DELETED_LOOSE; } if (!(update->flags & REF_ISPRUNING)) @@ -3825,16 +3821,38 @@ static int files_transaction_commit(struct ref_store *ref_store, ret = TRANSACTION_GENERIC_ERROR; goto cleanup; } - for_each_string_list_item(ref_to_delete, &refs_to_delete) - unlink_or_warn(git_path("logs/%s", ref_to_delete->string)); + + /* Delete the reflogs of any references that were deleted: */ + for_each_string_list_item(ref_to_delete, &refs_to_delete) { + if (!unlink_or_warn(git_path("logs/%s", ref_to_delete->string))) + try_remove_empty_parents(ref_to_delete->string, + REMOVE_EMPTY_PARENTS_REFLOG); + } + clear_loose_ref_cache(refs); cleanup: transaction->state = REF_TRANSACTION_CLOSED; - for (i = 0; i < transaction->nr; i++) - if (transaction->updates[i]->backend_data) - unlock_ref(transaction->updates[i]->backend_data); + for (i = 0; i < transaction->nr; i++) { + struct ref_update *update = transaction->updates[i]; + struct ref_lock *lock = update->backend_data; + + if (lock) + unlock_ref(lock); + + if (update->flags & REF_DELETED_LOOSE) { + /* + * The loose reference was deleted. Delete any + * empty parent directories. (Note that this + * can only work because we have already + * removed the lockfile.) + */ + try_remove_empty_parents(update->refname, + REMOVE_EMPTY_PARENTS_REF); + } + } + string_list_clear(&refs_to_delete, 0); free(head_ref); string_list_clear(&affected_refnames, 0); diff --git a/refs/refs-internal.h b/refs/refs-internal.h index 25444cf5b0..fa93c9a32e 100644 --- a/refs/refs-internal.h +++ b/refs/refs-internal.h @@ -56,17 +56,24 @@ #define REF_UPDATE_VIA_HEAD 0x100 /* + * Used as a flag in ref_update::flags when the loose reference has + * been deleted. + */ +#define REF_DELETED_LOOSE 0x200 + +/* * Return true iff refname is minimally safe. "Safe" here means that * deleting a loose reference by this name will not do any damage, for * example by causing a file that is not a reference to be deleted. * This function does not check that the reference name is legal; for * that, use check_refname_format(). * - * We consider a refname that starts with "refs/" to be safe as long - * as any ".." components that it might contain do not escape "refs/". - * Names that do not start with "refs/" are considered safe iff they - * consist entirely of upper case characters and '_' (like "HEAD" and - * "MERGE_HEAD" but not "config" or "FOO/BAR"). + * A refname that starts with "refs/" is considered safe iff it + * doesn't contain any "." or ".." components or consecutive '/' + * characters, end with '/', or (on Windows) contain any '\' + * characters. Names that do not start with "refs/" are considered + * safe iff they consist entirely of upper case characters and '_' + * (like "HEAD" and "MERGE_HEAD" but not "config" or "FOO/BAR"). */ int refname_is_safe(const char *refname); @@ -155,8 +162,9 @@ struct ref_update { /* * One or more of REF_HAVE_NEW, REF_HAVE_OLD, REF_NODEREF, - * REF_DELETING, REF_ISPRUNING, REF_LOG_ONLY, and - * REF_UPDATE_VIA_HEAD: + * REF_DELETING, REF_ISPRUNING, REF_LOG_ONLY, + * REF_UPDATE_VIA_HEAD, REF_NEEDS_COMMIT, and + * REF_DELETED_LOOSE: */ unsigned int flags; @@ -627,47 +635,14 @@ extern struct ref_storage_be refs_be_files; struct ref_store { /* The backend describing this ref_store's storage scheme: */ const struct ref_storage_be *be; - - /* - * The name of the submodule represented by this object, or - * the empty string if it represents the main repository's - * reference store: - */ - const char *submodule; - - /* - * Submodule reference store instances are stored in a linked - * list using this pointer. - */ - struct ref_store *next; }; /* - * Fill in the generic part of refs for the specified submodule and - * add it to our collection of reference stores. + * Fill in the generic part of refs and add it to our collection of + * reference stores. */ void base_ref_store_init(struct ref_store *refs, - const struct ref_storage_be *be, - const char *submodule); - -/* - * Create, record, and return a ref_store instance for the specified - * submodule (or the main repository if submodule is NULL). - * - * For backwards compatibility, submodule=="" is treated the same as - * submodule==NULL. - */ -struct ref_store *ref_store_init(const char *submodule); - -/* - * Return the ref_store instance for the specified submodule (or the - * main repository if submodule is NULL). If that ref_store hasn't - * been initialized yet, return NULL. - * - * For backwards compatibility, submodule=="" is treated the same as - * submodule==NULL. - */ -struct ref_store *lookup_ref_store(const char *submodule); + const struct ref_storage_be *be); /* * Return the ref_store instance for the specified submodule. For the @@ -681,10 +656,9 @@ struct ref_store *lookup_ref_store(const char *submodule); */ struct ref_store *get_ref_store(const char *submodule); -/* - * Die if refs is for a submodule (i.e., not for the main repository). - * caller is used in any necessary error messages. - */ -void assert_main_repository(struct ref_store *refs, const char *caller); +const char *resolve_ref_recursively(struct ref_store *refs, + const char *refname, + int resolve_flags, + unsigned char *sha1, int *flags); #endif /* REFS_REFS_INTERNAL_H */ @@ -693,7 +693,7 @@ static struct remote *remote_get_1(const char *name, name = get_default(current_branch, &name_given); ret = make_remote(name, 0); - if (valid_remote_nick(name)) { + if (valid_remote_nick(name) && have_git_dir()) { if (!valid_remote(ret)) read_remotes_file(ret); if (!valid_remote(ret)) @@ -254,7 +254,7 @@ int get_common_dir_noenv(struct strbuf *sb, const char *gitdir) if (!is_absolute_path(data.buf)) strbuf_addf(&path, "%s/", gitdir); strbuf_addbuf(&path, &data); - strbuf_addstr(sb, real_path(path.buf)); + strbuf_add_real_path(sb, path.buf); ret = 1; } else { strbuf_addstr(sb, gitdir); diff --git a/sha1_file.c b/sha1_file.c index ec957db5e1..2ee3c617a6 100644 --- a/sha1_file.c +++ b/sha1_file.c @@ -129,8 +129,10 @@ enum scld_error safe_create_leading_directories(char *path) *slash = '\0'; if (!stat(path, &st)) { /* path exists */ - if (!S_ISDIR(st.st_mode)) + if (!S_ISDIR(st.st_mode)) { + errno = ENOTDIR; ret = SCLD_EXISTS; + } } else if (mkdir(path, 0777)) { if (errno == EEXIST && !stat(path, &st) && S_ISDIR(st.st_mode)) @@ -158,13 +160,85 @@ enum scld_error safe_create_leading_directories(char *path) enum scld_error safe_create_leading_directories_const(const char *path) { + int save_errno; /* path points to cache entries, so xstrdup before messing with it */ char *buf = xstrdup(path); enum scld_error result = safe_create_leading_directories(buf); + + save_errno = errno; free(buf); + errno = save_errno; return result; } +int raceproof_create_file(const char *path, create_file_fn fn, void *cb) +{ + /* + * The number of times we will try to remove empty directories + * in the way of path. This is only 1 because if another + * process is racily creating directories that conflict with + * us, we don't want to fight against them. + */ + int remove_directories_remaining = 1; + + /* + * The number of times that we will try to create the + * directories containing path. We are willing to attempt this + * more than once, because another process could be trying to + * clean up empty directories at the same time as we are + * trying to create them. + */ + int create_directories_remaining = 3; + + /* A scratch copy of path, filled lazily if we need it: */ + struct strbuf path_copy = STRBUF_INIT; + + int ret, save_errno; + + /* Sanity check: */ + assert(*path); + +retry_fn: + ret = fn(path, cb); + save_errno = errno; + if (!ret) + goto out; + + if (errno == EISDIR && remove_directories_remaining-- > 0) { + /* + * A directory is in the way. Maybe it is empty; try + * to remove it: + */ + if (!path_copy.len) + strbuf_addstr(&path_copy, path); + + if (!remove_dir_recursively(&path_copy, REMOVE_DIR_EMPTY_ONLY)) + goto retry_fn; + } else if (errno == ENOENT && create_directories_remaining-- > 0) { + /* + * Maybe the containing directory didn't exist, or + * maybe it was just deleted by a process that is + * racing with us to clean up empty directories. Try + * to create it: + */ + enum scld_error scld_result; + + if (!path_copy.len) + strbuf_addstr(&path_copy, path); + + do { + scld_result = safe_create_leading_directories(path_copy.buf); + if (scld_result == SCLD_OK) + goto retry_fn; + } while (scld_result == SCLD_VANISHED && create_directories_remaining-- > 0); + } + +out: + strbuf_release(&path_copy); + errno = save_errno; + return ret; +} + static void fill_sha1_path(struct strbuf *buf, const unsigned char *sha1) { int i; @@ -2532,6 +2606,7 @@ void *unpack_entry(struct packed_git *p, off_t obj_offset, while (delta_stack_nr) { void *delta_data; void *base = data; + void *external_base = NULL; unsigned long delta_size, base_size = size; int i; @@ -2558,6 +2633,7 @@ void *unpack_entry(struct packed_git *p, off_t obj_offset, p->pack_name); mark_bad_packed_object(p, base_sha1); base = read_object(base_sha1, &type, &base_size); + external_base = base; } } @@ -2576,6 +2652,7 @@ void *unpack_entry(struct packed_git *p, off_t obj_offset, "at offset %"PRIuMAX" from %s", (uintmax_t)curpos, p->pack_name); data = NULL; + free(external_base); continue; } @@ -2595,6 +2672,7 @@ void *unpack_entry(struct packed_git *p, off_t obj_offset, error("failed to apply delta"); free(delta_data); + free(external_base); } *final_type = type; @@ -707,6 +707,17 @@ void strbuf_add_absolute_path(struct strbuf *sb, const char *path) strbuf_addstr(sb, path); } +void strbuf_add_real_path(struct strbuf *sb, const char *path) +{ + if (sb->len) { + struct strbuf resolved = STRBUF_INIT; + strbuf_realpath(&resolved, path, 1); + strbuf_addbuf(sb, &resolved); + strbuf_release(&resolved); + } else + strbuf_realpath(sb, path, 1); +} + int printf_ln(const char *fmt, ...) { int ret; @@ -441,6 +441,20 @@ extern int strbuf_getcwd(struct strbuf *sb); */ extern void strbuf_add_absolute_path(struct strbuf *sb, const char *path); +/** + * Canonize `path` (make it absolute, resolve symlinks, remove extra + * slashes) and append it to `sb`. Die with an informative error + * message if there is a problem. + * + * 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. + * + * Callers that don't mind links should use the more lightweight + * strbuf_add_absolute_path() instead. + */ +extern void strbuf_add_real_path(struct strbuf *sb, const char *path); + /** * Normalize in-place the path contained in the strbuf. See diff --git a/t/lib-httpd/apache.conf b/t/lib-httpd/apache.conf index 69174c6e31..0642ae7e6e 100644 --- a/t/lib-httpd/apache.conf +++ b/t/lib-httpd/apache.conf @@ -133,6 +133,15 @@ RewriteRule ^/ftp-redir/(.*)$ ftp://localhost:1000/$1 [R=302] RewriteRule ^/loop-redir/x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-(.*) /$1 [R=302] RewriteRule ^/loop-redir/(.*)$ /loop-redir/x-$1 [R=302] +# redir-to/502/x?y -> really-redir-to?path=502/x&qs=y which returns 502 +# redir-to/x?y -> really-redir-to?path=x&qs=y -> x?y +RewriteCond %{QUERY_STRING} ^(.*)$ +RewriteRule ^/redir-to/(.*)$ /really-redir-to?path=$1&qs=%1 [R=302] +RewriteCond %{QUERY_STRING} ^path=502/(.*)&qs=(.*)$ +RewriteRule ^/really-redir-to$ - [R=502,L] +RewriteCond %{QUERY_STRING} ^path=(.*)&qs=(.*)$ +RewriteRule ^/really-redir-to$ /%1?%2 [R=302] + # The first rule issues a client-side redirect to something # that _doesn't_ look like a git repo. The second rule is a # server-side rewrite, so that it turns out the odd-looking diff --git a/t/t0003-attributes.sh b/t/t0003-attributes.sh index f0fbb42554..f19ae4f8cc 100755 --- a/t/t0003-attributes.sh +++ b/t/t0003-attributes.sh @@ -13,10 +13,31 @@ attr_check () { test_line_count = 0 err } +attr_check_quote () { + + path="$1" + quoted_path="$2" + expect="$3" + + git check-attr test -- "$path" >actual && + echo "\"$quoted_path\": test: $expect" >expect && + test_cmp expect actual + +} + +test_expect_success 'open-quoted pathname' ' + echo "\"a test=a" >.gitattributes && + test_must_fail attr_check a a +' + + test_expect_success 'setup' ' mkdir -p a/b/d a/c b && ( echo "[attr]notest !test" + echo "\" d \" test=d" + echo " e test=e" + echo " e\" test=e" echo "f test=f" echo "a/i test=a/i" echo "onoff test -test" @@ -69,6 +90,11 @@ test_expect_success 'command line checks' ' ' test_expect_success 'attribute test' ' + + attr_check " d " d && + attr_check e e && + attr_check_quote e\" e\\\" e && + attr_check f f && attr_check a/f f && attr_check a/c/f f && diff --git a/t/t1300-repo-config.sh b/t/t1300-repo-config.sh index 923bfc5a26..afcca0d52c 100755 --- a/t/t1300-repo-config.sh +++ b/t/t1300-repo-config.sh @@ -1097,6 +1097,68 @@ test_expect_success 'multiple git -c appends config' ' test_cmp expect actual ' +test_expect_success 'last one wins: two level vars' ' + + # sec.var and sec.VAR are the same variable, as the first + # and the last level of a configuration variable name is + # case insensitive. + + echo VAL >expect && + + git -c sec.var=val -c sec.VAR=VAL config --get sec.var >actual && + test_cmp expect actual && + git -c SEC.var=val -c sec.var=VAL config --get sec.var >actual && + test_cmp expect actual && + + git -c sec.var=val -c sec.VAR=VAL config --get SEC.var >actual && + test_cmp expect actual && + git -c SEC.var=val -c sec.var=VAL config --get sec.VAR >actual && + test_cmp expect actual +' + +test_expect_success 'last one wins: three level vars' ' + + # v.a.r and v.A.r are not the same variable, as the middle + # level of a three-level configuration variable name is + # case sensitive. + + echo val >expect && + git -c v.a.r=val -c v.A.r=VAL config --get v.a.r >actual && + test_cmp expect actual && + git -c v.a.r=val -c v.A.r=VAL config --get V.a.R >actual && + test_cmp expect actual && + + # v.a.r and V.a.R are the same variable, as the first + # and the last level of a configuration variable name is + # case insensitive. + + echo VAL >expect && + git -c v.a.r=val -c v.a.R=VAL config --get v.a.r >actual && + test_cmp expect actual && + git -c v.a.r=val -c V.a.r=VAL config --get v.a.r >actual && + test_cmp expect actual && + git -c v.a.r=val -c v.a.R=VAL config --get V.a.R >actual && + test_cmp expect actual && + git -c v.a.r=val -c V.a.r=VAL config --get V.a.R >actual && + test_cmp expect actual +' + +for VAR in a .a a. a.0b a."b c". a."b c".0d +do + test_expect_success "git -c $VAR=VAL rejects invalid '$VAR'" ' + test_must_fail git -c "$VAR=VAL" config -l + ' +done + +for VAR in a.b a."b c".d +do + test_expect_success "git -c $VAR=VAL works with valid '$VAR'" ' + echo VAL >expect && + git -c "$VAR=VAL" config --get "$VAR" >actual && + test_cmp expect actual + ' +done + test_expect_success 'git -c is not confused by empty environment' ' GIT_CONFIG_PARAMETERS="" git -c x.one=1 config --list ' @@ -1177,6 +1239,111 @@ test_expect_success 'urlmatch' ' test_cmp expect actual ' +test_expect_success 'urlmatch favors more specific URLs' ' + cat >.git/config <<-\EOF && + [http "https://example.com/"] + cookieFile = /tmp/root.txt + [http "https://example.com/subdirectory"] + cookieFile = /tmp/subdirectory.txt + [http "https://user@example.com/"] + cookieFile = /tmp/user.txt + [http "https://averylonguser@example.com/"] + cookieFile = /tmp/averylonguser.txt + [http "https://preceding.example.com"] + cookieFile = /tmp/preceding.txt + [http "https://*.example.com"] + cookieFile = /tmp/wildcard.txt + [http "https://*.example.com/wildcardwithsubdomain"] + cookieFile = /tmp/wildcardwithsubdomain.txt + [http "https://trailing.example.com"] + cookieFile = /tmp/trailing.txt + [http "https://user@*.example.com/"] + cookieFile = /tmp/wildcardwithuser.txt + [http "https://sub.example.com/"] + cookieFile = /tmp/sub.txt + EOF + + echo http.cookiefile /tmp/root.txt >expect && + git config --get-urlmatch HTTP https://example.com >actual && + test_cmp expect actual && + + echo http.cookiefile /tmp/subdirectory.txt >expect && + git config --get-urlmatch HTTP https://example.com/subdirectory >actual && + test_cmp expect actual && + + echo http.cookiefile /tmp/subdirectory.txt >expect && + git config --get-urlmatch HTTP https://example.com/subdirectory/nested >actual && + test_cmp expect actual && + + echo http.cookiefile /tmp/user.txt >expect && + git config --get-urlmatch HTTP https://user@example.com/ >actual && + test_cmp expect actual && + + echo http.cookiefile /tmp/subdirectory.txt >expect && + git config --get-urlmatch HTTP https://averylonguser@example.com/subdirectory >actual && + test_cmp expect actual && + + echo http.cookiefile /tmp/preceding.txt >expect && + git config --get-urlmatch HTTP https://preceding.example.com >actual && + test_cmp expect actual && + + echo http.cookiefile /tmp/wildcard.txt >expect && + git config --get-urlmatch HTTP https://wildcard.example.com >actual && + test_cmp expect actual && + + echo http.cookiefile /tmp/sub.txt >expect && + git config --get-urlmatch HTTP https://sub.example.com/wildcardwithsubdomain >actual && + test_cmp expect actual && + + echo http.cookiefile /tmp/trailing.txt >expect && + git config --get-urlmatch HTTP https://trailing.example.com >actual && + test_cmp expect actual && + + echo http.cookiefile /tmp/sub.txt >expect && + git config --get-urlmatch HTTP https://user@sub.example.com >actual && + test_cmp expect actual +' + +test_expect_success 'urlmatch with wildcard' ' + cat >.git/config <<-\EOF && + [http] + sslVerify + [http "https://*.example.com"] + sslVerify = false + cookieFile = /tmp/cookie.txt + EOF + + test_expect_code 1 git config --bool --get-urlmatch doesnt.exist https://good.example.com >actual && + test_must_be_empty actual && + + echo true >expect && + git config --bool --get-urlmatch http.SSLverify https://example.com >actual && + test_cmp expect actual && + + echo true >expect && + git config --bool --get-urlmatch http.SSLverify https://good-example.com >actual && + test_cmp expect actual && + + echo true >expect && + git config --bool --get-urlmatch http.sslverify https://deep.nested.example.com >actual && + test_cmp expect actual && + + echo false >expect && + git config --bool --get-urlmatch http.sslverify https://good.example.com >actual && + test_cmp expect actual && + + { + echo http.cookiefile /tmp/cookie.txt && + echo http.sslverify false + } >expect && + git config --get-urlmatch HTTP https://good.example.com >actual && + test_cmp expect actual && + + echo http.sslverify >expect && + git config --get-urlmatch HTTP https://more.example.com.au >actual && + test_cmp expect actual +' + # good section hygiene test_expect_failure 'unsetting the last key in a section removes header' ' cat >.git/config <<-\EOF && diff --git a/t/t1400-update-ref.sh b/t/t1400-update-ref.sh index b0ffc0b573..825422341d 100755 --- a/t/t1400-update-ref.sh +++ b/t/t1400-update-ref.sh @@ -85,6 +85,24 @@ test_expect_success "delete $m (by HEAD)" ' ' rm -f .git/$m +test_expect_success "deleting current branch adds message to HEAD's log" ' + git update-ref $m $A && + git symbolic-ref HEAD $m && + git update-ref -m delete-$m -d $m && + ! test -f .git/$m && + grep "delete-$m$" .git/logs/HEAD +' +rm -f .git/$m + +test_expect_success "deleting by HEAD adds message to HEAD's log" ' + git update-ref $m $A && + git symbolic-ref HEAD $m && + git update-ref -m delete-by-head -d HEAD && + ! test -f .git/$m && + grep "delete-by-head$" .git/logs/HEAD +' +rm -f .git/$m + test_expect_success 'update-ref does not create reflogs by default' ' test_when_finished "git update-ref -d $outside" && git update-ref $outside $A && @@ -256,6 +274,33 @@ test_expect_success \ git update-ref HEAD'" $A && test $A"' = $(cat .git/'"$m"')' +test_expect_success "empty directory removal" ' + git branch d1/d2/r1 HEAD && + git branch d1/r2 HEAD && + test -f .git/refs/heads/d1/d2/r1 && + test -f .git/logs/refs/heads/d1/d2/r1 && + git branch -d d1/d2/r1 && + ! test -e .git/refs/heads/d1/d2 && + ! test -e .git/logs/refs/heads/d1/d2 && + test -f .git/refs/heads/d1/r2 && + test -f .git/logs/refs/heads/d1/r2 +' + +test_expect_success "symref empty directory removal" ' + git branch e1/e2/r1 HEAD && + git branch e1/r2 HEAD && + git checkout e1/e2/r1 && + test_when_finished "git checkout master" && + test -f .git/refs/heads/e1/e2/r1 && + test -f .git/logs/refs/heads/e1/e2/r1 && + git update-ref -d HEAD && + ! test -e .git/refs/heads/e1/e2 && + ! test -e .git/logs/refs/heads/e1/e2 && + test -f .git/refs/heads/e1/r2 && + test -f .git/logs/refs/heads/e1/r2 && + test -f .git/logs/HEAD +' + cat >expect <<EOF $Z $A $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150200 +0000 Initial Creation $A $B $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150260 +0000 Switch diff --git a/t/t1500-rev-parse.sh b/t/t1500-rev-parse.sh index 038e24c401..9ed8b8ccba 100755 --- a/t/t1500-rev-parse.sh +++ b/t/t1500-rev-parse.sh @@ -3,7 +3,7 @@ test_description='test git rev-parse' . ./test-lib.sh -# usage: [options] label is-bare is-inside-git is-inside-work prefix git-dir +# usage: [options] label is-bare is-inside-git is-inside-work prefix git-dir absolute-git-dir test_rev_parse () { d= bare= @@ -29,7 +29,8 @@ test_rev_parse () { --is-inside-git-dir \ --is-inside-work-tree \ --show-prefix \ - --git-dir + --git-dir \ + --absolute-git-dir do test $# -eq 0 && break expect="$1" @@ -62,29 +63,57 @@ test_expect_success 'setup' ' cp -R .git repo.git ' -test_rev_parse toplevel false false true '' .git +test_rev_parse toplevel false false true '' .git "$ROOT/.git" -test_rev_parse -C .git .git/ false true false '' . -test_rev_parse -C .git/objects .git/objects/ false true false '' "$ROOT/.git" +test_rev_parse -C .git .git/ false true false '' . "$ROOT/.git" +test_rev_parse -C .git/objects .git/objects/ false true false '' "$ROOT/.git" "$ROOT/.git" -test_rev_parse -C sub/dir subdirectory false false true sub/dir/ "$ROOT/.git" +test_rev_parse -C sub/dir subdirectory false false true sub/dir/ "$ROOT/.git" "$ROOT/.git" test_rev_parse -b t 'core.bare = true' true false false test_rev_parse -b u 'core.bare undefined' false false true -test_rev_parse -C work -g ../.git -b f 'GIT_DIR=../.git, core.bare = false' false false true '' +test_rev_parse -C work -g ../.git -b f 'GIT_DIR=../.git, core.bare = false' false false true '' "../.git" "$ROOT/.git" test_rev_parse -C work -g ../.git -b t 'GIT_DIR=../.git, core.bare = true' true false false '' test_rev_parse -C work -g ../.git -b u 'GIT_DIR=../.git, core.bare undefined' false false true '' -test_rev_parse -C work -g ../repo.git -b f 'GIT_DIR=../repo.git, core.bare = false' false false true '' +test_rev_parse -C work -g ../repo.git -b f 'GIT_DIR=../repo.git, core.bare = false' false false true '' "../repo.git" "$ROOT/repo.git" test_rev_parse -C work -g ../repo.git -b t 'GIT_DIR=../repo.git, core.bare = true' true false false '' test_rev_parse -C work -g ../repo.git -b u 'GIT_DIR=../repo.git, core.bare undefined' false false true '' +test_expect_success 'git-common-dir from worktree root' ' + echo .git >expect && + git rev-parse --git-common-dir >actual && + test_cmp expect actual +' + +test_expect_success 'git-common-dir inside sub-dir' ' + mkdir -p path/to/child && + test_when_finished "rm -rf path" && + echo "$(git -C path/to/child rev-parse --show-cdup).git" >expect && + git -C path/to/child rev-parse --git-common-dir >actual && + test_cmp expect actual +' + +test_expect_success 'git-path from worktree root' ' + echo .git/objects >expect && + git rev-parse --git-path objects >actual && + test_cmp expect actual +' + +test_expect_success 'git-path inside sub-dir' ' + mkdir -p path/to/child && + test_when_finished "rm -rf path" && + echo "$(git -C path/to/child rev-parse --show-cdup).git/objects" >expect && + git -C path/to/child rev-parse --git-path objects >actual && + test_cmp expect actual +' + test_done diff --git a/t/t1700-split-index.sh b/t/t1700-split-index.sh index 292a0720fc..6096f2c630 100755 --- a/t/t1700-split-index.sh +++ b/t/t1700-split-index.sh @@ -200,4 +200,20 @@ EOF test_cmp expect actual ' +test_expect_success 'rev-parse --shared-index-path' ' + test_create_repo split-index && + ( + cd split-index && + git update-index --split-index && + echo .git/sharedindex* >expect && + git rev-parse --shared-index-path >actual && + test_cmp expect actual && + mkdir subdirectory && + cd subdirectory && + echo ../.git/sharedindex* >expect && + git rev-parse --shared-index-path >actual && + test_cmp expect actual + ) +' + test_done diff --git a/t/t2027-worktree-list.sh b/t/t2027-worktree-list.sh index 465eeeacd3..848da5f368 100755 --- a/t/t2027-worktree-list.sh +++ b/t/t2027-worktree-list.sh @@ -14,10 +14,18 @@ test_expect_success 'rev-parse --git-common-dir on main worktree' ' test_cmp expected actual && mkdir sub && git -C sub rev-parse --git-common-dir >actual2 && - echo sub/.git >expected2 && + echo ../.git >expected2 && test_cmp expected2 actual2 ' +test_expect_success 'rev-parse --git-path objects linked worktree' ' + echo "$(git rev-parse --show-toplevel)/.git/objects" >expect && + test_when_finished "rm -rf linked-tree && git worktree prune" && + git worktree add --detach linked-tree master && + git -C linked-tree rev-parse --git-path objects >actual && + test_cmp expect actual +' + test_expect_success '"list" all worktrees from main' ' echo "$(git rev-parse --show-toplevel) $(git rev-parse --short HEAD) [$(git symbolic-ref --short HEAD)]" >expect && test_when_finished "rm -rf here && git worktree prune" && diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh index 8a833f354e..e36ed3b4e1 100755 --- a/t/t3200-branch.sh +++ b/t/t3200-branch.sh @@ -139,6 +139,12 @@ test_expect_success 'git branch -M baz bam should succeed when baz is checked ou test $(git rev-parse --abbrev-ref HEAD) = bam ' +test_expect_success 'git branch -M baz bam should add entries to .git/logs/HEAD' ' + msg="Branch: renamed refs/heads/baz to refs/heads/bam" && + grep " 0\{40\}.*$msg$" .git/logs/HEAD && + grep "^0\{40\}.*$msg$" .git/logs/HEAD +' + test_expect_success 'git branch -M baz bam should succeed when baz is checked out as linked working tree' ' git checkout master && git worktree add -b baz bazdir && diff --git a/t/t3203-branch-output.sh b/t/t3203-branch-output.sh index 52283dfc8c..5778c0afe1 100755 --- a/t/t3203-branch-output.sh +++ b/t/t3203-branch-output.sh @@ -194,7 +194,7 @@ test_expect_success 'local-branch symrefs shortened properly' ' git symbolic-ref refs/heads/ref-to-remote refs/remotes/origin/branch-one && cat >expect <<-\EOF && ref-to-branch -> branch-one - ref-to-remote -> refs/remotes/origin/branch-one + ref-to-remote -> origin/branch-one EOF git branch >actual.raw && grep ref-to <actual.raw >actual && @@ -225,4 +225,18 @@ test_expect_success 'sort branches, ignore case' ' ) ' +test_expect_success 'git branch --format option' ' + cat >expect <<-\EOF && + Refname is (HEAD detached from fromtag) + Refname is refs/heads/ambiguous + Refname is refs/heads/branch-one + Refname is refs/heads/branch-two + Refname is refs/heads/master + Refname is refs/heads/ref-to-branch + Refname is refs/heads/ref-to-remote + EOF + git branch --format="Refname is %(refname)" >actual && + test_cmp expect actual +' + test_done diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh index e2f18d11f6..33d392ba11 100755 --- a/t/t3404-rebase-interactive.sh +++ b/t/t3404-rebase-interactive.sh @@ -556,7 +556,7 @@ test_expect_success 'clean error after failed "exec"' ' echo "edited again" > file7 && git add file7 && test_must_fail git rebase --continue 2>error && - test_i18ngrep "You have staged changes in your working tree." error + test_i18ngrep "you have staged changes in your working tree" error ' test_expect_success 'rebase a detached HEAD' ' diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh index 2de3e18ce6..89877e4b52 100755 --- a/t/t3903-stash.sh +++ b/t/t3903-stash.sh @@ -274,9 +274,7 @@ test_expect_success 'stash --invalid-option' ' git add file2 && test_must_fail git stash --invalid-option && test_must_fail git stash save --invalid-option && - test bar5,bar6 = $(cat file),$(cat file2) && - git stash -- -message-starting-with-dash && - test bar,bar2 = $(cat file),$(cat file2) + test bar5,bar6 = $(cat file),$(cat file2) ' test_expect_success 'stash an added file' ' @@ -775,4 +773,138 @@ test_expect_success 'stash is not confused by partial renames' ' test_path_is_missing file ' +test_expect_success 'push -m shows right message' ' + >foo && + git add foo && + git stash push -m "test message" && + echo "stash@{0}: On master: test message" >expect && + git stash list -1 >actual && + test_cmp expect actual +' + +test_expect_success 'create stores correct message' ' + >foo && + git add foo && + STASH_ID=$(git stash create "create test message") && + echo "On master: create test message" >expect && + git show --pretty=%s -s ${STASH_ID} >actual && + test_cmp expect actual +' + +test_expect_success 'create with multiple arguments for the message' ' + >foo && + git add foo && + STASH_ID=$(git stash create test untracked) && + echo "On master: test untracked" >expect && + git show --pretty=%s -s ${STASH_ID} >actual && + test_cmp expect actual +' + +test_expect_success 'stash -- <pathspec> stashes and restores the file' ' + >foo && + >bar && + git add foo bar && + git stash push -- foo && + test_path_is_file bar && + test_path_is_missing foo && + git stash pop && + test_path_is_file foo && + test_path_is_file bar +' + +test_expect_success 'stash with multiple pathspec arguments' ' + >foo && + >bar && + >extra && + git add foo bar extra && + git stash push -- foo bar && + test_path_is_missing bar && + test_path_is_missing foo && + test_path_is_file extra && + git stash pop && + test_path_is_file foo && + test_path_is_file bar && + test_path_is_file extra +' + +test_expect_success 'stash with file including $IFS character' ' + >"foo bar" && + >foo && + >bar && + git add foo* && + git stash push -- "foo b*" && + test_path_is_missing "foo bar" && + test_path_is_file foo && + test_path_is_file bar && + git stash pop && + test_path_is_file "foo bar" && + test_path_is_file foo && + test_path_is_file bar +' + +test_expect_success 'stash with pathspec matching multiple paths' ' + echo original >file && + echo original >other-file && + git commit -m "two" file other-file && + echo modified >file && + echo modified >other-file && + git stash push -- "*file" && + echo original >expect && + test_cmp expect file && + test_cmp expect other-file && + git stash pop && + echo modified >expect && + test_cmp expect file && + test_cmp expect other-file +' + +test_expect_success 'stash push -p with pathspec shows no changes only once' ' + >foo && + git add foo && + git commit -m "tmp" && + git stash push -p foo >actual && + echo "No local changes to save" >expect && + git reset --hard HEAD~ && + test_cmp expect actual +' + +test_expect_success 'stash push with pathspec shows no changes when there are none' ' + >foo && + git add foo && + git commit -m "tmp" && + git stash push foo >actual && + echo "No local changes to save" >expect && + git reset --hard HEAD~ && + test_cmp expect actual +' + +test_expect_success 'stash push with pathspec not in the repository errors out' ' + >untracked && + test_must_fail git stash push untracked && + test_path_is_file untracked +' + +test_expect_success 'untracked files are left in place when -u is not given' ' + >file && + git add file && + >untracked && + git stash push file && + test_path_is_file untracked +' + +test_expect_success 'stash without verb with pathspec' ' + >"foo bar" && + >foo && + >bar && + git add foo* && + git stash -- "foo b*" && + test_path_is_missing "foo bar" && + test_path_is_file foo && + test_path_is_file bar && + git stash pop && + test_path_is_file "foo bar" && + test_path_is_file foo && + test_path_is_file bar +' + test_done diff --git a/t/t3905-stash-include-untracked.sh b/t/t3905-stash-include-untracked.sh index f372fc8ca8..193adc7b68 100755 --- a/t/t3905-stash-include-untracked.sh +++ b/t/t3905-stash-include-untracked.sh @@ -185,4 +185,30 @@ test_expect_success 'stash save --all is stash poppable' ' test -s .gitignore ' +test_expect_success 'stash push --include-untracked with pathspec' ' + >foo && + >bar && + git stash push --include-untracked -- foo && + test_path_is_file bar && + test_path_is_missing foo && + git stash pop && + test_path_is_file bar && + test_path_is_file foo +' + +test_expect_success 'stash push with $IFS character' ' + >"foo bar" && + >foo && + >bar && + git add foo* && + git stash push --include-untracked -- "foo b*" && + test_path_is_missing "foo bar" && + test_path_is_file foo && + test_path_is_file bar && + git stash pop && + test_path_is_file "foo bar" && + test_path_is_file foo && + test_path_is_file bar +' + test_done diff --git a/t/t5316-pack-delta-depth.sh b/t/t5316-pack-delta-depth.sh new file mode 100755 index 0000000000..37143ea0ac --- /dev/null +++ b/t/t5316-pack-delta-depth.sh @@ -0,0 +1,93 @@ +#!/bin/sh + +test_description='pack-objects breaks long cross-pack delta chains' +. ./test-lib.sh + +# This mirrors a repeated push setup: +# +# 1. A client repeatedly modifies some files, makes a +# commit, and pushes the result. It does this N times +# before we get around to repacking. +# +# 2. Each push generates a thin pack with the new version of +# various objects. Let's consider some file in the root tree +# which is updated in each commit. +# +# When generating push number X, we feed commit X-1 (and +# thus blob X-1) as a preferred base. The resulting pack has +# blob X as a thin delta against blob X-1. +# +# On the receiving end, "index-pack --fix-thin" will +# complete the pack with a base copy of blob X-1. +# +# 3. In older versions of git, if we used the delta from +# pack X, then we'd always find blob X-1 as a base in the +# same pack (and generate a fresh delta). +# +# But with the pack mru, we jump from delta to delta +# following the traversal order: +# +# a. We grab blob X from pack X as a delta, putting it at +# the tip of our mru list. +# +# b. Eventually we move onto commit X-1. We need other +# objects which are only in pack X-1 (in the test code +# below, it's the containing tree). That puts pack X-1 +# at the tip of our mru list. +# +# c. Eventually we look for blob X-1, and we find the +# version in pack X-1 (because it's the mru tip). +# +# Now we have blob X as a delta against X-1, which is a delta +# against X-2, and so forth. +# +# In the real world, these small pushes would get exploded by +# unpack-objects rather than "index-pack --fix-thin", but the +# same principle applies to larger pushes (they only need one +# repeatedly-modified file to generate the delta chain). + +test_expect_success 'create series of packs' ' + test-genrandom foo 4096 >content && + prev= && + for i in $(test_seq 1 10) + do + cat content >file && + echo $i >>file && + git add file && + git commit -m $i && + cur=$(git rev-parse HEAD^{tree}) && + { + test -n "$prev" && echo "-$prev" + echo $cur + echo "$(git rev-parse :file) file" + } | git pack-objects --stdout >tmp && + git index-pack --stdin --fix-thin <tmp || return 1 + prev=$cur + done +' + +max_chain() { + git index-pack --verify-stat-only "$1" >output && + perl -lne ' + /chain length = (\d+)/ and $len = $1; + END { print $len } + ' output +} + +# Note that this whole setup is pretty reliant on the current +# packing heuristics. We double-check that our test case +# actually produces a long chain. If it doesn't, it should be +# adjusted (or scrapped if the heuristics have become too unreliable) +test_expect_success 'packing produces a long delta' ' + # Use --window=0 to make sure we are seeing reused deltas, + # not computing a new long chain. + pack=$(git pack-objects --all --window=0 </dev/null pack) && + test 9 = "$(max_chain pack-$pack.pack)" +' + +test_expect_success '--depth limits depth' ' + pack=$(git pack-objects --all --depth=5 </dev/null pack) && + test 5 = "$(max_chain pack-$pack.pack)" +' + +test_done diff --git a/t/t5400-send-pack.sh b/t/t5400-send-pack.sh index 305ca7a930..3331e0f534 100755 --- a/t/t5400-send-pack.sh +++ b/t/t5400-send-pack.sh @@ -255,4 +255,42 @@ test_expect_success 'deny pushing to delete current branch' ' ) ' +extract_ref_advertisement () { + perl -lne ' + # \\ is there to skip capabilities after \0 + /push< ([^\\]+)/ or next; + exit 0 if $1 eq "0000"; + print $1; + ' +} + +test_expect_success 'receive-pack de-dupes .have lines' ' + git init shared && + git -C shared commit --allow-empty -m both && + git clone -s shared fork && + ( + cd shared && + git checkout -b only-shared && + git commit --allow-empty -m only-shared && + git update-ref refs/heads/foo HEAD + ) && + + # Notable things in this expectation: + # - local refs are not de-duped + # - .have does not duplicate locals + # - .have does not duplicate itself + local=$(git -C fork rev-parse HEAD) && + shared=$(git -C shared rev-parse only-shared) && + cat >expect <<-EOF && + $local refs/heads/master + $local refs/remotes/origin/HEAD + $local refs/remotes/origin/master + $shared .have + EOF + + GIT_TRACE_PACKET=$(pwd)/trace git push fork HEAD:foo && + extract_ref_advertisement <trace >refs && + test_cmp expect refs +' + test_done diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh index ba46e86de0..a6c0178f3a 100755 --- a/t/t5505-remote.sh +++ b/t/t5505-remote.sh @@ -153,6 +153,25 @@ test_expect_success 'remove errors out early when deleting non-existent branch' ) ' +test_expect_success 'remove remote with a branch without configured merge' ' + test_when_finished "( + git -C test checkout master; + git -C test branch -D two; + git -C test config --remove-section remote.two; + git -C test config --remove-section branch.second; + true + )" && + ( + cd test && + git remote add two ../two && + git fetch two && + git checkout -b second two/master^0 && + git config branch.second.remote two && + git checkout master && + git remote rm two + ) +' + test_expect_success 'rename errors out early when deleting non-existent branch' ' ( cd test && @@ -725,7 +744,7 @@ test_expect_success 'rename a remote' ' ( cd four && git remote rename origin upstream && - rmdir .git/refs/remotes/origin && + test -z "$(git for-each-ref refs/remotes/origin)" && test "$(git symbolic-ref refs/remotes/upstream/HEAD)" = "refs/remotes/upstream/master" && test "$(git rev-parse upstream/master)" = "$(git rev-parse master)" && test "$(git config remote.upstream.fetch)" = "+refs/heads/*:refs/remotes/upstream/*" && diff --git a/t/t5512-ls-remote.sh b/t/t5512-ls-remote.sh index 55fc83fc06..94fc9be9ce 100755 --- a/t/t5512-ls-remote.sh +++ b/t/t5512-ls-remote.sh @@ -248,4 +248,13 @@ test_expect_success PIPE,JGIT,GIT_DAEMON 'indicate no refs in standards-complian test_expect_code 2 git ls-remote --exit-code git://localhost:$JGIT_DAEMON_PORT/empty.git ' +test_expect_success 'ls-remote works outside repository' ' + # It is important for this repo to be inside the nongit + # area, as we want a repo name that does not include + # slashes (because those inhibit some of our configuration + # lookups). + nongit git init --bare dst.git && + nongit git ls-remote dst.git +' + test_done diff --git a/t/t5550-http-fetch-dumb.sh b/t/t5550-http-fetch-dumb.sh index aeb3a63f7c..87308cdced 100755 --- a/t/t5550-http-fetch-dumb.sh +++ b/t/t5550-http-fetch-dumb.sh @@ -34,6 +34,15 @@ test_expect_success 'clone http repository' ' test_cmp file clone/file ' +test_expect_success 'list refs from outside any repository' ' + cat >expect <<-EOF && + $(git rev-parse master) HEAD + $(git rev-parse master) refs/heads/master + EOF + nongit git ls-remote "$HTTPD_URL/dumb/repo.git" >actual && + test_cmp expect actual +' + test_expect_success 'create password-protected repository' ' mkdir -p "$HTTPD_DOCUMENT_ROOT_PATH/auth/dumb/" && cp -Rf "$HTTPD_DOCUMENT_ROOT_PATH/repo.git" \ @@ -378,5 +387,14 @@ test_expect_success 'http-alternates triggers not-from-user protocol check' ' clone $HTTPD_URL/dumb/evil.git evil-user ' +test_expect_success 'can redirect through non-"info/refs?service=git-upload-pack" URL' ' + git clone "$HTTPD_URL/redir-to/dumb/repo.git" +' + +test_expect_success 'print HTTP error when any intermediate redirect throws error' ' + test_must_fail git clone "$HTTPD_URL/redir-to/502" 2> stderr && + test_i18ngrep "unable to access.*/redir-to/502" stderr +' + stop_httpd test_done diff --git a/t/t5601-clone.sh b/t/t5601-clone.sh index 4241ea5b32..b52b8acf98 100755 --- a/t/t5601-clone.sh +++ b/t/t5601-clone.sh @@ -386,6 +386,47 @@ test_expect_success 'tortoiseplink is like putty, with extra arguments' ' expect_ssh "-batch -P 123" myhost src ' +test_expect_success 'double quoted plink.exe in GIT_SSH_COMMAND' ' + copy_ssh_wrapper_as "$TRASH_DIRECTORY/plink.exe" && + GIT_SSH_COMMAND="\"$TRASH_DIRECTORY/plink.exe\" -v" \ + git clone "[myhost:123]:src" ssh-bracket-clone-plink-3 && + expect_ssh "-v -P 123" myhost src +' + +SQ="'" +test_expect_success 'single quoted plink.exe in GIT_SSH_COMMAND' ' + copy_ssh_wrapper_as "$TRASH_DIRECTORY/plink.exe" && + GIT_SSH_COMMAND="$SQ$TRASH_DIRECTORY/plink.exe$SQ -v" \ + git clone "[myhost:123]:src" ssh-bracket-clone-plink-4 && + expect_ssh "-v -P 123" myhost src +' + +test_expect_success 'GIT_SSH_VARIANT overrides plink detection' ' + copy_ssh_wrapper_as "$TRASH_DIRECTORY/plink" && + GIT_SSH_VARIANT=ssh \ + git clone "[myhost:123]:src" ssh-bracket-clone-variant-1 && + expect_ssh "-p 123" myhost src +' + +test_expect_success 'ssh.variant overrides plink detection' ' + copy_ssh_wrapper_as "$TRASH_DIRECTORY/plink" && + git -c ssh.variant=ssh \ + clone "[myhost:123]:src" ssh-bracket-clone-variant-2 && + expect_ssh "-p 123" myhost src +' + +test_expect_success 'GIT_SSH_VARIANT overrides plink detection to plink' ' + GIT_SSH_VARIANT=plink \ + git clone "[myhost:123]:src" ssh-bracket-clone-variant-3 && + expect_ssh "-P 123" myhost src +' + +test_expect_success 'GIT_SSH_VARIANT overrides plink to tortoiseplink' ' + GIT_SSH_VARIANT=tortoiseplink \ + git clone "[myhost:123]:src" ssh-bracket-clone-variant-4 && + expect_ssh "-batch -P 123" myhost src +' + # Reset the GIT_SSH environment variable for clone tests. setup_ssh_wrapper diff --git a/t/t6007-rev-list-cherry-pick-file.sh b/t/t6007-rev-list-cherry-pick-file.sh index 1408b608eb..2959745196 100755 --- a/t/t6007-rev-list-cherry-pick-file.sh +++ b/t/t6007-rev-list-cherry-pick-file.sh @@ -99,6 +99,44 @@ test_expect_success '--cherry-pick bar does not come up empty (II)' ' test_cmp actual.named expect ' +test_expect_success 'name-rev multiple --refs combine inclusive' ' + git rev-list --left-right --cherry-pick F...E -- bar >actual && + git name-rev --stdin --name-only --refs="*tags/F" --refs="*tags/E" \ + <actual >actual.named && + test_cmp actual.named expect +' + +cat >expect <<EOF +<tags/F +EOF + +test_expect_success 'name-rev --refs excludes non-matched patterns' ' + git rev-list --left-right --right-only --cherry-pick F...E -- bar >>expect && + git rev-list --left-right --cherry-pick F...E -- bar >actual && + git name-rev --stdin --name-only --refs="*tags/F" \ + <actual >actual.named && + test_cmp actual.named expect +' + +cat >expect <<EOF +<tags/F +EOF + +test_expect_success 'name-rev --exclude excludes matched patterns' ' + git rev-list --left-right --right-only --cherry-pick F...E -- bar >>expect && + git rev-list --left-right --cherry-pick F...E -- bar >actual && + git name-rev --stdin --name-only --refs="*tags/*" --exclude="*E" \ + <actual >actual.named && + test_cmp actual.named expect +' + +test_expect_success 'name-rev --no-refs clears the refs list' ' + git rev-list --left-right --cherry-pick F...E -- bar >expect && + git name-rev --stdin --name-only --refs="*tags/F" --refs="*tags/E" --no-refs --refs="*tags/G" \ + <expect >actual && + test_cmp actual expect +' + cat >expect <<EOF +tags/F =tags/D diff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh index 3d5c238c81..97a07655a0 100755 --- a/t/t6040-tracking-info.sh +++ b/t/t6040-tracking-info.sh @@ -44,7 +44,7 @@ b1 [ahead 1, behind 1] d b2 [ahead 1, behind 1] d b3 [behind 1] b b4 [ahead 2] f -b5 g +b5 [gone] g b6 c EOF diff --git a/t/t6045-merge-rename-delete.sh b/t/t6045-merge-rename-delete.sh new file mode 100755 index 0000000000..5d33577d2f --- /dev/null +++ b/t/t6045-merge-rename-delete.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +test_description='Merge-recursive rename/delete conflict message' +. ./test-lib.sh + +test_expect_success 'rename/delete' ' + echo foo >A && + git add A && + git commit -m "initial" && + + git checkout -b rename && + git mv A B && + git commit -m "rename" && + + git checkout master && + git rm A && + git commit -m "delete" && + + test_must_fail git merge --strategy=recursive rename >output && + test_i18ngrep "CONFLICT (rename/delete): A deleted in HEAD and renamed to B in rename. Version rename of B left in tree." output +' + +test_done diff --git a/t/t6120-describe.sh b/t/t6120-describe.sh index 85f269411c..167491fd5b 100755 --- a/t/t6120-describe.sh +++ b/t/t6120-describe.sh @@ -182,6 +182,10 @@ check_describe "test2-lightweight-*" --tags --match="test2-*" check_describe "test2-lightweight-*" --long --tags --match="test2-*" HEAD^ +check_describe "test1-lightweight-*" --long --tags --match="test1-*" --match="test2-*" HEAD^ + +check_describe "test2-lightweight-*" --long --tags --match="test1-*" --no-match --match="test2-*" HEAD^ + test_expect_success 'name-rev with exact tags' ' echo A >expect && tag_object=$(git rev-parse refs/tags/A) && @@ -206,4 +210,27 @@ test_expect_success 'describe --contains with the exact tags' ' test_cmp expect actual ' +test_expect_success 'describe --contains and --match' ' + echo "A^0" >expect && + tagged_commit=$(git rev-parse "refs/tags/A^0") && + test_must_fail git describe --contains --match="B" $tagged_commit && + git describe --contains --match="B" --match="A" $tagged_commit >actual && + test_cmp expect actual +' + +test_expect_success 'describe --exclude' ' + echo "c~1" >expect && + tagged_commit=$(git rev-parse "refs/tags/A^0") && + test_must_fail git describe --contains --match="B" $tagged_commit && + git describe --contains --match="?" --exclude="A" $tagged_commit >actual && + test_cmp expect actual +' + +test_expect_success 'describe --contains and --no-match' ' + echo "A^0" >expect && + tagged_commit=$(git rev-parse "refs/tags/A^0") && + git describe --contains --match="B" --no-match $tagged_commit >actual && + test_cmp expect actual +' + test_done diff --git a/t/t6132-pathspec-exclude.sh b/t/t6132-pathspec-exclude.sh index d51595cf6b..9dd5cde5fc 100755 --- a/t/t6132-pathspec-exclude.sh +++ b/t/t6132-pathspec-exclude.sh @@ -25,8 +25,10 @@ EOF test_cmp expect actual ' -test_expect_success 'exclude only should error out' ' - test_must_fail git log --oneline --format=%s -- ":(exclude)sub" +test_expect_success 'exclude only no longer errors out' ' + git log --oneline --format=%s -- . ":(exclude)sub" >expect && + git log --oneline --format=%s -- ":(exclude)sub" >actual && + test_cmp expect actual ' test_expect_success 't_e_i() exclude sub' ' diff --git a/t/t6300-for-each-ref.sh b/t/t6300-for-each-ref.sh index aea1dfc714..834a9ed168 100755 --- a/t/t6300-for-each-ref.sh +++ b/t/t6300-for-each-ref.sh @@ -38,6 +38,7 @@ test_atom() { case "$1" in head) ref=refs/heads/master ;; tag) ref=refs/tags/testtag ;; + sym) ref=refs/heads/sym ;; *) ref=$1 ;; esac printf '%s\n' "$3" >expected @@ -50,16 +51,40 @@ test_atom() { test_atom head refname refs/heads/master test_atom head refname:short master +test_atom head refname:lstrip=1 heads/master +test_atom head refname:lstrip=2 master +test_atom head refname:lstrip=-1 master +test_atom head refname:lstrip=-2 heads/master +test_atom head refname:rstrip=1 refs/heads +test_atom head refname:rstrip=2 refs +test_atom head refname:rstrip=-1 refs +test_atom head refname:rstrip=-2 refs/heads test_atom head refname:strip=1 heads/master test_atom head refname:strip=2 master +test_atom head refname:strip=-1 master +test_atom head refname:strip=-2 heads/master test_atom head upstream refs/remotes/origin/master test_atom head upstream:short origin/master +test_atom head upstream:lstrip=2 origin/master +test_atom head upstream:lstrip=-2 origin/master +test_atom head upstream:rstrip=2 refs/remotes +test_atom head upstream:rstrip=-2 refs/remotes +test_atom head upstream:strip=2 origin/master +test_atom head upstream:strip=-2 origin/master test_atom head push refs/remotes/myfork/master test_atom head push:short myfork/master +test_atom head push:lstrip=1 remotes/myfork/master +test_atom head push:lstrip=-1 master +test_atom head push:rstrip=1 refs/remotes/myfork +test_atom head push:rstrip=-1 refs +test_atom head push:strip=1 remotes/myfork/master +test_atom head push:strip=-1 master test_atom head objecttype commit test_atom head objectsize 171 test_atom head objectname $(git rev-parse refs/heads/master) test_atom head objectname:short $(git rev-parse --short refs/heads/master) +test_atom head objectname:short=1 $(git rev-parse --short=1 refs/heads/master) +test_atom head objectname:short=10 $(git rev-parse --short=10 refs/heads/master) test_atom head tree $(git rev-parse refs/heads/master^{tree}) test_atom head parent '' test_atom head numparent 0 @@ -99,6 +124,8 @@ test_atom tag objecttype tag test_atom tag objectsize 154 test_atom tag objectname $(git rev-parse refs/tags/testtag) test_atom tag objectname:short $(git rev-parse --short refs/tags/testtag) +test_atom head objectname:short=1 $(git rev-parse --short=1 refs/heads/master) +test_atom head objectname:short=10 $(git rev-parse --short=10 refs/heads/master) test_atom tag tree '' test_atom tag parent '' test_atom tag numparent '' @@ -134,16 +161,6 @@ test_expect_success 'Check invalid atoms names are errors' ' test_must_fail git for-each-ref --format="%(INVALID)" refs/heads ' -test_expect_success 'arguments to :strip must be positive integers' ' - test_must_fail git for-each-ref --format="%(refname:strip=0)" && - test_must_fail git for-each-ref --format="%(refname:strip=-1)" && - test_must_fail git for-each-ref --format="%(refname:strip=foo)" -' - -test_expect_success 'stripping refnames too far gives an error' ' - test_must_fail git for-each-ref --format="%(refname:strip=3)" -' - test_expect_success 'Check format specifiers are ignored in naming date atoms' ' git for-each-ref --format="%(authordate)" refs/heads && git for-each-ref --format="%(authordate:default) %(authordate)" refs/heads && @@ -164,6 +181,12 @@ test_expect_success 'Check invalid format specifiers are errors' ' test_must_fail git for-each-ref --format="%(authordate:INVALID)" refs/heads ' +test_expect_success 'arguments to %(objectname:short=) must be positive integers' ' + test_must_fail git for-each-ref --format="%(objectname:short=0)" && + test_must_fail git for-each-ref --format="%(objectname:short=-1)" && + test_must_fail git for-each-ref --format="%(objectname:short=foo)" +' + test_date () { f=$1 && committer_date=$2 && @@ -362,6 +385,8 @@ test_expect_success 'setup for upstream:track[short]' ' test_atom head upstream:track '[ahead 1]' test_atom head upstream:trackshort '>' +test_atom head upstream:track,nobracket 'ahead 1' +test_atom head upstream:nobracket,track 'ahead 1' test_atom head push:track '[ahead 1]' test_atom head push:trackshort '>' @@ -372,7 +397,7 @@ test_expect_success 'Check that :track[short] cannot be used with other atoms' ' test_expect_success 'Check that :track[short] works when upstream is invalid' ' cat >expected <<-\EOF && - + [gone] EOF test_when_finished "git config branch.master.merge refs/heads/master" && @@ -554,11 +579,12 @@ test_expect_success 'Verify sort with multiple keys' ' test_cmp expected actual ' + test_expect_success 'do not dereference NULL upon %(HEAD) on unborn branch' ' test_when_finished "git checkout master" && git for-each-ref --format="%(HEAD) %(refname:short)" refs/heads/ >actual && sed -e "s/^\* / /" actual >expect && - git checkout --orphan HEAD && + git checkout --orphan orphaned-branch && git for-each-ref --format="%(HEAD) %(refname:short)" refs/heads/ >actual && test_cmp expect actual ' @@ -588,4 +614,52 @@ test_expect_success 'basic atom: head contents:trailers' ' test_cmp expect actual.clean ' +test_expect_success 'Add symbolic ref for the following tests' ' + git symbolic-ref refs/heads/sym refs/heads/master +' + +cat >expected <<EOF +refs/heads/master +EOF + +test_expect_success 'Verify usage of %(symref) atom' ' + git for-each-ref --format="%(symref)" refs/heads/sym >actual && + test_cmp expected actual +' + +cat >expected <<EOF +heads/master +EOF + +test_expect_success 'Verify usage of %(symref:short) atom' ' + git for-each-ref --format="%(symref:short)" refs/heads/sym >actual && + test_cmp expected actual +' + +cat >expected <<EOF +master +heads/master +EOF + +test_expect_success 'Verify usage of %(symref:lstrip) atom' ' + git for-each-ref --format="%(symref:lstrip=2)" refs/heads/sym > actual && + git for-each-ref --format="%(symref:lstrip=-2)" refs/heads/sym >> actual && + test_cmp expected actual && + + git for-each-ref --format="%(symref:strip=2)" refs/heads/sym > actual && + git for-each-ref --format="%(symref:strip=-2)" refs/heads/sym >> actual && + test_cmp expected actual +' + +cat >expected <<EOF +refs +refs/heads +EOF + +test_expect_success 'Verify usage of %(symref:rstrip) atom' ' + git for-each-ref --format="%(symref:rstrip=2)" refs/heads/sym > actual && + git for-each-ref --format="%(symref:rstrip=-2)" refs/heads/sym >> actual && + test_cmp expected actual +' + test_done diff --git a/t/t6302-for-each-ref-filter.sh b/t/t6302-for-each-ref-filter.sh index d0ab09f4bd..a09a1a46ef 100755 --- a/t/t6302-for-each-ref-filter.sh +++ b/t/t6302-for-each-ref-filter.sh @@ -327,4 +327,98 @@ test_expect_success 'reverse version sort' ' test_cmp expect actual ' +test_expect_success 'improper usage of %(if), %(then), %(else) and %(end) atoms' ' + test_must_fail git for-each-ref --format="%(if)" && + test_must_fail git for-each-ref --format="%(then) %(end)" && + test_must_fail git for-each-ref --format="%(else) %(end)" && + test_must_fail git for-each-ref --format="%(if) %(else) %(end)" && + test_must_fail git for-each-ref --format="%(if) %(then) %(then) %(end)" && + test_must_fail git for-each-ref --format="%(then) %(else) %(end)" && + test_must_fail git for-each-ref --format="%(if) %(else) %(end)" && + test_must_fail git for-each-ref --format="%(if) %(then) %(else)" && + test_must_fail git for-each-ref --format="%(if) %(else) %(then) %(end)" && + test_must_fail git for-each-ref --format="%(if) %(then) %(else) %(else) %(end)" && + test_must_fail git for-each-ref --format="%(if) %(end)" +' + +test_expect_success 'check %(if)...%(then)...%(end) atoms' ' + git for-each-ref --format="%(refname)%(if)%(authorname)%(then) Author: %(authorname)%(end)" >actual && + cat >expect <<-\EOF && + refs/heads/master Author: A U Thor + refs/heads/side Author: A U Thor + refs/odd/spot Author: A U Thor + refs/tags/annotated-tag + refs/tags/doubly-annotated-tag + refs/tags/doubly-signed-tag + refs/tags/foo1.10 Author: A U Thor + refs/tags/foo1.3 Author: A U Thor + refs/tags/foo1.6 Author: A U Thor + refs/tags/four Author: A U Thor + refs/tags/one Author: A U Thor + refs/tags/signed-tag + refs/tags/three Author: A U Thor + refs/tags/two Author: A U Thor + EOF + test_cmp expect actual +' + +test_expect_success 'check %(if)...%(then)...%(else)...%(end) atoms' ' + git for-each-ref --format="%(if)%(authorname)%(then)%(authorname)%(else)No author%(end): %(refname)" >actual && + cat >expect <<-\EOF && + A U Thor: refs/heads/master + A U Thor: refs/heads/side + A U Thor: refs/odd/spot + No author: refs/tags/annotated-tag + No author: refs/tags/doubly-annotated-tag + No author: refs/tags/doubly-signed-tag + A U Thor: refs/tags/foo1.10 + A U Thor: refs/tags/foo1.3 + A U Thor: refs/tags/foo1.6 + A U Thor: refs/tags/four + A U Thor: refs/tags/one + No author: refs/tags/signed-tag + A U Thor: refs/tags/three + A U Thor: refs/tags/two + EOF + test_cmp expect actual +' +test_expect_success 'ignore spaces in %(if) atom usage' ' + git for-each-ref --format="%(refname:short): %(if)%(HEAD)%(then)Head ref%(else)Not Head ref%(end)" >actual && + cat >expect <<-\EOF && + master: Head ref + side: Not Head ref + odd/spot: Not Head ref + annotated-tag: Not Head ref + doubly-annotated-tag: Not Head ref + doubly-signed-tag: Not Head ref + foo1.10: Not Head ref + foo1.3: Not Head ref + foo1.6: Not Head ref + four: Not Head ref + one: Not Head ref + signed-tag: Not Head ref + three: Not Head ref + two: Not Head ref + EOF + test_cmp expect actual +' + +test_expect_success 'check %(if:equals=<string>)' ' + git for-each-ref --format="%(if:equals=master)%(refname:short)%(then)Found master%(else)Not master%(end)" refs/heads/ >actual && + cat >expect <<-\EOF && + Found master + Not master + EOF + test_cmp expect actual +' + +test_expect_success 'check %(if:notequals=<string>)' ' + git for-each-ref --format="%(if:notequals=master)%(refname:short)%(then)Not master%(else)Found master%(end)" refs/heads/ >actual && + cat >expect <<-\EOF && + Found master + Not master + EOF + test_cmp expect actual +' + test_done diff --git a/t/t6500-gc.sh b/t/t6500-gc.sh index 1762dfa6a3..08de2e8ab0 100755 --- a/t/t6500-gc.sh +++ b/t/t6500-gc.sh @@ -67,5 +67,20 @@ test_expect_success 'auto gc with too many loose objects does not attempt to cre test_line_count = 2 new # There is one new pack and its .idx ' +test_expect_success 'background auto gc does not run if gc.log is present and recent but does if it is old' ' + test_commit foo && + test_commit bar && + git repack && + test_config gc.autopacklimit 1 && + test_config gc.autodetach true && + echo fleem >.git/gc.log && + test_must_fail git gc --auto 2>err && + test_i18ngrep "^error:" err && + test_config gc.logexpiry 5.days && + test-chmtime =-345600 .git/gc.log && + test_must_fail git gc --auto && + test_config gc.logexpiry 2.days && + git gc --auto +' test_done diff --git a/t/t7004-tag.sh b/t/t7004-tag.sh index 072e6c6b88..b4698ab5f5 100755 --- a/t/t7004-tag.sh +++ b/t/t7004-tag.sh @@ -81,9 +81,25 @@ test_expect_success 'creating a tag using default HEAD should succeed' ' ' test_expect_success 'creating a tag with --create-reflog should create reflog' ' + git log -1 \ + --format="format:tag: tagging %h (%s, %cd)%n" \ + --date=format:%Y-%m-%d >expected && test_when_finished "git tag -d tag_with_reflog" && git tag --create-reflog tag_with_reflog && - git reflog exists refs/tags/tag_with_reflog + git reflog exists refs/tags/tag_with_reflog && + sed -e "s/^.* //" .git/logs/refs/tags/tag_with_reflog >actual && + test_cmp expected actual +' + +test_expect_success 'annotated tag with --create-reflog has correct message' ' + git log -1 \ + --format="format:tag: tagging %h (%s, %cd)%n" \ + --date=format:%Y-%m-%d >expected && + test_when_finished "git tag -d tag_with_reflog" && + git tag -m "annotated tag" --create-reflog tag_with_reflog && + git reflog exists refs/tags/tag_with_reflog && + sed -e "s/^.* //" .git/logs/refs/tags/tag_with_reflog >actual && + test_cmp expected actual ' test_expect_success '--create-reflog does not create reflog on failure' ' diff --git a/t/t7518-ident-corner-cases.sh b/t/t7518-ident-corner-cases.sh new file mode 100755 index 0000000000..b22f631261 --- /dev/null +++ b/t/t7518-ident-corner-cases.sh @@ -0,0 +1,36 @@ +#!/bin/sh + +test_description='corner cases in ident strings' +. ./test-lib.sh + +# confirm that we do not segfault _and_ that we do not say "(null)", as +# glibc systems will quietly handle our NULL pointer +# +# Note also that we can't use "env" here because we need to unset a variable, +# and "-u" is not portable. +test_expect_success 'empty name and missing email' ' + ( + sane_unset GIT_AUTHOR_EMAIL && + GIT_AUTHOR_NAME= && + test_must_fail git commit --allow-empty -m foo 2>err && + test_i18ngrep ! null err + ) +' + +test_expect_success 'commit rejects all-crud name' ' + test_must_fail env GIT_AUTHOR_NAME=" .;<>" \ + git commit --allow-empty -m foo +' + +# We must test the actual error message here, as an unwanted +# auto-detection could fail for other reasons. +test_expect_success 'empty configured name does not auto-detect' ' + ( + sane_unset GIT_AUTHOR_NAME && + test_must_fail \ + git -c user.name= commit --allow-empty -m foo 2>err && + test_i18ngrep "empty ident name" err + ) +' + +test_done diff --git a/t/t7810-grep.sh b/t/t7810-grep.sh index 19f0108f8a..cee42097b0 100755 --- a/t/t7810-grep.sh +++ b/t/t7810-grep.sh @@ -982,6 +982,72 @@ test_expect_success 'grep -e -- -- path' ' test_cmp expected actual ' +test_expect_success 'dashdash disambiguates rev as rev' ' + test_when_finished "rm -f master" && + echo content >master && + echo master:hello.c >expect && + git grep -l o master -- hello.c >actual && + test_cmp expect actual +' + +test_expect_success 'dashdash disambiguates pathspec as pathspec' ' + test_when_finished "git rm -f master" && + echo content >master && + git add master && + echo master:content >expect && + git grep o -- master >actual && + test_cmp expect actual +' + +test_expect_success 'report bogus arg without dashdash' ' + test_must_fail git grep o does-not-exist +' + +test_expect_success 'report bogus rev with dashdash' ' + test_must_fail git grep o hello.c -- +' + +test_expect_success 'allow non-existent path with dashdash' ' + # We need a real match so grep exits with success. + tree=$(git ls-tree HEAD | + sed s/hello.c/not-in-working-tree/ | + git mktree) && + git grep o "$tree" -- not-in-working-tree +' + +test_expect_success 'grep --no-index pattern -- path' ' + rm -fr non && + mkdir -p non/git && + ( + GIT_CEILING_DIRECTORIES="$(pwd)/non" && + export GIT_CEILING_DIRECTORIES && + cd non/git && + echo hello >hello && + echo goodbye >goodbye && + echo hello:hello >expect && + git grep --no-index o -- hello >actual && + test_cmp expect actual + ) +' + +test_expect_success 'grep --no-index complains of revs' ' + test_must_fail git grep --no-index o master -- 2>err && + test_i18ngrep "cannot be used with revs" err +' + +test_expect_success 'grep --no-index prefers paths to revs' ' + test_when_finished "rm -f master" && + echo content >master && + echo master:content >expect && + git grep --no-index o master >actual && + test_cmp expect actual +' + +test_expect_success 'grep --no-index does not "diagnose" revs' ' + test_must_fail git grep --no-index o :1:hello.c 2>err && + test_i18ngrep ! -i "did you mean" err +' + cat >expected <<EOF hello.c:int main(int argc, const char **argv) hello.c: printf("Hello world.\n"); diff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh index 0f398dd160..60a80f60b2 100755 --- a/t/t9001-send-email.sh +++ b/t/t9001-send-email.sh @@ -148,7 +148,6 @@ cat >expected-cc <<\EOF !two@example.com! !three@example.com! !four@example.com! -!five@example.com! EOF " @@ -159,9 +158,9 @@ test_expect_success $PREREQ 'cc trailer with various syntax' ' Test Cc: trailers. Cc: one@example.com - Cc: <two@example.com> # this is part of the name - Cc: <three@example.com>, <four@example.com> # not.five@example.com - Cc: "Some # Body" <five@example.com> [part.of.name.too] + Cc: <two@example.com> # trailing comments are ignored + Cc: <three@example.com>, <not.four@example.com> one address per line + Cc: "Some # Body" <four@example.com> [ <also.a.comment> ] EOF clean_fake_sendmail && git send-email -1 --to=recipient@example.com \ diff --git a/t/t9902-completion.sh b/t/t9902-completion.sh index a34e55f874..d711bef240 100755 --- a/t/t9902-completion.sh +++ b/t/t9902-completion.sh @@ -98,7 +98,7 @@ test_gitcomp () { local -a COMPREPLY && sed -e 's/Z$//' >expected && - cur="$1" && + local cur="$1" && shift && __gitcomp "$@" && print_comp && @@ -113,7 +113,7 @@ test_gitcomp_nl () { local -a COMPREPLY && sed -e 's/Z$//' >expected && - cur="$1" && + local cur="$1" && shift && __gitcomp_nl "$@" && print_comp && @@ -124,140 +124,280 @@ invalid_variable_name='${foo.bar}' actual="$TRASH_DIRECTORY/actual" -test_expect_success 'setup for __gitdir tests' ' +if test_have_prereq MINGW +then + ROOT="$(pwd -W)" +else + ROOT="$(pwd)" +fi + +test_expect_success 'setup for __git_find_repo_path/__gitdir tests' ' mkdir -p subdir/subsubdir && + mkdir -p non-repo && git init otherrepo ' -test_expect_success '__gitdir - from command line (through $__git_dir)' ' - echo "$TRASH_DIRECTORY/otherrepo/.git" >expected && +test_expect_success '__git_find_repo_path - from command line (through $__git_dir)' ' + echo "$ROOT/otherrepo/.git" >expected && ( - __git_dir="$TRASH_DIRECTORY/otherrepo/.git" && - __gitdir >"$actual" + __git_dir="$ROOT/otherrepo/.git" && + __git_find_repo_path && + echo "$__git_repo_path" >"$actual" ) && test_cmp expected "$actual" ' -test_expect_success '__gitdir - repo as argument' ' - echo "otherrepo/.git" >expected && - __gitdir "otherrepo" >"$actual" && - test_cmp expected "$actual" -' - -test_expect_success '__gitdir - remote as argument' ' - echo "remote" >expected && - __gitdir "remote" >"$actual" && - test_cmp expected "$actual" -' - -test_expect_success '__gitdir - .git directory in cwd' ' +test_expect_success '__git_find_repo_path - .git directory in cwd' ' echo ".git" >expected && - __gitdir >"$actual" && + ( + __git_find_repo_path && + echo "$__git_repo_path" >"$actual" + ) && test_cmp expected "$actual" ' -test_expect_success '__gitdir - .git directory in parent' ' - echo "$(pwd -P)/.git" >expected && +test_expect_success '__git_find_repo_path - .git directory in parent' ' + echo "$ROOT/.git" >expected && ( cd subdir/subsubdir && - __gitdir >"$actual" + __git_find_repo_path && + echo "$__git_repo_path" >"$actual" ) && test_cmp expected "$actual" ' -test_expect_success '__gitdir - cwd is a .git directory' ' +test_expect_success '__git_find_repo_path - cwd is a .git directory' ' echo "." >expected && ( cd .git && - __gitdir >"$actual" + __git_find_repo_path && + echo "$__git_repo_path" >"$actual" ) && test_cmp expected "$actual" ' -test_expect_success '__gitdir - parent is a .git directory' ' - echo "$(pwd -P)/.git" >expected && +test_expect_success '__git_find_repo_path - parent is a .git directory' ' + echo "$ROOT/.git" >expected && ( cd .git/refs/heads && - __gitdir >"$actual" + __git_find_repo_path && + echo "$__git_repo_path" >"$actual" ) && test_cmp expected "$actual" ' -test_expect_success '__gitdir - $GIT_DIR set while .git directory in cwd' ' - echo "$TRASH_DIRECTORY/otherrepo/.git" >expected && +test_expect_success '__git_find_repo_path - $GIT_DIR set while .git directory in cwd' ' + echo "$ROOT/otherrepo/.git" >expected && ( - GIT_DIR="$TRASH_DIRECTORY/otherrepo/.git" && + GIT_DIR="$ROOT/otherrepo/.git" && export GIT_DIR && - __gitdir >"$actual" + __git_find_repo_path && + echo "$__git_repo_path" >"$actual" ) && test_cmp expected "$actual" ' -test_expect_success '__gitdir - $GIT_DIR set while .git directory in parent' ' - echo "$TRASH_DIRECTORY/otherrepo/.git" >expected && +test_expect_success '__git_find_repo_path - $GIT_DIR set while .git directory in parent' ' + echo "$ROOT/otherrepo/.git" >expected && ( - GIT_DIR="$TRASH_DIRECTORY/otherrepo/.git" && + GIT_DIR="$ROOT/otherrepo/.git" && export GIT_DIR && cd subdir && - __gitdir >"$actual" + __git_find_repo_path && + echo "$__git_repo_path" >"$actual" + ) && + test_cmp expected "$actual" +' + +test_expect_success '__git_find_repo_path - from command line while "git -C"' ' + echo "$ROOT/.git" >expected && + ( + __git_dir="$ROOT/.git" && + __git_C_args=(-C otherrepo) && + __git_find_repo_path && + echo "$__git_repo_path" >"$actual" ) && test_cmp expected "$actual" ' -test_expect_success '__gitdir - non-existing $GIT_DIR' ' +test_expect_success '__git_find_repo_path - relative dir from command line and "git -C"' ' + echo "$ROOT/otherrepo/.git" >expected && ( - GIT_DIR="$TRASH_DIRECTORY/non-existing" && + cd subdir && + __git_dir="otherrepo/.git" && + __git_C_args=(-C ..) && + __git_find_repo_path && + echo "$__git_repo_path" >"$actual" + ) && + test_cmp expected "$actual" +' + +test_expect_success '__git_find_repo_path - $GIT_DIR set while "git -C"' ' + echo "$ROOT/.git" >expected && + ( + GIT_DIR="$ROOT/.git" && export GIT_DIR && - test_must_fail __gitdir - ) + __git_C_args=(-C otherrepo) && + __git_find_repo_path && + echo "$__git_repo_path" >"$actual" + ) && + test_cmp expected "$actual" ' -function pwd_P_W () { - if test_have_prereq MINGW - then - pwd -W - else - pwd -P - fi -} +test_expect_success '__git_find_repo_path - relative dir in $GIT_DIR and "git -C"' ' + echo "$ROOT/otherrepo/.git" >expected && + ( + cd subdir && + GIT_DIR="otherrepo/.git" && + export GIT_DIR && + __git_C_args=(-C ..) && + __git_find_repo_path && + echo "$__git_repo_path" >"$actual" + ) && + test_cmp expected "$actual" +' -test_expect_success '__gitdir - gitfile in cwd' ' - echo "$(pwd_P_W)/otherrepo/.git" >expected && - echo "gitdir: $(pwd_P_W)/otherrepo/.git" >subdir/.git && +test_expect_success '__git_find_repo_path - "git -C" while .git directory in cwd' ' + echo "$ROOT/otherrepo/.git" >expected && + ( + __git_C_args=(-C otherrepo) && + __git_find_repo_path && + echo "$__git_repo_path" >"$actual" + ) && + test_cmp expected "$actual" +' + +test_expect_success '__git_find_repo_path - "git -C" while cwd is a .git directory' ' + echo "$ROOT/otherrepo/.git" >expected && + ( + cd .git && + __git_C_args=(-C .. -C otherrepo) && + __git_find_repo_path && + echo "$__git_repo_path" >"$actual" + ) && + test_cmp expected "$actual" +' + +test_expect_success '__git_find_repo_path - "git -C" while .git directory in parent' ' + echo "$ROOT/otherrepo/.git" >expected && + ( + cd subdir && + __git_C_args=(-C .. -C otherrepo) && + __git_find_repo_path && + echo "$__git_repo_path" >"$actual" + ) && + test_cmp expected "$actual" +' + +test_expect_success '__git_find_repo_path - non-existing path in "git -C"' ' + ( + __git_C_args=(-C non-existing) && + test_must_fail __git_find_repo_path && + printf "$__git_repo_path" >"$actual" + ) && + test_must_be_empty "$actual" +' + +test_expect_success '__git_find_repo_path - non-existing path in $__git_dir' ' + ( + __git_dir="non-existing" && + test_must_fail __git_find_repo_path && + printf "$__git_repo_path" >"$actual" + ) && + test_must_be_empty "$actual" +' + +test_expect_success '__git_find_repo_path - non-existing $GIT_DIR' ' + ( + GIT_DIR="$ROOT/non-existing" && + export GIT_DIR && + test_must_fail __git_find_repo_path && + printf "$__git_repo_path" >"$actual" + ) && + test_must_be_empty "$actual" +' + +test_expect_success '__git_find_repo_path - gitfile in cwd' ' + echo "$ROOT/otherrepo/.git" >expected && + echo "gitdir: $ROOT/otherrepo/.git" >subdir/.git && test_when_finished "rm -f subdir/.git" && ( cd subdir && - __gitdir >"$actual" + __git_find_repo_path && + echo "$__git_repo_path" >"$actual" ) && test_cmp expected "$actual" ' -test_expect_success '__gitdir - gitfile in parent' ' - echo "$(pwd_P_W)/otherrepo/.git" >expected && - echo "gitdir: $(pwd_P_W)/otherrepo/.git" >subdir/.git && +test_expect_success '__git_find_repo_path - gitfile in parent' ' + echo "$ROOT/otherrepo/.git" >expected && + echo "gitdir: $ROOT/otherrepo/.git" >subdir/.git && test_when_finished "rm -f subdir/.git" && ( cd subdir/subsubdir && - __gitdir >"$actual" + __git_find_repo_path && + echo "$__git_repo_path" >"$actual" ) && test_cmp expected "$actual" ' -test_expect_success SYMLINKS '__gitdir - resulting path avoids symlinks' ' - echo "$(pwd -P)/otherrepo/.git" >expected && +test_expect_success SYMLINKS '__git_find_repo_path - resulting path avoids symlinks' ' + echo "$ROOT/otherrepo/.git" >expected && mkdir otherrepo/dir && test_when_finished "rm -rf otherrepo/dir" && ln -s otherrepo/dir link && test_when_finished "rm -f link" && ( cd link && + __git_find_repo_path && + echo "$__git_repo_path" >"$actual" + ) && + test_cmp expected "$actual" +' + +test_expect_success '__git_find_repo_path - not a git repository' ' + ( + cd non-repo && + GIT_CEILING_DIRECTORIES="$ROOT" && + export GIT_CEILING_DIRECTORIES && + test_must_fail __git_find_repo_path && + printf "$__git_repo_path" >"$actual" + ) && + test_must_be_empty "$actual" +' + +test_expect_success '__gitdir - finds repo' ' + echo "$ROOT/.git" >expected && + ( + cd subdir/subsubdir && __gitdir >"$actual" ) && test_cmp expected "$actual" ' -test_expect_success '__gitdir - not a git repository' ' - nongit test_must_fail __gitdir + +test_expect_success '__gitdir - returns error when cant find repo' ' + ( + __git_dir="non-existing" && + test_must_fail __gitdir >"$actual" + ) && + test_must_be_empty "$actual" +' + +test_expect_success '__gitdir - repo as argument' ' + echo "otherrepo/.git" >expected && + ( + __gitdir "otherrepo" >"$actual" + ) && + test_cmp expected "$actual" +' + +test_expect_success '__gitdir - remote as argument' ' + echo "remote" >expected && + ( + __gitdir "remote" >"$actual" + ) && + test_cmp expected "$actual" ' test_expect_success '__gitcomp - trailing space - options' ' @@ -361,10 +501,286 @@ test_expect_success '__git_remotes - list remotes from $GIT_DIR/remotes and from git remote add remote_in_config_1 git://remote_1 && test_when_finished "git remote remove remote_in_config_2" && git remote add remote_in_config_2 git://remote_2 && - __git_remotes >actual && + ( + __git_remotes >actual + ) && test_cmp expect actual ' +test_expect_success '__git_is_configured_remote' ' + test_when_finished "git remote remove remote_1" && + git remote add remote_1 git://remote_1 && + test_when_finished "git remote remove remote_2" && + git remote add remote_2 git://remote_2 && + ( + verbose __git_is_configured_remote remote_2 && + test_must_fail __git_is_configured_remote non-existent + ) +' + +test_expect_success 'setup for ref completion' ' + git commit --allow-empty -m initial && + git branch matching-branch && + git tag matching-tag && + ( + cd otherrepo && + git commit --allow-empty -m initial && + git branch -m master master-in-other && + git branch branch-in-other && + git tag tag-in-other + ) && + git remote add other "$ROOT/otherrepo/.git" && + git fetch --no-tags other && + rm -f .git/FETCH_HEAD && + git init thirdrepo +' + +test_expect_success '__git_refs - simple' ' + cat >expected <<-EOF && + HEAD + master + matching-branch + other/branch-in-other + other/master-in-other + matching-tag + EOF + ( + cur= && + __git_refs >"$actual" + ) && + test_cmp expected "$actual" +' + +test_expect_success '__git_refs - full refs' ' + cat >expected <<-EOF && + refs/heads/master + refs/heads/matching-branch + EOF + ( + cur=refs/heads/ && + __git_refs >"$actual" + ) && + test_cmp expected "$actual" +' + +test_expect_success '__git_refs - repo given on the command line' ' + cat >expected <<-EOF && + HEAD + branch-in-other + master-in-other + tag-in-other + EOF + ( + __git_dir="$ROOT/otherrepo/.git" && + cur= && + __git_refs >"$actual" + ) && + test_cmp expected "$actual" +' + +test_expect_success '__git_refs - remote on local file system' ' + cat >expected <<-EOF && + HEAD + branch-in-other + master-in-other + tag-in-other + EOF + ( + cur= && + __git_refs otherrepo >"$actual" + ) && + test_cmp expected "$actual" +' + +test_expect_success '__git_refs - remote on local file system - full refs' ' + cat >expected <<-EOF && + refs/heads/branch-in-other + refs/heads/master-in-other + refs/tags/tag-in-other + EOF + ( + cur=refs/ && + __git_refs otherrepo >"$actual" + ) && + test_cmp expected "$actual" +' + +test_expect_success '__git_refs - configured remote' ' + cat >expected <<-EOF && + HEAD + branch-in-other + master-in-other + EOF + ( + cur= && + __git_refs other >"$actual" + ) && + test_cmp expected "$actual" +' + +test_expect_success '__git_refs - configured remote - full refs' ' + cat >expected <<-EOF && + refs/heads/branch-in-other + refs/heads/master-in-other + refs/tags/tag-in-other + EOF + ( + cur=refs/ && + __git_refs other >"$actual" + ) && + test_cmp expected "$actual" +' + +test_expect_success '__git_refs - configured remote - repo given on the command line' ' + cat >expected <<-EOF && + HEAD + branch-in-other + master-in-other + EOF + ( + cd thirdrepo && + __git_dir="$ROOT/.git" && + cur= && + __git_refs other >"$actual" + ) && + test_cmp expected "$actual" +' + +test_expect_success '__git_refs - configured remote - full refs - repo given on the command line' ' + cat >expected <<-EOF && + refs/heads/branch-in-other + refs/heads/master-in-other + refs/tags/tag-in-other + EOF + ( + cd thirdrepo && + __git_dir="$ROOT/.git" && + cur=refs/ && + __git_refs other >"$actual" + ) && + test_cmp expected "$actual" +' + +test_expect_success '__git_refs - configured remote - remote name matches a directory' ' + cat >expected <<-EOF && + HEAD + branch-in-other + master-in-other + EOF + mkdir other && + test_when_finished "rm -rf other" && + ( + cur= && + __git_refs other >"$actual" + ) && + test_cmp expected "$actual" +' + +test_expect_success '__git_refs - URL remote' ' + cat >expected <<-EOF && + HEAD + branch-in-other + master-in-other + tag-in-other + EOF + ( + cur= && + __git_refs "file://$ROOT/otherrepo/.git" >"$actual" + ) && + test_cmp expected "$actual" +' + +test_expect_success '__git_refs - URL remote - full refs' ' + cat >expected <<-EOF && + refs/heads/branch-in-other + refs/heads/master-in-other + refs/tags/tag-in-other + EOF + ( + cur=refs/ && + __git_refs "file://$ROOT/otherrepo/.git" >"$actual" + ) && + test_cmp expected "$actual" +' + +test_expect_success '__git_refs - non-existing remote' ' + ( + cur= && + __git_refs non-existing >"$actual" + ) && + test_must_be_empty "$actual" +' + +test_expect_success '__git_refs - non-existing remote - full refs' ' + ( + cur=refs/ && + __git_refs non-existing >"$actual" + ) && + test_must_be_empty "$actual" +' + +test_expect_success '__git_refs - non-existing URL remote' ' + ( + cur= && + __git_refs "file://$ROOT/non-existing" >"$actual" + ) && + test_must_be_empty "$actual" +' + +test_expect_success '__git_refs - non-existing URL remote - full refs' ' + ( + cur=refs/ && + __git_refs "file://$ROOT/non-existing" >"$actual" + ) && + test_must_be_empty "$actual" +' + +test_expect_success '__git_refs - not in a git repository' ' + ( + GIT_CEILING_DIRECTORIES="$ROOT" && + export GIT_CEILING_DIRECTORIES && + cd subdir && + cur= && + __git_refs >"$actual" + ) && + test_must_be_empty "$actual" +' + +test_expect_success '__git_refs - unique remote branches for git checkout DWIMery' ' + cat >expected <<-EOF && + HEAD + master + matching-branch + other/ambiguous + other/branch-in-other + other/master-in-other + remote/ambiguous + remote/branch-in-remote + matching-tag + branch-in-other + branch-in-remote + master-in-other + EOF + for remote_ref in refs/remotes/other/ambiguous \ + refs/remotes/remote/ambiguous \ + refs/remotes/remote/branch-in-remote + do + git update-ref $remote_ref master && + test_when_finished "git update-ref -d $remote_ref" + done && + ( + cur= && + __git_refs "" 1 >"$actual" + ) && + test_cmp expected "$actual" +' + +test_expect_success 'teardown after ref completion' ' + git branch -d matching-branch && + git tag -d matching-tag && + git remote remove other +' + test_expect_success '__git_get_config_variables' ' cat >expect <<-EOF && name-1 @@ -475,7 +891,12 @@ test_expect_success 'general options plus command' ' test_completion "git --namespace=foo check" "checkout " && test_completion "git --paginate check" "checkout " && test_completion "git --info-path check" "checkout " && - test_completion "git --no-replace-objects check" "checkout " + test_completion "git --no-replace-objects check" "checkout " && + test_completion "git --git-dir some/path check" "checkout " && + test_completion "git -c conf.var=value check" "checkout " && + test_completion "git -C some/path check" "checkout " && + test_completion "git --work-tree some/path check" "checkout " && + test_completion "git --namespace name/space check" "checkout " ' test_expect_success 'git --help completion' ' @@ -483,10 +904,10 @@ test_expect_success 'git --help completion' ' test_completion "git --help core" "core-tutorial " ' -test_expect_success 'setup for ref completion' ' +test_expect_success 'setup for integration tests' ' echo content >file1 && echo more >file2 && - git add . && + git add file1 file2 && git commit -m one && git branch mybranch && git tag mytag @@ -500,6 +921,12 @@ test_expect_success 'checkout completes ref names' ' EOF ' +test_expect_success 'git -C <path> checkout uses the right repo' ' + test_completion "git -C subdir -C subsubdir -C .. -C ../otherrepo checkout b" <<-\EOF + branch-in-other Z + EOF +' + test_expect_success 'show completes all refs' ' test_completion "git show m" <<-\EOF master Z @@ -517,7 +944,7 @@ test_expect_success '<ref>: completes paths' ' test_expect_success 'complete tree filename with spaces' ' echo content >"name with spaces" && - git add . && + git add "name with spaces" && git commit -m spaces && test_completion "git show HEAD:nam" <<-\EOF name with spaces Z @@ -526,7 +953,7 @@ test_expect_success 'complete tree filename with spaces' ' test_expect_success 'complete tree filename with metacharacters' ' echo content >"name with \${meta}" && - git add . && + git add "name with \${meta}" && git commit -m meta && test_completion "git show HEAD:nam" <<-\EOF name with ${meta} Z diff --git a/tempfile.c b/tempfile.c index ffcc272375..6843710670 100644 --- a/tempfile.c +++ b/tempfile.c @@ -247,8 +247,13 @@ int close_tempfile(struct tempfile *tempfile) tempfile->fd = -1; if (fp) { tempfile->fp = NULL; - err = ferror(fp); - err |= fclose(fp); + if (ferror(fp)) { + err = -1; + if (!fclose(fp)) + errno = EIO; + } else { + err = fclose(fp); + } } else { err = close(fd); } diff --git a/transport-helper.c b/transport-helper.c index 1258d6aedd..dc90a1fb76 100644 --- a/transport-helper.c +++ b/transport-helper.c @@ -124,8 +124,9 @@ static struct child_process *get_helper(struct transport *transport) helper->git_cmd = 0; helper->silent_exec_failure = 1; - argv_array_pushf(&helper->env_array, "%s=%s", GIT_DIR_ENVIRONMENT, - get_git_dir()); + if (have_git_dir()) + argv_array_pushf(&helper->env_array, "%s=%s", + GIT_DIR_ENVIRONMENT, get_git_dir()); code = start_command(helper); if (code < 0 && errno == ENOENT) diff --git a/transport.c b/transport.c index d72e089484..5828e06afd 100644 --- a/transport.c +++ b/transport.c @@ -299,7 +299,7 @@ void transport_update_tracking_ref(struct remote *remote, struct ref *ref, int v if (verbose) fprintf(stderr, "updating local tracking ref '%s'\n", rs.dst); if (ref->deletion) { - delete_ref(rs.dst, NULL, 0); + delete_ref(NULL, rs.dst, NULL, 0); } else update_ref("update by push", rs.dst, ref->new_oid.hash, NULL, 0, 0); @@ -1206,6 +1206,42 @@ literal_copy: return xstrdup(url); } +static void read_alternate_refs(const char *path, + alternate_ref_fn *cb, + void *data) +{ + struct child_process cmd = CHILD_PROCESS_INIT; + struct strbuf line = STRBUF_INIT; + FILE *fh; + + cmd.git_cmd = 1; + argv_array_pushf(&cmd.args, "--git-dir=%s", path); + argv_array_push(&cmd.args, "for-each-ref"); + argv_array_push(&cmd.args, "--format=%(objectname) %(refname)"); + cmd.env = local_repo_env; + cmd.out = -1; + + if (start_command(&cmd)) + return; + + fh = xfdopen(cmd.out, "r"); + while (strbuf_getline_lf(&line, fh) != EOF) { + struct object_id oid; + + if (get_oid_hex(line.buf, &oid) || + line.buf[GIT_SHA1_HEXSZ] != ' ') { + warning("invalid line while parsing alternate refs: %s", + line.buf); + break; + } + + cb(line.buf + GIT_SHA1_HEXSZ + 1, &oid, data); + } + + fclose(fh); + finish_command(&cmd); +} + struct alternate_refs_data { alternate_ref_fn *fn; void *data; @@ -1214,34 +1250,26 @@ struct alternate_refs_data { static int refs_from_alternate_cb(struct alternate_object_database *e, void *data) { - char *other; - size_t len; - struct remote *remote; - struct transport *transport; - const struct ref *extra; + struct strbuf path = STRBUF_INIT; + size_t base_len; struct alternate_refs_data *cb = data; - other = real_pathdup(e->path); - len = strlen(other); - - while (other[len-1] == '/') - other[--len] = '\0'; - if (len < 8 || memcmp(other + len - 8, "/objects", 8)) + if (!strbuf_realpath(&path, e->path, 0)) goto out; + if (!strbuf_strip_suffix(&path, "/objects")) + goto out; + base_len = path.len; + /* Is this a git repository with refs? */ - memcpy(other + len - 8, "/refs", 6); - if (!is_directory(other)) + strbuf_addstr(&path, "/refs"); + if (!is_directory(path.buf)) goto out; - other[len - 8] = '\0'; - remote = remote_get(other); - transport = transport_get(remote, other); - for (extra = transport_get_remote_refs(transport); - extra; - extra = extra->next) - cb->fn(extra, cb->data); - transport_disconnect(transport); + strbuf_setlen(&path, base_len); + + read_alternate_refs(path.buf, cb->fn, cb->data); + out: - free(other); + strbuf_release(&path); return 0; } diff --git a/transport.h b/transport.h index e597b31b38..bc5571574b 100644 --- a/transport.h +++ b/transport.h @@ -255,6 +255,6 @@ int transport_refs_pushed(struct ref *ref); void transport_print_push_status(const char *dest, struct ref *refs, int verbose, int porcelain, unsigned int *reject_reasons); -typedef void alternate_ref_fn(const struct ref *, void *); +typedef void alternate_ref_fn(const char *refname, const struct object_id *oid, void *); extern void for_each_alternate_ref(alternate_ref_fn, void *); #endif diff --git a/upload-pack.c b/upload-pack.c index 7597ba3405..ffb028d623 100644 --- a/upload-pack.c +++ b/upload-pack.c @@ -822,9 +822,13 @@ static void receive_needs(void) use_include_tag = 1; o = parse_object(sha1_buf); - if (!o) + if (!o) { + packet_write_fmt(1, + "ERR upload-pack: not our ref %s", + sha1_to_hex(sha1_buf)); die("git upload-pack: not our ref %s", sha1_to_hex(sha1_buf)); + } if (!(o->flags & WANTED)) { o->flags |= WANTED; if (!((allow_unadvertised_object_request & ALLOW_ANY_SHA1) == ALLOW_ANY_SHA1 diff --git a/urlmatch.c b/urlmatch.c index 132d342bc1..4bbde924e8 100644 --- a/urlmatch.c +++ b/urlmatch.c @@ -63,7 +63,50 @@ static int append_normalized_escapes(struct strbuf *buf, return 1; } -char *url_normalize(const char *url, struct url_info *out_info) +static const char *end_of_token(const char *s, int c, size_t n) +{ + const char *next = memchr(s, c, n); + if (!next) + next = s + n; + return next; +} + +static int match_host(const struct url_info *url_info, + const struct url_info *pattern_info) +{ + const char *url = url_info->url + url_info->host_off; + const char *pat = pattern_info->url + pattern_info->host_off; + int url_len = url_info->host_len; + int pat_len = pattern_info->host_len; + + while (url_len && pat_len) { + const char *url_next = end_of_token(url, '.', url_len); + const char *pat_next = end_of_token(pat, '.', pat_len); + + if (pat_next == pat + 1 && pat[0] == '*') + /* wildcard matches anything */ + ; + else if ((pat_next - pat) == (url_next - url) && + !memcmp(url, pat, url_next - url)) + /* the components are the same */ + ; + else + return 0; /* found an unmatch */ + + if (url_next < url + url_len) + url_next++; + url_len -= url_next - url; + url = url_next; + if (pat_next < pat + pat_len) + pat_next++; + pat_len -= pat_next - pat; + pat = pat_next; + } + + return (!url_len && !pat_len); +} + +static char *url_normalize_1(const char *url, struct url_info *out_info, char allow_globs) { /* * Normalize NUL-terminated url using the following rules: @@ -104,7 +147,7 @@ char *url_normalize(const char *url, struct url_info *out_info) struct strbuf norm; size_t spanned; size_t scheme_len, user_off=0, user_len=0, passwd_off=0, passwd_len=0; - size_t host_off=0, host_len=0, port_len=0, path_off, path_len, result_len; + size_t host_off=0, host_len=0, port_off=0, port_len=0, path_off, path_len, result_len; const char *slash_ptr, *at_ptr, *colon_ptr, *path_start; char *result; @@ -191,7 +234,12 @@ char *url_normalize(const char *url, struct url_info *out_info) strbuf_release(&norm); return NULL; } - spanned = strspn(url, URL_HOST_CHARS); + + if (allow_globs) + spanned = strspn(url, URL_HOST_CHARS "*"); + else + spanned = strspn(url, URL_HOST_CHARS); + if (spanned < colon_ptr - url) { /* Host name has invalid characters */ if (out_info) { @@ -258,6 +306,7 @@ char *url_normalize(const char *url, struct url_info *out_info) return NULL; } strbuf_addch(&norm, ':'); + port_off = norm.len; strbuf_add(&norm, url, slash_ptr - url); port_len = slash_ptr - url; } @@ -265,7 +314,7 @@ char *url_normalize(const char *url, struct url_info *out_info) url = slash_ptr; } if (host_off) - host_len = norm.len - host_off; + host_len = norm.len - host_off - (port_len ? port_len + 1 : 0); /* @@ -373,6 +422,7 @@ char *url_normalize(const char *url, struct url_info *out_info) out_info->passwd_len = passwd_len; out_info->host_off = host_off; out_info->host_len = host_len; + out_info->port_off = port_off; out_info->port_len = port_len; out_info->path_off = path_off; out_info->path_len = path_len; @@ -380,6 +430,11 @@ char *url_normalize(const char *url, struct url_info *out_info) return result; } +char *url_normalize(const char *url, struct url_info *out_info) +{ + return url_normalize_1(url, out_info, 0); +} + static size_t url_match_prefix(const char *url, const char *url_prefix, size_t url_prefix_len) @@ -414,7 +469,7 @@ static size_t url_match_prefix(const char *url, static int match_urls(const struct url_info *url, const struct url_info *url_prefix, - int *exactusermatch) + struct urlmatch_item *match) { /* * url_prefix matches url if the scheme, host and port of url_prefix @@ -433,8 +488,8 @@ static int match_urls(const struct url_info *url, * contained a user name or false if url_prefix did not have a * user name. If there is no match *exactusermatch is left untouched. */ - int usermatched = 0; - int pathmatchlen; + char usermatched = 0; + size_t pathmatchlen; if (!url || !url_prefix || !url->url || !url_prefix->url) return 0; @@ -454,33 +509,53 @@ static int match_urls(const struct url_info *url, usermatched = 1; } - /* check the host and port */ - if (url_prefix->host_len != url->host_len || - strncmp(url->url + url->host_off, - url_prefix->url + url_prefix->host_off, url->host_len)) - return 0; /* host names and/or ports do not match */ + /* check the host */ + if (!match_host(url, url_prefix)) + return 0; /* host names do not match */ + + /* check the port */ + if (url_prefix->port_len != url->port_len || + strncmp(url->url + url->port_off, + url_prefix->url + url_prefix->port_off, url->port_len)) + return 0; /* ports do not match */ /* check the path */ pathmatchlen = url_match_prefix( url->url + url->path_off, url_prefix->url + url_prefix->path_off, url_prefix->url_len - url_prefix->path_off); + if (!pathmatchlen) + return 0; /* paths do not match */ - if (pathmatchlen && exactusermatch) - *exactusermatch = usermatched; - return pathmatchlen; + if (match) { + match->hostmatch_len = url_prefix->host_len; + match->pathmatch_len = pathmatchlen; + match->user_matched = usermatched; + } + + return 1; +} + +static int cmp_matches(const struct urlmatch_item *a, + const struct urlmatch_item *b) +{ + if (a->hostmatch_len != b->hostmatch_len) + return a->hostmatch_len < b->hostmatch_len ? -1 : 1; + if (a->pathmatch_len != b->pathmatch_len) + return a->pathmatch_len < b->pathmatch_len ? -1 : 1; + if (a->user_matched != b->user_matched) + return b->user_matched ? -1 : 1; + return 0; } int urlmatch_config_entry(const char *var, const char *value, void *cb) { struct string_list_item *item; struct urlmatch_config *collect = cb; - struct urlmatch_item *matched; + struct urlmatch_item matched = {0}; struct url_info *url = &collect->url; const char *key, *dot; struct strbuf synthkey = STRBUF_INIT; - size_t matched_len = 0; - int user_matched = 0; int retval; if (!skip_prefix(var, collect->section, &key) || *(key++) != '.') { @@ -494,13 +569,13 @@ int urlmatch_config_entry(const char *var, const char *value, void *cb) struct url_info norm_info; config_url = xmemdupz(key, dot - key); - norm_url = url_normalize(config_url, &norm_info); + norm_url = url_normalize_1(config_url, &norm_info, 1); free(config_url); if (!norm_url) return 0; - matched_len = match_urls(url, &norm_info, &user_matched); + retval = match_urls(url, &norm_info, &matched); free(norm_url); - if (!matched_len) + if (!retval) return 0; key = dot + 1; } @@ -510,24 +585,18 @@ int urlmatch_config_entry(const char *var, const char *value, void *cb) item = string_list_insert(&collect->vars, key); if (!item->util) { - matched = xcalloc(1, sizeof(*matched)); - item->util = matched; + item->util = xcalloc(1, sizeof(matched)); } else { - matched = item->util; - /* - * Is our match shorter? Is our match the same - * length, and without user while the current - * candidate is with user? Then we cannot use it. - */ - if (matched_len < matched->matched_len || - ((matched_len == matched->matched_len) && - (!user_matched && matched->user_matched))) + if (cmp_matches(&matched, item->util) < 0) + /* + * Our match is worse than the old one, + * we cannot use it. + */ return 0; /* Otherwise, replace it with this one. */ } - matched->matched_len = matched_len; - matched->user_matched = user_matched; + memcpy(item->util, &matched, sizeof(matched)); strbuf_addstr(&synthkey, collect->section); strbuf_addch(&synthkey, '.'); strbuf_addstr(&synthkey, key); diff --git a/urlmatch.h b/urlmatch.h index 528862adc5..37ee5da85e 100644 --- a/urlmatch.h +++ b/urlmatch.h @@ -18,11 +18,12 @@ struct url_info { size_t passwd_len; /* length of passwd; if passwd_off != 0 but passwd_len == 0, an empty passwd was given */ size_t host_off; /* offset into url to start of host name (0 => none) */ - size_t host_len; /* length of host name; this INCLUDES any ':portnum'; + size_t host_len; /* length of host name; * file urls may have host_len == 0 */ - size_t port_len; /* if a portnum is present (port_len != 0), it has - * this length (excluding the leading ':') at the - * end of the host name (always 0 for file urls) */ + size_t port_off; /* offset into url to start of port number (0 => none) */ + size_t port_len; /* if a portnum is present (port_off != 0), it has + * this length (excluding the leading ':') starting + * from port_off (always 0 for file urls) */ size_t path_off; /* offset into url to the start of the url path; * this will always point to a '/' character * after the url has been normalized */ @@ -33,7 +34,8 @@ struct url_info { extern char *url_normalize(const char *, struct url_info *); struct urlmatch_item { - size_t matched_len; + size_t hostmatch_len; + size_t pathmatch_len; char user_matched; }; diff --git a/userdiff.c b/userdiff.c index 2125d6da26..8b732e40bc 100644 --- a/userdiff.c +++ b/userdiff.c @@ -262,25 +262,22 @@ struct userdiff_driver *userdiff_find_by_name(const char *name) { struct userdiff_driver *userdiff_find_by_path(const char *path) { - static struct git_attr *attr; - struct git_attr_check check; - - if (!attr) - attr = git_attr("diff"); - check.attr = attr; + static struct attr_check *check; + if (!check) + check = attr_check_initl("diff", NULL); if (!path) return NULL; - if (git_check_attr(path, 1, &check)) + if (git_check_attr(path, check)) return NULL; - if (ATTR_TRUE(check.value)) + if (ATTR_TRUE(check->items[0].value)) return &driver_true; - if (ATTR_FALSE(check.value)) + if (ATTR_FALSE(check->items[0].value)) return &driver_false; - if (ATTR_UNSET(check.value)) + if (ATTR_UNSET(check->items[0].value)) return NULL; - return userdiff_find_by_name(check.value); + return userdiff_find_by_name(check->items[0].value); } struct userdiff_driver *userdiff_get_textconv(struct userdiff_driver *driver) diff --git a/worktree.c b/worktree.c index d633761575..d7b911aac7 100644 --- a/worktree.c +++ b/worktree.c @@ -175,7 +175,7 @@ struct worktree **get_worktrees(unsigned flags) struct dirent *d; int counter = 0, alloc = 2; - list = xmalloc(alloc * sizeof(struct worktree *)); + ALLOC_ARRAY(list, alloc); list[counter++] = get_main_worktree(); @@ -440,23 +440,6 @@ int xmkstemp(char *template) return fd; } -/* git_mkstemp() - create tmp file honoring TMPDIR variable */ -int git_mkstemp(char *path, size_t len, const char *template) -{ - const char *tmp; - size_t n; - - tmp = getenv("TMPDIR"); - if (!tmp) - tmp = "/tmp"; - n = snprintf(path, len, "%s/%s", tmp, template); - if (len <= n) { - errno = ENAMETOOLONG; - return -1; - } - return mkstemp(path); -} - /* Adapted from libiberty's mkstemp.c. */ #undef TMP_MAX @@ -531,13 +514,6 @@ int git_mkstemp_mode(char *pattern, int mode) return git_mkstemps_mode(pattern, 0, mode); } -#ifdef NO_MKSTEMPS -int gitmkstemps(char *pattern, int suffix_len) -{ - return git_mkstemps_mode(pattern, suffix_len, 0600); -} -#endif - int xmkstemp_mode(char *template, int mode) { int fd; @@ -71,24 +71,17 @@ unsigned parse_whitespace_rule(const char *string) return rule; } -static void setup_whitespace_attr_check(struct git_attr_check *check) -{ - static struct git_attr *attr_whitespace; - - if (!attr_whitespace) - attr_whitespace = git_attr("whitespace"); - check[0].attr = attr_whitespace; -} - unsigned whitespace_rule(const char *pathname) { - struct git_attr_check attr_whitespace_rule; + static struct attr_check *attr_whitespace_rule; + + if (!attr_whitespace_rule) + attr_whitespace_rule = attr_check_initl("whitespace", NULL); - setup_whitespace_attr_check(&attr_whitespace_rule); - if (!git_check_attr(pathname, 1, &attr_whitespace_rule)) { + if (!git_check_attr(pathname, attr_whitespace_rule)) { const char *value; - value = attr_whitespace_rule.value; + value = attr_whitespace_rule->items[0].value; if (ATTR_TRUE(value)) { /* true (whitespace) */ unsigned all_rule = ws_tab_width(whitespace_rule_cfg); diff --git a/xdiff/xemit.c b/xdiff/xemit.c index 7389ce4102..8c88dbde38 100644 --- a/xdiff/xemit.c +++ b/xdiff/xemit.c @@ -183,16 +183,14 @@ int xdl_emit_diff(xdfenv_t *xe, xdchange_t *xscr, xdemitcb_t *ecb, /* * We don't need additional context if - * a whole function was added, possibly - * starting with empty lines. + * a whole function was added. */ - while (i2 < xe->xdf2.nrec && - is_empty_rec(&xe->xdf2, i2)) + while (i2 < xe->xdf2.nrec) { + if (match_func_rec(&xe->xdf2, xecfg, i2, + dummy, sizeof(dummy)) >= 0) + goto post_context_calculation; i2++; - if (i2 < xe->xdf2.nrec && - match_func_rec(&xe->xdf2, xecfg, i2, - dummy, sizeof(dummy)) >= 0) - goto post_context_calculation; + } /* * Otherwise get more context from the |