Merge branch 'as/pre-push-hook'
authorJunio C Hamano <gitster@pobox.com>
Thu, 24 Jan 2013 05:19:25 +0000 (21:19 -0800)
committerJunio C Hamano <gitster@pobox.com>
Thu, 24 Jan 2013 05:19:25 +0000 (21:19 -0800)
Add an extra hook so that "git push" that is run without making
sure what is being pushed is sane can be checked and rejected (as
opposed to the user deciding not pushing).

* as/pre-push-hook:
Add sample pre-push hook script
push: Add support for pre-push hooks
hooks: Add function to check if a hook exists

86 files changed:
.gitignore
Documentation/RelNotes/1.8.1.1.txt
Documentation/RelNotes/1.8.1.2.txt [new file with mode: 0644]
Documentation/RelNotes/1.8.2.txt
Documentation/config.txt
Documentation/git-check-ignore.txt [new file with mode: 0644]
Documentation/git-commit-tree.txt
Documentation/git-commit.txt
Documentation/git-cvsserver.txt
Documentation/git-for-each-ref.txt
Documentation/git-log.txt
Documentation/git-p4.txt
Documentation/git-reset.txt
Documentation/git-svn.txt
Documentation/git.txt
Documentation/gitignore.txt
Documentation/technical/api-directory-listing.txt
Makefile
attr.c
builtin.h
builtin/add.c
builtin/blame.c
builtin/check-ignore.c [new file with mode: 0644]
builtin/clean.c
builtin/clone.c
builtin/commit.c
builtin/log.c
builtin/ls-files.c
builtin/reset.c
builtin/shortlog.c
cache.h
command-list.txt
commit.h
contrib/completion/git-completion.bash
contrib/completion/git-completion.tcsh
contrib/hooks/post-receive-email
contrib/remote-helpers/git-remote-hg
contrib/remote-helpers/test-hg-hg-git.sh
contrib/vim/README
diff.c
dir.c
dir.h
git-compat-util.h
git-cvsserver.perl
git-p4.py
git-rebase--interactive.sh
git-remote-testpy.py
git-send-email.perl
git-svn.perl
git.c
gitk-git/gitk
gitk-git/po/sv.po
imap-send.c
log-tree.c
mailmap.c
mailmap.h
parse-options.h
pathspec.c [new file with mode: 0644]
pathspec.h [new file with mode: 0644]
perl/Git/SVN/Editor.pm
perl/Git/SVN/Utils.pm
pretty.c
refs.c
remote.c
revision.c
revision.h
setup.c
string-list.c
string-list.h
t/t0008-ignores.sh [new file with mode: 0755]
t/t2013-checkout-submodule.sh
t/t4203-mailmap.sh
t/t5516-fetch-push.sh
t/t5800-remote-testpy.sh
t/t7061-wtstatus-ignore.sh [new file with mode: 0755]
t/t7102-reset.sh
t/t7106-reset-unborn-branch.sh [new file with mode: 0755]
t/t7500/add-content-and-comment [new file with mode: 0755]
t/t7502-commit.sh
t/t9402-git-cvsserver-refs.sh [new file with mode: 0755]
t/t9800-git-p4-basic.sh
t/t9806-git-p4-options.sh
t/test-lib.sh
unpack-trees.c
upload-pack.c
wt-status.c
index aa258a6bcfe105cdb872c6d77cd9bd19c84e8dc5..6669bf0c6c9a0b42f74e2ed189350a6a9f11f17d 100644 (file)
@@ -22,6 +22,7 @@
 /git-bundle
 /git-cat-file
 /git-check-attr
+/git-check-ignore
 /git-check-ref-format
 /git-checkout
 /git-checkout-index
index 0dc37dcdd4c5e2813871a41b06afa7dbe579b399..6cde07ba290c61c56c9a2e9c429de57d1e8e75bf 100644 (file)
@@ -4,15 +4,56 @@ Git 1.8.1.1 Release Notes
 Fixes since v1.8.1
 ------------------
 
+ * The attribute mechanism didn't allow limiting attributes to be
+   applied to only a single directory itself with "path/" like the
+   exclude mechanism does.
+
+ * When attempting to read the XDG-style $HOME/.config/git/config and
+   finding that $HOME/.config/git is a file, we gave a wrong error
+   message, instead of treating the case as "a custom config file does
+   not exist there" and moving on.
+
  * After failing to create a temporary file using mkstemp(), failing
    pathname was not reported correctly on some platforms.
 
  * http transport was wrong to ask for the username when the
    authentication is done by certificate identity.
 
+ * The behaviour visible to the end users was confusing, when they
+   attempt to kill a process spawned in the editor that was in turn
+   launched by Git with SIGINT (or SIGQUIT), as Git would catch that
+   signal and die.  We ignore these signals now.
+
+ * A child process that was killed by a signal (e.g. SIGINT) was
+   reported in an inconsistent way depending on how the process was
+   spawned by us, with or without a shell in between.
+
  * After "git add -N" and then writing a tree object out of the
    index, the cache-tree data structure got corrupted.
 
+ * "git apply" misbehaved when fixing whitespace breakages by removing
+   excess trailing blank lines in some corner cases.
+
+ * A tar archive created by "git archive" recorded a directory in a
+   way that made NetBSD's implementation of "tar" sometimes unhappy.
+
+ * When "git clone --separate-git-dir=$over_there" is interrupted, it
+   failed to remove the real location of the $GIT_DIR it created.
+   This was most visible when interrupting a submodule update.
+
+ * "git fetch --mirror" and fetch that uses other forms of refspec
+   with wildcard used to attempt to update a symbolic ref that match
+   the wildcard on the receiving end, which made little sense (the
+   real ref that is pointed at by the symbolic ref would be updated
+   anyway).  Symbolic refs no longer are affected by such a fetch.
+
+ * The "log --graph" codepath fell into infinite loop in some
+   corner cases.
+
+ * "git merge" started calling prepare-commit-msg hook like "git
+   commit" does some time ago, but forgot to pay attention to the exit
+   status of the hook.
+
  * "git pack-refs" that ran in parallel to another process that
    created new refs had a race that can lose new ones.
 
@@ -20,10 +61,20 @@ Fixes since v1.8.1
    whose length exactly is the wrap width, "git shortlog -w" failed
    to add a newline after such a line.
 
+ * The way "git svn" asked for password using SSH_ASKPASS and
+   GIT_ASKPASS was not in line with the rest of the system.
+
+ * "gitweb", when sorting by age to show repositories with new
+   activities first, used to sort repositories with absolutely
+   nothing in it early, which was not very useful.
+
  * "gitweb", when sorting by age to show repositories with new
    activities first, used to sort repositories with absolutely
    nothing in it early, which was not very useful.
 
+ * When autoconf is used, any build on a different commit always ran
+   "config.status --recheck" even when unnecessary.
+
  * Some scripted programs written in Python did not get updated when
    PYTHON_PATH changed.
 
diff --git a/Documentation/RelNotes/1.8.1.2.txt b/Documentation/RelNotes/1.8.1.2.txt
new file mode 100644 (file)
index 0000000..76ad0b3
--- /dev/null
@@ -0,0 +1,13 @@
+Git 1.8.1.2 Release Notes
+=========================
+
+Fixes since v1.8.1.1
+--------------------
+
+ * "git archive" did not record uncompressed size in the header when
+   streaming a zip archive, which confused some implementations of unzip.
+
+ * When users spelled "cc:" in lowercase in the fake "header" in the
+   trailer part, "git send-email" failed to pick up the addresses from
+   there. As e-mail headers field names are case insensitive, this
+   script should follow suit and treat "cc:" and "Cc:" the same way.
index da27842f4844fb7f4f7d4ef387eed1c77dec8c93..0ae40f3b25a4971b93e7396abf3501cb08b92e19 100644 (file)
@@ -41,9 +41,14 @@ UI, Workflows & Features
  * The pathspec code learned to grok "foo/**/bar" as a pattern that
    matches "bar" in 0-or-more levels of subdirectory in "foo".
 
+ * "git blame" (and "git diff") learned the "--no-follow" option.
+
  * "git cherry-pick" can be used to replay a root commit to an unborn
    branch.
 
+ * "git commit" can be told to use --cleanup=whitespace by setting the
+   configuration variable commit.cleanup to 'whitespace'.
+
  * "git fetch --mirror" and fetch that uses other forms of refspec
    with wildcard used to attempt to update a symbolic ref that match
    the wildcard on the receiving end, which made little sense (the
@@ -60,6 +65,10 @@ UI, Workflows & Features
    allows patches from rerolled series to be stored under different
    names and makes it easier to reuse cover letter messsages.
 
+ * "git log" and friends can be told with --use-mailmap option to
+   rewrite the names and email addresses of people using the mailmap
+   mechanism.
+
  * "git push" now requires "-f" to update a tag, even if it is a
    fast-forward, as tags are meant to be fixed points.
 
@@ -69,6 +78,13 @@ UI, Workflows & Features
    been applied, but we probably would want to revisit this later, as
    it hurts the common case of not failing at all.
 
+ * Input and preconditions to "git reset" has been loosened where
+   appropriate.  "git reset $fromtree Makefile" requires $fromtree to
+   be any tree (it used to require it to be a commit), for example.
+   "git reset" (without options or parameters) used to error out when
+   you do not have any commits in your history, but it now gives you
+   an empty index (to match non-existent commit you are not even on).
+
  * "git submodule" started learning a new mode to integrate with the
    tip of the remote branch (as opposed to integrating with the commit
    recorded in the superproject's gitlink).
@@ -81,6 +97,11 @@ Foreign Interface
 
  * A new remote helper to interact with bzr has been added to contrib/.
 
+ * "git p4" got various bugfixes around its branch handling.
+
+ * The remote helper to interact with Hg in contrib/ has seen a few
+   fixes.
+
 
 Performance, Internal Implementation, etc.
 
@@ -90,6 +111,11 @@ Performance, Internal Implementation, etc.
  * Matching paths with common forms of pathspecs that contain wildcard
    characters has been optimized further.
 
+ * "git reset" internals has been reworked and should be faster in
+   general. We tried to be careful not to break any behaviour but
+   there could be corner cases, especially when running the command
+   from a conflicted state, that we may have missed.
+
  * The implementation of "imap-send" has been updated to reuse xml
    quoting code from http-push codepath.
 
@@ -136,8 +162,10 @@ details).
 
  * The attribute mechanism didn't allow limiting attributes to be
    applied to only a single directory itself with "path/" like the
-   exclude mechanism does.
-   (merge 94bc671 ja/directory-attrs later to maint).
+   exclude mechanism does.  The initial implementation of this that
+   was merged to 'maint' and 1.8.1.2 was with a severe performance
+   degradations and needs to merge a fix-up topic.
+   (merge 9db9eec nd/fix-directory-attrs-off-by-one later to maint).
 
  * "git apply" misbehaved when fixing whitespace breakages by removing
    excess trailing blank lines.
@@ -151,6 +179,14 @@ details).
    streaming a zip archive, which confused some implementations of unzip.
    (merge 5ea2c84 rs/zip-with-uncompressed-size-in-the-header later to maint).
 
+ * "git clean" showed what it was going to do, but sometimes end up
+   finding that it was not allowed to do so, which resulted in a
+   confusing output (e.g. after saying that it will remove an
+   untracked directory, it found an embedded git repository there
+   which it is not allowed to remove).  It now performs the actions
+   and then reports the outcome more faithfully.
+   (merge f538a91 zk/clean-report-failure later to maint).
+
  * When "git clone --separate-git-dir=$over_there" is interrupted, it
    failed to remove the real location of the $GIT_DIR it created.
    This was most visible when interrupting a submodule update.
@@ -176,6 +212,14 @@ details).
    index, the cache-tree data structure got corrupted.
    (merge eec3e7e nd/invalidate-i-t-a-cache-tree later to maint).
 
+ * "git clone" used to allow --bare and --separate-git-dir=$there
+   options at the same time, which was nonsensical.
+   (merge 95b63f1 nd/clone-no-separate-git-dir-with-bare later to maint).
+
+ * "git rebase --preserve-merges" lost empty merges in recent versions
+   of Git.
+   (merge 9869778 ph/rebase-preserve-all-merges later to maint).
+
  * "git merge --no-edit" computed who were involved in the work done
    on the side branch, even though that information is to be discarded
    without getting seen in the editor.
@@ -186,6 +230,16 @@ details).
    status of the hook.
    (merge 3e4141d ap/merge-stop-at-prepare-commit-msg-failure later to maint).
 
+ * When users spell "cc:" in lowercase in the fake "header" in the
+   trailer part, "git send-email" failed to pick up the addresses from
+   there. As e-mail headers field names are case insensitive, this
+   script should follow suit and treat "cc:" and "Cc:" the same way.
+   (merge 6310071 nz/send-email-headers-are-case-insensitive later to maint).
+
+ * Output from "git status --ignored" showed an unexpected interaction
+   with "--untracked".
+   (merge a45fb69 ap/status-ignored-in-ignored-directory later to maint).
+
  * "gitweb", when sorting by age to show repositories with new
    activities first, used to sort repositories with absolutely
    nothing in it early, which was not very useful.
@@ -201,6 +255,14 @@ details).
    to add a newline after such a line.
    (merge e0db176 sp/shortlog-missing-lf later to maint).
 
+ * Command line completion leaked an unnecessary error message while
+   looking for possible matches with paths in <tree-ish>.
+   (merge ca87dd6 ds/completion-silence-in-tree-path-probe later to maint).
+
+ * Command line completion for "tcsh" emitted an unwanted space
+   after completing a single directory name.
+   (merge 92f1c04 mk/complete-tcsh later to maint).
+
  * Some shells do not behave correctly when IFS is unset; work it
    around by explicitly setting it to the default value.
    (merge 393050c jc/maint-fbsd-sh-ifs-workaround later to maint).
index d5809e0e8c4dca414a863cfd019a6f8860c46c4d..b87f7446436ea73ffeb062317ea9869051d5be78 100644 (file)
@@ -923,6 +923,15 @@ column.tag::
        Specify whether to output tag listing in `git tag` in columns.
        See `column.ui` for details.
 
+commit.cleanup::
+       This setting overrides the default of the `--cleanup` option in
+       `git commit`. See linkgit:git-commit[1] for details. Changing the
+       default can be useful when you always want to keep lines that begin
+       with comment character `#` in your log message, in which case you
+       would do `git config commit.cleanup whitespace` (note that you will
+       have to remove the help lines that begin with `#` in the commit log
+       template yourself, if you do this).
+
 commit.status::
        A boolean to enable/disable inclusion of status information in the
        commit message template when using an editor to prepare the commit
@@ -1361,6 +1370,12 @@ help.autocorrect::
        value is 0 - the command will be just shown but not executed.
        This is the default.
 
+help.htmlpath::
+       Specify the path where the HTML documentation resides. File system paths
+       and URLs are supported. HTML pages will be prefixed with this path when
+       help is displayed in the 'web' format. This defaults to the documentation
+       path of your Git installation.
+
 http.proxy::
        Override the HTTP proxy, normally configured using the 'http_proxy',
        'https_proxy', and 'all_proxy' environment variables (see
@@ -1519,6 +1534,10 @@ log.showroot::
        Tools like linkgit:git-log[1] or linkgit:git-whatchanged[1], which
        normally hide the root commit will now show it. True by default.
 
+log.mailmap::
+       If true, makes linkgit:git-log[1], linkgit:git-show[1], and
+       linkgit:git-whatchanged[1] assume `--use-mailmap`.
+
 mailmap.file::
        The location of an augmenting mailmap file. The default
        mailmap, located in the root of the repository, is loaded
diff --git a/Documentation/git-check-ignore.txt b/Documentation/git-check-ignore.txt
new file mode 100644 (file)
index 0000000..854e4d0
--- /dev/null
@@ -0,0 +1,89 @@
+git-check-ignore(1)
+===================
+
+NAME
+----
+git-check-ignore - Debug gitignore / exclude files
+
+
+SYNOPSIS
+--------
+[verse]
+'git check-ignore' [options] pathname...
+'git check-ignore' [options] --stdin < <list-of-paths>
+
+DESCRIPTION
+-----------
+
+For each pathname given via the command-line or from a file via
+`--stdin`, show the pattern from .gitignore (or other input files to
+the exclude mechanism) that decides if the pathname is excluded or
+included.  Later patterns within a file take precedence over earlier
+ones.
+
+OPTIONS
+-------
+-q, --quiet::
+       Don't output anything, just set exit status.  This is only
+       valid with a single pathname.
+
+-v, --verbose::
+       Also output details about the matching pattern (if any)
+       for each given pathname.
+
+--stdin::
+       Read file names from stdin instead of from the command-line.
+
+-z::
+       The output format is modified to be machine-parseable (see
+       below).  If `--stdin` is also given, input paths are separated
+       with a NUL character instead of a linefeed character.
+
+OUTPUT
+------
+
+By default, any of the given pathnames which match an ignore pattern
+will be output, one per line.  If no pattern matches a given path,
+nothing will be output for that path; this means that path will not be
+ignored.
+
+If `--verbose` is specified, the output is a series of lines of the form:
+
+<source> <COLON> <linenum> <COLON> <pattern> <HT> <pathname>
+
+<pathname> is the path of a file being queried, <pattern> is the
+matching pattern, <source> is the pattern's source file, and <linenum>
+is the line number of the pattern within that source.  If the pattern
+contained a `!` prefix or `/` suffix, it will be preserved in the
+output.  <source> will be an absolute path when referring to the file
+configured by `core.excludesfile`, or relative to the repository root
+when referring to `.git/info/exclude` or a per-directory exclude file.
+
+If `-z` is specified, the pathnames in the output are delimited by the
+null character; if `--verbose` is also specified then null characters
+are also used instead of colons and hard tabs:
+
+<source> <NULL> <linenum> <NULL> <pattern> <NULL> <pathname> <NULL>
+
+
+EXIT STATUS
+-----------
+
+0::
+       One or more of the provided paths is ignored.
+
+1::
+       None of the provided paths are ignored.
+
+128::
+       A fatal error was encountered.
+
+SEE ALSO
+--------
+linkgit:gitignore[5]
+linkgit:gitconfig[5]
+linkgit:git-ls-files[5]
+
+GIT
+---
+Part of the linkgit:git[1] suite
index 6d5a04c83b6461396b44b3972a539eaae6368a19..a22116951527e4181d85ed23acb029f6920ee1ff 100644 (file)
@@ -72,13 +72,13 @@ if set:
        GIT_COMMITTER_NAME
        GIT_COMMITTER_EMAIL
        GIT_COMMITTER_DATE
-       EMAIL
 
 (nb "<", ">" and "\n"s are stripped)
 
 In case (some of) these environment variables are not set, the information
 is taken from the configuration items user.name and user.email, or, if not
-present, system user name and the hostname used for outgoing mail (taken
+present, the environment variable EMAIL, or, if that is not set,
+system user name and the hostname used for outgoing mail (taken
 from `/etc/mailname` and falling back to the fully qualified hostname when
 that file does not exist).
 
index 7bdb039d5ee9c6ed6c19c70173c733860de2d5b7..41b27da325053a9a817160364ac50e5140f5891c 100644 (file)
@@ -179,7 +179,9 @@ OPTIONS
        only if the message is to be edited. Otherwise only whitespace
        removed. The 'verbatim' mode does not change message at all,
        'whitespace' removes just leading/trailing whitespace lines
-       and 'strip' removes both whitespace and commentary.
+       and 'strip' removes both whitespace and commentary. The default
+       can be changed by the 'commit.cleanup' configuration variable
+       (see linkgit:git-config[1]).
 
 -e::
 --edit::
index 88d814af0e050eb3b31f5763cbf12f03a5f6cbc3..940c2ba66a6a92ed8680973e6981c34a7d599309 100644 (file)
@@ -359,6 +359,43 @@ Operations supported
 
 All the operations required for normal use are supported, including
 checkout, diff, status, update, log, add, remove, commit.
+
+Most CVS command arguments that read CVS tags or revision numbers
+(typically -r) work, and also support any git refspec
+(tag, branch, commit ID, etc).
+However, CVS revision numbers for non-default branches are not well
+emulated, and cvs log does not show tags or branches at
+all.  (Non-main-branch CVS revision numbers superficially resemble CVS
+revision numbers, but they actually encode a git commit ID directly,
+rather than represent the number of revisions since the branch point.)
+
+Note that there are two ways to checkout a particular branch.
+As described elsewhere on this page, the "module" parameter
+of cvs checkout is interpreted as a branch name, and it becomes
+the main branch.  It remains the main branch for a given sandbox
+even if you temporarily make another branch sticky with
+cvs update -r.  Alternatively, the -r argument can indicate
+some other branch to actually checkout, even though the module
+is still the "main" branch.  Tradeoffs (as currently
+implemented): Each new "module" creates a new database on disk with
+a history for the given module, and after the database is created,
+operations against that main branch are fast.  Or alternatively,
+-r doesn't take any extra disk space, but may be significantly slower for
+many operations, like cvs update.
+
+If you want to refer to a git refspec that has characters that are
+not allowed by CVS, you have two options.  First, it may just work
+to supply the git refspec directly to the appropriate CVS -r argument;
+some CVS clients don't seem to do much sanity checking of the argument.
+Second, if that fails, you can use a special character escape mechanism
+that only uses characters that are valid in CVS tags.  A sequence
+of 4 or 5 characters of the form (underscore (`"_"`), dash (`"-"`),
+one or two characters, and dash (`"-"`)) can encode various characters based
+on the one or two letters: `"s"` for slash (`"/"`), `"p"` for
+period (`"."`), `"u"` for underscore (`"_"`), or two hexadecimal digits
+for any byte value at all (typically an ASCII number, or perhaps a part
+of a UTF-8 encoded character).
+
 Legacy monitoring operations are not supported (edit, watch and related).
 Exports and tagging (tags and branches) are not supported at this stage.
 
index db55a4e0bbc524c20f08e6862daaa97dd1785b71..f2e08d11c19cf127c5f7d484b0c3d66fc475d547 100644 (file)
@@ -117,7 +117,7 @@ returns an empty string instead.
 
 As a special case for the date-type fields, you may specify a format for
 the date by adding one of `:default`, `:relative`, `:short`, `:local`,
-`:iso8601` or `:rfc2822` to the end of the fieldname; e.g.
+`:iso8601`, `:rfc2822` or `:raw` to the end of the fieldname; e.g.
 `%(taggerdate:relative)`.
 
 
index 08a185db7fb7192b4e94c83ae6781893e119c6cf..22c0d6e4b1074b71468ac1c9b16d7eeb0434e225 100644 (file)
@@ -47,6 +47,11 @@ OPTIONS
        Print out the ref name given on the command line by which each
        commit was reached.
 
+--use-mailmap::
+       Use mailmap file to map author and committer names and email
+       to canonical real names and email addresses. See
+       linkgit:git-shortlog[1].
+
 --full-diff::
        Without this flag, "git log -p <path>..." shows commits that
        touch the specified paths, and diffs about the same specified
index beff6229c8cc4541ca3a8ddd0faf724fceb439e7..f70ef9ded2c7e476c60a90f15ef45e650c663bde 100644 (file)
@@ -112,6 +112,11 @@ will be fetched and consulted first during a 'git p4 sync'.  Since
 importing directly from p4 is considerably slower than pulling changes
 from a git remote, this can be useful in a multi-developer environment.
 
+If there are multiple branches, doing 'git p4 sync' will automatically
+use the "BRANCH DETECTION" algorithm to try to partition new changes
+into the right branch.  This can be overridden with the '--branch'
+option to specify just a single branch to update.
+
 
 Rebase
 ~~~~~~
@@ -173,9 +178,11 @@ subsequent 'sync' operations.
 
 --branch <branch>::
        Import changes into given branch.  If the branch starts with
-       'refs/', it will be used as is, otherwise the path 'refs/heads/'
-       will be prepended.  The default branch is 'master'.  If used
-       with an initial clone, no HEAD will be checked out.
+       'refs/', it will be used as is.  Otherwise if it does not start
+       with 'p4/', that prefix is added.  The branch is assumed to
+       name a remote tracking, but this can be modified using
+       '--import-local', or by giving a full ref name.  The default
+       branch is 'master'.
 +
 This example imports a new remote "p4/proj2" into an existing
 git repository:
@@ -287,6 +294,11 @@ These options can be used to modify 'git p4 submit' behavior.
        to bypass the prompt, causing conflicting commits to be automatically
        skipped, or to quit trying to apply commits, without prompting.
 
+--branch <branch>::
+       After submitting, sync this named branch instead of the default
+       p4/master.  See the "Sync options" section above for more
+       information.
+
 Rebase options
 ~~~~~~~~~~~~~~
 These options can be used to modify 'git p4 rebase' behavior.
@@ -394,8 +406,10 @@ the path elements in the p4 repository.  The example above relied on the
 presence of the p4 branch.  Without p4 branches, the same result will
 occur with:
 ----
+git init depot
+cd depot
 git config git-p4.branchList main:branch1
-git p4 clone --detect-branches //depot@all
+git p4 clone --detect-branches //depot@all .
 ----
 
 
index 978d8da50c150a695565958d89daade5434ce8e0..a404b47b7bf80ef85e5f34595044090b7025f32c 100644 (file)
@@ -8,20 +8,20 @@ git-reset - Reset current HEAD to the specified state
 SYNOPSIS
 --------
 [verse]
-'git reset' [-q] [<commit>] [--] <paths>...
-'git reset' (--patch | -p) [<commit>] [--] [<paths>...]
+'git reset' [-q] [<tree-ish>] [--] <paths>...
+'git reset' (--patch | -p) [<tree-sh>] [--] [<paths>...]
 'git reset' [--soft | --mixed | --hard | --merge | --keep] [-q] [<commit>]
 
 DESCRIPTION
 -----------
-In the first and second form, copy entries from <commit> to the index.
+In the first and second form, copy entries from <tree-ish> to the index.
 In the third form, set the current branch head (HEAD) to <commit>, optionally
-modifying index and working tree to match.  The <commit> defaults to HEAD
-in all forms.
+modifying index and working tree to match.  The <tree-ish>/<commit> defaults
+to HEAD in all forms.
 
-'git reset' [-q] [<commit>] [--] <paths>...::
+'git reset' [-q] [<tree-ish>] [--] <paths>...::
        This form resets the index entries for all <paths> to their
-       state at <commit>.  (It does not affect the working tree, nor
+       state at <tree-ish>.  (It does not affect the working tree, nor
        the current branch.)
 +
 This means that `git reset <paths>` is the opposite of `git add
@@ -34,9 +34,9 @@ Alternatively, using linkgit:git-checkout[1] and specifying a commit, you
 can copy the contents of a path out of a commit to the index and to the
 working tree in one go.
 
-'git reset' (--patch | -p) [<commit>] [--] [<paths>...]::
+'git reset' (--patch | -p) [<tree-ish>] [--] [<paths>...]::
        Interactively select hunks in the difference between the index
-       and <commit> (defaults to HEAD).  The chosen hunks are applied
+       and <tree-ish> (defaults to HEAD).  The chosen hunks are applied
        in reverse to the index.
 +
 This means that `git reset -p` is the opposite of `git add -p`, i.e.
index 69decb13b0ec672cd9ccc83a67a08f1725473324..34d438b0abd583ca8b2e2156a085e322e8f827eb 100644 (file)
@@ -346,6 +346,16 @@ Any other arguments are passed directly to 'git log'
        corresponding git commit hash (this can optionally be followed by a
        tree-ish to specify which branch should be searched).  When given a
        tree-ish, returns the corresponding SVN revision number.
++
+--before;;
+       Don't require an exact match if given an SVN revision, instead find
+       the commit corresponding to the state of the SVN repository (on the
+       current branch) at the specified revision.
++
+--after;;
+       Don't require an exact match if given an SVN revision; if there is
+       not an exact match return the closest match searching forward in the
+       history.
 
 'set-tree'::
        You should consider using 'dcommit' instead of this command.
index c03b7adc97a98b41f6f4c709fa5e0a1ee4e71cd5..555250dfa0272f890b6d427ff61b4ea42ba0b814 100644 (file)
@@ -43,9 +43,10 @@ unreleased) version of git, that is available from 'master'
 branch of the `git.git` repository.
 Documentation for older releases are available here:
 
-* link:v1.8.1/git.html[documentation for release 1.8.1]
+* link:v1.8.1.1/git.html[documentation for release 1.8.1.1]
 
 * release notes for
+  link:RelNotes/1.8.1.1.txt[1.8.1.1],
   link:RelNotes/1.8.1.txt[1.8.1].
 
 * link:v1.8.0.3/git.html[documentation for release 1.8.0.3]
index 91a6438031949d83600abc791676bbf0397a0ddc..0da205fd97a8b3bf35ec4f920d555e24a75ad768 100644 (file)
@@ -184,8 +184,10 @@ The second .gitignore prevents git from ignoring
 
 SEE ALSO
 --------
-linkgit:git-rm[1], linkgit:git-update-index[1],
-linkgit:gitrepository-layout[5]
+linkgit:git-rm[1],
+linkgit:git-update-index[1],
+linkgit:gitrepository-layout[5],
+linkgit:git-check-ignore[1]
 
 GIT
 ---
index 944fc39fac8b26ceaee781fe4a18b9ba2d78ac9d..9d3e3527e4f2b149170352e35a2fae32670a3478 100644 (file)
@@ -67,11 +67,13 @@ marked. If you to exclude files, make sure you have loaded index first.
 * Prepare `struct dir_struct dir` and clear it with `memset(&dir, 0,
   sizeof(dir))`.
 
-* Call `add_exclude()` to add single exclude pattern,
-  `add_excludes_from_file()` to add patterns from a file
-  (e.g. `.git/info/exclude`), and/or set `dir.exclude_per_dir`.  A
-  short-hand function `setup_standard_excludes()` can be used to set up
-  the standard set of exclude settings.
+* To add single exclude pattern, call `add_exclude_list()` and then
+  `add_exclude()`.
+
+* To add patterns from a file (e.g. `.git/info/exclude`), call
+  `add_excludes_from_file()` , and/or set `dir.exclude_per_dir`.  A
+  short-hand function `setup_standard_excludes()` can be used to set
+  up the standard set of exclude settings.
 
 * Set options described in the Data Structure section above.
 
@@ -79,4 +81,6 @@ marked. If you to exclude files, make sure you have loaded index first.
 
 * Use `dir.entries[]`.
 
+* Call `clear_directory()` when none of the contained elements are no longer in use.
+
 (JC)
index 1b30d7bde301305cde0d3af74c7e478e36bd673b..11e85a639652f8b47aaad8113fcc2776eae982e5 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -241,11 +241,16 @@ all::
 # apostrophes to be ASCII so that cut&pasting examples to the shell
 # will work.
 #
+# Define PERL_PATH to the path of your Perl binary (usually /usr/bin/perl).
+#
 # Define NO_PERL_MAKEMAKER if you cannot use Makefiles generated by perl's
 # MakeMaker (e.g. using ActiveState under Cygwin).
 #
 # Define NO_PERL if you do not want Perl scripts or libraries at all.
 #
+# Define PYTHON_PATH to the path of your Python binary (often /usr/bin/python
+# but /usr/bin/python2.7 on some platforms).
+#
 # Define NO_PYTHON if you do not want Python scripts or libraries at all.
 #
 # Define NO_TCLTK if you do not want Tcl/Tk GUI.
@@ -649,7 +654,7 @@ LIB_H += list-objects.h
 LIB_H += ll-merge.h
 LIB_H += log-tree.h
 LIB_H += mailmap.h
-LIB_H += merge-file.h
+LIB_H += merge-blobs.h
 LIB_H += merge-recursive.h
 LIB_H += mergesort.h
 LIB_H += notes-cache.h
@@ -661,6 +666,7 @@ LIB_H += pack-revindex.h
 LIB_H += pack.h
 LIB_H += parse-options.h
 LIB_H += patch-ids.h
+LIB_H += pathspec.h
 LIB_H += pkt-line.h
 LIB_H += progress.h
 LIB_H += prompt.h
@@ -784,6 +790,7 @@ LIB_OBJS += parse-options-cb.o
 LIB_OBJS += patch-delta.o
 LIB_OBJS += patch-ids.o
 LIB_OBJS += path.o
+LIB_OBJS += pathspec.o
 LIB_OBJS += pkt-line.o
 LIB_OBJS += preload-index.o
 LIB_OBJS += pretty.o
@@ -849,6 +856,7 @@ BUILTIN_OBJS += builtin/branch.o
 BUILTIN_OBJS += builtin/bundle.o
 BUILTIN_OBJS += builtin/cat-file.o
 BUILTIN_OBJS += builtin/check-attr.o
+BUILTIN_OBJS += builtin/check-ignore.o
 BUILTIN_OBJS += builtin/check-ref-format.o
 BUILTIN_OBJS += builtin/checkout-index.o
 BUILTIN_OBJS += builtin/checkout.o
diff --git a/attr.c b/attr.c
index d6d71901b24855a1d33ef2edeb8fbc6ce8e969dc..4657cc233c4aa7cff90e745a58ac7ad0a5fa8cd2 100644 (file)
--- a/attr.c
+++ b/attr.c
@@ -564,25 +564,12 @@ static void bootstrap_attr_stack(void)
        attr_stack = elem;
 }
 
-static const char *find_basename(const char *path)
-{
-       const char *cp, *last_slash = NULL;
-
-       for (cp = path; *cp; cp++) {
-               if (*cp == '/' && cp[1])
-                       last_slash = cp;
-       }
-       return last_slash ? last_slash + 1 : path;
-}
-
-static void prepare_attr_stack(const char *path)
+static void prepare_attr_stack(const char *path, int dirlen)
 {
        struct attr_stack *elem, *info;
-       int dirlen, len;
+       int len;
        const char *cp;
 
-       dirlen = find_basename(path) - path;
-
        /*
         * At the bottom of the attribute stack is the built-in
         * set of attribute definitions, followed by the contents
@@ -704,7 +691,7 @@ static int fill_one(const char *what, struct match_attr *a, int rem)
 
                if (*n == ATTR__UNKNOWN) {
                        debug_set(what,
-                                 a->is_macro ? a->u.attr->name : a->u.pattern,
+                                 a->is_macro ? a->u.attr->name : a->u.pat.pattern,
                                  attr, v);
                        *n = v;
                        rem--;
@@ -762,15 +749,26 @@ static int macroexpand_one(int attr_nr, int rem)
 static void collect_all_attrs(const char *path)
 {
        struct attr_stack *stk;
-       int i, pathlen, rem;
-       const char *basename;
+       int i, pathlen, rem, dirlen;
+       const char *basename, *cp, *last_slash = NULL;
+
+       for (cp = path; *cp; cp++) {
+               if (*cp == '/' && cp[1])
+                       last_slash = cp;
+       }
+       pathlen = cp - path;
+       if (last_slash) {
+               basename = last_slash + 1;
+               dirlen = last_slash - path;
+       } else {
+               basename = path;
+               dirlen = 0;
+       }
 
-       prepare_attr_stack(path);
+       prepare_attr_stack(path, dirlen);
        for (i = 0; i < attr_nr; i++)
                check_all_attr[i].value = ATTR__UNKNOWN;
 
-       basename = find_basename(path);
-       pathlen = strlen(path);
        rem = attr_nr;
        for (stk = attr_stack; 0 < rem && stk; stk = stk->prev)
                rem = fill(path, pathlen, basename, stk, rem);
index 7e7bbd665a905b0fdf9ce5351f6f09dfd3dd7e8a..faef55913634a21a757b4814f855ed834c32915e 100644 (file)
--- a/builtin.h
+++ b/builtin.h
@@ -52,6 +52,7 @@ extern int cmd_cat_file(int argc, const char **argv, const char *prefix);
 extern int cmd_checkout(int argc, const char **argv, const char *prefix);
 extern int cmd_checkout_index(int argc, const char **argv, const char *prefix);
 extern int cmd_check_attr(int argc, const char **argv, const char *prefix);
+extern int cmd_check_ignore(int argc, const char **argv, const char *prefix);
 extern int cmd_check_ref_format(int argc, const char **argv, const char *prefix);
 extern int cmd_cherry(int argc, const char **argv, const char *prefix);
 extern int cmd_cherry_pick(int argc, const char **argv, const char *prefix);
index 075312afcd8813c3a7dbf08f6be519a37498fe4f..7cb6cca56dc58d02d075bf6042115f00265a9b68 100644 (file)
@@ -6,6 +6,7 @@
 #include "cache.h"
 #include "builtin.h"
 #include "dir.h"
+#include "pathspec.h"
 #include "exec_cmd.h"
 #include "cache-tree.h"
 #include "run-command.h"
@@ -97,39 +98,6 @@ int add_files_to_cache(const char *prefix, const char **pathspec, int flags)
        return !!data.add_errors;
 }
 
-static void fill_pathspec_matches(const char **pathspec, char *seen, int specs)
-{
-       int num_unmatched = 0, i;
-
-       /*
-        * Since we are walking the index as if we were walking the directory,
-        * we have to mark the matched pathspec as seen; otherwise we will
-        * mistakenly think that the user gave a pathspec that did not match
-        * anything.
-        */
-       for (i = 0; i < specs; i++)
-               if (!seen[i])
-                       num_unmatched++;
-       if (!num_unmatched)
-               return;
-       for (i = 0; i < active_nr; i++) {
-               struct cache_entry *ce = active_cache[i];
-               match_pathspec(pathspec, ce->name, ce_namelen(ce), 0, seen);
-       }
-}
-
-static char *find_used_pathspec(const char **pathspec)
-{
-       char *seen;
-       int i;
-
-       for (i = 0; pathspec[i];  i++)
-               ; /* just counting */
-       seen = xcalloc(i, 1);
-       fill_pathspec_matches(pathspec, seen, i);
-       return seen;
-}
-
 static char *prune_directory(struct dir_struct *dir, const char **pathspec, int prefix)
 {
        char *seen;
@@ -149,10 +117,14 @@ static char *prune_directory(struct dir_struct *dir, const char **pathspec, int
                        *dst++ = entry;
        }
        dir->nr = dst - dir->entries;
-       fill_pathspec_matches(pathspec, seen, specs);
+       add_pathspec_matches_against_index(pathspec, seen, specs);
        return seen;
 }
 
+/*
+ * Checks the index to see whether any path in pathspec refers to
+ * something inside a submodule.  If so, dies with an error message.
+ */
 static void treat_gitlinks(const char **pathspec)
 {
        int i;
@@ -160,24 +132,8 @@ static void treat_gitlinks(const char **pathspec)
        if (!pathspec || !*pathspec)
                return;
 
-       for (i = 0; i < active_nr; i++) {
-               struct cache_entry *ce = active_cache[i];
-               if (S_ISGITLINK(ce->ce_mode)) {
-                       int len = ce_namelen(ce), j;
-                       for (j = 0; pathspec[j]; j++) {
-                               int len2 = strlen(pathspec[j]);
-                               if (len2 <= len || pathspec[j][len] != '/' ||
-                                   memcmp(ce->name, pathspec[j], len))
-                                       continue;
-                               if (len2 == len + 1)
-                                       /* strip trailing slash */
-                                       pathspec[j] = xstrndup(ce->name, len);
-                               else
-                                       die (_("Path '%s' is in submodule '%.*s'"),
-                                               pathspec[j], len, ce->name);
-                       }
-               }
-       }
+       for (i = 0; pathspec[i]; i++)
+               pathspec[i] = check_path_for_gitlink(pathspec[i]);
 }
 
 static void refresh(int verbose, const char **pathspec)
@@ -197,17 +153,19 @@ static void refresh(int verbose, const char **pathspec)
         free(seen);
 }
 
-static const char **validate_pathspec(int argc, const char **argv, const char *prefix)
+/*
+ * Normalizes argv relative to prefix, via get_pathspec(), and then
+ * runs die_if_path_beyond_symlink() on each path in the normalized
+ * list.
+ */
+static const char **validate_pathspec(const char **argv, const char *prefix)
 {
        const char **pathspec = get_pathspec(prefix, argv);
 
        if (pathspec) {
                const char **p;
                for (p = pathspec; *p; p++) {
-                       if (has_symlink_leading_path(*p, strlen(*p))) {
-                               int len = prefix ? strlen(prefix) : 0;
-                               die(_("'%s' is beyond a symbolic link"), *p + len);
-                       }
+                       die_if_path_beyond_symlink(*p, prefix);
                }
        }
 
@@ -248,7 +206,7 @@ int interactive_add(int argc, const char **argv, const char *prefix, int patch)
        const char **pathspec = NULL;
 
        if (argc) {
-               pathspec = validate_pathspec(argc, argv, prefix);
+               pathspec = validate_pathspec(argv, prefix);
                if (!pathspec)
                        return -1;
        }
@@ -415,7 +373,7 @@ int cmd_add(int argc, const char **argv, const char *prefix)
                fprintf(stderr, _("Maybe you wanted to say 'git add .'?\n"));
                return 0;
        }
-       pathspec = validate_pathspec(argc, argv, prefix);
+       pathspec = validate_pathspec(argv, prefix);
 
        if (read_cache() < 0)
                die(_("index file corrupt"));
@@ -448,7 +406,7 @@ int cmd_add(int argc, const char **argv, const char *prefix)
 
                path_exclude_check_init(&check, &dir);
                if (!seen)
-                       seen = find_used_pathspec(pathspec);
+                       seen = find_pathspecs_matching_against_index(pathspec);
                for (i = 0; pathspec[i]; i++) {
                        if (!seen[i] && pathspec[i][0]
                            && !file_exists(pathspec[i])) {
index cfae5699051312943d90212fc9cbfda2aafb2288..b431ba320997918324a76c29b81dd01613cfa9db 100644 (file)
@@ -42,6 +42,7 @@ static int blank_boundary;
 static int incremental;
 static int xdl_opts;
 static int abbrev = -1;
+static int no_whole_file_rename;
 
 static enum date_mode blame_date_mode = DATE_ISO8601;
 static size_t blame_date_width;
@@ -1226,7 +1227,7 @@ static void pass_blame(struct scoreboard *sb, struct origin *origin, int opt)
         * The first pass looks for unrenamed path to optimize for
         * common cases, then we look for renames in the second pass.
         */
-       for (pass = 0; pass < 2; pass++) {
+       for (pass = 0; pass < 2 - no_whole_file_rename; pass++) {
                struct origin *(*find)(struct scoreboard *,
                                       struct commit *, struct origin *);
                find = pass ? find_rename : find_origin;
@@ -1321,30 +1322,31 @@ static void pass_blame(struct scoreboard *sb, struct origin *origin, int opt)
  * Information on commits, used for output.
  */
 struct commit_info {
-       const char *author;
-       const char *author_mail;
+       struct strbuf author;
+       struct strbuf author_mail;
        unsigned long author_time;
-       const char *author_tz;
+       struct strbuf author_tz;
 
        /* filled only when asked for details */
-       const char *committer;
-       const char *committer_mail;
+       struct strbuf committer;
+       struct strbuf committer_mail;
        unsigned long committer_time;
-       const char *committer_tz;
+       struct strbuf committer_tz;
 
-       const char *summary;
+       struct strbuf summary;
 };
 
 /*
  * Parse author/committer line in the commit object buffer
  */
 static void get_ac_line(const char *inbuf, const char *what,
-                       int person_len, char *person,
-                       int mail_len, char *mail,
-                       unsigned long *time, const char **tz)
+       struct strbuf *name, struct strbuf *mail,
+       unsigned long *time, struct strbuf *tz)
 {
-       int len, tzlen, maillen;
-       char *tmp, *endp, *timepos, *mailpos;
+       struct ident_split ident;
+       size_t len, maillen, namelen;
+       char *tmp, *endp;
+       const char *namebuf, *mailbuf;
 
        tmp = strstr(inbuf, what);
        if (!tmp)
@@ -1355,69 +1357,61 @@ static void get_ac_line(const char *inbuf, const char *what,
                len = strlen(tmp);
        else
                len = endp - tmp;
-       if (person_len <= len) {
+
+       if (split_ident_line(&ident, tmp, len)) {
        error_out:
                /* Ugh */
-               *tz = "(unknown)";
-               strcpy(person, *tz);
-               strcpy(mail, *tz);
+               tmp = "(unknown)";
+               strbuf_addstr(name, tmp);
+               strbuf_addstr(mail, tmp);
+               strbuf_addstr(tz, tmp);
                *time = 0;
                return;
        }
-       memcpy(person, tmp, len);
 
-       tmp = person;
-       tmp += len;
-       *tmp = 0;
-       while (person < tmp && *tmp != ' ')
-               tmp--;
-       if (tmp <= person)
-               goto error_out;
-       *tz = tmp+1;
-       tzlen = (person+len)-(tmp+1);
+       namelen = ident.name_end - ident.name_begin;
+       namebuf = ident.name_begin;
 
-       *tmp = 0;
-       while (person < tmp && *tmp != ' ')
-               tmp--;
-       if (tmp <= person)
-               goto error_out;
-       *time = strtoul(tmp, NULL, 10);
-       timepos = tmp;
+       maillen = ident.mail_end - ident.mail_begin;
+       mailbuf = ident.mail_begin;
 
-       *tmp = 0;
-       while (person < tmp && !(*tmp == ' ' && tmp[1] == '<'))
-               tmp--;
-       if (tmp <= person)
-               return;
-       mailpos = tmp + 1;
-       *tmp = 0;
-       maillen = timepos - tmp;
-       memcpy(mail, mailpos, maillen);
+       *time = strtoul(ident.date_begin, NULL, 10);
 
-       if (!mailmap.nr)
-               return;
-
-       /*
-        * mailmap expansion may make the name longer.
-        * make room by pushing stuff down.
-        */
-       tmp = person + person_len - (tzlen + 1);
-       memmove(tmp, *tz, tzlen);
-       tmp[tzlen] = 0;
-       *tz = tmp;
+       len = ident.tz_end - ident.tz_begin;
+       strbuf_add(tz, ident.tz_begin, len);
 
        /*
         * Now, convert both name and e-mail using mailmap
         */
-       if (map_user(&mailmap, mail+1, mail_len-1, person, tmp-person-1)) {
-               /* Add a trailing '>' to email, since map_user returns plain emails
-                  Note: It already has '<', since we replace from mail+1 */
-               mailpos = memchr(mail, '\0', mail_len);
-               if (mailpos && mailpos-mail < mail_len - 1) {
-                       *mailpos = '>';
-                       *(mailpos+1) = '\0';
-               }
-       }
+       map_user(&mailmap, &mailbuf, &maillen,
+                &namebuf, &namelen);
+
+       strbuf_addf(mail, "<%.*s>", (int)maillen, mailbuf);
+       strbuf_add(name, namebuf, namelen);
+}
+
+static void commit_info_init(struct commit_info *ci)
+{
+
+       strbuf_init(&ci->author, 0);
+       strbuf_init(&ci->author_mail, 0);
+       strbuf_init(&ci->author_tz, 0);
+       strbuf_init(&ci->committer, 0);
+       strbuf_init(&ci->committer_mail, 0);
+       strbuf_init(&ci->committer_tz, 0);
+       strbuf_init(&ci->summary, 0);
+}
+
+static void commit_info_destroy(struct commit_info *ci)
+{
+
+       strbuf_release(&ci->author);
+       strbuf_release(&ci->author_mail);
+       strbuf_release(&ci->author_tz);
+       strbuf_release(&ci->committer);
+       strbuf_release(&ci->committer_mail);
+       strbuf_release(&ci->committer_tz);
+       strbuf_release(&ci->summary);
 }
 
 static void get_commit_info(struct commit *commit,
@@ -1427,11 +1421,8 @@ static void get_commit_info(struct commit *commit,
        int len;
        const char *subject, *encoding;
        char *reencoded, *message;
-       static char author_name[1024];
-       static char author_mail[1024];
-       static char committer_name[1024];
-       static char committer_mail[1024];
-       static char summary_buf[1024];
+
+       commit_info_init(ret);
 
        /*
         * We've operated without save_commit_buffer, so
@@ -1449,11 +1440,8 @@ static void get_commit_info(struct commit *commit,
        encoding = get_log_output_encoding();
        reencoded = logmsg_reencode(commit, encoding);
        message   = reencoded ? reencoded : commit->buffer;
-       ret->author = author_name;
-       ret->author_mail = author_mail;
        get_ac_line(message, "\nauthor ",
-                   sizeof(author_name), author_name,
-                   sizeof(author_mail), author_mail,
+                   &ret->author, &ret->author_mail,
                    &ret->author_time, &ret->author_tz);
 
        if (!detailed) {
@@ -1461,21 +1449,16 @@ static void get_commit_info(struct commit *commit,
                return;
        }
 
-       ret->committer = committer_name;
-       ret->committer_mail = committer_mail;
        get_ac_line(message, "\ncommitter ",
-                   sizeof(committer_name), committer_name,
-                   sizeof(committer_mail), committer_mail,
+                   &ret->committer, &ret->committer_mail,
                    &ret->committer_time, &ret->committer_tz);
 
-       ret->summary = summary_buf;
        len = find_commit_subject(message, &subject);
-       if (len && len < sizeof(summary_buf)) {
-               memcpy(summary_buf, subject, len);
-               summary_buf[len] = 0;
-       } else {
-               sprintf(summary_buf, "(%s)", sha1_to_hex(commit->object.sha1));
-       }
+       if (len)
+               strbuf_add(&ret->summary, subject, len);
+       else
+               strbuf_addf(&ret->summary, "(%s)", sha1_to_hex(commit->object.sha1));
+
        free(reencoded);
 }
 
@@ -1504,15 +1487,15 @@ static int emit_one_suspect_detail(struct origin *suspect, int repeat)
 
        suspect->commit->object.flags |= METAINFO_SHOWN;
        get_commit_info(suspect->commit, &ci, 1);
-       printf("author %s\n", ci.author);
-       printf("author-mail %s\n", ci.author_mail);
+       printf("author %s\n", ci.author.buf);
+       printf("author-mail %s\n", ci.author_mail.buf);
        printf("author-time %lu\n", ci.author_time);
-       printf("author-tz %s\n", ci.author_tz);
-       printf("committer %s\n", ci.committer);
-       printf("committer-mail %s\n", ci.committer_mail);
+       printf("author-tz %s\n", ci.author_tz.buf);
+       printf("committer %s\n", ci.committer.buf);
+       printf("committer-mail %s\n", ci.committer_mail.buf);
        printf("committer-time %lu\n", ci.committer_time);
-       printf("committer-tz %s\n", ci.committer_tz);
-       printf("summary %s\n", ci.summary);
+       printf("committer-tz %s\n", ci.committer_tz.buf);
+       printf("summary %s\n", ci.summary.buf);
        if (suspect->commit->object.flags & UNINTERESTING)
                printf("boundary\n");
        if (suspect->previous) {
@@ -1520,6 +1503,9 @@ static int emit_one_suspect_detail(struct origin *suspect, int repeat)
                printf("previous %s ", sha1_to_hex(prev->commit->object.sha1));
                write_name_quoted(prev->path, stdout, '\n');
        }
+
+       commit_info_destroy(&ci);
+
        return 1;
 }
 
@@ -1706,11 +1692,11 @@ static void emit_other(struct scoreboard *sb, struct blame_entry *ent, int opt)
                if (opt & OUTPUT_ANNOTATE_COMPAT) {
                        const char *name;
                        if (opt & OUTPUT_SHOW_EMAIL)
-                               name = ci.author_mail;
+                               name = ci.author_mail.buf;
                        else
-                               name = ci.author;
+                               name = ci.author.buf;
                        printf("\t(%10s\t%10s\t%d)", name,
-                              format_time(ci.author_time, ci.author_tz,
+                              format_time(ci.author_time, ci.author_tz.buf,
                                           show_raw_time),
                               ent->lno + 1 + cnt);
                } else {
@@ -1729,14 +1715,14 @@ static void emit_other(struct scoreboard *sb, struct blame_entry *ent, int opt)
                                const char *name;
                                int pad;
                                if (opt & OUTPUT_SHOW_EMAIL)
-                                       name = ci.author_mail;
+                                       name = ci.author_mail.buf;
                                else
-                                       name = ci.author;
+                                       name = ci.author.buf;
                                pad = longest_author - utf8_strwidth(name);
                                printf(" (%s%*s %10s",
                                       name, pad, "",
                                       format_time(ci.author_time,
-                                                  ci.author_tz,
+                                                  ci.author_tz.buf,
                                                   show_raw_time));
                        }
                        printf(" %*d) ",
@@ -1751,6 +1737,8 @@ static void emit_other(struct scoreboard *sb, struct blame_entry *ent, int opt)
 
        if (sb->final_buf_size && cp[-1] != '\n')
                putchar('\n');
+
+       commit_info_destroy(&ci);
 }
 
 static void output(struct scoreboard *sb, int option)
@@ -1875,9 +1863,9 @@ static void find_alignment(struct scoreboard *sb, int *option)
                        suspect->commit->object.flags |= METAINFO_SHOWN;
                        get_commit_info(suspect->commit, &ci, 1);
                        if (*option & OUTPUT_SHOW_EMAIL)
-                               num = utf8_strwidth(ci.author_mail);
+                               num = utf8_strwidth(ci.author_mail.buf);
                        else
-                               num = utf8_strwidth(ci.author);
+                               num = utf8_strwidth(ci.author.buf);
                        if (longest_author < num)
                                longest_author = num;
                }
@@ -1889,6 +1877,8 @@ static void find_alignment(struct scoreboard *sb, int *option)
                        longest_dst_lines = num;
                if (largest_score < ent_score(sb, e))
                        largest_score = ent_score(sb, e);
+
+               commit_info_destroy(&ci);
        }
        max_orig_digits = decimal_width(longest_src_lines);
        max_digits = decimal_width(longest_dst_lines);
@@ -2403,6 +2393,7 @@ int cmd_blame(int argc, const char **argv, const char *prefix)
        init_revisions(&revs, NULL);
        revs.date_mode = blame_date_mode;
        DIFF_OPT_SET(&revs.diffopt, ALLOW_TEXTCONV);
+       DIFF_OPT_SET(&revs.diffopt, FOLLOW_RENAMES);
 
        save_commit_buffer = 0;
        dashdash_pos = 0;
@@ -2426,6 +2417,8 @@ int cmd_blame(int argc, const char **argv, const char *prefix)
                parse_revision_opt(&revs, &ctx, options, blame_opt_usage);
        }
 parse_done:
+       no_whole_file_rename = !DIFF_OPT_TST(&revs.diffopt, FOLLOW_RENAMES);
+       DIFF_OPT_CLR(&revs.diffopt, FOLLOW_RENAMES);
        argc = parse_options_end(&ctx);
 
        if (0 < abbrev)
diff --git a/builtin/check-ignore.c b/builtin/check-ignore.c
new file mode 100644 (file)
index 0000000..709535c
--- /dev/null
@@ -0,0 +1,173 @@
+#include "builtin.h"
+#include "cache.h"
+#include "dir.h"
+#include "quote.h"
+#include "pathspec.h"
+#include "parse-options.h"
+
+static int quiet, verbose, stdin_paths;
+static const char * const check_ignore_usage[] = {
+"git check-ignore [options] pathname...",
+"git check-ignore [options] --stdin < <list-of-paths>",
+NULL
+};
+
+static int null_term_line;
+
+static const struct option check_ignore_options[] = {
+       OPT__QUIET(&quiet, N_("suppress progress reporting")),
+       OPT__VERBOSE(&verbose, N_("be verbose")),
+       OPT_GROUP(""),
+       OPT_BOOLEAN(0, "stdin", &stdin_paths,
+                   N_("read file names from stdin")),
+       OPT_BOOLEAN('z', NULL, &null_term_line,
+                   N_("input paths are terminated by a null character")),
+       OPT_END()
+};
+
+static void output_exclude(const char *path, struct exclude *exclude)
+{
+       char *bang  = exclude->flags & EXC_FLAG_NEGATIVE  ? "!" : "";
+       char *slash = exclude->flags & EXC_FLAG_MUSTBEDIR ? "/" : "";
+       if (!null_term_line) {
+               if (!verbose) {
+                       write_name_quoted(path, stdout, '\n');
+               } else {
+                       quote_c_style(exclude->el->src, NULL, stdout, 0);
+                       printf(":%d:%s%s%s\t",
+                              exclude->srcpos,
+                              bang, exclude->pattern, slash);
+                       quote_c_style(path, NULL, stdout, 0);
+                       fputc('\n', stdout);
+               }
+       } else {
+               if (!verbose) {
+                       printf("%s%c", path, '\0');
+               } else {
+                       printf("%s%c%d%c%s%s%s%c%s%c",
+                              exclude->el->src, '\0',
+                              exclude->srcpos, '\0',
+                              bang, exclude->pattern, slash, '\0',
+                              path, '\0');
+               }
+       }
+}
+
+static int check_ignore(const char *prefix, const char **pathspec)
+{
+       struct dir_struct dir;
+       const char *path, *full_path;
+       char *seen;
+       int num_ignored = 0, dtype = DT_UNKNOWN, i;
+       struct path_exclude_check check;
+       struct exclude *exclude;
+
+       /* read_cache() is only necessary so we can watch out for submodules. */
+       if (read_cache() < 0)
+               die(_("index file corrupt"));
+
+       memset(&dir, 0, sizeof(dir));
+       dir.flags |= DIR_COLLECT_IGNORED;
+       setup_standard_excludes(&dir);
+
+       if (!pathspec || !*pathspec) {
+               if (!quiet)
+                       fprintf(stderr, "no pathspec given.\n");
+               return 0;
+       }
+
+       path_exclude_check_init(&check, &dir);
+       /*
+        * look for pathspecs matching entries in the index, since these
+        * should not be ignored, in order to be consistent with
+        * 'git status', 'git add' etc.
+        */
+       seen = find_pathspecs_matching_against_index(pathspec);
+       for (i = 0; pathspec[i]; i++) {
+               path = pathspec[i];
+               full_path = prefix_path(prefix, prefix
+                                       ? strlen(prefix) : 0, path);
+               full_path = check_path_for_gitlink(full_path);
+               die_if_path_beyond_symlink(full_path, prefix);
+               if (!seen[i] && path[0]) {
+                       exclude = last_exclude_matching_path(&check, full_path,
+                                                            -1, &dtype);
+                       if (exclude) {
+                               if (!quiet)
+                                       output_exclude(path, exclude);
+                               num_ignored++;
+                       }
+               }
+       }
+       free(seen);
+       clear_directory(&dir);
+       path_exclude_check_clear(&check);
+
+       return num_ignored;
+}
+
+static int check_ignore_stdin_paths(const char *prefix)
+{
+       struct strbuf buf, nbuf;
+       char **pathspec = NULL;
+       size_t nr = 0, alloc = 0;
+       int line_termination = null_term_line ? 0 : '\n';
+       int num_ignored;
+
+       strbuf_init(&buf, 0);
+       strbuf_init(&nbuf, 0);
+       while (strbuf_getline(&buf, stdin, line_termination) != EOF) {
+               if (line_termination && buf.buf[0] == '"') {
+                       strbuf_reset(&nbuf);
+                       if (unquote_c_style(&nbuf, buf.buf, NULL))
+                               die("line is badly quoted");
+                       strbuf_swap(&buf, &nbuf);
+               }
+               ALLOC_GROW(pathspec, nr + 1, alloc);
+               pathspec[nr] = xcalloc(strlen(buf.buf) + 1, sizeof(*buf.buf));
+               strcpy(pathspec[nr++], buf.buf);
+       }
+       ALLOC_GROW(pathspec, nr + 1, alloc);
+       pathspec[nr] = NULL;
+       num_ignored = check_ignore(prefix, (const char **)pathspec);
+       maybe_flush_or_die(stdout, "attribute to stdout");
+       strbuf_release(&buf);
+       strbuf_release(&nbuf);
+       free(pathspec);
+       return num_ignored;
+}
+
+int cmd_check_ignore(int argc, const char **argv, const char *prefix)
+{
+       int num_ignored;
+
+       git_config(git_default_config, NULL);
+
+       argc = parse_options(argc, argv, prefix, check_ignore_options,
+                            check_ignore_usage, 0);
+
+       if (stdin_paths) {
+               if (argc > 0)
+                       die(_("cannot specify pathnames with --stdin"));
+       } else {
+               if (null_term_line)
+                       die(_("-z only makes sense with --stdin"));
+               if (argc == 0)
+                       die(_("no path specified"));
+       }
+       if (quiet) {
+               if (argc > 1)
+                       die(_("--quiet is only valid with a single pathname"));
+               if (verbose)
+                       die(_("cannot have both --quiet and --verbose"));
+       }
+
+       if (stdin_paths) {
+               num_ignored = check_ignore_stdin_paths(prefix);
+       } else {
+               num_ignored = check_ignore(prefix, argv);
+               maybe_flush_or_die(stdout, "ignore to stdout");
+       }
+
+       return !num_ignored;
+}
index 69c1cda9061f248479377d2841a1ddf703ea59ba..04e396b17acc2443a663dd085ef812c3c2746d27 100644 (file)
@@ -10,6 +10,7 @@
 #include "cache.h"
 #include "dir.h"
 #include "parse-options.h"
+#include "refs.h"
 #include "string-list.h"
 #include "quote.h"
 
@@ -20,6 +21,12 @@ static const char *const builtin_clean_usage[] = {
        NULL
 };
 
+static const char *msg_remove = N_("Removing %s\n");
+static const char *msg_would_remove = N_("Would remove %s\n");
+static const char *msg_skip_git_dir = N_("Skipping repository %s\n");
+static const char *msg_would_skip_git_dir = N_("Would skip repository %s\n");
+static const char *msg_warn_remove_failed = N_("failed to remove %s");
+
 static int git_clean_config(const char *var, const char *value, void *cb)
 {
        if (!strcmp(var, "clean.requireforce"))
@@ -34,22 +41,124 @@ static int exclude_cb(const struct option *opt, const char *arg, int unset)
        return 0;
 }
 
+static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag,
+               int dry_run, int quiet, int *dir_gone)
+{
+       DIR *dir;
+       struct strbuf quoted = STRBUF_INIT;
+       struct dirent *e;
+       int res = 0, ret = 0, gone = 1, original_len = path->len, len, i;
+       unsigned char submodule_head[20];
+       struct string_list dels = STRING_LIST_INIT_DUP;
+
+       *dir_gone = 1;
+
+       if ((force_flag & REMOVE_DIR_KEEP_NESTED_GIT) &&
+                       !resolve_gitlink_ref(path->buf, "HEAD", submodule_head)) {
+               if (!quiet) {
+                       quote_path_relative(path->buf, strlen(path->buf), &quoted, prefix);
+                       printf(dry_run ?  _(msg_would_skip_git_dir) : _(msg_skip_git_dir),
+                                       quoted.buf);
+               }
+
+               *dir_gone = 0;
+               return 0;
+       }
+
+       dir = opendir(path->buf);
+       if (!dir) {
+               /* an empty dir could be removed even if it is unreadble */
+               res = dry_run ? 0 : rmdir(path->buf);
+               if (res) {
+                       quote_path_relative(path->buf, strlen(path->buf), &quoted, prefix);
+                       warning(_(msg_warn_remove_failed), quoted.buf);
+                       *dir_gone = 0;
+               }
+               return res;
+       }
+
+       if (path->buf[original_len - 1] != '/')
+               strbuf_addch(path, '/');
+
+       len = path->len;
+       while ((e = readdir(dir)) != NULL) {
+               struct stat st;
+               if (is_dot_or_dotdot(e->d_name))
+                       continue;
+
+               strbuf_setlen(path, len);
+               strbuf_addstr(path, e->d_name);
+               if (lstat(path->buf, &st))
+                       ; /* fall thru */
+               else if (S_ISDIR(st.st_mode)) {
+                       if (remove_dirs(path, prefix, force_flag, dry_run, quiet, &gone))
+                               ret = 1;
+                       if (gone) {
+                               quote_path_relative(path->buf, strlen(path->buf), &quoted, prefix);
+                               string_list_append(&dels, quoted.buf);
+                       } else
+                               *dir_gone = 0;
+                       continue;
+               } else {
+                       res = dry_run ? 0 : unlink(path->buf);
+                       if (!res) {
+                               quote_path_relative(path->buf, strlen(path->buf), &quoted, prefix);
+                               string_list_append(&dels, quoted.buf);
+                       } else {
+                               quote_path_relative(path->buf, strlen(path->buf), &quoted, prefix);
+                               warning(_(msg_warn_remove_failed), quoted.buf);
+                               *dir_gone = 0;
+                               ret = 1;
+                       }
+                       continue;
+               }
+
+               /* path too long, stat fails, or non-directory still exists */
+               *dir_gone = 0;
+               ret = 1;
+               break;
+       }
+       closedir(dir);
+
+       strbuf_setlen(path, original_len);
+
+       if (*dir_gone) {
+               res = dry_run ? 0 : rmdir(path->buf);
+               if (!res)
+                       *dir_gone = 1;
+               else {
+                       quote_path_relative(path->buf, strlen(path->buf), &quoted, prefix);
+                       warning(_(msg_warn_remove_failed), quoted.buf);
+                       *dir_gone = 0;
+                       ret = 1;
+               }
+       }
+
+       if (!*dir_gone && !quiet) {
+               for (i = 0; i < dels.nr; i++)
+                       printf(dry_run ?  _(msg_would_remove) : _(msg_remove), dels.items[i].string);
+       }
+       string_list_clear(&dels, 0);
+       return ret;
+}
+
 int cmd_clean(int argc, const char **argv, const char *prefix)
 {
-       int i;
-       int show_only = 0, remove_directories = 0, quiet = 0, ignored = 0;
-       int ignored_only = 0, config_set = 0, errors = 0;
+       int i, res;
+       int dry_run = 0, remove_directories = 0, quiet = 0, ignored = 0;
+       int ignored_only = 0, config_set = 0, errors = 0, gone = 1;
        int rm_flags = REMOVE_DIR_KEEP_NESTED_GIT;
        struct strbuf directory = STRBUF_INIT;
        struct dir_struct dir;
        static const char **pathspec;
        struct strbuf buf = STRBUF_INIT;
        struct string_list exclude_list = STRING_LIST_INIT_NODUP;
+       struct exclude_list *el;
        const char *qname;
        char *seen = NULL;
        struct option options[] = {
                OPT__QUIET(&quiet, N_("do not print names of files removed")),
-               OPT__DRY_RUN(&show_only, N_("dry run")),
+               OPT__DRY_RUN(&dry_run, N_("dry run")),
                OPT__FORCE(&force, N_("force")),
                OPT_BOOLEAN('d', NULL, &remove_directories,
                                N_("remove whole directories")),
@@ -77,7 +186,7 @@ int cmd_clean(int argc, const char **argv, const char *prefix)
        if (ignored && ignored_only)
                die(_("-x and -X cannot be used together"));
 
-       if (!show_only && !force) {
+       if (!dry_run && !force) {
                if (config_set)
                        die(_("clean.requireForce set to true and neither -n nor -f given; "
                                  "refusing to clean"));
@@ -97,9 +206,9 @@ int cmd_clean(int argc, const char **argv, const char *prefix)
        if (!ignored)
                setup_standard_excludes(&dir);
 
+       el = add_exclude_list(&dir, EXC_CMDL, "--exclude option");
        for (i = 0; i < exclude_list.nr; i++)
-               add_exclude(exclude_list.items[i].string, "", 0,
-                           &dir.exclude_list[EXC_CMDL]);
+               add_exclude(exclude_list.items[i].string, "", 0, el, -(i+1));
 
        pathspec = get_pathspec(prefix, argv);
 
@@ -149,38 +258,26 @@ int cmd_clean(int argc, const char **argv, const char *prefix)
 
                if (S_ISDIR(st.st_mode)) {
                        strbuf_addstr(&directory, ent->name);
-                       qname = quote_path_relative(directory.buf, directory.len, &buf, prefix);
-                       if (show_only && (remove_directories ||
-                           (matches == MATCHED_EXACTLY))) {
-                               printf(_("Would remove %s\n"), qname);
-                       } else if (remove_directories ||
-                                  (matches == MATCHED_EXACTLY)) {
-                               if (!quiet)
-                                       printf(_("Removing %s\n"), qname);
-                               if (remove_dir_recursively(&directory,
-                                                          rm_flags) != 0) {
-                                       warning(_("failed to remove %s"), qname);
+                       if (remove_directories || (matches == MATCHED_EXACTLY)) {
+                               if (remove_dirs(&directory, prefix, rm_flags, dry_run, quiet, &gone))
                                        errors++;
+                               if (gone && !quiet) {
+                                       qname = quote_path_relative(directory.buf, directory.len, &buf, prefix);
+                                       printf(dry_run ? _(msg_would_remove) : _(msg_remove), qname);
                                }
-                       } else if (show_only) {
-                               printf(_("Would not remove %s\n"), qname);
-                       } else {
-                               printf(_("Not removing %s\n"), qname);
                        }
                        strbuf_reset(&directory);
                } else {
                        if (pathspec && !matches)
                                continue;
-                       qname = quote_path_relative(ent->name, -1, &buf, prefix);
-                       if (show_only) {
-                               printf(_("Would remove %s\n"), qname);
-                               continue;
-                       } else if (!quiet) {
-                               printf(_("Removing %s\n"), qname);
-                       }
-                       if (unlink(ent->name) != 0) {
-                               warning(_("failed to remove %s"), qname);
+                       res = dry_run ? 0 : unlink(ent->name);
+                       if (res) {
+                               qname = quote_path_relative(ent->name, -1, &buf, prefix);
+                               warning(_(msg_warn_remove_failed), qname);
                                errors++;
+                       } else if (!quiet) {
+                               qname = quote_path_relative(ent->name, -1, &buf, prefix);
+                               printf(dry_run ? _(msg_would_remove) : _(msg_remove), qname);
                        }
                }
        }
index 8d23a62e8a1b875f1415f78a4c283f4cdefac011..36ec99db3f5a7dbd59705aa68bcd592c08c773cd 100644 (file)
@@ -704,6 +704,8 @@ int cmd_clone(int argc, const char **argv, const char *prefix)
                if (option_origin)
                        die(_("--bare and --origin %s options are incompatible."),
                            option_origin);
+               if (real_git_dir)
+                       die(_("--bare and --separate-git-dir are incompatible."));
                option_no_checkout = 1;
        }
 
index 65d08d21f4469c64ff3881e589d21a86b8373134..38b9a9cc0d0e1beba3258f907e95ae1ad76f85f5 100644 (file)
@@ -103,7 +103,7 @@ static enum {
        CLEANUP_NONE,
        CLEANUP_ALL
 } cleanup_mode;
-static char *cleanup_arg;
+static const char *cleanup_arg;
 
 static enum commit_whence whence;
 static int use_editor = 1, include_status = 1;
@@ -1320,6 +1320,8 @@ static int git_commit_config(const char *k, const char *v, void *cb)
                include_status = git_config_bool(k, v);
                return 0;
        }
+       if (!strcmp(k, "commit.cleanup"))
+               return git_config_string(&cleanup_arg, k, v);
 
        status = git_gpg_config(k, v, NULL);
        if (status)
index 5a4055eb0b0d4965785b6f69dbe92efe251c54dd..8f0b2e84fef5d1b9c07ea8846c9fbc1318d8d51b 100644 (file)
@@ -22,6 +22,7 @@
 #include "branch.h"
 #include "streaming.h"
 #include "version.h"
+#include "mailmap.h"
 
 /* Set a default date-time format for git log ("log.date" config variable) */
 static const char *default_date_mode = NULL;
@@ -30,6 +31,7 @@ static int default_abbrev_commit;
 static int default_show_root = 1;
 static int decoration_style;
 static int decoration_given;
+static int use_mailmap_config;
 static const char *fmt_patch_subject_prefix = "PATCH";
 static const char *fmt_pretty;
 
@@ -94,16 +96,18 @@ static void cmd_log_init_finish(int argc, const char **argv, const char *prefix,
                         struct rev_info *rev, struct setup_revision_opt *opt)
 {
        struct userformat_want w;
-       int quiet = 0, source = 0;
+       int quiet = 0, source = 0, mailmap = 0;
 
        const struct option builtin_log_options[] = {
                OPT_BOOLEAN(0, "quiet", &quiet, N_("suppress diff output")),
                OPT_BOOLEAN(0, "source", &source, N_("show source")),
+               OPT_BOOLEAN(0, "use-mailmap", &mailmap, N_("Use mail map file")),
                { OPTION_CALLBACK, 0, "decorate", NULL, NULL, N_("decorate options"),
                  PARSE_OPT_OPTARG, decorate_callback},
                OPT_END()
        };
 
+       mailmap = use_mailmap_config;
        argc = parse_options(argc, argv, prefix,
                             builtin_log_options, builtin_log_usage,
                             PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN |
@@ -136,6 +140,11 @@ static void cmd_log_init_finish(int argc, const char **argv, const char *prefix,
        if (source)
                rev->show_source = 1;
 
+       if (mailmap) {
+               rev->mailmap = xcalloc(1, sizeof(struct string_list));
+               read_mailmap(rev->mailmap, NULL);
+       }
+
        if (rev->pretty_given && rev->commit_format == CMIT_FMT_RAW) {
                /*
                 * "log --pretty=raw" is special; ignore UI oriented
@@ -351,6 +360,11 @@ static int git_log_config(const char *var, const char *value, void *cb)
        }
        if (!prefixcmp(var, "color.decorate."))
                return parse_decorate_color_config(var, 15, value);
+       if (!strcmp(var, "log.mailmap")) {
+               use_mailmap_config = git_config_bool(var, value);
+               return 0;
+       }
+
        if (grep_config(var, value, cb) < 0)
                return -1;
        return git_diff_ui_config(var, value, cb);
index 373c573449b5771f9b56d7d4ce04e4c0b3957cd9..175e6e3e72003387bfa4f78dbd0ed10c02c4e318 100644 (file)
@@ -35,6 +35,7 @@ static int error_unmatch;
 static char *ps_matched;
 static const char *with_tree;
 static int exc_given;
+static int exclude_args;
 
 static const char *tag_cached = "";
 static const char *tag_unmerged = "";
@@ -420,10 +421,10 @@ static int option_parse_z(const struct option *opt,
 static int option_parse_exclude(const struct option *opt,
                                const char *arg, int unset)
 {
-       struct exclude_list *list = opt->value;
+       struct string_list *exclude_list = opt->value;
 
        exc_given = 1;
-       add_exclude(arg, "", 0, list);
+       string_list_append(exclude_list, arg);
 
        return 0;
 }
@@ -452,9 +453,11 @@ static int option_parse_exclude_standard(const struct option *opt,
 
 int cmd_ls_files(int argc, const char **argv, const char *cmd_prefix)
 {
-       int require_work_tree = 0, show_tag = 0;
+       int require_work_tree = 0, show_tag = 0, i;
        const char *max_prefix;
        struct dir_struct dir;
+       struct exclude_list *el;
+       struct string_list exclude_list = STRING_LIST_INIT_NODUP;
        struct option builtin_ls_files_options[] = {
                { OPTION_CALLBACK, 'z', NULL, NULL, NULL,
                        N_("paths are separated with NUL character"),
@@ -488,7 +491,7 @@ int cmd_ls_files(int argc, const char **argv, const char *cmd_prefix)
                        N_("show unmerged files in the output")),
                OPT_BOOLEAN(0, "resolve-undo", &show_resolve_undo,
                            N_("show resolve-undo information")),
-               { OPTION_CALLBACK, 'x', "exclude", &dir.exclude_list[EXC_CMDL], N_("pattern"),
+               { OPTION_CALLBACK, 'x', "exclude", &exclude_list, N_("pattern"),
                        N_("skip files matching pattern"),
                        0, option_parse_exclude },
                { OPTION_CALLBACK, 'X', "exclude-from", &dir, N_("file"),
@@ -525,6 +528,10 @@ int cmd_ls_files(int argc, const char **argv, const char *cmd_prefix)
 
        argc = parse_options(argc, argv, prefix, builtin_ls_files_options,
                        ls_files_usage, 0);
+       el = add_exclude_list(&dir, EXC_CMDL, "--exclude option");
+       for (i = 0; i < exclude_list.nr; i++) {
+               add_exclude(exclude_list.items[i].string, "", 0, el, --exclude_args);
+       }
        if (show_tag || show_valid_bit) {
                tag_cached = "H ";
                tag_unmerged = "M ";
index 915cc9f86f855d517abaa55d31c502899fb77220..6032131a9087281e97d0abc05365b7485004121b 100644 (file)
@@ -23,8 +23,8 @@
 
 static const char * const git_reset_usage[] = {
        N_("git reset [--mixed | --soft | --hard | --merge | --keep] [-q] [<commit>]"),
-       N_("git reset [-q] <commit> [--] <paths>..."),
-       N_("git reset --patch [<commit>] [--] [<paths>...]"),
+       N_("git reset [-q] <tree-ish> [--] <paths>..."),
+       N_("git reset --patch [<tree-ish>] [--] [<paths>...]"),
        NULL
 };
 
@@ -38,14 +38,12 @@ static inline int is_merge(void)
        return !access(git_path("MERGE_HEAD"), F_OK);
 }
 
-static int reset_index_file(const unsigned char *sha1, int reset_type, int quiet)
+static int reset_index(const unsigned char *sha1, int reset_type, int quiet)
 {
        int nr = 1;
-       int newfd;
        struct tree_desc desc[2];
        struct tree *tree;
        struct unpack_trees_options opts;
-       struct lock_file *lock = xcalloc(1, sizeof(struct lock_file));
 
        memset(&opts, 0, sizeof(opts));
        opts.head_idx = 1;
@@ -67,8 +65,6 @@ static int reset_index_file(const unsigned char *sha1, int reset_type, int quiet
                opts.reset = 1;
        }
 
-       newfd = hold_locked_index(lock, 1);
-
        read_cache_unmerged();
 
        if (reset_type == KEEP) {
@@ -91,10 +87,6 @@ static int reset_index_file(const unsigned char *sha1, int reset_type, int quiet
                prime_cache_tree(&active_cache_tree, tree);
        }
 
-       if (write_cache(newfd, active_cache, active_nr) ||
-           commit_locked_index(lock))
-               return error(_("Could not write new index file."));
-
        return 0;
 }
 
@@ -117,36 +109,10 @@ static void print_new_head_line(struct commit *commit)
                printf("\n");
 }
 
-static int update_index_refresh(int fd, struct lock_file *index_lock, int flags)
-{
-       int result;
-
-       if (!index_lock) {
-               index_lock = xcalloc(1, sizeof(struct lock_file));
-               fd = hold_locked_index(index_lock, 1);
-       }
-
-       if (read_cache() < 0)
-               return error(_("Could not read index"));
-
-       result = refresh_index(&the_index, (flags), NULL, NULL,
-                              _("Unstaged changes after reset:")) ? 1 : 0;
-       if (write_cache(fd, active_cache, active_nr) ||
-                       commit_locked_index(index_lock))
-               return error ("Could not refresh index");
-       return result;
-}
-
 static void update_index_from_diff(struct diff_queue_struct *q,
                struct diff_options *opt, void *data)
 {
        int i;
-       int *discard_flag = data;
-
-       /* do_diff_cache() mangled the index */
-       discard_cache();
-       *discard_flag = 1;
-       read_cache();
 
        for (i = 0; i < q->nr; i++) {
                struct diff_filespec *one = q->queue[i]->one;
@@ -164,32 +130,15 @@ static void update_index_from_diff(struct diff_queue_struct *q,
        }
 }
 
-static int interactive_reset(const char *revision, const char **argv,
-                            const char *prefix)
+static int read_from_tree(const char **pathspec, unsigned char *tree_sha1)
 {
-       const char **pathspec = NULL;
-
-       if (*argv)
-               pathspec = get_pathspec(prefix, argv);
-
-       return run_add_interactive(revision, "--patch=reset", pathspec);
-}
-
-static int read_from_tree(const char *prefix, const char **argv,
-               unsigned char *tree_sha1, int refresh_flags)
-{
-       struct lock_file *lock = xcalloc(1, sizeof(struct lock_file));
-       int index_fd, index_was_discarded = 0;
        struct diff_options opt;
 
        memset(&opt, 0, sizeof(opt));
-       diff_tree_setup_paths(get_pathspec(prefix, (const char **)argv), &opt);
+       diff_tree_setup_paths(pathspec, &opt);
        opt.output_format = DIFF_FORMAT_CALLBACK;
        opt.format_callback = update_index_from_diff;
-       opt.format_callback_data = &index_was_discarded;
 
-       index_fd = hold_locked_index(lock, 1);
-       index_was_discarded = 0;
        read_cache();
        if (do_diff_cache(tree_sha1, &opt))
                return 1;
@@ -197,10 +146,7 @@ static int read_from_tree(const char *prefix, const char **argv,
        diff_flush(&opt);
        diff_tree_release_paths(&opt);
 
-       if (!index_was_discarded)
-               /* The index is still clobbered from do_diff_cache() */
-               discard_cache();
-       return update_index_refresh(index_fd, lock, refresh_flags);
+       return 0;
 }
 
 static void set_reflog_message(struct strbuf *sb, const char *action,
@@ -225,15 +171,79 @@ static void die_if_unmerged_cache(int reset_type)
 
 }
 
-int cmd_reset(int argc, const char **argv, const char *prefix)
+static const char **parse_args(const char **argv, const char *prefix, const char **rev_ret)
 {
-       int i = 0, reset_type = NONE, update_ref_status = 0, quiet = 0;
-       int patch_mode = 0;
        const char *rev = "HEAD";
-       unsigned char sha1[20], *orig = NULL, sha1_orig[20],
-                               *old_orig = NULL, sha1_old_orig[20];
-       struct commit *commit;
+       unsigned char unused[20];
+       /*
+        * Possible arguments are:
+        *
+        * git reset [-opts] [<rev>]
+        * git reset [-opts] <tree> [<paths>...]
+        * git reset [-opts] <tree> -- [<paths>...]
+        * git reset [-opts] -- [<paths>...]
+        * git reset [-opts] <paths>...
+        *
+        * At this point, argv points immediately after [-opts].
+        */
+
+       if (argv[0]) {
+               if (!strcmp(argv[0], "--")) {
+                       argv++; /* reset to HEAD, possibly with paths */
+               } else if (argv[1] && !strcmp(argv[1], "--")) {
+                       rev = argv[0];
+                       argv += 2;
+               }
+               /*
+                * Otherwise, argv[0] could be either <rev> or <paths> and
+                * has to be unambiguous. If there is a single argument, it
+                * can not be a tree
+                */
+               else if ((!argv[1] && !get_sha1_committish(argv[0], unused)) ||
+                        (argv[1] && !get_sha1_treeish(argv[0], unused))) {
+                       /*
+                        * Ok, argv[0] looks like a commit/tree; it should not
+                        * be a filename.
+                        */
+                       verify_non_filename(prefix, argv[0]);
+                       rev = *argv++;
+               } else {
+                       /* Otherwise we treat this as a filename */
+                       verify_filename(prefix, argv[0], 1);
+               }
+       }
+       *rev_ret = rev;
+       return argv[0] ? get_pathspec(prefix, argv) : NULL;
+}
+
+static int update_refs(const char *rev, const unsigned char *sha1)
+{
+       int update_ref_status;
        struct strbuf msg = STRBUF_INIT;
+       unsigned char *orig = NULL, sha1_orig[20],
+               *old_orig = NULL, sha1_old_orig[20];
+
+       if (!get_sha1("ORIG_HEAD", sha1_old_orig))
+               old_orig = sha1_old_orig;
+       if (!get_sha1("HEAD", sha1_orig)) {
+               orig = sha1_orig;
+               set_reflog_message(&msg, "updating ORIG_HEAD", NULL);
+               update_ref(msg.buf, "ORIG_HEAD", orig, old_orig, 0, MSG_ON_ERR);
+       } else if (old_orig)
+               delete_ref("ORIG_HEAD", old_orig, 0);
+       set_reflog_message(&msg, "updating HEAD", rev);
+       update_ref_status = update_ref(msg.buf, "HEAD", sha1, orig, 0, MSG_ON_ERR);
+       strbuf_release(&msg);
+       return update_ref_status;
+}
+
+int cmd_reset(int argc, const char **argv, const char *prefix)
+{
+       int reset_type = NONE, update_ref_status = 0, quiet = 0;
+       int patch_mode = 0, unborn;
+       const char *rev;
+       unsigned char sha1[20];
+       const char **pathspec = NULL;
        const struct option options[] = {
                OPT__QUIET(&quiet, N_("be quiet, only report errors")),
                OPT_SET_INT(0, "mixed", &reset_type,
@@ -253,73 +263,45 @@ int cmd_reset(int argc, const char **argv, const char *prefix)
 
        argc = parse_options(argc, argv, prefix, options, git_reset_usage,
                                                PARSE_OPT_KEEP_DASHDASH);
-
-       /*
-        * Possible arguments are:
-        *
-        * git reset [-opts] <rev> <paths>...
-        * git reset [-opts] <rev> -- <paths>...
-        * git reset [-opts] -- <paths>...
-        * git reset [-opts] <paths>...
-        *
-        * At this point, argv[i] points immediately after [-opts].
-        */
-
-       if (i < argc) {
-               if (!strcmp(argv[i], "--")) {
-                       i++; /* reset to HEAD, possibly with paths */
-               } else if (i + 1 < argc && !strcmp(argv[i+1], "--")) {
-                       rev = argv[i];
-                       i += 2;
-               }
-               /*
-                * Otherwise, argv[i] could be either <rev> or <paths> and
-                * has to be unambiguous.
-                */
-               else if (!get_sha1_committish(argv[i], sha1)) {
-                       /*
-                        * Ok, argv[i] looks like a rev; it should not
-                        * be a filename.
-                        */
-                       verify_non_filename(prefix, argv[i]);
-                       rev = argv[i++];
-               } else {
-                       /* Otherwise we treat this as a filename */
-                       verify_filename(prefix, argv[i], 1);
-               }
+       pathspec = parse_args(argv, prefix, &rev);
+
+       unborn = !strcmp(rev, "HEAD") && get_sha1("HEAD", sha1);
+       if (unborn) {
+               /* reset on unborn branch: treat as reset to empty tree */
+               hashcpy(sha1, EMPTY_TREE_SHA1_BIN);
+       } else if (!pathspec) {
+               struct commit *commit;
+               if (get_sha1_committish(rev, sha1))
+                       die(_("Failed to resolve '%s' as a valid revision."), rev);
+               commit = lookup_commit_reference(sha1);
+               if (!commit)
+                       die(_("Could not parse object '%s'."), rev);
+               hashcpy(sha1, commit->object.sha1);
+       } else {
+               struct tree *tree;
+               if (get_sha1_treeish(rev, sha1))
+                       die(_("Failed to resolve '%s' as a valid tree."), rev);
+               tree = parse_tree_indirect(sha1);
+               if (!tree)
+                       die(_("Could not parse object '%s'."), rev);
+               hashcpy(sha1, tree->object.sha1);
        }
 
-       if (get_sha1_committish(rev, sha1))
-               die(_("Failed to resolve '%s' as a valid ref."), rev);
-
-       /*
-        * NOTE: As "git reset $treeish -- $path" should be usable on
-        * any tree-ish, this is not strictly correct. We are not
-        * moving the HEAD to any commit; we are merely resetting the
-        * entries in the index to that of a treeish.
-        */
-       commit = lookup_commit_reference(sha1);
-       if (!commit)
-               die(_("Could not parse object '%s'."), rev);
-       hashcpy(sha1, commit->object.sha1);
-
        if (patch_mode) {
                if (reset_type != NONE)
                        die(_("--patch is incompatible with --{hard,mixed,soft}"));
-               return interactive_reset(rev, argv + i, prefix);
+               return run_add_interactive(sha1_to_hex(sha1), "--patch=reset", pathspec);
        }
 
        /* git reset tree [--] paths... can be used to
         * load chosen paths from the tree into the index without
         * affecting the working tree nor HEAD. */
-       if (i < argc) {
+       if (pathspec) {
                if (reset_type == MIXED)
                        warning(_("--mixed with paths is deprecated; use 'git reset -- <paths>' instead."));
                else if (reset_type != NONE)
                        die(_("Cannot do %s reset with paths."),
                                        _(reset_type_names[reset_type]));
-               return read_from_tree(prefix, argv + i, sha1,
-                               quiet ? REFRESH_QUIET : REFRESH_IN_PORCELAIN);
        }
        if (reset_type == NONE)
                reset_type = MIXED; /* by default */
@@ -334,49 +316,44 @@ int cmd_reset(int argc, const char **argv, const char *prefix)
        /* Soft reset does not touch the index file nor the working tree
         * at all, but requires them in a good order.  Other resets reset
         * the index file to the tree object we are switching to. */
-       if (reset_type == SOFT)
+       if (reset_type == SOFT || reset_type == KEEP)
                die_if_unmerged_cache(reset_type);
-       else {
-               int err;
-               if (reset_type == KEEP)
-                       die_if_unmerged_cache(reset_type);
-               err = reset_index_file(sha1, reset_type, quiet);
-               if (reset_type == KEEP)
-                       err = err || reset_index_file(sha1, MIXED, quiet);
-               if (err)
-                       die(_("Could not reset index file to revision '%s'."), rev);
-       }
 
-       /* Any resets update HEAD to the head being switched to,
-        * saving the previous head in ORIG_HEAD before. */
-       if (!get_sha1("ORIG_HEAD", sha1_old_orig))
-               old_orig = sha1_old_orig;
-       if (!get_sha1("HEAD", sha1_orig)) {
-               orig = sha1_orig;
-               set_reflog_message(&msg, "updating ORIG_HEAD", NULL);
-               update_ref(msg.buf, "ORIG_HEAD", orig, old_orig, 0, MSG_ON_ERR);
-       }
-       else if (old_orig)
-               delete_ref("ORIG_HEAD", old_orig, 0);
-       set_reflog_message(&msg, "updating HEAD", rev);
-       update_ref_status = update_ref(msg.buf, "HEAD", sha1, orig, 0, MSG_ON_ERR);
+       if (reset_type != SOFT) {
+               struct lock_file *lock = xcalloc(1, sizeof(struct lock_file));
+               int newfd = hold_locked_index(lock, 1);
+               if (reset_type == MIXED) {
+                       if (read_from_tree(pathspec, sha1))
+                               return 1;
+               } else {
+                       int err = reset_index(sha1, reset_type, quiet);
+                       if (reset_type == KEEP && !err)
+                               err = reset_index(sha1, MIXED, quiet);
+                       if (err)
+                               die(_("Could not reset index file to revision '%s'."), rev);
+               }
 
-       switch (reset_type) {
-       case HARD:
-               if (!update_ref_status && !quiet)
-                       print_new_head_line(commit);
-               break;
-       case SOFT: /* Nothing else to do. */
-               break;
-       case MIXED: /* Report what has not been updated. */
-               update_index_refresh(0, NULL,
-                               quiet ? REFRESH_QUIET : REFRESH_IN_PORCELAIN);
-               break;
+               if (reset_type == MIXED) { /* Report what has not been updated. */
+                       int flags = quiet ? REFRESH_QUIET : REFRESH_IN_PORCELAIN;
+                       refresh_index(&the_index, flags, NULL, NULL,
+                                     _("Unstaged changes after reset:"));
+               }
+
+               if (write_cache(newfd, active_cache, active_nr) ||
+                   commit_locked_index(lock))
+                       die(_("Could not write new index file."));
        }
 
-       remove_branch_state();
+       if (!pathspec && !unborn) {
+               /* Any resets without paths update HEAD to the head being
+                * switched to, saving the previous head in ORIG_HEAD before. */
+               update_ref_status = update_refs(rev, sha1);
 
-       strbuf_release(&msg);
+               if (reset_type == HARD && !update_ref_status && !quiet)
+                       print_new_head_line(lookup_commit_reference(sha1));
+       }
+       if (!pathspec)
+               remove_branch_state();
 
        return update_ref_status;
 }
index 83605143ac0cea68b421dd9a3c056d3a27f66be9..240bff3efa80b3af087fe1e6c16a75108ffee4de 100644 (file)
@@ -36,52 +36,28 @@ static void insert_one_record(struct shortlog *log,
        const char *dot3 = log->common_repo_prefix;
        char *buffer, *p;
        struct string_list_item *item;
-       char namebuf[1024];
-       char emailbuf[1024];
-       size_t len;
+       const char *mailbuf, *namebuf;
+       size_t namelen, maillen;
        const char *eol;
-       const char *boemail, *eoemail;
        struct strbuf subject = STRBUF_INIT;
+       struct strbuf namemailbuf = STRBUF_INIT;
+       struct ident_split ident;
 
-       boemail = strchr(author, '<');
-       if (!boemail)
-               return;
-       eoemail = strchr(boemail, '>');
-       if (!eoemail)
+       if (split_ident_line(&ident, author, strlen(author)))
                return;
 
-       /* copy author name to namebuf, to support matching on both name and email */
-       memcpy(namebuf, author, boemail - author);
-       len = boemail - author;
-       while (len > 0 && isspace(namebuf[len-1]))
-               len--;
-       namebuf[len] = 0;
-
-       /* copy email name to emailbuf, to allow email replacement as well */
-       memcpy(emailbuf, boemail+1, eoemail - boemail);
-       emailbuf[eoemail - boemail - 1] = 0;
-
-       if (!map_user(&log->mailmap, emailbuf, sizeof(emailbuf), namebuf, sizeof(namebuf))) {
-               while (author < boemail && isspace(*author))
-                       author++;
-               for (len = 0;
-                    len < sizeof(namebuf) - 1 && author + len < boemail;
-                    len++)
-                       namebuf[len] = author[len];
-               while (0 < len && isspace(namebuf[len-1]))
-                       len--;
-               namebuf[len] = '\0';
-       }
-       else
-               len = strlen(namebuf);
+       namebuf = ident.name_begin;
+       mailbuf = ident.mail_begin;
+       namelen = ident.name_end - ident.name_begin;
+       maillen = ident.mail_end - ident.mail_begin;
 
-       if (log->email) {
-               size_t room = sizeof(namebuf) - len - 1;
-               int maillen = strlen(emailbuf);
-               snprintf(namebuf + len, room, " <%.*s>", maillen, emailbuf);
-       }
+       map_user(&log->mailmap, &mailbuf, &maillen, &namebuf, &namelen);
+       strbuf_add(&namemailbuf, namebuf, namelen);
+
+       if (log->email)
+               strbuf_addf(&namemailbuf, " <%.*s>", (int)maillen, mailbuf);
 
-       item = string_list_insert(&log->list, namebuf);
+       item = string_list_insert(&log->list, namemailbuf.buf);
        if (item->util == NULL)
                item->util = xcalloc(1, sizeof(struct string_list));
 
diff --git a/cache.h b/cache.h
index c257953fa798110e0f4be9258f4f88f2d7952bdb..1f96f659e484fd1407dbc403c4095205cf3b0c54 100644 (file)
--- a/cache.h
+++ b/cache.h
@@ -1011,7 +1011,6 @@ struct ref {
                requires_force:1,
                merge:1,
                nonfastforward:1,
-               not_forwardable:1,
                update:1,
                deletion:1;
        enum {
@@ -1148,7 +1147,7 @@ extern int check_repository_format_version(const char *var, const char *value, v
 extern int git_env_bool(const char *, int);
 extern int git_config_system(void);
 extern int config_error_nonbool(const char *);
-#ifdef __GNUC__
+#if defined(__GNUC__) && ! defined(__clang__)
 #define config_error_nonbool(s) (config_error_nonbool(s), -1)
 #endif
 extern const char *get_log_output_encoding(void);
index 7e8cfec29d9e00cd6dfb26bca1f1295f64ae53fd..bf83303c48dc5cfbccfc4a31b347dcdec197d7d4 100644 (file)
@@ -12,6 +12,7 @@ git-branch                              mainporcelain common
 git-bundle                              mainporcelain
 git-cat-file                            plumbinginterrogators
 git-check-attr                          purehelpers
+git-check-ignore                        purehelpers
 git-checkout                            mainporcelain common
 git-checkout-index                      plumbingmanipulators
 git-check-ref-format                    purehelpers
index 0f469e507db7b8517e4f37107d855b7877176085..c16c8a75349f2ef4dba1f3c05528d04750a925da 100644 (file)
--- a/commit.h
+++ b/commit.h
@@ -89,6 +89,7 @@ struct pretty_print_context {
        char *notes_message;
        struct reflog_walk_info *reflog_info;
        const char *output_encoding;
+       struct string_list *mailmap;
        int color;
 };
 
index a4c48e179eb778077462a812b491e762e284486c..7147d64af20f8db8daa65958d68adb0802605303 100644 (file)
@@ -397,7 +397,7 @@ __git_complete_revlist_file ()
                *)   pfx="$ref:$pfx" ;;
                esac
 
-               __gitcomp_nl "$(git --git-dir="$(__gitdir)" ls-tree "$ls" \
+               __gitcomp_nl "$(git --git-dir="$(__gitdir)" ls-tree "$ls" 2>/dev/null \
                                | sed '/^100... blob /{
                                           s,^.*        ,,
                                           s,$, ,
@@ -563,6 +563,7 @@ __git_list_porcelain_commands ()
                archimport)       : import;;
                cat-file)         : plumbing;;
                check-attr)       : plumbing;;
+               check-ignore)     : plumbing;;
                check-ref-format) : plumbing;;
                checkout-index)   : plumbing;;
                commit-tree)      : plumbing;;
index 8aafb63315c52a08e6f581e0ef13aa9c239b03a8..3e3889f2b4fd3acb441e56284750cb22fef80547 100644 (file)
@@ -13,6 +13,7 @@
 #
 # To use this completion script:
 #
+#    0) You need tcsh 6.16.00 or newer.
 #    1) Copy both this file and the bash completion script to ${HOME}.
 #       You _must_ use the name ${HOME}/.git-completion.bash for the
 #       bash script.
 #        set autolist=ambiguous
 #       It will tell tcsh to list the possible completion choices.
 
+set __git_tcsh_completion_version = `\echo ${tcsh} | \sed 's/\./ /g'`
+if ( ${__git_tcsh_completion_version[1]} < 6 || \
+     ( ${__git_tcsh_completion_version[1]} == 6 && \
+       ${__git_tcsh_completion_version[2]} < 16 ) ) then
+       echo "git-completion.tcsh: Your version of tcsh is too old, you need version 6.16.00 or newer.  Git completion will not work."
+       exit
+endif
+unset __git_tcsh_completion_version
+
 set __git_tcsh_completion_original_script = ${HOME}/.git-completion.bash
 set __git_tcsh_completion_script = ${HOME}/.git-completion.tcsh.bash
 
@@ -64,9 +74,7 @@ fi
 _\${1}
 
 IFS=\$'\n'
-if [ \${#COMPREPLY[*]} -gt 0 ]; then
-       echo "\${COMPREPLY[*]}" | sort | uniq
-else
+if [ \${#COMPREPLY[*]} -eq 0 ]; then
        # No completions suggested.  In this case, we want tcsh to perform
        # standard file completion.  However, there does not seem to be way
        # to tell tcsh to do that.  To help the user, we try to simulate
@@ -85,19 +93,20 @@ else
                # We don't support ~ expansion: too tricky.
                if [ "\${TO_COMPLETE:0:1}" != "~" ]; then
                        # Use ls so as to add the '/' at the end of directories.
-                       RESULT=(\`ls -dp \${TO_COMPLETE}* 2> /dev/null\`)
-                       echo \${RESULT[*]}
-
-                       # If there is a single completion and it is a directory,
-                       # we output it a second time to trick tcsh into not adding a space
-                       # after it.
-                       if [ \${#RESULT[*]} -eq 1 ] && [ "\${RESULT[0]: -1}" == "/" ]; then
-                               echo \${RESULT[*]}
-                       fi
+                       COMPREPLY=(\`ls -dp \${TO_COMPLETE}* 2> /dev/null\`)
                fi
        fi
 fi
 
+# tcsh does not automatically remove duplicates, so we do it ourselves
+echo "\${COMPREPLY[*]}" | sort | uniq
+
+# If there is a single completion and it is a directory, we output it
+# a second time to trick tcsh into not adding a space after it.
+if [ \${#COMPREPLY[*]} -eq 1 ] && [ "\${COMPREPLY[0]: -1}" == "/" ]; then
+       echo "\${COMPREPLY[*]}"
+fi
+
 EOF
 
 # Don't need this variable anymore, so don't pollute the users environment
index b2171a092eafef4cf1e5e1abfa37695a0c111927..0e5b72d7f19185c3b6b36150085d475c7ef15db4 100755 (executable)
@@ -237,6 +237,7 @@ generate_email_header()
        X-Git-Reftype: $refname_type
        X-Git-Oldrev: $oldrev
        X-Git-Newrev: $newrev
+       Auto-Submitted: auto-generated
 
        This is an automated email from the git hooks/post-receive script. It was
        generated because a ref change was pushed to the repository containing
index c7006000a6f4a19fa4e5f4bf3fc543d4756a1f4d..328c2dc76dd33e47cfe69064757631e9717771e6 100755 (executable)
@@ -53,7 +53,7 @@ def gittz(tz):
     return '%+03d%02d' % (-tz / 3600, -tz % 3600 / 60)
 
 def hgmode(mode):
-    m = { '0100755': 'x', '0120000': 'l' }
+    m = { '100755': 'x', '120000': 'l' }
     return m.get(mode, '')
 
 def get_config(config):
@@ -720,6 +720,14 @@ def do_export(parser):
     if peer:
         parser.repo.push(peer, force=False)
 
+def fix_path(alias, repo, orig_url):
+    repo_url = util.url(repo.url())
+    url = util.url(orig_url)
+    if str(url) == str(repo_url):
+        return
+    cmd = ['git', 'config', 'remote.%s.url' % alias, "hg::%s" % repo_url]
+    subprocess.call(cmd)
+
 def main(args):
     global prefix, dirname, branches, bmarks
     global marks, blob_marks, parsed_refs
@@ -766,6 +774,9 @@ def main(args):
     repo = get_repo(url, alias)
     prefix = 'refs/hg/%s' % alias
 
+    if not is_tmp:
+        fix_path(alias, peer or repo, url)
+
     if not os.path.exists(dirname):
         os.makedirs(dirname)
 
index 3e76d9fb60bff8b25f5b2751c6e11cfabb6c9dc6..7e3967f5b6f0c19939bf1c0c30be0d756ec6aa04 100755 (executable)
@@ -109,6 +109,74 @@ setup () {
 
 setup
 
+test_expect_success 'executable bit' '
+       mkdir -p tmp && cd tmp &&
+       test_when_finished "cd .. && rm -rf tmp" &&
+
+       (
+       git init -q gitrepo &&
+       cd gitrepo &&
+       echo alpha > alpha &&
+       chmod 0644 alpha &&
+       git add alpha &&
+       git commit -m "add alpha" &&
+       chmod 0755 alpha &&
+       git add alpha &&
+       git commit -m "set executable bit" &&
+       chmod 0644 alpha &&
+       git add alpha &&
+       git commit -m "clear executable bit"
+       ) &&
+
+       for x in hg git; do
+               (
+               hg_clone_$x gitrepo hgrepo-$x &&
+               cd hgrepo-$x &&
+               hg_log . &&
+               hg manifest -r 1 -v &&
+               hg manifest -v
+               ) > output-$x &&
+
+               git_clone_$x hgrepo-$x gitrepo2-$x &&
+               git_log gitrepo2-$x > log-$x
+       done &&
+       cp -r log-* output-* /tmp/foo/ &&
+
+       test_cmp output-hg output-git &&
+       test_cmp log-hg log-git
+'
+
+test_expect_success 'symlink' '
+       mkdir -p tmp && cd tmp &&
+       test_when_finished "cd .. && rm -rf tmp" &&
+
+       (
+       git init -q gitrepo &&
+       cd gitrepo &&
+       echo alpha > alpha &&
+       git add alpha &&
+       git commit -m "add alpha" &&
+       ln -s alpha beta &&
+       git add beta &&
+       git commit -m "add beta"
+       ) &&
+
+       for x in hg git; do
+               (
+               hg_clone_$x gitrepo hgrepo-$x &&
+               cd hgrepo-$x &&
+               hg_log . &&
+               hg manifest -v
+               ) > output-$x &&
+
+               git_clone_$x hgrepo-$x gitrepo2-$x &&
+               git_log gitrepo2-$x > log-$x
+       done &&
+
+       test_cmp output-hg output-git &&
+       test_cmp log-hg log-git
+'
+
 test_expect_success 'merge conflict 1' '
        mkdir -p tmp && cd tmp &&
        test_when_finished "cd .. && rm -rf tmp" &&
index fca1e17251f1f414a1d97bf6e6240bdf20daa648..8f16d069721c5b9885b2e700981618878b54191e 100644 (file)
@@ -17,16 +17,6 @@ To install:
 
   1. Copy these files to vim's syntax directory $HOME/.vim/syntax
   2. To auto-detect the editing of various git-related filetypes:
-       $ cat >>$HOME/.vim/filetype.vim <<'EOF'
-       autocmd BufNewFile,BufRead *.git/COMMIT_EDITMSG    setf gitcommit
-       autocmd BufNewFile,BufRead *.git/config,.gitconfig setf gitconfig
-       autocmd BufNewFile,BufRead git-rebase-todo         setf gitrebase
-       autocmd BufNewFile,BufRead .msg.[0-9]*
-               \ if getline(1) =~ '^From.*# This line is ignored.$' |
-               \   setf gitsendemail |
-               \ endif
-       autocmd BufNewFile,BufRead *.git/**
-               \ if getline(1) =~ '^\x\{40\}\>\|^ref: ' |
-               \   setf git |
-               \ endif
-       EOF
+
+       $ curl http://ftp.vim.org/pub/vim/runtime/filetype.vim |
+               sed -ne '/^" Git$/, /^$/ p' >>$HOME/.vim/filetype.vim
diff --git a/diff.c b/diff.c
index 732d4c227595873bb94224da618ce5612a7415af..348f71b46256657860a2f7f79598e1d9b19d6c54 100644 (file)
--- a/diff.c
+++ b/diff.c
@@ -3626,6 +3626,8 @@ int diff_opt_parse(struct diff_options *options, const char **av, int ac)
                DIFF_OPT_SET(options, FIND_COPIES_HARDER);
        else if (!strcmp(arg, "--follow"))
                DIFF_OPT_SET(options, FOLLOW_RENAMES);
+       else if (!strcmp(arg, "--no-follow"))
+               DIFF_OPT_CLR(options, FOLLOW_RENAMES);
        else if (!strcmp(arg, "--color"))
                options->use_color = 1;
        else if (!prefixcmp(arg, "--color=")) {
diff --git a/dir.c b/dir.c
index e883a91483981ad9e2da4841063c1c8bad6a25fd..cf1e6b0082381670809a4293c4a49cfef681d756 100644 (file)
--- a/dir.c
+++ b/dir.c
@@ -194,12 +194,19 @@ static int match_one(const char *match, const char *name, int namelen)
 }
 
 /*
- * Given a name and a list of pathspecs, see if the name matches
- * any of the pathspecs.  The caller is also interested in seeing
- * all pathspec matches some names it calls this function with
- * (otherwise the user could have mistyped the unmatched pathspec),
- * and a mark is left in seen[] array for pathspec element that
- * actually matched anything.
+ * Given a name and a list of pathspecs, returns the nature of the
+ * closest (i.e. most specific) match of the name to any of the
+ * pathspecs.
+ *
+ * The caller typically calls this multiple times with the same
+ * pathspec and seen[] array but with different name/namelen
+ * (e.g. entries from the index) and is interested in seeing if and
+ * how each pathspec matches all the names it calls this function
+ * with.  A mark is left in the seen[] array for each pathspec element
+ * indicating the closest type of match that element achieved, so if
+ * seen[n] remains zero after multiple invocations, that means the nth
+ * pathspec did not match any names, which could indicate that the
+ * user mistyped the nth pathspec.
  */
 int match_pathspec(const char **pathspec, const char *name, int namelen,
                int prefix, char *seen)
@@ -269,12 +276,19 @@ static int match_pathspec_item(const struct pathspec_item *item, int prefix,
 }
 
 /*
- * Given a name and a list of pathspecs, see if the name matches
- * any of the pathspecs.  The caller is also interested in seeing
- * all pathspec matches some names it calls this function with
- * (otherwise the user could have mistyped the unmatched pathspec),
- * and a mark is left in seen[] array for pathspec element that
- * actually matched anything.
+ * Given a name and a list of pathspecs, returns the nature of the
+ * closest (i.e. most specific) match of the name to any of the
+ * pathspecs.
+ *
+ * The caller typically calls this multiple times with the same
+ * pathspec and seen[] array but with different name/namelen
+ * (e.g. entries from the index) and is interested in seeing if and
+ * how each pathspec matches all the names it calls this function
+ * with.  A mark is left in the seen[] array for each pathspec element
+ * indicating the closest type of match that element achieved, so if
+ * seen[n] remains zero after multiple invocations, that means the nth
+ * pathspec did not match any names, which could indicate that the
+ * user mistyped the nth pathspec.
  */
 int match_pathspec_depth(const struct pathspec *ps,
                         const char *name, int namelen,
@@ -379,7 +393,7 @@ void parse_exclude_pattern(const char **pattern,
 }
 
 void add_exclude(const char *string, const char *base,
-                int baselen, struct exclude_list *el)
+                int baselen, struct exclude_list *el, int srcpos)
 {
        struct exclude *x;
        int patternlen;
@@ -403,8 +417,10 @@ void add_exclude(const char *string, const char *base,
        x->base = base;
        x->baselen = baselen;
        x->flags = flags;
+       x->srcpos = srcpos;
        ALLOC_GROW(el->excludes, el->nr + 1, el->alloc);
        el->excludes[el->nr++] = x;
+       x->el = el;
 }
 
 static void *read_skip_worktree_file_from_index(const char *path, size_t *size)
@@ -441,20 +457,21 @@ void clear_exclude_list(struct exclude_list *el)
        for (i = 0; i < el->nr; i++)
                free(el->excludes[i]);
        free(el->excludes);
+       free(el->filebuf);
 
        el->nr = 0;
        el->excludes = NULL;
+       el->filebuf = NULL;
 }
 
 int add_excludes_from_file_to_list(const char *fname,
                                   const char *base,
                                   int baselen,
-                                  char **buf_p,
                                   struct exclude_list *el,
                                   int check_index)
 {
        struct stat st;
-       int fd, i;
+       int fd, i, lineno = 1;
        size_t size = 0;
        char *buf, *entry;
 
@@ -492,25 +509,43 @@ int add_excludes_from_file_to_list(const char *fname,
                close(fd);
        }
 
-       if (buf_p)
-               *buf_p = buf;
+       el->filebuf = buf;
        entry = buf;
        for (i = 0; i < size; i++) {
                if (buf[i] == '\n') {
                        if (entry != buf + i && entry[0] != '#') {
                                buf[i - (i && buf[i-1] == '\r')] = 0;
-                               add_exclude(entry, base, baselen, el);
+                               add_exclude(entry, base, baselen, el, lineno);
                        }
+                       lineno++;
                        entry = buf + i + 1;
                }
        }
        return 0;
 }
 
+struct exclude_list *add_exclude_list(struct dir_struct *dir,
+                                     int group_type, const char *src)
+{
+       struct exclude_list *el;
+       struct exclude_list_group *group;
+
+       group = &dir->exclude_list_group[group_type];
+       ALLOC_GROW(group->el, group->nr + 1, group->alloc);
+       el = &group->el[group->nr++];
+       memset(el, 0, sizeof(*el));
+       el->src = src;
+       return el;
+}
+
+/*
+ * Used to set up core.excludesfile and .git/info/exclude lists.
+ */
 void add_excludes_from_file(struct dir_struct *dir, const char *fname)
 {
-       if (add_excludes_from_file_to_list(fname, "", 0, NULL,
-                                          &dir->exclude_list[EXC_FILE], 0) < 0)
+       struct exclude_list *el;
+       el = add_exclude_list(dir, EXC_FILE, fname);
+       if (add_excludes_from_file_to_list(fname, "", 0, el, 0) < 0)
                die("cannot use %s as an exclude file", fname);
 }
 
@@ -520,6 +555,7 @@ void add_excludes_from_file(struct dir_struct *dir, const char *fname)
  */
 static void prep_exclude(struct dir_struct *dir, const char *base, int baselen)
 {
+       struct exclude_list_group *group;
        struct exclude_list *el;
        struct exclude_stack *stk = NULL;
        int current;
@@ -528,17 +564,21 @@ static void prep_exclude(struct dir_struct *dir, const char *base, int baselen)
            (baselen + strlen(dir->exclude_per_dir) >= PATH_MAX))
                return; /* too long a path -- ignore */
 
-       /* Pop the directories that are not the prefix of the path being checked. */
-       el = &dir->exclude_list[EXC_DIRS];
+       group = &dir->exclude_list_group[EXC_DIRS];
+
+       /* Pop the exclude lists from the EXCL_DIRS exclude_list_group
+        * which originate from directories not in the prefix of the
+        * path being checked. */
        while ((stk = dir->exclude_stack) != NULL) {
                if (stk->baselen <= baselen &&
                    !strncmp(dir->basebuf, base, stk->baselen))
                        break;
+               el = &group->el[dir->exclude_stack->exclude_ix];
                dir->exclude_stack = stk->prev;
-               while (stk->exclude_ix < el->nr)
-                       free(el->excludes[--el->nr]);
-               free(stk->filebuf);
+               free((char *)el->src); /* see strdup() below */
+               clear_exclude_list(el);
                free(stk);
+               group->nr--;
        }
 
        /* Read from the parent directories and push them down. */
@@ -559,13 +599,22 @@ static void prep_exclude(struct dir_struct *dir, const char *base, int baselen)
                }
                stk->prev = dir->exclude_stack;
                stk->baselen = cp - base;
-               stk->exclude_ix = el->nr;
                memcpy(dir->basebuf + current, base + current,
                       stk->baselen - current);
                strcpy(dir->basebuf + stk->baselen, dir->exclude_per_dir);
+               /*
+                * dir->basebuf gets reused by the traversal, but we
+                * need fname to remain unchanged to ensure the src
+                * member of each struct exclude correctly
+                * back-references its source file.  Other invocations
+                * of add_exclude_list provide stable strings, so we
+                * strdup() and free() here in the caller.
+                */
+               el = add_exclude_list(dir, EXC_DIRS, strdup(dir->basebuf));
+               stk->exclude_ix = group->nr - 1;
                add_excludes_from_file_to_list(dir->basebuf,
                                               dir->basebuf, stk->baselen,
-                                              &stk->filebuf, el, 1);
+                                              el, 1);
                dir->exclude_stack = stk;
                current = stk->baselen;
        }
@@ -712,18 +761,23 @@ static struct exclude *last_exclude_matching(struct dir_struct *dir,
                                             int *dtype_p)
 {
        int pathlen = strlen(pathname);
-       int st;
+       int i, j;
+       struct exclude_list_group *group;
        struct exclude *exclude;
        const char *basename = strrchr(pathname, '/');
        basename = (basename) ? basename+1 : pathname;
 
        prep_exclude(dir, pathname, basename-pathname);
-       for (st = EXC_CMDL; st <= EXC_FILE; st++) {
-               exclude = last_exclude_matching_from_list(
-                       pathname, pathlen, basename, dtype_p,
-                       &dir->exclude_list[st]);
-               if (exclude)
-                       return exclude;
+
+       for (i = EXC_CMDL; i <= EXC_FILE; i++) {
+               group = &dir->exclude_list_group[i];
+               for (j = group->nr - 1; j >= 0; j--) {
+                       exclude = last_exclude_matching_from_list(
+                               pathname, pathlen, basename, dtype_p,
+                               &group->el[j]);
+                       if (exclude)
+                               return exclude;
+               }
        }
        return NULL;
 }
@@ -842,7 +896,8 @@ static struct dir_entry *dir_entry_new(const char *pathname, int len)
 
 static struct dir_entry *dir_add_name(struct dir_struct *dir, const char *pathname, int len)
 {
-       if (cache_name_exists(pathname, len, ignore_case))
+       if (!(dir->flags & DIR_SHOW_IGNORED) &&
+           cache_name_exists(pathname, len, ignore_case))
                return NULL;
 
        ALLOC_GROW(dir->entries, dir->nr+1, dir->alloc);
@@ -944,8 +999,9 @@ static enum exist_status directory_exists_in_index(const char *dirname, int len)
  * traversal routine.
  *
  * Case 1: If we *already* have entries in the index under that
- * directory name, we always recurse into the directory to see
- * all the files.
+ * directory name, we recurse into the directory to see all the files,
+ * unless the directory is excluded and we want to show ignored
+ * directories
  *
  * Case 2: If we *already* have that directory name as a gitlink,
  * we always continue to see it as a gitlink, regardless of whether
@@ -959,6 +1015,9 @@ static enum exist_status directory_exists_in_index(const char *dirname, int len)
  *      just a directory, unless "hide_empty_directories" is
  *      also true and the directory is empty, in which case
  *      we just ignore it entirely.
+ *      if we are looking for ignored directories, look if it
+ *      contains only ignored files to decide if it must be shown as
+ *      ignored or not.
  *  (b) if it looks like a git directory, and we don't have
  *      'no_gitlinks' set we treat it as a gitlink, and show it
  *      as a directory.
@@ -971,12 +1030,15 @@ enum directory_treatment {
 };
 
 static enum directory_treatment treat_directory(struct dir_struct *dir,
-       const char *dirname, int len,
+       const char *dirname, int len, int exclude,
        const struct path_simplify *simplify)
 {
        /* The "len-1" is to strip the final '/' */
        switch (directory_exists_in_index(dirname, len-1)) {
        case index_directory:
+               if ((dir->flags & DIR_SHOW_OTHER_DIRECTORIES) && exclude)
+                       break;
+
                return recurse_into_directory;
 
        case index_gitdir:
@@ -996,13 +1058,68 @@ static enum directory_treatment treat_directory(struct dir_struct *dir,
        }
 
        /* This is the "show_other_directories" case */
-       if (!(dir->flags & DIR_HIDE_EMPTY_DIRECTORIES))
+
+       /*
+        * We are looking for ignored files and our directory is not ignored,
+        * check if it contains only ignored files
+        */
+       if ((dir->flags & DIR_SHOW_IGNORED) && !exclude) {
+               int ignored;
+               dir->flags &= ~DIR_SHOW_IGNORED;
+               dir->flags |= DIR_HIDE_EMPTY_DIRECTORIES;
+               ignored = read_directory_recursive(dir, dirname, len, 1, simplify);
+               dir->flags &= ~DIR_HIDE_EMPTY_DIRECTORIES;
+               dir->flags |= DIR_SHOW_IGNORED;
+
+               return ignored ? ignore_directory : show_directory;
+       }
+       if (!(dir->flags & DIR_SHOW_IGNORED) &&
+           !(dir->flags & DIR_HIDE_EMPTY_DIRECTORIES))
                return show_directory;
        if (!read_directory_recursive(dir, dirname, len, 1, simplify))
                return ignore_directory;
        return show_directory;
 }
 
+/*
+ * Decide what to do when we find a file while traversing the
+ * filesystem. Mostly two cases:
+ *
+ *  1. We are looking for ignored files
+ *   (a) File is ignored, include it
+ *   (b) File is in ignored path, include it
+ *   (c) File is not ignored, exclude it
+ *
+ *  2. Other scenarios, include the file if not excluded
+ *
+ * Return 1 for exclude, 0 for include.
+ */
+static int treat_file(struct dir_struct *dir, struct strbuf *path, int exclude, int *dtype)
+{
+       struct path_exclude_check check;
+       int exclude_file = 0;
+
+       if (exclude)
+               exclude_file = !(dir->flags & DIR_SHOW_IGNORED);
+       else if (dir->flags & DIR_SHOW_IGNORED) {
+               /* Always exclude indexed files */
+               struct cache_entry *ce = index_name_exists(&the_index,
+                   path->buf, path->len, ignore_case);
+
+               if (ce)
+                       return 1;
+
+               path_exclude_check_init(&check, dir);
+
+               if (!is_path_excluded(&check, path->buf, path->len, dtype))
+                       exclude_file = 1;
+
+               path_exclude_check_clear(&check);
+       }
+
+       return exclude_file;
+}
+
 /*
  * This is an inexact early pruning of any recursive directory
  * reading - if the path cannot possibly be in the pathspec,
@@ -1141,27 +1258,14 @@ static enum path_treatment treat_one_path(struct dir_struct *dir,
        if (dtype == DT_UNKNOWN)
                dtype = get_dtype(de, path->buf, path->len);
 
-       /*
-        * Do we want to see just the ignored files?
-        * We still need to recurse into directories,
-        * even if we don't ignore them, since the
-        * directory may contain files that we do..
-        */
-       if (!exclude && (dir->flags & DIR_SHOW_IGNORED)) {
-               if (dtype != DT_DIR)
-                       return path_ignored;
-       }
-
        switch (dtype) {
        default:
                return path_ignored;
        case DT_DIR:
                strbuf_addch(path, '/');
-               switch (treat_directory(dir, path->buf, path->len, simplify)) {
+
+               switch (treat_directory(dir, path->buf, path->len, exclude, simplify)) {
                case show_directory:
-                       if (exclude != !!(dir->flags
-                                         & DIR_SHOW_IGNORED))
-                               return path_ignored;
                        break;
                case recurse_into_directory:
                        return path_recurse;
@@ -1171,7 +1275,12 @@ static enum path_treatment treat_one_path(struct dir_struct *dir,
                break;
        case DT_REG:
        case DT_LNK:
-               break;
+               switch (treat_file(dir, path, exclude, &dtype)) {
+               case 1:
+                       return path_ignored;
+               default:
+                       break;
+               }
        }
        return path_handled;
 }
@@ -1572,3 +1681,33 @@ int limit_pathspec_to_literal(void)
                flag = git_env_bool(GIT_LITERAL_PATHSPECS_ENVIRONMENT, 0);
        return flag;
 }
+
+/*
+ * Frees memory within dir which was allocated for exclude lists and
+ * the exclude_stack.  Does not free dir itself.
+ */
+void clear_directory(struct dir_struct *dir)
+{
+       int i, j;
+       struct exclude_list_group *group;
+       struct exclude_list *el;
+       struct exclude_stack *stk;
+
+       for (i = EXC_CMDL; i <= EXC_FILE; i++) {
+               group = &dir->exclude_list_group[i];
+               for (j = 0; j < group->nr; j++) {
+                       el = &group->el[j];
+                       if (i == EXC_DIRS)
+                               free((char *)el->src);
+                       clear_exclude_list(el);
+               }
+               free(group->el);
+       }
+
+       stk = dir->exclude_stack;
+       while (stk) {
+               struct exclude_stack *prev = stk->prev;
+               free(stk);
+               stk = prev;
+       }
+}
diff --git a/dir.h b/dir.h
index ae1bc467ae909e6db151b055e212ded45fac34f6..c3eb4b520effb63e73653194e4266c0ef628cccd 100644 (file)
--- a/dir.h
+++ b/dir.h
@@ -16,21 +16,41 @@ struct dir_entry {
 #define EXC_FLAG_NEGATIVE 16
 
 /*
- * Each .gitignore file will be parsed into patterns which are then
- * appended to the relevant exclude_list (either EXC_DIRS or
- * EXC_FILE).  exclude_lists are also used to represent the list of
- * --exclude values passed via CLI args (EXC_CMDL).
+ * Each excludes file will be parsed into a fresh exclude_list which
+ * is appended to the relevant exclude_list_group (either EXC_DIRS or
+ * EXC_FILE).  An exclude_list within the EXC_CMDL exclude_list_group
+ * can also be used to represent the list of --exclude values passed
+ * via CLI args.
  */
 struct exclude_list {
        int nr;
        int alloc;
+
+       /* remember pointer to exclude file contents so we can free() */
+       char *filebuf;
+
+       /* origin of list, e.g. path to filename, or descriptive string */
+       const char *src;
+
        struct exclude {
+               /*
+                * This allows callers of last_exclude_matching() etc.
+                * to determine the origin of the matching pattern.
+                */
+               struct exclude_list *el;
+
                const char *pattern;
                int patternlen;
                int nowildcardlen;
                const char *base;
                int baselen;
                int flags;
+
+               /*
+                * Counting starts from 1 for line numbers in ignore files,
+                * and from -1 decrementing for patterns from CLI args.
+                */
+               int srcpos;
        } **excludes;
 };
 
@@ -42,9 +62,13 @@ struct exclude_list {
  */
 struct exclude_stack {
        struct exclude_stack *prev; /* the struct exclude_stack for the parent directory */
-       char *filebuf; /* remember pointer to per-directory exclude file contents so we can free() */
        int baselen;
-       int exclude_ix;
+       int exclude_ix; /* index of exclude_list within EXC_DIRS exclude_list_group */
+};
+
+struct exclude_list_group {
+       int nr, alloc;
+       struct exclude_list *el;
 };
 
 struct dir_struct {
@@ -62,16 +86,23 @@ struct dir_struct {
 
        /* Exclude info */
        const char *exclude_per_dir;
-       struct exclude_list exclude_list[3];
+
        /*
-        * We maintain three exclude pattern lists:
+        * We maintain three groups of exclude pattern lists:
+        *
         * EXC_CMDL lists patterns explicitly given on the command line.
         * EXC_DIRS lists patterns obtained from per-directory ignore files.
-        * EXC_FILE lists patterns from fallback ignore files.
+        * EXC_FILE lists patterns from fallback ignore files, e.g.
+        *   - .git/info/exclude
+        *   - core.excludesfile
+        *
+        * Each group contains multiple exclude lists, a single list
+        * per source.
         */
 #define EXC_CMDL 0
 #define EXC_DIRS 1
 #define EXC_FILE 2
+       struct exclude_list_group exclude_list_group[3];
 
        /*
         * Temporary variables which are used during loading of the
@@ -85,6 +116,12 @@ struct dir_struct {
        char basebuf[PATH_MAX];
 };
 
+/*
+ * The ordering of these constants is significant, with
+ * higher-numbered match types signifying "closer" (i.e. more
+ * specific) matches which will override lower-numbered match types
+ * when populating the seen[] array.
+ */
 #define MATCHED_RECURSIVELY 1
 #define MATCHED_FNMATCH 2
 #define MATCHED_EXACTLY 3
@@ -129,13 +166,16 @@ extern struct exclude *last_exclude_matching_path(struct path_exclude_check *, c
 extern int is_path_excluded(struct path_exclude_check *, const char *, int namelen, int *dtype);
 
 
+extern struct exclude_list *add_exclude_list(struct dir_struct *dir,
+                                            int group_type, const char *src);
 extern int add_excludes_from_file_to_list(const char *fname, const char *base, int baselen,
-                                         char **buf_p, struct exclude_list *el, int check_index);
+                                         struct exclude_list *el, int check_index);
 extern void add_excludes_from_file(struct dir_struct *, const char *fname);
 extern void parse_exclude_pattern(const char **string, int *patternlen, int *flags, int *nowildcardlen);
 extern void add_exclude(const char *string, const char *base,
-                       int baselen, struct exclude_list *el);
+                       int baselen, struct exclude_list *el, int srcpos);
 extern void clear_exclude_list(struct exclude_list *el);
+extern void clear_directory(struct dir_struct *dir);
 extern int file_exists(const char *);
 
 extern int is_inside_dir(const char *dir);
index e5a4b7450bfa7a700ce1c76f597266a9724a19f5..dab545e02e17b834da63d911615bd2afa04a702c 100644 (file)
@@ -297,7 +297,7 @@ extern void warning(const char *err, ...) __attribute__((format (printf, 1, 2)))
  * behavior. But since we're only trying to help gcc, anyway, it's OK; other
  * compilers will fall back to using the function as usual.
  */
-#ifdef __GNUC__
+#if defined(__GNUC__) && ! defined(__clang__)
 #define error(fmt, ...) (error((fmt), ##__VA_ARGS__), -1)
 #endif
 
index c5ebfa06365da82d68103300ad6e8c818ab37fb6..36790749830251854fa475f2bd8d33ece07203f6 100755 (executable)
@@ -60,6 +60,7 @@
     'Valid-responses' => \&req_Validresponses,
     'valid-requests'  => \&req_validrequests,
     'Directory'       => \&req_Directory,
+    'Sticky'          => \&req_Sticky,
     'Entry'           => \&req_Entry,
     'Modified'        => \&req_Modified,
     'Unchanged'       => \&req_Unchanged,
@@ -470,11 +471,19 @@ sub req_Directory
     {
         $log->info("Setting prepend to '$state->{path}'");
         $state->{prependdir} = $state->{path};
+        my %entries;
         foreach my $entry ( keys %{$state->{entries}} )
         {
-            $state->{entries}{$state->{prependdir} . $entry} = $state->{entries}{$entry};
-            delete $state->{entries}{$entry};
+            $entries{$state->{prependdir} . $entry} = $state->{entries}{$entry};
         }
+        $state->{entries}=\%entries;
+
+        my %dirMap;
+        foreach my $dir ( keys %{$state->{dirMap}} )
+        {
+            $dirMap{$state->{prependdir} . $dir} = $state->{dirMap}{$dir};
+        }
+        $state->{dirMap}=\%dirMap;
     }
 
     if ( defined ( $state->{prependdir} ) )
@@ -482,9 +491,60 @@ sub req_Directory
         $log->debug("Prepending '$state->{prependdir}' to state|directory");
         $state->{directory} = $state->{prependdir} . $state->{directory}
     }
+
+    if ( ! defined($state->{dirMap}{$state->{directory}}) )
+    {
+        $state->{dirMap}{$state->{directory}} =
+            {
+                'names' => {}
+                #'tagspec' => undef
+            };
+    }
+
     $log->debug("req_Directory : localdir=$data repository=$repository path=$state->{path} directory=$state->{directory} module=$state->{module}");
 }
 
+# Sticky tagspec \n
+#     Response expected: no. Tell the server that the directory most
+#     recently specified with Directory has a sticky tag or date
+#     tagspec. The first character of tagspec is T for a tag, D for
+#     a date, or some other character supplied by a Set-sticky
+#     response from a previous request to the server. The remainder
+#     of tagspec contains the actual tag or date, again as supplied
+#     by Set-sticky.
+#          The server should remember Static-directory and Sticky requests
+#     for a particular directory; the client need not resend them each
+#     time it sends a Directory request for a given directory. However,
+#     the server is not obliged to remember them beyond the context
+#     of a single command.
+sub req_Sticky
+{
+    my ( $cmd, $tagspec ) = @_;
+
+    my ( $stickyInfo );
+    if($tagspec eq "")
+    {
+        # nothing
+    }
+    elsif($tagspec=~/^T([^ ]+)\s*$/)
+    {
+        $stickyInfo = { 'tag' => $1 };
+    }
+    elsif($tagspec=~/^D([0-9.]+)\s*$/)
+    {
+        $stickyInfo= { 'date' => $1 };
+    }
+    else
+    {
+        die "Unknown tag_or_date format\n";
+    }
+    $state->{dirMap}{$state->{directory}}{stickyInfo}=$stickyInfo;
+
+    $log->debug("req_Sticky : tagspec=$tagspec repository=$state->{repository}"
+                . " path=$state->{path} directory=$state->{directory}"
+                . " module=$state->{module}");
+}
+
 # Entry entry-line \n
 #     Response expected: no. Tell the server what version of a file is on the
 #     local machine. The name in entry-line is a name relative to the directory
@@ -511,6 +571,8 @@ sub req_Entry
         tag_or_date => $data[5],
     };
 
+    $state->{dirMap}{$state->{directory}}{names}{$data[1]} = 'F';
+
     $log->info("Received entry line '$data' => '" . $state->{directory} . $data[1] . "'");
 }
 
@@ -549,7 +611,10 @@ sub req_add
     {
         $filename = filecleanup($filename);
 
-        my $meta = $updater->getmeta($filename);
+        # no -r, -A, or -D with add
+        my $stickyInfo = resolveStickyInfo($filename);
+
+        my $meta = $updater->getmeta($filename,$stickyInfo);
         my $wrev = revparse($filename);
 
         if ($wrev && $meta && ($wrev=~/^-/))
@@ -572,8 +637,10 @@ sub req_add
 
                 # this is an "entries" line
                 my $kopts = kopts_from_path($filename,"sha1",$meta->{filehash});
-                $log->debug("/$filepart/$meta->{revision}//$kopts/");
-                print "/$filepart/$meta->{revision}//$kopts/\n";
+                my $entryLine = "/$filepart/$meta->{revision}//$kopts/";
+                $entryLine .= getStickyTagOrDate($stickyInfo);
+                $log->debug($entryLine);
+                print "$entryLine\n";
                 # permissions
                 $log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");
                 print "u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";
@@ -604,7 +671,8 @@ sub req_add
         print "$filename\n";
         my $kopts = kopts_from_path($filename,"file",
                         $state->{entries}{$filename}{modified_filename});
-        print "/$filepart/0//$kopts/\n";
+        print "/$filepart/0//$kopts/" .
+              getStickyTagOrDate($stickyInfo) . "\n";
 
         my $requestedKopts = $state->{opt}{k};
         if(defined($requestedKopts))
@@ -672,7 +740,10 @@ sub req_remove
             next;
         }
 
-        my $meta = $updater->getmeta($filename);
+        # only from entries
+        my $stickyInfo = resolveStickyInfo($filename);
+
+        my $meta = $updater->getmeta($filename,$stickyInfo);
         my $wrev = revparse($filename);
 
         unless ( defined ( $wrev ) )
@@ -702,7 +773,7 @@ sub req_remove
         print "Checked-in $dirpart\n";
         print "$filename\n";
         my $kopts = kopts_from_path($filename,"sha1",$meta->{filehash});
-        print "/$filepart/-$wrev//$kopts/\n";
+        print "/$filepart/-$wrev//$kopts/" . getStickyTagOrDate($stickyInfo) . "\n";
 
         $rmcount++;
     }
@@ -882,6 +953,9 @@ sub req_co
         return 1;
     }
 
+    my $stickyInfo = { 'tag' => $state->{opt}{r},
+                       'date' => $state->{opt}{D} };
+
     my $module = $state->{args}[0];
     $state->{module} = $module;
     my $checkout_path = $module;
@@ -899,64 +973,32 @@ sub req_co
     my $updater = GITCVS::updater->new($state->{CVSROOT}, $module, $log);
     $updater->update();
 
-    $checkout_path =~ s|/$||; # get rid of trailing slashes
+    my $headHash;
+    if( defined($stickyInfo) && defined($stickyInfo->{tag}) )
+    {
+        $headHash = $updater->lookupCommitRef($stickyInfo->{tag});
+        if( !defined($headHash) )
+        {
+            print "error 1 no such tag `$stickyInfo->{tag}'\n";
+            cleanupWorkTree();
+            exit;
+        }
+    }
 
-    # Eclipse seems to need the Clear-sticky command
-    # to prepare the 'Entries' file for the new directory.
-    print "Clear-sticky $checkout_path/\n";
-    print $state->{CVSROOT} . "/$module/\n";
-    print "Clear-static-directory $checkout_path/\n";
-    print $state->{CVSROOT} . "/$module/\n";
-    print "Clear-sticky $checkout_path/\n"; # yes, twice
-    print $state->{CVSROOT} . "/$module/\n";
-    print "Template $checkout_path/\n";
-    print $state->{CVSROOT} . "/$module/\n";
-    print "0\n";
-
-    # instruct the client that we're checking out to $checkout_path
-    print "E cvs checkout: Updating $checkout_path\n";
+    $checkout_path =~ s|/$||; # get rid of trailing slashes
 
     my %seendirs = ();
     my $lastdir ='';
 
-    # recursive
-    sub prepdir {
-       my ($dir, $repodir, $remotedir, $seendirs) = @_;
-       my $parent = dirname($dir);
-       $dir       =~ s|/+$||;
-       $repodir   =~ s|/+$||;
-       $remotedir =~ s|/+$||;
-       $parent    =~ s|/+$||;
-       $log->debug("announcedir $dir, $repodir, $remotedir" );
-
-       if ($parent eq '.' || $parent eq './') {
-           $parent = '';
-       }
-       # recurse to announce unseen parents first
-       if (length($parent) && !exists($seendirs->{$parent})) {
-           prepdir($parent, $repodir, $remotedir, $seendirs);
-       }
-       # Announce that we are going to modify at the parent level
-       if ($parent) {
-           print "E cvs checkout: Updating $remotedir/$parent\n";
-       } else {
-           print "E cvs checkout: Updating $remotedir\n";
-       }
-       print "Clear-sticky $remotedir/$parent/\n";
-       print "$repodir/$parent/\n";
-
-       print "Clear-static-directory $remotedir/$dir/\n";
-       print "$repodir/$dir/\n";
-       print "Clear-sticky $remotedir/$parent/\n"; # yes, twice
-       print "$repodir/$parent/\n";
-       print "Template $remotedir/$dir/\n";
-       print "$repodir/$dir/\n";
-       print "0\n";
-
-       $seendirs->{$dir} = 1;
-    }
-
-    foreach my $git ( @{$updater->gethead} )
+    prepDirForOutput(
+            ".",
+            $state->{CVSROOT} . "/$module",
+            $checkout_path,
+            \%seendirs,
+            'checkout',
+            $state->{dirArgs} );
+
+    foreach my $git ( @{$updater->getAnyHead($headHash)} )
     {
         # Don't want to check out deleted files
         next if ( $git->{filehash} eq "deleted" );
@@ -964,16 +1006,13 @@ sub req_co
         my $fullName = $git->{name};
         ( $git->{name}, $git->{dir} ) = filenamesplit($git->{name});
 
-       if (length($git->{dir}) && $git->{dir} ne './'
-           && $git->{dir} ne $lastdir ) {
-           unless (exists($seendirs{$git->{dir}})) {
-               prepdir($git->{dir}, $state->{CVSROOT} . "/$module/",
-                       $checkout_path, \%seendirs);
-               $lastdir = $git->{dir};
-               $seendirs{$git->{dir}} = 1;
-           }
-           print "E cvs checkout: Updating /$checkout_path/$git->{dir}\n";
-       }
+        unless (exists($seendirs{$git->{dir}})) {
+            prepDirForOutput($git->{dir}, $state->{CVSROOT} . "/$module/",
+                             $checkout_path, \%seendirs, 'checkout',
+                             $state->{dirArgs} );
+            $lastdir = $git->{dir};
+            $seendirs{$git->{dir}} = 1;
+        }
 
         # modification time of this file
         print "Mod-time $git->{modified}\n";
@@ -993,7 +1032,8 @@ sub req_co
 
         # this is an "entries" line
         my $kopts = kopts_from_path($fullName,"sha1",$git->{filehash});
-        print "/$git->{name}/$git->{revision}//$kopts/\n";
+        print "/$git->{name}/$git->{revision}//$kopts/" .
+                        getStickyTagOrDate($stickyInfo) . "\n";
         # permissions
         print "u=$git->{mode},g=$git->{mode},o=$git->{mode}\n";
 
@@ -1006,6 +1046,119 @@ sub req_co
     statecleanup();
 }
 
+# used by req_co and req_update to set up directories for files
+# recursively handles parents
+sub prepDirForOutput
+{
+    my ($dir, $repodir, $remotedir, $seendirs, $request, $dirArgs) = @_;
+
+    my $parent = dirname($dir);
+    $dir       =~ s|/+$||;
+    $repodir   =~ s|/+$||;
+    $remotedir =~ s|/+$||;
+    $parent    =~ s|/+$||;
+
+    if ($parent eq '.' || $parent eq './')
+    {
+        $parent = '';
+    }
+    # recurse to announce unseen parents first
+    if( length($parent) &&
+        !exists($seendirs->{$parent}) &&
+        ( $request eq "checkout" ||
+          exists($dirArgs->{$parent}) ) )
+    {
+        prepDirForOutput($parent, $repodir, $remotedir,
+                         $seendirs, $request, $dirArgs);
+    }
+    # Announce that we are going to modify at the parent level
+    if ($dir eq '.' || $dir eq './')
+    {
+        $dir = '';
+    }
+    if(exists($seendirs->{$dir}))
+    {
+        return;
+    }
+    $log->debug("announcedir $dir, $repodir, $remotedir" );
+    my($thisRemoteDir,$thisRepoDir);
+    if ($dir ne "")
+    {
+        $thisRepoDir="$repodir/$dir";
+        if($remotedir eq ".")
+        {
+            $thisRemoteDir=$dir;
+        }
+        else
+        {
+            $thisRemoteDir="$remotedir/$dir";
+        }
+    }
+    else
+    {
+        $thisRepoDir=$repodir;
+        $thisRemoteDir=$remotedir;
+    }
+    unless ( $state->{globaloptions}{-Q} || $state->{globaloptions}{-q} )
+    {
+        print "E cvs $request: Updating $thisRemoteDir\n";
+    }
+
+    my ($opt_r)=$state->{opt}{r};
+    my $stickyInfo;
+    if(exists($state->{opt}{A}))
+    {
+        # $stickyInfo=undef;
+    }
+    elsif( defined($opt_r) && $opt_r ne "" )
+           # || ( defined($state->{opt}{D}) && $state->{opt}{D} ne "" ) # TODO
+    {
+        $stickyInfo={ 'tag' => (defined($opt_r)?$opt_r:undef) };
+
+        # TODO: Convert -D value into the form 2011.04.10.04.46.57,
+        #   similar to an entry line's sticky date, without the D prefix.
+        #   It sometimes (always?) arrives as something more like
+        #   '10 Apr 2011 04:46:57 -0000'...
+        # $stickyInfo={ 'date' => (defined($stickyDate)?$stickyDate:undef) };
+    }
+    else
+    {
+        $stickyInfo=getDirStickyInfo($state->{prependdir} . $dir);
+    }
+
+    my $stickyResponse;
+    if(defined($stickyInfo))
+    {
+        $stickyResponse = "Set-sticky $thisRemoteDir/\n" .
+                          "$thisRepoDir/\n" .
+                          getStickyTagOrDate($stickyInfo) . "\n";
+    }
+    else
+    {
+        $stickyResponse = "Clear-sticky $thisRemoteDir/\n" .
+                          "$thisRepoDir/\n";
+    }
+
+    unless ( $state->{globaloptions}{-n} )
+    {
+        print $stickyResponse;
+
+        print "Clear-static-directory $thisRemoteDir/\n";
+        print "$thisRepoDir/\n";
+        print $stickyResponse; # yes, twice
+        print "Template $thisRemoteDir/\n";
+        print "$thisRepoDir/\n";
+        print "0\n";
+    }
+
+    $seendirs->{$dir} = 1;
+
+    # FUTURE: This would more accurately emulate CVS by sending
+    #   another copy of sticky after processing the files in that
+    #   directory.  Or intermediate: perhaps send all sticky's for
+    #   $seendirs after after processing all files.
+}
+
 # update \n
 #     Response expected: yes. Actually do a cvs update command. This uses any
 #     previous Argument, Directory, Entry, or Modified requests, if they have
@@ -1049,29 +1202,19 @@ sub req_update
 
     #$log->debug("update state : " . Dumper($state));
 
-    my $last_dirname = "///";
+    my($repoDir);
+    $repoDir=$state->{CVSROOT} . "/$state->{module}/$state->{prependdir}";
+
+    my %seendirs = ();
 
     # foreach file specified on the command line ...
-    foreach my $filename ( @{$state->{args}} )
+    foreach my $argsFilename ( @{$state->{args}} )
     {
-        $filename = filecleanup($filename);
+        my $filename;
+        $filename = filecleanup($argsFilename);
 
         $log->debug("Processing file $filename");
 
-        unless ( $state->{globaloptions}{-Q} || $state->{globaloptions}{-q} )
-        {
-            my $cur_dirname = dirname($filename);
-            if ( $cur_dirname ne $last_dirname )
-            {
-                $last_dirname = $cur_dirname;
-                if ( $cur_dirname eq "" )
-                {
-                    $cur_dirname = ".";
-                }
-                print "E cvs update: Updating $cur_dirname\n";
-            }
-        }
-
         # if we have a -C we should pretend we never saw modified stuff
         if ( exists ( $state->{opt}{C} ) )
         {
@@ -1080,13 +1223,11 @@ sub req_update
             $state->{entries}{$filename}{unchanged} = 1;
         }
 
-        my $meta;
-        if ( defined($state->{opt}{r}) and $state->{opt}{r} =~ /^(1\.\d+)$/ )
-        {
-            $meta = $updater->getmeta($filename, $1);
-        } else {
-            $meta = $updater->getmeta($filename);
-        }
+        my $stickyInfo = resolveStickyInfo($filename,
+                                           $state->{opt}{r},
+                                           $state->{opt}{D},
+                                           exists($state->{opt}{A}));
+        my $meta = $updater->getmeta($filename, $stickyInfo);
 
         # If -p was given, "print" the contents of the requested revision.
         if ( exists ( $state->{opt}{p} ) ) {
@@ -1099,6 +1240,17 @@ sub req_update
             next;
         }
 
+        # Directories:
+        prepDirForOutput(
+                dirname($argsFilename),
+                $repoDir,
+                ".",
+                \%seendirs,
+                "update",
+                $state->{dirArgs} );
+
+        my $wrev = revparse($filename);
+
        if ( ! defined $meta )
        {
            $meta = {
@@ -1106,16 +1258,23 @@ sub req_update
                revision => '0',
                filehash => 'added'
            };
+           if($wrev ne "0")
+           {
+               $meta->{filehash}='deleted';
+           }
        }
 
         my $oldmeta = $meta;
 
-        my $wrev = revparse($filename);
-
         # If the working copy is an old revision, lets get that version too for comparison.
-        if ( defined($wrev) and $wrev ne $meta->{revision} )
+        my $oldWrev=$wrev;
+        if(defined($oldWrev))
         {
-            $oldmeta = $updater->getmeta($filename, $wrev);
+            $oldWrev=~s/^-//;
+            if($oldWrev ne $meta->{revision})
+            {
+                $oldmeta = $updater->getmeta($filename, $oldWrev);
+            }
         }
 
         #$log->debug("Target revision is $meta->{revision}, current working revision is $wrev");
@@ -1133,6 +1292,7 @@ sub req_update
         if ( defined ( $wrev )
              and defined($meta->{revision})
              and $wrev eq $meta->{revision}
+             and $wrev ne "0"
              and defined($state->{entries}{$filename}{modified_hash})
              and not exists ( $state->{opt}{C} ) )
         {
@@ -1143,7 +1303,7 @@ sub req_update
             next;
         }
 
-        if ( $meta->{filehash} eq "deleted" )
+        if ( $meta->{filehash} eq "deleted" && $wrev ne "0" )
         {
             # TODO: If it has been modified in the sandbox, error out
             #   with the appropriate message, rather than deleting a modified
@@ -1205,10 +1365,6 @@ sub req_update
                    $log->debug("Updating existing file 'Update-existing $dirpart'");
                } else {
                    # instruct client we're sending a file to put in this path as a new file
-                   print "Clear-static-directory $dirpart\n";
-                   print $state->{CVSROOT} . "/$state->{module}/$dirpart\n";
-                   print "Clear-sticky $dirpart\n";
-                   print $state->{CVSROOT} . "/$state->{module}/$dirpart\n";
 
                    $log->debug("Creating new file 'Created $dirpart'");
                    print "Created $dirpart\n";
@@ -1217,8 +1373,10 @@ sub req_update
 
                # this is an "entries" line
                my $kopts = kopts_from_path($filename,"sha1",$meta->{filehash});
-               $log->debug("/$filepart/$meta->{revision}//$kopts/");
-               print "/$filepart/$meta->{revision}//$kopts/\n";
+                my $entriesLine = "/$filepart/$meta->{revision}//$kopts/";
+                $entriesLine .= getStickyTagOrDate($stickyInfo);
+               $log->debug($entriesLine);
+               print "$entriesLine\n";
 
                # permissions
                $log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");
@@ -1266,7 +1424,9 @@ sub req_update
                     my $kopts = kopts_from_path("$dirpart/$filepart",
                                                 "file",$mergedFile);
                     $log->debug("/$filepart/$meta->{revision}//$kopts/");
-                    print "/$filepart/$meta->{revision}//$kopts/\n";
+                    my $entriesLine="/$filepart/$meta->{revision}//$kopts/";
+                    $entriesLine .= getStickyTagOrDate($stickyInfo);
+                    print "$entriesLine\n";
                 }
             }
             elsif ( $return == 1 )
@@ -1282,7 +1442,9 @@ sub req_update
                     print $state->{CVSROOT} . "/$state->{module}/$filename\n";
                     my $kopts = kopts_from_path("$dirpart/$filepart",
                                                 "file",$mergedFile);
-                    print "/$filepart/$meta->{revision}/+/$kopts/\n";
+                    my $entriesLine = "/$filepart/$meta->{revision}/+/$kopts/";
+                    $entriesLine .= getStickyTagOrDate($stickyInfo);
+                    print "$entriesLine\n";
                 }
             }
             else
@@ -1310,6 +1472,43 @@ sub req_update
 
     }
 
+    # prepDirForOutput() any other existing directories unless they already
+    # have the right sticky tag:
+    unless ( $state->{globaloptions}{n} )
+    {
+        my $dir;
+        foreach $dir (keys(%{$state->{dirMap}}))
+        {
+            if( ! $seendirs{$dir} &&
+                exists($state->{dirArgs}{$dir}) )
+            {
+                my($oldTag);
+                $oldTag=$state->{dirMap}{$dir}{tagspec};
+
+                unless( ( exists($state->{opt}{A}) &&
+                          defined($oldTag) ) ||
+                          ( defined($state->{opt}{r}) &&
+                            ( !defined($oldTag) ||
+                              $state->{opt}{r} ne $oldTag ) ) )
+                        # TODO?: OR sticky dir is different...
+                {
+                    next;
+                }
+
+                prepDirForOutput(
+                        $dir,
+                        $repoDir,
+                        ".",
+                        \%seendirs,
+                        'update',
+                        $state->{dirArgs} );
+            }
+
+            # TODO?: Consider sending a final duplicate Sticky response
+            #   to more closely mimic real CVS.
+        }
+    }
+
     print "ok\n";
 }
 
@@ -1342,23 +1541,11 @@ sub req_ci
     my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
     $updater->update();
 
-    # Remember where the head was at the beginning.
-    my $parenthash = `git show-ref -s refs/heads/$state->{module}`;
-    chomp $parenthash;
-    if ($parenthash !~ /^[0-9a-f]{40}$/) {
-           print "error 1 pserver cannot find the current HEAD of module";
-           cleanupWorkTree();
-           exit;
-    }
-
-    setupWorkTree($parenthash);
-
-    $log->info("Lockless commit start, basing commit on '$work->{workDir}', index file is '$work->{index}'");
-
-    $log->info("Created index '$work->{index}' for head $state->{module} - exit status $?");
-
     my @committedfiles = ();
     my %oldmeta;
+    my $stickyInfo;
+    my $branchRef;
+    my $parenthash;
 
     # foreach file specified on the command line ...
     foreach my $filename ( @{$state->{args}} )
@@ -1368,7 +1555,67 @@ sub req_ci
 
         next unless ( exists $state->{entries}{$filename}{modified_filename} or not $state->{entries}{$filename}{unchanged} );
 
-        my $meta = $updater->getmeta($filename);
+        #####
+        # Figure out which branch and parenthash we are committing
+        # to, and setup worktree:
+
+        # should always come from entries:
+        my $fileStickyInfo = resolveStickyInfo($filename);
+        if( !defined($branchRef) )
+        {
+            $stickyInfo = $fileStickyInfo;
+            if( defined($stickyInfo) &&
+                ( defined($stickyInfo->{date}) ||
+                  !defined($stickyInfo->{tag}) ) )
+            {
+                print "error 1 cannot commit with sticky date for file `$filename'\n";
+                cleanupWorkTree();
+                exit;
+            }
+
+            $branchRef = "refs/heads/$state->{module}";
+            if ( defined($stickyInfo) && defined($stickyInfo->{tag}) )
+            {
+                $branchRef = "refs/heads/$stickyInfo->{tag}";
+            }
+
+            $parenthash = `git show-ref -s $branchRef`;
+            chomp $parenthash;
+            if ($parenthash !~ /^[0-9a-f]{40}$/)
+            {
+                if ( defined($stickyInfo) && defined($stickyInfo->{tag}) )
+                {
+                    print "error 1 sticky tag `$stickyInfo->{tag}' for file `$filename' is not a branch\n";
+                }
+                else
+                {
+                    print "error 1 pserver cannot find the current HEAD of module";
+                }
+                cleanupWorkTree();
+                exit;
+            }
+
+            setupWorkTree($parenthash);
+
+            $log->info("Lockless commit start, basing commit on '$work->{workDir}', index file is '$work->{index}'");
+
+            $log->info("Created index '$work->{index}' for head $state->{module} - exit status $?");
+        }
+        elsif( !refHashEqual($stickyInfo,$fileStickyInfo) )
+        {
+            #TODO: We could split the cvs commit into multiple
+            #  git commits by distinct stickyTag values, but that
+            #  is lowish priority.
+            print "error 1 Committing different files to different"
+                  . " branches is not currently supported\n";
+            cleanupWorkTree();
+            exit;
+        }
+
+        #####
+        # Process this file:
+
+        my $meta = $updater->getmeta($filename,$stickyInfo);
        $oldmeta{$filename} = $meta;
 
         my $wrev = revparse($filename);
@@ -1470,7 +1717,7 @@ sub req_ci
     }
 
        ### Emulate git-receive-pack by running hooks/update
-       my @hook = ( $ENV{GIT_DIR}.'hooks/update', "refs/heads/$state->{module}",
+       my @hook = ( $ENV{GIT_DIR}.'hooks/update', $branchRef,
                        $parenthash, $commithash );
        if( -x $hook[0] ) {
                unless( system( @hook ) == 0 )
@@ -1484,7 +1731,7 @@ sub req_ci
 
        ### Update the ref
        if (system(qw(git update-ref -m), "cvsserver ci",
-                       "refs/heads/$state->{module}", $commithash, $parenthash)) {
+                       $branchRef, $commithash, $parenthash)) {
                $log->warn("update-ref for $state->{module} failed.");
                print "error 1 Cannot commit -- update first\n";
                cleanupWorkTree();
@@ -1498,7 +1745,7 @@ sub req_ci
 
                local $SIG{PIPE} = sub { die 'pipe broke' };
 
-               print $pipe "$parenthash $commithash refs/heads/$state->{module}\n";
+               print $pipe "$parenthash $commithash $branchRef\n";
 
                close $pipe || die "bad pipe: $! $?";
        }
@@ -1508,7 +1755,7 @@ sub req_ci
        ### Then hooks/post-update
        $hook = $ENV{GIT_DIR}.'hooks/post-update';
        if (-x $hook) {
-               system($hook, "refs/heads/$state->{module}");
+               system($hook, $branchRef);
        }
 
     # foreach file specified on the command line ...
@@ -1516,7 +1763,7 @@ sub req_ci
     {
         $filename = filecleanup($filename);
 
-        my $meta = $updater->getmeta($filename);
+        my $meta = $updater->getmeta($filename,$stickyInfo);
        unless (defined $meta->{revision}) {
          $meta->{revision} = "1.1";
        }
@@ -1540,7 +1787,8 @@ sub req_ci
             print "Checked-in $dirpart\n";
             print "$filename\n";
             my $kopts = kopts_from_path($filename,"sha1",$meta->{filehash});
-            print "/$filepart/$meta->{revision}//$kopts/\n";
+            print "/$filepart/$meta->{revision}//$kopts/" .
+                  getStickyTagOrDate($stickyInfo) . "\n";
         }
     }
 
@@ -1577,16 +1825,19 @@ sub req_status
            next;
         }
 
-        my $meta = $updater->getmeta($filename);
-        my $oldmeta = $meta;
-
         my $wrev = revparse($filename);
 
+        my $stickyInfo = resolveStickyInfo($filename);
+        my $meta = $updater->getmeta($filename,$stickyInfo);
+        my $oldmeta = $meta;
+
         # If the working copy is an old revision, lets get that
         # version too for comparison.
         if ( defined($wrev) and $wrev ne $meta->{revision} )
         {
-            $oldmeta = $updater->getmeta($filename, $wrev);
+            my($rmRev)=$wrev;
+            $rmRev=~s/^-//;
+            $oldmeta = $updater->getmeta($filename, $rmRev);
         }
 
         # TODO : All possible statuses aren't yet implemented
@@ -1629,6 +1880,7 @@ sub req_status
         # same revision but there are local changes
         if ( defined ( $wrev ) and defined($meta->{revision}) and
              $wrev eq $meta->{revision} and
+             $wrev ne "0" and
              $state->{entries}{$filename}{modified_filename} )
         {
             $status ||= "Locally Modified";
@@ -1644,7 +1896,8 @@ sub req_status
         }
 
         if ( defined ( $state->{entries}{$filename}{revision} ) and
-             not defined ( $meta->{revision} ) )
+             ( !defined($meta->{revision}) ||
+               $meta->{revision} eq "0" ) )
         {
             $status ||= "Locally Added";
         }
@@ -1740,98 +1993,133 @@ sub req_diff
     # be providing status on ...
     argsfromdir($updater);
 
+    my($foundDiff);
+
     # foreach file specified on the command line ...
-    foreach my $filename ( @{$state->{args}} )
+    foreach my $argFilename ( @{$state->{args}} )
     {
-        $filename = filecleanup($filename);
+        my($filename) = filecleanup($argFilename);
 
         my ( $fh, $file1, $file2, $meta1, $meta2, $filediff );
 
         my $wrev = revparse($filename);
 
-        # We need _something_ to diff against
-        next unless ( defined ( $wrev ) );
+        # Priority for revision1:
+        #  1. First -r (missing file: check -N)
+        #  2. wrev from client's Entry line
+        #      - missing line/file: check -N
+        #      - "0": added file not committed (empty contents for rev1)
+        #      - Prefixed with dash (to be removed): check -N
 
-        # if we have a -r switch, use it
         if ( defined ( $revision1 ) )
         {
-            ( undef, $file1 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
             $meta1 = $updater->getmeta($filename, $revision1);
-            unless ( defined ( $meta1 ) and $meta1->{filehash} ne "deleted" )
+        }
+        elsif( defined($wrev) && $wrev ne "0" )
+        {
+            my($rmRev)=$wrev;
+            $rmRev=~s/^-//;
+            $meta1 = $updater->getmeta($filename, $rmRev);
+        }
+        if ( !defined($meta1) ||
+             $meta1->{filehash} eq "deleted" )
+        {
+            if( !exists($state->{opt}{N}) )
             {
-                print "E File $filename at revision $revision1 doesn't exist\n";
+                if(!defined($revision1))
+                {
+                    print "E File $filename at revision $revision1 doesn't exist\n";
+                }
                 next;
             }
-            transmitfile($meta1->{filehash}, { targetfile => $file1 });
-        }
-        # otherwise we just use the working copy revision
-        else
-        {
-            ( undef, $file1 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
-            $meta1 = $updater->getmeta($filename, $wrev);
-            transmitfile($meta1->{filehash}, { targetfile => $file1 });
+            elsif( !defined($meta1) )
+            {
+                $meta1 = {
+                    name => $filename,
+                    revision => '0',
+                    filehash => 'deleted'
+                };
+            }
         }
 
+        # Priority for revision2:
+        #  1. Second -r (missing file: check -N)
+        #  2. Modified file contents from client
+        #  3. wrev from client's Entry line
+        #      - missing line/file: check -N
+        #      - Prefixed with dash (to be removed): check -N
+
         # if we have a second -r switch, use it too
         if ( defined ( $revision2 ) )
         {
-            ( undef, $file2 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
             $meta2 = $updater->getmeta($filename, $revision2);
-
-            unless ( defined ( $meta2 ) and $meta2->{filehash} ne "deleted" )
-            {
-                print "E File $filename at revision $revision2 doesn't exist\n";
-                next;
-            }
-
-            transmitfile($meta2->{filehash}, { targetfile => $file2 });
         }
-        # otherwise we just use the working copy
-        else
+        elsif(defined($state->{entries}{$filename}{modified_filename}))
         {
             $file2 = $state->{entries}{$filename}{modified_filename};
+           $meta2 = {
+                name => $filename,
+               revision => '0',
+               filehash => 'modified'
+            };
         }
-
-        # if we have been given -r, and we don't have a $file2 yet, lets
-        # get one
-        if ( defined ( $revision1 ) and not defined ( $file2 ) )
+        elsif( defined($wrev) && ($wrev!~/^-/) )
         {
-            ( undef, $file2 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
+            if(!defined($revision1))  # no revision and no modifications:
+            {
+                next;
+            }
             $meta2 = $updater->getmeta($filename, $wrev);
-            transmitfile($meta2->{filehash}, { targetfile => $file2 });
+        }
+        if(!defined($file2))
+        {
+            if ( !defined($meta2) ||
+                 $meta2->{filehash} eq "deleted" )
+            {
+                if( !exists($state->{opt}{N}) )
+                {
+                    if(!defined($revision2))
+                    {
+                        print "E File $filename at revision $revision2 doesn't exist\n";
+                    }
+                    next;
+                }
+                elsif( !defined($meta2) )
+                {
+                   $meta2 = {
+                        name => $filename,
+                       revision => '0',
+                       filehash => 'deleted'
+                    };
+                }
+            }
         }
 
-        # We need to have retrieved something useful
-        next unless ( defined ( $meta1 ) );
-
-        # Files to date if the working copy and repo copy have the same
-        # revision, and the working copy is unmodified
-        if ( not defined ( $meta2 ) and $wrev eq $meta1->{revision} and
-             ( ( $state->{entries}{$filename}{unchanged} and
-                 ( not defined ( $state->{entries}{$filename}{conflict} ) or
-                   $state->{entries}{$filename}{conflict} !~ /^\+=/ ) ) or
-               ( defined($state->{entries}{$filename}{modified_hash}) and
-                 $state->{entries}{$filename}{modified_hash} eq
-                        $meta1->{filehash} ) ) )
+        if( $meta1->{filehash} eq $meta2->{filehash} )
         {
+            $log->info("unchanged $filename");
             next;
         }
 
-        # Apparently we only show diffs for locally modified files
-        unless ( defined($meta2) or
-                 defined ( $state->{entries}{$filename}{modified_filename} ) )
+        # Retrieve revision contents:
+        ( undef, $file1 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
+        transmitfile($meta1->{filehash}, { targetfile => $file1 });
+
+        if(!defined($file2))
         {
-            next;
+            ( undef, $file2 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
+            transmitfile($meta2->{filehash}, { targetfile => $file2 });
         }
 
-        print "M Index: $filename\n";
+        # Generate the actual diff:
+        print "M Index: $argFilename\n";
         print "M =======" . ( "=" x 60 ) . "\n";
         print "M RCS file: $state->{CVSROOT}/$state->{module}/$filename,v\n";
-        if ( defined ( $meta1 ) )
+        if ( defined ( $meta1 ) && $meta1->{revision} ne "0" )
         {
             print "M retrieving revision $meta1->{revision}\n"
         }
-        if ( defined ( $meta2 ) )
+        if ( defined ( $meta2 ) && $meta2->{revision} ne "0" )
         {
             print "M retrieving revision $meta2->{revision}\n"
         }
@@ -1852,33 +2140,73 @@ sub req_diff
                 }
             }
         }
-        print "$filename\n";
+        print "$argFilename\n";
 
         $log->info("Diffing $filename -r $meta1->{revision} -r " .
                    ( $meta2->{revision} or "workingcopy" ));
 
-        ( $fh, $filediff ) = tempfile ( DIR => $TEMP_DIR );
-
-        if ( exists $state->{opt}{u} )
+        # TODO: Use --label instead of -L because -L is no longer
+        #  documented and may go away someday.  Not sure if there there are
+        #  versions that only support -L, which would make this change risky?
+        #  http://osdir.com/ml/bug-gnu-utils-gnu/2010-12/msg00060.html
+        #    ("man diff" should actually document the best migration strategy,
+        #  [current behavior, future changes, old compatibility issues
+        #  or lack thereof, etc], not just stop mentioning the option...)
+        # TODO: Real CVS seems to include a date in the label, before
+        #  the revision part, without the keyword "revision".  The following
+        #  has minimal changes compared to original versions of
+        #  git-cvsserver.perl.  (Mostly tab vs space after filename.)
+
+        my (@diffCmd) = ( 'diff' );
+        if ( exists($state->{opt}{N}) )
         {
-            system("diff -u -L '$filename revision $meta1->{revision}'" .
-                        " -L '$filename " .
-                        ( defined($meta2->{revision}) ?
-                                "revision $meta2->{revision}" :
-                                "working copy" ) .
-                        "' $file1 $file2 > $filediff" );
-        } else {
-            system("diff $file1 $file2 > $filediff");
+            push @diffCmd,"-N";
         }
+        if ( exists $state->{opt}{u} )
+        {
+            push @diffCmd,("-u","-L");
+            if( $meta1->{filehash} eq "deleted" )
+            {
+                push @diffCmd,"/dev/null";
+            } else {
+                push @diffCmd,("$argFilename\trevision $meta1->{revision}");
+            }
 
-        while ( <$fh> )
+            if( defined($meta2->{filehash}) )
+            {
+                if( $meta2->{filehash} eq "deleted" )
+                {
+                    push @diffCmd,("-L","/dev/null");
+                } else {
+                    push @diffCmd,("-L",
+                                   "$argFilename\trevision $meta2->{revision}");
+                }
+            } else {
+                push @diffCmd,("-L","$argFilename\tworking copy");
+            }
+        }
+        push @diffCmd,($file1,$file2);
+        if(!open(DIFF,"-|",@diffCmd))
         {
-            print "M $_";
+            $log->warn("Unable to run diff: $!");
         }
-        close $fh;
+        my($diffLine);
+        while(defined($diffLine=<DIFF>))
+        {
+            print "M $diffLine";
+            $foundDiff=1;
+        }
+        close(DIFF);
     }
 
-    print "ok\n";
+    if($foundDiff)
+    {
+        print "error  \n";
+    }
+    else
+    {
+        print "ok\n";
+    }
 }
 
 sub req_log
@@ -2098,7 +2426,7 @@ sub argsplit
         $opt = { A => 0, N => 0, P => 0, R => 0, c => 0, f => 0, l => 0, n => 0, p => 0, s => 0, r => 1, D => 1, d => 1, k => 1, j => 1, } if ( $type eq "co" );
         $opt = { v => 0, l => 0, R => 0 } if ( $type eq "status" );
         $opt = { A => 0, P => 0, C => 0, d => 0, f => 0, l => 0, R => 0, p => 0, k => 1, r => 1, D => 1, j => 1, I => 1, W => 1 } if ( $type eq "update" );
-        $opt = { l => 0, R => 0, k => 1, D => 1, D => 1, r => 2 } if ( $type eq "diff" );
+        $opt = { l => 0, R => 0, k => 1, D => 1, D => 1, r => 2, N => 0 } if ( $type eq "diff" );
         $opt = { c => 0, R => 0, l => 0, f => 0, F => 1, m => 1, r => 1 } if ( $type eq "ci" );
         $opt = { k => 1, m => 1 } if ( $type eq "add" );
         $opt = { f => 0, l => 0, R => 0 } if ( $type eq "remove" );
@@ -2164,62 +2492,335 @@ sub argsplit
     }
 }
 
-# This method uses $state->{directory} to populate $state->{args} with a list of filenames
-sub argsfromdir
+# Used by argsfromdir
+sub expandArg
 {
-    my $updater = shift;
-
-    $state->{args} = [] if ( scalar(@{$state->{args}}) == 1 and $state->{args}[0] eq "." );
+    my ($updater,$outNameMap,$outDirMap,$path,$isDir) = @_;
 
-    return if ( scalar ( @{$state->{args}} ) > 1 );
+    my $fullPath = filecleanup($path);
 
-    my @gethead = @{$updater->gethead};
+      # Is it a directory?
+    if( defined($state->{dirMap}{$fullPath}) ||
+        defined($state->{dirMap}{"$fullPath/"}) )
+    {
+          # It is a directory in the user's sandbox.
+        $isDir=1;
 
-    # push added files
-    foreach my $file (keys %{$state->{entries}}) {
-       if ( exists $state->{entries}{$file}{revision} &&
-               $state->{entries}{$file}{revision} eq '0' )
-       {
-           push @gethead, { name => $file, filehash => 'added' };
-       }
+        if(defined($state->{entries}{$fullPath}))
+        {
+            $log->fatal("Inconsistent file/dir type");
+            die "Inconsistent file/dir type";
+        }
     }
-
-    if ( scalar(@{$state->{args}}) == 1 )
+    elsif(defined($state->{entries}{$fullPath}))
     {
-        my $arg = $state->{args}[0];
-        $arg .= $state->{prependdir} if ( defined ( $state->{prependdir} ) );
-
-        $log->info("Only one arg specified, checking for directory expansion on '$arg'");
-
-        foreach my $file ( @gethead )
+          # It is a file in the user's sandbox.
+        $isDir=0;
+    }
+    my($revDirMap,$otherRevDirMap);
+    if(!defined($isDir) || $isDir)
+    {
+          # Resolve version tree for sticky tag:
+          # (for now we only want list of files for the version, not
+          # particular versions of those files: assume it is a directory
+          # for the moment; ignore Entry's stick tag)
+
+          # Order of precedence of sticky tags:
+          #    -A       [head]
+          #    -r /tag/
+          #    [file entry sticky tag, but that is only relevant to files]
+          #    [the tag specified in dir req_Sticky]
+          #    [the tag specified in a parent dir req_Sticky]
+          #    [head]
+          # Also, -r may appear twice (for diff).
+          #
+          # FUTURE: When/if -j (merges) are supported, we also
+          #  need to add relevant files from one or two
+          #  versions specified with -j.
+
+        if(exists($state->{opt}{A}))
         {
-            next if ( $file->{filehash} eq "deleted" and not defined ( $state->{entries}{$file->{name}} ) );
-            next unless ( $file->{name} =~ /^$arg\// or $file->{name} eq $arg  );
-            push @{$state->{args}}, $file->{name};
+            $revDirMap=$updater->getRevisionDirMap();
         }
-
-        shift @{$state->{args}} if ( scalar(@{$state->{args}}) > 1 );
-    } else {
-        $log->info("Only one arg specified, populating file list automatically");
-
-        $state->{args} = [];
-
-        foreach my $file ( @gethead )
+        elsif( defined($state->{opt}{r}) and
+               ref $state->{opt}{r} eq "ARRAY" )
         {
-            next if ( $file->{filehash} eq "deleted" and not defined ( $state->{entries}{$file->{name}} ) );
-            next unless ( $file->{name} =~ s/^$state->{prependdir}// );
-            push @{$state->{args}}, $file->{name};
+            $revDirMap=$updater->getRevisionDirMap($state->{opt}{r}[0]);
+            $otherRevDirMap=$updater->getRevisionDirMap($state->{opt}{r}[1]);
         }
-    }
-}
-
-# This method cleans up the $state variable after a command that uses arguments has run
+        elsif(defined($state->{opt}{r}))
+        {
+            $revDirMap=$updater->getRevisionDirMap($state->{opt}{r});
+        }
+        else
+        {
+            my($sticky)=getDirStickyInfo($fullPath);
+            $revDirMap=$updater->getRevisionDirMap($sticky->{tag});
+        }
+
+          # Is it a directory?
+        if( defined($revDirMap->{$fullPath}) ||
+            defined($otherRevDirMap->{$fullPath}) )
+        {
+            $isDir=1;
+        }
+    }
+
+      # What to do with it?
+    if(!$isDir)
+    {
+        $outNameMap->{$fullPath}=1;
+    }
+    else
+    {
+        $outDirMap->{$fullPath}=1;
+
+        if(defined($revDirMap->{$fullPath}))
+        {
+            addDirMapFiles($updater,$outNameMap,$outDirMap,
+                           $revDirMap->{$fullPath});
+        }
+        if( defined($otherRevDirMap) &&
+            defined($otherRevDirMap->{$fullPath}) )
+        {
+            addDirMapFiles($updater,$outNameMap,$outDirMap,
+                           $otherRevDirMap->{$fullPath});
+        }
+    }
+}
+
+# Used by argsfromdir
+# Add entries from dirMap to outNameMap.  Also recurse into entries
+# that are subdirectories.
+sub addDirMapFiles
+{
+    my($updater,$outNameMap,$outDirMap,$dirMap)=@_;
+
+    my($fullName);
+    foreach $fullName (keys(%$dirMap))
+    {
+        my $cleanName=$fullName;
+        if(defined($state->{prependdir}))
+        {
+            if(!($cleanName=~s/^\Q$state->{prependdir}\E//))
+            {
+                $log->fatal("internal error stripping prependdir");
+                die "internal error stripping prependdir";
+            }
+        }
+
+        if($dirMap->{$fullName} eq "F")
+        {
+            $outNameMap->{$cleanName}=1;
+        }
+        elsif($dirMap->{$fullName} eq "D")
+        {
+            if(!$state->{opt}{l})
+            {
+                expandArg($updater,$outNameMap,$outDirMap,$cleanName,1);
+            }
+        }
+        else
+        {
+            $log->fatal("internal error in addDirMapFiles");
+            die "internal error in addDirMapFiles";
+        }
+    }
+}
+
+# This method replaces $state->{args} with a directory-expanded
+# list of all relevant filenames (recursively unless -d), based
+# on $state->{entries}, and the "current" list of files in
+# each directory.  "Current" files as determined by
+# either the requested (-r/-A) or "req_Sticky" version of
+# that directory.
+#    Both the input args and the new output args are relative
+# to the cvs-client's CWD, although some of the internal
+# computations are relative to the top of the project.
+sub argsfromdir
+{
+    my $updater = shift;
+
+    # Notes about requirements for specific callers:
+    #   update # "standard" case (entries; a single -r/-A/default; -l)
+    #          # Special case: -d for create missing directories.
+    #   diff # 0 or 1 -r's: "standard" case.
+    #        # 2 -r's: We could ignore entries (just use the two -r's),
+    #        # but it doesn't really matter.
+    #   annotate # "standard" case
+    #   log # Punting: log -r has a more complex non-"standard"
+    #       # meaning, and we don't currently try to support log'ing
+    #       # branches at all (need a lot of work to
+    #       # support CVS-consistent branch relative version
+    #       # numbering).
+#HERE: But we still want to expand directories.  Maybe we should
+#  essentially force "-A".
+    #   status # "standard", except that -r/-A/default are not possible.
+    #          # Mostly only used to expand entries only)
+    #
+    # Don't use argsfromdir at all:
+    #   add # Explicit arguments required.  Directory args imply add
+    #       # the directory itself, not the files in it.
+    #   co  # Obtain list directly.
+    #   remove # HERE: TEST: MAYBE client does the recursion for us,
+    #          # since it only makes sense to remove stuff already in
+    #          # the sandobx?
+    #   ci # HERE: Similar to remove...
+    #      # Don't try to implement the confusing/weird
+    #      # ci -r bug er.."feature".
+
+    if(scalar(@{$state->{args}})==0)
+    {
+        $state->{args} = [ "." ];
+    }
+    my %allArgs;
+    my %allDirs;
+    for my $file (@{$state->{args}})
+    {
+        expandArg($updater,\%allArgs,\%allDirs,$file);
+    }
+
+    # Include any entries from sandbox.  Generally client won't
+    # send entries that shouldn't be used.
+    foreach my $file (keys %{$state->{entries}})
+    {
+        $allArgs{remove_prependdir($file)} = 1;
+    }
+
+    $state->{dirArgs} = \%allDirs;
+    $state->{args} = [
+        sort {
+                # Sort priority: by directory depth, then actual file name:
+            my @piecesA=split('/',$a);
+            my @piecesB=split('/',$b);
+
+            my $count=scalar(@piecesA);
+            my $tmp=scalar(@piecesB);
+            return $count<=>$tmp if($count!=$tmp);
+
+            for($tmp=0;$tmp<$count;$tmp++)
+            {
+                if($piecesA[$tmp] ne $piecesB[$tmp])
+                {
+                    return $piecesA[$tmp] cmp $piecesB[$tmp]
+                }
+            }
+            return 0;
+        } keys(%allArgs) ];
+}
+
+## look up directory sticky tag, of either fullPath or a parent:
+sub getDirStickyInfo
+{
+    my($fullPath)=@_;
+
+    $fullPath=~s%/+$%%;
+    while($fullPath ne "" && !defined($state->{dirMap}{"$fullPath/"}))
+    {
+        $fullPath=~s%/?[^/]*$%%;
+    }
+
+    if( !defined($state->{dirMap}{"$fullPath/"}) &&
+        ( $fullPath eq "" ||
+          $fullPath eq "." ) )
+    {
+        return $state->{dirMap}{""}{stickyInfo};
+    }
+    else
+    {
+        return $state->{dirMap}{"$fullPath/"}{stickyInfo};
+    }
+}
+
+# Resolve precedence of various ways of specifying which version of
+# a file you want.  Returns undef (for default head), or a ref to a hash
+# that contains "tag" and/or "date" keys.
+sub resolveStickyInfo
+{
+    my($filename,$stickyTag,$stickyDate,$reset) = @_;
+
+    # Order of precedence of sticky tags:
+    #    -A       [head]
+    #    -r /tag/
+    #    [file entry sticky tag]
+    #    [the tag specified in dir req_Sticky]
+    #    [the tag specified in a parent dir req_Sticky]
+    #    [head]
+
+    my $result;
+    if($reset)
+    {
+        # $result=undef;
+    }
+    elsif( defined($stickyTag) && $stickyTag ne "" )
+           # || ( defined($stickyDate) && $stickyDate ne "" )   # TODO
+    {
+        $result={ 'tag' => (defined($stickyTag)?$stickyTag:undef) };
+
+        # TODO: Convert -D value into the form 2011.04.10.04.46.57,
+        #   similar to an entry line's sticky date, without the D prefix.
+        #   It sometimes (always?) arrives as something more like
+        #   '10 Apr 2011 04:46:57 -0000'...
+        # $result={ 'date' => (defined($stickyDate)?$stickyDate:undef) };
+    }
+    elsif( defined($state->{entries}{$filename}) &&
+           defined($state->{entries}{$filename}{tag_or_date}) &&
+           $state->{entries}{$filename}{tag_or_date} ne "" )
+    {
+        my($tagOrDate)=$state->{entries}{$filename}{tag_or_date};
+        if($tagOrDate=~/^T([^ ]+)\s*$/)
+        {
+            $result = { 'tag' => $1 };
+        }
+        elsif($tagOrDate=~/^D([0-9.]+)\s*$/)
+        {
+            $result= { 'date' => $1 };
+        }
+        else
+        {
+            die "Unknown tag_or_date format\n";
+        }
+    }
+    else
+    {
+        $result=getDirStickyInfo($filename);
+    }
+
+    return $result;
+}
+
+# Convert a stickyInfo (ref to a hash) as returned by resolveStickyInfo into
+# a form appropriate for the sticky tag field of an Entries
+# line (field index 5, 0-based).
+sub getStickyTagOrDate
+{
+    my($stickyInfo)=@_;
+
+    my $result;
+    if(defined($stickyInfo) && defined($stickyInfo->{tag}))
+    {
+        $result="T$stickyInfo->{tag}";
+    }
+    # TODO: When/if we actually pick versions by {date} properly,
+    #   also handle it here:
+    #   "D$stickyInfo->{date}" (example: "D2011.04.13.20.37.07").
+    else
+    {
+        $result="";
+    }
+
+    return $result;
+}
+
+# This method cleans up the $state variable after a command that uses arguments has run
 sub statecleanup
 {
     $state->{files} = [];
+    $state->{dirArgs} = {};
     $state->{args} = [];
     $state->{arguments} = [];
     $state->{entries} = {};
+    $state->{dirMap} = {};
 }
 
 # Return working directory CVS revision "1.X" out
@@ -2309,6 +2910,9 @@ sub filenamesplit
     return ( $filepart, $dirpart );
 }
 
+# Cleanup various junk in filename (try to canonicalize it), and
+# add prependdir to accomodate running CVS client from a
+# subdirectory (so the output is relative to top directory of the project).
 sub filecleanup
 {
     my $filename = shift;
@@ -2320,11 +2924,36 @@ sub filecleanup
         return undef;
     }
 
+    if($filename eq ".")
+    {
+        $filename="";
+    }
     $filename =~ s/^\.\///g;
+    $filename =~ s%/+%/%g;
     $filename = $state->{prependdir} . $filename;
+    $filename =~ s%/$%%;
     return $filename;
 }
 
+# Remove prependdir from the path, so that is is relative to the directory
+# the CVS client was started from, rather than the top of the project.
+# Essentially the inverse of filecleanup().
+sub remove_prependdir
+{
+    my($path) = @_;
+    if(defined($state->{prependdir}) && $state->{prependdir} ne "")
+    {
+        my($pre)=$state->{prependdir};
+        $pre=~s%/$%%;
+        if(!($path=~s%^\Q$pre\E/?%%))
+        {
+            $log->fatal("internal error missing prependdir");
+            die("internal error missing prependdir");
+        }
+    }
+    return $path;
+}
+
 sub validateGitDir
 {
     if( !defined($state->{CVSROOT}) )
@@ -2738,6 +3367,45 @@ sub descramble
     return $ret;
 }
 
+# Test if the (deep) values of two references to a hash are the same.
+sub refHashEqual
+{
+    my($v1,$v2) = @_;
+
+    my $out;
+    if(!defined($v1))
+    {
+        if(!defined($v2))
+        {
+            $out=1;
+        }
+    }
+    elsif( !defined($v2) ||
+           scalar(keys(%{$v1})) != scalar(keys(%{$v2})) )
+    {
+        # $out=undef;
+    }
+    else
+    {
+        $out=1;
+
+        my $key;
+        foreach $key (keys(%{$v1}))
+        {
+            if( !exists($v2->{$key}) ||
+                defined($v1->{$key}) ne defined($v2->{$key}) ||
+                ( defined($v1->{$key}) &&
+                  $v1->{$key} ne $v2->{$key} ) )
+            {
+               $out=undef;
+               last;
+            }
+        }
+    }
+
+    return $out;
+}
+
 
 package GITCVS::log;
 
@@ -2958,6 +3626,9 @@ sub new
 
     die "Git repo '$self->{git_path}' doesn't exist" unless ( -d $self->{git_path} );
 
+    # Stores full sha1's for various branch/tag names, abbreviations, etc:
+    $self->{commitRefCache} = {};
+
     $self->{dbdriver} = $cfg->{gitcvs}{$state->{method}}{dbdriver} ||
         $cfg->{gitcvs}{dbdriver} || "SQLite";
     $self->{dbname} = $cfg->{gitcvs}{$state->{method}}{dbname} ||
@@ -3140,6 +3811,8 @@ sub update
     my $lastcommit = $self->_get_prop("last_commit");
 
     if (defined $lastcommit && $lastcommit eq $commitsha1) { # up-to-date
+         # invalidate the gethead cache
+         $self->clearCommitRefCaches();
          return 1;
     }
 
@@ -3157,45 +3830,10 @@ sub update
         push @git_log_params, $self->{module};
     }
     # git-rev-list is the backend / plumbing version of git-log
-    open(GITLOG, '-|', 'git', 'rev-list', @git_log_params) or die "Cannot call git-rev-list: $!";
-
-    my @commits;
-
-    my %commit = ();
-
-    while ( <GITLOG> )
-    {
-        chomp;
-        if (m/^commit\s+(.*)$/) {
-            # on ^commit lines put the just seen commit in the stack
-            # and prime things for the next one
-            if (keys %commit) {
-                my %copy = %commit;
-                unshift @commits, \%copy;
-                %commit = ();
-            }
-            my @parents = split(m/\s+/, $1);
-            $commit{hash} = shift @parents;
-            $commit{parents} = \@parents;
-        } elsif (m/^(\w+?):\s+(.*)$/ && !exists($commit{message})) {
-            # on rfc822-like lines seen before we see any message,
-            # lowercase the entry and put it in the hash as key-value
-            $commit{lc($1)} = $2;
-        } else {
-            # message lines - skip initial empty line
-            # and trim whitespace
-            if (!exists($commit{message}) && m/^\s*$/) {
-                # define it to mark the end of headers
-                $commit{message} = '';
-                next;
-            }
-            s/^\s+//; s/\s+$//; # trim ws
-            $commit{message} .= $_ . "\n";
-        }
-    }
-    close GITLOG;
-
-    unshift @commits, \%commit if ( keys %commit );
+    open(my $gitLogPipe, '-|', 'git', 'rev-list', @git_log_params)
+                or die "Cannot call git-rev-list: $!";
+    my @commits=readCommits($gitLogPipe);
+    close $gitLogPipe;
 
     # Now all the commits are in the @commits bucket
     # ordered by time DESC. for each commit that needs processing,
@@ -3294,7 +3932,7 @@ sub update
         }
 
         # convert the date to CVS-happy format
-        $commit->{date} = "$2 $1 $4 $3 $5" if ( $commit->{date} =~ /^\w+\s+(\w+)\s+(\d+)\s+(\d+:\d+:\d+)\s+(\d+)\s+([+-]\d+)$/ );
+        my $cvsDate = convertToCvsDate($commit->{date});
 
         if ( defined ( $lastpicked ) )
         {
@@ -3303,7 +3941,7 @@ sub update
             while ( <FILELIST> )
             {
                chomp;
-                unless ( /^:\d{6}\s+\d{3}(\d)\d{2}\s+[a-zA-Z0-9]{40}\s+([a-zA-Z0-9]{40})\s+(\w)$/o )
+                unless ( /^:\d{6}\s+([0-7]{6})\s+[a-f0-9]{40}\s+([a-f0-9]{40})\s+(\w)$/o )
                 {
                     die("Couldn't process git-diff-tree line : $_");
                 }
@@ -3313,11 +3951,7 @@ sub update
 
                 # $log->debug("File mode=$mode, hash=$hash, change=$change, name=$name");
 
-                my $git_perms = "";
-                $git_perms .= "r" if ( $mode & 4 );
-                $git_perms .= "w" if ( $mode & 2 );
-                $git_perms .= "x" if ( $mode & 1 );
-                $git_perms = "rw" if ( $git_perms eq "" );
+                my $dbMode = convertToDbMode($mode);
 
                 if ( $change eq "D" )
                 {
@@ -3327,11 +3961,11 @@ sub update
                         revision => $head->{$name}{revision} + 1,
                         filehash => "deleted",
                         commithash => $commit->{hash},
-                        modified => $commit->{date},
+                        modified => $cvsDate,
                         author => $commit->{author},
-                        mode => $git_perms,
+                        mode => $dbMode,
                     };
-                    $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
+                    $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $cvsDate, $commit->{author}, $dbMode);
                 }
                 elsif ( $change eq "M" || $change eq "T" )
                 {
@@ -3341,11 +3975,11 @@ sub update
                         revision => $head->{$name}{revision} + 1,
                         filehash => $hash,
                         commithash => $commit->{hash},
-                        modified => $commit->{date},
+                        modified => $cvsDate,
                         author => $commit->{author},
-                        mode => $git_perms,
+                        mode => $dbMode,
                     };
-                    $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
+                    $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $cvsDate, $commit->{author}, $dbMode);
                 }
                 elsif ( $change eq "A" )
                 {
@@ -3355,11 +3989,11 @@ sub update
                         revision => $head->{$name}{revision} ? $head->{$name}{revision}+1 : 1,
                         filehash => $hash,
                         commithash => $commit->{hash},
-                        modified => $commit->{date},
+                        modified => $cvsDate,
                         author => $commit->{author},
-                        mode => $git_perms,
+                        mode => $dbMode,
                     };
-                    $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
+                    $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $cvsDate, $commit->{author}, $dbMode);
                 }
                 else
                 {
@@ -3382,7 +4016,7 @@ sub update
                     die("Couldn't process git-ls-tree line : $_");
                 }
 
-                my ( $git_perms, $git_type, $git_hash, $git_filename ) = ( $1, $2, $3, $4 );
+                my ( $mode, $git_type, $git_hash, $git_filename ) = ( $1, $2, $3, $4 );
 
                 $seen_files->{$git_filename} = 1;
 
@@ -3392,18 +4026,10 @@ sub update
                     $head->{$git_filename}{mode}
                 );
 
-                if ( $git_perms =~ /^\d\d\d(\d)\d\d/o )
-                {
-                    $git_perms = "";
-                    $git_perms .= "r" if ( $1 & 4 );
-                    $git_perms .= "w" if ( $1 & 2 );
-                    $git_perms .= "x" if ( $1 & 1 );
-                } else {
-                    $git_perms = "rw";
-                }
+                my $dbMode = convertToDbMode($mode);
 
                 # unless the file exists with the same hash, we need to update it ...
-                unless ( defined($oldhash) and $oldhash eq $git_hash and defined($oldmode) and $oldmode eq $git_perms )
+                unless ( defined($oldhash) and $oldhash eq $git_hash and defined($oldmode) and $oldmode eq $dbMode )
                 {
                     my $newrevision = ( $oldrevision or 0 ) + 1;
 
@@ -3412,13 +4038,13 @@ sub update
                         revision => $newrevision,
                         filehash => $git_hash,
                         commithash => $commit->{hash},
-                        modified => $commit->{date},
+                        modified => $cvsDate,
                         author => $commit->{author},
-                        mode => $git_perms,
+                        mode => $dbMode,
                     };
 
 
-                    $self->insert_rev($git_filename, $newrevision, $git_hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
+                    $self->insert_rev($git_filename, $newrevision, $git_hash, $commit->{hash}, $cvsDate, $commit->{author}, $dbMode);
                 }
             }
             close FILELIST;
@@ -3431,10 +4057,10 @@ sub update
                     $head->{$file}{revision}++;
                     $head->{$file}{filehash} = "deleted";
                     $head->{$file}{commithash} = $commit->{hash};
-                    $head->{$file}{modified} = $commit->{date};
+                    $head->{$file}{modified} = $cvsDate;
                     $head->{$file}{author} = $commit->{author};
 
-                    $self->insert_rev($file, $head->{$file}{revision}, $head->{$file}{filehash}, $commit->{hash}, $commit->{date}, $commit->{author}, $head->{$file}{mode});
+                    $self->insert_rev($file, $head->{$file}{revision}, $head->{$file}{filehash}, $commit->{hash}, $cvsDate, $commit->{author}, $head->{$file}{mode});
                 }
             }
             # END : "Detect deleted files"
@@ -3465,13 +4091,94 @@ sub update
         );
     }
     # invalidate the gethead cache
-    $self->{gethead_cache} = undef;
+    $self->clearCommitRefCaches();
 
 
     # Ending exclusive lock here
     $self->{dbh}->commit() or die "Failed to commit changes to SQLite";
 }
 
+sub readCommits
+{
+    my $pipeHandle = shift;
+    my @commits;
+
+    my %commit = ();
+
+    while ( <$pipeHandle> )
+    {
+        chomp;
+        if (m/^commit\s+(.*)$/) {
+            # on ^commit lines put the just seen commit in the stack
+            # and prime things for the next one
+            if (keys %commit) {
+                my %copy = %commit;
+                unshift @commits, \%copy;
+                %commit = ();
+            }
+            my @parents = split(m/\s+/, $1);
+            $commit{hash} = shift @parents;
+            $commit{parents} = \@parents;
+        } elsif (m/^(\w+?):\s+(.*)$/ && !exists($commit{message})) {
+            # on rfc822-like lines seen before we see any message,
+            # lowercase the entry and put it in the hash as key-value
+            $commit{lc($1)} = $2;
+        } else {
+            # message lines - skip initial empty line
+            # and trim whitespace
+            if (!exists($commit{message}) && m/^\s*$/) {
+                # define it to mark the end of headers
+                $commit{message} = '';
+                next;
+            }
+            s/^\s+//; s/\s+$//; # trim ws
+            $commit{message} .= $_ . "\n";
+        }
+    }
+
+    unshift @commits, \%commit if ( keys %commit );
+
+    return @commits;
+}
+
+sub convertToCvsDate
+{
+    my $date = shift;
+    # Convert from: "git rev-list --pretty" formatted date
+    # Convert to: "the format specified by RFC822 as modified by RFC1123."
+    # Example: 26 May 1997 13:01:40 -0400
+    if( $date =~ /^\w+\s+(\w+)\s+(\d+)\s+(\d+:\d+:\d+)\s+(\d+)\s+([+-]\d+)$/ )
+    {
+        $date = "$2 $1 $4 $3 $5";
+    }
+
+    return $date;
+}
+
+sub convertToDbMode
+{
+    my $mode = shift;
+
+    # NOTE: The CVS protocol uses a string similar "u=rw,g=rw,o=rw",
+    #  but the database "mode" column historically (and currently)
+    #  only stores the "rw" (for user) part of the string.
+    #    FUTURE: It might make more sense to persist the raw
+    #  octal mode (or perhaps the final full CVS form) instead of
+    #  this half-converted form, but it isn't currently worth the
+    #  backwards compatibility headaches.
+
+    $mode=~/^\d\d(\d)\d{3}$/;
+    my $userBits=$1;
+
+    my $dbMode = "";
+    $dbMode .= "r" if ( $userBits & 4 );
+    $dbMode .= "w" if ( $userBits & 2 );
+    $dbMode .= "x" if ( $userBits & 1 );
+    $dbMode = "rw" if ( $dbMode eq "" );
+
+    return $dbMode;
+}
+
 sub insert_rev
 {
     my $self = shift;
@@ -3586,6 +4293,169 @@ sub gethead
     return $tree;
 }
 
+=head2 getAnyHead
+
+Returns a reference to an array of getmeta structures, one
+per file in the specified tree hash.
+
+=cut
+
+sub getAnyHead
+{
+    my ($self,$hash) = @_;
+
+    if(!defined($hash))
+    {
+        return $self->gethead();
+    }
+
+    my @files;
+    {
+        open(my $filePipe, '-|', 'git', 'ls-tree', '-z', '-r', $hash)
+                or die("Cannot call git-ls-tree : $!");
+        local $/ = "\0";
+        @files=<$filePipe>;
+        close $filePipe;
+    }
+
+    my $tree=[];
+    my($line);
+    foreach $line (@files)
+    {
+        $line=~s/\0$//;
+        unless ( $line=~/^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o )
+        {
+            die("Couldn't process git-ls-tree line : $_");
+        }
+
+        my($mode, $git_type, $git_hash, $git_filename) = ($1, $2, $3, $4);
+        push @$tree, $self->getMetaFromCommithash($git_filename,$hash);
+    }
+
+    return $tree;
+}
+
+=head2 getRevisionDirMap
+
+A "revision dir map" contains all the plain-file filenames associated
+with a particular revision (treeish), organized by directory:
+
+  $type = $out->{$dir}{$fullName}
+
+The type of each is "F" (for ordinary file) or "D" (for directory,
+for which the map $out->{$fullName} will also exist).
+
+=cut
+
+sub getRevisionDirMap
+{
+    my ($self,$ver)=@_;
+
+    if(!defined($self->{revisionDirMapCache}))
+    {
+        $self->{revisionDirMapCache}={};
+    }
+
+        # Get file list (previously cached results are dependent on HEAD,
+        # but are early in each case):
+    my $cacheKey;
+    my (@fileList);
+    if( !defined($ver) || $ver eq "" )
+    {
+        $cacheKey="";
+        if( defined($self->{revisionDirMapCache}{$cacheKey}) )
+        {
+            return $self->{revisionDirMapCache}{$cacheKey};
+        }
+
+        my @head = @{$self->gethead()};
+        foreach my $file ( @head )
+        {
+            next if ( $file->{filehash} eq "deleted" );
+
+            push @fileList,$file->{name};
+        }
+    }
+    else
+    {
+        my ($hash)=$self->lookupCommitRef($ver);
+        if( !defined($hash) )
+        {
+            return undef;
+        }
+
+        $cacheKey=$hash;
+        if( defined($self->{revisionDirMapCache}{$cacheKey}) )
+        {
+            return $self->{revisionDirMapCache}{$cacheKey};
+        }
+
+        open(my $filePipe, '-|', 'git', 'ls-tree', '-z', '-r', $hash)
+                or die("Cannot call git-ls-tree : $!");
+        local $/ = "\0";
+        while ( <$filePipe> )
+        {
+            chomp;
+            unless ( /^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o )
+            {
+                die("Couldn't process git-ls-tree line : $_");
+            }
+
+            my($mode, $git_type, $git_hash, $git_filename) = ($1, $2, $3, $4);
+
+            push @fileList, $git_filename;
+        }
+        close $filePipe;
+    }
+
+        # Convert to normalized form:
+    my %revMap;
+    my $file;
+    foreach $file (@fileList)
+    {
+        my($dir) = ($file=~m%^(?:(.*)/)?([^/]*)$%);
+        $dir='' if(!defined($dir));
+
+            # parent directories:
+            # ... create empty dir maps for parent dirs:
+        my($td)=$dir;
+        while(!defined($revMap{$td}))
+        {
+            $revMap{$td}={};
+
+            my($tp)=($td=~m%^(?:(.*)/)?([^/]*)$%);
+            $tp='' if(!defined($tp));
+            $td=$tp;
+        }
+            # ... add children to parent maps (now that they exist):
+        $td=$dir;
+        while($td ne "")
+        {
+            my($tp)=($td=~m%^(?:(.*)/)?([^/]*)$%);
+            $tp='' if(!defined($tp));
+
+            if(defined($revMap{$tp}{$td}))
+            {
+                if($revMap{$tp}{$td} ne 'D')
+                {
+                    die "Weird file/directory inconsistency in $cacheKey";
+                }
+                last;   # loop exit
+            }
+            $revMap{$tp}{$td}='D';
+
+            $td=$tp;
+        }
+
+            # file
+        $revMap{$dir}{$file}='F';
+    }
+
+        # Save in cache:
+    $self->{revisionDirMapCache}{$cacheKey}=\%revMap;
+    return $self->{revisionDirMapCache}{$cacheKey};
+}
+
 =head2 getlog
 
 See also gethistorydense().
@@ -3646,6 +4516,19 @@ =head2 getmeta
 This function takes a filename (with path) argument and returns a hashref of
 metadata for that file.
 
+There are several ways $revision can be specified:
+
+   - A reference to hash that contains a "tag" that is the
+     actual revision (one of the below).  TODO: Also allow it to
+     specify a "date" in the hash.
+   - undef, to refer to the latest version on the main branch.
+   - Full CVS client revision number (mapped to integer in DB, without the
+     "1." prefix),
+   - Complex CVS-compatible "special" revision number for
+     non-linear history (see comment below)
+   - git commit sha1 hash
+   - branch or tag name
+
 =cut
 
 sub getmeta
@@ -3656,23 +4539,144 @@ sub getmeta
     my $tablename_rev = $self->tablename("revision");
     my $tablename_head = $self->tablename("head");
 
-    my $db_query;
-    if ( defined($revision) and $revision =~ /^1\.(\d+)$/ )
+    if ( ref($revision) eq "HASH" )
     {
-        my ($intRev) = $1;
-        $db_query = $self->{dbh}->prepare_cached("SELECT * FROM $tablename_rev WHERE name=? AND revision=?",{},1);
-        $db_query->execute($filename, $intRev);
+        $revision = $revision->{tag};
     }
-    elsif ( defined($revision) and $revision =~ /^[a-zA-Z0-9]{40}$/ )
+
+    # Overview of CVS revision numbers:
+    #
+    # General CVS numbering scheme:
+    #   - Basic mainline branch numbers: "1.1", "1.2", "1.3", etc.
+    #   - Result of "cvs checkin -r" (possible, but not really
+    #     recommended): "2.1", "2.2", etc
+    #   - Branch tag: "1.2.0.n", where "1.2" is revision it was branched
+    #     from, "0" is a magic placeholder that identifies it as a
+    #     branch tag instead of a version tag, and n is 2 times the
+    #     branch number off of "1.2", starting with "2".
+    #   - Version on a branch: "1.2.n.x", where "1.2" is branch-from, "n"
+    #     is branch number off of "1.2" (like n above), and "x" is
+    #     the version number on the branch.
+    #   - Branches can branch off of branches: "1.3.2.7.4.1" (even number
+    #     of components).
+    #   - Odd "n"s are used by "vendor branches" that result
+    #     from "cvs import".  Vendor branches have additional
+    #     strangeness in the sense that the main rcs "head" of the main
+    #     branch will (temporarily until first normal commit) point
+    #     to the version on the vendor branch, rather than the actual
+    #     main branch.  (FUTURE: This may provide an opportunity
+    #     to use "strange" revision numbers for fast-forward-merged
+    #     branch tip when CVS client is asking for the main branch.)
+    #
+    # git-cvsserver CVS-compatible special numbering schemes:
+    #   - Currently git-cvsserver only tries to be identical to CVS for
+    #     simple "1.x" numbers on the "main" branch (as identified
+    #     by the module name that was originally cvs checkout'ed).
+    #   - The database only stores the "x" part, for historical reasons.
+    #     But most of the rest of the cvsserver preserves
+    #     and thinks using the full revision number.
+    #   - To handle non-linear history, it uses a version of the form
+    #     "2.1.1.2000.b.b.b."..., where the 2.1.1.2000 is to help uniquely
+    #     identify this as a special revision number, and there are
+    #     20 b's that together encode the sha1 git commit from which
+    #     this version of this file originated.  Each b is
+    #     the numerical value of the corresponding byte plus
+    #     100.
+    #      - "plus 100" avoids "0"s, and also reduces the
+    #        likelyhood of a collision in the case that someone someday
+    #        writes an import tool that tries to preserve original
+    #        CVS revision numbers, and the original CVS data had done
+    #        lots of branches off of branches and other strangeness to
+    #        end up with a real version number that just happens to look
+    #        like this special revision number form.  Also, if needed
+    #        there are several ways to extend/identify alternative encodings
+    #        within the "2.1.1.2000" part if necessary.
+    #      - Unlike real CVS revisions, you can't really reconstruct what
+    #        relation a revision of this form has to other revisions.
+    #   - FUTURE: TODO: Rework database somehow to make up and remember
+    #     fully-CVS-compatible branches and branch version numbers.
+
+    my $meta;
+    if ( defined($revision) )
     {
-        $db_query = $self->{dbh}->prepare_cached("SELECT * FROM $tablename_rev WHERE name=? AND commithash=?",{},1);
-        $db_query->execute($filename, $revision);
-    } else {
-        $db_query = $self->{dbh}->prepare_cached("SELECT * FROM $tablename_head WHERE name=?",{},1);
+        if ( $revision =~ /^1\.(\d+)$/ )
+        {
+            my ($intRev) = $1;
+            my $db_query;
+            $db_query = $self->{dbh}->prepare_cached(
+                "SELECT * FROM $tablename_rev WHERE name=? AND revision=?",
+                {},1);
+            $db_query->execute($filename, $intRev);
+            $meta = $db_query->fetchrow_hashref;
+        }
+        elsif ( $revision =~ /^2\.1\.1\.2000(\.[1-3][0-9][0-9]){20}$/ )
+        {
+            my ($commitHash)=($revision=~/^2\.1\.1\.2000(.*)$/);
+            $commitHash=~s/\.([0-9]+)/sprintf("%02x",$1-100)/eg;
+            if($commitHash=~/^[0-9a-f]{40}$/)
+            {
+                return $self->getMetaFromCommithash($filename,$commitHash);
+            }
+
+            # error recovery: fall back on head version below
+            print "E Failed to find $filename version=$revision or commit=$commitHash\n";
+            $log->warning("failed get $revision with commithash=$commitHash");
+            undef $revision;
+        }
+        elsif ( $revision =~ /^[0-9a-f]{40}$/ )
+        {
+            # Try DB first.  This is mostly only useful for req_annotate(),
+            # which only calls this for stuff that should already be in
+            # the DB.  It is fairly likely to be a waste of time
+            # in most other cases [unless the file happened to be
+            # modified in $revision specifically], but
+            # it is probably in the noise compared to how long
+            # getMetaFromCommithash() will take.
+            my $db_query;
+            $db_query = $self->{dbh}->prepare_cached(
+                "SELECT * FROM $tablename_rev WHERE name=? AND commithash=?",
+                {},1);
+            $db_query->execute($filename, $revision);
+            $meta = $db_query->fetchrow_hashref;
+
+            if(! $meta)
+            {
+                my($revCommit)=$self->lookupCommitRef($revision);
+                if($revCommit=~/^[0-9a-f]{40}$/)
+                {
+                    return $self->getMetaFromCommithash($filename,$revCommit);
+                }
+
+                # error recovery: nothing found:
+                print "E Failed to find $filename version=$revision\n";
+                $log->warning("failed get $revision");
+                return $meta;
+            }
+        }
+        else
+        {
+            my($revCommit)=$self->lookupCommitRef($revision);
+            if($revCommit=~/^[0-9a-f]{40}$/)
+            {
+                return $self->getMetaFromCommithash($filename,$revCommit);
+            }
+
+            # error recovery: fall back on head version below
+            print "E Failed to find $filename version=$revision\n";
+            $log->warning("failed get $revision");
+            undef $revision;  # Allow fallback
+        }
+    }
+
+    if(!defined($revision))
+    {
+        my $db_query;
+        $db_query = $self->{dbh}->prepare_cached(
+                "SELECT * FROM $tablename_head WHERE name=?",{},1);
         $db_query->execute($filename);
+        $meta = $db_query->fetchrow_hashref;
     }
 
-    my $meta = $db_query->fetchrow_hashref;
     if($meta)
     {
         $meta->{revision} = "1.$meta->{revision}";
@@ -3680,6 +4684,204 @@ sub getmeta
     return $meta;
 }
 
+sub getMetaFromCommithash
+{
+    my $self = shift;
+    my $filename = shift;
+    my $revCommit = shift;
+
+    # NOTE: This function doesn't scale well (lots of forks), especially
+    #   if you have many files that have not been modified for many commits
+    #   (each git-rev-parse redoes a lot of work for each file
+    #   that theoretically could be done in parallel by smarter
+    #   graph traversal).
+    #
+    # TODO: Possible optimization strategies:
+    #   - Solve the issue of assigning and remembering "real" CVS
+    #     revision numbers for branches, and ensure the
+    #     data structure can do this efficiently.  Perhaps something
+    #     similar to "git notes", and carefully structured to take
+    #     advantage same-sha1-is-same-contents, to roll the same
+    #     unmodified subdirectory data onto multiple commits?
+    #   - Write and use a C tool that is like git-blame, but
+    #     operates on multiple files with file granularity, instead
+    #     of one file with line granularity.  Cache
+    #     most-recently-modified in $self->{commitRefCache}{$revCommit}.
+    #     Try to be intelligent about how many files we do with
+    #     one fork (perhaps one directory at a time, without recursion,
+    #     and/or include directory as one line item, recurse from here
+    #     instead of in C tool?).
+    #   - Perhaps we could ask the DB for (filename,fileHash),
+    #     and just guess that it is correct (that the file hadn't
+    #     changed between $revCommit and the found commit, then
+    #     changed back, confusing anything trying to interpret
+    #     history).  Probably need to add another index to revisions
+    #     DB table for this.
+    #   - NOTE: Trying to store all (commit,file) keys in DB [to
+    #     find "lastModfiedCommit] (instead of
+    #     just files that changed in each commit as we do now) is
+    #     probably not practical from a disk space perspective.
+
+        # Does the file exist in $revCommit?
+    # TODO: Include file hash in dirmap cache.
+    my($dirMap)=$self->getRevisionDirMap($revCommit);
+    my($dir,$file)=($filename=~m%^(?:(.*)/)?([^/]*$)%);
+    if(!defined($dir))
+    {
+        $dir="";
+    }
+    if( !defined($dirMap->{$dir}) ||
+        !defined($dirMap->{$dir}{$filename}) )
+    {
+        my($fileHash)="deleted";
+
+        my($retVal)={};
+        $retVal->{name}=$filename;
+        $retVal->{filehash}=$fileHash;
+
+            # not needed and difficult to compute:
+        $retVal->{revision}="0";  # $revision;
+        $retVal->{commithash}=$revCommit;
+        #$retVal->{author}=$commit->{author};
+        #$retVal->{modified}=convertToCvsDate($commit->{date});
+        #$retVal->{mode}=convertToDbMode($mode);
+
+        return $retVal;
+    }
+
+    my($fileHash)=safe_pipe_capture("git","rev-parse","$revCommit:$filename");
+    chomp $fileHash;
+    if(!($fileHash=~/^[0-9a-f]{40}$/))
+    {
+        die "Invalid fileHash '$fileHash' looking up"
+                    ." '$revCommit:$filename'\n";
+    }
+
+    # information about most recent commit to modify $filename:
+    open(my $gitLogPipe, '-|', 'git', 'rev-list',
+         '--max-count=1', '--pretty', '--parents',
+         $revCommit, '--', $filename)
+                or die "Cannot call git-rev-list: $!";
+    my @commits=readCommits($gitLogPipe);
+    close $gitLogPipe;
+    if(scalar(@commits)!=1)
+    {
+        die "Can't find most recent commit changing $filename\n";
+    }
+    my($commit)=$commits[0];
+    if( !defined($commit) || !defined($commit->{hash}) )
+    {
+        return undef;
+    }
+
+    # does this (commit,file) have a real assigned CVS revision number?
+    my $tablename_rev = $self->tablename("revision");
+    my $db_query;
+    $db_query = $self->{dbh}->prepare_cached(
+        "SELECT * FROM $tablename_rev WHERE name=? AND commithash=?",
+        {},1);
+    $db_query->execute($filename, $commit->{hash});
+    my($meta)=$db_query->fetchrow_hashref;
+    if($meta)
+    {
+        $meta->{revision} = "1.$meta->{revision}";
+        return $meta;
+    }
+
+    # fall back on special revision number
+    my($revision)=$commit->{hash};
+    $revision=~s/(..)/'.' . (hex($1)+100)/eg;
+    $revision="2.1.1.2000$revision";
+
+    # meta data about $filename:
+    open(my $filePipe, '-|', 'git', 'ls-tree', '-z',
+                $commit->{hash}, '--', $filename)
+            or die("Cannot call git-ls-tree : $!");
+    local $/ = "\0";
+    my $line;
+    $line=<$filePipe>;
+    if(defined(<$filePipe>))
+    {
+        die "Expected only a single file for git-ls-tree $filename\n";
+    }
+    close $filePipe;
+
+    chomp $line;
+    unless ( $line=~m/^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o )
+    {
+        die("Couldn't process git-ls-tree line : $line\n");
+    }
+    my ( $mode, $git_type, $git_hash, $git_filename ) = ( $1, $2, $3, $4 );
+
+    # save result:
+    my($retVal)={};
+    $retVal->{name}=$filename;
+    $retVal->{revision}=$revision;
+    $retVal->{filehash}=$fileHash;
+    $retVal->{commithash}=$revCommit;
+    $retVal->{author}=$commit->{author};
+    $retVal->{modified}=convertToCvsDate($commit->{date});
+    $retVal->{mode}=convertToDbMode($mode);
+
+    return $retVal;
+}
+
+=head2 lookupCommitRef
+
+Convert tag/branch/abbreviation/etc into a commit sha1 hash.  Caches
+the result so looking it up again is fast.
+
+=cut
+
+sub lookupCommitRef
+{
+    my $self = shift;
+    my $ref = shift;
+
+    my $commitHash = $self->{commitRefCache}{$ref};
+    if(defined($commitHash))
+    {
+        return $commitHash;
+    }
+
+    $commitHash=safe_pipe_capture("git","rev-parse","--verify","--quiet",
+                                  $self->unescapeRefName($ref));
+    $commitHash=~s/\s*$//;
+    if(!($commitHash=~/^[0-9a-f]{40}$/))
+    {
+        $commitHash=undef;
+    }
+
+    if( defined($commitHash) )
+    {
+        my $type=safe_pipe_capture("git","cat-file","-t",$commitHash);
+        if( ! ($type=~/^commit\s*$/ ) )
+        {
+            $commitHash=undef;
+        }
+    }
+    if(defined($commitHash))
+    {
+        $self->{commitRefCache}{$ref}=$commitHash;
+    }
+    return $commitHash;
+}
+
+=head2 clearCommitRefCaches
+
+Clears cached commit cache (sha1's for various tags/abbeviations/etc),
+and related caches.
+
+=cut
+
+sub clearCommitRefCaches
+{
+    my $self = shift;
+    $self->{commitRefCache} = {};
+    $self->{revisionDirMapCache} = undef;
+    $self->{gethead_cache} = undef;
+}
+
 =head2 commitmessage
 
 this function takes a commithash and returns the commit message for that commit
@@ -3745,6 +4947,97 @@ sub gethistorydense
     return $result;
 }
 
+=head2 escapeRefName
+
+Apply an escape mechanism to compensate for characters that
+git ref names can have that CVS tags can not.
+
+=cut
+sub escapeRefName
+{
+    my($self,$refName)=@_;
+
+    # CVS officially only allows [-_A-Za-z0-9] in tag names (or in
+    # many contexts it can also be a CVS revision number).
+    #
+    # Git tags commonly use '/' and '.' as well, but also handle
+    # anything else just in case:
+    #
+    #   = "_-s-"  For '/'.
+    #   = "_-p-"  For '.'.
+    #   = "_-u-"  For underscore, in case someone wants a literal "_-" in
+    #     a tag name.
+    #   = "_-xx-" Where "xx" is the hexadecimal representation of the
+    #     desired ASCII character byte. (for anything else)
+
+    if(! $refName=~/^[1-9][0-9]*(\.[1-9][0-9]*)*$/)
+    {
+        $refName=~s/_-/_-u--/g;
+        $refName=~s/\./_-p-/g;
+        $refName=~s%/%_-s-%g;
+        $refName=~s/[^-_a-zA-Z0-9]/sprintf("_-%02x-",$1)/eg;
+    }
+}
+
+=head2 unescapeRefName
+
+Undo an escape mechanism to compensate for characters that
+git ref names can have that CVS tags can not.
+
+=cut
+sub unescapeRefName
+{
+    my($self,$refName)=@_;
+
+    # see escapeRefName() for description of escape mechanism.
+
+    $refName=~s/_-([spu]|[0-9a-f][0-9a-f])-/unescapeRefNameChar($1)/eg;
+
+    # allowed tag names
+    # TODO: Perhaps use git check-ref-format, with an in-process cache of
+    #  validated names?
+    if( !( $refName=~m%^[^-][-a-zA-Z0-9_/.]*$% ) ||
+        ( $refName=~m%[/.]$% ) ||
+        ( $refName=~/\.lock$/ ) ||
+        ( $refName=~m%\.\.|/\.|[[\\:?*~]|\@\{% ) )  # matching }
+    {
+        # Error:
+        $log->warn("illegal refName: $refName");
+        $refName=undef;
+    }
+    return $refName;
+}
+
+sub unescapeRefNameChar
+{
+    my($char)=@_;
+
+    if($char eq "s")
+    {
+        $char="/";
+    }
+    elsif($char eq "p")
+    {
+        $char=".";
+    }
+    elsif($char eq "u")
+    {
+        $char="_";
+    }
+    elsif($char=~/^[0-9a-f][0-9a-f]$/)
+    {
+        $char=chr(hex($char));
+    }
+    else
+    {
+        # Error case: Maybe it has come straight from user, and
+        # wasn't supposed to be escaped?  Restore it the way we got it:
+        $char="_-$char-";
+    }
+
+    return $char;
+}
+
 =head2 in_array()
 
 from Array::PAT - mimics the in_array() function
index 69f1452cf80066a002fe58bb1eeebda7fe63ba8b..2da564995dd82ac3fa96d8d7ad6273152f6f161d 100755 (executable)
--- a/git-p4.py
+++ b/git-p4.py
@@ -553,29 +553,49 @@ def gitConfigList(key):
         _gitConfig[key] = read_pipe("git config --get-all %s" % key, ignore_error=True).strip().split(os.linesep)
     return _gitConfig[key]
 
-def p4BranchesInGit(branchesAreInRemotes = True):
+def p4BranchesInGit(branchesAreInRemotes=True):
+    """Find all the branches whose names start with "p4/", looking
+       in remotes or heads as specified by the argument.  Return
+       a dictionary of { branch: revision } for each one found.
+       The branch names are the short names, without any
+       "p4/" prefix."""
+
     branches = {}
 
     cmdline = "git rev-parse --symbolic "
     if branchesAreInRemotes:
-        cmdline += " --remotes"
+        cmdline += "--remotes"
     else:
-        cmdline += " --branches"
+        cmdline += "--branches"
 
     for line in read_pipe_lines(cmdline):
         line = line.strip()
 
-        ## only import to p4/
-        if not line.startswith('p4/') or line == "p4/HEAD":
+        # only import to p4/
+        if not line.startswith('p4/'):
+            continue
+        # special symbolic ref to p4/master
+        if line == "p4/HEAD":
             continue
-        branch = line
 
-        # strip off p4
-        branch = re.sub ("^p4/", "", line)
+        # strip off p4/ prefix
+        branch = line[len("p4/"):]
 
         branches[branch] = parseRevision(line)
+
     return branches
 
+def branch_exists(branch):
+    """Make sure that the given ref name really exists."""
+
+    cmd = [ "git", "rev-parse", "--symbolic", "--verify", branch ]
+    p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+    out, _ = p.communicate()
+    if p.returncode:
+        return False
+    # expect exactly one line of output: the branch name
+    return out.rstrip() == branch
+
 def findUpstreamBranchPoint(head = "HEAD"):
     branches = p4BranchesInGit()
     # map from depot-path to branch name
@@ -907,7 +927,8 @@ def __init__(self):
                 optparse.make_option("--dry-run", "-n", dest="dry_run", action="store_true"),
                 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),
                 optparse.make_option("--conflict", dest="conflict_behavior",
-                                     choices=self.conflict_behavior_choices)
+                                     choices=self.conflict_behavior_choices),
+                optparse.make_option("--branch", dest="branch"),
         ]
         self.description = "Submit changes from git to the perforce depot."
         self.usage += " [name of git branch to submit into perforce depot]"
@@ -920,6 +941,7 @@ def __init__(self):
         self.isWindows = (platform.system() == "Windows")
         self.exportLabels = False
         self.p4HasMoveCommand = p4_has_move_command()
+        self.branch = None
 
     def check(self):
         if len(p4CmdList("opened ...")) > 0:
@@ -1656,6 +1678,8 @@ def run(self, args):
             print "All commits applied!"
 
             sync = P4Sync()
+            if self.branch:
+                sync.branch = self.branch
             sync.run([])
 
             rebase = P4Rebase()
@@ -2509,13 +2533,6 @@ def getBranchMappingFromGitBranches(self):
                 branch = branch[len(self.projectName):]
             self.knownBranches[branch] = branch
 
-    def listExistingP4GitBranches(self):
-        # branches holds mapping from name to commit
-        branches = p4BranchesInGit(self.importIntoRemotes)
-        self.p4BranchesInGit = branches.keys()
-        for branch in branches.keys():
-            self.initialParents[self.refPrefix + branch] = branches[branch]
-
     def updateOptionDict(self, d):
         option_keys = {}
         if self.keepRepoPath:
@@ -2687,6 +2704,7 @@ def importChanges(self, changes):
                     files = self.extractFilesFromCommit(description)
                     self.commit(description, files, self.branch,
                                 self.initialParent)
+                    # only needed once, to connect to the previous commit
                     self.initialParent = ""
             except IOError:
                 print self.gitError.read()
@@ -2752,34 +2770,31 @@ def importHeadRevision(self, revision):
     def run(self, args):
         self.depotPaths = []
         self.changeRange = ""
-        self.initialParent = ""
         self.previousDepotPaths = []
+        self.hasOrigin = False
 
         # map from branch depot path to parent branch
         self.knownBranches = {}
         self.initialParents = {}
-        self.hasOrigin = originP4BranchesExist()
-        if not self.syncWithOrigin:
-            self.hasOrigin = False
 
         if self.importIntoRemotes:
             self.refPrefix = "refs/remotes/p4/"
         else:
             self.refPrefix = "refs/heads/p4/"
 
-        if self.syncWithOrigin and self.hasOrigin:
-            if not self.silent:
-                print "Syncing with origin first by calling git fetch origin"
-            system("git fetch origin")
+        if self.syncWithOrigin:
+            self.hasOrigin = originP4BranchesExist()
+            if self.hasOrigin:
+                if not self.silent:
+                    print 'Syncing with origin first, using "git fetch origin"'
+                system("git fetch origin")
 
+        branch_arg_given = bool(self.branch)
         if len(self.branch) == 0:
             self.branch = self.refPrefix + "master"
             if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
                 system("git update-ref %s refs/heads/p4" % self.branch)
-                system("git branch -D p4");
-            # create it /after/ importing, when master exists
-            if not gitBranchExists(self.refPrefix + "HEAD") and self.importIntoRemotes and gitBranchExists(self.branch):
-                system("git symbolic-ref %sHEAD %s" % (self.refPrefix, self.branch))
+                system("git branch -D p4")
 
         # accept either the command-line option, or the configuration variable
         if self.useClientSpec:
@@ -2796,12 +2811,25 @@ def run(self, args):
         if args == []:
             if self.hasOrigin:
                 createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
-            self.listExistingP4GitBranches()
+
+            # branches holds mapping from branch name to sha1
+            branches = p4BranchesInGit(self.importIntoRemotes)
+
+            # restrict to just this one, disabling detect-branches
+            if branch_arg_given:
+                short = self.branch.split("/")[-1]
+                if short in branches:
+                    self.p4BranchesInGit = [ short ]
+            else:
+                self.p4BranchesInGit = branches.keys()
 
             if len(self.p4BranchesInGit) > 1:
                 if not self.silent:
                     print "Importing from/into multiple branches"
                 self.detectBranches = True
+                for branch in branches.keys():
+                    self.initialParents[self.refPrefix + branch] = \
+                        branches[branch]
 
             if self.verbose:
                 print "branches: %s" % self.p4BranchesInGit
@@ -2838,13 +2866,21 @@ def run(self, args):
             if p4Change > 0:
                 self.depotPaths = sorted(self.previousDepotPaths)
                 self.changeRange = "@%s,#head" % p4Change
-                if not self.detectBranches:
-                    self.initialParent = parseRevision(self.branch)
                 if not self.silent and not self.detectBranches:
                     print "Performing incremental import into %s git branch" % self.branch
 
+        # accept multiple ref name abbreviations:
+        #    refs/foo/bar/branch -> use it exactly
+        #    p4/branch -> prepend refs/remotes/ or refs/heads/
+        #    branch -> prepend refs/remotes/p4/ or refs/heads/p4/
         if not self.branch.startswith("refs/"):
-            self.branch = "refs/heads/" + self.branch
+            if self.importIntoRemotes:
+                prepend = "refs/remotes/"
+            else:
+                prepend = "refs/heads/"
+            if not self.branch.startswith("p4/"):
+                prepend += "p4/"
+            self.branch = prepend + self.branch
 
         if len(args) == 0 and self.depotPaths:
             if not self.silent:
@@ -2955,8 +2991,21 @@ def run(self, args):
             else:
                 # catch "git p4 sync" with no new branches, in a repo that
                 # does not have any existing p4 branches
-                if len(args) == 0 and not self.p4BranchesInGit:
-                    die("No remote p4 branches.  Perhaps you never did \"git p4 clone\" in here.");
+                if len(args) == 0:
+                    if not self.p4BranchesInGit:
+                        die("No remote p4 branches.  Perhaps you never did \"git p4 clone\" in here.")
+
+                    # The default branch is master, unless --branch is used to
+                    # specify something else.  Make sure it exists, or complain
+                    # nicely about how to use --branch.
+                    if not self.detectBranches:
+                        if not branch_exists(self.branch):
+                            if branch_arg_given:
+                                die("Error: branch %s does not exist." % self.branch)
+                            else:
+                                die("Error: no branch %s; perhaps specify one with --branch." %
+                                    self.branch)
+
                 if self.verbose:
                     print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
                                                               self.changeRange)
@@ -2974,6 +3023,14 @@ def run(self, args):
 
                 self.updatedBranches = set()
 
+                if not self.detectBranches:
+                    if args:
+                        # start a new branch
+                        self.initialParent = ""
+                    else:
+                        # build on a previous revision
+                        self.initialParent = parseRevision(self.branch)
+
                 self.importChanges(changes)
 
                 if not self.silent:
@@ -3006,6 +3063,13 @@ def run(self, args):
                 read_pipe("git update-ref -d %s" % branch)
             os.rmdir(os.path.join(os.environ.get("GIT_DIR", ".git"), self.tempBranchLocation))
 
+        # Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow
+        # a convenient shortcut refname "p4".
+        if self.importIntoRemotes:
+            head_ref = self.refPrefix + "HEAD"
+            if not gitBranchExists(head_ref) and gitBranchExists(self.branch):
+                system(["git", "symbolic-ref", head_ref, self.branch])
+
         return True
 
 class P4Rebase(Command):
@@ -3113,17 +3177,15 @@ def run(self, args):
 
         if not P4Sync.run(self, depotPaths):
             return False
-        if self.branch != "master":
-            if self.importIntoRemotes:
-                masterbranch = "refs/remotes/p4/master"
-            else:
-                masterbranch = "refs/heads/p4/master"
-            if gitBranchExists(masterbranch):
-                system("git branch master %s" % masterbranch)
-                if not self.cloneBare:
-                    system("git checkout -f")
-            else:
-                print "Could not detect main branch. No checkout/master branch created."
+
+        # create a master branch and check out a work tree
+        if gitBranchExists(self.branch):
+            system([ "git", "branch", "master", self.branch ])
+            if not self.cloneBare:
+                system([ "git", "checkout", "-f" ])
+        else:
+            print 'Not checking out any branch, use ' \
+                  '"git checkout -q -b master <branch>"'
 
         # auto-set this variable if invoked with --use-client-spec
         if self.useClientSpec_from_options:
index 44901d53c43d972e03a71bfbe0b769f2e8f22d7b..8ed7fccc18507b6f3e2508bb117ce056a764de77 100644 (file)
@@ -190,6 +190,11 @@ is_empty_commit() {
        test "$tree" = "$ptree"
 }
 
+is_merge_commit()
+{
+       git rev-parse --verify --quiet "$1"^2 >/dev/null 2>&1
+}
+
 # Run command with GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and
 # GIT_AUTHOR_DATE exported from the current environment.
 do_with_author () {
@@ -874,7 +879,7 @@ git rev-list $merges_option --pretty=oneline --abbrev-commit \
 while read -r shortsha1 rest
 do
 
-       if test -z "$keep_empty" && is_empty_commit $shortsha1
+       if test -z "$keep_empty" && is_empty_commit $shortsha1 && ! is_merge_commit $shortsha1
        then
                comment_out="# "
        else
index e4533b187d024fa5f044ecbbca8c05455a6662e2..d94a66a87094436efcb6cf8dd0171b0683f0376c 100644 (file)
@@ -164,6 +164,11 @@ def do_import(repo, args):
         ref = line[7:].strip()
         refs.append(ref)
 
+    print "feature done"
+
+    if os.environ.get("GIT_REMOTE_TESTGIT_FAILURE"):
+        die('Told to fail')
+
     repo = update_local_repo(repo)
     repo.exporter.export_repo(repo.gitdir, refs)
 
@@ -177,6 +182,9 @@ def do_export(repo, args):
     if not repo.gitdir:
         die("Need gitdir to export")
 
+    if os.environ.get("GIT_REMOTE_TESTGIT_FAILURE"):
+        die('Told to fail')
+
     update_local_repo(repo)
     changed = repo.importer.do_import(repo.gitdir)
 
index 94c7f76a156f1eaa4c622db267710f2982cf7826..be809e5b59394a4fd2f4f2b73cad6bac47a39600 100755 (executable)
@@ -1285,10 +1285,10 @@ sub send_message {
                }
 
                if (defined $input_format && $input_format eq 'mbox') {
-                       if (/^Subject:\s+(.*)$/) {
+                       if (/^Subject:\s+(.*)$/i) {
                                $subject = $1;
                        }
-                       elsif (/^From:\s+(.*)$/) {
+                       elsif (/^From:\s+(.*)$/i) {
                                ($author, $author_encoding) = unquote_rfc2047($1);
                                next if $suppress_cc{'author'};
                                next if $suppress_cc{'self'} and $author eq $sender;
@@ -1296,14 +1296,14 @@ sub send_message {
                                        $1, $_) unless $quiet;
                                push @cc, $1;
                        }
-                       elsif (/^To:\s+(.*)$/) {
+                       elsif (/^To:\s+(.*)$/i) {
                                foreach my $addr (parse_address_line($1)) {
                                        printf("(mbox) Adding to: %s from line '%s'\n",
                                                $addr, $_) unless $quiet;
                                        push @to, $addr;
                                }
                        }
-                       elsif (/^Cc:\s+(.*)$/) {
+                       elsif (/^Cc:\s+(.*)$/i) {
                                foreach my $addr (parse_address_line($1)) {
                                        if (unquote_rfc2047($addr) eq $sender) {
                                                next if ($suppress_cc{'self'});
@@ -1325,7 +1325,7 @@ sub send_message {
                        elsif (/^Message-Id: (.*)/i) {
                                $message_id = $1;
                        }
-                       elsif (!/^Date:\s/ && /^[-A-Za-z]+:\s+\S/) {
+                       elsif (!/^Date:\s/i && /^[-A-Za-z]+:\s+\S/) {
                                push @xh, $_;
                        }
 
index bd5266c86b299bbc75878b7529820798d194fc72..d0866946cee0076dd167cf6234dfec192d8d194e 100755 (executable)
@@ -114,6 +114,7 @@ sub _req_svn {
        $_message, $_file, $_branch_dest,
        $_template, $_shared,
        $_version, $_fetch_all, $_no_rebase, $_fetch_parent,
+       $_before, $_after,
        $_merge, $_strategy, $_preserve_merges, $_dry_run, $_local,
        $_prefix, $_no_checkout, $_url, $_verbose,
        $_commit_url, $_tag, $_merge_info, $_interactive);
@@ -258,7 +259,8 @@ sub _req_svn {
                        } ],
        'find-rev' => [ \&cmd_find_rev,
                        "Translate between SVN revision numbers and tree-ish",
-                       {} ],
+                       { 'before' => \$_before,
+                         'after' => \$_after } ],
        'rebase' => [ \&cmd_rebase, "Fetch and rebase your working directory",
                        { 'merge|m|M' => \$_merge,
                          'verbose|v' => \$_verbose,
@@ -1191,7 +1193,13 @@ sub cmd_find_rev {
                            "$head history\n";
                }
                my $desired_revision = substr($revision_or_hash, 1);
-               $result = $gs->rev_map_get($desired_revision, $uuid);
+               if ($_before) {
+                       $result = $gs->find_rev_before($desired_revision, 1);
+               } elsif ($_after) {
+                       $result = $gs->find_rev_after($desired_revision, 1);
+               } else {
+                       $result = $gs->rev_map_get($desired_revision, $uuid);
+               }
        } else {
                my (undef, $rev, undef) = cmt_metadata($revision_or_hash);
                $result = $rev;
diff --git a/git.c b/git.c
index ed66c660d15010af9a8b0c2ea1da8839e316e1db..b10c18b0419904631d7e8fa4a1fd11e8533b3034 100644 (file)
--- a/git.c
+++ b/git.c
@@ -313,6 +313,7 @@ static void handle_internal_command(int argc, const char **argv)
                { "bundle", cmd_bundle, RUN_SETUP_GENTLY },
                { "cat-file", cmd_cat_file, RUN_SETUP },
                { "check-attr", cmd_check_attr, RUN_SETUP },
+               { "check-ignore", cmd_check_ignore, RUN_SETUP | NEED_WORK_TREE },
                { "check-ref-format", cmd_check_ref_format },
                { "checkout", cmd_checkout, RUN_SETUP | NEED_WORK_TREE },
                { "checkout-index", cmd_checkout_index,
index d93bd990a9d7fc9ad7c926e25f5d58f4e7514260..b3706fc9b9bef1361f6872b00905443dea584a7d 100755 (executable)
@@ -2161,7 +2161,7 @@ proc makewindow {} {
     trace add variable sha1string write sha1change
     pack $sha1entry -side left -pady 2
 
-    image create bitmap bm-left -data {
+    set bm_left_data {
        #define left_width 16
        #define left_height 16
        static unsigned char left_bits[] = {
@@ -2169,7 +2169,7 @@ proc makewindow {} {
        0x0e, 0x00, 0xff, 0x7f, 0xff, 0x7f, 0xff, 0x7f, 0x0e, 0x00, 0x1c, 0x00,
        0x38, 0x00, 0x70, 0x00, 0xe0, 0x00, 0xc0, 0x01};
     }
-    image create bitmap bm-right -data {
+    set bm_right_data {
        #define right_width 16
        #define right_height 16
        static unsigned char right_bits[] = {
@@ -2177,11 +2177,24 @@ proc makewindow {} {
        0x00, 0x38, 0xff, 0x7f, 0xff, 0x7f, 0xff, 0x7f, 0x00, 0x38, 0x00, 0x1c,
        0x00, 0x0e, 0x00, 0x07, 0x80, 0x03, 0xc0, 0x01};
     }
-    ${NS}::button .tf.bar.leftbut -image bm-left -command goback \
-       -state disabled -width 26
+    image create bitmap bm-left -data $bm_left_data
+    image create bitmap bm-left-gray -data $bm_left_data -foreground "#999"
+    image create bitmap bm-right -data $bm_right_data
+    image create bitmap bm-right-gray -data $bm_right_data -foreground "#999"
+
+    ${NS}::button .tf.bar.leftbut -command goback -state disabled -width 26
+    if {$use_ttk} {
+       .tf.bar.leftbut configure -image [list bm-left disabled bm-left-gray]
+    } else {
+       .tf.bar.leftbut configure -image bm-left
+    }
     pack .tf.bar.leftbut -side left -fill y
-    ${NS}::button .tf.bar.rightbut -image bm-right -command goforw \
-       -state disabled -width 26
+    ${NS}::button .tf.bar.rightbut -command goforw -state disabled -width 26
+    if {$use_ttk} {
+       .tf.bar.rightbut configure -image [list bm-right disabled bm-right-gray]
+    } else {
+       .tf.bar.rightbut configure -image bm-right
+    }
     pack .tf.bar.rightbut -side left -fill y
 
     ${NS}::label .tf.bar.rowlabel -text [mc "Row"]
@@ -2361,6 +2374,8 @@ proc makewindow {} {
     $ctext tag conf mresult -font textfontbold
     $ctext tag conf msep -font textfontbold
     $ctext tag conf found -back yellow
+    $ctext tag conf currentsearchhit -back orange
+    $ctext tag conf wwrap -wrap word
 
     .pwbottom add .bleft
     if {!$use_ttk} {
@@ -2495,10 +2510,9 @@ proc makewindow {} {
     bindkey ? {dofind -1 1}
     bindkey f nextfile
     bind . <F5> updatecommits
-    bind . <Shift-F5> reloadcommits
+    bindmodfunctionkey Shift 5 reloadcommits
     bind . <F2> showrefs
-    bind . <Shift-F4> {newview 0}
-    catch { bind . <Shift-Key-XF86_Switch_VT_4> {newview 0} }
+    bindmodfunctionkey Shift 4 {newview 0}
     bind . <F4> edit_or_newview
     bind . <$M1B-q> doquit
     bind . <$M1B-f> {dofind 1 1}
@@ -2523,6 +2537,7 @@ proc makewindow {} {
     bind $cflist $ctxbut {pop_flist_menu %W %X %Y %x %y}
     bind $ctext $ctxbut {pop_diff_menu %W %X %Y %x %y}
     bind $ctext <Button-1> {focus %W}
+    bind $ctext <<Selection>> rehighlight_search_results
 
     set maincursor [. cget -cursor]
     set textcursor [$ctext cget -cursor]
@@ -2646,6 +2661,11 @@ proc bindkey {ev script} {
     }
 }
 
+proc bindmodfunctionkey {mod n script} {
+    bind . <$mod-F$n> $script
+    catch { bind . <$mod-XF86_Switch_VT_$n> $script }
+}
+
 # set the focus back to the toplevel for any click outside
 # the entry widgets
 proc click {w} {
@@ -2702,7 +2722,7 @@ proc savestuff {w} {
     global cmitmode wrapcomment datetimeformat limitdiffs
     global colors uicolor bgcolor fgcolor diffcolors diffcontext selectbgcolor
     global autoselect autosellen extdifftool perfile_attrs markbgcolor use_ttk
-    global hideremotes want_ttk
+    global hideremotes want_ttk maxrefs
 
     if {$stuffsaved} return
     if {![winfo viewable .]} return
@@ -2724,6 +2744,7 @@ proc savestuff {w} {
        puts $f [list set autoselect $autoselect]
        puts $f [list set autosellen $autosellen]
        puts $f [list set showneartags $showneartags]
+       puts $f [list set maxrefs $maxrefs]
        puts $f [list set hideremotes $hideremotes]
        puts $f [list set showlocalchanges $showlocalchanges]
        puts $f [list set datetimeformat $datetimeformat]
@@ -3309,6 +3330,7 @@ proc sel_flist {w x y} {
     } else {
        catch {$ctext yview [lindex $difffilestart [expr {$l - 2}]]}
     }
+    suppress_highlighting_file_for_current_scrollpos
 }
 
 proc pop_flist_menu {w X Y x y} {
@@ -6857,7 +6879,7 @@ proc viewnextline {dir} {
 # add a list of tag or branch names at position pos
 # returns the number of names inserted
 proc appendrefs {pos ids var} {
-    global ctext linknum curview $var maxrefs
+    global ctext linknum curview $var maxrefs mainheadid
 
     if {[catch {$ctext index $pos}]} {
        return 0
@@ -6870,24 +6892,54 @@ proc appendrefs {pos ids var} {
            lappend tags [list $tag $id]
        }
     }
+
+    set sep {}
+    set tags [lsort -index 0 -decreasing $tags]
+    set nutags 0
+
     if {[llength $tags] > $maxrefs} {
-       $ctext insert $pos "[mc "many"] ([llength $tags])"
-    } else {
-       set tags [lsort -index 0 -decreasing $tags]
-       set sep {}
-       foreach ti $tags {
-           set id [lindex $ti 1]
-           set lk link$linknum
-           incr linknum
-           $ctext tag delete $lk
-           $ctext insert $pos $sep
-           $ctext insert $pos [lindex $ti 0] $lk
-           setlink $id $lk
-           set sep ", "
+       # If we are displaying heads, and there are too many,
+       # see if there are some important heads to display.
+       # Currently this means "master" and the current head.
+       set itags {}
+       if {$var eq "idheads"} {
+           set utags {}
+           foreach ti $tags {
+               set hname [lindex $ti 0]
+               set id [lindex $ti 1]
+               if {($hname eq "master" || $id eq $mainheadid) &&
+                   [llength $itags] < $maxrefs} {
+                   lappend itags $ti
+               } else {
+                   lappend utags $ti
+               }
+           }
+           set tags $utags
+       }
+       if {$itags ne {}} {
+           set str [mc "and many more"]
+           set sep " "
+       } else {
+           set str [mc "many"]
        }
+       $ctext insert $pos "$str ([llength $tags])"
+       set nutags [llength $tags]
+       set tags $itags
+    }
+
+    foreach ti $tags {
+       set id [lindex $ti 1]
+       set lk link$linknum
+       incr linknum
+       $ctext tag delete $lk
+       $ctext insert $pos $sep
+       $ctext insert $pos [lindex $ti 0] $lk
+       setlink $id $lk
+       set sep ", "
     }
+    $ctext tag add wwrap "$pos linestart" "$pos lineend"
     $ctext conf -state disabled
-    return [llength $tags]
+    return [expr {[llength $tags] + $nutags}]
 }
 
 # called when we have finished computing the nearby tags
@@ -7947,32 +7999,45 @@ proc changediffdisp {} {
     $ctext tag conf dresult -elide [lindex $diffelide 1]
 }
 
-proc highlightfile {loc cline} {
-    global ctext cflist cflist_top
+proc highlightfile {cline} {
+    global cflist cflist_top
+
+    if {![info exists cflist_top]} return
 
-    $ctext yview $loc
     $cflist tag remove highlight $cflist_top.0 "$cflist_top.0 lineend"
     $cflist tag add highlight $cline.0 "$cline.0 lineend"
     $cflist see $cline.0
     set cflist_top $cline
 }
 
+proc highlightfile_for_scrollpos {topidx} {
+    global cmitmode difffilestart
+
+    if {$cmitmode eq "tree"} return
+    if {![info exists difffilestart]} return
+
+    set top [lindex [split $topidx .] 0]
+    if {$difffilestart eq {} || $top < [lindex $difffilestart 0]} {
+       highlightfile 0
+    } else {
+       highlightfile [expr {[bsearch $difffilestart $top] + 2}]
+    }
+}
+
 proc prevfile {} {
     global difffilestart ctext cmitmode
 
     if {$cmitmode eq "tree"} return
     set prev 0.0
-    set prevline 1
     set here [$ctext index @0,0]
     foreach loc $difffilestart {
        if {[$ctext compare $loc >= $here]} {
-           highlightfile $prev $prevline
+           $ctext yview $prev
            return
        }
        set prev $loc
-       incr prevline
     }
-    highlightfile $prev $prevline
+    $ctext yview $prev
 }
 
 proc nextfile {} {
@@ -7980,11 +8045,9 @@ proc nextfile {} {
 
     if {$cmitmode eq "tree"} return
     set here [$ctext index @0,0]
-    set line 1
     foreach loc $difffilestart {
-       incr line
        if {[$ctext compare $loc > $here]} {
-           highlightfile $loc $line
+           $ctext yview $loc
            return
        }
     }
@@ -8030,7 +8093,6 @@ proc settabs {{firstab {}}} {
 proc incrsearch {name ix op} {
     global ctext searchstring searchdirn
 
-    $ctext tag remove found 1.0 end
     if {[catch {$ctext index anchor}]} {
        # no anchor set, use start of selection, or of visible area
        set sel [$ctext tag ranges sel]
@@ -8043,12 +8105,17 @@ proc incrsearch {name ix op} {
        }
     }
     if {$searchstring ne {}} {
-       set here [$ctext search $searchdirn -- $searchstring anchor]
+       set here [$ctext search -count mlen $searchdirn -- $searchstring anchor]
        if {$here ne {}} {
            $ctext see $here
+           set mend "$here + $mlen c"
+           $ctext tag remove sel 1.0 end
+           $ctext tag add sel $here $mend
+           suppress_highlighting_file_for_current_scrollpos
+           highlightfile_for_scrollpos $here
        }
-       searchmarkvisible 1
     }
+    rehighlight_search_results
 }
 
 proc dosearch {} {
@@ -8071,9 +8138,12 @@ proc dosearch {} {
            return
        }
        $ctext see $match
+       suppress_highlighting_file_for_current_scrollpos
+       highlightfile_for_scrollpos $match
        set mend "$match + $mlen c"
        $ctext tag add sel $match $mend
        $ctext mark unset anchor
+       rehighlight_search_results
     }
 }
 
@@ -8097,21 +8167,41 @@ proc dosearchback {} {
            return
        }
        $ctext see $match
+       suppress_highlighting_file_for_current_scrollpos
+       highlightfile_for_scrollpos $match
        set mend "$match + $ml c"
        $ctext tag add sel $match $mend
        $ctext mark unset anchor
+       rehighlight_search_results
+    }
+}
+
+proc rehighlight_search_results {} {
+    global ctext searchstring
+
+    $ctext tag remove found 1.0 end
+    $ctext tag remove currentsearchhit 1.0 end
+
+    if {$searchstring ne {}} {
+       searchmarkvisible 1
     }
 }
 
 proc searchmark {first last} {
     global ctext searchstring
 
+    set sel [$ctext tag ranges sel]
+
     set mend $first.0
     while {1} {
        set match [$ctext search -count mlen -- $searchstring $mend $last.end]
        if {$match eq {}} break
        set mend "$match + $mlen c"
-       $ctext tag add found $match $mend
+       if {$sel ne {} && [$ctext compare $match == [lindex $sel 0]]} {
+           $ctext tag add currentsearchhit $match $mend
+       } else {
+           $ctext tag add found $match $mend
+       }
     }
 }
 
@@ -8137,8 +8227,23 @@ proc searchmarkvisible {doall} {
     }
 }
 
+proc suppress_highlighting_file_for_current_scrollpos {} {
+    global ctext suppress_highlighting_file_for_this_scrollpos
+
+    set suppress_highlighting_file_for_this_scrollpos [$ctext index @0,0]
+}
+
 proc scrolltext {f0 f1} {
-    global searchstring
+    global searchstring cmitmode ctext
+    global suppress_highlighting_file_for_this_scrollpos
+
+    set topidx [$ctext index @0,0]
+    if {![info exists suppress_highlighting_file_for_this_scrollpos]
+       || $topidx ne $suppress_highlighting_file_for_this_scrollpos} {
+       highlightfile_for_scrollpos $topidx
+    }
+
+    catch {unset suppress_highlighting_file_for_this_scrollpos}
 
     .bleft.bottom.sb set $f0 $f1
     if {$searchstring ne {}} {
@@ -10509,13 +10614,13 @@ proc anctags {id} {
 # including id itself if it has a head.
 proc descheads {id} {
     global arcnos arcstart arcids archeads idheads cached_dheads
-    global allparents
+    global allparents arcout
 
     if {![info exists allparents($id)]} {
        return {}
     }
     set aret {}
-    if {[llength $arcnos($id)] == 1 && [llength $allparents($id)] == 1} {
+    if {![info exists arcout($id)]} {
        # part-way along an arc; check it first
        set a [lindex $arcnos($id) 0]
        if {$archeads($a) ne {}} {
@@ -10864,7 +10969,7 @@ proc create_prefs_page {w} {
 proc prefspage_general {notebook} {
     global NS maxwidth maxgraphpct showneartags showlocalchanges
     global tabstop limitdiffs autoselect autosellen extdifftool perfile_attrs
-    global hideremotes want_ttk have_ttk
+    global hideremotes want_ttk have_ttk maxrefs
 
     set page [create_prefs_page $notebook.general]
 
@@ -10893,9 +10998,12 @@ proc prefspage_general {notebook} {
     ${NS}::label $page.tabstopl -text [mc "Tab spacing"]
     spinbox $page.tabstop -from 1 -to 20 -width 4 -textvariable tabstop
     grid x $page.tabstopl $page.tabstop -sticky w
-    ${NS}::checkbutton $page.ntag -text [mc "Display nearby tags"] \
+    ${NS}::checkbutton $page.ntag -text [mc "Display nearby tags/heads"] \
        -variable showneartags
     grid x $page.ntag -sticky w
+    ${NS}::label $page.maxrefsl -text [mc "Maximum # tags/heads to show"]
+    spinbox $page.maxrefs -from 1 -to 1000 -width 4 -textvariable maxrefs
+    grid x $page.maxrefsl $page.maxrefs -sticky w
     ${NS}::checkbutton $page.ldiff -text [mc "Limit diffs to listed paths"] \
        -variable limitdiffs
     grid x $page.ldiff -sticky w
index 2f07a2e2d93d655cfe5cf2d3535e7b86422233c6..8cc98dc0794d6a6fae196b4fa17b922ca5394436 100644 (file)
@@ -1,40 +1,50 @@
 # Swedish translation for gitk
-# Copyright (C) 2005-2010 Paul Mackerras
+# Copyright (C) 2005-2012 Paul Mackerras
 # This file is distributed under the same license as the gitk package.
 #
-# Peter Krefting <peter@softwolves.pp.se>, 2008-2010.
 # Mikael Magnusson <mikachu@gmail.com>, 2008.
+# Peter Krefting <peter@softwolves.pp.se>, 2008, 2009, 2010, 2012.
+#
 msgid ""
 msgstr ""
 "Project-Id-Version: sv\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2010-09-12 21:14+0100\n"
-"PO-Revision-Date: 2010-09-12 21:16+0100\n"
+"POT-Creation-Date: 2012-10-03 08:09+0100\n"
+"PO-Revision-Date: 2012-10-03 08:13+0100\n"
 "Last-Translator: Peter Krefting <peter@softwolves.pp.se>\n"
 "Language-Team: Swedish <tp-sv@listor.tp-sv.se>\n"
+"Language: sv\n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit"
+"Content-Transfer-Encoding: 8bit\n"
 
-#: gitk:115
+#: gitk:140
 msgid "Couldn't get list of unmerged files:"
 msgstr "Kunde inte hämta lista Ã¶ver ej sammanslagna filer:"
 
-#: gitk:274
+#: gitk:210 gitk:2317
+msgid "Color words"
+msgstr "Färga ord"
+
+#: gitk:215 gitk:2317 gitk:7888 gitk:7921
+msgid "Markup words"
+msgstr "Märk upp ord"
+
+#: gitk:312
 msgid "Error parsing revisions:"
 msgstr "Fel vid tolkning av revisioner:"
 
-#: gitk:330
+#: gitk:368
 msgid "Error executing --argscmd command:"
 msgstr "Fel vid körning av --argscmd-kommando:"
 
-#: gitk:343
+#: gitk:381
 msgid "No files selected: --merge specified but no files are unmerged."
 msgstr ""
 "Inga filer valdes: --merge angavs men det finns inga filer som inte har "
 "slagits samman."
 
-#: gitk:346
+#: gitk:384
 msgid ""
 "No files selected: --merge specified but no unmerged files are within file "
 "limit."
@@ -42,605 +52,617 @@ msgstr ""
 "Inga filer valdes: --merge angavs men det finns inga filer inom "
 "filbegränsningen."
 
-#: gitk:368 gitk:516
+#: gitk:406 gitk:554
 msgid "Error executing git log:"
 msgstr "Fel vid körning av git log:"
 
-#: gitk:386 gitk:532
+#: gitk:424 gitk:570
 msgid "Reading"
 msgstr "Läser"
 
-#: gitk:446 gitk:4271
+#: gitk:484 gitk:4353
 msgid "Reading commits..."
 msgstr "Läser incheckningar..."
 
-#: gitk:449 gitk:1580 gitk:4274
+#: gitk:487 gitk:1625 gitk:4356
 msgid "No commits selected"
 msgstr "Inga incheckningar markerade"
 
-#: gitk:1456
+#: gitk:1499
 msgid "Can't parse git log output:"
 msgstr "Kan inte tolka utdata frÃ¥n git log:"
 
-#: gitk:1676
+#: gitk:1719
 msgid "No commit information available"
 msgstr "Ingen incheckningsinformation Ã¤r tillgänglig"
 
-#: gitk:1818
+#: gitk:1876
 msgid "mc"
 msgstr "mc"
 
-#: gitk:1853 gitk:4064 gitk:9067 gitk:10607 gitk:10817
+#: gitk:1911 gitk:4146 gitk:9282 gitk:10824 gitk:11100
 msgid "OK"
 msgstr "OK"
 
-#: gitk:1855 gitk:4066 gitk:8657 gitk:8736 gitk:8851 gitk:8900 gitk:9069
-#: gitk:10608 gitk:10818
+#: gitk:1913 gitk:4148 gitk:8871 gitk:8950 gitk:9065 gitk:9114 gitk:9284
+#: gitk:10825 gitk:11101
 msgid "Cancel"
 msgstr "Avbryt"
 
-#: gitk:1980
+#: gitk:2040
 msgid "Update"
 msgstr "Uppdatera"
 
-#: gitk:1981
+#: gitk:2041
 msgid "Reload"
 msgstr "Ladda om"
 
-#: gitk:1982
+#: gitk:2042
 msgid "Reread references"
 msgstr "Läs om referenser"
 
-#: gitk:1983
+#: gitk:2043
 msgid "List references"
 msgstr "Visa referenser"
 
-#: gitk:1985
+#: gitk:2045
 msgid "Start git gui"
 msgstr "Starta git gui"
 
-#: gitk:1987
+#: gitk:2047
 msgid "Quit"
 msgstr "Avsluta"
 
-#: gitk:1979
+#: gitk:2039
 msgid "File"
 msgstr "Arkiv"
 
-#: gitk:1991
+#: gitk:2051
 msgid "Preferences"
 msgstr "Inställningar"
 
-#: gitk:1990
+#: gitk:2050
 msgid "Edit"
 msgstr "Redigera"
 
-#: gitk:1995
+#: gitk:2055
 msgid "New view..."
 msgstr "Ny vy..."
 
-#: gitk:1996
+#: gitk:2056
 msgid "Edit view..."
 msgstr "Ändra vy..."
 
-#: gitk:1997
+#: gitk:2057
 msgid "Delete view"
 msgstr "Ta bort vy"
 
-#: gitk:1999
+#: gitk:2059
 msgid "All files"
 msgstr "Alla filer"
 
-#: gitk:1994 gitk:3817
+#: gitk:2054 gitk:3899
 msgid "View"
 msgstr "Visa"
 
-#: gitk:2004 gitk:2014 gitk:2787
+#: gitk:2064 gitk:2074 gitk:2869
 msgid "About gitk"
 msgstr "Om gitk"
 
-#: gitk:2005 gitk:2019
+#: gitk:2065 gitk:2079
 msgid "Key bindings"
 msgstr "Tangentbordsbindningar"
 
-#: gitk:2003 gitk:2018
+#: gitk:2063 gitk:2078
 msgid "Help"
 msgstr "Hjälp"
 
-#: gitk:2096 gitk:8132
+#: gitk:2156 gitk:8330
 msgid "SHA1 ID:"
 msgstr "SHA1-id:"
 
-#: gitk:2127
+#: gitk:2192
 msgid "Row"
 msgstr "Rad"
 
-#: gitk:2165
+#: gitk:2230
 msgid "Find"
 msgstr "Sök"
 
-#: gitk:2166
+#: gitk:2231
 msgid "next"
 msgstr "nästa"
 
-#: gitk:2167
+#: gitk:2232
 msgid "prev"
 msgstr "föreg"
 
-#: gitk:2168
+#: gitk:2233
 msgid "commit"
 msgstr "incheckning"
 
-#: gitk:2171 gitk:2173 gitk:4432 gitk:4455 gitk:4479 gitk:6420 gitk:6492
-#: gitk:6576
+#: gitk:2236 gitk:2238 gitk:4514 gitk:4537 gitk:4561 gitk:6528 gitk:6600
+#: gitk:6685
 msgid "containing:"
 msgstr "som innehÃ¥ller:"
 
-#: gitk:2174 gitk:3298 gitk:3303 gitk:4507
+#: gitk:2239 gitk:3381 gitk:3386 gitk:4590
 msgid "touching paths:"
 msgstr "som rör sökväg:"
 
-#: gitk:2175 gitk:4512
+#: gitk:2240 gitk:4604
 msgid "adding/removing string:"
 msgstr "som lägger/till tar bort sträng:"
 
-#: gitk:2184 gitk:2186
+#: gitk:2249 gitk:2251 gitk:4593
 msgid "Exact"
 msgstr "Exakt"
 
-#: gitk:2186 gitk:4587 gitk:6388
+#: gitk:2251 gitk:4679 gitk:6496
 msgid "IgnCase"
 msgstr "IgnVersaler"
 
-#: gitk:2186 gitk:4481 gitk:4585 gitk:6384
+#: gitk:2251 gitk:4563 gitk:4677 gitk:6492
 msgid "Regexp"
 msgstr "Reg.uttr."
 
-#: gitk:2188 gitk:2189 gitk:4606 gitk:4636 gitk:4643 gitk:6512 gitk:6580
+#: gitk:2253 gitk:2254 gitk:4699 gitk:4729 gitk:4736 gitk:6621 gitk:6689
 msgid "All fields"
 msgstr "Alla fält"
 
-#: gitk:2189 gitk:4604 gitk:4636 gitk:6451
+#: gitk:2254 gitk:4696 gitk:4729 gitk:6559
 msgid "Headline"
 msgstr "Rubrik"
 
-#: gitk:2190 gitk:4604 gitk:6451 gitk:6580 gitk:7013
+#: gitk:2255 gitk:4696 gitk:6559 gitk:6689 gitk:7126
 msgid "Comments"
 msgstr "Kommentarer"
 
-#: gitk:2190 gitk:4604 gitk:4608 gitk:4643 gitk:6451 gitk:6948 gitk:8307
-#: gitk:8322
+#: gitk:2255 gitk:4696 gitk:4701 gitk:4736 gitk:6559 gitk:7061 gitk:8505
+#: gitk:8520
 msgid "Author"
 msgstr "Författare"
 
-#: gitk:2190 gitk:4604 gitk:6451 gitk:6950
+#: gitk:2255 gitk:4696 gitk:6559 gitk:7063
 msgid "Committer"
 msgstr "Incheckare"
 
-#: gitk:2221
+#: gitk:2286
 msgid "Search"
 msgstr "Sök"
 
-#: gitk:2229
+#: gitk:2294
 msgid "Diff"
 msgstr "Diff"
 
-#: gitk:2231
+#: gitk:2296
 msgid "Old version"
 msgstr "Gammal version"
 
-#: gitk:2233
+#: gitk:2298
 msgid "New version"
 msgstr "Ny version"
 
-#: gitk:2235
+#: gitk:2300
 msgid "Lines of context"
 msgstr "Rader sammanhang"
 
-#: gitk:2245
+#: gitk:2310
 msgid "Ignore space change"
 msgstr "Ignorera Ã¤ndringar i blanksteg"
 
-#: gitk:2304
+#: gitk:2314 gitk:2316 gitk:7646 gitk:7874
+msgid "Line diff"
+msgstr "Rad-diff"
+
+#: gitk:2379
 msgid "Patch"
 msgstr "Patch"
 
-#: gitk:2306
+#: gitk:2381
 msgid "Tree"
 msgstr "Träd"
 
-#: gitk:2463 gitk:2480
+#: gitk:2540 gitk:2559
 msgid "Diff this -> selected"
 msgstr "Diff denna -> markerad"
 
-#: gitk:2464 gitk:2481
+#: gitk:2541 gitk:2560
 msgid "Diff selected -> this"
 msgstr "Diff markerad -> denna"
 
-#: gitk:2465 gitk:2482
+#: gitk:2542 gitk:2561
 msgid "Make patch"
 msgstr "Skapa patch"
 
-#: gitk:2466 gitk:8715
+#: gitk:2543 gitk:8929
 msgid "Create tag"
 msgstr "Skapa tagg"
 
-#: gitk:2467 gitk:8831
+#: gitk:2544 gitk:9045
 msgid "Write commit to file"
 msgstr "Skriv incheckning till fil"
 
-#: gitk:2468 gitk:8888
+#: gitk:2545 gitk:9102
 msgid "Create new branch"
 msgstr "Skapa ny gren"
 
-#: gitk:2469
+#: gitk:2546
 msgid "Cherry-pick this commit"
 msgstr "Plocka denna incheckning"
 
-#: gitk:2470
+#: gitk:2547
 msgid "Reset HEAD branch to here"
 msgstr "Ã…terställ HEAD-grenen hit"
 
-#: gitk:2471
+#: gitk:2548
 msgid "Mark this commit"
 msgstr "Markera denna incheckning"
 
-#: gitk:2472
+#: gitk:2549
 msgid "Return to mark"
 msgstr "Ã…tergÃ¥ till markering"
 
-#: gitk:2473
+#: gitk:2550
 msgid "Find descendant of this and mark"
 msgstr "Hitta efterföljare till denna och markera"
 
-#: gitk:2474
+#: gitk:2551
 msgid "Compare with marked commit"
 msgstr "Jämför med markerad incheckning"
 
-#: gitk:2488
+#: gitk:2552 gitk:2562
+msgid "Diff this -> marked commit"
+msgstr "Diff denna -> markerad incheckning"
+
+#: gitk:2553 gitk:2563
+msgid "Diff marked commit -> this"
+msgstr "Diff markerad incheckning -> denna"
+
+#: gitk:2569
 msgid "Check out this branch"
 msgstr "Checka ut denna gren"
 
-#: gitk:2489
+#: gitk:2570
 msgid "Remove this branch"
 msgstr "Ta bort denna gren"
 
-#: gitk:2496
+#: gitk:2577
 msgid "Highlight this too"
 msgstr "Markera Ã¤ven detta"
 
-#: gitk:2497
+#: gitk:2578
 msgid "Highlight this only"
 msgstr "Markera bara detta"
 
-#: gitk:2498
+#: gitk:2579
 msgid "External diff"
 msgstr "Extern diff"
 
-#: gitk:2499
+#: gitk:2580
 msgid "Blame parent commit"
 msgstr "Klandra föräldraincheckning"
 
-#: gitk:2506
+#: gitk:2587
 msgid "Show origin of this line"
 msgstr "Visa ursprunget för den här raden"
 
-#: gitk:2507
+#: gitk:2588
 msgid "Run git gui blame on this line"
 msgstr "Kör git gui blame pÃ¥ den här raden"
 
-#: gitk:2789
+#: gitk:2871
 msgid ""
 "\n"
 "Gitk - a commit viewer for git\n"
 "\n"
-"Copyright Â©9 2005-2010 Paul Mackerras\n"
+"Copyright Â©9 2005-2011 Paul Mackerras\n"
 "\n"
 "Use and redistribute under the terms of the GNU General Public License"
 msgstr ""
 "\n"
 "Gitk - en incheckningsvisare för git\n"
 "\n"
-"Copyright Â©9 2005-2010 Paul Mackerras\n"
+"Copyright Â©9 2005-2011 Paul Mackerras\n"
 "\n"
 "Använd och vidareförmedla enligt villkoren i GNU General Public License"
 
-#: gitk:2797 gitk:2862 gitk:9253
+#: gitk:2879 gitk:2944 gitk:9468
 msgid "Close"
 msgstr "Stäng"
 
-#: gitk:2818
+#: gitk:2900
 msgid "Gitk key bindings"
 msgstr "Tangentbordsbindningar för Gitk"
 
-#: gitk:2821
+#: gitk:2903
 msgid "Gitk key bindings:"
 msgstr "Tangentbordsbindningar för Gitk:"
 
-#: gitk:2823
+#: gitk:2905
 #, tcl-format
 msgid "<%s-Q>\t\tQuit"
 msgstr "<%s-Q>\t\tAvsluta"
 
-#: gitk:2824
+#: gitk:2906
 #, tcl-format
 msgid "<%s-W>\t\tClose window"
 msgstr "<%s-W>\t\tStäng fönster"
 
-#: gitk:2825
+#: gitk:2907
 msgid "<Home>\t\tMove to first commit"
 msgstr "<Home>\t\tGÃ¥ till första incheckning"
 
-#: gitk:2826
+#: gitk:2908
 msgid "<End>\t\tMove to last commit"
 msgstr "<End>\t\tGÃ¥ till sista incheckning"
 
-#: gitk:2827
-msgid "<Up>, p, i\tMove up one commit"
-msgstr "<Upp>, p, i\tGÃ¥ en incheckning upp"
+#: gitk:2909
+msgid "<Up>, p, k\tMove up one commit"
+msgstr "<Upp>, p, k\tGÃ¥ en incheckning upp"
 
-#: gitk:2828
-msgid "<Down>, n, k\tMove down one commit"
-msgstr "<Ned>, n, k\tGÃ¥ en incheckning ned"
+#: gitk:2910
+msgid "<Down>, n, j\tMove down one commit"
+msgstr "<Ned>, n, j\tGÃ¥ en incheckning ned"
 
-#: gitk:2829
-msgid "<Left>, z, j\tGo back in history list"
-msgstr "<Vänster>, z, j\tGÃ¥ bakÃ¥t i historiken"
+#: gitk:2911
+msgid "<Left>, z, h\tGo back in history list"
+msgstr "<Vänster>, z, h\tGÃ¥ bakÃ¥t i historiken"
 
-#: gitk:2830
+#: gitk:2912
 msgid "<Right>, x, l\tGo forward in history list"
 msgstr "<Höger>, x, l\tGÃ¥ framÃ¥t i historiken"
 
-#: gitk:2831
+#: gitk:2913
 msgid "<PageUp>\tMove up one page in commit list"
 msgstr "<PageUp>\tGÃ¥ upp en sida i incheckningslistan"
 
-#: gitk:2832
+#: gitk:2914
 msgid "<PageDown>\tMove down one page in commit list"
 msgstr "<PageDown>\tGÃ¥ ned en sida i incheckningslistan"
 
-#: gitk:2833
+#: gitk:2915
 #, tcl-format
 msgid "<%s-Home>\tScroll to top of commit list"
 msgstr "<%s-Home>\tRulla till början av incheckningslistan"
 
-#: gitk:2834
+#: gitk:2916
 #, tcl-format
 msgid "<%s-End>\tScroll to bottom of commit list"
 msgstr "<%s-End>\tRulla till slutet av incheckningslistan"
 
-#: gitk:2835
+#: gitk:2917
 #, tcl-format
 msgid "<%s-Up>\tScroll commit list up one line"
 msgstr "<%s-Upp>\tRulla incheckningslistan upp ett steg"
 
-#: gitk:2836
+#: gitk:2918
 #, tcl-format
 msgid "<%s-Down>\tScroll commit list down one line"
 msgstr "<%s-Ned>\tRulla incheckningslistan ned ett steg"
 
-#: gitk:2837
+#: gitk:2919
 #, tcl-format
 msgid "<%s-PageUp>\tScroll commit list up one page"
 msgstr "<%s-PageUp>\tRulla incheckningslistan upp en sida"
 
-#: gitk:2838
+#: gitk:2920
 #, tcl-format
 msgid "<%s-PageDown>\tScroll commit list down one page"
 msgstr "<%s-PageDown>\tRulla incheckningslistan ned en sida"
 
-#: gitk:2839
+#: gitk:2921
 msgid "<Shift-Up>\tFind backwards (upwards, later commits)"
 msgstr "<Skift-Upp>\tSök bakÃ¥t (uppÃ¥t, senare incheckningar)"
 
-#: gitk:2840
+#: gitk:2922
 msgid "<Shift-Down>\tFind forwards (downwards, earlier commits)"
 msgstr "<Skift-Ned>\tSök framÃ¥t (nedÃ¥t, tidigare incheckningar)"
 
-#: gitk:2841
+#: gitk:2923
 msgid "<Delete>, b\tScroll diff view up one page"
 msgstr "<Delete>, b\tRulla diffvisningen upp en sida"
 
-#: gitk:2842
+#: gitk:2924
 msgid "<Backspace>\tScroll diff view up one page"
 msgstr "<Baksteg>\tRulla diffvisningen upp en sida"
 
-#: gitk:2843
+#: gitk:2925
 msgid "<Space>\t\tScroll diff view down one page"
 msgstr "<Blanksteg>\tRulla diffvisningen ned en sida"
 
-#: gitk:2844
+#: gitk:2926
 msgid "u\t\tScroll diff view up 18 lines"
 msgstr "u\t\tRulla diffvisningen upp 18 rader"
 
-#: gitk:2845
+#: gitk:2927
 msgid "d\t\tScroll diff view down 18 lines"
 msgstr "d\t\tRulla diffvisningen ned 18 rader"
 
-#: gitk:2846
+#: gitk:2928
 #, tcl-format
 msgid "<%s-F>\t\tFind"
 msgstr "<%s-F>\t\tSök"
 
-#: gitk:2847
+#: gitk:2929
 #, tcl-format
 msgid "<%s-G>\t\tMove to next find hit"
 msgstr "<%s-G>\t\tGÃ¥ till nästa sökträff"
 
-#: gitk:2848
+#: gitk:2930
 msgid "<Return>\tMove to next find hit"
 msgstr "<Return>\t\tGÃ¥ till nästa sökträff"
 
-#: gitk:2849
+#: gitk:2931
 msgid "/\t\tFocus the search box"
 msgstr "/\t\tFokusera sökrutan"
 
-#: gitk:2850
+#: gitk:2932
 msgid "?\t\tMove to previous find hit"
 msgstr "?\t\tGÃ¥ till föregÃ¥ende sökträff"
 
-#: gitk:2851
+#: gitk:2933
 msgid "f\t\tScroll diff view to next file"
 msgstr "f\t\tRulla diffvisningen till nästa fil"
 
-#: gitk:2852
+#: gitk:2934
 #, tcl-format
 msgid "<%s-S>\t\tSearch for next hit in diff view"
 msgstr "<%s-S>\t\tGÃ¥ till nästa sökträff i diffvisningen"
 
-#: gitk:2853
+#: gitk:2935
 #, tcl-format
 msgid "<%s-R>\t\tSearch for previous hit in diff view"
 msgstr "<%s-R>\t\tGÃ¥ till föregÃ¥ende sökträff i diffvisningen"
 
-#: gitk:2854
+#: gitk:2936
 #, tcl-format
 msgid "<%s-KP+>\tIncrease font size"
 msgstr "<%s-Num+>\tÖka teckenstorlek"
 
-#: gitk:2855
+#: gitk:2937
 #, tcl-format
 msgid "<%s-plus>\tIncrease font size"
 msgstr "<%s-plus>\tÖka teckenstorlek"
 
-#: gitk:2856
+#: gitk:2938
 #, tcl-format
 msgid "<%s-KP->\tDecrease font size"
 msgstr "<%s-Num->\tMinska teckenstorlek"
 
-#: gitk:2857
+#: gitk:2939
 #, tcl-format
 msgid "<%s-minus>\tDecrease font size"
 msgstr "<%s-minus>\tMinska teckenstorlek"
 
-#: gitk:2858
+#: gitk:2940
 msgid "<F5>\t\tUpdate"
 msgstr "<F5>\t\tUppdatera"
 
-#: gitk:3313 gitk:3322
+#: gitk:3395 gitk:3404
 #, tcl-format
 msgid "Error creating temporary directory %s:"
 msgstr "Fel vid skapande av temporär katalog %s:"
 
-#: gitk:3335
+#: gitk:3417
 #, tcl-format
 msgid "Error getting \"%s\" from %s:"
 msgstr "Fel vid hämtning av  \"%s\" frÃ¥n %s:"
 
-#: gitk:3398
+#: gitk:3480
 msgid "command failed:"
 msgstr "kommando misslyckades:"
 
-#: gitk:3547
+#: gitk:3629
 msgid "No such commit"
 msgstr "Incheckning saknas"
 
-#: gitk:3561
+#: gitk:3643
 msgid "git gui blame: command failed:"
 msgstr "git gui blame: kommando misslyckades:"
 
-#: gitk:3592
+#: gitk:3674
 #, tcl-format
 msgid "Couldn't read merge head: %s"
 msgstr "Kunde inte läsa sammanslagningshuvud: %s"
 
-#: gitk:3600
+#: gitk:3682
 #, tcl-format
 msgid "Error reading index: %s"
 msgstr "Fel vid läsning av index: %s"
 
-#: gitk:3625
+#: gitk:3707
 #, tcl-format
 msgid "Couldn't start git blame: %s"
 msgstr "Kunde inte starta git blame: %s"
 
-#: gitk:3628 gitk:6419
+#: gitk:3710 gitk:6527
 msgid "Searching"
 msgstr "Söker"
 
-#: gitk:3660
+#: gitk:3742
 #, tcl-format
 msgid "Error running git blame: %s"
 msgstr "Fel vid körning av git blame: %s"
 
-#: gitk:3688
+#: gitk:3770
 #, tcl-format
 msgid "That line comes from commit %s,  which is not in this view"
 msgstr "Raden kommer frÃ¥n incheckningen %s, som inte finns i denna vy"
 
-#: gitk:3702
+#: gitk:3784
 msgid "External diff viewer failed:"
 msgstr "Externt diff-verktyg misslyckades:"
 
-#: gitk:3820
+#: gitk:3902
 msgid "Gitk view definition"
 msgstr "Definition av Gitk-vy"
 
-#: gitk:3824
+#: gitk:3906
 msgid "Remember this view"
 msgstr "Spara denna vy"
 
-#: gitk:3825
+#: gitk:3907
 msgid "References (space separated list):"
 msgstr "Referenser (blankstegsavdelad lista):"
 
-#: gitk:3826
+#: gitk:3908
 msgid "Branches & tags:"
 msgstr "Grenar & taggar:"
 
-#: gitk:3827
+#: gitk:3909
 msgid "All refs"
 msgstr "Alla referenser"
 
-#: gitk:3828
+#: gitk:3910
 msgid "All (local) branches"
 msgstr "Alla (lokala) grenar"
 
-#: gitk:3829
+#: gitk:3911
 msgid "All tags"
 msgstr "Alla taggar"
 
-#: gitk:3830
+#: gitk:3912
 msgid "All remote-tracking branches"
 msgstr "Alla fjärrspÃ¥rande grenar"
 
-#: gitk:3831
+#: gitk:3913
 msgid "Commit Info (regular expressions):"
 msgstr "Incheckningsinfo (reguljära uttryck):"
 
-#: gitk:3832
+#: gitk:3914
 msgid "Author:"
 msgstr "Författare:"
 
-#: gitk:3833
+#: gitk:3915
 msgid "Committer:"
 msgstr "Incheckare:"
 
-#: gitk:3834
+#: gitk:3916
 msgid "Commit Message:"
 msgstr "Incheckningsmeddelande:"
 
-#: gitk:3835
+#: gitk:3917
 msgid "Matches all Commit Info criteria"
 msgstr "Motsvarar alla kriterier för incheckningsinfo"
 
-#: gitk:3836
+#: gitk:3918
 msgid "Changes to Files:"
 msgstr "Ändringar av filer:"
 
-#: gitk:3837
+#: gitk:3919
 msgid "Fixed String"
 msgstr "Fast sträng"
 
-#: gitk:3838
+#: gitk:3920
 msgid "Regular Expression"
 msgstr "Reguljärt uttryck"
 
-#: gitk:3839
+#: gitk:3921
 msgid "Search string:"
 msgstr "Söksträng:"
 
-#: gitk:3840
+#: gitk:3922
 msgid ""
 "Commit Dates (\"2 weeks ago\", \"2009-03-17 15:27:38\", \"March 17, 2009 "
 "15:27:38\"):"
@@ -648,201 +670,197 @@ msgstr ""
 "Incheckingsdatum (\"2 weeks ago\", \"2009-03-17 15:27:38\", \"March 17, 2009 "
 "15:27:38\"):"
 
-#: gitk:3841
+#: gitk:3923
 msgid "Since:"
 msgstr "FrÃ¥n:"
 
-#: gitk:3842
+#: gitk:3924
 msgid "Until:"
 msgstr "Till:"
 
-#: gitk:3843
+#: gitk:3925
 msgid "Limit and/or skip a number of revisions (positive integer):"
 msgstr "Begränsa och/eller hoppa Ã¶ver ett antal revisioner (positivt heltal):"
 
-#: gitk:3844
+#: gitk:3926
 msgid "Number to show:"
 msgstr "Antal att visa:"
 
-#: gitk:3845
+#: gitk:3927
 msgid "Number to skip:"
 msgstr "Antal att hoppa Ã¶ver:"
 
-#: gitk:3846
+#: gitk:3928
 msgid "Miscellaneous options:"
 msgstr "Diverse alternativ:"
 
-#: gitk:3847
+#: gitk:3929
 msgid "Strictly sort by date"
 msgstr "Strikt datumsortering"
 
-#: gitk:3848
+#: gitk:3930
 msgid "Mark branch sides"
 msgstr "Markera sidogrenar"
 
-#: gitk:3849
+#: gitk:3931
 msgid "Limit to first parent"
 msgstr "Begränsa till första förälder"
 
-#: gitk:3850
+#: gitk:3932
 msgid "Simple history"
 msgstr "Enkel historik"
 
-#: gitk:3851
+#: gitk:3933
 msgid "Additional arguments to git log:"
 msgstr "Ytterligare argument till git log:"
 
-#: gitk:3852
+#: gitk:3934
 msgid "Enter files and directories to include, one per line:"
 msgstr "Ange filer och kataloger att ta med, en per rad:"
 
-#: gitk:3853
+#: gitk:3935
 msgid "Command to generate more commits to include:"
 msgstr "Kommando för att generera fler incheckningar att ta med:"
 
-#: gitk:3977
+#: gitk:4059
 msgid "Gitk: edit view"
 msgstr "Gitk: redigera vy"
 
-#: gitk:3985
+#: gitk:4067
 msgid "-- criteria for selecting revisions"
 msgstr " - kriterier för val av revisioner"
 
-#: gitk:3990
+#: gitk:4072
 msgid "View Name"
 msgstr "Namn pÃ¥ vy"
 
-#: gitk:4065
+#: gitk:4147
 msgid "Apply (F5)"
 msgstr "Använd (F5)"
 
-#: gitk:4103
+#: gitk:4185
 msgid "Error in commit selection arguments:"
 msgstr "Fel i argument för val av incheckningar:"
 
-#: gitk:4156 gitk:4208 gitk:4656 gitk:4670 gitk:5931 gitk:11551 gitk:11552
+#: gitk:4238 gitk:4290 gitk:4749 gitk:4763 gitk:6027 gitk:11849 gitk:11850
 msgid "None"
 msgstr "Inget"
 
-#: gitk:4604 gitk:6451 gitk:8309 gitk:8324
-msgid "Date"
-msgstr "Datum"
-
-#: gitk:4604 gitk:6451
-msgid "CDate"
-msgstr "Skapat datum"
-
-#: gitk:4753 gitk:4758
+#: gitk:4846 gitk:4851
 msgid "Descendant"
 msgstr "Avkomling"
 
-#: gitk:4754
+#: gitk:4847
 msgid "Not descendant"
 msgstr "Inte avkomling"
 
-#: gitk:4761 gitk:4766
+#: gitk:4854 gitk:4859
 msgid "Ancestor"
 msgstr "Förfader"
 
-#: gitk:4762
+#: gitk:4855
 msgid "Not ancestor"
 msgstr "Inte förfader"
 
-#: gitk:5052
+#: gitk:5145
 msgid "Local changes checked in to index but not committed"
 msgstr "Lokala Ã¤ndringar sparade i indexet men inte incheckade"
 
-#: gitk:5088
+#: gitk:5181
 msgid "Local uncommitted changes, not checked in to index"
 msgstr "Lokala Ã¤ndringar, ej sparade i indexet"
 
-#: gitk:6769
+#: gitk:6882
 msgid "many"
 msgstr "mÃ¥nga"
 
-#: gitk:6952
+#: gitk:7065
 msgid "Tags:"
 msgstr "Taggar:"
 
-#: gitk:6969 gitk:6975 gitk:8302
+#: gitk:7082 gitk:7088 gitk:8500
 msgid "Parent"
 msgstr "Förälder"
 
-#: gitk:6980
+#: gitk:7093
 msgid "Child"
 msgstr "Barn"
 
-#: gitk:6989
+#: gitk:7102
 msgid "Branch"
 msgstr "Gren"
 
-#: gitk:6992
+#: gitk:7105
 msgid "Follows"
 msgstr "Följer"
 
-#: gitk:6995
+#: gitk:7108
 msgid "Precedes"
 msgstr "FöregÃ¥r"
 
-#: gitk:7532
+#: gitk:7653
 #, tcl-format
 msgid "Error getting diffs: %s"
 msgstr "Fel vid hämtning av diff: %s"
 
-#: gitk:8130
+#: gitk:8328
 msgid "Goto:"
 msgstr "GÃ¥ till:"
 
-#: gitk:8151
+#: gitk:8349
 #, tcl-format
 msgid "Short SHA1 id %s is ambiguous"
 msgstr "Förkortat SHA1-id %s Ã¤r tvetydigt"
 
-#: gitk:8158
+#: gitk:8356
 #, tcl-format
 msgid "Revision %s is not known"
 msgstr "Revisionen %s Ã¤r inte känd"
 
-#: gitk:8168
+#: gitk:8366
 #, tcl-format
 msgid "SHA1 id %s is not known"
 msgstr "SHA-id:t %s Ã¤r inte känt"
 
-#: gitk:8170
+#: gitk:8368
 #, tcl-format
 msgid "Revision %s is not in the current view"
 msgstr "Revisionen %s finns inte i den nuvarande vyn"
 
-#: gitk:8312
+#: gitk:8507 gitk:8522
+msgid "Date"
+msgstr "Datum"
+
+#: gitk:8510
 msgid "Children"
 msgstr "Barn"
 
-#: gitk:8370
+#: gitk:8573
 #, tcl-format
 msgid "Reset %s branch to here"
 msgstr "Ã…terställ grenen %s hit"
 
-#: gitk:8372
+#: gitk:8575
 msgid "Detached head: can't reset"
 msgstr "FrÃ¥nkopplad head: kan inte Ã¥terställa"
 
-#: gitk:8481 gitk:8487
+#: gitk:8680 gitk:8686
 msgid "Skipping merge commit "
 msgstr "Hoppar Ã¶ver sammanslagningsincheckning "
 
-#: gitk:8496 gitk:8501
+#: gitk:8695 gitk:8700
 msgid "Error getting patch ID for "
 msgstr "Fel vid hämtning av patch-id för "
 
-#: gitk:8497 gitk:8502
+#: gitk:8696 gitk:8701
 msgid " - stopping\n"
 msgstr " - stannar\n"
 
-#: gitk:8507 gitk:8510 gitk:8518 gitk:8532 gitk:8541
+#: gitk:8706 gitk:8709 gitk:8717 gitk:8731 gitk:8740
 msgid "Commit "
 msgstr "Incheckning "
 
-#: gitk:8511
+#: gitk:8710
 msgid ""
 " is the same patch as\n"
 "       "
@@ -850,7 +868,7 @@ msgstr ""
 " Ã¤r samma patch som\n"
 "       "
 
-#: gitk:8519
+#: gitk:8718
 msgid ""
 " differs from\n"
 "       "
@@ -858,7 +876,7 @@ msgstr ""
 " skiljer sig frÃ¥n\n"
 "       "
 
-#: gitk:8521
+#: gitk:8720
 msgid ""
 "Diff of commits:\n"
 "\n"
@@ -866,131 +884,131 @@ msgstr ""
 "Skillnad mellan incheckningar:\n"
 "\n"
 
-#: gitk:8533 gitk:8542
+#: gitk:8732 gitk:8741
 #, tcl-format
 msgid " has %s children - stopping\n"
 msgstr " har %s barn - stannar\n"
 
-#: gitk:8561
+#: gitk:8760
 #, tcl-format
 msgid "Error writing commit to file: %s"
 msgstr "Fel vid skrivning av incheckning till fil: %s"
 
-#: gitk:8567
+#: gitk:8766
 #, tcl-format
 msgid "Error diffing commits: %s"
 msgstr "Fel vid jämförelse av incheckningar: %s"
 
-#: gitk:8598
+#: gitk:8812
 msgid "Top"
 msgstr "Topp"
 
-#: gitk:8599
+#: gitk:8813
 msgid "From"
 msgstr "FrÃ¥n"
 
-#: gitk:8604
+#: gitk:8818
 msgid "To"
 msgstr "Till"
 
-#: gitk:8628
+#: gitk:8842
 msgid "Generate patch"
 msgstr "Generera patch"
 
-#: gitk:8630
+#: gitk:8844
 msgid "From:"
 msgstr "FrÃ¥n:"
 
-#: gitk:8639
+#: gitk:8853
 msgid "To:"
 msgstr "Till:"
 
-#: gitk:8648
+#: gitk:8862
 msgid "Reverse"
 msgstr "Vänd"
 
-#: gitk:8650 gitk:8845
+#: gitk:8864 gitk:9059
 msgid "Output file:"
 msgstr "Utdatafil:"
 
-#: gitk:8656
+#: gitk:8870
 msgid "Generate"
 msgstr "Generera"
 
-#: gitk:8694
+#: gitk:8908
 msgid "Error creating patch:"
 msgstr "Fel vid generering av patch:"
 
-#: gitk:8717 gitk:8833 gitk:8890
+#: gitk:8931 gitk:9047 gitk:9104
 msgid "ID:"
 msgstr "Id:"
 
-#: gitk:8726
+#: gitk:8940
 msgid "Tag name:"
 msgstr "Taggnamn:"
 
-#: gitk:8729
+#: gitk:8943
 msgid "Tag message is optional"
 msgstr "Taggmeddelandet Ã¤r valfritt"
 
-#: gitk:8731
+#: gitk:8945
 msgid "Tag message:"
 msgstr "Taggmeddelande:"
 
-#: gitk:8735 gitk:8899
+#: gitk:8949 gitk:9113
 msgid "Create"
 msgstr "Skapa"
 
-#: gitk:8753
+#: gitk:8967
 msgid "No tag name specified"
 msgstr "Inget taggnamn angavs"
 
-#: gitk:8757
+#: gitk:8971
 #, tcl-format
 msgid "Tag \"%s\" already exists"
 msgstr "Taggen \"%s\" finns redan"
 
-#: gitk:8767
+#: gitk:8981
 msgid "Error creating tag:"
 msgstr "Fel vid skapande av tagg:"
 
-#: gitk:8842
+#: gitk:9056
 msgid "Command:"
 msgstr "Kommando:"
 
-#: gitk:8850
+#: gitk:9064
 msgid "Write"
 msgstr "Skriv"
 
-#: gitk:8868
+#: gitk:9082
 msgid "Error writing commit:"
 msgstr "Fel vid skrivning av incheckning:"
 
-#: gitk:8895
+#: gitk:9109
 msgid "Name:"
 msgstr "Namn:"
 
-#: gitk:8918
+#: gitk:9132
 msgid "Please specify a name for the new branch"
 msgstr "Ange ett namn för den nya grenen"
 
-#: gitk:8923
+#: gitk:9137
 #, tcl-format
 msgid "Branch '%s' already exists. Overwrite?"
 msgstr "Grenen \"%s\" finns redan. Skriva Ã¶ver?"
 
-#: gitk:8989
+#: gitk:9204
 #, tcl-format
 msgid "Commit %s is already included in branch %s -- really re-apply it?"
 msgstr ""
 "Incheckningen %s finns redan pÃ¥ grenen %s -- skall den verkligen appliceras "
 "pÃ¥ nytt?"
 
-#: gitk:8994
+#: gitk:9209
 msgid "Cherry-picking"
 msgstr "Plockar"
 
-#: gitk:9003
+#: gitk:9218
 #, tcl-format
 msgid ""
 "Cherry-pick failed because of local changes to file '%s'.\n"
@@ -1000,7 +1018,7 @@ msgstr ""
 "Checka in, Ã¥terställ eller spara undan (stash) dina Ã¤ndringar och försök "
 "igen."
 
-#: gitk:9009
+#: gitk:9224
 msgid ""
 "Cherry-pick failed because of merge conflict.\n"
 "Do you wish to run git citool to resolve it?"
@@ -1008,32 +1026,32 @@ msgstr ""
 "Cherry-pick misslyckades pÃ¥ grund av en sammanslagningskonflikt.\n"
 "Vill du köra git citool för att lösa den?"
 
-#: gitk:9025
+#: gitk:9240
 msgid "No changes committed"
 msgstr "Inga Ã¤ndringar incheckade"
 
-#: gitk:9051
+#: gitk:9266
 msgid "Confirm reset"
 msgstr "Bekräfta Ã¥terställning"
 
-#: gitk:9053
+#: gitk:9268
 #, tcl-format
 msgid "Reset branch %s to %s?"
 msgstr "Ã…terställa grenen %s till %s?"
 
-#: gitk:9055
+#: gitk:9270
 msgid "Reset type:"
 msgstr "Typ av Ã¥terställning:"
 
-#: gitk:9058
+#: gitk:9273
 msgid "Soft: Leave working tree and index untouched"
 msgstr "Mjuk: Rör inte utcheckning och index"
 
-#: gitk:9061
+#: gitk:9276
 msgid "Mixed: Leave working tree untouched, reset index"
 msgstr "Blandad: Rör inte utcheckning, Ã¥terställ index"
 
-#: gitk:9064
+#: gitk:9279
 msgid ""
 "Hard: Reset working tree and index\n"
 "(discard ALL local changes)"
@@ -1041,19 +1059,19 @@ msgstr ""
 "HÃ¥rd: Ã…terställ utcheckning och index\n"
 "(förkastar ALLA lokala Ã¤ndringar)"
 
-#: gitk:9081
+#: gitk:9296
 msgid "Resetting"
 msgstr "Ã…terställer"
 
-#: gitk:9141
+#: gitk:9356
 msgid "Checking out"
 msgstr "Checkar ut"
 
-#: gitk:9194
+#: gitk:9409
 msgid "Cannot delete the currently checked-out branch"
 msgstr "Kan inte ta bort den just nu utcheckade grenen"
 
-#: gitk:9200
+#: gitk:9415
 #, tcl-format
 msgid ""
 "The commits on branch %s aren't on any other branch.\n"
@@ -1062,16 +1080,16 @@ msgstr ""
 "Incheckningarna pÃ¥ grenen %s existerar inte pÃ¥ nÃ¥gon annan gren.\n"
 "Vill du verkligen ta bort grenen %s?"
 
-#: gitk:9231
+#: gitk:9446
 #, tcl-format
 msgid "Tags and heads: %s"
 msgstr "Taggar och huvuden: %s"
 
-#: gitk:9246
+#: gitk:9461
 msgid "Filter"
 msgstr "Filter"
 
-#: gitk:9541
+#: gitk:9757
 msgid ""
 "Error reading commit topology information; branch and preceding/following "
 "tag information will be incomplete."
@@ -1079,206 +1097,219 @@ msgstr ""
 "Fel vid läsning av information om incheckningstopologi; information om "
 "grenar och föregÃ¥ende/senare taggar kommer inte vara komplett."
 
-#: gitk:10527
+#: gitk:10744
 msgid "Tag"
 msgstr "Tagg"
 
-#: gitk:10527
+#: gitk:10744
 msgid "Id"
 msgstr "Id"
 
-#: gitk:10576
+#: gitk:10793
 msgid "Gitk font chooser"
 msgstr "Teckensnittsväljare för Gitk"
 
-#: gitk:10593
+#: gitk:10810
 msgid "B"
 msgstr "F"
 
-#: gitk:10596
+#: gitk:10813
 msgid "I"
 msgstr "K"
 
-#: gitk:10714
-msgid "Gitk preferences"
-msgstr "Inställningar för Gitk"
-
-#: gitk:10716
+#: gitk:10931
 msgid "Commit list display options"
 msgstr "Alternativ för incheckningslistvy"
 
-#: gitk:10719
+#: gitk:10934
 msgid "Maximum graph width (lines)"
 msgstr "Maximal grafbredd (rader)"
 
-#: gitk:10722
+#: gitk:10937
 #, tcl-format
 msgid "Maximum graph width (% of pane)"
 msgstr "Maximal grafbredd (% av ruta)"
 
-#: gitk:10725
+#: gitk:10940
 msgid "Show local changes"
 msgstr "Visa lokala Ã¤ndringar"
 
-#: gitk:10728
-msgid "Auto-select SHA1"
-msgstr "Välj SHA1 automatiskt"
+#: gitk:10943
+msgid "Auto-select SHA1 (length)"
+msgstr "Välj SHA1 (längd) automatiskt"
 
-#: gitk:10731
+#: gitk:10947
 msgid "Hide remote refs"
 msgstr "Dölj fjärr-referenser"
 
-#: gitk:10735
+#: gitk:10951
 msgid "Diff display options"
 msgstr "Alternativ för diffvy"
 
-#: gitk:10737
+#: gitk:10953
 msgid "Tab spacing"
 msgstr "Blanksteg för tabulatortecken"
 
-#: gitk:10740
+#: gitk:10956
 msgid "Display nearby tags"
 msgstr "Visa närliggande taggar"
 
-#: gitk:10743
+#: gitk:10959
 msgid "Limit diffs to listed paths"
 msgstr "Begränsa diff till listade sökvägar"
 
-#: gitk:10746
+#: gitk:10962
 msgid "Support per-file encodings"
 msgstr "Stöd för filspecifika teckenkodningar"
 
-#: gitk:10752 gitk:10832
+#: gitk:10968 gitk:11115
 msgid "External diff tool"
 msgstr "Externt diff-verktyg"
 
-#: gitk:10753
+#: gitk:10969
 msgid "Choose..."
 msgstr "Välj..."
 
-#: gitk:10758
+#: gitk:10974
 msgid "General options"
 msgstr "Allmänna inställningar"
 
-#: gitk:10761
+#: gitk:10977
 msgid "Use themed widgets"
 msgstr "Använd tema pÃ¥ fönsterelement"
 
-#: gitk:10763
+#: gitk:10979
 msgid "(change requires restart)"
 msgstr "(ändringen kräver omstart)"
 
-#: gitk:10765
+#: gitk:10981
 msgid "(currently unavailable)"
 msgstr "(för närvarande inte tillgängligt)"
 
-#: gitk:10769
+#: gitk:10992
 msgid "Colors: press to choose"
 msgstr "Färger: tryck för att välja"
 
-#: gitk:10772
+#: gitk:10995
 msgid "Interface"
 msgstr "Gränssnitt"
 
-#: gitk:10773
+#: gitk:10996
 msgid "interface"
 msgstr "gränssnitt"
 
-#: gitk:10776
+#: gitk:10999
 msgid "Background"
 msgstr "Bakgrund"
 
-#: gitk:10777 gitk:10807
+#: gitk:11000 gitk:11030
 msgid "background"
 msgstr "bakgrund"
 
-#: gitk:10780
+#: gitk:11003
 msgid "Foreground"
 msgstr "Förgrund"
 
-#: gitk:10781
+#: gitk:11004
 msgid "foreground"
 msgstr "förgrund"
 
-#: gitk:10784
+#: gitk:11007
 msgid "Diff: old lines"
 msgstr "Diff: gamla rader"
 
-#: gitk:10785
+#: gitk:11008
 msgid "diff old lines"
 msgstr "diff gamla rader"
 
-#: gitk:10789
+#: gitk:11012
 msgid "Diff: new lines"
 msgstr "Diff: nya rader"
 
-#: gitk:10790
+#: gitk:11013
 msgid "diff new lines"
 msgstr "diff nya rader"
 
-#: gitk:10794
+#: gitk:11017
 msgid "Diff: hunk header"
 msgstr "Diff: delhuvud"
 
-#: gitk:10796
+#: gitk:11019
 msgid "diff hunk header"
 msgstr "diff delhuvud"
 
-#: gitk:10800
+#: gitk:11023
 msgid "Marked line bg"
 msgstr "Markerad rad bakgrund"
 
-#: gitk:10802
+#: gitk:11025
 msgid "marked line background"
 msgstr "markerad rad bakgrund"
 
-#: gitk:10806
+#: gitk:11029
 msgid "Select bg"
 msgstr "Markerad bakgrund"
 
-#: gitk:10810
+#: gitk:11038
 msgid "Fonts: press to choose"
 msgstr "Teckensnitt: tryck för att välja"
 
-#: gitk:10812
+#: gitk:11040
 msgid "Main font"
 msgstr "Huvudteckensnitt"
 
-#: gitk:10813
+#: gitk:11041
 msgid "Diff display font"
 msgstr "Teckensnitt för diffvisning"
 
-#: gitk:10814
+#: gitk:11042
 msgid "User interface font"
 msgstr "Teckensnitt för användargränssnitt"
 
-#: gitk:10842
+#: gitk:11064
+msgid "Gitk preferences"
+msgstr "Inställningar för Gitk"
+
+#: gitk:11073
+msgid "General"
+msgstr "Allmänt"
+
+#: gitk:11074
+msgid "Colors"
+msgstr "Färger"
+
+#: gitk:11075
+msgid "Fonts"
+msgstr "Teckensnitt"
+
+#: gitk:11125
 #, tcl-format
 msgid "Gitk: choose color for %s"
 msgstr "Gitk: välj färg för %s"
 
-#: gitk:11445
+#: gitk:11745
 msgid "Cannot find a git repository here."
 msgstr "Hittar inget git-arkiv här."
 
-#: gitk:11449
-#, tcl-format
-msgid "Cannot find the git directory \"%s\"."
-msgstr "Hittar inte git-katalogen \"%s\"."
-
-#: gitk:11496
+#: gitk:11792
 #, tcl-format
 msgid "Ambiguous argument '%s': both revision and filename"
 msgstr "Tvetydigt argument \"%s\": bÃ¥de revision och filnamn"
 
-#: gitk:11508
+#: gitk:11804
 msgid "Bad arguments to gitk:"
 msgstr "Felaktiga argument till gitk:"
 
-#: gitk:11604
+#: gitk:11907
 msgid "Command line"
 msgstr "Kommandorad"
 
+#~ msgid "CDate"
+#~ msgstr "Skapat datum"
+
+#~ msgid "Cannot find the git directory \"%s\"."
+#~ msgstr "Hittar inte git-katalogen \"%s\"."
+
 #~ msgid "SHA1 ID: "
 #~ msgstr "SHA1-id: "
 
index e521e2fd223766eee22705674dd3becbb3d52a57..21dc20b57d392c5789043968d28a3b958b2b50c7 100644 (file)
@@ -33,46 +33,6 @@ typedef void *SSL;
 #include <openssl/hmac.h>
 #endif
 
-struct store_conf {
-       char *name;
-       const char *path; /* should this be here? its interpretation is driver-specific */
-       char *map_inbox;
-       char *trash;
-       unsigned max_size; /* off_t is overkill */
-       unsigned trash_remote_new:1, trash_only_new:1;
-};
-
-/* For message->status */
-#define M_RECENT       (1<<0) /* unsyncable flag; maildir_* depend on this being 1<<0 */
-#define M_DEAD         (1<<1) /* expunged */
-#define M_FLAGS        (1<<2) /* flags fetched */
-
-struct message {
-       struct message *next;
-       size_t size; /* zero implies "not fetched" */
-       int uid;
-       unsigned char flags, status;
-};
-
-struct store {
-       struct store_conf *conf; /* foreign */
-
-       /* currently open mailbox */
-       const char *name; /* foreign! maybe preset? */
-       char *path; /* own */
-       struct message *msgs; /* own */
-       int uidvalidity;
-       unsigned char opts; /* maybe preset? */
-       /* note that the following do _not_ reflect stats from msgs, but mailbox totals */
-       int count; /* # of messages */
-       int recent; /* # of recent messages - don't trust this beyond the initial read */
-};
-
-struct msg_data {
-       struct strbuf data;
-       unsigned char flags;
-};
-
 static const char imap_send_usage[] = "git imap-send < <mbox>";
 
 #undef DRV_OK
@@ -90,8 +50,6 @@ static void imap_warn(const char *, ...);
 
 static char *next_arg(char **);
 
-static void free_generic_messages(struct message *);
-
 __attribute__((format (printf, 3, 4)))
 static int nfsnprintf(char *buf, int blen, const char *fmt, ...);
 
@@ -135,20 +93,6 @@ static struct imap_server_conf server = {
        NULL,   /* auth_method */
 };
 
-struct imap_store_conf {
-       struct store_conf gen;
-       struct imap_server_conf *server;
-};
-
-#define NIL    (void *)0x1
-#define LIST   (void *)0x2
-
-struct imap_list {
-       struct imap_list *next, *child;
-       char *val;
-       int len;
-};
-
 struct imap_socket {
        int fd[2];
        SSL *ssl;
@@ -165,7 +109,6 @@ struct imap_cmd;
 
 struct imap {
        int uidnext; /* from SELECT responses */
-       struct imap_list *ns_personal, *ns_other, *ns_shared; /* NAMESPACE info */
        unsigned caps, rcaps; /* CAPABILITY results */
        /* command queue */
        int nexttag, num_in_progress, literal_pending;
@@ -174,11 +117,11 @@ struct imap {
 };
 
 struct imap_store {
-       struct store gen;
+       /* currently open mailbox */
+       const char *name; /* foreign! maybe preset? */
        int uidvalidity;
        struct imap *imap;
        const char *prefix;
-       unsigned /*currentnc:1,*/ trashnc:1;
 };
 
 struct imap_cmd_cb {
@@ -225,14 +168,6 @@ static const char *cap_list[] = {
 static int get_cmd_result(struct imap_store *ctx, struct imap_cmd *tcmd);
 
 
-static const char *Flags[] = {
-       "Draft",
-       "Flagged",
-       "Answered",
-       "Seen",
-       "Deleted",
-};
-
 #ifndef NO_OPENSSL
 static void ssl_socket_perror(const char *func)
 {
@@ -476,16 +411,6 @@ static char *next_arg(char **s)
        return ret;
 }
 
-static void free_generic_messages(struct message *msgs)
-{
-       struct message *tmsg;
-
-       for (; msgs; msgs = tmsg) {
-               tmsg = msgs->next;
-               free(msgs);
-       }
-}
-
 static int nfsnprintf(char *buf, int blen, const char *fmt, ...)
 {
        int ret;
@@ -613,35 +538,9 @@ static int imap_exec_m(struct imap_store *ctx, struct imap_cmd_cb *cb,
        }
 }
 
-static int is_atom(struct imap_list *list)
-{
-       return list && list->val && list->val != NIL && list->val != LIST;
-}
-
-static int is_list(struct imap_list *list)
-{
-       return list && list->val == LIST;
-}
-
-static void free_list(struct imap_list *list)
-{
-       struct imap_list *tmp;
-
-       for (; list; list = tmp) {
-               tmp = list->next;
-               if (is_list(list))
-                       free_list(list->child);
-               else if (is_atom(list))
-                       free(list->val);
-               free(list);
-       }
-}
-
-static int parse_imap_list_l(struct imap *imap, char **sp, struct imap_list **curp, int level)
+static int skip_imap_list_l(char **sp, int level)
 {
-       struct imap_list *cur;
-       char *s = *sp, *p;
-       int n, bytes;
+       char *s = *sp;
 
        for (;;) {
                while (isspace((unsigned char)*s))
@@ -650,68 +549,23 @@ static int parse_imap_list_l(struct imap *imap, char **sp, struct imap_list **cu
                        s++;
                        break;
                }
-               *curp = cur = xmalloc(sizeof(*cur));
-               curp = &cur->next;
-               cur->val = NULL; /* for clean bail */
                if (*s == '(') {
                        /* sublist */
                        s++;
-                       cur->val = LIST;
-                       if (parse_imap_list_l(imap, &s, &cur->child, level + 1))
-                               goto bail;
-               } else if (imap && *s == '{') {
-                       /* literal */
-                       bytes = cur->len = strtol(s + 1, &s, 10);
-                       if (*s != '}')
-                               goto bail;
-
-                       s = cur->val = xmalloc(cur->len);
-
-                       /* dump whats left over in the input buffer */
-                       n = imap->buf.bytes - imap->buf.offset;
-
-                       if (n > bytes)
-                               /* the entire message fit in the buffer */
-                               n = bytes;
-
-                       memcpy(s, imap->buf.buf + imap->buf.offset, n);
-                       s += n;
-                       bytes -= n;
-
-                       /* mark that we used part of the buffer */
-                       imap->buf.offset += n;
-
-                       /* now read the rest of the message */
-                       while (bytes > 0) {
-                               if ((n = socket_read(&imap->buf.sock, s, bytes)) <= 0)
-                                       goto bail;
-                               s += n;
-                               bytes -= n;
-                       }
-
-                       if (buffer_gets(&imap->buf, &s))
+                       if (skip_imap_list_l(&s, level + 1))
                                goto bail;
                } else if (*s == '"') {
                        /* quoted string */
                        s++;
-                       p = s;
                        for (; *s != '"'; s++)
                                if (!*s)
                                        goto bail;
-                       cur->len = s - p;
                        s++;
-                       cur->val = xmemdupz(p, cur->len);
                } else {
                        /* atom */
-                       p = s;
                        for (; *s && !isspace((unsigned char)*s); s++)
                                if (level && *s == ')')
                                        break;
-                       cur->len = s - p;
-                       if (cur->len == 3 && !memcmp("NIL", p, 3))
-                               cur->val = NIL;
-                       else
-                               cur->val = xmemdupz(p, cur->len);
                }
 
                if (!level)
@@ -720,27 +574,15 @@ static int parse_imap_list_l(struct imap *imap, char **sp, struct imap_list **cu
                        goto bail;
        }
        *sp = s;
-       *curp = NULL;
        return 0;
 
 bail:
-       *curp = NULL;
        return -1;
 }
 
-static struct imap_list *parse_imap_list(struct imap *imap, char **sp)
+static void skip_list(char **sp)
 {
-       struct imap_list *head;
-
-       if (!parse_imap_list_l(imap, sp, &head, 0))
-               return head;
-       free_list(head);
-       return NULL;
-}
-
-static struct imap_list *parse_list(char **sp)
-{
-       return parse_imap_list(NULL, sp);
+       skip_imap_list_l(sp, 0);
 }
 
 static void parse_capability(struct imap *imap, char *cmd)
@@ -772,7 +614,7 @@ static int parse_response_code(struct imap_store *ctx, struct imap_cmd_cb *cb,
        *p++ = 0;
        arg = next_arg(&s);
        if (!strcmp("UIDVALIDITY", arg)) {
-               if (!(arg = next_arg(&s)) || !(ctx->gen.uidvalidity = atoi(arg))) {
+               if (!(arg = next_arg(&s)) || !(ctx->uidvalidity = atoi(arg))) {
                        fprintf(stderr, "IMAP error: malformed UIDVALIDITY status\n");
                        return RESP_BAD;
                }
@@ -790,7 +632,7 @@ static int parse_response_code(struct imap_store *ctx, struct imap_cmd_cb *cb,
                for (; isspace((unsigned char)*p); p++);
                fprintf(stderr, "*** IMAP ALERT *** %s\n", p);
        } else if (cb && cb->ctx && !strcmp("APPENDUID", arg)) {
-               if (!(arg = next_arg(&s)) || !(ctx->gen.uidvalidity = atoi(arg)) ||
+               if (!(arg = next_arg(&s)) || !(ctx->uidvalidity = atoi(arg)) ||
                    !(arg = next_arg(&s)) || !(*(int *)cb->ctx = atoi(arg))) {
                        fprintf(stderr, "IMAP error: malformed APPENDUID status\n");
                        return RESP_BAD;
@@ -819,20 +661,28 @@ static int get_cmd_result(struct imap_store *ctx, struct imap_cmd *tcmd)
                        }
 
                        if (!strcmp("NAMESPACE", arg)) {
-                               imap->ns_personal = parse_list(&cmd);
-                               imap->ns_other = parse_list(&cmd);
-                               imap->ns_shared = parse_list(&cmd);
+                               /* rfc2342 NAMESPACE response. */
+                               skip_list(&cmd); /* Personal mailboxes */
+                               skip_list(&cmd); /* Others' mailboxes */
+                               skip_list(&cmd); /* Shared mailboxes */
                        } else if (!strcmp("OK", arg) || !strcmp("BAD", arg) ||
                                   !strcmp("NO", arg) || !strcmp("BYE", arg)) {
                                if ((resp = parse_response_code(ctx, NULL, cmd)) != RESP_OK)
                                        return resp;
-                       } else if (!strcmp("CAPABILITY", arg))
+                       } else if (!strcmp("CAPABILITY", arg)) {
                                parse_capability(imap, cmd);
-                       else if ((arg1 = next_arg(&cmd))) {
-                               if (!strcmp("EXISTS", arg1))
-                                       ctx->gen.count = atoi(arg);
-                               else if (!strcmp("RECENT", arg1))
-                                       ctx->gen.recent = atoi(arg);
+                       } else if ((arg1 = next_arg(&cmd))) {
+                               ; /*
+                                  * Unhandled response-data with at least two words.
+                                  * Ignore it.
+                                  *
+                                  * NEEDSWORK: Previously this case handled '<num> EXISTS'
+                                  * and '<num> RECENT' but as a probably-unintended side
+                                  * effect it ignores other unrecognized two-word
+                                  * responses.  imap-send doesn't ever try to read
+                                  * messages or mailboxes these days, so consider
+                                  * eliminating this case.
+                                  */
                        } else {
                                fprintf(stderr, "IMAP error: unable to parse untagged response\n");
                                return RESP_BAD;
@@ -934,16 +784,12 @@ static void imap_close_server(struct imap_store *ictx)
                imap_exec(ictx, NULL, "LOGOUT");
                socket_shutdown(&imap->buf.sock);
        }
-       free_list(imap->ns_personal);
-       free_list(imap->ns_other);
-       free_list(imap->ns_shared);
        free(imap);
 }
 
-static void imap_close_store(struct store *ctx)
+static void imap_close_store(struct imap_store *ctx)
 {
-       imap_close_server((struct imap_store *)ctx);
-       free_generic_messages(ctx->msgs);
+       imap_close_server(ctx);
        free(ctx);
 }
 
@@ -1028,7 +874,7 @@ static int auth_cram_md5(struct imap_store *ctx, struct imap_cmd *cmd, const cha
        return 0;
 }
 
-static struct store *imap_open_store(struct imap_server_conf *srvc)
+static struct imap_store *imap_open_store(struct imap_server_conf *srvc)
 {
        struct imap_store *ctx;
        struct imap *imap;
@@ -1238,103 +1084,69 @@ static struct store *imap_open_store(struct imap_server_conf *srvc)
        } /* !preauth */
 
        ctx->prefix = "";
-       ctx->trashnc = 1;
-       return (struct store *)ctx;
+       return ctx;
 
 bail:
-       imap_close_store(&ctx->gen);
+       imap_close_store(ctx);
        return NULL;
 }
 
-static int imap_make_flags(int flags, char *buf)
-{
-       const char *s;
-       unsigned i, d;
-
-       for (i = d = 0; i < ARRAY_SIZE(Flags); i++)
-               if (flags & (1 << i)) {
-                       buf[d++] = ' ';
-                       buf[d++] = '\\';
-                       for (s = Flags[i]; *s; s++)
-                               buf[d++] = *s;
-               }
-       buf[0] = '(';
-       buf[d++] = ')';
-       return d;
-}
-
+/*
+ * Insert CR characters as necessary in *msg to ensure that every LF
+ * character in *msg is preceded by a CR.
+ */
 static void lf_to_crlf(struct strbuf *msg)
 {
-       size_t new_len;
        char *new;
-       int i, j, lfnum = 0;
-
-       if (msg->buf[0] == '\n')
-               lfnum++;
-       for (i = 1; i < msg->len; i++) {
-               if (msg->buf[i - 1] != '\r' && msg->buf[i] == '\n')
-                       lfnum++;
+       size_t i, j;
+       char lastc;
+
+       /* First pass: tally, in j, the size of the new string: */
+       for (i = j = 0, lastc = '\0'; i < msg->len; i++) {
+               if (msg->buf[i] == '\n' && lastc != '\r')
+                       j++; /* a CR will need to be added here */
+               lastc = msg->buf[i];
+               j++;
        }
 
-       new_len = msg->len + lfnum;
-       new = xmalloc(new_len + 1);
-       if (msg->buf[0] == '\n') {
-               new[0] = '\r';
-               new[1] = '\n';
-               i = 1;
-               j = 2;
-       } else {
-               new[0] = msg->buf[0];
-               i = 1;
-               j = 1;
-       }
-       for ( ; i < msg->len; i++) {
-               if (msg->buf[i] != '\n') {
-                       new[j++] = msg->buf[i];
-                       continue;
-               }
-               if (msg->buf[i - 1] != '\r')
+       new = xmalloc(j + 1);
+
+       /*
+        * Second pass: write the new string.  Note that this loop is
+        * otherwise identical to the first pass.
+        */
+       for (i = j = 0, lastc = '\0'; i < msg->len; i++) {
+               if (msg->buf[i] == '\n' && lastc != '\r')
                        new[j++] = '\r';
-               /* otherwise it already had CR before */
-               new[j++] = '\n';
+               lastc = new[j++] = msg->buf[i];
        }
-       strbuf_attach(msg, new, new_len, new_len + 1);
+       strbuf_attach(msg, new, j, j + 1);
 }
 
 /*
  * Store msg to IMAP.  Also detach and free the data from msg->data,
  * leaving msg->data empty.
  */
-static int imap_store_msg(struct store *gctx, struct msg_data *msg)
+static int imap_store_msg(struct imap_store *ctx, struct strbuf *msg)
 {
-       struct imap_store *ctx = (struct imap_store *)gctx;
        struct imap *imap = ctx->imap;
        struct imap_cmd_cb cb;
        const char *prefix, *box;
-       int ret, d;
-       char flagstr[128];
+       int ret;
 
-       lf_to_crlf(&msg->data);
+       lf_to_crlf(msg);
        memset(&cb, 0, sizeof(cb));
 
-       cb.dlen = msg->data.len;
-       cb.data = strbuf_detach(&msg->data, NULL);
-
-       d = 0;
-       if (msg->flags) {
-               d = imap_make_flags(msg->flags, flagstr);
-               flagstr[d++] = ' ';
-       }
-       flagstr[d] = 0;
+       cb.dlen = msg->len;
+       cb.data = strbuf_detach(msg, NULL);
 
-       box = gctx->name;
+       box = ctx->name;
        prefix = !strcmp(box, "INBOX") ? "" : ctx->prefix;
        cb.create = 0;
-       ret = imap_exec_m(ctx, &cb, "APPEND \"%s%s\" %s", prefix, box, flagstr);
+       ret = imap_exec_m(ctx, &cb, "APPEND \"%s%s\" ", prefix, box);
        imap->caps = imap->rcaps;
        if (ret != DRV_OK)
                return ret;
-       gctx->count++;
 
        return DRV_OK;
 }
@@ -1483,8 +1295,8 @@ static int git_imap_config(const char *key, const char *val, void *cb)
 int main(int argc, char **argv)
 {
        struct strbuf all_msgs = STRBUF_INIT;
-       struct msg_data msg = {STRBUF_INIT, 0};
-       struct store *ctx = NULL;
+       struct strbuf msg = STRBUF_INIT;
+       struct imap_store *ctx = NULL;
        int ofs = 0;
        int r;
        int total, n = 0;
@@ -1545,10 +1357,10 @@ int main(int argc, char **argv)
                unsigned percent = n * 100 / total;
 
                fprintf(stderr, "%4u%% (%d/%d) done\r", percent, n, total);
-               if (!split_msg(&all_msgs, &msg.data, &ofs))
+               if (!split_msg(&all_msgs, &msg, &ofs))
                        break;
                if (server.use_html)
-                       wrap_in_html(&msg.data);
+                       wrap_in_html(&msg);
                r = imap_store_msg(ctx, &msg);
                if (r != DRV_OK)
                        break;
index f8487f8a8a7694ce09f1b079c701bceabf688b20..5dc45c4812bdfd0d7a6b71d529eaa37df8178186 100644 (file)
@@ -681,6 +681,7 @@ void show_log(struct rev_info *opt)
        ctx.preserve_subject = opt->preserve_subject;
        ctx.reflog_info = opt->reflog_info;
        ctx.fmt = opt->commit_format;
+       ctx.mailmap = opt->mailmap;
        ctx.color = opt->diffopt.use_color;
        pretty_print_commit(&ctx, commit, &msgbuf);
 
index b16542febec14ed86fd7ef21ba19899d08c95a64..2a7b36628cb5623eb0ffb95bfb54e52a6dd300f4 100644 (file)
--- a/mailmap.c
+++ b/mailmap.c
@@ -235,6 +235,7 @@ int read_mailmap(struct string_list *map, char **repo_abbrev)
        int err = 0;
 
        map->strdup_strings = 1;
+       map->cmp = strcasecmp;
 
        if (!git_mailmap_blob && is_bare_repository())
                git_mailmap_blob = "HEAD:.mailmap";
@@ -253,60 +254,95 @@ void clear_mailmap(struct string_list *map)
        debug_mm("mailmap: cleared\n");
 }
 
+/*
+ * Look for an entry in map that match string[0:len]; string[len]
+ * does not have to be NUL (but it could be).
+ */
+static struct string_list_item *lookup_prefix(struct string_list *map,
+                                             const char *string, size_t len)
+{
+       int i = string_list_find_insert_index(map, string, 1);
+       if (i < 0) {
+               /* exact match */
+               i = -1 - i;
+               if (!string[len])
+                       return &map->items[i];
+               /*
+                * that map entry matches exactly to the string, including
+                * the cruft at the end beyond "len".  That is not a match
+                * with string[0:len] that we are looking for.
+                */
+       } else if (!string[len]) {
+               /*
+                * asked with the whole string, and got nothing.  No
+                * matching entry can exist in the map.
+                */
+               return NULL;
+       }
+
+       /*
+        * i is at the exact match to an overlong key, or location the
+        * overlong key would be inserted, which must come after the
+        * real location of the key if one exists.
+        */
+       while (0 <= --i && i < map->nr) {
+               int cmp = strncasecmp(map->items[i].string, string, len);
+               if (cmp < 0)
+                       /*
+                        * "i" points at a key definitely below the prefix;
+                        * the map does not have string[0:len] in it.
+                        */
+                       break;
+               else if (!cmp && !map->items[i].string[len])
+                       /* found it */
+                       return &map->items[i];
+               /*
+                * otherwise, the string at "i" may be string[0:len]
+                * followed by a string that sorts later than string[len:];
+                * keep trying.
+                */
+       }
+       return NULL;
+}
+
 int map_user(struct string_list *map,
-            char *email, int maxlen_email, char *name, int maxlen_name)
+                        const char **email, size_t *emaillen,
+                        const char **name, size_t *namelen)
 {
-       char *end_of_email;
        struct string_list_item *item;
        struct mailmap_entry *me;
-       char buf[1024], *mailbuf;
-       int i;
-
-       /* figure out space requirement for email */
-       end_of_email = strchr(email, '>');
-       if (!end_of_email) {
-               /* email passed in might not be wrapped in <>, but end with a \0 */
-               end_of_email = memchr(email, '\0', maxlen_email);
-               if (!end_of_email)
-                       return 0;
-       }
-       if (end_of_email - email + 1 < sizeof(buf))
-               mailbuf = buf;
-       else
-               mailbuf = xmalloc(end_of_email - email + 1);
-
-       /* downcase the email address */
-       for (i = 0; i < end_of_email - email; i++)
-               mailbuf[i] = tolower(email[i]);
-       mailbuf[i] = 0;
-
-       debug_mm("map_user: map '%s' <%s>\n", name, mailbuf);
-       item = string_list_lookup(map, mailbuf);
+
+       debug_mm("map_user: map '%.*s' <%.*s>\n",
+                *name, *namelen, *emaillen, *email);
+
+       item = lookup_prefix(map, *email, *emaillen);
        if (item != NULL) {
                me = (struct mailmap_entry *)item->util;
                if (me->namemap.nr) {
                        /* The item has multiple items, so we'll look up on name too */
                        /* If the name is not found, we choose the simple entry      */
-                       struct string_list_item *subitem = string_list_lookup(&me->namemap, name);
+                       struct string_list_item *subitem;
+                       subitem = lookup_prefix(&me->namemap, *name, *namelen);
                        if (subitem)
                                item = subitem;
                }
        }
-       if (mailbuf != buf)
-               free(mailbuf);
        if (item != NULL) {
                struct mailmap_info *mi = (struct mailmap_info *)item->util;
-               if (mi->name == NULL && (mi->email == NULL || maxlen_email == 0)) {
+               if (mi->name == NULL && mi->email == NULL) {
                        debug_mm("map_user:  -- (no simple mapping)\n");
                        return 0;
                }
-               if (maxlen_email && mi->email)
-                       strlcpy(email, mi->email, maxlen_email);
-               else
-                       *end_of_email = '\0';
-               if (maxlen_name && mi->name)
-                       strlcpy(name, mi->name, maxlen_name);
-               debug_mm("map_user:  to '%s' <%s>\n", name, mi->email ? mi->email : "");
+               if (mi->email) {
+                               *email = mi->email;
+                               *emaillen = strlen(*email);
+               }
+               if (mi->name) {
+                               *name = mi->name;
+                               *namelen = strlen(*name);
+               }
+               debug_mm("map_user:  to '%.*s' <.*%s>\n", *namelen, *name,
+                                *emaillen, *email);
                return 1;
        }
        debug_mm("map_user:  --\n");
index d5c3664322023d6552cb24006d833aa08224ff6f..ed7c93b05c3cc904c3052bae199346a698eec1e6 100644 (file)
--- a/mailmap.h
+++ b/mailmap.h
@@ -4,7 +4,7 @@
 int read_mailmap(struct string_list *map, char **repo_abbrev);
 void clear_mailmap(struct string_list *map);
 
-int map_user(struct string_list *mailmap,
-            char *email, int maxlen_email, char *name, int maxlen_name);
+int map_user(struct string_list *map,
+                        const char **email, size_t *emaillen, const char **name, size_t *namelen);
 
 #endif
index e703853749f0949c58cde0a27ff1d01991609400..1c8bd8d5a0894d9deda1e849165ff38e8d594a4d 100644 (file)
@@ -177,7 +177,7 @@ extern NORETURN void usage_msg_opt(const char *msg,
 
 extern int optbug(const struct option *opt, const char *reason);
 extern int opterror(const struct option *opt, const char *reason, int flags);
-#ifdef __GNUC__
+#if defined(__GNUC__) && ! defined(clang)
 #define opterror(o,r,f) (opterror((o),(r),(f)), -1)
 #endif
 
diff --git a/pathspec.c b/pathspec.c
new file mode 100644 (file)
index 0000000..284f397
--- /dev/null
@@ -0,0 +1,101 @@
+#include "cache.h"
+#include "dir.h"
+#include "pathspec.h"
+
+/*
+ * Finds which of the given pathspecs match items in the index.
+ *
+ * For each pathspec, sets the corresponding entry in the seen[] array
+ * (which should be specs items long, i.e. the same size as pathspec)
+ * to the nature of the "closest" (i.e. most specific) match found for
+ * that pathspec in the index, if it was a closer type of match than
+ * the existing entry.  As an optimization, matching is skipped
+ * altogether if seen[] already only contains non-zero entries.
+ *
+ * If seen[] has not already been written to, it may make sense
+ * to use find_pathspecs_matching_against_index() instead.
+ */
+void add_pathspec_matches_against_index(const char **pathspec,
+                                       char *seen, int specs)
+{
+       int num_unmatched = 0, i;
+
+       /*
+        * Since we are walking the index as if we were walking the directory,
+        * we have to mark the matched pathspec as seen; otherwise we will
+        * mistakenly think that the user gave a pathspec that did not match
+        * anything.
+        */
+       for (i = 0; i < specs; i++)
+               if (!seen[i])
+                       num_unmatched++;
+       if (!num_unmatched)
+               return;
+       for (i = 0; i < active_nr; i++) {
+               struct cache_entry *ce = active_cache[i];
+               match_pathspec(pathspec, ce->name, ce_namelen(ce), 0, seen);
+       }
+}
+
+/*
+ * Finds which of the given pathspecs match items in the index.
+ *
+ * This is a one-shot wrapper around add_pathspec_matches_against_index()
+ * which allocates, populates, and returns a seen[] array indicating the
+ * nature of the "closest" (i.e. most specific) matches which each of the
+ * given pathspecs achieves against all items in the index.
+ */
+char *find_pathspecs_matching_against_index(const char **pathspec)
+{
+       char *seen;
+       int i;
+
+       for (i = 0; pathspec[i];  i++)
+               ; /* just counting */
+       seen = xcalloc(i, 1);
+       add_pathspec_matches_against_index(pathspec, seen, i);
+       return seen;
+}
+
+/*
+ * Check the index to see whether path refers to a submodule, or
+ * something inside a submodule.  If the former, returns the path with
+ * any trailing slash stripped.  If the latter, dies with an error
+ * message.
+ */
+const char *check_path_for_gitlink(const char *path)
+{
+       int i, path_len = strlen(path);
+       for (i = 0; i < active_nr; i++) {
+               struct cache_entry *ce = active_cache[i];
+               if (S_ISGITLINK(ce->ce_mode)) {
+                       int ce_len = ce_namelen(ce);
+                       if (path_len <= ce_len || path[ce_len] != '/' ||
+                           memcmp(ce->name, path, ce_len))
+                               /* path does not refer to this
+                                * submodule or anything inside it */
+                               continue;
+                       if (path_len == ce_len + 1) {
+                               /* path refers to submodule;
+                                * strip trailing slash */
+                               return xstrndup(ce->name, ce_len);
+                       } else {
+                               die (_("Path '%s' is in submodule '%.*s'"),
+                                    path, ce_len, ce->name);
+                       }
+               }
+       }
+       return path;
+}
+
+/*
+ * Dies if the given path refers to a file inside a symlinked
+ * directory in the index.
+ */
+void die_if_path_beyond_symlink(const char *path, const char *prefix)
+{
+       if (has_symlink_leading_path(path, strlen(path))) {
+               int len = prefix ? strlen(prefix) : 0;
+               die(_("'%s' is beyond a symbolic link"), path + len);
+       }
+}
diff --git a/pathspec.h b/pathspec.h
new file mode 100644 (file)
index 0000000..db0184a
--- /dev/null
@@ -0,0 +1,9 @@
+#ifndef PATHSPEC_H
+#define PATHSPEC_H
+
+extern char *find_pathspecs_matching_against_index(const char **pathspec);
+extern void add_pathspec_matches_against_index(const char **pathspec, char *seen, int specs);
+extern const char *check_path_for_gitlink(const char *path);
+extern void die_if_path_beyond_symlink(const char *path, const char *prefix);
+
+#endif /* PATHSPEC_H */
index 3bbc20a054576b75e77a15faffd82c46e553fef8..3db152155c62fb43b6243c10ce25cc193895afe3 100644 (file)
@@ -145,7 +145,8 @@ sub repo_path {
 sub url_path {
        my ($self, $path) = @_;
        if ($self->{url} =~ m#^https?://#) {
-               $path =~ s!([^~a-zA-Z0-9_./-])!uc sprintf("%%%02x",ord($1))!eg;
+               # characters are taken from subversion/libsvn_subr/path.c
+               $path =~ s#([^~a-zA-Z0-9_./!$&'()*+,-])#uc sprintf("%%%02x",ord($1))#eg;
        }
        $self->{url} . '/' . $self->repo_path($path);
 }
@@ -358,12 +359,12 @@ sub T {
                        mode_a => $m->{mode_a}, mode_b => '000000',
                        sha1_a => $m->{sha1_a}, sha1_b => '0' x 40,
                        chg => 'D', file_b => $m->{file_b}
-               });
+               }, $deletions);
                $self->A({
                        mode_a => '000000', mode_b => $m->{mode_b},
                        sha1_a => '0' x 40, sha1_b => $m->{sha1_b},
                        chg => 'A', file_b => $m->{file_b}
-               });
+               }, $deletions);
                return;
        }
 
index 8b8cf3755c268e03ef334534092bcd7959070cd2..3d1a0933a2eb27dc123730c67470be503e91bf93 100644 (file)
@@ -155,7 +155,7 @@ sub _canonicalize_url_path {
 
        my @parts;
        foreach my $part (split m{/+}, $uri_path) {
-               $part =~ s/([^~\w.%+-]|%(?![a-fA-F0-9]{2}))/sprintf("%%%02X",ord($1))/eg;
+               $part =~ s/([^!\$%&'()*+,.\/\w:=\@_`~-]|%(?![a-fA-F0-9]{2}))/sprintf("%%%02X",ord($1))/eg;
                push @parts, $part;
        }
 
index 92c839fe641da15d05c2afe2b498a9fe390624c8..07fc0628656c5e37eaf04c969a7273455a391f8d 100644 (file)
--- a/pretty.c
+++ b/pretty.c
@@ -387,56 +387,79 @@ void pp_user_info(const struct pretty_print_context *pp,
                  const char *what, struct strbuf *sb,
                  const char *line, const char *encoding)
 {
+       struct strbuf name;
+       struct strbuf mail;
+       struct ident_split ident;
+       int linelen;
+       char *line_end, *date;
+       const char *mailbuf, *namebuf;
+       size_t namelen, maillen;
        int max_length = 78; /* per rfc2822 */
-       char *date;
-       int namelen;
        unsigned long time;
        int tz;
 
        if (pp->fmt == CMIT_FMT_ONELINE)
                return;
-       date = strchr(line, '>');
-       if (!date)
+
+       line_end = strchr(line, '\n');
+       if (!line_end) {
+               line_end = strchr(line, '\0');
+               if (!line_end)
+                       return;
+       }
+
+       linelen = ++line_end - line;
+       if (split_ident_line(&ident, line, linelen))
                return;
-       namelen = ++date - line;
-       time = strtoul(date, &date, 10);
+
+
+       mailbuf = ident.mail_begin;
+       maillen = ident.mail_end - ident.mail_begin;
+       namebuf = ident.name_begin;
+       namelen = ident.name_end - ident.name_begin;
+
+       if (pp->mailmap)
+               map_user(pp->mailmap, &mailbuf, &maillen, &namebuf, &namelen);
+
+       strbuf_init(&mail, 0);
+       strbuf_init(&name, 0);
+
+       strbuf_add(&mail, mailbuf, maillen);
+       strbuf_add(&name, namebuf, namelen);
+
+       namelen = name.len + mail.len + 3; /* ' ' + '<' + '>' */
+       time = strtoul(ident.date_begin, &date, 10);
        tz = strtol(date, NULL, 10);
 
        if (pp->fmt == CMIT_FMT_EMAIL) {
-               char *name_tail = strchr(line, '<');
-               int display_name_length;
-               if (!name_tail)
-                       return;
-               while (line < name_tail && isspace(name_tail[-1]))
-                       name_tail--;
-               display_name_length = name_tail - line;
                strbuf_addstr(sb, "From: ");
-               if (needs_rfc2047_encoding(line, display_name_length, RFC2047_ADDRESS)) {
-                       add_rfc2047(sb, line, display_name_length,
-                                               encoding, RFC2047_ADDRESS);
+               if (needs_rfc2047_encoding(name.buf, name.len, RFC2047_ADDRESS)) {
+                       add_rfc2047(sb, name.buf, name.len,
+                                   encoding, RFC2047_ADDRESS);
                        max_length = 76; /* per rfc2047 */
-               } else if (needs_rfc822_quoting(line, display_name_length)) {
+               } else if (needs_rfc822_quoting(name.buf, name.len)) {
                        struct strbuf quoted = STRBUF_INIT;
-                       add_rfc822_quoted(&quoted, line, display_name_length);
+                       add_rfc822_quoted(&quoted, name.buf, name.len);
                        strbuf_add_wrapped_bytes(sb, quoted.buf, quoted.len,
                                                        -6, 1, max_length);
                        strbuf_release(&quoted);
                } else {
-                       strbuf_add_wrapped_bytes(sb, line, display_name_length,
-                                                       -6, 1, max_length);
+                       strbuf_add_wrapped_bytes(sb, name.buf, name.len,
+                                                -6, 1, max_length);
                }
-               if (namelen - display_name_length + last_line_length(sb) > max_length) {
+               if (namelen - name.len + last_line_length(sb) > max_length)
                        strbuf_addch(sb, '\n');
-                       if (!isspace(name_tail[0]))
-                               strbuf_addch(sb, ' ');
-               }
-               strbuf_add(sb, name_tail, namelen - display_name_length);
-               strbuf_addch(sb, '\n');
+
+               strbuf_addf(sb, " <%s>\n", mail.buf);
        } else {
-               strbuf_addf(sb, "%s: %.*s%.*s\n", what,
+               strbuf_addf(sb, "%s: %.*s%s <%s>\n", what,
                              (pp->fmt == CMIT_FMT_FULLER) ? 4 : 0,
-                             "    ", namelen, line);
+                             "    ", name.buf, mail.buf);
        }
+
+       strbuf_release(&mail);
+       strbuf_release(&name);
+
        switch (pp->fmt) {
        case CMIT_FMT_MEDIUM:
                strbuf_addf(sb, "Date:   %s\n", show_date(time, tz, pp->date_mode));
@@ -586,7 +609,8 @@ char *logmsg_reencode(const struct commit *commit,
        return out;
 }
 
-static int mailmap_name(char *email, int email_len, char *name, int name_len)
+static int mailmap_name(const char **email, size_t *email_len,
+                       const char **name, size_t *name_len)
 {
        static struct string_list *mail_map;
        if (!mail_map) {
@@ -603,36 +627,26 @@ static size_t format_person_part(struct strbuf *sb, char part,
        const int placeholder_len = 2;
        int tz;
        unsigned long date = 0;
-       char person_name[1024];
-       char person_mail[1024];
        struct ident_split s;
-       const char *name_start, *name_end, *mail_start, *mail_end;
+       const char *name, *mail;
+       size_t maillen, namelen;
 
        if (split_ident_line(&s, msg, len) < 0)
                goto skip;
 
-       name_start = s.name_begin;
-       name_end = s.name_end;
-       mail_start = s.mail_begin;
-       mail_end = s.mail_end;
-
-       if (part == 'N' || part == 'E') { /* mailmap lookup */
-               snprintf(person_name, sizeof(person_name), "%.*s",
-                        (int)(name_end - name_start), name_start);
-               snprintf(person_mail, sizeof(person_mail), "%.*s",
-                        (int)(mail_end - mail_start), mail_start);
-               mailmap_name(person_mail, sizeof(person_mail), person_name, sizeof(person_name));
-               name_start = person_name;
-               name_end = name_start + strlen(person_name);
-               mail_start = person_mail;
-               mail_end = mail_start +  strlen(person_mail);
-       }
+       name = s.name_begin;
+       namelen = s.name_end - s.name_begin;
+       mail = s.mail_begin;
+       maillen = s.mail_end - s.mail_begin;
+
+       if (part == 'N' || part == 'E') /* mailmap lookup */
+               mailmap_name(&mail, &maillen, &name, &namelen);
        if (part == 'n' || part == 'N') {       /* name */
-               strbuf_add(sb, name_start, name_end-name_start);
+               strbuf_add(sb, name, namelen);
                return placeholder_len;
        }
        if (part == 'e' || part == 'E') {       /* email */
-               strbuf_add(sb, mail_start, mail_end-mail_start);
+               strbuf_add(sb, mail, maillen);
                return placeholder_len;
        }
 
@@ -966,7 +980,7 @@ static size_t format_commit_one(struct strbuf *sb, const char *placeholder,
 
                        if (!end)
                                return 0;
-                       if (!memcmp(begin, "auto,", 5)) {
+                       if (!prefixcmp(begin, "auto,")) {
                                if (!want_color(c->pretty_ctx->color))
                                        return end - placeholder + 1;
                                begin += 5;
@@ -1301,7 +1315,7 @@ static void pp_header(const struct pretty_print_context *pp,
                        continue;
                }
 
-               if (!memcmp(line, "parent ", 7)) {
+               if (!prefixcmp(line, "parent ")) {
                        if (linelen != 48)
                                die("bad parent line in commit");
                        continue;
@@ -1325,11 +1339,11 @@ static void pp_header(const struct pretty_print_context *pp,
                 * FULL shows both authors but not dates.
                 * FULLER shows both authors and dates.
                 */
-               if (!memcmp(line, "author ", 7)) {
+               if (!prefixcmp(line, "author ")) {
                        strbuf_grow(sb, linelen + 80);
                        pp_user_info(pp, "Author", sb, line + 7, encoding);
                }
-               if (!memcmp(line, "committer ", 10) &&
+               if (!prefixcmp(line, "committer ") &&
                    (pp->fmt == CMIT_FMT_FULL || pp->fmt == CMIT_FMT_FULLER)) {
                        strbuf_grow(sb, linelen + 80);
                        pp_user_info(pp, "Commit", sb, line + 10, encoding);
diff --git a/refs.c b/refs.c
index 541fec20658082f13ef4b73b621787512b300ba6..29628253087fb0d54a8f7a45938624527f35d4a8 100644 (file)
--- a/refs.c
+++ b/refs.c
@@ -333,14 +333,12 @@ struct string_slice {
 
 static int ref_entry_cmp_sslice(const void *key_, const void *ent_)
 {
-       struct string_slice *key = (struct string_slice *)key_;
-       struct ref_entry *ent = *(struct ref_entry **)ent_;
-       int entlen = strlen(ent->name);
-       int cmplen = key->len < entlen ? key->len : entlen;
-       int cmp = memcmp(key->str, ent->name, cmplen);
+       const struct string_slice *key = key_;
+       const struct ref_entry *ent = *(const struct ref_entry * const *)ent_;
+       int cmp = strncmp(key->str, ent->name, key->len);
        if (cmp)
                return cmp;
-       return key->len - entlen;
+       return '\0' - (unsigned char)ent->name[key->len];
 }
 
 /*
index 4b1153f02c715ac0c441110bf5aa83aceb081aa0..9e21b1d78755cec7b30224f10c7f8ecbef3c9a49 100644 (file)
--- a/remote.c
+++ b/remote.c
@@ -1279,26 +1279,6 @@ int match_push_refs(struct ref *src, struct ref **dst,
        return 0;
 }
 
-static inline int is_forwardable(struct ref* ref)
-{
-       struct object *o;
-
-       if (!prefixcmp(ref->name, "refs/tags/"))
-               return 0;
-
-       /* old object must be a commit */
-       o = parse_object(ref->old_sha1);
-       if (!o || o->type != OBJ_COMMIT)
-               return 0;
-
-       /* new object must be commit-ish */
-       o = deref_tag(parse_object(ref->new_sha1), NULL, 0);
-       if (!o || o->type != OBJ_COMMIT)
-               return 0;
-
-       return 1;
-}
-
 void set_ref_status_for_push(struct ref *remote_refs, int send_mirror,
        int force_update)
 {
@@ -1320,32 +1300,23 @@ void set_ref_status_for_push(struct ref *remote_refs, int send_mirror,
                }
 
                /*
-                * The below logic determines whether an individual
-                * refspec A:B can be pushed.  The push will succeed
-                * if any of the following are true:
+                * Decide whether an individual refspec A:B can be
+                * pushed.  The push will succeed if any of the
+                * following are true:
                 *
                 * (1) the remote reference B does not exist
                 *
                 * (2) the remote reference B is being removed (i.e.,
                 *     pushing :B where no source is specified)
                 *
-                * (3) the update meets all fast-forwarding criteria:
-                *
-                *     (a) the destination is not under refs/tags/
-                *     (b) the old is a commit
-                *     (c) the new is a descendant of the old
-                *
-                *     NOTE: We must actually have the old object in
-                *     order to overwrite it in the remote reference,
-                *     and the new object must be commit-ish.  These are
-                *     implied by (b) and (c) respectively.
+                * (3) the destination is not under refs/tags/, and
+                *     if the old and new value is a commit, the new
+                *     is a descendant of the old.
                 *
                 * (4) it is forced using the +A:B notation, or by
                 *     passing the --force argument
                 */
 
-               ref->not_forwardable = !is_forwardable(ref);
-
                ref->update =
                        !ref->deletion &&
                        !is_null_sha1(ref->old_sha1);
@@ -1355,7 +1326,7 @@ void set_ref_status_for_push(struct ref *remote_refs, int send_mirror,
                                !has_sha1_file(ref->old_sha1)
                                  || !ref_newer(ref->new_sha1, ref->old_sha1);
 
-                       if (ref->not_forwardable) {
+                       if (!prefixcmp(ref->name, "refs/tags/")) {
                                ref->requires_force = 1;
                                if (!force_ref_update) {
                                        ref->status = REF_STATUS_REJECT_ALREADY_EXISTS;
index 95d21e6472921ab993373ef1848357379440d46e..d7562ee5004379774c6fe1a74b0c503475a34ab5 100644 (file)
@@ -13,6 +13,7 @@
 #include "decorate.h"
 #include "log-tree.h"
 #include "string-list.h"
+#include "mailmap.h"
 
 volatile show_early_output_fn_t show_early_output;
 
@@ -2219,6 +2220,51 @@ static int rewrite_parents(struct rev_info *revs, struct commit *commit)
        return 0;
 }
 
+static int commit_rewrite_person(struct strbuf *buf, const char *what, struct string_list *mailmap)
+{
+       char *person, *endp;
+       size_t len, namelen, maillen;
+       const char *name;
+       const char *mail;
+       struct ident_split ident;
+
+       person = strstr(buf->buf, what);
+       if (!person)
+               return 0;
+
+       person += strlen(what);
+       endp = strchr(person, '\n');
+       if (!endp)
+               return 0;
+
+       len = endp - person;
+
+       if (split_ident_line(&ident, person, len))
+               return 0;
+
+       mail = ident.mail_begin;
+       maillen = ident.mail_end - ident.mail_begin;
+       name = ident.name_begin;
+       namelen = ident.name_end - ident.name_begin;
+
+       if (map_user(mailmap, &mail, &maillen, &name, &namelen)) {
+               struct strbuf namemail = STRBUF_INIT;
+
+               strbuf_addf(&namemail, "%.*s <%.*s>",
+                           (int)namelen, name, (int)maillen, mail);
+
+               strbuf_splice(buf, ident.name_begin - buf->buf,
+                             ident.mail_end - ident.name_begin + 1,
+                             namemail.buf, namemail.len);
+
+               strbuf_release(&namemail);
+
+               return 1;
+       }
+
+       return 0;
+}
+
 static int commit_match(struct commit *commit, struct rev_info *opt)
 {
        int retval;
@@ -2237,6 +2283,14 @@ static int commit_match(struct commit *commit, struct rev_info *opt)
        if (buf.len)
                strbuf_addstr(&buf, commit->buffer);
 
+       if (opt->grep_filter.header_list && opt->mailmap) {
+               if (!buf.len)
+                       strbuf_addstr(&buf, commit->buffer);
+
+               commit_rewrite_person(&buf, "\nauthor ", opt->mailmap);
+               commit_rewrite_person(&buf, "\ncommitter ", opt->mailmap);
+       }
+
        /* Append "fake" message parts as needed */
        if (opt->show_notes) {
                if (!buf.len)
index a395c3639a30d4d0d55036a5fec316e4cdb9546f..5da09ee3efa976b503cba5d13e347aad0f6c764c 100644 (file)
@@ -144,6 +144,7 @@ struct rev_info {
        const char      *subject_prefix;
        int             no_inline;
        int             show_log_size;
+       struct string_list *mailmap;
 
        /* Filter by commit log message */
        struct grep_opt grep_filter;
diff --git a/setup.c b/setup.c
index f108c4b990c0e4f6a98c1fe34d3031040c84195a..1ccfafaa7a85875338a49953518207b74dca62c4 100644 (file)
--- a/setup.c
+++ b/setup.c
@@ -246,6 +246,25 @@ static const char *prefix_pathspec(const char *prefix, int prefixlen, const char
                return prefix_path(prefix, prefixlen, copyfrom);
 }
 
+/*
+ * N.B. get_pathspec() is deprecated in favor of the "struct pathspec"
+ * based interface - see pathspec_magic above.
+ *
+ * Arguments:
+ *  - prefix - a path relative to the root of the working tree
+ *  - pathspec - a list of paths underneath the prefix path
+ *
+ * Iterates over pathspec, prepending each path with prefix,
+ * and return the resulting list.
+ *
+ * If pathspec is empty, return a singleton list containing prefix.
+ *
+ * If pathspec and prefix are both empty, return an empty list.
+ *
+ * This is typically used by built-in commands such as add.c, in order
+ * to normalize argv arguments provided to the built-in into a list of
+ * paths to process, all relative to the root of the working tree.
+ */
 const char **get_pathspec(const char *prefix, const char **pathspec)
 {
        const char *entry = *pathspec;
index 480173fe6dbee5428e3b135ff27ba7551c527ad8..aabb25ef4c1040dde015d6ac37b8213d9bc958ea 100644 (file)
@@ -7,10 +7,11 @@ static int get_entry_index(const struct string_list *list, const char *string,
                int *exact_match)
 {
        int left = -1, right = list->nr;
+       compare_strings_fn cmp = list->cmp ? list->cmp : strcmp;
 
        while (left + 1 < right) {
                int middle = (left + right) / 2;
-               int compare = strcmp(string, list->items[middle].string);
+               int compare = cmp(string, list->items[middle].string);
                if (compare < 0)
                        right = middle;
                else if (compare > 0)
@@ -96,8 +97,9 @@ void string_list_remove_duplicates(struct string_list *list, int free_util)
 {
        if (list->nr > 1) {
                int src, dst;
+               compare_strings_fn cmp = list->cmp ? list->cmp : strcmp;
                for (src = dst = 1; src < list->nr; src++) {
-                       if (!strcmp(list->items[dst - 1].string, list->items[src].string)) {
+                       if (!cmp(list->items[dst - 1].string, list->items[src].string)) {
                                if (list->strdup_strings)
                                        free(list->items[src].string);
                                if (free_util)
@@ -210,15 +212,20 @@ struct string_list_item *string_list_append(struct string_list *list,
                        list->strdup_strings ? xstrdup(string) : (char *)string);
 }
 
+/* Yuck */
+static compare_strings_fn compare_for_qsort;
+
+/* Only call this from inside sort_string_list! */
 static int cmp_items(const void *a, const void *b)
 {
        const struct string_list_item *one = a;
        const struct string_list_item *two = b;
-       return strcmp(one->string, two->string);
+       return compare_for_qsort(one->string, two->string);
 }
 
 void sort_string_list(struct string_list *list)
 {
+       compare_for_qsort = list->cmp ? list->cmp : strcmp;
        qsort(list->items, list->nr, sizeof(*list->items), cmp_items);
 }
 
@@ -226,8 +233,10 @@ struct string_list_item *unsorted_string_list_lookup(struct string_list *list,
                                                     const char *string)
 {
        int i;
+       compare_strings_fn cmp = list->cmp ? list->cmp : strcmp;
+
        for (i = 0; i < list->nr; i++)
-               if (!strcmp(string, list->items[i].string))
+               if (!cmp(string, list->items[i].string))
                        return list->items + i;
        return NULL;
 }
index db1284861adb707b675714edc1ecacf9357f6661..de6769c92dd109791c872b8d320c2caeee9f2b8d 100644 (file)
@@ -5,10 +5,14 @@ struct string_list_item {
        char *string;
        void *util;
 };
+
+typedef int (*compare_strings_fn)(const char *, const char *);
+
 struct string_list {
        struct string_list_item *items;
        unsigned int nr, alloc;
        unsigned int strdup_strings:1;
+       compare_strings_fn cmp; /* NULL uses strcmp() */
 };
 
 #define STRING_LIST_INIT_NODUP { NULL, 0, 0, 0 }
diff --git a/t/t0008-ignores.sh b/t/t0008-ignores.sh
new file mode 100755 (executable)
index 0000000..d7df719
--- /dev/null
@@ -0,0 +1,637 @@
+#!/bin/sh
+
+test_description=check-ignore
+
+. ./test-lib.sh
+
+init_vars () {
+       global_excludes="$(pwd)/global-excludes"
+}
+
+enable_global_excludes () {
+       init_vars &&
+       git config core.excludesfile "$global_excludes"
+}
+
+expect_in () {
+       dest="$HOME/expected-$1" text="$2"
+       if test -z "$text"
+       then
+               >"$dest" # avoid newline
+       else
+               echo "$text" >"$dest"
+       fi
+}
+
+expect () {
+       expect_in stdout "$1"
+}
+
+expect_from_stdin () {
+       cat >"$HOME/expected-stdout"
+}
+
+test_stderr () {
+       expected="$1"
+       expect_in stderr "$1" &&
+       test_cmp "$HOME/expected-stderr" "$HOME/stderr"
+}
+
+stderr_contains () {
+       regexp="$1"
+       if grep "$regexp" "$HOME/stderr"
+       then
+               return 0
+       else
+               echo "didn't find /$regexp/ in $HOME/stderr"
+               cat "$HOME/stderr"
+               return 1
+       fi
+}
+
+stderr_empty_on_success () {
+       expect_code="$1"
+       if test $expect_code = 0
+       then
+               test_stderr ""
+       else
+               # If we expect failure then stderr might or might not be empty
+               # due to --quiet - the caller can check its contents
+               return 0
+       fi
+}
+
+test_check_ignore () {
+       args="$1" expect_code="${2:-0}" global_args="$3"
+
+       init_vars &&
+       rm -f "$HOME/stdout" "$HOME/stderr" "$HOME/cmd" &&
+       echo git $global_args check-ignore $quiet_opt $verbose_opt $args \
+               >"$HOME/cmd" &&
+       test_expect_code "$expect_code" \
+               git $global_args check-ignore $quiet_opt $verbose_opt $args \
+               >"$HOME/stdout" 2>"$HOME/stderr" &&
+       test_cmp "$HOME/expected-stdout" "$HOME/stdout" &&
+       stderr_empty_on_success "$expect_code"
+}
+
+test_expect_success_multi () {
+       prereq=
+       if test $# -eq 4
+       then
+               prereq=$1
+               shift
+       fi
+       testname="$1" expect_verbose="$2" code="$3"
+
+       expect=$( echo "$expect_verbose" | sed -e 's/.* //' )
+
+       test_expect_success $prereq "$testname" '
+               expect "$expect" &&
+               eval "$code"
+       '
+
+       for quiet_opt in '-q' '--quiet'
+       do
+               test_expect_success $prereq "$testname${quiet_opt:+ with $quiet_opt}" "
+                       expect '' &&
+                       $code
+               "
+       done
+       quiet_opt=
+
+       for verbose_opt in '-v' '--verbose'
+       do
+               test_expect_success $prereq "$testname${verbose_opt:+ with $verbose_opt}" "
+                       expect '$expect_verbose' &&
+                       $code
+               "
+       done
+       verbose_opt=
+}
+
+test_expect_success 'setup' '
+       init_vars &&
+       mkdir -p a/b/ignored-dir a/submodule b &&
+       if test_have_prereq SYMLINKS
+       then
+               ln -s b a/symlink
+       fi &&
+       (
+               cd a/submodule &&
+               git init &&
+               echo a >a &&
+               git add a &&
+               git commit -m"commit in submodule"
+       ) &&
+       git add a/submodule &&
+       cat <<-\EOF >.gitignore &&
+               one
+               ignored-*
+       EOF
+       for dir in . a
+       do
+               : >$dir/not-ignored &&
+               : >$dir/ignored-and-untracked &&
+               : >$dir/ignored-but-in-index
+       done &&
+       git add -f ignored-but-in-index a/ignored-but-in-index &&
+       cat <<-\EOF >a/.gitignore &&
+               two*
+               *three
+       EOF
+       cat <<-\EOF >a/b/.gitignore &&
+               four
+               five
+               # this comment should affect the line numbers
+               six
+               ignored-dir/
+               # and so should this blank line:
+
+               !on*
+               !two
+       EOF
+       echo "seven" >a/b/ignored-dir/.gitignore &&
+       test -n "$HOME" &&
+       cat <<-\EOF >"$global_excludes" &&
+               globalone
+               !globaltwo
+               globalthree
+       EOF
+       cat <<-\EOF >>.git/info/exclude
+               per-repo
+       EOF
+'
+
+############################################################################
+#
+# test invalid inputs
+
+test_expect_success_multi 'empty command line' '' '
+       test_check_ignore "" 128 &&
+       stderr_contains "fatal: no path specified"
+'
+
+test_expect_success_multi '--stdin with empty STDIN' '' '
+       test_check_ignore "--stdin" 1 </dev/null &&
+       if test -n "$quiet_opt"; then
+               test_stderr ""
+       else
+               test_stderr "no pathspec given."
+       fi
+'
+
+test_expect_success '-q with multiple args' '
+       expect "" &&
+       test_check_ignore "-q one two" 128 &&
+       stderr_contains "fatal: --quiet is only valid with a single pathname"
+'
+
+test_expect_success '--quiet with multiple args' '
+       expect "" &&
+       test_check_ignore "--quiet one two" 128 &&
+       stderr_contains "fatal: --quiet is only valid with a single pathname"
+'
+
+for verbose_opt in '-v' '--verbose'
+do
+       for quiet_opt in '-q' '--quiet'
+       do
+               test_expect_success "$quiet_opt $verbose_opt" "
+                       expect '' &&
+                       test_check_ignore '$quiet_opt $verbose_opt foo' 128 &&
+                       stderr_contains 'fatal: cannot have both --quiet and --verbose'
+               "
+       done
+done
+
+test_expect_success '--quiet with multiple args' '
+       expect "" &&
+       test_check_ignore "--quiet one two" 128 &&
+       stderr_contains "fatal: --quiet is only valid with a single pathname"
+'
+
+test_expect_success_multi 'erroneous use of --' '' '
+       test_check_ignore "--" 128 &&
+       stderr_contains "fatal: no path specified"
+'
+
+test_expect_success_multi '--stdin with superfluous arg' '' '
+       test_check_ignore "--stdin foo" 128 &&
+       stderr_contains "fatal: cannot specify pathnames with --stdin"
+'
+
+test_expect_success_multi '--stdin -z with superfluous arg' '' '
+       test_check_ignore "--stdin -z foo" 128 &&
+       stderr_contains "fatal: cannot specify pathnames with --stdin"
+'
+
+test_expect_success_multi '-z without --stdin' '' '
+       test_check_ignore "-z" 128 &&
+       stderr_contains "fatal: -z only makes sense with --stdin"
+'
+
+test_expect_success_multi '-z without --stdin and superfluous arg' '' '
+       test_check_ignore "-z foo" 128 &&
+       stderr_contains "fatal: -z only makes sense with --stdin"
+'
+
+test_expect_success_multi 'needs work tree' '' '
+       (
+               cd .git &&
+               test_check_ignore "foo" 128
+       ) &&
+       stderr_contains "fatal: This operation must be run in a work tree"
+'
+
+############################################################################
+#
+# test standard ignores
+
+# First make sure that the presence of a file in the working tree
+# does not impact results, but that the presence of a file in the
+# index does.
+
+for subdir in '' 'a/'
+do
+       if test -z "$subdir"
+       then
+               where="at top-level"
+       else
+               where="in subdir $subdir"
+       fi
+
+       test_expect_success_multi "non-existent file $where not ignored" '' "
+               test_check_ignore '${subdir}non-existent' 1
+       "
+
+       test_expect_success_multi "non-existent file $where ignored" \
+               ".gitignore:1:one       ${subdir}one" "
+               test_check_ignore '${subdir}one'
+       "
+
+       test_expect_success_multi "existing untracked file $where not ignored" '' "
+               test_check_ignore '${subdir}not-ignored' 1
+       "
+
+       test_expect_success_multi "existing tracked file $where not ignored" '' "
+               test_check_ignore '${subdir}ignored-but-in-index' 1
+       "
+
+       test_expect_success_multi "existing untracked file $where ignored" \
+               ".gitignore:2:ignored-* ${subdir}ignored-and-untracked" "
+               test_check_ignore '${subdir}ignored-and-untracked'
+       "
+done
+
+# Having established the above, from now on we mostly test against
+# files which do not exist in the working tree or index.
+
+test_expect_success 'sub-directory local ignore' '
+       expect "a/3-three" &&
+       test_check_ignore "a/3-three a/three-not-this-one"
+'
+
+test_expect_success 'sub-directory local ignore with --verbose'  '
+       expect "a/.gitignore:2:*three   a/3-three" &&
+       test_check_ignore "--verbose a/3-three a/three-not-this-one"
+'
+
+test_expect_success 'local ignore inside a sub-directory' '
+       expect "3-three" &&
+       (
+               cd a &&
+               test_check_ignore "3-three three-not-this-one"
+       )
+'
+test_expect_success 'local ignore inside a sub-directory with --verbose' '
+       expect "a/.gitignore:2:*three   3-three" &&
+       (
+               cd a &&
+               test_check_ignore "--verbose 3-three three-not-this-one"
+       )
+'
+
+test_expect_success_multi 'nested include' \
+       'a/b/.gitignore:8:!on*  a/b/one' '
+       test_check_ignore "a/b/one"
+'
+
+############################################################################
+#
+# test ignored sub-directories
+
+test_expect_success_multi 'ignored sub-directory' \
+       'a/b/.gitignore:5:ignored-dir/  a/b/ignored-dir' '
+       test_check_ignore "a/b/ignored-dir"
+'
+
+test_expect_success 'multiple files inside ignored sub-directory' '
+       expect_from_stdin <<-\EOF &&
+               a/b/ignored-dir/foo
+               a/b/ignored-dir/twoooo
+               a/b/ignored-dir/seven
+       EOF
+       test_check_ignore "a/b/ignored-dir/foo a/b/ignored-dir/twoooo a/b/ignored-dir/seven"
+'
+
+test_expect_success 'multiple files inside ignored sub-directory with -v' '
+       expect_from_stdin <<-\EOF &&
+               a/b/.gitignore:5:ignored-dir/   a/b/ignored-dir/foo
+               a/b/.gitignore:5:ignored-dir/   a/b/ignored-dir/twoooo
+               a/b/.gitignore:5:ignored-dir/   a/b/ignored-dir/seven
+       EOF
+       test_check_ignore "-v a/b/ignored-dir/foo a/b/ignored-dir/twoooo a/b/ignored-dir/seven"
+'
+
+test_expect_success 'cd to ignored sub-directory' '
+       expect_from_stdin <<-\EOF &&
+               foo
+               twoooo
+               ../one
+               seven
+               ../../one
+       EOF
+       (
+               cd a/b/ignored-dir &&
+               test_check_ignore "foo twoooo ../one seven ../../one"
+       )
+'
+
+test_expect_success 'cd to ignored sub-directory with -v' '
+       expect_from_stdin <<-\EOF &&
+               a/b/.gitignore:5:ignored-dir/   foo
+               a/b/.gitignore:5:ignored-dir/   twoooo
+               a/b/.gitignore:8:!on*   ../one
+               a/b/.gitignore:5:ignored-dir/   seven
+               .gitignore:1:one        ../../one
+       EOF
+       (
+               cd a/b/ignored-dir &&
+               test_check_ignore "-v foo twoooo ../one seven ../../one"
+       )
+'
+
+############################################################################
+#
+# test handling of symlinks
+
+test_expect_success_multi SYMLINKS 'symlink' '' '
+       test_check_ignore "a/symlink" 1
+'
+
+test_expect_success_multi SYMLINKS 'beyond a symlink' '' '
+       test_check_ignore "a/symlink/foo" 128 &&
+       test_stderr "fatal: '\''a/symlink/foo'\'' is beyond a symbolic link"
+'
+
+test_expect_success_multi SYMLINKS 'beyond a symlink from subdirectory' '' '
+       (
+               cd a &&
+               test_check_ignore "symlink/foo" 128
+       ) &&
+       test_stderr "fatal: '\''symlink/foo'\'' is beyond a symbolic link"
+'
+
+############################################################################
+#
+# test handling of submodules
+
+test_expect_success_multi 'submodule' '' '
+       test_check_ignore "a/submodule/one" 128 &&
+       test_stderr "fatal: Path '\''a/submodule/one'\'' is in submodule '\''a/submodule'\''"
+'
+
+test_expect_success_multi 'submodule from subdirectory' '' '
+       (
+               cd a &&
+               test_check_ignore "submodule/one" 128
+       ) &&
+       test_stderr "fatal: Path '\''a/submodule/one'\'' is in submodule '\''a/submodule'\''"
+'
+
+############################################################################
+#
+# test handling of global ignore files
+
+test_expect_success 'global ignore not yet enabled' '
+       expect_from_stdin <<-\EOF &&
+               .git/info/exclude:7:per-repo    per-repo
+               a/.gitignore:2:*three   a/globalthree
+               .git/info/exclude:7:per-repo    a/per-repo
+       EOF
+       test_check_ignore "-v globalone per-repo a/globalthree a/per-repo not-ignored a/globaltwo"
+'
+
+test_expect_success 'global ignore' '
+       enable_global_excludes &&
+       expect_from_stdin <<-\EOF &&
+               globalone
+               per-repo
+               globalthree
+               a/globalthree
+               a/per-repo
+               globaltwo
+       EOF
+       test_check_ignore "globalone per-repo globalthree a/globalthree a/per-repo not-ignored globaltwo"
+'
+
+test_expect_success 'global ignore with -v' '
+       enable_global_excludes &&
+       expect_from_stdin <<-EOF &&
+               $global_excludes:1:globalone    globalone
+               .git/info/exclude:7:per-repo    per-repo
+               $global_excludes:3:globalthree  globalthree
+               a/.gitignore:2:*three   a/globalthree
+               .git/info/exclude:7:per-repo    a/per-repo
+               $global_excludes:2:!globaltwo   globaltwo
+       EOF
+       test_check_ignore "-v globalone per-repo globalthree a/globalthree a/per-repo not-ignored globaltwo"
+'
+
+############################################################################
+#
+# test --stdin
+
+cat <<-\EOF >stdin
+       one
+       not-ignored
+       a/one
+       a/not-ignored
+       a/b/on
+       a/b/one
+       a/b/one one
+       "a/b/one two"
+       "a/b/one\"three"
+       a/b/not-ignored
+       a/b/two
+       a/b/twooo
+       globaltwo
+       a/globaltwo
+       a/b/globaltwo
+       b/globaltwo
+EOF
+cat <<-\EOF >expected-default
+       one
+       a/one
+       a/b/on
+       a/b/one
+       a/b/one one
+       a/b/one two
+       "a/b/one\"three"
+       a/b/two
+       a/b/twooo
+       globaltwo
+       a/globaltwo
+       a/b/globaltwo
+       b/globaltwo
+EOF
+cat <<-EOF >expected-verbose
+       .gitignore:1:one        one
+       .gitignore:1:one        a/one
+       a/b/.gitignore:8:!on*   a/b/on
+       a/b/.gitignore:8:!on*   a/b/one
+       a/b/.gitignore:8:!on*   a/b/one one
+       a/b/.gitignore:8:!on*   a/b/one two
+       a/b/.gitignore:8:!on*   "a/b/one\"three"
+       a/b/.gitignore:9:!two   a/b/two
+       a/.gitignore:1:two*     a/b/twooo
+       $global_excludes:2:!globaltwo   globaltwo
+       $global_excludes:2:!globaltwo   a/globaltwo
+       $global_excludes:2:!globaltwo   a/b/globaltwo
+       $global_excludes:2:!globaltwo   b/globaltwo
+EOF
+
+sed -e 's/^"//' -e 's/\\//' -e 's/"$//' stdin | \
+       tr "\n" "\0" >stdin0
+sed -e 's/^"//' -e 's/\\//' -e 's/"$//' expected-default | \
+       tr "\n" "\0" >expected-default0
+sed -e 's/     "/      /' -e 's/\\//' -e 's/"$//' expected-verbose | \
+       tr ":\t\n" "\0" >expected-verbose0
+
+test_expect_success '--stdin' '
+       expect_from_stdin <expected-default &&
+       test_check_ignore "--stdin" <stdin
+'
+
+test_expect_success '--stdin -q' '
+       expect "" &&
+       test_check_ignore "-q --stdin" <stdin
+'
+
+test_expect_success '--stdin -v' '
+       expect_from_stdin <expected-verbose &&
+       test_check_ignore "-v --stdin" <stdin
+'
+
+for opts in '--stdin -z' '-z --stdin'
+do
+       test_expect_success "$opts" "
+               expect_from_stdin <expected-default0 &&
+               test_check_ignore '$opts' <stdin0
+       "
+
+       test_expect_success "$opts -q" "
+               expect "" &&
+               test_check_ignore '-q $opts' <stdin0
+       "
+
+       test_expect_success "$opts -v" "
+               expect_from_stdin <expected-verbose0 &&
+               test_check_ignore '-v $opts' <stdin0
+       "
+done
+
+cat <<-\EOF >stdin
+       ../one
+       ../not-ignored
+       one
+       not-ignored
+       b/on
+       b/one
+       b/one one
+       "b/one two"
+       "b/one\"three"
+       b/two
+       b/not-ignored
+       b/twooo
+       ../globaltwo
+       globaltwo
+       b/globaltwo
+       ../b/globaltwo
+EOF
+cat <<-\EOF >expected-default
+       ../one
+       one
+       b/on
+       b/one
+       b/one one
+       b/one two
+       "b/one\"three"
+       b/two
+       b/twooo
+       ../globaltwo
+       globaltwo
+       b/globaltwo
+       ../b/globaltwo
+EOF
+cat <<-EOF >expected-verbose
+       .gitignore:1:one        ../one
+       .gitignore:1:one        one
+       a/b/.gitignore:8:!on*   b/on
+       a/b/.gitignore:8:!on*   b/one
+       a/b/.gitignore:8:!on*   b/one one
+       a/b/.gitignore:8:!on*   b/one two
+       a/b/.gitignore:8:!on*   "b/one\"three"
+       a/b/.gitignore:9:!two   b/two
+       a/.gitignore:1:two*     b/twooo
+       $global_excludes:2:!globaltwo   ../globaltwo
+       $global_excludes:2:!globaltwo   globaltwo
+       $global_excludes:2:!globaltwo   b/globaltwo
+       $global_excludes:2:!globaltwo   ../b/globaltwo
+EOF
+
+sed -e 's/^"//' -e 's/\\//' -e 's/"$//' stdin | \
+       tr "\n" "\0" >stdin0
+sed -e 's/^"//' -e 's/\\//' -e 's/"$//' expected-default | \
+       tr "\n" "\0" >expected-default0
+sed -e 's/     "/      /' -e 's/\\//' -e 's/"$//' expected-verbose | \
+       tr ":\t\n" "\0" >expected-verbose0
+
+test_expect_success '--stdin from subdirectory' '
+       expect_from_stdin <expected-default &&
+       (
+               cd a &&
+               test_check_ignore "--stdin" <../stdin
+       )
+'
+
+test_expect_success '--stdin from subdirectory with -v' '
+       expect_from_stdin <expected-verbose &&
+       (
+               cd a &&
+               test_check_ignore "--stdin -v" <../stdin
+       )
+'
+
+for opts in '--stdin -z' '-z --stdin'
+do
+       test_expect_success "$opts from subdirectory" '
+               expect_from_stdin <expected-default0 &&
+               (
+                       cd a &&
+                       test_check_ignore "'"$opts"'" <../stdin0
+               )
+       '
+
+       test_expect_success "$opts from subdirectory with -v" '
+               expect_from_stdin <expected-verbose0 &&
+               (
+                       cd a &&
+                       test_check_ignore "'"$opts"' -v" <../stdin0
+               )
+       '
+done
+
+
+test_done
index 70edbb33e26c0ee6d5cec81d2417c8a830d6fab1..06b18f8bc1e2991170736254bf3c558ab4673d13 100755 (executable)
@@ -23,7 +23,7 @@ test_expect_success '"reset <submodule>" updates the index' '
        git update-index --refresh &&
        git diff-files --quiet &&
        git diff-index --quiet --cached HEAD &&
-       test_must_fail git reset HEAD^ submodule &&
+       git reset HEAD^ submodule &&
        test_must_fail git diff-files --quiet &&
        git reset submodule &&
        git diff-files --quiet
index aae30d97b1a0b95f9cae5c2659f77cf2cb9c3947..842b7549ec3ed6b1fa268a96d085048d2f0b7793 100755 (executable)
@@ -337,6 +337,62 @@ test_expect_success 'Log output (complex mapping)' '
        test_cmp expect actual
 '
 
+cat >expect <<\EOF
+Author: CTO <cto@company.xx>
+Author: Santa Claus <santa.claus@northpole.xx>
+Author: Santa Claus <santa.claus@northpole.xx>
+Author: Other Author <other@author.xx>
+Author: Other Author <other@author.xx>
+Author: Some Dude <some@dude.xx>
+Author: A U Thor <author@example.com>
+EOF
+
+test_expect_success 'Log output with --use-mailmap' '
+       git log --use-mailmap | grep Author >actual &&
+       test_cmp expect actual
+'
+
+cat >expect <<\EOF
+Author: CTO <cto@company.xx>
+Author: Santa Claus <santa.claus@northpole.xx>
+Author: Santa Claus <santa.claus@northpole.xx>
+Author: Other Author <other@author.xx>
+Author: Other Author <other@author.xx>
+Author: Some Dude <some@dude.xx>
+Author: A U Thor <author@example.com>
+EOF
+
+test_expect_success 'Log output with log.mailmap' '
+       git -c log.mailmap=True log | grep Author >actual &&
+       test_cmp expect actual
+'
+
+cat >expect <<\EOF
+Author: Santa Claus <santa.claus@northpole.xx>
+Author: Santa Claus <santa.claus@northpole.xx>
+EOF
+
+test_expect_success 'Grep author with --use-mailmap' '
+       git log --use-mailmap --author Santa | grep Author >actual &&
+       test_cmp expect actual
+'
+cat >expect <<\EOF
+Author: Santa Claus <santa.claus@northpole.xx>
+Author: Santa Claus <santa.claus@northpole.xx>
+EOF
+
+test_expect_success 'Grep author with log.mailmap' '
+       git -c log.mailmap=True log --author Santa | grep Author >actual &&
+       test_cmp expect actual
+'
+
+>expect
+
+test_expect_success 'Only grep replaced author with --use-mailmap' '
+       git log --use-mailmap --author "<cto@coompany.xx>" >actual &&
+       test_cmp expect actual
+'
+
 # git blame
 cat >expect <<\EOF
 ^OBJI (A U Thor     DATE 1) one
index 60093728fecd626ae5e005c4d41cee330ac1112c..8f024a08f0b175c12747c33ac0a07cacb73937d9 100755 (executable)
@@ -950,27 +950,6 @@ test_expect_success 'push requires --force to update lightweight tag' '
        )
 '
 
-test_expect_success 'push requires --force to update annotated tag' '
-       mk_test heads/master &&
-       mk_child child1 &&
-       mk_child child2 &&
-       (
-               cd child1 &&
-               git tag -a -m "message 1" Tag &&
-               git push ../child2 Tag:refs/tmp/Tag &&
-               git push ../child2 Tag:refs/tmp/Tag &&
-               >file1 &&
-               git add file1 &&
-               git commit -m "file1" &&
-               git tag -f -a -m "message 2" Tag &&
-               test_must_fail git push ../child2 Tag:refs/tmp/Tag &&
-               git push --force ../child2 Tag:refs/tmp/Tag &&
-               git tag -f -a -m "message 3" Tag HEAD~ &&
-               test_must_fail git push ../child2 Tag:refs/tmp/Tag &&
-               git push --force ../child2 Tag:refs/tmp/Tag
-       )
-'
-
 test_expect_success 'push --porcelain' '
        mk_empty &&
        echo >.git/foo  "To testrepo" &&
index 6750961507bbfa1733785bb0b15e1e79ac8dd66e..1e683d42203c4a7077481c1ffc262a493612d4bf 100755 (executable)
@@ -145,4 +145,25 @@ test_expect_failure 'push new branch with old:new refspec' '
        compare_refs clone HEAD server refs/heads/new-refspec
 '
 
+test_expect_success 'proper failure checks for fetching' '
+       (GIT_REMOTE_TESTGIT_FAILURE=1 &&
+       export GIT_REMOTE_TESTGIT_FAILURE &&
+       cd localclone &&
+       test_must_fail git fetch 2>&1 | \
+               grep "Error while running fast-import"
+       )
+'
+
+# We sleep to give fast-export a chance to catch the SIGPIPE
+test_expect_failure 'proper failure checks for pushing' '
+       (GIT_REMOTE_TESTGIT_FAILURE=1 &&
+       export GIT_REMOTE_TESTGIT_FAILURE &&
+       GIT_REMOTE_TESTGIT_SLEEPY=1 &&
+       export GIT_REMOTE_TESTGIT_SLEEPY &&
+       cd localclone &&
+       test_must_fail git push --all 2>&1 | \
+               grep "Error while running fast-export"
+       )
+'
+
 test_done
diff --git a/t/t7061-wtstatus-ignore.sh b/t/t7061-wtstatus-ignore.sh
new file mode 100755 (executable)
index 0000000..0da1214
--- /dev/null
@@ -0,0 +1,146 @@
+#!/bin/sh
+
+test_description='git-status ignored files'
+
+. ./test-lib.sh
+
+cat >expected <<\EOF
+?? .gitignore
+?? actual
+?? expected
+?? untracked/
+EOF
+
+test_expect_success 'status untracked directory with --ignored' '
+       echo "ignored" >.gitignore &&
+       mkdir untracked &&
+       : >untracked/ignored &&
+       : >untracked/uncommitted &&
+       git status --porcelain --ignored >actual &&
+       test_cmp expected actual
+'
+
+cat >expected <<\EOF
+?? .gitignore
+?? actual
+?? expected
+?? untracked/uncommitted
+!! untracked/ignored
+EOF
+
+test_expect_success 'status untracked directory with --ignored -u' '
+       git status --porcelain --ignored -u >actual &&
+       test_cmp expected actual
+'
+
+cat >expected <<\EOF
+?? .gitignore
+?? actual
+?? expected
+!! ignored/
+EOF
+
+test_expect_success 'status ignored directory with --ignore' '
+       rm -rf untracked &&
+       mkdir ignored &&
+       : >ignored/uncommitted &&
+       git status --porcelain --ignored >actual &&
+       test_cmp expected actual
+'
+
+cat >expected <<\EOF
+?? .gitignore
+?? actual
+?? expected
+!! ignored/uncommitted
+EOF
+
+test_expect_success 'status ignored directory with --ignore -u' '
+       git status --porcelain --ignored -u >actual &&
+       test_cmp expected actual
+'
+
+cat >expected <<\EOF
+?? .gitignore
+?? actual
+?? expected
+!! untracked-ignored/
+EOF
+
+test_expect_success 'status untracked directory with ignored files with --ignore' '
+       rm -rf ignored &&
+       mkdir untracked-ignored &&
+       mkdir untracked-ignored/test &&
+       : >untracked-ignored/ignored &&
+       : >untracked-ignored/test/ignored &&
+       git status --porcelain --ignored >actual &&
+       test_cmp expected actual
+'
+
+cat >expected <<\EOF
+?? .gitignore
+?? actual
+?? expected
+!! untracked-ignored/ignored
+!! untracked-ignored/test/ignored
+EOF
+
+test_expect_success 'status untracked directory with ignored files with --ignore -u' '
+       git status --porcelain --ignored -u >actual &&
+       test_cmp expected actual
+'
+
+cat >expected <<\EOF
+?? .gitignore
+?? actual
+?? expected
+EOF
+
+test_expect_success 'status ignored tracked directory with --ignore' '
+       rm -rf untracked-ignored &&
+       mkdir tracked &&
+       : >tracked/committed &&
+       git add tracked/committed &&
+       git commit -m. &&
+       echo "tracked" >.gitignore &&
+       git status --porcelain --ignored >actual &&
+       test_cmp expected actual
+'
+
+cat >expected <<\EOF
+?? .gitignore
+?? actual
+?? expected
+EOF
+
+test_expect_success 'status ignored tracked directory with --ignore -u' '
+       git status --porcelain --ignored -u >actual &&
+       test_cmp expected actual
+'
+
+cat >expected <<\EOF
+?? .gitignore
+?? actual
+?? expected
+!! tracked/
+EOF
+
+test_expect_success 'status ignored tracked directory and uncommitted file with --ignore' '
+       : >tracked/uncommitted &&
+       git status --porcelain --ignored >actual &&
+       test_cmp expected actual
+'
+
+cat >expected <<\EOF
+?? .gitignore
+?? actual
+?? expected
+!! tracked/uncommitted
+EOF
+
+test_expect_success 'status ignored tracked directory and uncommitted file with --ignore -u' '
+       git status --porcelain --ignored -u >actual &&
+       test_cmp expected actual
+'
+
+test_done
index b096dc88c2f4caf28887cf8fea5d700f0132f5a3..1fa2a5fc23da25643552cdf43408cb75480194ff 100755 (executable)
@@ -388,7 +388,8 @@ test_expect_success 'test --mixed <paths>' '
        echo 4 > file4 &&
        echo 5 > file1 &&
        git add file1 file3 file4 &&
-       test_must_fail git reset HEAD -- file1 file2 file3 &&
+       git reset HEAD -- file1 file2 file3 &&
+       test_must_fail git diff --quiet &&
        git diff > output &&
        test_cmp output expect &&
        git diff --cached > output &&
@@ -402,7 +403,8 @@ test_expect_success 'test resetting the index at give paths' '
        >sub/file2 &&
        git update-index --add sub/file1 sub/file2 &&
        T=$(git write-tree) &&
-       test_must_fail git reset HEAD sub/file2 &&
+       git reset HEAD sub/file2 &&
+       test_must_fail git diff --quiet &&
        U=$(git write-tree) &&
        echo "$T" &&
        echo "$U" &&
@@ -440,7 +442,8 @@ test_expect_success 'resetting specific path that is unmerged' '
                echo "100644 $F3 3      file2"
        } | git update-index --index-info &&
        git ls-files -u &&
-       test_must_fail git reset HEAD file2 &&
+       git reset HEAD file2 &&
+       test_must_fail git diff --quiet &&
        git diff-index --exit-code --cached HEAD
 '
 
@@ -449,7 +452,8 @@ test_expect_success 'disambiguation (1)' '
        git reset --hard &&
        >secondfile &&
        git add secondfile &&
-       test_must_fail git reset secondfile &&
+       git reset secondfile &&
+       test_must_fail git diff --quiet -- secondfile &&
        test -z "$(git diff --cached --name-only)" &&
        test -f secondfile &&
        test ! -s secondfile
@@ -474,7 +478,8 @@ test_expect_success 'disambiguation (3)' '
        >secondfile &&
        git add secondfile &&
        rm -f secondfile &&
-       test_must_fail git reset HEAD secondfile &&
+       git reset HEAD secondfile &&
+       test_must_fail git diff --quiet &&
        test -z "$(git diff --cached --name-only)" &&
        test ! -f secondfile
 
@@ -486,9 +491,18 @@ test_expect_success 'disambiguation (4)' '
        >secondfile &&
        git add secondfile &&
        rm -f secondfile &&
-       test_must_fail git reset -- secondfile &&
+       git reset -- secondfile &&
+       test_must_fail git diff --quiet &&
        test -z "$(git diff --cached --name-only)" &&
        test ! -f secondfile
 '
 
+test_expect_success 'reset with paths accepts tree' '
+       # for simpler tests, drop last commit containing added files
+       git reset --hard HEAD^ &&
+       git reset HEAD^^{tree} -- . &&
+       git diff --cached HEAD^ --exit-code &&
+       git diff HEAD --exit-code
+'
+
 test_done
diff --git a/t/t7106-reset-unborn-branch.sh b/t/t7106-reset-unborn-branch.sh
new file mode 100755 (executable)
index 0000000..8062cf5
--- /dev/null
@@ -0,0 +1,52 @@
+#!/bin/sh
+
+test_description='git reset should work on unborn branch'
+. ./test-lib.sh
+
+test_expect_success 'setup' '
+       echo a >a &&
+       echo b >b
+'
+
+test_expect_success 'reset' '
+       git add a b &&
+       git reset &&
+       test "$(git ls-files)" = ""
+'
+
+test_expect_success 'reset HEAD' '
+       rm .git/index &&
+       git add a b &&
+       test_must_fail git reset HEAD
+'
+
+test_expect_success 'reset $file' '
+       rm .git/index &&
+       git add a b &&
+       git reset a &&
+       test "$(git ls-files)" = "b"
+'
+
+test_expect_success 'reset -p' '
+       rm .git/index &&
+       git add a &&
+       echo y | git reset -p &&
+       test "$(git ls-files)" = ""
+'
+
+test_expect_success 'reset --soft is a no-op' '
+       rm .git/index &&
+       git add a &&
+       git reset --soft
+       test "$(git ls-files)" = "a"
+'
+
+test_expect_success 'reset --hard' '
+       rm .git/index &&
+       git add a &&
+       git reset --hard &&
+       test "$(git ls-files)" = "" &&
+       test_path_is_missing a
+'
+
+test_done
diff --git a/t/t7500/add-content-and-comment b/t/t7500/add-content-and-comment
new file mode 100755 (executable)
index 0000000..c4dccff
--- /dev/null
@@ -0,0 +1,5 @@
+#!/bin/sh
+echo "commit message" >> "$1"
+echo "# comment" >> "$1"
+exit 0
+
index 1a5cb6983c527b6d3e1ad6fad427d3513e8704b1..b1c76483866f55b4429fc76e7035e65984e7b4c2 100755 (executable)
@@ -4,6 +4,15 @@ test_description='git commit porcelain-ish'
 
 . ./test-lib.sh
 
+commit_msg_is () {
+       expect=commit_msg_is.expect
+       actual=commit_msg_is.actual
+
+       printf "%s" "$(git log --pretty=format:%s%b -1)" >$actual &&
+       printf "%s" "$1" >$expect &&
+       test_i18ncmp $expect $actual
+}
+
 # Arguments: [<prefix] [<commit message>] [<commit options>]
 check_summary_oneline() {
        test_tick &&
@@ -168,7 +177,7 @@ test_expect_success 'verbose respects diff config' '
        git config --unset color.diff
 '
 
-test_expect_success 'cleanup commit messages (verbatim,-t)' '
+test_expect_success 'cleanup commit messages (verbatim option,-t)' '
 
        echo >>negative &&
        { echo;echo "# text";echo; } >expect &&
@@ -178,7 +187,7 @@ test_expect_success 'cleanup commit messages (verbatim,-t)' '
 
 '
 
-test_expect_success 'cleanup commit messages (verbatim,-F)' '
+test_expect_success 'cleanup commit messages (verbatim option,-F)' '
 
        echo >>negative &&
        git commit --cleanup=verbatim -F expect -a &&
@@ -187,7 +196,7 @@ test_expect_success 'cleanup commit messages (verbatim,-F)' '
 
 '
 
-test_expect_success 'cleanup commit messages (verbatim,-m)' '
+test_expect_success 'cleanup commit messages (verbatim option,-m)' '
 
        echo >>negative &&
        git commit --cleanup=verbatim -m "$(cat expect)" -a &&
@@ -196,7 +205,7 @@ test_expect_success 'cleanup commit messages (verbatim,-m)' '
 
 '
 
-test_expect_success 'cleanup commit messages (whitespace,-F)' '
+test_expect_success 'cleanup commit messages (whitespace option,-F)' '
 
        echo >>negative &&
        { echo;echo "# text";echo; } >text &&
@@ -207,7 +216,7 @@ test_expect_success 'cleanup commit messages (whitespace,-F)' '
 
 '
 
-test_expect_success 'cleanup commit messages (strip,-F)' '
+test_expect_success 'cleanup commit messages (strip option,-F)' '
 
        echo >>negative &&
        { echo;echo "# text";echo sample;echo; } >text &&
@@ -218,7 +227,7 @@ test_expect_success 'cleanup commit messages (strip,-F)' '
 
 '
 
-test_expect_success 'cleanup commit messages (strip,-F,-e)' '
+test_expect_success 'cleanup commit messages (strip option,-F,-e)' '
 
        echo >>negative &&
        { echo;echo sample;echo; } >text &&
@@ -231,10 +240,71 @@ echo "sample
 # Please enter the commit message for your changes. Lines starting
 # with '#' will be ignored, and an empty message aborts the commit." >expect
 
-test_expect_success 'cleanup commit messages (strip,-F,-e): output' '
+test_expect_success 'cleanup commit messages (strip option,-F,-e): output' '
        test_i18ncmp expect actual
 '
 
+test_expect_success 'cleanup commit message (fail on invalid cleanup mode option)' '
+       test_must_fail git commit --cleanup=non-existent
+'
+
+test_expect_success 'cleanup commit message (fail on invalid cleanup mode configuration)' '
+       test_must_fail git -c commit.cleanup=non-existent commit
+'
+
+test_expect_success 'cleanup commit message (no config and no option uses default)' '
+       echo content >>file &&
+       git add file &&
+       test_set_editor "$TEST_DIRECTORY"/t7500/add-content-and-comment &&
+       git commit --no-status &&
+       commit_msg_is "commit message"
+'
+
+test_expect_success 'cleanup commit message (option overrides default)' '
+       echo content >>file &&
+       git add file &&
+       test_set_editor "$TEST_DIRECTORY"/t7500/add-content-and-comment &&
+       git commit --cleanup=whitespace --no-status &&
+       commit_msg_is "commit message # comment"
+'
+
+test_expect_success 'cleanup commit message (config overrides default)' '
+       echo content >>file &&
+       git add file &&
+       test_set_editor "$TEST_DIRECTORY"/t7500/add-content-and-comment &&
+       git -c commit.cleanup=whitespace commit --no-status &&
+       commit_msg_is "commit message # comment"
+'
+
+test_expect_success 'cleanup commit message (option overrides config)' '
+       echo content >>file &&
+       git add file &&
+       test_set_editor "$TEST_DIRECTORY"/t7500/add-content-and-comment &&
+       git -c commit.cleanup=whitespace commit --cleanup=default &&
+       commit_msg_is "commit message"
+'
+
+test_expect_success 'cleanup commit message (default, -m)' '
+       echo content >>file &&
+       git add file &&
+       git commit -m "message #comment " &&
+       commit_msg_is "message #comment"
+'
+
+test_expect_success 'cleanup commit message (whitespace option, -m)' '
+       echo content >>file &&
+       git add file &&
+       git commit --cleanup=whitespace --no-status -m "message #comment " &&
+       commit_msg_is "message #comment"
+'
+
+test_expect_success 'cleanup commit message (whitespace config, -m)' '
+       echo content >>file &&
+       git add file &&
+       git -c commit.cleanup=whitespace commit --no-status -m "message #comment " &&
+       commit_msg_is "message #comment"
+'
+
 test_expect_success 'message shows author when it is not equal to committer' '
        echo >>negative &&
        git commit -e -m "sample" -a &&
diff --git a/t/t9402-git-cvsserver-refs.sh b/t/t9402-git-cvsserver-refs.sh
new file mode 100755 (executable)
index 0000000..735a018
--- /dev/null
@@ -0,0 +1,551 @@
+#!/bin/sh
+
+test_description='git-cvsserver and git refspecs
+
+tests ability for git-cvsserver to switch between and compare
+tags, branches and other git refspecs'
+
+. ./test-lib.sh
+
+#########
+
+check_start_tree() {
+       rm -f "$WORKDIR/list.expected"
+       echo "start $1" >>"${WORKDIR}/check.log"
+}
+
+check_file() {
+       sandbox="$1"
+       file="$2"
+       ver="$3"
+       GIT_DIR=$SERVERDIR git show "${ver}:${file}" \
+               >"$WORKDIR/check.got" 2>"$WORKDIR/check.stderr"
+       test_cmp "$WORKDIR/check.got" "$sandbox/$file"
+       stat=$?
+       echo "check_file $sandbox $file $ver : $stat" >>"$WORKDIR/check.log"
+       echo "$file" >>"$WORKDIR/list.expected"
+       return $stat
+}
+
+check_end_tree() {
+       sandbox="$1" &&
+       find "$sandbox" -name CVS -prune -o -type f -print >"$WORKDIR/list.actual" &&
+       sort <"$WORKDIR/list.expected" >expected &&
+       sort <"$WORKDIR/list.actual" | sed -e "s%cvswork/%%" >actual &&
+       test_cmp expected actual &&
+       rm expected actual
+}
+
+check_end_full_tree() {
+       sandbox="$1" &&
+       sort <"$WORKDIR/list.expected" >expected &&
+       find "$sandbox" -name CVS -prune -o -type f -print |
+       sed -e "s%$sandbox/%%" | sort >act1 &&
+       test_cmp expected act1 &&
+       git ls-tree --name-only -r "$2" | sort >act2 &&
+       test_cmp expected act2 &&
+       rm expected act1 act2
+}
+
+#########
+
+check_diff() {
+       diffFile="$1"
+       vOld="$2"
+       vNew="$3"
+       rm -rf diffSandbox
+       git clone -q -n . diffSandbox &&
+       (
+               cd diffSandbox &&
+               git checkout "$vOld" &&
+               git apply -p0 --index <"../$diffFile" &&
+               git diff --exit-code "$vNew"
+       ) >check_diff_apply.out 2>&1
+}
+
+#########
+
+cvs >/dev/null 2>&1
+if test $? -ne 1
+then
+       skip_all='skipping git-cvsserver tests, cvs not found'
+       test_done
+fi
+if ! test_have_prereq PERL
+then
+       skip_all='skipping git-cvsserver tests, perl not available'
+       test_done
+fi
+"$PERL_PATH" -e 'use DBI; use DBD::SQLite' >/dev/null 2>&1 || {
+       skip_all='skipping git-cvsserver tests, Perl SQLite interface unavailable'
+       test_done
+}
+
+unset GIT_DIR GIT_CONFIG
+WORKDIR=$(pwd)
+SERVERDIR=$(pwd)/gitcvs.git
+git_config="$SERVERDIR/config"
+CVSROOT=":fork:$SERVERDIR"
+CVSWORK="$(pwd)/cvswork"
+CVS_SERVER=git-cvsserver
+export CVSROOT CVS_SERVER
+
+rm -rf "$CVSWORK" "$SERVERDIR"
+test_expect_success 'setup v1, b1' '
+       echo "Simple text file" >textfile.c &&
+       echo "t2" >t2 &&
+       mkdir adir &&
+       echo "adir/afile line1" >adir/afile &&
+       echo "adir/afile line2" >>adir/afile &&
+       echo "adir/afile line3" >>adir/afile &&
+       echo "adir/afile line4" >>adir/afile &&
+       echo "adir/a2file" >>adir/a2file &&
+       mkdir adir/bdir &&
+       echo "adir/bdir/bfile line 1" >adir/bdir/bfile &&
+       echo "adir/bdir/bfile line 2" >>adir/bdir/bfile &&
+       echo "adir/bdir/b2file" >adir/bdir/b2file &&
+       git add textfile.c t2 adir &&
+       git commit -q -m "First Commit (v1)" &&
+       git tag v1 &&
+       git branch b1 &&
+       git clone -q --bare "$WORKDIR/.git" "$SERVERDIR" >/dev/null 2>&1 &&
+       GIT_DIR="$SERVERDIR" git config --bool gitcvs.enabled true &&
+       GIT_DIR="$SERVERDIR" git config gitcvs.logfile "$SERVERDIR/gitcvs.log"
+'
+
+rm -rf cvswork
+test_expect_success 'cvs co v1' '
+       cvs -f -Q co -r v1 -d cvswork master >cvs.log 2>&1 &&
+       check_start_tree cvswork &&
+       check_file cvswork textfile.c v1 &&
+       check_file cvswork t2 v1 &&
+       check_file cvswork adir/afile v1 &&
+       check_file cvswork adir/a2file v1 &&
+       check_file cvswork adir/bdir/bfile v1 &&
+       check_file cvswork adir/bdir/b2file v1 &&
+       check_end_tree cvswork
+'
+
+rm -rf cvswork
+test_expect_success 'cvs co b1' '
+       cvs -f co -r b1 -d cvswork master >cvs.log 2>&1 &&
+       check_start_tree cvswork &&
+       check_file cvswork textfile.c v1 &&
+       check_file cvswork t2 v1 &&
+       check_file cvswork adir/afile v1 &&
+       check_file cvswork adir/a2file v1 &&
+       check_file cvswork adir/bdir/bfile v1 &&
+       check_file cvswork adir/bdir/b2file v1 &&
+       check_end_tree cvswork
+'
+
+test_expect_success 'cvs co b1 [cvswork3]' '
+       cvs -f co -r b1 -d cvswork3 master >cvs.log 2>&1 &&
+       check_start_tree cvswork3 &&
+       check_file cvswork3 textfile.c v1 &&
+       check_file cvswork3 t2 v1 &&
+       check_file cvswork3 adir/afile v1 &&
+       check_file cvswork3 adir/a2file v1 &&
+       check_file cvswork3 adir/bdir/bfile v1 &&
+       check_file cvswork3 adir/bdir/b2file v1 &&
+       check_end_full_tree cvswork3 v1
+'
+
+test_expect_success 'edit cvswork3 and save diff' '
+       (
+               cd cvswork3 &&
+               sed -e "s/line1/line1 - data/" adir/afile >adir/afileNEW &&
+               mv -f adir/afileNEW adir/afile &&
+               echo "afile5" >adir/afile5 &&
+               rm t2 &&
+               cvs -f add adir/afile5 &&
+               cvs -f rm t2 &&
+               ! cvs -f diff -N -u >"$WORKDIR/cvswork3edit.diff"
+       )
+'
+
+test_expect_success 'setup v1.2 on b1' '
+       git checkout b1 &&
+       echo "new v1.2" >t3 &&
+       rm t2 &&
+       sed -e "s/line3/line3 - more data/" adir/afile >adir/afileNEW &&
+       mv -f adir/afileNEW adir/afile &&
+       rm adir/a2file &&
+       echo "a3file" >>adir/a3file &&
+       echo "bfile line 3" >>adir/bdir/bfile &&
+       rm adir/bdir/b2file &&
+       echo "b3file" >adir/bdir/b3file &&
+       mkdir cdir &&
+       echo "cdir/cfile" >cdir/cfile &&
+       git add -A cdir adir t3 t2 &&
+       git commit -q -m 'v1.2' &&
+       git tag v1.2 &&
+       git push --tags gitcvs.git b1:b1
+'
+
+test_expect_success 'cvs -f up (on b1 adir)' '
+       ( cd cvswork/adir && cvs -f up -d ) >cvs.log 2>&1 &&
+       check_start_tree cvswork &&
+       check_file cvswork textfile.c v1 &&
+       check_file cvswork t2 v1 &&
+       check_file cvswork adir/afile v1.2 &&
+       check_file cvswork adir/a3file v1.2 &&
+       check_file cvswork adir/bdir/bfile v1.2 &&
+       check_file cvswork adir/bdir/b3file v1.2 &&
+       check_end_tree cvswork
+'
+
+test_expect_success 'cvs up (on b1 /)' '
+       ( cd cvswork && cvs -f up -d ) >cvs.log 2>&1 &&
+       check_start_tree cvswork &&
+       check_file cvswork textfile.c v1.2 &&
+       check_file cvswork t3 v1.2 &&
+       check_file cvswork adir/afile v1.2 &&
+       check_file cvswork adir/a3file v1.2 &&
+       check_file cvswork adir/bdir/bfile v1.2 &&
+       check_file cvswork adir/bdir/b3file v1.2 &&
+       check_file cvswork cdir/cfile v1.2 &&
+       check_end_tree cvswork
+'
+
+# Make sure "CVS/Tag" files didn't get messed up:
+test_expect_success 'cvs up (on b1 /) (again; check CVS/Tag files)' '
+       ( cd cvswork && cvs -f up -d ) >cvs.log 2>&1 &&
+       check_start_tree cvswork &&
+       check_file cvswork textfile.c v1.2 &&
+       check_file cvswork t3 v1.2 &&
+       check_file cvswork adir/afile v1.2 &&
+       check_file cvswork adir/a3file v1.2 &&
+       check_file cvswork adir/bdir/bfile v1.2 &&
+       check_file cvswork adir/bdir/b3file v1.2 &&
+       check_file cvswork cdir/cfile v1.2 &&
+       check_end_tree cvswork
+'
+
+# update to another version:
+test_expect_success 'cvs up -r v1' '
+       ( cd cvswork && cvs -f up -r v1 ) >cvs.log 2>&1 &&
+       check_start_tree cvswork &&
+       check_file cvswork textfile.c v1 &&
+       check_file cvswork t2 v1 &&
+       check_file cvswork adir/afile v1 &&
+       check_file cvswork adir/a2file v1 &&
+       check_file cvswork adir/bdir/bfile v1 &&
+       check_file cvswork adir/bdir/b2file v1 &&
+       check_end_tree cvswork
+'
+
+test_expect_success 'cvs up' '
+       ( cd cvswork && cvs -f up ) >cvs.log 2>&1 &&
+       check_start_tree cvswork &&
+       check_file cvswork textfile.c v1 &&
+       check_file cvswork t2 v1 &&
+       check_file cvswork adir/afile v1 &&
+       check_file cvswork adir/a2file v1 &&
+       check_file cvswork adir/bdir/bfile v1 &&
+       check_file cvswork adir/bdir/b2file v1 &&
+       check_end_tree cvswork
+'
+
+test_expect_success 'cvs up (again; check CVS/Tag files)' '
+       ( cd cvswork && cvs -f up -d ) >cvs.log 2>&1 &&
+       check_start_tree cvswork &&
+       check_file cvswork textfile.c v1 &&
+       check_file cvswork t2 v1 &&
+       check_file cvswork adir/afile v1 &&
+       check_file cvswork adir/a2file v1 &&
+       check_file cvswork adir/bdir/bfile v1 &&
+       check_file cvswork adir/bdir/b2file v1 &&
+       check_end_tree cvswork
+'
+
+test_expect_success 'setup simple b2' '
+       git branch b2 v1 &&
+       git push --tags gitcvs.git b2:b2
+'
+
+test_expect_success 'cvs co b2 [into cvswork2]' '
+       cvs -f co -r b2 -d cvswork2 master >cvs.log 2>&1 &&
+       check_start_tree cvswork &&
+       check_file cvswork textfile.c v1 &&
+       check_file cvswork t2 v1 &&
+       check_file cvswork adir/afile v1 &&
+       check_file cvswork adir/a2file v1 &&
+       check_file cvswork adir/bdir/bfile v1 &&
+       check_file cvswork adir/bdir/b2file v1 &&
+       check_end_tree cvswork
+'
+
+test_expect_success 'root dir edit [cvswork2]' '
+       (
+               cd cvswork2 && echo "Line 2" >>textfile.c &&
+               ! cvs -f diff -u >"$WORKDIR/cvsEdit1.diff" &&
+               cvs -f commit -m "edit textfile.c" textfile.c
+       ) >cvsEdit1.log 2>&1
+'
+
+test_expect_success 'root dir rm file [cvswork2]' '
+       (
+               cd cvswork2 &&
+               cvs -f rm -f t2 &&
+               cvs -f diff -u >../cvsEdit2-empty.diff &&
+               ! cvs -f diff -N -u >"$WORKDIR/cvsEdit2-N.diff" &&
+               cvs -f commit -m "rm t2"
+       ) >cvsEdit2.log 2>&1
+'
+
+test_expect_success 'subdir edit/add/rm files [cvswork2]' '
+       (
+               cd cvswork2 &&
+               sed -e "s/line 1/line 1 (v2)/" adir/bdir/bfile >adir/bdir/bfileNEW &&
+               mv -f adir/bdir/bfileNEW adir/bdir/bfile &&
+               rm adir/bdir/b2file &&
+               cd adir &&
+               cvs -f rm bdir/b2file &&
+               echo "4th file" >bdir/b4file &&
+               cvs -f add bdir/b4file &&
+               ! cvs -f diff -N -u >"$WORKDIR/cvsEdit3.diff" &&
+               git fetch gitcvs.git b2:b2 &&
+               (
+                 cd .. &&
+                 ! cvs -f diff -u -N -r v1.2 >"$WORKDIR/cvsEdit3-v1.2.diff" &&
+                 ! cvs -f diff -u -N -r v1.2 -r v1 >"$WORKDIR/cvsEdit3-v1.2-v1.diff"
+               ) &&
+               cvs -f commit -m "various add/rm/edit"
+       ) >cvs.log 2>&1
+'
+
+test_expect_success 'validate result of edits [cvswork2]' '
+       git fetch gitcvs.git b2:b2 &&
+       git tag v2 b2 &&
+       git push --tags gitcvs.git b2:b2 &&
+       check_start_tree cvswork2 &&
+       check_file cvswork2 textfile.c v2 &&
+       check_file cvswork2 adir/afile v2 &&
+       check_file cvswork2 adir/a2file v2 &&
+       check_file cvswork2 adir/bdir/bfile v2 &&
+       check_file cvswork2 adir/bdir/b4file v2 &&
+       check_end_full_tree cvswork2 v2
+'
+
+test_expect_success 'validate basic diffs saved during above cvswork2 edits' '
+       test $(grep Index: cvsEdit1.diff | wc -l) = 1 &&
+       test ! -s cvsEdit2-empty.diff &&
+       test $(grep Index: cvsEdit2-N.diff | wc -l) = 1 &&
+       test $(grep Index: cvsEdit3.diff | wc -l) = 3 &&
+       rm -rf diffSandbox &&
+       git clone -q -n . diffSandbox &&
+       (
+               cd diffSandbox &&
+               git checkout v1 &&
+               git apply -p0 --index <"$WORKDIR/cvsEdit1.diff" &&
+               git apply -p0 --index <"$WORKDIR/cvsEdit2-N.diff" &&
+               git apply -p0 --directory=adir --index <"$WORKDIR/cvsEdit3.diff" &&
+               git diff --exit-code v2
+       ) >"check_diff_apply.out" 2>&1
+'
+
+test_expect_success 'validate v1.2 diff saved during last cvswork2 edit' '
+       test $(grep Index: cvsEdit3-v1.2.diff | wc -l) = 9 &&
+       check_diff cvsEdit3-v1.2.diff v1.2 v2
+'
+
+test_expect_success 'validate v1.2 v1 diff saved during last cvswork2 edit' '
+       test $(grep Index: cvsEdit3-v1.2-v1.diff | wc -l) = 9 &&
+       check_diff cvsEdit3-v1.2-v1.diff v1.2 v1
+'
+
+test_expect_success 'cvs up [cvswork2]' '
+       ( cd cvswork2 && cvs -f up ) >cvs.log 2>&1 &&
+       check_start_tree cvswork2 &&
+       check_file cvswork2 textfile.c v2 &&
+       check_file cvswork2 adir/afile v2 &&
+       check_file cvswork2 adir/a2file v2 &&
+       check_file cvswork2 adir/bdir/bfile v2 &&
+       check_file cvswork2 adir/bdir/b4file v2 &&
+       check_end_full_tree cvswork2 v2
+'
+
+test_expect_success 'cvs up -r b2 [back to cvswork]' '
+       ( cd cvswork && cvs -f up -r b2 ) >cvs.log 2>&1 &&
+       check_start_tree cvswork &&
+       check_file cvswork textfile.c v2 &&
+       check_file cvswork adir/afile v2 &&
+       check_file cvswork adir/a2file v2 &&
+       check_file cvswork adir/bdir/bfile v2 &&
+       check_file cvswork adir/bdir/b4file v2 &&
+       check_end_full_tree cvswork v2
+'
+
+test_expect_success 'cvs up -r b1' '
+       ( cd cvswork && cvs -f up -r b1 ) >cvs.log 2>&1 &&
+       check_start_tree cvswork &&
+       check_file cvswork textfile.c v1.2 &&
+       check_file cvswork t3 v1.2 &&
+       check_file cvswork adir/afile v1.2 &&
+       check_file cvswork adir/a3file v1.2 &&
+       check_file cvswork adir/bdir/bfile v1.2 &&
+       check_file cvswork adir/bdir/b3file v1.2 &&
+       check_file cvswork cdir/cfile v1.2 &&
+       check_end_full_tree cvswork v1.2
+'
+
+test_expect_success 'cvs up -A' '
+       ( cd cvswork && cvs -f up -A ) >cvs.log 2>&1 &&
+       check_start_tree cvswork &&
+       check_file cvswork textfile.c v1 &&
+       check_file cvswork t2 v1 &&
+       check_file cvswork adir/afile v1 &&
+       check_file cvswork adir/a2file v1 &&
+       check_file cvswork adir/bdir/bfile v1 &&
+       check_file cvswork adir/bdir/b2file v1 &&
+       check_end_full_tree cvswork v1
+'
+
+test_expect_success 'cvs up (check CVS/Tag files)' '
+       ( cd cvswork && cvs -f up ) >cvs.log 2>&1 &&
+       check_start_tree cvswork &&
+       check_file cvswork textfile.c v1 &&
+       check_file cvswork t2 v1 &&
+       check_file cvswork adir/afile v1 &&
+       check_file cvswork adir/a2file v1 &&
+       check_file cvswork adir/bdir/bfile v1 &&
+       check_file cvswork adir/bdir/b2file v1 &&
+       check_end_full_tree cvswork v1
+'
+
+# This is not really legal CVS, but it seems to work anyway:
+test_expect_success 'cvs up -r heads/b1' '
+       ( cd cvswork && cvs -f up -r heads/b1 ) >cvs.log 2>&1 &&
+       check_start_tree cvswork &&
+       check_file cvswork textfile.c v1.2 &&
+       check_file cvswork t3 v1.2 &&
+       check_file cvswork adir/afile v1.2 &&
+       check_file cvswork adir/a3file v1.2 &&
+       check_file cvswork adir/bdir/bfile v1.2 &&
+       check_file cvswork adir/bdir/b3file v1.2 &&
+       check_file cvswork cdir/cfile v1.2 &&
+       check_end_full_tree cvswork v1.2
+'
+
+# But this should work even if CVS client checks -r more carefully:
+test_expect_success 'cvs up -r heads_-s-b2 (cvsserver escape mechanism)' '
+       ( cd cvswork && cvs -f up -r heads_-s-b2 ) >cvs.log 2>&1 &&
+       check_start_tree cvswork &&
+       check_file cvswork textfile.c v2 &&
+       check_file cvswork adir/afile v2 &&
+       check_file cvswork adir/a2file v2 &&
+       check_file cvswork adir/bdir/bfile v2 &&
+       check_file cvswork adir/bdir/b4file v2 &&
+       check_end_full_tree cvswork v2
+'
+
+v1hash=$(git rev-parse v1)
+test_expect_success 'cvs up -r $(git rev-parse v1)' '
+       test -n "$v1hash" &&
+       ( cd cvswork && cvs -f up -r "$v1hash" ) >cvs.log 2>&1 &&
+       check_start_tree cvswork &&
+       check_file cvswork textfile.c v1 &&
+       check_file cvswork t2 v1 &&
+       check_file cvswork adir/afile v1 &&
+       check_file cvswork adir/a2file v1 &&
+       check_file cvswork adir/bdir/bfile v1 &&
+       check_file cvswork adir/bdir/b2file v1 &&
+       check_end_full_tree cvswork v1
+'
+
+test_expect_success 'cvs diff -r v1 -u' '
+       ( cd cvswork && cvs -f diff -r v1 -u ) >cvsDiff.out 2>cvs.log &&
+       test ! -s cvsDiff.out &&
+       test ! -s cvs.log
+'
+
+test_expect_success 'cvs diff -N -r v2 -u' '
+       ( cd cvswork && ! cvs -f diff -N -r v2 -u ) >cvsDiff.out 2>cvs.log &&
+       test ! -s cvs.log &&
+       test -s cvsDiff.out &&
+       check_diff cvsDiff.out v2 v1 >check_diff.out 2>&1
+'
+
+test_expect_success 'cvs diff -N -r v2 -r v1.2' '
+       ( cd cvswork && ! cvs -f diff -N -r v2 -r v1.2 -u ) >cvsDiff.out 2>cvs.log &&
+       test ! -s cvs.log &&
+       test -s cvsDiff.out &&
+       check_diff cvsDiff.out v2 v1.2 >check_diff.out 2>&1
+'
+
+test_expect_success 'apply early [cvswork3] diff to b3' '
+       git clone -q . gitwork3 &&
+       (
+               cd gitwork3 &&
+               git checkout -b b3 v1 &&
+               git apply -p0 --index <"$WORKDIR/cvswork3edit.diff" &&
+               git commit -m "cvswork3 edits applied"
+       ) &&
+       git fetch gitwork3 b3:b3 &&
+       git tag v3 b3
+'
+
+test_expect_success 'check [cvswork3] diff' '
+       ( cd cvswork3 && ! cvs -f diff -N -u ) >"$WORKDIR/cvsDiff.out" 2>cvs.log &&
+       test ! -s cvs.log &&
+       test -s cvsDiff.out &&
+       test $(grep Index: cvsDiff.out | wc -l) = 3 &&
+       test_cmp cvsDiff.out cvswork3edit.diff &&
+       check_diff cvsDiff.out v1 v3 >check_diff.out 2>&1
+'
+
+test_expect_success 'merge early [cvswork3] b3 with b1' '
+       ( cd gitwork3 && git merge "message" HEAD b1 ) &&
+       git fetch gitwork3 b3:b3 &&
+       git tag v3merged b3 &&
+       git push --tags gitcvs.git b3:b3
+'
+
+# This test would fail if cvsserver properly created a ".#afile"* file
+# for the merge.
+# TODO: Validate that the .# file was saved properly, and then
+#   delete/ignore it when checking the tree.
+test_expect_success 'cvs up dirty [cvswork3]' '
+       (
+               cd cvswork3 &&
+               cvs -f up &&
+               ! cvs -f diff -N -u >"$WORKDIR/cvsDiff.out"
+       ) >cvs.log 2>&1 &&
+       test -s cvsDiff.out &&
+       test $(grep Index: cvsDiff.out | wc -l) = 2 &&
+       check_start_tree cvswork3 &&
+       check_file cvswork3 textfile.c v3merged &&
+       check_file cvswork3 t3 v3merged &&
+       check_file cvswork3 adir/afile v3merged &&
+       check_file cvswork3 adir/a3file v3merged &&
+       check_file cvswork3 adir/afile5 v3merged &&
+       check_file cvswork3 adir/bdir/bfile v3merged &&
+       check_file cvswork3 adir/bdir/b3file v3merged &&
+       check_file cvswork3 cdir/cfile v3merged &&
+       check_end_full_tree cvswork3 v3merged
+'
+
+# TODO: test cvs status
+
+test_expect_success 'cvs commit [cvswork3]' '
+       (
+               cd cvswork3 &&
+               cvs -f commit -m "dirty sandbox after auto-merge"
+       ) >cvs.log 2>&1 &&
+       check_start_tree cvswork3 &&
+       check_file cvswork3 textfile.c v3merged &&
+       check_file cvswork3 t3 v3merged &&
+       check_file cvswork3 adir/afile v3merged &&
+       check_file cvswork3 adir/a3file v3merged &&
+       check_file cvswork3 adir/afile5 v3merged &&
+       check_file cvswork3 adir/bdir/bfile v3merged &&
+       check_file cvswork3 adir/bdir/b3file v3merged &&
+       check_file cvswork3 cdir/cfile v3merged &&
+       check_end_full_tree cvswork3 v3merged &&
+       git fetch gitcvs.git b3:b4 &&
+       git tag v4.1 b4 &&
+       git diff --exit-code v4.1 v3merged >check_diff_apply.out 2>&1
+'
+
+test_done
index 8c5979647ffceeb6614f0cdb58ae06170afac7c4..166e75209fb561f975a4c01f800f845e15ababae 100755 (executable)
@@ -160,9 +160,12 @@ test_expect_success 'clone --bare should make a bare repository' '
        test_when_finished cleanup_git &&
        (
                cd "$git" &&
-               test ! -d .git &&
-               bare=`git config --get core.bare` &&
-               test "$bare" = true
+               test_path_is_missing .git &&
+               git config --get --bool core.bare true &&
+               git rev-parse --verify refs/remotes/p4/master &&
+               git rev-parse --verify refs/remotes/p4/HEAD &&
+               git rev-parse --verify refs/heads/master &&
+               git rev-parse --verify HEAD
        )
 '
 
index fa40cc8bb59a4e785681013e1dc676709d808410..4f077eeca86946aa897f15e9f1729f9d0588cfb9 100755 (executable)
@@ -27,14 +27,102 @@ test_expect_success 'clone no --git-dir' '
        test_must_fail git p4 clone --git-dir=xx //depot
 '
 
-test_expect_success 'clone --branch' '
+test_expect_success 'clone --branch should checkout master' '
        git p4 clone --branch=refs/remotes/p4/sb --dest="$git" //depot &&
        test_when_finished cleanup_git &&
        (
                cd "$git" &&
-               git ls-files >files &&
-               test_line_count = 0 files &&
-               test_path_is_file .git/refs/remotes/p4/sb
+               git rev-parse refs/remotes/p4/sb >sb &&
+               git rev-parse refs/heads/master >master &&
+               test_cmp sb master &&
+               git rev-parse HEAD >head &&
+               test_cmp sb head
+       )
+'
+
+test_expect_success 'sync when no master branch prints a nice error' '
+       test_when_finished cleanup_git &&
+       git p4 clone --branch=refs/remotes/p4/sb --dest="$git" //depot@2 &&
+       (
+               cd "$git" &&
+               test_must_fail git p4 sync 2>err &&
+               grep "Error: no branch refs/remotes/p4/master" err
+       )
+'
+
+test_expect_success 'sync --branch builds the full ref name correctly' '
+       test_when_finished cleanup_git &&
+       (
+               cd "$git" &&
+               git init &&
+
+               git p4 sync --branch=b1 //depot &&
+               git rev-parse --verify refs/remotes/p4/b1 &&
+               git p4 sync --branch=p4/b2 //depot &&
+               git rev-parse --verify refs/remotes/p4/b2 &&
+
+               git p4 sync --import-local --branch=h1 //depot &&
+               git rev-parse --verify refs/heads/p4/h1 &&
+               git p4 sync --import-local --branch=p4/h2 //depot &&
+               git rev-parse --verify refs/heads/p4/h2 &&
+
+               git p4 sync --branch=refs/stuff //depot &&
+               git rev-parse --verify refs/stuff
+       )
+'
+
+# engages --detect-branches code, which will do filename filtering so
+# no sync to either b1 or b2
+test_expect_success 'sync when two branches but no master should noop' '
+       test_when_finished cleanup_git &&
+       (
+               cd "$git" &&
+               git init &&
+               git p4 sync --branch=refs/remotes/p4/b1 //depot@2 &&
+               git p4 sync --branch=refs/remotes/p4/b2 //depot@2 &&
+               git p4 sync &&
+               git show -s --format=%s refs/remotes/p4/b1 >show &&
+               grep "Initial import" show &&
+               git show -s --format=%s refs/remotes/p4/b2 >show &&
+               grep "Initial import" show
+       )
+'
+
+test_expect_success 'sync --branch updates specific branch, no detection' '
+       test_when_finished cleanup_git &&
+       (
+               cd "$git" &&
+               git init &&
+               git p4 sync --branch=b1 //depot@2 &&
+               git p4 sync --branch=b2 //depot@2 &&
+               git p4 sync --branch=b2 &&
+               git show -s --format=%s refs/remotes/p4/b1 >show &&
+               grep "Initial import" show &&
+               git show -s --format=%s refs/remotes/p4/b2 >show &&
+               grep "change 3" show
+       )
+'
+
+# allows using the refname "p4" as a short name for p4/master
+test_expect_success 'clone creates HEAD symbolic reference' '
+       git p4 clone --dest="$git" //depot &&
+       test_when_finished cleanup_git &&
+       (
+               cd "$git" &&
+               git rev-parse --verify refs/remotes/p4/master >master &&
+               git rev-parse --verify p4 >p4 &&
+               test_cmp master p4
+       )
+'
+
+test_expect_success 'clone --branch creates HEAD symbolic reference' '
+       git p4 clone --branch=refs/remotes/p4/sb --dest="$git" //depot &&
+       test_when_finished cleanup_git &&
+       (
+               cd "$git" &&
+               git rev-parse --verify refs/remotes/p4/sb >sb &&
+               git rev-parse --verify p4 >p4 &&
+               test_cmp sb p4
        )
 '
 
@@ -138,9 +226,11 @@ test_expect_success 'clone --use-client-spec' '
                View: //depot/sub/... //client2/bus/...
                EOF
        ) &&
-       P4CLIENT=client2 &&
        test_when_finished cleanup_git &&
-       git p4 clone --dest="$git" --use-client-spec //depot/... &&
+       (
+               P4CLIENT=client2 &&
+               git p4 clone --dest="$git" --use-client-spec //depot/...
+       ) &&
        (
                cd "$git" &&
                test_path_is_file bus/dir/f4 &&
@@ -153,6 +243,7 @@ test_expect_success 'clone --use-client-spec' '
                cd "$git" &&
                git init &&
                git config git-p4.useClientSpec true &&
+               P4CLIENT=client2 &&
                git p4 sync //depot/... &&
                git checkout -b master p4/master &&
                test_path_is_file bus/dir/f4 &&
@@ -160,6 +251,31 @@ test_expect_success 'clone --use-client-spec' '
        )
 '
 
+test_expect_success 'submit works with no p4/master' '
+       test_when_finished cleanup_git &&
+       git p4 clone --branch=b1 //depot@1,2 --destination="$git" &&
+       (
+               cd "$git" &&
+               test_commit submit-1-branch &&
+               git config git-p4.skipSubmitEdit true &&
+               git p4 submit --branch=b1
+       )
+'
+
+# The sync/rebase part post-submit will engage detect-branches
+# machinery which will not do anything in this particular test.
+test_expect_success 'submit works with two branches' '
+       test_when_finished cleanup_git &&
+       git p4 clone --branch=b1 //depot@1,2 --destination="$git" &&
+       (
+               cd "$git" &&
+               git p4 sync --branch=b2 //depot@1,3 &&
+               test_commit submit-2-branches &&
+               git config git-p4.skipSubmitEdit true &&
+               git p4 submit
+       )
+'
+
 test_expect_success 'kill p4d' '
        kill_p4d
 '
index d8ec408dfd8d3a78fd14aed75c5c0c2f28e66c3f..1a6c4ab08cd5adb56869855913272ec42eceb45f 100644 (file)
@@ -86,7 +86,7 @@ unset VISUAL EMAIL LANGUAGE COLUMNS $("$PERL_PATH" -e '
                PROVE
                VALGRIND
                UNZIP
-               PERF_AGGREGATING_LATER
+               PERF_
        ));
        my @vars = grep(/^GIT_/ && !/^GIT_($ok)/o, @env);
        print join("\n", @vars);
index 0e1a196ace33110e2bb74cde90d6f62358413ead..09e53df3b2a39e7ec8c7d601c5b8b8ae4de39b9a 100644 (file)
@@ -1020,7 +1020,7 @@ int unpack_trees(unsigned len, struct tree_desc *t, struct unpack_trees_options
        if (!core_apply_sparse_checkout || !o->update)
                o->skip_sparse_checkout = 1;
        if (!o->skip_sparse_checkout) {
-               if (add_excludes_from_file_to_list(git_path("info/sparse-checkout"), "", 0, NULL, &el, 0) < 0)
+               if (add_excludes_from_file_to_list(git_path("info/sparse-checkout"), "", 0, &el, 0) < 0)
                        o->skip_sparse_checkout = 1;
                else
                        o->el = &el;
index 6142421ea1172f7fe6b57486a4f41b81d92bab85..95d83135ae95b2fa7980c69cbd7b49e3a6ff2d0a 100644 (file)
@@ -603,6 +603,8 @@ static void receive_needs(void)
                        object = parse_object(sha1);
                        if (!object)
                                die("did not find object for %s", line);
+                       if (object->type != OBJ_COMMIT)
+                               die("invalid shallow object %s", sha1_to_hex(sha1));
                        object->flags |= CLIENT_SHALLOW;
                        add_object_array(object, NULL, &shallows);
                        continue;
index 2a9658bad4c5035893e9363b51985f285dd3b34c..d7cfe8f31cd3d2b4b8bdc18c5da2449255b0531b 100644 (file)
@@ -516,7 +516,9 @@ static void wt_status_collect_untracked(struct wt_status *s)
 
        if (s->show_ignored_files) {
                dir.nr = 0;
-               dir.flags = DIR_SHOW_IGNORED | DIR_SHOW_OTHER_DIRECTORIES;
+               dir.flags = DIR_SHOW_IGNORED;
+               if (s->show_untracked_files != SHOW_ALL_UNTRACKED_FILES)
+                       dir.flags |= DIR_SHOW_OTHER_DIRECTORIES;
                fill_directory(&dir, s->pathspec);
                for (i = 0; i < dir.nr; i++) {
                        struct dir_entry *ent = dir.entries[i];