diff options
40 files changed, 2042 insertions, 562 deletions
diff --git a/.gitignore b/.gitignore index 27e5aeb8a0..bd49cd4627 100644 --- a/.gitignore +++ b/.gitignore @@ -159,7 +159,7 @@ common-cmds.h *.deb git-core.spec *.exe -*.[ao] +*.[aos] *.py[co] config.mak autom4te.cache diff --git a/Documentation/config.txt b/Documentation/config.txt index de408b6571..a2057d9d24 100644 --- a/Documentation/config.txt +++ b/Documentation/config.txt @@ -531,7 +531,7 @@ merge.summary:: merge.tool:: Controls which merge resolution program is used by gitlink:git-mergetool[l]. Valid values are: "kdiff3", "tkdiff", - "meld", "xxdiff", "emerge", "vimdiff", and "opendiff" + "meld", "xxdiff", "emerge", "vimdiff", "gvimdiff", and "opendiff". merge.verbosity:: Controls the amount of output shown by the recursive merge diff --git a/Documentation/diff-options.txt b/Documentation/diff-options.txt index b2a05937f9..0f07c9c4a8 100644 --- a/Documentation/diff-options.txt +++ b/Documentation/diff-options.txt @@ -86,7 +86,7 @@ Detect renames. -C:: - Detect copies as well as renames. + Detect copies as well as renames. See also `--find-copies-harder`. --diff-filter=[ACDMRTUXB*]:: Select only files that are Added (`A`), Copied (`C`), @@ -100,12 +100,13 @@ that matches other criteria, nothing is selected. --find-copies-harder:: - For performance reasons, by default, -C option finds copies only + For performance reasons, by default, `-C` option finds copies only if the original file of the copy was modified in the same changeset. This flag makes the command inspect unmodified files as candidates for the source of copy. This is a very expensive operation for large - projects, so use it with caution. + projects, so use it with caution. Giving more than one + `-C` option has the same effect. -l<num>:: -M and -C options require O(n^2) processing time where n diff --git a/Documentation/git-cherry.txt b/Documentation/git-cherry.txt index b62c97097f..e6943822cd 100644 --- a/Documentation/git-cherry.txt +++ b/Documentation/git-cherry.txt @@ -53,6 +53,9 @@ OPTIONS <head>:: Working branch; defaults to HEAD. +<limit>:: + Do not report commits up to (and including) limit. + Author ------ Written by Junio C Hamano <junkio@cox.net> diff --git a/Documentation/git-cvsimport.txt b/Documentation/git-cvsimport.txt index 3985e0164b..fdd7ec7edd 100644 --- a/Documentation/git-cvsimport.txt +++ b/Documentation/git-cvsimport.txt @@ -13,7 +13,7 @@ SYNOPSIS [-A <author-conv-file>] [-p <options-for-cvsps>] [-P <file>] [-C <git_repository>] [-z <fuzz>] [-i] [-k] [-u] [-s <subst>] [-a] [-m] [-M <regex>] [-S <regex>] [-L <commitlimit>] - [<CVS_module>] + [-r <remote>] [<CVS_module>] DESCRIPTION @@ -25,10 +25,12 @@ Splitting the CVS log into patch sets is done by 'cvsps'. At least version 2.1 is required. You should *never* do any work of your own on the branches that are -created by git-cvsimport. The initial import will create and populate a +created by git-cvsimport. By default initial import will create and populate a "master" branch from the CVS repository's main branch which you're free to work with; after that, you need to 'git merge' incremental imports, or -any CVS branches, yourself. +any CVS branches, yourself. It is advisable to specify a named remote via +-r to separate and protect the incoming branches. + OPTIONS ------- @@ -51,10 +53,19 @@ OPTIONS The git repository to import to. If the directory doesn't exist, it will be created. Default is the current directory. +-r <remote>:: + The git remote to import this CVS repository into. + Moves all CVS branches into remotes/<remote>/<branch> + akin to the git-clone --use-separate-remote option. + -o <branch-for-HEAD>:: - The 'HEAD' branch from CVS is imported to the 'origin' branch within - the git repository, as 'HEAD' already has a special meaning for git. - Use this option if you want to import into a different branch. + When no remote is specified (via -r) the 'HEAD' branch + from CVS is imported to the 'origin' branch within the git + repository, as 'HEAD' already has a special meaning for git. + When a remote is specified the 'HEAD' branch is named + remotes/<remote>/master mirroring git-clone behaviour. + Use this option if you want to import into a different + branch. + Use '-o master' for continuing an import that was initially done by the old cvs2git tool. diff --git a/Documentation/git-mergetool.txt b/Documentation/git-mergetool.txt index b89c51c65b..6c32c6d18e 100644 --- a/Documentation/git-mergetool.txt +++ b/Documentation/git-mergetool.txt @@ -25,7 +25,7 @@ OPTIONS -t or --tool=<tool>:: Use the merge resolution program specified by <tool>. Valid merge tools are: - kdiff3, tkdiff, meld, xxdiff, emerge, vimdiff, and opendiff + kdiff3, tkdiff, meld, xxdiff, emerge, vimdiff, gvimdiff, and opendiff + If a merge resolution program is not specified, 'git mergetool' will use the configuration variable merge.tool. If the diff --git a/Documentation/git-mktag.txt b/Documentation/git-mktag.txt index 0ac3be10c7..ea7a75234a 100644 --- a/Documentation/git-mktag.txt +++ b/Documentation/git-mktag.txt @@ -19,18 +19,18 @@ The output is the new tag's <object> identifier. Tag Format ---------- -A tag signature file has a very simple fixed format: three lines of +A tag signature file has a very simple fixed format: four lines of object <sha1> type <typename> tag <tagname> + tagger <tagger> -followed by some 'optional' free-form signature that git itself -doesn't care about, but that can be verified with gpg or similar. - -The size of the full object is artificially limited to 8kB. (Just -because I'm a lazy bastard, and if you can't fit a signature in that -size, you're doing something wrong) +followed by some 'optional' free-form message (some tags created +by older git may not have `tagger` line). The message, when +exists, is separated by a blank line from the header. The +message part may contain a signature that git itself doesn't +care about, but that can be verified with gpg. Author diff --git a/Documentation/git-submodule.txt b/Documentation/git-submodule.txt index cb0424f77b..f8fb80f18b 100644 --- a/Documentation/git-submodule.txt +++ b/Documentation/git-submodule.txt @@ -23,15 +23,15 @@ status:: repository. This command is the default command for git-submodule. init:: - Initialize the submodules, i.e. clone the git repositories specified - in the .gitmodules file and checkout the submodule commits specified - in the index of the containing repository. This will make the - submodules HEAD be detached. + Initialize the submodules, i.e. register in .git/config each submodule + path and url found in .gitmodules. The key used in git/config is + `submodule.$path.url`. This command does not alter existing information + in .git/config. update:: - Update the initialized submodules, i.e. checkout the submodule commits - specified in the index of the containing repository. This will make - the submodules HEAD be detached. + Update the registered submodules, i.e. clone missing submodules and + checkout the commit specified in the index of the containing repository. + This will make the submodules HEAD be detached. OPTIONS @@ -50,7 +50,7 @@ OPTIONS FILES ----- -When cloning submodules, a .gitmodules file in the top-level directory +When initializing submodules, a .gitmodules file in the top-level directory of the containing repository is used to find the url of each submodule. This file should be formatted in the same way as $GIR_DIR/config. The key to each submodule url is "module.$path.url". 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 957cd00761..0bfa21b3d2 100644 --- a/Documentation/user-manual.txt +++ b/Documentation/user-manual.txt @@ -27,7 +27,7 @@ $ man git-clone See also <<git-quick-start>> for a brief overview of git commands, without any explanation. -Also, see <<todo>> for ways that you can help make this manual more +Finally, see <<todo>> 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 @@ -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 289c8067b5..06c360b267 100755 --- a/GIT-VERSION-GEN +++ b/GIT-VERSION-GEN @@ -43,3 +43,5 @@ test "$VN" = "$VC" || { echo >&2 "GIT_VERSION = $VN" echo "GIT_VERSION = $VN" >$GVF } + + @@ -144,8 +144,8 @@ STRIP ?= strip prefix = $(HOME) bindir = $(prefix)/bin gitexecdir = $(bindir) -sharedir = $(prefix)/share/ -template_dir = $(sharedir)/git-core/templates/ +sharedir = $(prefix)/share +template_dir = $(sharedir)/git-core/templates ifeq ($(prefix),/usr) sysconfdir = /etc else @@ -845,6 +845,8 @@ git$X git.spec \ %.o: %.c GIT-CFLAGS $(QUIET_CC)$(CC) -o $*.o -c $(ALL_CFLAGS) $< +%.s: %.c GIT-CFLAGS + $(QUIET_CC)$(CC) -S $(ALL_CFLAGS) $< %.o: %.S $(QUIET_CC)$(CC) -o $*.o -c $(ALL_CFLAGS) $< diff --git a/builtin-init-db.c b/builtin-init-db.c index 4df9fd0fad..0be2d2ef6e 100644 --- a/builtin-init-db.c +++ b/builtin-init-db.c @@ -7,7 +7,7 @@ #include "builtin.h" #ifndef DEFAULT_GIT_TEMPLATE_DIR -#define DEFAULT_GIT_TEMPLATE_DIR "/usr/share/git-core/templates/" +#define DEFAULT_GIT_TEMPLATE_DIR "/usr/share/git-core/templates" #endif #ifdef NO_TRUSTABLE_FILEMODE @@ -225,6 +225,21 @@ extern void verify_non_filename(const char *prefix, const char *name); #define alloc_nr(x) (((x)+16)*3/2) +/* + * Realloc the buffer pointed at by variable 'x' so that it can hold + * at least 'nr' entries; the number of entries currently allocated + * is 'alloc', using the standard growing factor alloc_nr() macro. + * + * DO NOT USE any expression with side-effect for 'x' or 'alloc'. + */ +#define ALLOC_GROW(x, nr, alloc) \ + do { \ + if ((nr) >= alloc) { \ + alloc = alloc_nr(alloc); \ + x = xrealloc((x), alloc * sizeof(*(x))); \ + } \ + } while(0) + /* Initialize and use the cache information */ extern int read_index(struct index_state *); extern int read_index_from(struct index_state *, const char *path); @@ -2201,6 +2201,8 @@ int diff_opt_parse(struct diff_options *options, const char **av, int ac) options->detect_rename = DIFF_DETECT_RENAME; } else if (!prefixcmp(arg, "-C")) { + if (options->detect_rename == DIFF_DETECT_COPY) + options->find_copies_harder = 1; if ((options->rename_score = diff_scoreopt_parse(arg)) == -1) return -1; @@ -271,27 +271,26 @@ int excluded(struct dir_struct *dir, const char *pathname) return 0; } -struct dir_entry *dir_add_name(struct dir_struct *dir, const char *pathname, int len) -{ +static struct dir_entry *dir_entry_new(const char *pathname, int len) { struct dir_entry *ent; - if (cache_name_pos(pathname, len) >= 0) - return NULL; - - if (dir->nr == dir->alloc) { - int alloc = alloc_nr(dir->alloc); - dir->alloc = alloc; - dir->entries = xrealloc(dir->entries, alloc*sizeof(ent)); - } ent = xmalloc(sizeof(*ent) + len + 1); ent->ignored = ent->ignored_dir = 0; ent->len = len; memcpy(ent->name, pathname, len); ent->name[len] = 0; - dir->entries[dir->nr++] = ent; return ent; } +struct dir_entry *dir_add_name(struct dir_struct *dir, const char *pathname, int len) +{ + if (cache_name_pos(pathname, len) >= 0) + return NULL; + + ALLOC_GROW(dir->entries, dir->nr, dir->alloc); + return dir->entries[dir->nr++] = dir_entry_new(pathname, len); +} + enum exist_status { index_nonexistent = 0, index_directory, diff --git a/git-cvsimport.perl b/git-cvsimport.perl index 3225a2a25d..433b7fd324 100755 --- a/git-cvsimport.perl +++ b/git-cvsimport.perl @@ -29,7 +29,7 @@ use IPC::Open2; $SIG{'PIPE'}="IGNORE"; $ENV{'TZ'}="UTC"; -our ($opt_h,$opt_o,$opt_v,$opt_k,$opt_u,$opt_d,$opt_p,$opt_C,$opt_z,$opt_i,$opt_P, $opt_s,$opt_m,$opt_M,$opt_A,$opt_S,$opt_L, $opt_a); +our ($opt_h,$opt_o,$opt_v,$opt_k,$opt_u,$opt_d,$opt_p,$opt_C,$opt_z,$opt_i,$opt_P, $opt_s,$opt_m,$opt_M,$opt_A,$opt_S,$opt_L, $opt_a, $opt_r); my (%conv_author_name, %conv_author_email); sub usage(;$) { @@ -40,7 +40,7 @@ Usage: ${\basename $0} # fetch/update GIT from CVS [-o branch-for-HEAD] [-h] [-v] [-d CVSROOT] [-A author-conv-file] [-p opts-for-cvsps] [-P file] [-C GIT_repository] [-z fuzz] [-i] [-k] [-u] [-s subst] [-a] [-m] [-M regex] [-S regex] [-L commitlimit] - [CVS_module] + [-r remote] [CVS_module] END exit(1); } @@ -114,7 +114,7 @@ sub read_repo_config { } } -my $opts = "haivmkuo:d:p:C:z:s:M:P:A:S:L:"; +my $opts = "haivmkuo:d:p:r:C:z:s:M:P:A:S:L:"; read_repo_config($opts); getopts($opts) or usage(); usage if $opt_h; @@ -134,13 +134,21 @@ if ($opt_d) { } else { usage("CVSROOT needs to be set"); } -$opt_o ||= "origin"; $opt_s ||= "-"; $opt_a ||= 0; my $git_tree = $opt_C; $git_tree ||= "."; +my $remote; +if (defined $opt_r) { + $remote = 'refs/remotes/' . $opt_r; + $opt_o ||= "master"; +} else { + $opt_o ||= "origin"; + $remote = 'refs/heads'; +} + my $cvs_tree; if ($#ARGV == 0) { $cvs_tree = $ARGV[0]; @@ -522,7 +530,7 @@ sub get_headref ($$) { my $name = shift; my $git_dir = shift; - my $f = "$git_dir/refs/heads/$name"; + my $f = "$git_dir/$remote/$name"; if (open(my $fh, $f)) { chomp(my $r = <$fh>); is_sha1($r) or die "Cannot get head id for $name ($r): $!"; @@ -573,12 +581,12 @@ unless (-d $git_dir) { # Get the last import timestamps my $fmt = '($ref, $author) = (%(refname), %(author));'; - open(H, "git-for-each-ref --perl --format='$fmt' refs/heads |") or + open(H, "git-for-each-ref --perl --format='$fmt' $remote |") or die "Cannot run git-for-each-ref: $!\n"; while (defined(my $entry = <H>)) { my ($ref, $author); eval($entry) || die "cannot eval refs list: $@"; - my ($head) = ($ref =~ m|^refs/heads/(.*)|); + my ($head) = ($ref =~ m|^$remote/(.*)|); $author =~ /^.*\s(\d+)\s[-+]\d{4}$/; $branch_date{$head} = $1; } @@ -701,9 +709,9 @@ sub commit { $index{$branch} = tmpnam(); $ENV{GIT_INDEX_FILE} = $index{$branch}; if ($ancestor) { - system("git-read-tree", $ancestor); + system("git-read-tree", "$remote/$ancestor"); } else { - system("git-read-tree", $branch); + system("git-read-tree", "$remote/$branch"); } die "read-tree failed: $?\n" if $?; } @@ -762,7 +770,7 @@ sub commit { waitpid($pid,0); die "Error running git-commit-tree: $?\n" if $?; - system("git-update-ref refs/heads/$branch $cid") == 0 + system("git-update-ref $remote/$branch $cid") == 0 or die "Cannot write branch $branch for update: $!\n"; if ($tag) { @@ -772,30 +780,8 @@ sub commit { $xtag =~ tr/_/\./ if ( $opt_u ); $xtag =~ s/[\/]/$opt_s/g; - my $pid = open2($in, $out, 'git-mktag'); - print $out "object $cid\n". - "type commit\n". - "tag $xtag\n". - "tagger $author_name <$author_email>\n" - or die "Cannot create tag object $xtag: $!\n"; - close($out) - or die "Cannot create tag object $xtag: $!\n"; - - my $tagobj = <$in>; - chomp $tagobj; - - if ( !close($in) or waitpid($pid, 0) != $pid or - $? != 0 or $tagobj !~ /^[0123456789abcdef]{40}$/ ) { - die "Cannot create tag object $xtag: $!\n"; - } - - - open(C,">$git_dir/refs/tags/$xtag") + system('git-tag', $xtag, $cid) == 0 or die "Cannot create tag $xtag: $!\n"; - print C "$tagobj\n" - or die "Cannot write tag $xtag: $!\n"; - close(C) - or die "Cannot write tag $xtag: $!\n"; print "Created tag '$xtag' on '$branch'\n" if $opt_v; } @@ -883,12 +869,12 @@ while (<CVS>) { print STDERR "Branch $branch erroneously stems from itself -- changed ancestor to $opt_o\n"; $ancestor = $opt_o; } - if (-f "$git_dir/refs/heads/$branch") { + if (-f "$git_dir/$remote/$branch") { print STDERR "Branch $branch already exists!\n"; $state=11; next; } - unless (open(H,"$git_dir/refs/heads/$ancestor")) { + unless (open(H,"$git_dir/$remote/$ancestor")) { print STDERR "Branch $ancestor does not exist!\n"; $ignorebranch{$branch} = 1; $state=11; @@ -896,7 +882,7 @@ while (<CVS>) { } chomp(my $id = <H>); close(H); - unless (open(H,"> $git_dir/refs/heads/$branch")) { + unless (open(H,"> $git_dir/$remote/$branch")) { print STDERR "Could not create branch $branch: $!\n"; $ignorebranch{$branch} = 1; $state=11; @@ -1010,14 +996,16 @@ if ($orig_branch) { die "Fast-forward update failed: $?\n" if $?; } else { - system(qw(git-merge cvsimport HEAD), "refs/heads/$opt_o"); + system(qw(git-merge cvsimport HEAD), "$remote/$opt_o"); die "Could not merge $opt_o into the current branch.\n" if $?; } } else { $orig_branch = "master"; print "DONE; creating $orig_branch branch\n" if $opt_v; - system("git-update-ref", "refs/heads/master", "refs/heads/$opt_o") + system("git-update-ref", "refs/heads/master", "$remote/$opt_o") unless -f "$git_dir/refs/heads/master"; + system("git-symbolic-ref", "$remote/HEAD", "$remote/$opt_o") + if ($opt_r && $opt_o ne 'HEAD'); system('git-update-ref', 'HEAD', "$orig_branch"); unless ($opt_i) { system('git checkout'); diff --git a/git-gui/GIT-VERSION-GEN b/git-gui/GIT-VERSION-GEN index eee495a986..9770b0bc27 100755 --- a/git-gui/GIT-VERSION-GEN +++ b/git-gui/GIT-VERSION-GEN @@ -1,7 +1,7 @@ #!/bin/sh GVF=GIT-VERSION-FILE -DEF_VER=0.7.GITGUI +DEF_VER=0.8.GITGUI LF=' ' diff --git a/git-gui/git-gui.sh b/git-gui/git-gui.sh index dba585111c..3237f3d596 100755 --- a/git-gui/git-gui.sh +++ b/git-gui/git-gui.sh @@ -22,6 +22,40 @@ 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 +} + +###################################################################### +## +## enable verbose loading? + +if {![catch {set _verbose $env(GITGUI_VERBOSE)}]} { + unset _verbose + rename auto_load real__auto_load + proc auto_load {name args} { + puts stderr "auto_load $name" + return [uplevel 1 real__auto_load $name $args] + } + rename source real__source + proc source {name} { + puts stderr "source $name" + uplevel 1 real__source $name + } +} + +###################################################################### +## ## configure our library set oguilib {@@GITGUI_LIBDIR@@} @@ -32,26 +66,33 @@ if {$oguirel eq {1}} { } elseif {[string match @@* $oguirel]} { set oguilib [file join [file dirname [file normalize $argv0]] lib] } + set idx [file join $oguilib tclIndex] -catch { - set fd [open $idx r] - if {[gets $fd] eq {# Autogenerated by git-gui Makefile}} { - set idx [list] - while {[gets $fd n] >= 0} { - if {$n ne {} && ![string match #* $n]} { - lappend idx $n - } +if {[catch {set fd [open $idx r]} err]} { + catch {wm withdraw .} + tk_messageBox \ + -icon error \ + -type ok \ + -title "git-gui: fatal error" \ + -message $err + exit 1 +} +if {[gets $fd] eq {# Autogenerated by git-gui Makefile}} { + set idx [list] + while {[gets $fd n] >= 0} { + if {$n ne {} && ![string match #* $n]} { + lappend idx $n } - } else { - set idx {} } - close $fd +} else { + set idx {} } +close $fd + if {$idx ne {}} { set loaded [list] foreach p $idx { if {[lsearch -exact $loaded $p] >= 0} continue - puts $p source [file join $oguilib $p] lappend loaded $p } @@ -59,21 +100,7 @@ if {$idx ne {}} { } else { set auto_path [concat [list $oguilib] $auto_path] } -unset -nocomplain oguilib oguirel idx fd - -if {![catch {set _verbose $env(GITGUI_VERBOSE)}]} { - unset _verbose - rename auto_load real__auto_load - proc auto_load {name args} { - puts stderr "auto_load $name" - return [uplevel 1 real__auto_load $name $args] - } - rename source real__source - proc source {name} { - puts stderr "source $name" - uplevel 1 real__source $name - } -} +unset -nocomplain oguirel idx fd ###################################################################### ## @@ -189,6 +216,15 @@ proc is_config_true {name} { } } +proc get_config {name} { + global repo_config + if {[catch {set v $repo_config($name)}]} { + return {} + } else { + return $v + } +} + proc load_config {include_global} { global repo_config global_config default_config @@ -242,6 +278,17 @@ proc git {args} { return [eval exec git $args] } +proc current-branch {} { + set ref {} + set fd [open [gitdir HEAD] r] + if {[gets $fd ref] <16 + || ![regsub {^ref: refs/heads/} $ref {} ref]} { + set ref {} + } + close $fd + return $ref +} + auto_load tk_optionMenu rename tk_optionMenu real__tkOptionMenu proc tk_optionMenu {w varName args} { @@ -390,15 +437,7 @@ proc repository_state {ctvar hdvar mhvar} { set mh [list] - if {[catch {set current_branch [git symbolic-ref HEAD]}]} { - set current_branch {} - } else { - regsub ^refs/((heads|tags|remotes)/)? \ - $current_branch \ - {} \ - current_branch - } - + set current_branch [current-branch] if {[catch {set hd [git rev-parse --verify HEAD]}]} { set hd {} set ct initial @@ -455,7 +494,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 +1048,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,16 +1241,22 @@ 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 +if {[is_Windows] || [is_MacOSX]} { + option add *Menu.tearOff 0 +} + if {[is_MacOSX]} { set M1B M1 set M1T Cmd @@ -1233,16 +1280,20 @@ 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 } } +set default_config(merge.diffstat) true set default_config(merge.summary) false set default_config(merge.verbosity) 2 set default_config(user.name) {} set default_config(user.email) {} +set default_config(gui.pruneduringfetch) false set default_config(gui.trustmtime) false set default_config(gui.diffcontext) 5 set default_config(gui.newbranchtemplate) {} @@ -1404,6 +1455,11 @@ if {[is_enabled branch]} { lappend disable_on_lock [list .mbar.branch entryconf \ [.mbar.branch index last] -state] + .mbar.branch add command -label {Rename...} \ + -command branch_rename::dialog + lappend disable_on_lock [list .mbar.branch entryconf \ + [.mbar.branch index last] -state] + .mbar.branch add command -label {Delete...} \ -command do_delete_branch lappend disable_on_lock [list .mbar.branch entryconf \ @@ -1501,6 +1557,8 @@ if {[is_enabled transport]} { menu .mbar.push .mbar.push add command -label {Push...} \ -command do_push_anywhere + .mbar.push add command -label {Delete...} \ + -command remote_branch_delete::dialog } if {[is_MacOSX]} { @@ -1600,7 +1658,7 @@ unset browser doc_path doc_url # -- Standard bindings # -bind . <Destroy> 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]} @@ -1618,14 +1676,8 @@ switch -- $subcommand { browser { set subcommand_args {rev?} switch [llength $argv] { - 0 { - set current_branch [git symbolic-ref HEAD] - regsub ^refs/((heads|tags|remotes)/)? \ - $current_branch {} current_branch - } - 1 { - set current_branch [lindex $argv 0] - } + 0 { set current_branch [current-branch] } + 1 { set current_branch [lindex $argv 0] } default usage } browser::new $current_branch @@ -1658,9 +1710,7 @@ blame { unset is_path if {$head eq {}} { - set current_branch [git symbolic-ref HEAD] - regsub ^refs/((heads|tags|remotes)/)? \ - $current_branch {} current_branch + set current_branch [current-branch] } else { set current_branch $head } @@ -1715,7 +1765,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 +1785,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 +1802,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 +1961,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 +2086,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} + +# Persistant data (survives loads) +# +field history {}; # viewer history: {commit path} +field header ; # array commit,key -> header field + +# 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 +} -field w -field w_line -field w_load -field w_file -field w_cmit -field status +# Current blame data; cleared/reset on each load +# +field commit ; # input commit to blame +field path ; # input filename to view in $commit -field highlight_line -1 ; # current line selected -field highlight_commit {} ; # sha1 of commit selected +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 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 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 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 - - frame $w.out - text $w.out.loaded_t \ + -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 <Button-1> " + 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 + + 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 - - text $w.out.file_t \ + $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 \ + <Button-1> \ + "[cb _load_commit $w_amov @amov_data @%x,%y];break" + + 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 \ + <Button-1> \ + "[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 - - label $w.status \ - -textvariable @status \ - -anchor w \ - -justify left \ - -borderwidth 1 \ - -relief sunken - pack $w.status -side bottom -fill x - - frame $w.cm - text $w.cm.t \ + set w_columns [list $w_amov $w_asim $w_line $w_file] + + 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 + + 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 [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 <Button-1> "[cb _click $i @%x,%y]; focus $i" + foreach i $w_columns { + for {set g 0} {$g < [llength $group_colors]} {incr g} { + $i tag conf color$g -background [lindex $group_colors $g] + } + + $i conf -cursor $cursor_ptr + $i conf -yscrollcommand [list many2scrollbar \ + $w_columns yview $w.file_pane.out.sby] + bind $i <Button-1> " + [cb _hide_tooltip] + [cb _click $i @%x,%y] + focus $i + " + bind $i <Any-Motion> [cb _show_tooltip $i @%x,%y] + bind $i <Any-Enter> [cb _hide_tooltip] + bind $i <Any-Leave> [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 <Key-Up> {catch {%W yview scroll -1 units};break} bind $i <Key-Down> {catch {%W yview scroll 1 units};break} bind $i <Key-Left> {catch {%W xview scroll -1 units};break} @@ -166,10 +287,95 @@ constructor new {i_commit i_path} { bind $i <Control-Key-f> {catch {%W yview scroll 1 pages};break} } - bind $w.cm.t <Button-1> [list focus $w.cm.t] + bind $w_cviewer <Button-1> [list focus $w_cviewer] bind $top <Visibility> [list focus $top] - bind $top <Destroy> [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 <Configure> \ + "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 + 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_line == -1} { - if {[lindex [$w_file yview] 0] == 0} { + if {$highlight_column eq $cur_w} { + if {$highlight_line == -1 + && [lindex [$w_file yview] 0] == 0} { $w_file see $lno.0 - _showcommit $this $lno + 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)}]} { + if {$cur_w eq $w_asim} { + set dat [lindex $asim_data $lno] + set highlight_column $w_asim + } else { + 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_cmit insert end "Loading annotation..." + $w_cviewer insert end "Loading annotation..." still_loading } 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 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 + + $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 -$msg" + 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/branch_rename.tcl b/git-gui/lib/branch_rename.tcl new file mode 100644 index 0000000000..405101637f --- /dev/null +++ b/git-gui/lib/branch_rename.tcl @@ -0,0 +1,136 @@ +# git-gui branch rename support +# Copyright (C) 2007 Shawn Pearce + +class branch_rename { + +field w +field oldname +field newname + +constructor dialog {} { + global all_heads current_branch + + make_toplevel top w + wm title $top "[appname] ([reponame]): Rename Branch" + if {$top ne {.}} { + wm geometry $top "+[winfo rootx .]+[winfo rooty .]" + } + + set oldname $current_branch + set newname [get_config gui.newbranchtemplate] + + label $w.header -text {Rename Branch} -font font_uibold + pack $w.header -side top -fill x + + frame $w.buttons + button $w.buttons.rename -text Rename \ + -default active \ + -command [cb _rename] + pack $w.buttons.rename -side right + button $w.buttons.cancel -text {Cancel} \ + -command [list destroy $w] + pack $w.buttons.cancel -side right -padx 5 + pack $w.buttons -side bottom -fill x -pady 10 -padx 10 + + frame $w.rename + label $w.rename.oldname_l -text {Branch:} + eval tk_optionMenu $w.rename.oldname_m @oldname $all_heads + + label $w.rename.newname_l -text {New Name:} + entry $w.rename.newname_t \ + -borderwidth 1 \ + -relief sunken \ + -width 40 \ + -textvariable @newname \ + -validate key \ + -validatecommand { + if {%d == 1 && [regexp {[~^:?*\[\0- ]} %S]} {return 0} + return 1 + } + + grid $w.rename.oldname_l $w.rename.oldname_m -sticky w -padx {0 5} + grid $w.rename.newname_l $w.rename.newname_t -sticky we -padx {0 5} + grid columnconfigure $w.rename 1 -weight 1 + pack $w.rename -anchor nw -fill x -pady 5 -padx 5 + + bind $w <Key-Return> [cb _rename] + bind $w <Key-Escape> [list destroy $w] + bind $w <Visibility> " + grab $w + $w.rename.newname_t icursor end + focus $w.rename.newname_t + " + tkwait window $w +} + +method _rename {} { + global all_heads current_branch + + if {$oldname eq {}} { + tk_messageBox \ + -icon error \ + -type ok \ + -title [wm title $w] \ + -parent $w \ + -message "Please select a branch to rename." + focus $w.rename.oldname_m + return + } + if {$newname eq {} + || $newname eq [get_config gui.newbranchtemplate]} { + tk_messageBox \ + -icon error \ + -type ok \ + -title [wm title $w] \ + -parent $w \ + -message "Please supply a branch name." + focus $w.rename.newname_t + return + } + if {![catch {git show-ref --verify -- "refs/heads/$newname"}]} { + tk_messageBox \ + -icon error \ + -type ok \ + -title [wm title $w] \ + -parent $w \ + -message "Branch '$newname' already exists." + focus $w.rename.newname_t + return + } + if {[catch {git check-ref-format "heads/$newname"}]} { + tk_messageBox \ + -icon error \ + -type ok \ + -title [wm title $w] \ + -parent $w \ + -message "We do not like '$newname' as a branch name." + focus $w.rename.newname_t + return + } + + if {[catch {git branch -m $oldname $newname} err]} { + tk_messageBox \ + -icon error \ + -type ok \ + -title [wm title $w] \ + -parent $w \ + -message "Failed to rename '$oldname'.\n\n$err" + return + } + + set oldidx [lsearch -exact -sorted $all_heads $oldname] + if {$oldidx >= 0} { + set all_heads [lreplace $all_heads $oldidx $oldidx] + } + lappend all_heads $newname + set all_heads [lsort $all_heads] + populate_branch_menu + + if {$current_branch eq $oldname} { + set current_branch $newname + } + + destroy $w +} + +} 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 <Right> break bind $w_list <Visibility> [list focus $w_list] - bind $w_list <Destroy> [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 72494c1a1e..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] + " + } } 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 <Destroy> {} + + wm protocol $w WM_DELETE_WINDOW {} destroy $w } @@ -250,7 +251,7 @@ proc dialog {} { bind $w <$M1B-Key-Return> $_start bind $w <Visibility> "grab $w; focus $w.source.l" bind $w <Key-Escape> "unlock_index;destroy $w" - bind $w <Destroy> 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..ae19a8f9cf 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) @@ -52,7 +55,7 @@ proc save_config {} { } proc do_about {} { - global appvers copyright + global appvers copyright oguilib global tcl_patchLevel tk_patchLevel set w .about_dialog @@ -91,6 +94,10 @@ $copyright" \ append v ", Tk version $tk_patchLevel" } + set d {} + append d "git exec dir: [gitexec]\n" + append d "git-gui lib: $oguilib" + label $w.vers \ -text $v \ -padx 5 -pady 5 \ @@ -100,6 +107,15 @@ $copyright" \ -relief solid pack $w.vers -side top -fill x -padx 5 -pady 5 + label $w.dirs \ + -text $d \ + -padx 5 -pady 5 \ + -justify left \ + -anchor w \ + -borderwidth 1 \ + -relief solid + pack $w.dirs -side top -fill x -padx 5 -pady 5 + menu $w.ctxm -tearoff 0 $w.ctxm add command \ -label {Copy} \ @@ -171,9 +187,11 @@ proc do_options {} { {b merge.summary {Summarize Merge Commits}} {i-1..5 merge.verbosity {Merge Verbosity}} + {b merge.diffstat {Show Diffstat After Merge}} {b gui.trustmtime {Trust File Modification Timestamps}} - {i-1..99 gui.diffcontext {Number of Diff Context Lines}} + {b gui.pruneduringfetch {Prune Tracking Branches During Fetch}} + {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-gui/lib/remote.tcl b/git-gui/lib/remote.tcl index 99f353ed7d..b54824ab72 100644 --- a/git-gui/lib/remote.tcl +++ b/git-gui/lib/remote.tcl @@ -95,6 +95,7 @@ proc populate_fetch_menu {} { global all_remotes repo_config set m .mbar.fetch + set prune_list [list] foreach r $all_remotes { set enable 0 if {![catch {set a $repo_config(remote.$r.url)}]} { @@ -115,11 +116,21 @@ proc populate_fetch_menu {} { } if {$enable} { + lappend prune_list $r $m add command \ -label "Fetch from $r..." \ -command [list fetch_from $r] } } + + if {$prune_list ne {}} { + $m add separator + } + foreach r $prune_list { + $m add command \ + -label "Prune from $r..." \ + -command [list prune_from $r] + } } proc populate_push_menu {} { diff --git a/git-gui/lib/remote_branch_delete.tcl b/git-gui/lib/remote_branch_delete.tcl new file mode 100644 index 0000000000..b83e1b6315 --- /dev/null +++ b/git-gui/lib/remote_branch_delete.tcl @@ -0,0 +1,347 @@ +# git-gui remote branch deleting support +# Copyright (C) 2007 Shawn Pearce + +class remote_branch_delete { + +field w +field head_m + +field urltype {url} +field remote {} +field url {} + +field checktype {head} +field check_head {} + +field status {} +field idle_id {} +field full_list {} +field head_list {} +field active_ls {} +field head_cache +field full_cache +field cached + +constructor dialog {} { + global all_remotes M1B + + make_toplevel top w + wm title $top "[appname] ([reponame]): Delete Remote Branch" + if {$top ne {.}} { + wm geometry $top "+[winfo rootx .]+[winfo rooty .]" + } + + label $w.header -text {Delete Remote Branch} -font font_uibold + pack $w.header -side top -fill x + + frame $w.buttons + button $w.buttons.delete -text Delete \ + -default active \ + -command [cb _delete] + pack $w.buttons.delete -side right + button $w.buttons.cancel -text {Cancel} \ + -command [list destroy $w] + pack $w.buttons.cancel -side right -padx 5 + pack $w.buttons -side bottom -fill x -pady 10 -padx 10 + + labelframe $w.dest -text {From Repository} + if {$all_remotes ne {}} { + radiobutton $w.dest.remote_r \ + -text {Remote:} \ + -value remote \ + -variable @urltype + eval tk_optionMenu $w.dest.remote_m @remote $all_remotes + grid $w.dest.remote_r $w.dest.remote_m -sticky w + if {[lsearch -sorted -exact $all_remotes origin] != -1} { + set remote origin + } else { + set remote [lindex $all_remotes 0] + } + set urltype remote + trace add variable @remote write [cb _write_remote] + } else { + set urltype url + } + radiobutton $w.dest.url_r \ + -text {Arbitrary URL:} \ + -value url \ + -variable @urltype + entry $w.dest.url_t \ + -borderwidth 1 \ + -relief sunken \ + -width 50 \ + -textvariable @url \ + -validate key \ + -validatecommand { + if {%d == 1 && [regexp {\s} %S]} {return 0} + return 1 + } + trace add variable @url write [cb _write_url] + grid $w.dest.url_r $w.dest.url_t -sticky we -padx {0 5} + grid columnconfigure $w.dest 1 -weight 1 + pack $w.dest -anchor nw -fill x -pady 5 -padx 5 + + labelframe $w.heads -text {Branches} + listbox $w.heads.l \ + -height 10 \ + -width 70 \ + -listvariable @head_list \ + -selectmode extended \ + -yscrollcommand [list $w.heads.sby set] + scrollbar $w.heads.sby -command [list $w.heads.l yview] + + frame $w.heads.footer + label $w.heads.footer.status \ + -textvariable @status \ + -anchor w \ + -justify left + button $w.heads.footer.rescan \ + -text {Rescan} \ + -command [cb _rescan] + pack $w.heads.footer.status -side left -fill x -expand 1 + pack $w.heads.footer.rescan -side right + + pack $w.heads.footer -side bottom -fill x -expand 1 + pack $w.heads.sby -side right -fill y + pack $w.heads.l -side left -fill both -expand 1 + pack $w.heads -fill both -expand 1 -pady 5 -padx 5 + + labelframe $w.validate -text {Delete Only If} + radiobutton $w.validate.head_r \ + -text {Merged Into:} \ + -value head \ + -variable @checktype + set head_m [tk_optionMenu $w.validate.head_m @check_head {}] + trace add variable @head_list write [cb _write_head_list] + trace add variable @check_head write [cb _write_check_head] + grid $w.validate.head_r $w.validate.head_m -sticky w + radiobutton $w.validate.always_r \ + -text {Always (Do not perform merge checks)} \ + -value always \ + -variable @checktype + grid $w.validate.always_r -columnspan 2 -sticky w + grid columnconfigure $w.validate 1 -weight 1 + pack $w.validate -anchor nw -fill x -pady 5 -padx 5 + + trace add variable @urltype write [cb _write_urltype] + _rescan $this + + bind $w <Key-F5> [cb _rescan] + bind $w <$M1B-Key-r> [cb _rescan] + bind $w <$M1B-Key-R> [cb _rescan] + bind $w <Key-Return> [cb _delete] + bind $w <Key-Escape> [list destroy $w] + return $w +} + +method _delete {} { + switch $urltype { + remote {set uri $remote} + url {set uri $url} + } + + set cache $urltype:$uri + set crev {} + if {$checktype eq {head}} { + if {$check_head eq {}} { + tk_messageBox \ + -icon error \ + -type ok \ + -title [wm title $w] \ + -parent $w \ + -message "A branch is required for 'Merged Into'." + return + } + set crev $full_cache("$cache\nrefs/heads/$check_head") + } + + set not_merged [list] + set need_fetch 0 + set have_selection 0 + set push_cmd [list git push] + lappend push_cmd -v + lappend push_cmd $uri + + foreach i [$w.heads.l curselection] { + set ref [lindex $full_list $i] + if {$crev ne {}} { + set obj $full_cache("$cache\n$ref") + if {[catch {set m [git merge-base $obj $crev]}]} { + set need_fetch 1 + set m {} + } + if {$obj ne $m} { + lappend not_merged [lindex $head_list $i] + continue + } + } + + lappend push_cmd :$ref + set have_selection 1 + } + + if {$not_merged ne {}} { + set msg "The following branches are not completely merged into $check_head: + + - [join $not_merged "\n - "]" + + if {$need_fetch} { + append msg " + +One or more of the merge tests failed because you have not fetched the necessary commits. Try fetching from $uri first." + } + + tk_messageBox \ + -icon info \ + -type ok \ + -title [wm title $w] \ + -parent $w \ + -message $msg + if {!$have_selection} return + } + + if {!$have_selection} { + tk_messageBox \ + -icon error \ + -type ok \ + -title [wm title $w] \ + -parent $w \ + -message "Please select one or more branches to delete." + return + } + + if {[tk_messageBox \ + -icon warning \ + -type yesno \ + -title [wm title $w] \ + -parent $w \ + -message {Recovering deleted branches is difficult. + +Delete the selected branches?}] ne yes} { + return + } + + destroy $w + + set cons [console::new \ + "push $uri" \ + "Deleting branches from $uri"] + console::exec $cons $push_cmd +} + +method _rescan {{force 1}} { + switch $urltype { + remote {set uri $remote} + url {set uri $url} + } + + if {$force} { + unset -nocomplain cached($urltype:$uri) + } + + if {$idle_id ne {}} { + after cancel $idle_id + set idle_id {} + } + + _load $this $urltype:$uri $uri +} + +method _write_remote {args} { set urltype remote } +method _write_url {args} { set urltype url } +method _write_check_head {args} { set checktype head } + +method _write_head_list {args} { + $head_m delete 0 end + foreach abr $head_list { + $head_m insert end radiobutton \ + -label $abr \ + -value $abr \ + -variable @check_head + } + if {[lsearch -exact -sorted $head_list $check_head] < 0} { + set check_head {} + } +} + +method _write_urltype {args} { + if {$urltype eq {url}} { + if {$idle_id ne {}} { + after cancel $idle_id + } + _load $this none: {} + set idle_id [after 1000 [cb _rescan 0]] + } else { + _rescan $this 0 + } +} + +method _load {cache uri} { + if {$active_ls ne {}} { + catch {close $active_ls} + } + + if {$uri eq {}} { + $w.heads.l conf -state disabled + set head_list [list] + set full_list [list] + set status {No repository selected.} + return + } + + if {[catch {set x $cached($cache)}]} { + set status "Scanning $uri..." + $w.heads.l conf -state disabled + set head_list [list] + set full_list [list] + set head_cache($cache) [list] + set full_cache($cache) [list] + set active_ls [open "| [list git ls-remote $uri]" r] + fconfigure $active_ls \ + -blocking 0 \ + -translation lf \ + -encoding utf-8 + fileevent $active_ls readable [cb _read $cache $active_ls] + } else { + set status {} + set full_list $full_cache($cache) + set head_list $head_cache($cache) + $w.heads.l conf -state normal + } +} + +method _read {cache fd} { + if {$fd ne $active_ls} { + catch {close $fd} + return + } + + while {[gets $fd line] >= 0} { + if {[string match {*^{}} $line]} continue + if {[regexp {^([0-9a-f]{40}) (.*)$} $line _junk obj ref]} { + if {[regsub ^refs/heads/ $ref {} abr]} { + lappend head_list $abr + lappend head_cache($cache) $abr + lappend full_list $ref + lappend full_cache($cache) $ref + set full_cache("$cache\n$ref") $obj + } + } + } + + if {[eof $fd]} { + if {[catch {close $fd} err]} { + set status $err + set head_list [list] + set full_list [list] + } else { + set status {} + set cached($cache) 1 + $w.heads.l conf -state normal + } + } +} ifdeleted { + catch {close $fd} +} + +} diff --git a/git-gui/lib/transport.tcl b/git-gui/lib/transport.tcl index c0e7d20fce..e8ebc6eda0 100644 --- a/git-gui/lib/transport.tcl +++ b/git-gui/lib/transport.tcl @@ -5,9 +5,19 @@ proc fetch_from {remote} { set w [console::new \ "fetch $remote" \ "Fetching new changes from $remote"] - set cmd [list git fetch] - lappend cmd $remote - console::exec $w $cmd + set cmds [list] + lappend cmds [list exec git fetch $remote] + if {[is_config_true gui.pruneduringfetch]} { + lappend cmds [list exec git remote prune $remote] + } + console::chain $w $cmds +} + +proc prune_from {remote} { + set w [console::new \ + "remote prune $remote" \ + "Pruning tracking branches deleted from $remote"] + console::exec $w [list git remote prune $remote] } proc push_to {remote} { diff --git a/git-mergetool.sh b/git-mergetool.sh index bb21b037d6..7b66309193 100755 --- a/git-mergetool.sh +++ b/git-mergetool.sh @@ -215,6 +215,12 @@ merge_file () { check_unchanged save_backup ;; + gvimdiff) + touch "$BACKUP" + gvimdiff -f -- "$LOCAL" "$path" "$REMOTE" + check_unchanged + save_backup + ;; xxdiff) touch "$BACKUP" if base_present ; then @@ -293,7 +299,7 @@ done if test -z "$merge_tool"; then merge_tool=`git-config merge.tool` case "$merge_tool" in - kdiff3 | tkdiff | xxdiff | meld | opendiff | emerge | vimdiff | "") + kdiff3 | tkdiff | xxdiff | meld | opendiff | emerge | vimdiff | gvimdiff | "") ;; # happy *) echo >&2 "git config option merge.tool set to unknown tool: $merge_tool" @@ -304,28 +310,42 @@ if test -z "$merge_tool"; then fi if test -z "$merge_tool" ; then - if type kdiff3 >/dev/null 2>&1 && test -n "$DISPLAY"; then - merge_tool="kdiff3"; - elif type tkdiff >/dev/null 2>&1 && test -n "$DISPLAY"; then - merge_tool=tkdiff - elif type xxdiff >/dev/null 2>&1 && test -n "$DISPLAY"; then - merge_tool=xxdiff - elif type meld >/dev/null 2>&1 && test -n "$DISPLAY"; then - merge_tool=meld - elif type opendiff >/dev/null 2>&1; then - merge_tool=opendiff - elif type emacs >/dev/null 2>&1; then - merge_tool=emerge - elif type vimdiff >/dev/null 2>&1; then - merge_tool=vimdiff - else + if test -n "$DISPLAY"; then + merge_tool_candidates="kdiff3 tkdiff xxdiff meld gvimdiff" + if test -n "$GNOME_DESKTOP_SESSION_ID" ; then + merge_tool_candidates="meld $merge_tool_candidates" + fi + if test "$KDE_FULL_SESSION" = "true"; then + merge_tool_candidates="kdiff3 $merge_tool_candidates" + fi + fi + if echo "${VISUAL:-$EDITOR}" | grep 'emacs' > /dev/null 2>&1; then + merge_tool_candidates="$merge_tool_candidates emerge" + fi + if echo "${VISUAL:-$EDITOR}" | grep 'vim' > /dev/null 2>&1; then + merge_tool_candidates="$merge_tool_candidates vimdiff" + fi + merge_tool_candidates="$merge_tool_candidates opendiff emerge vimdiff" + echo "merge tool candidates: $merge_tool_candidates" + for i in $merge_tool_candidates; do + if test $i = emerge ; then + cmd=emacs + else + cmd=$i + fi + if type $cmd > /dev/null 2>&1; then + merge_tool=$i + break + fi + done + if test -z "$merge_tool" ; then echo "No available merge resolution programs available." exit 1 fi fi case "$merge_tool" in - kdiff3|tkdiff|meld|xxdiff|vimdiff|opendiff) + kdiff3|tkdiff|meld|xxdiff|vimdiff|gvimdiff|opendiff) if ! type "$merge_tool" > /dev/null 2>&1; then echo "The merge tool $merge_tool is not available" exit 1 diff --git a/git-remote.perl b/git-remote.perl index 5763799127..b59cafdf87 100755 --- a/git-remote.perl +++ b/git-remote.perl @@ -258,6 +258,7 @@ sub show_remote { if ($info->{'PUSH'}) { my @pushed = map { s|^refs/heads/||; + s|^\+refs/heads/|+|; s|:refs/heads/|:|; $_; } @{$info->{'PUSH'}}; 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/git-submodule.sh b/git-submodule.sh index 6ed5a6ced2..8bdd99a2f3 100755 --- a/git-submodule.sh +++ b/git-submodule.sh @@ -25,8 +25,35 @@ say() fi } + # -# Run clone + checkout on missing submodules +# Clone a submodule +# +module_clone() +{ + path=$1 + url=$2 + + # If there already is a directory at the submodule path, + # expect it to be empty (since that is the default checkout + # action) and try to remove it. + # Note: if $path is a symlink to a directory the test will + # succeed but the rmdir will fail. We might want to fix this. + if test -d "$path" + then + rmdir "$path" 2>/dev/null || + die "Directory '$path' exist, but is neither empty nor a git repository" + fi + + test -e "$path" && + die "A file already exist at path '$path'" + + git-clone -n "$url" "$path" || + die "Clone of submodule '$path' failed" +} + +# +# Register submodules in .git/config # # $@ = requested paths (default to all) # @@ -35,52 +62,23 @@ modules_init() git ls-files --stage -- "$@" | grep -e '^160000 ' | while read mode sha1 stage path do - # Skip submodule paths that already contain a .git directory. - # This will also trigger if $path is a symlink to a git - # repository - test -d "$path"/.git && continue - - # If there already is a directory at the submodule path, - # expect it to be empty (since that is the default checkout - # action) and try to remove it. - # Note: if $path is a symlink to a directory the test will - # succeed but the rmdir will fail. We might want to fix this. - if test -d "$path" - then - rmdir "$path" 2>/dev/null || - die "Directory '$path' exist, but is neither empty nor a git repository" - fi - - test -e "$path" && - die "A file already exist at path '$path'" + # Skip already registered paths + url=$(git-config submodule."$path".url) + test -z "$url" || continue url=$(GIT_CONFIG=.gitmodules git-config module."$path".url) test -z "$url" && die "No url found for submodule '$path' in .gitmodules" - # MAYBE FIXME: this would be the place to check GIT_CONFIG - # for a preferred url for this submodule, possibly like this: - # - # modname=$(GIT_CONFIG=.gitmodules git-config module."$path".name) - # alturl=$(git-config module."$modname".url) - # - # This would let the versioned .gitmodules file use the submodule - # path as key, while the unversioned GIT_CONFIG would use the - # logical modulename (if present) as key. But this would need - # another fallback mechanism if the module wasn't named. - - git-clone -n "$url" "$path" || - die "Clone of submodule '$path' failed" + git-config submodule."$path".url "$url" || + die "Failed to register url for submodule '$path'" - (unset GIT_DIR && cd "$path" && git-checkout -q "$sha1") || - die "Checkout of submodule '$path' failed" - - say "Submodule '$path' initialized" + say "Submodule '$path' registered with url '$url'" done } # -# Checkout correct revision of each initialized submodule +# Update each submodule path to correct revision, using clone and checkout as needed # # $@ = requested paths (default to all) # @@ -89,14 +87,21 @@ modules_update() git ls-files --stage -- "$@" | grep -e '^160000 ' | while read mode sha1 stage path do - if ! test -d "$path"/.git + url=$(git-config submodule."$path".url) + if test -z "$url" then # Only mention uninitialized submodules when its # path have been specified test "$#" != "0" && say "Submodule '$path' not initialized" - continue; + continue fi + + if ! test -d "$path"/.git + then + module_clone "$path" "$url" || exit + fi + subsha1=$(unset GIT_DIR && cd "$path" && git-rev-parse --verify HEAD) || die "Unable to find current revision of submodule '$path'" diff --git a/gitweb/gitweb.css b/gitweb/gitweb.css index 9f0822fab3..7908fe3b5f 100644 --- a/gitweb/gitweb.css +++ b/gitweb/gitweb.css @@ -181,10 +181,15 @@ table.diff_tree { font-family: monospace; } +table.combined.diff_tree th { + text-align: center; +} + table.combined.diff_tree td { padding-right: 24px; } +table.combined.diff_tree th.link, table.combined.diff_tree td.link { padding: 0px 2px; } diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index e92596c295..dbfb0441a6 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -954,7 +954,200 @@ sub format_subject_html { } } -# format patch (diff) line (rather not to be used for diff headers) +# format git diff header line, i.e. "diff --(git|combined|cc) ..." +sub format_git_diff_header_line { + my $line = shift; + my $diffinfo = shift; + my ($from, $to) = @_; + + if ($diffinfo->{'nparents'}) { + # combined diff + $line =~ s!^(diff (.*?) )"?.*$!$1!; + if ($to->{'href'}) { + $line .= $cgi->a({-href => $to->{'href'}, -class => "path"}, + esc_path($to->{'file'})); + } else { # file was deleted (no href) + $line .= esc_path($to->{'file'}); + } + } else { + # "ordinary" diff + $line =~ s!^(diff (.*?) )"?a/.*$!$1!; + if ($from->{'href'}) { + $line .= $cgi->a({-href => $from->{'href'}, -class => "path"}, + 'a/' . esc_path($from->{'file'})); + } else { # file was added (no href) + $line .= 'a/' . esc_path($from->{'file'}); + } + $line .= ' '; + if ($to->{'href'}) { + $line .= $cgi->a({-href => $to->{'href'}, -class => "path"}, + 'b/' . esc_path($to->{'file'})); + } else { # file was deleted + $line .= 'b/' . esc_path($to->{'file'}); + } + } + + return "<div class=\"diff header\">$line</div>\n"; +} + +# format extended diff header line, before patch itself +sub format_extended_diff_header_line { + my $line = shift; + my $diffinfo = shift; + my ($from, $to) = @_; + + # match <path> + if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) { + $line .= $cgi->a({-href=>$from->{'href'}, -class=>"path"}, + esc_path($from->{'file'})); + } + if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) { + $line .= $cgi->a({-href=>$to->{'href'}, -class=>"path"}, + esc_path($to->{'file'})); + } + # match single <mode> + if ($line =~ m/\s(\d{6})$/) { + $line .= '<span class="info"> (' . + file_type_long($1) . + ')</span>'; + } + # match <hash> + if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) { + # can match only for combined diff + $line = 'index '; + for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) { + if ($from->{'href'}[$i]) { + $line .= $cgi->a({-href=>$from->{'href'}[$i], + -class=>"hash"}, + substr($diffinfo->{'from_id'}[$i],0,7)); + } else { + $line .= '0' x 7; + } + # separator + $line .= ',' if ($i < $diffinfo->{'nparents'} - 1); + } + $line .= '..'; + if ($to->{'href'}) { + $line .= $cgi->a({-href=>$to->{'href'}, -class=>"hash"}, + substr($diffinfo->{'to_id'},0,7)); + } else { + $line .= '0' x 7; + } + + } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) { + # can match only for ordinary diff + my ($from_link, $to_link); + if ($from->{'href'}) { + $from_link = $cgi->a({-href=>$from->{'href'}, -class=>"hash"}, + substr($diffinfo->{'from_id'},0,7)); + } else { + $from_link = '0' x 7; + } + if ($to->{'href'}) { + $to_link = $cgi->a({-href=>$to->{'href'}, -class=>"hash"}, + substr($diffinfo->{'to_id'},0,7)); + } else { + $to_link = '0' x 7; + } + my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'}); + $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!; + } + + return $line . "<br/>\n"; +} + +# format from-file/to-file diff header +sub format_diff_from_to_header { + my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_; + my $line; + my $result = ''; + + $line = $from_line; + #assert($line =~ m/^---/) if DEBUG; + # no extra formatting for "^--- /dev/null" + if (! $diffinfo->{'nparents'}) { + # ordinary (single parent) diff + if ($line =~ m!^--- "?a/!) { + if ($from->{'href'}) { + $line = '--- a/' . + $cgi->a({-href=>$from->{'href'}, -class=>"path"}, + esc_path($from->{'file'})); + } else { + $line = '--- a/' . + esc_path($from->{'file'}); + } + } + $result .= qq!<div class="diff from_file">$line</div>\n!; + + } else { + # combined diff (merge commit) + for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) { + if ($from->{'href'}[$i]) { + $line = '--- ' . + $cgi->a({-href=>href(action=>"blobdiff", + hash_parent=>$diffinfo->{'from_id'}[$i], + hash_parent_base=>$parents[$i], + file_parent=>$from->{'file'}[$i], + hash=>$diffinfo->{'to_id'}, + hash_base=>$hash, + file_name=>$to->{'file'}), + -class=>"path", + -title=>"diff" . ($i+1)}, + $i+1) . + '/' . + $cgi->a({-href=>$from->{'href'}[$i], -class=>"path"}, + esc_path($from->{'file'}[$i])); + } else { + $line = '--- /dev/null'; + } + $result .= qq!<div class="diff from_file">$line</div>\n!; + } + } + + $line = $to_line; + #assert($line =~ m/^\+\+\+/) if DEBUG; + # no extra formatting for "^+++ /dev/null" + if ($line =~ m!^\+\+\+ "?b/!) { + if ($to->{'href'}) { + $line = '+++ b/' . + $cgi->a({-href=>$to->{'href'}, -class=>"path"}, + esc_path($to->{'file'})); + } else { + $line = '+++ b/' . + esc_path($to->{'file'}); + } + } + $result .= qq!<div class="diff to_file">$line</div>\n!; + + return $result; +} + +# create note for patch simplified by combined diff +sub format_diff_cc_simplified { + my ($diffinfo, @parents) = @_; + my $result = ''; + + $result .= "<div class=\"diff header\">" . + "diff --cc "; + if (!is_deleted($diffinfo)) { + $result .= $cgi->a({-href => href(action=>"blob", + hash_base=>$hash, + hash=>$diffinfo->{'to_id'}, + file_name=>$diffinfo->{'to_file'}), + -class => "path"}, + esc_path($diffinfo->{'to_file'})); + } else { + $result .= esc_path($diffinfo->{'to_file'}); + } + $result .= "</div>\n" . # class="diff header" + "<div class=\"diff nodifferences\">" . + "Simple merge" . + "</div>\n"; # class="diff nodifferences" + + return $result; +} + +# format patch (diff) line (not to be used for diff headers) sub format_diff_line { my $line = shift; my ($from, $to) = @_; @@ -1680,6 +1873,48 @@ sub parse_ls_tree_line ($;%) { return wantarray ? %res : \%res; } +# generates _two_ hashes, references to which are passed as 2 and 3 argument +sub parse_from_to_diffinfo { + my ($diffinfo, $from, $to, @parents) = @_; + + if ($diffinfo->{'nparents'}) { + # combined diff + $from->{'file'} = []; + $from->{'href'} = []; + fill_from_file_info($diffinfo, @parents) + unless exists $diffinfo->{'from_file'}; + for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) { + $from->{'file'}[$i] = $diffinfo->{'from_file'}[$i] || $diffinfo->{'to_file'}; + if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file + $from->{'href'}[$i] = href(action=>"blob", + hash_base=>$parents[$i], + hash=>$diffinfo->{'from_id'}[$i], + file_name=>$from->{'file'}[$i]); + } else { + $from->{'href'}[$i] = undef; + } + } + } else { + $from->{'file'} = $diffinfo->{'from_file'} || $diffinfo->{'file'}; + if ($diffinfo->{'status'} ne "A") { # not new (added) file + $from->{'href'} = href(action=>"blob", hash_base=>$hash_parent, + hash=>$diffinfo->{'from_id'}, + file_name=>$from->{'file'}); + } else { + delete $from->{'href'}; + } + } + + $to->{'file'} = $diffinfo->{'to_file'} || $diffinfo->{'file'}; + if (!is_deleted($diffinfo)) { # file exists in result + $to->{'href'} = href(action=>"blob", hash_base=>$hash, + hash=>$diffinfo->{'to_id'}, + file_name=>$to->{'file'}); + } else { + delete $to->{'href'}; + } +} + ## ...................................................................... ## parse to array of hashes functions @@ -2387,6 +2622,11 @@ sub from_ids_eq { } } +sub is_deleted { + my $diffinfo = shift; + + return $diffinfo->{'to_id'} eq ('0' x 40); +} sub git_difftree_body { my ($difftree, $hash, @parents) = @_; @@ -2401,6 +2641,26 @@ sub git_difftree_body { print "<table class=\"" . (@parents > 1 ? "combined " : "") . "diff_tree\">\n"; + + # header only for combined diff in 'commitdiff' view + my $has_header = @parents > 1 && $action eq 'commitdiff'; + if ($has_header) { + # table header + print "<thead><tr>\n" . + "<th></th><th></th>\n"; # filename, patchN link + for (my $i = 0; $i < @parents; $i++) { + my $par = $parents[$i]; + print "<th>" . + $cgi->a({-href => href(action=>"commitdiff", + hash=>$hash, hash_parent=>$par), + -title => 'commitdiff to parent number ' . + ($i+1) . ': ' . substr($par,0,7)}, + $i+1) . + " </th>\n"; + } + print "</tr></thead>\n<tbody>\n"; + } + my $alternate = 1; my $patchno = 0; foreach my $line (@{$difftree}) { @@ -2424,7 +2684,7 @@ sub git_difftree_body { fill_from_file_info($diff, @parents) unless exists $diff->{'from_file'}; - if ($diff->{'to_id'} ne ('0' x 40)) { + if (!is_deleted($diff)) { # file exists in the result (child) commit print "<td>" . $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'}, @@ -2673,6 +2933,7 @@ sub git_difftree_body { } # we should not encounter Unmerged (U) or Unknown (X) status print "</tr>\n"; } + print "</tbody>" if $has_header; print "</table>\n"; } @@ -2737,13 +2998,35 @@ sub git_patchset_body { # advance raw git-diff output if needed $patch_idx++ if defined $diffinfo; - # read and prepare patch information - if (ref($difftree->[$patch_idx]) eq "HASH") { - # pre-parsed (or generated by hand) - $diffinfo = $difftree->[$patch_idx]; - } else { - $diffinfo = parse_difftree_raw_line($difftree->[$patch_idx]); + # compact combined diff output can have some patches skipped + # find which patch (using pathname of result) we are at now + my $to_name; + if ($diff_header[0] =~ m!^diff --cc "?(.*)"?$!) { + $to_name = $1; } + + do { + # read and prepare patch information + if (ref($difftree->[$patch_idx]) eq "HASH") { + # pre-parsed (or generated by hand) + $diffinfo = $difftree->[$patch_idx]; + } else { + $diffinfo = parse_difftree_raw_line($difftree->[$patch_idx]); + } + + # check if current raw line has no patch (it got simplified) + if (defined $to_name && $to_name ne $diffinfo->{'to_file'}) { + print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" . + format_diff_cc_simplified($diffinfo, @hash_parents) . + "</div>\n"; # class="patch" + + $patch_idx++; + $patch_number++; + } + } until (!defined $to_name || $to_name eq $diffinfo->{'to_file'} || + $patch_idx > $#$difftree); + # modifies %from, %to hashes + parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents); if ($diffinfo->{'nparents'}) { # combined diff $from{'file'} = []; @@ -2773,7 +3056,7 @@ sub git_patchset_body { } $to{'file'} = $diffinfo->{'to_file'} || $diffinfo->{'file'}; - if ($diffinfo->{'to_id'} ne ('0' x 40)) { # file exists in result + if (!is_deleted($diffinfo)) { # file exists in result $to{'href'} = href(action=>"blob", hash_base=>$hash, hash=>$diffinfo->{'to_id'}, file_name=>$to{'file'}); @@ -2787,105 +3070,15 @@ sub git_patchset_body { # print "git diff" header $patch_line = shift @diff_header; - if ($diffinfo->{'nparents'}) { - - # combined diff - $patch_line =~ s!^(diff (.*?) )"?.*$!$1!; - if ($to{'href'}) { - $patch_line .= $cgi->a({-href => $to{'href'}, -class => "path"}, - esc_path($to{'file'})); - } else { # file was deleted - $patch_line .= esc_path($to{'file'}); - } - - } else { - - $patch_line =~ s!^(diff (.*?) )"?a/.*$!$1!; - if ($from{'href'}) { - $patch_line .= $cgi->a({-href => $from{'href'}, -class => "path"}, - 'a/' . esc_path($from{'file'})); - } else { # file was added - $patch_line .= 'a/' . esc_path($from{'file'}); - } - $patch_line .= ' '; - if ($to{'href'}) { - $patch_line .= $cgi->a({-href => $to{'href'}, -class => "path"}, - 'b/' . esc_path($to{'file'})); - } else { # file was deleted - $patch_line .= 'b/' . esc_path($to{'file'}); - } - - } - print "<div class=\"diff header\">$patch_line</div>\n"; + print format_git_diff_header_line($patch_line, $diffinfo, + \%from, \%to); # print extended diff header print "<div class=\"diff extended_header\">\n" if (@diff_header > 0); EXTENDED_HEADER: foreach $patch_line (@diff_header) { - # match <path> - if ($patch_line =~ s!^((copy|rename) from ).*$!$1! && $from{'href'}) { - $patch_line .= $cgi->a({-href=>$from{'href'}, -class=>"path"}, - esc_path($from{'file'})); - } - if ($patch_line =~ s!^((copy|rename) to ).*$!$1! && $to{'href'}) { - $patch_line .= $cgi->a({-href=>$to{'href'}, -class=>"path"}, - esc_path($to{'file'})); - } - # match single <mode> - if ($patch_line =~ m/\s(\d{6})$/) { - $patch_line .= '<span class="info"> (' . - file_type_long($1) . - ')</span>'; - } - # match <hash> - if ($patch_line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) { - # can match only for combined diff - $patch_line = 'index '; - for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) { - if ($from{'href'}[$i]) { - $patch_line .= $cgi->a({-href=>$from{'href'}[$i], - -class=>"hash"}, - substr($diffinfo->{'from_id'}[$i],0,7)); - } else { - $patch_line .= '0' x 7; - } - # separator - $patch_line .= ',' if ($i < $diffinfo->{'nparents'} - 1); - } - $patch_line .= '..'; - if ($to{'href'}) { - $patch_line .= $cgi->a({-href=>$to{'href'}, -class=>"hash"}, - substr($diffinfo->{'to_id'},0,7)); - } else { - $patch_line .= '0' x 7; - } - - } elsif ($patch_line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) { - # can match only for ordinary diff - my ($from_link, $to_link); - if ($from{'href'}) { - $from_link = $cgi->a({-href=>$from{'href'}, -class=>"hash"}, - substr($diffinfo->{'from_id'},0,7)); - } else { - $from_link = '0' x 7; - } - if ($to{'href'}) { - $to_link = $cgi->a({-href=>$to{'href'}, -class=>"hash"}, - substr($diffinfo->{'to_id'},0,7)); - } else { - $to_link = '0' x 7; - } - #affirm { - # my ($from_hash, $to_hash) = - # ($patch_line =~ m/^index ([0-9a-fA-F]{40})..([0-9a-fA-F]{40})/); - # my ($from_id, $to_id) = - # ($diffinfo->{'from_id'}, $diffinfo->{'to_id'}); - # ($from_hash eq $from_id) && ($to_hash eq $to_id); - #} if DEBUG; - my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'}); - $patch_line =~ s!$from_id\.\.$to_id!$from_link..$to_link!; - } - print $patch_line . "<br/>\n"; + print format_extended_diff_header_line($patch_line, $diffinfo, + \%from, \%to); } print "</div>\n" if (@diff_header > 0); # class="diff extended_header" @@ -2897,24 +3090,15 @@ sub git_patchset_body { } next PATCH if ($patch_line =~ m/^diff /); #assert($patch_line =~ m/^---/) if DEBUG; - if (!$diffinfo->{'nparents'} && # not from-file line for combined diff - $from{'href'} && $patch_line =~ m!^--- "?a/!) { - $patch_line = '--- a/' . - $cgi->a({-href=>$from{'href'}, -class=>"path"}, - esc_path($from{'file'})); - } - print "<div class=\"diff from_file\">$patch_line</div>\n"; + #assert($patch_line eq $last_patch_line) if DEBUG; $patch_line = <$fd>; chomp $patch_line; + #assert($patch_line =~ m/^\+\+\+/) if DEBUG; - #assert($patch_line =~ m/^+++/) if DEBUG; - if ($to{'href'} && $patch_line =~ m!^\+\+\+ "?b/!) { - $patch_line = '+++ b/' . - $cgi->a({-href=>$to{'href'}, -class=>"path"}, - esc_path($to{'file'})); - } - print "<div class=\"diff to_file\">$patch_line</div>\n"; + print format_diff_from_to_header($last_patch_line, $patch_line, + $diffinfo, \%from, \%to, + @hash_parents); # the patch itself LINE: @@ -2930,6 +3114,27 @@ sub git_patchset_body { print "</div>\n"; # class="patch" } + # for compact combined (--cc) format, with chunk and patch simpliciaction + # patchset might be empty, but there might be unprocessed raw lines + for ($patch_idx++ if $patch_number > 0; + $patch_idx < @$difftree; + $patch_idx++) { + # read and prepare patch information + if (ref($difftree->[$patch_idx]) eq "HASH") { + # pre-parsed (or generated by hand) + $diffinfo = $difftree->[$patch_idx]; + } else { + $diffinfo = parse_difftree_raw_line($difftree->[$patch_idx]); + } + + # generate anchor for "patch" links in difftree / whatchanged part + print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" . + format_diff_cc_simplified($diffinfo, @hash_parents) . + "</div>\n"; # class="patch" + + $patch_number++; + } + if ($patch_number == 0) { if (@hash_parents > 1) { print "<div class=\"diff nodifferences\">Trivial merge</div>\n"; @@ -4001,8 +4206,10 @@ sub git_snapshot { my $git = git_cmd_str(); my $name = $project; + $name =~ s,([^/])/*\.git$,$1,; + $name = basename($name); + my $filename = to_utf8($name); $name =~ s/\047/\047\\\047\047/g; - my $filename = to_utf8(basename($project)); my $cmd; if ($suffix eq 'zip') { $filename .= "-$hash.$suffix"; @@ -4443,7 +4650,11 @@ sub git_commitdiff { die_error(undef, "Unknown commit object"); } - # we need to prepare $formats_nav before any parameter munging + # choose format for commitdiff for merge + if (! defined $hash_parent && @{$co{'parents'}} > 1) { + $hash_parent = '--cc'; + } + # we need to prepare $formats_nav before almost any parameter munging my $formats_nav; if ($format eq 'html') { $formats_nav = @@ -4451,14 +4662,22 @@ sub git_commitdiff { hash=>$hash, hash_parent=>$hash_parent)}, "raw"); - if (defined $hash_parent) { + if (defined $hash_parent && + $hash_parent ne '-c' && $hash_parent ne '--cc') { # commitdiff with two commits given my $hash_parent_short = $hash_parent; if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) { $hash_parent_short = substr($hash_parent, 0, 7); } $formats_nav .= - ' (from: ' . + ' (from'; + for (my $i = 0; $i < @{$co{'parents'}}; $i++) { + if ($co{'parents'}[$i] eq $hash_parent) { + $formats_nav .= ' parent ' . ($i+1); + last; + } + } + $formats_nav .= ': ' . $cgi->a({-href => href(action=>"commitdiff", hash=>$hash_parent)}, esc_html($hash_parent_short)) . @@ -4476,6 +4695,17 @@ sub git_commitdiff { ')'; } else { # merge commit + if ($hash_parent eq '--cc') { + $formats_nav .= ' | ' . + $cgi->a({-href => href(action=>"commitdiff", + hash=>$hash, hash_parent=>'-c')}, + 'combined'); + } else { # $hash_parent eq '-c' + $formats_nav .= ' | ' . + $cgi->a({-href => href(action=>"commitdiff", + hash=>$hash, hash_parent=>'--cc')}, + 'compact'); + } $formats_nav .= ' (merge: ' . join(' ', map { @@ -4488,9 +4718,10 @@ sub git_commitdiff { } my $hash_parent_param = $hash_parent; - if (!defined $hash_parent) { + if (!defined $hash_parent_param) { + # --cc for multiple parents, --root for parentless $hash_parent_param = - @{$co{'parents'}} > 1 ? '-c' : $co{'parent'} || '--root'; + @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root'; } # read commitdiff @@ -4567,10 +4798,14 @@ TEXT # write patch if ($format eq 'html') { - git_difftree_body(\@difftree, $hash, $hash_parent || @{$co{'parents'}}); + my $use_parents = !defined $hash_parent || + $hash_parent eq '-c' || $hash_parent eq '--cc'; + git_difftree_body(\@difftree, $hash, + $use_parents ? @{$co{'parents'}} : $hash_parent); print "<br/>\n"; - git_patchset_body($fd, \@difftree, $hash, $hash_parent || @{$co{'parents'}}); + git_patchset_body($fd, \@difftree, $hash, + $use_parents ? @{$co{'parents'}} : $hash_parent); close $fd; print "</div>\n"; # class="page_body" git_footer_html(); @@ -542,7 +542,7 @@ int match_refs(struct ref *src, struct ref *dst, struct ref ***dst_tail, strcpy(dst_name, pat->dst); strcat(dst_name, src->name + strlen(pat->src)); } else - dst_name = strdup(src->name); + dst_name = xstrdup(src->name); dst_peer = find_ref_by_name(dst, dst_name); if (dst_peer && dst_peer->peer_ref) /* We're already sending something to this ref. */ diff --git a/t/t7400-submodule-basic.sh b/t/t7400-submodule-basic.sh index 6274729729..3940433b8f 100755 --- a/t/t7400-submodule-basic.sh +++ b/t/t7400-submodule-basic.sh @@ -40,7 +40,7 @@ test_expect_success 'Prepare submodule testing' ' git-add a lib z && git-commit -m "super commit 1" && mv lib .subrepo && - GIT_CONFIG=.gitmodules git-config module.lib.url ./.subrepo + GIT_CONFIG=.gitmodules git-config module.lib.url git://example.com/lib.git ' test_expect_success 'status should only print one line' ' @@ -52,41 +52,55 @@ test_expect_success 'status should initially be "missing"' ' git-submodule status | grep "^-$rev1" ' -test_expect_success 'init should fail when path is used by a file' ' +test_expect_success 'init should register submodule url in .git/config' ' + git-submodule init && + url=$(git-config submodule.lib.url) && + if test "$url" != "git://example.com/lib.git" + then + echo "[OOPS] init succeeded but submodule url is wrong" + false + elif ! git-config submodule.lib.url ./.subrepo + then + echo "[OOPS] init succeeded but update of url failed" + false + fi +' + +test_expect_success 'update should fail when path is used by a file' ' echo "hello" >lib && - if git-submodule init + if git-submodule update then - echo "[OOPS] init should have failed" + echo "[OOPS] update should have failed" false elif test -f lib && test "$(cat lib)" != "hello" then - echo "[OOPS] init failed but lib file was molested" + echo "[OOPS] update failed but lib file was molested" false else rm lib fi ' -test_expect_success 'init should fail when path is used by a nonempty directory' ' +test_expect_success 'update should fail when path is used by a nonempty directory' ' mkdir lib && echo "hello" >lib/a && - if git-submodule init + if git-submodule update then - echo "[OOPS] init should have failed" + echo "[OOPS] update should have failed" false elif test "$(cat lib/a)" != "hello" then - echo "[OOPS] init failed but lib/a was molested" + echo "[OOPS] update failed but lib/a was molested" false else rm lib/a fi ' -test_expect_success 'init should work when path is an empty dir' ' +test_expect_success 'update should work when path is an empty dir' ' rm -rf lib && mkdir lib && - git-submodule init && + git-submodule update && head=$(cd lib && git-rev-parse HEAD) && if test -z "$head" then @@ -99,7 +113,7 @@ test_expect_success 'init should work when path is an empty dir' ' fi ' -test_expect_success 'status should be "up-to-date" after init' ' +test_expect_success 'status should be "up-to-date" after update' ' git-submodule status | grep "^ $rev1" ' diff --git a/templates/Makefile b/templates/Makefile index b8352e731b..aaa39d30fa 100644 --- a/templates/Makefile +++ b/templates/Makefile @@ -7,7 +7,7 @@ endif INSTALL ?= install TAR ?= tar prefix ?= $(HOME) -template_dir ?= $(prefix)/share/git-core/templates/ +template_dir ?= $(prefix)/share/git-core/templates # DESTDIR= # Shell quote (do not use $(call) to accommodate ancient setups); |