From 35874c163e2f5c1def3cc5c3465beceea0355b8f Mon Sep 17 00:00:00 2001 From: "Shawn O. Pearce" Date: Mon, 29 Jan 2007 00:50:41 -0500 Subject: [PATCH] git-gui: Implemented file browser and incremental blame. This rather huge change provides a browser for the current branch. The browser simply shows the contents of tree HEAD, and lets the user drill down through the tree. The icons used really stink, as I just copied in icon which we already had. I really need to replace the file_dir and file_uplevel icons with something more useful. If the user double clicks on a file within the browser we open it in a blame viewer. This makes use of the new incremental blame feature that Linus just added yesterday to core Git. Fortunately the feature will be in 1.5.0 final so we can rely on having it available here. Since the blame engine is incremental the user will get blame data for groups which can be determined early. Git will slowly fill in the remaining lines as it goes. Signed-off-by: Shawn O. Pearce --- git-gui.sh | 459 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 459 insertions(+) diff --git a/git-gui.sh b/git-gui.sh index 46e019becd..0192f80dd7 100755 --- a/git-gui.sh +++ b/git-gui.sh @@ -2947,6 +2947,431 @@ proc reset_hard_wait {fd} { ###################################################################### ## +## browser + +set next_browser_id 0 + +proc new_browser {} { + global next_browser_id cursor_ptr + global browser_commit browser_status browser_stack browser_path browser_busy + + set w .browser[incr next_browser_id] + set w_list $w.list.l + set browser_commit($w_list) HEAD + set browser_status($w_list) {Starting...} + set browser_stack($w_list) {} + set browser_path($w_list) $browser_commit($w_list): + set browser_busy($w_list) 1 + + toplevel $w + label $w.path -textvariable browser_path($w_list) \ + -anchor w \ + -justify left \ + -borderwidth 1 \ + -relief sunken \ + -font font_uibold + pack $w.path -anchor w -side top -fill x + + frame $w.list + text $w_list -background white -borderwidth 0 \ + -cursor $cursor_ptr \ + -state disabled \ + -wrap none \ + -height 20 \ + -width 70 \ + -xscrollcommand [list $w.list.sbx set] \ + -yscrollcommand [list $w.list.sby set] \ + -font font_ui + $w_list tag conf in_sel \ + -background [$w_list cget -foreground] \ + -foreground [$w_list cget -background] + scrollbar $w.list.sbx -orient h -command [list $w_list xview] + scrollbar $w.list.sby -orient v -command [list $w_list yview] + pack $w.list.sbx -side bottom -fill x + pack $w.list.sby -side right -fill y + pack $w_list -side left -fill both -expand 1 + pack $w.list -side top -fill both -expand 1 + + label $w.status -textvariable browser_status($w_list) \ + -anchor w \ + -justify left \ + -borderwidth 1 \ + -relief sunken \ + -font font_ui + pack $w.status -anchor w -side bottom -fill x + + bind $w_list "browser_click 0 $w_list @%x,%y;break" + bind $w_list "browser_click 1 $w_list @%x,%y;break" + + bind $w "focus $w" + bind $w " + array unset browser_buffer $w_list + array unset browser_files $w_list + array unset browser_status $w_list + array unset browser_stack $w_list + array unset browser_path $w_list + array unset browser_commit $w_list + array unset browser_busy $w_list + " + wm title $w "[appname] ([reponame]): File Browser" + ls_tree $w_list $browser_commit($w_list) {} +} + +proc browser_click {was_double_click w pos} { + global browser_files browser_status browser_path + global browser_commit browser_stack browser_busy + + if {$browser_busy($w)} return + set lno [lindex [split [$w index $pos] .] 0] + set info [lindex $browser_files($w) [expr {$lno - 1}]] + + $w conf -state normal + $w tag remove sel 0.0 end + $w tag remove in_sel 0.0 end + if {$info ne {}} { + $w tag add in_sel $lno.0 [expr {$lno + 1}].0 + if {$was_double_click} { + switch -- [lindex $info 0] { + parent { + set parent [lindex $browser_stack($w) end-1] + set browser_stack($w) [lrange $browser_stack($w) 0 end-2] + if {$browser_stack($w) eq {}} { + regsub {:.*$} $browser_path($w) {:} browser_path($w) + } else { + regsub {/[^/]+$} $browser_path($w) {} browser_path($w) + } + set browser_status($w) "Loading $browser_path($w)..." + ls_tree $w [lindex $parent 0] [lindex $parent 1] + } + tree { + set name [lindex $info 2] + set escn [escape_path $name] + set browser_status($w) "Loading $escn..." + append browser_path($w) $escn + ls_tree $w [lindex $info 1] $name + } + blob { + set name [lindex $info 2] + set p {} + foreach n $browser_stack($w) { + append p [lindex $n 1] + } + append p $name + show_blame $browser_commit($w) $p + } + } + } + } + $w conf -state disabled +} + +proc ls_tree {w tree_id name} { + global browser_buffer browser_files browser_stack browser_busy + + set browser_buffer($w) {} + set browser_files($w) {} + set browser_busy($w) 1 + + $w conf -state normal + $w tag remove in_sel 0.0 end + $w tag remove sel 0.0 end + $w delete 0.0 end + if {$browser_stack($w) ne {}} { + $w image create end \ + -align center -padx 5 -pady 1 \ + -name icon0 \ + -image file_uplevel + $w insert end {[Up To Parent]} + lappend browser_files($w) parent + } + lappend browser_stack($w) [list $tree_id $name] + $w conf -state disabled + + set fd [open "| git ls-tree -z $tree_id" r] + fconfigure $fd -blocking 0 -translation binary -encoding binary + fileevent $fd readable [list read_ls_tree $fd $w] +} + +proc read_ls_tree {fd w} { + global browser_buffer browser_files browser_status browser_busy + + if {![winfo exists $w]} { + catch {close $fd} + return + } + + append browser_buffer($w) [read $fd] + set pck [split $browser_buffer($w) "\0"] + set browser_buffer($w) [lindex $pck end] + + set n [llength $browser_files($w)] + $w conf -state normal + foreach p [lrange $pck 0 end-1] { + set info [split $p "\t"] + set path [lindex $info 1] + set info [split [lindex $info 0] { }] + set type [lindex $info 1] + set object [lindex $info 2] + + switch -- $type { + blob { + set image file_plain + } + tree { + set image file_dir + append path / + } + default { + set image file_question + } + } + + if {$n > 0} {$w insert end "\n"} + $w image create end \ + -align center -padx 5 -pady 1 \ + -name icon[incr n] \ + -image $image + $w insert end [escape_path $path] + lappend browser_files($w) [list $type $object $path] + } + $w conf -state disabled + + if {[eof $fd]} { + close $fd + set browser_status($w) Ready. + set browser_busy($w) 0 + array unset browser_buffer $w + } +} + +proc show_blame {commit path} { + global next_browser_id blame_status blame_data + + set w .browser[incr next_browser_id] + set blame_status($w) {Loading current file content...} + set texts [list] + + toplevel $w + panedwindow $w.out -orient horizontal + + label $w.path -text "$commit:$path" \ + -anchor w \ + -justify left \ + -borderwidth 1 \ + -relief sunken \ + -font font_uibold + pack $w.path -anchor w -side top -fill x + + text $w.out.commit -background white -borderwidth 0 \ + -state disabled \ + -wrap none \ + -height 40 \ + -width 8 \ + -font font_diff + $w.out add $w.out.commit + lappend texts $w.out.commit + + text $w.out.author -background white -borderwidth 0 \ + -state disabled \ + -wrap none \ + -height 40 \ + -width 20 \ + -font font_diff + $w.out add $w.out.author + lappend texts $w.out.author + + text $w.out.date -background white -borderwidth 0 \ + -state disabled \ + -wrap none \ + -height 40 \ + -width [string length "yyyy-mm-dd hh:mm:ss"] \ + -font font_diff + $w.out add $w.out.date + lappend texts $w.out.date + + text $w.out.linenumber -background white -borderwidth 0 \ + -state disabled \ + -wrap none \ + -height 40 \ + -width 5 \ + -font font_diff + $w.out.linenumber tag conf linenumber -justify right + $w.out add $w.out.linenumber + lappend texts $w.out.linenumber + + text $w.out.file -background white -borderwidth 0 \ + -state disabled \ + -wrap none \ + -height 40 \ + -width 80 \ + -font font_diff + $w.out add $w.out.file + lappend texts $w.out.file + + label $w.status -textvariable blame_status($w) \ + -anchor w \ + -justify left \ + -borderwidth 1 \ + -relief sunken \ + -font font_ui + pack $w.status -anchor w -side bottom -fill x + + scrollbar $w.sby -orient v -command [list scrollbar2many $texts yview] + pack $w.sby -side right -fill y + pack $w.out -side left -fill both -expand 1 + + menu $w.ctxm -tearoff 0 + $w.ctxm add command -label "Copy Commit" \ + -font font_ui \ + -command "blame_copycommit $w \$cursorW @\$cursorX,\$cursorY" + + foreach i $texts { + $i tag conf in_sel \ + -background [$i cget -foreground] \ + -foreground [$i cget -background] + $i conf -yscrollcommand [list many2scrollbar $texts yview $w.sby] + bind $i "blame_highlight $i @%x,%y $texts;break" + bind_button3 $i " + set cursorX %x + set cursorY %y + set cursorW %W + tk_popup $w.ctxm %X %Y + " + } + + bind $w "focus $w" + bind $w " + array unset blame_status $w + array unset blame_data $w,* + " + wm title $w "[appname] ([reponame]): File Viewer" + + set blame_data($w,total_lines) 0 + set fd [open "| git cat-file blob $commit:$path" r] + fconfigure $fd -blocking 0 -translation lf -encoding binary + fileevent $fd readable [list read_blame_catfile $fd $w $commit $path \ + $texts $w.out.linenumber $w.out.file] +} + +proc read_blame_catfile {fd w commit path texts w_lno w_file} { + global blame_status blame_data + + if {![winfo exists $w_file]} { + catch {close $fd} + return + } + + set n $blame_data($w,total_lines) + foreach i $texts {$i conf -state normal} + while {[gets $fd line] >= 0} { + regsub "\r\$" $line {} line + incr n + $w_lno insert end $n linenumber + $w_file insert end $line + foreach i $texts {$i insert end "\n"} + } + foreach i $texts {$i conf -state disabled} + set blame_data($w,total_lines) $n + + if {[eof $fd]} { + close $fd + set blame_status($w) {Loading annotations...} + set fd [open "| git blame --incremental $commit -- $path" r] + fconfigure $fd -blocking 0 -translation lf -encoding binary + fileevent $fd readable "read_blame_incremental $fd $w $texts" + } +} + +proc read_blame_incremental {fd w w_commit w_author w_date w_lno w_file} { + global blame_status blame_data + + if {![winfo exists $w_commit]} { + catch {close $fd} + return + } + + $w_commit conf -state normal + $w_author conf -state normal + $w_date conf -state normal + + while {[gets $fd line] >= 0} { + if {[regexp {^([a-z0-9]{40}) (\d+) (\d+) (\d+)$} $line line \ + commit original_line final_line line_count]} { + set blame_data($w,commit) $commit + set blame_data($w,original_line) $original_line + set blame_data($w,final_line) $final_line + set blame_data($w,line_count) $line_count + } elseif {[string match {filename *} $line]} { + set n $blame_data($w,line_count) + set lno $blame_data($w,final_line) + set file [string range $line 9 end] + set commit $blame_data($w,commit) + set abbrev [string range $commit 0 8] + + if {[catch {set author $blame_data($w,$commit,author)} err]} { + puts $err + set author {} + } + + if {[catch {set atime $blame_data($w,$commit,author-time)}]} { + set atime {} + } else { + set atime [clock format $atime -format {%Y-%m-%d %T}] + } + + while {$n > 0} { + $w_commit delete $lno.0 "$lno.0 lineend" + $w_author delete $lno.0 "$lno.0 lineend" + $w_date delete $lno.0 "$lno.0 lineend" + + $w_commit insert $lno.0 $abbrev + $w_author insert $lno.0 $author + $w_date insert $lno.0 $atime + set blame_data($w,line$lno,commit) $commit + + incr n -1 + incr lno + } + } elseif {[regexp {^([a-z-]+) (.*)$} $line line header data]} { + set blame_data($w,$blame_data($w,commit),$header) $data + } + } + + $w_commit conf -state disabled + $w_author conf -state disabled + $w_date conf -state disabled + + if {[eof $fd]} { + close $fd + set blame_status($w) {Annotation complete.} + } +} + +proc blame_highlight {w pos args} { + set lno [lindex [split [$w index $pos] .] 0] + foreach i $args { + $i tag remove in_sel 0.0 end + } + if {$lno eq {}} return + foreach i $args { + $i tag add in_sel $lno.0 "$lno.0 + 1 line" + } +} + +proc blame_copycommit {w i pos} { + global blame_data + set lno [lindex [split [$i index $pos] .] 0] + if {![catch {set commit $blame_data($w,line$lno,commit)}]} { + clipboard clear + clipboard append \ + -format STRING \ + -type STRING \ + -- $commit + } +} + +###################################################################### +## ## icons set filemask { @@ -3021,6 +3446,24 @@ static unsigned char file_merge_bits[] = { 0xfa, 0x17, 0x02, 0x10, 0xfe, 0x1f}; } -maskdata $filemask +image create bitmap file_dir -background white -foreground blue -data { +#define mod_width 14 +#define mod_height 15 +static unsigned char mod_bits[] = { + 0xfe, 0x01, 0x02, 0x03, 0x7a, 0x05, 0x02, 0x09, 0x7a, 0x1f, 0x02, 0x10, + 0xfa, 0x17, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10, + 0xfa, 0x17, 0x02, 0x10, 0xfe, 0x1f}; +} -maskdata $filemask + +image create bitmap file_uplevel -background white -foreground blue -data { +#define mod_width 14 +#define mod_height 15 +static unsigned char mod_bits[] = { + 0xfe, 0x01, 0x02, 0x03, 0x7a, 0x05, 0x02, 0x09, 0x7a, 0x1f, 0x02, 0x10, + 0xfa, 0x17, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10, + 0xfa, 0x17, 0x02, 0x10, 0xfe, 0x1f}; +} -maskdata $filemask + set ui_index .vpane.files.index.list set ui_workdir .vpane.files.workdir.list @@ -3077,6 +3520,15 @@ proc bind_button3 {w cmd} { } } +proc scrollbar2many {list mode args} { + foreach w $list {eval $w $mode $args} +} + +proc many2scrollbar {list mode sb top bottom} { + $sb set $top $bottom + foreach w $list {$w $mode moveto $top} +} + proc incr_font_size {font {amt 1}} { set sz [font configure $font -size] incr sz $amt @@ -4251,6 +4703,13 @@ if {!$single_commit} { # -- Repository Menu # menu .mbar.repository + +.mbar.repository add command \ + -label {Browse Current Branch} \ + -command new_browser \ + -font font_ui +.mbar.repository add separator + .mbar.repository add command \ -label {Visualize Current Branch} \ -command {do_gitk {}} \ -- 2.11.4.GIT