diff --git a/.gitignore b/.gitignore index c03ba8ea74..2179b5786b 100644 --- a/.gitignore +++ b/.gitignore @@ -156,7 +156,7 @@ common-cmds.h *.tar.gz *.dsc *.deb -git-core.spec +git.spec *.exe *.[ao] *.py[co] diff --git a/Documentation/Makefile b/Documentation/Makefile index 9cef4806d1..4edf788c3a 100644 --- a/Documentation/Makefile +++ b/Documentation/Makefile @@ -37,6 +37,9 @@ man7dir=$(mandir)/man7 ASCIIDOC=asciidoc ASCIIDOC_EXTRA = +ifdef ASCIIDOC8 +ASCIIDOC_EXTRA += -a asciidoc7compatible +endif INSTALL?=install DOC_REF = origin/man diff --git a/Documentation/RelNotes-1.5.2.2.txt b/Documentation/RelNotes-1.5.2.2.txt new file mode 100644 index 0000000000..f6393f8a94 --- /dev/null +++ b/Documentation/RelNotes-1.5.2.2.txt @@ -0,0 +1,61 @@ +GIT v1.5.2.2 Release Notes +========================== + +Fixes since v1.5.2.1 +-------------------- + +* Usability fix + + - git-gui is shipped with its updated blame interface. It is + rumored that the older one was not just unusable but was + active health hazard, but this one is actually pretty. + Please see for yourself. + +* Bugfixes + + - "git checkout fubar" was utterly confused when there is a + branch fubar and a tag fubar at the same time. It correctly + checks out the branch fubar now. + + - "git clone /path/foo" to clone a local /path/foo.git + repository left an incorrect configuration. + + - "git send-email" correctly unquotes RFC 2047 quoted names in + the patch-email before using their values. + + - We did not accept number of seconds since epoch older than + year 2000 as a valid timestamp. We now interpret positive + integers more than 8 digits as such, which allows us to + express timestamps more recent than March 1973. + + - git-cvsimport did not work when you have GIT_DIR to point + your repository at a nonstandard location. + + - Some systems (notably, Solaris) lack hstrerror() to make + h_errno human readable; prepare a replacement + implementation. + + - .gitignore file listed git-core.spec but what we generate is + git.spec, and nobody noticed for a long time. + + - "git-merge-recursive" does not try to run file level merge + on binary files. + + - "git-branch --track" did not create tracking configuration + correctly when the branch name had slash in it. + + - The email address of the user specified with user.email + configuration was overriden by EMAIL environment variable. + + - The tree parser did not warn about tree entries with + nonsense file modes, and assumed they must be blobs. + + - "git log -z" without any other request to generate diff still + invoked the diff machinery, wasting cycles. + +* Documentation + + - Many updates to fix stale or missing documentation. + + - Although our documentation was primarily meant to be formatted + with AsciiDoc7, formatting with AsciiDoc8 is supported better. diff --git a/Documentation/asciidoc.conf b/Documentation/asciidoc.conf index 60e15ba349..e061f73867 100644 --- a/Documentation/asciidoc.conf +++ b/Documentation/asciidoc.conf @@ -8,7 +8,8 @@ # the command. [attributes] -caret=^ +plus=+ +caret=^ startsb=[ endsb=] tilde=~ diff --git a/Documentation/git-archive.txt b/Documentation/git-archive.txt index 721e0351c3..4da07c1580 100644 --- a/Documentation/git-archive.txt +++ b/Documentation/git-archive.txt @@ -3,7 +3,7 @@ git-archive(1) NAME ---- -git-archive - Creates an archive of files from a named tree +git-archive - Create an archive of files from a named tree SYNOPSIS diff --git a/Documentation/git-cherry.txt b/Documentation/git-cherry.txt index 27b67b81a5..8c7d9670d3 100644 --- a/Documentation/git-cherry.txt +++ b/Documentation/git-cherry.txt @@ -53,6 +53,9 @@ OPTIONS :: Working branch; defaults to HEAD. +:: + Do not report commits up to (and including) limit. + Author ------ Written by Junio C Hamano diff --git a/Documentation/git-clone.txt b/Documentation/git-clone.txt index 644bf126fb..ac938d39df 100644 --- a/Documentation/git-clone.txt +++ b/Documentation/git-clone.txt @@ -3,7 +3,7 @@ git-clone(1) NAME ---- -git-clone - Clones a repository into a new directory +git-clone - Clone a repository into a new directory SYNOPSIS diff --git a/Documentation/git-cvsexportcommit.txt b/Documentation/git-cvsexportcommit.txt index fd7f54093f..f3590dee04 100644 --- a/Documentation/git-cvsexportcommit.txt +++ b/Documentation/git-cvsexportcommit.txt @@ -73,7 +73,7 @@ $ git-cvsexportcommit -v $ cvs commit -F .mgs ------------ -Merge pending patches into CVS automatically -- only if you really know what you are doing :: +Merge pending patches into CVS automatically -- only if you really know what you are doing:: + ------------ $ export GIT_DIR=~/project/.git diff --git a/Documentation/git-prune.txt b/Documentation/git-prune.txt index 0b44f3015d..b8166a210f 100644 --- a/Documentation/git-prune.txt +++ b/Documentation/git-prune.txt @@ -3,7 +3,7 @@ git-prune(1) NAME ---- -git-prune - Prunes all unreachable objects from the object database +git-prune - Prune all unreachable objects from the object database SYNOPSIS diff --git a/Documentation/git-unpack-objects.txt b/Documentation/git-unpack-objects.txt index ff6184b0f7..b1b3ec9772 100644 --- a/Documentation/git-unpack-objects.txt +++ b/Documentation/git-unpack-objects.txt @@ -27,8 +27,8 @@ new packs and replace existing ones. OPTIONS ------- -n:: - Only list the objects that would be unpacked, don't actually unpack - them. + Dry run. Check the pack file without actually unpacking + the objects. -q:: The command usually shows percentage progress. This diff --git a/Documentation/tutorial.txt b/Documentation/tutorial.txt index f55d4083ed..118ff72869 100644 --- a/Documentation/tutorial.txt +++ b/Documentation/tutorial.txt @@ -132,8 +132,8 @@ and newly modified files, and in both cases it takes a snapshot of the given files and stages that content in the index, ready for inclusion in the next commit. -Viewing the changelog ---------------------- +Viewing project history +----------------------- At any point you can view the history of your changes using diff --git a/Documentation/user-manual.txt b/Documentation/user-manual.txt index 7eaafa80e9..714e6a9942 100644 --- a/Documentation/user-manual.txt +++ b/Documentation/user-manual.txt @@ -27,7 +27,7 @@ $ man git-clone See also <> for a brief overview of git commands, without any explanation. -Also, see <> for ways that you can help make this manual more +Finally, see <> for ways that you can help make this manual more complete. @@ -921,6 +921,7 @@ echo "git diff --stat --summary -M v$last v$new > ../diffstat-$new" and then he just cut-and-pastes the output commands after verifying that they look OK. +[[Finding-comments-with-given-content]] Finding commits referencing a file with given content ----------------------------------------------------- @@ -1682,7 +1683,7 @@ automatically set the default remote branch to pull from at the time that a branch is created: ------------------------------------------------- -$ git checkout --track -b origin/maint maint +$ git checkout --track -b maint origin/maint ------------------------------------------------- In addition to saving you keystrokes, "git pull" also helps you by @@ -2756,8 +2757,8 @@ As a result, the general consistency of an object can always be tested independently of the contents or the type of the object: all objects can be validated by verifying that (a) their hashes match the content of the file and (b) the object successfully inflates to a stream of bytes that -forms a sequence of + + + + . +forms a sequence of {plus} {plus} {plus} {plus} . The structured objects can further have their structure and connectivity to other objects verified. This is generally done with @@ -3669,11 +3670,11 @@ itself! include::glossary.txt[] [[git-quick-start]] -Appendix A: Git Quick Start -=========================== +Appendix A: Git Quick Reference +=============================== -This is a quick summary of the major commands; the following chapters -will explain how these work in more detail. +This is a quick summary of the major commands; the previous chapters +explain how these work in more detail. [[quick-creating-a-new-repository]] Creating a new repository @@ -3951,3 +3952,7 @@ CVS, Subversion, and just imports of series of release tarballs. More details on gitweb? Write a chapter on using plumbing and writing scripts. + +Alternates, clone -reference, etc. + +git unpack-objects -r for recovery diff --git a/GIT-VERSION-GEN b/GIT-VERSION-GEN index bd30398dd3..3c3cd2f27d 100755 --- a/GIT-VERSION-GEN +++ b/GIT-VERSION-GEN @@ -1,7 +1,7 @@ #!/bin/sh GVF=GIT-VERSION-FILE -DEF_VER=v1.5.2.1.GIT +DEF_VER=v1.5.2.2.GIT LF=' ' diff --git a/Makefile b/Makefile index 48fa6a1af2..07ec6897f2 100644 --- a/Makefile +++ b/Makefile @@ -107,6 +107,8 @@ all: # Define USE_STDEV below if you want git to care about the underlying device # change being considered an inode change from the update-cache perspective. # +# Define ASCIIDOC8 if you want to format documentation with AsciiDoc 8 +# # Define NO_PERL_MAKEMAKER if you cannot use Makefiles generated by perl's # MakeMaker (e.g. using ActiveState under Cygwin). # @@ -410,6 +412,7 @@ ifeq ($(uname_S),SunOS) NEEDS_NSL = YesPlease SHELL_PATH = /bin/bash NO_STRCASESTR = YesPlease + NO_HSTRERROR = YesPlease ifeq ($(uname_R),5.8) NEEDS_LIBICONV = YesPlease NO_UNSETENV = YesPlease @@ -683,6 +686,10 @@ endif ifdef NO_PERL_MAKEMAKER export NO_PERL_MAKEMAKER endif +ifdef NO_HSTRERROR + COMPAT_CFLAGS += -DNO_HSTRERROR + COMPAT_OBJS += compat/hstrerror.o +endif ifeq ($(TCLTK_PATH),) NO_TCLTK=NoThanks @@ -713,6 +720,10 @@ ifndef V endif endif +ifdef ASCIIDOC8 + export ASCIIDOC8 +endif + # Shell quote (do not use $(call) to accommodate ancient setups); SHA1_HEADER_SQ = $(subst ','\'',$(SHA1_HEADER)) diff --git a/RelNotes b/RelNotes index 403fb97037..61f977884d 120000 --- a/RelNotes +++ b/RelNotes @@ -1 +1 @@ -Documentation/RelNotes-1.5.2.1.txt \ No newline at end of file +Documentation/RelNotes-1.5.2.2.txt \ No newline at end of file diff --git a/builtin-branch.c b/builtin-branch.c index 8956d0f842..94dba6e232 100644 --- a/builtin-branch.c +++ b/builtin-branch.c @@ -317,8 +317,6 @@ static void print_ref_list(int kinds, int detached, int verbose, int abbrev) static char *config_repo; static char *config_remote; static const char *start_ref; -static int start_len; -static int base_len; static int get_remote_branch_name(const char *value) { @@ -334,26 +332,41 @@ static int get_remote_branch_name(const char *value) end = value + strlen(value); - /* Try an exact match first. */ + /* + * Try an exact match first. I.e. handle the case where the + * value is "$anything:refs/foo/bar/baz" and start_ref is exactly + * "refs/foo/bar/baz". Then the name at the remote is $anything. + */ if (!strcmp(colon + 1, start_ref)) { - /* Truncate the value before the colon. */ + /* Truncate the value before the colon. */ nfasprintf(&config_repo, "%.*s", colon - value, value); return 1; } - /* Try with a wildcard match now. */ - if (end - value > 2 && end[-2] == '/' && end[-1] == '*' && - colon - value > 2 && colon[-2] == '/' && colon[-1] == '*' && - (end - 2) - (colon + 1) == base_len && - !strncmp(colon + 1, start_ref, base_len)) { - /* Replace the star with the remote branch name. */ - nfasprintf(&config_repo, "%.*s%s", - (colon - 2) - value, value, - start_ref + base_len); - return 1; - } + /* + * Is this a wildcard match? + */ + if ((end - 2 <= value) || end[-2] != '/' || end[-1] != '*' || + (colon - 2 <= value) || colon[-2] != '/' || colon[-1] != '*') + return 0; - return 0; + /* + * Value is "refs/foo/bar/:refs/baz/boa/" + * and start_ref begins with "refs/baz/boa/"; the name at the + * remote is refs/foo/bar/ with the remaining part of the + * start_ref. The length of the prefix on the RHS is (end - + * colon - 2), including the slash immediately before the + * asterisk. + */ + if ((strlen(start_ref) < end - colon - 2) || + memcmp(start_ref, colon + 1, end - colon - 2)) + return 0; /* does not match prefix */ + + /* Replace the asterisk with the remote branch name. */ + nfasprintf(&config_repo, "%.*s%s", + (colon - 1) - value, value, + start_ref + (end - colon - 2)); + return 1; } static int get_remote_config(const char *key, const char *value) @@ -363,10 +376,12 @@ static int get_remote_config(const char *key, const char *value) return 0; var = strrchr(key, '.'); - if (var == key + 6) + if (var == key + 6 || strcmp(var, ".fetch")) return 0; - - if (!strcmp(var, ".fetch") && get_remote_branch_name(value)) + /* + * Ok, we are looking at key == "remote.$foo.fetch"; + */ + if (get_remote_branch_name(value)) nfasprintf(&config_remote, "%.*s", var - (key + 7), key + 7); return 0; @@ -392,14 +407,14 @@ static void set_branch_merge(const char *name, const char *config_remote, static void set_branch_defaults(const char *name, const char *real_ref) { - const char *slash = strrchr(real_ref, '/'); - - if (!slash) - return; - + /* + * name is the name of new branch under refs/heads; + * real_ref is typically refs/remotes/$foo/$bar, where + * $foo is the remote name (there typically are no slashes) + * and $bar is the branch name we map from the remote + * (it could have slashes). + */ start_ref = real_ref; - start_len = strlen(real_ref); - base_len = slash - real_ref; git_config(get_remote_config); if (!config_repo && !config_remote && !prefixcmp(real_ref, "refs/heads/")) { diff --git a/compat/hstrerror.c b/compat/hstrerror.c new file mode 100644 index 0000000000..069c555da4 --- /dev/null +++ b/compat/hstrerror.c @@ -0,0 +1,21 @@ +#include +#include +#include + +const char *githstrerror(int err) +{ + static char buffer[48]; + switch (err) + { + case HOST_NOT_FOUND: + return "Authoritative answer: host not found"; + case NO_DATA: + return "Valid name, no data record of requested type"; + case NO_RECOVERY: + return "Non recoverable errors, FORMERR, REFUSED, NOTIMP"; + case TRY_AGAIN: + return "Non-authoritative \"host not found\", or SERVERFAIL"; + } + sprintf(buffer, "Name resolution error %d", err); + return buffer; +} diff --git a/date.c b/date.c index 93c4eaf26d..2d5f2da68a 100644 --- a/date.c +++ b/date.c @@ -414,9 +414,11 @@ static int match_digit(const char *date, struct tm *tm, int *offset, int *tm_gmt num = strtoul(date, &end, 10); /* - * Seconds since 1970? We trigger on that for anything after Jan 1, 2000 + * Seconds since 1970? We trigger on that for any numbers with + * more than 8 digits. This is because we don't want to rule out + * numbers like 20070606 as a YYYYMMDD date. */ - if (num > 946684800) { + if (num >= 100000000) { time_t time = num; if (gmtime_r(&time, tm)) { *tm_gmt = 1; diff --git a/diff.c b/diff.c index b3a50ac72e..07b326638c 100644 --- a/diff.c +++ b/diff.c @@ -1108,10 +1108,8 @@ static void setup_diff_attr_check(struct git_attr_check *check) check->attr = attr_diff; } -#define FIRST_FEW_BYTES 8000 static int file_is_binary(struct diff_filespec *one) { - unsigned long sz; struct git_attr_check attr_diff_check; setup_diff_attr_check(&attr_diff_check); @@ -1128,10 +1126,7 @@ static int file_is_binary(struct diff_filespec *one) return 0; diff_populate_filespec(one, 0); } - sz = one->size; - if (FIRST_FEW_BYTES < sz) - sz = FIRST_FEW_BYTES; - return !!memchr(one->data, 0, sz); + return buffer_is_binary(one->data, one->size); } static void builtin_diff(const char *name_a, diff --git a/git-checkout.sh b/git-checkout.sh index ed7c2c5f6a..7c5ca3d62f 100755 --- a/git-checkout.sh +++ b/git-checkout.sh @@ -63,12 +63,13 @@ while [ "$#" != "0" ]; do echo "unknown flag $arg" exit 1 fi - new="$rev" new_name="$arg" if git-show-ref --verify --quiet -- "refs/heads/$arg" then + rev=$(git-rev-parse --verify "refs/heads/$arg^0") branch="$arg" fi + new="$rev" elif rev=$(git-rev-parse --verify "$arg^{tree}" 2>/dev/null) then # checking out selected paths from a tree-ish. diff --git a/git-clone.sh b/git-clone.sh old mode 100755 new mode 100644 index 1ff9f2987b..05c9398541 --- a/git-clone.sh +++ b/git-clone.sh @@ -20,7 +20,7 @@ usage() { get_repo_base() { ( cd "`/bin/pwd -W`" && - cd "$1" && + cd "$1" || cd "$1.git" && { cd .git pwd -W diff --git a/git-compat-util.h b/git-compat-util.h index 515331d0b5..dcae52ac8d 100644 --- a/git-compat-util.h +++ b/git-compat-util.h @@ -178,6 +178,11 @@ extern size_t gitstrlcpy(char *, const char *, size_t); extern uintmax_t gitstrtoumax(const char *, char **, int); #endif +#ifdef NO_HSTRERROR +#define hstrerror githstrerror +extern const char *githstrerror(int herror); +#endif + extern void release_pack_memory(size_t, int); static inline char* xstrdup(const char *str) diff --git a/git-cvsimport.perl b/git-cvsimport.perl index f68afe78a0..4e6c9c6cc7 100755 --- a/git-cvsimport.perl +++ b/git-cvsimport.perl @@ -692,8 +692,8 @@ sub commit { if ($branch eq $opt_o && !$index{branch} && !get_headref($branch, $git_dir)) { # looks like an initial commit # use the index primed by git-init - $ENV{GIT_INDEX_FILE} = '.git/index'; - $index{$branch} = '.git/index'; + $ENV{GIT_INDEX_FILE} = "$git_dir/index"; + $index{$branch} = "$git_dir/index"; } else { # use an index per branch to speed up # imports of projects with many branches @@ -984,7 +984,7 @@ if ($line =~ /^(\d+) objects, (\d+) kilobytes$/) { } foreach my $git_index (values %index) { - if ($git_index ne '.git/index') { + if ($git_index ne "$git_dir/index") { unlink($git_index); } } diff --git a/git-gui/GIT-VERSION-GEN b/git-gui/GIT-VERSION-GEN index 25647c8060..eee495a986 100755 --- a/git-gui/GIT-VERSION-GEN +++ b/git-gui/GIT-VERSION-GEN @@ -78,5 +78,3 @@ test "$VN" = "$VC" || { echo >&2 "GITGUI_VERSION = $VN" echo "GITGUI_VERSION = $VN" >$GVF } - - diff --git a/git-gui/git-gui.sh b/git-gui/git-gui.sh index dba585111c..97de595f27 100755 --- a/git-gui/git-gui.sh +++ b/git-gui/git-gui.sh @@ -20,6 +20,22 @@ You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA} +###################################################################### +## +## Tcl/Tk sanity check + +if {[catch {package require Tcl 8.4} err] + || [catch {package require Tk 8.4} err] +} { + catch {wm withdraw .} + tk_messageBox \ + -icon error \ + -type ok \ + -title "git-gui: fatal error" \ + -message $err + exit 1 +} + ###################################################################### ## ## configure our library @@ -455,7 +471,8 @@ proc rescan {after {honor_trustmtime 1}} { if {![$ui_comm edit modified] || [string trim [$ui_comm get 0.0 end]] eq {}} { - if {[load_message GITGUI_MSG]} { + if {[string match amend* $commit_type]} { + } elseif {[load_message GITGUI_MSG]} { } elseif {[load_message MERGE_MSG]} { } elseif {[load_message SQUASH_MSG]} { } @@ -1008,6 +1025,7 @@ proc incr_font_size {font {amt 1}} { incr sz $amt font configure $font -size $sz font configure ${font}bold -size $sz + font configure ${font}italic -size $sz } ###################################################################### @@ -1200,12 +1218,14 @@ catch { destroy .dummy } +font create font_uiitalic font create font_uibold font create font_diffbold +font create font_diffitalic foreach class {Button Checkbutton Entry Label Labelframe Listbox Menu Message - Radiobutton Text} { + Radiobutton Spinbox Text} { option add *$class.font font_ui } unset class @@ -1233,8 +1253,10 @@ proc apply_config {} { } foreach {cn cv} [font configure $font] { font configure ${font}bold $cn $cv + font configure ${font}italic $cn $cv } font configure ${font}bold -weight bold + font configure ${font}italic -slant italic } } @@ -1600,7 +1622,7 @@ unset browser doc_path doc_url # -- Standard bindings # -bind . do_quit +wm protocol . WM_DELETE_WINDOW do_quit bind all <$M1B-Key-q> do_quit bind all <$M1B-Key-Q> do_quit bind all <$M1B-Key-w> {destroy [winfo toplevel %W]} @@ -1715,7 +1737,7 @@ pack .vpane -anchor n -side top -fill both -expand 1 # frame .vpane.files.index -height 100 -width 200 label .vpane.files.index.title -text {Staged Changes (Will Be Committed)} \ - -background green + -background lightgreen text $ui_index -background white -borderwidth 0 \ -width 20 -height 10 \ -wrap none \ @@ -1735,7 +1757,7 @@ pack $ui_index -side left -fill both -expand 1 # frame .vpane.files.workdir -height 100 -width 200 label .vpane.files.workdir.title -text {Unstaged Changes (Will Not Be Committed)} \ - -background red + -background lightsalmon text $ui_workdir -background white -borderwidth 0 \ -width 20 -height 10 \ -wrap none \ @@ -1752,10 +1774,8 @@ pack $ui_workdir -side left -fill both -expand 1 .vpane.files add .vpane.files.workdir -sticky nsew foreach i [list $ui_index $ui_workdir] { - $i tag conf in_diff -font font_uibold - $i tag conf in_sel \ - -background [$i cget -foreground] \ - -foreground [$i cget -background] + $i tag conf in_diff -background lightgray + $i tag conf in_sel -background lightgray } unset i @@ -1913,18 +1933,18 @@ proc trace_current_diff_path {varname args} { } trace add variable current_diff_path write trace_current_diff_path -frame .vpane.lower.diff.header -background orange +frame .vpane.lower.diff.header -background gold label .vpane.lower.diff.header.status \ - -background orange \ + -background gold \ -width $max_status_desc \ -anchor w \ -justify left label .vpane.lower.diff.header.file \ - -background orange \ + -background gold \ -anchor w \ -justify left label .vpane.lower.diff.header.path \ - -background orange \ + -background gold \ -anchor w \ -justify left pack .vpane.lower.diff.header.status -side left @@ -2038,17 +2058,17 @@ lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state] $ctxm add separator $ctxm add command \ -label {Show Less Context} \ - -command {if {$repo_config(gui.diffcontext) >= 2} { + -command {if {$repo_config(gui.diffcontext) >= 1} { incr repo_config(gui.diffcontext) -1 reshow_diff }} lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state] $ctxm add command \ -label {Show More Context} \ - -command { + -command {if {$repo_config(gui.diffcontext) < 99} { incr repo_config(gui.diffcontext) reshow_diff - } + }} lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state] $ctxm add separator $ctxm add command -label {Options...} \ diff --git a/git-gui/lib/blame.tcl b/git-gui/lib/blame.tcl index 8b032d9590..139171d39e 100644 --- a/git-gui/lib/blame.tcl +++ b/git-gui/lib/blame.tcl @@ -3,145 +3,270 @@ class blame { -field commit ; # input commit to blame -field path ; # input filename to view in $commit +image create photo ::blame::img_back_arrow -data {R0lGODlhGAAYAIUAAPwCBEzKXFTSZIz+nGzmhGzqfGTidIT+nEzGXHTqhGzmfGzifFzadETCVES+VARWDFzWbHzyjAReDGTadFTOZDSyRDyyTCymPARaFGTedFzSbDy2TCyqRCyqPARaDAyCHES6VDy6VCyiPAR6HCSeNByWLARyFARiDARqFGTifARiFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAAYABgAAAajQIBwSCwaj8ikcsk0BppJwRPqHEypQwHBis0WDAdEFyBIKBaMAKLBdjQeSkFBYTBAIvgEoS6JmhUTEwIUDQ4VFhcMGEhyCgoZExoUaxsWHB0THkgfAXUGAhoBDSAVFR0XBnCbDRmgog0hpSIiDJpJIyEQhBUcJCIlwA22SSYVogknEg8eD82qSigdDSknY0IqJQXPYxIl1dZCGNvWw+Dm510GQQAh/mhDcmVhdGVkIGJ5IEJNUFRvR0lGIFBybyB2ZXJzaW9uIDIuNQ0KqSBEZXZlbENvciAxOTk3LDE5OTguIEFsbCByaWdodHMgcmVzZXJ2ZWQuDQpodHRwOi8vd3d3LmRldmVsY29yLmNvbQA7} -field w -field w_line -field w_load -field w_file -field w_cmit -field status +# Persistant data (survives loads) +# +field history {}; # viewer history: {commit path} +field header ; # array commit,key -> header field -field highlight_line -1 ; # current line selected -field highlight_commit {} ; # sha1 of commit selected +# Tk UI control paths +# +field w ; # top window in this viewer +field w_back ; # our back button +field w_path ; # label showing the current file path +field w_columns ; # list of all column widgets in the viewer +field w_line ; # text column: all line numbers +field w_amov ; # text column: annotations + move tracking +field w_asim ; # text column: annotations (simple computation) +field w_file ; # text column: actual file data +field w_cviewer ; # pane showing commit message +field status ; # text variable bound to status bar +field old_height ; # last known height of $w.file_pane + +# Tk UI colors +# +variable active_color #c0edc5 +variable group_colors { + #d6d6d6 + #e1e1e1 + #ececec +} + +# Current blame data; cleared/reset on each load +# +field commit ; # input commit to blame +field path ; # input filename to view in $commit + +field current_fd {} ; # background process running +field highlight_line -1 ; # current line selected +field highlight_column {} ; # current commit column selected +field highlight_commit {} ; # sha1 of commit selected field total_lines 0 ; # total length of file field blame_lines 0 ; # number of lines computed -field commit_count 0 ; # number of commits in $commit_list -field commit_list {} ; # list of commit sha1 in receipt order -field order ; # array commit -> receipt order -field header ; # array commit,key -> header field -field line_commit ; # array line -> sha1 commit -field line_file ; # array line -> file name +field amov_data ; # list of {commit origfile origline} +field asim_data ; # list of {commit origfile origline} -field r_commit ; # commit currently being parsed -field r_orig_line ; # original line number -field r_final_line ; # final line number -field r_line_count ; # lines in this region +field r_commit ; # commit currently being parsed +field r_orig_line ; # original line number +field r_final_line ; # final line number +field r_line_count ; # lines in this region + +field tooltip_wm {} ; # Current tooltip toplevel, if open +field tooltip_t {} ; # Text widget in $tooltip_wm +field tooltip_timer {} ; # Current timer event for our tooltip +field tooltip_commit {} ; # Commit(s) in tooltip constructor new {i_commit i_path} { + global cursor_ptr + variable active_color + variable group_colors + set commit $i_commit set path $i_path make_toplevel top w wm title $top "[appname] ([reponame]): File Viewer" - set status "Loading $commit:$path..." - label $w.path -text "$commit:$path" \ + frame $w.header -background gold + label $w.header.commit_l \ + -text {Commit:} \ + -background gold \ -anchor w \ - -justify left \ - -borderwidth 1 \ - -relief sunken \ - -font font_uibold - pack $w.path -side top -fill x + -justify left + set w_back $w.header.commit_b + label $w_back \ + -image ::blame::img_back_arrow \ + -borderwidth 0 \ + -relief flat \ + -state disabled \ + -background gold \ + -activebackground gold + bind $w_back " + if {\[$w_back cget -state\] eq {normal}} { + [cb _history_menu] + } + " + label $w.header.commit \ + -textvariable @commit \ + -background gold \ + -anchor w \ + -justify left + label $w.header.path_l \ + -text {File:} \ + -background gold \ + -anchor w \ + -justify left + set w_path $w.header.path + label $w_path \ + -background gold \ + -anchor w \ + -justify left + pack $w.header.commit_l -side left + pack $w_back -side left + pack $w.header.commit -side left + pack $w_path -fill x -side right + pack $w.header.path_l -side right - frame $w.out - text $w.out.loaded_t \ + panedwindow $w.file_pane -orient vertical + frame $w.file_pane.out + frame $w.file_pane.cm + $w.file_pane add $w.file_pane.out \ + -sticky nsew \ + -minsize 100 \ + -height 100 \ + -width 100 + $w.file_pane add $w.file_pane.cm \ + -sticky nsew \ + -minsize 25 \ + -height 25 \ + -width 100 + + set w_line $w.file_pane.out.linenumber_t + text $w_line \ + -takefocus 0 \ + -highlightthickness 0 \ + -padx 0 -pady 0 \ -background white -borderwidth 0 \ -state disabled \ -wrap none \ -height 40 \ - -width 1 \ + -width 6 \ -font font_diff - $w.out.loaded_t tag conf annotated -background grey + $w_line tag conf linenumber -justify right -rmargin 5 - text $w.out.linenumber_t \ + set w_amov $w.file_pane.out.amove_t + text $w_amov \ + -takefocus 0 \ + -highlightthickness 0 \ + -padx 0 -pady 0 \ -background white -borderwidth 0 \ -state disabled \ -wrap none \ -height 40 \ -width 5 \ -font font_diff - $w.out.linenumber_t tag conf linenumber -justify right + $w_amov tag conf author_abbr -justify right -rmargin 5 + $w_amov tag conf curr_commit + $w_amov tag conf prior_commit -foreground blue -underline 1 + $w_amov tag bind prior_commit \ + \ + "[cb _load_commit $w_amov @amov_data @%x,%y];break" - text $w.out.file_t \ + set w_asim $w.file_pane.out.asimple_t + text $w_asim \ + -takefocus 0 \ + -highlightthickness 0 \ + -padx 0 -pady 0 \ + -background white -borderwidth 0 \ + -state disabled \ + -wrap none \ + -height 40 \ + -width 4 \ + -font font_diff + $w_asim tag conf author_abbr -justify right + $w_asim tag conf curr_commit + $w_asim tag conf prior_commit -foreground blue -underline 1 + $w_asim tag bind prior_commit \ + \ + "[cb _load_commit $w_asim @asim_data @%x,%y];break" + + set w_file $w.file_pane.out.file_t + text $w_file \ + -takefocus 0 \ + -highlightthickness 0 \ + -padx 0 -pady 0 \ -background white -borderwidth 0 \ -state disabled \ -wrap none \ -height 40 \ -width 80 \ - -xscrollcommand [list $w.out.sbx set] \ + -xscrollcommand [list $w.file_pane.out.sbx set] \ -font font_diff - scrollbar $w.out.sbx -orient h -command [list $w.out.file_t xview] - scrollbar $w.out.sby -orient v \ - -command [list scrollbar2many [list \ - $w.out.loaded_t \ - $w.out.linenumber_t \ - $w.out.file_t \ - ] yview] - grid \ - $w.out.linenumber_t \ - $w.out.loaded_t \ - $w.out.file_t \ - $w.out.sby \ - -sticky nsew - grid conf $w.out.sbx -column 2 -sticky we - grid columnconfigure $w.out 2 -weight 1 - grid rowconfigure $w.out 0 -weight 1 - pack $w.out -fill both -expand 1 + set w_columns [list $w_amov $w_asim $w_line $w_file] - label $w.status \ - -textvariable @status \ - -anchor w \ - -justify left \ - -borderwidth 1 \ - -relief sunken - pack $w.status -side bottom -fill x + scrollbar $w.file_pane.out.sbx \ + -orient h \ + -command [list $w_file xview] + scrollbar $w.file_pane.out.sby \ + -orient v \ + -command [list scrollbar2many $w_columns yview] + eval grid $w_columns $w.file_pane.out.sby -sticky nsew + grid conf \ + $w.file_pane.out.sbx \ + -column [expr {[llength $w_columns] - 1}] \ + -sticky we + grid columnconfigure \ + $w.file_pane.out \ + [expr {[llength $w_columns] - 1}] \ + -weight 1 + grid rowconfigure $w.file_pane.out 0 -weight 1 - frame $w.cm - text $w.cm.t \ + set w_cviewer $w.file_pane.cm.t + text $w_cviewer \ -background white -borderwidth 0 \ -state disabled \ -wrap none \ -height 10 \ -width 80 \ - -xscrollcommand [list $w.cm.sbx set] \ - -yscrollcommand [list $w.cm.sby set] \ + -xscrollcommand [list $w.file_pane.cm.sbx set] \ + -yscrollcommand [list $w.file_pane.cm.sby set] \ -font font_diff - scrollbar $w.cm.sbx -orient h -command [list $w.cm.t xview] - scrollbar $w.cm.sby -orient v -command [list $w.cm.t yview] - pack $w.cm.sby -side right -fill y - pack $w.cm.sbx -side bottom -fill x - pack $w.cm.t -expand 1 -fill both - pack $w.cm -side bottom -fill x + $w_cviewer tag conf still_loading \ + -font font_uiitalic \ + -justify center + $w_cviewer tag conf header_key \ + -tabs {3c} \ + -background $active_color \ + -font font_uibold + $w_cviewer tag conf header_val \ + -background $active_color \ + -font font_ui + $w_cviewer tag raise sel + scrollbar $w.file_pane.cm.sbx \ + -orient h \ + -command [list $w_cviewer xview] + scrollbar $w.file_pane.cm.sby \ + -orient v \ + -command [list $w_cviewer yview] + pack $w.file_pane.cm.sby -side right -fill y + pack $w.file_pane.cm.sbx -side bottom -fill x + pack $w_cviewer -expand 1 -fill both + + frame $w.status \ + -borderwidth 1 \ + -relief sunken + label $w.status.l \ + -textvariable @status \ + -anchor w \ + -justify left + pack $w.status.l -side left menu $w.ctxm -tearoff 0 $w.ctxm add command \ -label "Copy Commit" \ -command [cb _copycommit] - set w_line $w.out.linenumber_t - set w_load $w.out.loaded_t - set w_file $w.out.file_t - set w_cmit $w.cm.t + foreach i $w_columns { + for {set g 0} {$g < [llength $group_colors]} {incr g} { + $i tag conf color$g -background [lindex $group_colors $g] + } - foreach i [list \ - $w.out.loaded_t \ - $w.out.linenumber_t \ - $w.out.file_t] { - $i tag conf in_sel \ - -background [$i cget -foreground] \ - -foreground [$i cget -background] - $i conf -yscrollcommand \ - [list many2scrollbar [list \ - $w.out.loaded_t \ - $w.out.linenumber_t \ - $w.out.file_t \ - ] yview $w.out.sby] - bind $i "[cb _click $i @%x,%y]; focus $i" + $i conf -cursor $cursor_ptr + $i conf -yscrollcommand [list many2scrollbar \ + $w_columns yview $w.file_pane.out.sby] + bind $i " + [cb _hide_tooltip] + [cb _click $i @%x,%y] + focus $i + " + bind $i [cb _show_tooltip $i @%x,%y] + bind $i [cb _hide_tooltip] + bind $i [cb _hide_tooltip] bind_button3 $i " + [cb _hide_tooltip] set cursorX %x set cursorY %y set cursorW %W @@ -149,11 +274,7 @@ constructor new {i_commit i_path} { " } - foreach i [list \ - $w.out.loaded_t \ - $w.out.linenumber_t \ - $w.out.file_t \ - $w.cm.t] { + foreach i [concat $w_columns $w_cviewer] { bind $i {catch {%W yview scroll -1 units};break} bind $i {catch {%W yview scroll 1 units};break} bind $i {catch {%W xview scroll -1 units};break} @@ -166,10 +287,95 @@ constructor new {i_commit i_path} { bind $i {catch {%W yview scroll 1 pages};break} } - bind $w.cm.t [list focus $w.cm.t] + bind $w_cviewer [list focus $w_cviewer] bind $top [list focus $top] - bind $top [list delete_this $this] + grid configure $w.header -sticky ew + grid configure $w.file_pane -sticky nsew + grid configure $w.status -sticky ew + grid columnconfigure $top 0 -weight 1 + grid rowconfigure $top 0 -weight 0 + grid rowconfigure $top 1 -weight 1 + grid rowconfigure $top 2 -weight 0 + + set req_w [winfo reqwidth $top] + set req_h [winfo reqheight $top] + if {$req_w < 600} {set req_w 600} + if {$req_h < 400} {set req_h 400} + set g "${req_w}x${req_h}" + wm geometry $top $g + update + + set old_height [winfo height $w.file_pane] + $w.file_pane sash place 0 \ + [lindex [$w.file_pane sash coord 0] 0] \ + [expr {int($old_height * 0.70)}] + bind $w.file_pane \ + "if {{$w.file_pane} eq {%W}} {[cb _resize %h]}" + + _load $this {} +} + +method _load {jump} { + variable group_colors + + _hide_tooltip $this + + if {$total_lines != 0 || $current_fd ne {}} { + if {$current_fd ne {}} { + catch {close $current_fd} + set current_fd {} + } + + foreach i $w_columns { + $i conf -state normal + $i delete 0.0 end + foreach g [$i tag names] { + if {[regexp {^g[0-9a-f]{40}$} $g]} { + $i tag delete $g + } + } + $i conf -state disabled + } + + $w_cviewer conf -state normal + $w_cviewer delete 0.0 end + $w_cviewer conf -state disabled + + set highlight_line -1 + set highlight_column {} + set highlight_commit {} + set total_lines 0 + } + + if {[winfo exists $w.status.c]} { + $w.status.c coords bar 0 0 0 20 + } else { + canvas $w.status.c \ + -width 100 \ + -height [expr {int([winfo reqheight $w.status.l] * 0.6)}] \ + -borderwidth 1 \ + -relief groove \ + -highlightt 0 + $w.status.c create rectangle 0 0 0 20 -tags bar -fill navy + pack $w.status.c -side right + } + + if {$history eq {}} { + $w_back conf -state disabled + } else { + $w_back conf -state normal + } + + # Index 0 is always empty. There is never line 0 as + # we use only 1 based lines, as that matches both with + # git-blame output and with Tk's text widget. + # + set amov_data [list [list]] + set asim_data [list [list]] + + set status "Loading $commit:[escape_path $path]..." + $w_path conf -text [escape_path $path] if {$commit eq {}} { set fd [open $path r] } else { @@ -177,41 +383,133 @@ constructor new {i_commit i_path} { set fd [open "| $cmd" r] } fconfigure $fd -blocking 0 -translation lf -encoding binary - fileevent $fd readable [cb _read_file $fd] + fileevent $fd readable [cb _read_file $fd $jump] + set current_fd $fd } -method _read_file {fd} { - $w_load conf -state normal - $w_line conf -state normal - $w_file conf -state normal +method _history_menu {} { + set m $w.backmenu + if {[winfo exists $m]} { + $m delete 0 end + } else { + menu $m -tearoff 0 + } + + for {set i [expr {[llength $history] - 1}] + } {$i >= 0} {incr i -1} { + set e [lindex $history $i] + set c [lindex $e 0] + set f [lindex $e 1] + + if {[regexp {^[0-9a-f]{40}$} $c]} { + set t [string range $c 0 8]... + } elseif {$c eq {}} { + set t {Working Directory} + } else { + set t $c + } + if {![catch {set summary $header($c,summary)}]} { + append t " $summary" + if {[string length $t] > 70} { + set t [string range $t 0 66]... + } + } + + $m add command -label $t -command [cb _goback $i] + } + set X [winfo rootx $w_back] + set Y [expr {[winfo rooty $w_back] + [winfo height $w_back]}] + tk_popup $m $X $Y +} + +method _goback {i} { + set dat [lindex $history $i] + set history [lrange $history 0 [expr {$i - 1}]] + set commit [lindex $dat 0] + set path [lindex $dat 1] + _load $this [lrange $dat 2 5] +} + +method _read_file {fd jump} { + if {$fd ne $current_fd} { + catch {close $fd} + return + } + + foreach i $w_columns {$i conf -state normal} while {[gets $fd line] >= 0} { regsub "\r\$" $line {} line incr total_lines - $w_load insert end "\n" - $w_line insert end "$total_lines\n" linenumber - $w_file insert end "$line\n" + lappend amov_data {} + lappend asim_data {} + + if {$total_lines > 1} { + foreach i $w_columns {$i insert end "\n"} + } + + $w_line insert end "$total_lines" linenumber + $w_file insert end "$line" } - $w_load conf -state disabled - $w_line conf -state disabled - $w_file conf -state disabled + + set ln_wc [expr {[string length $total_lines] + 2}] + if {[$w_line cget -width] < $ln_wc} { + $w_line conf -width $ln_wc + } + + foreach i $w_columns {$i conf -state disabled} if {[eof $fd]} { close $fd - _status $this - set cmd [list git blame -M -C --incremental] - if {$commit eq {}} { - lappend cmd --contents $path - } else { - lappend cmd $commit + + # If we don't force Tk to update the widgets *right now* + # none of our jump commands will cause a change in the UI. + # + update + + if {[llength $jump] == 1} { + set highlight_line [lindex $jump 0] + $w_file see "$highlight_line.0" + } elseif {[llength $jump] == 4} { + set highlight_column [lindex $jump 0] + set highlight_line [lindex $jump 1] + $w_file xview moveto [lindex $jump 2] + $w_file yview moveto [lindex $jump 3] } - lappend cmd -- $path - set fd [open "| $cmd" r] - fconfigure $fd -blocking 0 -translation lf -encoding binary - fileevent $fd readable [cb _read_blame $fd] + + _exec_blame $this $w_asim @asim_data \ + [list] \ + { copy/move tracking} } } ifdeleted { catch {close $fd} } -method _read_blame {fd} { +method _exec_blame {cur_w cur_d options cur_s} { + set cmd [list nice git blame] + set cmd [concat $cmd $options] + lappend cmd --incremental + if {$commit eq {}} { + lappend cmd --contents $path + } else { + lappend cmd $commit + } + lappend cmd -- $path + set fd [open "| $cmd" r] + fconfigure $fd -blocking 0 -translation lf -encoding binary + fileevent $fd readable [cb _read_blame $fd $cur_w $cur_d $cur_s] + set current_fd $fd + set blame_lines 0 + _status $this $cur_s +} + +method _read_blame {fd cur_w cur_d cur_s} { + upvar #0 $cur_d line_data + variable group_colors + + if {$fd ne $current_fd} { + catch {close $fd} + return + } + + $cur_w conf -state normal while {[gets $fd line] >= 0} { if {[regexp {^([a-z0-9]{40}) (\d+) (\d+) (\d+)$} $line line \ cmit original_line final_line line_count]} { @@ -219,127 +517,240 @@ method _read_blame {fd} { set r_orig_line $original_line set r_final_line $final_line set r_line_count $line_count - - if {[catch {set g $order($cmit)}]} { - $w_line tag conf g$cmit - $w_file tag conf g$cmit - $w_line tag raise in_sel - $w_file tag raise in_sel - $w_file tag raise sel - set order($cmit) $commit_count - incr commit_count - lappend commit_list $cmit - } } elseif {[string match {filename *} $line]} { set file [string range $line 9 end] set n $r_line_count set lno $r_final_line + set oln $r_orig_line set cmit $r_commit + if {[regexp {^0{40}$} $cmit]} { + set commit_abbr work + set commit_type curr_commit + } elseif {$cmit eq $commit} { + set commit_abbr this + set commit_type curr_commit + } else { + set commit_type prior_commit + set commit_abbr [string range $cmit 0 3] + } + + set author_abbr {} + set a_name {} + catch {set a_name $header($cmit,author)} + while {$a_name ne {}} { + if {![regexp {^([[:upper:]])} $a_name _a]} break + append author_abbr $_a + unset _a + if {![regsub \ + {^[[:upper:]][^\s]*\s+} \ + $a_name {} a_name ]} break + } + if {$author_abbr eq {}} { + set author_abbr { |} + } else { + set author_abbr [string range $author_abbr 0 3] + } + unset a_name + + set first_lno $lno + while { + $first_lno > 1 + && $cmit eq [lindex $line_data [expr {$first_lno - 1}] 0] + && $file eq [lindex $line_data [expr {$first_lno - 1}] 1] + } { + incr first_lno -1 + } + + set color {} + if {$first_lno < $lno} { + foreach g [$w_file tag names $first_lno.0] { + if {[regexp {^color[0-9]+$} $g]} { + set color $g + break + } + } + } else { + set i [lsort [concat \ + [$w_file tag names "[expr {$first_lno - 1}].0"] \ + [$w_file tag names "[expr {$lno + $n}].0"] \ + ]] + for {set g 0} {$g < [llength $group_colors]} {incr g} { + if {[lsearch -sorted -exact $i color$g] == -1} { + set color color$g + break + } + } + } + if {$color eq {}} { + set color color0 + } + while {$n > 0} { set lno_e "$lno.0 lineend + 1c" - if {[catch {set g g$line_commit($lno)}]} { - $w_load tag add annotated $lno.0 $lno_e + if {[lindex $line_data $lno] ne {}} { + set g [lindex $line_data $lno 0] + foreach i $w_columns { + $i tag remove g$g $lno.0 $lno_e + } + } + lset line_data $lno [list $cmit $file $oln] + + $cur_w delete $lno.0 "$lno.0 lineend" + if {$lno == $first_lno} { + $cur_w insert $lno.0 $commit_abbr $commit_type + } elseif {$lno == [expr {$first_lno + 1}]} { + $cur_w insert $lno.0 $author_abbr author_abbr } else { - $w_line tag remove g$g $lno.0 $lno_e - $w_file tag remove g$g $lno.0 $lno_e + $cur_w insert $lno.0 { |} } - set line_commit($lno) $cmit - set line_file($lno) $file - $w_line tag add g$cmit $lno.0 $lno_e - $w_file tag add g$cmit $lno.0 $lno_e - - if {$highlight_line == -1} { - if {[lindex [$w_file yview] 0] == 0} { - $w_file see $lno.0 - _showcommit $this $lno + foreach i $w_columns { + if {$cur_w eq $w_amov} { + for {set g 0} \ + {$g < [llength $group_colors]} \ + {incr g} { + $i tag remove color$g $lno.0 $lno_e + } + $i tag add $color $lno.0 $lno_e + } + $i tag add g$cmit $lno.0 $lno_e + } + + if {$highlight_column eq $cur_w} { + if {$highlight_line == -1 + && [lindex [$w_file yview] 0] == 0} { + $w_file see $lno.0 + set highlight_line $lno + } + if {$highlight_line == $lno} { + _showcommit $this $cur_w $lno } - } elseif {$highlight_line == $lno} { - _showcommit $this $lno } incr n -1 incr lno + incr oln incr blame_lines } - set hc $highlight_commit - if {$hc ne {} - && [expr {$order($hc) + 1}] == $order($cmit)} { - _showcommit $this $highlight_line + while { + $cmit eq [lindex $line_data $lno 0] + && $file eq [lindex $line_data $lno 1] + } { + $cur_w delete $lno.0 "$lno.0 lineend" + + if {$lno == $first_lno} { + $cur_w insert $lno.0 $commit_abbr $commit_type + } elseif {$lno == [expr {$first_lno + 1}]} { + $cur_w insert $lno.0 $author_abbr author_abbr + } else { + $cur_w insert $lno.0 { |} + } + + if {$cur_w eq $w_amov} { + foreach i $w_columns { + for {set g 0} \ + {$g < [llength $group_colors]} \ + {incr g} { + $i tag remove color$g $lno.0 $lno_e + } + $i tag add $color $lno.0 $lno_e + } + } + + incr lno } + } elseif {[regexp {^([a-z-]+) (.*)$} $line line key data]} { set header($r_commit,$key) $data } } + $cur_w conf -state disabled if {[eof $fd]} { close $fd - set status {Annotation complete.} + if {$cur_w eq $w_asim} { + _exec_blame $this $w_amov @amov_data \ + [list -M -C -C] \ + { original location} + } else { + set current_fd {} + set status {Annotation complete.} + destroy $w.status.c + } } else { - _status $this + _status $this $cur_s } } ifdeleted { catch {close $fd} } -method _status {} { +method _status {cur_s} { set have $blame_lines set total $total_lines set pdone 0 if {$total} {set pdone [expr {100 * $have / $total}]} set status [format \ - "Loading annotations... %i of %i lines annotated (%2i%%)" \ - $have $total $pdone] + "Loading%s annotations... %i of %i lines annotated (%2i%%)" \ + $cur_s $have $total $pdone] + $w.status.c coords bar 0 0 $pdone 20 } method _click {cur_w pos} { set lno [lindex [split [$cur_w index $pos] .] 0] - if {$lno eq {}} return - - set lno_e "$lno.0 + 1 line" - $w_line tag remove in_sel 0.0 end - $w_file tag remove in_sel 0.0 end - $w_line tag add in_sel $lno.0 $lno_e - $w_file tag add in_sel $lno.0 $lno_e - - _showcommit $this $lno + _showcommit $this $cur_w $lno } -variable blame_colors { - #ff4040 - #ff40ff - #4040ff +method _load_commit {cur_w cur_d pos} { + upvar #0 $cur_d line_data + set lno [lindex [split [$cur_w index $pos] .] 0] + set dat [lindex $line_data $lno] + if {$dat ne {}} { + lappend history [list \ + $commit $path \ + $highlight_column \ + $highlight_line \ + [lindex [$w_file xview] 0] \ + [lindex [$w_file yview] 0] \ + ] + set commit [lindex $dat 0] + set path [lindex $dat 1] + _load $this [list [lindex $dat 2]] + } } -method _showcommit {lno} { +method _showcommit {cur_w lno} { global repo_config - variable blame_colors + variable active_color if {$highlight_commit ne {}} { - set idx $order($highlight_commit) - set i 0 - foreach c $blame_colors { - set h [lindex $commit_list [expr {$idx - 1 + $i}]] - $w_line tag conf g$h -background white - $w_file tag conf g$h -background white - incr i + foreach i $w_columns { + $i tag conf g$highlight_commit -background {} + $i tag lower g$highlight_commit } } - $w_cmit conf -state normal - $w_cmit delete 0.0 end - if {[catch {set cmit $line_commit($lno)}]} { - set cmit {} - $w_cmit insert end "Loading annotation..." + if {$cur_w eq $w_asim} { + set dat [lindex $asim_data $lno] + set highlight_column $w_asim } else { - set idx $order($cmit) - set i 0 - foreach c $blame_colors { - set h [lindex $commit_list [expr {$idx - 1 + $i}]] - $w_line tag conf g$h -background $c - $w_file tag conf g$h -background $c - incr i + set dat [lindex $amov_data $lno] + set highlight_column $w_amov + } + + $w_cviewer conf -state normal + $w_cviewer delete 0.0 end + + if {$dat eq {}} { + set cmit {} + $w_cviewer insert end "Loading annotation..." still_loading + } else { + set cmit [lindex $dat 0] + set file [lindex $dat 1] + + foreach i $w_columns { + $i tag conf g$cmit -background $active_color + $i tag raise g$cmit } set author_name {} @@ -388,29 +799,199 @@ method _showcommit {lno} { set header($cmit,message) $msg } - $w_cmit insert end "commit $cmit -Author: $author_name $author_email $author_time -Committer: $committer_name $committer_email $committer_time -Original File: [escape_path $line_file($lno)] + $w_cviewer insert end "commit $cmit\n" header_key + $w_cviewer insert end "Author:\t" header_key + $w_cviewer insert end "$author_name $author_email" header_val + $w_cviewer insert end " $author_time\n" header_val -$msg" + $w_cviewer insert end "Committer:\t" header_key + $w_cviewer insert end "$committer_name $committer_email" header_val + $w_cviewer insert end " $committer_time\n" header_val + + if {$file ne $path} { + $w_cviewer insert end "Original File:\t" header_key + $w_cviewer insert end "[escape_path $file]\n" header_val + } + + $w_cviewer insert end "\n$msg" } - $w_cmit conf -state disabled + $w_cviewer conf -state disabled set highlight_line $lno set highlight_commit $cmit + + if {[lsearch -exact $tooltip_commit $highlight_commit] != -1} { + _hide_tooltip $this + } } method _copycommit {} { set pos @$::cursorX,$::cursorY set lno [lindex [split [$::cursorW index $pos] .] 0] - if {![catch {set commit $line_commit($lno)}]} { + set dat [lindex $amov_data $lno] + if {$dat ne {}} { clipboard clear clipboard append \ -format STRING \ -type STRING \ - -- $commit + -- [lindex $dat 0] } } +method _show_tooltip {cur_w pos} { + if {$tooltip_wm ne {}} { + _open_tooltip $this $cur_w + } elseif {$tooltip_timer eq {}} { + set tooltip_timer [after 1000 [cb _open_tooltip $cur_w]] + } +} + +method _open_tooltip {cur_w} { + set tooltip_timer {} + set pos_x [winfo pointerx $cur_w] + set pos_y [winfo pointery $cur_w] + if {[winfo containing $pos_x $pos_y] ne $cur_w} { + _hide_tooltip $this + return + } + + if {$tooltip_wm ne "$cur_w.tooltip"} { + _hide_tooltip $this + + set tooltip_wm [toplevel $cur_w.tooltip -borderwidth 1] + wm overrideredirect $tooltip_wm 1 + wm transient $tooltip_wm [winfo toplevel $cur_w] + set tooltip_t $tooltip_wm.label + text $tooltip_t \ + -takefocus 0 \ + -highlightthickness 0 \ + -relief flat \ + -borderwidth 0 \ + -wrap none \ + -background lightyellow \ + -foreground black + $tooltip_t tag conf section_header -font font_uibold + pack $tooltip_t + } else { + $tooltip_t conf -state normal + $tooltip_t delete 0.0 end + } + + set pos @[join [list \ + [expr {$pos_x - [winfo rootx $cur_w]}] \ + [expr {$pos_y - [winfo rooty $cur_w]}]] ,] + set lno [lindex [split [$cur_w index $pos] .] 0] + if {$cur_w eq $w_amov} { + set dat [lindex $amov_data $lno] + set org {} + } else { + set dat [lindex $asim_data $lno] + set org [lindex $amov_data $lno] + } + + set cmit [lindex $dat 0] + set tooltip_commit [list $cmit] + + set author_name {} + set summary {} + set author_time {} + catch {set author_name $header($cmit,author)} + catch {set summary $header($cmit,summary)} + catch {set author_time [clock format \ + $header($cmit,author-time) \ + -format {%Y-%m-%d %H:%M:%S} + ]} + + $tooltip_t insert end "commit $cmit\n" + $tooltip_t insert end "$author_name $author_time\n" + $tooltip_t insert end "$summary" + + if {$org ne {} && [lindex $org 0] ne $cmit} { + set save [$tooltip_t get 0.0 end] + $tooltip_t delete 0.0 end + + set cmit [lindex $org 0] + set file [lindex $org 1] + lappend tooltip_commit $cmit + + set author_name {} + set summary {} + set author_time {} + catch {set author_name $header($cmit,author)} + catch {set summary $header($cmit,summary)} + catch {set author_time [clock format \ + $header($cmit,author-time) \ + -format {%Y-%m-%d %H:%M:%S} + ]} + + $tooltip_t insert end "Originally By:\n" section_header + $tooltip_t insert end "commit $cmit\n" + $tooltip_t insert end "$author_name $author_time\n" + $tooltip_t insert end "$summary\n" + + if {$file ne $path} { + $tooltip_t insert end "In File: " section_header + $tooltip_t insert end "$file\n" + } + + $tooltip_t insert end "\n" + $tooltip_t insert end "Copied Or Moved Here By:\n" section_header + $tooltip_t insert end $save + } + + $tooltip_t conf -state disabled + _position_tooltip $this +} + +method _position_tooltip {} { + set max_h [lindex [split [$tooltip_t index end] .] 0] + set max_w 0 + for {set i 1} {$i <= $max_h} {incr i} { + set c [lindex [split [$tooltip_t index "$i.0 lineend"] .] 1] + if {$c > $max_w} {set max_w $c} + } + $tooltip_t conf -width $max_w -height $max_h + + set req_w [winfo reqwidth $tooltip_t] + set req_h [winfo reqheight $tooltip_t] + set pos_x [expr {[winfo pointerx .] + 5}] + set pos_y [expr {[winfo pointery .] + 10}] + + set g "${req_w}x${req_h}" + if {$pos_x >= 0} {append g +} + append g $pos_x + if {$pos_y >= 0} {append g +} + append g $pos_y + + wm geometry $tooltip_wm $g + raise $tooltip_wm +} + +method _hide_tooltip {} { + if {$tooltip_wm ne {}} { + destroy $tooltip_wm + set tooltip_wm {} + set tooltip_commit {} + } + if {$tooltip_timer ne {}} { + after cancel $tooltip_timer + set tooltip_timer {} + } +} + +method _resize {new_height} { + set diff [expr {$new_height - $old_height}] + if {$diff == 0} return + + set my [expr {[winfo height $w.file_pane] - 25}] + set o [$w.file_pane sash coord 0] + set ox [lindex $o 0] + set oy [expr {[lindex $o 1] + $diff}] + if {$oy < 0} {set oy 0} + if {$oy > $my} {set oy $my} + $w.file_pane sash place 0 $ox $oy + + set old_height $new_height +} + } diff --git a/git-gui/lib/branch.tcl b/git-gui/lib/branch.tcl index caaee5cf17..4f648b2bc7 100644 --- a/git-gui/lib/branch.tcl +++ b/git-gui/lib/branch.tcl @@ -201,12 +201,14 @@ proc do_create_branch {} { pack $w.desc -anchor nw -fill x -pady 5 -padx 5 labelframe $w.from -text {Starting Revision} - radiobutton $w.from.head_r \ - -text {Local Branch:} \ - -value head \ - -variable create_branch_revtype - eval tk_optionMenu $w.from.head_m create_branch_head $all_heads - grid $w.from.head_r $w.from.head_m -sticky w + if {$all_heads ne {}} { + radiobutton $w.from.head_r \ + -text {Local Branch:} \ + -value head \ + -variable create_branch_revtype + eval tk_optionMenu $w.from.head_m create_branch_head $all_heads + grid $w.from.head_r $w.from.head_m -sticky w + } set all_trackings [all_tracking_branches] if {$all_trackings ne {}} { set create_branch_trackinghead [lindex $all_trackings 0] diff --git a/git-gui/lib/browser.tcl b/git-gui/lib/browser.tcl index fd86b11217..3d6341bcc5 100644 --- a/git-gui/lib/browser.tcl +++ b/git-gui/lib/browser.tcl @@ -70,7 +70,6 @@ constructor new {commit} { bind $w_list break bind $w_list [list focus $w_list] - bind $w_list [list delete_this $this] set w $w_list _ls $this $browser_commit return $this diff --git a/git-gui/lib/class.tcl b/git-gui/lib/class.tcl index 88b056522a..9d298d0dcc 100644 --- a/git-gui/lib/class.tcl +++ b/git-gui/lib/class.tcl @@ -120,10 +120,21 @@ proc delete_this {{t {}}} { if {[namespace exists $t]} {namespace delete $t} } -proc make_toplevel {t w} { - upvar $t top $w pfx +proc make_toplevel {t w args} { + upvar $t top $w pfx this this + + if {[llength $args] % 2} { + error "make_toplevel topvar winvar {options}" + } + set autodelete 1 + foreach {name value} $args { + switch -exact -- $name { + -autodelete {set autodelete $value} + default {error "unsupported option $name"} + } + } + if {[winfo ismapped .]} { - upvar this this regsub -all {::} $this {__} w set top .$w set pfx $top @@ -132,6 +143,13 @@ proc make_toplevel {t w} { set top . set pfx {} } + + if {$autodelete} { + wm protocol $top WM_DELETE_WINDOW " + [list delete_this $this] + [list destroy $top] + " + } } @@ -151,4 +169,3 @@ auto_mkindex_parser::command constructor {name args} { [format { [list source [file join $dir %s]]} \ [file split $scriptFile]] "\n" } - diff --git a/git-gui/lib/console.tcl b/git-gui/lib/console.tcl index 8c112f3a89..ce25d92cac 100644 --- a/git-gui/lib/console.tcl +++ b/git-gui/lib/console.tcl @@ -17,7 +17,7 @@ constructor new {short_title long_title} { method _init {} { global M1B - make_toplevel top w + make_toplevel top w -autodelete 0 wm title $top "[appname] ([reponame]): $t_short" set console_cr 1.0 diff --git a/git-gui/lib/diff.tcl b/git-gui/lib/diff.tcl index 7e715a6865..29436b50cb 100644 --- a/git-gui/lib/diff.tcl +++ b/git-gui/lib/diff.tcl @@ -145,7 +145,7 @@ proc show_diff {path w {lno {}}} { lappend cmd -p lappend cmd --no-color - if {$repo_config(gui.diffcontext) > 0} { + if {$repo_config(gui.diffcontext) >= 0} { lappend cmd "-U$repo_config(gui.diffcontext)" } if {$w eq $ui_index} { diff --git a/git-gui/lib/merge.tcl b/git-gui/lib/merge.tcl index 24ed24b3d0..ae0389df5b 100644 --- a/git-gui/lib/merge.tcl +++ b/git-gui/lib/merge.tcl @@ -125,7 +125,8 @@ Please select fewer branches. To merge more than 15 branches, merge the branche set cons [console::new "Merge" $msg] console::exec $cons $cmd \ [namespace code [list _finish $revcnt $cons]] - bind $w {} + + wm protocol $w WM_DELETE_WINDOW {} destroy $w } @@ -250,7 +251,7 @@ proc dialog {} { bind $w <$M1B-Key-Return> $_start bind $w "grab $w; focus $w.source.l" bind $w "unlock_index;destroy $w" - bind $w unlock_index + wm protocol $w WM_DELETE_WINDOW "unlock_index;destroy $w" wm title $w "[appname] ([reponame]): Merge" tkwait window $w } diff --git a/git-gui/lib/option.tcl b/git-gui/lib/option.tcl index 17fcc65f78..b29e14e64d 100644 --- a/git-gui/lib/option.tcl +++ b/git-gui/lib/option.tcl @@ -15,6 +15,9 @@ proc save_config {} { font configure ${font}bold \ -family $global_config_new(gui.$font^^family) \ -size $global_config_new(gui.$font^^size) + font configure ${font}italic \ + -family $global_config_new(gui.$font^^family) \ + -size $global_config_new(gui.$font^^size) set global_config_new(gui.$name) [font configure $font] unset global_config_new(gui.$font^^family) unset global_config_new(gui.$font^^size) @@ -173,7 +176,7 @@ proc do_options {} { {i-1..5 merge.verbosity {Merge Verbosity}} {b gui.trustmtime {Trust File Modification Timestamps}} - {i-1..99 gui.diffcontext {Number of Diff Context Lines}} + {i-0..99 gui.diffcontext {Number of Diff Context Lines}} {t gui.newbranchtemplate {New Branch Name Template}} } { set type [lindex $option 0] diff --git a/git-send-email.perl b/git-send-email.perl index eb876f88dd..7c0c90bd21 100755 --- a/git-send-email.perl +++ b/git-send-email.perl @@ -561,7 +561,8 @@ foreach my $t (@files) { $subject = $1; } elsif (/^(Cc|From):\s+(.*)$/) { - if ($2 eq $from) { + if (unquote_rfc2047($2) eq $from) { + $from = $2; next if ($suppress_from); } elsif ($1 eq 'From') { diff --git a/grep.c b/grep.c index fcc6762302..f67d6716ea 100644 --- a/grep.c +++ b/grep.c @@ -1,5 +1,6 @@ #include "cache.h" #include "grep.h" +#include "xdiff-interface.h" void append_grep_pattern(struct grep_opt *opt, const char *pat, const char *origin, int no, enum grep_pat_token t) @@ -232,17 +233,6 @@ static void show_line(struct grep_opt *opt, const char *bol, const char *eol, printf("%.*s\n", (int)(eol-bol), bol); } -/* - * NEEDSWORK: share code with diff.c - */ -#define FIRST_FEW_BYTES 8000 -static int buffer_is_binary(const char *ptr, unsigned long size) -{ - if (FIRST_FEW_BYTES < size) - size = FIRST_FEW_BYTES; - return !!memchr(ptr, 0, size); -} - static int fixmatch(const char *pattern, char *line, regmatch_t *match) { char *hit = strstr(line, pattern); diff --git a/ident.c b/ident.c index 35dc4c6072..f62d1ec470 100644 --- a/ident.c +++ b/ident.c @@ -201,10 +201,10 @@ const char *fmt_ident(const char *name, const char *email, setup_ident(); if (!name) name = git_default_name; - if (!email) - email = getenv("EMAIL"); if (!email) email = git_default_email; + if (!email) + email = getenv("EMAIL"); if (!*name) { #ifndef NO_ETC_PASSWD diff --git a/merge-recursive.c b/merge-recursive.c index 8f72b2c079..4a82b741ae 100644 --- a/merge-recursive.c +++ b/merge-recursive.c @@ -680,6 +680,12 @@ static int ll_xdl_merge(const struct ll_merge_driver *drv_unused, { xpparam_t xpp; + if (buffer_is_binary(orig->ptr, orig->size) || + buffer_is_binary(src1->ptr, src1->size) || + buffer_is_binary(src2->ptr, src2->size)) + return error("Cannot merge binary files: %s vs. %s\n", + name1, name2); + memset(&xpp, 0, sizeof(xpp)); return xdl_merge(orig, src1, name1, diff --git a/object.c b/object.c index cfc4969ed9..16793d9958 100644 --- a/object.c +++ b/object.c @@ -160,8 +160,11 @@ struct object *parse_object_buffer(const unsigned char *sha1, enum object_type t parse_tag_buffer(tag, buffer, size); obj = &tag->object; } else { + warning("object %s has unknown type id %d\n", sha1_to_hex(sha1), type); obj = NULL; } + if (obj && obj->type == OBJ_NONE) + obj->type = type; *eaten_p = eaten; return obj; } diff --git a/revision.c b/revision.c index 0125d41136..e43c648978 100644 --- a/revision.c +++ b/revision.c @@ -1171,7 +1171,8 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, const ch opts = diff_opt_parse(&revs->diffopt, argv+i, argc-i); if (opts > 0) { - revs->diff = 1; + if (strcmp(argv[i], "-z")) + revs->diff = 1; i += opts - 1; continue; } diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh index 10e73828b0..a051c8b440 100755 --- a/t/t3200-branch.sh +++ b/t/t3200-branch.sh @@ -138,8 +138,8 @@ test_expect_success 'test tracking setup (non-wildcard, not matching)' \ git-config remote.local.fetch refs/heads/s:refs/remotes/local/s && (git-show-ref -q refs/remotes/local/master || git-fetch local) && git-branch --track my5 local/master && - ! test $(git-config branch.my5.remote) = local && - ! test $(git-config branch.my5.merge) = refs/heads/master' + ! test "$(git-config branch.my5.remote)" = local && + ! test "$(git-config branch.my5.merge)" = refs/heads/master' test_expect_success 'test tracking setup via config' \ 'git-config branch.autosetupmerge true && @@ -157,14 +157,22 @@ test_expect_success 'test overriding tracking setup via --no-track' \ (git-show-ref -q refs/remotes/local/master || git-fetch local) && git-branch --no-track my2 local/master && git-config branch.autosetupmerge false && - ! test $(git-config branch.my2.remote) = local && - ! test $(git-config branch.my2.merge) = refs/heads/master' + ! test "$(git-config branch.my2.remote)" = local && + ! test "$(git-config branch.my2.merge)" = refs/heads/master' test_expect_success 'test local tracking setup' \ 'git branch --track my6 s && test $(git-config branch.my6.remote) = . && test $(git-config branch.my6.merge) = refs/heads/s' +test_expect_success 'test tracking setup via --track but deeper' \ + 'git-config remote.local.url . && + git-config remote.local.fetch refs/heads/*:refs/remotes/local/* && + (git-show-ref -q refs/remotes/local/o/o || git-fetch local) && + git-branch --track my7 local/o/o && + test "$(git-config branch.my7.remote)" = local && + test "$(git-config branch.my7.merge)" = refs/heads/o/o' + # Keep this test last, as it changes the current branch cat >expect < 1117150200 +0000 branch: Created from master diff --git a/t/t5701-clone-local.sh b/t/t5701-clone-local.sh new file mode 100755 index 0000000000..b0933274db --- /dev/null +++ b/t/t5701-clone-local.sh @@ -0,0 +1,46 @@ +#!/bin/sh + +test_description='test local clone' +. ./test-lib.sh + +D=`pwd` + +test_expect_success 'preparing origin repository' ' + : >file && git add . && git commit -m1 && + git clone --bare . a.git && + git clone --bare . x +' + +test_expect_success 'local clone without .git suffix' ' + cd "$D" && + git clone -l -s a b && + cd b && + git fetch +' + +test_expect_success 'local clone with .git suffix' ' + cd "$D" && + git clone -l -s a.git c && + cd c && + git fetch +' + +test_expect_success 'local clone from x' ' + cd "$D" && + git clone -l -s x y && + cd y && + git fetch +' + +test_expect_success 'local clone from x.git that does not exist' ' + cd "$D" && + if git clone -l -s x.git z + then + echo "Oops, should have failed" + false + else + echo happy + fi +' + +test_done diff --git a/t/t6024-recursive-merge.sh b/t/t6024-recursive-merge.sh index a398556137..058db9cc52 100755 --- a/t/t6024-recursive-merge.sh +++ b/t/t6024-recursive-merge.sh @@ -81,4 +81,18 @@ EOF test_expect_success "virtual trees were processed" "git diff expect out" +git reset --hard +test_expect_success 'refuse to merge binary files' ' + printf "\0" > binary-file && + git add binary-file && + git commit -m binary && + git checkout G && + printf "\0\0" > binary-file && + git add binary-file && + git commit -m binary2 && + ! git merge F > merge.out 2> merge.err && + grep "Cannot merge binary files: HEAD:binary-file vs. F:binary-file" \ + merge.err +' + test_done diff --git a/t/t7201-co.sh b/t/t7201-co.sh index 5fa6a45577..ed2e9ee3c6 100755 --- a/t/t7201-co.sh +++ b/t/t7201-co.sh @@ -190,4 +190,44 @@ test_expect_success 'checkout to detach HEAD with HEAD^0' ' fi ' +test_expect_success 'checkout with ambiguous tag/branch names' ' + + git tag both side && + git branch both master && + git reset --hard && + git checkout master && + + git checkout both && + H=$(git rev-parse --verify HEAD) && + M=$(git show-ref -s --verify refs/heads/master) && + test "z$H" = "z$M" && + name=$(git symbolic-ref HEAD 2>/dev/null) && + test "z$name" = zrefs/heads/both + +' + +test_expect_success 'checkout with ambiguous tag/branch names' ' + + git reset --hard && + git checkout master && + + git tag frotz side && + git branch frotz master && + git reset --hard && + git checkout master && + + git checkout tags/frotz && + H=$(git rev-parse --verify HEAD) && + S=$(git show-ref -s --verify refs/heads/side) && + test "z$H" = "z$S" && + if name=$(git symbolic-ref HEAD 2>/dev/null) + then + echo "Bad -- should have detached" + false + else + : happy + fi + +' + test_done diff --git a/tree.c b/tree.c index e4a39aa3c3..e946dac069 100644 --- a/tree.c +++ b/tree.c @@ -173,8 +173,13 @@ static void track_tree_refs(struct tree *item) continue; if (S_ISDIR(entry.mode)) obj = &lookup_tree(entry.sha1)->object; - else + else if (S_ISREG(entry.mode) || S_ISLNK(entry.mode)) obj = &lookup_blob(entry.sha1)->object; + else { + warning("in tree %s: entry %s has bad mode %.6o\n", + sha1_to_hex(item->object.sha1), entry.path, entry.mode); + obj = lookup_unknown_object(entry.sha1); + } refs->ref[i++] = obj; } set_object_refs(&item->object, refs); diff --git a/xdiff-interface.c b/xdiff-interface.c index 10816e95a0..963bb89b08 100644 --- a/xdiff-interface.c +++ b/xdiff-interface.c @@ -122,4 +122,12 @@ int read_mmfile(mmfile_t *ptr, const char *filename) return 0; } +#define FIRST_FEW_BYTES 8000 +int buffer_is_binary(const char *ptr, unsigned long size) +{ + if (FIRST_FEW_BYTES < size) + size = FIRST_FEW_BYTES; + return !!memchr(ptr, 0, size); +} + diff --git a/xdiff-interface.h b/xdiff-interface.h index 1918808081..536f4e4d97 100644 --- a/xdiff-interface.h +++ b/xdiff-interface.h @@ -18,5 +18,6 @@ int parse_hunk_header(char *line, int len, int *ob, int *on, int *nb, int *nn); int read_mmfile(mmfile_t *ptr, const char *filename); +int buffer_is_binary(const char *ptr, unsigned long size); #endif