Git 2.28-rc1
[alt-git.git] / git-gui / lib / blame.tcl
blob62ec083667688dc6648952fc2be117aa5247fcc1
1 # git-gui blame viewer
2 # Copyright (C) 2006, 2007 Shawn Pearce
4 class blame {
6 image create photo ::blame::img_back_arrow -data {R0lGODlhGAAYAIUAAPwCBEzKXFTSZIz+nGzmhGzqfGTidIT+nEzGXHTqhGzmfGzifFzadETCVES+VARWDFzWbHzyjAReDGTadFTOZDSyRDyyTCymPARaFGTedFzSbDy2TCyqRCyqPARaDAyCHES6VDy6VCyiPAR6HCSeNByWLARyFARiDARqFGTifARiFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAAYABgAAAajQIBwSCwaj8ikcsk0BppJwRPqHEypQwHBis0WDAdEFyBIKBaMAKLBdjQeSkFBYTBAIvgEoS6JmhUTEwIUDQ4VFhcMGEhyCgoZExoUaxsWHB0THkgfAXUGAhoBDSAVFR0XBnCbDRmgog0hpSIiDJpJIyEQhBUcJCIlwA22SSYVogknEg8eD82qSigdDSknY0IqJQXPYxIl1dZCGNvWw+Dm510GQQAh/mhDcmVhdGVkIGJ5IEJNUFRvR0lGIFBybyB2ZXJzaW9uIDIuNQ0KqSBEZXZlbENvciAxOTk3LDE5OTguIEFsbCByaWdodHMgcmVzZXJ2ZWQuDQpodHRwOi8vd3d3LmRldmVsY29yLmNvbQA7}
8 # Persistent data (survives loads)
10 field history {}; # viewer history: {commit path}
11 field header ; # array commit,key -> header field
13 # Tk UI control paths
15 field w ; # top window in this viewer
16 field w_back ; # our back button
17 field w_path ; # label showing the current file path
18 field w_columns ; # list of all column widgets in the viewer
19 field w_line ; # text column: all line numbers
20 field w_amov ; # text column: annotations + move tracking
21 field w_asim ; # text column: annotations (simple computation)
22 field w_file ; # text column: actual file data
23 field w_cviewer ; # pane showing commit message
24 field finder ; # find mini-dialog frame
25 field gotoline ; # line goto mini-dialog frame
26 field status ; # status mega-widget instance
27 field status_operation ; # operation displayed by status mega-widget
28 field old_height ; # last known height of $w.file_pane
31 # Tk UI colors
33 variable active_color #c0edc5
34 variable group_colors {
35 #d6d6d6
36 #e1e1e1
37 #ececec
40 # Current blame data; cleared/reset on each load
42 field commit ; # input commit to blame
43 field path ; # input filename to view in $commit
45 field current_fd {} ; # background process running
46 field highlight_line -1 ; # current line selected
47 field highlight_column {} ; # current commit column selected
48 field highlight_commit {} ; # sha1 of commit selected
50 field total_lines 0 ; # total length of file
51 field blame_lines 0 ; # number of lines computed
52 field amov_data ; # list of {commit origfile origline}
53 field asim_data ; # list of {commit origfile origline}
55 field r_commit ; # commit currently being parsed
56 field r_orig_line ; # original line number
57 field r_final_line ; # final line number
58 field r_line_count ; # lines in this region
60 field tooltip_wm {} ; # Current tooltip toplevel, if open
61 field tooltip_t {} ; # Text widget in $tooltip_wm
62 field tooltip_timer {} ; # Current timer event for our tooltip
63 field tooltip_commit {} ; # Commit(s) in tooltip
65 constructor 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
70 set commit $i_commit
71 set path $i_path
73 make_toplevel top w
74 wm title $top [mc "%s (%s): File Viewer" [appname] [reponame]]
76 set font_w [font measure font_diff "0"]
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]
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
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
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
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"
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"
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
209 $w_file tag conf found \
210 -background yellow
212 set w_columns [list $w_amov $w_asim $w_line $w_file]
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
232 set finder [::searchbar::new \
233 $w.file_pane.out.ff $w_file \
234 -column 0 \
235 -columnspan [expr {[llength $w_columns] + 1}] \
238 set gotoline [::linebar::new \
239 $w.file_pane.out.lf $w_file \
240 -column 0 \
241 -columnspan [expr {[llength $w_columns] + 1}] \
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
277 set status [::status_bar::new $w.status]
278 set status_operation {}
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]
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]
314 if {$i eq $w_file} {
315 $w_file tag raise found
317 $i tag raise sel
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
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_button3 $i "
332 [cb _hide_tooltip]
333 set cursorX %x
334 set cursorY %y
335 set cursorW %W
336 tk_popup $w.ctxm %X %Y
338 bind $i <Shift-Tab> "[list focus $w_cviewer];break"
339 bind $i <Tab> "[cb _focus_search $w_cviewer];break"
342 foreach i [concat $w_columns $w_cviewer] {
343 bind $i <Key-Up> {catch {%W yview scroll -1 units};break}
344 bind $i <Key-Down> {catch {%W yview scroll 1 units};break}
345 bind $i <Key-Left> {catch {%W xview scroll -1 units};break}
346 bind $i <Key-Right> {catch {%W xview scroll 1 units};break}
347 bind $i <Key-k> {catch {%W yview scroll -1 units};break}
348 bind $i <Key-j> {catch {%W yview scroll 1 units};break}
349 bind $i <Key-h> {catch {%W xview scroll -1 units};break}
350 bind $i <Key-l> {catch {%W xview scroll 1 units};break}
351 bind $i <Control-Key-b> {catch {%W yview scroll -1 pages};break}
352 bind $i <Control-Key-f> {catch {%W yview scroll 1 pages};break}
355 bind $w_cviewer <Shift-Tab> "[cb _focus_search $w_file];break"
356 bind $w_cviewer <Tab> "[list focus $w_file];break"
357 bind $w_cviewer <Button-1> [list focus $w_cviewer]
358 bind $w_file <Visibility> [cb _focus_search $w_file]
359 bind $top <F7> [cb _show_finder]
360 bind $top <Key-slash> [cb _show_finder]
361 bind $top <Control-Key-s> [cb _show_finder]
362 bind $top <Escape> [list searchbar::hide $finder]
363 bind $top <F3> [list searchbar::find_next $finder]
364 bind $top <Shift-F3> [list searchbar::find_prev $finder]
365 bind $top <Control-Key-g> [cb _show_linebar]
366 catch { bind $top <Shift-Key-XF86_Switch_VT_3> [list searchbar::find_prev $finder] }
368 grid configure $w.header -sticky ew
369 grid configure $w.file_pane -sticky nsew
370 grid configure $w.status -sticky ew
371 grid columnconfigure $top 0 -weight 1
372 grid rowconfigure $top 0 -weight 0
373 grid rowconfigure $top 1 -weight 1
374 grid rowconfigure $top 2 -weight 0
376 set req_w [winfo reqwidth $top]
377 set req_h [winfo reqheight $top]
378 set scr_w [expr {[winfo screenwidth $top] - 40}]
379 set scr_h [expr {[winfo screenheight $top] - 120}]
380 set opt_w [expr {$font_w * (80 + 5*3 + 3)}]
381 if {$req_w < $opt_w} {set req_w $opt_w}
382 if {$req_w > $scr_w} {set req_w $scr_w}
383 set opt_h [expr {$req_w*4/3}]
384 if {$req_h < $scr_h} {set req_h $scr_h}
385 if {$req_h > $opt_h} {set req_h $opt_h}
386 set g "${req_w}x${req_h}"
387 wm geometry $top $g
388 update
390 set old_height [winfo height $w.file_pane]
391 $w.file_pane sash place 0 \
392 [lindex [$w.file_pane sash coord 0] 0] \
393 [expr {int($old_height * 0.80)}]
394 bind $w.file_pane <Configure> \
395 "if {{$w.file_pane} eq {%W}} {[cb _resize %h]}"
397 wm protocol $top WM_DELETE_WINDOW "destroy $top"
398 bind $top <Destroy> [cb _handle_destroy %W]
400 _load $this $i_jump
403 method _focus_search {win} {
404 if {[searchbar::visible $finder]} {
405 focus [searchbar::editor $finder]
406 } else {
407 focus $win
411 method _handle_destroy {win} {
412 if {$win eq $w} {
413 _kill $this
414 delete_this
418 method _kill {} {
419 if {$current_fd ne {}} {
420 kill_file_process $current_fd
421 catch {close $current_fd}
422 set current_fd {}
426 method _load {jump} {
427 variable group_colors
429 _hide_tooltip $this
431 if {$total_lines != 0 || $current_fd ne {}} {
432 _kill $this
434 foreach i $w_columns {
435 $i conf -state normal
436 $i delete 0.0 end
437 foreach g [$i tag names] {
438 if {[regexp {^g[0-9a-f]{40}$} $g]} {
439 $i tag delete $g
442 $i conf -state disabled
445 $w_cviewer conf -state normal
446 $w_cviewer delete 0.0 end
447 $w_cviewer conf -state disabled
449 set highlight_line -1
450 set highlight_column {}
451 set highlight_commit {}
452 set total_lines 0
455 if {$history eq {}} {
456 $w_back conf -state disabled
457 } else {
458 $w_back conf -state normal
461 # Index 0 is always empty. There is never line 0 as
462 # we use only 1 based lines, as that matches both with
463 # git-blame output and with Tk's text widget.
465 set amov_data [list [list]]
466 set asim_data [list [list]]
468 $status show [mc "Reading %s..." "$commit:[escape_path $path]"]
469 $w_path conf -text [escape_path $path]
471 set do_textconv 0
472 if {![is_config_false gui.textconv] && [git-version >= 1.7.2]} {
473 set filter [gitattr $path diff set]
474 set textconv [get_config [join [list diff $filter textconv] .]]
475 if {$filter ne {set} && $textconv ne {}} {
476 set do_textconv 1
479 if {$commit eq {}} {
480 if {$do_textconv ne 0} {
481 set fd [open_cmd_pipe $textconv $path]
482 } else {
483 set fd [open $path r]
485 fconfigure $fd -eofchar {}
486 } else {
487 if {$do_textconv ne 0} {
488 set fd [git_read cat-file --textconv "$commit:$path"]
489 } else {
490 set fd [git_read cat-file blob "$commit:$path"]
493 fconfigure $fd \
494 -blocking 0 \
495 -translation lf \
496 -encoding [get_path_encoding $path]
497 fileevent $fd readable [cb _read_file $fd $jump]
498 set current_fd $fd
501 method _history_menu {} {
502 set m $w.backmenu
503 if {[winfo exists $m]} {
504 $m delete 0 end
505 } else {
506 menu $m -tearoff 0
509 for {set i [expr {[llength $history] - 1}]
510 } {$i >= 0} {incr i -1} {
511 set e [lindex $history $i]
512 set c [lindex $e 0]
513 set f [lindex $e 1]
515 if {[regexp {^[0-9a-f]{40}$} $c]} {
516 set t [string range $c 0 8]...
517 } elseif {$c eq {}} {
518 set t {Working Directory}
519 } else {
520 set t $c
522 if {![catch {set summary $header($c,summary)}]} {
523 append t " $summary"
524 if {[string length $t] > 70} {
525 set t [string range $t 0 66]...
529 $m add command -label $t -command [cb _goback $i]
531 set X [winfo rootx $w_back]
532 set Y [expr {[winfo rooty $w_back] + [winfo height $w_back]}]
533 tk_popup $m $X $Y
536 method _goback {i} {
537 set dat [lindex $history $i]
538 set history [lrange $history 0 [expr {$i - 1}]]
539 set commit [lindex $dat 0]
540 set path [lindex $dat 1]
541 _load $this [lrange $dat 2 5]
544 method _read_file {fd jump} {
545 if {$fd ne $current_fd} {
546 catch {close $fd}
547 return
550 foreach i $w_columns {$i conf -state normal}
551 while {[gets $fd line] >= 0} {
552 regsub "\r\$" $line {} line
553 incr total_lines
554 lappend amov_data {}
555 lappend asim_data {}
557 if {$total_lines > 1} {
558 foreach i $w_columns {$i insert end "\n"}
561 $w_line insert end "$total_lines" linenumber
562 $w_file insert end "$line"
565 set ln_wc [expr {[string length $total_lines] + 2}]
566 if {[$w_line cget -width] < $ln_wc} {
567 $w_line conf -width $ln_wc
570 foreach i $w_columns {$i conf -state disabled}
572 if {[eof $fd]} {
573 fconfigure $fd -blocking 1; # enable error reporting on close
574 if {[catch {close $fd} err]} {
575 tk_messageBox -icon error -title [mc Error] \
576 -message $err
579 # If we don't force Tk to update the widgets *right now*
580 # none of our jump commands will cause a change in the UI.
582 update
584 if {[llength $jump] == 1} {
585 set highlight_line [lindex $jump 0]
586 $w_file see "$highlight_line.0"
587 } elseif {[llength $jump] == 4} {
588 set highlight_column [lindex $jump 0]
589 set highlight_line [lindex $jump 1]
590 $w_file xview moveto [lindex $jump 2]
591 $w_file yview moveto [lindex $jump 3]
594 _exec_blame $this $w_asim @asim_data \
595 [list] \
596 [mc "Loading copy/move tracking annotations..."]
598 } ifdeleted { catch {close $fd} }
600 method _exec_blame {cur_w cur_d options cur_s} {
601 lappend options --incremental --encoding=utf-8
602 if {$commit eq {}} {
603 lappend options --contents $path
604 } else {
605 lappend options $commit
608 # We may recurse in from another call to _exec_blame and already have
609 # a status operation.
610 if {$status_operation == {}} {
611 set status_operation [$status start \
612 $cur_s \
613 [mc "lines annotated"]]
614 } else {
615 $status_operation restart $cur_s
618 lappend options -- $path
619 set fd [eval git_read --nice blame $options]
620 fconfigure $fd -blocking 0 -translation lf -encoding utf-8
621 fileevent $fd readable [cb _read_blame $fd $cur_w $cur_d]
622 set current_fd $fd
623 set blame_lines 0
626 method _read_blame {fd cur_w cur_d} {
627 upvar #0 $cur_d line_data
628 variable group_colors
630 if {$fd ne $current_fd} {
631 catch {close $fd}
632 return
635 $cur_w conf -state normal
636 while {[gets $fd line] >= 0} {
637 if {[regexp {^([a-z0-9]{40}) (\d+) (\d+) (\d+)$} $line line \
638 cmit original_line final_line line_count]} {
639 set r_commit $cmit
640 set r_orig_line $original_line
641 set r_final_line $final_line
642 set r_line_count $line_count
643 } elseif {[string match {filename *} $line]} {
644 set file [string range $line 9 end]
645 set n $r_line_count
646 set lno $r_final_line
647 set oln $r_orig_line
648 set cmit $r_commit
650 if {[regexp {^0{40}$} $cmit]} {
651 set commit_abbr work
652 set commit_type curr_commit
653 } elseif {$cmit eq $commit} {
654 set commit_abbr this
655 set commit_type curr_commit
656 } else {
657 set commit_type prior_commit
658 set commit_abbr [string range $cmit 0 3]
661 set author_abbr {}
662 set a_name {}
663 catch {set a_name $header($cmit,author)}
664 while {$a_name ne {}} {
665 if {$author_abbr ne {}
666 && [string index $a_name 0] eq {'}} {
667 regsub {^'[^']+'\s+} $a_name {} a_name
669 if {![regexp {^([[:upper:]])} $a_name _a]} break
670 append author_abbr $_a
671 unset _a
672 if {![regsub \
673 {^[[:upper:]][^\s]*\s+} \
674 $a_name {} a_name ]} break
676 if {$author_abbr eq {}} {
677 set author_abbr { |}
678 } else {
679 set author_abbr [string range $author_abbr 0 3]
681 unset a_name
683 set first_lno $lno
684 while {
685 $first_lno > 1
686 && $cmit eq [lindex $line_data [expr {$first_lno - 1}] 0]
687 && $file eq [lindex $line_data [expr {$first_lno - 1}] 1]
689 incr first_lno -1
692 set color {}
693 if {$first_lno < $lno} {
694 foreach g [$w_file tag names $first_lno.0] {
695 if {[regexp {^color[0-9]+$} $g]} {
696 set color $g
697 break
700 } else {
701 set i [lsort [concat \
702 [$w_file tag names "[expr {$first_lno - 1}].0"] \
703 [$w_file tag names "[expr {$lno + $n}].0"] \
705 for {set g 0} {$g < [llength $group_colors]} {incr g} {
706 if {[lsearch -sorted -exact $i color$g] == -1} {
707 set color color$g
708 break
712 if {$color eq {}} {
713 set color color0
716 while {$n > 0} {
717 set lno_e "$lno.0 lineend + 1c"
718 if {[lindex $line_data $lno] ne {}} {
719 set g [lindex $line_data $lno 0]
720 foreach i $w_columns {
721 $i tag remove g$g $lno.0 $lno_e
724 lset line_data $lno [list $cmit $file $oln]
726 $cur_w delete $lno.0 "$lno.0 lineend"
727 if {$lno == $first_lno} {
728 $cur_w insert $lno.0 $commit_abbr $commit_type
729 } elseif {$lno == [expr {$first_lno + 1}]} {
730 $cur_w insert $lno.0 $author_abbr author_abbr
731 } else {
732 $cur_w insert $lno.0 { |}
735 foreach i $w_columns {
736 if {$cur_w eq $w_amov} {
737 for {set g 0} \
738 {$g < [llength $group_colors]} \
739 {incr g} {
740 $i tag remove color$g $lno.0 $lno_e
742 $i tag add $color $lno.0 $lno_e
744 $i tag add g$cmit $lno.0 $lno_e
747 if {$highlight_column eq $cur_w} {
748 if {$highlight_line == -1
749 && [lindex [$w_file yview] 0] == 0} {
750 $w_file see $lno.0
751 set highlight_line $lno
753 if {$highlight_line == $lno} {
754 _showcommit $this $cur_w $lno
758 incr n -1
759 incr lno
760 incr oln
761 incr blame_lines
764 while {
765 $cmit eq [lindex $line_data $lno 0]
766 && $file eq [lindex $line_data $lno 1]
768 $cur_w delete $lno.0 "$lno.0 lineend"
770 if {$lno == $first_lno} {
771 $cur_w insert $lno.0 $commit_abbr $commit_type
772 } elseif {$lno == [expr {$first_lno + 1}]} {
773 $cur_w insert $lno.0 $author_abbr author_abbr
774 } else {
775 $cur_w insert $lno.0 { |}
778 if {$cur_w eq $w_amov} {
779 foreach i $w_columns {
780 for {set g 0} \
781 {$g < [llength $group_colors]} \
782 {incr g} {
783 $i tag remove color$g $lno.0 $lno_e
785 $i tag add $color $lno.0 $lno_e
789 incr lno
792 } elseif {[regexp {^([a-z-]+) (.*)$} $line line key data]} {
793 set header($r_commit,$key) $data
796 $cur_w conf -state disabled
798 if {[eof $fd]} {
799 close $fd
800 if {$cur_w eq $w_asim} {
801 # Switches for original location detection
802 set threshold [get_config gui.copyblamethreshold]
803 set original_options [list "-C$threshold"]
805 if {![is_config_true gui.fastcopyblame]} {
806 # thorough copy search; insert before the threshold
807 set original_options [linsert $original_options 0 -C]
809 if {[git-version >= 1.5.3]} {
810 lappend original_options -w ; # ignore indentation changes
813 _exec_blame $this $w_amov @amov_data \
814 $original_options \
815 [mc "Loading original location annotations..."]
816 } else {
817 set current_fd {}
818 $status_operation stop [mc "Annotation complete."]
819 set status_operation {}
821 } else {
822 $status_operation update $blame_lines $total_lines
824 } ifdeleted { catch {close $fd} }
826 method _find_commit_bound {data_list start_idx delta} {
827 upvar #0 $data_list line_data
828 set pos $start_idx
829 set limit [expr {[llength $line_data] - 1}]
830 set base_commit [lindex $line_data $pos 0]
832 while {$pos > 0 && $pos < $limit} {
833 set new_pos [expr {$pos + $delta}]
834 if {[lindex $line_data $new_pos 0] ne $base_commit} {
835 return $pos
838 set pos $new_pos
841 return $pos
844 method _fullcopyblame {} {
845 if {$current_fd ne {}} {
846 tk_messageBox \
847 -icon error \
848 -type ok \
849 -title [mc "Busy"] \
850 -message [mc "Annotation process is already running."]
852 return
855 # Switches for original location detection
856 set threshold [get_config gui.copyblamethreshold]
857 set original_options [list -C -C "-C$threshold"]
859 if {[git-version >= 1.5.3]} {
860 lappend original_options -w ; # ignore indentation changes
863 # Find the line range
864 set pos @$::cursorX,$::cursorY
865 set lno [lindex [split [$::cursorW index $pos] .] 0]
866 set min_amov_lno [_find_commit_bound $this @amov_data $lno -1]
867 set max_amov_lno [_find_commit_bound $this @amov_data $lno 1]
868 set min_asim_lno [_find_commit_bound $this @asim_data $lno -1]
869 set max_asim_lno [_find_commit_bound $this @asim_data $lno 1]
871 if {$min_asim_lno < $min_amov_lno} {
872 set min_amov_lno $min_asim_lno
875 if {$max_asim_lno > $max_amov_lno} {
876 set max_amov_lno $max_asim_lno
879 lappend original_options -L "$min_amov_lno,$max_amov_lno"
881 # Clear lines
882 for {set i $min_amov_lno} {$i <= $max_amov_lno} {incr i} {
883 lset amov_data $i [list ]
886 # Start the back-end process
887 _exec_blame $this $w_amov @amov_data \
888 $original_options \
889 [mc "Running thorough copy detection..."]
892 method _click {cur_w pos} {
893 set lno [lindex [split [$cur_w index $pos] .] 0]
894 _showcommit $this $cur_w $lno
897 method _setencoding {enc} {
898 force_path_encoding $path $enc
899 _load $this [list \
900 $highlight_column \
901 $highlight_line \
902 [lindex [$w_file xview] 0] \
903 [lindex [$w_file yview] 0] \
907 method _load_commit {cur_w cur_d pos} {
908 upvar #0 $cur_d line_data
909 set lno [lindex [split [$cur_w index $pos] .] 0]
910 set dat [lindex $line_data $lno]
911 if {$dat ne {}} {
912 _load_new_commit $this \
913 [lindex $dat 0] \
914 [lindex $dat 1] \
915 [list [lindex $dat 2]]
919 method _load_new_commit {new_commit new_path jump} {
920 lappend history [list \
921 $commit $path \
922 $highlight_column \
923 $highlight_line \
924 [lindex [$w_file xview] 0] \
925 [lindex [$w_file yview] 0] \
928 set commit $new_commit
929 set path $new_path
930 _load $this $jump
933 method _showcommit {cur_w lno} {
934 global repo_config
935 variable active_color
937 if {$highlight_commit ne {}} {
938 foreach i $w_columns {
939 $i tag conf g$highlight_commit -background {}
940 $i tag lower g$highlight_commit
944 if {$cur_w eq $w_asim} {
945 set dat [lindex $asim_data $lno]
946 set highlight_column $w_asim
947 } else {
948 set dat [lindex $amov_data $lno]
949 set highlight_column $w_amov
952 $w_cviewer conf -state normal
953 $w_cviewer delete 0.0 end
955 if {$dat eq {}} {
956 set cmit {}
957 $w_cviewer insert end [mc "Loading annotation..."] still_loading
958 } else {
959 set cmit [lindex $dat 0]
960 set file [lindex $dat 1]
962 foreach i $w_columns {
963 $i tag conf g$cmit -background $active_color
964 $i tag raise g$cmit
965 if {$i eq $w_file} {
966 $w_file tag raise found
968 $i tag raise sel
971 set author_name {}
972 set author_email {}
973 set author_time {}
974 catch {set author_name $header($cmit,author)}
975 catch {set author_email $header($cmit,author-mail)}
976 catch {set author_time [format_date $header($cmit,author-time)]}
978 set committer_name {}
979 set committer_email {}
980 set committer_time {}
981 catch {set committer_name $header($cmit,committer)}
982 catch {set committer_email $header($cmit,committer-mail)}
983 catch {set committer_time [format_date $header($cmit,committer-time)]}
985 if {[catch {set msg $header($cmit,message)}]} {
986 set msg {}
987 catch {
988 set fd [git_read cat-file commit $cmit]
989 fconfigure $fd -encoding binary -translation lf
990 # By default commits are assumed to be in utf-8
991 set enc utf-8
992 while {[gets $fd line] > 0} {
993 if {[string match {encoding *} $line]} {
994 set enc [string tolower [string range $line 9 end]]
997 set msg [read $fd]
998 close $fd
1000 set enc [tcl_encoding $enc]
1001 if {$enc ne {}} {
1002 set msg [encoding convertfrom $enc $msg]
1004 set msg [string trim $msg]
1006 set header($cmit,message) $msg
1009 $w_cviewer insert end "commit $cmit\n" header_key
1010 $w_cviewer insert end [strcat [mc "Author:"] "\t"] header_key
1011 $w_cviewer insert end "$author_name $author_email" header_val
1012 $w_cviewer insert end " $author_time\n" header_val
1014 $w_cviewer insert end [strcat [mc "Committer:"] "\t"] header_key
1015 $w_cviewer insert end "$committer_name $committer_email" header_val
1016 $w_cviewer insert end " $committer_time\n" header_val
1018 if {$file ne $path} {
1019 $w_cviewer insert end [strcat [mc "Original File:"] "\t"] header_key
1020 $w_cviewer insert end "[escape_path $file]\n" header_val
1023 $w_cviewer insert end "\n$msg"
1025 $w_cviewer conf -state disabled
1027 set highlight_line $lno
1028 set highlight_commit $cmit
1030 if {[lsearch -exact $tooltip_commit $highlight_commit] != -1} {
1031 _hide_tooltip $this
1035 method _get_click_amov_info {} {
1036 set pos @$::cursorX,$::cursorY
1037 set lno [lindex [split [$::cursorW index $pos] .] 0]
1038 return [lindex $amov_data $lno]
1041 method _copycommit {} {
1042 set dat [_get_click_amov_info $this]
1043 if {$dat ne {}} {
1044 clipboard clear
1045 clipboard append \
1046 -format STRING \
1047 -type STRING \
1048 -- [lindex $dat 0]
1052 method _format_offset_date {base offset} {
1053 set exval [expr {$base + $offset*24*60*60}]
1054 return [clock format $exval -format {%Y-%m-%d}]
1057 method _gitkcommit {} {
1058 global nullid
1060 set dat [_get_click_amov_info $this]
1061 if {$dat ne {}} {
1062 set cmit [lindex $dat 0]
1064 # If the line belongs to the working copy, use HEAD instead
1065 if {$cmit eq $nullid} {
1066 if {[catch {set cmit [git rev-parse --verify HEAD]} err]} {
1067 error_popup [strcat [mc "Cannot find HEAD commit:"] "\n\n$err"]
1068 return;
1072 set radius [get_config gui.blamehistoryctx]
1073 set cmdline [list --select-commit=$cmit]
1075 if {$radius > 0} {
1076 set author_time {}
1077 set committer_time {}
1079 catch {set author_time $header($cmit,author-time)}
1080 catch {set committer_time $header($cmit,committer-time)}
1082 if {$committer_time eq {}} {
1083 set committer_time $author_time
1086 set after_time [_format_offset_date $this $committer_time [expr {-$radius}]]
1087 set before_time [_format_offset_date $this $committer_time $radius]
1089 lappend cmdline --after=$after_time --before=$before_time
1092 lappend cmdline $cmit
1094 set base_rev "HEAD"
1095 if {$commit ne {}} {
1096 set base_rev $commit
1099 if {$base_rev ne $cmit} {
1100 lappend cmdline $base_rev
1103 do_gitk $cmdline
1107 method _blameparent {} {
1108 global nullid
1110 set dat [_get_click_amov_info $this]
1111 if {$dat ne {}} {
1112 set cmit [lindex $dat 0]
1113 set new_path [lindex $dat 1]
1115 # Allow using Blame Parent on lines modified in the working copy
1116 if {$cmit eq $nullid} {
1117 set parent_ref "HEAD"
1118 } else {
1119 set parent_ref "$cmit^"
1121 if {[catch {set cparent [git rev-parse --verify $parent_ref]} err]} {
1122 error_popup [strcat [mc "Cannot find parent commit:"] "\n\n$err"]
1123 return;
1126 _kill $this
1128 # Generate a diff between the commit and its parent,
1129 # and use the hunks to update the line number.
1130 # Request zero context to simplify calculations.
1131 if {$cmit eq $nullid} {
1132 set diffcmd [list diff-index --unified=0 $cparent -- $new_path]
1133 } else {
1134 set diffcmd [list diff-tree --unified=0 $cparent $cmit -- $new_path]
1136 if {[catch {set fd [eval git_read $diffcmd]} err]} {
1137 $status_operation stop [mc "Unable to display parent"]
1138 error_popup [strcat [mc "Error loading diff:"] "\n\n$err"]
1139 return
1142 set r_orig_line [lindex $dat 2]
1144 fconfigure $fd \
1145 -blocking 0 \
1146 -encoding binary \
1147 -translation binary
1148 fileevent $fd readable [cb _read_diff_load_commit \
1149 $fd $cparent $new_path $r_orig_line]
1150 set current_fd $fd
1154 method _read_diff_load_commit {fd cparent new_path tline} {
1155 if {$fd ne $current_fd} {
1156 catch {close $fd}
1157 return
1160 while {[gets $fd line] >= 0} {
1161 if {[regexp {^@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@} $line line \
1162 old_line osz old_size new_line nsz new_size]} {
1164 if {$osz eq {}} { set old_size 1 }
1165 if {$nsz eq {}} { set new_size 1 }
1167 if {$new_line <= $tline} {
1168 if {[expr {$new_line + $new_size}] > $tline} {
1169 # Target line within the hunk
1170 set line_shift [expr {
1171 ($new_size-$old_size)*($tline-$new_line)/$new_size
1173 } else {
1174 set line_shift [expr {$new_size-$old_size}]
1177 set r_orig_line [expr {$r_orig_line - $line_shift}]
1182 if {[eof $fd]} {
1183 close $fd
1184 set current_fd {}
1186 _load_new_commit $this \
1187 $cparent \
1188 $new_path \
1189 [list $r_orig_line]
1191 } ifdeleted { catch {close $fd} }
1193 method _show_tooltip {cur_w pos} {
1194 if {$tooltip_wm ne {}} {
1195 _open_tooltip $this $cur_w
1196 } elseif {$tooltip_timer eq {}} {
1197 set tooltip_timer [after 1000 [cb _open_tooltip $cur_w]]
1201 method _open_tooltip {cur_w} {
1202 set tooltip_timer {}
1203 set pos_x [winfo pointerx $cur_w]
1204 set pos_y [winfo pointery $cur_w]
1205 if {[winfo containing $pos_x $pos_y] ne $cur_w} {
1206 _hide_tooltip $this
1207 return
1210 if {$tooltip_wm ne "$cur_w.tooltip"} {
1211 _hide_tooltip $this
1213 set tooltip_wm [toplevel $cur_w.tooltip -borderwidth 1]
1214 catch {wm attributes $tooltip_wm -type tooltip}
1215 wm overrideredirect $tooltip_wm 1
1216 wm transient $tooltip_wm [winfo toplevel $cur_w]
1217 set tooltip_t $tooltip_wm.label
1218 text $tooltip_t \
1219 -takefocus 0 \
1220 -highlightthickness 0 \
1221 -relief flat \
1222 -borderwidth 0 \
1223 -wrap none \
1224 -background lightyellow \
1225 -foreground black
1226 $tooltip_t tag conf section_header -font font_uibold
1227 pack $tooltip_t
1228 } else {
1229 $tooltip_t conf -state normal
1230 $tooltip_t delete 0.0 end
1233 set pos @[join [list \
1234 [expr {$pos_x - [winfo rootx $cur_w]}] \
1235 [expr {$pos_y - [winfo rooty $cur_w]}]] ,]
1236 set lno [lindex [split [$cur_w index $pos] .] 0]
1237 if {$cur_w eq $w_amov} {
1238 set dat [lindex $amov_data $lno]
1239 set org {}
1240 } else {
1241 set dat [lindex $asim_data $lno]
1242 set org [lindex $amov_data $lno]
1245 if {$dat eq {}} {
1246 _hide_tooltip $this
1247 return
1250 set cmit [lindex $dat 0]
1251 set tooltip_commit [list $cmit]
1253 set author_name {}
1254 set summary {}
1255 set author_time {}
1256 catch {set author_name $header($cmit,author)}
1257 catch {set summary $header($cmit,summary)}
1258 catch {set author_time [format_date $header($cmit,author-time)]}
1260 $tooltip_t insert end "commit $cmit\n"
1261 $tooltip_t insert end "$author_name $author_time\n"
1262 $tooltip_t insert end "$summary"
1264 if {$org ne {} && [lindex $org 0] ne $cmit} {
1265 set save [$tooltip_t get 0.0 end]
1266 $tooltip_t delete 0.0 end
1268 set cmit [lindex $org 0]
1269 set file [lindex $org 1]
1270 lappend tooltip_commit $cmit
1272 set author_name {}
1273 set summary {}
1274 set author_time {}
1275 catch {set author_name $header($cmit,author)}
1276 catch {set summary $header($cmit,summary)}
1277 catch {set author_time [format_date $header($cmit,author-time)]}
1279 $tooltip_t insert end [strcat [mc "Originally By:"] "\n"] section_header
1280 $tooltip_t insert end "commit $cmit\n"
1281 $tooltip_t insert end "$author_name $author_time\n"
1282 $tooltip_t insert end "$summary\n"
1284 if {$file ne $path} {
1285 $tooltip_t insert end [strcat [mc "In File:"] " "] section_header
1286 $tooltip_t insert end "$file\n"
1289 $tooltip_t insert end "\n"
1290 $tooltip_t insert end [strcat [mc "Copied Or Moved Here By:"] "\n"] section_header
1291 $tooltip_t insert end $save
1294 $tooltip_t conf -state disabled
1295 _position_tooltip $this
1297 # On MacOS raising a window causes it to acquire focus.
1298 # Tk 8.5 on MacOS seems to properly support wm transient,
1299 # so we can safely counter the effect there.
1300 if {$::have_tk85 && [is_MacOSX]} {
1301 update
1302 if {$w eq {}} {
1303 raise .
1304 } else {
1305 raise $w
1310 method _position_tooltip {} {
1311 set max_h [lindex [split [$tooltip_t index end] .] 0]
1312 set max_w 0
1313 for {set i 1} {$i <= $max_h} {incr i} {
1314 set c [lindex [split [$tooltip_t index "$i.0 lineend"] .] 1]
1315 if {$c > $max_w} {set max_w $c}
1317 $tooltip_t conf -width $max_w -height $max_h
1319 set req_w [winfo reqwidth $tooltip_t]
1320 set req_h [winfo reqheight $tooltip_t]
1321 set pos_x [expr {[winfo pointerx .] + 5}]
1322 set pos_y [expr {[winfo pointery .] + 10}]
1324 set g "${req_w}x${req_h}"
1325 if {[tk windowingsystem] eq "win32" || $pos_x >= 0} {append g +}
1326 append g $pos_x
1327 if {[tk windowingsystem] eq "win32" || $pos_y >= 0} {append g +}
1328 append g $pos_y
1330 wm geometry $tooltip_wm $g
1331 if {![is_MacOSX]} {
1332 raise $tooltip_wm
1336 method _hide_tooltip {} {
1337 if {$tooltip_wm ne {}} {
1338 destroy $tooltip_wm
1339 set tooltip_wm {}
1340 set tooltip_commit {}
1342 if {$tooltip_timer ne {}} {
1343 after cancel $tooltip_timer
1344 set tooltip_timer {}
1348 method _resize {new_height} {
1349 set diff [expr {$new_height - $old_height}]
1350 if {$diff == 0} return
1352 set my [expr {[winfo height $w.file_pane] - 25}]
1353 set o [$w.file_pane sash coord 0]
1354 set ox [lindex $o 0]
1355 set oy [expr {[lindex $o 1] + $diff}]
1356 if {$oy < 0} {set oy 0}
1357 if {$oy > $my} {set oy $my}
1358 $w.file_pane sash place 0 $ox $oy
1360 set old_height $new_height
1363 method _show_finder {} {
1364 linebar::hide $gotoline
1365 searchbar::show $finder
1368 method _show_linebar {} {
1369 searchbar::hide $finder
1370 linebar::show $gotoline