diff options
132 files changed, 9577 insertions, 1769 deletions
diff --git a/.gitignore b/.gitignore index bd49cd462..20ee64242 100644 --- a/.gitignore +++ b/.gitignore @@ -96,6 +96,7 @@ git-push git-quiltimport git-read-tree git-rebase +git-rebase--interactive git-receive-pack git-reflog git-relink @@ -123,6 +124,7 @@ git-ssh-fetch git-ssh-pull git-ssh-push git-ssh-upload +git-stash git-status git-stripspace git-submodule @@ -157,7 +159,7 @@ common-cmds.h *.tar.gz *.dsc *.deb -git-core.spec +git.spec *.exe *.[aos] *.py[co] diff --git a/Documentation/Makefile b/Documentation/Makefile index 9cef4806d..f3a6c733b 100644 --- a/Documentation/Makefile +++ b/Documentation/Makefile @@ -2,7 +2,7 @@ MAN1_TXT= \ $(filter-out $(addsuffix .txt, $(ARTICLES) $(SP_ARTICLES)), \ $(wildcard git-*.txt)) \ gitk.txt -MAN5_TXT=gitattributes.txt gitignore.txt +MAN5_TXT=gitattributes.txt gitignore.txt gitmodules.txt MAN7_TXT=git.txt DOC_HTML=$(patsubst %.txt,%.html,$(MAN1_TXT) $(MAN5_TXT) $(MAN7_TXT)) @@ -29,7 +29,7 @@ DOC_MAN7=$(patsubst %.txt,%.7,$(MAN7_TXT)) prefix?=$(HOME) bindir?=$(prefix)/bin -mandir?=$(prefix)/man +mandir?=$(prefix)/share/man man1dir=$(mandir)/man1 man5dir=$(mandir)/man5 man7dir=$(mandir)/man7 @@ -37,6 +37,9 @@ man7dir=$(mandir)/man7 ASCIIDOC=asciidoc ASCIIDOC_EXTRA = +ifdef ASCIIDOC8 +ASCIIDOC_EXTRA += -a asciidoc7compatible +endif INSTALL?=install DOC_REF = origin/man diff --git a/Documentation/RelNotes-1.5.2.2.txt b/Documentation/RelNotes-1.5.2.2.txt new file mode 100644 index 000000000..f6393f8a9 --- /dev/null +++ b/Documentation/RelNotes-1.5.2.2.txt @@ -0,0 +1,61 @@ +GIT v1.5.2.2 Release Notes +========================== + +Fixes since v1.5.2.1 +-------------------- + +* Usability fix + + - git-gui is shipped with its updated blame interface. It is + rumored that the older one was not just unusable but was + active health hazard, but this one is actually pretty. + Please see for yourself. + +* Bugfixes + + - "git checkout fubar" was utterly confused when there is a + branch fubar and a tag fubar at the same time. It correctly + checks out the branch fubar now. + + - "git clone /path/foo" to clone a local /path/foo.git + repository left an incorrect configuration. + + - "git send-email" correctly unquotes RFC 2047 quoted names in + the patch-email before using their values. + + - We did not accept number of seconds since epoch older than + year 2000 as a valid timestamp. We now interpret positive + integers more than 8 digits as such, which allows us to + express timestamps more recent than March 1973. + + - git-cvsimport did not work when you have GIT_DIR to point + your repository at a nonstandard location. + + - Some systems (notably, Solaris) lack hstrerror() to make + h_errno human readable; prepare a replacement + implementation. + + - .gitignore file listed git-core.spec but what we generate is + git.spec, and nobody noticed for a long time. + + - "git-merge-recursive" does not try to run file level merge + on binary files. + + - "git-branch --track" did not create tracking configuration + correctly when the branch name had slash in it. + + - The email address of the user specified with user.email + configuration was overriden by EMAIL environment variable. + + - The tree parser did not warn about tree entries with + nonsense file modes, and assumed they must be blobs. + + - "git log -z" without any other request to generate diff still + invoked the diff machinery, wasting cycles. + +* Documentation + + - Many updates to fix stale or missing documentation. + + - Although our documentation was primarily meant to be formatted + with AsciiDoc7, formatting with AsciiDoc8 is supported better. diff --git a/Documentation/RelNotes-1.5.2.3.txt b/Documentation/RelNotes-1.5.2.3.txt new file mode 100644 index 000000000..addb22955 --- /dev/null +++ b/Documentation/RelNotes-1.5.2.3.txt @@ -0,0 +1,27 @@ +GIT v1.5.2.3 Release Notes +========================== + +Fixes since v1.5.2.2 +-------------------- + + * Bugfixes + + - Version 2 pack index format was introduced in version 1.5.2 + to support pack files that has offset that cannot be + represented in 32-bit. The runtime code to validate such + an index mishandled such an index for an empty pack. + + - Commit walkers (most notably, fetch over http protocol) + tried to traverse commit objects contained in trees (aka + subproject); they shouldn't. + + - A build option NO_R_TO_GCC_LINKER was not explained in Makefile + comment correctly. + + * Documentation Fixes and Updates + + - git-config --regexp was not documented properly. + + - git-repack -a was not documented properly. + + - git-remote -n was not documented properly. diff --git a/Documentation/RelNotes-1.5.3.txt b/Documentation/RelNotes-1.5.3.txt index d111661a7..ef2f95b3c 100644 --- a/Documentation/RelNotes-1.5.3.txt +++ b/Documentation/RelNotes-1.5.3.txt @@ -1,4 +1,4 @@ -GIT v1.5.3 Release Notes (draft) +GIT v1.5.3 Release Notes ======================== Updates since v1.5.2 @@ -10,8 +10,23 @@ Updates since v1.5.2 * Thee are a handful pack-objects changes to help you cope better with repositories with pathologically large blobs in them. +* For people who need to import from Perforce, a front-end for + fast-import is in contrib/fast-import/ now. + +* Comes with git-gui 0.8.0. + +* Comes with updated gitk. + * New commands and options. + - "git log" learned a new option '--follow', to follow + renaming history of a single file. + + - "git-filter-branch" is a reborn cg-admin-rewritehist. + + - "git-cvsserver" learned new options (--base-path, --export-all, + --strict-paths) inspired by git-daemon. + - "git-submodule" command helps you manage the projects from the superproject that contain them. @@ -36,9 +51,45 @@ Updates since v1.5.2 - "git repack" can be told to split resulting packs to avoid exceeding limit specified with "--max-pack-size". + - "git fsck" gained --verbose option. This is really really + verbose but it might help you identify exact commit that is + corrupt in your repository. + + - "git format-patch" learned --numbered-files option. This + may be useful for MH users. + + - "git tag -n -l" shows tag annotations while listing tags. + + - "git cvsimport" can optionally use the separate-remote layout. + + - "git blame" can be told to see through commits that changes + whitespaces and indentation levels with "-w" option. + + - "git send-email" can be told not to thread the messages when + sending out more than one patches. + + - "git config" learned NUL terminated output format via -z to + help scripts. + * Updated behavior of existing commands. - - "git push" pretends that you immediately fetched back from + - "git mergetool" chooses its backend more wisely, taking + notice of its environment such as use of X, Gnome/KDE, etc. + + - "gitweb" shows merge commits a lot nicer than before. The + default view uses more compact --cc format, while the UI + allows to choose normal diff with any parent. + + - snapshot files "gitweb" creates from a repository at + $path/$project/.git are more useful. We use $project part + in the filename, which we used to discard. + + - "git cvsimort" creates lightweight tag; there is not any + interesting information we can record in an annotated tag, + and the handcrafted ones the old code created was not + properly formed anyway. + + - "git-push" pretends that you immediately fetched back from the remote by updating corresponding remote tracking branches if you have any. @@ -48,17 +99,25 @@ Updates since v1.5.2 - "git-apply --whitespace=strip" removes blank lines added at the end of the file. - - fetch over git native protocols with -v shows connection + - "git-fetch" over git native protocols with -v shows connection status, and the IP address of the other end, to help diagnosing problems. - - core.legacyheaders is no more, although we still can read - objects created in a new loose object format. + - We used to have core.legacyheaders configuration, when + set to false, allowed git to write loose objects in a format + that mimicks the format used by objects stored in packs. It + turns out that this was not so useful. Although we will + continue to read objects written in that format, we do not + honor that configuration anymore and create loose objects in + the legacy/traditional format. + + - "--find-copies-harder" option to diff family can now be + spelled as "-C -C" for brevity. - "git-mailsplit" (hence "git-am") can read from Maildir formatted mailboxes. - - "git cvsserver" does not barf upon seeing "cvs login" + - "git-cvsserver" does not barf upon seeing "cvs login" request. - "pack-objects" honors "delta" attribute set in @@ -68,10 +127,25 @@ Updates since v1.5.2 - new-workdir script (in contrib) can now be used with a bare repository. + - "git-mergetool" learned to use gvimdiff. + + - "gitview" (in contrib) has a better blame interface. + + - "git log" and friends did not handle a commit log message + that is larger than 16kB; they do now. + + - "--pretty=oneline" output format for "git log" and friends + deals with "malformed" commit log messages that have more + than one lines in the first paragraph better. We used to + show the first line, cutting the title at mid-sentence; we + concatenate them into a single line and treat the result as + "oneline". * Builds - - + - old-style function definitions (most notably, a function + without parameter defined with "func()", not "func(void)") + have been eradicated. * Performance Tweaks @@ -88,6 +162,10 @@ Updates since v1.5.2 the object requested the last time, which exploits the locality of references. + - verifying pack contents done by "git fsck --full" got boost + by carefully choosing the order to verify objects in them. + + Fixes since v1.5.2 ------------------ @@ -96,14 +174,11 @@ this release, unless otherwise noted. * Bugfixes - - .... This has not - been backported to 1.5.2.x series, as it is rather an - intrusive change. - + - "gitweb" had trouble handling non UTF-8 text with older + Encode.pm Perl module. -- exec >/var/tmp/1 -O=v1.5.2-45-ged82edc -O=v1.5.2-172-g1a8b769 +O=v1.5.2.2-603-g7c85173 echo O=`git describe refs/heads/master` git shortlog --no-merges $O..refs/heads/master ^refs/heads/maint diff --git a/Documentation/asciidoc.conf b/Documentation/asciidoc.conf index 99302c5be..6b6220dfd 100644 --- a/Documentation/asciidoc.conf +++ b/Documentation/asciidoc.conf @@ -8,7 +8,8 @@ # the command. [attributes] -caret=^ +plus=+ +caret=^ startsb=[ endsb=] tilde=~ diff --git a/Documentation/cmd-list.perl b/Documentation/cmd-list.perl index a181f753e..f50f61387 100755 --- a/Documentation/cmd-list.perl +++ b/Documentation/cmd-list.perl @@ -86,6 +86,7 @@ git-check-attr purehelpers git-check-ref-format purehelpers git-cherry ancillaryinterrogators git-cherry-pick mainporcelain +git-citool mainporcelain git-clean mainporcelain git-clone mainporcelain git-commit mainporcelain @@ -111,6 +112,7 @@ git-fsck ancillaryinterrogators git-gc mainporcelain git-get-tar-commit-id ancillaryinterrogators git-grep mainporcelain +git-gui mainporcelain git-hash-object plumbingmanipulators git-http-fetch synchelpers git-http-push synchelpers @@ -176,6 +178,7 @@ git-show-ref plumbinginterrogators git-sh-setup purehelpers git-ssh-fetch synchingrepositories git-ssh-upload synchingrepositories +git-stash mainporcelain git-status mainporcelain git-stripspace purehelpers git-submodule mainporcelain diff --git a/Documentation/config.txt b/Documentation/config.txt index a2057d9d2..50503e84b 100644 --- a/Documentation/config.txt +++ b/Documentation/config.txt @@ -117,6 +117,18 @@ core.fileMode:: the working copy are ignored; useful on broken filesystems like FAT. See gitlink:git-update-index[1]. True by default. +core.quotepath:: + The commands that output paths (e.g. `ls-files`, + `diff`), when not given the `-z` option, will quote + "unusual" characters in the pathname by enclosing the + pathname in a double-quote pair and with backslashes the + same way strings in C source code are quoted. If this + variable is set to false, the bytes higher than 0x80 are + not quoted but output as verbatim. Note that double + quote, backslash and control characters are always + quoted without `-z` regardless of the setting of this + variable. + core.autocrlf:: If true, makes git convert `CRLF` at the end of lines in text files to `LF` when reading from the filesystem, and convert in reverse when @@ -172,6 +184,13 @@ repository that ends in "/.git" is assumed to be not bare (bare = false), while all other repositories are assumed to be bare (bare = true). +core.worktree:: + Set the path to the working tree. The value will not be + used in combination with repositories found automatically in + a .git directory (i.e. $GIT_DIR is not set). + This can be overriden by the GIT_WORK_TREE environment + variable and the '--work-tree' command line option. + core.logAllRefUpdates:: Updates to a ref <ref> is logged to the file "$GIT_DIR/logs/<ref>", by appending the new and old diff --git a/Documentation/diff-format.txt b/Documentation/diff-format.txt index 18d49d2c3..001503205 100644 --- a/Documentation/diff-format.txt +++ b/Documentation/diff-format.txt @@ -126,6 +126,13 @@ the file that rename/copy produces, respectively. If there is need for such substitution then the whole pathname is put in double quotes. +The similarity index is the percentage of unchanged lines, and +the dissimilarity index is the percentage of changed lines. It +is a rounded down integer, followed by a percent sign. The +similarity index value of 100% is thus reserved for two equal +files, while 100% dissimilarity means that no line from the old +file made it into the new one. + combined diff format -------------------- diff --git a/Documentation/git-branch.txt b/Documentation/git-branch.txt index 8d72bb936..bb6b57dc2 100644 --- a/Documentation/git-branch.txt +++ b/Documentation/git-branch.txt @@ -41,7 +41,7 @@ to happen. With a `-d` or `-D` option, `<branchname>` will be deleted. You may specify more than one branch for deletion. If the branch currently -has a ref log then the ref log will also be deleted. Use -r together with -d +has a reflog then the reflog will also be deleted. Use -r together with -d to delete remote-tracking branches. @@ -54,9 +54,9 @@ OPTIONS Delete a branch irrespective of its index status. -l:: - Create the branch's ref log. This activates recording of - all changes to made the branch ref, enabling use of date - based sha1 expressions such as "<branchname>@{yesterday}". + Create the branch's reflog. This activates recording of + all changes made to the branch ref, enabling use of date + based sha1 expressions such as "<branchname>@\{yesterday}". -f:: Force the creation of a new branch even if it means deleting diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt index ea26da8e2..818b720b9 100644 --- a/Documentation/git-checkout.txt +++ b/Documentation/git-checkout.txt @@ -62,9 +62,9 @@ OPTIONS configuration variable. -l:: - Create the new branch's ref log. This activates recording of - all changes to made the branch ref, enabling use of date - based sha1 expressions such as "<branchname>@{yesterday}". + Create the new branch's reflog. This activates recording of + all changes made to the branch ref, enabling use of date + based sha1 expressions such as "<branchname>@\{yesterday}". -m:: If you have local modifications to one or more files that diff --git a/Documentation/git-citool.txt b/Documentation/git-citool.txt new file mode 100644 index 000000000..5217ab223 --- /dev/null +++ b/Documentation/git-citool.txt @@ -0,0 +1,32 @@ +git-citool(1) +============= + +NAME +---- +git-citool - Graphical alternative to git-commit + +SYNOPSIS +-------- +'git citool' + +DESCRIPTION +----------- +A Tcl/Tk based graphical interface to review modified files, stage +them into the index, enter a commit message and record the new +commit onto the current branch. This interface is an alternative +to the less interactive gitlink:git-commit[1] program. + +git-citool is actually a standard alias for 'git gui citool'. +See gitlink:git-gui[1] for more details. + +Author +------ +Written by Shawn O. Pearce <spearce@spearce.org>. + +Documentation +-------------- +Documentation by Shawn O. Pearce <spearce@spearce.org>. + +GIT +--- +Part of the gitlink:git[7] suite diff --git a/Documentation/git-config.txt b/Documentation/git-config.txt index f2c67176f..5f66a7fcd 100644 --- a/Documentation/git-config.txt +++ b/Documentation/git-config.txt @@ -9,16 +9,17 @@ git-config - Get and set repository or global options SYNOPSIS -------- [verse] -'git-config' [--system | --global] name [value [value_regex]] -'git-config' [--system | --global] --add name value -'git-config' [--system | --global] --replace-all name [value [value_regex]] -'git-config' [--system | --global] [type] --get name [value_regex] -'git-config' [--system | --global] [type] --get-all name [value_regex] +'git-config' [--system | --global] [type] [-z|--null] name [value [value_regex]] +'git-config' [--system | --global] [type] --add name value +'git-config' [--system | --global] [type] --replace-all name [value [value_regex]] +'git-config' [--system | --global] [type] [-z|--null] --get name [value_regex] +'git-config' [--system | --global] [type] [-z|--null] --get-all name [value_regex] +'git-config' [--system | --global] [type] [-z|--null] --get-regexp name_regex [value_regex] 'git-config' [--system | --global] --unset name [value_regex] 'git-config' [--system | --global] --unset-all name [value_regex] 'git-config' [--system | --global] --rename-section old_name new_name 'git-config' [--system | --global] --remove-section name -'git-config' [--system | --global] -l | --list +'git-config' [--system | --global] [-z|--null] -l | --list DESCRIPTION ----------- @@ -36,8 +37,7 @@ prepend a single exclamation mark in front (see also <<EXAMPLES>>). The type specifier can be either '--int' or '--bool', which will make 'git-config' ensure that the variable(s) are of the given type and convert the value to the canonical form (simple decimal number for int, -a "true" or "false" string for bool). Type specifiers currently only -take effect for reading operations. If no type specifier is passed, +a "true" or "false" string for bool). If no type specifier is passed, no checks or transformations are performed on the value. This command will fail if: @@ -73,6 +73,7 @@ OPTIONS --get-regexp:: Like --get-all, but interprets the name as a regular expression. + Also outputs the key names. --global:: For writing options: write to global ~/.gitconfig file rather than @@ -116,6 +117,14 @@ See also <<FILES>>. in the config file will cause the value to be multiplied by 1024, 1048576, or 1073741824 prior to output. +-z, --null:: + For all options that output values and/or keys, always + end values with with the null character (instead of a + newline). Use newline instead as a delimiter between + key and value. This allows for secure parsing of the + output without getting confused e.g. by values that + contain line breaks. + [[FILES]] FILES diff --git a/Documentation/git-cvsexportcommit.txt b/Documentation/git-cvsexportcommit.txt index 827711c3c..6c423e3a2 100644 --- a/Documentation/git-cvsexportcommit.txt +++ b/Documentation/git-cvsexportcommit.txt @@ -76,7 +76,7 @@ $ git-cvsexportcommit -v <commit-sha1> $ cvs commit -F .mgs <files> ------------ -Merge pending patches into CVS automatically -- only if you really know what you are doing :: +Merge pending patches into CVS automatically -- only if you really know what you are doing:: + ------------ $ export GIT_DIR=~/project/.git diff --git a/Documentation/git-cvsserver.txt b/Documentation/git-cvsserver.txt index e5005f02f..60d0bcf0f 100644 --- a/Documentation/git-cvsserver.txt +++ b/Documentation/git-cvsserver.txt @@ -7,10 +7,53 @@ git-cvsserver - A CVS server emulator for git SYNOPSIS -------- + +SSH: + [verse] export CVS_SERVER=git-cvsserver 'cvs' -d :ext:user@server/path/repo.git co <HEAD_name> +pserver (/etc/inetd.conf): + +[verse] +cvspserver stream tcp nowait nobody /usr/bin/git-cvsserver git-cvsserver pserver + +Usage: + +[verse] +'git-cvsserver' [options] [pserver|server] [<directory> ...] + +OPTIONS +------- + +All these options obviously only make sense if enforced by the server side. +They have been implemented to resemble the gitlink:git-daemon[1] options as +closely as possible. + +--base-path <path>:: +Prepend 'path' to requested CVSROOT + +--strict-paths:: +Don't allow recursing into subdirectories + +--export-all:: +Don't check for `gitcvs.enabled` in config. You also have to specify a list +of allowed directories (see below) if you want to use this option. + +--version, -V:: +Print version information and exit + +--help, -h, -H:: +Print usage information and exit + +<directory>:: +You can specify a list of allowed directories. If no directories +are given, all are allowed. This is an additional restriction, gitcvs +access still needs to be enabled by the `gitcvs.enabled` config option +unless '--export-all' was given, too. + + DESCRIPTION ----------- diff --git a/Documentation/git-gui.txt b/Documentation/git-gui.txt new file mode 100644 index 000000000..bd613b2fc --- /dev/null +++ b/Documentation/git-gui.txt @@ -0,0 +1,115 @@ +git-gui(1) +========== + +NAME +---- +git-gui - A portable graphical interface to Git + +SYNOPSIS +-------- +'git gui' [<command>] [arguments] + +DESCRIPTION +----------- +A Tcl/Tk based graphical user interface to Git. git-gui focuses +on allowing users to make changes to their repository by making +new commits, amending existing ones, creating branches, performing +local merges, and fetching/pushing to remote repositories. + +Unlike gitlink:gitk[1], git-gui focuses on commit generation +and single file annotation, and does not show project history. +It does however supply menu actions to start a gitk session from +within git-gui. + +git-gui is known to work on all popular UNIX systems, Mac OS X, +and Windows (under both Cygwin and MSYS). To the extent possible +OS specific user interface guidelines are followed, making git-gui +a fairly native interface for users. + +COMMANDS +-------- +blame:: + Start a blame viewer on the specified file on the given + version (or working directory if not specified). + +browser:: + Start a tree browser showing all files in the specified + commit (or 'HEAD' by default). Files selected through the + browser are opened in the blame viewer. + +citool:: + Start git-gui and arrange to make exactly one commit before + exiting and returning to the shell. The interface is limited + to only commit actions, slightly reducing the application's + startup time and simplifying the menubar. + +version:: + Display the currently running version of git-gui. + + +Examples +-------- +git gui blame Makefile:: + + Show the contents of the file 'Makefile' in the current + working directory, and provide annotations for both the + original author of each line, and who moved the line to its + current location. The uncommitted file is annotated, and + uncommitted changes (if any) are explicitly attributed to + 'Not Yet Committed'. + +git gui blame v0.99.8 Makefile:: + + Show the contents of 'Makefile' in revision 'v0.99.8' + and provide annotations for each line. Unlike the above + example the file is read from the object database and not + the working directory. + +git gui citool:: + + Make one commit and return to the shell when it is complete. + +git citool:: + + Same as 'git gui citool' (above). + +git gui browser maint:: + + Show a browser for the tree of the 'maint' branch. Files + selected in the browser can be viewed with the internal + blame viewer. + +See Also +-------- +'gitk(1)':: + The git repository browser. Shows branches, commit history + and file differences. gitk is the utility started by + git-gui's Repository Visualize actions. + +Other +----- +git-gui is actually maintained as an independent project, but stable +versions are distributed as part of the Git suite for the convience +of end users. + +A git-gui development repository can be obtained from: + + git clone git://repo.or.cz/git-gui.git + +or + + git clone http://repo.or.cz/r/git-gui.git + +or browsed online at http://repo.or.cz/w/git-gui.git/[]. + +Author +------ +Written by Shawn O. Pearce <spearce@spearce.org>. + +Documentation +-------------- +Documentation by Shawn O. Pearce <spearce@spearce.org>. + +GIT +--- +Part of the gitlink:git[7] suite diff --git a/Documentation/git-log.txt b/Documentation/git-log.txt index 6157edb40..7adcdefac 100644 --- a/Documentation/git-log.txt +++ b/Documentation/git-log.txt @@ -52,7 +52,14 @@ include::pretty-options.txt[] See also gitlink:git-reflog[1]. --decorate:: - Print out the ref names of any commits that are shown. + Print out the ref names of any commits that are shown. + +--full-diff:: + Without this flag, "git log -p <paths>..." shows commits that + touch the specified paths, and diffs about the same specified + paths. With this, the full diff is shown for commits that touch + the specified paths; this means that "<paths>..." limits only + commits, and doesn't limit diff for those commits. <paths>...:: Show only commits that affect the specified paths. diff --git a/Documentation/git-push.txt b/Documentation/git-push.txt index 366c5dbdc..665f6dc70 100644 --- a/Documentation/git-push.txt +++ b/Documentation/git-push.txt @@ -53,9 +53,8 @@ side are updated. + `tag <tag>` means the same as `refs/tags/<tag>:refs/tags/<tag>`. + -A parameter <ref> without a colon is equivalent to -<ref>`:`<ref>, hence updates <ref> in the destination from <ref> -in the source. +A parameter <ref> without a colon pushes the <ref> from the source +repository to the destination repository under the same name. + Pushing an empty <src> allows you to delete the <dst> ref from the remote repository. @@ -98,6 +97,26 @@ the remote repository. include::urls.txt[] + +Examples +-------- + +git push origin master:: + Find a ref that matches `master` in the source repository + (most likely, it would find `refs/heads/master`), and update + the same ref (e.g. `refs/heads/master`) in `origin` repository + with it. + +git push origin :experimental:: + Find a ref that matches `experimental` in the `origin` repository + (e.g. `refs/heads/experimental`), and delete it. + +git push origin master:satellite/master:: + Find a ref that matches `master` in the source repository + (most likely, it would find `refs/heads/master`), and update + the ref that matches `satellite/master` (most likely, it would + be `refs/remotes/satellite/master`) in `origin` repository with it. + Author ------ Written by Junio C Hamano <junkio@cox.net>, later rewritten in C diff --git a/Documentation/git-read-tree.txt b/Documentation/git-read-tree.txt index 84184d629..74c5478ba 100644 --- a/Documentation/git-read-tree.txt +++ b/Documentation/git-read-tree.txt @@ -8,7 +8,7 @@ git-read-tree - Reads tree information into the index SYNOPSIS -------- -'git-read-tree' (<tree-ish> | [[-m [--aggressive] | --reset | --prefix=<prefix>] [-u | -i]] [--exclude-per-directory=<gitignore>] [--index-output=<file>] <tree-ish1> [<tree-ish2> [<tree-ish3>]]) +'git-read-tree' (<tree-ish> | [[-m [--trivial] [--aggressive] | --reset | --prefix=<prefix>] [-u | -i]] [--exclude-per-directory=<gitignore>] [--index-output=<file>] <tree-ish1> [<tree-ish2> [<tree-ish3>]]) DESCRIPTION @@ -50,6 +50,12 @@ OPTIONS trees that are not directly related to the current working tree status into a temporary index file. +--trivial:: + Restrict three-way merge by `git-read-tree` to happen + only if there is no file-level merging required, instead + of resolving merge for trivial cases and leaving + conflicting files unresolved in the index. + --aggressive:: Usually a three-way merge by `git-read-tree` resolves the merge for really trivial cases and leaves other diff --git a/Documentation/git-rebase.txt b/Documentation/git-rebase.txt index 0c00090a6..96907d486 100644 --- a/Documentation/git-rebase.txt +++ b/Documentation/git-rebase.txt @@ -8,7 +8,8 @@ git-rebase - Forward-port local commits to the updated upstream head SYNOPSIS -------- [verse] -'git-rebase' [-v] [--merge] [-C<n>] [--onto <newbase>] <upstream> [<branch>] +'git-rebase' [-i | --interactive] [-v | --verbose] [--merge] [-C<n>] + [-p | --preserve-merges] [--onto <newbase>] <upstream> [<branch>] 'git-rebase' --continue | --skip | --abort DESCRIPTION @@ -208,6 +209,14 @@ OPTIONS context exist they all must match. By default no context is ever ignored. +-i, \--interactive:: + Make a list of the commits which are about to be rebased. Let the + user edit that list before rebasing. + +-p, \--preserve-merges:: + Instead of ignoring merges, try to recreate them. This option + only works in interactive mode. + include::merge-strategies.txt[] NOTES @@ -226,9 +235,100 @@ pre-rebase hook script for an example. You must be in the top directory of your project to start (or continue) a rebase. Upon completion, <branch> will be the current branch. -Author +INTERACTIVE MODE +---------------- + +Rebasing interactively means that you have a chance to edit the commits +which are rebased. You can reorder the commits, and you can +remove them (weeding out bad or otherwise unwanted patches). + +The interactive mode is meant for this type of workflow: + +1. have a wonderful idea +2. hack on the code +3. prepare a series for submission +4. submit + +where point 2. consists of several instances of + +a. regular use + 1. finish something worthy of a commit + 2. commit +b. independent fixup + 1. realize that something does not work + 2. fix that + 3. commit it + +Sometimes the thing fixed in b.2. cannot be amended to the not-quite +perfect commit it fixes, because that commit is buried deeply in a +patch series. That is exactly what interactive rebase is for: use it +after plenty of "a"s and "b"s, by rearranging and editing +commits, and squashing multiple commits into one. + +Start it with the last commit you want to retain as-is: + + git rebase -i <after-this-commit> + +An editor will be fired up with all the commits in your current branch +(ignoring merge commits), which come after the given commit. You can +reorder the commits in this list to your heart's content, and you can +remove them. The list looks more or less like this: + +------------------------------------------- +pick deadbee The oneline of this commit +pick fa1afe1 The oneline of the next commit +... +------------------------------------------- + +The oneline descriptions are purely for your pleasure; `git-rebase` will +not look at them but at the commit names ("deadbee" and "fa1afe1" in this +example), so do not delete or edit the names. + +By replacing the command "pick" with the command "edit", you can tell +`git-rebase` to stop after applying that commit, so that you can edit +the files and/or the commit message, amend the commit, and continue +rebasing. + +If you want to fold two or more commits into one, replace the command +"pick" with "squash" for the second and subsequent commit. If the +commits had different authors, it will attribute the squashed commit to +the author of the last commit. + +In both cases, or when a "pick" does not succeed (because of merge +errors), the loop will stop to let you fix things, and you can continue +the loop with `git rebase --continue`. + +For example, if you want to reorder the last 5 commits, such that what +was HEAD~4 becomes the new HEAD. To achieve that, you would call +`git-rebase` like this: + +---------------------- +$ git rebase -i HEAD~5 +---------------------- + +And move the first patch to the end of the list. + +You might want to preserve merges, if you have a history like this: + +------------------ + X + \ + A---M---B + / +---o---O---P---Q +------------------ + +Suppose you want to rebase the side branch starting at "A" to "Q". Make +sure that the current HEAD is "B", and call + +----------------------------- +$ git rebase -i -p --onto Q O +----------------------------- + +Authors ------ -Written by Junio C Hamano <junkio@cox.net> +Written by Junio C Hamano <junkio@cox.net> and +Johannes E. Schindelin <johannes.schindelin@gmx.de> Documentation -------------- diff --git a/Documentation/git-receive-pack.txt b/Documentation/git-receive-pack.txt index 6914aa59c..4ef184047 100644 --- a/Documentation/git-receive-pack.txt +++ b/Documentation/git-receive-pack.txt @@ -48,8 +48,8 @@ standard input of the hook will be one line per ref to be updated: The refname value is relative to $GIT_DIR; e.g. for the master head this is "refs/heads/master". The two sha1 values before each refname are the object names for the refname before and after -the update. Refs to be created will have sha1-old equal to 0{40}, -while refs to be deleted will have sha1-new equal to 0{40}, otherwise +the update. Refs to be created will have sha1-old equal to 0\{40}, +while refs to be deleted will have sha1-new equal to 0\{40}, otherwise sha1-old and sha1-new should be valid objects in the repository. This hook is called before any refname is updated and before any @@ -71,7 +71,7 @@ The refname parameter is relative to $GIT_DIR; e.g. for the master head this is "refs/heads/master". The two sha1 arguments are the object names for the refname before and after the update. Note that the hook is called before the refname is updated, -so either sha1-old is 0{40} (meaning there is no such ref yet), +so either sha1-old is 0\{40} (meaning there is no such ref yet), or it should match what is recorded in refname. The hook should exit with non-zero status if it wants to disallow @@ -96,8 +96,8 @@ The refname value is relative to $GIT_DIR; e.g. for the master head this is "refs/heads/master". The two sha1 values before each refname are the object names for the refname before and after the update. Refs that were created will have sha1-old equal to -0{40}, while refs that were deleted will have sha1-new equal to -0{40}, otherwise sha1-old and sha1-new should be valid objects in +0\{40}, while refs that were deleted will have sha1-new equal to +0\{40}, otherwise sha1-old and sha1-new should be valid objects in the repository. Using this hook, it is easy to generate mails describing the updates diff --git a/Documentation/git-reflog.txt b/Documentation/git-reflog.txt index f717e1e30..89bc9c51e 100644 --- a/Documentation/git-reflog.txt +++ b/Documentation/git-reflog.txt @@ -39,6 +39,19 @@ the current branch. It is basically an alias for 'git log -g --abbrev-commit OPTIONS ------- +--stale-fix:: + This revamps the logic -- the definition of "broken commit" + becomes: a commit that is not reachable from any of the refs and + there is a missing object among the commit, tree, or blob + objects reachable from it that is not reachable from any of the + refs. ++ +This computation involves traversing all the reachable objects, i.e. it +has the same cost as 'git prune'. Fortunately, once this is run, we +should not have to ever worry about missing objects, because the current +prune and pack-objects know about reflogs and protect objects referred by +them. + --expire=<time>:: Entries older than this time are pruned. Without the option it is taken from configuration `gc.reflogExpire`, diff --git a/Documentation/git-remote.txt b/Documentation/git-remote.txt index ab232c2f6..61a6022ce 100644 --- a/Documentation/git-remote.txt +++ b/Documentation/git-remote.txt @@ -49,6 +49,9 @@ branch the `HEAD` at the remote repository actually points at. 'show':: Gives some information about the remote <name>. ++ +With `-n` option, the remote heads are not queried first with +`git ls-remote <name>`; cached information is used instead. 'prune':: @@ -56,6 +59,10 @@ Deletes all stale tracking branches under <name>. These stale branches have already been removed from the remote repository referenced by <name>, but are still locally available in "remotes/<name>". ++ +With `-n` option, the remote heads are not confirmed first with `git +ls-remote <name>`; cached information is used instead. Use with +caution. 'update':: diff --git a/Documentation/git-repack.txt b/Documentation/git-repack.txt index c33a512ff..28949397c 100644 --- a/Documentation/git-repack.txt +++ b/Documentation/git-repack.txt @@ -14,7 +14,8 @@ DESCRIPTION ----------- This script is used to combine all objects that do not currently -reside in a "pack", into a pack. +reside in a "pack", into a pack. It can also be used to re-organise +existing packs into a single, more efficient pack. A pack is a collection of objects, individually compressed, with delta compression applied, stored in a single file, with an @@ -28,11 +29,13 @@ OPTIONS -a:: Instead of incrementally packing the unpacked objects, - pack everything available into a single pack. + pack everything referenced into a single pack. Especially useful when packing a repository that is used for private development and there is no need to worry - about people fetching via dumb file transfer protocols - from it. Use with '-d'. + about people fetching via dumb protocols from it. Use + with '-d'. This will clean up the objects that `git prune` + leaves behind, but `git fsck --full` shows as + dangling. -d:: After packing, if the newly created packs make some diff --git a/Documentation/git-rev-list.txt b/Documentation/git-rev-list.txt index 0dba73f27..20dcac625 100644 --- a/Documentation/git-rev-list.txt +++ b/Documentation/git-rev-list.txt @@ -16,11 +16,13 @@ SYNOPSIS [ \--sparse ] [ \--no-merges ] [ \--remove-empty ] + [ \--full-history ] [ \--not ] [ \--all ] [ \--stdin ] [ \--topo-order ] [ \--parents ] + [ \--timestamp ] [ \--left-right ] [ \--cherry-pick ] [ \--encoding[=<encoding>] ] @@ -116,6 +118,9 @@ e.g. "2 hours ago". Print the parents of the commit. +--timestamp:: + Print the raw commit timestamp. + --left-right:: Mark which side of a symmetric diff a commit is reachable from. @@ -228,6 +233,14 @@ limiting may be applied. Stop when a given path disappears from the tree. +--full-history:: + + Show also parts of history irrelevant to current state of a given + path. This turns off history simplification, which removed merges + which didn't change anything at all at some child. It will still actually + simplify away merges that didn't change anything at all into either + child. + --no-merges:: Do not print commits with more than one parent. @@ -271,9 +284,9 @@ excluded from the output. + With '\--pretty' format other than oneline (for obvious reasons), this causes the output to have two extra lines of information -taken from the reflog. By default, 'commit@{Nth}' notation is +taken from the reflog. By default, 'commit@\{Nth}' notation is used in the output. When the starting commit is specified as -'commit@{now}', output also uses 'commit@{timestamp}' notation +'commit@{now}', output also uses 'commit@\{timestamp}' notation instead. Under '\--pretty=oneline', the commit message is prefixed with this information on the same line. diff --git a/Documentation/git-rev-parse.txt b/Documentation/git-rev-parse.txt index e1cb4ef85..eea9c9cfe 100644 --- a/Documentation/git-rev-parse.txt +++ b/Documentation/git-rev-parse.txt @@ -89,6 +89,17 @@ OPTIONS --git-dir:: Show `$GIT_DIR` if defined else show the path to the .git directory. +--is-inside-git-dir:: + When the current working directory is below the repository + directory print "true", otherwise "false". + +--is-inside-work-tree:: + When the current working directory is inside the work tree of the + repository print "true", otherwise "false". + +--is-bare-repository:: + When the repository is bare print "true", otherwise "false". + --short, --short=number:: Instead of outputting the full SHA1 values of object names try to abbreviate them to a shorter unique name. When no length is specified diff --git a/Documentation/git-send-email.txt b/Documentation/git-send-email.txt index 946bd76af..293686c31 100644 --- a/Documentation/git-send-email.txt +++ b/Documentation/git-send-email.txt @@ -59,9 +59,11 @@ The --cc option must be repeated for each user you want on the cc list. Only necessary if --compose is also set. If --compose is not set, this will be prompted for. ---no-signed-off-by-cc:: - Do not add emails found in Signed-off-by: or Cc: lines to the - cc list. +--signed-off-by-cc, --no-signed-off-by-cc:: + If this is set, add emails found in Signed-off-by: or Cc: lines to the + cc list. + Default is the value of 'sendemail.signedoffbycc' configuration value; + if that is unspecified, default to --signed-off-by-cc. --quiet:: Make git-send-email less verbose. One line per email should be @@ -82,9 +84,18 @@ The --cc option must be repeated for each user you want on the cc list. Only necessary if --compose is also set. If --compose is not set, this will be prompted for. ---suppress-from:: - Do not add the From: address to the cc: list, if it shows up in a From: - line. +--suppress-from, --no-suppress-from:: + If this is set, do not add the From: address to the cc: list, if it + shows up in a From: line. + Default is the value of 'sendemail.suppressfrom' configuration value; + if that is unspecified, default to --no-supress-from. + +--thread, --no-thread:: + If this is set, the In-Reply-To header will be set on each email sent. + If disabled with "--no-thread", no emails will have the In-Reply-To + header set. + Default is the value of the 'sendemail.thread' configuration value; + if that is unspecified, default to --thread. --dry-run:: Do everything except actually send the emails. diff --git a/Documentation/git-stash.txt b/Documentation/git-stash.txt new file mode 100644 index 000000000..b7d263d65 --- /dev/null +++ b/Documentation/git-stash.txt @@ -0,0 +1,161 @@ +git-stash(1) +============ + +NAME +---- +git-stash - Stash the changes in a dirty working directory away + +SYNOPSIS +-------- +[verse] +'git-stash' (save | list | show [<stash>] | apply [<stash>] | clear) + +DESCRIPTION +----------- + +Use 'git-stash save' when you want to record the current state of the +working directory and the index, but want to go back to a clean +working directory. The command saves your local modifications away +and reverts the working directory to match the `HEAD` commit. + +The modifications stashed away by this command can be listed with +`git-stash list`, inspected with `git-stash show`, and restored +(potentially on top of a different commit) with `git-stash apply`. +Calling git-stash without any arguments is equivalent to `git-stash +list`. + +The latest stash you created is stored in `$GIT_DIR/refs/stash`; older +stashes are found in the reflog of this reference and can be named using +the usual reflog syntax (e.g. `stash@\{1}` is the most recently +created stash, `stash@\{2}` is the one before it, `stash@\{2.hours.ago}` +is also possible). + +OPTIONS +------- + +save:: + + Save your local modifications to a new 'stash', and run `git-reset + --hard` to revert them. + +list:: + + List the stashes that you currently have. Each 'stash' is listed + with its name (e.g. `stash@\{0}` is the latest stash, `stash@\{1} is + the one before, etc.), the name of the branch that was current when the + stash was made, and a short description of the commit the stash was + based on. ++ +---------------------------------------------------------------- +stash@{0}: submit: 6ebd0e2... Add git-stash +stash@{1}: master: 9cc0589... Merge branch 'master' of gfi +---------------------------------------------------------------- + +show [<stash>]:: + + Show the changes recorded in the stash as a diff between the the + stashed state and its original parent. When no `<stash>` is given, + shows the latest one. By default, the command shows the diffstat, but + it will accept any format known to `git-diff` (e.g., `git-stash show + -p stash@\{2}` to view the second most recent stash in patch form). + +apply [<stash>]:: + + Restore the changes recorded in the stash on top of the current + working tree state. When no `<stash>` is given, applies the latest + one. The working directory must match the index. ++ +This operation can fail with conflicts; you need to resolve them +by hand in the working tree. + +clear:: + Remove all the stashed states. Note that those states will then + be subject to pruning, and may be difficult or impossible to recover. + + +DISCUSSION +---------- + +A stash is represented as a commit whose tree records the state of the +working directory, and its first parent is the commit at `HEAD` when +the stash was created. The tree of the second parent records the +state of the index when the stash is made, and it is made a child of +the `HEAD` commit. The ancestry graph looks like this: + + .----W + / / + ...--H----I + +where `H` is the `HEAD` commit, `I` is a commit that records the state +of the index, and `W` is a commit that records the state of the working +tree. + + +EXAMPLES +-------- + +Pulling into a dirty tree:: + +When you are in the middle of something, you learn that there are +upstream changes that are possibly relevant to what you are +doing. When your local changes do not conflict with the changes in +the upstream, a simple `git pull` will let you move forward. ++ +However, there are cases in which your local changes do conflict with +the upstream changes, and `git pull` refuses to overwrite your +changes. In such a case, you can stash your changes away, +perform a pull, and then unstash, like this: ++ +---------------------------------------------------------------- +$ git pull +... +file foobar not up to date, cannot merge. +$ git stash +$ git pull +$ git stash apply +---------------------------------------------------------------- + +Interrupted workflow:: + +When you are in the middle of something, your boss comes in and +demands that you fix something immediately. Traditionally, you would +make a commit to a temporary branch to store your changes away, and +return to your original branch to make the emergency fix, like this: ++ +---------------------------------------------------------------- +... hack hack hack ... +$ git checkout -b my_wip +$ git commit -a -m "WIP" +$ git checkout master +$ edit emergency fix +$ git commit -a -m "Fix in a hurry" +$ git checkout my_wip +$ git reset --soft HEAD^ +... continue hacking ... +---------------------------------------------------------------- ++ +You can use `git-stash` to simplify the above, like this: ++ +---------------------------------------------------------------- +... hack hack hack ... +$ git stash +$ edit emergency fix +$ git commit -a -m "Fix in a hurry" +$ git stash apply +... continue hacking ... +---------------------------------------------------------------- + +SEE ALSO +-------- +gitlink:git-checkout[1], +gitlink:git-commit[1], +gitlink:git-reflog[1], +gitlink:git-reset[1] + +AUTHOR +------ +Written by Nanako Shiraishi <nanako3@bluebottle.com> + +GIT +--- +Part of the gitlink:git[7] suite diff --git a/Documentation/git-submodule.txt b/Documentation/git-submodule.txt index f8fb80f18..7f0904e29 100644 --- a/Documentation/git-submodule.txt +++ b/Documentation/git-submodule.txt @@ -8,11 +8,19 @@ git-submodule - Initialize, update or inspect submodules SYNOPSIS -------- +'git-submodule' [--quiet] [-b branch] add <repository> [<path>] 'git-submodule' [--quiet] [--cached] [status|init|update] [--] [<path>...] COMMANDS -------- +add:: + Add the given repository as a submodule at the given path + to the changeset to be committed next. In particular, the + repository is cloned at the specified path, added to the + changeset and registered in .gitmodules. If no path is + specified, the path is deduced from the repository specification. + status:: Show the status of the submodules. This will print the SHA-1 of the currently checked out commit for each submodule, along with the @@ -39,6 +47,9 @@ OPTIONS -q, --quiet:: Only print error messages. +-b, --branch:: + Branch of repository to add as submodule. + --cached:: Display the SHA-1 stored in the index, not the SHA-1 of the currently checked out submodule commit. This option is only valid for the diff --git a/Documentation/git-svn.txt b/Documentation/git-svn.txt index c0d7d9597..0a210e4be 100644 --- a/Documentation/git-svn.txt +++ b/Documentation/git-svn.txt @@ -66,9 +66,10 @@ COMMANDS to the names of remotes if trunk/branches/tags are specified. The prefix does not automatically include a trailing slash, so be sure you include one in the - argument if that is what you want. This is useful if - you wish to track multiple projects that share a common - repository. + argument if that is what you want. If --branches/-b is + specified, the prefix must include a trailing slash. + Setting a prefix is useful if you wish to track multiple + projects that share a common repository. 'fetch':: Fetch unfetched revisions from the Subversion remote we are diff --git a/Documentation/git.txt b/Documentation/git.txt index ba077c39c..10c7bb3f4 100644 --- a/Documentation/git.txt +++ b/Documentation/git.txt @@ -10,7 +10,8 @@ SYNOPSIS -------- [verse] 'git' [--version] [--exec-path[=GIT_EXEC_PATH]] [-p|--paginate] - [--bare] [--git-dir=GIT_DIR] [--help] COMMAND [ARGS] + [--bare] [--git-dir=GIT_DIR] [--work-tree=GIT_WORK_TREE] + [--help] COMMAND [ARGS] DESCRIPTION ----------- @@ -41,9 +42,12 @@ unreleased) version of git, that is available from 'master' branch of the `git.git` repository. Documentation for older releases are available here: -* link:v1.5.2/git.html[documentation for release 1.5.2] +* link:v1.5.2.3/git.html[documentation for release 1.5.2.3] * release notes for + link:RelNotes-1.5.2.3.txt[1.5.2.3], + link:RelNotes-1.5.2.2.txt[1.5.2.2], + link:RelNotes-1.5.2.1.txt[1.5.2.1], link:RelNotes-1.5.2.txt[1.5.2]. * link:v1.5.1.6/git.html[documentation for release 1.5.1.6] @@ -101,6 +105,14 @@ OPTIONS Set the path to the repository. This can also be controlled by setting the GIT_DIR environment variable. +--work-tree=<path>:: + Set the path to the working tree. The value will not be + used in combination with repositories found automatically in + a .git directory (i.e. $GIT_DIR is not set). + This can also be controlled by setting the GIT_WORK_TREE + environment variable and the core.worktree configuration + variable. + --bare:: Same as --git-dir=`pwd`. @@ -345,6 +357,13 @@ git so take care if using Cogito etc. specifies a path to use instead of the default `.git` for the base of the repository. +'GIT_WORK_TREE':: + Set the path to the working tree. The value will not be + used in combination with repositories found automatically in + a .git directory (i.e. $GIT_DIR is not set). + This can also be controlled by the '--work-tree' command line + option and the core.worktree configuration variable. + git Commits ~~~~~~~~~~~ 'GIT_AUTHOR_NAME':: @@ -394,6 +413,16 @@ other 'GIT_PAGER':: This environment variable overrides `$PAGER`. +'GIT_FLUSH':: + If this environment variable is set to "1", then commands such + as git-blame (in incremental mode), git-rev-list, git-log, + git-whatchanged, etc., will force a flush of the output stream + after each commit-oriented record have been flushed. If this + variable is set to "0", the output of these commands will be done + using completely buffered I/O. If this environment variable is + not set, git will choose buffered or record-oriented flushing + based on whether stdout appears to be redirected to a file or not. + 'GIT_TRACE':: If this variable is set to "1", "2" or "true" (comparison is case insensitive), git will print `trace:` messages on diff --git a/Documentation/gitmodules.txt b/Documentation/gitmodules.txt new file mode 100644 index 000000000..035294e20 --- /dev/null +++ b/Documentation/gitmodules.txt @@ -0,0 +1,62 @@ +gitmodules(5) +============= + +NAME +---- +gitmodules - defining submodule properties + +SYNOPSIS +-------- +gitmodules + + +DESCRIPTION +----------- + +The `.gitmodules` file, located in the top-level directory of a git +working tree, is a text file with a syntax matching the requirements +of gitlink:git-config[1]. + +The file contains one subsection per submodule, and the subsection value +is the name of the submodule. Each submodule section also contains the +following required keys: + +submodule.<name>.path:: + Defines the path, relative to the top-level directory of the git + working tree, where the submodule is expected to be checked out. + The path name must not end with a `/`. All submodule paths must + be unique within the .gitmodules file. + +submodule.<name>.url:: + Defines an url from where the submodule repository can be cloned. + + +EXAMPLES +-------- + +Consider the following .gitmodules file: + + [submodule "libfoo"] + path = include/foo + url = git://foo.com/git/lib.git + + [submodule "libbar"] + path = include/bar + url = git://bar.com/git/lib.git + + +This defines two submodules, `libfoo` and `libbar`. These are expected to +be checked out in the paths 'include/foo' and 'include/bar', and for both +submodules an url is specified which can be used for cloning the submodules. + +SEE ALSO +-------- +gitlink:git-submodule[1] gitlink:git-config[1] + +DOCUMENTATION +------------- +Documentation by Lars Hjemli <hjemli@gmail.com> + +GIT +--- +Part of the gitlink:git[7] suite diff --git a/Documentation/pretty-options.txt b/Documentation/pretty-options.txt index 6338def5a..746bc5b7f 100644 --- a/Documentation/pretty-options.txt +++ b/Documentation/pretty-options.txt @@ -5,6 +5,15 @@ 'full', 'fuller', 'email', 'raw' and 'format:<string>'. When left out the format default to 'medium'. +--abbrev-commit:: + Instead of showing the full 40-byte hexadecimal commit object + name, show only handful hexdigits prefix. Non default number of + digits can be specified with "--abbrev=<n>" (which also modifies + diff output, if it is displayed). ++ +This should make "--pretty=oneline" a whole lot more readable for +people using 80-column terminals. + --encoding[=<encoding>]:: The commit objects record the encoding used for the log message in their encoding header; this option can be used to tell the diff --git a/Documentation/user-manual.txt b/Documentation/user-manual.txt index 0bfa21b3d..ff7c71d4f 100644 --- a/Documentation/user-manual.txt +++ b/Documentation/user-manual.txt @@ -2757,8 +2757,8 @@ As a result, the general consistency of an object can always be tested independently of the contents or the type of the object: all objects can be validated by verifying that (a) their hashes match the content of the file and (b) the object successfully inflates to a stream of bytes that -forms a sequence of <ascii type without space> + <space> + <ascii decimal -size> + <byte\0> + <binary object data>. +forms a sequence of <ascii type without space> {plus} <space> {plus} <ascii decimal +size> {plus} <byte\0> {plus} <binary object data>. The structured objects can further have their structure and connectivity to other objects verified. This is generally done with @@ -89,10 +89,16 @@ Issues of note: will include them. Note that config.mak is not distributed; the name is reserved for local settings. - - To build and install documentation suite, you need to have the - asciidoc/xmlto toolchain. Alternatively, pre-formatted - documentation are available in "html" and "man" branches of the git - repository itself. For example, you could: + - To build and install documentation suite, you need to have + the asciidoc/xmlto toolchain. Because not many people are + inclined to install the tools, the default build target + ("make all") does _not_ build them. The documentation is + written for AsciiDoc 7, but "make ASCIIDOC8=YesPlease doc" + will let you format with AsciiDoc 8. + + Alternatively, pre-formatted documentation are available in + "html" and "man" branches of the git repository itself. For + example, you could: $ mkdir manual && cd manual $ git init @@ -94,9 +94,9 @@ all:: # Define OLD_ICONV if your library has an old iconv(), where the second # (input buffer pointer) parameter is declared with type (const char **). # -# Define NO_R_TO_GCC if your gcc does not like "-R/path/lib" that -# tells runtime paths to dynamic libraries; "-Wl,-rpath=/path/lib" -# is used instead. +# Define NO_R_TO_GCC_LINKER if your gcc does not like "-R/path/lib" +# that tells runtime paths to dynamic libraries; +# "-Wl,-rpath=/path/lib" is used instead. # # Define USE_NSEC below if you want git to care about sub-second file mtimes # and ctimes. Note that you need recent glibc (at least 2.2.4) for this, and @@ -107,6 +107,8 @@ all:: # Define USE_STDEV below if you want git to care about the underlying device # change being considered an inode change from the update-cache perspective. # +# Define ASCIIDOC8 if you want to format documentation with AsciiDoc 8 +# # Define NO_PERL_MAKEMAKER if you cannot use Makefiles generated by perl's # MakeMaker (e.g. using ActiveState under Cygwin). # @@ -202,14 +204,16 @@ SCRIPT_SH = \ git-fetch.sh \ git-ls-remote.sh \ git-merge-one-file.sh git-mergetool.sh git-parse-remote.sh \ - git-pull.sh git-rebase.sh \ + git-pull.sh git-rebase.sh git-rebase--interactive.sh \ git-repack.sh git-request-pull.sh git-reset.sh \ git-sh-setup.sh \ git-tag.sh git-verify-tag.sh \ git-am.sh \ git-merge.sh git-merge-stupid.sh git-merge-octopus.sh \ git-merge-resolve.sh git-merge-ours.sh \ - git-lost-found.sh git-quiltimport.sh git-submodule.sh + git-lost-found.sh git-quiltimport.sh git-submodule.sh \ + git-filter-branch.sh \ + git-stash.sh SCRIPT_PERL = \ git-add--interactive.perl \ @@ -410,6 +414,7 @@ ifeq ($(uname_S),SunOS) NEEDS_NSL = YesPlease SHELL_PATH = /bin/bash NO_STRCASESTR = YesPlease + NO_HSTRERROR = YesPlease ifeq ($(uname_R),5.8) NEEDS_LIBICONV = YesPlease NO_UNSETENV = YesPlease @@ -654,6 +659,10 @@ endif ifdef NO_PERL_MAKEMAKER export NO_PERL_MAKEMAKER endif +ifdef NO_HSTRERROR + COMPAT_CFLAGS += -DNO_HSTRERROR + COMPAT_OBJS += compat/hstrerror.o +endif ifeq ($(TCLTK_PATH),) NO_TCLTK=NoThanks @@ -684,6 +693,10 @@ ifndef V endif endif +ifdef ASCIIDOC8 + export ASCIIDOC8 +endif + # Shell quote (do not use $(call) to accommodate ancient setups); SHA1_HEADER_SQ = $(subst ','\'',$(SHA1_HEADER)) @@ -1017,15 +1030,16 @@ git.spec: git.spec.in mv $@+ $@ GIT_TARNAME=git-$(GIT_VERSION) -dist: git.spec git-archive +dist: git.spec git-archive configure ./git-archive --format=tar \ --prefix=$(GIT_TARNAME)/ HEAD^{tree} > $(GIT_TARNAME).tar @mkdir -p $(GIT_TARNAME) - @cp git.spec $(GIT_TARNAME) + @cp git.spec configure $(GIT_TARNAME) @echo $(GIT_VERSION) > $(GIT_TARNAME)/version @$(MAKE) -C git-gui TARDIR=../$(GIT_TARNAME)/git-gui dist-version $(TAR) rf $(GIT_TARNAME).tar \ $(GIT_TARNAME)/git.spec \ + $(GIT_TARNAME)/configure \ $(GIT_TARNAME)/version \ $(GIT_TARNAME)/git-gui/version @rm -rf $(GIT_TARNAME) diff --git a/builtin-add.c b/builtin-add.c index 159117106..734547994 100644 --- a/builtin-add.c +++ b/builtin-add.c @@ -40,42 +40,29 @@ static void prune_directory(struct dir_struct *dir, const char **pathspec, int p dir->nr = dst - dir->entries; for (i = 0; i < specs; i++) { - struct stat st; - const char *match; - if (seen[i]) - continue; - - match = pathspec[i]; - if (!match[0]) - continue; - - /* Existing file? We must have ignored it */ - if (!lstat(match, &st)) { - struct dir_entry *ent; - - ent = dir_add_name(dir, match, strlen(match)); - ent->ignored = 1; - if (S_ISDIR(st.st_mode)) - ent->ignored_dir = 1; - continue; - } - die("pathspec '%s' did not match any files", match); + if (!seen[i] && !file_exists(pathspec[i])) + die("pathspec '%s' did not match any files", + pathspec[i]); } } -static void fill_directory(struct dir_struct *dir, const char **pathspec) +static void fill_directory(struct dir_struct *dir, const char **pathspec, + int ignored_too) { const char *path, *base; int baselen; /* Set up the default git porcelain excludes */ memset(dir, 0, sizeof(*dir)); - dir->exclude_per_dir = ".gitignore"; - path = git_path("info/exclude"); - if (!access(path, R_OK)) - add_excludes_from_file(dir, path); - if (!access(excludes_file, R_OK)) - add_excludes_from_file(dir, excludes_file); + if (!ignored_too) { + dir->collect_ignored = 1; + dir->exclude_per_dir = ".gitignore"; + path = git_path("info/exclude"); + if (!access(path, R_OK)) + add_excludes_from_file(dir, path); + if (!access(excludes_file, R_OK)) + add_excludes_from_file(dir, excludes_file); + } /* * Calculate common prefix for the pathspec, and @@ -219,13 +206,11 @@ int cmd_add(int argc, const char **argv, const char *prefix) } pathspec = get_pathspec(prefix, argv + i); - fill_directory(&dir, pathspec); + fill_directory(&dir, pathspec, ignored_too); if (show_only) { const char *sep = "", *eof = ""; for (i = 0; i < dir.nr; i++) { - if (!ignored_too && dir.entries[i]->ignored) - continue; printf("%s%s", sep, dir.entries[i]->name); sep = " "; eof = "\n"; @@ -237,25 +222,13 @@ int cmd_add(int argc, const char **argv, const char *prefix) if (read_cache() < 0) die("index file corrupt"); - if (!ignored_too) { - int has_ignored = 0; - for (i = 0; i < dir.nr; i++) - if (dir.entries[i]->ignored) - has_ignored = 1; - if (has_ignored) { - fprintf(stderr, ignore_warning); - for (i = 0; i < dir.nr; i++) { - if (!dir.entries[i]->ignored) - continue; - fprintf(stderr, "%s", dir.entries[i]->name); - if (dir.entries[i]->ignored_dir) - fprintf(stderr, " (directory)"); - fputc('\n', stderr); - } - fprintf(stderr, - "Use -f if you really want to add them.\n"); - exit(1); + if (dir.ignored_nr) { + fprintf(stderr, ignore_warning); + for (i = 0; i < dir.ignored_nr; i++) { + fprintf(stderr, "%s\n", dir.ignored[i]->name); } + fprintf(stderr, "Use -f if you really want to add them.\n"); + exit(1); } for (i = 0; i < dir.nr; i++) diff --git a/builtin-blame.c b/builtin-blame.c index f7e2c1388..da23a6f9c 100644 --- a/builtin-blame.c +++ b/builtin-blame.c @@ -1459,6 +1459,7 @@ static void found_guilty_entry(struct blame_entry *ent) printf("boundary\n"); } write_filename_info(suspect->path); + maybe_flush_or_die(stdout, "stdout"); } } diff --git a/builtin-branch.c b/builtin-branch.c index da480519d..77b85dde1 100644 --- a/builtin-branch.c +++ b/builtin-branch.c @@ -85,6 +85,7 @@ static int delete_branches(int argc, const char **argv, int force, int kinds) unsigned char sha1[20]; char *name = NULL; const char *fmt, *remote; + char section[PATH_MAX]; int i; int ret = 0; @@ -152,9 +153,13 @@ static int delete_branches(int argc, const char **argv, int force, int kinds) error("Error deleting %sbranch '%s'", remote, argv[i]); ret = 1; - } else + } else { printf("Deleted %sbranch %s.\n", remote, argv[i]); - + snprintf(section, sizeof(section), "branch.%s", + argv[i]); + if (git_config_rename_section(section, NULL) < 0) + warning("Update of config-file failed"); + } } if (name) @@ -242,7 +247,6 @@ static void print_ref_item(struct ref_item *item, int maxwidth, int verbose, char c; int color; struct commit *commit; - char subject[256]; switch (item->kind) { case REF_LOCAL_BRANCH: @@ -263,17 +267,23 @@ static void print_ref_item(struct ref_item *item, int maxwidth, int verbose, } if (verbose) { + char *subject = NULL; + unsigned long subject_len = 0; + const char *sub = " **** invalid ref ****"; + commit = lookup_commit(item->sha1); - if (commit && !parse_commit(commit)) + if (commit && !parse_commit(commit)) { pretty_print_commit(CMIT_FMT_ONELINE, commit, ~0, - subject, sizeof(subject), 0, + &subject, &subject_len, 0, NULL, NULL, 0); - else - strcpy(subject, " **** invalid ref ****"); + sub = subject; + } printf("%c %s%-*s%s %s %s\n", c, branch_get_color(color), maxwidth, item->name, branch_get_color(COLOR_BRANCH_RESET), - find_unique_abbrev(item->sha1, abbrev), subject); + find_unique_abbrev(item->sha1, abbrev), sub); + if (subject) + free(subject); } else { printf("%c %s%s%s\n", c, branch_get_color(color), item->name, branch_get_color(COLOR_BRANCH_RESET)); diff --git a/builtin-config.c b/builtin-config.c index b2515f7e6..7d2063c1d 100644 --- a/builtin-config.c +++ b/builtin-config.c @@ -2,7 +2,7 @@ #include "cache.h" static const char git_config_set_usage[] = -"git-config [ --global | --system ] [ --bool | --int ] [--get | --get-all | --get-regexp | --replace-all | --add | --unset | --unset-all] name [value [value_regex]] | --rename-section old_name new_name | --remove-section name | --list"; +"git-config [ --global | --system ] [ --bool | --int ] [ -z | --null ] [--get | --get-all | --get-regexp | --replace-all | --add | --unset | --unset-all] name [value [value_regex]] | --rename-section old_name new_name | --remove-section name | --list"; static char *key; static regex_t *key_regexp; @@ -12,14 +12,17 @@ static int use_key_regexp; static int do_all; static int do_not_match; static int seen; +static char delim = '='; +static char key_delim = ' '; +static char term = '\n'; static enum { T_RAW, T_INT, T_BOOL } type = T_RAW; static int show_all_config(const char *key_, const char *value_) { if (value_) - printf("%s=%s\n", key_, value_); + printf("%s%c%s%c", key_, delim, value_, term); else - printf("%s\n", key_); + printf("%s%c", key_, term); return 0; } @@ -38,8 +41,12 @@ static int show_config(const char* key_, const char* value_) regexec(regexp, (value_?value_:""), 0, NULL, 0))) return 0; - if (show_keys) - printf("%s ", key_); + if (show_keys) { + if (value_) + printf("%s%c", key_, key_delim); + else + printf("%s", key_); + } if (seen && !do_all) dup_error = 1; if (type == T_INT) @@ -54,7 +61,7 @@ static int show_config(const char* key_, const char* value_) key_, vptr); } else - printf("%s\n", vptr); + printf("%s%c", vptr, term); return 0; } @@ -131,9 +138,33 @@ free_strings: return ret; } +char *normalize_value(const char *key, const char *value) +{ + char *normalized; + + if (!value) + return NULL; + + if (type == T_RAW) + normalized = xstrdup(value); + else { + normalized = xmalloc(64); + if (type == T_INT) { + int v = git_config_int(key, value); + sprintf(normalized, "%d", v); + } + else if (type == T_BOOL) + sprintf(normalized, "%s", + git_config_bool(key, value) ? "true" : "false"); + } + + return normalized; +} + int cmd_config(int argc, const char **argv, const char *prefix) { int nongit = 0; + char* value; setup_git_directory_gently(&nongit); while (1 < argc) { @@ -147,14 +178,19 @@ int cmd_config(int argc, const char **argv, const char *prefix) char *home = getenv("HOME"); if (home) { char *user_config = xstrdup(mkpath("%s/.gitconfig", home)); - setenv("GIT_CONFIG", user_config, 1); + setenv(CONFIG_ENVIRONMENT, user_config, 1); free(user_config); } else { die("$HOME not set"); } } else if (!strcmp(argv[1], "--system")) - setenv("GIT_CONFIG", ETC_GITCONFIG, 1); + setenv(CONFIG_ENVIRONMENT, ETC_GITCONFIG, 1); + else if (!strcmp(argv[1], "--null") || !strcmp(argv[1], "-z")) { + term = '\0'; + delim = '\n'; + key_delim = '\n'; + } else if (!strcmp(argv[1], "--rename-section")) { int ret; if (argc != 4) @@ -205,9 +241,10 @@ int cmd_config(int argc, const char **argv, const char *prefix) use_key_regexp = 1; do_all = 1; return get_value(argv[2], NULL); - } else - - return git_config_set(argv[1], argv[2]); + } else { + value = normalize_value(argv[1], argv[2]); + return git_config_set(argv[1], value); + } case 4: if (!strcmp(argv[1], "--unset")) return git_config_set_multivar(argv[2], NULL, argv[3], 0); @@ -223,17 +260,21 @@ int cmd_config(int argc, const char **argv, const char *prefix) use_key_regexp = 1; do_all = 1; return get_value(argv[2], argv[3]); - } else if (!strcmp(argv[1], "--add")) - return git_config_set_multivar(argv[2], argv[3], "^$", 0); - else if (!strcmp(argv[1], "--replace-all")) - - return git_config_set_multivar(argv[2], argv[3], NULL, 1); - else - - return git_config_set_multivar(argv[1], argv[2], argv[3], 0); + } else if (!strcmp(argv[1], "--add")) { + value = normalize_value(argv[2], argv[3]); + return git_config_set_multivar(argv[2], value, "^$", 0); + } else if (!strcmp(argv[1], "--replace-all")) { + value = normalize_value(argv[2], argv[3]); + return git_config_set_multivar(argv[2], value, NULL, 1); + } else { + value = normalize_value(argv[1], argv[2]); + return git_config_set_multivar(argv[1], value, argv[3], 0); + } case 5: - if (!strcmp(argv[1], "--replace-all")) - return git_config_set_multivar(argv[2], argv[3], argv[4], 1); + if (!strcmp(argv[1], "--replace-all")) { + value = normalize_value(argv[2], argv[3]); + return git_config_set_multivar(argv[2], value, argv[4], 1); + } case 1: default: usage(git_config_set_usage); diff --git a/builtin-init-db.c b/builtin-init-db.c index 0be2d2ef6..976f47b32 100644 --- a/builtin-init-db.c +++ b/builtin-init-db.c @@ -40,7 +40,8 @@ static int copy_file(const char *dst, const char *src, int mode) return fdo; } status = copy_fd(fdi, fdo); - close(fdo); + if (close(fdo) != 0) + return error("%s: write error: %s", dst, strerror(errno)); if (!status && adjust_shared_perm(dst)) return -1; diff --git a/builtin-log.c b/builtin-log.c index 0aede7683..a4186eac8 100644 --- a/builtin-log.c +++ b/builtin-log.c @@ -58,6 +58,11 @@ static void cmd_log_init(int argc, const char **argv, const char *prefix, argc = setup_revisions(argc, argv, rev, "HEAD"); if (rev->diffopt.pickaxe || rev->diffopt.filter) rev->always_show_header = 0; + if (rev->diffopt.follow_renames) { + rev->always_show_header = 0; + if (rev->diffopt.nr_paths != 1) + usage("git logs can only follow renames on one pathname at a time"); + } for (i = 1; i < argc; i++) { const char *arg = argv[i]; if (!strcmp(arg, "--decorate")) { @@ -584,7 +589,7 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix) get_patch_ids(&rev, &ids, prefix); if (!use_stdout) - realstdout = fdopen(dup(1), "w"); + realstdout = xfdopen(xdup(1), "w"); prepare_revision_walk(&rev); while ((commit = get_revision(&rev)) != NULL) { @@ -742,11 +747,13 @@ int cmd_cherry(int argc, const char **argv, const char *prefix) sign = '-'; if (verbose) { - static char buf[16384]; + char *buf = NULL; + unsigned long buflen = 0; pretty_print_commit(CMIT_FMT_ONELINE, commit, ~0, - buf, sizeof(buf), 0, NULL, NULL, 0); + &buf, &buflen, 0, NULL, NULL, 0); printf("%c %s %s\n", sign, sha1_to_hex(commit->object.sha1), buf); + free(buf); } else { printf("%c %s\n", sign, diff --git a/builtin-ls-files.c b/builtin-ls-files.c index 5398a4141..61577ea13 100644 --- a/builtin-ls-files.c +++ b/builtin-ls-files.c @@ -470,7 +470,7 @@ int cmd_ls_files(int argc, const char **argv, const char *prefix) } if (require_work_tree && - (is_bare_repository() || is_inside_git_dir())) + (!is_inside_work_tree() || is_inside_git_dir())) die("This operation must be run in a work tree"); pathspec = get_pathspec(prefix, argv + i); diff --git a/builtin-pack-refs.c b/builtin-pack-refs.c index 1952950c9..758499238 100644 --- a/builtin-pack-refs.c +++ b/builtin-pack-refs.c @@ -105,6 +105,8 @@ static int pack_refs(unsigned int flags) fprintf(cbdata.refs_file, "# pack-refs with: peeled \n"); for_each_ref(handle_one_ref, &cbdata); + if (ferror(cbdata.refs_file)) + die("failed to write ref-pack file"); if (fflush(cbdata.refs_file) || fsync(fd) || fclose(cbdata.refs_file)) die("failed to write ref-pack file (%s)", strerror(errno)); if (commit_lock_file(&packed) < 0) diff --git a/builtin-read-tree.c b/builtin-read-tree.c index 316fb0f8d..41f811023 100644 --- a/builtin-read-tree.c +++ b/builtin-read-tree.c @@ -84,7 +84,7 @@ static void prime_cache_tree(void) } -static const char read_tree_usage[] = "git-read-tree (<sha> | [[-m [--aggressive] | --reset | --prefix=<prefix>] [-u | -i]] [--exclude-per-directory=<gitignore>] [--index-output=<file>] <sha1> [<sha2> [<sha3>]])"; +static const char read_tree_usage[] = "git-read-tree (<sha> | [[-m [--trivial] [--aggressive] | --reset | --prefix=<prefix>] [-u | -i]] [--exclude-per-directory=<gitignore>] [--index-output=<file>] <sha1> [<sha2> [<sha3>]])"; static struct lock_file lock_file; diff --git a/builtin-rerere.c b/builtin-rerere.c index f6409b93c..29fb075d2 100644 --- a/builtin-rerere.c +++ b/builtin-rerere.c @@ -57,7 +57,8 @@ static int write_rr(struct path_list *rr, int out_fd) write_in_full(out_fd, path, length) != length) die("unable to write rerere record"); } - close(out_fd); + if (close(out_fd) != 0) + die("unable to write rerere record"); return commit_lock_file(&write_lock); } diff --git a/builtin-rev-list.c b/builtin-rev-list.c index ebf53f594..86db8b03f 100644 --- a/builtin-rev-list.c +++ b/builtin-rev-list.c @@ -92,13 +92,15 @@ static void show_commit(struct commit *commit) putchar('\n'); if (revs.verbose_header) { - static char pretty_header[16384]; + char *buf = NULL; + unsigned long buflen = 0; pretty_print_commit(revs.commit_format, commit, ~0, - pretty_header, sizeof(pretty_header), + &buf, &buflen, revs.abbrev, NULL, NULL, revs.date_mode); - printf("%s%c", pretty_header, hdr_termination); + printf("%s%c", buf, hdr_termination); + free(buf); } - fflush(stdout); + maybe_flush_or_die(stdout, "stdout"); if (commit->parents) { free_commit_list(commit->parents); commit->parents = NULL; diff --git a/builtin-rev-parse.c b/builtin-rev-parse.c index 37addb25f..497903a85 100644 --- a/builtin-rev-parse.c +++ b/builtin-rev-parse.c @@ -352,6 +352,16 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix) : "false"); continue; } + if (!strcmp(arg, "--is-inside-work-tree")) { + printf("%s\n", is_inside_work_tree() ? "true" + : "false"); + continue; + } + if (!strcmp(arg, "--is-bare-repository")) { + printf("%s\n", is_bare_repository() ? "true" + : "false"); + continue; + } if (!prefixcmp(arg, "--since=")) { show_datestring("--max-age=", arg+8); continue; diff --git a/builtin-show-branch.c b/builtin-show-branch.c index c892f1f7a..4fa87f608 100644 --- a/builtin-show-branch.c +++ b/builtin-show-branch.c @@ -259,17 +259,19 @@ static void join_revs(struct commit_list **list_p, static void show_one_commit(struct commit *commit, int no_name) { - char pretty[256], *cp; + char *pretty = NULL; + const char *pretty_str = "(unavailable)"; + unsigned long pretty_len = 0; struct commit_name *name = commit->util; - if (commit->object.parsed) + + if (commit->object.parsed) { pretty_print_commit(CMIT_FMT_ONELINE, commit, ~0, - pretty, sizeof(pretty), 0, NULL, NULL, 0); - else - strcpy(pretty, "(unavailable)"); - if (!prefixcmp(pretty, "[PATCH] ")) - cp = pretty + 8; - else - cp = pretty; + &pretty, &pretty_len, + 0, NULL, NULL, 0); + pretty_str = pretty; + } + if (!prefixcmp(pretty_str, "[PATCH] ")) + pretty_str += 8; if (!no_name) { if (name && name->head_name) { @@ -286,7 +288,8 @@ static void show_one_commit(struct commit *commit, int no_name) printf("[%s] ", find_unique_abbrev(commit->object.sha1, 7)); } - puts(cp); + puts(pretty_str); + free(pretty); } static char *ref_name[MAX_REVS + 1]; diff --git a/builtin-stripspace.c b/builtin-stripspace.c index 62bd4b547..d8358e28f 100644 --- a/builtin-stripspace.c +++ b/builtin-stripspace.c @@ -1,58 +1,79 @@ #include "builtin.h" +#include "cache.h" /* - * Remove empty lines from the beginning and end. + * Remove trailing spaces from a line. * - * Turn multiple consecutive empty lines into just one - * empty line. Return true if it is an incomplete line. + * If the line ends with newline, it will be removed too. + * Returns the new length of the string. */ -static int cleanup(char *line) +static int cleanup(char *line, int len) { - int len = strlen(line); + if (len) { + if (line[len - 1] == '\n') + len--; - if (len && line[len-1] == '\n') { - if (len == 1) - return 0; - do { - unsigned char c = line[len-2]; + while (len) { + unsigned char c = line[len - 1]; if (!isspace(c)) break; - line[len-2] = '\n'; len--; - line[len] = 0; - } while (len > 1); - return 0; + } + line[len] = 0; } - return 1; + return len; } -static void stripspace(FILE *in, FILE *out) +/* + * Remove empty lines from the beginning and end + * and also trailing spaces from every line. + * + * Turn multiple consecutive empty lines between paragraphs + * into just one empty line. + * + * If the input has only empty lines and spaces, + * no output will be produced. + * + * Enable skip_comments to skip every line starting with "#". + */ +void stripspace(FILE *in, FILE *out, int skip_comments) { int empties = -1; - int incomplete = 0; - char line[1024]; + int alloc = 1024; + char *line = xmalloc(alloc); + + while (fgets(line, alloc, in)) { + int len = strlen(line); - while (fgets(line, sizeof(line), in)) { - incomplete = cleanup(line); + while (len == alloc - 1 && line[len - 1] != '\n') { + alloc = alloc_nr(alloc); + line = xrealloc(line, alloc); + fgets(line + len, alloc - len, in); + len += strlen(line + len); + } + + if (skip_comments && line[0] == '#') + continue; + len = cleanup(line, len); /* Not just an empty line? */ - if (line[0] != '\n') { + if (len) { if (empties > 0) fputc('\n', out); empties = 0; fputs(line, out); + fputc('\n', out); continue; } if (empties < 0) continue; empties++; } - if (incomplete) - fputc('\n', out); + free(line); } int cmd_stripspace(int argc, const char **argv, const char *prefix) { - stripspace(stdin, stdout); + stripspace(stdin, stdout, 0); return 0; } @@ -7,6 +7,7 @@ extern const char git_version_string[]; extern const char git_usage_string[]; extern void help_unknown_cmd(const char *cmd); +extern void stripspace(FILE *in, FILE *out, int skip_comments); extern int write_tree(unsigned char *sha1, int missing_ok, const char *prefix); extern void prune_packed_objects(int); diff --git a/cache-tree.c b/cache-tree.c index 350a79b76..077f03436 100644 --- a/cache-tree.c +++ b/cache-tree.c @@ -478,7 +478,7 @@ static struct cache_tree *read_one(const char **buffer, unsigned long *size_p) if (0 <= it->entry_count) { if (size < 20) goto free_return; - hashcpy(it->sha1, (unsigned char*)buf); + hashcpy(it->sha1, (const unsigned char*)buf); buf += 20; size -= 20; } @@ -192,6 +192,7 @@ enum object_type { }; #define GIT_DIR_ENVIRONMENT "GIT_DIR" +#define GIT_WORK_TREE_ENVIRONMENT "GIT_WORK_TREE" #define DEFAULT_GIT_DIR_ENVIRONMENT ".git" #define DB_ENVIRONMENT "GIT_OBJECT_DIRECTORY" #define INDEX_ENVIRONMENT "GIT_INDEX_FILE" @@ -207,6 +208,7 @@ enum object_type { extern int is_bare_repository_cfg; extern int is_bare_repository(void); extern int is_inside_git_dir(void); +extern int is_inside_work_tree(void); extern const char *get_git_dir(void); extern char *get_object_directory(void); extern char *get_refs_directory(void); @@ -234,8 +236,11 @@ extern void verify_non_filename(const char *prefix, const char *name); */ #define ALLOC_GROW(x, nr, alloc) \ do { \ - if ((nr) >= alloc) { \ - alloc = alloc_nr(alloc); \ + if ((nr) > alloc) { \ + if (alloc_nr(alloc) < (nr)) \ + alloc = (nr); \ + else \ + alloc = alloc_nr(alloc); \ x = xrealloc((x), alloc * sizeof(*(x))); \ } \ } while(0) @@ -289,6 +294,7 @@ extern int delete_ref(const char *, const unsigned char *sha1); /* Environment bits from configuration mechanism */ extern int trust_executable_bit; +extern int quote_path_fully; extern int has_symlinks; extern int assume_unchanged; extern int prefer_symlink_refs; @@ -529,6 +535,8 @@ extern char git_default_name[MAX_GITNAME]; extern const char *git_commit_encoding; extern const char *git_log_output_encoding; +/* IO helper functions */ +extern void maybe_flush_or_die(FILE *, const char *); extern int copy_fd(int ifd, int ofd); extern int read_in_full(int fd, void *buf, size_t count); extern int write_in_full(int fd, const void *buf, size_t count); @@ -529,6 +529,14 @@ static int add_rfc2047(char *buf, const char *line, int len, return bp - buf; } +static unsigned long bound_rfc2047(unsigned long len, const char *encoding) +{ + /* upper bound of q encoded string of length 'len' */ + unsigned long elen = strlen(encoding); + + return len * 3 + elen + 100; +} + static int add_user_info(const char *what, enum cmit_fmt fmt, char *buf, const char *line, enum date_mode dmode, const char *encoding) @@ -776,7 +784,7 @@ static void fill_person(struct interp *table, const char *msg, int len) } static long format_commit_message(const struct commit *commit, - const char *msg, char *buf, unsigned long space) + const char *msg, char **buf_p, unsigned long *space_p) { struct interp table[] = { { "%H" }, /* commit hash */ @@ -905,31 +913,252 @@ static long format_commit_message(const struct commit *commit, if (!table[i].value) interp_set_entry(table, i, "<unknown>"); - interpolate(buf, space, user_format, table, ARRAY_SIZE(table)); + do { + char *buf = *buf_p; + unsigned long space = *space_p; + + space = interpolate(buf, space, user_format, + table, ARRAY_SIZE(table)); + if (!space) + break; + buf = xrealloc(buf, space); + *buf_p = buf; + *space_p = space; + } while (1); interp_clear_table(table, ARRAY_SIZE(table)); - return strlen(buf); + return strlen(*buf_p); +} + +static void pp_header(enum cmit_fmt fmt, + int abbrev, + enum date_mode dmode, + const char *encoding, + const struct commit *commit, + const char **msg_p, + unsigned long *len_p, + unsigned long *ofs_p, + char **buf_p, + unsigned long *space_p) +{ + int parents_shown = 0; + + for (;;) { + const char *line = *msg_p; + char *dst; + int linelen = get_one_line(*msg_p, *len_p); + unsigned long len; + + if (!linelen) + return; + *msg_p += linelen; + *len_p -= linelen; + + if (linelen == 1) + /* End of header */ + return; + + ALLOC_GROW(*buf_p, linelen + *ofs_p + 20, *space_p); + dst = *buf_p + *ofs_p; + + if (fmt == CMIT_FMT_RAW) { + memcpy(dst, line, linelen); + *ofs_p += linelen; + continue; + } + + if (!memcmp(line, "parent ", 7)) { + if (linelen != 48) + die("bad parent line in commit"); + continue; + } + + if (!parents_shown) { + struct commit_list *parent; + int num; + for (parent = commit->parents, num = 0; + parent; + parent = parent->next, num++) + ; + /* with enough slop */ + num = *ofs_p + num * 50 + 20; + ALLOC_GROW(*buf_p, num, *space_p); + dst = *buf_p + *ofs_p; + *ofs_p += add_merge_info(fmt, dst, commit, abbrev); + parents_shown = 1; + } + + /* + * MEDIUM == DEFAULT shows only author with dates. + * FULL shows both authors but not dates. + * FULLER shows both authors and dates. + */ + if (!memcmp(line, "author ", 7)) { + len = linelen; + if (fmt == CMIT_FMT_EMAIL) + len = bound_rfc2047(linelen, encoding); + ALLOC_GROW(*buf_p, *ofs_p + len + 80, *space_p); + dst = *buf_p + *ofs_p; + *ofs_p += add_user_info("Author", fmt, dst, + line + 7, dmode, encoding); + } + + if (!memcmp(line, "committer ", 10) && + (fmt == CMIT_FMT_FULL || fmt == CMIT_FMT_FULLER)) { + len = linelen; + if (fmt == CMIT_FMT_EMAIL) + len = bound_rfc2047(linelen, encoding); + ALLOC_GROW(*buf_p, *ofs_p + len + 80, *space_p); + dst = *buf_p + *ofs_p; + *ofs_p += add_user_info("Commit", fmt, dst, + line + 10, dmode, encoding); + } + } +} + +static void pp_title_line(enum cmit_fmt fmt, + const char **msg_p, + unsigned long *len_p, + unsigned long *ofs_p, + char **buf_p, + unsigned long *space_p, + int indent, + const char *subject, + const char *after_subject, + const char *encoding, + int plain_non_ascii) +{ + char *title; + unsigned long title_alloc, title_len; + unsigned long len; + + title_len = 0; + title_alloc = 80; + title = xmalloc(title_alloc); + for (;;) { + const char *line = *msg_p; + int linelen = get_one_line(line, *len_p); + *msg_p += linelen; + *len_p -= linelen; + + if (!linelen || is_empty_line(line, &linelen)) + break; + + if (title_alloc <= title_len + linelen + 2) { + title_alloc = title_len + linelen + 80; + title = xrealloc(title, title_alloc); + } + len = 0; + if (title_len) { + if (fmt == CMIT_FMT_EMAIL) { + len++; + title[title_len++] = '\n'; + } + len++; + title[title_len++] = ' '; + } + memcpy(title + title_len, line, linelen); + title_len += linelen; + } + + /* Enough slop for the MIME header and rfc2047 */ + len = bound_rfc2047(title_len, encoding)+ 1000; + if (subject) + len += strlen(subject); + if (after_subject) + len += strlen(after_subject); + if (encoding) + len += strlen(encoding); + ALLOC_GROW(*buf_p, title_len + *ofs_p + len, *space_p); + + if (subject) { + len = strlen(subject); + memcpy(*buf_p + *ofs_p, subject, len); + *ofs_p += len; + *ofs_p += add_rfc2047(*buf_p + *ofs_p, + title, title_len, encoding); + } else { + memcpy(*buf_p + *ofs_p, title, title_len); + *ofs_p += title_len; + } + (*buf_p)[(*ofs_p)++] = '\n'; + if (plain_non_ascii) { + const char *header_fmt = + "MIME-Version: 1.0\n" + "Content-Type: text/plain; charset=%s\n" + "Content-Transfer-Encoding: 8bit\n"; + *ofs_p += snprintf(*buf_p + *ofs_p, + *space_p - *ofs_p, + header_fmt, encoding); + } + if (after_subject) { + len = strlen(after_subject); + memcpy(*buf_p + *ofs_p, after_subject, len); + *ofs_p += len; + } + free(title); + if (fmt == CMIT_FMT_EMAIL) { + ALLOC_GROW(*buf_p, *ofs_p + 20, *space_p); + (*buf_p)[(*ofs_p)++] = '\n'; + } +} + +static void pp_remainder(enum cmit_fmt fmt, + const char **msg_p, + unsigned long *len_p, + unsigned long *ofs_p, + char **buf_p, + unsigned long *space_p, + int indent) +{ + int first = 1; + for (;;) { + const char *line = *msg_p; + int linelen = get_one_line(line, *len_p); + *msg_p += linelen; + *len_p -= linelen; + + if (!linelen) + break; + + if (is_empty_line(line, &linelen)) { + if (first) + continue; + if (fmt == CMIT_FMT_SHORT) + break; + } + first = 0; + + ALLOC_GROW(*buf_p, *ofs_p + linelen + indent + 20, *space_p); + if (indent) { + memset(*buf_p + *ofs_p, ' ', indent); + *ofs_p += indent; + } + memcpy(*buf_p + *ofs_p, line, linelen); + *ofs_p += linelen; + (*buf_p)[(*ofs_p)++] = '\n'; + } } unsigned long pretty_print_commit(enum cmit_fmt fmt, const struct commit *commit, unsigned long len, - char *buf, unsigned long space, + char **buf_p, unsigned long *space_p, int abbrev, const char *subject, const char *after_subject, enum date_mode dmode) { - int hdr = 1, body = 0, seen_title = 0; unsigned long offset = 0; + unsigned long beginning_of_body; int indent = 4; - int parents_shown = 0; const char *msg = commit->buffer; int plain_non_ascii = 0; char *reencoded; const char *encoding; + char *buf; if (fmt == CMIT_FMT_USERFORMAT) - return format_commit_message(commit, msg, buf, space); + return format_commit_message(commit, msg, buf_p, space_p); encoding = (git_log_output_encoding ? git_log_output_encoding @@ -937,8 +1166,10 @@ unsigned long pretty_print_commit(enum cmit_fmt fmt, if (!encoding) encoding = "utf-8"; reencoded = logmsg_reencode(commit, encoding); - if (reencoded) + if (reencoded) { msg = reencoded; + len = strlen(reencoded); + } if (fmt == CMIT_FMT_ONELINE || fmt == CMIT_FMT_EMAIL) indent = 0; @@ -969,137 +1200,57 @@ unsigned long pretty_print_commit(enum cmit_fmt fmt, } } + pp_header(fmt, abbrev, dmode, encoding, + commit, &msg, &len, + &offset, buf_p, space_p); + if (fmt != CMIT_FMT_ONELINE && !subject) { + ALLOC_GROW(*buf_p, offset + 20, *space_p); + (*buf_p)[offset++] = '\n'; + } + + /* Skip excess blank lines at the beginning of body, if any... */ for (;;) { - const char *line = msg; int linelen = get_one_line(msg, len); - + int ll = linelen; if (!linelen) break; - - /* - * We want some slop for indentation and a possible - * final "...". Thus the "+ 20". - */ - if (offset + linelen + 20 > space) { - memcpy(buf + offset, " ...\n", 8); - offset += 8; + if (!is_empty_line(msg, &ll)) break; - } - msg += linelen; len -= linelen; - if (hdr) { - if (linelen == 1) { - hdr = 0; - if ((fmt != CMIT_FMT_ONELINE) && !subject) - buf[offset++] = '\n'; - continue; - } - if (fmt == CMIT_FMT_RAW) { - memcpy(buf + offset, line, linelen); - offset += linelen; - continue; - } - if (!memcmp(line, "parent ", 7)) { - if (linelen != 48) - die("bad parent line in commit"); - continue; - } - - if (!parents_shown) { - offset += add_merge_info(fmt, buf + offset, - commit, abbrev); - parents_shown = 1; - continue; - } - /* - * MEDIUM == DEFAULT shows only author with dates. - * FULL shows both authors but not dates. - * FULLER shows both authors and dates. - */ - if (!memcmp(line, "author ", 7)) - offset += add_user_info("Author", fmt, - buf + offset, - line + 7, - dmode, - encoding); - if (!memcmp(line, "committer ", 10) && - (fmt == CMIT_FMT_FULL || fmt == CMIT_FMT_FULLER)) - offset += add_user_info("Commit", fmt, - buf + offset, - line + 10, - dmode, - encoding); - continue; - } + } - if (!subject) - body = 1; + /* These formats treat the title line specially. */ + if (fmt == CMIT_FMT_ONELINE + || fmt == CMIT_FMT_EMAIL) + pp_title_line(fmt, &msg, &len, &offset, + buf_p, space_p, indent, + subject, after_subject, encoding, + plain_non_ascii); - if (is_empty_line(line, &linelen)) { - if (!seen_title) - continue; - if (!body) - continue; - if (subject) - continue; - if (fmt == CMIT_FMT_SHORT) - break; - } + beginning_of_body = offset; + if (fmt != CMIT_FMT_ONELINE) + pp_remainder(fmt, &msg, &len, &offset, + buf_p, space_p, indent); - seen_title = 1; - if (subject) { - int slen = strlen(subject); - memcpy(buf + offset, subject, slen); - offset += slen; - offset += add_rfc2047(buf + offset, line, linelen, - encoding); - } - else { - memset(buf + offset, ' ', indent); - memcpy(buf + offset + indent, line, linelen); - offset += linelen + indent; - } - buf[offset++] = '\n'; - if (fmt == CMIT_FMT_ONELINE) - break; - if (subject && plain_non_ascii) { - int sz; - char header[512]; - const char *header_fmt = - "MIME-Version: 1.0\n" - "Content-Type: text/plain; charset=%s\n" - "Content-Transfer-Encoding: 8bit\n"; - sz = snprintf(header, sizeof(header), header_fmt, - encoding); - if (sizeof(header) < sz) - die("Encoding name %s too long", encoding); - memcpy(buf + offset, header, sz); - offset += sz; - } - if (after_subject) { - int slen = strlen(after_subject); - if (slen > space - offset - 1) - slen = space - offset - 1; - memcpy(buf + offset, after_subject, slen); - offset += slen; - after_subject = NULL; - } - subject = NULL; - } - while (offset && isspace(buf[offset-1])) + while (offset && isspace((*buf_p)[offset-1])) offset--; + + ALLOC_GROW(*buf_p, offset + 20, *space_p); + buf = *buf_p; + /* Make sure there is an EOLN for the non-oneline case */ if (fmt != CMIT_FMT_ONELINE) buf[offset++] = '\n'; + /* - * make sure there is another EOLN to separate the headers from whatever - * body the caller appends if we haven't already written a body + * The caller may append additional body text in e-mail + * format. Make sure we did not strip the blank line + * between the header and the body. */ - if (fmt == CMIT_FMT_EMAIL && !body) + if (fmt == CMIT_FMT_EMAIL && offset <= beginning_of_body) buf[offset++] = '\n'; buf[offset] = '\0'; - free(reencoded); return offset; } @@ -61,7 +61,7 @@ enum cmit_fmt { }; extern enum cmit_fmt get_commit_format(const char *arg); -extern unsigned long pretty_print_commit(enum cmit_fmt fmt, const struct commit *, unsigned long len, char *buf, unsigned long space, int abbrev, const char *subject, const char *after_subject, enum date_mode dmode); +extern unsigned long pretty_print_commit(enum cmit_fmt fmt, const struct commit *, unsigned long len, char **buf_p, unsigned long *space_p, int abbrev, const char *subject, const char *after_subject, enum date_mode dmode); /** Removes the first commit from a list sorted by date, and adds all * of its parents. diff --git a/compat/hstrerror.c b/compat/hstrerror.c new file mode 100644 index 000000000..069c555da --- /dev/null +++ b/compat/hstrerror.c @@ -0,0 +1,21 @@ +#include <string.h> +#include <stdio.h> +#include <netdb.h> + +const char *githstrerror(int err) +{ + static char buffer[48]; + switch (err) + { + case HOST_NOT_FOUND: + return "Authoritative answer: host not found"; + case NO_DATA: + return "Valid name, no data record of requested type"; + case NO_RECOVERY: + return "Non recoverable errors, FORMERR, REFUSED, NOTIMP"; + case TRY_AGAIN: + return "Non-authoritative \"host not found\", or SERVERFAIL"; + } + sprintf(buffer, "Name resolution error %d", err); + return buffer; +} @@ -271,6 +271,11 @@ int git_default_config(const char *var, const char *value) return 0; } + if (!strcmp(var, "core.quotepath")) { + quote_path_fully = git_config_bool(var, value); + return 0; + } + if (!strcmp(var, "core.symlinks")) { has_symlinks = git_config_bool(var, value); return 0; @@ -224,11 +224,10 @@ static int git_tcp_connect_sock(char *host, int flags) } if (connect(sockfd, ai->ai_addr, ai->ai_addrlen) < 0) { saved_errno = errno; - fprintf(stderr, "%s[%d: %s]: net=%s, errno=%s\n", + fprintf(stderr, "%s[%d: %s]: errno=%s\n", host, cnt, ai_name(ai), - hstrerror(h_errno), strerror(saved_errno)); close(sockfd); sockfd = -1; @@ -315,11 +314,10 @@ static int git_tcp_connect_sock(char *host, int flags) if (connect(sockfd, (struct sockaddr *)&sa, sizeof sa) < 0) { saved_errno = errno; - fprintf(stderr, "%s[%d: %s]: net=%s, errno=%s\n", + fprintf(stderr, "%s[%d: %s]: errno=%s\n", host, cnt, inet_ntoa(*(struct in_addr *)&sa.sin_addr), - hstrerror(h_errno), strerror(saved_errno)); close(sockfd); sockfd = -1; @@ -589,6 +587,7 @@ pid_t git_connect(int fd[2], char *url, const char *prog, int flags) unsetenv(ALTERNATE_DB_ENVIRONMENT); unsetenv(DB_ENVIRONMENT); unsetenv(GIT_DIR_ENVIRONMENT); + unsetenv(GIT_WORK_TREE_ENVIRONMENT); unsetenv(GRAFT_ENVIRONMENT); unsetenv(INDEX_ENVIRONMENT); execlp("sh", "sh", "-c", command, NULL); diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash index 9e72f0f7b..f2b10fa5f 100755 --- a/contrib/completion/git-completion.bash +++ b/contrib/completion/git-completion.bash @@ -682,8 +682,11 @@ _git_push () esac __gitcomp "$(__git_refs "$remote")" "" "${cur#*:}" ;; + +*) + __gitcomp "$(__git_refs)" + "${cur#+}" + ;; *) - __gitcomp "$(__git_refs2)" + __gitcomp "$(__git_refs)" ;; esac ;; diff --git a/contrib/fast-import/git-p4 b/contrib/fast-import/git-p4 new file mode 100755 index 000000000..54a05eb99 --- /dev/null +++ b/contrib/fast-import/git-p4 @@ -0,0 +1,1577 @@ +#!/usr/bin/env python +# +# git-p4.py -- A tool for bidirectional operation between a Perforce depot and git. +# +# Author: Simon Hausmann <simon@lst.de> +# Copyright: 2007 Simon Hausmann <simon@lst.de> +# 2007 Trolltech ASA +# License: MIT <http://www.opensource.org/licenses/mit-license.php> +# + +import optparse, sys, os, marshal, popen2, subprocess, shelve +import tempfile, getopt, sha, os.path, time, platform +import re + +from sets import Set; + +verbose = False + +def die(msg): + if verbose: + raise Exception(msg) + else: + sys.stderr.write(msg + "\n") + sys.exit(1) + +def write_pipe(c, str): + if verbose: + sys.stderr.write('Writing pipe: %s\n' % c) + + pipe = os.popen(c, 'w') + val = pipe.write(str) + if pipe.close(): + die('Command failed: %s' % c) + + return val + +def read_pipe(c, ignore_error=False): + if verbose: + sys.stderr.write('Reading pipe: %s\n' % c) + + pipe = os.popen(c, 'rb') + val = pipe.read() + if pipe.close() and not ignore_error: + die('Command failed: %s' % c) + + return val + + +def read_pipe_lines(c): + if verbose: + sys.stderr.write('Reading pipe: %s\n' % c) + ## todo: check return status + pipe = os.popen(c, 'rb') + val = pipe.readlines() + if pipe.close(): + die('Command failed: %s' % c) + + return val + +def system(cmd): + if verbose: + sys.stderr.write("executing %s\n" % cmd) + if os.system(cmd) != 0: + die("command failed: %s" % cmd) + +def p4CmdList(cmd): + cmd = "p4 -G %s" % cmd + if verbose: + sys.stderr.write("Opening pipe: %s\n" % cmd) + pipe = os.popen(cmd, "rb") + + result = [] + try: + while True: + entry = marshal.load(pipe) + result.append(entry) + except EOFError: + pass + exitCode = pipe.close() + if exitCode != None: + entry = {} + entry["p4ExitCode"] = exitCode + result.append(entry) + + return result + +def p4Cmd(cmd): + list = p4CmdList(cmd) + result = {} + for entry in list: + result.update(entry) + return result; + +def p4Where(depotPath): + if not depotPath.endswith("/"): + depotPath += "/" + output = p4Cmd("where %s..." % depotPath) + if output["code"] == "error": + return "" + clientPath = "" + if "path" in output: + clientPath = output.get("path") + elif "data" in output: + data = output.get("data") + lastSpace = data.rfind(" ") + clientPath = data[lastSpace + 1:] + + if clientPath.endswith("..."): + clientPath = clientPath[:-3] + return clientPath + +def currentGitBranch(): + return read_pipe("git name-rev HEAD").split(" ")[1].strip() + +def isValidGitDir(path): + if (os.path.exists(path + "/HEAD") + and os.path.exists(path + "/refs") and os.path.exists(path + "/objects")): + return True; + return False + +def parseRevision(ref): + return read_pipe("git rev-parse %s" % ref).strip() + +def extractLogMessageFromGitCommit(commit): + logMessage = "" + + ## fixme: title is first line of commit, not 1st paragraph. + foundTitle = False + for log in read_pipe_lines("git cat-file commit %s" % commit): + if not foundTitle: + if len(log) == 1: + foundTitle = True + continue + + logMessage += log + return logMessage + +def extractSettingsGitLog(log): + values = {} + for line in log.split("\n"): + line = line.strip() + m = re.search (r"^ *\[git-p4: (.*)\]$", line) + if not m: + continue + + assignments = m.group(1).split (':') + for a in assignments: + vals = a.split ('=') + key = vals[0].strip() + val = ('='.join (vals[1:])).strip() + if val.endswith ('\"') and val.startswith('"'): + val = val[1:-1] + + values[key] = val + + paths = values.get("depot-paths") + if not paths: + paths = values.get("depot-path") + if paths: + values['depot-paths'] = paths.split(',') + return values + +def gitBranchExists(branch): + proc = subprocess.Popen(["git", "rev-parse", branch], + stderr=subprocess.PIPE, stdout=subprocess.PIPE); + return proc.wait() == 0; + +def gitConfig(key): + return read_pipe("git config %s" % key, ignore_error=True).strip() + +def findUpstreamBranchPoint(head = "HEAD"): + settings = None + branchPoint = "" + parent = 0 + while parent < 65535: + commit = head + "~%s" % parent + log = extractLogMessageFromGitCommit(commit) + settings = extractSettingsGitLog(log) + if not settings.has_key("depot-paths"): + parent = parent + 1 + continue + + names = read_pipe_lines("git name-rev \"--refs=refs/remotes/p4/*\" \"%s\"" % commit) + if len(names) <= 0: + continue + + # strip away the beginning of 'HEAD~42 refs/remotes/p4/foo' + branchPoint = names[0].strip()[len(commit) + 1:] + break + + return [branchPoint, settings] + +class Command: + def __init__(self): + self.usage = "usage: %prog [options]" + self.needsGit = True + +class P4Debug(Command): + def __init__(self): + Command.__init__(self) + self.options = [ + optparse.make_option("--verbose", dest="verbose", action="store_true", + default=False), + ] + self.description = "A tool to debug the output of p4 -G." + self.needsGit = False + self.verbose = False + + def run(self, args): + j = 0 + for output in p4CmdList(" ".join(args)): + print 'Element: %d' % j + j += 1 + print output + return True + +class P4RollBack(Command): + def __init__(self): + Command.__init__(self) + self.options = [ + optparse.make_option("--verbose", dest="verbose", action="store_true"), + optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true") + ] + self.description = "A tool to debug the multi-branch import. Don't use :)" + self.verbose = False + self.rollbackLocalBranches = False + + def run(self, args): + if len(args) != 1: + return False + maxChange = int(args[0]) + + if "p4ExitCode" in p4Cmd("changes -m 1"): + die("Problems executing p4"); + + if self.rollbackLocalBranches: + refPrefix = "refs/heads/" + lines = read_pipe_lines("git rev-parse --symbolic --branches") + else: + refPrefix = "refs/remotes/" + lines = read_pipe_lines("git rev-parse --symbolic --remotes") + + for line in lines: + if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"): + line = line.strip() + ref = refPrefix + line + log = extractLogMessageFromGitCommit(ref) + settings = extractSettingsGitLog(log) + + depotPaths = settings['depot-paths'] + change = settings['change'] + + changed = False + + if len(p4Cmd("changes -m 1 " + ' '.join (['%s...@%s' % (p, maxChange) + for p in depotPaths]))) == 0: + print "Branch %s did not exist at change %s, deleting." % (ref, maxChange) + system("git update-ref -d %s `git rev-parse %s`" % (ref, ref)) + continue + + while change and int(change) > maxChange: + changed = True + if self.verbose: + print "%s is at %s ; rewinding towards %s" % (ref, change, maxChange) + system("git update-ref %s \"%s^\"" % (ref, ref)) + log = extractLogMessageFromGitCommit(ref) + settings = extractSettingsGitLog(log) + + + depotPaths = settings['depot-paths'] + change = settings['change'] + + if changed: + print "%s rewound to %s" % (ref, change) + + return True + +class P4Submit(Command): + def __init__(self): + Command.__init__(self) + self.options = [ + optparse.make_option("--continue", action="store_false", dest="firstTime"), + optparse.make_option("--verbose", dest="verbose", action="store_true"), + optparse.make_option("--origin", dest="origin"), + optparse.make_option("--reset", action="store_true", dest="reset"), + optparse.make_option("--log-substitutions", dest="substFile"), + optparse.make_option("--dry-run", action="store_true"), + optparse.make_option("--direct", dest="directSubmit", action="store_true"), + optparse.make_option("--trust-me-like-a-fool", dest="trustMeLikeAFool", action="store_true"), + ] + self.description = "Submit changes from git to the perforce depot." + self.usage += " [name of git branch to submit into perforce depot]" + self.firstTime = True + self.reset = False + self.interactive = True + self.dryRun = False + self.substFile = "" + self.firstTime = True + self.origin = "" + self.directSubmit = False + self.trustMeLikeAFool = False + self.verbose = False + self.isWindows = (platform.system() == "Windows") + + self.logSubstitutions = {} + self.logSubstitutions["<enter description here>"] = "%log%" + self.logSubstitutions["\tDetails:"] = "\tDetails: %log%" + + def check(self): + if len(p4CmdList("opened ...")) > 0: + die("You have files opened with perforce! Close them before starting the sync.") + + def start(self): + if len(self.config) > 0 and not self.reset: + die("Cannot start sync. Previous sync config found at %s\n" + "If you want to start submitting again from scratch " + "maybe you want to call git-p4 submit --reset" % self.configFile) + + commits = [] + if self.directSubmit: + commits.append("0") + else: + for line in read_pipe_lines("git rev-list --no-merges %s..%s" % (self.origin, self.master)): + commits.append(line.strip()) + commits.reverse() + + self.config["commits"] = commits + + def prepareLogMessage(self, template, message): + result = "" + + for line in template.split("\n"): + if line.startswith("#"): + result += line + "\n" + continue + + substituted = False + for key in self.logSubstitutions.keys(): + if line.find(key) != -1: + value = self.logSubstitutions[key] + value = value.replace("%log%", message) + if value != "@remove@": + result += line.replace(key, value) + "\n" + substituted = True + break + + if not substituted: + result += line + "\n" + + return result + + def applyCommit(self, id): + if self.directSubmit: + print "Applying local change in working directory/index" + diff = self.diffStatus + else: + print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id)) + diff = read_pipe_lines("git diff-tree -r --name-status \"%s^\" \"%s\"" % (id, id)) + filesToAdd = set() + filesToDelete = set() + editedFiles = set() + for line in diff: + modifier = line[0] + path = line[1:].strip() + if modifier == "M": + system("p4 edit \"%s\"" % path) + editedFiles.add(path) + elif modifier == "A": + filesToAdd.add(path) + if path in filesToDelete: + filesToDelete.remove(path) + elif modifier == "D": + filesToDelete.add(path) + if path in filesToAdd: + filesToAdd.remove(path) + else: + die("unknown modifier %s for %s" % (modifier, path)) + + if self.directSubmit: + diffcmd = "cat \"%s\"" % self.diffFile + else: + diffcmd = "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id) + patchcmd = diffcmd + " | git apply " + tryPatchCmd = patchcmd + "--check -" + applyPatchCmd = patchcmd + "--check --apply -" + + if os.system(tryPatchCmd) != 0: + print "Unfortunately applying the change failed!" + print "What do you want to do?" + response = "x" + while response != "s" and response != "a" and response != "w": + response = raw_input("[s]kip this patch / [a]pply the patch forcibly " + "and with .rej files / [w]rite the patch to a file (patch.txt) ") + if response == "s": + print "Skipping! Good luck with the next patches..." + return + elif response == "a": + os.system(applyPatchCmd) + if len(filesToAdd) > 0: + print "You may also want to call p4 add on the following files:" + print " ".join(filesToAdd) + if len(filesToDelete): + print "The following files should be scheduled for deletion with p4 delete:" + print " ".join(filesToDelete) + die("Please resolve and submit the conflict manually and " + + "continue afterwards with git-p4 submit --continue") + elif response == "w": + system(diffcmd + " > patch.txt") + print "Patch saved to patch.txt in %s !" % self.clientPath + die("Please resolve and submit the conflict manually and " + "continue afterwards with git-p4 submit --continue") + + system(applyPatchCmd) + + for f in filesToAdd: + system("p4 add \"%s\"" % f) + for f in filesToDelete: + system("p4 revert \"%s\"" % f) + system("p4 delete \"%s\"" % f) + + logMessage = "" + if not self.directSubmit: + logMessage = extractLogMessageFromGitCommit(id) + logMessage = logMessage.replace("\n", "\n\t") + if self.isWindows: + logMessage = logMessage.replace("\n", "\r\n") + logMessage = logMessage.strip() + + template = read_pipe("p4 change -o") + + if self.interactive: + submitTemplate = self.prepareLogMessage(template, logMessage) + diff = read_pipe("p4 diff -du ...") + + for newFile in filesToAdd: + diff += "==== new file ====\n" + diff += "--- /dev/null\n" + diff += "+++ %s\n" % newFile + f = open(newFile, "r") + for line in f.readlines(): + diff += "+" + line + f.close() + + separatorLine = "######## everything below this line is just the diff #######" + if platform.system() == "Windows": + separatorLine += "\r" + separatorLine += "\n" + + response = "e" + if self.trustMeLikeAFool: + response = "y" + + firstIteration = True + while response == "e": + if not firstIteration: + response = raw_input("Do you want to submit this change? [y]es/[e]dit/[n]o/[s]kip ") + firstIteration = False + if response == "e": + [handle, fileName] = tempfile.mkstemp() + tmpFile = os.fdopen(handle, "w+") + tmpFile.write(submitTemplate + separatorLine + diff) + tmpFile.close() + defaultEditor = "vi" + if platform.system() == "Windows": + defaultEditor = "notepad" + editor = os.environ.get("EDITOR", defaultEditor); + system(editor + " " + fileName) + tmpFile = open(fileName, "rb") + message = tmpFile.read() + tmpFile.close() + os.remove(fileName) + submitTemplate = message[:message.index(separatorLine)] + if self.isWindows: + submitTemplate = submitTemplate.replace("\r\n", "\n") + + if response == "y" or response == "yes": + if self.dryRun: + print submitTemplate + raw_input("Press return to continue...") + else: + if self.directSubmit: + print "Submitting to git first" + os.chdir(self.oldWorkingDirectory) + write_pipe("git commit -a -F -", submitTemplate) + os.chdir(self.clientPath) + + write_pipe("p4 submit -i", submitTemplate) + elif response == "s": + for f in editedFiles: + system("p4 revert \"%s\"" % f); + for f in filesToAdd: + system("p4 revert \"%s\"" % f); + system("rm %s" %f) + for f in filesToDelete: + system("p4 delete \"%s\"" % f); + return + else: + print "Not submitting!" + self.interactive = False + else: + fileName = "submit.txt" + file = open(fileName, "w+") + file.write(self.prepareLogMessage(template, logMessage)) + file.close() + print ("Perforce submit template written as %s. " + + "Please review/edit and then use p4 submit -i < %s to submit directly!" + % (fileName, fileName)) + + def run(self, args): + if len(args) == 0: + self.master = currentGitBranch() + if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master): + die("Detecting current git branch failed!") + elif len(args) == 1: + self.master = args[0] + else: + return False + + [upstream, settings] = findUpstreamBranchPoint() + depotPath = settings['depot-paths'][0] + if len(self.origin) == 0: + self.origin = upstream + + if self.verbose: + print "Origin branch is " + self.origin + + if len(depotPath) == 0: + print "Internal error: cannot locate perforce depot path from existing branches" + sys.exit(128) + + self.clientPath = p4Where(depotPath) + + if len(self.clientPath) == 0: + print "Error: Cannot locate perforce checkout of %s in client view" % depotPath + sys.exit(128) + + print "Perforce checkout for depot path %s located at %s" % (depotPath, self.clientPath) + self.oldWorkingDirectory = os.getcwd() + + if self.directSubmit: + self.diffStatus = read_pipe_lines("git diff -r --name-status HEAD") + if len(self.diffStatus) == 0: + print "No changes in working directory to submit." + return True + patch = read_pipe("git diff -p --binary --diff-filter=ACMRTUXB HEAD") + self.diffFile = self.gitdir + "/p4-git-diff" + f = open(self.diffFile, "wb") + f.write(patch) + f.close(); + + os.chdir(self.clientPath) + response = raw_input("Do you want to sync %s with p4 sync? [y]es/[n]o " % self.clientPath) + if response == "y" or response == "yes": + system("p4 sync ...") + + if self.reset: + self.firstTime = True + + if len(self.substFile) > 0: + for line in open(self.substFile, "r").readlines(): + tokens = line.strip().split("=") + self.logSubstitutions[tokens[0]] = tokens[1] + + self.check() + self.configFile = self.gitdir + "/p4-git-sync.cfg" + self.config = shelve.open(self.configFile, writeback=True) + + if self.firstTime: + self.start() + + commits = self.config.get("commits", []) + + while len(commits) > 0: + self.firstTime = False + commit = commits[0] + commits = commits[1:] + self.config["commits"] = commits + self.applyCommit(commit) + if not self.interactive: + break + + self.config.close() + + if self.directSubmit: + os.remove(self.diffFile) + + if len(commits) == 0: + if self.firstTime: + print "No changes found to apply between %s and current HEAD" % self.origin + else: + print "All changes applied!" + os.chdir(self.oldWorkingDirectory) + response = raw_input("Do you want to sync from Perforce now using git-p4 rebase? [y]es/[n]o ") + if response == "y" or response == "yes": + rebase = P4Rebase() + rebase.run([]) + os.remove(self.configFile) + + return True + +class P4Sync(Command): + def __init__(self): + Command.__init__(self) + self.options = [ + optparse.make_option("--branch", dest="branch"), + optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"), + optparse.make_option("--changesfile", dest="changesFile"), + optparse.make_option("--silent", dest="silent", action="store_true"), + optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"), + optparse.make_option("--verbose", dest="verbose", action="store_true"), + optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false", + help="Import into refs/heads/ , not refs/remotes"), + optparse.make_option("--max-changes", dest="maxChanges"), + optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true', + help="Keep entire BRANCH/DIR/SUBDIR prefix during import") + ] + self.description = """Imports from Perforce into a git repository.\n + example: + //depot/my/project/ -- to import the current head + //depot/my/project/@all -- to import everything + //depot/my/project/@1,6 -- to import only from revision 1 to 6 + + (a ... is not needed in the path p4 specification, it's added implicitly)""" + + self.usage += " //depot/path[@revRange]" + self.silent = False + self.createdBranches = Set() + self.committedChanges = Set() + self.branch = "" + self.detectBranches = False + self.detectLabels = False + self.changesFile = "" + self.syncWithOrigin = True + self.verbose = False + self.importIntoRemotes = True + self.maxChanges = "" + self.isWindows = (platform.system() == "Windows") + self.keepRepoPath = False + self.depotPaths = None + self.p4BranchesInGit = [] + + if gitConfig("git-p4.syncFromOrigin") == "false": + self.syncWithOrigin = False + + def extractFilesFromCommit(self, commit): + files = [] + fnum = 0 + while commit.has_key("depotFile%s" % fnum): + path = commit["depotFile%s" % fnum] + + found = [p for p in self.depotPaths + if path.startswith (p)] + if not found: + fnum = fnum + 1 + continue + + file = {} + file["path"] = path + file["rev"] = commit["rev%s" % fnum] + file["action"] = commit["action%s" % fnum] + file["type"] = commit["type%s" % fnum] + files.append(file) + fnum = fnum + 1 + return files + + def stripRepoPath(self, path, prefixes): + if self.keepRepoPath: + prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])] + + for p in prefixes: + if path.startswith(p): + path = path[len(p):] + + return path + + def splitFilesIntoBranches(self, commit): + branches = {} + fnum = 0 + while commit.has_key("depotFile%s" % fnum): + path = commit["depotFile%s" % fnum] + found = [p for p in self.depotPaths + if path.startswith (p)] + if not found: + fnum = fnum + 1 + continue + + file = {} + file["path"] = path + file["rev"] = commit["rev%s" % fnum] + file["action"] = commit["action%s" % fnum] + file["type"] = commit["type%s" % fnum] + fnum = fnum + 1 + + relPath = self.stripRepoPath(path, self.depotPaths) + + for branch in self.knownBranches.keys(): + + # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2 + if relPath.startswith(branch + "/"): + if branch not in branches: + branches[branch] = [] + branches[branch].append(file) + break + + return branches + + ## Should move this out, doesn't use SELF. + def readP4Files(self, files): + files = [f for f in files + if f['action'] != 'delete'] + + if not files: + return + + # We cannot put all the files on the command line + # OS have limitations on the max lenght of arguments + # POSIX says it's 4096 bytes, default for Linux seems to be 130 K. + # and all OS from the table below seems to be higher than POSIX. + # See http://www.in-ulm.de/~mascheck/various/argmax/ + argmax = min(4000, os.sysconf('SC_ARG_MAX')) + chunk = '' + filedata = [] + for i in xrange(len(files)): + f = files[i] + chunk += '"%s#%s" ' % (f['path'], f['rev']) + if len(chunk) > argmax or i == len(files)-1: + data = p4CmdList('print %s' % chunk) + if "p4ExitCode" in data[0]: + die("Problems executing p4. Error: [%d]." % (data[0]['p4ExitCode'])); + filedata.extend(data) + chunk = '' + + j = 0; + contents = {} + while j < len(filedata): + stat = filedata[j] + j += 1 + text = '' + while j < len(filedata) and filedata[j]['code'] in ('text', + 'binary'): + text += filedata[j]['data'] + j += 1 + + + if not stat.has_key('depotFile'): + sys.stderr.write("p4 print fails with: %s\n" % repr(stat)) + continue + + contents[stat['depotFile']] = text + + for f in files: + assert not f.has_key('data') + f['data'] = contents[f['path']] + + def commit(self, details, files, branch, branchPrefixes, parent = ""): + epoch = details["time"] + author = details["user"] + + if self.verbose: + print "commit into %s" % branch + + # start with reading files; if that fails, we should not + # create a commit. + new_files = [] + for f in files: + if [p for p in branchPrefixes if f['path'].startswith(p)]: + new_files.append (f) + else: + sys.stderr.write("Ignoring file outside of prefix: %s\n" % path) + files = new_files + self.readP4Files(files) + + + + + self.gitStream.write("commit %s\n" % branch) +# gitStream.write("mark :%s\n" % details["change"]) + self.committedChanges.add(int(details["change"])) + committer = "" + if author not in self.users: + self.getUserMapFromPerforceServer() + if author in self.users: + committer = "%s %s %s" % (self.users[author], epoch, self.tz) + else: + committer = "%s <a@b> %s %s" % (author, epoch, self.tz) + + self.gitStream.write("committer %s\n" % committer) + + self.gitStream.write("data <<EOT\n") + self.gitStream.write(details["desc"]) + self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s" + % (','.join (branchPrefixes), details["change"])) + if len(details['options']) > 0: + self.gitStream.write(": options = %s" % details['options']) + self.gitStream.write("]\nEOT\n\n") + + if len(parent) > 0: + if self.verbose: + print "parent %s" % parent + self.gitStream.write("from %s\n" % parent) + + for file in files: + if file["type"] == "apple": + print "\nfile %s is a strange apple file that forks. Ignoring!" % file['path'] + continue + + relPath = self.stripRepoPath(file['path'], branchPrefixes) + if file["action"] == "delete": + self.gitStream.write("D %s\n" % relPath) + else: + mode = 644 + if file["type"].startswith("x"): + mode = 755 + + data = file['data'] + + if self.isWindows and file["type"].endswith("text"): + data = data.replace("\r\n", "\n") + + self.gitStream.write("M %d inline %s\n" % (mode, relPath)) + self.gitStream.write("data %s\n" % len(data)) + self.gitStream.write(data) + self.gitStream.write("\n") + + self.gitStream.write("\n") + + change = int(details["change"]) + + if self.labels.has_key(change): + label = self.labels[change] + labelDetails = label[0] + labelRevisions = label[1] + if self.verbose: + print "Change %s is labelled %s" % (change, labelDetails) + + files = p4CmdList("files " + ' '.join (["%s...@%s" % (p, change) + for p in branchPrefixes])) + + if len(files) == len(labelRevisions): + + cleanedFiles = {} + for info in files: + if info["action"] == "delete": + continue + cleanedFiles[info["depotFile"]] = info["rev"] + + if cleanedFiles == labelRevisions: + self.gitStream.write("tag tag_%s\n" % labelDetails["label"]) + self.gitStream.write("from %s\n" % branch) + + owner = labelDetails["Owner"] + tagger = "" + if author in self.users: + tagger = "%s %s %s" % (self.users[owner], epoch, self.tz) + else: + tagger = "%s <a@b> %s %s" % (owner, epoch, self.tz) + self.gitStream.write("tagger %s\n" % tagger) + self.gitStream.write("data <<EOT\n") + self.gitStream.write(labelDetails["Description"]) + self.gitStream.write("EOT\n\n") + + else: + if not self.silent: + print ("Tag %s does not match with change %s: files do not match." + % (labelDetails["label"], change)) + + else: + if not self.silent: + print ("Tag %s does not match with change %s: file count is different." + % (labelDetails["label"], change)) + + def getUserCacheFilename(self): + return os.environ["HOME"] + "/.gitp4-usercache.txt" + + def getUserMapFromPerforceServer(self): + if self.userMapFromPerforceServer: + return + self.users = {} + + for output in p4CmdList("users"): + if not output.has_key("User"): + continue + self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">" + + + s = '' + for (key, val) in self.users.items(): + s += "%s\t%s\n" % (key, val) + + open(self.getUserCacheFilename(), "wb").write(s) + self.userMapFromPerforceServer = True + + def loadUserMapFromCache(self): + self.users = {} + self.userMapFromPerforceServer = False + try: + cache = open(self.getUserCacheFilename(), "rb") + lines = cache.readlines() + cache.close() + for line in lines: + entry = line.strip().split("\t") + self.users[entry[0]] = entry[1] + except IOError: + self.getUserMapFromPerforceServer() + + def getLabels(self): + self.labels = {} + + l = p4CmdList("labels %s..." % ' '.join (self.depotPaths)) + if len(l) > 0 and not self.silent: + print "Finding files belonging to labels in %s" % `self.depotPath` + + for output in l: + label = output["label"] + revisions = {} + newestChange = 0 + if self.verbose: + print "Querying files for label %s" % label + for file in p4CmdList("files " + + ' '.join (["%s...@%s" % (p, label) + for p in self.depotPaths])): + revisions[file["depotFile"]] = file["rev"] + change = int(file["change"]) + if change > newestChange: + newestChange = change + + self.labels[newestChange] = [output, revisions] + + if self.verbose: + print "Label changes: %s" % self.labels.keys() + + def guessProjectName(self): + for p in self.depotPaths: + if p.endswith("/"): + p = p[:-1] + p = p[p.strip().rfind("/") + 1:] + if not p.endswith("/"): + p += "/" + return p + + def getBranchMapping(self): + lostAndFoundBranches = set() + + for info in p4CmdList("branches"): + details = p4Cmd("branch -o %s" % info["branch"]) + viewIdx = 0 + while details.has_key("View%s" % viewIdx): + paths = details["View%s" % viewIdx].split(" ") + viewIdx = viewIdx + 1 + # require standard //depot/foo/... //depot/bar/... mapping + if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."): + continue + source = paths[0] + destination = paths[1] + ## HACK + if source.startswith(self.depotPaths[0]) and destination.startswith(self.depotPaths[0]): + source = source[len(self.depotPaths[0]):-4] + destination = destination[len(self.depotPaths[0]):-4] + + if destination in self.knownBranches: + if not self.silent: + print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination) + print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination) + continue + + self.knownBranches[destination] = source + + lostAndFoundBranches.discard(destination) + + if source not in self.knownBranches: + lostAndFoundBranches.add(source) + + + for branch in lostAndFoundBranches: + self.knownBranches[branch] = branch + + def listExistingP4GitBranches(self): + self.p4BranchesInGit = [] + + cmdline = "git rev-parse --symbolic " + if self.importIntoRemotes: + cmdline += " --remotes" + else: + 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": + continue + branch = line + + # strip off p4 + branch = re.sub ("^p4/", "", line) + + self.p4BranchesInGit.append(branch) + self.initialParents[self.refPrefix + branch] = parseRevision(line) + + def createOrUpdateBranchesFromOrigin(self): + if not self.silent: + print ("Creating/updating branch(es) in %s based on origin branch(es)" + % self.refPrefix) + + originPrefix = "origin/p4/" + + for line in read_pipe_lines("git rev-parse --symbolic --remotes"): + line = line.strip() + if (not line.startswith(originPrefix)) or line.endswith("HEAD"): + continue + + headName = line[len(originPrefix):] + remoteHead = self.refPrefix + headName + originHead = line + + original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead)) + if (not original.has_key('depot-paths') + or not original.has_key('change')): + continue + + update = False + if not gitBranchExists(remoteHead): + if self.verbose: + print "creating %s" % remoteHead + update = True + else: + settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead)) + if settings.has_key('change') > 0: + if settings['depot-paths'] == original['depot-paths']: + originP4Change = int(original['change']) + p4Change = int(settings['change']) + if originP4Change > p4Change: + print ("%s (%s) is newer than %s (%s). " + "Updating p4 branch from origin." + % (originHead, originP4Change, + remoteHead, p4Change)) + update = True + else: + print ("Ignoring: %s was imported from %s while " + "%s was imported from %s" + % (originHead, ','.join(original['depot-paths']), + remoteHead, ','.join(settings['depot-paths']))) + + if update: + system("git update-ref %s %s" % (remoteHead, originHead)) + + def updateOptionDict(self, d): + option_keys = {} + if self.keepRepoPath: + option_keys['keepRepoPath'] = 1 + + d["options"] = ' '.join(sorted(option_keys.keys())) + + def readOptions(self, d): + self.keepRepoPath = (d.has_key('options') + and ('keepRepoPath' in d['options'])) + + def run(self, args): + self.depotPaths = [] + self.changeRange = "" + self.initialParent = "" + self.previousDepotPaths = [] + + # map from branch depot path to parent branch + self.knownBranches = {} + self.initialParents = {} + self.hasOrigin = gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master") + 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 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: + system("git symbolic-ref %sHEAD %s" % (self.refPrefix, self.branch)) + + # TODO: should always look at previous commits, + # merge with previous imports, if possible. + if args == []: + if self.hasOrigin: + self.createOrUpdateBranchesFromOrigin() + self.listExistingP4GitBranches() + + if len(self.p4BranchesInGit) > 1: + if not self.silent: + print "Importing from/into multiple branches" + self.detectBranches = True + + if self.verbose: + print "branches: %s" % self.p4BranchesInGit + + p4Change = 0 + for branch in self.p4BranchesInGit: + logMsg = extractLogMessageFromGitCommit(self.refPrefix + branch) + + settings = extractSettingsGitLog(logMsg) + + self.readOptions(settings) + if (settings.has_key('depot-paths') + and settings.has_key ('change')): + change = int(settings['change']) + 1 + p4Change = max(p4Change, change) + + depotPaths = sorted(settings['depot-paths']) + if self.previousDepotPaths == []: + self.previousDepotPaths = depotPaths + else: + paths = [] + for (prev, cur) in zip(self.previousDepotPaths, depotPaths): + for i in range(0, min(len(cur), len(prev))): + if cur[i] <> prev[i]: + i = i - 1 + break + + paths.append (cur[:i + 1]) + + self.previousDepotPaths = paths + + 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 + + if not self.branch.startswith("refs/"): + self.branch = "refs/heads/" + self.branch + + if len(args) == 0 and self.depotPaths: + if not self.silent: + print "Depot paths: %s" % ' '.join(self.depotPaths) + else: + if self.depotPaths and self.depotPaths != args: + print ("previous import used depot path %s and now %s was specified. " + "This doesn't work!" % (' '.join (self.depotPaths), + ' '.join (args))) + sys.exit(1) + + self.depotPaths = sorted(args) + + self.revision = "" + self.users = {} + + newPaths = [] + for p in self.depotPaths: + if p.find("@") != -1: + atIdx = p.index("@") + self.changeRange = p[atIdx:] + if self.changeRange == "@all": + self.changeRange = "" + elif ',' not in self.changeRange: + self.revision = self.changeRange + self.changeRange = "" + p = p[0:atIdx] + elif p.find("#") != -1: + hashIdx = p.index("#") + self.revision = p[hashIdx:] + p = p[0:hashIdx] + elif self.previousDepotPaths == []: + self.revision = "#head" + + p = re.sub ("\.\.\.$", "", p) + if not p.endswith("/"): + p += "/" + + newPaths.append(p) + + self.depotPaths = newPaths + + + self.loadUserMapFromCache() + self.labels = {} + if self.detectLabels: + self.getLabels(); + + if self.detectBranches: + ## FIXME - what's a P4 projectName ? + self.projectName = self.guessProjectName() + + if not self.hasOrigin: + self.getBranchMapping(); + if self.verbose: + print "p4-git branches: %s" % self.p4BranchesInGit + print "initial parents: %s" % self.initialParents + for b in self.p4BranchesInGit: + if b != "master": + + ## FIXME + b = b[len(self.projectName):] + self.createdBranches.add(b) + + self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60)) + + importProcess = subprocess.Popen(["git", "fast-import"], + stdin=subprocess.PIPE, stdout=subprocess.PIPE, + stderr=subprocess.PIPE); + self.gitOutput = importProcess.stdout + self.gitStream = importProcess.stdin + self.gitError = importProcess.stderr + + if self.revision: + print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), self.revision, self.branch) + + details = { "user" : "git perforce import user", "time" : int(time.time()) } + details["desc"] = ("Initial import of %s from the state at revision %s" + % (' '.join(self.depotPaths), self.revision)) + details["change"] = self.revision + newestRevision = 0 + + fileCnt = 0 + for info in p4CmdList("files " + + ' '.join(["%s...%s" + % (p, self.revision) + for p in self.depotPaths])): + + if info['code'] == 'error': + sys.stderr.write("p4 returned an error: %s\n" + % info['data']) + sys.exit(1) + + + change = int(info["change"]) + if change > newestRevision: + newestRevision = change + + if info["action"] == "delete": + # don't increase the file cnt, otherwise details["depotFile123"] will have gaps! + #fileCnt = fileCnt + 1 + continue + + for prop in ["depotFile", "rev", "action", "type" ]: + details["%s%s" % (prop, fileCnt)] = info[prop] + + fileCnt = fileCnt + 1 + + details["change"] = newestRevision + self.updateOptionDict(details) + try: + self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths) + except IOError: + print "IO error with git fast-import. Is your git version recent enough?" + print self.gitError.read() + + else: + changes = [] + + if len(self.changesFile) > 0: + output = open(self.changesFile).readlines() + changeSet = Set() + for line in output: + changeSet.add(int(line)) + + for change in changeSet: + changes.append(change) + + changes.sort() + else: + if self.verbose: + print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths), + self.changeRange) + assert self.depotPaths + output = read_pipe_lines("p4 changes " + ' '.join (["%s...%s" % (p, self.changeRange) + for p in self.depotPaths])) + + for line in output: + changeNum = line.split(" ")[1] + changes.append(changeNum) + + changes.reverse() + + if len(self.maxChanges) > 0: + changes = changes[0:min(int(self.maxChanges), len(changes))] + + if len(changes) == 0: + if not self.silent: + print "No changes to import!" + return True + + if not self.silent and not self.detectBranches: + print "Import destination: %s" % self.branch + + self.updatedBranches = set() + + cnt = 1 + for change in changes: + description = p4Cmd("describe %s" % change) + self.updateOptionDict(description) + + if not self.silent: + sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes))) + sys.stdout.flush() + cnt = cnt + 1 + + try: + if self.detectBranches: + branches = self.splitFilesIntoBranches(description) + for branch in branches.keys(): + ## HACK --hwn + branchPrefix = self.depotPaths[0] + branch + "/" + + parent = "" + + filesForCommit = branches[branch] + + if self.verbose: + print "branch is %s" % branch + + self.updatedBranches.add(branch) + + if branch not in self.createdBranches: + self.createdBranches.add(branch) + parent = self.knownBranches[branch] + if parent == branch: + parent = "" + elif self.verbose: + print "parent determined through known branches: %s" % parent + + # main branch? use master + if branch == "main": + branch = "master" + else: + + ## FIXME + branch = self.projectName + branch + + if parent == "main": + parent = "master" + elif len(parent) > 0: + ## FIXME + parent = self.projectName + parent + + branch = self.refPrefix + branch + if len(parent) > 0: + parent = self.refPrefix + parent + + if self.verbose: + print "looking for initial parent for %s; current parent is %s" % (branch, parent) + + if len(parent) == 0 and branch in self.initialParents: + parent = self.initialParents[branch] + del self.initialParents[branch] + + self.commit(description, filesForCommit, branch, [branchPrefix], parent) + else: + files = self.extractFilesFromCommit(description) + self.commit(description, files, self.branch, self.depotPaths, + self.initialParent) + self.initialParent = "" + except IOError: + print self.gitError.read() + sys.exit(1) + + if not self.silent: + print "" + if len(self.updatedBranches) > 0: + sys.stdout.write("Updated branches: ") + for b in self.updatedBranches: + sys.stdout.write("%s " % b) + sys.stdout.write("\n") + + + self.gitStream.close() + if importProcess.wait() != 0: + die("fast-import failed: %s" % self.gitError.read()) + self.gitOutput.close() + self.gitError.close() + + return True + +class P4Rebase(Command): + def __init__(self): + Command.__init__(self) + self.options = [ ] + self.description = ("Fetches the latest revision from perforce and " + + "rebases the current work (branch) against it") + self.verbose = False + + def run(self, args): + sync = P4Sync() + sync.run([]) + + [upstream, settings] = findUpstreamBranchPoint() + if len(upstream) == 0: + die("Cannot find upstream branchpoint for rebase") + + # the branchpoint may be p4/foo~3, so strip off the parent + upstream = re.sub("~[0-9]+$", "", upstream) + + print "Rebasing the current branch onto %s" % upstream + oldHead = read_pipe("git rev-parse HEAD").strip() + system("git rebase %s" % upstream) + system("git diff-tree --stat --summary -M %s HEAD" % oldHead) + return True + +class P4Clone(P4Sync): + def __init__(self): + P4Sync.__init__(self) + self.description = "Creates a new git repository and imports from Perforce into it" + self.usage = "usage: %prog [options] //depot/path[@revRange]" + self.options.append( + optparse.make_option("--destination", dest="cloneDestination", + action='store', default=None, + help="where to leave result of the clone")) + self.cloneDestination = None + self.needsGit = False + + def defaultDestination(self, args): + ## TODO: use common prefix of args? + depotPath = args[0] + depotDir = re.sub("(@[^@]*)$", "", depotPath) + depotDir = re.sub("(#[^#]*)$", "", depotDir) + depotDir = re.sub(r"\.\.\.$,", "", depotDir) + depotDir = re.sub(r"/$", "", depotDir) + return os.path.split(depotDir)[1] + + def run(self, args): + if len(args) < 1: + return False + + if self.keepRepoPath and not self.cloneDestination: + sys.stderr.write("Must specify destination for --keep-path\n") + sys.exit(1) + + depotPaths = args + + if not self.cloneDestination and len(depotPaths) > 1: + self.cloneDestination = depotPaths[-1] + depotPaths = depotPaths[:-1] + + for p in depotPaths: + if not p.startswith("//"): + return False + + if not self.cloneDestination: + self.cloneDestination = self.defaultDestination(args) + + print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination) + if not os.path.exists(self.cloneDestination): + os.makedirs(self.cloneDestination) + os.chdir(self.cloneDestination) + system("git init") + self.gitdir = os.getcwd() + "/.git" + if not P4Sync.run(self, depotPaths): + return False + if self.branch != "master": + if gitBranchExists("refs/remotes/p4/master"): + system("git branch master refs/remotes/p4/master") + system("git checkout -f") + else: + print "Could not detect main branch. No checkout/master branch created." + + return True + +class P4Branches(Command): + def __init__(self): + Command.__init__(self) + self.options = [ ] + self.description = ("Shows the git branches that hold imports and their " + + "corresponding perforce depot paths") + self.verbose = False + + def run(self, args): + cmdline = "git rev-parse --symbolic " + cmdline += " --remotes" + + for line in read_pipe_lines(cmdline): + line = line.strip() + + if not line.startswith('p4/') or line == "p4/HEAD": + continue + branch = line + + log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch) + settings = extractSettingsGitLog(log) + + print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"]) + return True + +class HelpFormatter(optparse.IndentedHelpFormatter): + def __init__(self): + optparse.IndentedHelpFormatter.__init__(self) + + def format_description(self, description): + if description: + return description + "\n" + else: + return "" + +def printUsage(commands): + print "usage: %s <command> [options]" % sys.argv[0] + print "" + print "valid commands: %s" % ", ".join(commands) + print "" + print "Try %s <command> --help for command specific help." % sys.argv[0] + print "" + +commands = { + "debug" : P4Debug, + "submit" : P4Submit, + "sync" : P4Sync, + "rebase" : P4Rebase, + "clone" : P4Clone, + "rollback" : P4RollBack, + "branches" : P4Branches +} + + +def main(): + if len(sys.argv[1:]) == 0: + printUsage(commands.keys()) + sys.exit(2) + + cmd = "" + cmdName = sys.argv[1] + try: + klass = commands[cmdName] + cmd = klass() + except KeyError: + print "unknown command %s" % cmdName + print "" + printUsage(commands.keys()) + sys.exit(2) + + options = cmd.options + cmd.gitdir = os.environ.get("GIT_DIR", None) + + args = sys.argv[2:] + + if len(options) > 0: + options.append(optparse.make_option("--git-dir", dest="gitdir")) + + parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName), + options, + description = cmd.description, + formatter = HelpFormatter()) + + (cmd, args) = parser.parse_args(sys.argv[2:], cmd); + global verbose + verbose = cmd.verbose + if cmd.needsGit: + if cmd.gitdir == None: + cmd.gitdir = os.path.abspath(".git") + if not isValidGitDir(cmd.gitdir): + cmd.gitdir = read_pipe("git rev-parse --git-dir").strip() + if os.path.exists(cmd.gitdir): + cdup = read_pipe("git rev-parse --show-cdup").strip() + if len(cdup) > 0: + os.chdir(cdup); + + if not isValidGitDir(cmd.gitdir): + if isValidGitDir(cmd.gitdir + "/.git"): + cmd.gitdir += "/.git" + else: + die("fatal: cannot locate git repository at %s" % cmd.gitdir) + + os.environ["GIT_DIR"] = cmd.gitdir + + if not cmd.run(args): + parser.print_help() + + +if __name__ == '__main__': + main() diff --git a/contrib/fast-import/git-p4.bat b/contrib/fast-import/git-p4.bat new file mode 100644 index 000000000..9f97e884f --- /dev/null +++ b/contrib/fast-import/git-p4.bat @@ -0,0 +1 @@ +@python "%~d0%~p0git-p4" %* diff --git a/contrib/fast-import/git-p4.txt b/contrib/fast-import/git-p4.txt new file mode 100644 index 000000000..b16a8384b --- /dev/null +++ b/contrib/fast-import/git-p4.txt @@ -0,0 +1,159 @@ +git-p4 - Perforce <-> Git converter using git-fast-import + +Usage +===== + +git-p4 supports two main modes: Importing from Perforce to a Git repository is +done using "git-p4 sync" or "git-p4 rebase". Submitting changes from Git back +to Perforce is done using "git-p4 submit". + +Importing +========= + +You can simply start with + + git-p4 clone //depot/path/project + +or + + git-p4 clone //depot/path/project myproject + +This will create an empty git repository in a subdirectory called "project" (or +"myproject" with the second command), import the head revision from the +specified perforce path into a git "p4" branch (remotes/p4 actually), create a +master branch off it and check it out. If you want the entire history (not just +the head revision) then you can simply append a "@all" to the depot path: + + git-p4 clone //depot/project/main@all myproject + + + +If you want more control you can also use the git-p4 sync command directly: + + mkdir repo-git + cd repo-git + git init + git-p4 sync //path/in/your/perforce/depot + +This will import the current head revision of the specified depot path into a +"remotes/p4/master" branch of your git repository. You can use the +--branch=mybranch option to use a different branch. + +If you want to import the entire history of a given depot path just use + + git-p4 sync //path/in/depot@all + +To achieve optimal compression you may want to run 'git repack -a -d -f' after +a big import. This may take a while. + +Support for Perforce integrations is still work in progress. Don't bother +trying it unless you want to hack on it :) + +Incremental Imports +=================== + +After an initial import you can easily synchronize your git repository with +newer changes from the Perforce depot by just calling + + git-p4 sync + +in your git repository. By default the "remotes/p4/master" branch is updated. + +It is recommended to run 'git repack -a -d -f' from time to time when using +incremental imports to optimally combine the individual git packs that each +incremental import creates through the use of git-fast-import. + + +A useful setup may be that you have a periodically updated git repository +somewhere that contains a complete import of a Perforce project. That git +repository can be used to clone the working repository from and one would +import from Perforce directly after cloning using git-p4. If the connection to +the Perforce server is slow and the working repository hasn't been synced for a +while it may be desirable to fetch changes from the origin git repository using +the efficient git protocol. git-p4 supports this setup by calling "git fetch origin" +by default if there is an origin branch. You can disable this using + + git config git-p4.syncFromOrigin false + +Updating +======== + +A common working pattern is to fetch the latest changes from the Perforce depot +and merge them with local uncommitted changes. The recommended way is to use +git's rebase mechanism to preserve linear history. git-p4 provides a convenient + + git-p4 rebase + +command that calls git-p4 sync followed by git rebase to rebase the current +working branch. + +Submitting +========== + +git-p4 has support for submitting changes from a git repository back to the +Perforce depot. This requires a Perforce checkout separate to your git +repository. To submit all changes that are in the current git branch but not in +the "p4" branch (or "origin" if "p4" doesn't exist) simply call + + git-p4 submit + +in your git repository. If you want to submit changes in a specific branch that +is not your current git branch you can also pass that as an argument: + + git-p4 submit mytopicbranch + +You can override the reference branch with the --origin=mysourcebranch option. + +If a submit fails you may have to "p4 resolve" and submit manually. You can +continue importing the remaining changes with + + git-p4 submit --continue + +After submitting you should sync your perforce import branch ("p4" or "origin") +from Perforce using git-p4's sync command. + +If you have changes in your working directory that you haven't committed into +git yet but that you want to commit to Perforce directly ("quick fixes") then +you do not have to go through the intermediate step of creating a git commit +first but you can just call + + git-p4 submit --direct + + +Example +======= + +# Clone a repository + git-p4 clone //depot/path/project +# Enter the newly cloned directory + cd project +# Do some work... + vi foo.h +# ... and commit locally to gi + git commit foo.h +# In the meantime somebody submitted changes to the Perforce depot. Rebase your latest +# changes against the latest changes in Perforce: + git-p4 rebase +# Submit your locally committed changes back to Perforce + git-p4 submit +# ... and synchronize with Perforce + git-p4 rebase + + +Implementation Details... +========================= + +* Changesets from Perforce are imported using git fast-import. +* The import does not require anything from the Perforce client view as it just uses + "p4 print //depot/path/file#revision" to get the actual file contents. +* Every imported changeset has a special [git-p4...] line at the + end of the log message that gives information about the corresponding + Perforce change number and is also used by git-p4 itself to find out + where to continue importing when doing incremental imports. + Basically when syncing it extracts the perforce change number of the + latest commit in the "p4" branch and uses "p4 changes //depot/path/...@changenum,#head" + to find out which changes need to be imported. +* git-p4 submit uses "git rev-list" to pick the commits between the "p4" branch + and the current branch. + The commits themselves are applied using git diff/format-patch ... | git apply + diff --git a/contrib/workdir/git-new-workdir b/contrib/workdir/git-new-workdir index f2a3615bb..3ff6bd166 100755 --- a/contrib/workdir/git-new-workdir +++ b/contrib/workdir/git-new-workdir @@ -24,6 +24,11 @@ git_dir=$(cd "$orig_git" 2>/dev/null && git rev-parse --git-dir 2>/dev/null) || die "\"$orig_git\" is not a git repository!" +if test "$git_dir" = ".git" +then + git_dir="$orig_git/.git" +fi + # don't link to a workdir if test -L "$git_dir/config" then @@ -1813,6 +1813,11 @@ static void diff_fill_sha1_info(struct diff_filespec *one) hashclr(one->sha1); } +static int similarity_index(struct diff_filepair *p) +{ + return p->score * 100 / MAX_SCORE; +} + static void run_diff(struct diff_filepair *p, struct diff_options *o) { const char *pgm = external_diff(); @@ -1847,23 +1852,20 @@ static void run_diff(struct diff_filepair *p, struct diff_options *o) "similarity index %d%%\n" "copy from %s\n" "copy to %s\n", - (int)(0.5 + p->score * 100.0/MAX_SCORE), - name_munged, other_munged); + similarity_index(p), name_munged, other_munged); break; case DIFF_STATUS_RENAMED: len += snprintf(msg + len, sizeof(msg) - len, "similarity index %d%%\n" "rename from %s\n" "rename to %s\n", - (int)(0.5 + p->score * 100.0/MAX_SCORE), - name_munged, other_munged); + similarity_index(p), name_munged, other_munged); break; case DIFF_STATUS_MODIFIED: if (p->score) { len += snprintf(msg + len, sizeof(msg) - len, "dissimilarity index %d%%\n", - (int)(0.5 + p->score * - 100.0/MAX_SCORE)); + similarity_index(p)); complete_rewrite = 1; break; } @@ -2210,6 +2212,8 @@ int diff_opt_parse(struct diff_options *options, const char **av, int ac) } else if (!strcmp(arg, "--find-copies-harder")) options->find_copies_harder = 1; + else if (!strcmp(arg, "--follow")) + options->follow_renames = 1; else if (!strcmp(arg, "--abbrev")) options->abbrev = DEFAULT_ABBREV; else if (!prefixcmp(arg, "--abbrev=")) { @@ -2385,8 +2389,7 @@ static void diff_flush_raw(struct diff_filepair *p, } if (p->score) - sprintf(status, "%c%03d", p->status, - (int)(0.5 + p->score * 100.0/MAX_SCORE)); + sprintf(status, "%c%03d", p->status, similarity_index(p)); else { status[0] = p->status; status[1] = 0; @@ -2668,8 +2671,7 @@ static void show_rename_copy(const char *renamecopy, struct diff_filepair *p) { char *names = pprint_rename(p->one->path, p->two->path); - printf(" %s %s (%d%%)\n", renamecopy, names, - (int)(0.5 + p->score * 100.0/MAX_SCORE)); + printf(" %s %s (%d%%)\n", renamecopy, names, similarity_index(p)); free(names); show_mode_change(p, 0); } @@ -2693,7 +2695,7 @@ static void diff_summary(struct diff_filepair *p) if (p->score) { char *name = quote_one(p->two->path); printf(" rewrite %s (%d%%)\n", name, - (int)(0.5 + p->score * 100.0/MAX_SCORE)); + similarity_index(p)); free(name); show_mode_change(p, 0); } else show_mode_change(p, 1); @@ -3003,6 +3005,22 @@ void diffcore_std(struct diff_options *options) { if (options->quiet) return; + + /* + * break/rename count similarity differently depending on + * the binary-ness. + */ + if ((options->break_opt != -1) || (options->detect_rename)) { + struct diff_queue_struct *q = &diff_queued_diff; + int i; + + for (i = 0; i < q->nr; i++) { + struct diff_filepair *p = q->queue[i]; + p->one->is_binary = file_is_binary(p->one); + p->two->is_binary = file_is_binary(p->two); + } + } + if (options->break_opt != -1) diffcore_break(options->break_opt); if (options->detect_rename) @@ -55,6 +55,7 @@ struct diff_options { full_index:1, silent_on_remove:1, find_copies_harder:1, + follow_renames:1, color_diff:1, color_diff_words:1, has_changes:1, diff --git a/diffcore-break.c b/diffcore-break.c index 9c19b8cab..ae8a7d03e 100644 --- a/diffcore-break.c +++ b/diffcore-break.c @@ -66,8 +66,7 @@ static int should_break(struct diff_filespec *src, if (base_size < MINIMUM_BREAK_SIZE) return 0; /* we do not break too small filepair */ - if (diffcore_count_changes(src->data, src->size, - dst->data, dst->size, + if (diffcore_count_changes(src, dst, NULL, NULL, 0, &src_copied, &literal_added)) diff --git a/diffcore-delta.c b/diffcore-delta.c index 7338a40c5..a038b166c 100644 --- a/diffcore-delta.c +++ b/diffcore-delta.c @@ -5,23 +5,20 @@ /* * Idea here is very simple. * - * We have total of (sz-N+1) N-byte overlapping sequences in buf whose - * size is sz. If the same N-byte sequence appears in both source and - * destination, we say the byte that starts that sequence is shared - * between them (i.e. copied from source to destination). + * Almost all data we are interested in are text, but sometimes we have + * to deal with binary data. So we cut them into chunks delimited by + * LF byte, or 64-byte sequence, whichever comes first, and hash them. * - * For each possible N-byte sequence, if the source buffer has more - * instances of it than the destination buffer, that means the - * difference are the number of bytes not copied from source to - * destination. If the counts are the same, everything was copied - * from source to destination. If the destination has more, - * everything was copied, and destination added more. + * For those chunks, if the source buffer has more instances of it + * than the destination buffer, that means the difference are the + * number of bytes not copied from source to destination. If the + * counts are the same, everything was copied from source to + * destination. If the destination has more, everything was copied, + * and destination added more. * * We are doing an approximation so we do not really have to waste * memory by actually storing the sequence. We just hash them into * somewhere around 2^16 hashbuckets and count the occurrences. - * - * The length of the sequence is arbitrarily set to 8 for now. */ /* Wild guess at the initial hash size */ @@ -125,11 +122,14 @@ static struct spanhash_top *add_spanhash(struct spanhash_top *top, } } -static struct spanhash_top *hash_chars(unsigned char *buf, unsigned int sz) +static struct spanhash_top *hash_chars(struct diff_filespec *one) { int i, n; unsigned int accum1, accum2, hashval; struct spanhash_top *hash; + unsigned char *buf = one->data; + unsigned int sz = one->size; + int is_text = !one->is_binary; i = INITIAL_HASH_SIZE; hash = xmalloc(sizeof(*hash) + sizeof(struct spanhash) * (1<<i)); @@ -143,6 +143,11 @@ static struct spanhash_top *hash_chars(unsigned char *buf, unsigned int sz) unsigned int c = *buf++; unsigned int old_1 = accum1; sz--; + + /* Ignore CR in CRLF sequence if text */ + if (is_text && c == '\r' && sz && *buf == '\n') + continue; + accum1 = (accum1 << 7) ^ (accum2 >> 25); accum2 = (accum2 << 7) ^ (old_1 >> 25); accum1 += c; @@ -156,8 +161,8 @@ static struct spanhash_top *hash_chars(unsigned char *buf, unsigned int sz) return hash; } -int diffcore_count_changes(void *src, unsigned long src_size, - void *dst, unsigned long dst_size, +int diffcore_count_changes(struct diff_filespec *src, + struct diff_filespec *dst, void **src_count_p, void **dst_count_p, unsigned long delta_limit, @@ -172,14 +177,14 @@ int diffcore_count_changes(void *src, unsigned long src_size, if (src_count_p) src_count = *src_count_p; if (!src_count) { - src_count = hash_chars(src, src_size); + src_count = hash_chars(src); if (src_count_p) *src_count_p = src_count; } if (dst_count_p) dst_count = *dst_count_p; if (!dst_count) { - dst_count = hash_chars(dst, dst_size); + dst_count = hash_chars(dst); if (dst_count_p) *dst_count_p = dst_count; } diff --git a/diffcore-rename.c b/diffcore-rename.c index 93c40d9e0..6bde4396f 100644 --- a/diffcore-rename.c +++ b/diffcore-rename.c @@ -119,10 +119,26 @@ static int is_exact_match(struct diff_filespec *src, return 0; } +static int basename_same(struct diff_filespec *src, struct diff_filespec *dst) +{ + int src_len = strlen(src->path), dst_len = strlen(dst->path); + while (src_len && dst_len) { + char c1 = src->path[--src_len]; + char c2 = dst->path[--dst_len]; + if (c1 != c2) + return 0; + if (c1 == '/') + return 1; + } + return (!src_len || src->path[src_len - 1] == '/') && + (!dst_len || dst->path[dst_len - 1] == '/'); +} + struct diff_score { int src; /* index in rename_src */ int dst; /* index in rename_dst */ int score; + int name_score; }; static int estimate_similarity(struct diff_filespec *src, @@ -174,8 +190,7 @@ static int estimate_similarity(struct diff_filespec *src, delta_limit = (unsigned long) (base_size * (MAX_SCORE-minimum_score) / MAX_SCORE); - if (diffcore_count_changes(src->data, src->size, - dst->data, dst->size, + if (diffcore_count_changes(src, dst, &src->cnt_data, &dst->cnt_data, delta_limit, &src_copied, &literal_added)) @@ -224,6 +239,10 @@ static void record_rename_pair(int dst_index, int src_index, int score) static int score_compare(const void *a_, const void *b_) { const struct diff_score *a = a_, *b = b_; + + if (a->score == b->score) + return b->name_score - a->name_score; + return b->score - a->score; } @@ -295,9 +314,22 @@ void diffcore_rename(struct diff_options *options) if (rename_dst[i].pair) continue; /* dealt with an earlier round */ for (j = 0; j < rename_src_nr; j++) { + int k; struct diff_filespec *one = rename_src[j].one; if (!is_exact_match(one, two, contents_too)) continue; + + /* see if there is a basename match, too */ + for (k = j; k < rename_src_nr; k++) { + one = rename_src[k].one; + if (basename_same(one, two) && + is_exact_match(one, two, + contents_too)) { + j = k; + break; + } + } + record_rename_pair(i, j, (int)MAX_SCORE); rename_count++; break; /* we are done with this entry */ @@ -329,6 +361,7 @@ void diffcore_rename(struct diff_options *options) m->dst = i; m->score = estimate_similarity(one, two, minimum_score); + m->name_score = basename_same(one, two); diff_free_filespec_data(one); } /* We do not need the text anymore */ diff --git a/diffcore.h b/diffcore.h index 7b9294eab..0c8abb5b9 100644 --- a/diffcore.h +++ b/diffcore.h @@ -37,6 +37,7 @@ struct diff_filespec { #define DIFF_FILE_VALID(spec) (((spec)->mode) != 0) unsigned should_free : 1; /* data should be free()'ed */ unsigned should_munmap : 1; /* data should be munmap()'ed */ + unsigned is_binary : 1; /* data should be considered "binary" */ }; extern struct diff_filespec *alloc_filespec(const char *); @@ -103,8 +104,8 @@ void diff_debug_queue(const char *, struct diff_queue_struct *); #define diff_debug_queue(a,b) do {} while(0) #endif -extern int diffcore_count_changes(void *src, unsigned long src_size, - void *dst, unsigned long dst_size, +extern int diffcore_count_changes(struct diff_filespec *src, + struct diff_filespec *dst, void **src_count_p, void **dst_count_p, unsigned long delta_limit, @@ -275,7 +275,6 @@ static struct dir_entry *dir_entry_new(const char *pathname, int len) { struct dir_entry *ent; ent = xmalloc(sizeof(*ent) + len + 1); - ent->ignored = ent->ignored_dir = 0; ent->len = len; memcpy(ent->name, pathname, len); ent->name[len] = 0; @@ -287,10 +286,19 @@ struct dir_entry *dir_add_name(struct dir_struct *dir, const char *pathname, int if (cache_name_pos(pathname, len) >= 0) return NULL; - ALLOC_GROW(dir->entries, dir->nr, dir->alloc); + ALLOC_GROW(dir->entries, dir->nr+1, dir->alloc); return dir->entries[dir->nr++] = dir_entry_new(pathname, len); } +struct dir_entry *dir_add_ignored(struct dir_struct *dir, const char *pathname, int len) +{ + if (cache_name_pos(pathname, len) >= 0) + return NULL; + + ALLOC_GROW(dir->ignored, dir->ignored_nr+1, dir->ignored_alloc); + return dir->ignored[dir->ignored_nr++] = dir_entry_new(pathname, len); +} + enum exist_status { index_nonexistent = 0, index_directory, @@ -423,6 +431,18 @@ static int simplify_away(const char *path, int pathlen, const struct path_simpli return 0; } +static int in_pathspec(const char *path, int len, const struct path_simplify *simplify) +{ + if (simplify) { + for (; simplify->path; simplify++) { + if (len == simplify->len + && !memcmp(path, simplify->path, len)) + return 1; + } + } + return 0; +} + /* * Read a directory tree. We currently ignore anything but * directories, regular files and symlinks. That's because git @@ -463,6 +483,9 @@ static int read_directory_recursive(struct dir_struct *dir, const char *path, co continue; exclude = excluded(dir, fullname); + if (exclude && dir->collect_ignored + && in_pathspec(fullname, baselen + len, simplify)) + dir_add_ignored(dir, fullname, baselen + len); if (exclude != dir->show_ignored) { if (!dir->show_ignored || DTYPE(de) != DT_DIR) { continue; @@ -609,6 +632,7 @@ int read_directory(struct dir_struct *dir, const char *path, const char *base, i read_directory_recursive(dir, path, base, baselen, 0, simplify); free_simplify(simplify); qsort(dir->entries, dir->nr, sizeof(struct dir_entry *), cmp_name); + qsort(dir->ignored, dir->ignored_nr, sizeof(struct dir_entry *), cmp_name); return dir->nr; } @@ -13,9 +13,7 @@ struct dir_entry { - unsigned int ignored : 1; - unsigned int ignored_dir : 1; - unsigned int len : 30; + unsigned int len; char name[FLEX_ARRAY]; /* more */ }; @@ -31,11 +29,14 @@ struct exclude_list { struct dir_struct { int nr, alloc; + int ignored_nr, ignored_alloc; unsigned int show_ignored:1, show_other_directories:1, hide_empty_directories:1, - no_gitlinks:1; + no_gitlinks:1, + collect_ignored:1; struct dir_entry **entries; + struct dir_entry **ignored; /* Exclude info */ const char *exclude_per_dir; diff --git a/environment.c b/environment.c index 8b9b89d0a..1c2773f1b 100644 --- a/environment.c +++ b/environment.c @@ -12,6 +12,7 @@ char git_default_email[MAX_GITNAME]; char git_default_name[MAX_GITNAME]; int trust_executable_bit = 1; +int quote_path_fully = 1; int has_symlinks = 1; int assume_unchanged; int prefer_symlink_refs; @@ -46,6 +46,9 @@ static int process_tree(struct tree *tree) while (tree_entry(&desc, &entry)) { struct object *obj = NULL; + /* submodule commits are not stored in the superproject */ + if (S_ISGITLINK(entry.mode)) + continue; if (S_ISDIR(entry.mode)) { struct tree *tree = lookup_tree(entry.sha1); if (tree) diff --git a/git-clone.sh b/git-clone.sh index 3a410624d..4cbf60f55 100755 --- a/git-clone.sh +++ b/git-clone.sh @@ -72,6 +72,17 @@ Perhaps git-update-server-info needs to be run there?" rm -fr "$clone_tmp" http_fetch "$1/HEAD" "$GIT_DIR/REMOTE_HEAD" || rm -f "$GIT_DIR/REMOTE_HEAD" + if test -f "$GIT_DIR/REMOTE_HEAD"; then + head_sha1=`cat "$GIT_DIR/REMOTE_HEAD"` + case "$head_sha1" in + 'ref: refs/'*) + ;; + *) + git-http-fetch $v -a "$head_sha1" "$1" || + rm -f "$GIT_DIR/REMOTE_HEAD" + ;; + esac + fi } quiet= @@ -377,6 +388,13 @@ then ) ) + # Upstream URL + git-config remote."$origin".url "$repo" && + + # Set up the mappings to track the remote branches. + git-config remote."$origin".fetch \ + "+refs/heads/*:$remote_top/*" '^$' && + # Write out remote.$origin config, and update our "$head_points_at". case "$head_points_at" in ?*) @@ -384,21 +402,20 @@ then git-symbolic-ref HEAD "refs/heads/$head_points_at" && # Tracking branch for the primary branch at the remote. - origin_track="$remote_top/$head_points_at" && git-update-ref HEAD "$head_sha1" && - # Upstream URL - git-config remote."$origin".url "$repo" && - - # Set up the mappings to track the remote branches. - git-config remote."$origin".fetch \ - "+refs/heads/*:$remote_top/*" '^$' && rm -f "refs/remotes/$origin/HEAD" git-symbolic-ref "refs/remotes/$origin/HEAD" \ "refs/remotes/$origin/$head_points_at" && git-config branch."$head_points_at".remote "$origin" && git-config branch."$head_points_at".merge "refs/heads/$head_points_at" + ;; + '') + # Source had detached HEAD pointing nowhere + git-update-ref --no-deref HEAD "$head_sha1" && + rm -f "refs/remotes/$origin/HEAD" + ;; esac case "$no_checkout" in diff --git a/git-commit.sh b/git-commit.sh index 5547a0295..d43bdd87c 100755 --- a/git-commit.sh +++ b/git-commit.sh @@ -483,34 +483,8 @@ fi >>"$GIT_DIR"/COMMIT_EDITMSG # Author if test '' != "$use_commit" then - pick_author_script=' - /^author /{ - s/'\''/'\''\\'\'\''/g - h - s/^author \([^<]*\) <[^>]*> .*$/\1/ - s/'\''/'\''\'\'\''/g - s/.*/GIT_AUTHOR_NAME='\''&'\''/p - - g - s/^author [^<]* <\([^>]*\)> .*$/\1/ - s/'\''/'\''\'\'\''/g - s/.*/GIT_AUTHOR_EMAIL='\''&'\''/p - - g - s/^author [^<]* <[^>]*> \(.*\)$/\1/ - s/'\''/'\''\'\'\''/g - s/.*/GIT_AUTHOR_DATE='\''&'\''/p - - q - } - ' - encoding=$(git config i18n.commitencoding || echo UTF-8) - set_author_env=`git show -s --pretty=raw --encoding="$encoding" "$use_commit" | - LANG=C LC_ALL=C sed -ne "$pick_author_script"` - eval "$set_author_env" - export GIT_AUTHOR_NAME - export GIT_AUTHOR_EMAIL - export GIT_AUTHOR_DATE + eval "$(get_author_ident_from_commit "$use_commit")" + export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_AUTHOR_DATE fi if test '' != "$force_author" then diff --git a/git-compat-util.h b/git-compat-util.h index 6bd8987b2..362e040f5 100644 --- a/git-compat-util.h +++ b/git-compat-util.h @@ -167,6 +167,11 @@ extern size_t gitstrlcpy(char *, const char *, size_t); extern uintmax_t gitstrtoumax(const char *, char **, int); #endif +#ifdef NO_HSTRERROR +#define hstrerror githstrerror +extern const char *githstrerror(int herror); +#endif + extern void release_pack_memory(size_t, int); static inline char* xstrdup(const char *str) @@ -282,6 +287,22 @@ static inline ssize_t xwrite(int fd, const void *buf, size_t len) } } +static inline int xdup(int fd) +{ + int ret = dup(fd); + if (ret < 0) + die("dup failed: %s", strerror(errno)); + return ret; +} + +static inline FILE *xfdopen(int fd, const char *mode) +{ + FILE *stream = fdopen(fd, mode); + if (stream == NULL) + die("Out of memory? fdopen failed: %s", strerror(errno)); + return stream; +} + static inline size_t xsize_t(off_t len) { return (size_t)len; diff --git a/git-cvsimport.perl b/git-cvsimport.perl index 433b7fd32..ba23eb8ee 100755 --- a/git-cvsimport.perl +++ b/git-cvsimport.perl @@ -774,7 +774,6 @@ sub commit { or die "Cannot write branch $branch for update: $!\n"; if ($tag) { - my ($in, $out) = ('',''); my ($xtag) = $tag; $xtag =~ s/\s+\*\*.*$//; # Remove stuff like ** INVALID ** and ** FUNKY ** $xtag =~ tr/_/\./ if ( $opt_u ); @@ -1008,7 +1007,7 @@ if ($orig_branch) { if ($opt_r && $opt_o ne 'HEAD'); system('git-update-ref', 'HEAD', "$orig_branch"); unless ($opt_i) { - system('git checkout'); + system('git checkout -f'); die "checkout failed: $?\n" if $?; } } diff --git a/git-cvsserver.perl b/git-cvsserver.perl index d41b29f30..5cbf27eeb 100755 --- a/git-cvsserver.perl +++ b/git-cvsserver.perl @@ -22,6 +22,9 @@ use bytes; use Fcntl; use File::Temp qw/tempdir tempfile/; use File::Basename; +use Getopt::Long qw(:config require_order no_ignore_case); + +my $VERSION = '@@GIT_VERSION@@'; my $log = GITCVS::log->new(); my $cfg; @@ -85,15 +88,57 @@ my $methods = { my $state = { prependdir => '' }; $log->info("--------------- STARTING -----------------"); +my $usage = + "Usage: git-cvsserver [options] [pserver|server] [<directory> ...]\n". + " --base-path <path> : Prepend to requested CVSROOT\n". + " --strict-paths : Don't allow recursing into subdirectories\n". + " --export-all : Don't check for gitcvs.enabled in config\n". + " --version, -V : Print version information and exit\n". + " --help, -h, -H : Print usage information and exit\n". + "\n". + "<directory> ... is a list of allowed directories. If no directories\n". + "are given, all are allowed. This is an additional restriction, gitcvs\n". + "access still needs to be enabled by the gitcvs.enabled config option.\n"; + +my @opts = ( 'help|h|H', 'version|V', + 'base-path=s', 'strict-paths', 'export-all' ); +GetOptions( $state, @opts ) + or die $usage; + +if ($state->{version}) { + print "git-cvsserver version $VERSION\n"; + exit; +} +if ($state->{help}) { + print $usage; + exit; +} + my $TEMP_DIR = tempdir( CLEANUP => 1 ); $log->debug("Temporary directory is '$TEMP_DIR'"); +$state->{method} = 'ext'; +if (@ARGV) { + if ($ARGV[0] eq 'pserver') { + $state->{method} = 'pserver'; + shift @ARGV; + } elsif ($ARGV[0] eq 'server') { + shift @ARGV; + } +} + +# everything else is a directory +$state->{allowed_roots} = [ @ARGV ]; + +# don't export the whole system unless the users requests it +if ($state->{'export-all'} && !@{$state->{allowed_roots}}) { + die "--export-all can only be used together with an explicit whitelist\n"; +} + # if we are called with a pserver argument, # deal with the authentication cat before entering the # main loop -$state->{method} = 'ext'; -if (@ARGV && $ARGV[0] eq 'pserver') { - $state->{method} = 'pserver'; +if ($state->{method} eq 'pserver') { my $line = <STDIN>; chomp $line; unless( $line =~ /^BEGIN (AUTH|VERIFICATION) REQUEST$/) { die "E Do not understand $line - expecting BEGIN AUTH REQUEST\n"; @@ -172,19 +217,48 @@ sub req_Root return 0; } + my $cvsroot = $state->{'base-path'} || ''; + $cvsroot =~ s#/+$##; + $cvsroot .= $data; + if ($state->{CVSROOT} - && ($state->{CVSROOT} ne $data)) { + && ($state->{CVSROOT} ne $cvsroot)) { print "error 1 Conflicting roots specified\n"; return 0; } - $state->{CVSROOT} = $data; + $state->{CVSROOT} = $cvsroot; $ENV{GIT_DIR} = $state->{CVSROOT} . "/"; + + if (@{$state->{allowed_roots}}) { + my $allowed = 0; + foreach my $dir (@{$state->{allowed_roots}}) { + next unless $dir =~ m#^/#; + $dir =~ s#/+$##; + if ($state->{'strict-paths'}) { + if ($ENV{GIT_DIR} =~ m#^\Q$dir\E/?$#) { + $allowed = 1; + last; + } + } elsif ($ENV{GIT_DIR} =~ m#^\Q$dir\E(/?$|/)#) { + $allowed = 1; + last; + } + } + + unless ($allowed) { + print "E $ENV{GIT_DIR} does not seem to be a valid GIT repository\n"; + print "E \n"; + print "error 1 $ENV{GIT_DIR} is not a valid repository\n"; + return 0; + } + } + unless (-d $ENV{GIT_DIR} && -e $ENV{GIT_DIR}.'HEAD') { print "E $ENV{GIT_DIR} does not seem to be a valid GIT repository\n"; - print "E \n"; - print "error 1 $ENV{GIT_DIR} is not a valid repository\n"; + print "E \n"; + print "error 1 $ENV{GIT_DIR} is not a valid repository\n"; return 0; } @@ -207,7 +281,8 @@ sub req_Root my $enabled = ($cfg->{gitcvs}{$state->{method}}{enabled} || $cfg->{gitcvs}{enabled}); - unless ($enabled && $enabled =~ /^\s*(1|true|yes)\s*$/i) { + unless ($state->{'export-all'} || + ($enabled && $enabled =~ /^\s*(1|true|yes)\s*$/i)) { print "E GITCVS emulation needs to be enabled on this repo\n"; print "E the repo config file needs a [gitcvs] section added, and the parameter 'enabled' set to 1\n"; print "E \n"; diff --git a/git-filter-branch.sh b/git-filter-branch.sh new file mode 100644 index 000000000..a2fcebc1c --- /dev/null +++ b/git-filter-branch.sh @@ -0,0 +1,442 @@ +#!/bin/sh +# +# Rewrite revision history +# Copyright (c) Petr Baudis, 2006 +# Minimal changes to "port" it to core-git (c) Johannes Schindelin, 2007 +# +# Lets you rewrite GIT revision history by creating a new branch from +# your current branch by applying custom filters on each revision. +# Those filters can modify each tree (e.g. removing a file or running +# a perl rewrite on all files) or information about each commit. +# Otherwise, all information (including original commit times or merge +# information) will be preserved. +# +# The command takes the new branch name as a mandatory argument and +# the filters as optional arguments. If you specify no filters, the +# commits will be recommitted without any changes, which would normally +# have no effect and result with the new branch pointing to the same +# branch as your current branch. (Nevertheless, this may be useful in +# the future for compensating for some Git bugs or such, therefore +# such a usage is permitted.) +# +# WARNING! The rewritten history will have different ids for all the +# objects and will not converge with the original branch. You will not +# be able to easily push and distribute the rewritten branch. Please do +# not use this command if you do not know the full implications, and +# avoid using it anyway - do not do what a simple single commit on top +# of the current version would fix. +# +# Always verify that the rewritten version is correct before disposing +# the original branch. +# +# Note that since this operation is extensively I/O expensive, it might +# be a good idea to do it off-disk, e.g. on tmpfs. Reportedly the speedup +# is very noticeable. +# +# OPTIONS +# ------- +# -d TEMPDIR:: The path to the temporary tree used for rewriting +# When applying a tree filter, the command needs to temporary +# checkout the tree to some directory, which may consume +# considerable space in case of large projects. By default it +# does this in the '.git-rewrite/' directory but you can override +# that choice by this parameter. +# +# Filters +# ~~~~~~~ +# The filters are applied in the order as listed below. The COMMAND +# argument is always evaluated in shell using the 'eval' command. +# The $GIT_COMMIT environment variable is permanently set to contain +# the id of the commit being rewritten. The author/committer environment +# variables are set before the first filter is run. +# +# A 'map' function is available that takes an "original sha1 id" argument +# and outputs a "rewritten sha1 id" if the commit has been already +# rewritten, fails otherwise; the 'map' function can return several +# ids on separate lines if your commit filter emitted multiple commits +# (see below). +# +# --env-filter COMMAND:: The filter for modifying environment +# This is the filter for modifying the environment in which +# the commit will be performed. Specifically, you might want +# to rewrite the author/committer name/email/time environment +# variables (see `git-commit` for details). Do not forget to +# re-export the variables. +# +# --tree-filter COMMAND:: The filter for rewriting tree (and its contents) +# This is the filter for rewriting the tree and its contents. +# The COMMAND argument is evaluated in shell with the working +# directory set to the root of the checked out tree. The new tree +# is then used as-is (new files are auto-added, disappeared files +# are auto-removed - .gitignore files nor any other ignore rules +# HAVE NO EFFECT!). +# +# --index-filter COMMAND:: The filter for rewriting index +# This is the filter for rewriting the Git's directory index. +# It is similar to the tree filter but does not check out the +# tree, which makes it much faster. However, you must use the +# lowlevel Git index manipulation commands to do your work. +# +# --parent-filter COMMAND:: The filter for rewriting parents +# This is the filter for rewriting the commit's parent list. +# It will receive the parent string on stdin and shall output +# the new parent string on stdout. The parent string is in +# format accepted by `git-commit-tree`: empty for initial +# commit, "-p parent" for a normal commit and "-p parent1 +# -p parent2 -p parent3 ..." for a merge commit. +# +# --msg-filter COMMAND:: The filter for rewriting commit message +# This is the filter for rewriting the commit messages. +# The COMMAND argument is evaluated in shell with the original +# commit message on standard input; its standard output is +# is used as the new commit message. +# +# --commit-filter COMMAND:: The filter for performing the commit +# If this filter is passed, it will be called instead of the +# `git-commit-tree` command, with those arguments: +# +# TREE_ID [-p PARENT_COMMIT_ID]... +# +# and the log message on stdin. The commit id is expected on +# stdout. As a special extension, the commit filter may emit +# multiple commit ids; in that case, all of them will be used +# as parents instead of the original commit in further commits. +# +# --tag-name-filter COMMAND:: The filter for rewriting tag names. +# If this filter is passed, it will be called for every tag ref +# that points to a rewritten object (or to a tag object which +# points to a rewritten object). The original tag name is passed +# via standard input, and the new tag name is expected on standard +# output. +# +# The original tags are not deleted, but can be overwritten; +# use "--tag-name-filter=cat" to simply update the tags. In this +# case, be very careful and make sure you have the old tags +# backed up in case the conversion has run afoul. +# +# Note that there is currently no support for proper rewriting of +# tag objects; in layman terms, if the tag has a message or signature +# attached, the rewritten tag won't have it. Sorry. (It is by +# definition impossible to preserve signatures at any rate, though.) +# +# --subdirectory-filter DIRECTORY:: Only regard the history, as seen by +# the given subdirectory. The result will contain that directory as +# its project root. +# +# EXAMPLE USAGE +# ------------- +# Suppose you want to remove a file (containing confidential information +# or copyright violation) from all commits: +# +# git-filter-branch --tree-filter 'rm filename' newbranch +# +# A significantly faster version: +# +# git-filter-branch --index-filter 'git-update-index --remove filename' newbranch +# +# Now, you will get the rewritten history saved in the branch 'newbranch' +# (your current branch is left untouched). +# +# To "etch-graft" a commit to the revision history (set a commit to be +# the parent of the current initial commit and propagate that): +# +# git-filter-branch --parent-filter sed\ 's/^$/-p graftcommitid/' newbranch +# +# (if the parent string is empty - therefore we are dealing with the +# initial commit - add graftcommit as a parent). Note that this assumes +# history with a single root (that is, no git-merge without common ancestors +# happened). If this is not the case, use: +# +# git-filter-branch --parent-filter 'cat; [ "$GIT_COMMIT" = "COMMIT" ] && echo "-p GRAFTCOMMIT"' newbranch +# +# To remove commits authored by "Darl McBribe" from the history: +# +# git-filter-branch --commit-filter 'if [ "$GIT_AUTHOR_NAME" = "Darl McBribe" ]; then shift; while [ -n "$1" ]; do shift; echo "$1"; shift; done; else git-commit-tree "$@"; fi' newbranch +# +# (the shift magic first throws away the tree id and then the -p +# parameters). Note that this handles merges properly! In case Darl +# committed a merge between P1 and P2, it will be propagated properly +# and all children of the merge will become merge commits with P1,P2 +# as their parents instead of the merge commit. +# +# To restrict rewriting to only part of the history, specify a revision +# range in addition to the new branch name. The new branch name will +# point to the top-most revision that a 'git rev-list' of this range +# will print. +# +# Consider this history: +# +# D--E--F--G--H +# / / +# A--B-----C +# +# To rewrite commits D,E,F,G,H, use: +# +# git-filter-branch ... new-H C..H +# +# To rewrite commits E,F,G,H, use one of these: +# +# git-filter-branch ... new-H C..H --not D +# git-filter-branch ... new-H D..H --not C +# +# To move the whole tree into a subdirectory, or remove it from there: +# +# git-filter-branch --index-filter \ +# 'git-ls-files -s | sed "s-\t-&newsubdir/-" | +# GIT_INDEX_FILE=$GIT_INDEX_FILE.new \ +# git-update-index --index-info && +# mv $GIT_INDEX_FILE.new $GIT_INDEX_FILE' directorymoved + +# Testsuite: TODO + +set -e + +USAGE="git-filter-branch [-d TEMPDIR] [FILTERS] DESTBRANCH [REV-RANGE]" +. git-sh-setup + +map() +{ + # if it was not rewritten, take the original + test -r "$workdir/../map/$1" || echo "$1" + cat "$workdir/../map/$1" +} + +# When piped a commit, output a script to set the ident of either +# "author" or "committer + +set_ident () { + lid="$(echo "$1" | tr "A-Z" "a-z")" + uid="$(echo "$1" | tr "a-z" "A-Z")" + pick_id_script=' + /^'$lid' /{ + s/'\''/'\''\\'\'\''/g + h + s/^'$lid' \([^<]*\) <[^>]*> .*$/\1/ + s/'\''/'\''\'\'\''/g + s/.*/export GIT_'$uid'_NAME='\''&'\''/p + + g + s/^'$lid' [^<]* <\([^>]*\)> .*$/\1/ + s/'\''/'\''\'\'\''/g + s/.*/export GIT_'$uid'_EMAIL='\''&'\''/p + + g + s/^'$lid' [^<]* <[^>]*> \(.*\)$/\1/ + s/'\''/'\''\'\'\''/g + s/.*/export GIT_'$uid'_DATE='\''&'\''/p + + q + } + ' + + LANG=C LC_ALL=C sed -ne "$pick_id_script" + # Ensure non-empty id name. + echo "[ -n \"\$GIT_${uid}_NAME\" ] || export GIT_${uid}_NAME=\"\${GIT_${uid}_EMAIL%%@*}\"" +} + +tempdir=.git-rewrite +filter_env= +filter_tree= +filter_index= +filter_parent= +filter_msg=cat +filter_commit='git-commit-tree "$@"' +filter_tag_name= +filter_subdir= +while case "$#" in 0) usage;; esac +do + case "$1" in + --) + shift + break + ;; + -*) + ;; + *) + break; + esac + + # all switches take one argument + ARG="$1" + case "$#" in 1) usage ;; esac + shift + OPTARG="$1" + shift + + case "$ARG" in + -d) + tempdir="$OPTARG" + ;; + --env-filter) + filter_env="$OPTARG" + ;; + --tree-filter) + filter_tree="$OPTARG" + ;; + --index-filter) + filter_index="$OPTARG" + ;; + --parent-filter) + filter_parent="$OPTARG" + ;; + --msg-filter) + filter_msg="$OPTARG" + ;; + --commit-filter) + filter_commit="$OPTARG" + ;; + --tag-name-filter) + filter_tag_name="$OPTARG" + ;; + --subdirectory-filter) + filter_subdir="$OPTARG" + ;; + *) + usage + ;; + esac +done + +dstbranch="$1" +shift +test -n "$dstbranch" || die "missing branch name" +git-show-ref "refs/heads/$dstbranch" 2> /dev/null && + die "branch $dstbranch already exists" + +test ! -e "$tempdir" || die "$tempdir already exists, please remove it" +mkdir -p "$tempdir/t" +cd "$tempdir/t" +workdir="$(pwd)" + +case "$GIT_DIR" in +/*) + ;; +*) + GIT_DIR="$(pwd)/../../$GIT_DIR" + ;; +esac +export GIT_DIR GIT_WORK_TREE=. + +export GIT_INDEX_FILE="$(pwd)/../index" +git-read-tree # seed the index file + +ret=0 + + +mkdir ../map # map old->new commit ids for rewriting parents + +case "$filter_subdir" in +"") + git-rev-list --reverse --topo-order --default HEAD \ + --parents "$@" + ;; +*) + git-rev-list --reverse --topo-order --default HEAD \ + --parents --full-history "$@" -- "$filter_subdir" +esac > ../revs +commits=$(cat ../revs | wc -l | tr -d " ") + +test $commits -eq 0 && die "Found nothing to rewrite" + +i=0 +while read commit parents; do + i=$(($i+1)) + printf "$commit ($i/$commits) " + + case "$filter_subdir" in + "") + git-read-tree -i -m $commit + ;; + *) + git-read-tree -i -m $commit:"$filter_subdir" + esac + + export GIT_COMMIT=$commit + git-cat-file commit "$commit" >../commit + + eval "$(set_ident AUTHOR <../commit)" + eval "$(set_ident COMMITTER <../commit)" + eval "$filter_env" < /dev/null + + if [ "$filter_tree" ]; then + git-checkout-index -f -u -a + # files that $commit removed are now still in the working tree; + # remove them, else they would be added again + git-ls-files -z --others | xargs -0 rm -f + eval "$filter_tree" < /dev/null + git-diff-index -r $commit | cut -f 2- | tr '\n' '\0' | \ + xargs -0 git-update-index --add --replace --remove + git-ls-files -z --others | \ + xargs -0 git-update-index --add --replace --remove + fi + + eval "$filter_index" < /dev/null + + parentstr= + for parent in $parents; do + for reparent in $(map "$parent"); do + parentstr="$parentstr -p $reparent" + done + done + if [ "$filter_parent" ]; then + parentstr="$(echo "$parentstr" | eval "$filter_parent")" + fi + + sed -e '1,/^$/d' <../commit | \ + eval "$filter_msg" | \ + sh -c "$filter_commit" git-commit-tree $(git-write-tree) $parentstr | \ + tee ../map/$commit +done <../revs + +src_head=$(tail -n 1 ../revs | sed -e 's/ .*//') +target_head=$(head -n 1 ../map/$src_head) +case "$target_head" in +'') + echo Nothing rewritten + ;; +*) + git-update-ref refs/heads/"$dstbranch" $target_head + if [ $(cat ../map/$src_head | wc -l) -gt 1 ]; then + echo "WARNING: Your commit filter caused the head commit to expand to several rewritten commits. Only the first such commit was recorded as the current $dstbranch head but you will need to resolve the situation now (probably by manually merging the other commits). These are all the commits:" >&2 + sed 's/^/ /' ../map/$src_head >&2 + ret=1 + fi + ;; +esac + +if [ "$filter_tag_name" ]; then + git-for-each-ref --format='%(objectname) %(objecttype) %(refname)' refs/tags | + while read sha1 type ref; do + ref="${ref#refs/tags/}" + # XXX: Rewrite tagged trees as well? + if [ "$type" != "commit" -a "$type" != "tag" ]; then + continue; + fi + + if [ "$type" = "tag" ]; then + # Dereference to a commit + sha1t="$sha1" + sha1="$(git-rev-parse "$sha1"^{commit} 2>/dev/null)" || continue + fi + + [ -f "../map/$sha1" ] || continue + new_sha1="$(cat "../map/$sha1")" + export GIT_COMMIT="$sha1" + new_ref="$(echo "$ref" | eval "$filter_tag_name")" + + echo "$ref -> $new_ref ($sha1 -> $new_sha1)" + + if [ "$type" = "tag" ]; then + # Warn that we are not rewriting the tag object itself. + warn "unreferencing tag object $sha1t" + fi + + git-update-ref "refs/tags/$new_ref" "$new_sha1" + done +fi + +cd ../.. +rm -rf "$tempdir" +echo "Rewritten history saved to the $dstbranch branch" + +exit $ret diff --git a/git-gui/Makefile b/git-gui/Makefile index 3de0de1a2..1bac6fed4 100644 --- a/git-gui/Makefile +++ b/git-gui/Makefile @@ -7,6 +7,8 @@ GIT-VERSION-FILE: .FORCE-GIT-VERSION-FILE @$(SHELL_PATH) ./GIT-VERSION-GEN -include GIT-VERSION-FILE +uname_O := $(shell sh -c 'uname -o 2>/dev/null || echo not') + SCRIPT_SH = git-gui.sh GITGUI_BUILT_INS = git-citool ALL_PROGRAMS = $(GITGUI_BUILT_INS) $(patsubst %.sh,%,$(SCRIPT_SH)) @@ -29,11 +31,35 @@ ifndef INSTALL INSTALL = install endif +INSTALL_D0 = $(INSTALL) -d -m755 # space is required here +INSTALL_D1 = +INSTALL_R0 = $(INSTALL) -m644 # space is required here +INSTALL_R1 = +INSTALL_X0 = $(INSTALL) -m755 # space is required here +INSTALL_X1 = +INSTALL_L0 = rm -f # space is required here +INSTALL_L1 = && ln # space is required here +INSTALL_L2 = +INSTALL_L3 = + ifndef V - QUIET_GEN = @echo ' ' GEN $@; - QUIET_BUILT_IN = @echo ' ' BUILTIN $@; - QUIET_INDEX = @echo ' ' INDEX $(dir $@); + QUIET = @ + QUIET_GEN = $(QUIET)echo ' ' GEN $@ && + QUIET_BUILT_IN = $(QUIET)echo ' ' BUILTIN $@ && + QUIET_INDEX = $(QUIET)echo ' ' INDEX $(dir $@) && QUIET_2DEVNULL = 2>/dev/null + + INSTALL_D0 = dir= + INSTALL_D1 = && echo ' ' DEST $$dir && $(INSTALL) -d -m755 "$$dir" + INSTALL_R0 = src= + INSTALL_R1 = && echo ' ' INSTALL 644 `basename $$src` && $(INSTALL) -m644 $$src + INSTALL_X0 = src= + INSTALL_X1 = && echo ' ' INSTALL 755 `basename $$src` && $(INSTALL) -m755 $$src + + INSTALL_L0 = dst= + INSTALL_L1 = && src= + INSTALL_L2 = && dst= + INSTALL_L3 = && echo ' ' 'LINK ' `basename "$$dst"` '->' `basename "$$src"` && rm -f "$$dst" && ln "$$src" "$$dst" endif TCL_PATH ?= tclsh @@ -58,11 +84,15 @@ exedir_SQ = $(subst ','\'',$(exedir)) $(patsubst %.sh,%,$(SCRIPT_SH)) : % : %.sh $(QUIET_GEN)rm -f $@ $@+ && \ + GITGUI_RELATIVE= && \ if test '$(exedir_SQ)' = '$(libdir_SQ)'; then \ - GITGUI_RELATIVE=1; \ + if test "$(uname_O)" = Cygwin; \ + then GITGUI_RELATIVE= ; \ + else GITGUI_RELATIVE=1; \ + fi; \ fi && \ sed -e '1s|#!.*/sh|#!$(SHELL_PATH_SQ)|' \ - -e 's|^exec wish "$$0"|exec $(subst |,'\|',$(TCLTK_PATH_SQ)) "$$0"|' \ + -e 's|^ exec wish "$$0"| exec $(subst |,'\|',$(TCLTK_PATH_SQ)) "$$0"|' \ -e 's/@@GITGUI_VERSION@@/$(GITGUI_VERSION)/g' \ -e 's|@@GITGUI_RELATIVE@@|'$$GITGUI_RELATIVE'|' \ -e $$GITGUI_RELATIVE's|@@GITGUI_LIBDIR@@|$(libdir_SQ)|' \ @@ -109,12 +139,12 @@ GIT-GUI-VARS: .FORCE-GIT-GUI-VARS all:: $(ALL_PROGRAMS) lib/tclIndex install: all - $(INSTALL) -d -m755 '$(DESTDIR_SQ)$(gitexecdir_SQ)' - $(INSTALL) git-gui '$(DESTDIR_SQ)$(gitexecdir_SQ)' - $(foreach p,$(GITGUI_BUILT_INS), rm -f '$(DESTDIR_SQ)$(gitexecdir_SQ)/$p' && ln '$(DESTDIR_SQ)$(gitexecdir_SQ)/git-gui' '$(DESTDIR_SQ)$(gitexecdir_SQ)/$p' ;) - $(INSTALL) -d -m755 '$(DESTDIR_SQ)$(libdir_SQ)' - $(INSTALL) -m644 lib/tclIndex '$(DESTDIR_SQ)$(libdir_SQ)' - $(foreach p,$(ALL_LIBFILES), $(INSTALL) -m644 $p '$(DESTDIR_SQ)$(libdir_SQ)' ;) + $(QUIET)$(INSTALL_D0)'$(DESTDIR_SQ)$(gitexecdir_SQ)' $(INSTALL_D1) + $(QUIET)$(INSTALL_X0)git-gui $(INSTALL_X1) '$(DESTDIR_SQ)$(gitexecdir_SQ)' + $(QUIET)$(foreach p,$(GITGUI_BUILT_INS), $(INSTALL_L0)'$(DESTDIR_SQ)$(gitexecdir_SQ)/$p' $(INSTALL_L1)'$(DESTDIR_SQ)$(gitexecdir_SQ)/git-gui' $(INSTALL_L2)'$(DESTDIR_SQ)$(gitexecdir_SQ)/$p' $(INSTALL_L3) &&) true + $(QUIET)$(INSTALL_D0)'$(DESTDIR_SQ)$(libdir_SQ)' $(INSTALL_D1) + $(QUIET)$(INSTALL_R0)lib/tclIndex $(INSTALL_R1) '$(DESTDIR_SQ)$(libdir_SQ)' + $(QUIET)$(foreach p,$(ALL_LIBFILES), $(INSTALL_R0)$p $(INSTALL_R1) '$(DESTDIR_SQ)$(libdir_SQ)' &&) true dist-version: @mkdir -p $(TARDIR) diff --git a/git-gui/git-gui.sh b/git-gui/git-gui.sh index 3237f3d59..9df2e4702 100755 --- a/git-gui/git-gui.sh +++ b/git-gui/git-gui.sh @@ -1,6 +1,12 @@ #!/bin/sh # Tcl ignores the next line -*- tcl -*- \ -exec wish "$0" -- "$@" + if test "z$*" = zversion \ + || test "z$*" = z--version; \ + then \ + echo 'git-gui version @@GITGUI_VERSION@@'; \ + exit; \ + fi; \ + exec wish "$0" -- "$@" set appvers {@@GITGUI_VERSION@@} set copyright { @@ -302,11 +308,6 @@ proc tk_optionMenu {w varName args} { ## ## version check -if {{--version} eq $argv || {version} eq $argv} { - puts "git-gui version $appvers" - exit -} - set req_maj 1 set req_min 5 @@ -1580,8 +1581,7 @@ if {[is_MacOSX]} { # -- Tools Menu # - if {[file exists /usr/local/miga/lib/gui-miga] - && [file exists .pvcsrc]} { + if {[is_Cygwin] && [file exists /usr/local/miga/lib/gui-miga]} { proc do_miga {} { global ui_status_value if {![lock_index update]} return diff --git a/git-gui/lib/blame.tcl b/git-gui/lib/blame.tcl index 139171d39..b52365481 100644 --- a/git-gui/lib/blame.tcl +++ b/git-gui/lib/blame.tcl @@ -272,6 +272,8 @@ constructor new {i_commit i_path} { set cursorW %W tk_popup $w.ctxm %X %Y " + bind $i <Shift-Tab> "[list focus $w_cviewer];break" + bind $i <Tab> "[list focus $w_cviewer];break" } foreach i [concat $w_columns $w_cviewer] { @@ -287,8 +289,10 @@ constructor new {i_commit i_path} { bind $i <Control-Key-f> {catch {%W yview scroll 1 pages};break} } + bind $w_cviewer <Shift-Tab> "[list focus $w_file];break" + bind $w_cviewer <Tab> "[list focus $w_file];break" bind $w_cviewer <Button-1> [list focus $w_cviewer] - bind $top <Visibility> [list focus $top] + bind $w_file <Visibility> [list focus $w_file] grid configure $w.header -sticky ew grid configure $w.file_pane -sticky nsew @@ -483,7 +487,11 @@ method _read_file {fd jump} { } ifdeleted { catch {close $fd} } method _exec_blame {cur_w cur_d options cur_s} { - set cmd [list nice git blame] + set cmd [list] + if {![is_Windows] || [is_Cygwin]} { + lappend cmd nice + } + lappend cmd git blame set cmd [concat $cmd $options] lappend cmd --incremental if {$commit eq {}} { diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh new file mode 100755 index 000000000..0c2a9697c --- /dev/null +++ b/git-rebase--interactive.sh @@ -0,0 +1,410 @@ +#!/bin/sh +# +# Copyright (c) 2006 Johannes E. Schindelin + +# SHORT DESCRIPTION +# +# This script makes it easy to fix up commits in the middle of a series, +# and rearrange commits. +# +# The original idea comes from Eric W. Biederman, in +# http://article.gmane.org/gmane.comp.version-control.git/22407 + +USAGE='(--continue | --abort | --skip | [--preserve-merges] [--verbose] + [--onto <branch>] <upstream> [<branch>])' + +. git-sh-setup +require_work_tree + +DOTEST="$GIT_DIR/.dotest-merge" +TODO="$DOTEST"/todo +DONE="$DOTEST"/done +REWRITTEN="$DOTEST"/rewritten +PRESERVE_MERGES= +STRATEGY= +VERBOSE= + +warn () { + echo "$*" >&2 +} + +require_clean_work_tree () { + # test if working tree is dirty + git rev-parse --verify HEAD > /dev/null && + git update-index --refresh && + git diff-files --quiet && + git diff-index --cached --quiet HEAD || + die "Working tree is dirty" +} + +ORIG_REFLOG_ACTION="$GIT_REFLOG_ACTION" + +comment_for_reflog () { + case "$ORIG_REFLOG_ACTION" in + ''|rebase*) + GIT_REFLOG_ACTION="rebase -i ($1)" + export GIT_REFLOG_ACTION + esac +} + +mark_action_done () { + sed -e 1q < "$TODO" >> "$DONE" + sed -e 1d < "$TODO" >> "$TODO".new + mv -f "$TODO".new "$TODO" +} + +make_patch () { + parent_sha1=$(git rev-parse --verify "$1"^ 2> /dev/null) + git diff "$parent_sha1".."$1" > "$DOTEST"/patch +} + +die_with_patch () { + make_patch "$1" + die "$2" +} + +die_abort () { + rm -rf "$DOTEST" + die "$1" +} + +pick_one () { + case "$1" in -n) sha1=$2 ;; *) sha1=$1 ;; esac + git rev-parse --verify $sha1 || die "Invalid commit name: $sha1" + test -d "$REWRITTEN" && + pick_one_preserving_merges "$@" && return + parent_sha1=$(git rev-parse --verify $sha1^ 2>/dev/null) + current_sha1=$(git rev-parse --verify HEAD) + if [ $current_sha1 = $parent_sha1 ]; then + git reset --hard $sha1 + sha1=$(git rev-parse --short $sha1) + warn Fast forward to $sha1 + else + git cherry-pick $STRATEGY "$@" + fi +} + +pick_one_preserving_merges () { + case "$1" in -n) sha1=$2 ;; *) sha1=$1 ;; esac + sha1=$(git rev-parse $sha1) + + if [ -f "$DOTEST"/current-commit ] + then + current_commit=$(cat "$DOTEST"/current-commit) && + git rev-parse HEAD > "$REWRITTEN"/$current_commit && + rm "$DOTEST"/current-commit || + die "Cannot write current commit's replacement sha1" + fi + + # rewrite parents; if none were rewritten, we can fast-forward. + fast_forward=t + preserve=t + new_parents= + for p in $(git rev-list --parents -1 $sha1 | cut -d\ -f2-) + do + if [ -f "$REWRITTEN"/$p ] + then + preserve=f + new_p=$(cat "$REWRITTEN"/$p) + test $p != $new_p && fast_forward=f + case "$new_parents" in + *$new_p*) + ;; # do nothing; that parent is already there + *) + new_parents="$new_parents $new_p" + esac + fi + done + case $fast_forward in + t) + echo "Fast forward to $sha1" + test $preserve=f && echo $sha1 > "$REWRITTEN"/$sha1 + ;; + f) + test "a$1" = a-n && die "Refusing to squash a merge: $sha1" + + first_parent=$(expr "$new_parents" : " \([^ ]*\)") + # detach HEAD to current parent + git checkout $first_parent 2> /dev/null || + die "Cannot move HEAD to $first_parent" + + echo $sha1 > "$DOTEST"/current-commit + case "$new_parents" in + \ *\ *) + # redo merge + author_script=$(get_author_ident_from_commit $sha1) + eval "$author_script" + msg="$(git cat-file commit $sha1 | \ + sed -e '1,/^$/d' -e "s/[\"\\]/\\\\&/g")" + # NEEDSWORK: give rerere a chance + if ! git merge $STRATEGY -m "$msg" $new_parents + then + echo "$msg" > "$GIT_DIR"/MERGE_MSG + warn Error redoing merge $sha1 + warn + warn After fixup, please use + die "$author_script git commit" + fi + ;; + *) + git cherry-pick $STRATEGY "$@" || + die_with_patch $sha1 "Could not pick $sha1" + esac + esac +} + +do_next () { + read command sha1 rest < "$TODO" + case "$command" in + \#|'') + mark_action_done + continue + ;; + pick) + comment_for_reflog pick + + mark_action_done + pick_one $sha1 || + die_with_patch $sha1 "Could not apply $sha1... $rest" + ;; + edit) + comment_for_reflog edit + + mark_action_done + pick_one $sha1 || + die_with_patch $sha1 "Could not apply $sha1... $rest" + make_patch $sha1 + warn + warn "You can amend the commit now, with" + warn + warn " git commit --amend" + warn + exit 0 + ;; + squash) + comment_for_reflog squash + + test -z "$(grep -ve '^$' -e '^#' < $DONE)" && + die "Cannot 'squash' without a previous commit" + + mark_action_done + failed=f + pick_one -n $sha1 || failed=t + MSG="$DOTEST"/message + echo "# This is a combination of two commits." > "$MSG" + echo "# The first commit's message is:" >> "$MSG" + echo >> "$MSG" + git cat-file commit HEAD | sed -e '1,/^$/d' >> "$MSG" + echo >> "$MSG" + echo "# And this is the 2nd commit message:" >> "$MSG" + echo >> "$MSG" + git cat-file commit $sha1 | sed -e '1,/^$/d' >> "$MSG" + git reset --soft HEAD^ + author_script=$(get_author_ident_from_commit $sha1) + case $failed in + f) + # This is like --amend, but with a different message + eval "$author_script" + export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_AUTHOR_DATE + git commit -F "$MSG" -e + ;; + t) + cp "$MSG" "$GIT_DIR"/MERGE_MSG + warn + warn "Could not apply $sha1... $rest" + warn "After you fixed that, commit the result with" + warn + warn " $(echo $author_script | tr '\012' ' ') \\" + warn " git commit -F \"$GIT_DIR\"/MERGE_MSG -e" + die_with_patch $sha1 "" + esac + ;; + *) + warn "Unknown command: $command $sha1 $rest" + die_with_patch $sha1 "Please fix this in the file $TODO." + esac + test -s "$TODO" && return + + comment_for_reflog finish && + HEADNAME=$(cat "$DOTEST"/head-name) && + OLDHEAD=$(cat "$DOTEST"/head) && + SHORTONTO=$(git rev-parse --short $(cat "$DOTEST"/onto)) && + if [ -d "$REWRITTEN" ] + then + test -f "$DOTEST"/current-commit && + current_commit=$(cat "$DOTEST"/current-commit) && + git rev-parse HEAD > "$REWRITTEN"/$current_commit + NEWHEAD=$(cat "$REWRITTEN"/$OLDHEAD) + else + NEWHEAD=$(git rev-parse HEAD) + fi && + message="$GIT_REFLOG_ACTION: $HEADNAME onto $SHORTONTO)" && + git update-ref -m "$message" $HEADNAME $NEWHEAD $OLDHEAD && + git symbolic-ref HEAD $HEADNAME && + rm -rf "$DOTEST" && + warn "Successfully rebased and updated $HEADNAME." + + exit +} + +do_rest () { + while : + do + do_next + done + test -f "$DOTEST"/verbose && + git diff --stat $(cat "$DOTEST"/head)..HEAD + exit +} + +while case $# in 0) break ;; esac +do + case "$1" in + --continue) + comment_for_reflog continue + + test -d "$DOTEST" || die "No interactive rebase running" + + require_clean_work_tree + do_rest + ;; + --abort) + comment_for_reflog abort + + test -d "$DOTEST" || die "No interactive rebase running" + + HEADNAME=$(cat "$DOTEST"/head-name) + HEAD=$(cat "$DOTEST"/head) + git symbolic-ref HEAD $HEADNAME && + git reset --hard $HEAD && + rm -rf "$DOTEST" + exit + ;; + --skip) + comment_for_reflog skip + + test -d "$DOTEST" || die "No interactive rebase running" + + git reset --hard && do_rest + ;; + -s|--strategy) + shift + case "$#,$1" in + *,*=*) + STRATEGY="-s `expr "z$1" : 'z-[^=]*=\(.*\)'`" ;; + 1,*) + usage ;; + *) + STRATEGY="-s $2" + shift ;; + esac + ;; + --merge) + # we use merge anyway + ;; + -C*) + die "Interactive rebase uses merge, so $1 does not make sense" + ;; + -v|--verbose) + VERBOSE=t + ;; + -p|--preserve-merges) + PRESERVE_MERGES=t + ;; + -i|--interactive) + # yeah, we know + ;; + ''|-h) + usage + ;; + *) + test -d "$DOTEST" && + die "Interactive rebase already started" + + git var GIT_COMMITTER_IDENT >/dev/null || + die "You need to set your committer info first" + + comment_for_reflog start + + ONTO= + case "$1" in + --onto) + ONTO=$(git rev-parse --verify "$2") || + die "Does not point to a valid commit: $2" + shift; shift + ;; + esac + + require_clean_work_tree + + if [ ! -z "$2"] + then + git show-ref --verify --quiet "refs/heads/$2" || + die "Invalid branchname: $2" + git checkout "$2" || + die "Could not checkout $2" + fi + + HEAD=$(git rev-parse --verify HEAD) || die "No HEAD?" + UPSTREAM=$(git rev-parse --verify "$1") || die "Invalid base" + + test -z "$ONTO" && ONTO=$UPSTREAM + + mkdir "$DOTEST" || die "Could not create temporary $DOTEST" + : > "$DOTEST"/interactive || die "Could not mark as interactive" + git symbolic-ref HEAD > "$DOTEST"/head-name || + die "Could not get HEAD" + + echo $HEAD > "$DOTEST"/head + echo $UPSTREAM > "$DOTEST"/upstream + echo $ONTO > "$DOTEST"/onto + test t = "$VERBOSE" && : > "$DOTEST"/verbose + if [ t = "$PRESERVE_MERGES" ] + then + # $REWRITTEN contains files for each commit that is + # reachable by at least one merge base of $HEAD and + # $UPSTREAM. They are not necessarily rewritten, but + # their children might be. + # This ensures that commits on merged, but otherwise + # unrelated side branches are left alone. (Think "X" + # in the man page's example.) + mkdir "$REWRITTEN" && + for c in $(git merge-base --all $HEAD $UPSTREAM) + do + echo $ONTO > "$REWRITTEN"/$c || + die "Could not init rewritten commits" + done + MERGES_OPTION= + else + MERGES_OPTION=--no-merges + fi + + SHORTUPSTREAM=$(git rev-parse --short $UPSTREAM) + SHORTHEAD=$(git rev-parse --short $HEAD) + SHORTONTO=$(git rev-parse --short $ONTO) + cat > "$TODO" << EOF +# Rebasing $SHORTUPSTREAM..$SHORTHEAD onto $SHORTONTO +# +# Commands: +# pick = use commit +# edit = use commit, but stop for amending +# squash = use commit, but meld into previous commit +EOF + git rev-list $MERGES_OPTION --pretty=oneline --abbrev-commit \ + --abbrev=7 --reverse $UPSTREAM..$HEAD | \ + sed "s/^/pick /" >> "$TODO" + + test -z "$(grep -ve '^$' -e '^#' < $TODO)" && + die_abort "Nothing to do" + + cp "$TODO" "$TODO".backup + ${VISUAL:-${EDITOR:-vi}} "$TODO" || + die "Could not execute editor" + + test -z "$(grep -ve '^$' -e '^#' < $TODO)" && + die_abort "Nothing to do" + + git checkout $ONTO && do_rest + esac + shift +done diff --git a/git-rebase.sh b/git-rebase.sh index 2aa3a011d..388752661 100755 --- a/git-rebase.sh +++ b/git-rebase.sh @@ -3,7 +3,7 @@ # Copyright (c) 2005 Junio C Hamano. # -USAGE='[-v] [--onto <newbase>] <upstream> [<branch>]' +USAGE='[--interactive | -i] [-v] [--onto <newbase>] <upstream> [<branch>]' LONG_USAGE='git-rebase replaces <branch> with a new branch of the same name. When the --onto option is provided the new branch starts out with a HEAD equal to <newbase>, otherwise it is equal to <upstream> @@ -120,6 +120,16 @@ finish_rb_merge () { echo "All done." } +is_interactive () { + test -f "$dotest"/interactive || + while case $#,"$1" in 0,|*,-i|*,--interactive) break ;; esac + do + shift + done && test -n "$1" +} + +is_interactive "$@" && exec git-rebase--interactive "$@" + while case "$#" in 0) break ;; esac do case "$1" in diff --git a/git-send-email.perl b/git-send-email.perl index 7c0c90bd2..87f59fa31 100755 --- a/git-send-email.perl +++ b/git-send-email.perl @@ -64,15 +64,17 @@ Options: email sent, rather than to the first email sent. Defaults to on. - --no-signed-off-cc Suppress the automatic addition of email addresses - that appear in Signed-off-by: or Cc: lines to the cc: - list. Note: Using this option is not recommended. + --signed-off-cc Automatically add email addresses that appear in + Signed-off-by: or Cc: lines to the cc: list. Defaults to on. --smtp-server If set, specifies the outgoing SMTP server to use. Defaults to localhost. --suppress-from Suppress sending emails to yourself if your address - appears in a From: line. + appears in a From: line. Defaults to off. + + --thread Specify that the "In-Reply-To:" header should be set on all + emails. Defaults to on. --quiet Make git-send-email less verbose. One line per email should be all that is output. @@ -137,9 +139,6 @@ my $compose_filename = ".msg.$$"; my (@to,@cc,@initial_cc,@bcclist,@xh, $initial_reply_to,$initial_subject,@files,$from,$compose,$time); -# Behavior modification variables -my ($chain_reply_to, $quiet, $suppress_from, $no_signed_off_cc, - $dry_run) = (1, 0, 0, 0, 0); my $smtp_server; my $envelope_sender; @@ -154,9 +153,22 @@ if ($@) { $term = new FakeTerm "$@: going non-interactive"; } -my $def_chain = $repo->config_bool('sendemail.chainreplyto'); -if (defined $def_chain and not $def_chain) { - $chain_reply_to = 0; +# Behavior modification variables +my ($quiet, $dry_run) = (0, 0); + +# Variables with corresponding config settings +my ($thread, $chain_reply_to, $suppress_from, $signed_off_cc); + +my %config_settings = ( + "thread" => [\$thread, 1], + "chainreplyto" => [\$chain_reply_to, 1], + "suppressfrom" => [\$suppress_from, 0], + "signedoffcc" => [\$signed_off_cc, 1], +); + +foreach my $setting (keys %config_settings) { + my $config = $repo->config_bool("sendemail.$setting"); + ${$config_settings{$setting}->[0]} = (defined $config) ? $config : $config_settings{$setting}->[1]; } @bcclist = $repo->config('sendemail.bcc'); @@ -177,10 +189,11 @@ my $rc = GetOptions("from=s" => \$from, "smtp-server=s" => \$smtp_server, "compose" => \$compose, "quiet" => \$quiet, - "suppress-from" => \$suppress_from, - "no-signed-off-cc|no-signed-off-by-cc" => \$no_signed_off_cc, + "suppress-from!" => \$suppress_from, + "signed-off-cc|signed-off-by-cc!" => \$signed_off_cc, "dry-run" => \$dry_run, "envelope-sender=s" => \$envelope_sender, + "thread!" => \$thread, ); unless ($rc) { @@ -287,7 +300,7 @@ if (!defined $initial_subject && $compose) { $prompting++; } -if (!defined $initial_reply_to && $prompting) { +if ($thread && !defined $initial_reply_to && $prompting) { do { $_= $term->readline("Message-ID to be used as In-Reply-To for the first email? ", $initial_reply_to); @@ -412,13 +425,21 @@ sub extract_valid_address { # 1 second since the last time we were called. # We'll setup a template for the message id, using the "from" address: -my $message_id_from = extract_valid_address($from); -my $message_id_template = "<%s-git-send-email-$message_id_from>"; sub make_message_id { my $date = time; my $pseudo_rand = int (rand(4200)); + my $du_part; + for ($from, $committer, $author) { + $du_part = extract_valid_address($_); + last if ($du_part ne ''); + } + if ($du_part eq '') { + use Sys::Hostname qw(); + $du_part = 'user@' . Sys::Hostname::hostname(); + } + my $message_id_template = "<%s-git-send-email-$du_part>"; $message_id = sprintf $message_id_template, "$date$pseudo_rand"; #print "new message id = $message_id\n"; # Was useful for debugging } @@ -467,6 +488,8 @@ sub send_message $ccline = "\nCc: $cc"; } $from = sanitize_address_rfc822($from); + make_message_id(); + my $header = "From: $from To: $to${ccline} Subject: $subject @@ -474,7 +497,7 @@ Date: $date Message-Id: $message_id X-Mailer: git-send-email $gitversion "; - if ($reply_to) { + if ($thread && $reply_to) { $header .= "In-Reply-To: $reply_to\n"; $header .= "References: $references\n"; @@ -533,7 +556,6 @@ X-Mailer: git-send-email $gitversion $reply_to = $initial_reply_to; $references = $initial_reply_to || ''; -make_message_id(); $subject = $initial_subject; foreach my $t (@files) { @@ -600,7 +622,7 @@ foreach my $t (@files) { } } else { $message .= $_; - if (/^(Signed-off-by|Cc): (.*)$/i && !$no_signed_off_cc) { + if (/^(Signed-off-by|Cc): (.*)$/i && $signed_off_cc) { my $c = $2; chomp $c; push @cc, $c; @@ -627,7 +649,6 @@ foreach my $t (@files) { $references = "$message_id"; } } - make_message_id(); } if ($compose) { diff --git a/git-sh-setup.sh b/git-sh-setup.sh index f24c7f2d2..98959600e 100755 --- a/git-sh-setup.sh +++ b/git-sh-setup.sh @@ -29,11 +29,7 @@ set_reflog_action() { } is_bare_repository () { - git-config --bool --get core.bare || - case "$GIT_DIR" in - .git | */.git) echo false ;; - *) echo true ;; - esac + git-rev-parse --is-bare-repository } cd_to_toplevel () { @@ -48,11 +44,38 @@ cd_to_toplevel () { } require_work_tree () { - test $(is_bare_repository) = false && + test $(git-rev-parse --is-inside-work-tree) = true && test $(git-rev-parse --is-inside-git-dir) = false || die "fatal: $0 cannot be used without a working tree." } +get_author_ident_from_commit () { + pick_author_script=' + /^author /{ + s/'\''/'\''\\'\'\''/g + h + s/^author \([^<]*\) <[^>]*> .*$/\1/ + s/'\''/'\''\'\'\''/g + s/.*/GIT_AUTHOR_NAME='\''&'\''/p + + g + s/^author [^<]* <\([^>]*\)> .*$/\1/ + s/'\''/'\''\'\'\''/g + s/.*/GIT_AUTHOR_EMAIL='\''&'\''/p + + g + s/^author [^<]* <[^>]*> \(.*\)$/\1/ + s/'\''/'\''\'\'\''/g + s/.*/GIT_AUTHOR_DATE='\''&'\''/p + + q + } + ' + encoding=$(git config i18n.commitencoding || echo UTF-8) + git show -s --pretty=raw --encoding="$encoding" "$1" | + LANG=C LC_ALL=C sed -ne "$pick_author_script" +} + if [ -z "$LONG_USAGE" ] then LONG_USAGE="Usage: $0 $USAGE" diff --git a/git-stash.sh b/git-stash.sh new file mode 100755 index 000000000..18d3322ab --- /dev/null +++ b/git-stash.sh @@ -0,0 +1,165 @@ +#!/bin/sh +# Copyright (c) 2007, Nanako Shiraishi + +USAGE='[ | list | show | apply | clear]' + +. git-sh-setup +require_work_tree + +TMP="$GIT_DIR/.git-stash.$$" +trap 'rm -f "$TMP-*"' 0 + +ref_stash=refs/stash + +no_changes () { + git-diff-index --quiet --cached HEAD && + git-diff-files --quiet +} + +clear_stash () { + logfile="$GIT_DIR/logs/$ref_stash" && + mkdir -p "$(dirname "$logfile")" && + : >"$logfile" +} + +save_stash () { + if no_changes + then + echo >&2 'No local changes to save' + exit 0 + fi + test -f "$GIT_DIR/logs/$ref_stash" || + clear_stash || die "Cannot initialize stash" + + # state of the base commit + if b_commit=$(git-rev-parse --verify HEAD) + then + head=$(git-log --abbrev-commit --pretty=oneline -n 1 HEAD) + else + die "You do not have the initial commit yet" + fi + + if branch=$(git-symbolic-ref -q HEAD) + then + branch=${branch#refs/heads/} + else + branch='(no branch)' + fi + msg=$(printf '%s: %s' "$branch" "$head") + + # state of the index + i_tree=$(git-write-tree) && + i_commit=$(printf 'index on %s' "$msg" | + git-commit-tree $i_tree -p $b_commit) || + die "Cannot save the current index state" + + # state of the working tree + w_tree=$( ( + GIT_INDEX_FILE="$TMP-index" && + export GIT_INDEX_FILE && + + rm -f "$TMP-index" && + git-read-tree $i_tree && + git-add -u && + git-write-tree && + rm -f "$TMP-index" + ) ) || + die "Cannot save the current worktree state" + + # create the stash + w_commit=$(printf 'WIP on %s' "$msg" | + git-commit-tree $w_tree -p $b_commit -p $i_commit) || + die "Cannot record working tree state" + + git-update-ref -m "$msg" $ref_stash $w_commit || + die "Cannot save the current status" + printf >&2 'Saved WIP on %s\n' "$msg" +} + +have_stash () { + git-rev-parse --verify $ref_stash >/dev/null 2>&1 +} + +list_stash () { + have_stash || return 0 + git-log --pretty=oneline -g "$@" $ref_stash | + sed -n -e 's/^[.0-9a-f]* refs\///p' +} + +show_stash () { + flags=$(git-rev-parse --no-revs --flags "$@") + if test -z "$flags" + then + flags=--stat + fi + s=$(git-rev-parse --revs-only --no-flags --default $ref_stash "$@") + + w_commit=$(git-rev-parse --verify "$s") && + b_commit=$(git-rev-parse --verify "$s^") && + git-diff $flags $b_commit $w_commit +} + +apply_stash () { + git-diff-files --quiet || + die 'Cannot restore on top of a dirty state' + + # current index state + c_tree=$(git-write-tree) || + die 'Cannot apply a stash in the middle of a merge' + + s=$(git-rev-parse --revs-only --no-flags --default $ref_stash "$@") && + w_tree=$(git-rev-parse --verify "$s:") && + b_tree=$(git-rev-parse --verify "$s^:") || + die "$*: no valid stashed state found" + + eval " + GITHEAD_$w_tree='Stashed changes' && + GITHEAD_$c_tree='Updated upstream' && + GITHEAD_$b_tree='Version stash was based on' && + export GITHEAD_$w_tree GITHEAD_$c_tree GITHEAD_$b_tree + " + + if git-merge-recursive $b_tree -- $c_tree $w_tree + then + # No conflict + a="$TMP-added" && + git-diff --cached --name-only --diff-filter=A $c_tree >"$a" && + git-read-tree --reset $c_tree && + git-update-index --add --stdin <"$a" || + die "Cannot unstage modified files" + git-status + rm -f "$a" + else + # Merge conflict; keep the exit status from merge-recursive + exit + fi +} + +# Main command set +case "$1" in +list | '') + test $# -gt 0 && shift + if test $# = 0 + then + set x -n 10 + shift + fi + list_stash "$@" + ;; +show) + shift + show_stash "$@" + ;; +apply) + shift + apply_stash "$@" + ;; +clear) + clear_stash + ;; +save) + save_stash && git-reset --hard + ;; +*) + usage +esac diff --git a/git-submodule.sh b/git-submodule.sh index 8bdd99a2f..c29e2c3c9 100755 --- a/git-submodule.sh +++ b/git-submodule.sh @@ -1,13 +1,15 @@ #!/bin/sh # -# git-submodules.sh: init, update or list git submodules +# git-submodules.sh: add, init, update or list git submodules # # Copyright (c) 2007 Lars Hjemli -USAGE='[--quiet] [--cached] [status|init|update] [--] [<path>...]' +USAGE='[--quiet] [--cached] [add <repo> [-b branch]|status|init|update] [--] [<path>...]' . git-sh-setup require_work_tree +add= +branch= init= update= status= @@ -25,10 +27,40 @@ say() fi } +# NEEDSWORK: identical function exists in get_repo_base in clone.sh +get_repo_base() { + ( + cd "`/bin/pwd`" && + cd "$1" || cd "$1.git" && + { + cd .git + pwd + } + ) 2>/dev/null +} + +# +# Map submodule path to submodule name +# +# $1 = path +# +module_name() +{ + name=$(GIT_CONFIG=.gitmodules git-config --get-regexp '^submodule\..*\.path$' "$1" | + sed -nre 's/^submodule\.(.+)\.path .+$/\1/p') + test -z "$name" && + die "No submodule mapping found in .gitmodules for path '$path'" + echo "$name" +} # # Clone a submodule # +# Prior to calling, modules_update checks that a possibly existing +# path is not a git repository. +# Likewise, module_add checks that path does not exist at all, +# since it is the location of a new submodule. +# module_clone() { path=$1 @@ -49,7 +81,54 @@ module_clone() die "A file already exist at path '$path'" git-clone -n "$url" "$path" || - die "Clone of submodule '$path' failed" + die "Clone of '$url' into submodule path '$path' failed" +} + +# +# Add a new submodule to the working tree, .gitmodules and the index +# +# $@ = repo [path] +# +# optional branch is stored in global branch variable +# +module_add() +{ + repo=$1 + path=$2 + + if test -z "$repo"; then + usage + fi + + # Turn the source into an absolute path if + # it is local + if base=$(get_repo_base "$repo"); then + repo="$base" + fi + + # Guess path from repo if not specified or strip trailing slashes + if test -z "$path"; then + path=$(echo "$repo" | sed -e 's|/*$||' -e 's|:*/*\.git$||' -e 's|.*[/:]||g') + else + path=$(echo "$path" | sed -e 's|/*$||') + fi + + test -e "$path" && + die "'$path' already exists" + + git-ls-files --error-unmatch "$path" > /dev/null 2>&1 && + die "'$path' already exists in the index" + + module_clone "$path" "$repo" || exit + (unset GIT_DIR && cd "$path" && git checkout -q ${branch:+-b "$branch" "origin/$branch"}) || + die "Unable to checkout submodule '$path'" + git add "$path" || + die "Failed to add submodule '$path'" + + GIT_CONFIG=.gitmodules git config submodule."$path".path "$path" && + GIT_CONFIG=.gitmodules git config submodule."$path".url "$repo" && + git add .gitmodules || + die "Failed to register submodule '$path'" } # @@ -63,17 +142,18 @@ modules_init() while read mode sha1 stage path do # Skip already registered paths - url=$(git-config submodule."$path".url) + name=$(module_name "$path") || exit + url=$(git-config submodule."$name".url) test -z "$url" || continue - url=$(GIT_CONFIG=.gitmodules git-config module."$path".url) + url=$(GIT_CONFIG=.gitmodules git-config submodule."$name".url) test -z "$url" && - die "No url found for submodule '$path' in .gitmodules" + die "No url found for submodule path '$path' in .gitmodules" - git-config submodule."$path".url "$url" || - die "Failed to register url for submodule '$path'" + git-config submodule."$name".url "$url" || + die "Failed to register url for submodule path '$path'" - say "Submodule '$path' registered with url '$url'" + say "Submodule '$name' ($url) registered for path '$path'" done } @@ -87,38 +167,52 @@ modules_update() git ls-files --stage -- "$@" | grep -e '^160000 ' | while read mode sha1 stage path do - url=$(git-config submodule."$path".url) + name=$(module_name "$path") || exit + url=$(git-config submodule."$name".url) if test -z "$url" then # Only mention uninitialized submodules when its # path have been specified test "$#" != "0" && - say "Submodule '$path' not initialized" + say "Submodule path '$path' not initialized" continue fi if ! test -d "$path"/.git then module_clone "$path" "$url" || exit + subsha1= + else + subsha1=$(unset GIT_DIR && cd "$path" && + git-rev-parse --verify HEAD) || + die "Unable to find current revision in submodule path '$path'" fi - subsha1=$(unset GIT_DIR && cd "$path" && - git-rev-parse --verify HEAD) || - die "Unable to find current revision of submodule '$path'" - if test "$subsha1" != "$sha1" then (unset GIT_DIR && cd "$path" && git-fetch && git-checkout -q "$sha1") || - die "Unable to checkout '$sha1' in submodule '$path'" + die "Unable to checkout '$sha1' in submodule path '$path'" - say "Submodule '$path': checked out '$sha1'" + say "Submodule path '$path': checked out '$sha1'" fi done } +set_name_rev () { + revname=$( ( + unset GIT_DIR && + cd "$1" && { + git-describe "$2" 2>/dev/null || + git-describe --tags "$2" 2>/dev/null || + git-describe --contains --tags "$2" + } + ) ) + test -z "$revname" || revname=" ($revname)" +} + # -# List all registered submodules, prefixed with: +# List all submodules, prefixed with: # - submodule not initialized # + different revision checked out # @@ -132,22 +226,25 @@ modules_list() git ls-files --stage -- "$@" | grep -e '^160000 ' | while read mode sha1 stage path do - if ! test -d "$path"/.git + name=$(module_name "$path") || exit + url=$(git-config submodule."$name".url) + if test -z "url" || ! test -d "$path"/.git then say "-$sha1 $path" continue; fi - revname=$(unset GIT_DIR && cd "$path" && git-describe $sha1) + revname=$(unset GIT_DIR && cd "$path" && git-describe --tags $sha1) + set_name_rev "$path" $"sha1" if git diff-files --quiet -- "$path" then - say " $sha1 $path ($revname)" + say " $sha1 $path$revname" else if test -z "$cached" then sha1=$(unset GIT_DIR && cd "$path" && git-rev-parse --verify HEAD) - revname=$(unset GIT_DIR && cd "$path" && git-describe $sha1) + set_name_rev "$path" $"sha1" fi - say "+$sha1 $path ($revname)" + say "+$sha1 $path$revname" fi done } @@ -155,6 +252,9 @@ modules_list() while case "$#" in 0) break ;; esac do case "$1" in + add) + add=1 + ;; init) init=1 ;; @@ -167,6 +267,14 @@ do -q|--quiet) quiet=1 ;; + -b|--branch) + case "$2" in + '') + usage + ;; + esac + branch="$2"; shift + ;; --cached) cached=1 ;; @@ -183,14 +291,27 @@ do shift done -case "$init,$update,$status,$cached" in -1,,,) +case "$add,$branch" in +1,*) + ;; +,) + ;; +,*) + usage + ;; +esac + +case "$add,$init,$update,$status,$cached" in +1,,,,) + module_add "$@" + ;; +,1,,,) modules_init "$@" ;; -,1,,) +,,1,,) modules_update "$@" ;; -,,*,*) +,,,1,*) modules_list "$@" ;; *) diff --git a/git-svn.perl b/git-svn.perl index 42906767a..51979f963 100755 --- a/git-svn.perl +++ b/git-svn.perl @@ -38,14 +38,16 @@ use IPC::Open3; use Git; BEGIN { - my $s; + # import functions from Git into our packages, en masse + no strict 'refs'; foreach (qw/command command_oneline command_noisy command_output_pipe command_input_pipe command_close_pipe/) { - $s .= "*SVN::Git::Editor::$_ = *SVN::Git::Fetcher::$_ = ". - "*Git::SVN::Migration::$_ = ". - "*Git::SVN::Log::$_ = *Git::SVN::$_ = *$_ = *Git::$_; "; + for my $package ( qw(SVN::Git::Editor SVN::Git::Fetcher + Git::SVN::Migration Git::SVN::Log Git::SVN), + __PACKAGE__) { + *{"${package}::$_"} = \&{"Git::$_"}; + } } - eval $s; } my ($SVN); @@ -590,8 +592,7 @@ sub post_fetch_checkout { my $index = $ENV{GIT_INDEX_FILE} || "$ENV{GIT_DIR}/index"; return if -f $index; - chomp(my $bare = `git config --bool --get core.bare`); - return if $bare eq 'true'; + return if command_oneline(qw/rev-parse --is-inside-work-tree/) eq 'false'; return if command_oneline(qw/rev-parse --is-inside-git-dir/) eq 'true'; command_noisy(qw/read-tree -m -u -v HEAD HEAD/); print STDERR "Checked out HEAD:\n ", @@ -781,12 +782,12 @@ sub read_repo_config { sub extract_metadata { my $id = shift or return (undef, undef, undef); - my ($url, $rev, $uuid) = ($id =~ /^git-svn-id:\s(\S+?)\@(\d+) + my ($url, $rev, $uuid) = ($id =~ /^\s*git-svn-id:\s+(.*)\@(\d+) \s([a-f\d\-]+)$/x); if (!defined $rev || !$uuid || !$url) { # some of the original repositories I made had # identifiers like this: - ($rev, $uuid) = ($id =~/^git-svn-id:\s(\d+)\@([a-f\d\-]+)/); + ($rev, $uuid) = ($id =~/^\s*git-svn-id:\s(\d+)\@([a-f\d\-]+)/); } return ($url, $rev, $uuid); } @@ -798,20 +799,29 @@ sub cmt_metadata { sub working_head_info { my ($head, $refs) = @_; - my ($fh, $ctx) = command_output_pipe('rev-list', $head); - while (my $hash = <$fh>) { - chomp($hash); - my ($url, $rev, $uuid) = cmt_metadata($hash); + my ($fh, $ctx) = command_output_pipe('log', $head); + my $hash; + my %max; + while (<$fh>) { + if ( m{^commit ($::sha1)$} ) { + unshift @$refs, $hash if $hash and $refs; + $hash = $1; + next; + } + next unless s{^\s*(git-svn-id:)}{$1}; + my ($url, $rev, $uuid) = extract_metadata($_); if (defined $url && defined $rev) { + next if $max{$url} and $max{$url} < $rev; if (my $gs = Git::SVN->find_by_url($url)) { my $c = $gs->rev_db_get($rev); if ($c && $c eq $hash) { close $fh; # break the pipe return ($url, $rev, $uuid, $gs); + } else { + $max{$url} ||= $gs->rev_db_max; } } } - unshift @$refs, $hash if $refs; } command_close_pipe($fh, $ctx); (undef, undef, undef, undef); @@ -895,26 +905,26 @@ BEGIN { # some options are read globally, but can be overridden locally # per [svn-remote "..."] section. Command-line options will *NOT* # override options set in an [svn-remote "..."] section - my $e; - foreach (qw/follow_parent no_metadata use_svm_props - use_svnsync_props/) { - my $key = $_; + no strict 'refs'; + for my $option (qw/follow_parent no_metadata use_svm_props + use_svnsync_props/) { + my $key = $option; $key =~ tr/_//d; - $e .= "sub $_ { - my (\$self) = \@_; - return \$self->{-$_} if exists \$self->{-$_}; - my \$k = \"svn-remote.\$self->{repo_id}\.$key\"; - eval { command_oneline(qw/config --get/, \$k) }; - if (\$@) { - \$self->{-$_} = \$Git::SVN::_$_; + my $prop = "-$option"; + *$option = sub { + my ($self) = @_; + return $self->{$prop} if exists $self->{$prop}; + my $k = "svn-remote.$self->{repo_id}.$key"; + eval { command_oneline(qw/config --get/, $k) }; + if ($@) { + $self->{$prop} = ${"Git::SVN::_$option"}; } else { - my \$v = command_oneline(qw/config --bool/,\$k); - \$self->{-$_} = \$v eq 'false' ? 0 : 1; + my $v = command_oneline(qw/config --bool/,$k); + $self->{$prop} = $v eq 'false' ? 0 : 1; } - return \$self->{-$_} }\n"; + return $self->{$prop}; + } } - $e .= "1;\n"; - eval $e or die $@; } my %LOCKFILES; @@ -2018,16 +2028,19 @@ sub rebuild { return; } print "Rebuilding $db_path ...\n"; - my ($rev_list, $ctx) = command_output_pipe("rev-list", $self->refname); + my ($log, $ctx) = command_output_pipe("log", $self->refname); my $latest; my $full_url = $self->full_url; remove_username($full_url); my $svn_uuid; - while (<$rev_list>) { - chomp; - my $c = $_; - die "Non-SHA1: $c\n" unless $c =~ /^$::sha1$/o; - my ($url, $rev, $uuid) = ::cmt_metadata($c); + my $c; + while (<$log>) { + if ( m{^commit ($::sha1)$} ) { + $c = $1; + next; + } + next unless s{^\s*(git-svn-id:)}{$1}; + my ($url, $rev, $uuid) = ::extract_metadata($_); remove_username($url); # ignore merges (from set-tree) @@ -2045,7 +2058,7 @@ sub rebuild { $self->rev_db_set($rev, $c); print "r$rev = $c\n"; } - command_close_pipe($rev_list, $ctx); + command_close_pipe($log, $ctx); print "Done rebuilding $db_path\n"; } @@ -2953,17 +2966,17 @@ my ($can_do_switch, %ignored_err, $RA); BEGIN { # enforce temporary pool usage for some simple functions - my $e; - foreach (qw/rev_proplist get_latest_revnum get_uuid get_repos_root/) { - $e .= "sub $_ { - my \$self = shift; - my \$pool = SVN::Pool->new; - my \@ret = \$self->SUPER::$_(\@_,\$pool); - \$pool->clear; - wantarray ? \@ret : \$ret[0]; }\n"; + no strict 'refs'; + for my $f (qw/rev_proplist get_latest_revnum get_uuid get_repos_root/) { + my $SUPER = "SUPER::$f"; + *$f = sub { + my $self = shift; + my $pool = SVN::Pool->new; + my @ret = $self->$SUPER(@_,$pool); + $pool->clear; + wantarray ? @ret : $ret[0]; + }; } - - eval "$e; 1;" or die $@; } sub new { @@ -2977,6 +2990,7 @@ sub new { SVN::Client::get_ssl_server_trust_file_provider(), SVN::Client::get_simple_prompt_provider( \&Git::SVN::Prompt::simple, 2), + SVN::Client::get_ssl_client_cert_file_provider(), SVN::Client::get_ssl_client_cert_prompt_provider( \&Git::SVN::Prompt::ssl_client_cert, 2), SVN::Client::get_ssl_client_cert_pw_prompt_provider( diff --git a/git-svnimport.perl b/git-svnimport.perl index f4597626b..b73d6494d 100755 --- a/git-svnimport.perl +++ b/git-svnimport.perl @@ -867,34 +867,14 @@ sub commit { or die "Cannot write branch $dest for update: $!\n"; } - if($tag) { - my($in, $out) = ('',''); + if ($tag) { $last_rev = "-" if %$changed_paths; # the tag was 'complex', i.e. did not refer to a "real" revision $dest =~ tr/_/\./ if $opt_u; - $branch = $dest; - - my $pid = open2($in, $out, 'git-mktag'); - print $out ("object $cid\n". - "type commit\n". - "tag $dest\n". - "tagger $committer_name <$committer_email> 0 +0000\n") and - close($out) - or die "Cannot create tag object $dest: $!\n"; - - my $tagobj = <$in>; - chomp $tagobj; - - if ( !close($in) or waitpid($pid, 0) != $pid or - $? != 0 or $tagobj !~ /^[0123456789abcdef]{40}$/ ) { - die "Cannot create tag object $dest: $!\n"; - } - open(C,">$git_dir/refs/tags/$dest") and - print C ("$tagobj\n") and - close(C) - or die "Cannot create tag $branch: $!\n"; + system('git-tag', $dest, $cid) == 0 + or die "Cannot create tag $dest: $!\n"; print "Created tag '$dest' on '$branch'\n" if $opt_v; } diff --git a/git-tag.sh b/git-tag.sh index c84043902..1ff5b41e7 100755 --- a/git-tag.sh +++ b/git-tag.sh @@ -19,28 +19,40 @@ do case "$1" in -a) annotate=1 + shift ;; -s) annotate=1 signed=1 + shift ;; -f) force=1 + shift ;; -n) - case $2 in - -*) LINES=1 # no argument + case "$#,$2" in + 1,* | *,-*) + LINES=1 # no argument ;; *) shift LINES=$(expr "$1" : '\([0-9]*\)') [ -z "$LINES" ] && LINES=1 # 1 line is default when -n is used ;; esac + shift ;; -l) list=1 shift - PATTERN="$1" # select tags by shell pattern, not re + case $# in + 0) PATTERN= + ;; + *) + PATTERN="$1" # select tags by shell pattern, not re + shift + ;; + esac git rev-parse --symbolic --tags | sort | while read TAG do @@ -51,12 +63,16 @@ do [ "$LINES" -le 0 ] && { echo "$TAG"; continue ;} OBJTYPE=$(git cat-file -t "$TAG") case $OBJTYPE in - tag) ANNOTATION=$(git cat-file tag "$TAG" | - sed -e '1,/^$/d' \ - -e '/^-----BEGIN PGP SIGNATURE-----$/Q' ) - printf "%-15s %s\n" "$TAG" "$ANNOTATION" | - sed -e '2,$s/^/ /' \ - -e "${LINES}q" + tag) + ANNOTATION=$(git cat-file tag "$TAG" | + sed -e '1,/^$/d' | + sed -n -e " + /^-----BEGIN PGP SIGNATURE-----\$/q + 2,\$s/^/ / + p + ${LINES}q + ") + printf "%-15s %s\n" "$TAG" "$ANNOTATION" ;; *) echo "$TAG" ;; @@ -70,7 +86,9 @@ do if test "$#" = "0"; then die "error: option -m needs an argument" else + message="$1" message_given=1 + shift fi ;; -F) @@ -81,13 +99,19 @@ do else message="$(cat "$1")" message_given=1 + shift fi ;; -u) annotate=1 signed=1 shift - username="$1" + if test "$#" = "0"; then + die "error: option -u needs an argument" + else + username="$1" + shift + fi ;; -d) shift @@ -122,7 +146,6 @@ do break ;; esac - shift done [ -n "$list" ] && exit 0 diff --git a/git-verify-tag.sh b/git-verify-tag.sh index f2d5597db..68858b694 100755 --- a/git-verify-tag.sh +++ b/git-verify-tag.sh @@ -37,8 +37,9 @@ esac trap 'rm -f "$GIT_DIR/.tmp-vtag"' 0 git-cat-file tag "$1" >"$GIT_DIR/.tmp-vtag" || exit 1 - -cat "$GIT_DIR/.tmp-vtag" | -sed '/-----BEGIN PGP/Q' | +sed -n -e ' + /^-----BEGIN PGP SIGNATURE-----$/q + p +' <"$GIT_DIR/.tmp-vtag" | gpg --verify "$GIT_DIR/.tmp-vtag" - || exit 1 rm -f "$GIT_DIR/.tmp-vtag" @@ -4,7 +4,7 @@ #include "quote.h" const char git_usage_string[] = - "git [--version] [--exec-path[=GIT_EXEC_PATH]] [-p|--paginate] [--bare] [--git-dir=GIT_DIR] [--help] COMMAND [ARGS]"; + "git [--version] [--exec-path[=GIT_EXEC_PATH]] [-p|--paginate] [--bare] [--git-dir=GIT_DIR] [--work-tree=GIT_WORK_TREE] [--help] COMMAND [ARGS]"; static void prepend_to_path(const char *dir, int len) { @@ -28,7 +28,7 @@ static void prepend_to_path(const char *dir, int len) free(path); } -static int handle_options(const char*** argv, int* argc) +static int handle_options(const char*** argv, int* argc, int* envchanged) { int handled = 0; @@ -64,14 +64,34 @@ static int handle_options(const char*** argv, int* argc) usage(git_usage_string); } setenv(GIT_DIR_ENVIRONMENT, (*argv)[1], 1); + if (envchanged) + *envchanged = 1; (*argv)++; (*argc)--; handled++; } else if (!prefixcmp(cmd, "--git-dir=")) { setenv(GIT_DIR_ENVIRONMENT, cmd + 10, 1); + if (envchanged) + *envchanged = 1; + } else if (!strcmp(cmd, "--work-tree")) { + if (*argc < 2) { + fprintf(stderr, "No directory given for --work-tree.\n" ); + usage(git_usage_string); + } + setenv(GIT_WORK_TREE_ENVIRONMENT, (*argv)[1], 1); + if (envchanged) + *envchanged = 1; + (*argv)++; + (*argc)--; + } else if (!prefixcmp(cmd, "--work-tree=")) { + setenv(GIT_WORK_TREE_ENVIRONMENT, cmd + 12, 1); + if (envchanged) + *envchanged = 1; } else if (!strcmp(cmd, "--bare")) { static char git_dir[PATH_MAX+1]; setenv(GIT_DIR_ENVIRONMENT, getcwd(git_dir, sizeof(git_dir)), 1); + if (envchanged) + *envchanged = 1; } else { fprintf(stderr, "Unknown option: %s\n", cmd); usage(git_usage_string); @@ -150,7 +170,7 @@ static int split_cmdline(char *cmdline, const char ***argv) static int handle_alias(int *argcp, const char ***argv) { - int nongit = 0, ret = 0, saved_errno = errno; + int nongit = 0, envchanged = 0, ret = 0, saved_errno = errno; const char *subdir; int count, option_count; const char** new_argv; @@ -161,6 +181,21 @@ static int handle_alias(int *argcp, const char ***argv) git_config(git_alias_config); if (alias_string) { if (alias_string[0] == '!') { + if (*argcp > 1) { + int i, sz = PATH_MAX; + char *s = xmalloc(sz), *new_alias = s; + + add_to_string(&s, &sz, alias_string, 0); + free(alias_string); + alias_string = new_alias; + for (i = 1; i < *argcp && + !add_to_string(&s, &sz, " ", 0) && + !add_to_string(&s, &sz, (*argv)[i], 1) + ; i++) + ; /* do nothing */ + if (!sz) + die("Too many or long arguments"); + } trace_printf("trace: alias to shell cmd: %s => %s\n", alias_command, alias_string + 1); ret = system(alias_string + 1); @@ -171,7 +206,11 @@ static int handle_alias(int *argcp, const char ***argv) alias_string + 1, alias_command); } count = split_cmdline(alias_string, &new_argv); - option_count = handle_options(&new_argv, &count); + option_count = handle_options(&new_argv, &count, &envchanged); + if (envchanged) + die("alias '%s' changes environment variables\n" + "You can use '!git' in the alias to do this.", + alias_command); memmove(new_argv - option_count, new_argv, count * sizeof(char *)); new_argv -= option_count; @@ -214,17 +253,56 @@ const char git_version_string[] = GIT_VERSION; * require working tree to be present -- anything uses this needs * RUN_SETUP for reading from the configuration file. */ -#define NOT_BARE (1<<2) +#define NEED_WORK_TREE (1<<2) + +struct cmd_struct { + const char *cmd; + int (*fn)(int, const char **, const char *); + int option; +}; -static void handle_internal_command(int argc, const char **argv, char **envp) +static int run_command(struct cmd_struct *p, int argc, const char **argv) +{ + int status; + struct stat st; + const char *prefix; + + prefix = NULL; + if (p->option & RUN_SETUP) + prefix = setup_git_directory(); + if (p->option & USE_PAGER) + setup_pager(); + if ((p->option & NEED_WORK_TREE) && + (!is_inside_work_tree() || is_inside_git_dir())) + die("%s must be run in a work tree", p->cmd); + trace_argv_printf(argv, argc, "trace: built-in: git"); + + status = p->fn(argc, argv, prefix); + if (status) + return status; + + /* Somebody closed stdout? */ + if (fstat(fileno(stdout), &st)) + return 0; + /* Ignore write errors for pipes and sockets.. */ + if (S_ISFIFO(st.st_mode) || S_ISSOCK(st.st_mode)) + return 0; + + /* Check for ENOSPC and EIO errors.. */ + if (fflush(stdout)) + die("write failure on standard output: %s", strerror(errno)); + if (ferror(stdout)) + die("unknown write failure on standard output"); + if (fclose(stdout)) + die("close failed on standard output: %s", strerror(errno)); + return 0; +} + +static void handle_internal_command(int argc, const char **argv) { const char *cmd = argv[0]; - static struct cmd_struct { - const char *cmd; - int (*fn)(int, const char **, const char *); - int option; - } commands[] = { - { "add", cmd_add, RUN_SETUP | NOT_BARE }, + static struct cmd_struct commands[] = { + { "add", cmd_add, RUN_SETUP | NEED_WORK_TREE }, { "annotate", cmd_annotate, RUN_SETUP | USE_PAGER }, { "apply", cmd_apply }, { "archive", cmd_archive }, @@ -234,9 +312,9 @@ static void handle_internal_command(int argc, const char **argv, char **envp) { "cat-file", cmd_cat_file, RUN_SETUP }, { "checkout-index", cmd_checkout_index, RUN_SETUP }, { "check-ref-format", cmd_check_ref_format }, - { "check-attr", cmd_check_attr, RUN_SETUP | NOT_BARE }, + { "check-attr", cmd_check_attr, RUN_SETUP | NEED_WORK_TREE }, { "cherry", cmd_cherry, RUN_SETUP }, - { "cherry-pick", cmd_cherry_pick, RUN_SETUP | NOT_BARE }, + { "cherry-pick", cmd_cherry_pick, RUN_SETUP | NEED_WORK_TREE }, { "commit-tree", cmd_commit_tree, RUN_SETUP }, { "config", cmd_config }, { "count-objects", cmd_count_objects, RUN_SETUP }, @@ -264,7 +342,7 @@ static void handle_internal_command(int argc, const char **argv, char **envp) { "mailsplit", cmd_mailsplit }, { "merge-base", cmd_merge_base, RUN_SETUP }, { "merge-file", cmd_merge_file }, - { "mv", cmd_mv, RUN_SETUP | NOT_BARE }, + { "mv", cmd_mv, RUN_SETUP | NEED_WORK_TREE }, { "name-rev", cmd_name_rev, RUN_SETUP }, { "pack-objects", cmd_pack_objects, RUN_SETUP }, { "pickaxe", cmd_blame, RUN_SETUP | USE_PAGER }, @@ -277,9 +355,9 @@ static void handle_internal_command(int argc, const char **argv, char **envp) { "rerere", cmd_rerere, RUN_SETUP }, { "rev-list", cmd_rev_list, RUN_SETUP }, { "rev-parse", cmd_rev_parse, RUN_SETUP }, - { "revert", cmd_revert, RUN_SETUP | NOT_BARE }, - { "rm", cmd_rm, RUN_SETUP | NOT_BARE }, - { "runstatus", cmd_runstatus, RUN_SETUP | NOT_BARE }, + { "revert", cmd_revert, RUN_SETUP | NEED_WORK_TREE }, + { "rm", cmd_rm, RUN_SETUP | NEED_WORK_TREE }, + { "runstatus", cmd_runstatus, RUN_SETUP | NEED_WORK_TREE }, { "shortlog", cmd_shortlog, RUN_SETUP | USE_PAGER }, { "show-branch", cmd_show_branch, RUN_SETUP }, { "show", cmd_show, RUN_SETUP | USE_PAGER }, @@ -307,25 +385,13 @@ static void handle_internal_command(int argc, const char **argv, char **envp) for (i = 0; i < ARRAY_SIZE(commands); i++) { struct cmd_struct *p = commands+i; - const char *prefix; if (strcmp(p->cmd, cmd)) continue; - - prefix = NULL; - if (p->option & RUN_SETUP) - prefix = setup_git_directory(); - if (p->option & USE_PAGER) - setup_pager(); - if ((p->option & NOT_BARE) && - (is_bare_repository() || is_inside_git_dir())) - die("%s must be run in a work tree", cmd); - trace_argv_printf(argv, argc, "trace: built-in: git"); - - exit(p->fn(argc, argv, prefix)); + exit(run_command(p, argc, argv)); } } -int main(int argc, const char **argv, char **envp) +int main(int argc, const char **argv) { const char *cmd = argv[0] ? argv[0] : "git-help"; char *slash = strrchr(cmd, '/'); @@ -358,14 +424,14 @@ int main(int argc, const char **argv, char **envp) if (!prefixcmp(cmd, "git-")) { cmd += 4; argv[0] = cmd; - handle_internal_command(argc, argv, envp); + handle_internal_command(argc, argv); die("cannot handle %s internally", cmd); } /* Look for flags.. */ argv++; argc--; - handle_options(&argv, &argc); + handle_options(&argv, &argc, NULL); if (argc > 0) { if (!prefixcmp(argv[0], "--")) argv[0] += 2; @@ -390,7 +456,7 @@ int main(int argc, const char **argv, char **envp) while (1) { /* See if it's an internal command */ - handle_internal_command(argc, argv, envp); + handle_internal_command(argc, argv); /* .. then try the external ones */ execv_git_cmd(argv); diff --git a/git.spec.in b/git.spec.in index b9dc1d59e..27182baa8 100644 --- a/git.spec.in +++ b/git.spec.in @@ -164,11 +164,10 @@ rm -rf $RPM_BUILD_ROOT %{_bindir}/git-gui %{_bindir}/git-citool %{_datadir}/git-gui/ -# Not Yet... -# %{!?_without_docs: %{_mandir}/man1/git-gui.1} -# %{!?_without_docs: %doc Documentation/git-gui.html} -# %{!?_without_docs: %{_mandir}/man1/git-citool.1} -# %{!?_without_docs: %doc Documentation/git-citool.html} +%{!?_without_docs: %{_mandir}/man1/git-gui.1*} +%{!?_without_docs: %doc Documentation/git-gui.html} +%{!?_without_docs: %{_mandir}/man1/git-citool.1*} +%{!?_without_docs: %doc Documentation/git-citool.html} %files -n gitk %defattr(-,root,root) @@ -188,6 +187,12 @@ rm -rf $RPM_BUILD_ROOT %{!?_without_docs: %doc Documentation/technical} %changelog +* Tue Jun 26 2007 Quy Tonthat <qtonthat@gmail.com> +- Fixed problems looking for wrong manpages. + +* Thu Jun 21 2007 Shawn O. Pearce <spearce@spearce.org> +- Added documentation files for git-gui + * Tue May 13 2007 Quy Tonthat <qtonthat@gmail.com> - Added lib files for git-gui - Added Documentation/technical (As needed by Git Users Manual) @@ -16,13 +16,76 @@ proc gitdir {} { } } +# A simple scheduler for compute-intensive stuff. +# The aim is to make sure that event handlers for GUI actions can +# run at least every 50-100 ms. Unfortunately fileevent handlers are +# run before X event handlers, so reading from a fast source can +# make the GUI completely unresponsive. +proc run args { + global isonrunq runq + + set script $args + if {[info exists isonrunq($script)]} return + if {$runq eq {}} { + after idle dorunq + } + lappend runq [list {} $script] + set isonrunq($script) 1 +} + +proc filerun {fd script} { + fileevent $fd readable [list filereadable $fd $script] +} + +proc filereadable {fd script} { + global runq + + fileevent $fd readable {} + if {$runq eq {}} { + after idle dorunq + } + lappend runq [list $fd $script] +} + +proc dorunq {} { + global isonrunq runq + + set tstart [clock clicks -milliseconds] + set t0 $tstart + while {$runq ne {}} { + set fd [lindex $runq 0 0] + set script [lindex $runq 0 1] + set repeat [eval $script] + set t1 [clock clicks -milliseconds] + set t [expr {$t1 - $t0}] + set runq [lrange $runq 1 end] + if {$repeat ne {} && $repeat} { + if {$fd eq {} || $repeat == 2} { + # script returns 1 if it wants to be readded + # file readers return 2 if they could do more straight away + lappend runq [list $fd $script] + } else { + fileevent $fd readable [list filereadable $fd $script] + } + } elseif {$fd eq {}} { + unset isonrunq($script) + } + set t0 $t1 + if {$t1 - $tstart >= 80} break + } + if {$runq ne {}} { + after idle dorunq + } +} + +# Start off a git rev-list process and arrange to read its output proc start_rev_list {view} { - global startmsecs nextupdate + global startmsecs global commfd leftover tclencoding datemode global viewargs viewfiles commitidx + global lookingforhead showlocalchanges set startmsecs [clock clicks -milliseconds] - set nextupdate [expr {$startmsecs + 100}] set commitidx($view) 0 set args $viewargs($view) if {$viewfiles($view) ne {}} { @@ -41,11 +104,12 @@ proc start_rev_list {view} { } set commfd($view) $fd set leftover($view) {} + set lookingforhead $showlocalchanges fconfigure $fd -blocking 0 -translation lf if {$tclencoding != {}} { fconfigure $fd -encoding $tclencoding } - fileevent $fd readable [list getcommitlines $fd $view] + filerun $fd [list getcommitlines $fd $view] nowbusy $view } @@ -72,15 +136,17 @@ proc getcommits {} { } proc getcommitlines {fd view} { - global commitlisted nextupdate + global commitlisted global leftover commfd global displayorder commitidx commitrow commitdata - global parentlist childlist children curview hlview - global vparentlist vchildlist vdisporder vcmitlisted + global parentlist children curview hlview + global vparentlist vdisporder vcmitlisted set stuff [read $fd 500000] if {$stuff == {}} { - if {![eof $fd]} return + if {![eof $fd]} { + return 1 + } global viewname unset commfd($view) notbusy $view @@ -105,9 +171,9 @@ proc getcommitlines {fd view} { error_popup $err } if {$view == $curview} { - after idle finishcommits + run chewcommits $view } - return + return 0 } set start 0 set gotsome 0 @@ -171,41 +237,52 @@ proc getcommitlines {fd view} { incr commitidx($view) if {$view == $curview} { lappend parentlist $olds - lappend childlist $children($view,$id) lappend displayorder $id lappend commitlisted $listed } else { lappend vparentlist($view) $olds - lappend vchildlist($view) $children($view,$id) lappend vdisporder($view) $id lappend vcmitlisted($view) $listed } set gotsome 1 } if {$gotsome} { - if {$view == $curview} { - while {[layoutmore $nextupdate]} doupdate - } elseif {[info exists hlview] && $view == $hlview} { - vhighlightmore - } - } - if {[clock clicks -milliseconds] >= $nextupdate} { - doupdate + run chewcommits $view } + return 2 } -proc doupdate {} { - global commfd nextupdate numcommits +proc chewcommits {view} { + global curview hlview commfd + global selectedline pending_select + + set more 0 + if {$view == $curview} { + set allread [expr {![info exists commfd($view)]}] + set tlimit [expr {[clock clicks -milliseconds] + 50}] + set more [layoutmore $tlimit $allread] + if {$allread && !$more} { + global displayorder nullid commitidx phase + global numcommits startmsecs - foreach v [array names commfd] { - fileevent $commfd($v) readable {} + if {[info exists pending_select]} { + set row [expr {[lindex $displayorder 0] eq $nullid}] + selectline $row 1 + } + if {$commitidx($curview) > 0} { + #set ms [expr {[clock clicks -milliseconds] - $startmsecs}] + #puts "overall $ms ms for $numcommits commits" + } else { + show_status "No commits selected" + } + notbusy layout + set phase {} + } } - update - set nextupdate [expr {[clock clicks -milliseconds] + 100}] - foreach v [array names commfd] { - set fd $commfd($v) - fileevent $fd readable [list getcommitlines $fd $v] + if {[info exists hlview] && $view == $hlview} { + vhighlightmore } + return $more } proc readcommit {id} { @@ -230,8 +307,9 @@ proc updatecommits {} { catch {unset selectedline} catch {unset thickerline} catch {unset viewdata($n)} - discardallcommits readrefs + changedrefs + regetallcommits showview $n } @@ -263,12 +341,16 @@ proc parsecommit {id contents listed} { } } set headline {} - # take the first line of the comment as the headline - set i [string first "\n" $comment] + # take the first non-blank line of the comment as the headline + set headline [string trimleft $comment] + set i [string first "\n" $headline] if {$i >= 0} { - set headline [string trim [string range $comment 0 $i]] - } else { - set headline $comment + set headline [string range $headline 0 $i] + } + set headline [string trimright $headline] + set i [string first "\r" $headline] + if {$i >= 0} { + set headline [string trimright [string range $headline 0 $i]] } if {!$listed} { # git rev-list indents the comment by 4 spaces; @@ -303,47 +385,39 @@ proc getcommit {id} { } proc readrefs {} { - global tagids idtags headids idheads tagcontents - global otherrefids idotherrefs mainhead + global tagids idtags headids idheads tagobjid + global otherrefids idotherrefs mainhead mainheadid foreach v {tagids idtags headids idheads otherrefids idotherrefs} { catch {unset $v} } - set refd [open [list | git show-ref] r] - while {0 <= [set n [gets $refd line]]} { - if {![regexp {^([0-9a-f]{40}) refs/([^^]*)$} $line \ - match id path]} { - continue - } - if {[regexp {^remotes/.*/HEAD$} $path match]} { - continue - } - if {![regexp {^(tags|heads)/(.*)$} $path match type name]} { - set type others - set name $path - } - if {[regexp {^remotes/} $path match]} { - set type heads - } - if {$type == "tags"} { - set tagids($name) $id - lappend idtags($id) $name - set obj {} - set type {} - set tag {} - catch { - set commit [exec git rev-parse "$id^0"] - if {$commit != $id} { - set tagids($name) $commit - lappend idtags($commit) $name - } - } - catch { - set tagcontents($name) [exec git cat-file tag $id] + set refd [open [list | git show-ref -d] r] + while {[gets $refd line] >= 0} { + if {[string index $line 40] ne " "} continue + set id [string range $line 0 39] + set ref [string range $line 41 end] + if {![string match "refs/*" $ref]} continue + set name [string range $ref 5 end] + if {[string match "remotes/*" $name]} { + if {![string match "*/HEAD" $name]} { + set headids($name) $id + lappend idheads($id) $name } - } elseif { $type == "heads" } { + } elseif {[string match "heads/*" $name]} { + set name [string range $name 6 end] set headids($name) $id lappend idheads($id) $name + } elseif {[string match "tags/*" $name]} { + # this lets refs/tags/foo^{} overwrite refs/tags/foo, + # which is what we want since the former is the commit ID + set name [string range $name 5 end] + if {[string match "*^{}" $name]} { + set name [string range $name 0 end-3] + } else { + set tagobjid($name) $id + } + set tagids($name) $id + lappend idtags($id) $name } else { set otherrefids($name) $id lappend idotherrefs($id) $name @@ -351,12 +425,40 @@ proc readrefs {} { } close $refd set mainhead {} + set mainheadid {} catch { set thehead [exec git symbolic-ref HEAD] if {[string match "refs/heads/*" $thehead]} { set mainhead [string range $thehead 11 end] + if {[info exists headids($mainhead)]} { + set mainheadid $headids($mainhead) + } + } + } +} + +# update things for a head moved to a child of its previous location +proc movehead {id name} { + global headids idheads + + removehead $headids($name) $name + set headids($name) $id + lappend idheads($id) $name +} + +# update things when a head has been removed +proc removehead {id name} { + global headids idheads + + if {$idheads($id) eq $name} { + unset idheads($id) + } else { + set i [lsearch -exact $idheads($id) $name] + if {$i >= 0} { + set idheads($id) [lreplace $idheads($id) $i $i] } } + unset headids($name) } proc show_error {w top msg} { @@ -395,14 +497,14 @@ proc confirm_popup msg { proc makewindow {} { global canv canv2 canv3 linespc charspc ctext cflist - global textfont mainfont uifont + global textfont mainfont uifont tabstop global findtype findtypemenu findloc findstring fstring geometry global entries sha1entry sha1string sha1but global maincursor textcursor curtextcursor - global rowctxmenu mergemax wrapcomment + global rowctxmenu fakerowmenu mergemax wrapcomment global highlight_files gdttype global searchstring sstring - global bgcolor fgcolor bglist fglist diffcolors + global bgcolor fgcolor bglist fglist diffcolors selectbgcolor global headctxmenu menu .bar @@ -457,15 +559,18 @@ proc makewindow {} { set cscroll .tf.histframe.csb set canv .tf.histframe.pwclist.canv canvas $canv \ + -selectbackground $selectbgcolor \ -background $bgcolor -bd 0 \ -yscrollincr $linespc -yscrollcommand "scrollcanv $cscroll" .tf.histframe.pwclist add $canv set canv2 .tf.histframe.pwclist.canv2 canvas $canv2 \ + -selectbackground $selectbgcolor \ -background $bgcolor -bd 0 -yscrollincr $linespc .tf.histframe.pwclist add $canv2 set canv3 .tf.histframe.pwclist.canv3 canvas $canv3 \ + -selectbackground $selectbgcolor \ -background $bgcolor -bd 0 -yscrollincr $linespc .tf.histframe.pwclist add $canv3 eval .tf.histframe.pwclist sash place 0 $geometry(pwsash0) @@ -612,6 +717,7 @@ proc makewindow {} { pack .bleft.mid.diff .bleft.mid.old .bleft.mid.new -side left set ctext .bleft.ctext text $ctext -background $bgcolor -foreground $fgcolor \ + -tabs "[expr {$tabstop * $charspc}]" \ -state disabled -font $textfont \ -yscrollcommand scrolltext -wrap none scrollbar .bleft.sb -command "$ctext yview" @@ -666,6 +772,7 @@ proc makewindow {} { set cflist .bright.cfiles set indent [font measure $mainfont "nn"] text $cflist \ + -selectbackground $selectbgcolor \ -background $bgcolor -foreground $fgcolor \ -font $mainfont \ -tabs [list $indent [expr {2 * $indent}]] \ @@ -766,6 +873,19 @@ proc makewindow {} { $rowctxmenu add command -label "Create new branch" -command mkbranch $rowctxmenu add command -label "Cherry-pick this commit" \ -command cherrypick + $rowctxmenu add command -label "Reset HEAD branch to here" \ + -command resethead + + set fakerowmenu .fakerowmenu + menu $fakerowmenu -tearoff 0 + $fakerowmenu add command -label "Diff this -> selected" \ + -command {diffvssel 0} + $fakerowmenu add command -label "Diff selected -> this" \ + -command {diffvssel 1} + $fakerowmenu add command -label "Make patch" -command mkpatch +# $fakerowmenu add command -label "Commit" -command {mkcommit 0} +# $fakerowmenu add command -label "Commit all" -command {mkcommit 1} +# $fakerowmenu add command -label "Revert local changes" -command revertlocal set headctxmenu .headctxmenu menu $headctxmenu -tearoff 0 @@ -820,12 +940,12 @@ proc click {w} { } proc savestuff {w} { - global canv canv2 canv3 ctext cflist mainfont textfont uifont + global canv canv2 canv3 ctext cflist mainfont textfont uifont tabstop global stuffsaved findmergefiles maxgraphpct - global maxwidth showneartags + global maxwidth showneartags showlocalchanges global viewname viewfiles viewargs viewperm nextviewnum global cmitmode wrapcomment - global colors bgcolor fgcolor diffcolors + global colors bgcolor fgcolor diffcolors selectbgcolor if {$stuffsaved} return if {![winfo viewable .]} return @@ -834,16 +954,19 @@ proc savestuff {w} { puts $f [list set mainfont $mainfont] puts $f [list set textfont $textfont] puts $f [list set uifont $uifont] + puts $f [list set tabstop $tabstop] puts $f [list set findmergefiles $findmergefiles] puts $f [list set maxgraphpct $maxgraphpct] puts $f [list set maxwidth $maxwidth] puts $f [list set cmitmode $cmitmode] puts $f [list set wrapcomment $wrapcomment] puts $f [list set showneartags $showneartags] + puts $f [list set showlocalchanges $showlocalchanges] puts $f [list set bgcolor $bgcolor] puts $f [list set fgcolor $fgcolor] puts $f [list set colors $colors] puts $f [list set diffcolors $diffcolors] + puts $f [list set selectbgcolor $selectbgcolor] puts $f "set geometry(main) [wm geometry .]" puts $f "set geometry(topwidth) [winfo width .tf]" @@ -1562,9 +1685,9 @@ proc newviewok {top n} { set viewargs($n) $newargs addviewmenu $n if {!$newishighlight} { - after idle showview $n + run showview $n } else { - after idle addvhighlight $n + run addvhighlight $n } } else { # editing an existing view @@ -1580,7 +1703,7 @@ proc newviewok {top n} { set viewfiles($n) $files set viewargs($n) $newargs if {$curview == $n} { - after idle updatecommits + run updatecommits } } } @@ -1631,16 +1754,16 @@ proc unflatten {var l} { proc showview {n} { global curview viewdata viewfiles - global displayorder parentlist childlist rowidlist rowoffsets + global displayorder parentlist rowidlist rowoffsets global colormap rowtextx commitrow nextcolor canvxmax - global numcommits rowrangelist commitlisted idrowranges + global numcommits rowrangelist commitlisted idrowranges rowchk global selectedline currentid canv canvy0 global matchinglines treediffs global pending_select phase - global commitidx rowlaidout rowoptim linesegends - global commfd nextupdate - global selectedview - global vparentlist vchildlist vdisporder vcmitlisted + global commitidx rowlaidout rowoptim + global commfd + global selectedview selectfirst + global vparentlist vdisporder vcmitlisted global hlview selectedhlview if {$n == $curview} return @@ -1657,20 +1780,22 @@ proc showview {n} { } else { set yscreen [expr {($ybot - $ytop) / 2}] } + } elseif {[info exists pending_select]} { + set selid $pending_select + unset pending_select } unselectline normalline stopfindproc if {$curview >= 0} { set vparentlist($curview) $parentlist - set vchildlist($curview) $childlist set vdisporder($curview) $displayorder set vcmitlisted($curview) $commitlisted if {$phase ne {}} { set viewdata($curview) \ [list $phase $rowidlist $rowoffsets $rowrangelist \ [flatten idrowranges] [flatten idinlist] \ - $rowlaidout $rowoptim $numcommits $linesegends] + $rowlaidout $rowoptim $numcommits] } elseif {![info exists viewdata($curview)] || [lindex $viewdata($curview) 0] ne {}} { set viewdata($curview) \ @@ -1691,7 +1816,9 @@ proc showview {n} { .bar.view entryconf Delete* -state [expr {$n == 0? "disabled": "normal"}] if {![info exists viewdata($n)]} { - set pending_select $selid + if {$selid ne {}} { + set pending_select $selid + } getcommits return } @@ -1700,7 +1827,6 @@ proc showview {n} { set phase [lindex $v 0] set displayorder $vdisporder($n) set parentlist $vparentlist($n) - set childlist $vchildlist($n) set commitlisted $vcmitlisted($n) set rowidlist [lindex $v 1] set rowoffsets [lindex $v 2] @@ -1714,7 +1840,7 @@ proc showview {n} { set rowlaidout [lindex $v 6] set rowoptim [lindex $v 7] set numcommits [lindex $v 8] - set linesegends [lindex $v 9] + catch {unset rowchk} } catch {unset colormap} @@ -1725,7 +1851,8 @@ proc showview {n} { set row 0 setcanvscroll set yf 0 - set row 0 + set row {} + set selectfirst 0 if {$selid ne {} && [info exists commitrow($n,$selid)]} { set row $commitrow($n,$selid) # try to get the selected row in the same position on the screen @@ -1738,16 +1865,23 @@ proc showview {n} { } allcanvs yview moveto $yf drawvisible - selectline $row 0 + if {$row ne {}} { + selectline $row 0 + } elseif {$selid ne {}} { + set pending_select $selid + } else { + set row [expr {[lindex $displayorder 0] eq $nullid}] + if {$row < $numcommits} { + selectline $row 0 + } else { + set selectfirst 1 + } + } if {$phase ne {}} { if {$phase eq "getcommits"} { show_status "Reading commits..." } - if {[info exists commfd($n)]} { - layoutmore {} - } else { - finishcommits - } + run chewcommits $n } elseif {$numcommits == 0} { show_status "No commits selected" } @@ -1825,7 +1959,6 @@ proc addvhighlight {n} { if {$n != $curview && ![info exists viewdata($n)]} { set viewdata($n) [list getcommits {{}} {{}} {} {} {} 0 0 0 {}] set vparentlist($n) {} - set vchildlist($n) {} set vdisporder($n) {} set vcmitlisted($n) {} start_rev_list $n @@ -1935,7 +2068,7 @@ proc do_file_hl {serial} { set cmd [concat | git diff-tree -r -s --stdin $gdtargs] set filehighlight [open $cmd r+] fconfigure $filehighlight -blocking 0 - fileevent $filehighlight readable readfhighlight + filerun $filehighlight readfhighlight set fhl_list {} drawvisible flushhighlights @@ -1963,7 +2096,11 @@ proc readfhighlight {} { global filehighlight fhighlights commitrow curview mainfont iddrawn global fhl_list - while {[gets $filehighlight line] >= 0} { + if {![info exists filehighlight]} { + return 0 + } + set nr 0 + while {[incr nr] <= 100 && [gets $filehighlight line] >= 0} { set line [string trim $line] set i [lsearch -exact $fhl_list $line] if {$i < 0} continue @@ -1987,8 +2124,10 @@ proc readfhighlight {} { puts "oops, git diff-tree died" catch {close $filehighlight} unset filehighlight + return 0 } next_hlcont + return 1 } proc find_change {name ix op} { @@ -2055,7 +2194,7 @@ proc vrel_change {name ix op} { rhighlight_none if {$highlight_related ne "None"} { - after idle drawvisible + run drawvisible } } @@ -2070,7 +2209,7 @@ proc rhighlight_sel {a} { set anc_todo [list $a] if {$highlight_related ne "None"} { rhighlight_none - after idle drawvisible + run drawvisible } } @@ -2288,17 +2427,15 @@ proc ntimes {n o} { } proc usedinrange {id l1 l2} { - global children commitrow childlist curview + global children commitrow curview if {[info exists commitrow($curview,$id)]} { set r $commitrow($curview,$id) if {$l1 <= $r && $r <= $l2} { return [expr {$r - $l1 + 1}] } - set kids [lindex $childlist $r] - } else { - set kids $children($curview,$id) } + set kids $children($curview,$id) foreach c $kids { set r $commitrow($curview,$c) if {$l1 <= $r && $r <= $l2} { @@ -2341,7 +2478,7 @@ proc sanity {row {full 0}} { } proc makeuparrow {oid x y z} { - global rowidlist rowoffsets uparrowlen idrowranges + global rowidlist rowoffsets uparrowlen idrowranges displayorder for {set i 1} {$i < $uparrowlen && $y > 1} {incr i} { incr y -1 @@ -2364,7 +2501,7 @@ proc makeuparrow {oid x y z} { } set tmp [lreplace [lindex $rowoffsets $y] $x $x {}] lset rowoffsets $y [incrange $tmp [expr {$x+1}] -1] - lappend idrowranges($oid) $y + lappend idrowranges($oid) [lindex $displayorder $y] } proc initlayout {} { @@ -2373,15 +2510,14 @@ proc initlayout {} { global idinlist rowchk rowrangelist idrowranges global numcommits canvxmax canv global nextcolor - global parentlist childlist children + global parentlist global colormap rowtextx - global linesegends + global selectfirst set numcommits 0 set displayorder {} set commitlisted {} set parentlist {} - set childlist {} set rowrangelist {} set nextcolor 0 set rowidlist {{}} @@ -2394,7 +2530,7 @@ proc initlayout {} { catch {unset colormap} catch {unset rowtextx} catch {unset idrowranges} - set linesegends {} + set selectfirst 1 } proc setcanvscroll {} { @@ -2425,15 +2561,18 @@ proc visiblerows {} { return [list $r0 $r1] } -proc layoutmore {tmax} { +proc layoutmore {tmax allread} { global rowlaidout rowoptim commitidx numcommits optim_delay - global uparrowlen curview + global uparrowlen curview rowidlist idinlist + set showlast 0 + set showdelay $optim_delay + set optdelay [expr {$uparrowlen + 1}] while {1} { - if {$rowoptim - $optim_delay > $numcommits} { - showstuff [expr {$rowoptim - $optim_delay}] - } elseif {$rowlaidout - $uparrowlen - 1 > $rowoptim} { - set nr [expr {$rowlaidout - $uparrowlen - 1 - $rowoptim}] + if {$rowoptim - $showdelay > $numcommits} { + showstuff [expr {$rowoptim - $showdelay}] $showlast + } elseif {$rowlaidout - $optdelay > $rowoptim} { + set nr [expr {$rowlaidout - $optdelay - $rowoptim}] if {$nr > 100} { set nr 100 } @@ -2447,10 +2586,24 @@ proc layoutmore {tmax} { set nr 150 } set row $rowlaidout - set rowlaidout [layoutrows $row [expr {$row + $nr}] 0] + set rowlaidout [layoutrows $row [expr {$row + $nr}] $allread] if {$rowlaidout == $row} { return 0 } + } elseif {$allread} { + set optdelay 0 + set nrows $commitidx($curview) + if {[lindex $rowidlist $nrows] ne {} || + [array names idinlist] ne {}} { + layouttail + set rowlaidout $commitidx($curview) + } elseif {$rowoptim == $nrows} { + set showdelay 0 + set showlast 1 + if {$numcommits == $nrows} { + return 0 + } + } } else { return 0 } @@ -2460,57 +2613,116 @@ proc layoutmore {tmax} { } } -proc showstuff {canshow} { - global numcommits commitrow pending_select selectedline - global linesegends idrowranges idrangedrawn curview +proc showstuff {canshow last} { + global numcommits commitrow pending_select selectedline curview + global lookingforhead mainheadid displayorder nullid selectfirst + global lastscrollset if {$numcommits == 0} { global phase set phase "incrdraw" allcanvs delete all } - set row $numcommits + set r0 $numcommits + set prev $numcommits set numcommits $canshow - setcanvscroll + set t [clock clicks -milliseconds] + if {$prev < 100 || $last || $t - $lastscrollset > 500} { + set lastscrollset $t + setcanvscroll + } set rows [visiblerows] - set r0 [lindex $rows 0] set r1 [lindex $rows 1] - set selrow -1 - for {set r $row} {$r < $canshow} {incr r} { - foreach id [lindex $linesegends [expr {$r+1}]] { - set i -1 - foreach {s e} [rowranges $id] { - incr i - if {$e ne {} && $e < $numcommits && $s <= $r1 && $e >= $r0 - && ![info exists idrangedrawn($id,$i)]} { - drawlineseg $id $i - set idrangedrawn($id,$i) 1 - } - } - } - } - if {$canshow > $r1} { - set canshow $r1 + if {$r1 >= $canshow} { + set r1 [expr {$canshow - 1}] } - while {$row < $canshow} { - drawcmitrow $row - incr row + if {$r0 <= $r1} { + drawcommits $r0 $r1 } if {[info exists pending_select] && [info exists commitrow($curview,$pending_select)] && $commitrow($curview,$pending_select) < $numcommits} { selectline $commitrow($curview,$pending_select) 1 } - if {![info exists selectedline] && ![info exists pending_select]} { - selectline 0 1 + if {$selectfirst} { + if {[info exists selectedline] || [info exists pending_select]} { + set selectfirst 0 + } else { + set l [expr {[lindex $displayorder 0] eq $nullid}] + selectline $l 1 + set selectfirst 0 + } + } + if {$lookingforhead && [info exists commitrow($curview,$mainheadid)] + && ($last || $commitrow($curview,$mainheadid) < $numcommits - 1)} { + set lookingforhead 0 + dodiffindex + } +} + +proc doshowlocalchanges {} { + global lookingforhead curview mainheadid phase commitrow + + if {[info exists commitrow($curview,$mainheadid)] && + ($phase eq {} || $commitrow($curview,$mainheadid) < $numcommits - 1)} { + dodiffindex + } elseif {$phase ne {}} { + set lookingforhead 1 + } +} + +proc dohidelocalchanges {} { + global lookingforhead localrow lserial + + set lookingforhead 0 + if {$localrow >= 0} { + removerow $localrow + set localrow -1 + } + incr lserial +} + +# spawn off a process to do git diff-index HEAD +proc dodiffindex {} { + global localrow lserial + + incr lserial + set localrow -1 + set fd [open "|git diff-index HEAD" r] + fconfigure $fd -blocking 0 + filerun $fd [list readdiffindex $fd $lserial] +} + +proc readdiffindex {fd serial} { + global localrow commitrow mainheadid nullid curview + global commitinfo commitdata lserial + + if {[gets $fd line] < 0} { + if {[eof $fd]} { + close $fd + return 0 + } + return 1 + } + # we only need to see one line and we don't really care what it says... + close $fd + + if {$serial == $lserial && $localrow == -1} { + # add the line for the local diff to the graph + set localrow $commitrow($curview,$mainheadid) + set hl "Local uncommitted changes" + set commitinfo($nullid) [list $hl {} {} {} {} " $hl\n"] + set commitdata($nullid) "\n $hl\n" + insertrow $localrow $nullid } + return 0 } proc layoutrows {row endrow last} { global rowidlist rowoffsets displayorder global uparrowlen downarrowlen maxwidth mingaplen - global childlist parentlist - global idrowranges linesegends + global children parentlist + global idrowranges global commitidx curview global idinlist rowchk rowrangelist @@ -2527,7 +2739,6 @@ proc layoutrows {row endrow last} { lappend oldolds $p } } - set lse {} set nev [expr {[llength $idlist] + [llength $newolds] + [llength $oldolds] - $maxwidth + 1}] if {$nev > 0} { @@ -2544,8 +2755,7 @@ proc layoutrows {row endrow last} { set offs [incrange $offs $x 1] set idinlist($i) 0 set rm1 [expr {$row - 1}] - lappend lse $i - lappend idrowranges($i) $rm1 + lappend idrowranges($i) [lindex $displayorder $rm1] if {[incr nev -1] <= 0} break continue } @@ -2555,14 +2765,13 @@ proc layoutrows {row endrow last} { lset rowidlist $row $idlist lset rowoffsets $row $offs } - lappend linesegends $lse set col [lsearch -exact $idlist $id] if {$col < 0} { set col [llength $idlist] lappend idlist $id lset rowidlist $row $idlist set z {} - if {[lindex $childlist $row] ne {}} { + if {$children($curview,$id) ne {}} { set z [expr {[llength [lindex $rowidlist [expr {$row-1}]]] - $col}] unset idinlist($id) } @@ -2577,7 +2786,7 @@ proc layoutrows {row endrow last} { set ranges {} if {[info exists idrowranges($id)]} { set ranges $idrowranges($id) - lappend ranges $row + lappend ranges $id unset idrowranges($id) } lappend rowrangelist $ranges @@ -2602,7 +2811,7 @@ proc layoutrows {row endrow last} { } foreach i $newolds { set idinlist($i) 1 - set idrowranges($i) $row + set idrowranges($i) $id } incr col $l foreach oid $oldolds { @@ -2621,7 +2830,7 @@ proc layoutrows {row endrow last} { proc addextraid {id row} { global displayorder commitrow commitinfo global commitidx commitlisted - global parentlist childlist children curview + global parentlist children curview incr commitidx($curview) lappend displayorder $id @@ -2635,7 +2844,6 @@ proc addextraid {id row} { if {![info exists children($curview,$id)]} { set children($curview,$id) {} } - lappend childlist $children($curview,$id) } proc layouttail {} { @@ -2660,6 +2868,7 @@ proc layouttail {} { } foreach id [array names idinlist] { + unset idinlist($id) addextraid $id $row lset rowidlist $row [list $id] lset rowoffsets $row 0 @@ -2683,7 +2892,7 @@ proc insert_pad {row col npad} { } proc optimize_rows {row col endrow} { - global rowidlist rowoffsets idrowranges displayorder + global rowidlist rowoffsets displayorder for {} {$row < $endrow} {incr row} { set idlist [lindex $rowidlist $row] @@ -2707,7 +2916,13 @@ proc optimize_rows {row col endrow} { set isarrow 1 } } + # Looking at lines from this row to the previous row, + # make them go straight up if they end in an arrow on + # the previous row; otherwise make them go straight up + # or at 45 degrees. if {$z < -1 || ($z < 0 && $isarrow)} { + # Line currently goes left too much; + # insert pads in the previous row, then optimize it set npad [expr {-1 - $z + $isarrow}] set offs [incrange $offs $col $npad] insert_pad $y0 $x0 $npad @@ -2718,6 +2933,8 @@ proc optimize_rows {row col endrow} { set x0 [expr {$col + $z}] set z0 [lindex $rowoffsets $y0 $x0] } elseif {$z > 1 || ($z > 0 && $isarrow)} { + # Line currently goes right too much; + # insert pads in this line and adjust the next's rowoffsets set npad [expr {$z - 1 + $isarrow}] set y1 [expr {$row + 1}] set offs2 [lindex $rowoffsets $y1] @@ -2748,6 +2965,7 @@ proc optimize_rows {row col endrow} { set z0 [expr {$xc - $x0}] } } + # avoid lines jigging left then immediately right if {$z0 ne {} && $z < 0 && $z0 > 0} { insert_pad $y0 $x0 1 set offs [incrange $offs $col 1] @@ -2756,6 +2974,7 @@ proc optimize_rows {row col endrow} { } if {!$haspad} { set o {} + # Find the first column that doesn't have a line going right for {set col [llength $idlist]} {[incr col -1] >= 0} {} { set o [lindex $offs $col] if {$o eq {}} { @@ -2774,6 +2993,8 @@ proc optimize_rows {row col endrow} { } if {$o eq {} || $o <= 0} break } + # Insert a pad at that column as long as it has a line and + # isn't the last column, and adjust the next row' offsets if {$o ne {} && [incr col] < [llength $idlist]} { set y1 [expr {$row + 1}] set offs2 [lindex $rowoffsets $y1] @@ -2827,99 +3048,216 @@ proc rowranges {id} { } elseif {[info exists idrowranges($id)]} { set ranges $idrowranges($id) } - return $ranges + set linenos {} + foreach rid $ranges { + lappend linenos $commitrow($curview,$rid) + } + if {$linenos ne {}} { + lset linenos 0 [expr {[lindex $linenos 0] + 1}] + } + return $linenos } -proc drawlineseg {id i} { - global rowoffsets rowidlist - global displayorder - global canv colormap linespc - global numcommits commitrow curview +# work around tk8.4 refusal to draw arrows on diagonal segments +proc adjarrowhigh {coords} { + global linespc - set ranges [rowranges $id] - set downarrow 1 - if {[info exists commitrow($curview,$id)] - && $commitrow($curview,$id) < $numcommits} { - set downarrow [expr {$i < [llength $ranges] / 2 - 1}] - } else { - set downarrow 1 - } - set startrow [lindex $ranges [expr {2 * $i}]] - set row [lindex $ranges [expr {2 * $i + 1}]] - if {$startrow == $row} return - assigncolor $id - set coords {} - set col [lsearch -exact [lindex $rowidlist $row] $id] - if {$col < 0} { - puts "oops: drawline: id $id not on row $row" - return + set x0 [lindex $coords 0] + set x1 [lindex $coords 2] + if {$x0 != $x1} { + set y0 [lindex $coords 1] + set y1 [lindex $coords 3] + if {$y0 - $y1 <= 2 * $linespc && $x1 == [lindex $coords 4]} { + # we have a nearby vertical segment, just trim off the diag bit + set coords [lrange $coords 2 end] + } else { + set slope [expr {($x0 - $x1) / ($y0 - $y1)}] + set xi [expr {$x0 - $slope * $linespc / 2}] + set yi [expr {$y0 - $linespc / 2}] + set coords [lreplace $coords 0 1 $xi $y0 $xi $yi] + } } - set lasto {} - set ns 0 + return $coords +} + +proc drawlineseg {id row endrow arrowlow} { + global rowidlist displayorder iddrawn linesegs + global canv colormap linespc curview maxlinelen + + set cols [list [lsearch -exact [lindex $rowidlist $row] $id]] + set le [expr {$row + 1}] + set arrowhigh 1 while {1} { - set o [lindex $rowoffsets $row $col] - if {$o eq {}} break - if {$o ne $lasto} { - # changing direction - set x [xc $row $col] - set y [yc $row] - lappend coords $x $y - set lasto $o + set c [lsearch -exact [lindex $rowidlist $le] $id] + if {$c < 0} { + incr le -1 + break + } + lappend cols $c + set x [lindex $displayorder $le] + if {$x eq $id} { + set arrowhigh 0 + break } - incr col $o - incr row -1 + if {[info exists iddrawn($x)] || $le == $endrow} { + set c [lsearch -exact [lindex $rowidlist [expr {$le+1}]] $id] + if {$c >= 0} { + lappend cols $c + set arrowhigh 0 + } + break + } + incr le } - set x [xc $row $col] - set y [yc $row] - lappend coords $x $y - if {$i == 0} { - # draw the link to the first child as part of this line - incr row -1 - set child [lindex $displayorder $row] - set ccol [lsearch -exact [lindex $rowidlist $row] $child] - if {$ccol >= 0} { - set x [xc $row $ccol] - set y [yc $row] - if {$ccol < $col - 1} { - lappend coords [xc $row [expr {$col - 1}]] [yc $row] - } elseif {$ccol > $col + 1} { - lappend coords [xc $row [expr {$col + 1}]] [yc $row] + if {$le <= $row} { + return $row + } + + set lines {} + set i 0 + set joinhigh 0 + if {[info exists linesegs($id)]} { + set lines $linesegs($id) + foreach li $lines { + set r0 [lindex $li 0] + if {$r0 > $row} { + if {$r0 == $le && [lindex $li 1] - $row <= $maxlinelen} { + set joinhigh 1 + } + break } - lappend coords $x $y - } - } - if {[llength $coords] < 4} return - if {$downarrow} { - # This line has an arrow at the lower end: check if the arrow is - # on a diagonal segment, and if so, work around the Tk 8.4 - # refusal to draw arrows on diagonal lines. - set x0 [lindex $coords 0] - set x1 [lindex $coords 2] - if {$x0 != $x1} { - set y0 [lindex $coords 1] - set y1 [lindex $coords 3] - if {$y0 - $y1 <= 2 * $linespc && $x1 == [lindex $coords 4]} { - # we have a nearby vertical segment, just trim off the diag bit - set coords [lrange $coords 2 end] + incr i + } + } + set joinlow 0 + if {$i > 0} { + set li [lindex $lines [expr {$i-1}]] + set r1 [lindex $li 1] + if {$r1 == $row && $le - [lindex $li 0] <= $maxlinelen} { + set joinlow 1 + } + } + + set x [lindex $cols [expr {$le - $row}]] + set xp [lindex $cols [expr {$le - 1 - $row}]] + set dir [expr {$xp - $x}] + if {$joinhigh} { + set ith [lindex $lines $i 2] + set coords [$canv coords $ith] + set ah [$canv itemcget $ith -arrow] + set arrowhigh [expr {$ah eq "first" || $ah eq "both"}] + set x2 [lindex $cols [expr {$le + 1 - $row}]] + if {$x2 ne {} && $x - $x2 == $dir} { + set coords [lrange $coords 0 end-2] + } + } else { + set coords [list [xc $le $x] [yc $le]] + } + if {$joinlow} { + set itl [lindex $lines [expr {$i-1}] 2] + set al [$canv itemcget $itl -arrow] + set arrowlow [expr {$al eq "last" || $al eq "both"}] + } elseif {$arrowlow && + [lsearch -exact [lindex $rowidlist [expr {$row-1}]] $id] >= 0} { + set arrowlow 0 + } + set arrow [lindex {none first last both} [expr {$arrowhigh + 2*$arrowlow}]] + for {set y $le} {[incr y -1] > $row} {} { + set x $xp + set xp [lindex $cols [expr {$y - 1 - $row}]] + set ndir [expr {$xp - $x}] + if {$dir != $ndir || $xp < 0} { + lappend coords [xc $y $x] [yc $y] + } + set dir $ndir + } + if {!$joinlow} { + if {$xp < 0} { + # join parent line to first child + set ch [lindex $displayorder $row] + set xc [lsearch -exact [lindex $rowidlist $row] $ch] + if {$xc < 0} { + puts "oops: drawlineseg: child $ch not on row $row" + } else { + if {$xc < $x - 1} { + lappend coords [xc $row [expr {$x-1}]] [yc $row] + } elseif {$xc > $x + 1} { + lappend coords [xc $row [expr {$x+1}]] [yc $row] + } + set x $xc + } + lappend coords [xc $row $x] [yc $row] + } else { + set xn [xc $row $xp] + set yn [yc $row] + # work around tk8.4 refusal to draw arrows on diagonal segments + if {$arrowlow && $xn != [lindex $coords end-1]} { + if {[llength $coords] < 4 || + [lindex $coords end-3] != [lindex $coords end-1] || + [lindex $coords end] - $yn > 2 * $linespc} { + set xn [xc $row [expr {$xp - 0.5 * $dir}]] + set yo [yc [expr {$row + 0.5}]] + lappend coords $xn $yo $xn $yn + } } else { - set slope [expr {($x0 - $x1) / ($y0 - $y1)}] - set xi [expr {$x0 - $slope * $linespc / 2}] - set yi [expr {$y0 - $linespc / 2}] - set coords [lreplace $coords 0 1 $xi $y0 $xi $yi] + lappend coords $xn $yn + } + } + if {!$joinhigh} { + if {$arrowhigh} { + set coords [adjarrowhigh $coords] + } + assigncolor $id + set t [$canv create line $coords -width [linewidth $id] \ + -fill $colormap($id) -tags lines.$id -arrow $arrow] + $canv lower $t + bindline $t $id + set lines [linsert $lines $i [list $row $le $t]] + } else { + $canv coords $ith $coords + if {$arrow ne $ah} { + $canv itemconf $ith -arrow $arrow } + lset lines $i 0 $row + } + } else { + set xo [lsearch -exact [lindex $rowidlist [expr {$row - 1}]] $id] + set ndir [expr {$xo - $xp}] + set clow [$canv coords $itl] + if {$dir == $ndir} { + set clow [lrange $clow 2 end] + } + set coords [concat $coords $clow] + if {!$joinhigh} { + lset lines [expr {$i-1}] 1 $le + if {$arrowhigh} { + set coords [adjarrowhigh $coords] + } + } else { + # coalesce two pieces + $canv delete $ith + set b [lindex $lines [expr {$i-1}] 0] + set e [lindex $lines $i 1] + set lines [lreplace $lines [expr {$i-1}] $i [list $b $e $itl]] + } + $canv coords $itl $coords + if {$arrow ne $al} { + $canv itemconf $itl -arrow $arrow } } - set arrow [expr {2 * ($i > 0) + $downarrow}] - set arrow [lindex {none first last both} $arrow] - set t [$canv create line $coords -width [linewidth $id] \ - -fill $colormap($id) -tags lines.$id -arrow $arrow] - $canv lower $t - bindline $t $id + + set linesegs($id) $lines + return $le } -proc drawparentlinks {id row col olds} { - global rowidlist canv colormap +proc drawparentlinks {id row} { + global rowidlist canv colormap curview parentlist + global idpos + set rowids [lindex $rowidlist $row] + set col [lsearch -exact $rowids $id] + if {$col < 0} return + set olds [lindex $parentlist $row] set row2 [expr {$row + 1}] set x [xc $row $col] set y [yc $row] @@ -2937,9 +3275,7 @@ proc drawparentlinks {id row col olds} { if {$x2 > $rmx} { set rmx $x2 } - set ranges [rowranges $p] - if {$ranges ne {} && $row2 == [lindex $ranges 0] - && $row2 < [lindex $ranges 1]} { + if {[lsearch -exact $rowids $p] < 0} { # drawlineseg will do this one for us continue } @@ -2957,40 +3293,30 @@ proc drawparentlinks {id row col olds} { $canv lower $t bindline $t $p } - return $rmx + if {$rmx > [lindex $idpos($id) 1]} { + lset idpos($id) 1 $rmx + redrawtags $id + } } proc drawlines {id} { - global colormap canv - global idrangedrawn - global children iddrawn commitrow rowidlist curview - - $canv delete lines.$id - set nr [expr {[llength [rowranges $id]] / 2}] - for {set i 0} {$i < $nr} {incr i} { - if {[info exists idrangedrawn($id,$i)]} { - drawlineseg $id $i - } - } - foreach child $children($curview,$id) { - if {[info exists iddrawn($child)]} { - set row $commitrow($curview,$child) - set col [lsearch -exact [lindex $rowidlist $row] $child] - if {$col >= 0} { - drawparentlinks $child $row $col [list $id] - } - } - } + global canv + + $canv itemconf lines.$id -width [linewidth $id] } -proc drawcmittext {id row col rmx} { +proc drawcmittext {id row col} { global linespc canv canv2 canv3 canvy0 fgcolor - global commitlisted commitinfo rowidlist + global commitlisted commitinfo rowidlist parentlist global rowtextx idpos idtags idheads idotherrefs global linehtag linentag linedtag - global mainfont canvxmax boldrows boldnamerows fgcolor + global mainfont canvxmax boldrows boldnamerows fgcolor nullid - set ofill [expr {[lindex $commitlisted $row]? "blue": "white"}] + if {$id eq $nullid} { + set ofill red + } else { + set ofill [expr {[lindex $commitlisted $row]? "blue": "white"}] + } set x [xc $row $col] set y [yc $row] set orad [expr {$linespc / 3}] @@ -2999,10 +3325,18 @@ proc drawcmittext {id row col rmx} { -fill $ofill -outline $fgcolor -width 1 -tags circle] $canv raise $t $canv bind $t <1> {selcanvline {} %x %y} - set xt [xc $row [llength [lindex $rowidlist $row]]] - if {$xt < $rmx} { - set xt $rmx + set rmx [llength [lindex $rowidlist $row]] + set olds [lindex $parentlist $row] + if {$olds ne {}} { + set nextids [lindex $rowidlist [expr {$row + 1}]] + foreach p $olds { + set i [lsearch -exact $nextids $p] + if {$i > $rmx} { + set rmx $i + } + } } + set xt [xc $row $rmx] set rowtextx($row) $xt set idpos($id) [list $x $xt $y] if {[info exists idtags($id)] || [info exists idheads($id)] @@ -3040,29 +3374,13 @@ proc drawcmittext {id row col rmx} { proc drawcmitrow {row} { global displayorder rowidlist - global idrangedrawn iddrawn + global iddrawn global commitinfo parentlist numcommits global filehighlight fhighlights findstring nhighlights global hlview vhighlights global highlight_related rhighlights if {$row >= $numcommits} return - foreach id [lindex $rowidlist $row] { - if {$id eq {}} continue - set i -1 - foreach {s e} [rowranges $id] { - incr i - if {$row < $s} continue - if {$e eq {}} break - if {$row <= $e} { - if {$e < $numcommits && ![info exists idrangedrawn($id,$i)]} { - drawlineseg $id $i - set idrangedrawn($id,$i) 1 - } - break - } - } - } set id [lindex $displayorder $row] if {[info exists hlview] && ![info exists vhighlights($row)]} { @@ -3087,49 +3405,99 @@ proc drawcmitrow {row} { getcommit $id } assigncolor $id - set olds [lindex $parentlist $row] - if {$olds ne {}} { - set rmx [drawparentlinks $id $row $col $olds] - } else { - set rmx 0 - } - drawcmittext $id $row $col $rmx + drawcmittext $id $row $col set iddrawn($id) 1 } -proc drawfrac {f0 f1} { - global numcommits canv - global linespc +proc drawcommits {row {endrow {}}} { + global numcommits iddrawn displayorder curview + global parentlist rowidlist - set ymax [lindex [$canv cget -scrollregion] 3] - if {$ymax eq {} || $ymax == 0} return - set y0 [expr {int($f0 * $ymax)}] - set row [expr {int(($y0 - 3) / $linespc) - 1}] if {$row < 0} { set row 0 } - set y1 [expr {int($f1 * $ymax)}] - set endrow [expr {int(($y1 - 3) / $linespc) + 1}] + if {$endrow eq {}} { + set endrow $row + } if {$endrow >= $numcommits} { set endrow [expr {$numcommits - 1}] } - for {} {$row <= $endrow} {incr row} { - drawcmitrow $row + + # make the lines join to already-drawn rows either side + set r [expr {$row - 1}] + if {$r < 0 || ![info exists iddrawn([lindex $displayorder $r])]} { + set r $row + } + set er [expr {$endrow + 1}] + if {$er >= $numcommits || + ![info exists iddrawn([lindex $displayorder $er])]} { + set er $endrow + } + for {} {$r <= $er} {incr r} { + set id [lindex $displayorder $r] + set wasdrawn [info exists iddrawn($id)] + if {!$wasdrawn} { + drawcmitrow $r + } + if {$r == $er} break + set nextid [lindex $displayorder [expr {$r + 1}]] + if {$wasdrawn && [info exists iddrawn($nextid)]} { + catch {unset prevlines} + continue + } + drawparentlinks $id $r + + if {[info exists lineends($r)]} { + foreach lid $lineends($r) { + unset prevlines($lid) + } + } + set rowids [lindex $rowidlist $r] + foreach lid $rowids { + if {$lid eq {}} continue + if {$lid eq $id} { + # see if this is the first child of any of its parents + foreach p [lindex $parentlist $r] { + if {[lsearch -exact $rowids $p] < 0} { + # make this line extend up to the child + set le [drawlineseg $p $r $er 0] + lappend lineends($le) $p + set prevlines($p) 1 + } + } + } elseif {![info exists prevlines($lid)]} { + set le [drawlineseg $lid $r $er 1] + lappend lineends($le) $lid + set prevlines($lid) 1 + } + } } } +proc drawfrac {f0 f1} { + global canv linespc + + set ymax [lindex [$canv cget -scrollregion] 3] + if {$ymax eq {} || $ymax == 0} return + set y0 [expr {int($f0 * $ymax)}] + set row [expr {int(($y0 - 3) / $linespc) - 1}] + set y1 [expr {int($f1 * $ymax)}] + set endrow [expr {int(($y1 - 3) / $linespc) + 1}] + drawcommits $row $endrow +} + proc drawvisible {} { global canv eval drawfrac [$canv yview] } proc clear_display {} { - global iddrawn idrangedrawn + global iddrawn linesegs global vhighlights fhighlights nhighlights rhighlights allcanvs delete all catch {unset iddrawn} - catch {unset idrangedrawn} + catch {unset linesegs} catch {unset vhighlights} catch {unset fhighlights} catch {unset nhighlights} @@ -3356,27 +3724,14 @@ proc show_status {msg} { -tags text -fill $fgcolor } -proc finishcommits {} { - global commitidx phase curview - global pending_select - - if {$commitidx($curview) > 0} { - drawrest - } else { - show_status "No commits selected" - } - set phase {} - catch {unset pending_select} -} - # Insert a new commit as the child of the commit on row $row. # The new commit will be displayed on row $row and the commits # on that row and below will move down one row. proc insertrow {row newcmit} { - global displayorder parentlist childlist commitlisted + global displayorder parentlist commitlisted children global commitrow curview rowidlist rowoffsets numcommits - global rowrangelist idrowranges rowlaidout rowoptim numcommits - global linesegends selectedline + global rowrangelist rowlaidout rowoptim numcommits + global selectedline rowchk commitidx if {$row >= $numcommits} { puts "oops, inserting new row $row but only have $numcommits rows" @@ -3385,16 +3740,17 @@ proc insertrow {row newcmit} { set p [lindex $displayorder $row] set displayorder [linsert $displayorder $row $newcmit] set parentlist [linsert $parentlist $row $p] - set kids [lindex $childlist $row] + set kids $children($curview,$p) lappend kids $newcmit - lset childlist $row $kids - set childlist [linsert $childlist $row {}] + set children($curview,$p) $kids + set children($curview,$newcmit) {} set commitlisted [linsert $commitlisted $row 1] set l [llength $displayorder] for {set r $row} {$r < $l} {incr r} { set id [lindex $displayorder $r] set commitrow($curview,$id) $r } + incr commitidx($curview) set idlist [lindex $rowidlist $row] set offs [lindex $rowoffsets $row] @@ -3419,47 +3775,18 @@ proc insertrow {row newcmit} { set rowoffsets [linsert $rowoffsets [expr {$row+1}] $newoffs] set rowrangelist [linsert $rowrangelist $row {}] - set l [llength $rowrangelist] - for {set r 0} {$r < $l} {incr r} { - set ranges [lindex $rowrangelist $r] - if {$ranges ne {} && [lindex $ranges end] >= $row} { - set newranges {} - foreach x $ranges { - if {$x >= $row} { - lappend newranges [expr {$x + 1}] - } else { - lappend newranges $x - } - } - lset rowrangelist $r $newranges - } - } if {[llength $kids] > 1} { set rp1 [expr {$row + 1}] set ranges [lindex $rowrangelist $rp1] if {$ranges eq {}} { - set ranges [list $row $rp1] - } elseif {[lindex $ranges end-1] == $rp1} { - lset ranges end-1 $row + set ranges [list $newcmit $p] + } elseif {[lindex $ranges end-1] eq $p} { + lset ranges end-1 $newcmit } lset rowrangelist $rp1 $ranges } - foreach id [array names idrowranges] { - set ranges $idrowranges($id) - if {$ranges ne {} && [lindex $ranges end] >= $row} { - set newranges {} - foreach x $ranges { - if {$x >= $row} { - lappend newranges [expr {$x + 1}] - } else { - lappend newranges $x - } - } - set idrowranges($id) $newranges - } - } - set linesegends [linsert $linesegends $row {}] + catch {unset rowchk} incr rowlaidout incr rowoptim @@ -3471,6 +3798,65 @@ proc insertrow {row newcmit} { redisplay } +# Remove a commit that was inserted with insertrow on row $row. +proc removerow {row} { + global displayorder parentlist commitlisted children + global commitrow curview rowidlist rowoffsets numcommits + global rowrangelist idrowranges rowlaidout rowoptim numcommits + global linesegends selectedline rowchk commitidx + + if {$row >= $numcommits} { + puts "oops, removing row $row but only have $numcommits rows" + return + } + set rp1 [expr {$row + 1}] + set id [lindex $displayorder $row] + set p [lindex $parentlist $row] + set displayorder [lreplace $displayorder $row $row] + set parentlist [lreplace $parentlist $row $row] + set commitlisted [lreplace $commitlisted $row $row] + set kids $children($curview,$p) + set i [lsearch -exact $kids $id] + if {$i >= 0} { + set kids [lreplace $kids $i $i] + set children($curview,$p) $kids + } + set l [llength $displayorder] + for {set r $row} {$r < $l} {incr r} { + set id [lindex $displayorder $r] + set commitrow($curview,$id) $r + } + incr commitidx($curview) -1 + + set rowidlist [lreplace $rowidlist $row $row] + set rowoffsets [lreplace $rowoffsets $rp1 $rp1] + if {$kids ne {}} { + set offs [lindex $rowoffsets $row] + set offs [lreplace $offs end end] + lset rowoffsets $row $offs + } + + set rowrangelist [lreplace $rowrangelist $row $row] + if {[llength $kids] > 0} { + set ranges [lindex $rowrangelist $row] + if {[lindex $ranges end-1] eq $id} { + set ranges [lreplace $ranges end-1 end] + lset rowrangelist $row $ranges + } + } + + catch {unset rowchk} + + incr rowlaidout -1 + incr rowoptim -1 + incr numcommits -1 + + if {[info exists selectedline] && $selectedline > $row} { + incr selectedline -1 + } + redisplay +} + # Don't change the text pane cursor if it is currently the hand cursor, # showing that we are over a sha1 ID link. proc settextcursor {c} { @@ -3502,25 +3888,6 @@ proc notbusy {what} { } } -proc drawrest {} { - global startmsecs - global rowlaidout commitidx curview - global pending_select - - set row $rowlaidout - layoutrows $rowlaidout $commitidx($curview) 1 - layouttail - optimize_rows $row 0 $commitidx($curview) - showstuff $commitidx($curview) - if {[info exists pending_select]} { - selectline 0 1 - } - - set drawmsecs [expr {[clock clicks -milliseconds] - $startmsecs}] - #global numcommits - #puts "overall $drawmsecs ms for $numcommits commits" -} - proc findmatches {f} { global findtype foundstring foundstrlen if {$findtype == "Regexp"} { @@ -3594,13 +3961,13 @@ proc dofind {} { if {$matches == {}} continue set doesmatch 1 if {$ty == "Headline"} { - drawcmitrow $l + drawcommits $l markmatches $canv $l $f $linehtag($l) $matches $mainfont } elseif {$ty == "Author"} { - drawcmitrow $l + drawcommits $l markmatches $canv2 $l $f $linentag($l) $matches $mainfont } elseif {$ty == "Date"} { - drawcmitrow $l + drawcommits $l markmatches $canv3 $l $f $linedtag($l) $matches $mainfont } } @@ -3693,7 +4060,7 @@ proc stopfindproc {{done 0}} { proc markheadline {l id} { global canv mainfont linehtag - drawcmitrow $l + drawcommits $l set bbox [$canv bbox $linehtag($l)] set t [$canv create rect $bbox -outline {} -tags matches -fill yellow] $canv lower $t @@ -3798,75 +4165,106 @@ proc viewnextline {dir} { # add a list of tag or branch names at position pos # returns the number of names inserted -proc appendrefs {pos tags var} { - global ctext commitrow linknum curview $var +proc appendrefs {pos ids var} { + global ctext commitrow linknum curview $var maxrefs if {[catch {$ctext index $pos}]} { return 0 } - set tags [lsort $tags] - set sep {} - foreach tag $tags { - set id [set $var\($tag\)] - set lk link$linknum - incr linknum - $ctext insert $pos $sep - $ctext insert $pos $tag $lk - $ctext tag conf $lk -foreground blue - if {[info exists commitrow($curview,$id)]} { - $ctext tag bind $lk <1> \ - [list selectline $commitrow($curview,$id) 1] - $ctext tag conf $lk -underline 1 - $ctext tag bind $lk <Enter> { %W configure -cursor hand2 } - $ctext tag bind $lk <Leave> { %W configure -cursor $curtextcursor } + $ctext conf -state normal + $ctext delete $pos "$pos lineend" + set tags {} + foreach id $ids { + foreach tag [set $var\($id\)] { + lappend tags [list $tag $id] + } + } + if {[llength $tags] > $maxrefs} { + $ctext insert $pos "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 + if {[info exists commitrow($curview,$id)]} { + $ctext tag conf $lk -foreground blue + $ctext tag bind $lk <1> \ + [list selectline $commitrow($curview,$id) 1] + $ctext tag conf $lk -underline 1 + $ctext tag bind $lk <Enter> { %W configure -cursor hand2 } + $ctext tag bind $lk <Leave> \ + { %W configure -cursor $curtextcursor } + } + set sep ", " } - set sep ", " } + $ctext conf -state disabled return [llength $tags] } -proc taglist {ids} { - global idtags +# called when we have finished computing the nearby tags +proc dispneartags {delay} { + global selectedline currentid showneartags tagphase - set tags {} - foreach id $ids { - foreach tag $idtags($id) { - lappend tags $tag - } + if {![info exists selectedline] || !$showneartags} return + after cancel dispnexttag + if {$delay} { + after 200 dispnexttag + set tagphase -1 + } else { + after idle dispnexttag + set tagphase 0 } - return $tags } -# called when we have finished computing the nearby tags -proc dispneartags {} { - global selectedline currentid ctext anc_tags desc_tags showneartags - global desc_heads +proc dispnexttag {} { + global selectedline currentid showneartags tagphase ctext if {![info exists selectedline] || !$showneartags} return - set id $currentid - $ctext conf -state normal - if {[info exists desc_heads($id)]} { - if {[appendrefs branch $desc_heads($id) headids] > 1} { - $ctext insert "branch -2c" "es" + switch -- $tagphase { + 0 { + set dtags [desctags $currentid] + if {$dtags ne {}} { + appendrefs precedes $dtags idtags + } + } + 1 { + set atags [anctags $currentid] + if {$atags ne {}} { + appendrefs follows $atags idtags + } + } + 2 { + set dheads [descheads $currentid] + if {$dheads ne {}} { + if {[appendrefs branch $dheads idheads] > 1 + && [$ctext get "branch -3c"] eq "h"} { + # turn "Branch" into "Branches" + $ctext conf -state normal + $ctext insert "branch -2c" "es" + $ctext conf -state disabled + } + } } } - if {[info exists anc_tags($id)]} { - appendrefs follows [taglist $anc_tags($id)] tagids - } - if {[info exists desc_tags($id)]} { - appendrefs precedes [taglist $desc_tags($id)] tagids + if {[incr tagphase] <= 2} { + after idle dispnexttag } - $ctext conf -state disabled } proc selectline {l isnew} { global canv canv2 canv3 ctext commitinfo selectedline global displayorder linehtag linentag linedtag - global canvy0 linespc parentlist childlist + global canvy0 linespc parentlist children curview global currentid sha1entry global commentend idtags linknum global mergemax numcommits pending_select - global cmitmode desc_tags anc_tags showneartags allcommits desc_heads + global cmitmode showneartags allcommits catch {unset pending_select} $canv delete hover @@ -3973,7 +4371,7 @@ proc selectline {l isnew} { } } - foreach c [lindex $childlist $l] { + foreach c $children($curview,$id) { append headers "Child: [commit_descriptor $c]" } @@ -3986,30 +4384,22 @@ proc selectline {l isnew} { $ctext insert end "Branch: " $ctext mark set branch "end -1c" $ctext mark gravity branch left - if {[info exists desc_heads($id)]} { - if {[appendrefs branch $desc_heads($id) headids] > 1} { - # turn "Branch" into "Branches" - $ctext insert "branch -2c" "es" - } - } $ctext insert end "\nFollows: " $ctext mark set follows "end -1c" $ctext mark gravity follows left - if {[info exists anc_tags($id)]} { - appendrefs follows [taglist $anc_tags($id)] tagids - } $ctext insert end "\nPrecedes: " $ctext mark set precedes "end -1c" $ctext mark gravity precedes left - if {[info exists desc_tags($id)]} { - appendrefs precedes [taglist $desc_tags($id)] tagids - } $ctext insert end "\n" + dispneartags 1 } $ctext insert end "\n" - appendwithlinks [lindex $info 5] {comment} + set comment [lindex $info 5] + if {[string first "\r" $comment] >= 0} { + set comment [string map {"\r" "\n "} $comment] + } + appendwithlinks $comment {comment} - $ctext tag delete Comments $ctext tag remove found 1.0 end $ctext conf -state disabled set commentend [$ctext index "end - 1c"] @@ -4144,20 +4534,25 @@ proc goforw {} { } proc gettree {id} { - global treefilelist treeidlist diffids diffmergeid treepending + global treefilelist treeidlist diffids diffmergeid treepending nullid set diffids $id catch {unset diffmergeid} if {![info exists treefilelist($id)]} { if {![info exists treepending]} { - if {[catch {set gtf [open [concat | git ls-tree -r $id] r]}]} { + if {$id ne $nullid} { + set cmd [concat | git ls-tree -r $id] + } else { + set cmd [concat | git ls-files] + } + if {[catch {set gtf [open $cmd r]}]} { return } set treepending $id set treefilelist($id) {} set treeidlist($id) {} fconfigure $gtf -blocking 0 - fileevent $gtf readable [list gettreeline $gtf $id] + filerun $gtf [list gettreeline $gtf $id] } } else { setfilelist $id @@ -4165,16 +4560,28 @@ proc gettree {id} { } proc gettreeline {gtf id} { - global treefilelist treeidlist treepending cmitmode diffids - - while {[gets $gtf line] >= 0} { - if {[lindex $line 1] ne "blob"} continue - set sha1 [lindex $line 2] - set fname [lindex $line 3] + global treefilelist treeidlist treepending cmitmode diffids nullid + + set nl 0 + while {[incr nl] <= 1000 && [gets $gtf line] >= 0} { + if {$diffids ne $nullid} { + if {[lindex $line 1] ne "blob"} continue + set i [string first "\t" $line] + if {$i < 0} continue + set sha1 [lindex $line 2] + set fname [string range $line [expr {$i+1}] end] + if {[string index $fname 0] eq "\""} { + set fname [lindex $fname 0] + } + lappend treeidlist($id) $sha1 + } else { + set fname $line + } lappend treefilelist($id) $fname - lappend treeidlist($id) $sha1 } - if {![eof $gtf]} return + if {![eof $gtf]} { + return [expr {$nl >= 1000? 2: 1}] + } close $gtf unset treepending if {$cmitmode ne "tree"} { @@ -4186,10 +4593,11 @@ proc gettreeline {gtf id} { } else { setfilelist $id } + return 0 } proc showfile {f} { - global treefilelist treeidlist diffids + global treefilelist treeidlist diffids nullid global ctext commentend set i [lsearch -exact $treefilelist($diffids) $f] @@ -4197,13 +4605,20 @@ proc showfile {f} { puts "oops, $f not in list for id $diffids" return } - set blob [lindex $treeidlist($diffids) $i] - if {[catch {set bf [open [concat | git cat-file blob $blob] r]} err]} { - puts "oops, error reading blob $blob: $err" - return + if {$diffids ne $nullid} { + set blob [lindex $treeidlist($diffids) $i] + if {[catch {set bf [open [concat | git cat-file blob $blob] r]} err]} { + puts "oops, error reading blob $blob: $err" + return + } + } else { + if {[catch {set bf [open $f r]} err]} { + puts "oops, can't read $f: $err" + return + } } fconfigure $bf -blocking 0 - fileevent $bf readable [list getblobline $bf $diffids] + filerun $bf [list getblobline $bf $diffids] $ctext config -state normal clear_ctext $commentend $ctext insert end "\n" @@ -4217,18 +4632,21 @@ proc getblobline {bf id} { if {$id ne $diffids || $cmitmode ne "tree"} { catch {close $bf} - return + return 0 } $ctext config -state normal - while {[gets $bf line] >= 0} { + set nl 0 + while {[incr nl] <= 1000 && [gets $bf line] >= 0} { $ctext insert end "$line\n" } if {[eof $bf]} { # delete last newline $ctext delete "end - 2c" "end - 1c" close $bf + return 0 } $ctext config -state disabled + return [expr {$nl >= 1000? 2: 1}] } proc mergediff {id l} { @@ -4248,91 +4666,86 @@ proc mergediff {id l} { fconfigure $mdf -blocking 0 set mdifffd($id) $mdf set np [llength [lindex $parentlist $l]] - fileevent $mdf readable [list getmergediffline $mdf $id $np] - set nextupdate [expr {[clock clicks -milliseconds] + 100}] + filerun $mdf [list getmergediffline $mdf $id $np] } proc getmergediffline {mdf id np} { - global diffmergeid ctext cflist nextupdate mergemax + global diffmergeid ctext cflist mergemax global difffilestart mdifffd - set n [gets $mdf line] - if {$n < 0} { - if {[eof $mdf]} { + $ctext conf -state normal + set nr 0 + while {[incr nr] <= 1000 && [gets $mdf line] >= 0} { + if {![info exists diffmergeid] || $id != $diffmergeid + || $mdf != $mdifffd($id)} { close $mdf + return 0 } - return - } - if {![info exists diffmergeid] || $id != $diffmergeid - || $mdf != $mdifffd($id)} { - return - } - $ctext conf -state normal - if {[regexp {^diff --cc (.*)} $line match fname]} { - # start of a new file - $ctext insert end "\n" - set here [$ctext index "end - 1c"] - lappend difffilestart $here - add_flist [list $fname] - set l [expr {(78 - [string length $fname]) / 2}] - set pad [string range "----------------------------------------" 1 $l] - $ctext insert end "$pad $fname $pad\n" filesep - } elseif {[regexp {^@@} $line]} { - $ctext insert end "$line\n" hunksep - } elseif {[regexp {^[0-9a-f]{40}$} $line] || [regexp {^index} $line]} { - # do nothing - } else { - # parse the prefix - one ' ', '-' or '+' for each parent - set spaces {} - set minuses {} - set pluses {} - set isbad 0 - for {set j 0} {$j < $np} {incr j} { - set c [string range $line $j $j] - if {$c == " "} { - lappend spaces $j - } elseif {$c == "-"} { - lappend minuses $j - } elseif {$c == "+"} { - lappend pluses $j - } else { - set isbad 1 - break + if {[regexp {^diff --cc (.*)} $line match fname]} { + # start of a new file + $ctext insert end "\n" + set here [$ctext index "end - 1c"] + lappend difffilestart $here + add_flist [list $fname] + set l [expr {(78 - [string length $fname]) / 2}] + set pad [string range "----------------------------------------" 1 $l] + $ctext insert end "$pad $fname $pad\n" filesep + } elseif {[regexp {^@@} $line]} { + $ctext insert end "$line\n" hunksep + } elseif {[regexp {^[0-9a-f]{40}$} $line] || [regexp {^index} $line]} { + # do nothing + } else { + # parse the prefix - one ' ', '-' or '+' for each parent + set spaces {} + set minuses {} + set pluses {} + set isbad 0 + for {set j 0} {$j < $np} {incr j} { + set c [string range $line $j $j] + if {$c == " "} { + lappend spaces $j + } elseif {$c == "-"} { + lappend minuses $j + } elseif {$c == "+"} { + lappend pluses $j + } else { + set isbad 1 + break + } } - } - set tags {} - set num {} - if {!$isbad && $minuses ne {} && $pluses eq {}} { - # line doesn't appear in result, parents in $minuses have the line - set num [lindex $minuses 0] - } elseif {!$isbad && $pluses ne {} && $minuses eq {}} { - # line appears in result, parents in $pluses don't have the line - lappend tags mresult - set num [lindex $spaces 0] - } - if {$num ne {}} { - if {$num >= $mergemax} { - set num "max" + set tags {} + set num {} + if {!$isbad && $minuses ne {} && $pluses eq {}} { + # line doesn't appear in result, parents in $minuses have the line + set num [lindex $minuses 0] + } elseif {!$isbad && $pluses ne {} && $minuses eq {}} { + # line appears in result, parents in $pluses don't have the line + lappend tags mresult + set num [lindex $spaces 0] + } + if {$num ne {}} { + if {$num >= $mergemax} { + set num "max" + } + lappend tags m$num } - lappend tags m$num + $ctext insert end "$line\n" $tags } - $ctext insert end "$line\n" $tags } $ctext conf -state disabled - if {[clock clicks -milliseconds] >= $nextupdate} { - incr nextupdate 100 - fileevent $mdf readable {} - update - fileevent $mdf readable [list getmergediffline $mdf $id $np] + if {[eof $mdf]} { + close $mdf + return 0 } + return [expr {$nr >= 1000? 2: 1}] } proc startdiff {ids} { - global treediffs diffids treepending diffmergeid + global treediffs diffids treepending diffmergeid nullid set diffids $ids catch {unset diffmergeid} - if {![info exists treediffs($ids)]} { + if {![info exists treediffs($ids)] || [lsearch -exact $ids $nullid] >= 0} { if {![info exists treepending]} { gettreediffs $ids } @@ -4347,59 +4760,83 @@ proc addtocflist {ids} { getblobdiffs $ids } +proc diffcmd {ids flags} { + global nullid + + set i [lsearch -exact $ids $nullid] + if {$i >= 0} { + set cmd [concat | git diff-index $flags] + if {[llength $ids] > 1} { + if {$i == 0} { + lappend cmd -R [lindex $ids 1] + } else { + lappend cmd [lindex $ids 0] + } + } else { + lappend cmd HEAD + } + } else { + set cmd [concat | git diff-tree --no-commit-id -r $flags $ids] + } + return $cmd +} + proc gettreediffs {ids} { global treediff treepending + set treepending $ids set treediff {} - if {[catch \ - {set gdtf [open [concat | git diff-tree --no-commit-id -r $ids] r]} \ - ]} return + if {[catch {set gdtf [open [diffcmd $ids {}] r]}]} return fconfigure $gdtf -blocking 0 - fileevent $gdtf readable [list gettreediffline $gdtf $ids] + filerun $gdtf [list gettreediffline $gdtf $ids] } proc gettreediffline {gdtf ids} { global treediff treediffs treepending diffids diffmergeid global cmitmode - set n [gets $gdtf line] - if {$n < 0} { - if {![eof $gdtf]} return - close $gdtf - set treediffs($ids) $treediff - unset treepending - if {$cmitmode eq "tree"} { - gettree $diffids - } elseif {$ids != $diffids} { - if {![info exists diffmergeid]} { - gettreediffs $diffids + set nr 0 + while {[incr nr] <= 1000 && [gets $gdtf line] >= 0} { + set i [string first "\t" $line] + if {$i >= 0} { + set file [string range $line [expr {$i+1}] end] + if {[string index $file 0] eq "\""} { + set file [lindex $file 0] } - } else { - addtocflist $ids + lappend treediff $file } - return } - set file [lindex $line 5] - lappend treediff $file + if {![eof $gdtf]} { + return [expr {$nr >= 1000? 2: 1}] + } + close $gdtf + set treediffs($ids) $treediff + unset treepending + if {$cmitmode eq "tree"} { + gettree $diffids + } elseif {$ids != $diffids} { + if {![info exists diffmergeid]} { + gettreediffs $diffids + } + } else { + addtocflist $ids + } + return 0 } proc getblobdiffs {ids} { - global diffopts blobdifffd diffids env curdifftag curtagstart - global nextupdate diffinhdr treediffs + global diffopts blobdifffd diffids env + global diffinhdr treediffs set env(GIT_DIFF_OPTS) $diffopts - set cmd [concat | git diff-tree --no-commit-id -r -p -C $ids] - if {[catch {set bdf [open $cmd r]} err]} { + if {[catch {set bdf [open [diffcmd $ids {-p -C}] r]} err]} { puts "error getting diffs: $err" return } set diffinhdr 0 fconfigure $bdf -blocking 0 set blobdifffd($ids) $bdf - set curdifftag Comments - set curtagstart 0.0 - fileevent $bdf readable [list getblobdiffline $bdf $diffids] - set nextupdate [expr {[clock clicks -milliseconds] + 100}] + filerun $bdf [list getblobdiffline $bdf $diffids] } proc setinlist {var i val} { @@ -4415,84 +4852,111 @@ proc setinlist {var i val} { } } +proc makediffhdr {fname ids} { + global ctext curdiffstart treediffs + + set i [lsearch -exact $treediffs($ids) $fname] + if {$i >= 0} { + setinlist difffilestart $i $curdiffstart + } + set l [expr {(78 - [string length $fname]) / 2}] + set pad [string range "----------------------------------------" 1 $l] + $ctext insert $curdiffstart "$pad $fname $pad" filesep +} + proc getblobdiffline {bdf ids} { - global diffids blobdifffd ctext curdifftag curtagstart + global diffids blobdifffd ctext curdiffstart global diffnexthead diffnextnote difffilestart - global nextupdate diffinhdr treediffs + global diffinhdr treediffs - set n [gets $bdf line] - if {$n < 0} { - if {[eof $bdf]} { - close $bdf - if {$ids == $diffids && $bdf == $blobdifffd($ids)} { - $ctext tag add $curdifftag $curtagstart end - } - } - return - } - if {$ids != $diffids || $bdf != $blobdifffd($ids)} { - return - } + set nr 0 $ctext conf -state normal - if {[regexp {^diff --git a/(.*) b/(.*)} $line match fname newname]} { - # start of a new file - $ctext insert end "\n" - $ctext tag add $curdifftag $curtagstart end - set here [$ctext index "end - 1c"] - set curtagstart $here - set header $newname - set i [lsearch -exact $treediffs($ids) $fname] - if {$i >= 0} { - setinlist difffilestart $i $here + while {[incr nr] <= 1000 && [gets $bdf line] >= 0} { + if {$ids != $diffids || $bdf != $blobdifffd($ids)} { + close $bdf + return 0 } - if {$newname ne $fname} { - set i [lsearch -exact $treediffs($ids) $newname] - if {$i >= 0} { - setinlist difffilestart $i $here + if {![string compare -length 11 "diff --git " $line]} { + # trim off "diff --git " + set line [string range $line 11 end] + set diffinhdr 1 + # start of a new file + $ctext insert end "\n" + set curdiffstart [$ctext index "end - 1c"] + $ctext insert end "\n" filesep + # If the name hasn't changed the length will be odd, + # the middle char will be a space, and the two bits either + # side will be a/name and b/name, or "a/name" and "b/name". + # If the name has changed we'll get "rename from" and + # "rename to" lines following this, and we'll use them + # to get the filenames. + # This complexity is necessary because spaces in the filename(s) + # don't get escaped. + set l [string length $line] + set i [expr {$l / 2}] + if {!(($l & 1) && [string index $line $i] eq " " && + [string range $line 2 [expr {$i - 1}]] eq \ + [string range $line [expr {$i + 3}] end])} { + continue + } + # unescape if quoted and chop off the a/ from the front + if {[string index $line 0] eq "\""} { + set fname [string range [lindex $line 0] 2 end] + } else { + set fname [string range $line 2 [expr {$i - 1}]] + } + makediffhdr $fname $ids + + } elseif {[regexp {^@@ -([0-9]+)(,[0-9]+)? \+([0-9]+)(,[0-9]+)? @@(.*)} \ + $line match f1l f1c f2l f2c rest]} { + $ctext insert end "$line\n" hunksep + set diffinhdr 0 + + } elseif {$diffinhdr} { + if {![string compare -length 12 "rename from " $line]} { + set fname [string range $line 12 end] + if {[string index $fname 0] eq "\""} { + set fname [lindex $fname 0] + } + set i [lsearch -exact $treediffs($ids) $fname] + if {$i >= 0} { + setinlist difffilestart $i $curdiffstart + } + } elseif {![string compare -length 10 $line "rename to "]} { + set fname [string range $line 10 end] + if {[string index $fname 0] eq "\""} { + set fname [lindex $fname 0] + } + makediffhdr $fname $ids + } elseif {[string compare -length 3 $line "---"] == 0} { + # do nothing + continue + } elseif {[string compare -length 3 $line "+++"] == 0} { + set diffinhdr 0 + continue } - } - set curdifftag "f:$fname" - $ctext tag delete $curdifftag - set l [expr {(78 - [string length $header]) / 2}] - set pad [string range "----------------------------------------" 1 $l] - $ctext insert end "$pad $header $pad\n" filesep - set diffinhdr 1 - } elseif {$diffinhdr && [string compare -length 3 $line "---"] == 0} { - # do nothing - } elseif {$diffinhdr && [string compare -length 3 $line "+++"] == 0} { - set diffinhdr 0 - } elseif {[regexp {^@@ -([0-9]+),([0-9]+) \+([0-9]+),([0-9]+) @@(.*)} \ - $line match f1l f1c f2l f2c rest]} { - $ctext insert end "$line\n" hunksep - set diffinhdr 0 - } else { - set x [string range $line 0 0] - if {$x == "-" || $x == "+"} { - set tag [expr {$x == "+"}] - $ctext insert end "$line\n" d$tag - } elseif {$x == " "} { - $ctext insert end "$line\n" - } elseif {$diffinhdr || $x == "\\"} { - # e.g. "\ No newline at end of file" $ctext insert end "$line\n" filesep + } else { - # Something else we don't recognize - if {$curdifftag != "Comments"} { - $ctext insert end "\n" - $ctext tag add $curdifftag $curtagstart end - set curtagstart [$ctext index "end - 1c"] - set curdifftag Comments + set x [string range $line 0 0] + if {$x == "-" || $x == "+"} { + set tag [expr {$x == "+"}] + $ctext insert end "$line\n" d$tag + } elseif {$x == " "} { + $ctext insert end "$line\n" + } else { + # "\ No newline at end of file", + # or something else we don't recognize + $ctext insert end "$line\n" hunksep } - $ctext insert end "$line\n" filesep } } $ctext conf -state disabled - if {[clock clicks -milliseconds] >= $nextupdate} { - incr nextupdate 100 - fileevent $bdf readable {} - update - fileevent $bdf readable "getblobdiffline $bdf {$ids}" + if {[eof $bdf]} { + close $bdf + return 0 } + return [expr {$nr >= 1000? 2: 1}] } proc changediffdisp {} { @@ -4690,13 +5154,15 @@ proc redisplay {} { } proc incrfont {inc} { - global mainfont textfont ctext canv phase + global mainfont textfont ctext canv phase cflist + global charspc tabstop global stopped entries unmarkmatches set mainfont [lreplace $mainfont 1 1 [expr {[lindex $mainfont 1] + $inc}]] set textfont [lreplace $textfont 1 1 [expr {[lindex $textfont 1] + $inc}]] setcoords - $ctext conf -font $textfont + $ctext conf -font $textfont -tabs "[expr {$tabstop * $charspc}]" + $cflist conf -font $textfont $ctext tag conf filesep -font [concat $textfont bold] foreach e $entries { $e conf -font $mainfont @@ -4954,18 +5420,25 @@ proc mstime {} { proc rowmenu {x y id} { global rowctxmenu commitrow selectedline rowmenuid curview + global nullid fakerowmenu mainhead + set rowmenuid $id if {![info exists selectedline] || $commitrow($curview,$id) eq $selectedline} { set state disabled } else { set state normal } - $rowctxmenu entryconfigure "Diff this*" -state $state - $rowctxmenu entryconfigure "Diff selected*" -state $state - $rowctxmenu entryconfigure "Make patch" -state $state - set rowmenuid $id - tk_popup $rowctxmenu $x $y + if {$id ne $nullid} { + set menu $rowctxmenu + $menu entryconfigure 7 -label "Reset $mainhead branch to here" + } else { + set menu $fakerowmenu + } + $menu entryconfigure "Diff this*" -state $state + $menu entryconfigure "Diff selected*" -state $state + $menu entryconfigure "Make patch" -state $state + tk_popup $menu $x $y } proc diffvssel {dirn} { @@ -5005,7 +5478,6 @@ proc doseldiff {oldid newid} { $ctext insert end [lindex $commitinfo($newid) 0] $ctext insert end "\n" $ctext conf -state disabled - $ctext tag delete Comments $ctext tag remove found 1.0 end startdiff [list $oldid $newid] } @@ -5076,12 +5548,20 @@ proc mkpatchrev {} { } proc mkpatchgo {} { - global patchtop + global patchtop nullid set oldid [$patchtop.fromsha1 get] set newid [$patchtop.tosha1 get] set fname [$patchtop.fname get] - if {[catch {exec git diff-tree -p $oldid $newid >$fname &} err]} { + if {$newid eq $nullid} { + set cmd [list git diff-index -p $oldid] + } elseif {$oldid eq $nullid} { + set cmd [list git diff-index -p -R $newid] + } else { + set cmd [list git diff-tree -p $oldid $newid] + } + lappend cmd >$fname & + if {[catch {eval exec $cmd} err]} { error_popup "Error creating patch: $err" } catch {destroy $patchtop} @@ -5158,10 +5638,11 @@ proc domktag {} { proc redrawtags {id} { global canv linehtag commitrow idpos selectedline curview - global mainfont canvxmax + global mainfont canvxmax iddrawn if {![info exists commitrow($curview,$id)]} return - drawcmitrow $commitrow($curview,$id) + if {![info exists iddrawn($id)]} return + drawcommits $commitrow($curview,$id) $canv delete tag.$id set xt [eval drawtags $id $idpos($id)] $canv coords $linehtag($commitrow($curview,$id)) $xt [lindex $idpos($id) 2] @@ -5288,26 +5769,28 @@ proc mkbrgo {top} { notbusy newbranch error_popup $err } else { + set headids($name) $id + lappend idheads($id) $name addedhead $id $name - # XXX should update list of heads displayed for selected commit notbusy newbranch redrawtags $id + dispneartags 0 } } proc cherrypick {} { global rowmenuid curview commitrow - global mainhead desc_heads anc_tags desc_tags allparents allchildren + global mainhead - if {[info exists desc_heads($rowmenuid)] - && [lsearch -exact $desc_heads($rowmenuid) $mainhead] >= 0} { + set oldhead [exec git rev-parse HEAD] + set dheads [descheads $rowmenuid] + if {$dheads ne {} && [lsearch -exact $dheads $oldhead] >= 0} { set ok [confirm_popup "Commit [string range $rowmenuid 0 7] is already\ included in branch $mainhead -- really re-apply it?"] if {!$ok} return } nowbusy cherrypick update - set oldhead [exec git rev-parse HEAD] # Unfortunately git-cherry-pick writes stuff to stderr even when # no error occurs, and exec takes that as an indication of error... if {[catch {exec sh -c "git cherry-pick -r $rowmenuid 2>&1"} err]} { @@ -5321,16 +5804,11 @@ proc cherrypick {} { error_popup "No changes committed" return } - set allparents($newhead) $oldhead - lappend allchildren($oldhead) $newhead - set desc_heads($newhead) $mainhead - if {[info exists anc_tags($oldhead)]} { - set anc_tags($newhead) $anc_tags($oldhead) - } - set desc_tags($newhead) {} + addnewchild $newhead $oldhead if {[info exists commitrow($curview,$oldhead)]} { insertrow $commitrow($curview,$oldhead) $newhead if {$mainhead ne {}} { + movehead $newhead $mainhead movedhead $newhead $mainhead } redrawtags $oldhead @@ -5339,48 +5817,146 @@ proc cherrypick {} { notbusy cherrypick } +proc resethead {} { + global mainheadid mainhead rowmenuid confirm_ok resettype + global showlocalchanges + + set confirm_ok 0 + set w ".confirmreset" + toplevel $w + wm transient $w . + wm title $w "Confirm reset" + message $w.m -text \ + "Reset branch $mainhead to [string range $rowmenuid 0 7]?" \ + -justify center -aspect 1000 + pack $w.m -side top -fill x -padx 20 -pady 20 + frame $w.f -relief sunken -border 2 + message $w.f.rt -text "Reset type:" -aspect 1000 + grid $w.f.rt -sticky w + set resettype mixed + radiobutton $w.f.soft -value soft -variable resettype -justify left \ + -text "Soft: Leave working tree and index untouched" + grid $w.f.soft -sticky w + radiobutton $w.f.mixed -value mixed -variable resettype -justify left \ + -text "Mixed: Leave working tree untouched, reset index" + grid $w.f.mixed -sticky w + radiobutton $w.f.hard -value hard -variable resettype -justify left \ + -text "Hard: Reset working tree and index\n(discard ALL local changes)" + grid $w.f.hard -sticky w + pack $w.f -side top -fill x + button $w.ok -text OK -command "set confirm_ok 1; destroy $w" + pack $w.ok -side left -fill x -padx 20 -pady 20 + button $w.cancel -text Cancel -command "destroy $w" + pack $w.cancel -side right -fill x -padx 20 -pady 20 + bind $w <Visibility> "grab $w; focus $w" + tkwait window $w + if {!$confirm_ok} return + if {[catch {set fd [open \ + [list | sh -c "git reset --$resettype $rowmenuid 2>&1"] r]} err]} { + error_popup $err + } else { + dohidelocalchanges + set w ".resetprogress" + filerun $fd [list readresetstat $fd $w] + toplevel $w + wm transient $w + wm title $w "Reset progress" + message $w.m -text "Reset in progress, please wait..." \ + -justify center -aspect 1000 + pack $w.m -side top -fill x -padx 20 -pady 5 + canvas $w.c -width 150 -height 20 -bg white + $w.c create rect 0 0 0 20 -fill green -tags rect + pack $w.c -side top -fill x -padx 20 -pady 5 -expand 1 + nowbusy reset + } +} + +proc readresetstat {fd w} { + global mainhead mainheadid showlocalchanges + + if {[gets $fd line] >= 0} { + if {[regexp {([0-9]+)% \(([0-9]+)/([0-9]+)\)} $line match p m n]} { + set x [expr {($m * 150) / $n}] + $w.c coords rect 0 0 $x 20 + } + return 1 + } + destroy $w + notbusy reset + if {[catch {close $fd} err]} { + error_popup $err + } + set oldhead $mainheadid + set newhead [exec git rev-parse HEAD] + if {$newhead ne $oldhead} { + movehead $newhead $mainhead + movedhead $newhead $mainhead + set mainheadid $newhead + redrawtags $oldhead + redrawtags $newhead + } + if {$showlocalchanges} { + doshowlocalchanges + } + return 0 +} + # context menu for a head proc headmenu {x y id head} { - global headmenuid headmenuhead headctxmenu + global headmenuid headmenuhead headctxmenu mainhead set headmenuid $id set headmenuhead $head + set state normal + if {$head eq $mainhead} { + set state disabled + } + $headctxmenu entryconfigure 0 -state $state + $headctxmenu entryconfigure 1 -state $state tk_popup $headctxmenu $x $y } proc cobranch {} { global headmenuid headmenuhead mainhead headids + global showlocalchanges mainheadid # check the tree is clean first?? set oldmainhead $mainhead nowbusy checkout update + dohidelocalchanges if {[catch { - exec git checkout $headmenuhead + exec git checkout -q $headmenuhead } err]} { notbusy checkout error_popup $err } else { notbusy checkout set mainhead $headmenuhead + set mainheadid $headmenuid if {[info exists headids($oldmainhead)]} { redrawtags $headids($oldmainhead) } redrawtags $headmenuid } + if {$showlocalchanges} { + dodiffindex + } } proc rmbranch {} { - global desc_heads headmenuid headmenuhead mainhead + global headmenuid headmenuhead mainhead global headids idheads set head $headmenuhead set id $headmenuid + # this check shouldn't be needed any more... if {$head eq $mainhead} { error_popup "Cannot delete the currently checked-out branch" return } - if {$desc_heads($id) eq $head} { + set dheads [descheads $id] + if {$dheads eq $headids($head)} { # the stuff on this branch isn't on any other branch if {![confirm_popup "The commits on branch $head aren't on any other\ branch.\nReally delete branch $head?"]} return @@ -5392,385 +5968,886 @@ proc rmbranch {} { error_popup $err return } + removehead $id $head removedhead $id $head redrawtags $id notbusy rmbranch + dispneartags 0 } # Stuff for finding nearby tags proc getallcommits {} { - global allcstart allcommits allcfd allids + global allcommits allids nbmp nextarc seeds set allids {} - set fd [open [concat | git rev-list --all --topo-order --parents] r] - set allcfd $fd - fconfigure $fd -blocking 0 - set allcommits "reading" - nowbusy allcommits - restartgetall $fd + set nbmp 0 + set nextarc 0 + set allcommits 0 + set seeds {} + regetallcommits } -proc discardallcommits {} { - global allparents allchildren allcommits allcfd - global desc_tags anc_tags alldtags tagisdesc allids desc_heads +# Called when the graph might have changed +proc regetallcommits {} { + global allcommits seeds - if {![info exists allcommits]} return - if {$allcommits eq "reading"} { - catch {close $allcfd} + set cmd [concat | git rev-list --all --parents] + foreach id $seeds { + lappend cmd "^$id" } - foreach v {allcommits allchildren allparents allids desc_tags anc_tags - alldtags tagisdesc desc_heads} { - catch {unset $v} - } -} - -proc restartgetall {fd} { - global allcstart + set fd [open $cmd r] + fconfigure $fd -blocking 0 + incr allcommits + nowbusy allcommits + filerun $fd [list getallclines $fd] +} + +# Since most commits have 1 parent and 1 child, we group strings of +# such commits into "arcs" joining branch/merge points (BMPs), which +# are commits that either don't have 1 parent or don't have 1 child. +# +# arcnos(id) - incoming arcs for BMP, arc we're on for other nodes +# arcout(id) - outgoing arcs for BMP +# arcids(a) - list of IDs on arc including end but not start +# arcstart(a) - BMP ID at start of arc +# arcend(a) - BMP ID at end of arc +# growing(a) - arc a is still growing +# arctags(a) - IDs out of arcids (excluding end) that have tags +# archeads(a) - IDs out of arcids (excluding end) that have heads +# The start of an arc is at the descendent end, so "incoming" means +# coming from descendents, and "outgoing" means going towards ancestors. - fileevent $fd readable [list getallclines $fd] - set allcstart [clock clicks -milliseconds] -} +proc getallclines {fd} { + global allids allparents allchildren idtags idheads nextarc nbmp + global arcnos arcids arctags arcout arcend arcstart archeads growing + global seeds allcommits -proc combine_dtags {l1 l2} { - global tagisdesc notfirstd + set nid 0 + while {[incr nid] <= 1000 && [gets $fd line] >= 0} { + set id [lindex $line 0] + if {[info exists allparents($id)]} { + # seen it already + continue + } + lappend allids $id + set olds [lrange $line 1 end] + set allparents($id) $olds + if {![info exists allchildren($id)]} { + set allchildren($id) {} + set arcnos($id) {} + lappend seeds $id + } else { + set a $arcnos($id) + if {[llength $olds] == 1 && [llength $a] == 1} { + lappend arcids($a) $id + if {[info exists idtags($id)]} { + lappend arctags($a) $id + } + if {[info exists idheads($id)]} { + lappend archeads($a) $id + } + if {[info exists allparents($olds)]} { + # seen parent already + if {![info exists arcout($olds)]} { + splitarc $olds + } + lappend arcids($a) $olds + set arcend($a) $olds + unset growing($a) + } + lappend allchildren($olds) $id + lappend arcnos($olds) $a + continue + } + } + incr nbmp + foreach a $arcnos($id) { + lappend arcids($a) $id + set arcend($a) $id + unset growing($a) + } - set res [lsort -unique [concat $l1 $l2]] - for {set i 0} {$i < [llength $res]} {incr i} { - set x [lindex $res $i] - for {set j [expr {$i+1}]} {$j < [llength $res]} {} { - set y [lindex $res $j] - if {[info exists tagisdesc($x,$y)]} { - if {$tagisdesc($x,$y) > 0} { - # x is a descendent of y, exclude x - set res [lreplace $res $i $i] - incr i -1 - break - } else { - # y is a descendent of x, exclude y - set res [lreplace $res $j $j] + set ao {} + foreach p $olds { + lappend allchildren($p) $id + set a [incr nextarc] + set arcstart($a) $id + set archeads($a) {} + set arctags($a) {} + set archeads($a) {} + set arcids($a) {} + lappend ao $a + set growing($a) 1 + if {[info exists allparents($p)]} { + # seen it already, may need to make a new branch + if {![info exists arcout($p)]} { + splitarc $p } - } else { - # no relation, keep going - incr j + lappend arcids($a) $p + set arcend($a) $p + unset growing($a) } + lappend arcnos($p) $a } + set arcout($id) $ao } - return $res + if {$nid > 0} { + global cached_dheads cached_dtags cached_atags + catch {unset cached_dheads} + catch {unset cached_dtags} + catch {unset cached_atags} + } + if {![eof $fd]} { + return [expr {$nid >= 1000? 2: 1}] + } + close $fd + if {[incr allcommits -1] == 0} { + notbusy allcommits + } + dispneartags 0 + return 0 } -proc combine_atags {l1 l2} { - global tagisdesc +proc recalcarc {a} { + global arctags archeads arcids idtags idheads - set res [lsort -unique [concat $l1 $l2]] - for {set i 0} {$i < [llength $res]} {incr i} { - set x [lindex $res $i] - for {set j [expr {$i+1}]} {$j < [llength $res]} {} { - set y [lindex $res $j] - if {[info exists tagisdesc($x,$y)]} { - if {$tagisdesc($x,$y) < 0} { - # x is an ancestor of y, exclude x - set res [lreplace $res $i $i] - incr i -1 - break - } else { - # y is an ancestor of x, exclude y - set res [lreplace $res $j $j] - } - } else { - # no relation, keep going - incr j - } + set at {} + set ah {} + foreach id [lrange $arcids($a) 0 end-1] { + if {[info exists idtags($id)]} { + lappend at $id + } + if {[info exists idheads($id)]} { + lappend ah $id } } - return $res + set arctags($a) $at + set archeads($a) $ah } -proc forward_pass {id children} { - global idtags desc_tags idheads desc_heads alldtags tagisdesc +proc splitarc {p} { + global arcnos arcids nextarc nbmp arctags archeads idtags idheads + global arcstart arcend arcout allparents growing - set dtags {} - set dheads {} - foreach child $children { - if {[info exists idtags($child)]} { - set ctags [list $child] + set a $arcnos($p) + if {[llength $a] != 1} { + puts "oops splitarc called but [llength $a] arcs already" + return + } + set a [lindex $a 0] + set i [lsearch -exact $arcids($a) $p] + if {$i < 0} { + puts "oops splitarc $p not in arc $a" + return + } + set na [incr nextarc] + if {[info exists arcend($a)]} { + set arcend($na) $arcend($a) + } else { + set l [lindex $allparents([lindex $arcids($a) end]) 0] + set j [lsearch -exact $arcnos($l) $a] + set arcnos($l) [lreplace $arcnos($l) $j $j $na] + } + set tail [lrange $arcids($a) [expr {$i+1}] end] + set arcids($a) [lrange $arcids($a) 0 $i] + set arcend($a) $p + set arcstart($na) $p + set arcout($p) $na + set arcids($na) $tail + if {[info exists growing($a)]} { + set growing($na) 1 + unset growing($a) + } + incr nbmp + + foreach id $tail { + if {[llength $arcnos($id)] == 1} { + set arcnos($id) $na } else { - set ctags $desc_tags($child) + set j [lsearch -exact $arcnos($id) $a] + set arcnos($id) [lreplace $arcnos($id) $j $j $na] } - if {$dtags eq {}} { - set dtags $ctags - } elseif {$ctags ne $dtags} { - set dtags [combine_dtags $dtags $ctags] + } + + # reconstruct tags and heads lists + if {$arctags($a) ne {} || $archeads($a) ne {}} { + recalcarc $a + recalcarc $na + } else { + set arctags($na) {} + set archeads($na) {} + } +} + +# Update things for a new commit added that is a child of one +# existing commit. Used when cherry-picking. +proc addnewchild {id p} { + global allids allparents allchildren idtags nextarc nbmp + global arcnos arcids arctags arcout arcend arcstart archeads growing + global seeds + + lappend allids $id + set allparents($id) [list $p] + set allchildren($id) {} + set arcnos($id) {} + lappend seeds $id + incr nbmp + lappend allchildren($p) $id + set a [incr nextarc] + set arcstart($a) $id + set archeads($a) {} + set arctags($a) {} + set arcids($a) [list $p] + set arcend($a) $p + if {![info exists arcout($p)]} { + splitarc $p + } + lappend arcnos($p) $a + set arcout($id) [list $a] +} + +# Returns 1 if a is an ancestor of b, -1 if b is an ancestor of a, +# or 0 if neither is true. +proc anc_or_desc {a b} { + global arcout arcstart arcend arcnos cached_isanc + + if {$arcnos($a) eq $arcnos($b)} { + # Both are on the same arc(s); either both are the same BMP, + # or if one is not a BMP, the other is also not a BMP or is + # the BMP at end of the arc (and it only has 1 incoming arc). + if {$a eq $b} { + return 0 } - set cheads $desc_heads($child) - if {$dheads eq {}} { - set dheads $cheads - } elseif {$cheads ne $dheads} { - set dheads [lsort -unique [concat $dheads $cheads]] + # assert {[llength $arcnos($a)] == 1} + set arc [lindex $arcnos($a) 0] + set i [lsearch -exact $arcids($arc) $a] + set j [lsearch -exact $arcids($arc) $b] + if {$i < 0 || $i > $j} { + return 1 + } else { + return -1 } } - set desc_tags($id) $dtags - if {[info exists idtags($id)]} { - set adt $dtags - foreach tag $dtags { - set adt [concat $adt $alldtags($tag)] + + if {![info exists arcout($a)]} { + set arc [lindex $arcnos($a) 0] + if {[info exists arcend($arc)]} { + set aend $arcend($arc) + } else { + set aend {} } - set adt [lsort -unique $adt] - set alldtags($id) $adt - foreach tag $adt { - set tagisdesc($id,$tag) -1 - set tagisdesc($tag,$id) 1 + set a $arcstart($arc) + } else { + set aend $a + } + if {![info exists arcout($b)]} { + set arc [lindex $arcnos($b) 0] + if {[info exists arcend($arc)]} { + set bend $arcend($arc) + } else { + set bend {} } + set b $arcstart($arc) + } else { + set bend $b } - if {[info exists idheads($id)]} { - set dheads [concat $dheads $idheads($id)] + if {$a eq $bend} { + return 1 + } + if {$b eq $aend} { + return -1 + } + if {[info exists cached_isanc($a,$bend)]} { + if {$cached_isanc($a,$bend)} { + return 1 + } + } + if {[info exists cached_isanc($b,$aend)]} { + if {$cached_isanc($b,$aend)} { + return -1 + } + if {[info exists cached_isanc($a,$bend)]} { + return 0 + } } - set desc_heads($id) $dheads -} -proc getallclines {fd} { - global allparents allchildren allcommits allcstart - global desc_tags anc_tags idtags tagisdesc allids - global idheads travindex + set todo [list $a $b] + set anc($a) a + set anc($b) b + for {set i 0} {$i < [llength $todo]} {incr i} { + set x [lindex $todo $i] + if {$anc($x) eq {}} { + continue + } + foreach arc $arcnos($x) { + set xd $arcstart($arc) + if {$xd eq $bend} { + set cached_isanc($a,$bend) 1 + set cached_isanc($b,$aend) 0 + return 1 + } elseif {$xd eq $aend} { + set cached_isanc($b,$aend) 1 + set cached_isanc($a,$bend) 0 + return -1 + } + if {![info exists anc($xd)]} { + set anc($xd) $anc($x) + lappend todo $xd + } elseif {$anc($xd) ne $anc($x)} { + set anc($xd) {} + } + } + } + set cached_isanc($a,$bend) 0 + set cached_isanc($b,$aend) 0 + return 0 +} - while {[gets $fd line] >= 0} { - set id [lindex $line 0] - lappend allids $id - set olds [lrange $line 1 end] - set allparents($id) $olds - if {![info exists allchildren($id)]} { - set allchildren($id) {} +# This identifies whether $desc has an ancestor that is +# a growing tip of the graph and which is not an ancestor of $anc +# and returns 0 if so and 1 if not. +# If we subsequently discover a tag on such a growing tip, and that +# turns out to be a descendent of $anc (which it could, since we +# don't necessarily see children before parents), then $desc +# isn't a good choice to display as a descendent tag of +# $anc (since it is the descendent of another tag which is +# a descendent of $anc). Similarly, $anc isn't a good choice to +# display as a ancestor tag of $desc. +# +proc is_certain {desc anc} { + global arcnos arcout arcstart arcend growing problems + + set certain {} + if {[llength $arcnos($anc)] == 1} { + # tags on the same arc are certain + if {$arcnos($desc) eq $arcnos($anc)} { + return 1 } - foreach p $olds { - lappend allchildren($p) $id + if {![info exists arcout($anc)]} { + # if $anc is partway along an arc, use the start of the arc instead + set a [lindex $arcnos($anc) 0] + set anc $arcstart($a) } - # compute nearest tagged descendents as we go - # also compute descendent heads - forward_pass $id $allchildren($id) - if {[clock clicks -milliseconds] - $allcstart >= 50} { - fileevent $fd readable {} - after idle restartgetall $fd - return + } + if {[llength $arcnos($desc)] > 1 || [info exists arcout($desc)]} { + set x $desc + } else { + set a [lindex $arcnos($desc) 0] + set x $arcend($a) + } + if {$x == $anc} { + return 1 + } + set anclist [list $x] + set dl($x) 1 + set nnh 1 + set ngrowanc 0 + for {set i 0} {$i < [llength $anclist] && ($nnh > 0 || $ngrowanc > 0)} {incr i} { + set x [lindex $anclist $i] + if {$dl($x)} { + incr nnh -1 + } + set done($x) 1 + foreach a $arcout($x) { + if {[info exists growing($a)]} { + if {![info exists growanc($x)] && $dl($x)} { + set growanc($x) 1 + incr ngrowanc + } + } else { + set y $arcend($a) + if {[info exists dl($y)]} { + if {$dl($y)} { + if {!$dl($x)} { + set dl($y) 0 + if {![info exists done($y)]} { + incr nnh -1 + } + if {[info exists growanc($x)]} { + incr ngrowanc -1 + } + set xl [list $y] + for {set k 0} {$k < [llength $xl]} {incr k} { + set z [lindex $xl $k] + foreach c $arcout($z) { + if {[info exists arcend($c)]} { + set v $arcend($c) + if {[info exists dl($v)] && $dl($v)} { + set dl($v) 0 + if {![info exists done($v)]} { + incr nnh -1 + } + if {[info exists growanc($v)]} { + incr ngrowanc -1 + } + lappend xl $v + } + } + } + } + } + } + } elseif {$y eq $anc || !$dl($x)} { + set dl($y) 0 + lappend anclist $y + } else { + set dl($y) 1 + lappend anclist $y + incr nnh + } + } } } - if {[eof $fd]} { - set travindex [llength $allids] - set allcommits "traversing" - after idle restartatags - if {[catch {close $fd} err]} { - error_popup "Error reading full commit graph: $err.\n\ - Results may be incomplete." + foreach x [array names growanc] { + if {$dl($x)} { + return 0 } + return 0 } + return 1 } -# walk backward through the tree and compute nearest tagged ancestors -proc restartatags {} { - global allids allparents idtags anc_tags travindex +proc validate_arctags {a} { + global arctags idtags - set t0 [clock clicks -milliseconds] - set i $travindex - while {[incr i -1] >= 0} { - set id [lindex $allids $i] - set atags {} - foreach p $allparents($id) { - if {[info exists idtags($p)]} { - set ptags [list $p] - } else { - set ptags $anc_tags($p) + set i -1 + set na $arctags($a) + foreach id $arctags($a) { + incr i + if {![info exists idtags($id)]} { + set na [lreplace $na $i $i] + incr i -1 + } + } + set arctags($a) $na +} + +proc validate_archeads {a} { + global archeads idheads + + set i -1 + set na $archeads($a) + foreach id $archeads($a) { + incr i + if {![info exists idheads($id)]} { + set na [lreplace $na $i $i] + incr i -1 + } + } + set archeads($a) $na +} + +# Return the list of IDs that have tags that are descendents of id, +# ignoring IDs that are descendents of IDs already reported. +proc desctags {id} { + global arcnos arcstart arcids arctags idtags allparents + global growing cached_dtags + + if {![info exists allparents($id)]} { + return {} + } + set t1 [clock clicks -milliseconds] + set argid $id + if {[llength $arcnos($id)] == 1 && [llength $allparents($id)] == 1} { + # part-way along an arc; check that arc first + set a [lindex $arcnos($id) 0] + if {$arctags($a) ne {}} { + validate_arctags $a + set i [lsearch -exact $arcids($a) $id] + set tid {} + foreach t $arctags($a) { + set j [lsearch -exact $arcids($a) $t] + if {$j >= $i} break + set tid $t } - if {$atags eq {}} { - set atags $ptags - } elseif {$ptags ne $atags} { - set atags [combine_atags $atags $ptags] + if {$tid ne {}} { + return $tid } } - set anc_tags($id) $atags - if {[clock clicks -milliseconds] - $t0 >= 50} { - set travindex $i - after idle restartatags - return + set id $arcstart($a) + if {[info exists idtags($id)]} { + return $id + } + } + if {[info exists cached_dtags($id)]} { + return $cached_dtags($id) + } + + set origid $id + set todo [list $id] + set queued($id) 1 + set nc 1 + for {set i 0} {$i < [llength $todo] && $nc > 0} {incr i} { + set id [lindex $todo $i] + set done($id) 1 + set ta [info exists hastaggedancestor($id)] + if {!$ta} { + incr nc -1 + } + # ignore tags on starting node + if {!$ta && $i > 0} { + if {[info exists idtags($id)]} { + set tagloc($id) $id + set ta 1 + } elseif {[info exists cached_dtags($id)]} { + set tagloc($id) $cached_dtags($id) + set ta 1 + } + } + foreach a $arcnos($id) { + set d $arcstart($a) + if {!$ta && $arctags($a) ne {}} { + validate_arctags $a + if {$arctags($a) ne {}} { + lappend tagloc($id) [lindex $arctags($a) end] + } + } + if {$ta || $arctags($a) ne {}} { + set tomark [list $d] + for {set j 0} {$j < [llength $tomark]} {incr j} { + set dd [lindex $tomark $j] + if {![info exists hastaggedancestor($dd)]} { + if {[info exists done($dd)]} { + foreach b $arcnos($dd) { + lappend tomark $arcstart($b) + } + if {[info exists tagloc($dd)]} { + unset tagloc($dd) + } + } elseif {[info exists queued($dd)]} { + incr nc -1 + } + set hastaggedancestor($dd) 1 + } + } + } + if {![info exists queued($d)]} { + lappend todo $d + set queued($d) 1 + if {![info exists hastaggedancestor($d)]} { + incr nc + } + } } } - set allcommits "done" - set travindex 0 - notbusy allcommits - dispneartags -} - -# update the desc_tags and anc_tags arrays for a new tag just added -proc addedtag {id} { - global desc_tags anc_tags allparents allchildren allcommits - global idtags tagisdesc alldtags - - if {![info exists desc_tags($id)]} return - set adt $desc_tags($id) - foreach t $desc_tags($id) { - set adt [concat $adt $alldtags($t)] - } - set adt [lsort -unique $adt] - set alldtags($id) $adt - foreach t $adt { - set tagisdesc($id,$t) -1 - set tagisdesc($t,$id) 1 - } - if {[info exists anc_tags($id)]} { - set todo $anc_tags($id) - while {$todo ne {}} { - set do [lindex $todo 0] - set todo [lrange $todo 1 end] - if {[info exists tagisdesc($id,$do)]} continue - set tagisdesc($do,$id) -1 - set tagisdesc($id,$do) 1 - if {[info exists anc_tags($do)]} { - set todo [concat $todo $anc_tags($do)] + set tags {} + foreach id [array names tagloc] { + if {![info exists hastaggedancestor($id)]} { + foreach t $tagloc($id) { + if {[lsearch -exact $tags $t] < 0} { + lappend tags $t + } } } } + set t2 [clock clicks -milliseconds] + set loopix $i - set lastold $desc_tags($id) - set lastnew [list $id] - set nup 0 - set nch 0 - set todo $allparents($id) - while {$todo ne {}} { - set do [lindex $todo 0] - set todo [lrange $todo 1 end] - if {![info exists desc_tags($do)]} continue - if {$desc_tags($do) ne $lastold} { - set lastold $desc_tags($do) - set lastnew [combine_dtags $lastold [list $id]] - incr nch - } - if {$lastold eq $lastnew} continue - set desc_tags($do) $lastnew - incr nup - if {![info exists idtags($do)]} { - set todo [concat $todo $allparents($do)] + # remove tags that are descendents of other tags + for {set i 0} {$i < [llength $tags]} {incr i} { + set a [lindex $tags $i] + for {set j 0} {$j < $i} {incr j} { + set b [lindex $tags $j] + set r [anc_or_desc $a $b] + if {$r == 1} { + set tags [lreplace $tags $j $j] + incr j -1 + incr i -1 + } elseif {$r == -1} { + set tags [lreplace $tags $i $i] + incr i -1 + break + } } } - if {![info exists anc_tags($id)]} return - set lastold $anc_tags($id) - set lastnew [list $id] - set nup 0 - set nch 0 - set todo $allchildren($id) - while {$todo ne {}} { - set do [lindex $todo 0] - set todo [lrange $todo 1 end] - if {![info exists anc_tags($do)]} continue - if {$anc_tags($do) ne $lastold} { - set lastold $anc_tags($do) - set lastnew [combine_atags $lastold [list $id]] - incr nch + if {[array names growing] ne {}} { + # graph isn't finished, need to check if any tag could get + # eclipsed by another tag coming later. Simply ignore any + # tags that could later get eclipsed. + set ctags {} + foreach t $tags { + if {[is_certain $t $origid]} { + lappend ctags $t + } } - if {$lastold eq $lastnew} continue - set anc_tags($do) $lastnew - incr nup - if {![info exists idtags($do)]} { - set todo [concat $todo $allchildren($do)] + if {$tags eq $ctags} { + set cached_dtags($origid) $tags + } else { + set tags $ctags } + } else { + set cached_dtags($origid) $tags + } + set t3 [clock clicks -milliseconds] + if {0 && $t3 - $t1 >= 100} { + puts "iterating descendents ($loopix/[llength $todo] nodes) took\ + [expr {$t2-$t1}]+[expr {$t3-$t2}]ms, $nc candidates left" } + return $tags } -# update the desc_heads array for a new head just added -proc addedhead {hid head} { - global desc_heads allparents headids idheads - - set headids($head) $hid - lappend idheads($hid) $head - - set todo [list $hid] - while {$todo ne {}} { - set do [lindex $todo 0] - set todo [lrange $todo 1 end] - if {![info exists desc_heads($do)] || - [lsearch -exact $desc_heads($do) $head] >= 0} continue - set oldheads $desc_heads($do) - lappend desc_heads($do) $head - set heads $desc_heads($do) - while {1} { - set p $allparents($do) - if {[llength $p] != 1 || ![info exists desc_heads($p)] || - $desc_heads($p) ne $oldheads} break - set do $p - set desc_heads($do) $heads +proc anctags {id} { + global arcnos arcids arcout arcend arctags idtags allparents + global growing cached_atags + + if {![info exists allparents($id)]} { + return {} + } + set t1 [clock clicks -milliseconds] + set argid $id + if {[llength $arcnos($id)] == 1 && [llength $allparents($id)] == 1} { + # part-way along an arc; check that arc first + set a [lindex $arcnos($id) 0] + if {$arctags($a) ne {}} { + validate_arctags $a + set i [lsearch -exact $arcids($a) $id] + foreach t $arctags($a) { + set j [lsearch -exact $arcids($a) $t] + if {$j > $i} { + return $t + } + } + } + if {![info exists arcend($a)]} { + return {} + } + set id $arcend($a) + if {[info exists idtags($id)]} { + return $id + } + } + if {[info exists cached_atags($id)]} { + return $cached_atags($id) + } + + set origid $id + set todo [list $id] + set queued($id) 1 + set taglist {} + set nc 1 + for {set i 0} {$i < [llength $todo] && $nc > 0} {incr i} { + set id [lindex $todo $i] + set done($id) 1 + set td [info exists hastaggeddescendent($id)] + if {!$td} { + incr nc -1 + } + # ignore tags on starting node + if {!$td && $i > 0} { + if {[info exists idtags($id)]} { + set tagloc($id) $id + set td 1 + } elseif {[info exists cached_atags($id)]} { + set tagloc($id) $cached_atags($id) + set td 1 + } + } + foreach a $arcout($id) { + if {!$td && $arctags($a) ne {}} { + validate_arctags $a + if {$arctags($a) ne {}} { + lappend tagloc($id) [lindex $arctags($a) 0] + } + } + if {![info exists arcend($a)]} continue + set d $arcend($a) + if {$td || $arctags($a) ne {}} { + set tomark [list $d] + for {set j 0} {$j < [llength $tomark]} {incr j} { + set dd [lindex $tomark $j] + if {![info exists hastaggeddescendent($dd)]} { + if {[info exists done($dd)]} { + foreach b $arcout($dd) { + if {[info exists arcend($b)]} { + lappend tomark $arcend($b) + } + } + if {[info exists tagloc($dd)]} { + unset tagloc($dd) + } + } elseif {[info exists queued($dd)]} { + incr nc -1 + } + set hastaggeddescendent($dd) 1 + } + } + } + if {![info exists queued($d)]} { + lappend todo $d + set queued($d) 1 + if {![info exists hastaggeddescendent($d)]} { + incr nc + } + } + } + } + set t2 [clock clicks -milliseconds] + set loopix $i + set tags {} + foreach id [array names tagloc] { + if {![info exists hastaggeddescendent($id)]} { + foreach t $tagloc($id) { + if {[lsearch -exact $tags $t] < 0} { + lappend tags $t + } + } } - set todo [concat $todo $p] } -} -# update the desc_heads array for a head just removed -proc removedhead {hid head} { - global desc_heads allparents headids idheads + # remove tags that are ancestors of other tags + for {set i 0} {$i < [llength $tags]} {incr i} { + set a [lindex $tags $i] + for {set j 0} {$j < $i} {incr j} { + set b [lindex $tags $j] + set r [anc_or_desc $a $b] + if {$r == -1} { + set tags [lreplace $tags $j $j] + incr j -1 + incr i -1 + } elseif {$r == 1} { + set tags [lreplace $tags $i $i] + incr i -1 + break + } + } + } - unset headids($head) - if {$idheads($hid) eq $head} { - unset idheads($hid) - } else { - set i [lsearch -exact $idheads($hid) $head] - if {$i >= 0} { - set idheads($hid) [lreplace $idheads($hid) $i $i] + if {[array names growing] ne {}} { + # graph isn't finished, need to check if any tag could get + # eclipsed by another tag coming later. Simply ignore any + # tags that could later get eclipsed. + set ctags {} + foreach t $tags { + if {[is_certain $origid $t]} { + lappend ctags $t + } + } + if {$tags eq $ctags} { + set cached_atags($origid) $tags + } else { + set tags $ctags } + } else { + set cached_atags($origid) $tags + } + set t3 [clock clicks -milliseconds] + if {0 && $t3 - $t1 >= 100} { + puts "iterating ancestors ($loopix/[llength $todo] nodes) took\ + [expr {$t2-$t1}]+[expr {$t3-$t2}]ms, $nc candidates left" } + return $tags +} - set todo [list $hid] - while {$todo ne {}} { - set do [lindex $todo 0] - set todo [lrange $todo 1 end] - if {![info exists desc_heads($do)]} continue - set i [lsearch -exact $desc_heads($do) $head] - if {$i < 0} continue - set oldheads $desc_heads($do) - set heads [lreplace $desc_heads($do) $i $i] - while {1} { - set desc_heads($do) $heads - set p $allparents($do) - if {[llength $p] != 1 || ![info exists desc_heads($p)] || - $desc_heads($p) ne $oldheads} break - set do $p +# Return the list of IDs that have heads that are descendents of id, +# including id itself if it has a head. +proc descheads {id} { + global arcnos arcstart arcids archeads idheads cached_dheads + global allparents + + if {![info exists allparents($id)]} { + return {} + } + set aret {} + if {[llength $arcnos($id)] == 1 && [llength $allparents($id)] == 1} { + # part-way along an arc; check it first + set a [lindex $arcnos($id) 0] + if {$archeads($a) ne {}} { + validate_archeads $a + set i [lsearch -exact $arcids($a) $id] + foreach t $archeads($a) { + set j [lsearch -exact $arcids($a) $t] + if {$j > $i} break + lappend aret $t + } } - set todo [concat $todo $p] + set id $arcstart($a) } + set origid $id + set todo [list $id] + set seen($id) 1 + set ret {} + for {set i 0} {$i < [llength $todo]} {incr i} { + set id [lindex $todo $i] + if {[info exists cached_dheads($id)]} { + set ret [concat $ret $cached_dheads($id)] + } else { + if {[info exists idheads($id)]} { + lappend ret $id + } + foreach a $arcnos($id) { + if {$archeads($a) ne {}} { + validate_archeads $a + if {$archeads($a) ne {}} { + set ret [concat $ret $archeads($a)] + } + } + set d $arcstart($a) + if {![info exists seen($d)]} { + lappend todo $d + set seen($d) 1 + } + } + } + } + set ret [lsort -unique $ret] + set cached_dheads($origid) $ret + return [concat $ret $aret] } -# update things for a head moved to a child of its previous location -proc movedhead {id name} { - global headids idheads +proc addedtag {id} { + global arcnos arcout cached_dtags cached_atags - set oldid $headids($name) - set headids($name) $id - if {$idheads($oldid) eq $name} { - unset idheads($oldid) - } else { - set i [lsearch -exact $idheads($oldid) $name] - if {$i >= 0} { - set idheads($oldid) [lreplace $idheads($oldid) $i $i] - } + if {![info exists arcnos($id)]} return + if {![info exists arcout($id)]} { + recalcarc [lindex $arcnos($id) 0] } - lappend idheads($id) $name + catch {unset cached_dtags} + catch {unset cached_atags} } -proc changedrefs {} { - global desc_heads desc_tags anc_tags allcommits allids - global allchildren allparents idtags travindex +proc addedhead {hid head} { + global arcnos arcout cached_dheads - if {![info exists allcommits]} return - catch {unset desc_heads} - catch {unset desc_tags} - catch {unset anc_tags} - catch {unset alldtags} - catch {unset tagisdesc} - foreach id $allids { - forward_pass $id $allchildren($id) + if {![info exists arcnos($hid)]} return + if {![info exists arcout($hid)]} { + recalcarc [lindex $arcnos($hid) 0] } - if {$allcommits ne "reading"} { - set travindex [llength $allids] - if {$allcommits ne "traversing"} { - set allcommits "traversing" - after idle restartatags + catch {unset cached_dheads} +} + +proc removedhead {hid head} { + global cached_dheads + + catch {unset cached_dheads} +} + +proc movedhead {hid head} { + global arcnos arcout cached_dheads + + if {![info exists arcnos($hid)]} return + if {![info exists arcout($hid)]} { + recalcarc [lindex $arcnos($hid) 0] + } + catch {unset cached_dheads} +} + +proc changedrefs {} { + global cached_dheads cached_dtags cached_atags + global arctags archeads arcnos arcout idheads idtags + + foreach id [concat [array names idheads] [array names idtags]] { + if {[info exists arcnos($id)] && ![info exists arcout($id)]} { + set a [lindex $arcnos($id) 0] + if {![info exists donearc($a)]} { + recalcarc $a + set donearc($a) 1 + } } } + catch {unset cached_dtags} + catch {unset cached_atags} + catch {unset cached_dheads} } proc rereadrefs {} { @@ -5817,7 +6894,7 @@ proc listrefs {id} { } proc showtag {tag isnew} { - global ctext tagcontents tagids linknum + global ctext tagcontents tagids linknum tagobjid if {$isnew} { addtohistory [list showtag $tag 0] @@ -5825,6 +6902,11 @@ proc showtag {tag isnew} { $ctext conf -state normal clear_ctext set linknum 0 + if {![info exists tagcontents($tag)]} { + catch { + set tagcontents($tag) [exec git cat-file tag $tagobjid($tag)] + } + } if {[info exists tagcontents($tag)]} { set text $tagcontents($tag) } else { @@ -5844,9 +6926,9 @@ proc doquit {} { proc doprefs {} { global maxwidth maxgraphpct diffopts - global oldprefs prefstop showneartags - global bgcolor fgcolor ctext diffcolors - global uifont + global oldprefs prefstop showneartags showlocalchanges + global bgcolor fgcolor ctext diffcolors selectbgcolor + global uifont tabstop set top .gitkprefs set prefstop $top @@ -5854,7 +6936,7 @@ proc doprefs {} { raise $top return } - foreach v {maxwidth maxgraphpct diffopts showneartags} { + foreach v {maxwidth maxgraphpct diffopts showneartags showlocalchanges} { set oldprefs($v) [set $v] } toplevel $top @@ -5871,6 +6953,11 @@ proc doprefs {} { -font optionfont spinbox $top.maxpct -from 1 -to 100 -width 4 -textvariable maxgraphpct grid x $top.maxpctl $top.maxpct -sticky w + frame $top.showlocal + label $top.showlocal.l -text "Show local changes" -font optionfont + checkbutton $top.showlocal.b -variable showlocalchanges + pack $top.showlocal.b $top.showlocal.l -side left + grid x $top.showlocal -sticky w label $top.ddisp -text "Diff display options" $top.ddisp configure -font $uifont @@ -5884,6 +6971,9 @@ proc doprefs {} { checkbutton $top.ntag.b -variable showneartags pack $top.ntag.b $top.ntag.l -side left grid x $top.ntag -sticky w + label $top.tabstopl -text "tabstop" -font optionfont + spinbox $top.tabstop -from 1 -to 20 -width 4 -textvariable tabstop + grid x $top.tabstopl $top.tabstop -sticky w label $top.cdisp -text "Colors: press to choose" $top.cdisp configure -font $uifont @@ -5912,6 +7002,10 @@ proc doprefs {} { "diff hunk header" \ [list $ctext tag conf hunksep -foreground]] grid x $top.hunksepbut $top.hunksep -sticky w + label $top.selbgsep -padx 40 -relief sunk -background $selectbgcolor + button $top.selbgbut -text "Select bg" -font optionfont \ + -command [list choosecolor selectbgcolor 0 $top.selbgsep background setselbg] + grid x $top.selbgbut $top.selbgsep -sticky w frame $top.buts button $top.buts.ok -text "OK" -command prefsok -default active @@ -5936,6 +7030,16 @@ proc choosecolor {v vi w x cmd} { eval $cmd $c } +proc setselbg {c} { + global bglist cflist + foreach w $bglist { + $w configure -selectbackground $c + } + $cflist tag configure highlight \ + -background [$cflist cget -selectbackground] + allcanvs itemconf secsel -fill $c +} + proc setbg {c} { global bglist @@ -5956,9 +7060,9 @@ proc setfg {c} { proc prefscan {} { global maxwidth maxgraphpct diffopts - global oldprefs prefstop showneartags + global oldprefs prefstop showneartags showlocalchanges - foreach v {maxwidth maxgraphpct diffopts showneartags} { + foreach v {maxwidth maxgraphpct diffopts showneartags showlocalchanges} { set $v $oldprefs($v) } catch {destroy $prefstop} @@ -5967,10 +7071,19 @@ proc prefscan {} { proc prefsok {} { global maxwidth maxgraphpct - global oldprefs prefstop showneartags + global oldprefs prefstop showneartags showlocalchanges + global charspc ctext tabstop catch {destroy $prefstop} unset prefstop + $ctext configure -tabs "[expr {$tabstop * $charspc}]" + if {$showlocalchanges != $oldprefs(showlocalchanges)} { + if {$showlocalchanges} { + doshowlocalchanges + } else { + dohidelocalchanges + } + } if {$maxwidth != $oldprefs(maxwidth) || $maxgraphpct != $oldprefs(maxgraphpct)} { redisplay @@ -5980,7 +7093,10 @@ proc prefsok {} { } proc formatdate {d} { - return [clock format $d -format "%Y-%m-%d %H:%M:%S"] + if {$d ne {}} { + set d [clock format $d -format "%Y-%m-%d %H:%M:%S"] + } + return $d } # This list of encoding names and aliases is distilled from @@ -6276,6 +7392,7 @@ if {$tclencoding == {}} { set mainfont {Helvetica 9} set textfont {Courier 9} set uifont {Helvetica 9 bold} +set tabstop 8 set findmergefiles 0 set maxgraphpct 50 set maxwidth 16 @@ -6287,11 +7404,15 @@ set mingaplen 30 set cmitmode "patch" set wrapcomment "none" set showneartags 1 +set maxrefs 20 +set maxlinelen 200 +set showlocalchanges 1 set colors {green red blue magenta darkgrey brown orange} set bgcolor white set fgcolor black set diffcolors {red "#00a000" blue} +set selectbgcolor gray85 catch {source ~/.gitk} @@ -6338,6 +7459,9 @@ if {$i >= 0} { } } +set nullid "0000000000000000000000000000000000000000" + +set runq {} set history {} set historyindex 0 set fh_serial 0 @@ -6362,6 +7486,9 @@ set cmdlineok 0 set stopped 0 set stuffsaved 0 set patchnum 0 +set lookingforhead 0 +set localrow -1 +set lserial 0 setcoords makewindow wm title . "[file tail $argv0]: [file tail [pwd]]" diff --git a/index-pack.c b/index-pack.c index 82c8da368..8403c36b6 100644 --- a/index-pack.c +++ b/index-pack.c @@ -634,7 +634,8 @@ static void final(const char *final_pack_name, const char *curr_pack_name, write_or_die(keep_fd, keep_msg, keep_msg_len); write_or_die(keep_fd, "\n", 1); } - close(keep_fd); + if (close(keep_fd) != 0) + die("cannot write keep file"); report = "keep"; } } diff --git a/interpolate.c b/interpolate.c index fb30694f4..00826778f 100644 --- a/interpolate.c +++ b/interpolate.c @@ -44,33 +44,33 @@ void interp_clear_table(struct interp *table, int ninterps) * { "%%", "%"}, * } * - * Returns 1 on a successful substitution pass that fits in result, - * Returns 0 on a failed or overflowing substitution pass. + * Returns 0 on a successful substitution pass that fits in result, + * Returns a number of bytes needed to hold the full substituted + * string otherwise. */ -int interpolate(char *result, int reslen, +unsigned long interpolate(char *result, unsigned long reslen, const char *orig, const struct interp *interps, int ninterps) { const char *src = orig; char *dest = result; - int newlen = 0; + unsigned long newlen = 0; const char *name, *value; - int namelen, valuelen; + unsigned long namelen, valuelen; int i; char c; memset(result, 0, reslen); - while ((c = *src) && newlen < reslen - 1) { + while ((c = *src)) { if (c == '%') { /* Try to match an interpolation string. */ for (i = 0; i < ninterps; i++) { name = interps[i].name; namelen = strlen(name); - if (strncmp(src, name, namelen) == 0) { + if (strncmp(src, name, namelen) == 0) break; - } } /* Check for valid interpolation. */ @@ -78,29 +78,25 @@ int interpolate(char *result, int reslen, value = interps[i].value; valuelen = strlen(value); - if (newlen + valuelen < reslen - 1) { + if (newlen + valuelen + 1 < reslen) { /* Substitute. */ strncpy(dest, value, valuelen); - newlen += valuelen; dest += valuelen; - src += namelen; - } else { - /* Something's not fitting. */ - return 0; } - - } else { - /* Skip bogus interpolation. */ - *dest++ = *src++; - newlen++; + newlen += valuelen; + src += namelen; + continue; } - - } else { - /* Straight copy one non-interpolation character. */ - *dest++ = *src++; - newlen++; } + /* Straight copy one non-interpolation character. */ + if (newlen + 1 < reslen) + *dest++ = *src; + src++; + newlen++; } - return newlen < reslen - 1; + if (newlen + 1 < reslen) + return 0; + else + return newlen + 2; } diff --git a/interpolate.h b/interpolate.h index 16a26b998..77407e67d 100644 --- a/interpolate.h +++ b/interpolate.h @@ -19,8 +19,8 @@ struct interp { extern void interp_set_entry(struct interp *table, int slot, const char *value); extern void interp_clear_table(struct interp *table, int ninterps); -extern int interpolate(char *result, int reslen, - const char *orig, - const struct interp *interps, int ninterps); +extern unsigned long interpolate(char *result, unsigned long reslen, + const char *orig, + const struct interp *interps, int ninterps); #endif /* INTERPOLATE_H */ diff --git a/log-tree.c b/log-tree.c index 4bef90914..ced3f332e 100644 --- a/log-tree.c +++ b/log-tree.c @@ -79,16 +79,25 @@ static int detect_any_signoff(char *letter, int size) return seen_head && seen_name; } -static int append_signoff(char *buf, int buf_sz, int at, const char *signoff) +static unsigned long append_signoff(char **buf_p, unsigned long *buf_sz_p, + unsigned long at, const char *signoff) { static const char signed_off_by[] = "Signed-off-by: "; - int signoff_len = strlen(signoff); + size_t signoff_len = strlen(signoff); int has_signoff = 0; - char *cp = buf; - - /* Do we have enough space to add it? */ - if (buf_sz - at <= strlen(signed_off_by) + signoff_len + 3) - return at; + char *cp; + char *buf; + unsigned long buf_sz; + + buf = *buf_p; + buf_sz = *buf_sz_p; + if (buf_sz <= at + strlen(signed_off_by) + signoff_len + 3) { + buf_sz += strlen(signed_off_by) + signoff_len + 3; + buf = xrealloc(buf, buf_sz); + *buf_p = buf; + *buf_sz_p = buf_sz; + } + cp = buf; /* First see if we already have the sign-off by the signer */ while ((cp = strstr(cp, signed_off_by))) { @@ -133,7 +142,8 @@ static unsigned int digits_in_number(unsigned int number) void show_log(struct rev_info *opt, const char *sep) { - static char this_header[16384]; + char *msgbuf = NULL; + unsigned long msgbuf_len = 0; struct log_info *log = opt->loginfo; struct commit *commit = log->commit, *parent = log->parent; int abbrev = opt->diffopt.abbrev; @@ -278,14 +288,15 @@ void show_log(struct rev_info *opt, const char *sep) /* * And then the pretty-printed message itself */ - len = pretty_print_commit(opt->commit_format, commit, ~0u, this_header, - sizeof(this_header), abbrev, subject, + len = pretty_print_commit(opt->commit_format, commit, ~0u, + &msgbuf, &msgbuf_len, abbrev, subject, extra_headers, opt->date_mode); if (opt->add_signoff) - len = append_signoff(this_header, sizeof(this_header), len, + len = append_signoff(&msgbuf, &msgbuf_len, len, opt->add_signoff); - printf("%s%s%s", this_header, extra, sep); + printf("%s%s%s", msgbuf, extra, sep); + free(msgbuf); } int log_tree_diff_flush(struct rev_info *opt) @@ -397,5 +408,6 @@ int log_tree_commit(struct rev_info *opt, struct commit *commit) shown = 1; } opt->loginfo = NULL; + maybe_flush_or_die(stdout, "stdout"); return shown; } @@ -252,7 +252,7 @@ char *enter_repo(char *path, int strict) if (access("objects", X_OK) == 0 && access("refs", X_OK) == 0 && validate_headref("HEAD") == 0) { - setenv("GIT_DIR", ".", 1); + setenv(GIT_DIR_ENVIRONMENT, ".", 1); check_repository_format(); return path; } @@ -188,7 +188,8 @@ static int quote_c_style_counted(const char *name, int namelen, #define EMITQ() EMIT('\\') const char *sp; - int ch, count = 0, needquote = 0; + unsigned char ch; + int count = 0, needquote = 0; if (!no_dq) EMIT('"'); @@ -197,7 +198,7 @@ static int quote_c_style_counted(const char *name, int namelen, if (!ch) break; if ((ch < ' ') || (ch == '"') || (ch == '\\') || - (ch >= 0177)) { + (quote_path_fully && (ch >= 0177))) { needquote = 1; switch (ch) { case '\a': EMITQ(); ch = 'a'; break; diff --git a/read-cache.c b/read-cache.c index 4362b11f4..a363f312c 100644 --- a/read-cache.c +++ b/read-cache.c @@ -350,6 +350,34 @@ int remove_file_from_index(struct index_state *istate, const char *path) return 0; } +static int compare_name(struct cache_entry *ce, const char *path, int namelen) +{ + return namelen != ce_namelen(ce) || memcmp(path, ce->name, namelen); +} + +static int index_name_pos_also_unmerged(struct index_state *istate, + const char *path, int namelen) +{ + int pos = index_name_pos(istate, path, namelen); + struct cache_entry *ce; + + if (pos >= 0) + return pos; + + /* maybe unmerged? */ + pos = -1 - pos; + if (pos >= istate->cache_nr || + compare_name((ce = istate->cache[pos]), path, namelen)) + return -1; + + /* order of preference: stage 2, 1, 3 */ + if (ce_stage(ce) == 1 && pos + 1 < istate->cache_nr && + ce_stage((ce = istate->cache[pos + 1])) == 2 && + !compare_name(ce, path, namelen)) + pos++; + return pos; +} + int add_file_to_index(struct index_state *istate, const char *path, int verbose) { int size, namelen; @@ -380,7 +408,7 @@ int add_file_to_index(struct index_state *istate, const char *path, int verbose) * from it, otherwise assume unexecutable regular file. */ struct cache_entry *ent; - int pos = index_name_pos(istate, path, namelen); + int pos = index_name_pos_also_unmerged(istate, path, namelen); ent = (0 <= pos) ? istate->cache[pos] : NULL; ce->ce_mode = ce_mode_from_stat(ent, st.st_mode); @@ -1106,8 +1106,7 @@ static int log_ref_write(const char *ref_name, const unsigned char *old_sha1, len += sprintf(logrec + len - 1, "\t%.*s\n", msglen, msg) - 1; written = len <= maxlen ? write_in_full(logfd, logrec, len) : -1; free(logrec); - close(logfd); - if (written != len) + if (close(logfd) != 0 || written != len) return error("Unable to append to %s", log_file); return 0; } @@ -1204,8 +1203,7 @@ int create_symref(const char *ref_target, const char *refs_heads_master, goto error_free_return; } written = write_in_full(fd, ref, len); - close(fd); - if (written != len) { + if (close(fd) != 0 || written != len) { error("Unable to write to %s", lockpath); goto error_unlink_return; } @@ -333,7 +333,6 @@ static int count_refspec_match(const char *pattern, for (weak_match = match = 0; refs; refs = refs->next) { char *name = refs->name; int namelen = strlen(name); - int weak_match; if (namelen < patlen || memcmp(name + namelen - patlen, pattern, patlen)) @@ -406,90 +405,96 @@ static struct ref *try_explicit_object_name(const char *name) return ref; } -static int match_explicit_refs(struct ref *src, struct ref *dst, - struct ref ***dst_tail, struct refspec *rs, - int rs_nr) +static struct ref *make_dst(const char *name, struct ref ***dst_tail) { - int i, errs; - for (i = errs = 0; i < rs_nr; i++) { - struct ref *matched_src, *matched_dst; + struct ref *dst; + size_t len; - const char *dst_value = rs[i].dst; + len = strlen(name) + 1; + dst = xcalloc(1, sizeof(*dst) + len); + memcpy(dst->name, name, len); + link_dst_tail(dst, dst_tail); + return dst; +} - if (rs[i].pattern) - continue; +static int match_explicit(struct ref *src, struct ref *dst, + struct ref ***dst_tail, + struct refspec *rs, + int errs) +{ + struct ref *matched_src, *matched_dst; - if (dst_value == NULL) - dst_value = rs[i].src; + const char *dst_value = rs->dst; - matched_src = matched_dst = NULL; - switch (count_refspec_match(rs[i].src, src, &matched_src)) { - case 1: - break; - case 0: - /* The source could be in the get_sha1() format - * not a reference name. :refs/other is a - * way to delete 'other' ref at the remote end. - */ - matched_src = try_explicit_object_name(rs[i].src); - if (matched_src) - break; - errs = 1; - error("src refspec %s does not match any.", - rs[i].src); - break; - default: - errs = 1; - error("src refspec %s matches more than one.", - rs[i].src); - break; - } - switch (count_refspec_match(dst_value, dst, &matched_dst)) { - case 1: - break; - case 0: - if (!memcmp(dst_value, "refs/", 5)) { - int len = strlen(dst_value) + 1; - matched_dst = xcalloc(1, sizeof(*dst) + len); - memcpy(matched_dst->name, dst_value, len); - link_dst_tail(matched_dst, dst_tail); - } - else if (!strcmp(rs[i].src, dst_value) && - matched_src) { - /* pushing "master:master" when - * remote does not have master yet. - */ - int len = strlen(matched_src->name) + 1; - matched_dst = xcalloc(1, sizeof(*dst) + len); - memcpy(matched_dst->name, matched_src->name, - len); - link_dst_tail(matched_dst, dst_tail); - } - else { - errs = 1; - error("dst refspec %s does not match any " - "existing ref on the remote and does " - "not start with refs/.", dst_value); - } - break; - default: - errs = 1; - error("dst refspec %s matches more than one.", - dst_value); + if (rs->pattern) + return errs; + + matched_src = matched_dst = NULL; + switch (count_refspec_match(rs->src, src, &matched_src)) { + case 1: + break; + case 0: + /* The source could be in the get_sha1() format + * not a reference name. :refs/other is a + * way to delete 'other' ref at the remote end. + */ + matched_src = try_explicit_object_name(rs->src); + if (matched_src) break; - } - if (errs) - continue; - if (matched_dst->peer_ref) { - errs = 1; - error("dst ref %s receives from more than one src.", - matched_dst->name); - } - else { - matched_dst->peer_ref = matched_src; - matched_dst->force = rs[i].force; - } + error("src refspec %s does not match any.", + rs->src); + break; + default: + matched_src = NULL; + error("src refspec %s matches more than one.", + rs->src); + break; + } + + if (!matched_src) + errs = 1; + + if (dst_value == NULL) + dst_value = matched_src->name; + + switch (count_refspec_match(dst_value, dst, &matched_dst)) { + case 1: + break; + case 0: + if (!memcmp(dst_value, "refs/", 5)) + matched_dst = make_dst(dst_value, dst_tail); + else + error("dst refspec %s does not match any " + "existing ref on the remote and does " + "not start with refs/.", dst_value); + break; + default: + matched_dst = NULL; + error("dst refspec %s matches more than one.", + dst_value); + break; + } + if (errs || matched_dst == NULL) + return 1; + if (matched_dst->peer_ref) { + errs = 1; + error("dst ref %s receives from more than one src.", + matched_dst->name); + } + else { + matched_dst->peer_ref = matched_src; + matched_dst->force = rs->force; } + return errs; +} + +static int match_explicit_refs(struct ref *src, struct ref *dst, + struct ref ***dst_tail, struct refspec *rs, + int rs_nr) +{ + int i, errs; + for (i = errs = 0; i < rs_nr; i++) + errs |= match_explicit(src, dst, dst_tail, &rs[i], errs); return -errs; } @@ -513,6 +518,11 @@ static const struct refspec *check_pattern_match(const struct refspec *rs, return NULL; } +/* + * Note. This is used only by "push"; refspec matching rules for + * push and fetch are subtly different, so do not try to reuse it + * without thinking. + */ int match_refs(struct ref *src, struct ref *dst, struct ref ***dst_tail, int nr_refspec, char **refspec, int all) { @@ -536,10 +546,11 @@ int match_refs(struct ref *src, struct ref *dst, struct ref ***dst_tail, } if (pat) { - dst_name = xmalloc(strlen(pat->dst) + + const char *dst_side = pat->dst ? pat->dst : pat->src; + dst_name = xmalloc(strlen(dst_side) + strlen(src->name) - strlen(pat->src) + 2); - strcpy(dst_name, pat->dst); + strcpy(dst_name, dst_side); strcat(dst_name, src->name + strlen(pat->src)); } else dst_name = xstrdup(src->name); @@ -554,11 +565,8 @@ int match_refs(struct ref *src, struct ref *dst, struct ref ***dst_tail, goto free_name; if (!dst_peer) { /* Create a new one and link it */ - int len = strlen(dst_name) + 1; - dst_peer = xcalloc(1, sizeof(*dst_peer) + len); - memcpy(dst_peer->name, dst_name, len); + dst_peer = make_dst(dst_name, dst_tail); hashcpy(dst_peer->new_sha1, src->new_sha1); - link_dst_tail(dst_peer, dst_tail); } dst_peer->peer_ref = src; free_name: diff --git a/revision.c b/revision.c index b12c25e2b..7834bb108 100644 --- a/revision.c +++ b/revision.c @@ -1180,7 +1180,8 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, const ch opts = diff_opt_parse(&revs->diffopt, argv+i, argc-i); if (opts > 0) { - revs->diff = 1; + if (strcmp(argv[i], "-z")) + revs->diff = 1; i += opts - 1; continue; } @@ -1229,7 +1230,9 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, const ch if (revs->prune_data) { diff_tree_setup_paths(revs->prune_data, &revs->pruning); - revs->prune_fn = try_to_simplify_commit; + /* Can't prune commits with rename following: the paths change.. */ + if (!revs->diffopt.follow_renames) + revs->prune_fn = try_to_simplify_commit; if (!revs->full_diff) diff_tree_setup_paths(revs->prune_data, &revs->diffopt); } @@ -95,7 +95,7 @@ void verify_non_filename(const char *prefix, const char *arg) const char *name; struct stat st; - if (is_inside_git_dir()) + if (!is_inside_work_tree() || is_inside_git_dir()) return; if (*arg == '-') return; /* flag */ @@ -174,41 +174,96 @@ static int inside_git_dir = -1; int is_inside_git_dir(void) { - if (inside_git_dir < 0) { - char buffer[1024]; - - if (is_bare_repository()) - return (inside_git_dir = 1); - if (getcwd(buffer, sizeof(buffer))) { - const char *git_dir = get_git_dir(), *cwd = buffer; - while (*git_dir && *git_dir == *cwd) { - git_dir++; - cwd++; - } - inside_git_dir = !*git_dir; - } else - inside_git_dir = 0; + if (inside_git_dir >= 0) + return inside_git_dir; + die("BUG: is_inside_git_dir called before setup_git_directory"); +} + +static int inside_work_tree = -1; + +int is_inside_work_tree(void) +{ + if (inside_git_dir >= 0) + return inside_work_tree; + die("BUG: is_inside_work_tree called before setup_git_directory"); +} + +static char *gitworktree_config; + +static int git_setup_config(const char *var, const char *value) +{ + if (!strcmp(var, "core.worktree")) { + if (gitworktree_config) + strlcpy(gitworktree_config, value, PATH_MAX); + return 0; } - return inside_git_dir; + return git_default_config(var, value); } const char *setup_git_directory_gently(int *nongit_ok) { static char cwd[PATH_MAX+1]; - const char *gitdirenv; - int len, offset; + char worktree[PATH_MAX+1], gitdir[PATH_MAX+1]; + const char *gitdirenv, *gitworktree; + int wt_rel_gitdir = 0; - /* - * If GIT_DIR is set explicitly, we're not going - * to do any discovery, but we still do repository - * validation. - */ gitdirenv = getenv(GIT_DIR_ENVIRONMENT); - if (gitdirenv) { - if (PATH_MAX - 40 < strlen(gitdirenv)) - die("'$%s' too big", GIT_DIR_ENVIRONMENT); - if (is_git_directory(gitdirenv)) + if (!gitdirenv) { + int len, offset; + + if (!getcwd(cwd, sizeof(cwd)-1) || cwd[0] != '/') + die("Unable to read current working directory"); + + offset = len = strlen(cwd); + for (;;) { + if (is_git_directory(".git")) + break; + if (offset == 0) { + offset = -1; + break; + } + chdir(".."); + while (cwd[--offset] != '/') + ; /* do nothing */ + } + + if (offset >= 0) { + inside_work_tree = 1; + git_config(git_default_config); + if (offset == len) { + inside_git_dir = 0; + return NULL; + } + + cwd[len++] = '/'; + cwd[len] = '\0'; + inside_git_dir = !prefixcmp(cwd + offset + 1, ".git/"); + return cwd + offset + 1; + } + + if (chdir(cwd)) + die("Cannot come back to cwd"); + if (!is_git_directory(".")) { + if (nongit_ok) { + *nongit_ok = 1; + return NULL; + } + die("Not a git repository"); + } + setenv(GIT_DIR_ENVIRONMENT, cwd, 1); + gitdirenv = getenv(GIT_DIR_ENVIRONMENT); + if (!gitdirenv) + die("getenv after setenv failed"); + } + + if (PATH_MAX - 40 < strlen(gitdirenv)) { + if (nongit_ok) { + *nongit_ok = 1; return NULL; + } + die("$%s too big", GIT_DIR_ENVIRONMENT); + } + if (!is_git_directory(gitdirenv)) { if (nongit_ok) { *nongit_ok = 1; return NULL; @@ -218,41 +273,92 @@ const char *setup_git_directory_gently(int *nongit_ok) if (!getcwd(cwd, sizeof(cwd)-1) || cwd[0] != '/') die("Unable to read current working directory"); + if (chdir(gitdirenv)) { + if (nongit_ok) { + *nongit_ok = 1; + return NULL; + } + die("Cannot change directory to $%s '%s'", + GIT_DIR_ENVIRONMENT, gitdirenv); + } + if (!getcwd(gitdir, sizeof(gitdir)-1) || gitdir[0] != '/') + die("Unable to read current working directory"); + if (chdir(cwd)) + die("Cannot come back to cwd"); - offset = len = strlen(cwd); - for (;;) { - if (is_git_directory(".git")) - break; - chdir(".."); - do { - if (!offset) { - if (is_git_directory(cwd)) { - if (chdir(cwd)) - die("Cannot come back to cwd"); - setenv(GIT_DIR_ENVIRONMENT, cwd, 1); - inside_git_dir = 1; - return NULL; - } - if (nongit_ok) { - if (chdir(cwd)) - die("Cannot come back to cwd"); - *nongit_ok = 1; - return NULL; - } - die("Not a git repository"); + /* + * In case there is a work tree we may change the directory, + * therefore make GIT_DIR an absolute path. + */ + if (gitdirenv[0] != '/') { + setenv(GIT_DIR_ENVIRONMENT, gitdir, 1); + gitdirenv = getenv(GIT_DIR_ENVIRONMENT); + if (!gitdirenv) + die("getenv after setenv failed"); + if (PATH_MAX - 40 < strlen(gitdirenv)) { + if (nongit_ok) { + *nongit_ok = 1; + return NULL; } - } while (cwd[--offset] != '/'); + die("$%s too big after expansion to absolute path", + GIT_DIR_ENVIRONMENT); + } + } + + strcat(cwd, "/"); + strcat(gitdir, "/"); + inside_git_dir = !prefixcmp(cwd, gitdir); + + gitworktree = getenv(GIT_WORK_TREE_ENVIRONMENT); + if (!gitworktree) { + gitworktree_config = worktree; + worktree[0] = '\0'; + } + git_config(git_setup_config); + if (!gitworktree) { + gitworktree_config = NULL; + if (worktree[0]) + gitworktree = worktree; + if (gitworktree && gitworktree[0] != '/') + wt_rel_gitdir = 1; + } + + if (wt_rel_gitdir && chdir(gitdirenv)) + die("Cannot change directory to $%s '%s'", + GIT_DIR_ENVIRONMENT, gitdirenv); + if (gitworktree && chdir(gitworktree)) { + if (nongit_ok) { + if (wt_rel_gitdir && chdir(cwd)) + die("Cannot come back to cwd"); + *nongit_ok = 1; + return NULL; + } + if (wt_rel_gitdir) + die("Cannot change directory to working tree '%s'" + " from $%s", gitworktree, GIT_DIR_ENVIRONMENT); + else + die("Cannot change directory to working tree '%s'", + gitworktree); } + if (!getcwd(worktree, sizeof(worktree)-1) || worktree[0] != '/') + die("Unable to read current working directory"); + strcat(worktree, "/"); + inside_work_tree = !prefixcmp(cwd, worktree); - if (offset == len) + if (gitworktree && inside_work_tree && !prefixcmp(worktree, gitdir) && + strcmp(worktree, gitdir)) { + inside_git_dir = 0; + } + + if (!inside_work_tree) { + if (chdir(cwd)) + die("Cannot come back to cwd"); return NULL; + } - /* Make "offset" point to past the '/', and add a '/' at the end */ - offset++; - cwd[len++] = '/'; - cwd[len] = 0; - inside_git_dir = !prefixcmp(cwd + offset, ".git/"); - return cwd + offset; + if (!strcmp(cwd, worktree)) + return NULL; + return cwd+strlen(worktree); } int git_config_perm(const char *var, const char *value) diff --git a/sha1_file.c b/sha1_file.c index 7628ee97d..f2b1ae032 100644 --- a/sha1_file.c +++ b/sha1_file.c @@ -510,7 +510,10 @@ static int check_packed_git_idx(const char *path, struct packed_git *p) * for offsets larger than 2^31. */ unsigned long min_size = 8 + 4*256 + nr*(20 + 4 + 4) + 20 + 20; - if (idx_size < min_size || idx_size > min_size + (nr - 1)*8) { + unsigned long max_size = min_size; + if (nr) + max_size += (nr - 1)*8; + if (idx_size < min_size || idx_size > max_size) { munmap(idx_map, idx_size); return error("wrong index file size in %s", path); } diff --git a/t/t0022-crlf-rename.sh b/t/t0022-crlf-rename.sh new file mode 100755 index 000000000..430a1d1d3 --- /dev/null +++ b/t/t0022-crlf-rename.sh @@ -0,0 +1,33 @@ +#!/bin/sh + +test_description='ignore CR in CRLF sequence while computing similiarity' + +. ./test-lib.sh + +test_expect_success setup ' + + cat ../t0022-crlf-rename.sh >sample && + git add sample && + + test_tick && + git commit -m Initial && + + sed -e "s/\$/
/" ../t0022-crlf-rename.sh >elpmas && + git add elpmas && + rm -f sample && + + test_tick && + git commit -a -m Second + +' + +test_expect_success 'diff -M' ' + + git diff-tree -M -r --name-status HEAD^ HEAD | + sed -e "s/R[0-9]*/RNUM/" >actual && + echo "RNUM sample elpmas" >expect && + diff -u expect actual + +' + +test_done diff --git a/t/t0030-stripspace.sh b/t/t0030-stripspace.sh new file mode 100755 index 000000000..f4294d72d --- /dev/null +++ b/t/t0030-stripspace.sh @@ -0,0 +1,355 @@ +#!/bin/sh +# +# Copyright (c) 2007 Carlos Rica +# + +test_description='git-stripspace' + +. ./test-lib.sh + +t40='A quick brown fox jumps over the lazy do' +s40=' ' +sss="$s40$s40$s40$s40$s40$s40$s40$s40$s40$s40" # 400 +ttt="$t40$t40$t40$t40$t40$t40$t40$t40$t40$t40" # 400 + +test_expect_success \ + 'long lines without spaces should be unchanged' ' + echo "$ttt" >expect && + git-stripspace <expect >actual && + git diff expect actual && + + echo "$ttt$ttt" >expect && + git-stripspace <expect >actual && + git diff expect actual && + + echo "$ttt$ttt$ttt" >expect && + git-stripspace <expect >actual && + git diff expect actual && + + echo "$ttt$ttt$ttt$ttt" >expect && + git-stripspace <expect >actual && + git diff expect actual +' + +test_expect_success \ + 'lines with spaces at the beginning should be unchanged' ' + echo "$sss$ttt" >expect && + git-stripspace <expect >actual && + git diff expect actual && + + echo "$sss$sss$ttt" >expect && + git-stripspace <expect >actual && + git diff expect actual && + + echo "$sss$sss$sss$ttt" >expect && + git-stripspace <expect >actual && + git diff expect actual +' + +test_expect_success \ + 'lines with intermediate spaces should be unchanged' ' + echo "$ttt$sss$ttt" >expect && + git-stripspace <expect >actual && + git diff expect actual && + + echo "$ttt$sss$sss$ttt" >expect && + git-stripspace <expect >actual && + git diff expect actual +' + +test_expect_success \ + 'consecutive blank lines should be unified' ' + printf "$ttt\n\n$ttt\n" > expect && + printf "$ttt\n\n\n\n\n$ttt\n" | git-stripspace >actual && + git diff expect actual && + + printf "$ttt$ttt\n\n$ttt\n" > expect && + printf "$ttt$ttt\n\n\n\n\n$ttt\n" | git-stripspace >actual && + git diff expect actual && + + printf "$ttt$ttt$ttt\n\n$ttt\n" > expect && + printf "$ttt$ttt$ttt\n\n\n\n\n$ttt\n" | git-stripspace >actual && + git diff expect actual && + + printf "$ttt\n\n$ttt\n" > expect && + printf "$ttt\n\n\n\n\n$ttt\n" | git-stripspace >actual && + git diff expect actual && + + printf "$ttt\n\n$ttt$ttt\n" > expect && + printf "$ttt\n\n\n\n\n$ttt$ttt\n" | git-stripspace >actual && + git diff expect actual && + + printf "$ttt\n\n$ttt$ttt$ttt\n" > expect && + printf "$ttt\n\n\n\n\n$ttt$ttt$ttt\n" | git-stripspace >actual && + git diff expect actual +' + +test_expect_success \ + 'consecutive blank lines at the beginning should be removed' ' + printf "" > expect && + printf "\n" | git-stripspace >actual && + git diff expect actual && + + printf "" > expect && + printf "\n\n\n" | git-stripspace >actual && + git diff expect actual && + + printf "" > expect && + printf "$sss\n$sss\n$sss\n" | git-stripspace >actual && + git diff expect actual && + + printf "" > expect && + printf "$sss$sss\n$sss\n\n" | git-stripspace >actual && + git diff expect actual && + + printf "" > expect && + printf "\n$sss\n$sss$sss\n" | git-stripspace >actual && + git diff expect actual && + + printf "" > expect && + printf "$sss$sss$sss$sss\n\n\n" | git-stripspace >actual && + git diff expect actual && + + printf "" > expect && + printf "\n$sss$sss$sss$sss\n\n" | git-stripspace >actual && + git diff expect actual && + + printf "" > expect && + printf "\n\n$sss$sss$sss$sss\n" | git-stripspace >actual && + git diff expect actual && + + printf "$ttt\n" > expect && + printf "\n$ttt\n" | git-stripspace >actual && + git diff expect actual && + + printf "$ttt\n" > expect && + printf "\n\n\n$ttt\n" | git-stripspace >actual && + git diff expect actual && + + printf "$ttt$ttt\n" > expect && + printf "\n\n\n$ttt$ttt\n" | git-stripspace >actual && + git diff expect actual && + + printf "$ttt$ttt$ttt\n" > expect && + printf "\n\n\n$ttt$ttt$ttt\n" | git-stripspace >actual && + git diff expect actual && + + printf "$ttt$ttt$ttt$ttt\n" > expect && + printf "\n\n\n$ttt$ttt$ttt$ttt\n" | git-stripspace >actual && + git diff expect actual && + + printf "$ttt\n" > expect && + printf "$sss\n$sss\n$sss\n$ttt\n" | git-stripspace >actual && + git diff expect actual && + + printf "$ttt\n" > expect && + printf "\n$sss\n$sss$sss\n$ttt\n" | git-stripspace >actual && + git diff expect actual && + + printf "$ttt\n" > expect && + printf "$sss$sss\n$sss\n\n$ttt\n" | git-stripspace >actual && + git diff expect actual && + + printf "$ttt\n" > expect && + printf "$sss$sss$sss\n\n\n$ttt\n" | git-stripspace >actual && + git diff expect actual && + + printf "$ttt\n" > expect && + printf "\n$sss$sss$sss\n\n$ttt\n" | git-stripspace >actual && + git diff expect actual && + + printf "$ttt\n" > expect && + printf "\n\n$sss$sss$sss\n$ttt\n" | git-stripspace >actual && + git diff expect actual +' + +test_expect_success \ + 'consecutive blank lines at the end should be removed' ' + printf "$ttt\n" > expect && + printf "$ttt\n\n" | git-stripspace >actual && + git diff expect actual && + + printf "$ttt\n" > expect && + printf "$ttt\n\n\n\n" | git-stripspace >actual && + git diff expect actual && + + printf "$ttt$ttt\n" > expect && + printf "$ttt$ttt\n\n\n\n" | git-stripspace >actual && + git diff expect actual && + + printf "$ttt$ttt$ttt\n" > expect && + printf "$ttt$ttt$ttt\n\n\n\n" | git-stripspace >actual && + git diff expect actual && + + printf "$ttt$ttt$ttt$ttt\n" > expect && + printf "$ttt$ttt$ttt$ttt\n\n\n\n" | git-stripspace >actual && + git diff expect actual && + + printf "$ttt\n" > expect && + printf "$ttt\n$sss\n$sss\n$sss\n" | git-stripspace >actual && + git diff expect actual && + + printf "$ttt\n" > expect && + printf "$ttt\n\n$sss\n$sss$sss\n" | git-stripspace >actual && + git diff expect actual && + + printf "$ttt\n" > expect && + printf "$ttt\n$sss$sss\n$sss\n\n" | git-stripspace >actual && + git diff expect actual && + + printf "$ttt\n" > expect && + printf "$ttt\n$sss$sss$sss\n\n\n" | git-stripspace >actual && + git diff expect actual && + + printf "$ttt\n" > expect && + printf "$ttt\n\n$sss$sss$sss\n\n" | git-stripspace >actual && + git diff expect actual && + + printf "$ttt\n" > expect && + printf "$ttt\n\n\n$sss$sss$sss\n" | git-stripspace >actual && + git diff expect actual +' + +test_expect_success \ + 'text without newline at end should end with newline' ' + test `printf "$ttt" | git-stripspace | wc -l` -gt 0 && + test `printf "$ttt$ttt" | git-stripspace | wc -l` -gt 0 && + test `printf "$ttt$ttt$ttt" | git-stripspace | wc -l` -gt 0 && + test `printf "$ttt$ttt$ttt$ttt" | git-stripspace | wc -l` -gt 0 +' + +# text plus spaces at the end: + +test_expect_success \ + 'text plus spaces without newline at end should end with newline' ' + test `printf "$ttt$sss" | git-stripspace | wc -l` -gt 0 && + test `printf "$ttt$ttt$sss" | git-stripspace | wc -l` -gt 0 && + test `printf "$ttt$ttt$ttt$sss" | git-stripspace | wc -l` -gt 0 + test `printf "$ttt$sss$sss" | git-stripspace | wc -l` -gt 0 && + test `printf "$ttt$ttt$sss$sss" | git-stripspace | wc -l` -gt 0 && + test `printf "$ttt$sss$sss$sss" | git-stripspace | wc -l` -gt 0 +' + +test_expect_failure \ + 'text plus spaces without newline at end should not show spaces' ' + printf "$ttt$sss" | git-stripspace | grep -q " " || + printf "$ttt$ttt$sss" | git-stripspace | grep -q " " || + printf "$ttt$ttt$ttt$sss" | git-stripspace | grep -q " " || + printf "$ttt$sss$sss" | git-stripspace | grep -q " " || + printf "$ttt$ttt$sss$sss" | git-stripspace | grep -q " " || + printf "$ttt$sss$sss$sss" | git-stripspace | grep -q " " +' + +test_expect_success \ + 'text plus spaces without newline should show the correct lines' ' + printf "$ttt\n" >expect && + printf "$ttt$sss" | git-stripspace >actual && + git diff expect actual && + + printf "$ttt\n" >expect && + printf "$ttt$sss$sss" | git-stripspace >actual && + git diff expect actual && + + printf "$ttt\n" >expect && + printf "$ttt$sss$sss$sss" | git-stripspace >actual && + git diff expect actual && + + printf "$ttt$ttt\n" >expect && + printf "$ttt$ttt$sss" | git-stripspace >actual && + git diff expect actual && + + printf "$ttt$ttt\n" >expect && + printf "$ttt$ttt$sss$sss" | git-stripspace >actual && + git diff expect actual && + + printf "$ttt$ttt$ttt\n" >expect && + printf "$ttt$ttt$ttt$sss" | git-stripspace >actual && + git diff expect actual +' + +test_expect_failure \ + 'text plus spaces at end should not show spaces' ' + echo "$ttt$sss" | git-stripspace | grep -q " " || + echo "$ttt$ttt$sss" | git-stripspace | grep -q " " || + echo "$ttt$ttt$ttt$sss" | git-stripspace | grep -q " " || + echo "$ttt$sss$sss" | git-stripspace | grep -q " " || + echo "$ttt$ttt$sss$sss" | git-stripspace | grep -q " " || + echo "$ttt$sss$sss$sss" | git-stripspace | grep -q " " +' + +test_expect_success \ + 'text plus spaces at end should be cleaned and newline must remain' ' + echo "$ttt" >expect && + echo "$ttt$sss" | git-stripspace >actual && + git diff expect actual && + + echo "$ttt" >expect && + echo "$ttt$sss$sss" | git-stripspace >actual && + git diff expect actual && + + echo "$ttt" >expect && + echo "$ttt$sss$sss$sss" | git-stripspace >actual && + git diff expect actual && + + echo "$ttt$ttt" >expect && + echo "$ttt$ttt$sss" | git-stripspace >actual && + git diff expect actual && + + echo "$ttt$ttt" >expect && + echo "$ttt$ttt$sss$sss" | git-stripspace >actual && + git diff expect actual && + + echo "$ttt$ttt$ttt" >expect && + echo "$ttt$ttt$ttt$sss" | git-stripspace >actual && + git diff expect actual +' + +# spaces only: + +test_expect_success \ + 'spaces with newline at end should be replaced with empty string' ' + printf "" >expect && + + echo | git-stripspace >actual && + git diff expect actual && + + echo "$sss" | git-stripspace >actual && + git diff expect actual && + + echo "$sss$sss" | git-stripspace >actual && + git diff expect actual && + + echo "$sss$sss$sss" | git-stripspace >actual && + git diff expect actual && + + echo "$sss$sss$sss$sss" | git-stripspace >actual && + git diff expect actual +' + +test_expect_failure \ + 'spaces without newline at end should not show spaces' ' + printf "" | git-stripspace | grep -q " " || + printf "$sss" | git-stripspace | grep -q " " || + printf "$sss$sss" | git-stripspace | grep -q " " || + printf "$sss$sss$sss" | git-stripspace | grep -q " " || + printf "$sss$sss$sss$sss" | git-stripspace | grep -q " " +' + +test_expect_success \ + 'spaces without newline at end should be replaced with empty string' ' + printf "" >expect && + + printf "" | git-stripspace >actual && + git diff expect actual + + printf "$sss$sss" | git-stripspace >actual && + git diff expect actual + + printf "$sss$sss$sss" | git-stripspace >actual && + git diff expect actual + + printf "$sss$sss$sss$sss" | git-stripspace >actual && + git diff expect actual +' + +test_done diff --git a/t/t1300-repo-config.sh b/t/t1300-repo-config.sh index 7731fa72c..a2c11c463 100755 --- a/t/t1300-repo-config.sh +++ b/t/t1300-repo-config.sh @@ -283,6 +283,12 @@ EOF test_expect_success 'get variable with no value' \ 'git-config --get novalue.variable ^$' +echo novalue.variable > expect + +test_expect_success 'get-regexp variable with no value' \ + 'git-config --get-regexp novalue > output && + cmp output expect' + git-config > output 2>&1 test_expect_success 'no arguments, but no crash' \ @@ -465,11 +471,57 @@ test_expect_success bool ' done && cmp expect result' -test_expect_failure 'invalid bool' ' +test_expect_failure 'invalid bool (--get)' ' git-config bool.nobool foobar && git-config --bool --get bool.nobool' +test_expect_failure 'invalid bool (set)' ' + + git-config --bool bool.nobool foobar' + +rm .git/config + +cat > expect <<\EOF +[bool] + true1 = true + true2 = true + true3 = true + true4 = true + false1 = false + false2 = false + false3 = false + false4 = false +EOF + +test_expect_success 'set --bool' ' + + git-config --bool bool.true1 01 && + git-config --bool bool.true2 -1 && + git-config --bool bool.true3 YeS && + git-config --bool bool.true4 true && + git-config --bool bool.false1 000 && + git-config --bool bool.false2 "" && + git-config --bool bool.false3 nO && + git-config --bool bool.false4 FALSE && + cmp expect .git/config' + +rm .git/config + +cat > expect <<\EOF +[int] + val1 = 1 + val2 = -1 + val3 = 5242880 +EOF + +test_expect_success 'set --int' ' + + git-config --int int.val1 01 && + git-config --int int.val2 -1 && + git-config --int int.val3 5m && + cmp expect .git/config' + rm .git/config git-config quote.leading " test" @@ -513,4 +565,34 @@ git config --list > result test_expect_success 'value continued on next line' 'cmp result expect' +cat > .git/config <<\EOF +[section "sub=section"] + val1 = foo=bar + val2 = foo\nbar + val3 = \n\n + val4 = + val5 +EOF + +cat > expect <<\EOF +section.sub=section.val1 +foo=barQsection.sub=section.val2 +foo +barQsection.sub=section.val3 + + +Qsection.sub=section.val4 +Qsection.sub=section.val5Q +EOF + +git config --null --list | tr '[\000]' 'Q' > result +echo >>result + +test_expect_success '--null --list' 'cmp result expect' + +git config --null --get-regexp 'val[0-9]' | tr '[\000]' 'Q' > result +echo >>result + +test_expect_success '--null --get-regexp' 'cmp result expect' + test_done diff --git a/t/t1500-rev-parse.sh b/t/t1500-rev-parse.sh new file mode 100755 index 000000000..ec4996637 --- /dev/null +++ b/t/t1500-rev-parse.sh @@ -0,0 +1,77 @@ +#!/bin/sh + +test_description='test git rev-parse' +. ./test-lib.sh + +test_rev_parse() { + name=$1 + shift + + test_expect_success "$name: is-bare-repository" \ + "test '$1' = \"\$(git rev-parse --is-bare-repository)\"" + shift + [ $# -eq 0 ] && return + + test_expect_success "$name: is-inside-git-dir" \ + "test '$1' = \"\$(git rev-parse --is-inside-git-dir)\"" + shift + [ $# -eq 0 ] && return + + test_expect_success "$name: is-inside-work-tree" \ + "test '$1' = \"\$(git rev-parse --is-inside-work-tree)\"" + shift + [ $# -eq 0 ] && return + + test_expect_success "$name: prefix" \ + "test '$1' = \"\$(git rev-parse --show-prefix)\"" + shift + [ $# -eq 0 ] && return +} + +test_rev_parse toplevel false false true '' + +cd .git || exit 1 +test_rev_parse .git/ false true true .git/ +cd objects || exit 1 +test_rev_parse .git/objects/ false true true .git/objects/ +cd ../.. || exit 1 + +mkdir -p sub/dir || exit 1 +cd sub/dir || exit 1 +test_rev_parse subdirectory false false true sub/dir/ +cd ../.. || exit 1 + +git config core.bare true +test_rev_parse 'core.bare = true' true false true + +git config --unset core.bare +test_rev_parse 'core.bare undefined' false false true + +mkdir work || exit 1 +cd work || exit 1 +export GIT_DIR=../.git +export GIT_CONFIG="$GIT_DIR"/config + +git config core.bare false +test_rev_parse 'GIT_DIR=../.git, core.bare = false' false false true '' + +git config core.bare true +test_rev_parse 'GIT_DIR=../.git, core.bare = true' true false true '' + +git config --unset core.bare +test_rev_parse 'GIT_DIR=../.git, core.bare undefined' false false true '' + +mv ../.git ../repo.git || exit 1 +export GIT_DIR=../repo.git +export GIT_CONFIG="$GIT_DIR"/config + +git config core.bare false +test_rev_parse 'GIT_DIR=../repo.git, core.bare = false' false false true '' + +git config core.bare true +test_rev_parse 'GIT_DIR=../repo.git, core.bare = true' true false true '' + +git config --unset core.bare +test_rev_parse 'GIT_DIR=../repo.git, core.bare undefined' true false true '' + +test_done diff --git a/t/t1501-worktree.sh b/t/t1501-worktree.sh new file mode 100755 index 000000000..aadeeab9a --- /dev/null +++ b/t/t1501-worktree.sh @@ -0,0 +1,92 @@ +#!/bin/sh + +test_description='test separate work tree' +. ./test-lib.sh + +test_rev_parse() { + name=$1 + shift + + test_expect_success "$name: is-bare-repository" \ + "test '$1' = \"\$(git rev-parse --is-bare-repository)\"" + shift + [ $# -eq 0 ] && return + + test_expect_success "$name: is-inside-git-dir" \ + "test '$1' = \"\$(git rev-parse --is-inside-git-dir)\"" + shift + [ $# -eq 0 ] && return + + test_expect_success "$name: is-inside-work-tree" \ + "test '$1' = \"\$(git rev-parse --is-inside-work-tree)\"" + shift + [ $# -eq 0 ] && return + + test_expect_success "$name: prefix" \ + "test '$1' = \"\$(git rev-parse --show-prefix)\"" + shift + [ $# -eq 0 ] && return +} + +mkdir -p work/sub/dir || exit 1 +mv .git repo.git || exit 1 + +say "core.worktree = relative path" +export GIT_DIR=repo.git +export GIT_CONFIG=$GIT_DIR/config +unset GIT_WORK_TREE +git config core.worktree ../work +test_rev_parse 'outside' false false false +cd work || exit 1 +export GIT_DIR=../repo.git +export GIT_CONFIG=$GIT_DIR/config +test_rev_parse 'inside' false false true '' +cd sub/dir || exit 1 +export GIT_DIR=../../../repo.git +export GIT_CONFIG=$GIT_DIR/config +test_rev_parse 'subdirectory' false false true sub/dir/ +cd ../../.. || exit 1 + +say "core.worktree = absolute path" +export GIT_DIR=$(pwd)/repo.git +export GIT_CONFIG=$GIT_DIR/config +git config core.worktree "$(pwd)/work" +test_rev_parse 'outside' false false false +cd work || exit 1 +test_rev_parse 'inside' false false true '' +cd sub/dir || exit 1 +test_rev_parse 'subdirectory' false false true sub/dir/ +cd ../../.. || exit 1 + +say "GIT_WORK_TREE=relative path (override core.worktree)" +export GIT_DIR=$(pwd)/repo.git +export GIT_CONFIG=$GIT_DIR/config +git config core.worktree non-existent +export GIT_WORK_TREE=work +test_rev_parse 'outside' false false false +cd work || exit 1 +export GIT_WORK_TREE=. +test_rev_parse 'inside' false false true '' +cd sub/dir || exit 1 +export GIT_WORK_TREE=../.. +test_rev_parse 'subdirectory' false false true sub/dir/ +cd ../../.. || exit 1 + +mv work repo.git/work + +say "GIT_WORK_TREE=absolute path, work tree below git dir" +export GIT_DIR=$(pwd)/repo.git +export GIT_CONFIG=$GIT_DIR/config +export GIT_WORK_TREE=$(pwd)/repo.git/work +test_rev_parse 'outside' false false false +cd repo.git || exit 1 +test_rev_parse 'in repo.git' false true false +cd objects || exit 1 +test_rev_parse 'in repo.git/objects' false true false +cd ../work || exit 1 +test_rev_parse 'in repo.git/work' false false true '' +cd sub/dir || exit 1 +test_rev_parse 'in repo.git/sub/dir' false false true sub/dir/ +cd ../../../.. || exit 1 + +test_done diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh index 6f6d8844e..f1793d0b9 100755 --- a/t/t3200-branch.sh +++ b/t/t3200-branch.sh @@ -171,6 +171,15 @@ test_expect_success 'test tracking setup via --track but deeper' \ test "$(git-config branch.my7.remote)" = local && test "$(git-config branch.my7.merge)" = refs/heads/o/o' +test_expect_success 'test deleting branch deletes branch config' \ + 'git-branch -d my7 && + test "$(git-config branch.my7.remote)" = "" && + test "$(git-config branch.my7.merge)" = ""' + +test_expect_success 'test deleting branch without config' \ + 'git-branch my7 s && + test "$(git-branch -d my7 2>&1)" = "Deleted branch my7."' + # Keep this test last, as it changes the current branch cat >expect <<EOF 0000000000000000000000000000000000000000 $HEAD $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150200 +0000 branch: Created from master diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh new file mode 100755 index 000000000..883cf2959 --- /dev/null +++ b/t/t3404-rebase-interactive.sh @@ -0,0 +1,191 @@ +#!/bin/sh +# +# Copyright (c) 2007 Johannes E. Schindelin +# + +test_description='git rebase interactive + +This test runs git rebase "interactively", by faking an edit, and verifies +that the result still makes sense. +' +. ./test-lib.sh + +# set up two branches like this: +# +# A - B - C - D - E +# \ +# F - G - H +# \ +# I +# +# where B, D and G touch the same file. + +test_expect_success 'setup' ' + : > file1 && + git add file1 && + test_tick && + git commit -m A && + git tag A && + echo 1 > file1 && + test_tick && + git commit -m B file1 && + : > file2 && + git add file2 && + test_tick && + git commit -m C && + echo 2 > file1 && + test_tick && + git commit -m D file1 && + : > file3 && + git add file3 && + test_tick && + git commit -m E && + git checkout -b branch1 A && + : > file4 && + git add file4 && + test_tick && + git commit -m F && + git tag F && + echo 3 > file1 && + test_tick && + git commit -m G file1 && + : > file5 && + git add file5 && + test_tick && + git commit -m H && + git checkout -b branch2 F && + : > file6 && + git add file6 && + test_tick && + git commit -m I && + git tag I +' + +cat > fake-editor.sh << EOF +#!/bin/sh +test "\$1" = .git/COMMIT_EDITMSG && exit +test -z "\$FAKE_LINES" && exit +grep -v "^#" < "\$1" > "\$1".tmp +rm "\$1" +cat "\$1".tmp +action=pick +for line in \$FAKE_LINES; do + case \$line in + squash) + action="\$line";; + *) + echo sed -n "\${line}s/^pick/\$action/p" + sed -n "\${line}p" < "\$1".tmp + sed -n "\${line}s/^pick/\$action/p" < "\$1".tmp >> "\$1" + action=pick;; + esac +done +EOF + +chmod a+x fake-editor.sh +VISUAL="$(pwd)/fake-editor.sh" +export VISUAL + +test_expect_success 'no changes are a nop' ' + git rebase -i F && + test $(git rev-parse I) = $(git rev-parse HEAD) +' + +test_expect_success 'rebase on top of a non-conflicting commit' ' + git checkout branch1 && + git tag original-branch1 && + git rebase -i branch2 && + test file6 = $(git diff --name-only original-branch1) && + test $(git rev-parse I) = $(git rev-parse HEAD~2) +' + +test_expect_success 'reflog for the branch shows state before rebase' ' + test $(git rev-parse branch1@{1}) = $(git rev-parse original-branch1) +' + +test_expect_success 'exchange two commits' ' + FAKE_LINES="2 1" git rebase -i HEAD~2 && + test H = $(git cat-file commit HEAD^ | tail -n 1) && + test G = $(git cat-file commit HEAD | tail -n 1) +' + +cat > expect << EOF +diff --git a/file1 b/file1 +index e69de29..00750ed 100644 +--- a/file1 ++++ b/file1 +@@ -0,0 +1 @@ ++3 +EOF + +cat > expect2 << EOF +<<<<<<< HEAD:file1 +2 +======= +3 +>>>>>>> b7ca976... G:file1 +EOF + +test_expect_success 'stop on conflicting pick' ' + git tag new-branch1 && + ! git rebase -i master && + diff -u expect .git/.dotest-merge/patch && + diff -u expect2 file1 && + test 4 = $(grep -v "^#" < .git/.dotest-merge/done | wc -l) && + test 0 = $(grep -v "^#" < .git/.dotest-merge/todo | wc -l) +' + +test_expect_success 'abort' ' + git rebase --abort && + test $(git rev-parse new-branch1) = $(git rev-parse HEAD) && + ! test -d .git/.dotest-merge +' + +test_expect_success 'retain authorship' ' + echo A > file7 && + git add file7 && + test_tick && + GIT_AUTHOR_NAME="Twerp Snog" git commit -m "different author" && + git tag twerp && + git rebase -i --onto master HEAD^ && + git show HEAD | grep "^Author: Twerp Snog" +' + +test_expect_success 'squash' ' + git reset --hard twerp && + echo B > file7 && + test_tick && + GIT_AUTHOR_NAME="Nitfol" git commit -m "nitfol" file7 && + echo "******************************" && + FAKE_LINES="1 squash 2" git rebase -i --onto master HEAD~2 && + test B = $(cat file7) && + test $(git rev-parse HEAD^) = $(git rev-parse master) +' + +test_expect_success 'retain authorship when squashing' ' + git show HEAD | grep "^Author: Nitfol" +' + +test_expect_success 'preserve merges with -p' ' + git checkout -b to-be-preserved master^ && + : > unrelated-file && + git add unrelated-file && + test_tick && + git commit -m "unrelated" && + git checkout -b to-be-rebased master && + echo B > file1 && + test_tick && + git commit -m J file1 && + test_tick && + git merge to-be-preserved && + echo C > file1 && + test_tick && + git commit -m K file1 && + git rebase -i -p --onto branch1 master && + test $(git rev-parse HEAD^^2) = $(git rev-parse to-be-preserved) && + test $(git rev-parse HEAD~3) = $(git rev-parse branch1) && + test $(git show HEAD:file1) = C && + test $(git show HEAD~2:file1) = B +' + +test_done diff --git a/t/t3700-add.sh b/t/t3700-add.sh index ad8cc7d4a..0d80c6aea 100755 --- a/t/t3700-add.sh +++ b/t/t3700-add.sh @@ -110,4 +110,30 @@ test_expect_success 'check correct prefix detection' ' git add 1/2/a 1/3/b 1/2/c ' +test_expect_success 'git add and filemode=0 with unmerged entries' ' + echo 1 > stage1 && + echo 2 > stage2 && + echo 3 > stage3 && + for s in 1 2 3 + do + echo "100755 $(git hash-object -w stage$s) $s file" + done | git update-index --index-info && + git config core.filemode 0 && + echo new > file && + git add file && + git ls-files --stage | grep "^100755 .* 0 file$" +' + +test_expect_success 'git add and filemode=0 prefers stage 2 over stage 1' ' + git rm --cached -f file && + ( + echo "100644 $(git hash-object -w stage1) 1 file" + echo "100755 $(git hash-object -w stage2) 2 file" + ) | git update-index --index-info && + git config core.filemode 0 && + echo new > file && + git add file && + git ls-files --stage | grep "^100755 .* 0 file$" +' + test_done diff --git a/t/t3902-quoted.sh b/t/t3902-quoted.sh new file mode 100755 index 000000000..63f950b17 --- /dev/null +++ b/t/t3902-quoted.sh @@ -0,0 +1,126 @@ +#!/bin/sh +# +# Copyright (c) 2006 Junio C Hamano +# + +test_description='quoted output' + +. ./test-lib.sh + +FN='濱野' +GN='純' +HT=' ' +LF=' +' +DQ='"' + +for_each_name () { + for name in \ + Name "Name and a${LF}LF" "Name and an${HT}HT" "Name${DQ}" \ + "$FN$HT$GN" "$FN$LF$GN" "$FN $GN" "$FN$GN" "$FN$DQ$GN" \ + "With SP in it" + do + eval "$1" + done +} + +test_expect_success setup ' + + for_each_name "echo initial >\"\$name\"" + git add . && + git commit -q -m Initial && + + for_each_name "echo second >\"\$name\"" && + git commit -a -m Second + + for_each_name "echo modified >\"\$name\"" + +' + +cat >expect.quoted <<\EOF +Name +"Name and a\nLF" +"Name and an\tHT" +"Name\"" +With SP in it +"\346\277\261\351\207\216\t\347\264\224" +"\346\277\261\351\207\216\n\347\264\224" +"\346\277\261\351\207\216 \347\264\224" +"\346\277\261\351\207\216\"\347\264\224" +"\346\277\261\351\207\216\347\264\224" +EOF + +cat >expect.raw <<\EOF +Name +"Name and a\nLF" +"Name and an\tHT" +"Name\"" +With SP in it +"濱野\t純" +"濱野\n純" +濱野 純 +"濱野\"純" +濱野純 +EOF + +test_expect_success 'check fully quoted output from ls-files' ' + + git ls-files >current && diff -u expect.quoted current + +' + +test_expect_success 'check fully quoted output from diff-files' ' + + git diff --name-only >current && + diff -u expect.quoted current + +' + +test_expect_success 'check fully quoted output from diff-index' ' + + git diff --name-only HEAD >current && + diff -u expect.quoted current + +' + +test_expect_success 'check fully quoted output from diff-tree' ' + + git diff --name-only HEAD^ HEAD >current && + diff -u expect.quoted current + +' + +test_expect_success 'setting core.quotepath' ' + + git config --bool core.quotepath false + +' + +test_expect_success 'check fully quoted output from ls-files' ' + + git ls-files >current && diff -u expect.raw current + +' + +test_expect_success 'check fully quoted output from diff-files' ' + + git diff --name-only >current && + diff -u expect.raw current + +' + +test_expect_success 'check fully quoted output from diff-index' ' + + git diff --name-only HEAD >current && + diff -u expect.raw current + +' + +test_expect_success 'check fully quoted output from diff-tree' ' + + git diff --name-only HEAD^ HEAD >current && + diff -u expect.raw current + +' + +test_done diff --git a/t/t4001-diff-rename.sh b/t/t4001-diff-rename.sh index 2e3c20d6b..90c085f82 100755 --- a/t/t4001-diff-rename.sh +++ b/t/t4001-diff-rename.sh @@ -64,4 +64,17 @@ test_expect_success \ 'validate the output.' \ 'compare_diff_patch current expected' +test_expect_success 'favour same basenames over different ones' ' + cp path1 another-path && + git add another-path && + git commit -m 1 && + git rm path1 && + mkdir subdir && + git mv another-path subdir/path1 && + git runstatus | grep "renamed: .*path1 -> subdir/path1"' + +test_expect_success 'favour same basenames even with minor differences' ' + git show HEAD:path1 | sed "s/15/16/" > subdir/path1 && + git runstatus | grep "renamed: .*path1 -> subdir/path1"' + test_done diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh index dba018f66..c0fa2ba40 100755 --- a/t/t5516-fetch-push.sh +++ b/t/t5516-fetch-push.sh @@ -15,12 +15,58 @@ mk_empty () { ) } +mk_test () { + mk_empty && + ( + for ref in "$@" + do + git push testrepo $the_first_commit:refs/$ref || { + echo "Oops, push refs/$ref failure" + exit 1 + } + done && + cd testrepo && + for ref in "$@" + do + r=$(git show-ref -s --verify refs/$ref) && + test "z$r" = "z$the_first_commit" || { + echo "Oops, refs/$ref is wrong" + exit 1 + } + done && + git fsck --full + ) +} + +check_push_result () { + ( + cd testrepo && + it="$1" && + shift + for ref in "$@" + do + r=$(git show-ref -s --verify refs/$ref) && + test "z$r" = "z$it" || { + echo "Oops, refs/$ref is wrong" + exit 1 + } + done && + git fsck --full + ) +} + test_expect_success setup ' : >path1 && git add path1 && test_tick && git commit -a -m repo && + the_first_commit=$(git show-ref -s --verify refs/heads/master) && + + : >path2 && + git add path2 && + test_tick && + git commit -a -m second && the_commit=$(git show-ref -s --verify refs/heads/master) ' @@ -79,4 +125,122 @@ test_expect_success 'push with wildcard' ' ) ' +test_expect_success 'push with matching heads' ' + + mk_test heads/master && + git push testrepo && + check_push_result $the_commit heads/master + +' + +test_expect_success 'push with no ambiguity (1)' ' + + mk_test heads/master && + git push testrepo master:master && + check_push_result $the_commit heads/master + +' + +test_expect_success 'push with no ambiguity (2)' ' + + mk_test remotes/origin/master && + git push testrepo master:master && + check_push_result $the_commit remotes/origin/master + +' + +test_expect_success 'push with weak ambiguity (1)' ' + + mk_test heads/master remotes/origin/master && + git push testrepo master:master && + check_push_result $the_commit heads/master && + check_push_result $the_first_commit remotes/origin/master + +' + +test_expect_success 'push with weak ambiguity (2)' ' + + mk_test heads/master remotes/origin/master remotes/another/master && + git push testrepo master:master && + check_push_result $the_commit heads/master && + check_push_result $the_first_commit remotes/origin/master remotes/another/master + +' + +test_expect_success 'push with ambiguity (1)' ' + + mk_test remotes/origin/master remotes/frotz/master && + if git push testrepo master:master + then + echo "Oops, should have failed" + false + else + check_push_result $the_first_commit remotes/origin/master remotes/frotz/master + fi +' + +test_expect_success 'push with ambiguity (2)' ' + + mk_test heads/frotz tags/frotz && + if git push testrepo master:frotz + then + echo "Oops, should have failed" + false + else + check_push_result $the_first_commit heads/frotz tags/frotz + fi + +' + +test_expect_success 'push with colon-less refspec (1)' ' + + mk_test heads/frotz tags/frotz && + git branch -f frotz master && + git push testrepo frotz && + check_push_result $the_commit heads/frotz && + check_push_result $the_first_commit tags/frotz + +' + +test_expect_success 'push with colon-less refspec (2)' ' + + mk_test heads/frotz tags/frotz && + if git show-ref --verify -q refs/heads/frotz + then + git branch -D frotz + fi && + git tag -f frotz && + git push testrepo frotz && + check_push_result $the_commit tags/frotz && + check_push_result $the_first_commit heads/frotz + +' + +test_expect_success 'push with colon-less refspec (3)' ' + + mk_test && + if git show-ref --verify -q refs/tags/frotz + then + git tag -d frotz + fi && + git branch -f frotz master && + git push testrepo frotz && + check_push_result $the_commit heads/frotz && + test 1 = $( cd testrepo && git show-ref | wc -l ) +' + +test_expect_success 'push with colon-less refspec (4)' ' + + mk_test && + if git show-ref --verify -q refs/heads/frotz + then + git branch -D frotz + fi && + git tag -f frotz && + git push testrepo frotz && + check_push_result $the_commit tags/frotz && + test 1 = $( cd testrepo && git show-ref | wc -l ) + +' + test_done diff --git a/t/t6010-merge-base.sh b/t/t6010-merge-base.sh index b15920b85..22e089305 100755 --- a/t/t6010-merge-base.sh +++ b/t/t6010-merge-base.sh @@ -34,6 +34,12 @@ doit() { echo $commit } +# E---D---C---B---A +# \'-_ \ \ +# \ `---------G \ +# \ \ +# F----------------H + # Setup... E=$(doit 5 E) D=$(doit 4 D $E) @@ -44,6 +50,18 @@ A=$(doit 1 A $B) G=$(doit 7 G $B $E) H=$(doit 8 H $A $F) +test_expect_success 'compute merge-base (single)' \ + 'MB=$(git-merge-base G H) && + expr "$(git-name-rev "$MB")" : "[0-9a-f]* tags/B"' + +test_expect_success 'compute merge-base (all)' \ + 'MB=$(git-merge-base --all G H) && + expr "$(git-name-rev "$MB")" : "[0-9a-f]* tags/B"' + +test_expect_success 'compute merge-base with show-branch' \ + 'MB=$(git-show-branch --merge-base G H) && + expr "$(git-name-rev "$MB")" : "[0-9a-f]* tags/B"' + # Setup for second test to demonstrate that relying on timestamps in a # distributed SCM to provide a _consistent_ partial ordering of commits # leads to insanity. @@ -82,18 +100,6 @@ PL=$(doit 4 PL $L2 $C2) PR=$(doit 4 PR $C2 $R2) test_expect_success 'compute merge-base (single)' \ - 'MB=$(git-merge-base G H) && - expr "$(git-name-rev "$MB")" : "[0-9a-f]* tags/B"' - -test_expect_success 'compute merge-base (all)' \ - 'MB=$(git-merge-base --all G H) && - expr "$(git-name-rev "$MB")" : "[0-9a-f]* tags/B"' - -test_expect_success 'compute merge-base with show-branch' \ - 'MB=$(git-show-branch --merge-base G H) && - expr "$(git-name-rev "$MB")" : "[0-9a-f]* tags/B"' - -test_expect_success 'compute merge-base (single)' \ 'MB=$(git-merge-base PL PR) && expr "$(git-name-rev "$MB")" : "[0-9a-f]* tags/C2"' diff --git a/t/t7003-filter-branch.sh b/t/t7003-filter-branch.sh new file mode 100755 index 000000000..f00c262e4 --- /dev/null +++ b/t/t7003-filter-branch.sh @@ -0,0 +1,110 @@ +#!/bin/sh + +test_description='git-filter-branch' +. ./test-lib.sh + +make_commit () { + lower=$(echo $1 | tr A-Z a-z) + echo $lower > $lower + git add $lower + test_tick + git commit -m $1 + git tag $1 +} + +test_expect_success 'setup' ' + make_commit A + make_commit B + git checkout -b branch B + make_commit D + make_commit E + git checkout master + make_commit C + git checkout branch + git merge C + git tag F + make_commit G + make_commit H +' + +H=$(git-rev-parse H) + +test_expect_success 'rewrite identically' ' + git-filter-branch H2 +' + +test_expect_success 'result is really identical' ' + test $H = $(git-rev-parse H2) +' + +test_expect_success 'rewrite, renaming a specific file' ' + git-filter-branch --tree-filter "mv d doh || :" H3 +' + +test_expect_success 'test that the file was renamed' ' + test d = $(git show H3:doh) +' + +git tag oldD H3~4 +test_expect_success 'rewrite one branch, keeping a side branch' ' + git-filter-branch --tree-filter "mv b boh || :" modD D..oldD +' + +test_expect_success 'common ancestor is still common (unchanged)' ' + test "$(git-merge-base modD D)" = "$(git-rev-parse B)" +' + +test_expect_success 'filter subdirectory only' ' + mkdir subdir && + touch subdir/new && + git add subdir/new && + test_tick && + git commit -m "subdir" && + echo H > a && + test_tick && + git commit -m "not subdir" a && + echo A > subdir/new && + test_tick && + git commit -m "again subdir" subdir/new && + git rm a && + test_tick && + git commit -m "again not subdir" && + git-filter-branch --subdirectory-filter subdir sub +' + +test_expect_success 'subdirectory filter result looks okay' ' + test 2 = $(git-rev-list sub | wc -l) && + git show sub:new && + ! git show sub:subdir +' + +test_expect_success 'setup and filter history that requires --full-history' ' + git checkout master && + mkdir subdir && + echo A > subdir/new && + git add subdir/new && + test_tick && + git commit -m "subdir on master" subdir/new && + git rm a && + test_tick && + git commit -m "again subdir on master" && + git merge branch && + git-filter-branch --subdirectory-filter subdir sub-master +' + +test_expect_success 'subdirectory filter result looks okay' ' + test 3 = $(git-rev-list -1 --parents sub-master | wc -w) && + git show sub-master^:new && + git show sub-master^2:new && + ! git show sub:subdir +' + +test_expect_success 'use index-filter to move into a subdirectory' ' + git-filter-branch --index-filter \ + "git-ls-files -s | sed \"s-\\t-&newsubdir/-\" | + GIT_INDEX_FILE=\$GIT_INDEX_FILE.new \ + git-update-index --index-info && + mv \$GIT_INDEX_FILE.new \$GIT_INDEX_FILE" directorymoved && + test -z "$(git diff HEAD directorymoved:newsubdir)"' + +test_done diff --git a/t/t7004-tag.sh b/t/t7004-tag.sh new file mode 100755 index 000000000..5d15449be --- /dev/null +++ b/t/t7004-tag.sh @@ -0,0 +1,686 @@ +#!/bin/sh +# +# Copyright (c) 2007 Carlos Rica +# + +test_description='git-tag + +Basic tests for operations with tags.' + +. ./test-lib.sh + +# creating and listing lightweight tags: + +tag_exists () { + git show-ref --quiet --verify refs/tags/"$1" +} + +# todo: git tag -l now returns always zero, when fixed, change this test +test_expect_success 'listing all tags in an empty tree should succeed' \ + 'git tag -l' + +test_expect_success 'listing all tags in an empty tree should output nothing' \ + 'test `git-tag -l | wc -l` -eq 0' + +test_expect_failure 'looking for a tag in an empty tree should fail' \ + 'tag_exists mytag' + +test_expect_success 'creating a tag in an empty tree should fail' ' + ! git-tag mynotag && + ! tag_exists mynotag +' + +test_expect_success 'creating a tag for HEAD in an empty tree should fail' ' + ! git-tag mytaghead HEAD && + ! tag_exists mytaghead +' + +test_expect_success 'creating a tag for an unknown revision should fail' ' + ! git-tag mytagnorev aaaaaaaaaaa && + ! tag_exists mytagnorev +' + +# commit used in the tests, test_tick is also called here to freeze the date: +test_expect_success 'creating a tag using default HEAD should succeed' ' + test_tick && + echo foo >foo && + git add foo && + git commit -m Foo && + git tag mytag +' + +test_expect_success 'listing all tags if one exists should succeed' \ + 'git-tag -l' + +test_expect_success 'listing all tags if one exists should output that tag' \ + 'test `git-tag -l` = mytag' + +# pattern matching: + +test_expect_success 'listing a tag using a matching pattern should succeed' \ + 'git-tag -l mytag' + +test_expect_success \ + 'listing a tag using a matching pattern should output that tag' \ + 'test `git-tag -l mytag` = mytag' + +# todo: git tag -l now returns always zero, when fixed, change this test +test_expect_success \ + 'listing tags using a non-matching pattern should suceed' \ + 'git-tag -l xxx' + +test_expect_success \ + 'listing tags using a non-matching pattern should output nothing' \ + 'test `git-tag -l xxx | wc -l` -eq 0' + +# special cases for creating tags: + +test_expect_failure \ + 'trying to create a tag with the name of one existing should fail' \ + 'git tag mytag' + +test_expect_success \ + 'trying to create a tag with a non-valid name should fail' ' + test `git-tag -l | wc -l` -eq 1 && + ! git tag "" && + ! git tag .othertag && + ! git tag "other tag" && + ! git tag "othertag^" && + ! git tag "other~tag" && + test `git-tag -l | wc -l` -eq 1 +' + +test_expect_success 'creating a tag using HEAD directly should succeed' ' + git tag myhead HEAD && + tag_exists myhead +' + +# deleting tags: + +test_expect_success 'trying to delete an unknown tag should fail' ' + ! tag_exists unknown-tag && + ! git-tag -d unknown-tag +' + +cat >expect <<EOF +myhead +mytag +EOF +test_expect_success \ + 'trying to delete tags without params should succeed and do nothing' ' + git tag -l > actual && git diff expect actual && + git-tag -d && + git tag -l > actual && git diff expect actual +' + +test_expect_success \ + 'deleting two existing tags in one command should succeed' ' + tag_exists mytag && + tag_exists myhead && + git-tag -d mytag myhead && + ! tag_exists mytag && + ! tag_exists myhead +' + +test_expect_success \ + 'creating a tag with the name of another deleted one should succeed' ' + ! tag_exists mytag && + git-tag mytag && + tag_exists mytag +' + +test_expect_success \ + 'trying to delete two tags, existing and not, should fail in the 2nd' ' + tag_exists mytag && + ! tag_exists myhead && + ! git-tag -d mytag anothertag && + ! tag_exists mytag && + ! tag_exists myhead +' + +test_expect_failure 'trying to delete an already deleted tag should fail' \ + 'git-tag -d mytag' + +# listing various tags with pattern matching: + +cat >expect <<EOF +a1 +aa1 +cba +t210 +t211 +v0.2.1 +v1.0 +v1.0.1 +v1.1.3 +EOF +test_expect_success 'listing all tags should print them ordered' ' + git tag v1.0.1 && + git tag t211 && + git tag aa1 && + git tag v0.2.1 && + git tag v1.1.3 && + git tag cba && + git tag a1 && + git tag v1.0 && + git tag t210 && + git tag -l > actual + git diff expect actual +' + +cat >expect <<EOF +a1 +aa1 +cba +EOF +test_expect_success \ + 'listing tags with substring as pattern must print those matching' ' + git-tag -l a > actual && + git-diff expect actual +' + +cat >expect <<EOF +v0.2.1 +v1.0.1 +v1.1.3 +EOF +test_expect_success \ + 'listing tags with substring as pattern must print those matching' ' + git-tag -l .1 > actual && + git-diff expect actual +' + +cat >expect <<EOF +t210 +t211 +EOF +test_expect_success \ + 'listing tags with substring as pattern must print those matching' ' + git-tag -l t21 > actual && + git-diff expect actual +' + +cat >expect <<EOF +a1 +aa1 +EOF +test_expect_success \ + 'listing tags using a name as pattern must print those matching' ' + git-tag -l a1 > actual && + git-diff expect actual +' + +cat >expect <<EOF +v1.0 +v1.0.1 +EOF +test_expect_success \ + 'listing tags using a name as pattern must print those matching' ' + git-tag -l v1.0 > actual && + git-diff expect actual +' + +cat >expect <<EOF +v1.1.3 +EOF +test_expect_success \ + 'listing tags with ? in the pattern should print those matching' ' + git-tag -l "1.1?" > actual && + git-diff expect actual +' + +>expect +test_expect_success \ + 'listing tags using v.* should print nothing because none have v.' ' + git-tag -l "v.*" > actual && + git-diff expect actual +' + +cat >expect <<EOF +v0.2.1 +v1.0 +v1.0.1 +v1.1.3 +EOF +test_expect_success \ + 'listing tags using v* should print only those having v' ' + git-tag -l "v*" > actual && + git-diff expect actual +' + +# creating and verifying lightweight tags: + +test_expect_success \ + 'a non-annotated tag created without parameters should point to HEAD' ' + git-tag non-annotated-tag && + test $(git-cat-file -t non-annotated-tag) = commit && + test $(git-rev-parse non-annotated-tag) = $(git-rev-parse HEAD) +' + +test_expect_failure 'trying to verify an unknown tag should fail' \ + 'git-tag -v unknown-tag' + +test_expect_failure \ + 'trying to verify a non-annotated and non-signed tag should fail' \ + 'git-tag -v non-annotated-tag' + +# creating annotated tags: + +get_tag_msg () { + git cat-file tag "$1" | sed -e "/BEGIN PGP/q" +} + +# run test_tick before committing always gives the time in that timezone +get_tag_header () { +cat <<EOF +object $2 +type $3 +tag $1 +tagger C O Mitter <committer@example.com> $4 -0700 + +EOF +} + +commit=$(git rev-parse HEAD) +time=$test_tick + +get_tag_header annotated-tag $commit commit $time >expect +echo "A message" >>expect +test_expect_success \ + 'creating an annotated tag with -m message should succeed' ' + git-tag -m "A message" annotated-tag && + get_tag_msg annotated-tag >actual && + git diff expect actual +' + +cat >msgfile <<EOF +Another message +in a file. +EOF +get_tag_header file-annotated-tag $commit commit $time >expect +cat msgfile >>expect +test_expect_success \ + 'creating an annotated tag with -F messagefile should succeed' ' + git-tag -F msgfile file-annotated-tag && + get_tag_msg file-annotated-tag >actual && + git diff expect actual +' + +# blank and empty messages: + +get_tag_header empty-annotated-tag $commit commit $time >expect +test_expect_success \ + 'creating a tag with an empty -m message should succeed' ' + git-tag -m "" empty-annotated-tag && + get_tag_msg empty-annotated-tag >actual && + git diff expect actual +' + +>emptyfile +get_tag_header emptyfile-annotated-tag $commit commit $time >expect +test_expect_success \ + 'creating a tag with an empty -F messagefile should succeed' ' + git-tag -F emptyfile emptyfile-annotated-tag && + get_tag_msg emptyfile-annotated-tag >actual && + git diff expect actual +' + +printf '\n\n \n\t\nLeading blank lines\n' >blanksfile +printf '\n\t \t \nRepeated blank lines\n' >>blanksfile +printf '\n\n\nTrailing spaces \t \n' >>blanksfile +printf '\nTrailing blank lines\n\n\t \n\n' >>blanksfile +get_tag_header blanks-annotated-tag $commit commit $time >expect +cat >>expect <<EOF +Leading blank lines + +Repeated blank lines + +Trailing spaces + +Trailing blank lines +EOF +test_expect_success \ + 'extra blanks in the message for an annotated tag should be removed' ' + git-tag -F blanksfile blanks-annotated-tag && + get_tag_msg blanks-annotated-tag >actual && + git diff expect actual +' + +get_tag_header blank-annotated-tag $commit commit $time >expect +test_expect_success \ + 'creating a tag with blank -m message with spaces should succeed' ' + git-tag -m " " blank-annotated-tag && + get_tag_msg blank-annotated-tag >actual && + git diff expect actual +' + +echo ' ' >blankfile +echo '' >>blankfile +echo ' ' >>blankfile +get_tag_header blankfile-annotated-tag $commit commit $time >expect +test_expect_success \ + 'creating a tag with blank -F messagefile with spaces should succeed' ' + git-tag -F blankfile blankfile-annotated-tag && + get_tag_msg blankfile-annotated-tag >actual && + git diff expect actual +' + +printf ' ' >blanknonlfile +get_tag_header blanknonlfile-annotated-tag $commit commit $time >expect +test_expect_success \ + 'creating a tag with -F file of spaces and no newline should succeed' ' + git-tag -F blanknonlfile blanknonlfile-annotated-tag && + get_tag_msg blanknonlfile-annotated-tag >actual && + git diff expect actual +' + +# messages with commented lines: + +cat >commentsfile <<EOF +# A comment + +############ +The message. +############ +One line. + + +# commented lines +# commented lines + +Another line. +# comments + +Last line. +EOF +get_tag_header comments-annotated-tag $commit commit $time >expect +cat >>expect <<EOF +The message. +One line. + +Another line. + +Last line. +EOF +test_expect_success \ + 'creating a tag using a -F messagefile with #comments should succeed' ' + git-tag -F commentsfile comments-annotated-tag && + get_tag_msg comments-annotated-tag >actual && + git diff expect actual +' + +get_tag_header comment-annotated-tag $commit commit $time >expect +test_expect_success \ + 'creating a tag with a #comment in the -m message should succeed' ' + git-tag -m "#comment" comment-annotated-tag && + get_tag_msg comment-annotated-tag >actual && + git diff expect actual +' + +echo '#comment' >commentfile +echo '' >>commentfile +echo '####' >>commentfile +get_tag_header commentfile-annotated-tag $commit commit $time >expect +test_expect_success \ + 'creating a tag with #comments in the -F messagefile should succeed' ' + git-tag -F commentfile commentfile-annotated-tag && + get_tag_msg commentfile-annotated-tag >actual && + git diff expect actual +' + +printf '#comment' >commentnonlfile +get_tag_header commentnonlfile-annotated-tag $commit commit $time >expect +test_expect_success \ + 'creating a tag with a file of #comment and no newline should succeed' ' + git-tag -F commentnonlfile commentnonlfile-annotated-tag && + get_tag_msg commentnonlfile-annotated-tag >actual && + git diff expect actual +' + +# trying to verify annotated non-signed tags: + +test_expect_success \ + 'trying to verify an annotated non-signed tag should fail' ' + tag_exists annotated-tag && + ! git-tag -v annotated-tag +' + +test_expect_success \ + 'trying to verify a file-annotated non-signed tag should fail' ' + tag_exists file-annotated-tag && + ! git-tag -v file-annotated-tag +' + +# creating and verifying signed tags: + +gpg --version >/dev/null +if [ $? -eq 127 ]; then + echo "Skipping signed tags tests, because gpg was not found" + test_done + exit +fi + +# key generation info: gpg --homedir t/t7004 --gen-key +# Type DSA and Elgamal, size 2048 bits, no expiration date. +# Name and email: C O Mitter <committer@example.com> +# No password given, to enable non-interactive operation. + +cp -R ../t7004 ./gpghome +chmod 0700 gpghome +export GNUPGHOME="$(pwd)/gpghome" + +get_tag_header signed-tag $commit commit $time >expect +echo 'A signed tag message' >>expect +echo '-----BEGIN PGP SIGNATURE-----' >>expect +test_expect_success 'creating a signed tag with -m message should succeed' ' + git-tag -s -m "A signed tag message" signed-tag && + get_tag_msg signed-tag >actual && + git-diff expect actual +' + +test_expect_success 'verifying a signed tag should succeed' \ + 'git-tag -v signed-tag' + +test_expect_success 'verifying a forged tag should fail' ' + forged=$(git cat-file tag signed-tag | + sed -e "s/signed-tag/forged-tag/" | + git mktag) && + git tag forged-tag $forged && + ! git-tag -v forged-tag +' + +# blank and empty messages for signed tags: + +get_tag_header empty-signed-tag $commit commit $time >expect +echo '-----BEGIN PGP SIGNATURE-----' >>expect +test_expect_success \ + 'creating a signed tag with an empty -m message should succeed' ' + git-tag -s -m "" empty-signed-tag && + get_tag_msg empty-signed-tag >actual && + git diff expect actual && + git-tag -v empty-signed-tag +' + +>sigemptyfile +get_tag_header emptyfile-signed-tag $commit commit $time >expect +echo '-----BEGIN PGP SIGNATURE-----' >>expect +test_expect_success \ + 'creating a signed tag with an empty -F messagefile should succeed' ' + git-tag -s -F sigemptyfile emptyfile-signed-tag && + get_tag_msg emptyfile-signed-tag >actual && + git diff expect actual && + git-tag -v emptyfile-signed-tag +' + +printf '\n\n \n\t\nLeading blank lines\n' > sigblanksfile +printf '\n\t \t \nRepeated blank lines\n' >>sigblanksfile +printf '\n\n\nTrailing spaces \t \n' >>sigblanksfile +printf '\nTrailing blank lines\n\n\t \n\n' >>sigblanksfile +get_tag_header blanks-signed-tag $commit commit $time >expect +cat >>expect <<EOF +Leading blank lines + +Repeated blank lines + +Trailing spaces + +Trailing blank lines +EOF +echo '-----BEGIN PGP SIGNATURE-----' >>expect +test_expect_success \ + 'extra blanks in the message for a signed tag should be removed' ' + git-tag -s -F sigblanksfile blanks-signed-tag && + get_tag_msg blanks-signed-tag >actual && + git diff expect actual && + git-tag -v blanks-signed-tag +' + +get_tag_header blank-signed-tag $commit commit $time >expect +echo '-----BEGIN PGP SIGNATURE-----' >>expect +test_expect_success \ + 'creating a signed tag with a blank -m message should succeed' ' + git-tag -s -m " " blank-signed-tag && + get_tag_msg blank-signed-tag >actual && + git diff expect actual && + git-tag -v blank-signed-tag +' + +echo ' ' >sigblankfile +echo '' >>sigblankfile +echo ' ' >>sigblankfile +get_tag_header blankfile-signed-tag $commit commit $time >expect +echo '-----BEGIN PGP SIGNATURE-----' >>expect +test_expect_success \ + 'creating a signed tag with blank -F file with spaces should succeed' ' + git-tag -s -F sigblankfile blankfile-signed-tag && + get_tag_msg blankfile-signed-tag >actual && + git diff expect actual && + git-tag -v blankfile-signed-tag +' + +printf ' ' >sigblanknonlfile +get_tag_header blanknonlfile-signed-tag $commit commit $time >expect +echo '-----BEGIN PGP SIGNATURE-----' >>expect +test_expect_success \ + 'creating a signed tag with spaces and no newline should succeed' ' + git-tag -s -F sigblanknonlfile blanknonlfile-signed-tag && + get_tag_msg blanknonlfile-signed-tag >actual && + git diff expect actual && + git-tag -v signed-tag +' + +# messages with commented lines for signed tags: + +cat >sigcommentsfile <<EOF +# A comment + +############ +The message. +############ +One line. + + +# commented lines +# commented lines + +Another line. +# comments + +Last line. +EOF +get_tag_header comments-signed-tag $commit commit $time >expect +cat >>expect <<EOF +The message. +One line. + +Another line. + +Last line. +EOF +echo '-----BEGIN PGP SIGNATURE-----' >>expect +test_expect_success \ + 'creating a signed tag with a -F file with #comments should succeed' ' + git-tag -s -F sigcommentsfile comments-signed-tag && + get_tag_msg comments-signed-tag >actual && + git diff expect actual && + git-tag -v comments-signed-tag +' + +get_tag_header comment-signed-tag $commit commit $time >expect +echo '-----BEGIN PGP SIGNATURE-----' >>expect +test_expect_success \ + 'creating a signed tag with #commented -m message should succeed' ' + git-tag -s -m "#comment" comment-signed-tag && + get_tag_msg comment-signed-tag >actual && + git diff expect actual && + git-tag -v comment-signed-tag +' + +echo '#comment' >sigcommentfile +echo '' >>sigcommentfile +echo '####' >>sigcommentfile +get_tag_header commentfile-signed-tag $commit commit $time >expect +echo '-----BEGIN PGP SIGNATURE-----' >>expect +test_expect_success \ + 'creating a signed tag with #commented -F messagefile should succeed' ' + git-tag -s -F sigcommentfile commentfile-signed-tag && + get_tag_msg commentfile-signed-tag >actual && + git diff expect actual && + git-tag -v commentfile-signed-tag +' + +printf '#comment' >sigcommentnonlfile +get_tag_header commentnonlfile-signed-tag $commit commit $time >expect +echo '-----BEGIN PGP SIGNATURE-----' >>expect +test_expect_success \ + 'creating a signed tag with a #comment and no newline should succeed' ' + git-tag -s -F sigcommentnonlfile commentnonlfile-signed-tag && + get_tag_msg commentnonlfile-signed-tag >actual && + git diff expect actual && + git-tag -v commentnonlfile-signed-tag +' + +# tags pointing to objects different from commits: + +tree=$(git rev-parse HEAD^{tree}) +blob=$(git rev-parse HEAD:foo) +tag=$(git rev-parse signed-tag) + +get_tag_header tree-signed-tag $tree tree $time >expect +echo "A message for a tree" >>expect +echo '-----BEGIN PGP SIGNATURE-----' >>expect +test_expect_success \ + 'creating a signed tag pointing to a tree should succeed' ' + git-tag -s -m "A message for a tree" tree-signed-tag HEAD^{tree} && + get_tag_msg tree-signed-tag >actual && + git diff expect actual +' + +get_tag_header blob-signed-tag $blob blob $time >expect +echo "A message for a blob" >>expect +echo '-----BEGIN PGP SIGNATURE-----' >>expect +test_expect_success \ + 'creating a signed tag pointing to a blob should succeed' ' + git-tag -s -m "A message for a blob" blob-signed-tag HEAD:foo && + get_tag_msg blob-signed-tag >actual && + git diff expect actual +' + +get_tag_header tag-signed-tag $tag tag $time >expect +echo "A message for another tag" >>expect +echo '-----BEGIN PGP SIGNATURE-----' >>expect +test_expect_success \ + 'creating a signed tag pointing to another tag should succeed' ' + git-tag -s -m "A message for another tag" tag-signed-tag signed-tag && + get_tag_msg tag-signed-tag >actual && + git diff expect actual +' + +# try to verify without gpg: + +rm -rf gpghome +test_expect_failure \ + 'verify signed tag fails when public key is not present' \ + 'git-tag -v signed-tag' + +test_done diff --git a/t/t7004/pubring.gpg b/t/t7004/pubring.gpg Binary files differnew file mode 100644 index 000000000..83855fa4e --- /dev/null +++ b/t/t7004/pubring.gpg diff --git a/t/t7004/random_seed b/t/t7004/random_seed Binary files differnew file mode 100644 index 000000000..8fed1339e --- /dev/null +++ b/t/t7004/random_seed diff --git a/t/t7004/secring.gpg b/t/t7004/secring.gpg Binary files differnew file mode 100644 index 000000000..d831cd9eb --- /dev/null +++ b/t/t7004/secring.gpg diff --git a/t/t7004/trustdb.gpg b/t/t7004/trustdb.gpg Binary files differnew file mode 100644 index 000000000..abace962b --- /dev/null +++ b/t/t7004/trustdb.gpg diff --git a/t/t7400-submodule-basic.sh b/t/t7400-submodule-basic.sh index 3940433b8..7a9b505b1 100755 --- a/t/t7400-submodule-basic.sh +++ b/t/t7400-submodule-basic.sh @@ -18,7 +18,7 @@ subcommands of git-submodule. # -add directory lib to 'superproject', this creates a DIRLINK entry # -add a couple of regular files to enable testing of submodule filtering # -mv lib subrepo -# -add an entry to .gitmodules for path 'lib' +# -add an entry to .gitmodules for submodule 'example' # test_expect_success 'Prepare submodule testing' ' mkdir lib && @@ -40,7 +40,19 @@ test_expect_success 'Prepare submodule testing' ' git-add a lib z && git-commit -m "super commit 1" && mv lib .subrepo && - GIT_CONFIG=.gitmodules git-config module.lib.url git://example.com/lib.git + GIT_CONFIG=.gitmodules git-config submodule.example.url git://example.com/lib.git +' + +test_expect_success 'status should fail for unmapped paths' ' + if git-submodule status + then + echo "[OOPS] submodule status succeeded" + false + elif ! GIT_CONFIG=.gitmodules git-config submodule.example.path lib + then + echo "[OOPS] git-config failed to update .gitmodules" + false + fi ' test_expect_success 'status should only print one line' ' @@ -54,12 +66,12 @@ test_expect_success 'status should initially be "missing"' ' test_expect_success 'init should register submodule url in .git/config' ' git-submodule init && - url=$(git-config submodule.lib.url) && + url=$(git-config submodule.example.url) && if test "$url" != "git://example.com/lib.git" then echo "[OOPS] init succeeded but submodule url is wrong" false - elif ! git-config submodule.lib.url ./.subrepo + elif ! git-config submodule.example.url ./.subrepo then echo "[OOPS] init succeeded but update of url failed" false @@ -72,7 +84,7 @@ test_expect_success 'update should fail when path is used by a file' ' then echo "[OOPS] update should have failed" false - elif test -f lib && test "$(cat lib)" != "hello" + elif test "$(cat lib)" != "hello" then echo "[OOPS] update failed but lib file was molested" false diff --git a/t/t9400-git-cvsserver-server.sh b/t/t9400-git-cvsserver-server.sh index 41dcf646d..641303e0a 100755 --- a/t/t9400-git-cvsserver-server.sh +++ b/t/t9400-git-cvsserver-server.sh @@ -38,7 +38,7 @@ echo >empty && git commit -q -m "First Commit" && git clone -q --local --bare "$WORKDIR/.git" "$SERVERDIR" >/dev/null 2>&1 && GIT_DIR="$SERVERDIR" git config --bool gitcvs.enabled true && - GIT_DIR="$SERVERDIR" git config --bool gitcvs.logfile "$SERVERDIR/gitcvs.log" || + GIT_DIR="$SERVERDIR" git config gitcvs.logfile "$SERVERDIR/gitcvs.log" || exit 1 # note that cvs doesn't accept absolute pathnames @@ -143,6 +143,51 @@ test_expect_success 'req_Root failure (conflicting roots)' \ 'cat request-conflict | git-cvsserver pserver >log 2>&1 && tail log | grep -q "^error 1 Conflicting roots specified$"' +test_expect_success 'req_Root (strict paths)' \ + 'cat request-anonymous | git-cvsserver --strict-paths pserver $SERVERDIR >log 2>&1 && + tail -n1 log | grep -q "^I LOVE YOU$"' + +test_expect_failure 'req_Root failure (strict-paths)' \ + 'cat request-anonymous | git-cvsserver --strict-paths pserver $WORKDIR >log 2>&1' + +test_expect_success 'req_Root (w/o strict-paths)' \ + 'cat request-anonymous | git-cvsserver pserver $WORKDIR/ >log 2>&1 && + tail -n1 log | grep -q "^I LOVE YOU$"' + +test_expect_failure 'req_Root failure (w/o strict-paths)' \ + 'cat request-anonymous | git-cvsserver pserver $WORKDIR/gitcvs >log 2>&1' + +cat >request-base <<EOF +BEGIN AUTH REQUEST +/gitcvs.git +anonymous + +END AUTH REQUEST +Root /gitcvs.git +EOF + +test_expect_success 'req_Root (base-path)' \ + 'cat request-base | git-cvsserver --strict-paths --base-path $WORKDIR/ pserver $SERVERDIR >log 2>&1 && + tail -n1 log | grep -q "^I LOVE YOU$"' + +test_expect_failure 'req_Root failure (base-path)' \ + 'cat request-anonymous | git-cvsserver --strict-paths --base-path $WORKDIR pserver $SERVERDIR >log 2>&1' + +GIT_DIR="$SERVERDIR" git config --bool gitcvs.enabled false || exit 1 + +test_expect_success 'req_Root (export-all)' \ + 'cat request-anonymous | git-cvsserver --export-all pserver $WORKDIR >log 2>&1 && + tail -n1 log | grep -q "^I LOVE YOU$"' + +test_expect_failure 'req_Root failure (export-all w/o whitelist)' \ + 'cat request-anonymous | git-cvsserver --export-all pserver >log 2>&1 || + false' + +test_expect_success 'req_Root (everything together)' \ + 'cat request-base | git-cvsserver --export-all --strict-paths --base-path $WORKDIR/ pserver $SERVERDIR >log 2>&1 && + tail -n1 log | grep -q "^I LOVE YOU$"' + +GIT_DIR="$SERVERDIR" git config --bool gitcvs.enabled true || exit 1 #-------------- # CONFIG TESTS @@ -210,7 +255,7 @@ rm -fr "$SERVERDIR" cd "$WORKDIR" && git clone -q --local --bare "$WORKDIR/.git" "$SERVERDIR" >/dev/null 2>&1 && GIT_DIR="$SERVERDIR" git config --bool gitcvs.enabled true && -GIT_DIR="$SERVERDIR" git config --bool gitcvs.logfile "$SERVERDIR/gitcvs.log" || +GIT_DIR="$SERVERDIR" git config gitcvs.logfile "$SERVERDIR/gitcvs.log" || exit 1 test_expect_success 'cvs update (create new file)' \ diff --git a/t/t9500-gitweb-standalone-no-errors.sh b/t/t9500-gitweb-standalone-no-errors.sh index 44ae503b9..d94872456 100755 --- a/t/t9500-gitweb-standalone-no-errors.sh +++ b/t/t9500-gitweb-standalone-no-errors.sh @@ -60,6 +60,12 @@ gitweb_run () { . ./test-lib.sh +perl -MEncode -e 'decode_utf8("", Encode::FB_CROAK)' >/dev/null 2>&1 || { + test_expect_success 'skipping gitweb tests, perl version is too old' : + test_done + exit +} + gitweb_init # ---------------------------------------------------------------------- diff --git a/t/test-lib.sh b/t/test-lib.sh index 8bf4cf49a..78d7e87e8 100644 --- a/t/test-lib.sh +++ b/t/test-lib.sh @@ -26,6 +26,7 @@ GIT_COMMITTER_EMAIL=committer@example.com GIT_COMMITTER_NAME='C O Mitter' unset GIT_DIFF_OPTS unset GIT_DIR +unset GIT_WORK_TREE unset GIT_EXTERNAL_DIFF unset GIT_INDEX_FILE unset GIT_OBJECT_DIRECTORY diff --git a/tree-diff.c b/tree-diff.c index 852498eb4..26bdbdd2b 100644 --- a/tree-diff.c +++ b/tree-diff.c @@ -3,6 +3,7 @@ */ #include "cache.h" #include "diff.h" +#include "diffcore.h" #include "tree.h" static char *malloc_base(const char *base, int baselen, const char *path, int pathlen) @@ -290,6 +291,78 @@ int diff_tree(struct tree_desc *t1, struct tree_desc *t2, const char *base, stru return 0; } +/* + * Does it look like the resulting diff might be due to a rename? + * - single entry + * - not a valid previous file + */ +static inline int diff_might_be_rename(void) +{ + return diff_queued_diff.nr == 1 && + !DIFF_FILE_VALID(diff_queued_diff.queue[0]->one); +} + +static void try_to_follow_renames(struct tree_desc *t1, struct tree_desc *t2, const char *base, struct diff_options *opt) +{ + struct diff_options diff_opts; + struct diff_queue_struct *q = &diff_queued_diff; + struct diff_filepair *choice; + const char *paths[1]; + int i; + + /* Remove the file creation entry from the diff queue, and remember it */ + choice = q->queue[0]; + q->nr = 0; + + diff_setup(&diff_opts); + diff_opts.recursive = 1; + diff_opts.detect_rename = DIFF_DETECT_RENAME; + diff_opts.output_format = DIFF_FORMAT_NO_OUTPUT; + diff_opts.single_follow = opt->paths[0]; + paths[0] = NULL; + diff_tree_setup_paths(paths, &diff_opts); + if (diff_setup_done(&diff_opts) < 0) + die("unable to set up diff options to follow renames"); + diff_tree(t1, t2, base, &diff_opts); + diffcore_std(&diff_opts); + + /* Go through the new set of filepairing, and see if we find a more interesting one */ + for (i = 0; i < q->nr; i++) { + struct diff_filepair *p = q->queue[i]; + + /* + * Found a source? Not only do we use that for the new + * diff_queued_diff, we will also use that as the path in + * the future! + */ + if ((p->status == 'R' || p->status == 'C') && !strcmp(p->two->path, opt->paths[0])) { + /* Switch the file-pairs around */ + q->queue[i] = choice; + choice = p; + + /* Update the path we use from now on.. */ + opt->paths[0] = xstrdup(p->one->path); + diff_tree_setup_paths(opt->paths, opt); + break; + } + } + + /* + * Then, discard all the non-relevane file pairs... + */ + for (i = 0; i < q->nr; i++) { + struct diff_filepair *p = q->queue[i]; + diff_free_filepair(p); + } + + /* + * .. and re-instate the one we want (which might be either the + * original one, or the rename/copy we found) + */ + q->queue[0] = choice; + q->nr = 1; +} + int diff_tree_sha1(const unsigned char *old, const unsigned char *new, const char *base, struct diff_options *opt) { void *tree1, *tree2; @@ -306,6 +379,11 @@ int diff_tree_sha1(const unsigned char *old, const unsigned char *new, const cha init_tree_desc(&t1, tree1, size1); init_tree_desc(&t2, tree2, size2); retval = diff_tree(&t1, &t2, base, opt); + if (opt->follow_renames && diff_might_be_rename()) { + init_tree_desc(&t1, tree1, size1); + init_tree_desc(&t2, tree2, size2); + try_to_follow_renames(&t1, &t2, base, opt); + } free(tree1); free(tree2); return retval; diff --git a/tree-walk.h b/tree-walk.h index ee747aba0..db0fbdc70 100644 --- a/tree-walk.h +++ b/tree-walk.h @@ -22,7 +22,7 @@ static inline const unsigned char *tree_entry_extract(struct tree_desc *desc, co static inline int tree_entry_len(const char *name, const unsigned char *sha1) { - return (char *)sha1 - (char *)name - 1; + return (const char *)sha1 - name - 1; } void update_tree_entry(struct tree_desc *); diff --git a/write_or_die.c b/write_or_die.c index 5c4bc8515..e125e11d3 100644 --- a/write_or_die.c +++ b/write_or_die.c @@ -1,5 +1,45 @@ #include "cache.h" +/* + * Some cases use stdio, but want to flush after the write + * to get error handling (and to get better interactive + * behaviour - not buffering excessively). + * + * Of course, if the flush happened within the write itself, + * we've already lost the error code, and cannot report it any + * more. So we just ignore that case instead (and hope we get + * the right error code on the flush). + * + * If the file handle is stdout, and stdout is a file, then skip the + * flush entirely since it's not needed. + */ +void maybe_flush_or_die(FILE *f, const char *desc) +{ + static int skip_stdout_flush = -1; + struct stat st; + char *cp; + + if (f == stdout) { + if (skip_stdout_flush < 0) { + cp = getenv("GIT_FLUSH"); + if (cp) + skip_stdout_flush = (atoi(cp) == 0); + else if ((fstat(fileno(stdout), &st) == 0) && + S_ISREG(st.st_mode)) + skip_stdout_flush = 1; + else + skip_stdout_flush = 0; + } + if (skip_stdout_flush && !ferror(f)) + return; + } + if (fflush(f)) { + if (errno == EPIPE) + exit(0); + die("write failure on %s: %s", desc, strerror(errno)); + } +} + int read_in_full(int fd, void *buf, size_t count) { char *p = buf; |