diff options
308 files changed, 10606 insertions, 2146 deletions
diff --git a/.gitignore b/.gitignore index 20560b810b..87b833c9d8 100644 --- a/.gitignore +++ b/.gitignore @@ -112,6 +112,8 @@ /git-remote-https /git-remote-ftp /git-remote-ftps +/git-remote-fd +/git-remote-ext /git-remote-testgit /git-repack /git-replace diff --git a/Documentation/CodingGuidelines b/Documentation/CodingGuidelines index 09ffc46563..1b1c45df5c 100644 --- a/Documentation/CodingGuidelines +++ b/Documentation/CodingGuidelines @@ -31,6 +31,10 @@ But if you must have a list of rules, here they are. For shell scripts specifically (not exhaustive): + - We use tabs for indentation. + + - Case arms are indented at the same depth as case and esac lines. + - We prefer $( ... ) for command substitution; unlike ``, it properly nests. It should have been the way Bourne spelled it from day one, but unfortunately isn't. @@ -139,3 +143,55 @@ For C programs: - When we pass <string, length> pair to functions, we should try to pass them in that order. + +Writing Documentation: + + Every user-visible change should be reflected in the documentation. + The same general rule as for code applies -- imitate the existing + conventions. A few commented examples follow to provide reference + when writing or modifying command usage strings and synopsis sections + in the manual pages: + + Placeholders are enclosed in angle brackets: + <file> + --sort=<key> + --abbrev[=<n>] + + Possibility of multiple occurences is indicated by three dots: + <file>... + (One or more of <file>.) + + Optional parts are enclosed in square brackets: + [<extra>] + (Zero or one <extra>.) + + --exec-path[=<path>] + (Option with an optional argument. Note that the "=" is inside the + brackets.) + + [<patch>...] + (Zero or more of <patch>. Note that the dots are inside, not + outside the brackets.) + + Multiple alternatives are indicated with vertical bar: + [-q | --quiet] + [--utf8 | --no-utf8] + + Parentheses are used for grouping: + [(<rev>|<range>)...] + (Any number of either <rev> or <range>. Parens are needed to make + it clear that "..." pertains to both <rev> and <range>.) + + [(-p <parent>)...] + (Any number of option -p, each with one <parent> argument.) + + git remote set-head <name> (-a | -d | <branch>) + (One and only one of "-a", "-d" or "<branch>" _must_ (no square + brackets) be provided.) + + And a somewhat more contrived example: + --diff-filter=[(A|C|D|M|R|T|U|X|B)...[*]] + Here "=" is outside the brackets, because "--diff-filter=" is a + valid usage. "*" has its own pair of brackets, because it can + (optionally) be specified only when one or more of the letters is + also provided. diff --git a/Documentation/RelNotes/1.7.0.8.txt b/Documentation/RelNotes/1.7.0.8.txt new file mode 100644 index 0000000000..7f05b48e17 --- /dev/null +++ b/Documentation/RelNotes/1.7.0.8.txt @@ -0,0 +1,10 @@ +Git v1.7.0.8 Release Notes +========================== + +This is primarily to backport support for the new "add.ignoreErrors" +name given to the existing "add.ignore-errors" configuration variable. + +The next version, Git 1.7.4, and future versions, will support both +old and incorrect name and the new corrected name, but without this +backport, users who want to use the new name "add.ignoreErrors" in +their repositories cannot use older versions of Git. diff --git a/Documentation/RelNotes/1.7.1.3.txt b/Documentation/RelNotes/1.7.1.3.txt new file mode 100644 index 0000000000..5b18518449 --- /dev/null +++ b/Documentation/RelNotes/1.7.1.3.txt @@ -0,0 +1,10 @@ +Git v1.7.1.3 Release Notes +========================== + +This is primarily to backport support for the new "add.ignoreErrors" +name given to the existing "add.ignore-errors" configuration variable. + +The next version, Git 1.7.4, and future versions, will support both +old and incorrect name and the new corrected name, but without this +backport, users who want to use the new name "add.ignoreErrors" in +their repositories cannot use older versions of Git. diff --git a/Documentation/RelNotes/1.7.2.4.txt b/Documentation/RelNotes/1.7.2.4.txt new file mode 100644 index 0000000000..f7950a4c04 --- /dev/null +++ b/Documentation/RelNotes/1.7.2.4.txt @@ -0,0 +1,10 @@ +Git v1.7.2.4 Release Notes +========================== + +This is primarily to backport support for the new "add.ignoreErrors" +name given to the existing "add.ignore-errors" configuration variable. + +The next version, Git 1.7.4, and future versions, will support both +old and incorrect name and the new corrected name, but without this +backport, users who want to use the new name "add.ignoreErrors" in +their repositories cannot use older versions of Git. diff --git a/Documentation/RelNotes/1.7.3.3.txt b/Documentation/RelNotes/1.7.3.3.txt new file mode 100644 index 0000000000..9b2b2448df --- /dev/null +++ b/Documentation/RelNotes/1.7.3.3.txt @@ -0,0 +1,54 @@ +Git v1.7.3.3 Release Notes +========================== + +In addition to the usual fixes, this release also includes support for +the new "add.ignoreErrors" name given to the existing "add.ignore-errors" +configuration variable. + +The next version, Git 1.7.4, and future versions, will support both +old and incorrect name and the new corrected name, but without this +backport, users who want to use the new name "add.ignoreErrors" in +their repositories cannot use older versions of Git. + +Fixes since v1.7.3.2 +-------------------- + + * "git apply" segfaulted when a bogus input is fed to it. + + * Running "git cherry-pick --ff" on a root commit segfaulted. + + * "diff", "blame" and friends incorrectly applied textconv filters to + symlinks. + + * Highlighting of whitespace breakage in "diff" output was showing + incorrect amount of whitespaces when blank-at-eol is set and the line + consisted only of whitespaces and a TAB. + + * "diff" was overly inefficient when trying to find the line to use for + the function header (i.e. equivalent to --show-c-function of GNU diff). + + * "git imap-send" depends on libcrypto but our build rule relied on the + linker to implicitly link it via libssl, which was wrong. + + * "git merge-file" can be called from within a subdirectory now. + + * "git repack -f" expanded and recompressed non-delta objects in the + existing pack, which was wasteful. Use new "-F" option if you really + want to (e.g. when changing the pack.compression level). + + * "git rev-list --format="...%x00..." incorrectly chopped its output + at NUL. + + * "git send-email" did not correctly remove duplicate mail addresses from + the Cc: header that appear on the To: header. + + * The completion script (in contrib/completion) ignored lightweight tags + in __git_ps1(). + + * "git-blame" mode (in contrib/emacs) didn't say (require 'format-spec) + even though it depends on it; it didn't work with Emacs 22 or older + unless Gnus is used. + + * "git-p4" (in contrib/) did not correctly handle deleted files. + +Other minor fixes and documentation updates are also included. diff --git a/Documentation/RelNotes/1.7.3.4.txt b/Documentation/RelNotes/1.7.3.4.txt new file mode 100644 index 0000000000..925178f608 --- /dev/null +++ b/Documentation/RelNotes/1.7.3.4.txt @@ -0,0 +1,32 @@ +Git v1.7.3.4 Release Notes +========================== + +Fixes since v1.7.3.3 +-------------------- + + * Smart HTTP transport used to incorrectly retry redirected POST + request with GET request. + + * "git apply" did not correctly handle patches that only change modes + if told to apply while stripping leading paths with -p option. + + * "git apply" can deal with patches with timezone formatted with a + colon between the hours and minutes part (e.g. "-08:00" instead of + "-0800"). + + * "git cherry-pick" or "git revert" refused to work when a path that + would be modified by the operation was stat-dirty without a real + difference in the contents of the file. + + * "git diff --check" reported an incorrect line number for added + blank lines at the end of file. + + * Setting log.decorate configuration variable to "0" or "1" to mean + "false" or "true" did not work. + + * "git tag -v" did not work with GPG signatures in rfc1991 mode. + + * The post-receive-email sample hook was accidentally broken in 1.7.3.3 + update. + +Other minor fixes and documentation updates are also included. diff --git a/Documentation/RelNotes/1.7.4.txt b/Documentation/RelNotes/1.7.4.txt index 98bd62f022..6084f7ddf6 100644 --- a/Documentation/RelNotes/1.7.4.txt +++ b/Documentation/RelNotes/1.7.4.txt @@ -19,9 +19,30 @@ Updates since v1.7.3 /etc/gitattributes; core.attributesfile configuration variable can be used to customize the path to this file. + * The thread structure generated by "git send-email" has changed + slightly. Setting the cover letter of the latest series as a reply + to the cover letter of the previous series with --in-reply-to used + to make the new cover letter and all the patches replies to the + cover letter of the previous series; this has been changed to make + the patches in the new series replies to the new cover letter. + + * Bash completion script in contrib/ has been adjusted to be also + usable by zsh. + + * "git blame" learned --show-email option to display the e-mail + addresses instead of the names of authors. + + * "git daemon" can be built in MinGW environment. + + * "git daemon" can take more than one --listen option to listen to + multiple addresses. + * "git diff" and "git grep" learned how functions and subroutines in Fortran look like. + * "git mergetool" tells vim/gvim to show three-way diff by default + (use vimdiff2/gvimdiff2 as the tool name for old behaviour). + * "git log -G<pattern>" limits the output to commits whose change has added or deleted lines that match the given pattern. @@ -29,13 +50,43 @@ Updates since v1.7.3 deprecated; we might want to remove it in the future. Users can use the new --empty option to be more explicit instead. + * "git repack -f" does not spend cycles to recompress objects in the + non-delta representation anymore (use -F if you really mean it when + e.g. you changed the compression level). + * "git merge --log" used to limit the resulting merge log to 20 entries; this is now customizable by giving e.g. "--log=47". + * "git merge" may work better when all files were moved out of a + directory in one branch while a new file is created in place of that + directory in the other branch. + + * "git rebase --autosquash" can use SHA-1 object names to name which + commit to fix up (e.g. "fixup! e83c5163"). + + * The default "recursive" merge strategy learned --rename-threshold + option to influence the rename detection, similar to the -M option + of "git diff". E.g. "git merge -Xrename-threshold=50% ..." to use + this. + + * The "recursive" strategy also learned to ignore various whitespace + changes; the most notable is -Xignore-space-at-eol. + + * "git send-email" learned "--to-cmd", similar to "--cc-cmd", to read + recipient list from a command output. + + * "git send-email" learned to read and use "To:" from its input files. + * you can extend "git shell", which is often used on boxes that allow git-only login over ssh as login shell, with custom set of commands. + * "git submodule sync" updates metainformation for all submodules, + not just the ones that have been checked out. + + * gitweb can use custom 'highlight' command with its configuration file. + + Also contains various documentation updates. @@ -45,14 +96,28 @@ Fixes since v1.7.3 All of the fixes in v1.7.3.X maintenance series are included in this release, unless otherwise noted. + * "git checkout" removed an untracked file "foo" from the working + tree when switching to a branch that contains a tracked path + "foo/bar". Prevent this, just like the case where the conflicting + path were "foo" (c752e7f..7980872d). + * "git log --author=me --author=her" did not find commits written by me or by her; instead it looked for commits written by me and by her, which is impossible. + * "git merge" into an unborn branch removed an untracked file "foo" + from the working tree when merged branch had "foo" (2caf20c..172b642). + + * "git push --progress" shows progress indicators now. + + * "git repack" places its temporary packs under $GIT_OBJECT_DIRECTORY/pack + instead of $GIT_OBJECT_DIRECTORY/ to avoid cross directory renames. + + * "git submodule update --recursive --other-flags" passes flags down + to its subinvocations. --- exec >/var/tmp/1 -O=v1.7.3 -O=v1.7.3.1-42-g34289ec +O=v1.7.3.2-450-g5b9c331 echo O=$(git describe master) git shortlog --no-merges ^maint ^$O master diff --git a/Documentation/config.txt b/Documentation/config.txt index 538ebb5e2e..0f8579331e 100644 --- a/Documentation/config.txt +++ b/Documentation/config.txt @@ -374,6 +374,15 @@ core.warnAmbiguousRefs:: If true, git will warn you if the ref name you passed it is ambiguous and might match multiple refs in the .git/refs/ tree. True by default. +core.abbrevguard:: + Even though git makes sure that it uses enough hexdigits to show + an abbreviated object name unambiguously, as more objects are + added to the repository over time, a short name that used to be + unique will stop being unique. Git uses this many extra hexdigits + that are more than necessary to make the object name currently + unique, in the hope that its output will stay unique a bit longer. + Defaults to 0. + core.compression:: An integer -1..9, indicating a default compression level. -1 is the zlib default. 0 means no compression, @@ -554,9 +563,13 @@ core.sparseCheckout:: linkgit:git-read-tree[1] for more information. add.ignore-errors:: +add.ignoreErrors:: Tells 'git add' to continue adding files when some files cannot be added due to indexing errors. Equivalent to the '--ignore-errors' - option of linkgit:git-add[1]. + option of linkgit:git-add[1]. Older versions of git accept only + `add.ignore-errors`, which does not follow the usual naming + convention for configuration variables. Newer versions of git + honor `add.ignoreErrors` as well. alias.*:: Command aliases for the linkgit:git[1] command wrapper - e.g. @@ -601,8 +614,9 @@ branch.autosetupmerge:: this behavior can be chosen per-branch using the `--track` and `--no-track` options. The valid settings are: `false` -- no automatic setup is done; `true` -- automatic setup is done when the - starting point is a remote branch; `always` -- automatic setup is - done when the starting point is either a local branch or remote + starting point is a remote-tracking branch; `always` -- + automatic setup is done when the starting point is either a + local branch or remote-tracking branch. This option defaults to true. branch.autosetuprebase:: @@ -613,7 +627,7 @@ branch.autosetuprebase:: When `local`, rebase is set to true for tracked branches of other local branches. When `remote`, rebase is set to true for tracked branches of - remote branches. + remote-tracking branches. When `always`, rebase will be set to true for all tracking branches. See "branch.autosetupmerge" for details on how to set up a @@ -680,7 +694,7 @@ color.branch:: color.branch.<slot>:: Use customized color for branch coloration. `<slot>` is one of `current` (the current branch), `local` (a local branch), - `remote` (a tracking branch in refs/remotes/), `plain` (other + `remote` (a remote-tracking branch in refs/remotes/), `plain` (other refs). + The value for these configuration variables is a list of colors (at most @@ -708,7 +722,7 @@ color.diff.<slot>:: color.decorate.<slot>:: Use customized color for 'git log --decorate' output. `<slot>` is one of `branch`, `remoteBranch`, `tag`, `stash` or `HEAD` for local - branches, remote tracking branches, tags, stash and HEAD, respectively. + branches, remote-tracking branches, tags, stash and HEAD, respectively. color.grep:: When set to `always`, always highlight matches. When `false` (or @@ -1102,7 +1116,7 @@ gui.newbranchtemplate:: linkgit:git-gui[1]. gui.pruneduringfetch:: - "true" if linkgit:git-gui[1] should prune tracking branches when + "true" if linkgit:git-gui[1] should prune remote-tracking branches when performing a fetch. The default value is "false". gui.trustmtime:: @@ -1531,11 +1545,13 @@ pack.packSizeLimit:: supported. pager.<cmd>:: - Allows turning on or off pagination of the output of a - particular git subcommand when writing to a tty. If - `\--paginate` or `\--no-pager` is specified on the command line, - it takes precedence over this option. To disable pagination for - all commands, set `core.pager` or `GIT_PAGER` to `cat`. + If the value is boolean, turns on or off pagination of the + output of a particular git subcommand when writing to a tty. + Otherwise, turns on pagination for the subcommand using the + pager specified by the value of `pager.<cmd>`. If `\--paginate` + or `\--no-pager` is specified on the command line, it takes + precedence over this option. To disable pagination for all + commands, set `core.pager` or `GIT_PAGER` to `cat`. pretty.<name>:: Alias for a --pretty= format string, as specified in diff --git a/Documentation/everyday.txt b/Documentation/everyday.txt index e0ba8cc075..ae413e52a5 100644 --- a/Documentation/everyday.txt +++ b/Documentation/everyday.txt @@ -180,12 +180,12 @@ directory; clone from it to start a repository on the satellite machine. <2> clone sets these configuration variables by default. It arranges `git pull` to fetch and store the branches of mothership -machine to local `remotes/origin/*` tracking branches. +machine to local `remotes/origin/*` remote-tracking branches. <3> arrange `git push` to push local `master` branch to `remotes/satellite/master` branch of the mothership machine. <4> push will stash our work away on `remotes/satellite/master` -tracking branch on the mothership machine. You could use this as -a back-up method. +remote-tracking branch on the mothership machine. You could use this +as a back-up method. <5> on mothership machine, merge the work done on the satellite machine into the master branch. diff --git a/Documentation/fetch-options.txt b/Documentation/fetch-options.txt index 5ce1e72745..678675ccdf 100644 --- a/Documentation/fetch-options.txt +++ b/Documentation/fetch-options.txt @@ -36,7 +36,7 @@ ifndef::git-pull[] -p:: --prune:: - After fetching, remove any remote tracking branches which + After fetching, remove any remote-tracking branches which no longer exist on the remote. endif::git-pull[] diff --git a/Documentation/git-add.txt b/Documentation/git-add.txt index 73378b2bef..54aaaeb41b 100644 --- a/Documentation/git-add.txt +++ b/Documentation/git-add.txt @@ -92,9 +92,11 @@ See ``Interactive mode'' for details. edit it. After the editor was closed, adjust the hunk headers and apply the patch to the index. + -*NOTE*: Obviously, if you change anything else than the first character -on lines beginning with a space or a minus, the patch will no longer -apply. +The intent of this option is to pick and choose lines of the patch to +apply, or even to modify the contents of lines to be staged. This can be +quicker and more flexible than using the interactive hunk selector. +However, it is easy to confuse oneself and create a patch that does not +apply to the index. See EDITING PATCHES below. -u:: --update:: @@ -295,6 +297,78 @@ diff:: This lets you review what will be committed (i.e. between HEAD and index). + +EDITING PATCHES +--------------- + +Invoking `git add -e` or selecting `e` from the interactive hunk +selector will open a patch in your editor; after the editor exits, the +result is applied to the index. You are free to make arbitrary changes +to the patch, but note that some changes may have confusing results, or +even result in a patch that cannot be applied. If you want to abort the +operation entirely (i.e., stage nothing new in the index), simply delete +all lines of the patch. The list below describes some common things you +may see in a patch, and which editing operations make sense on them. + +-- +added content:: + +Added content is represented by lines beginning with "{plus}". You can +prevent staging any addition lines by deleting them. + +removed content:: + +Removed content is represented by lines beginning with "-". You can +prevent staging their removal by converting the "-" to a " " (space). + +modified content:: + +Modified content is represented by "-" lines (removing the old content) +followed by "{plus}" lines (adding the replacement content). You can +prevent staging the modification by converting "-" lines to " ", and +removing "{plus}" lines. Beware that modifying only half of the pair is +likely to introduce confusing changes to the index. +-- + +There are also more complex operations that can be performed. But beware +that because the patch is applied only to the index and not the working +tree, the working tree will appear to "undo" the change in the index. +For example, introducing a a new line into the index that is in neither +the HEAD nor the working tree will stage the new line for commit, but +the line will appear to be reverted in the working tree. + +Avoid using these constructs, or do so with extreme caution. + +-- +removing untouched content:: + +Content which does not differ between the index and working tree may be +shown on context lines, beginning with a " " (space). You can stage +context lines for removal by converting the space to a "-". The +resulting working tree file will appear to re-add the content. + +modifying existing content:: + +One can also modify context lines by staging them for removal (by +converting " " to "-") and adding a "{plus}" line with the new content. +Similarly, one can modify "{plus}" lines for existing additions or +modifications. In all cases, the new modification will appear reverted +in the working tree. + +new content:: + +You may also add new content that does not exist in the patch; simply +add new lines, each starting with "{plus}". The addition will appear +reverted in the working tree. +-- + +There are also several operations which should be avoided entirely, as +they will make the patch impossible to apply: + +* adding context (" ") or removal ("-") lines +* deleting context or removal lines +* modifying the contents of context or removal lines + SEE ALSO -------- linkgit:git-status[1] diff --git a/Documentation/git-blame.txt b/Documentation/git-blame.txt index a27f43950f..c71671b4f9 100644 --- a/Documentation/git-blame.txt +++ b/Documentation/git-blame.txt @@ -8,7 +8,7 @@ git-blame - Show what revision and author last modified each line of a file SYNOPSIS -------- [verse] -'git blame' [-c] [-b] [-l] [--root] [-t] [-f] [-n] [-s] [-p] [-w] [--incremental] [-L n,m] +'git blame' [-c] [-b] [-l] [--root] [-t] [-f] [-n] [-s] [-e] [-p] [-w] [--incremental] [-L n,m] [-S <revs-file>] [-M] [-C] [-C] [-C] [--since=<date>] [<rev> | --contents <file> | --reverse <rev>] [--] <file> @@ -65,6 +65,10 @@ include::blame-options.txt[] -s:: Suppress the author name and timestamp from the output. +-e:: +--show-email:: + Show the author email instead of author name (Default: off). + -w:: Ignore whitespace when comparing the parent's version and the child's to find where the lines came from. diff --git a/Documentation/git-branch.txt b/Documentation/git-branch.txt index 1940256930..9106d38e40 100644 --- a/Documentation/git-branch.txt +++ b/Documentation/git-branch.txt @@ -37,11 +37,12 @@ Note that this will create the new branch, but it will not switch the working tree to it; use "git checkout <newbranch>" to switch to the new branch. -When a local branch is started off a remote branch, git sets up the +When a local branch is started off a remote-tracking branch, git sets up the branch so that 'git pull' will appropriately merge from -the remote branch. This behavior may be changed via the global +the remote-tracking branch. This behavior may be changed via the global `branch.autosetupmerge` configuration flag. That setting can be -overridden by using the `--track` and `--no-track` options. +overridden by using the `--track` and `--no-track` options, and +changed later using `git branch --set-upstream`. With a '-m' or '-M' option, <oldbranch> will be renamed to <newbranch>. If <oldbranch> had a corresponding reflog, it is renamed to match @@ -89,7 +90,8 @@ OPTIONS Move/rename a branch even if the new branch name already exists. --color[=<when>]:: - Color branches to highlight current, local, and remote branches. + Color branches to highlight current, local, and + remote-tracking branches. The value must be always (the default), never, or auto. --no-color:: @@ -125,11 +127,11 @@ OPTIONS it directs `git pull` without arguments to pull from the upstream when the new branch is checked out. + -This behavior is the default when the start point is a remote branch. +This behavior is the default when the start point is a remote-tracking branch. Set the branch.autosetupmerge configuration variable to `false` if you want `git checkout` and `git branch` to always behave as if '--no-track' were given. Set it to `always` if you want this behavior when the -start-point is either a local or remote branch. +start-point is either a local or remote-tracking branch. --no-track:: Do not set up "upstream" configuration, even if the diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt index 22d36114df..880763d391 100644 --- a/Documentation/git-checkout.txt +++ b/Documentation/git-checkout.txt @@ -98,7 +98,7 @@ entries; instead, unmerged entries are ignored. "--track" in linkgit:git-branch[1] for details. + If no '-b' option is given, the name of the new branch will be -derived from the remote branch. If "remotes/" or "refs/remotes/" +derived from the remote-tracking branch. If "remotes/" or "refs/remotes/" is prefixed it is stripped away, and then the part up to the next slash (which would be the nickname of the remote) is removed. This would tell us to use "hack" as the local branch when branching diff --git a/Documentation/git-cherry-pick.txt b/Documentation/git-cherry-pick.txt index 3c96fa8c86..73008705eb 100644 --- a/Documentation/git-cherry-pick.txt +++ b/Documentation/git-cherry-pick.txt @@ -92,7 +92,7 @@ git cherry-pick ^HEAD master:: Apply the changes introduced by all commits that are ancestors of master but not of HEAD to produce new commits. -git cherry-pick master\~4 master~2:: +git cherry-pick master{tilde}4 master{tilde}2:: Apply the changes introduced by the fifth and third last commits pointed to by master and create 2 new commits with diff --git a/Documentation/git-clone.txt b/Documentation/git-clone.txt index ab7293351d..42e7021215 100644 --- a/Documentation/git-clone.txt +++ b/Documentation/git-clone.txt @@ -12,7 +12,8 @@ SYNOPSIS 'git clone' [--template=<template_directory>] [-l] [-s] [--no-hardlinks] [-q] [-n] [--bare] [--mirror] [-o <name>] [-b <name>] [-u <upload-pack>] [--reference <repository>] - [--depth <depth>] [--recursive] [--] <repository> [<directory>] + [--depth <depth>] [--recursive|--recurse-submodules] [--] <repository> + [<directory>] DESCRIPTION ----------- @@ -131,7 +132,7 @@ objects from the source repository into a pack in the cloned repository. Set up a mirror of the source repository. This implies `--bare`. Compared to `--bare`, `--mirror` not only maps local branches of the source to local branches of the target, it maps all refs (including - remote branches, notes etc.) and sets up a refspec configuration such + remote-tracking branches, notes etc.) and sets up a refspec configuration such that all these refs are overwritten by a `git remote update` in the target repository. @@ -167,6 +168,7 @@ objects from the source repository into a pack in the cloned repository. as patches. --recursive:: +--recurse-submodules:: After the clone is created, initialize all submodules within, using their default settings. This is equivalent to running `git submodule update --init --recursive` immediately after diff --git a/Documentation/git-commit.txt b/Documentation/git-commit.txt index 42fb1f57b2..b586c0f442 100644 --- a/Documentation/git-commit.txt +++ b/Documentation/git-commit.txt @@ -9,10 +9,10 @@ SYNOPSIS -------- [verse] 'git commit' [-a | --interactive] [-s] [-v] [-u<mode>] [--amend] [--dry-run] - [(-c | -C) <commit>] [-F <file> | -m <msg>] [--reset-author] - [--allow-empty] [--allow-empty-message] [--no-verify] [-e] [--author=<author>] - [--date=<date>] [--cleanup=<mode>] [--status | --no-status] [--] - [[-i | -o ]<file>...] + [(-c | -C | --fixup | --squash) <commit>] [-F <file> | -m <msg>] + [--reset-author] [--allow-empty] [--allow-empty-message] [--no-verify] + [-e] [--author=<author>] [--date=<date>] [--cleanup=<mode>] + [--status | --no-status] [-i | -o] [--] [<file>...] DESCRIPTION ----------- @@ -70,6 +70,19 @@ OPTIONS Like '-C', but with '-c' the editor is invoked, so that the user can further edit the commit message. +--fixup=<commit>:: + Construct a commit message for use with `rebase --autosquash`. + The commit message will be the subject line from the specified + commit with a prefix of "fixup! ". See linkgit:git-rebase[1] + for details. + +--squash=<commit>:: + Construct a commit message for use with `rebase --autosquash`. + The commit message subject line is taken from the specified + commit with a prefix of "squash! ". Can be used with additional + commit message options (`-m`/`-c`/`-C`/`-F`). See + linkgit:git-rebase[1] for details. + --reset-author:: When used with -C/-c/--amend options, declare that the authorship of the resulting commit now belongs of the committer. diff --git a/Documentation/git-daemon.txt b/Documentation/git-daemon.txt index 5054f790a1..d15cb6a845 100644 --- a/Documentation/git-daemon.txt +++ b/Documentation/git-daemon.txt @@ -78,7 +78,8 @@ OPTIONS --inetd:: Have the server run as an inetd service. Implies --syslog. - Incompatible with --port, --listen, --user and --group options. + Incompatible with --detach, --port, --listen, --user and --group + options. --listen=<host_or_ipaddr>:: Listen on a specific IP address or hostname. IP addresses can diff --git a/Documentation/git-describe.txt b/Documentation/git-describe.txt index 7ef9d51577..02e015ad9c 100644 --- a/Documentation/git-describe.txt +++ b/Documentation/git-describe.txt @@ -37,7 +37,7 @@ OPTIONS --all:: Instead of using only the annotated tags, use any ref found in `.git/refs/`. This option enables matching - any known branch, remote branch, or lightweight tag. + any known branch, remote-tracking branch, or lightweight tag. --tags:: Instead of using only the annotated tags, use any tag diff --git a/Documentation/git-diff.txt b/Documentation/git-diff.txt index dd1fb32786..f6ac847507 100644 --- a/Documentation/git-diff.txt +++ b/Documentation/git-diff.txt @@ -8,12 +8,17 @@ git-diff - Show changes between commits, commit and working tree, etc SYNOPSIS -------- -'git diff' [<common diff options>] <commit>{0,2} [--] [<path>...] +[verse] +'git diff' [options] [<commit>] [--] [<path>...] +'git diff' [options] --cached [<commit>] [--] [<path>...] +'git diff' [options] <commit> <commit> [--] [<path>...] +'git diff' [options] [--no-index] [--] <path> <path> DESCRIPTION ----------- -Show changes between two trees, a tree and the working tree, a -tree and the index file, or the index file and the working tree. +Show changes between the working tree and the index or a tree, changes +between the index and a tree, changes between two trees, or changes +between two files on disk. 'git diff' [--options] [--] [<path>...]:: diff --git a/Documentation/git-difftool.txt b/Documentation/git-difftool.txt index 8250bad2ce..6fffbc7bf8 100644 --- a/Documentation/git-difftool.txt +++ b/Documentation/git-difftool.txt @@ -7,13 +7,14 @@ git-difftool - Show changes using common diff tools SYNOPSIS -------- -'git difftool' [<options>] <commit>{0,2} [--] [<path>...] +'git difftool' [<options>] [<commit> [<commit>]] [--] [<path>...] DESCRIPTION ----------- 'git difftool' is a git command that allows you to compare and edit files between revisions using common diff tools. 'git difftool' is a frontend -to 'git diff' and accepts the same options and arguments. +to 'git diff' and accepts the same options and arguments. See +linkgit:git-diff[1]. OPTIONS ------- diff --git a/Documentation/git-fetch.txt b/Documentation/git-fetch.txt index d159e88292..c76e313923 100644 --- a/Documentation/git-fetch.txt +++ b/Documentation/git-fetch.txt @@ -26,7 +26,7 @@ The ref names and their object names of fetched refs are stored in `.git/FETCH_HEAD`. This information is left for a later merge operation done by 'git merge'. -When <refspec> stores the fetched result in tracking branches, +When <refspec> stores the fetched result in remote-tracking branches, the tags that point at these branches are automatically followed. This is done by first fetching from the remote using the given <refspec>s, and if the repository has objects that are diff --git a/Documentation/git-gc.txt b/Documentation/git-gc.txt index 315f07ef1c..801aede609 100644 --- a/Documentation/git-gc.txt +++ b/Documentation/git-gc.txt @@ -89,7 +89,7 @@ are not part of the current project most users will want to expire them sooner. This option defaults to '30 days'. The above two configuration variables can be given to a pattern. For -example, this sets non-default expiry values only to remote tracking +example, this sets non-default expiry values only to remote-tracking branches: ------------ @@ -128,8 +128,8 @@ Notes 'git gc' tries very hard to be safe about the garbage it collects. In particular, it will keep not only objects referenced by your current set -of branches and tags, but also objects referenced by the index, remote -tracking branches, refs saved by 'git filter-branch' in +of branches and tags, but also objects referenced by the index, +remote-tracking branches, refs saved by 'git filter-branch' in refs/original/, or reflogs (which may reference commits in branches that were later amended or rewound). diff --git a/Documentation/git-log.txt b/Documentation/git-log.txt index 6d40f0011b..ff41784c60 100644 --- a/Documentation/git-log.txt +++ b/Documentation/git-log.txt @@ -116,7 +116,7 @@ git log --follow builtin-rev-list.c:: git log --branches --not --remotes=origin:: Shows all commits that are in any of local branches but not in - any of remote tracking branches for 'origin' (what you have that + any of remote-tracking branches for 'origin' (what you have that origin doesn't). git log master --not --remotes=*/master:: diff --git a/Documentation/git-merge.txt b/Documentation/git-merge.txt index d43416d299..c1efaaa5c5 100644 --- a/Documentation/git-merge.txt +++ b/Documentation/git-merge.txt @@ -13,6 +13,7 @@ SYNOPSIS [-s <strategy>] [-X <strategy-option>] [--[no-]rerere-autoupdate] [-m <msg>] <commit>... 'git merge' <msg> HEAD <commit>... +'git merge' --abort DESCRIPTION ----------- @@ -47,6 +48,14 @@ The second syntax (<msg> `HEAD` <commit>...) is supported for historical reasons. Do not use it from the command line or in new scripts. It is the same as `git merge -m <msg> <commit>...`. +The third syntax ("`git merge --abort`") can only be run after the +merge has resulted in conflicts. 'git merge --abort' will abort the +merge process and try to reconstruct the pre-merge state. However, +if there were uncommitted changes when the merge started (and +especially if those changes were further modified after the merge +was started), 'git merge --abort' will in some cases be unable to +reconstruct the original (pre-merge) changes. Therefore: + *Warning*: Running 'git merge' with uncommitted changes is discouraged: while possible, it leaves you in a state that is hard to back out of in the case of a conflict. @@ -72,6 +81,18 @@ invocations. Allow the rerere mechanism to update the index with the result of auto-conflict resolution if possible. +--abort:: + Abort the current conflict resolution process, and + try to reconstruct the pre-merge state. ++ +If there were uncommitted worktree changes present when the merge +started, 'git merge --abort' will in some cases be unable to +reconstruct these changes. It is therefore recommended to always +commit or stash your changes before running 'git merge'. ++ +'git merge --abort' is equivalent to 'git reset --merge' when +`MERGE_HEAD` is present. + <commit>...:: Commits, usually other branch heads, to merge into our branch. You need at least one <commit>. Specifying more than one @@ -142,7 +163,7 @@ happens: i.e. matching `HEAD`. If you tried a merge which resulted in complex conflicts and -want to start over, you can recover with `git reset --merge`. +want to start over, you can recover with `git merge --abort`. HOW CONFLICTS ARE PRESENTED --------------------------- @@ -213,8 +234,8 @@ After seeing a conflict, you can do two things: * Decide not to merge. The only clean-ups you need are to reset the index file to the `HEAD` commit to reverse 2. and to clean - up working tree changes made by 2. and 3.; `git-reset --hard` can - be used for this. + up working tree changes made by 2. and 3.; `git merge --abort` + can be used for this. * Resolve the conflicts. Git will mark the conflicts in the working tree. Edit the files into shape and diff --git a/Documentation/git-notes.txt b/Documentation/git-notes.txt index 2981d8c5ef..296f314eae 100644 --- a/Documentation/git-notes.txt +++ b/Documentation/git-notes.txt @@ -14,8 +14,12 @@ SYNOPSIS 'git notes' append [-F <file> | -m <msg> | (-c | -C) <object>] [<object>] 'git notes' edit [<object>] 'git notes' show [<object>] +'git notes' merge [-v | -q] [-s <strategy> ] <notes_ref> +'git notes' merge --commit [-v | -q] +'git notes' merge --abort [-v | -q] 'git notes' remove [<object>] 'git notes' prune [-n | -v] +'git notes' get-ref DESCRIPTION @@ -83,6 +87,21 @@ edit:: show:: Show the notes for a given object (defaults to HEAD). +merge:: + Merge the given notes ref into the current notes ref. + This will try to merge the changes made by the given + notes ref (called "remote") since the merge-base (if + any) into the current notes ref (called "local"). ++ +If conflicts arise and a strategy for automatically resolving +conflicting notes (see the -s/--strategy option) is not given, +the "manual" resolver is used. This resolver checks out the +conflicting notes in a special worktree (`.git/NOTES_MERGE_WORKTREE`), +and instructs the user to manually resolve the conflicts there. +When done, the user can either finalize the merge with +'git notes merge --commit', or abort the merge with +'git notes merge --abort'. + remove:: Remove the notes for a given object (defaults to HEAD). This is equivalent to specifying an empty note message to @@ -91,6 +110,10 @@ remove:: prune:: Remove all notes for non-existing/unreachable objects. +get-ref:: + Print the current notes ref. This provides an easy way to + retrieve the current notes ref (e.g. from scripts). + OPTIONS ------- -f:: @@ -133,9 +156,37 @@ OPTIONS Do not remove anything; just report the object names whose notes would be removed. +-s <strategy>:: +--strategy=<strategy>:: + When merging notes, resolve notes conflicts using the given + strategy. The following strategies are recognized: "manual" + (default), "ours", "theirs", "union" and "cat_sort_uniq". + See the "NOTES MERGE STRATEGIES" section below for more + information on each notes merge strategy. + +--commit:: + Finalize an in-progress 'git notes merge'. Use this option + when you have resolved the conflicts that 'git notes merge' + stored in .git/NOTES_MERGE_WORKTREE. This amends the partial + merge commit created by 'git notes merge' (stored in + .git/NOTES_MERGE_PARTIAL) by adding the notes in + .git/NOTES_MERGE_WORKTREE. The notes ref stored in the + .git/NOTES_MERGE_REF symref is updated to the resulting commit. + +--abort:: + Abort/reset a in-progress 'git notes merge', i.e. a notes merge + with conflicts. This simply removes all files related to the + notes merge. + +-q:: +--quiet:: + When merging notes, operate quietly. + -v:: --verbose:: - Report all object names whose notes are removed. + When merging notes, be more verbose. + When pruning notes, report all object names whose notes are + removed. DISCUSSION @@ -163,6 +214,38 @@ object, in which case the history of the notes can be read with `git log -p -g <refname>`. +NOTES MERGE STRATEGIES +---------------------- + +The default notes merge strategy is "manual", which checks out +conflicting notes in a special work tree for resolving notes conflicts +(`.git/NOTES_MERGE_WORKTREE`), and instructs the user to resolve the +conflicts in that work tree. +When done, the user can either finalize the merge with +'git notes merge --commit', or abort the merge with +'git notes merge --abort'. + +"ours" automatically resolves conflicting notes in favor of the local +version (i.e. the current notes ref). + +"theirs" automatically resolves notes conflicts in favor of the remote +version (i.e. the given notes ref being merged into the current notes +ref). + +"union" automatically resolves notes conflicts by concatenating the +local and remote versions. + +"cat_sort_uniq" is similar to "union", but in addition to concatenating +the local and remote versions, this strategy also sorts the resulting +lines, and removes duplicate lines from the result. This is equivalent +to applying the "cat | sort | uniq" shell pipeline to the local and +remote versions. This strategy is useful if the notes follow a line-based +format where one wants to avoid duplicated lines in the merge result. +Note that if either the local or remote version contain duplicate lines +prior to the merge, these will also be removed by this notes merge +strategy. + + EXAMPLES -------- diff --git a/Documentation/git-pull.txt b/Documentation/git-pull.txt index e1b0bd2868..4db73737b0 100644 --- a/Documentation/git-pull.txt +++ b/Documentation/git-pull.txt @@ -26,9 +26,9 @@ With `--rebase`, it runs 'git rebase' instead of 'git merge'. <repository> should be the name of a remote repository as passed to linkgit:git-fetch[1]. <refspec> can name an arbitrary remote ref (for example, the name of a tag) or even -a collection of refs with corresponding remote tracking branches -(e.g., refs/heads/*:refs/remotes/origin/*), but usually it is -the name of a branch in the remote repository. +a collection of refs with corresponding remote-tracking branches +(e.g., refs/heads/{asterisk}:refs/remotes/origin/{asterisk}), +but usually it is the name of a branch in the remote repository. Default values for <repository> and <branch> are read from the "remote" and "merge" configuration for the current branch @@ -136,7 +136,7 @@ and if there is not any such variable, the value on `URL: ` line in `$GIT_DIR/remotes/<origin>` file is used. In order to determine what remote branches to fetch (and -optionally store in the tracking branches) when the command is +optionally store in the remote-tracking branches) when the command is run without any refspec parameters on the command line, values of the configuration variable `remote.<origin>.fetch` are consulted, and if there aren't any, `$GIT_DIR/remotes/<origin>` @@ -149,9 +149,9 @@ refs/heads/*:refs/remotes/origin/* ------------ A globbing refspec must have a non-empty RHS (i.e. must store -what were fetched in tracking branches), and its LHS and RHS +what were fetched in remote-tracking branches), and its LHS and RHS must end with `/*`. The above specifies that all remote -branches are tracked using tracking branches in +branches are tracked using remote-tracking branches in `refs/remotes/origin/` hierarchy under the same name. The rule to determine which remote branch to merge after diff --git a/Documentation/git-remote-ext.txt b/Documentation/git-remote-ext.txt new file mode 100644 index 0000000000..f4fbf67209 --- /dev/null +++ b/Documentation/git-remote-ext.txt @@ -0,0 +1,125 @@ +git-remote-ext(1) +================= + +NAME +---- +git-remote-ext - Bridge smart transport to external command. + +SYNOPSIS +-------- +git remote add nick "ext::<command>[ <arguments>...]" + +DESCRIPTION +----------- +This remote helper uses the specified 'program' to connect +to a remote git server. + +Data written to stdin of this specified 'program' is assumed +to be sent to git:// server, git-upload-pack, git-receive-pack +or git-upload-archive (depending on situation), and data read +from stdout of this program is assumed to be received from +the same service. + +Command and arguments are separated by unescaped space. + +The following sequences have a special meaning: + +'% ':: + Literal space in command or argument. + +'%%':: + Literal percent sign. + +'%s':: + Replaced with name (receive-pack, upload-pack, or + upload-archive) of the service git wants to invoke. + +'%S':: + Replaced with long name (git-receive-pack, + git-upload-pack, or git-upload-archive) of the service + git wants to invoke. + +'%G' (must be first characters in argument):: + This argument will not be passed to 'program'. Instead, it + will cause helper to start by sending git:// service request to + remote side with service field set to approiate value and + repository field set to rest of the argument. Default is not to send + such request. ++ +This is useful if remote side is git:// server accessed over +some tunnel. + +'%V' (must be first characters in argument):: + This argument will not be passed to 'program'. Instead it sets + the vhost field in git:// service request (to rest of the argument). + Default is not to send vhost in such request (if sent). + +ENVIRONMENT VARIABLES: +---------------------- + +GIT_TRANSLOOP_DEBUG:: + If set, prints debugging information about various reads/writes. + +ENVIRONMENT VARIABLES PASSED TO COMMAND: +---------------------------------------- + +GIT_EXT_SERVICE:: + Set to long name (git-upload-pack, etc...) of service helper needs + to invoke. + +GIT_EXT_SERVICE_NOPREFIX:: + Set to long name (upload-pack, etc...) of service helper needs + to invoke. + + +EXAMPLES: +--------- +This remote helper is transparently used by git when +you use commands such as "git fetch <URL>", "git clone <URL>", +, "git push <URL>" or "git remote add nick <URL>", where <URL> +begins with `ext::`. Examples: + +"ext::ssh -i /home/foo/.ssh/somekey user@host.example %S 'foo/repo'":: + Like host.example:foo/repo, but use /home/foo/.ssh/somekey as + keypair and user as user on remote side. This avoids needing to + edit .ssh/config. + +"ext::socat -t3600 - ABSTRACT-CONNECT:/git-server %G/somerepo":: + Represents repository with path /somerepo accessable over + git protocol at abstract namespace address /git-server. + +"ext::git-server-alias foo %G/repo":: + Represents a repository with path /repo accessed using the + helper program "git-server-alias foo". The path to the + repository and type of request are not passed on the command + line but as part of the protocol stream, as usual with git:// + protocol. + +"ext::git-server-alias foo %G/repo %Vfoo":: + Represents a repository with path /repo accessed using the + helper program "git-server-alias foo". The hostname for the + remote server passed in the protocol stream will be "foo" + (this allows multiple virtual git servers to share a + link-level address). + +"ext::git-server-alias foo %G/repo% with% spaces %Vfoo":: + Represents a repository with path '/repo with spaces' accessed + using the helper program "git-server-alias foo". The hostname for + the remote server passed in the protocol stream will be "foo" + (this allows multiple virtual git servers to share a + link-level address). + +"ext::git-ssl foo.example /bar":: + Represents a repository accessed using the helper program + "git-ssl foo.example /bar". The type of request can be + determined by the helper using environment variables (see + above). + +Documentation +-------------- +Documentation by Ilari Liusvaara, Jonathan Nieder and the git list +<git@vger.kernel.org> + +GIT +--- +Part of the linkgit:git[1] suite diff --git a/Documentation/git-remote-fd.txt b/Documentation/git-remote-fd.txt new file mode 100644 index 0000000000..abc49441be --- /dev/null +++ b/Documentation/git-remote-fd.txt @@ -0,0 +1,59 @@ +git-remote-fd(1) +================ + +NAME +---- +git-remote-fd - Reflect smart transport stream back to caller + +SYNOPSIS +-------- +"fd::<infd>[,<outfd>][/<anything>]" (as URL) + +DESCRIPTION +----------- +This helper uses specified file descriptors to connect to remote git server. +This is not meant for end users but for programs and scripts calling git +fetch, push or archive. + +If only <infd> is given, it is assumed to be bidirectional socket connected +to remote git server (git-upload-pack, git-receive-pack or +git-upload-achive). If both <infd> and <outfd> are given, they are assumed +to be pipes connected to remote git server (<infd> being the inbound pipe +and <outfd> being the outbound pipe. + +It is assumed that any handshaking procedures have already been completed +(such as sending service request for git://) before this helper is started. + +<anything> can be any string. It is ignored. It is meant for provoding +information to user in the URL in case that URL is displayed in some +context. + +ENVIRONMENT VARIABLES +--------------------- +GIT_TRANSLOOP_DEBUG:: + If set, prints debugging information about various reads/writes. + +EXAMPLES +-------- +git fetch fd::17 master:: + Fetch master, using file descriptor #17 to communicate with + git-upload-pack. + +git fetch fd::17/foo master:: + Same as above. + +git push fd::7,8 master (as URL):: + Push master, using file descriptor #7 to read data from + git-receive-pack and file descriptor #8 to write data to + same service. + +git push fd::7,8/bar master:: + Same as above. + +Documentation +-------------- +Documentation by Ilari Liusvaara and the git list <git@vger.kernel.org> + +GIT +--- +Part of the linkgit:git[1] suite diff --git a/Documentation/git-remote.txt b/Documentation/git-remote.txt index 0d28febe1b..c258ea48db 100644 --- a/Documentation/git-remote.txt +++ b/Documentation/git-remote.txt @@ -75,7 +75,7 @@ was passed. 'rename':: -Rename the remote named <old> to <new>. All remote tracking branches and +Rename the remote named <old> to <new>. All remote-tracking branches and configuration settings for the remote are updated. + In case <old> and <new> are the same, and <old> is a file under @@ -84,7 +84,7 @@ the configuration file format. 'rm':: -Remove the remote named <name>. All remote tracking branches and +Remove the remote named <name>. All remote-tracking branches and configuration settings for the remote are removed. 'set-head':: @@ -146,7 +146,7 @@ With `-n` option, the remote heads are not queried first with 'prune':: -Deletes all stale tracking branches under <name>. +Deletes all stale remote-tracking branches under <name>. These stale branches have already been removed from the remote repository referenced by <name>, but are still locally available in "remotes/<name>". diff --git a/Documentation/git-revert.txt b/Documentation/git-revert.txt index f40984d144..752fc88e76 100644 --- a/Documentation/git-revert.txt +++ b/Documentation/git-revert.txt @@ -87,7 +87,7 @@ git revert HEAD~3:: Revert the changes specified by the fourth last commit in HEAD and create a new commit with the reverted changes. -git revert -n master\~5..master~2:: +git revert -n master{tilde}5..master{tilde}2:: Revert the changes done by commits from the fifth last commit in master (included) to the third last commit in master diff --git a/Documentation/git-rm.txt b/Documentation/git-rm.txt index 71e3d9fc23..0adbe8b1f8 100644 --- a/Documentation/git-rm.txt +++ b/Documentation/git-rm.txt @@ -89,8 +89,8 @@ the paths that have disappeared from the filesystem. However, depending on the use case, there are several ways that can be done. -Using "git commit -a" -~~~~~~~~~~~~~~~~~~~~~ +Using ``git commit -a'' +~~~~~~~~~~~~~~~~~~~~~~~ If you intend that your next commit should record all modifications of tracked files in the working tree and record all removals of files that have been removed from the working tree with `rm` @@ -98,8 +98,8 @@ files that have been removed from the working tree with `rm` automatically notice and record all removals. You can also have a similar effect without committing by using `git add -u`. -Using "git add -A" -~~~~~~~~~~~~~~~~~~ +Using ``git add -A'' +~~~~~~~~~~~~~~~~~~~~ When accepting a new code drop for a vendor branch, you probably want to record both the removal of paths and additions of new paths as well as modifications of existing paths. @@ -111,8 +111,8 @@ tree using this command: git ls-files -z | xargs -0 rm -f ---------------- -and then "untar" the new code in the working tree. Alternately -you could "rsync" the changes into the working tree. +and then untar the new code in the working tree. Alternately +you could 'rsync' the changes into the working tree. After that, the easiest way to record all removals, additions, and modifications in the working tree is: diff --git a/Documentation/git-send-email.txt b/Documentation/git-send-email.txt index 05904e0e7f..7ec9dabe68 100644 --- a/Documentation/git-send-email.txt +++ b/Documentation/git-send-email.txt @@ -82,11 +82,26 @@ See the CONFIGURATION section for 'sendemail.multiedit'. set, as returned by "git var -l". --in-reply-to=<identifier>:: - Specify the contents of the first In-Reply-To header. - Subsequent emails will refer to the previous email - instead of this if --chain-reply-to is set. - Only necessary if --compose is also set. If --compose - is not set, this will be prompted for. + Make the first mail (or all the mails with `--no-thread`) appear as a + reply to the given Message-Id, which avoids breaking threads to + provide a new patch series. + The second and subsequent emails will be sent as replies according to + the `--[no]-chain-reply-to` setting. ++ +So for example when `--thread` and `--no-chain-reply-to` are specified, the +second and subsequent patches will be replies to the first one like in the +illustration below where `[PATCH v2 0/3]` is in reply to `[PATCH 0/2]`: ++ + [PATCH 0/2] Here is what I did... + [PATCH 1/2] Clean up and tests + [PATCH 2/2] Implementation + [PATCH v2 0/3] Here is a reroll + [PATCH v2 1/3] Clean up + [PATCH v2 2/3] New tests + [PATCH v2 3/3] Implementation ++ +Only necessary if --compose is also set. If --compose +is not set, this will be prompted for. --subject=<string>:: Specify the initial subject of the email thread. @@ -307,6 +322,9 @@ have been specified, in which case default to 'compose'. Default is the value of 'sendemail.validate'; if this is not set, default to '--validate'. +--force:: + Send emails even if safety checks would prevent it. + CONFIGURATION ------------- diff --git a/Documentation/git-tag.txt b/Documentation/git-tag.txt index 31c78a81e0..8b169e364a 100644 --- a/Documentation/git-tag.txt +++ b/Documentation/git-tag.txt @@ -177,7 +177,7 @@ On Automatic following ~~~~~~~~~~~~~~~~~~~~~~ If you are following somebody else's tree, you are most likely -using tracking branches (`refs/heads/origin` in traditional +using remote-tracking branches (`refs/heads/origin` in traditional layout, or `refs/remotes/origin/master` in the separate-remote layout). You usually want the tags from the other end. @@ -232,7 +232,7 @@ this case. It may well be that among networking people, they may want to exchange the tags internal to their group, but in that workflow they are most likely tracking with each other's progress by -having tracking branches. Again, the heuristic to automatically +having remote-tracking branches. Again, the heuristic to automatically follow such tags is a good thing. diff --git a/Documentation/git-verify-tag.txt b/Documentation/git-verify-tag.txt index dada21242c..711219749c 100644 --- a/Documentation/git-verify-tag.txt +++ b/Documentation/git-verify-tag.txt @@ -15,6 +15,10 @@ Validates the gpg signature created by 'git tag'. OPTIONS ------- +-v:: +--verbose:: + Print the contents of the tag object before validating it. + <tag>...:: SHA1 identifiers of git tag objects. diff --git a/Documentation/git.txt b/Documentation/git.txt index 0c897df6a7..8216083081 100644 --- a/Documentation/git.txt +++ b/Documentation/git.txt @@ -44,31 +44,35 @@ unreleased) version of git, that is available from 'master' branch of the `git.git` repository. Documentation for older releases are available here: -* link:v1.7.3.2/git.html[documentation for release 1.7.3.2] +* link:v1.7.3.3/git.html[documentation for release 1.7.3.3] * release notes for + link:RelNotes/1.7.3.3.txt[1.7.3.3], link:RelNotes/1.7.3.2.txt[1.7.3.2], link:RelNotes/1.7.3.1.txt[1.7.3.1], link:RelNotes/1.7.3.txt[1.7.3]. -* link:v1.7.2.3/git.html[documentation for release 1.7.2.3] +* link:v1.7.2.4/git.html[documentation for release 1.7.2.4] * release notes for + link:RelNotes/1.7.2.4.txt[1.7.2.4], link:RelNotes/1.7.2.3.txt[1.7.2.3], link:RelNotes/1.7.2.2.txt[1.7.2.2], link:RelNotes/1.7.2.1.txt[1.7.2.1], link:RelNotes/1.7.2.txt[1.7.2]. -* link:v1.7.1.2/git.html[documentation for release 1.7.1.2] +* link:v1.7.1.3/git.html[documentation for release 1.7.1.3] * release notes for + link:RelNotes/1.7.1.3.txt[1.7.1.3], link:RelNotes/1.7.1.2.txt[1.7.1.2], link:RelNotes/1.7.1.1.txt[1.7.1.1], link:RelNotes/1.7.1.txt[1.7.1]. -* link:v1.7.0.7/git.html[documentation for release 1.7.0.7] +* link:v1.7.0.8/git.html[documentation for release 1.7.0.8] * release notes for + link:RelNotes/1.7.0.8.txt[1.7.0.8], link:RelNotes/1.7.0.7.txt[1.7.0.7], link:RelNotes/1.7.0.6.txt[1.7.0.6], link:RelNotes/1.7.0.5.txt[1.7.0.5], diff --git a/Documentation/gitignore.txt b/Documentation/gitignore.txt index 7dc2e8b0bc..8416f3445a 100644 --- a/Documentation/gitignore.txt +++ b/Documentation/gitignore.txt @@ -14,11 +14,8 @@ DESCRIPTION A `gitignore` file specifies intentionally untracked files that git should ignore. -Note that all the `gitignore` files really concern only files -that are not already tracked by git; -in order to ignore uncommitted changes in already tracked files, -please refer to the 'git update-index --assume-unchanged' -documentation. +Files already tracked by git are not affected; see the NOTES +below for details. Each line in a `gitignore` file specifies a pattern. When deciding whether to ignore a path, git normally checks @@ -62,7 +59,8 @@ files specified by command-line options. Higher-level git tools, such as 'git status' and 'git add', use patterns from the sources specified above. -Patterns have the following format: +PATTERN FORMAT +-------------- - A blank line matches no files, so it can serve as a separator for readability. @@ -98,7 +96,20 @@ Patterns have the following format: For example, "/{asterisk}.c" matches "cat-file.c" but not "mozilla-sha1/sha1.c". -An example: +NOTES +----- + +The purpose of gitignore files is to ensure that certain files +not tracked by git remain untracked. + +To ignore uncommitted changes in a file that is already tracked, +use 'git update-index {litdd}assume-unchanged'. + +To stop tracking a file that is currently tracked, use +'git rm --cached'. + +EXAMPLES +-------- -------------------------------------------------------------- $ git status @@ -140,6 +151,11 @@ Another example: The second .gitignore prevents git from ignoring `arch/foo/kernel/vmlinux.lds.S`. +SEE ALSO +-------- +linkgit:git-rm[1], linkgit:git-update-index[1], +linkgit:gitrepository-layout[5] + Documentation ------------- Documentation by David Greaves, Junio C Hamano, Josh Triplett, diff --git a/Documentation/gittutorial-2.txt b/Documentation/gittutorial-2.txt index ecab0c09d0..7fe5848d1f 100644 --- a/Documentation/gittutorial-2.txt +++ b/Documentation/gittutorial-2.txt @@ -373,7 +373,7 @@ $ git status # # new file: closing.txt # -# Changed but not updated: +# Changes not staged for commit: # (use "git add <file>..." to update what will be committed) # # modified: file.txt diff --git a/Documentation/gittutorial.txt b/Documentation/gittutorial.txt index 1c1606696e..0982f74ef6 100644 --- a/Documentation/gittutorial.txt +++ b/Documentation/gittutorial.txt @@ -385,7 +385,7 @@ alice$ git fetch bob Unlike the longhand form, when Alice fetches from Bob using a remote repository shorthand set up with 'git remote', what was -fetched is stored in a remote tracking branch, in this case +fetched is stored in a remote-tracking branch, in this case `bob/master`. So after this: ------------------------------------- @@ -402,8 +402,8 @@ could merge the changes into her master branch: alice$ git merge bob/master ------------------------------------- -This `merge` can also be done by 'pulling from her own remote -tracking branch', like this: +This `merge` can also be done by 'pulling from her own remote-tracking +branch', like this: ------------------------------------- alice$ git pull . remotes/bob/master diff --git a/Documentation/glossary-content.txt b/Documentation/glossary-content.txt index 1f029f8aa0..f04b48ef0d 100644 --- a/Documentation/glossary-content.txt +++ b/Documentation/glossary-content.txt @@ -131,7 +131,7 @@ to point at the new commit. you have. In such these cases, you do not make a new <<def_merge,merge>> <<def_commit,commit>> but instead just update to his revision. This will happen frequently on a - <<def_tracking_branch,tracking branch>> of a remote + <<def_remote_tracking_branch,remote-tracking branch>> of a remote <<def_repository,repository>>. [[def_fetch]]fetch:: @@ -260,7 +260,7 @@ This commit is referred to as a "merge commit", or sometimes just a The default upstream <<def_repository,repository>>. Most projects have at least one upstream project which they track. By default 'origin' is used for that purpose. New upstream updates - will be fetched into remote <<def_tracking_branch,tracking branches>> named + will be fetched into remote <<def_remote_tracking_branch,remote-tracking branches>> named origin/name-of-upstream-branch, which you can see using `git branch -r`. @@ -349,6 +349,14 @@ This commit is referred to as a "merge commit", or sometimes just a master branch head as to-upstream branch at $URL". See also linkgit:git-push[1]. +[[def_remote_tracking_branch]]remote-tracking branch:: + A regular git <<def_branch,branch>> that is used to follow changes from + another <<def_repository,repository>>. A remote-tracking + branch should not contain direct modifications or have local commits + made to it. A remote-tracking branch can usually be + identified as the right-hand-side <<def_ref,ref>> in a Pull: + <<def_refspec,refspec>>. + [[def_repository]]repository:: A collection of <<def_ref,refs>> together with an <<def_object_database,object database>> containing all objects @@ -418,14 +426,6 @@ This commit is referred to as a "merge commit", or sometimes just a that each contain very well defined concepts or small incremental yet related changes. -[[def_tracking_branch]]tracking branch:: - A regular git <<def_branch,branch>> that is used to follow changes from - another <<def_repository,repository>>. A tracking - branch should not contain direct modifications or have local commits - made to it. A tracking branch can usually be - identified as the right-hand-side <<def_ref,ref>> in a Pull: - <<def_refspec,refspec>>. - [[def_tree]]tree:: Either a <<def_working_tree,working tree>>, or a <<def_tree_object,tree object>> together with the dependent <<def_blob_object,blob>> and tree objects diff --git a/Documentation/rev-list-options.txt b/Documentation/rev-list-options.txt index 42ca059908..44a2ef1de1 100644 --- a/Documentation/rev-list-options.txt +++ b/Documentation/rev-list-options.txt @@ -269,7 +269,7 @@ endif::git-rev-list[] Pretend as if all the refs in `refs/remotes` are listed on the command line as '<commit>'. If '<pattern>' is given, limit - remote tracking branches to ones matching given shell glob. + remote-tracking branches to ones matching given shell glob. If pattern lacks '?', '*', or '[', '/*' at the end is implied. --glob=<glob-pattern>:: diff --git a/Documentation/technical/api-parse-options.txt b/Documentation/technical/api-parse-options.txt index c5d141cd63..f6a4a361bd 100644 --- a/Documentation/technical/api-parse-options.txt +++ b/Documentation/technical/api-parse-options.txt @@ -118,13 +118,16 @@ There are some macros to easily define options: `OPT__COLOR(&int_var, description)`:: Add `\--color[=<when>]` and `--no-color`. -`OPT__DRY_RUN(&int_var)`:: +`OPT__DRY_RUN(&int_var, description)`:: Add `-n, \--dry-run`. -`OPT__QUIET(&int_var)`:: +`OPT__FORCE(&int_var, description)`:: + Add `-f, \--force`. + +`OPT__QUIET(&int_var, description)`:: Add `-q, \--quiet`. -`OPT__VERBOSE(&int_var)`:: +`OPT__VERBOSE(&int_var, description)`:: Add `-v, \--verbose`. `OPT_GROUP(description)`:: diff --git a/Documentation/user-manual.txt b/Documentation/user-manual.txt index fc56da677c..f13a846131 100644 --- a/Documentation/user-manual.txt +++ b/Documentation/user-manual.txt @@ -344,7 +344,8 @@ Examining branches from a remote repository The "master" branch that was created at the time you cloned is a copy of the HEAD in the repository that you cloned from. That repository may also have had other branches, though, and your local repository -keeps branches which track each of those remote branches, which you +keeps branches which track each of those remote branches, called +remote-tracking branches, which you can view using the "-r" option to linkgit:git-branch[1]: ------------------------------------------------ @@ -359,13 +360,23 @@ $ git branch -r origin/todo ------------------------------------------------ -You cannot check out these remote-tracking branches, but you can -examine them on a branch of your own, just as you would a tag: +In this example, "origin" is called a remote repository, or "remote" +for short. The branches of this repository are called "remote +branches" from our point of view. The remote-tracking branches listed +above were created based on the remote branches at clone time and will +be updated by "git fetch" (hence "git pull") and "git push". See +<<Updating-a-repository-With-git-fetch>> for details. + +You might want to build on one of these remote-tracking branches +on a branch of your own, just as you would for a tag: ------------------------------------------------ $ git checkout -b my-todo-copy origin/todo ------------------------------------------------ +You can also check out "origin/todo" directly to examine it or +write a one-off patch. See <<detached-head,detached head>>. + Note that the name "origin" is just the name that git uses by default to refer to the repository that you cloned from. @@ -435,7 +446,7 @@ linux-nfs/master origin/master ------------------------------------------------- -If you run "git fetch <remote>" later, the tracking branches for the +If you run "git fetch <remote>" later, the remote-tracking branches for the named <remote> will be updated. If you examine the file .git/config, you will see that git has added @@ -1700,7 +1711,7 @@ may wish to check the original repository for updates and merge them into your own work. We have already seen <<Updating-a-repository-With-git-fetch,how to -keep remote tracking branches up to date>> with linkgit:git-fetch[1], +keep remote-tracking branches up to date>> with linkgit:git-fetch[1], and how to merge two branches. So you can merge in changes from the original repository's master branch with: @@ -1716,15 +1727,21 @@ one step: $ git pull origin master ------------------------------------------------- -In fact, if you have "master" checked out, then by default "git pull" -merges from the HEAD branch of the origin repository. So often you can +In fact, if you have "master" checked out, then this branch has been +configured by "git clone" to get changes from the HEAD branch of the +origin repository. So often you can accomplish the above with just a simple ------------------------------------------------- $ git pull ------------------------------------------------- -More generally, a branch that is created from a remote branch will pull +This command will fetch changes from the remote branches to your +remote-tracking branches `origin/*`, and merge the default branch into +the current branch. + +More generally, a branch that is created from a remote-tracking branch +will pull by default from that branch. See the descriptions of the branch.<name>.remote and branch.<name>.merge options in linkgit:git-config[1], and the discussion of the `--track` option in @@ -2106,7 +2123,7 @@ $ git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git $ cd work ------------------------------------------------- -Linus's tree will be stored in the remote branch named origin/master, +Linus's tree will be stored in the remote-tracking branch named origin/master, and can be updated using linkgit:git-fetch[1]; you can track other public trees using linkgit:git-remote[1] to set up a "remote" and linkgit:git-fetch[1] to keep them up-to-date; see @@ -2800,8 +2817,8 @@ Be aware that commits that the old version of example/master pointed at may be lost, as we saw in the previous section. [[remote-branch-configuration]] -Configuring remote branches ---------------------------- +Configuring remote-tracking branches +------------------------------------ We saw above that "origin" is just a shortcut to refer to the repository that you originally cloned from. This information is @@ -70,6 +70,11 @@ all:: # # Define NO_STRTOK_R if you don't have strtok_r in the C library. # +# Define NO_FNMATCH if you don't have fnmatch in the C library. +# +# Define NO_FNMATCH_CASEFOLD if your fnmatch function doesn't have the +# FNM_CASEFOLD GNU extension. +# # Define NO_LIBGEN_H if you don't have libgen.h. # # Define NEEDS_LIBGEN if your libgen needs -lgen when linking @@ -401,6 +406,7 @@ EXTRA_PROGRAMS = # ... and all the rest that could be moved out of bindir to gitexecdir PROGRAMS += $(EXTRA_PROGRAMS) +PROGRAM_OBJS += daemon.o PROGRAM_OBJS += fast-import.o PROGRAM_OBJS += imap-send.o PROGRAM_OBJS += shell.o @@ -496,6 +502,8 @@ LIB_H += compat/bswap.h LIB_H += compat/cygwin.h LIB_H += compat/mingw.h LIB_H += compat/win32/pthread.h +LIB_H += compat/win32/syslog.h +LIB_H += compat/win32/sys/poll.h LIB_H += csum-file.h LIB_H += decorate.h LIB_H += delta.h @@ -517,6 +525,7 @@ LIB_H += mailmap.h LIB_H += merge-recursive.h LIB_H += notes.h LIB_H += notes-cache.h +LIB_H += notes-merge.h LIB_H += object.h LIB_H += pack.h LIB_H += pack-refs.h @@ -607,6 +616,7 @@ LIB_OBJS += merge-recursive.o LIB_OBJS += name-hash.o LIB_OBJS += notes.o LIB_OBJS += notes-cache.o +LIB_OBJS += notes-merge.o LIB_OBJS += object.o LIB_OBJS += pack-check.o LIB_OBJS += pack-refs.o @@ -662,6 +672,7 @@ LIB_OBJS += write_or_die.o LIB_OBJS += ws.o LIB_OBJS += wt-status.o LIB_OBJS += xdiff-interface.o +LIB_OBJS += zlib.o BUILTIN_OBJS += builtin/add.o BUILTIN_OBJS += builtin/annotate.o @@ -728,6 +739,8 @@ BUILTIN_OBJS += builtin/read-tree.o BUILTIN_OBJS += builtin/receive-pack.o BUILTIN_OBJS += builtin/reflog.o BUILTIN_OBJS += builtin/remote.o +BUILTIN_OBJS += builtin/remote-ext.o +BUILTIN_OBJS += builtin/remote-fd.o BUILTIN_OBJS += builtin/replace.o BUILTIN_OBJS += builtin/rerere.o BUILTIN_OBJS += builtin/reset.o @@ -846,6 +859,7 @@ ifeq ($(uname_S),SunOS) NO_MKDTEMP = YesPlease NO_MKSTEMPS = YesPlease NO_REGEX = YesPlease + NO_FNMATCH_CASEFOLD = YesPlease ifeq ($(uname_R),5.6) SOCKLEN_T = int NO_HSTRERROR = YesPlease @@ -1052,6 +1066,7 @@ ifeq ($(uname_S),Windows) NO_STRCASESTR = YesPlease NO_STRLCPY = YesPlease NO_STRTOK_R = YesPlease + NO_FNMATCH = YesPlease NO_MEMMEM = YesPlease # NEEDS_LIBICONV = YesPlease NO_ICONV = YesPlease @@ -1064,7 +1079,6 @@ ifeq ($(uname_S),Windows) NO_SVN_TESTS = YesPlease NO_PERL_MAKEMAKER = YesPlease RUNTIME_PREFIX = YesPlease - NO_POSIX_ONLY_PROGRAMS = YesPlease NO_ST_BLOCKS_IN_STRUCT_STAT = YesPlease NO_NSEC = YesPlease USE_WIN32_MMAP = YesPlease @@ -1075,14 +1089,15 @@ ifeq ($(uname_S),Windows) NO_CURL = YesPlease NO_PYTHON = YesPlease BLK_SHA1 = YesPlease + NO_POSIX_GOODIES = UnfortunatelyYes NATIVE_CRLF = YesPlease CC = compat/vcbuild/scripts/clink.pl AR = compat/vcbuild/scripts/lib.pl CFLAGS = BASIC_CFLAGS = -nologo -I. -I../zlib -Icompat/vcbuild -Icompat/vcbuild/include -DWIN32 -D_CONSOLE -DHAVE_STRING_H -D_CRT_SECURE_NO_WARNINGS -D_CRT_NONSTDC_NO_DEPRECATE - COMPAT_OBJS = compat/msvc.o compat/fnmatch/fnmatch.o compat/winansi.o compat/win32/pthread.o - COMPAT_CFLAGS = -D__USE_MINGW_ACCESS -DNOGDI -DHAVE_STRING_H -DHAVE_ALLOCA_H -Icompat -Icompat/fnmatch -Icompat/regex -Icompat/fnmatch -Icompat/win32 -DSTRIP_EXTENSION=\".exe\" + COMPAT_OBJS = compat/msvc.o compat/winansi.o compat/win32/pthread.o compat/win32/syslog.o compat/win32/sys/poll.o + COMPAT_CFLAGS = -D__USE_MINGW_ACCESS -DNOGDI -DHAVE_STRING_H -DHAVE_ALLOCA_H -Icompat -Icompat/regex -Icompat/win32 -DSTRIP_EXTENSION=\".exe\" BASIC_LDFLAGS = -IGNORE:4217 -IGNORE:4049 -NOLOGO -SUBSYSTEM:CONSOLE -NODEFAULTLIB:MSVCRT.lib EXTLIBS = advapi32.lib shell32.lib wininet.lib ws2_32.lib PTHREAD_LIBS = @@ -1096,6 +1111,25 @@ else endif X = .exe endif +ifeq ($(uname_S),Interix) + NO_SYS_POLL_H = YesPlease + NO_INTTYPES_H = YesPlease + NO_INITGROUPS = YesPlease + NO_IPV6 = YesPlease + NO_MEMMEM = YesPlease + NO_MKDTEMP = YesPlease + NO_STRTOUMAX = YesPlease + NO_NSEC = YesPlease + NO_MKSTEMPS = YesPlease + ifeq ($(uname_R),3.5) + NO_INET_NTOP = YesPlease + NO_INET_PTON = YesPlease + endif + ifeq ($(uname_R),5.2) + NO_INET_NTOP = YesPlease + NO_INET_PTON = YesPlease + endif +endif ifneq (,$(findstring MINGW,$(uname_S))) pathsep = ; NO_PREAD = YesPlease @@ -1107,6 +1141,7 @@ ifneq (,$(findstring MINGW,$(uname_S))) NO_STRCASESTR = YesPlease NO_STRLCPY = YesPlease NO_STRTOK_R = YesPlease + NO_FNMATCH = YesPlease NO_MEMMEM = YesPlease NEEDS_LIBICONV = YesPlease OLD_ICONV = YesPlease @@ -1117,7 +1152,6 @@ ifneq (,$(findstring MINGW,$(uname_S))) NO_SVN_TESTS = YesPlease NO_PERL_MAKEMAKER = YesPlease RUNTIME_PREFIX = YesPlease - NO_POSIX_ONLY_PROGRAMS = YesPlease NO_ST_BLOCKS_IN_STRUCT_STAT = YesPlease NO_NSEC = YesPlease USE_WIN32_MMAP = YesPlease @@ -1128,10 +1162,14 @@ ifneq (,$(findstring MINGW,$(uname_S))) NO_PYTHON = YesPlease BLK_SHA1 = YesPlease ETAGS_TARGET = ETAGS - COMPAT_CFLAGS += -D__USE_MINGW_ACCESS -DNOGDI -Icompat -Icompat/fnmatch -Icompat/win32 + NO_INET_PTON = YesPlease + NO_INET_NTOP = YesPlease + NO_POSIX_GOODIES = UnfortunatelyYes + COMPAT_CFLAGS += -D__USE_MINGW_ACCESS -DNOGDI -Icompat -Icompat/win32 COMPAT_CFLAGS += -DSTRIP_EXTENSION=\".exe\" - COMPAT_OBJS += compat/mingw.o compat/fnmatch/fnmatch.o compat/winansi.o \ - compat/win32/pthread.o + COMPAT_OBJS += compat/mingw.o compat/winansi.o \ + compat/win32/pthread.o compat/win32/syslog.o \ + compat/win32/sys/poll.o EXTLIBS += -lws2_32 PTHREAD_LIBS = X = .exe @@ -1246,9 +1284,6 @@ ifdef ZLIB_PATH endif EXTLIBS += -lz -ifndef NO_POSIX_ONLY_PROGRAMS - PROGRAM_OBJS += daemon.o -endif ifndef NO_OPENSSL OPENSSL_LIBSSL = -lssl ifdef OPENSSLDIR @@ -1342,6 +1377,17 @@ ifdef NO_STRTOK_R COMPAT_CFLAGS += -DNO_STRTOK_R COMPAT_OBJS += compat/strtok_r.o endif +ifdef NO_FNMATCH + COMPAT_CFLAGS += -Icompat/fnmatch + COMPAT_CFLAGS += -DNO_FNMATCH + COMPAT_OBJS += compat/fnmatch/fnmatch.o +else +ifdef NO_FNMATCH_CASEFOLD + COMPAT_CFLAGS += -Icompat/fnmatch + COMPAT_CFLAGS += -DNO_FNMATCH_CASEFOLD + COMPAT_OBJS += compat/fnmatch/fnmatch.o +endif +endif ifdef NO_SETENV COMPAT_CFLAGS += -DNO_SETENV COMPAT_OBJS += compat/setenv.o @@ -1360,6 +1406,15 @@ endif ifdef NO_SYS_SELECT_H BASIC_CFLAGS += -DNO_SYS_SELECT_H endif +ifdef NO_SYS_POLL_H + BASIC_CFLAGS += -DNO_SYS_POLL_H +endif +ifdef NO_INTTYPES_H + BASIC_CFLAGS += -DNO_INTTYPES_H +endif +ifdef NO_INITGROUPS + BASIC_CFLAGS += -DNO_INITGROUPS +endif ifdef NO_MMAP COMPAT_CFLAGS += -DNO_MMAP COMPAT_OBJS += compat/mmap.o @@ -1397,9 +1452,11 @@ endif endif ifdef NO_INET_NTOP LIB_OBJS += compat/inet_ntop.o + BASIC_CFLAGS += -DNO_INET_NTOP endif ifdef NO_INET_PTON LIB_OBJS += compat/inet_pton.o + BASIC_CFLAGS += -DNO_INET_PTON endif ifdef NO_ICONV @@ -1414,6 +1471,10 @@ ifdef NO_DEFLATE_BOUND BASIC_CFLAGS += -DNO_DEFLATE_BOUND endif +ifdef NO_POSIX_GOODIES + BASIC_CFLAGS += -DNO_POSIX_GOODIES +endif + ifdef BLK_SHA1 SHA1_HEADER = "block-sha1/sha1.h" LIB_OBJS += block-sha1/sha1.o @@ -1766,6 +1827,8 @@ XDIFF_OBJS = xdiff/xdiffi.o xdiff/xprepare.o xdiff/xutils.o xdiff/xemit.o \ xdiff/xmerge.o xdiff/xpatience.o VCSSVN_OBJS = vcs-svn/string_pool.o vcs-svn/line_buffer.o \ vcs-svn/repo_tree.o vcs-svn/fast_export.o vcs-svn/svndump.o +VCSSVN_TEST_OBJS = test-obj-pool.o test-string-pool.o \ + test-line-buffer.o test-treap.o OBJECTS := $(GIT_OBJS) $(XDIFF_OBJS) $(VCSSVN_OBJS) dep_files := $(foreach f,$(OBJECTS),$(dir $f).depend/$(notdir $f).d) @@ -1874,13 +1937,12 @@ builtin/branch.o builtin/checkout.o builtin/clone.o builtin/reset.o branch.o tra builtin/bundle.o bundle.o transport.o: bundle.h builtin/bisect--helper.o builtin/rev-list.o bisect.o: bisect.h builtin/clone.o builtin/fetch-pack.o transport.o: fetch-pack.h -builtin/grep.o: thread-utils.h +builtin/grep.o builtin/pack-objects.o transport-helper.o: thread-utils.h builtin/send-pack.o transport.o: send-pack.h builtin/log.o builtin/shortlog.o: shortlog.h builtin/prune.o builtin/reflog.o reachable.o: reachable.h builtin/commit.o builtin/revert.o wt-status.o: wt-status.h builtin/tar-tree.o archive-tar.o: tar.h -builtin/pack-objects.o: thread-utils.h connect.o transport.o http-backend.o: url.h http-fetch.o http-walker.o remote-curl.o transport.o walker.o: walker.h http.o http-walker.o http-push.o http-fetch.o remote-curl.o: http.h @@ -1889,10 +1951,12 @@ xdiff-interface.o $(XDIFF_OBJS): \ xdiff/xinclude.h xdiff/xmacros.h xdiff/xdiff.h xdiff/xtypes.h \ xdiff/xutils.h xdiff/xprepare.h xdiff/xdiffi.h xdiff/xemit.h -$(VCSSVN_OBJS): \ +$(VCSSVN_OBJS) $(VCSSVN_TEST_OBJS): $(LIB_H) \ vcs-svn/obj_pool.h vcs-svn/trp.h vcs-svn/string_pool.h \ vcs-svn/line_buffer.h vcs-svn/repo_tree.h vcs-svn/fast_export.h \ vcs-svn/svndump.h + +test-svn-fe.o: vcs-svn/svndump.h endif exec_cmd.s exec_cmd.o: EXTRA_CPPFLAGS = \ @@ -1927,7 +1991,7 @@ git-%$X: %.o $(GITLIBS) git-imap-send$X: imap-send.o $(GITLIBS) $(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) \ - $(LIBS) $(OPENSSL_LINK) $(OPENSSL_LIBSSL) + $(LIBS) $(OPENSSL_LINK) $(OPENSSL_LIBSSL) $(LIB_4_CRYPTO) git-http-fetch$X: revision.o http.o http-walker.o http-fetch.o $(GITLIBS) $(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) \ @@ -314,7 +314,7 @@ static int parse_archive_args(int argc, const char **argv, "write the archive to this file"), OPT_BOOLEAN(0, "worktree-attributes", &worktree_attributes, "read .gitattributes in working directory"), - OPT__VERBOSE(&verbose), + OPT__VERBOSE(&verbose, "report archived files on stderr"), OPT__COMPR('0', &compression_level, "store only", 0), OPT__COMPR('1', &compression_level, "compress faster", 1), OPT__COMPR_HIDDEN('2', &compression_level, 2), @@ -22,8 +22,8 @@ void create_branch(const char *head, const char *name, const char *start_name, void remove_branch_state(void); /* - * Configure local branch "local" to merge remote branch "remote" - * taken from origin "origin". + * Configure local branch "local" as downstream to branch "remote" + * from remote "origin". Used by git branch --set-upstream. */ #define BRANCH_CONFIG_VERBOSE 01 extern void install_branch_config(int flag, const char *local, const char *origin, const char *remote); @@ -16,7 +16,7 @@ extern const char git_more_info_string[]; extern void prune_packed_objects(int); extern int fmt_merge_msg(struct strbuf *in, struct strbuf *out, int merge_title, int shortlog_len); -extern int commit_notes(struct notes_tree *t, const char *msg); +extern void commit_notes(struct notes_tree *t, const char *msg); struct notes_rewrite_cfg { struct notes_tree **trees; @@ -108,6 +108,8 @@ extern int cmd_read_tree(int argc, const char **argv, const char *prefix); extern int cmd_receive_pack(int argc, const char **argv, const char *prefix); extern int cmd_reflog(int argc, const char **argv, const char *prefix); extern int cmd_remote(int argc, const char **argv, const char *prefix); +extern int cmd_remote_ext(int argc, const char **argv, const char *prefix); +extern int cmd_remote_fd(int argc, const char **argv, const char *prefix); extern int cmd_config(int argc, const char **argv, const char *prefix); extern int cmd_rerere(int argc, const char **argv, const char *prefix); extern int cmd_reset(int argc, const char **argv, const char *prefix); diff --git a/builtin/add.c b/builtin/add.c index 56a4e0af6b..12b964e642 100644 --- a/builtin/add.c +++ b/builtin/add.c @@ -313,13 +313,13 @@ static int verbose = 0, show_only = 0, ignored_too = 0, refresh_only = 0; static int ignore_add_errors, addremove, intent_to_add, ignore_missing = 0; static struct option builtin_add_options[] = { - OPT__DRY_RUN(&show_only), - OPT__VERBOSE(&verbose), + OPT__DRY_RUN(&show_only, "dry run"), + OPT__VERBOSE(&verbose, "be verbose"), OPT_GROUP(""), OPT_BOOLEAN('i', "interactive", &add_interactive, "interactive picking"), OPT_BOOLEAN('p', "patch", &patch_interactive, "interactive patching"), OPT_BOOLEAN('e', "edit", &edit_interactive, "edit current diff and apply"), - OPT_BOOLEAN('f', "force", &ignored_too, "allow adding otherwise ignored files"), + OPT__FORCE(&ignored_too, "allow adding otherwise ignored files"), OPT_BOOLEAN('u', "update", &take_worktree_changes, "update tracked files"), OPT_BOOLEAN('N', "intent-to-add", &intent_to_add, "record only the fact that the path will be added later"), OPT_BOOLEAN('A', "all", &addremove, "add all, noticing removal of tracked files"), @@ -331,7 +331,8 @@ static struct option builtin_add_options[] = { static int add_config(const char *var, const char *value, void *cb) { - if (!strcasecmp(var, "add.ignore-errors")) { + if (!strcasecmp(var, "add.ignoreerrors") || + !strcasecmp(var, "add.ignore-errors")) { ignore_add_errors = git_config_bool(var, value); return 0; } @@ -446,7 +447,8 @@ int cmd_add(int argc, const char **argv, const char *prefix) if (!seen[i] && pathspec[i][0] && !file_exists(pathspec[i])) { if (ignore_missing) { - if (excluded(&dir, pathspec[i], DT_UNKNOWN)) + int dtype = DT_UNKNOWN; + if (excluded(&dir, pathspec[i], &dtype)) dir_add_ignored(&dir, pathspec[i], strlen(pathspec[i])); } else die("pathspec '%s' did not match any files", diff --git a/builtin/apply.c b/builtin/apply.c index f051e66dcc..14951daedf 100644 --- a/builtin/apply.c +++ b/builtin/apply.c @@ -449,7 +449,7 @@ static char *find_name_gnu(const char *line, char *def, int p_value) return squash_slash(strbuf_detach(&name, NULL)); } -static size_t tz_len(const char *line, size_t len) +static size_t sane_tz_len(const char *line, size_t len) { const char *tz, *p; @@ -467,6 +467,24 @@ static size_t tz_len(const char *line, size_t len) return line + len - tz; } +static size_t tz_with_colon_len(const char *line, size_t len) +{ + const char *tz, *p; + + if (len < strlen(" +08:00") || line[len - strlen(":00")] != ':') + return 0; + tz = line + len - strlen(" +08:00"); + + if (tz[0] != ' ' || (tz[1] != '+' && tz[1] != '-')) + return 0; + p = tz + 2; + if (!isdigit(*p++) || !isdigit(*p++) || *p++ != ':' || + !isdigit(*p++) || !isdigit(*p++)) + return 0; + + return line + len - tz; +} + static size_t date_len(const char *line, size_t len) { const char *date, *p; @@ -561,7 +579,9 @@ static size_t diff_timestamp_len(const char *line, size_t len) if (!isdigit(end[-1])) return 0; - n = tz_len(line, end - line); + n = sane_tz_len(line, end - line); + if (!n) + n = tz_with_colon_len(line, end - line); end -= n; n = short_time_len(line, end - line); @@ -733,8 +753,8 @@ static int has_epoch_timestamp(const char *nameline) " " "[0-2][0-9]:[0-5][0-9]:00(\\.0+)?" " " - "([-+][0-2][0-9][0-5][0-9])\n"; - const char *timestamp = NULL, *cp; + "([-+][0-2][0-9]:?[0-5][0-9])\n"; + const char *timestamp = NULL, *cp, *colon; static regex_t *stamp; regmatch_t m[10]; int zoneoffset; @@ -764,8 +784,11 @@ static int has_epoch_timestamp(const char *nameline) return 0; } - zoneoffset = strtol(timestamp + m[3].rm_so + 1, NULL, 10); - zoneoffset = (zoneoffset / 100) * 60 + (zoneoffset % 100); + zoneoffset = strtol(timestamp + m[3].rm_so + 1, (char **) &colon, 10); + if (*colon == ':') + zoneoffset = zoneoffset * 60 + strtol(colon + 1, NULL, 10); + else + zoneoffset = (zoneoffset / 100) * 60 + (zoneoffset % 100); if (timestamp[m[3].rm_so] == '-') zoneoffset = -zoneoffset; @@ -919,28 +942,28 @@ static int gitdiff_newfile(const char *line, struct patch *patch) static int gitdiff_copysrc(const char *line, struct patch *patch) { patch->is_copy = 1; - patch->old_name = find_name(line, NULL, 0, 0); + patch->old_name = find_name(line, NULL, p_value ? p_value - 1 : 0, 0); return 0; } static int gitdiff_copydst(const char *line, struct patch *patch) { patch->is_copy = 1; - patch->new_name = find_name(line, NULL, 0, 0); + patch->new_name = find_name(line, NULL, p_value ? p_value - 1 : 0, 0); return 0; } static int gitdiff_renamesrc(const char *line, struct patch *patch) { patch->is_rename = 1; - patch->old_name = find_name(line, NULL, 0, 0); + patch->old_name = find_name(line, NULL, p_value ? p_value - 1 : 0, 0); return 0; } static int gitdiff_renamedst(const char *line, struct patch *patch) { patch->is_rename = 1; - patch->new_name = find_name(line, NULL, 0, 0); + patch->new_name = find_name(line, NULL, p_value ? p_value - 1 : 0, 0); return 0; } @@ -1025,7 +1048,7 @@ static char *git_header_name(char *line, int llen) { const char *name; const char *second = NULL; - size_t len; + size_t len, line_len; line += strlen("diff --git "); llen -= strlen("diff --git "); @@ -1125,6 +1148,10 @@ static char *git_header_name(char *line, int llen) * Accept a name only if it shows up twice, exactly the same * form. */ + second = strchr(name, '\n'); + if (!second) + return NULL; + line_len = second - name; for (len = 0 ; ; len++) { switch (name[len]) { default: @@ -1132,15 +1159,11 @@ static char *git_header_name(char *line, int llen) case '\n': return NULL; case '\t': case ' ': - second = name+len; - for (;;) { - char c = *second++; - if (c == '\n') - return NULL; - if (c == '/') - break; - } - if (second[len] == '\n' && !memcmp(name, second, len)) { + second = stop_at_slash(name + len, line_len - len); + if (!second) + return NULL; + second++; + if (second[len] == '\n' && !strncmp(name, second, len)) { return xmemdupz(name, len); } } @@ -3849,7 +3872,7 @@ int cmd_apply(int argc, const char **argv, const char *prefix_) "don't expect at least one line of context"), OPT_BOOLEAN(0, "reject", &apply_with_reject, "leave the rejected hunks in corresponding *.rej files"), - OPT__VERBOSE(&apply_verbosely), + OPT__VERBOSE(&apply_verbosely, "be verbose"), OPT_BIT(0, "inaccurate-eof", &options, "tolerate incorrectly detected missing new-line at the end of file", INACCURATE_EOF), diff --git a/builtin/blame.c b/builtin/blame.c index f5fccc1f67..cb25ec9ce8 100644 --- a/builtin/blame.c +++ b/builtin/blame.c @@ -1617,6 +1617,7 @@ static const char *format_time(unsigned long time, const char *tz_str, #define OUTPUT_SHOW_NUMBER 040 #define OUTPUT_SHOW_SCORE 0100 #define OUTPUT_NO_AUTHOR 0200 +#define OUTPUT_SHOW_EMAIL 0400 static void emit_porcelain(struct scoreboard *sb, struct blame_entry *ent) { @@ -1682,12 +1683,17 @@ static void emit_other(struct scoreboard *sb, struct blame_entry *ent, int opt) } printf("%.*s", length, hex); - if (opt & OUTPUT_ANNOTATE_COMPAT) - printf("\t(%10s\t%10s\t%d)", ci.author, + if (opt & OUTPUT_ANNOTATE_COMPAT) { + const char *name; + if (opt & OUTPUT_SHOW_EMAIL) + name = ci.author_mail; + else + name = ci.author; + printf("\t(%10s\t%10s\t%d)", name, format_time(ci.author_time, ci.author_tz, show_raw_time), ent->lno + 1 + cnt); - else { + } else { if (opt & OUTPUT_SHOW_SCORE) printf(" %*d %02d", max_score_digits, ent->score, @@ -1700,9 +1706,15 @@ static void emit_other(struct scoreboard *sb, struct blame_entry *ent, int opt) ent->s_lno + 1 + cnt); if (!(opt & OUTPUT_NO_AUTHOR)) { - int pad = longest_author - utf8_strwidth(ci.author); + const char *name; + int pad; + if (opt & OUTPUT_SHOW_EMAIL) + name = ci.author_mail; + else + name = ci.author; + pad = longest_author - utf8_strwidth(name); printf(" (%s%*s %10s", - ci.author, pad, "", + name, pad, "", format_time(ci.author_time, ci.author_tz, show_raw_time)); @@ -1840,7 +1852,10 @@ static void find_alignment(struct scoreboard *sb, int *option) if (!(suspect->commit->object.flags & METAINFO_SHOWN)) { suspect->commit->object.flags |= METAINFO_SHOWN; get_commit_info(suspect->commit, &ci, 1); - num = utf8_strwidth(ci.author); + if (*option & OUTPUT_SHOW_EMAIL) + num = utf8_strwidth(ci.author_mail); + else + num = utf8_strwidth(ci.author); if (longest_author < num) longest_author = num; } @@ -2289,6 +2304,7 @@ int cmd_blame(int argc, const char **argv, const char *prefix) OPT_BIT('t', NULL, &output_option, "Show raw timestamp (Default: off)", OUTPUT_RAW_TIMESTAMP), OPT_BIT('l', NULL, &output_option, "Show long commit SHA1 (Default: off)", OUTPUT_LONG_OBJECT_NAME), OPT_BIT('s', NULL, &output_option, "Suppress author name and timestamp (Default: off)", OUTPUT_NO_AUTHOR), + OPT_BIT('e', "show-email", &output_option, "Show author email instead of name (Default: off)", OUTPUT_SHOW_EMAIL), OPT_BIT('w', NULL, &xdl_opts, "Ignore whitespace differences", XDF_IGNORE_WHITESPACE), OPT_STRING('S', NULL, &revs_file, "file", "Use revisions from <file> instead of calling git-rev-list"), OPT_STRING(0, "contents", &contents_from, "file", "Use <file>'s contents as the final image"), diff --git a/builtin/branch.c b/builtin/branch.c index 87976f0921..0cad20bb5a 100644 --- a/builtin/branch.c +++ b/builtin/branch.c @@ -621,7 +621,8 @@ int cmd_branch(int argc, const char **argv, const char *prefix) struct option options[] = { OPT_GROUP("Generic options"), - OPT__VERBOSE(&verbose), + OPT__VERBOSE(&verbose, + "show hash and subject, give twice for upstream branch"), OPT_SET_INT('t', "track", &track, "set up tracking mode (see git-pull(1))", BRANCH_TRACK_EXPLICIT), OPT_SET_INT( 0, "set-upstream", &track, "change upstream info", @@ -651,7 +652,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix) OPT_BIT('m', NULL, &rename, "move/rename a branch and its reflog", 1), OPT_BIT('M', NULL, &rename, "move/rename a branch, even if target exists", 2), OPT_BOOLEAN('l', NULL, &reflog, "create the branch's reflog"), - OPT_BOOLEAN('f', "force", &force_create, "force creation (when already exists)"), + OPT__FORCE(&force_create, "force creation (when already exists)"), { OPTION_CALLBACK, 0, "no-merged", &merge_filter_ref, "commit", "print only not merged branches", @@ -667,6 +668,9 @@ int cmd_branch(int argc, const char **argv, const char *prefix) OPT_END(), }; + if (argc == 2 && !strcmp(argv[1], "-h")) + usage_with_options(builtin_branch_usage, options); + git_config(git_branch_config, NULL); if (branch_use_color == -1) diff --git a/builtin/checkout-index.c b/builtin/checkout-index.c index 65cbee0552..f1fec24745 100644 --- a/builtin/checkout-index.c +++ b/builtin/checkout-index.c @@ -217,9 +217,9 @@ int cmd_checkout_index(int argc, const char **argv, const char *prefix) struct option builtin_checkout_index_options[] = { OPT_BOOLEAN('a', "all", &all, "checks out all files in the index"), - OPT_BOOLEAN('f', "force", &force, - "forces overwrite of existing files"), - OPT__QUIET(&quiet), + OPT__FORCE(&force, "forces overwrite of existing files"), + OPT__QUIET(&quiet, + "no warning for existing files and files not in index"), OPT_BOOLEAN('n', "no-create", ¬_new, "don't checkout new files"), { OPTION_CALLBACK, 'u', "index", &newfd, NULL, @@ -241,6 +241,9 @@ int cmd_checkout_index(int argc, const char **argv, const char *prefix) OPT_END() }; + if (argc == 2 && !strcmp(argv[1], "-h")) + usage_with_options(builtin_checkout_index_usage, + builtin_checkout_index_options); git_config(git_default_config, NULL); state.base_dir = ""; prefix_length = prefix ? strlen(prefix) : 0; diff --git a/builtin/checkout.c b/builtin/checkout.c index 9240fafb2a..757f9a08dd 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -686,7 +686,7 @@ int cmd_checkout(int argc, const char **argv, const char *prefix) int patch_mode = 0; int dwim_new_local_branch = 1; struct option options[] = { - OPT__QUIET(&opts.quiet), + OPT__QUIET(&opts.quiet, "suppress progress reporting"), OPT_STRING('b', NULL, &opts.new_branch, "branch", "create and checkout a new branch"), OPT_STRING('B', NULL, &opts.new_branch_force, "branch", @@ -699,7 +699,7 @@ int cmd_checkout(int argc, const char **argv, const char *prefix) 2), OPT_SET_INT('3', "theirs", &opts.writeout_stage, "checkout their version for unmerged files", 3), - OPT_BOOLEAN('f', "force", &opts.force, "force checkout (throw away local modifications)"), + OPT__FORCE(&opts.force, "force checkout (throw away local modifications)"), OPT_BOOLEAN('m', "merge", &opts.merge, "perform a 3-way merge with the new branch"), OPT_STRING(0, "conflict", &conflict_style, "style", "conflict style (merge or diff3)"), @@ -784,9 +784,9 @@ int cmd_checkout(int argc, const char **argv, const char *prefix) * between A and B, A...B names that merge base. * * With no paths, if <something> is _not_ a commit, no -t nor -b - * was given, and there is a tracking branch whose name is + * was given, and there is a remote-tracking branch whose name is * <something> in one and only one remote, then this is a short-hand - * to fork local <something> from that remote tracking branch. + * to fork local <something> from that remote-tracking branch. * * Otherwise <something> shall not be ambiguous. * - If it's *only* a reference, treat it like case (1). diff --git a/builtin/clean.c b/builtin/clean.c index fb24030751..4a312abc6b 100644 --- a/builtin/clean.c +++ b/builtin/clean.c @@ -48,9 +48,9 @@ int cmd_clean(int argc, const char **argv, const char *prefix) const char *qname; char *seen = NULL; struct option options[] = { - OPT__QUIET(&quiet), - OPT__DRY_RUN(&show_only), - OPT_BOOLEAN('f', "force", &force, "force"), + OPT__QUIET(&quiet, "do not print names of files removed"), + OPT__DRY_RUN(&show_only, "dry run"), + OPT__FORCE(&force, "force"), OPT_BOOLEAN('d', NULL, &remove_directories, "remove whole directories"), { OPTION_CALLBACK, 'e', "exclude", &exclude_list, "pattern", diff --git a/builtin/clone.c b/builtin/clone.c index 19ed64041d..61e0989b5a 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -66,6 +66,8 @@ static struct option builtin_clone_options[] = { "setup as shared repository"), OPT_BOOLEAN(0, "recursive", &option_recursive, "initialize submodules in the clone"), + OPT_BOOLEAN(0, "recurse_submodules", &option_recursive, + "initialize submodules in the clone"), OPT_STRING(0, "template", &option_template, "path", "path the template repository"), OPT_STRING(0, "reference", &option_reference, "repo", diff --git a/builtin/commit.c b/builtin/commit.c index 66fdd22024..c045c9ef8c 100644 --- a/builtin/commit.c +++ b/builtin/commit.c @@ -69,6 +69,7 @@ static enum { static const char *logfile, *force_author; static const char *template_file; static char *edit_message, *use_message; +static char *fixup_message, *squash_message; static char *author_name, *author_email, *author_date; static int all, edit_flag, also, interactive, only, amend, signoff; static int quiet, verbose, no_verify, allow_empty, dry_run, renew_authorship; @@ -114,8 +115,8 @@ static int opt_parse_m(const struct option *opt, const char *arg, int unset) } static struct option builtin_commit_options[] = { - OPT__QUIET(&quiet), - OPT__VERBOSE(&verbose), + OPT__QUIET(&quiet, "suppress summary after successful commit"), + OPT__VERBOSE(&verbose, "show diff in commit message template"), OPT_GROUP("Commit message options"), OPT_FILENAME('F', "file", &logfile, "read log from file"), @@ -124,6 +125,8 @@ static struct option builtin_commit_options[] = { OPT_CALLBACK('m', "message", &message, "MESSAGE", "specify commit message", opt_parse_m), OPT_STRING('c', "reedit-message", &edit_message, "COMMIT", "reuse and edit message from specified commit"), OPT_STRING('C', "reuse-message", &use_message, "COMMIT", "reuse message from specified commit"), + OPT_STRING(0, "fixup", &fixup_message, "COMMIT", "use autosquash formatted message to fixup specified commit"), + OPT_STRING(0, "squash", &squash_message, "COMMIT", "use autosquash formatted message to squash specified commit"), OPT_BOOLEAN(0, "reset-author", &renew_authorship, "the commit is authored by me now (used with -C-c/--amend)"), OPT_BOOLEAN('s', "signoff", &signoff, "add Signed-off-by:"), OPT_FILENAME('t', "template", &template_file, "use specified template file"), @@ -565,6 +568,25 @@ static int prepare_to_commit(const char *index_file, const char *prefix, if (!no_verify && run_hook(index_file, "pre-commit", NULL)) return 0; + if (squash_message) { + /* + * Insert the proper subject line before other commit + * message options add their content. + */ + if (use_message && !strcmp(use_message, squash_message)) + strbuf_addstr(&sb, "squash! "); + else { + struct pretty_print_context ctx = {0}; + struct commit *c; + c = lookup_commit_reference_by_name(squash_message); + if (!c) + die("could not lookup commit %s", squash_message); + ctx.output_encoding = get_commit_output_encoding(); + format_commit_message(c, "squash! %s\n\n", &sb, + &ctx); + } + } + if (message.len) { strbuf_addbuf(&sb, &message); hook_arg1 = "message"; @@ -586,6 +608,16 @@ static int prepare_to_commit(const char *index_file, const char *prefix, strbuf_add(&sb, buffer + 2, strlen(buffer + 2)); hook_arg1 = "commit"; hook_arg2 = use_message; + } else if (fixup_message) { + struct pretty_print_context ctx = {0}; + struct commit *commit; + commit = lookup_commit_reference_by_name(fixup_message); + if (!commit) + die("could not lookup commit %s", fixup_message); + ctx.output_encoding = get_commit_output_encoding(); + format_commit_message(commit, "fixup! %s\n\n", + &sb, &ctx); + hook_arg1 = "message"; } else if (!stat(git_path("MERGE_MSG"), &statbuf)) { if (strbuf_read_file(&sb, git_path("MERGE_MSG"), 0) < 0) die_errno("could not read MERGE_MSG"); @@ -607,6 +639,16 @@ static int prepare_to_commit(const char *index_file, const char *prefix, else if (in_merge) hook_arg1 = "merge"; + if (squash_message) { + /* + * If squash_commit was used for the commit subject, + * then we're possibly hijacking other commit log options. + * Reset the hook args to tell the real story. + */ + hook_arg1 = "message"; + hook_arg2 = ""; + } + fp = fopen(git_path(commit_editmsg), "w"); if (fp == NULL) die_errno("could not open '%s'", git_path(commit_editmsg)); @@ -863,7 +905,7 @@ static int parse_and_validate_options(int argc, const char *argv[], if (force_author && renew_authorship) die("Using both --reset-author and --author does not make sense"); - if (logfile || message.len || use_message) + if (logfile || message.len || use_message || fixup_message) use_editor = 0; if (edit_flag) use_editor = 1; @@ -878,48 +920,35 @@ static int parse_and_validate_options(int argc, const char *argv[], die("You have nothing to amend."); if (amend && in_merge) die("You are in the middle of a merge -- cannot amend."); - + if (fixup_message && squash_message) + die("Options --squash and --fixup cannot be used together"); if (use_message) f++; if (edit_message) f++; + if (fixup_message) + f++; if (logfile) f++; if (f > 1) - die("Only one of -c/-C/-F can be used."); + die("Only one of -c/-C/-F/--fixup can be used."); if (message.len && f > 0) - die("Option -m cannot be combined with -c/-C/-F."); + die("Option -m cannot be combined with -c/-C/-F/--fixup."); if (edit_message) use_message = edit_message; - if (amend && !use_message) + if (amend && !use_message && !fixup_message) use_message = "HEAD"; if (!use_message && renew_authorship) die("--reset-author can be used only with -C, -c or --amend."); if (use_message) { - unsigned char sha1[20]; - static char utf8[] = "UTF-8"; const char *out_enc; - char *enc, *end; struct commit *commit; - if (get_sha1(use_message, sha1)) + commit = lookup_commit_reference_by_name(use_message); + if (!commit) die("could not lookup commit %s", use_message); - commit = lookup_commit_reference(sha1); - if (!commit || parse_commit(commit)) - die("could not parse commit %s", use_message); - - enc = strstr(commit->buffer, "\nencoding"); - if (enc) { - end = strchr(enc + 10, '\n'); - enc = xstrndup(enc + 10, end - (enc + 10)); - } else { - enc = utf8; - } - out_enc = git_commit_encoding ? git_commit_encoding : utf8; - - if (strcmp(out_enc, enc)) - use_message_buffer = - reencode_string(commit->buffer, out_enc, enc); + out_enc = get_commit_output_encoding(); + use_message_buffer = logmsg_reencode(commit, out_enc); /* * If we failed to reencode the buffer, just copy it @@ -929,8 +958,6 @@ static int parse_and_validate_options(int argc, const char *argv[], */ if (use_message_buffer == NULL) use_message_buffer = xstrdup(commit->buffer); - if (enc != utf8) - free(enc); } if (!!also + !!only + !!all + !!interactive > 1) @@ -1048,7 +1075,7 @@ int cmd_status(int argc, const char **argv, const char *prefix) int fd; unsigned char sha1[20]; static struct option builtin_status_options[] = { - OPT__VERBOSE(&verbose), + OPT__VERBOSE(&verbose, "be verbose"), OPT_SET_INT('s', "short", &status_format, "show status concisely", STATUS_FORMAT_SHORT), OPT_BOOLEAN('b', "branch", &status_show_branch, @@ -1070,6 +1097,9 @@ int cmd_status(int argc, const char **argv, const char *prefix) OPT_END(), }; + if (argc == 2 && !strcmp(argv[1], "-h")) + usage_with_options(builtin_status_usage, builtin_status_options); + if (null_termination && status_format == STATUS_FORMAT_LONG) status_format = STATUS_FORMAT_PORCELAIN; @@ -1255,6 +1285,9 @@ int cmd_commit(int argc, const char **argv, const char *prefix) int allow_fast_forward = 1; struct wt_status s; + if (argc == 2 && !strcmp(argv[1], "-h")) + usage_with_options(builtin_commit_usage, builtin_commit_options); + wt_status_prepare(&s); git_config(git_commit_config, &s); in_merge = file_exists(git_path("MERGE_HEAD")); diff --git a/builtin/count-objects.c b/builtin/count-objects.c index 2bdd8ebde1..c37cb98c31 100644 --- a/builtin/count-objects.c +++ b/builtin/count-objects.c @@ -79,7 +79,7 @@ int cmd_count_objects(int argc, const char **argv, const char *prefix) unsigned long loose = 0, packed = 0, packed_loose = 0, garbage = 0; off_t loose_size = 0; struct option opts[] = { - OPT__VERBOSE(&verbose), + OPT__VERBOSE(&verbose, "be verbose"), OPT_END(), }; diff --git a/builtin/diff.c b/builtin/diff.c index a43d326363..945e7583a8 100644 --- a/builtin/diff.c +++ b/builtin/diff.c @@ -22,7 +22,7 @@ struct blobinfo { }; static const char builtin_diff_usage[] = -"git diff <options> <rev>{0,2} -- <path>*"; +"git diff [<options>] [<commit> [<commit>]] [--] [<path>...]"; static void stuff_change(struct diff_options *opt, unsigned old_mode, unsigned new_mode, diff --git a/builtin/fetch.c b/builtin/fetch.c index d35f000c03..6bcce55c0c 100644 --- a/builtin/fetch.c +++ b/builtin/fetch.c @@ -43,8 +43,7 @@ static struct option builtin_fetch_options[] = { "append to .git/FETCH_HEAD instead of overwriting"), OPT_STRING(0, "upload-pack", &upload_pack, "PATH", "path to upload pack on remote end"), - OPT_BOOLEAN('f', "force", &force, - "force overwrite of local branch"), + OPT__FORCE(&force, "force overwrite of local branch"), OPT_BOOLEAN('m', "multiple", &multiple, "fetch from multiple remotes"), OPT_SET_INT('t', "tags", &tags, @@ -52,7 +51,7 @@ static struct option builtin_fetch_options[] = { OPT_SET_INT('n', NULL, &tags, "do not fetch all tags (--no-tags)", TAGS_UNSET), OPT_BOOLEAN('p', "prune", &prune, - "prune tracking branches no longer on remote"), + "prune remote-tracking branches no longer on remote"), OPT_BOOLEAN(0, "dry-run", &dry_run, "dry run"), OPT_BOOLEAN('k', "keep", &keep, "keep downloaded pack"), @@ -98,7 +97,7 @@ static void add_merge_config(struct ref **head, continue; /* - * Not fetched to a tracking branch? We need to fetch + * Not fetched to a remote-tracking branch? We need to fetch * it anyway to allow this branch's "branch.$name.merge" * to be honored by 'git pull', but we do not have to * fail if branch.$name.merge is misconfigured to point @@ -359,7 +358,7 @@ static int store_updated_refs(const char *raw_url, const char *remote_name, what = rm->name + 10; } else if (!prefixcmp(rm->name, "refs/remotes/")) { - kind = "remote branch"; + kind = "remote-tracking branch"; what = rm->name + 13; } else { diff --git a/builtin/fmt-merge-msg.c b/builtin/fmt-merge-msg.c index 78c77742b6..5189b16c9e 100644 --- a/builtin/fmt-merge-msg.c +++ b/builtin/fmt-merge-msg.c @@ -100,8 +100,8 @@ static int handle_line(char *line) origin = line; string_list_append(&src_data->tag, origin + 4); src_data->head_status |= 2; - } else if (!prefixcmp(line, "remote branch ")) { - origin = line + 14; + } else if (!prefixcmp(line, "remote-tracking branch ")) { + origin = line + strlen("remote-tracking branch "); string_list_append(&src_data->r_branch, origin); src_data->head_status |= 2; } else { @@ -233,7 +233,7 @@ static void do_fmt_merge_msg_title(struct strbuf *out, if (src_data->r_branch.nr) { strbuf_addstr(out, subsep); subsep = ", "; - print_joined("remote branch ", "remote branches ", + print_joined("remote-tracking branch ", "remote-tracking branches ", &src_data->r_branch, out); } if (src_data->tag.nr) { diff --git a/builtin/fsck.c b/builtin/fsck.c index 0929c7f245..6d5ebca7a9 100644 --- a/builtin/fsck.c +++ b/builtin/fsck.c @@ -572,7 +572,7 @@ static char const * const fsck_usage[] = { }; static struct option fsck_opts[] = { - OPT__VERBOSE(&verbose), + OPT__VERBOSE(&verbose, "be verbose"), OPT_BOOLEAN(0, "unreachable", &show_unreachable, "show unreachable objects"), OPT_BOOLEAN(0, "tags", &show_tags, "report tags"), OPT_BOOLEAN(0, "root", &show_root, "report root nodes"), diff --git a/builtin/gc.c b/builtin/gc.c index c304638b78..1a80702b3d 100644 --- a/builtin/gc.c +++ b/builtin/gc.c @@ -180,7 +180,7 @@ int cmd_gc(int argc, const char **argv, const char *prefix) char buf[80]; struct option builtin_gc_options[] = { - OPT__QUIET(&quiet), + OPT__QUIET(&quiet, "suppress progress reporting"), { OPTION_STRING, 0, "prune", &prune_expire, "date", "prune unreferenced objects", PARSE_OPT_OPTARG, NULL, (intptr_t)prune_expire }, @@ -189,6 +189,9 @@ int cmd_gc(int argc, const char **argv, const char *prefix) OPT_END() }; + if (argc == 2 && !strcmp(argv[1], "-h")) + usage_with_options(builtin_gc_usage, builtin_gc_options); + git_config(gc_config, NULL); if (pack_refs < 0) diff --git a/builtin/grep.c b/builtin/grep.c index 3d5f6ace97..fdf7131efd 100644 --- a/builtin/grep.c +++ b/builtin/grep.c @@ -17,11 +17,7 @@ #include "grep.h" #include "quote.h" #include "dir.h" - -#ifndef NO_PTHREADS -#include <pthread.h> #include "thread-utils.h" -#endif static char const * const grep_usage[] = { "git grep [options] [-e] <pattern> [<rev>...] [[--] <path>...]", @@ -915,8 +911,8 @@ int cmd_grep(int argc, const char **argv, const char *prefix) { OPTION_CALLBACK, ')', NULL, &opt, NULL, "", PARSE_OPT_NOARG | PARSE_OPT_NONEG | PARSE_OPT_NODASH, close_callback }, - OPT_BOOLEAN('q', "quiet", &opt.status_only, - "indicate hit with exit status without output"), + OPT__QUIET(&opt.status_only, + "indicate hit with exit status without output"), OPT_BOOLEAN(0, "all-match", &opt.all_match, "show only matches from files that match all patterns"), OPT_GROUP(""), diff --git a/builtin/log.c b/builtin/log.c index 22d12903ac..4191d9c4e5 100644 --- a/builtin/log.c +++ b/builtin/log.c @@ -329,8 +329,7 @@ static void show_tagger(char *buf, int len, struct rev_info *rev) struct strbuf out = STRBUF_INIT; pp_user_info("Tagger", rev->commit_format, &out, buf, rev->date_mode, - git_log_output_encoding ? - git_log_output_encoding: git_commit_encoding); + get_log_output_encoding()); printf("%s", out.buf); strbuf_release(&out); } @@ -1365,7 +1364,7 @@ int cmd_cherry(int argc, const char **argv, const char *prefix) struct option options[] = { OPT__ABBREV(&abbrev), - OPT__VERBOSE(&verbose), + OPT__VERBOSE(&verbose, "be verbose"), OPT_END() }; diff --git a/builtin/ls-files.c b/builtin/ls-files.c index 6a307ab784..fb2d5f4b1f 100644 --- a/builtin/ls-files.c +++ b/builtin/ls-files.c @@ -530,6 +530,9 @@ int cmd_ls_files(int argc, const char **argv, const char *cmd_prefix) OPT_END() }; + if (argc == 2 && !strcmp(argv[1], "-h")) + usage_with_options(ls_files_usage, builtin_ls_files_options); + memset(&dir, 0, sizeof(dir)); prefix = cmd_prefix; if (prefix) diff --git a/builtin/mailinfo.c b/builtin/mailinfo.c index 2320d981ce..71e6262a87 100644 --- a/builtin/mailinfo.c +++ b/builtin/mailinfo.c @@ -1032,7 +1032,7 @@ int cmd_mailinfo(int argc, const char **argv, const char *prefix) */ git_config(git_mailinfo_config, NULL); - def_charset = (git_commit_encoding ? git_commit_encoding : "UTF-8"); + def_charset = get_commit_output_encoding(); metainfo_charset = def_charset; while (1 < argc && argv[1][0] == '-') { diff --git a/builtin/merge-file.c b/builtin/merge-file.c index 6c4afb5a38..237abd3c0b 100644 --- a/builtin/merge-file.c +++ b/builtin/merge-file.c @@ -40,7 +40,7 @@ int cmd_merge_file(int argc, const char **argv, const char *prefix) XDL_MERGE_FAVOR_UNION), OPT_INTEGER(0, "marker-size", &xmp.marker_size, "for conflicts, use this marker size"), - OPT__QUIET(&quiet), + OPT__QUIET(&quiet, "do not warn about conflicts"), OPT_CALLBACK('L', NULL, names, "name", "set labels for file1/orig_file/file2", &label_cb), OPT_END(), diff --git a/builtin/merge.c b/builtin/merge.c index 10f091b519..42fff387e6 100644 --- a/builtin/merge.c +++ b/builtin/merge.c @@ -57,6 +57,7 @@ static const char *branch; static int option_renormalize; static int verbosity; static int allow_rerere_auto; +static int abort_current_merge; static struct strategy all_strategy[] = { { "recursive", DEFAULT_TWOHEAD | NO_TRIVIAL }, @@ -197,6 +198,8 @@ static struct option builtin_merge_options[] = { "message to be used for the merge commit (if any)", option_parse_message), OPT__VERBOSITY(&verbosity), + OPT_BOOLEAN(0, "abort", &abort_current_merge, + "abort the current in-progress merge"), OPT_END() }; @@ -234,6 +237,24 @@ static void save_state(void) die("not a valid object: %s", buffer.buf); } +static void read_empty(unsigned const char *sha1, int verbose) +{ + int i = 0; + const char *args[7]; + + args[i++] = "read-tree"; + if (verbose) + args[i++] = "-v"; + args[i++] = "-m"; + args[i++] = "-u"; + args[i++] = EMPTY_TREE_SHA1_HEX; + args[i++] = sha1_to_hex(sha1); + args[i] = NULL; + + if (run_command_v_opt(args, RUN_GIT_CMD)) + die("read-tree failed"); +} + static void reset_hard(unsigned const char *sha1, int verbose) { int i = 0; @@ -403,7 +424,7 @@ static void merge_name(const char *remote, struct strbuf *msg) goto cleanup; } if (!prefixcmp(found_ref, "refs/remotes/")) { - strbuf_addf(msg, "%s\t\tremote branch '%s' of .\n", + strbuf_addf(msg, "%s\t\tremote-tracking branch '%s' of .\n", sha1_to_hex(branch_head), remote); goto cleanup; } @@ -901,22 +922,9 @@ int cmd_merge(int argc, const char **argv, const char *prefix) const char *best_strategy = NULL, *wt_strategy = NULL; struct commit_list **remotes = &remoteheads; - if (read_cache_unmerged()) { - die_resolve_conflict("merge"); - } - if (file_exists(git_path("MERGE_HEAD"))) { - /* - * There is no unmerged entry, don't advise 'git - * add/rm <file>', just 'git commit'. - */ - if (advice_resolve_conflict) - die("You have not concluded your merge (MERGE_HEAD exists).\n" - "Please, commit your changes before you can merge."); - else - die("You have not concluded your merge (MERGE_HEAD exists)."); - } + if (argc == 2 && !strcmp(argv[1], "-h")) + usage_with_options(builtin_merge_usage, builtin_merge_options); - resolve_undo_clear(); /* * Check if we are _not_ on a detached HEAD, i.e. if there is a * current branch. @@ -935,6 +943,34 @@ int cmd_merge(int argc, const char **argv, const char *prefix) argc = parse_options(argc, argv, prefix, builtin_merge_options, builtin_merge_usage, 0); + + if (abort_current_merge) { + int nargc = 2; + const char *nargv[] = {"reset", "--merge", NULL}; + + if (!file_exists(git_path("MERGE_HEAD"))) + die("There is no merge to abort (MERGE_HEAD missing)."); + + /* Invoke 'git reset --merge' */ + return cmd_reset(nargc, nargv, prefix); + } + + if (read_cache_unmerged()) + die_resolve_conflict("merge"); + + if (file_exists(git_path("MERGE_HEAD"))) { + /* + * There is no unmerged entry, don't advise 'git + * add/rm <file>', just 'git commit'. + */ + if (advice_resolve_conflict) + die("You have not concluded your merge (MERGE_HEAD exists).\n" + "Please, commit your changes before you can merge."); + else + die("You have not concluded your merge (MERGE_HEAD exists)."); + } + resolve_undo_clear(); + if (verbosity < 0) show_diffstat = 0; @@ -985,7 +1021,7 @@ int cmd_merge(int argc, const char **argv, const char *prefix) die("%s - not something we can merge", argv[0]); update_ref("initial pull", "HEAD", remote_head->sha1, NULL, 0, DIE_ON_ERR); - reset_hard(remote_head->sha1, 0); + read_empty(remote_head->sha1, 0); return 0; } else { struct strbuf merge_names = STRBUF_INIT; diff --git a/builtin/mv.c b/builtin/mv.c index cdbb09473c..93e8995d9e 100644 --- a/builtin/mv.c +++ b/builtin/mv.c @@ -55,8 +55,8 @@ int cmd_mv(int argc, const char **argv, const char *prefix) int i, newfd; int verbose = 0, show_only = 0, force = 0, ignore_errors = 0; struct option builtin_mv_options[] = { - OPT__DRY_RUN(&show_only), - OPT_BOOLEAN('f', "force", &force, "force move/rename even if target exists"), + OPT__DRY_RUN(&show_only, "dry run"), + OPT__FORCE(&force, "force move/rename even if target exists"), OPT_BOOLEAN('k', NULL, &ignore_errors, "skip move/rename errors"), OPT_END(), }; diff --git a/builtin/notes.c b/builtin/notes.c index 6d07aac80c..4d5556e2cb 100644 --- a/builtin/notes.c +++ b/builtin/notes.c @@ -17,6 +17,7 @@ #include "run-command.h" #include "parse-options.h" #include "string-list.h" +#include "notes-merge.h" static const char * const git_notes_usage[] = { "git notes [--ref <notes_ref>] [list [<object>]]", @@ -25,8 +26,12 @@ static const char * const git_notes_usage[] = { "git notes [--ref <notes_ref>] append [-m <msg> | -F <file> | (-c | -C) <object>] [<object>]", "git notes [--ref <notes_ref>] edit [<object>]", "git notes [--ref <notes_ref>] show [<object>]", + "git notes [--ref <notes_ref>] merge [-v | -q] [-s <strategy> ] <notes_ref>", + "git notes merge --commit [-v | -q]", + "git notes merge --abort [-v | -q]", "git notes [--ref <notes_ref>] remove [<object>]", "git notes [--ref <notes_ref>] prune [-n | -v]", + "git notes [--ref <notes_ref>] get-ref", NULL }; @@ -61,6 +66,13 @@ static const char * const git_notes_show_usage[] = { NULL }; +static const char * const git_notes_merge_usage[] = { + "git notes merge [<options>] <notes_ref>", + "git notes merge --commit [<options>]", + "git notes merge --abort [<options>]", + NULL +}; + static const char * const git_notes_remove_usage[] = { "git notes remove [<object>]", NULL @@ -71,6 +83,11 @@ static const char * const git_notes_prune_usage[] = { NULL }; +static const char * const git_notes_get_ref_usage[] = { + "git notes get-ref", + NULL +}; + static const char note_template[] = "\n" "#\n" @@ -83,6 +100,16 @@ struct msg_arg { struct strbuf buf; }; +static void expand_notes_ref(struct strbuf *sb) +{ + if (!prefixcmp(sb->buf, "refs/notes/")) + return; /* we're happy */ + else if (!prefixcmp(sb->buf, "notes/")) + strbuf_insert(sb, 0, "refs/", 5); + else + strbuf_insert(sb, 0, "refs/notes/", 11); +} + static int list_each_note(const unsigned char *object_sha1, const unsigned char *note_sha1, char *note_path, void *cb_data) @@ -271,18 +298,17 @@ static int parse_reedit_arg(const struct option *opt, const char *arg, int unset return parse_reuse_arg(opt, arg, unset); } -int commit_notes(struct notes_tree *t, const char *msg) +void commit_notes(struct notes_tree *t, const char *msg) { - struct commit_list *parent; - unsigned char tree_sha1[20], prev_commit[20], new_commit[20]; struct strbuf buf = STRBUF_INIT; + unsigned char commit_sha1[20]; if (!t) t = &default_notes_tree; if (!t->initialized || !t->ref || !*t->ref) die("Cannot commit uninitialized/unreferenced notes tree"); if (!t->dirty) - return 0; /* don't have to commit an unchanged tree */ + return; /* don't have to commit an unchanged tree */ /* Prepare commit message and reflog message */ strbuf_addstr(&buf, "notes: "); /* commit message starts at index 7 */ @@ -290,27 +316,10 @@ int commit_notes(struct notes_tree *t, const char *msg) if (buf.buf[buf.len - 1] != '\n') strbuf_addch(&buf, '\n'); /* Make sure msg ends with newline */ - /* Convert notes tree to tree object */ - if (write_notes_tree(t, tree_sha1)) - die("Failed to write current notes tree to database"); - - /* Create new commit for the tree object */ - if (!read_ref(t->ref, prev_commit)) { /* retrieve parent commit */ - parent = xmalloc(sizeof(*parent)); - parent->item = lookup_commit(prev_commit); - parent->next = NULL; - } else { - hashclr(prev_commit); - parent = NULL; - } - if (commit_tree(buf.buf + 7, tree_sha1, parent, new_commit, NULL)) - die("Failed to commit notes tree to database"); - - /* Update notes ref with new commit */ - update_ref(buf.buf, t->ref, new_commit, prev_commit, 0, DIE_ON_ERR); + create_notes_commit(t, NULL, buf.buf + 7, commit_sha1); + update_ref(buf.buf, t->ref, commit_sha1, NULL, 0, DIE_ON_ERR); strbuf_release(&buf); - return 0; } combine_notes_fn parse_combine_notes_fn(const char *v) @@ -321,6 +330,8 @@ combine_notes_fn parse_combine_notes_fn(const char *v) return combine_notes_ignore; else if (!strcasecmp(v, "concatenate")) return combine_notes_concatenate; + else if (!strcasecmp(v, "cat_sort_uniq")) + return combine_notes_cat_sort_uniq; else return NULL; } @@ -538,7 +549,7 @@ static int add(int argc, const char **argv, const char *prefix) { OPTION_CALLBACK, 'C', "reuse-message", &msg, "OBJECT", "reuse specified note object", PARSE_OPT_NONEG, parse_reuse_arg}, - OPT_BOOLEAN('f', "force", &force, "replace existing notes"), + OPT__FORCE(&force, "replace existing notes"), OPT_END() }; @@ -573,8 +584,8 @@ static int add(int argc, const char **argv, const char *prefix) if (is_null_sha1(new_note)) remove_note(t, object); - else - add_note(t, object, new_note, combine_notes_overwrite); + else if (add_note(t, object, new_note, combine_notes_overwrite)) + die("BUG: combine_notes_overwrite failed"); snprintf(logmsg, sizeof(logmsg), "Notes %s by 'git notes %s'", is_null_sha1(new_note) ? "removed" : "added", "add"); @@ -594,7 +605,7 @@ static int copy(int argc, const char **argv, const char *prefix) struct notes_tree *t; const char *rewrite_cmd = NULL; struct option options[] = { - OPT_BOOLEAN('f', "force", &force, "replace existing notes"), + OPT__FORCE(&force, "replace existing notes"), OPT_BOOLEAN(0, "stdin", &from_stdin, "read objects from stdin"), OPT_STRING(0, "for-rewrite", &rewrite_cmd, "command", "load rewriting config for <command> (implies " @@ -653,7 +664,8 @@ static int copy(int argc, const char **argv, const char *prefix) goto out; } - add_note(t, object, from_note, combine_notes_overwrite); + if (add_note(t, object, from_note, combine_notes_overwrite)) + die("BUG: combine_notes_overwrite failed"); commit_notes(t, "Notes added by 'git notes copy'"); out: free_notes(t); @@ -712,8 +724,8 @@ static int append_edit(int argc, const char **argv, const char *prefix) if (is_null_sha1(new_note)) remove_note(t, object); - else - add_note(t, object, new_note, combine_notes_overwrite); + else if (add_note(t, object, new_note, combine_notes_overwrite)) + die("BUG: combine_notes_overwrite failed"); snprintf(logmsg, sizeof(logmsg), "Notes %s by 'git notes %s'", is_null_sha1(new_note) ? "removed" : "added", argv[0]); @@ -761,6 +773,180 @@ static int show(int argc, const char **argv, const char *prefix) return retval; } +static int merge_abort(struct notes_merge_options *o) +{ + int ret = 0; + + /* + * Remove .git/NOTES_MERGE_PARTIAL and .git/NOTES_MERGE_REF, and call + * notes_merge_abort() to remove .git/NOTES_MERGE_WORKTREE. + */ + + if (delete_ref("NOTES_MERGE_PARTIAL", NULL, 0)) + ret += error("Failed to delete ref NOTES_MERGE_PARTIAL"); + if (delete_ref("NOTES_MERGE_REF", NULL, REF_NODEREF)) + ret += error("Failed to delete ref NOTES_MERGE_REF"); + if (notes_merge_abort(o)) + ret += error("Failed to remove 'git notes merge' worktree"); + return ret; +} + +static int merge_commit(struct notes_merge_options *o) +{ + struct strbuf msg = STRBUF_INIT; + unsigned char sha1[20], parent_sha1[20]; + struct notes_tree *t; + struct commit *partial; + struct pretty_print_context pretty_ctx; + + /* + * Read partial merge result from .git/NOTES_MERGE_PARTIAL, + * and target notes ref from .git/NOTES_MERGE_REF. + */ + + if (get_sha1("NOTES_MERGE_PARTIAL", sha1)) + die("Failed to read ref NOTES_MERGE_PARTIAL"); + else if (!(partial = lookup_commit_reference(sha1))) + die("Could not find commit from NOTES_MERGE_PARTIAL."); + else if (parse_commit(partial)) + die("Could not parse commit from NOTES_MERGE_PARTIAL."); + + if (partial->parents) + hashcpy(parent_sha1, partial->parents->item->object.sha1); + else + hashclr(parent_sha1); + + t = xcalloc(1, sizeof(struct notes_tree)); + init_notes(t, "NOTES_MERGE_PARTIAL", combine_notes_overwrite, 0); + + o->local_ref = resolve_ref("NOTES_MERGE_REF", sha1, 0, 0); + if (!o->local_ref) + die("Failed to resolve NOTES_MERGE_REF"); + + if (notes_merge_commit(o, t, partial, sha1)) + die("Failed to finalize notes merge"); + + /* Reuse existing commit message in reflog message */ + memset(&pretty_ctx, 0, sizeof(pretty_ctx)); + format_commit_message(partial, "%s", &msg, &pretty_ctx); + strbuf_trim(&msg); + strbuf_insert(&msg, 0, "notes: ", 7); + update_ref(msg.buf, o->local_ref, sha1, + is_null_sha1(parent_sha1) ? NULL : parent_sha1, + 0, DIE_ON_ERR); + + free_notes(t); + strbuf_release(&msg); + return merge_abort(o); +} + +static int merge(int argc, const char **argv, const char *prefix) +{ + struct strbuf remote_ref = STRBUF_INIT, msg = STRBUF_INIT; + unsigned char result_sha1[20]; + struct notes_tree *t; + struct notes_merge_options o; + int do_merge = 0, do_commit = 0, do_abort = 0; + int verbosity = 0, result; + const char *strategy = NULL; + struct option options[] = { + OPT_GROUP("General options"), + OPT__VERBOSITY(&verbosity), + OPT_GROUP("Merge options"), + OPT_STRING('s', "strategy", &strategy, "strategy", + "resolve notes conflicts using the given strategy " + "(manual/ours/theirs/union/cat_sort_uniq)"), + OPT_GROUP("Committing unmerged notes"), + { OPTION_BOOLEAN, 0, "commit", &do_commit, NULL, + "finalize notes merge by committing unmerged notes", + PARSE_OPT_NOARG | PARSE_OPT_NONEG }, + OPT_GROUP("Aborting notes merge resolution"), + { OPTION_BOOLEAN, 0, "abort", &do_abort, NULL, + "abort notes merge", + PARSE_OPT_NOARG | PARSE_OPT_NONEG }, + OPT_END() + }; + + argc = parse_options(argc, argv, prefix, options, + git_notes_merge_usage, 0); + + if (strategy || do_commit + do_abort == 0) + do_merge = 1; + if (do_merge + do_commit + do_abort != 1) { + error("cannot mix --commit, --abort or -s/--strategy"); + usage_with_options(git_notes_merge_usage, options); + } + + if (do_merge && argc != 1) { + error("Must specify a notes ref to merge"); + usage_with_options(git_notes_merge_usage, options); + } else if (!do_merge && argc) { + error("too many parameters"); + usage_with_options(git_notes_merge_usage, options); + } + + init_notes_merge_options(&o); + o.verbosity = verbosity + NOTES_MERGE_VERBOSITY_DEFAULT; + + if (do_abort) + return merge_abort(&o); + if (do_commit) + return merge_commit(&o); + + o.local_ref = default_notes_ref(); + strbuf_addstr(&remote_ref, argv[0]); + expand_notes_ref(&remote_ref); + o.remote_ref = remote_ref.buf; + + if (strategy) { + if (!strcmp(strategy, "manual")) + o.strategy = NOTES_MERGE_RESOLVE_MANUAL; + else if (!strcmp(strategy, "ours")) + o.strategy = NOTES_MERGE_RESOLVE_OURS; + else if (!strcmp(strategy, "theirs")) + o.strategy = NOTES_MERGE_RESOLVE_THEIRS; + else if (!strcmp(strategy, "union")) + o.strategy = NOTES_MERGE_RESOLVE_UNION; + else if (!strcmp(strategy, "cat_sort_uniq")) + o.strategy = NOTES_MERGE_RESOLVE_CAT_SORT_UNIQ; + else { + error("Unknown -s/--strategy: %s", strategy); + usage_with_options(git_notes_merge_usage, options); + } + } + + t = init_notes_check("merge"); + + strbuf_addf(&msg, "notes: Merged notes from %s into %s", + remote_ref.buf, default_notes_ref()); + strbuf_add(&(o.commit_msg), msg.buf + 7, msg.len - 7); /* skip "notes: " */ + + result = notes_merge(&o, t, result_sha1); + + if (result >= 0) /* Merge resulted (trivially) in result_sha1 */ + /* Update default notes ref with new commit */ + update_ref(msg.buf, default_notes_ref(), result_sha1, NULL, + 0, DIE_ON_ERR); + else { /* Merge has unresolved conflicts */ + /* Update .git/NOTES_MERGE_PARTIAL with partial merge result */ + update_ref(msg.buf, "NOTES_MERGE_PARTIAL", result_sha1, NULL, + 0, DIE_ON_ERR); + /* Store ref-to-be-updated into .git/NOTES_MERGE_REF */ + if (create_symref("NOTES_MERGE_REF", default_notes_ref(), NULL)) + die("Failed to store link to current notes ref (%s)", + default_notes_ref()); + printf("Automatic notes merge failed. Fix conflicts in %s and " + "commit the result with 'git notes merge --commit', or " + "abort the merge with 'git notes merge --abort'.\n", + git_path(NOTES_MERGE_WORKTREE)); + } + + free_notes(t); + strbuf_release(&remote_ref); + strbuf_release(&msg); + return result < 0; /* return non-zero on conflicts */ +} + static int remove_cmd(int argc, const char **argv, const char *prefix) { struct option options[] = { @@ -804,9 +990,8 @@ static int prune(int argc, const char **argv, const char *prefix) struct notes_tree *t; int show_only = 0, verbose = 0; struct option options[] = { - OPT_BOOLEAN('n', "dry-run", &show_only, - "do not remove, show only"), - OPT_BOOLEAN('v', "verbose", &verbose, "report pruned notes"), + OPT__DRY_RUN(&show_only, "do not remove, show only"), + OPT__VERBOSE(&verbose, "report pruned notes"), OPT_END() }; @@ -828,6 +1013,21 @@ static int prune(int argc, const char **argv, const char *prefix) return 0; } +static int get_ref(int argc, const char **argv, const char *prefix) +{ + struct option options[] = { OPT_END() }; + argc = parse_options(argc, argv, prefix, options, + git_notes_get_ref_usage, 0); + + if (argc) { + error("too many parameters"); + usage_with_options(git_notes_get_ref_usage, options); + } + + puts(default_notes_ref()); + return 0; +} + int cmd_notes(int argc, const char **argv, const char *prefix) { int result; @@ -844,13 +1044,8 @@ int cmd_notes(int argc, const char **argv, const char *prefix) if (override_notes_ref) { struct strbuf sb = STRBUF_INIT; - if (!prefixcmp(override_notes_ref, "refs/notes/")) - /* we're happy */; - else if (!prefixcmp(override_notes_ref, "notes/")) - strbuf_addstr(&sb, "refs/"); - else - strbuf_addstr(&sb, "refs/notes/"); strbuf_addstr(&sb, override_notes_ref); + expand_notes_ref(&sb); setenv("GIT_NOTES_REF", sb.buf, 1); strbuf_release(&sb); } @@ -865,10 +1060,14 @@ int cmd_notes(int argc, const char **argv, const char *prefix) result = append_edit(argc, argv, prefix); else if (!strcmp(argv[0], "show")) result = show(argc, argv, prefix); + else if (!strcmp(argv[0], "merge")) + result = merge(argc, argv, prefix); else if (!strcmp(argv[0], "remove")) result = remove_cmd(argc, argv, prefix); else if (!strcmp(argv[0], "prune")) result = prune(argc, argv, prefix); + else if (!strcmp(argv[0], "get-ref")) + result = get_ref(argc, argv, prefix); else { result = error("Unknown subcommand: %s", argv[0]); usage_with_options(git_notes_usage, options); diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c index f8eba53c82..b0503b202a 100644 --- a/builtin/pack-objects.c +++ b/builtin/pack-objects.c @@ -16,11 +16,7 @@ #include "list-objects.h" #include "progress.h" #include "refs.h" - -#ifndef NO_PTHREADS -#include <pthread.h> #include "thread-utils.h" -#endif static const char pack_usage[] = "git pack-objects [ -q | --progress | --all-progress ]\n" @@ -1298,9 +1294,23 @@ static int try_delta(struct unpacked *trg, struct unpacked *src, read_lock(); src->data = read_sha1_file(src_entry->idx.sha1, &type, &sz); read_unlock(); - if (!src->data) + if (!src->data) { + if (src_entry->preferred_base) { + static int warned = 0; + if (!warned++) + warning("object %s cannot be read", + sha1_to_hex(src_entry->idx.sha1)); + /* + * Those objects are not included in the + * resulting pack. Be resilient and ignore + * them if they can't be read, in case the + * pack could be created nevertheless. + */ + return 0; + } die("object %s cannot be read", sha1_to_hex(src_entry->idx.sha1)); + } if (sz != src_size) die("object %s inconsistent object length (%lu vs %lu)", sha1_to_hex(src_entry->idx.sha1), sz, src_size); @@ -1529,7 +1539,7 @@ static void try_to_free_from_threads(size_t size) read_unlock(); } -try_to_free_t old_try_to_free_routine; +static try_to_free_t old_try_to_free_routine; /* * The main thread waits on the condition that (at least) one of the workers diff --git a/builtin/prune.c b/builtin/prune.c index 99218ba49e..e65690ba37 100644 --- a/builtin/prune.c +++ b/builtin/prune.c @@ -125,9 +125,8 @@ int cmd_prune(int argc, const char **argv, const char *prefix) { struct rev_info revs; const struct option options[] = { - OPT_BOOLEAN('n', "dry-run", &show_only, - "do not remove, show only"), - OPT_BOOLEAN('v', "verbose", &verbose, "report pruned objects"), + OPT__DRY_RUN(&show_only, "do not remove, show only"), + OPT__VERBOSE(&verbose, "report pruned objects"), OPT_DATE(0, "expire", &expire, "expire objects older than <time>"), OPT_END() diff --git a/builtin/read-tree.c b/builtin/read-tree.c index eb1e3e7467..73c89ed15b 100644 --- a/builtin/read-tree.c +++ b/builtin/read-tree.c @@ -109,7 +109,7 @@ int cmd_read_tree(int argc, const char **argv, const char *unused_prefix) PARSE_OPT_NONEG, index_output_cb }, OPT_SET_INT(0, "empty", &read_empty, "only empty the index", 1), - OPT__VERBOSE(&opts.verbose_update), + OPT__VERBOSE(&opts.verbose_update, "be verbose"), OPT_GROUP("Merging"), OPT_SET_INT('m', NULL, &opts.merge, "perform a merge in addition to a read", 1), diff --git a/builtin/remote-ext.c b/builtin/remote-ext.c new file mode 100644 index 0000000000..1f773171cb --- /dev/null +++ b/builtin/remote-ext.c @@ -0,0 +1,246 @@ +#include "git-compat-util.h" +#include "transport.h" +#include "run-command.h" + +/* + * URL syntax: + * 'command [arg1 [arg2 [...]]]' Invoke command with given arguments. + * Special characters: + * '% ': Literal space in argument. + * '%%': Literal percent sign. + * '%S': Name of service (git-upload-pack/git-upload-archive/ + * git-receive-pack. + * '%s': Same as \s, but with possible git- prefix stripped. + * '%G': Only allowed as first 'character' of argument. Do not pass this + * Argument to command, instead send this as name of repository + * in in-line git://-style request (also activates sending this + * style of request). + * '%V': Only allowed as first 'character' of argument. Used in + * conjunction with '%G': Do not pass this argument to command, + * instead send this as vhost in git://-style request (note: does + * not activate sending git:// style request). + */ + +static char *git_req; +static char *git_req_vhost; + +static char *strip_escapes(const char *str, const char *service, + const char **next) +{ + size_t rpos = 0; + int escape = 0; + char special = 0; + size_t pslen = 0; + size_t pSlen = 0; + size_t psoff = 0; + struct strbuf ret = STRBUF_INIT; + + /* Calculate prefix length for \s and lengths for \s and \S */ + if (!strncmp(service, "git-", 4)) + psoff = 4; + pSlen = strlen(service); + pslen = pSlen - psoff; + + /* Pass the service to command. */ + setenv("GIT_EXT_SERVICE", service, 1); + setenv("GIT_EXT_SERVICE_NOPREFIX", service + psoff, 1); + + /* Scan the length of argument. */ + while (str[rpos] && (escape || str[rpos] != ' ')) { + if (escape) { + switch (str[rpos]) { + case ' ': + case '%': + case 's': + case 'S': + break; + case 'G': + case 'V': + special = str[rpos]; + if (rpos == 1) + break; + /* Fall-through to error. */ + default: + die("Bad remote-ext placeholder '%%%c'.", + str[rpos]); + } + escape = 0; + } else + escape = (str[rpos] == '%'); + rpos++; + } + if (escape && !str[rpos]) + die("remote-ext command has incomplete placeholder"); + *next = str + rpos; + if (**next == ' ') + ++*next; /* Skip over space */ + + /* + * Do the actual placeholder substitution. The string will be short + * enough not to overflow integers. + */ + rpos = special ? 2 : 0; /* Skip first 2 bytes in specials. */ + escape = 0; + while (str[rpos] && (escape || str[rpos] != ' ')) { + if (escape) { + switch (str[rpos]) { + case ' ': + case '%': + strbuf_addch(&ret, str[rpos]); + break; + case 's': + strbuf_addstr(&ret, service + psoff); + break; + case 'S': + strbuf_addstr(&ret, service); + break; + } + escape = 0; + } else + switch (str[rpos]) { + case '%': + escape = 1; + break; + default: + strbuf_addch(&ret, str[rpos]); + break; + } + rpos++; + } + switch (special) { + case 'G': + git_req = strbuf_detach(&ret, NULL); + return NULL; + case 'V': + git_req_vhost = strbuf_detach(&ret, NULL); + return NULL; + default: + return strbuf_detach(&ret, NULL); + } +} + +/* Should be enough... */ +#define MAXARGUMENTS 256 + +static const char **parse_argv(const char *arg, const char *service) +{ + int arguments = 0; + int i; + const char **ret; + char *temparray[MAXARGUMENTS + 1]; + + while (*arg) { + char *expanded; + if (arguments == MAXARGUMENTS) + die("remote-ext command has too many arguments"); + expanded = strip_escapes(arg, service, &arg); + if (expanded) + temparray[arguments++] = expanded; + } + + ret = xmalloc((arguments + 1) * sizeof(char *)); + for (i = 0; i < arguments; i++) + ret[i] = temparray[i]; + ret[arguments] = NULL; + return ret; +} + +static void send_git_request(int stdin_fd, const char *serv, const char *repo, + const char *vhost) +{ + size_t bufferspace; + size_t wpos = 0; + char *buffer; + + /* + * Request needs 12 bytes extra if there is vhost (xxxx \0host=\0) and + * 6 bytes extra (xxxx \0) if there is no vhost. + */ + if (vhost) + bufferspace = strlen(serv) + strlen(repo) + strlen(vhost) + 12; + else + bufferspace = strlen(serv) + strlen(repo) + 6; + + if (bufferspace > 0xFFFF) + die("Request too large to send"); + buffer = xmalloc(bufferspace); + + /* Make the packet. */ + wpos = sprintf(buffer, "%04x%s %s%c", (unsigned)bufferspace, + serv, repo, 0); + + /* Add vhost if any. */ + if (vhost) + sprintf(buffer + wpos, "host=%s%c", vhost, 0); + + /* Send the request */ + if (write_in_full(stdin_fd, buffer, bufferspace) < 0) + die_errno("Failed to send request"); + + free(buffer); +} + +static int run_child(const char *arg, const char *service) +{ + int r; + struct child_process child; + + memset(&child, 0, sizeof(child)); + child.in = -1; + child.out = -1; + child.err = 0; + child.argv = parse_argv(arg, service); + + if (start_command(&child) < 0) + die("Can't run specified command"); + + if (git_req) + send_git_request(child.in, service, git_req, git_req_vhost); + + r = bidirectional_transfer_loop(child.out, child.in); + if (!r) + r = finish_command(&child); + else + finish_command(&child); + return r; +} + +#define MAXCOMMAND 4096 + +static int command_loop(const char *child) +{ + char buffer[MAXCOMMAND]; + + while (1) { + size_t length; + if (!fgets(buffer, MAXCOMMAND - 1, stdin)) { + if (ferror(stdin)) + die("Comammand input error"); + exit(0); + } + /* Strip end of line characters. */ + length = strlen(buffer); + while (isspace((unsigned char)buffer[length - 1])) + buffer[--length] = 0; + + if (!strcmp(buffer, "capabilities")) { + printf("*connect\n\n"); + fflush(stdout); + } else if (!strncmp(buffer, "connect ", 8)) { + printf("\n"); + fflush(stdout); + return run_child(child, buffer + 8); + } else { + fprintf(stderr, "Bad command"); + return 1; + } + } +} + +int cmd_remote_ext(int argc, const char **argv, const char *prefix) +{ + if (argc != 3) + die("Expected two arguments"); + + return command_loop(argv[2]); +} diff --git a/builtin/remote-fd.c b/builtin/remote-fd.c new file mode 100644 index 0000000000..1f2467bdb7 --- /dev/null +++ b/builtin/remote-fd.c @@ -0,0 +1,79 @@ +#include "git-compat-util.h" +#include "transport.h" + +/* + * URL syntax: + * 'fd::<inoutfd>[/<anything>]' Read/write socket pair + * <inoutfd>. + * 'fd::<infd>,<outfd>[/<anything>]' Read pipe <infd> and write + * pipe <outfd>. + * [foo] indicates 'foo' is optional. <anything> is any string. + * + * The data output to <outfd>/<inoutfd> should be passed unmolested to + * git-receive-pack/git-upload-pack/git-upload-archive and output of + * git-receive-pack/git-upload-pack/git-upload-archive should be passed + * unmolested to <infd>/<inoutfd>. + * + */ + +#define MAXCOMMAND 4096 + +static void command_loop(int input_fd, int output_fd) +{ + char buffer[MAXCOMMAND]; + + while (1) { + size_t i; + if (!fgets(buffer, MAXCOMMAND - 1, stdin)) { + if (ferror(stdin)) + die("Input error"); + return; + } + /* Strip end of line characters. */ + i = strlen(buffer); + while (i > 0 && isspace(buffer[i - 1])) + buffer[--i] = 0; + + if (!strcmp(buffer, "capabilities")) { + printf("*connect\n\n"); + fflush(stdout); + } else if (!strncmp(buffer, "connect ", 8)) { + printf("\n"); + fflush(stdout); + if (bidirectional_transfer_loop(input_fd, + output_fd)) + die("Copying data between file descriptors failed"); + return; + } else { + die("Bad command: %s", buffer); + } + } +} + +int cmd_remote_fd(int argc, const char **argv, const char *prefix) +{ + int input_fd = -1; + int output_fd = -1; + char *end; + + if (argc != 3) + die("Expected two arguments"); + + input_fd = (int)strtoul(argv[2], &end, 10); + + if ((end == argv[2]) || (*end != ',' && *end != '/' && *end)) + die("Bad URL syntax"); + + if (*end == '/' || !*end) { + output_fd = input_fd; + } else { + char *end2; + output_fd = (int)strtoul(end + 1, &end2, 10); + + if ((end2 == end + 1) || (*end2 != '/' && *end2)) + die("Bad URL syntax"); + } + + command_loop(input_fd, output_fd); + return 0; +} diff --git a/builtin/remote.c b/builtin/remote.c index e9a6e09257..cb26080956 100644 --- a/builtin/remote.c +++ b/builtin/remote.c @@ -507,7 +507,7 @@ static int add_branch_for_removal(const char *refname, return 0; } - /* don't delete non-remote refs */ + /* don't delete non-remote-tracking refs */ if (prefixcmp(refname, "refs/remotes")) { /* advise user how to delete local branches */ if (!prefixcmp(refname, "refs/heads/")) @@ -791,9 +791,9 @@ static int rm(int argc, const char **argv) if (skipped.nr) { fprintf(stderr, skipped.nr == 1 ? - "Note: A non-remote branch was not removed; " + "Note: A branch outside the refs/remotes/ hierarchy was not removed;\n" "to delete it, use:\n" : - "Note: Non-remote branches were not removed; " + "Note: Some branches outside the refs/remotes/ hierarchy were not removed;\n" "to delete them, use:\n"); for (i = 0; i < skipped.nr; i++) fprintf(stderr, " git branch -d %s\n", @@ -1200,7 +1200,7 @@ static int prune(int argc, const char **argv) { int dry_run = 0, result = 0; struct option options[] = { - OPT__DRY_RUN(&dry_run), + OPT__DRY_RUN(&dry_run, "dry run"), OPT_END() }; @@ -1512,7 +1512,7 @@ static int show_all(void) int cmd_remote(int argc, const char **argv, const char *prefix) { struct option options[] = { - OPT_BOOLEAN('v', "verbose", &verbose, "be verbose; must be placed before a subcommand"), + OPT__VERBOSE(&verbose, "be verbose; must be placed before a subcommand"), OPT_END() }; int result; diff --git a/builtin/reset.c b/builtin/reset.c index 0037be4693..5de2bceeec 100644 --- a/builtin/reset.c +++ b/builtin/reset.c @@ -243,7 +243,7 @@ int cmd_reset(int argc, const char **argv, const char *prefix) struct commit *commit; char *reflog_action, msg[1024]; const struct option options[] = { - OPT__QUIET(&quiet), + OPT__QUIET(&quiet, "be quiet, only report errors"), OPT_SET_INT(0, "mixed", &reset_type, "reset HEAD and index", MIXED), OPT_SET_INT(0, "soft", &reset_type, "reset only HEAD", SOFT), diff --git a/builtin/revert.c b/builtin/revert.c index 57b51e4a0e..bb6e9e83b7 100644 --- a/builtin/revert.c +++ b/builtin/revert.c @@ -547,6 +547,21 @@ static void prepare_revs(struct rev_info *revs) die("empty commit set passed"); } +static void read_and_refresh_cache(const char *me) +{ + static struct lock_file index_lock; + int index_fd = hold_locked_index(&index_lock, 0); + if (read_index_preload(&the_index, NULL) < 0) + die("git %s: failed to read the index", me); + refresh_index(&the_index, REFRESH_QUIET|REFRESH_UNMERGED, NULL, NULL, NULL); + if (the_index.cache_changed) { + if (write_index(&the_index, index_fd) || + commit_locked_index(&index_lock)) + die("git %s: failed to refresh the index", me); + } + rollback_lock_file(&index_lock); +} + static int revert_or_cherry_pick(int argc, const char **argv) { struct rev_info revs; @@ -567,8 +582,7 @@ static int revert_or_cherry_pick(int argc, const char **argv) die("cherry-pick --ff cannot be used with --edit"); } - if (read_cache() < 0) - die("git %s: failed to read the index", me); + read_and_refresh_cache(me); prepare_revs(&revs); diff --git a/builtin/rm.c b/builtin/rm.c index f3772c84de..c7b7bb37a2 100644 --- a/builtin/rm.c +++ b/builtin/rm.c @@ -139,10 +139,10 @@ static int show_only = 0, force = 0, index_only = 0, recursive = 0, quiet = 0; static int ignore_unmatch = 0; static struct option builtin_rm_options[] = { - OPT__DRY_RUN(&show_only), - OPT__QUIET(&quiet), + OPT__DRY_RUN(&show_only, "dry run"), + OPT__QUIET(&quiet, "do not list removed files"), OPT_BOOLEAN( 0 , "cached", &index_only, "only remove from the index"), - OPT_BOOLEAN('f', "force", &force, "override the up-to-date check"), + OPT__FORCE(&force, "override the up-to-date check"), OPT_BOOLEAN('r', NULL, &recursive, "allow recursive removal"), OPT_BOOLEAN( 0 , "ignore-unmatch", &ignore_unmatch, "exit with a zero status even if nothing matched"), diff --git a/builtin/show-ref.c b/builtin/show-ref.c index be9b512eeb..45f0340c3e 100644 --- a/builtin/show-ref.c +++ b/builtin/show-ref.c @@ -193,7 +193,8 @@ static const struct option show_ref_options[] = { "only show SHA1 hash using <n> digits", PARSE_OPT_OPTARG, &hash_callback }, OPT__ABBREV(&abbrev), - OPT__QUIET(&quiet), + OPT__QUIET(&quiet, + "do not print results to stdout (useful with --verify)"), { OPTION_CALLBACK, 0, "exclude-existing", &exclude_existing_arg, "pattern", "show refs from stdin that aren't in local repository", PARSE_OPT_OPTARG | PARSE_OPT_NONEG, exclude_existing_callback }, diff --git a/builtin/symbolic-ref.c b/builtin/symbolic-ref.c index ca855a5eb2..dea849c3c5 100644 --- a/builtin/symbolic-ref.c +++ b/builtin/symbolic-ref.c @@ -30,7 +30,8 @@ int cmd_symbolic_ref(int argc, const char **argv, const char *prefix) int quiet = 0; const char *msg = NULL; struct option options[] = { - OPT__QUIET(&quiet), + OPT__QUIET(&quiet, + "suppress error message for non-symbolic (detached) refs"), OPT_STRING('m', NULL, &msg, "reason", "reason of the update"), OPT_END(), }; diff --git a/builtin/tag.c b/builtin/tag.c index d311491e49..aa1f87d47a 100644 --- a/builtin/tag.c +++ b/builtin/tag.c @@ -29,8 +29,6 @@ struct tag_filter { struct commit_list *with_commit; }; -#define PGP_SIGNATURE "-----BEGIN PGP SIGNATURE-----" - static int show_reference(const char *refname, const unsigned char *sha1, int flag, void *cb_data) { @@ -70,9 +68,9 @@ static int show_reference(const char *refname, const unsigned char *sha1, return 0; } /* only take up to "lines" lines, and strip the signature */ + size = parse_signature(buf, size); for (i = 0, sp += 2; - i < filter->lines && sp < buf + size && - prefixcmp(sp, PGP_SIGNATURE "\n"); + i < filter->lines && sp < buf + size; i++) { if (i) printf("\n "); @@ -242,8 +240,7 @@ static void write_tag_body(int fd, const unsigned char *sha1) { unsigned long size; enum object_type type; - char *buf, *sp, *eob; - size_t len; + char *buf, *sp; buf = read_sha1_file(sha1, &type, &size); if (!buf) @@ -256,12 +253,7 @@ static void write_tag_body(int fd, const unsigned char *sha1) return; } sp += 2; /* skip the 2 LFs */ - eob = strstr(sp, "\n" PGP_SIGNATURE "\n"); - if (eob) - len = eob - sp; - else - len = buf + size - sp; - write_or_die(fd, sp, len); + write_or_die(fd, sp, parse_signature(sp, buf + size - sp)); free(buf); } @@ -390,7 +382,7 @@ int cmd_tag(int argc, const char **argv, const char *prefix) OPT_BOOLEAN('s', NULL, &sign, "annotated and GPG-signed tag"), OPT_STRING('u', NULL, &keyid, "key-id", "use another key to sign the tag"), - OPT_BOOLEAN('f', "force", &force, "replace the tag if exists"), + OPT__FORCE(&force, "replace the tag if exists"), OPT_GROUP("Tag listing options"), { diff --git a/builtin/update-index.c b/builtin/update-index.c index 62d9f3f0fa..200c7efed1 100644 --- a/builtin/update-index.c +++ b/builtin/update-index.c @@ -589,6 +589,9 @@ int cmd_update_index(int argc, const char **argv, const char *prefix) int lock_error = 0; struct lock_file *lock_file; + if (argc == 2 && !strcmp(argv[1], "-h")) + usage(update_index_usage); + git_config(git_default_config, NULL); /* We can't free this memory, it becomes part of a linked list parsed atexit() */ diff --git a/builtin/update-server-info.c b/builtin/update-server-info.c index 2b3fddcc69..b90dce6358 100644 --- a/builtin/update-server-info.c +++ b/builtin/update-server-info.c @@ -11,8 +11,7 @@ int cmd_update_server_info(int argc, const char **argv, const char *prefix) { int force = 0; struct option options[] = { - OPT_BOOLEAN('f', "force", &force, - "update the info files from scratch"), + OPT__FORCE(&force, "update the info files from scratch"), OPT_END() }; diff --git a/builtin/verify-tag.c b/builtin/verify-tag.c index 9f482c29f5..3134766049 100644 --- a/builtin/verify-tag.c +++ b/builtin/verify-tag.c @@ -17,13 +17,11 @@ static const char * const verify_tag_usage[] = { NULL }; -#define PGP_SIGNATURE "-----BEGIN PGP SIGNATURE-----" - static int run_gpg_verify(const char *buf, unsigned long size, int verbose) { struct child_process gpg; const char *args_gpg[] = {"gpg", "--verify", "FILE", "-", NULL}; - char path[PATH_MAX], *eol; + char path[PATH_MAX]; size_t len; int fd, ret; @@ -37,11 +35,7 @@ static int run_gpg_verify(const char *buf, unsigned long size, int verbose) close(fd); /* find the length without signature */ - len = 0; - while (len < size && prefixcmp(buf + len, PGP_SIGNATURE)) { - eol = memchr(buf + len, '\n', size - len); - len += eol ? eol - (buf + len) + 1 : size - len; - } + len = parse_signature(buf, size); if (verbose) write_in_full(1, buf, len); @@ -93,7 +87,7 @@ int cmd_verify_tag(int argc, const char **argv, const char *prefix) { int i = 1, verbose = 0, had_error = 0; const struct option verify_tag_options[] = { - OPT__VERBOSE(&verbose), + OPT__VERBOSE(&verbose, "print tag contents"), OPT_END() }; @@ -545,6 +545,7 @@ extern int assume_unchanged; extern int prefer_symlink_refs; extern int log_all_ref_updates; extern int warn_ambiguous_refs; +extern int unique_abbrev_extra_length; extern int shared_repository; extern const char *apply_default_whitespace; extern const char *apply_default_ignorewhitespace; @@ -859,7 +860,7 @@ struct cache_def { extern int has_symlink_leading_path(const char *name, int len); extern int threaded_has_symlink_leading_path(struct cache_def *, const char *, int); -extern int has_symlink_or_noent_leading_path(const char *name, int len); +extern int check_leading_path(const char *name, int len); extern int has_dirs_only_path(const char *name, int len, int prefix_len); extern void schedule_dir_for_removal(const char *name, int len); extern void remove_scheduled_dirs(void); @@ -1003,6 +1004,9 @@ extern int git_env_bool(const char *, int); extern int git_config_system(void); extern int git_config_global(void); extern int config_error_nonbool(const char *); +extern const char *get_log_output_encoding(void); +extern const char *get_commit_output_encoding(void); + extern const char *config_exclusive_filename; #define MAX_GITNAME (1000) @@ -49,6 +49,19 @@ struct commit *lookup_commit(const unsigned char *sha1) return check_commit(obj, sha1, 0); } +struct commit *lookup_commit_reference_by_name(const char *name) +{ + unsigned char sha1[20]; + struct commit *commit; + + if (get_sha1(name, sha1)) + return NULL; + commit = lookup_commit_reference(sha1); + if (!commit || parse_commit(commit)) + return NULL; + return commit; +} + static unsigned long parse_commit_date(const char *buf, const char *tail) { const char *dateptr; @@ -137,12 +150,8 @@ struct commit_graft *read_graft_line(char *buf, int len) buf[--len] = '\0'; if (buf[0] == '#' || buf[0] == '\0') return NULL; - if ((len + 1) % 41) { - bad_graft_data: - error("bad graft data: %s", buf); - free(graft); - return NULL; - } + if ((len + 1) % 41) + goto bad_graft_data; i = (len + 1) / 41 - 1; graft = xmalloc(sizeof(*graft) + 20 * i); graft->nr_parent = i; @@ -155,6 +164,11 @@ struct commit_graft *read_graft_line(char *buf, int len) goto bad_graft_data; } return graft; + +bad_graft_data: + error("bad graft data: %s", buf); + free(graft); + return NULL; } static int read_graft_file(const char *graft_file) @@ -36,6 +36,7 @@ struct commit *lookup_commit(const unsigned char *sha1); struct commit *lookup_commit_reference(const unsigned char *sha1); struct commit *lookup_commit_reference_gently(const unsigned char *sha1, int quiet); +struct commit *lookup_commit_reference_by_name(const char *name); int parse_commit_buffer(struct commit *item, void *buffer, unsigned long size); @@ -76,6 +77,7 @@ struct pretty_print_context int need_8bit_cte; int show_notes; struct reflog_walk_info *reflog_info; + const char *output_encoding; }; struct userformat_want { @@ -84,6 +86,8 @@ struct userformat_want { extern int has_non_ascii(const char *text); struct rev_info; /* in revision.h, it circularly uses enum cmit_fmt */ +extern char *logmsg_reencode(const struct commit *commit, + const char *output_encoding); extern char *reencode_commit_message(const struct commit *commit, const char **encoding_p); extern void get_commit_format(const char *arg, struct rev_info *); diff --git a/compat/inet_ntop.c b/compat/inet_ntop.c index f44498258d..ea249c6ac6 100644 --- a/compat/inet_ntop.c +++ b/compat/inet_ntop.c @@ -17,9 +17,9 @@ #include <errno.h> #include <sys/types.h> -#include <sys/socket.h> -#include <netinet/in.h> -#include <arpa/inet.h> + +#include "../git-compat-util.h" + #include <stdio.h> #include <string.h> @@ -50,10 +50,7 @@ * Paul Vixie, 1996. */ static const char * -inet_ntop4(src, dst, size) - const u_char *src; - char *dst; - size_t size; +inet_ntop4(const u_char *src, char *dst, size_t size) { static const char fmt[] = "%u.%u.%u.%u"; char tmp[sizeof "255.255.255.255"]; @@ -78,10 +75,7 @@ inet_ntop4(src, dst, size) * Paul Vixie, 1996. */ static const char * -inet_ntop6(src, dst, size) - const u_char *src; - char *dst; - size_t size; +inet_ntop6(const u_char *src, char *dst, size_t size) { /* * Note that int32_t and int16_t need only be "at least" large enough @@ -178,11 +172,7 @@ inet_ntop6(src, dst, size) * Paul Vixie, 1996. */ const char * -inet_ntop(af, src, dst, size) - int af; - const void *src; - char *dst; - size_t size; +inet_ntop(int af, const void *src, char *dst, size_t size) { switch (af) { case AF_INET: diff --git a/compat/inet_pton.c b/compat/inet_pton.c index 4078fc0877..2ec995e63d 100644 --- a/compat/inet_pton.c +++ b/compat/inet_pton.c @@ -17,9 +17,9 @@ #include <errno.h> #include <sys/types.h> -#include <sys/socket.h> -#include <netinet/in.h> -#include <arpa/inet.h> + +#include "../git-compat-util.h" + #include <stdio.h> #include <string.h> @@ -41,7 +41,9 @@ */ static int inet_pton4(const char *src, unsigned char *dst); +#ifndef NO_IPV6 static int inet_pton6(const char *src, unsigned char *dst); +#endif /* int * inet_pton4(src, dst) diff --git a/compat/mingw.c b/compat/mingw.c index 6590f33cc8..fdbf093f6e 100644 --- a/compat/mingw.c +++ b/compat/mingw.c @@ -198,9 +198,10 @@ static inline time_t filetime_to_time_t(const FILETIME *ft) */ static int do_lstat(int follow, const char *file_name, struct stat *buf) { + int err; WIN32_FILE_ATTRIBUTE_DATA fdata; - if (!(errno = get_file_attr(file_name, &fdata))) { + if (!(err = get_file_attr(file_name, &fdata))) { buf->st_ino = 0; buf->st_gid = 0; buf->st_uid = 0; @@ -233,6 +234,7 @@ static int do_lstat(int follow, const char *file_name, struct stat *buf) } return 0; } + errno = err; return -1; } @@ -408,71 +410,6 @@ int pipe(int filedes[2]) return 0; } -int poll(struct pollfd *ufds, unsigned int nfds, int timeout) -{ - int i, pending; - - if (timeout >= 0) { - if (nfds == 0) { - Sleep(timeout); - return 0; - } - return errno = EINVAL, error("poll timeout not supported"); - } - - /* When there is only one fd to wait for, then we pretend that - * input is available and let the actual wait happen when the - * caller invokes read(). - */ - if (nfds == 1) { - if (!(ufds[0].events & POLLIN)) - return errno = EINVAL, error("POLLIN not set"); - ufds[0].revents = POLLIN; - return 0; - } - -repeat: - pending = 0; - for (i = 0; i < nfds; i++) { - DWORD avail = 0; - HANDLE h = (HANDLE) _get_osfhandle(ufds[i].fd); - if (h == INVALID_HANDLE_VALUE) - return -1; /* errno was set */ - - if (!(ufds[i].events & POLLIN)) - return errno = EINVAL, error("POLLIN not set"); - - /* this emulation works only for pipes */ - if (!PeekNamedPipe(h, NULL, 0, NULL, &avail, NULL)) { - int err = GetLastError(); - if (err == ERROR_BROKEN_PIPE) { - ufds[i].revents = POLLHUP; - pending++; - } else { - errno = EINVAL; - return error("PeekNamedPipe failed," - " GetLastError: %u", err); - } - } else if (avail) { - ufds[i].revents = POLLIN; - pending++; - } else - ufds[i].revents = 0; - } - if (!pending) { - /* The only times that we spin here is when the process - * that is connected through the pipes is waiting for - * its own input data to become available. But since - * the process (pack-objects) is itself CPU intensive, - * it will happily pick up the time slice that we are - * relinquishing here. - */ - Sleep(0); - goto repeat; - } - return 0; -} - struct tm *gmtime_r(const time_t *timep, struct tm *result) { /* gmtime() in MSVCRT.DLL is thread-safe, but not reentrant */ @@ -702,6 +639,14 @@ static int env_compare(const void *a, const void *b) return strcasecmp(*ea, *eb); } +struct pinfo_t { + struct pinfo_t *next; + pid_t pid; + HANDLE proc; +} pinfo_t; +struct pinfo_t *pinfo = NULL; +CRITICAL_SECTION pinfo_cs; + static pid_t mingw_spawnve_fd(const char *cmd, const char **argv, char **env, const char *dir, int prepend_cmd, int fhin, int fhout, int fherr) @@ -794,7 +739,26 @@ static pid_t mingw_spawnve_fd(const char *cmd, const char **argv, char **env, return -1; } CloseHandle(pi.hThread); - return (pid_t)pi.hProcess; + + /* + * The process ID is the human-readable identifier of the process + * that we want to present in log and error messages. The handle + * is not useful for this purpose. But we cannot close it, either, + * because it is not possible to turn a process ID into a process + * handle after the process terminated. + * Keep the handle in a list for waitpid. + */ + EnterCriticalSection(&pinfo_cs); + { + struct pinfo_t *info = xmalloc(sizeof(struct pinfo_t)); + info->pid = pi.dwProcessId; + info->proc = pi.hProcess; + info->next = pinfo; + pinfo = info; + } + LeaveCriticalSection(&pinfo_cs); + + return (pid_t)pi.dwProcessId; } static pid_t mingw_spawnve(const char *cmd, const char **argv, char **env, @@ -909,6 +873,25 @@ void mingw_execv(const char *cmd, char *const *argv) mingw_execve(cmd, argv, environ); } +int mingw_kill(pid_t pid, int sig) +{ + if (pid > 0 && sig == SIGTERM) { + HANDLE h = OpenProcess(PROCESS_TERMINATE, FALSE, pid); + + if (TerminateProcess(h, -1)) { + CloseHandle(h); + return 0; + } + + errno = err_win_to_posix(GetLastError()); + CloseHandle(h); + return -1; + } + + errno = EINVAL; + return -1; +} + static char **copy_environ(void) { char **env; @@ -993,19 +976,22 @@ static int WSAAPI getaddrinfo_stub(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res) { - struct hostent *h = gethostbyname(node); + struct hostent *h = NULL; struct addrinfo *ai; struct sockaddr_in *sin; - if (!h) - return WSAGetLastError(); + if (node) { + h = gethostbyname(node); + if (!h) + return WSAGetLastError(); + } ai = xmalloc(sizeof(struct addrinfo)); *res = ai; ai->ai_flags = 0; ai->ai_family = AF_INET; - ai->ai_socktype = hints->ai_socktype; - switch (hints->ai_socktype) { + ai->ai_socktype = hints ? hints->ai_socktype : 0; + switch (ai->ai_socktype) { case SOCK_STREAM: ai->ai_protocol = IPPROTO_TCP; break; @@ -1017,14 +1003,25 @@ static int WSAAPI getaddrinfo_stub(const char *node, const char *service, break; } ai->ai_addrlen = sizeof(struct sockaddr_in); - ai->ai_canonname = strdup(h->h_name); + if (hints && (hints->ai_flags & AI_CANONNAME)) + ai->ai_canonname = h ? strdup(h->h_name) : NULL; + else + ai->ai_canonname = NULL; sin = xmalloc(ai->ai_addrlen); memset(sin, 0, ai->ai_addrlen); sin->sin_family = AF_INET; + /* Note: getaddrinfo is supposed to allow service to be a string, + * which should be looked up using getservbyname. This is + * currently not implemented */ if (service) sin->sin_port = htons(atoi(service)); - sin->sin_addr = *(struct in_addr *)h->h_addr; + if (h) + sin->sin_addr = *(struct in_addr *)h->h_addr; + else if (hints && (hints->ai_flags & AI_PASSIVE)) + sin->sin_addr.s_addr = INADDR_ANY; + else + sin->sin_addr.s_addr = INADDR_LOOPBACK; ai->ai_addr = (struct sockaddr *)sin; ai->ai_next = 0; return 0; @@ -1175,7 +1172,10 @@ int mingw_getnameinfo(const struct sockaddr *sa, socklen_t salen, int mingw_socket(int domain, int type, int protocol) { int sockfd; - SOCKET s = WSASocket(domain, type, protocol, NULL, 0, 0); + SOCKET s; + + ensure_socket_initialization(); + s = WSASocket(domain, type, protocol, NULL, 0, 0); if (s == INVALID_SOCKET) { /* * WSAGetLastError() values are regular BSD error codes @@ -1205,6 +1205,45 @@ int mingw_connect(int sockfd, struct sockaddr *sa, size_t sz) return connect(s, sa, sz); } +#undef bind +int mingw_bind(int sockfd, struct sockaddr *sa, size_t sz) +{ + SOCKET s = (SOCKET)_get_osfhandle(sockfd); + return bind(s, sa, sz); +} + +#undef setsockopt +int mingw_setsockopt(int sockfd, int lvl, int optname, void *optval, int optlen) +{ + SOCKET s = (SOCKET)_get_osfhandle(sockfd); + return setsockopt(s, lvl, optname, (const char*)optval, optlen); +} + +#undef listen +int mingw_listen(int sockfd, int backlog) +{ + SOCKET s = (SOCKET)_get_osfhandle(sockfd); + return listen(s, backlog); +} + +#undef accept +int mingw_accept(int sockfd1, struct sockaddr *sa, socklen_t *sz) +{ + int sockfd2; + + SOCKET s1 = (SOCKET)_get_osfhandle(sockfd1); + SOCKET s2 = accept(s1, sa, sz); + + /* convert into a file descriptor */ + if ((sockfd2 = _open_osfhandle(s2, O_RDWR|O_BINARY)) < 0) { + int err = errno; + closesocket(s2); + return error("unable to make a socket file descriptor: %s", + strerror(err)); + } + return sockfd2; +} + #undef rename int mingw_rename(const char *pold, const char *pnew) { @@ -1476,6 +1515,58 @@ char *getpass(const char *prompt) return strbuf_detach(&buf, NULL); } +pid_t waitpid(pid_t pid, int *status, unsigned options) +{ + HANDLE h = OpenProcess(SYNCHRONIZE | PROCESS_QUERY_INFORMATION, + FALSE, pid); + if (!h) { + errno = ECHILD; + return -1; + } + + if (pid > 0 && options & WNOHANG) { + if (WAIT_OBJECT_0 != WaitForSingleObject(h, 0)) { + CloseHandle(h); + return 0; + } + options &= ~WNOHANG; + } + + if (options == 0) { + struct pinfo_t **ppinfo; + if (WaitForSingleObject(h, INFINITE) != WAIT_OBJECT_0) { + CloseHandle(h); + return 0; + } + + if (status) + GetExitCodeProcess(h, (LPDWORD)status); + + EnterCriticalSection(&pinfo_cs); + + ppinfo = &pinfo; + while (*ppinfo) { + struct pinfo_t *info = *ppinfo; + if (info->pid == pid) { + CloseHandle(info->proc); + *ppinfo = info->next; + free(info); + break; + } + ppinfo = &info->next; + } + + LeaveCriticalSection(&pinfo_cs); + + CloseHandle(h); + return pid; + } + CloseHandle(h); + + errno = EINVAL; + return -1; +} + #ifndef NO_MINGW_REPLACE_READDIR /* MinGW readdir implementation to avoid extra lstats for Git */ struct mingw_DIR diff --git a/compat/mingw.h b/compat/mingw.h index 83e35e833b..35d9813b6b 100644 --- a/compat/mingw.h +++ b/compat/mingw.h @@ -7,6 +7,7 @@ typedef int pid_t; typedef int uid_t; +typedef int socklen_t; #define hstrerror strerror #define S_IFLNK 0120000 /* Symbolic link */ @@ -36,6 +37,9 @@ typedef int uid_t; #define WEXITSTATUS(x) ((x) & 0xff) #define WTERMSIG(x) SIGTERM +#define EWOULDBLOCK EAGAIN +#define SHUT_WR SD_SEND + #define SIGHUP 1 #define SIGQUIT 3 #define SIGKILL 9 @@ -47,6 +51,9 @@ typedef int uid_t; #define F_SETFD 2 #define FD_CLOEXEC 0x1 +#define EAFNOSUPPORT WSAEAFNOSUPPORT +#define ECONNABORTED WSAECONNABORTED + struct passwd { char *pw_name; char *pw_gecos; @@ -55,16 +62,6 @@ struct passwd { extern char *getpass(const char *prompt); -#ifndef POLLIN -struct pollfd { - int fd; /* file descriptor */ - short events; /* requested events */ - short revents; /* returned events */ -}; -#define POLLIN 1 -#define POLLHUP 2 -#endif - typedef void (__cdecl *sig_handler_t)(int); struct sigaction { sig_handler_t sa_handler; @@ -136,13 +133,11 @@ static inline int mingw_unlink(const char *pathname) } #define unlink mingw_unlink -static inline pid_t waitpid(pid_t pid, int *status, unsigned options) -{ - if (options == 0) - return _cwait(status, pid, 0); - errno = EINVAL; - return -1; -} +#define WNOHANG 1 +pid_t waitpid(pid_t pid, int *status, unsigned options); + +#define kill mingw_kill +int mingw_kill(pid_t pid, int sig); #ifndef NO_OPENSSL #include <openssl/ssl.h> @@ -173,7 +168,6 @@ int pipe(int filedes[2]); unsigned int sleep (unsigned int seconds); int mkstemp(char *template); int gettimeofday(struct timeval *tv, void *tz); -int poll(struct pollfd *ufds, unsigned int nfds, int timeout); struct tm *gmtime_r(const time_t *timep, struct tm *result); struct tm *localtime_r(const time_t *timep, struct tm *result); int getpagesize(void); /* defined in MinGW's libgcc.a */ @@ -225,6 +219,18 @@ int mingw_socket(int domain, int type, int protocol); int mingw_connect(int sockfd, struct sockaddr *sa, size_t sz); #define connect mingw_connect +int mingw_bind(int sockfd, struct sockaddr *sa, size_t sz); +#define bind mingw_bind + +int mingw_setsockopt(int sockfd, int lvl, int optname, void *optval, int optlen); +#define setsockopt mingw_setsockopt + +int mingw_listen(int sockfd, int backlog); +#define listen mingw_listen + +int mingw_accept(int sockfd, struct sockaddr *sa, socklen_t *sz); +#define accept mingw_accept + int mingw_rename(const char*, const char*); #define rename mingw_rename @@ -305,11 +311,13 @@ void free_environ(char **env); static int mingw_main(); \ int main(int argc, const char **argv) \ { \ + extern CRITICAL_SECTION pinfo_cs; \ _fmode = _O_BINARY; \ _setmode(_fileno(stdin), _O_BINARY); \ _setmode(_fileno(stdout), _O_BINARY); \ _setmode(_fileno(stderr), _O_BINARY); \ argv[0] = xstrdup(_pgmptr); \ + InitializeCriticalSection(&pinfo_cs); \ return mingw_main(argc, argv); \ } \ static int mingw_main(c,v) diff --git a/compat/win32/sys/poll.c b/compat/win32/sys/poll.c new file mode 100644 index 0000000000..7e74ebe59a --- /dev/null +++ b/compat/win32/sys/poll.c @@ -0,0 +1,596 @@ +/* Emulation for poll(2) + Contributed by Paolo Bonzini. + + Copyright 2001-2003, 2006-2010 Free Software Foundation, Inc. + + This file is part of gnulib. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, + Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + +/* Tell gcc not to warn about the (nfd < 0) tests, below. */ +#if (__GNUC__ == 4 && 3 <= __GNUC_MINOR__) || 4 < __GNUC__ +# pragma GCC diagnostic ignored "-Wtype-limits" +#endif + +#include <malloc.h> + +#include <sys/types.h> +#include "poll.h" +#include <errno.h> +#include <limits.h> +#include <assert.h> + +#if (defined _WIN32 || defined __WIN32__) && ! defined __CYGWIN__ +# define WIN32_NATIVE +# include <winsock2.h> +# include <windows.h> +# include <io.h> +# include <stdio.h> +# include <conio.h> +#else +# include <sys/time.h> +# include <sys/socket.h> +# include <sys/select.h> +# include <unistd.h> +#endif + +#ifdef HAVE_SYS_IOCTL_H +# include <sys/ioctl.h> +#endif +#ifdef HAVE_SYS_FILIO_H +# include <sys/filio.h> +#endif + +#include <time.h> + +#ifndef INFTIM +# define INFTIM (-1) +#endif + +/* BeOS does not have MSG_PEEK. */ +#ifndef MSG_PEEK +# define MSG_PEEK 0 +#endif + +#ifdef WIN32_NATIVE + +#define IsConsoleHandle(h) (((long) (h) & 3) == 3) + +static BOOL +IsSocketHandle (HANDLE h) +{ + WSANETWORKEVENTS ev; + + if (IsConsoleHandle (h)) + return FALSE; + + /* Under Wine, it seems that getsockopt returns 0 for pipes too. + WSAEnumNetworkEvents instead distinguishes the two correctly. */ + ev.lNetworkEvents = 0xDEADBEEF; + WSAEnumNetworkEvents ((SOCKET) h, NULL, &ev); + return ev.lNetworkEvents != 0xDEADBEEF; +} + +/* Declare data structures for ntdll functions. */ +typedef struct _FILE_PIPE_LOCAL_INFORMATION { + ULONG NamedPipeType; + ULONG NamedPipeConfiguration; + ULONG MaximumInstances; + ULONG CurrentInstances; + ULONG InboundQuota; + ULONG ReadDataAvailable; + ULONG OutboundQuota; + ULONG WriteQuotaAvailable; + ULONG NamedPipeState; + ULONG NamedPipeEnd; +} FILE_PIPE_LOCAL_INFORMATION, *PFILE_PIPE_LOCAL_INFORMATION; + +typedef struct _IO_STATUS_BLOCK +{ + union { + DWORD Status; + PVOID Pointer; + } u; + ULONG_PTR Information; +} IO_STATUS_BLOCK, *PIO_STATUS_BLOCK; + +typedef enum _FILE_INFORMATION_CLASS { + FilePipeLocalInformation = 24 +} FILE_INFORMATION_CLASS, *PFILE_INFORMATION_CLASS; + +typedef DWORD (WINAPI *PNtQueryInformationFile) + (HANDLE, IO_STATUS_BLOCK *, VOID *, ULONG, FILE_INFORMATION_CLASS); + +# ifndef PIPE_BUF +# define PIPE_BUF 512 +# endif + +/* Compute revents values for file handle H. If some events cannot happen + for the handle, eliminate them from *P_SOUGHT. */ + +static int +win32_compute_revents (HANDLE h, int *p_sought) +{ + int i, ret, happened; + INPUT_RECORD *irbuffer; + DWORD avail, nbuffer; + BOOL bRet; + IO_STATUS_BLOCK iosb; + FILE_PIPE_LOCAL_INFORMATION fpli; + static PNtQueryInformationFile NtQueryInformationFile; + static BOOL once_only; + + switch (GetFileType (h)) + { + case FILE_TYPE_PIPE: + if (!once_only) + { + NtQueryInformationFile = (PNtQueryInformationFile) + GetProcAddress (GetModuleHandle ("ntdll.dll"), + "NtQueryInformationFile"); + once_only = TRUE; + } + + happened = 0; + if (PeekNamedPipe (h, NULL, 0, NULL, &avail, NULL) != 0) + { + if (avail) + happened |= *p_sought & (POLLIN | POLLRDNORM); + } + else if (GetLastError () == ERROR_BROKEN_PIPE) + happened |= POLLHUP; + + else + { + /* It was the write-end of the pipe. Check if it is writable. + If NtQueryInformationFile fails, optimistically assume the pipe is + writable. This could happen on Win9x, where NtQueryInformationFile + is not available, or if we inherit a pipe that doesn't permit + FILE_READ_ATTRIBUTES access on the write end (I think this should + not happen since WinXP SP2; WINE seems fine too). Otherwise, + ensure that enough space is available for atomic writes. */ + memset (&iosb, 0, sizeof (iosb)); + memset (&fpli, 0, sizeof (fpli)); + + if (!NtQueryInformationFile + || NtQueryInformationFile (h, &iosb, &fpli, sizeof (fpli), + FilePipeLocalInformation) + || fpli.WriteQuotaAvailable >= PIPE_BUF + || (fpli.OutboundQuota < PIPE_BUF && + fpli.WriteQuotaAvailable == fpli.OutboundQuota)) + happened |= *p_sought & (POLLOUT | POLLWRNORM | POLLWRBAND); + } + return happened; + + case FILE_TYPE_CHAR: + ret = WaitForSingleObject (h, 0); + if (!IsConsoleHandle (h)) + return ret == WAIT_OBJECT_0 ? *p_sought & ~(POLLPRI | POLLRDBAND) : 0; + + nbuffer = avail = 0; + bRet = GetNumberOfConsoleInputEvents (h, &nbuffer); + if (bRet) + { + /* Input buffer. */ + *p_sought &= POLLIN | POLLRDNORM; + if (nbuffer == 0) + return POLLHUP; + if (!*p_sought) + return 0; + + irbuffer = (INPUT_RECORD *) alloca (nbuffer * sizeof (INPUT_RECORD)); + bRet = PeekConsoleInput (h, irbuffer, nbuffer, &avail); + if (!bRet || avail == 0) + return POLLHUP; + + for (i = 0; i < avail; i++) + if (irbuffer[i].EventType == KEY_EVENT) + return *p_sought; + return 0; + } + else + { + /* Screen buffer. */ + *p_sought &= POLLOUT | POLLWRNORM | POLLWRBAND; + return *p_sought; + } + + default: + ret = WaitForSingleObject (h, 0); + if (ret == WAIT_OBJECT_0) + return *p_sought & ~(POLLPRI | POLLRDBAND); + + return *p_sought & (POLLOUT | POLLWRNORM | POLLWRBAND); + } +} + +/* Convert fd_sets returned by select into revents values. */ + +static int +win32_compute_revents_socket (SOCKET h, int sought, long lNetworkEvents) +{ + int happened = 0; + + if ((lNetworkEvents & (FD_READ | FD_ACCEPT | FD_CLOSE)) == FD_ACCEPT) + happened |= (POLLIN | POLLRDNORM) & sought; + + else if (lNetworkEvents & (FD_READ | FD_ACCEPT | FD_CLOSE)) + { + int r, error; + + char data[64]; + WSASetLastError (0); + r = recv (h, data, sizeof (data), MSG_PEEK); + error = WSAGetLastError (); + WSASetLastError (0); + + if (r > 0 || error == WSAENOTCONN) + happened |= (POLLIN | POLLRDNORM) & sought; + + /* Distinguish hung-up sockets from other errors. */ + else if (r == 0 || error == WSAESHUTDOWN || error == WSAECONNRESET + || error == WSAECONNABORTED || error == WSAENETRESET) + happened |= POLLHUP; + + else + happened |= POLLERR; + } + + if (lNetworkEvents & (FD_WRITE | FD_CONNECT)) + happened |= (POLLOUT | POLLWRNORM | POLLWRBAND) & sought; + + if (lNetworkEvents & FD_OOB) + happened |= (POLLPRI | POLLRDBAND) & sought; + + return happened; +} + +#else /* !MinGW */ + +/* Convert select(2) returned fd_sets into poll(2) revents values. */ +static int +compute_revents (int fd, int sought, fd_set *rfds, fd_set *wfds, fd_set *efds) +{ + int happened = 0; + if (FD_ISSET (fd, rfds)) + { + int r; + int socket_errno; + +# if defined __MACH__ && defined __APPLE__ + /* There is a bug in Mac OS X that causes it to ignore MSG_PEEK + for some kinds of descriptors. Detect if this descriptor is a + connected socket, a server socket, or something else using a + 0-byte recv, and use ioctl(2) to detect POLLHUP. */ + r = recv (fd, NULL, 0, MSG_PEEK); + socket_errno = (r < 0) ? errno : 0; + if (r == 0 || socket_errno == ENOTSOCK) + ioctl (fd, FIONREAD, &r); +# else + char data[64]; + r = recv (fd, data, sizeof (data), MSG_PEEK); + socket_errno = (r < 0) ? errno : 0; +# endif + if (r == 0) + happened |= POLLHUP; + + /* If the event happened on an unconnected server socket, + that's fine. */ + else if (r > 0 || ( /* (r == -1) && */ socket_errno == ENOTCONN)) + happened |= (POLLIN | POLLRDNORM) & sought; + + /* Distinguish hung-up sockets from other errors. */ + else if (socket_errno == ESHUTDOWN || socket_errno == ECONNRESET + || socket_errno == ECONNABORTED || socket_errno == ENETRESET) + happened |= POLLHUP; + + else + happened |= POLLERR; + } + + if (FD_ISSET (fd, wfds)) + happened |= (POLLOUT | POLLWRNORM | POLLWRBAND) & sought; + + if (FD_ISSET (fd, efds)) + happened |= (POLLPRI | POLLRDBAND) & sought; + + return happened; +} +#endif /* !MinGW */ + +int +poll (pfd, nfd, timeout) + struct pollfd *pfd; + nfds_t nfd; + int timeout; +{ +#ifndef WIN32_NATIVE + fd_set rfds, wfds, efds; + struct timeval tv; + struct timeval *ptv; + int maxfd, rc; + nfds_t i; + +# ifdef _SC_OPEN_MAX + static int sc_open_max = -1; + + if (nfd < 0 + || (nfd > sc_open_max + && (sc_open_max != -1 + || nfd > (sc_open_max = sysconf (_SC_OPEN_MAX))))) + { + errno = EINVAL; + return -1; + } +# else /* !_SC_OPEN_MAX */ +# ifdef OPEN_MAX + if (nfd < 0 || nfd > OPEN_MAX) + { + errno = EINVAL; + return -1; + } +# endif /* OPEN_MAX -- else, no check is needed */ +# endif /* !_SC_OPEN_MAX */ + + /* EFAULT is not necessary to implement, but let's do it in the + simplest case. */ + if (!pfd) + { + errno = EFAULT; + return -1; + } + + /* convert timeout number into a timeval structure */ + if (timeout == 0) + { + ptv = &tv; + ptv->tv_sec = 0; + ptv->tv_usec = 0; + } + else if (timeout > 0) + { + ptv = &tv; + ptv->tv_sec = timeout / 1000; + ptv->tv_usec = (timeout % 1000) * 1000; + } + else if (timeout == INFTIM) + /* wait forever */ + ptv = NULL; + else + { + errno = EINVAL; + return -1; + } + + /* create fd sets and determine max fd */ + maxfd = -1; + FD_ZERO (&rfds); + FD_ZERO (&wfds); + FD_ZERO (&efds); + for (i = 0; i < nfd; i++) + { + if (pfd[i].fd < 0) + continue; + + if (pfd[i].events & (POLLIN | POLLRDNORM)) + FD_SET (pfd[i].fd, &rfds); + + /* see select(2): "the only exceptional condition detectable + is out-of-band data received on a socket", hence we push + POLLWRBAND events onto wfds instead of efds. */ + if (pfd[i].events & (POLLOUT | POLLWRNORM | POLLWRBAND)) + FD_SET (pfd[i].fd, &wfds); + if (pfd[i].events & (POLLPRI | POLLRDBAND)) + FD_SET (pfd[i].fd, &efds); + if (pfd[i].fd >= maxfd + && (pfd[i].events & (POLLIN | POLLOUT | POLLPRI + | POLLRDNORM | POLLRDBAND + | POLLWRNORM | POLLWRBAND))) + { + maxfd = pfd[i].fd; + if (maxfd > FD_SETSIZE) + { + errno = EOVERFLOW; + return -1; + } + } + } + + /* examine fd sets */ + rc = select (maxfd + 1, &rfds, &wfds, &efds, ptv); + if (rc < 0) + return rc; + + /* establish results */ + rc = 0; + for (i = 0; i < nfd; i++) + if (pfd[i].fd < 0) + pfd[i].revents = 0; + else + { + int happened = compute_revents (pfd[i].fd, pfd[i].events, + &rfds, &wfds, &efds); + if (happened) + { + pfd[i].revents = happened; + rc++; + } + } + + return rc; +#else + static struct timeval tv0; + static HANDLE hEvent; + WSANETWORKEVENTS ev; + HANDLE h, handle_array[FD_SETSIZE + 2]; + DWORD ret, wait_timeout, nhandles; + fd_set rfds, wfds, xfds; + BOOL poll_again; + MSG msg; + int rc = 0; + nfds_t i; + + if (nfd < 0 || timeout < -1) + { + errno = EINVAL; + return -1; + } + + if (!hEvent) + hEvent = CreateEvent (NULL, FALSE, FALSE, NULL); + + handle_array[0] = hEvent; + nhandles = 1; + FD_ZERO (&rfds); + FD_ZERO (&wfds); + FD_ZERO (&xfds); + + /* Classify socket handles and create fd sets. */ + for (i = 0; i < nfd; i++) + { + int sought = pfd[i].events; + pfd[i].revents = 0; + if (pfd[i].fd < 0) + continue; + if (!(sought & (POLLIN | POLLRDNORM | POLLOUT | POLLWRNORM | POLLWRBAND + | POLLPRI | POLLRDBAND))) + continue; + + h = (HANDLE) _get_osfhandle (pfd[i].fd); + assert (h != NULL); + if (IsSocketHandle (h)) + { + int requested = FD_CLOSE; + + /* see above; socket handles are mapped onto select. */ + if (sought & (POLLIN | POLLRDNORM)) + { + requested |= FD_READ | FD_ACCEPT; + FD_SET ((SOCKET) h, &rfds); + } + if (sought & (POLLOUT | POLLWRNORM | POLLWRBAND)) + { + requested |= FD_WRITE | FD_CONNECT; + FD_SET ((SOCKET) h, &wfds); + } + if (sought & (POLLPRI | POLLRDBAND)) + { + requested |= FD_OOB; + FD_SET ((SOCKET) h, &xfds); + } + + if (requested) + WSAEventSelect ((SOCKET) h, hEvent, requested); + } + else + { + /* Poll now. If we get an event, do not poll again. Also, + screen buffer handles are waitable, and they'll block until + a character is available. win32_compute_revents eliminates + bits for the "wrong" direction. */ + pfd[i].revents = win32_compute_revents (h, &sought); + if (sought) + handle_array[nhandles++] = h; + if (pfd[i].revents) + timeout = 0; + } + } + + if (select (0, &rfds, &wfds, &xfds, &tv0) > 0) + { + /* Do MsgWaitForMultipleObjects anyway to dispatch messages, but + no need to call select again. */ + poll_again = FALSE; + wait_timeout = 0; + } + else + { + poll_again = TRUE; + if (timeout == INFTIM) + wait_timeout = INFINITE; + else + wait_timeout = timeout; + } + + for (;;) + { + ret = MsgWaitForMultipleObjects (nhandles, handle_array, FALSE, + wait_timeout, QS_ALLINPUT); + + if (ret == WAIT_OBJECT_0 + nhandles) + { + /* new input of some other kind */ + BOOL bRet; + while ((bRet = PeekMessage (&msg, NULL, 0, 0, PM_REMOVE)) != 0) + { + TranslateMessage (&msg); + DispatchMessage (&msg); + } + } + else + break; + } + + if (poll_again) + select (0, &rfds, &wfds, &xfds, &tv0); + + /* Place a sentinel at the end of the array. */ + handle_array[nhandles] = NULL; + nhandles = 1; + for (i = 0; i < nfd; i++) + { + int happened; + + if (pfd[i].fd < 0) + continue; + if (!(pfd[i].events & (POLLIN | POLLRDNORM | + POLLOUT | POLLWRNORM | POLLWRBAND))) + continue; + + h = (HANDLE) _get_osfhandle (pfd[i].fd); + if (h != handle_array[nhandles]) + { + /* It's a socket. */ + WSAEnumNetworkEvents ((SOCKET) h, NULL, &ev); + WSAEventSelect ((SOCKET) h, 0, 0); + + /* If we're lucky, WSAEnumNetworkEvents already provided a way + to distinguish FD_READ and FD_ACCEPT; this saves a recv later. */ + if (FD_ISSET ((SOCKET) h, &rfds) + && !(ev.lNetworkEvents & (FD_READ | FD_ACCEPT))) + ev.lNetworkEvents |= FD_READ | FD_ACCEPT; + if (FD_ISSET ((SOCKET) h, &wfds)) + ev.lNetworkEvents |= FD_WRITE | FD_CONNECT; + if (FD_ISSET ((SOCKET) h, &xfds)) + ev.lNetworkEvents |= FD_OOB; + + happened = win32_compute_revents_socket ((SOCKET) h, pfd[i].events, + ev.lNetworkEvents); + } + else + { + /* Not a socket. */ + int sought = pfd[i].events; + happened = win32_compute_revents (h, &sought); + nhandles++; + } + + if ((pfd[i].revents |= happened) != 0) + rc++; + } + + return rc; +#endif +} diff --git a/compat/win32/sys/poll.h b/compat/win32/sys/poll.h new file mode 100644 index 0000000000..b7aa59d973 --- /dev/null +++ b/compat/win32/sys/poll.h @@ -0,0 +1,53 @@ +/* Header for poll(2) emulation + Contributed by Paolo Bonzini. + + Copyright 2001, 2002, 2003, 2007, 2009, 2010 Free Software Foundation, Inc. + + This file is part of gnulib. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, + Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + +#ifndef _GL_POLL_H +#define _GL_POLL_H + +/* fake a poll(2) environment */ +#define POLLIN 0x0001 /* any readable data available */ +#define POLLPRI 0x0002 /* OOB/Urgent readable data */ +#define POLLOUT 0x0004 /* file descriptor is writeable */ +#define POLLERR 0x0008 /* some poll error occurred */ +#define POLLHUP 0x0010 /* file descriptor was "hung up" */ +#define POLLNVAL 0x0020 /* requested events "invalid" */ +#define POLLRDNORM 0x0040 +#define POLLRDBAND 0x0080 +#define POLLWRNORM 0x0100 +#define POLLWRBAND 0x0200 + +struct pollfd +{ + int fd; /* which file descriptor to poll */ + short events; /* events we are interested in */ + short revents; /* events found on return */ +}; + +typedef unsigned long nfds_t; + +extern int poll (struct pollfd *pfd, nfds_t nfd, int timeout); + +/* Define INFTIM only if doing so conforms to POSIX. */ +#if !defined (_POSIX_C_SOURCE) && !defined (_XOPEN_SOURCE) +#define INFTIM (-1) +#endif + +#endif /* _GL_POLL_H */ diff --git a/compat/win32/syslog.c b/compat/win32/syslog.c new file mode 100644 index 0000000000..42b95a9b51 --- /dev/null +++ b/compat/win32/syslog.c @@ -0,0 +1,72 @@ +#include "../../git-compat-util.h" +#include "../../strbuf.h" + +static HANDLE ms_eventlog; + +void openlog(const char *ident, int logopt, int facility) +{ + if (ms_eventlog) + return; + + ms_eventlog = RegisterEventSourceA(NULL, ident); + + if (!ms_eventlog) + warning("RegisterEventSource() failed: %lu", GetLastError()); +} + +void syslog(int priority, const char *fmt, ...) +{ + struct strbuf sb = STRBUF_INIT; + struct strbuf_expand_dict_entry dict[] = { + {"1", "% 1"}, + {NULL, NULL} + }; + WORD logtype; + char *str; + int str_len; + va_list ap; + + if (!ms_eventlog) + return; + + va_start(ap, fmt); + str_len = vsnprintf(NULL, 0, fmt, ap); + va_end(ap); + + if (str_len < 0) { + warning("vsnprintf failed: '%s'", strerror(errno)); + return; + } + + str = malloc(str_len + 1); + va_start(ap, fmt); + vsnprintf(str, str_len + 1, fmt, ap); + va_end(ap); + strbuf_expand(&sb, str, strbuf_expand_dict_cb, &dict); + free(str); + + switch (priority) { + case LOG_EMERG: + case LOG_ALERT: + case LOG_CRIT: + case LOG_ERR: + logtype = EVENTLOG_ERROR_TYPE; + break; + + case LOG_WARNING: + logtype = EVENTLOG_WARNING_TYPE; + break; + + case LOG_NOTICE: + case LOG_INFO: + case LOG_DEBUG: + default: + logtype = EVENTLOG_INFORMATION_TYPE; + break; + } + + ReportEventA(ms_eventlog, logtype, 0, 0, NULL, 1, 0, + (const char **)&sb.buf, NULL); + + strbuf_release(&sb); +} diff --git a/compat/win32/syslog.h b/compat/win32/syslog.h new file mode 100644 index 0000000000..70daa7c08b --- /dev/null +++ b/compat/win32/syslog.h @@ -0,0 +1,20 @@ +#ifndef SYSLOG_H +#define SYSLOG_H + +#define LOG_PID 0x01 + +#define LOG_EMERG 0 +#define LOG_ALERT 1 +#define LOG_CRIT 2 +#define LOG_ERR 3 +#define LOG_WARNING 4 +#define LOG_NOTICE 5 +#define LOG_INFO 6 +#define LOG_DEBUG 7 + +#define LOG_DAEMON (3<<3) + +void openlog(const char *ident, int logopt, int facility); +void syslog(int priority, const char *fmt, ...); + +#endif /* SYSLOG_H */ @@ -410,7 +410,7 @@ unsigned long git_config_ulong(const char *name, const char *value) return ret; } -int git_config_maybe_bool(const char *name, const char *value) +static int git_config_maybe_bool_text(const char *name, const char *value) { if (!value) return 1; @@ -427,9 +427,21 @@ int git_config_maybe_bool(const char *name, const char *value) return -1; } +int git_config_maybe_bool(const char *name, const char *value) +{ + int v = git_config_maybe_bool_text(name, value); + if (0 <= v) + return v; + if (!strcmp(value, "0")) + return 0; + if (!strcmp(value, "1")) + return 1; + return -1; +} + int git_config_bool_or_int(const char *name, const char *value, int *is_bool) { - int v = git_config_maybe_bool(name, value); + int v = git_config_maybe_bool_text(name, value); if (0 <= v) { *is_bool = 1; return v; @@ -489,6 +501,13 @@ static int git_default_core_config(const char *var, const char *value) return 0; } + if (!strcmp(var, "core.abbrevguard")) { + unique_abbrev_extra_length = git_config_int(var, value); + if (unique_abbrev_extra_length < 0) + unique_abbrev_extra_length = 0; + return 0; + } + if (!strcmp(var, "core.bare")) { is_bare_repository_cfg = git_config_bool(var, value); return 0; diff --git a/config.mak.in b/config.mak.in index 55dcd30009..9614973057 100644 --- a/config.mak.in +++ b/config.mak.in @@ -47,6 +47,8 @@ NO_C99_FORMAT=@NO_C99_FORMAT@ NO_HSTRERROR=@NO_HSTRERROR@ NO_STRCASESTR=@NO_STRCASESTR@ NO_STRTOK_R=@NO_STRTOK_R@ +NO_FNMATCH=@NO_FNMATCH@ +NO_FNMATCH_CASEFOLD=@NO_FNMATCH_CASEFOLD@ NO_MEMMEM=@NO_MEMMEM@ NO_STRLCPY=@NO_STRLCPY@ NO_UINTMAX_T=@NO_UINTMAX_T@ diff --git a/configure.ac b/configure.ac index e9ca1178bb..5792425a49 100644 --- a/configure.ac +++ b/configure.ac @@ -617,6 +617,18 @@ AC_CHECK_HEADER([sys/select.h], [NO_SYS_SELECT_H=UnfortunatelyYes]) AC_SUBST(NO_SYS_SELECT_H) # +# Define NO_SYS_POLL_H if you don't have sys/poll.h +AC_CHECK_HEADER([sys/poll.h], +[NO_SYS_POLL_H=], +[NO_SYS_POLL_H=UnfortunatelyYes]) +AC_SUBST(NO_SYS_POLL_H) +# +# Define NO_INTTYPES_H if you don't have inttypes.h +AC_CHECK_HEADER([inttypes.h], +[NO_INTTYPES_H=], +[NO_INTTYPES_H=UnfortunatelyYes]) +AC_SUBST(NO_INTTYPES_H) +# # Define OLD_ICONV if your library has an old iconv(), where the second # (input buffer pointer) parameter is declared with type (const char **). AC_DEFUN([OLDICONVTEST_SRC], [[ @@ -818,6 +830,34 @@ GIT_CHECK_FUNC(strtok_r, [NO_STRTOK_R=YesPlease]) AC_SUBST(NO_STRTOK_R) # +# Define NO_FNMATCH if you don't have fnmatch +GIT_CHECK_FUNC(fnmatch, +[NO_FNMATCH=], +[NO_FNMATCH=YesPlease]) +AC_SUBST(NO_FNMATCH) +# +# Define NO_FNMATCH_CASEFOLD if your fnmatch function doesn't have the +# FNM_CASEFOLD GNU extension. +AC_CACHE_CHECK([whether the fnmatch function supports the FNMATCH_CASEFOLD GNU extension], + [ac_cv_c_excellent_fnmatch], [ +AC_EGREP_CPP(yippeeyeswehaveit, + AC_LANG_PROGRAM([ +#include <fnmatch.h> +], +[#ifdef FNM_CASEFOLD +yippeeyeswehaveit +#endif +]), + [ac_cv_c_excellent_fnmatch=yes], + [ac_cv_c_excellent_fnmatch=no]) +]) +if test $ac_cv_c_excellent_fnmatch = yes; then + NO_FNMATCH_CASEFOLD= +else + NO_FNMATCH_CASEFOLD=YesPlease +fi +AC_SUBST(NO_FNMATCH_CASEFOLD) +# # Define NO_MEMMEM if you don't have memmem. GIT_CHECK_FUNC(memmem, [NO_MEMMEM=], @@ -868,6 +908,12 @@ GIT_CHECK_FUNC(mkstemps, [NO_MKSTEMPS=YesPlease]) AC_SUBST(NO_MKSTEMPS) # +# Define NO_INITGROUPS if you don't have initgroups in the C library. +GIT_CHECK_FUNC(initgroups, +[NO_INITGROUPS=], +[NO_INITGROUPS=YesPlease]) +AC_SUBST(NO_INITGROUPS) +# # # Define NO_MMAP if you want to avoid mmap. # diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash index f71046947f..604fa794cc 100755 --- a/contrib/completion/git-completion.bash +++ b/contrib/completion/git-completion.bash @@ -261,7 +261,7 @@ __git_ps1 () (describe) git describe HEAD ;; (* | default) - git describe --exact-match HEAD ;; + git describe --tags --exact-match HEAD ;; esac 2>/dev/null)" || b="$(cut -c1-7 "$g/HEAD" 2>/dev/null)..." || diff --git a/contrib/examples/builtin-fetch--tool.c b/contrib/examples/builtin-fetch--tool.c index cd10dbcbc9..3140e405fa 100644 --- a/contrib/examples/builtin-fetch--tool.c +++ b/contrib/examples/builtin-fetch--tool.c @@ -148,7 +148,7 @@ static int append_fetch_head(FILE *fp, what = remote_name + 10; } else if (!strncmp(remote_name, "refs/remotes/", 13)) { - kind = "remote branch"; + kind = "remote-tracking branch"; what = remote_name + 13; } else { diff --git a/contrib/hooks/post-receive-email b/contrib/hooks/post-receive-email index 85724bfc08..f99ea95850 100755 --- a/contrib/hooks/post-receive-email +++ b/contrib/hooks/post-receive-email @@ -144,13 +144,13 @@ prep_for_email() short_refname=${refname##refs/remotes/} echo >&2 "*** Push-update of tracking branch, $refname" echo >&2 "*** - no email generated." - exit 0 + return 1 ;; *) # Anything else (is there anything else?) echo >&2 "*** Unknown type of update to $refname ($rev_type)" echo >&2 "*** - no email generated" - return 0 + return 1 ;; esac @@ -166,10 +166,10 @@ prep_for_email() esac echo >&2 "*** $config_name is not set so no email will be sent" echo >&2 "*** for $refname update $oldrev->$newrev" - return 0 + return 1 fi - return 1 + return 0 } # @@ -5,8 +5,6 @@ #include "strbuf.h" #include "string-list.h" -#include <syslog.h> - #ifndef HOST_NAME_MAX #define HOST_NAME_MAX 256 #endif @@ -15,6 +13,10 @@ #define NI_MAXSERV 32 #endif +#ifdef NO_INITGROUPS +#define initgroups(x, y) (0) /* nothing */ +#endif + static int log_syslog; static int verbose; static int reuseaddr; @@ -25,10 +27,10 @@ static const char daemon_usage[] = " [--strict-paths] [--base-path=<path>] [--base-path-relaxed]\n" " [--user-path | --user-path=<path>]\n" " [--interpolated-path=<path>]\n" -" [--reuseaddr] [--detach] [--pid-file=<file>]\n" +" [--reuseaddr] [--pid-file=<file>]\n" " [--(enable|disable|allow-override|forbid-override)=<service>]\n" " [--inetd | [--listen=<host_or_ipaddr>] [--port=<n>]\n" -" [--user=<user> [--group=<group>]]\n" +" [--detach] [--user=<user> [--group=<group>]]\n" " [<directory>...]"; /* List of acceptable pathname prefixes */ @@ -69,12 +71,14 @@ static void logreport(int priority, const char *err, va_list params) syslog(priority, "%s", buf); } else { /* - * Since stderr is set to linebuffered mode, the + * Since stderr is set to buffered mode, the * logging of different processes will not overlap + * unless they overflow the (rather big) buffers. */ fprintf(stderr, "[%"PRIuMAX"] ", (uintmax_t)getpid()); vfprintf(stderr, err, params); fputc('\n', stderr); + fflush(stderr); } } @@ -516,37 +520,14 @@ static void parse_host_arg(char *extra_args, int buflen) } -static int execute(struct sockaddr *addr) +static int execute(void) { static char line[1000]; int pktlen, len, i; + char *addr = getenv("REMOTE_ADDR"), *port = getenv("REMOTE_PORT"); - if (addr) { - char addrbuf[256] = ""; - int port = -1; - - if (addr->sa_family == AF_INET) { - struct sockaddr_in *sin_addr = (void *) addr; - inet_ntop(addr->sa_family, &sin_addr->sin_addr, addrbuf, sizeof(addrbuf)); - port = ntohs(sin_addr->sin_port); -#ifndef NO_IPV6 - } else if (addr && addr->sa_family == AF_INET6) { - struct sockaddr_in6 *sin6_addr = (void *) addr; - - char *buf = addrbuf; - *buf++ = '['; *buf = '\0'; /* stpcpy() is cool */ - inet_ntop(AF_INET6, &sin6_addr->sin6_addr, buf, sizeof(addrbuf) - 1); - strcat(buf, "]"); - - port = ntohs(sin6_addr->sin6_port); -#endif - } - loginfo("Connection from %s:%d", addrbuf, port); - setenv("REMOTE_ADDR", addrbuf, 1); - } - else { - unsetenv("REMOTE_ADDR"); - } + if (addr) + loginfo("Connection from %s:%s", addr, port); alarm(init_timeout ? init_timeout : timeout); pktlen = packet_read_line(0, line, sizeof(line)); @@ -616,17 +597,17 @@ static unsigned int live_children; static struct child { struct child *next; - pid_t pid; + struct child_process cld; struct sockaddr_storage address; } *firstborn; -static void add_child(pid_t pid, struct sockaddr *addr, int addrlen) +static void add_child(struct child_process *cld, struct sockaddr *addr, socklen_t addrlen) { struct child *newborn, **cradle; newborn = xcalloc(1, sizeof(*newborn)); live_children++; - newborn->pid = pid; + memcpy(&newborn->cld, cld, sizeof(*cld)); memcpy(&newborn->address, addr, addrlen); for (cradle = &firstborn; *cradle; cradle = &(*cradle)->next) if (!addrcmp(&(*cradle)->address, &newborn->address)) @@ -635,19 +616,6 @@ static void add_child(pid_t pid, struct sockaddr *addr, int addrlen) *cradle = newborn; } -static void remove_child(pid_t pid) -{ - struct child **cradle, *blanket; - - for (cradle = &firstborn; (blanket = *cradle); cradle = &blanket->next) - if (blanket->pid == pid) { - *cradle = blanket->next; - live_children--; - free(blanket); - break; - } -} - /* * This gets called if the number of connections grows * past "max_connections". @@ -663,7 +631,7 @@ static void kill_some_child(void) for (; (next = blanket->next); blanket = next) if (!addrcmp(&blanket->address, &next->address)) { - kill(blanket->pid, SIGTERM); + kill(blanket->cld.pid, SIGTERM); break; } } @@ -673,18 +641,28 @@ static void check_dead_children(void) int status; pid_t pid; - while ((pid = waitpid(-1, &status, WNOHANG)) > 0) { - const char *dead = ""; - remove_child(pid); - if (!WIFEXITED(status) || (WEXITSTATUS(status) > 0)) - dead = " (with error)"; - loginfo("[%"PRIuMAX"] Disconnected%s", (uintmax_t)pid, dead); - } + struct child **cradle, *blanket; + for (cradle = &firstborn; (blanket = *cradle);) + if ((pid = waitpid(blanket->cld.pid, &status, WNOHANG)) > 1) { + const char *dead = ""; + if (status) + dead = " (with error)"; + loginfo("[%"PRIuMAX"] Disconnected%s", (uintmax_t)pid, dead); + + /* remove the child */ + *cradle = blanket->next; + live_children--; + free(blanket); + } else + cradle = &blanket->next; } -static void handle(int incoming, struct sockaddr *addr, int addrlen) +static char **cld_argv; +static void handle(int incoming, struct sockaddr *addr, socklen_t addrlen) { - pid_t pid; + struct child_process cld = { 0 }; + char addrbuf[300] = "REMOTE_ADDR=", portbuf[300]; + char *env[] = { addrbuf, portbuf, NULL }; if (max_connections && live_children >= max_connections) { kill_some_child(); @@ -697,22 +675,37 @@ static void handle(int incoming, struct sockaddr *addr, int addrlen) } } - if ((pid = fork())) { - close(incoming); - if (pid < 0) { - logerror("Couldn't fork %s", strerror(errno)); - return; - } + if (addr->sa_family == AF_INET) { + struct sockaddr_in *sin_addr = (void *) addr; + inet_ntop(addr->sa_family, &sin_addr->sin_addr, addrbuf + 12, + sizeof(addrbuf) - 12); + snprintf(portbuf, sizeof(portbuf), "REMOTE_PORT=%d", + ntohs(sin_addr->sin_port)); +#ifndef NO_IPV6 + } else if (addr && addr->sa_family == AF_INET6) { + struct sockaddr_in6 *sin6_addr = (void *) addr; - add_child(pid, addr, addrlen); - return; + char *buf = addrbuf + 12; + *buf++ = '['; *buf = '\0'; /* stpcpy() is cool */ + inet_ntop(AF_INET6, &sin6_addr->sin6_addr, buf, + sizeof(addrbuf) - 13); + strcat(buf, "]"); + + snprintf(portbuf, sizeof(portbuf), "REMOTE_PORT=%d", + ntohs(sin6_addr->sin6_port)); +#endif } - dup2(incoming, 0); - dup2(incoming, 1); - close(incoming); + cld.env = (const char **)env; + cld.argv = (const char **)cld_argv; + cld.in = incoming; + cld.out = dup(incoming); - exit(execute(addr)); + if (start_command(&cld)) + logerror("unable to fork"); + else + add_child(&cld, addr, addrlen); + close(incoming); } static void child_handler(int signo) @@ -914,9 +907,15 @@ static int service_loop(struct socketlist *socklist) for (i = 0; i < socklist->nr; i++) { if (pfd[i].revents & POLLIN) { - struct sockaddr_storage ss; - unsigned int sslen = sizeof(ss); - int incoming = accept(pfd[i].fd, (struct sockaddr *)&ss, &sslen); + union { + struct sockaddr sa; + struct sockaddr_in sai; +#ifndef NO_IPV6 + struct sockaddr_in6 sai6; +#endif + } ss; + socklen_t sslen = sizeof(ss); + int incoming = accept(pfd[i].fd, &ss.sa, &sslen); if (incoming < 0) { switch (errno) { case EAGAIN: @@ -927,7 +926,7 @@ static int service_loop(struct socketlist *socklist) die_errno("accept returned"); } } - handle(incoming, (struct sockaddr *)&ss, sslen); + handle(incoming, &ss.sa, sslen); } } } @@ -945,6 +944,62 @@ static void sanitize_stdfds(void) close(fd); } +#ifdef NO_POSIX_GOODIES + +struct credentials; + +static void drop_privileges(struct credentials *cred) +{ + /* nothing */ +} + +static void daemonize(void) +{ + die("--detach not supported on this platform"); +} + +static struct credentials *prepare_credentials(const char *user_name, + const char *group_name) +{ + die("--user not supported on this platform"); +} + +#else + +struct credentials { + struct passwd *pass; + gid_t gid; +}; + +static void drop_privileges(struct credentials *cred) +{ + if (cred && (initgroups(cred->pass->pw_name, cred->gid) || + setgid (cred->gid) || setuid(cred->pass->pw_uid))) + die("cannot drop privileges"); +} + +static struct credentials *prepare_credentials(const char *user_name, + const char *group_name) +{ + static struct credentials c; + + c.pass = getpwnam(user_name); + if (!c.pass) + die("user not found - %s", user_name); + + if (!group_name) + c.gid = c.pass->pw_gid; + else { + struct group *group = getgrnam(group_name); + if (!group) + die("group not found - %s", group_name); + + c.gid = group->gr_gid; + } + + return &c; +} + static void daemonize(void) { switch (fork()) { @@ -962,6 +1017,7 @@ static void daemonize(void) close(2); sanitize_stdfds(); } +#endif static void store_pid(const char *path) { @@ -972,7 +1028,8 @@ static void store_pid(const char *path) die_errno("failed to write pid file '%s'", path); } -static int serve(struct string_list *listen_addr, int listen_port, struct passwd *pass, gid_t gid) +static int serve(struct string_list *listen_addr, int listen_port, + struct credentials *cred) { struct socketlist socklist = { NULL, 0, 0 }; @@ -981,10 +1038,7 @@ static int serve(struct string_list *listen_addr, int listen_port, struct passwd die("unable to allocate any listen sockets on port %u", listen_port); - if (pass && gid && - (initgroups(pass->pw_name, gid) || setgid (gid) || - setuid(pass->pw_uid))) - die("cannot drop privileges"); + drop_privileges(cred); return service_loop(&socklist); } @@ -993,12 +1047,10 @@ int main(int argc, char **argv) { int listen_port = 0; struct string_list listen_addr = STRING_LIST_INIT_NODUP; - int inetd_mode = 0; + int serve_mode = 0, inetd_mode = 0; const char *pid_file = NULL, *user_name = NULL, *group_name = NULL; int detach = 0; - struct passwd *pass = NULL; - struct group *group; - gid_t gid = 0; + struct credentials *cred = NULL; int i; git_extract_argv0_path(argv[0]); @@ -1019,6 +1071,10 @@ int main(int argc, char **argv) continue; } } + if (!strcmp(arg, "--serve")) { + serve_mode = 1; + continue; + } if (!strcmp(arg, "--inetd")) { inetd_mode = 1; log_syslog = 1; @@ -1127,10 +1183,10 @@ int main(int argc, char **argv) set_die_routine(daemon_die); } else /* avoid splitting a message in the middle */ - setvbuf(stderr, NULL, _IOLBF, 0); + setvbuf(stderr, NULL, _IOFBF, 4096); - if (inetd_mode && (group_name || user_name)) - die("--user and --group are incompatible with --inetd"); + if (inetd_mode && (detach || group_name || user_name)) + die("--detach, --user and --group are incompatible with --inetd"); if (inetd_mode && (listen_port || (listen_addr.nr > 0))) die("--listen= and --port= are incompatible with --inetd"); @@ -1140,21 +1196,8 @@ int main(int argc, char **argv) if (group_name && !user_name) die("--group supplied without --user"); - if (user_name) { - pass = getpwnam(user_name); - if (!pass) - die("user not found - %s", user_name); - - if (!group_name) - gid = pass->pw_gid; - else { - group = getgrnam(group_name); - if (!group) - die("group not found - %s", group_name); - - gid = group->gr_gid; - } - } + if (user_name) + cred = prepare_credentials(user_name, group_name); if (strict_paths && (!ok_paths || !*ok_paths)) die("option --strict-paths requires a whitelist"); @@ -1164,19 +1207,13 @@ int main(int argc, char **argv) base_path); if (inetd_mode) { - struct sockaddr_storage ss; - struct sockaddr *peer = (struct sockaddr *)&ss; - socklen_t slen = sizeof(ss); - if (!freopen("/dev/null", "w", stderr)) die_errno("failed to redirect stderr to /dev/null"); - - if (getpeername(0, peer, &slen)) - peer = NULL; - - return execute(peer); } + if (inetd_mode || serve_mode) + return execute(); + if (detach) { daemonize(); loginfo("Ready to rumble"); @@ -1187,5 +1224,12 @@ int main(int argc, char **argv) if (pid_file) store_pid(pid_file); - return serve(&listen_addr, listen_port, pass, gid); + /* prepare argv for serving-processes */ + cld_argv = xmalloc(sizeof (char *) * (argc + 2)); + for (i = 0; i < argc; ++i) + cld_argv[i] = argv[i]; + cld_argv[argc] = "--serve"; + cld_argv[argc+1] = NULL; + + return serve(&listen_addr, listen_port, cred); } @@ -2158,7 +2158,7 @@ static void builtin_checkdiff(const char *name_a, const char *name_b, ecbdata.ws_rule = data.ws_rule; check_blank_at_eof(&mf1, &mf2, &ecbdata); - blank_at_eof = ecbdata.blank_at_eof_in_preimage; + blank_at_eof = ecbdata.blank_at_eof_in_postimage; if (blank_at_eof) { static char *err; @@ -2391,10 +2391,14 @@ int diff_populate_filespec(struct diff_filespec *s, int size_only) } else { enum object_type type; - if (size_only) + if (size_only) { type = sha1_object_info(s->sha1, &s->size); - else { + if (type < 0) + die("unable to read %s", sha1_to_hex(s->sha1)); + } else { s->data = read_sha1_file(s->sha1, &type, &s->size); + if (!s->data) + die("unable to read %s", sha1_to_hex(s->sha1)); s->should_free = 1; } } @@ -3148,12 +3152,12 @@ int diff_opt_parse(struct diff_options *options, const char **av, int ac) else if (!prefixcmp(arg, "-B") || !prefixcmp(arg, "--break-rewrites=") || !strcmp(arg, "--break-rewrites")) { if ((options->break_opt = diff_scoreopt_parse(arg)) == -1) - return -1; + return error("invalid argument to -B: %s", arg+2); } else if (!prefixcmp(arg, "-M") || !prefixcmp(arg, "--detect-renames=") || !strcmp(arg, "--detect-renames")) { if ((options->rename_score = diff_scoreopt_parse(arg)) == -1) - return -1; + return error("invalid argument to -M: %s", arg+2); options->detect_rename = DIFF_DETECT_RENAME; } else if (!prefixcmp(arg, "-C") || !prefixcmp(arg, "--detect-copies=") || @@ -3161,7 +3165,7 @@ int diff_opt_parse(struct diff_options *options, const char **av, int ac) if (options->detect_rename == DIFF_DETECT_COPY) DIFF_OPT_SET(options, FIND_COPIES_HARDER); if ((options->rename_score = diff_scoreopt_parse(arg)) == -1) - return -1; + return error("invalid argument to -C: %s", arg+2); options->detect_rename = DIFF_DETECT_COPY; } else if (!strcmp(arg, "--no-renames")) @@ -18,6 +18,22 @@ static int read_directory_recursive(struct dir_struct *dir, const char *path, in int check_only, const struct path_simplify *simplify); static int get_dtype(struct dirent *de, const char *path, int len); +/* helper string functions with support for the ignore_case flag */ +int strcmp_icase(const char *a, const char *b) +{ + return ignore_case ? strcasecmp(a, b) : strcmp(a, b); +} + +int strncmp_icase(const char *a, const char *b, size_t count) +{ + return ignore_case ? strncasecmp(a, b, count) : strncmp(a, b, count); +} + +int fnmatch_icase(const char *pattern, const char *string, int flags) +{ + return fnmatch(pattern, string, flags | (ignore_case ? FNM_CASEFOLD : 0)); +} + static int common_prefix(const char **pathspec) { const char *path, *slash, *next; @@ -91,16 +107,30 @@ static int match_one(const char *match, const char *name, int namelen) if (!*match) return MATCHED_RECURSIVELY; - for (;;) { - unsigned char c1 = *match; - unsigned char c2 = *name; - if (c1 == '\0' || is_glob_special(c1)) - break; - if (c1 != c2) - return 0; - match++; - name++; - namelen--; + if (ignore_case) { + for (;;) { + unsigned char c1 = tolower(*match); + unsigned char c2 = tolower(*name); + if (c1 == '\0' || is_glob_special(c1)) + break; + if (c1 != c2) + return 0; + match++; + name++; + namelen--; + } + } else { + for (;;) { + unsigned char c1 = *match; + unsigned char c2 = *name; + if (c1 == '\0' || is_glob_special(c1)) + break; + if (c1 != c2) + return 0; + match++; + name++; + namelen--; + } } @@ -109,8 +139,8 @@ static int match_one(const char *match, const char *name, int namelen) * we need to match by fnmatch */ matchlen = strlen(match); - if (strncmp(match, name, matchlen)) - return !fnmatch(match, name, 0) ? MATCHED_FNMATCH : 0; + if (strncmp_icase(match, name, matchlen)) + return !fnmatch_icase(match, name, 0) ? MATCHED_FNMATCH : 0; if (namelen == matchlen) return MATCHED_EXACTLY; @@ -375,14 +405,14 @@ int excluded_from_list(const char *pathname, if (x->flags & EXC_FLAG_NODIR) { /* match basename */ if (x->flags & EXC_FLAG_NOWILDCARD) { - if (!strcmp(exclude, basename)) + if (!strcmp_icase(exclude, basename)) return to_exclude; } else if (x->flags & EXC_FLAG_ENDSWITH) { if (x->patternlen - 1 <= pathlen && - !strcmp(exclude + 1, pathname + pathlen - x->patternlen + 1)) + !strcmp_icase(exclude + 1, pathname + pathlen - x->patternlen + 1)) return to_exclude; } else { - if (fnmatch(exclude, basename, 0) == 0) + if (fnmatch_icase(exclude, basename, 0) == 0) return to_exclude; } } @@ -397,14 +427,14 @@ int excluded_from_list(const char *pathname, if (pathlen < baselen || (baselen && pathname[baselen-1] != '/') || - strncmp(pathname, x->base, baselen)) + strncmp_icase(pathname, x->base, baselen)) continue; if (x->flags & EXC_FLAG_NOWILDCARD) { - if (!strcmp(exclude, pathname + baselen)) + if (!strcmp_icase(exclude, pathname + baselen)) return to_exclude; } else { - if (fnmatch(exclude, pathname+baselen, + if (fnmatch_icase(exclude, pathname+baselen, FNM_PATHNAME) == 0) return to_exclude; } @@ -470,6 +500,39 @@ enum exist_status { }; /* + * Do not use the alphabetically stored index to look up + * the directory name; instead, use the case insensitive + * name hash. + */ +static enum exist_status directory_exists_in_index_icase(const char *dirname, int len) +{ + struct cache_entry *ce = index_name_exists(&the_index, dirname, len + 1, ignore_case); + unsigned char endchar; + + if (!ce) + return index_nonexistent; + endchar = ce->name[len]; + + /* + * The cache_entry structure returned will contain this dirname + * and possibly additional path components. + */ + if (endchar == '/') + return index_directory; + + /* + * If there are no additional path components, then this cache_entry + * represents a submodule. Submodules, despite being directories, + * are stored in the cache without a closing slash. + */ + if (!endchar && S_ISGITLINK(ce->ce_mode)) + return index_gitdir; + + /* This should never be hit, but it exists just in case. */ + return index_nonexistent; +} + +/* * The index sorts alphabetically by entry name, which * means that a gitlink sorts as '\0' at the end, while * a directory (which is defined not as an entry, but as @@ -478,7 +541,12 @@ enum exist_status { */ static enum exist_status directory_exists_in_index(const char *dirname, int len) { - int pos = cache_name_pos(dirname, len); + int pos; + + if (ignore_case) + return directory_exists_in_index_icase(dirname, len); + + pos = cache_name_pos(dirname, len); if (pos < 0) pos = -pos-1; while (pos < active_nr) { @@ -101,4 +101,8 @@ extern int remove_dir_recursively(struct strbuf *path, int flag); /* tries to remove the path with empty directories along it, ignores ENOENT */ extern int remove_path(const char *path); +extern int strcmp_icase(const char *a, const char *b); +extern int strncmp_icase(const char *a, const char *b, size_t count); +extern int fnmatch_icase(const char *pattern, const char *string, int flags); + #endif diff --git a/environment.c b/environment.c index de5581fe51..c79f2a9b56 100644 --- a/environment.c +++ b/environment.c @@ -21,6 +21,7 @@ int prefer_symlink_refs; int is_bare_repository_cfg = -1; /* unspecified */ int log_all_ref_updates = -1; /* unspecified */ int warn_ambiguous_refs = 1; +int unique_abbrev_extra_length; int repository_format_version; const char *git_commit_encoding; const char *git_log_output_encoding; @@ -87,6 +88,7 @@ const char * const local_repo_env[LOCAL_REPO_ENV_SIZE + 1] = { static void setup_git_env(void) { git_dir = getenv(GIT_DIR_ENVIRONMENT); + git_dir = git_dir ? xstrdup(git_dir) : NULL; if (!git_dir) { git_dir = read_gitfile_gently(DEFAULT_GIT_DIR_ENVIRONMENT); git_dir = git_dir ? xstrdup(git_dir) : NULL; @@ -171,6 +173,43 @@ char *get_object_directory(void) return git_object_dir; } +int odb_mkstemp(char *template, size_t limit, const char *pattern) +{ + int fd; + /* + * we let the umask do its job, don't try to be more + * restrictive except to remove write permission. + */ + int mode = 0444; + snprintf(template, limit, "%s/%s", + get_object_directory(), pattern); + fd = git_mkstemp_mode(template, mode); + if (0 <= fd) + return fd; + + /* slow path */ + /* some mkstemp implementations erase template on failure */ + snprintf(template, limit, "%s/%s", + get_object_directory(), pattern); + safe_create_leading_directories(template); + return xmkstemp_mode(template, mode); +} + +int odb_pack_keep(char *name, size_t namesz, unsigned char *sha1) +{ + int fd; + + snprintf(name, namesz, "%s/pack/pack-%s.keep", + get_object_directory(), sha1_to_hex(sha1)); + fd = open(name, O_RDWR|O_CREAT|O_EXCL, 0600); + if (0 <= fd) + return fd; + + /* slow path */ + safe_create_leading_directories(name); + return open(name, O_RDWR|O_CREAT|O_EXCL, 0600); +} + char *get_index_file(void) { if (!git_index_file) @@ -192,3 +231,14 @@ int set_git_dir(const char *path) setup_git_env(); return 0; } + +const char *get_log_output_encoding(void) +{ + return git_log_output_encoding ? git_log_output_encoding + : get_commit_output_encoding(); +} + +const char *get_commit_output_encoding(void) +{ + return git_commit_encoding ? git_commit_encoding : "UTF-8"; +} diff --git a/fast-import.c b/fast-import.c index 77549ebd6f..534c68db6f 100644 --- a/fast-import.c +++ b/fast-import.c @@ -156,6 +156,7 @@ Format of STDIN stream: #include "csum-file.h" #include "quote.h" #include "exec_cmd.h" +#include "dir.h" #define PACK_ID_BITS 16 #define MAX_PACK_ID ((1<<PACK_ID_BITS)-1) @@ -1437,6 +1438,20 @@ static void store_tree(struct tree_entry *root) t->entry_count -= del; } +static void tree_content_replace( + struct tree_entry *root, + const unsigned char *sha1, + const uint16_t mode, + struct tree_content *newtree) +{ + if (!S_ISDIR(mode)) + die("Root cannot be a non-directory"); + hashcpy(root->versions[1].sha1, sha1); + if (root->tree) + release_tree_content_recursive(root->tree); + root->tree = newtree; +} + static int tree_content_set( struct tree_entry *root, const char *p, @@ -1444,7 +1459,7 @@ static int tree_content_set( const uint16_t mode, struct tree_content *subtree) { - struct tree_content *t = root->tree; + struct tree_content *t; const char *slash1; unsigned int i, n; struct tree_entry *e; @@ -1454,23 +1469,17 @@ static int tree_content_set( n = slash1 - p; else n = strlen(p); - if (!slash1 && !n) { - if (!S_ISDIR(mode)) - die("Root cannot be a non-directory"); - hashcpy(root->versions[1].sha1, sha1); - if (root->tree) - release_tree_content_recursive(root->tree); - root->tree = subtree; - return 1; - } if (!n) die("Empty path component found in input"); if (!slash1 && !S_ISDIR(mode) && subtree) die("Non-directories cannot have subtrees"); + if (!root->tree) + load_tree(root); + t = root->tree; for (i = 0; i < t->entry_count; i++) { e = t->entries[i]; - if (e->name->str_len == n && !strncmp(p, e->name->str_dat, n)) { + if (e->name->str_len == n && !strncmp_icase(p, e->name->str_dat, n)) { if (!slash1) { if (!S_ISDIR(mode) && e->versions[1].mode == mode @@ -1523,7 +1532,7 @@ static int tree_content_remove( const char *p, struct tree_entry *backup_leaf) { - struct tree_content *t = root->tree; + struct tree_content *t; const char *slash1; unsigned int i, n; struct tree_entry *e; @@ -1534,9 +1543,12 @@ static int tree_content_remove( else n = strlen(p); + if (!root->tree) + load_tree(root); + t = root->tree; for (i = 0; i < t->entry_count; i++) { e = t->entries[i]; - if (e->name->str_len == n && !strncmp(p, e->name->str_dat, n)) { + if (e->name->str_len == n && !strncmp_icase(p, e->name->str_dat, n)) { if (slash1 && !S_ISDIR(e->versions[1].mode)) /* * If p names a file in some subdirectory, and a @@ -1581,7 +1593,7 @@ static int tree_content_get( const char *p, struct tree_entry *leaf) { - struct tree_content *t = root->tree; + struct tree_content *t; const char *slash1; unsigned int i, n; struct tree_entry *e; @@ -1592,9 +1604,12 @@ static int tree_content_get( else n = strlen(p); + if (!root->tree) + load_tree(root); + t = root->tree; for (i = 0; i < t->entry_count; i++) { e = t->entries[i]; - if (e->name->str_len == n && !strncmp(p, e->name->str_dat, n)) { + if (e->name->str_len == n && !strncmp_icase(p, e->name->str_dat, n)) { if (!slash1) { memcpy(leaf, e, sizeof(*leaf)); if (e->tree && is_null_sha1(e->versions[1].sha1)) @@ -2218,6 +2233,10 @@ static void file_change_m(struct branch *b) command_buf.buf); } + if (!*p) { + tree_content_replace(&b->branch_tree, sha1, mode, NULL); + return; + } tree_content_set(&b->branch_tree, p, sha1, mode, NULL); } @@ -2276,6 +2295,13 @@ static void file_change_cr(struct branch *b, int rename) tree_content_get(&b->branch_tree, s, &leaf); if (!leaf.versions[1].mode) die("Path %s not in branch", s); + if (!*d) { /* C "path/to/subdir" "" */ + tree_content_replace(&b->branch_tree, + leaf.versions[1].sha1, + leaf.versions[1].mode, + leaf.tree); + return; + } tree_content_set(&b->branch_tree, d, leaf.versions[1].sha1, leaf.versions[1].mode, diff --git a/git-add--interactive.perl b/git-add--interactive.perl index 77f60fa396..a329c5a1f8 100755 --- a/git-add--interactive.perl +++ b/git-add--interactive.perl @@ -89,6 +89,7 @@ my %patch_modes = ( TARGET => '', PARTICIPLE => 'staging', FILTER => 'file-only', + IS_REVERSE => 0, }, 'stash' => { DIFF => 'diff-index -p HEAD', @@ -98,6 +99,7 @@ my %patch_modes = ( TARGET => '', PARTICIPLE => 'stashing', FILTER => undef, + IS_REVERSE => 0, }, 'reset_head' => { DIFF => 'diff-index -p --cached', @@ -107,6 +109,7 @@ my %patch_modes = ( TARGET => '', PARTICIPLE => 'unstaging', FILTER => 'index-only', + IS_REVERSE => 1, }, 'reset_nothead' => { DIFF => 'diff-index -R -p --cached', @@ -116,6 +119,7 @@ my %patch_modes = ( TARGET => ' to index', PARTICIPLE => 'applying', FILTER => 'index-only', + IS_REVERSE => 0, }, 'checkout_index' => { DIFF => 'diff-files -p', @@ -125,6 +129,7 @@ my %patch_modes = ( TARGET => ' from worktree', PARTICIPLE => 'discarding', FILTER => 'file-only', + IS_REVERSE => 1, }, 'checkout_head' => { DIFF => 'diff-index -p', @@ -134,6 +139,7 @@ my %patch_modes = ( TARGET => ' from index and worktree', PARTICIPLE => 'discarding', FILTER => undef, + IS_REVERSE => 1, }, 'checkout_nothead' => { DIFF => 'diff-index -R -p', @@ -143,6 +149,7 @@ my %patch_modes = ( TARGET => ' to index and worktree', PARTICIPLE => 'applying', FILTER => undef, + IS_REVERSE => 0, }, ); @@ -1001,10 +1008,12 @@ sub edit_hunk_manually { print $fh "# Manual hunk edit mode -- see bottom for a quick guide\n"; print $fh @$oldtext; my $participle = $patch_mode_flavour{PARTICIPLE}; + my $is_reverse = $patch_mode_flavour{IS_REVERSE}; + my ($remove_plus, $remove_minus) = $is_reverse ? ('-', '+') : ('+', '-'); print $fh <<EOF; # --- -# To remove '-' lines, make them ' ' lines (context). -# To remove '+' lines, delete them. +# To remove '$remove_minus' lines, make them ' ' lines (context). +# To remove '$remove_plus' lines, delete them. # Lines starting with # will be removed. # # If the patch applies cleanly, the edited hunk will immediately be diff --git a/git-compat-util.h b/git-compat-util.h index 2af8d3edbe..d6d269f138 100644 --- a/git-compat-util.h +++ b/git-compat-util.h @@ -104,9 +104,14 @@ #include <assert.h> #include <regex.h> #include <utime.h> +#include <syslog.h> +#ifndef NO_SYS_POLL_H +#include <sys/poll.h> +#else +#include <poll.h> +#endif #ifndef __MINGW32__ #include <sys/wait.h> -#include <sys/poll.h> #include <sys/socket.h> #include <sys/ioctl.h> #include <termios.h> @@ -118,7 +123,11 @@ #include <arpa/inet.h> #include <netdb.h> #include <pwd.h> +#ifndef NO_INTTYPES_H #include <inttypes.h> +#else +#include <stdint.h> +#endif #if defined(__CYGWIN__) #undef _XOPEN_SOURCE #include <grp.h> @@ -386,6 +395,14 @@ static inline void *gitmempcpy(void *dest, const void *src, size_t n) } #endif +#ifdef NO_INET_PTON +int inet_pton(int af, const char *src, void *dst); +#endif + +#ifdef NO_INET_NTOP +const char *inet_ntop(int af, const void *src, char *dst, size_t size); +#endif + extern void release_pack_memory(size_t, int); typedef void (*try_to_free_t)(size_t); @@ -404,6 +421,7 @@ extern ssize_t xwrite(int fd, const void *buf, size_t len); extern int xdup(int fd); extern FILE *xfdopen(int fd, const char *mode); extern int xmkstemp(char *template); +extern int xmkstemp_mode(char *template, int mode); extern int odb_mkstemp(char *template, size_t limit, const char *pattern); extern int odb_pack_keep(char *name, size_t namesz, unsigned char *sha1); diff --git a/git-instaweb.sh b/git-instaweb.sh index e6f6ecda17..10fcebb119 100755 --- a/git-instaweb.sh +++ b/git-instaweb.sh @@ -580,6 +580,8 @@ gitweb_conf() { our \$projectroot = "$(dirname "$fqgitdir")"; our \$git_temp = "$fqgitdir/gitweb/tmp"; our \$projects_list = \$projectroot; + +\$feature{'remote_heads'}{'default'} = [1]; EOF } diff --git a/git-parse-remote.sh b/git-parse-remote.sh index 5f47b18141..1cc2ba6e09 100644 --- a/git-parse-remote.sh +++ b/git-parse-remote.sh @@ -66,7 +66,7 @@ get_remote_merge_branch () { origin="$1" default=$(get_default_remote) test -z "$origin" && origin=$default - curr_branch=$(git symbolic-ref -q HEAD) + curr_branch=$(git symbolic-ref -q HEAD) && [ "$origin" = "$default" ] && echo $(git for-each-ref --format='%(upstream)' $curr_branch) ;; @@ -89,7 +89,13 @@ get_remote_merge_branch () { refs/heads/*) remote=${remote#refs/heads/} ;; refs/* | tags/* | remotes/* ) remote= esac - - [ -n "$remote" ] && echo "refs/remotes/$repo/$remote" + [ -n "$remote" ] && case "$repo" in + .) + echo "refs/heads/$remote" + ;; + *) + echo "refs/remotes/$repo/$remote" + ;; + esac esac } diff --git a/git-pull.sh b/git-pull.sh index 8eb74d45de..20a3bbea07 100755 --- a/git-pull.sh +++ b/git-pull.sh @@ -201,10 +201,7 @@ test true = "$rebase" && { die "updating an unborn branch with changes added to the index" fi else - git update-index --ignore-submodules --refresh && - git diff-files --ignore-submodules --quiet && - git diff-index --ignore-submodules --cached --quiet HEAD -- || - die "refusing to pull with rebase: your working tree is not up-to-date" + require_clean_work_tree "pull with rebase" "Please commit or stash them." fi oldremoteref= && . git-parse-remote && diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh index a27952d9fd..5934b97fa1 100755 --- a/git-rebase--interactive.sh +++ b/git-rebase--interactive.sh @@ -153,14 +153,6 @@ run_pre_rebase_hook () { fi } -require_clean_work_tree () { - # test if working tree is dirty - git rev-parse --verify HEAD > /dev/null && - git update-index --ignore-submodules --refresh && - git diff-files --quiet --ignore-submodules && - git diff-index --cached --quiet HEAD --ignore-submodules -- || - die "Working tree is dirty" -} ORIG_REFLOG_ACTION="$GIT_REFLOG_ACTION" @@ -557,7 +549,7 @@ do_next () { exit "$status" fi # Run in subshell because require_clean_work_tree can die. - if ! (require_clean_work_tree) + if ! (require_clean_work_tree "rebase") then warn "Commit or stash your changes, and then run" warn @@ -675,9 +667,27 @@ get_saved_options () { # comes immediately after the former, and change "pick" to # "fixup"/"squash". rearrange_squash () { - sed -n -e 's/^pick \([0-9a-f]*\) \(squash\)! /\1 \2 /p' \ - -e 's/^pick \([0-9a-f]*\) \(fixup\)! /\1 \2 /p' \ - "$1" >"$1.sq" + # extract fixup!/squash! lines and resolve any referenced sha1's + while read -r pick sha1 message + do + case "$message" in + "squash! "*|"fixup! "*) + action="${message%%!*}" + rest="${message#*! }" + echo "$sha1 $action $rest" + # if it's a single word, try to resolve to a full sha1 and + # emit a second copy. This allows us to match on both message + # and on sha1 prefix + if test "${rest#* }" = "$rest"; then + fullsha="$(git rev-parse -q --verify "$rest" 2>/dev/null)" + if test -n "$fullsha"; then + # prefix the action to uniquely identify this line as + # intended for full sha1 match + echo "$sha1 +$action $fullsha" + fi + fi + esac + done >"$1.sq" <"$1" test -s "$1.sq" || return used= @@ -687,14 +697,26 @@ rearrange_squash () { *" $sha1 "*) continue ;; esac printf '%s\n' "$pick $sha1 $message" + used="$used$sha1 " while read -r squash action msg do - case "$message" in - "$msg"*) + case " $used" in + *" $squash "*) continue ;; + esac + emit=0 + case "$action" in + +*) + action="${action#+}" + # full sha1 prefix test + case "$msg" in "$sha1"*) emit=1;; esac ;; + *) + # message prefix test + case "$message" in "$msg"*) emit=1;; esac ;; + esac + if test $emit = 1; then printf '%s\n' "$action $squash $action! $msg" used="$used$squash " - ;; - esac + fi done <"$1.sq" done >"$1.rearranged" <"$1" cat "$1.rearranged" >"$1" @@ -768,7 +790,7 @@ first and then run 'git rebase --continue' again." record_in_rewritten "$(cat "$DOTEST"/stopped-sha)" - require_clean_work_tree + require_clean_work_tree "rebase" do_rest ;; --abort) @@ -866,7 +888,7 @@ first and then run 'git rebase --continue' again." comment_for_reflog start - require_clean_work_tree + require_clean_work_tree "rebase" "Please commit or stash them." if test ! -z "$1" then diff --git a/git-rebase.sh b/git-rebase.sh index 10a238ae3c..9a6d7a4730 100755 --- a/git-rebase.sh +++ b/git-rebase.sh @@ -49,7 +49,8 @@ do_merge= dotest="$GIT_DIR"/rebase-merge prec=4 verbose= -diffstat=$(git config --bool rebase.stat) +diffstat= +test "$(git config --bool rebase.stat)" = true && diffstat=t git_am_opt= rebase_root= force_rebase= @@ -412,19 +413,7 @@ else fi fi -# The tree must be really really clean. -if ! git update-index --ignore-submodules --refresh > /dev/null; then - echo >&2 "cannot rebase: you have unstaged changes" - git diff-files --name-status -r --ignore-submodules -- >&2 - exit 1 -fi -diff=$(git diff-index --cached --name-status -r --ignore-submodules HEAD --) -case "$diff" in -?*) echo >&2 "cannot rebase: your index contains uncommitted changes" - echo >&2 "$diff" - exit 1 - ;; -esac +require_clean_work_tree "rebase" "Please commit or stash them." if test -z "$rebase_root" then diff --git a/git-send-email.perl b/git-send-email.perl index f68ed5a5d3..76565de2ee 100755 --- a/git-send-email.perl +++ b/git-send-email.perl @@ -960,7 +960,7 @@ sub maildomain { sub send_message { my @recipients = unique_email_list(@to); @cc = (grep { my $cc = extract_valid_address($_); - not grep { $cc eq $_ } @recipients + not grep { $cc eq $_ || $_ =~ /<\Q${cc}\E>$/ } @recipients } map { sanitize_address($_) } @cc); @@ -1319,7 +1319,8 @@ foreach my $t (@files) { # set up for the next message if ($thread && $message_was_sent && - (chain_reply_to() || !defined $reply_to || length($reply_to) == 0)) { + (chain_reply_to() || !defined $reply_to || length($reply_to) == 0 || + $message_num == 1)) { $reply_to = $message_id; if (length $references > 0) { $references .= "\n $message_id"; diff --git a/git-sh-setup.sh b/git-sh-setup.sh index ae031a1375..aa16b83565 100644 --- a/git-sh-setup.sh +++ b/git-sh-setup.sh @@ -145,6 +145,35 @@ require_work_tree () { die "fatal: $0 cannot be used without a working tree." } +require_clean_work_tree () { + git rev-parse --verify HEAD >/dev/null || exit 1 + git update-index -q --ignore-submodules --refresh + err=0 + + if ! git diff-files --quiet --ignore-submodules + then + echo >&2 "Cannot $1: You have unstaged changes." + err=1 + fi + + if ! git diff-index --cached --quiet --ignore-submodules HEAD -- + then + if [ $err = 0 ] + then + echo >&2 "Cannot $1: Your index contains uncommitted changes." + else + echo >&2 "Additionally, your index contains uncommitted changes." + fi + err=1 + fi + + if [ $err = 1 ] + then + test -n "$2" && echo >&2 "$2" + exit 1 + fi +} + get_author_ident_from_commit () { pick_author_script=' /^author /{ diff --git a/git-svn.perl b/git-svn.perl index 757de82161..177dd259cd 100755 --- a/git-svn.perl +++ b/git-svn.perl @@ -84,7 +84,7 @@ my ($_stdin, $_help, $_edit, $_version, $_fetch_all, $_no_rebase, $_fetch_parent, $_merge, $_strategy, $_dry_run, $_local, $_prefix, $_no_checkout, $_url, $_verbose, - $_git_format, $_commit_url, $_tag); + $_git_format, $_commit_url, $_tag, $_merge_info); $Git::SVN::_follow_parent = 1; $_q ||= 0; my %remote_opts = ( 'username=s' => \$Git::SVN::Prompt::_username, @@ -154,6 +154,7 @@ my %cmd = ( 'commit-url=s' => \$_commit_url, 'revision|r=i' => \$_revision, 'no-rebase' => \$_no_rebase, + 'mergeinfo=s' => \$_merge_info, %cmt_opts, %fc_opts } ], branch => [ \&cmd_branch, 'Create a branch in the SVN repository', @@ -569,6 +570,7 @@ sub cmd_dcommit { print "Committed r$_[0]\n"; $cmt_rev = $_[0]; }, + mergeinfo => $_merge_info, svn_path => ''); if (!SVN::Git::Editor->new(\%ed_opts)->apply_diff) { print "No changes\n$d~1 == $d\n"; @@ -4451,6 +4453,7 @@ sub new { $self->{path_prefix} = length $self->{svn_path} ? "$self->{svn_path}/" : ''; $self->{config} = $opts->{config}; + $self->{mergeinfo} = $opts->{mergeinfo}; return $self; } @@ -4760,6 +4763,11 @@ sub change_file_prop { $self->SUPER::change_file_prop($fbat, $pname, $pval, $self->{pool}); } +sub change_dir_prop { + my ($self, $pbat, $pname, $pval) = @_; + $self->SUPER::change_dir_prop($pbat, $pname, $pval, $self->{pool}); +} + sub _chg_file_get_blob ($$$$) { my ($self, $fbat, $m, $which) = @_; my $fh = $::_repository->temp_acquire("git_blob_$which"); @@ -4853,6 +4861,11 @@ sub apply_diff { fatal("Invalid change type: $f"); } } + + if (defined($self->{mergeinfo})) { + $self->change_dir_prop($self->{bat}{''}, "svn:mergeinfo", + $self->{mergeinfo}); + } $self->rmdirs if $_rmdir; if (@$mods == 0) { $self->abort_edit; @@ -19,14 +19,22 @@ static struct startup_info git_startup_info; static int use_pager = -1; struct pager_config { const char *cmd; - int val; + int want; + char *value; }; static int pager_command_config(const char *var, const char *value, void *data) { struct pager_config *c = data; - if (!prefixcmp(var, "pager.") && !strcmp(var + 6, c->cmd)) - c->val = git_config_bool(var, value); + if (!prefixcmp(var, "pager.") && !strcmp(var + 6, c->cmd)) { + int b = git_config_maybe_bool(var, value); + if (b >= 0) + c->want = b; + else { + c->want = 1; + c->value = xstrdup(value); + } + } return 0; } @@ -35,9 +43,12 @@ int check_pager_config(const char *cmd) { struct pager_config c; c.cmd = cmd; - c.val = -1; + c.want = -1; + c.value = NULL; git_config(pager_command_config, &c); - return c.val; + if (c.value) + pager_program = c.value; + return c.want; } static void commit_pager_choice(void) { @@ -374,6 +385,8 @@ static void handle_internal_command(int argc, const char **argv) { "receive-pack", cmd_receive_pack }, { "reflog", cmd_reflog, RUN_SETUP }, { "remote", cmd_remote, RUN_SETUP }, + { "remote-ext", cmd_remote_ext }, + { "remote-fd", cmd_remote_fd }, { "replace", cmd_replace, RUN_SETUP }, { "repo-config", cmd_config, RUN_SETUP_GENTLY }, { "rerere", cmd_rerere, RUN_SETUP }, diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index 679f2da3ee..75306ca967 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -17,12 +17,10 @@ use Encode; use Fcntl ':mode'; use File::Find qw(); use File::Basename qw(basename); +use Time::HiRes qw(gettimeofday tv_interval); binmode STDOUT, ':utf8'; -our $t0; -if (eval { require Time::HiRes; 1; }) { - $t0 = [Time::HiRes::gettimeofday()]; -} +our $t0 = [ gettimeofday() ]; our $number_of_git_cmds = 0; BEGIN { @@ -493,6 +491,18 @@ our %feature = ( 'sub' => sub { feature_bool('highlight', @_) }, 'override' => 0, 'default' => [0]}, + + # Enable displaying of remote heads in the heads list + + # To enable system wide have in $GITWEB_CONFIG + # $feature{'remote_heads'}{'default'} = [1]; + # To have project specific config enable override in $GITWEB_CONFIG + # $feature{'remote_heads'}{'override'} = 1; + # and in project config gitweb.remote_heads = 0|1; + 'remote_heads' => { + 'sub' => sub { feature_bool('remote_heads', @_) }, + 'override' => 0, + 'default' => [0]}, ); sub gitweb_get_feature { @@ -707,6 +717,7 @@ our %actions = ( "log" => \&git_log, "patch" => \&git_patch, "patches" => \&git_patches, + "remotes" => \&git_remotes, "rss" => \&git_rss, "atom" => \&git_atom, "search" => \&git_search, @@ -1065,7 +1076,7 @@ sub dispatch { } sub reset_timer { - our $t0 = [Time::HiRes::gettimeofday()] + our $t0 = [ gettimeofday() ] if defined $t0; our $number_of_git_cmds = 0; } @@ -2759,6 +2770,44 @@ sub git_get_last_activity { return (undef, undef); } +# Implementation note: when a single remote is wanted, we cannot use 'git +# remote show -n' because that command always work (assuming it's a remote URL +# if it's not defined), and we cannot use 'git remote show' because that would +# try to make a network roundtrip. So the only way to find if that particular +# remote is defined is to walk the list provided by 'git remote -v' and stop if +# and when we find what we want. +sub git_get_remotes_list { + my $wanted = shift; + my %remotes = (); + + open my $fd, '-|' , git_cmd(), 'remote', '-v'; + return unless $fd; + while (my $remote = <$fd>) { + chomp $remote; + $remote =~ s!\t(.*?)\s+\((\w+)\)$!!; + next if $wanted and not $remote eq $wanted; + my ($url, $key) = ($1, $2); + + $remotes{$remote} ||= { 'heads' => () }; + $remotes{$remote}{$key} = $url; + } + close $fd or return; + return wantarray ? %remotes : \%remotes; +} + +# Takes a hash of remotes as first parameter and fills it by adding the +# available remote heads for each of the indicated remotes. +sub fill_remote_heads { + my $remotes = shift; + my @heads = map { "remotes/$_" } keys %$remotes; + my @remoteheads = git_get_heads_list(undef, @heads); + foreach my $remote (keys %$remotes) { + $remotes->{$remote}{'heads'} = [ grep { + $_->{'name'} =~ s!^$remote/!! + } @remoteheads ]; + } +} + sub git_get_references { my $type = shift || ""; my %refs; @@ -3157,13 +3206,15 @@ sub parse_from_to_diffinfo { ## parse to array of hashes functions sub git_get_heads_list { - my $limit = shift; + my ($limit, @classes) = @_; + @classes = ('heads') unless @classes; + my @patterns = map { "refs/$_" } @classes; my @headslist; open my $fd, '-|', git_cmd(), 'for-each-ref', ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate', '--format=%(objectname) %(refname) %(subject)%00%(committer)', - 'refs/heads' + @patterns or return; while (my $line = <$fd>) { my %ref_item; @@ -3174,7 +3225,7 @@ sub git_get_heads_list { my ($committer, $epoch, $tz) = ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/); $ref_item{'fullname'} = $name; - $name =~ s!^refs/heads/!!; + $name =~ s!^refs/(?:head|remote)s/!!; $ref_item{'name'} = $name; $ref_item{'id'} = $hash; @@ -3511,7 +3562,15 @@ EOF if (defined $project) { print $cgi->a({-href => href(action=>"summary")}, esc_html($project)); if (defined $action) { - print " / $action"; + my $action_print = $action ; + if (defined $opts{-action_extra}) { + $action_print = $cgi->a({-href => href(action=>$action)}, + $action); + } + print " / $action_print"; + } + if (defined $opts{-action_extra}) { + print " / $opts{-action_extra}"; } print "\n"; } @@ -3590,7 +3649,7 @@ sub git_footer_html { print "<div id=\"generating_info\">\n"; print 'This page took '. '<span id="generating_time" class="time_span">'. - Time::HiRes::tv_interval($t0, [Time::HiRes::gettimeofday()]). + tv_interval($t0, [ gettimeofday() ]). ' seconds </span>'. ' and '. '<span id="generating_cmd">'. @@ -3718,6 +3777,19 @@ sub git_print_page_nav { "</div>\n"; } +# returns a submenu for the nagivation of the refs views (tags, heads, +# remotes) with the current view disabled and the remotes view only +# available if the feature is enabled +sub format_ref_views { + my ($current) = @_; + my @ref_views = qw{tags heads}; + push @ref_views, 'remotes' if gitweb_check_feature('remote_heads'); + return join " | ", map { + $_ eq $current ? $_ : + $cgi->a({-href => href(action=>$_)}, $_) + } @ref_views +} + sub format_paging_nav { my ($action, $page, $has_next_link) = @_; my $paging_nav; @@ -3761,6 +3833,49 @@ sub git_print_header_div { "\n</div>\n"; } +sub format_repo_url { + my ($name, $url) = @_; + return "<tr class=\"metadata_url\"><td>$name</td><td>$url</td></tr>\n"; +} + +# Group output by placing it in a DIV element and adding a header. +# Options for start_div() can be provided by passing a hash reference as the +# first parameter to the function. +# Options to git_print_header_div() can be provided by passing an array +# reference. This must follow the options to start_div if they are present. +# The content can be a scalar, which is output as-is, a scalar reference, which +# is output after html escaping, an IO handle passed either as *handle or +# *handle{IO}, or a function reference. In the latter case all following +# parameters will be taken as argument to the content function call. +sub git_print_section { + my ($div_args, $header_args, $content); + my $arg = shift; + if (ref($arg) eq 'HASH') { + $div_args = $arg; + $arg = shift; + } + if (ref($arg) eq 'ARRAY') { + $header_args = $arg; + $arg = shift; + } + $content = $arg; + + print $cgi->start_div($div_args); + git_print_header_div(@$header_args); + + if (ref($content) eq 'CODE') { + $content->(@_); + } elsif (ref($content) eq 'SCALAR') { + print esc_html($$content); + } elsif (ref($content) eq 'GLOB' or ref($content) eq 'IO::Handle') { + print <$content>; + } elsif (!ref($content) && defined($content)) { + print $content; + } + + print $cgi->end_div; +} + sub print_local_time { print format_local_time(@_); } @@ -4960,7 +5075,7 @@ sub git_heads_body { "<td class=\"link\">" . $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " . $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " . - $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'name'})}, "tree") . + $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'fullname'})}, "tree") . "</td>\n" . "</tr>"; } @@ -4972,6 +5087,101 @@ sub git_heads_body { print "</table>\n"; } +# Display a single remote block +sub git_remote_block { + my ($remote, $rdata, $limit, $head) = @_; + + my $heads = $rdata->{'heads'}; + my $fetch = $rdata->{'fetch'}; + my $push = $rdata->{'push'}; + + my $urls_table = "<table class=\"projects_list\">\n" ; + + if (defined $fetch) { + if ($fetch eq $push) { + $urls_table .= format_repo_url("URL", $fetch); + } else { + $urls_table .= format_repo_url("Fetch URL", $fetch); + $urls_table .= format_repo_url("Push URL", $push) if defined $push; + } + } elsif (defined $push) { + $urls_table .= format_repo_url("Push URL", $push); + } else { + $urls_table .= format_repo_url("", "No remote URL"); + } + + $urls_table .= "</table>\n"; + + my $dots; + if (defined $limit && $limit < @$heads) { + $dots = $cgi->a({-href => href(action=>"remotes", hash=>$remote)}, "..."); + } + + print $urls_table; + git_heads_body($heads, $head, 0, $limit, $dots); +} + +# Display a list of remote names with the respective fetch and push URLs +sub git_remotes_list { + my ($remotedata, $limit) = @_; + print "<table class=\"heads\">\n"; + my $alternate = 1; + my @remotes = sort keys %$remotedata; + + my $limited = $limit && $limit < @remotes; + + $#remotes = $limit - 1 if $limited; + + while (my $remote = shift @remotes) { + my $rdata = $remotedata->{$remote}; + my $fetch = $rdata->{'fetch'}; + my $push = $rdata->{'push'}; + if ($alternate) { + print "<tr class=\"dark\">\n"; + } else { + print "<tr class=\"light\">\n"; + } + $alternate ^= 1; + print "<td>" . + $cgi->a({-href=> href(action=>'remotes', hash=>$remote), + -class=> "list name"},esc_html($remote)) . + "</td>"; + print "<td class=\"link\">" . + (defined $fetch ? $cgi->a({-href=> $fetch}, "fetch") : "fetch") . + " | " . + (defined $push ? $cgi->a({-href=> $push}, "push") : "push") . + "</td>"; + + print "</tr>\n"; + } + + if ($limited) { + print "<tr>\n" . + "<td colspan=\"3\">" . + $cgi->a({-href => href(action=>"remotes")}, "...") . + "</td>\n" . "</tr>\n"; + } + + print "</table>"; +} + +# Display remote heads grouped by remote, unless there are too many +# remotes, in which case we only display the remote names +sub git_remotes_body { + my ($remotedata, $limit, $head) = @_; + if ($limit and $limit < keys %$remotedata) { + git_remotes_list($remotedata, $limit); + } else { + fill_remote_heads($remotedata); + while (my ($remote, $rdata) = each %$remotedata) { + git_print_section({-class=>"remote", -id=>$remote}, + ["remotes", $remote, $remote], sub { + git_remote_block($remote, $rdata, $limit, $head); + }); + } + } +} + sub git_search_grep_body { my ($commitlist, $from, $to, $extra) = @_; $from = 0 unless defined $from; @@ -5109,6 +5319,7 @@ sub git_summary { my %co = parse_commit("HEAD"); my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : (); my $head = $co{'id'}; + my $remote_heads = gitweb_check_feature('remote_heads'); my $owner = git_get_project_owner($project); @@ -5117,6 +5328,7 @@ sub git_summary { # there are more ... my @taglist = git_get_tags_list(16); my @headlist = git_get_heads_list(16); + my %remotedata = $remote_heads ? git_get_remotes_list() : (); my @forklist; my $check_forks = gitweb_check_feature('forks'); @@ -5142,7 +5354,7 @@ sub git_summary { @url_list = map { "$_/$project" } @git_base_url_list unless @url_list; foreach my $git_url (@url_list) { next unless $git_url; - print "<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n"; + print format_repo_url($url_tag, $git_url); $url_tag = ""; } @@ -5194,6 +5406,11 @@ sub git_summary { $cgi->a({-href => href(action=>"heads")}, "...")); } + if (%remotedata) { + git_print_header_div('remotes'); + git_remotes_body(\%remotedata, 15, $head); + } + if (@forklist) { git_print_header_div('forks'); git_project_list_body(\@forklist, 'age', 0, 15, @@ -5298,7 +5515,7 @@ sub git_blame_common { print 'END'; if (defined $t0 && gitweb_check_feature('timed')) { print ' '. - Time::HiRes::tv_interval($t0, [Time::HiRes::gettimeofday()]). + tv_interval($t0, [ gettimeofday() ]). ' '.$number_of_git_cmds; } print "\n"; @@ -5485,7 +5702,7 @@ sub git_blame_data { sub git_tags { my $head = git_get_head_hash($project); git_header_html(); - git_print_page_nav('','', $head,undef,$head); + git_print_page_nav('','', $head,undef,$head,format_ref_views('tags')); git_print_header_div('summary', $project); my @tagslist = git_get_tags_list(); @@ -5498,7 +5715,7 @@ sub git_tags { sub git_heads { my $head = git_get_head_hash($project); git_header_html(); - git_print_page_nav('','', $head,undef,$head); + git_print_page_nav('','', $head,undef,$head,format_ref_views('heads')); git_print_header_div('summary', $project); my @headslist = git_get_heads_list(); @@ -5508,6 +5725,39 @@ sub git_heads { git_footer_html(); } +# used both for single remote view and for list of all the remotes +sub git_remotes { + gitweb_check_feature('remote_heads') + or die_error(403, "Remote heads view is disabled"); + + my $head = git_get_head_hash($project); + my $remote = $input_params{'hash'}; + + my $remotedata = git_get_remotes_list($remote); + die_error(500, "Unable to get remote information") unless defined $remotedata; + + unless (%$remotedata) { + die_error(404, defined $remote ? + "Remote $remote not found" : + "No remotes found"); + } + + git_header_html(undef, undef, -action_extra => $remote); + git_print_page_nav('', '', $head, undef, $head, + format_ref_views($remote ? '' : 'remotes')); + + fill_remote_heads($remotedata); + if (defined $remote) { + git_print_header_div('remotes', "$remote remote for $project"); + git_remote_block($remote, $remotedata->{$remote}, undef, $head); + } else { + git_print_header_div('summary', "$project remotes"); + git_remotes_body($remotedata, undef, $head); + } + + git_footer_html(); +} + sub git_blob_plain { my $type = shift; my $expires; diff --git a/gitweb/static/gitweb.css b/gitweb/static/gitweb.css index 4132aabcdb..79d7eebba7 100644 --- a/gitweb/static/gitweb.css +++ b/gitweb/static/gitweb.css @@ -573,6 +573,12 @@ div.binary { font-style: italic; } +div.remote { + margin: .5em; + border: 1px solid #d9d8d1; + display: inline-block; +} + /* Style definition generated by highlight 2.4.5, http://www.andre-simon.de/ */ /* Highlighting theme definition: */ @@ -2,6 +2,7 @@ #include "pack.h" #include "sideband.h" #include "run-command.h" +#include "url.h" int data_received; int active_requests; @@ -279,6 +280,11 @@ static CURL *get_curl_handle(void) } curl_easy_setopt(result, CURLOPT_FOLLOWLOCATION, 1); +#if LIBCURL_VERSION_NUM >= 0x071301 + curl_easy_setopt(result, CURLOPT_POSTREDIR, CURL_REDIR_POST_ALL); +#elif LIBCURL_VERSION_NUM >= 0x071101 + curl_easy_setopt(result, CURLOPT_POST301, 1); +#endif if (getenv("GIT_CURL_VERBOSE")) curl_easy_setopt(result, CURLOPT_VERBOSE, 1); @@ -297,7 +303,7 @@ static CURL *get_curl_handle(void) static void http_auth_init(const char *url) { - char *at, *colon, *cp, *slash; + char *at, *colon, *cp, *slash, *decoded; int len; cp = strstr(url, "://"); @@ -322,16 +328,25 @@ static void http_auth_init(const char *url) user_name = xmalloc(len + 1); memcpy(user_name, cp, len); user_name[len] = '\0'; + decoded = url_decode(user_name); + free(user_name); + user_name = decoded; user_pass = NULL; } else { len = colon - cp; user_name = xmalloc(len + 1); memcpy(user_name, cp, len); user_name[len] = '\0'; + decoded = url_decode(user_name); + free(user_name); + user_name = decoded; len = at - (colon + 1); user_pass = xmalloc(len + 1); memcpy(user_pass, colon + 1, len); user_pass[len] = '\0'; + decoded = url_decode(user_pass); + free(user_pass); + user_pass = decoded; } } diff --git a/merge-recursive.c b/merge-recursive.c index 875859f68e..16c2dbeab9 100644 --- a/merge-recursive.c +++ b/merge-recursive.c @@ -63,6 +63,22 @@ static int sha_eq(const unsigned char *a, const unsigned char *b) return a && b && hashcmp(a, b) == 0; } +enum rename_type { + RENAME_NORMAL = 0, + RENAME_DELETE, + RENAME_ONE_FILE_TO_TWO +}; + +struct rename_df_conflict_info { + enum rename_type rename_type; + struct diff_filepair *pair1; + struct diff_filepair *pair2; + const char *branch1; + const char *branch2; + struct stage_data *dst_entry1; + struct stage_data *dst_entry2; +}; + /* * Since we want to write the index eventually, we cannot reuse the index * for these (temporary) data. @@ -74,9 +90,37 @@ struct stage_data unsigned mode; unsigned char sha[20]; } stages[4]; + struct rename_df_conflict_info *rename_df_conflict_info; unsigned processed:1; }; +static inline void setup_rename_df_conflict_info(enum rename_type rename_type, + struct diff_filepair *pair1, + struct diff_filepair *pair2, + const char *branch1, + const char *branch2, + struct stage_data *dst_entry1, + struct stage_data *dst_entry2) +{ + struct rename_df_conflict_info *ci = xcalloc(1, sizeof(struct rename_df_conflict_info)); + ci->rename_type = rename_type; + ci->pair1 = pair1; + ci->branch1 = branch1; + ci->branch2 = branch2; + + ci->dst_entry1 = dst_entry1; + dst_entry1->rename_df_conflict_info = ci; + dst_entry1->processed = 0; + + assert(!pair2 == !dst_entry2); + if (dst_entry2) { + ci->dst_entry2 = dst_entry2; + ci->pair2 = pair2; + dst_entry2->rename_df_conflict_info = ci; + dst_entry2->processed = 0; + } +} + static int show(struct merge_options *o, int v) { return (!o->call_depth && o->verbosity >= v) || o->verbosity >= 5; @@ -302,6 +346,63 @@ static struct string_list *get_unmerged(void) return unmerged; } +static void make_room_for_directories_of_df_conflicts(struct merge_options *o, + struct string_list *entries) +{ + /* If there are D/F conflicts, and the paths currently exist + * in the working copy as a file, we want to remove them to + * make room for the corresponding directory. Such paths will + * later be processed in process_df_entry() at the end. If + * the corresponding directory ends up being removed by the + * merge, then the file will be reinstated at that time; + * otherwise, if the file is not supposed to be removed by the + * merge, the contents of the file will be placed in another + * unique filename. + * + * NOTE: This function relies on the fact that entries for a + * D/F conflict will appear adjacent in the index, with the + * entries for the file appearing before entries for paths + * below the corresponding directory. + */ + const char *last_file = NULL; + int last_len = 0; + struct stage_data *last_e; + int i; + + for (i = 0; i < entries->nr; i++) { + const char *path = entries->items[i].string; + int len = strlen(path); + struct stage_data *e = entries->items[i].util; + + /* + * Check if last_file & path correspond to a D/F conflict; + * i.e. whether path is last_file+'/'+<something>. + * If so, remove last_file to make room for path and friends. + */ + if (last_file && + len > last_len && + memcmp(path, last_file, last_len) == 0 && + path[last_len] == '/') { + output(o, 3, "Removing %s to make room for subdirectory; may re-add later.", last_file); + unlink(last_file); + } + + /* + * Determine whether path could exist as a file in the + * working directory as a possible D/F conflict. This + * will only occur when it exists in stage 2 as a + * file. + */ + if (S_ISREG(e->stages[2].mode) || S_ISLNK(e->stages[2].mode)) { + last_file = path; + last_len = len; + last_e = e; + } else { + last_file = NULL; + } + } +} + struct rename { struct diff_filepair *pair; @@ -374,11 +475,10 @@ static struct string_list *get_renames(struct merge_options *o, return renames; } -static int update_stages(const char *path, struct diff_filespec *o, +static int update_stages_options(const char *path, struct diff_filespec *o, struct diff_filespec *a, struct diff_filespec *b, - int clear) + int clear, int options) { - int options = ADD_CACHE_OK_TO_ADD | ADD_CACHE_OK_TO_REPLACE; if (clear) if (remove_file_from_cache(path)) return -1; @@ -394,6 +494,34 @@ static int update_stages(const char *path, struct diff_filespec *o, return 0; } +static int update_stages(const char *path, struct diff_filespec *o, + struct diff_filespec *a, struct diff_filespec *b, + int clear) +{ + int options = ADD_CACHE_OK_TO_ADD | ADD_CACHE_OK_TO_REPLACE; + return update_stages_options(path, o, a, b, clear, options); +} + +static int update_stages_and_entry(const char *path, + struct stage_data *entry, + struct diff_filespec *o, + struct diff_filespec *a, + struct diff_filespec *b, + int clear) +{ + int options; + + entry->processed = 0; + entry->stages[1].mode = o->mode; + entry->stages[2].mode = a->mode; + entry->stages[3].mode = b->mode; + hashcpy(entry->stages[1].sha, o->sha1); + hashcpy(entry->stages[2].sha, a->sha1); + hashcpy(entry->stages[3].sha, b->sha1); + options = ADD_CACHE_OK_TO_ADD | ADD_CACHE_SKIP_DFCHECK; + return update_stages_options(path, o, a, b, clear, options); +} + static int remove_file(struct merge_options *o, int clean, const char *path, int no_wd) { @@ -732,29 +860,56 @@ static struct merge_file_info merge_file(struct merge_options *o, return result; } -static void conflict_rename_rename(struct merge_options *o, - struct rename *ren1, - const char *branch1, - struct rename *ren2, - const char *branch2) +static void conflict_rename_delete(struct merge_options *o, + struct diff_filepair *pair, + const char *rename_branch, + const char *other_branch) +{ + char *dest_name = pair->two->path; + int df_conflict = 0; + struct stat st; + + output(o, 1, "CONFLICT (rename/delete): Rename %s->%s in %s " + "and deleted in %s", + pair->one->path, pair->two->path, rename_branch, + other_branch); + if (!o->call_depth) + update_stages(dest_name, NULL, + rename_branch == o->branch1 ? pair->two : NULL, + rename_branch == o->branch1 ? NULL : pair->two, + 1); + if (lstat(dest_name, &st) == 0 && S_ISDIR(st.st_mode)) { + dest_name = unique_path(o, dest_name, rename_branch); + df_conflict = 1; + } + update_file(o, 0, pair->two->sha1, pair->two->mode, dest_name); + if (df_conflict) + free(dest_name); +} + +static void conflict_rename_rename_1to2(struct merge_options *o, + struct diff_filepair *pair1, + const char *branch1, + struct diff_filepair *pair2, + const char *branch2) { + /* One file was renamed in both branches, but to different names. */ char *del[2]; int delp = 0; - const char *ren1_dst = ren1->pair->two->path; - const char *ren2_dst = ren2->pair->two->path; + const char *ren1_dst = pair1->two->path; + const char *ren2_dst = pair2->two->path; const char *dst_name1 = ren1_dst; const char *dst_name2 = ren2_dst; - if (string_list_has_string(&o->current_directory_set, ren1_dst)) { + struct stat st; + if (lstat(ren1_dst, &st) == 0 && S_ISDIR(st.st_mode)) { dst_name1 = del[delp++] = unique_path(o, ren1_dst, branch1); output(o, 1, "%s is a directory in %s adding as %s instead", ren1_dst, branch2, dst_name1); - remove_file(o, 0, ren1_dst, 0); } - if (string_list_has_string(&o->current_directory_set, ren2_dst)) { + if (lstat(ren2_dst, &st) == 0 && S_ISDIR(st.st_mode)) { dst_name2 = del[delp++] = unique_path(o, ren2_dst, branch2); output(o, 1, "%s is a directory in %s adding as %s instead", ren2_dst, branch1, dst_name2); - remove_file(o, 0, ren2_dst, 0); } if (o->call_depth) { remove_file_from_cache(dst_name1); @@ -762,34 +917,27 @@ static void conflict_rename_rename(struct merge_options *o, /* * Uncomment to leave the conflicting names in the resulting tree * - * update_file(o, 0, ren1->pair->two->sha1, ren1->pair->two->mode, dst_name1); - * update_file(o, 0, ren2->pair->two->sha1, ren2->pair->two->mode, dst_name2); + * update_file(o, 0, pair1->two->sha1, pair1->two->mode, dst_name1); + * update_file(o, 0, pair2->two->sha1, pair2->two->mode, dst_name2); */ } else { - update_stages(dst_name1, NULL, ren1->pair->two, NULL, 1); - update_stages(dst_name2, NULL, NULL, ren2->pair->two, 1); + update_stages(ren1_dst, NULL, pair1->two, NULL, 1); + update_stages(ren2_dst, NULL, NULL, pair2->two, 1); + + update_file(o, 0, pair1->two->sha1, pair1->two->mode, dst_name1); + update_file(o, 0, pair2->two->sha1, pair2->two->mode, dst_name2); } while (delp--) free(del[delp]); } -static void conflict_rename_dir(struct merge_options *o, - struct rename *ren1, - const char *branch1) -{ - char *new_path = unique_path(o, ren1->pair->two->path, branch1); - output(o, 1, "Renaming %s to %s instead", ren1->pair->one->path, new_path); - remove_file(o, 0, ren1->pair->two->path, 0); - update_file(o, 0, ren1->pair->two->sha1, ren1->pair->two->mode, new_path); - free(new_path); -} - -static void conflict_rename_rename_2(struct merge_options *o, - struct rename *ren1, - const char *branch1, - struct rename *ren2, - const char *branch2) +static void conflict_rename_rename_2to1(struct merge_options *o, + struct rename *ren1, + const char *branch1, + struct rename *ren2, + const char *branch2) { + /* Two files were renamed to the same thing. */ char *new_path1 = unique_path(o, ren1->pair->two->path, branch1); char *new_path2 = unique_path(o, ren2->pair->two->path, branch2); output(o, 1, "Renaming %s to %s and %s to %s instead", @@ -879,84 +1027,60 @@ static int process_renames(struct merge_options *o, ren2->dst_entry->processed = 1; ren2->processed = 1; if (strcmp(ren1_dst, ren2_dst) != 0) { - clean_merge = 0; - output(o, 1, "CONFLICT (rename/rename): " - "Rename \"%s\"->\"%s\" in branch \"%s\" " - "rename \"%s\"->\"%s\" in \"%s\"%s", - src, ren1_dst, branch1, - src, ren2_dst, branch2, - o->call_depth ? " (left unresolved)": ""); - if (o->call_depth) { - remove_file_from_cache(src); - update_file(o, 0, ren1->pair->one->sha1, - ren1->pair->one->mode, src); - } - conflict_rename_rename(o, ren1, branch1, ren2, branch2); + setup_rename_df_conflict_info(RENAME_ONE_FILE_TO_TWO, + ren1->pair, + ren2->pair, + branch1, + branch2, + ren1->dst_entry, + ren2->dst_entry); } else { - struct merge_file_info mfi; remove_file(o, 1, ren1_src, 1); - mfi = merge_file(o, - ren1->pair->one, - ren1->pair->two, - ren2->pair->two, - branch1, - branch2); - if (mfi.merge || !mfi.clean) - output(o, 1, "Renaming %s->%s", src, ren1_dst); - - if (mfi.merge) - output(o, 2, "Auto-merging %s", ren1_dst); - - if (!mfi.clean) { - output(o, 1, "CONFLICT (content): merge conflict in %s", - ren1_dst); - clean_merge = 0; - - if (!o->call_depth) - update_stages(ren1_dst, - ren1->pair->one, - ren1->pair->two, - ren2->pair->two, - 1 /* clear */); - } - update_file(o, mfi.clean, mfi.sha, mfi.mode, ren1_dst); + update_stages_and_entry(ren1_dst, + ren1->dst_entry, + ren1->pair->one, + ren1->pair->two, + ren2->pair->two, + 1 /* clear */); } } else { /* Renamed in 1, maybe changed in 2 */ struct string_list_item *item; /* we only use sha1 and mode of these */ struct diff_filespec src_other, dst_other; - int try_merge, stage = a_renames == renames1 ? 3: 2; + int try_merge; - remove_file(o, 1, ren1_src, o->call_depth || stage == 3); + /* + * unpack_trees loads entries from common-commit + * into stage 1, from head-commit into stage 2, and + * from merge-commit into stage 3. We keep track + * of which side corresponds to the rename. + */ + int renamed_stage = a_renames == renames1 ? 2 : 3; + int other_stage = a_renames == renames1 ? 3 : 2; - hashcpy(src_other.sha1, ren1->src_entry->stages[stage].sha); - src_other.mode = ren1->src_entry->stages[stage].mode; - hashcpy(dst_other.sha1, ren1->dst_entry->stages[stage].sha); - dst_other.mode = ren1->dst_entry->stages[stage].mode; + remove_file(o, 1, ren1_src, o->call_depth || renamed_stage == 2); + hashcpy(src_other.sha1, ren1->src_entry->stages[other_stage].sha); + src_other.mode = ren1->src_entry->stages[other_stage].mode; + hashcpy(dst_other.sha1, ren1->dst_entry->stages[other_stage].sha); + dst_other.mode = ren1->dst_entry->stages[other_stage].mode; try_merge = 0; - if (string_list_has_string(&o->current_directory_set, ren1_dst)) { - clean_merge = 0; - output(o, 1, "CONFLICT (rename/directory): Rename %s->%s in %s " - " directory %s added in %s", - ren1_src, ren1_dst, branch1, - ren1_dst, branch2); - conflict_rename_dir(o, ren1, branch1); - } else if (sha_eq(src_other.sha1, null_sha1)) { - clean_merge = 0; - output(o, 1, "CONFLICT (rename/delete): Rename %s->%s in %s " - "and deleted in %s", - ren1_src, ren1_dst, branch1, - branch2); - update_file(o, 0, ren1->pair->two->sha1, ren1->pair->two->mode, ren1_dst); - if (!o->call_depth) - update_stages(ren1_dst, NULL, - branch1 == o->branch1 ? - ren1->pair->two : NULL, - branch1 == o->branch1 ? - NULL : ren1->pair->two, 1); + if (sha_eq(src_other.sha1, null_sha1)) { + if (string_list_has_string(&o->current_directory_set, ren1_dst)) { + ren1->dst_entry->processed = 0; + setup_rename_df_conflict_info(RENAME_DELETE, + ren1->pair, + NULL, + branch1, + branch2, + ren1->dst_entry, + NULL); + } else { + clean_merge = 0; + conflict_rename_delete(o, ren1->pair, branch1, branch2); + } } else if ((dst_other.mode == ren1->pair->two->mode) && sha_eq(dst_other.sha1, ren1->pair->two->sha1)) { /* Added file on the other side @@ -991,6 +1115,7 @@ static int process_renames(struct merge_options *o, mfi.sha, mfi.mode, ren1_dst); + try_merge = 0; } else { new_path = unique_path(o, ren1_dst, branch2); output(o, 1, "Adding as %s instead", new_path); @@ -1005,13 +1130,12 @@ static int process_renames(struct merge_options *o, "Rename %s->%s in %s", ren1_src, ren1_dst, branch1, ren2->pair->one->path, ren2->pair->two->path, branch2); - conflict_rename_rename_2(o, ren1, branch1, ren2, branch2); + conflict_rename_rename_2to1(o, ren1, branch1, ren2, branch2); } else try_merge = 1; if (try_merge) { struct diff_filespec *one, *a, *b; - struct merge_file_info mfi; src_other.path = (char *)ren1_src; one = ren1->pair->one; @@ -1022,41 +1146,15 @@ static int process_renames(struct merge_options *o, b = ren1->pair->two; a = &src_other; } - mfi = merge_file(o, one, a, b, - o->branch1, o->branch2); - - if (mfi.clean && - sha_eq(mfi.sha, ren1->pair->two->sha1) && - mfi.mode == ren1->pair->two->mode) { - /* - * This message is part of - * t6022 test. If you change - * it update the test too. - */ - output(o, 3, "Skipped %s (merged same as existing)", ren1_dst); - - /* There may be higher stage entries left - * in the index (e.g. due to a D/F - * conflict) that need to be resolved. - */ - if (!ren1->dst_entry->stages[2].mode != - !ren1->dst_entry->stages[3].mode) - ren1->dst_entry->processed = 0; - } else { - if (mfi.merge || !mfi.clean) - output(o, 1, "Renaming %s => %s", ren1_src, ren1_dst); - if (mfi.merge) - output(o, 2, "Auto-merging %s", ren1_dst); - if (!mfi.clean) { - output(o, 1, "CONFLICT (rename/modify): Merge conflict in %s", - ren1_dst); - clean_merge = 0; - - if (!o->call_depth) - update_stages(ren1_dst, - one, a, b, 1); - } - update_file(o, mfi.clean, mfi.sha, mfi.mode, ren1_dst); + update_stages_and_entry(ren1_dst, ren1->dst_entry, one, a, b, 1); + if (string_list_has_string(&o->current_directory_set, ren1_dst)) { + setup_rename_df_conflict_info(RENAME_NORMAL, + ren1->pair, + NULL, + branch1, + NULL, + ren1->dst_entry, + NULL); } } } @@ -1119,6 +1217,90 @@ error_return: return ret; } +static void handle_delete_modify(struct merge_options *o, + const char *path, + const char *new_path, + unsigned char *a_sha, int a_mode, + unsigned char *b_sha, int b_mode) +{ + if (!a_sha) { + output(o, 1, "CONFLICT (delete/modify): %s deleted in %s " + "and modified in %s. Version %s of %s left in tree%s%s.", + path, o->branch1, + o->branch2, o->branch2, path, + path == new_path ? "" : " at ", + path == new_path ? "" : new_path); + update_file(o, 0, b_sha, b_mode, new_path); + } else { + output(o, 1, "CONFLICT (delete/modify): %s deleted in %s " + "and modified in %s. Version %s of %s left in tree%s%s.", + path, o->branch2, + o->branch1, o->branch1, path, + path == new_path ? "" : " at ", + path == new_path ? "" : new_path); + update_file(o, 0, a_sha, a_mode, new_path); + } +} + +static int merge_content(struct merge_options *o, + const char *path, + unsigned char *o_sha, int o_mode, + unsigned char *a_sha, int a_mode, + unsigned char *b_sha, int b_mode, + const char *df_rename_conflict_branch) +{ + const char *reason = "content"; + struct merge_file_info mfi; + struct diff_filespec one, a, b; + struct stat st; + unsigned df_conflict_remains = 0; + + if (!o_sha) { + reason = "add/add"; + o_sha = (unsigned char *)null_sha1; + } + one.path = a.path = b.path = (char *)path; + hashcpy(one.sha1, o_sha); + one.mode = o_mode; + hashcpy(a.sha1, a_sha); + a.mode = a_mode; + hashcpy(b.sha1, b_sha); + b.mode = b_mode; + + mfi = merge_file(o, &one, &a, &b, o->branch1, o->branch2); + if (df_rename_conflict_branch && + lstat(path, &st) == 0 && S_ISDIR(st.st_mode)) { + df_conflict_remains = 1; + } + + if (mfi.clean && !df_conflict_remains && + sha_eq(mfi.sha, a_sha) && mfi.mode == a.mode) + output(o, 3, "Skipped %s (merged same as existing)", path); + else + output(o, 2, "Auto-merging %s", path); + + if (!mfi.clean) { + if (S_ISGITLINK(mfi.mode)) + reason = "submodule"; + output(o, 1, "CONFLICT (%s): Merge conflict in %s", + reason, path); + } + + if (df_conflict_remains) { + const char *new_path; + update_file_flags(o, mfi.sha, mfi.mode, path, + o->call_depth || mfi.clean, 0); + new_path = unique_path(o, path, df_rename_conflict_branch); + mfi.clean = 0; + output(o, 1, "Adding as %s instead", new_path); + update_file_flags(o, mfi.sha, mfi.mode, new_path, 0, 1); + } else { + update_file(o, mfi.clean, mfi.sha, mfi.mode, path); + } + return mfi.clean; + +} + /* Per entry merge function */ static int process_entry(struct merge_options *o, const char *path, struct stage_data *entry) @@ -1136,6 +1318,9 @@ static int process_entry(struct merge_options *o, unsigned char *a_sha = stage_sha(entry->stages[2].sha, a_mode); unsigned char *b_sha = stage_sha(entry->stages[3].sha, b_mode); + if (entry->rename_df_conflict_info) + return 1; /* Such cases are handled elsewhere. */ + entry->processed = 1; if (o_sha && (!a_sha || !b_sha)) { /* Case A: Deleted in one */ @@ -1148,22 +1333,15 @@ static int process_entry(struct merge_options *o, output(o, 2, "Removing %s", path); /* do not touch working file if it did not exist */ remove_file(o, 1, path, !a_sha); + } else if (string_list_has_string(&o->current_directory_set, + path)) { + entry->processed = 0; + return 1; /* Assume clean until processed */ } else { /* Deleted in one and changed in the other */ clean_merge = 0; - if (!a_sha) { - output(o, 1, "CONFLICT (delete/modify): %s deleted in %s " - "and modified in %s. Version %s of %s left in tree.", - path, o->branch1, - o->branch2, o->branch2, path); - update_file(o, 0, b_sha, b_mode, path); - } else { - output(o, 1, "CONFLICT (delete/modify): %s deleted in %s " - "and modified in %s. Version %s of %s left in tree.", - path, o->branch2, - o->branch1, o->branch1, path); - update_file(o, 0, a_sha, a_mode, path); - } + handle_delete_modify(o, path, path, + a_sha, a_mode, b_sha, b_mode); } } else if ((!o_sha && a_sha && !b_sha) || @@ -1182,15 +1360,7 @@ static int process_entry(struct merge_options *o, if (string_list_has_string(&o->current_directory_set, path)) { /* Handle D->F conflicts after all subfiles */ entry->processed = 0; - /* But get any file out of the way now, so conflicted - * entries below the directory of the same name can - * be put in the working directory. - */ - if (a_sha) - output(o, 2, "Removing %s", path); - /* do not touch working file if it did not exist */ - remove_file(o, 0, path, !a_sha); - return 1; /* Assume clean till processed */ + return 1; /* Assume clean until processed */ } else { output(o, 2, "Adding %s", path); update_file(o, 1, sha, mode, path); @@ -1198,34 +1368,9 @@ static int process_entry(struct merge_options *o, } else if (a_sha && b_sha) { /* Case C: Added in both (check for same permissions) and */ /* case D: Modified in both, but differently. */ - const char *reason = "content"; - struct merge_file_info mfi; - struct diff_filespec one, a, b; - - if (!o_sha) { - reason = "add/add"; - o_sha = (unsigned char *)null_sha1; - } - output(o, 2, "Auto-merging %s", path); - one.path = a.path = b.path = (char *)path; - hashcpy(one.sha1, o_sha); - one.mode = o_mode; - hashcpy(a.sha1, a_sha); - a.mode = a_mode; - hashcpy(b.sha1, b_sha); - b.mode = b_mode; - - mfi = merge_file(o, &one, &a, &b, - o->branch1, o->branch2); - - clean_merge = mfi.clean; - if (!mfi.clean) { - if (S_ISGITLINK(mfi.mode)) - reason = "submodule"; - output(o, 1, "CONFLICT (%s): Merge conflict in %s", - reason, path); - } - update_file(o, mfi.clean, mfi.sha, mfi.mode, path); + clean_merge = merge_content(o, path, + o_sha, o_mode, a_sha, a_mode, b_sha, b_mode, + NULL); } else if (!o_sha && !a_sha && !b_sha) { /* * this entry was deleted altogether. a_mode == 0 means @@ -1239,13 +1384,19 @@ static int process_entry(struct merge_options *o, } /* - * Per entry merge function for D/F conflicts, to be called only after - * all files below dir have been processed. We do this because in the - * cases we can cleanly resolve D/F conflicts, process_entry() can clean - * out all the files below the directory for us. + * Per entry merge function for D/F (and/or rename) conflicts. In the + * cases we can cleanly resolve D/F conflicts, process_entry() can + * clean out all the files below the directory for us. All D/F + * conflict cases must be handled here at the end to make sure any + * directories that can be cleaned out, are. + * + * Some rename conflicts may also be handled here that don't necessarily + * involve D/F conflicts, since the code to handle them is generic enough + * to handle those rename conflicts with or without D/F conflicts also + * being involved. */ static int process_df_entry(struct merge_options *o, - const char *path, struct stage_data *entry) + const char *path, struct stage_data *entry) { int clean_merge = 1; unsigned o_mode = entry->stages[1].mode; @@ -1254,43 +1405,91 @@ static int process_df_entry(struct merge_options *o, unsigned char *o_sha = stage_sha(entry->stages[1].sha, o_mode); unsigned char *a_sha = stage_sha(entry->stages[2].sha, a_mode); unsigned char *b_sha = stage_sha(entry->stages[3].sha, b_mode); - const char *add_branch; - const char *other_branch; - unsigned mode; - const unsigned char *sha; - const char *conf; struct stat st; - /* We currently only handle D->F cases */ - assert((!o_sha && a_sha && !b_sha) || - (!o_sha && !a_sha && b_sha)); - entry->processed = 1; - - if (a_sha) { - add_branch = o->branch1; - other_branch = o->branch2; - mode = a_mode; - sha = a_sha; - conf = "file/directory"; - } else { - add_branch = o->branch2; - other_branch = o->branch1; - mode = b_mode; - sha = b_sha; - conf = "directory/file"; - } - if (lstat(path, &st) == 0 && S_ISDIR(st.st_mode)) { - const char *new_path = unique_path(o, path, add_branch); + if (entry->rename_df_conflict_info) { + struct rename_df_conflict_info *conflict_info = entry->rename_df_conflict_info; + char *src; + switch (conflict_info->rename_type) { + case RENAME_NORMAL: + clean_merge = merge_content(o, path, + o_sha, o_mode, a_sha, a_mode, b_sha, b_mode, + conflict_info->branch1); + break; + case RENAME_DELETE: + clean_merge = 0; + conflict_rename_delete(o, conflict_info->pair1, + conflict_info->branch1, + conflict_info->branch2); + break; + case RENAME_ONE_FILE_TO_TWO: + src = conflict_info->pair1->one->path; + clean_merge = 0; + output(o, 1, "CONFLICT (rename/rename): " + "Rename \"%s\"->\"%s\" in branch \"%s\" " + "rename \"%s\"->\"%s\" in \"%s\"%s", + src, conflict_info->pair1->two->path, conflict_info->branch1, + src, conflict_info->pair2->two->path, conflict_info->branch2, + o->call_depth ? " (left unresolved)" : ""); + if (o->call_depth) { + remove_file_from_cache(src); + update_file(o, 0, conflict_info->pair1->one->sha1, + conflict_info->pair1->one->mode, src); + } + conflict_rename_rename_1to2(o, conflict_info->pair1, + conflict_info->branch1, + conflict_info->pair2, + conflict_info->branch2); + conflict_info->dst_entry2->processed = 1; + break; + default: + entry->processed = 0; + break; + } + } else if (o_sha && (!a_sha || !b_sha)) { + /* Modify/delete; deleted side may have put a directory in the way */ + const char *new_path = path; + if (lstat(path, &st) == 0 && S_ISDIR(st.st_mode)) + new_path = unique_path(o, path, a_sha ? o->branch1 : o->branch2); clean_merge = 0; - output(o, 1, "CONFLICT (%s): There is a directory with name %s in %s. " - "Adding %s as %s", - conf, path, other_branch, path, new_path); - remove_file(o, 0, path, 0); - update_file(o, 0, sha, mode, new_path); + handle_delete_modify(o, path, new_path, + a_sha, a_mode, b_sha, b_mode); + } else if (!o_sha && !!a_sha != !!b_sha) { + /* directory -> (directory, file) */ + const char *add_branch; + const char *other_branch; + unsigned mode; + const unsigned char *sha; + const char *conf; + + if (a_sha) { + add_branch = o->branch1; + other_branch = o->branch2; + mode = a_mode; + sha = a_sha; + conf = "file/directory"; + } else { + add_branch = o->branch2; + other_branch = o->branch1; + mode = b_mode; + sha = b_sha; + conf = "directory/file"; + } + if (lstat(path, &st) == 0 && S_ISDIR(st.st_mode)) { + const char *new_path = unique_path(o, path, add_branch); + clean_merge = 0; + output(o, 1, "CONFLICT (%s): There is a directory with name %s in %s. " + "Adding %s as %s", + conf, path, other_branch, path, new_path); + update_file(o, 0, sha, mode, new_path); + } else { + output(o, 2, "Adding %s", path); + update_file(o, 1, sha, mode, path); + } } else { - output(o, 2, "Adding %s", path); - update_file(o, 1, sha, mode, path); + entry->processed = 0; + return 1; /* not handled; assume clean until processed */ } return clean_merge; @@ -1335,6 +1534,7 @@ int merge_trees(struct merge_options *o, get_files_dirs(o, merge); entries = get_unmerged(); + make_room_for_directories_of_df_conflicts(o, entries); re_head = get_renames(o, head, common, head, merge, entries); re_merge = get_renames(o, merge, common, head, merge, entries); clean = process_renames(o, re_head, re_merge); @@ -1352,6 +1552,12 @@ int merge_trees(struct merge_options *o, && !process_df_entry(o, path, e)) clean = 0; } + for (i = 0; i < entries->nr; i++) { + struct stage_data *e = entries->items[i].util; + if (!e->processed) + die("Unprocessed path??? %s", + entries->items[i].string); + } string_list_clear(re_merge, 0); string_list_clear(re_head, 0); diff --git a/name-hash.c b/name-hash.c index 0031d78e8c..c6b6a3fe4c 100644 --- a/name-hash.c +++ b/name-hash.c @@ -32,6 +32,42 @@ static unsigned int hash_name(const char *name, int namelen) return hash; } +static void hash_index_entry_directories(struct index_state *istate, struct cache_entry *ce) +{ + /* + * Throw each directory component in the hash for quick lookup + * during a git status. Directory components are stored with their + * closing slash. Despite submodules being a directory, they never + * reach this point, because they are stored without a closing slash + * in the cache. + * + * Note that the cache_entry stored with the directory does not + * represent the directory itself. It is a pointer to an existing + * filename, and its only purpose is to represent existence of the + * directory in the cache. It is very possible multiple directory + * hash entries may point to the same cache_entry. + */ + unsigned int hash; + void **pos; + + const char *ptr = ce->name; + while (*ptr) { + while (*ptr && *ptr != '/') + ++ptr; + if (*ptr == '/') { + ++ptr; + hash = hash_name(ce->name, ptr - ce->name); + if (!lookup_hash(hash, &istate->name_hash)) { + pos = insert_hash(hash, ce, &istate->name_hash); + if (pos) { + ce->next = *pos; + *pos = ce; + } + } + } + } +} + static void hash_index_entry(struct index_state *istate, struct cache_entry *ce) { void **pos; @@ -47,6 +83,9 @@ static void hash_index_entry(struct index_state *istate, struct cache_entry *ce) ce->next = *pos; *pos = ce; } + + if (ignore_case) + hash_index_entry_directories(istate, ce); } static void lazy_init_name_hash(struct index_state *istate) @@ -97,7 +136,21 @@ static int same_name(const struct cache_entry *ce, const char *name, int namelen if (len == namelen && !cache_name_compare(name, namelen, ce->name, len)) return 1; - return icase && slow_same_name(name, namelen, ce->name, len); + if (!icase) + return 0; + + /* + * If the entry we're comparing is a filename (no trailing slash), then compare + * the lengths exactly. + */ + if (name[namelen - 1] != '/') + return slow_same_name(name, namelen, ce->name, len); + + /* + * For a directory, we point to an arbitrary cache_entry filename. Just + * make sure the directory portion matches. + */ + return slow_same_name(name, namelen, ce->name, namelen < len ? namelen : len); } struct cache_entry *index_name_exists(struct index_state *istate, const char *name, int namelen, int icase) @@ -115,5 +168,22 @@ struct cache_entry *index_name_exists(struct index_state *istate, const char *na } ce = ce->next; } + + /* + * Might be a submodule. Despite submodules being directories, + * they are stored in the name hash without a closing slash. + * When ignore_case is 1, directories are stored in the name hash + * with their closing slash. + * + * The side effect of this storage technique is we have need to + * remove the slash from name and perform the lookup again without + * the slash. If a match is made, S_ISGITLINK(ce->mode) will be + * true. + */ + if (icase && name[namelen - 1] == '/') { + ce = index_name_exists(istate, name, namelen - 1, icase); + if (ce && S_ISGITLINK(ce->ce_mode)) + return ce; + } return NULL; } diff --git a/notes-cache.c b/notes-cache.c index dee6d62e72..4c8984ede1 100644 --- a/notes-cache.c +++ b/notes-cache.c @@ -89,6 +89,5 @@ int notes_cache_put(struct notes_cache *c, unsigned char key_sha1[20], if (write_sha1_file(data, size, "blob", value_sha1) < 0) return -1; - add_note(&c->tree, key_sha1, value_sha1, NULL); - return 0; + return add_note(&c->tree, key_sha1, value_sha1, NULL); } diff --git a/notes-merge.c b/notes-merge.c new file mode 100644 index 0000000000..71c4d45fcd --- /dev/null +++ b/notes-merge.c @@ -0,0 +1,737 @@ +#include "cache.h" +#include "commit.h" +#include "refs.h" +#include "diff.h" +#include "diffcore.h" +#include "xdiff-interface.h" +#include "ll-merge.h" +#include "dir.h" +#include "notes.h" +#include "notes-merge.h" +#include "strbuf.h" + +struct notes_merge_pair { + unsigned char obj[20], base[20], local[20], remote[20]; +}; + +void init_notes_merge_options(struct notes_merge_options *o) +{ + memset(o, 0, sizeof(struct notes_merge_options)); + strbuf_init(&(o->commit_msg), 0); + o->verbosity = NOTES_MERGE_VERBOSITY_DEFAULT; +} + +#define OUTPUT(o, v, ...) \ + do { \ + if ((o)->verbosity >= (v)) { \ + printf(__VA_ARGS__); \ + puts(""); \ + } \ + } while (0) + +static int path_to_sha1(const char *path, unsigned char *sha1) +{ + char hex_sha1[40]; + int i = 0; + while (*path && i < 40) { + if (*path != '/') + hex_sha1[i++] = *path; + path++; + } + if (*path || i != 40) + return -1; + return get_sha1_hex(hex_sha1, sha1); +} + +static int verify_notes_filepair(struct diff_filepair *p, unsigned char *sha1) +{ + switch (p->status) { + case DIFF_STATUS_MODIFIED: + assert(p->one->mode == p->two->mode); + assert(!is_null_sha1(p->one->sha1)); + assert(!is_null_sha1(p->two->sha1)); + break; + case DIFF_STATUS_ADDED: + assert(is_null_sha1(p->one->sha1)); + break; + case DIFF_STATUS_DELETED: + assert(is_null_sha1(p->two->sha1)); + break; + default: + return -1; + } + assert(!strcmp(p->one->path, p->two->path)); + return path_to_sha1(p->one->path, sha1); +} + +static struct notes_merge_pair *find_notes_merge_pair_pos( + struct notes_merge_pair *list, int len, unsigned char *obj, + int insert_new, int *occupied) +{ + /* + * Both diff_tree_remote() and diff_tree_local() tend to process + * merge_pairs in ascending order. Therefore, cache last returned + * index, and search sequentially from there until the appropriate + * position is found. + * + * Since inserts only happen from diff_tree_remote() (which mainly + * _appends_), we don't care that inserting into the middle of the + * list is expensive (using memmove()). + */ + static int last_index; + int i = last_index < len ? last_index : len - 1; + int prev_cmp = 0, cmp = -1; + while (i >= 0 && i < len) { + cmp = hashcmp(obj, list[i].obj); + if (!cmp) /* obj belongs @ i */ + break; + else if (cmp < 0 && prev_cmp <= 0) /* obj belongs < i */ + i--; + else if (cmp < 0) /* obj belongs between i-1 and i */ + break; + else if (cmp > 0 && prev_cmp >= 0) /* obj belongs > i */ + i++; + else /* if (cmp > 0) */ { /* obj belongs between i and i+1 */ + i++; + break; + } + prev_cmp = cmp; + } + if (i < 0) + i = 0; + /* obj belongs at, or immediately preceding, index i (0 <= i <= len) */ + + if (!cmp) + *occupied = 1; + else { + *occupied = 0; + if (insert_new && i < len) { + memmove(list + i + 1, list + i, + (len - i) * sizeof(struct notes_merge_pair)); + memset(list + i, 0, sizeof(struct notes_merge_pair)); + } + } + last_index = i; + return list + i; +} + +static unsigned char uninitialized[20] = + "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" \ + "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"; + +static struct notes_merge_pair *diff_tree_remote(struct notes_merge_options *o, + const unsigned char *base, + const unsigned char *remote, + int *num_changes) +{ + struct diff_options opt; + struct notes_merge_pair *changes; + int i, len = 0; + + trace_printf("\tdiff_tree_remote(base = %.7s, remote = %.7s)\n", + sha1_to_hex(base), sha1_to_hex(remote)); + + diff_setup(&opt); + DIFF_OPT_SET(&opt, RECURSIVE); + opt.output_format = DIFF_FORMAT_NO_OUTPUT; + if (diff_setup_done(&opt) < 0) + die("diff_setup_done failed"); + diff_tree_sha1(base, remote, "", &opt); + diffcore_std(&opt); + + changes = xcalloc(diff_queued_diff.nr, sizeof(struct notes_merge_pair)); + + for (i = 0; i < diff_queued_diff.nr; i++) { + struct diff_filepair *p = diff_queued_diff.queue[i]; + struct notes_merge_pair *mp; + int occupied; + unsigned char obj[20]; + + if (verify_notes_filepair(p, obj)) { + trace_printf("\t\tCannot merge entry '%s' (%c): " + "%.7s -> %.7s. Skipping!\n", p->one->path, + p->status, sha1_to_hex(p->one->sha1), + sha1_to_hex(p->two->sha1)); + continue; + } + mp = find_notes_merge_pair_pos(changes, len, obj, 1, &occupied); + if (occupied) { + /* We've found an addition/deletion pair */ + assert(!hashcmp(mp->obj, obj)); + if (is_null_sha1(p->one->sha1)) { /* addition */ + assert(is_null_sha1(mp->remote)); + hashcpy(mp->remote, p->two->sha1); + } else if (is_null_sha1(p->two->sha1)) { /* deletion */ + assert(is_null_sha1(mp->base)); + hashcpy(mp->base, p->one->sha1); + } else + assert(!"Invalid existing change recorded"); + } else { + hashcpy(mp->obj, obj); + hashcpy(mp->base, p->one->sha1); + hashcpy(mp->local, uninitialized); + hashcpy(mp->remote, p->two->sha1); + len++; + } + trace_printf("\t\tStored remote change for %s: %.7s -> %.7s\n", + sha1_to_hex(mp->obj), sha1_to_hex(mp->base), + sha1_to_hex(mp->remote)); + } + diff_flush(&opt); + diff_tree_release_paths(&opt); + + *num_changes = len; + return changes; +} + +static void diff_tree_local(struct notes_merge_options *o, + struct notes_merge_pair *changes, int len, + const unsigned char *base, + const unsigned char *local) +{ + struct diff_options opt; + int i; + + trace_printf("\tdiff_tree_local(len = %i, base = %.7s, local = %.7s)\n", + len, sha1_to_hex(base), sha1_to_hex(local)); + + diff_setup(&opt); + DIFF_OPT_SET(&opt, RECURSIVE); + opt.output_format = DIFF_FORMAT_NO_OUTPUT; + if (diff_setup_done(&opt) < 0) + die("diff_setup_done failed"); + diff_tree_sha1(base, local, "", &opt); + diffcore_std(&opt); + + for (i = 0; i < diff_queued_diff.nr; i++) { + struct diff_filepair *p = diff_queued_diff.queue[i]; + struct notes_merge_pair *mp; + int match; + unsigned char obj[20]; + + if (verify_notes_filepair(p, obj)) { + trace_printf("\t\tCannot merge entry '%s' (%c): " + "%.7s -> %.7s. Skipping!\n", p->one->path, + p->status, sha1_to_hex(p->one->sha1), + sha1_to_hex(p->two->sha1)); + continue; + } + mp = find_notes_merge_pair_pos(changes, len, obj, 0, &match); + if (!match) { + trace_printf("\t\tIgnoring local-only change for %s: " + "%.7s -> %.7s\n", sha1_to_hex(obj), + sha1_to_hex(p->one->sha1), + sha1_to_hex(p->two->sha1)); + continue; + } + + assert(!hashcmp(mp->obj, obj)); + if (is_null_sha1(p->two->sha1)) { /* deletion */ + /* + * Either this is a true deletion (1), or it is part + * of an A/D pair (2), or D/A pair (3): + * + * (1) mp->local is uninitialized; set it to null_sha1 + * (2) mp->local is not uninitialized; don't touch it + * (3) mp->local is uninitialized; set it to null_sha1 + * (will be overwritten by following addition) + */ + if (!hashcmp(mp->local, uninitialized)) + hashclr(mp->local); + } else if (is_null_sha1(p->one->sha1)) { /* addition */ + /* + * Either this is a true addition (1), or it is part + * of an A/D pair (2), or D/A pair (3): + * + * (1) mp->local is uninitialized; set to p->two->sha1 + * (2) mp->local is uninitialized; set to p->two->sha1 + * (3) mp->local is null_sha1; set to p->two->sha1 + */ + assert(is_null_sha1(mp->local) || + !hashcmp(mp->local, uninitialized)); + hashcpy(mp->local, p->two->sha1); + } else { /* modification */ + /* + * This is a true modification. p->one->sha1 shall + * match mp->base, and mp->local shall be uninitialized. + * Set mp->local to p->two->sha1. + */ + assert(!hashcmp(p->one->sha1, mp->base)); + assert(!hashcmp(mp->local, uninitialized)); + hashcpy(mp->local, p->two->sha1); + } + trace_printf("\t\tStored local change for %s: %.7s -> %.7s\n", + sha1_to_hex(mp->obj), sha1_to_hex(mp->base), + sha1_to_hex(mp->local)); + } + diff_flush(&opt); + diff_tree_release_paths(&opt); +} + +static void check_notes_merge_worktree(struct notes_merge_options *o) +{ + if (!o->has_worktree) { + /* + * Must establish NOTES_MERGE_WORKTREE. + * Abort if NOTES_MERGE_WORKTREE already exists + */ + if (file_exists(git_path(NOTES_MERGE_WORKTREE))) { + if (advice_resolve_conflict) + die("You have not concluded your previous " + "notes merge (%s exists).\nPlease, use " + "'git notes merge --commit' or 'git notes " + "merge --abort' to commit/abort the " + "previous merge before you start a new " + "notes merge.", git_path("NOTES_MERGE_*")); + else + die("You have not concluded your notes merge " + "(%s exists).", git_path("NOTES_MERGE_*")); + } + + if (safe_create_leading_directories(git_path( + NOTES_MERGE_WORKTREE "/.test"))) + die_errno("unable to create directory %s", + git_path(NOTES_MERGE_WORKTREE)); + o->has_worktree = 1; + } else if (!file_exists(git_path(NOTES_MERGE_WORKTREE))) + /* NOTES_MERGE_WORKTREE should already be established */ + die("missing '%s'. This should not happen", + git_path(NOTES_MERGE_WORKTREE)); +} + +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)) + die_errno("unable to create directory for '%s'", path); + if (file_exists(path)) + die("found existing file at '%s'", path); + + fd = open(path, O_WRONLY | O_TRUNC | O_CREAT, 0666); + if (fd < 0) + die_errno("failed to open '%s'", path); + + while (size > 0) { + long ret = write_in_full(fd, buf, size); + if (ret < 0) { + /* Ignore epipe */ + if (errno == EPIPE) + break; + die_errno("notes-merge"); + } else if (!ret) { + die("notes-merge: disk full?"); + } + size -= ret; + buf += ret; + } + + close(fd); +} + +static void write_note_to_worktree(const unsigned char *obj, + const unsigned char *note) +{ + enum object_type type; + unsigned long size; + void *buf = read_sha1_file(note, &type, &size); + + if (!buf) + die("cannot read note %s for object %s", + sha1_to_hex(note), sha1_to_hex(obj)); + if (type != OBJ_BLOB) + die("blob expected in note %s for object %s", + sha1_to_hex(note), sha1_to_hex(obj)); + write_buf_to_worktree(obj, buf, size); + free(buf); +} + +static int ll_merge_in_worktree(struct notes_merge_options *o, + struct notes_merge_pair *p) +{ + mmbuffer_t result_buf; + mmfile_t base, local, remote; + int status; + + read_mmblob(&base, p->base); + read_mmblob(&local, p->local); + read_mmblob(&remote, p->remote); + + status = ll_merge(&result_buf, sha1_to_hex(p->obj), &base, NULL, + &local, o->local_ref, &remote, o->remote_ref, 0); + + free(base.ptr); + free(local.ptr); + free(remote.ptr); + + if ((status < 0) || !result_buf.ptr) + die("Failed to execute internal merge"); + + write_buf_to_worktree(p->obj, result_buf.ptr, result_buf.size); + free(result_buf.ptr); + + return status; +} + +static int merge_one_change_manual(struct notes_merge_options *o, + struct notes_merge_pair *p, + struct notes_tree *t) +{ + const char *lref = o->local_ref ? o->local_ref : "local version"; + const char *rref = o->remote_ref ? o->remote_ref : "remote version"; + + trace_printf("\t\t\tmerge_one_change_manual(obj = %.7s, base = %.7s, " + "local = %.7s, remote = %.7s)\n", + sha1_to_hex(p->obj), sha1_to_hex(p->base), + sha1_to_hex(p->local), sha1_to_hex(p->remote)); + + /* add "Conflicts:" section to commit message first time through */ + if (!o->has_worktree) + strbuf_addstr(&(o->commit_msg), "\n\nConflicts:\n"); + + strbuf_addf(&(o->commit_msg), "\t%s\n", sha1_to_hex(p->obj)); + + OUTPUT(o, 2, "Auto-merging notes for %s", sha1_to_hex(p->obj)); + check_notes_merge_worktree(o); + if (is_null_sha1(p->local)) { + /* D/F conflict, checkout p->remote */ + assert(!is_null_sha1(p->remote)); + OUTPUT(o, 1, "CONFLICT (delete/modify): Notes for object %s " + "deleted in %s and modified in %s. Version from %s " + "left in tree.", sha1_to_hex(p->obj), lref, rref, rref); + write_note_to_worktree(p->obj, p->remote); + } else if (is_null_sha1(p->remote)) { + /* D/F conflict, checkout p->local */ + assert(!is_null_sha1(p->local)); + OUTPUT(o, 1, "CONFLICT (delete/modify): Notes for object %s " + "deleted in %s and modified in %s. Version from %s " + "left in tree.", sha1_to_hex(p->obj), rref, lref, lref); + write_note_to_worktree(p->obj, p->local); + } else { + /* "regular" conflict, checkout result of ll_merge() */ + const char *reason = "content"; + if (is_null_sha1(p->base)) + reason = "add/add"; + assert(!is_null_sha1(p->local)); + assert(!is_null_sha1(p->remote)); + OUTPUT(o, 1, "CONFLICT (%s): Merge conflict in notes for " + "object %s", reason, sha1_to_hex(p->obj)); + ll_merge_in_worktree(o, p); + } + + trace_printf("\t\t\tremoving from partial merge result\n"); + remove_note(t, p->obj); + + return 1; +} + +static int merge_one_change(struct notes_merge_options *o, + struct notes_merge_pair *p, struct notes_tree *t) +{ + /* + * Return 0 if change is successfully resolved (stored in notes_tree). + * Return 1 is change results in a conflict (NOT stored in notes_tree, + * but instead written to NOTES_MERGE_WORKTREE with conflict markers). + */ + switch (o->strategy) { + case NOTES_MERGE_RESOLVE_MANUAL: + return merge_one_change_manual(o, p, t); + case NOTES_MERGE_RESOLVE_OURS: + OUTPUT(o, 2, "Using local notes for %s", sha1_to_hex(p->obj)); + /* nothing to do */ + return 0; + case NOTES_MERGE_RESOLVE_THEIRS: + OUTPUT(o, 2, "Using remote notes for %s", sha1_to_hex(p->obj)); + if (add_note(t, p->obj, p->remote, combine_notes_overwrite)) + die("BUG: combine_notes_overwrite failed"); + return 0; + case NOTES_MERGE_RESOLVE_UNION: + OUTPUT(o, 2, "Concatenating local and remote notes for %s", + sha1_to_hex(p->obj)); + if (add_note(t, p->obj, p->remote, combine_notes_concatenate)) + die("failed to concatenate notes " + "(combine_notes_concatenate)"); + return 0; + case NOTES_MERGE_RESOLVE_CAT_SORT_UNIQ: + OUTPUT(o, 2, "Concatenating unique lines in local and remote " + "notes for %s", sha1_to_hex(p->obj)); + if (add_note(t, p->obj, p->remote, combine_notes_cat_sort_uniq)) + die("failed to concatenate notes " + "(combine_notes_cat_sort_uniq)"); + return 0; + } + die("Unknown strategy (%i).", o->strategy); +} + +static int merge_changes(struct notes_merge_options *o, + struct notes_merge_pair *changes, int *num_changes, + struct notes_tree *t) +{ + int i, conflicts = 0; + + trace_printf("\tmerge_changes(num_changes = %i)\n", *num_changes); + for (i = 0; i < *num_changes; i++) { + struct notes_merge_pair *p = changes + i; + trace_printf("\t\t%.7s: %.7s -> %.7s/%.7s\n", + sha1_to_hex(p->obj), sha1_to_hex(p->base), + sha1_to_hex(p->local), sha1_to_hex(p->remote)); + + if (!hashcmp(p->base, p->remote)) { + /* no remote change; nothing to do */ + trace_printf("\t\t\tskipping (no remote change)\n"); + } else if (!hashcmp(p->local, p->remote)) { + /* same change in local and remote; nothing to do */ + trace_printf("\t\t\tskipping (local == remote)\n"); + } else if (!hashcmp(p->local, uninitialized) || + !hashcmp(p->local, p->base)) { + /* no local change; adopt remote change */ + trace_printf("\t\t\tno local change, adopted remote\n"); + if (add_note(t, p->obj, p->remote, + combine_notes_overwrite)) + die("BUG: combine_notes_overwrite failed"); + } else { + /* need file-level merge between local and remote */ + trace_printf("\t\t\tneed content-level merge\n"); + conflicts += merge_one_change(o, p, t); + } + } + + return conflicts; +} + +static int merge_from_diffs(struct notes_merge_options *o, + const unsigned char *base, + const unsigned char *local, + const unsigned char *remote, struct notes_tree *t) +{ + struct notes_merge_pair *changes; + int num_changes, conflicts; + + trace_printf("\tmerge_from_diffs(base = %.7s, local = %.7s, " + "remote = %.7s)\n", sha1_to_hex(base), sha1_to_hex(local), + sha1_to_hex(remote)); + + changes = diff_tree_remote(o, base, remote, &num_changes); + diff_tree_local(o, changes, num_changes, base, local); + + conflicts = merge_changes(o, changes, &num_changes, t); + free(changes); + + OUTPUT(o, 4, "Merge result: %i unmerged notes and a %s notes tree", + conflicts, t->dirty ? "dirty" : "clean"); + + return conflicts ? -1 : 1; +} + +void create_notes_commit(struct notes_tree *t, struct commit_list *parents, + const char *msg, unsigned char *result_sha1) +{ + unsigned char tree_sha1[20]; + + assert(t->initialized); + + if (write_notes_tree(t, tree_sha1)) + die("Failed to write notes tree to database"); + + if (!parents) { + /* Deduce parent commit from t->ref */ + unsigned char parent_sha1[20]; + if (!read_ref(t->ref, parent_sha1)) { + struct commit *parent = lookup_commit(parent_sha1); + if (!parent || parse_commit(parent)) + die("Failed to find/parse commit %s", t->ref); + commit_list_insert(parent, &parents); + } + /* else: t->ref points to nothing, assume root/orphan commit */ + } + + if (commit_tree(msg, tree_sha1, parents, result_sha1, NULL)) + die("Failed to commit notes tree to database"); +} + +int notes_merge(struct notes_merge_options *o, + struct notes_tree *local_tree, + unsigned char *result_sha1) +{ + unsigned char local_sha1[20], remote_sha1[20]; + struct commit *local, *remote; + struct commit_list *bases = NULL; + const unsigned char *base_sha1, *base_tree_sha1; + int result = 0; + + assert(o->local_ref && o->remote_ref); + assert(!strcmp(o->local_ref, local_tree->ref)); + hashclr(result_sha1); + + trace_printf("notes_merge(o->local_ref = %s, o->remote_ref = %s)\n", + o->local_ref, o->remote_ref); + + /* Dereference o->local_ref into local_sha1 */ + if (!resolve_ref(o->local_ref, local_sha1, 0, NULL)) + die("Failed to resolve local notes ref '%s'", o->local_ref); + else if (!check_ref_format(o->local_ref) && is_null_sha1(local_sha1)) + local = NULL; /* local_sha1 == null_sha1 indicates unborn ref */ + else if (!(local = lookup_commit_reference(local_sha1))) + die("Could not parse local commit %s (%s)", + sha1_to_hex(local_sha1), o->local_ref); + trace_printf("\tlocal commit: %.7s\n", sha1_to_hex(local_sha1)); + + /* Dereference o->remote_ref into remote_sha1 */ + if (get_sha1(o->remote_ref, remote_sha1)) { + /* + * Failed to get remote_sha1. If o->remote_ref looks like an + * unborn ref, perform the merge using an empty notes tree. + */ + if (!check_ref_format(o->remote_ref)) { + hashclr(remote_sha1); + remote = NULL; + } else { + die("Failed to resolve remote notes ref '%s'", + o->remote_ref); + } + } else if (!(remote = lookup_commit_reference(remote_sha1))) { + die("Could not parse remote commit %s (%s)", + sha1_to_hex(remote_sha1), o->remote_ref); + } + trace_printf("\tremote commit: %.7s\n", sha1_to_hex(remote_sha1)); + + if (!local && !remote) + die("Cannot merge empty notes ref (%s) into empty notes ref " + "(%s)", o->remote_ref, o->local_ref); + if (!local) { + /* result == remote commit */ + hashcpy(result_sha1, remote_sha1); + goto found_result; + } + if (!remote) { + /* result == local commit */ + hashcpy(result_sha1, local_sha1); + goto found_result; + } + assert(local && remote); + + /* Find merge bases */ + bases = get_merge_bases(local, remote, 1); + if (!bases) { + base_sha1 = null_sha1; + base_tree_sha1 = (unsigned char *)EMPTY_TREE_SHA1_BIN; + OUTPUT(o, 4, "No merge base found; doing history-less merge"); + } else if (!bases->next) { + base_sha1 = bases->item->object.sha1; + base_tree_sha1 = bases->item->tree->object.sha1; + OUTPUT(o, 4, "One merge base found (%.7s)", + sha1_to_hex(base_sha1)); + } else { + /* TODO: How to handle multiple merge-bases? */ + base_sha1 = bases->item->object.sha1; + base_tree_sha1 = bases->item->tree->object.sha1; + OUTPUT(o, 3, "Multiple merge bases found. Using the first " + "(%.7s)", sha1_to_hex(base_sha1)); + } + + OUTPUT(o, 4, "Merging remote commit %.7s into local commit %.7s with " + "merge-base %.7s", sha1_to_hex(remote->object.sha1), + sha1_to_hex(local->object.sha1), sha1_to_hex(base_sha1)); + + if (!hashcmp(remote->object.sha1, base_sha1)) { + /* Already merged; result == local commit */ + OUTPUT(o, 2, "Already up-to-date!"); + hashcpy(result_sha1, local->object.sha1); + goto found_result; + } + if (!hashcmp(local->object.sha1, base_sha1)) { + /* Fast-forward; result == remote commit */ + OUTPUT(o, 2, "Fast-forward"); + hashcpy(result_sha1, remote->object.sha1); + goto found_result; + } + + result = merge_from_diffs(o, base_tree_sha1, local->tree->object.sha1, + remote->tree->object.sha1, local_tree); + + if (result != 0) { /* non-trivial merge (with or without conflicts) */ + /* Commit (partial) result */ + struct commit_list *parents = NULL; + commit_list_insert(remote, &parents); /* LIFO order */ + commit_list_insert(local, &parents); + create_notes_commit(local_tree, parents, o->commit_msg.buf, + result_sha1); + } + +found_result: + free_commit_list(bases); + strbuf_release(&(o->commit_msg)); + trace_printf("notes_merge(): result = %i, result_sha1 = %.7s\n", + result, sha1_to_hex(result_sha1)); + return result; +} + +int notes_merge_commit(struct notes_merge_options *o, + struct notes_tree *partial_tree, + struct commit *partial_commit, + unsigned char *result_sha1) +{ + /* + * Iterate through files in .git/NOTES_MERGE_WORKTREE and add all + * found notes to 'partial_tree'. Write the updates notes tree to + * the DB, and commit the resulting tree object while reusing the + * commit message and parents from 'partial_commit'. + * Finally store the new commit object SHA1 into 'result_sha1'. + */ + struct dir_struct dir; + const char *path = git_path(NOTES_MERGE_WORKTREE "/"); + int path_len = strlen(path), i; + const char *msg = strstr(partial_commit->buffer, "\n\n"); + + OUTPUT(o, 3, "Committing notes in notes merge worktree at %.*s", + path_len - 1, path); + + if (!msg || msg[2] == '\0') + die("partial notes commit has empty message"); + msg += 2; + + memset(&dir, 0, sizeof(dir)); + read_directory(&dir, path, path_len, NULL); + for (i = 0; i < dir.nr; i++) { + struct dir_entry *ent = dir.entries[i]; + struct stat st; + const char *relpath = ent->name + path_len; + unsigned char obj_sha1[20], blob_sha1[20]; + + if (ent->len - path_len != 40 || get_sha1_hex(relpath, obj_sha1)) { + OUTPUT(o, 3, "Skipping non-SHA1 entry '%s'", ent->name); + continue; + } + + /* write file as blob, and add to partial_tree */ + if (stat(ent->name, &st)) + die_errno("Failed to stat '%s'", ent->name); + if (index_path(blob_sha1, ent->name, &st, 1)) + die("Failed to write blob object from '%s'", ent->name); + if (add_note(partial_tree, obj_sha1, blob_sha1, NULL)) + die("Failed to add resolved note '%s' to notes tree", + ent->name); + OUTPUT(o, 4, "Added resolved note for object %s: %s", + sha1_to_hex(obj_sha1), sha1_to_hex(blob_sha1)); + } + + create_notes_commit(partial_tree, partial_commit->parents, msg, + result_sha1); + OUTPUT(o, 4, "Finalized notes merge commit: %s", + sha1_to_hex(result_sha1)); + return 0; +} + +int notes_merge_abort(struct notes_merge_options *o) +{ + /* Remove .git/NOTES_MERGE_WORKTREE directory and all files within */ + struct strbuf buf = STRBUF_INIT; + int ret; + + strbuf_addstr(&buf, git_path(NOTES_MERGE_WORKTREE)); + OUTPUT(o, 3, "Removing notes merge worktree at %s", buf.buf); + ret = remove_dir_recursively(&buf, 0); + strbuf_release(&buf); + return ret; +} diff --git a/notes-merge.h b/notes-merge.h new file mode 100644 index 0000000000..168a6724cd --- /dev/null +++ b/notes-merge.h @@ -0,0 +1,98 @@ +#ifndef NOTES_MERGE_H +#define NOTES_MERGE_H + +#define NOTES_MERGE_WORKTREE "NOTES_MERGE_WORKTREE" + +enum notes_merge_verbosity { + NOTES_MERGE_VERBOSITY_DEFAULT = 2, + NOTES_MERGE_VERBOSITY_MAX = 5 +}; + +struct notes_merge_options { + const char *local_ref; + const char *remote_ref; + struct strbuf commit_msg; + int verbosity; + enum { + NOTES_MERGE_RESOLVE_MANUAL = 0, + NOTES_MERGE_RESOLVE_OURS, + NOTES_MERGE_RESOLVE_THEIRS, + NOTES_MERGE_RESOLVE_UNION, + NOTES_MERGE_RESOLVE_CAT_SORT_UNIQ + } strategy; + unsigned has_worktree:1; +}; + +void init_notes_merge_options(struct notes_merge_options *o); + +/* + * Create new notes commit from the given notes tree + * + * Properties of the created commit: + * - tree: the result of converting t to a tree object with write_notes_tree(). + * - parents: the given parents OR (if NULL) the commit referenced by t->ref. + * - author/committer: the default determined by commmit_tree(). + * - commit message: msg + * + * The resulting commit SHA1 is stored in result_sha1. + */ +void create_notes_commit(struct notes_tree *t, struct commit_list *parents, + const char *msg, unsigned char *result_sha1); + +/* + * Merge notes from o->remote_ref into o->local_ref + * + * The given notes_tree 'local_tree' must be the notes_tree referenced by the + * o->local_ref. This is the notes_tree in which the object-level merge is + * performed. + * + * The commits given by the two refs are merged, producing one of the following + * outcomes: + * + * 1. The merge trivially results in an existing commit (e.g. fast-forward or + * already-up-to-date). 'local_tree' is untouched, the SHA1 of the result + * is written into 'result_sha1' and 0 is returned. + * 2. The merge successfully completes, producing a merge commit. local_tree + * contains the updated notes tree, the SHA1 of the resulting commit is + * written into 'result_sha1', and 1 is returned. + * 3. The merge results in conflicts. This is similar to #2 in that the + * partial merge result (i.e. merge result minus the unmerged entries) + * are stored in 'local_tree', and the SHA1 or the resulting commit + * (to be amended when the conflicts have been resolved) is written into + * 'result_sha1'. The unmerged entries are written into the + * .git/NOTES_MERGE_WORKTREE directory with conflict markers. + * -1 is returned. + * + * Both o->local_ref and o->remote_ref must be given (non-NULL), but either ref + * (although not both) may refer to a non-existing notes ref, in which case + * that notes ref is interpreted as an empty notes tree, and the merge + * trivially results in what the other ref points to. + */ +int notes_merge(struct notes_merge_options *o, + struct notes_tree *local_tree, + unsigned char *result_sha1); + +/* + * Finalize conflict resolution from an earlier notes_merge() + * + * The given notes tree 'partial_tree' must be the notes_tree corresponding to + * the given 'partial_commit', the partial result commit created by a previous + * call to notes_merge(). + * + * This function will add the (now resolved) notes in .git/NOTES_MERGE_WORKTREE + * to 'partial_tree', and create a final notes merge commit, the SHA1 of which + * will be stored in 'result_sha1'. + */ +int notes_merge_commit(struct notes_merge_options *o, + struct notes_tree *partial_tree, + struct commit *partial_commit, + unsigned char *result_sha1); + +/* + * Abort conflict resolution from an earlier notes_merge() + * + * Removes the notes merge worktree in .git/NOTES_MERGE_WORKTREE. + */ +int notes_merge_abort(struct notes_merge_options *o); + +#endif @@ -150,86 +150,6 @@ static struct leaf_node *note_tree_find(struct notes_tree *t, } /* - * To insert a leaf_node: - * Search to the tree location appropriate for the given leaf_node's key: - * - If location is unused (NULL), store the tweaked pointer directly there - * - If location holds a note entry that matches the note-to-be-inserted, then - * combine the two notes (by calling the given combine_notes function). - * - If location holds a note entry that matches the subtree-to-be-inserted, - * then unpack the subtree-to-be-inserted into the location. - * - If location holds a matching subtree entry, unpack the subtree at that - * location, and restart the insert operation from that level. - * - Else, create a new int_node, holding both the node-at-location and the - * node-to-be-inserted, and store the new int_node into the location. - */ -static void note_tree_insert(struct notes_tree *t, struct int_node *tree, - unsigned char n, struct leaf_node *entry, unsigned char type, - combine_notes_fn combine_notes) -{ - struct int_node *new_node; - struct leaf_node *l; - void **p = note_tree_search(t, &tree, &n, entry->key_sha1); - - assert(GET_PTR_TYPE(entry) == 0); /* no type bits set */ - l = (struct leaf_node *) CLR_PTR_TYPE(*p); - switch (GET_PTR_TYPE(*p)) { - case PTR_TYPE_NULL: - assert(!*p); - *p = SET_PTR_TYPE(entry, type); - return; - case PTR_TYPE_NOTE: - switch (type) { - case PTR_TYPE_NOTE: - if (!hashcmp(l->key_sha1, entry->key_sha1)) { - /* skip concatenation if l == entry */ - if (!hashcmp(l->val_sha1, entry->val_sha1)) - return; - - if (combine_notes(l->val_sha1, entry->val_sha1)) - die("failed to combine notes %s and %s" - " for object %s", - sha1_to_hex(l->val_sha1), - sha1_to_hex(entry->val_sha1), - sha1_to_hex(l->key_sha1)); - free(entry); - return; - } - break; - case PTR_TYPE_SUBTREE: - if (!SUBTREE_SHA1_PREFIXCMP(l->key_sha1, - entry->key_sha1)) { - /* unpack 'entry' */ - load_subtree(t, entry, tree, n); - free(entry); - return; - } - break; - } - break; - case PTR_TYPE_SUBTREE: - if (!SUBTREE_SHA1_PREFIXCMP(entry->key_sha1, l->key_sha1)) { - /* unpack 'l' and restart insert */ - *p = NULL; - load_subtree(t, l, tree, n); - free(l); - note_tree_insert(t, tree, n, entry, type, - combine_notes); - return; - } - break; - } - - /* non-matching leaf_node */ - assert(GET_PTR_TYPE(*p) == PTR_TYPE_NOTE || - GET_PTR_TYPE(*p) == PTR_TYPE_SUBTREE); - new_node = (struct int_node *) xcalloc(sizeof(struct int_node), 1); - note_tree_insert(t, new_node, n + 1, l, GET_PTR_TYPE(*p), - combine_notes); - *p = SET_PTR_TYPE(new_node, PTR_TYPE_INTERNAL); - note_tree_insert(t, new_node, n + 1, entry, type, combine_notes); -} - -/* * How to consolidate an int_node: * If there are > 1 non-NULL entries, give up and return non-zero. * Otherwise replace the int_node at the given index in the given parent node @@ -305,6 +225,93 @@ static void note_tree_remove(struct notes_tree *t, i--; } +/* + * To insert a leaf_node: + * Search to the tree location appropriate for the given leaf_node's key: + * - If location is unused (NULL), store the tweaked pointer directly there + * - If location holds a note entry that matches the note-to-be-inserted, then + * combine the two notes (by calling the given combine_notes function). + * - If location holds a note entry that matches the subtree-to-be-inserted, + * then unpack the subtree-to-be-inserted into the location. + * - If location holds a matching subtree entry, unpack the subtree at that + * location, and restart the insert operation from that level. + * - Else, create a new int_node, holding both the node-at-location and the + * node-to-be-inserted, and store the new int_node into the location. + */ +static int note_tree_insert(struct notes_tree *t, struct int_node *tree, + unsigned char n, struct leaf_node *entry, unsigned char type, + combine_notes_fn combine_notes) +{ + struct int_node *new_node; + struct leaf_node *l; + void **p = note_tree_search(t, &tree, &n, entry->key_sha1); + int ret = 0; + + assert(GET_PTR_TYPE(entry) == 0); /* no type bits set */ + l = (struct leaf_node *) CLR_PTR_TYPE(*p); + switch (GET_PTR_TYPE(*p)) { + case PTR_TYPE_NULL: + assert(!*p); + if (is_null_sha1(entry->val_sha1)) + free(entry); + else + *p = SET_PTR_TYPE(entry, type); + return 0; + case PTR_TYPE_NOTE: + switch (type) { + case PTR_TYPE_NOTE: + if (!hashcmp(l->key_sha1, entry->key_sha1)) { + /* skip concatenation if l == entry */ + if (!hashcmp(l->val_sha1, entry->val_sha1)) + return 0; + + ret = combine_notes(l->val_sha1, + entry->val_sha1); + if (!ret && is_null_sha1(l->val_sha1)) + note_tree_remove(t, tree, n, entry); + free(entry); + return ret; + } + break; + case PTR_TYPE_SUBTREE: + if (!SUBTREE_SHA1_PREFIXCMP(l->key_sha1, + entry->key_sha1)) { + /* unpack 'entry' */ + load_subtree(t, entry, tree, n); + free(entry); + return 0; + } + break; + } + break; + case PTR_TYPE_SUBTREE: + if (!SUBTREE_SHA1_PREFIXCMP(entry->key_sha1, l->key_sha1)) { + /* unpack 'l' and restart insert */ + *p = NULL; + load_subtree(t, l, tree, n); + free(l); + return note_tree_insert(t, tree, n, entry, type, + combine_notes); + } + break; + } + + /* non-matching leaf_node */ + assert(GET_PTR_TYPE(*p) == PTR_TYPE_NOTE || + GET_PTR_TYPE(*p) == PTR_TYPE_SUBTREE); + if (is_null_sha1(entry->val_sha1)) { /* skip insertion of empty note */ + free(entry); + return 0; + } + new_node = (struct int_node *) xcalloc(sizeof(struct int_node), 1); + ret = note_tree_insert(t, new_node, n + 1, l, GET_PTR_TYPE(*p), + combine_notes); + if (ret) + return ret; + *p = SET_PTR_TYPE(new_node, PTR_TYPE_INTERNAL); + return note_tree_insert(t, new_node, n + 1, entry, type, combine_notes); +} + /* Free the entire notes data contained in the given tree */ static void note_tree_free(struct int_node *tree) { @@ -445,8 +452,12 @@ static void load_subtree(struct notes_tree *t, struct leaf_node *subtree, l->key_sha1[19] = (unsigned char) len; type = PTR_TYPE_SUBTREE; } - note_tree_insert(t, node, n, l, type, - combine_notes_concatenate); + if (note_tree_insert(t, node, n, l, type, + combine_notes_concatenate)) + die("Failed to load %s %s into notes tree " + "from %s", + type == PTR_TYPE_NOTE ? "note" : "subtree", + sha1_to_hex(l->key_sha1), t->ref); } continue; @@ -804,16 +815,17 @@ int combine_notes_concatenate(unsigned char *cur_sha1, return 0; } - /* we will separate the notes by a newline anyway */ + /* we will separate the notes by two newlines anyway */ if (cur_msg[cur_len - 1] == '\n') cur_len--; /* concatenate cur_msg and new_msg into buf */ - buf_len = cur_len + 1 + new_len; + buf_len = cur_len + 2 + new_len; buf = (char *) xmalloc(buf_len); memcpy(buf, cur_msg, cur_len); buf[cur_len] = '\n'; - memcpy(buf + cur_len + 1, new_msg, new_len); + buf[cur_len + 1] = '\n'; + memcpy(buf + cur_len + 2, new_msg, new_len); free(cur_msg); free(new_msg); @@ -836,6 +848,82 @@ int combine_notes_ignore(unsigned char *cur_sha1, return 0; } +static int string_list_add_note_lines(struct string_list *sort_uniq_list, + const unsigned char *sha1) +{ + char *data; + unsigned long len; + enum object_type t; + struct strbuf buf = STRBUF_INIT; + struct strbuf **lines = NULL; + int i, list_index; + + if (is_null_sha1(sha1)) + return 0; + + /* read_sha1_file NUL-terminates */ + data = read_sha1_file(sha1, &t, &len); + if (t != OBJ_BLOB || !data || !len) { + free(data); + return t != OBJ_BLOB || !data; + } + + strbuf_attach(&buf, data, len, len + 1); + lines = strbuf_split(&buf, '\n'); + + for (i = 0; lines[i]; i++) { + if (lines[i]->buf[lines[i]->len - 1] == '\n') + strbuf_setlen(lines[i], lines[i]->len - 1); + if (!lines[i]->len) + continue; /* skip empty lines */ + list_index = string_list_find_insert_index(sort_uniq_list, + lines[i]->buf, 0); + if (list_index < 0) + continue; /* skip duplicate lines */ + string_list_insert_at_index(sort_uniq_list, list_index, + lines[i]->buf); + } + + strbuf_list_free(lines); + strbuf_release(&buf); + return 0; +} + +static int string_list_join_lines_helper(struct string_list_item *item, + void *cb_data) +{ + struct strbuf *buf = cb_data; + strbuf_addstr(buf, item->string); + strbuf_addch(buf, '\n'); + return 0; +} + +int combine_notes_cat_sort_uniq(unsigned char *cur_sha1, + const unsigned char *new_sha1) +{ + struct string_list sort_uniq_list = { NULL, 0, 0, 1 }; + struct strbuf buf = STRBUF_INIT; + int ret = 1; + + /* read both note blob objects into unique_lines */ + if (string_list_add_note_lines(&sort_uniq_list, cur_sha1)) + goto out; + if (string_list_add_note_lines(&sort_uniq_list, new_sha1)) + goto out; + + /* create a new blob object from sort_uniq_list */ + if (for_each_string_list(&sort_uniq_list, + string_list_join_lines_helper, &buf)) + goto out; + + ret = write_sha1_file(buf.buf, buf.len, blob_type, cur_sha1); + +out: + strbuf_release(&buf); + string_list_clear(&sort_uniq_list, 0); + return ret; +} + static int string_list_add_one_ref(const char *path, const unsigned char *sha1, int flag, void *cb) { @@ -893,7 +981,7 @@ static int notes_display_config(const char *k, const char *v, void *cb) return 0; } -static const char *default_notes_ref(void) +const char *default_notes_ref(void) { const char *notes_ref = NULL; if (!notes_ref) @@ -935,7 +1023,7 @@ void init_notes(struct notes_tree *t, const char *notes_ref, return; if (get_tree_entry(object_sha1, "", sha1, &mode)) die("Failed to read notes tree referenced by %s (%s)", - notes_ref, object_sha1); + notes_ref, sha1_to_hex(object_sha1)); hashclr(root_tree.key_sha1); hashcpy(root_tree.val_sha1, sha1); @@ -989,7 +1077,7 @@ void init_display_notes(struct display_notes_opt *opt) string_list_clear(&display_notes_refs, 0); } -void add_note(struct notes_tree *t, const unsigned char *object_sha1, +int add_note(struct notes_tree *t, const unsigned char *object_sha1, const unsigned char *note_sha1, combine_notes_fn combine_notes) { struct leaf_node *l; @@ -1003,7 +1091,7 @@ void add_note(struct notes_tree *t, const unsigned char *object_sha1, l = (struct leaf_node *) xmalloc(sizeof(struct leaf_node)); hashcpy(l->key_sha1, object_sha1); hashcpy(l->val_sha1, note_sha1); - note_tree_insert(t, t->root, 0, l, PTR_TYPE_NOTE, combine_notes); + return note_tree_insert(t, t->root, 0, l, PTR_TYPE_NOTE, combine_notes); } int remove_note(struct notes_tree *t, const unsigned char *object_sha1) @@ -1182,7 +1270,7 @@ void format_display_notes(const unsigned char *object_sha1, int copy_note(struct notes_tree *t, const unsigned char *from_obj, const unsigned char *to_obj, - int force, combine_notes_fn combine_fn) + int force, combine_notes_fn combine_notes) { const unsigned char *note = get_note(t, from_obj); const unsigned char *existing_note = get_note(t, to_obj); @@ -1191,9 +1279,9 @@ int copy_note(struct notes_tree *t, return 1; if (note) - add_note(t, to_obj, note, combine_fn); + return add_note(t, to_obj, note, combine_notes); else if (existing_note) - add_note(t, to_obj, null_sha1, combine_fn); + return add_note(t, to_obj, null_sha1, combine_notes); return 0; } @@ -12,7 +12,10 @@ * resulting SHA1 into the first SHA1 argument (cur_sha1). A non-zero return * value indicates failure. * - * The two given SHA1s must both be non-NULL and different from each other. + * The two given SHA1s shall both be non-NULL and different from each other. + * Either of them (but not both) may be == null_sha1, which indicates an + * empty/non-existent note. If the resulting SHA1 (cur_sha1) is == null_sha1, + * the note will be removed from the notes tree. * * The default combine_notes function (you get this when passing NULL) is * combine_notes_concatenate(), which appends the contents of the new note to @@ -24,6 +27,7 @@ typedef int (*combine_notes_fn)(unsigned char *cur_sha1, const unsigned char *ne int combine_notes_concatenate(unsigned char *cur_sha1, const unsigned char *new_sha1); int combine_notes_overwrite(unsigned char *cur_sha1, const unsigned char *new_sha1); int combine_notes_ignore(unsigned char *cur_sha1, const unsigned char *new_sha1); +int combine_notes_cat_sort_uniq(unsigned char *cur_sha1, const unsigned char *new_sha1); /* * Notes tree object @@ -44,6 +48,20 @@ extern struct notes_tree { } default_notes_tree; /* + * Return the default notes ref. + * + * The default notes ref is the notes ref that is used when notes_ref == NULL + * is passed to init_notes(). + * + * This the first of the following to be defined: + * 1. The '--ref' option to 'git notes', if given + * 2. The $GIT_NOTES_REF environment variable, if set + * 3. The value of the core.notesRef config variable, if set + * 4. GIT_NOTES_DEFAULT_REF (i.e. "refs/notes/commits") + */ +const char *default_notes_ref(void); + +/* * Flags controlling behaviour of notes tree initialization * * Default behaviour is to initialize the notes tree from the tree object @@ -76,11 +94,24 @@ void init_notes(struct notes_tree *t, const char *notes_ref, /* * Add the given note object to the given notes_tree structure * + * If there already exists a note for the given object_sha1, the given + * combine_notes function is invoked to break the tie. If not given (i.e. + * combine_notes == NULL), the default combine_notes function for the given + * notes_tree is used. + * + * Passing note_sha1 == null_sha1 indicates the addition of an + * empty/non-existent note. This is a (potentially expensive) no-op unless + * there already exists a note for the given object_sha1, AND combining that + * note with the empty note (using the given combine_notes function) results + * in a new/changed note. + * + * Returns zero on success; non-zero means combine_notes failed. + * * IMPORTANT: The changes made by add_note() to the given notes_tree structure * are not persistent until a subsequent call to write_notes_tree() returns * zero. */ -void add_note(struct notes_tree *t, const unsigned char *object_sha1, +int add_note(struct notes_tree *t, const unsigned char *object_sha1, const unsigned char *note_sha1, combine_notes_fn combine_notes); /* @@ -105,11 +136,18 @@ const unsigned char *get_note(struct notes_tree *t, /* * Copy a note from one object to another in the given notes_tree. * - * Fails if the to_obj already has a note unless 'force' is true. + * Returns 1 if the to_obj already has a note and 'force' is false. Otherwise, + * returns non-zero if 'force' is true, but the given combine_notes function + * failed to combine from_obj's note with to_obj's existing note. + * Returns zero on success. + * + * IMPORTANT: The changes made by copy_note() to the given notes_tree structure + * are not persistent until a subsequent call to write_notes_tree() returns + * zero. */ int copy_note(struct notes_tree *t, const unsigned char *from_obj, const unsigned char *to_obj, - int force, combine_notes_fn combine_fn); + int force, combine_notes_fn combine_notes); /* * Flags controlling behaviour of for_each_note() @@ -151,6 +189,7 @@ int copy_note(struct notes_tree *t, * notes tree) from within the callback: * - add_note() * - remove_note() + * - copy_note() * - free_notes() */ typedef int each_note_fn(const unsigned char *object_sha1, diff --git a/parse-options.h b/parse-options.h index d982f0f1bf..ae8647d893 100644 --- a/parse-options.h +++ b/parse-options.h @@ -198,14 +198,15 @@ extern int parse_opt_verbosity_cb(const struct option *, const char *, int); extern int parse_opt_with_commit(const struct option *, const char *, int); extern int parse_opt_tertiary(const struct option *, const char *, int); -#define OPT__VERBOSE(var) OPT_BOOLEAN('v', "verbose", (var), "be verbose") -#define OPT__QUIET(var) OPT_BOOLEAN('q', "quiet", (var), "be quiet") +#define OPT__VERBOSE(var, h) OPT_BOOLEAN('v', "verbose", (var), (h)) +#define OPT__QUIET(var, h) OPT_BOOLEAN('q', "quiet", (var), (h)) #define OPT__VERBOSITY(var) \ { OPTION_CALLBACK, 'v', "verbose", (var), NULL, "be more verbose", \ PARSE_OPT_NOARG, &parse_opt_verbosity_cb, 0 }, \ { OPTION_CALLBACK, 'q', "quiet", (var), NULL, "be more quiet", \ PARSE_OPT_NOARG, &parse_opt_verbosity_cb, 0 } -#define OPT__DRY_RUN(var) OPT_BOOLEAN('n', "dry-run", (var), "dry run") +#define OPT__DRY_RUN(var, h) OPT_BOOLEAN('n', "dry-run", (var), (h)) +#define OPT__FORCE(var, h) OPT_BOOLEAN('f', "force", (var), (h)) #define OPT__ABBREV(var) \ { OPTION_CALLBACK, 0, "abbrev", (var), "n", \ "use <n> digits to display SHA-1s", \ @@ -161,119 +161,6 @@ char *git_path_submodule(const char *path, const char *fmt, ...) return cleanup_path(pathname); } -/* git_mkstemp() - create tmp file honoring TMPDIR variable */ -int git_mkstemp(char *path, size_t len, const char *template) -{ - const char *tmp; - size_t n; - - tmp = getenv("TMPDIR"); - if (!tmp) - tmp = "/tmp"; - n = snprintf(path, len, "%s/%s", tmp, template); - if (len <= n) { - errno = ENAMETOOLONG; - return -1; - } - return mkstemp(path); -} - -/* git_mkstemps() - create tmp file with suffix honoring TMPDIR variable. */ -int git_mkstemps(char *path, size_t len, const char *template, int suffix_len) -{ - const char *tmp; - size_t n; - - tmp = getenv("TMPDIR"); - if (!tmp) - tmp = "/tmp"; - n = snprintf(path, len, "%s/%s", tmp, template); - if (len <= n) { - errno = ENAMETOOLONG; - return -1; - } - return mkstemps(path, suffix_len); -} - -/* Adapted from libiberty's mkstemp.c. */ - -#undef TMP_MAX -#define TMP_MAX 16384 - -int git_mkstemps_mode(char *pattern, int suffix_len, int mode) -{ - static const char letters[] = - "abcdefghijklmnopqrstuvwxyz" - "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "0123456789"; - static const int num_letters = 62; - uint64_t value; - struct timeval tv; - char *template; - size_t len; - int fd, count; - - len = strlen(pattern); - - if (len < 6 + suffix_len) { - errno = EINVAL; - return -1; - } - - if (strncmp(&pattern[len - 6 - suffix_len], "XXXXXX", 6)) { - errno = EINVAL; - return -1; - } - - /* - * Replace pattern's XXXXXX characters with randomness. - * Try TMP_MAX different filenames. - */ - gettimeofday(&tv, NULL); - value = ((size_t)(tv.tv_usec << 16)) ^ tv.tv_sec ^ getpid(); - template = &pattern[len - 6 - suffix_len]; - for (count = 0; count < TMP_MAX; ++count) { - uint64_t v = value; - /* Fill in the random bits. */ - template[0] = letters[v % num_letters]; v /= num_letters; - template[1] = letters[v % num_letters]; v /= num_letters; - template[2] = letters[v % num_letters]; v /= num_letters; - template[3] = letters[v % num_letters]; v /= num_letters; - template[4] = letters[v % num_letters]; v /= num_letters; - template[5] = letters[v % num_letters]; v /= num_letters; - - fd = open(pattern, O_CREAT | O_EXCL | O_RDWR, mode); - if (fd > 0) - return fd; - /* - * Fatal error (EPERM, ENOSPC etc). - * It doesn't make sense to loop. - */ - if (errno != EEXIST) - break; - /* - * This is a random value. It is only necessary that - * the next TMP_MAX values generated by adding 7777 to - * VALUE are different with (module 2^32). - */ - value += 7777; - } - /* We return the null string if we can't find a unique file name. */ - pattern[0] = '\0'; - return -1; -} - -int git_mkstemp_mode(char *pattern, int mode) -{ - /* mkstemp is just mkstemps with no suffix */ - return git_mkstemps_mode(pattern, 0, mode); -} - -int gitmkstemps(char *pattern, int suffix_len) -{ - return git_mkstemps_mode(pattern, suffix_len, 0600); -} - int validate_headref(const char *path) { struct stat st; @@ -403,8 +403,8 @@ static char *replace_encoding_header(char *buf, const char *encoding) return strbuf_detach(&tmp, NULL); } -static char *logmsg_reencode(const struct commit *commit, - const char *output_encoding) +char *logmsg_reencode(const struct commit *commit, + const char *output_encoding) { static const char *utf8 = "UTF-8"; const char *use_encoding; @@ -555,6 +555,7 @@ struct format_commit_context { const struct pretty_print_context *pretty_ctx; unsigned commit_header_parsed:1; unsigned commit_message_parsed:1; + char *message; size_t width, indent1, indent2; /* These offsets are relative to the start of the commit message. */ @@ -591,7 +592,7 @@ static int add_again(struct strbuf *sb, struct chunk *chunk) static void parse_commit_header(struct format_commit_context *context) { - const char *msg = context->commit->buffer; + const char *msg = context->message; int i; for (i = 0; msg[i]; i++) { @@ -677,8 +678,8 @@ const char *format_subject(struct strbuf *sb, const char *msg, static void parse_commit_message(struct format_commit_context *c) { - const char *msg = c->commit->buffer + c->message_off; - const char *start = c->commit->buffer; + const char *msg = c->message + c->message_off; + const char *start = c->message; msg = skip_empty_lines(msg); c->subject_off = msg - start; @@ -741,7 +742,7 @@ static size_t format_commit_one(struct strbuf *sb, const char *placeholder, { struct format_commit_context *c = context; const struct commit *commit = c->commit; - const char *msg = commit->buffer; + const char *msg = c->message; struct commit_list *p; int h1, h2; @@ -886,8 +887,7 @@ static size_t format_commit_one(struct strbuf *sb, const char *placeholder, case 'N': if (c->pretty_ctx->show_notes) { format_display_notes(commit->object.sha1, sb, - git_log_output_encoding ? git_log_output_encoding - : git_commit_encoding, 0); + get_log_output_encoding(), 0); return 1; } return 0; @@ -1012,13 +1012,27 @@ void format_commit_message(const struct commit *commit, const struct pretty_print_context *pretty_ctx) { struct format_commit_context context; + static const char utf8[] = "UTF-8"; + const char *enc; + const char *output_enc = pretty_ctx->output_encoding; memset(&context, 0, sizeof(context)); context.commit = commit; context.pretty_ctx = pretty_ctx; context.wrap_start = sb->len; + context.message = commit->buffer; + if (output_enc) { + enc = get_header(commit, "encoding"); + enc = enc ? enc : utf8; + if (strcmp(enc, output_enc)) + context.message = logmsg_reencode(commit, output_enc); + } + strbuf_expand(sb, format, format_commit_item, &context); rewrap_message_tail(sb, &context, 0, 0, 0); + + if (context.message != commit->buffer) + free(context.message); } static void pp_header(enum cmit_fmt fmt, @@ -1159,11 +1173,7 @@ char *reencode_commit_message(const struct commit *commit, const char **encoding { const char *encoding; - encoding = (git_log_output_encoding - ? git_log_output_encoding - : git_commit_encoding); - if (!encoding) - encoding = "UTF-8"; + encoding = get_log_output_encoding(); if (encoding_p) *encoding_p = encoding; return logmsg_reencode(commit, encoding); diff --git a/read-cache.c b/read-cache.c index 1f42473e80..4f2e890b01 100644 --- a/read-cache.c +++ b/read-cache.c @@ -608,6 +608,29 @@ int add_to_index(struct index_state *istate, const char *path, struct stat *st, ce->ce_mode = ce_mode_from_stat(ent, st_mode); } + /* When core.ignorecase=true, determine if a directory of the same name but differing + * case already exists within the Git repository. If it does, ensure the directory + * case of the file being added to the repository matches (is folded into) the existing + * entry's directory case. + */ + if (ignore_case) { + const char *startPtr = ce->name; + const char *ptr = startPtr; + while (*ptr) { + while (*ptr && *ptr != '/') + ++ptr; + if (*ptr == '/') { + struct cache_entry *foundce; + ++ptr; + foundce = index_name_exists(&the_index, ce->name, ptr - ce->name, ignore_case); + if (foundce) { + memcpy((void *)startPtr, foundce->name + (startPtr - ce->name), ptr - startPtr); + startPtr = ptr; + } + } + } + } + alias = index_name_exists(istate, ce->name, ce_namelen(ce), ignore_case); if (alias && !ce_stage(alias) && !ie_match_stat(istate, alias, st, ce_option)) { /* Nothing changed, really */ @@ -493,7 +493,7 @@ static void read_config(void) } /* - * We need to make sure the tracking branches are well formed, but a + * We need to make sure the remote-tracking branches are well formed, but a * wildcard refspec in "struct refspec" must have a trailing slash. We * temporarily drop the trailing '/' while calling check_ref_format(), * and put it back. The caller knows that a CHECK_REF_FORMAT_ONELEVEL diff --git a/sha1_file.c b/sha1_file.c index 0cd9435619..1cafdfa617 100644 --- a/sha1_file.c +++ b/sha1_file.c @@ -35,6 +35,8 @@ static size_t sz_fmt(size_t s) { return s; } const unsigned char null_sha1[20]; +static int git_open_noatime(const char *name, struct packed_git *p); + int safe_create_leading_directories(char *path) { char *pos = path + offset_1st_component(path); @@ -298,7 +300,7 @@ static void read_info_alternates(const char * relative_base, int depth) int fd; sprintf(path, "%s/%s", relative_base, alt_file_name); - fd = open(path, O_RDONLY); + fd = git_open_noatime(path, NULL); if (fd < 0) return; if (fstat(fd, &st) || (st.st_size == 0)) { @@ -411,7 +413,7 @@ static int check_packed_git_idx(const char *path, struct packed_git *p) struct pack_idx_header *hdr; size_t idx_size; uint32_t version, nr, i, *index; - int fd = open(path, O_RDONLY); + int fd = git_open_noatime(path, p); struct stat st; if (fd < 0) @@ -576,6 +578,21 @@ void release_pack_memory(size_t need, int fd) ; /* nothing */ } +void *xmmap(void *start, size_t length, + int prot, int flags, int fd, off_t offset) +{ + void *ret = mmap(start, length, prot, flags, fd, offset); + if (ret == MAP_FAILED) { + if (!length) + return NULL; + release_pack_memory(length, fd); + ret = mmap(start, length, prot, flags, fd, offset); + if (ret == MAP_FAILED) + die_errno("Out of memory? mmap failed"); + } + return ret; +} + void close_pack_windows(struct packed_git *p) { while (p->windows) { @@ -655,9 +672,7 @@ static int open_packed_git_1(struct packed_git *p) if (!p->index_data && open_pack_index(p)) return error("packfile %s index unavailable", p->pack_name); - p->pack_fd = open(p->pack_name, O_RDONLY); - while (p->pack_fd < 0 && errno == EMFILE && unuse_one_window(p, -1)) - p->pack_fd = open(p->pack_name, O_RDONLY); + p->pack_fd = git_open_noatime(p->pack_name, p); if (p->pack_fd < 0 || fstat(p->pack_fd, &st)) return -1; @@ -803,11 +818,22 @@ static struct packed_git *alloc_packed_git(int extra) return p; } +static void try_to_free_pack_memory(size_t size) +{ + release_pack_memory(size, -1); +} + struct packed_git *add_packed_git(const char *path, int path_len, int local) { + static int have_set_try_to_free_routine; struct stat st; struct packed_git *p = alloc_packed_git(path_len + 2); + if (!have_set_try_to_free_routine) { + have_set_try_to_free_routine = 1; + set_try_to_free_routine(try_to_free_pack_memory); + } + /* * Make sure a corresponding .pack file exists and that * the index looks sane. @@ -874,7 +900,7 @@ static void prepare_packed_git_one(char *objdir, int local) sprintf(path, "%s/pack", objdir); len = strlen(path); dir = opendir(path); - while (!dir && errno == EMFILE && unuse_one_window(packed_git, -1)) + while (!dir && errno == EMFILE && unuse_one_window(NULL, -1)) dir = opendir(path); if (!dir) { if (errno != ENOENT) @@ -1003,7 +1029,7 @@ static void mark_bad_packed_object(struct packed_git *p, p->num_bad_objects++; } -static int has_packed_and_bad(const unsigned char *sha1) +static const struct packed_git *has_packed_and_bad(const unsigned char *sha1) { struct packed_git *p; unsigned i; @@ -1011,8 +1037,8 @@ static int has_packed_and_bad(const unsigned char *sha1) for (p = packed_git; p; p = p->next) for (i = 0; i < p->num_bad_objects; i++) if (!hashcmp(sha1, p->bad_object_sha1 + 20 * i)) - return 1; - return 0; + return p; + return NULL; } int check_sha1_signature(const unsigned char *sha1, void *map, unsigned long size, const char *type) @@ -1022,18 +1048,31 @@ int check_sha1_signature(const unsigned char *sha1, void *map, unsigned long siz return hashcmp(sha1, real_sha1) ? -1 : 0; } -static int git_open_noatime(const char *name) +static int git_open_noatime(const char *name, struct packed_git *p) { static int sha1_file_open_flag = O_NOATIME; - int fd = open(name, O_RDONLY | sha1_file_open_flag); - /* Might the failure be due to O_NOATIME? */ - if (fd < 0 && errno != ENOENT && sha1_file_open_flag) { - fd = open(name, O_RDONLY); + for (;;) { + int fd = open(name, O_RDONLY | sha1_file_open_flag); if (fd >= 0) + return fd; + + /* Might the failure be insufficient file descriptors? */ + if (errno == EMFILE) { + if (unuse_one_window(p, -1)) + continue; + else + return -1; + } + + /* Might the failure be due to O_NOATIME? */ + if (errno != ENOENT && sha1_file_open_flag) { sha1_file_open_flag = 0; + continue; + } + + return -1; } - return fd; } static int open_sha1_file(const unsigned char *sha1) @@ -1042,7 +1081,7 @@ static int open_sha1_file(const unsigned char *sha1) char *name = sha1_file_name(sha1); struct alternate_object_database *alt; - fd = git_open_noatime(name); + fd = git_open_noatime(name, NULL); if (fd >= 0) return fd; @@ -1051,7 +1090,7 @@ static int open_sha1_file(const unsigned char *sha1) for (alt = alt_odb_list; alt; alt = alt->next) { name = alt->name; fill_sha1_path(name, sha1); - fd = git_open_noatime(alt->base); + fd = git_open_noatime(alt->base, NULL); if (fd >= 0) return fd; } @@ -2079,36 +2118,48 @@ static void *read_object(const unsigned char *sha1, enum object_type *type, return read_packed_sha1(sha1, type, size); } +/* + * This function dies on corrupt objects; the callers who want to + * deal with them should arrange to call read_object() and give error + * messages themselves. + */ void *read_sha1_file_repl(const unsigned char *sha1, enum object_type *type, unsigned long *size, const unsigned char **replacement) { const unsigned char *repl = lookup_replace_object(sha1); - void *data = read_object(repl, type, size); + void *data; char *path; + const struct packed_git *p; + + errno = 0; + data = read_object(repl, type, size); + if (data) { + if (replacement) + *replacement = repl; + return data; + } + + if (errno != ENOENT) + die_errno("failed to read object %s", sha1_to_hex(sha1)); /* die if we replaced an object with one that does not exist */ - if (!data && repl != sha1) + if (repl != sha1) die("replacement %s not found for %s", sha1_to_hex(repl), sha1_to_hex(sha1)); - /* legacy behavior is to die on corrupted objects */ - if (!data) { - if (has_loose_object(repl)) { - path = sha1_file_name(sha1); - die("loose object %s (stored in %s) is corrupted", sha1_to_hex(repl), path); - } - if (has_packed_and_bad(repl)) { - path = sha1_pack_name(sha1); - die("packed object %s (stored in %s) is corrupted", sha1_to_hex(repl), path); - } + if (has_loose_object(repl)) { + path = sha1_file_name(sha1); + die("loose object %s (stored in %s) is corrupt", + sha1_to_hex(repl), path); } - if (replacement) - *replacement = repl; + if ((p = has_packed_and_bad(repl)) != NULL) + die("packed object %s (stored in %s) is corrupt", + sha1_to_hex(repl), p->pack_name); - return data; + return NULL; } void *read_object_with_reference(const unsigned char *sha1, @@ -2300,7 +2351,7 @@ static int write_loose_object(const unsigned char *sha1, char *hdr, int hdrlen, filename = sha1_file_name(sha1); fd = create_tmpfile(tmpfile, sizeof(tmpfile), filename); - while (fd < 0 && errno == EMFILE && unuse_one_window(packed_git, -1)) + while (fd < 0 && errno == EMFILE && unuse_one_window(NULL, -1)) fd = create_tmpfile(tmpfile, sizeof(tmpfile), filename); if (fd < 0) { if (errno == EACCES) diff --git a/sha1_name.c b/sha1_name.c index 3e856b8036..2c3a5fb363 100644 --- a/sha1_name.c +++ b/sha1_name.c @@ -206,7 +206,9 @@ const char *find_unique_abbrev(const unsigned char *sha1, int len) if (exists ? !status : status == SHORT_NAME_NOT_FOUND) { - hex[len] = 0; + int cut_at = len + unique_abbrev_extra_length; + cut_at = (cut_at < 40) ? cut_at : 40; + hex[cut_at] = 0; return hex; } len++; @@ -934,6 +936,24 @@ int interpret_branch_name(const char *name, struct strbuf *buf) return len; } +int strbuf_branchname(struct strbuf *sb, const char *name) +{ + int len = strlen(name); + if (interpret_branch_name(name, sb) == len) + return 0; + strbuf_add(sb, name, len); + return len; +} + +int strbuf_check_branch_ref(struct strbuf *sb, const char *name) +{ + strbuf_branchname(sb, name); + if (name[0] == '-') + return CHECK_REF_FORMAT_ERROR; + strbuf_splice(sb, 0, 0, "refs/heads/", 11); + return check_ref_format(sb->buf); +} + /* * This is like "get_sha1_basic()", except it allows "sha1 expressions", * notably "xyz^" for "parent of xyz" @@ -386,21 +386,3 @@ int strbuf_read_file(struct strbuf *sb, const char *path, size_t hint) return len; } - -int strbuf_branchname(struct strbuf *sb, const char *name) -{ - int len = strlen(name); - if (interpret_branch_name(name, sb) == len) - return 0; - strbuf_add(sb, name, len); - return len; -} - -int strbuf_check_branch_ref(struct strbuf *sb, const char *name) -{ - strbuf_branchname(sb, name); - if (name[0] == '-') - return CHECK_REF_FORMAT_ERROR; - strbuf_splice(sb, 0, 0, "refs/heads/", 11); - return check_ref_format(sb->buf); -} diff --git a/symlinks.c b/symlinks.c index 8860120011..3cacebd91a 100644 --- a/symlinks.c +++ b/symlinks.c @@ -64,11 +64,13 @@ static inline void reset_lstat_cache(struct cache_def *cache) * of the prefix, where the cache should use the stat() function * instead of the lstat() function to test each path component. */ -static int lstat_cache(struct cache_def *cache, const char *name, int len, - int track_flags, int prefix_len_stat_func) +static int lstat_cache_matchlen(struct cache_def *cache, + const char *name, int len, + int *ret_flags, int track_flags, + int prefix_len_stat_func) { int match_len, last_slash, last_slash_dir, previous_slash; - int match_flags, ret_flags, save_flags, max_len, ret; + int save_flags, max_len, ret; struct stat st; if (cache->track_flags != track_flags || @@ -90,13 +92,13 @@ static int lstat_cache(struct cache_def *cache, const char *name, int len, match_len = last_slash = longest_path_match(name, len, cache->path, cache->len, &previous_slash); - match_flags = cache->flags & track_flags & (FL_NOENT|FL_SYMLINK); + *ret_flags = cache->flags & track_flags & (FL_NOENT|FL_SYMLINK); if (!(track_flags & FL_FULLPATH) && match_len == len) match_len = last_slash = previous_slash; - if (match_flags && match_len == cache->len) - return match_flags; + if (*ret_flags && match_len == cache->len) + return match_len; /* * If we now have match_len > 0, we would know that * the matched part will always be a directory. @@ -105,16 +107,16 @@ static int lstat_cache(struct cache_def *cache, const char *name, int len, * a substring of the cache on a path component basis, * we can return immediately. */ - match_flags = track_flags & FL_DIR; - if (match_flags && len == match_len) - return match_flags; + *ret_flags = track_flags & FL_DIR; + if (*ret_flags && len == match_len) + return match_len; } /* * Okay, no match from the cache so far, so now we have to * check the rest of the path components. */ - ret_flags = FL_DIR; + *ret_flags = FL_DIR; last_slash_dir = last_slash; max_len = len < PATH_MAX ? len : PATH_MAX; while (match_len < max_len) { @@ -133,16 +135,16 @@ static int lstat_cache(struct cache_def *cache, const char *name, int len, ret = lstat(cache->path, &st); if (ret) { - ret_flags = FL_LSTATERR; + *ret_flags = FL_LSTATERR; if (errno == ENOENT) - ret_flags |= FL_NOENT; + *ret_flags |= FL_NOENT; } else if (S_ISDIR(st.st_mode)) { last_slash_dir = last_slash; continue; } else if (S_ISLNK(st.st_mode)) { - ret_flags = FL_SYMLINK; + *ret_flags = FL_SYMLINK; } else { - ret_flags = FL_ERR; + *ret_flags = FL_ERR; } break; } @@ -152,7 +154,7 @@ static int lstat_cache(struct cache_def *cache, const char *name, int len, * path types, FL_NOENT, FL_SYMLINK and FL_DIR, can be cached * for the moment! */ - save_flags = ret_flags & track_flags & (FL_NOENT|FL_SYMLINK); + save_flags = *ret_flags & track_flags & (FL_NOENT|FL_SYMLINK); if (save_flags && last_slash > 0 && last_slash <= PATH_MAX) { cache->path[last_slash] = '\0'; cache->len = last_slash; @@ -176,7 +178,16 @@ static int lstat_cache(struct cache_def *cache, const char *name, int len, } else { reset_lstat_cache(cache); } - return ret_flags; + return match_len; +} + +static int lstat_cache(struct cache_def *cache, const char *name, int len, + int track_flags, int prefix_len_stat_func) +{ + int flags; + (void)lstat_cache_matchlen(cache, name, len, &flags, track_flags, + prefix_len_stat_func); + return flags; } #define USE_ONLY_LSTAT 0 @@ -198,15 +209,26 @@ int has_symlink_leading_path(const char *name, int len) } /* - * Return non-zero if path 'name' has a leading symlink component or + * Return zero if path 'name' has a leading symlink component or * if some leading path component does not exists. + * + * Return -1 if leading path exists and is a directory. + * + * Return path length if leading path exists and is neither a + * directory nor a symlink. */ -int has_symlink_or_noent_leading_path(const char *name, int len) +int check_leading_path(const char *name, int len) { struct cache_def *cache = &default_cache; /* FIXME */ - return lstat_cache(cache, name, len, - FL_SYMLINK|FL_NOENT|FL_DIR, USE_ONLY_LSTAT) & - (FL_SYMLINK|FL_NOENT); + int flags; + int match_len = lstat_cache_matchlen(cache, name, len, &flags, + FL_SYMLINK|FL_NOENT|FL_DIR, USE_ONLY_LSTAT); + if (flags & (FL_SYMLINK|FL_NOENT)) + return 0; + else if (flags & FL_DIR) + return -1; + else + return match_len; } /* @@ -265,14 +265,11 @@ Do: test ... That way all of the commands in your tests will succeed or fail. If - you must ignore the return value of something (e.g., the return - after unsetting a variable that was already unset is unportable) it's - best to indicate so explicitly with a semicolon: - - unset HLAGH; - git merge hla && - git push gh && - test ... + you must ignore the return value of something, consider using a + helper function (e.g. use sane_unset instead of unset, in order + to avoid unportable return value for unsetting a variable that was + already unset), or prepending the command with test_might_fail or + test_must_fail. - Check the test coverage for your tests. See the "Test coverage" below. @@ -401,13 +398,6 @@ library for your script to use. Like test_expect_success this function can optionally use a three argument invocation with a prerequisite as the first argument. - - test_expect_code [<prereq>] <code> <message> <script> - - Analogous to test_expect_success, but pass the test if it exits - with a given exit <code> - - test_expect_code 1 'Merge with d/f conflicts' 'git merge "merge msg" B master' - - test_debug <script> This takes a single argument, <script>, and evaluates it only @@ -488,6 +478,15 @@ library for your script to use. 'Perl API' \ "$PERL_PATH" "$TEST_DIRECTORY"/t9700/test.pl + - test_expect_code <exit-code> <command> + + Run a command and ensure that it exits with the given exit code. + For example: + + test_expect_success 'Merge with d/f conflicts' ' + test_expect_code 1 git merge "merge msg" B master + ' + - test_must_fail <git-command> Run a git command and ensure it fails in a controlled way. Use @@ -507,6 +506,10 @@ library for your script to use. <expected> file. This behaves like "cmp" but produces more helpful output when the test is run with "-v" option. + - test_line_count (= | -lt | -ge | ...) <length> <file> + + Check whether a file has the length it is expected to. + - test_path_is_file <file> [<diagnosis>] test_path_is_dir <dir> [<diagnosis>] test_path_is_missing <path> [<diagnosis>] diff --git a/t/annotate-tests.sh b/t/annotate-tests.sh index 141b60cdcb..d34208cc27 100644 --- a/t/annotate-tests.sh +++ b/t/annotate-tests.sh @@ -38,8 +38,8 @@ test_expect_success \ 'prepare reference tree' \ 'echo "1A quick brown fox jumps over the" >file && echo "lazy dog" >>file && - git add file - GIT_AUTHOR_NAME="A" git commit -a -m "Initial."' + git add file && + GIT_AUTHOR_NAME="A" GIT_AUTHOR_EMAIL="A@test.git" git commit -a -m "Initial."' test_expect_success \ 'check all lines blamed on A' \ @@ -49,7 +49,7 @@ test_expect_success \ 'Setup new lines blamed on B' \ 'echo "2A quick brown fox jumps over the" >>file && echo "lazy dog" >> file && - GIT_AUTHOR_NAME="B" git commit -a -m "Second."' + GIT_AUTHOR_NAME="B" GIT_AUTHOR_EMAIL="B@test.git" git commit -a -m "Second."' test_expect_success \ 'Two lines blamed on A, two on B' \ @@ -60,7 +60,7 @@ test_expect_success \ 'git checkout -b branch1 master && echo "3A slow green fox jumps into the" >> file && echo "well." >> file && - GIT_AUTHOR_NAME="B1" git commit -a -m "Branch1-1"' + GIT_AUTHOR_NAME="B1" GIT_AUTHOR_EMAIL="B1@test.git" git commit -a -m "Branch1-1"' test_expect_success \ 'Two lines blamed on A, two on B, two on B1' \ @@ -71,7 +71,7 @@ test_expect_success \ 'git checkout -b branch2 master && sed -e "s/2A quick brown/4A quick brown lazy dog/" < file > file.new && mv file.new file && - GIT_AUTHOR_NAME="B2" git commit -a -m "Branch2-1"' + GIT_AUTHOR_NAME="B2" GIT_AUTHOR_EMAIL="B2@test.git" git commit -a -m "Branch2-1"' test_expect_success \ 'Two lines blamed on A, one on B, one on B2' \ @@ -105,7 +105,7 @@ test_expect_success \ test_expect_success \ 'an incomplete line added' \ 'echo "incomplete" | tr -d "\\012" >>file && - GIT_AUTHOR_NAME="C" git commit -a -m "Incomplete"' + GIT_AUTHOR_NAME="C" GIT_AUTHOR_EMAIL="C@test.git" git commit -a -m "Incomplete"' test_expect_success \ 'With incomplete lines.' \ @@ -119,7 +119,7 @@ test_expect_success \ echo } | sed -e "s/^3A/99/" -e "/^1A/d" -e "/^incomplete/d" > file && echo "incomplete" | tr -d "\\012" >>file && - GIT_AUTHOR_NAME="D" git commit -a -m "edit"' + GIT_AUTHOR_NAME="D" GIT_AUTHOR_EMAIL="D@test.git" git commit -a -m "edit"' test_expect_success \ 'some edit' \ diff --git a/t/lib-httpd.sh b/t/lib-httpd.sh index e733f6516f..3f24384371 100644 --- a/t/lib-httpd.sh +++ b/t/lib-httpd.sh @@ -75,12 +75,14 @@ fi prepare_httpd() { mkdir -p "$HTTPD_DOCUMENT_ROOT_PATH" + cp "$TEST_PATH"/passwd "$HTTPD_ROOT_PATH" ln -s "$LIB_HTTPD_MODULE_PATH" "$HTTPD_ROOT_PATH/modules" if test -n "$LIB_HTTPD_SSL" then HTTPD_URL=https://127.0.0.1:$LIB_HTTPD_PORT + AUTH_HTTPD_URL=https://user%40host:user%40host@127.0.0.1:$LIB_HTTPD_PORT RANDFILE_PATH="$HTTPD_ROOT_PATH"/.rnd openssl req \ -config "$TEST_PATH/ssl.cnf" \ @@ -92,6 +94,7 @@ prepare_httpd() { HTTPD_PARA="$HTTPD_PARA -DSSL" else HTTPD_URL=http://127.0.0.1:$LIB_HTTPD_PORT + AUTH_HTTPD_URL=http://user%40host:user%40host@127.0.0.1:$LIB_HTTPD_PORT fi if test -n "$LIB_HTTPD_DAV" -o -n "$LIB_HTTPD_SVN" diff --git a/t/lib-httpd/apache.conf b/t/lib-httpd/apache.conf index 4961505d1d..0a4cdfa93e 100644 --- a/t/lib-httpd/apache.conf +++ b/t/lib-httpd/apache.conf @@ -17,8 +17,33 @@ ErrorLog error.log <IfModule !mod_env.c> LoadModule env_module modules/mod_env.so </IfModule> +<IfModule !mod_rewrite.c> + LoadModule rewrite_module modules/mod_rewrite.so +</IFModule> +<IfModule !mod_version.c> + LoadModule version_module modules/mod_version.so +</IfModule> + +<IfVersion < 2.1> +<IfModule !mod_auth.c> + LoadModule auth_module modules/mod_auth.so +</IfModule> +</IfVersion> + +<IfVersion >= 2.1> +<IfModule !mod_auth_basic.c> + LoadModule auth_basic_module modules/mod_auth_basic.so +</IfModule> +<IfModule !mod_authn_file.c> + LoadModule authn_file_module modules/mod_authn_file.so +</IfModule> +<IfModule !mod_authz_user.c> + LoadModule authz_user_module modules/mod_authz_user.so +</IfModule> +</IfVersion> Alias /dumb/ www/ +Alias /auth/ www/auth/ <Location /smart/> SetEnv GIT_EXEC_PATH ${GIT_EXEC_PATH} @@ -36,6 +61,10 @@ ScriptAlias /smart_noexport/ ${GIT_EXEC_PATH}/git-http-backend/ Options ExecCGI </Files> +RewriteEngine on +RewriteRule ^/smart-redir-perm/(.*)$ /smart/$1 [R=301] +RewriteRule ^/smart-redir-temp/(.*)$ /smart/$1 [R=302] + <IfDefine SSL> LoadModule ssl_module modules/mod_ssl.so @@ -48,6 +77,13 @@ SSLMutex file:ssl_mutex SSLEngine On </IfDefine> +<Location /auth/> + AuthType Basic + AuthName "git-auth" + AuthUserFile passwd + Require valid-user +</Location> + <IfDefine DAV> LoadModule dav_module modules/mod_dav.so LoadModule dav_fs_module modules/mod_dav_fs.so diff --git a/t/lib-httpd/passwd b/t/lib-httpd/passwd new file mode 100644 index 0000000000..f2fbcad33e --- /dev/null +++ b/t/lib-httpd/passwd @@ -0,0 +1 @@ +user@host:nKpa8pZUHx/ic diff --git a/t/t0000-basic.sh b/t/t0000-basic.sh index f688bd3ef5..2f7002a5e5 100755 --- a/t/t0000-basic.sh +++ b/t/t0000-basic.sh @@ -130,22 +130,57 @@ test_expect_success 'tests clean up after themselves' ' test_when_finished clean=yes ' -cleaner=no -test_expect_code 1 'tests clean up even after a failure' ' - test_when_finished cleaner=yes && - (exit 1) -' - -if test $clean$cleaner != yesyes +if test $clean != yes then - say "bug in test framework: cleanup commands do not work reliably" + say "bug in test framework: basic cleanup command does not work reliably" exit 1 fi -test_expect_code 2 'failure to clean up causes the test to fail' ' - test_when_finished "(exit 2)" +test_expect_success 'tests clean up even on failures' " + mkdir failing-cleanup && + (cd failing-cleanup && + cat >failing-cleanup.sh <<EOF && +#!$SHELL_PATH + +test_description='Failing tests with cleanup commands' + +# Point to the t/test-lib.sh, which isn't in ../ as usual +TEST_DIRECTORY=\"$TEST_DIRECTORY\" +. \"\$TEST_DIRECTORY\"/test-lib.sh + +test_expect_success 'tests clean up even after a failure' ' + touch clean-after-failure && + test_when_finished rm clean-after-failure && + (exit 1) +' + +test_expect_success 'failure to clean up causes the test to fail' ' + test_when_finished \"(exit 2)\" ' +test_done +EOF + chmod +x failing-cleanup.sh && + test_must_fail ./failing-cleanup.sh >out 2>err && + ! test -s err && + ! test -f \"trash directory.failing-cleanup/clean-after-failure\" && +sed -e 's/Z$//' >expect <<\EOF && +not ok - 1 tests clean up even after a failure +# Z +# touch clean-after-failure && +# test_when_finished rm clean-after-failure && +# (exit 1) +# Z +not ok - 2 failure to clean up causes the test to fail +# Z +# test_when_finished \"(exit 2)\" +# Z +# failed 2 among 2 test(s) +1..2 +EOF + test_cmp expect out) +" + ################################################################ # Basics of the basics diff --git a/t/t0001-init.sh b/t/t0001-init.sh index 7fe8883ae0..d44194c35f 100755 --- a/t/t0001-init.sh +++ b/t/t0001-init.sh @@ -25,7 +25,7 @@ check_config () { test_expect_success 'plain' ' ( - unset GIT_DIR GIT_WORK_TREE + sane_unset GIT_DIR GIT_WORK_TREE && mkdir plain && cd plain && git init @@ -35,7 +35,7 @@ test_expect_success 'plain' ' test_expect_success 'plain with GIT_WORK_TREE' ' if ( - unset GIT_DIR + sane_unset GIT_DIR && mkdir plain-wt && cd plain-wt && GIT_WORK_TREE=$(pwd) git init @@ -48,7 +48,7 @@ test_expect_success 'plain with GIT_WORK_TREE' ' test_expect_success 'plain bare' ' ( - unset GIT_DIR GIT_WORK_TREE GIT_CONFIG + sane_unset GIT_DIR GIT_WORK_TREE GIT_CONFIG && mkdir plain-bare-1 && cd plain-bare-1 && git --bare init @@ -58,7 +58,7 @@ test_expect_success 'plain bare' ' test_expect_success 'plain bare with GIT_WORK_TREE' ' if ( - unset GIT_DIR GIT_CONFIG + sane_unset GIT_DIR GIT_CONFIG && mkdir plain-bare-2 && cd plain-bare-2 && GIT_WORK_TREE=$(pwd) git --bare init @@ -72,7 +72,7 @@ test_expect_success 'plain bare with GIT_WORK_TREE' ' test_expect_success 'GIT_DIR bare' ' ( - unset GIT_CONFIG + sane_unset GIT_CONFIG && mkdir git-dir-bare.git && GIT_DIR=git-dir-bare.git git init ) && @@ -82,7 +82,7 @@ test_expect_success 'GIT_DIR bare' ' test_expect_success 'init --bare' ' ( - unset GIT_DIR GIT_WORK_TREE GIT_CONFIG + sane_unset GIT_DIR GIT_WORK_TREE GIT_CONFIG && mkdir init-bare.git && cd init-bare.git && git init --bare @@ -93,7 +93,7 @@ test_expect_success 'init --bare' ' test_expect_success 'GIT_DIR non-bare' ' ( - unset GIT_CONFIG + sane_unset GIT_CONFIG && mkdir non-bare && cd non-bare && GIT_DIR=.git git init @@ -104,7 +104,7 @@ test_expect_success 'GIT_DIR non-bare' ' test_expect_success 'GIT_DIR & GIT_WORK_TREE (1)' ' ( - unset GIT_CONFIG + sane_unset GIT_CONFIG && mkdir git-dir-wt-1.git && GIT_WORK_TREE=$(pwd) GIT_DIR=git-dir-wt-1.git git init ) && @@ -114,7 +114,7 @@ test_expect_success 'GIT_DIR & GIT_WORK_TREE (1)' ' test_expect_success 'GIT_DIR & GIT_WORK_TREE (2)' ' if ( - unset GIT_CONFIG + sane_unset GIT_CONFIG && mkdir git-dir-wt-2.git && GIT_WORK_TREE=$(pwd) GIT_DIR=git-dir-wt-2.git git --bare init ) @@ -127,7 +127,7 @@ test_expect_success 'GIT_DIR & GIT_WORK_TREE (2)' ' test_expect_success 'reinit' ' ( - unset GIT_CONFIG GIT_WORK_TREE GIT_CONFIG + sane_unset GIT_CONFIG GIT_WORK_TREE GIT_CONFIG && mkdir again && cd again && @@ -175,8 +175,8 @@ test_expect_success 'init with init.templatedir set' ' git config -f "$test_config" init.templatedir "${HOME}/templatedir-source" && mkdir templatedir-set && cd templatedir-set && - unset GIT_CONFIG_NOGLOBAL && - unset GIT_TEMPLATE_DIR && + sane_unset GIT_CONFIG_NOGLOBAL && + sane_unset GIT_TEMPLATE_DIR && NO_SET_GIT_TEMPLATE_DIR=t && export NO_SET_GIT_TEMPLATE_DIR && git init @@ -187,7 +187,7 @@ test_expect_success 'init with init.templatedir set' ' test_expect_success 'init --bare/--shared overrides system/global config' ' ( test_config="$HOME"/.gitconfig && - unset GIT_CONFIG_NOGLOBAL && + sane_unset GIT_CONFIG_NOGLOBAL && git config -f "$test_config" core.bare false && git config -f "$test_config" core.sharedRepository 0640 && mkdir init-bare-shared-override && @@ -202,7 +202,7 @@ test_expect_success 'init --bare/--shared overrides system/global config' ' test_expect_success 'init honors global core.sharedRepository' ' ( test_config="$HOME"/.gitconfig && - unset GIT_CONFIG_NOGLOBAL && + sane_unset GIT_CONFIG_NOGLOBAL && git config -f "$test_config" core.sharedRepository 0666 && mkdir shared-honor-global && cd shared-honor-global && diff --git a/t/t0003-attributes.sh b/t/t0003-attributes.sh index e75153bdea..ebbc7554a7 100755 --- a/t/t0003-attributes.sh +++ b/t/t0003-attributes.sh @@ -72,7 +72,7 @@ test_expect_success 'core.attributesfile' ' test_expect_success 'attribute test: read paths from stdin' ' - cat <<EOF > expect + cat <<EOF > expect && f: test: f a/f: test: f a/c/f: test: f diff --git a/t/t0020-crlf.sh b/t/t0020-crlf.sh index 234a94f3e6..1a8f44c44c 100755 --- a/t/t0020-crlf.sh +++ b/t/t0020-crlf.sh @@ -439,7 +439,7 @@ test_expect_success 'checkout when deleting .gitattributes' ' git rm .gitattributes && echo "contentsQ" | q_to_cr > .file2 && git add .file2 && - git commit -m third + git commit -m third && git checkout master~1 && git checkout master && diff --git a/t/t0024-crlf-archive.sh b/t/t0024-crlf-archive.sh index c7d0324374..ec6c1b3f8a 100755 --- a/t/t0024-crlf-archive.sh +++ b/t/t0024-crlf-archive.sh @@ -7,7 +7,7 @@ UNZIP=${UNZIP:-unzip} test_expect_success setup ' - git config core.autocrlf true + git config core.autocrlf true && printf "CRLF line ending\r\nAnd another\r\n" > sample && git add sample && @@ -20,7 +20,7 @@ test_expect_success setup ' test_expect_success 'tar archive' ' git archive --format=tar HEAD | - ( mkdir untarred && cd untarred && "$TAR" -xf - ) + ( mkdir untarred && cd untarred && "$TAR" -xf - ) && test_cmp sample untarred/sample diff --git a/t/t0026-eol-config.sh b/t/t0026-eol-config.sh index f37ac8fa0b..fe0164be62 100755 --- a/t/t0026-eol-config.sh +++ b/t/t0026-eol-config.sh @@ -12,7 +12,7 @@ test_expect_success setup ' git config core.autocrlf false && - echo "one text" > .gitattributes + echo "one text" > .gitattributes && for w in Hello world how are you; do echo $w; done >one && for w in I am very very fine thank you; do echo $w; done >two && diff --git a/t/t0050-filesystem.sh b/t/t0050-filesystem.sh index 41df6bcf27..057c97c49f 100755 --- a/t/t0050-filesystem.sh +++ b/t/t0050-filesystem.sh @@ -12,14 +12,14 @@ unibad= no_symlinks= test_expect_success 'see what we expect' ' - test_case=test_expect_success - test_unicode=test_expect_success + test_case=test_expect_success && + test_unicode=test_expect_success && mkdir junk && echo good >junk/CamelCase && echo bad >junk/camelcase && if test "$(cat junk/CamelCase)" != good then - test_case=test_expect_failure + test_case=test_expect_failure && case_insensitive=t fi && rm -fr junk && @@ -27,7 +27,7 @@ test_expect_success 'see what we expect' ' >junk/"$auml" && case "$(cd junk && echo *)" in "$aumlcdiar") - test_unicode=test_expect_failure + test_unicode=test_expect_failure && unibad=t ;; *) ;; @@ -36,7 +36,7 @@ test_expect_success 'see what we expect' ' { ln -s x y 2> /dev/null && test -h y 2> /dev/null || - no_symlinks=1 + no_symlinks=1 && rm -f y } ' @@ -128,7 +128,7 @@ test_expect_success "setup unicode normalization tests" ' cd unicode && touch "$aumlcdiar" && git add "$aumlcdiar" && - git commit -m initial + git commit -m initial && git tag initial && git checkout -b topic && git mv $aumlcdiar tmp && diff --git a/t/t1000-read-tree-m-3way.sh b/t/t1000-read-tree-m-3way.sh index 4f171722d9..ca8a4098fa 100755 --- a/t/t1000-read-tree-m-3way.sh +++ b/t/t1000-read-tree-m-3way.sh @@ -309,7 +309,7 @@ test_expect_success \ test_expect_success \ '6 - must not exist in O && !A && !B case' " rm -f .git/index DD && - echo DD >DD + echo DD >DD && git update-index --add DD && test_must_fail git read-tree -m $tree_O $tree_A $tree_B " diff --git a/t/t1001-read-tree-m-2way.sh b/t/t1001-read-tree-m-2way.sh index 93ca84f9e6..680d992f22 100755 --- a/t/t1001-read-tree-m-2way.sh +++ b/t/t1001-read-tree-m-2way.sh @@ -98,8 +98,8 @@ test_expect_success \ git checkout-index -u -f -q -a && git update-index --add yomin && read_tree_twoway $treeH $treeM && - git ls-files --stage >4.out || return 1 - git diff --no-index M.out 4.out >4diff.out + git ls-files --stage >4.out && + test_must_fail git diff --no-index M.out 4.out >4diff.out && compare_change 4diff.out expected && check_cache_at yomin clean' @@ -112,8 +112,8 @@ test_expect_success \ git update-index --add yomin && echo yomin yomin >yomin && read_tree_twoway $treeH $treeM && - git ls-files --stage >5.out || return 1 - git diff --no-index M.out 5.out >5diff.out + git ls-files --stage >5.out && + test_must_fail git diff --no-index M.out 5.out >5diff.out && compare_change 5diff.out expected && check_cache_at yomin dirty' @@ -213,8 +213,8 @@ test_expect_success \ echo nitfol nitfol >nitfol && git update-index --add nitfol && read_tree_twoway $treeH $treeM && - git ls-files --stage >14.out || return 1 - git diff --no-index M.out 14.out >14diff.out + git ls-files --stage >14.out && + test_must_fail git diff --no-index M.out 14.out >14diff.out && compare_change 14diff.out expected && check_cache_at nitfol clean' @@ -227,8 +227,8 @@ test_expect_success \ git update-index --add nitfol && echo nitfol nitfol nitfol >nitfol && read_tree_twoway $treeH $treeM && - git ls-files --stage >15.out || return 1 - git diff --no-index M.out 15.out >15diff.out + git ls-files --stage >15.out && + test_must_fail git diff --no-index M.out 15.out >15diff.out && compare_change 15diff.out expected && check_cache_at nitfol dirty' @@ -377,7 +377,7 @@ test_expect_success \ git ls-files --stage >treeM.out && rm -f a && - mkdir a + mkdir a && : >a/b && git update-index --add --remove a a/b && treeH=`git write-tree` && @@ -394,7 +394,7 @@ test_expect_success '-m references the correct modified tree' ' echo >file-a && echo >file-b && git add file-a file-b && - git commit -a -m "test for correct modified tree" + git commit -a -m "test for correct modified tree" && git branch initial-mod && echo b >file-b && git commit -a -m "B" && diff --git a/t/t1002-read-tree-m-u-2way.sh b/t/t1002-read-tree-m-u-2way.sh index 0241329a08..a4a17e0017 100755 --- a/t/t1002-read-tree-m-u-2way.sh +++ b/t/t1002-read-tree-m-u-2way.sh @@ -205,8 +205,8 @@ test_expect_success \ echo nitfol nitfol >nitfol && git update-index --add nitfol && git read-tree -m -u $treeH $treeM && - git ls-files --stage >14.out || return 1 - git diff -U0 --no-index M.out 14.out >14diff.out + git ls-files --stage >14.out && + test_must_fail git diff -U0 --no-index M.out 14.out >14diff.out && compare_change 14diff.out expected && sum bozbar frotz >actual14.sum && grep -v nitfol M.sum > expected14.sum && @@ -226,8 +226,8 @@ test_expect_success \ git update-index --add nitfol && echo nitfol nitfol nitfol >nitfol && git read-tree -m -u $treeH $treeM && - git ls-files --stage >15.out || return 1 - git diff -U0 --no-index M.out 15.out >15diff.out + git ls-files --stage >15.out && + test_must_fail git diff -U0 --no-index M.out 15.out >15diff.out && compare_change 15diff.out expected && check_cache_at nitfol dirty && sum bozbar frotz >actual15.sum && @@ -314,7 +314,7 @@ test_expect_success \ # Also make sure we did not break DF vs DF/DF case. test_expect_success \ 'DF vs DF/DF case setup.' \ - 'rm -f .git/index + 'rm -f .git/index && echo DF >DF && git update-index --add DF && treeDF=`git write-tree` && diff --git a/t/t1011-read-tree-sparse-checkout.sh b/t/t1011-read-tree-sparse-checkout.sh index 8008fa2d89..0ef11bccb4 100755 --- a/t/t1011-read-tree-sparse-checkout.sh +++ b/t/t1011-read-tree-sparse-checkout.sh @@ -49,7 +49,7 @@ test_expect_success 'read-tree without .git/info/sparse-checkout' ' ' test_expect_success 'read-tree with .git/info/sparse-checkout but disabled' ' - echo >.git/info/sparse-checkout + echo >.git/info/sparse-checkout && git read-tree -m -u HEAD && git ls-files -t >result && test_cmp expected.swt result && diff --git a/t/t1200-tutorial.sh b/t/t1200-tutorial.sh index ab55eda158..bfa2c2190d 100755 --- a/t/t1200-tutorial.sh +++ b/t/t1200-tutorial.sh @@ -42,7 +42,7 @@ test_expect_success 'git diff' ' ' test_expect_success 'tree' ' - tree=$(git write-tree 2>/dev/null) + tree=$(git write-tree 2>/dev/null) && test 8988da15d077d4829fc51d8544c097def6644dbb = $tree ' diff --git a/t/t1302-repo-version.sh b/t/t1302-repo-version.sh index a6bf1bf4d6..0e47662406 100755 --- a/t/t1302-repo-version.sh +++ b/t/t1302-repo-version.sh @@ -39,7 +39,7 @@ test_expect_success 'gitdir selection on unsupported repo' ' ( cd test2 && git config core.repositoryformatversion >../actual - ) + ) && test_cmp expect actual ' diff --git a/t/t1400-update-ref.sh b/t/t1400-update-ref.sh index 54ba3df95f..ff747f8229 100755 --- a/t/t1400-update-ref.sh +++ b/t/t1400-update-ref.sh @@ -52,9 +52,8 @@ rm -f .git/$m test_expect_success \ "fail to create $n" \ - "touch .git/$n_dir - git update-ref $n $A >out 2>err"' - test $? != 0' + "touch .git/$n_dir && + test_must_fail git update-ref $n $A >out 2>err" rm -f .git/$n_dir out err test_expect_success \ @@ -185,55 +184,55 @@ gd="Thu, 26 May 2005 18:33:00 -0500" ld="Thu, 26 May 2005 18:43:00 -0500" test_expect_success \ 'Query "master@{May 25 2005}" (before history)' \ - 'rm -f o e + 'rm -f o e && git rev-parse --verify "master@{May 25 2005}" >o 2>e && test '"$C"' = $(cat o) && test "warning: Log for '\'master\'' only goes back to $ed." = "$(cat e)"' test_expect_success \ "Query master@{2005-05-25} (before history)" \ - 'rm -f o e + 'rm -f o e && git rev-parse --verify master@{2005-05-25} >o 2>e && test '"$C"' = $(cat o) && echo test "warning: Log for '\'master\'' only goes back to $ed." = "$(cat e)"' test_expect_success \ 'Query "master@{May 26 2005 23:31:59}" (1 second before history)' \ - 'rm -f o e + 'rm -f o e && git rev-parse --verify "master@{May 26 2005 23:31:59}" >o 2>e && test '"$C"' = $(cat o) && test "warning: Log for '\''master'\'' only goes back to $ed." = "$(cat e)"' test_expect_success \ 'Query "master@{May 26 2005 23:32:00}" (exactly history start)' \ - 'rm -f o e + 'rm -f o e && git rev-parse --verify "master@{May 26 2005 23:32:00}" >o 2>e && test '"$C"' = $(cat o) && test "" = "$(cat e)"' test_expect_success \ 'Query "master@{May 26 2005 23:32:30}" (first non-creation change)' \ - 'rm -f o e + 'rm -f o e && git rev-parse --verify "master@{May 26 2005 23:32:30}" >o 2>e && test '"$A"' = $(cat o) && test "" = "$(cat e)"' test_expect_success \ 'Query "master@{2005-05-26 23:33:01}" (middle of history with gap)' \ - 'rm -f o e + 'rm -f o e && git rev-parse --verify "master@{2005-05-26 23:33:01}" >o 2>e && test '"$B"' = $(cat o) && test "warning: Log .git/logs/'"$m has gap after $gd"'." = "$(cat e)"' test_expect_success \ 'Query "master@{2005-05-26 23:38:00}" (middle of history)' \ - 'rm -f o e + 'rm -f o e && git rev-parse --verify "master@{2005-05-26 23:38:00}" >o 2>e && test '"$Z"' = $(cat o) && test "" = "$(cat e)"' test_expect_success \ 'Query "master@{2005-05-26 23:43:00}" (exact end of history)' \ - 'rm -f o e + 'rm -f o e && git rev-parse --verify "master@{2005-05-26 23:43:00}" >o 2>e && test '"$E"' = $(cat o) && test "" = "$(cat e)"' test_expect_success \ 'Query "master@{2005-05-28}" (past end of history)' \ - 'rm -f o e + 'rm -f o e && git rev-parse --verify "master@{2005-05-28}" >o 2>e && test '"$D"' = $(cat o) && test "warning: Log .git/logs/'"$m unexpectedly ended on $ld"'." = "$(cat e)"' @@ -247,7 +246,7 @@ test_expect_success \ git add F && GIT_AUTHOR_DATE="2005-05-26 23:30" \ GIT_COMMITTER_DATE="2005-05-26 23:30" git commit -m add -a && - h_TEST=$(git rev-parse --verify HEAD) + h_TEST=$(git rev-parse --verify HEAD) && echo The other day this did not work. >M && echo And then Bob told me how to fix it. >>M && echo OTHER >F && diff --git a/t/t1401-symbolic-ref.sh b/t/t1401-symbolic-ref.sh index 7fa5f5b22a..2c96551ed0 100755 --- a/t/t1401-symbolic-ref.sh +++ b/t/t1401-symbolic-ref.sh @@ -28,7 +28,7 @@ test_expect_success 'symbolic-ref refuses non-ref for HEAD' ' reset_to_sane test_expect_success 'symbolic-ref refuses bare sha1' ' - echo content >file && git add file && git commit -m one + echo content >file && git add file && git commit -m one && test_must_fail git symbolic-ref HEAD `git rev-parse HEAD` ' reset_to_sane diff --git a/t/t1402-check-ref-format.sh b/t/t1402-check-ref-format.sh index 782e75d000..1b0f82fa4c 100755 --- a/t/t1402-check-ref-format.sh +++ b/t/t1402-check-ref-format.sh @@ -32,7 +32,7 @@ test_expect_success "check-ref-format --branch @{-1}" ' T=$(git write-tree) && sha1=$(echo A | git commit-tree $T) && git update-ref refs/heads/master $sha1 && - git update-ref refs/remotes/origin/master $sha1 + git update-ref refs/remotes/origin/master $sha1 && git checkout master && git checkout origin/master && git checkout master && @@ -47,7 +47,7 @@ test_expect_success 'check-ref-format --branch from subdir' ' T=$(git write-tree) && sha1=$(echo A | git commit-tree $T) && git update-ref refs/heads/master $sha1 && - git update-ref refs/remotes/origin/master $sha1 + git update-ref refs/remotes/origin/master $sha1 && git checkout master && git checkout origin/master && git checkout master && diff --git a/t/t1410-reflog.sh b/t/t1410-reflog.sh index 25046c4208..252fc82837 100755 --- a/t/t1410-reflog.sh +++ b/t/t1410-reflog.sh @@ -186,8 +186,8 @@ test_expect_success 'delete' ' test_tick && git commit -m tiger C && - HEAD_entry_count=$(git reflog | wc -l) - master_entry_count=$(git reflog show master | wc -l) + HEAD_entry_count=$(git reflog | wc -l) && + master_entry_count=$(git reflog show master | wc -l) && test $HEAD_entry_count = 5 && test $master_entry_count = 5 && @@ -199,13 +199,13 @@ test_expect_success 'delete' ' test $HEAD_entry_count = $(git reflog | wc -l) && ! grep ox < output && - master_entry_count=$(wc -l < output) + master_entry_count=$(wc -l < output) && git reflog delete HEAD@{1} && test $(($HEAD_entry_count -1)) = $(git reflog | wc -l) && test $master_entry_count = $(git reflog show master | wc -l) && - HEAD_entry_count=$(git reflog | wc -l) + HEAD_entry_count=$(git reflog | wc -l) && git reflog delete master@{07.04.2005.15:15:00.-0700} && git reflog show master > output && diff --git a/t/t1450-fsck.sh b/t/t1450-fsck.sh index 1be415e334..bb01d5ab8f 100755 --- a/t/t1450-fsck.sh +++ b/t/t1450-fsck.sh @@ -61,7 +61,7 @@ test_expect_success 'object with bad sha1' ' sha=$(echo blob | git hash-object -w --stdin) && old=$(echo $sha | sed "s+^..+&/+") && new=$(dirname $old)/ffffffffffffffffffffffffffffffffffffff && - sha="$(dirname $new)$(basename $new)" + sha="$(dirname $new)$(basename $new)" && mv .git/objects/$old .git/objects/$new && test_when_finished "remove_object $sha" && git update-index --add --cacheinfo 100644 $sha foo && @@ -111,7 +111,7 @@ test_expect_success 'email with embedded > is not okay' ' ' test_expect_success 'tag pointing to nonexistent' ' - cat >invalid-tag <<-\EOF + cat >invalid-tag <<-\EOF && object ffffffffffffffffffffffffffffffffffffffff type commit tag invalid diff --git a/t/t1502-rev-parse-parseopt.sh b/t/t1502-rev-parse-parseopt.sh index b3195c4707..1efd7f76dd 100755 --- a/t/t1502-rev-parse-parseopt.sh +++ b/t/t1502-rev-parse-parseopt.sh @@ -40,7 +40,7 @@ extra1 line above used to cause a segfault but no longer does EOF test_expect_success 'test --parseopt help output' ' - git rev-parse --parseopt -- -h > output < optionspec + test_expect_code 129 git rev-parse --parseopt -- -h > output < optionspec && test_cmp expect output ' diff --git a/t/t1504-ceiling-dirs.sh b/t/t1504-ceiling-dirs.sh index df5ad8c686..cce87a5ab5 100755 --- a/t/t1504-ceiling-dirs.sh +++ b/t/t1504-ceiling-dirs.sh @@ -9,8 +9,9 @@ test_prefix() { } test_fail() { - test_expect_code 128 "$1: prefix" \ - "git rev-parse --show-prefix" + test_expect_success "$1: prefix" ' + test_expect_code 128 git rev-parse --show-prefix + ' } TRASH_ROOT="$PWD" diff --git a/t/t1507-rev-parse-upstream.sh b/t/t1507-rev-parse-upstream.sh index 8c8dfdaf9f..a4555510c3 100755 --- a/t/t1507-rev-parse-upstream.sh +++ b/t/t1507-rev-parse-upstream.sh @@ -85,7 +85,7 @@ test_expect_success 'merge my-side@{u} records the correct name' ' git branch -t new my-side@{u} && git merge -s ours new@{u} && git show -s --pretty=format:%s >actual && - echo "Merge remote branch ${sq}origin/side${sq}" >expect && + echo "Merge remote-tracking branch ${sq}origin/side${sq}" >expect && test_cmp expect actual ) ' diff --git a/t/t2006-checkout-index-basic.sh b/t/t2006-checkout-index-basic.sh new file mode 100755 index 0000000000..b8559838b1 --- /dev/null +++ b/t/t2006-checkout-index-basic.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +test_description='basic checkout-index tests +' + +. ./test-lib.sh + +test_expect_success 'checkout-index --gobbledegook' ' + test_expect_code 129 git checkout-index --gobbledegook 2>err && + grep "[Uu]sage" err +' + +test_expect_success 'checkout-index -h in broken repository' ' + mkdir broken && + ( + cd broken && + git init && + >.git/index && + test_expect_code 129 git checkout-index -h >usage 2>&1 + ) && + grep "[Uu]sage" broken/usage +' + +test_done diff --git a/t/t2007-checkout-symlink.sh b/t/t2007-checkout-symlink.sh index a74ee227b8..e6f59f1914 100755 --- a/t/t2007-checkout-symlink.sh +++ b/t/t2007-checkout-symlink.sh @@ -17,7 +17,7 @@ test_expect_success SYMLINKS setup ' git branch side && echo goodbye >nitfol && - git add nitfol + git add nitfol && test_tick && git commit -m "master adds file nitfol" && diff --git a/t/t2016-checkout-patch.sh b/t/t2016-checkout-patch.sh index a463b13b27..9cd0ac4ba3 100755 --- a/t/t2016-checkout-patch.sh +++ b/t/t2016-checkout-patch.sh @@ -32,7 +32,7 @@ test_expect_success PERL 'git checkout -p' ' ' test_expect_success PERL 'git checkout -p with staged changes' ' - set_state dir/foo work index + set_state dir/foo work index && (echo n; echo y) | git checkout -p && verify_saved_state bar && verify_state dir/foo index index diff --git a/t/t2017-checkout-orphan.sh b/t/t2017-checkout-orphan.sh index 2d2f63f22e..0e3b8582f2 100755 --- a/t/t2017-checkout-orphan.sh +++ b/t/t2017-checkout-orphan.sh @@ -14,7 +14,7 @@ TEST_FILE=foo test_expect_success 'Setup' ' echo "Initial" >"$TEST_FILE" && git add "$TEST_FILE" && - git commit -m "First Commit" + git commit -m "First Commit" && test_tick && echo "State 1" >>"$TEST_FILE" && git add "$TEST_FILE" && diff --git a/t/t2050-git-dir-relative.sh b/t/t2050-git-dir-relative.sh index b7131d8c08..21f4659a9d 100755 --- a/t/t2050-git-dir-relative.sh +++ b/t/t2050-git-dir-relative.sh @@ -26,7 +26,7 @@ chmod +x .git/hooks/post-commit' test_expect_success 'post-commit hook used ordinarily' ' echo initial >top && -git add top +git add top && git commit -m initial && test -r "${COMMIT_FILE}" ' @@ -45,7 +45,7 @@ test -r "${COMMIT_FILE}" rm -rf "${COMMIT_FILE}" test_expect_success 'post-commit-hook from sub dir' ' -echo changed again >top +echo changed again >top && cd subdir && git --git-dir .git --work-tree .. add ../top && git --git-dir .git --work-tree .. commit -m subcommit && diff --git a/t/t2101-update-index-reupdate.sh b/t/t2101-update-index-reupdate.sh index 76ad7c344c..c8bce8c2e4 100755 --- a/t/t2101-update-index-reupdate.sh +++ b/t/t2101-update-index-reupdate.sh @@ -51,7 +51,7 @@ test_expect_success 'update-index again' \ echo hello world >dir1/file3 && echo goodbye people >file2 && git update-index --add file2 dir1/file3 && - echo hello everybody >file2 + echo hello everybody >file2 && echo happy >dir1/file3 && git update-index --again && git ls-files -s >current && diff --git a/t/t2107-update-index-basic.sh b/t/t2107-update-index-basic.sh new file mode 100755 index 0000000000..33f8be0cd7 --- /dev/null +++ b/t/t2107-update-index-basic.sh @@ -0,0 +1,32 @@ +#!/bin/sh + +test_description='basic update-index tests + +Tests for command-line parsing and basic operation. +' + +. ./test-lib.sh + +test_expect_success 'update-index --nonsense fails' ' + test_must_fail git update-index --nonsense 2>msg && + cat msg && + test -s msg +' + +test_expect_failure 'update-index --nonsense dumps usage' ' + test_expect_code 129 git update-index --nonsense 2>err && + grep "[Uu]sage: git update-index" err +' + +test_expect_success 'update-index -h with corrupt index' ' + mkdir broken && + ( + cd broken && + git init && + >.git/index && + test_expect_code 129 git update-index -h >usage 2>&1 + ) && + grep "[Uu]sage: git update-index" broken/usage +' + +test_done diff --git a/t/t2200-add-update.sh b/t/t2200-add-update.sh index 2ad2819a34..0692427cb6 100755 --- a/t/t2200-add-update.sh +++ b/t/t2200-add-update.sh @@ -25,7 +25,7 @@ test_expect_success setup ' echo initial >dir1/sub2 && echo initial >dir2/sub3 && git add check dir1 dir2 top foo && - test_tick + test_tick && git commit -m initial && echo changed >check && diff --git a/t/t3001-ls-files-others-exclude.sh b/t/t3001-ls-files-others-exclude.sh index 6d2f2b67ee..c8fe978267 100755 --- a/t/t3001-ls-files-others-exclude.sh +++ b/t/t3001-ls-files-others-exclude.sh @@ -156,7 +156,7 @@ test_expect_success 'trailing slash in exclude allows directory match (2)' ' test_expect_success 'trailing slash in exclude forces directory match (1)' ' - >two + >two && git ls-files --others --exclude=two/ >output && grep "^two" output diff --git a/t/t3004-ls-files-basic.sh b/t/t3004-ls-files-basic.sh new file mode 100755 index 0000000000..490e052875 --- /dev/null +++ b/t/t3004-ls-files-basic.sh @@ -0,0 +1,39 @@ +#!/bin/sh + +test_description='basic ls-files tests + +This test runs git ls-files with various unusual or malformed +command-line arguments. +' + +. ./test-lib.sh + +>empty + +test_expect_success 'ls-files in empty repository' ' + git ls-files >actual && + test_cmp empty actual +' + +test_expect_success 'ls-files with nonexistent path' ' + git ls-files doesnotexist >actual && + test_cmp empty actual +' + +test_expect_success 'ls-files with nonsense option' ' + test_expect_code 129 git ls-files --nonsense 2>actual && + grep "[Uu]sage: git ls-files" actual +' + +test_expect_success 'ls-files -h in corrupt repository' ' + mkdir broken && + ( + cd broken && + git init && + >.git/index && + test_expect_code 129 git ls-files -h >usage 2>&1 + ) && + grep "[Uu]sage: git ls-files " broken/usage +' + +test_done diff --git a/t/t3030-merge-recursive.sh b/t/t3030-merge-recursive.sh index e66e550b24..34794f8a70 100755 --- a/t/t3030-merge-recursive.sh +++ b/t/t3030-merge-recursive.sh @@ -25,6 +25,10 @@ test_expect_success 'setup 1' ' git branch submod && git branch copy && git branch rename && + if test_have_prereq SYMLINKS + then + git branch rename-ln + fi && echo hello >>a && cp a d/e && @@ -255,7 +259,16 @@ test_expect_success 'setup 8' ' git mv a e && git add e && test_tick && - git commit -m "rename a->e" + git commit -m "rename a->e" && + if test_have_prereq SYMLINKS + then + git checkout rename-ln && + git mv a e && + ln -s e a && + git add a e && + test_tick && + git commit -m "rename a->e, symlink a->e" + fi ' test_expect_success 'setup 9' ' @@ -544,7 +557,7 @@ test_expect_success 'reset and bind merge' ' echo "100644 $o0 0 c" echo "100644 $o1 0 d/e" ) >expected && - test_cmp expected actual + test_cmp expected actual && git read-tree --prefix=z/ master && git ls-files -s >actual && @@ -615,4 +628,26 @@ test_expect_success 'merge-recursive copy vs. rename' ' test_cmp expected actual ' +if test_have_prereq SYMLINKS +then + test_expect_success 'merge-recursive rename vs. rename/symlink' ' + + git checkout -f rename && + git merge rename-ln && + ( git ls-tree -r HEAD ; git ls-files -s ) >actual && + ( + echo "100644 blob $o0 b" + echo "100644 blob $o0 c" + echo "100644 blob $o0 d/e" + echo "100644 blob $o0 e" + echo "100644 $o0 0 b" + echo "100644 $o0 0 c" + echo "100644 $o0 0 d/e" + echo "100644 $o0 0 e" + ) >expected && + test_cmp expected actual + ' +fi + + test_done diff --git a/t/t3050-subprojects-fetch.sh b/t/t3050-subprojects-fetch.sh index 4261e9641e..2f5f41a012 100755 --- a/t/t3050-subprojects-fetch.sh +++ b/t/t3050-subprojects-fetch.sh @@ -10,10 +10,10 @@ test_expect_success setup ' cd sub && git init && >subfile && - git add subfile + git add subfile && git commit -m "subproject commit #1" ) && - >mainfile + >mainfile && git add sub mainfile && test_tick && git commit -m "superproject commit #1" diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh index f54a533456..f308235f5d 100755 --- a/t/t3200-branch.sh +++ b/t/t3200-branch.sh @@ -26,6 +26,17 @@ test_expect_success \ ! test -f .git/refs/heads/--help ' +test_expect_success 'branch -h in broken repository' ' + mkdir broken && + ( + cd broken && + git init && + >.git/refs/heads/master && + test_expect_code 129 git branch -h >usage 2>&1 + ) && + grep "[Uu]sage" broken/usage +' + test_expect_success \ 'git branch abc should create a branch' \ 'git branch abc && test -f .git/refs/heads/abc' diff --git a/t/t3203-branch-output.sh b/t/t3203-branch-output.sh index 809d1c4ed4..6028748c6c 100755 --- a/t/t3203-branch-output.sh +++ b/t/t3203-branch-output.sh @@ -12,13 +12,13 @@ test_expect_success 'make commits' ' ' test_expect_success 'make branches' ' - git branch branch-one + git branch branch-one && git branch branch-two HEAD^ ' test_expect_success 'make remote branches' ' - git update-ref refs/remotes/origin/branch-one branch-one - git update-ref refs/remotes/origin/branch-two branch-two + git update-ref refs/remotes/origin/branch-one branch-one && + git update-ref refs/remotes/origin/branch-two branch-two && git symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/branch-one ' diff --git a/t/t3300-funny-names.sh b/t/t3300-funny-names.sh index f39a261d80..5e29a05259 100755 --- a/t/t3300-funny-names.sh +++ b/t/t3300-funny-names.sh @@ -43,8 +43,8 @@ test_expect_success TABS_IN_FILENAMES 'git ls-files no-funny' \ test_cmp expected current' test_expect_success TABS_IN_FILENAMES 'setup expect' ' -t0=`git write-tree` -echo "$t0" >t0 +t0=`git write-tree` && +echo "$t0" >t0 && cat > expected <<\EOF just space @@ -69,8 +69,8 @@ test_expect_success TABS_IN_FILENAMES 'git ls-files -z with-funny' \ test_cmp expected current' test_expect_success TABS_IN_FILENAMES 'setup expect' ' -t1=`git write-tree` -echo "$t1" >t1 +t1=`git write-tree` && +echo "$t1" >t1 && cat > expected <<\EOF just space diff --git a/t/t3301-notes.sh b/t/t3301-notes.sh index a2b79a0430..dc2e04a016 100755 --- a/t/t3301-notes.sh +++ b/t/t3301-notes.sh @@ -52,7 +52,7 @@ test_expect_success 'refusing to edit notes in refs/remotes/' ' # 1 indicates caught gracefully by die, 128 means git-show barked test_expect_success 'handle empty notes gracefully' ' - git notes show ; test 1 = $? + test_expect_code 1 git notes show ' test_expect_success 'show non-existent notes entry with %N' ' @@ -627,16 +627,16 @@ test_expect_success '--show-notes=ref accumulates' ' test_expect_success 'Allow notes on non-commits (trees, blobs, tags)' ' git config core.notesRef refs/notes/other && - echo "Note on a tree" > expect + echo "Note on a tree" > expect && git notes add -m "Note on a tree" HEAD: && git notes show HEAD: > actual && test_cmp expect actual && - echo "Note on a blob" > expect + echo "Note on a blob" > expect && filename=$(git ls-tree --name-only HEAD | head -n1) && git notes add -m "Note on a blob" HEAD:$filename && git notes show HEAD:$filename > actual && test_cmp expect actual && - echo "Note on a tag" > expect + echo "Note on a tag" > expect && git tag -a -m "This is an annotated tag" foobar HEAD^ && git notes add -m "Note on a tag" foobar && git notes show foobar > actual && @@ -962,6 +962,7 @@ Date: Thu Apr 7 15:27:13 2005 -0700 Notes (other): a fresh note +$whitespace another fresh note EOF @@ -983,8 +984,11 @@ Date: Thu Apr 7 15:27:13 2005 -0700 Notes (other): a fresh note +$whitespace another fresh note +$whitespace append 1 +$whitespace append 2 EOF @@ -1061,4 +1065,23 @@ test_expect_success 'git notes copy diagnoses too many or too few parameters' ' test_must_fail git notes copy one two three ' +test_expect_success 'git notes get-ref (no overrides)' ' + git config --unset core.notesRef && + unset GIT_NOTES_REF && + test "$(git notes get-ref)" = "refs/notes/commits" +' + +test_expect_success 'git notes get-ref (core.notesRef)' ' + git config core.notesRef refs/notes/foo && + test "$(git notes get-ref)" = "refs/notes/foo" +' + +test_expect_success 'git notes get-ref (GIT_NOTES_REF)' ' + test "$(GIT_NOTES_REF=refs/notes/bar git notes get-ref)" = "refs/notes/bar" +' + +test_expect_success 'git notes get-ref (--ref)' ' + test "$(GIT_NOTES_REF=refs/notes/bar git notes --ref=baz get-ref)" = "refs/notes/baz" +' + test_done diff --git a/t/t3303-notes-subtrees.sh b/t/t3303-notes-subtrees.sh index 75ec18778e..704aee81ef 100755 --- a/t/t3303-notes-subtrees.sh +++ b/t/t3303-notes-subtrees.sh @@ -168,15 +168,16 @@ INPUT_END } verify_concatenated_notes () { - git log | grep "^ " > output && - i=$number_of_commits && - while [ $i -gt 0 ]; do - echo " commit #$i" && - echo " first note for commit #$i" && - echo " second note for commit #$i" && - i=$(($i-1)); - done > expect && - test_cmp expect output + git log | grep "^ " > output && + i=$number_of_commits && + while [ $i -gt 0 ]; do + echo " commit #$i" && + echo " first note for commit #$i" && + echo " " && + echo " second note for commit #$i" && + i=$(($i-1)); + done > expect && + test_cmp expect output } test_expect_success 'test notes in no fanout concatenated with 2/38-fanout' 'test_concatenated_notes "s|^..|&/|" ""' diff --git a/t/t3307-notes-man.sh b/t/t3307-notes-man.sh index 3269f2eebd..2ea3be6546 100755 --- a/t/t3307-notes-man.sh +++ b/t/t3307-notes-man.sh @@ -26,7 +26,7 @@ test_expect_success 'example 1: notes to add an Acked-by line' ' ' test_expect_success 'example 2: binary notes' ' - cp "$TEST_DIRECTORY"/test4012.png . + cp "$TEST_DIRECTORY"/test4012.png . && git checkout B && blob=$(git hash-object -w test4012.png) && git notes --ref=logo add -C "$blob" && diff --git a/t/t3308-notes-merge.sh b/t/t3308-notes-merge.sh new file mode 100755 index 0000000000..24d82b49bb --- /dev/null +++ b/t/t3308-notes-merge.sh @@ -0,0 +1,368 @@ +#!/bin/sh +# +# Copyright (c) 2010 Johan Herland +# + +test_description='Test merging of notes trees' + +. ./test-lib.sh + +test_expect_success setup ' + test_commit 1st && + test_commit 2nd && + test_commit 3rd && + test_commit 4th && + test_commit 5th && + # Create notes on 4 first commits + git config core.notesRef refs/notes/x && + git notes add -m "Notes on 1st commit" 1st && + git notes add -m "Notes on 2nd commit" 2nd && + git notes add -m "Notes on 3rd commit" 3rd && + git notes add -m "Notes on 4th commit" 4th +' + +commit_sha1=$(git rev-parse 1st^{commit}) +commit_sha2=$(git rev-parse 2nd^{commit}) +commit_sha3=$(git rev-parse 3rd^{commit}) +commit_sha4=$(git rev-parse 4th^{commit}) +commit_sha5=$(git rev-parse 5th^{commit}) + +verify_notes () { + notes_ref="$1" + git -c core.notesRef="refs/notes/$notes_ref" notes | + sort >"output_notes_$notes_ref" && + test_cmp "expect_notes_$notes_ref" "output_notes_$notes_ref" && + git -c core.notesRef="refs/notes/$notes_ref" log --format="%H %s%n%N" \ + >"output_log_$notes_ref" && + test_cmp "expect_log_$notes_ref" "output_log_$notes_ref" +} + +cat <<EOF | sort >expect_notes_x +5e93d24084d32e1cb61f7070505b9d2530cca987 $commit_sha4 +8366731eeee53787d2bdf8fc1eff7d94757e8da0 $commit_sha3 +eede89064cd42441590d6afec6c37b321ada3389 $commit_sha2 +daa55ffad6cb99bf64226532147ffcaf5ce8bdd1 $commit_sha1 +EOF + +cat >expect_log_x <<EOF +$commit_sha5 5th + +$commit_sha4 4th +Notes on 4th commit + +$commit_sha3 3rd +Notes on 3rd commit + +$commit_sha2 2nd +Notes on 2nd commit + +$commit_sha1 1st +Notes on 1st commit + +EOF + +test_expect_success 'verify initial notes (x)' ' + verify_notes x +' + +cp expect_notes_x expect_notes_y +cp expect_log_x expect_log_y + +test_expect_success 'fail to merge empty notes ref into empty notes ref (z => y)' ' + test_must_fail git -c "core.notesRef=refs/notes/y" notes merge z +' + +test_expect_success 'fail to merge into various non-notes refs' ' + test_must_fail git -c "core.notesRef=refs/notes" notes merge x && + test_must_fail git -c "core.notesRef=refs/notes/" notes merge x && + mkdir -p .git/refs/notes/dir && + test_must_fail git -c "core.notesRef=refs/notes/dir" notes merge x && + test_must_fail git -c "core.notesRef=refs/notes/dir/" notes merge x && + test_must_fail git -c "core.notesRef=refs/heads/master" notes merge x && + test_must_fail git -c "core.notesRef=refs/notes/y:" notes merge x && + test_must_fail git -c "core.notesRef=refs/notes/y:foo" notes merge x && + test_must_fail git -c "core.notesRef=refs/notes/foo^{bar" notes merge x +' + +test_expect_success 'fail to merge various non-note-trees' ' + git config core.notesRef refs/notes/y && + test_must_fail git notes merge refs/notes && + test_must_fail git notes merge refs/notes/ && + test_must_fail git notes merge refs/notes/dir && + test_must_fail git notes merge refs/notes/dir/ && + test_must_fail git notes merge refs/heads/master && + test_must_fail git notes merge x: && + test_must_fail git notes merge x:foo && + test_must_fail git notes merge foo^{bar +' + +test_expect_success 'merge notes into empty notes ref (x => y)' ' + git config core.notesRef refs/notes/y && + git notes merge x && + verify_notes y && + # x and y should point to the same notes commit + test "$(git rev-parse refs/notes/x)" = "$(git rev-parse refs/notes/y)" +' + +test_expect_success 'merge empty notes ref (z => y)' ' + git notes merge z && + # y should not change (still == x) + test "$(git rev-parse refs/notes/x)" = "$(git rev-parse refs/notes/y)" +' + +test_expect_success 'change notes on other notes ref (y)' ' + # Not touching notes to 1st commit + git notes remove 2nd && + git notes append -m "More notes on 3rd commit" 3rd && + git notes add -f -m "New notes on 4th commit" 4th && + git notes add -m "Notes on 5th commit" 5th +' + +test_expect_success 'merge previous notes commit (y^ => y) => No-op' ' + pre_state="$(git rev-parse refs/notes/y)" && + git notes merge y^ && + # y should not move + test "$pre_state" = "$(git rev-parse refs/notes/y)" +' + +cat <<EOF | sort >expect_notes_y +0f2efbd00262f2fd41dfae33df8765618eeacd99 $commit_sha5 +dec2502dac3ea161543f71930044deff93fa945c $commit_sha4 +4069cdb399fd45463ec6eef8e051a16a03592d91 $commit_sha3 +daa55ffad6cb99bf64226532147ffcaf5ce8bdd1 $commit_sha1 +EOF + +cat >expect_log_y <<EOF +$commit_sha5 5th +Notes on 5th commit + +$commit_sha4 4th +New notes on 4th commit + +$commit_sha3 3rd +Notes on 3rd commit + +More notes on 3rd commit + +$commit_sha2 2nd + +$commit_sha1 1st +Notes on 1st commit + +EOF + +test_expect_success 'verify changed notes on other notes ref (y)' ' + verify_notes y +' + +test_expect_success 'verify unchanged notes on original notes ref (x)' ' + verify_notes x +' + +test_expect_success 'merge original notes (x) into changed notes (y) => No-op' ' + git notes merge -vvv x && + verify_notes y && + verify_notes x +' + +cp expect_notes_y expect_notes_x +cp expect_log_y expect_log_x + +test_expect_success 'merge changed (y) into original (x) => Fast-forward' ' + git config core.notesRef refs/notes/x && + git notes merge y && + verify_notes x && + verify_notes y && + # x and y should point to same the notes commit + test "$(git rev-parse refs/notes/x)" = "$(git rev-parse refs/notes/y)" +' + +test_expect_success 'merge empty notes ref (z => y)' ' + # Prepare empty (but valid) notes ref (z) + git config core.notesRef refs/notes/z && + git notes add -m "foo" && + git notes remove && + git notes >output_notes_z && + test_cmp /dev/null output_notes_z && + # Do the merge (z => y) + git config core.notesRef refs/notes/y && + git notes merge z && + verify_notes y && + # y should no longer point to the same notes commit as x + test "$(git rev-parse refs/notes/x)" != "$(git rev-parse refs/notes/y)" +' + +cat <<EOF | sort >expect_notes_y +0f2efbd00262f2fd41dfae33df8765618eeacd99 $commit_sha5 +dec2502dac3ea161543f71930044deff93fa945c $commit_sha4 +4069cdb399fd45463ec6eef8e051a16a03592d91 $commit_sha3 +d000d30e6ddcfce3a8122c403226a2ce2fd04d9d $commit_sha2 +43add6bd0c8c0bc871ac7991e0f5573cfba27804 $commit_sha1 +EOF + +cat >expect_log_y <<EOF +$commit_sha5 5th +Notes on 5th commit + +$commit_sha4 4th +New notes on 4th commit + +$commit_sha3 3rd +Notes on 3rd commit + +More notes on 3rd commit + +$commit_sha2 2nd +New notes on 2nd commit + +$commit_sha1 1st +Notes on 1st commit + +More notes on 1st commit + +EOF + +test_expect_success 'change notes on other notes ref (y)' ' + # Append to 1st commit notes + git notes append -m "More notes on 1st commit" 1st && + # Add new notes to 2nd commit + git notes add -m "New notes on 2nd commit" 2nd && + verify_notes y +' + +cat <<EOF | sort >expect_notes_x +0f2efbd00262f2fd41dfae33df8765618eeacd99 $commit_sha5 +1f257a3a90328557c452f0817d6cc50c89d315d4 $commit_sha4 +daa55ffad6cb99bf64226532147ffcaf5ce8bdd1 $commit_sha1 +EOF + +cat >expect_log_x <<EOF +$commit_sha5 5th +Notes on 5th commit + +$commit_sha4 4th +New notes on 4th commit + +More notes on 4th commit + +$commit_sha3 3rd + +$commit_sha2 2nd + +$commit_sha1 1st +Notes on 1st commit + +EOF + +test_expect_success 'change notes on notes ref (x)' ' + git config core.notesRef refs/notes/x && + git notes remove 3rd && + git notes append -m "More notes on 4th commit" 4th && + verify_notes x +' + +cat <<EOF | sort >expect_notes_x +0f2efbd00262f2fd41dfae33df8765618eeacd99 $commit_sha5 +1f257a3a90328557c452f0817d6cc50c89d315d4 $commit_sha4 +d000d30e6ddcfce3a8122c403226a2ce2fd04d9d $commit_sha2 +43add6bd0c8c0bc871ac7991e0f5573cfba27804 $commit_sha1 +EOF + +cat >expect_log_x <<EOF +$commit_sha5 5th +Notes on 5th commit + +$commit_sha4 4th +New notes on 4th commit + +More notes on 4th commit + +$commit_sha3 3rd + +$commit_sha2 2nd +New notes on 2nd commit + +$commit_sha1 1st +Notes on 1st commit + +More notes on 1st commit + +EOF + +test_expect_success 'merge y into x => Non-conflicting 3-way merge' ' + git notes merge y && + verify_notes x && + verify_notes y +' + +cat <<EOF | sort >expect_notes_w +05a4927951bcef347f51486575b878b2b60137f2 $commit_sha3 +d000d30e6ddcfce3a8122c403226a2ce2fd04d9d $commit_sha2 +EOF + +cat >expect_log_w <<EOF +$commit_sha5 5th + +$commit_sha4 4th + +$commit_sha3 3rd +New notes on 3rd commit + +$commit_sha2 2nd +New notes on 2nd commit + +$commit_sha1 1st + +EOF + +test_expect_success 'create notes on new, separate notes ref (w)' ' + git config core.notesRef refs/notes/w && + # Add same note as refs/notes/y on 2nd commit + git notes add -m "New notes on 2nd commit" 2nd && + # Add new note on 3rd commit (non-conflicting) + git notes add -m "New notes on 3rd commit" 3rd && + # Verify state of notes on new, separate notes ref (w) + verify_notes w +' + +cat <<EOF | sort >expect_notes_x +0f2efbd00262f2fd41dfae33df8765618eeacd99 $commit_sha5 +1f257a3a90328557c452f0817d6cc50c89d315d4 $commit_sha4 +05a4927951bcef347f51486575b878b2b60137f2 $commit_sha3 +d000d30e6ddcfce3a8122c403226a2ce2fd04d9d $commit_sha2 +43add6bd0c8c0bc871ac7991e0f5573cfba27804 $commit_sha1 +EOF + +cat >expect_log_x <<EOF +$commit_sha5 5th +Notes on 5th commit + +$commit_sha4 4th +New notes on 4th commit + +More notes on 4th commit + +$commit_sha3 3rd +New notes on 3rd commit + +$commit_sha2 2nd +New notes on 2nd commit + +$commit_sha1 1st +Notes on 1st commit + +More notes on 1st commit + +EOF + +test_expect_success 'merge w into x => Non-conflicting history-less merge' ' + git config core.notesRef refs/notes/x && + git notes merge w && + # Verify new state of notes on other notes ref (x) + verify_notes x && + # Also verify that nothing changed on other notes refs (y and w) + verify_notes y && + verify_notes w +' + +test_done diff --git a/t/t3309-notes-merge-auto-resolve.sh b/t/t3309-notes-merge-auto-resolve.sh new file mode 100755 index 0000000000..461fd84755 --- /dev/null +++ b/t/t3309-notes-merge-auto-resolve.sh @@ -0,0 +1,647 @@ +#!/bin/sh +# +# Copyright (c) 2010 Johan Herland +# + +test_description='Test notes merging with auto-resolving strategies' + +. ./test-lib.sh + +# Set up a notes merge scenario with all kinds of potential conflicts +test_expect_success 'setup commits' ' + test_commit 1st && + test_commit 2nd && + test_commit 3rd && + test_commit 4th && + test_commit 5th && + test_commit 6th && + test_commit 7th && + test_commit 8th && + test_commit 9th && + test_commit 10th && + test_commit 11th && + test_commit 12th && + test_commit 13th && + test_commit 14th && + test_commit 15th +' + +commit_sha1=$(git rev-parse 1st^{commit}) +commit_sha2=$(git rev-parse 2nd^{commit}) +commit_sha3=$(git rev-parse 3rd^{commit}) +commit_sha4=$(git rev-parse 4th^{commit}) +commit_sha5=$(git rev-parse 5th^{commit}) +commit_sha6=$(git rev-parse 6th^{commit}) +commit_sha7=$(git rev-parse 7th^{commit}) +commit_sha8=$(git rev-parse 8th^{commit}) +commit_sha9=$(git rev-parse 9th^{commit}) +commit_sha10=$(git rev-parse 10th^{commit}) +commit_sha11=$(git rev-parse 11th^{commit}) +commit_sha12=$(git rev-parse 12th^{commit}) +commit_sha13=$(git rev-parse 13th^{commit}) +commit_sha14=$(git rev-parse 14th^{commit}) +commit_sha15=$(git rev-parse 15th^{commit}) + +verify_notes () { + notes_ref="$1" + suffix="$2" + git -c core.notesRef="refs/notes/$notes_ref" notes | + sort >"output_notes_$suffix" && + test_cmp "expect_notes_$suffix" "output_notes_$suffix" && + git -c core.notesRef="refs/notes/$notes_ref" log --format="%H %s%n%N" \ + >"output_log_$suffix" && + test_cmp "expect_log_$suffix" "output_log_$suffix" +} + +test_expect_success 'setup merge base (x)' ' + git config core.notesRef refs/notes/x && + git notes add -m "x notes on 6th commit" 6th && + git notes add -m "x notes on 7th commit" 7th && + git notes add -m "x notes on 8th commit" 8th && + git notes add -m "x notes on 9th commit" 9th && + git notes add -m "x notes on 10th commit" 10th && + git notes add -m "x notes on 11th commit" 11th && + git notes add -m "x notes on 12th commit" 12th && + git notes add -m "x notes on 13th commit" 13th && + git notes add -m "x notes on 14th commit" 14th && + git notes add -m "x notes on 15th commit" 15th +' + +cat <<EOF | sort >expect_notes_x +457a85d6c814ea208550f15fcc48f804ac8dc023 $commit_sha15 +b0c95b954301d69da2bc3723f4cb1680d355937c $commit_sha14 +5d30216a129eeffa97d9694ffe8c74317a560315 $commit_sha13 +dd161bc149470fd890dd4ab52a4cbd79bbd18c36 $commit_sha12 +7abbc45126d680336fb24294f013a7cdfa3ed545 $commit_sha11 +b8d03e173f67f6505a76f6e00cf93440200dd9be $commit_sha10 +20c613c835011c48a5abe29170a2402ca6354910 $commit_sha9 +a3daf8a1e4e5dc3409a303ad8481d57bfea7f5d6 $commit_sha8 +897003322b53bc6ca098e9324ee508362347e734 $commit_sha7 +11d97fdebfa5ceee540a3da07bce6fa0222bc082 $commit_sha6 +EOF + +cat >expect_log_x <<EOF +$commit_sha15 15th +x notes on 15th commit + +$commit_sha14 14th +x notes on 14th commit + +$commit_sha13 13th +x notes on 13th commit + +$commit_sha12 12th +x notes on 12th commit + +$commit_sha11 11th +x notes on 11th commit + +$commit_sha10 10th +x notes on 10th commit + +$commit_sha9 9th +x notes on 9th commit + +$commit_sha8 8th +x notes on 8th commit + +$commit_sha7 7th +x notes on 7th commit + +$commit_sha6 6th +x notes on 6th commit + +$commit_sha5 5th + +$commit_sha4 4th + +$commit_sha3 3rd + +$commit_sha2 2nd + +$commit_sha1 1st + +EOF + +test_expect_success 'verify state of merge base (x)' 'verify_notes x x' + +test_expect_success 'setup local branch (y)' ' + git update-ref refs/notes/y refs/notes/x && + git config core.notesRef refs/notes/y && + git notes add -f -m "y notes on 3rd commit" 3rd && + git notes add -f -m "y notes on 4th commit" 4th && + git notes add -f -m "y notes on 5th commit" 5th && + git notes remove 6th && + git notes remove 7th && + git notes remove 8th && + git notes add -f -m "y notes on 12th commit" 12th && + git notes add -f -m "y notes on 13th commit" 13th && + git notes add -f -m "y notes on 14th commit" 14th && + git notes add -f -m "y notes on 15th commit" 15th +' + +cat <<EOF | sort >expect_notes_y +68b8630d25516028bed862719855b3d6768d7833 $commit_sha15 +5de7ea7ad4f47e7ff91989fb82234634730f75df $commit_sha14 +3a631fdb6f41b05b55d8f4baf20728ba8f6fccbc $commit_sha13 +a66055fa82f7a03fe0c02a6aba3287a85abf7c62 $commit_sha12 +7abbc45126d680336fb24294f013a7cdfa3ed545 $commit_sha11 +b8d03e173f67f6505a76f6e00cf93440200dd9be $commit_sha10 +20c613c835011c48a5abe29170a2402ca6354910 $commit_sha9 +154508c7a0bcad82b6fe4b472bc4c26b3bf0825b $commit_sha5 +e2bfd06a37dd2031684a59a6e2b033e212239c78 $commit_sha4 +5772f42408c0dd6f097a7ca2d24de0e78d1c46b1 $commit_sha3 +EOF + +cat >expect_log_y <<EOF +$commit_sha15 15th +y notes on 15th commit + +$commit_sha14 14th +y notes on 14th commit + +$commit_sha13 13th +y notes on 13th commit + +$commit_sha12 12th +y notes on 12th commit + +$commit_sha11 11th +x notes on 11th commit + +$commit_sha10 10th +x notes on 10th commit + +$commit_sha9 9th +x notes on 9th commit + +$commit_sha8 8th + +$commit_sha7 7th + +$commit_sha6 6th + +$commit_sha5 5th +y notes on 5th commit + +$commit_sha4 4th +y notes on 4th commit + +$commit_sha3 3rd +y notes on 3rd commit + +$commit_sha2 2nd + +$commit_sha1 1st + +EOF + +test_expect_success 'verify state of local branch (y)' 'verify_notes y y' + +test_expect_success 'setup remote branch (z)' ' + git update-ref refs/notes/z refs/notes/x && + git config core.notesRef refs/notes/z && + git notes add -f -m "z notes on 2nd commit" 2nd && + git notes add -f -m "y notes on 4th commit" 4th && + git notes add -f -m "z notes on 5th commit" 5th && + git notes remove 6th && + git notes add -f -m "z notes on 8th commit" 8th && + git notes remove 9th && + git notes add -f -m "z notes on 11th commit" 11th && + git notes remove 12th && + git notes add -f -m "y notes on 14th commit" 14th && + git notes add -f -m "z notes on 15th commit" 15th +' + +cat <<EOF | sort >expect_notes_z +9b4b2c61f0615412da3c10f98ff85b57c04ec765 $commit_sha15 +5de7ea7ad4f47e7ff91989fb82234634730f75df $commit_sha14 +5d30216a129eeffa97d9694ffe8c74317a560315 $commit_sha13 +7e3c53503a3db8dd996cb62e37c66e070b44b54d $commit_sha11 +b8d03e173f67f6505a76f6e00cf93440200dd9be $commit_sha10 +851e1638784a884c7dd26c5d41f3340f6387413a $commit_sha8 +897003322b53bc6ca098e9324ee508362347e734 $commit_sha7 +99fc34adfc400b95c67b013115e37e31aa9a6d23 $commit_sha5 +e2bfd06a37dd2031684a59a6e2b033e212239c78 $commit_sha4 +283b48219aee9a4105f6cab337e789065c82c2b9 $commit_sha2 +EOF + +cat >expect_log_z <<EOF +$commit_sha15 15th +z notes on 15th commit + +$commit_sha14 14th +y notes on 14th commit + +$commit_sha13 13th +x notes on 13th commit + +$commit_sha12 12th + +$commit_sha11 11th +z notes on 11th commit + +$commit_sha10 10th +x notes on 10th commit + +$commit_sha9 9th + +$commit_sha8 8th +z notes on 8th commit + +$commit_sha7 7th +x notes on 7th commit + +$commit_sha6 6th + +$commit_sha5 5th +z notes on 5th commit + +$commit_sha4 4th +y notes on 4th commit + +$commit_sha3 3rd + +$commit_sha2 2nd +z notes on 2nd commit + +$commit_sha1 1st + +EOF + +test_expect_success 'verify state of remote branch (z)' 'verify_notes z z' + +# At this point, before merging z into y, we have the following status: +# +# commit | base/x | local/y | remote/z | diff from x to y/z | result +# -------|---------|---------|----------|----------------------------|------- +# 1st | [none] | [none] | [none] | unchanged / unchanged | [none] +# 2nd | [none] | [none] | 283b482 | unchanged / added | 283b482 +# 3rd | [none] | 5772f42 | [none] | added / unchanged | 5772f42 +# 4th | [none] | e2bfd06 | e2bfd06 | added / added (same) | e2bfd06 +# 5th | [none] | 154508c | 99fc34a | added / added (diff) | ??? +# 6th | 11d97fd | [none] | [none] | removed / removed | [none] +# 7th | 8970033 | [none] | 8970033 | removed / unchanged | [none] +# 8th | a3daf8a | [none] | 851e163 | removed / changed | ??? +# 9th | 20c613c | 20c613c | [none] | unchanged / removed | [none] +# 10th | b8d03e1 | b8d03e1 | b8d03e1 | unchanged / unchanged | b8d03e1 +# 11th | 7abbc45 | 7abbc45 | 7e3c535 | unchanged / changed | 7e3c535 +# 12th | dd161bc | a66055f | [none] | changed / removed | ??? +# 13th | 5d30216 | 3a631fd | 5d30216 | changed / unchanged | 3a631fd +# 14th | b0c95b9 | 5de7ea7 | 5de7ea7 | changed / changed (same) | 5de7ea7 +# 15th | 457a85d | 68b8630 | 9b4b2c6 | changed / changed (diff) | ??? + +test_expect_success 'merge z into y with invalid strategy => Fail/No changes' ' + git config core.notesRef refs/notes/y && + test_must_fail git notes merge --strategy=foo z && + # Verify no changes (y) + verify_notes y y +' + +cat <<EOF | sort >expect_notes_ours +68b8630d25516028bed862719855b3d6768d7833 $commit_sha15 +5de7ea7ad4f47e7ff91989fb82234634730f75df $commit_sha14 +3a631fdb6f41b05b55d8f4baf20728ba8f6fccbc $commit_sha13 +a66055fa82f7a03fe0c02a6aba3287a85abf7c62 $commit_sha12 +7e3c53503a3db8dd996cb62e37c66e070b44b54d $commit_sha11 +b8d03e173f67f6505a76f6e00cf93440200dd9be $commit_sha10 +154508c7a0bcad82b6fe4b472bc4c26b3bf0825b $commit_sha5 +e2bfd06a37dd2031684a59a6e2b033e212239c78 $commit_sha4 +5772f42408c0dd6f097a7ca2d24de0e78d1c46b1 $commit_sha3 +283b48219aee9a4105f6cab337e789065c82c2b9 $commit_sha2 +EOF + +cat >expect_log_ours <<EOF +$commit_sha15 15th +y notes on 15th commit + +$commit_sha14 14th +y notes on 14th commit + +$commit_sha13 13th +y notes on 13th commit + +$commit_sha12 12th +y notes on 12th commit + +$commit_sha11 11th +z notes on 11th commit + +$commit_sha10 10th +x notes on 10th commit + +$commit_sha9 9th + +$commit_sha8 8th + +$commit_sha7 7th + +$commit_sha6 6th + +$commit_sha5 5th +y notes on 5th commit + +$commit_sha4 4th +y notes on 4th commit + +$commit_sha3 3rd +y notes on 3rd commit + +$commit_sha2 2nd +z notes on 2nd commit + +$commit_sha1 1st + +EOF + +test_expect_success 'merge z into y with "ours" strategy => Non-conflicting 3-way merge' ' + git notes merge --strategy=ours z && + verify_notes y ours +' + +test_expect_success 'reset to pre-merge state (y)' ' + git update-ref refs/notes/y refs/notes/y^1 && + # Verify pre-merge state + verify_notes y y +' + +cat <<EOF | sort >expect_notes_theirs +9b4b2c61f0615412da3c10f98ff85b57c04ec765 $commit_sha15 +5de7ea7ad4f47e7ff91989fb82234634730f75df $commit_sha14 +3a631fdb6f41b05b55d8f4baf20728ba8f6fccbc $commit_sha13 +7e3c53503a3db8dd996cb62e37c66e070b44b54d $commit_sha11 +b8d03e173f67f6505a76f6e00cf93440200dd9be $commit_sha10 +851e1638784a884c7dd26c5d41f3340f6387413a $commit_sha8 +99fc34adfc400b95c67b013115e37e31aa9a6d23 $commit_sha5 +e2bfd06a37dd2031684a59a6e2b033e212239c78 $commit_sha4 +5772f42408c0dd6f097a7ca2d24de0e78d1c46b1 $commit_sha3 +283b48219aee9a4105f6cab337e789065c82c2b9 $commit_sha2 +EOF + +cat >expect_log_theirs <<EOF +$commit_sha15 15th +z notes on 15th commit + +$commit_sha14 14th +y notes on 14th commit + +$commit_sha13 13th +y notes on 13th commit + +$commit_sha12 12th + +$commit_sha11 11th +z notes on 11th commit + +$commit_sha10 10th +x notes on 10th commit + +$commit_sha9 9th + +$commit_sha8 8th +z notes on 8th commit + +$commit_sha7 7th + +$commit_sha6 6th + +$commit_sha5 5th +z notes on 5th commit + +$commit_sha4 4th +y notes on 4th commit + +$commit_sha3 3rd +y notes on 3rd commit + +$commit_sha2 2nd +z notes on 2nd commit + +$commit_sha1 1st + +EOF + +test_expect_success 'merge z into y with "theirs" strategy => Non-conflicting 3-way merge' ' + git notes merge --strategy=theirs z && + verify_notes y theirs +' + +test_expect_success 'reset to pre-merge state (y)' ' + git update-ref refs/notes/y refs/notes/y^1 && + # Verify pre-merge state + verify_notes y y +' + +cat <<EOF | sort >expect_notes_union +7c4e546efd0fe939f876beb262ece02797880b54 $commit_sha15 +5de7ea7ad4f47e7ff91989fb82234634730f75df $commit_sha14 +3a631fdb6f41b05b55d8f4baf20728ba8f6fccbc $commit_sha13 +a66055fa82f7a03fe0c02a6aba3287a85abf7c62 $commit_sha12 +7e3c53503a3db8dd996cb62e37c66e070b44b54d $commit_sha11 +b8d03e173f67f6505a76f6e00cf93440200dd9be $commit_sha10 +851e1638784a884c7dd26c5d41f3340f6387413a $commit_sha8 +6c841cc36ea496027290967ca96bd2bef54dbb47 $commit_sha5 +e2bfd06a37dd2031684a59a6e2b033e212239c78 $commit_sha4 +5772f42408c0dd6f097a7ca2d24de0e78d1c46b1 $commit_sha3 +283b48219aee9a4105f6cab337e789065c82c2b9 $commit_sha2 +EOF + +cat >expect_log_union <<EOF +$commit_sha15 15th +y notes on 15th commit + +z notes on 15th commit + +$commit_sha14 14th +y notes on 14th commit + +$commit_sha13 13th +y notes on 13th commit + +$commit_sha12 12th +y notes on 12th commit + +$commit_sha11 11th +z notes on 11th commit + +$commit_sha10 10th +x notes on 10th commit + +$commit_sha9 9th + +$commit_sha8 8th +z notes on 8th commit + +$commit_sha7 7th + +$commit_sha6 6th + +$commit_sha5 5th +y notes on 5th commit + +z notes on 5th commit + +$commit_sha4 4th +y notes on 4th commit + +$commit_sha3 3rd +y notes on 3rd commit + +$commit_sha2 2nd +z notes on 2nd commit + +$commit_sha1 1st + +EOF + +test_expect_success 'merge z into y with "union" strategy => Non-conflicting 3-way merge' ' + git notes merge --strategy=union z && + verify_notes y union +' + +test_expect_success 'reset to pre-merge state (y)' ' + git update-ref refs/notes/y refs/notes/y^1 && + # Verify pre-merge state + verify_notes y y +' + +cat <<EOF | sort >expect_notes_union2 +d682107b8bf7a7aea1e537a8d5cb6a12b60135f1 $commit_sha15 +5de7ea7ad4f47e7ff91989fb82234634730f75df $commit_sha14 +3a631fdb6f41b05b55d8f4baf20728ba8f6fccbc $commit_sha13 +a66055fa82f7a03fe0c02a6aba3287a85abf7c62 $commit_sha12 +7e3c53503a3db8dd996cb62e37c66e070b44b54d $commit_sha11 +b8d03e173f67f6505a76f6e00cf93440200dd9be $commit_sha10 +851e1638784a884c7dd26c5d41f3340f6387413a $commit_sha8 +357b6ca14c7afd59b7f8b8aaaa6b8b723771135b $commit_sha5 +e2bfd06a37dd2031684a59a6e2b033e212239c78 $commit_sha4 +5772f42408c0dd6f097a7ca2d24de0e78d1c46b1 $commit_sha3 +283b48219aee9a4105f6cab337e789065c82c2b9 $commit_sha2 +EOF + +cat >expect_log_union2 <<EOF +$commit_sha15 15th +z notes on 15th commit + +y notes on 15th commit + +$commit_sha14 14th +y notes on 14th commit + +$commit_sha13 13th +y notes on 13th commit + +$commit_sha12 12th +y notes on 12th commit + +$commit_sha11 11th +z notes on 11th commit + +$commit_sha10 10th +x notes on 10th commit + +$commit_sha9 9th + +$commit_sha8 8th +z notes on 8th commit + +$commit_sha7 7th + +$commit_sha6 6th + +$commit_sha5 5th +z notes on 5th commit + +y notes on 5th commit + +$commit_sha4 4th +y notes on 4th commit + +$commit_sha3 3rd +y notes on 3rd commit + +$commit_sha2 2nd +z notes on 2nd commit + +$commit_sha1 1st + +EOF + +test_expect_success 'merge y into z with "union" strategy => Non-conflicting 3-way merge' ' + git config core.notesRef refs/notes/z && + git notes merge --strategy=union y && + verify_notes z union2 +' + +test_expect_success 'reset to pre-merge state (z)' ' + git update-ref refs/notes/z refs/notes/z^1 && + # Verify pre-merge state + verify_notes z z +' + +cat <<EOF | sort >expect_notes_cat_sort_uniq +6be90240b5f54594203e25d9f2f64b7567175aee $commit_sha15 +5de7ea7ad4f47e7ff91989fb82234634730f75df $commit_sha14 +3a631fdb6f41b05b55d8f4baf20728ba8f6fccbc $commit_sha13 +a66055fa82f7a03fe0c02a6aba3287a85abf7c62 $commit_sha12 +7e3c53503a3db8dd996cb62e37c66e070b44b54d $commit_sha11 +b8d03e173f67f6505a76f6e00cf93440200dd9be $commit_sha10 +851e1638784a884c7dd26c5d41f3340f6387413a $commit_sha8 +660311d7f78dc53db12ac373a43fca7465381a7e $commit_sha5 +e2bfd06a37dd2031684a59a6e2b033e212239c78 $commit_sha4 +5772f42408c0dd6f097a7ca2d24de0e78d1c46b1 $commit_sha3 +283b48219aee9a4105f6cab337e789065c82c2b9 $commit_sha2 +EOF + +cat >expect_log_cat_sort_uniq <<EOF +$commit_sha15 15th +y notes on 15th commit +z notes on 15th commit + +$commit_sha14 14th +y notes on 14th commit + +$commit_sha13 13th +y notes on 13th commit + +$commit_sha12 12th +y notes on 12th commit + +$commit_sha11 11th +z notes on 11th commit + +$commit_sha10 10th +x notes on 10th commit + +$commit_sha9 9th + +$commit_sha8 8th +z notes on 8th commit + +$commit_sha7 7th + +$commit_sha6 6th + +$commit_sha5 5th +y notes on 5th commit +z notes on 5th commit + +$commit_sha4 4th +y notes on 4th commit + +$commit_sha3 3rd +y notes on 3rd commit + +$commit_sha2 2nd +z notes on 2nd commit + +$commit_sha1 1st + +EOF + +test_expect_success 'merge y into z with "cat_sort_uniq" strategy => Non-conflicting 3-way merge' ' + git notes merge --strategy=cat_sort_uniq y && + verify_notes z cat_sort_uniq +' + +test_done diff --git a/t/t3310-notes-merge-manual-resolve.sh b/t/t3310-notes-merge-manual-resolve.sh new file mode 100755 index 0000000000..4ec4d11450 --- /dev/null +++ b/t/t3310-notes-merge-manual-resolve.sh @@ -0,0 +1,556 @@ +#!/bin/sh +# +# Copyright (c) 2010 Johan Herland +# + +test_description='Test notes merging with manual conflict resolution' + +. ./test-lib.sh + +# Set up a notes merge scenario with different kinds of conflicts +test_expect_success 'setup commits' ' + test_commit 1st && + test_commit 2nd && + test_commit 3rd && + test_commit 4th && + test_commit 5th +' + +commit_sha1=$(git rev-parse 1st^{commit}) +commit_sha2=$(git rev-parse 2nd^{commit}) +commit_sha3=$(git rev-parse 3rd^{commit}) +commit_sha4=$(git rev-parse 4th^{commit}) +commit_sha5=$(git rev-parse 5th^{commit}) + +verify_notes () { + notes_ref="$1" + git -c core.notesRef="refs/notes/$notes_ref" notes | + sort >"output_notes_$notes_ref" && + test_cmp "expect_notes_$notes_ref" "output_notes_$notes_ref" && + git -c core.notesRef="refs/notes/$notes_ref" log --format="%H %s%n%N" \ + >"output_log_$notes_ref" && + test_cmp "expect_log_$notes_ref" "output_log_$notes_ref" +} + +cat <<EOF | sort >expect_notes_x +6e8e3febca3c2bb896704335cc4d0c34cb2f8715 $commit_sha4 +e5388c10860456ee60673025345fe2e153eb8cf8 $commit_sha3 +ceefa674873670e7ecd131814d909723cce2b669 $commit_sha2 +EOF + +cat >expect_log_x <<EOF +$commit_sha5 5th + +$commit_sha4 4th +x notes on 4th commit + +$commit_sha3 3rd +x notes on 3rd commit + +$commit_sha2 2nd +x notes on 2nd commit + +$commit_sha1 1st + +EOF + +test_expect_success 'setup merge base (x)' ' + git config core.notesRef refs/notes/x && + git notes add -m "x notes on 2nd commit" 2nd && + git notes add -m "x notes on 3rd commit" 3rd && + git notes add -m "x notes on 4th commit" 4th && + verify_notes x +' + +cat <<EOF | sort >expect_notes_y +e2bfd06a37dd2031684a59a6e2b033e212239c78 $commit_sha4 +5772f42408c0dd6f097a7ca2d24de0e78d1c46b1 $commit_sha3 +b0a6021ec006d07e80e9b20ec9b444cbd9d560d3 $commit_sha1 +EOF + +cat >expect_log_y <<EOF +$commit_sha5 5th + +$commit_sha4 4th +y notes on 4th commit + +$commit_sha3 3rd +y notes on 3rd commit + +$commit_sha2 2nd + +$commit_sha1 1st +y notes on 1st commit + +EOF + +test_expect_success 'setup local branch (y)' ' + git update-ref refs/notes/y refs/notes/x && + git config core.notesRef refs/notes/y && + git notes add -f -m "y notes on 1st commit" 1st && + git notes remove 2nd && + git notes add -f -m "y notes on 3rd commit" 3rd && + git notes add -f -m "y notes on 4th commit" 4th && + verify_notes y +' + +cat <<EOF | sort >expect_notes_z +cff59c793c20bb49a4e01bc06fb06bad642e0d54 $commit_sha4 +283b48219aee9a4105f6cab337e789065c82c2b9 $commit_sha2 +0a81da8956346e19bcb27a906f04af327e03e31b $commit_sha1 +EOF + +cat >expect_log_z <<EOF +$commit_sha5 5th + +$commit_sha4 4th +z notes on 4th commit + +$commit_sha3 3rd + +$commit_sha2 2nd +z notes on 2nd commit + +$commit_sha1 1st +z notes on 1st commit + +EOF + +test_expect_success 'setup remote branch (z)' ' + git update-ref refs/notes/z refs/notes/x && + git config core.notesRef refs/notes/z && + git notes add -f -m "z notes on 1st commit" 1st && + git notes add -f -m "z notes on 2nd commit" 2nd && + git notes remove 3rd && + git notes add -f -m "z notes on 4th commit" 4th && + verify_notes z +' + +# At this point, before merging z into y, we have the following status: +# +# commit | base/x | local/y | remote/z | diff from x to y/z +# -------|---------|---------|----------|--------------------------- +# 1st | [none] | b0a6021 | 0a81da8 | added / added (diff) +# 2nd | ceefa67 | [none] | 283b482 | removed / changed +# 3rd | e5388c1 | 5772f42 | [none] | changed / removed +# 4th | 6e8e3fe | e2bfd06 | cff59c7 | changed / changed (diff) +# 5th | [none] | [none] | [none] | [none] + +cat <<EOF | sort >expect_conflicts +$commit_sha1 +$commit_sha2 +$commit_sha3 +$commit_sha4 +EOF + +cat >expect_conflict_$commit_sha1 <<EOF +<<<<<<< refs/notes/m +y notes on 1st commit +======= +z notes on 1st commit +>>>>>>> refs/notes/z +EOF + +cat >expect_conflict_$commit_sha2 <<EOF +z notes on 2nd commit +EOF + +cat >expect_conflict_$commit_sha3 <<EOF +y notes on 3rd commit +EOF + +cat >expect_conflict_$commit_sha4 <<EOF +<<<<<<< refs/notes/m +y notes on 4th commit +======= +z notes on 4th commit +>>>>>>> refs/notes/z +EOF + +cp expect_notes_y expect_notes_m +cp expect_log_y expect_log_m + +git rev-parse refs/notes/y > pre_merge_y +git rev-parse refs/notes/z > pre_merge_z + +test_expect_success 'merge z into m (== y) with default ("manual") resolver => Conflicting 3-way merge' ' + git update-ref refs/notes/m refs/notes/y && + git config core.notesRef refs/notes/m && + test_must_fail git notes merge z >output && + # Output should point to where to resolve conflicts + grep -q "\\.git/NOTES_MERGE_WORKTREE" output && + # Inspect merge conflicts + ls .git/NOTES_MERGE_WORKTREE >output_conflicts && + test_cmp expect_conflicts output_conflicts && + ( for f in $(cat expect_conflicts); do + test_cmp "expect_conflict_$f" ".git/NOTES_MERGE_WORKTREE/$f" || + exit 1 + done ) && + # Verify that current notes tree (pre-merge) has not changed (m == y) + verify_notes y && + verify_notes m && + test "$(git rev-parse refs/notes/m)" = "$(cat pre_merge_y)" +' + +cat <<EOF | sort >expect_notes_z +00494adecf2d9635a02fa431308d67993f853968 $commit_sha4 +283b48219aee9a4105f6cab337e789065c82c2b9 $commit_sha2 +0a81da8956346e19bcb27a906f04af327e03e31b $commit_sha1 +EOF + +cat >expect_log_z <<EOF +$commit_sha5 5th + +$commit_sha4 4th +z notes on 4th commit + +More z notes on 4th commit + +$commit_sha3 3rd + +$commit_sha2 2nd +z notes on 2nd commit + +$commit_sha1 1st +z notes on 1st commit + +EOF + +test_expect_success 'change notes in z' ' + git notes --ref z append -m "More z notes on 4th commit" 4th && + verify_notes z +' + +test_expect_success 'cannot do merge w/conflicts when previous merge is unfinished' ' + test -d .git/NOTES_MERGE_WORKTREE && + test_must_fail git notes merge z >output 2>&1 && + # Output should indicate what is wrong + grep -q "\\.git/NOTES_MERGE_\\* exists" output +' + +# Setup non-conflicting merge between x and new notes ref w + +cat <<EOF | sort >expect_notes_w +ceefa674873670e7ecd131814d909723cce2b669 $commit_sha2 +f75d1df88cbfe4258d49852f26cfc83f2ad4494b $commit_sha1 +EOF + +cat >expect_log_w <<EOF +$commit_sha5 5th + +$commit_sha4 4th + +$commit_sha3 3rd + +$commit_sha2 2nd +x notes on 2nd commit + +$commit_sha1 1st +w notes on 1st commit + +EOF + +test_expect_success 'setup unrelated notes ref (w)' ' + git config core.notesRef refs/notes/w && + git notes add -m "w notes on 1st commit" 1st && + git notes add -m "x notes on 2nd commit" 2nd && + verify_notes w +' + +cat <<EOF | sort >expect_notes_w +6e8e3febca3c2bb896704335cc4d0c34cb2f8715 $commit_sha4 +e5388c10860456ee60673025345fe2e153eb8cf8 $commit_sha3 +ceefa674873670e7ecd131814d909723cce2b669 $commit_sha2 +f75d1df88cbfe4258d49852f26cfc83f2ad4494b $commit_sha1 +EOF + +cat >expect_log_w <<EOF +$commit_sha5 5th + +$commit_sha4 4th +x notes on 4th commit + +$commit_sha3 3rd +x notes on 3rd commit + +$commit_sha2 2nd +x notes on 2nd commit + +$commit_sha1 1st +w notes on 1st commit + +EOF + +test_expect_success 'can do merge without conflicts even if previous merge is unfinished (x => w)' ' + test -d .git/NOTES_MERGE_WORKTREE && + git notes merge x && + verify_notes w && + # Verify that other notes refs has not changed (x and y) + verify_notes x && + verify_notes y +' + +cat <<EOF | sort >expect_notes_m +021faa20e931fb48986ffc6282b4bb05553ac946 $commit_sha4 +5772f42408c0dd6f097a7ca2d24de0e78d1c46b1 $commit_sha3 +283b48219aee9a4105f6cab337e789065c82c2b9 $commit_sha2 +0a59e787e6d688aa6309e56e8c1b89431a0fc1c1 $commit_sha1 +EOF + +cat >expect_log_m <<EOF +$commit_sha5 5th + +$commit_sha4 4th +y and z notes on 4th commit + +$commit_sha3 3rd +y notes on 3rd commit + +$commit_sha2 2nd +z notes on 2nd commit + +$commit_sha1 1st +y and z notes on 1st commit + +EOF + +test_expect_success 'finalize conflicting merge (z => m)' ' + # Resolve conflicts and finalize merge + cat >.git/NOTES_MERGE_WORKTREE/$commit_sha1 <<EOF && +y and z notes on 1st commit +EOF + cat >.git/NOTES_MERGE_WORKTREE/$commit_sha4 <<EOF && +y and z notes on 4th commit +EOF + git notes merge --commit && + # No .git/NOTES_MERGE_* files left + test_must_fail ls .git/NOTES_MERGE_* >output 2>/dev/null && + test_cmp /dev/null output && + # Merge commit has pre-merge y and pre-merge z as parents + test "$(git rev-parse refs/notes/m^1)" = "$(cat pre_merge_y)" && + test "$(git rev-parse refs/notes/m^2)" = "$(cat pre_merge_z)" && + # Merge commit mentions the notes refs merged + git log -1 --format=%B refs/notes/m > merge_commit_msg && + grep -q refs/notes/m merge_commit_msg && + grep -q refs/notes/z merge_commit_msg && + # Merge commit mentions conflicting notes + grep -q "Conflicts" merge_commit_msg && + ( for sha1 in $(cat expect_conflicts); do + grep -q "$sha1" merge_commit_msg || + exit 1 + done ) && + # Verify contents of merge result + verify_notes m && + # Verify that other notes refs has not changed (w, x, y and z) + verify_notes w && + verify_notes x && + verify_notes y && + verify_notes z +' + +cat >expect_conflict_$commit_sha4 <<EOF +<<<<<<< refs/notes/m +y notes on 4th commit +======= +z notes on 4th commit + +More z notes on 4th commit +>>>>>>> refs/notes/z +EOF + +cp expect_notes_y expect_notes_m +cp expect_log_y expect_log_m + +git rev-parse refs/notes/y > pre_merge_y +git rev-parse refs/notes/z > pre_merge_z + +test_expect_success 'redo merge of z into m (== y) with default ("manual") resolver => Conflicting 3-way merge' ' + git update-ref refs/notes/m refs/notes/y && + git config core.notesRef refs/notes/m && + test_must_fail git notes merge z >output && + # Output should point to where to resolve conflicts + grep -q "\\.git/NOTES_MERGE_WORKTREE" output && + # Inspect merge conflicts + ls .git/NOTES_MERGE_WORKTREE >output_conflicts && + test_cmp expect_conflicts output_conflicts && + ( for f in $(cat expect_conflicts); do + test_cmp "expect_conflict_$f" ".git/NOTES_MERGE_WORKTREE/$f" || + exit 1 + done ) && + # Verify that current notes tree (pre-merge) has not changed (m == y) + verify_notes y && + verify_notes m && + test "$(git rev-parse refs/notes/m)" = "$(cat pre_merge_y)" +' + +test_expect_success 'abort notes merge' ' + git notes merge --abort && + # No .git/NOTES_MERGE_* files left + test_must_fail ls .git/NOTES_MERGE_* >output 2>/dev/null && + test_cmp /dev/null output && + # m has not moved (still == y) + test "$(git rev-parse refs/notes/m)" = "$(cat pre_merge_y)" + # Verify that other notes refs has not changed (w, x, y and z) + verify_notes w && + verify_notes x && + verify_notes y && + verify_notes z +' + +git rev-parse refs/notes/y > pre_merge_y +git rev-parse refs/notes/z > pre_merge_z + +test_expect_success 'redo merge of z into m (== y) with default ("manual") resolver => Conflicting 3-way merge' ' + test_must_fail git notes merge z >output && + # Output should point to where to resolve conflicts + grep -q "\\.git/NOTES_MERGE_WORKTREE" output && + # Inspect merge conflicts + ls .git/NOTES_MERGE_WORKTREE >output_conflicts && + test_cmp expect_conflicts output_conflicts && + ( for f in $(cat expect_conflicts); do + test_cmp "expect_conflict_$f" ".git/NOTES_MERGE_WORKTREE/$f" || + exit 1 + done ) && + # Verify that current notes tree (pre-merge) has not changed (m == y) + verify_notes y && + verify_notes m && + test "$(git rev-parse refs/notes/m)" = "$(cat pre_merge_y)" +' + +cat <<EOF | sort >expect_notes_m +304dfb4325cf243025b9957486eb605a9b51c199 $commit_sha5 +283b48219aee9a4105f6cab337e789065c82c2b9 $commit_sha2 +0a59e787e6d688aa6309e56e8c1b89431a0fc1c1 $commit_sha1 +EOF + +cat >expect_log_m <<EOF +$commit_sha5 5th +new note on 5th commit + +$commit_sha4 4th + +$commit_sha3 3rd + +$commit_sha2 2nd +z notes on 2nd commit + +$commit_sha1 1st +y and z notes on 1st commit + +EOF + +test_expect_success 'add + remove notes in finalized merge (z => m)' ' + # Resolve one conflict + cat >.git/NOTES_MERGE_WORKTREE/$commit_sha1 <<EOF && +y and z notes on 1st commit +EOF + # Remove another conflict + rm .git/NOTES_MERGE_WORKTREE/$commit_sha4 && + # Remove a D/F conflict + rm .git/NOTES_MERGE_WORKTREE/$commit_sha3 && + # Add a new note + echo "new note on 5th commit" > .git/NOTES_MERGE_WORKTREE/$commit_sha5 && + # Finalize merge + git notes merge --commit && + # No .git/NOTES_MERGE_* files left + test_must_fail ls .git/NOTES_MERGE_* >output 2>/dev/null && + test_cmp /dev/null output && + # Merge commit has pre-merge y and pre-merge z as parents + test "$(git rev-parse refs/notes/m^1)" = "$(cat pre_merge_y)" && + test "$(git rev-parse refs/notes/m^2)" = "$(cat pre_merge_z)" && + # Merge commit mentions the notes refs merged + git log -1 --format=%B refs/notes/m > merge_commit_msg && + grep -q refs/notes/m merge_commit_msg && + grep -q refs/notes/z merge_commit_msg && + # Merge commit mentions conflicting notes + grep -q "Conflicts" merge_commit_msg && + ( for sha1 in $(cat expect_conflicts); do + grep -q "$sha1" merge_commit_msg || + exit 1 + done ) && + # Verify contents of merge result + verify_notes m && + # Verify that other notes refs has not changed (w, x, y and z) + verify_notes w && + verify_notes x && + verify_notes y && + verify_notes z +' + +cp expect_notes_y expect_notes_m +cp expect_log_y expect_log_m + +test_expect_success 'redo merge of z into m (== y) with default ("manual") resolver => Conflicting 3-way merge' ' + git update-ref refs/notes/m refs/notes/y && + test_must_fail git notes merge z >output && + # Output should point to where to resolve conflicts + grep -q "\\.git/NOTES_MERGE_WORKTREE" output && + # Inspect merge conflicts + ls .git/NOTES_MERGE_WORKTREE >output_conflicts && + test_cmp expect_conflicts output_conflicts && + ( for f in $(cat expect_conflicts); do + test_cmp "expect_conflict_$f" ".git/NOTES_MERGE_WORKTREE/$f" || + exit 1 + done ) && + # Verify that current notes tree (pre-merge) has not changed (m == y) + verify_notes y && + verify_notes m && + test "$(git rev-parse refs/notes/m)" = "$(cat pre_merge_y)" +' + +cp expect_notes_w expect_notes_m +cp expect_log_w expect_log_m + +test_expect_success 'reset notes ref m to somewhere else (w)' ' + git update-ref refs/notes/m refs/notes/w && + verify_notes m && + test "$(git rev-parse refs/notes/m)" = "$(git rev-parse refs/notes/w)" +' + +test_expect_success 'fail to finalize conflicting merge if underlying ref has moved in the meantime (m != NOTES_MERGE_PARTIAL^1)' ' + # Resolve conflicts + cat >.git/NOTES_MERGE_WORKTREE/$commit_sha1 <<EOF && +y and z notes on 1st commit +EOF + cat >.git/NOTES_MERGE_WORKTREE/$commit_sha4 <<EOF && +y and z notes on 4th commit +EOF + # Fail to finalize merge + test_must_fail git notes merge --commit >output 2>&1 && + # .git/NOTES_MERGE_* must remain + test -f .git/NOTES_MERGE_PARTIAL && + test -f .git/NOTES_MERGE_REF && + test -f .git/NOTES_MERGE_WORKTREE/$commit_sha1 && + test -f .git/NOTES_MERGE_WORKTREE/$commit_sha2 && + test -f .git/NOTES_MERGE_WORKTREE/$commit_sha3 && + test -f .git/NOTES_MERGE_WORKTREE/$commit_sha4 && + # Refs are unchanged + test "$(git rev-parse refs/notes/m)" = "$(git rev-parse refs/notes/w)" + test "$(git rev-parse refs/notes/y)" = "$(git rev-parse NOTES_MERGE_PARTIAL^1)" + test "$(git rev-parse refs/notes/m)" != "$(git rev-parse NOTES_MERGE_PARTIAL^1)" + # Mention refs/notes/m, and its current and expected value in output + grep -q "refs/notes/m" output && + grep -q "$(git rev-parse refs/notes/m)" output && + grep -q "$(git rev-parse NOTES_MERGE_PARTIAL^1)" output && + # Verify that other notes refs has not changed (w, x, y and z) + verify_notes w && + verify_notes x && + verify_notes y && + verify_notes z +' + +test_expect_success 'resolve situation by aborting the notes merge' ' + git notes merge --abort && + # No .git/NOTES_MERGE_* files left + test_must_fail ls .git/NOTES_MERGE_* >output 2>/dev/null && + test_cmp /dev/null output && + # m has not moved (still == w) + test "$(git rev-parse refs/notes/m)" = "$(git rev-parse refs/notes/w)" + # Verify that other notes refs has not changed (w, x, y and z) + verify_notes w && + verify_notes x && + verify_notes y && + verify_notes z +' + +test_done diff --git a/t/t3311-notes-merge-fanout.sh b/t/t3311-notes-merge-fanout.sh new file mode 100755 index 0000000000..93516ef67c --- /dev/null +++ b/t/t3311-notes-merge-fanout.sh @@ -0,0 +1,436 @@ +#!/bin/sh +# +# Copyright (c) 2010 Johan Herland +# + +test_description='Test notes merging at various fanout levels' + +. ./test-lib.sh + +verify_notes () { + notes_ref="$1" + commit="$2" + if test -f "expect_notes_$notes_ref" + then + git -c core.notesRef="refs/notes/$notes_ref" notes | + sort >"output_notes_$notes_ref" && + test_cmp "expect_notes_$notes_ref" "output_notes_$notes_ref" || + return 1 + fi && + git -c core.notesRef="refs/notes/$notes_ref" log --format="%H %s%n%N" \ + "$commit" >"output_log_$notes_ref" && + test_cmp "expect_log_$notes_ref" "output_log_$notes_ref" +} + +verify_fanout () { + notes_ref="$1" + # Expect entire notes tree to have a fanout == 1 + git rev-parse --quiet --verify "refs/notes/$notes_ref" >/dev/null && + git ls-tree -r --name-only "refs/notes/$notes_ref" | + while read path + do + case "$path" in + ??/??????????????????????????????????????) + : true + ;; + *) + echo "Invalid path \"$path\"" && + return 1 + ;; + esac + done +} + +verify_no_fanout () { + notes_ref="$1" + # Expect entire notes tree to have a fanout == 0 + git rev-parse --quiet --verify "refs/notes/$notes_ref" >/dev/null && + git ls-tree -r --name-only "refs/notes/$notes_ref" | + while read path + do + case "$path" in + ????????????????????????????????????????) + : true + ;; + *) + echo "Invalid path \"$path\"" && + return 1 + ;; + esac + done +} + +# Set up a notes merge scenario with different kinds of conflicts +test_expect_success 'setup a few initial commits with notes (notes ref: x)' ' + git config core.notesRef refs/notes/x && + for i in 1 2 3 4 5 + do + test_commit "commit$i" >/dev/null && + git notes add -m "notes for commit$i" || return 1 + done +' + +commit_sha1=$(git rev-parse commit1^{commit}) +commit_sha2=$(git rev-parse commit2^{commit}) +commit_sha3=$(git rev-parse commit3^{commit}) +commit_sha4=$(git rev-parse commit4^{commit}) +commit_sha5=$(git rev-parse commit5^{commit}) + +cat <<EOF | sort >expect_notes_x +aed91155c7a72c2188e781fdf40e0f3761b299db $commit_sha5 +99fab268f9d7ee7b011e091a436c78def8eeee69 $commit_sha4 +953c20ae26c7aa0b428c20693fe38bc687f9d1a9 $commit_sha3 +6358796131b8916eaa2dde6902642942a1cb37e1 $commit_sha2 +b02d459c32f0e68f2fe0981033bb34f38776ba47 $commit_sha1 +EOF + +cat >expect_log_x <<EOF +$commit_sha5 commit5 +notes for commit5 + +$commit_sha4 commit4 +notes for commit4 + +$commit_sha3 commit3 +notes for commit3 + +$commit_sha2 commit2 +notes for commit2 + +$commit_sha1 commit1 +notes for commit1 + +EOF + +test_expect_success 'sanity check (x)' ' + verify_notes x commit5 && + verify_no_fanout x +' + +num=300 + +cp expect_log_x expect_log_y + +test_expect_success 'Add a few hundred commits w/notes to trigger fanout (x -> y)' ' + git update-ref refs/notes/y refs/notes/x && + git config core.notesRef refs/notes/y && + i=5 && + while test $i -lt $num + do + i=$(($i + 1)) && + test_commit "commit$i" >/dev/null && + git notes add -m "notes for commit$i" || return 1 + done && + test "$(git rev-parse refs/notes/y)" != "$(git rev-parse refs/notes/x)" && + # Expected number of commits and notes + test $(git rev-list HEAD | wc -l) = $num && + test $(git notes list | wc -l) = $num && + # 5 first notes unchanged + verify_notes y commit5 +' + +test_expect_success 'notes tree has fanout (y)' 'verify_fanout y' + +test_expect_success 'No-op merge (already included) (x => y)' ' + git update-ref refs/notes/m refs/notes/y && + git config core.notesRef refs/notes/m && + git notes merge x && + test "$(git rev-parse refs/notes/m)" = "$(git rev-parse refs/notes/y)" +' + +test_expect_success 'Fast-forward merge (y => x)' ' + git update-ref refs/notes/m refs/notes/x && + git notes merge y && + test "$(git rev-parse refs/notes/m)" = "$(git rev-parse refs/notes/y)" +' + +cat <<EOF | sort >expect_notes_z +9f506ee70e20379d7f78204c77b334f43d77410d $commit_sha3 +23a47d6ea7d589895faf800752054818e1e7627b $commit_sha2 +b02d459c32f0e68f2fe0981033bb34f38776ba47 $commit_sha1 +EOF + +cat >expect_log_z <<EOF +$commit_sha5 commit5 + +$commit_sha4 commit4 + +$commit_sha3 commit3 +notes for commit3 + +appended notes for commit3 + +$commit_sha2 commit2 +new notes for commit2 + +$commit_sha1 commit1 +notes for commit1 + +EOF + +test_expect_success 'change some of the initial 5 notes (x -> z)' ' + git update-ref refs/notes/z refs/notes/x && + git config core.notesRef refs/notes/z && + git notes add -f -m "new notes for commit2" commit2 && + git notes append -m "appended notes for commit3" commit3 && + git notes remove commit4 && + git notes remove commit5 && + verify_notes z commit5 +' + +test_expect_success 'notes tree has no fanout (z)' 'verify_no_fanout z' + +cp expect_log_z expect_log_m + +test_expect_success 'successful merge without conflicts (y => z)' ' + git update-ref refs/notes/m refs/notes/z && + git config core.notesRef refs/notes/m && + git notes merge y && + verify_notes m commit5 && + # x/y/z unchanged + verify_notes x commit5 && + verify_notes y commit5 && + verify_notes z commit5 +' + +test_expect_success 'notes tree still has fanout after merge (m)' 'verify_fanout m' + +cat >expect_log_w <<EOF +$commit_sha5 commit5 + +$commit_sha4 commit4 +other notes for commit4 + +$commit_sha3 commit3 +other notes for commit3 + +$commit_sha2 commit2 +notes for commit2 + +$commit_sha1 commit1 +other notes for commit1 + +EOF + +test_expect_success 'introduce conflicting changes (y -> w)' ' + git update-ref refs/notes/w refs/notes/y && + git config core.notesRef refs/notes/w && + git notes add -f -m "other notes for commit1" commit1 && + git notes add -f -m "other notes for commit3" commit3 && + git notes add -f -m "other notes for commit4" commit4 && + git notes remove commit5 && + verify_notes w commit5 +' + +cat >expect_log_m <<EOF +$commit_sha5 commit5 + +$commit_sha4 commit4 +other notes for commit4 + +$commit_sha3 commit3 +other notes for commit3 + +$commit_sha2 commit2 +new notes for commit2 + +$commit_sha1 commit1 +other notes for commit1 + +EOF + +test_expect_success 'successful merge using "ours" strategy (z => w)' ' + git update-ref refs/notes/m refs/notes/w && + git config core.notesRef refs/notes/m && + git notes merge -s ours z && + verify_notes m commit5 && + # w/x/y/z unchanged + verify_notes w commit5 && + verify_notes x commit5 && + verify_notes y commit5 && + verify_notes z commit5 +' + +test_expect_success 'notes tree still has fanout after merge (m)' 'verify_fanout m' + +cat >expect_log_m <<EOF +$commit_sha5 commit5 + +$commit_sha4 commit4 + +$commit_sha3 commit3 +notes for commit3 + +appended notes for commit3 + +$commit_sha2 commit2 +new notes for commit2 + +$commit_sha1 commit1 +other notes for commit1 + +EOF + +test_expect_success 'successful merge using "theirs" strategy (z => w)' ' + git update-ref refs/notes/m refs/notes/w && + git notes merge -s theirs z && + verify_notes m commit5 && + # w/x/y/z unchanged + verify_notes w commit5 && + verify_notes x commit5 && + verify_notes y commit5 && + verify_notes z commit5 +' + +test_expect_success 'notes tree still has fanout after merge (m)' 'verify_fanout m' + +cat >expect_log_m <<EOF +$commit_sha5 commit5 + +$commit_sha4 commit4 +other notes for commit4 + +$commit_sha3 commit3 +other notes for commit3 + +notes for commit3 + +appended notes for commit3 + +$commit_sha2 commit2 +new notes for commit2 + +$commit_sha1 commit1 +other notes for commit1 + +EOF + +test_expect_success 'successful merge using "union" strategy (z => w)' ' + git update-ref refs/notes/m refs/notes/w && + git notes merge -s union z && + verify_notes m commit5 && + # w/x/y/z unchanged + verify_notes w commit5 && + verify_notes x commit5 && + verify_notes y commit5 && + verify_notes z commit5 +' + +test_expect_success 'notes tree still has fanout after merge (m)' 'verify_fanout m' + +cat >expect_log_m <<EOF +$commit_sha5 commit5 + +$commit_sha4 commit4 +other notes for commit4 + +$commit_sha3 commit3 +appended notes for commit3 +notes for commit3 +other notes for commit3 + +$commit_sha2 commit2 +new notes for commit2 + +$commit_sha1 commit1 +other notes for commit1 + +EOF + +test_expect_success 'successful merge using "cat_sort_uniq" strategy (z => w)' ' + git update-ref refs/notes/m refs/notes/w && + git notes merge -s cat_sort_uniq z && + verify_notes m commit5 && + # w/x/y/z unchanged + verify_notes w commit5 && + verify_notes x commit5 && + verify_notes y commit5 && + verify_notes z commit5 +' + +test_expect_success 'notes tree still has fanout after merge (m)' 'verify_fanout m' + +# We're merging z into w. Here are the conflicts we expect: +# +# commit | x -> w | x -> z | conflict? +# -------|-----------|-----------|---------- +# 1 | changed | unchanged | no, use w +# 2 | unchanged | changed | no, use z +# 3 | changed | changed | yes (w, then z in conflict markers) +# 4 | changed | deleted | yes (w) +# 5 | deleted | deleted | no, deleted + +test_expect_success 'fails to merge using "manual" strategy (z => w)' ' + git update-ref refs/notes/m refs/notes/w && + test_must_fail git notes merge z +' + +test_expect_success 'notes tree still has fanout after merge (m)' 'verify_fanout m' + +cat <<EOF | sort >expect_conflicts +$commit_sha3 +$commit_sha4 +EOF + +cat >expect_conflict_$commit_sha3 <<EOF +<<<<<<< refs/notes/m +other notes for commit3 +======= +notes for commit3 + +appended notes for commit3 +>>>>>>> refs/notes/z +EOF + +cat >expect_conflict_$commit_sha4 <<EOF +other notes for commit4 +EOF + +test_expect_success 'verify conflict entries (with no fanout)' ' + ls .git/NOTES_MERGE_WORKTREE >output_conflicts && + test_cmp expect_conflicts output_conflicts && + ( for f in $(cat expect_conflicts); do + test_cmp "expect_conflict_$f" ".git/NOTES_MERGE_WORKTREE/$f" || + exit 1 + done ) && + # Verify that current notes tree (pre-merge) has not changed (m == w) + test "$(git rev-parse refs/notes/m)" = "$(git rev-parse refs/notes/w)" +' + +cat >expect_log_m <<EOF +$commit_sha5 commit5 + +$commit_sha4 commit4 +other notes for commit4 + +$commit_sha3 commit3 +other notes for commit3 + +appended notes for commit3 + +$commit_sha2 commit2 +new notes for commit2 + +$commit_sha1 commit1 +other notes for commit1 + +EOF + +test_expect_success 'resolve and finalize merge (z => w)' ' + cat >.git/NOTES_MERGE_WORKTREE/$commit_sha3 <<EOF && +other notes for commit3 + +appended notes for commit3 +EOF + git notes merge --commit && + verify_notes m commit5 && + # w/x/y/z unchanged + verify_notes w commit5 && + verify_notes x commit5 && + verify_notes y commit5 && + verify_notes z commit5 +' + +test_expect_success 'notes tree still has fanout after merge (m)' 'verify_fanout m' + +test_done diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh index 7d20a74c5c..d3a3bd2679 100755 --- a/t/t3404-rebase-interactive.sh +++ b/t/t3404-rebase-interactive.sh @@ -7,34 +7,39 @@ test_description='git rebase interactive This test runs git rebase "interactively", by faking an edit, and verifies that the result still makes sense. + +Initial setup: + + one - two - three - four (conflict-branch) + / + A - B - C - D - E (master) + | \ + | F - G - H (branch1) + | \ + |\ I (branch2) + | \ + | J - K - L - M (no-conflict-branch) + \ + N - O - P (no-ff-branch) + + where A, B, D and G all touch file1, and one, two, three, four all + touch file "conflict". ' . ./test-lib.sh . "$TEST_DIRECTORY"/lib-rebase.sh +test_cmp_rev () { + git rev-parse --verify "$1" >expect.rev && + git rev-parse --verify "$2" >actual.rev && + test_cmp expect.rev actual.rev +} + set_fake_editor -# Set up the repository like this: -# -# one - two - three - four (conflict-branch) -# / -# A - B - C - D - E (master) -# | \ -# | F - G - H (branch1) -# | \ -# |\ I (branch2) -# | \ -# | J - K - L - M (no-conflict-branch) -# \ -# N - O - P (no-ff-branch) -# -# where A, B, D and G all touch file1, and one, two, three, four all -# touch file "conflict". -# # WARNING: Modifications to the initial repository can change the SHA ID used # in the expect2 file for the 'stop on conflicting pick' test. - test_expect_success 'setup' ' test_commit A file1 && test_commit B file1 && @@ -46,22 +51,21 @@ test_expect_success 'setup' ' test_commit G file1 && test_commit H file5 && git checkout -b branch2 F && - test_commit I file6 + test_commit I file6 && git checkout -b conflict-branch A && - for n in one two three four - do - test_commit $n conflict - done && + test_commit one conflict && + test_commit two conflict && + test_commit three conflict && + test_commit four conflict && git checkout -b no-conflict-branch A && - for n in J K L M - do - test_commit $n file$n - done && + test_commit J fileJ && + test_commit K fileK && + test_commit L fileL && + test_commit M fileM && git checkout -b no-ff-branch A && - for n in N O P - do - test_commit $n file$n - done + test_commit N fileN && + test_commit O fileO && + test_commit P fileP ' # "exec" commands are ran with the user shell by default, but this may @@ -82,20 +86,12 @@ test_expect_success 'rebase -i with the exec command' ' test_path_is_file touch-one && test_path_is_file touch-two && test_path_is_missing touch-three " (should have stopped before)" && - test $(git rev-parse C) = $(git rev-parse HEAD) || { - echo "Stopped at wrong revision:" - echo "($(git describe --tags HEAD) instead of C)" - false - } && + test_cmp_rev C HEAD && git rebase --continue && test_path_is_file touch-three && test_path_is_file "touch-file name with spaces" && test_path_is_file touch-after-semicolon && - test $(git rev-parse master) = $(git rev-parse HEAD) || { - echo "Stopped at wrong revision:" - echo "($(git describe --tags HEAD) instead of master)" - false - } && + test_cmp_rev master HEAD && rm -f touch-* ' @@ -116,11 +112,7 @@ test_expect_success 'rebase -i with the exec command checks tree cleanness' ' export FAKE_LINES && test_must_fail git rebase -i HEAD^ ) && - test $(git rev-parse master^) = $(git rev-parse HEAD) || { - echo "Stopped at wrong revision:" - echo "($(git describe --tags HEAD) instead of master^)" - false - } && + test_cmp_rev master^ HEAD && git reset --hard && git rebase --continue ' @@ -584,7 +576,7 @@ test_expect_success 'do "noop" when there is nothing to cherry-pick' ' git checkout -b branch4 HEAD && GIT_EDITOR=: git commit --amend \ - --author="Somebody else <somebody@else.com>" + --author="Somebody else <somebody@else.com>" && test $(git rev-parse branch3) != $(git rev-parse branch4) && git rebase -i branch3 && test $(git rev-parse branch3) = $(git rev-parse branch4) @@ -599,7 +591,7 @@ test_expect_success 'submodule rebase setup' ' git add elif && git commit -m "submodule initial" ) && echo 1 >file1 && - git add file1 sub + git add file1 sub && test_tick && git commit -m "One" && echo 2 >file1 && @@ -655,6 +647,7 @@ test_expect_success 'rebase -i can copy notes' ' cat >expect <<EOF an earlier note + a note EOF diff --git a/t/t3406-rebase-message.sh b/t/t3406-rebase-message.sh index 85fc7c4af8..fe5f936988 100755 --- a/t/t3406-rebase-message.sh +++ b/t/t3406-rebase-message.sh @@ -43,20 +43,20 @@ test_expect_success 'rebase -m' ' ' test_expect_success 'rebase --stat' ' - git reset --hard start + git reset --hard start && git rebase --stat master >diffstat.txt && grep "^ fileX | *1 +$" diffstat.txt ' test_expect_success 'rebase w/config rebase.stat' ' - git reset --hard start + git reset --hard start && git config rebase.stat true && git rebase master >diffstat.txt && grep "^ fileX | *1 +$" diffstat.txt ' test_expect_success 'rebase -n overrides config rebase.stat config' ' - git reset --hard start + git reset --hard start && git config rebase.stat true && git rebase -n master >diffstat.txt && ! grep "^ fileX | *1 +$" diffstat.txt diff --git a/t/t3408-rebase-multi-line.sh b/t/t3408-rebase-multi-line.sh index 2062b858bb..6b84e6042a 100755 --- a/t/t3408-rebase-multi-line.sh +++ b/t/t3408-rebase-multi-line.sh @@ -16,7 +16,7 @@ test_expect_success setup ' git commit -a -m "A sample commit log message that has a long summary that spills over multiple lines. -But otherwise with a sane description." +But otherwise with a sane description." && git branch side && diff --git a/t/t3409-rebase-preserve-merges.sh b/t/t3409-rebase-preserve-merges.sh index 74161a42ec..19341e5ca1 100755 --- a/t/t3409-rebase-preserve-merges.sh +++ b/t/t3409-rebase-preserve-merges.sh @@ -72,7 +72,7 @@ test_expect_success 'rebase -p fakes interactive rebase' ' git fetch && git rebase -p origin/topic && test 1 = $(git rev-list --all --pretty=oneline | grep "Modify A" | wc -l) && - test 1 = $(git rev-list --all --pretty=oneline | grep "Merge remote branch " | wc -l) + test 1 = $(git rev-list --all --pretty=oneline | grep "Merge remote-tracking branch " | wc -l) ) ' diff --git a/t/t3412-rebase-root.sh b/t/t3412-rebase-root.sh index 5869061c5b..086c91c7b4 100755 --- a/t/t3412-rebase-root.sh +++ b/t/t3412-rebase-root.sh @@ -173,14 +173,14 @@ EOF test_expect_success 'pre-rebase hook stops rebase' ' git checkout -b stops1 other && test_must_fail git rebase --root --onto master && - test "z$(git symbolic-ref HEAD)" = zrefs/heads/stops1 + test "z$(git symbolic-ref HEAD)" = zrefs/heads/stops1 && test 0 = $(git rev-list other...stops1 | wc -l) ' test_expect_success 'pre-rebase hook stops rebase -i' ' git checkout -b stops2 other && test_must_fail git rebase --root --onto master && - test "z$(git symbolic-ref HEAD)" = zrefs/heads/stops2 + test "z$(git symbolic-ref HEAD)" = zrefs/heads/stops2 && test 0 = $(git rev-list other...stops2 | wc -l) ' diff --git a/t/t3415-rebase-autosquash.sh b/t/t3415-rebase-autosquash.sh index fd2184ce71..b38be8e937 100755 --- a/t/t3415-rebase-autosquash.sh +++ b/t/t3415-rebase-autosquash.sh @@ -14,6 +14,7 @@ test_expect_success setup ' git add . && test_tick && git commit -m "first commit" && + git tag first-commit && echo 3 >file3 && git add . && test_tick && @@ -21,7 +22,7 @@ test_expect_success setup ' git tag base ' -test_auto_fixup() { +test_auto_fixup () { git reset --hard base && echo 1 >file1 && git add -u && @@ -50,7 +51,7 @@ test_expect_success 'auto fixup (config)' ' test_must_fail test_auto_fixup final-fixup-config-false ' -test_auto_squash() { +test_auto_squash () { git reset --hard base && echo 1 >file1 && git add -u && @@ -94,4 +95,102 @@ test_expect_success 'misspelled auto squash' ' test 0 = $(git rev-list final-missquash...HEAD | wc -l) ' +test_expect_success 'auto squash that matches 2 commits' ' + git reset --hard base && + echo 4 >file4 && + git add file4 && + test_tick && + git commit -m "first new commit" && + echo 1 >file1 && + git add -u && + test_tick && + git commit -m "squash! first" && + git tag final-multisquash && + test_tick && + git rebase --autosquash -i HEAD~4 && + git log --oneline >actual && + test 4 = $(wc -l <actual) && + git diff --exit-code final-multisquash && + test 1 = "$(git cat-file blob HEAD^^:file1)" && + test 2 = $(git cat-file commit HEAD^^ | grep first | wc -l) && + test 1 = $(git cat-file commit HEAD | grep first | wc -l) +' + +test_expect_success 'auto squash that matches a commit after the squash' ' + git reset --hard base && + echo 1 >file1 && + git add -u && + test_tick && + git commit -m "squash! third" && + echo 4 >file4 && + git add file4 && + test_tick && + git commit -m "third commit" && + git tag final-presquash && + test_tick && + git rebase --autosquash -i HEAD~4 && + git log --oneline >actual && + test 5 = $(wc -l <actual) && + git diff --exit-code final-presquash && + test 0 = "$(git cat-file blob HEAD^^:file1)" && + test 1 = "$(git cat-file blob HEAD^:file1)" && + test 1 = $(git cat-file commit HEAD | grep third | wc -l) && + test 1 = $(git cat-file commit HEAD^ | grep third | wc -l) +' +test_expect_success 'auto squash that matches a sha1' ' + git reset --hard base && + echo 1 >file1 && + git add -u && + test_tick && + git commit -m "squash! $(git rev-parse --short HEAD^)" && + git tag final-shasquash && + test_tick && + git rebase --autosquash -i HEAD^^^ && + git log --oneline >actual && + test 3 = $(wc -l <actual) && + git diff --exit-code final-shasquash && + test 1 = "$(git cat-file blob HEAD^:file1)" && + test 1 = $(git cat-file commit HEAD^ | grep squash | wc -l) +' + +test_expect_success 'auto squash that matches longer sha1' ' + git reset --hard base && + echo 1 >file1 && + git add -u && + test_tick && + git commit -m "squash! $(git rev-parse --short=11 HEAD^)" && + git tag final-longshasquash && + test_tick && + git rebase --autosquash -i HEAD^^^ && + git log --oneline >actual && + test 3 = $(wc -l <actual) && + git diff --exit-code final-longshasquash && + test 1 = "$(git cat-file blob HEAD^:file1)" && + test 1 = $(git cat-file commit HEAD^ | grep squash | wc -l) +' + +test_auto_commit_flags () { + git reset --hard base && + echo 1 >file1 && + git add -u && + test_tick && + git commit --$1 first-commit && + git tag final-commit-$1 && + test_tick && + git rebase --autosquash -i HEAD^^^ && + git log --oneline >actual && + test 3 = $(wc -l <actual) && + git diff --exit-code final-commit-$1 && + test 1 = "$(git cat-file blob HEAD^:file1)" && + test $2 = $(git cat-file commit HEAD^ | grep first | wc -l) +} + +test_expect_success 'use commit --fixup' ' + test_auto_commit_flags fixup 1 +' + +test_expect_success 'use commit --squash' ' + test_auto_commit_flags squash 2 +' + test_done diff --git a/t/t3417-rebase-whitespace-fix.sh b/t/t3417-rebase-whitespace-fix.sh index 220a740ee8..1fb3e499b4 100755 --- a/t/t3417-rebase-whitespace-fix.sh +++ b/t/t3417-rebase-whitespace-fix.sh @@ -89,7 +89,7 @@ test_expect_success 'same, but do not remove trailing spaces' ' git config core.whitespace "-blank-at-eol" && git reset --hard HEAD^ && cp third file && git add file && git commit -m third && - git rebase --whitespace=fix HEAD^^ + git rebase --whitespace=fix HEAD^^ && git diff --exit-code HEAD^:file expect-second && test_cmp file third ' diff --git a/t/t3501-revert-cherry-pick.sh b/t/t3501-revert-cherry-pick.sh index bc7aedd048..043954422c 100755 --- a/t/t3501-revert-cherry-pick.sh +++ b/t/t3501-revert-cherry-pick.sh @@ -81,6 +81,16 @@ test_expect_success 'revert after renaming branch' ' ' +test_expect_success 'cherry-pick on stat-dirty working tree' ' + git clone . copy && + ( + cd copy && + git checkout initial && + test-chmtime +40 oops && + git cherry-pick added + ) +' + test_expect_success 'revert forbidden on dirty working tree' ' echo content >extra_file && diff --git a/t/t3504-cherry-pick-rerere.sh b/t/t3504-cherry-pick-rerere.sh index f7b3518a32..e6a64816ef 100755 --- a/t/t3504-cherry-pick-rerere.sh +++ b/t/t3504-cherry-pick-rerere.sh @@ -23,7 +23,7 @@ test_expect_success 'conflicting merge' ' test_expect_success 'fixup' ' echo foo-dev >foo && git add foo && test_tick && git commit -q -m 4 && - git reset --hard HEAD^ + git reset --hard HEAD^ && echo foo-dev >expect ' @@ -33,7 +33,7 @@ test_expect_success 'cherry-pick conflict' ' ' test_expect_success 'reconfigure' ' - git config rerere.enabled false + git config rerere.enabled false && git reset --hard ' diff --git a/t/t3509-cherry-pick-merge-df.sh b/t/t3509-cherry-pick-merge-df.sh index a5ccdbf8fc..948ca1bce6 100755 --- a/t/t3509-cherry-pick-merge-df.sh +++ b/t/t3509-cherry-pick-merge-df.sh @@ -32,4 +32,70 @@ test_expect_success SYMLINKS 'Cherry-pick succeeds with rename across D/F confli git cherry-pick branch ' +test_expect_success 'Setup rename with file on one side matching directory name on other' ' + git checkout --orphan nick-testcase && + git rm -rf . && + + >empty && + git add empty && + git commit -m "Empty file" && + + git checkout -b simple && + mv empty file && + mkdir empty && + mv file empty && + git add empty/file && + git commit -m "Empty file under empty dir" && + + echo content >newfile && + git add newfile && + git commit -m "New file" +' + +test_expect_success 'Cherry-pick succeeds with was_a_dir/file -> was_a_dir (resolve)' ' + git reset --hard && + git checkout -q nick-testcase^0 && + git cherry-pick --strategy=resolve simple +' + +test_expect_success 'Cherry-pick succeeds with was_a_dir/file -> was_a_dir (recursive)' ' + git reset --hard && + git checkout -q nick-testcase^0 && + git cherry-pick --strategy=recursive simple +' + +test_expect_success 'Setup rename with file on one side matching different dirname on other' ' + git reset --hard && + git checkout --orphan mergeme && + git rm -rf . && + + mkdir sub && + mkdir othersub && + echo content > sub/file && + echo foo > othersub/whatever && + git add -A && + git commit -m "Common commmit" && + + git rm -rf othersub && + git mv sub/file othersub && + git commit -m "Commit to merge" && + + git checkout -b newhead mergeme~1 && + >independent-change && + git add independent-change && + git commit -m "Completely unrelated change" +' + +test_expect_success 'Cherry-pick with rename to different D/F conflict succeeds (resolve)' ' + git reset --hard && + git checkout -q newhead^0 && + git cherry-pick --strategy=resolve mergeme +' + +test_expect_success 'Cherry-pick with rename to different D/F conflict succeeds (recursive)' ' + git reset --hard && + git checkout -q newhead^0 && + git cherry-pick --strategy=recursive mergeme +' + test_done diff --git a/t/t3900-i18n-commit.sh b/t/t3900-i18n-commit.sh index 256c4c9701..c06a5ee766 100755 --- a/t/t3900-i18n-commit.sh +++ b/t/t3900-i18n-commit.sh @@ -133,4 +133,33 @@ do ' done +test_commit_autosquash_flags () { + H=$1 + flag=$2 + test_expect_success "commit --$flag with $H encoding" ' + git config i18n.commitencoding $H && + git checkout -b $H-$flag C0 && + echo $H >>F && + git commit -a -F "$TEST_DIRECTORY"/t3900/$H.txt && + test_tick && + echo intermediate stuff >>G && + git add G && + git commit -a -m "intermediate commit" && + test_tick && + echo $H $flag >>F && + git commit -a --$flag HEAD~1 $3 && + E=$(git cat-file commit '$H-$flag' | + sed -ne "s/^encoding //p") && + test "z$E" = "z$H" && + git config --unset-all i18n.commitencoding && + git rebase --autosquash -i HEAD^^^ && + git log --oneline >actual && + test 3 = $(wc -l <actual) + ' +} + +test_commit_autosquash_flags eucJP fixup + +test_commit_autosquash_flags ISO-2022-JP squash '-m "squash message"' + test_done diff --git a/t/t3902-quoted.sh b/t/t3902-quoted.sh index 7d49469841..da82b655b3 100755 --- a/t/t3902-quoted.sh +++ b/t/t3902-quoted.sh @@ -36,19 +36,19 @@ for_each_name () { test_expect_success TABS_IN_FILENAMES 'setup' ' mkdir "$FN" && - for_each_name "echo initial >\"\$name\"" + for_each_name "echo initial >\"\$name\"" && git add . && git commit -q -m Initial && for_each_name "echo second >\"\$name\"" && - git commit -a -m Second + git commit -a -m Second && for_each_name "echo modified >\"\$name\"" ' test_expect_success TABS_IN_FILENAMES 'setup expected files' ' -cat >expect.quoted <<\EOF +cat >expect.quoted <<\EOF && Name "Name and a\nLF" "Name and an\tHT" diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh index 903a122efe..6fd560ccf1 100755 --- a/t/t3903-stash.sh +++ b/t/t3903-stash.sh @@ -157,7 +157,7 @@ EOF test_expect_success 'stash branch' ' echo foo > file && - git commit file -m first + git commit file -m first && echo bar > file && echo bar2 > file2 && git add file2 && @@ -255,7 +255,7 @@ test_expect_success 'stash rm and ignore' ' echo file >.gitignore && git stash save "rm and ignore" && test bar = "$(cat file)" && - test file = "$(cat .gitignore)" + test file = "$(cat .gitignore)" && git stash apply && ! test -r file && test file = "$(cat .gitignore)" @@ -268,7 +268,7 @@ test_expect_success 'stash rm and ignore (stage .gitignore)' ' git add .gitignore && git stash save "rm and ignore (stage .gitignore)" && test bar = "$(cat file)" && - ! test -r .gitignore + ! test -r .gitignore && git stash apply && ! test -r file && test file = "$(cat .gitignore)" diff --git a/t/t3904-stash-patch.sh b/t/t3904-stash-patch.sh index d1819ca23a..1e7193ac0b 100755 --- a/t/t3904-stash-patch.sh +++ b/t/t3904-stash-patch.sh @@ -20,7 +20,7 @@ test_expect_success PERL 'setup' ' # note: bar sorts before dir, so the first 'n' is always to skip 'bar' test_expect_success PERL 'saying "n" does nothing' ' - set_state dir/foo work index + set_state dir/foo work index && (echo n; echo n) | test_must_fail git stash save -p && verify_state dir/foo work index && verify_saved_state bar diff --git a/t/t4002-diff-basic.sh b/t/t4002-diff-basic.sh index 73441a5165..9fb8ca06a8 100755 --- a/t/t4002-diff-basic.sh +++ b/t/t4002-diff-basic.sh @@ -205,8 +205,8 @@ test_expect_success \ 'rm -fr Z [A-Z][A-Z] && git read-tree $tree_A && git checkout-index -f -a && - git read-tree --reset $tree_O || return 1 - git update-index --refresh >/dev/null ;# this can exit non-zero + git read-tree --reset $tree_O && + test_must_fail git update-index --refresh -q && git diff-files >.test-a && cmp_diff_files_output .test-a .test-recursive-OA' @@ -215,8 +215,8 @@ test_expect_success \ 'rm -fr Z [A-Z][A-Z] && git read-tree $tree_B && git checkout-index -f -a && - git read-tree --reset $tree_O || return 1 - git update-index --refresh >/dev/null ;# this can exit non-zero + git read-tree --reset $tree_O && + test_must_fail git update-index --refresh -q && git diff-files >.test-a && cmp_diff_files_output .test-a .test-recursive-OB' @@ -225,8 +225,8 @@ test_expect_success \ 'rm -fr Z [A-Z][A-Z] && git read-tree $tree_B && git checkout-index -f -a && - git read-tree --reset $tree_A || return 1 - git update-index --refresh >/dev/null ;# this can exit non-zero + git read-tree --reset $tree_A && + test_must_fail git update-index --refresh -q && git diff-files >.test-a && cmp_diff_files_output .test-a .test-recursive-AB' diff --git a/t/t4008-diff-break-rewrite.sh b/t/t4008-diff-break-rewrite.sh index e19ca65885..d79d9e1e71 100755 --- a/t/t4008-diff-break-rewrite.sh +++ b/t/t4008-diff-break-rewrite.sh @@ -155,7 +155,7 @@ test_expect_success \ git checkout-index -f -u -a && sed -e "s/git/GIT/" file0 >file1 && sed -e "s/git/GET/" file0 >file2 && - rm -f file0 + rm -f file0 && git update-index --add --remove file0 file1 file2' test_expect_success \ diff --git a/t/t4015-diff-whitespace.sh b/t/t4015-diff-whitespace.sh index a8736f7cbe..8622eb51c7 100755 --- a/t/t4015-diff-whitespace.sh +++ b/t/t4015-diff-whitespace.sh @@ -330,7 +330,7 @@ test_expect_success 'check space before tab in indent (space-before-tab: on)' ' test_expect_success 'check spaces as indentation (indent-with-non-tab: off)' ' - git config core.whitespace "-indent-with-non-tab" + git config core.whitespace "-indent-with-non-tab" && echo " foo ();" > x && git diff --check diff --git a/t/t4017-diff-retval.sh b/t/t4017-diff-retval.sh index 61589853df..95a7ca7070 100755 --- a/t/t4017-diff-retval.sh +++ b/t/t4017-diff-retval.sh @@ -29,66 +29,49 @@ test_expect_success 'git diff --quiet -w HEAD^ HEAD' ' ' test_expect_success 'git diff-tree HEAD^ HEAD' ' - git diff-tree --exit-code HEAD^ HEAD - test $? = 1 + test_expect_code 1 git diff-tree --exit-code HEAD^ HEAD ' test_expect_success 'git diff-tree HEAD^ HEAD -- a' ' git diff-tree --exit-code HEAD^ HEAD -- a - test $? = 0 ' test_expect_success 'git diff-tree HEAD^ HEAD -- b' ' - git diff-tree --exit-code HEAD^ HEAD -- b - test $? = 1 + test_expect_code 1 git diff-tree --exit-code HEAD^ HEAD -- b ' test_expect_success 'echo HEAD | git diff-tree --stdin' ' - echo $(git rev-parse HEAD) | git diff-tree --exit-code --stdin - test $? = 1 + echo $(git rev-parse HEAD) | test_expect_code 1 git diff-tree --exit-code --stdin ' test_expect_success 'git diff-tree HEAD HEAD' ' git diff-tree --exit-code HEAD HEAD - test $? = 0 ' test_expect_success 'git diff-files' ' git diff-files --exit-code - test $? = 0 ' test_expect_success 'git diff-index --cached HEAD' ' git diff-index --exit-code --cached HEAD - test $? = 0 ' test_expect_success 'git diff-index --cached HEAD^' ' - git diff-index --exit-code --cached HEAD^ - test $? = 1 + test_expect_code 1 git diff-index --exit-code --cached HEAD^ ' test_expect_success 'git diff-index --cached HEAD^' ' echo text >>b && echo 3 >c && - git add . && { - git diff-index --exit-code --cached HEAD^ - test $? = 1 - } + git add . && + test_expect_code 1 git diff-index --exit-code --cached HEAD^ ' test_expect_success 'git diff-tree -Stext HEAD^ HEAD -- b' ' - git commit -m "text in b" && { - git diff-tree -p --exit-code -Stext HEAD^ HEAD -- b - test $? = 1 - } + git commit -m "text in b" && + test_expect_code 1 git diff-tree -p --exit-code -Stext HEAD^ HEAD -- b ' test_expect_success 'git diff-tree -Snot-found HEAD^ HEAD -- b' ' git diff-tree -p --exit-code -Snot-found HEAD^ HEAD -- b - test $? = 0 ' test_expect_success 'git diff-files' ' - echo 3 >>c && { - git diff-files --exit-code - test $? = 1 - } + echo 3 >>c && + test_expect_code 1 git diff-files --exit-code ' test_expect_success 'git diff-index --cached HEAD' ' - git update-index c && { - git diff-index --exit-code --cached HEAD - test $? = 1 - } + git update-index c && + test_expect_code 1 git diff-index --exit-code --cached HEAD ' test_expect_success '--check --exit-code returns 0 for no difference' ' @@ -100,30 +83,26 @@ test_expect_success '--check --exit-code returns 0 for no difference' ' test_expect_success '--check --exit-code returns 1 for a clean difference' ' echo "good" > a && - git diff --check --exit-code - test $? = 1 + test_expect_code 1 git diff --check --exit-code ' test_expect_success '--check --exit-code returns 3 for a dirty difference' ' echo "bad " >> a && - git diff --check --exit-code - test $? = 3 + test_expect_code 3 git diff --check --exit-code ' test_expect_success '--check with --no-pager returns 2 for dirty difference' ' - git --no-pager diff --check - test $? = 2 + test_expect_code 2 git --no-pager diff --check ' test_expect_success 'check should test not just the last line' ' echo "" >>a && - git --no-pager diff --check - test $? = 2 + test_expect_code 2 git --no-pager diff --check ' @@ -133,10 +112,8 @@ test_expect_success 'check detects leftover conflict markers' ' echo binary >>b && git commit -m "side" b && test_must_fail git merge master && - git add b && ( - git --no-pager diff --cached --check >test.out - test $? = 2 - ) && + git add b && + test_expect_code 2 git --no-pager diff --cached --check >test.out && test 3 = $(grep "conflict marker" test.out | wc -l) && git reset --hard ' @@ -146,19 +123,13 @@ test_expect_success 'check honors conflict marker length' ' echo ">>>>>>> boo" >>b && echo "======" >>a && git diff --check a && - ( - git diff --check b - test $? = 2 - ) && + test_expect_code 2 git diff --check b && git reset --hard && echo ">>>>>>>> boo" >>b && echo "========" >>a && git diff --check && echo "b conflict-marker-size=8" >.gitattributes && - ( - git diff --check b - test $? = 2 - ) && + test_expect_code 2 git diff --check b && git diff --check a && git reset --hard ' diff --git a/t/t4019-diff-wserror.sh b/t/t4019-diff-wserror.sh index f6d1f1ebab..3fa836f9d3 100755 --- a/t/t4019-diff-wserror.sh +++ b/t/t4019-diff-wserror.sh @@ -36,11 +36,12 @@ prepare_output () { git diff --color >output $grep_a "$blue_grep" output >error $grep_a -v "$blue_grep" output >normal + return 0 } test_expect_success default ' - prepare_output + prepare_output && grep Eight normal >/dev/null && grep HT error >/dev/null && @@ -52,8 +53,8 @@ test_expect_success default ' test_expect_success 'without -trail' ' - git config core.whitespace -trail - prepare_output + git config core.whitespace -trail && + prepare_output && grep Eight normal >/dev/null && grep HT error >/dev/null && @@ -65,9 +66,9 @@ test_expect_success 'without -trail' ' test_expect_success 'without -trail (attribute)' ' - git config --unset core.whitespace - echo "F whitespace=-trail" >.gitattributes - prepare_output + test_might_fail git config --unset core.whitespace && + echo "F whitespace=-trail" >.gitattributes && + prepare_output && grep Eight normal >/dev/null && grep HT error >/dev/null && @@ -79,9 +80,9 @@ test_expect_success 'without -trail (attribute)' ' test_expect_success 'without -space' ' - rm -f .gitattributes - git config core.whitespace -space - prepare_output + rm -f .gitattributes && + git config core.whitespace -space && + prepare_output && grep Eight normal >/dev/null && grep HT normal >/dev/null && @@ -93,9 +94,9 @@ test_expect_success 'without -space' ' test_expect_success 'without -space (attribute)' ' - git config --unset core.whitespace - echo "F whitespace=-space" >.gitattributes - prepare_output + test_might_fail git config --unset core.whitespace && + echo "F whitespace=-space" >.gitattributes && + prepare_output && grep Eight normal >/dev/null && grep HT normal >/dev/null && @@ -107,9 +108,9 @@ test_expect_success 'without -space (attribute)' ' test_expect_success 'with indent-non-tab only' ' - rm -f .gitattributes - git config core.whitespace indent,-trailing,-space - prepare_output + rm -f .gitattributes && + git config core.whitespace indent,-trailing,-space && + prepare_output && grep Eight error >/dev/null && grep HT normal >/dev/null && @@ -121,9 +122,9 @@ test_expect_success 'with indent-non-tab only' ' test_expect_success 'with indent-non-tab only (attribute)' ' - git config --unset core.whitespace - echo "F whitespace=indent,-trailing,-space" >.gitattributes - prepare_output + test_might_fail git config --unset core.whitespace && + echo "F whitespace=indent,-trailing,-space" >.gitattributes && + prepare_output && grep Eight error >/dev/null && grep HT normal >/dev/null && @@ -135,9 +136,9 @@ test_expect_success 'with indent-non-tab only (attribute)' ' test_expect_success 'with cr-at-eol' ' - rm -f .gitattributes - git config core.whitespace cr-at-eol - prepare_output + rm -f .gitattributes && + git config core.whitespace cr-at-eol && + prepare_output && grep Eight normal >/dev/null && grep HT error >/dev/null && @@ -149,9 +150,9 @@ test_expect_success 'with cr-at-eol' ' test_expect_success 'with cr-at-eol (attribute)' ' - git config --unset core.whitespace - echo "F whitespace=trailing,cr-at-eol" >.gitattributes - prepare_output + test_might_fail git config --unset core.whitespace && + echo "F whitespace=trailing,cr-at-eol" >.gitattributes && + prepare_output && grep Eight normal >/dev/null && grep HT error >/dev/null && @@ -178,12 +179,21 @@ test_expect_success 'trailing empty lines (2)' ' ' +test_expect_success 'checkdiff shows correct line number for trailing blank lines' ' + + printf "a\nb\n" > G && + git add G && + printf "x\nx\nx\na\nb\nc\n\n" > G && + [ "$(git diff --check -- G)" = "G:7: new blank line at EOF." ] + +' + test_expect_success 'do not color trailing cr in context' ' - git config --unset core.whitespace + test_might_fail git config --unset core.whitespace && rm -f .gitattributes && echo AAAQ | tr Q "\015" >G && git add G && - echo BBBQ | tr Q "\015" >>G + echo BBBQ | tr Q "\015" >>G && git diff --color G | tr "\015" Q >output && grep "BBB.*${blue_grep}Q" output && grep "AAA.*\[mQ" output diff --git a/t/t4021-format-patch-numbered.sh b/t/t4021-format-patch-numbered.sh index 709b3231ca..886494b58f 100755 --- a/t/t4021-format-patch-numbered.sh +++ b/t/t4021-format-patch-numbered.sh @@ -95,7 +95,7 @@ test_expect_success 'format.numbered && --keep-subject' ' test_expect_success 'format.numbered = auto' ' - git config format.numbered auto + git config format.numbered auto && git format-patch --stdout HEAD~2 > patch5 && test_numbered patch5 diff --git a/t/t4026-color.sh b/t/t4026-color.sh index d5ccdd0cf8..3726a0e201 100755 --- a/t/t4026-color.sh +++ b/t/t4026-color.sh @@ -74,7 +74,6 @@ test_expect_success 'extra character after attribute' ' ' test_expect_success 'unknown color slots are ignored (diff)' ' - git config --unset diff.color.new git config color.diff.nosuchslotwilleverbedefined white && git diff --color ' diff --git a/t/t4027-diff-submodule.sh b/t/t4027-diff-submodule.sh index d99814ac64..241a74d2a2 100755 --- a/t/t4027-diff-submodule.sh +++ b/t/t4027-diff-submodule.sh @@ -316,11 +316,11 @@ test_expect_success 'git diff (empty submodule dir)' ' test_expect_success 'conflicted submodule setup' ' # 39 efs - c=fffffffffffffffffffffffffffffffffffffff + c=fffffffffffffffffffffffffffffffffffffff && ( - echo "000000 $_z40 0 sub" - echo "160000 1$c 1 sub" - echo "160000 2$c 2 sub" + echo "000000 $_z40 0 sub" && + echo "160000 1$c 1 sub" && + echo "160000 2$c 2 sub" && echo "160000 3$c 3 sub" ) | git update-index --index-info && echo >expect.nosub '\''diff --cc sub diff --git a/t/t4034-diff-words.sh b/t/t4034-diff-words.sh index 3f3c7577ca..8096d8a337 100755 --- a/t/t4034-diff-words.sh +++ b/t/t4034-diff-words.sh @@ -6,8 +6,8 @@ test_description='word diff colors' test_expect_success setup ' - git config diff.color.old red - git config diff.color.new green + git config diff.color.old red && + git config diff.color.new green && git config diff.color.func magenta ' diff --git a/t/t4103-apply-binary.sh b/t/t4103-apply-binary.sh index 08ad6d8b9e..dbbf56cba9 100755 --- a/t/t4103-apply-binary.sh +++ b/t/t4103-apply-binary.sh @@ -50,11 +50,11 @@ test_expect_success 'setup' " " test_expect_success 'stat binary diff -- should not fail.' \ - 'git checkout master + 'git checkout master && git apply --stat --summary B.diff' test_expect_success 'stat binary diff (copy) -- should not fail.' \ - 'git checkout master + 'git checkout master && git apply --stat --summary C.diff' test_expect_success 'check binary diff -- should fail.' \ @@ -78,11 +78,11 @@ test_expect_success \ ' test_expect_success 'check binary diff with replacement.' \ - 'git checkout master + 'git checkout master && git apply --check --allow-binary-replacement BF.diff' test_expect_success 'check binary diff with replacement (copy).' \ - 'git checkout master + 'git checkout master && git apply --check --allow-binary-replacement CF.diff' # Now we start applying them. diff --git a/t/t4111-apply-subdir.sh b/t/t4111-apply-subdir.sh index a52d94ae21..7c398432ba 100755 --- a/t/t4111-apply-subdir.sh +++ b/t/t4111-apply-subdir.sh @@ -89,7 +89,7 @@ test_expect_success 'apply --index from subdir of toplevel' ' test_expect_success 'apply from .git dir' ' cp postimage expected && cp preimage .git/file && - cp preimage .git/objects/file + cp preimage .git/objects/file && ( cd .git && git apply "$patch" @@ -100,7 +100,7 @@ test_expect_success 'apply from .git dir' ' test_expect_success 'apply from subdir of .git dir' ' cp postimage expected && cp preimage .git/file && - cp preimage .git/objects/file + cp preimage .git/objects/file && ( cd .git/objects && git apply "$patch" diff --git a/t/t4119-apply-config.sh b/t/t4119-apply-config.sh index 3c73a783a7..3d0384daa8 100755 --- a/t/t4119-apply-config.sh +++ b/t/t4119-apply-config.sh @@ -73,7 +73,7 @@ D=`pwd` test_expect_success 'apply --whitespace=strip in subdir' ' cd "$D" && - git config --unset-all apply.whitespace + git config --unset-all apply.whitespace && rm -f sub/file1 && cp saved sub/file1 && git update-index --refresh && diff --git a/t/t4120-apply-popt.sh b/t/t4120-apply-popt.sh index 2b2d00b334..579c9e6105 100755 --- a/t/t4120-apply-popt.sh +++ b/t/t4120-apply-popt.sh @@ -56,4 +56,30 @@ test_expect_success 'apply with too large -p and fancy filename' ' grep "removing 3 leading" err ' +test_expect_success 'apply (-p2) diff, mode change only' ' + cat >patch.chmod <<-\EOF && + diff --git a/sub/file1 b/sub/file1 + old mode 100644 + new mode 100755 + EOF + chmod 644 file1 && + git apply -p2 patch.chmod && + test -x file1 +' + +test_expect_success 'apply (-p2) diff, rename' ' + cat >patch.rename <<-\EOF && + diff --git a/sub/file1 b/sub/file2 + similarity index 100% + rename from sub/file1 + rename to sub/file2 + EOF + echo A >expected && + + cp file1.saved file1 && + rm -f file2 && + git apply -p2 patch.rename && + test_cmp expected file2 +' + test_done diff --git a/t/t4124-apply-ws-rule.sh b/t/t4124-apply-ws-rule.sh index 8a676a5dcd..61bfc569c3 100755 --- a/t/t4124-apply-ws-rule.sh +++ b/t/t4124-apply-ws-rule.sh @@ -176,9 +176,8 @@ test_expect_success 'trailing whitespace & no newline at the end of file' ' ' test_expect_success 'blank at EOF with --whitespace=fix (1)' ' - : these can fail depending on what we did before - git config --unset core.whitespace - rm -f .gitattributes + test_might_fail git config --unset core.whitespace && + rm -f .gitattributes && { echo a; echo b; echo c; } >one && git add one && @@ -368,7 +367,7 @@ test_expect_success 'missing blanks at EOF must only match blank lines' ' git diff -- one >patch && echo a >one && - test_must_fail git apply patch + test_must_fail git apply patch && test_must_fail git apply --whitespace=fix patch && test_must_fail git apply --ignore-space-change --whitespace=fix patch ' @@ -419,7 +418,7 @@ test_expect_success 'same, but with CR-LF line endings && cr-at-eol set' ' printf "b\r\n" >>one && printf "c\r\n" >>one && cp one save-one && - printf " \r\n" >>one + printf " \r\n" >>one && git add one && printf "d\r\n" >>one && cp one expect && @@ -436,7 +435,7 @@ test_expect_success 'same, but with CR-LF line endings && cr-at-eol unset' ' printf "b\r\n" >>one && printf "c\r\n" >>one && cp one save-one && - printf " \r\n" >>one + printf " \r\n" >>one && git add one && cp one expect && printf "d\r\n" >>one && diff --git a/t/t4127-apply-same-fn.sh b/t/t4127-apply-same-fn.sh index 77200c0b2d..972946c174 100755 --- a/t/t4127-apply-same-fn.sh +++ b/t/t4127-apply-same-fn.sh @@ -31,7 +31,7 @@ test_expect_success 'apply same filename with independent changes' ' ' test_expect_success 'apply same filename with overlapping changes' ' - git reset --hard + git reset --hard && modify "s/^d/z/" same_fn && git diff > patch0 && git add same_fn && @@ -44,8 +44,8 @@ test_expect_success 'apply same filename with overlapping changes' ' ' test_expect_success 'apply same new filename after rename' ' - git reset --hard - git mv same_fn new_fn + git reset --hard && + git mv same_fn new_fn && modify "s/^d/z/" new_fn && git add new_fn && git diff -M --cached > patch1 && @@ -58,12 +58,12 @@ test_expect_success 'apply same new filename after rename' ' ' test_expect_success 'apply same old filename after rename -- should fail.' ' - git reset --hard - git mv same_fn new_fn + git reset --hard && + git mv same_fn new_fn && modify "s/^d/z/" new_fn && git add new_fn && git diff -M --cached > patch1 && - git mv new_fn same_fn + git mv new_fn same_fn && modify "s/^e/y/" same_fn && git diff >> patch1 && git reset --hard && @@ -71,13 +71,13 @@ test_expect_success 'apply same old filename after rename -- should fail.' ' ' test_expect_success 'apply A->B (rename), C->A (rename), A->A -- should pass.' ' - git reset --hard - git mv same_fn new_fn + git reset --hard && + git mv same_fn new_fn && modify "s/^d/z/" new_fn && git add new_fn && git diff -M --cached > patch1 && git commit -m "a rename" && - git mv other_fn same_fn + git mv other_fn same_fn && modify "s/^e/y/" same_fn && git add same_fn && git diff -M --cached >> patch1 && diff --git a/t/t4130-apply-criss-cross-rename.sh b/t/t4130-apply-criss-cross-rename.sh index 7cfa2d6287..d173acde0f 100755 --- a/t/t4130-apply-criss-cross-rename.sh +++ b/t/t4130-apply-criss-cross-rename.sh @@ -44,7 +44,7 @@ test_expect_success 'criss-cross rename' ' git reset --hard && mv file1 tmp && mv file2 file1 && - mv file3 file2 + mv file3 file2 && mv tmp file3 && cp file1 file1-swapped && cp file2 file2-swapped && diff --git a/t/t4132-apply-removal.sh b/t/t4132-apply-removal.sh index bb1ffe3b6c..a2bc1cd37d 100755 --- a/t/t4132-apply-removal.sh +++ b/t/t4132-apply-removal.sh @@ -30,6 +30,7 @@ test_expect_success setup ' epocWest="1969-12-31 16:00:00.000000000 -0800" && epocGMT="1970-01-01 00:00:00.000000000 +0000" && epocEast="1970-01-01 09:00:00.000000000 +0900" && + epocWest2="1969-12-31 16:00:00 -08:00" && sed -e "s/TS0/$epocWest/" -e "s/TS1/$timeWest/" <c >createWest.patch && sed -e "s/TS0/$epocEast/" -e "s/TS1/$timeEast/" <c >createEast.patch && @@ -46,6 +47,7 @@ test_expect_success setup ' sed -e "s/TS0/$timeWest/" -e "s/TS1/$epocWest/" <d >removeWest.patch && sed -e "s/TS0/$timeEast/" -e "s/TS1/$epocEast/" <d >removeEast.patch && sed -e "s/TS0/$timeGMT/" -e "s/TS1/$epocGMT/" <d >removeGMT.patch && + sed -e "s/TS0/$timeWest/" -e "s/TS1/$epocWest2/" <d >removeWest2.patch && echo something >something && >empty diff --git a/t/t4133-apply-filenames.sh b/t/t4133-apply-filenames.sh index 34218071b6..94da99075c 100755 --- a/t/t4133-apply-filenames.sh +++ b/t/t4133-apply-filenames.sh @@ -8,7 +8,7 @@ test_description='git apply filename consistency check' . ./test-lib.sh test_expect_success setup ' - cat > bad1.patch <<EOF + cat > bad1.patch <<EOF && diff --git a/f b/f new file mode 100644 index 0000000..d00491f @@ -29,9 +29,9 @@ EOF ' test_expect_success 'apply diff with inconsistent filenames in headers' ' - test_must_fail git apply bad1.patch 2>err - grep "inconsistent new filename" err - test_must_fail git apply bad2.patch 2>err + test_must_fail git apply bad1.patch 2>err && + grep "inconsistent new filename" err && + test_must_fail git apply bad2.patch 2>err && grep "inconsistent old filename" err ' diff --git a/t/t4134-apply-submodule.sh b/t/t4134-apply-submodule.sh index 1b82f93cff..0043930ca6 100755 --- a/t/t4134-apply-submodule.sh +++ b/t/t4134-apply-submodule.sh @@ -8,7 +8,7 @@ test_description='git apply submodule tests' . ./test-lib.sh test_expect_success setup ' - cat > create-sm.patch <<EOF + cat > create-sm.patch <<EOF && diff --git a/dir/sm b/dir/sm new file mode 160000 index 0000000..0123456 diff --git a/t/t4135-apply-weird-filenames.sh b/t/t4135-apply-weird-filenames.sh index 1e5aad57ab..bf5dc57286 100755 --- a/t/t4135-apply-weird-filenames.sh +++ b/t/t4135-apply-weird-filenames.sh @@ -72,4 +72,20 @@ test_expect_success 'whitespace-damaged traditional patch' ' test_cmp expected postimage.txt ' +test_expect_success 'traditional patch with colon in timezone' ' + echo postimage >expected && + reset_preimage && + rm -f "post image.txt" && + git apply "$vector/funny-tz.diff" && + test_cmp expected "post image.txt" +' + +test_expect_success 'traditional, whitespace-damaged, colon in timezone' ' + echo postimage >expected && + reset_preimage && + rm -f "post image.txt" && + git apply "$vector/damaged-tz.diff" && + test_cmp expected "post image.txt" +' + test_done diff --git a/t/t4135/damaged-tz.diff b/t/t4135/damaged-tz.diff new file mode 100644 index 0000000000..07aaf08370 --- /dev/null +++ b/t/t4135/damaged-tz.diff @@ -0,0 +1,5 @@ +diff -urN -X /usr/people/jes/exclude-linux linux-2.6.12-rc2-mm3-vanilla/post image.txt linux-2.6.12-rc2-mm3/post image.txt +--- linux-2.6.12-rc2-mm3-vanilla/post image.txt 1969-12-31 16:00:00 -08:00 ++++ linux-2.6.12-rc2-mm3/post image.txt 2005-04-12 02:14:06 -07:00 +@@ -0,0 +1 @@ ++postimage diff --git a/t/t4135/funny-tz.diff b/t/t4135/funny-tz.diff new file mode 100644 index 0000000000..998e3a867e --- /dev/null +++ b/t/t4135/funny-tz.diff @@ -0,0 +1,5 @@ +diff -urN -X /usr/people/jes/exclude-linux linux-2.6.12-rc2-mm3-vanilla/post image.txt linux-2.6.12-rc2-mm3/post image.txt +--- linux-2.6.12-rc2-mm3-vanilla/post image.txt 1969-12-31 16:00:00 -08:00 ++++ linux-2.6.12-rc2-mm3/post image.txt 2005-04-12 02:14:06 -07:00 +@@ -0,0 +1 @@ ++postimage diff --git a/t/t4150-am.sh b/t/t4150-am.sh index 1c3d8ed548..850fc96d1f 100755 --- a/t/t4150-am.sh +++ b/t/t4150-am.sh @@ -219,7 +219,7 @@ test_expect_success 'am stays in branch' ' test_expect_success 'am --signoff does not add Signed-off-by: line if already there' ' git format-patch --stdout HEAD^ >patch3 && - sed -e "/^Subject/ s,\[PATCH,Re: Re: Re: & 1/5 v2," patch3 >patch4 + sed -e "/^Subject/ s,\[PATCH,Re: Re: Re: & 1/5 v2," patch3 >patch4 && rm -fr .git/rebase-apply && git reset --hard && git checkout HEAD^ && diff --git a/t/t4201-shortlog.sh b/t/t4201-shortlog.sh index cdb70b4b33..6872ba1a42 100755 --- a/t/t4201-shortlog.sh +++ b/t/t4201-shortlog.sh @@ -35,7 +35,7 @@ test_expect_success 'setup' ' tr 1234 "\370\235\204\236")" a1 && echo 5 >a1 && - git commit --quiet -m "a 12 34 56 78" a1 + git commit --quiet -m "a 12 34 56 78" a1 && echo 6 >a1 && git commit --quiet -m "Commit by someone else" \ diff --git a/t/t4202-log.sh b/t/t4202-log.sh index 2e51356947..2fcc31a6f3 100755 --- a/t/t4202-log.sh +++ b/t/t4202-log.sh @@ -191,7 +191,7 @@ test_expect_success 'git show <commits> leaves list of commits as given' ' test_expect_success 'setup case sensitivity tests' ' echo case >one && test_tick && - git add one + git add one && git commit -a -m Second ' @@ -341,7 +341,7 @@ test_expect_success 'set up more tangled history' ' test_commit octopus-b && git checkout master && test_commit seventh && - git merge octopus-a octopus-b + git merge octopus-a octopus-b && git merge reach ' @@ -393,7 +393,7 @@ test_expect_success 'log --graph with merge' ' ' test_expect_success 'log.decorate configuration' ' - git config --unset-all log.decorate || : + test_might_fail git config --unset-all log.decorate && git log --oneline >expect.none && git log --oneline --decorate >expect.short && @@ -422,6 +422,15 @@ test_expect_success 'log.decorate configuration' ' test_cmp expect.full actual && git config --unset-all log.decorate && + git config log.decorate 1 && + git log --oneline >actual && + test_cmp expect.short actual && + git log --oneline --decorate=full >actual && + test_cmp expect.full actual && + git log --oneline --decorate=no >actual && + test_cmp expect.none actual && + + git config --unset-all log.decorate && git config log.decorate short && git log --oneline >actual && test_cmp expect.short actual && diff --git a/t/t4252-am-options.sh b/t/t4252-am-options.sh index f603c1b133..e758e634a3 100755 --- a/t/t4252-am-options.sh +++ b/t/t4252-am-options.sh @@ -59,7 +59,7 @@ test_expect_success 'interrupted am --directory="frotz nitfol"' ' ' test_expect_success 'apply to a funny path' ' - with_sq="with'\''sq" + with_sq="with'\''sq" && rm -fr .git/rebase-apply && git reset --hard initial && git am --directory="$with_sq" "$tm"/am-test-5-2 && diff --git a/t/t5300-pack-object.sh b/t/t5300-pack-object.sh index bbb9c1251d..602806d09c 100755 --- a/t/t5300-pack-object.sh +++ b/t/t5300-pack-object.sh @@ -12,7 +12,7 @@ TRASH=`pwd` test_expect_success \ 'setup' \ - 'rm -f .git/index* + 'rm -f .git/index* && perl -e "print \"a\" x 4096;" > a && perl -e "print \"b\" x 4096;" > b && perl -e "print \"c\" x 4096;" > c && diff --git a/t/t5301-sliding-window.sh b/t/t5301-sliding-window.sh index 0a24e61ff9..2fc5af6007 100755 --- a/t/t5301-sliding-window.sh +++ b/t/t5301-sliding-window.sh @@ -8,7 +8,7 @@ test_description='mmap sliding window tests' test_expect_success \ 'setup' \ - 'rm -f .git/index* + 'rm -f .git/index* && for i in a b c do echo $i >$i && @@ -48,7 +48,7 @@ test_expect_success \ git repack -a -d && test "`git count-objects`" = "0 objects, 0 kilobytes" && pack2=`ls .git/objects/pack/*.pack` && - test -f "$pack2" + test -f "$pack2" && test "$pack1" \!= "$pack2"' test_expect_success \ diff --git a/t/t5302-pack-index.sh b/t/t5302-pack-index.sh index fb3a270822..b34ea93a80 100755 --- a/t/t5302-pack-index.sh +++ b/t/t5302-pack-index.sh @@ -8,7 +8,7 @@ test_description='pack index with 64-bit offsets and object CRC' test_expect_success \ 'setup' \ - 'rm -rf .git + 'rm -rf .git && git init && git config pack.threads 1 && i=1 && diff --git a/t/t5400-send-pack.sh b/t/t5400-send-pack.sh index 5bcf0b867a..b0b2684a1f 100755 --- a/t/t5400-send-pack.sh +++ b/t/t5400-send-pack.sh @@ -129,7 +129,7 @@ test_expect_success 'denyNonFastforwards trumps --force' ' test "$victim_orig" = "$victim_head" ' -test_expect_success 'push --all excludes remote tracking hierarchy' ' +test_expect_success 'push --all excludes remote-tracking hierarchy' ' mkdir parent && ( cd parent && diff --git a/t/t5500-fetch-pack.sh b/t/t5500-fetch-pack.sh index 18376d6608..bafcca765e 100755 --- a/t/t5500-fetch-pack.sh +++ b/t/t5500-fetch-pack.sh @@ -91,7 +91,7 @@ test_expect_success 'setup' ' prev=$cur && cur=$(($cur+1)) done && - add B1 $A1 + add B1 $A1 && echo $ATIP > .git/refs/heads/A && echo $BTIP > .git/refs/heads/B && git symbolic-ref HEAD refs/heads/B diff --git a/t/t5502-quickfetch.sh b/t/t5502-quickfetch.sh index 1037a723fe..7a46cbdbe6 100755 --- a/t/t5502-quickfetch.sh +++ b/t/t5502-quickfetch.sh @@ -57,7 +57,7 @@ test_expect_success 'copy commit and tree but not blob by hand' ' cd cloned && git count-objects | sed -e "s/ *objects,.*//" ) ) && - test $cnt -eq 6 + test $cnt -eq 6 && blob=$(git rev-parse HEAD:file | sed -e "s|..|&/|") && test -f "cloned/.git/objects/$blob" && diff --git a/t/t5503-tagfollow.sh b/t/t5503-tagfollow.sh index aa0ada0147..60de2d6ede 100755 --- a/t/t5503-tagfollow.sh +++ b/t/t5503-tagfollow.sh @@ -49,7 +49,7 @@ EOF ' test_expect_success NOT_MINGW 'fetch A (new commit : 1 connection)' ' - rm -f $U + rm -f $U && ( cd cloned && GIT_DEBUG_SEND_PACK=3 git fetch 3>../$U && @@ -82,7 +82,7 @@ EOF ' test_expect_success NOT_MINGW 'fetch C, T (new branch, tag : 1 connection)' ' - rm -f $U + rm -f $U && ( cd cloned && GIT_DEBUG_SEND_PACK=3 git fetch 3>../$U && @@ -121,7 +121,7 @@ EOF ' test_expect_success NOT_MINGW 'fetch B, S (commit and tag : 1 connection)' ' - rm -f $U + rm -f $U && ( cd cloned && GIT_DEBUG_SEND_PACK=3 git fetch 3>../$U && diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh index 5d1c66ea71..d189add2d0 100755 --- a/t/t5505-remote.sh +++ b/t/t5505-remote.sh @@ -107,16 +107,18 @@ test_expect_success 'remove remote' ' ) ' -test_expect_success 'remove remote protects non-remote branches' ' +test_expect_success 'remove remote protects local branches' ' ( cd test && { cat >expect1 <<EOF -Note: A non-remote branch was not removed; to delete it, use: +Note: A branch outside the refs/remotes/ hierarchy was not removed; +to delete it, use: git branch -d master EOF } && { cat >expect2 <<EOF -Note: Non-remote branches were not removed; to delete them, use: +Note: Some branches outside the refs/remotes/ hierarchy were not removed; +to delete them, use: git branch -d foobranch git branch -d master EOF diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh index 9a884751ec..7e433b179f 100755 --- a/t/t5510-fetch.sh +++ b/t/t5510-fetch.sh @@ -119,7 +119,7 @@ test_expect_success 'fetch must not resolve short tag name' ' test_expect_success 'fetch must not resolve short remote name' ' cd "$D" && - git update-ref refs/remotes/six/HEAD HEAD + git update-ref refs/remotes/six/HEAD HEAD && mkdir six && cd six && diff --git a/t/t5513-fetch-track.sh b/t/t5513-fetch-track.sh index 9e7486274b..65d1e05bd6 100755 --- a/t/t5513-fetch-track.sh +++ b/t/t5513-fetch-track.sh @@ -1,6 +1,6 @@ #!/bin/sh -test_description='fetch follows remote tracking branches correctly' +test_description='fetch follows remote-tracking branches correctly' . ./test-lib.sh diff --git a/t/t5514-fetch-multiple.sh b/t/t5514-fetch-multiple.sh index b73733219d..227dd56137 100755 --- a/t/t5514-fetch-multiple.sh +++ b/t/t5514-fetch-multiple.sh @@ -27,7 +27,7 @@ test_expect_success setup ' ( cd two && git branch another ) && - git clone --mirror two three + git clone --mirror two three && git clone one test ' diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh index b11da79c9c..d73731e644 100755 --- a/t/t5516-fetch-push.sh +++ b/t/t5516-fetch-push.sh @@ -586,7 +586,7 @@ test_expect_success 'push --delete refuses src:dest refspecs' ' ' test_expect_success 'warn on push to HEAD of non-bare repository' ' - mk_test heads/master + mk_test heads/master && ( cd testrepo && git checkout master && @@ -597,7 +597,7 @@ test_expect_success 'warn on push to HEAD of non-bare repository' ' ' test_expect_success 'deny push to HEAD of non-bare repository' ' - mk_test heads/master + mk_test heads/master && ( cd testrepo && git checkout master && @@ -607,7 +607,7 @@ test_expect_success 'deny push to HEAD of non-bare repository' ' ' test_expect_success 'allow push to HEAD of bare repository (bare)' ' - mk_test heads/master + mk_test heads/master && ( cd testrepo && git checkout master && @@ -619,7 +619,7 @@ test_expect_success 'allow push to HEAD of bare repository (bare)' ' ' test_expect_success 'allow push to HEAD of non-bare repository (config)' ' - mk_test heads/master + mk_test heads/master && ( cd testrepo && git checkout master && diff --git a/t/t5519-push-alternates.sh b/t/t5519-push-alternates.sh index 96be5236a2..c00c9b071d 100755 --- a/t/t5519-push-alternates.sh +++ b/t/t5519-push-alternates.sh @@ -123,7 +123,7 @@ test_expect_success 'bob works and pushes again' ' ( cd alice-pub && git cat-file commit master >../bob-work/commit - ) + ) && ( # This time Bob does not pull from Alice, and # the master branch at her public repository points diff --git a/t/t5520-pull.sh b/t/t5520-pull.sh index 0b489f5b12..0470a81be0 100755 --- a/t/t5520-pull.sh +++ b/t/t5520-pull.sh @@ -222,4 +222,11 @@ test_expect_success 'git pull --rebase does not reapply old patches' ' ) ' +test_expect_success 'git pull --rebase against local branch' ' + git checkout -b copy2 to-rebase-orig && + git pull --rebase . to-rebase && + test "conflicting modification" = "$(cat file)" && + test file = "$(cat file2)" +' + test_done diff --git a/t/t5531-deep-submodule-push.sh b/t/t5531-deep-submodule-push.sh index 65d8d474bc..faa2e96337 100755 --- a/t/t5531-deep-submodule-push.sh +++ b/t/t5531-deep-submodule-push.sh @@ -6,7 +6,7 @@ test_description='unpack-objects' test_expect_success setup ' mkdir pub.git && - GIT_DIR=pub.git git init --bare + GIT_DIR=pub.git git init --bare && GIT_DIR=pub.git git config receive.fsckobjects true && mkdir work && ( diff --git a/t/t5550-http-fetch.sh b/t/t5550-http-fetch.sh index 2fb48d09ed..8c2ac353b7 100755 --- a/t/t5550-http-fetch.sh +++ b/t/t5550-http-fetch.sh @@ -34,6 +34,13 @@ test_expect_success 'clone http repository' ' test_cmp file clone/file ' +test_expect_success 'clone http repository with authentication' ' + mkdir "$HTTPD_DOCUMENT_ROOT_PATH/auth/" && + cp -Rf "$HTTPD_DOCUMENT_ROOT_PATH/repo.git" "$HTTPD_DOCUMENT_ROOT_PATH/auth/repo.git" && + git clone $AUTH_HTTPD_URL/auth/repo.git clone-auth && + test_cmp file clone-auth/file +' + test_expect_success 'fetch changes via http' ' echo content >>file && git commit -a -m two && diff --git a/t/t5551-http-fetch.sh b/t/t5551-http-fetch.sh index fd19121372..26d355725f 100755 --- a/t/t5551-http-fetch.sh +++ b/t/t5551-http-fetch.sh @@ -101,5 +101,13 @@ test_expect_success 'used upload-pack service' ' test_cmp exp act ' +test_expect_success 'follow redirects (301)' ' + git clone $HTTPD_URL/smart-redir-perm/repo.git --quiet repo-p +' + +test_expect_success 'follow redirects (302)' ' + git clone $HTTPD_URL/smart-redir-temp/repo.git --quiet repo-t +' + stop_httpd test_done diff --git a/t/t556x_common b/t/t556x_common index 51287d89d8..82926cfdb7 100755 --- a/t/t556x_common +++ b/t/t556x_common @@ -52,21 +52,21 @@ get_static_files() { SMART=smart GIT_HTTP_EXPORT_ALL=1 && export GIT_HTTP_EXPORT_ALL test_expect_success 'direct refs/heads/master not found' ' - log_div "refs/heads/master" + log_div "refs/heads/master" && GET refs/heads/master "404 Not Found" ' test_expect_success 'static file is ok' ' - log_div "getanyfile default" + log_div "getanyfile default" && get_static_files "200 OK" ' SMART=smart_noexport unset GIT_HTTP_EXPORT_ALL test_expect_success 'no export by default' ' - log_div "no git-daemon-export-ok" + log_div "no git-daemon-export-ok" && get_static_files "404 Not Found" ' test_expect_success 'export if git-daemon-export-ok' ' - log_div "git-daemon-export-ok" + log_div "git-daemon-export-ok" && (cd "$HTTPD_DOCUMENT_ROOT_PATH/repo.git" && touch git-daemon-export-ok ) && @@ -75,47 +75,47 @@ test_expect_success 'export if git-daemon-export-ok' ' SMART=smart GIT_HTTP_EXPORT_ALL=1 && export GIT_HTTP_EXPORT_ALL test_expect_success 'static file if http.getanyfile true is ok' ' - log_div "getanyfile true" + log_div "getanyfile true" && config http.getanyfile true && get_static_files "200 OK" ' test_expect_success 'static file if http.getanyfile false fails' ' - log_div "getanyfile false" + log_div "getanyfile false" && config http.getanyfile false && get_static_files "403 Forbidden" ' test_expect_success 'http.uploadpack default enabled' ' - log_div "uploadpack default" + log_div "uploadpack default" && GET info/refs?service=git-upload-pack "200 OK" && POST git-upload-pack 0000 "200 OK" ' test_expect_success 'http.uploadpack true' ' - log_div "uploadpack true" + log_div "uploadpack true" && config http.uploadpack true && GET info/refs?service=git-upload-pack "200 OK" && POST git-upload-pack 0000 "200 OK" ' test_expect_success 'http.uploadpack false' ' - log_div "uploadpack false" + log_div "uploadpack false" && config http.uploadpack false && GET info/refs?service=git-upload-pack "403 Forbidden" && POST git-upload-pack 0000 "403 Forbidden" ' test_expect_success 'http.receivepack default disabled' ' - log_div "receivepack default" + log_div "receivepack default" && GET info/refs?service=git-receive-pack "403 Forbidden" && POST git-receive-pack 0000 "403 Forbidden" ' test_expect_success 'http.receivepack true' ' - log_div "receivepack true" + log_div "receivepack true" && config http.receivepack true && GET info/refs?service=git-receive-pack "200 OK" && POST git-receive-pack 0000 "200 OK" ' test_expect_success 'http.receivepack false' ' - log_div "receivepack false" + log_div "receivepack false" && config http.receivepack false && GET info/refs?service=git-receive-pack "403 Forbidden" && POST git-receive-pack 0000 "403 Forbidden" diff --git a/t/t5602-clone-remote-exec.sh b/t/t5602-clone-remote-exec.sh index deffdaee49..3f353d99e8 100755 --- a/t/t5602-clone-remote-exec.sh +++ b/t/t5602-clone-remote-exec.sh @@ -5,21 +5,29 @@ test_description=clone . ./test-lib.sh test_expect_success setup ' - echo "#!/bin/sh" > not_ssh - echo "echo \"\$*\" > not_ssh_output" >> not_ssh - echo "exit 1" >> not_ssh + echo "#!/bin/sh" > not_ssh && + echo "echo \"\$*\" > not_ssh_output" >> not_ssh && + echo "exit 1" >> not_ssh && chmod +x not_ssh ' test_expect_success 'clone calls git upload-pack unqualified with no -u option' ' - GIT_SSH=./not_ssh git clone localhost:/path/to/repo junk - echo "localhost git-upload-pack '\''/path/to/repo'\''" >expected + ( + GIT_SSH=./not_ssh && + export GIT_SSH && + test_must_fail git clone localhost:/path/to/repo junk + ) && + echo "localhost git-upload-pack '\''/path/to/repo'\''" >expected && test_cmp expected not_ssh_output ' test_expect_success 'clone calls specified git upload-pack with -u option' ' - GIT_SSH=./not_ssh git clone -u ./something/bin/git-upload-pack localhost:/path/to/repo junk - echo "localhost ./something/bin/git-upload-pack '\''/path/to/repo'\''" >expected + ( + GIT_SSH=./not_ssh && + export GIT_SSH && + test_must_fail git clone -u ./something/bin/git-upload-pack localhost:/path/to/repo junk + ) && + echo "localhost ./something/bin/git-upload-pack '\''/path/to/repo'\''" >expected && test_cmp expected not_ssh_output ' diff --git a/t/t5701-clone-local.sh b/t/t5701-clone-local.sh index 8b4c356cd2..0f4d487be3 100755 --- a/t/t5701-clone-local.sh +++ b/t/t5701-clone-local.sh @@ -10,11 +10,11 @@ test_expect_success 'preparing origin repository' ' git clone --bare . a.git && git clone --bare . x && test "$(GIT_CONFIG=a.git/config git config --bool core.bare)" = true && - test "$(GIT_CONFIG=x/config git config --bool core.bare)" = true + test "$(GIT_CONFIG=x/config git config --bool core.bare)" = true && git bundle create b1.bundle --all && git bundle create b2.bundle master && mkdir dir && - cp b1.bundle dir/b3 + cp b1.bundle dir/b3 && cp b1.bundle b4 ' @@ -112,7 +112,7 @@ test_expect_success 'bundle clone with nonexistent HEAD' ' cd "$D" && git clone b2.bundle b2 && cd b2 && - git fetch + git fetch && test ! -e .git/refs/heads/master ' diff --git a/t/t6001-rev-list-graft.sh b/t/t6001-rev-list-graft.sh index fc57e7d3fd..8efcd13079 100755 --- a/t/t6001-rev-list-graft.sh +++ b/t/t6001-rev-list-graft.sh @@ -90,22 +90,22 @@ check () { for type in basic parents parents-raw do test_expect_success 'without grafts' " - rm -f .git/info/grafts + rm -f .git/info/grafts && check $type $B2 -- $B2 $B1 $B0 " test_expect_success 'with grafts' " - echo '$B0 $A2' >.git/info/grafts + echo '$B0 $A2' >.git/info/grafts && check $type $B2 -- $B2 $B1 $B0 $A2 $A1 $A0 " test_expect_success 'without grafts, with pathlimit' " - rm -f .git/info/grafts + rm -f .git/info/grafts && check $type $B2 subdir -- $B2 $B0 " test_expect_success 'with grafts, with pathlimit' " - echo '$B0 $A2' >.git/info/grafts + echo '$B0 $A2' >.git/info/grafts && check $type $B2 subdir -- $B2 $B0 $A2 $A0 " diff --git a/t/t6009-rev-list-parent.sh b/t/t6009-rev-list-parent.sh index c8a96a9a99..52f7b277ce 100755 --- a/t/t6009-rev-list-parent.sh +++ b/t/t6009-rev-list-parent.sh @@ -18,7 +18,7 @@ test_expect_success setup ' commit one && - test_tick=$(($test_tick - 2400)) + test_tick=$(($test_tick - 2400)) && commit two && commit three && diff --git a/t/t6010-merge-base.sh b/t/t6010-merge-base.sh index 62197a3d35..082032edc3 100755 --- a/t/t6010-merge-base.sh +++ b/t/t6010-merge-base.sh @@ -131,7 +131,7 @@ test_expect_success 'unsynchronized clocks' ' R2=$(doit 3 R2 $R1) && PL=$(doit 4 PL $L2 $C2) && - PR=$(doit 4 PR $C2 $R2) + PR=$(doit 4 PR $C2 $R2) && git name-rev $C2 >expected && diff --git a/t/t6016-rev-list-graph-simplify-history.sh b/t/t6016-rev-list-graph-simplify-history.sh index 27fd52b7be..f7181d1d6a 100755 --- a/t/t6016-rev-list-graph-simplify-history.sh +++ b/t/t6016-rev-list-graph-simplify-history.sh @@ -29,7 +29,7 @@ test_expect_success 'set up rev-list --graph test' ' # Octopus merge B and C into branch A git checkout A && git merge B C && - git tag A4 + git tag A4 && test_commit A5 bar.txt && @@ -39,7 +39,7 @@ test_expect_success 'set up rev-list --graph test' ' test_commit C4 bar.txt && git checkout A && git merge -s ours C && - git tag A6 + git tag A6 && test_commit A7 bar.txt && @@ -90,7 +90,7 @@ test_expect_success '--graph --all' ' # that undecorated merges are interesting, even with --simplify-by-decoration test_expect_success '--graph --simplify-by-decoration' ' rm -f expected && - git tag -d A4 + git tag -d A4 && echo "* $A7" >> expected && echo "* $A6" >> expected && echo "|\\ " >> expected && @@ -116,12 +116,15 @@ test_expect_success '--graph --simplify-by-decoration' ' test_cmp expected actual ' -# Get rid of all decorations on branch B, and graph with it simplified away +test_expect_success 'setup: get rid of decorations on B' ' + git tag -d B2 && + git tag -d B1 && + git branch -d B +' + +# Graph with branch B simplified away test_expect_success '--graph --simplify-by-decoration prune branch B' ' rm -f expected && - git tag -d B2 - git tag -d B1 - git branch -d B echo "* $A7" >> expected && echo "* $A6" >> expected && echo "|\\ " >> expected && @@ -143,9 +146,6 @@ test_expect_success '--graph --simplify-by-decoration prune branch B' ' test_expect_success '--graph --full-history -- bar.txt' ' rm -f expected && - git tag -d B2 - git tag -d B1 - git branch -d B echo "* $A7" >> expected && echo "* $A6" >> expected && echo "|\\ " >> expected && @@ -163,9 +163,6 @@ test_expect_success '--graph --full-history -- bar.txt' ' test_expect_success '--graph --full-history --simplify-merges -- bar.txt' ' rm -f expected && - git tag -d B2 - git tag -d B1 - git branch -d B echo "* $A7" >> expected && echo "* $A6" >> expected && echo "|\\ " >> expected && @@ -181,9 +178,6 @@ test_expect_success '--graph --full-history --simplify-merges -- bar.txt' ' test_expect_success '--graph -- bar.txt' ' rm -f expected && - git tag -d B2 - git tag -d B1 - git branch -d B echo "* $A7" >> expected && echo "* $A5" >> expected && echo "* $A3" >> expected && @@ -196,9 +190,6 @@ test_expect_success '--graph -- bar.txt' ' test_expect_success '--graph --sparse -- bar.txt' ' rm -f expected && - git tag -d B2 - git tag -d B1 - git branch -d B echo "* $A7" >> expected && echo "* $A6" >> expected && echo "* $A5" >> expected && diff --git a/t/t6020-merge-df.sh b/t/t6020-merge-df.sh index 490d397114..eec8f4e3ed 100755 --- a/t/t6020-merge-df.sh +++ b/t/t6020-merge-df.sh @@ -6,21 +6,26 @@ test_description='Test merge with directory/file conflicts' . ./test-lib.sh -test_expect_success 'prepare repository' \ -'echo "Hello" > init && -git add init && -git commit -m "Initial commit" && -git branch B && -mkdir dir && -echo "foo" > dir/foo && -git add dir/foo && -git commit -m "File: dir/foo" && -git checkout B && -echo "file dir" > dir && -git add dir && -git commit -m "File: dir"' - -test_expect_code 1 'Merge with d/f conflicts' 'git merge "merge msg" B master' +test_expect_success 'prepare repository' ' + echo Hello >init && + git add init && + git commit -m initial && + + git branch B && + mkdir dir && + echo foo >dir/foo && + git add dir/foo && + git commit -m "File: dir/foo" && + + git checkout B && + echo file dir >dir && + git add dir && + git commit -m "File: dir" +' + +test_expect_success 'Merge with d/f conflicts' ' + test_expect_code 1 git merge "merge msg" B master +' test_expect_success 'F/D conflict' ' git reset --hard && @@ -45,4 +50,51 @@ test_expect_success 'F/D conflict' ' git merge master ' +test_expect_success 'setup modify/delete + directory/file conflict' ' + git checkout --orphan modify && + git rm -rf . && + git clean -fdqx && + + printf "a\nb\nc\nd\ne\nf\ng\nh\n" >letters && + git add letters && + git commit -m initial && + + echo i >>letters && + git add letters && + git commit -m modified && + + git checkout -b delete HEAD^ && + git rm letters && + mkdir letters && + >letters/file && + git add letters && + git commit -m deleted +' + +test_expect_success 'modify/delete + directory/file conflict' ' + git checkout delete^0 && + test_must_fail git merge modify && + + test 3 = $(git ls-files -s | wc -l) && + test 2 = $(git ls-files -u | wc -l) && + test 1 = $(git ls-files -o | wc -l) && + + test -f letters/file && + test -f letters~modify +' + +test_expect_success 'modify/delete + directory/file conflict; other way' ' + git reset --hard && + git clean -f && + git checkout modify^0 && + test_must_fail git merge delete && + + test 3 = $(git ls-files -s | wc -l) && + test 2 = $(git ls-files -u | wc -l) && + test 1 = $(git ls-files -o | wc -l) && + + test -f letters/file && + test -f letters~HEAD +' + test_done diff --git a/t/t6022-merge-rename.sh b/t/t6022-merge-rename.sh index b66544b76d..1ed259d864 100755 --- a/t/t6022-merge-rename.sh +++ b/t/t6022-merge-rename.sh @@ -3,6 +3,11 @@ test_description='Merge-recursive merging renames' . ./test-lib.sh +modify () { + sed -e "$1" <"$2" >"$2.x" && + mv "$2.x" "$2" +} + test_expect_success setup \ ' cat >A <<\EOF && @@ -94,245 +99,147 @@ git checkout master' test_expect_success 'pull renaming branch into unrenaming one' \ ' - git show-branch - git pull . white && { - echo "BAD: should have conflicted" - return 1 - } - git ls-files -s - test "$(git ls-files -u B | wc -l)" -eq 3 || { - echo "BAD: should have left stages for B" - return 1 - } - test "$(git ls-files -s N | wc -l)" -eq 1 || { - echo "BAD: should have merged N" - return 1 - } + git show-branch && + test_expect_code 1 git pull . white && + git ls-files -s && + git ls-files -u B >b.stages && + test_line_count = 3 b.stages && + git ls-files -s N >n.stages && + test_line_count = 1 n.stages && sed -ne "/^g/{ p q - }" B | grep master || { - echo "BAD: should have listed our change first" - return 1 - } - test "$(git diff white N | wc -l)" -eq 0 || { - echo "BAD: should have taken colored branch" - return 1 - } + }" B | grep master && + git diff --exit-code white N ' test_expect_success 'pull renaming branch into another renaming one' \ ' - rm -f B - git reset --hard - git checkout red - git pull . white && { - echo "BAD: should have conflicted" - return 1 - } - test "$(git ls-files -u B | wc -l)" -eq 3 || { - echo "BAD: should have left stages" - return 1 - } - test "$(git ls-files -s N | wc -l)" -eq 1 || { - echo "BAD: should have merged N" - return 1 - } + rm -f B && + git reset --hard && + git checkout red && + test_expect_code 1 git pull . white && + git ls-files -u B >b.stages && + test_line_count = 3 b.stages && + git ls-files -s N >n.stages && + test_line_count = 1 n.stages && sed -ne "/^g/{ p q - }" B | grep red || { - echo "BAD: should have listed our change first" - return 1 - } - test "$(git diff white N | wc -l)" -eq 0 || { - echo "BAD: should have taken colored branch" - return 1 - } + }" B | grep red && + git diff --exit-code white N ' test_expect_success 'pull unrenaming branch into renaming one' \ ' - git reset --hard - git show-branch - git pull . master && { - echo "BAD: should have conflicted" - return 1 - } - test "$(git ls-files -u B | wc -l)" -eq 3 || { - echo "BAD: should have left stages" - return 1 - } - test "$(git ls-files -s N | wc -l)" -eq 1 || { - echo "BAD: should have merged N" - return 1 - } + git reset --hard && + git show-branch && + test_expect_code 1 git pull . master && + git ls-files -u B >b.stages && + test_line_count = 3 b.stages && + git ls-files -s N >n.stages && + test_line_count = 1 n.stages && sed -ne "/^g/{ p q - }" B | grep red || { - echo "BAD: should have listed our change first" - return 1 - } - test "$(git diff white N | wc -l)" -eq 0 || { - echo "BAD: should have taken colored branch" - return 1 - } + }" B | grep red && + git diff --exit-code white N ' test_expect_success 'pull conflicting renames' \ ' - git reset --hard - git show-branch - git pull . blue && { - echo "BAD: should have conflicted" - return 1 - } - test "$(git ls-files -u A | wc -l)" -eq 1 || { - echo "BAD: should have left a stage" - return 1 - } - test "$(git ls-files -u B | wc -l)" -eq 1 || { - echo "BAD: should have left a stage" - return 1 - } - test "$(git ls-files -u C | wc -l)" -eq 1 || { - echo "BAD: should have left a stage" - return 1 - } - test "$(git ls-files -s N | wc -l)" -eq 1 || { - echo "BAD: should have merged N" - return 1 - } + git reset --hard && + git show-branch && + test_expect_code 1 git pull . blue && + git ls-files -u A >a.stages && + test_line_count = 1 a.stages && + git ls-files -u B >b.stages && + test_line_count = 1 b.stages && + git ls-files -u C >c.stages && + test_line_count = 1 c.stages && + git ls-files -s N >n.stages && + test_line_count = 1 n.stages && sed -ne "/^g/{ p q - }" B | grep red || { - echo "BAD: should have listed our change first" - return 1 - } - test "$(git diff white N | wc -l)" -eq 0 || { - echo "BAD: should have taken colored branch" - return 1 - } + }" B | grep red && + git diff --exit-code white N ' test_expect_success 'interference with untracked working tree file' ' - - git reset --hard - git show-branch - echo >A this file should not matter - git pull . white && { - echo "BAD: should have conflicted" - return 1 - } - test -f A || { - echo "BAD: should have left A intact" - return 1 - } + git reset --hard && + git show-branch && + echo >A this file should not matter && + test_expect_code 1 git pull . white && + test_path_is_file A ' test_expect_success 'interference with untracked working tree file' ' - - git reset --hard - git checkout white - git show-branch - rm -f A - echo >A this file should not matter - git pull . red && { - echo "BAD: should have conflicted" - return 1 - } - test -f A || { - echo "BAD: should have left A intact" - return 1 - } + git reset --hard && + git checkout white && + git show-branch && + rm -f A && + echo >A this file should not matter && + test_expect_code 1 git pull . red && + test_path_is_file A ' test_expect_success 'interference with untracked working tree file' ' - - git reset --hard - rm -f A M - git checkout -f master - git tag -f anchor - git show-branch - git pull . yellow || { - echo "BAD: should have cleanly merged" - return 1 - } - test -f M && { - echo "BAD: should have removed M" - return 1 - } + git reset --hard && + rm -f A M && + git checkout -f master && + git tag -f anchor && + git show-branch && + git pull . yellow && + test_path_is_missing M && git reset --hard anchor ' test_expect_success 'updated working tree file should prevent the merge' ' - - git reset --hard - rm -f A M - git checkout -f master - git tag -f anchor - git show-branch - echo >>M one line addition - cat M >M.saved - git pull . yellow && { - echo "BAD: should have complained" - return 1 - } - test_cmp M M.saved || { - echo "BAD: should have left M intact" - return 1 - } + git reset --hard && + rm -f A M && + git checkout -f master && + git tag -f anchor && + git show-branch && + echo >>M one line addition && + cat M >M.saved && + test_expect_code 128 git pull . yellow && + test_cmp M M.saved && rm -f M.saved ' test_expect_success 'updated working tree file should prevent the merge' ' - - git reset --hard - rm -f A M - git checkout -f master - git tag -f anchor - git show-branch - echo >>M one line addition - cat M >M.saved - git update-index M - git pull . yellow && { - echo "BAD: should have complained" - return 1 - } - test_cmp M M.saved || { - echo "BAD: should have left M intact" - return 1 - } + git reset --hard && + rm -f A M && + git checkout -f master && + git tag -f anchor && + git show-branch && + echo >>M one line addition && + cat M >M.saved && + git update-index M && + test_expect_code 128 git pull . yellow && + test_cmp M M.saved && rm -f M.saved ' test_expect_success 'interference with untracked working tree file' ' - - git reset --hard - rm -f A M - git checkout -f yellow - git tag -f anchor - git show-branch - echo >M this file should not matter - git pull . master || { - echo "BAD: should have cleanly merged" - return 1 - } - test -f M || { - echo "BAD: should have left M intact" - return 1 - } - git ls-files -s | grep M && { - echo "BAD: M must be untracked in the result" - return 1 - } + git reset --hard && + rm -f A M && + git checkout -f yellow && + git tag -f anchor && + git show-branch && + echo >M this file should not matter && + git pull . master && + test_path_is_file M && + ! { + git ls-files -s | + grep M + } && git reset --hard anchor ' test_expect_success 'merge of identical changes in a renamed file' ' - rm -f A M N + rm -f A M N && git reset --hard && git checkout change+rename && GIT_MERGE_VERBOSITY=3 git merge change | grep "^Skipped B" && @@ -341,4 +248,365 @@ test_expect_success 'merge of identical changes in a renamed file' ' GIT_MERGE_VERBOSITY=3 git merge change+rename | grep "^Skipped B" ' +test_expect_success 'setup for rename + d/f conflicts' ' + git reset --hard && + git checkout --orphan dir-in-way && + git rm -rf . && + + mkdir sub && + mkdir dir && + printf "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n" >sub/file && + echo foo >dir/file-in-the-way && + git add -A && + git commit -m "Common commmit" && + + echo 11 >>sub/file && + echo more >>dir/file-in-the-way && + git add -u && + git commit -m "Commit to merge, with dir in the way" && + + git checkout -b dir-not-in-way && + git reset --soft HEAD^ && + git rm -rf dir && + git commit -m "Commit to merge, with dir removed" -- dir sub/file && + + git checkout -b renamed-file-has-no-conflicts dir-in-way~1 && + git rm -rf dir && + git rm sub/file && + printf "1\n2\n3\n4\n5555\n6\n7\n8\n9\n10\n" >dir && + git add dir && + git commit -m "Independent change" && + + git checkout -b renamed-file-has-conflicts dir-in-way~1 && + git rm -rf dir && + git mv sub/file dir && + echo 12 >>dir && + git add dir && + git commit -m "Conflicting change" +' + +printf "1\n2\n3\n4\n5555\n6\n7\n8\n9\n10\n11\n" >expected + +test_expect_success 'Rename+D/F conflict; renamed file merges + dir not in way' ' + git reset --hard && + git checkout -q renamed-file-has-no-conflicts^0 && + git merge --strategy=recursive dir-not-in-way && + git diff --quiet && + test -f dir && + test_cmp expected dir +' + +test_expect_success 'Rename+D/F conflict; renamed file merges but dir in way' ' + git reset --hard && + rm -rf dir~* && + git checkout -q renamed-file-has-no-conflicts^0 && + test_must_fail git merge --strategy=recursive dir-in-way >output && + + grep "CONFLICT (delete/modify): dir/file-in-the-way" output && + grep "Auto-merging dir" output && + grep "Adding as dir~HEAD instead" output && + + test 2 -eq "$(git ls-files -u | wc -l)" && + test 2 -eq "$(git ls-files -u dir/file-in-the-way | wc -l)" && + + test_must_fail git diff --quiet && + test_must_fail git diff --cached --quiet && + + test -f dir/file-in-the-way && + test -f dir~HEAD && + test_cmp expected dir~HEAD +' + +test_expect_success 'Same as previous, but merged other way' ' + git reset --hard && + rm -rf dir~* && + git checkout -q dir-in-way^0 && + test_must_fail git merge --strategy=recursive renamed-file-has-no-conflicts >output 2>errors && + + ! grep "error: refusing to lose untracked file at" errors && + grep "CONFLICT (delete/modify): dir/file-in-the-way" output && + grep "Auto-merging dir" output && + grep "Adding as dir~renamed-file-has-no-conflicts instead" output && + + test 2 -eq "$(git ls-files -u | wc -l)" && + test 2 -eq "$(git ls-files -u dir/file-in-the-way | wc -l)" && + + test_must_fail git diff --quiet && + test_must_fail git diff --cached --quiet && + + test -f dir/file-in-the-way && + test -f dir~renamed-file-has-no-conflicts && + test_cmp expected dir~renamed-file-has-no-conflicts +' + +cat >expected <<\EOF && +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +<<<<<<< HEAD +12 +======= +11 +>>>>>>> dir-not-in-way +EOF + +test_expect_success 'Rename+D/F conflict; renamed file cannot merge, dir not in way' ' + git reset --hard && + rm -rf dir~* && + git checkout -q renamed-file-has-conflicts^0 && + test_must_fail git merge --strategy=recursive dir-not-in-way && + + test 3 -eq "$(git ls-files -u | wc -l)" && + test 3 -eq "$(git ls-files -u dir | wc -l)" && + + test_must_fail git diff --quiet && + test_must_fail git diff --cached --quiet && + + test -f dir && + test_cmp expected dir +' + +test_expect_success 'Rename+D/F conflict; renamed file cannot merge and dir in the way' ' + modify s/dir-not-in-way/dir-in-way/ expected && + + git reset --hard && + rm -rf dir~* && + git checkout -q renamed-file-has-conflicts^0 && + test_must_fail git merge --strategy=recursive dir-in-way && + + test 5 -eq "$(git ls-files -u | wc -l)" && + test 3 -eq "$(git ls-files -u dir | grep -v file-in-the-way | wc -l)" && + test 2 -eq "$(git ls-files -u dir/file-in-the-way | wc -l)" && + + test_must_fail git diff --quiet && + test_must_fail git diff --cached --quiet && + + test -f dir/file-in-the-way && + test -f dir~HEAD && + test_cmp expected dir~HEAD +' + +cat >expected <<\EOF && +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +<<<<<<< HEAD +11 +======= +12 +>>>>>>> renamed-file-has-conflicts +EOF + +test_expect_success 'Same as previous, but merged other way' ' + git reset --hard && + rm -rf dir~* && + git checkout -q dir-in-way^0 && + test_must_fail git merge --strategy=recursive renamed-file-has-conflicts && + + test 5 -eq "$(git ls-files -u | wc -l)" && + test 3 -eq "$(git ls-files -u dir | grep -v file-in-the-way | wc -l)" && + test 2 -eq "$(git ls-files -u dir/file-in-the-way | wc -l)" && + + test_must_fail git diff --quiet && + test_must_fail git diff --cached --quiet && + + test -f dir/file-in-the-way && + test -f dir~renamed-file-has-conflicts && + test_cmp expected dir~renamed-file-has-conflicts +' + +test_expect_success 'setup both rename source and destination involved in D/F conflict' ' + git reset --hard && + git checkout --orphan rename-dest && + git rm -rf . && + git clean -fdqx && + + mkdir one && + echo stuff >one/file && + git add -A && + git commit -m "Common commmit" && + + git mv one/file destdir && + git commit -m "Renamed to destdir" && + + git checkout -b source-conflict HEAD~1 && + git rm -rf one && + mkdir destdir && + touch one destdir/foo && + git add -A && + git commit -m "Conflicts in the way" +' + +test_expect_success 'both rename source and destination involved in D/F conflict' ' + git reset --hard && + rm -rf dir~* && + git checkout -q rename-dest^0 && + test_must_fail git merge --strategy=recursive source-conflict && + + test 1 -eq "$(git ls-files -u | wc -l)" && + + test_must_fail git diff --quiet && + + test -f destdir/foo && + test -f one && + test -f destdir~HEAD && + test "stuff" = "$(cat destdir~HEAD)" +' + +test_expect_success 'setup pair rename to parent of other (D/F conflicts)' ' + git reset --hard && + git checkout --orphan rename-two && + git rm -rf . && + git clean -fdqx && + + mkdir one && + mkdir two && + echo stuff >one/file && + echo other >two/file && + git add -A && + git commit -m "Common commmit" && + + git rm -rf one && + git mv two/file one && + git commit -m "Rename two/file -> one" && + + git checkout -b rename-one HEAD~1 && + git rm -rf two && + git mv one/file two && + rm -r one && + git commit -m "Rename one/file -> two" +' + +test_expect_success 'pair rename to parent of other (D/F conflicts) w/ untracked dir' ' + git checkout -q rename-one^0 && + mkdir one && + test_must_fail git merge --strategy=recursive rename-two && + + test 2 -eq "$(git ls-files -u | wc -l)" && + test 1 -eq "$(git ls-files -u one | wc -l)" && + test 1 -eq "$(git ls-files -u two | wc -l)" && + + test_must_fail git diff --quiet && + + test 4 -eq $(find . | grep -v .git | wc -l) && + + test -d one && + test -f one~rename-two && + test -f two && + test "other" = $(cat one~rename-two) && + test "stuff" = $(cat two) +' + +test_expect_success 'pair rename to parent of other (D/F conflicts) w/ clean start' ' + git reset --hard && + git clean -fdqx && + test_must_fail git merge --strategy=recursive rename-two && + + test 2 -eq "$(git ls-files -u | wc -l)" && + test 1 -eq "$(git ls-files -u one | wc -l)" && + test 1 -eq "$(git ls-files -u two | wc -l)" && + + test_must_fail git diff --quiet && + + test 3 -eq $(find . | grep -v .git | wc -l) && + + test -f one && + test -f two && + test "other" = $(cat one) && + test "stuff" = $(cat two) +' + +test_expect_success 'setup rename of one file to two, with directories in the way' ' + git reset --hard && + git checkout --orphan first-rename && + git rm -rf . && + git clean -fdqx && + + echo stuff >original && + git add -A && + git commit -m "Common commmit" && + + mkdir two && + >two/file && + git add two/file && + git mv original one && + git commit -m "Put two/file in the way, rename to one" && + + git checkout -b second-rename HEAD~1 && + mkdir one && + >one/file && + git add one/file && + git mv original two && + git commit -m "Put one/file in the way, rename to two" +' + +test_expect_success 'check handling of differently renamed file with D/F conflicts' ' + git checkout -q first-rename^0 && + test_must_fail git merge --strategy=recursive second-rename && + + test 5 -eq "$(git ls-files -s | wc -l)" && + test 3 -eq "$(git ls-files -u | wc -l)" && + test 1 -eq "$(git ls-files -u one | wc -l)" && + test 1 -eq "$(git ls-files -u two | wc -l)" && + test 1 -eq "$(git ls-files -u original | wc -l)" && + test 2 -eq "$(git ls-files -o | wc -l)" && + + test -f one/file && + test -f two/file && + test -f one~HEAD && + test -f two~second-rename && + ! test -f original +' + +test_expect_success 'setup rename one file to two; directories moving out of the way' ' + git reset --hard && + git checkout --orphan first-rename-redo && + git rm -rf . && + git clean -fdqx && + + echo stuff >original && + mkdir one two && + touch one/file two/file && + git add -A && + git commit -m "Common commmit" && + + git rm -rf one && + git mv original one && + git commit -m "Rename to one" && + + git checkout -b second-rename-redo HEAD~1 && + git rm -rf two && + git mv original two && + git commit -m "Rename to two" +' + +test_expect_success 'check handling of differently renamed file with D/F conflicts' ' + git checkout -q first-rename-redo^0 && + test_must_fail git merge --strategy=recursive second-rename-redo && + + test 3 -eq "$(git ls-files -u | wc -l)" && + test 1 -eq "$(git ls-files -u one | wc -l)" && + test 1 -eq "$(git ls-files -u two | wc -l)" && + test 1 -eq "$(git ls-files -u original | wc -l)" && + test 0 -eq "$(git ls-files -o | wc -l)" && + + test -f one && + test -f two && + ! test -f original +' + test_done diff --git a/t/t6024-recursive-merge.sh b/t/t6024-recursive-merge.sh index b3fbf659c0..755d30ce2a 100755 --- a/t/t6024-recursive-merge.sh +++ b/t/t6024-recursive-merge.sh @@ -104,7 +104,7 @@ test_expect_success 'mark rename/delete as unmerged' ' test_tick && git commit -m delete && git checkout -b rename HEAD^ && - git mv a1 a2 + git mv a1 a2 && test_tick && git commit -m rename && test_must_fail git merge delete && diff --git a/t/t6029-merge-subtree.sh b/t/t6029-merge-subtree.sh index 3900d9f61f..73fc240e85 100755 --- a/t/t6029-merge-subtree.sh +++ b/t/t6029-merge-subtree.sh @@ -6,7 +6,7 @@ test_description='subtree merge strategy' test_expect_success setup ' - s="1 2 3 4 5 6 7 8" + s="1 2 3 4 5 6 7 8" && for i in $s; do echo $i; done >hello && git add hello && git commit -m initial && diff --git a/t/t6030-bisect-porcelain.sh b/t/t6030-bisect-porcelain.sh index 3b042aacd6..b5063b6fe6 100755 --- a/t/t6030-bisect-porcelain.sh +++ b/t/t6030-bisect-porcelain.sh @@ -517,13 +517,13 @@ test_expect_success '"parallel" side branch creation' ' add_line_into_file "2(para): line 2 on parallel branch" dir2/file2 && PARA_HASH2=$(git rev-parse --verify HEAD) && add_line_into_file "3(para): line 3 on parallel branch" dir2/file3 && - PARA_HASH3=$(git rev-parse --verify HEAD) + PARA_HASH3=$(git rev-parse --verify HEAD) && git merge -m "merge HASH4 and PARA_HASH3" "$HASH4" && - PARA_HASH4=$(git rev-parse --verify HEAD) + PARA_HASH4=$(git rev-parse --verify HEAD) && add_line_into_file "5(para): add line on parallel branch" dir1/file1 && - PARA_HASH5=$(git rev-parse --verify HEAD) + PARA_HASH5=$(git rev-parse --verify HEAD) && add_line_into_file "6(para): add line on parallel branch" dir2/file2 && - PARA_HASH6=$(git rev-parse --verify HEAD) + PARA_HASH6=$(git rev-parse --verify HEAD) && git merge -m "merge HASH7 and PARA_HASH6" "$HASH7" && PARA_HASH7=$(git rev-parse --verify HEAD) ' diff --git a/t/t6032-merge-large-rename.sh b/t/t6032-merge-large-rename.sh index eac5ebac24..fdb6c25371 100755 --- a/t/t6032-merge-large-rename.sh +++ b/t/t6032-merge-large-rename.sh @@ -70,4 +70,34 @@ test_expect_success 'set merge.renamelimit to 5' ' test_rename 5 ok test_rename 6 fail +test_expect_success 'setup large simple rename' ' + git config --unset merge.renamelimit && + git config --unset diff.renamelimit && + + git reset --hard initial && + for i in $(count 200); do + make_text foo bar baz >$i + done && + git add . && + git commit -m create-files && + + git branch simple-change && + git checkout -b simple-rename && + + mkdir builtin && + git mv [0-9]* builtin/ && + git commit -m renamed && + + git checkout simple-change && + >unrelated-change && + git add unrelated-change && + git commit -m unrelated-change +' + +test_expect_success 'massive simple rename does not spam added files' ' + unset GIT_MERGE_VERBOSITY && + git merge --no-stat simple-rename | grep -v Removing >output && + test 5 -gt "$(wc -l < output)" +' + test_done diff --git a/t/t6036-recursive-corner-cases.sh b/t/t6036-recursive-corner-cases.sh index b874141658..871577d90c 100755 --- a/t/t6036-recursive-corner-cases.sh +++ b/t/t6036-recursive-corner-cases.sh @@ -14,7 +14,80 @@ test_description='recursive merge corner cases' # R1 R2 # -test_expect_success setup ' +test_expect_success 'setup basic criss-cross + rename with no modifications' ' + ten="0 1 2 3 4 5 6 7 8 9" && + for i in $ten + do + echo line $i in a sample file + done >one && + for i in $ten + do + echo line $i in another sample file + done >two && + git add one two && + test_tick && git commit -m initial && + + git branch L1 && + git checkout -b R1 && + git mv one three && + test_tick && git commit -m R1 && + + git checkout L1 && + git mv two three && + test_tick && git commit -m L1 && + + git checkout L1^0 && + test_tick && git merge -s ours R1 && + git tag L2 && + + git checkout R1^0 && + test_tick && git merge -s ours L1 && + git tag R2 +' + +test_expect_success 'merge simple rename+criss-cross with no modifications' ' + git reset --hard && + git checkout L2^0 && + + test_must_fail git merge -s recursive R2^0 && + + test 5 = $(git ls-files -s | wc -l) && + test 3 = $(git ls-files -u | wc -l) && + test 0 = $(git ls-files -o | wc -l) && + + test $(git rev-parse :0:one) = $(git rev-parse L2:one) && + test $(git rev-parse :0:two) = $(git rev-parse R2:two) && + test $(git rev-parse :2:three) = $(git rev-parse L2:three) && + test $(git rev-parse :3:three) = $(git rev-parse R2:three) && + + cp two merged && + >empty && + test_must_fail git merge-file \ + -L "Temporary merge branch 2" \ + -L "" \ + -L "Temporary merge branch 1" \ + merged empty one && + test $(git rev-parse :1:three) = $(git hash-object merged) +' + +# +# Same as before, but modify L1 slightly: +# +# L1m L2 +# o---o +# / \ / \ +# o X ? +# \ / \ / +# o---o +# R1 R2 +# + +test_expect_success 'setup criss-cross + rename merges with basic modification' ' + git rm -rf . && + git clean -fdqx && + rm -rf .git && + git init && + ten="0 1 2 3 4 5 6 7 8 9" for i in $ten do @@ -30,6 +103,8 @@ test_expect_success setup ' git branch L1 && git checkout -b R1 && git mv one three && + echo more >>two && + git add two && test_tick && git commit -m R1 && git checkout L1 && @@ -45,11 +120,115 @@ test_expect_success setup ' git tag R2 ' -test_expect_success merge ' +test_expect_success 'merge criss-cross + rename merges with basic modification' ' git reset --hard && git checkout L2^0 && - test_must_fail git merge -s recursive R2^0 + test_must_fail git merge -s recursive R2^0 && + + test 5 = $(git ls-files -s | wc -l) && + test 3 = $(git ls-files -u | wc -l) && + test 0 = $(git ls-files -o | wc -l) && + + test $(git rev-parse :0:one) = $(git rev-parse L2:one) && + test $(git rev-parse :0:two) = $(git rev-parse R2:two) && + test $(git rev-parse :2:three) = $(git rev-parse L2:three) && + test $(git rev-parse :3:three) = $(git rev-parse R2:three) && + + head -n 10 two >merged && + cp one merge-me && + >empty && + test_must_fail git merge-file \ + -L "Temporary merge branch 2" \ + -L "" \ + -L "Temporary merge branch 1" \ + merged empty merge-me && + test $(git rev-parse :1:three) = $(git hash-object merged) +' + +# +# For the next test, we start with three commits in two lines of development +# which setup a rename/add conflict: +# Commit A: File 'a' exists +# Commit B: Rename 'a' -> 'new_a' +# Commit C: Modify 'a', create different 'new_a' +# Later, two different people merge and resolve differently: +# Commit D: Merge B & C, ignoring separately created 'new_a' +# Commit E: Merge B & C making use of some piece of secondary 'new_a' +# Finally, someone goes to merge D & E. Does git detect the conflict? +# +# B D +# o---o +# / \ / \ +# A o X ? F +# \ / \ / +# o---o +# C E +# + +test_expect_success 'setup differently handled merges of rename/add conflict' ' + git rm -rf . && + git clean -fdqx && + rm -rf .git && + git init && + + printf "0\n1\n2\n3\n4\n5\n6\n7\n8\n9\n" >a && + git add a && + test_tick && git commit -m A && + + git branch B && + git checkout -b C && + echo 10 >>a && + echo "other content" >>new_a && + git add a new_a && + test_tick && git commit -m C && + + git checkout B && + git mv a new_a && + test_tick && git commit -m B && + + git checkout B^0 && + test_must_fail git merge C && + git clean -f && + test_tick && git commit -m D && + git tag D && + + git checkout C^0 && + test_must_fail git merge B && + rm new_a~HEAD new_a && + printf "Incorrectly merged content" >>new_a && + git add -u && + test_tick && git commit -m E && + git tag E +' + +test_expect_success 'git detects differently handled merges conflict' ' + git reset --hard && + git checkout D^0 && + + git merge -s recursive E^0 && { + echo "BAD: should have conflicted" + test "Incorrectly merged content" = "$(cat new_a)" && + echo "BAD: Silently accepted wrong content" + return 1 + } + + test 3 = $(git ls-files -s | wc -l) && + test 3 = $(git ls-files -u | wc -l) && + test 0 = $(git ls-files -o | wc -l) && + + test $(git rev-parse :2:new_a) = $(git rev-parse D:new_a) && + test $(git rev-parse :3:new_a) = $(git rev-parse E:new_a) && + + git cat-file -p B:new_a >>merged && + git cat-file -p C:new_a >>merge-me && + >empty && + test_must_fail git merge-file \ + -L "Temporary merge branch 2" \ + -L "" \ + -L "Temporary merge branch 1" \ + merged empty merge-me && + test $(git rev-parse :1:new_a) = $(git hash-object merged) ' test_done diff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh index 1785e178a4..1e0447f615 100755 --- a/t/t6040-tracking-info.sh +++ b/t/t6040-tracking-info.sh @@ -60,7 +60,7 @@ test_expect_success 'checkout' ' test_expect_success 'checkout with local tracked branch' ' git checkout master && - git checkout follower >actual + git checkout follower >actual && grep "is ahead of" actual ' diff --git a/t/t6050-replace.sh b/t/t6050-replace.sh index 95b180f469..ae2194e07d 100755 --- a/t/t6050-replace.sh +++ b/t/t6050-replace.sh @@ -53,7 +53,7 @@ test_expect_success 'set up buggy branch' ' echo "line 12" >> hello && echo "line 13" >> hello && add_and_commit_file hello "2 more lines" && - HASH6=$(git rev-parse --verify HEAD) + HASH6=$(git rev-parse --verify HEAD) && echo "line 14" >> hello && echo "line 15" >> hello && echo "line 16" >> hello && diff --git a/t/t6500-gc.sh b/t/t6500-gc.sh new file mode 100755 index 0000000000..82f3639937 --- /dev/null +++ b/t/t6500-gc.sh @@ -0,0 +1,28 @@ +#!/bin/sh + +test_description='basic git gc tests +' + +. ./test-lib.sh + +test_expect_success 'gc empty repository' ' + git gc +' + +test_expect_success 'gc --gobbledegook' ' + test_expect_code 129 git gc --nonsense 2>err && + grep "[Uu]sage: git gc" err +' + +test_expect_success 'gc -h with invalid configuration' ' + mkdir broken && + ( + cd broken && + git init && + echo "[gc] pruneexpire = CORRUPT" >>.git/config && + test_expect_code 129 git gc -h >usage 2>&1 + ) && + grep "[Uu]sage" broken/usage +' + +test_done diff --git a/t/t7001-mv.sh b/t/t7001-mv.sh index 65a35d94a0..a845b154e4 100755 --- a/t/t7001-mv.sh +++ b/t/t7001-mv.sh @@ -61,7 +61,7 @@ test_expect_success \ test_expect_success \ 'checking -f on untracked file with existing target' \ 'touch path0/untracked1 && - git mv -f untracked1 path0 + test_must_fail git mv -f untracked1 path0 && test ! -f .git/index.lock && test -f untracked1 && test -f path0/untracked1' @@ -207,7 +207,7 @@ test_expect_success 'git mv should not change sha1 of moved cache entry' ' git init && echo 1 >dirty && git add dirty && - entry="$(git ls-files --stage dirty | cut -f 1)" + entry="$(git ls-files --stage dirty | cut -f 1)" && git mv dirty dirty2 && [ "$entry" = "$(git ls-files --stage dirty2 | cut -f 1)" ] && echo 2 >dirty2 && diff --git a/t/t7004-tag.sh b/t/t7004-tag.sh index 700b556fe8..3e7baaf89f 100755 --- a/t/t7004-tag.sh +++ b/t/t7004-tag.sh @@ -1030,6 +1030,72 @@ test_expect_success GPG \ test_cmp expect actual ' +# usage with rfc1991 signatures +echo "rfc1991" > gpghome/gpg.conf +get_tag_header rfc1991-signed-tag $commit commit $time >expect +echo "RFC1991 signed tag" >>expect +echo '-----BEGIN PGP MESSAGE-----' >>expect +test_expect_success GPG \ + 'creating a signed tag with rfc1991' ' + git tag -s -m "RFC1991 signed tag" rfc1991-signed-tag $commit && + get_tag_msg rfc1991-signed-tag >actual && + test_cmp expect actual +' + +cat >fakeeditor <<'EOF' +#!/bin/sh +cp "$1" actual +EOF +chmod +x fakeeditor + +test_expect_success GPG \ + 'reediting a signed tag body omits signature' ' + echo "RFC1991 signed tag" >expect && + GIT_EDITOR=./fakeeditor git tag -f -s rfc1991-signed-tag $commit && + test_cmp expect actual +' + +test_expect_success GPG \ + 'verifying rfc1991 signature' ' + git tag -v rfc1991-signed-tag +' + +test_expect_success GPG \ + 'list tag with rfc1991 signature' ' + echo "rfc1991-signed-tag RFC1991 signed tag" >expect && + git tag -l -n1 rfc1991-signed-tag >actual && + test_cmp expect actual && + git tag -l -n2 rfc1991-signed-tag >actual && + test_cmp expect actual && + git tag -l -n999 rfc1991-signed-tag >actual && + test_cmp expect actual +' + +rm -f gpghome/gpg.conf + +test_expect_success GPG \ + 'verifying rfc1991 signature without --rfc1991' ' + git tag -v rfc1991-signed-tag +' + +test_expect_success GPG \ + 'list tag with rfc1991 signature without --rfc1991' ' + echo "rfc1991-signed-tag RFC1991 signed tag" >expect && + git tag -l -n1 rfc1991-signed-tag >actual && + test_cmp expect actual && + git tag -l -n2 rfc1991-signed-tag >actual && + test_cmp expect actual && + git tag -l -n999 rfc1991-signed-tag >actual && + test_cmp expect actual +' + +test_expect_success GPG \ + 'reediting a signed tag body omits signature' ' + echo "RFC1991 signed tag" >expect && + GIT_EDITOR=./fakeeditor git tag -f -s rfc1991-signed-tag $commit && + test_cmp expect actual +' + # try to sign with bad user.signingkey git config user.signingkey BobTheMouse test_expect_success GPG \ @@ -1107,7 +1173,7 @@ hash1=$(git rev-parse HEAD) test_expect_success 'creating second commit and tag' ' echo foo-2.0 >foo && git add foo && - git commit -m second + git commit -m second && git tag v2.0 ' @@ -1132,18 +1198,18 @@ v2.0 EOF test_expect_success 'checking that first commit is in all tags (hash)' " - git tag -l --contains $hash1 v* >actual + git tag -l --contains $hash1 v* >actual && test_cmp expected actual " # other ways of specifying the commit test_expect_success 'checking that first commit is in all tags (tag)' " - git tag -l --contains v1.0 v* >actual + git tag -l --contains v1.0 v* >actual && test_cmp expected actual " test_expect_success 'checking that first commit is in all tags (relative)' " - git tag -l --contains HEAD~2 v* >actual + git tag -l --contains HEAD~2 v* >actual && test_cmp expected actual " @@ -1152,7 +1218,7 @@ v2.0 EOF test_expect_success 'checking that second commit only has one tag' " - git tag -l --contains $hash2 v* >actual + git tag -l --contains $hash2 v* >actual && test_cmp expected actual " @@ -1161,7 +1227,7 @@ cat > expected <<EOF EOF test_expect_success 'checking that third commit has no tags' " - git tag -l --contains $hash3 v* >actual + git tag -l --contains $hash3 v* >actual && test_cmp expected actual " @@ -1171,7 +1237,7 @@ test_expect_success 'creating simple branch' ' git branch stable v2.0 && git checkout stable && echo foo-3.0 > foo && - git commit foo -m fourth + git commit foo -m fourth && git tag v3.0 ' @@ -1182,7 +1248,7 @@ v3.0 EOF test_expect_success 'checking that branch head only has one tag' " - git tag -l --contains $hash4 v* >actual + git tag -l --contains $hash4 v* >actual && test_cmp expected actual " @@ -1196,7 +1262,7 @@ v4.0 EOF test_expect_success 'checking that original branch head has one tag now' " - git tag -l --contains $hash3 v* >actual + git tag -l --contains $hash3 v* >actual && test_cmp expected actual " @@ -1211,18 +1277,18 @@ v4.0 EOF test_expect_success 'checking that initial commit is in all tags' " - git tag -l --contains $hash1 v* >actual + git tag -l --contains $hash1 v* >actual && test_cmp expected actual " # mixing modes and options: test_expect_success 'mixing incompatibles modes and options is forbidden' ' - test_must_fail git tag -a - test_must_fail git tag -l -v - test_must_fail git tag -n 100 - test_must_fail git tag -l -m msg - test_must_fail git tag -l -F some file + test_must_fail git tag -a && + test_must_fail git tag -l -v && + test_must_fail git tag -n 100 && + test_must_fail git tag -l -m msg && + test_must_fail git tag -l -F some file && test_must_fail git tag -v -s ' diff --git a/t/t7006-pager.sh b/t/t7006-pager.sh index 5641b59559..ed7575d0fd 100755 --- a/t/t7006-pager.sh +++ b/t/t7006-pager.sh @@ -12,7 +12,7 @@ cleanup_fail() { } test_expect_success 'setup' ' - unset GIT_PAGER GIT_PAGER_IN_USE; + sane_unset GIT_PAGER GIT_PAGER_IN_USE && test_might_fail git config --unset core.pager && PAGER="cat >paginated.out" && @@ -220,7 +220,7 @@ test_default_pager() { parse_args "$@" $test_expectation SIMPLEPAGER,TTY "$cmd - default pager is used by default" " - unset PAGER GIT_PAGER; + sane_unset PAGER GIT_PAGER && test_might_fail git config --unset core.pager && rm -f default_pager_used || cleanup_fail && @@ -243,7 +243,7 @@ test_PAGER_overrides() { parse_args "$@" $test_expectation TTY "$cmd - PAGER overrides default pager" " - unset GIT_PAGER; + sane_unset GIT_PAGER && test_might_fail git config --unset core.pager && rm -f PAGER_used || cleanup_fail && @@ -271,7 +271,7 @@ test_core_pager() { parse_args "$@" $test_expectation TTY "$cmd - repository-local core.pager setting $used_if_wanted" " - unset GIT_PAGER; + sane_unset GIT_PAGER && rm -f core.pager_used || cleanup_fail && @@ -299,7 +299,7 @@ test_pager_subdir_helper() { parse_args "$@" $test_expectation TTY "$cmd - core.pager $used_if_wanted from subdirectory" " - unset GIT_PAGER; + sane_unset GIT_PAGER && rm -f core.pager_used && rm -fr sub || cleanup_fail && @@ -401,4 +401,33 @@ test_core_pager_subdir expect_success 'git -p shortlog' test_core_pager_subdir expect_success test_must_fail \ 'git -p apply </dev/null' +test_expect_success TTY 'command-specific pager' ' + unset PAGER GIT_PAGER; + echo "foo:initial" >expect && + >actual && + git config --unset core.pager && + git config pager.log "sed s/^/foo:/ >actual" && + test_terminal git log --format=%s -1 && + test_cmp expect actual +' + +test_expect_success TTY 'command-specific pager overrides core.pager' ' + unset PAGER GIT_PAGER; + echo "foo:initial" >expect && + >actual && + git config core.pager "exit 1" + git config pager.log "sed s/^/foo:/ >actual" && + test_terminal git log --format=%s -1 && + test_cmp expect actual +' + +test_expect_success TTY 'command-specific pager overridden by environment' ' + GIT_PAGER="sed s/^/foo:/ >actual" && export GIT_PAGER && + >actual && + echo "foo:initial" >expect && + git config pager.log "exit 1" && + test_terminal git log --format=%s -1 && + test_cmp expect actual +' + test_done diff --git a/t/t7105-reset-patch.sh b/t/t7105-reset-patch.sh index 9891e2c1f5..95fab20361 100755 --- a/t/t7105-reset-patch.sh +++ b/t/t7105-reset-patch.sh @@ -18,7 +18,7 @@ test_expect_success PERL 'setup' ' # note: bar sorts before foo, so the first 'n' is always to skip 'bar' test_expect_success PERL 'saying "n" does nothing' ' - set_and_save_state dir/foo work work + set_and_save_state dir/foo work work && (echo n; echo n) | git reset -p && verify_saved_state dir/foo && verify_saved_state bar @@ -42,14 +42,14 @@ test_expect_success PERL 'git reset -p HEAD^' ' # the failure case (and thus get out of the loop). test_expect_success PERL 'git reset -p dir' ' - set_state dir/foo work work + set_state dir/foo work work && (echo y; echo n) | git reset -p dir && verify_state dir/foo work head && verify_saved_state bar ' test_expect_success PERL 'git reset -p -- foo (inside dir)' ' - set_state dir/foo work work + set_state dir/foo work work && (echo y; echo n) | (cd dir && git reset -p -- foo) && verify_state dir/foo work head && verify_saved_state bar diff --git a/t/t7300-clean.sh b/t/t7300-clean.sh index 6c776e9bec..02f67b73b7 100755 --- a/t/t7300-clean.sh +++ b/t/t7300-clean.sh @@ -179,11 +179,11 @@ test_expect_success 'git clean -d with prefix and path' ' ' -test_expect_success 'git clean symbolic link' ' +test_expect_success SYMLINKS 'git clean symbolic link' ' mkdir -p build docs && touch a.out src/part3.c docs/manual.txt obj.o build/lib.so && - ln -s docs/manual.txt src/part4.c + ln -s docs/manual.txt src/part4.c && git clean && test -f Makefile && test -f README && diff --git a/t/t7500-commit.sh b/t/t7500-commit.sh index aa9c577e9e..162527c211 100755 --- a/t/t7500-commit.sh +++ b/t/t7500-commit.sh @@ -215,4 +215,84 @@ test_expect_success 'Commit a message with --allow-empty-message' ' commit_msg_is "hello there" ' +commit_for_rebase_autosquash_setup () { + echo "first content line" >>foo && + git add foo && + cat >log <<EOF && +target message subject line + +target message body line 1 +target message body line 2 +EOF + git commit -F log && + echo "second content line" >>foo && + git add foo && + git commit -m "intermediate commit" && + echo "third content line" >>foo && + git add foo +} + +test_expect_success 'commit --fixup provides correct one-line commit message' ' + commit_for_rebase_autosquash_setup && + git commit --fixup HEAD~1 && + commit_msg_is "fixup! target message subject line" +' + +test_expect_success 'commit --squash works with -F' ' + commit_for_rebase_autosquash_setup && + echo "log message from file" >msgfile && + git commit --squash HEAD~1 -F msgfile && + commit_msg_is "squash! target message subject linelog message from file" +' + +test_expect_success 'commit --squash works with -m' ' + commit_for_rebase_autosquash_setup && + git commit --squash HEAD~1 -m "foo bar\nbaz" && + commit_msg_is "squash! target message subject linefoo bar\nbaz" +' + +test_expect_success 'commit --squash works with -C' ' + commit_for_rebase_autosquash_setup && + git commit --squash HEAD~1 -C HEAD && + commit_msg_is "squash! target message subject lineintermediate commit" +' + +test_expect_success 'commit --squash works with -c' ' + commit_for_rebase_autosquash_setup && + test_set_editor "$TEST_DIRECTORY"/t7500/edit-content && + git commit --squash HEAD~1 -c HEAD && + commit_msg_is "squash! target message subject lineedited commit" +' + +test_expect_success 'commit --squash works with -C for same commit' ' + commit_for_rebase_autosquash_setup && + git commit --squash HEAD -C HEAD && + commit_msg_is "squash! intermediate commit" +' + +test_expect_success 'commit --squash works with -c for same commit' ' + commit_for_rebase_autosquash_setup && + test_set_editor "$TEST_DIRECTORY"/t7500/edit-content && + git commit --squash HEAD -c HEAD && + commit_msg_is "squash! edited commit" +' + +test_expect_success 'commit --squash works with editor' ' + commit_for_rebase_autosquash_setup && + test_set_editor "$TEST_DIRECTORY"/t7500/add-content && + git commit --squash HEAD~1 && + commit_msg_is "squash! target message subject linecommit message" +' + +test_expect_success 'invalid message options when using --fixup' ' + echo changes >>foo && + echo "message" >log && + git add foo && + test_must_fail git commit --fixup HEAD~1 --squash HEAD~2 && + test_must_fail git commit --fixup HEAD~1 -C HEAD~2 && + test_must_fail git commit --fixup HEAD~1 -c HEAD~2 && + test_must_fail git commit --fixup HEAD~1 -m "cmdline message" && + test_must_fail git commit --fixup HEAD~1 -F log +' + test_done diff --git a/t/t7500/edit-content b/t/t7500/edit-content new file mode 100755 index 0000000000..08db9fdd2e --- /dev/null +++ b/t/t7500/edit-content @@ -0,0 +1,4 @@ +#!/bin/sh +sed -e "s/intermediate/edited/g" <"$1" >"$1-" +mv "$1-" "$1" +exit 0 diff --git a/t/t7502-commit.sh b/t/t7502-commit.sh index ac2e187a57..50da034cd3 100755 --- a/t/t7502-commit.sh +++ b/t/t7502-commit.sh @@ -252,8 +252,8 @@ test_expect_success 'committer is automatic' ' echo >>negative && ( - unset GIT_COMMITTER_EMAIL - unset GIT_COMMITTER_NAME + sane_unset GIT_COMMITTER_EMAIL && + sane_unset GIT_COMMITTER_NAME && # must fail because there is no change test_must_fail git commit -e -m "sample" ) && @@ -390,7 +390,7 @@ try_commit_status_combo () { test_expect_success 'commit --no-status' ' clear_config commit.status && - try_commit --no-status + try_commit --no-status && ! grep "^# Changes to be committed:" .git/COMMIT_EDITMSG ' diff --git a/t/t7508-status.sh b/t/t7508-status.sh index c9300f3c8b..b73ab42936 100755 --- a/t/t7508-status.sh +++ b/t/t7508-status.sh @@ -7,6 +7,30 @@ test_description='git status' . ./test-lib.sh +test_expect_success 'status -h in broken repository' ' + mkdir broken && + test_when_finished "rm -fr broken" && + ( + cd broken && + git init && + echo "[status] showuntrackedfiles = CORRUPT" >>.git/config && + test_expect_code 129 git status -h >usage 2>&1 + ) && + grep "[Uu]sage" broken/usage +' + +test_expect_success 'commit -h in broken repository' ' + mkdir broken && + test_when_finished "rm -fr broken" && + ( + cd broken && + git init && + echo "[status] showuntrackedfiles = CORRUPT" >>.git/config && + test_expect_code 129 git commit -h >usage 2>&1 + ) && + grep "[Uu]sage" broken/usage +' + test_expect_success 'setup' ' : >tracked && : >modified && @@ -44,7 +68,7 @@ cat >expect <<\EOF # # new file: dir2/added # -# Changed but not updated: +# Changes not staged for commit: # (use "git add <file>..." to update what will be committed) # (use "git checkout -- <file>..." to discard changes in working directory) # @@ -73,7 +97,7 @@ cat >expect <<\EOF # Changes to be committed: # new file: dir2/added # -# Changed but not updated: +# Changes not staged for commit: # modified: dir1/modified # # Untracked files: @@ -140,7 +164,7 @@ cat >expect <<EOF # # new file: dir2/added # -# Changed but not updated: +# Changes not staged for commit: # (use "git add <file>..." to update what will be committed) # (use "git checkout -- <file>..." to discard changes in working directory) # @@ -167,7 +191,7 @@ cat >expect <<EOF # Changes to be committed: # new file: dir2/added # -# Changed but not updated: +# Changes not staged for commit: # modified: dir1/modified # # Untracked files not listed @@ -202,7 +226,7 @@ cat >expect <<EOF # # new file: dir2/added # -# Changed but not updated: +# Changes not staged for commit: # (use "git add <file>..." to update what will be committed) # (use "git checkout -- <file>..." to discard changes in working directory) # @@ -260,7 +284,7 @@ cat >expect <<EOF # # new file: dir2/added # -# Changed but not updated: +# Changes not staged for commit: # (use "git add <file>..." to update what will be committed) # (use "git checkout -- <file>..." to discard changes in working directory) # @@ -320,7 +344,7 @@ cat >expect <<\EOF # # new file: ../dir2/added # -# Changed but not updated: +# Changes not staged for commit: # (use "git add <file>..." to update what will be committed) # (use "git checkout -- <file>..." to discard changes in working directory) # @@ -392,7 +416,7 @@ cat >expect <<\EOF # # <GREEN>new file: dir2/added<RESET> # -# Changed but not updated: +# Changes not staged for commit: # (use "git add <file>..." to update what will be committed) # (use "git checkout -- <file>..." to discard changes in working directory) # @@ -521,7 +545,7 @@ cat >expect <<\EOF # # new file: dir2/added # -# Changed but not updated: +# Changes not staged for commit: # (use "git add <file>..." to update what will be committed) # (use "git checkout -- <file>..." to discard changes in working directory) # @@ -614,7 +638,7 @@ cat >expect <<EOF # new file: dir2/added # new file: sm # -# Changed but not updated: +# Changes not staged for commit: # (use "git add <file>..." to update what will be committed) # (use "git checkout -- <file>..." to discard changes in working directory) # @@ -673,7 +697,7 @@ cat >expect <<EOF # new file: dir2/added # new file: sm # -# Changed but not updated: +# Changes not staged for commit: # (use "git add <file>..." to update what will be committed) # (use "git checkout -- <file>..." to discard changes in working directory) # @@ -718,7 +742,7 @@ test_expect_success 'status -s submodule summary' ' cat >expect <<EOF # On branch master -# Changed but not updated: +# Changes not staged for commit: # (use "git add <file>..." to update what will be committed) # (use "git checkout -- <file>..." to discard changes in working directory) # @@ -766,7 +790,7 @@ cat >expect <<EOF # new file: dir2/added # new file: sm # -# Changed but not updated: +# Changes not staged for commit: # (use "git add <file>..." to update what will be committed) # (use "git checkout -- <file>..." to discard changes in working directory) # @@ -819,7 +843,7 @@ cat > expect << EOF # # modified: sm # -# Changed but not updated: +# Changes not staged for commit: # (use "git add <file>..." to update what will be committed) # (use "git checkout -- <file>..." to discard changes in working directory) # @@ -931,7 +955,7 @@ cat > expect << EOF # # modified: sm # -# Changed but not updated: +# Changes not staged for commit: # (use "git add <file>..." to update what will be committed) # (use "git checkout -- <file>..." to discard changes in working directory) # (commit or discard the untracked or modified content in submodules) @@ -989,7 +1013,7 @@ cat > expect << EOF # # modified: sm # -# Changed but not updated: +# Changes not staged for commit: # (use "git add <file>..." to update what will be committed) # (use "git checkout -- <file>..." to discard changes in working directory) # @@ -1067,7 +1091,7 @@ test_expect_success ".git/config ignore=dirty doesn't suppress submodule summary cat > expect << EOF # On branch master -# Changed but not updated: +# Changes not staged for commit: # (use "git add <file>..." to update what will be committed) # (use "git checkout -- <file>..." to discard changes in working directory) # diff --git a/t/t7509-commit.sh b/t/t7509-commit.sh index 643ab03f99..77b6920029 100755 --- a/t/t7509-commit.sh +++ b/t/t7509-commit.sh @@ -40,7 +40,7 @@ test_expect_success '-C option copies only the message with --reset-author' ' test_tick && git commit -a -C Initial --reset-author && echo "author $GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL> $GIT_AUTHOR_DATE" >expect && - author_header HEAD >actual + author_header HEAD >actual && test_cmp expect actual && message_body Initial >expect && diff --git a/t/t7600-merge.sh b/t/t7600-merge.sh index b4f40e4c3a..b147a1bd69 100755 --- a/t/t7600-merge.sh +++ b/t/t7600-merge.sh @@ -144,6 +144,17 @@ test_expect_success 'test option parsing' ' test_must_fail git merge ' +test_expect_success 'merge -h with invalid index' ' + mkdir broken && + ( + cd broken && + git init && + >.git/index && + test_expect_code 129 git merge -h 2>usage + ) && + grep "[Uu]sage: git merge" broken/usage +' + test_expect_success 'reject non-strategy with a git-merge-foo name' ' test_must_fail git merge -s index c1 ' diff --git a/t/t7601-merge-pull-config.sh b/t/t7601-merge-pull-config.sh index 7ba94ea99b..b44b293950 100755 --- a/t/t7601-merge-pull-config.sh +++ b/t/t7601-merge-pull-config.sh @@ -114,13 +114,13 @@ test_expect_success 'setup conflicted merge' ' test_expect_success 'merge picks up the best result' ' git config --unset-all pull.twohead && git reset --hard c5 && - git merge -s resolve c6 + test_must_fail git merge -s resolve c6 && resolve_count=$(conflict_count) && git reset --hard c5 && - git merge -s recursive c6 + test_must_fail git merge -s recursive c6 && recursive_count=$(conflict_count) && git reset --hard c5 && - git merge -s recursive -s resolve c6 + test_must_fail git merge -s recursive -s resolve c6 && auto_count=$(conflict_count) && test $auto_count = $recursive_count && test $auto_count != $resolve_count @@ -129,13 +129,13 @@ test_expect_success 'merge picks up the best result' ' test_expect_success 'merge picks up the best result (from config)' ' git config pull.twohead "recursive resolve" && git reset --hard c5 && - git merge -s resolve c6 + test_must_fail git merge -s resolve c6 && resolve_count=$(conflict_count) && git reset --hard c5 && - git merge -s recursive c6 + test_must_fail git merge -s recursive c6 && recursive_count=$(conflict_count) && git reset --hard c5 && - git merge c6 + test_must_fail git merge c6 && auto_count=$(conflict_count) && test $auto_count = $recursive_count && test $auto_count != $resolve_count diff --git a/t/t7602-merge-octopus-many.sh b/t/t7602-merge-octopus-many.sh index 2746169514..0a46795ae7 100755 --- a/t/t7602-merge-octopus-many.sh +++ b/t/t7602-merge-octopus-many.sh @@ -31,7 +31,7 @@ test_expect_success 'merge c1 with c2, c3, c4, ... c29' ' do refs="$refs c$i" i=`expr $i + 1` - done + done && git merge $refs && test "$(git rev-parse c1)" != "$(git rev-parse HEAD)" && i=1 && diff --git a/t/t7607-merge-overwrite.sh b/t/t7607-merge-overwrite.sh index d82349a6a8..4d5ce4e682 100755 --- a/t/t7607-merge-overwrite.sh +++ b/t/t7607-merge-overwrite.sh @@ -7,48 +7,54 @@ Do not overwrite changes.' . ./test-lib.sh test_expect_success 'setup' ' - echo c0 > c0.c && - git add c0.c && - git commit -m c0 && - git tag c0 && - echo c1 > c1.c && - git add c1.c && - git commit -m c1 && - git tag c1 && + test_commit c0 c0.c && + test_commit c1 c1.c && + test_commit c1a c1.c "c1 a" && git reset --hard c0 && - echo c2 > c2.c && - git add c2.c && - git commit -m c2 && - git tag c2 && - git reset --hard c1 && - echo "c1 a" > c1.c && - git add c1.c && - git commit -m "c1 a" && - git tag c1a && + test_commit c2 c2.c && + git reset --hard c0 && + mkdir sub && + echo "sub/f" > sub/f && + mkdir sub2 && + echo "sub2/f" > sub2/f && + git add sub/f sub2/f && + git commit -m sub && + git tag sub && echo "VERY IMPORTANT CHANGES" > important ' test_expect_success 'will not overwrite untracked file' ' git reset --hard c1 && - cat important > c2.c && + cp important c2.c && test_must_fail git merge c2 && + test_path_is_missing .git/MERGE_HEAD && test_cmp important c2.c ' +test_expect_success 'will overwrite tracked file' ' + git reset --hard c1 && + cp important c2.c && + git add c2.c && + git commit -m important && + git checkout c2 +' + test_expect_success 'will not overwrite new file' ' git reset --hard c1 && - cat important > c2.c && + cp important c2.c && git add c2.c && test_must_fail git merge c2 && + test_path_is_missing .git/MERGE_HEAD && test_cmp important c2.c ' test_expect_success 'will not overwrite staged changes' ' git reset --hard c1 && - cat important > c2.c && + cp important c2.c && git add c2.c && rm c2.c && test_must_fail git merge c2 && + test_path_is_missing .git/MERGE_HEAD && git checkout c2.c && test_cmp important c2.c ' @@ -57,7 +63,7 @@ test_expect_success 'will not overwrite removed file' ' git reset --hard c1 && git rm c1.c && git commit -m "rm c1.c" && - cat important > c1.c && + cp important c1.c && test_must_fail git merge c1a && test_cmp important c1.c ' @@ -66,9 +72,10 @@ test_expect_success 'will not overwrite re-added file' ' git reset --hard c1 && git rm c1.c && git commit -m "rm c1.c" && - cat important > c1.c && + cp important c1.c && git add c1.c && test_must_fail git merge c1a && + test_path_is_missing .git/MERGE_HEAD && test_cmp important c1.c ' @@ -76,12 +83,77 @@ test_expect_success 'will not overwrite removed file with staged changes' ' git reset --hard c1 && git rm c1.c && git commit -m "rm c1.c" && - cat important > c1.c && + cp important c1.c && git add c1.c && rm c1.c && test_must_fail git merge c1a && + test_path_is_missing .git/MERGE_HEAD && git checkout c1.c && test_cmp important c1.c ' +test_expect_success 'will not overwrite untracked subtree' ' + git reset --hard c0 && + rm -rf sub && + mkdir -p sub/f && + cp important sub/f/important && + test_must_fail git merge sub && + test_path_is_missing .git/MERGE_HEAD && + test_cmp important sub/f/important +' + +cat >expect <<\EOF +error: The following untracked working tree files would be overwritten by merge: + sub + sub2 +Please move or remove them before you can merge. +EOF + +test_expect_success 'will not overwrite untracked file in leading path' ' + git reset --hard c0 && + rm -rf sub && + cp important sub && + cp important sub2 && + test_must_fail git merge sub 2>out && + test_cmp out expect && + test_path_is_missing .git/MERGE_HEAD && + test_cmp important sub && + test_cmp important sub2 && + rm -f sub sub2 +' + +test_expect_failure SYMLINKS 'will not overwrite untracked symlink in leading path' ' + git reset --hard c0 && + rm -rf sub && + mkdir sub2 && + ln -s sub2 sub && + test_must_fail git merge sub && + test_path_is_missing .git/MERGE_HEAD +' + +test_expect_success SYMLINKS 'will not be confused by symlink in leading path' ' + git reset --hard c0 && + rm -rf sub && + ln -s sub2 sub && + git add sub && + git commit -m ln && + git checkout sub +' + +cat >expect <<\EOF +error: Untracked working tree file 'c0.c' would be overwritten by merge. +fatal: read-tree failed +EOF + +test_expect_success 'will not overwrite untracked file on unborn branch' ' + git reset --hard c0 && + git rm -fr . && + git checkout --orphan new && + cp important c0.c && + test_must_fail git merge c0 2>out && + test_cmp out expect && + test_path_is_missing .git/MERGE_HEAD && + test_cmp important c0.c +' + test_done diff --git a/t/t7608-merge-messages.sh b/t/t7608-merge-messages.sh index 28d56797b1..9225fa6f02 100755 --- a/t/t7608-merge-messages.sh +++ b/t/t7608-merge-messages.sh @@ -47,14 +47,14 @@ test_expect_success 'ambiguous tag' ' check_oneline "Merge commit QambiguousQ" ' -test_expect_success 'remote branch' ' +test_expect_success 'remote-tracking branch' ' git checkout -b remote master && test_commit remote-1 && git update-ref refs/remotes/origin/master remote && git checkout master && test_commit master-5 && git merge origin/master && - check_oneline "Merge remote branch Qorigin/masterQ" + check_oneline "Merge remote-tracking branch Qorigin/masterQ" ' test_done diff --git a/t/t7609-merge-abort.sh b/t/t7609-merge-abort.sh new file mode 100755 index 0000000000..61890bc892 --- /dev/null +++ b/t/t7609-merge-abort.sh @@ -0,0 +1,313 @@ +#!/bin/sh + +test_description='test aborting in-progress merges + +Set up repo with conflicting and non-conflicting branches: + +There are three files foo/bar/baz, and the following graph illustrates the +content of these files in each commit: + +# foo/bar/baz --- foo/bar/bazz <-- master +# \ +# --- foo/barf/bazf <-- conflict_branch +# \ +# --- foo/bart/baz <-- clean_branch + +Next, test git merge --abort with the following variables: +- before/after successful merge (should fail when not in merge context) +- with/without conflicts +- clean/dirty index before merge +- clean/dirty worktree before merge +- dirty index before merge matches contents on remote branch +- changed/unchanged worktree after merge +- changed/unchanged index after merge +' +. ./test-lib.sh + +test_expect_success 'setup' ' + # Create the above repo + echo foo > foo && + echo bar > bar && + echo baz > baz && + git add foo bar baz && + git commit -m initial && + echo bazz > baz && + git commit -a -m "second" && + git checkout -b conflict_branch HEAD^ && + echo barf > bar && + echo bazf > baz && + git commit -a -m "conflict" && + git checkout -b clean_branch HEAD^ && + echo bart > bar && + git commit -a -m "clean" && + git checkout master +' + +pre_merge_head="$(git rev-parse HEAD)" + +test_expect_success 'fails without MERGE_HEAD (unstarted merge)' ' + test_must_fail git merge --abort 2>output && + grep -q MERGE_HEAD output && + test ! -f .git/MERGE_HEAD && + test "$pre_merge_head" = "$(git rev-parse HEAD)" +' + +test_expect_success 'fails without MERGE_HEAD (completed merge)' ' + git merge clean_branch && + test ! -f .git/MERGE_HEAD && + # Merge successfully completed + post_merge_head="$(git rev-parse HEAD)" && + test_must_fail git merge --abort 2>output && + grep -q MERGE_HEAD output && + test ! -f .git/MERGE_HEAD && + test "$post_merge_head" = "$(git rev-parse HEAD)" +' + +test_expect_success 'Forget previous merge' ' + git reset --hard "$pre_merge_head" +' + +test_expect_success 'Abort after --no-commit' ' + # Redo merge, but stop before creating merge commit + git merge --no-commit clean_branch && + test -f .git/MERGE_HEAD && + # Abort non-conflicting merge + git merge --abort && + test ! -f .git/MERGE_HEAD && + test "$pre_merge_head" = "$(git rev-parse HEAD)" && + test -z "$(git diff)" && + test -z "$(git diff --staged)" +' + +test_expect_success 'Abort after conflicts' ' + # Create conflicting merge + test_must_fail git merge conflict_branch && + test -f .git/MERGE_HEAD && + # Abort conflicting merge + git merge --abort && + test ! -f .git/MERGE_HEAD && + test "$pre_merge_head" = "$(git rev-parse HEAD)" && + test -z "$(git diff)" && + test -z "$(git diff --staged)" +' + +test_expect_success 'Clean merge with dirty index fails' ' + echo xyzzy >> foo && + git add foo && + git diff --staged > expect && + test_must_fail git merge clean_branch && + test ! -f .git/MERGE_HEAD && + test "$pre_merge_head" = "$(git rev-parse HEAD)" && + test -z "$(git diff)" && + git diff --staged > actual && + test_cmp expect actual +' + +test_expect_success 'Conflicting merge with dirty index fails' ' + test_must_fail git merge conflict_branch && + test ! -f .git/MERGE_HEAD && + test "$pre_merge_head" = "$(git rev-parse HEAD)" && + test -z "$(git diff)" && + git diff --staged > actual && + test_cmp expect actual +' + +test_expect_success 'Reset index (but preserve worktree changes)' ' + git reset "$pre_merge_head" && + git diff > actual && + test_cmp expect actual +' + +test_expect_success 'Abort clean merge with non-conflicting dirty worktree' ' + git merge --no-commit clean_branch && + test -f .git/MERGE_HEAD && + # Abort merge + git merge --abort && + test ! -f .git/MERGE_HEAD && + test "$pre_merge_head" = "$(git rev-parse HEAD)" && + test -z "$(git diff --staged)" && + git diff > actual && + test_cmp expect actual +' + +test_expect_success 'Abort conflicting merge with non-conflicting dirty worktree' ' + test_must_fail git merge conflict_branch && + test -f .git/MERGE_HEAD && + # Abort merge + git merge --abort && + test ! -f .git/MERGE_HEAD && + test "$pre_merge_head" = "$(git rev-parse HEAD)" && + test -z "$(git diff --staged)" && + git diff > actual && + test_cmp expect actual +' + +test_expect_success 'Reset worktree changes' ' + git reset --hard "$pre_merge_head" +' + +test_expect_success 'Fail clean merge with conflicting dirty worktree' ' + echo xyzzy >> bar && + git diff > expect && + test_must_fail git merge --no-commit clean_branch && + test ! -f .git/MERGE_HEAD && + test "$pre_merge_head" = "$(git rev-parse HEAD)" && + test -z "$(git diff --staged)" && + git diff > actual && + test_cmp expect actual +' + +test_expect_success 'Fail conflicting merge with conflicting dirty worktree' ' + test_must_fail git merge conflict_branch && + test ! -f .git/MERGE_HEAD && + test "$pre_merge_head" = "$(git rev-parse HEAD)" && + test -z "$(git diff --staged)" && + git diff > actual && + test_cmp expect actual +' + +test_expect_success 'Reset worktree changes' ' + git reset --hard "$pre_merge_head" +' + +test_expect_success 'Fail clean merge with matching dirty worktree' ' + echo bart > bar && + git diff > expect && + test_must_fail git merge --no-commit clean_branch && + test ! -f .git/MERGE_HEAD && + test "$pre_merge_head" = "$(git rev-parse HEAD)" && + test -z "$(git diff --staged)" && + git diff > actual && + test_cmp expect actual +' + +test_expect_success 'Abort clean merge with matching dirty index' ' + git add bar && + git diff --staged > expect && + git merge --no-commit clean_branch && + test -f .git/MERGE_HEAD && + ### When aborting the merge, git will discard all staged changes, + ### including those that were staged pre-merge. In other words, + ### --abort will LOSE any staged changes (the staged changes that + ### are lost must match the merge result, or the merge would not + ### have been allowed to start). Change expectations accordingly: + rm expect && + touch expect && + # Abort merge + git merge --abort && + test ! -f .git/MERGE_HEAD && + test "$pre_merge_head" = "$(git rev-parse HEAD)" && + git diff --staged > actual && + test_cmp expect actual && + test -z "$(git diff)" +' + +test_expect_success 'Reset worktree changes' ' + git reset --hard "$pre_merge_head" +' + +test_expect_success 'Fail conflicting merge with matching dirty worktree' ' + echo barf > bar && + git diff > expect && + test_must_fail git merge conflict_branch && + test ! -f .git/MERGE_HEAD && + test "$pre_merge_head" = "$(git rev-parse HEAD)" && + test -z "$(git diff --staged)" && + git diff > actual && + test_cmp expect actual +' + +test_expect_success 'Abort conflicting merge with matching dirty index' ' + git add bar && + git diff --staged > expect && + test_must_fail git merge conflict_branch && + test -f .git/MERGE_HEAD && + ### When aborting the merge, git will discard all staged changes, + ### including those that were staged pre-merge. In other words, + ### --abort will LOSE any staged changes (the staged changes that + ### are lost must match the merge result, or the merge would not + ### have been allowed to start). Change expectations accordingly: + rm expect && + touch expect && + # Abort merge + git merge --abort && + test ! -f .git/MERGE_HEAD && + test "$pre_merge_head" = "$(git rev-parse HEAD)" && + git diff --staged > actual && + test_cmp expect actual && + test -z "$(git diff)" +' + +test_expect_success 'Reset worktree changes' ' + git reset --hard "$pre_merge_head" +' + +test_expect_success 'Abort merge with pre- and post-merge worktree changes' ' + # Pre-merge worktree changes + echo xyzzy > foo && + echo barf > bar && + git add bar && + git diff > expect && + git diff --staged > expect-staged && + # Perform merge + test_must_fail git merge conflict_branch && + test -f .git/MERGE_HEAD && + # Post-merge worktree changes + echo yzxxz > foo && + echo blech > baz && + ### When aborting the merge, git will discard staged changes (bar) + ### and unmerged changes (baz). Other changes that are neither + ### staged nor marked as unmerged (foo), will be preserved. For + ### these changed, git cannot tell pre-merge changes apart from + ### post-merge changes, so the post-merge changes will be + ### preserved. Change expectations accordingly: + git diff -- foo > expect && + rm expect-staged && + touch expect-staged && + # Abort merge + git merge --abort && + test ! -f .git/MERGE_HEAD && + test "$pre_merge_head" = "$(git rev-parse HEAD)" && + git diff > actual && + test_cmp expect actual && + git diff --staged > actual-staged && + test_cmp expect-staged actual-staged +' + +test_expect_success 'Reset worktree changes' ' + git reset --hard "$pre_merge_head" +' + +test_expect_success 'Abort merge with pre- and post-merge index changes' ' + # Pre-merge worktree changes + echo xyzzy > foo && + echo barf > bar && + git add bar && + git diff > expect && + git diff --staged > expect-staged && + # Perform merge + test_must_fail git merge conflict_branch && + test -f .git/MERGE_HEAD && + # Post-merge worktree changes + echo yzxxz > foo && + echo blech > baz && + git add foo bar && + ### When aborting the merge, git will discard all staged changes + ### (foo, bar and baz), and no changes will be preserved. Whether + ### the changes were staged pre- or post-merge does not matter + ### (except for not preventing starting the merge). + ### Change expectations accordingly: + rm expect expect-staged && + touch expect && + touch expect-staged && + # Abort merge + git merge --abort && + test ! -f .git/MERGE_HEAD && + test "$pre_merge_head" = "$(git rev-parse HEAD)" && + git diff > actual && + test_cmp expect actual && + git diff --staged > actual-staged && + test_cmp expect-staged actual-staged +' + +test_done diff --git a/t/t7609-merge-co-error-msgs.sh b/t/t7609-merge-co-error-msgs.sh index 114d2bd785..c994836c53 100755 --- a/t/t7609-merge-co-error-msgs.sh +++ b/t/t7609-merge-co-error-msgs.sh @@ -27,10 +27,10 @@ test_expect_success 'setup' ' cat >expect <<\EOF error: The following untracked working tree files would be overwritten by merge: - two - three - four five + four + three + two Please move or remove them before you can merge. EOF @@ -49,9 +49,9 @@ test_expect_success 'untracked files overwritten by merge (fast and non-fast for cat >expect <<\EOF error: Your local changes to the following files would be overwritten by merge: - two - three four + three + two Please, commit your changes or stash them before you can merge. error: The following untracked working tree files would be overwritten by merge: five @@ -68,8 +68,8 @@ test_expect_success 'untracked files or local changes ovewritten by merge' ' cat >expect <<\EOF error: Your local changes to the following files would be overwritten by checkout: - rep/two rep/one + rep/two Please, commit your changes or stash them before you can switch branches. EOF @@ -89,8 +89,8 @@ test_expect_success 'cannot switch branches because of local changes' ' cat >expect <<\EOF error: Your local changes to the following files would be overwritten by checkout: - rep/two rep/one + rep/two Please, commit your changes or stash them before you can switch branches. EOF @@ -102,8 +102,8 @@ test_expect_success 'not uptodate file porcelain checkout error' ' cat >expect <<\EOF error: Updating the following directories would lose untracked files in it: - rep2 rep + rep2 EOF diff --git a/t/t7610-mergetool.sh b/t/t7610-mergetool.sh index 3bd74042ef..d78bdec330 100755 --- a/t/t7610-mergetool.sh +++ b/t/t7610-mergetool.sh @@ -54,7 +54,7 @@ test_expect_success 'custom mergetool' ' test_expect_success 'mergetool crlf' ' git config core.autocrlf true && - git checkout -b test2 branch1 + git checkout -b test2 branch1 && test_must_fail git merge master >/dev/null 2>&1 && ( yes "" | git mergetool file1 >/dev/null 2>&1 ) && ( yes "" | git mergetool file2 >/dev/null 2>&1 ) && diff --git a/t/t7700-repack.sh b/t/t7700-repack.sh index c2f66ff170..d954b846a1 100755 --- a/t/t7700-repack.sh +++ b/t/t7700-repack.sh @@ -56,7 +56,7 @@ test_expect_success 'loose objects in alternate ODB are not repacked' ' ' test_expect_success 'packed obs in alt ODB are repacked even when local repo is packless' ' - mkdir alt_objects/pack + mkdir alt_objects/pack && mv .git/objects/pack/* alt_objects/pack && git repack -a && myidx=$(ls -1 .git/objects/pack/*.idx) && @@ -95,14 +95,14 @@ test_expect_success 'packed obs in alternate ODB kept pack are repacked' ' # swap the .keep so the commit object is in the pack with .keep for p in alt_objects/pack/*.pack do - base_name=$(basename $p .pack) + base_name=$(basename $p .pack) && if test -f alt_objects/pack/$base_name.keep then rm alt_objects/pack/$base_name.keep else touch alt_objects/pack/$base_name.keep fi - done + done && git repack -a -d && myidx=$(ls -1 .git/objects/pack/*.idx) && test -f "$myidx" && diff --git a/t/t7800-difftool.sh b/t/t7800-difftool.sh index 58dc6f6452..4048d106d4 100755 --- a/t/t7800-difftool.sh +++ b/t/t7800-difftool.sh @@ -98,7 +98,7 @@ test_expect_success PERL 'difftool --gui works without configured diff.guitool' # Specify the diff tool using $GIT_DIFF_TOOL test_expect_success PERL 'GIT_DIFF_TOOL variable' ' - git config --unset diff.tool + test_might_fail git config --unset diff.tool && GIT_DIFF_TOOL=test-tool && export GIT_DIFF_TOOL && @@ -166,7 +166,7 @@ test_expect_success PERL 'difftool.prompt config variable is false' ' # Test that we don't have to pass --no-prompt when mergetool.prompt is false test_expect_success PERL 'difftool merge.prompt = false' ' - git config --unset difftool.prompt + test_might_fail git config --unset difftool.prompt && git config mergetool.prompt false && diff=$(git difftool branch) && @@ -211,7 +211,7 @@ test_expect_success PERL 'difftool last flag wins' ' # git-difftool falls back to git-mergetool config variables # so test that behavior here test_expect_success PERL 'difftool + mergetool config variables' ' - remove_config_vars + remove_config_vars && git config merge.tool test-tool && git config mergetool.test-tool.cmd "cat \$LOCAL" && @@ -254,17 +254,17 @@ test_expect_success PERL 'difftool -x cat' ' ' test_expect_success PERL 'difftool --extcmd echo arg1' ' - diff=$(git difftool --no-prompt --extcmd sh\ -c\ \"echo\ \$1\" branch) + diff=$(git difftool --no-prompt --extcmd sh\ -c\ \"echo\ \$1\" branch) && test "$diff" = file ' test_expect_success PERL 'difftool --extcmd cat arg1' ' - diff=$(git difftool --no-prompt --extcmd sh\ -c\ \"cat\ \$1\" branch) + diff=$(git difftool --no-prompt --extcmd sh\ -c\ \"cat\ \$1\" branch) && test "$diff" = master ' test_expect_success PERL 'difftool --extcmd cat arg2' ' - diff=$(git difftool --no-prompt --extcmd sh\ -c\ \"cat\ \$2\" branch) + diff=$(git difftool --no-prompt --extcmd sh\ -c\ \"cat\ \$2\" branch) && test "$diff" = branch ' diff --git a/t/t7810-grep.sh b/t/t7810-grep.sh index 50658845ca..c8777589ca 100755 --- a/t/t7810-grep.sh +++ b/t/t7810-grep.sh @@ -479,7 +479,7 @@ test_expect_success 'outside of git repository' ' echo file1:hello && echo sub/file2:world } >non/expect.full && - echo file2:world >non/expect.sub + echo file2:world >non/expect.sub && ( GIT_CEILING_DIRECTORIES="$(pwd)/non/git" && export GIT_CEILING_DIRECTORIES && @@ -505,7 +505,7 @@ test_expect_success 'inside git repository but with --no-index' ' echo sub/file2:world } >is/expect.full && : >is/expect.empty && - echo file2:world >is/expect.sub + echo file2:world >is/expect.sub && ( cd is/git && git init && diff --git a/t/t8002-blame.sh b/t/t8002-blame.sh index 597cf0486f..d3a51e1269 100755 --- a/t/t8002-blame.sh +++ b/t/t8002-blame.sh @@ -6,4 +6,9 @@ test_description='git blame' PROG='git blame -c' . "$TEST_DIRECTORY"/annotate-tests.sh +PROG='git blame -c -e' +test_expect_success 'Blame --show-email works' ' + check_count "<A@test.git>" 1 "<B@test.git>" 1 "<B1@test.git>" 1 "<B2@test.git>" 1 "<author@example.com>" 1 "<C@test.git>" 1 "<D@test.git>" 1 +' + test_done diff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh index d1ba25205b..5e48318013 100755 --- a/t/t9001-send-email.sh +++ b/t/t9001-send-email.sh @@ -313,6 +313,49 @@ test_expect_success $PREREQ 'Valid In-Reply-To when prompting' ' ! grep "^In-Reply-To: < *>" msgtxt1 ' +test_expect_success $PREREQ 'In-Reply-To without --chain-reply-to' ' + clean_fake_sendmail && + echo "<unique-message-id@example.com>" >expect && + git send-email \ + --from="Example <nobody@example.com>" \ + --to=nobody@example.com \ + --no-chain-reply-to \ + --in-reply-to="$(cat expect)" \ + --smtp-server="$(pwd)/fake.sendmail" \ + $patches $patches $patches \ + 2>errors && + # The first message is a reply to --in-reply-to + sed -n -e "s/^In-Reply-To: *\(.*\)/\1/p" msgtxt1 >actual && + test_cmp expect actual && + # Second and subsequent messages are replies to the first one + sed -n -e "s/^Message-Id: *\(.*\)/\1/p" msgtxt1 >expect && + sed -n -e "s/^In-Reply-To: *\(.*\)/\1/p" msgtxt2 >actual && + test_cmp expect actual && + sed -n -e "s/^In-Reply-To: *\(.*\)/\1/p" msgtxt3 >actual && + test_cmp expect actual +' + +test_expect_success $PREREQ 'In-Reply-To with --chain-reply-to' ' + clean_fake_sendmail && + echo "<unique-message-id@example.com>" >expect && + git send-email \ + --from="Example <nobody@example.com>" \ + --to=nobody@example.com \ + --chain-reply-to \ + --in-reply-to="$(cat expect)" \ + --smtp-server="$(pwd)/fake.sendmail" \ + $patches $patches $patches \ + 2>errors && + sed -n -e "s/^In-Reply-To: *\(.*\)/\1/p" msgtxt1 >actual && + test_cmp expect actual && + sed -n -e "s/^Message-Id: *\(.*\)/\1/p" msgtxt1 >expect && + sed -n -e "s/^In-Reply-To: *\(.*\)/\1/p" msgtxt2 >actual && + test_cmp expect actual && + sed -n -e "s/^Message-Id: *\(.*\)/\1/p" msgtxt2 >expect && + sed -n -e "s/^In-Reply-To: *\(.*\)/\1/p" msgtxt3 >actual && + test_cmp expect actual +' + test_expect_success $PREREQ 'setup fake editor' ' (echo "#!$SHELL_PATH" && echo "echo fake edit >>\"\$1\"" diff --git a/t/t9104-git-svn-follow-parent.sh b/t/t9104-git-svn-follow-parent.sh index f7f3c5ab8e..13b179e721 100755 --- a/t/t9104-git-svn-follow-parent.sh +++ b/t/t9104-git-svn-follow-parent.sh @@ -190,7 +190,7 @@ test_expect_success "follow-parent is atomic" ' git svn init --minimize-url -i stunk "$svnrepo"/stunk && git svn fetch -i stunk && git svn init --minimize-url -i flunked "$svnrepo"/flunked && - git svn fetch -i flunked + git svn fetch -i flunked && test "`git rev-parse --verify refs/remotes/flunk@18`" \ = "`git rev-parse --verify refs/remotes/stunk`" && test "`git rev-parse --verify refs/remotes/flunk~1`" \ diff --git a/t/t9123-git-svn-rebuild-with-rewriteroot.sh b/t/t9123-git-svn-rebuild-with-rewriteroot.sh index 0ed90d982d..fd8184787f 100755 --- a/t/t9123-git-svn-rebuild-with-rewriteroot.sh +++ b/t/t9123-git-svn-rebuild-with-rewriteroot.sh @@ -16,7 +16,7 @@ rm -rf import test_expect_success 'init, fetch and checkout repository' ' git svn init --rewrite-root=http://invalid.invalid/ "$svnrepo" && - git svn fetch + git svn fetch && git checkout -b mybranch ${remotes_git_svn} ' diff --git a/t/t9124-git-svn-dcommit-auto-props.sh b/t/t9124-git-svn-dcommit-auto-props.sh index d6b076f6b7..aa841e1299 100755 --- a/t/t9124-git-svn-dcommit-auto-props.sh +++ b/t/t9124-git-svn-dcommit-auto-props.sh @@ -24,7 +24,7 @@ test_expect_success 'initialize git svn' ' svn_cmd import -m "import for git svn" . "$svnrepo" ) && rm -rf import && - git svn init "$svnrepo" + git svn init "$svnrepo" && git svn fetch ' diff --git a/t/t9143-git-svn-gc.sh b/t/t9143-git-svn-gc.sh index 337ea59711..4594e1ae2f 100755 --- a/t/t9143-git-svn-gc.sh +++ b/t/t9143-git-svn-gc.sh @@ -37,13 +37,11 @@ test_expect_success 'git svn gc runs' 'git svn gc' test_expect_success 'git svn index removed' '! test -f .git/svn/refs/remotes/git-svn/index' -if perl -MCompress::Zlib -e 0 2>/dev/null +if test -r .git/svn/refs/remotes/git-svn/unhandled.log.gz then test_expect_success 'git svn gc produces a valid gzip file' ' gunzip .git/svn/refs/remotes/git-svn/unhandled.log.gz ' -else - say "# Perl Compress::Zlib unavailable, skipping gunzip test" fi test_expect_success 'git svn gc does not change unhandled.log files' ' diff --git a/t/t9146-git-svn-empty-dirs.sh b/t/t9146-git-svn-empty-dirs.sh index 565365cbd3..158c8e33ef 100755 --- a/t/t9146-git-svn-empty-dirs.sh +++ b/t/t9146-git-svn-empty-dirs.sh @@ -33,7 +33,7 @@ test_expect_success 'more emptiness' ' ' test_expect_success 'git svn rebase creates empty directory' ' - ( cd cloned && git svn rebase ) + ( cd cloned && git svn rebase ) && test -d cloned/"! !" ' diff --git a/t/t9151-svn-mergeinfo.sh b/t/t9151-svn-mergeinfo.sh index 250c651eae..4f6c06ecb2 100755 --- a/t/t9151-svn-mergeinfo.sh +++ b/t/t9151-svn-mergeinfo.sh @@ -18,39 +18,39 @@ test_expect_success 'load svn dump' " test_expect_success 'all svn merges became git merge commits' ' unmarked=$(git rev-list --parents --all --grep=Merge | - grep -v " .* " | cut -f1 -d" ") + grep -v " .* " | cut -f1 -d" ") && [ -z "$unmarked" ] ' test_expect_success 'cherry picks did not become git merge commits' ' bad_cherries=$(git rev-list --parents --all --grep=Cherry | - grep " .* " | cut -f1 -d" ") + grep " .* " | cut -f1 -d" ") && [ -z "$bad_cherries" ] ' test_expect_success 'svn non-merge merge commits did not become git merge commits' ' bad_non_merges=$(git rev-list --parents --all --grep=non-merge | - grep " .* " | cut -f1 -d" ") + grep " .* " | cut -f1 -d" ") && [ -z "$bad_non_merges" ] ' test_expect_success 'commit made to merged branch is reachable from the merge' ' - before_commit=$(git rev-list --all --grep="trunk commit before merging trunk to b2") - merge_commit=$(git rev-list --all --grep="Merge trunk to b2") - not_reachable=$(git rev-list -1 $before_commit --not $merge_commit) + before_commit=$(git rev-list --all --grep="trunk commit before merging trunk to b2") && + merge_commit=$(git rev-list --all --grep="Merge trunk to b2") && + not_reachable=$(git rev-list -1 $before_commit --not $merge_commit) && [ -z "$not_reachable" ] ' test_expect_success 'merging two branches in one commit is detected correctly' ' - f1_commit=$(git rev-list --all --grep="make f1 branch from trunk") - f2_commit=$(git rev-list --all --grep="make f2 branch from trunk") - merge_commit=$(git rev-list --all --grep="Merge f1 and f2 to trunk") - not_reachable=$(git rev-list -1 $f1_commit $f2_commit --not $merge_commit) + f1_commit=$(git rev-list --all --grep="make f1 branch from trunk") && + f2_commit=$(git rev-list --all --grep="make f2 branch from trunk") && + merge_commit=$(git rev-list --all --grep="Merge f1 and f2 to trunk") && + not_reachable=$(git rev-list -1 $f1_commit $f2_commit --not $merge_commit) && [ -z "$not_reachable" ] ' test_expect_failure 'everything got merged in the end' ' - unmerged=$(git rev-list --all --not master) + unmerged=$(git rev-list --all --not master) && [ -z "$unmerged" ] ' diff --git a/t/t9158-git-svn-mergeinfo.sh b/t/t9158-git-svn-mergeinfo.sh new file mode 100644 index 0000000000..3ab43902b3 --- /dev/null +++ b/t/t9158-git-svn-mergeinfo.sh @@ -0,0 +1,41 @@ +#!/bin/sh +# +# Copyright (c) 2010 Steven Walter +# + +test_description='git svn mergeinfo propagation' + +. ./lib-git-svn.sh + +say 'define NO_SVN_TESTS to skip git svn tests' + +test_expect_success 'initialize source svn repo' ' + svn_cmd mkdir -m x "$svnrepo"/trunk && + svn_cmd co "$svnrepo"/trunk "$SVN_TREE" && + ( + cd "$SVN_TREE" && + touch foo && + svn_cmd add foo && + svn_cmd commit -m "initial commit" + ) && + rm -rf "$SVN_TREE" +' + +test_expect_success 'clone svn repo' ' + git svn init "$svnrepo"/trunk && + git svn fetch +' + +test_expect_success 'change svn:mergeinfo' ' + touch bar && + git add bar && + git commit -m "bar" && + git svn dcommit --mergeinfo="/branches/foo:1-10" +' + +test_expect_success 'verify svn:mergeinfo' ' + mergeinfo=$(svn_cmd propget svn:mergeinfo "$svnrepo"/trunk) + test "$mergeinfo" = "/branches/foo:1-10" +' + +test_done diff --git a/t/t9300-fast-import.sh b/t/t9300-fast-import.sh index 3c0cf0509d..e8034d410f 100755 --- a/t/t9300-fast-import.sh +++ b/t/t9300-fast-import.sh @@ -321,7 +321,7 @@ test_expect_success \ 'for p in .git/objects/pack/*.pack;do git verify-pack $p||exit;done' test_expect_success \ 'C: validate reuse existing blob' \ - 'test $newf = `git rev-parse --verify branch:file2/newf` + 'test $newf = `git rev-parse --verify branch:file2/newf` && test $oldf = `git rev-parse --verify branch:file2/oldf`' cat >expect <<EOF @@ -928,6 +928,114 @@ test_expect_success \ git diff-tree -C --find-copies-harder -r N5^^ N5 >actual && compare_diff_raw expect actual' +test_expect_success \ + 'N: reject foo/ syntax' \ + 'subdir=$(git rev-parse refs/heads/branch^0:file2) && + test_must_fail git fast-import <<-INPUT_END + commit refs/heads/N5B + committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE + data <<COMMIT + copy with invalid syntax + COMMIT + + from refs/heads/branch^0 + M 040000 $subdir file3/ + INPUT_END' + +test_expect_success \ + 'N: copy to root by id and modify' \ + 'echo "hello, world" >expect.foo && + echo hello >expect.bar && + git fast-import <<-SETUP_END && + commit refs/heads/N7 + committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE + data <<COMMIT + hello, tree + COMMIT + + deleteall + M 644 inline foo/bar + data <<EOF + hello + EOF + SETUP_END + + tree=$(git rev-parse --verify N7:) && + git fast-import <<-INPUT_END && + commit refs/heads/N8 + committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE + data <<COMMIT + copy to root by id and modify + COMMIT + + M 040000 $tree "" + M 644 inline foo/foo + data <<EOF + hello, world + EOF + INPUT_END + git show N8:foo/foo >actual.foo && + git show N8:foo/bar >actual.bar && + test_cmp expect.foo actual.foo && + test_cmp expect.bar actual.bar' + +test_expect_success \ + 'N: extract subtree' \ + 'branch=$(git rev-parse --verify refs/heads/branch^{tree}) && + cat >input <<-INPUT_END && + commit refs/heads/N9 + committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE + data <<COMMIT + extract subtree branch:newdir + COMMIT + + M 040000 $branch "" + C "newdir" "" + INPUT_END + git fast-import <input && + git diff --exit-code branch:newdir N9' + +test_expect_success \ + 'N: modify subtree, extract it, and modify again' \ + 'echo hello >expect.baz && + echo hello, world >expect.qux && + git fast-import <<-SETUP_END && + commit refs/heads/N10 + committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE + data <<COMMIT + hello, tree + COMMIT + + deleteall + M 644 inline foo/bar/baz + data <<EOF + hello + EOF + SETUP_END + + tree=$(git rev-parse --verify N10:) && + git fast-import <<-INPUT_END && + commit refs/heads/N11 + committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE + data <<COMMIT + copy to root by id and modify + COMMIT + + M 040000 $tree "" + M 100644 inline foo/bar/qux + data <<EOF + hello, world + EOF + R "foo" "" + C "bar/qux" "bar/quux" + INPUT_END + git show N11:bar/baz >actual.baz && + git show N11:bar/qux >actual.qux && + git show N11:bar/quux >actual.quux && + test_cmp expect.baz actual.baz && + test_cmp expect.qux actual.qux && + test_cmp expect.qux actual.quux' + ### ### series O ### diff --git a/t/t9301-fast-import-notes.sh b/t/t9301-fast-import-notes.sh index a5c99d8507..7cf8cd8a2f 100755 --- a/t/t9301-fast-import-notes.sh +++ b/t/t9301-fast-import-notes.sh @@ -255,13 +255,18 @@ EOF INPUT_END +whitespace=" " + cat >expect <<EXPECT_END fourth commit pre-prefix of note for fourth commit +$whitespace prefix of note for fourth commit +$whitespace third note for fourth commit third commit prefix of note for third commit +$whitespace third note for third commit second commit third note for second commit diff --git a/t/t9350-fast-export.sh b/t/t9350-fast-export.sh index 8c8e679468..f823c05305 100755 --- a/t/t9350-fast-export.sh +++ b/t/t9350-fast-export.sh @@ -26,7 +26,7 @@ test_expect_success 'setup' ' test_tick && git tag rein && git checkout -b wer HEAD^ && - echo lange > file2 + echo lange > file2 && test_tick && git commit -m sitzt file2 && test_tick && diff --git a/t/t9400-git-cvsserver-server.sh b/t/t9400-git-cvsserver-server.sh index 36c457e7f2..9199550ef4 100755 --- a/t/t9400-git-cvsserver-server.sh +++ b/t/t9400-git-cvsserver-server.sh @@ -57,7 +57,7 @@ test_expect_success 'setup' ' # as argument to co -d test_expect_success 'basic checkout' \ 'GIT_CONFIG="$git_config" cvs -Q co -d cvswork master && - test "$(echo $(grep -v ^D cvswork/CVS/Entries|cut -d/ -f2,3,5 | head -n 1))" = "empty/1.1/" + test "$(echo $(grep -v ^D cvswork/CVS/Entries|cut -d/ -f2,3,5 | head -n 1))" = "empty/1.1/" && test "$(echo $(grep -v ^D cvswork/CVS/Entries|cut -d/ -f2,3,5 | sed -ne \$p))" = "secondrootfile/1.1/"' #------------------------ diff --git a/t/t9401-git-cvsserver-crlf.sh b/t/t9401-git-cvsserver-crlf.sh index 1bbfd824e5..ff6d6fb473 100755 --- a/t/t9401-git-cvsserver-crlf.sh +++ b/t/t9401-git-cvsserver-crlf.sh @@ -70,7 +70,7 @@ test_expect_success 'setup' ' mkdir subdir && echo "Another text file" > subdir/file.h && echo "Another binary: Q (this time CR)" | q_to_cr > subdir/withCr.bin && - echo "Mixed up NUL, but marked text: Q <- there" | q_to_nul > mixedUp.c + echo "Mixed up NUL, but marked text: Q <- there" | q_to_nul > mixedUp.c && echo "Unspecified" > subdir/unspecified.other && echo "/*.bin -crlf" > .gitattributes && echo "/*.c crlf" >> .gitattributes && diff --git a/t/test-lib.sh b/t/test-lib.sh index 38e5a59ff7..48fa516004 100644 --- a/t/test-lib.sh +++ b/t/test-lib.sh @@ -305,6 +305,17 @@ remove_cr () { tr '\015' Q | sed -e 's/Q$//' } +# In some bourne shell implementations, the "unset" builtin returns +# nonzero status when a variable to be unset was not set in the first +# place. +# +# Use sane_unset when that should not be considered an error. + +sane_unset () { + unset "$@" + return 0 +} + test_tick () { if test -z "${test_tick+set}" then @@ -521,24 +532,6 @@ test_expect_success () { echo >&3 "" } -test_expect_code () { - test "$#" = 4 && { prereq=$1; shift; } || prereq= - test "$#" = 3 || - error "bug in the test script: not 3 or 4 parameters to test-expect-code" - if ! test_skip "$@" - then - say >&3 "expecting exit code $1: $3" - test_run_ "$3" - if [ "$?" = 0 -a "$eval_ret" = "$1" ] - then - test_ok_ "$2" - else - test_failure_ "$@" - fi - fi - echo >&3 "" -} - # test_external runs external test scripts that provide continuous # test output about their progress, and succeeds/fails on # zero/non-zero exit code. It outputs the test output on stdout even @@ -654,6 +647,28 @@ test_path_is_missing () { fi } +# test_line_count checks that a file has the number of lines it +# ought to. For example: +# +# test_expect_success 'produce exactly one line of output' ' +# do something >output && +# test_line_count = 1 output +# ' +# +# is like "test $(wc -l <output) = 1" except that it passes the +# output through when the number of lines is wrong. + +test_line_count () { + if test $# != 3 + then + error "bug in the test script: not 3 parameters to test_line_count" + elif ! test $(wc -l <"$3") "$1" "$2" + then + echo "test_line_count: line count for $3 !$1 $2" + cat "$3" + return 1 + fi +} # This is not among top-level (test_expect_success | test_expect_failure) # but is a prefix that can be used in the test script, like: @@ -707,6 +722,28 @@ test_might_fail () { return 0 } +# Similar to test_must_fail and test_might_fail, but check that a +# given command exited with a given exit code. Meant to be used as: +# +# test_expect_success 'Merge with d/f conflicts' ' +# test_expect_code 1 git merge "merge msg" B master +# ' + +test_expect_code () { + want_code=$1 + shift + "$@" + exit_code=$? + if test $exit_code = $want_code + then + echo >&2 "test_expect_code: command exited with $exit_code: $*" + return 0 + else + echo >&2 "test_expect_code: command exited with $exit_code, we wanted $want_code $*" + return 1 + fi +} + # test_cmp is a helper function to compare actual and expected output. # You can use it like: # @@ -4,6 +4,9 @@ #include "tree.h" #include "blob.h" +#define PGP_SIGNATURE "-----BEGIN PGP SIGNATURE-----" +#define PGP_MESSAGE "-----BEGIN PGP MESSAGE-----" + const char *tag_type = "tag"; struct object *deref_tag(struct object *o, const char *warn, int warnlen) @@ -133,3 +136,15 @@ int parse_tag(struct tag *item) free(data); return ret; } + +size_t parse_signature(const char *buf, unsigned long size) +{ + char *eol; + size_t len = 0; + while (len < size && prefixcmp(buf + len, PGP_SIGNATURE) && + prefixcmp(buf + len, PGP_MESSAGE)) { + eol = memchr(buf + len, '\n', size - len); + len += eol ? eol - (buf + len) + 1 : size - len; + } + return len; +} @@ -16,5 +16,6 @@ extern struct tag *lookup_tag(const unsigned char *sha1); extern int parse_tag_buffer(struct tag *item, void *data, unsigned long size); extern int parse_tag(struct tag *item); extern struct object *deref_tag(struct object *, const char *, int); +extern size_t parse_signature(const char *buf, unsigned long size); #endif /* TAG_H */ diff --git a/test-parse-options.c b/test-parse-options.c index acd1a2ba70..0828592162 100644 --- a/test-parse-options.c +++ b/test-parse-options.c @@ -66,9 +66,9 @@ int main(int argc, const char **argv) "negative ambiguity", PARSE_OPT_NOARG | PARSE_OPT_NONEG }, OPT_GROUP("Standard options"), OPT__ABBREV(&abbrev), - OPT__VERBOSE(&verbose), - OPT__DRY_RUN(&dry_run), - OPT__QUIET(&quiet), + OPT__VERBOSE(&verbose, "be verbose"), + OPT__DRY_RUN(&dry_run, "dry run"), + OPT__QUIET(&quiet, "be quiet"), OPT_END(), }; int i; diff --git a/thread-utils.h b/thread-utils.h index 1727a03333..6fb98c333c 100644 --- a/thread-utils.h +++ b/thread-utils.h @@ -1,7 +1,11 @@ #ifndef THREAD_COMPAT_H #define THREAD_COMPAT_H +#ifndef NO_PTHREADS +#include <pthread.h> + extern int online_cpus(void); extern int init_recursive_mutex(pthread_mutex_t*); +#endif #endif /* THREAD_COMPAT_H */ diff --git a/transport-helper.c b/transport-helper.c index acfc88e3f1..4e4754c32b 100644 --- a/transport-helper.c +++ b/transport-helper.c @@ -8,6 +8,7 @@ #include "quote.h" #include "remote.h" #include "string-list.h" +#include "thread-utils.h" static int debug; @@ -862,3 +863,314 @@ int transport_helper_init(struct transport *transport, const char *name) transport->smart_options = &(data->transport_options); return 0; } + +/* + * Linux pipes can buffer 65536 bytes at once (and most platforms can + * buffer less), so attempt reads and writes with up to that size. + */ +#define BUFFERSIZE 65536 +/* This should be enough to hold debugging message. */ +#define PBUFFERSIZE 8192 + +/* Print bidirectional transfer loop debug message. */ +static void transfer_debug(const char *fmt, ...) +{ + va_list args; + char msgbuf[PBUFFERSIZE]; + static int debug_enabled = -1; + + if (debug_enabled < 0) + debug_enabled = getenv("GIT_TRANSLOOP_DEBUG") ? 1 : 0; + if (!debug_enabled) + return; + + va_start(args, fmt); + vsnprintf(msgbuf, PBUFFERSIZE, fmt, args); + va_end(args); + fprintf(stderr, "Transfer loop debugging: %s\n", msgbuf); +} + +/* Stream state: More data may be coming in this direction. */ +#define SSTATE_TRANSFERING 0 +/* + * Stream state: No more data coming in this direction, flushing rest of + * data. + */ +#define SSTATE_FLUSHING 1 +/* Stream state: Transfer in this direction finished. */ +#define SSTATE_FINISHED 2 + +#define STATE_NEEDS_READING(state) ((state) <= SSTATE_TRANSFERING) +#define STATE_NEEDS_WRITING(state) ((state) <= SSTATE_FLUSHING) +#define STATE_NEEDS_CLOSING(state) ((state) == SSTATE_FLUSHING) + +/* Unidirectional transfer. */ +struct unidirectional_transfer { + /* Source */ + int src; + /* Destination */ + int dest; + /* Is source socket? */ + int src_is_sock; + /* Is destination socket? */ + int dest_is_sock; + /* Transfer state (TRANSFERING/FLUSHING/FINISHED) */ + int state; + /* Buffer. */ + char buf[BUFFERSIZE]; + /* Buffer used. */ + size_t bufuse; + /* Name of source. */ + const char *src_name; + /* Name of destination. */ + const char *dest_name; +}; + +/* Closes the target (for writing) if transfer has finished. */ +static void udt_close_if_finished(struct unidirectional_transfer *t) +{ + if (STATE_NEEDS_CLOSING(t->state) && !t->bufuse) { + t->state = SSTATE_FINISHED; + if (t->dest_is_sock) + shutdown(t->dest, SHUT_WR); + else + close(t->dest); + transfer_debug("Closed %s.", t->dest_name); + } +} + +/* + * Tries to read read data from source into buffer. If buffer is full, + * no data is read. Returns 0 on success, -1 on error. + */ +static int udt_do_read(struct unidirectional_transfer *t) +{ + ssize_t bytes; + + if (t->bufuse == BUFFERSIZE) + return 0; /* No space for more. */ + + transfer_debug("%s is readable", t->src_name); + bytes = read(t->src, t->buf + t->bufuse, BUFFERSIZE - t->bufuse); + if (bytes < 0 && errno != EWOULDBLOCK && errno != EAGAIN && + errno != EINTR) { + error("read(%s) failed: %s", t->src_name, strerror(errno)); + return -1; + } else if (bytes == 0) { + transfer_debug("%s EOF (with %i bytes in buffer)", + t->src_name, t->bufuse); + t->state = SSTATE_FLUSHING; + } else if (bytes > 0) { + t->bufuse += bytes; + transfer_debug("Read %i bytes from %s (buffer now at %i)", + (int)bytes, t->src_name, (int)t->bufuse); + } + return 0; +} + +/* Tries to write data from buffer into destination. If buffer is empty, + * no data is written. Returns 0 on success, -1 on error. + */ +static int udt_do_write(struct unidirectional_transfer *t) +{ + size_t bytes; + + if (t->bufuse == 0) + return 0; /* Nothing to write. */ + + transfer_debug("%s is writable", t->dest_name); + bytes = write(t->dest, t->buf, t->bufuse); + if (bytes < 0 && errno != EWOULDBLOCK && errno != EAGAIN && + errno != EINTR) { + error("write(%s) failed: %s", t->dest_name, strerror(errno)); + return -1; + } else if (bytes > 0) { + t->bufuse -= bytes; + if (t->bufuse) + memmove(t->buf, t->buf + bytes, t->bufuse); + transfer_debug("Wrote %i bytes to %s (buffer now at %i)", + (int)bytes, t->dest_name, (int)t->bufuse); + } + return 0; +} + + +/* State of bidirectional transfer loop. */ +struct bidirectional_transfer_state { + /* Direction from program to git. */ + struct unidirectional_transfer ptg; + /* Direction from git to program. */ + struct unidirectional_transfer gtp; +}; + +static void *udt_copy_task_routine(void *udt) +{ + struct unidirectional_transfer *t = (struct unidirectional_transfer *)udt; + while (t->state != SSTATE_FINISHED) { + if (STATE_NEEDS_READING(t->state)) + if (udt_do_read(t)) + return NULL; + if (STATE_NEEDS_WRITING(t->state)) + if (udt_do_write(t)) + return NULL; + if (STATE_NEEDS_CLOSING(t->state)) + udt_close_if_finished(t); + } + return udt; /* Just some non-NULL value. */ +} + +#ifndef NO_PTHREADS + +/* + * Join thread, with apporiate errors on failure. Name is name for the + * thread (for error messages). Returns 0 on success, 1 on failure. + */ +static int tloop_join(pthread_t thread, const char *name) +{ + int err; + void *tret; + err = pthread_join(thread, &tret); + if (!tret) { + error("%s thread failed", name); + return 1; + } + if (err) { + error("%s thread failed to join: %s", name, strerror(err)); + return 1; + } + return 0; +} + +/* + * Spawn the transfer tasks and then wait for them. Returns 0 on success, + * -1 on failure. + */ +static int tloop_spawnwait_tasks(struct bidirectional_transfer_state *s) +{ + pthread_t gtp_thread; + pthread_t ptg_thread; + int err; + int ret = 0; + err = pthread_create(>p_thread, NULL, udt_copy_task_routine, + &s->gtp); + if (err) + die("Can't start thread for copying data: %s", strerror(err)); + err = pthread_create(&ptg_thread, NULL, udt_copy_task_routine, + &s->ptg); + if (err) + die("Can't start thread for copying data: %s", strerror(err)); + + ret |= tloop_join(gtp_thread, "Git to program copy"); + ret |= tloop_join(ptg_thread, "Program to git copy"); + return ret; +} +#else + +/* Close the source and target (for writing) for transfer. */ +static void udt_kill_transfer(struct unidirectional_transfer *t) +{ + t->state = SSTATE_FINISHED; + /* + * Socket read end left open isn't a disaster if nobody + * attempts to read from it (mingw compat headers do not + * have SHUT_RD)... + * + * We can't fully close the socket since otherwise gtp + * task would first close the socket it sends data to + * while closing the ptg file descriptors. + */ + if (!t->src_is_sock) + close(t->src); + if (t->dest_is_sock) + shutdown(t->dest, SHUT_WR); + else + close(t->dest); +} + +/* + * Join process, with apporiate errors on failure. Name is name for the + * process (for error messages). Returns 0 on success, 1 on failure. + */ +static int tloop_join(pid_t pid, const char *name) +{ + int tret; + if (waitpid(pid, &tret, 0) < 0) { + error("%s process failed to wait: %s", name, strerror(errno)); + return 1; + } + if (!WIFEXITED(tret) || WEXITSTATUS(tret)) { + error("%s process failed", name); + return 1; + } + return 0; +} + +/* + * Spawn the transfer tasks and then wait for them. Returns 0 on success, + * -1 on failure. + */ +static int tloop_spawnwait_tasks(struct bidirectional_transfer_state *s) +{ + pid_t pid1, pid2; + int ret = 0; + + /* Fork thread #1: git to program. */ + pid1 = fork(); + if (pid1 < 0) + die_errno("Can't start thread for copying data"); + else if (pid1 == 0) { + udt_kill_transfer(&s->ptg); + exit(udt_copy_task_routine(&s->gtp) ? 0 : 1); + } + + /* Fork thread #2: program to git. */ + pid2 = fork(); + if (pid2 < 0) + die_errno("Can't start thread for copying data"); + else if (pid2 == 0) { + udt_kill_transfer(&s->gtp); + exit(udt_copy_task_routine(&s->ptg) ? 0 : 1); + } + + /* + * Close both streams in parent as to not interfere with + * end of file detection and wait for both tasks to finish. + */ + udt_kill_transfer(&s->gtp); + udt_kill_transfer(&s->ptg); + ret |= tloop_join(pid1, "Git to program copy"); + ret |= tloop_join(pid2, "Program to git copy"); + return ret; +} +#endif + +/* + * Copies data from stdin to output and from input to stdout simultaneously. + * Additionally filtering through given filter. If filter is NULL, uses + * identity filter. + */ +int bidirectional_transfer_loop(int input, int output) +{ + struct bidirectional_transfer_state state; + + /* Fill the state fields. */ + state.ptg.src = input; + state.ptg.dest = 1; + state.ptg.src_is_sock = (input == output); + state.ptg.dest_is_sock = 0; + state.ptg.state = SSTATE_TRANSFERING; + state.ptg.bufuse = 0; + state.ptg.src_name = "remote input"; + state.ptg.dest_name = "stdout"; + + state.gtp.src = 0; + state.gtp.dest = output; + state.gtp.src_is_sock = 0; + state.gtp.dest_is_sock = (input == output); + state.gtp.state = SSTATE_TRANSFERING; + state.gtp.bufuse = 0; + state.gtp.src_name = "stdin"; + state.gtp.dest_name = "remote output"; + + return tloop_spawnwait_tasks(&state); +} diff --git a/transport.h b/transport.h index c59d97388e..e803c0e7ba 100644 --- a/transport.h +++ b/transport.h @@ -154,6 +154,7 @@ int transport_connect(struct transport *transport, const char *name, /* Transport methods defined outside transport.c */ int transport_helper_init(struct transport *transport, const char *name); +int bidirectional_transfer_loop(int input, int output); /* common methods used by transport.c and builtin-send-pack.c */ void transport_verify_remote_names(int nr_heads, const char **heads); diff --git a/unpack-trees.c b/unpack-trees.c index 803445aa7b..d5a453079a 100644 --- a/unpack-trees.c +++ b/unpack-trees.c @@ -53,6 +53,7 @@ const char *unpack_plumbing_errors[NB_UNPACK_TREES_ERROR_TYPES] = { void setup_unpack_trees_porcelain(struct unpack_trees_options *opts, const char *cmd) { + int i; const char **msgs = opts->msgs; const char *msg; char *tmp; @@ -96,6 +97,9 @@ void setup_unpack_trees_porcelain(struct unpack_trees_options *opts, "The following Working tree files would be removed by sparse checkout update:\n%s"; opts->show_all_errors = 1; + /* rejected paths may not have a static buffer */ + for (i = 0; i < ARRAY_SIZE(opts->unpack_rejects); i++) + opts->unpack_rejects[i].strdup_strings = 1; } static void add_entry(struct unpack_trees_options *o, struct cache_entry *ce, @@ -124,7 +128,6 @@ static int add_rejected_path(struct unpack_trees_options *o, enum unpack_trees_error_types e, const char *path) { - struct rejected_paths_list *newentry; if (!o->show_all_errors) return error(ERRORMSG(o, e), path); @@ -132,45 +135,28 @@ static int add_rejected_path(struct unpack_trees_options *o, * Otherwise, insert in a list for future display by * display_error_msgs() */ - newentry = xmalloc(sizeof(struct rejected_paths_list)); - newentry->path = (char *)path; - newentry->next = o->unpack_rejects[e]; - o->unpack_rejects[e] = newentry; + string_list_append(&o->unpack_rejects[e], path); return -1; } /* - * free all the structures allocated for the error <e> - */ -static void free_rejected_paths(struct unpack_trees_options *o, - enum unpack_trees_error_types e) -{ - while (o->unpack_rejects[e]) { - struct rejected_paths_list *del = o->unpack_rejects[e]; - o->unpack_rejects[e] = o->unpack_rejects[e]->next; - free(del); - } - free(o->unpack_rejects[e]); -} - -/* * display all the error messages stored in a nice way */ static void display_error_msgs(struct unpack_trees_options *o) { - int e; + int e, i; int something_displayed = 0; for (e = 0; e < NB_UNPACK_TREES_ERROR_TYPES; e++) { - if (o->unpack_rejects[e]) { - struct rejected_paths_list *rp; + struct string_list *rejects = &o->unpack_rejects[e]; + if (rejects->nr > 0) { struct strbuf path = STRBUF_INIT; something_displayed = 1; - for (rp = o->unpack_rejects[e]; rp; rp = rp->next) - strbuf_addf(&path, "\t%s\n", rp->path); + for (i = 0; i < rejects->nr; i++) + strbuf_addf(&path, "\t%s\n", rejects->items[i].string); error(ERRORMSG(o, e), path.buf); strbuf_release(&path); - free_rejected_paths(o, e); } + string_list_clear(rejects, 0); } if (something_displayed) printf("Aborting\n"); @@ -182,7 +168,7 @@ static void display_error_msgs(struct unpack_trees_options *o) */ static void unlink_entry(struct cache_entry *ce) { - if (has_symlink_or_noent_leading_path(ce->name, ce_namelen(ce))) + if (!check_leading_path(ce->name, ce_namelen(ce))) return; if (remove_or_warn(ce->ce_mode, ce->name)) return; @@ -1127,14 +1113,65 @@ static int verify_clean_subdirectory(struct cache_entry *ce, * See if we can find a case-insensitive match in the index that also * matches the stat information, and assume it's that other file! */ -static int icase_exists(struct unpack_trees_options *o, struct cache_entry *dst, struct stat *st) +static int icase_exists(struct unpack_trees_options *o, const char *name, int len, struct stat *st) { struct cache_entry *src; - src = index_name_exists(o->src_index, dst->name, ce_namelen(dst), 1); + src = index_name_exists(o->src_index, name, len, 1); return src && !ie_match_stat(o->src_index, src, st, CE_MATCH_IGNORE_VALID|CE_MATCH_IGNORE_SKIP_WORKTREE); } +static int check_ok_to_remove(const char *name, int len, int dtype, + struct cache_entry *ce, struct stat *st, + enum unpack_trees_error_types error_type, + struct unpack_trees_options *o) +{ + struct cache_entry *result; + + /* + * It may be that the 'lstat()' succeeded even though + * target 'ce' was absent, because there is an old + * entry that is different only in case.. + * + * Ignore that lstat() if it matches. + */ + if (ignore_case && icase_exists(o, name, len, st)) + return 0; + + if (o->dir && excluded(o->dir, name, &dtype)) + /* + * ce->name is explicitly excluded, so it is Ok to + * overwrite it. + */ + return 0; + if (S_ISDIR(st->st_mode)) { + /* + * We are checking out path "foo" and + * found "foo/." in the working tree. + * This is tricky -- if we have modified + * files that are in "foo/" we would lose + * them. + */ + if (verify_clean_subdirectory(ce, error_type, o) < 0) + return -1; + return 0; + } + + /* + * The previous round may already have decided to + * delete this path, which is in a subdirectory that + * is being replaced with a blob. + */ + result = index_name_exists(&o->result, name, len, 0); + if (result) { + if (result->ce_flags & CE_REMOVE) + return 0; + } + + return o->gently ? -1 : + add_rejected_path(o, error_type, name); +} + /* * We do not want to remove or overwrite a working tree file that * is not tracked, unless it is ignored. @@ -1143,63 +1180,31 @@ static int verify_absent_1(struct cache_entry *ce, enum unpack_trees_error_types error_type, struct unpack_trees_options *o) { + int len; struct stat st; if (o->index_only || o->reset || !o->update) return 0; - if (has_symlink_or_noent_leading_path(ce->name, ce_namelen(ce))) + len = check_leading_path(ce->name, ce_namelen(ce)); + if (!len) return 0; + else if (len > 0) { + char path[PATH_MAX + 1]; + memcpy(path, ce->name, len); + path[len] = 0; + lstat(path, &st); + + return check_ok_to_remove(path, len, DT_UNKNOWN, NULL, &st, + error_type, o); + } else if (!lstat(ce->name, &st)) + return check_ok_to_remove(ce->name, ce_namelen(ce), + ce_to_dtype(ce), ce, &st, + error_type, o); - if (!lstat(ce->name, &st)) { - int dtype = ce_to_dtype(ce); - struct cache_entry *result; - - /* - * It may be that the 'lstat()' succeeded even though - * target 'ce' was absent, because there is an old - * entry that is different only in case.. - * - * Ignore that lstat() if it matches. - */ - if (ignore_case && icase_exists(o, ce, &st)) - return 0; - - if (o->dir && excluded(o->dir, ce->name, &dtype)) - /* - * ce->name is explicitly excluded, so it is Ok to - * overwrite it. - */ - return 0; - if (S_ISDIR(st.st_mode)) { - /* - * We are checking out path "foo" and - * found "foo/." in the working tree. - * This is tricky -- if we have modified - * files that are in "foo/" we would lose - * them. - */ - if (verify_clean_subdirectory(ce, error_type, o) < 0) - return -1; - return 0; - } - - /* - * The previous round may already have decided to - * delete this path, which is in a subdirectory that - * is being replaced with a blob. - */ - result = index_name_exists(&o->result, ce->name, ce_namelen(ce), 0); - if (result) { - if (result->ce_flags & CE_REMOVE) - return 0; - } - - return o->gently ? -1 : - add_rejected_path(o, error_type, ce->name); - } return 0; } + static int verify_absent(struct cache_entry *ce, enum unpack_trees_error_types error_type, struct unpack_trees_options *o) diff --git a/unpack-trees.h b/unpack-trees.h index 7c0187d11a..cd11a08365 100644 --- a/unpack-trees.h +++ b/unpack-trees.h @@ -1,6 +1,8 @@ #ifndef UNPACK_TREES_H #define UNPACK_TREES_H +#include "string-list.h" + #define MAX_UNPACK_TREES 8 struct unpack_trees_options; @@ -29,11 +31,6 @@ enum unpack_trees_error_types { void setup_unpack_trees_porcelain(struct unpack_trees_options *opts, const char *cmd); -struct rejected_paths_list { - char *path; - struct rejected_paths_list *next; -}; - struct unpack_trees_options { unsigned int reset, merge, @@ -59,7 +56,7 @@ struct unpack_trees_options { * Store error messages in an array, each case * corresponding to a error message type */ - struct rejected_paths_list *unpack_rejects[NB_UNPACK_TREES_ERROR_TYPES]; + struct string_list unpack_rejects[NB_UNPACK_TREES_ERROR_TYPES]; int head_idx; int merge_size; @@ -3,12 +3,11 @@ */ #include "cache.h" -static void try_to_free_builtin(size_t size) +static void do_nothing(size_t size) { - release_pack_memory(size, -1); } -static void (*try_to_free_routine)(size_t size) = try_to_free_builtin; +static void (*try_to_free_routine)(size_t size) = do_nothing; try_to_free_t set_try_to_free_routine(try_to_free_t routine) { @@ -108,21 +107,6 @@ void *xcalloc(size_t nmemb, size_t size) return ret; } -void *xmmap(void *start, size_t length, - int prot, int flags, int fd, off_t offset) -{ - void *ret = mmap(start, length, prot, flags, fd, offset); - if (ret == MAP_FAILED) { - if (!length) - return NULL; - release_pack_memory(length, fd); - ret = mmap(start, length, prot, flags, fd, offset); - if (ret == MAP_FAILED) - die_errno("Out of memory? mmap failed"); - } - return ret; -} - /* * xread() is the same a read(), but it automatically restarts read() * operations with a recoverable error (EAGAIN and EINTR). xread() @@ -219,111 +203,127 @@ int xmkstemp(char *template) return fd; } -int xmkstemp_mode(char *template, int mode) +/* git_mkstemp() - create tmp file honoring TMPDIR variable */ +int git_mkstemp(char *path, size_t len, const char *template) { - int fd; - - fd = git_mkstemp_mode(template, mode); - if (fd < 0) - die_errno("Unable to create temporary file"); - return fd; + const char *tmp; + size_t n; + + tmp = getenv("TMPDIR"); + if (!tmp) + tmp = "/tmp"; + n = snprintf(path, len, "%s/%s", tmp, template); + if (len <= n) { + errno = ENAMETOOLONG; + return -1; + } + return mkstemp(path); } -/* - * zlib wrappers to make sure we don't silently miss errors - * at init time. - */ -void git_inflate_init(z_streamp strm) +/* git_mkstemps() - create tmp file with suffix honoring TMPDIR variable. */ +int git_mkstemps(char *path, size_t len, const char *template, int suffix_len) { - const char *err; - - switch (inflateInit(strm)) { - case Z_OK: - return; - - case Z_MEM_ERROR: - err = "out of memory"; - break; - case Z_VERSION_ERROR: - err = "wrong version"; - break; - default: - err = "error"; + const char *tmp; + size_t n; + + tmp = getenv("TMPDIR"); + if (!tmp) + tmp = "/tmp"; + n = snprintf(path, len, "%s/%s", tmp, template); + if (len <= n) { + errno = ENAMETOOLONG; + return -1; } - die("inflateInit: %s (%s)", err, strm->msg ? strm->msg : "no message"); + return mkstemps(path, suffix_len); } -void git_inflate_end(z_streamp strm) +/* Adapted from libiberty's mkstemp.c. */ + +#undef TMP_MAX +#define TMP_MAX 16384 + +int git_mkstemps_mode(char *pattern, int suffix_len, int mode) { - if (inflateEnd(strm) != Z_OK) - error("inflateEnd: %s", strm->msg ? strm->msg : "failed"); + static const char letters[] = + "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "0123456789"; + static const int num_letters = 62; + uint64_t value; + struct timeval tv; + char *template; + size_t len; + int fd, count; + + len = strlen(pattern); + + if (len < 6 + suffix_len) { + errno = EINVAL; + return -1; + } + + if (strncmp(&pattern[len - 6 - suffix_len], "XXXXXX", 6)) { + errno = EINVAL; + return -1; + } + + /* + * Replace pattern's XXXXXX characters with randomness. + * Try TMP_MAX different filenames. + */ + gettimeofday(&tv, NULL); + value = ((size_t)(tv.tv_usec << 16)) ^ tv.tv_sec ^ getpid(); + template = &pattern[len - 6 - suffix_len]; + for (count = 0; count < TMP_MAX; ++count) { + uint64_t v = value; + /* Fill in the random bits. */ + template[0] = letters[v % num_letters]; v /= num_letters; + template[1] = letters[v % num_letters]; v /= num_letters; + template[2] = letters[v % num_letters]; v /= num_letters; + template[3] = letters[v % num_letters]; v /= num_letters; + template[4] = letters[v % num_letters]; v /= num_letters; + template[5] = letters[v % num_letters]; v /= num_letters; + + fd = open(pattern, O_CREAT | O_EXCL | O_RDWR, mode); + if (fd > 0) + return fd; + /* + * Fatal error (EPERM, ENOSPC etc). + * It doesn't make sense to loop. + */ + if (errno != EEXIST) + break; + /* + * This is a random value. It is only necessary that + * the next TMP_MAX values generated by adding 7777 to + * VALUE are different with (module 2^32). + */ + value += 7777; + } + /* We return the null string if we can't find a unique file name. */ + pattern[0] = '\0'; + return -1; } -int git_inflate(z_streamp strm, int flush) +int git_mkstemp_mode(char *pattern, int mode) { - int ret = inflate(strm, flush); - const char *err; - - switch (ret) { - /* Out of memory is fatal. */ - case Z_MEM_ERROR: - die("inflate: out of memory"); - - /* Data corruption errors: we may want to recover from them (fsck) */ - case Z_NEED_DICT: - err = "needs dictionary"; break; - case Z_DATA_ERROR: - err = "data stream error"; break; - case Z_STREAM_ERROR: - err = "stream consistency error"; break; - default: - err = "unknown error"; break; - - /* Z_BUF_ERROR: normal, needs more space in the output buffer */ - case Z_BUF_ERROR: - case Z_OK: - case Z_STREAM_END: - return ret; - } - error("inflate: %s (%s)", err, strm->msg ? strm->msg : "no message"); - return ret; + /* mkstemp is just mkstemps with no suffix */ + return git_mkstemps_mode(pattern, 0, mode); } -int odb_mkstemp(char *template, size_t limit, const char *pattern) +int gitmkstemps(char *pattern, int suffix_len) { - int fd; - /* - * we let the umask do its job, don't try to be more - * restrictive except to remove write permission. - */ - int mode = 0444; - snprintf(template, limit, "%s/%s", - get_object_directory(), pattern); - fd = git_mkstemp_mode(template, mode); - if (0 <= fd) - return fd; - - /* slow path */ - /* some mkstemp implementations erase template on failure */ - snprintf(template, limit, "%s/%s", - get_object_directory(), pattern); - safe_create_leading_directories(template); - return xmkstemp_mode(template, mode); + return git_mkstemps_mode(pattern, suffix_len, 0600); } -int odb_pack_keep(char *name, size_t namesz, unsigned char *sha1) +int xmkstemp_mode(char *template, int mode) { int fd; - snprintf(name, namesz, "%s/pack/pack-%s.keep", - get_object_directory(), sha1_to_hex(sha1)); - fd = open(name, O_RDWR|O_CREAT|O_EXCL, 0600); - if (0 <= fd) - return fd; - - /* slow path */ - safe_create_leading_directories(name); - return open(name, O_RDWR|O_CREAT|O_EXCL, 0600); + fd = git_mkstemp_mode(template, mode); + if (fd < 0) + die_errno("Unable to create temporary file"); + return fd; } static int warn_if_unremovable(const char *op, const char *file, int rc) diff --git a/wt-status.c b/wt-status.c index fc2438f60b..06ae161c67 100644 --- a/wt-status.c +++ b/wt-status.c @@ -88,7 +88,7 @@ static void wt_status_print_dirty_header(struct wt_status *s, { const char *c = color(WT_STATUS_HEADER, s); - color_fprintf_ln(s->fp, c, "# Changed but not updated:"); + color_fprintf_ln(s->fp, c, "# Changes not staged for commit:"); if (!advice_status_hints) return; if (!has_deleted) @@ -744,10 +744,20 @@ static void wt_shortstatus_status(int null_termination, struct string_list_item const char *one; if (d->head_path) { one = quote_path(d->head_path, -1, &onebuf, s->prefix); + if (*one != '"' && strchr(one, ' ') != NULL) { + putchar('"'); + strbuf_addch(&onebuf, '"'); + one = onebuf.buf; + } printf("%s -> ", one); strbuf_release(&onebuf); } one = quote_path(it->string, -1, &onebuf, s->prefix); + if (*one != '"' && strchr(one, ' ') != NULL) { + putchar('"'); + strbuf_addch(&onebuf, '"'); + one = onebuf.buf; + } printf("%s\n", one); strbuf_release(&onebuf); } diff --git a/zlib.c b/zlib.c new file mode 100644 index 0000000000..c4d58da4e9 --- /dev/null +++ b/zlib.c @@ -0,0 +1,61 @@ +/* + * zlib wrappers to make sure we don't silently miss errors + * at init time. + */ +#include "cache.h" + +void git_inflate_init(z_streamp strm) +{ + const char *err; + + switch (inflateInit(strm)) { + case Z_OK: + return; + + case Z_MEM_ERROR: + err = "out of memory"; + break; + case Z_VERSION_ERROR: + err = "wrong version"; + break; + default: + err = "error"; + } + die("inflateInit: %s (%s)", err, strm->msg ? strm->msg : "no message"); +} + +void git_inflate_end(z_streamp strm) +{ + if (inflateEnd(strm) != Z_OK) + error("inflateEnd: %s", strm->msg ? strm->msg : "failed"); +} + +int git_inflate(z_streamp strm, int flush) +{ + int ret = inflate(strm, flush); + const char *err; + + switch (ret) { + /* Out of memory is fatal. */ + case Z_MEM_ERROR: + die("inflate: out of memory"); + + /* Data corruption errors: we may want to recover from them (fsck) */ + case Z_NEED_DICT: + err = "needs dictionary"; break; + case Z_DATA_ERROR: + err = "data stream error"; break; + case Z_STREAM_ERROR: + err = "stream consistency error"; break; + default: + err = "unknown error"; break; + + /* Z_BUF_ERROR: normal, needs more space in the output buffer */ + case Z_BUF_ERROR: + case Z_OK: + case Z_STREAM_END: + return ret; + } + error("inflate: %s (%s)", err, strm->msg ? strm->msg : "no message"); + return ret; +} |