1# git-gui blame viewer 2# Copyright (C) 2006, 2007 Shawn Pearce 3 4class blame { 5 6image create photo ::blame::img_back_arrow -data {R0lGODlhGAAYAIUAAPwCBEzKXFTSZIz+nGzmhGzqfGTidIT+nEzGXHTqhGzmfGzifFzadETCVES+VARWDFzWbHzyjAReDGTadFTOZDSyRDyyTCymPARaFGTedFzSbDy2TCyqRCyqPARaDAyCHES6VDy6VCyiPAR6HCSeNByWLARyFARiDARqFGTifARiFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAAYABgAAAajQIBwSCwaj8ikcsk0BppJwRPqHEypQwHBis0WDAdEFyBIKBaMAKLBdjQeSkFBYTBAIvgEoS6JmhUTEwIUDQ4VFhcMGEhyCgoZExoUaxsWHB0THkgfAXUGAhoBDSAVFR0XBnCbDRmgog0hpSIiDJpJIyEQhBUcJCIlwA22SSYVogknEg8eD82qSigdDSknY0IqJQXPYxIl1dZCGNvWw+Dm510GQQAh/mhDcmVhdGVkIGJ5IEJNUFRvR0lGIFBybyB2ZXJzaW9uIDIuNQ0KqSBEZXZlbENvciAxOTk3LDE5OTguIEFsbCByaWdodHMgcmVzZXJ2ZWQuDQpodHRwOi8vd3d3LmRldmVsY29yLmNvbQA7} 7 8# Persistent data (survives loads) 9# 10field history {}; # viewer history: {commit path} 11field header ; # array commit,key -> header field 12 13# Tk UI control paths 14# 15field w ; # top window in this viewer 16field w_back ; # our back button 17field w_path ; # label showing the current file path 18field w_columns ; # list of all column widgets in the viewer 19field w_line ; # text column: all line numbers 20field w_amov ; # text column: annotations + move tracking 21field w_asim ; # text column: annotations (simple computation) 22field w_file ; # text column: actual file data 23field w_cviewer ; # pane showing commit message 24field finder ; # find mini-dialog frame 25field gotoline ; # line goto mini-dialog frame 26field status ; # status mega-widget instance 27field status_operation ; # operation displayed by status mega-widget 28field old_height ; # last known height of $w.file_pane 29 30 31# Tk UI colors 32# 33variable active_color #c0edc5 34variable group_colors { 35 #d6d6d6 36 #e1e1e1 37 #ececec 38} 39 40# Current blame data; cleared/reset on each load 41# 42field commit ; # input commit to blame 43field path ; # input filename to view in $commit 44 45field current_fd {} ; # background process running 46field highlight_line -1 ; # current line selected 47field highlight_column {} ; # current commit column selected 48field highlight_commit {} ; # sha1 of commit selected 49 50field total_lines 0 ; # total length of file 51field blame_lines 0 ; # number of lines computed 52field amov_data ; # list of {commit origfile origline} 53field asim_data ; # list of {commit origfile origline} 54 55field r_commit ; # commit currently being parsed 56field r_orig_line ; # original line number 57field r_final_line ; # final line number 58field r_line_count ; # lines in this region 59 60field tooltip_wm {} ; # Current tooltip toplevel, if open 61field tooltip_t {} ; # Text widget in $tooltip_wm 62field tooltip_timer {} ; # Current timer event for our tooltip 63field tooltip_commit {} ; # Commit(s) in tooltip 64 65constructor new {i_commit i_path i_jump} { 66 global cursor_ptr M1B M1T have_tk85 use_ttk NS 67 variable active_color 68 variable group_colors 69 70 set commit $i_commit 71 set path $i_path 72 73 make_toplevel top w 74 wm title $top [mc "%s (%s): File Viewer" [appname] [reponame]] 75 76 set font_w [font measure font_diff "0"] 77 78 gold_frame $w.header 79 tlabel $w.header.commit_l \ 80 -text [mc "Commit:"] \ 81 -background gold \ 82 -foreground black \ 83 -anchor w \ 84 -justify left 85 set w_back $w.header.commit_b 86 tlabel $w_back \ 87 -image ::blame::img_back_arrow \ 88 -borderwidth 0 \ 89 -relief flat \ 90 -state disabled \ 91 -background gold \ 92 -foreground black \ 93 -activebackground gold 94 bind $w_back <Button-1> " 95 if {\[$w_back cget -state\] eq {normal}} { 96 [cb _history_menu] 97 } 98 " 99 tlabel $w.header.commit \ 100 -textvariable @commit \ 101 -background gold \ 102 -foreground black \ 103 -anchor w \ 104 -justify left 105 tlabel $w.header.path_l \ 106 -text [mc "File:"] \ 107 -background gold \ 108 -foreground black \ 109 -anchor w \ 110 -justify left 111 set w_path $w.header.path 112 tlabel $w_path \ 113 -background gold \ 114 -foreground black \ 115 -anchor w \ 116 -justify left 117 pack $w.header.commit_l -side left 118 pack $w_back -side left 119 pack $w.header.commit -side left 120 pack $w_path -fill x -side right 121 pack $w.header.path_l -side right 122 123 panedwindow $w.file_pane -orient vertical -borderwidth 0 -sashwidth 3 124 frame $w.file_pane.out -relief flat -borderwidth 1 125 frame $w.file_pane.cm -relief sunken -borderwidth 1 126 $w.file_pane add $w.file_pane.out \ 127 -sticky nsew \ 128 -minsize 100 \ 129 -height 100 \ 130 -width 100 131 $w.file_pane add $w.file_pane.cm \ 132 -sticky nsew \ 133 -minsize 25 \ 134 -height 25 \ 135 -width 100 136 137 set w_line $w.file_pane.out.linenumber_t 138 text $w_line \ 139 -takefocus 0 \ 140 -highlightthickness 0 \ 141 -padx 0 -pady 0 \ 142 -background white \ 143 -foreground black \ 144 -borderwidth 0 \ 145 -state disabled \ 146 -wrap none \ 147 -height 40 \ 148 -width 6 \ 149 -font font_diff 150 $w_line tag conf linenumber -justify right -rmargin 5 151 152 set w_amov $w.file_pane.out.amove_t 153 text $w_amov \ 154 -takefocus 0 \ 155 -highlightthickness 0 \ 156 -padx 0 -pady 0 \ 157 -background white \ 158 -foreground black \ 159 -borderwidth 0 \ 160 -state disabled \ 161 -wrap none \ 162 -height 40 \ 163 -width 5 \ 164 -font font_diff 165 $w_amov tag conf author_abbr -justify right -rmargin 5 166 $w_amov tag conf curr_commit 167 $w_amov tag conf prior_commit -foreground blue -underline 1 168 $w_amov tag bind prior_commit \ 169 <Button-1> \ 170 "[cb _load_commit $w_amov @amov_data @%x,%y];break" 171 172 set w_asim $w.file_pane.out.asimple_t 173 text $w_asim \ 174 -takefocus 0 \ 175 -highlightthickness 0 \ 176 -padx 0 -pady 0 \ 177 -background white \ 178 -foreground black \ 179 -borderwidth 0 \ 180 -state disabled \ 181 -wrap none \ 182 -height 40 \ 183 -width 4 \ 184 -font font_diff 185 $w_asim tag conf author_abbr -justify right 186 $w_asim tag conf curr_commit 187 $w_asim tag conf prior_commit -foreground blue -underline 1 188 $w_asim tag bind prior_commit \ 189 <Button-1> \ 190 "[cb _load_commit $w_asim @asim_data @%x,%y];break" 191 192 set w_file $w.file_pane.out.file_t 193 text $w_file \ 194 -takefocus 0 \ 195 -highlightthickness 0 \ 196 -padx 0 -pady 0 \ 197 -background white \ 198 -foreground black \ 199 -borderwidth 0 \ 200 -state disabled \ 201 -wrap none \ 202 -height 40 \ 203 -width 80 \ 204 -xscrollcommand [list $w.file_pane.out.sbx set] \ 205 -font font_diff 206 if {$have_tk85} { 207 $w_file configure -inactiveselectbackground darkblue 208 } 209 $w_file tag conf found \ 210 -background yellow 211 212 set w_columns [list $w_amov $w_asim $w_line $w_file] 213 214 ${NS}::scrollbar $w.file_pane.out.sbx \ 215 -orient h \ 216 -command [list $w_file xview] 217 ${NS}::scrollbar $w.file_pane.out.sby \ 218 -orient v \ 219 -command [list scrollbar2many $w_columns yview] 220 eval grid $w_columns $w.file_pane.out.sby -sticky nsew 221 grid conf \ 222 $w.file_pane.out.sbx \ 223 -column 0 \ 224 -columnspan [expr {[llength $w_columns] + 1}] \ 225 -sticky we 226 grid columnconfigure \ 227 $w.file_pane.out \ 228 [expr {[llength $w_columns] - 1}] \ 229 -weight 1 230 grid rowconfigure $w.file_pane.out 0 -weight 1 231 232 set finder [::searchbar::new \ 233 $w.file_pane.out.ff $w_file \ 234 -column 0 \ 235 -columnspan [expr {[llength $w_columns] + 1}] \ 236 ] 237 238 set gotoline [::linebar::new \ 239 $w.file_pane.out.lf $w_file \ 240 -column 0 \ 241 -columnspan [expr {[llength $w_columns] + 1}] \ 242 ] 243 244 set w_cviewer $w.file_pane.cm.t 245 text $w_cviewer \ 246 -background white \ 247 -foreground black \ 248 -borderwidth 0 \ 249 -state disabled \ 250 -wrap none \ 251 -height 10 \ 252 -width 80 \ 253 -xscrollcommand [list $w.file_pane.cm.sbx set] \ 254 -yscrollcommand [list $w.file_pane.cm.sby set] \ 255 -font font_diff 256 $w_cviewer tag conf still_loading \ 257 -font font_uiitalic \ 258 -justify center 259 $w_cviewer tag conf header_key \ 260 -tabs {3c} \ 261 -background $active_color \ 262 -font font_uibold 263 $w_cviewer tag conf header_val \ 264 -background $active_color \ 265 -font font_ui 266 $w_cviewer tag raise sel 267 ${NS}::scrollbar $w.file_pane.cm.sbx \ 268 -orient h \ 269 -command [list $w_cviewer xview] 270 ${NS}::scrollbar $w.file_pane.cm.sby \ 271 -orient v \ 272 -command [list $w_cviewer yview] 273 pack $w.file_pane.cm.sby -side right -fill y 274 pack $w.file_pane.cm.sbx -side bottom -fill x 275 pack $w_cviewer -expand 1 -fill both 276 277 set status [::status_bar::new $w.status] 278 set status_operation {} 279 280 menu $w.ctxm -tearoff 0 281 $w.ctxm add command \ 282 -label [mc "Copy Commit"] \ 283 -command [cb _copycommit] 284 $w.ctxm add separator 285 $w.ctxm add command \ 286 -label [mc "Find Text..."] \ 287 -accelerator F7 \ 288 -command [cb _show_finder] 289 $w.ctxm add command \ 290 -label [mc "Goto Line..."] \ 291 -accelerator "Ctrl-G" \ 292 -command [cb _show_linebar] 293 menu $w.ctxm.enc 294 build_encoding_menu $w.ctxm.enc [cb _setencoding] 295 $w.ctxm add cascade \ 296 -label [mc "Encoding"] \ 297 -menu $w.ctxm.enc 298 $w.ctxm add command \ 299 -label [mc "Do Full Copy Detection"] \ 300 -command [cb _fullcopyblame] 301 $w.ctxm add separator 302 $w.ctxm add command \ 303 -label [mc "Show History Context"] \ 304 -command [cb _gitkcommit] 305 $w.ctxm add command \ 306 -label [mc "Blame Parent Commit"] \ 307 -command [cb _blameparent] 308 309 foreach i $w_columns { 310 for {set g 0} {$g < [llength $group_colors]} {incr g} { 311 $i tag conf color$g -background [lindex $group_colors $g] 312 } 313 314 if {$i eq $w_file} { 315 $w_file tag raise found 316 } 317 $i tag raise sel 318 319 $i conf -cursor $cursor_ptr 320 $i conf -yscrollcommand \ 321 "[list ::searchbar::scrolled $finder] 322 [list many2scrollbar $w_columns yview $w.file_pane.out.sby]" 323 bind $i <Button-1> " 324 [cb _hide_tooltip] 325 [cb _click $i @%x,%y] 326 focus $i 327 " 328 bind $i <Any-Motion> [cb _show_tooltip $i @%x,%y] 329 bind $i <Any-Enter> [cb _hide_tooltip] 330 bind $i <Any-Leave> [cb _hide_tooltip] 331 bind $i <Deactivate> [cb _hide_tooltip] 332 bind_button3 $i " 333 [cb _hide_tooltip] 334 set cursorX %x 335 set cursorY %y 336 set cursorW %W 337 tk_popup $w.ctxm %X %Y 338 " 339 bind $i <Shift-Tab> "[list focus $w_cviewer];break" 340 bind $i <Tab> "[cb _focus_search $w_cviewer];break" 341 } 342 343 foreach i [concat $w_columns $w_cviewer] { 344 bind $i <Key-Up> {catch {%W yview scroll -1 units};break} 345 bind $i <Key-Down> {catch {%W yview scroll 1 units};break} 346 bind $i <Key-Left> {catch {%W xview scroll -1 units};break} 347 bind $i <Key-Right> {catch {%W xview scroll 1 units};break} 348 bind $i <Key-k> {catch {%W yview scroll -1 units};break} 349 bind $i <Key-j> {catch {%W yview scroll 1 units};break} 350 bind $i <Key-h> {catch {%W xview scroll -1 units};break} 351 bind $i <Key-l> {catch {%W xview scroll 1 units};break} 352 bind $i <Control-Key-b> {catch {%W yview scroll -1 pages};break} 353 bind $i <Control-Key-f> {catch {%W yview scroll 1 pages};break} 354 } 355 356 bind $w_cviewer <Shift-Tab> "[cb _focus_search $w_file];break" 357 bind $w_cviewer <Tab> "[list focus $w_file];break" 358 bind $w_cviewer <Button-1> [list focus $w_cviewer] 359 bind $w_file <Visibility> [cb _focus_search $w_file] 360 bind $top <F7> [cb _show_finder] 361 bind $top <Key-slash> [cb _show_finder] 362 bind $top <Control-Key-s> [cb _show_finder] 363 bind $top <Escape> [list searchbar::hide $finder] 364 bind $top <F3> [list searchbar::find_next $finder] 365 bind $top <Shift-F3> [list searchbar::find_prev $finder] 366 bind $top <Control-Key-g> [cb _show_linebar] 367 catch { bind $top <Shift-Key-XF86_Switch_VT_3> [list searchbar::find_prev $finder] } 368 369 grid configure $w.header -sticky ew 370 grid configure $w.file_pane -sticky nsew 371 grid configure $w.status -sticky ew 372 grid columnconfigure $top 0 -weight 1 373 grid rowconfigure $top 0 -weight 0 374 grid rowconfigure $top 1 -weight 1 375 grid rowconfigure $top 2 -weight 0 376 377 set req_w [winfo reqwidth $top] 378 set req_h [winfo reqheight $top] 379 set scr_w [expr {[winfo screenwidth $top] - 40}] 380 set scr_h [expr {[winfo screenheight $top] - 120}] 381 set opt_w [expr {$font_w * (80 + 5*3 + 3)}] 382 if {$req_w < $opt_w} {set req_w $opt_w} 383 if {$req_w > $scr_w} {set req_w $scr_w} 384 set opt_h [expr {$req_w*4/3}] 385 if {$req_h < $scr_h} {set req_h $scr_h} 386 if {$req_h > $opt_h} {set req_h $opt_h} 387 set g "${req_w}x${req_h}" 388 wm geometry $top $g 389 update 390 391 set old_height [winfo height $w.file_pane] 392 $w.file_pane sash place 0 \ 393 [lindex [$w.file_pane sash coord 0] 0] \ 394 [expr {int($old_height * 0.80)}] 395 bind $w.file_pane <Configure> \ 396 "if {{$w.file_pane} eq {%W}} {[cb _resize %h]}" 397 398 wm protocol $top WM_DELETE_WINDOW "destroy $top" 399 bind $top <Destroy> [cb _handle_destroy %W] 400 401 _load $this $i_jump 402} 403 404method _focus_search {win} { 405 if {[searchbar::visible $finder]} { 406 focus [searchbar::editor $finder] 407 } else { 408 focus $win 409 } 410} 411 412method _handle_destroy {win} { 413 if {$win eq $w} { 414 _kill $this 415 delete_this 416 } 417} 418 419method _kill {} { 420 if {$current_fd ne {}} { 421 kill_file_process $current_fd 422 catch {close $current_fd} 423 set current_fd {} 424 } 425} 426 427method _load {jump} { 428 variable group_colors 429 430 _hide_tooltip $this 431 432 if {$total_lines != 0 || $current_fd ne {}} { 433 _kill $this 434 435 foreach i $w_columns { 436 $i conf -state normal 437 $i delete 0.0 end 438 foreach g [$i tag names] { 439 if {[regexp {^g[0-9a-f]{40}$} $g]} { 440 $i tag delete $g 441 } 442 } 443 $i conf -state disabled 444 } 445 446 $w_cviewer conf -state normal 447 $w_cviewer delete 0.0 end 448 $w_cviewer conf -state disabled 449 450 set highlight_line -1 451 set highlight_column {} 452 set highlight_commit {} 453 set total_lines 0 454 } 455 456 if {$history eq {}} { 457 $w_back conf -state disabled 458 } else { 459 $w_back conf -state normal 460 } 461 462 # Index 0 is always empty. There is never line 0 as 463 # we use only 1 based lines, as that matches both with 464 # git-blame output and with Tk's text widget. 465 # 466 set amov_data [list [list]] 467 set asim_data [list [list]] 468 469 $status show [mc "Reading %s..." "$commit:[escape_path $path]"] 470 $w_path conf -text [escape_path $path] 471 472 set do_textconv 0 473 if {![is_config_false gui.textconv] && [git-version >= 1.7.2]} { 474 set filter [gitattr $path diff set] 475 set textconv [get_config [join [list diff $filter textconv] .]] 476 if {$filter ne {set} && $textconv ne {}} { 477 set do_textconv 1 478 } 479 } 480 if {$commit eq {}} { 481 if {$do_textconv ne 0} { 482 set fd [open_cmd_pipe $textconv $path] 483 } else { 484 set fd [open $path r] 485 } 486 fconfigure $fd -eofchar {} 487 } else { 488 if {$do_textconv ne 0} { 489 set fd [git_read cat-file --textconv "$commit:$path"] 490 } else { 491 set fd [git_read cat-file blob "$commit:$path"] 492 } 493 } 494 fconfigure $fd \ 495 -blocking 0 \ 496 -translation lf \ 497 -encoding [get_path_encoding $path] 498 fileevent $fd readable [cb _read_file $fd $jump] 499 set current_fd $fd 500} 501 502method _history_menu {} { 503 set m $w.backmenu 504 if {[winfo exists $m]} { 505 $m delete 0 end 506 } else { 507 menu $m -tearoff 0 508 } 509 510 for {set i [expr {[llength $history] - 1}] 511 } {$i >= 0} {incr i -1} { 512 set e [lindex $history $i] 513 set c [lindex $e 0] 514 set f [lindex $e 1] 515 516 if {[regexp {^[0-9a-f]{40}$} $c]} { 517 set t [string range $c 0 8]... 518 } elseif {$c eq {}} { 519 set t {Working Directory} 520 } else { 521 set t $c 522 } 523 if {![catch {set summary $header($c,summary)}]} { 524 append t " $summary" 525 if {[string length $t] > 70} { 526 set t [string range $t 0 66]... 527 } 528 } 529 530 $m add command -label $t -command [cb _goback $i] 531 } 532 set X [winfo rootx $w_back] 533 set Y [expr {[winfo rooty $w_back] + [winfo height $w_back]}] 534 tk_popup $m $X $Y 535} 536 537method _goback {i} { 538 set dat [lindex $history $i] 539 set history [lrange $history 0 [expr {$i - 1}]] 540 set commit [lindex $dat 0] 541 set path [lindex $dat 1] 542 _load $this [lrange $dat 2 5] 543} 544 545method _read_file {fd jump} { 546 if {$fd ne $current_fd} { 547 catch {close $fd} 548 return 549 } 550 551 foreach i $w_columns {$i conf -state normal} 552 while {[gets $fd line] >= 0} { 553 regsub "\r\$" $line {} line 554 incr total_lines 555 lappend amov_data {} 556 lappend asim_data {} 557 558 if {$total_lines > 1} { 559 foreach i $w_columns {$i insert end "\n"} 560 } 561 562 $w_line insert end "$total_lines" linenumber 563 $w_file insert end "$line" 564 } 565 566 set ln_wc [expr {[string length $total_lines] + 2}] 567 if {[$w_line cget -width] < $ln_wc} { 568 $w_line conf -width $ln_wc 569 } 570 571 foreach i $w_columns {$i conf -state disabled} 572 573 if {[eof $fd]} { 574 fconfigure $fd -blocking 1; # enable error reporting on close 575 if {[catch {close $fd} err]} { 576 tk_messageBox -icon error -title [mc Error] \ 577 -message $err 578 } 579 580 # If we don't force Tk to update the widgets *right now* 581 # none of our jump commands will cause a change in the UI. 582 # 583 update 584 585 if {[llength $jump] == 1} { 586 set highlight_line [lindex $jump 0] 587 $w_file see "$highlight_line.0" 588 } elseif {[llength $jump] == 4} { 589 set highlight_column [lindex $jump 0] 590 set highlight_line [lindex $jump 1] 591 $w_file xview moveto [lindex $jump 2] 592 $w_file yview moveto [lindex $jump 3] 593 } 594 595 _exec_blame $this $w_asim @asim_data \ 596 [list] \ 597 [mc "Loading copy/move tracking annotations..."] 598 } 599} ifdeleted { catch {close $fd} } 600 601method _exec_blame {cur_w cur_d options cur_s} { 602 lappend options --incremental --encoding=utf-8 603 if {$commit eq {}} { 604 lappend options --contents $path 605 } else { 606 lappend options $commit 607 } 608 609 # We may recurse in from another call to _exec_blame and already have 610 # a status operation. 611 if {$status_operation == {}} { 612 set status_operation [$status start \ 613 $cur_s \ 614 [mc "lines annotated"]] 615 } else { 616 $status_operation restart $cur_s 617 } 618 619 lappend options -- $path 620 set fd [eval git_read --nice blame $options] 621 fconfigure $fd -blocking 0 -translation lf -encoding utf-8 622 fileevent $fd readable [cb _read_blame $fd $cur_w $cur_d] 623 set current_fd $fd 624 set blame_lines 0 625} 626 627method _read_blame {fd cur_w cur_d} { 628 upvar #0 $cur_d line_data 629 variable group_colors 630 631 if {$fd ne $current_fd} { 632 catch {close $fd} 633 return 634 } 635 636 $cur_w conf -state normal 637 while {[gets $fd line] >= 0} { 638 if {[regexp {^([a-z0-9]{40}) (\d+) (\d+) (\d+)$} $line line \ 639 cmit original_line final_line line_count]} { 640 set r_commit $cmit 641 set r_orig_line $original_line 642 set r_final_line $final_line 643 set r_line_count $line_count 644 } elseif {[string match {filename *} $line]} { 645 set file [string range $line 9 end] 646 set n $r_line_count 647 set lno $r_final_line 648 set oln $r_orig_line 649 set cmit $r_commit 650 651 if {[regexp {^0{40}$} $cmit]} { 652 set commit_abbr work 653 set commit_type curr_commit 654 } elseif {$cmit eq $commit} { 655 set commit_abbr this 656 set commit_type curr_commit 657 } else { 658 set commit_type prior_commit 659 set commit_abbr [string range $cmit 0 3] 660 } 661 662 set author_abbr {} 663 set a_name {} 664 catch {set a_name $header($cmit,author)} 665 while {$a_name ne {}} { 666 if {$author_abbr ne {} 667 && [string index $a_name 0] eq {'}} { 668 regsub {^'[^']+'\s+} $a_name {} a_name 669 } 670 if {![regexp {^([[:upper:]])} $a_name _a]} break 671 append author_abbr $_a 672 unset _a 673 if {![regsub \ 674 {^[[:upper:]][^\s]*\s+} \ 675 $a_name {} a_name ]} break 676 } 677 if {$author_abbr eq {}} { 678 set author_abbr { |} 679 } else { 680 set author_abbr [string range $author_abbr 0 3] 681 } 682 unset a_name 683 684 set first_lno $lno 685 while { 686 $first_lno > 1 687 && $cmit eq [lindex $line_data [expr {$first_lno - 1}] 0] 688 && $file eq [lindex $line_data [expr {$first_lno - 1}] 1] 689 } { 690 incr first_lno -1 691 } 692 693 set color {} 694 if {$first_lno < $lno} { 695 foreach g [$w_file tag names $first_lno.0] { 696 if {[regexp {^color[0-9]+$} $g]} { 697 set color $g 698 break 699 } 700 } 701 } else { 702 set i [lsort [concat \ 703 [$w_file tag names "[expr {$first_lno - 1}].0"] \ 704 [$w_file tag names "[expr {$lno + $n}].0"] \ 705 ]] 706 for {set g 0} {$g < [llength $group_colors]} {incr g} { 707 if {[lsearch -sorted -exact $i color$g] == -1} { 708 set color color$g 709 break 710 } 711 } 712 } 713 if {$color eq {}} { 714 set color color0 715 } 716 717 while {$n > 0} { 718 set lno_e "$lno.0 lineend + 1c" 719 if {[lindex $line_data $lno] ne {}} { 720 set g [lindex $line_data $lno 0] 721 foreach i $w_columns { 722 $i tag remove g$g $lno.0 $lno_e 723 } 724 } 725 lset line_data $lno [list $cmit $file $oln] 726 727 $cur_w delete $lno.0 "$lno.0 lineend" 728 if {$lno == $first_lno} { 729 $cur_w insert $lno.0 $commit_abbr $commit_type 730 } elseif {$lno == [expr {$first_lno + 1}]} { 731 $cur_w insert $lno.0 $author_abbr author_abbr 732 } else { 733 $cur_w insert $lno.0 { |} 734 } 735 736 foreach i $w_columns { 737 if {$cur_w eq $w_amov} { 738 for {set g 0} \ 739 {$g < [llength $group_colors]} \ 740 {incr g} { 741 $i tag remove color$g $lno.0 $lno_e 742 } 743 $i tag add $color $lno.0 $lno_e 744 } 745 $i tag add g$cmit $lno.0 $lno_e 746 } 747 748 if {$highlight_column eq $cur_w} { 749 if {$highlight_line == -1 750 && [lindex [$w_file yview] 0] == 0} { 751 $w_file see $lno.0 752 set highlight_line $lno 753 } 754 if {$highlight_line == $lno} { 755 _showcommit $this $cur_w $lno 756 } 757 } 758 759 incr n -1 760 incr lno 761 incr oln 762 incr blame_lines 763 } 764 765 while { 766 $cmit eq [lindex $line_data $lno 0] 767 && $file eq [lindex $line_data $lno 1] 768 } { 769 $cur_w delete $lno.0 "$lno.0 lineend" 770 771 if {$lno == $first_lno} { 772 $cur_w insert $lno.0 $commit_abbr $commit_type 773 } elseif {$lno == [expr {$first_lno + 1}]} { 774 $cur_w insert $lno.0 $author_abbr author_abbr 775 } else { 776 $cur_w insert $lno.0 { |} 777 } 778 779 if {$cur_w eq $w_amov} { 780 foreach i $w_columns { 781 for {set g 0} \ 782 {$g < [llength $group_colors]} \ 783 {incr g} { 784 $i tag remove color$g $lno.0 $lno_e 785 } 786 $i tag add $color $lno.0 $lno_e 787 } 788 } 789 790 incr lno 791 } 792 793 } elseif {[regexp {^([a-z-]+) (.*)$} $line line key data]} { 794 set header($r_commit,$key) $data 795 } 796 } 797 $cur_w conf -state disabled 798 799 if {[eof $fd]} { 800 close $fd 801 if {$cur_w eq $w_asim} { 802 # Switches for original location detection 803 set threshold [get_config gui.copyblamethreshold] 804 set original_options [list "-C$threshold"] 805 806 if {![is_config_true gui.fastcopyblame]} { 807 # thorough copy search; insert before the threshold 808 set original_options [linsert $original_options 0 -C] 809 } 810 if {[git-version >= 1.5.3]} { 811 lappend original_options -w ; # ignore indentation changes 812 } 813 814 _exec_blame $this $w_amov @amov_data \ 815 $original_options \ 816 [mc "Loading original location annotations..."] 817 } else { 818 set current_fd {} 819 $status_operation stop [mc "Annotation complete."] 820 set status_operation {} 821 } 822 } else { 823 $status_operation update $blame_lines $total_lines 824 } 825} ifdeleted { catch {close $fd} } 826 827method _find_commit_bound {data_list start_idx delta} { 828 upvar #0 $data_list line_data 829 set pos $start_idx 830 set limit [expr {[llength $line_data] - 1}] 831 set base_commit [lindex $line_data $pos 0] 832 833 while {$pos > 0 && $pos < $limit} { 834 set new_pos [expr {$pos + $delta}] 835 if {[lindex $line_data $new_pos 0] ne $base_commit} { 836 return $pos 837 } 838 839 set pos $new_pos 840 } 841 842 return $pos 843} 844 845method _fullcopyblame {} { 846 if {$current_fd ne {}} { 847 tk_messageBox \ 848 -icon error \ 849 -type ok \ 850 -title [mc "Busy"] \ 851 -message [mc "Annotation process is already running."] 852 853 return 854 } 855 856 # Switches for original location detection 857 set threshold [get_config gui.copyblamethreshold] 858 set original_options [list -C -C "-C$threshold"] 859 860 if {[git-version >= 1.5.3]} { 861 lappend original_options -w ; # ignore indentation changes 862 } 863 864 # Find the line range 865 set pos @$::cursorX,$::cursorY 866 set lno [lindex [split [$::cursorW index $pos] .] 0] 867 set min_amov_lno [_find_commit_bound $this @amov_data $lno -1] 868 set max_amov_lno [_find_commit_bound $this @amov_data $lno 1] 869 set min_asim_lno [_find_commit_bound $this @asim_data $lno -1] 870 set max_asim_lno [_find_commit_bound $this @asim_data $lno 1] 871 872 if {$min_asim_lno < $min_amov_lno} { 873 set min_amov_lno $min_asim_lno 874 } 875 876 if {$max_asim_lno > $max_amov_lno} { 877 set max_amov_lno $max_asim_lno 878 } 879 880 lappend original_options -L "$min_amov_lno,$max_amov_lno" 881 882 # Clear lines 883 for {set i $min_amov_lno} {$i <= $max_amov_lno} {incr i} { 884 lset amov_data $i [list ] 885 } 886 887 # Start the back-end process 888 _exec_blame $this $w_amov @amov_data \ 889 $original_options \ 890 [mc "Running thorough copy detection..."] 891} 892 893method _click {cur_w pos} { 894 set lno [lindex [split [$cur_w index $pos] .] 0] 895 _showcommit $this $cur_w $lno 896} 897 898method _setencoding {enc} { 899 force_path_encoding $path $enc 900 _load $this [list \ 901 $highlight_column \ 902 $highlight_line \ 903 [lindex [$w_file xview] 0] \ 904 [lindex [$w_file yview] 0] \ 905 ] 906} 907 908method _load_commit {cur_w cur_d pos} { 909 upvar #0 $cur_d line_data 910 set lno [lindex [split [$cur_w index $pos] .] 0] 911 set dat [lindex $line_data $lno] 912 if {$dat ne {}} { 913 _load_new_commit $this \ 914 [lindex $dat 0] \ 915 [lindex $dat 1] \ 916 [list [lindex $dat 2]] 917 } 918} 919 920method _load_new_commit {new_commit new_path jump} { 921 lappend history [list \ 922 $commit $path \ 923 $highlight_column \ 924 $highlight_line \ 925 [lindex [$w_file xview] 0] \ 926 [lindex [$w_file yview] 0] \ 927 ] 928 929 set commit $new_commit 930 set path $new_path 931 _load $this $jump 932} 933 934method _showcommit {cur_w lno} { 935 global repo_config 936 variable active_color 937 938 if {$highlight_commit ne {}} { 939 foreach i $w_columns { 940 $i tag conf g$highlight_commit -background {} 941 $i tag lower g$highlight_commit 942 } 943 } 944 945 if {$cur_w eq $w_asim} { 946 set dat [lindex $asim_data $lno] 947 set highlight_column $w_asim 948 } else { 949 set dat [lindex $amov_data $lno] 950 set highlight_column $w_amov 951 } 952 953 $w_cviewer conf -state normal 954 $w_cviewer delete 0.0 end 955 956 if {$dat eq {}} { 957 set cmit {} 958 $w_cviewer insert end [mc "Loading annotation..."] still_loading 959 } else { 960 set cmit [lindex $dat 0] 961 set file [lindex $dat 1] 962 963 foreach i $w_columns { 964 $i tag conf g$cmit -background $active_color 965 $i tag raise g$cmit 966 if {$i eq $w_file} { 967 $w_file tag raise found 968 } 969 $i tag raise sel 970 } 971 972 set author_name {} 973 set author_email {} 974 set author_time {} 975 catch {set author_name $header($cmit,author)} 976 catch {set author_email $header($cmit,author-mail)} 977 catch {set author_time [format_date $header($cmit,author-time)]} 978 979 set committer_name {} 980 set committer_email {} 981 set committer_time {} 982 catch {set committer_name $header($cmit,committer)} 983 catch {set committer_email $header($cmit,committer-mail)} 984 catch {set committer_time [format_date $header($cmit,committer-time)]} 985 986 if {[catch {set msg $header($cmit,message)}]} { 987 set msg {} 988 catch { 989 set fd [git_read cat-file commit $cmit] 990 fconfigure $fd -encoding binary -translation lf 991 # By default commits are assumed to be in utf-8 992 set enc utf-8 993 while {[gets $fd line] > 0} { 994 if {[string match {encoding *} $line]} { 995 set enc [string tolower [string range $line 9 end]] 996 } 997 } 998 set msg [read $fd] 999 close $fd 1000 1001 set enc [tcl_encoding $enc] 1002 if {$enc ne {}} { 1003 set msg [encoding convertfrom $enc $msg] 1004 } 1005 set msg [string trim $msg] 1006 } 1007 set header($cmit,message) $msg 1008 } 1009 1010 $w_cviewer insert end "commit $cmit\n" header_key 1011 $w_cviewer insert end [strcat [mc "Author:"] "\t"] header_key 1012 $w_cviewer insert end "$author_name $author_email" header_val 1013 $w_cviewer insert end " $author_time\n" header_val 1014 1015 $w_cviewer insert end [strcat [mc "Committer:"] "\t"] header_key 1016 $w_cviewer insert end "$committer_name $committer_email" header_val 1017 $w_cviewer insert end " $committer_time\n" header_val 1018 1019 if {$file ne $path} { 1020 $w_cviewer insert end [strcat [mc "Original File:"] "\t"] header_key 1021 $w_cviewer insert end "[escape_path $file]\n" header_val 1022 } 1023 1024 $w_cviewer insert end "\n$msg" 1025 } 1026 $w_cviewer conf -state disabled 1027 1028 set highlight_line $lno 1029 set highlight_commit $cmit 1030 1031 if {[lsearch -exact $tooltip_commit $highlight_commit] != -1} { 1032 _hide_tooltip $this 1033 } 1034} 1035 1036method _get_click_amov_info {} { 1037 set pos @$::cursorX,$::cursorY 1038 set lno [lindex [split [$::cursorW index $pos] .] 0] 1039 return [lindex $amov_data $lno] 1040} 1041 1042method _copycommit {} { 1043 set dat [_get_click_amov_info $this] 1044 if {$dat ne {}} { 1045 clipboard clear 1046 clipboard append \ 1047 -format STRING \ 1048 -type STRING \ 1049 -- [lindex $dat 0] 1050 } 1051} 1052 1053method _format_offset_date {base offset} { 1054 set exval [expr {$base + $offset*24*60*60}] 1055 return [clock format $exval -format {%Y-%m-%d}] 1056} 1057 1058method _gitkcommit {} { 1059 global nullid 1060 1061 set dat [_get_click_amov_info $this] 1062 if {$dat ne {}} { 1063 set cmit [lindex $dat 0] 1064 1065 # If the line belongs to the working copy, use HEAD instead 1066 if {$cmit eq $nullid} { 1067 if {[catch {set cmit [git rev-parse --verify HEAD]} err]} { 1068 error_popup [strcat [mc "Cannot find HEAD commit:"] "\n\n$err"] 1069 return; 1070 } 1071 } 1072 1073 set radius [get_config gui.blamehistoryctx] 1074 set cmdline [list --select-commit=$cmit] 1075 1076 if {$radius > 0} { 1077 set author_time {} 1078 set committer_time {} 1079 1080 catch {set author_time $header($cmit,author-time)} 1081 catch {set committer_time $header($cmit,committer-time)} 1082 1083 if {$committer_time eq {}} { 1084 set committer_time $author_time 1085 } 1086 1087 set after_time [_format_offset_date $this $committer_time [expr {-$radius}]] 1088 set before_time [_format_offset_date $this $committer_time $radius] 1089 1090 lappend cmdline --after=$after_time --before=$before_time 1091 } 1092 1093 lappend cmdline $cmit 1094 1095 set base_rev "HEAD" 1096 if {$commit ne {}} { 1097 set base_rev $commit 1098 } 1099 1100 if {$base_rev ne $cmit} { 1101 lappend cmdline $base_rev 1102 } 1103 1104 do_gitk $cmdline 1105 } 1106} 1107 1108method _blameparent {} { 1109 global nullid 1110 1111 set dat [_get_click_amov_info $this] 1112 if {$dat ne {}} { 1113 set cmit [lindex $dat 0] 1114 set new_path [lindex $dat 1] 1115 1116 # Allow using Blame Parent on lines modified in the working copy 1117 if {$cmit eq $nullid} { 1118 set parent_ref "HEAD" 1119 } else { 1120 set parent_ref "$cmit^" 1121 } 1122 if {[catch {set cparent [git rev-parse --verify $parent_ref]} err]} { 1123 error_popup [strcat [mc "Cannot find parent commit:"] "\n\n$err"] 1124 return; 1125 } 1126 1127 _kill $this 1128 1129 # Generate a diff between the commit and its parent, 1130 # and use the hunks to update the line number. 1131 # Request zero context to simplify calculations. 1132 if {$cmit eq $nullid} { 1133 set diffcmd [list diff-index --unified=0 $cparent -- $new_path] 1134 } else { 1135 set diffcmd [list diff-tree --unified=0 $cparent $cmit -- $new_path] 1136 } 1137 if {[catch {set fd [eval git_read $diffcmd]} err]} { 1138 $status_operation stop [mc "Unable to display parent"] 1139 error_popup [strcat [mc "Error loading diff:"] "\n\n$err"] 1140 return 1141 } 1142 1143 set r_orig_line [lindex $dat 2] 1144 1145 fconfigure $fd \ 1146 -blocking 0 \ 1147 -encoding binary \ 1148 -translation binary 1149 fileevent $fd readable [cb _read_diff_load_commit \ 1150 $fd $cparent $new_path $r_orig_line] 1151 set current_fd $fd 1152 } 1153} 1154 1155method _read_diff_load_commit {fd cparent new_path tline} { 1156 if {$fd ne $current_fd} { 1157 catch {close $fd} 1158 return 1159 } 1160 1161 while {[gets $fd line] >= 0} { 1162 if {[regexp {^@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@} $line line \ 1163 old_line osz old_size new_line nsz new_size]} { 1164 1165 if {$osz eq {}} { set old_size 1 } 1166 if {$nsz eq {}} { set new_size 1 } 1167 1168 if {$new_line <= $tline} { 1169 if {[expr {$new_line + $new_size}] > $tline} { 1170 # Target line within the hunk 1171 set line_shift [expr { 1172 ($new_size-$old_size)*($tline-$new_line)/$new_size 1173 }] 1174 } else { 1175 set line_shift [expr {$new_size-$old_size}] 1176 } 1177 1178 set r_orig_line [expr {$r_orig_line - $line_shift}] 1179 } 1180 } 1181 } 1182 1183 if {[eof $fd]} { 1184 close $fd 1185 set current_fd {} 1186 1187 _load_new_commit $this \ 1188 $cparent \ 1189 $new_path \ 1190 [list $r_orig_line] 1191 } 1192} ifdeleted { catch {close $fd} } 1193 1194method _show_tooltip {cur_w pos} { 1195 if {$tooltip_wm ne {}} { 1196 _open_tooltip $this $cur_w 1197 } elseif {$tooltip_timer eq {}} { 1198 set tooltip_timer [after 1000 [cb _open_tooltip $cur_w]] 1199 } 1200} 1201 1202method _open_tooltip {cur_w} { 1203 set tooltip_timer {} 1204 set pos_x [winfo pointerx $cur_w] 1205 set pos_y [winfo pointery $cur_w] 1206 if {[winfo containing $pos_x $pos_y] ne $cur_w} { 1207 _hide_tooltip $this 1208 return 1209 } 1210 1211 if {$tooltip_wm ne "$cur_w.tooltip"} { 1212 _hide_tooltip $this 1213 1214 set tooltip_wm [toplevel $cur_w.tooltip -borderwidth 1] 1215 catch {wm attributes $tooltip_wm -type tooltip} 1216 wm overrideredirect $tooltip_wm 1 1217 wm transient $tooltip_wm [winfo toplevel $cur_w] 1218 set tooltip_t $tooltip_wm.label 1219 text $tooltip_t \ 1220 -takefocus 0 \ 1221 -highlightthickness 0 \ 1222 -relief flat \ 1223 -borderwidth 0 \ 1224 -wrap none \ 1225 -background lightyellow \ 1226 -foreground black 1227 $tooltip_t tag conf section_header -font font_uibold 1228 pack $tooltip_t 1229 } else { 1230 $tooltip_t conf -state normal 1231 $tooltip_t delete 0.0 end 1232 } 1233 1234 set pos @[join [list \ 1235 [expr {$pos_x - [winfo rootx $cur_w]}] \ 1236 [expr {$pos_y - [winfo rooty $cur_w]}]] ,] 1237 set lno [lindex [split [$cur_w index $pos] .] 0] 1238 if {$cur_w eq $w_amov} { 1239 set dat [lindex $amov_data $lno] 1240 set org {} 1241 } else { 1242 set dat [lindex $asim_data $lno] 1243 set org [lindex $amov_data $lno] 1244 } 1245 1246 if {$dat eq {}} { 1247 _hide_tooltip $this 1248 return 1249 } 1250 1251 set cmit [lindex $dat 0] 1252 set tooltip_commit [list $cmit] 1253 1254 set author_name {} 1255 set summary {} 1256 set author_time {} 1257 catch {set author_name $header($cmit,author)} 1258 catch {set summary $header($cmit,summary)} 1259 catch {set author_time [format_date $header($cmit,author-time)]} 1260 1261 $tooltip_t insert end "commit $cmit\n" 1262 $tooltip_t insert end "$author_name $author_time\n" 1263 $tooltip_t insert end "$summary" 1264 1265 if {$org ne {} && [lindex $org 0] ne $cmit} { 1266 set save [$tooltip_t get 0.0 end] 1267 $tooltip_t delete 0.0 end 1268 1269 set cmit [lindex $org 0] 1270 set file [lindex $org 1] 1271 lappend tooltip_commit $cmit 1272 1273 set author_name {} 1274 set summary {} 1275 set author_time {} 1276 catch {set author_name $header($cmit,author)} 1277 catch {set summary $header($cmit,summary)} 1278 catch {set author_time [format_date $header($cmit,author-time)]} 1279 1280 $tooltip_t insert end [strcat [mc "Originally By:"] "\n"] section_header 1281 $tooltip_t insert end "commit $cmit\n" 1282 $tooltip_t insert end "$author_name $author_time\n" 1283 $tooltip_t insert end "$summary\n" 1284 1285 if {$file ne $path} { 1286 $tooltip_t insert end [strcat [mc "In File:"] " "] section_header 1287 $tooltip_t insert end "$file\n" 1288 } 1289 1290 $tooltip_t insert end "\n" 1291 $tooltip_t insert end [strcat [mc "Copied Or Moved Here By:"] "\n"] section_header 1292 $tooltip_t insert end $save 1293 } 1294 1295 $tooltip_t conf -state disabled 1296 _position_tooltip $this 1297 1298 # On MacOS raising a window causes it to acquire focus. 1299 # Tk 8.5 on MacOS seems to properly support wm transient, 1300 # so we can safely counter the effect there. 1301 if {$::have_tk85 && [is_MacOSX]} { 1302 update 1303 if {$w eq {}} { 1304 raise . 1305 } else { 1306 raise $w 1307 } 1308 } 1309} 1310 1311method _position_tooltip {} { 1312 set max_h [lindex [split [$tooltip_t index end] .] 0] 1313 set max_w 0 1314 for {set i 1} {$i <= $max_h} {incr i} { 1315 set c [lindex [split [$tooltip_t index "$i.0 lineend"] .] 1] 1316 if {$c > $max_w} {set max_w $c} 1317 } 1318 $tooltip_t conf -width $max_w -height $max_h 1319 1320 set req_w [winfo reqwidth $tooltip_t] 1321 set req_h [winfo reqheight $tooltip_t] 1322 set pos_x [expr {[winfo pointerx .] + 5}] 1323 set pos_y [expr {[winfo pointery .] + 10}] 1324 1325 set g "${req_w}x${req_h}" 1326 if {[tk windowingsystem] eq "win32" || $pos_x >= 0} {append g +} 1327 append g $pos_x 1328 if {[tk windowingsystem] eq "win32" || $pos_y >= 0} {append g +} 1329 append g $pos_y 1330 1331 wm geometry $tooltip_wm $g 1332 if {![is_MacOSX]} { 1333 raise $tooltip_wm 1334 } 1335} 1336 1337method _hide_tooltip {} { 1338 if {$tooltip_wm ne {}} { 1339 destroy $tooltip_wm 1340 set tooltip_wm {} 1341 set tooltip_commit {} 1342 } 1343 if {$tooltip_timer ne {}} { 1344 after cancel $tooltip_timer 1345 set tooltip_timer {} 1346 } 1347} 1348 1349method _resize {new_height} { 1350 set diff [expr {$new_height - $old_height}] 1351 if {$diff == 0} return 1352 1353 set my [expr {[winfo height $w.file_pane] - 25}] 1354 set o [$w.file_pane sash coord 0] 1355 set ox [lindex $o 0] 1356 set oy [expr {[lindex $o 1] + $diff}] 1357 if {$oy < 0} {set oy 0} 1358 if {$oy > $my} {set oy $my} 1359 $w.file_pane sash place 0 $ox $oy 1360 1361 set old_height $new_height 1362} 1363 1364method _show_finder {} { 1365 linebar::hide $gotoline 1366 searchbar::show $finder 1367} 1368 1369method _show_linebar {} { 1370 searchbar::hide $finder 1371 linebar::show $gotoline 1372} 1373 1374} 1375