diff options
60 files changed, 4239 insertions, 1222 deletions
diff --git a/Documentation/RelNotes-1.5.3.txt b/Documentation/RelNotes-1.5.3.txt index d111661a7b..ef2f95b3c5 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/config.txt b/Documentation/config.txt index 34b1c97102..50503e84b9 100644 --- a/Documentation/config.txt +++ b/Documentation/config.txt @@ -184,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/git-config.txt b/Documentation/git-config.txt index f2c67176f4..5f66a7fcd5 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-remote.txt b/Documentation/git-remote.txt index ab232c2f68..61a6022ce8 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 c33a512ffb..28949397ca 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-parse.txt b/Documentation/git-rev-parse.txt index 87771b832b..eea9c9cfe9 100644 --- a/Documentation/git-rev-parse.txt +++ b/Documentation/git-rev-parse.txt @@ -90,8 +90,15 @@ OPTIONS Show `$GIT_DIR` if defined else show the path to the .git directory. --is-inside-git-dir:: - Return "true" if we are in the git directory, otherwise "false". - Some commands require to be run in a working directory. + 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 diff --git a/Documentation/git-send-email.txt b/Documentation/git-send-email.txt index 946bd76afc..293686c31f 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.txt b/Documentation/git.txt index 20b5b7bb48..2cc0b214d2 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 ----------- @@ -103,6 +104,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`. @@ -347,6 +356,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':: @@ -396,6 +412,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 @@ -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 diff --git a/builtin-blame.c b/builtin-blame.c index f7e2c13885..da23a6f9c9 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-config.c b/builtin-config.c index b2515f7e65..3f7cab16d5 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) { @@ -155,6 +186,11 @@ int cmd_config(int argc, const char **argv, const char *prefix) } else if (!strcmp(argv[1], "--system")) setenv("GIT_CONFIG", 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 0be2d2ef6e..976f47b323 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 073a2a16a3..a4186eac8e 100644 --- a/builtin-log.c +++ b/builtin-log.c @@ -589,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) { diff --git a/builtin-ls-files.c b/builtin-ls-files.c index 5398a41415..61577ea13f 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 1952950c9a..758499238f 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-rerere.c b/builtin-rerere.c index f6409b93c1..29fb075d29 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 813aadf596..86db8b03fe 100644 --- a/builtin-rev-list.c +++ b/builtin-rev-list.c @@ -100,7 +100,7 @@ static void show_commit(struct commit *commit) 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 37addb25fa..497903a85a 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-stripspace.c b/builtin-stripspace.c index 62bd4b547b..d8358e28f0 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); @@ -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); @@ -533,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); @@ -587,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 c7c9963347..f2b10fa5f6 100755 --- a/contrib/completion/git-completion.bash +++ b/contrib/completion/git-completion.bash @@ -682,6 +682,9 @@ _git_push () esac __gitcomp "$(__git_refs "$remote")" "" "${cur#*:}" ;; + +*) + __gitcomp "$(__git_refs)" + "${cur#+}" + ;; *) __gitcomp "$(__git_refs)" ;; diff --git a/contrib/workdir/git-new-workdir b/contrib/workdir/git-new-workdir index 709b2a3ac0..3ff6bd166a 100755 --- a/contrib/workdir/git-new-workdir +++ b/contrib/workdir/git-new-workdir @@ -24,7 +24,7 @@ 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" +if test "$git_dir" = ".git" then git_dir="$orig_git/.git" fi @@ -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-compat-util.h b/git-compat-util.h index b2ab3f8256..362e040f52 100644 --- a/git-compat-util.h +++ b/git-compat-util.h @@ -287,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 69ccb88dde..ba23eb8eeb 100755 --- a/git-cvsimport.perl +++ b/git-cvsimport.perl @@ -1007,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-filter-branch.sh b/git-filter-branch.sh index 8fa5ce6467..a2fcebc1c6 100644 --- a/git-filter-branch.sh +++ b/git-filter-branch.sh @@ -312,9 +312,10 @@ case "$GIT_DIR" in /*) ;; *) - export GIT_DIR="$(pwd)/../../$GIT_DIR" + 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 diff --git a/git-gui/Makefile b/git-gui/Makefile index 3de0de1a23..1bac6fed46 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 3237f3d596..9df2e47029 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 139171d39e..b523654815 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-send-email.perl b/git-send-email.perl index 9f75551673..87f59fa313 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); @@ -484,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"; @@ -609,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; diff --git a/git-sh-setup.sh b/git-sh-setup.sh index f24c7f2d23..0de49e8459 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,7 +44,7 @@ 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." } diff --git a/git-svn.perl b/git-svn.perl index 50128d7285..03d5e2d979 100755 --- a/git-svn.perl +++ b/git-svn.perl @@ -596,8 +596,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 ", @@ -787,12 +786,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); } @@ -804,20 +803,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); @@ -1966,16 +1974,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) @@ -1993,7 +2004,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"; } @@ -2925,6 +2936,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-tag.sh b/git-tag.sh index c84043902f..1ff5b41e7f 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 f2d5597dba..68858b694d 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; @@ -171,7 +191,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 +238,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 +297,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 +327,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 +340,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 +370,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 +409,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 +441,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 287057e816..27182baa84 100644 --- a/git.spec.in +++ b/git.spec.in @@ -164,9 +164,9 @@ rm -rf $RPM_BUILD_ROOT %{_bindir}/git-gui %{_bindir}/git-citool %{_datadir}/git-gui/ -%{!?_without_docs: %{_mandir}/man1/git-gui.1} +%{!?_without_docs: %{_mandir}/man1/git-gui.1*} %{!?_without_docs: %doc Documentation/git-gui.html} -%{!?_without_docs: %{_mandir}/man1/git-citool.1} +%{!?_without_docs: %{_mandir}/man1/git-citool.1*} %{!?_without_docs: %doc Documentation/git-citool.html} %files -n gitk @@ -187,6 +187,9 @@ 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 @@ -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 82c8da3683..8403c36b63 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/log-tree.c b/log-tree.c index 0cf21bc051..ced3f332ef 100644 --- a/log-tree.c +++ b/log-tree.c @@ -408,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; } diff --git a/read-cache.c b/read-cache.c index 4362b11f47..a363f312c7 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; } @@ -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 7628ee97d9..f2b1ae0325 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/t0030-stripspace.sh b/t/t0030-stripspace.sh new file mode 100755 index 0000000000..f4294d72d9 --- /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 7731fa72ce..a2c11c4639 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 0000000000..ec4996637d --- /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 0000000000..aadeeab9ab --- /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/t3700-add.sh b/t/t3700-add.sh index ad8cc7d4ae..0d80c6aead 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/t5516-fetch-push.sh b/t/t5516-fetch-push.sh index 08d58e1c8c..c0fa2ba404 100755 --- a/t/t5516-fetch-push.sh +++ b/t/t5516-fetch-push.sh @@ -226,7 +226,7 @@ test_expect_success 'push with colon-less refspec (3)' ' git branch -f frotz master && git push testrepo frotz && check_push_result $the_commit heads/frotz && - test "$( cd testrepo && git show-ref | wc -l )" = 1 + test 1 = $( cd testrepo && git show-ref | wc -l ) ' test_expect_success 'push with colon-less refspec (4)' ' @@ -239,7 +239,7 @@ test_expect_success 'push with colon-less refspec (4)' ' git tag -f frotz && git push testrepo frotz && check_push_result $the_commit tags/frotz && - test "$( cd testrepo && git show-ref | wc -l )" = 1 + test 1 = $( cd testrepo && git show-ref | wc -l ) ' diff --git a/t/t6010-merge-base.sh b/t/t6010-merge-base.sh index b15920b852..22e0893056 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/t7004-tag.sh b/t/t7004-tag.sh new file mode 100755 index 0000000000..5d15449be5 --- /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 0000000000..83855fa4e1 --- /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 0000000000..8fed1339ed --- /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 0000000000..d831cd9eb3 --- /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 0000000000..abace962b8 --- /dev/null +++ b/t/t7004/trustdb.gpg diff --git a/t/t9400-git-cvsserver-server.sh b/t/t9400-git-cvsserver-server.sh index 0331770686..641303e0a1 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 @@ -255,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/test-lib.sh b/t/test-lib.sh index 8bf4cf49a2..78d7e87e86 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/write_or_die.c b/write_or_die.c index 5c4bc8515a..e125e11d3b 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; |