diff options
221 files changed, 9599 insertions, 2271 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.5.0.txt b/Documentation/RelNotes/2.5.0.txt new file mode 100644 index 0000000000..5030b24d85 --- /dev/null +++ b/Documentation/RelNotes/2.5.0.txt @@ -0,0 +1,508 @@ +Git 2.5 Release Notes +===================== + +Updates since v2.4 +------------------ + +UI, Workflows & Features + + * The bash completion script (in contrib/) learned a few options that + "git revert" takes. + + * Whitespace breakages in deleted and context lines can also be + painted in the output of "git diff" and friends with the new + --ws-error-highlight option. + + * 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. + + * git p4 attempts to better handle branches in Perforce. + + * "git p4" learned "--changes-block-size <n>" to read the changes in + chunks from Perforce, instead of making one call to "p4 changes" + that may trigger "too many rows scanned" error from Perforce. + + * More workaround for Perforce's row number limit in "git p4". + + * Unlike "$EDITOR" and "$GIT_EDITOR" that can hold the path to the + command and initial options (e.g. "/path/to/emacs -nw"), 'git p4' + did not let the shell interpolate the contents of the environment + variable that name the editor "$P4EDITOR" (and "$EDITOR", too). + This release makes it in line with the rest of Git, as well as with + Perforce. + + * A new short-hand <branch>@{push} denotes the remote-tracking branch + that tracks the branch at the remote the <branch> would be pushed + to. + + * "git show-branch --topics HEAD" (with no other arguments) did not + do anything interesting. Instead, contrast the given revision + against all the local branches by default. + + * A replacement for contrib/workdir/git-new-workdir that does not + rely on symbolic links and make sharing of objects and refs safer + by making the borrowee and borrowers aware of each other. + + Consider this as still an experimental feature; the UI will likely + to change. + + * Tweak the sample "store" backend of the credential helper to honor + XDG configuration file locations when specified. + + * A heuristic we use to catch mistyped paths on the command line + "git <cmd> <revs> <pathspec>" is to make sure that all the non-rev + parameters in the later part of the command line are names of the + files in the working tree, but that means "git grep $str -- \*.c" + must always be disambiguated with "--", because nobody sane will + create a file whose name literally is asterisk-dot-see. Loosen the + heuristic to declare that with a wildcard string the user likely + meant to give us a pathspec. + + * "git merge FETCH_HEAD" learned that the previous "git fetch" could + be to create an Octopus merge, i.e. recording multiple branches + that are not marked as "not-for-merge"; this allows us to lose an + old style invocation "git merge <msg> HEAD $commits..." in the + implementation of "git pull" script; the old style syntax can now + be deprecated (but not removed yet). + + * Filter scripts were run with SIGPIPE disabled on the Git side, + expecting that they may not read what Git feeds them to filter. + We however treated a filter that does not read its input fully + before exiting as an error. We no longer do and ignore EPIPE + when writing to feed the filter scripts. + + This changes semantics, but arguably in a good way. If a filter + can produce its output without fully consuming its input using + whatever magic, we now let it do so, instead of diagnosing it + as a programming error. + + * Instead of dying immediately upon failing to obtain a lock, the + locking (of refs etc) retries after a short while with backoff. + + * Introduce http.<url>.SSLCipherList configuration variable to tweak + the list of cipher suite to be used with libcURL when talking with + https:// sites. + + * "git subtree" script (in contrib/) used "echo -n" to produce + progress messages in a non-portable way. + + * "git subtree" script (in contrib/) does not have --squash option + when pushing, but the documentation and help text pretended as if + it did. + + * The Git subcommand completion (in contrib/) no longer lists credential + helpers among candidates; they are not something the end user would + 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. + + Consider this as still an experimental and incomplete feature: + + - We may want to do the same for in-index objects, e.g. + asking for :RelNotes with this option should give + :Documentation/RelNotes/2.5.0.txt, too + + - "git cat-file --follow-symlinks blob HEAD:RelNotes" + may also be something we want to allow in the future. + + * "git send-email" learned the alias file format used by the sendmail + program (in a simplified form; we obviously do not feed pipes). + + * "git am" learned am.threeWay configuration variable. + + * Traditionally, external low-level 3-way merge drivers are expected + to produce their results based solely on the contents of the three + variants given in temporary files named by %O, %A and %B on their + command line. Additionally allow them to look at the final path + (given by %P). + + * "git blame" learned blame.showEmail configuration variable. + + * "git apply" cannot diagnose a patch corruption when the breakage is + to mark the length of the hunk shorter than it really is on the + hunk header line "@@ -l,k +m,n @@"; one special case it could is + when the hunk becomes no-op (e.g. k == n == 2 for two-line context + patch output), and it learned to do so in this special case. + + * Add the "--allow-unknown-type" option to "cat-file" to allow + inspecting loose objects of an experimental or a broken type. + + * Many long-running operations show progress eye-candy, even when + they are later backgrounded. Hide the eye-candy when the process + is sent to the background instead. + (merge 9a9a41d lm/squelch-bg-progress later to maint). + + +Performance, Internal Implementation, Development Support etc. + + * "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 + end, when completed. + + * for_each_ref() callback functions were taught to name the objects + not with "unsigned char sha1[20]" but with "struct object_id". + + * Catch a programmer mistake to feed a pointer not an array to + ARRAY_SIZE() macro, by using a couple of GCC extensions. + + * Some error messages in "git config" were emitted without calling + the usual error() facility. + + * When "add--interactive" splits a hunk into two overlapping hunks + and then let the user choose only one, it sometimes feeds an + incorrect patch text to "git apply". Add tests to demonstrate + this. + + I have a slight suspicion that this may be $gmane/87202 coming back + and biting us (I seem to have said "let's run with this and see + what happens" back then). + + * More line-ending tests. + + * An earlier rewrite to use strbuf_getwholeline() instead of fgets(3) + to read packed-refs file revealed that the former is unacceptably + inefficient. It has been optimized by using getdelim(3) when + available. + + * The refs API uses ref_lock struct which had its own "int fd", even + though the same file descriptor was in the lock struct it contains. + Clean-up the code to lose this redundant field. + + * There was a dead code that used to handle "git pull --tags" and + show special-cased error message, which was made irrelevant when + the semantics of the option changed back in Git 1.9 days. + (merge 19d122b pt/pull-tags-error-diag later to maint). + + * Help us to find broken test script that splits the body part of the + test by mistaken use of wrong kind of quotes. + (merge d93d5d5 jc/test-prereq-validate later to maint). + + * Developer support to automatically detect broken &&-chain in the + test scripts is now turned on by default. + (merge 92b269f jk/test-chain-lint later to maint). + + * Error reporting mechanism used in "refs" API has been made more + consistent. + + * "git pull" has more test coverage now. + + * "git pull" has become more aware of the options meant for + underlying "git fetch" and then learned to use parse-options + parser. + +Also contains various documentation updates and code clean-ups. + + +Fixes since v2.4 +---------------- + +Unless otherwise noted, all the fixes since v2.4 in the maintenance +track are contained in this release (see the maintenance releases' +notes for details). + + * Git 2.4 broke setting verbosity and progress levels on "git clone" + with native transports. + (merge 822f0c4 mh/clone-verbosity-fix later to maint). + + * "git add -e" did not allow the user to abort the operation by + killing the editor. + (merge cb64800 jk/add-e-kill-editor later to maint). + + * Memory usage of "git index-pack" has been trimmed by tens of + per-cent. + (merge a78c5b3 nd/slim-index-pack-memory-usage later to maint). + + * "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. + (merge b6e8a3b jk/still-interesting later to maint). + + * "hash-object --literally" introduced in v2.2 was not prepared to + take a really long object type name. + (merge 1427a7f jc/hash-object later to maint). + + * "git rebase --quiet" was not quite quiet when there is nothing to + do. + (merge 22946a9 jk/rebase-quiet-noop later to maint). + + * The completion for "log --decorate=" parameter value was incorrect. + (merge af16bda sg/complete-decorate-full-not-long later to maint). + + * "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". + (merge df06201 jk/filter-branch-use-of-sed-on-incomplete-line later to maint). + + * "git daemon" fails to build from the source under NO_IPV6 + configuration (regression in 2.4). + (merge d358f77 jc/daemon-no-ipv6-for-2.4.1 later to maint). + + * 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 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 + ".git" directory directly at the top level of the working tree, but + the code misdetected the case in which the working tree is at the + root level of the filesystem (which arguably is a silly thing to + do, but still valid). + (merge 84ccad8 jk/init-core-worktree-at-root later to maint). + + * "git commit --date=now" or anything that relies on approxidate lost + the daylight-saving-time offset. + (merge f6e6362 jc/epochtime-wo-tz later to maint). + + * Access to objects in repositories that borrow from another one on a + slow NFS server unnecessarily got more expensive due to recent code + becoming more cautious in a naive way not to lose objects to pruning. + (merge ee1c6c3 jk/prune-mtime later to maint). + + * The codepaths that read .gitignore and .gitattributes files have been + taught that these files encoded in UTF-8 may have UTF-8 BOM marker at + the beginning; this makes it in line with what we do for configuration + files already. + (merge 27547e5 cn/bom-in-gitignore later to maint). + + * a few helper scripts in the test suite did not report errors + correctly. + (merge de248e9 ep/fix-test-lib-functions-report later to maint). + + * The default $HOME/.gitconfig file created upon "git config --global" + that edits it had incorrectly spelled user.name and user.email + entries in it. + (merge 7e11052 oh/fix-config-default-user-name-section later to maint). + + * "git cat-file bl $blob" failed to barf even though there is no + object type that is "bl". + (merge b7994af jk/type-from-string-gently later to maint). + + * The usual "git diff" when seeing a file turning into a directory + showed a patchset to remove the file and create all files in the + directory, but "git diff --no-index" simply refused to work. Also, + when asked to compare a file and a directory, imitate POSIX "diff" + and compare the file with the file with the same name in the + directory, instead of refusing to run. + (merge 0615173 jc/diff-no-index-d-f later to maint). + + * "git rebase -i" moved the "current" command from "todo" to "done" a + bit too prematurely, losing a step when a "pick" did not even start. + (merge 8cbc57c ph/rebase-i-redo later to maint). + + * The connection initiation code for "ssh" transport tried to absorb + differences between the stock "ssh" and Putty-supplied "plink" and + its derivatives, but the logic to tell that we are using "plink" + variants were too loose and falsely triggered when "plink" appeared + anywhere in the path (e.g. "/home/me/bin/uplink/ssh"). + (merge baaf233 bc/connect-plink later to maint). + + * 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). + (merge a0b4507 jk/git-no-more-argv0-path-munging later to maint). + + * core.excludesfile (defaulting to $XDG_HOME/git/ignore) is supposed + to be overridden by repository-specific .git/info/exclude file, but + the order was swapped from the beginning. This belatedly fixes it. + (merge 099d2d8 jc/gitignore-precedence later to maint). + + * After "git add -N", the path appeared in output of "git diff HEAD" + and "git diff --cached HEAD", leading "git status" to classify it + as "Changes to be committed". Such a path, however, is not yet to + be scheduled to be committed. "git diff" showed the change to the + path as modification, not as a "new file", in the header of its + output. + + Treat such paths as "yet to be added to the index but Git already + know about them"; "git diff HEAD" and "git diff --cached HEAD" + should not talk about them, and "git diff" should show them as new + files yet to be added to the index. + (merge d95d728 nd/diff-i-t-a later to maint). + + * There was a commented-out (instead of being marked to expect + failure) test that documented a breakage that was fixed since the + test was written; turn it into a proper test. + (merge 66d2e04 sb/t1020-cleanup later to maint). + + * The "log --decorate" enhancement in Git 2.4 that shows the commit + at the tip of the current branch e.g. "HEAD -> master", did not + work with --decorate=full. + (merge 429ad20 mg/log-decorate-HEAD later to maint). + + * The ref API did not handle cases where 'refs/heads/xyzzy/frotz' is + removed at the same time as 'refs/heads/xyzzy' is added (or vice + versa) very well. + (merge c628edf mh/ref-directory-file later to maint). + + * Multi-ref transaction support we merged a few releases ago + unnecessarily kept many file descriptors open, risking to fail with + resource exhaustion. This is for 2.4.x track. + (merge 185ce3a mh/write-refs-sooner-2.4 later to maint). + + * "git bundle verify" did not diagnose extra parameters on the + command line. + (merge 7886cfa ps/bundle-verify-arg later to maint). + + * Various documentation mark-up fixes to make the output more + consistent in general and also make AsciiDoctor (an alternative + formatter) happier. + (merge d0258b9 jk/asciidoc-markup-fix later to maint). + (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). + + * Recent "git prune" traverses young unreachable objects to safekeep + old objects in the reachability chain from them, which sometimes + caused error messages that are unnecessarily alarming. + (merge ce4e7b2 jk/squelch-missing-link-warning-for-unreachable later to maint). + + * The configuration reader/writer uses mmap(2) interface to access + the files; when we find a directory, it barfed with "Out of memory?". + (merge 9ca0aaf jk/diagnose-config-mmap-failure later to maint). + + * "color.diff.plain" was a misnomer; give it 'color.diff.context' as + a more logical synonym. + (merge 8dbf3eb jk/color-diff-plain-is-context later to maint). + + * The setup code used to die when core.bare and core.worktree are set + inconsistently, even for commands that do not need working tree. + (merge fada767 jk/die-on-bogus-worktree-late later to maint). + + * Recent Mac OS X updates breaks the logic to detect that the machine + is on the AC power in the sample pre-auto-gc script. + (merge c54c7b3 pa/auto-gc-mac-osx later to maint). + + * "git commit --cleanup=scissors" was not careful enough to protect + against getting fooled by a line that looked like scissors. + (merge fbfa097 sg/commit-cleanup-scissors later to maint). + + * "Have we lost a race with competing repack?" check was too + expensive, especially while receiving a huge object transfer + that runs index-pack (e.g. "clone" or "fetch"). + (merge 0eeb077 jk/index-pack-reduce-recheck later to maint). + + * The tcsh completion writes a bash scriptlet but that would have + failed for users with noclobber set. + (merge 0b1f688 af/tcsh-completion-noclobber later to maint). + + * "git for-each-ref" reported "missing object" for 0{40} when it + encounters a broken ref. The lack of object whose name is 0{40} is + not the problem; the ref being broken is. + (merge 501cf47 mh/reporting-broken-refs-from-for-each-ref later to maint). + + * Various fixes around "git am" that applies a patch to a history + that is not there yet. + (merge 6ea3b67 pt/am-abort-fix later to maint). + + * "git fsck" used to ignore missing or invalid objects recorded in reflog. + (merge 19bf6c9 mh/fsck-reflog-entries later to maint). + + * "git format-patch --ignore-if-upstream A..B" did not like to be fed + tags as boundary commits. + (merge 9b7a61d jc/do-not-feed-tags-to-clear-commit-marks 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 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). + (merge 319b678 jk/sha1-file-reduce-useless-warnings later to maint). + (merge 9a35c14 fg/document-commit-message-stripping later to maint). + (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). + (merge e6a268c sb/glossary-submodule later to maint). + (merge ec48a76 sb/submodule-doc-intro later to maint). + (merge 14f8b9b jk/clone-dissociate later to maint). + (merge 055c7e9 sb/pack-protocol-mention-smart-http later to maint). + (merge 7c37a5d jk/make-fix-dependencies later to maint). + (merge fc0aa39 sg/merge-summary-config later to maint). + (merge 329af6c pt/t0302-needs-sanity later to maint). + (merge d614f07 fk/doc-format-patch-vn later to maint). + (merge 72dbb36 sg/completion-commit-cleanup later to maint). + (merge e654eb2 es/utf8-stupid-compiler-workaround later to maint). + (merge 34b935c es/osx-header-pollutes-mask-macro 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 2e919f0df8..3e37b93ed2 100644 --- a/Documentation/config.txt +++ b/Documentation/config.txt @@ -453,6 +453,8 @@ false), while all other repositories are assumed to be bare (bare core.worktree:: Set the path to the root of the working tree. + If GIT_COMMON_DIR environment variable is set, core.worktree + is ignored and not used for determining the root of working tree. This can be overridden by the GIT_WORK_TREE environment variable and the '--work-tree' command-line option. The value can be an absolute path or relative to the path to @@ -622,6 +624,12 @@ core.commentChar:: If set to "auto", `git-commit` would select a character that is not the beginning character of any line in existing commit messages. +core.packedRefsTimeout:: + The length of time, in milliseconds, to retry when trying to + lock the `packed-refs` file. Value 0 means not to retry at + all; -1 means to try indefinitely. Default is 1000 (i.e., + retry for 1 second). + sequence.editor:: Text editor used by `git rebase -i` for editing the rebase instruction file. The value is meant to be interpreted by the shell when it is used. @@ -761,6 +769,14 @@ am.keepcr:: by giving '--no-keep-cr' from the command line. See linkgit:git-am[1], linkgit:git-mailsplit[1]. +am.threeWay:: + By default, `git am` will fail if the patch does not apply cleanly. When + set to true, this setting tells `git am` to fall back on 3-way merge if + the patch records the identity of blobs it is supposed to apply to and + we have those blobs available locally (equivalent to giving the `--3way` + option from the command line). Defaults to `false`. + See linkgit:git-am[1]. + apply.ignoreWhitespace:: When set to 'change', tells 'git apply' to ignore changes in whitespace, in the same way as the '--ignore-space-change' @@ -1275,6 +1291,13 @@ gc.pruneExpire:: "now" may be used to disable this grace period and always prune unreachable objects immediately. +gc.pruneWorktreesExpire:: + When 'git gc' is run, it will call + 'prune --worktrees --expire 3.months.ago'. + Override the grace period with this config variable. The value + "now" may be used to disable the grace period and prune + $GIT_DIR/worktrees immediately. + gc.reflogExpire:: gc.<pattern>.reflogExpire:: 'git reflog expire' removes reflog entries older than @@ -1561,6 +1584,19 @@ http.saveCookies:: If set, store cookies received during requests to the file specified by http.cookieFile. Has no effect if http.cookieFile is unset. +http.sslCipherList:: + A list of SSL ciphers to use when negotiating an SSL connection. + The available ciphers depend on whether libcurl was built against + NSS or OpenSSL and the particular configuration of the crypto + library in use. Internally this sets the 'CURLOPT_SSL_CIPHER_LIST' + option; see the libcurl documentation for more details on the format + of this list. ++ +Can be overridden by the 'GIT_SSL_CIPHER_LIST' environment variable. +To force git to use libcurl's default cipher list and ignore any +explicit http.sslCipherList option, set 'GIT_SSL_CIPHER_LIST' to the +empty string. + http.sslVerify:: Whether to verify the SSL certificate when fetching or pushing over HTTPS. Can be overridden by the 'GIT_SSL_NO_VERIFY' environment @@ -2531,14 +2567,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 3ad6404dbc..d56ca90998 100644 --- a/Documentation/diff-options.txt +++ b/Documentation/diff-options.txt @@ -291,6 +291,16 @@ ifndef::git-format-patch[] initial indent of the line are considered whitespace errors. Exits with non-zero status if problems are found. Not compatible with --exit-code. + +--ws-error-highlight=<kind>:: + Highlight whitespace errors on lines specified by <kind> + in the color specified by `color.diff.whitespace`. <kind> + is a comma separated list of `old`, `new`, `context`. When + this option is not given, only whitespace errors in `new` + lines are highlighted. E.g. `--ws-error-highlight=new,old` + highlights whitespace errors on both deleted and added lines. + `all` can be used as a short-hand for `old,new,context`. + endif::git-format-patch[] --full-index:: diff --git a/Documentation/git-am.txt b/Documentation/git-am.txt index 0d8ba48f79..dbea6e7ae9 100644 --- a/Documentation/git-am.txt +++ b/Documentation/git-am.txt @@ -10,7 +10,7 @@ SYNOPSIS -------- [verse] 'git am' [--signoff] [--keep] [--[no-]keep-cr] [--[no-]utf8] - [--3way] [--interactive] [--committer-date-is-author-date] + [--[no-]3way] [--interactive] [--committer-date-is-author-date] [--ignore-date] [--ignore-space-change | --ignore-whitespace] [--whitespace=<option>] [-C<n>] [-p<n>] [--directory=<dir>] [--exclude=<path>] [--include=<path>] [--reject] [-q | --quiet] @@ -90,10 +90,13 @@ default. You can use `--no-utf8` to override this. -3:: --3way:: +--no-3way:: When the patch does not apply cleanly, fall back on 3-way merge if the patch records the identity of blobs it is supposed to apply to and we have those blobs - available locally. + available locally. `--no-3way` can be used to override + am.threeWay configuration variable. For more information, + see am.threeWay in linkgit:git-config[1]. --ignore-space-change:: --ignore-whitespace:: diff --git a/Documentation/git-blame.txt b/Documentation/git-blame.txt index 9f23a861ce..e6e947c808 100644 --- a/Documentation/git-blame.txt +++ b/Documentation/git-blame.txt @@ -76,6 +76,8 @@ include::blame-options.txt[] -e:: --show-email:: Show the author email instead of author name (Default: off). + This can also be controlled via the `blame.showEmail` config + option. -w:: Ignore whitespace when comparing the parent's version and diff --git a/Documentation/git-cat-file.txt b/Documentation/git-cat-file.txt index f6a16f4300..319ab4cb08 100644 --- a/Documentation/git-cat-file.txt +++ b/Documentation/git-cat-file.txt @@ -9,8 +9,8 @@ git-cat-file - Provide content or type and size information for repository objec SYNOPSIS -------- [verse] -'git cat-file' (-t | -s | -e | -p | <type> | --textconv ) <object> -'git cat-file' (--batch | --batch-check) < <list-of-objects> +'git cat-file' (-t [--allow-unknown-type]| -s [--allow-unknown-type]| -e | -p | <type> | --textconv ) <object> +'git cat-file' (--batch | --batch-check) [--follow-symlinks] < <list-of-objects> DESCRIPTION ----------- @@ -69,6 +69,65 @@ OPTIONS not be combined with any other options or arguments. See the section `BATCH OUTPUT` below for details. +--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>. @@ -148,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-checkout.txt b/Documentation/git-checkout.txt index d5041082e8..d263a5652f 100644 --- a/Documentation/git-checkout.txt +++ b/Documentation/git-checkout.txt @@ -225,6 +225,19 @@ This means that you can use `git checkout -p` to selectively discard edits from your current working tree. See the ``Interactive Mode'' section of linkgit:git-add[1] to learn how to operate the `--patch` mode. +--to=<path>:: + Check out a branch in a separate working directory at + `<path>`. A new working directory is linked to the current + repository, sharing everything except working directory + specific files such as HEAD, index... See "MULTIPLE WORKING + TREES" section for more information. + +--ignore-other-worktrees:: + `git checkout` refuses when the wanted ref is already checked + out by another worktree. This option makes it check the ref + out anyway. In other words, the ref can be held by more than one + worktree. + <branch>:: Branch to checkout; if it refers to a branch (i.e., a name that, when prepended with "refs/heads/", is a valid ref), then that @@ -388,6 +401,71 @@ $ git reflog -2 HEAD # or $ git log -g -2 HEAD ------------ +MULTIPLE WORKING TREES +---------------------- + +A git repository can support multiple working trees, allowing you to check +out more than one branch at a time. With `git checkout --to` a new working +tree is associated with the repository. This new working tree is called a +"linked working tree" as opposed to the "main working tree" prepared by "git +init" or "git clone". A repository has one main working tree (if it's not a +bare repository) and zero or more linked working trees. + +Each linked working tree has a private sub-directory in the repository's +$GIT_DIR/worktrees directory. The private sub-directory's name is usually +the base name of the linked working tree's path, possibly appended with a +number to make it unique. For example, when `$GIT_DIR=/path/main/.git` the +command `git checkout --to /path/other/test-next next` creates the linked +working tree in `/path/other/test-next` and also creates a +`$GIT_DIR/worktrees/test-next` directory (or `$GIT_DIR/worktrees/test-next1` +if `test-next` is already taken). + +Within a linked working tree, $GIT_DIR is set to point to this private +directory (e.g. `/path/main/.git/worktrees/test-next` in the example) and +$GIT_COMMON_DIR is set to point back to the main working tree's $GIT_DIR +(e.g. `/path/main/.git`). These settings are made in a `.git` file located at +the top directory of the linked working tree. + +Path resolution via `git rev-parse --git-path` uses either +$GIT_DIR or $GIT_COMMON_DIR depending on the path. For example, in the +linked working tree `git rev-parse --git-path HEAD` returns +`/path/main/.git/worktrees/test-next/HEAD` (not +`/path/other/test-next/.git/HEAD` or `/path/main/.git/HEAD`) while `git +rev-parse --git-path refs/heads/master` uses +$GIT_COMMON_DIR and returns `/path/main/.git/refs/heads/master`, +since refs are shared across all working trees. + +See linkgit:gitrepository-layout[5] for more information. The rule of +thumb is do not make any assumption about whether a path belongs to +$GIT_DIR or $GIT_COMMON_DIR when you need to directly access something +inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path. + +When you are done with a linked working tree you can simply delete it. +The working tree's entry in the repository's $GIT_DIR/worktrees +directory will eventually be removed automatically (see +`gc.pruneworktreesexpire` in linkgit::git-config[1]), or you can run +`git prune --worktrees` in the main or any linked working tree to +clean up any stale entries in $GIT_DIR/worktrees. + +If you move a linked working directory to another file system, or +within a file system that does not support hard links, you need to run +at least one git command inside the linked working directory +(e.g. `git status`) in order to update its entry in $GIT_DIR/worktrees +so that it does not get automatically removed. + +To prevent a $GIT_DIR/worktrees entry from from being pruned (which +can be useful in some situations, such as when the +entry's working tree is stored on a portable device), add a file named +'locked' to the entry's directory. The file contains the reason in +plain text. For example, if a linked working tree's `.git` file points +to `/path/main/.git/worktrees/test-next` then a file named +`/path/main/.git/worktrees/test-next/locked` will prevent the +`test-next` entry from being pruned. See +linkgit:gitrepository-layout[5] for details. + +Multiple checkout support for submodules is incomplete. It is NOT +recommended to make multiple checkouts of a superproject. + EXAMPLES -------- 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-format-patch.txt b/Documentation/git-format-patch.txt index bb3ea9372f..0dac4e9b86 100644 --- a/Documentation/git-format-patch.txt +++ b/Documentation/git-format-patch.txt @@ -170,7 +170,7 @@ will want to ensure that threading is disabled for `git send-email`. -v <n>:: --reroll-count=<n>:: Mark the series as the <n>-th iteration of the topic. The - output filenames have `v<n>` pretended to them, and the + output filenames have `v<n>` prepended to them, and the subject prefix ("PATCH" by default, but configurable via the `--subject-prefix` option) has ` v<n>` appended to it. E.g. `--reroll-count=4` may produce `v4-0001-add-makefile.patch` diff --git a/Documentation/git-merge.txt b/Documentation/git-merge.txt index 1f94908e3c..273a1009be 100644 --- a/Documentation/git-merge.txt +++ b/Documentation/git-merge.txt @@ -104,6 +104,10 @@ commit or stash your changes before running 'git merge'. If no commit is given from the command line, merge the remote-tracking branches that the current branch is configured to use as its upstream. See also the configuration section of this manual page. ++ +When `FETCH_HEAD` (and no other commit) is specified, the branches +recorded in the `.git/FETCH_HEAD` file by the previous invocation +of `git fetch` for merging are merged to the current branch. PRE-MERGE CHECKS diff --git a/Documentation/git-p4.txt b/Documentation/git-p4.txt index a1664b9f68..82aa5d6073 100644 --- a/Documentation/git-p4.txt +++ b/Documentation/git-p4.txt @@ -225,9 +225,20 @@ Git repository: they can find the p4 branches in refs/heads. --max-changes <n>:: - Limit the number of imported changes to 'n'. Useful to - limit the amount of history when using the '@all' p4 revision - specifier. + Import at most 'n' changes, rather than the entire range of + changes included in the given revision specifier. A typical + usage would be use '@all' as the revision specifier, but then + to use '--max-changes 1000' to import only the last 1000 + revisions rather than the entire revision history. + +--changes-block-size <n>:: + The internal block size to use when converting a revision + specifier such as '@all' into a list of specific change + numbers. Instead of using a single call to 'p4 changes' to + find the full list of changes for the conversion, there are a + sequence of calls to 'p4 changes -m', each of which requests + one block of changes of the given size. The default block size + is 500, which should usually be suitable. --keep-path:: The mapping of file names from the p4 depot path to Git, by diff --git a/Documentation/git-prune.txt b/Documentation/git-prune.txt index 7a493c80f7..1cf3bed4ab 100644 --- a/Documentation/git-prune.txt +++ b/Documentation/git-prune.txt @@ -48,6 +48,9 @@ OPTIONS --expire <time>:: Only expire loose objects older than <time>. +--worktrees:: + Prune dead working tree information in $GIT_DIR/worktrees. + <head>...:: In addition to objects reachable from any of our references, keep objects diff --git a/Documentation/git-pull.txt b/Documentation/git-pull.txt index 712ab4baed..93c72a29ce 100644 --- a/Documentation/git-pull.txt +++ b/Documentation/git-pull.txt @@ -74,9 +74,6 @@ pulling or stash them away with linkgit:git-stash[1]. OPTIONS ------- -Options meant for 'git pull' itself and the underlying 'git merge' -must be given before the options meant for 'git fetch'. - -q:: --quiet:: This is passed to both underlying git-fetch to squelch reporting of diff --git a/Documentation/git-rev-parse.txt b/Documentation/git-rev-parse.txt index bf81b9734e..c483100e75 100644 --- a/Documentation/git-rev-parse.txt +++ b/Documentation/git-rev-parse.txt @@ -216,6 +216,9 @@ If `$GIT_DIR` is not defined and the current directory is not detected to lie in a Git repository or work tree print a message to stderr and exit with nonzero status. +--git-common-dir:: + Show `$GIT_COMMON_DIR` if defined, else `$GIT_DIR`. + --is-inside-git-dir:: When the current working directory is below the repository directory print "true", otherwise "false". @@ -233,6 +236,13 @@ print a message to stderr and exit with nonzero status. repository. If <path> is a gitfile then the resolved path to the real repository is printed. +--git-path <path>:: + Resolve "$GIT_DIR/<path>" and takes other path relocation + variables such as $GIT_OBJECT_DIRECTORY, + $GIT_INDEX_FILE... into account. For example, if + $GIT_OBJECT_DIRECTORY is set to /foo/bar then "git rev-parse + --git-path objects/abc" returns /foo/bar/abc. + --show-cdup:: When the command is invoked from a subdirectory, show the path of the top-level directory relative to the current diff --git a/Documentation/git-send-email.txt b/Documentation/git-send-email.txt index 804554609d..7ae467ba41 100644 --- a/Documentation/git-send-email.txt +++ b/Documentation/git-send-email.txt @@ -383,7 +383,24 @@ sendemail.aliasesFile:: sendemail.aliasFileType:: Format of the file(s) specified in sendemail.aliasesFile. Must be - one of 'mutt', 'mailrc', 'pine', 'elm', or 'gnus'. + one of 'mutt', 'mailrc', 'pine', 'elm', or 'gnus', or 'sendmail'. ++ +What an alias file in each format looks like can be found in +the documentation of the email program of the same name. The +differences and limitations from the standard formats are +described below: ++ +-- +sendmail;; +* Quoted aliases and quoted addresses are not supported: lines that + contain a `"` symbol are ignored. +* Redirection to a file (`/path/name`) or pipe (`|command`) is not + supported. +* File inclusion (`:include: /path/name`) is not supported. +* Warnings are printed on the standard error output for any + explicitly unsupported constructs, and any other lines that are not + recognized by the parser. +-- sendemail.multiEdit:: If true (default), a single editor instance will be spawned to edit 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 a4a4cf39b2..613ce6bb36 100644 --- a/Documentation/git.txt +++ b/Documentation/git.txt @@ -839,6 +839,15 @@ Git so take care if using Cogito etc. an explicit repository directory set via 'GIT_DIR' or on the command line. +'GIT_COMMON_DIR':: + If this variable is set to a path, non-worktree files that are + normally in $GIT_DIR will be taken from this path + instead. Worktree-specific files such as HEAD or index are + taken from $GIT_DIR. See linkgit:gitrepository-layout[5] and + the section 'MULTIPLE CHECKOUT MODE' in linkgit:checkout[1] + details. This variable has lower precedence than other path + variables such as GIT_INDEX_FILE, GIT_OBJECT_DIRECTORY... + Git Commits ~~~~~~~~~~~ 'GIT_AUTHOR_NAME':: diff --git a/Documentation/gitattributes.txt b/Documentation/gitattributes.txt index 70899b3023..81fe586948 100644 --- a/Documentation/gitattributes.txt +++ b/Documentation/gitattributes.txt @@ -774,7 +774,7 @@ To define a custom merge driver `filfre`, add a section to your ---------------------------------------------------------------- [merge "filfre"] name = feel-free merge driver - driver = filfre %O %A %B + driver = filfre %O %A %B %L %P recursive = binary ---------------------------------------------------------------- @@ -800,6 +800,9 @@ merge between common ancestors, when there are more than one. When left unspecified, the driver itself is used for both internal merge and the final merge. +The merge driver can learn the pathname in which the merged result +will be stored via placeholder `%P`. + `conflict-marker-size` ^^^^^^^^^^^^^^^^^^^^^^ diff --git a/Documentation/gitrepository-layout.txt b/Documentation/gitrepository-layout.txt index 79653f3134..7173b38830 100644 --- a/Documentation/gitrepository-layout.txt +++ b/Documentation/gitrepository-layout.txt @@ -46,6 +46,9 @@ of incomplete object store is not suitable to be published for use with dumb transports but otherwise is OK as long as `objects/info/alternates` points at the object stores it borrows from. ++ +This directory is ignored if $GIT_COMMON_DIR is set and +"$GIT_COMMON_DIR/objects" will be used instead. objects/[0-9a-f][0-9a-f]:: A newly created object is stored in its own file. @@ -92,7 +95,8 @@ refs:: References are stored in subdirectories of this directory. The 'git prune' command knows to preserve objects reachable from refs found in this directory and - its subdirectories. + its subdirectories. This directory is ignored if $GIT_COMMON_DIR + is set and "$GIT_COMMON_DIR/refs" will be used instead. refs/heads/`name`:: records tip-of-the-tree commit objects of branch `name` @@ -114,7 +118,8 @@ refs/replace/`<obj-sha1>`:: packed-refs:: records the same information as refs/heads/, refs/tags/, and friends record in a more efficient way. See - linkgit:git-pack-refs[1]. + linkgit:git-pack-refs[1]. This file is ignored if $GIT_COMMON_DIR + is set and "$GIT_COMMON_DIR/packed-refs" will be used instead. HEAD:: A symref (see glossary) to the `refs/heads/` namespace @@ -133,6 +138,11 @@ being a symref to point at the current branch. Such a state is often called 'detached HEAD.' See linkgit:git-checkout[1] for details. +config:: + Repository specific configuration file. This file is ignored + if $GIT_COMMON_DIR is set and "$GIT_COMMON_DIR/config" will be + used instead. + branches:: A slightly deprecated way to store shorthands to be used to specify a URL to 'git fetch', 'git pull' and 'git push'. @@ -140,7 +150,10 @@ branches:: 'name' can be given to these commands in place of 'repository' argument. See the REMOTES section in linkgit:git-fetch[1] for details. This mechanism is legacy - and not likely to be found in modern repositories. + and not likely to be found in modern repositories. This + directory is ignored if $GIT_COMMON_DIR is set and + "$GIT_COMMON_DIR/branches" will be used instead. + hooks:: Hooks are customization scripts used by various Git @@ -149,7 +162,9 @@ hooks:: default. To enable, the `.sample` suffix has to be removed from the filename by renaming. Read linkgit:githooks[5] for more details about - each hook. + each hook. This directory is ignored if $GIT_COMMON_DIR is set + and "$GIT_COMMON_DIR/hooks" will be used instead. + index:: The current index file for the repository. It is @@ -161,7 +176,8 @@ sharedindex.<SHA-1>:: info:: Additional information about the repository is recorded - in this directory. + in this directory. This directory is ignored if $GIT_COMMON_DIR + is set and "$GIT_COMMON_DIR/index" will be used instead. info/refs:: This file helps dumb transports discover what refs are @@ -201,12 +217,15 @@ remotes:: when interacting with remote repositories via 'git fetch', 'git pull' and 'git push' commands. See the REMOTES section in linkgit:git-fetch[1] for details. This mechanism is legacy - and not likely to be found in modern repositories. + and not likely to be found in modern repositories. This + directory is ignored if $GIT_COMMON_DIR is set and + "$GIT_COMMON_DIR/remotes" will be used instead. logs:: - Records of changes made to refs are stored in this - directory. See linkgit:git-update-ref[1] - for more information. + Records of changes made to refs are stored in this directory. + See linkgit:git-update-ref[1] for more information. This + directory is ignored if $GIT_COMMON_DIR is set and + "$GIT_COMMON_DIR/logs" will be used instead. logs/refs/heads/`name`:: Records all changes made to the branch tip named `name`. @@ -217,11 +236,46 @@ logs/refs/tags/`name`:: shallow:: This is similar to `info/grafts` but is internally used and maintained by shallow clone mechanism. See `--depth` - option to linkgit:git-clone[1] and linkgit:git-fetch[1]. + option to linkgit:git-clone[1] and linkgit:git-fetch[1]. This + file is ignored if $GIT_COMMON_DIR is set and + "$GIT_COMMON_DIR/shallow" will be used instead. + +commondir:: + If this file exists, $GIT_COMMON_DIR (see linkgit:git[1]) will + be set to the path specified in this file if it is not + explicitly set. If the specified path is relative, it is + relative to $GIT_DIR. The repository with commondir is + incomplete without the repository pointed by "commondir". modules:: Contains the git-repositories of the submodules. +worktrees:: + Contains worktree specific information of linked + checkouts. Each subdirectory contains the worktree-related + part of a linked checkout. This directory is ignored if + $GIT_COMMON_DIR is set and "$GIT_COMMON_DIR/worktrees" will be + used instead. + +worktrees/<id>/gitdir:: + A text file containing the absolute path back to the .git file + that points to here. This is used to check if the linked + repository has been manually removed and there is no need to + keep this directory any more. mtime of this file should be + updated every time the linked repository is accessed. + +worktrees/<id>/locked:: + If this file exists, the linked repository may be on a + portable device and not available. It does not mean that the + linked repository is gone and `worktrees/<id>` could be + removed. The file's content contains a reason string on why + the repository is locked. + +worktrees/<id>/link:: + If this file exists, it is a hard link to the linked .git + file. It is used to detect if the linked repository is + manually removed. + SEE ALSO -------- linkgit:git-init[1], 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/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-ref-iteration.txt b/Documentation/technical/api-ref-iteration.txt index 02adfd45d3..37379d8337 100644 --- a/Documentation/technical/api-ref-iteration.txt +++ b/Documentation/technical/api-ref-iteration.txt @@ -6,7 +6,7 @@ Iteration of refs is done by using an iterate function which will call a callback function for every ref. The callback function has this signature: - int handle_one_ref(const char *refname, const unsigned char *sha1, + int handle_one_ref(const char *refname, const struct object_id *oid, int flags, void *cb_data); There are different kinds of iterate functions which all take a 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> ----------------- diff --git a/GIT-VERSION-GEN b/GIT-VERSION-GEN index 159d52645b..bfb715d3c8 100755 --- a/GIT-VERSION-GEN +++ b/GIT-VERSION-GEN @@ -1,7 +1,7 @@ #!/bin/sh GVF=GIT-VERSION-FILE -DEF_VER=v2.4.5 +DEF_VER=v2.4.0.GIT LF=' ' @@ -359,6 +359,8 @@ all:: # compiler is detected to support it. # # Define HAVE_BSD_SYSCTL if your platform has a BSD-compatible sysctl function. +# +# Define HAVE_GETDELIM if your system has the getdelim() function. GIT-VERSION-FILE: FORCE @$(SHELL_PATH) ./GIT-VERSION-GEN @@ -572,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 @@ -1437,6 +1440,10 @@ ifdef HAVE_BSD_SYSCTL BASIC_CFLAGS += -DHAVE_BSD_SYSCTL endif +ifdef HAVE_GETDELIM + BASIC_CFLAGS += -DHAVE_GETDELIM +endif + ifeq ($(TCLTK_PATH),) NO_TCLTK = NoThanks endif @@ -1687,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):\ @@ -2449,7 +2456,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) ;; \ @@ -2457,7 +2464,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 | \ @@ -1 +1 @@ -Documentation/RelNotes/2.4.5.txt
\ No newline at end of file +Documentation/RelNotes/2.5.0.txt
\ No newline at end of file diff --git a/archive-zip.c b/archive-zip.c index ffb3535e93..ae3d67f9d3 100644 --- a/archive-zip.c +++ b/archive-zip.c @@ -448,12 +448,12 @@ static void write_zip_trailer(const unsigned char *sha1) copy_le16(trailer.entries, zip_dir_entries); copy_le32(trailer.size, zip_dir_offset); copy_le32(trailer.offset, zip_offset); - copy_le16(trailer.comment_length, sha1 ? 40 : 0); + copy_le16(trailer.comment_length, sha1 ? GIT_SHA1_HEXSZ : 0); write_or_die(1, zip_dir, zip_dir_offset); write_or_die(1, &trailer, ZIP_DIR_TRAILER_SIZE); if (sha1) - write_or_die(1, sha1_to_hex(sha1), 40); + write_or_die(1, sha1_to_hex(sha1), GIT_SHA1_HEXSZ); } static void dos_time(time_t *time, int *dos_date, int *dos_time) @@ -101,7 +101,7 @@ static void setup_archive_check(struct git_attr_check *check) struct directory { struct directory *up; - unsigned char sha1[20]; + struct object_id oid; int baselen, len; unsigned mode; int stage; @@ -177,7 +177,7 @@ static void queue_directory(const unsigned char *sha1, d->stage = stage; c->bottom = d; d->len = sprintf(d->path, "%.*s%s/", (int)base->len, base->buf, filename); - hashcpy(d->sha1, sha1); + hashcpy(d->oid.hash, sha1); } static int write_directory(struct archiver_context *c) @@ -191,7 +191,7 @@ static int write_directory(struct archiver_context *c) d->path[d->len - 1] = '\0'; /* no trailing slash */ ret = write_directory(c) || - write_archive_entry(d->sha1, d->path, d->baselen, + write_archive_entry(d->oid.hash, d->path, d->baselen, d->path + d->baselen, d->mode, d->stage, c) != READ_TREE_RECURSIVE; free(d); @@ -354,7 +354,7 @@ static void parse_treeish_arg(const char **argv, time_t archive_time; struct tree *tree; const struct commit *commit; - unsigned char sha1[20]; + struct object_id oid; /* Remotes are only allowed to fetch actual refs */ if (remote && !remote_allow_unreachable) { @@ -362,15 +362,15 @@ static void parse_treeish_arg(const char **argv, const char *colon = strchrnul(name, ':'); int refnamelen = colon - name; - if (!dwim_ref(name, refnamelen, sha1, &ref)) + if (!dwim_ref(name, refnamelen, oid.hash, &ref)) die("no such ref: %.*s", refnamelen, name); free(ref); } - if (get_sha1(name, sha1)) + if (get_sha1(name, oid.hash)) die("Not a valid object name"); - commit = lookup_commit_reference_gently(sha1, 1); + commit = lookup_commit_reference_gently(oid.hash, 1); if (commit) { commit_sha1 = commit->object.sha1; archive_time = commit->date; @@ -379,21 +379,21 @@ static void parse_treeish_arg(const char **argv, archive_time = time(NULL); } - tree = parse_tree_indirect(sha1); + tree = parse_tree_indirect(oid.hash); if (tree == NULL) die("not a tree object"); if (prefix) { - unsigned char tree_sha1[20]; + struct object_id tree_oid; unsigned int mode; int err; err = get_tree_entry(tree->object.sha1, prefix, - tree_sha1, &mode); + tree_oid.hash, &mode); if (err || !S_ISDIR(mode)) die("current working directory is untracked"); - tree = parse_tree_indirect(tree_sha1); + tree = parse_tree_indirect(tree_oid.hash); } ar_args->tree = tree; ar_args->commit_sha1 = commit_sha1; @@ -15,7 +15,7 @@ static struct sha1_array good_revs; static struct sha1_array skipped_revs; -static unsigned char *current_bad_sha1; +static struct object_id *current_bad_oid; static const char *argv_checkout[] = {"checkout", "-q", NULL, "--", NULL}; static const char *argv_show_branch[] = {"show-branch", NULL, NULL}; @@ -400,16 +400,16 @@ struct commit_list *find_bisection(struct commit_list *list, return best; } -static int register_ref(const char *refname, const unsigned char *sha1, +static int register_ref(const char *refname, const struct object_id *oid, int flags, void *cb_data) { if (!strcmp(refname, "bad")) { - current_bad_sha1 = xmalloc(20); - hashcpy(current_bad_sha1, sha1); + current_bad_oid = xmalloc(sizeof(*current_bad_oid)); + oidcpy(current_bad_oid, oid); } else if (starts_with(refname, "good-")) { - sha1_array_append(&good_revs, sha1); + sha1_array_append(&good_revs, oid->hash); } else if (starts_with(refname, "skip-")) { - sha1_array_append(&skipped_revs, sha1); + sha1_array_append(&skipped_revs, oid->hash); } return 0; @@ -564,7 +564,7 @@ static struct commit_list *skip_away(struct commit_list *list, int count) for (i = 0; cur; cur = cur->next, i++) { if (i == index) { - if (hashcmp(cur->item->object.sha1, current_bad_sha1)) + if (hashcmp(cur->item->object.sha1, current_bad_oid->hash)) return cur; if (previous) return previous; @@ -607,7 +607,7 @@ static void bisect_rev_setup(struct rev_info *revs, const char *prefix, /* rev_argv.argv[0] will be ignored by setup_revisions */ argv_array_push(&rev_argv, "bisect_rev_setup"); - argv_array_pushf(&rev_argv, bad_format, sha1_to_hex(current_bad_sha1)); + argv_array_pushf(&rev_argv, bad_format, oid_to_hex(current_bad_oid)); for (i = 0; i < good_revs.nr; i++) argv_array_pushf(&rev_argv, good_format, sha1_to_hex(good_revs.sha1[i])); @@ -628,7 +628,7 @@ static void bisect_common(struct rev_info *revs) } static void exit_if_skipped_commits(struct commit_list *tried, - const unsigned char *bad) + const struct object_id *bad) { if (!tried) return; @@ -637,12 +637,12 @@ static void exit_if_skipped_commits(struct commit_list *tried, "The first bad commit could be any of:\n"); print_commit_list(tried, "%s\n", "%s\n"); if (bad) - printf("%s\n", sha1_to_hex(bad)); + printf("%s\n", oid_to_hex(bad)); printf("We cannot bisect more!\n"); exit(2); } -static int is_expected_rev(const unsigned char *sha1) +static int is_expected_rev(const struct object_id *oid) { const char *filename = git_path("BISECT_EXPECTED_REV"); struct stat st; @@ -658,7 +658,7 @@ static int is_expected_rev(const unsigned char *sha1) return 0; if (strbuf_getline(&str, fp, '\n') != EOF) - res = !strcmp(str.buf, sha1_to_hex(sha1)); + res = !strcmp(str.buf, oid_to_hex(oid)); strbuf_release(&str); fclose(fp); @@ -719,7 +719,7 @@ static struct commit **get_bad_and_good_commits(int *rev_nr) struct commit **rev = xmalloc(len * sizeof(*rev)); int i, n = 0; - rev[n++] = get_commit_reference(current_bad_sha1); + rev[n++] = get_commit_reference(current_bad_oid->hash); for (i = 0; i < good_revs.nr; i++) rev[n++] = get_commit_reference(good_revs.sha1[i]); *rev_nr = n; @@ -729,8 +729,8 @@ static struct commit **get_bad_and_good_commits(int *rev_nr) static void handle_bad_merge_base(void) { - if (is_expected_rev(current_bad_sha1)) { - char *bad_hex = sha1_to_hex(current_bad_sha1); + if (is_expected_rev(current_bad_oid)) { + char *bad_hex = oid_to_hex(current_bad_oid); char *good_hex = join_sha1_array_hex(&good_revs, ' '); fprintf(stderr, "The merge base %s is bad.\n" @@ -750,7 +750,7 @@ static void handle_bad_merge_base(void) static void handle_skipped_merge_base(const unsigned char *mb) { char *mb_hex = sha1_to_hex(mb); - char *bad_hex = sha1_to_hex(current_bad_sha1); + char *bad_hex = sha1_to_hex(current_bad_oid->hash); char *good_hex = join_sha1_array_hex(&good_revs, ' '); warning("the merge base between %s and [%s] " @@ -781,7 +781,7 @@ static void check_merge_bases(int no_checkout) for (; result; result = result->next) { const unsigned char *mb = result->item->object.sha1; - if (!hashcmp(mb, current_bad_sha1)) { + if (!hashcmp(mb, current_bad_oid->hash)) { handle_bad_merge_base(); } else if (0 <= sha1_array_lookup(&good_revs, mb)) { continue; @@ -838,7 +838,7 @@ static void check_good_are_ancestors_of_bad(const char *prefix, int no_checkout) struct stat st; int fd; - if (!current_bad_sha1) + if (!current_bad_oid) die("a bad revision is needed"); /* Check if file BISECT_ANCESTORS_OK exists. */ @@ -903,7 +903,7 @@ int bisect_next_all(const char *prefix, int no_checkout) struct commit_list *tried; int reaches = 0, all = 0, nr, steps; const unsigned char *bisect_rev; - char bisect_rev_hex[41]; + char bisect_rev_hex[GIT_SHA1_HEXSZ + 1]; if (read_bisect_refs()) die("reading bisect refs failed"); @@ -927,7 +927,7 @@ int bisect_next_all(const char *prefix, int no_checkout) exit_if_skipped_commits(tried, NULL); printf("%s was both good and bad\n", - sha1_to_hex(current_bad_sha1)); + oid_to_hex(current_bad_oid)); exit(1); } @@ -938,10 +938,10 @@ int bisect_next_all(const char *prefix, int no_checkout) } bisect_rev = revs.commits->item->object.sha1; - memcpy(bisect_rev_hex, sha1_to_hex(bisect_rev), 41); + memcpy(bisect_rev_hex, sha1_to_hex(bisect_rev), GIT_SHA1_HEXSZ + 1); - if (!hashcmp(bisect_rev, current_bad_sha1)) { - exit_if_skipped_commits(tried, current_bad_sha1); + if (!hashcmp(bisect_rev, current_bad_oid->hash)) { + exit_if_skipped_commits(tried, current_bad_oid); printf("%s is the first bad commit\n", bisect_rev_hex); show_diff_tree(prefix, revs.commits->item); /* This means the bisection process succeeded. */ diff --git a/builtin/apply.c b/builtin/apply.c index 0769b09287..54aba4e351 100644 --- a/builtin/apply.c +++ b/builtin/apply.c @@ -208,7 +208,7 @@ struct patch { struct patch *next; /* three-way fallback result */ - unsigned char threeway_stage[3][20]; + struct object_id threeway_stage[3]; }; static void free_fragment_list(struct fragment *list) @@ -1638,6 +1638,9 @@ static int parse_fragment(const char *line, unsigned long size, } if (oldlines || newlines) return -1; + if (!deleted && !added) + return -1; + fragment->leading = leading; fragment->trailing = trailing; @@ -3426,11 +3429,11 @@ static int try_threeway(struct image *image, struct patch *patch, if (status) { patch->conflicted_threeway = 1; if (patch->is_new) - hashclr(patch->threeway_stage[0]); + oidclr(&patch->threeway_stage[0]); else - hashcpy(patch->threeway_stage[0], pre_sha1); - hashcpy(patch->threeway_stage[1], our_sha1); - hashcpy(patch->threeway_stage[2], post_sha1); + hashcpy(patch->threeway_stage[0].hash, pre_sha1); + hashcpy(patch->threeway_stage[1].hash, our_sha1); + hashcpy(patch->threeway_stage[2].hash, post_sha1); fprintf(stderr, "Applied patch to '%s' with conflicts.\n", patch->new_name); } else { fprintf(stderr, "Applied patch to '%s' cleanly.\n", patch->new_name); @@ -4186,14 +4189,14 @@ static void add_conflicted_stages_file(struct patch *patch) remove_file_from_cache(patch->new_name); for (stage = 1; stage < 4; stage++) { - if (is_null_sha1(patch->threeway_stage[stage - 1])) + if (is_null_oid(&patch->threeway_stage[stage - 1])) continue; ce = xcalloc(1, ce_size); memcpy(ce->name, patch->new_name, namelen); ce->ce_mode = create_ce_mode(mode); ce->ce_flags = create_ce_flags(stage); ce->ce_namelen = namelen; - hashcpy(ce->sha1, patch->threeway_stage[stage - 1]); + hashcpy(ce->sha1, patch->threeway_stage[stage - 1].hash); if (add_cache_entry(ce, ADD_CACHE_OK_TO_ADD) < 0) die(_("unable to add cache entry for %s"), patch->new_name); } diff --git a/builtin/blame.c b/builtin/blame.c index b3e948e757..a22ac17407 100644 --- a/builtin/blame.c +++ b/builtin/blame.c @@ -2176,6 +2176,14 @@ static int git_blame_config(const char *var, const char *value, void *cb) blank_boundary = git_config_bool(var, value); return 0; } + if (!strcmp(var, "blame.showemail")) { + int *output_option = cb; + if (git_config_bool(var, value)) + *output_option |= OUTPUT_SHOW_EMAIL; + else + *output_option &= ~OUTPUT_SHOW_EMAIL; + return 0; + } if (!strcmp(var, "blame.date")) { if (!value) return config_error_nonbool(var); @@ -2520,7 +2528,7 @@ int cmd_blame(int argc, const char **argv, const char *prefix) unsigned int range_i; long anchor; - git_config(git_blame_config, NULL); + git_config(git_blame_config, &output_option); init_revisions(&revs, NULL); revs.date_mode = blame_date_mode; DIFF_OPT_SET(&revs.diffopt, ALLOW_TEXTCONV); diff --git a/builtin/branch.c b/builtin/branch.c index a0a03fc6de..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); } @@ -328,7 +326,7 @@ static int match_patterns(const char **pattern, const char *refname) return 0; } -static int append_ref(const char *refname, const unsigned char *sha1, int flags, void *cb_data) +static int append_ref(const char *refname, const struct object_id *oid, int flags, void *cb_data) { struct append_ref_cb *cb = (struct append_ref_cb *)(cb_data); struct ref_list *ref_list = cb->ref_list; @@ -365,7 +363,7 @@ static int append_ref(const char *refname, const unsigned char *sha1, int flags, commit = NULL; if (ref_list->verbose || ref_list->with_commit || merge_filter != NO_FILTER) { - commit = lookup_commit_reference_gently(sha1, 1); + commit = lookup_commit_reference_gently(oid->hash, 1); if (!commit) { cb->ret = error(_("branch '%s' does not point at a commit"), refname); return 0; @@ -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), @@ -771,7 +763,6 @@ static const char edit_description[] = "BRANCH_DESCRIPTION"; static int edit_branch_description(const char *branch_name) { - FILE *fp; int status; struct strbuf buf = STRBUF_INIT; struct strbuf name = STRBUF_INIT; @@ -784,8 +775,7 @@ static int edit_branch_description(const char *branch_name) " %s\n" "Lines starting with '%c' will be stripped.\n", branch_name, comment_line_char); - fp = fopen(git_path(edit_description), "w"); - if ((fwrite(buf.buf, 1, buf.len, fp) < buf.len) || fclose(fp)) { + if (write_file(git_path(edit_description), 0, "%s", buf.buf)) { strbuf_release(&buf); return error(_("could not write branch description template: %s"), strerror(errno)); diff --git a/builtin/cat-file.c b/builtin/cat-file.c index df99df4db1..049a95f1f1 100644 --- a/builtin/cat-file.c +++ b/builtin/cat-file.c @@ -8,14 +8,22 @@ #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) +static int cat_one_file(int opt, const char *exp_type, const char *obj_name, + int unknown_type) { unsigned char sha1[20]; enum object_type type; char *buf; unsigned long size; struct object_context obj_context; + struct object_info oi = {NULL}; + struct strbuf sb = STRBUF_INIT; + unsigned flags = LOOKUP_REPLACE_OBJECT; + + if (unknown_type) + flags |= LOOKUP_UNKNOWN_OBJECT; if (get_sha1_with_context(obj_name, 0, sha1, &obj_context)) die("Not a valid object name %s", obj_name); @@ -23,20 +31,22 @@ static int cat_one_file(int opt, const char *exp_type, const char *obj_name) buf = NULL; switch (opt) { case 't': - type = sha1_object_info(sha1, NULL); - if (type > 0) { - printf("%s\n", typename(type)); + oi.typename = &sb; + if (sha1_object_info_extended(sha1, &oi, flags) < 0) + die("git cat-file: could not get object info"); + if (sb.len) { + printf("%s\n", sb.buf); + strbuf_release(&sb); return 0; } break; case 's': - type = sha1_object_info(sha1, &size); - if (type > 0) { - printf("%lu\n", size); - return 0; - } - break; + oi.sizep = &size; + if (sha1_object_info_extended(sha1, &oi, flags) < 0) + die("git cat-file: could not get object info"); + printf("%lu\n", size); + return 0; case 'e': return !has_sha1_file(sha1); @@ -224,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; }; @@ -232,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; } @@ -323,8 +366,8 @@ static int batch_objects(struct batch_options *opt) } static const char * const cat_file_usage[] = { - N_("git cat-file (-t | -s | -e | -p | <type> | --textconv) <object>"), - N_("git cat-file (--batch | --batch-check) < <list-of-objects>"), + N_("git cat-file (-t [--allow-unknown-type]|-s [--allow-unknown-type]|-e|-p|<type>|--textconv) <object>"), + N_("git cat-file (--batch | --batch-check) [--follow-symlinks] < <list-of-objects>"), NULL }; @@ -342,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; @@ -359,30 +401,32 @@ int cmd_cat_file(int argc, const char **argv, const char *prefix) int opt = 0; const char *exp_type = NULL, *obj_name = NULL; struct batch_options batch = {0}; + int unknown_type = 0; const struct option options[] = { OPT_GROUP(N_("<type> can be one of: blob, tree, commit, tag")), - OPT_SET_INT('t', NULL, &opt, N_("show object type"), 't'), - OPT_SET_INT('s', NULL, &opt, N_("show object size"), 's'), - OPT_SET_INT('e', NULL, &opt, + OPT_CMDMODE('t', NULL, &opt, N_("show object type"), 't'), + OPT_CMDMODE('s', NULL, &opt, N_("show object size"), 's'), + OPT_CMDMODE('e', NULL, &opt, N_("exit with zero when there's no error"), 'e'), - OPT_SET_INT('p', NULL, &opt, N_("pretty-print object's content"), 'p'), - OPT_SET_INT(0, "textconv", &opt, + OPT_CMDMODE('p', NULL, &opt, N_("pretty-print object's content"), 'p'), + OPT_CMDMODE(0, "textconv", &opt, N_("for blob objects, run textconv on object's content"), 'c'), + OPT_BOOL( 0, "allow-unknown-type", &unknown_type, + N_("allow -s and -t to work with broken/corrupt objects")), { OPTION_CALLBACK, 0, "batch", &batch, "format", N_("show info and content of objects fed from the standard input"), PARSE_OPT_OPTARG, batch_option_callback }, { 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() }; git_config(git_cat_file_config, NULL); - if (argc != 3 && argc != 2) - usage_with_options(cat_file_usage, options); - argc = parse_options(argc, argv, prefix, options, cat_file_usage, 0); if (opt) { @@ -402,8 +446,14 @@ 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); - return cat_one_file(opt, exp_type, obj_name); + if (unknown_type && opt != 't' && opt != 's') + die("git cat-file --allow-unknown-type: use with -s or -t"); + return cat_one_file(opt, exp_type, obj_name, unknown_type); } diff --git a/builtin/checkout-index.c b/builtin/checkout-index.c index 9ca2da1583..8028c3768f 100644 --- a/builtin/checkout-index.c +++ b/builtin/checkout-index.c @@ -241,7 +241,7 @@ int cmd_checkout_index(int argc, const char **argv, const char *prefix) /* Check out named files first */ for (i = 0; i < argc; i++) { const char *arg = argv[i]; - const char *p; + char *p; if (all) die("git checkout-index: don't mix '--all' and explicit filenames"); @@ -249,8 +249,7 @@ int cmd_checkout_index(int argc, const char **argv, const char *prefix) die("git checkout-index: don't mix '--stdin' and explicit filenames"); p = prefix_path(prefix, prefix_length, arg); checkout_file(p, prefix); - if (p < arg || p > arg + strlen(arg)) - free((char *)p); + free(p); } if (read_from_stdin) { @@ -260,7 +259,7 @@ int cmd_checkout_index(int argc, const char **argv, const char *prefix) die("git checkout-index: don't mix '--all' and '--stdin'"); while (strbuf_getline(&buf, stdin, line_termination) != EOF) { - const char *p; + char *p; if (line_termination && buf.buf[0] == '"') { strbuf_reset(&nbuf); if (unquote_c_style(&nbuf, buf.buf, NULL)) @@ -269,8 +268,7 @@ int cmd_checkout_index(int argc, const char **argv, const char *prefix) } p = prefix_path(prefix, prefix_length, buf.buf); checkout_file(p, prefix); - if (p < buf.buf || p > buf.buf + buf.len) - free((char *)p); + free(p); } strbuf_release(&nbuf); strbuf_release(&buf); diff --git a/builtin/checkout.c b/builtin/checkout.c index 3e141fc149..9b49f0e413 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -20,6 +20,7 @@ #include "resolve-undo.h" #include "submodule.h" #include "argv-array.h" +#include "sigchain.h" static const char * const checkout_usage[] = { N_("git checkout [<options>] <branch>"), @@ -36,6 +37,7 @@ struct checkout_opts { int writeout_stage; int overwrite_ignore; int ignore_skipworktree; + int ignore_other_worktrees; const char *new_branch; const char *new_branch_force; @@ -48,6 +50,10 @@ struct checkout_opts { const char *prefix; struct pathspec pathspec; struct tree *source_tree; + + const char *new_worktree; + const char **saved_argv; + int new_worktree_mode; }; static int post_checkout_hook(struct commit *old, struct commit *new, @@ -267,6 +273,9 @@ static int checkout_paths(const struct checkout_opts *opts, die(_("Cannot update paths and switch to branch '%s' at the same time."), opts->new_branch); + if (opts->new_worktree) + die(_("'%s' cannot be used with updating paths"), "--to"); + if (opts->patch_mode) return run_add_interactive(revision, "--patch=checkout", &opts->pathspec); @@ -441,6 +450,11 @@ struct branch_info { const char *name; /* The short name used */ const char *path; /* The full name of a real branch */ struct commit *commit; /* The named commit */ + /* + * if not null the branch is detached because it's already + * checked out in this checkout + */ + char *checkout; }; static void setup_branch_path(struct branch_info *branch) @@ -502,7 +516,7 @@ static int merge_working_tree(const struct checkout_opts *opts, topts.dir->flags |= DIR_SHOW_IGNORED; setup_standard_excludes(topts.dir); } - tree = parse_tree_indirect(old->commit ? + tree = parse_tree_indirect(old->commit && !opts->new_worktree_mode ? old->commit->object.sha1 : EMPTY_TREE_SHA1_BIN); init_tree_desc(&trees[0], tree->buffer, tree->size); @@ -606,18 +620,21 @@ static void update_refs_for_switch(const struct checkout_opts *opts, if (opts->new_orphan_branch) { if (opts->new_branch_log && !log_all_ref_updates) { int temp; - char log_file[PATH_MAX]; - char *ref_name = mkpath("refs/heads/%s", opts->new_orphan_branch); + struct strbuf log_file = STRBUF_INIT; + int ret; + const char *ref_name; + ref_name = mkpath("refs/heads/%s", opts->new_orphan_branch); temp = log_all_ref_updates; log_all_ref_updates = 1; - if (log_ref_setup(ref_name, log_file, sizeof(log_file))) { + ret = log_ref_setup(ref_name, &log_file); + log_all_ref_updates = temp; + strbuf_release(&log_file); + if (ret) { fprintf(stderr, _("Can not do reflog for '%s'\n"), opts->new_orphan_branch); - log_all_ref_updates = temp; return; } - log_all_ref_updates = temp; } } else @@ -685,10 +702,10 @@ static void update_refs_for_switch(const struct checkout_opts *opts, } static int add_pending_uninteresting_ref(const char *refname, - const unsigned char *sha1, + const struct object_id *oid, int flags, void *cb_data) { - add_pending_sha1(cb_data, refname, sha1, UNINTERESTING); + add_pending_sha1(cb_data, refname, oid->hash, UNINTERESTING); return 0; } @@ -743,10 +760,17 @@ static void suggest_reattach(struct commit *commit, struct rev_info *revs) if (advice_detached_head) fprintf(stderr, - _( + Q_( + /* The singular version */ + "If you want to keep it by creating a new branch, " + "this may be a good time\nto do so with:\n\n" + " git branch <new-branch-name> %s\n\n", + /* The plural version */ "If you want to keep them by creating a new branch, " "this may be a good time\nto do so with:\n\n" - " git branch <new-branch-name> %s\n\n"), + " git branch <new-branch-name> %s\n\n", + /* Give ngettext() the count */ + lost), find_unique_abbrev(commit->object.sha1, DEFAULT_ABBREV)); } @@ -815,7 +839,8 @@ static int switch_branches(const struct checkout_opts *opts, return ret; } - if (!opts->quiet && !old.path && old.commit && new->commit != old.commit) + if (!opts->quiet && !old.path && old.commit && + new->commit != old.commit && !opts->new_worktree_mode) orphaned_commit_warning(old.commit, new->commit); update_refs_for_switch(opts, &old, new); @@ -825,6 +850,138 @@ static int switch_branches(const struct checkout_opts *opts, return ret || writeout_error; } +static char *junk_work_tree; +static char *junk_git_dir; +static int is_junk; +static pid_t junk_pid; + +static void remove_junk(void) +{ + struct strbuf sb = STRBUF_INIT; + if (!is_junk || getpid() != junk_pid) + return; + if (junk_git_dir) { + strbuf_addstr(&sb, junk_git_dir); + remove_dir_recursively(&sb, 0); + strbuf_reset(&sb); + } + if (junk_work_tree) { + strbuf_addstr(&sb, junk_work_tree); + remove_dir_recursively(&sb, 0); + } + strbuf_release(&sb); +} + +static void remove_junk_on_signal(int signo) +{ + remove_junk(); + sigchain_pop(signo); + raise(signo); +} + +static int prepare_linked_checkout(const struct checkout_opts *opts, + struct branch_info *new) +{ + struct strbuf sb_git = STRBUF_INIT, sb_repo = STRBUF_INIT; + struct strbuf sb = STRBUF_INIT; + const char *path = opts->new_worktree, *name; + struct stat st; + struct child_process cp; + int counter = 0, len, ret; + + if (!new->commit) + die(_("no branch specified")); + if (file_exists(path) && !is_empty_dir(path)) + die(_("'%s' already exists"), path); + + len = strlen(path); + while (len && is_dir_sep(path[len - 1])) + len--; + + for (name = path + len - 1; name > path; name--) + if (is_dir_sep(*name)) { + name++; + break; + } + strbuf_addstr(&sb_repo, + git_path("worktrees/%.*s", (int)(path + len - name), name)); + len = sb_repo.len; + if (safe_create_leading_directories_const(sb_repo.buf)) + die_errno(_("could not create leading directories of '%s'"), + sb_repo.buf); + while (!stat(sb_repo.buf, &st)) { + counter++; + strbuf_setlen(&sb_repo, len); + strbuf_addf(&sb_repo, "%d", counter); + } + name = strrchr(sb_repo.buf, '/') + 1; + + junk_pid = getpid(); + atexit(remove_junk); + sigchain_push_common(remove_junk_on_signal); + + if (mkdir(sb_repo.buf, 0777)) + die_errno(_("could not create directory of '%s'"), sb_repo.buf); + junk_git_dir = xstrdup(sb_repo.buf); + is_junk = 1; + + /* + * lock the incomplete repo so prune won't delete it, unlock + * after the preparation is over. + */ + strbuf_addf(&sb, "%s/locked", sb_repo.buf); + write_file(sb.buf, 1, "initializing\n"); + + strbuf_addf(&sb_git, "%s/.git", path); + if (safe_create_leading_directories_const(sb_git.buf)) + die_errno(_("could not create leading directories of '%s'"), + sb_git.buf); + junk_work_tree = xstrdup(path); + + strbuf_reset(&sb); + strbuf_addf(&sb, "%s/gitdir", sb_repo.buf); + write_file(sb.buf, 1, "%s\n", real_path(sb_git.buf)); + write_file(sb_git.buf, 1, "gitdir: %s/worktrees/%s\n", + real_path(get_git_common_dir()), name); + /* + * This is to keep resolve_ref() happy. We need a valid HEAD + * or is_git_directory() will reject the directory. Any valid + * value would do because this value will be ignored and + * replaced at the next (real) checkout. + */ + strbuf_reset(&sb); + strbuf_addf(&sb, "%s/HEAD", sb_repo.buf); + write_file(sb.buf, 1, "%s\n", sha1_to_hex(new->commit->object.sha1)); + strbuf_reset(&sb); + strbuf_addf(&sb, "%s/commondir", sb_repo.buf); + write_file(sb.buf, 1, "../..\n"); + + if (!opts->quiet) + fprintf_ln(stderr, _("Enter %s (identifier %s)"), path, name); + + setenv("GIT_CHECKOUT_NEW_WORKTREE", "1", 1); + setenv(GIT_DIR_ENVIRONMENT, sb_git.buf, 1); + setenv(GIT_WORK_TREE_ENVIRONMENT, path, 1); + memset(&cp, 0, sizeof(cp)); + cp.git_cmd = 1; + cp.argv = opts->saved_argv; + ret = run_command(&cp); + if (!ret) { + is_junk = 0; + free(junk_work_tree); + free(junk_git_dir); + junk_work_tree = NULL; + junk_git_dir = NULL; + } + strbuf_reset(&sb); + strbuf_addf(&sb, "%s/locked", sb_repo.buf); + unlink_or_warn(sb.buf); + strbuf_release(&sb); + strbuf_release(&sb_repo); + strbuf_release(&sb_git); + return ret; +} + static int git_checkout_config(const char *var, const char *value, void *cb) { if (!strcmp(var, "diff.ignoresubmodules")) { @@ -880,13 +1037,80 @@ static const char *unique_tracking_name(const char *name, unsigned char *sha1) return NULL; } +static void check_linked_checkout(struct branch_info *new, const char *id) +{ + struct strbuf sb = STRBUF_INIT; + struct strbuf path = STRBUF_INIT; + struct strbuf gitdir = STRBUF_INIT; + const char *start, *end; + + if (id) + strbuf_addf(&path, "%s/worktrees/%s/HEAD", get_git_common_dir(), id); + else + strbuf_addf(&path, "%s/HEAD", get_git_common_dir()); + + if (strbuf_read_file(&sb, path.buf, 0) < 0 || + !skip_prefix(sb.buf, "ref:", &start)) + goto done; + while (isspace(*start)) + start++; + end = start; + while (*end && !isspace(*end)) + end++; + if (strncmp(start, new->path, end - start) || new->path[end - start] != '\0') + goto done; + if (id) { + strbuf_reset(&path); + strbuf_addf(&path, "%s/worktrees/%s/gitdir", get_git_common_dir(), id); + if (strbuf_read_file(&gitdir, path.buf, 0) <= 0) + goto done; + strbuf_rtrim(&gitdir); + } else + strbuf_addstr(&gitdir, get_git_common_dir()); + die(_("'%s' is already checked out at '%s'"), new->name, gitdir.buf); +done: + strbuf_release(&path); + strbuf_release(&sb); + strbuf_release(&gitdir); +} + +static void check_linked_checkouts(struct branch_info *new) +{ + struct strbuf path = STRBUF_INIT; + DIR *dir; + struct dirent *d; + + strbuf_addf(&path, "%s/worktrees", get_git_common_dir()); + if ((dir = opendir(path.buf)) == NULL) { + strbuf_release(&path); + return; + } + + /* + * $GIT_COMMON_DIR/HEAD is practically outside + * $GIT_DIR so resolve_ref_unsafe() won't work (it + * uses git_path). Parse the ref ourselves. + */ + check_linked_checkout(new, NULL); + + while ((d = readdir(dir)) != NULL) { + if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, "..")) + continue; + check_linked_checkout(new, d->d_name); + } + strbuf_release(&path); + closedir(dir); +} + static int parse_branchname_arg(int argc, const char **argv, int dwim_new_local_branch_ok, struct branch_info *new, - struct tree **source_tree, - unsigned char rev[20], - const char **new_branch) + struct checkout_opts *opts, + unsigned char rev[20]) { + struct tree **source_tree = &opts->source_tree; + const char **new_branch = &opts->new_branch; + int force_detach = opts->force_detach; int argcount = 0; unsigned char branch_rev[20]; const char *arg; @@ -1007,6 +1231,17 @@ static int parse_branchname_arg(int argc, const char **argv, else new->path = NULL; /* not an existing branch */ + if (new->path && !force_detach && !*new_branch) { + unsigned char sha1[20]; + int flag; + char *head_ref = resolve_refdup("HEAD", 0, sha1, &flag); + if (head_ref && + (!(flag & REF_ISSYMREF) || strcmp(head_ref, new->path)) && + !opts->ignore_other_worktrees) + check_linked_checkouts(new); + free(head_ref); + } + new->commit = lookup_commit_reference_gently(rev, 1); if (!new->commit) { /* not a commit */ @@ -1086,6 +1321,9 @@ static int checkout_branch(struct checkout_opts *opts, die(_("Cannot switch branch to a non-commit '%s'"), new->name); + if (opts->new_worktree) + return prepare_linked_checkout(opts, new); + if (!new->commit && opts->new_branch) { unsigned char rev[20]; int flag; @@ -1128,6 +1366,10 @@ int cmd_checkout(int argc, const char **argv, const char *prefix) N_("do not limit pathspecs to sparse entries only")), OPT_HIDDEN_BOOL(0, "guess", &dwim_new_local_branch, N_("second guess 'git checkout <no-such-branch>'")), + OPT_FILENAME(0, "to", &opts.new_worktree, + N_("check a branch out in a separate working directory")), + OPT_BOOL(0, "ignore-other-worktrees", &opts.ignore_other_worktrees, + N_("do not check if another worktree is holding the given ref")), OPT_END(), }; @@ -1136,6 +1378,9 @@ int cmd_checkout(int argc, const char **argv, const char *prefix) opts.overwrite_ignore = 1; opts.prefix = prefix; + opts.saved_argv = xmalloc(sizeof(const char *) * (argc + 2)); + memcpy(opts.saved_argv, argv, sizeof(const char *) * (argc + 1)); + gitmodules_config(); git_config(git_checkout_config, &opts); @@ -1144,6 +1389,14 @@ int cmd_checkout(int argc, const char **argv, const char *prefix) argc = parse_options(argc, argv, prefix, options, checkout_usage, PARSE_OPT_KEEP_DASHDASH); + /* recursive execution from checkout_new_worktree() */ + opts.new_worktree_mode = getenv("GIT_CHECKOUT_NEW_WORKTREE") != NULL; + if (opts.new_worktree_mode) + opts.new_worktree = NULL; + + if (!opts.new_worktree) + setup_work_tree(); + if (conflict_style) { opts.merge = 1; /* implied */ git_xmerge_config("merge.conflictstyle", conflict_style, NULL); @@ -1197,8 +1450,7 @@ int cmd_checkout(int argc, const char **argv, const char *prefix) opts.track == BRANCH_TRACK_UNSPECIFIED && !opts.new_branch; int n = parse_branchname_arg(argc, argv, dwim_ok, - &new, &opts.source_tree, - rev, &opts.new_branch); + &new, &opts, rev); argv += n; argc -= n; } diff --git a/builtin/clone.c b/builtin/clone.c index 4646922d6e..00535d0178 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -284,16 +284,17 @@ static void copy_alternates(struct strbuf *src, struct strbuf *dst, struct strbuf line = STRBUF_INIT; while (strbuf_getline(&line, in, '\n') != EOF) { - char *abs_path, abs_buf[PATH_MAX]; + char *abs_path; if (!line.len || line.buf[0] == '#') continue; if (is_absolute_path(line.buf)) { add_to_alternates_file(line.buf); continue; } - abs_path = mkpath("%s/objects/%s", src_repo, line.buf); - normalize_path_copy(abs_buf, abs_path); - add_to_alternates_file(abs_buf); + abs_path = mkpathdup("%s/objects/%s", src_repo, line.buf); + normalize_path_copy(abs_path, abs_path); + add_to_alternates_file(abs_path); + free(abs_path); } strbuf_release(&line); fclose(in); diff --git a/builtin/commit.c b/builtin/commit.c index c2ebea4ed3..254477fd1d 100644 --- a/builtin/commit.c +++ b/builtin/commit.c @@ -170,7 +170,7 @@ static void determine_whence(struct wt_status *s) whence = FROM_MERGE; else if (file_exists(git_path("CHERRY_PICK_HEAD"))) { whence = FROM_CHERRY_PICK; - if (file_exists(git_path("sequencer"))) + if (file_exists(git_path(SEQ_DIR))) sequencer_in_use = 1; } else @@ -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/config.c b/builtin/config.c index a58f99c2d7..7188405f7e 100644 --- a/builtin/config.c +++ b/builtin/config.c @@ -193,7 +193,7 @@ static int get_value(const char *key_, const char *regex_) key_regexp = (regex_t*)xmalloc(sizeof(regex_t)); if (regcomp(key_regexp, key, REG_EXTENDED)) { - fprintf(stderr, "Invalid key pattern: %s\n", key_); + error("invalid key pattern: %s", key_); free(key_regexp); key_regexp = NULL; ret = CONFIG_INVALID_PATTERN; @@ -214,7 +214,7 @@ static int get_value(const char *key_, const char *regex_) regexp = (regex_t*)xmalloc(sizeof(regex_t)); if (regcomp(regexp, regex_, REG_EXTENDED)) { - fprintf(stderr, "Invalid pattern: %s\n", regex_); + error("invalid pattern: %s", regex_); free(regexp); regexp = NULL; ret = CONFIG_INVALID_PATTERN; diff --git a/builtin/count-objects.c b/builtin/count-objects.c index e47ef0b1af..ad0c79954a 100644 --- a/builtin/count-objects.c +++ b/builtin/count-objects.c @@ -70,8 +70,10 @@ int cmd_count_objects(int argc, const char **argv, const char *prefix) /* we do not take arguments other than flags for now */ if (argc) usage_with_options(count_objects_usage, opts); - if (verbose) + if (verbose) { report_garbage = real_report_garbage; + report_linked_checkout_garbage(); + } for_each_loose_file_in_objdir(get_object_directory(), count_loose, count_cruft, NULL, NULL); diff --git a/builtin/describe.c b/builtin/describe.c index e00a75b121..a36c829e57 100644 --- a/builtin/describe.c +++ b/builtin/describe.c @@ -119,10 +119,10 @@ static void add_to_known_names(const char *path, } } -static int get_name(const char *path, const unsigned char *sha1, int flag, void *cb_data) +static int get_name(const char *path, const struct object_id *oid, int flag, void *cb_data) { int is_tag = starts_with(path, "refs/tags/"); - unsigned char peeled[20]; + struct object_id peeled; int is_annotated, prio; /* Reject anything outside refs/tags/ unless --all */ @@ -134,10 +134,10 @@ static int get_name(const char *path, const unsigned char *sha1, int flag, void return 0; /* Is it annotated? */ - if (!peel_ref(path, peeled)) { - is_annotated = !!hashcmp(sha1, peeled); + if (!peel_ref(path, peeled.hash)) { + is_annotated = !!oidcmp(oid, &peeled); } else { - hashcpy(peeled, sha1); + oidcpy(&peeled, oid); is_annotated = 0; } @@ -154,7 +154,7 @@ static int get_name(const char *path, const unsigned char *sha1, int flag, void else prio = 0; - add_to_known_names(all ? path + 5 : path + 10, peeled, prio, sha1); + add_to_known_names(all ? path + 5 : path + 10, peeled.hash, prio, oid->hash); return 0; } diff --git a/builtin/fetch.c b/builtin/fetch.c index f9512652cf..8d5b2dba2b 100644 --- a/builtin/fetch.c +++ b/builtin/fetch.c @@ -179,13 +179,15 @@ static void add_merge_config(struct ref **head, } } -static int add_existing(const char *refname, const unsigned char *sha1, +static int add_existing(const char *refname, const struct object_id *oid, int flag, void *cbdata) { struct string_list *list = (struct string_list *)cbdata; struct string_list_item *item = string_list_insert(list, refname); - item->util = xmalloc(20); - hashcpy(item->util, sha1); + struct object_id *old_oid = xmalloc(sizeof(*old_oid)); + + oidcpy(old_oid, oid); + item->util = old_oid; return 0; } @@ -588,7 +590,8 @@ static int store_updated_refs(const char *raw_url, const char *remote_name, struct strbuf note = STRBUF_INIT; const char *what, *kind; struct ref *rm; - char *url, *filename = dry_run ? "/dev/null" : git_path("FETCH_HEAD"); + char *url; + const char *filename = dry_run ? "/dev/null" : git_path("FETCH_HEAD"); int want_status; fp = fopen(filename, "a"); @@ -822,7 +825,7 @@ static void check_not_current_branch(struct ref *ref_map) static int truncate_fetch_head(void) { - char *filename = git_path("FETCH_HEAD"); + const char *filename = git_path("FETCH_HEAD"); FILE *fp = fopen(filename, "w"); if (!fp) @@ -912,9 +915,10 @@ static int do_fetch(struct transport *transport, struct string_list_item *peer_item = string_list_lookup(&existing_refs, rm->peer_ref->name); - if (peer_item) - hashcpy(rm->peer_ref->old_sha1, - peer_item->util); + if (peer_item) { + struct object_id *old_oid = peer_item->util; + hashcpy(rm->peer_ref->old_sha1, old_oid->hash); + } } } diff --git a/builtin/for-each-ref.c b/builtin/for-each-ref.c index 83f9cf9163..cb7db230d3 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) @@ -840,7 +854,8 @@ struct grab_ref_cbdata { * A call-back given to for_each_ref(). Filter refs and keep them for * later object processing. */ -static int grab_single_ref(const char *refname, const unsigned char *sha1, int flag, void *cb_data) +static int grab_single_ref(const char *refname, const struct object_id *oid, + int flag, void *cb_data) { struct grab_ref_cbdata *cb = cb_data; struct refinfo *ref; @@ -851,6 +866,11 @@ static int grab_single_ref(const char *refname, const unsigned char *sha1, int f return 0; } + if (flag & REF_ISBROKEN) { + warning("ignoring broken ref %s", refname); + return 0; + } + if (*cb->grab_pattern) { const char **pattern; int namelen = strlen(refname); @@ -878,7 +898,7 @@ static int grab_single_ref(const char *refname, const unsigned char *sha1, int f */ ref = xcalloc(1, sizeof(*ref)); ref->refname = xstrdup(refname); - hashcpy(ref->objectname, sha1); + hashcpy(ref->objectname, oid->hash); ref->flag = flag; cnt = cb->grab_cnt; diff --git a/builtin/fsck.c b/builtin/fsck.c index 0c757862e8..2679793049 100644 --- a/builtin/fsck.c +++ b/builtin/fsck.c @@ -25,7 +25,7 @@ static int include_reflogs = 1; static int check_full = 1; static int check_strict; static int keep_cache_objects; -static unsigned char head_sha1[20]; +static struct object_id head_oid; static const char *head_points_at; static int errors_found; static int write_lost_and_found; @@ -225,12 +225,12 @@ static void check_unreachable_object(struct object *obj) printf("dangling %s %s\n", typename(obj->type), sha1_to_hex(obj->sha1)); if (write_lost_and_found) { - char *filename = git_path("lost-found/%s/%s", + const char *filename = git_path("lost-found/%s/%s", obj->type == OBJ_COMMIT ? "commit" : "other", sha1_to_hex(obj->sha1)); FILE *f; - if (safe_create_leading_directories(filename)) { + if (safe_create_leading_directories_const(filename)) { error("Could not create lost-found"); return; } @@ -451,44 +451,52 @@ static void fsck_dir(int i, char *path) static int default_refs; +static void fsck_handle_reflog_sha1(const char *refname, unsigned char *sha1) +{ + struct object *obj; + + if (!is_null_sha1(sha1)) { + obj = lookup_object(sha1); + if (obj) { + obj->used = 1; + mark_object_reachable(obj); + } else { + error("%s: invalid reflog entry %s", refname, sha1_to_hex(sha1)); + errors_found |= ERROR_REACHABLE; + } + } +} + static int fsck_handle_reflog_ent(unsigned char *osha1, unsigned char *nsha1, const char *email, unsigned long timestamp, int tz, const char *message, void *cb_data) { - struct object *obj; + const char *refname = cb_data; if (verbose) fprintf(stderr, "Checking reflog %s->%s\n", sha1_to_hex(osha1), sha1_to_hex(nsha1)); - if (!is_null_sha1(osha1)) { - obj = lookup_object(osha1); - if (obj) { - obj->used = 1; - mark_object_reachable(obj); - } - } - obj = lookup_object(nsha1); - if (obj) { - obj->used = 1; - mark_object_reachable(obj); - } + fsck_handle_reflog_sha1(refname, osha1); + fsck_handle_reflog_sha1(refname, nsha1); return 0; } -static int fsck_handle_reflog(const char *logname, const unsigned char *sha1, int flag, void *cb_data) +static int fsck_handle_reflog(const char *logname, const struct object_id *oid, + int flag, void *cb_data) { - for_each_reflog_ent(logname, fsck_handle_reflog_ent, NULL); + for_each_reflog_ent(logname, fsck_handle_reflog_ent, (void *)logname); return 0; } -static int fsck_handle_ref(const char *refname, const unsigned char *sha1, int flag, void *cb_data) +static int fsck_handle_ref(const char *refname, const struct object_id *oid, + int flag, void *cb_data) { struct object *obj; - obj = parse_object(sha1); + obj = parse_object(oid->hash); if (!obj) { - error("%s: invalid sha1 pointer %s", refname, sha1_to_hex(sha1)); + error("%s: invalid sha1 pointer %s", refname, oid_to_hex(oid)); errors_found |= ERROR_REACHABLE; /* We'll continue with the rest despite the error.. */ return 0; @@ -504,8 +512,8 @@ static int fsck_handle_ref(const char *refname, const unsigned char *sha1, int f static void get_default_heads(void) { - if (head_points_at && !is_null_sha1(head_sha1)) - fsck_handle_ref("HEAD", head_sha1, 0, NULL); + if (head_points_at && !is_null_oid(&head_oid)) + fsck_handle_ref("HEAD", &head_oid, 0, NULL); for_each_rawref(fsck_handle_ref, NULL); if (include_reflogs) for_each_reflog(fsck_handle_reflog, NULL); @@ -556,7 +564,7 @@ static int fsck_head_link(void) if (verbose) fprintf(stderr, "Checking HEAD link\n"); - head_points_at = resolve_ref_unsafe("HEAD", 0, head_sha1, &flag); + head_points_at = resolve_ref_unsafe("HEAD", 0, head_oid.hash, &flag); if (!head_points_at) return error("Invalid HEAD"); if (!strcmp(head_points_at, "HEAD")) @@ -565,7 +573,7 @@ static int fsck_head_link(void) else if (!starts_with(head_points_at, "refs/heads/")) return error("HEAD points to something strange (%s)", head_points_at); - if (is_null_sha1(head_sha1)) { + if (is_null_oid(&head_oid)) { if (null_is_error) return error("HEAD: detached HEAD points at nothing"); fprintf(stderr, "notice: HEAD points to an unborn branch (%s)\n", diff --git a/builtin/gc.c b/builtin/gc.c index 5c634afc00..36fe33300f 100644 --- a/builtin/gc.c +++ b/builtin/gc.c @@ -33,11 +33,13 @@ static int gc_auto_threshold = 6700; static int gc_auto_pack_limit = 50; static int detach_auto = 1; static const char *prune_expire = "2.weeks.ago"; +static const char *prune_worktrees_expire = "3.months.ago"; static struct argv_array pack_refs_cmd = ARGV_ARRAY_INIT; static struct argv_array reflog = ARGV_ARRAY_INIT; static struct argv_array repack = ARGV_ARRAY_INIT; static struct argv_array prune = ARGV_ARRAY_INIT; +static struct argv_array prune_worktrees = ARGV_ARRAY_INIT; static struct argv_array rerere = ARGV_ARRAY_INIT; static char *pidfile; @@ -55,6 +57,17 @@ static void remove_pidfile_on_signal(int signo) raise(signo); } +static void git_config_date_string(const char *key, const char **output) +{ + if (git_config_get_string_const(key, output)) + return; + if (strcmp(*output, "now")) { + unsigned long now = approxidate("now"); + if (approxidate(*output) >= now) + git_die_config(key, _("Invalid %s: '%s'"), key, *output); + } +} + static void gc_config(void) { const char *value; @@ -71,16 +84,8 @@ static void gc_config(void) git_config_get_int("gc.auto", &gc_auto_threshold); git_config_get_int("gc.autopacklimit", &gc_auto_pack_limit); git_config_get_bool("gc.autodetach", &detach_auto); - - if (!git_config_get_string_const("gc.pruneexpire", &prune_expire)) { - if (strcmp(prune_expire, "now")) { - unsigned long now = approxidate("now"); - if (approxidate(prune_expire) >= now) { - git_die_config("gc.pruneexpire", _("Invalid gc.pruneexpire: '%s'"), - prune_expire); - } - } - } + git_config_date_string("gc.pruneexpire", &prune_expire); + git_config_date_string("gc.pruneworktreesexpire", &prune_worktrees_expire); git_config(git_default_config, NULL); } @@ -287,7 +292,8 @@ int cmd_gc(int argc, const char **argv, const char *prefix) argv_array_pushl(&pack_refs_cmd, "pack-refs", "--all", "--prune", NULL); argv_array_pushl(&reflog, "reflog", "expire", "--all", NULL); argv_array_pushl(&repack, "repack", "-d", "-l", NULL); - argv_array_pushl(&prune, "prune", "--expire", NULL ); + argv_array_pushl(&prune, "prune", "--expire", NULL); + argv_array_pushl(&prune_worktrees, "prune", "--worktrees", "--expire", NULL); argv_array_pushl(&rerere, "rerere", "gc", NULL); gc_config(); @@ -357,6 +363,12 @@ int cmd_gc(int argc, const char **argv, const char *prefix) return error(FAILED_RUN, prune.argv[0]); } + if (prune_worktrees_expire) { + argv_array_push(&prune_worktrees, prune_worktrees_expire); + if (run_command_v_opt(prune_worktrees.argv, RUN_GIT_CMD)) + return error(FAILED_RUN, prune_worktrees.argv[0]); + } + if (run_command_v_opt(rerere.argv, RUN_GIT_CMD)) return error(FAILED_RUN, rerere.argv[0]); diff --git a/builtin/index-pack.c b/builtin/index-pack.c index cf654df09b..48fa4724aa 100644 --- a/builtin/index-pack.c +++ b/builtin/index-pack.c @@ -18,16 +18,14 @@ static const char index_pack_usage[] = struct object_entry { struct pack_idx_entry idx; unsigned long size; - unsigned int hdr_size; - enum object_type type; - enum object_type real_type; - unsigned delta_depth; - int base_object_no; + unsigned char hdr_size; + signed char type; + signed char real_type; }; -union delta_base { - unsigned char sha1[20]; - off_t offset; +struct object_stat { + unsigned delta_depth; + int base_object_no; }; struct base_data { @@ -49,25 +47,28 @@ struct thread_local { int pack_fd; }; -/* - * Even if sizeof(union delta_base) == 24 on 64-bit archs, we really want - * to memcmp() only the first 20 bytes. - */ -#define UNION_BASE_SZ 20 - #define FLAG_LINK (1u<<20) #define FLAG_CHECKED (1u<<21) -struct delta_entry { - union delta_base base; +struct ofs_delta_entry { + off_t offset; + int obj_no; +}; + +struct ref_delta_entry { + unsigned char sha1[20]; int obj_no; }; static struct object_entry *objects; -static struct delta_entry *deltas; +static struct object_stat *obj_stat; +static struct ofs_delta_entry *ofs_deltas; +static struct ref_delta_entry *ref_deltas; static struct thread_local nothread_data; static int nr_objects; -static int nr_deltas; +static int nr_ofs_deltas; +static int nr_ref_deltas; +static int ref_deltas_alloc; static int nr_resolved_deltas; static int nr_threads; @@ -476,7 +477,8 @@ static void *unpack_entry_data(unsigned long offset, unsigned long size, } static void *unpack_raw_entry(struct object_entry *obj, - union delta_base *delta_base, + off_t *ofs_offset, + unsigned char *ref_sha1, unsigned char *sha1) { unsigned char *p; @@ -505,11 +507,10 @@ static void *unpack_raw_entry(struct object_entry *obj, switch (obj->type) { case OBJ_REF_DELTA: - hashcpy(delta_base->sha1, fill(20)); + hashcpy(ref_sha1, fill(20)); use(20); break; case OBJ_OFS_DELTA: - memset(delta_base, 0, sizeof(*delta_base)); p = fill(1); c = *p; use(1); @@ -523,8 +524,8 @@ static void *unpack_raw_entry(struct object_entry *obj, use(1); base_offset = (base_offset << 7) + (c & 127); } - delta_base->offset = obj->idx.offset - base_offset; - if (delta_base->offset <= 0 || delta_base->offset >= obj->idx.offset) + *ofs_offset = obj->idx.offset - base_offset; + if (*ofs_offset <= 0 || *ofs_offset >= obj->idx.offset) bad_object(obj->idx.offset, _("delta base offset is out of bound")); break; case OBJ_COMMIT: @@ -608,55 +609,110 @@ static void *get_data_from_pack(struct object_entry *obj) return unpack_data(obj, NULL, NULL); } -static int compare_delta_bases(const union delta_base *base1, - const union delta_base *base2, - enum object_type type1, - enum object_type type2) +static int compare_ofs_delta_bases(off_t offset1, off_t offset2, + enum object_type type1, + enum object_type type2) { int cmp = type1 - type2; if (cmp) return cmp; - return memcmp(base1, base2, UNION_BASE_SZ); + return offset1 < offset2 ? -1 : + offset1 > offset2 ? 1 : + 0; } -static int find_delta(const union delta_base *base, enum object_type type) +static int find_ofs_delta(const off_t offset, enum object_type type) { - int first = 0, last = nr_deltas; - - while (first < last) { - int next = (first + last) / 2; - struct delta_entry *delta = &deltas[next]; - int cmp; - - cmp = compare_delta_bases(base, &delta->base, - type, objects[delta->obj_no].type); - if (!cmp) - return next; - if (cmp < 0) { - last = next; - continue; - } - first = next+1; - } - return -first-1; + int first = 0, last = nr_ofs_deltas; + + while (first < last) { + int next = (first + last) / 2; + struct ofs_delta_entry *delta = &ofs_deltas[next]; + int cmp; + + cmp = compare_ofs_delta_bases(offset, delta->offset, + type, objects[delta->obj_no].type); + if (!cmp) + return next; + if (cmp < 0) { + last = next; + continue; + } + first = next+1; + } + return -first-1; } -static void find_delta_children(const union delta_base *base, - int *first_index, int *last_index, - enum object_type type) +static void find_ofs_delta_children(off_t offset, + int *first_index, int *last_index, + enum object_type type) { - int first = find_delta(base, type); + int first = find_ofs_delta(offset, type); int last = first; - int end = nr_deltas - 1; + int end = nr_ofs_deltas - 1; if (first < 0) { *first_index = 0; *last_index = -1; return; } - while (first > 0 && !memcmp(&deltas[first - 1].base, base, UNION_BASE_SZ)) + while (first > 0 && ofs_deltas[first - 1].offset == offset) --first; - while (last < end && !memcmp(&deltas[last + 1].base, base, UNION_BASE_SZ)) + while (last < end && ofs_deltas[last + 1].offset == offset) + ++last; + *first_index = first; + *last_index = last; +} + +static int compare_ref_delta_bases(const unsigned char *sha1, + const unsigned char *sha2, + enum object_type type1, + enum object_type type2) +{ + int cmp = type1 - type2; + if (cmp) + return cmp; + return hashcmp(sha1, sha2); +} + +static int find_ref_delta(const unsigned char *sha1, enum object_type type) +{ + int first = 0, last = nr_ref_deltas; + + while (first < last) { + int next = (first + last) / 2; + struct ref_delta_entry *delta = &ref_deltas[next]; + int cmp; + + cmp = compare_ref_delta_bases(sha1, delta->sha1, + type, objects[delta->obj_no].type); + if (!cmp) + return next; + if (cmp < 0) { + last = next; + continue; + } + first = next+1; + } + return -first-1; +} + +static void find_ref_delta_children(const unsigned char *sha1, + int *first_index, int *last_index, + enum object_type type) +{ + int first = find_ref_delta(sha1, type); + int last = first; + int end = nr_ref_deltas - 1; + + if (first < 0) { + *first_index = 0; + *last_index = -1; + return; + } + while (first > 0 && !hashcmp(ref_deltas[first - 1].sha1, sha1)) + --first; + while (last < end && !hashcmp(ref_deltas[last + 1].sha1, sha1)) ++last; *first_index = first; *last_index = last; @@ -730,7 +786,7 @@ static void sha1_object(const void *data, struct object_entry *obj_entry, assert(data || obj_entry); read_lock(); - collision_test_needed = has_sha1_file(sha1); + collision_test_needed = has_sha1_file_with_flags(sha1, HAS_SHA1_QUICK); read_unlock(); if (collision_test_needed && !data) { @@ -873,13 +929,15 @@ static void resolve_delta(struct object_entry *delta_obj, void *base_data, *delta_data; if (show_stat) { - delta_obj->delta_depth = base->obj->delta_depth + 1; + int i = delta_obj - objects; + int j = base->obj - objects; + obj_stat[i].delta_depth = obj_stat[j].delta_depth + 1; deepest_delta_lock(); - if (deepest_delta < delta_obj->delta_depth) - deepest_delta = delta_obj->delta_depth; + if (deepest_delta < obj_stat[i].delta_depth) + deepest_delta = obj_stat[i].delta_depth; deepest_delta_unlock(); + obj_stat[i].base_object_no = j; } - delta_obj->base_object_no = base->obj - objects; delta_data = get_data_from_pack(delta_obj); base_data = get_base_data(base); result->obj = delta_obj; @@ -902,7 +960,7 @@ static void resolve_delta(struct object_entry *delta_obj, * "want"; if so, swap in "set" and return true. Otherwise, leave it untouched * and return false. */ -static int compare_and_swap_type(enum object_type *type, +static int compare_and_swap_type(signed char *type, enum object_type want, enum object_type set) { @@ -921,16 +979,13 @@ static struct base_data *find_unresolved_deltas_1(struct base_data *base, struct base_data *prev_base) { if (base->ref_last == -1 && base->ofs_last == -1) { - union delta_base base_spec; + find_ref_delta_children(base->obj->idx.sha1, + &base->ref_first, &base->ref_last, + OBJ_REF_DELTA); - hashcpy(base_spec.sha1, base->obj->idx.sha1); - find_delta_children(&base_spec, - &base->ref_first, &base->ref_last, OBJ_REF_DELTA); - - memset(&base_spec, 0, sizeof(base_spec)); - base_spec.offset = base->obj->idx.offset; - find_delta_children(&base_spec, - &base->ofs_first, &base->ofs_last, OBJ_OFS_DELTA); + find_ofs_delta_children(base->obj->idx.offset, + &base->ofs_first, &base->ofs_last, + OBJ_OFS_DELTA); if (base->ref_last == -1 && base->ofs_last == -1) { free(base->data); @@ -941,7 +996,7 @@ static struct base_data *find_unresolved_deltas_1(struct base_data *base, } if (base->ref_first <= base->ref_last) { - struct object_entry *child = objects + deltas[base->ref_first].obj_no; + struct object_entry *child = objects + ref_deltas[base->ref_first].obj_no; struct base_data *result = alloc_base_data(); if (!compare_and_swap_type(&child->real_type, OBJ_REF_DELTA, @@ -957,7 +1012,7 @@ static struct base_data *find_unresolved_deltas_1(struct base_data *base, } if (base->ofs_first <= base->ofs_last) { - struct object_entry *child = objects + deltas[base->ofs_first].obj_no; + struct object_entry *child = objects + ofs_deltas[base->ofs_first].obj_no; struct base_data *result = alloc_base_data(); assert(child->real_type == OBJ_OFS_DELTA); @@ -993,15 +1048,22 @@ static void find_unresolved_deltas(struct base_data *base) } } -static int compare_delta_entry(const void *a, const void *b) +static int compare_ofs_delta_entry(const void *a, const void *b) { - const struct delta_entry *delta_a = a; - const struct delta_entry *delta_b = b; + const struct ofs_delta_entry *delta_a = a; + const struct ofs_delta_entry *delta_b = b; - /* group by type (ref vs ofs) and then by value (sha-1 or offset) */ - return compare_delta_bases(&delta_a->base, &delta_b->base, - objects[delta_a->obj_no].type, - objects[delta_b->obj_no].type); + return delta_a->offset < delta_b->offset ? -1 : + delta_a->offset > delta_b->offset ? 1 : + 0; +} + +static int compare_ref_delta_entry(const void *a, const void *b) +{ + const struct ref_delta_entry *delta_a = a; + const struct ref_delta_entry *delta_b = b; + + return hashcmp(delta_a->sha1, delta_b->sha1); } static void resolve_base(struct object_entry *obj) @@ -1047,7 +1109,8 @@ static void *threaded_second_pass(void *data) static void parse_pack_objects(unsigned char *sha1) { int i, nr_delays = 0; - struct delta_entry *delta = deltas; + struct ofs_delta_entry *ofs_delta = ofs_deltas; + unsigned char ref_delta_sha1[20]; struct stat st; if (verbose) @@ -1056,12 +1119,18 @@ static void parse_pack_objects(unsigned char *sha1) nr_objects); for (i = 0; i < nr_objects; i++) { struct object_entry *obj = &objects[i]; - void *data = unpack_raw_entry(obj, &delta->base, obj->idx.sha1); + void *data = unpack_raw_entry(obj, &ofs_delta->offset, + ref_delta_sha1, obj->idx.sha1); obj->real_type = obj->type; - if (is_delta_type(obj->type)) { - nr_deltas++; - delta->obj_no = i; - delta++; + if (obj->type == OBJ_OFS_DELTA) { + nr_ofs_deltas++; + ofs_delta->obj_no = i; + ofs_delta++; + } else if (obj->type == OBJ_REF_DELTA) { + ALLOC_GROW(ref_deltas, nr_ref_deltas + 1, ref_deltas_alloc); + hashcpy(ref_deltas[nr_ref_deltas].sha1, ref_delta_sha1); + ref_deltas[nr_ref_deltas].obj_no = i; + nr_ref_deltas++; } else if (!data) { /* large blobs, check later */ obj->real_type = OBJ_BAD; @@ -1112,15 +1181,18 @@ static void resolve_deltas(void) { int i; - if (!nr_deltas) + if (!nr_ofs_deltas && !nr_ref_deltas) return; /* Sort deltas by base SHA1/offset for fast searching */ - qsort(deltas, nr_deltas, sizeof(struct delta_entry), - compare_delta_entry); + qsort(ofs_deltas, nr_ofs_deltas, sizeof(struct ofs_delta_entry), + compare_ofs_delta_entry); + qsort(ref_deltas, nr_ref_deltas, sizeof(struct ref_delta_entry), + compare_ref_delta_entry); if (verbose) - progress = start_progress(_("Resolving deltas"), nr_deltas); + progress = start_progress(_("Resolving deltas"), + nr_ref_deltas + nr_ofs_deltas); #ifndef NO_PTHREADS nr_dispatched = 0; @@ -1158,7 +1230,7 @@ static void resolve_deltas(void) static void fix_unresolved_deltas(struct sha1file *f, int nr_unresolved); static void conclude_pack(int fix_thin_pack, const char *curr_pack, unsigned char *pack_sha1) { - if (nr_deltas == nr_resolved_deltas) { + if (nr_ref_deltas + nr_ofs_deltas == nr_resolved_deltas) { stop_progress(&progress); /* Flush remaining pack final 20-byte SHA1. */ flush(); @@ -1169,7 +1241,7 @@ static void conclude_pack(int fix_thin_pack, const char *curr_pack, unsigned cha struct sha1file *f; unsigned char read_sha1[20], tail_sha1[20]; struct strbuf msg = STRBUF_INIT; - int nr_unresolved = nr_deltas - nr_resolved_deltas; + int nr_unresolved = nr_ofs_deltas + nr_ref_deltas - nr_resolved_deltas; int nr_objects_initial = nr_objects; if (nr_unresolved <= 0) die(_("confusion beyond insanity")); @@ -1191,11 +1263,11 @@ static void conclude_pack(int fix_thin_pack, const char *curr_pack, unsigned cha die(_("Unexpected tail checksum for %s " "(disk corruption?)"), curr_pack); } - if (nr_deltas != nr_resolved_deltas) + if (nr_ofs_deltas + nr_ref_deltas != nr_resolved_deltas) die(Q_("pack has %d unresolved delta", "pack has %d unresolved deltas", - nr_deltas - nr_resolved_deltas), - nr_deltas - nr_resolved_deltas); + nr_ofs_deltas + nr_ref_deltas - nr_resolved_deltas), + nr_ofs_deltas + nr_ref_deltas - nr_resolved_deltas); } static int write_compressed(struct sha1file *f, void *in, unsigned int size) @@ -1254,14 +1326,14 @@ static struct object_entry *append_obj_to_pack(struct sha1file *f, static int delta_pos_compare(const void *_a, const void *_b) { - struct delta_entry *a = *(struct delta_entry **)_a; - struct delta_entry *b = *(struct delta_entry **)_b; + struct ref_delta_entry *a = *(struct ref_delta_entry **)_a; + struct ref_delta_entry *b = *(struct ref_delta_entry **)_b; return a->obj_no - b->obj_no; } static void fix_unresolved_deltas(struct sha1file *f, int nr_unresolved) { - struct delta_entry **sorted_by_pos; + struct ref_delta_entry **sorted_by_pos; int i, n = 0; /* @@ -1275,28 +1347,25 @@ static void fix_unresolved_deltas(struct sha1file *f, int nr_unresolved) * resolving deltas in the same order as their position in the pack. */ sorted_by_pos = xmalloc(nr_unresolved * sizeof(*sorted_by_pos)); - for (i = 0; i < nr_deltas; i++) { - if (objects[deltas[i].obj_no].real_type != OBJ_REF_DELTA) - continue; - sorted_by_pos[n++] = &deltas[i]; - } + for (i = 0; i < nr_ref_deltas; i++) + sorted_by_pos[n++] = &ref_deltas[i]; qsort(sorted_by_pos, n, sizeof(*sorted_by_pos), delta_pos_compare); for (i = 0; i < n; i++) { - struct delta_entry *d = sorted_by_pos[i]; + struct ref_delta_entry *d = sorted_by_pos[i]; enum object_type type; struct base_data *base_obj = alloc_base_data(); if (objects[d->obj_no].real_type != OBJ_REF_DELTA) continue; - base_obj->data = read_sha1_file(d->base.sha1, &type, &base_obj->size); + base_obj->data = read_sha1_file(d->sha1, &type, &base_obj->size); if (!base_obj->data) continue; - if (check_sha1_signature(d->base.sha1, base_obj->data, + if (check_sha1_signature(d->sha1, base_obj->data, base_obj->size, typename(type))) - die(_("local object %s is corrupt"), sha1_to_hex(d->base.sha1)); - base_obj->obj = append_obj_to_pack(f, d->base.sha1, + die(_("local object %s is corrupt"), sha1_to_hex(d->sha1)); + base_obj->obj = append_obj_to_pack(f, d->sha1, base_obj->data, base_obj->size, type); find_unresolved_deltas(base_obj); display_progress(progress, nr_resolved_deltas); @@ -1488,7 +1557,7 @@ static void read_idx_option(struct pack_idx_option *opts, const char *pack_name) static void show_pack_info(int stat_only) { - int i, baseobjects = nr_objects - nr_deltas; + int i, baseobjects = nr_objects - nr_ref_deltas - nr_ofs_deltas; unsigned long *chain_histogram = NULL; if (deepest_delta) @@ -1498,7 +1567,7 @@ static void show_pack_info(int stat_only) struct object_entry *obj = &objects[i]; if (is_delta_type(obj->type)) - chain_histogram[obj->delta_depth - 1]++; + chain_histogram[obj_stat[i].delta_depth - 1]++; if (stat_only) continue; printf("%s %-6s %lu %lu %"PRIuMAX, @@ -1507,8 +1576,8 @@ static void show_pack_info(int stat_only) (unsigned long)(obj[1].idx.offset - obj->idx.offset), (uintmax_t)obj->idx.offset); if (is_delta_type(obj->type)) { - struct object_entry *bobj = &objects[obj->base_object_no]; - printf(" %u %s", obj->delta_depth, sha1_to_hex(bobj->idx.sha1)); + struct object_entry *bobj = &objects[obj_stat[i].base_object_no]; + printf(" %u %s", obj_stat[i].delta_depth, sha1_to_hex(bobj->idx.sha1)); } putchar('\n'); } @@ -1671,11 +1740,14 @@ int cmd_index_pack(int argc, const char **argv, const char *prefix) curr_pack = open_pack_file(pack_name); parse_pack_header(); objects = xcalloc(nr_objects + 1, sizeof(struct object_entry)); - deltas = xcalloc(nr_objects, sizeof(struct delta_entry)); + if (show_stat) + obj_stat = xcalloc(nr_objects + 1, sizeof(struct object_stat)); + ofs_deltas = xcalloc(nr_objects, sizeof(struct ofs_delta_entry)); parse_pack_objects(pack_sha1); resolve_deltas(); conclude_pack(fix_thin_pack, curr_pack, pack_sha1); - free(deltas); + free(ofs_deltas); + free(ref_deltas); if (strict) foreign_nr = check_objects(); diff --git a/builtin/init-db.c b/builtin/init-db.c index ab9f86b889..4335738135 100644 --- a/builtin/init-db.c +++ b/builtin/init-db.c @@ -362,7 +362,6 @@ int set_git_dir_init(const char *git_dir, const char *real_git_dir, static void separate_git_dir(const char *git_dir) { struct stat st; - FILE *fp; if (!stat(git_link, &st)) { const char *src; @@ -378,11 +377,7 @@ static void separate_git_dir(const char *git_dir) die_errno(_("unable to move %s to %s"), src, git_dir); } - fp = fopen(git_link, "w"); - if (!fp) - die(_("Could not create git link %s"), git_link); - fprintf(fp, "gitdir: %s\n", git_dir); - fclose(fp); + write_file(git_link, 1, "gitdir: %s\n", git_dir); } int init_db(const char *template_dir, unsigned int flags) diff --git a/builtin/log.c b/builtin/log.c index 4c4e6be28c..878104943f 100644 --- a/builtin/log.c +++ b/builtin/log.c @@ -795,7 +795,7 @@ static int reopen_stdout(struct commit *commit, const char *subject, static void get_patch_ids(struct rev_info *rev, struct patch_ids *ids) { struct rev_info check_rev; - struct commit *commit; + struct commit *commit, *c1, *c2; struct object *o1, *o2; unsigned flags1, flags2; @@ -803,9 +803,11 @@ static void get_patch_ids(struct rev_info *rev, struct patch_ids *ids) die(_("Need exactly one range.")); o1 = rev->pending.objects[0].item; - flags1 = o1->flags; o2 = rev->pending.objects[1].item; + flags1 = o1->flags; flags2 = o2->flags; + c1 = lookup_commit_reference(o1->sha1); + c2 = lookup_commit_reference(o2->sha1); if ((flags1 & UNINTERESTING) == (flags2 & UNINTERESTING)) die(_("Not a range.")); @@ -827,10 +829,8 @@ static void get_patch_ids(struct rev_info *rev, struct patch_ids *ids) } /* reset for next revision walk */ - clear_commit_marks((struct commit *)o1, - SEEN | UNINTERESTING | SHOWN | ADDED); - clear_commit_marks((struct commit *)o2, - SEEN | UNINTERESTING | SHOWN | ADDED); + clear_commit_marks(c1, SEEN | UNINTERESTING | SHOWN | ADDED); + clear_commit_marks(c2, SEEN | UNINTERESTING | SHOWN | ADDED); o1->flags = flags1; o2->flags = flags2; } @@ -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 3b0f8f96d4..85c54dcd5a 100644 --- a/builtin/merge.c +++ b/builtin/merge.c @@ -492,8 +492,7 @@ static void merge_name(const char *remote, struct strbuf *msg) } if (len) { struct strbuf truname = STRBUF_INIT; - strbuf_addstr(&truname, "refs/heads/"); - strbuf_addstr(&truname, remote); + strbuf_addf(&truname, "refs/heads/%s", remote); strbuf_setlen(&truname, truname.len - len); if (ref_exists(truname.buf)) { strbuf_addf(msg, @@ -504,28 +503,7 @@ static void merge_name(const char *remote, struct strbuf *msg) strbuf_release(&truname); goto cleanup; } - } - - if (!strcmp(remote, "FETCH_HEAD") && - !access(git_path("FETCH_HEAD"), R_OK)) { - const char *filename; - FILE *fp; - struct strbuf line = STRBUF_INIT; - char *ptr; - - filename = git_path("FETCH_HEAD"); - fp = fopen(filename, "r"); - if (!fp) - die_errno(_("could not open '%s' for reading"), - filename); - strbuf_getline(&line, fp, '\n'); - fclose(fp); - ptr = strstr(line.buf, "\tnot-for-merge\t"); - if (ptr) - strbuf_remove(&line, ptr-line.buf+1, 13); - strbuf_addbuf(msg, &line); - strbuf_release(&line); - goto cleanup; + strbuf_release(&truname); } if (remote_head->util) { @@ -955,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.")); @@ -1037,28 +1015,24 @@ static int default_edit_option(void) st_stdin.st_mode == st_stdout.st_mode); } -static struct commit_list *collect_parents(struct commit *head_commit, - int *head_subsumed, - int argc, const char **argv) +static struct commit_list *reduce_parents(struct commit *head_commit, + int *head_subsumed, + struct commit_list *remoteheads) { - int i; - struct commit_list *remoteheads = NULL, *parents, *next; - struct commit_list **remotes = &remoteheads; + struct commit_list *parents, *next, **remotes = &remoteheads; - if (head_commit) - remotes = &commit_list_insert(head_commit, remotes)->next; - for (i = 0; i < argc; i++) { - struct commit *commit = get_merge_parent(argv[i]); - if (!commit) - help_unknown_ref(argv[i], "merge", - "not something we can merge"); - remotes = &commit_list_insert(commit, remotes)->next; - } - *remotes = NULL; + /* + * Is the current HEAD reachable from another commit being + * merged? If so we do not want to record it as a parent of + * the resulting merge, unless --no-ff is given. We will flip + * this variable to 0 when we find HEAD among the independent + * tips being merged. + */ + *head_subsumed = 1; + /* Find what parents to record by checking independent ones. */ parents = reduce_heads(remoteheads); - *head_subsumed = 1; /* we will flip this to 0 when we find it */ for (remoteheads = NULL, remotes = &remoteheads; parents; parents = next) { @@ -1068,7 +1042,119 @@ static struct commit_list *collect_parents(struct commit *head_commit, *head_subsumed = 0; else remotes = &commit_list_insert(commit, remotes)->next; + free(parents); + } + return remoteheads; +} + +static void prepare_merge_message(struct strbuf *merge_names, struct strbuf *merge_msg) +{ + struct fmt_merge_msg_opts opts; + + memset(&opts, 0, sizeof(opts)); + opts.add_title = !have_message; + opts.shortlog_len = shortlog_len; + opts.credit_people = (0 < option_edit); + + fmt_merge_msg(merge_names, merge_msg, &opts); + if (merge_msg->len) + strbuf_setlen(merge_msg, merge_msg->len - 1); +} + +static void handle_fetch_head(struct commit_list **remotes, struct strbuf *merge_names) +{ + const char *filename; + int fd, pos, npos; + struct strbuf fetch_head_file = STRBUF_INIT; + + if (!merge_names) + merge_names = &fetch_head_file; + + filename = git_path("FETCH_HEAD"); + fd = open(filename, O_RDONLY); + if (fd < 0) + die_errno(_("could not open '%s' for reading"), filename); + + if (strbuf_read(merge_names, fd, 0) < 0) + die_errno(_("could not read '%s'"), filename); + if (close(fd) < 0) + die_errno(_("could not close '%s'"), filename); + + for (pos = 0; pos < merge_names->len; pos = npos) { + unsigned char sha1[20]; + char *ptr; + struct commit *commit; + + ptr = strchr(merge_names->buf + pos, '\n'); + if (ptr) + npos = ptr - merge_names->buf + 1; + else + npos = merge_names->len; + + if (npos - pos < 40 + 2 || + get_sha1_hex(merge_names->buf + pos, sha1)) + commit = NULL; /* bad */ + else if (memcmp(merge_names->buf + pos + 40, "\t\t", 2)) + continue; /* not-for-merge */ + else { + char saved = merge_names->buf[pos + 40]; + merge_names->buf[pos + 40] = '\0'; + commit = get_merge_parent(merge_names->buf + pos); + merge_names->buf[pos + 40] = saved; + } + if (!commit) { + if (ptr) + *ptr = '\0'; + die("not something we can merge in %s: %s", + filename, merge_names->buf + pos); + } + remotes = &commit_list_insert(commit, remotes)->next; + } + + if (merge_names == &fetch_head_file) + strbuf_release(&fetch_head_file); +} + +static struct commit_list *collect_parents(struct commit *head_commit, + int *head_subsumed, + int argc, const char **argv, + struct strbuf *merge_msg) +{ + int i; + struct commit_list *remoteheads = NULL; + struct commit_list **remotes = &remoteheads; + struct strbuf merge_names = STRBUF_INIT, *autogen = NULL; + + if (merge_msg && (!have_message || shortlog_len)) + autogen = &merge_names; + + if (head_commit) + remotes = &commit_list_insert(head_commit, remotes)->next; + + if (argc == 1 && !strcmp(argv[0], "FETCH_HEAD")) { + handle_fetch_head(remotes, autogen); + remoteheads = reduce_parents(head_commit, head_subsumed, remoteheads); + } else { + for (i = 0; i < argc; i++) { + struct commit *commit = get_merge_parent(argv[i]); + if (!commit) + help_unknown_ref(argv[i], "merge", + "not something we can merge"); + remotes = &commit_list_insert(commit, remotes)->next; + } + remoteheads = reduce_parents(head_commit, head_subsumed, remoteheads); + if (autogen) { + struct commit_list *p; + for (p = remoteheads; p; p = p->next) + merge_name(merge_remote_util(p->item)->name, autogen); + } } + + if (autogen) { + prepare_merge_message(autogen, merge_msg); + strbuf_release(autogen); + } + return remoteheads; } @@ -1158,61 +1244,62 @@ int cmd_merge(int argc, const char **argv, const char *prefix) option_commit = 0; } - if (!abort_current_merge) { - if (!argc) { - if (default_to_upstream) - argc = setup_with_upstream(&argv); - else - die(_("No commit specified and merge.defaultToUpstream not set.")); - } else if (argc == 1 && !strcmp(argv[0], "-")) - argv[0] = "@{-1}"; + if (!argc) { + if (default_to_upstream) + argc = setup_with_upstream(&argv); + else + die(_("No commit specified and merge.defaultToUpstream not set.")); + } else if (argc == 1 && !strcmp(argv[0], "-")) { + argv[0] = "@{-1}"; } + if (!argc) usage_with_options(builtin_merge_usage, builtin_merge_options); - /* - * This could be traditional "merge <msg> HEAD <commit>..." and - * the way we can tell it is to see if the second token is HEAD, - * but some people might have misused the interface and used a - * commit-ish that is the same as HEAD there instead. - * Traditional format never would have "-m" so it is an - * additional safety measure to check for it. - */ - - if (!have_message && head_commit && - is_old_style_invocation(argc, argv, head_commit->object.sha1)) { - strbuf_addstr(&merge_msg, argv[0]); - head_arg = argv[1]; - argv += 2; - argc -= 2; - remoteheads = collect_parents(head_commit, &head_subsumed, argc, argv); - } else if (!head_commit) { + if (!head_commit) { struct commit *remote_head; /* * If the merged head is a valid one there is no reason * to forbid "git merge" into a branch yet to be born. * We do the same for "git pull". */ - if (argc != 1) - die(_("Can merge only exactly one commit into " - "empty head")); if (squash) die(_("Squash commit into empty head not supported yet")); if (fast_forward == FF_NO) die(_("Non-fast-forward commit does not make sense into " "an empty head")); - remoteheads = collect_parents(head_commit, &head_subsumed, argc, argv); + remoteheads = collect_parents(head_commit, &head_subsumed, + argc, argv, NULL); remote_head = remoteheads->item; if (!remote_head) die(_("%s - not something we can merge"), argv[0]); + if (remoteheads->next) + die(_("Can merge only exactly one commit into empty head")); read_empty(remote_head->object.sha1, 0); update_ref("initial pull", "HEAD", remote_head->object.sha1, NULL, 0, UPDATE_REFS_DIE_ON_ERR); goto done; - } else { - struct strbuf merge_names = STRBUF_INIT; + } + /* + * This could be traditional "merge <msg> HEAD <commit>..." and + * the way we can tell it is to see if the second token is HEAD, + * but some people might have misused the interface and used a + * commit-ish that is the same as HEAD there instead. + * Traditional format never would have "-m" so it is an + * additional safety measure to check for it. + */ + if (!have_message && + is_old_style_invocation(argc, argv, head_commit->object.sha1)) { + warning("old-style 'git merge <msg> HEAD <commit>' is deprecated."); + strbuf_addstr(&merge_msg, argv[0]); + head_arg = argv[1]; + argv += 2; + argc -= 2; + remoteheads = collect_parents(head_commit, &head_subsumed, + argc, argv, NULL); + } else { /* We are invoked directly as the first-class UI. */ head_arg = "HEAD"; @@ -1221,21 +1308,8 @@ int cmd_merge(int argc, const char **argv, const char *prefix) * the standard merge summary message to be appended * to the given message. */ - remoteheads = collect_parents(head_commit, &head_subsumed, argc, argv); - for (p = remoteheads; p; p = p->next) - merge_name(merge_remote_util(p->item)->name, &merge_names); - - if (!have_message || shortlog_len) { - struct fmt_merge_msg_opts opts; - memset(&opts, 0, sizeof(opts)); - opts.add_title = !have_message; - opts.shortlog_len = shortlog_len; - opts.credit_people = (0 < option_edit); - - fmt_merge_msg(&merge_names, &merge_msg, &opts); - if (merge_msg.len) - strbuf_setlen(&merge_msg, merge_msg.len - 1); - } + remoteheads = collect_parents(head_commit, &head_subsumed, + argc, argv, &merge_msg); } if (!head_commit || !argc) diff --git a/builtin/name-rev.c b/builtin/name-rev.c index 9736d4452f..248a3eb260 100644 --- a/builtin/name-rev.c +++ b/builtin/name-rev.c @@ -138,9 +138,9 @@ static int tipcmp(const void *a_, const void *b_) return hashcmp(a->sha1, b->sha1); } -static int name_ref(const char *path, const unsigned char *sha1, int flags, void *cb_data) +static int name_ref(const char *path, const struct object_id *oid, int flags, void *cb_data) { - struct object *o = parse_object(sha1); + struct object *o = parse_object(oid->hash); struct name_ref_data *data = cb_data; int can_abbreviate_output = data->tags_only && data->name_only; int deref = 0; @@ -160,7 +160,7 @@ static int name_ref(const char *path, const unsigned char *sha1, int flags, void } } - add_to_tip_table(sha1, path, can_abbreviate_output); + add_to_tip_table(oid->hash, path, can_abbreviate_output); while (o && o->type == OBJ_TAG) { struct tag *t = (struct tag *) o; diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c index c067107a6a..80fe8c7dc1 100644 --- a/builtin/pack-objects.c +++ b/builtin/pack-objects.c @@ -540,11 +540,11 @@ static enum write_one_status write_one(struct sha1file *f, return WRITE_ONE_WRITTEN; } -static int mark_tagged(const char *path, const unsigned char *sha1, int flag, +static int mark_tagged(const char *path, const struct object_id *oid, int flag, void *cb_data) { unsigned char peeled[20]; - struct object_entry *entry = packlist_find(&to_pack, sha1, NULL); + struct object_entry *entry = packlist_find(&to_pack, oid->hash, NULL); if (entry) entry->tagged = 1; @@ -2097,14 +2097,14 @@ static void ll_find_deltas(struct object_entry **list, unsigned list_size, #define ll_find_deltas(l, s, w, d, p) find_deltas(l, &s, w, d, p) #endif -static int add_ref_tag(const char *path, const unsigned char *sha1, int flag, void *cb_data) +static int add_ref_tag(const char *path, const struct object_id *oid, int flag, void *cb_data) { - unsigned char peeled[20]; + struct object_id peeled; if (starts_with(path, "refs/tags/") && /* is a tag? */ - !peel_ref(path, peeled) && /* peelable? */ - packlist_find(&to_pack, peeled, NULL)) /* object packed? */ - add_object_entry(sha1, OBJ_TAG, NULL, 0); + !peel_ref(path, peeled.hash) && /* peelable? */ + packlist_find(&to_pack, peeled.hash, NULL)) /* object packed? */ + add_object_entry(oid->hash, OBJ_TAG, NULL, 0); return 0; } diff --git a/builtin/patch-id.c b/builtin/patch-id.c index 77db8739b5..ba34dac4d2 100644 --- a/builtin/patch-id.c +++ b/builtin/patch-id.c @@ -1,14 +1,14 @@ #include "builtin.h" -static void flush_current_id(int patchlen, unsigned char *id, unsigned char *result) +static void flush_current_id(int patchlen, struct object_id *id, struct object_id *result) { char name[50]; if (!patchlen) return; - memcpy(name, sha1_to_hex(id), 41); - printf("%s %s\n", sha1_to_hex(result), name); + memcpy(name, oid_to_hex(id), GIT_SHA1_HEXSZ + 1); + printf("%s %s\n", oid_to_hex(result), name); } static int remove_space(char *line) @@ -53,23 +53,23 @@ static int scan_hunk_header(const char *p, int *p_before, int *p_after) return 1; } -static void flush_one_hunk(unsigned char *result, git_SHA_CTX *ctx) +static void flush_one_hunk(struct object_id *result, git_SHA_CTX *ctx) { - unsigned char hash[20]; + unsigned char hash[GIT_SHA1_RAWSZ]; unsigned short carry = 0; int i; git_SHA1_Final(hash, ctx); git_SHA1_Init(ctx); /* 20-byte sum, with carry */ - for (i = 0; i < 20; ++i) { - carry += result[i] + hash[i]; - result[i] = carry; + for (i = 0; i < GIT_SHA1_RAWSZ; ++i) { + carry += result->hash[i] + hash[i]; + result->hash[i] = carry; carry >>= 8; } } -static int get_one_patchid(unsigned char *next_sha1, unsigned char *result, +static int get_one_patchid(struct object_id *next_oid, struct object_id *result, struct strbuf *line_buf, int stable) { int patchlen = 0, found_next = 0; @@ -77,7 +77,7 @@ static int get_one_patchid(unsigned char *next_sha1, unsigned char *result, git_SHA_CTX ctx; git_SHA1_Init(&ctx); - hashclr(result); + oidclr(result); while (strbuf_getwholeline(line_buf, stdin, '\n') != EOF) { char *line = line_buf->buf; @@ -93,7 +93,7 @@ static int get_one_patchid(unsigned char *next_sha1, unsigned char *result, else if (!memcmp(line, "\\ ", 2) && 12 < strlen(line)) continue; - if (!get_sha1_hex(p, next_sha1)) { + if (!get_oid_hex(p, next_oid)) { found_next = 1; break; } @@ -143,7 +143,7 @@ static int get_one_patchid(unsigned char *next_sha1, unsigned char *result, } if (!found_next) - hashclr(next_sha1); + oidclr(next_oid); flush_one_hunk(result, &ctx); @@ -152,15 +152,15 @@ static int get_one_patchid(unsigned char *next_sha1, unsigned char *result, static void generate_id_list(int stable) { - unsigned char sha1[20], n[20], result[20]; + struct object_id oid, n, result; int patchlen; struct strbuf line_buf = STRBUF_INIT; - hashclr(sha1); + oidclr(&oid); while (!feof(stdin)) { - patchlen = get_one_patchid(n, result, &line_buf, stable); - flush_current_id(patchlen, sha1, result); - hashcpy(sha1, n); + patchlen = get_one_patchid(&n, &result, &line_buf, stable); + flush_current_id(patchlen, &oid, &result); + oidcpy(&oid, &n); } strbuf_release(&line_buf); } diff --git a/builtin/prune.c b/builtin/prune.c index 17094ad954..0c73246c72 100644 --- a/builtin/prune.c +++ b/builtin/prune.c @@ -76,6 +76,95 @@ static int prune_subdir(int nr, const char *path, void *data) return 0; } +static int prune_worktree(const char *id, struct strbuf *reason) +{ + struct stat st; + char *path; + int fd, len; + + if (!is_directory(git_path("worktrees/%s", id))) { + strbuf_addf(reason, _("Removing worktrees/%s: not a valid directory"), id); + return 1; + } + if (file_exists(git_path("worktrees/%s/locked", id))) + return 0; + if (stat(git_path("worktrees/%s/gitdir", id), &st)) { + strbuf_addf(reason, _("Removing worktrees/%s: gitdir file does not exist"), id); + return 1; + } + fd = open(git_path("worktrees/%s/gitdir", id), O_RDONLY); + if (fd < 0) { + strbuf_addf(reason, _("Removing worktrees/%s: unable to read gitdir file (%s)"), + id, strerror(errno)); + return 1; + } + len = st.st_size; + path = xmalloc(len + 1); + read_in_full(fd, path, len); + close(fd); + while (len && (path[len - 1] == '\n' || path[len - 1] == '\r')) + len--; + if (!len) { + strbuf_addf(reason, _("Removing worktrees/%s: invalid gitdir file"), id); + free(path); + return 1; + } + path[len] = '\0'; + if (!file_exists(path)) { + struct stat st_link; + free(path); + /* + * the repo is moved manually and has not been + * accessed since? + */ + if (!stat(git_path("worktrees/%s/link", id), &st_link) && + st_link.st_nlink > 1) + return 0; + if (st.st_mtime <= expire) { + strbuf_addf(reason, _("Removing worktrees/%s: gitdir file points to non-existent location"), id); + return 1; + } else { + return 0; + } + } + free(path); + return 0; +} + +static void prune_worktrees(void) +{ + struct strbuf reason = STRBUF_INIT; + struct strbuf path = STRBUF_INIT; + DIR *dir = opendir(git_path("worktrees")); + struct dirent *d; + int ret; + if (!dir) + return; + while ((d = readdir(dir)) != NULL) { + if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, "..")) + continue; + strbuf_reset(&reason); + if (!prune_worktree(d->d_name, &reason)) + continue; + if (show_only || verbose) + printf("%s\n", reason.buf); + if (show_only) + continue; + strbuf_reset(&path); + strbuf_addstr(&path, git_path("worktrees/%s", d->d_name)); + ret = remove_dir_recursively(&path, 0); + if (ret < 0 && errno == ENOTDIR) + ret = unlink(path.buf); + if (ret) + error(_("failed to remove: %s"), strerror(errno)); + } + closedir(dir); + if (!show_only) + rmdir(git_path("worktrees")); + strbuf_release(&reason); + strbuf_release(&path); +} + /* * Write errors (particularly out of space) can result in * failed temporary packs (and more rarely indexes and other @@ -102,10 +191,12 @@ int cmd_prune(int argc, const char **argv, const char *prefix) { struct rev_info revs; struct progress *progress = NULL; + int do_prune_worktrees = 0; const struct option options[] = { OPT__DRY_RUN(&show_only, N_("do not remove, show only")), OPT__VERBOSE(&verbose, N_("report pruned objects")), OPT_BOOL(0, "progress", &show_progress, N_("show progress")), + OPT_BOOL(0, "worktrees", &do_prune_worktrees, N_("prune .git/worktrees")), OPT_EXPIRY_DATE(0, "expire", &expire, N_("expire objects older than <time>")), OPT_END() @@ -119,6 +210,14 @@ int cmd_prune(int argc, const char **argv, const char *prefix) init_revisions(&revs, prefix); argc = parse_options(argc, argv, prefix, options, prune_usage, 0); + + if (do_prune_worktrees) { + if (argc) + die(_("--worktrees does not take extra arguments")); + prune_worktrees(); + return 0; + } + while (argc--) { unsigned char sha1[20]; const char *name = *argv++; diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c index 5292bb5a50..94d0571776 100644 --- a/builtin/receive-pack.c +++ b/builtin/receive-pack.c @@ -197,7 +197,7 @@ static void show_ref(const char *path, const unsigned char *sha1) } } -static int show_ref_cb(const char *path, const unsigned char *sha1, int flag, void *unused) +static int show_ref_cb(const char *path, const struct object_id *oid, int flag, void *unused) { path = strip_namespace(path); /* @@ -210,7 +210,7 @@ static int show_ref_cb(const char *path, const unsigned char *sha1, int flag, vo */ if (!path) path = ".have"; - show_ref(path, sha1); + show_ref(path, oid->hash); return 0; } @@ -228,6 +228,7 @@ static void collect_one_alternate_ref(const struct ref *ref, void *data) static void write_head_info(void) { struct sha1_array sa = SHA1_ARRAY_INIT; + for_each_alternate_ref(collect_one_alternate_ref, &sa); sha1_array_for_each_unique(&sa, show_one_alternate_sha1, NULL); sha1_array_clear(&sa); @@ -1008,7 +1009,7 @@ static void run_update_post_hook(struct command *commands) int argc; const char **argv; struct child_process proc = CHILD_PROCESS_INIT; - char *hook; + const char *hook; hook = find_hook("post-update"); for (argc = 0, cmd = commands; cmd; cmd = cmd->next) { diff --git a/builtin/reflog.c b/builtin/reflog.c index 8182b648b9..c2eb8ff840 100644 --- a/builtin/reflog.c +++ b/builtin/reflog.c @@ -313,14 +313,14 @@ static int should_expire_reflog_ent(unsigned char *osha1, unsigned char *nsha1, return 0; } -static int push_tip_to_list(const char *refname, const unsigned char *sha1, +static int push_tip_to_list(const char *refname, const struct object_id *oid, int flags, void *cb_data) { struct commit_list **list = cb_data; struct commit *tip_commit; if (flags & REF_ISSYMREF) return 0; - tip_commit = lookup_commit_reference_gently(sha1, 1); + tip_commit = lookup_commit_reference_gently(oid->hash, 1); if (!tip_commit) return 0; commit_list_insert(tip_commit, list); @@ -352,6 +352,7 @@ static void reflog_expiry_prepare(const char *refname, if (cb->unreachable_expire_kind != UE_ALWAYS) { if (cb->unreachable_expire_kind == UE_HEAD) { struct commit_list *elem; + for_each_ref(push_tip_to_list, &cb->tips); for (elem = cb->tips; elem; elem = elem->next) commit_list_insert(elem->item, &cb->mark_list); @@ -379,14 +380,14 @@ static void reflog_expiry_cleanup(void *cb_data) } } -static int collect_reflog(const char *ref, const unsigned char *sha1, int unused, void *cb_data) +static int collect_reflog(const char *ref, const struct object_id *oid, int unused, void *cb_data) { struct collected_reflog *e; struct collect_reflog_cb *cb = cb_data; size_t namelen = strlen(ref); e = xmalloc(sizeof(*e) + namelen + 1); - hashcpy(e->sha1, sha1); + hashcpy(e->sha1, oid->hash); memcpy(e->reflog, ref, namelen + 1); ALLOC_GROW(cb->e, cb->nr + 1, cb->alloc); cb->e[cb->nr++] = e; diff --git a/builtin/remote.c b/builtin/remote.c index 5d3ab906bc..f4a6ec9f13 100644 --- a/builtin/remote.c +++ b/builtin/remote.c @@ -509,11 +509,10 @@ struct branches_for_remote { }; static int add_branch_for_removal(const char *refname, - const unsigned char *sha1, int flags, void *cb_data) + const struct object_id *oid, int flags, void *cb_data) { struct branches_for_remote *branches = cb_data; struct refspec refspec; - struct string_list_item *item; struct known_remote *kr; memset(&refspec, 0, sizeof(refspec)); @@ -543,9 +542,7 @@ static int add_branch_for_removal(const char *refname, if (flags & REF_ISSYMREF) return unlink(git_path("%s", refname)); - item = string_list_append(branches->branches, refname); - item->util = xmalloc(20); - hashcpy(item->util, sha1); + string_list_append(branches->branches, refname); return 0; } @@ -557,20 +554,20 @@ struct rename_info { }; static int read_remote_branches(const char *refname, - const unsigned char *sha1, int flags, void *cb_data) + const struct object_id *oid, int flags, void *cb_data) { struct rename_info *rename = cb_data; struct strbuf buf = STRBUF_INIT; struct string_list_item *item; int flag; - unsigned char orig_sha1[20]; + struct object_id orig_oid; const char *symref; strbuf_addf(&buf, "refs/remotes/%s/", rename->old); if (starts_with(refname, buf.buf)) { item = string_list_append(rename->remote_branches, xstrdup(refname)); symref = resolve_ref_unsafe(refname, RESOLVE_REF_READING, - orig_sha1, &flag); + orig_oid.hash, &flag); if (flag & REF_ISSYMREF) item->util = xstrdup(symref); else @@ -584,7 +581,7 @@ static int migrate_file(struct remote *remote) { struct strbuf buf = STRBUF_INIT; int i; - char *path = NULL; + const char *path = NULL; strbuf_addf(&buf, "remote.%s.url", remote->name); for (i = 0; i < remote->url_nr; i++) @@ -704,9 +701,9 @@ static int mv(int argc, const char **argv) for (i = 0; i < remote_branches.nr; i++) { struct string_list_item *item = remote_branches.items + i; int flag = 0; - unsigned char sha1[20]; + struct object_id oid; - read_ref_full(item->string, RESOLVE_REF_READING, sha1, &flag); + read_ref_full(item->string, RESOLVE_REF_READING, oid.hash, &flag); if (!(flag & REF_ISSYMREF)) continue; if (delete_ref(item->string, NULL, REF_NODEREF)) @@ -826,7 +823,7 @@ static int rm(int argc, const char **argv) if (!result) result = remove_branches(&branches); - string_list_clear(&branches, 1); + string_list_clear(&branches, 0); if (skipped.nr) { fprintf_ln(stderr, @@ -867,7 +864,7 @@ static void free_remote_ref_states(struct ref_states *states) } static int append_ref_to_tracked_list(const char *refname, - const unsigned char *sha1, int flags, void *cb_data) + const struct object_id *oid, int flags, void *cb_data) { struct ref_states *states = cb_data; struct refspec refspec; diff --git a/builtin/repack.c b/builtin/repack.c index f2edeb0f4c..af7340c7ba 100644 --- a/builtin/repack.c +++ b/builtin/repack.c @@ -285,7 +285,8 @@ int cmd_repack(int argc, const char **argv, const char *prefix) failed = 0; for_each_string_list_item(item, &names) { for (ext = 0; ext < ARRAY_SIZE(exts); ext++) { - char *fname, *fname_old; + const char *fname_old; + char *fname; fname = mkpathdup("%s/pack-%s%s", packdir, item->string, exts[ext].name); if (!file_exists(fname)) { @@ -313,7 +314,8 @@ int cmd_repack(int argc, const char **argv, const char *prefix) if (failed) { struct string_list rollback_failure = STRING_LIST_INIT_DUP; for_each_string_list_item(item, &rollback) { - char *fname, *fname_old; + const char *fname_old; + char *fname; fname = mkpathdup("%s/%s", packdir, item->string); fname_old = mkpath("%s/old-%s", packdir, item->string); if (rename(fname_old, fname)) @@ -366,7 +368,7 @@ int cmd_repack(int argc, const char **argv, const char *prefix) /* Remove the "old-" files */ for_each_string_list_item(item, &names) { for (ext = 0; ext < ARRAY_SIZE(exts); ext++) { - char *fname; + const char *fname; fname = mkpath("%s/old-%s%s", packdir, item->string, diff --git a/builtin/replace.c b/builtin/replace.c index 54bf01acb4..0d52e7fa1d 100644 --- a/builtin/replace.c +++ b/builtin/replace.c @@ -35,7 +35,7 @@ struct show_data { enum replace_format format; }; -static int show_reference(const char *refname, const unsigned char *sha1, +static int show_reference(const char *refname, const struct object_id *oid, int flag, void *cb_data) { struct show_data *data = cb_data; @@ -44,19 +44,19 @@ static int show_reference(const char *refname, const unsigned char *sha1, if (data->format == REPLACE_FORMAT_SHORT) printf("%s\n", refname); else if (data->format == REPLACE_FORMAT_MEDIUM) - printf("%s -> %s\n", refname, sha1_to_hex(sha1)); + printf("%s -> %s\n", refname, oid_to_hex(oid)); else { /* data->format == REPLACE_FORMAT_LONG */ - unsigned char object[20]; + struct object_id object; enum object_type obj_type, repl_type; - if (get_sha1(refname, object)) + if (get_sha1(refname, object.hash)) return error("Failed to resolve '%s' as a valid ref.", refname); - obj_type = sha1_object_info(object, NULL); - repl_type = sha1_object_info(sha1, NULL); + obj_type = sha1_object_info(object.hash, NULL); + repl_type = sha1_object_info(oid->hash, NULL); printf("%s (%s) -> %s (%s)\n", refname, typename(obj_type), - sha1_to_hex(sha1), typename(repl_type)); + oid_to_hex(oid), typename(repl_type)); } } @@ -82,7 +82,7 @@ static int list_replace_refs(const char *pattern, const char *format) "valid formats are 'short', 'medium' and 'long'\n", format); - for_each_replace_ref(show_reference, (void *) &data); + for_each_replace_ref(show_reference, (void *)&data); return 0; } diff --git a/builtin/rev-parse.c b/builtin/rev-parse.c index 3626c61da6..b6232390a6 100644 --- a/builtin/rev-parse.c +++ b/builtin/rev-parse.c @@ -190,17 +190,17 @@ static int show_default(void) return 0; } -static int show_reference(const char *refname, const unsigned char *sha1, int flag, void *cb_data) +static int show_reference(const char *refname, const struct object_id *oid, int flag, void *cb_data) { if (ref_excluded(ref_excludes, refname)) return 0; - show_rev(NORMAL, sha1, refname); + show_rev(NORMAL, oid->hash, refname); return 0; } -static int anti_reference(const char *refname, const unsigned char *sha1, int flag, void *cb_data) +static int anti_reference(const char *refname, const struct object_id *oid, int flag, void *cb_data) { - show_rev(REVERSED, sha1, refname); + show_rev(REVERSED, oid->hash, refname); return 0; } @@ -533,6 +533,13 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix) for (i = 1; i < argc; i++) { const char *arg = argv[i]; + if (!strcmp(arg, "--git-path")) { + if (!argv[i + 1]) + die("--git-path requires an argument"); + puts(git_path("%s", argv[i + 1])); + i++; + continue; + } if (as_is) { if (show_file(arg, output_prefix) && as_is < 2) verify_filename(prefix, arg, 0); @@ -755,6 +762,10 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix) free(cwd); continue; } + if (!strcmp(arg, "--git-common-dir")) { + puts(get_git_common_dir()); + continue; + } if (!strcmp(arg, "--resolve-git-dir")) { const char *gitdir = argv[++i]; if (!gitdir) diff --git a/builtin/show-branch.c b/builtin/show-branch.c index f3fb5fb2bf..323f857463 100644 --- a/builtin/show-branch.c +++ b/builtin/show-branch.c @@ -369,10 +369,10 @@ static void sort_ref_range(int bottom, int top) compare_ref_name); } -static int append_ref(const char *refname, const unsigned char *sha1, +static int append_ref(const char *refname, const struct object_id *oid, int allow_dups) { - struct commit *commit = lookup_commit_reference_gently(sha1, 1); + struct commit *commit = lookup_commit_reference_gently(oid->hash, 1); int i; if (!commit) @@ -394,39 +394,42 @@ static int append_ref(const char *refname, const unsigned char *sha1, return 0; } -static int append_head_ref(const char *refname, const unsigned char *sha1, int flag, void *cb_data) +static int append_head_ref(const char *refname, const struct object_id *oid, + int flag, void *cb_data) { - unsigned char tmp[20]; + struct object_id tmp; int ofs = 11; if (!starts_with(refname, "refs/heads/")) return 0; /* If both heads/foo and tags/foo exists, get_sha1 would * get confused. */ - if (get_sha1(refname + ofs, tmp) || hashcmp(tmp, sha1)) + if (get_sha1(refname + ofs, tmp.hash) || oidcmp(&tmp, oid)) ofs = 5; - return append_ref(refname + ofs, sha1, 0); + return append_ref(refname + ofs, oid, 0); } -static int append_remote_ref(const char *refname, const unsigned char *sha1, int flag, void *cb_data) +static int append_remote_ref(const char *refname, const struct object_id *oid, + int flag, void *cb_data) { - unsigned char tmp[20]; + struct object_id tmp; int ofs = 13; if (!starts_with(refname, "refs/remotes/")) return 0; /* If both heads/foo and tags/foo exists, get_sha1 would * get confused. */ - if (get_sha1(refname + ofs, tmp) || hashcmp(tmp, sha1)) + if (get_sha1(refname + ofs, tmp.hash) || oidcmp(&tmp, oid)) ofs = 5; - return append_ref(refname + ofs, sha1, 0); + return append_ref(refname + ofs, oid, 0); } -static int append_tag_ref(const char *refname, const unsigned char *sha1, int flag, void *cb_data) +static int append_tag_ref(const char *refname, const struct object_id *oid, + int flag, void *cb_data) { if (!starts_with(refname, "refs/tags/")) return 0; - return append_ref(refname + 5, sha1, 0); + return append_ref(refname + 5, oid, 0); } static const char *match_ref_pattern = NULL; @@ -440,7 +443,8 @@ static int count_slash(const char *s) return cnt; } -static int append_matching_ref(const char *refname, const unsigned char *sha1, int flag, void *cb_data) +static int append_matching_ref(const char *refname, const struct object_id *oid, + int flag, void *cb_data) { /* we want to allow pattern hold/<asterisk> to show all * branches under refs/heads/hold/, and v0.99.9? to show @@ -456,21 +460,23 @@ static int append_matching_ref(const char *refname, const unsigned char *sha1, i if (wildmatch(match_ref_pattern, tail, 0, NULL)) return 0; if (starts_with(refname, "refs/heads/")) - return append_head_ref(refname, sha1, flag, cb_data); + return append_head_ref(refname, oid, flag, cb_data); if (starts_with(refname, "refs/tags/")) - return append_tag_ref(refname, sha1, flag, cb_data); - return append_ref(refname, sha1, 0); + return append_tag_ref(refname, oid, flag, cb_data); + return append_ref(refname, oid, 0); } static void snarf_refs(int head, int remotes) { if (head) { int orig_cnt = ref_name_cnt; + for_each_ref(append_head_ref, NULL); sort_ref_range(orig_cnt, ref_name_cnt); } if (remotes) { int orig_cnt = ref_name_cnt; + for_each_ref(append_remote_ref, NULL); sort_ref_range(orig_cnt, ref_name_cnt); } @@ -530,14 +536,15 @@ static int show_independent(struct commit **rev, static void append_one_rev(const char *av) { - unsigned char revkey[20]; - if (!get_sha1(av, revkey)) { - append_ref(av, revkey, 0); + struct object_id revkey; + if (!get_sha1(av, revkey.hash)) { + append_ref(av, &revkey, 0); return; } if (strchr(av, '*') || strchr(av, '?') || strchr(av, '[')) { /* glob style match */ int saved_matches = ref_name_cnt; + match_ref_pattern = av; match_ref_slash = count_slash(av); for_each_ref(append_matching_ref, NULL); @@ -636,7 +643,7 @@ int cmd_show_branch(int ac, const char **av, const char *prefix) char head[128]; const char *head_p; int head_len; - unsigned char head_sha1[20]; + struct object_id head_oid; int merge_base = 0; int independent = 0; int no_name = 0; @@ -718,11 +725,11 @@ int cmd_show_branch(int ac, const char **av, const char *prefix) } /* If nothing is specified, show all branches by default */ - if (ac + all_heads + all_remotes == 0) + if (ac <= topics && all_heads + all_remotes == 0) all_heads = 1; if (reflog) { - unsigned char sha1[20]; + struct object_id oid; char nth_desc[256]; char *ref; int base = 0; @@ -733,7 +740,7 @@ int cmd_show_branch(int ac, const char **av, const char *prefix) fake_av[0] = resolve_refdup("HEAD", RESOLVE_REF_READING, - sha1, NULL); + oid.hash, NULL); fake_av[1] = NULL; av = fake_av; ac = 1; @@ -744,7 +751,7 @@ int cmd_show_branch(int ac, const char **av, const char *prefix) if (MAX_REVS < reflog) die("Only %d entries can be shown at one time.", MAX_REVS); - if (!dwim_ref(*av, strlen(*av), sha1, &ref)) + if (!dwim_ref(*av, strlen(*av), oid.hash, &ref)) die("No such ref %s", *av); /* Has the base been specified? */ @@ -755,7 +762,7 @@ int cmd_show_branch(int ac, const char **av, const char *prefix) /* Ah, that is a date spec... */ unsigned long at; at = approxidate(reflog_base); - read_ref_at(ref, flags, at, -1, sha1, NULL, + read_ref_at(ref, flags, at, -1, oid.hash, NULL, NULL, NULL, &base); } } @@ -766,7 +773,7 @@ int cmd_show_branch(int ac, const char **av, const char *prefix) unsigned long timestamp; int tz; - if (read_ref_at(ref, flags, 0, base+i, sha1, &logmsg, + if (read_ref_at(ref, flags, 0, base+i, oid.hash, &logmsg, ×tamp, &tz, NULL)) { reflog = i; break; @@ -781,21 +788,21 @@ int cmd_show_branch(int ac, const char **av, const char *prefix) msg); free(logmsg); sprintf(nth_desc, "%s@{%d}", *av, base+i); - append_ref(nth_desc, sha1, 1); + append_ref(nth_desc, &oid, 1); } free(ref); } - else if (all_heads + all_remotes) - snarf_refs(all_heads, all_remotes); else { while (0 < ac) { append_one_rev(*av); ac--; av++; } + if (all_heads + all_remotes) + snarf_refs(all_heads, all_remotes); } head_p = resolve_ref_unsafe("HEAD", RESOLVE_REF_READING, - head_sha1, NULL); + head_oid.hash, NULL); if (head_p) { head_len = strlen(head_p); memcpy(head, head_p, head_len + 1); @@ -814,7 +821,7 @@ int cmd_show_branch(int ac, const char **av, const char *prefix) if (rev_is_head(head, head_len, ref_name[i], - head_sha1, NULL)) + head_oid.hash, NULL)) has_head++; } if (!has_head) { @@ -829,17 +836,17 @@ int cmd_show_branch(int ac, const char **av, const char *prefix) } for (num_rev = 0; ref_name[num_rev]; num_rev++) { - unsigned char revkey[20]; + struct object_id revkey; unsigned int flag = 1u << (num_rev + REV_SHIFT); if (MAX_REVS <= num_rev) die("cannot handle more than %d revs.", MAX_REVS); - if (get_sha1(ref_name[num_rev], revkey)) + if (get_sha1(ref_name[num_rev], revkey.hash)) die("'%s' is not a valid ref.", ref_name[num_rev]); - commit = lookup_commit_reference(revkey); + commit = lookup_commit_reference(revkey.hash); if (!commit) die("cannot find commit %s (%s)", - ref_name[num_rev], revkey); + ref_name[num_rev], oid_to_hex(&revkey)); parse_commit(commit); mark_seen(commit, &seen); @@ -873,7 +880,7 @@ int cmd_show_branch(int ac, const char **av, const char *prefix) int is_head = rev_is_head(head, head_len, ref_name[i], - head_sha1, + head_oid.hash, rev[i]->object.sha1); if (extra < 0) printf("%c [%s] ", diff --git a/builtin/show-ref.c b/builtin/show-ref.c index afb10309d6..dfbc314ac2 100644 --- a/builtin/show-ref.c +++ b/builtin/show-ref.c @@ -17,19 +17,20 @@ static int deref_tags, show_head, tags_only, heads_only, found_match, verify, static const char **pattern; static const char *exclude_existing_arg; -static void show_one(const char *refname, const unsigned char *sha1) +static void show_one(const char *refname, const struct object_id *oid) { - const char *hex = find_unique_abbrev(sha1, abbrev); + const char *hex = find_unique_abbrev(oid->hash, abbrev); if (hash_only) printf("%s\n", hex); else printf("%s %s\n", hex, refname); } -static int show_ref(const char *refname, const unsigned char *sha1, int flag, void *cbdata) +static int show_ref(const char *refname, const struct object_id *oid, + int flag, void *cbdata) { const char *hex; - unsigned char peeled[20]; + struct object_id peeled; if (show_head && !strcmp(refname, "HEAD")) goto match; @@ -69,26 +70,27 @@ match: * detect and return error if the repository is corrupt and * ref points at a nonexistent object. */ - if (!has_sha1_file(sha1)) + if (!has_sha1_file(oid->hash)) die("git show-ref: bad ref %s (%s)", refname, - sha1_to_hex(sha1)); + oid_to_hex(oid)); if (quiet) return 0; - show_one(refname, sha1); + show_one(refname, oid); if (!deref_tags) return 0; - if (!peel_ref(refname, peeled)) { - hex = find_unique_abbrev(peeled, abbrev); + if (!peel_ref(refname, peeled.hash)) { + hex = find_unique_abbrev(peeled.hash, abbrev); printf("%s %s^{}\n", hex, refname); } return 0; } -static int add_existing(const char *refname, const unsigned char *sha1, int flag, void *cbdata) +static int add_existing(const char *refname, const struct object_id *oid, + int flag, void *cbdata) { struct string_list *list = (struct string_list *)cbdata; string_list_insert(list, refname); @@ -208,12 +210,12 @@ int cmd_show_ref(int argc, const char **argv, const char *prefix) if (!pattern) die("--verify requires a reference"); while (*pattern) { - unsigned char sha1[20]; + struct object_id oid; if (starts_with(*pattern, "refs/") && - !read_ref(*pattern, sha1)) { + !read_ref(*pattern, oid.hash)) { if (!quiet) - show_one(*pattern, sha1); + show_one(*pattern, &oid); } else if (!quiet) die("'%s' - not a valid ref", *pattern); diff --git a/builtin/tag.c b/builtin/tag.c index 6f07ac6b93..5f6cdc5a03 100644 --- a/builtin/tag.c +++ b/builtin/tag.c @@ -176,7 +176,7 @@ static enum contains_result contains(struct commit *candidate, return contains_test(candidate, want); } -static void show_tag_lines(const unsigned char *sha1, int lines) +static void show_tag_lines(const struct object_id *oid, int lines) { int i; unsigned long size; @@ -184,14 +184,14 @@ static void show_tag_lines(const unsigned char *sha1, int lines) char *buf, *sp, *eol; size_t len; - buf = read_sha1_file(sha1, &type, &size); + buf = read_sha1_file(oid->hash, &type, &size); if (!buf) - die_errno("unable to read object %s", sha1_to_hex(sha1)); + die_errno("unable to read object %s", oid_to_hex(oid)); if (type != OBJ_COMMIT && type != OBJ_TAG) goto free_return; if (!size) die("an empty %s object %s?", - typename(type), sha1_to_hex(sha1)); + typename(type), oid_to_hex(oid)); /* skip header */ sp = strstr(buf, "\n\n"); @@ -215,7 +215,7 @@ free_return: free(buf); } -static int show_reference(const char *refname, const unsigned char *sha1, +static int show_reference(const char *refname, const struct object_id *oid, int flag, void *cb_data) { struct tag_filter *filter = cb_data; @@ -224,14 +224,14 @@ static int show_reference(const char *refname, const unsigned char *sha1, if (filter->with_commit) { struct commit *commit; - commit = lookup_commit_reference_gently(sha1, 1); + commit = lookup_commit_reference_gently(oid->hash, 1); if (!commit) return 0; if (!contains(commit, filter->with_commit)) return 0; } - if (points_at.nr && !match_points_at(refname, sha1)) + if (points_at.nr && !match_points_at(refname, oid->hash)) return 0; if (!filter->lines) { @@ -242,7 +242,7 @@ static int show_reference(const char *refname, const unsigned char *sha1, return 0; } printf("%-15s ", refname); - show_tag_lines(sha1, filter->lines); + show_tag_lines(oid, filter->lines); putchar('\n'); } @@ -268,7 +268,7 @@ static int list_tags(const char **patterns, int lines, memset(&filter.tags, 0, sizeof(filter.tags)); filter.tags.strdup_strings = 1; - for_each_tag_ref(show_reference, (void *) &filter); + for_each_tag_ref(show_reference, (void *)&filter); if (sort) { int i; if ((sort & SORT_MASK) == VERCMP_SORT) diff --git a/builtin/update-index.c b/builtin/update-index.c index 6271b54adc..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); @@ -532,10 +693,9 @@ static int do_unresolve(int ac, const char **av, for (i = 1; i < ac; i++) { const char *arg = av[i]; - const char *p = prefix_path(prefix, prefix_length, arg); + char *p = prefix_path(prefix, prefix_length, arg); err |= unresolve_one(p); - if (p < arg || p > arg + strlen(arg)) - free((char *)p); + free(p); } return err; } @@ -742,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; @@ -833,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() }; @@ -871,14 +1036,14 @@ int cmd_update_index(int argc, const char **argv, const char *prefix) case PARSE_OPT_DONE: { const char *path = ctx.argv[0]; - const char *p; + char *p; setup_work_tree(); p = prefix_path(prefix, prefix_length, path); update_one(p); if (set_executable_bit) chmod_path(set_executable_bit, p); - free((char *)p); + free(p); ctx.argc--; ctx.argv++; break; @@ -909,7 +1074,7 @@ int cmd_update_index(int argc, const char **argv, const char *prefix) setup_work_tree(); while (strbuf_getline(&buf, stdin, line_termination) != EOF) { - const char *p; + char *p; if (line_termination && buf.buf[0] == '"') { strbuf_reset(&nbuf); if (unquote_c_style(&nbuf, buf.buf, NULL)) @@ -920,7 +1085,7 @@ int cmd_update_index(int argc, const char **argv, const char *prefix) update_one(p); if (set_executable_bit) chmod_path(set_executable_bit, p); - free((char *)p); + free(p); } strbuf_release(&nbuf); strbuf_release(&buf); @@ -939,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) { diff --git a/bulk-checkin.c b/bulk-checkin.c index 8d157eba45..7cffc3a579 100644 --- a/bulk-checkin.c +++ b/bulk-checkin.c @@ -24,7 +24,7 @@ static struct bulk_checkin_state { static void finish_bulk_checkin(struct bulk_checkin_state *state) { - unsigned char sha1[20]; + struct object_id oid; struct strbuf packname = STRBUF_INIT; int i; @@ -36,11 +36,11 @@ static void finish_bulk_checkin(struct bulk_checkin_state *state) unlink(state->pack_tmp_name); goto clear_exit; } else if (state->nr_written == 1) { - sha1close(state->f, sha1, CSUM_FSYNC); + sha1close(state->f, oid.hash, CSUM_FSYNC); } else { - int fd = sha1close(state->f, sha1, 0); - fixup_pack_header_footer(fd, sha1, state->pack_tmp_name, - state->nr_written, sha1, + int fd = sha1close(state->f, oid.hash, 0); + fixup_pack_header_footer(fd, oid.hash, state->pack_tmp_name, + state->nr_written, oid.hash, state->offset); close(fd); } @@ -48,7 +48,7 @@ static void finish_bulk_checkin(struct bulk_checkin_state *state) strbuf_addf(&packname, "%s/pack/pack-", get_object_directory()); finish_tmp_packfile(&packname, state->pack_tmp_name, state->written, state->nr_written, - &state->pack_idx_opts, sha1); + &state->pack_idx_opts, oid.hash); for (i = 0; i < state->nr_written; i++) free(state->written[i]); @@ -43,6 +43,14 @@ int git_deflate_end_gently(git_zstream *); int git_deflate(git_zstream *, int flush); unsigned long git_deflate_bound(git_zstream *, unsigned long); +/* The length in bytes and in hex digits of an object name (SHA-1 value). */ +#define GIT_SHA1_RAWSZ 20 +#define GIT_SHA1_HEXSZ (2 * GIT_SHA1_RAWSZ) + +struct object_id { + unsigned char hash[GIT_SHA1_RAWSZ]; +}; + #if defined(DT_UNKNOWN) && !defined(NO_D_TYPE_IN_DIRENT) #define DTYPE(de) ((de)->d_type) #else @@ -289,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; @@ -304,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; @@ -370,6 +382,7 @@ static inline enum object_type object_type(unsigned int mode) /* Double-check local_repo_env below if you add to this list. */ #define GIT_DIR_ENVIRONMENT "GIT_DIR" +#define GIT_COMMON_DIR_ENVIRONMENT "GIT_COMMON_DIR" #define GIT_NAMESPACE_ENVIRONMENT "GIT_NAMESPACE" #define GIT_WORK_TREE_ENVIRONMENT "GIT_WORK_TREE" #define GIT_PREFIX_ENVIRONMENT "GIT_PREFIX" @@ -423,11 +436,13 @@ extern int is_inside_git_dir(void); extern char *git_work_tree_cfg; extern int is_inside_work_tree(void); extern const char *get_git_dir(void); +extern const char *get_git_common_dir(void); extern int is_git_directory(const char *path); extern char *get_object_directory(void); extern char *get_index_file(void); extern char *get_graft_file(void); extern int set_git_dir(const char *path); +extern int get_common_dir(struct strbuf *sb, const char *gitdir); extern const char *get_git_namespace(void); extern const char *strip_namespace(const char *namespaced_ref); extern const char *get_git_work_tree(void); @@ -552,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); @@ -612,6 +629,7 @@ extern int core_apply_sparse_checkout; extern int precomposed_unicode; extern int protect_hfs; extern int protect_ntfs; +extern int git_db_env, git_index_env, git_graft_env, git_common_dir_env; /* * Include broken refs in all ref iterations, which will @@ -682,18 +700,19 @@ extern int check_repository_format(void); extern char *mksnpath(char *buf, size_t n, const char *fmt, ...) __attribute__((format (printf, 3, 4))); -extern char *git_snpath(char *buf, size_t n, const char *fmt, ...) - __attribute__((format (printf, 3, 4))); +extern void strbuf_git_path(struct strbuf *sb, const char *fmt, ...) + __attribute__((format (printf, 2, 3))); extern char *git_pathdup(const char *fmt, ...) __attribute__((format (printf, 1, 2))); extern char *mkpathdup(const char *fmt, ...) __attribute__((format (printf, 1, 2))); /* Return a statically allocated filename matching the sha1 signature */ -extern char *mkpath(const char *fmt, ...) __attribute__((format (printf, 1, 2))); -extern char *git_path(const char *fmt, ...) __attribute__((format (printf, 1, 2))); -extern char *git_path_submodule(const char *path, const char *fmt, ...) +extern const char *mkpath(const char *fmt, ...) __attribute__((format (printf, 1, 2))); +extern const char *git_path(const char *fmt, ...) __attribute__((format (printf, 1, 2))); +extern const char *git_path_submodule(const char *path, const char *fmt, ...) __attribute__((format (printf, 2, 3))); +extern void report_linked_checkout_garbage(void); /* * Return the name of the file in the local object database that would @@ -718,13 +737,13 @@ extern char *sha1_pack_name(const unsigned char *sha1); extern char *sha1_pack_index_name(const unsigned char *sha1); extern const char *find_unique_abbrev(const unsigned char *sha1, int); -extern const unsigned char null_sha1[20]; +extern const unsigned char null_sha1[GIT_SHA1_RAWSZ]; static inline int hashcmp(const unsigned char *sha1, const unsigned char *sha2) { int i; - for (i = 0; i < 20; i++, sha1++, sha2++) { + for (i = 0; i < GIT_SHA1_RAWSZ; i++, sha1++, sha2++) { if (*sha1 != *sha2) return *sha1 - *sha2; } @@ -732,20 +751,42 @@ static inline int hashcmp(const unsigned char *sha1, const unsigned char *sha2) return 0; } +static inline int oidcmp(const struct object_id *oid1, const struct object_id *oid2) +{ + return hashcmp(oid1->hash, oid2->hash); +} + static inline int is_null_sha1(const unsigned char *sha1) { return !hashcmp(sha1, null_sha1); } +static inline int is_null_oid(const struct object_id *oid) +{ + return !hashcmp(oid->hash, null_sha1); +} + static inline void hashcpy(unsigned char *sha_dst, const unsigned char *sha_src) { - memcpy(sha_dst, sha_src, 20); + memcpy(sha_dst, sha_src, GIT_SHA1_RAWSZ); } + +static inline void oidcpy(struct object_id *dst, const struct object_id *src) +{ + hashcpy(dst->hash, src->hash); +} + static inline void hashclr(unsigned char *hash) { - memset(hash, 0, 20); + memset(hash, 0, GIT_SHA1_RAWSZ); +} + +static inline void oidclr(struct object_id *oid) +{ + hashclr(oid->hash); } + #define EMPTY_TREE_SHA1_HEX \ "4b825dc642cb6eb9a060e54bf8d69288fbee4904" #define EMPTY_TREE_SHA1_BIN_LITERAL \ @@ -844,6 +885,7 @@ extern char *xdg_config_home(const char *filename); /* object replacement */ #define LOOKUP_REPLACE_OBJECT 1 +#define LOOKUP_UNKNOWN_OBJECT 2 extern void *read_sha1_file_extended(const unsigned char *sha1, enum object_type *type, unsigned long *size, unsigned flag); static inline void *read_sha1_file(const unsigned char *sha1, enum object_type *type, unsigned long *size) { @@ -901,8 +943,17 @@ extern int has_sha1_pack(const unsigned char *sha1); * Return true iff we have an object named sha1, whether local or in * an alternate object database, and whether packed or loose. This * function does not respect replace references. + * + * If the QUICK flag is set, do not re-check the pack directory + * when we cannot find the object (this means we may give a false + * negative answer if another process is simultaneously repacking). */ -extern int has_sha1_file(const unsigned char *sha1); +#define HAS_SHA1_QUICK 0x1 +extern int has_sha1_file_with_flags(const unsigned char *sha1, int flags); +static inline int has_sha1_file(const unsigned char *sha1) +{ + return has_sha1_file_with_flags(sha1, 0); +} /* * Return true iff an alternate object database has a loose object @@ -929,15 +980,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); @@ -959,8 +1016,10 @@ extern int for_each_abbrev(const char *prefix, each_abbrev_fn, void *); * null-terminated string. */ extern int get_sha1_hex(const char *hex, unsigned char *sha1); +extern int get_oid_hex(const char *hex, struct object_id *sha1); extern char *sha1_to_hex(const unsigned char *sha1); /* static buffer result! */ +extern char *oid_to_hex(const struct object_id *oid); /* same static buffer as sha1_to_hex */ extern int read_ref_full(const char *refname, int resolve_flags, unsigned char *sha1, int *flags); extern int read_ref(const char *refname, unsigned char *sha1); @@ -1314,6 +1373,7 @@ struct object_info { unsigned long *sizep; unsigned long *disk_sizep; unsigned char *delta_base_sha1; + struct strbuf *typename; /* Response */ enum { @@ -1502,9 +1562,13 @@ extern const char *git_mailmap_blob; extern void maybe_flush_or_die(FILE *, const char *); __attribute__((format (printf, 2, 3))) extern void fprintf_or_die(FILE *, const char *fmt, ...); + +#define COPY_READ_ERROR (-2) +#define COPY_WRITE_ERROR (-3) extern int copy_fd(int ifd, int ofd); extern int copy_file(const char *dst, const char *src, int mode); extern int copy_file_with_time(const char *dst, const char *src, int mode); + extern void write_or_die(int fd, const void *buf, size_t count); extern int write_or_whine(int fd, const void *buf, size_t count, const char *msg); extern int write_or_whine_pipe(int fd, const void *buf, size_t count, const char *msg); @@ -1518,6 +1582,8 @@ static inline ssize_t write_str_in_full(int fd, const char *str) { return write_in_full(fd, str, strlen(str)); } +__attribute__((format (printf, 3, 4))) +extern int write_file(const char *path, int fatal, const char *fmt, ...); /* pager.c */ extern void setup_pager(void); @@ -1641,5 +1707,6 @@ int stat_validity_check(struct stat_validity *sv, const char *path); void stat_validity_update(struct stat_validity *sv, int fd); int versioncmp(const char *s1, const char *s2); +void sleep_millisec(int millisec); #endif /* CACHE_H */ diff --git a/combine-diff.c b/combine-diff.c index d777e92aa0..30c7eb6d3c 100644 --- a/combine-diff.c +++ b/combine-diff.c @@ -44,9 +44,9 @@ static struct combine_diff_path *intersect_paths(struct combine_diff_path *curr, memset(p->parent, 0, sizeof(p->parent[0]) * num_parent); - hashcpy(p->sha1, q->queue[i]->two->sha1); + hashcpy(p->oid.hash, q->queue[i]->two->sha1); p->mode = q->queue[i]->two->mode; - hashcpy(p->parent[n].sha1, q->queue[i]->one->sha1); + hashcpy(p->parent[n].oid.hash, q->queue[i]->one->sha1); p->parent[n].mode = q->queue[i]->one->mode; p->parent[n].status = q->queue[i]->status; *tail = p; @@ -77,7 +77,7 @@ static struct combine_diff_path *intersect_paths(struct combine_diff_path *curr, continue; } - hashcpy(p->parent[n].sha1, q->queue[i]->one->sha1); + hashcpy(p->parent[n].oid.hash, q->queue[i]->one->sha1); p->parent[n].mode = q->queue[i]->one->mode; p->parent[n].status = q->queue[i]->status; @@ -284,7 +284,7 @@ static struct lline *coalesce_lines(struct lline *base, int *lenbase, return base; } -static char *grab_blob(const unsigned char *sha1, unsigned int mode, +static char *grab_blob(const struct object_id *oid, unsigned int mode, unsigned long *size, struct userdiff_driver *textconv, const char *path) { @@ -294,20 +294,20 @@ static char *grab_blob(const unsigned char *sha1, unsigned int mode, if (S_ISGITLINK(mode)) { blob = xmalloc(100); *size = snprintf(blob, 100, - "Subproject commit %s\n", sha1_to_hex(sha1)); - } else if (is_null_sha1(sha1)) { + "Subproject commit %s\n", oid_to_hex(oid)); + } else if (is_null_oid(oid)) { /* deleted blob */ *size = 0; return xcalloc(1, 1); } else if (textconv) { struct diff_filespec *df = alloc_filespec(path); - fill_filespec(df, sha1, 1, mode); + fill_filespec(df, oid->hash, 1, mode); *size = fill_textconv(textconv, df, &blob); free_filespec(df); } else { - blob = read_sha1_file(sha1, &type, size); + blob = read_sha1_file(oid->hash, &type, size); if (type != OBJ_BLOB) - die("object '%s' is not a blob!", sha1_to_hex(sha1)); + die("object '%s' is not a blob!", oid_to_hex(oid)); } return blob; } @@ -389,7 +389,7 @@ static void consume_line(void *state_, char *line, unsigned long len) } } -static void combine_diff(const unsigned char *parent, unsigned int mode, +static void combine_diff(const struct object_id *parent, unsigned int mode, mmfile_t *result_file, struct sline *sline, unsigned int cnt, int n, int num_parent, int result_deleted, @@ -897,7 +897,7 @@ static void show_combined_header(struct combine_diff_path *elem, int show_file_header) { struct diff_options *opt = &rev->diffopt; - int abbrev = DIFF_OPT_TST(opt, FULL_INDEX) ? 40 : DEFAULT_ABBREV; + int abbrev = DIFF_OPT_TST(opt, FULL_INDEX) ? GIT_SHA1_HEXSZ : DEFAULT_ABBREV; const char *a_prefix = opt->a_prefix ? opt->a_prefix : "a/"; const char *b_prefix = opt->b_prefix ? opt->b_prefix : "b/"; const char *c_meta = diff_get_color_opt(opt, DIFF_METAINFO); @@ -914,11 +914,11 @@ static void show_combined_header(struct combine_diff_path *elem, "", elem->path, line_prefix, c_meta, c_reset); printf("%s%sindex ", line_prefix, c_meta); for (i = 0; i < num_parent; i++) { - abb = find_unique_abbrev(elem->parent[i].sha1, + abb = find_unique_abbrev(elem->parent[i].oid.hash, abbrev); printf("%s%s", i ? "," : "", abb); } - abb = find_unique_abbrev(elem->sha1, abbrev); + abb = find_unique_abbrev(elem->oid.hash, abbrev); printf("..%s%s\n", abb, c_reset); if (mode_differs) { @@ -991,7 +991,7 @@ static void show_patch_diff(struct combine_diff_path *elem, int num_parent, /* Read the result of merge first */ if (!working_tree_file) - result = grab_blob(elem->sha1, elem->mode, &result_size, + result = grab_blob(&elem->oid, elem->mode, &result_size, textconv, elem->path); else { /* Used by diff-tree to read from the working tree */ @@ -1013,12 +1013,12 @@ static void show_patch_diff(struct combine_diff_path *elem, int num_parent, result = strbuf_detach(&buf, NULL); elem->mode = canon_mode(st.st_mode); } else if (S_ISDIR(st.st_mode)) { - unsigned char sha1[20]; - if (resolve_gitlink_ref(elem->path, "HEAD", sha1) < 0) - result = grab_blob(elem->sha1, elem->mode, + struct object_id oid; + if (resolve_gitlink_ref(elem->path, "HEAD", oid.hash) < 0) + result = grab_blob(&elem->oid, elem->mode, &result_size, NULL, NULL); else - result = grab_blob(sha1, elem->mode, + result = grab_blob(&oid, elem->mode, &result_size, NULL, NULL); } else if (textconv) { struct diff_filespec *df = alloc_filespec(elem->path); @@ -1090,7 +1090,7 @@ static void show_patch_diff(struct combine_diff_path *elem, int num_parent, for (i = 0; !is_binary && i < num_parent; i++) { char *buf; unsigned long size; - buf = grab_blob(elem->parent[i].sha1, + buf = grab_blob(&elem->parent[i].oid, elem->parent[i].mode, &size, NULL, NULL); if (buffer_is_binary(buf, size)) @@ -1139,14 +1139,14 @@ static void show_patch_diff(struct combine_diff_path *elem, int num_parent, for (i = 0; i < num_parent; i++) { int j; for (j = 0; j < i; j++) { - if (!hashcmp(elem->parent[i].sha1, - elem->parent[j].sha1)) { + if (!oidcmp(&elem->parent[i].oid, + &elem->parent[j].oid)) { reuse_combine_diff(sline, cnt, i, j); break; } } if (i <= j) - combine_diff(elem->parent[i].sha1, + combine_diff(&elem->parent[i].oid, elem->parent[i].mode, &result_file, sline, cnt, i, num_parent, result_deleted, @@ -1206,9 +1206,9 @@ static void show_raw_diff(struct combine_diff_path *p, int num_parent, struct re /* Show sha1's */ for (i = 0; i < num_parent; i++) - printf(" %s", diff_unique_abbrev(p->parent[i].sha1, + printf(" %s", diff_unique_abbrev(p->parent[i].oid.hash, opt->abbrev)); - printf(" %s ", diff_unique_abbrev(p->sha1, opt->abbrev)); + printf(" %s ", diff_unique_abbrev(p->oid.hash, opt->abbrev)); } if (opt->output_format & (DIFF_FORMAT_RAW | DIFF_FORMAT_NAME_STATUS)) { @@ -1271,16 +1271,16 @@ static struct diff_filepair *combined_pair(struct combine_diff_path *p, for (i = 0; i < num_parent; i++) { pair->one[i].path = p->path; pair->one[i].mode = p->parent[i].mode; - hashcpy(pair->one[i].sha1, p->parent[i].sha1); - pair->one[i].sha1_valid = !is_null_sha1(p->parent[i].sha1); + hashcpy(pair->one[i].sha1, p->parent[i].oid.hash); + pair->one[i].sha1_valid = !is_null_oid(&p->parent[i].oid); pair->one[i].has_more_entries = 1; } pair->one[num_parent - 1].has_more_entries = 0; pair->two->path = p->path; pair->two->mode = p->mode; - hashcpy(pair->two->sha1, p->sha1); - pair->two->sha1_valid = !is_null_sha1(p->sha1); + hashcpy(pair->two->sha1, p->oid.hash); + pair->two->sha1_valid = !is_null_oid(&p->oid); return pair; } diff --git a/command-list.txt b/command-list.txt index f1eae0810d..b17c011bfd 100644 --- a/command-list.txt +++ b/command-list.txt @@ -1,29 +1,39 @@ -# List of known git commands. -# command name category [deprecated] [common] -git-add mainporcelain common +# 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 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,42 +45,42 @@ 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-fast-export ancillarymanipulators +git-fast-import ancillarymanipulators +git-fetch mainporcelain remote git-fetch-pack synchingrepositories git-filter-branch ancillarymanipulators git-fmt-merge-msg purehelpers git-for-each-ref plumbinginterrogators git-format-patch mainporcelain -git-fsck ancillaryinterrogators +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 +git-help ancillaryinterrogators git-http-backend synchingrepositories 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 @@ -55,12 +55,12 @@ struct commit *lookup_commit(const unsigned char *sha1) struct commit *lookup_commit_reference_by_name(const char *name) { - unsigned char sha1[20]; + struct object_id oid; struct commit *commit; - if (get_sha1_committish(name, sha1)) + if (get_sha1_committish(name, oid.hash)) return NULL; - commit = lookup_commit_reference(sha1); + commit = lookup_commit_reference(oid.hash); if (parse_commit(commit)) return NULL; return commit; @@ -99,7 +99,7 @@ static int commit_graft_alloc, commit_graft_nr; static const unsigned char *commit_graft_sha1_access(size_t index, void *table) { struct commit_graft **commit_graft_table = table; - return commit_graft_table[index]->sha1; + return commit_graft_table[index]->oid.hash; } static int commit_graft_pos(const unsigned char *sha1) @@ -110,7 +110,7 @@ static int commit_graft_pos(const unsigned char *sha1) int register_commit_graft(struct commit_graft *graft, int ignore_dups) { - int pos = commit_graft_pos(graft->sha1); + int pos = commit_graft_pos(graft->oid.hash); if (0 <= pos) { if (ignore_dups) @@ -138,22 +138,23 @@ struct commit_graft *read_graft_line(char *buf, int len) /* The format is just "Commit Parent1 Parent2 ...\n" */ int i; struct commit_graft *graft = NULL; + const int entry_size = GIT_SHA1_HEXSZ + 1; while (len && isspace(buf[len-1])) buf[--len] = '\0'; if (buf[0] == '#' || buf[0] == '\0') return NULL; - if ((len + 1) % 41) + if ((len + 1) % entry_size) goto bad_graft_data; - i = (len + 1) / 41 - 1; - graft = xmalloc(sizeof(*graft) + 20 * i); + i = (len + 1) / entry_size - 1; + graft = xmalloc(sizeof(*graft) + GIT_SHA1_RAWSZ * i); graft->nr_parent = i; - if (get_sha1_hex(buf, graft->sha1)) + if (get_oid_hex(buf, &graft->oid)) goto bad_graft_data; - for (i = 40; i < len; i += 41) { + for (i = GIT_SHA1_HEXSZ; i < len; i += entry_size) { if (buf[i] != ' ') goto bad_graft_data; - if (get_sha1_hex(buf + i + 1, graft->parent[i/41])) + if (get_sha1_hex(buf + i + 1, graft->parent[i/entry_size].hash)) goto bad_graft_data; } return graft; @@ -302,39 +303,42 @@ int parse_commit_buffer(struct commit *item, const void *buffer, unsigned long s { const char *tail = buffer; const char *bufptr = buffer; - unsigned char parent[20]; + struct object_id parent; struct commit_list **pptr; struct commit_graft *graft; + const int tree_entry_len = GIT_SHA1_HEXSZ + 5; + const int parent_entry_len = GIT_SHA1_HEXSZ + 7; if (item->object.parsed) return 0; item->object.parsed = 1; tail += size; - if (tail <= bufptr + 46 || memcmp(bufptr, "tree ", 5) || bufptr[45] != '\n') + if (tail <= bufptr + tree_entry_len + 1 || memcmp(bufptr, "tree ", 5) || + bufptr[tree_entry_len] != '\n') return error("bogus commit object %s", sha1_to_hex(item->object.sha1)); - if (get_sha1_hex(bufptr + 5, parent) < 0) + if (get_sha1_hex(bufptr + 5, parent.hash) < 0) return error("bad tree pointer in commit %s", sha1_to_hex(item->object.sha1)); - item->tree = lookup_tree(parent); - bufptr += 46; /* "tree " + "hex sha1" + "\n" */ + item->tree = lookup_tree(parent.hash); + bufptr += tree_entry_len + 1; /* "tree " + "hex sha1" + "\n" */ pptr = &item->parents; graft = lookup_commit_graft(item->object.sha1); - while (bufptr + 48 < tail && !memcmp(bufptr, "parent ", 7)) { + while (bufptr + parent_entry_len < tail && !memcmp(bufptr, "parent ", 7)) { struct commit *new_parent; - if (tail <= bufptr + 48 || - get_sha1_hex(bufptr + 7, parent) || - bufptr[47] != '\n') + if (tail <= bufptr + parent_entry_len + 1 || + get_sha1_hex(bufptr + 7, parent.hash) || + bufptr[parent_entry_len] != '\n') return error("bad parents in commit %s", sha1_to_hex(item->object.sha1)); - bufptr += 48; + bufptr += parent_entry_len + 1; /* * The clone is shallow if nr_parent < 0, and we must * not traverse its real parents even when we unhide them. */ if (graft && (graft->nr_parent < 0 || grafts_replace_parents)) continue; - new_parent = lookup_commit(parent); + new_parent = lookup_commit(parent.hash); if (new_parent) pptr = &commit_list_insert(new_parent, pptr)->next; } @@ -342,7 +346,7 @@ int parse_commit_buffer(struct commit *item, const void *buffer, unsigned long s int i; struct commit *new_parent; for (i = 0; i < graft->nr_parent; i++) { - new_parent = lookup_commit(graft->parent[i]); + new_parent = lookup_commit(graft->parent[i].hash); if (!new_parent) continue; pptr = &commit_list_insert(new_parent, pptr)->next; @@ -1581,10 +1585,10 @@ struct commit *get_merge_parent(const char *name) { struct object *obj; struct commit *commit; - unsigned char sha1[20]; - if (get_sha1(name, sha1)) + struct object_id oid; + if (get_sha1(name, oid.hash)) return NULL; - obj = parse_object(sha1); + obj = parse_object(oid.hash); commit = (struct commit *)peel_to_type(name, 0, obj, OBJ_COMMIT); if (commit && !commit->util) { struct merge_remote_desc *desc; @@ -230,9 +230,9 @@ enum rev_sort_order { void sort_in_topological_order(struct commit_list **, enum rev_sort_order); struct commit_graft { - unsigned char sha1[20]; + struct object_id oid; int nr_parent; /* < 0 if shallow commit */ - unsigned char parent[FLEX_ARRAY][20]; /* more */ + struct object_id parent[FLEX_ARRAY]; /* more */ }; typedef int (*each_commit_graft_fn)(const struct commit_graft *, void *); 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 5e499cfb71..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 @@ -98,8 +106,6 @@ static inline unsigned int alarm(unsigned int seconds) { return 0; } static inline int fsync(int fd) { return _commit(fd); } -static inline pid_t getppid(void) -{ return 1; } static inline void sync(void) {} static inline uid_t getuid(void) @@ -121,6 +127,12 @@ static inline int sigaddset(sigset_t *set, int signum) #define SIG_UNBLOCK 0 static inline int sigprocmask(int how, const sigset_t *set, sigset_t *oldset) { return 0; } +static inline pid_t getppid(void) +{ return 1; } +static inline pid_t getpgid(pid_t pid) +{ return pid == 0 ? getpid() : pid; } +static inline pid_t tcgetpgrp(int fd) +{ return getpid(); } /* * simple adaptors @@ -171,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 @@ -50,7 +50,7 @@ static struct config_set the_config_set; static int config_file_fgetc(struct config_source *conf) { - return fgetc(conf->u.file); + return getc_unlocked(conf->u.file); } static int config_file_ungetc(int c, struct config_source *conf) @@ -1088,7 +1088,9 @@ int git_config_from_file(config_fn_t fn, const char *filename, void *data) f = fopen(filename, "r"); if (f) { + flockfile(f); ret = do_config_from_file(fn, filename, filename, f, data); + funlockfile(f); fclose(f); } return ret; diff --git a/config.mak.uname b/config.mak.uname index f4e77cb9e5..943c43965e 100644 --- a/config.mak.uname +++ b/config.mak.uname @@ -36,6 +36,7 @@ ifeq ($(uname_S),Linux) HAVE_DEV_TTY = YesPlease HAVE_CLOCK_GETTIME = YesPlease HAVE_CLOCK_MONOTONIC = YesPlease + HAVE_GETDELIM = YesPlease endif ifeq ($(uname_S),GNU/kFreeBSD) HAVE_ALLOCA_H = YesPlease @@ -101,6 +102,9 @@ ifeq ($(uname_S),Darwin) ifeq ($(shell expr "$(uname_R)" : '[15]\.'),2) NO_STRLCPY = YesPlease endif + ifeq ($(shell test "`expr "$(uname_R)" : '\([0-9][0-9]*\)\.'`" -ge 11 && echo 1),1) + HAVE_GETDELIM = YesPlease + endif NO_MEMMEM = YesPlease USE_ST_TIMESPEC = YesPlease HAVE_DEV_TTY = YesPlease diff --git a/configure.ac b/configure.ac index bbdde85c3d..14012fad72 100644 --- a/configure.ac +++ b/configure.ac @@ -1041,6 +1041,12 @@ GIT_CHECK_FUNC(initgroups, [NO_INITGROUPS=YesPlease]) GIT_CONF_SUBST([NO_INITGROUPS]) # +# Define HAVE_GETDELIM if you have getdelim in the C library. +GIT_CHECK_FUNC(getdelim, +[HAVE_GETDELIM=YesPlease], +[HAVE_GETDELIM=]) +GIT_CONF_SUBST([HAVE_GETDELIM]) +# # # Define NO_MMAP if you want to avoid mmap. # diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash index eae9dce590..c97c648d7e 100644 --- a/contrib/completion/git-completion.bash +++ b/contrib/completion/git-completion.bash @@ -665,8 +665,8 @@ __git_list_porcelain_commands () checkout-index) : plumbing;; commit-tree) : plumbing;; count-objects) : infrequent;; - credential-cache) : credentials helper;; - credential-store) : credentials helper;; + credential) : credentials;; + credential-*) : credentials helper;; cvsexportcommit) : export;; cvsimport) : import;; cvsserver) : daemon;; @@ -735,35 +735,29 @@ __git_list_porcelain_commands () __git_porcelain_commands= __git_compute_porcelain_commands () { - __git_compute_all_commands test -n "$__git_porcelain_commands" || __git_porcelain_commands=$(__git_list_porcelain_commands) } -__git_pretty_aliases () +# Lists all set config variables starting with the given section prefix, +# with the prefix removed. +__git_get_config_variables () { - local i IFS=$'\n' - for i in $(git --git-dir="$(__gitdir)" config --get-regexp "pretty\..*" 2>/dev/null); do - case "$i" in - pretty.*) - i="${i#pretty.}" - echo "${i/ */}" - ;; - esac + local section="$1" i IFS=$'\n' + for i in $(git --git-dir="$(__gitdir)" config --get-regexp "^$section\..*" 2>/dev/null); do + i="${i#$section.}" + echo "${i/ */}" done } +__git_pretty_aliases () +{ + __git_get_config_variables "pretty" +} + __git_aliases () { - local i IFS=$'\n' - for i in $(git --git-dir="$(__gitdir)" config --get-regexp "alias\..*" 2>/dev/null); do - case "$i" in - alias.*) - i="${i#alias.}" - echo "${i/ */}" - ;; - esac - done + __git_get_config_variables "alias" } # __git_aliased_command requires 1 argument @@ -1114,7 +1108,7 @@ _git_commit () case "$cur" in --cleanup=*) - __gitcomp "default strip verbatim whitespace + __gitcomp "default scissors strip verbatim whitespace " "" "${cur##--cleanup=}" return ;; @@ -2123,6 +2117,7 @@ _git_config () http.noEPSV http.postBuffer http.proxy + http.sslCipherList http.sslCAInfo http.sslCAPath http.sslCert @@ -2260,12 +2255,7 @@ _git_remote () __git_complete_remote_or_refspec ;; update) - local i c='' IFS=$'\n' - for i in $(git --git-dir="$(__gitdir)" config --get-regexp "remotes\..*" 2>/dev/null); do - i="${i#remotes.}" - c="$c ${i/ */}" - done - __gitcomp "$c" + __gitcomp "$(__git_get_config_variables "remotes")" ;; *) ;; @@ -2292,6 +2282,11 @@ _git_reset () _git_revert () { + local dir="$(__gitdir)" + if [ -f "$dir"/REVERT_HEAD ]; then + __gitcomp "--continue --quit --abort" + return + fi case "$cur" in --*) __gitcomp "--edit --mainline --no-edit --no-commit --signoff" diff --git a/contrib/completion/git-completion.tcsh b/contrib/completion/git-completion.tcsh index 6104a42a23..4a790d8f4e 100644 --- a/contrib/completion/git-completion.tcsh +++ b/contrib/completion/git-completion.tcsh @@ -41,7 +41,7 @@ if ( ! -e ${__git_tcsh_completion_original_script} ) then exit endif -cat << EOF > ${__git_tcsh_completion_script} +cat << EOF >! ${__git_tcsh_completion_script} #!bash # # This script is GENERATED and will be overwritten automatically. diff --git a/contrib/hooks/multimail/CHANGES b/contrib/hooks/multimail/CHANGES index 3603d56c26..0b823d8f5f 100644 --- a/contrib/hooks/multimail/CHANGES +++ b/contrib/hooks/multimail/CHANGES @@ -1,3 +1,51 @@ +Release 1.1.0 +============= + +* When a single commit is pushed, omit the reference changed email. + Set multimailhook.combineWhenSingleCommit to false to disable this + new feature. + +* In gitolite environments, the pusher's email address can be used as + the From address by creating a specially formatted comment block in + gitolite.conf (see multimailhook.from in README). + +* Support for SMTP authentication and SSL/TLS encryption was added, + see smtpUser, smtpPass, smtpEncryption in README. + +* A new option scanCommitForCc was added to allow git-multimail to + search the commit message for 'Cc: ...' lines, and add the + corresponding emails in Cc. + +* If $USER is not set, use the variable $USERNAME. This is needed on + Windows platform to recognize the pusher. + +* The emailPrefix variable can now be set to an empty string to remove + the prefix. + +* A short tutorial was added in doc/gitolite.rst to set up + git-multimail with gitolite. + +* The post-receive file was renamed to post-receive.example. It has + always been an example (the standard way to call git-multimail is to + call git_multimail.py), but it was unclear to many users. + +* A new refchangeShowGraph option was added to make it possible to + include both a graph and a log in the summary emails. The options + to control the graph formatting can be set via the new graphOpts + option. + +* New option --force-send was added to disable new commit detection + for update hook. One use-case is to run git_multimail.py after + running "git fetch" to send emails about commits that have just been + fetched (the detection of new commits was unreliable in this mode). + +* The testing infrastructure was considerably improved (continuous + integration with travis-ci, automatic check of PEP8 and RST syntax, + many improvements to the test scripts). + +This version has been tested with Python 2.4 to 2.7, and Git 1.7.1 to +2.4. + Release 1.0.0 ============= diff --git a/contrib/hooks/multimail/README b/contrib/hooks/multimail/README index 6efa4ffe17..3a33cb734a 100644 --- a/contrib/hooks/multimail/README +++ b/contrib/hooks/multimail/README @@ -1,5 +1,8 @@ - git-multimail - ============= +git-multimail Version 1.1.0 +=========================== + +.. image:: https://travis-ci.org/git-multimail/git-multimail.svg?branch=master + :target: https://travis-ci.org/git-multimail/git-multimail git-multimail is a tool for sending notification emails on pushes to a Git repository. It includes a Python module called git_multimail.py, @@ -38,17 +41,17 @@ By default, for each push received by the repository, git-multimail: list) makes it easy to scan through the emails, jump to patches that need further attention, and write comments about specific commits. Commits are handled in reverse topological order (i.e., - parents shown before children). For example, - - [git] branch master updated - + [git] 01/08: doc: fix xref link from api docs to manual pages - + [git] 02/08: api-credentials.txt: show the big picture first - + [git] 03/08: api-credentials.txt: mention credential.helper explicitly - + [git] 04/08: api-credentials.txt: add "see also" section - + [git] 05/08: t3510 (cherry-pick-sequence): add missing '&&' - + [git] 06/08: Merge branch 'rr/maint-t3510-cascade-fix' - + [git] 07/08: Merge branch 'mm/api-credentials-doc' - + [git] 08/08: Git 1.7.11-rc2 + parents shown before children). For example:: + + [git] branch master updated + + [git] 01/08: doc: fix xref link from api docs to manual pages + + [git] 02/08: api-credentials.txt: show the big picture first + + [git] 03/08: api-credentials.txt: mention credential.helper explicitly + + [git] 04/08: api-credentials.txt: add "see also" section + + [git] 05/08: t3510 (cherry-pick-sequence): add missing '&&' + + [git] 06/08: Merge branch 'rr/maint-t3510-cascade-fix' + + [git] 07/08: Merge branch 'mm/api-credentials-doc' + + [git] 08/08: Git 1.7.11-rc2 Each commit appears in exactly one commit email, the first time that it is pushed to the repository. If a commit is later merged @@ -74,19 +77,19 @@ Requirements 3.x. The example scripts invoke Python using the following shebang line - (following PEP 394 [1]): + (following PEP 394 [1]_):: #! /usr/bin/env python2 If your system's Python2 interpreter is not in your PATH or is not - called "python2", you can change the lines accordingly. Or you can + called ``python2``, you can change the lines accordingly. Or you can invoke the Python interpreter explicitly, for example via a tiny - shell script like + shell script like:: #! /bin/sh /usr/local/bin/python /path/to/git_multimail.py "$@" -* The "git" command must be in your PATH. git-multimail is known to +* The ``git`` command must be in your PATH. git-multimail is known to work with Git versions back to 1.7.1. (Earlier versions have not been tested; if you do so, please report your results.) @@ -101,7 +104,7 @@ Requirements Invocation ---------- -git_multimail.py is designed to be used as a "post-receive" hook in a +git_multimail.py is designed to be used as a ``post-receive`` hook in a Git repository (see githooks(5)). Link or copy it to $GIT_DIR/hooks/post-receive within the repository for which email notifications are desired. Usually it should be installed on the @@ -109,10 +112,10 @@ central repository for a project, to which all commits are eventually pushed. For use on pre-v1.5.1 Git servers, git_multimail.py can also work as -an "update" hook, taking its arguments on the command line. To use +an ``update`` hook, taking its arguments on the command line. To use this script in this manner, link or copy it to $GIT_DIR/hooks/update. Please note that the script is not completely reliable in this mode -[2]. +[2]_. Alternatively, git_multimail.py can be imported as a Python module into your own Python post-receive script. This method is a bit more @@ -129,7 +132,7 @@ arbitrary Python code. For example, you can use a custom environment only about changes affecting particular files or subdirectories) Or you can change how emails are sent by writing your own Mailer -class. The "post-receive" script in this directory demonstrates how +class. The ``post-receive`` script in this directory demonstrates how to use git_multimail.py as a Python module. (If you make interesting changes of this type, please consider sharing them with the community.) @@ -139,18 +142,26 @@ Configuration ------------- By default, git-multimail mostly takes its configuration from the -following "git config" settings: +following ``git config`` settings: multimailhook.environment This describes the general environment of the repository. Currently supported values: - "generic" -- the username of the pusher is read from $USER and the - repository name is derived from the repository's path. + * generic + + the username of the pusher is read from $USER or $USERNAME and + the repository name is derived from the repository's path. + + * gitolite - "gitolite" -- the username of the pusher is read from $GL_USER and - the repository name from $GL_REPO. + the username of the pusher is read from $GL_USER, the repository + name is read from $GL_REPO, and the From: header value is + optionally read from gitolite.conf (see multimailhook.from). + + For more information about gitolite and git-multimail, read + doc/gitolite.rst If neither of these environments is suitable for your setup, then you can implement a Python class that inherits from Environment @@ -160,8 +171,8 @@ multimailhook.environment The environment value can be specified on the command line using the --environment option. If it is not specified on the command line or by multimailhook.environment, then it defaults to - "gitolite" if the environment contains variables $GL_USER and - $GL_REPO; otherwise "generic". + ``gitolite`` if the environment contains variables $GL_USER and + $GL_REPO; otherwise ``generic``. multimailhook.repoName @@ -219,61 +230,109 @@ multimailhook.announceShortlog not so straightforward, then the shortlog might be confusing rather than useful. Default is false. +multimailhook.refchangeShowGraph + + If this option is set to true, then summary emails about reference + changes will additionally include: + + * a graph of the added commits (if any) + + * a graph of the discarded commits (if any) + + The log is generated by running ``git log --graph`` with the options + specified in graphOpts. The default is false. + multimailhook.refchangeShowLog If this option is set to true, then summary emails about reference changes will include a detailed log of the added commits in addition to the one line summary. The log is generated by running - "git log" with the options specified in multimailhook.logOpts. + ``git log`` with the options specified in multimailhook.logOpts. Default is false. multimailhook.mailer This option changes the way emails are sent. Accepted values are: - - sendmail (the default): use the command /usr/sbin/sendmail or - /usr/lib/sendmail (or sendmailCommand, if configured). This + - sendmail (the default): use the command ``/usr/sbin/sendmail`` or + ``/usr/lib/sendmail`` (or sendmailCommand, if configured). This mode can be further customized via the following options: - multimailhook.sendmailCommand + * multimailhook.sendmailCommand - The command used by mailer "sendmail" to send emails. Shell - quoting is allowed in the value of this setting, but remember that - Git requires double-quotes to be escaped; e.g., + The command used by mailer ``sendmail`` to send emails. Shell + quoting is allowed in the value of this setting, but remember that + Git requires double-quotes to be escaped; e.g.:: git config multimailhook.sendmailcommand '/usr/sbin/sendmail -oi -t -F \"Git Repo\"' - Default is '/usr/sbin/sendmail -oi -t' or - '/usr/lib/sendmail -oi -t' (depending on which file is - present and executable). + Default is '/usr/sbin/sendmail -oi -t' or + '/usr/lib/sendmail -oi -t' (depending on which file is + present and executable). - multimailhook.envelopeSender + * multimailhook.envelopeSender - If set then pass this value to sendmail via the -f option to set - the envelope sender address. + If set then pass this value to sendmail via the -f option to set + the envelope sender address. - smtp: use Python's smtplib. This is useful when the sendmail command is not available on the system. This mode can be further customized via the following options: - multimailhook.smtpServer + * multimailhook.smtpServer + + The name of the SMTP server to connect to. The value can + also include a colon and a port number; e.g., + ``mail.example.com:25``. Default is 'localhost' using port 25. + + * multimailhook.smtpUser + * multimailhook.smtpPass + + Server username and password. Required if smtpEncryption is 'ssl'. + Note that the username and password currently need to be + set cleartext in the configuration file, which is not + recommended. If you need to use this option, be sure your + configuration file is read-only. + + * multimailhook.envelopeSender + + The sender address to be passed to the SMTP server. If + unset, then the value of multimailhook.from is used. + + * multimailhook.smtpServerTimeout + + Timeout in seconds. - The name of the SMTP server to connect to. The value can - also include a colon and a port number; e.g., - "mail.example.com:25". Default is 'localhost' using port - 25. + * multimailhook.smtpEncryption - multimailhook.envelopeSender + Set the security type. Allowed values: none, ssl. + Default=none. - The sender address to be passed to the SMTP server. If - unset, then the value of multimailhook.from is used. + * multimailhook.smtpServerDebugLevel + + Integer number. Set to greater than 0 to activate debugging. multimailhook.from - If set then use this value in the From: field of generated emails. - If unset, then use the repository's user configuration (user.name - and user.email). If user.email is also unset, then use - multimailhook.envelopeSender. + If set, use this value in the From: field of generated emails. If + unset, the value of the From: header is determined as follows: + + 1. (gitolite environment only) Parse gitolite.conf, looking for a + block of comments that looks like this:: + + # BEGIN USER EMAILS + # username Firstname Lastname <email@example.com> + # END USER EMAILS + + If that block exists, and there is a line between the BEGIN + USER EMAILS and END USER EMAILS lines where the first field + matches the gitolite username ($GL_USER), use the rest of the + line for the From: header. + + 2. If the user.email configuration setting is set, use its value + (and the value of user.name, if set). + + 3. Use the value of multimailhook.envelopeSender. multimailhook.administrator @@ -287,7 +346,8 @@ multimailhook.emailPrefix All emails have this string prepended to their subjects, to aid email filtering (though filtering based on the X-Git-* email headers is probably more robust). Default is the short name of - the repository in square brackets; e.g., "[myrepo]". + the repository in square brackets; e.g., ``[myrepo]``. Set this + value to the empty string to suppress the email prefix. multimailhook.emailMaxLines @@ -299,7 +359,7 @@ multimailhook.emailMaxLines multimailhook.emailMaxLineLength The maximum length of a line in the email body. Lines longer than - this limit are truncated to this length with a trailing " [...]" + this limit are truncated to this length with a trailing `` [...]`` added to indicate the missing text. The default is 500, because (a) diffs with longer lines are probably from binary files, for which a diff is useless, and (b) even if a text file has such long @@ -316,54 +376,62 @@ multimailhook.maxCommitEmails multimailhook.emailStrictUTF8 - If this boolean option is set to "true", then the main part of the + If this boolean option is set to `true`, then the main part of the email body is forced to be valid UTF-8. Any characters that are not valid UTF-8 are converted to the Unicode replacement - character, U+FFFD. The default is "true". + character, U+FFFD. The default is `true`. multimailhook.diffOpts - Options passed to "git diff-tree" when generating the summary - information for ReferenceChange emails. Default is "--stat - --summary --find-copies-harder". Add -p to those options to + Options passed to ``git diff-tree`` when generating the summary + information for ReferenceChange emails. Default is ``--stat + --summary --find-copies-harder``. Add -p to those options to include a unified diff of changes in addition to the usual summary output. Shell quoting is allowed; see multimailhook.logOpts for details. +multimailhook.graphOpts + + Options passed to ``git log --graph`` when generating graphs for the + reference change summary emails (used only if refchangeShowGraph + is true). The default is '--oneline --decorate'. + + Shell quoting is allowed; see logOpts for details. + multimailhook.logOpts - Options passed to "git log" to generate additional info for + Options passed to ``git log`` to generate additional info for reference change emails (used only if refchangeShowLog is set). - For example, adding --graph will show the graph of revisions, -p - will show the complete diff, etc. The default is empty. + For example, adding -p will show each commit's complete diff. The + default is empty. Shell quoting is allowed; for example, a log format that contains - spaces can be specified using something like: + spaces can be specified using something like:: git config multimailhook.logopts '--pretty=format:"%h %aN <%aE>%n%s%n%n%b%n"' If you want to set this by editing your configuration file directly, remember that Git requires double-quotes to be escaped - (see git-config(1) for more information): + (see git-config(1) for more information):: [multimailhook] logopts = --pretty=format:\"%h %aN <%aE>%n%s%n%n%b%n\" multimailhook.commitLogOpts - Options passed to "git log" to generate additional info for + Options passed to ``git log`` to generate additional info for revision change emails. For example, adding --ignore-all-spaces - will suppress whitespace changes. The default options are "-C - --stat -p --cc". Shell quoting is allowed; see + will suppress whitespace changes. The default options are ``-C + --stat -p --cc``. Shell quoting is allowed; see multimailhook.logOpts for details. multimailhook.emailDomain Domain name appended to the username of the person doing the push - to convert it into an email address (via "%s@%s" % (username, - emaildomain)). More complicated schemes can be implemented by - overriding Environment and overriding its get_pusher_email() - method. + to convert it into an email address + (via ``"%s@%s" % (username, emaildomain)``). More complicated + schemes can be implemented by overriding Environment and + overriding its get_pusher_email() method. multimailhook.replyTo multimailhook.replyToCommit @@ -377,26 +445,48 @@ multimailhook.replyToRefchange - An email address, which will be used directly. - - The value "pusher", in which case the pusher's address (if + - The value `pusher`, in which case the pusher's address (if available) will be used. This is the default for refchange emails. - - The value "author" (meaningful only for replyToCommit), in which + - The value `author` (meaningful only for replyToCommit), in which case the commit author's address will be used. This is the default for commit emails. - - The value "none", in which case the Reply-To: field will be + - The value `none`, in which case the Reply-To: field will be omitted. +multimailhook.quiet + + Do not output the list of email recipients from the hook + +multimailhook.stdout + + For debugging, send emails to stdout rather than to the + mailer. Equivalent to the --stdout command line option + +multimailhook.scanCommitForCc + + If this option is set to true, than recipients from lines in commit body + that starts with ``CC:`` will be added to CC list. + Default: false + +multimailhook.combineWhenSingleCommit + + If this option is set to true and a single new commit is pushed to + a branch, combine the summary and commit email messages into a + single email. + Default: true + Email filtering aids -------------------- All emails include extra headers to enable fine tuned filtering and give information for debugging. All emails include the headers -"X-Git-Host", "X-Git-Repo", "X-Git-Refname", and "X-Git-Reftype". -ReferenceChange emails also include headers "X-Git-Oldrev" and "X-Git-Newrev"; -Revision emails also include header "X-Git-Rev". +``X-Git-Host``, ``X-Git-Repo``, ``X-Git-Refname``, and ``X-Git-Reftype``. +ReferenceChange emails also include headers ``X-Git-Oldrev`` and ``X-Git-Newrev``; +Revision emails also include header ``X-Git-Rev``. Customizing email contents @@ -420,16 +510,17 @@ environment are built in: * GenericEnvironment: a stand-alone Git repository. * GitoliteEnvironment: a Git repository that is managed by gitolite - [3]. For such repositories, the identity of the pusher is read from - environment variable $GL_USER, and the name of the repository is - read from $GL_REPO (if it is not overridden by - multimailhook.reponame). + [3]_. For such repositories, the identity of the pusher is read from + environment variable $GL_USER, the name of the repository is read + from $GL_REPO (if it is not overridden by multimailhook.reponame), + and the From: header value is optionally read from gitolite.conf + (see multimailhook.from). By default, git-multimail assumes GitoliteEnvironment if $GL_USER and $GL_REPO are set, and otherwise assumes GenericEnvironment. Alternatively, you can choose one of these two environments explicitly -by setting a "multimailhook.environment" config setting (which can -have the value "generic" or "gitolite") or by passing an --environment +by setting a ``multimailhook.environment`` config setting (which can +have the value `generic` or `gitolite`) or by passing an --environment option to the script. If you need to customize the script in ways that are not supported by @@ -439,8 +530,8 @@ git_multimail.py as a Python module, as demonstrated by the example post-receive script. Then implement your environment class; it should usually inherit from one of the existing Environment classes and possibly one or more of the EnvironmentMixin classes. Then set the -"environment" variable to an instance of your own environment class -and pass it to run_as_post_receive_hook(). +``environment`` variable to an instance of your own environment class +and pass it to ``run_as_post_receive_hook()``. The standard environment classes, GenericEnvironment and GitoliteEnvironment, are in fact themselves put together out of a @@ -490,12 +581,14 @@ don't overlook them. Footnotes --------- -[1] http://www.python.org/dev/peps/pep-0394/ +.. [1] http://www.python.org/dev/peps/pep-0394/ -[2] Because of the way information is passed to update hooks, the - script's method of determining whether a commit has already been - seen does not work when it is used as an "update" script. In - particular, no notification email will be generated for a new - commit that is added to multiple references in the same push. +.. [2] Because of the way information is passed to update hooks, the + script's method of determining whether a commit has already + been seen does not work when it is used as an ``update`` script. + In particular, no notification email will be generated for a + new commit that is added to multiple references in the same + push. A workaround is to use --force-send to force sending the + emails. -[3] https://github.com/sitaramc/gitolite +.. [3] https://github.com/sitaramc/gitolite diff --git a/contrib/hooks/multimail/README.Git b/contrib/hooks/multimail/README.Git index ab3ece5221..449d36f156 100644 --- a/contrib/hooks/multimail/README.Git +++ b/contrib/hooks/multimail/README.Git @@ -6,10 +6,10 @@ website: https://github.com/git-multimail/git-multimail The version in this directory was obtained from the upstream project -on 2015-04-27 and consists of the "git-multimail" subdirectory from +on Jun 18 2015 and consists of the "git-multimail" subdirectory from revision - 8c3aaafa873bf10de8dddf1d202c449b3eff3b42 refs/tags/1.0.2 + 1f0dbb3b60035767889b913df16d9231ecdb8709 refs/tags/1.1.0 Please see the README file in this directory for information about how to report bugs or contribute to git-multimail. diff --git a/contrib/hooks/multimail/git_multimail.py b/contrib/hooks/multimail/git_multimail.py index 8b58ed6444..7cb2b36cb4 100755 --- a/contrib/hooks/multimail/git_multimail.py +++ b/contrib/hooks/multimail/git_multimail.py @@ -1,5 +1,6 @@ #! /usr/bin/env python2 +# Copyright (c) 2015 Matthieu Moy and others # Copyright (c) 2012-2014 Michael Haggerty and others # Derived from contrib/hooks/post-receive-email, which is # Copyright (c) 2007 Andy Parkins @@ -99,6 +100,10 @@ REF_DELETED_SUBJECT_TEMPLATE = ( ' (was %(oldrev_short)s)' ) +COMBINED_REFCHANGE_REVISION_SUBJECT_TEMPLATE = ( + '%(emailprefix)s%(refname_type)s %(short_refname)s updated: %(oneline)s' + ) + REFCHANGE_HEADER_TEMPLATE = """\ Date: %(send_date)s To: %(recipients)s @@ -230,6 +235,7 @@ how to provide full information about this reference change. REVISION_HEADER_TEMPLATE = """\ Date: %(send_date)s To: %(recipients)s +Cc: %(cc_recipients)s Subject: %(emailprefix)s%(num)02d/%(tot)02d: %(oneline)s MIME-Version: 1.0 Content-Type: text/plain; charset=%(charset)s @@ -258,6 +264,38 @@ in repository %(repo_shortname)s. REVISION_FOOTER_TEMPLATE = FOOTER_TEMPLATE +# Combined, meaning refchange+revision email (for single-commit additions) +COMBINED_HEADER_TEMPLATE = """\ +Date: %(send_date)s +To: %(recipients)s +Subject: %(subject)s +MIME-Version: 1.0 +Content-Type: text/plain; charset=%(charset)s +Content-Transfer-Encoding: 8bit +Message-ID: %(msgid)s +From: %(fromaddr)s +Reply-To: %(reply_to)s +X-Git-Host: %(fqdn)s +X-Git-Repo: %(repo_shortname)s +X-Git-Refname: %(refname)s +X-Git-Reftype: %(refname_type)s +X-Git-Oldrev: %(oldrev)s +X-Git-Newrev: %(newrev)s +X-Git-Rev: %(rev)s +Auto-Submitted: auto-generated +""" + +COMBINED_INTRO_TEMPLATE = """\ +This is an automated email from the git hooks/post-receive script. + +%(pusher)s pushed a commit to %(refname_type)s %(short_refname)s +in repository %(repo_shortname)s. + +""" + +COMBINED_FOOTER_TEMPLATE = FOOTER_TEMPLATE + + class CommandError(Exception): def __init__(self, cmd, retcode): self.cmd = cmd @@ -336,6 +374,47 @@ def read_git_lines(args, keepends=False, **kw): return read_git_output(args, keepends=True, **kw).splitlines(keepends) +def git_rev_list_ish(cmd, spec, args=None, **kw): + """Common functionality for invoking a 'git rev-list'-like command. + + Parameters: + * cmd is the Git command to run, e.g., 'rev-list' or 'log'. + * spec is a list of revision arguments to pass to the named + command. If None, this function returns an empty list. + * args is a list of extra arguments passed to the named command. + * All other keyword arguments (if any) are passed to the + underlying read_git_lines() function. + + Return the output of the Git command in the form of a list, one + entry per output line. + """ + if spec is None: + return [] + if args is None: + args = [] + args = [cmd, '--stdin'] + args + spec_stdin = ''.join(s + '\n' for s in spec) + return read_git_lines(args, input=spec_stdin, **kw) + + +def git_rev_list(spec, **kw): + """Run 'git rev-list' with the given list of revision arguments. + + See git_rev_list_ish() for parameter and return value + documentation. + """ + return git_rev_list_ish('rev-list', spec, **kw) + + +def git_log(spec, **kw): + """Run 'git log' with the given list of revision arguments. + + See git_rev_list_ish() for parameter and return value + documentation. + """ + return git_rev_list_ish('log', spec, **kw) + + def header_encode(text, header_name=None): """Encode and line-wrap the value of an email header field.""" @@ -388,9 +467,9 @@ class Config(object): def get(self, name, default=None): try: values = self._split(read_git_output( - ['config', '--get', '--null', '%s.%s' % (self.section, name)], - env=self.env, keepends=True, - )) + ['config', '--get', '--null', '%s.%s' % (self.section, name)], + env=self.env, keepends=True, + )) assert len(values) == 1 return values[0] except CommandError: @@ -449,9 +528,14 @@ class Config(object): env=self.env, ) - def has_key(self, name): + def __contains__(self, name): return self.get_all(name, default=None) is not None + # We don't use this method anymore internally, but keep it here in + # case somebody is calling it from their own code: + def has_key(self, name): + return name in self + def unset_all(self, name): try: read_git_output( @@ -579,7 +663,7 @@ class Change(object): self._values = None def _compute_values(self): - """Return a dictionary {keyword : expansion} for this Change. + """Return a dictionary {keyword: expansion} for this Change. Derived classes overload this method to add more entries to the return value. This method is used internally by @@ -589,7 +673,7 @@ class Change(object): return self.environment.get_values() def get_values(self, **extra_values): - """Return a dictionary {keyword : expansion} for this Change. + """Return a dictionary {keyword: expansion} for this Change. Return a dictionary mapping keywords to the values that they should be expanded to for this Change (used when interpolating @@ -636,7 +720,7 @@ class Change(object): value = value % values except KeyError, e: if DEBUG: - sys.stderr.write( + self.environment.log_warning( 'Warning: unknown variable %r in the following line; line skipped:\n' ' %s\n' % (e.args[0], line,) @@ -711,6 +795,8 @@ class Change(object): class Revision(Change): """A Change consisting of a single git commit.""" + CC_RE = re.compile(r'^\s*C[Cc]:\s*(?P<to>[^#]+@[^\s#]*)\s*(#.*)?$') + def __init__(self, reference_change, rev, num, tot): Change.__init__(self, reference_change.environment) self.reference_change = reference_change @@ -722,6 +808,24 @@ class Revision(Change): self.author = read_git_output(['log', '--no-walk', '--format=%aN <%aE>', self.rev.sha1]) self.recipients = self.environment.get_revision_recipients(self) + self.cc_recipients = '' + if self.environment.get_scancommitforcc(): + self.cc_recipients = ', '.join(to.strip() for to in self._cc_recipients()) + if self.cc_recipients: + self.environment.log_msg( + 'Add %s to CC for %s\n' % (self.cc_recipients, self.rev.sha1)) + + def _cc_recipients(self): + cc_recipients = [] + message = read_git_output(['log', '--no-walk', '--format=%b', self.rev.sha1]) + lines = message.strip().split('\n') + for line in lines: + m = re.match(self.CC_RE, line) + if m: + cc_recipients.append(m.group('to')) + + return cc_recipients + def _compute_values(self): values = Change._compute_values(self) @@ -739,6 +843,8 @@ class Revision(Change): values['num'] = self.num values['tot'] = self.tot values['recipients'] = self.recipients + if self.cc_recipients: + values['cc_recipients'] = self.cc_recipients values['oneline'] = oneline values['author'] = self.author @@ -750,8 +856,8 @@ class Revision(Change): def generate_email_header(self, **extra_values): for line in self.expand_header_lines( - REVISION_HEADER_TEMPLATE, **extra_values - ): + REVISION_HEADER_TEMPLATE, **extra_values + ): yield line def generate_email_intro(self): @@ -822,26 +928,26 @@ class ReferenceChange(Change): klass = BranchChange elif area == 'remotes': # Tracking branch: - sys.stderr.write( + environment.log_warning( '*** Push-update of tracking branch %r\n' '*** - incomplete email generated.\n' - % (refname,) + % (refname,) ) klass = OtherReferenceChange else: # Some other reference namespace: - sys.stderr.write( + environment.log_warning( '*** Push-update of strange reference %r\n' '*** - incomplete email generated.\n' - % (refname,) + % (refname,) ) klass = OtherReferenceChange else: # Anything else (is there anything else?) - sys.stderr.write( + environment.log_warning( '*** Unknown type of update to %r (%s)\n' '*** - incomplete email generated.\n' - % (refname, rev.type,) + % (refname, rev.type,) ) klass = OtherReferenceChange @@ -854,9 +960,9 @@ class ReferenceChange(Change): def __init__(self, environment, refname, short_refname, old, new, rev): Change.__init__(self, environment) self.change_type = { - (False, True) : 'create', - (True, True) : 'update', - (True, False) : 'delete', + (False, True): 'create', + (True, True): 'update', + (True, False): 'delete', }[bool(old), bool(new)] self.refname = refname self.short_refname = short_refname @@ -865,10 +971,16 @@ class ReferenceChange(Change): self.rev = rev self.msgid = make_msgid() self.diffopts = environment.diffopts + self.graphopts = environment.graphopts self.logopts = environment.logopts self.commitlogopts = environment.commitlogopts + self.showgraph = environment.refchange_showgraph self.showlog = environment.refchange_showlog + self.header_template = REFCHANGE_HEADER_TEMPLATE + self.intro_template = REFCHANGE_INTRO_TEMPLATE + self.footer_template = FOOTER_TEMPLATE + def _compute_values(self): values = Change._compute_values(self) @@ -894,11 +1006,39 @@ class ReferenceChange(Change): return values + def send_single_combined_email(self, known_added_sha1s): + """Determine if a combined refchange/revision email should be sent + + If there is only a single new (non-merge) commit added by a + change, it is useful to combine the ReferenceChange and + Revision emails into one. In such a case, return the single + revision; otherwise, return None. + + This method is overridden in BranchChange.""" + + return None + + def generate_combined_email(self, push, revision, body_filter=None, extra_header_values={}): + """Generate an email describing this change AND specified revision. + + Iterate over the lines (including the header lines) of an + email describing this change. If body_filter is not None, + then use it to filter the lines that are intended for the + email body. + + The extra_header_values field is received as a dict and not as + **kwargs, to allow passing other keyword arguments in the + future (e.g. passing extra values to generate_email_intro() + + This method is overridden in BranchChange.""" + + raise NotImplementedError + def get_subject(self): template = { - 'create' : REF_CREATED_SUBJECT_TEMPLATE, - 'update' : REF_UPDATED_SUBJECT_TEMPLATE, - 'delete' : REF_DELETED_SUBJECT_TEMPLATE, + 'create': REF_CREATED_SUBJECT_TEMPLATE, + 'update': REF_UPDATED_SUBJECT_TEMPLATE, + 'delete': REF_DELETED_SUBJECT_TEMPLATE, }[self.change_type] return self.expand(template) @@ -907,12 +1047,12 @@ class ReferenceChange(Change): extra_values['subject'] = self.get_subject() for line in self.expand_header_lines( - REFCHANGE_HEADER_TEMPLATE, **extra_values - ): + self.header_template, **extra_values + ): yield line def generate_email_intro(self): - for line in self.expand_lines(REFCHANGE_INTRO_TEMPLATE): + for line in self.expand_lines(self.intro_template): yield line def generate_email_body(self, push): @@ -922,9 +1062,9 @@ class ReferenceChange(Change): generate_update_summary() / generate_delete_summary().""" change_summary = { - 'create' : self.generate_create_summary, - 'delete' : self.generate_delete_summary, - 'update' : self.generate_update_summary, + 'create': self.generate_create_summary, + 'delete': self.generate_delete_summary, + 'update': self.generate_update_summary, }[self.change_type](push) for line in change_summary: yield line @@ -933,7 +1073,23 @@ class ReferenceChange(Change): yield line def generate_email_footer(self): - return self.expand_lines(FOOTER_TEMPLATE) + return self.expand_lines(self.footer_template) + + def generate_revision_change_graph(self, push): + if self.showgraph: + args = ['--graph'] + self.graphopts + for newold in ('new', 'old'): + has_newold = False + spec = push.get_commits_spec(newold, self) + for line in git_log(spec, args=args, keepends=True): + if not has_newold: + has_newold = True + yield '\n' + yield 'Graph of %s commits:\n\n' % ( + {'new': 'new', 'old': 'discarded'}[newold],) + yield ' ' + line + if has_newold: + yield '\n' def generate_revision_change_log(self, new_commits_list): if self.showlog: @@ -945,9 +1101,17 @@ class ReferenceChange(Change): + new_commits_list + ['--'], keepends=True, - ): + ): yield line + def generate_new_revision_summary(self, tot, new_commits_list, push): + for line in self.expand_lines(NEW_REVISIONS_TEMPLATE, tot=tot): + yield line + for line in self.generate_revision_change_graph(push): + yield line + for line in self.generate_revision_change_log(new_commits_list): + yield line + def generate_revision_change_summary(self, push): """Generate a summary of the revisions added/removed by this change.""" @@ -960,7 +1124,7 @@ class ReferenceChange(Change): sha1s.reverse() tot = len(sha1s) new_revisions = [ - Revision(self, GitObject(sha1), num=i+1, tot=tot) + Revision(self, GitObject(sha1), num=i + 1, tot=tot) for (i, sha1) in enumerate(sha1s) ] @@ -973,9 +1137,8 @@ class ReferenceChange(Change): BRIEF_SUMMARY_TEMPLATE, action='new', text=subject, ) yield '\n' - for line in self.expand_lines(NEW_REVISIONS_TEMPLATE, tot=tot): - yield line - for line in self.generate_revision_change_log([r.rev.sha1 for r in new_revisions]): + for line in self.generate_new_revision_summary( + tot, [r.rev.sha1 for r in new_revisions], push): yield line else: for line in self.expand_lines(NO_NEW_REVISIONS_TEMPLATE): @@ -993,16 +1156,16 @@ class ReferenceChange(Change): # revisions in the summary even though we will not send # new notification emails for them. adds = list(generate_summaries( - '--topo-order', '--reverse', '%s..%s' - % (self.old.commit_sha1, self.new.commit_sha1,) - )) + '--topo-order', '--reverse', '%s..%s' + % (self.old.commit_sha1, self.new.commit_sha1,) + )) # List of the revisions that were removed from the branch # by this update. This will be empty except for # non-fast-forward updates. discards = list(generate_summaries( - '%s..%s' % (self.new.commit_sha1, self.old.commit_sha1,) - )) + '%s..%s' % (self.new.commit_sha1, self.old.commit_sha1,) + )) if adds: new_commits_list = push.get_new_commits(self) @@ -1071,13 +1234,14 @@ class ReferenceChange(Change): yield '\n' if new_commits: - for line in self.expand_lines(NEW_REVISIONS_TEMPLATE, tot=len(new_commits)): - yield line - for line in self.generate_revision_change_log(new_commits_list): + for line in self.generate_new_revision_summary( + len(new_commits), new_commits_list, push): yield line else: for line in self.expand_lines(NO_NEW_REVISIONS_TEMPLATE): yield line + for line in self.generate_revision_change_graph(push): + yield line # The diffstat is shown from the old revision to the new # revision. This is to show the truth of what happened in @@ -1089,11 +1253,11 @@ class ReferenceChange(Change): yield '\n' yield 'Summary of changes:\n' for line in read_git_lines( - ['diff-tree'] - + self.diffopts - + ['%s..%s' % (self.old.commit_sha1, self.new.commit_sha1,)], - keepends=True, - ): + ['diff-tree'] + + self.diffopts + + ['%s..%s' % (self.old.commit_sha1, self.new.commit_sha1,)], + keepends=True, + ): yield line elif self.old.commit_sha1 and not self.new.commit_sha1: @@ -1103,7 +1267,7 @@ class ReferenceChange(Change): sha1s = list(push.get_discarded_commits(self)) tot = len(sha1s) discarded_revisions = [ - Revision(self, GitObject(sha1), num=i+1, tot=tot) + Revision(self, GitObject(sha1), num=i + 1, tot=tot) for (i, sha1) in enumerate(sha1s) ] @@ -1116,6 +1280,8 @@ class ReferenceChange(Change): yield r.expand( BRIEF_SUMMARY_TEMPLATE, action='discards', text=subject, ) + for line in self.generate_revision_change_graph(push): + yield line else: for line in self.expand_lines(NO_DISCARDED_REVISIONS_TEMPLATE): yield line @@ -1161,6 +1327,150 @@ class BranchChange(ReferenceChange): old=old, new=new, rev=rev, ) self.recipients = environment.get_refchange_recipients(self) + self._single_revision = None + + def send_single_combined_email(self, known_added_sha1s): + if not self.environment.combine_when_single_commit: + return None + + # In the sadly-all-too-frequent usecase of people pushing only + # one of their commits at a time to a repository, users feel + # the reference change summary emails are noise rather than + # important signal. This is because, in this particular + # usecase, there is a reference change summary email for each + # new commit, and all these summaries do is point out that + # there is one new commit (which can readily be inferred by + # the existence of the individual revision email that is also + # sent). In such cases, our users prefer there to be a combined + # reference change summary/new revision email. + # + # So, if the change is an update and it doesn't discard any + # commits, and it adds exactly one non-merge commit (gerrit + # forces a workflow where every commit is individually merged + # and the git-multimail hook fired off for just this one + # change), then we send a combined refchange/revision email. + try: + # If this change is a reference update that doesn't discard + # any commits... + if self.change_type != 'update': + return None + + if read_git_lines( + ['merge-base', self.old.sha1, self.new.sha1] + ) != [self.old.sha1]: + return None + + # Check if this update introduced exactly one non-merge + # commit: + + def split_line(line): + """Split line into (sha1, [parent,...]).""" + + words = line.split() + return (words[0], words[1:]) + + # Get the new commits introduced by the push as a list of + # (sha1, [parent,...]) + new_commits = [ + split_line(line) + for line in read_git_lines( + [ + 'log', '-3', '--format=%H %P', + '%s..%s' % (self.old.sha1, self.new.sha1), + ] + ) + ] + + if not new_commits: + return None + + # If the newest commit is a merge, save it for a later check + # but otherwise ignore it + merge = None + tot = len(new_commits) + if len(new_commits[0][1]) > 1: + merge = new_commits[0][0] + del new_commits[0] + + # Our primary check: we can't combine if more than one commit + # is introduced. We also currently only combine if the new + # commit is a non-merge commit, though it may make sense to + # combine if it is a merge as well. + if not ( + len(new_commits) == 1 + and len(new_commits[0][1]) == 1 + and new_commits[0][0] in known_added_sha1s + ): + return None + + # We do not want to combine revision and refchange emails if + # those go to separate locations. + rev = Revision(self, GitObject(new_commits[0][0]), 1, tot) + if rev.recipients != self.recipients: + return None + + # We ignored the newest commit if it was just a merge of the one + # commit being introduced. But we don't want to ignore that + # merge commit it it involved conflict resolutions. Check that. + if merge and merge != read_git_output(['diff-tree', '--cc', merge]): + return None + + # We can combine the refchange and one new revision emails + # into one. Return the Revision that a combined email should + # be sent about. + return rev + except CommandError: + # Cannot determine number of commits in old..new or new..old; + # don't combine reference/revision emails: + return None + + def generate_combined_email(self, push, revision, body_filter=None, extra_header_values={}): + values = revision.get_values() + if extra_header_values: + values.update(extra_header_values) + if 'subject' not in extra_header_values: + values['subject'] = self.expand(COMBINED_REFCHANGE_REVISION_SUBJECT_TEMPLATE, **values) + + self._single_revision = revision + self.header_template = COMBINED_HEADER_TEMPLATE + self.intro_template = COMBINED_INTRO_TEMPLATE + self.footer_template = COMBINED_FOOTER_TEMPLATE + for line in self.generate_email(push, body_filter, values): + yield line + + def generate_email_body(self, push): + '''Call the appropriate body generation routine. + + If this is a combined refchange/revision email, the special logic + for handling this combined email comes from this function. For + other cases, we just use the normal handling.''' + + # If self._single_revision isn't set; don't override + if not self._single_revision: + for line in super(BranchChange, self).generate_email_body(push): + yield line + return + + # This is a combined refchange/revision email; we first provide + # some info from the refchange portion, and then call the revision + # generate_email_body function to handle the revision portion. + adds = list(generate_summaries( + '--topo-order', '--reverse', '%s..%s' + % (self.old.commit_sha1, self.new.commit_sha1,) + )) + + yield self.expand("The following commit(s) were added to %(refname)s by this push:\n") + for (sha1, subject) in adds: + yield self.expand( + BRIEF_SUMMARY_TEMPLATE, action='new', + rev_short=sha1, text=subject, + ) + + yield self._single_revision.rev.short + " is described below\n" + yield '\n' + + for line in self._single_revision.generate_email_body(push): + yield line class AnnotatedTagChange(ReferenceChange): @@ -1390,13 +1700,17 @@ class SendMailer(Mailer): sys.exit(1) try: p.stdin.writelines(lines) - except: + except Exception, e: sys.stderr.write( '*** Error while generating commit email\n' '*** - mail sending aborted.\n' ) - p.terminate() - raise + try: + # subprocess.terminate() is not available in Python 2.4 + p.terminate() + except AttributeError: + pass + raise e else: p.stdin.close() retcode = p.wait() @@ -1407,34 +1721,72 @@ class SendMailer(Mailer): class SMTPMailer(Mailer): """Send emails using Python's smtplib.""" - def __init__(self, envelopesender, smtpserver): + def __init__(self, envelopesender, smtpserver, + smtpservertimeout=10.0, smtpserverdebuglevel=0, + smtpencryption='none', + smtpuser='', smtppass='', + ): if not envelopesender: sys.stderr.write( 'fatal: git_multimail: cannot use SMTPMailer without a sender address.\n' 'please set either multimailhook.envelopeSender or user.email\n' ) sys.exit(1) + if smtpencryption == 'ssl' and not (smtpuser and smtppass): + raise ConfigurationException( + 'Cannot use SMTPMailer with security option ssl ' + 'without options username and password.' + ) self.envelopesender = envelopesender self.smtpserver = smtpserver + self.smtpservertimeout = smtpservertimeout + self.smtpserverdebuglevel = smtpserverdebuglevel + self.security = smtpencryption + self.username = smtpuser + self.password = smtppass try: - self.smtp = smtplib.SMTP(self.smtpserver) + if self.security == 'none': + self.smtp = smtplib.SMTP(self.smtpserver, timeout=self.smtpservertimeout) + elif self.security == 'ssl': + self.smtp = smtplib.SMTP_SSL(self.smtpserver, timeout=self.smtpservertimeout) + elif self.security == 'tls': + if ':' not in self.smtpserver: + self.smtpserver += ':587' # default port for TLS + self.smtp = smtplib.SMTP(self.smtpserver, timeout=self.smtpservertimeout) + self.smtp.ehlo() + self.smtp.starttls() + self.smtp.ehlo() + else: + sys.stdout.write('*** Error: Control reached an invalid option. ***') + sys.exit(1) + if self.smtpserverdebuglevel > 0: + sys.stdout.write( + "*** Setting debug on for SMTP server connection (%s) ***\n" + % self.smtpserverdebuglevel) + self.smtp.set_debuglevel(self.smtpserverdebuglevel) except Exception, e: - sys.stderr.write('*** Error establishing SMTP connection to %s***\n' % self.smtpserver) + sys.stderr.write( + '*** Error establishing SMTP connection to %s ***\n' + % self.smtpserver) sys.stderr.write('*** %s\n' % str(e)) sys.exit(1) def __del__(self): - self.smtp.quit() + if hasattr(self, 'smtp'): + self.smtp.quit() def send(self, lines, to_addrs): try: + if self.username or self.password: + sys.stderr.write("*** Authenticating as %s ***\n" % self.username) + self.smtp.login(self.username, self.password) msg = ''.join(lines) # turn comma-separated list into Python list if needed. if isinstance(to_addrs, basestring): to_addrs = [email for (name, email) in getaddresses([to_addrs])] self.smtp.sendmail(self.envelopesender, to_addrs, msg) except Exception, e: - sys.stderr.write('*** Error sending email***\n') + sys.stderr.write('*** Error sending email ***\n') sys.stderr.write('*** %s\n' % str(e)) self.smtp.quit() sys.exit(1) @@ -1549,6 +1901,10 @@ class Environment(object): True iff announce emails should include a shortlog. + refchange_showgraph (bool) + + True iff refchanges emails should include a detailed graph. + refchange_showlog (bool) True iff refchanges emails should include a detailed log. @@ -1559,6 +1915,12 @@ class Environment(object): summary email. The value should be a list of strings representing words to be passed to the command. + graphopts (list of strings) + + Analogous to diffopts, but contains options passed to + 'git log --graph' when generating the detailed graph for + a set of commits (see refchange_showgraph) + logopts (list of strings) Analogous to diffopts, but contains options passed to @@ -1571,6 +1933,17 @@ class Environment(object): commit mail. The value should be a list of strings representing words to be passed to the command. + quiet (bool) + On success do not write to stderr + + stdout (bool) + Write email to stdout rather than emailing. Useful for debugging + + combine_when_single_commit (bool) + + True if a combined email should be produced when a single + new commit is pushed to a branch, False otherwise. + """ REPO_NAME_RE = re.compile(r'^(?P<name>.+?)(?:\.git)$') @@ -1580,9 +1953,14 @@ class Environment(object): self.announce_show_shortlog = False self.maxcommitemails = 500 self.diffopts = ['--stat', '--summary', '--find-copies-harder'] + self.graphopts = ['--oneline', '--decorate'] self.logopts = [] + self.refchange_showgraph = False self.refchange_showlog = False self.commitlogopts = ['-C', '--stat', '-p', '--cc'] + self.quiet = False + self.stdout = False + self.combine_when_single_commit = True self.COMPUTED_KEYS = [ 'administrator', @@ -1614,6 +1992,14 @@ class Environment(object): def get_pusher_email(self): return None + def get_fromaddr(self): + config = Config('user') + fromname = config.get('name', default='') + fromemail = config.get('email', default='') + if fromemail: + return formataddr([fromname, fromemail]) + return self.get_sender() + def get_administrator(self): return 'the administrator of this repository' @@ -1631,7 +2017,7 @@ class Environment(object): return CHARSET def get_values(self): - """Return a dictionary {keyword : expansion} for this Environment. + """Return a dictionary {keyword: expansion} for this Environment. This method is called by Change._compute_values(). The keys in the returned dictionary are available to be used in any of @@ -1699,6 +2085,24 @@ class Environment(object): return lines + def log_msg(self, msg): + """Write the string msg on a log file or on stderr. + + Sends the text to stderr by default, override to change the behavior.""" + sys.stderr.write(msg) + + def log_warning(self, msg): + """Write the string msg on a log file or on stderr. + + Sends the text to stderr by default, override to change the behavior.""" + sys.stderr.write(msg) + + def log_error(self, msg): + """Write the string msg on a log file or on stderr. + + Sends the text to stderr by default, override to change the behavior.""" + sys.stderr.write(msg) + class ConfigEnvironmentMixin(Environment): """A mixin that sets self.config to its constructor's config argument. @@ -1723,20 +2127,23 @@ class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin): config=config, **kw ) - self.announce_show_shortlog = config.get_bool( - 'announceshortlog', default=self.announce_show_shortlog - ) - - self.refchange_showlog = config.get_bool( - 'refchangeshowlog', default=self.refchange_showlog - ) + for var, cfg in ( + ('announce_show_shortlog', 'announceshortlog'), + ('refchange_showgraph', 'refchangeShowGraph'), + ('refchange_showlog', 'refchangeshowlog'), + ('quiet', 'quiet'), + ('stdout', 'stdout'), + ): + val = config.get_bool(cfg) + if val is not None: + setattr(self, var, val) maxcommitemails = config.get('maxcommitemails') if maxcommitemails is not None: try: self.maxcommitemails = int(maxcommitemails) except ValueError: - sys.stderr.write( + self.log_warning( '*** Malformed value for multimailhook.maxCommitEmails: %s\n' % maxcommitemails + '*** Expected a number. Ignoring.\n' ) @@ -1745,6 +2152,10 @@ class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin): if diffopts is not None: self.diffopts = shlex.split(diffopts) + graphopts = config.get('graphOpts') + if graphopts is not None: + self.graphopts = shlex.split(graphopts) + logopts = config.get('logopts') if logopts is not None: self.logopts = shlex.split(logopts) @@ -1756,14 +2167,18 @@ class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin): reply_to = config.get('replyTo') self.__reply_to_refchange = config.get('replyToRefchange', default=reply_to) if ( - self.__reply_to_refchange is not None - and self.__reply_to_refchange.lower() == 'author' - ): + self.__reply_to_refchange is not None + and self.__reply_to_refchange.lower() == 'author' + ): raise ConfigurationException( '"author" is not an allowed setting for replyToRefchange' ) self.__reply_to_commit = config.get('replyToCommit', default=reply_to) + combine = config.get_bool('combineWhenSingleCommit') + if combine is not None: + self.combine_when_single_commit = combine + def get_administrator(self): return ( self.config.get('administrator') @@ -1779,8 +2194,12 @@ class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin): def get_emailprefix(self): emailprefix = self.config.get('emailprefix') - if emailprefix and emailprefix.strip(): - return emailprefix.strip() + ' ' + if emailprefix is not None: + emailprefix = emailprefix.strip() + if emailprefix: + return emailprefix + ' ' + else: + return '' else: return '[%s] ' % (self.get_repo_shortname(),) @@ -1791,14 +2210,7 @@ class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin): fromaddr = self.config.get('from') if fromaddr: return fromaddr - else: - config = Config('user') - fromname = config.get('name', default='') - fromemail = config.get('email', default='') - if fromemail: - return formataddr([fromname, fromemail]) - else: - return self.get_sender() + return super(ConfigOptionsEnvironmentMixin, self).get_fromaddr() def get_reply_to_refchange(self, refchange): if self.__reply_to_refchange is None: @@ -1814,7 +2226,7 @@ class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin): if self.__reply_to_commit is None: return super(ConfigOptionsEnvironmentMixin, self).get_reply_to_commit(revision) elif self.__reply_to_commit.lower() == 'author': - return revision.get_author() + return revision.author elif self.__reply_to_commit.lower() == 'pusher': return self.get_pusher_email() elif self.__reply_to_commit.lower() == 'none': @@ -1822,6 +2234,9 @@ class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin): else: return self.__reply_to_commit + def get_scancommitforcc(self): + return self.config.get('scancommitforcc') + class FilterLinesEnvironmentMixin(Environment): """Handle encoding and maximum line length of body lines. @@ -1862,9 +2277,9 @@ class FilterLinesEnvironmentMixin(Environment): class ConfigFilterLinesEnvironmentMixin( - ConfigEnvironmentMixin, - FilterLinesEnvironmentMixin, - ): + ConfigEnvironmentMixin, + FilterLinesEnvironmentMixin, + ): """Handle encoding and maximum line length based on config.""" def __init__(self, config, **kw): @@ -1896,9 +2311,9 @@ class MaxlinesEnvironmentMixin(Environment): class ConfigMaxlinesEnvironmentMixin( - ConfigEnvironmentMixin, - MaxlinesEnvironmentMixin, - ): + ConfigEnvironmentMixin, + MaxlinesEnvironmentMixin, + ): """Limit the email body to the number of lines specified in config.""" def __init__(self, config, **kw): @@ -1927,9 +2342,9 @@ class FQDNEnvironmentMixin(Environment): class ConfigFQDNEnvironmentMixin( - ConfigEnvironmentMixin, - FQDNEnvironmentMixin, - ): + ConfigEnvironmentMixin, + FQDNEnvironmentMixin, + ): """Read the FQDN from the config.""" def __init__(self, config, **kw): @@ -1970,10 +2385,10 @@ class StaticRecipientsEnvironmentMixin(Environment): """Set recipients statically based on constructor parameters.""" def __init__( - self, - refchange_recipients, announce_recipients, revision_recipients, - **kw - ): + self, + refchange_recipients, announce_recipients, revision_recipients, scancommitforcc, + **kw + ): super(StaticRecipientsEnvironmentMixin, self).__init__(**kw) # The recipients for various types of notification emails, as @@ -1985,7 +2400,8 @@ class StaticRecipientsEnvironmentMixin(Environment): # compute them once and for all: if not (refchange_recipients or announce_recipients - or revision_recipients): + or revision_recipients + or scancommitforcc): raise ConfigurationException('No email recipients configured!') self.__refchange_recipients = refchange_recipients self.__announce_recipients = announce_recipients @@ -2002,9 +2418,9 @@ class StaticRecipientsEnvironmentMixin(Environment): class ConfigRecipientsEnvironmentMixin( - ConfigEnvironmentMixin, - StaticRecipientsEnvironmentMixin - ): + ConfigEnvironmentMixin, + StaticRecipientsEnvironmentMixin + ): """Determine recipients statically based on config.""" def __init__(self, config, **kw): @@ -2019,6 +2435,7 @@ class ConfigRecipientsEnvironmentMixin( revision_recipients=self._get_recipients( config, 'commitlist', 'mailinglist', ), + scancommitforcc=config.get('scancommitforcc'), **kw ) @@ -2067,20 +2484,20 @@ class ProjectdescEnvironmentMixin(Environment): class GenericEnvironmentMixin(Environment): def get_pusher(self): - return self.osenv.get('USER', 'unknown user') + return self.osenv.get('USER', self.osenv.get('USERNAME', 'unknown user')) class GenericEnvironment( - ProjectdescEnvironmentMixin, - ConfigMaxlinesEnvironmentMixin, - ComputeFQDNEnvironmentMixin, - ConfigFilterLinesEnvironmentMixin, - ConfigRecipientsEnvironmentMixin, - PusherDomainEnvironmentMixin, - ConfigOptionsEnvironmentMixin, - GenericEnvironmentMixin, - Environment, - ): + ProjectdescEnvironmentMixin, + ConfigMaxlinesEnvironmentMixin, + ComputeFQDNEnvironmentMixin, + ConfigFilterLinesEnvironmentMixin, + ConfigRecipientsEnvironmentMixin, + PusherDomainEnvironmentMixin, + ConfigOptionsEnvironmentMixin, + GenericEnvironmentMixin, + Environment, + ): pass @@ -2097,6 +2514,45 @@ class GitoliteEnvironmentMixin(Environment): def get_pusher(self): return self.osenv.get('GL_USER', 'unknown user') + def get_fromaddr(self): + GL_USER = self.osenv.get('GL_USER') + if GL_USER is not None: + # Find the path to gitolite.conf. Note that gitolite v3 + # did away with the GL_ADMINDIR and GL_CONF environment + # variables (they are now hard-coded). + GL_ADMINDIR = self.osenv.get( + 'GL_ADMINDIR', + os.path.expanduser(os.path.join('~', '.gitolite'))) + GL_CONF = self.osenv.get( + 'GL_CONF', + os.path.join(GL_ADMINDIR, 'conf', 'gitolite.conf')) + if os.path.isfile(GL_CONF): + f = open(GL_CONF, 'rU') + try: + in_user_emails_section = False + re_template = r'^\s*#\s*{}\s*$' + re_begin, re_user, re_end = ( + re.compile(re_template.format(x)) + for x in ( + r'BEGIN\s+USER\s+EMAILS', + re.escape(GL_USER) + r'\s+(.*)', + r'END\s+USER\s+EMAILS', + )) + for l in f: + l = l.rstrip('\n') + if not in_user_emails_section: + if re_begin.match(l): + in_user_emails_section = True + continue + if re_end.match(l): + break + m = re_user.match(l) + if m: + return m.group(1) + finally: + f.close() + return super(GitoliteEnvironmentMixin, self).get_fromaddr() + class IncrementalDateTime(object): """Simple wrapper to give incremental date/times. @@ -2116,16 +2572,16 @@ class IncrementalDateTime(object): class GitoliteEnvironment( - ProjectdescEnvironmentMixin, - ConfigMaxlinesEnvironmentMixin, - ComputeFQDNEnvironmentMixin, - ConfigFilterLinesEnvironmentMixin, - ConfigRecipientsEnvironmentMixin, - PusherDomainEnvironmentMixin, - ConfigOptionsEnvironmentMixin, - GitoliteEnvironmentMixin, - Environment, - ): + ProjectdescEnvironmentMixin, + ConfigMaxlinesEnvironmentMixin, + ComputeFQDNEnvironmentMixin, + ConfigFilterLinesEnvironmentMixin, + ConfigRecipientsEnvironmentMixin, + PusherDomainEnvironmentMixin, + ConfigOptionsEnvironmentMixin, + GitoliteEnvironmentMixin, + Environment, + ): pass @@ -2149,9 +2605,9 @@ class Push(object): references. The first step is to determine the "other" references--those - unaffected by the current push. They are computed by - Push._compute_other_ref_sha1s() by listing all references then - removing any affected by this push. + unaffected by the current push. They are computed by listing all + references then removing any affected by this push. The results + are stored in Push._other_ref_sha1s. The commits contained in the repository before this push were @@ -2187,7 +2643,7 @@ class Push(object): possible and working with SHA1s thereafter (because SHA1s are immutable).""" - # A map {(changeclass, changetype) : integer} specifying the order + # A map {(changeclass, changetype): integer} specifying the order # that reference changes will be processed if multiple reference # changes are included in a single push. The order is significant # mostly because new commit notifications are threaded together @@ -2211,66 +2667,134 @@ class Push(object): ]) ) - def __init__(self, changes): + def __init__(self, changes, ignore_other_refs=False): self.changes = sorted(changes, key=self._sort_key) + self.__other_ref_sha1s = None + self.__cached_commits_spec = {} - # The SHA-1s of commits referred to by references unaffected - # by this push: - other_ref_sha1s = self._compute_other_ref_sha1s() + if ignore_other_refs: + self.__other_ref_sha1s = set() - self._old_rev_exclusion_spec = self._compute_rev_exclusion_spec( - other_ref_sha1s.union( - change.old.sha1 + @classmethod + def _sort_key(klass, change): + return (klass.SORT_ORDER[change.__class__, change.change_type], change.refname,) + + @property + def _other_ref_sha1s(self): + """The GitObjects referred to by references unaffected by this push. + """ + if self.__other_ref_sha1s is None: + # The refnames being changed by this push: + updated_refs = set( + change.refname for change in self.changes - if change.old.type in ['commit', 'tag'] ) - ) - self._new_rev_exclusion_spec = self._compute_rev_exclusion_spec( - other_ref_sha1s.union( - change.new.sha1 - for change in self.changes - if change.new.type in ['commit', 'tag'] + + # The SHA-1s of commits referred to by all references in this + # repository *except* updated_refs: + sha1s = set() + fmt = ( + '%(objectname) %(objecttype) %(refname)\n' + '%(*objectname) %(*objecttype) %(refname)' ) - ) + for line in read_git_lines( + ['for-each-ref', '--format=%s' % (fmt,)]): + (sha1, type, name) = line.split(' ', 2) + if sha1 and type == 'commit' and name not in updated_refs: + sha1s.add(sha1) - @classmethod - def _sort_key(klass, change): - return (klass.SORT_ORDER[change.__class__, change.change_type], change.refname,) + self.__other_ref_sha1s = sha1s + + return self.__other_ref_sha1s + + def _get_commits_spec_incl(self, new_or_old, reference_change=None): + """Get new or old SHA-1 from one or each of the changed refs. - def _compute_other_ref_sha1s(self): - """Return the GitObjects referred to by references unaffected by this push.""" + Return a list of SHA-1 commit identifier strings suitable as + arguments to 'git rev-list' (or 'git log' or ...). The + returned identifiers are either the old or new values from one + or all of the changed references, depending on the values of + new_or_old and reference_change. - # The refnames being changed by this push: - updated_refs = set( - change.refname + new_or_old is either the string 'new' or the string 'old'. If + 'new', the returned SHA-1 identifiers are the new values from + each changed reference. If 'old', the SHA-1 identifiers are + the old values from each changed reference. + + If reference_change is specified and not None, only the new or + old reference from the specified reference is included in the + return value. + + This function returns None if there are no matching revisions + (e.g., because a branch was deleted and new_or_old is 'new'). + """ + + if not reference_change: + incl_spec = sorted( + getattr(change, new_or_old).sha1 + for change in self.changes + if getattr(change, new_or_old) + ) + if not incl_spec: + incl_spec = None + elif not getattr(reference_change, new_or_old).commit_sha1: + incl_spec = None + else: + incl_spec = [getattr(reference_change, new_or_old).commit_sha1] + return incl_spec + + def _get_commits_spec_excl(self, new_or_old): + """Get exclusion revisions for determining new or discarded commits. + + Return a list of strings suitable as arguments to 'git + rev-list' (or 'git log' or ...) that will exclude all + commits that, depending on the value of new_or_old, were + either previously in the repository (useful for determining + which commits are new to the repository) or currently in the + repository (useful for determining which commits were + discarded from the repository). + + new_or_old is either the string 'new' or the string 'old'. If + 'new', the commits to be excluded are those that were in the + repository before the push. If 'old', the commits to be + excluded are those that are currently in the repository. """ + + old_or_new = {'old': 'new', 'new': 'old'}[new_or_old] + excl_revs = self._other_ref_sha1s.union( + getattr(change, old_or_new).sha1 for change in self.changes + if getattr(change, old_or_new).type in ['commit', 'tag'] ) + return ['^' + sha1 for sha1 in sorted(excl_revs)] - # The SHA-1s of commits referred to by all references in this - # repository *except* updated_refs: - sha1s = set() - fmt = ( - '%(objectname) %(objecttype) %(refname)\n' - '%(*objectname) %(*objecttype) %(refname)' - ) - for line in read_git_lines(['for-each-ref', '--format=%s' % (fmt,)]): - (sha1, type, name) = line.split(' ', 2) - if sha1 and type == 'commit' and name not in updated_refs: - sha1s.add(sha1) + def get_commits_spec(self, new_or_old, reference_change=None): + """Get rev-list arguments for added or discarded commits. - return sha1s + Return a list of strings suitable as arguments to 'git + rev-list' (or 'git log' or ...) that select those commits + that, depending on the value of new_or_old, are either new to + the repository or were discarded from the repository. - def _compute_rev_exclusion_spec(self, sha1s): - """Return an exclusion specification for 'git rev-list'. + new_or_old is either the string 'new' or the string 'old'. If + 'new', the returned list is used to select commits that are + new to the repository. If 'old', the returned value is used + to select the commits that have been discarded from the + repository. - git_objects is an iterable over GitObject instances. Return a - string that can be passed to the standard input of 'git - rev-list --stdin' to exclude all of the commits referred to by - git_objects.""" + If reference_change is specified and not None, the new or + discarded commits are limited to those that are reachable from + the new or old value of the specified reference. - return ''.join( - ['^%s\n' % (sha1,) for sha1 in sorted(sha1s)] - ) + This function returns None if there are no added (or discarded) + revisions. + """ + key = (new_or_old, reference_change) + if key not in self.__cached_commits_spec: + ret = self._get_commits_spec_incl(new_or_old, reference_change) + if ret is not None: + ret.extend(self._get_commits_spec_excl(new_or_old)) + self.__cached_commits_spec[key] = ret + return self.__cached_commits_spec[key] def get_new_commits(self, reference_change=None): """Return a list of commits added by this push. @@ -2280,19 +2804,8 @@ class Push(object): reference_change is None, then return a list of *all* commits added by this push.""" - if not reference_change: - new_revs = sorted( - change.new.sha1 - for change in self.changes - if change.new - ) - elif not reference_change.new.commit_sha1: - return [] - else: - new_revs = [reference_change.new.commit_sha1] - - cmd = ['rev-list', '--stdin'] + new_revs - return read_git_lines(cmd, input=self._old_rev_exclusion_spec) + spec = self.get_commits_spec('new', reference_change) + return git_rev_list(spec) def get_discarded_commits(self, reference_change): """Return a list of commits discarded by this push. @@ -2301,13 +2814,8 @@ class Push(object): entirely discarded from the repository by the part of this push represented by reference_change.""" - if not reference_change.old.commit_sha1: - return [] - else: - old_revs = [reference_change.old.commit_sha1] - - cmd = ['rev-list', '--stdin'] + old_revs - return read_git_lines(cmd, input=self._new_rev_exclusion_spec) + spec = self.get_commits_spec('old', reference_change) + return git_rev_list(spec) def send_emails(self, mailer, body_filter=None): """Use send all of the notification emails needed for this push. @@ -2325,30 +2833,43 @@ class Push(object): unhandled_sha1s = set(self.get_new_commits()) send_date = IncrementalDateTime() for change in self.changes: + sha1s = [] + for sha1 in reversed(list(self.get_new_commits(change))): + if sha1 in unhandled_sha1s: + sha1s.append(sha1) + unhandled_sha1s.remove(sha1) + # Check if we've got anyone to send to if not change.recipients: - sys.stderr.write( + change.environment.log_warning( '*** no recipients configured so no email will be sent\n' '*** for %r update %s->%s\n' % (change.refname, change.old.sha1, change.new.sha1,) ) else: - sys.stderr.write('Sending notification emails to: %s\n' % (change.recipients,)) - extra_values = {'send_date' : send_date.next()} - mailer.send( - change.generate_email(self, body_filter, extra_values), - change.recipients, - ) + if not change.environment.quiet: + change.environment.log_msg( + 'Sending notification emails to: %s\n' % (change.recipients,)) + extra_values = {'send_date': send_date.next()} - sha1s = [] - for sha1 in reversed(list(self.get_new_commits(change))): - if sha1 in unhandled_sha1s: - sha1s.append(sha1) - unhandled_sha1s.remove(sha1) + rev = change.send_single_combined_email(sha1s) + if rev: + mailer.send( + change.generate_combined_email(self, rev, body_filter, extra_values), + rev.recipients, + ) + # This change is now fully handled; no need to handle + # individual revisions any further. + continue + else: + mailer.send( + change.generate_email(self, body_filter, extra_values), + change.recipients, + ) max_emails = change.environment.maxcommitemails if max_emails and len(sha1s) > max_emails: - sys.stderr.write( + change.environment.log_warning( '*** Too many new commits (%d), not sending commit emails.\n' % len(sha1s) + '*** Try setting multimailhook.maxCommitEmails to a greater value\n' + '*** Currently, multimailhook.maxCommitEmails=%d\n' % max_emails @@ -2356,9 +2877,13 @@ class Push(object): return for (num, sha1) in enumerate(sha1s): - rev = Revision(change, GitObject(sha1), num=num+1, tot=len(sha1s)) + rev = Revision(change, GitObject(sha1), num=num + 1, tot=len(sha1s)) + if not rev.recipients and rev.cc_recipients: + change.environment.log_msg('*** Replacing Cc: with To:\n') + rev.recipients = rev.cc_recipients + rev.cc_recipients = None if rev.recipients: - extra_values = {'send_date' : send_date.next()} + extra_values = {'send_date': send_date.next()} mailer.send( rev.generate_email(self, body_filter, extra_values), rev.recipients, @@ -2366,7 +2891,7 @@ class Push(object): # Consistency check: if unhandled_sha1s: - sys.stderr.write( + change.environment.log_error( 'ERROR: No emails were sent for the following new commits:\n' ' %s\n' % ('\n '.join(sorted(unhandled_sha1s)),) @@ -2384,7 +2909,7 @@ def run_as_post_receive_hook(environment, mailer): push.send_emails(mailer, body_filter=environment.filter_body) -def run_as_update_hook(environment, mailer, refname, oldrev, newrev): +def run_as_update_hook(environment, mailer, refname, oldrev, newrev, force_send=False): changes = [ ReferenceChange.create( environment, @@ -2393,7 +2918,7 @@ def run_as_update_hook(environment, mailer, refname, oldrev, newrev): refname, ), ] - push = Push(changes) + push = Push(changes, force_send) push.send_emails(mailer, body_filter=environment.filter_body) @@ -2402,9 +2927,18 @@ def choose_mailer(config, environment): if mailer == 'smtp': smtpserver = config.get('smtpserver', default='localhost') + smtpservertimeout = float(config.get('smtpservertimeout', default=10.0)) + smtpserverdebuglevel = int(config.get('smtpserverdebuglevel', default=0)) + smtpencryption = config.get('smtpencryption', default='none') + smtpuser = config.get('smtpuser', default='') + smtppass = config.get('smtppass', default='') mailer = SMTPMailer( envelopesender=(environment.get_sender() or environment.get_fromaddr()), - smtpserver=smtpserver, + smtpserver=smtpserver, smtpservertimeout=smtpservertimeout, + smtpserverdebuglevel=smtpserverdebuglevel, + smtpencryption=smtpencryption, + smtpuser=smtpuser, + smtppass=smtppass, ) elif mailer == 'sendmail': command = config.get('sendmailcommand') @@ -2412,7 +2946,7 @@ def choose_mailer(config, environment): command = shlex.split(command) mailer = SendMailer(command=command, envelopesender=environment.get_sender()) else: - sys.stderr.write( + environment.log_error( 'fatal: multimailhook.mailer is set to an incorrect value: "%s"\n' % mailer + 'please use one of "smtp" or "sendmail".\n' ) @@ -2421,8 +2955,8 @@ def choose_mailer(config, environment): KNOWN_ENVIRONMENTS = { - 'generic' : GenericEnvironmentMixin, - 'gitolite' : GitoliteEnvironmentMixin, + 'generic': GenericEnvironmentMixin, + 'gitolite': GitoliteEnvironmentMixin, } @@ -2439,8 +2973,8 @@ def choose_environment(config, osenv=None, env=None, recipients=None): ConfigOptionsEnvironmentMixin, ] environment_kw = { - 'osenv' : osenv, - 'config' : config, + 'osenv': osenv, + 'config': config, } if not env: @@ -2459,6 +2993,7 @@ def choose_environment(config, osenv=None, env=None, recipients=None): environment_kw['refchange_recipients'] = recipients environment_kw['announce_recipients'] = recipients environment_kw['revision_recipients'] = recipients + environment_kw['scancommitforcc'] = config.get('scancommitforcc') else: environment_mixins.insert(0, ConfigRecipientsEnvironmentMixin) @@ -2499,6 +3034,14 @@ def main(args): '(intended for debugging purposes).' ), ) + parser.add_option( + '--force-send', action='store_true', default=False, + help=( + 'Force sending refchange email when using as an update hook. ' + 'This is useful to work around the unreliable new commits ' + 'detection in this mode.' + ), + ) (options, args) = parser.parse_args(args) @@ -2513,11 +3056,11 @@ def main(args): if options.show_env: sys.stderr.write('Environment values:\n') - for (k,v) in sorted(environment.get_values().items()): - sys.stderr.write(' %s : %r\n' % (k,v)) + for (k, v) in sorted(environment.get_values().items()): + sys.stderr.write(' %s : %r\n' % (k, v)) sys.stderr.write('\n') - if options.stdout: + if options.stdout or environment.stdout: mailer = OutputMailer(sys.stdout) else: mailer = choose_mailer(config, environment) @@ -2528,7 +3071,7 @@ def main(args): if len(args) != 3: parser.error('Need zero or three non-option arguments') (refname, oldrev, newrev) = args - run_as_update_hook(environment, mailer, refname, oldrev, newrev) + run_as_update_hook(environment, mailer, refname, oldrev, newrev, options.force_send) else: run_as_post_receive_hook(environment, mailer) except ConfigurationException, e: diff --git a/contrib/hooks/multimail/migrate-mailhook-config b/contrib/hooks/multimail/migrate-mailhook-config index 04eeaac413..d0e9b39201 100755 --- a/contrib/hooks/multimail/migrate-mailhook-config +++ b/contrib/hooks/multimail/migrate-mailhook-config @@ -22,6 +22,7 @@ OLD_NAMES = [ 'showrev', 'emailmaxlines', 'diffopts', + 'scancommitforcc', ] NEW_NAMES = [ @@ -38,6 +39,7 @@ NEW_NAMES = [ 'emailmaxlines', 'diffopts', 'emaildomain', + 'scancommitforcc', ] @@ -61,7 +63,7 @@ def _check_old_config_exists(old): """Check that at least one old configuration value is set.""" for name in OLD_NAMES: - if old.has_key(name): + if name in old: return True return False @@ -72,7 +74,7 @@ def _check_new_config_clear(new): retval = True for name in NEW_NAMES: - if new.has_key(name): + if name in new: if retval: sys.stderr.write('INFO: The following configuration values already exist:\n\n') sys.stderr.write(' "%s.%s"\n' % (new.section, name)) @@ -83,7 +85,7 @@ def _check_new_config_clear(new): def erase_values(config, names): for name in names: - if config.has_key(name): + if name in config: try: sys.stderr.write('...unsetting "%s.%s"\n' % (config.section, name)) config.unset_all(name) @@ -170,7 +172,7 @@ def migrate_config(strict=False, retain=False, overwrite=False): ) name = 'showrev' - if old.has_key(name): + if name in old: msg = 'git-multimail does not support "%s.%s"' % (old.section, name,) if strict: sys.exit( @@ -182,7 +184,7 @@ def migrate_config(strict=False, retain=False, overwrite=False): sys.stderr.write('\nWARNING: %s (ignoring).\n\n' % (msg,)) for name in ['mailinglist', 'announcelist']: - if old.has_key(name): + if name in old: sys.stderr.write( '...copying "%s.%s" to "%s.%s"\n' % (old.section, name, new.section, name) ) @@ -198,15 +200,15 @@ def migrate_config(strict=False, retain=False, overwrite=False): ) new.set('announceshortlog', 'true') - for name in ['envelopesender', 'emailmaxlines', 'diffopts']: - if old.has_key(name): + for name in ['envelopesender', 'emailmaxlines', 'diffopts', 'scancommitforcc']: + if name in old: sys.stderr.write( '...copying "%s.%s" to "%s.%s"\n' % (old.section, name, new.section, name) ) new.set(name, old.get(name)) name = 'emailprefix' - if old.has_key(name): + if name in old: sys.stderr.write( '...copying "%s.%s" to "%s.%s"\n' % (old.section, name, new.section, name) ) diff --git a/contrib/hooks/multimail/post-receive b/contrib/hooks/multimail/post-receive.example index 4d46828ba5..43f7b6b635 100755 --- a/contrib/hooks/multimail/post-receive +++ b/contrib/hooks/multimail/post-receive.example @@ -2,16 +2,18 @@ """Example post-receive hook based on git-multimail. -This script is a simple example of a post-receive hook implemented -using git_multimail.py as a Python module. It is intended to be -customized before use; see the comments in the script to help you get -started. +The simplest way to use git-multimail is to use the script +git_multimail.py directly as a post-receive hook, and to configure it +using Git's configuration files and command-line parameters. You can +also write your own Python wrapper for more advanced configurability, +using git_multimail.py as a Python module. -It is possible to use git_multimail.py itself as a post-receive or -update hook, configured via git config settings and/or command-line -parameters. But for more flexibility, it can also be imported as a -Python module by a custom post-receive script as done here. The -latter has the following advantages: +This script is a simple example of such a post-receive hook. It is +intended to be customized before use; see the comments in the script +to help you get started. + +Using git-multimail as a Python module as done here provides more +flexibility. It has the following advantages: * The tool's behavior can be customized using arbitrary Python code, without having to edit git_multimail.py. @@ -56,8 +58,11 @@ config = git_multimail.Config('multimailhook') # Select the type of environment: -environment = git_multimail.GenericEnvironment(config=config) -#environment = git_multimail.GitoliteEnvironment(config=config) +try: + environment = git_multimail.GenericEnvironment(config=config) + #environment = git_multimail.GitoliteEnvironment(config=config) +except git_multimail.ConfigurationException, e: + sys.exit(str(e)) # Choose the method of sending emails based on the git config: diff --git a/contrib/hooks/pre-auto-gc-battery b/contrib/hooks/pre-auto-gc-battery index 9d0c2d1990..6a2cdebdb7 100755 --- a/contrib/hooks/pre-auto-gc-battery +++ b/contrib/hooks/pre-auto-gc-battery @@ -33,7 +33,7 @@ elif grep -q "AC Power \+: 1" /proc/pmu/info 2>/dev/null then exit 0 elif test -x /usr/bin/pmset && /usr/bin/pmset -g batt | - grep -q "Currently drawing from 'AC Power'" + grep -q "drawing from 'AC Power'" then exit 0 fi diff --git a/contrib/subtree/git-subtree.sh b/contrib/subtree/git-subtree.sh index fa1a5839af..07bd77c4c8 100755 --- a/contrib/subtree/git-subtree.sh +++ b/contrib/subtree/git-subtree.sh @@ -26,7 +26,7 @@ b,branch= create a new branch from the split subtree ignore-joins ignore prior --rejoin commits onto= try connecting new tree to an existing one rejoin merge the new branch back into HEAD - options for 'add', 'merge', 'pull' and 'push' + options for 'add', 'merge', and 'pull' squash merge subtree changes as a single commit " eval "$(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?)" @@ -51,14 +51,21 @@ prefix= debug() { if [ -n "$debug" ]; then - echo "$@" >&2 + printf "%s\n" "$*" >&2 fi } say() { if [ -z "$quiet" ]; then - echo "$@" >&2 + printf "%s\n" "$*" >&2 + fi +} + +progress() +{ + if [ -z "$quiet" ]; then + printf "%s\r" "$*" >&2 fi } @@ -599,7 +606,7 @@ cmd_split() eval "$grl" | while read rev parents; do revcount=$(($revcount + 1)) - say -n "$revcount/$revmax ($createcount)
" + progress "$revcount/$revmax ($createcount)" debug "Processing commit: $rev" exists=$(cache_get $rev) if [ -n "$exists" ]; then diff --git a/contrib/subtree/git-subtree.txt b/contrib/subtree/git-subtree.txt index 54e4b4a243..60d76cdddf 100644 --- a/contrib/subtree/git-subtree.txt +++ b/contrib/subtree/git-subtree.txt @@ -146,7 +146,7 @@ OPTIONS OPTIONS FOR add, merge, push, pull ---------------------------------- --squash:: - This option is only valid for add, merge, push and pull + This option is only valid for add, merge, and pull commands. + Instead of merging the entire history from the subtree project, produce @@ -356,9 +356,14 @@ static int filter_buffer_or_fd(int in, int out, void *data) sigchain_push(SIGPIPE, SIG_IGN); if (params->src) { - write_err = (write_in_full(child_process.in, params->src, params->size) < 0); + write_err = (write_in_full(child_process.in, + params->src, params->size) < 0); + if (errno == EPIPE) + write_err = 0; } else { write_err = copy_fd(params->fd, child_process.in); + if (write_err == COPY_WRITE_ERROR && errno == EPIPE) + write_err = 0; } if (close(child_process.in)) @@ -7,13 +7,10 @@ int copy_fd(int ifd, int ofd) ssize_t len = xread(ifd, buffer, sizeof(buffer)); if (!len) break; - if (len < 0) { - return error("copy-fd: read returned %s", - strerror(errno)); - } + if (len < 0) + return COPY_READ_ERROR; if (write_in_full(ofd, buffer, len) < 0) - return error("copy-fd: write returned %s", - strerror(errno)); + return COPY_WRITE_ERROR; } return 0; } @@ -43,6 +40,14 @@ int copy_file(const char *dst, const char *src, int mode) return fdo; } status = copy_fd(fdi, fdo); + switch (status) { + case COPY_READ_ERROR: + error("copy-fd: read returned %s", strerror(errno)); + break; + case COPY_WRITE_ERROR: + error("copy-fd: write returned %s", strerror(errno)); + break; + } close(fdi); if (close(fdo) != 0) return error("%s: close error: %s", dst, strerror(errno)); @@ -1166,15 +1166,6 @@ static struct credentials *prepare_credentials(const char *user_name, } #endif -static void store_pid(const char *path) -{ - FILE *f = fopen(path, "w"); - if (!f) - die_errno("cannot open pid file '%s'", path); - if (fprintf(f, "%"PRIuMAX"\n", (uintmax_t) getpid()) < 0 || fclose(f) != 0) - die_errno("failed to write pid file '%s'", path); -} - static int serve(struct string_list *listen_addr, int listen_port, struct credentials *cred) { @@ -1385,7 +1376,7 @@ int main(int argc, char **argv) sanitize_stdfds(); if (pid_file) - store_pid(pid_file); + write_file(pid_file, 1, "%"PRIuMAX"\n", (uintmax_t) getpid()); /* prepare argv for serving-processes */ cld_argv = xmalloc(sizeof (char *) * (argc + 2)); diff --git a/diff-lib.c b/diff-lib.c index a85c4971ac..241a8435eb 100644 --- a/diff-lib.c +++ b/diff-lib.c @@ -125,7 +125,7 @@ int run_diff_files(struct rev_info *revs, unsigned int option) dpath->next = NULL; memcpy(dpath->path, ce->name, path_len); dpath->path[path_len] = '\0'; - hashclr(dpath->sha1); + oidclr(&dpath->oid); memset(&(dpath->parent[0]), 0, sizeof(struct combine_diff_parent)*5); @@ -155,7 +155,7 @@ int run_diff_files(struct rev_info *revs, unsigned int option) if (2 <= stage) { int mode = nce->ce_mode; num_compare_stages++; - hashcpy(dpath->parent[stage-2].sha1, nce->sha1); + hashcpy(dpath->parent[stage-2].oid.hash, nce->sha1); dpath->parent[stage-2].mode = ce_mode_from_stat(nce, mode); dpath->parent[stage-2].status = DIFF_STATUS_MODIFIED; @@ -339,14 +339,14 @@ static int show_modified(struct rev_info *revs, memcpy(p->path, new->name, pathlen); p->path[pathlen] = 0; p->mode = mode; - hashclr(p->sha1); + oidclr(&p->oid); memset(p->parent, 0, 2 * sizeof(struct combine_diff_parent)); p->parent[0].status = DIFF_STATUS_MODIFIED; p->parent[0].mode = new->ce_mode; - hashcpy(p->parent[0].sha1, new->sha1); + hashcpy(p->parent[0].oid.hash, new->sha1); p->parent[1].status = DIFF_STATUS_MODIFIED; p->parent[1].mode = old->ce_mode; - hashcpy(p->parent[1].sha1, old->sha1); + hashcpy(p->parent[1].oid.hash, old->sha1); show_combined_diff(p, 2, revs->dense_combined_merges, revs); free(p); return 0; @@ -478,26 +478,59 @@ static int new_blank_line_at_eof(struct emit_callback *ecbdata, const char *line return ws_blank_line(line, len, ecbdata->ws_rule); } -static void emit_add_line(const char *reset, - struct emit_callback *ecbdata, - const char *line, int len) +static void emit_line_checked(const char *reset, + struct emit_callback *ecbdata, + const char *line, int len, + enum color_diff color, + unsigned ws_error_highlight, + char sign) { - const char *ws = diff_get_color(ecbdata->color_diff, DIFF_WHITESPACE); - const char *set = diff_get_color(ecbdata->color_diff, DIFF_FILE_NEW); + const char *set = diff_get_color(ecbdata->color_diff, color); + const char *ws = NULL; - if (!*ws) - emit_line_0(ecbdata->opt, set, reset, '+', line, len); - else if (new_blank_line_at_eof(ecbdata, line, len)) + if (ecbdata->opt->ws_error_highlight & ws_error_highlight) { + ws = diff_get_color(ecbdata->color_diff, DIFF_WHITESPACE); + if (!*ws) + ws = NULL; + } + + if (!ws) + emit_line_0(ecbdata->opt, set, reset, sign, line, len); + else if (sign == '+' && new_blank_line_at_eof(ecbdata, line, len)) /* Blank line at EOF - paint '+' as well */ - emit_line_0(ecbdata->opt, ws, reset, '+', line, len); + emit_line_0(ecbdata->opt, ws, reset, sign, line, len); else { /* Emit just the prefix, then the rest. */ - emit_line_0(ecbdata->opt, set, reset, '+', "", 0); + emit_line_0(ecbdata->opt, set, reset, sign, "", 0); ws_check_emit(line, len, ecbdata->ws_rule, ecbdata->opt->file, set, reset, ws); } } +static void emit_add_line(const char *reset, + struct emit_callback *ecbdata, + const char *line, int len) +{ + emit_line_checked(reset, ecbdata, line, len, + DIFF_FILE_NEW, WSEH_NEW, '+'); +} + +static void emit_del_line(const char *reset, + struct emit_callback *ecbdata, + const char *line, int len) +{ + emit_line_checked(reset, ecbdata, line, len, + DIFF_FILE_OLD, WSEH_OLD, '-'); +} + +static void emit_context_line(const char *reset, + struct emit_callback *ecbdata, + const char *line, int len) +{ + emit_line_checked(reset, ecbdata, line, len, + DIFF_CONTEXT, WSEH_CONTEXT, ' '); +} + static void emit_hunk_header(struct emit_callback *ecbdata, const char *line, int len) { @@ -603,7 +636,6 @@ static void emit_rewrite_lines(struct emit_callback *ecb, { const char *endp = NULL; static const char *nneof = " No newline at end of file\n"; - const char *old = diff_get_color(ecb->color_diff, DIFF_FILE_OLD); const char *reset = diff_get_color(ecb->color_diff, DIFF_RESET); while (0 < size) { @@ -613,8 +645,7 @@ static void emit_rewrite_lines(struct emit_callback *ecb, len = endp ? (endp - data + 1) : size; if (prefix != '+') { ecb->lno_in_preimage++; - emit_line_0(ecb->opt, old, reset, '-', - data, len); + emit_del_line(reset, ecb, data, len); } else { ecb->lno_in_postimage++; emit_add_line(reset, ecb, data, len); @@ -1250,17 +1281,27 @@ static void fn_out_consume(void *priv, char *line, unsigned long len) return; } - if (line[0] != '+') { - const char *color = - diff_get_color(ecbdata->color_diff, - line[0] == '-' ? DIFF_FILE_OLD : DIFF_CONTEXT); - ecbdata->lno_in_preimage++; - if (line[0] == ' ') - ecbdata->lno_in_postimage++; - emit_line(ecbdata->opt, color, reset, line, len); - } else { + switch (line[0]) { + case '+': ecbdata->lno_in_postimage++; emit_add_line(reset, ecbdata, line + 1, len - 1); + break; + case '-': + ecbdata->lno_in_preimage++; + emit_del_line(reset, ecbdata, line + 1, len - 1); + break; + case ' ': + ecbdata->lno_in_postimage++; + ecbdata->lno_in_preimage++; + emit_context_line(reset, ecbdata, line + 1, len - 1); + break; + default: + /* incomplete line at the end */ + ecbdata->lno_in_preimage++; + emit_line(ecbdata->opt, + diff_get_color(ecbdata->color_diff, DIFF_CONTEXT), + reset, line, len); + break; } } @@ -3223,6 +3264,7 @@ void diff_setup(struct diff_options *options) options->rename_limit = -1; options->dirstat_permille = diff_dirstat_permille_default; options->context = diff_context_default; + options->ws_error_highlight = WSEH_NEW; DIFF_OPT_SET(options, RENAME_EMPTY); /* pathchange left =NULL by default */ @@ -3609,6 +3651,40 @@ static void enable_patch_output(int *fmt) { *fmt |= DIFF_FORMAT_PATCH; } +static int parse_one_token(const char **arg, const char *token) +{ + return skip_prefix(*arg, token, arg) && (!**arg || **arg == ','); +} + +static int parse_ws_error_highlight(struct diff_options *opt, const char *arg) +{ + const char *orig_arg = arg; + unsigned val = 0; + while (*arg) { + if (parse_one_token(&arg, "none")) + val = 0; + else if (parse_one_token(&arg, "default")) + val = WSEH_NEW; + else if (parse_one_token(&arg, "all")) + val = WSEH_NEW | WSEH_OLD | WSEH_CONTEXT; + else if (parse_one_token(&arg, "new")) + val |= WSEH_NEW; + else if (parse_one_token(&arg, "old")) + val |= WSEH_OLD; + else if (parse_one_token(&arg, "context")) + val |= WSEH_CONTEXT; + else { + error("unknown value after ws-error-highlight=%.*s", + (int)(arg - orig_arg), orig_arg); + return 0; + } + if (*arg) + arg++; + } + opt->ws_error_highlight = val; + return 1; +} + int diff_opt_parse(struct diff_options *options, const char **av, int ac) { const char *arg = av[0]; @@ -3806,6 +3882,8 @@ int diff_opt_parse(struct diff_options *options, const char **av, int ac) DIFF_OPT_SET(options, SUBMODULE_LOG); else if (skip_prefix(arg, "--submodule=", &arg)) return parse_submodule_opt(options, arg); + else if (skip_prefix(arg, "--ws-error-highlight=", &arg)) + return parse_ws_error_highlight(options, arg); /* misc options */ else if (!strcmp(arg, "-z")) @@ -6,6 +6,7 @@ #include "tree-walk.h" #include "pathspec.h" +#include "object.h" struct rev_info; struct diff_options; @@ -137,6 +138,11 @@ struct diff_options { int dirstat_permille; int setup; int abbrev; +/* white-space error highlighting */ +#define WSEH_NEW 1 +#define WSEH_CONTEXT 2 +#define WSEH_OLD 4 + unsigned ws_error_highlight; const char *prefix; int prefix_length; const char *stat_sep; @@ -207,11 +213,11 @@ struct combine_diff_path { struct combine_diff_path *next; char *path; unsigned int mode; - unsigned char sha1[20]; + struct object_id oid; struct combine_diff_parent { char status; unsigned int mode; - unsigned char sha1[20]; + struct object_id oid; } parent[FLEX_ARRAY]; }; #define combine_diff_path_size(n, l) \ @@ -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); @@ -508,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; @@ -527,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; } @@ -571,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; @@ -589,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); @@ -602,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; } @@ -613,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; @@ -636,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) { @@ -653,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) @@ -835,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]; @@ -872,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) { @@ -884,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); @@ -910,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 @@ -923,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; @@ -1105,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) { @@ -1132,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); } /* @@ -1248,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) @@ -1300,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: @@ -1308,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); @@ -1324,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; + } } /* @@ -1340,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; } @@ -1389,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); @@ -1467,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) { @@ -1480,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() @@ -1506,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; } @@ -1676,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) @@ -1733,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/environment.c b/environment.c index a40044c3bf..61c685b8d9 100644 --- a/environment.c +++ b/environment.c @@ -92,8 +92,9 @@ static char *work_tree; static const char *namespace; static size_t namespace_len; -static const char *git_dir; +static const char *git_dir, *git_common_dir; static char *git_object_dir, *git_index_file, *git_graft_file; +int git_db_env, git_index_env, git_graft_env, git_common_dir_env; /* * Repository-local GIT_* environment variables; see cache.h for details. @@ -111,6 +112,7 @@ const char * const local_repo_env[] = { NO_REPLACE_OBJECTS_ENVIRONMENT, GIT_PREFIX_ENVIRONMENT, GIT_SHALLOW_FILE_ENVIRONMENT, + GIT_COMMON_DIR_ENVIRONMENT, NULL }; @@ -135,14 +137,23 @@ static char *expand_namespace(const char *raw_namespace) return strbuf_detach(&buf, NULL); } -static char *git_path_from_env(const char *envvar, const char *path) +static char *git_path_from_env(const char *envvar, const char *git_dir, + const char *path, int *fromenv) { const char *value = getenv(envvar); - return value ? xstrdup(value) : git_pathdup("%s", path); + if (!value) { + char *buf = xmalloc(strlen(git_dir) + strlen(path) + 2); + sprintf(buf, "%s/%s", git_dir, path); + return buf; + } + if (fromenv) + *fromenv = 1; + return xstrdup(value); } static void setup_git_env(void) { + struct strbuf sb = STRBUF_INIT; const char *gitfile; const char *shallow_file; @@ -151,9 +162,15 @@ static void setup_git_env(void) git_dir = DEFAULT_GIT_DIR_ENVIRONMENT; gitfile = read_gitfile(git_dir); git_dir = xstrdup(gitfile ? gitfile : git_dir); - git_object_dir = git_path_from_env(DB_ENVIRONMENT, "objects"); - git_index_file = git_path_from_env(INDEX_ENVIRONMENT, "index"); - git_graft_file = git_path_from_env(GRAFT_ENVIRONMENT, "info/grafts"); + if (get_common_dir(&sb, git_dir)) + git_common_dir_env = 1; + git_common_dir = strbuf_detach(&sb, NULL); + git_object_dir = git_path_from_env(DB_ENVIRONMENT, git_common_dir, + "objects", &git_db_env); + git_index_file = git_path_from_env(INDEX_ENVIRONMENT, git_dir, + "index", &git_index_env); + git_graft_file = git_path_from_env(GRAFT_ENVIRONMENT, git_common_dir, + "info/grafts", &git_graft_env); if (getenv(NO_REPLACE_OBJECTS_ENVIRONMENT)) check_replace_refs = 0; namespace = expand_namespace(getenv(GIT_NAMESPACE_ENVIRONMENT)); @@ -176,6 +193,11 @@ const char *get_git_dir(void) return git_dir; } +const char *get_git_common_dir(void) +{ + return git_common_dir; +} + const char *get_git_namespace(void) { if (!namespace) diff --git a/ewah/bitmap.c b/ewah/bitmap.c index 710e58c1bf..47ad6747c4 100644 --- a/ewah/bitmap.c +++ b/ewah/bitmap.c @@ -20,8 +20,8 @@ #include "git-compat-util.h" #include "ewok.h" -#define MASK(x) ((eword_t)1 << (x % BITS_IN_WORD)) -#define BLOCK(x) (x / BITS_IN_WORD) +#define EWAH_MASK(x) ((eword_t)1 << (x % BITS_IN_EWORD)) +#define EWAH_BLOCK(x) (x / BITS_IN_EWORD) struct bitmap *bitmap_new(void) { @@ -33,7 +33,7 @@ struct bitmap *bitmap_new(void) void bitmap_set(struct bitmap *self, size_t pos) { - size_t block = BLOCK(pos); + size_t block = EWAH_BLOCK(pos); if (block >= self->word_alloc) { size_t old_size = self->word_alloc; @@ -45,22 +45,22 @@ void bitmap_set(struct bitmap *self, size_t pos) (self->word_alloc - old_size) * sizeof(eword_t)); } - self->words[block] |= MASK(pos); + self->words[block] |= EWAH_MASK(pos); } void bitmap_clear(struct bitmap *self, size_t pos) { - size_t block = BLOCK(pos); + size_t block = EWAH_BLOCK(pos); if (block < self->word_alloc) - self->words[block] &= ~MASK(pos); + self->words[block] &= ~EWAH_MASK(pos); } int bitmap_get(struct bitmap *self, size_t pos) { - size_t block = BLOCK(pos); + size_t block = EWAH_BLOCK(pos); return block < self->word_alloc && - (self->words[block] & MASK(pos)) != 0; + (self->words[block] & EWAH_MASK(pos)) != 0; } struct ewah_bitmap *bitmap_to_ewah(struct bitmap *bitmap) @@ -127,7 +127,7 @@ void bitmap_and_not(struct bitmap *self, struct bitmap *other) void bitmap_or_ewah(struct bitmap *self, struct ewah_bitmap *other) { size_t original_size = self->word_alloc; - size_t other_final = (other->bit_size / BITS_IN_WORD) + 1; + size_t other_final = (other->bit_size / BITS_IN_EWORD) + 1; size_t i = 0; struct ewah_iterator it; eword_t word; @@ -155,17 +155,17 @@ void bitmap_each_bit(struct bitmap *self, ewah_callback callback, void *data) uint32_t offset; if (word == (eword_t)~0) { - for (offset = 0; offset < BITS_IN_WORD; ++offset) + for (offset = 0; offset < BITS_IN_EWORD; ++offset) callback(pos++, data); } else { - for (offset = 0; offset < BITS_IN_WORD; ++offset) { + for (offset = 0; offset < BITS_IN_EWORD; ++offset) { if ((word >> offset) == 0) break; offset += ewah_bit_ctz64(word >> offset); callback(pos + offset, data); } - pos += BITS_IN_WORD; + pos += BITS_IN_EWORD; } } } diff --git a/ewah/ewah_bitmap.c b/ewah/ewah_bitmap.c index fccb42b52c..b522437c0a 100644 --- a/ewah/ewah_bitmap.c +++ b/ewah/ewah_bitmap.c @@ -102,7 +102,7 @@ size_t ewah_add_empty_words(struct ewah_bitmap *self, int v, size_t number) if (number == 0) return 0; - self->bit_size += number * BITS_IN_WORD; + self->bit_size += number * BITS_IN_EWORD; return add_empty_words(self, v, number); } @@ -152,7 +152,7 @@ void ewah_add_dirty_words( self->buffer_size += can_add; } - self->bit_size += can_add * BITS_IN_WORD; + self->bit_size += can_add * BITS_IN_EWORD; if (number - can_add == 0) break; @@ -197,7 +197,7 @@ static size_t add_empty_word(struct ewah_bitmap *self, int v) size_t ewah_add(struct ewah_bitmap *self, eword_t word) { - self->bit_size += BITS_IN_WORD; + self->bit_size += BITS_IN_EWORD; if (word == 0) return add_empty_word(self, 0); @@ -211,8 +211,8 @@ size_t ewah_add(struct ewah_bitmap *self, eword_t word) void ewah_set(struct ewah_bitmap *self, size_t i) { const size_t dist = - (i + BITS_IN_WORD) / BITS_IN_WORD - - (self->bit_size + BITS_IN_WORD - 1) / BITS_IN_WORD; + (i + BITS_IN_EWORD) / BITS_IN_EWORD - + (self->bit_size + BITS_IN_EWORD - 1) / BITS_IN_EWORD; assert(i >= self->bit_size); @@ -222,19 +222,19 @@ void ewah_set(struct ewah_bitmap *self, size_t i) if (dist > 1) add_empty_words(self, 0, dist - 1); - add_literal(self, (eword_t)1 << (i % BITS_IN_WORD)); + add_literal(self, (eword_t)1 << (i % BITS_IN_EWORD)); return; } if (rlw_get_literal_words(self->rlw) == 0) { rlw_set_running_len(self->rlw, rlw_get_running_len(self->rlw) - 1); - add_literal(self, (eword_t)1 << (i % BITS_IN_WORD)); + add_literal(self, (eword_t)1 << (i % BITS_IN_EWORD)); return; } self->buffer[self->buffer_size - 1] |= - ((eword_t)1 << (i % BITS_IN_WORD)); + ((eword_t)1 << (i % BITS_IN_EWORD)); /* check if we just completed a stream of 1s */ if (self->buffer[self->buffer_size - 1] == (eword_t)(~0)) { @@ -255,11 +255,11 @@ void ewah_each_bit(struct ewah_bitmap *self, void (*callback)(size_t, void*), vo eword_t *word = &self->buffer[pointer]; if (rlw_get_run_bit(word)) { - size_t len = rlw_get_running_len(word) * BITS_IN_WORD; + size_t len = rlw_get_running_len(word) * BITS_IN_EWORD; for (k = 0; k < len; ++k, ++pos) callback(pos, payload); } else { - pos += rlw_get_running_len(word) * BITS_IN_WORD; + pos += rlw_get_running_len(word) * BITS_IN_EWORD; } ++pointer; @@ -268,7 +268,7 @@ void ewah_each_bit(struct ewah_bitmap *self, void (*callback)(size_t, void*), vo int c; /* todo: zero count optimization */ - for (c = 0; c < BITS_IN_WORD; ++c, ++pos) { + for (c = 0; c < BITS_IN_EWORD; ++c, ++pos) { if ((self->buffer[pointer] & ((eword_t)1 << c)) != 0) callback(pos, payload); } 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..6e2c5e1e3d 100644 --- a/ewah/ewok.h +++ b/ewah/ewok.h @@ -30,8 +30,9 @@ # define ewah_calloc xcalloc #endif +struct strbuf; typedef uint64_t eword_t; -#define BITS_IN_WORD (sizeof(eword_t) * 8) +#define BITS_IN_EWORD (sizeof(eword_t) * 8) /** * Do not use __builtin_popcountll. The GCC implementation @@ -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/fast-import.c b/fast-import.c index e78ca107b3..6378726993 100644 --- a/fast-import.c +++ b/fast-import.c @@ -405,7 +405,7 @@ static void dump_marks_helper(FILE *, uintmax_t, struct mark_set *); static void write_crash_report(const char *err) { - char *loc = git_path("fast_import_crash_%"PRIuMAX, (uintmax_t) getpid()); + const char *loc = git_path("fast_import_crash_%"PRIuMAX, (uintmax_t) getpid()); FILE *rpt = fopen(loc, "w"); struct branch *b; unsigned long lu; @@ -3113,12 +3113,9 @@ static void parse_progress(void) static char* make_fast_import_path(const char *path) { - struct strbuf abs_path = STRBUF_INIT; - if (!relative_marks_paths || is_absolute_path(path)) return xstrdup(path); - strbuf_addf(&abs_path, "%s/info/fast-import/%s", get_git_dir(), path); - return strbuf_detach(&abs_path, NULL); + return xstrdup(git_path("info/fast-import/%s", path)); } static void option_import_marks(const char *marks, diff --git a/fetch-pack.c b/fetch-pack.c index 48526aa54b..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) { @@ -60,7 +65,7 @@ static void rev_list_push(struct commit *commit, int mark) } } -static int rev_list_insert_ref(const char *refname, const unsigned char *sha1, int flag, void *cb_data) +static int rev_list_insert_ref(const char *refname, const unsigned char *sha1) { struct object *o = deref_tag(parse_object(sha1), refname, 0); @@ -70,9 +75,16 @@ static int rev_list_insert_ref(const char *refname, const unsigned char *sha1, i return 0; } -static int clear_marks(const char *refname, const unsigned char *sha1, int flag, void *cb_data) +static int rev_list_insert_ref_oid(const char *refname, const struct object_id *oid, + int flag, void *cb_data) { - struct object *o = deref_tag(parse_object(sha1), refname, 0); + return rev_list_insert_ref(refname, oid->hash); +} + +static int clear_marks(const char *refname, const struct object_id *oid, + int flag, void *cb_data) +{ + struct object *o = deref_tag(parse_object(oid->hash), refname, 0); if (o && o->type == OBJ_COMMIT) clear_commit_marks((struct commit *)o, @@ -226,7 +238,7 @@ static void send_request(struct fetch_pack_args *args, static void insert_one_alternate_ref(const struct ref *ref, void *unused) { - rev_list_insert_ref(NULL, ref->old_sha1, 0, NULL); + rev_list_insert_ref(NULL, ref->old_sha1); } #define INITIAL_FLUSH 16 @@ -263,7 +275,7 @@ static int find_common(struct fetch_pack_args *args, for_each_ref(clear_marks, NULL); marked = 1; - for_each_ref(rev_list_insert_ref, NULL); + for_each_ref(rev_list_insert_ref_oid, NULL); for_each_alternate_ref(insert_one_alternate_ref, NULL); fetching = 0; @@ -466,7 +478,7 @@ done: static struct commit_list *complete; -static int mark_complete(const char *refname, const unsigned char *sha1, int flag, void *cb_data) +static int mark_complete(const unsigned char *sha1) { struct object *o = parse_object(sha1); @@ -487,6 +499,12 @@ static int mark_complete(const char *refname, const unsigned char *sha1, int fla return 0; } +static int mark_complete_oid(const char *refname, const struct object_id *oid, + int flag, void *cb_data) +{ + return mark_complete(oid->hash); +} + static void mark_recent_complete_commits(struct fetch_pack_args *args, unsigned long cutoff) { @@ -542,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]; @@ -564,7 +583,7 @@ static void filter_refs(struct fetch_pack_args *args, static void mark_alternate_complete(const struct ref *ref, void *unused) { - mark_complete(NULL, ref->old_sha1, 0, NULL); + mark_complete(ref->old_sha1); } static int everything_local(struct fetch_pack_args *args, @@ -599,7 +618,7 @@ static int everything_local(struct fetch_pack_args *args, } if (!args->depth) { - for_each_ref(mark_complete, NULL); + for_each_ref(mark_complete_oid, NULL); for_each_alternate_ref(mark_alternate_complete, NULL); commit_list_sort_by_date(&complete); if (cutoff) @@ -821,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 "};" @@ -69,6 +69,8 @@ then cmdline="$cmdline -3" fi +empty_tree=4b825dc642cb6eb9a060e54bf8d69288fbee4904 + sq () { git rev-parse --sq-quote "$@" } @@ -85,7 +87,7 @@ safe_to_abort () { return 1 fi - if ! test -s "$dotest/abort-safety" + if ! test -f "$dotest/abort-safety" then return 0 fi @@ -177,7 +179,8 @@ It does not apply to blobs recorded in its index.")" then GIT_MERGE_VERBOSITY=0 && export GIT_MERGE_VERBOSITY fi - git-merge-recursive $orig_tree -- HEAD $his_tree || { + our_tree=$(git rev-parse --verify -q HEAD || echo $empty_tree) + git-merge-recursive $orig_tree -- $our_tree $his_tree || { git rerere $allow_rerere_autoupdate die "$(gettext "Failed to merge in the changes.")" } @@ -378,6 +381,7 @@ committer_date_is_author_date= ignore_date= allow_rerere_autoupdate= gpg_sign_opt= +threeway= if test "$(git config --bool --get am.messageid)" = true then @@ -389,6 +393,11 @@ then keepcr=t fi +if test "$(git config --bool --get am.threeWay)" = true +then + threeway=t +fi + while test $# != 0 do case "$1" in @@ -400,6 +409,8 @@ it will be removed. Please do not use it anymore." ;; -3|--3way) threeway=t ;; + --no-3way) + threeway=f ;; -s|--signoff) sign=t ;; -u|--utf8) @@ -502,10 +513,11 @@ then ;; t,) git rerere clear - git read-tree --reset -u HEAD HEAD - orig_head=$(cat "$GIT_DIR/ORIG_HEAD") - git reset HEAD - git update-ref ORIG_HEAD $orig_head + head_tree=$(git rev-parse --verify -q HEAD || echo $empty_tree) && + git read-tree --reset -u $head_tree $head_tree && + index_tree=$(git write-tree) && + git read-tree -m -u $index_tree $head_tree + git read-tree $head_tree ;; ,t) if test -f "$dotest/rebasing" @@ -515,8 +527,19 @@ then git rerere clear if safe_to_abort then - git read-tree --reset -u HEAD ORIG_HEAD - git reset ORIG_HEAD + head_tree=$(git rev-parse --verify -q HEAD || echo $empty_tree) && + git read-tree --reset -u $head_tree $head_tree && + index_tree=$(git write-tree) && + orig_head=$(git rev-parse --verify -q ORIG_HEAD || echo $empty_tree) && + git read-tree -m -u $index_tree $orig_head + if git rev-parse --verify -q ORIG_HEAD >/dev/null 2>&1 + then + git reset ORIG_HEAD + else + git read-tree $empty_tree + curr_branch=$(git symbolic-ref HEAD 2>/dev/null) && + git update-ref -d $curr_branch + fi fi rm -fr "$dotest" exit ;; @@ -657,6 +680,8 @@ fi if test "$(cat "$dotest/threeway")" = t then threeway=t +else + threeway=f fi git_apply_opt=$(cat "$dotest/apply-opt") if test "$(cat "$dotest/sign")" = t @@ -827,10 +852,10 @@ To restore the original branch and stop patching run \"\$cmdline --abort\"." continue fi - if test -x "$GIT_DIR"/hooks/applypatch-msg + hook="$(git rev-parse --git-path hooks/applypatch-msg)" + if test -x "$hook" then - "$GIT_DIR"/hooks/applypatch-msg "$dotest/final-commit" || - stop_here $this + "$hook" "$dotest/final-commit" || stop_here $this fi if test -f "$dotest/final-commit" @@ -904,9 +929,10 @@ did you forget to use 'git add'?" stop_here_user_resolve $this fi - if test -x "$GIT_DIR"/hooks/pre-applypatch + hook="$(git rev-parse --git-path hooks/pre-applypatch)" + if test -x "$hook" then - "$GIT_DIR"/hooks/pre-applypatch || stop_here $this + "$hook" || stop_here $this fi tree=$(git write-tree) && @@ -933,18 +959,17 @@ did you forget to use 'git add'?" echo "$(cat "$dotest/original-commit") $commit" >> "$dotest/rewritten" fi - if test -x "$GIT_DIR"/hooks/post-applypatch - then - "$GIT_DIR"/hooks/post-applypatch - fi + hook="$(git rev-parse --git-path hooks/post-applypatch)" + test -x "$hook" && "$hook" go_next done if test -s "$dotest"/rewritten; then git notes copy --for-rewrite=rebase < "$dotest"/rewritten - if test -x "$GIT_DIR"/hooks/post-rewrite; then - "$GIT_DIR"/hooks/post-rewrite rebase < "$dotest"/rewritten + hook="$(git rev-parse --git-path hooks/post-rewrite)" + if test -x "$hook"; then + "$hook" rebase < "$dotest"/rewritten fi fi diff --git a/git-compat-util.h b/git-compat-util.h index 3be44f146b..0cc7ae84ba 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> @@ -936,4 +937,10 @@ struct tm *git_gmtime_r(const time_t *, struct tm *); # define SHELL_PATH "/bin/sh" #endif +#ifndef _POSIX_THREAD_SAFE_FUNCTIONS +#define flockfile(fh) +#define funlockfile(fh) +#define getc_unlocked(fh) getc(fh) +#endif + #endif diff --git a/git-cvsimport.perl b/git-cvsimport.perl index 73d367cea8..82ecb0343a 100755 --- a/git-cvsimport.perl +++ b/git-cvsimport.perl @@ -1162,7 +1162,7 @@ if ($orig_branch) { die "Fast-forward update failed: $?\n" if $?; } else { - system(qw(git merge cvsimport HEAD), "$remote/$opt_o"); + system(qw(git merge -m cvsimport), "$remote/$opt_o"); die "Could not merge $opt_o into the current branch.\n" if $?; } } else { 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 @@ -43,6 +43,9 @@ verbose = False # Only labels/tags matching this will be imported/exported defaultLabelRegexp = r'[a-zA-Z0-9_\-.]+$' +# Grab changes in blocks of this many revisions, unless otherwise requested +defaultBlockSize = 512 + def p4_build_cmd(cmd): """Build a suitable p4 command line. @@ -249,6 +252,10 @@ def p4_reopen(type, f): def p4_move(src, dest): p4_system(["move", "-k", wildcard_encode(src), wildcard_encode(dest)]) +def p4_last_change(): + results = p4CmdList(["changes", "-m", "1"]) + return int(results[0]['change']) + def p4_describe(change): """Make sure it returns a valid result by checking for the presence of field "time". Return a dict of the @@ -368,7 +375,7 @@ def getP4OpenedType(file): # Returns the perforce file type for the given file. result = p4_read_pipe(["opened", wildcard_encode(file)]) - match = re.match(".*\((.+)\)\r?$", result) + match = re.match(".*\((.+)\)( \*exclusive\*)?\r?$", result) if match: return match.group(1) else: @@ -502,12 +509,14 @@ def p4Cmd(cmd): def p4Where(depotPath): if not depotPath.endswith("/"): depotPath += "/" - depotPath = depotPath + "..." - outputList = p4CmdList(["where", depotPath]) + depotPathLong = depotPath + "..." + outputList = p4CmdList(["where", depotPathLong]) output = None for entry in outputList: if "depotFile" in entry: - if entry["depotFile"] == depotPath: + # Search for the base client side depot path, as long as it starts with the branch's P4 path. + # The base path always ends with "/...". + if entry["depotFile"].find(depotPath) == 0 and entry["depotFile"][-4:] == "/...": output = entry break elif "data" in entry: @@ -740,17 +749,77 @@ def createOrUpdateBranchesFromOrigin(localRefPrefix = "refs/remotes/p4/", silent def originP4BranchesExist(): return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master") -def p4ChangesForPaths(depotPaths, changeRange): + +def p4ParseNumericChangeRange(parts): + changeStart = int(parts[0][1:]) + if parts[1] == '#head': + changeEnd = p4_last_change() + else: + changeEnd = int(parts[1]) + + return (changeStart, changeEnd) + +def chooseBlockSize(blockSize): + if blockSize: + return blockSize + else: + return defaultBlockSize + +def p4ChangesForPaths(depotPaths, changeRange, requestedBlockSize): assert depotPaths - cmd = ['changes'] - for p in depotPaths: - cmd += ["%s...%s" % (p, changeRange)] - output = p4_read_pipe_lines(cmd) + # Parse the change range into start and end. Try to find integer + # revision ranges as these can be broken up into blocks to avoid + # hitting server-side limits (maxrows, maxscanresults). But if + # that doesn't work, fall back to using the raw revision specifier + # strings, without using block mode. + + if changeRange is None or changeRange == '': + changeStart = 1 + changeEnd = p4_last_change() + block_size = chooseBlockSize(requestedBlockSize) + else: + parts = changeRange.split(',') + assert len(parts) == 2 + try: + (changeStart, changeEnd) = p4ParseNumericChangeRange(parts) + block_size = chooseBlockSize(requestedBlockSize) + except: + changeStart = parts[0][1:] + changeEnd = parts[1] + if requestedBlockSize: + die("cannot use --changes-block-size with non-numeric revisions") + block_size = None + + # Accumulate change numbers in a dictionary to avoid duplicates changes = {} - for line in output: - changeNum = int(line.split(" ")[1]) - changes[changeNum] = True + + for p in depotPaths: + # Retrieve changes a block at a time, to prevent running + # into a MaxResults/MaxScanRows error from the server. + + while True: + cmd = ['changes'] + + if block_size: + end = min(changeEnd, changeStart + block_size) + revisionRange = "%d,%d" % (changeStart, end) + else: + revisionRange = "%s,%s" % (changeStart, changeEnd) + + cmd += ["%s...@%s" % (p, revisionRange)] + + for line in p4_read_pipe_lines(cmd): + changeNum = int(line.split(" ")[1]) + changes[changeNum] = True + + if not block_size: + break + + if end >= changeEnd: + break + + changeStart = end + 1 changelist = changes.keys() changelist.sort() @@ -1220,7 +1289,7 @@ class P4Submit(Command, P4UserMap): editor = os.environ.get("P4EDITOR") else: editor = read_pipe("git var GIT_EDITOR").strip() - system([editor, template_file]) + system(["sh", "-c", ('%s "$@"' % editor), editor, template_file]) # If the file was not saved, prompt to see if this patch should # be skipped. But skip this verification step if configured so. @@ -1627,7 +1696,10 @@ class P4Submit(Command, P4UserMap): if self.useClientSpec: self.clientSpecDirs = getClientSpec() - if self.useClientSpec: + # Check for the existance of P4 branches + branchesDetected = (len(p4BranchesInGit().keys()) > 1) + + if self.useClientSpec and not branchesDetected: # all files are relative to the client spec self.clientPath = getClientRoot() else: @@ -1911,7 +1983,10 @@ class P4Sync(Command, P4UserMap): optparse.make_option("--import-labels", dest="importLabels", action="store_true"), optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false", help="Import into refs/heads/ , not refs/remotes"), - optparse.make_option("--max-changes", dest="maxChanges"), + optparse.make_option("--max-changes", dest="maxChanges", + help="Maximum number of changes to import"), + optparse.make_option("--changes-block-size", dest="changes_block_size", type="int", + help="Internal block size to use when iteratively calling p4 changes"), optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true', help="Keep entire BRANCH/DIR/SUBDIR prefix during import"), optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true', @@ -1940,6 +2015,7 @@ class P4Sync(Command, P4UserMap): self.syncWithOrigin = True self.importIntoRemotes = True self.maxChanges = "" + self.changes_block_size = None self.keepRepoPath = False self.depotPaths = None self.p4BranchesInGit = [] @@ -2110,7 +2186,7 @@ class P4Sync(Command, P4UserMap): # them back too. This is not needed to the cygwin windows version, # just the native "NT" type. # - text = p4_read_pipe(['print', '-q', '-o', '-', file['depotFile']]) + text = p4_read_pipe(['print', '-q', '-o', '-', "%s@%s" % (file['depotFile'], file['change']) ]) if p4_version_string().find("/NT") >= 0: text = text.replace("\r\n", "\n") contents = [ text ] @@ -2586,7 +2662,7 @@ class P4Sync(Command, P4UserMap): branchPrefix = self.depotPaths[0] + branch + "/" range = "@1,%s" % maxChange #print "prefix" + branchPrefix - changes = p4ChangesForPaths([branchPrefix], range) + changes = p4ChangesForPaths([branchPrefix], range, self.changes_block_size) if len(changes) <= 0: return False firstChange = changes[0] @@ -3002,7 +3078,7 @@ class P4Sync(Command, P4UserMap): if self.verbose: print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths), self.changeRange) - changes = p4ChangesForPaths(self.depotPaths, self.changeRange) + changes = p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size) if len(self.maxChanges) > 0: changes = changes[:min(int(self.maxChanges), len(changes))] diff --git a/git-pull.sh b/git-pull.sh index 23781e5611..a814bf61aa 100755 --- a/git-pull.sh +++ b/git-pull.sh @@ -4,13 +4,53 @@ # # Fetch one or more remote refs and merge it/them into the current HEAD. -USAGE='[-n | --no-stat] [--[no-]commit] [--[no-]squash] [--[no-]ff|--ff-only] [--[no-]rebase|--rebase=preserve] [-s strategy]... [<fetch-options>] <repo> <head>...' -LONG_USAGE='Fetch one or more remote refs and integrate it/them with the current HEAD.' SUBDIRECTORY_OK=Yes -OPTIONS_SPEC= +OPTIONS_KEEPDASHDASH= +OPTIONS_STUCKLONG=Yes +OPTIONS_SPEC="\ +git pull [options] [<repository> [<refspec>...]] + +Fetch one or more remote refs and integrate it/them with the current HEAD. +-- +v,verbose be more verbose +q,quiet be more quiet +progress force progress reporting + + Options related to merging +r,rebase?false|true|preserve incorporate changes by rebasing rather than merging +n! do not show a diffstat at the end of the merge +stat show a diffstat at the end of the merge +summary (synonym to --stat) +log?n add (at most <n>) entries from shortlog to merge commit message +squash create a single commit instead of doing a merge +commit perform a commit if the merge succeeds (default) +e,edit edit message before committing +ff allow fast-forward +ff-only! abort if fast-forward is not possible +verify-signatures verify that the named commit has a valid GPG signature +s,strategy=strategy merge strategy to use +X,strategy-option=option option for selected merge strategy +S,gpg-sign?key-id GPG sign commit + + Options related to fetching +all fetch from all remotes +a,append append to .git/FETCH_HEAD instead of overwriting +upload-pack=path path to upload pack on remote end +f,force force overwrite of local branch +t,tags fetch all tags and associated objects +p,prune prune remote-tracking branches no longer on remote +recurse-submodules?on-demand control recursive fetching of submodules +dry-run dry run +k,keep keep downloaded pack +depth=depth deepen history of shallow clone +unshallow convert to a complete repository +update-shallow accept refs that update .git/shallow +refmap=refmap specify fetch refmap +" +test $# -gt 0 && args="$*" . git-sh-setup . git-sh-i18n -set_reflog_action "pull${1+ $*}" +set_reflog_action "pull${args+ $args}" require_work_tree_exists cd_to_toplevel @@ -44,7 +84,8 @@ bool_or_string_config () { strategy_args= diffstat= no_commit= squash= no_ff= ff_only= log_arg= verbosity= progress= recurse_submodules= verify_signatures= -merge_args= edit= rebase_args= +merge_args= edit= rebase_args= all= append= upload_pack= force= tags= prune= +keep= depth= unshallow= update_shallow= refmap= curr_branch=$(git symbolic-ref -q HEAD) curr_branch_short="${curr_branch#refs/heads/}" rebase=$(bool_or_string_config branch.$curr_branch_short.rebase) @@ -86,17 +127,17 @@ do diffstat=--stat ;; --log|--log=*|--no-log) log_arg="$1" ;; - --no-c|--no-co|--no-com|--no-comm|--no-commi|--no-commit) + --no-commit) no_commit=--no-commit ;; - --c|--co|--com|--comm|--commi|--commit) + --commit) no_commit=--commit ;; -e|--edit) edit=--edit ;; --no-edit) edit=--no-edit ;; - --sq|--squ|--squa|--squas|--squash) + --squash) squash=--squash ;; - --no-sq|--no-squ|--no-squa|--no-squas|--no-squash) + --no-squash) squash=--no-squash ;; --ff) no_ff=--ff ;; @@ -104,39 +145,19 @@ do no_ff=--no-ff ;; --ff-only) ff_only=--ff-only ;; - -s=*|--s=*|--st=*|--str=*|--stra=*|--strat=*|--strate=*|\ - --strateg=*|--strategy=*|\ - -s|--s|--st|--str|--stra|--strat|--strate|--strateg|--strategy) - case "$#,$1" in - *,*=*) - strategy=$(expr "z$1" : 'z-[^=]*=\(.*\)') ;; - 1,*) - usage ;; - *) - strategy="$2" - shift ;; - esac - strategy_args="${strategy_args}-s $strategy " + -s*|--strategy=*) + strategy_args="$strategy_args $1" ;; - -X*) - case "$#,$1" in - 1,-X) - usage ;; - *,-X) - xx="-X $(git rev-parse --sq-quote "$2")" - shift ;; - *,*) - xx=$(git rev-parse --sq-quote "$1") ;; - esac - merge_args="$merge_args$xx " + -X*|--strategy-option=*) + merge_args="$merge_args $(git rev-parse --sq-quote "$1")" ;; - -r=*|--r=*|--re=*|--reb=*|--reba=*|--rebas=*|--rebase=*) + -r*|--rebase=*) rebase="${1#*=}" ;; - -r|--r|--re|--reb|--reba|--rebas|--rebase) + --rebase) rebase=true ;; - --no-r|--no-re|--no-reb|--no-reba|--no-rebas|--no-rebase) + --no-rebase) rebase=false ;; --recurse-submodules) @@ -163,16 +184,41 @@ do -S*) gpg_sign_args=$(git rev-parse --sq-quote "$1") ;; - --d|--dr|--dry|--dry-|--dry-r|--dry-ru|--dry-run) + --dry-run) dry_run=--dry-run ;; + --all|--no-all) + all=$1 ;; + -a|--append|--no-append) + append=$1 ;; + --upload-pack=*|--no-upload-pack) + upload_pack=$1 ;; + -f|--force|--no-force) + force="$force $1" ;; + -t|--tags|--no-tags) + tags=$1 ;; + -p|--prune|--no-prune) + prune=$1 ;; + -k|--keep|--no-keep) + keep=$1 ;; + --depth=*|--no-depth) + depth=$1 ;; + --unshallow|--no-unshallow) + unshallow=$1 ;; + --update-shallow|--no-update-shallow) + update_shallow=$1 ;; + --refmap=*|--no-refmap) + refmap=$1 ;; -h|--help-all) usage ;; - *) - # Pass thru anything that may be meant for fetch. + --) + shift break ;; + *) + usage + ;; esac shift done @@ -234,7 +280,7 @@ test true = "$rebase" && { if ! git rev-parse -q --verify HEAD >/dev/null then # On an unborn branch - if test -f "$GIT_DIR/index" + if test -f "$(git rev-parse --git-path index)" then die "$(gettext "updating an unborn branch with changes added to the index")" fi @@ -248,7 +294,9 @@ test true = "$rebase" && { oldremoteref=$(git merge-base --fork-point "$remoteref" $curr_branch 2>/dev/null) } orig_head=$(git rev-parse -q --verify HEAD) -git fetch $verbosity $progress $dry_run $recurse_submodules --update-head-ok "$@" || exit 1 +git fetch $verbosity $progress $dry_run $recurse_submodules $all $append \ +$upload_pack $force $tags $prune $keep $depth $unshallow $update_shallow \ +$refmap --update-head-ok "$@" || exit 1 test -z "$dry_run" || exit 0 curr_head=$(git rev-parse -q --verify HEAD) @@ -317,7 +365,6 @@ then fi fi -merge_name=$(git fmt-merge-msg $log_arg <"$GIT_DIR/FETCH_HEAD") || exit case "$rebase" in true) eval="git-rebase $diffstat $strategy_args $merge_args $rebase_args $verbosity" @@ -328,7 +375,7 @@ true) eval="git-merge $diffstat $no_commit $verify_signatures $edit $squash $no_ff $ff_only" eval="$eval $log_arg $strategy_args $merge_args $verbosity $progress" eval="$eval $gpg_sign_args" - eval="$eval \"\$merge_name\" HEAD $merge_head" + eval="$eval FETCH_HEAD" ;; esac eval "exec $eval" diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh index 8c5de4c1eb..dc3133f681 100644 --- a/git-rebase--interactive.sh +++ b/git-rebase--interactive.sh @@ -655,9 +655,9 @@ do_next () { git notes copy --for-rewrite=rebase < "$rewritten_list" || true # we don't care if this copying failed } && - if test -x "$GIT_DIR"/hooks/post-rewrite && - test -s "$rewritten_list"; then - "$GIT_DIR"/hooks/post-rewrite rebase < "$rewritten_list" + hook="$(git rev-parse --git-path hooks/post-rewrite)" + if test -x "$hook" && test -s "$rewritten_list"; then + "$hook" rebase < "$rewritten_list" true # we don't care if this hook failed fi && warn "Successfully rebased and updated $head_name." diff --git a/git-rebase--merge.sh b/git-rebase--merge.sh index d3fb67d75b..2cc2a6d273 100644 --- a/git-rebase--merge.sh +++ b/git-rebase--merge.sh @@ -94,10 +94,8 @@ finish_rb_merge () { if test -s "$state_dir"/rewritten then git notes copy --for-rewrite=rebase <"$state_dir"/rewritten - if test -x "$GIT_DIR"/hooks/post-rewrite - then - "$GIT_DIR"/hooks/post-rewrite rebase <"$state_dir"/rewritten - fi + hook="$(git rev-parse --git-path hooks/post-rewrite)" + test -x "$hook" && "$hook" rebase <"$state_dir"/rewritten fi say All done. } diff --git a/git-rebase.sh b/git-rebase.sh index 90854e38cb..1757404bc2 100755 --- a/git-rebase.sh +++ b/git-rebase.sh @@ -202,9 +202,9 @@ run_specific_rebase () { run_pre_rebase_hook () { if test -z "$ok_to_skip_pre_rebase" && - test -x "$GIT_DIR/hooks/pre-rebase" + test -x "$(git rev-parse --git-path hooks/pre-rebase)" then - "$GIT_DIR/hooks/pre-rebase" ${1+"$@"} || + "$(git rev-parse --git-path hooks/pre-rebase)" ${1+"$@"} || die "$(gettext "The pre-rebase hook refused to rebase.")" fi } diff --git a/git-send-email.perl b/git-send-email.perl index e1e9b1460c..ae9f8698c5 100755 --- a/git-send-email.perl +++ b/git-send-email.perl @@ -487,6 +487,37 @@ sub split_addrs { } my %aliases; + +sub parse_sendmail_alias { + local $_ = shift; + if (/"/) { + print STDERR "warning: sendmail alias with quotes is not supported: $_\n"; + } elsif (/:include:/) { + print STDERR "warning: `:include:` not supported: $_\n"; + } elsif (/[\/|]/) { + print STDERR "warning: `/file` or `|pipe` redirection not supported: $_\n"; + } elsif (/^(\S+?)\s*:\s*(.+)$/) { + my ($alias, $addr) = ($1, $2); + $aliases{$alias} = [ split_addrs($addr) ]; + } else { + print STDERR "warning: sendmail line is not recognized: $_\n"; + } +} + +sub parse_sendmail_aliases { + my $fh = shift; + my $s = ''; + while (<$fh>) { + chomp; + next if /^\s*$/ || /^\s*#/; + $s .= $_, next if $s =~ s/\\$// || s/^\s+//; + parse_sendmail_alias($s) if $s; + $s = $_; + } + $s =~ s/\\$//; # silently tolerate stray '\' on last line + parse_sendmail_alias($s) if $s; +} + my %parse_alias = ( # multiline formats can be supported in the future mutt => sub { my $fh = shift; while (<$fh>) { @@ -515,7 +546,7 @@ my %parse_alias = ( $aliases{$alias} = [ split_addrs($addr) ]; } } }, - + sendmail => \&parse_sendmail_aliases, gnus => sub { my $fh = shift; while (<$fh>) { if (/\(define-mail-alias\s+"(\S+?)"\s+"(\S+?)"\)/) { $aliases{$1} = [ $2 ]; diff --git a/git-sh-setup.sh b/git-sh-setup.sh index c42c6e6365..4691fbcb64 100644 --- a/git-sh-setup.sh +++ b/git-sh-setup.sh @@ -344,7 +344,7 @@ git_dir_init () { echo >&2 "Unable to determine absolute path of git directory" exit 1 } - : ${GIT_OBJECT_DIRECTORY="$GIT_DIR/objects"} + : ${GIT_OBJECT_DIRECTORY="$(git rev-parse --git-path objects)"} } if test -z "$NONGIT_OK" diff --git a/git-stash.sh b/git-stash.sh index 0fddd5424b..8e9e2cd7d5 100755 --- a/git-stash.sh +++ b/git-stash.sh @@ -20,7 +20,7 @@ require_work_tree cd_to_toplevel TMP="$GIT_DIR/.git-stash.$$" -TMPindex=${GIT_INDEX_FILE-"$GIT_DIR/index"}.stash.$$ +TMPindex=${GIT_INDEX_FILE-"$(git rev-parse --git-path index)"}.stash.$$ trap 'rm -f "$TMP-"* "$TMPindex"' 0 ref_stash=refs/stash @@ -184,7 +184,7 @@ store_stash () { fi # Make sure the reflog for stash is kept. - : >>"$GIT_DIR/logs/$ref_stash" + : >>"$(git rev-parse --git-path logs/$ref_stash)" git update-ref -m "$stash_msg" $ref_stash $w_commit ret=$? test $ret != 0 && test -z $quiet && @@ -262,7 +262,7 @@ save_stash () { say "$(gettext "No local changes to save")" exit 0 fi - test -f "$GIT_DIR/logs/$ref_stash" || + test -f "$(git rev-parse --git-path logs/$ref_stash)" || clear_stash || die "$(gettext "Cannot initialize stash")" create_stash "$stash_msg" $untracked @@ -457,8 +457,6 @@ apply_stash () { assert_stash_like "$@" git update-index -q --refresh || die "$(gettext "unable to refresh index")" - git diff-index --cached --quiet --ignore-submodules HEAD -- || - die "$(gettext "Cannot apply stash: Your index contains uncommitted changes.")" # current index state c_tree=$(git write-tree) || @@ -382,7 +382,7 @@ static struct cmd_struct commands[] = { { "check-ignore", cmd_check_ignore, RUN_SETUP | NEED_WORK_TREE }, { "check-mailmap", cmd_check_mailmap, RUN_SETUP }, { "check-ref-format", cmd_check_ref_format }, - { "checkout", cmd_checkout, RUN_SETUP | NEED_WORK_TREE }, + { "checkout", cmd_checkout, RUN_SETUP }, { "checkout-index", cmd_checkout_index, RUN_SETUP | NEED_WORK_TREE}, { "cherry", cmd_cherry, RUN_SETUP }, @@ -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)); @@ -372,7 +394,7 @@ const char *help_unknown_cmd(const char *cmd) if (autocorrect > 0) { fprintf_ln(stderr, _("in %0.1f seconds automatically..."), (float)autocorrect/10.0); - poll(NULL, 0, autocorrect * 100); + sleep_millisec(autocorrect * 100); } return assumed; } @@ -407,7 +429,7 @@ struct similar_ref_cb { struct string_list *similar_refs; }; -static int append_similar_ref(const char *refname, const unsigned char *sha1, +static int append_similar_ref(const char *refname, const struct object_id *oid, int flags, void *cb_data) { struct similar_ref_cb *cb = (struct similar_ref_cb *)(cb_data); @@ -38,7 +38,7 @@ const signed char hexval_table[256] = { int get_sha1_hex(const char *hex, unsigned char *sha1) { int i; - for (i = 0; i < 20; i++) { + for (i = 0; i < GIT_SHA1_RAWSZ; i++) { unsigned int val; /* * hex[1]=='\0' is caught when val is checked below, @@ -56,15 +56,20 @@ int get_sha1_hex(const char *hex, unsigned char *sha1) return 0; } +int get_oid_hex(const char *hex, struct object_id *oid) +{ + return get_sha1_hex(hex, oid->hash); +} + char *sha1_to_hex(const unsigned char *sha1) { static int bufno; - static char hexbuffer[4][41]; + static char hexbuffer[4][GIT_SHA1_HEXSZ + 1]; static const char hex[] = "0123456789abcdef"; char *buffer = hexbuffer[3 & ++bufno], *buf = buffer; int i; - for (i = 0; i < 20; i++) { + for (i = 0; i < GIT_SHA1_RAWSZ; i++) { unsigned int val = *sha1++; *buf++ = hex[val >> 4]; *buf++ = hex[val & 0xf]; @@ -73,3 +78,8 @@ char *sha1_to_hex(const unsigned char *sha1) return buffer; } + +char *oid_to_hex(const struct object_id *oid) +{ + return sha1_to_hex(oid->hash); +} diff --git a/http-backend.c b/http-backend.c index 6bf139b768..501bf797c0 100644 --- a/http-backend.c +++ b/http-backend.c @@ -421,16 +421,16 @@ static void run_service(const char **argv, int buffer_input) exit(1); } -static int show_text_ref(const char *name, const unsigned char *sha1, - int flag, void *cb_data) +static int show_text_ref(const char *name, const struct object_id *oid, + int flag, void *cb_data) { const char *name_nons = strip_namespace(name); struct strbuf *buf = cb_data; - struct object *o = parse_object(sha1); + struct object *o = parse_object(oid->hash); if (!o) return 0; - strbuf_addf(buf, "%s\t%s\n", sha1_to_hex(sha1), name_nons); + strbuf_addf(buf, "%s\t%s\n", oid_to_hex(oid), name_nons); if (o->type == OBJ_TAG) { o = deref_tag(o, name, 0); if (!o) @@ -473,21 +473,21 @@ static void get_info_refs(char *arg) strbuf_release(&buf); } -static int show_head_ref(const char *refname, const unsigned char *sha1, - int flag, void *cb_data) +static int show_head_ref(const char *refname, const struct object_id *oid, + int flag, void *cb_data) { struct strbuf *buf = cb_data; if (flag & REF_ISSYMREF) { - unsigned char unused[20]; + struct object_id unused; const char *target = resolve_ref_unsafe(refname, RESOLVE_REF_READING, - unused, NULL); + unused.hash, NULL); const char *target_nons = strip_namespace(target); strbuf_addf(buf, "ref: %s\n", target_nons); } else { - strbuf_addf(buf, "%s\n", sha1_to_hex(sha1)); + strbuf_addf(buf, "%s\n", oid_to_hex(oid)); } return 0; @@ -36,6 +36,7 @@ char curl_errorstr[CURL_ERROR_SIZE]; static int curl_ssl_verify = -1; static int curl_ssl_try; static const char *ssl_cert; +static const char *ssl_cipherlist; #if LIBCURL_VERSION_NUM >= 0x070903 static const char *ssl_key; #endif @@ -187,6 +188,8 @@ static int http_options(const char *var, const char *value, void *cb) curl_ssl_verify = git_config_bool(var, value); return 0; } + if (!strcmp("http.sslcipherlist", var)) + return git_config_string(&ssl_cipherlist, var, value); if (!strcmp("http.sslcert", var)) return git_config_string(&ssl_cert, var, value); #if LIBCURL_VERSION_NUM >= 0x070903 @@ -361,6 +364,13 @@ static CURL *get_curl_handle(void) if (http_proactive_auth) init_curl_http_auth(result); + if (getenv("GIT_SSL_CIPHER_LIST")) + ssl_cipherlist = getenv("GIT_SSL_CIPHER_LIST"); + + if (ssl_cipherlist != NULL && *ssl_cipherlist) + curl_easy_setopt(result, CURLOPT_SSL_CIPHER_LIST, + ssl_cipherlist); + if (ssl_cert != NULL) curl_easy_setopt(result, CURLOPT_SSLCERT, ssl_cert); if (has_cert_password()) diff --git a/ll-merge.c b/ll-merge.c index 8ea03e536a..fc3c049594 100644 --- a/ll-merge.c +++ b/ll-merge.c @@ -9,6 +9,7 @@ #include "xdiff-interface.h" #include "run-command.h" #include "ll-merge.h" +#include "quote.h" struct ll_merge_driver; @@ -166,17 +167,20 @@ static int ll_ext_merge(const struct ll_merge_driver *fn, { char temp[4][50]; struct strbuf cmd = STRBUF_INIT; - struct strbuf_expand_dict_entry dict[5]; + struct strbuf_expand_dict_entry dict[6]; + struct strbuf path_sq = STRBUF_INIT; const char *args[] = { NULL, NULL }; int status, fd, i; struct stat st; assert(opts); + sq_quote_buf(&path_sq, path); dict[0].placeholder = "O"; dict[0].value = temp[0]; dict[1].placeholder = "A"; dict[1].value = temp[1]; dict[2].placeholder = "B"; dict[2].value = temp[2]; dict[3].placeholder = "L"; dict[3].value = temp[3]; - dict[4].placeholder = NULL; dict[4].value = NULL; + dict[4].placeholder = "P"; dict[4].value = path_sq.buf; + dict[5].placeholder = NULL; dict[5].value = NULL; if (fn->cmdline == NULL) die("custom merge driver %s lacks command line.", fn->name); @@ -210,6 +214,7 @@ static int ll_ext_merge(const struct ll_merge_driver *fn, for (i = 0; i < 3; i++) unlink_or_warn(temp[i]); strbuf_release(&cmd); + strbuf_release(&path_sq); return status; } @@ -269,6 +274,7 @@ static int read_merge_config(const char *var, const char *value, void *cb) * %A - temporary file name for our version. * %B - temporary file name for the other branches' version. * %L - conflict marker length + * %P - the original path (safely quoted for the shell) * * The external merge driver should write the results in the * file named by %A, and signal that it has done with zero exit diff --git a/lockfile.c b/lockfile.c index 9889277751..993bb82748 100644 --- a/lockfile.c +++ b/lockfile.c @@ -157,6 +157,67 @@ static int lock_file(struct lock_file *lk, const char *path, int flags) return lk->fd; } +/* + * Constants defining the gaps between attempts to lock a file. The + * first backoff period is approximately INITIAL_BACKOFF_MS + * milliseconds. The longest backoff period is approximately + * (BACKOFF_MAX_MULTIPLIER * INITIAL_BACKOFF_MS) milliseconds. + */ +#define INITIAL_BACKOFF_MS 1L +#define BACKOFF_MAX_MULTIPLIER 1000 + +/* + * Try locking path, retrying with quadratic backoff for at least + * timeout_ms milliseconds. If timeout_ms is 0, try locking the file + * exactly once. If timeout_ms is -1, try indefinitely. + */ +static int lock_file_timeout(struct lock_file *lk, const char *path, + int flags, long timeout_ms) +{ + int n = 1; + int multiplier = 1; + long remaining_ms = 0; + static int random_initialized = 0; + + if (timeout_ms == 0) + return lock_file(lk, path, flags); + + if (!random_initialized) { + srand((unsigned int)getpid()); + random_initialized = 1; + } + + if (timeout_ms > 0) + remaining_ms = timeout_ms; + + while (1) { + long backoff_ms, wait_ms; + int fd; + + fd = lock_file(lk, path, flags); + + if (fd >= 0) + return fd; /* success */ + else if (errno != EEXIST) + return -1; /* failure other than lock held */ + else if (timeout_ms > 0 && remaining_ms <= 0) + return -1; /* failure due to timeout */ + + backoff_ms = multiplier * INITIAL_BACKOFF_MS; + /* back off for between 0.75*backoff_ms and 1.25*backoff_ms */ + wait_ms = (750 + rand() % 500) * backoff_ms / 1000; + sleep_millisec(wait_ms); + remaining_ms -= wait_ms; + + /* Recursion: (n+1)^2 = n^2 + 2n + 1 */ + multiplier += 2*n + 1; + if (multiplier > BACKOFF_MAX_MULTIPLIER) + multiplier = BACKOFF_MAX_MULTIPLIER; + else + n++; + } +} + void unable_to_lock_message(const char *path, int err, struct strbuf *buf) { if (err == EEXIST) { @@ -179,9 +240,10 @@ NORETURN void unable_to_lock_die(const char *path, int err) } /* This should return a meaningful errno on failure */ -int hold_lock_file_for_update(struct lock_file *lk, const char *path, int flags) +int hold_lock_file_for_update_timeout(struct lock_file *lk, const char *path, + int flags, long timeout_ms) { - int fd = lock_file(lk, path, flags); + int fd = lock_file_timeout(lk, path, flags, timeout_ms); if (fd < 0 && (flags & LOCK_DIE_ON_ERROR)) unable_to_lock_die(path, errno); return fd; @@ -214,7 +276,7 @@ int hold_lock_file_for_append(struct lock_file *lk, const char *path, int flags) int save_errno = errno; if (flags & LOCK_DIE_ON_ERROR) - exit(128); + die("failed to prepare '%s' for appending", path); close(orig_fd); rollback_lock_file(lk); errno = save_errno; diff --git a/lockfile.h b/lockfile.h index cd2ec95d30..b4abc61c00 100644 --- a/lockfile.h +++ b/lockfile.h @@ -74,8 +74,20 @@ struct lock_file { extern void unable_to_lock_message(const char *path, int err, struct strbuf *buf); extern NORETURN void unable_to_lock_die(const char *path, int err); -extern int hold_lock_file_for_update(struct lock_file *, const char *path, int); -extern int hold_lock_file_for_append(struct lock_file *, const char *path, int); +extern int hold_lock_file_for_update_timeout( + struct lock_file *lk, const char *path, + int flags, long timeout_ms); + +static inline int hold_lock_file_for_update( + struct lock_file *lk, const char *path, + int flags) +{ + return hold_lock_file_for_update_timeout(lk, path, flags, 0); +} + +extern int hold_lock_file_for_append(struct lock_file *lk, const char *path, + int flags); + extern FILE *fdopen_lock_file(struct lock_file *, const char *mode); extern char *get_locked_file_path(struct lock_file *); extern int commit_lock_file_to(struct lock_file *, const char *path); diff --git a/log-tree.c b/log-tree.c index c931615d92..01beb11f65 100644 --- a/log-tree.c +++ b/log-tree.c @@ -89,7 +89,8 @@ const struct name_decoration *get_name_decoration(const struct object *obj) return lookup_decoration(&name_decoration, obj); } -static int add_ref_decoration(const char *refname, const unsigned char *sha1, int flags, void *cb_data) +static int add_ref_decoration(const char *refname, const struct object_id *oid, + int flags, void *cb_data) { struct object *obj; enum decoration_type type = DECORATION_NONE; @@ -97,20 +98,20 @@ static int add_ref_decoration(const char *refname, const unsigned char *sha1, in assert(cb_data == NULL); if (starts_with(refname, "refs/replace/")) { - unsigned char original_sha1[20]; + struct object_id original_oid; if (!check_replace_refs) return 0; - if (get_sha1_hex(refname + 13, original_sha1)) { + if (get_oid_hex(refname + 13, &original_oid)) { warning("invalid replace ref %s", refname); return 0; } - obj = parse_object(original_sha1); + obj = parse_object(original_oid.hash); if (obj) add_name_decoration(DECORATION_GRAFTED, "replaced", obj); return 0; } - obj = parse_object(sha1); + obj = parse_object(oid->hash); if (!obj) return 0; @@ -139,7 +140,7 @@ static int add_ref_decoration(const char *refname, const unsigned char *sha1, in static int add_graft_decoration(const struct commit_graft *graft, void *cb_data) { - struct commit *commit = lookup_commit(graft->sha1); + struct commit *commit = lookup_commit(graft->oid.hash); if (!commit) return 0; add_name_decoration(DECORATION_GRAFTED, "grafted", &commit->object); @@ -149,6 +150,7 @@ static int add_graft_decoration(const struct commit_graft *graft, void *cb_data) void load_ref_decorations(int flags) { if (!decoration_loaded) { + decoration_loaded = 1; decoration_flags = flags; for_each_ref(add_ref_decoration, NULL); 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/notes-merge.c b/notes-merge.c index 109ff4ef41..0b2b82c41f 100644 --- a/notes-merge.c +++ b/notes-merge.c @@ -280,7 +280,7 @@ static void check_notes_merge_worktree(struct notes_merge_options *o) "(%s exists).", git_path("NOTES_MERGE_*")); } - if (safe_create_leading_directories(git_path( + if (safe_create_leading_directories_const(git_path( NOTES_MERGE_WORKTREE "/.test"))) die_errno("unable to create directory %s", git_path(NOTES_MERGE_WORKTREE)); @@ -295,8 +295,8 @@ static void write_buf_to_worktree(const unsigned char *obj, const char *buf, unsigned long size) { int fd; - char *path = git_path(NOTES_MERGE_WORKTREE "/%s", sha1_to_hex(obj)); - if (safe_create_leading_directories(path)) + const char *path = git_path(NOTES_MERGE_WORKTREE "/%s", sha1_to_hex(obj)); + if (safe_create_leading_directories_const(path)) die_errno("unable to create directory for '%s'", path); if (file_exists(path)) die("found existing file at '%s'", path); @@ -918,7 +918,7 @@ out: return ret; } -static int string_list_add_one_ref(const char *refname, const unsigned char *sha1, +static int string_list_add_one_ref(const char *refname, const struct object_id *oid, int flag, void *cb) { struct string_list *refs = cb; diff --git a/pack-bitmap.c b/pack-bitmap.c index 2b3ff23797..637770af81 100644 --- a/pack-bitmap.c +++ b/pack-bitmap.c @@ -622,7 +622,7 @@ static void show_objects_for_type( while (i < objects->word_alloc && ewah_iterator_next(&filter, &it)) { eword_t word = objects->words[i] & filter; - for (offset = 0; offset < BITS_IN_WORD; ++offset) { + for (offset = 0; offset < BITS_IN_EWORD; ++offset) { const unsigned char *sha1; struct revindex_entry *entry; uint32_t hash = 0; @@ -644,7 +644,7 @@ static void show_objects_for_type( show_reach(sha1, object_type, 0, hash, bitmap_git.pack, entry->offset); } - pos += BITS_IN_WORD; + pos += BITS_IN_EWORD; i++; } } @@ -776,7 +776,7 @@ int reuse_partial_packfile_from_bitmap(struct packed_git **packfile, break; } - reuse_objects += BITS_IN_WORD; + reuse_objects += BITS_IN_EWORD; } #ifdef GIT_BITMAP_DEBUG @@ -1001,7 +1001,7 @@ static int rebuild_bitmap(uint32_t *reposition, while (ewah_iterator_next(&word, &it)) { uint32_t offset, bit_pos; - for (offset = 0; offset < BITS_IN_WORD; ++offset) { + for (offset = 0; offset < BITS_IN_EWORD; ++offset) { if ((word >> offset) == 0) break; @@ -1014,7 +1014,7 @@ static int rebuild_bitmap(uint32_t *reposition, return -1; } - pos += BITS_IN_WORD; + pos += BITS_IN_EWORD; } return 0; } @@ -4,6 +4,7 @@ #include "cache.h" #include "strbuf.h" #include "string-list.h" +#include "dir.h" static int get_st_mode_bits(const char *path, int *mode) { @@ -16,11 +17,15 @@ static int get_st_mode_bits(const char *path, int *mode) static char bad_path[] = "/bad-path/"; -static char *get_pathname(void) +static struct strbuf *get_pathname(void) { - static char pathname_array[4][PATH_MAX]; + static struct strbuf pathname_array[4] = { + STRBUF_INIT, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT + }; static int index; - return pathname_array[3 & ++index]; + struct strbuf *sb = &pathname_array[3 & ++index]; + strbuf_reset(sb); + return sb; } static char *cleanup_path(char *path) @@ -34,6 +39,13 @@ static char *cleanup_path(char *path) return path; } +static void strbuf_cleanup_path(struct strbuf *sb) +{ + char *path = cleanup_path(sb->buf); + if (path > sb->buf) + strbuf_remove(sb, 0, path - sb->buf); +} + char *mksnpath(char *buf, size_t n, const char *fmt, ...) { va_list args; @@ -49,124 +61,192 @@ char *mksnpath(char *buf, size_t n, const char *fmt, ...) return cleanup_path(buf); } -static char *vsnpath(char *buf, size_t n, const char *fmt, va_list args) +static int dir_prefix(const char *buf, const char *dir) { - const char *git_dir = get_git_dir(); - size_t len; + int len = strlen(dir); + return !strncmp(buf, dir, len) && + (is_dir_sep(buf[len]) || buf[len] == '\0'); +} - len = strlen(git_dir); - if (n < len + 1) - goto bad; - memcpy(buf, git_dir, len); - if (len && !is_dir_sep(git_dir[len-1])) - buf[len++] = '/'; - len += vsnprintf(buf + len, n - len, fmt, args); - if (len >= n) - goto bad; - return cleanup_path(buf); -bad: - strlcpy(buf, bad_path, n); - return buf; +/* $buf =~ m|$dir/+$file| but without regex */ +static int is_dir_file(const char *buf, const char *dir, const char *file) +{ + int len = strlen(dir); + if (strncmp(buf, dir, len) || !is_dir_sep(buf[len])) + return 0; + while (is_dir_sep(buf[len])) + len++; + return !strcmp(buf + len, file); +} + +static void replace_dir(struct strbuf *buf, int len, const char *newdir) +{ + int newlen = strlen(newdir); + int need_sep = (buf->buf[len] && !is_dir_sep(buf->buf[len])) && + !is_dir_sep(newdir[newlen - 1]); + if (need_sep) + len--; /* keep one char, to be replaced with '/' */ + strbuf_splice(buf, 0, len, newdir, newlen); + if (need_sep) + buf->buf[newlen] = '/'; +} + +static const char *common_list[] = { + "/branches", "/hooks", "/info", "!/logs", "/lost-found", + "/objects", "/refs", "/remotes", "/worktrees", "/rr-cache", "/svn", + "config", "!gc.pid", "packed-refs", "shallow", + NULL +}; + +static void update_common_dir(struct strbuf *buf, int git_dir_len) +{ + char *base = buf->buf + git_dir_len; + const char **p; + + if (is_dir_file(base, "logs", "HEAD") || + is_dir_file(base, "info", "sparse-checkout")) + return; /* keep this in $GIT_DIR */ + for (p = common_list; *p; p++) { + const char *path = *p; + int is_dir = 0; + if (*path == '!') + path++; + if (*path == '/') { + path++; + is_dir = 1; + } + if (is_dir && dir_prefix(base, path)) { + replace_dir(buf, git_dir_len, get_git_common_dir()); + return; + } + if (!is_dir && !strcmp(base, path)) { + replace_dir(buf, git_dir_len, get_git_common_dir()); + return; + } + } +} + +void report_linked_checkout_garbage(void) +{ + struct strbuf sb = STRBUF_INIT; + const char **p; + int len; + + if (!git_common_dir_env) + return; + strbuf_addf(&sb, "%s/", get_git_dir()); + len = sb.len; + for (p = common_list; *p; p++) { + const char *path = *p; + if (*path == '!') + continue; + strbuf_setlen(&sb, len); + strbuf_addstr(&sb, path); + if (file_exists(sb.buf)) + report_garbage("unused in linked checkout", sb.buf); + } + strbuf_release(&sb); +} + +static void adjust_git_path(struct strbuf *buf, int git_dir_len) +{ + const char *base = buf->buf + git_dir_len; + if (git_graft_env && is_dir_file(base, "info", "grafts")) + strbuf_splice(buf, 0, buf->len, + get_graft_file(), strlen(get_graft_file())); + else if (git_index_env && !strcmp(base, "index")) + strbuf_splice(buf, 0, buf->len, + get_index_file(), strlen(get_index_file())); + else if (git_db_env && dir_prefix(base, "objects")) + replace_dir(buf, git_dir_len + 7, get_object_directory()); + else if (git_common_dir_env) + update_common_dir(buf, git_dir_len); +} + +static void do_git_path(struct strbuf *buf, const char *fmt, va_list args) +{ + int gitdir_len; + strbuf_addstr(buf, get_git_dir()); + if (buf->len && !is_dir_sep(buf->buf[buf->len - 1])) + strbuf_addch(buf, '/'); + gitdir_len = buf->len; + strbuf_vaddf(buf, fmt, args); + adjust_git_path(buf, gitdir_len); + strbuf_cleanup_path(buf); } -char *git_snpath(char *buf, size_t n, const char *fmt, ...) +void strbuf_git_path(struct strbuf *sb, const char *fmt, ...) { - char *ret; va_list args; va_start(args, fmt); - ret = vsnpath(buf, n, fmt, args); + do_git_path(sb, fmt, args); va_end(args); - return ret; } -char *git_pathdup(const char *fmt, ...) +const char *git_path(const char *fmt, ...) { - char path[PATH_MAX], *ret; + struct strbuf *pathname = get_pathname(); va_list args; va_start(args, fmt); - ret = vsnpath(path, sizeof(path), fmt, args); + do_git_path(pathname, fmt, args); va_end(args); - return xstrdup(ret); + return pathname->buf; } -char *mkpathdup(const char *fmt, ...) +char *git_pathdup(const char *fmt, ...) { - char *path; - struct strbuf sb = STRBUF_INIT; + struct strbuf path = STRBUF_INIT; va_list args; - va_start(args, fmt); - strbuf_vaddf(&sb, fmt, args); + do_git_path(&path, fmt, args); va_end(args); - path = xstrdup(cleanup_path(sb.buf)); - - strbuf_release(&sb); - return path; + return strbuf_detach(&path, NULL); } -char *mkpath(const char *fmt, ...) +char *mkpathdup(const char *fmt, ...) { + struct strbuf sb = STRBUF_INIT; va_list args; - unsigned len; - char *pathname = get_pathname(); - va_start(args, fmt); - len = vsnprintf(pathname, PATH_MAX, fmt, args); + strbuf_vaddf(&sb, fmt, args); va_end(args); - if (len >= PATH_MAX) - return bad_path; - return cleanup_path(pathname); + strbuf_cleanup_path(&sb); + return strbuf_detach(&sb, NULL); } -char *git_path(const char *fmt, ...) +const char *mkpath(const char *fmt, ...) { - char *pathname = get_pathname(); va_list args; - char *ret; - + struct strbuf *pathname = get_pathname(); va_start(args, fmt); - ret = vsnpath(pathname, PATH_MAX, fmt, args); + strbuf_vaddf(pathname, fmt, args); va_end(args); - return ret; + return cleanup_path(pathname->buf); } -char *git_path_submodule(const char *path, const char *fmt, ...) +const char *git_path_submodule(const char *path, const char *fmt, ...) { - char *pathname = get_pathname(); - struct strbuf buf = STRBUF_INIT; + struct strbuf *buf = get_pathname(); const char *git_dir; va_list args; - unsigned len; - len = strlen(path); - if (len > PATH_MAX-100) - return bad_path; + strbuf_addstr(buf, path); + if (buf->len && buf->buf[buf->len - 1] != '/') + strbuf_addch(buf, '/'); + strbuf_addstr(buf, ".git"); - strbuf_addstr(&buf, path); - if (len && path[len-1] != '/') - strbuf_addch(&buf, '/'); - strbuf_addstr(&buf, ".git"); - - git_dir = read_gitfile(buf.buf); + git_dir = read_gitfile(buf->buf); if (git_dir) { - strbuf_reset(&buf); - strbuf_addstr(&buf, git_dir); + strbuf_reset(buf); + strbuf_addstr(buf, git_dir); } - strbuf_addch(&buf, '/'); - - if (buf.len >= PATH_MAX) - return bad_path; - memcpy(pathname, buf.buf, buf.len + 1); - - strbuf_release(&buf); - len = strlen(pathname); + strbuf_addch(buf, '/'); va_start(args, fmt); - len += vsnprintf(pathname + len, PATH_MAX - len, fmt, args); + strbuf_vaddf(buf, fmt, args); va_end(args); - if (len >= PATH_MAX) - return bad_path; - return cleanup_path(pathname); + strbuf_cleanup_path(buf); + return buf->buf; } int validate_headref(const char *path) diff --git a/progress.c b/progress.c index 412e6b1ecc..2e31bec60f 100644 --- a/progress.c +++ b/progress.c @@ -72,6 +72,12 @@ static void clear_progress_signal(void) progress_update = 0; } +static int is_foreground_fd(int fd) +{ + int tpgrp = tcgetpgrp(fd); + return tpgrp < 0 || tpgrp == getpgid(0); +} + static int display(struct progress *progress, unsigned n, const char *done) { const char *eol, *tp; @@ -98,16 +104,21 @@ static int display(struct progress *progress, unsigned n, const char *done) unsigned percent = n * 100 / progress->total; if (percent != progress->last_percent || progress_update) { progress->last_percent = percent; - fprintf(stderr, "%s: %3u%% (%u/%u)%s%s", - progress->title, percent, n, - progress->total, tp, eol); - fflush(stderr); + if (is_foreground_fd(fileno(stderr)) || done) { + fprintf(stderr, "%s: %3u%% (%u/%u)%s%s", + progress->title, percent, n, + progress->total, tp, eol); + fflush(stderr); + } progress_update = 0; return 1; } } else if (progress_update) { - fprintf(stderr, "%s: %u%s%s", progress->title, n, tp, eol); - fflush(stderr); + if (is_foreground_fd(fileno(stderr)) || done) { + fprintf(stderr, "%s: %u%s%s", + progress->title, n, tp, eol); + fflush(stderr); + } progress_update = 0; return 1; } diff --git a/reachable.c b/reachable.c index 69fa6851da..9cff25b490 100644 --- a/reachable.c +++ b/reachable.c @@ -22,9 +22,10 @@ static void update_progress(struct connectivity_progress *cp) display_progress(cp->progress, cp->count); } -static int add_one_ref(const char *path, const unsigned char *sha1, int flag, void *cb_data) +static int add_one_ref(const char *path, const struct object_id *oid, + int flag, void *cb_data) { - struct object *object = parse_object_or_die(sha1, path); + struct object *object = parse_object_or_die(oid->hash, path); struct rev_info *revs = (struct rev_info *)cb_data; add_pending_object(revs, object, ""); diff --git a/read-cache.c b/read-cache.c index bf322708f0..89dbc0837a 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,9 @@ static int add_index_entry_with_check(struct index_state *istate, struct cache_e } pos = -pos-1; + if (!(option & ADD_CACHE_KEEP_CACHE_TREE)) + 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 +1392,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 +1690,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 +2078,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; @@ -10,8 +10,7 @@ struct ref_lock { char *ref_name; char *orig_ref_name; struct lock_file *lk; - unsigned char old_sha1[20]; - int lock_fd; + struct object_id old_oid; }; /* @@ -162,7 +161,7 @@ struct ref_value { * null. If REF_ISSYMREF, then this is the name of the object * referred to by the last reference in the symlink chain. */ - unsigned char sha1[20]; + struct object_id oid; /* * If REF_KNOWS_PEELED, then this field holds the peeled value @@ -170,7 +169,7 @@ struct ref_value { * be peelable. See the documentation for peel_ref() for an * exact definition of "peelable". */ - unsigned char peeled[20]; + struct object_id peeled; }; struct ref_cache; @@ -350,12 +349,10 @@ static struct ref_entry *create_ref_entry(const char *refname, if (check_name && check_refname_format(refname, REFNAME_ALLOW_ONELEVEL)) die("Reference has invalid format: '%s'", refname); - if (!check_name && !refname_is_safe(refname)) - die("Reference has invalid name: '%s'", refname); len = strlen(refname) + 1; ref = xmalloc(sizeof(struct ref_entry) + len); - hashcpy(ref->u.value.sha1, sha1); - hashclr(ref->u.value.peeled); + hashcpy(ref->u.value.oid.hash, sha1); + oidclr(&ref->u.value.peeled); memcpy(ref->name, refname, len); ref->flag = flag; return ref; @@ -629,7 +626,7 @@ static int is_dup_ref(const struct ref_entry *ref1, const struct ref_entry *ref2 /* This is impossible by construction */ die("Reference directory conflict: %s", ref1->name); - if (hashcmp(ref1->u.value.sha1, ref2->u.value.sha1)) + if (oidcmp(&ref1->u.value.oid, &ref2->u.value.oid)) die("Duplicated ref, and SHA1s don't match: %s", ref1->name); warning("Duplicated ref: %s", ref1->name); @@ -677,7 +674,7 @@ static int ref_resolves_to_object(struct ref_entry *entry) { if (entry->flag & REF_ISBROKEN) return 0; - if (!has_sha1_file(entry->u.value.sha1)) { + if (!has_sha1_file(entry->u.value.oid.hash)) { error("%s does not point to a valid object!", entry->name); return 0; } @@ -725,7 +722,7 @@ static int do_one_ref(struct ref_entry *entry, void *cb_data) /* Store the old value, in case this is a recursive call: */ old_current_ref = current_ref; current_ref = entry; - retval = data->fn(entry->name + data->trim, entry->u.value.sha1, + retval = data->fn(entry->name + data->trim, &entry->u.value.oid, entry->flag, data->cb_data); current_ref = old_current_ref; return retval; @@ -1244,6 +1241,8 @@ static void read_packed_refs(FILE *f, struct ref_dir *dir) int flag = REF_ISPACKED; if (check_refname_format(refname, REFNAME_ALLOW_ONELEVEL)) { + if (!refname_is_safe(refname)) + die("packed refname is dangerous: %s", refname); hashclr(sha1); flag |= REF_BAD_NAME | REF_ISBROKEN; } @@ -1259,7 +1258,7 @@ static void read_packed_refs(FILE *f, struct ref_dir *dir) line.len == PEELED_LINE_LENGTH && line.buf[PEELED_LINE_LENGTH - 1] == '\n' && !get_sha1_hex(line.buf + 1, sha1)) { - hashcpy(last->u.value.peeled, sha1); + hashcpy(last->u.value.peeled.hash, sha1); /* * Regardless of what the file header said, * we definitely know the value of *this* @@ -1374,21 +1373,38 @@ static void read_loose_refs(const char *dirname, struct ref_dir *dir) create_dir_entry(refs, refname.buf, refname.len, 1)); } else { + int read_ok; + if (*refs->name) { hashclr(sha1); flag = 0; - if (resolve_gitlink_ref(refs->name, refname.buf, sha1) < 0) { - hashclr(sha1); - flag |= REF_ISBROKEN; - } - } else if (read_ref_full(refname.buf, - RESOLVE_REF_READING, - sha1, &flag)) { + read_ok = !resolve_gitlink_ref(refs->name, + refname.buf, sha1); + } else { + read_ok = !read_ref_full(refname.buf, + RESOLVE_REF_READING, + sha1, &flag); + } + + if (!read_ok) { hashclr(sha1); flag |= REF_ISBROKEN; + } else if (is_null_sha1(sha1)) { + /* + * It is so astronomically unlikely + * that NULL_SHA1 is the SHA-1 of an + * actual object that we consider its + * appearance in a loose reference + * file to be repo corruption + * (probably due to a software bug). + */ + flag |= REF_ISBROKEN; } + if (check_refname_format(refname.buf, REFNAME_ALLOW_ONELEVEL)) { + if (!refname_is_safe(refname.buf)) + die("loose refname is dangerous: %s", refname.buf); hashclr(sha1); flag |= REF_BAD_NAME | REF_ISBROKEN; } @@ -1438,7 +1454,7 @@ static int resolve_gitlink_packed_ref(struct ref_cache *refs, if (ref == NULL) return -1; - hashcpy(sha1, ref->u.value.sha1); + hashcpy(sha1, ref->u.value.oid.hash); return 0; } @@ -1448,7 +1464,7 @@ static int resolve_gitlink_ref_recursive(struct ref_cache *refs, { int fd, len; char buffer[128], *p; - char *path; + const char *path; if (recursion > MAXDEPTH || strlen(refname) > MAXREFLEN) return -1; @@ -1525,7 +1541,7 @@ static int resolve_missing_loose_ref(const char *refname, */ entry = get_packed_ref(refname); if (entry) { - hashcpy(sha1, entry->u.value.sha1); + hashcpy(sha1, entry->u.value.oid.hash); if (flags) *flags |= REF_ISPACKED; return 0; @@ -1541,7 +1557,11 @@ static int resolve_missing_loose_ref(const char *refname, } /* This function needs to return a meaningful errno on failure */ -const char *resolve_ref_unsafe(const char *refname, int resolve_flags, unsigned char *sha1, int *flags) +static const char *resolve_ref_unsafe_1(const char *refname, + int resolve_flags, + unsigned char *sha1, + int *flags, + struct strbuf *sb_path) { int depth = MAXDEPTH; ssize_t len; @@ -1572,7 +1592,7 @@ const char *resolve_ref_unsafe(const char *refname, int resolve_flags, unsigned bad_name = 1; } for (;;) { - char path[PATH_MAX]; + const char *path; struct stat st; char *buf; int fd; @@ -1582,7 +1602,9 @@ const char *resolve_ref_unsafe(const char *refname, int resolve_flags, unsigned return NULL; } - git_snpath(path, sizeof(path), "%s", refname); + strbuf_reset(sb_path); + strbuf_git_path(sb_path, "%s", refname); + path = sb_path->buf; /* * We might have to loop back here to avoid a race @@ -1709,6 +1731,16 @@ const char *resolve_ref_unsafe(const char *refname, int resolve_flags, unsigned } } +const char *resolve_ref_unsafe(const char *refname, int resolve_flags, + unsigned char *sha1, int *flags) +{ + struct strbuf sb_path = STRBUF_INIT; + const char *ret = resolve_ref_unsafe_1(refname, resolve_flags, + sha1, flags, &sb_path); + strbuf_release(&sb_path); + return ret; +} + char *resolve_refdup(const char *ref, int resolve_flags, unsigned char *sha1, int *flags) { return xstrdup_or_null(resolve_ref_unsafe(ref, resolve_flags, sha1, flags)); @@ -1739,13 +1771,14 @@ int ref_exists(const char *refname) return !!resolve_ref_unsafe(refname, RESOLVE_REF_READING, sha1, NULL); } -static int filter_refs(const char *refname, const unsigned char *sha1, int flags, - void *data) +static int filter_refs(const char *refname, const struct object_id *oid, + int flags, void *data) { struct ref_filter *filter = (struct ref_filter *)data; + if (wildmatch(filter->pattern, refname, 0, NULL)) return 0; - return filter->fn(refname, sha1, flags, filter->cb_data); + return filter->fn(refname, oid, flags, filter->cb_data); } enum peel_status { @@ -1819,9 +1852,9 @@ static enum peel_status peel_entry(struct ref_entry *entry, int repeel) if (entry->flag & REF_KNOWS_PEELED) { if (repeel) { entry->flag &= ~REF_KNOWS_PEELED; - hashclr(entry->u.value.peeled); + oidclr(&entry->u.value.peeled); } else { - return is_null_sha1(entry->u.value.peeled) ? + return is_null_oid(&entry->u.value.peeled) ? PEEL_NON_TAG : PEEL_PEELED; } } @@ -1830,7 +1863,7 @@ static enum peel_status peel_entry(struct ref_entry *entry, int repeel) if (entry->flag & REF_ISSYMREF) return PEEL_IS_SYMREF; - status = peel_object(entry->u.value.sha1, entry->u.value.peeled); + status = peel_object(entry->u.value.oid.hash, entry->u.value.peeled.hash); if (status == PEEL_PEELED || status == PEEL_NON_TAG) entry->flag |= REF_KNOWS_PEELED; return status; @@ -1845,7 +1878,7 @@ int peel_ref(const char *refname, unsigned char *sha1) || !strcmp(current_ref->name, refname))) { if (peel_entry(current_ref, 0)) return -1; - hashcpy(sha1, current_ref->u.value.peeled); + hashcpy(sha1, current_ref->u.value.peeled.hash); return 0; } @@ -1865,7 +1898,7 @@ int peel_ref(const char *refname, unsigned char *sha1) if (r) { if (peel_entry(r, 0)) return -1; - hashcpy(sha1, r->u.value.peeled); + hashcpy(sha1, r->u.value.peeled.hash); return 0; } } @@ -1880,17 +1913,17 @@ struct warn_if_dangling_data { const char *msg_fmt; }; -static int warn_if_dangling_symref(const char *refname, const unsigned char *sha1, +static int warn_if_dangling_symref(const char *refname, const struct object_id *oid, int flags, void *cb_data) { struct warn_if_dangling_data *d = cb_data; const char *resolves_to; - unsigned char junk[20]; + struct object_id junk; if (!(flags & REF_ISSYMREF)) return 0; - resolves_to = resolve_ref_unsafe(refname, 0, junk, NULL); + resolves_to = resolve_ref_unsafe(refname, 0, junk.hash, NULL); if (!resolves_to || (d->refname ? strcmp(resolves_to, d->refname) @@ -2010,18 +2043,18 @@ static int do_for_each_ref(struct ref_cache *refs, const char *base, static int do_head_ref(const char *submodule, each_ref_fn fn, void *cb_data) { - unsigned char sha1[20]; + struct object_id oid; int flag; if (submodule) { - if (resolve_gitlink_ref(submodule, "HEAD", sha1) == 0) - return fn("HEAD", sha1, 0, cb_data); + if (resolve_gitlink_ref(submodule, "HEAD", oid.hash) == 0) + return fn("HEAD", &oid, 0, cb_data); return 0; } - if (!read_ref_full("HEAD", RESOLVE_REF_READING, sha1, &flag)) - return fn("HEAD", sha1, flag, cb_data); + if (!read_ref_full("HEAD", RESOLVE_REF_READING, oid.hash, &flag)) + return fn("HEAD", &oid, flag, cb_data); return 0; } @@ -2096,12 +2129,12 @@ int head_ref_namespaced(each_ref_fn fn, void *cb_data) { struct strbuf buf = STRBUF_INIT; int ret = 0; - unsigned char sha1[20]; + struct object_id oid; int flag; strbuf_addf(&buf, "%sHEAD", get_git_namespace()); - if (!read_ref_full(buf.buf, RESOLVE_REF_READING, sha1, &flag)) - ret = fn(buf.buf, sha1, flag, cb_data); + if (!read_ref_full(buf.buf, RESOLVE_REF_READING, oid.hash, &flag)) + ret = fn(buf.buf, &oid, flag, cb_data); strbuf_release(&buf); return ret; @@ -2201,27 +2234,35 @@ static void unlock_ref(struct ref_lock *lock) free(lock); } -/* This function should make sure errno is meaningful on error */ -static struct ref_lock *verify_lock(struct ref_lock *lock, - const unsigned char *old_sha1, int mustexist) +/* + * Verify that the reference locked by lock has the value old_sha1. + * Fail if the reference doesn't exist and mustexist is set. Return 0 + * on success. On error, write an error message to err, set errno, and + * return a negative value. + */ +static int verify_lock(struct ref_lock *lock, + const unsigned char *old_sha1, int mustexist, + struct strbuf *err) { + assert(err); + if (read_ref_full(lock->ref_name, mustexist ? RESOLVE_REF_READING : 0, - lock->old_sha1, NULL)) { + lock->old_oid.hash, NULL)) { int save_errno = errno; - error("Can't verify ref %s", lock->ref_name); - unlock_ref(lock); + strbuf_addf(err, "can't verify ref %s", lock->ref_name); errno = save_errno; - return NULL; + return -1; } - if (hashcmp(lock->old_sha1, old_sha1)) { - error("Ref %s is at %s but expected %s", lock->ref_name, - sha1_to_hex(lock->old_sha1), sha1_to_hex(old_sha1)); - unlock_ref(lock); + if (hashcmp(lock->old_oid.hash, old_sha1)) { + strbuf_addf(err, "ref %s is at %s but expected %s", + lock->ref_name, + sha1_to_hex(lock->old_oid.hash), + sha1_to_hex(old_sha1)); errno = EBUSY; - return NULL; + return -1; } - return lock; + return 0; } static int remove_empty_directories(const char *file) @@ -2342,7 +2383,7 @@ static struct ref_lock *lock_ref_sha1_basic(const char *refname, unsigned int flags, int *type_p, struct strbuf *err) { - char *ref_file; + const char *ref_file; const char *orig_refname = refname; struct ref_lock *lock; int last_errno = 0; @@ -2354,7 +2395,6 @@ static struct ref_lock *lock_ref_sha1_basic(const char *refname, assert(err); lock = xcalloc(1, sizeof(struct ref_lock)); - lock->lock_fd = -1; if (mustexist) resolve_flags |= RESOLVE_REF_READING; @@ -2365,7 +2405,7 @@ static struct ref_lock *lock_ref_sha1_basic(const char *refname, } refname = resolve_ref_unsafe(refname, resolve_flags, - lock->old_sha1, &type); + lock->old_oid.hash, &type); if (!refname && errno == EISDIR) { /* we are trying to lock foo but we used to * have foo/bar which now does not exist; @@ -2384,7 +2424,7 @@ static struct ref_lock *lock_ref_sha1_basic(const char *refname, goto error_return; } refname = resolve_ref_unsafe(orig_refname, resolve_flags, - lock->old_sha1, &type); + lock->old_oid.hash, &type); } if (type_p) *type_p = type; @@ -2404,7 +2444,7 @@ static struct ref_lock *lock_ref_sha1_basic(const char *refname, * refname, nor a packed ref whose name is a proper prefix of * our refname. */ - if (is_null_sha1(lock->old_sha1) && + if (is_null_oid(&lock->old_oid) && verify_refname_available(refname, extras, skip, get_packed_refs(&ref_cache), err)) { last_errno = ENOTDIR; @@ -2423,7 +2463,7 @@ static struct ref_lock *lock_ref_sha1_basic(const char *refname, ref_file = git_path("%s", refname); retry: - switch (safe_create_leading_directories(ref_file)) { + switch (safe_create_leading_directories_const(ref_file)) { case SCLD_OK: break; /* success */ case SCLD_VANISHED: @@ -2436,8 +2476,7 @@ static struct ref_lock *lock_ref_sha1_basic(const char *refname, goto error_return; } - lock->lock_fd = hold_lock_file_for_update(lock->lk, ref_file, lflags); - if (lock->lock_fd < 0) { + if (hold_lock_file_for_update(lock->lk, ref_file, lflags) < 0) { last_errno = errno; if (errno == ENOENT && --attempts_remaining > 0) /* @@ -2451,7 +2490,11 @@ static struct ref_lock *lock_ref_sha1_basic(const char *refname, goto error_return; } } - return old_sha1 ? verify_lock(lock, old_sha1, mustexist) : lock; + if (old_sha1 && verify_lock(lock, old_sha1, mustexist, err)) { + last_errno = errno; + goto error_return; + } + return lock; error_return: unlock_ref(lock); @@ -2481,18 +2524,28 @@ static int write_packed_entry_fn(struct ref_entry *entry, void *cb_data) if (peel_status != PEEL_PEELED && peel_status != PEEL_NON_TAG) error("internal error: %s is not a valid packed reference!", entry->name); - write_packed_entry(cb_data, entry->name, entry->u.value.sha1, + write_packed_entry(cb_data, entry->name, entry->u.value.oid.hash, peel_status == PEEL_PEELED ? - entry->u.value.peeled : NULL); + entry->u.value.peeled.hash : NULL); return 0; } /* This should return a meaningful errno on failure */ int lock_packed_refs(int flags) { + static int timeout_configured = 0; + static int timeout_value = 1000; + struct packed_ref_cache *packed_ref_cache; - if (hold_lock_file_for_update(&packlock, git_path("packed-refs"), flags) < 0) + if (!timeout_configured) { + git_config_get_int("core.packedrefstimeout", &timeout_value); + timeout_configured = 1; + } + + if (hold_lock_file_for_update_timeout( + &packlock, git_path("packed-refs"), + flags, timeout_value) < 0) return -1; /* * Get the current packed-refs while holding the lock. If the @@ -2590,24 +2643,24 @@ static int pack_if_possible_fn(struct ref_entry *entry, void *cb_data) peel_status = peel_entry(entry, 1); if (peel_status != PEEL_PEELED && peel_status != PEEL_NON_TAG) die("internal error peeling reference %s (%s)", - entry->name, sha1_to_hex(entry->u.value.sha1)); + entry->name, oid_to_hex(&entry->u.value.oid)); packed_entry = find_ref(cb->packed_refs, entry->name); if (packed_entry) { /* Overwrite existing packed entry with info from loose entry */ packed_entry->flag = REF_ISPACKED | REF_KNOWS_PEELED; - hashcpy(packed_entry->u.value.sha1, entry->u.value.sha1); + oidcpy(&packed_entry->u.value.oid, &entry->u.value.oid); } else { - packed_entry = create_ref_entry(entry->name, entry->u.value.sha1, + packed_entry = create_ref_entry(entry->name, entry->u.value.oid.hash, REF_ISPACKED | REF_KNOWS_PEELED, 0); add_ref(cb->packed_refs, packed_entry); } - hashcpy(packed_entry->u.value.peeled, entry->u.value.peeled); + oidcpy(&packed_entry->u.value.peeled, &entry->u.value.peeled); /* Schedule the loose reference for pruning if requested. */ if ((cb->flags & PACK_REFS_PRUNE)) { int namelen = strlen(entry->name) + 1; struct ref_to_prune *n = xcalloc(1, sizeof(*n) + namelen); - hashcpy(n->sha1, entry->u.value.sha1); + hashcpy(n->sha1, entry->u.value.oid.hash); strcpy(n->name, entry->name); n->next = cb->ref_to_prune; cb->ref_to_prune = n; @@ -2798,7 +2851,7 @@ static int rename_tmp_log(const char *newrefname) int attempts_remaining = 4; retry: - switch (safe_create_leading_directories(git_path("logs/%s", newrefname))) { + switch (safe_create_leading_directories_const(git_path("logs/%s", newrefname))) { case SCLD_OK: break; /* success */ case SCLD_VANISHED: @@ -2918,7 +2971,7 @@ int rename_ref(const char *oldrefname, const char *newrefname, const char *logms strbuf_release(&err); goto rollback; } - hashcpy(lock->old_sha1, orig_sha1); + hashcpy(lock->old_oid.hash, orig_sha1); if (write_ref_to_lockfile(lock, orig_sha1) || commit_ref_update(lock, orig_sha1, logmsg)) { @@ -2959,7 +3012,6 @@ static int close_ref(struct ref_lock *lock) { if (close_lock_file(lock->lk)) return -1; - lock->lock_fd = -1; return 0; } @@ -2967,7 +3019,6 @@ static int commit_ref(struct ref_lock *lock) { if (commit_lock_file(lock->lk)) return -1; - lock->lock_fd = -1; return 0; } @@ -2998,11 +3049,15 @@ static int copy_msg(char *buf, const char *msg) } /* This function must set a meaningful errno on failure */ -int log_ref_setup(const char *refname, char *logfile, int bufsize) +int log_ref_setup(const char *refname, struct strbuf *sb_logfile) { int logfd, oflags = O_APPEND | O_WRONLY; + char *logfile; - git_snpath(logfile, bufsize, "logs/%s", refname); + strbuf_git_path(sb_logfile, "logs/%s", refname); + logfile = sb_logfile->buf; + /* make sure the rest of the function can't change "logfile" */ + sb_logfile = NULL; if (log_all_ref_updates && (starts_with(refname, "refs/heads/") || starts_with(refname, "refs/remotes/") || @@ -3073,18 +3128,22 @@ static int log_ref_write_fd(int fd, const unsigned char *old_sha1, return 0; } -static int log_ref_write(const char *refname, const unsigned char *old_sha1, - const unsigned char *new_sha1, const char *msg) +static int log_ref_write_1(const char *refname, const unsigned char *old_sha1, + const unsigned char *new_sha1, const char *msg, + struct strbuf *sb_log_file) { int logfd, result, oflags = O_APPEND | O_WRONLY; - char log_file[PATH_MAX]; + char *log_file; if (log_all_ref_updates < 0) log_all_ref_updates = !is_bare_repository(); - result = log_ref_setup(refname, log_file, sizeof(log_file)); + result = log_ref_setup(refname, sb_log_file); if (result) return result; + log_file = sb_log_file->buf; + /* make sure the rest of the function can't change "log_file" */ + sb_log_file = NULL; logfd = open(log_file, oflags); if (logfd < 0) @@ -3107,6 +3166,15 @@ static int log_ref_write(const char *refname, const unsigned char *old_sha1, return 0; } +static int log_ref_write(const char *refname, const unsigned char *old_sha1, + const unsigned char *new_sha1, const char *msg) +{ + struct strbuf sb = STRBUF_INIT; + int ret = log_ref_write_1(refname, old_sha1, new_sha1, msg, &sb); + strbuf_release(&sb); + return ret; +} + int is_branch(const char *refname) { return !strcmp(refname, "HEAD") || starts_with(refname, "refs/heads/"); @@ -3137,8 +3205,8 @@ static int write_ref_to_lockfile(struct ref_lock *lock, errno = EINVAL; return -1; } - if (write_in_full(lock->lock_fd, sha1_to_hex(sha1), 40) != 40 || - write_in_full(lock->lock_fd, &term, 1) != 1 || + if (write_in_full(lock->lk->fd, sha1_to_hex(sha1), 40) != 40 || + write_in_full(lock->lk->fd, &term, 1) != 1 || close_ref(lock) < 0) { int save_errno = errno; error("Couldn't write %s", lock->lk->filename.buf); @@ -3158,9 +3226,9 @@ static int commit_ref_update(struct ref_lock *lock, const unsigned char *sha1, const char *logmsg) { clear_loose_ref_cache(&ref_cache); - if (log_ref_write(lock->ref_name, lock->old_sha1, sha1, logmsg) < 0 || + if (log_ref_write(lock->ref_name, lock->old_oid.hash, sha1, logmsg) < 0 || (strcmp(lock->ref_name, lock->orig_ref_name) && - log_ref_write(lock->orig_ref_name, lock->old_sha1, sha1, logmsg) < 0)) { + log_ref_write(lock->orig_ref_name, lock->old_oid.hash, sha1, logmsg) < 0)) { unlock_ref(lock); return -1; } @@ -3184,7 +3252,7 @@ static int commit_ref_update(struct ref_lock *lock, head_sha1, &head_flag); if (head_ref && (head_flag & REF_ISSYMREF) && !strcmp(head_ref, lock->ref_name)) - log_ref_write("HEAD", lock->old_sha1, sha1, logmsg); + log_ref_write("HEAD", lock->old_oid.hash, sha1, logmsg); } if (commit_ref(lock)) { error("Couldn't set %s", lock->ref_name); @@ -3576,11 +3644,12 @@ static int do_for_each_reflog(struct strbuf *name, each_ref_fn fn, void *cb_data strbuf_addch(name, '/'); retval = do_for_each_reflog(name, fn, cb_data); } else { - unsigned char sha1[20]; - if (read_ref_full(name->buf, 0, sha1, NULL)) + struct object_id oid; + + if (read_ref_full(name->buf, 0, oid.hash, NULL)) retval = error("bad ref for %s", name->buf); else - retval = fn(name->buf, sha1, 0, cb_data); + retval = fn(name->buf, &oid, 0, cb_data); } if (retval) break; @@ -3870,7 +3939,7 @@ int ref_transaction_commit(struct ref_transaction *transaction, ? TRANSACTION_NAME_CONFLICT : TRANSACTION_GENERIC_ERROR; reason = strbuf_detach(err, NULL); - strbuf_addf(err, "Cannot lock ref '%s': %s", + strbuf_addf(err, "cannot lock ref '%s': %s", update->refname, reason); free(reason); goto cleanup; @@ -3881,7 +3950,7 @@ int ref_transaction_commit(struct ref_transaction *transaction, (update->flags & REF_NODEREF)); if (!overwriting_symref && - !hashcmp(update->lock->old_sha1, update->new_sha1)) { + !hashcmp(update->lock->old_oid.hash, update->new_sha1)) { /* * The reference already has the desired * value, so we don't need to write it. @@ -3893,7 +3962,7 @@ int ref_transaction_commit(struct ref_transaction *transaction, * write_ref_to_lockfile(): */ update->lock = NULL; - strbuf_addf(err, "Cannot update the ref '%s'.", + strbuf_addf(err, "cannot update the ref '%s'.", update->refname); ret = TRANSACTION_GENERIC_ERROR; goto cleanup; @@ -4222,9 +4291,9 @@ int reflog_expire(const char *refname, const unsigned char *sha1, status |= error("couldn't write %s: %s", log_file, strerror(errno)); } else if (update && - (write_in_full(lock->lock_fd, + (write_in_full(lock->lk->fd, sha1_to_hex(cb.last_kept_sha1), 40) != 40 || - write_str_in_full(lock->lock_fd, "\n") != 1 || + write_str_in_full(lock->lk->fd, "\n") != 1 || close_ref(lock) < 0)) { status |= error("couldn't write %s", lock->lk->filename.buf); @@ -67,7 +67,7 @@ struct ref_transaction; * single callback invocation. */ typedef int each_ref_fn(const char *refname, - const unsigned char *sha1, int flags, void *cb_data); + const struct object_id *oid, int flags, void *cb_data); /* * The following functions invoke the specified callback function for @@ -191,7 +191,7 @@ extern int peel_ref(const char *refname, unsigned char *sha1); /* * Setup reflog before using. Set errno to something meaningful on failure. */ -int log_ref_setup(const char *refname, char *logfile, int bufsize); +int log_ref_setup(const char *refname, struct strbuf *logfile); /** Reads log for the value of ref during at_time. **/ extern int read_ref_at(const char *refname, unsigned int flags, @@ -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"), @@ -2024,7 +2168,8 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb) return 1; } -static int one_local_ref(const char *refname, const unsigned char *sha1, int flag, void *cb_data) +static int one_local_ref(const char *refname, const struct object_id *oid, + int flag, void *cb_data) { struct ref ***local_tail = cb_data; struct ref *ref; @@ -2036,7 +2181,7 @@ static int one_local_ref(const char *refname, const unsigned char *sha1, int fla len = strlen(refname) + 1; ref = xcalloc(1, sizeof(*ref) + len); - hashcpy(ref->new_sha1, sha1); + hashcpy(ref->new_sha1, oid->hash); memcpy(ref->name, refname, len); **local_tail = ref; *local_tail = &ref->next; @@ -2046,6 +2191,7 @@ static int one_local_ref(const char *refname, const unsigned char *sha1, int fla struct ref *get_local_heads(void) { struct ref *local_refs = NULL, **local_tail = &local_refs; + for_each_ref(one_local_ref, &local_tail); return local_refs; } @@ -2098,8 +2244,8 @@ struct stale_heads_info { int ref_count; }; -static int get_stale_heads_cb(const char *refname, - const unsigned char *sha1, int flags, void *cb_data) +static int get_stale_heads_cb(const char *refname, const struct object_id *oid, + int flags, void *cb_data) { struct stale_heads_info *info = cb_data; struct string_list matches = STRING_LIST_INIT_DUP; @@ -2128,7 +2274,7 @@ static int get_stale_heads_cb(const char *refname, if (stale) { struct ref *ref = make_linked_ref(refname, &info->stale_refs_tail); - hashcpy(ref->new_sha1, sha1); + hashcpy(ref->new_sha1, oid->hash); } clean_exit: @@ -2141,6 +2287,7 @@ struct ref *get_stale_heads(struct refspec *refs, int ref_count, struct ref *fet struct ref *ref, *stale_refs = NULL; struct string_list ref_names = STRING_LIST_INIT_NODUP; struct stale_heads_info info; + info.ref_names = &ref_names; info.stale_refs_tail = &stale_refs; info.refs = refs; @@ -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); diff --git a/replace_object.c b/replace_object.c index 0ab2dc1374..f0b39f06d5 100644 --- a/replace_object.c +++ b/replace_object.c @@ -53,7 +53,7 @@ static int register_replace_object(struct replace_object *replace, } static int register_replace_ref(const char *refname, - const unsigned char *sha1, + const struct object_id *oid, int flag, void *cb_data) { /* Get sha1 from refname */ @@ -68,7 +68,7 @@ static int register_replace_ref(const char *refname, } /* Copy sha1 from the read ref */ - hashcpy(repl_obj->replacement, sha1); + hashcpy(repl_obj->replacement, oid->hash); /* Register new object */ if (register_replace_object(repl_obj, 1)) diff --git a/revision.c b/revision.c index 0b322b4fdc..3ff8723da4 100644 --- a/revision.c +++ b/revision.c @@ -1218,7 +1218,8 @@ int ref_excluded(struct string_list *ref_excludes, const char *path) return 0; } -static int handle_one_ref(const char *path, const unsigned char *sha1, int flag, void *cb_data) +static int handle_one_ref(const char *path, const struct object_id *oid, + int flag, void *cb_data) { struct all_refs_cb *cb = cb_data; struct object *object; @@ -1226,9 +1227,9 @@ static int handle_one_ref(const char *path, const unsigned char *sha1, int flag, if (ref_excluded(cb->all_revs->ref_excludes, path)) return 0; - object = get_reference(cb->all_revs, path, sha1, cb->all_flags); + object = get_reference(cb->all_revs, path, oid->hash, cb->all_flags); add_rev_cmdline(cb->all_revs, object, path, REV_CMD_REF, cb->all_flags); - add_pending_sha1(cb->all_revs, path, sha1, cb->all_flags); + add_pending_sha1(cb->all_revs, path, oid->hash, cb->all_flags); return 0; } @@ -1292,7 +1293,8 @@ static int handle_one_reflog_ent(unsigned char *osha1, unsigned char *nsha1, return 0; } -static int handle_one_reflog(const char *path, const unsigned char *sha1, int flag, void *cb_data) +static int handle_one_reflog(const char *path, const struct object_id *oid, + int flag, void *cb_data) { struct all_refs_cb *cb = cb_data; cb->warned_bad_reflog = 0; @@ -1304,6 +1306,7 @@ static int handle_one_reflog(const char *path, const unsigned char *sha1, int fl void add_reflogs_to_pending(struct rev_info *revs, unsigned flags) { struct all_refs_cb cb; + cb.all_revs = revs; cb.all_flags = flags; for_each_reflog(handle_one_reflog, &cb); diff --git a/run-command.c b/run-command.c index aad03ab705..4d73e90fad 100644 --- a/run-command.c +++ b/run-command.c @@ -795,9 +795,9 @@ int finish_async(struct async *async) #endif } -char *find_hook(const char *name) +const char *find_hook(const char *name) { - char *path = git_path("hooks/%s", name); + const char *path = git_path("hooks/%s", name); if (access(path, X_OK) < 0) path = NULL; diff --git a/run-command.h b/run-command.h index 263b9662ad..1103805af1 100644 --- a/run-command.h +++ b/run-command.h @@ -52,7 +52,7 @@ int start_command(struct child_process *); int finish_command(struct child_process *); int run_command(struct child_process *); -extern char *find_hook(const char *name); +extern const char *find_hook(const char *name); LAST_ARG_MUST_BE_NULL extern int run_hook_le(const char *const *env, const char *name, ...); extern int run_hook_ve(const char *const *env, const char *name, va_list args); diff --git a/send-pack.c b/send-pack.c index 2e07ac3339..2a64fec949 100644 --- a/send-pack.c +++ b/send-pack.c @@ -182,7 +182,7 @@ static int advertise_shallow_grafts_cb(const struct commit_graft *graft, void *c { struct strbuf *sb = cb; if (graft->nr_parent == -1) - packet_buf_write(sb, "shallow %s\n", sha1_to_hex(graft->sha1)); + packet_buf_write(sb, "shallow %s\n", oid_to_hex(&graft->oid)); return 0; } diff --git a/server-info.c b/server-info.c index 34b0253177..c82e9ee396 100644 --- a/server-info.c +++ b/server-info.c @@ -47,14 +47,15 @@ out: return ret; } -static int add_info_ref(const char *path, const unsigned char *sha1, int flag, void *cb_data) +static int add_info_ref(const char *path, const struct object_id *oid, + int flag, void *cb_data) { FILE *fp = cb_data; - struct object *o = parse_object(sha1); + struct object *o = parse_object(oid->hash); if (!o) return -1; - if (fprintf(fp, "%s %s\n", sha1_to_hex(sha1), path) < 0) + if (fprintf(fp, "%s %s\n", oid_to_hex(oid), path) < 0) return -1; if (o->type == OBJ_TAG) { @@ -141,7 +141,9 @@ int check_filename(const char *prefix, const char *arg) if (arg[2] == '\0') /* ":/" is root dir, always exists */ return 1; name = arg + 2; - } else if (prefix) + } else if (!no_wildcard(arg)) + return 1; + else if (prefix) name = prefix_filename(prefix, strlen(prefix), arg); else name = arg; @@ -225,6 +227,36 @@ void verify_non_filename(const char *prefix, const char *arg) "'git <command> [<revision>...] -- [<file>...]'", arg); } +int get_common_dir(struct strbuf *sb, const char *gitdir) +{ + struct strbuf data = STRBUF_INIT; + struct strbuf path = STRBUF_INIT; + const char *git_common_dir = getenv(GIT_COMMON_DIR_ENVIRONMENT); + int ret = 0; + if (git_common_dir) { + strbuf_addstr(sb, git_common_dir); + return 1; + } + strbuf_addf(&path, "%s/commondir", gitdir); + if (file_exists(path.buf)) { + if (strbuf_read_file(&data, path.buf, 0) <= 0) + die_errno(_("failed to read %s"), path.buf); + while (data.len && (data.buf[data.len - 1] == '\n' || + data.buf[data.len - 1] == '\r')) + data.len--; + data.buf[data.len] = '\0'; + strbuf_reset(&path); + if (!is_absolute_path(data.buf)) + strbuf_addf(&path, "%s/", gitdir); + strbuf_addbuf(&path, &data); + strbuf_addstr(sb, real_path(path.buf)); + ret = 1; + } else + strbuf_addstr(sb, gitdir); + strbuf_release(&data); + strbuf_release(&path); + return ret; +} /* * Test if it looks like we're at a git directory. @@ -239,31 +271,40 @@ void verify_non_filename(const char *prefix, const char *arg) */ int is_git_directory(const char *suspect) { - char path[PATH_MAX]; - size_t len = strlen(suspect); + struct strbuf path = STRBUF_INIT; + int ret = 0; + size_t len; + + /* Check worktree-related signatures */ + strbuf_addf(&path, "%s/HEAD", suspect); + if (validate_headref(path.buf)) + goto done; - if (PATH_MAX <= len + strlen("/objects")) - die("Too long path: %.*s", 60, suspect); - strcpy(path, suspect); + strbuf_reset(&path); + get_common_dir(&path, suspect); + len = path.len; + + /* Check non-worktree-related signatures */ if (getenv(DB_ENVIRONMENT)) { if (access(getenv(DB_ENVIRONMENT), X_OK)) - return 0; + goto done; } else { - strcpy(path + len, "/objects"); - if (access(path, X_OK)) - return 0; + strbuf_setlen(&path, len); + strbuf_addstr(&path, "/objects"); + if (access(path.buf, X_OK)) + goto done; } - strcpy(path + len, "/refs"); - if (access(path, X_OK)) - return 0; + strbuf_setlen(&path, len); + strbuf_addstr(&path, "/refs"); + if (access(path.buf, X_OK)) + goto done; - strcpy(path + len, "/HEAD"); - if (validate_headref(path)) - return 0; - - return 1; + ret = 1; +done: + strbuf_release(&path); + return ret; } int is_inside_git_dir(void) @@ -309,9 +350,28 @@ void setup_work_tree(void) initialized = 1; } +static int check_repo_format(const char *var, const char *value, void *cb) +{ + if (strcmp(var, "core.repositoryformatversion") == 0) + repository_format_version = git_config_int(var, value); + else if (strcmp(var, "core.sharedrepository") == 0) + shared_repository = git_config_perm(var, value); + return 0; +} + static int check_repository_format_gently(const char *gitdir, int *nongit_ok) { - char repo_config[PATH_MAX+1]; + struct strbuf sb = STRBUF_INIT; + const char *repo_config; + config_fn_t fn; + int ret = 0; + + if (get_common_dir(&sb, gitdir)) + fn = check_repo_format; + else + fn = check_repository_format_version; + strbuf_addstr(&sb, "/config"); + repo_config = sb.buf; /* * git_config() can't be used here because it calls git_pathdup() @@ -322,8 +382,7 @@ static int check_repository_format_gently(const char *gitdir, int *nongit_ok) * Use a gentler version of git_config() to check if this repo * is a good one. */ - snprintf(repo_config, PATH_MAX, "%s/config", gitdir); - git_config_early(check_repository_format_version, NULL, repo_config); + git_config_early(fn, NULL, repo_config); if (GIT_REPO_VERSION < repository_format_version) { if (!nongit_ok) die ("Expected git repo version <= %d, found %d", @@ -332,9 +391,21 @@ static int check_repository_format_gently(const char *gitdir, int *nongit_ok) GIT_REPO_VERSION, repository_format_version); warning("Please upgrade Git"); *nongit_ok = -1; - return -1; + ret = -1; } - return 0; + strbuf_release(&sb); + return ret; +} + +static void update_linked_gitdir(const char *gitfile, const char *gitdir) +{ + struct strbuf path = STRBUF_INIT; + struct stat st; + + strbuf_addf(&path, "%s/gitfile", gitdir); + if (stat(path.buf, &st) || st.st_mtime + 24 * 3600 < time(NULL)) + write_file(path.buf, 0, "%s\n", gitfile); + strbuf_release(&path); } /* @@ -385,6 +456,8 @@ const char *read_gitfile(const char *path) if (!is_git_directory(dir)) die("Not a git repository: %s", dir); + + update_linked_gitdir(path, dir); path = real_path(dir); free(buf); @@ -807,11 +880,10 @@ int git_config_perm(const char *var, const char *value) int check_repository_format_version(const char *var, const char *value, void *cb) { - if (strcmp(var, "core.repositoryformatversion") == 0) - repository_format_version = git_config_int(var, value); - else if (strcmp(var, "core.sharedrepository") == 0) - shared_repository = git_config_perm(var, value); - else if (strcmp(var, "core.bare") == 0) { + int ret = check_repo_format(var, value, cb); + if (ret) + return ret; + if (strcmp(var, "core.bare") == 0) { is_bare_repository_cfg = git_config_bool(var, value); if (is_bare_repository_cfg == 1) inside_work_tree = -1; diff --git a/sha1_file.c b/sha1_file.c index 56c69cebc8..77cd81db31 100644 --- a/sha1_file.c +++ b/sha1_file.c @@ -405,7 +405,7 @@ void add_to_alternates_file(const char *reference) { struct lock_file *lock = xcalloc(1, sizeof(struct lock_file)); int fd = hold_lock_file_for_append(lock, git_path("objects/info/alternates"), LOCK_DIE_ON_ERROR); - char *alt = mkpath("%s\n", reference); + const char *alt = mkpath("%s\n", reference); write_or_die(fd, alt, strlen(alt)); if (commit_lock_file(lock)) die("could not close alternates file"); @@ -1571,6 +1571,40 @@ int unpack_sha1_header(git_zstream *stream, unsigned char *map, unsigned long ma return git_inflate(stream, 0); } +static int unpack_sha1_header_to_strbuf(git_zstream *stream, unsigned char *map, + unsigned long mapsize, void *buffer, + unsigned long bufsiz, struct strbuf *header) +{ + int status; + + status = unpack_sha1_header(stream, map, mapsize, buffer, bufsiz); + + /* + * Check if entire header is unpacked in the first iteration. + */ + if (memchr(buffer, '\0', stream->next_out - (unsigned char *)buffer)) + return 0; + + /* + * buffer[0..bufsiz] was not large enough. Copy the partial + * result out to header, and then append the result of further + * reading the stream. + */ + strbuf_add(header, buffer, stream->next_out - (unsigned char *)buffer); + stream->next_out = buffer; + stream->avail_out = bufsiz; + + do { + status = git_inflate(stream, 0); + strbuf_add(header, buffer, stream->next_out - (unsigned char *)buffer); + if (memchr(buffer, '\0', stream->next_out - (unsigned char *)buffer)) + return 0; + stream->next_out = buffer; + stream->avail_out = bufsiz; + } while (status != Z_STREAM_END); + return -1; +} + static void *unpack_sha1_rest(git_zstream *stream, void *buffer, unsigned long size, const unsigned char *sha1) { int bytes = strlen(buffer) + 1; @@ -1621,27 +1655,38 @@ static void *unpack_sha1_rest(git_zstream *stream, void *buffer, unsigned long s * too permissive for what we want to check. So do an anal * object header parse by hand. */ -int parse_sha1_header(const char *hdr, unsigned long *sizep) +static int parse_sha1_header_extended(const char *hdr, struct object_info *oi, + unsigned int flags) { - char type[10]; - int i; + const char *type_buf = hdr; unsigned long size; + int type, type_len = 0; /* - * The type can be at most ten bytes (including the - * terminating '\0' that we add), and is followed by + * The type can be of any size but is followed by * a space. */ - i = 0; for (;;) { char c = *hdr++; if (c == ' ') break; - type[i++] = c; - if (i >= sizeof(type)) - return -1; + type_len++; } - type[i] = 0; + + type = type_from_string_gently(type_buf, type_len, 1); + if (oi->typename) + strbuf_add(oi->typename, type_buf, type_len); + /* + * Set type to 0 if its an unknown object and + * we're obtaining the type using '--allow-unkown-type' + * option. + */ + if ((flags & LOOKUP_UNKNOWN_OBJECT) && (type < 0)) + type = 0; + else if (type < 0) + die("invalid object type"); + if (oi->typep) + *oi->typep = type; /* * The length must follow immediately, and be in canonical @@ -1659,12 +1704,24 @@ int parse_sha1_header(const char *hdr, unsigned long *sizep) size = size * 10 + c; } } - *sizep = size; + + if (oi->sizep) + *oi->sizep = size; /* * The length must be followed by a zero byte */ - return *hdr ? -1 : type_from_string(type); + return *hdr ? -1 : type; +} + +int parse_sha1_header(const char *hdr, unsigned long *sizep) +{ + struct object_info oi; + + oi.sizep = sizep; + oi.typename = NULL; + oi.typep = NULL; + return parse_sha1_header_extended(hdr, &oi, LOOKUP_REPLACE_OBJECT); } static void *unpack_sha1_file(void *map, unsigned long mapsize, enum object_type *type, unsigned long *size, const unsigned char *sha1) @@ -2529,13 +2586,15 @@ struct packed_git *find_sha1_pack(const unsigned char *sha1, } static int sha1_loose_object_info(const unsigned char *sha1, - struct object_info *oi) + struct object_info *oi, + int flags) { - int status; - unsigned long mapsize, size; + int status = 0; + unsigned long mapsize; void *map; git_zstream stream; char hdr[32]; + struct strbuf hdrbuf = STRBUF_INIT; if (oi->delta_base_sha1) hashclr(oi->delta_base_sha1); @@ -2548,7 +2607,7 @@ static int sha1_loose_object_info(const unsigned char *sha1, * return value implicitly indicates whether the * object even exists. */ - if (!oi->typep && !oi->sizep) { + if (!oi->typep && !oi->typename && !oi->sizep) { struct stat st; if (stat_sha1_file(sha1, &st) < 0) return -1; @@ -2562,17 +2621,26 @@ static int sha1_loose_object_info(const unsigned char *sha1, return -1; if (oi->disk_sizep) *oi->disk_sizep = mapsize; - if (unpack_sha1_header(&stream, map, mapsize, hdr, sizeof(hdr)) < 0) + if ((flags & LOOKUP_UNKNOWN_OBJECT)) { + if (unpack_sha1_header_to_strbuf(&stream, map, mapsize, hdr, sizeof(hdr), &hdrbuf) < 0) + status = error("unable to unpack %s header with --allow-unknown-type", + sha1_to_hex(sha1)); + } else if (unpack_sha1_header(&stream, map, mapsize, hdr, sizeof(hdr)) < 0) status = error("unable to unpack %s header", sha1_to_hex(sha1)); - else if ((status = parse_sha1_header(hdr, &size)) < 0) + if (status < 0) + ; /* Do nothing */ + else if (hdrbuf.len) { + if ((status = parse_sha1_header_extended(hdrbuf.buf, oi, flags)) < 0) + status = error("unable to parse %s header with --allow-unknown-type", + sha1_to_hex(sha1)); + } else if ((status = parse_sha1_header_extended(hdr, oi, flags)) < 0) status = error("unable to parse %s header", sha1_to_hex(sha1)); - else if (oi->sizep) - *oi->sizep = size; git_inflate_end(&stream); munmap(map, mapsize); - if (oi->typep) + if (status && oi->typep) *oi->typep = status; + strbuf_release(&hdrbuf); return 0; } @@ -2581,6 +2649,7 @@ int sha1_object_info_extended(const unsigned char *sha1, struct object_info *oi, struct cached_object *co; struct pack_entry e; int rtype; + enum object_type real_type; const unsigned char *real = lookup_replace_object_extended(sha1, flags); co = find_cached_object(real); @@ -2593,13 +2662,15 @@ int sha1_object_info_extended(const unsigned char *sha1, struct object_info *oi, *(oi->disk_sizep) = 0; if (oi->delta_base_sha1) hashclr(oi->delta_base_sha1); + if (oi->typename) + strbuf_addstr(oi->typename, typename(co->type)); oi->whence = OI_CACHED; return 0; } if (!find_pack_entry(real, &e)) { /* Most likely it's a loose object. */ - if (!sha1_loose_object_info(real, oi)) { + if (!sha1_loose_object_info(real, oi, flags)) { oi->whence = OI_LOOSE; return 0; } @@ -2610,9 +2681,18 @@ int sha1_object_info_extended(const unsigned char *sha1, struct object_info *oi, return -1; } + /* + * packed_object_info() does not follow the delta chain to + * find out the real type, unless it is given oi->typep. + */ + if (oi->typename && !oi->typep) + oi->typep = &real_type; + rtype = packed_object_info(e.p, e.offset, oi); if (rtype < 0) { mark_bad_packed_object(e.p, real); + if (oi->typep == &real_type) + oi->typep = NULL; return sha1_object_info_extended(real, oi, 0); } else if (in_delta_base_cache(e.p, e.offset)) { oi->whence = OI_DBCACHED; @@ -2623,6 +2703,10 @@ int sha1_object_info_extended(const unsigned char *sha1, struct object_info *oi, oi->u.packed.is_delta = (rtype == OBJ_REF_DELTA || rtype == OBJ_OFS_DELTA); } + if (oi->typename) + strbuf_addstr(oi->typename, typename(*oi->typep)); + if (oi->typep == &real_type) + oi->typep = NULL; return 0; } @@ -3084,7 +3168,7 @@ int has_sha1_pack(const unsigned char *sha1) return find_pack_entry(sha1, &e); } -int has_sha1_file(const unsigned char *sha1) +int has_sha1_file_with_flags(const unsigned char *sha1, int flags) { struct pack_entry e; @@ -3092,6 +3176,8 @@ int has_sha1_file(const unsigned char *sha1) return 1; if (has_loose_object(sha1)) return 1; + if (flags & HAS_SHA1_QUICK) + return 0; reprepare_packed_git(); return find_pack_entry(sha1, &e); } diff --git a/sha1_name.c b/sha1_name.c index 6de8c87c8a..e57513e610 100644 --- a/sha1_name.c +++ b/sha1_name.c @@ -416,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)) @@ -430,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); @@ -477,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; } @@ -832,11 +845,11 @@ static int get_sha1_1(const char *name, int len, unsigned char *sha1, unsigned l /* Remember to update object flag allocation in object.h */ #define ONELINE_SEEN (1u<<20) -static int handle_one_ref(const char *path, - const unsigned char *sha1, int flag, void *cb_data) +static int handle_one_ref(const char *path, const struct object_id *oid, + int flag, void *cb_data) { struct commit_list **list = cb_data; - struct object *object = parse_object(sha1); + struct object *object = parse_object(oid->hash); if (!object) return 0; if (object->type == OBJ_TAG) { @@ -1056,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; } @@ -1146,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; } @@ -1370,6 +1379,7 @@ static int get_sha1_with_context_1(const char *name, int pos; if (!only_to_die && namelen > 2 && name[1] == '/') { struct commit_list *list = NULL; + for_each_ref(handle_one_ref, &list); commit_list_sort_by_date(&list); return get_sha1_oneline(name + 2, sha1, list); @@ -1433,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)); @@ -1468,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); } @@ -31,7 +31,7 @@ int register_shallow(const unsigned char *sha1) xmalloc(sizeof(struct commit_graft)); struct commit *commit = lookup_commit(sha1); - hashcpy(graft->sha1, sha1); + hashcpy(graft->oid.hash, sha1); graft->nr_parent = -1; if (commit && commit->object.parsed) commit->parents = NULL; @@ -159,11 +159,11 @@ struct write_shallow_data { static int write_one_shallow(const struct commit_graft *graft, void *cb_data) { struct write_shallow_data *data = cb_data; - const char *hex = sha1_to_hex(graft->sha1); + const char *hex = oid_to_hex(&graft->oid); if (graft->nr_parent != -1) return 0; if (data->flags & SEEN_ONLY) { - struct commit *c = lookup_commit(graft->sha1); + struct commit *c = lookup_commit(graft->oid.hash); if (!c || !(c->object.flags & SEEN)) { if (data->flags & VERBOSE) printf("Removing %s from .git/shallow\n", @@ -282,7 +282,7 @@ static int advertise_shallow_grafts_cb(const struct commit_graft *graft, void *c { int fd = *(int *)cb; if (graft->nr_parent == -1) - packet_write(fd, "shallow %s\n", sha1_to_hex(graft->sha1)); + packet_write(fd, "shallow %s\n", oid_to_hex(&graft->oid)); return 0; } @@ -475,11 +475,10 @@ static void paint_down(struct paint_info *info, const unsigned char *sha1, free(tmp); } -static int mark_uninteresting(const char *refname, - const unsigned char *sha1, +static int mark_uninteresting(const char *refname, const struct object_id *oid, int flags, void *cb_data) { - struct commit *commit = lookup_commit_reference_gently(sha1, 1); + struct commit *commit = lookup_commit_reference_gently(oid->hash, 1); if (!commit) return 0; commit->object.flags |= UNINTERESTING; @@ -584,12 +583,12 @@ struct commit_array { int nr, alloc; }; -static int add_ref(const char *refname, - const unsigned char *sha1, int flags, void *cb_data) +static int add_ref(const char *refname, const struct object_id *oid, + int flags, void *cb_data) { struct commit_array *ca = cb_data; ALLOC_GROW(ca->commits, ca->nr + 1, ca->alloc); - ca->commits[ca->nr] = lookup_commit_reference_gently(sha1, 1); + ca->commits[ca->nr] = lookup_commit_reference_gently(oid->hash, 1); if (ca->commits[ca->nr]) ca->nr++; return 0; @@ -674,6 +673,7 @@ int delayed_reachability_test(struct shallow_info *si, int c) if (!si->commits) { struct commit_array ca; + memset(&ca, 0, sizeof(ca)); head_ref(add_ref, &ca); for_each_ref(add_ref, &ca); 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; } @@ -435,6 +435,47 @@ int strbuf_getcwd(struct strbuf *sb) return -1; } +#ifdef HAVE_GETDELIM +int strbuf_getwholeline(struct strbuf *sb, FILE *fp, int term) +{ + ssize_t r; + + if (feof(fp)) + return EOF; + + strbuf_reset(sb); + + /* Translate slopbuf to NULL, as we cannot call realloc on it */ + if (!sb->alloc) + sb->buf = NULL; + r = getdelim(&sb->buf, &sb->alloc, term, fp); + + if (r > 0) { + sb->len = r; + return 0; + } + assert(r == -1); + + /* + * Normally we would have called xrealloc, which will try to free + * memory and recover. But we have no way to tell getdelim() to do so. + * Worse, we cannot try to recover ENOMEM ourselves, because we have + * no idea how many bytes were read by getdelim. + * + * Dying here is reasonable. It mirrors what xrealloc would do on + * catastrophic memory failure. We skip the opportunity to free pack + * memory and retry, but that's unlikely to help for a malloc small + * enough to hold a single line of input, anyway. + */ + if (errno == ENOMEM) + die("Out of memory, getdelim failed"); + + /* Restore slopbuf that we moved out of the way before */ + if (!sb->buf) + strbuf_init(sb, 0); + return EOF; +} +#else int strbuf_getwholeline(struct strbuf *sb, FILE *fp, int term) { int ch; @@ -443,18 +484,22 @@ int strbuf_getwholeline(struct strbuf *sb, FILE *fp, int term) return EOF; strbuf_reset(sb); - while ((ch = fgetc(fp)) != EOF) { - strbuf_grow(sb, 1); + flockfile(fp); + while ((ch = getc_unlocked(fp)) != EOF) { + if (!strbuf_avail(sb)) + strbuf_grow(sb, 1); sb->buf[sb->len++] = ch; if (ch == term) break; } + funlockfile(fp); if (ch == EOF && sb->len == 0) return EOF; sb->buf[sb->len] = '\0'; return 0; } +#endif int strbuf_getline(struct strbuf *sb, FILE *fp, int term) { @@ -205,7 +205,8 @@ extern int strbuf_cmp(const struct strbuf *, const struct strbuf *); */ static inline void strbuf_addch(struct strbuf *sb, int c) { - strbuf_grow(sb, 1); + if (!strbuf_avail(sb)) + strbuf_grow(sb, 1); sb->buf[sb->len++] = c; sb->buf[sb->len] = '\0'; } diff --git a/submodule.c b/submodule.c index 5a563ad100..15e90d1c10 100644 --- a/submodule.c +++ b/submodule.c @@ -422,7 +422,8 @@ void set_config_fetch_recurse_submodules(int value) config_fetch_recurse_submodules = value; } -static int has_remote(const char *refname, const unsigned char *sha1, int flags, void *cb_data) +static int has_remote(const char *refname, const struct object_id *oid, + int flags, void *cb_data) { return 1; } @@ -616,10 +617,10 @@ static void submodule_collect_changed_cb(struct diff_queue_struct *q, } } -static int add_sha1_to_array(const char *ref, const unsigned char *sha1, +static int add_sha1_to_array(const char *ref, const struct object_id *oid, int flags, void *data) { - sha1_array_append(data, sha1); + sha1_array_append(data, oid->hash); return 0; } @@ -1099,16 +1100,11 @@ void connect_work_tree_and_git_dir(const char *work_tree, const char *git_dir) struct strbuf file_name = STRBUF_INIT; struct strbuf rel_path = STRBUF_INIT; const char *real_work_tree = xstrdup(real_path(work_tree)); - FILE *fp; /* Update gitfile */ strbuf_addf(&file_name, "%s/.git", work_tree); - fp = fopen(file_name.buf, "w"); - if (!fp) - die(_("Could not create git link %s"), file_name.buf); - fprintf(fp, "gitdir: %s\n", relative_path(git_dir, real_work_tree, - &rel_path)); - fclose(fp); + write_file(file_name.buf, 1, "gitdir: %s\n", + relative_path(git_dir, real_work_tree, &rel_path)); /* Update core.worktree setting */ strbuf_reset(&file_name); diff --git a/t/lib-git-p4.sh b/t/lib-git-p4.sh index 5aa8adcf9c..75482254a3 100644 --- a/t/lib-git-p4.sh +++ b/t/lib-git-p4.sh @@ -69,7 +69,7 @@ start_p4d() { ( cd "$db" && { - p4d -q -p $P4DPORT & + p4d -q -p $P4DPORT "$@" & echo $! >"$pidfile" } ) && diff --git a/t/t0021-conversion.sh b/t/t0021-conversion.sh index e9f1626b09..718efa04d3 100755 --- a/t/t0021-conversion.sh +++ b/t/t0021-conversion.sh @@ -204,6 +204,16 @@ test_expect_success 'filtering large input to small output should use little mem GIT_MMAP_LIMIT=1m GIT_ALLOC_LIMIT=1m git add 30MB ' +test_expect_success 'filter that does not read is fine' ' + test-genrandom foo $((128 * 1024 + 1)) >big && + echo "big filter=epipe" >.gitattributes && + git config filter.epipe.clean "echo xyzzy" && + git add big && + git cat-file blob :big >actual && + echo xyzzy >expect && + test_cmp expect actual +' + test_expect_success EXPENSIVE 'filter large file' ' git config filter.largefile.smudge cat && git config filter.largefile.clean cat && diff --git a/t/t0027-auto-crlf.sh b/t/t0027-auto-crlf.sh index 452320df83..1a56e5e82e 100755 --- a/t/t0027-auto-crlf.sh +++ b/t/t0027-auto-crlf.sh @@ -57,28 +57,35 @@ create_gitattributes () { check_warning () { case "$1" in - LF_CRLF) grep "LF will be replaced by CRLF" $2;; - CRLF_LF) grep "CRLF will be replaced by LF" $2;; - '') - >expect - grep "will be replaced by" $2 >actual - test_cmp expect actual - ;; - *) false ;; + LF_CRLF) echo "warning: LF will be replaced by CRLF" >"$2".expect ;; + CRLF_LF) echo "warning: CRLF will be replaced by LF" >"$2".expect ;; + '') >"$2".expect ;; + *) echo >&2 "Illegal 1": "$1" ; return false ;; esac + grep "will be replaced by" "$2" | sed -e "s/\(.*\) in [^ ]*$/\1/" >"$2".actual + test_cmp "$2".expect "$2".actual } -create_file_in_repo () { +commit_check_warn () { crlf=$1 attr=$2 lfname=$3 crlfname=$4 - lfmixcrlf=$5 - lfmixcr=$6 - crlfnul=$7 - create_gitattributes "$attr" && + repoMIX=$5 + lfmixcrlf=$6 + lfmixcr=$7 + crlfnul=$8 pfx=crlf_${crlf}_attr_${attr} - for f in LF CRLF LF_mix_CR CRLF_mix_LF CRLF_nul + # Special handling for repoMIX: It should already be in the repo + # with CRLF + f=repoMIX + fname=${pfx}_$f.txt + echo >.gitattributes && + cp $f $fname && + git -c core.autocrlf=false add $fname 2>"${pfx}_$f.err" && + git commit -m "repoMIX" && + create_gitattributes "$attr" && + for f in LF CRLF repoMIX LF_mix_CR CRLF_mix_LF LF_nul CRLF_nul do fname=${pfx}_$f.txt && cp $f $fname && @@ -109,7 +116,7 @@ check_files_in_repo () { } -check_files_in_ws () { +checkout_files () { eol=$1 crlf=$2 attr=$3 @@ -122,7 +129,7 @@ check_files_in_ws () { git config core.autocrlf $crlf && pfx=eol_${eol}_crlf_${crlf}_attr_${attr}_ && src=crlf_false_attr__ && - for f in LF CRLF LF_mix_CR CRLF_mix_LF CRLF_nul + for f in LF CRLF LF_mix_CR CRLF_mix_LF LF_nul do rm $src$f.txt && if test -z "$eol"; then @@ -144,8 +151,8 @@ check_files_in_ws () { test_expect_success "checkout core.eol=$eol core.autocrlf=$crlf gitattributes=$attr file=LF_mix_CR" " compare_ws_file $pfx $lfmixcr ${src}LF_mix_CR.txt " - test_expect_success "checkout core.eol=$eol core.autocrlf=$crlf gitattributes=$attr file=CRLF_nul" " - compare_ws_file $pfx $crlfnul ${src}CRLF_nul.txt + test_expect_success "checkout core.eol=$eol core.autocrlf=$crlf gitattributes=$attr file=LF_nul" " + compare_ws_file $pfx $crlfnul ${src}LF_nul.txt " } @@ -157,6 +164,7 @@ test_expect_success 'setup master' ' git commit -m "add .gitattributes" "" && printf "line1\nline2\nline3" >LF && printf "line1\r\nline2\r\nline3" >CRLF && + printf "line1\r\nline2\nline3" >repoMIX && printf "line1\r\nline2\nline3" >CRLF_mix_LF && printf "line1\nline2\rline3" >LF_mix_CR && printf "line1\r\nline2\rline3" >CRLF_mix_CR && @@ -169,40 +177,55 @@ test_expect_success 'setup master' ' warn_LF_CRLF="LF will be replaced by CRLF" warn_CRLF_LF="CRLF will be replaced by LF" -test_expect_success 'add files empty attr' ' - create_file_in_repo false "" "" "" "" "" "" && - create_file_in_repo true "" "LF_CRLF" "" "LF_CRLF" "" "" && - create_file_in_repo input "" "" "CRLF_LF" "CRLF_LF" "" "" +# WILC stands for "Warn if (this OS) converts LF into CRLF". +# WICL: Warn if CRLF becomes LF +# WAMIX: Mixed line endings: either CRLF->LF or LF->CRLF +if test_have_prereq NATIVE_CRLF +then + WILC=LF_CRLF + WICL= + WAMIX=LF_CRLF +else + WILC= + WICL=CRLF_LF + WAMIX=CRLF_LF +fi + +# attr LF CRLF repoMIX CRLFmixLF LFmixCR CRLFNUL +test_expect_success 'commit files empty attr' ' + commit_check_warn false "" "" "" "" "" "" "" && + commit_check_warn true "" "LF_CRLF" "" "LF_CRLF" "LF_CRLF" "" "" && + commit_check_warn input "" "" "CRLF_LF" "CRLF_LF" "CRLF_LF" "" "" ' -test_expect_success 'add files attr=auto' ' - create_file_in_repo false "auto" "" "CRLF_LF" "CRLF_LF" "" "" && - create_file_in_repo true "auto" "LF_CRLF" "" "LF_CRLF" "" "" && - create_file_in_repo input "auto" "" "CRLF_LF" "CRLF_LF" "" "" +test_expect_success 'commit files attr=auto' ' + commit_check_warn false "auto" "$WILC" "$WICL" "$WAMIX" "$WAMIX" "" "" && + commit_check_warn true "auto" "LF_CRLF" "" "LF_CRLF" "LF_CRLF" "" "" && + commit_check_warn input "auto" "" "CRLF_LF" "CRLF_LF" "CRLF_LF" "" "" ' -test_expect_success 'add files attr=text' ' - create_file_in_repo false "text" "" "CRLF_LF" "CRLF_LF" "" "CRLF_LF" && - create_file_in_repo true "text" "LF_CRLF" "" "LF_CRLF" "LF_CRLF" "" && - create_file_in_repo input "text" "" "CRLF_LF" "CRLF_LF" "" "CRLF_LF" +test_expect_success 'commit files attr=text' ' + commit_check_warn false "text" "$WILC" "$WICL" "$WAMIX" "$WAMIX" "$WILC" "$WICL" && + commit_check_warn true "text" "LF_CRLF" "" "LF_CRLF" "LF_CRLF" "LF_CRLF" "" && + commit_check_warn input "text" "" "CRLF_LF" "CRLF_LF" "CRLF_LF" "" "CRLF_LF" ' -test_expect_success 'add files attr=-text' ' - create_file_in_repo false "-text" "" "" "" "" "" && - create_file_in_repo true "-text" "" "" "" "" "" && - create_file_in_repo input "-text" "" "" "" "" "" +test_expect_success 'commit files attr=-text' ' + commit_check_warn false "-text" "" "" "" "" "" "" && + commit_check_warn true "-text" "" "" "" "" "" "" && + commit_check_warn input "-text" "" "" "" "" "" "" ' -test_expect_success 'add files attr=lf' ' - create_file_in_repo false "lf" "" "CRLF_LF" "CRLF_LF" "" "CRLF_LF" && - create_file_in_repo true "lf" "" "CRLF_LF" "CRLF_LF" "" "CRLF_LF" && - create_file_in_repo input "lf" "" "CRLF_LF" "CRLF_LF" "" "CRLF_LF" +test_expect_success 'commit files attr=lf' ' + commit_check_warn false "lf" "" "CRLF_LF" "CRLF_LF" "CRLF_LF" "" "CRLF_LF" && + commit_check_warn true "lf" "" "CRLF_LF" "CRLF_LF" "CRLF_LF" "" "CRLF_LF" && + commit_check_warn input "lf" "" "CRLF_LF" "CRLF_LF" "CRLF_LF" "" "CRLF_LF" ' -test_expect_success 'add files attr=crlf' ' - create_file_in_repo false "crlf" "LF_CRLF" "" "LF_CRLF" "LF_CRLF" "" && - create_file_in_repo true "crlf" "LF_CRLF" "" "LF_CRLF" "LF_CRLF" "" && - create_file_in_repo input "crlf" "LF_CRLF" "" "LF_CRLF" "LF_CRLF" "" +test_expect_success 'commit files attr=crlf' ' + commit_check_warn false "crlf" "LF_CRLF" "" "LF_CRLF" "LF_CRLF" "LF_CRLF" "" && + commit_check_warn true "crlf" "LF_CRLF" "" "LF_CRLF" "LF_CRLF" "LF_CRLF" "" && + commit_check_warn input "crlf" "LF_CRLF" "" "LF_CRLF" "LF_CRLF" "LF_CRLF" "" ' test_expect_success 'create files cleanup' ' @@ -237,7 +260,7 @@ test_expect_success 'commit -text' ' ################################################################################ # Check how files in the repo are changed when they are checked out # How to read the table below: -# - check_files_in_ws will check multiple files with a combination of settings +# - checkout_files will check multiple files with a combination of settings # and attributes (core.autocrlf=input is forbidden with core.eol=crlf) # - parameter $1 : core.eol lf | crlf # - parameter $2 : core.autocrlf false | true | input @@ -249,87 +272,89 @@ test_expect_success 'commit -text' ' # - parameter $8 : reference for a file with CRLF and a NUL (should be handled as binary when auto) # What we have in the repo: -# ----------------- EOL in repo ---------------- -# LF CRLF CRLF_mix_LF LF_mix_CR CRLF_nul +# ----------------- EOL in repo ---------------- +# LF CRLF CRLF_mix_LF LF_mix_CR CRLF_nul # settings with checkout: # core. core. .gitattr # eol acrlf # ---------------------------------------------- # What we want to have in the working tree: -if test_have_prereq MINGW +if test_have_prereq NATIVE_CRLF then MIX_CRLF_LF=CRLF MIX_LF_CR=CRLF_mix_CR NL=CRLF +LFNUL=CRLF_nul else MIX_CRLF_LF=CRLF_mix_LF MIX_LF_CR=LF_mix_CR NL=LF +LFNUL=LF_nul fi export CRLF_MIX_LF_CR MIX NL -check_files_in_ws lf false "" LF CRLF CRLF_mix_LF LF_mix_CR CRLF_nul -check_files_in_ws lf true "" CRLF CRLF CRLF_mix_LF LF_mix_CR CRLF_nul -check_files_in_ws lf input "" LF CRLF CRLF_mix_LF LF_mix_CR CRLF_nul -check_files_in_ws lf false "auto" LF CRLF CRLF_mix_LF LF_mix_CR CRLF_nul -check_files_in_ws lf true "auto" CRLF CRLF CRLF LF_mix_CR CRLF_nul -check_files_in_ws lf input "auto" LF CRLF CRLF_mix_LF LF_mix_CR CRLF_nul -check_files_in_ws lf false "text" LF CRLF CRLF_mix_LF LF_mix_CR CRLF_nul -check_files_in_ws lf true "text" CRLF CRLF CRLF CRLF_mix_CR CRLF_nul -check_files_in_ws lf input "text" LF CRLF CRLF_mix_LF LF_mix_CR CRLF_nul -check_files_in_ws lf false "-text" LF CRLF CRLF_mix_LF LF_mix_CR CRLF_nul -check_files_in_ws lf true "-text" LF CRLF CRLF_mix_LF LF_mix_CR CRLF_nul -check_files_in_ws lf input "-text" LF CRLF CRLF_mix_LF LF_mix_CR CRLF_nul -check_files_in_ws lf false "lf" LF CRLF CRLF_mix_LF LF_mix_CR CRLF_nul -check_files_in_ws lf true "lf" LF CRLF CRLF_mix_LF LF_mix_CR CRLF_nul -check_files_in_ws lf input "lf" LF CRLF CRLF_mix_LF LF_mix_CR CRLF_nul -check_files_in_ws lf false "crlf" CRLF CRLF CRLF CRLF_mix_CR CRLF_nul -check_files_in_ws lf true "crlf" CRLF CRLF CRLF CRLF_mix_CR CRLF_nul -check_files_in_ws lf input "crlf" CRLF CRLF CRLF CRLF_mix_CR CRLF_nul - -check_files_in_ws crlf false "" LF CRLF CRLF_mix_LF LF_mix_CR CRLF_nul -check_files_in_ws crlf true "" CRLF CRLF CRLF_mix_LF LF_mix_CR CRLF_nul -check_files_in_ws crlf false "auto" CRLF CRLF CRLF LF_mix_CR CRLF_nul -check_files_in_ws crlf true "auto" CRLF CRLF CRLF LF_mix_CR CRLF_nul -check_files_in_ws crlf false "text" CRLF CRLF CRLF CRLF_mix_CR CRLF_nul -check_files_in_ws crlf true "text" CRLF CRLF CRLF CRLF_mix_CR CRLF_nul -check_files_in_ws crlf false "-text" LF CRLF CRLF_mix_LF LF_mix_CR CRLF_nul -check_files_in_ws crlf true "-text" LF CRLF CRLF_mix_LF LF_mix_CR CRLF_nul -check_files_in_ws crlf false "lf" LF CRLF CRLF_mix_LF LF_mix_CR CRLF_nul -check_files_in_ws crlf true "lf" LF CRLF CRLF_mix_LF LF_mix_CR CRLF_nul -check_files_in_ws crlf false "crlf" CRLF CRLF CRLF CRLF_mix_CR CRLF_nul -check_files_in_ws crlf true "crlf" CRLF CRLF CRLF CRLF_mix_CR CRLF_nul - -check_files_in_ws "" false "" LF CRLF CRLF_mix_LF LF_mix_CR CRLF_nul -check_files_in_ws "" true "" CRLF CRLF CRLF_mix_LF LF_mix_CR CRLF_nul -check_files_in_ws "" input "" LF CRLF CRLF_mix_LF LF_mix_CR CRLF_nul -check_files_in_ws "" false "auto" $NL CRLF $MIX_CRLF_LF LF_mix_CR CRLF_nul -check_files_in_ws "" true "auto" CRLF CRLF CRLF LF_mix_CR CRLF_nul -check_files_in_ws "" input "auto" LF CRLF CRLF_mix_LF LF_mix_CR CRLF_nul -check_files_in_ws "" false "text" $NL CRLF $MIX_CRLF_LF $MIX_LF_CR CRLF_nul -check_files_in_ws "" true "text" CRLF CRLF CRLF CRLF_mix_CR CRLF_nul -check_files_in_ws "" input "text" LF CRLF CRLF_mix_LF LF_mix_CR CRLF_nul -check_files_in_ws "" false "-text" LF CRLF CRLF_mix_LF LF_mix_CR CRLF_nul -check_files_in_ws "" true "-text" LF CRLF CRLF_mix_LF LF_mix_CR CRLF_nul -check_files_in_ws "" input "-text" LF CRLF CRLF_mix_LF LF_mix_CR CRLF_nul -check_files_in_ws "" false "lf" LF CRLF CRLF_mix_LF LF_mix_CR CRLF_nul -check_files_in_ws "" true "lf" LF CRLF CRLF_mix_LF LF_mix_CR CRLF_nul -check_files_in_ws "" input "lf" LF CRLF CRLF_mix_LF LF_mix_CR CRLF_nul -check_files_in_ws "" false "crlf" CRLF CRLF CRLF CRLF_mix_CR CRLF_nul -check_files_in_ws "" true "crlf" CRLF CRLF CRLF CRLF_mix_CR CRLF_nul -check_files_in_ws "" input "crlf" CRLF CRLF CRLF CRLF_mix_CR CRLF_nul - -check_files_in_ws native false "" LF CRLF CRLF_mix_LF LF_mix_CR CRLF_nul -check_files_in_ws native true "" CRLF CRLF CRLF_mix_LF LF_mix_CR CRLF_nul -check_files_in_ws native false "auto" $NL CRLF $MIX_CRLF_LF LF_mix_CR CRLF_nul -check_files_in_ws native true "auto" CRLF CRLF CRLF LF_mix_CR CRLF_nul -check_files_in_ws native false "text" $NL CRLF $MIX_CRLF_LF $MIX_LF_CR CRLF_nul -check_files_in_ws native true "text" CRLF CRLF CRLF CRLF_mix_CR CRLF_nul -check_files_in_ws native false "-text" LF CRLF CRLF_mix_LF LF_mix_CR CRLF_nul -check_files_in_ws native true "-text" LF CRLF CRLF_mix_LF LF_mix_CR CRLF_nul -check_files_in_ws native false "lf" LF CRLF CRLF_mix_LF LF_mix_CR CRLF_nul -check_files_in_ws native true "lf" LF CRLF CRLF_mix_LF LF_mix_CR CRLF_nul -check_files_in_ws native false "crlf" CRLF CRLF CRLF CRLF_mix_CR CRLF_nul -check_files_in_ws native true "crlf" CRLF CRLF CRLF CRLF_mix_CR CRLF_nul +checkout_files lf false "" LF CRLF CRLF_mix_LF LF_mix_CR LF_nul +checkout_files lf true "" CRLF CRLF CRLF_mix_LF LF_mix_CR LF_nul +checkout_files lf input "" LF CRLF CRLF_mix_LF LF_mix_CR LF_nul +checkout_files lf false "auto" LF CRLF CRLF_mix_LF LF_mix_CR LF_nul +checkout_files lf true "auto" CRLF CRLF CRLF LF_mix_CR LF_nul +checkout_files lf input "auto" LF CRLF CRLF_mix_LF LF_mix_CR LF_nul +checkout_files lf false "text" LF CRLF CRLF_mix_LF LF_mix_CR LF_nul +checkout_files lf true "text" CRLF CRLF CRLF CRLF_mix_CR CRLF_nul +checkout_files lf input "text" LF CRLF CRLF_mix_LF LF_mix_CR LF_nul +checkout_files lf false "-text" LF CRLF CRLF_mix_LF LF_mix_CR LF_nul +checkout_files lf true "-text" LF CRLF CRLF_mix_LF LF_mix_CR LF_nul +checkout_files lf input "-text" LF CRLF CRLF_mix_LF LF_mix_CR LF_nul +checkout_files lf false "lf" LF CRLF CRLF_mix_LF LF_mix_CR LF_nul +checkout_files lf true "lf" LF CRLF CRLF_mix_LF LF_mix_CR LF_nul +checkout_files lf input "lf" LF CRLF CRLF_mix_LF LF_mix_CR LF_nul +checkout_files lf false "crlf" CRLF CRLF CRLF CRLF_mix_CR CRLF_nul +checkout_files lf true "crlf" CRLF CRLF CRLF CRLF_mix_CR CRLF_nul +checkout_files lf input "crlf" CRLF CRLF CRLF CRLF_mix_CR CRLF_nul + +checkout_files crlf false "" LF CRLF CRLF_mix_LF LF_mix_CR LF_nul +checkout_files crlf true "" CRLF CRLF CRLF_mix_LF LF_mix_CR LF_nul +checkout_files crlf false "auto" CRLF CRLF CRLF LF_mix_CR LF_nul +checkout_files crlf true "auto" CRLF CRLF CRLF LF_mix_CR LF_nul +checkout_files crlf false "text" CRLF CRLF CRLF CRLF_mix_CR CRLF_nul +checkout_files crlf true "text" CRLF CRLF CRLF CRLF_mix_CR CRLF_nul +checkout_files crlf false "-text" LF CRLF CRLF_mix_LF LF_mix_CR LF_nul +checkout_files crlf true "-text" LF CRLF CRLF_mix_LF LF_mix_CR LF_nul +checkout_files crlf false "lf" LF CRLF CRLF_mix_LF LF_mix_CR LF_nul +checkout_files crlf true "lf" LF CRLF CRLF_mix_LF LF_mix_CR LF_nul +checkout_files crlf false "crlf" CRLF CRLF CRLF CRLF_mix_CR CRLF_nul +checkout_files crlf true "crlf" CRLF CRLF CRLF CRLF_mix_CR CRLF_nul + +checkout_files "" false "" LF CRLF CRLF_mix_LF LF_mix_CR LF_nul +checkout_files "" true "" CRLF CRLF CRLF_mix_LF LF_mix_CR LF_nul +checkout_files "" input "" LF CRLF CRLF_mix_LF LF_mix_CR LF_nul +checkout_files "" false "auto" $NL CRLF $MIX_CRLF_LF LF_mix_CR LF_nul +checkout_files "" true "auto" CRLF CRLF CRLF LF_mix_CR LF_nul +checkout_files "" input "auto" LF CRLF CRLF_mix_LF LF_mix_CR LF_nul +checkout_files "" false "text" $NL CRLF $MIX_CRLF_LF $MIX_LF_CR $LFNUL +checkout_files "" true "text" CRLF CRLF CRLF CRLF_mix_CR CRLF_nul +checkout_files "" input "text" LF CRLF CRLF_mix_LF LF_mix_CR LF_nul +checkout_files "" false "-text" LF CRLF CRLF_mix_LF LF_mix_CR LF_nul +checkout_files "" true "-text" LF CRLF CRLF_mix_LF LF_mix_CR LF_nul +checkout_files "" input "-text" LF CRLF CRLF_mix_LF LF_mix_CR LF_nul +checkout_files "" false "lf" LF CRLF CRLF_mix_LF LF_mix_CR LF_nul +checkout_files "" true "lf" LF CRLF CRLF_mix_LF LF_mix_CR LF_nul +checkout_files "" input "lf" LF CRLF CRLF_mix_LF LF_mix_CR LF_nul +checkout_files "" false "crlf" CRLF CRLF CRLF CRLF_mix_CR CRLF_nul +checkout_files "" true "crlf" CRLF CRLF CRLF CRLF_mix_CR CRLF_nul +checkout_files "" input "crlf" CRLF CRLF CRLF CRLF_mix_CR CRLF_nul + +checkout_files native false "" LF CRLF CRLF_mix_LF LF_mix_CR LF_nul +checkout_files native true "" CRLF CRLF CRLF_mix_LF LF_mix_CR LF_nul +checkout_files native false "auto" $NL CRLF $MIX_CRLF_LF LF_mix_CR LF_nul +checkout_files native true "auto" CRLF CRLF CRLF LF_mix_CR LF_nul +checkout_files native false "text" $NL CRLF $MIX_CRLF_LF $MIX_LF_CR $LFNUL +checkout_files native true "text" CRLF CRLF CRLF CRLF_mix_CR CRLF_nul +checkout_files native false "-text" LF CRLF CRLF_mix_LF LF_mix_CR LF_nul +checkout_files native true "-text" LF CRLF CRLF_mix_LF LF_mix_CR LF_nul +checkout_files native false "lf" LF CRLF CRLF_mix_LF LF_mix_CR LF_nul +checkout_files native true "lf" LF CRLF CRLF_mix_LF LF_mix_CR LF_nul +checkout_files native false "crlf" CRLF CRLF CRLF CRLF_mix_CR CRLF_nul +checkout_files native true "crlf" CRLF CRLF CRLF CRLF_mix_CR CRLF_nul test_done diff --git a/t/t0060-path-utils.sh b/t/t0060-path-utils.sh index c0143a0a70..93605f42f2 100755 --- a/t/t0060-path-utils.sh +++ b/t/t0060-path-utils.sh @@ -19,6 +19,14 @@ relative_path() { "test \"\$(test-path-utils relative_path '$1' '$2')\" = '$expected'" } +test_git_path() { + test_expect_success "git-path $1 $2 => $3" " + $1 git rev-parse --git-path $2 >actual && + echo $3 >expect && + test_cmp expect actual + " +} + # On Windows, we are using MSYS's bash, which mangles the paths. # Absolute paths are anchored at the MSYS installation directory, # which means that the path / accounts for this many characters: @@ -244,4 +252,32 @@ relative_path "<null>" "<empty>" ./ relative_path "<null>" "<null>" ./ relative_path "<null>" /foo/a/b ./ +test_git_path A=B info/grafts .git/info/grafts +test_git_path GIT_GRAFT_FILE=foo info/grafts foo +test_git_path GIT_GRAFT_FILE=foo info/////grafts foo +test_git_path GIT_INDEX_FILE=foo index foo +test_git_path GIT_INDEX_FILE=foo index/foo .git/index/foo +test_git_path GIT_INDEX_FILE=foo index2 .git/index2 +test_expect_success 'setup fake objects directory foo' 'mkdir foo' +test_git_path GIT_OBJECT_DIRECTORY=foo objects foo +test_git_path GIT_OBJECT_DIRECTORY=foo objects/foo foo/foo +test_git_path GIT_OBJECT_DIRECTORY=foo objects2 .git/objects2 +test_expect_success 'setup common repository' 'git --git-dir=bar init' +test_git_path GIT_COMMON_DIR=bar index .git/index +test_git_path GIT_COMMON_DIR=bar HEAD .git/HEAD +test_git_path GIT_COMMON_DIR=bar logs/HEAD .git/logs/HEAD +test_git_path GIT_COMMON_DIR=bar objects bar/objects +test_git_path GIT_COMMON_DIR=bar objects/bar bar/objects/bar +test_git_path GIT_COMMON_DIR=bar info/exclude bar/info/exclude +test_git_path GIT_COMMON_DIR=bar info/grafts bar/info/grafts +test_git_path GIT_COMMON_DIR=bar info/sparse-checkout .git/info/sparse-checkout +test_git_path GIT_COMMON_DIR=bar remotes/bar bar/remotes/bar +test_git_path GIT_COMMON_DIR=bar branches/bar bar/branches/bar +test_git_path GIT_COMMON_DIR=bar logs/refs/heads/master bar/logs/refs/heads/master +test_git_path GIT_COMMON_DIR=bar refs/heads/master bar/refs/heads/master +test_git_path GIT_COMMON_DIR=bar hooks/me bar/hooks/me +test_git_path GIT_COMMON_DIR=bar config bar/config +test_git_path GIT_COMMON_DIR=bar packed-refs bar/packed-refs +test_git_path GIT_COMMON_DIR=bar shallow bar/shallow + test_done diff --git a/t/t0302-credential-store.sh b/t/t0302-credential-store.sh index 0979df93a1..1d8d1f210b 100755 --- a/t/t0302-credential-store.sh +++ b/t/t0302-credential-store.sh @@ -75,7 +75,7 @@ test_expect_success 'get: use xdg file if home file has no matches' ' EOF ' -test_expect_success POSIXPERM 'get: use xdg file if home file is unreadable' ' +test_expect_success POSIXPERM,SANITY 'get: use xdg file if home file is unreadable' ' echo "https://home-user:home-pass@example.com" >"$HOME/.git-credentials" && chmod -r "$HOME/.git-credentials" && mkdir -p "$HOME/.config/git" && diff --git a/t/t1006-cat-file.sh b/t/t1006-cat-file.sh index ab36b1eb72..93a4794930 100755 --- a/t/t1006-cat-file.sh +++ b/t/t1006-cat-file.sh @@ -47,6 +47,18 @@ $content" test_cmp expect actual ' + test_expect_success "Type of $type is correct using --allow-unknown-type" ' + echo $type >expect && + git cat-file -t --allow-unknown-type $sha1 >actual && + test_cmp expect actual + ' + + test_expect_success "Size of $type is correct using --allow-unknown-type" ' + echo $size >expect && + git cat-file -s --allow-unknown-type $sha1 >actual && + test_cmp expect actual + ' + test -z "$content" || test_expect_success "Content of $type is correct" ' maybe_remove_timestamp "$content" $no_ts >expect && @@ -189,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" = \ @@ -296,4 +315,236 @@ test_expect_success '%(deltabase) reports packed delta bases' ' } ' +bogus_type="bogus" +bogus_content="bogus" +bogus_size=$(strlen "$bogus_content") +bogus_sha1=$(echo_without_newline "$bogus_content" | git hash-object -t $bogus_type --literally -w --stdin) + +test_expect_success "Type of broken object is correct" ' + echo $bogus_type >expect && + git cat-file -t --allow-unknown-type $bogus_sha1 >actual && + test_cmp expect actual +' + +test_expect_success "Size of broken object is correct" ' + echo $bogus_size >expect && + git cat-file -s --allow-unknown-type $bogus_sha1 >actual && + test_cmp expect actual +' +bogus_type="abcdefghijklmnopqrstuvwxyz1234679" +bogus_content="bogus" +bogus_size=$(strlen "$bogus_content") +bogus_sha1=$(echo_without_newline "$bogus_content" | git hash-object -t $bogus_type --literally -w --stdin) + +test_expect_success "Type of broken object is correct when type is large" ' + echo $bogus_type >expect && + git cat-file -t --allow-unknown-type $bogus_sha1 >actual && + test_cmp expect actual +' + +test_expect_success "Size of large broken object is correct when type is large" ' + echo $bogus_size >expect && + git cat-file -s --allow-unknown-type $bogus_sha1 >actual && + 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/t1400-update-ref.sh b/t/t1400-update-ref.sh index ba89f4c009..d787bf50f8 100755 --- a/t/t1400-update-ref.sh +++ b/t/t1400-update-ref.sh @@ -519,7 +519,7 @@ test_expect_success 'stdin create ref works with path with space to blob' ' test_expect_success 'stdin update ref fails with wrong old value' ' echo "update $c $m $m~1" >stdin && test_must_fail git update-ref --stdin <stdin 2>err && - grep "fatal: Cannot lock ref '"'"'$c'"'"'" err && + grep "fatal: cannot lock ref '"'"'$c'"'"'" err && test_must_fail git rev-parse --verify -q $c ' @@ -555,7 +555,7 @@ test_expect_success 'stdin update ref works with right old value' ' test_expect_success 'stdin delete ref fails with wrong old value' ' echo "delete $a $m~1" >stdin && test_must_fail git update-ref --stdin <stdin 2>err && - grep "fatal: Cannot lock ref '"'"'$a'"'"'" err && + grep "fatal: cannot lock ref '"'"'$a'"'"'" err && git rev-parse $m >expect && git rev-parse $a >actual && test_cmp expect actual @@ -688,7 +688,7 @@ test_expect_success 'stdin update refs fails with wrong old value' ' update $c '' EOF test_must_fail git update-ref --stdin <stdin 2>err && - grep "fatal: Cannot lock ref '"'"'$c'"'"'" err && + grep "fatal: cannot lock ref '"'"'$c'"'"'" err && git rev-parse $m >expect && git rev-parse $a >actual && test_cmp expect actual && @@ -883,7 +883,7 @@ test_expect_success 'stdin -z create ref works with path with space to blob' ' test_expect_success 'stdin -z update ref fails with wrong old value' ' printf $F "update $c" "$m" "$m~1" >stdin && test_must_fail git update-ref -z --stdin <stdin 2>err && - grep "fatal: Cannot lock ref '"'"'$c'"'"'" err && + grep "fatal: cannot lock ref '"'"'$c'"'"'" err && test_must_fail git rev-parse --verify -q $c ' @@ -899,7 +899,7 @@ test_expect_success 'stdin -z create ref fails when ref exists' ' git rev-parse "$c" >expect && printf $F "create $c" "$m~1" >stdin && test_must_fail git update-ref -z --stdin <stdin 2>err && - grep "fatal: Cannot lock ref '"'"'$c'"'"'" err && + grep "fatal: cannot lock ref '"'"'$c'"'"'" err && git rev-parse "$c" >actual && test_cmp expect actual ' @@ -930,7 +930,7 @@ test_expect_success 'stdin -z update ref works with right old value' ' test_expect_success 'stdin -z delete ref fails with wrong old value' ' printf $F "delete $a" "$m~1" >stdin && test_must_fail git update-ref -z --stdin <stdin 2>err && - grep "fatal: Cannot lock ref '"'"'$a'"'"'" err && + grep "fatal: cannot lock ref '"'"'$a'"'"'" err && git rev-parse $m >expect && git rev-parse $a >actual && test_cmp expect actual @@ -1045,7 +1045,7 @@ test_expect_success 'stdin -z update refs fails with wrong old value' ' git update-ref $c $m && printf $F "update $a" "$m" "$m" "update $b" "$m" "$m" "update $c" "$m" "$Z" >stdin && test_must_fail git update-ref -z --stdin <stdin 2>err && - grep "fatal: Cannot lock ref '"'"'$c'"'"'" err && + grep "fatal: cannot lock ref '"'"'$c'"'"'" err && git rev-parse $m >expect && git rev-parse $a >actual && test_cmp expect actual && diff --git a/t/t1430-bad-ref-name.sh b/t/t1430-bad-ref-name.sh index 468e85621a..16d0b8bd1a 100755 --- a/t/t1430-bad-ref-name.sh +++ b/t/t1430-bad-ref-name.sh @@ -68,6 +68,14 @@ test_expect_success 'branch -D cannot delete non-ref in .git dir' ' test_cmp expect .git/my-private-file ' +test_expect_success 'branch -D cannot delete ref in .git dir' ' + git rev-parse HEAD >.git/my-private-file && + git rev-parse HEAD >expect && + git branch foo/legit && + test_must_fail git branch -D foo////./././../../../my-private-file && + test_cmp expect .git/my-private-file +' + test_expect_success 'branch -D cannot delete absolute path' ' git branch -f extra && test_must_fail git branch -D "$(pwd)/.git/refs/heads/extra" && diff --git a/t/t1501-worktree.sh b/t/t1501-worktree.sh index 8f36aa9fc4..cc5b870e58 100755 --- a/t/t1501-worktree.sh +++ b/t/t1501-worktree.sh @@ -346,4 +346,81 @@ test_expect_success 'relative $GIT_WORK_TREE and git subprocesses' ' test_cmp expected actual ' +test_expect_success 'Multi-worktree setup' ' + mkdir work && + mkdir -p repo.git/repos/foo && + cp repo.git/HEAD repo.git/index repo.git/repos/foo && + test_might_fail cp repo.git/sharedindex.* repo.git/repos/foo && + sane_unset GIT_DIR GIT_CONFIG GIT_WORK_TREE +' + +test_expect_success 'GIT_DIR set (1)' ' + echo "gitdir: repo.git/repos/foo" >gitfile && + echo ../.. >repo.git/repos/foo/commondir && + ( + cd work && + GIT_DIR=../gitfile git rev-parse --git-common-dir >actual && + test-path-utils real_path "$TRASH_DIRECTORY/repo.git" >expect && + test_cmp expect actual + ) +' + +test_expect_success 'GIT_DIR set (2)' ' + echo "gitdir: repo.git/repos/foo" >gitfile && + echo "$(pwd)/repo.git" >repo.git/repos/foo/commondir && + ( + cd work && + GIT_DIR=../gitfile git rev-parse --git-common-dir >actual && + test-path-utils real_path "$TRASH_DIRECTORY/repo.git" >expect && + test_cmp expect actual + ) +' + +test_expect_success 'Auto discovery' ' + echo "gitdir: repo.git/repos/foo" >.git && + echo ../.. >repo.git/repos/foo/commondir && + ( + cd work && + git rev-parse --git-common-dir >actual && + test-path-utils real_path "$TRASH_DIRECTORY/repo.git" >expect && + test_cmp expect actual && + echo haha >data1 && + git add data1 && + git ls-files --full-name :/ | grep data1 >actual && + echo work/data1 >expect && + test_cmp expect actual + ) +' + +test_expect_success '$GIT_DIR/common overrides core.worktree' ' + mkdir elsewhere && + git --git-dir=repo.git config core.worktree "$TRASH_DIRECTORY/elsewhere" && + echo "gitdir: repo.git/repos/foo" >.git && + echo ../.. >repo.git/repos/foo/commondir && + ( + cd work && + git rev-parse --git-common-dir >actual && + test-path-utils real_path "$TRASH_DIRECTORY/repo.git" >expect && + test_cmp expect actual && + echo haha >data2 && + git add data2 && + git ls-files --full-name :/ | grep data2 >actual && + echo work/data2 >expect && + test_cmp expect actual + ) +' + +test_expect_success '$GIT_WORK_TREE overrides $GIT_DIR/common' ' + echo "gitdir: repo.git/repos/foo" >.git && + echo ../.. >repo.git/repos/foo/commondir && + ( + cd work && + echo haha >data3 && + git --git-dir=../.git --work-tree=. add data3 && + git ls-files --full-name -- :/ | grep data3 >actual && + echo data3 >expect && + 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/t1510-repo-setup.sh b/t/t1510-repo-setup.sh index f6aa3c70f8..13ae12dfa7 100755 --- a/t/t1510-repo-setup.sh +++ b/t/t1510-repo-setup.sh @@ -106,6 +106,7 @@ setup_env () { expect () { cat >"$1/expected" <<-EOF setup: git_dir: $2 + setup: git_common_dir: $2 setup: worktree: $3 setup: cwd: $4 setup: prefix: $5 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/t2025-checkout-to.sh b/t/t2025-checkout-to.sh new file mode 100755 index 0000000000..f8e4df4818 --- /dev/null +++ b/t/t2025-checkout-to.sh @@ -0,0 +1,129 @@ +#!/bin/sh + +test_description='test git checkout --to' + +. ./test-lib.sh + +test_expect_success 'setup' ' + test_commit init +' + +test_expect_success 'checkout --to not updating paths' ' + test_must_fail git checkout --to -- init.t +' + +test_expect_success 'checkout --to an existing worktree' ' + mkdir -p existing/subtree && + test_must_fail git checkout --detach --to existing master +' + +test_expect_success 'checkout --to an existing empty worktree' ' + mkdir existing_empty && + git checkout --detach --to existing_empty master +' + +test_expect_success 'checkout --to refuses to checkout locked branch' ' + test_must_fail git checkout --to zere master && + ! test -d zere && + ! test -d .git/worktrees/zere +' + +test_expect_success 'checkout --to a new worktree' ' + git rev-parse HEAD >expect && + git checkout --detach --to here master && + ( + cd here && + test_cmp ../init.t init.t && + test_must_fail git symbolic-ref HEAD && + git rev-parse HEAD >actual && + test_cmp ../expect actual && + git fsck + ) +' + +test_expect_success 'checkout --to a new worktree from a subdir' ' + ( + mkdir sub && + cd sub && + git checkout --detach --to here master && + cd here && + test_cmp ../../init.t init.t + ) +' + +test_expect_success 'checkout --to from a linked checkout' ' + ( + cd here && + git checkout --detach --to nested-here master && + cd nested-here && + git fsck + ) +' + +test_expect_success 'checkout --to a new worktree creating new branch' ' + git checkout --to there -b newmaster master && + ( + cd there && + test_cmp ../init.t init.t && + git symbolic-ref HEAD >actual && + echo refs/heads/newmaster >expect && + test_cmp expect actual && + git fsck + ) +' + +test_expect_success 'die the same branch is already checked out' ' + ( + cd here && + test_must_fail git checkout newmaster + ) +' + +test_expect_success 'not die the same branch is already checked out' ' + ( + cd here && + git checkout --ignore-other-worktrees --to anothernewmaster newmaster + ) +' + +test_expect_success 'not die on re-checking out current branch' ' + ( + cd there && + git checkout newmaster + ) +' + +test_expect_success 'checkout --to from a bare repo' ' + ( + git clone --bare . bare && + cd bare && + git checkout --to ../there2 -b bare-master master + ) +' + +test_expect_success 'checkout from a bare repo without --to' ' + ( + cd bare && + test_must_fail git checkout master + ) +' + +test_expect_success 'checkout with grafts' ' + test_when_finished rm .git/info/grafts && + test_commit abc && + SHA1=`git rev-parse HEAD` && + test_commit def && + test_commit xyz && + echo "`git rev-parse HEAD` $SHA1" >.git/info/grafts && + cat >expected <<-\EOF && + xyz + abc + EOF + git log --format=%s -2 >actual && + test_cmp expected actual && + git checkout --detach --to grafted master && + git --git-dir=grafted/.git log --format=%s -2 >actual && + test_cmp expected actual +' + +test_done diff --git a/t/t2026-prune-linked-checkouts.sh b/t/t2026-prune-linked-checkouts.sh new file mode 100755 index 0000000000..1821a480c5 --- /dev/null +++ b/t/t2026-prune-linked-checkouts.sh @@ -0,0 +1,96 @@ +#!/bin/sh + +test_description='prune $GIT_DIR/worktrees' + +. ./test-lib.sh + +test_expect_success initialize ' + git commit --allow-empty -m init +' + +test_expect_success 'prune --worktrees on normal repo' ' + git prune --worktrees && + test_must_fail git prune --worktrees abc +' + +test_expect_success 'prune files inside $GIT_DIR/worktrees' ' + mkdir .git/worktrees && + : >.git/worktrees/abc && + git prune --worktrees --verbose >actual && + cat >expect <<EOF && +Removing worktrees/abc: not a valid directory +EOF + test_i18ncmp expect actual && + ! test -f .git/worktrees/abc && + ! test -d .git/worktrees +' + +test_expect_success 'prune directories without gitdir' ' + mkdir -p .git/worktrees/def/abc && + : >.git/worktrees/def/def && + cat >expect <<EOF && +Removing worktrees/def: gitdir file does not exist +EOF + git prune --worktrees --verbose >actual && + test_i18ncmp expect actual && + ! test -d .git/worktrees/def && + ! test -d .git/worktrees +' + +test_expect_success SANITY 'prune directories with unreadable gitdir' ' + mkdir -p .git/worktrees/def/abc && + : >.git/worktrees/def/def && + : >.git/worktrees/def/gitdir && + chmod u-r .git/worktrees/def/gitdir && + git prune --worktrees --verbose >actual && + test_i18ngrep "Removing worktrees/def: unable to read gitdir file" actual && + ! test -d .git/worktrees/def && + ! test -d .git/worktrees +' + +test_expect_success 'prune directories with invalid gitdir' ' + mkdir -p .git/worktrees/def/abc && + : >.git/worktrees/def/def && + : >.git/worktrees/def/gitdir && + git prune --worktrees --verbose >actual && + test_i18ngrep "Removing worktrees/def: invalid gitdir file" actual && + ! test -d .git/worktrees/def && + ! test -d .git/worktrees +' + +test_expect_success 'prune directories with gitdir pointing to nowhere' ' + mkdir -p .git/worktrees/def/abc && + : >.git/worktrees/def/def && + echo "$(pwd)"/nowhere >.git/worktrees/def/gitdir && + git prune --worktrees --verbose >actual && + test_i18ngrep "Removing worktrees/def: gitdir file points to non-existent location" actual && + ! test -d .git/worktrees/def && + ! test -d .git/worktrees +' + +test_expect_success 'not prune locked checkout' ' + test_when_finished rm -r .git/worktrees && + mkdir -p .git/worktrees/ghi && + : >.git/worktrees/ghi/locked && + git prune --worktrees && + test -d .git/worktrees/ghi +' + +test_expect_success 'not prune recent checkouts' ' + test_when_finished rm -r .git/worktrees && + mkdir zz && + mkdir -p .git/worktrees/jlm && + echo "$(pwd)"/zz >.git/worktrees/jlm/gitdir && + rmdir zz && + git prune --worktrees --verbose --expire=2.days.ago && + test -d .git/worktrees/jlm +' + +test_expect_success 'not prune proper checkouts' ' + test_when_finished rm -r .git/worktrees && + git checkout "--to=$PWD/nop" --detach master && + git prune --worktrees && + test -d .git/worktrees/nop +' + +test_done diff --git a/t/t3033-merge-toplevel.sh b/t/t3033-merge-toplevel.sh new file mode 100755 index 0000000000..46aadc410b --- /dev/null +++ b/t/t3033-merge-toplevel.sh @@ -0,0 +1,136 @@ +#!/bin/sh + +test_description='"git merge" top-level frontend' + +. ./test-lib.sh + +t3033_reset () { + git checkout -B master two && + git branch -f left three && + git branch -f right four +} + +test_expect_success setup ' + test_commit one && + git branch left && + git branch right && + test_commit two && + git checkout left && + test_commit three && + git checkout right && + test_commit four && + git checkout master +' + +# Local branches + +test_expect_success 'merge an octopus into void' ' + t3033_reset && + git checkout --orphan test && + git rm -fr . && + test_must_fail git merge left right && + test_must_fail git rev-parse --verify HEAD && + git diff --quiet && + test_must_fail git rev-parse HEAD +' + +test_expect_success 'merge an octopus, fast-forward (ff)' ' + t3033_reset && + git reset --hard one && + git merge left right && + # one is ancestor of three (left) and four (right) + test_must_fail git rev-parse --verify HEAD^3 && + git rev-parse HEAD^1 HEAD^2 | sort >actual && + git rev-parse three four | sort >expect && + test_cmp expect actual +' + +test_expect_success 'merge octopus, non-fast-forward (ff)' ' + t3033_reset && + git reset --hard one && + git merge --no-ff left right && + # one is ancestor of three (left) and four (right) + test_must_fail git rev-parse --verify HEAD^4 && + git rev-parse HEAD^1 HEAD^2 HEAD^3 | sort >actual && + git rev-parse one three four | sort >expect && + test_cmp expect actual +' + +test_expect_success 'merge octopus, fast-forward (does not ff)' ' + t3033_reset && + git merge left right && + # two (master) is not an ancestor of three (left) and four (right) + test_must_fail git rev-parse --verify HEAD^4 && + git rev-parse HEAD^1 HEAD^2 HEAD^3 | sort >actual && + git rev-parse two three four | sort >expect && + test_cmp expect actual +' + +test_expect_success 'merge octopus, non-fast-forward' ' + t3033_reset && + git merge --no-ff left right && + test_must_fail git rev-parse --verify HEAD^4 && + git rev-parse HEAD^1 HEAD^2 HEAD^3 | sort >actual && + git rev-parse two three four | sort >expect && + test_cmp expect actual +' + +# The same set with FETCH_HEAD + +test_expect_success 'merge FETCH_HEAD octopus into void' ' + t3033_reset && + git checkout --orphan test && + git rm -fr . && + git fetch . left right && + test_must_fail git merge FETCH_HEAD && + test_must_fail git rev-parse --verify HEAD && + git diff --quiet && + test_must_fail git rev-parse HEAD +' + +test_expect_success 'merge FETCH_HEAD octopus fast-forward (ff)' ' + t3033_reset && + git reset --hard one && + git fetch . left right && + git merge FETCH_HEAD && + # one is ancestor of three (left) and four (right) + test_must_fail git rev-parse --verify HEAD^3 && + git rev-parse HEAD^1 HEAD^2 | sort >actual && + git rev-parse three four | sort >expect && + test_cmp expect actual +' + +test_expect_success 'merge FETCH_HEAD octopus non-fast-forward (ff)' ' + t3033_reset && + git reset --hard one && + git fetch . left right && + git merge --no-ff FETCH_HEAD && + # one is ancestor of three (left) and four (right) + test_must_fail git rev-parse --verify HEAD^4 && + git rev-parse HEAD^1 HEAD^2 HEAD^3 | sort >actual && + git rev-parse one three four | sort >expect && + test_cmp expect actual +' + +test_expect_success 'merge FETCH_HEAD octopus fast-forward (does not ff)' ' + t3033_reset && + git fetch . left right && + git merge FETCH_HEAD && + # two (master) is not an ancestor of three (left) and four (right) + test_must_fail git rev-parse --verify HEAD^4 && + git rev-parse HEAD^1 HEAD^2 HEAD^3 | sort >actual && + git rev-parse two three four | sort >expect && + test_cmp expect actual +' + +test_expect_success 'merge FETCH_HEAD octopus non-fast-forward' ' + t3033_reset && + git fetch . left right && + git merge --no-ff FETCH_HEAD && + test_must_fail git rev-parse --verify HEAD^4 && + git rev-parse HEAD^1 HEAD^2 HEAD^3 | sort >actual && + git rev-parse two three four | sort >expect && + test_cmp expect actual +' + +test_done diff --git a/t/t3210-pack-refs.sh b/t/t3210-pack-refs.sh index aa9eb3a3e5..8aae98d482 100755 --- a/t/t3210-pack-refs.sh +++ b/t/t3210-pack-refs.sh @@ -187,4 +187,21 @@ test_expect_success 'notice d/f conflict with existing ref' ' test_must_fail git branch foo/bar/baz/lots/of/extra/components ' +test_expect_success 'timeout if packed-refs.lock exists' ' + LOCK=.git/packed-refs.lock && + >"$LOCK" && + test_when_finished "rm -f $LOCK" && + test_must_fail git pack-refs --all --prune +' + +test_expect_success 'retry acquiring packed-refs.lock' ' + LOCK=.git/packed-refs.lock && + >"$LOCK" && + test_when_finished "wait; rm -f $LOCK" && + { + ( sleep 1 ; rm -f $LOCK ) & + } && + git -c core.packedrefstimeout=3000 pack-refs --all --prune +' + test_done diff --git a/t/t3402-rebase-merge.sh b/t/t3402-rebase-merge.sh index 5a27ec9b5e..8f64505e4f 100755 --- a/t/t3402-rebase-merge.sh +++ b/t/t3402-rebase-merge.sh @@ -47,7 +47,7 @@ test_expect_success setup ' ' test_expect_success 'reference merge' ' - git merge -s recursive "reference merge" HEAD master + git merge -s recursive -m "reference merge" master ' PRE_REBASE=$(git rev-parse test-rebase) diff --git a/t/t3701-add-interactive.sh b/t/t3701-add-interactive.sh index 24ddd8a704..deae948c76 100755 --- a/t/t3701-add-interactive.sh +++ b/t/t3701-add-interactive.sh @@ -326,15 +326,34 @@ test_expect_success 'split hunk "add -p (edit)"' ' # 2. Correct version applies the (not)edited version, and asks # about the next hunk, against which we say q and program # exits. - for a in s e q n q q - do - echo $a - done | + printf "%s\n" s e q n q q | EDITOR=: git add -p && git diff >actual && ! grep "^+15" actual ' +test_expect_failure 'split hunk "add -p (no, yes, edit)"' ' + cat >test <<-\EOF && + 5 + 10 + 20 + 21 + 30 + 31 + 40 + 50 + 60 + EOF + git reset && + # test sequence is s(plit), n(o), y(es), e(dit) + # q n q q is there to make sure we exit at the end. + printf "%s\n" s n y e q n q q | + EDITOR=: git add -p 2>error && + test_must_be_empty error && + git diff >actual && + ! grep "^+31" actual +' + test_expect_success 'patch mode ignores unmerged entries' ' git reset --hard && test_commit conflict && diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh index 7396ca9911..f5f18b7d21 100755 --- a/t/t3903-stash.sh +++ b/t/t3903-stash.sh @@ -45,13 +45,6 @@ test_expect_success 'applying bogus stash does nothing' ' test_cmp expect file ' -test_expect_success 'apply requires a clean index' ' - test_when_finished "git reset --hard" && - echo changed >other-file && - git add other-file && - test_must_fail git stash apply -' - test_expect_success 'apply does not need clean working directory' ' echo 4 >other-file && git stash apply && diff --git a/t/t3904-stash-patch.sh b/t/t3904-stash-patch.sh index 70655c1848..38e730090f 100755 --- a/t/t3904-stash-patch.sh +++ b/t/t3904-stash-patch.sh @@ -1,9 +1,15 @@ #!/bin/sh -test_description='git checkout --patch' +test_description='stash -p' . ./lib-patch-mode.sh -test_expect_success PERL 'setup' ' +if ! test_have_prereq PERL +then + skip_all='skipping stash -p tests, perl not available' + test_done +fi + +test_expect_success 'setup' ' mkdir dir && echo parent > dir/foo && echo dummy > bar && @@ -20,7 +26,7 @@ test_expect_success PERL 'setup' ' # note: order of files with unstaged changes: HEAD bar dir/foo -test_expect_success PERL 'saying "n" does nothing' ' +test_expect_success 'saying "n" does nothing' ' set_state HEAD HEADfile_work HEADfile_index && set_state dir/foo work index && (echo n; echo n; echo n) | test_must_fail git stash save -p && @@ -29,7 +35,7 @@ test_expect_success PERL 'saying "n" does nothing' ' verify_state dir/foo work index ' -test_expect_success PERL 'git stash -p' ' +test_expect_success 'git stash -p' ' (echo y; echo n; echo y) | git stash save -p && verify_state HEAD committed HEADfile_index && verify_saved_state bar && @@ -41,7 +47,7 @@ test_expect_success PERL 'git stash -p' ' verify_state dir/foo work head ' -test_expect_success PERL 'git stash -p --no-keep-index' ' +test_expect_success 'git stash -p --no-keep-index' ' set_state HEAD HEADfile_work HEADfile_index && set_state bar bar_work bar_index && set_state dir/foo work index && @@ -56,7 +62,7 @@ test_expect_success PERL 'git stash -p --no-keep-index' ' verify_state dir/foo work index ' -test_expect_success PERL 'git stash --no-keep-index -p' ' +test_expect_success 'git stash --no-keep-index -p' ' set_state HEAD HEADfile_work HEADfile_index && set_state bar bar_work bar_index && set_state dir/foo work index && @@ -71,8 +77,31 @@ test_expect_success PERL 'git stash --no-keep-index -p' ' verify_state dir/foo work index ' -test_expect_success PERL 'none of this moved HEAD' ' +test_expect_success 'none of this moved HEAD' ' verify_saved_head ' +test_expect_failure 'stash -p with split hunk' ' + git reset --hard && + cat >test <<-\EOF && + aaa + bbb + ccc + EOF + git add test && + git commit -m "initial" && + cat >test <<-\EOF && + aaa + added line 1 + bbb + added line 2 + ccc + EOF + printf "%s\n" s n y q | + test_might_fail git stash -p 2>error && + ! test_must_be_empty error && + grep "added line 1" test && + ! grep "added line 2" test +' + test_done diff --git a/t/t4014-format-patch.sh b/t/t4014-format-patch.sh index c39e50028f..890db1174f 100755 --- a/t/t4014-format-patch.sh +++ b/t/t4014-format-patch.sh @@ -57,6 +57,14 @@ test_expect_success "format-patch --ignore-if-in-upstream" ' ' +test_expect_success "format-patch --ignore-if-in-upstream handles tags" ' + git tag -a v1 -m tag side && + git tag -a v2 -m tag master && + git format-patch --stdout --ignore-if-in-upstream v2..v1 >patch1 && + cnt=$(grep "^From " patch1 | wc -l) && + test $cnt = 2 +' + test_expect_success "format-patch doesn't consider merge commits" ' git checkout -b slave master && diff --git a/t/t4015-diff-whitespace.sh b/t/t4015-diff-whitespace.sh index 604a838c1a..2434157aa7 100755 --- a/t/t4015-diff-whitespace.sh +++ b/t/t4015-diff-whitespace.sh @@ -9,138 +9,144 @@ test_description='Test special whitespace in diff engine. . ./test-lib.sh . "$TEST_DIRECTORY"/diff-lib.sh -# Ray Lehtiniemi's example +test_expect_success "Ray Lehtiniemi's example" ' + cat <<-\EOF >x && + do { + nothing; + } while (0); + EOF + git update-index --add x && -cat << EOF > x -do { - nothing; -} while (0); -EOF + cat <<-\EOF >x && + do + { + nothing; + } + while (0); + EOF + + cat <<-\EOF >expect && + diff --git a/x b/x + index adf3937..6edc172 100644 + --- a/x + +++ b/x + @@ -1,3 +1,5 @@ + -do { + +do + +{ + nothing; + -} while (0); + +} + +while (0); + EOF -git update-index --add x + git diff >out && + test_cmp expect out && -cat << EOF > x -do -{ - nothing; -} -while (0); -EOF + git diff -w >out && + test_cmp expect out && -cat << EOF > expect -diff --git a/x b/x -index adf3937..6edc172 100644 ---- a/x -+++ b/x -@@ -1,3 +1,5 @@ --do { -+do -+{ - nothing; --} while (0); -+} -+while (0); -EOF + git diff -b >out && + test_cmp expect out +' -git diff > out -test_expect_success "Ray's example without options" 'test_cmp expect out' +test_expect_success 'another test, without options' ' + tr Q "\015" <<-\EOF >x && + whitespace at beginning + whitespace change + whitespace in the middle + whitespace at end + unchanged line + CR at endQ + EOF -git diff -w > out -test_expect_success "Ray's example with -w" 'test_cmp expect out' + git update-index x && -git diff -b > out -test_expect_success "Ray's example with -b" 'test_cmp expect out' + tr "_" " " <<-\EOF >x && + _ whitespace at beginning + whitespace change + white space in the middle + whitespace at end__ + unchanged line + CR at end + EOF -tr 'Q' '\015' << EOF > x -whitespace at beginning -whitespace change -whitespace in the middle -whitespace at end -unchanged line -CR at endQ -EOF + tr "Q_" "\015 " <<-\EOF >expect && + diff --git a/x b/x + index d99af23..22d9f73 100644 + --- a/x + +++ b/x + @@ -1,6 +1,6 @@ + -whitespace at beginning + -whitespace change + -whitespace in the middle + -whitespace at end + + whitespace at beginning + +whitespace change + +white space in the middle + +whitespace at end__ + unchanged line + -CR at endQ + +CR at end + EOF -git update-index x + git diff >out && + test_cmp expect out && -tr '_' ' ' << EOF > x - whitespace at beginning -whitespace change -white space in the middle -whitespace at end__ -unchanged line -CR at end -EOF + >expect && + git diff -w >out && + test_cmp expect out && -tr 'Q_' '\015 ' << EOF > expect -diff --git a/x b/x -index d99af23..8b32fb5 100644 ---- a/x -+++ b/x -@@ -1,6 +1,6 @@ --whitespace at beginning --whitespace change --whitespace in the middle --whitespace at end -+ whitespace at beginning -+whitespace change -+white space in the middle -+whitespace at end__ - unchanged line --CR at endQ -+CR at end -EOF -git diff > out -test_expect_success 'another test, without options' 'test_cmp expect out' + git diff -w -b >out && + test_cmp expect out && -cat << EOF > expect -EOF -git diff -w > out -test_expect_success 'another test, with -w' 'test_cmp expect out' -git diff -w -b > out -test_expect_success 'another test, with -w -b' 'test_cmp expect out' -git diff -w --ignore-space-at-eol > out -test_expect_success 'another test, with -w --ignore-space-at-eol' 'test_cmp expect out' -git diff -w -b --ignore-space-at-eol > out -test_expect_success 'another test, with -w -b --ignore-space-at-eol' 'test_cmp expect out' - -tr 'Q_' '\015 ' << EOF > expect -diff --git a/x b/x -index d99af23..8b32fb5 100644 ---- a/x -+++ b/x -@@ -1,6 +1,6 @@ --whitespace at beginning -+ whitespace at beginning - whitespace change --whitespace in the middle -+white space in the middle - whitespace at end__ - unchanged line - CR at end -EOF -git diff -b > out -test_expect_success 'another test, with -b' 'test_cmp expect out' -git diff -b --ignore-space-at-eol > out -test_expect_success 'another test, with -b --ignore-space-at-eol' 'test_cmp expect out' - -tr 'Q_' '\015 ' << EOF > expect -diff --git a/x b/x -index d99af23..8b32fb5 100644 ---- a/x -+++ b/x -@@ -1,6 +1,6 @@ --whitespace at beginning --whitespace change --whitespace in the middle -+ whitespace at beginning -+whitespace change -+white space in the middle - whitespace at end__ - unchanged line - CR at end -EOF -git diff --ignore-space-at-eol > out -test_expect_success 'another test, with --ignore-space-at-eol' 'test_cmp expect out' + git diff -w --ignore-space-at-eol >out && + test_cmp expect out && + + git diff -w -b --ignore-space-at-eol >out && + test_cmp expect out && + + + tr "Q_" "\015 " <<-\EOF >expect && + diff --git a/x b/x + index d99af23..22d9f73 100644 + --- a/x + +++ b/x + @@ -1,6 +1,6 @@ + -whitespace at beginning + +_ whitespace at beginning + whitespace change + -whitespace in the middle + +white space in the middle + whitespace at end__ + unchanged line + CR at end + EOF + git diff -b >out && + test_cmp expect out && + + git diff -b --ignore-space-at-eol >out && + test_cmp expect out && + + tr "Q_" "\015 " <<-\EOF >expect && + diff --git a/x b/x + index d99af23..22d9f73 100644 + --- a/x + +++ b/x + @@ -1,6 +1,6 @@ + -whitespace at beginning + -whitespace change + -whitespace in the middle + +_ whitespace at beginning + +whitespace change + +white space in the middle + whitespace at end__ + unchanged line + CR at end + EOF + git diff --ignore-space-at-eol >out && + test_cmp expect out +' test_expect_success 'ignore-blank-lines: only new lines' ' test_seq 5 >x && @@ -489,291 +495,219 @@ test_expect_success 'ignore-blank-lines: mix changes and blank lines' ' ' test_expect_success 'check mixed spaces and tabs in indent' ' - # This is indented with SP HT SP. - echo " foo();" > x && + echo " foo();" >x && git diff --check | grep "space before tab in indent" - ' test_expect_success 'check mixed tabs and spaces in indent' ' - # This is indented with HT SP HT. - echo " foo();" > x && + echo " foo();" >x && git diff --check | grep "space before tab in indent" - ' test_expect_success 'check with no whitespace errors' ' - git commit -m "snapshot" && - echo "foo();" > x && + echo "foo();" >x && git diff --check - ' test_expect_success 'check with trailing whitespace' ' - - echo "foo(); " > x && + echo "foo(); " >x && test_must_fail git diff --check - ' test_expect_success 'check with space before tab in indent' ' - # indent has space followed by hard tab - echo " foo();" > x && + echo " foo();" >x && test_must_fail git diff --check - ' test_expect_success '--check and --exit-code are not exclusive' ' - git checkout x && git diff --check --exit-code - ' test_expect_success '--check and --quiet are not exclusive' ' - git diff --check --quiet - ' test_expect_success 'check staged with no whitespace errors' ' - - echo "foo();" > x && + echo "foo();" >x && git add x && git diff --cached --check - ' test_expect_success 'check staged with trailing whitespace' ' - - echo "foo(); " > x && + echo "foo(); " >x && git add x && test_must_fail git diff --cached --check - ' test_expect_success 'check staged with space before tab in indent' ' - # indent has space followed by hard tab - echo " foo();" > x && + echo " foo();" >x && git add x && test_must_fail git diff --cached --check - ' test_expect_success 'check with no whitespace errors (diff-index)' ' - - echo "foo();" > x && + echo "foo();" >x && git add x && git diff-index --check HEAD - ' test_expect_success 'check with trailing whitespace (diff-index)' ' - - echo "foo(); " > x && + echo "foo(); " >x && git add x && test_must_fail git diff-index --check HEAD - ' test_expect_success 'check with space before tab in indent (diff-index)' ' - # indent has space followed by hard tab - echo " foo();" > x && + echo " foo();" >x && git add x && test_must_fail git diff-index --check HEAD - ' test_expect_success 'check staged with no whitespace errors (diff-index)' ' - - echo "foo();" > x && + echo "foo();" >x && git add x && git diff-index --cached --check HEAD - ' test_expect_success 'check staged with trailing whitespace (diff-index)' ' - - echo "foo(); " > x && + echo "foo(); " >x && git add x && test_must_fail git diff-index --cached --check HEAD - ' test_expect_success 'check staged with space before tab in indent (diff-index)' ' - # indent has space followed by hard tab - echo " foo();" > x && + echo " foo();" >x && git add x && test_must_fail git diff-index --cached --check HEAD - ' test_expect_success 'check with no whitespace errors (diff-tree)' ' - - echo "foo();" > x && + echo "foo();" >x && git commit -m "new commit" x && git diff-tree --check HEAD^ HEAD - ' test_expect_success 'check with trailing whitespace (diff-tree)' ' - - echo "foo(); " > x && + echo "foo(); " >x && git commit -m "another commit" x && test_must_fail git diff-tree --check HEAD^ HEAD - ' test_expect_success 'check with space before tab in indent (diff-tree)' ' - # indent has space followed by hard tab - echo " foo();" > x && + echo " foo();" >x && git commit -m "yet another" x && test_must_fail git diff-tree --check HEAD^ HEAD - ' test_expect_success 'check trailing whitespace (trailing-space: off)' ' - git config core.whitespace "-trailing-space" && - echo "foo (); " > x && + echo "foo (); " >x && git diff --check - ' test_expect_success 'check trailing whitespace (trailing-space: on)' ' - git config core.whitespace "trailing-space" && - echo "foo (); " > x && + echo "foo (); " >x && test_must_fail git diff --check - ' test_expect_success 'check space before tab in indent (space-before-tab: off)' ' - # indent contains space followed by HT git config core.whitespace "-space-before-tab" && - echo " foo ();" > x && + echo " foo ();" >x && git diff --check - ' test_expect_success 'check space before tab in indent (space-before-tab: on)' ' - # indent contains space followed by HT git config core.whitespace "space-before-tab" && - echo " foo (); " > x && + echo " foo (); " >x && test_must_fail git diff --check - ' test_expect_success 'check spaces as indentation (indent-with-non-tab: off)' ' - git config core.whitespace "-indent-with-non-tab" && - echo " foo ();" > x && + echo " foo ();" >x && git diff --check - ' test_expect_success 'check spaces as indentation (indent-with-non-tab: on)' ' - git config core.whitespace "indent-with-non-tab" && - echo " foo ();" > x && + echo " foo ();" >x && test_must_fail git diff --check - ' test_expect_success 'ditto, but tabwidth=9' ' - git config core.whitespace "indent-with-non-tab,tabwidth=9" && git diff --check - ' test_expect_success 'check tabs and spaces as indentation (indent-with-non-tab: on)' ' - git config core.whitespace "indent-with-non-tab" && - echo " foo ();" > x && + echo " foo ();" >x && test_must_fail git diff --check - ' test_expect_success 'ditto, but tabwidth=10' ' - git config core.whitespace "indent-with-non-tab,tabwidth=10" && test_must_fail git diff --check - ' test_expect_success 'ditto, but tabwidth=20' ' - git config core.whitespace "indent-with-non-tab,tabwidth=20" && git diff --check - ' test_expect_success 'check tabs as indentation (tab-in-indent: off)' ' - git config core.whitespace "-tab-in-indent" && - echo " foo ();" > x && + echo " foo ();" >x && git diff --check - ' test_expect_success 'check tabs as indentation (tab-in-indent: on)' ' - git config core.whitespace "tab-in-indent" && - echo " foo ();" > x && + echo " foo ();" >x && test_must_fail git diff --check - ' test_expect_success 'check tabs and spaces as indentation (tab-in-indent: on)' ' - git config core.whitespace "tab-in-indent" && - echo " foo ();" > x && + echo " foo ();" >x && test_must_fail git diff --check - ' test_expect_success 'ditto, but tabwidth=1 (must be irrelevant)' ' - git config core.whitespace "tab-in-indent,tabwidth=1" && test_must_fail git diff --check - ' test_expect_success 'check tab-in-indent and indent-with-non-tab conflict' ' - git config core.whitespace "tab-in-indent,indent-with-non-tab" && - echo "foo ();" > x && + echo "foo ();" >x && test_must_fail git diff --check - ' test_expect_success 'check tab-in-indent excluded from wildcard whitespace attribute' ' - git config --unset core.whitespace && - echo "x whitespace" > .gitattributes && - echo " foo ();" > x && + echo "x whitespace" >.gitattributes && + echo " foo ();" >x && git diff --check && rm -f .gitattributes - ' test_expect_success 'line numbers in --check output are correct' ' - - echo "" > x && - echo "foo(); " >> x && + echo "" >x && + echo "foo(); " >>x && git diff --check | grep "x:2:" - ' test_expect_success 'checkdiff detects new trailing blank lines (1)' ' @@ -876,29 +810,127 @@ test_expect_success 'setup diff colors' ' git config color.diff.old red && git config color.diff.new green && git config color.diff.commit yellow && - git config color.diff.whitespace "normal red" && + git config color.diff.whitespace blue && git config core.autocrlf false ' -cat >expected <<\EOF -<BOLD>diff --git a/x b/x<RESET> -<BOLD>index 9daeafb..2874b91 100644<RESET> -<BOLD>--- a/x<RESET> -<BOLD>+++ b/x<RESET> -<CYAN>@@ -1 +1,4 @@<RESET> - test<RESET> -<GREEN>+<RESET><GREEN>{<RESET> -<GREEN>+<RESET><BRED> <RESET> -<GREEN>+<RESET><GREEN>}<RESET> -EOF test_expect_success 'diff that introduces a line with only tabs' ' git config core.whitespace blank-at-eol && git reset --hard && - echo "test" > x && + echo "test" >x && git commit -m "initial" x && - echo "{NTN}" | tr "NT" "\n\t" >> x && + echo "{NTN}" | tr "NT" "\n\t" >>x && git -c color.diff=always diff | test_decode_color >current && + + cat >expected <<-\EOF && + <BOLD>diff --git a/x b/x<RESET> + <BOLD>index 9daeafb..2874b91 100644<RESET> + <BOLD>--- a/x<RESET> + <BOLD>+++ b/x<RESET> + <CYAN>@@ -1 +1,4 @@<RESET> + test<RESET> + <GREEN>+<RESET><GREEN>{<RESET> + <GREEN>+<RESET><BLUE> <RESET> + <GREEN>+<RESET><GREEN>}<RESET> + EOF + + test_cmp expected current +' + +test_expect_success 'diff that introduces and removes ws breakages' ' + git reset --hard && + { + echo "0. blank-at-eol " && + echo "1. blank-at-eol " + } >x && + git commit -a --allow-empty -m preimage && + { + echo "0. blank-at-eol " && + echo "1. still-blank-at-eol " && + echo "2. and a new line " + } >x && + + git -c color.diff=always diff | + test_decode_color >current && + + cat >expected <<-\EOF && + <BOLD>diff --git a/x b/x<RESET> + <BOLD>index d0233a2..700886e 100644<RESET> + <BOLD>--- a/x<RESET> + <BOLD>+++ b/x<RESET> + <CYAN>@@ -1,2 +1,3 @@<RESET> + 0. blank-at-eol <RESET> + <RED>-1. blank-at-eol <RESET> + <GREEN>+<RESET><GREEN>1. still-blank-at-eol<RESET><BLUE> <RESET> + <GREEN>+<RESET><GREEN>2. and a new line<RESET><BLUE> <RESET> + EOF + + test_cmp expected current +' + +test_expect_success 'the same with --ws-error-highlight' ' + git reset --hard && + { + echo "0. blank-at-eol " && + echo "1. blank-at-eol " + } >x && + git commit -a --allow-empty -m preimage && + { + echo "0. blank-at-eol " && + echo "1. still-blank-at-eol " && + echo "2. and a new line " + } >x && + + git -c color.diff=always diff --ws-error-highlight=default,old | + test_decode_color >current && + + cat >expected <<-\EOF && + <BOLD>diff --git a/x b/x<RESET> + <BOLD>index d0233a2..700886e 100644<RESET> + <BOLD>--- a/x<RESET> + <BOLD>+++ b/x<RESET> + <CYAN>@@ -1,2 +1,3 @@<RESET> + 0. blank-at-eol <RESET> + <RED>-<RESET><RED>1. blank-at-eol<RESET><BLUE> <RESET> + <GREEN>+<RESET><GREEN>1. still-blank-at-eol<RESET><BLUE> <RESET> + <GREEN>+<RESET><GREEN>2. and a new line<RESET><BLUE> <RESET> + EOF + + test_cmp expected current && + + git -c color.diff=always diff --ws-error-highlight=all | + test_decode_color >current && + + cat >expected <<-\EOF && + <BOLD>diff --git a/x b/x<RESET> + <BOLD>index d0233a2..700886e 100644<RESET> + <BOLD>--- a/x<RESET> + <BOLD>+++ b/x<RESET> + <CYAN>@@ -1,2 +1,3 @@<RESET> + <RESET>0. blank-at-eol<RESET><BLUE> <RESET> + <RED>-<RESET><RED>1. blank-at-eol<RESET><BLUE> <RESET> + <GREEN>+<RESET><GREEN>1. still-blank-at-eol<RESET><BLUE> <RESET> + <GREEN>+<RESET><GREEN>2. and a new line<RESET><BLUE> <RESET> + EOF + + test_cmp expected current && + + git -c color.diff=always diff --ws-error-highlight=none | + test_decode_color >current && + + cat >expected <<-\EOF && + <BOLD>diff --git a/x b/x<RESET> + <BOLD>index d0233a2..700886e 100644<RESET> + <BOLD>--- a/x<RESET> + <BOLD>+++ b/x<RESET> + <CYAN>@@ -1,2 +1,3 @@<RESET> + 0. blank-at-eol <RESET> + <RED>-1. blank-at-eol <RESET> + <GREEN>+1. still-blank-at-eol <RESET> + <GREEN>+2. and a new line <RESET> + EOF + test_cmp expected current ' diff --git a/t/t4136-apply-check.sh b/t/t4136-apply-check.sh index a321f7c245..4b0a374b63 100755 --- a/t/t4136-apply-check.sh +++ b/t/t4136-apply-check.sh @@ -16,4 +16,17 @@ test_expect_success 'apply --check exits non-zero with unrecognized input' ' EOF ' +test_expect_success 'apply exits non-zero with no-op patch' ' + cat >input <<-\EOF && + diff --get a/1 b/1 + index 6696ea4..606eddd 100644 + --- a/1 + +++ b/1 + @@ -1,1 +1,1 @@ + 1 + EOF + test_must_fail git apply --stat input && + test_must_fail git apply --check input +' + test_done diff --git a/t/t4150-am.sh b/t/t4150-am.sh index 306e6f39ac..b822a3918d 100755 --- a/t/t4150-am.sh +++ b/t/t4150-am.sh @@ -274,15 +274,21 @@ test_expect_success 'am --keep-non-patch really keeps the non-patch part' ' grep "^\[foo\] third" actual ' -test_expect_success 'am -3 falls back to 3-way merge' ' +test_expect_success 'setup am -3' ' rm -fr .git/rebase-apply && git reset --hard && - git checkout -b lorem2 master2 && + git checkout -b base3way master2 && sed -n -e "3,\$p" msg >file && head -n 9 msg >>file && git add file && test_tick && - git commit -m "copied stuff" && + git commit -m "copied stuff" +' + +test_expect_success 'am -3 falls back to 3-way merge' ' + rm -fr .git/rebase-apply && + git reset --hard && + git checkout -b lorem2 base3way && git am -3 lorem-move.patch && test_path_is_missing .git/rebase-apply && git diff --exit-code lorem @@ -291,17 +297,31 @@ test_expect_success 'am -3 falls back to 3-way merge' ' test_expect_success 'am -3 -p0 can read --no-prefix patch' ' rm -fr .git/rebase-apply && git reset --hard && - git checkout -b lorem3 master2 && - sed -n -e "3,\$p" msg >file && - head -n 9 msg >>file && - git add file && - test_tick && - git commit -m "copied stuff" && + git checkout -b lorem3 base3way && git am -3 -p0 lorem-zero.patch && test_path_is_missing .git/rebase-apply && git diff --exit-code lorem ' +test_expect_success 'am with config am.threeWay falls back to 3-way merge' ' + rm -fr .git/rebase-apply && + git reset --hard && + git checkout -b lorem4 base3way && + test_config am.threeWay 1 && + git am lorem-move.patch && + test_path_is_missing .git/rebase-apply && + git diff --exit-code lorem +' + +test_expect_success 'am with config am.threeWay overridden by --no-3way' ' + rm -fr .git/rebase-apply && + git reset --hard && + git checkout -b lorem5 base3way && + test_config am.threeWay 1 && + test_must_fail git am --no-3way lorem-move.patch && + test_path_is_dir .git/rebase-apply +' + test_expect_success 'am can rename a file' ' grep "^rename from" rename.patch && rm -fr .git/rebase-apply && @@ -338,12 +358,7 @@ test_expect_success 'am -3 can rename a file after falling back to 3-way merge' test_expect_success 'am -3 -q is quiet' ' rm -fr .git/rebase-apply && git checkout -f lorem2 && - git reset master2 --hard && - sed -n -e "3,\$p" msg >file && - head -n 9 msg >>file && - git add file && - test_tick && - git commit -m "copied stuff" && + git reset base3way --hard && git am -3 -q lorem-move.patch >output.out 2>&1 && ! test -s output.out ' diff --git a/t/t4151-am-abort.sh b/t/t4151-am-abort.sh index 8d90634ab8..833e7b2cea 100755 --- a/t/t4151-am-abort.sh +++ b/t/t4151-am-abort.sh @@ -14,6 +14,7 @@ test_expect_success setup ' git add file-1 file-2 && git commit -m initial && git tag initial && + git format-patch --stdout --root initial >initial.patch && for i in 2 3 4 5 6 do echo $i >>file-1 && @@ -63,6 +64,28 @@ do done +test_expect_success 'am -3 --skip removes otherfile-4' ' + git reset --hard initial && + test_must_fail git am -3 0003-*.patch && + test 3 -eq $(git ls-files -u | wc -l) && + test 4 = "$(cat otherfile-4)" && + git am --skip && + test_cmp_rev initial HEAD && + test -z "$(git ls-files -u)" && + test_path_is_missing otherfile-4 +' + +test_expect_success 'am -3 --abort removes otherfile-4' ' + git reset --hard initial && + test_must_fail git am -3 0003-*.patch && + test 3 -eq $(git ls-files -u | wc -l) && + test 4 = "$(cat otherfile-4)" && + git am --abort && + test_cmp_rev initial HEAD && + test -z $(git ls-files -u) && + test_path_is_missing otherfile-4 +' + test_expect_success 'am --abort will keep the local commits intact' ' test_must_fail git am 0004-*.patch && test_commit unrelated && @@ -72,4 +95,62 @@ test_expect_success 'am --abort will keep the local commits intact' ' test_cmp expect actual ' +test_expect_success 'am -3 stops on conflict on unborn branch' ' + git checkout -f --orphan orphan && + git reset && + rm -f otherfile-4 && + test_must_fail git am -3 0003-*.patch && + test 2 -eq $(git ls-files -u | wc -l) && + test 4 = "$(cat otherfile-4)" +' + +test_expect_success 'am -3 --skip clears index on unborn branch' ' + test_path_is_dir .git/rebase-apply && + echo tmpfile >tmpfile && + git add tmpfile && + git am --skip && + test -z "$(git ls-files)" && + test_path_is_missing otherfile-4 && + test_path_is_missing tmpfile +' + +test_expect_success 'am -3 --abort removes otherfile-4 on unborn branch' ' + git checkout -f --orphan orphan && + git reset && + rm -f otherfile-4 file-1 && + test_must_fail git am -3 0003-*.patch && + test 2 -eq $(git ls-files -u | wc -l) && + test 4 = "$(cat otherfile-4)" && + git am --abort && + test -z "$(git ls-files -u)" && + test_path_is_missing otherfile-4 +' + +test_expect_success 'am -3 --abort on unborn branch removes applied commits' ' + git checkout -f --orphan orphan && + git reset && + rm -f otherfile-4 otherfile-2 file-1 file-2 && + test_must_fail git am -3 initial.patch 0003-*.patch && + test 3 -eq $(git ls-files -u | wc -l) && + test 4 = "$(cat otherfile-4)" && + git am --abort && + test -z "$(git ls-files -u)" && + test_path_is_missing otherfile-4 && + test_path_is_missing file-1 && + test_path_is_missing file-2 && + test 0 -eq $(git log --oneline 2>/dev/null | wc -l) && + test refs/heads/orphan = "$(git symbolic-ref HEAD)" +' + +test_expect_success 'am --abort on unborn branch will keep local commits intact' ' + git checkout -f --orphan orphan && + git reset && + test_must_fail git am 0004-*.patch && + test_commit unrelated2 && + git rev-parse HEAD >expect && + git am --abort && + git rev-parse HEAD >actual && + test_cmp expect actual +' + test_done 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/t5520-pull.sh b/t/t5520-pull.sh index 227d293350..f4a7193677 100755 --- a/t/t5520-pull.sh +++ b/t/t5520-pull.sh @@ -9,36 +9,27 @@ modify () { mv "$2.x" "$2" } -D=`pwd` - test_expect_success setup ' - echo file >file && git add file && git commit -a -m original - ' test_expect_success 'pulling into void' ' - mkdir cloned && - cd cloned && - git init && - git pull .. -' - -cd "$D" - -test_expect_success 'checking the results' ' + git init cloned && + ( + cd cloned && + git pull .. + ) && test -f file && test -f cloned/file && test_cmp file cloned/file ' test_expect_success 'pulling into void using master:master' ' - mkdir cloned-uho && + git init cloned-uho && ( cd cloned-uho && - git init && git pull .. master:master ) && test -f file && @@ -71,7 +62,6 @@ test_expect_success 'pulling into void does not overwrite staged files' ' ) ' - test_expect_success 'pulling into void does not remove new staged files' ' git init cloned-staged-new && ( @@ -86,17 +76,29 @@ test_expect_success 'pulling into void does not remove new staged files' ' ) ' -test_expect_success 'test . as a remote' ' +test_expect_success 'pulling into void must not create an octopus' ' + git init cloned-octopus && + ( + cd cloned-octopus && + test_must_fail git pull .. master master && + ! test -f file + ) +' +test_expect_success 'test . as a remote' ' git branch copy master && git config branch.copy.remote . && git config branch.copy.merge refs/heads/master && echo updated >file && git commit -a -m updated && git checkout copy && - test `cat file` = file && + test "$(cat file)" = file && git pull && - test `cat file` = updated + test "$(cat file)" = updated && + git reflog -1 >reflog.actual && + sed "s/^[0-9a-f][0-9a-f]*/OBJID/" reflog.actual >reflog.fuzzy && + echo "OBJID HEAD@{0}: pull: Fast-forward" >reflog.expected && + test_cmp reflog.expected reflog.fuzzy ' test_expect_success 'the default remote . should not break explicit pull' ' @@ -105,9 +107,120 @@ test_expect_success 'the default remote . should not break explicit pull' ' git commit -a -m modified && git checkout copy && git reset --hard HEAD^ && - test `cat file` = file && + test "$(cat file)" = file && git pull . second && - test `cat file` = modified + test "$(cat file)" = modified && + git reflog -1 >reflog.actual && + sed "s/^[0-9a-f][0-9a-f]*/OBJID/" reflog.actual >reflog.fuzzy && + echo "OBJID HEAD@{0}: pull . second: Fast-forward" >reflog.expected && + test_cmp reflog.expected reflog.fuzzy +' + +test_expect_success 'fail if wildcard spec does not match any refs' ' + git checkout -b test copy^ && + test_when_finished "git checkout -f copy && git branch -D test" && + test "$(cat file)" = file && + test_must_fail git pull . "refs/nonexisting1/*:refs/nonexisting2/*" 2>err && + test_i18ngrep "no candidates for merging" err && + test "$(cat file)" = file +' + +test_expect_success 'fail if no branches specified with non-default remote' ' + git remote add test_remote . && + test_when_finished "git remote remove test_remote" && + git checkout -b test copy^ && + test_when_finished "git checkout -f copy && git branch -D test" && + test "$(cat file)" = file && + test_config branch.test.remote origin && + test_must_fail git pull test_remote 2>err && + test_i18ngrep "specify a branch on the command line" err && + test "$(cat file)" = file +' + +test_expect_success 'fail if not on a branch' ' + git remote add origin . && + test_when_finished "git remote remove origin" && + git checkout HEAD^ && + test_when_finished "git checkout -f copy" && + test "$(cat file)" = file && + test_must_fail git pull 2>err && + test_i18ngrep "not currently on a branch" err && + test "$(cat file)" = file +' + +test_expect_success 'fail if no configuration for current branch' ' + git remote add test_remote . && + test_when_finished "git remote remove test_remote" && + git checkout -b test copy^ && + test_when_finished "git checkout -f copy && git branch -D test" && + test_config branch.test.remote test_remote && + test "$(cat file)" = file && + test_must_fail git pull 2>err && + test_i18ngrep "no tracking information" err && + test "$(cat file)" = file +' + +test_expect_success 'pull --all: fail if no configuration for current branch' ' + git remote add test_remote . && + test_when_finished "git remote remove test_remote" && + git checkout -b test copy^ && + test_when_finished "git checkout -f copy && git branch -D test" && + test_config branch.test.remote test_remote && + test "$(cat file)" = file && + test_must_fail git pull --all 2>err && + test_i18ngrep "There is no tracking information" err && + test "$(cat file)" = file +' + +test_expect_success 'fail if upstream branch does not exist' ' + git checkout -b test copy^ && + test_when_finished "git checkout -f copy && git branch -D test" && + test_config branch.test.remote . && + test_config branch.test.merge refs/heads/nonexisting && + test "$(cat file)" = file && + test_must_fail git pull 2>err && + test_i18ngrep "no such ref was fetched" err && + test "$(cat file)" = file +' + +test_expect_success 'fail if the index has unresolved entries' ' + git checkout -b third second^ && + test_when_finished "git checkout -f copy && git branch -D third" && + test "$(cat file)" = file && + test_commit modified2 file && + test -z "$(git ls-files -u)" && + test_must_fail git pull . second && + test -n "$(git ls-files -u)" && + cp file expected && + test_must_fail git pull . second 2>err && + test_i18ngrep "Pull is not possible because you have unmerged files" err && + test_cmp expected file && + git add file && + test -z "$(git ls-files -u)" && + test_must_fail git pull . second 2>err && + test_i18ngrep "You have not concluded your merge" err && + test_cmp expected file +' + +test_expect_success 'fast-forwards working tree if branch head is updated' ' + git checkout -b third second^ && + test_when_finished "git checkout -f copy && git branch -D third" && + test "$(cat file)" = file && + git pull . second:third 2>err && + test_i18ngrep "fetch updated the current branch head" err && + test "$(cat file)" = modified && + test "$(git rev-parse third)" = "$(git rev-parse second)" +' + +test_expect_success 'fast-forward fails with conflicting work tree' ' + git checkout -b third second^ && + test_when_finished "git checkout -f copy && git branch -D third" && + test "$(cat file)" = file && + echo conflict >file && + test_must_fail git pull . second:third 2>err && + test_i18ngrep "Cannot fast-forward your working tree" err && + test "$(cat file)" = conflict && + test "$(git rev-parse third)" = "$(git rev-parse second)" ' test_expect_success '--rebase' ' @@ -120,23 +233,32 @@ test_expect_success '--rebase' ' git commit -m "new file" && git tag before-rebase && git pull --rebase . copy && - test $(git rev-parse HEAD^) = $(git rev-parse copy) && - test new = $(git show HEAD:file2) + test "$(git rev-parse HEAD^)" = "$(git rev-parse copy)" && + test new = "$(git show HEAD:file2)" +' + +test_expect_success '--rebase fails with multiple branches' ' + git reset --hard before-rebase && + test_must_fail git pull --rebase . copy master 2>err && + test "$(git rev-parse HEAD)" = "$(git rev-parse before-rebase)" && + test_i18ngrep "Cannot rebase onto multiple branches" err && + test modified = "$(git show HEAD:file)" ' + test_expect_success 'pull.rebase' ' git reset --hard before-rebase && test_config pull.rebase true && git pull . copy && - test $(git rev-parse HEAD^) = $(git rev-parse copy) && - test new = $(git show HEAD:file2) + test "$(git rev-parse HEAD^)" = "$(git rev-parse copy)" && + test new = "$(git show HEAD:file2)" ' test_expect_success 'branch.to-rebase.rebase' ' git reset --hard before-rebase && test_config branch.to-rebase.rebase true && git pull . copy && - test $(git rev-parse HEAD^) = $(git rev-parse copy) && - test new = $(git show HEAD:file2) + test "$(git rev-parse HEAD^)" = "$(git rev-parse copy)" && + test new = "$(git show HEAD:file2)" ' test_expect_success 'branch.to-rebase.rebase should override pull.rebase' ' @@ -144,8 +266,8 @@ test_expect_success 'branch.to-rebase.rebase should override pull.rebase' ' test_config pull.rebase true && test_config branch.to-rebase.rebase false && git pull . copy && - test $(git rev-parse HEAD^) != $(git rev-parse copy) && - test new = $(git show HEAD:file2) + test "$(git rev-parse HEAD^)" != "$(git rev-parse copy)" && + test new = "$(git show HEAD:file2)" ' # add a feature branch, keep-merge, that is merged into master, so the @@ -164,33 +286,33 @@ test_expect_success 'pull.rebase=false create a new merge commit' ' git reset --hard before-preserve-rebase && test_config pull.rebase false && git pull . copy && - test $(git rev-parse HEAD^1) = $(git rev-parse before-preserve-rebase) && - test $(git rev-parse HEAD^2) = $(git rev-parse copy) && - test file3 = $(git show HEAD:file3.t) + test "$(git rev-parse HEAD^1)" = "$(git rev-parse before-preserve-rebase)" && + test "$(git rev-parse HEAD^2)" = "$(git rev-parse copy)" && + test file3 = "$(git show HEAD:file3.t)" ' test_expect_success 'pull.rebase=true flattens keep-merge' ' git reset --hard before-preserve-rebase && test_config pull.rebase true && git pull . copy && - test $(git rev-parse HEAD^^) = $(git rev-parse copy) && - test file3 = $(git show HEAD:file3.t) + test "$(git rev-parse HEAD^^)" = "$(git rev-parse copy)" && + test file3 = "$(git show HEAD:file3.t)" ' test_expect_success 'pull.rebase=1 is treated as true and flattens keep-merge' ' git reset --hard before-preserve-rebase && test_config pull.rebase 1 && git pull . copy && - test $(git rev-parse HEAD^^) = $(git rev-parse copy) && - test file3 = $(git show HEAD:file3.t) + test "$(git rev-parse HEAD^^)" = "$(git rev-parse copy)" && + test file3 = "$(git show HEAD:file3.t)" ' test_expect_success 'pull.rebase=preserve rebases and merges keep-merge' ' git reset --hard before-preserve-rebase && test_config pull.rebase preserve && git pull . copy && - test $(git rev-parse HEAD^^) = $(git rev-parse copy) && - test $(git rev-parse HEAD^2) = $(git rev-parse keep-merge) + test "$(git rev-parse HEAD^^)" = "$(git rev-parse copy)" && + test "$(git rev-parse HEAD^2)" = "$(git rev-parse keep-merge)" ' test_expect_success 'pull.rebase=invalid fails' ' @@ -203,25 +325,25 @@ test_expect_success '--rebase=false create a new merge commit' ' git reset --hard before-preserve-rebase && test_config pull.rebase true && git pull --rebase=false . copy && - test $(git rev-parse HEAD^1) = $(git rev-parse before-preserve-rebase) && - test $(git rev-parse HEAD^2) = $(git rev-parse copy) && - test file3 = $(git show HEAD:file3.t) + test "$(git rev-parse HEAD^1)" = "$(git rev-parse before-preserve-rebase)" && + test "$(git rev-parse HEAD^2)" = "$(git rev-parse copy)" && + test file3 = "$(git show HEAD:file3.t)" ' test_expect_success '--rebase=true rebases and flattens keep-merge' ' git reset --hard before-preserve-rebase && test_config pull.rebase preserve && git pull --rebase=true . copy && - test $(git rev-parse HEAD^^) = $(git rev-parse copy) && - test file3 = $(git show HEAD:file3.t) + test "$(git rev-parse HEAD^^)" = "$(git rev-parse copy)" && + test file3 = "$(git show HEAD:file3.t)" ' test_expect_success '--rebase=preserve rebases and merges keep-merge' ' git reset --hard before-preserve-rebase && test_config pull.rebase true && git pull --rebase=preserve . copy && - test $(git rev-parse HEAD^^) = $(git rev-parse copy) && - test $(git rev-parse HEAD^2) = $(git rev-parse keep-merge) + test "$(git rev-parse HEAD^^)" = "$(git rev-parse copy)" && + test "$(git rev-parse HEAD^2)" = "$(git rev-parse keep-merge)" ' test_expect_success '--rebase=invalid fails' ' @@ -233,8 +355,8 @@ test_expect_success '--rebase overrides pull.rebase=preserve and flattens keep-m git reset --hard before-preserve-rebase && test_config pull.rebase preserve && git pull --rebase . copy && - test $(git rev-parse HEAD^^) = $(git rev-parse copy) && - test file3 = $(git show HEAD:file3.t) + test "$(git rev-parse HEAD^^)" = "$(git rev-parse copy)" && + test file3 = "$(git show HEAD:file3.t)" ' test_expect_success '--rebase with rebased upstream' ' @@ -251,10 +373,18 @@ test_expect_success '--rebase with rebased upstream' ' git tag to-rebase-orig && git pull --rebase me copy && test "conflicting modification" = "$(cat file)" && - test file = $(cat file2) + test file = "$(cat file2)" ' +test_expect_success '--rebase -f with rebased upstream' ' + test_when_finished "test_might_fail git rebase --abort" && + git reset --hard to-rebase-orig && + git pull --rebase -f me copy && + test "conflicting modification" = "$(cat file)" && + test file = "$(cat file2)" +' + test_expect_success '--rebase with rebased default upstream' ' git update-ref refs/remotes/me/copy copy-orig && @@ -262,7 +392,7 @@ test_expect_success '--rebase with rebased default upstream' ' git reset --hard to-rebase-orig && git pull --rebase && test "conflicting modification" = "$(cat file)" && - test file = $(cat file2) + test file = "$(cat file2)" ' @@ -283,7 +413,7 @@ test_expect_success 'pull --rebase dies early with dirty working directory' ' git checkout to-rebase && git update-ref refs/remotes/me/copy copy^ && - COPY=$(git rev-parse --verify me/copy) && + COPY="$(git rev-parse --verify me/copy)" && git rebase --onto $COPY copy && test_config branch.to-rebase.remote me && test_config branch.to-rebase.merge refs/heads/copy && @@ -291,10 +421,10 @@ test_expect_success 'pull --rebase dies early with dirty working directory' ' echo dirty >> file && git add file && test_must_fail git pull && - test $COPY = $(git rev-parse --verify me/copy) && + test "$COPY" = "$(git rev-parse --verify me/copy)" && git checkout HEAD -- file && git pull && - test $COPY != $(git rev-parse --verify me/copy) + test "$COPY" != "$(git rev-parse --verify me/copy)" ' @@ -309,6 +439,21 @@ test_expect_success 'pull --rebase works on branch yet to be born' ' test_cmp expect actual ' +test_expect_success 'pull --rebase fails on unborn branch with staged changes' ' + test_when_finished "rm -rf empty_repo2" && + git init empty_repo2 && + ( + cd empty_repo2 && + echo staged-file >staged-file && + git add staged-file && + test "$(git ls-files)" = staged-file && + test_must_fail git pull --rebase .. master 2>err && + test "$(git ls-files)" = staged-file && + test "$(git show :staged-file)" = staged-file && + test_i18ngrep "unborn branch with changes added to the index" err + ) +' + test_expect_success 'setup for detecting upstreamed changes' ' mkdir src && (cd src && diff --git a/t/t5521-pull-options.sh b/t/t5521-pull-options.sh index 453aba53f4..18372caa15 100755 --- a/t/t5521-pull-options.sh +++ b/t/t5521-pull-options.sh @@ -117,4 +117,31 @@ test_expect_success 'git pull --all' ' ) ' +test_expect_success 'git pull --dry-run' ' + test_when_finished "rm -rf clonedry" && + git init clonedry && + ( + cd clonedry && + git pull --dry-run ../parent && + test_path_is_missing .git/FETCH_HEAD && + test_path_is_missing .git/refs/heads/master && + test_path_is_missing .git/index && + test_path_is_missing file + ) +' + +test_expect_success 'git pull --all --dry-run' ' + test_when_finished "rm -rf cloneddry" && + git init clonedry && + ( + cd clonedry && + git remote add origin ../parent && + git pull --all --dry-run && + test_path_is_missing .git/FETCH_HEAD && + test_path_is_missing .git/refs/remotes/origin/master && + test_path_is_missing .git/index && + test_path_is_missing file + ) +' + test_done diff --git a/t/t6020-merge-df.sh b/t/t6020-merge-df.sh index 27c3d73961..2af1beec5f 100755 --- a/t/t6020-merge-df.sh +++ b/t/t6020-merge-df.sh @@ -24,7 +24,7 @@ test_expect_success 'prepare repository' ' ' test_expect_success 'Merge with d/f conflicts' ' - test_expect_code 1 git merge "merge msg" B master + test_expect_code 1 git merge -m "merge msg" master ' test_expect_success 'F/D conflict' ' diff --git a/t/t6021-merge-criss-cross.sh b/t/t6021-merge-criss-cross.sh index d15b313d4b..213deecab1 100755 --- a/t/t6021-merge-criss-cross.sh +++ b/t/t6021-merge-criss-cross.sh @@ -48,7 +48,7 @@ echo "1 " > file && git commit -m "C3" file && git branch C3 && -git merge "pre E3 merge" B A && +git merge -m "pre E3 merge" A && echo "1 2 3 changed in E3, branch B. New file size @@ -61,7 +61,7 @@ echo "1 " > file && git commit -m "E3" file && git checkout A && -git merge "pre D8 merge" A C3 && +git merge -m "pre D8 merge" C3 && echo "1 2 3 changed in C3, branch B @@ -73,7 +73,7 @@ echo "1 9" > file && git commit -m D8 file' -test_expect_success 'Criss-cross merge' 'git merge "final merge" A B' +test_expect_success 'Criss-cross merge' 'git merge -m "final merge" B' cat > file-expect <<EOF 1 diff --git a/t/t6026-merge-attr.sh b/t/t6026-merge-attr.sh index 3c21938a68..04c0509c47 100755 --- a/t/t6026-merge-attr.sh +++ b/t/t6026-merge-attr.sh @@ -85,11 +85,12 @@ test_expect_success 'retry the merge with longer context' ' cat >./custom-merge <<\EOF #!/bin/sh -orig="$1" ours="$2" theirs="$3" exit="$4" +orig="$1" ours="$2" theirs="$3" exit="$4" path=$5 ( echo "orig is $orig" echo "ours is $ours" echo "theirs is $theirs" + echo "path is $path" echo "=== orig ===" cat "$orig" echo "=== ours ===" @@ -110,7 +111,7 @@ test_expect_success 'custom merge backend' ' git reset --hard anchor && git config --replace-all \ - merge.custom.driver "./custom-merge %O %A %B 0" && + merge.custom.driver "./custom-merge %O %A %B 0 %P" && git config --replace-all \ merge.custom.name "custom merge driver for testing" && @@ -121,7 +122,7 @@ test_expect_success 'custom merge backend' ' o=$(git unpack-file master^:text) && a=$(git unpack-file side^:text) && b=$(git unpack-file master:text) && - sh -c "./custom-merge $o $a $b 0" && + sh -c "./custom-merge $o $a $b 0 'text'" && sed -e 1,3d $a >check-2 && cmp check-1 check-2 && rm -f $o $a $b @@ -131,7 +132,7 @@ test_expect_success 'custom merge backend' ' git reset --hard anchor && git config --replace-all \ - merge.custom.driver "./custom-merge %O %A %B 1" && + merge.custom.driver "./custom-merge %O %A %B 1 %P" && git config --replace-all \ merge.custom.name "custom merge driver for testing" && @@ -148,9 +149,12 @@ test_expect_success 'custom merge backend' ' o=$(git unpack-file master^:text) && a=$(git unpack-file anchor:text) && b=$(git unpack-file master:text) && - sh -c "./custom-merge $o $a $b 0" && + sh -c "./custom-merge $o $a $b 0 'text'" && sed -e 1,3d $a >check-2 && cmp check-1 check-2 && + sed -e 1,3d -e 4q $a >check-3 && + echo "path is text" >expect && + cmp expect check-3 && rm -f $o $a $b ' 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/t6301-for-each-ref-errors.sh b/t/t6301-for-each-ref-errors.sh new file mode 100755 index 0000000000..cdb67a03b7 --- /dev/null +++ b/t/t6301-for-each-ref-errors.sh @@ -0,0 +1,56 @@ +#!/bin/sh + +test_description='for-each-ref errors for broken refs' + +. ./test-lib.sh + +ZEROS=$_z40 +MISSING=abababababababababababababababababababab + +test_expect_success setup ' + git commit --allow-empty -m "Initial" && + git tag testtag && + git for-each-ref >full-list && + git for-each-ref --format="%(objectname) %(refname)" >brief-list +' + +test_expect_success 'Broken refs are reported correctly' ' + r=refs/heads/bogus && + : >.git/$r && + test_when_finished "rm -f .git/$r" && + echo "warning: ignoring broken ref $r" >broken-err && + git for-each-ref >out 2>err && + test_cmp full-list out && + test_cmp broken-err err +' + +test_expect_success 'NULL_SHA1 refs are reported correctly' ' + r=refs/heads/zeros && + echo $ZEROS >.git/$r && + test_when_finished "rm -f .git/$r" && + echo "warning: ignoring broken ref $r" >zeros-err && + git for-each-ref >out 2>err && + test_cmp full-list out && + test_cmp zeros-err err && + git for-each-ref --format="%(objectname) %(refname)" >brief-out 2>brief-err && + test_cmp brief-list brief-out && + test_cmp zeros-err brief-err +' + +test_expect_success 'Missing objects are reported correctly' ' + r=refs/heads/missing && + echo $MISSING >.git/$r && + test_when_finished "rm -f .git/$r" && + echo "fatal: missing object $MISSING for $r" >missing-err && + test_must_fail git for-each-ref 2>err && + test_cmp missing-err err && + ( + cat brief-list && + echo "$MISSING $r" + ) | sort -k 2 >missing-brief-expected && + git for-each-ref --format="%(objectname) %(refname)" >brief-out 2>brief-err && + test_cmp missing-brief-expected brief-out && + test_must_be_empty brief-err +' + +test_done 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/t7410-submodule-checkout-to.sh b/t/t7410-submodule-checkout-to.sh new file mode 100755 index 0000000000..8f30aed6cc --- /dev/null +++ b/t/t7410-submodule-checkout-to.sh @@ -0,0 +1,50 @@ +#!/bin/sh + +test_description='Combination of submodules and multiple workdirs' + +. ./test-lib.sh + +base_path=$(pwd -P) + +test_expect_success 'setup: make origin' \ + 'mkdir -p origin/sub && ( cd origin/sub && git init && + echo file1 >file1 && + git add file1 && + git commit -m file1 ) && + mkdir -p origin/main && ( cd origin/main && git init && + git submodule add ../sub && + git commit -m "add sub" ) && + ( cd origin/sub && + echo file1updated >file1 && + git add file1 && + git commit -m "file1 updated" ) && + ( cd origin/main/sub && git pull ) && + ( cd origin/main && + git add sub && + git commit -m "sub updated" )' + +test_expect_success 'setup: clone' \ + 'mkdir clone && ( cd clone && + git clone --recursive "$base_path/origin/main")' + +rev1_hash_main=$(git --git-dir=origin/main/.git show --pretty=format:%h -q "HEAD~1") +rev1_hash_sub=$(git --git-dir=origin/sub/.git show --pretty=format:%h -q "HEAD~1") + +test_expect_success 'checkout main' \ + 'mkdir default_checkout && + (cd clone/main && + git checkout --to "$base_path/default_checkout/main" "$rev1_hash_main")' + +test_expect_failure 'can see submodule diffs just after checkout' \ + '(cd default_checkout/main && git diff --submodule master"^!" | grep "file1 updated")' + +test_expect_success 'checkout main and initialize independed clones' \ + 'mkdir fully_cloned_submodule && + (cd clone/main && + git checkout --to "$base_path/fully_cloned_submodule/main" "$rev1_hash_main") && + (cd fully_cloned_submodule/main && git submodule update)' + +test_expect_success 'can see submodule diffs after independed cloning' \ + '(cd fully_cloned_submodule/main && git diff --submodule master"^!" | grep "file1 updated")' + +test_done diff --git a/t/t7502-commit.sh b/t/t7502-commit.sh index 051489ea33..b39e313ac2 100755 --- a/t/t7502-commit.sh +++ b/t/t7502-commit.sh @@ -229,14 +229,36 @@ test_expect_success 'cleanup commit messages (scissors option,-F,-e)' ' cat >text <<EOF && # to be kept + + # ------------------------ >8 ------------------------ +# to be kept, too # ------------------------ >8 ------------------------ to be removed +# ------------------------ >8 ------------------------ +to be removed, too +EOF + + cat >expect <<EOF && +# to be kept + + # ------------------------ >8 ------------------------ +# to be kept, too EOF - echo "# to be kept" >expect && git commit --cleanup=scissors -e -F text -a && git cat-file -p HEAD |sed -e "1,/^\$/d">actual && test_cmp expect actual +' +test_expect_success 'cleanup commit messages (scissors option,-F,-e, scissors on first line)' ' + + echo >>negative && + cat >text <<EOF && +# ------------------------ >8 ------------------------ +to be removed +EOF + git commit --cleanup=scissors -e -F text -a --allow-empty-message && + git cat-file -p HEAD |sed -e "1,/^\$/d">actual && + test_must_be_empty actual ' test_expect_success 'cleanup commit messages (strip option,-F)' ' @@ -370,7 +392,7 @@ exit 0 EOF test_expect_success !AUTOIDENT 'do not fire editor when committer is bogus' ' - >.git/result + >.git/result && >expect && echo >>negative && diff --git a/t/t8002-blame.sh b/t/t8002-blame.sh index 5cdf3f178e..ff09aced68 100755 --- a/t/t8002-blame.sh +++ b/t/t8002-blame.sh @@ -19,4 +19,66 @@ test_expect_success 'blame --show-email' ' "<E at test dot git>" 1 ' +test_expect_success 'setup showEmail tests' ' + echo "bin: test number 1" >one && + git add one && + GIT_AUTHOR_NAME=name1 \ + GIT_AUTHOR_EMAIL=email1@test.git \ + git commit -m First --date="2010-01-01 01:00:00" && + cat >expected_n <<-\EOF && + (name1 2010-01-01 01:00:00 +0000 1) bin: test number 1 + EOF + cat >expected_e <<-\EOF + (<email1@test.git> 2010-01-01 01:00:00 +0000 1) bin: test number 1 + EOF +' + +find_blame () { + sed -e 's/^[^(]*//' +} + +test_expect_success 'blame with no options and no config' ' + git blame one >blame && + find_blame <blame >result && + test_cmp expected_n result +' + +test_expect_success 'blame with showemail options' ' + git blame --show-email one >blame1 && + find_blame <blame1 >result && + test_cmp expected_e result && + git blame -e one >blame2 && + find_blame <blame2 >result && + test_cmp expected_e result && + git blame --no-show-email one >blame3 && + find_blame <blame3 >result && + test_cmp expected_n result +' + +test_expect_success 'blame with showEmail config false' ' + git config blame.showEmail false && + git blame one >blame1 && + find_blame <blame1 >result && + test_cmp expected_n result && + git blame --show-email one >blame2 && + find_blame <blame2 >result && + test_cmp expected_e result && + git blame -e one >blame3 && + find_blame <blame3 >result && + test_cmp expected_e result && + git blame --no-show-email one >blame4 && + find_blame <blame4 >result && + test_cmp expected_n result +' + +test_expect_success 'blame with showEmail config true' ' + git config blame.showEmail true && + git blame one >blame1 && + find_blame <blame1 >result && + test_cmp expected_e result && + git blame --no-show-email one >blame2 && + find_blame <blame2 >result && + test_cmp expected_n result +' + test_done diff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh index 7be14a4e37..db2f45e83b 100755 --- a/t/t9001-send-email.sh +++ b/t/t9001-send-email.sh @@ -1537,7 +1537,7 @@ test_expect_success $PREREQ 'sendemail.aliasfiletype=mailrc' ' test_expect_success $PREREQ 'sendemail.aliasfile=~/.mailrc' ' clean_fake_sendmail && - echo "alias sbd someone@example.org" >~/.mailrc && + echo "alias sbd someone@example.org" >"$HOME/.mailrc" && git config --replace-all sendemail.aliasesfile "~/.mailrc" && git config sendemail.aliasfiletype mailrc && git send-email \ @@ -1549,6 +1549,78 @@ test_expect_success $PREREQ 'sendemail.aliasfile=~/.mailrc' ' grep "^!someone@example\.org!$" commandline1 ' +test_sendmail_aliases () { + msg="$1" && shift && + expect="$@" && + cat >.tmp-email-aliases && + + test_expect_success $PREREQ "$msg" ' + clean_fake_sendmail && rm -fr outdir && + git format-patch -1 -o outdir && + git config --replace-all sendemail.aliasesfile \ + "$(pwd)/.tmp-email-aliases" && + git config sendemail.aliasfiletype sendmail && + git send-email \ + --from="Example <nobody@example.com>" \ + --to=alice --to=bcgrp \ + --smtp-server="$(pwd)/fake.sendmail" \ + outdir/0001-*.patch \ + 2>errors >out && + for i in $expect + do + grep "^!$i!$" commandline1 || return 1 + done + ' +} + +test_sendmail_aliases 'sendemail.aliasfiletype=sendmail' \ + 'awol@example\.com' \ + 'bob@example\.com' \ + 'chloe@example\.com' \ + 'o@example\.com' <<-\EOF + alice: Alice W Land <awol@example.com> + bob: Robert Bobbyton <bob@example.com> + # this is a comment + # this is also a comment + chloe: chloe@example.com + abgroup: alice, bob + bcgrp: bob, chloe, Other <o@example.com> + EOF + +test_sendmail_aliases 'sendmail aliases line folding' \ + alice1 \ + bob1 bob2 \ + chuck1 chuck2 \ + darla1 darla2 darla3 \ + elton1 elton2 elton3 \ + fred1 fred2 \ + greg1 <<-\EOF + alice: alice1 + bob: bob1,\ + bob2 + chuck: chuck1, + chuck2 + darla: darla1,\ + darla2, + darla3 + elton: elton1, + elton2,\ + elton3 + fred: fred1,\ + fred2 + greg: greg1 + bcgrp: bob, chuck, darla, elton, fred, greg + EOF + +test_sendmail_aliases 'sendmail aliases tolerate bogus line folding' \ + alice1 bob1 <<-\EOF + alice: alice1 + bcgrp: bob1\ + EOF + +test_sendmail_aliases 'sendmail aliases empty' alice bcgrp <<-\EOF + EOF + do_xmailer_test () { expected=$1 params=$2 && git format-patch -1 && diff --git a/t/t9402-git-cvsserver-refs.sh b/t/t9402-git-cvsserver-refs.sh index 1e266effff..d00df08731 100755 --- a/t/t9402-git-cvsserver-refs.sh +++ b/t/t9402-git-cvsserver-refs.sh @@ -496,7 +496,7 @@ test_expect_success 'check [cvswork3] diff' ' ' test_expect_success 'merge early [cvswork3] b3 with b1' ' - ( cd gitwork3 && git merge "message" HEAD b1 ) && + ( cd gitwork3 && git merge -m "message" b1 ) && git fetch gitwork3 b3:b3 && git tag v3merged b3 && git push --tags gitcvs.git b3:b3 diff --git a/t/t9800-git-p4-basic.sh b/t/t9800-git-p4-basic.sh index 5b562122a1..90d41ed954 100755 --- a/t/t9800-git-p4-basic.sh +++ b/t/t9800-git-p4-basic.sh @@ -131,6 +131,44 @@ test_expect_success 'clone two dirs, @all, conflicting files' ' ) ' +revision_ranges="2000/01/01,#head \ + 1,2080/01/01 \ + 2000/01/01,2080/01/01 \ + 2000/01/01,1000 \ + 1,1000" + +test_expect_success 'clone using non-numeric revision ranges' ' + test_when_finished cleanup_git && + for r in $revision_ranges + do + rm -fr "$git" && + test ! -d "$git" && + git p4 clone --dest="$git" //depot@$r && + ( + cd "$git" && + git ls-files >lines && + test_line_count = 6 lines + ) + done +' + +test_expect_success 'clone with date range, excluding some changes' ' + test_when_finished cleanup_git && + before=$(date +%Y/%m/%d:%H:%M:%S) && + sleep 2 && + ( + cd "$cli" && + :>date_range_test && + p4 add date_range_test && + p4 submit -d "Adding file" + ) && + git p4 clone --dest="$git" //depot@1,$before && + ( + cd "$git" && + test_path_is_missing date_range_test + ) +' + test_expect_success 'exit when p4 fails to produce marshaled output' ' mkdir badp4dir && test_when_finished "rm badp4dir/p4 && rmdir badp4dir" && diff --git a/t/t9801-git-p4-branch.sh b/t/t9801-git-p4-branch.sh index 2bf142d09c..0aafd03334 100755 --- a/t/t9801-git-p4-branch.sh +++ b/t/t9801-git-p4-branch.sh @@ -504,6 +504,112 @@ test_expect_success 'use-client-spec detect-branches skips files in branches' ' ) ' +test_expect_success 'restart p4d' ' + kill_p4d && + start_p4d +' + +# +# 1: //depot/branch1/base/file1 +# //depot/branch1/base/file2 +# //depot/branch1/base/dir/sub_file1 +# 2: integrate //depot/branch1/base/... -> //depot/branch2/base/... +# 3: //depot/branch1/base/file3 +# 4: //depot/branch1/base/file2 (edit) +# 5: integrate //depot/branch1/base/... -> //depot/branch3/base/... +# +# Note: the client view removes the "base" folder from the workspace +# and moves sub_file1 one level up. +test_expect_success 'add simple p4 branches with common base folder on each branch' ' + ( + cd "$cli" && + client_view "//depot/branch1/base/... //client/branch1/..." \ + "//depot/branch1/base/dir/sub_file1 //client/branch1/sub_file1" \ + "//depot/branch2/base/... //client/branch2/..." \ + "//depot/branch3/base/... //client/branch3/..." && + mkdir -p branch1 && + cd branch1 && + echo file1 >file1 && + echo file2 >file2 && + mkdir dir && + echo sub_file1 >sub_file1 && + p4 add file1 file2 sub_file1 && + p4 submit -d "Create branch1" && + p4 integrate //depot/branch1/base/... //depot/branch2/base/... && + p4 submit -d "Integrate branch2 from branch1" && + echo file3 >file3 && + p4 add file3 && + p4 submit -d "add file3 in branch1" && + p4 open file2 && + echo update >>file2 && + p4 submit -d "update file2 in branch1" && + p4 integrate //depot/branch1/base/... //depot/branch3/base/... && + p4 submit -d "Integrate branch3 from branch1" + ) +' + +# Configure branches through git-config and clone them. +# All files are tested to make sure branches were cloned correctly. +# Finally, make an update to branch1 on P4 side to check if it is imported +# correctly by git p4. +# git p4 is expected to use the client view to also not include the common +# "base" folder in the imported directory structure. +test_expect_success 'git p4 clone simple branches with base folder on server side' ' + test_create_repo "$git" && + ( + cd "$git" && + git config git-p4.branchList branch1:branch2 && + git config --add git-p4.branchList branch1:branch3 && + git p4 clone --dest=. --use-client-spec --detect-branches //depot@all && + git log --all --graph --decorate --stat && + git reset --hard p4/depot/branch1 && + test -f file1 && + test -f file2 && + test -f file3 && + test -f sub_file1 && + grep update file2 && + git reset --hard p4/depot/branch2 && + test -f file1 && + test -f file2 && + test ! -f file3 && + test -f sub_file1 && + ! grep update file2 && + git reset --hard p4/depot/branch3 && + test -f file1 && + test -f file2 && + test -f file3 && + test -f sub_file1 && + grep update file2 && + cd "$cli" && + cd branch1 && + p4 edit file2 && + echo file2_ >>file2 && + p4 submit -d "update file2 in branch1" && + cd "$git" && + git reset --hard p4/depot/branch1 && + git p4 rebase && + grep file2_ file2 + ) +' + +# Now update a file in one of the branches in git and submit to P4 +test_expect_success 'Update a file in git side and submit to P4 using client view' ' + test_when_finished cleanup_git && + ( + cd "$git" && + git reset --hard p4/depot/branch1 && + echo "client spec" >> file1 && + git add -u . && + git commit -m "update file1 in branch1" && + git config git-p4.skipSubmitEdit true && + git p4 submit --verbose && + cd "$cli" && + p4 sync ... && + cd branch1 && + grep "client spec" file1 + ) +' + test_expect_success 'kill p4d' ' kill_p4d ' diff --git a/t/t9803-git-p4-shell-metachars.sh b/t/t9803-git-p4-shell-metachars.sh index fbacff34fe..d950c7d665 100755 --- a/t/t9803-git-p4-shell-metachars.sh +++ b/t/t9803-git-p4-shell-metachars.sh @@ -28,7 +28,7 @@ test_expect_success 'shell metachars in filenames' ' echo f2 >"file with spaces" && git add "file with spaces" && git commit -m "add files" && - P4EDITOR=touch git p4 submit + P4EDITOR="test-chmtime +5" git p4 submit ) && ( cd "$cli" && @@ -47,7 +47,7 @@ test_expect_success 'deleting with shell metachars' ' git rm foo\$bar && git rm file\ with\ spaces && git commit -m "remove files" && - P4EDITOR=touch git p4 submit + P4EDITOR="test-chmtime +5" git p4 submit ) && ( cd "$cli" && diff --git a/t/t9805-git-p4-skip-submit-edit.sh b/t/t9805-git-p4-skip-submit-edit.sh index 89311886db..5fbf904dc8 100755 --- a/t/t9805-git-p4-skip-submit-edit.sh +++ b/t/t9805-git-p4-skip-submit-edit.sh @@ -90,7 +90,7 @@ test_expect_success 'no config, edited' ' cd "$git" && echo line >>file1 && git commit -a -m "change 5" && - P4EDITOR="$TRASH_DIRECTORY/ed.sh" && + P4EDITOR="\"$TRASH_DIRECTORY/ed.sh\"" && export P4EDITOR && git p4 submit && p4 changes //depot/... >wc && diff --git a/t/t9813-git-p4-preserve-users.sh b/t/t9813-git-p4-preserve-users.sh index 166b840bfa..0fe2312807 100755 --- a/t/t9813-git-p4-preserve-users.sh +++ b/t/t9813-git-p4-preserve-users.sh @@ -53,7 +53,9 @@ test_expect_success 'preserve users' ' git commit --author "Alice <alice@example.com>" -m "a change by alice" file1 && git commit --author "Bob <bob@example.com>" -m "a change by bob" file2 && git config git-p4.skipSubmitEditCheck true && - P4EDITOR=touch P4USER=alice P4PASSWD=secret git p4 commit --preserve-user && + P4EDITOR="test-chmtime +5" P4USER=alice P4PASSWD=secret && + export P4EDITOR P4USER P4PASSWD && + git p4 commit --preserve-user && p4_check_commit_author file1 alice && p4_check_commit_author file2 bob ) @@ -69,7 +71,7 @@ test_expect_success 'refuse to preserve users without perms' ' git config git-p4.skipSubmitEditCheck true && echo "username-noperms: a change by alice" >>file1 && git commit --author "Alice <alice@example.com>" -m "perms: a change by alice" file1 && - P4EDITOR=touch P4USER=bob P4PASSWD=secret && + P4EDITOR="test-chmtime +5" P4USER=bob P4PASSWD=secret && export P4EDITOR P4USER P4PASSWD && test_must_fail git p4 commit --preserve-user && ! git diff --exit-code HEAD..p4/master @@ -87,7 +89,7 @@ test_expect_success 'preserve user where author is unknown to p4' ' git commit --author "Bob <bob@example.com>" -m "preserve: a change by bob" file1 && echo "username-unknown: a change by charlie" >>file1 && git commit --author "Charlie <charlie@example.com>" -m "preserve: a change by charlie" file1 && - P4EDITOR=touch P4USER=alice P4PASSWD=secret && + P4EDITOR="test-chmtime +5" P4USER=alice P4PASSWD=secret && export P4EDITOR P4USER P4PASSWD && test_must_fail git p4 commit --preserve-user && ! git diff --exit-code HEAD..p4/master && diff --git a/t/t9814-git-p4-rename.sh b/t/t9814-git-p4-rename.sh index 99bb71b89c..c89992cf95 100755 --- a/t/t9814-git-p4-rename.sh +++ b/t/t9814-git-p4-rename.sh @@ -226,14 +226,9 @@ test_expect_success 'detect copies' ' # See if configurables can be set, and in particular if the run.move.allow # variable exists, which allows admins to disable the "p4 move" command. -test_expect_success 'p4 configure command and run.move.allow are available' ' - p4 configure show run.move.allow >out ; retval=$? && - test $retval = 0 && - { - egrep ^run.move.allow: out && - test_set_prereq P4D_HAVE_CONFIGURABLE_RUN_MOVE_ALLOW || - true - } || true +test_lazy_prereq P4D_HAVE_CONFIGURABLE_RUN_MOVE_ALLOW ' + p4 configure show run.move.allow >out && + egrep ^run.move.allow: out ' # If move can be disabled, turn it off and test p4 move handling diff --git a/t/t9816-git-p4-locked.sh b/t/t9816-git-p4-locked.sh index e71e543343..d048bd33fa 100755 --- a/t/t9816-git-p4-locked.sh +++ b/t/t9816-git-p4-locked.sh @@ -35,13 +35,13 @@ test_expect_success 'edit with lock not taken' ' ) ' -test_expect_failure 'add with lock not taken' ' +test_expect_success 'add with lock not taken' ' test_when_finished cleanup_git && git p4 clone --dest="$git" //depot && ( cd "$git" && echo line1 >>add-lock-not-taken && - git add file2 && + git add add-lock-not-taken && git commit -m "add add-lock-not-taken" && git config git-p4.skipSubmitEdit true && git p4 submit --verbose @@ -107,7 +107,7 @@ test_expect_failure 'chmod with lock taken' ' ) ' -test_expect_failure 'copy with lock taken' ' +test_expect_success 'copy with lock taken' ' lock_in_another_client && test_when_finished cleanup_git && test_when_finished "cd \"$cli\" && p4 revert file2 && rm -f file2" && @@ -130,8 +130,8 @@ test_expect_failure 'move with lock taken' ' git p4 clone --dest="$git" //depot && ( cd "$git" && - git mv file1 file2 && - git commit -m "mv file1 to file2" && + git mv file1 file3 && + git commit -m "mv file1 to file3" && git config git-p4.skipSubmitEdit true && git config git-p4.detectRenames true && git p4 submit --verbose diff --git a/t/t9818-git-p4-block.sh b/t/t9818-git-p4-block.sh new file mode 100755 index 0000000000..3b3ae1f59a --- /dev/null +++ b/t/t9818-git-p4-block.sh @@ -0,0 +1,119 @@ +#!/bin/sh + +test_description='git p4 fetching changes in multiple blocks' + +. ./lib-git-p4.sh + +test_expect_success 'start p4d' ' + start_p4d +' + +create_restricted_group() { + p4 group -i <<-EOF + Group: restricted + MaxResults: 7 + MaxScanRows: 40 + Users: author + EOF +} + +test_expect_success 'Create group with limited maxrows' ' + create_restricted_group +' + +test_expect_success 'Create a repo with many changes' ' + ( + client_view "//depot/included/... //client/included/..." \ + "//depot/excluded/... //client/excluded/..." && + mkdir -p "$cli/included" "$cli/excluded" && + cd "$cli/included" && + >file.txt && + p4 add file.txt && + p4 submit -d "Add file.txt" && + for i in $(test_seq 0 5) + do + >outer$i.txt && + p4 add outer$i.txt && + p4 submit -d "Adding outer$i.txt" && + for j in $(test_seq 0 5) + do + p4 edit file.txt && + echo $i$j >file.txt && + p4 submit -d "Commit $i$j" || exit + done || exit + done + ) +' + +test_expect_success 'Default user cannot fetch changes' ' + ! p4 changes -m 1 //depot/... +' + +test_expect_success 'Clone the repo' ' + git p4 clone --dest="$git" --changes-block-size=7 --verbose //depot/included@all +' + +test_expect_success 'All files are present' ' + echo file.txt >expected && + test_write_lines outer0.txt outer1.txt outer2.txt outer3.txt outer4.txt >>expected && + test_write_lines outer5.txt >>expected && + ls "$git" >current && + test_cmp expected current +' + +test_expect_success 'file.txt is correct' ' + echo 55 >expected && + test_cmp expected "$git/file.txt" +' + +test_expect_success 'Correct number of commits' ' + (cd "$git" && git log --oneline) >log && + wc -l log && + test_line_count = 43 log +' + +test_expect_success 'Previous version of file.txt is correct' ' + (cd "$git" && git checkout HEAD^^) && + echo 53 >expected && + test_cmp expected "$git/file.txt" +' + +# Test git-p4 sync, with some files outside the client specification. + +p4_add_file() { + (cd "$cli" && + >$1 && + p4 add $1 && + p4 submit -d "Added a file" $1 + ) +} + +test_expect_success 'Add some more files' ' + for i in $(test_seq 0 10) + do + p4_add_file "included/x$i" && + p4_add_file "excluded/x$i" + done && + for i in $(test_seq 0 10) + do + p4_add_file "excluded/y$i" + done +' + +# This should pick up the 10 new files in "included", but not be confused +# by the additional files in "excluded" +test_expect_success 'Syncing files' ' + ( + cd "$git" && + git p4 sync --changes-block-size=7 && + git checkout p4/master && + ls -l x* > log && + test_line_count = 11 log + ) +' + +test_expect_success 'kill p4d' ' + kill_p4d +' + +test_done diff --git a/t/t9819-git-p4-case-folding.sh b/t/t9819-git-p4-case-folding.sh new file mode 100755 index 0000000000..78f1d0f92d --- /dev/null +++ b/t/t9819-git-p4-case-folding.sh @@ -0,0 +1,54 @@ +#!/bin/sh + +test_description='interaction with P4 case-folding' + +. ./lib-git-p4.sh + +test_expect_success 'start p4d with case folding enabled' ' + start_p4d -C1 +' + +test_expect_success 'Create a repo, name is lowercase' ' + ( + client_view "//depot/... //client/..." && + cd "$cli" && + mkdir -p lc UC && + >lc/file.txt && >UC/file.txt && + p4 add lc/file.txt UC/file.txt && + p4 submit -d "Add initial lc and UC repos" + ) +' + +test_expect_success 'Check p4 is in case-folding mode' ' + ( + cd "$cli" && + >lc/FILE.TXT && + p4 add lc/FILE.TXT && + test_must_fail p4 submit -d "Cannot add file differing only in case" lc/FILE.TXT + ) +' + +# Check we created the repo properly +test_expect_success 'Clone lc repo using lc name' ' + git p4 clone //depot/lc/... && + test_path_is_file lc/file.txt && + git p4 clone //depot/UC/... && + test_path_is_file UC/file.txt +' + +# The clone should fail, since there is no repo called LC, but because +# we have case-insensitive p4d enabled, it appears to go ahead and work, +# but leaves an empty git repo in place. +test_expect_failure 'Clone lc repo using uc name' ' + test_must_fail git p4 clone //depot/LC/... +' + +test_expect_failure 'Clone UC repo with lc name' ' + test_must_fail git p4 clone //depot/uc/... +' + +test_expect_success 'kill p4d' ' + kill_p4d +' + +test_done diff --git a/t/t9820-git-p4-editor-handling.sh b/t/t9820-git-p4-editor-handling.sh new file mode 100755 index 0000000000..6dc6df032e --- /dev/null +++ b/t/t9820-git-p4-editor-handling.sh @@ -0,0 +1,38 @@ +#!/bin/sh + +test_description='git p4 handling of EDITOR' + +. ./lib-git-p4.sh + +test_expect_success 'start p4d' ' + start_p4d +' + +test_expect_success 'init depot' ' + ( + cd "$cli" && + echo file1 >file1 && + p4 add file1 && + p4 submit -d "file1" + ) +' + +# Check that the P4EDITOR argument can be given command-line +# options, which git-p4 will then pass through to the shell. +test_expect_success 'EDITOR with options' ' + git p4 clone --dest="$git" //depot && + test_when_finished cleanup_git && + ( + cd "$git" && + echo change >file1 && + git commit -m "change" file1 && + P4EDITOR=": >\"$git/touched\" && test-chmtime +5" git p4 submit && + test_path_is_file "$git/touched" + ) +' + +test_expect_success 'kill p4d' ' + kill_p4d +' + +test_done diff --git a/t/t9902-completion.sh b/t/t9902-completion.sh index 4a14a5892e..2ba62fbc17 100755 --- a/t/t9902-completion.sh +++ b/t/t9902-completion.sh @@ -370,6 +370,40 @@ test_expect_success '__git_remotes - list remotes from $GIT_DIR/remotes and from test_cmp expect actual ' +test_expect_success '__git_get_config_variables' ' + cat >expect <<-EOF && + name-1 + name-2 + EOF + test_config interesting.name-1 good && + test_config interesting.name-2 good && + test_config subsection.interesting.name-3 bad && + __git_get_config_variables interesting >actual && + test_cmp expect actual +' + +test_expect_success '__git_pretty_aliases' ' + cat >expect <<-EOF && + author + hash + EOF + test_config pretty.author "%an %ae" && + test_config pretty.hash %H && + __git_pretty_aliases >actual && + test_cmp expect actual +' + +test_expect_success '__git_aliases' ' + cat >expect <<-EOF && + ci + co + EOF + test_config alias.ci commit && + test_config alias.co checkout && + __git_aliases >actual && + test_cmp expect actual +' + test_expect_success 'basic' ' run_completion "git " && # built-in diff --git a/t/test-lib-functions.sh b/t/test-lib-functions.sh index 8f8858a5f0..e8d3c0fdbc 100644 --- a/t/test-lib-functions.sh +++ b/t/test-lib-functions.sh @@ -348,11 +348,18 @@ test_declared_prereq () { return 1 } +test_verify_prereq () { + test -z "$test_prereq" || + expr >/dev/null "$test_prereq" : '[A-Z0-9_,!]*$' || + error "bug in the test script: '$test_prereq' does not look like a prereq" +} + test_expect_failure () { test_start_ test "$#" = 3 && { test_prereq=$1; shift; } || test_prereq= test "$#" = 2 || error "bug in the test script: not 2 or 3 parameters to test-expect-failure" + test_verify_prereq export test_prereq if ! test_skip "$@" then @@ -372,6 +379,7 @@ test_expect_success () { test "$#" = 3 && { test_prereq=$1; shift; } || test_prereq= test "$#" = 2 || error "bug in the test script: not 2 or 3 parameters to test-expect-success" + test_verify_prereq export test_prereq if ! test_skip "$@" then @@ -400,6 +408,7 @@ test_external () { error >&5 "bug in the test script: not 3 or 4 parameters to test_external" descr="$1" shift + test_verify_prereq export test_prereq if ! test_skip "$descr" "$@" then diff --git a/t/test-lib.sh b/t/test-lib.sh index 4ea99a209d..39da9c2d99 100644 --- a/t/test-lib.sh +++ b/t/test-lib.sh @@ -529,7 +529,7 @@ test_run_ () { test_cleanup=: expecting_failure=$2 - if test "${GIT_TEST_CHAIN_LINT:-0}" != 0; then + if test "${GIT_TEST_CHAIN_LINT:-1}" != 0; then # 117 is magic because it is unlikely to match the exit # code of other programs test_eval_ "(exit 117) && $1" diff --git a/templates/hooks--applypatch-msg.sample b/templates/hooks--applypatch-msg.sample index 8b2a2fe84f..a5d7b84a67 100755 --- a/templates/hooks--applypatch-msg.sample +++ b/templates/hooks--applypatch-msg.sample @@ -10,6 +10,6 @@ # To enable this hook, rename this file to "applypatch-msg". . git-sh-setup -test -x "$GIT_DIR/hooks/commit-msg" && - exec "$GIT_DIR/hooks/commit-msg" ${1+"$@"} +commitmsg="$(git rev-parse --git-path hooks/commit-msg)" +test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"} : diff --git a/templates/hooks--pre-applypatch.sample b/templates/hooks--pre-applypatch.sample index b1f187c2e9..4142082bcb 100755 --- a/templates/hooks--pre-applypatch.sample +++ b/templates/hooks--pre-applypatch.sample @@ -9,6 +9,6 @@ # To enable this hook, rename this file to "pre-applypatch". . git-sh-setup -test -x "$GIT_DIR/hooks/pre-commit" && - exec "$GIT_DIR/hooks/pre-commit" ${1+"$@"} +precommit="$(git rev-parse --git-path hooks/pre-commit)" +test -x "$precommit" && exec "$precommit" ${1+"$@"} : 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; +} @@ -310,6 +310,7 @@ void trace_repo_setup(const char *prefix) prefix = "(null)"; trace_printf_key(&key, "setup: git_dir: %s\n", quote_crnl(get_git_dir())); + trace_printf_key(&key, "setup: git_common_dir: %s\n", quote_crnl(get_git_common_dir())); trace_printf_key(&key, "setup: worktree: %s\n", quote_crnl(git_work_tree)); trace_printf_key(&key, "setup: cwd: %s\n", quote_crnl(cwd)); trace_printf_key(&key, "setup: prefix: %s\n", quote_crnl(prefix)); diff --git a/transport.c b/transport.c index eca9b8c817..40692f8ae8 100644 --- a/transport.c +++ b/transport.c @@ -278,12 +278,11 @@ static int fetch_objs_via_rsync(struct transport *transport, return run_command(&rsync); } -static int write_one_ref(const char *name, const unsigned char *sha1, - int flags, void *data) +static int write_one_ref(const char *name, const struct object_id *oid, + int flags, void *data) { struct strbuf *buf = data; int len = buf->len; - FILE *f; /* when called via for_each_ref(), flags is non-zero */ if (flags && !starts_with(name, "refs/heads/") && @@ -292,27 +291,26 @@ static int write_one_ref(const char *name, const unsigned char *sha1, strbuf_addstr(buf, name); if (safe_create_leading_directories(buf->buf) || - !(f = fopen(buf->buf, "w")) || - fprintf(f, "%s\n", sha1_to_hex(sha1)) < 0 || - fclose(f)) - return error("problems writing temporary file %s", buf->buf); + write_file(buf->buf, 0, "%s\n", oid_to_hex(oid))) + return error("problems writing temporary file %s: %s", + buf->buf, strerror(errno)); strbuf_setlen(buf, len); return 0; } static int write_refs_to_temp_dir(struct strbuf *temp_dir, - int refspec_nr, const char **refspec) + int refspec_nr, const char **refspec) { int i; for (i = 0; i < refspec_nr; i++) { - unsigned char sha1[20]; + struct object_id oid; char *ref; - if (dwim_ref(refspec[i], strlen(refspec[i]), sha1, &ref) != 1) + if (dwim_ref(refspec[i], strlen(refspec[i]), oid.hash, &ref) != 1) return error("Could not get ref %s", refspec[i]); - if (write_one_ref(ref, sha1, 0, temp_dir)) { + if (write_one_ref(ref, &oid, 0, temp_dir)) { free(ref); return -1; } diff --git a/tree-diff.c b/tree-diff.c index e7b378c8b2..290a1da4ce 100644 --- a/tree-diff.c +++ b/tree-diff.c @@ -64,7 +64,7 @@ static int emit_diff_first_parent_only(struct diff_options *opt, struct combine_ { struct combine_diff_parent *p0 = &p->parent[0]; if (p->mode && p0->mode) { - opt->change(opt, p0->mode, p->mode, p0->sha1, p->sha1, + opt->change(opt, p0->mode, p->mode, p0->oid.hash, p->oid.hash, 1, 1, p->path, 0, 0); } else { @@ -74,11 +74,11 @@ static int emit_diff_first_parent_only(struct diff_options *opt, struct combine_ if (p->mode) { addremove = '+'; - sha1 = p->sha1; + sha1 = p->oid.hash; mode = p->mode; } else { addremove = '-'; - sha1 = p0->sha1; + sha1 = p0->oid.hash; mode = p0->mode; } @@ -151,7 +151,7 @@ static struct combine_diff_path *path_appendnew(struct combine_diff_path *last, memcpy(p->path + base->len, path, pathlen); p->path[len] = 0; p->mode = mode; - hashcpy(p->sha1, sha1 ? sha1 : null_sha1); + hashcpy(p->oid.hash, sha1 ? sha1 : null_sha1); return p; } @@ -238,7 +238,7 @@ static struct combine_diff_path *emit_path(struct combine_diff_path *p, } p->parent[i].mode = mode_i; - hashcpy(p->parent[i].sha1, sha1_i ? sha1_i : null_sha1); + hashcpy(p->parent[i].oid.hash, sha1_i ? sha1_i : null_sha1); } keep = 1; 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 aa84576500..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; @@ -74,7 +78,7 @@ static int write_one_shallow(const struct commit_graft *graft, void *cb_data) { FILE *fp = cb_data; if (graft->nr_parent == -1) - fprintf(fp, "--shallow %s\n", sha1_to_hex(graft->sha1)); + fprintf(fp, "--shallow %s\n", oid_to_hex(&graft->oid)); return 0; } @@ -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; @@ -681,9 +690,9 @@ static void receive_needs(void) } /* return non-zero if the ref is hidden, otherwise 0 */ -static int mark_our_ref(const char *refname, const unsigned char *sha1) +static int mark_our_ref(const char *refname, const struct object_id *oid) { - struct object *o = lookup_unknown_object(sha1); + struct object *o = lookup_unknown_object(oid->hash); if (ref_is_hidden(refname)) { o->flags |= HIDDEN_REF; @@ -693,9 +702,10 @@ static int mark_our_ref(const char *refname, const unsigned char *sha1) return 0; } -static int check_ref(const char *refname, const unsigned char *sha1, int flag, void *cb_data) +static int check_ref(const char *refname, const struct object_id *oid, + int flag, void *cb_data) { - mark_our_ref(refname, sha1); + mark_our_ref(refname, oid); return 0; } @@ -709,48 +719,52 @@ static void format_symref_info(struct strbuf *buf, struct string_list *symref) strbuf_addf(buf, " symref=%s:%s", item->string, (char *)item->util); } -static int send_ref(const char *refname, const unsigned char *sha1, int flag, void *cb_data) +static int send_ref(const char *refname, const struct object_id *oid, + int flag, void *cb_data) { static const char *capabilities = "multi_ack thin-pack side-band" " side-band-64k ofs-delta shallow no-progress" " include-tag multi_ack_detailed"; const char *refname_nons = strip_namespace(refname); - unsigned char peeled[20]; + struct object_id peeled; - if (mark_our_ref(refname, sha1)) + if (mark_our_ref(refname, oid)) return 0; if (capabilities) { 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", - sha1_to_hex(sha1), refname_nons, + 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()); strbuf_release(&symref_info); } else { - packet_write(1, "%s %s\n", sha1_to_hex(sha1), refname_nons); + packet_write(1, "%s %s\n", oid_to_hex(oid), refname_nons); } capabilities = NULL; - if (!peel_ref(refname, peeled)) - packet_write(1, "%s %s^{}\n", sha1_to_hex(peeled), refname_nons); + if (!peel_ref(refname, peeled.hash)) + packet_write(1, "%s %s^{}\n", oid_to_hex(&peeled), refname_nons); return 0; } -static int find_symref(const char *refname, const unsigned char *sha1, int flag, - void *cb_data) +static int find_symref(const char *refname, const struct object_id *oid, + int flag, void *cb_data) { const char *symref_target; struct string_list_item *item; - unsigned char unused[20]; + struct object_id unused; if ((flag & REF_ISSYMREF) == 0) return 0; - symref_target = resolve_ref_unsafe(refname, 0, unused, &flag); + symref_target = resolve_ref_unsafe(refname, 0, unused.hash, &flag); if (!symref_target || (flag & REF_ISSYMREF) == 0) die("'%s' is a symref but it is not?", refname); item = string_list_append(cb_data, refname); @@ -787,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; @@ -31,7 +31,9 @@ char *reencode_string_len(const char *in, int insz, const char *in_encoding, int *outsz); #else -#define reencode_string_len(a,b,c,d,e) NULL +static inline char *reencode_string_len(const char *a, int b, + const char *c, const char *d, int *e) +{ if (e) *e = 0; return NULL; } #endif static inline char *reencode_string(const char *in, @@ -200,9 +200,11 @@ static int interpret_target(struct walker *walker, char *target, unsigned char * return -1; } -static int mark_complete(const char *path, const unsigned char *sha1, int flag, void *cb_data) +static int mark_complete(const char *path, const struct object_id *oid, + int flag, void *cb_data) { - struct commit *commit = lookup_commit_reference_gently(sha1, 1); + struct commit *commit = lookup_commit_reference_gently(oid->hash, 1); + if (commit) { commit->object.flags |= COMPLETE; commit_list_insert(commit, &complete); @@ -564,3 +564,39 @@ char *xgetcwd(void) die_errno(_("unable to get current working directory")); return strbuf_detach(&sb, NULL); } + +int write_file(const char *path, int fatal, const char *fmt, ...) +{ + struct strbuf sb = STRBUF_INIT; + va_list params; + int fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0666); + if (fd < 0) { + if (fatal) + die_errno(_("could not open %s for writing"), path); + return -1; + } + va_start(params, fmt); + strbuf_vaddf(&sb, fmt, params); + va_end(params); + if (write_in_full(fd, sb.buf, sb.len) != sb.len) { + int err = errno; + close(fd); + strbuf_release(&sb); + errno = err; + if (fatal) + die_errno(_("could not write to %s"), path); + return -1; + } + strbuf_release(&sb); + if (close(fd)) { + if (fatal) + die_errno(_("could not close %s"), path); + return -1; + } + return 0; +} + +void sleep_millisec(int millisec) +{ + poll(NULL, 0, millisec); +} diff --git a/wt-status.c b/wt-status.c index 38cb165f12..eaed4fed32 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); @@ -823,10 +825,11 @@ void wt_status_truncate_message_at_cut_line(struct strbuf *buf) const char *p; struct strbuf pattern = STRBUF_INIT; - strbuf_addf(&pattern, "%c %s", comment_line_char, cut_line); - p = strstr(buf->buf, pattern.buf); - if (p && (p == buf->buf || p[-1] == '\n')) - strbuf_setlen(buf, p - buf->buf); + strbuf_addf(&pattern, "\n%c %s", comment_line_char, cut_line); + if (starts_with(buf->buf, pattern.buf + 1)) + strbuf_setlen(buf, 0); + else if ((p = strstr(buf->buf, pattern.buf))) + strbuf_setlen(buf, p - buf->buf + 1); strbuf_release(&pattern); } @@ -1532,21 +1535,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); |