diff options
79 files changed, 3559 insertions, 419 deletions
diff --git a/.gitignore b/.gitignore index a05241916c..422c5382c1 100644 --- a/.gitignore +++ b/.gitignore @@ -184,6 +184,7 @@ /test-delta /test-dump-cache-tree /test-dump-split-index +/test-dump-untracked-cache /test-scrap-cache-tree /test-genrandom /test-hashmap diff --git a/Documentation/RelNotes/2.4.2.txt b/Documentation/RelNotes/2.4.2.txt new file mode 100644 index 0000000000..250cdc423c --- /dev/null +++ b/Documentation/RelNotes/2.4.2.txt @@ -0,0 +1,45 @@ +Git v2.4.2 Release Notes +======================== + +Fixes since v2.4.1 +------------------ + + * "git rev-list --objects $old --not --all" to see if everything that + is reachable from $old is already connected to the existing refs + was very inefficient. + + * "hash-object --literally" introduced in v2.2 was not prepared to + take a really long object type name. + + * "git rebase --quiet" was not quite quiet when there is nothing to + do. + + * The completion for "log --decorate=" parameter value was incorrect. + + * "filter-branch" corrupted commit log message that ends with an + incomplete line on platforms with some "sed" implementations that + munge such a line. Work it around by avoiding to use "sed". + + * "git daemon" fails to build from the source under NO_IPV6 + configuration (regression in 2.4). + + * "git stash pop/apply" forgot to make sure that not just the working + tree is clean but also the index is clean. The latter is important + as a stash application can conflict and the index will be used for + conflict resolution. + + * We have prepended $GIT_EXEC_PATH and the path "git" is installed in + (typically "/usr/bin") to $PATH when invoking subprograms and hooks + for almost eternity, but the original use case the latter tried to + support was semi-bogus (i.e. install git to /opt/foo/git and run it + without having /opt/foo on $PATH), and more importantly it has + become less and less relevant as Git grew more mainstream (i.e. the + users would _want_ to have it on their $PATH). Stop prepending the + path in which "git" is installed to users' $PATH, as that would + interfere the command search order people depend on (e.g. they may + not like versions of programs that are unrelated to Git in /usr/bin + and want to override them by having different ones in /usr/local/bin + and have the latter directory earlier in their $PATH). + +Also contains typofixes, documentation updates and trivial code +clean-ups. diff --git a/Documentation/RelNotes/2.5.0.txt b/Documentation/RelNotes/2.5.0.txt index b102837bb0..0b7fe2af33 100644 --- a/Documentation/RelNotes/2.5.0.txt +++ b/Documentation/RelNotes/2.5.0.txt @@ -9,6 +9,9 @@ Ports UI, Workflows & Features + * List of commands shown by "git help" are grouped along the workflow + elements to help early learners. + * "git p4" now detects the filetype (e.g. binary) correctly even when the files are opened exclusively. @@ -79,12 +82,30 @@ UI, Workflows & Features * The Git subcommand completion (in contrib/) listed credential helpers among candidates, which is not something the end user would - invoke interatively. + invoke interactively. + + * The index file can be taught with "update-index --untracked-cache" + to optionally remember already seen untracked files, in order to + speed up "git status" in a working tree with tons of cruft. + + * "git mergetool" learned to drive WinMerge as a backend. + + * "git upload-pack" that serves "git fetch" can be told to serve + commits that are not at the tip of any ref, as long as they are + reachable from a ref, with uploadpack.allowReachableSHA1InWant + configuration variable. + + * "git cat-file --batch(-check)" learned the "--follow-symlinks" + option that follows an in-tree symbolic link when asked about an + object via extended SHA-1 syntax, e.g. HEAD:RelNotes that points at + Documentation/RelNotes/2.5.0.txt. With the new option, the command + behaves as if HEAD:Documentation/RelNotes/2.5.0.txt was given as + input instead. Performance, Internal Implementation, Development Support etc. - * "unsigned char [20]" used thoughout the code to represent object + * "unsigned char [20]" used throughout the code to represent object names are being converted into a semi-opaque "struct object_id". This effort is expected to interfere with other topics in flight, but hopefully will give us one extra level of abstraction in the @@ -180,9 +201,9 @@ notes for details). * Some time ago, "git blame" (incorrectly) lost the convert_to_git() call when synthesizing a fake "tip" commit that represents the state in the working tree, which broke folks who record the history - with LF line ending to make their project portabile across - platforms while terminating lines in their working tree files with - CRLF for their platform. + with LF line ending to make their project portable across platforms + while terminating lines in their working tree files with CRLF for + their platform. (merge 4bf256d tb/blame-resurrect-convert-to-git later to maint). * We avoid setting core.worktree when the repository location is the @@ -208,7 +229,7 @@ notes for details). (merge 27547e5 cn/bom-in-gitignore later to maint). * a few helper scripts in the test suite did not report errors - correcty. + correctly. (merge de248e9 ep/fix-test-lib-functions-report later to maint). * The default $HOME/.gitconfig file created upon "git config --global" @@ -308,10 +329,57 @@ notes for details). (merge ad3967a jk/stripspace-asciidoctor-fix later to maint). (merge 975e382 ja/tutorial-asciidoctor-fix later to maint). + * The code to read pack-bitmap wanted to allocate a few hundred + pointers to a structure, but by mistake allocated and leaked memory + enough to hold that many actual structures. Correct the allocation + size and also have it on stack, as it is small enough. + (merge 599dc76 rs/plug-leak-in-pack-bitmaps later to maint). + + * The pull.ff configuration was supposed to override the merge.ff + configuration, but it didn't. + (merge db9bb28 pt/pull-ff-vs-merge-ff later to maint). + + * "git pull --log" and "git pull --no-log" worked as expected, but + "git pull --log=20" did not. + (merge 5061a44 pt/pull-log-n later to maint). + + * "git rerere forget" in a repository without rerere enabled gave a + cryptic error message; it should be a silent no-op instead. + (merge 0544574 jk/rerere-forget-check-enabled later to maint). + + * "git rebase -i" fired post-rewrite hook when it shouldn't (namely, + when it was told to stop sequencing with 'exec' insn). + (merge 141ff8f mm/rebase-i-post-rewrite-exec later to maint). + + * Clarify that "log --raw" and "log --format=raw" are unrelated + concepts. + (merge 92de921 mm/log-format-raw-doc later to maint). + + * Make "git stash something --help" error out, so that users can + safely say "git stash drop --help". + (merge 5ba2831 jk/stash-options later to maint). + + * The clean/smudge interface did not work well when filtering an + empty contents (failed and then passed the empty input through). + It can be argued that a filter that produces anything but empty for + an empty input is nonsense, but if the user wants to do strange + things, then why not? + (merge f6a1e1e jh/filter-empty-contents later to maint). + + * Communication between the HTTP server and http_backend process can + lead to a dead-lock when relaying a large ref negotiation request. + Diagnose the situation better, and mitigate it by reading such a + request first into core (to a reasonable limit). + (merge 636614f jk/http-backend-deadlock later to maint). + + * "git clean pathspec..." tried to lstat(2) and complain even for + paths outside the given pathspec. + (merge 838d6a9 dt/clean-pathspec-filter-then-lstat later to maint). + * Code cleanups and documentation updates. (merge 0269f96 mm/usage-log-l-can-take-regex later to maint). (merge 64f2589 nd/t1509-chroot-test later to maint). - (merge f86a374 sb/test-bitmap-free-at-end later to maint). + (merge d201a1e sb/test-bitmap-free-at-end later to maint). (merge 05bfc7d sb/line-log-plug-pairdiff-leak later to maint). (merge 846e5df pt/xdg-config-path later to maint). (merge 1154aa4 jc/plug-fmt-merge-msg-leak later to maint). @@ -320,3 +388,6 @@ notes for details). (merge bbf431c ps/doc-packfile-vs-pack-file later to maint). (merge 309a9e3 jk/skip-http-tests-under-no-curl later to maint). (merge ccd593c dl/branch-error-message later to maint). + (merge 22570b6 rs/janitorial later to maint). + (merge 5c2a581 mc/commit-doc-grammofix later to maint). + (merge ce41720 ah/usage-strings later to maint). diff --git a/Documentation/cmd-list.perl b/Documentation/cmd-list.perl index 04f99778d8..5aa73cfe45 100755 --- a/Documentation/cmd-list.perl +++ b/Documentation/cmd-list.perl @@ -38,6 +38,10 @@ sub format_one { } } +while (<>) { + last if /^### command list/; +} + my %cmds = (); for (sort <>) { next if /^#/; diff --git a/Documentation/config.txt b/Documentation/config.txt index 0f668bbe11..4d21ce1647 100644 --- a/Documentation/config.txt +++ b/Documentation/config.txt @@ -2061,7 +2061,7 @@ pull.ff:: a case (equivalent to giving the `--no-ff` option from the command line). When set to `only`, only such fast-forward merges are allowed (equivalent to giving the `--ff-only` option from the - command line). + command line). This setting overrides `merge.ff` when pulling. pull.rebase:: When true, rebase branches on top of the fetched branch, instead @@ -2558,14 +2558,20 @@ uploadpack.hideRefs:: are under the hierarchies listed on the value of this variable is excluded, and is hidden from `git ls-remote`, `git fetch`, etc. An attempt to fetch a hidden ref by `git - fetch` will fail. See also `uploadpack.allowtipsha1inwant`. + fetch` will fail. See also `uploadpack.allowTipSHA1InWant`. -uploadpack.allowtipsha1inwant:: +uploadpack.allowTipSHA1InWant:: When `uploadpack.hideRefs` is in effect, allow `upload-pack` to accept a fetch request that asks for an object at the tip of a hidden ref (by default, such a request is rejected). see also `uploadpack.hideRefs`. +uploadpack.allowReachableSHA1InWant:: + Allow `upload-pack` to accept a fetch request that asks for an + object that is reachable from any ref tip. However, note that + calculating object reachability is computationally expensive. + Defaults to `false`. + uploadpack.keepAlive:: When `upload-pack` has started `pack-objects`, there may be a quiet period while `pack-objects` prepares the pack. Normally diff --git a/Documentation/diff-options.txt b/Documentation/diff-options.txt index b7c3afeb3a..3ad6404dbc 100644 --- a/Documentation/diff-options.txt +++ b/Documentation/diff-options.txt @@ -43,10 +43,19 @@ endif::git-format-patch[] ifndef::git-format-patch[] --raw:: - Generate the raw format. +ifndef::git-log[] + Generate the diff in raw format. ifdef::git-diff-core[] This is the default. endif::git-diff-core[] +endif::git-log[] +ifdef::git-log[] + For each commit, show a summary of changes using the raw diff + format. See the "RAW OUTPUT FORMAT" section of + linkgit:git-diff[1]. This is different from showing the log + itself in raw format, which you can achieve with + `--format=raw`. +endif::git-log[] endif::git-format-patch[] ifndef::git-format-patch[] diff --git a/Documentation/git-cat-file.txt b/Documentation/git-cat-file.txt index 499ae7b98a..319ab4cb08 100644 --- a/Documentation/git-cat-file.txt +++ b/Documentation/git-cat-file.txt @@ -10,7 +10,7 @@ SYNOPSIS -------- [verse] 'git cat-file' (-t [--allow-unknown-type]| -s [--allow-unknown-type]| -e | -p | <type> | --textconv ) <object> -'git cat-file' (--batch | --batch-check) < <list-of-objects> +'git cat-file' (--batch | --batch-check) [--follow-symlinks] < <list-of-objects> DESCRIPTION ----------- @@ -72,6 +72,62 @@ OPTIONS --allow-unknown-type:: Allow -s or -t to query broken/corrupt objects of unknown type. +--follow-symlinks:: + With --batch or --batch-check, follow symlinks inside the + repository when requesting objects with extended SHA-1 + expressions of the form tree-ish:path-in-tree. Instead of + providing output about the link itself, provide output about + the linked-to object. If a symlink points outside the + tree-ish (e.g. a link to /foo or a root-level link to ../foo), + the portion of the link which is outside the tree will be + printed. ++ +This option does not (currently) work correctly when an object in the +index is specified (e.g. `:link` instead of `HEAD:link`) rather than +one in the tree. ++ +This option cannot (currently) be used unless `--batch` or +`--batch-check` is used. ++ +For example, consider a git repository containing: ++ +-- + f: a file containing "hello\n" + link: a symlink to f + dir/link: a symlink to ../f + plink: a symlink to ../f + alink: a symlink to /etc/passwd +-- ++ +For a regular file `f`, `echo HEAD:f | git cat-file --batch` would print ++ +-- + ce013625030ba8dba906f756967f9e9ca394464a blob 6 +-- ++ +And `echo HEAD:link | git cat-file --batch --follow-symlinks` would +print the same thing, as would `HEAD:dir/link`, as they both point at +`HEAD:f`. ++ +Without `--follow-symlinks`, these would print data about the symlink +itself. In the case of `HEAD:link`, you would see ++ +-- + 4d1ae35ba2c8ec712fa2a379db44ad639ca277bd blob 1 +-- ++ +Both `plink` and `alink` point outside the tree, so they would +respectively print: ++ +-- + symlink 4 + ../f + + symlink 11 + /etc/passwd +-- + + OUTPUT ------ If '-t' is specified, one of the <type>. @@ -151,6 +207,47 @@ the repository, then `cat-file` will ignore any custom format and print: <object> SP missing LF ------------ +If --follow-symlinks is used, and a symlink in the repository points +outside the repository, then `cat-file` will ignore any custom format +and print: + +------------ +symlink SP <size> LF +<symlink> LF +------------ + +The symlink will either be absolute (beginning with a /), or relative +to the tree root. For instance, if dir/link points to ../../foo, then +<symlink> will be ../foo. <size> is the size of the symlink in bytes. + +If --follow-symlinks is used, the following error messages will be +displayed: + +------------ +<object> SP missing LF +------------ +is printed when the initial symlink requested does not exist. + +------------ +dangling SP <size> LF +<object> LF +------------ +is printed when the initial symlink exists, but something that +it (transitive-of) points to does not. + +------------ +loop SP <size> LF +<object> LF +------------ +is printed for symlink loops (or any symlinks that +require more than 40 link resolutions to resolve). + +------------ +notdir SP <size> LF +<object> LF +------------ +is printed when, during symlink resolution, a file is used as a +directory name. CAVEATS ------- diff --git a/Documentation/git-commit.txt b/Documentation/git-commit.txt index 617dea083e..904dafa0f7 100644 --- a/Documentation/git-commit.txt +++ b/Documentation/git-commit.txt @@ -94,7 +94,7 @@ OPTIONS --reset-author:: When used with -C/-c/--amend options, or when committing after a a conflicting cherry-pick, declare that the authorship of the - resulting commit now belongs of the committer. This also renews + resulting commit now belongs to the committer. This also renews the author timestamp. --short:: diff --git a/Documentation/git-for-each-ref.txt b/Documentation/git-for-each-ref.txt index 42408752d0..7f8d9a5b5f 100644 --- a/Documentation/git-for-each-ref.txt +++ b/Documentation/git-for-each-ref.txt @@ -97,6 +97,12 @@ upstream:: or "=" (in sync). Has no effect if the ref does not have tracking information associated with it. +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. + HEAD:: '*' if HEAD matches current ref (the checked out branch), ' ' otherwise. diff --git a/Documentation/git-http-backend.txt b/Documentation/git-http-backend.txt index 3ca18c4de5..9268fb6b1e 100644 --- a/Documentation/git-http-backend.txt +++ b/Documentation/git-http-backend.txt @@ -255,6 +255,15 @@ The GIT_HTTP_EXPORT_ALL environmental variable may be passed to 'git-http-backend' to bypass the check for the "git-daemon-export-ok" file in each repository before allowing export of that repository. +The `GIT_HTTP_MAX_REQUEST_BUFFER` environment variable (or the +`http.maxRequestBuffer` config variable) may be set to change the +largest ref negotiation request that git will handle during a fetch; any +fetch requiring a larger buffer will not succeed. This value should not +normally need to be changed, but may be helpful if you are fetching from +a repository with an extremely large number of refs. The value can be +specified with a unit (e.g., `100M` for 100 megabytes). The default is +10 megabytes. + The backend process sets GIT_COMMITTER_NAME to '$REMOTE_USER' and GIT_COMMITTER_EMAIL to '$\{REMOTE_USER}@http.$\{REMOTE_ADDR\}', ensuring that any reflogs created by 'git-receive-pack' contain some diff --git a/Documentation/git-status.txt b/Documentation/git-status.txt index 5221f950ce..335f312335 100644 --- a/Documentation/git-status.txt +++ b/Documentation/git-status.txt @@ -66,7 +66,10 @@ When `-u` option is not used, untracked files and directories are shown (i.e. the same as specifying `normal`), to help you avoid forgetting to add newly created files. Because it takes extra work to find untracked files in the filesystem, this mode may take some -time in a large working tree. You can use `no` to have `git status` +time in a large working tree. +Consider enabling untracked cache and split index if supported (see +`git update-index --untracked-cache` and `git update-index +--split-index`), Otherwise you can use `no` to have `git status` return more quickly without showing untracked files. + The default can be changed using the status.showUntrackedFiles diff --git a/Documentation/git-update-index.txt b/Documentation/git-update-index.txt index aff01798cd..1a296bc29a 100644 --- a/Documentation/git-update-index.txt +++ b/Documentation/git-update-index.txt @@ -170,6 +170,20 @@ may not support it yet. the shared index file. This mode is designed for very large indexes that take a significant amount of time to read or write. +--untracked-cache:: +--no-untracked-cache:: + Enable or disable untracked cache extension. This could speed + up for commands that involve determining untracked files such + as `git status`. The underlying operating system and file + system must change `st_mtime` field of a directory if files + are added or deleted in that directory. + +--force-untracked-cache:: + For safety, `--untracked-cache` performs tests on the working + directory to make sure untracked cache can be used. These + tests can take a few seconds. `--force-untracked-cache` can be + used to skip the tests. + \--:: Do not interpret any more arguments as options. diff --git a/Documentation/git.txt b/Documentation/git.txt index 38cc004065..ccc12b2806 100644 --- a/Documentation/git.txt +++ b/Documentation/git.txt @@ -43,9 +43,10 @@ unreleased) version of Git, that is available from the 'master' branch of the `git.git` repository. Documentation for older releases are available here: -* link:v2.4.1/git.html[documentation for release 2.4.1] +* link:v2.4.2/git.html[documentation for release 2.4.2] * release notes for + link:RelNotes/2.4.2.txt[2.4.2], link:RelNotes/2.4.1.txt[2.4.1], link:RelNotes/2.4.0.txt[2.4]. diff --git a/Documentation/howto/new-command.txt b/Documentation/howto/new-command.txt index d7de5a3e9e..6d772bd927 100644 --- a/Documentation/howto/new-command.txt +++ b/Documentation/howto/new-command.txt @@ -95,7 +95,9 @@ your language, document it in the INSTALL file. that categorizes commands by type, so they can be listed in appropriate subsections in the documentation's summary command list. Add an entry for yours. To understand the categories, look at git-commands.txt -in the main directory. +in the main directory. If the new command is part of the typical Git +workflow and you believe it common enough to be mentioned in 'git help', +map this command to a common group in the column [common]. 7. Give the maintainer one paragraph to include in the RelNotes file to describe the new feature; a good place to do so is in the cover diff --git a/Documentation/pretty-formats.txt b/Documentation/pretty-formats.txt index dcf7429a47..dc865cbb27 100644 --- a/Documentation/pretty-formats.txt +++ b/Documentation/pretty-formats.txt @@ -79,7 +79,10 @@ stored in the commit object. Notably, the SHA-1s are displayed in full, regardless of whether --abbrev or --no-abbrev are used, and 'parents' information show the true parent commits, without taking grafts or history -simplification into account. +simplification into account. Note that this format affects the way +commits are displayed, but not the way the diff is shown e.g. with +`git log --raw`. To get full object names in a raw diff format, +use `--no-abbrev`. * 'format:<string>' + diff --git a/Documentation/revisions.txt b/Documentation/revisions.txt index 07961185fe..d85e303364 100644 --- a/Documentation/revisions.txt +++ b/Documentation/revisions.txt @@ -98,6 +98,31 @@ some output processing may assume ref names in UTF-8. `branch.<name>.merge`). A missing branchname defaults to the current one. +'<branchname>@\{push\}', e.g. 'master@\{push\}', '@\{push\}':: + The suffix '@\{push}' reports the branch "where we would push to" if + `git push` were run while `branchname` was checked out (or the current + 'HEAD' if no branchname is specified). Since our push destination is + in a remote repository, of course, we report the local tracking branch + that corresponds to that branch (i.e., something in 'refs/remotes/'). ++ +Here's an example to make it more clear: ++ +------------------------------ +$ git config push.default current +$ git config remote.pushdefault myfork +$ git checkout -b mybranch origin/master + +$ git rev-parse --symbolic-full-name @{upstream} +refs/remotes/origin/master + +$ git rev-parse --symbolic-full-name @{push} +refs/remotes/myfork/mybranch +------------------------------ ++ +Note in the example that we set up a triangular workflow, where we pull +from one location and push to another. In a non-triangular workflow, +'@\{push}' is the same as '@\{upstream}', and there is no need for it. + '<rev>{caret}', e.g. 'HEAD{caret}, v1.5.1{caret}0':: A suffix '{caret}' to a revision parameter means the first parent of that commit object. '{caret}<n>' means the <n>th parent (i.e. diff --git a/Documentation/technical/api-remote.txt b/Documentation/technical/api-remote.txt index 5d245aa9d1..2cfdd224a8 100644 --- a/Documentation/technical/api-remote.txt +++ b/Documentation/technical/api-remote.txt @@ -97,10 +97,6 @@ It contains: The name of the remote listed in the configuration. -`remote`:: - - The struct remote for that remote. - `merge_name`:: An array of the "merge" lines in the configuration. diff --git a/Documentation/technical/http-protocol.txt b/Documentation/technical/http-protocol.txt index 229f845dfa..1c561bdd92 100644 --- a/Documentation/technical/http-protocol.txt +++ b/Documentation/technical/http-protocol.txt @@ -319,7 +319,8 @@ Servers SHOULD support all capabilities defined here. Clients MUST send at least one "want" command in the request body. Clients MUST NOT reference an id in a "want" command which did not appear in the response obtained through ref discovery unless the -server advertises capability `allow-tip-sha1-in-want`. +server advertises capability `allow-tip-sha1-in-want` or +`allow-reachable-sha1-in-want`. compute_request = want_list have_list diff --git a/Documentation/technical/index-format.txt b/Documentation/technical/index-format.txt index 35112e4966..b7093af8b2 100644 --- a/Documentation/technical/index-format.txt +++ b/Documentation/technical/index-format.txt @@ -233,3 +233,65 @@ Git index format The remaining index entries after replaced ones will be added to the final index. These added entries are also sorted by entry name then stage. + +== Untracked cache + + Untracked cache saves the untracked file list and necessary data to + verify the cache. The signature for this extension is { 'U', 'N', + 'T', 'R' }. + + The extension starts with + + - A sequence of NUL-terminated strings, preceded by the size of the + sequence in variable width encoding. Each string describes the + environment where the cache can be used. + + - Stat data of $GIT_DIR/info/exclude. See "Index entry" section from + ctime field until "file size". + + - Stat data of core.excludesfile + + - 32-bit dir_flags (see struct dir_struct) + + - 160-bit SHA-1 of $GIT_DIR/info/exclude. Null SHA-1 means the file + does not exist. + + - 160-bit SHA-1 of core.excludesfile. Null SHA-1 means the file does + not exist. + + - NUL-terminated string of per-dir exclude file name. This usually + is ".gitignore". + + - The number of following directory blocks, variable width + encoding. If this number is zero, the extension ends here with a + following NUL. + + - A number of directory blocks in depth-first-search order, each + consists of + + - The number of untracked entries, variable width encoding. + + - The number of sub-directory blocks, variable width encoding. + + - The directory name terminated by NUL. + + - A number of untrached file/dir names terminated by NUL. + +The remaining data of each directory block is grouped by type: + + - An ewah bitmap, the n-th bit marks whether the n-th directory has + valid untracked cache entries. + + - An ewah bitmap, the n-th bit records "check-only" bit of + read_directory_recursive() for the n-th directory. + + - An ewah bitmap, the n-th bit indicates whether SHA-1 and stat data + is valid for the n-th directory and exists in the next data. + + - An array of stat data. The n-th data corresponds with the n-th + "one" bit in the previous ewah bitmap. + + - An array of SHA-1. The n-th SHA-1 corresponds with the n-th "one" bit + in the previous ewah bitmap. + + - One NUL. diff --git a/Documentation/technical/protocol-capabilities.txt b/Documentation/technical/protocol-capabilities.txt index 4f8a7bfb4c..eaab6b4ac7 100644 --- a/Documentation/technical/protocol-capabilities.txt +++ b/Documentation/technical/protocol-capabilities.txt @@ -260,6 +260,13 @@ If the upload-pack server advertises this capability, fetch-pack may send "want" lines with SHA-1s that exist at the server but are not advertised by upload-pack. +allow-reachable-sha1-in-want +---------------------------- + +If the upload-pack server advertises this capability, fetch-pack may +send "want" lines with SHA-1s that exist at the server but are not +advertised by upload-pack. + push-cert=<nonce> ----------------- @@ -574,6 +574,7 @@ TEST_PROGRAMS_NEED_X += test-date TEST_PROGRAMS_NEED_X += test-delta TEST_PROGRAMS_NEED_X += test-dump-cache-tree TEST_PROGRAMS_NEED_X += test-dump-split-index +TEST_PROGRAMS_NEED_X += test-dump-untracked-cache TEST_PROGRAMS_NEED_X += test-genrandom TEST_PROGRAMS_NEED_X += test-hashmap TEST_PROGRAMS_NEED_X += test-index-version @@ -1693,10 +1694,10 @@ $(BUILT_INS): git$X ln -s $< $@ 2>/dev/null || \ cp $< $@ -common-cmds.h: ./generate-cmdlist.sh command-list.txt +common-cmds.h: generate-cmdlist.perl command-list.txt common-cmds.h: $(wildcard Documentation/git-*.txt) - $(QUIET_GEN)./generate-cmdlist.sh > $@+ && mv $@+ $@ + $(QUIET_GEN)$(PERL_PATH) generate-cmdlist.perl command-list.txt > $@+ && mv $@+ $@ SCRIPT_DEFINES = $(SHELL_PATH_SQ):$(DIFF_SQ):$(GIT_VERSION):\ $(localedir_SQ):$(NO_CURL):$(USE_GETTEXT_SCHEME):$(SANE_TOOL_PATH_SQ):\ @@ -2454,7 +2455,7 @@ check-docs:: esac ; \ test -f "Documentation/$$v.txt" || \ echo "no doc: $$v"; \ - sed -e '/^#/d' command-list.txt | \ + sed -e '1,/^### command list/d' -e '/^#/d' command-list.txt | \ grep -q "^$$v[ ]" || \ case "$$v" in \ git) ;; \ @@ -2462,7 +2463,8 @@ check-docs:: esac ; \ done; \ ( \ - sed -e '/^#/d' \ + sed -e '1,/^### command list/d' \ + -e '/^#/d' \ -e 's/[ ].*//' \ -e 's/^/listed /' command-list.txt; \ $(MAKE) -C Documentation print-man1 | \ diff --git a/builtin/blame.c b/builtin/blame.c index 8d70623cb8..b3e948e757 100644 --- a/builtin/blame.c +++ b/builtin/blame.c @@ -26,8 +26,9 @@ #include "userdiff.h" #include "line-range.h" #include "line-log.h" +#include "dir.h" -static char blame_usage[] = N_("git blame [<options>] [<rev-opts>] [<rev>] [--] file"); +static char blame_usage[] = N_("git blame [<options>] [<rev-opts>] [<rev>] [--] <file>"); static const char *blame_opt_usage[] = { blame_usage, @@ -2151,16 +2152,6 @@ static void sanity_check_refcnt(struct scoreboard *sb) } } -/* - * Used for the command line parsing; check if the path exists - * in the working tree. - */ -static int has_string_in_work_tree(const char *path) -{ - struct stat st; - return !lstat(path, &st); -} - static unsigned parse_score(const char *arg) { char *end; @@ -2656,14 +2647,14 @@ parse_done: if (argc < 2) usage_with_options(blame_opt_usage, options); path = add_prefix(prefix, argv[argc - 1]); - if (argc == 3 && !has_string_in_work_tree(path)) { /* (2b) */ + if (argc == 3 && !file_exists(path)) { /* (2b) */ path = add_prefix(prefix, argv[1]); argv[1] = argv[2]; } argv[argc - 1] = "--"; setup_work_tree(); - if (!has_string_in_work_tree(path)) + if (!file_exists(path)) die_errno("cannot stat path '%s'", path); } diff --git a/builtin/branch.c b/builtin/branch.c index 0d3b9af6e9..b42e5b6dbc 100644 --- a/builtin/branch.c +++ b/builtin/branch.c @@ -123,14 +123,12 @@ static int branch_merged(int kind, const char *name, if (kind == REF_LOCAL_BRANCH) { struct branch *branch = branch_get(name); + const char *upstream = branch_get_upstream(branch, NULL); unsigned char sha1[20]; - if (branch && - branch->merge && - branch->merge[0] && - branch->merge[0]->dst && + if (upstream && (reference_name = reference_name_to_free = - resolve_refdup(branch->merge[0]->dst, RESOLVE_REF_READING, + resolve_refdup(upstream, RESOLVE_REF_READING, sha1, NULL)) != NULL) reference_rev = lookup_commit_reference(sha1); } @@ -427,25 +425,19 @@ static void fill_tracking_info(struct strbuf *stat, const char *branch_name, 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; - switch (stat_tracking_info(branch, &ours, &theirs)) { - case 0: - /* no base */ - return; - case -1: - /* with "gone" base */ + if (stat_tracking_info(branch, &ours, &theirs, &upstream) < 0) { + if (!upstream) + return; upstream_is_gone = 1; - break; - default: - /* with base */ - break; } if (show_upstream_ref) { - ref = shorten_unambiguous_ref(branch->merge[0]->dst, 0); + ref = shorten_unambiguous_ref(upstream, 0); if (want_color(branch_use_color)) strbuf_addf(&fancy, "%s%s%s", branch_get_color(BRANCH_COLOR_UPSTREAM), diff --git a/builtin/cat-file.c b/builtin/cat-file.c index ecb488822f..049a95f1f1 100644 --- a/builtin/cat-file.c +++ b/builtin/cat-file.c @@ -8,6 +8,7 @@ #include "parse-options.h" #include "userdiff.h" #include "streaming.h" +#include "tree-walk.h" static int cat_one_file(int opt, const char *exp_type, const char *obj_name, int unknown_type) @@ -233,6 +234,7 @@ static void print_object_or_die(int fd, struct expand_data *data) struct batch_options { int enabled; + int follow_symlinks; int print_contents; const char *format; }; @@ -241,12 +243,44 @@ static int batch_one_object(const char *obj_name, struct batch_options *opt, struct expand_data *data) { struct strbuf buf = STRBUF_INIT; + struct object_context ctx; + int flags = opt->follow_symlinks ? GET_SHA1_FOLLOW_SYMLINKS : 0; + enum follow_symlinks_result result; if (!obj_name) return 1; - if (get_sha1(obj_name, data->sha1)) { - printf("%s missing\n", obj_name); + result = get_sha1_with_context(obj_name, flags, data->sha1, &ctx); + if (result != FOUND) { + switch (result) { + case MISSING_OBJECT: + printf("%s missing\n", obj_name); + break; + case DANGLING_SYMLINK: + printf("dangling %"PRIuMAX"\n%s\n", + (uintmax_t)strlen(obj_name), obj_name); + break; + case SYMLINK_LOOP: + printf("loop %"PRIuMAX"\n%s\n", + (uintmax_t)strlen(obj_name), obj_name); + break; + case NOT_DIR: + printf("notdir %"PRIuMAX"\n%s\n", + (uintmax_t)strlen(obj_name), obj_name); + break; + default: + die("BUG: unknown get_sha1_with_context result %d\n", + result); + break; + } + fflush(stdout); + return 0; + } + + if (ctx.mode == 0) { + printf("symlink %"PRIuMAX"\n%s\n", + (uintmax_t)ctx.symlink_path.len, + ctx.symlink_path.buf); fflush(stdout); return 0; } @@ -333,7 +367,7 @@ static int batch_objects(struct batch_options *opt) static const char * const cat_file_usage[] = { N_("git cat-file (-t [--allow-unknown-type]|-s [--allow-unknown-type]|-e|-p|<type>|--textconv) <object>"), - N_("git cat-file (--batch | --batch-check) < <list-of-objects>"), + N_("git cat-file (--batch | --batch-check) [--follow-symlinks] < <list-of-objects>"), NULL }; @@ -351,9 +385,8 @@ static int batch_option_callback(const struct option *opt, { struct batch_options *bo = opt->value; - if (unset) { - memset(bo, 0, sizeof(*bo)); - return 0; + if (bo->enabled) { + return 1; } bo->enabled = 1; @@ -387,6 +420,8 @@ int cmd_cat_file(int argc, const char **argv, const char *prefix) { OPTION_CALLBACK, 0, "batch-check", &batch, "format", N_("show info about objects fed from the standard input"), PARSE_OPT_OPTARG, batch_option_callback }, + OPT_BOOL(0, "follow-symlinks", &batch.follow_symlinks, + N_("follow in-tree symlinks (used with --batch or --batch-check)")), OPT_END() }; @@ -411,6 +446,10 @@ int cmd_cat_file(int argc, const char **argv, const char *prefix) usage_with_options(cat_file_usage, options); } + if (batch.follow_symlinks && !batch.enabled) { + usage_with_options(cat_file_usage, options); + } + if (batch.enabled) return batch_objects(&batch); diff --git a/builtin/clean.c b/builtin/clean.c index 98c103fa8b..6dcb72e644 100644 --- a/builtin/clean.c +++ b/builtin/clean.c @@ -314,7 +314,6 @@ static void print_highlight_menu_stuff(struct menu_stuff *stuff, int **chosen) { struct string_list menu_list = STRING_LIST_INIT_DUP; struct strbuf menu = STRBUF_INIT; - struct strbuf buf = STRBUF_INIT; struct menu_item *menu_item; struct string_list_item *string_list_item; int i; @@ -363,7 +362,6 @@ static void print_highlight_menu_stuff(struct menu_stuff *stuff, int **chosen) pretty_print_menus(&menu_list); strbuf_release(&menu); - strbuf_release(&buf); string_list_clear(&menu_list, 0); } @@ -941,15 +939,15 @@ int cmd_clean(int argc, const char **argv, const char *prefix) if (!cache_name_is_other(ent->name, ent->len)) continue; - if (lstat(ent->name, &st)) - die_errno("Cannot lstat '%s'", ent->name); - if (pathspec.nr) matches = dir_path_match(ent, &pathspec, 0, NULL); if (pathspec.nr && !matches) continue; + if (lstat(ent->name, &st)) + die_errno("Cannot lstat '%s'", ent->name); + if (S_ISDIR(st.st_mode) && !remove_directories && matches != MATCHED_EXACTLY) continue; diff --git a/builtin/commit.c b/builtin/commit.c index d6515a2a50..254477fd1d 100644 --- a/builtin/commit.c +++ b/builtin/commit.c @@ -1366,13 +1366,14 @@ int cmd_status(int argc, const char **argv, const char *prefix) refresh_index(&the_index, REFRESH_QUIET|REFRESH_UNMERGED, &s.pathspec, NULL, NULL); fd = hold_locked_index(&index_lock, 0); - if (0 <= fd) - update_index_if_able(&the_index, &index_lock); s.is_initial = get_sha1(s.reference, sha1) ? 1 : 0; s.ignore_submodule_arg = ignore_submodule_arg; wt_status_collect(&s); + if (0 <= fd) + update_index_if_able(&the_index, &index_lock); + if (s.relative_paths) s.prefix = prefix; diff --git a/builtin/for-each-ref.c b/builtin/for-each-ref.c index 05ce28cebb..f7e51a7fad 100644 --- a/builtin/for-each-ref.c +++ b/builtin/for-each-ref.c @@ -74,6 +74,7 @@ static struct { { "contents:body" }, { "contents:signature" }, { "upstream" }, + { "push" }, { "symref" }, { "flag" }, { "HEAD" }, @@ -659,15 +660,26 @@ static void populate_value(struct refinfo *ref) else if (starts_with(name, "symref")) refname = ref->symref ? ref->symref : ""; else if (starts_with(name, "upstream")) { + const char *branch_name; /* only local branches may have an upstream */ - if (!starts_with(ref->refname, "refs/heads/")) + if (!skip_prefix(ref->refname, "refs/heads/", + &branch_name)) continue; - branch = branch_get(ref->refname + 11); + branch = branch_get(branch_name); - if (!branch || !branch->merge || !branch->merge[0] || - !branch->merge[0]->dst) + refname = branch_get_upstream(branch, NULL); + if (!refname) + continue; + } else if (starts_with(name, "push")) { + const char *branch_name; + if (!skip_prefix(ref->refname, "refs/heads/", + &branch_name)) + continue; + branch = branch_get(branch_name); + + refname = branch_get_push(branch, NULL); + if (!refname) continue; - refname = branch->merge[0]->dst; } else if (starts_with(name, "color:")) { char color[COLOR_MAXLEN] = ""; @@ -713,11 +725,12 @@ static void populate_value(struct refinfo *ref) refname = shorten_unambiguous_ref(refname, warn_ambiguous_refs); else if (!strcmp(formatp, "track") && - starts_with(name, "upstream")) { + (starts_with(name, "upstream") || + starts_with(name, "push"))) { char buf[40]; if (stat_tracking_info(branch, &num_ours, - &num_theirs) != 1) + &num_theirs, NULL)) continue; if (!num_ours && !num_theirs) @@ -735,11 +748,12 @@ static void populate_value(struct refinfo *ref) } continue; } else if (!strcmp(formatp, "trackshort") && - starts_with(name, "upstream")) { + (starts_with(name, "upstream") || + starts_with(name, "push"))) { assert(branch); if (stat_tracking_info(branch, &num_ours, - &num_theirs) != 1) + &num_theirs, NULL)) continue; if (!num_ours && !num_theirs) diff --git a/builtin/log.c b/builtin/log.c index dd8f3fcfc4..e67671e37a 100644 --- a/builtin/log.c +++ b/builtin/log.c @@ -38,7 +38,7 @@ static const char *fmt_patch_subject_prefix = "PATCH"; static const char *fmt_pretty; static const char * const builtin_log_usage[] = { - N_("git log [<options>] [<revision range>] [[--] <path>...]"), + N_("git log [<options>] [<revision-range>] [[--] <path>...]"), N_("git show [<options>] <object>..."), NULL }; @@ -1632,16 +1632,13 @@ int cmd_cherry(int argc, const char **argv, const char *prefix) break; default: current_branch = branch_get(NULL); - if (!current_branch || !current_branch->merge - || !current_branch->merge[0] - || !current_branch->merge[0]->dst) { + upstream = branch_get_upstream(current_branch, NULL); + if (!upstream) { fprintf(stderr, _("Could not find a tracked" " remote branch, please" " specify <upstream> manually.\n")); usage_with_options(cherry_usage, options); } - - upstream = current_branch->merge[0]->dst; } init_revisions(&revs, prefix); diff --git a/builtin/merge.c b/builtin/merge.c index f89f60e11a..85c54dcd5a 100644 --- a/builtin/merge.c +++ b/builtin/merge.c @@ -933,7 +933,7 @@ static int setup_with_upstream(const char ***argv) if (!branch) die(_("No current branch.")); - if (!branch->remote) + if (!branch->remote_name) die(_("No remote for the current branch.")); if (!branch->merge_nr) die(_("No default upstream defined for the current branch.")); diff --git a/builtin/rm.c b/builtin/rm.c index 3304bff42a..80b972f92f 100644 --- a/builtin/rm.c +++ b/builtin/rm.c @@ -84,7 +84,6 @@ static int check_submodules_use_gitfiles(void) const char *name = list.entry[i].name; int pos; const struct cache_entry *ce; - struct stat st; pos = cache_name_pos(name, strlen(name)); if (pos < 0) { @@ -95,7 +94,7 @@ static int check_submodules_use_gitfiles(void) ce = active_cache[pos]; if (!S_ISGITLINK(ce->ce_mode) || - (lstat(ce->name, &st) < 0) || + !file_exists(ce->name) || is_empty_dir(name)) continue; diff --git a/builtin/update-index.c b/builtin/update-index.c index 0665b31ea1..7431938fa6 100644 --- a/builtin/update-index.c +++ b/builtin/update-index.c @@ -33,6 +33,7 @@ static int mark_valid_only; static int mark_skip_worktree_only; #define MARK_FLAG 1 #define UNMARK_FLAG 2 +static struct strbuf mtime_dir = STRBUF_INIT; __attribute__((format (printf, 1, 2))) static void report(const char *fmt, ...) @@ -48,6 +49,166 @@ static void report(const char *fmt, ...) va_end(vp); } +static void remove_test_directory(void) +{ + if (mtime_dir.len) + remove_dir_recursively(&mtime_dir, 0); +} + +static const char *get_mtime_path(const char *path) +{ + static struct strbuf sb = STRBUF_INIT; + strbuf_reset(&sb); + strbuf_addf(&sb, "%s/%s", mtime_dir.buf, path); + return sb.buf; +} + +static void xmkdir(const char *path) +{ + path = get_mtime_path(path); + if (mkdir(path, 0700)) + die_errno(_("failed to create directory %s"), path); +} + +static int xstat_mtime_dir(struct stat *st) +{ + if (stat(mtime_dir.buf, st)) + die_errno(_("failed to stat %s"), mtime_dir.buf); + return 0; +} + +static int create_file(const char *path) +{ + int fd; + path = get_mtime_path(path); + fd = open(path, O_CREAT | O_RDWR, 0644); + if (fd < 0) + die_errno(_("failed to create file %s"), path); + return fd; +} + +static void xunlink(const char *path) +{ + path = get_mtime_path(path); + if (unlink(path)) + die_errno(_("failed to delete file %s"), path); +} + +static void xrmdir(const char *path) +{ + path = get_mtime_path(path); + if (rmdir(path)) + die_errno(_("failed to delete directory %s"), path); +} + +static void avoid_racy(void) +{ + /* + * not use if we could usleep(10) if USE_NSEC is defined. The + * field nsec could be there, but the OS could choose to + * ignore it? + */ + sleep(1); +} + +static int test_if_untracked_cache_is_supported(void) +{ + struct stat st; + struct stat_data base; + int fd, ret = 0; + + strbuf_addstr(&mtime_dir, "mtime-test-XXXXXX"); + if (!mkdtemp(mtime_dir.buf)) + die_errno("Could not make temporary directory"); + + fprintf(stderr, _("Testing ")); + atexit(remove_test_directory); + xstat_mtime_dir(&st); + fill_stat_data(&base, &st); + fputc('.', stderr); + + avoid_racy(); + fd = create_file("newfile"); + xstat_mtime_dir(&st); + if (!match_stat_data(&base, &st)) { + close(fd); + fputc('\n', stderr); + fprintf_ln(stderr,_("directory stat info does not " + "change after adding a new file")); + goto done; + } + fill_stat_data(&base, &st); + fputc('.', stderr); + + avoid_racy(); + xmkdir("new-dir"); + xstat_mtime_dir(&st); + if (!match_stat_data(&base, &st)) { + close(fd); + fputc('\n', stderr); + fprintf_ln(stderr, _("directory stat info does not change " + "after adding a new directory")); + goto done; + } + fill_stat_data(&base, &st); + fputc('.', stderr); + + avoid_racy(); + write_or_die(fd, "data", 4); + close(fd); + xstat_mtime_dir(&st); + if (match_stat_data(&base, &st)) { + fputc('\n', stderr); + fprintf_ln(stderr, _("directory stat info changes " + "after updating a file")); + goto done; + } + fputc('.', stderr); + + avoid_racy(); + close(create_file("new-dir/new")); + xstat_mtime_dir(&st); + if (match_stat_data(&base, &st)) { + fputc('\n', stderr); + fprintf_ln(stderr, _("directory stat info changes after " + "adding a file inside subdirectory")); + goto done; + } + fputc('.', stderr); + + avoid_racy(); + xunlink("newfile"); + xstat_mtime_dir(&st); + if (!match_stat_data(&base, &st)) { + fputc('\n', stderr); + fprintf_ln(stderr, _("directory stat info does not " + "change after deleting a file")); + goto done; + } + fill_stat_data(&base, &st); + fputc('.', stderr); + + avoid_racy(); + xunlink("new-dir/new"); + xrmdir("new-dir"); + xstat_mtime_dir(&st); + if (!match_stat_data(&base, &st)) { + fputc('\n', stderr); + fprintf_ln(stderr, _("directory stat info does not " + "change after deleting a directory")); + goto done; + } + + if (rmdir(mtime_dir.buf)) + die_errno(_("failed to delete directory %s"), mtime_dir.buf); + fprintf_ln(stderr, _(" OK")); + ret = 1; + +done: + strbuf_release(&mtime_dir); + return ret; +} + static int mark_ce_flags(const char *path, int flag, int mark) { int namelen = strlen(path); @@ -741,6 +902,7 @@ static int reupdate_callback(struct parse_opt_ctx_t *ctx, int cmd_update_index(int argc, const char **argv, const char *prefix) { int newfd, entries, has_errors = 0, line_termination = '\n'; + int untracked_cache = -1; int read_from_stdin = 0; int prefix_length = prefix ? strlen(prefix) : 0; int preferred_index_format = 0; @@ -832,6 +994,10 @@ int cmd_update_index(int argc, const char **argv, const char *prefix) N_("write index in this format")), OPT_BOOL(0, "split-index", &split_index, N_("enable or disable split index")), + OPT_BOOL(0, "untracked-cache", &untracked_cache, + N_("enable/disable untracked cache")), + OPT_SET_INT(0, "force-untracked-cache", &untracked_cache, + N_("enable untracked cache without testing the filesystem"), 2), OPT_END() }; @@ -938,6 +1104,28 @@ int cmd_update_index(int argc, const char **argv, const char *prefix) the_index.split_index = NULL; the_index.cache_changed |= SOMETHING_CHANGED; } + if (untracked_cache > 0) { + struct untracked_cache *uc; + + if (untracked_cache < 2) { + setup_work_tree(); + if (!test_if_untracked_cache_is_supported()) + return 1; + } + if (!the_index.untracked) { + uc = xcalloc(1, sizeof(*uc)); + strbuf_init(&uc->ident, 100); + uc->exclude_per_dir = ".gitignore"; + /* should be the same flags used by git-status */ + uc->dir_flags = DIR_SHOW_OTHER_DIRECTORIES | DIR_HIDE_EMPTY_DIRECTORIES; + the_index.untracked = uc; + } + add_untracked_ident(the_index.untracked); + the_index.cache_changed |= UNTRACKED_CHANGED; + } else if (!untracked_cache && the_index.untracked) { + the_index.untracked = NULL; + the_index.cache_changed |= UNTRACKED_CHANGED; + } if (active_cache_changed) { if (newfd < 0) { @@ -297,8 +297,11 @@ static inline unsigned int canon_mode(unsigned int mode) #define RESOLVE_UNDO_CHANGED (1 << 4) #define CACHE_TREE_CHANGED (1 << 5) #define SPLIT_INDEX_ORDERED (1 << 6) +#define UNTRACKED_CHANGED (1 << 7) struct split_index; +struct untracked_cache; + struct index_state { struct cache_entry **cache; unsigned int version; @@ -312,6 +315,7 @@ struct index_state { struct hashmap name_hash; struct hashmap dir_hash; unsigned char sha1[20]; + struct untracked_cache *untracked; }; extern struct index_state the_index; @@ -563,6 +567,8 @@ extern void fill_stat_data(struct stat_data *sd, struct stat *st); * INODE_CHANGED, and DATA_CHANGED. */ extern int match_stat_data(const struct stat_data *sd, struct stat *st); +extern int match_stat_data_racy(const struct index_state *istate, + const struct stat_data *sd, struct stat *st); extern void fill_stat_cache_info(struct cache_entry *ce, struct stat *st); @@ -965,15 +971,21 @@ struct object_context { unsigned char tree[20]; char path[PATH_MAX]; unsigned mode; + /* + * symlink_path is only used by get_tree_entry_follow_symlinks, + * and only for symlinks that point outside the repository. + */ + struct strbuf symlink_path; }; -#define GET_SHA1_QUIETLY 01 -#define GET_SHA1_COMMIT 02 -#define GET_SHA1_COMMITTISH 04 -#define GET_SHA1_TREE 010 -#define GET_SHA1_TREEISH 020 -#define GET_SHA1_BLOB 040 -#define GET_SHA1_ONLY_TO_DIE 04000 +#define GET_SHA1_QUIETLY 01 +#define GET_SHA1_COMMIT 02 +#define GET_SHA1_COMMITTISH 04 +#define GET_SHA1_TREE 010 +#define GET_SHA1_TREEISH 020 +#define GET_SHA1_BLOB 040 +#define GET_SHA1_FOLLOW_SYMLINKS 0100 +#define GET_SHA1_ONLY_TO_DIE 04000 extern int get_sha1(const char *str, unsigned char *sha1); extern int get_sha1_commit(const char *str, unsigned char *sha1); diff --git a/command-list.txt b/command-list.txt index 54d8d21ad2..b17c011bfd 100644 --- a/command-list.txt +++ b/command-list.txt @@ -1,29 +1,39 @@ -# List of known git commands. +# common commands are grouped by themes +# these groups are output by 'git help' in the order declared here. +# map each common command in the command list to one of these groups. +### common groups (do not change this line) +init start a working area (see also: git help tutorial) +worktree work on the current change (see also: git help everyday) +info examine the history and state (see also: git help revisions) +history grow, mark and tweak your common history +remote collaborate (see also: git help workflows) + +### command list (do not change this line) # command name category [deprecated] [common] -git-add mainporcelain common +git-add mainporcelain worktree git-am mainporcelain git-annotate ancillaryinterrogators git-apply plumbingmanipulators git-archimport foreignscminterface git-archive mainporcelain -git-bisect mainporcelain common +git-bisect mainporcelain info git-blame ancillaryinterrogators -git-branch mainporcelain common +git-branch mainporcelain history git-bundle mainporcelain git-cat-file plumbinginterrogators git-check-attr purehelpers git-check-ignore purehelpers git-check-mailmap purehelpers -git-checkout mainporcelain common +git-checkout mainporcelain history git-checkout-index plumbingmanipulators git-check-ref-format purehelpers git-cherry ancillaryinterrogators git-cherry-pick mainporcelain git-citool mainporcelain git-clean mainporcelain -git-clone mainporcelain common +git-clone mainporcelain init git-column purehelpers -git-commit mainporcelain common +git-commit mainporcelain history git-commit-tree plumbingmanipulators git-config ancillarymanipulators git-count-objects ancillaryinterrogators @@ -35,14 +45,14 @@ git-cvsimport foreignscminterface git-cvsserver foreignscminterface git-daemon synchingrepositories git-describe mainporcelain -git-diff mainporcelain common +git-diff mainporcelain history git-diff-files plumbinginterrogators git-diff-index plumbinginterrogators git-diff-tree plumbinginterrogators git-difftool ancillaryinterrogators git-fast-export ancillarymanipulators git-fast-import ancillarymanipulators -git-fetch mainporcelain common +git-fetch mainporcelain remote git-fetch-pack synchingrepositories git-filter-branch ancillarymanipulators git-fmt-merge-msg purehelpers @@ -51,7 +61,7 @@ git-format-patch mainporcelain git-fsck ancillaryinterrogators git-gc mainporcelain git-get-tar-commit-id ancillaryinterrogators -git-grep mainporcelain common +git-grep mainporcelain info git-gui mainporcelain git-hash-object plumbingmanipulators git-help ancillaryinterrogators @@ -60,17 +70,17 @@ git-http-fetch synchelpers git-http-push synchelpers git-imap-send foreignscminterface git-index-pack plumbingmanipulators -git-init mainporcelain common +git-init mainporcelain init git-instaweb ancillaryinterrogators git-interpret-trailers purehelpers gitk mainporcelain -git-log mainporcelain common +git-log mainporcelain info git-ls-files plumbinginterrogators git-ls-remote plumbinginterrogators git-ls-tree plumbinginterrogators git-mailinfo purehelpers git-mailsplit purehelpers -git-merge mainporcelain common +git-merge mainporcelain history git-merge-base plumbinginterrogators git-merge-file plumbingmanipulators git-merge-index plumbingmanipulators @@ -79,7 +89,7 @@ git-mergetool ancillarymanipulators git-merge-tree ancillaryinterrogators git-mktag plumbingmanipulators git-mktree plumbingmanipulators -git-mv mainporcelain common +git-mv mainporcelain worktree git-name-rev plumbinginterrogators git-notes mainporcelain git-p4 foreignscminterface @@ -90,11 +100,11 @@ git-parse-remote synchelpers git-patch-id purehelpers git-prune ancillarymanipulators git-prune-packed plumbingmanipulators -git-pull mainporcelain common -git-push mainporcelain common +git-pull mainporcelain remote +git-push mainporcelain remote git-quiltimport foreignscminterface git-read-tree plumbingmanipulators -git-rebase mainporcelain common +git-rebase mainporcelain history git-receive-pack synchelpers git-reflog ancillarymanipulators git-relink ancillarymanipulators @@ -103,28 +113,28 @@ git-repack ancillarymanipulators git-replace ancillarymanipulators git-request-pull foreignscminterface git-rerere ancillaryinterrogators -git-reset mainporcelain common +git-reset mainporcelain worktree git-revert mainporcelain git-rev-list plumbinginterrogators git-rev-parse ancillaryinterrogators -git-rm mainporcelain common +git-rm mainporcelain worktree git-send-email foreignscminterface git-send-pack synchingrepositories git-shell synchelpers git-shortlog mainporcelain -git-show mainporcelain common +git-show mainporcelain info git-show-branch ancillaryinterrogators git-show-index plumbinginterrogators git-show-ref plumbinginterrogators git-sh-i18n purehelpers git-sh-setup purehelpers git-stash mainporcelain -git-status mainporcelain common +git-status mainporcelain info git-stripspace purehelpers git-submodule mainporcelain git-svn foreignscminterface git-symbolic-ref plumbingmanipulators -git-tag mainporcelain common +git-tag mainporcelain history git-unpack-file plumbinginterrogators git-unpack-objects plumbingmanipulators git-update-index plumbingmanipulators diff --git a/compat/mingw.c b/compat/mingw.c index 70f3191a4f..496e6f8bb0 100644 --- a/compat/mingw.c +++ b/compat/mingw.c @@ -2128,3 +2128,14 @@ void mingw_startup() /* initialize Unicode console */ winansi_init(); } + +int uname(struct utsname *buf) +{ + DWORD v = GetVersion(); + memset(buf, 0, sizeof(*buf)); + strcpy(buf->sysname, "Windows"); + sprintf(buf->release, "%u.%u", v & 0xff, (v >> 8) & 0xff); + /* assuming NT variants only.. */ + sprintf(buf->version, "%u", (v >> 16) & 0x7fff); + return 0; +} diff --git a/compat/mingw.h b/compat/mingw.h index 98c5e44294..738865c6c0 100644 --- a/compat/mingw.h +++ b/compat/mingw.h @@ -76,6 +76,14 @@ struct itimerval { }; #define ITIMER_REAL 0 +struct utsname { + char sysname[16]; + char nodename[1]; + char release[16]; + char version[16]; + char machine[1]; +}; + /* * sanitize preprocessor namespace polluted by Windows headers defining * macros which collide with git local versions @@ -175,6 +183,7 @@ struct passwd *getpwuid(uid_t uid); int setitimer(int type, struct itimerval *in, struct itimerval *out); int sigaction(int sig, struct sigaction *in, struct sigaction *out); int link(const char *oldpath, const char *newpath); +int uname(struct utsname *buf); /* * replacements of existing functions @@ -13,6 +13,8 @@ #include "wildmatch.h" #include "pathspec.h" #include "utf8.h" +#include "varint.h" +#include "ewah/ewok.h" struct path_simplify { int len; @@ -32,8 +34,22 @@ enum path_treatment { path_untracked }; +/* + * Support data structure for our opendir/readdir/closedir wrappers + */ +struct cached_dir { + DIR *fdir; + struct untracked_cache_dir *untracked; + int nr_files; + int nr_dirs; + + struct dirent *de; + const char *file; + struct untracked_cache_dir *ucd; +}; + static enum path_treatment read_directory_recursive(struct dir_struct *dir, - const char *path, int len, + const char *path, int len, struct untracked_cache_dir *untracked, int check_only, const struct path_simplify *simplify); static int get_dtype(struct dirent *de, const char *path, int len); @@ -385,7 +401,6 @@ int report_path_error(const char *ps_matched, /* * Make sure all pathspec matched; otherwise it is an error. */ - struct strbuf sb = STRBUF_INIT; int num, errors = 0; for (num = 0; num < pathspec->nr; num++) { int other, found_dup; @@ -417,7 +432,6 @@ int report_path_error(const char *ps_matched, pathspec->items[num].original); errors++; } - strbuf_release(&sb); return errors; } @@ -510,7 +524,8 @@ void add_exclude(const char *string, const char *base, x->el = el; } -static void *read_skip_worktree_file_from_index(const char *path, size_t *size) +static void *read_skip_worktree_file_from_index(const char *path, size_t *size, + struct sha1_stat *sha1_stat) { int pos, len; unsigned long sz; @@ -529,6 +544,10 @@ static void *read_skip_worktree_file_from_index(const char *path, size_t *size) return NULL; } *size = xsize_t(sz); + if (sha1_stat) { + memset(&sha1_stat->stat, 0, sizeof(sha1_stat->stat)); + hashcpy(sha1_stat->sha1, active_cache[pos]->sha1); + } return data; } @@ -573,11 +592,93 @@ static void trim_trailing_spaces(char *buf) *last_space = '\0'; } -int add_excludes_from_file_to_list(const char *fname, - const char *base, - int baselen, - struct exclude_list *el, - int check_index) +/* + * Given a subdirectory name and "dir" of the current directory, + * search the subdir in "dir" and return it, or create a new one if it + * does not exist in "dir". + * + * If "name" has the trailing slash, it'll be excluded in the search. + */ +static struct untracked_cache_dir *lookup_untracked(struct untracked_cache *uc, + struct untracked_cache_dir *dir, + const char *name, int len) +{ + int first, last; + struct untracked_cache_dir *d; + if (!dir) + return NULL; + if (len && name[len - 1] == '/') + len--; + first = 0; + last = dir->dirs_nr; + while (last > first) { + int cmp, next = (last + first) >> 1; + d = dir->dirs[next]; + cmp = strncmp(name, d->name, len); + if (!cmp && strlen(d->name) > len) + cmp = -1; + if (!cmp) + return d; + if (cmp < 0) { + last = next; + continue; + } + first = next+1; + } + + uc->dir_created++; + d = xmalloc(sizeof(*d) + len + 1); + memset(d, 0, sizeof(*d)); + memcpy(d->name, name, len); + d->name[len] = '\0'; + + ALLOC_GROW(dir->dirs, dir->dirs_nr + 1, dir->dirs_alloc); + memmove(dir->dirs + first + 1, dir->dirs + first, + (dir->dirs_nr - first) * sizeof(*dir->dirs)); + dir->dirs_nr++; + dir->dirs[first] = d; + return d; +} + +static void do_invalidate_gitignore(struct untracked_cache_dir *dir) +{ + int i; + dir->valid = 0; + dir->untracked_nr = 0; + for (i = 0; i < dir->dirs_nr; i++) + do_invalidate_gitignore(dir->dirs[i]); +} + +static void invalidate_gitignore(struct untracked_cache *uc, + struct untracked_cache_dir *dir) +{ + uc->gitignore_invalidated++; + do_invalidate_gitignore(dir); +} + +static void invalidate_directory(struct untracked_cache *uc, + struct untracked_cache_dir *dir) +{ + int i; + uc->dir_invalidated++; + dir->valid = 0; + dir->untracked_nr = 0; + for (i = 0; i < dir->dirs_nr; i++) + dir->dirs[i]->recurse = 0; +} + +/* + * Given a file with name "fname", read it (either from disk, or from + * the index if "check_index" is non-zero), parse it and store the + * exclude rules in "el". + * + * If "ss" is not NULL, compute SHA-1 of the exclude file and fill + * stat data from disk (only valid if add_excludes returns zero). If + * ss_valid is non-zero, "ss" must contain good value as input. + */ +static int add_excludes(const char *fname, const char *base, int baselen, + struct exclude_list *el, int check_index, + struct sha1_stat *sha1_stat) { struct stat st; int fd, i, lineno = 1; @@ -591,7 +692,7 @@ int add_excludes_from_file_to_list(const char *fname, if (0 <= fd) close(fd); if (!check_index || - (buf = read_skip_worktree_file_from_index(fname, &size)) == NULL) + (buf = read_skip_worktree_file_from_index(fname, &size, sha1_stat)) == NULL) return -1; if (size == 0) { free(buf); @@ -604,6 +705,11 @@ int add_excludes_from_file_to_list(const char *fname, } else { size = xsize_t(st.st_size); if (size == 0) { + if (sha1_stat) { + fill_stat_data(&sha1_stat->stat, &st); + hashcpy(sha1_stat->sha1, EMPTY_BLOB_SHA1_BIN); + sha1_stat->valid = 1; + } close(fd); return 0; } @@ -615,6 +721,22 @@ int add_excludes_from_file_to_list(const char *fname, } buf[size++] = '\n'; close(fd); + if (sha1_stat) { + int pos; + if (sha1_stat->valid && + !match_stat_data_racy(&the_index, &sha1_stat->stat, &st)) + ; /* no content change, ss->sha1 still good */ + else if (check_index && + (pos = cache_name_pos(fname, strlen(fname))) >= 0 && + !ce_stage(active_cache[pos]) && + ce_uptodate(active_cache[pos]) && + !would_convert_to_git(fname)) + hashcpy(sha1_stat->sha1, active_cache[pos]->sha1); + else + hash_sha1_file(buf, size, "blob", sha1_stat->sha1); + fill_stat_data(&sha1_stat->stat, &st); + sha1_stat->valid = 1; + } } el->filebuf = buf; @@ -638,6 +760,13 @@ int add_excludes_from_file_to_list(const char *fname, return 0; } +int add_excludes_from_file_to_list(const char *fname, const char *base, + int baselen, struct exclude_list *el, + int check_index) +{ + return add_excludes(fname, base, baselen, el, check_index, NULL); +} + struct exclude_list *add_exclude_list(struct dir_struct *dir, int group_type, const char *src) { @@ -655,14 +784,28 @@ struct exclude_list *add_exclude_list(struct dir_struct *dir, /* * Used to set up core.excludesfile and .git/info/exclude lists. */ -void add_excludes_from_file(struct dir_struct *dir, const char *fname) +static void add_excludes_from_file_1(struct dir_struct *dir, const char *fname, + struct sha1_stat *sha1_stat) { struct exclude_list *el; + /* + * catch setup_standard_excludes() that's called before + * dir->untracked is assigned. That function behaves + * differently when dir->untracked is non-NULL. + */ + if (!dir->untracked) + dir->unmanaged_exclude_files++; el = add_exclude_list(dir, EXC_FILE, fname); - if (add_excludes_from_file_to_list(fname, "", 0, el, 0) < 0) + if (add_excludes(fname, "", 0, el, 0, sha1_stat) < 0) die("cannot use %s as an exclude file", fname); } +void add_excludes_from_file(struct dir_struct *dir, const char *fname) +{ + dir->unmanaged_exclude_files++; /* see validate_untracked_cache() */ + add_excludes_from_file_1(dir, fname, NULL); +} + int match_basename(const char *basename, int basenamelen, const char *pattern, int prefix, int patternlen, int flags) @@ -837,6 +980,7 @@ static void prep_exclude(struct dir_struct *dir, const char *base, int baselen) struct exclude_list_group *group; struct exclude_list *el; struct exclude_stack *stk = NULL; + struct untracked_cache_dir *untracked; int current; group = &dir->exclude_list_group[EXC_DIRS]; @@ -874,8 +1018,14 @@ static void prep_exclude(struct dir_struct *dir, const char *base, int baselen) /* Read from the parent directories and push them down. */ current = stk ? stk->baselen : -1; strbuf_setlen(&dir->basebuf, current < 0 ? 0 : current); + if (dir->untracked) + untracked = stk ? stk->ucd : dir->untracked->root; + else + untracked = NULL; + while (current < baselen) { const char *cp; + struct sha1_stat sha1_stat; stk = xcalloc(1, sizeof(*stk)); if (current < 0) { @@ -886,10 +1036,15 @@ static void prep_exclude(struct dir_struct *dir, const char *base, int baselen) if (!cp) die("oops in prep_exclude"); cp++; + untracked = + lookup_untracked(dir->untracked, untracked, + base + current, + cp - base - current); } stk->prev = dir->exclude_stack; stk->baselen = cp - base; stk->exclude_ix = group->nr; + stk->ucd = untracked; el = add_exclude_list(dir, EXC_DIRS, NULL); strbuf_add(&dir->basebuf, base + current, stk->baselen - current); assert(stk->baselen == dir->basebuf.len); @@ -912,7 +1067,23 @@ static void prep_exclude(struct dir_struct *dir, const char *base, int baselen) } /* Try to read per-directory file */ - if (dir->exclude_per_dir) { + hashclr(sha1_stat.sha1); + sha1_stat.valid = 0; + if (dir->exclude_per_dir && + /* + * If we know that no files have been added in + * this directory (i.e. valid_cached_dir() has + * been executed and set untracked->valid) .. + */ + (!untracked || !untracked->valid || + /* + * .. and .gitignore does not exist before + * (i.e. null exclude_sha1 and skip_worktree is + * not set). Then we can skip loading .gitignore, + * which would result in ENOENT anyway. + * skip_worktree is taken care in read_directory() + */ + !is_null_sha1(untracked->exclude_sha1))) { /* * dir->basebuf gets reused by the traversal, but we * need fname to remain unchanged to ensure the src @@ -925,8 +1096,27 @@ static void prep_exclude(struct dir_struct *dir, const char *base, int baselen) strbuf_addbuf(&sb, &dir->basebuf); strbuf_addstr(&sb, dir->exclude_per_dir); el->src = strbuf_detach(&sb, NULL); - add_excludes_from_file_to_list(el->src, el->src, - stk->baselen, el, 1); + add_excludes(el->src, el->src, stk->baselen, el, 1, + untracked ? &sha1_stat : NULL); + } + /* + * NEEDSWORK: when untracked cache is enabled, prep_exclude() + * will first be called in valid_cached_dir() then maybe many + * times more in last_exclude_matching(). When the cache is + * used, last_exclude_matching() will not be called and + * reading .gitignore content will be a waste. + * + * So when it's called by valid_cached_dir() and we can get + * .gitignore SHA-1 from the index (i.e. .gitignore is not + * modified on work tree), we could delay reading the + * .gitignore content until we absolutely need it in + * last_exclude_matching(). Be careful about ignore rule + * order, though, if you do that. + */ + if (untracked && + hashcmp(sha1_stat.sha1, untracked->exclude_sha1)) { + invalidate_gitignore(dir->untracked, untracked); + hashcpy(untracked->exclude_sha1, sha1_stat.sha1); } dir->exclude_stack = stk; current = stk->baselen; @@ -1107,6 +1297,7 @@ static enum exist_status directory_exists_in_index(const char *dirname, int len) * (c) otherwise, we recurse into it. */ static enum path_treatment treat_directory(struct dir_struct *dir, + struct untracked_cache_dir *untracked, const char *dirname, int len, int exclude, const struct path_simplify *simplify) { @@ -1134,7 +1325,9 @@ static enum path_treatment treat_directory(struct dir_struct *dir, if (!(dir->flags & DIR_HIDE_EMPTY_DIRECTORIES)) return exclude ? path_excluded : path_untracked; - return read_directory_recursive(dir, dirname, len, 1, simplify); + untracked = lookup_untracked(dir->untracked, untracked, dirname, len); + return read_directory_recursive(dir, dirname, len, + untracked, 1, simplify); } /* @@ -1250,6 +1443,7 @@ static int get_dtype(struct dirent *de, const char *path, int len) } static enum path_treatment treat_one_path(struct dir_struct *dir, + struct untracked_cache_dir *untracked, struct strbuf *path, const struct path_simplify *simplify, int dtype, struct dirent *de) @@ -1302,7 +1496,7 @@ static enum path_treatment treat_one_path(struct dir_struct *dir, return path_none; case DT_DIR: strbuf_addch(path, '/'); - return treat_directory(dir, path->buf, path->len, exclude, + return treat_directory(dir, untracked, path->buf, path->len, exclude, simplify); case DT_REG: case DT_LNK: @@ -1310,14 +1504,52 @@ static enum path_treatment treat_one_path(struct dir_struct *dir, } } +static enum path_treatment treat_path_fast(struct dir_struct *dir, + struct untracked_cache_dir *untracked, + struct cached_dir *cdir, + struct strbuf *path, + int baselen, + const struct path_simplify *simplify) +{ + strbuf_setlen(path, baselen); + if (!cdir->ucd) { + strbuf_addstr(path, cdir->file); + return path_untracked; + } + strbuf_addstr(path, cdir->ucd->name); + /* treat_one_path() does this before it calls treat_directory() */ + if (path->buf[path->len - 1] != '/') + strbuf_addch(path, '/'); + if (cdir->ucd->check_only) + /* + * check_only is set as a result of treat_directory() getting + * to its bottom. Verify again the same set of directories + * with check_only set. + */ + return read_directory_recursive(dir, path->buf, path->len, + cdir->ucd, 1, simplify); + /* + * We get path_recurse in the first run when + * directory_exists_in_index() returns index_nonexistent. We + * are sure that new changes in the index does not impact the + * outcome. Return now. + */ + return path_recurse; +} + static enum path_treatment treat_path(struct dir_struct *dir, - struct dirent *de, + struct untracked_cache_dir *untracked, + struct cached_dir *cdir, struct strbuf *path, int baselen, const struct path_simplify *simplify) { int dtype; + struct dirent *de = cdir->de; + if (!de) + return treat_path_fast(dir, untracked, cdir, path, + baselen, simplify); if (is_dot_or_dotdot(de->d_name) || !strcmp(de->d_name, ".git")) return path_none; strbuf_setlen(path, baselen); @@ -1326,7 +1558,121 @@ static enum path_treatment treat_path(struct dir_struct *dir, return path_none; dtype = DTYPE(de); - return treat_one_path(dir, path, simplify, dtype, de); + return treat_one_path(dir, untracked, path, simplify, dtype, de); +} + +static void add_untracked(struct untracked_cache_dir *dir, const char *name) +{ + if (!dir) + return; + ALLOC_GROW(dir->untracked, dir->untracked_nr + 1, + dir->untracked_alloc); + dir->untracked[dir->untracked_nr++] = xstrdup(name); +} + +static int valid_cached_dir(struct dir_struct *dir, + struct untracked_cache_dir *untracked, + struct strbuf *path, + int check_only) +{ + struct stat st; + + if (!untracked) + return 0; + + if (stat(path->len ? path->buf : ".", &st)) { + invalidate_directory(dir->untracked, untracked); + memset(&untracked->stat_data, 0, sizeof(untracked->stat_data)); + return 0; + } + if (!untracked->valid || + match_stat_data_racy(&the_index, &untracked->stat_data, &st)) { + if (untracked->valid) + invalidate_directory(dir->untracked, untracked); + fill_stat_data(&untracked->stat_data, &st); + return 0; + } + + if (untracked->check_only != !!check_only) { + invalidate_directory(dir->untracked, untracked); + return 0; + } + + /* + * prep_exclude will be called eventually on this directory, + * but it's called much later in last_exclude_matching(). We + * need it now to determine the validity of the cache for this + * path. The next calls will be nearly no-op, the way + * prep_exclude() is designed. + */ + if (path->len && path->buf[path->len - 1] != '/') { + strbuf_addch(path, '/'); + prep_exclude(dir, path->buf, path->len); + strbuf_setlen(path, path->len - 1); + } else + prep_exclude(dir, path->buf, path->len); + + /* hopefully prep_exclude() haven't invalidated this entry... */ + return untracked->valid; +} + +static int open_cached_dir(struct cached_dir *cdir, + struct dir_struct *dir, + struct untracked_cache_dir *untracked, + struct strbuf *path, + int check_only) +{ + memset(cdir, 0, sizeof(*cdir)); + cdir->untracked = untracked; + if (valid_cached_dir(dir, untracked, path, check_only)) + return 0; + cdir->fdir = opendir(path->len ? path->buf : "."); + if (dir->untracked) + dir->untracked->dir_opened++; + if (!cdir->fdir) + return -1; + return 0; +} + +static int read_cached_dir(struct cached_dir *cdir) +{ + if (cdir->fdir) { + cdir->de = readdir(cdir->fdir); + if (!cdir->de) + return -1; + return 0; + } + while (cdir->nr_dirs < cdir->untracked->dirs_nr) { + struct untracked_cache_dir *d = cdir->untracked->dirs[cdir->nr_dirs]; + if (!d->recurse) { + cdir->nr_dirs++; + continue; + } + cdir->ucd = d; + cdir->nr_dirs++; + return 0; + } + cdir->ucd = NULL; + if (cdir->nr_files < cdir->untracked->untracked_nr) { + struct untracked_cache_dir *d = cdir->untracked; + cdir->file = d->untracked[cdir->nr_files++]; + return 0; + } + return -1; +} + +static void close_cached_dir(struct cached_dir *cdir) +{ + if (cdir->fdir) + closedir(cdir->fdir); + /* + * We have gone through this directory and found no untracked + * entries. Mark it valid. + */ + if (cdir->untracked) { + cdir->untracked->valid = 1; + cdir->untracked->recurse = 1; + } } /* @@ -1342,38 +1688,48 @@ static enum path_treatment treat_path(struct dir_struct *dir, */ static enum path_treatment read_directory_recursive(struct dir_struct *dir, const char *base, int baselen, - int check_only, + struct untracked_cache_dir *untracked, int check_only, const struct path_simplify *simplify) { - DIR *fdir; + struct cached_dir cdir; enum path_treatment state, subdir_state, dir_state = path_none; - struct dirent *de; struct strbuf path = STRBUF_INIT; strbuf_add(&path, base, baselen); - fdir = opendir(path.len ? path.buf : "."); - if (!fdir) + if (open_cached_dir(&cdir, dir, untracked, &path, check_only)) goto out; - while ((de = readdir(fdir)) != NULL) { + if (untracked) + untracked->check_only = !!check_only; + + while (!read_cached_dir(&cdir)) { /* check how the file or directory should be treated */ - state = treat_path(dir, de, &path, baselen, simplify); + state = treat_path(dir, untracked, &cdir, &path, baselen, simplify); + if (state > dir_state) dir_state = state; /* recurse into subdir if instructed by treat_path */ if (state == path_recurse) { - subdir_state = read_directory_recursive(dir, path.buf, - path.len, check_only, simplify); + struct untracked_cache_dir *ud; + ud = lookup_untracked(dir->untracked, untracked, + path.buf + baselen, + path.len - baselen); + subdir_state = + read_directory_recursive(dir, path.buf, path.len, + ud, check_only, simplify); if (subdir_state > dir_state) dir_state = subdir_state; } if (check_only) { /* abort early if maximum state has been reached */ - if (dir_state == path_untracked) + if (dir_state == path_untracked) { + if (cdir.fdir) + add_untracked(untracked, path.buf + baselen); break; + } /* skip the dir_add_* part */ continue; } @@ -1391,15 +1747,18 @@ static enum path_treatment read_directory_recursive(struct dir_struct *dir, break; case path_untracked: - if (!(dir->flags & DIR_SHOW_IGNORED)) - dir_add_name(dir, path.buf, path.len); + if (dir->flags & DIR_SHOW_IGNORED) + break; + dir_add_name(dir, path.buf, path.len); + if (cdir.fdir) + add_untracked(untracked, path.buf + baselen); break; default: break; } } - closedir(fdir); + close_cached_dir(&cdir); out: strbuf_release(&path); @@ -1469,7 +1828,7 @@ static int treat_leading_path(struct dir_struct *dir, break; if (simplify_away(sb.buf, sb.len, simplify)) break; - if (treat_one_path(dir, &sb, simplify, + if (treat_one_path(dir, NULL, &sb, simplify, DT_DIR, NULL) == path_none) break; /* do not recurse into it */ if (len <= baselen) { @@ -1482,9 +1841,139 @@ static int treat_leading_path(struct dir_struct *dir, return rc; } +static const char *get_ident_string(void) +{ + static struct strbuf sb = STRBUF_INIT; + struct utsname uts; + + if (sb.len) + return sb.buf; + if (uname(&uts)) + die_errno(_("failed to get kernel name and information")); + strbuf_addf(&sb, "Location %s, system %s %s %s", get_git_work_tree(), + uts.sysname, uts.release, uts.version); + return sb.buf; +} + +static int ident_in_untracked(const struct untracked_cache *uc) +{ + const char *end = uc->ident.buf + uc->ident.len; + const char *p = uc->ident.buf; + + for (p = uc->ident.buf; p < end; p += strlen(p) + 1) + if (!strcmp(p, get_ident_string())) + return 1; + return 0; +} + +void add_untracked_ident(struct untracked_cache *uc) +{ + if (ident_in_untracked(uc)) + return; + strbuf_addstr(&uc->ident, get_ident_string()); + /* this strbuf contains a list of strings, save NUL too */ + strbuf_addch(&uc->ident, 0); +} + +static struct untracked_cache_dir *validate_untracked_cache(struct dir_struct *dir, + int base_len, + const struct pathspec *pathspec) +{ + struct untracked_cache_dir *root; + int i; + + if (!dir->untracked || getenv("GIT_DISABLE_UNTRACKED_CACHE")) + return NULL; + + /* + * We only support $GIT_DIR/info/exclude and core.excludesfile + * as the global ignore rule files. Any other additions + * (e.g. from command line) invalidate the cache. This + * condition also catches running setup_standard_excludes() + * before setting dir->untracked! + */ + if (dir->unmanaged_exclude_files) + return NULL; + + /* + * Optimize for the main use case only: whole-tree git + * status. More work involved in treat_leading_path() if we + * use cache on just a subset of the worktree. pathspec + * support could make the matter even worse. + */ + if (base_len || (pathspec && pathspec->nr)) + return NULL; + + /* Different set of flags may produce different results */ + if (dir->flags != dir->untracked->dir_flags || + /* + * See treat_directory(), case index_nonexistent. Without + * this flag, we may need to also cache .git file content + * for the resolve_gitlink_ref() call, which we don't. + */ + !(dir->flags & DIR_SHOW_OTHER_DIRECTORIES) || + /* We don't support collecting ignore files */ + (dir->flags & (DIR_SHOW_IGNORED | DIR_SHOW_IGNORED_TOO | + DIR_COLLECT_IGNORED))) + return NULL; + + /* + * If we use .gitignore in the cache and now you change it to + * .gitexclude, everything will go wrong. + */ + if (dir->exclude_per_dir != dir->untracked->exclude_per_dir && + strcmp(dir->exclude_per_dir, dir->untracked->exclude_per_dir)) + return NULL; + + /* + * EXC_CMDL is not considered in the cache. If people set it, + * skip the cache. + */ + if (dir->exclude_list_group[EXC_CMDL].nr) + return NULL; + + /* + * An optimization in prep_exclude() does not play well with + * CE_SKIP_WORKTREE. It's a rare case anyway, if a single + * entry has that bit set, disable the whole untracked cache. + */ + for (i = 0; i < active_nr; i++) + if (ce_skip_worktree(active_cache[i])) + return NULL; + + if (!ident_in_untracked(dir->untracked)) { + warning(_("Untracked cache is disabled on this system.")); + return NULL; + } + + if (!dir->untracked->root) { + const int len = sizeof(*dir->untracked->root); + dir->untracked->root = xmalloc(len); + memset(dir->untracked->root, 0, len); + } + + /* Validate $GIT_DIR/info/exclude and core.excludesfile */ + root = dir->untracked->root; + if (hashcmp(dir->ss_info_exclude.sha1, + dir->untracked->ss_info_exclude.sha1)) { + invalidate_gitignore(dir->untracked, root); + dir->untracked->ss_info_exclude = dir->ss_info_exclude; + } + if (hashcmp(dir->ss_excludes_file.sha1, + dir->untracked->ss_excludes_file.sha1)) { + invalidate_gitignore(dir->untracked, root); + dir->untracked->ss_excludes_file = dir->ss_excludes_file; + } + + /* Make sure this directory is not dropped out at saving phase */ + root->recurse = 1; + return root; +} + int read_directory(struct dir_struct *dir, const char *path, int len, const struct pathspec *pathspec) { struct path_simplify *simplify; + struct untracked_cache_dir *untracked; /* * Check out create_simplify() @@ -1508,11 +1997,39 @@ int read_directory(struct dir_struct *dir, const char *path, int len, const stru * create_simplify(). */ simplify = create_simplify(pathspec ? pathspec->_raw : NULL); + untracked = validate_untracked_cache(dir, len, pathspec); + if (!untracked) + /* + * make sure untracked cache code path is disabled, + * e.g. prep_exclude() + */ + dir->untracked = NULL; if (!len || treat_leading_path(dir, path, len, simplify)) - read_directory_recursive(dir, path, len, 0, simplify); + read_directory_recursive(dir, path, len, untracked, 0, simplify); free_simplify(simplify); qsort(dir->entries, dir->nr, sizeof(struct dir_entry *), cmp_name); qsort(dir->ignored, dir->ignored_nr, sizeof(struct dir_entry *), cmp_name); + if (dir->untracked) { + static struct trace_key trace_untracked_stats = TRACE_KEY_INIT(UNTRACKED_STATS); + trace_printf_key(&trace_untracked_stats, + "node creation: %u\n" + "gitignore invalidation: %u\n" + "directory invalidation: %u\n" + "opendir: %u\n", + dir->untracked->dir_created, + dir->untracked->gitignore_invalidated, + dir->untracked->dir_invalidated, + dir->untracked->dir_opened); + if (dir->untracked == the_index.untracked && + (dir->untracked->dir_opened || + dir->untracked->gitignore_invalidated || + dir->untracked->dir_invalidated)) + the_index.cache_changed |= UNTRACKED_CHANGED; + if (dir->untracked != the_index.untracked) { + free(dir->untracked); + dir->untracked = NULL; + } + } return dir->nr; } @@ -1678,12 +2195,14 @@ void setup_standard_excludes(struct dir_struct *dir) if (!excludes_file) excludes_file = xdg_config_home("ignore"); if (excludes_file && !access_or_warn(excludes_file, R_OK, 0)) - add_excludes_from_file(dir, excludes_file); + add_excludes_from_file_1(dir, excludes_file, + dir->untracked ? &dir->ss_excludes_file : NULL); /* per repository user preference */ path = git_path("info/exclude"); if (!access_or_warn(path, R_OK, 0)) - add_excludes_from_file(dir, path); + add_excludes_from_file_1(dir, path, + dir->untracked ? &dir->ss_info_exclude : NULL); } int remove_path(const char *name) @@ -1735,3 +2254,404 @@ void clear_directory(struct dir_struct *dir) } strbuf_release(&dir->basebuf); } + +struct ondisk_untracked_cache { + struct stat_data info_exclude_stat; + struct stat_data excludes_file_stat; + uint32_t dir_flags; + unsigned char info_exclude_sha1[20]; + unsigned char excludes_file_sha1[20]; + char exclude_per_dir[FLEX_ARRAY]; +}; + +#define ouc_size(len) (offsetof(struct ondisk_untracked_cache, exclude_per_dir) + len + 1) + +struct write_data { + int index; /* number of written untracked_cache_dir */ + struct ewah_bitmap *check_only; /* from untracked_cache_dir */ + struct ewah_bitmap *valid; /* from untracked_cache_dir */ + struct ewah_bitmap *sha1_valid; /* set if exclude_sha1 is not null */ + struct strbuf out; + struct strbuf sb_stat; + struct strbuf sb_sha1; +}; + +static void stat_data_to_disk(struct stat_data *to, const struct stat_data *from) +{ + to->sd_ctime.sec = htonl(from->sd_ctime.sec); + to->sd_ctime.nsec = htonl(from->sd_ctime.nsec); + to->sd_mtime.sec = htonl(from->sd_mtime.sec); + to->sd_mtime.nsec = htonl(from->sd_mtime.nsec); + to->sd_dev = htonl(from->sd_dev); + to->sd_ino = htonl(from->sd_ino); + to->sd_uid = htonl(from->sd_uid); + to->sd_gid = htonl(from->sd_gid); + to->sd_size = htonl(from->sd_size); +} + +static void write_one_dir(struct untracked_cache_dir *untracked, + struct write_data *wd) +{ + struct stat_data stat_data; + struct strbuf *out = &wd->out; + unsigned char intbuf[16]; + unsigned int intlen, value; + int i = wd->index++; + + /* + * untracked_nr should be reset whenever valid is clear, but + * for safety.. + */ + if (!untracked->valid) { + untracked->untracked_nr = 0; + untracked->check_only = 0; + } + + if (untracked->check_only) + ewah_set(wd->check_only, i); + if (untracked->valid) { + ewah_set(wd->valid, i); + stat_data_to_disk(&stat_data, &untracked->stat_data); + strbuf_add(&wd->sb_stat, &stat_data, sizeof(stat_data)); + } + if (!is_null_sha1(untracked->exclude_sha1)) { + ewah_set(wd->sha1_valid, i); + strbuf_add(&wd->sb_sha1, untracked->exclude_sha1, 20); + } + + intlen = encode_varint(untracked->untracked_nr, intbuf); + strbuf_add(out, intbuf, intlen); + + /* skip non-recurse directories */ + for (i = 0, value = 0; i < untracked->dirs_nr; i++) + if (untracked->dirs[i]->recurse) + value++; + intlen = encode_varint(value, intbuf); + strbuf_add(out, intbuf, intlen); + + strbuf_add(out, untracked->name, strlen(untracked->name) + 1); + + for (i = 0; i < untracked->untracked_nr; i++) + strbuf_add(out, untracked->untracked[i], + strlen(untracked->untracked[i]) + 1); + + for (i = 0; i < untracked->dirs_nr; i++) + if (untracked->dirs[i]->recurse) + write_one_dir(untracked->dirs[i], wd); +} + +void write_untracked_extension(struct strbuf *out, struct untracked_cache *untracked) +{ + struct ondisk_untracked_cache *ouc; + struct write_data wd; + unsigned char varbuf[16]; + int len = 0, varint_len; + if (untracked->exclude_per_dir) + len = strlen(untracked->exclude_per_dir); + ouc = xmalloc(sizeof(*ouc) + len + 1); + stat_data_to_disk(&ouc->info_exclude_stat, &untracked->ss_info_exclude.stat); + stat_data_to_disk(&ouc->excludes_file_stat, &untracked->ss_excludes_file.stat); + hashcpy(ouc->info_exclude_sha1, untracked->ss_info_exclude.sha1); + hashcpy(ouc->excludes_file_sha1, untracked->ss_excludes_file.sha1); + ouc->dir_flags = htonl(untracked->dir_flags); + memcpy(ouc->exclude_per_dir, untracked->exclude_per_dir, len + 1); + + varint_len = encode_varint(untracked->ident.len, varbuf); + strbuf_add(out, varbuf, varint_len); + strbuf_add(out, untracked->ident.buf, untracked->ident.len); + + strbuf_add(out, ouc, ouc_size(len)); + free(ouc); + ouc = NULL; + + if (!untracked->root) { + varint_len = encode_varint(0, varbuf); + strbuf_add(out, varbuf, varint_len); + return; + } + + wd.index = 0; + wd.check_only = ewah_new(); + wd.valid = ewah_new(); + wd.sha1_valid = ewah_new(); + strbuf_init(&wd.out, 1024); + strbuf_init(&wd.sb_stat, 1024); + strbuf_init(&wd.sb_sha1, 1024); + write_one_dir(untracked->root, &wd); + + varint_len = encode_varint(wd.index, varbuf); + strbuf_add(out, varbuf, varint_len); + strbuf_addbuf(out, &wd.out); + ewah_serialize_strbuf(wd.valid, out); + ewah_serialize_strbuf(wd.check_only, out); + ewah_serialize_strbuf(wd.sha1_valid, out); + strbuf_addbuf(out, &wd.sb_stat); + strbuf_addbuf(out, &wd.sb_sha1); + strbuf_addch(out, '\0'); /* safe guard for string lists */ + + ewah_free(wd.valid); + ewah_free(wd.check_only); + ewah_free(wd.sha1_valid); + strbuf_release(&wd.out); + strbuf_release(&wd.sb_stat); + strbuf_release(&wd.sb_sha1); +} + +static void free_untracked(struct untracked_cache_dir *ucd) +{ + int i; + if (!ucd) + return; + for (i = 0; i < ucd->dirs_nr; i++) + free_untracked(ucd->dirs[i]); + for (i = 0; i < ucd->untracked_nr; i++) + free(ucd->untracked[i]); + free(ucd->untracked); + free(ucd->dirs); + free(ucd); +} + +void free_untracked_cache(struct untracked_cache *uc) +{ + if (uc) + free_untracked(uc->root); + free(uc); +} + +struct read_data { + int index; + struct untracked_cache_dir **ucd; + struct ewah_bitmap *check_only; + struct ewah_bitmap *valid; + struct ewah_bitmap *sha1_valid; + const unsigned char *data; + const unsigned char *end; +}; + +static void stat_data_from_disk(struct stat_data *to, const struct stat_data *from) +{ + to->sd_ctime.sec = get_be32(&from->sd_ctime.sec); + to->sd_ctime.nsec = get_be32(&from->sd_ctime.nsec); + to->sd_mtime.sec = get_be32(&from->sd_mtime.sec); + to->sd_mtime.nsec = get_be32(&from->sd_mtime.nsec); + to->sd_dev = get_be32(&from->sd_dev); + to->sd_ino = get_be32(&from->sd_ino); + to->sd_uid = get_be32(&from->sd_uid); + to->sd_gid = get_be32(&from->sd_gid); + to->sd_size = get_be32(&from->sd_size); +} + +static int read_one_dir(struct untracked_cache_dir **untracked_, + struct read_data *rd) +{ + struct untracked_cache_dir ud, *untracked; + const unsigned char *next, *data = rd->data, *end = rd->end; + unsigned int value; + int i, len; + + memset(&ud, 0, sizeof(ud)); + + next = data; + value = decode_varint(&next); + if (next > end) + return -1; + ud.recurse = 1; + ud.untracked_alloc = value; + ud.untracked_nr = value; + if (ud.untracked_nr) + ud.untracked = xmalloc(sizeof(*ud.untracked) * ud.untracked_nr); + data = next; + + next = data; + ud.dirs_alloc = ud.dirs_nr = decode_varint(&next); + if (next > end) + return -1; + ud.dirs = xmalloc(sizeof(*ud.dirs) * ud.dirs_nr); + data = next; + + len = strlen((const char *)data); + next = data + len + 1; + if (next > rd->end) + return -1; + *untracked_ = untracked = xmalloc(sizeof(*untracked) + len); + memcpy(untracked, &ud, sizeof(ud)); + memcpy(untracked->name, data, len + 1); + data = next; + + for (i = 0; i < untracked->untracked_nr; i++) { + len = strlen((const char *)data); + next = data + len + 1; + if (next > rd->end) + return -1; + untracked->untracked[i] = xstrdup((const char*)data); + data = next; + } + + rd->ucd[rd->index++] = untracked; + rd->data = data; + + for (i = 0; i < untracked->dirs_nr; i++) { + len = read_one_dir(untracked->dirs + i, rd); + if (len < 0) + return -1; + } + return 0; +} + +static void set_check_only(size_t pos, void *cb) +{ + struct read_data *rd = cb; + struct untracked_cache_dir *ud = rd->ucd[pos]; + ud->check_only = 1; +} + +static void read_stat(size_t pos, void *cb) +{ + struct read_data *rd = cb; + struct untracked_cache_dir *ud = rd->ucd[pos]; + if (rd->data + sizeof(struct stat_data) > rd->end) { + rd->data = rd->end + 1; + return; + } + stat_data_from_disk(&ud->stat_data, (struct stat_data *)rd->data); + rd->data += sizeof(struct stat_data); + ud->valid = 1; +} + +static void read_sha1(size_t pos, void *cb) +{ + struct read_data *rd = cb; + struct untracked_cache_dir *ud = rd->ucd[pos]; + if (rd->data + 20 > rd->end) { + rd->data = rd->end + 1; + return; + } + hashcpy(ud->exclude_sha1, rd->data); + rd->data += 20; +} + +static void load_sha1_stat(struct sha1_stat *sha1_stat, + const struct stat_data *stat, + const unsigned char *sha1) +{ + stat_data_from_disk(&sha1_stat->stat, stat); + hashcpy(sha1_stat->sha1, sha1); + sha1_stat->valid = 1; +} + +struct untracked_cache *read_untracked_extension(const void *data, unsigned long sz) +{ + const struct ondisk_untracked_cache *ouc; + struct untracked_cache *uc; + struct read_data rd; + const unsigned char *next = data, *end = (const unsigned char *)data + sz; + const char *ident; + int ident_len, len; + + if (sz <= 1 || end[-1] != '\0') + return NULL; + end--; + + ident_len = decode_varint(&next); + if (next + ident_len > end) + return NULL; + ident = (const char *)next; + next += ident_len; + + ouc = (const struct ondisk_untracked_cache *)next; + if (next + ouc_size(0) > end) + return NULL; + + uc = xcalloc(1, sizeof(*uc)); + strbuf_init(&uc->ident, ident_len); + strbuf_add(&uc->ident, ident, ident_len); + load_sha1_stat(&uc->ss_info_exclude, &ouc->info_exclude_stat, + ouc->info_exclude_sha1); + load_sha1_stat(&uc->ss_excludes_file, &ouc->excludes_file_stat, + ouc->excludes_file_sha1); + uc->dir_flags = get_be32(&ouc->dir_flags); + uc->exclude_per_dir = xstrdup(ouc->exclude_per_dir); + /* NUL after exclude_per_dir is covered by sizeof(*ouc) */ + next += ouc_size(strlen(ouc->exclude_per_dir)); + if (next >= end) + goto done2; + + len = decode_varint(&next); + if (next > end || len == 0) + goto done2; + + rd.valid = ewah_new(); + rd.check_only = ewah_new(); + rd.sha1_valid = ewah_new(); + rd.data = next; + rd.end = end; + rd.index = 0; + rd.ucd = xmalloc(sizeof(*rd.ucd) * len); + + if (read_one_dir(&uc->root, &rd) || rd.index != len) + goto done; + + next = rd.data; + len = ewah_read_mmap(rd.valid, next, end - next); + if (len < 0) + goto done; + + next += len; + len = ewah_read_mmap(rd.check_only, next, end - next); + if (len < 0) + goto done; + + next += len; + len = ewah_read_mmap(rd.sha1_valid, next, end - next); + if (len < 0) + goto done; + + ewah_each_bit(rd.check_only, set_check_only, &rd); + rd.data = next + len; + ewah_each_bit(rd.valid, read_stat, &rd); + ewah_each_bit(rd.sha1_valid, read_sha1, &rd); + next = rd.data; + +done: + free(rd.ucd); + ewah_free(rd.valid); + ewah_free(rd.check_only); + ewah_free(rd.sha1_valid); +done2: + if (next != end) { + free_untracked_cache(uc); + uc = NULL; + } + return uc; +} + +void untracked_cache_invalidate_path(struct index_state *istate, + const char *path) +{ + const char *sep; + struct untracked_cache_dir *d; + if (!istate->untracked || !istate->untracked->root) + return; + sep = strrchr(path, '/'); + if (sep) + d = lookup_untracked(istate->untracked, + istate->untracked->root, + path, sep - path); + else + d = istate->untracked->root; + istate->untracked->dir_invalidated++; + d->valid = 0; + d->untracked_nr = 0; +} + +void untracked_cache_remove_from_index(struct index_state *istate, + const char *path) +{ + untracked_cache_invalidate_path(istate, path); +} + +void untracked_cache_add_to_index(struct index_state *istate, + const char *path) +{ + untracked_cache_invalidate_path(istate, path); +} @@ -66,6 +66,7 @@ struct exclude_stack { struct exclude_stack *prev; /* the struct exclude_stack for the parent directory */ int baselen; int exclude_ix; /* index of exclude_list within EXC_DIRS exclude_list_group */ + struct untracked_cache_dir *ucd; }; struct exclude_list_group { @@ -73,6 +74,73 @@ struct exclude_list_group { struct exclude_list *el; }; +struct sha1_stat { + struct stat_data stat; + unsigned char sha1[20]; + int valid; +}; + +/* + * Untracked cache + * + * The following inputs are sufficient to determine what files in a + * directory are excluded: + * + * - The list of files and directories of the directory in question + * - The $GIT_DIR/index + * - dir_struct flags + * - The content of $GIT_DIR/info/exclude + * - The content of core.excludesfile + * - The content (or the lack) of .gitignore of all parent directories + * from $GIT_WORK_TREE + * - The check_only flag in read_directory_recursive (for + * DIR_HIDE_EMPTY_DIRECTORIES) + * + * The first input can be checked using directory mtime. In many + * filesystems, directory mtime (stat_data field) is updated when its + * files or direct subdirs are added or removed. + * + * The second one can be hooked from cache_tree_invalidate_path(). + * Whenever a file (or a submodule) is added or removed from a + * directory, we invalidate that directory. + * + * The remaining inputs are easy, their SHA-1 could be used to verify + * their contents (exclude_sha1[], info_exclude_sha1[] and + * excludes_file_sha1[]) + */ +struct untracked_cache_dir { + struct untracked_cache_dir **dirs; + char **untracked; + struct stat_data stat_data; + unsigned int untracked_alloc, dirs_nr, dirs_alloc; + unsigned int untracked_nr; + unsigned int check_only : 1; + /* all data except 'dirs' in this struct are good */ + unsigned int valid : 1; + unsigned int recurse : 1; + /* null SHA-1 means this directory does not have .gitignore */ + unsigned char exclude_sha1[20]; + char name[FLEX_ARRAY]; +}; + +struct untracked_cache { + struct sha1_stat ss_info_exclude; + struct sha1_stat ss_excludes_file; + const char *exclude_per_dir; + struct strbuf ident; + /* + * dir_struct#flags must match dir_flags or the untracked + * cache is ignored. + */ + unsigned dir_flags; + struct untracked_cache_dir *root; + /* Statistics */ + int dir_created; + int gitignore_invalidated; + int dir_invalidated; + int dir_opened; +}; + struct dir_struct { int nr, alloc; int ignored_nr, ignored_alloc; @@ -120,6 +188,12 @@ struct dir_struct { struct exclude_stack *exclude_stack; struct exclude *exclude; struct strbuf basebuf; + + /* Enable untracked file cache if set */ + struct untracked_cache *untracked; + struct sha1_stat ss_info_exclude; + struct sha1_stat ss_excludes_file; + unsigned unmanaged_exclude_files; }; /* @@ -226,4 +300,12 @@ static inline int dir_path_match(const struct dir_entry *ent, has_trailing_dir); } +void untracked_cache_invalidate_path(struct index_state *, const char *); +void untracked_cache_remove_from_index(struct index_state *, const char *); +void untracked_cache_add_to_index(struct index_state *, const char *); + +void free_untracked_cache(struct untracked_cache *); +struct untracked_cache *read_untracked_extension(const void *data, unsigned long sz); +void write_untracked_extension(struct strbuf *out, struct untracked_cache *untracked); +void add_untracked_ident(struct untracked_cache *); #endif diff --git a/ewah/ewah_io.c b/ewah/ewah_io.c index 1c2d7afd4c..43481b9c60 100644 --- a/ewah/ewah_io.c +++ b/ewah/ewah_io.c @@ -19,6 +19,7 @@ */ #include "git-compat-util.h" #include "ewok.h" +#include "strbuf.h" int ewah_serialize_native(struct ewah_bitmap *self, int fd) { @@ -110,6 +111,18 @@ int ewah_serialize(struct ewah_bitmap *self, int fd) return ewah_serialize_to(self, write_helper, (void *)(intptr_t)fd); } +static int write_strbuf(void *user_data, const void *data, size_t len) +{ + struct strbuf *sb = user_data; + strbuf_add(sb, data, len); + return len; +} + +int ewah_serialize_strbuf(struct ewah_bitmap *self, struct strbuf *sb) +{ + return ewah_serialize_to(self, write_strbuf, sb); +} + int ewah_read_mmap(struct ewah_bitmap *self, const void *map, size_t len) { const uint8_t *ptr = map; diff --git a/ewah/ewok.h b/ewah/ewok.h index 13c6e20412..e732525367 100644 --- a/ewah/ewok.h +++ b/ewah/ewok.h @@ -30,6 +30,7 @@ # define ewah_calloc xcalloc #endif +struct strbuf; typedef uint64_t eword_t; #define BITS_IN_WORD (sizeof(eword_t) * 8) @@ -98,6 +99,7 @@ int ewah_serialize_to(struct ewah_bitmap *self, void *out); int ewah_serialize(struct ewah_bitmap *self, int fd); int ewah_serialize_native(struct ewah_bitmap *self, int fd); +int ewah_serialize_strbuf(struct ewah_bitmap *self, struct strbuf *); int ewah_deserialize(struct ewah_bitmap *self, int fd); int ewah_read_mmap(struct ewah_bitmap *self, const void *map, size_t len); diff --git a/fetch-pack.c b/fetch-pack.c index a1dcb27f72..a912935a63 100644 --- a/fetch-pack.c +++ b/fetch-pack.c @@ -43,7 +43,12 @@ static int marked; #define MAX_IN_VAIN 256 static struct prio_queue rev_list = { compare_commits_by_commit_date }; -static int non_common_revs, multi_ack, use_sideband, allow_tip_sha1_in_want; +static int non_common_revs, multi_ack, use_sideband; +/* Allow specifying sha1 if it is a ref tip. */ +#define ALLOW_TIP_SHA1 01 +/* Allow request of a sha1 if it is reachable from a ref (possibly hidden ref). */ +#define ALLOW_REACHABLE_SHA1 02 +static unsigned int allow_unadvertised_object_request; static void rev_list_push(struct commit *commit, int mark) { @@ -555,7 +560,8 @@ static void filter_refs(struct fetch_pack_args *args, } /* Append unmatched requests to the list */ - if (allow_tip_sha1_in_want) { + if ((allow_unadvertised_object_request & + (ALLOW_TIP_SHA1 | ALLOW_REACHABLE_SHA1))) { for (i = 0; i < nr_sought; i++) { unsigned char sha1[20]; @@ -834,7 +840,12 @@ static struct ref *do_fetch_pack(struct fetch_pack_args *args, if (server_supports("allow-tip-sha1-in-want")) { if (args->verbose) fprintf(stderr, "Server supports allow-tip-sha1-in-want\n"); - allow_tip_sha1_in_want = 1; + allow_unadvertised_object_request |= ALLOW_TIP_SHA1; + } + if (server_supports("allow-reachable-sha1-in-want")) { + if (args->verbose) + fprintf(stderr, "Server supports allow-reachable-sha1-in-want\n"); + allow_unadvertised_object_request |= ALLOW_REACHABLE_SHA1; } if (!server_supports("thin-pack")) args->use_thin_pack = 0; diff --git a/generate-cmdlist.perl b/generate-cmdlist.perl new file mode 100755 index 0000000000..31516e36ac --- /dev/null +++ b/generate-cmdlist.perl @@ -0,0 +1,50 @@ +#!/usr/bin/perl +use strict; +use warnings; + +print <<"EOT"; +/* Automatically generated by $0 */ + +struct cmdname_help { + char name[16]; + char help[80]; + unsigned char group; +}; + +static char *common_cmd_groups[] = { +EOT + +my $n = 0; +my %grp; +while (<>) { + last if /^### command list/; + next if (1../^### common groups/) || /^#/ || /^\s*$/; + chop; + my ($k, $v) = split ' ', $_, 2; + $grp{$k} = $n++; + print "\tN_(\"$v\"),\n"; +} + +print "};\n\nstatic struct cmdname_help common_cmds[] = {\n"; + +while (<>) { + next if /^#/ || /^\s*$/; + my @tags = split; + my $cmd = shift @tags; + for my $t (@tags) { + if (exists $grp{$t}) { + my $s; + open my $f, '<', "Documentation/$cmd.txt" or die; + while (<$f>) { + ($s) = /^$cmd - (.+)$/; + last if $s; + } + close $f; + $cmd =~ s/^git-//; + print "\t{\"$cmd\", N_(\"$s\"), $grp{$t}},\n"; + last; + } + } +} + +print "};\n"; diff --git a/generate-cmdlist.sh b/generate-cmdlist.sh deleted file mode 100755 index 9a4c9b94e6..0000000000 --- a/generate-cmdlist.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/sh - -echo "/* Automatically generated by $0 */ -struct cmdname_help { - char name[16]; - char help[80]; -}; - -static struct cmdname_help common_cmds[] = {" - -sed -n -e 's/^git-\([^ ]*\)[ ].* common.*/\1/p' command-list.txt | -sort | -while read cmd -do - sed -n ' - /^NAME/,/git-'"$cmd"'/H - ${ - x - s/.*git-'"$cmd"' - \(.*\)/ {"'"$cmd"'", N_("\1")},/ - p - }' "Documentation/git-$cmd.txt" -done -echo "};" diff --git a/git-compat-util.h b/git-compat-util.h index b7a97fbe83..17584adbd0 100644 --- a/git-compat-util.h +++ b/git-compat-util.h @@ -189,6 +189,7 @@ #elif defined(_MSC_VER) #include "compat/msvc.h" #else +#include <sys/utsname.h> #include <sys/wait.h> #include <sys/resource.h> #include <sys/socket.h> diff --git a/git-mergetool--lib.sh b/git-mergetool--lib.sh index fe61e89f31..14b039de65 100644 --- a/git-mergetool--lib.sh +++ b/git-mergetool--lib.sh @@ -2,6 +2,9 @@ : ${MERGE_TOOLS_DIR=$(git --exec-path)/mergetools} +IFS=' +' + mode_ok () { if diff_mode then diff --git a/git-mergetool.sh b/git-mergetool.sh index d20581c15c..9f77e3a8bb 100755 --- a/git-mergetool.sh +++ b/git-mergetool.sh @@ -451,8 +451,6 @@ fi printf "Merging:\n" printf "%s\n" "$files" -IFS=' -' rc=0 for i in $files do diff --git a/git-pull.sh b/git-pull.sh index 9005171b74..0917d0d056 100755 --- a/git-pull.sh +++ b/git-pull.sh @@ -54,8 +54,11 @@ then fi # Setup default fast-forward options via `pull.ff` -pull_ff=$(git config pull.ff) +pull_ff=$(bool_or_string_config pull.ff) case "$pull_ff" in +true) + no_ff=--ff + ;; false) no_ff=--no-ff ;; @@ -81,8 +84,8 @@ do diffstat=--no-stat ;; --stat|--summary) diffstat=--stat ;; - --log|--no-log) - log_arg=$1 ;; + --log|--log=*|--no-log) + log_arg="$1" ;; --no-c|--no-co|--no-com|--no-comm|--no-commi|--no-commit) no_commit=--no-commit ;; --c|--co|--com|--comm|--commi|--commit) diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh index bab0dccc04..dc3133f681 100644 --- a/git-rebase--interactive.sh +++ b/git-rebase--interactive.sh @@ -502,7 +502,7 @@ do_pick () { } do_next () { - rm -f "$msg" "$author_script" "$amend" || exit + rm -f "$msg" "$author_script" "$amend" "$state_dir"/stopped-sha || exit read -r command sha1 rest < "$todo" case "$command" in "$comment_char"*|''|noop) @@ -592,9 +592,6 @@ do_next () { read -r command rest < "$todo" mark_action_done printf 'Executing: %s\n' "$rest" - # "exec" command doesn't take a sha1 in the todo-list. - # => can't just use $sha1 here. - git rev-parse --verify HEAD > "$state_dir"/stopped-sha ${SHELL:-@SHELL_PATH@} -c "$rest" # Actual execution status=$? # Run in subshell because require_clean_work_tree can die. @@ -890,7 +887,10 @@ first and then run 'git rebase --continue' again." fi fi - record_in_rewritten "$(cat "$state_dir"/stopped-sha)" + if test -r "$state_dir"/stopped-sha + then + record_in_rewritten "$(cat "$state_dir"/stopped-sha)" + fi require_clean_work_tree "rebase" do_rest diff --git a/git-stash.sh b/git-stash.sh index 7911f30c63..1f5ea877d7 100755 --- a/git-stash.sh +++ b/git-stash.sh @@ -219,6 +219,9 @@ save_stash () { -a|--all) untracked=all ;; + --help) + show_help + ;; --) shift break @@ -301,11 +304,17 @@ list_stash () { } show_stash () { + ALLOW_UNKNOWN_FLAGS=t assert_stash_like "$@" git diff ${FLAGS:---stat} $b_commit $w_commit } +show_help () { + exec git help stash + exit 1 +} + # # Parses the remaining options looking for flags and # at most one revision defaulting to ${ref_stash}@{0} @@ -332,13 +341,14 @@ show_stash () { # # GIT_QUIET is set to t if -q is specified # INDEX_OPTION is set to --index if --index is specified. -# FLAGS is set to the remaining flags +# FLAGS is set to the remaining flags (if allowed) # # dies if: # * too many revisions specified # * no revision is specified and there is no stash stack # * a revision is specified which cannot be resolve to a SHA1 # * a non-existent stash reference is specified +# * unknown flags were set and ALLOW_UNKNOWN_FLAGS is not "t" # parse_flags_and_rev() @@ -371,7 +381,12 @@ parse_flags_and_rev() --index) INDEX_OPTION=--index ;; + --help) + show_help + ;; -*) + test "$ALLOW_UNKNOWN_FLAGS" = t || + die "$(eval_gettext "unknown option: \$opt")" FLAGS="${FLAGS}${FLAGS:+ }$opt" ;; esac @@ -218,17 +218,39 @@ void list_commands(unsigned int colopts, } } +static int cmd_group_cmp(const void *elem1, const void *elem2) +{ + const struct cmdname_help *e1 = elem1; + const struct cmdname_help *e2 = elem2; + + if (e1->group < e2->group) + return -1; + if (e1->group > e2->group) + return 1; + return strcmp(e1->name, e2->name); +} + void list_common_cmds_help(void) { int i, longest = 0; + int current_grp = -1; for (i = 0; i < ARRAY_SIZE(common_cmds); i++) { if (longest < strlen(common_cmds[i].name)) longest = strlen(common_cmds[i].name); } - puts(_("The most commonly used git commands are:")); + qsort(common_cmds, ARRAY_SIZE(common_cmds), + sizeof(common_cmds[0]), cmd_group_cmp); + + puts(_("These are common Git commands used in various situations:")); + for (i = 0; i < ARRAY_SIZE(common_cmds); i++) { + if (common_cmds[i].group != current_grp) { + printf("\n%s\n", _(common_cmd_groups[common_cmds[i].group])); + current_grp = common_cmds[i].group; + } + printf(" %s ", common_cmds[i].name); mput_char(' ', longest - strlen(common_cmds[i].name)); puts(_(common_cmds[i].help)); diff --git a/http-backend.c b/http-backend.c index 55353ad66a..501bf797c0 100644 --- a/http-backend.c +++ b/http-backend.c @@ -13,18 +13,20 @@ static const char content_type[] = "Content-Type"; static const char content_length[] = "Content-Length"; static const char last_modified[] = "Last-Modified"; static int getanyfile = 1; +static unsigned long max_request_buffer = 10 * 1024 * 1024; static struct string_list *query_params; struct rpc_service { const char *name; const char *config_name; + unsigned buffer_input : 1; signed enabled : 2; }; static struct rpc_service rpc_service[] = { - { "upload-pack", "uploadpack", 1 }, - { "receive-pack", "receivepack", -1 }, + { "upload-pack", "uploadpack", 1, 1 }, + { "receive-pack", "receivepack", 0, -1 }, }; static struct string_list *get_parameters(void) @@ -225,6 +227,7 @@ static void http_config(void) struct strbuf var = STRBUF_INIT; git_config_get_bool("http.getanyfile", &getanyfile); + git_config_get_ulong("http.maxrequestbuffer", &max_request_buffer); for (i = 0; i < ARRAY_SIZE(rpc_service); i++) { struct rpc_service *svc = &rpc_service[i]; @@ -266,9 +269,52 @@ static struct rpc_service *select_service(const char *name) return svc; } -static void inflate_request(const char *prog_name, int out) +/* + * This is basically strbuf_read(), except that if we + * hit max_request_buffer we die (we'd rather reject a + * maliciously large request than chew up infinite memory). + */ +static ssize_t read_request(int fd, unsigned char **out) +{ + size_t len = 0, alloc = 8192; + unsigned char *buf = xmalloc(alloc); + + if (max_request_buffer < alloc) + max_request_buffer = alloc; + + while (1) { + ssize_t cnt; + + cnt = read_in_full(fd, buf + len, alloc - len); + if (cnt < 0) { + free(buf); + return -1; + } + + /* partial read from read_in_full means we hit EOF */ + len += cnt; + if (len < alloc) { + *out = buf; + return len; + } + + /* otherwise, grow and try again (if we can) */ + if (alloc == max_request_buffer) + die("request was larger than our maximum size (%lu);" + " try setting GIT_HTTP_MAX_REQUEST_BUFFER", + max_request_buffer); + + alloc = alloc_nr(alloc); + if (alloc > max_request_buffer) + alloc = max_request_buffer; + REALLOC_ARRAY(buf, alloc); + } +} + +static void inflate_request(const char *prog_name, int out, int buffer_input) { git_zstream stream; + unsigned char *full_request = NULL; unsigned char in_buf[8192]; unsigned char out_buf[8192]; unsigned long cnt = 0; @@ -277,11 +323,21 @@ static void inflate_request(const char *prog_name, int out) git_inflate_init_gzip_only(&stream); while (1) { - ssize_t n = xread(0, in_buf, sizeof(in_buf)); + ssize_t n; + + if (buffer_input) { + if (full_request) + n = 0; /* nothing left to read */ + else + n = read_request(0, &full_request); + stream.next_in = full_request; + } else { + n = xread(0, in_buf, sizeof(in_buf)); + stream.next_in = in_buf; + } + if (n <= 0) die("request ended in the middle of the gzip stream"); - - stream.next_in = in_buf; stream.avail_in = n; while (0 < stream.avail_in) { @@ -307,9 +363,22 @@ static void inflate_request(const char *prog_name, int out) done: git_inflate_end(&stream); close(out); + free(full_request); +} + +static void copy_request(const char *prog_name, int out) +{ + unsigned char *buf; + ssize_t n = read_request(0, &buf); + if (n < 0) + die_errno("error reading request body"); + if (write_in_full(out, buf, n) != n) + die("%s aborted reading request", prog_name); + close(out); + free(buf); } -static void run_service(const char **argv) +static void run_service(const char **argv, int buffer_input) { const char *encoding = getenv("HTTP_CONTENT_ENCODING"); const char *user = getenv("REMOTE_USER"); @@ -334,7 +403,7 @@ static void run_service(const char **argv) "GIT_COMMITTER_EMAIL=%s@http.%s", user, host); cld.argv = argv; - if (gzipped_request) + if (buffer_input || gzipped_request) cld.in = -1; cld.git_cmd = 1; if (start_command(&cld)) @@ -342,7 +411,9 @@ static void run_service(const char **argv) close(1); if (gzipped_request) - inflate_request(argv[0], cld.in); + inflate_request(argv[0], cld.in, buffer_input); + else if (buffer_input) + copy_request(argv[0], cld.in); else close(0); @@ -392,7 +463,7 @@ static void get_info_refs(char *arg) packet_flush(1); argv[0] = svc->name; - run_service(argv); + run_service(argv, 0); } else { select_getanyfile(); @@ -496,25 +567,28 @@ static void service_rpc(char *service_name) end_headers(); argv[0] = svc->name; - run_service(argv); + run_service(argv, svc->buffer_input); strbuf_release(&buf); } +static int dead; static NORETURN void die_webcgi(const char *err, va_list params) { - static int dead; + if (dead <= 1) { + vreportf("fatal: ", err, params); - if (!dead) { - dead = 1; http_status(500, "Internal Server Error"); hdr_nocache(); end_headers(); - - vreportf("fatal: ", err, params); } exit(0); /* we successfully reported a failure ;-) */ } +static int die_webcgi_recursing(void) +{ + return dead++ > 1; +} + static char* getdir(void) { struct strbuf buf = STRBUF_INIT; @@ -569,6 +643,7 @@ int main(int argc, char **argv) git_extract_argv0_path(argv[0]); set_die_routine(die_webcgi); + set_die_is_recursing_routine(die_webcgi_recursing); if (!method) die("No REQUEST_METHOD from server"); @@ -619,6 +694,9 @@ int main(int argc, char **argv) not_found("Repository not exported: '%s'", dir); http_config(); + max_request_buffer = git_env_ulong("GIT_HTTP_MAX_REQUEST_BUFFER", + max_request_buffer); + cmd->imp(cmd_arg); return 0; } diff --git a/merge-recursive.c b/merge-recursive.c index 1c9c30db6c..44d85bea4b 100644 --- a/merge-recursive.c +++ b/merge-recursive.c @@ -611,7 +611,6 @@ static char *unique_path(struct merge_options *o, const char *path, const char * { struct strbuf newpath = STRBUF_INIT; int suffix = 0; - struct stat st; size_t base_len; strbuf_addf(&newpath, "%s~", path); @@ -620,7 +619,7 @@ static char *unique_path(struct merge_options *o, const char *path, const char * base_len = newpath.len; while (string_list_has_string(&o->current_file_set, newpath.buf) || string_list_has_string(&o->current_directory_set, newpath.buf) || - lstat(newpath.buf, &st) == 0) { + file_exists(newpath.buf)) { strbuf_setlen(&newpath, base_len); strbuf_addf(&newpath, "_%d", suffix++); } diff --git a/mergetools/winmerge b/mergetools/winmerge new file mode 100644 index 0000000000..74a66d4e8d --- /dev/null +++ b/mergetools/winmerge @@ -0,0 +1,36 @@ +diff_cmd () { + "$merge_tool_path" -u -e "$LOCAL" "$REMOTE" + return 0 +} + +merge_cmd () { + # mergetool.winmerge.trustExitCode is implicitly false. + # touch $BACKUP so that we can check_unchanged. + touch "$BACKUP" + "$merge_tool_path" -u -e -dl Local -dr Remote \ + "$LOCAL" "$REMOTE" "$MERGED" + check_unchanged +} + +translate_merge_tool_path() { + # Use WinMergeU.exe if it exists in $PATH + if type -p WinMergeU.exe >/dev/null 2>&1 + then + printf WinMergeU.exe + return + fi + + # Look for WinMergeU.exe in the typical locations + winmerge_exe="WinMerge/WinMergeU.exe" + for directory in $(env | grep -Ei '^PROGRAM(FILES(\(X86\))?|W6432)=' | + cut -d '=' -f 2- | sort -u) + do + if test -n "$directory" && test -x "$directory/$winmerge_exe" + then + printf '%s' "$directory/$winmerge_exe" + return + fi + done + + printf WinMergeU.exe +} diff --git a/pack-bitmap.c b/pack-bitmap.c index 62a98cc119..2b3ff23797 100644 --- a/pack-bitmap.c +++ b/pack-bitmap.c @@ -209,14 +209,12 @@ static inline uint8_t read_u8(const unsigned char *buffer, size_t *pos) return buffer[(*pos)++]; } +#define MAX_XOR_OFFSET 160 + static int load_bitmap_entries_v1(struct bitmap_index *index) { - static const size_t MAX_XOR_OFFSET = 160; - uint32_t i; - struct stored_bitmap **recent_bitmaps; - - recent_bitmaps = xcalloc(MAX_XOR_OFFSET, sizeof(struct stored_bitmap)); + struct stored_bitmap *recent_bitmaps[MAX_XOR_OFFSET] = { NULL }; for (i = 0; i < index->entry_count; ++i) { int xor_offset, flags; @@ -987,7 +985,7 @@ void test_bitmap_walk(struct rev_info *revs) else fprintf(stderr, "Mismatch!\n"); - free(result); + bitmap_free(result); } static int rebuild_bitmap(uint32_t *reposition, diff --git a/read-cache.c b/read-cache.c index 36ff89f29e..723d48dddf 100644 --- a/read-cache.c +++ b/read-cache.c @@ -39,11 +39,12 @@ static struct cache_entry *refresh_cache_entry(struct cache_entry *ce, #define CACHE_EXT_TREE 0x54524545 /* "TREE" */ #define CACHE_EXT_RESOLVE_UNDO 0x52455543 /* "REUC" */ #define CACHE_EXT_LINK 0x6c696e6b /* "link" */ +#define CACHE_EXT_UNTRACKED 0x554E5452 /* "UNTR" */ /* changes that can be kept in $GIT_DIR/index (basically all extensions) */ #define EXTMASK (RESOLVE_UNDO_CHANGED | CACHE_TREE_CHANGED | \ CE_ENTRY_ADDED | CE_ENTRY_REMOVED | CE_ENTRY_CHANGED | \ - SPLIT_INDEX_ORDERED) + SPLIT_INDEX_ORDERED | UNTRACKED_CHANGED) struct index_state the_index; static const char *alternate_index_output; @@ -79,6 +80,7 @@ void rename_index_entry_at(struct index_state *istate, int nr, const char *new_n memcpy(new->name, new_name, namelen + 1); cache_tree_invalidate_path(istate, old->name); + untracked_cache_remove_from_index(istate, old->name); remove_index_entry_at(istate, nr); add_index_entry(istate, new, ADD_CACHE_OK_TO_ADD|ADD_CACHE_OK_TO_REPLACE); } @@ -270,20 +272,34 @@ static int ce_match_stat_basic(const struct cache_entry *ce, struct stat *st) return changed; } -static int is_racy_timestamp(const struct index_state *istate, - const struct cache_entry *ce) +static int is_racy_stat(const struct index_state *istate, + const struct stat_data *sd) { - return (!S_ISGITLINK(ce->ce_mode) && - istate->timestamp.sec && + return (istate->timestamp.sec && #ifdef USE_NSEC /* nanosecond timestamped files can also be racy! */ - (istate->timestamp.sec < ce->ce_stat_data.sd_mtime.sec || - (istate->timestamp.sec == ce->ce_stat_data.sd_mtime.sec && - istate->timestamp.nsec <= ce->ce_stat_data.sd_mtime.nsec)) + (istate->timestamp.sec < sd->sd_mtime.sec || + (istate->timestamp.sec == sd->sd_mtime.sec && + istate->timestamp.nsec <= sd->sd_mtime.nsec)) #else - istate->timestamp.sec <= ce->ce_stat_data.sd_mtime.sec + istate->timestamp.sec <= sd->sd_mtime.sec #endif - ); + ); +} + +static int is_racy_timestamp(const struct index_state *istate, + const struct cache_entry *ce) +{ + return (!S_ISGITLINK(ce->ce_mode) && + is_racy_stat(istate, &ce->ce_stat_data)); +} + +int match_stat_data_racy(const struct index_state *istate, + const struct stat_data *sd, struct stat *st) +{ + if (is_racy_stat(istate, sd)) + return MTIME_CHANGED; + return match_stat_data(sd, st); } int ie_match_stat(const struct index_state *istate, @@ -538,6 +554,7 @@ int remove_file_from_index(struct index_state *istate, const char *path) if (pos < 0) pos = -pos-1; cache_tree_invalidate_path(istate, path); + untracked_cache_remove_from_index(istate, path); while (pos < istate->cache_nr && !strcmp(istate->cache[pos]->name, path)) remove_index_entry_at(istate, pos); return 0; @@ -982,6 +999,8 @@ static int add_index_entry_with_check(struct index_state *istate, struct cache_e } pos = -pos-1; + untracked_cache_add_to_index(istate, ce->name); + /* * Inserting a merged entry ("stage 0") into the index * will always replace all non-merged entries.. @@ -1372,6 +1391,9 @@ static int read_index_extension(struct index_state *istate, if (read_link_extension(istate, data, sz)) return -1; break; + case CACHE_EXT_UNTRACKED: + istate->untracked = read_untracked_extension(data, sz); + break; default: if (*ext < 'A' || 'Z' < *ext) return error("index uses %.4s extension, which we do not understand", @@ -1667,6 +1689,8 @@ int discard_index(struct index_state *istate) istate->cache = NULL; istate->cache_alloc = 0; discard_split_index(istate); + free_untracked_cache(istate->untracked); + istate->untracked = NULL; return 0; } @@ -2053,6 +2077,17 @@ static int do_write_index(struct index_state *istate, int newfd, if (err) return -1; } + if (!strip_extensions && istate->untracked) { + struct strbuf sb = STRBUF_INIT; + + write_untracked_extension(&sb, istate->untracked); + err = write_index_ext_header(&c, newfd, CACHE_EXT_UNTRACKED, + sb.len) < 0 || + ce_write(&c, newfd, sb.buf, sb.len) < 0; + strbuf_release(&sb); + if (err) + return -1; + } if (ce_flush(&c, newfd, istate->sha1) || fstat(newfd, &st)) return -1; @@ -49,10 +49,7 @@ static int branches_alloc; static int branches_nr; static struct branch *current_branch; -static const char *default_remote_name; -static const char *branch_pushremote_name; static const char *pushremote_name; -static int explicit_default_remote_name; static struct rewrites rewrites; static struct rewrites rewrites_push; @@ -367,16 +364,9 @@ static int handle_config(const char *key, const char *value, void *cb) return 0; branch = make_branch(name, subkey - name); if (!strcmp(subkey, ".remote")) { - if (git_config_string(&branch->remote_name, key, value)) - return -1; - if (branch == current_branch) { - default_remote_name = branch->remote_name; - explicit_default_remote_name = 1; - } + return git_config_string(&branch->remote_name, key, value); } else if (!strcmp(subkey, ".pushremote")) { - if (branch == current_branch) - if (git_config_string(&branch_pushremote_name, key, value)) - return -1; + return git_config_string(&branch->pushremote_name, key, value); } else if (!strcmp(subkey, ".merge")) { if (!value) return config_error_nonbool(key); @@ -501,12 +491,15 @@ static void alias_all_urls(void) static void read_config(void) { + static int loaded; unsigned char sha1[20]; const char *head_ref; int flag; - if (default_remote_name) /* did this already */ + + if (loaded) return; - default_remote_name = "origin"; + loaded = 1; + current_branch = NULL; head_ref = resolve_ref_unsafe("HEAD", 0, sha1, &flag); if (head_ref && (flag & REF_ISSYMREF) && @@ -514,10 +507,6 @@ static void read_config(void) current_branch = make_branch(head_ref, 0); } git_config(handle_config, NULL); - if (branch_pushremote_name) { - free((char *)pushremote_name); - pushremote_name = branch_pushremote_name; - } alias_all_urls(); } @@ -696,22 +685,45 @@ static int valid_remote_nick(const char *name) return !strchr(name, '/'); /* no slash */ } -static struct remote *remote_get_1(const char *name, const char *pushremote_name) +const char *remote_for_branch(struct branch *branch, int *explicit) +{ + if (branch && branch->remote_name) { + if (explicit) + *explicit = 1; + return branch->remote_name; + } + if (explicit) + *explicit = 0; + return "origin"; +} + +const char *pushremote_for_branch(struct branch *branch, int *explicit) +{ + if (branch && branch->pushremote_name) { + if (explicit) + *explicit = 1; + return branch->pushremote_name; + } + if (pushremote_name) { + if (explicit) + *explicit = 1; + return pushremote_name; + } + return remote_for_branch(branch, explicit); +} + +static struct remote *remote_get_1(const char *name, + const char *(*get_default)(struct branch *, int *)) { struct remote *ret; int name_given = 0; + read_config(); + if (name) name_given = 1; - else { - if (pushremote_name) { - name = pushremote_name; - name_given = 1; - } else { - name = default_remote_name; - name_given = explicit_default_remote_name; - } - } + else + name = get_default(current_branch, &name_given); ret = make_remote(name, 0); if (valid_remote_nick(name)) { @@ -731,14 +743,12 @@ static struct remote *remote_get_1(const char *name, const char *pushremote_name struct remote *remote_get(const char *name) { - read_config(); - return remote_get_1(name, NULL); + return remote_get_1(name, remote_for_branch); } struct remote *pushremote_get(const char *name) { - read_config(); - return remote_get_1(name, pushremote_name); + return remote_get_1(name, pushremote_for_branch); } int remote_is_configured(const char *name) @@ -1633,15 +1643,31 @@ void set_ref_status_for_push(struct ref *remote_refs, int send_mirror, static void set_merge(struct branch *ret) { + struct remote *remote; char *ref; unsigned char sha1[20]; int i; + if (!ret) + return; /* no branch */ + if (ret->merge) + return; /* already run */ + if (!ret->remote_name || !ret->merge_nr) { + /* + * no merge config; let's make sure we don't confuse callers + * with a non-zero merge_nr but a NULL merge + */ + ret->merge_nr = 0; + return; + } + + remote = remote_get(ret->remote_name); + ret->merge = xcalloc(ret->merge_nr, sizeof(*ret->merge)); for (i = 0; i < ret->merge_nr; i++) { ret->merge[i] = xcalloc(1, sizeof(**ret->merge)); ret->merge[i]->src = xstrdup(ret->merge_name[i]); - if (!remote_find_tracking(ret->remote, ret->merge[i]) || + if (!remote_find_tracking(remote, ret->merge[i]) || strcmp(ret->remote_name, ".")) continue; if (dwim_ref(ret->merge_name[i], strlen(ret->merge_name[i]), @@ -1661,11 +1687,7 @@ struct branch *branch_get(const char *name) ret = current_branch; else ret = make_branch(name, 0); - if (ret && ret->remote_name) { - ret->remote = remote_get(ret->remote_name); - if (ret->merge_nr) - set_merge(ret); - } + set_merge(ret); return ret; } @@ -1683,6 +1705,130 @@ int branch_merge_matches(struct branch *branch, return refname_match(branch->merge[i]->src, refname); } +__attribute((format (printf,2,3))) +static const char *error_buf(struct strbuf *err, const char *fmt, ...) +{ + if (err) { + va_list ap; + va_start(ap, fmt); + strbuf_vaddf(err, fmt, ap); + va_end(ap); + } + return NULL; +} + +const char *branch_get_upstream(struct branch *branch, struct strbuf *err) +{ + if (!branch) + return error_buf(err, _("HEAD does not point to a branch")); + + if (!branch->merge || !branch->merge[0]) { + /* + * no merge config; is it because the user didn't define any, + * or because it is not a real branch, and get_branch + * auto-vivified it? + */ + if (!ref_exists(branch->refname)) + return error_buf(err, _("no such branch: '%s'"), + branch->name); + return error_buf(err, + _("no upstream configured for branch '%s'"), + branch->name); + } + + if (!branch->merge[0]->dst) + return error_buf(err, + _("upstream branch '%s' not stored as a remote-tracking branch"), + branch->merge[0]->src); + + return branch->merge[0]->dst; +} + +static const char *tracking_for_push_dest(struct remote *remote, + const char *refname, + struct strbuf *err) +{ + char *ret; + + ret = apply_refspecs(remote->fetch, remote->fetch_refspec_nr, refname); + if (!ret) + return error_buf(err, + _("push destination '%s' on remote '%s' has no local tracking branch"), + refname, remote->name); + return ret; +} + +static const char *branch_get_push_1(struct branch *branch, struct strbuf *err) +{ + struct remote *remote; + + if (!branch) + return error_buf(err, _("HEAD does not point to a branch")); + + remote = remote_get(pushremote_for_branch(branch, NULL)); + if (!remote) + return error_buf(err, + _("branch '%s' has no remote for pushing"), + branch->name); + + if (remote->push_refspec_nr) { + char *dst; + const char *ret; + + dst = apply_refspecs(remote->push, remote->push_refspec_nr, + branch->refname); + if (!dst) + return error_buf(err, + _("push refspecs for '%s' do not include '%s'"), + remote->name, branch->name); + + ret = tracking_for_push_dest(remote, dst, err); + free(dst); + return ret; + } + + if (remote->mirror) + return tracking_for_push_dest(remote, branch->refname, err); + + switch (push_default) { + case PUSH_DEFAULT_NOTHING: + return error_buf(err, _("push has no destination (push.default is 'nothing')")); + + case PUSH_DEFAULT_MATCHING: + case PUSH_DEFAULT_CURRENT: + return tracking_for_push_dest(remote, branch->refname, err); + + case PUSH_DEFAULT_UPSTREAM: + return branch_get_upstream(branch, err); + + case PUSH_DEFAULT_UNSPECIFIED: + case PUSH_DEFAULT_SIMPLE: + { + const char *up, *cur; + + up = branch_get_upstream(branch, err); + if (!up) + return NULL; + cur = tracking_for_push_dest(remote, branch->refname, err); + if (!cur) + return NULL; + if (strcmp(cur, up)) + return error_buf(err, + _("cannot resolve 'simple' push to a single destination")); + return cur; + } + } + + die("BUG: unhandled push situation"); +} + +const char *branch_get_push(struct branch *branch, struct strbuf *err) +{ + if (!branch->push_tracking_ref) + branch->push_tracking_ref = branch_get_push_1(branch, err); + return branch->push_tracking_ref; +} + static int ignore_symref_update(const char *refname) { unsigned char sha1[20]; @@ -1877,12 +2023,15 @@ int ref_newer(const unsigned char *new_sha1, const unsigned char *old_sha1) /* * Compare a branch with its upstream, and save their differences (number - * of commits) in *num_ours and *num_theirs. + * of commits) in *num_ours and *num_theirs. The name of the upstream branch + * (or NULL if no upstream is defined) is returned via *upstream_name, if it + * is not itself NULL. * - * Return 0 if branch has no upstream (no base), -1 if upstream is missing - * (with "gone" base), otherwise 1 (with base). + * Returns -1 if num_ours and num_theirs could not be filled in (e.g., no + * upstream defined, or ref does not exist), 0 otherwise. */ -int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs) +int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs, + const char **upstream_name) { unsigned char sha1[20]; struct commit *ours, *theirs; @@ -1892,12 +2041,13 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs) int rev_argc; /* Cannot stat unless we are marked to build on top of somebody else. */ - if (!branch || - !branch->merge || !branch->merge[0] || !branch->merge[0]->dst) - return 0; + base = branch_get_upstream(branch, NULL); + if (upstream_name) + *upstream_name = base; + if (!base) + return -1; /* Cannot stat if what we used to build on no longer exists */ - base = branch->merge[0]->dst; if (read_ref(base, sha1)) return -1; theirs = lookup_commit_reference(sha1); @@ -1913,7 +2063,7 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs) /* are we the same? */ if (theirs == ours) { *num_theirs = *num_ours = 0; - return 1; + return 0; } /* Run "rev-list --left-right ours...theirs" internally... */ @@ -1949,7 +2099,7 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs) /* clear object flags smudged by the above traversal */ clear_commit_marks(ours, ALL_REV_FLAGS); clear_commit_marks(theirs, ALL_REV_FLAGS); - return 1; + return 0; } /* @@ -1958,23 +2108,17 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs) int format_tracking_info(struct branch *branch, struct strbuf *sb) { int ours, theirs; + const char *full_base; char *base; int upstream_is_gone = 0; - switch (stat_tracking_info(branch, &ours, &theirs)) { - case 0: - /* no base */ - return 0; - case -1: - /* with "gone" base */ + if (stat_tracking_info(branch, &ours, &theirs, &full_base) < 0) { + if (!full_base) + return 0; upstream_is_gone = 1; - break; - default: - /* with base */ - break; } - base = shorten_unambiguous_ref(branch->merge[0]->dst, 0); + base = shorten_unambiguous_ref(full_base, 0); if (upstream_is_gone) { strbuf_addf(sb, _("Your branch is based on '%s', but the upstream is gone.\n"), @@ -203,19 +203,42 @@ struct branch { const char *refname; const char *remote_name; - struct remote *remote; + const char *pushremote_name; const char **merge_name; struct refspec **merge; int merge_nr; int merge_alloc; + + const char *push_tracking_ref; }; struct branch *branch_get(const char *name); +const char *remote_for_branch(struct branch *branch, int *explicit); +const char *pushremote_for_branch(struct branch *branch, int *explicit); int branch_has_merge_config(struct branch *branch); int branch_merge_matches(struct branch *, int n, const char *); +/** + * Return the fully-qualified refname of the tracking branch for `branch`. + * I.e., what "branch@{upstream}" would give you. Returns NULL if no + * upstream is defined. + * + * If `err` is not NULL and no upstream is defined, a more specific error + * message is recorded there (if the function does not return NULL, then + * `err` is not touched). + */ +const char *branch_get_upstream(struct branch *branch, struct strbuf *err); + +/** + * Return the tracking branch that corresponds to the ref we would push to + * given a bare `git push` while `branch` is checked out. + * + * The return value and `err` conventions match those of `branch_get_upstream`. + */ +const char *branch_get_push(struct branch *branch, struct strbuf *err); + /* Flags to match_refs. */ enum match_refs_flags { MATCH_REFS_NONE = 0, @@ -226,7 +249,8 @@ enum match_refs_flags { }; /* Reporting of tracking info */ -int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs); +int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs, + const char **upstream_name); int format_tracking_info(struct branch *branch, struct strbuf *sb); struct ref *get_local_heads(void); @@ -659,6 +659,8 @@ int rerere_forget(struct pathspec *pathspec) return error("Could not read index"); fd = setup_rerere(&merge_rr, RERERE_NOAUTOUPDATE); + if (fd < 0) + return 0; unmerge_cache(pathspec); find_conflict(&conflict); diff --git a/sha1_file.c b/sha1_file.c index ccc6dac54b..7e38148fe5 100644 --- a/sha1_file.c +++ b/sha1_file.c @@ -3286,7 +3286,7 @@ static int index_core(unsigned char *sha1, int fd, size_t size, int ret; if (!size) { - ret = index_mem(sha1, NULL, size, type, path, flags); + ret = index_mem(sha1, "", size, type, path, flags); } else if (size <= SMALL_FILE_SIZE) { char *buf = xmalloc(size); if (size == read_in_full(fd, buf, size)) diff --git a/sha1_name.c b/sha1_name.c index 1cb810877a..e57513e610 100644 --- a/sha1_name.c +++ b/sha1_name.c @@ -6,6 +6,7 @@ #include "tree-walk.h" #include "refs.h" #include "remote.h" +#include "dir.h" static int get_sha1_oneline(const char *, unsigned char *, struct commit_list *); @@ -415,12 +416,12 @@ static int ambiguous_path(const char *path, int len) return slash; } -static inline int upstream_mark(const char *string, int len) +static inline int at_mark(const char *string, int len, + const char **suffix, int nr) { - const char *suffix[] = { "@{upstream}", "@{u}" }; int i; - for (i = 0; i < ARRAY_SIZE(suffix); i++) { + for (i = 0; i < nr; i++) { int suffix_len = strlen(suffix[i]); if (suffix_len <= len && !memcmp(string, suffix[i], suffix_len)) @@ -429,6 +430,18 @@ static inline int upstream_mark(const char *string, int len) return 0; } +static inline int upstream_mark(const char *string, int len) +{ + const char *suffix[] = { "@{upstream}", "@{u}" }; + return at_mark(string, len, suffix, ARRAY_SIZE(suffix)); +} + +static inline int push_mark(const char *string, int len) +{ + const char *suffix[] = { "@{push}" }; + return at_mark(string, len, suffix, ARRAY_SIZE(suffix)); +} + static int get_sha1_1(const char *name, int len, unsigned char *sha1, unsigned lookup_flags); static int interpret_nth_prior_checkout(const char *name, int namelen, struct strbuf *buf); @@ -476,7 +489,8 @@ static int get_sha1_basic(const char *str, int len, unsigned char *sha1, nth_prior = 1; continue; } - if (!upstream_mark(str + at, len - at)) { + if (!upstream_mark(str + at, len - at) && + !push_mark(str + at, len - at)) { reflog_len = (len-1) - (at+2); len = at; } @@ -1055,46 +1069,36 @@ static void set_shortened_ref(struct strbuf *buf, const char *ref) free(s); } -static const char *get_upstream_branch(const char *branch_buf, int len) -{ - char *branch = xstrndup(branch_buf, len); - struct branch *upstream = branch_get(*branch ? branch : NULL); - - /* - * Upstream can be NULL only if branch refers to HEAD and HEAD - * points to something different than a branch. - */ - if (!upstream) - die(_("HEAD does not point to a branch")); - if (!upstream->merge || !upstream->merge[0]->dst) { - if (!ref_exists(upstream->refname)) - die(_("No such branch: '%s'"), branch); - if (!upstream->merge) { - die(_("No upstream configured for branch '%s'"), - upstream->name); - } - die( - _("Upstream branch '%s' not stored as a remote-tracking branch"), - upstream->merge[0]->src); - } - free(branch); - - return upstream->merge[0]->dst; -} - -static int interpret_upstream_mark(const char *name, int namelen, - int at, struct strbuf *buf) +static int interpret_branch_mark(const char *name, int namelen, + int at, struct strbuf *buf, + int (*get_mark)(const char *, int), + const char *(*get_data)(struct branch *, + struct strbuf *)) { int len; + struct branch *branch; + struct strbuf err = STRBUF_INIT; + const char *value; - len = upstream_mark(name + at, namelen - at); + len = get_mark(name + at, namelen - at); if (!len) return -1; if (memchr(name, ':', at)) return -1; - set_shortened_ref(buf, get_upstream_branch(name, at)); + if (at) { + char *name_str = xmemdupz(name, at); + branch = branch_get(name_str); + free(name_str); + } else + branch = branch_get(NULL); + + value = get_data(branch, &err); + if (!value) + die("%s", err.buf); + + set_shortened_ref(buf, value); return len + at; } @@ -1145,7 +1149,13 @@ int interpret_branch_name(const char *name, int namelen, struct strbuf *buf) if (len > 0) return reinterpret(name, namelen, len, buf); - len = interpret_upstream_mark(name, namelen, at - name, buf); + len = interpret_branch_mark(name, namelen, at - name, buf, + upstream_mark, branch_get_upstream); + if (len > 0) + return len; + + len = interpret_branch_mark(name, namelen, at - name, buf, + push_mark, branch_get_push); if (len > 0) return len; } @@ -1237,14 +1247,13 @@ static void diagnose_invalid_sha1_path(const char *prefix, const char *object_name, int object_name_len) { - struct stat st; unsigned char sha1[20]; unsigned mode; if (!prefix) prefix = ""; - if (!lstat(filename, &st)) + if (file_exists(filename)) die("Path '%s' exists on disk, but not in '%.*s'.", filename, object_name_len, object_name); if (errno == ENOENT || errno == ENOTDIR) { @@ -1271,7 +1280,6 @@ static void diagnose_invalid_index_path(int stage, const char *prefix, const char *filename) { - struct stat st; const struct cache_entry *ce; int pos; unsigned namelen = strlen(filename); @@ -1314,7 +1322,7 @@ static void diagnose_invalid_index_path(int stage, ce_stage(ce), filename); } - if (!lstat(filename, &st)) + if (file_exists(filename)) die("Path '%s' exists on disk, but not in the index.", filename); if (errno == ENOENT || errno == ENOTDIR) die("Path '%s' does not exist (neither on disk nor in the index).", @@ -1435,11 +1443,19 @@ static int get_sha1_with_context_1(const char *name, new_filename = resolve_relative_path(filename); if (new_filename) filename = new_filename; - ret = get_tree_entry(tree_sha1, filename, sha1, &oc->mode); - if (ret && only_to_die) { - diagnose_invalid_sha1_path(prefix, filename, - tree_sha1, - name, len); + if (flags & GET_SHA1_FOLLOW_SYMLINKS) { + ret = get_tree_entry_follow_symlinks(tree_sha1, + filename, sha1, &oc->symlink_path, + &oc->mode); + } else { + ret = get_tree_entry(tree_sha1, filename, + sha1, &oc->mode); + if (ret && only_to_die) { + diagnose_invalid_sha1_path(prefix, + filename, + tree_sha1, + name, len); + } } hashcpy(oc->tree, tree_sha1); strlcpy(oc->path, filename, sizeof(oc->path)); @@ -1470,5 +1486,7 @@ void maybe_die_on_misspelt_object_name(const char *name, const char *prefix) int get_sha1_with_context(const char *str, unsigned flags, unsigned char *sha1, struct object_context *orc) { + if (flags & GET_SHA1_FOLLOW_SYMLINKS && flags & GET_SHA1_ONLY_TO_DIE) + die("BUG: incompatible flags for get_sha1_with_context"); return get_sha1_with_context_1(str, flags, NULL, sha1, orc); } diff --git a/split-index.c b/split-index.c index 21485e2066..968b780a06 100644 --- a/split-index.c +++ b/split-index.c @@ -41,13 +41,6 @@ int read_link_extension(struct index_state *istate, return 0; } -static int write_strbuf(void *user_data, const void *data, size_t len) -{ - struct strbuf *sb = user_data; - strbuf_add(sb, data, len); - return len; -} - int write_link_extension(struct strbuf *sb, struct index_state *istate) { @@ -55,8 +48,8 @@ int write_link_extension(struct strbuf *sb, strbuf_add(sb, si->base_sha1, 20); if (!si->delete_bitmap && !si->replace_bitmap) return 0; - ewah_serialize_to(si->delete_bitmap, write_strbuf, sb); - ewah_serialize_to(si->replace_bitmap, write_strbuf, sb); + ewah_serialize_strbuf(si->delete_bitmap, sb); + ewah_serialize_strbuf(si->replace_bitmap, sb); return 0; } diff --git a/submodule.c b/submodule.c index e4c59df5ac..15e90d1c10 100644 --- a/submodule.c +++ b/submodule.c @@ -892,7 +892,6 @@ int submodule_uses_gitfile(const char *path) int ok_to_remove_submodule(const char *path) { - struct stat st; ssize_t len; struct child_process cp = CHILD_PROCESS_INIT; const char *argv[] = { @@ -905,7 +904,7 @@ int ok_to_remove_submodule(const char *path) struct strbuf buf = STRBUF_INIT; int ok_to_remove = 1; - if ((lstat(path, &st) < 0) || is_empty_dir(path)) + if (!file_exists(path) || is_empty_dir(path)) return 1; if (!submodule_uses_gitfile(path)) diff --git a/t/t0021-conversion.sh b/t/t0021-conversion.sh index e0200b9f33..718efa04d3 100755 --- a/t/t0021-conversion.sh +++ b/t/t0021-conversion.sh @@ -226,4 +226,30 @@ test_expect_success EXPENSIVE 'filter large file' ' ! test -s err ' +test_expect_success "filter: clean empty file" ' + git config filter.in-repo-header.clean "echo cleaned && cat" && + git config filter.in-repo-header.smudge "sed 1d" && + + echo "empty-in-worktree filter=in-repo-header" >>.gitattributes && + >empty-in-worktree && + + echo cleaned >expected && + git add empty-in-worktree && + git show :empty-in-worktree >actual && + test_cmp expected actual +' + +test_expect_success "filter: smudge empty file" ' + git config filter.empty-in-repo.clean "cat >/dev/null" && + git config filter.empty-in-repo.smudge "echo smudged && cat" && + + echo "empty-in-repo filter=empty-in-repo" >>.gitattributes && + echo dead data walking >empty-in-repo && + git add empty-in-repo && + + echo smudged >expected && + git checkout-index --prefix=filtered- empty-in-repo && + test_cmp expected filtered-empty-in-repo +' + test_done diff --git a/t/t1006-cat-file.sh b/t/t1006-cat-file.sh index 4f225db9a7..93a4794930 100755 --- a/t/t1006-cat-file.sh +++ b/t/t1006-cat-file.sh @@ -201,6 +201,13 @@ do ' done +for opt in t s e p +do + test_expect_success "Passing -$opt with --follow-symlinks fails" ' + test_must_fail git cat-file --follow-symlinks -$opt $hello_sha1 + ' +done + test_expect_success "--batch-check for a non-existent named object" ' test "foobar42 missing foobar84 missing" = \ @@ -341,4 +348,203 @@ test_expect_success "Size of large broken object is correct when type is large" test_cmp expect actual ' +# Tests for git cat-file --follow-symlinks +test_expect_success 'prep for symlink tests' ' + echo_without_newline "$hello_content" >morx && + test_ln_s_add morx same-dir-link && + test_ln_s_add dir link-to-dir && + test_ln_s_add ../fleem out-of-repo-link && + test_ln_s_add .. out-of-repo-link-dir && + test_ln_s_add same-dir-link link-to-link && + test_ln_s_add nope broken-same-dir-link && + mkdir dir && + test_ln_s_add ../morx dir/parent-dir-link && + test_ln_s_add .. dir/link-dir && + test_ln_s_add ../../escape dir/out-of-repo-link && + test_ln_s_add ../.. dir/out-of-repo-link-dir && + test_ln_s_add nope dir/broken-link-in-dir && + mkdir dir/subdir && + test_ln_s_add ../../morx dir/subdir/grandparent-dir-link && + test_ln_s_add ../../../great-escape dir/subdir/out-of-repo-link && + test_ln_s_add ../../.. dir/subdir/out-of-repo-link-dir && + test_ln_s_add ../../../ dir/subdir/out-of-repo-link-dir-trailing && + test_ln_s_add ../parent-dir-link dir/subdir/parent-dir-link-to-link && + echo_without_newline "$hello_content" >dir/subdir/ind2 && + echo_without_newline "$hello_content" >dir/ind1 && + test_ln_s_add dir dirlink && + test_ln_s_add dir/subdir subdirlink && + test_ln_s_add subdir/ind2 dir/link-to-child && + test_ln_s_add dir/link-to-child link-to-down-link && + test_ln_s_add dir/.. up-down && + test_ln_s_add dir/../ up-down-trailing && + test_ln_s_add dir/../morx up-down-file && + test_ln_s_add dir/../../morx up-up-down-file && + test_ln_s_add subdirlink/../../morx up-two-down-file && + test_ln_s_add loop1 loop2 && + test_ln_s_add loop2 loop1 && + git add morx dir/subdir/ind2 dir/ind1 && + git commit -am "test" && + echo $hello_sha1 blob $hello_size >found +' + +test_expect_success 'git cat-file --batch-check --follow-symlinks works for non-links' ' + echo HEAD:morx | git cat-file --batch-check --follow-symlinks >actual && + test_cmp found actual && + echo HEAD:nope missing >expect && + echo HEAD:nope | git cat-file --batch-check --follow-symlinks >actual && + test_cmp expect actual +' + +test_expect_success 'git cat-file --batch-check --follow-symlinks works for in-repo, same-dir links' ' + echo HEAD:same-dir-link | git cat-file --batch-check --follow-symlinks >actual && + test_cmp found actual +' + +test_expect_success 'git cat-file --batch-check --follow-symlinks works for in-repo, links to dirs' ' + echo HEAD:link-to-dir/ind1 | git cat-file --batch-check --follow-symlinks >actual && + test_cmp found actual +' + + +test_expect_success 'git cat-file --batch-check --follow-symlinks works for broken in-repo, same-dir links' ' + echo dangling 25 >expect && + echo HEAD:broken-same-dir-link >>expect && + echo HEAD:broken-same-dir-link | git cat-file --batch-check --follow-symlinks >actual && + test_cmp expect actual +' + +test_expect_success 'git cat-file --batch-check --follow-symlinks works for same-dir links-to-links' ' + echo HEAD:link-to-link | git cat-file --batch-check --follow-symlinks >actual && + test_cmp found actual +' + +test_expect_success 'git cat-file --batch-check --follow-symlinks works for parent-dir links' ' + echo HEAD:dir/parent-dir-link | git cat-file --batch-check --follow-symlinks >actual && + test_cmp found actual && + echo notdir 29 >expect && + echo HEAD:dir/parent-dir-link/nope >>expect && + echo HEAD:dir/parent-dir-link/nope | git cat-file --batch-check --follow-symlinks >actual && + test_cmp expect actual +' + +test_expect_success 'git cat-file --batch-check --follow-symlinks works for .. links' ' + echo dangling 22 >expect && + echo HEAD:dir/link-dir/nope >>expect && + echo HEAD:dir/link-dir/nope | git cat-file --batch-check --follow-symlinks >actual && + test_cmp expect actual && + echo HEAD:dir/link-dir/morx | git cat-file --batch-check --follow-symlinks >actual && + test_cmp found actual && + echo dangling 27 >expect && + echo HEAD:dir/broken-link-in-dir >>expect && + echo HEAD:dir/broken-link-in-dir | git cat-file --batch-check --follow-symlinks >actual && + test_cmp expect actual +' + +test_expect_success 'git cat-file --batch-check --follow-symlinks works for ../.. links' ' + echo notdir 41 >expect && + echo HEAD:dir/subdir/grandparent-dir-link/nope >>expect && + echo HEAD:dir/subdir/grandparent-dir-link/nope | git cat-file --batch-check --follow-symlinks >actual && + test_cmp expect actual && + echo HEAD:dir/subdir/grandparent-dir-link | git cat-file --batch-check --follow-symlinks >actual && + test_cmp found actual && + echo HEAD:dir/subdir/parent-dir-link-to-link | git cat-file --batch-check --follow-symlinks >actual && + test_cmp found actual +' + +test_expect_success 'git cat-file --batch-check --follow-symlinks works for dir/ links' ' + echo dangling 17 >expect && + echo HEAD:dirlink/morx >>expect && + echo HEAD:dirlink/morx | git cat-file --batch-check --follow-symlinks >actual && + test_cmp expect actual && + echo $hello_sha1 blob $hello_size >expect && + echo HEAD:dirlink/ind1 | git cat-file --batch-check --follow-symlinks >actual && + test_cmp expect actual +' + +test_expect_success 'git cat-file --batch-check --follow-symlinks works for dir/subdir links' ' + echo dangling 20 >expect && + echo HEAD:subdirlink/morx >>expect && + echo HEAD:subdirlink/morx | git cat-file --batch-check --follow-symlinks >actual && + test_cmp expect actual && + echo HEAD:subdirlink/ind2 | git cat-file --batch-check --follow-symlinks >actual && + test_cmp found actual +' + +test_expect_success 'git cat-file --batch-check --follow-symlinks works for dir ->subdir links' ' + echo notdir 27 >expect && + echo HEAD:dir/link-to-child/morx >>expect && + echo HEAD:dir/link-to-child/morx | git cat-file --batch-check --follow-symlinks >actual && + test_cmp expect actual && + echo HEAD:dir/link-to-child | git cat-file --batch-check --follow-symlinks >actual && + test_cmp found actual && + echo HEAD:link-to-down-link | git cat-file --batch-check --follow-symlinks >actual && + test_cmp found actual +' + +test_expect_success 'git cat-file --batch-check --follow-symlinks works for out-of-repo symlinks' ' + echo symlink 8 >expect && + echo ../fleem >>expect && + echo HEAD:out-of-repo-link | git cat-file --batch-check --follow-symlinks >actual && + test_cmp expect actual && + echo symlink 2 >expect && + echo .. >>expect && + echo HEAD:out-of-repo-link-dir | git cat-file --batch-check --follow-symlinks >actual && + test_cmp expect actual +' + +test_expect_success 'git cat-file --batch-check --follow-symlinks works for out-of-repo symlinks in dirs' ' + echo symlink 9 >expect && + echo ../escape >>expect && + echo HEAD:dir/out-of-repo-link | git cat-file --batch-check --follow-symlinks >actual && + test_cmp expect actual && + echo symlink 2 >expect && + echo .. >>expect && + echo HEAD:dir/out-of-repo-link-dir | git cat-file --batch-check --follow-symlinks >actual && + test_cmp expect actual +' + +test_expect_success 'git cat-file --batch-check --follow-symlinks works for out-of-repo symlinks in subdirs' ' + echo symlink 15 >expect && + echo ../great-escape >>expect && + echo HEAD:dir/subdir/out-of-repo-link | git cat-file --batch-check --follow-symlinks >actual && + test_cmp expect actual && + echo symlink 2 >expect && + echo .. >>expect && + echo HEAD:dir/subdir/out-of-repo-link-dir | git cat-file --batch-check --follow-symlinks >actual && + test_cmp expect actual && + echo symlink 3 >expect && + echo ../ >>expect && + echo HEAD:dir/subdir/out-of-repo-link-dir-trailing | git cat-file --batch-check --follow-symlinks >actual && + test_cmp expect actual +' + +test_expect_success 'git cat-file --batch-check --follow-symlinks works for symlinks with internal ..' ' + echo HEAD: | git cat-file --batch-check >expect && + echo HEAD:up-down | git cat-file --batch-check --follow-symlinks >actual && + test_cmp expect actual && + echo HEAD:up-down-trailing | git cat-file --batch-check --follow-symlinks >actual && + test_cmp expect actual && + echo HEAD:up-down-file | git cat-file --batch-check --follow-symlinks >actual && + test_cmp found actual && + echo symlink 7 >expect && + echo ../morx >>expect && + echo HEAD:up-up-down-file | git cat-file --batch-check --follow-symlinks >actual && + test_cmp expect actual && + echo HEAD:up-two-down-file | git cat-file --batch-check --follow-symlinks >actual && + test_cmp found actual +' + +test_expect_success 'git cat-file --batch-check --follow-symlink breaks loops' ' + echo loop 10 >expect && + echo HEAD:loop1 >>expect && + echo HEAD:loop1 | git cat-file --batch-check --follow-symlinks >actual && + test_cmp expect actual +' + +test_expect_success 'git cat-file --batch --follow-symlink returns correct sha and mode' ' + echo HEAD:morx | git cat-file --batch >expect && + echo HEAD:morx | git cat-file --batch --follow-symlinks >actual && + test_cmp expect actual +' + test_done diff --git a/t/t1507-rev-parse-upstream.sh b/t/t1507-rev-parse-upstream.sh index 1978947c41..46ef1f22dc 100755 --- a/t/t1507-rev-parse-upstream.sh +++ b/t/t1507-rev-parse-upstream.sh @@ -150,7 +150,7 @@ test_expect_success 'branch@{u} works when tracking a local branch' ' test_expect_success 'branch@{u} error message when no upstream' ' cat >expect <<-EOF && - fatal: No upstream configured for branch ${sq}non-tracking${sq} + fatal: no upstream configured for branch ${sq}non-tracking${sq} EOF error_message non-tracking@{u} 2>actual && test_i18ncmp expect actual @@ -158,7 +158,7 @@ test_expect_success 'branch@{u} error message when no upstream' ' test_expect_success '@{u} error message when no upstream' ' cat >expect <<-EOF && - fatal: No upstream configured for branch ${sq}master${sq} + fatal: no upstream configured for branch ${sq}master${sq} EOF test_must_fail git rev-parse --verify @{u} 2>actual && test_i18ncmp expect actual @@ -166,7 +166,7 @@ test_expect_success '@{u} error message when no upstream' ' test_expect_success 'branch@{u} error message with misspelt branch' ' cat >expect <<-EOF && - fatal: No such branch: ${sq}no-such-branch${sq} + fatal: no such branch: ${sq}no-such-branch${sq} EOF error_message no-such-branch@{u} 2>actual && test_i18ncmp expect actual @@ -183,7 +183,7 @@ test_expect_success '@{u} error message when not on a branch' ' test_expect_success 'branch@{u} error message if upstream branch not fetched' ' cat >expect <<-EOF && - fatal: Upstream branch ${sq}refs/heads/side${sq} not stored as a remote-tracking branch + fatal: upstream branch ${sq}refs/heads/side${sq} not stored as a remote-tracking branch EOF error_message bad-upstream@{u} 2>actual && test_i18ncmp expect actual diff --git a/t/t1514-rev-parse-push.sh b/t/t1514-rev-parse-push.sh new file mode 100755 index 0000000000..7214f5b33f --- /dev/null +++ b/t/t1514-rev-parse-push.sh @@ -0,0 +1,63 @@ +#!/bin/sh + +test_description='test <branch>@{push} syntax' +. ./test-lib.sh + +resolve () { + echo "$2" >expect && + git rev-parse --symbolic-full-name "$1" >actual && + test_cmp expect actual +} + +test_expect_success 'setup' ' + git init --bare parent.git && + git init --bare other.git && + git remote add origin parent.git && + git remote add other other.git && + test_commit base && + git push origin HEAD && + git branch --set-upstream-to=origin/master master && + git branch --track topic origin/master && + git push origin topic && + git push other topic +' + +test_expect_success '@{push} with default=nothing' ' + test_config push.default nothing && + test_must_fail git rev-parse master@{push} +' + +test_expect_success '@{push} with default=simple' ' + test_config push.default simple && + resolve master@{push} refs/remotes/origin/master +' + +test_expect_success 'triangular @{push} fails with default=simple' ' + test_config push.default simple && + test_must_fail git rev-parse topic@{push} +' + +test_expect_success '@{push} with default=current' ' + test_config push.default current && + resolve topic@{push} refs/remotes/origin/topic +' + +test_expect_success '@{push} with default=matching' ' + test_config push.default matching && + resolve topic@{push} refs/remotes/origin/topic +' + +test_expect_success '@{push} with pushremote defined' ' + test_config push.default current && + test_config branch.topic.pushremote other && + resolve topic@{push} refs/remotes/other/topic +' + +test_expect_success '@{push} with push refspecs' ' + test_config push.default nothing && + test_config remote.origin.push refs/heads/*:refs/heads/magic/* && + git push && + resolve topic@{push} refs/remotes/origin/magic/topic +' + +test_done diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh index 0746eeeff7..7396ca9911 100755 --- a/t/t3903-stash.sh +++ b/t/t3903-stash.sh @@ -100,6 +100,10 @@ test_expect_success 'unstashing in a subdirectory' ' ) ' +test_expect_success 'stash drop complains of extra options' ' + test_must_fail git stash drop --foo +' + test_expect_success 'drop top stash' ' git reset --hard && git stash list > stashlist1 && diff --git a/t/t5407-post-rewrite-hook.sh b/t/t5407-post-rewrite-hook.sh index ea2e0d4b48..7a48236e87 100755 --- a/t/t5407-post-rewrite-hook.sh +++ b/t/t5407-post-rewrite-hook.sh @@ -61,10 +61,10 @@ test_expect_success 'git rebase' ' git add foo && git rebase --continue && echo rebase >expected.args && - cat >expected.data <<EOF && -$(git rev-parse C) $(git rev-parse HEAD^) -$(git rev-parse D) $(git rev-parse HEAD) -EOF + cat >expected.data <<-EOF && + $(git rev-parse C) $(git rev-parse HEAD^) + $(git rev-parse D) $(git rev-parse HEAD) + EOF verify_hook_input ' @@ -77,9 +77,9 @@ test_expect_success 'git rebase --skip' ' git add foo && git rebase --continue && echo rebase >expected.args && - cat >expected.data <<EOF && -$(git rev-parse D) $(git rev-parse HEAD) -EOF + cat >expected.data <<-EOF && + $(git rev-parse D) $(git rev-parse HEAD) + EOF verify_hook_input ' @@ -89,9 +89,9 @@ test_expect_success 'git rebase --skip the last one' ' test_must_fail git rebase --onto D A && git rebase --skip && echo rebase >expected.args && - cat >expected.data <<EOF && -$(git rev-parse E) $(git rev-parse HEAD) -EOF + cat >expected.data <<-EOF && + $(git rev-parse E) $(git rev-parse HEAD) + EOF verify_hook_input ' @@ -103,10 +103,10 @@ test_expect_success 'git rebase -m' ' git add foo && git rebase --continue && echo rebase >expected.args && - cat >expected.data <<EOF && -$(git rev-parse C) $(git rev-parse HEAD^) -$(git rev-parse D) $(git rev-parse HEAD) -EOF + cat >expected.data <<-EOF && + $(git rev-parse C) $(git rev-parse HEAD^) + $(git rev-parse D) $(git rev-parse HEAD) + EOF verify_hook_input ' @@ -119,9 +119,9 @@ test_expect_success 'git rebase -m --skip' ' git add foo && git rebase --continue && echo rebase >expected.args && - cat >expected.data <<EOF && -$(git rev-parse D) $(git rev-parse HEAD) -EOF + cat >expected.data <<-EOF && + $(git rev-parse D) $(git rev-parse HEAD) + EOF verify_hook_input ' @@ -148,10 +148,10 @@ test_expect_success 'git rebase -i (unchanged)' ' git add foo && git rebase --continue && echo rebase >expected.args && - cat >expected.data <<EOF && -$(git rev-parse C) $(git rev-parse HEAD^) -$(git rev-parse D) $(git rev-parse HEAD) -EOF + cat >expected.data <<-EOF && + $(git rev-parse C) $(git rev-parse HEAD^) + $(git rev-parse D) $(git rev-parse HEAD) + EOF verify_hook_input ' @@ -163,9 +163,9 @@ test_expect_success 'git rebase -i (skip)' ' git add foo && git rebase --continue && echo rebase >expected.args && - cat >expected.data <<EOF && -$(git rev-parse D) $(git rev-parse HEAD) -EOF + cat >expected.data <<-EOF && + $(git rev-parse D) $(git rev-parse HEAD) + EOF verify_hook_input ' @@ -177,10 +177,10 @@ test_expect_success 'git rebase -i (squash)' ' git add foo && git rebase --continue && echo rebase >expected.args && - cat >expected.data <<EOF && -$(git rev-parse C) $(git rev-parse HEAD) -$(git rev-parse D) $(git rev-parse HEAD) -EOF + cat >expected.data <<-EOF && + $(git rev-parse C) $(git rev-parse HEAD) + $(git rev-parse D) $(git rev-parse HEAD) + EOF verify_hook_input ' @@ -189,10 +189,10 @@ test_expect_success 'git rebase -i (fixup without conflict)' ' clear_hook_input && FAKE_LINES="1 fixup 2" git rebase -i B && echo rebase >expected.args && - cat >expected.data <<EOF && -$(git rev-parse C) $(git rev-parse HEAD) -$(git rev-parse D) $(git rev-parse HEAD) -EOF + cat >expected.data <<-EOF && + $(git rev-parse C) $(git rev-parse HEAD) + $(git rev-parse D) $(git rev-parse HEAD) + EOF verify_hook_input ' @@ -205,10 +205,27 @@ test_expect_success 'git rebase -i (double edit)' ' git add foo && git rebase --continue && echo rebase >expected.args && - cat >expected.data <<EOF && -$(git rev-parse C) $(git rev-parse HEAD^) -$(git rev-parse D) $(git rev-parse HEAD) -EOF + cat >expected.data <<-EOF && + $(git rev-parse C) $(git rev-parse HEAD^) + $(git rev-parse D) $(git rev-parse HEAD) + EOF + verify_hook_input +' + +test_expect_success 'git rebase -i (exec)' ' + git reset --hard D && + clear_hook_input && + FAKE_LINES="edit 1 exec_false 2" git rebase -i B && + echo something >bar && + git add bar && + # Fails because of exec false + test_must_fail git rebase --continue && + git rebase --continue && + echo rebase >expected.args && + cat >expected.data <<-EOF && + $(git rev-parse C) $(git rev-parse HEAD^) + $(git rev-parse D) $(git rev-parse HEAD) + EOF verify_hook_input ' diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh index 8a5f2363a9..ec22c98445 100755 --- a/t/t5516-fetch-push.sh +++ b/t/t5516-fetch-push.sh @@ -1120,6 +1120,61 @@ test_expect_success 'fetch exact SHA1' ' ) ' +for configallowtipsha1inwant in true false +do + test_expect_success "shallow fetch reachable SHA1 (but not a ref), allowtipsha1inwant=$configallowtipsha1inwant" ' + mk_empty testrepo && + ( + cd testrepo && + git config uploadpack.allowtipsha1inwant $configallowtipsha1inwant && + git commit --allow-empty -m foo && + git commit --allow-empty -m bar + ) && + SHA1=$(git --git-dir=testrepo/.git rev-parse HEAD^) && + mk_empty shallow && + ( + cd shallow && + test_must_fail git fetch --depth=1 ../testrepo/.git $SHA1 && + git --git-dir=../testrepo/.git config uploadpack.allowreachablesha1inwant true && + git fetch --depth=1 ../testrepo/.git $SHA1 && + git cat-file commit $SHA1 + ) + ' + + test_expect_success "deny fetch unreachable SHA1, allowtipsha1inwant=$configallowtipsha1inwant" ' + mk_empty testrepo && + ( + cd testrepo && + git config uploadpack.allowtipsha1inwant $configallowtipsha1inwant && + git commit --allow-empty -m foo && + git commit --allow-empty -m bar && + git commit --allow-empty -m xyz + ) && + SHA1_1=$(git --git-dir=testrepo/.git rev-parse HEAD^^) && + SHA1_2=$(git --git-dir=testrepo/.git rev-parse HEAD^) && + SHA1_3=$(git --git-dir=testrepo/.git rev-parse HEAD) && + ( + cd testrepo && + git reset --hard $SHA1_2 && + git cat-file commit $SHA1_1 && + git cat-file commit $SHA1_3 + ) && + mk_empty shallow && + ( + cd shallow && + test_must_fail git fetch ../testrepo/.git $SHA1_3 && + test_must_fail git fetch ../testrepo/.git $SHA1_1 && + git --git-dir=../testrepo/.git config uploadpack.allowreachablesha1inwant true && + git fetch ../testrepo/.git $SHA1_1 && + git cat-file commit $SHA1_1 && + test_must_fail git cat-file commit $SHA1_2 && + git fetch ../testrepo/.git $SHA1_2 && + git cat-file commit $SHA1_2 && + test_must_fail git fetch ../testrepo/.git $SHA1_3 + ) + ' +done + test_expect_success 'fetch follows tags by default' ' mk_test testrepo heads/master && rm -fr src dst && diff --git a/t/t5524-pull-msg.sh b/t/t5524-pull-msg.sh index 8cccecc2fc..c278adaa5a 100755 --- a/t/t5524-pull-msg.sh +++ b/t/t5524-pull-msg.sh @@ -17,6 +17,9 @@ test_expect_success setup ' git commit -m "add bfile" ) && test_tick && test_tick && + echo "second" >afile && + git add afile && + git commit -m "second commit" && echo "original $dollar" >afile && git add afile && git commit -m "do not clobber $dollar signs" @@ -32,4 +35,18 @@ test_expect_success pull ' ) ' +test_expect_success '--log=1 limits shortlog length' ' +( + cd cloned && + git reset --hard HEAD^ && + test "$(cat afile)" = original && + test "$(cat bfile)" = added && + git pull --log=1 && + git log -3 && + git cat-file commit HEAD >result && + grep Dollar result && + ! grep "second commit" result +) +' + test_done diff --git a/t/t5551-http-fetch-smart.sh b/t/t5551-http-fetch-smart.sh index 2b29311901..58207d8825 100755 --- a/t/t5551-http-fetch-smart.sh +++ b/t/t5551-http-fetch-smart.sh @@ -218,27 +218,35 @@ test_expect_success 'transfer.hiderefs works over smart-http' ' git -C hidden.git rev-parse --verify b ' -test_expect_success 'create 2,000 tags in the repo' ' - ( - cd "$HTTPD_DOCUMENT_ROOT_PATH/repo.git" && - for i in $(test_seq 2000) +# create an arbitrary number of tags, numbered from tag-$1 to tag-$2 +create_tags () { + rm -f marks && + for i in $(test_seq "$1" "$2") do - echo "commit refs/heads/too-many-refs" - echo "mark :$i" - echo "committer git <git@example.com> $i +0000" - echo "data 0" - echo "M 644 inline bla.txt" - echo "data 4" - echo "bla" + # don't use here-doc, because it requires a process + # per loop iteration + echo "commit refs/heads/too-many-refs-$1" && + echo "mark :$i" && + echo "committer git <git@example.com> $i +0000" && + echo "data 0" && + echo "M 644 inline bla.txt" && + echo "data 4" && + echo "bla" && # make every commit dangling by always # rewinding the branch after each commit - echo "reset refs/heads/too-many-refs" - echo "from :1" + echo "reset refs/heads/too-many-refs-$1" && + echo "from :$1" done | git fast-import --export-marks=marks && # now assign tags to all the dangling commits we created above tag=$(perl -e "print \"bla\" x 30") && sed -e "s|^:\([^ ]*\) \(.*\)$|\2 refs/tags/$tag-\1|" <marks >>packed-refs +} + +test_expect_success 'create 2,000 tags in the repo' ' + ( + cd "$HTTPD_DOCUMENT_ROOT_PATH/repo.git" && + create_tags 1 2000 ) ' @@ -259,5 +267,20 @@ test_expect_success 'large fetch-pack requests can be split across POSTs' ' test_line_count = 2 posts ' +test_expect_success EXPENSIVE 'http can handle enormous ref negotiation' ' + ( + cd "$HTTPD_DOCUMENT_ROOT_PATH/repo.git" && + create_tags 2001 50000 + ) && + git -C too-many-refs fetch -q --tags && + ( + cd "$HTTPD_DOCUMENT_ROOT_PATH/repo.git" && + create_tags 50001 100000 + ) && + git -C too-many-refs fetch -q --tags && + git -C too-many-refs for-each-ref refs/tags >tags && + test_line_count = 100000 tags +' + stop_httpd test_done diff --git a/t/t6300-for-each-ref.sh b/t/t6300-for-each-ref.sh index c66bf7981c..24fc2ba55d 100755 --- a/t/t6300-for-each-ref.sh +++ b/t/t6300-for-each-ref.sh @@ -28,7 +28,10 @@ test_expect_success setup ' git update-ref refs/remotes/origin/master master && git remote add origin nowhere && git config branch.master.remote origin && - git config branch.master.merge refs/heads/master + git config branch.master.merge refs/heads/master && + git remote add myfork elsewhere && + git config remote.pushdefault myfork && + git config push.default current ' test_atom() { @@ -47,6 +50,7 @@ test_atom() { test_atom head refname refs/heads/master test_atom head upstream refs/remotes/origin/master +test_atom head push refs/remotes/myfork/master test_atom head objecttype commit test_atom head objectsize 171 test_atom head objectname $(git rev-parse refs/heads/master) @@ -83,6 +87,7 @@ test_atom head HEAD '*' test_atom tag refname refs/tags/testtag test_atom tag upstream '' +test_atom tag push '' test_atom tag objecttype tag test_atom tag objectsize 154 test_atom tag objectname $(git rev-parse refs/tags/testtag) @@ -347,6 +352,12 @@ test_expect_success 'Check that :track[short] works when upstream is invalid' ' test_cmp expected actual ' +test_expect_success '%(push) supports tracking specifiers, too' ' + echo "[ahead 1]" >expected && + git for-each-ref --format="%(push:track)" refs/heads >actual && + test_cmp expected actual +' + cat >expected <<EOF $(git rev-parse --short HEAD) EOF diff --git a/t/t7063-status-untracked-cache.sh b/t/t7063-status-untracked-cache.sh new file mode 100755 index 0000000000..bd4806c12a --- /dev/null +++ b/t/t7063-status-untracked-cache.sh @@ -0,0 +1,357 @@ +#!/bin/sh + +test_description='test untracked cache' + +. ./test-lib.sh + +avoid_racy() { + sleep 1 +} + +# It's fine if git update-index returns an error code other than one, +# it'll be caught in the first test. +test_lazy_prereq UNTRACKED_CACHE ' + { git update-index --untracked-cache; ret=$?; } && + test $ret -ne 1 +' + +if ! test_have_prereq UNTRACKED_CACHE; then + skip_all='This system does not support untracked cache' + test_done +fi + +test_expect_success 'setup' ' + git init worktree && + cd worktree && + mkdir done dtwo dthree && + touch one two three done/one dtwo/two dthree/three && + git add one two done/one && + : >.git/info/exclude && + git update-index --untracked-cache +' + +test_expect_success 'untracked cache is empty' ' + test-dump-untracked-cache >../actual && + cat >../expect <<EOF && +info/exclude 0000000000000000000000000000000000000000 +core.excludesfile 0000000000000000000000000000000000000000 +exclude_per_dir .gitignore +flags 00000006 +EOF + test_cmp ../expect ../actual +' + +cat >../status.expect <<EOF && +A done/one +A one +A two +?? dthree/ +?? dtwo/ +?? three +EOF + +cat >../dump.expect <<EOF && +info/exclude e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 +core.excludesfile 0000000000000000000000000000000000000000 +exclude_per_dir .gitignore +flags 00000006 +/ 0000000000000000000000000000000000000000 recurse valid +dthree/ +dtwo/ +three +/done/ 0000000000000000000000000000000000000000 recurse valid +/dthree/ 0000000000000000000000000000000000000000 recurse check_only valid +three +/dtwo/ 0000000000000000000000000000000000000000 recurse check_only valid +two +EOF + +test_expect_success 'status first time (empty cache)' ' + avoid_racy && + : >../trace && + GIT_TRACE_UNTRACKED_STATS="$TRASH_DIRECTORY/trace" \ + git status --porcelain >../actual && + test_cmp ../status.expect ../actual && + cat >../trace.expect <<EOF && +node creation: 3 +gitignore invalidation: 1 +directory invalidation: 0 +opendir: 4 +EOF + test_cmp ../trace.expect ../trace +' + +test_expect_success 'untracked cache after first status' ' + test-dump-untracked-cache >../actual && + test_cmp ../dump.expect ../actual +' + +test_expect_success 'status second time (fully populated cache)' ' + avoid_racy && + : >../trace && + GIT_TRACE_UNTRACKED_STATS="$TRASH_DIRECTORY/trace" \ + git status --porcelain >../actual && + test_cmp ../status.expect ../actual && + cat >../trace.expect <<EOF && +node creation: 0 +gitignore invalidation: 0 +directory invalidation: 0 +opendir: 0 +EOF + test_cmp ../trace.expect ../trace +' + +test_expect_success 'untracked cache after second status' ' + test-dump-untracked-cache >../actual && + test_cmp ../dump.expect ../actual +' + +test_expect_success 'modify in root directory, one dir invalidation' ' + avoid_racy && + : >four && + : >../trace && + GIT_TRACE_UNTRACKED_STATS="$TRASH_DIRECTORY/trace" \ + git status --porcelain >../actual && + cat >../status.expect <<EOF && +A done/one +A one +A two +?? dthree/ +?? dtwo/ +?? four +?? three +EOF + test_cmp ../status.expect ../actual && + cat >../trace.expect <<EOF && +node creation: 0 +gitignore invalidation: 0 +directory invalidation: 1 +opendir: 1 +EOF + test_cmp ../trace.expect ../trace + +' + +test_expect_success 'verify untracked cache dump' ' + test-dump-untracked-cache >../actual && + cat >../expect <<EOF && +info/exclude e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 +core.excludesfile 0000000000000000000000000000000000000000 +exclude_per_dir .gitignore +flags 00000006 +/ 0000000000000000000000000000000000000000 recurse valid +dthree/ +dtwo/ +four +three +/done/ 0000000000000000000000000000000000000000 recurse valid +/dthree/ 0000000000000000000000000000000000000000 recurse check_only valid +three +/dtwo/ 0000000000000000000000000000000000000000 recurse check_only valid +two +EOF + test_cmp ../expect ../actual +' + +test_expect_success 'new .gitignore invalidates recursively' ' + avoid_racy && + echo four >.gitignore && + : >../trace && + GIT_TRACE_UNTRACKED_STATS="$TRASH_DIRECTORY/trace" \ + git status --porcelain >../actual && + cat >../status.expect <<EOF && +A done/one +A one +A two +?? .gitignore +?? dthree/ +?? dtwo/ +?? three +EOF + test_cmp ../status.expect ../actual && + cat >../trace.expect <<EOF && +node creation: 0 +gitignore invalidation: 1 +directory invalidation: 1 +opendir: 4 +EOF + test_cmp ../trace.expect ../trace + +' + +test_expect_success 'verify untracked cache dump' ' + test-dump-untracked-cache >../actual && + cat >../expect <<EOF && +info/exclude e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 +core.excludesfile 0000000000000000000000000000000000000000 +exclude_per_dir .gitignore +flags 00000006 +/ e6fcc8f2ee31bae321d66afd183fcb7237afae6e recurse valid +.gitignore +dthree/ +dtwo/ +three +/done/ 0000000000000000000000000000000000000000 recurse valid +/dthree/ 0000000000000000000000000000000000000000 recurse check_only valid +three +/dtwo/ 0000000000000000000000000000000000000000 recurse check_only valid +two +EOF + test_cmp ../expect ../actual +' + +test_expect_success 'new info/exclude invalidates everything' ' + avoid_racy && + echo three >>.git/info/exclude && + : >../trace && + GIT_TRACE_UNTRACKED_STATS="$TRASH_DIRECTORY/trace" \ + git status --porcelain >../actual && + cat >../status.expect <<EOF && +A done/one +A one +A two +?? .gitignore +?? dtwo/ +EOF + test_cmp ../status.expect ../actual && + cat >../trace.expect <<EOF && +node creation: 0 +gitignore invalidation: 1 +directory invalidation: 0 +opendir: 4 +EOF + test_cmp ../trace.expect ../trace +' + +test_expect_success 'verify untracked cache dump' ' + test-dump-untracked-cache >../actual && + cat >../expect <<EOF && +info/exclude 13263c0978fb9fad16b2d580fb800b6d811c3ff0 +core.excludesfile 0000000000000000000000000000000000000000 +exclude_per_dir .gitignore +flags 00000006 +/ e6fcc8f2ee31bae321d66afd183fcb7237afae6e recurse valid +.gitignore +dtwo/ +/done/ 0000000000000000000000000000000000000000 recurse valid +/dthree/ 0000000000000000000000000000000000000000 recurse check_only valid +/dtwo/ 0000000000000000000000000000000000000000 recurse check_only valid +two +EOF + test_cmp ../expect ../actual +' + +test_expect_success 'move two from tracked to untracked' ' + git rm --cached two && + test-dump-untracked-cache >../actual && + cat >../expect <<EOF && +info/exclude 13263c0978fb9fad16b2d580fb800b6d811c3ff0 +core.excludesfile 0000000000000000000000000000000000000000 +exclude_per_dir .gitignore +flags 00000006 +/ e6fcc8f2ee31bae321d66afd183fcb7237afae6e recurse +/done/ 0000000000000000000000000000000000000000 recurse valid +/dthree/ 0000000000000000000000000000000000000000 recurse check_only valid +/dtwo/ 0000000000000000000000000000000000000000 recurse check_only valid +two +EOF + test_cmp ../expect ../actual +' + +test_expect_success 'status after the move' ' + : >../trace && + GIT_TRACE_UNTRACKED_STATS="$TRASH_DIRECTORY/trace" \ + git status --porcelain >../actual && + cat >../status.expect <<EOF && +A done/one +A one +?? .gitignore +?? dtwo/ +?? two +EOF + test_cmp ../status.expect ../actual && + cat >../trace.expect <<EOF && +node creation: 0 +gitignore invalidation: 0 +directory invalidation: 0 +opendir: 1 +EOF + test_cmp ../trace.expect ../trace +' + +test_expect_success 'verify untracked cache dump' ' + test-dump-untracked-cache >../actual && + cat >../expect <<EOF && +info/exclude 13263c0978fb9fad16b2d580fb800b6d811c3ff0 +core.excludesfile 0000000000000000000000000000000000000000 +exclude_per_dir .gitignore +flags 00000006 +/ e6fcc8f2ee31bae321d66afd183fcb7237afae6e recurse valid +.gitignore +dtwo/ +two +/done/ 0000000000000000000000000000000000000000 recurse valid +/dthree/ 0000000000000000000000000000000000000000 recurse check_only valid +/dtwo/ 0000000000000000000000000000000000000000 recurse check_only valid +two +EOF + test_cmp ../expect ../actual +' + +test_expect_success 'move two from untracked to tracked' ' + git add two && + test-dump-untracked-cache >../actual && + cat >../expect <<EOF && +info/exclude 13263c0978fb9fad16b2d580fb800b6d811c3ff0 +core.excludesfile 0000000000000000000000000000000000000000 +exclude_per_dir .gitignore +flags 00000006 +/ e6fcc8f2ee31bae321d66afd183fcb7237afae6e recurse +/done/ 0000000000000000000000000000000000000000 recurse valid +/dthree/ 0000000000000000000000000000000000000000 recurse check_only valid +/dtwo/ 0000000000000000000000000000000000000000 recurse check_only valid +two +EOF + test_cmp ../expect ../actual +' + +test_expect_success 'status after the move' ' + : >../trace && + GIT_TRACE_UNTRACKED_STATS="$TRASH_DIRECTORY/trace" \ + git status --porcelain >../actual && + cat >../status.expect <<EOF && +A done/one +A one +A two +?? .gitignore +?? dtwo/ +EOF + test_cmp ../status.expect ../actual && + cat >../trace.expect <<EOF && +node creation: 0 +gitignore invalidation: 0 +directory invalidation: 0 +opendir: 1 +EOF + test_cmp ../trace.expect ../trace +' + +test_expect_success 'verify untracked cache dump' ' + test-dump-untracked-cache >../actual && + cat >../expect <<EOF && +info/exclude 13263c0978fb9fad16b2d580fb800b6d811c3ff0 +core.excludesfile 0000000000000000000000000000000000000000 +exclude_per_dir .gitignore +flags 00000006 +/ e6fcc8f2ee31bae321d66afd183fcb7237afae6e recurse valid +.gitignore +dtwo/ +/done/ 0000000000000000000000000000000000000000 recurse valid +/dthree/ 0000000000000000000000000000000000000000 recurse check_only valid +/dtwo/ 0000000000000000000000000000000000000000 recurse check_only valid +two +EOF + test_cmp ../expect ../actual +' + +test_done diff --git a/t/t7601-merge-pull-config.sh b/t/t7601-merge-pull-config.sh index f768c900ab..c6c44ec570 100755 --- a/t/t7601-merge-pull-config.sh +++ b/t/t7601-merge-pull-config.sh @@ -45,6 +45,14 @@ test_expect_success 'fast-forward pull succeeds with "true" in pull.ff' ' test "$(git rev-parse HEAD)" = "$(git rev-parse c1)" ' +test_expect_success 'pull.ff=true overrides merge.ff=false' ' + git reset --hard c0 && + test_config merge.ff false && + test_config pull.ff true && + git pull . c1 && + test "$(git rev-parse HEAD)" = "$(git rev-parse c1)" +' + test_expect_success 'fast-forward pull creates merge with "false" in pull.ff' ' git reset --hard c0 && test_config pull.ff false && diff --git a/test-dump-untracked-cache.c b/test-dump-untracked-cache.c new file mode 100644 index 0000000000..25d855d98b --- /dev/null +++ b/test-dump-untracked-cache.c @@ -0,0 +1,62 @@ +#include "cache.h" +#include "dir.h" + +static int compare_untracked(const void *a_, const void *b_) +{ + const char *const *a = a_; + const char *const *b = b_; + return strcmp(*a, *b); +} + +static int compare_dir(const void *a_, const void *b_) +{ + const struct untracked_cache_dir *const *a = a_; + const struct untracked_cache_dir *const *b = b_; + return strcmp((*a)->name, (*b)->name); +} + +static void dump(struct untracked_cache_dir *ucd, struct strbuf *base) +{ + int i, len; + qsort(ucd->untracked, ucd->untracked_nr, sizeof(*ucd->untracked), + compare_untracked); + qsort(ucd->dirs, ucd->dirs_nr, sizeof(*ucd->dirs), + compare_dir); + len = base->len; + strbuf_addf(base, "%s/", ucd->name); + printf("%s %s", base->buf, + sha1_to_hex(ucd->exclude_sha1)); + if (ucd->recurse) + fputs(" recurse", stdout); + if (ucd->check_only) + fputs(" check_only", stdout); + if (ucd->valid) + fputs(" valid", stdout); + printf("\n"); + for (i = 0; i < ucd->untracked_nr; i++) + printf("%s\n", ucd->untracked[i]); + for (i = 0; i < ucd->dirs_nr; i++) + dump(ucd->dirs[i], base); + strbuf_setlen(base, len); +} + +int main(int ac, char **av) +{ + struct untracked_cache *uc; + struct strbuf base = STRBUF_INIT; + setup_git_directory(); + if (read_cache() < 0) + die("unable to read index file"); + uc = the_index.untracked; + if (!uc) { + printf("no untracked cache\n"); + return 0; + } + printf("info/exclude %s\n", sha1_to_hex(uc->ss_info_exclude.sha1)); + printf("core.excludesfile %s\n", sha1_to_hex(uc->ss_excludes_file.sha1)); + printf("exclude_per_dir %s\n", uc->exclude_per_dir); + printf("flags %08x\n", uc->dir_flags); + if (uc->root) + dump(uc->root, &base); + return 0; +} diff --git a/tree-walk.c b/tree-walk.c index 5dd9a71804..6dccd2d5dd 100644 --- a/tree-walk.c +++ b/tree-walk.c @@ -415,6 +415,12 @@ int traverse_trees(int n, struct tree_desc *t, struct traverse_info *info) return error; } +struct dir_state { + void *tree; + unsigned long size; + unsigned char sha1[20]; +}; + static int find_tree_entry(struct tree_desc *t, const char *name, unsigned char *result, unsigned *mode) { int namelen = strlen(name); @@ -478,6 +484,206 @@ int get_tree_entry(const unsigned char *tree_sha1, const char *name, unsigned ch return retval; } +/* + * This is Linux's built-in max for the number of symlinks to follow. + * That limit, of course, does not affect git, but it's a reasonable + * choice. + */ +#define GET_TREE_ENTRY_FOLLOW_SYMLINKS_MAX_LINKS 40 + +/** + * Find a tree entry by following symlinks in tree_sha (which is + * assumed to be the root of the repository). In the event that a + * symlink points outside the repository (e.g. a link to /foo or a + * root-level link to ../foo), the portion of the link which is + * outside the repository will be returned in result_path, and *mode + * will be set to 0. It is assumed that result_path is uninitialized. + * If there are no symlinks, or the end result of the symlink chain + * points to an object inside the repository, result will be filled in + * with the sha1 of the found object, and *mode will hold the mode of + * the object. + * + * See the code for enum follow_symlink_result for a description of + * the return values. + */ +enum follow_symlinks_result get_tree_entry_follow_symlinks(unsigned char *tree_sha1, const char *name, unsigned char *result, struct strbuf *result_path, unsigned *mode) +{ + int retval = MISSING_OBJECT; + struct dir_state *parents = NULL; + size_t parents_alloc = 0; + ssize_t parents_nr = 0; + unsigned char current_tree_sha1[20]; + struct strbuf namebuf = STRBUF_INIT; + struct tree_desc t; + int follows_remaining = GET_TREE_ENTRY_FOLLOW_SYMLINKS_MAX_LINKS; + int i; + + init_tree_desc(&t, NULL, 0UL); + strbuf_init(result_path, 0); + strbuf_addstr(&namebuf, name); + hashcpy(current_tree_sha1, tree_sha1); + + while (1) { + int find_result; + char *first_slash; + char *remainder = NULL; + + if (!t.buffer) { + void *tree; + unsigned char root[20]; + unsigned long size; + tree = read_object_with_reference(current_tree_sha1, + tree_type, &size, + root); + if (!tree) + goto done; + + ALLOC_GROW(parents, parents_nr + 1, parents_alloc); + parents[parents_nr].tree = tree; + parents[parents_nr].size = size; + hashcpy(parents[parents_nr].sha1, root); + parents_nr++; + + if (namebuf.buf[0] == '\0') { + hashcpy(result, root); + retval = FOUND; + goto done; + } + + if (!size) + goto done; + + /* descend */ + init_tree_desc(&t, tree, size); + } + + /* Handle symlinks to e.g. a//b by removing leading slashes */ + while (namebuf.buf[0] == '/') { + strbuf_remove(&namebuf, 0, 1); + } + + /* Split namebuf into a first component and a remainder */ + if ((first_slash = strchr(namebuf.buf, '/'))) { + *first_slash = 0; + remainder = first_slash + 1; + } + + if (!strcmp(namebuf.buf, "..")) { + struct dir_state *parent; + /* + * We could end up with .. in the namebuf if it + * appears in a symlink. + */ + + if (parents_nr == 1) { + if (remainder) + *first_slash = '/'; + strbuf_add(result_path, namebuf.buf, + namebuf.len); + *mode = 0; + retval = FOUND; + goto done; + } + parent = &parents[parents_nr - 1]; + free(parent->tree); + parents_nr--; + parent = &parents[parents_nr - 1]; + init_tree_desc(&t, parent->tree, parent->size); + strbuf_remove(&namebuf, 0, remainder ? 3 : 2); + continue; + } + + /* We could end up here via a symlink to dir/.. */ + if (namebuf.buf[0] == '\0') { + hashcpy(result, parents[parents_nr - 1].sha1); + retval = FOUND; + goto done; + } + + /* Look up the first (or only) path component in the tree. */ + find_result = find_tree_entry(&t, namebuf.buf, + current_tree_sha1, mode); + if (find_result) { + goto done; + } + + if (S_ISDIR(*mode)) { + if (!remainder) { + hashcpy(result, current_tree_sha1); + retval = FOUND; + goto done; + } + /* Descend the tree */ + t.buffer = NULL; + strbuf_remove(&namebuf, 0, + 1 + first_slash - namebuf.buf); + } else if (S_ISREG(*mode)) { + if (!remainder) { + hashcpy(result, current_tree_sha1); + retval = FOUND; + } else { + retval = NOT_DIR; + } + goto done; + } else if (S_ISLNK(*mode)) { + /* Follow a symlink */ + unsigned long link_len; + size_t len; + char *contents, *contents_start; + struct dir_state *parent; + enum object_type type; + + if (follows_remaining-- == 0) { + /* Too many symlinks followed */ + retval = SYMLINK_LOOP; + goto done; + } + + /* + * At this point, we have followed at a least + * one symlink, so on error we need to report this. + */ + retval = DANGLING_SYMLINK; + + contents = read_sha1_file(current_tree_sha1, &type, + &link_len); + + if (!contents) + goto done; + + if (contents[0] == '/') { + strbuf_addstr(result_path, contents); + free(contents); + *mode = 0; + retval = FOUND; + goto done; + } + + if (remainder) + len = first_slash - namebuf.buf; + else + len = namebuf.len; + + contents_start = contents; + + parent = &parents[parents_nr - 1]; + init_tree_desc(&t, parent->tree, parent->size); + strbuf_splice(&namebuf, 0, len, + contents_start, link_len); + if (remainder) + namebuf.buf[link_len] = '/'; + free(contents); + } + } +done: + for (i = 0; i < parents_nr; i++) + free(parents[i].tree); + free(parents); + + strbuf_release(&namebuf); + return retval; +} + static int match_entry(const struct pathspec_item *item, const struct name_entry *entry, int pathlen, const char *match, int matchlen, diff --git a/tree-walk.h b/tree-walk.h index ae7fb3a824..3b2f7bf17d 100644 --- a/tree-walk.h +++ b/tree-walk.h @@ -40,6 +40,24 @@ struct traverse_info; typedef int (*traverse_callback_t)(int n, unsigned long mask, unsigned long dirmask, struct name_entry *entry, struct traverse_info *); int traverse_trees(int n, struct tree_desc *t, struct traverse_info *info); +enum follow_symlinks_result { + FOUND = 0, /* This includes out-of-tree links */ + MISSING_OBJECT = -1, /* The initial symlink is missing */ + DANGLING_SYMLINK = -2, /* + * The initial symlink is there, but + * (transitively) points to a missing + * in-tree file + */ + SYMLINK_LOOP = -3, + NOT_DIR = -4, /* + * Somewhere along the symlink chain, a path is + * requested which contains a file as a + * non-final element. + */ +}; + +enum follow_symlinks_result get_tree_entry_follow_symlinks(unsigned char *tree_sha1, const char *name, unsigned char *result, struct strbuf *result_path, unsigned *mode); + struct traverse_info { struct traverse_info *prev; struct name_entry name; diff --git a/unpack-trees.c b/unpack-trees.c index be84ba2607..2927660d92 100644 --- a/unpack-trees.c +++ b/unpack-trees.c @@ -9,6 +9,7 @@ #include "refs.h" #include "attr.h" #include "split-index.h" +#include "dir.h" /* * Error messages expected by scripts out of plumbing commands such as @@ -1259,8 +1260,10 @@ static int verify_uptodate_sparse(const struct cache_entry *ce, static void invalidate_ce_path(const struct cache_entry *ce, struct unpack_trees_options *o) { - if (ce) - cache_tree_invalidate_path(o->src_index, ce->name); + if (!ce) + return; + cache_tree_invalidate_path(o->src_index, ce->name); + untracked_cache_invalidate_path(o->src_index, ce->name); } /* diff --git a/upload-pack.c b/upload-pack.c index 1cb9a948aa..89e832b64a 100644 --- a/upload-pack.c +++ b/upload-pack.c @@ -35,7 +35,11 @@ static int multi_ack; static int no_done; static int use_thin_pack, use_ofs_delta, use_include_tag; static int no_progress, daemon_mode; -static int allow_tip_sha1_in_want; +/* Allow specifying sha1 if it is a ref tip. */ +#define ALLOW_TIP_SHA1 01 +/* Allow request of a sha1 if it is reachable from a ref (possibly hidden ref). */ +#define ALLOW_REACHABLE_SHA1 02 +static unsigned int allow_unadvertised_object_request; static int shallow_nr; static struct object_array have_obj; static struct object_array want_obj; @@ -442,8 +446,9 @@ static int get_common_commits(void) static int is_our_ref(struct object *o) { - return o->flags & - ((allow_tip_sha1_in_want ? HIDDEN_REF : 0) | OUR_REF); + int allow_hidden_ref = (allow_unadvertised_object_request & + (ALLOW_TIP_SHA1 | ALLOW_REACHABLE_SHA1)); + return o->flags & ((allow_hidden_ref ? HIDDEN_REF : 0) | OUR_REF); } static void check_non_tip(void) @@ -456,8 +461,12 @@ static void check_non_tip(void) char namebuf[42]; /* ^ + SHA-1 + LF */ int i; - /* In the normal in-process case non-tip request can never happen */ - if (!stateless_rpc) + /* + * In the normal in-process case without + * uploadpack.allowReachableSHA1InWant, + * non-tip requests can never happen. + */ + if (!stateless_rpc && !(allow_unadvertised_object_request & ALLOW_REACHABLE_SHA1)) goto error; cmd.argv = argv; @@ -726,10 +735,13 @@ static int send_ref(const char *refname, const struct object_id *oid, struct strbuf symref_info = STRBUF_INIT; format_symref_info(&symref_info, cb_data); - packet_write(1, "%s %s%c%s%s%s%s agent=%s\n", + packet_write(1, "%s %s%c%s%s%s%s%s agent=%s\n", oid_to_hex(oid), refname_nons, 0, capabilities, - allow_tip_sha1_in_want ? " allow-tip-sha1-in-want" : "", + (allow_unadvertised_object_request & ALLOW_TIP_SHA1) ? + " allow-tip-sha1-in-want" : "", + (allow_unadvertised_object_request & ALLOW_REACHABLE_SHA1) ? + " allow-reachable-sha1-in-want" : "", stateless_rpc ? " no-done" : "", symref_info.buf, git_user_agent_sanitized()); @@ -789,9 +801,17 @@ static void upload_pack(void) static int upload_pack_config(const char *var, const char *value, void *unused) { - if (!strcmp("uploadpack.allowtipsha1inwant", var)) - allow_tip_sha1_in_want = git_config_bool(var, value); - else if (!strcmp("uploadpack.keepalive", var)) { + if (!strcmp("uploadpack.allowtipsha1inwant", var)) { + if (git_config_bool(var, value)) + allow_unadvertised_object_request |= ALLOW_TIP_SHA1; + else + allow_unadvertised_object_request &= ~ALLOW_TIP_SHA1; + } else if (!strcmp("uploadpack.allowreachablesha1inwant", var)) { + if (git_config_bool(var, value)) + allow_unadvertised_object_request |= ALLOW_REACHABLE_SHA1; + else + allow_unadvertised_object_request &= ~ALLOW_REACHABLE_SHA1; + } else if (!strcmp("uploadpack.keepalive", var)) { keepalive = git_config_int(var, value); if (!keepalive) keepalive = -1; diff --git a/wt-status.c b/wt-status.c index 38cb165f12..c56c78fb6f 100644 --- a/wt-status.c +++ b/wt-status.c @@ -585,6 +585,8 @@ static void wt_status_collect_untracked(struct wt_status *s) DIR_SHOW_OTHER_DIRECTORIES | DIR_HIDE_EMPTY_DIRECTORIES; if (s->show_ignored_files) dir.flags |= DIR_SHOW_IGNORED_TOO; + else + dir.untracked = the_index.untracked; setup_standard_excludes(&dir); fill_directory(&dir, &s->pathspec); @@ -1532,21 +1534,15 @@ static void wt_shortstatus_print_tracking(struct wt_status *s) color_fprintf(s->fp, branch_color_local, "%s", branch_name); - switch (stat_tracking_info(branch, &num_ours, &num_theirs)) { - case 0: - /* no base */ - fputc(s->null_termination ? '\0' : '\n', s->fp); - return; - case -1: - /* with "gone" base */ + if (stat_tracking_info(branch, &num_ours, &num_theirs, &base) < 0) { + if (!base) { + fputc(s->null_termination ? '\0' : '\n', s->fp); + return; + } + upstream_is_gone = 1; - break; - default: - /* with base */ - break; } - base = branch->merge[0]->dst; base = shorten_unambiguous_ref(base, 0); color_fprintf(s->fp, header_color, "..."); color_fprintf(s->fp, branch_color_remote, "%s", base); |