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