blame.tcl 31.4 KB
Newer Older
1 2 3
# git-gui blame viewer
# Copyright (C) 2006, 2007 Shawn Pearce

4 5
class blame {

6 7
image create photo ::blame::img_back_arrow -data {R0lGODlhGAAYAIUAAPwCBEzKXFTSZIz+nGzmhGzqfGTidIT+nEzGXHTqhGzmfGzifFzadETCVES+VARWDFzWbHzyjAReDGTadFTOZDSyRDyyTCymPARaFGTedFzSbDy2TCyqRCyqPARaDAyCHES6VDy6VCyiPAR6HCSeNByWLARyFARiDARqFGTifARiFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAAYABgAAAajQIBwSCwaj8ikcsk0BppJwRPqHEypQwHBis0WDAdEFyBIKBaMAKLBdjQeSkFBYTBAIvgEoS6JmhUTEwIUDQ4VFhcMGEhyCgoZExoUaxsWHB0THkgfAXUGAhoBDSAVFR0XBnCbDRmgog0hpSIiDJpJIyEQhBUcJCIlwA22SSYVogknEg8eD82qSigdDSknY0IqJQXPYxIl1dZCGNvWw+Dm510GQQAh/mhDcmVhdGVkIGJ5IEJNUFRvR0lGIFBybyB2ZXJzaW9uIDIuNQ0KqSBEZXZlbENvciAxOTk3LDE5OTguIEFsbCByaWdodHMgcmVzZXJ2ZWQuDQpodHRwOi8vd3d3LmRldmVsY29yLmNvbQA7}

8 9
# Persistant data (survives loads)
#
10
field history {}; # viewer history: {commit path}
11
field header    ; # array commit,key -> header field
12

13 14
# Tk UI control paths
#
15 16 17
field w          ; # top window in this viewer
field w_back     ; # our back button
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 status     ; # status mega-widget instance
25 26
field old_height ; # last known height of $w.file_pane

27 28
# Tk UI colors
#
29 30
variable active_color #c0edc5
variable group_colors {
31 32 33 34 35 36 37 38 39 40 41 42
	#d6d6d6
	#e1e1e1
	#ececec
}

# Current blame data; cleared/reset on each load
#
field commit               ; # input commit to blame
field path                 ; # input filename to view in $commit

field current_fd        {} ; # background process running
field highlight_line    -1 ; # current line selected
43
field highlight_column  {} ; # current commit column selected
44
field highlight_commit  {} ; # sha1 of commit selected
45 46 47

field total_lines       0  ; # total length of file
field blame_lines       0  ; # number of lines computed
48
field amov_data            ; # list of {commit origfile origline}
49
field asim_data            ; # list of {commit origfile origline}
50

51 52 53 54
field r_commit             ; # commit currently being parsed
field r_orig_line          ; # original line number
field r_final_line         ; # final line number
field r_line_count         ; # lines in this region
55

56
field tooltip_wm        {} ; # Current tooltip toplevel, if open
57
field tooltip_t         {} ; # Text widget in $tooltip_wm
58
field tooltip_timer     {} ; # Current timer event for our tooltip
59
field tooltip_commit    {} ; # Commit(s) in tooltip
60

61
constructor new {i_commit i_path i_jump} {
62
	global cursor_ptr
63 64
	variable active_color
	variable group_colors
65

66 67 68 69
	set commit $i_commit
	set path   $i_path

	make_toplevel top w
70
	wm title $top [append "[appname] ([reponame]): " [mc "File Viewer"]]
71

72
	frame $w.header -background gold
73
	label $w.header.commit_l \
74
		-text [mc "Commit:"] \
75
		-background gold \
76
		-foreground black \
77
		-anchor w \
78 79
		-justify left
	set w_back $w.header.commit_b
80
	label $w_back \
81 82 83 84
		-image ::blame::img_back_arrow \
		-borderwidth 0 \
		-relief flat \
		-state disabled \
85
		-background gold \
86
		-foreground black \
87
		-activebackground gold
88 89 90 91 92
	bind $w_back <Button-1> "
		if {\[$w_back cget -state\] eq {normal}} {
			[cb _history_menu]
		}
		"
93 94
	label $w.header.commit \
		-textvariable @commit \
95
		-background gold \
96
		-foreground black \
97 98 99
		-anchor w \
		-justify left
	label $w.header.path_l \
100
		-text [mc "File:"] \
101
		-background gold \
102
		-foreground black \
103 104 105 106
		-anchor w \
		-justify left
	set w_path $w.header.path
	label $w_path \
107
		-background gold \
108
		-foreground black \
109 110 111 112 113 114 115
		-anchor w \
		-justify left
	pack $w.header.commit_l -side left
	pack $w_back -side left
	pack $w.header.commit -side left
	pack $w_path -fill x -side right
	pack $w.header.path_l -side right
116

117 118 119 120 121 122 123 124 125 126 127 128 129 130 131
	panedwindow $w.file_pane -orient vertical
	frame $w.file_pane.out
	frame $w.file_pane.cm
	$w.file_pane add $w.file_pane.out \
		-sticky nsew \
		-minsize 100 \
		-height 100 \
		-width 100
	$w.file_pane add $w.file_pane.cm \
		-sticky nsew \
		-minsize 25 \
		-height 25 \
		-width 100

	set w_line $w.file_pane.out.linenumber_t
132
	text $w_line \
133 134 135
		-takefocus 0 \
		-highlightthickness 0 \
		-padx 0 -pady 0 \
136 137 138
		-background white \
		-foreground black \
		-borderwidth 0 \
139 140 141
		-state disabled \
		-wrap none \
		-height 40 \
142
		-width 6 \
143
		-font font_diff
144
	$w_line tag conf linenumber -justify right -rmargin 5
145

146 147
	set w_amov $w.file_pane.out.amove_t
	text $w_amov \
148 149 150
		-takefocus 0 \
		-highlightthickness 0 \
		-padx 0 -pady 0 \
151 152 153
		-background white \
		-foreground black \
		-borderwidth 0 \
154 155 156
		-state disabled \
		-wrap none \
		-height 40 \
157
		-width 5 \
158
		-font font_diff
159
	$w_amov tag conf author_abbr -justify right -rmargin 5
160
	$w_amov tag conf curr_commit
161
	$w_amov tag conf prior_commit -foreground blue -underline 1
162
	$w_amov tag bind prior_commit \
163
		<Button-1> \
164 165 166 167 168 169 170
		"[cb _load_commit $w_amov @amov_data @%x,%y];break"

	set w_asim $w.file_pane.out.asimple_t
	text $w_asim \
		-takefocus 0 \
		-highlightthickness 0 \
		-padx 0 -pady 0 \
171 172 173
		-background white \
		-foreground black \
		-borderwidth 0 \
174 175 176 177 178 179 180 181 182 183 184
		-state disabled \
		-wrap none \
		-height 40 \
		-width 4 \
		-font font_diff
	$w_asim tag conf author_abbr -justify right
	$w_asim tag conf curr_commit
	$w_asim tag conf prior_commit -foreground blue -underline 1
	$w_asim tag bind prior_commit \
		<Button-1> \
		"[cb _load_commit $w_asim @asim_data @%x,%y];break"
185

186
	set w_file $w.file_pane.out.file_t
187
	text $w_file \
188 189 190
		-takefocus 0 \
		-highlightthickness 0 \
		-padx 0 -pady 0 \
191 192 193
		-background white \
		-foreground black \
		-borderwidth 0 \
194 195 196 197
		-state disabled \
		-wrap none \
		-height 40 \
		-width 80 \
198
		-xscrollcommand [list $w.file_pane.out.sbx set] \
199 200
		-font font_diff

201
	set w_columns [list $w_amov $w_asim $w_line $w_file]
202

203 204 205 206 207
	scrollbar $w.file_pane.out.sbx \
		-orient h \
		-command [list $w_file xview]
	scrollbar $w.file_pane.out.sby \
		-orient v \
208 209 210 211 212 213 214 215 216 217
		-command [list scrollbar2many $w_columns yview]
	eval grid $w_columns $w.file_pane.out.sby -sticky nsew
	grid conf \
		$w.file_pane.out.sbx \
		-column [expr {[llength $w_columns] - 1}] \
		-sticky we
	grid columnconfigure \
		$w.file_pane.out \
		[expr {[llength $w_columns] - 1}] \
		-weight 1
218
	grid rowconfigure $w.file_pane.out 0 -weight 1
219

220 221
	set w_cviewer $w.file_pane.cm.t
	text $w_cviewer \
222 223 224
		-background white \
		-foreground black \
		-borderwidth 0 \
225 226 227 228
		-state disabled \
		-wrap none \
		-height 10 \
		-width 80 \
229 230
		-xscrollcommand [list $w.file_pane.cm.sbx set] \
		-yscrollcommand [list $w.file_pane.cm.sby set] \
231
		-font font_diff
232 233 234
	$w_cviewer tag conf still_loading \
		-font font_uiitalic \
		-justify center
235
	$w_cviewer tag conf header_key \
236 237 238
		-tabs {3c} \
		-background $active_color \
		-font font_uibold
239
	$w_cviewer tag conf header_val \
240 241
		-background $active_color \
		-font font_ui
242
	$w_cviewer tag raise sel
243 244
	scrollbar $w.file_pane.cm.sbx \
		-orient h \
245
		-command [list $w_cviewer xview]
246 247
	scrollbar $w.file_pane.cm.sby \
		-orient v \
248
		-command [list $w_cviewer yview]
249 250
	pack $w.file_pane.cm.sby -side right -fill y
	pack $w.file_pane.cm.sbx -side bottom -fill x
251
	pack $w_cviewer -expand 1 -fill both
252

253
	set status [::status_bar::new $w.status]
254

255
	menu $w.ctxm -tearoff 0
256
	$w.ctxm add command \
257
		-label [mc "Copy Commit"] \
258
		-command [cb _copycommit]
259 260 261 262 263 264
	$w.ctxm add separator
	menu $w.ctxm.enc
	build_encoding_menu $w.ctxm.enc [cb _setencoding]
	$w.ctxm add cascade \
		-label [mc "Encoding"] \
		-menu $w.ctxm.enc
265 266 267
	$w.ctxm add command \
		-label [mc "Do Full Copy Detection"] \
		-command [cb _fullcopyblame]
268
	$w.ctxm add separator
269 270 271
	$w.ctxm add command \
		-label [mc "Show History Context"] \
		-command [cb _gitkcommit]
272 273 274
	$w.ctxm add command \
		-label [mc "Blame Parent Commit"] \
		-command [cb _blameparent]
275

276
	foreach i $w_columns {
277 278 279 280
		for {set g 0} {$g < [llength $group_colors]} {incr g} {
			$i tag conf color$g -background [lindex $group_colors $g]
		}

281
		$i conf -cursor $cursor_ptr
282 283
		$i conf -yscrollcommand [list many2scrollbar \
			$w_columns yview $w.file_pane.out.sby]
284
		bind $i <Button-1> "
285 286 287 288 289 290 291
			[cb _hide_tooltip]
			[cb _click $i @%x,%y]
			focus $i
		"
		bind $i <Any-Motion>  [cb _show_tooltip $i @%x,%y]
		bind $i <Any-Enter>   [cb _hide_tooltip]
		bind $i <Any-Leave>   [cb _hide_tooltip]
292
		bind_button3 $i "
293
			[cb _hide_tooltip]
294 295 296 297 298
			set cursorX %x
			set cursorY %y
			set cursorW %W
			tk_popup $w.ctxm %X %Y
		"
299 300
		bind $i <Shift-Tab> "[list focus $w_cviewer];break"
		bind $i <Tab>       "[list focus $w_cviewer];break"
301 302
	}

303
	foreach i [concat $w_columns $w_cviewer] {
304 305 306 307 308 309 310 311 312 313 314 315
		bind $i <Key-Up>        {catch {%W yview scroll -1 units};break}
		bind $i <Key-Down>      {catch {%W yview scroll  1 units};break}
		bind $i <Key-Left>      {catch {%W xview scroll -1 units};break}
		bind $i <Key-Right>     {catch {%W xview scroll  1 units};break}
		bind $i <Key-k>         {catch {%W yview scroll -1 units};break}
		bind $i <Key-j>         {catch {%W yview scroll  1 units};break}
		bind $i <Key-h>         {catch {%W xview scroll -1 units};break}
		bind $i <Key-l>         {catch {%W xview scroll  1 units};break}
		bind $i <Control-Key-b> {catch {%W yview scroll -1 pages};break}
		bind $i <Control-Key-f> {catch {%W yview scroll  1 pages};break}
	}

316 317
	bind $w_cviewer <Shift-Tab> "[list focus $w_file];break"
	bind $w_cviewer <Tab>       "[list focus $w_file];break"
318
	bind $w_cviewer <Button-1> [list focus $w_cviewer]
319
	bind $w_file    <Visibility> [list focus $w_file]
320

321
	grid configure $w.header -sticky ew
322 323 324 325 326 327 328 329 330
	grid configure $w.file_pane -sticky nsew
	grid configure $w.status -sticky ew
	grid columnconfigure $top 0 -weight 1
	grid rowconfigure $top 0 -weight 0
	grid rowconfigure $top 1 -weight 1
	grid rowconfigure $top 2 -weight 0

	set req_w [winfo reqwidth  $top]
	set req_h [winfo reqheight $top]
331
	set scr_h [expr {[winfo screenheight $top] - 100}]
332
	if {$req_w < 600} {set req_w 600}
333
	if {$req_h < $scr_h} {set req_h $scr_h}
334 335 336 337 338 339 340 341 342 343 344
	set g "${req_w}x${req_h}"
	wm geometry $top $g
	update

	set old_height [winfo height $w.file_pane]
	$w.file_pane sash place 0 \
		[lindex [$w.file_pane sash coord 0] 0] \
		[expr {int($old_height * 0.70)}]
	bind $w.file_pane <Configure> \
	"if {{$w.file_pane} eq {%W}} {[cb _resize %h]}"

345 346 347
	wm protocol $top WM_DELETE_WINDOW "destroy $top"
	bind $top <Destroy> [cb _kill]

348
	_load $this $i_jump
349 350
}

351 352 353 354 355 356 357 358
method _kill {} {
	if {$current_fd ne {}} {
		kill_file_process $current_fd
		catch {close $current_fd}
		set current_fd {}
	}
}

359
method _load {jump} {
360 361
	variable group_colors

362 363 364
	_hide_tooltip $this

	if {$total_lines != 0 || $current_fd ne {}} {
365
		_kill $this
366

367 368 369
		foreach i $w_columns {
			$i conf -state normal
			$i delete 0.0 end
370 371 372 373
			foreach g [$i tag names] {
				if {[regexp {^g[0-9a-f]{40}$} $g]} {
					$i tag delete $g
				}
374 375 376 377
			}
			$i conf -state disabled
		}

378 379 380 381
		$w_cviewer conf -state normal
		$w_cviewer delete 0.0 end
		$w_cviewer conf -state disabled

382
		set highlight_line -1
383
		set highlight_column {}
384 385 386 387 388 389 390 391 392 393
		set highlight_commit {}
		set total_lines 0
	}

	if {$history eq {}} {
		$w_back conf -state disabled
	} else {
		$w_back conf -state normal
	}

394 395 396 397
	# Index 0 is always empty.  There is never line 0 as
	# we use only 1 based lines, as that matches both with
	# git-blame output and with Tk's text widget.
	#
398
	set amov_data [list [list]]
399
	set asim_data [list [list]]
400

401
	$status show [mc "Reading %s..." "$commit:[escape_path $path]"]
402
	$w_path conf -text [escape_path $path]
403 404
	if {$commit eq {}} {
		set fd [open $path r]
405
		fconfigure $fd -eofchar {}
406
	} else {
407
		set fd [git_read cat-file blob "$commit:$path"]
408
	}
409 410 411
	fconfigure $fd \
		-blocking 0 \
		-translation lf \
412
		-encoding [get_path_encoding $path]
413
	fileevent $fd readable [cb _read_file $fd $jump]
414 415 416 417 418 419 420 421 422 423 424
	set current_fd $fd
}

method _history_menu {} {
	set m $w.backmenu
	if {[winfo exists $m]} {
		$m delete 0 end
	} else {
		menu $m -tearoff 0
	}

425
	for {set i [expr {[llength $history] - 1}]
426 427 428 429 430 431 432
		} {$i >= 0} {incr i -1} {
		set e [lindex $history $i]
		set c [lindex $e 0]
		set f [lindex $e 1]

		if {[regexp {^[0-9a-f]{40}$} $c]} {
			set t [string range $c 0 8]...
433 434
		} elseif {$c eq {}} {
			set t {Working Directory}
435 436 437 438 439
		} else {
			set t $c
		}
		if {![catch {set summary $header($c,summary)}]} {
			append t " $summary"
440 441 442
			if {[string length $t] > 70} {
				set t [string range $t 0 66]...
			}
443 444
		}

445
		$m add command -label $t -command [cb _goback $i]
446 447 448 449 450 451
	}
	set X [winfo rootx $w_back]
	set Y [expr {[winfo rooty $w_back] + [winfo height $w_back]}]
	tk_popup $m $X $Y
}

452 453
method _goback {i} {
	set dat [lindex $history $i]
454
	set history [lrange $history 0 [expr {$i - 1}]]
455 456 457
	set commit [lindex $dat 0]
	set path [lindex $dat 1]
	_load $this [lrange $dat 2 5]
458 459
}

460
method _read_file {fd jump} {
461 462 463 464 465
	if {$fd ne $current_fd} {
		catch {close $fd}
		return
	}

466
	foreach i $w_columns {$i conf -state normal}
467 468
	while {[gets $fd line] >= 0} {
		regsub "\r\$" $line {} line
469
		incr total_lines
470
		lappend amov_data {}
471
		lappend asim_data {}
472

473
		if {$total_lines > 1} {
474
			foreach i $w_columns {$i insert end "\n"}
475 476 477 478
		}

		$w_line insert end "$total_lines" linenumber
		$w_file insert end "$line"
479
	}
480 481 482 483 484 485

	set ln_wc [expr {[string length $total_lines] + 2}]
	if {[$w_line cget -width] < $ln_wc} {
		$w_line conf -width $ln_wc
	}

486
	foreach i $w_columns {$i conf -state disabled}
487 488 489

	if {[eof $fd]} {
		close $fd
490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505

		# If we don't force Tk to update the widgets *right now*
		# none of our jump commands will cause a change in the UI.
		#
		update

		if {[llength $jump] == 1} {
			set highlight_line [lindex $jump 0]
			$w_file see "$highlight_line.0"
		} elseif {[llength $jump] == 4} {
			set highlight_column [lindex $jump 0]
			set highlight_line [lindex $jump 1]
			$w_file xview moveto [lindex $jump 2]
			$w_file yview moveto [lindex $jump 3]
		}

506 507
		_exec_blame $this $w_asim @asim_data \
			[list] \
508
			[mc "Loading copy/move tracking annotations..."]
509
	}
510
} ifdeleted { catch {close $fd} }
511

512
method _exec_blame {cur_w cur_d options cur_s} {
513
	lappend options --incremental
514
	if {$commit eq {}} {
515
		lappend options --contents $path
516
	} else {
517
		lappend options $commit
518
	}
519 520
	lappend options -- $path
	set fd [eval git_read --nice blame $options]
521
	fconfigure $fd -blocking 0 -translation lf -encoding utf-8
522
	fileevent $fd readable [cb _read_blame $fd $cur_w $cur_d]
523 524
	set current_fd $fd
	set blame_lines 0
525 526

	$status start \
527 528
		$cur_s \
		[mc "lines annotated"]
529 530
}

531
method _read_blame {fd cur_w cur_d} {
532
	upvar #0 $cur_d line_data
533
	variable group_colors
534

535 536 537 538 539
	if {$fd ne $current_fd} {
		catch {close $fd}
		return
	}

540
	$cur_w conf -state normal
541 542 543
	while {[gets $fd line] >= 0} {
		if {[regexp {^([a-z0-9]{40}) (\d+) (\d+) (\d+)$} $line line \
			cmit original_line final_line line_count]} {
544 545 546 547
			set r_commit     $cmit
			set r_orig_line  $original_line
			set r_final_line $final_line
			set r_line_count $line_count
548 549
		} elseif {[string match {filename *} $line]} {
			set file [string range $line 9 end]
550 551
			set n    $r_line_count
			set lno  $r_final_line
552
			set oln  $r_orig_line
553
			set cmit $r_commit
554 555

			if {[regexp {^0{40}$} $cmit]} {
556
				set commit_abbr work
557 558 559 560
				set commit_type curr_commit
			} elseif {$cmit eq $commit} {
				set commit_abbr this
				set commit_type curr_commit
561
			} else {
562
				set commit_type prior_commit
563
				set commit_abbr [string range $cmit 0 3]
564 565
			}

566 567 568 569
			set author_abbr {}
			set a_name {}
			catch {set a_name $header($cmit,author)}
			while {$a_name ne {}} {
570 571 572 573
				if {$author_abbr ne {}
					&& [string index $a_name 0] eq {'}} {
					regsub {^'[^']+'\s+} $a_name {} a_name
				}
574 575 576 577 578 579 580 581 582 583 584
				if {![regexp {^([[:upper:]])} $a_name _a]} break
				append author_abbr $_a
				unset _a
				if {![regsub \
					{^[[:upper:]][^\s]*\s+} \
					$a_name {} a_name ]} break
			}
			if {$author_abbr eq {}} {
				set author_abbr { |}
			} else {
				set author_abbr [string range $author_abbr 0 3]
585
			}
586 587 588 589
			unset a_name

			set first_lno $lno
			while {
590
			   $first_lno > 1
591 592
			&& $cmit eq [lindex $line_data [expr {$first_lno - 1}] 0]
			&& $file eq [lindex $line_data [expr {$first_lno - 1}] 1]
593 594 595
			} {
				incr first_lno -1
			}
596

597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620
			set color {}
			if {$first_lno < $lno} {
				foreach g [$w_file tag names $first_lno.0] {
					if {[regexp {^color[0-9]+$} $g]} {
						set color $g
						break
					}
				}
			} else {
				set i [lsort [concat \
					[$w_file tag names "[expr {$first_lno - 1}].0"] \
					[$w_file tag names "[expr {$lno + $n}].0"] \
					]]
				for {set g 0} {$g < [llength $group_colors]} {incr g} {
					if {[lsearch -sorted -exact $i color$g] == -1} {
						set color color$g
						break
					}
				}
			}
			if {$color eq {}} {
				set color color0
			}

621
			while {$n > 0} {
622
				set lno_e "$lno.0 lineend + 1c"
623 624
				if {[lindex $line_data $lno] ne {}} {
					set g [lindex $line_data $lno 0]
625 626 627
					foreach i $w_columns {
						$i tag remove g$g $lno.0 $lno_e
					}
628
				}
629
				lset line_data $lno [list $cmit $file $oln]
630

631
				$cur_w delete $lno.0 "$lno.0 lineend"
632
				if {$lno == $first_lno} {
633
					$cur_w insert $lno.0 $commit_abbr $commit_type
634
				} elseif {$lno == [expr {$first_lno + 1}]} {
635
					$cur_w insert $lno.0 $author_abbr author_abbr
636
				} else {
637
					$cur_w insert $lno.0 { |}
638
				}
639

640
				foreach i $w_columns {
641 642 643 644 645 646 647 648
					if {$cur_w eq $w_amov} {
						for {set g 0} \
							{$g < [llength $group_colors]} \
							{incr g} {
							$i tag remove color$g $lno.0 $lno_e
						}
						$i tag add $color $lno.0 $lno_e
					}
649 650
					$i tag add g$cmit $lno.0 $lno_e
				}
651

652 653 654
				if {$highlight_column eq $cur_w} {
					if {$highlight_line == -1
					 && [lindex [$w_file yview] 0] == 0} {
655
						$w_file see $lno.0
656 657 658 659
						set highlight_line $lno
					}
					if {$highlight_line == $lno} {
						_showcommit $this $cur_w $lno
660 661 662 663 664
					}
				}

				incr n -1
				incr lno
665
				incr oln
666
				incr blame_lines
667 668
			}

669
			while {
670 671
			   $cmit eq [lindex $line_data $lno 0]
			&& $file eq [lindex $line_data $lno 1]
672
			} {
673
				$cur_w delete $lno.0 "$lno.0 lineend"
674 675

				if {$lno == $first_lno} {
676
					$cur_w insert $lno.0 $commit_abbr $commit_type
677
				} elseif {$lno == [expr {$first_lno + 1}]} {
678
					$cur_w insert $lno.0 $author_abbr author_abbr
679
				} else {
680
					$cur_w insert $lno.0 { |}
681
				}
682 683 684 685 686 687 688 689 690 691 692 693

				if {$cur_w eq $w_amov} {
					foreach i $w_columns {
						for {set g 0} \
							{$g < [llength $group_colors]} \
							{incr g} {
							$i tag remove color$g $lno.0 $lno_e
						}
						$i tag add $color $lno.0 $lno_e
					}
				}

694
				incr lno
695
			}
696

697 698
		} elseif {[regexp {^([a-z-]+) (.*)$} $line line key data]} {
			set header($r_commit,$key) $data
699 700
		}
	}
701
	$cur_w conf -state disabled
702 703 704

	if {[eof $fd]} {
		close $fd
705
		if {$cur_w eq $w_asim} {
706 707 708 709 710 711 712 713 714 715 716 717
			# Switches for original location detection
			set threshold [get_config gui.copyblamethreshold]
			set original_options [list "-C$threshold"]

			if {![is_config_true gui.fastcopyblame]} {
				# thorough copy search; insert before the threshold
				set original_options [linsert $original_options 0 -C]
			}
			if {[git-version >= 1.5.3]} {
				lappend original_options -w ; # ignore indentation changes
			}

718
			_exec_blame $this $w_amov @amov_data \
719
				$original_options \
720
				[mc "Loading original location annotations..."]
721 722
		} else {
			set current_fd {}
723
			$status stop [mc "Annotation complete."]
724
		}
725
	} else {
726
		$status update $blame_lines $total_lines
727
	}
728
} ifdeleted { catch {close $fd} }
729

730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795
method _find_commit_bound {data_list start_idx delta} {
	upvar #0 $data_list line_data
	set pos $start_idx
	set limit       [expr {[llength $line_data] - 1}]
	set base_commit [lindex $line_data $pos 0]

	while {$pos > 0 && $pos < $limit} {
		set new_pos [expr {$pos + $delta}]
		if {[lindex $line_data $new_pos 0] ne $base_commit} {
			return $pos
		}

		set pos $new_pos
	}

	return $pos
}

method _fullcopyblame {} {
	if {$current_fd ne {}} {
		tk_messageBox \
			-icon error \
			-type ok \
			-title [mc "Busy"] \
			-message [mc "Annotation process is already running."]

		return
	}

	# Switches for original location detection
	set threshold [get_config gui.copyblamethreshold]
	set original_options [list -C -C "-C$threshold"]

	if {[git-version >= 1.5.3]} {
		lappend original_options -w ; # ignore indentation changes
	}

	# Find the line range
	set pos @$::cursorX,$::cursorY
	set lno [lindex [split [$::cursorW index $pos] .] 0]
	set min_amov_lno [_find_commit_bound $this @amov_data $lno -1]
	set max_amov_lno [_find_commit_bound $this @amov_data $lno 1]
	set min_asim_lno [_find_commit_bound $this @asim_data $lno -1]
	set max_asim_lno [_find_commit_bound $this @asim_data $lno 1]

	if {$min_asim_lno < $min_amov_lno} {
		set min_amov_lno $min_asim_lno
	}

	if {$max_asim_lno > $max_amov_lno} {
		set max_amov_lno $max_asim_lno
	}

	lappend original_options -L "$min_amov_lno,$max_amov_lno"

	# Clear lines
	for {set i $min_amov_lno} {$i <= $max_amov_lno} {incr i} {
		lset amov_data $i [list ]
	}

	# Start the back-end process
	_exec_blame $this $w_amov @amov_data \
		$original_options \
		[mc "Running thorough copy detection..."]
}

796
method _click {cur_w pos} {
797
	set lno [lindex [split [$cur_w index $pos] .] 0]
798
	_showcommit $this $cur_w $lno
799 800
}

801 802 803 804 805 806 807 808 809 810
method _setencoding {enc} {
	force_path_encoding $path $enc
	_load $this [list \
		$highlight_column \
		$highlight_line \
		[lindex [$w_file xview] 0] \
		[lindex [$w_file yview] 0] \
		]
}

811 812 813 814
method _load_commit {cur_w cur_d pos} {
	upvar #0 $cur_d line_data
	set lno [lindex [split [$cur_w index $pos] .] 0]
	set dat [lindex $line_data $lno]
815
	if {$dat ne {}} {
816 817 818 819
		_load_new_commit $this  \
			[lindex $dat 0] \
			[lindex $dat 1] \
			[list [lindex $dat 2]]
820
	}
821 822
}

823 824 825 826 827 828 829 830 831 832 833 834 835 836
method _load_new_commit {new_commit new_path jump} {
	lappend history [list \
		$commit $path \
		$highlight_column \
		$highlight_line \
		[lindex [$w_file xview] 0] \
		[lindex [$w_file yview] 0] \
		]

	set commit $new_commit
	set path   $new_path
	_load $this $jump
}

837
method _showcommit {cur_w lno} {
838
	global repo_config
839
	variable active_color
840

841
	if {$highlight_commit ne {}} {
842
		foreach i $w_columns {
843
			$i tag conf g$highlight_commit -background {}
844
			$i tag lower g$highlight_commit
845
		}
846 847
	}

848
	if {$cur_w eq $w_asim} {
849 850
		set dat [lindex $asim_data $lno]
		set highlight_column $w_asim
851 852 853
	} else {
		set dat [lindex $amov_data $lno]
		set highlight_column $w_amov
854 855
	}

856 857
	$w_cviewer conf -state normal
	$w_cviewer delete 0.0 end
858 859

	if {$dat eq {}} {
860
		set cmit {}
861
		$w_cviewer insert end [mc "Loading annotation..."] still_loading
862
	} else {
863 864 865
		set cmit [lindex $dat 0]
		set file [lindex $dat 1]

866 867
		foreach i $w_columns {
			$i tag conf g$cmit -background $active_color
868
			$i tag raise g$cmit
869
		}
870 871 872 873

		set author_name {}
		set author_email {}
		set author_time {}
874 875
		catch {set author_name $header($cmit,author)}
		catch {set author_email $header($cmit,author-mail)}
876
		catch {set author_time [format_date $header($cmit,author-time)]}
877 878 879 880

		set committer_name {}
		set committer_email {}
		set committer_time {}
881 882
		catch {set committer_name $header($cmit,committer)}
		catch {set committer_email $header($cmit,committer-mail)}
883
		catch {set committer_time [format_date $header($cmit,committer-time)]}
884

885
		if {[catch {set msg $header($cmit,message)}]} {
886 887
			set msg {}
			catch {
888
				set fd [git_read cat-file commit $cmit]
889 890 891 892 893 894 895 896 897
				fconfigure $fd -encoding binary -translation lf
				if {[catch {set enc $repo_config(i18n.commitencoding)}]} {
					set enc utf-8
				}
				while {[gets $fd line] > 0} {
					if {[string match {encoding *} $line]} {
						set enc [string tolower [string range $line 9 end]]
					}
				}
898
				set msg [read $fd]
899 900
				close $fd

901 902 903 904 905
				set enc [tcl_encoding $enc]
				if {$enc ne {}} {
					set msg [encoding convertfrom $enc $msg]
				}
				set msg [string trim $msg]
906
			}
907
			set header($cmit,message) $msg
908 909
		}

910
		$w_cviewer insert end "commit $cmit\n" header_key
911
		$w_cviewer insert end [strcat [mc "Author:"] "\t"] header_key
912 913
		$w_cviewer insert end "$author_name $author_email" header_val
		$w_cviewer insert end "  $author_time\n" header_val
914

915
		$w_cviewer insert end [strcat [mc "Committer:"] "\t"] header_key
916 917
		$w_cviewer insert end "$committer_name $committer_email" header_val
		$w_cviewer insert end "  $committer_time\n" header_val
918

919
		if {$file ne $path} {
920
			$w_cviewer insert end [strcat [mc "Original File:"] "\t"] header_key
921
			$w_cviewer insert end "[escape_path $file]\n" header_val
922
		}
923

924
		$w_cviewer insert end "\n$msg"
925
	}
926
	$w_cviewer conf -state disabled
927

928 929
	set highlight_line $lno
	set highlight_commit $cmit
930

931
	if {[lsearch -exact $tooltip_commit $highlight_commit] != -1} {
932 933
		_hide_tooltip $this
	}
934 935
}

936
method _get_click_amov_info {} {
937 938
	set pos @$::cursorX,$::cursorY
	set lno [lindex [split [$::cursorW index $pos] .] 0]
939 940 941 942 943
	return [lindex $amov_data $lno]
}

method _copycommit {} {
	set dat [_get_click_amov_info $this]
944
	if {$dat ne {}} {
945 946 947 948
		clipboard clear
		clipboard append \
			-format STRING \
			-type STRING \
949
			-- [lindex $dat 0]
950 951
	}
}
952

953 954 955 956 957 958
method _format_offset_date {base offset} {
	set exval [expr {$base + $offset*24*60*60}]
	return [clock format $exval -format {%Y-%m-%d}]
}

method _gitkcommit {} {
959 960
	global nullid

961 962 963
	set dat [_get_click_amov_info $this]
	if {$dat ne {}} {
		set cmit [lindex $dat 0]
964 965 966 967 968 969 970 971 972

		# If the line belongs to the working copy, use HEAD instead
		if {$cmit eq $nullid} {
			if {[catch {set cmit [git rev-parse --verify HEAD]} err]} {
				error_popup [strcat [mc "Cannot find HEAD commit:"] "\n\n$err"]
				return;
			}
		}

973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007
		set radius [get_config gui.blamehistoryctx]
		set cmdline [list --select-commit=$cmit]

                if {$radius > 0} {
			set author_time {}
			set committer_time {}

			catch {set author_time $header($cmit,author-time)}
			catch {set committer_time $header($cmit,committer-time)}

			if {$committer_time eq {}} {
				set committer_time $author_time
			}

			set after_time [_format_offset_date $this $committer_time [expr {-$radius}]]
			set before_time [_format_offset_date $this $committer_time $radius]

			lappend cmdline --after=$after_time --before=$before_time
		}

		lappend cmdline $cmit

		set base_rev "HEAD"
		if {$commit ne {}} {
			set base_rev $commit
		}

		if {$base_rev ne $cmit} {
			lappend cmdline $base_rev
		}

		do_gitk $cmdline
	}
}

1008
method _blameparent {} {
1009 1010
	global nullid

1011 1012 1013
	set dat [_get_click_amov_info $this]
	if {$dat ne {}} {
		set cmit [lindex $dat 0]
1014
		set new_path [lindex $dat 1]
1015

1016 1017 1018 1019 1020 1021 1022
		# Allow using Blame Parent on lines modified in the working copy
		if {$cmit eq $nullid} {
			set parent_ref "HEAD"
		} else {
			set parent_ref "$cmit^"
		}
		if {[catch {set cparent [git rev-parse --verify $parent_ref]} err]} {
1023 1024 1025 1026
			error_popup [strcat [mc "Cannot find parent commit:"] "\n\n$err"]
			return;
		}

1027 1028 1029 1030 1031
		_kill $this

		# Generate a diff between the commit and its parent,
		# and use the hunks to update the line number.
		# Request zero context to simplify calculations.
1032 1033 1034 1035 1036 1037
		if {$cmit eq $nullid} {
			set diffcmd [list diff-index --unified=0 $cparent -- $new_path]
		} else {
			set diffcmd [list diff-tree --unified=0 $cparent $cmit -- $new_path]
		}
		if {[catch {set fd [eval git_read $diffcmd]} err]} {
1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051
			$status stop [mc "Unable to display parent"]
			error_popup [strcat [mc "Error loading diff:"] "\n\n$err"]
			return
		}

		set r_orig_line [lindex $dat 2]

		fconfigure $fd \
			-blocking 0 \
			-encoding binary \
			-translation binary
		fileevent $fd readable [cb _read_diff_load_commit \
			$fd $cparent $new_path $r_orig_line]
		set current_fd $fd
1052 1053 1054
	}
}

1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093
method _read_diff_load_commit {fd cparent new_path tline} {
	if {$fd ne $current_fd} {
		catch {close $fd}
		return
	}

	while {[gets $fd line] >= 0} {
		if {[regexp {^@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@} $line line \
			old_line osz old_size new_line nsz new_size]} {

			if {$osz eq {}} { set old_size 1 }
			if {$nsz eq {}} { set new_size 1 }

			if {$new_line <= $tline} {
				if {[expr {$new_line + $new_size}] > $tline} {
					# Target line within the hunk
					set line_shift [expr {
						($new_size-$old_size)*($tline-$new_line)/$new_size
						}]
				} else {
					set line_shift [expr {$new_size-$old_size}]
				}

				set r_orig_line [expr {$r_orig_line - $line_shift}]
			}
		}
	}

	if {[eof $fd]} {
		close $fd;
		set current_fd {}

		_load_new_commit $this  \
			$cparent        \
			$new_path       \
			[list $r_orig_line]
	}
} ifdeleted { catch {close $fd} }

1094
method _show_tooltip {cur_w pos} {
1095
	if {$tooltip_wm ne {}} {
1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110
		_open_tooltip $this $cur_w
	} elseif {$tooltip_timer eq {}} {
		set tooltip_timer [after 1000 [cb _open_tooltip $cur_w]]
	}
}

method _open_tooltip {cur_w} {
	set tooltip_timer {}
	set pos_x [winfo pointerx $cur_w]
	set pos_y [winfo pointery $cur_w]
	if {[winfo containing $pos_x $pos_y] ne $cur_w} {
		_hide_tooltip $this
		return
	}

1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132
	if {$tooltip_wm ne "$cur_w.tooltip"} {
		_hide_tooltip $this

		set tooltip_wm [toplevel $cur_w.tooltip -borderwidth 1]
		wm overrideredirect $tooltip_wm 1
		wm transient $tooltip_wm [winfo toplevel $cur_w]
		set tooltip_t $tooltip_wm.label
		text $tooltip_t \
			-takefocus 0 \
			-highlightthickness 0 \
			-relief flat \
			-borderwidth 0 \
			-wrap none \
			-background lightyellow \
			-foreground black
		$tooltip_t tag conf section_header -font font_uibold
		pack $tooltip_t
	} else {
		$tooltip_t conf -state normal
		$tooltip_t delete 0.0 end
	}

1133 1134 1135 1136
	set pos @[join [list \
		[expr {$pos_x - [winfo rootx $cur_w]}] \
		[expr {$pos_y - [winfo rooty $cur_w]}]] ,]
	set lno [lindex [split [$cur_w index $pos] .] 0]
1137 1138
	if {$cur_w eq $w_amov} {
		set dat [lindex $amov_data $lno]
1139
		set org {}
1140 1141
	} else {
		set dat [lindex $asim_data $lno]
1142
		set org [lindex $amov_data $lno]
1143
	}
1144

1145 1146 1147 1148 1149
	if {$dat eq {}} {
		_hide_tooltip $this
		return
	}

1150
	set cmit [lindex $dat 0]
1151
	set tooltip_commit [list $cmit]
1152 1153

	set author_name {}
1154
	set summary     {}
1155 1156
	set author_time {}
	catch {set author_name $header($cmit,author)}
1157
	catch {set summary     $header($cmit,summary)}
1158
	catch {set author_time [format_date $header($cmit,author-time)]}
1159

1160 1161 1162
	$tooltip_t insert end "commit $cmit\n"
	$tooltip_t insert end "$author_name  $author_time\n"
	$tooltip_t insert end "$summary"
1163

1164
	if {$org ne {} && [lindex $org 0] ne $cmit} {
1165 1166 1167
		set save [$tooltip_t get 0.0 end]
		$tooltip_t delete 0.0 end

1168 1169 1170
		set cmit [lindex $org 0]
		set file [lindex $org 1]
		lappend tooltip_commit $cmit
1171

1172 1173 1174 1175 1176
		set author_name {}
		set summary     {}
		set author_time {}
		catch {set author_name $header($cmit,author)}
		catch {set summary     $header($cmit,summary)}
1177
		catch {set author_time [format_date $header($cmit,author-time)]}
1178

1179
		$tooltip_t insert end [strcat [mc "Originally By:"] "\n"] section_header
1180 1181
		$tooltip_t insert end "commit $cmit\n"
		$tooltip_t insert end "$author_name  $author_time\n"
1182
		$tooltip_t insert end "$summary\n"
1183

1184
		if {$file ne $path} {
1185
			$tooltip_t insert end [strcat [mc "In File:"] " "] section_header
1186
			$tooltip_t insert end "$file\n"
1187
		}
1188 1189

		$tooltip_t insert end "\n"
1190
		$tooltip_t insert end [strcat [mc "Copied Or Moved Here By:"] "\n"] section_header
1191
		$tooltip_t insert end $save
1192 1193
	}

1194
	$tooltip_t conf -state disabled
1195 1196 1197 1198
	_position_tooltip $this
}

method _position_tooltip {} {
1199 1200 1201 1202 1203 1204 1205 1206 1207 1208
	set max_h [lindex [split [$tooltip_t index end] .] 0]
	set max_w 0
	for {set i 1} {$i <= $max_h} {incr i} {
		set c [lindex [split [$tooltip_t index "$i.0 lineend"] .] 1]
		if {$c > $max_w} {set max_w $c}
	}
	$tooltip_t conf -width $max_w -height $max_h

	set req_w [winfo reqwidth  $tooltip_t]
	set req_h [winfo reqheight $tooltip_t]
1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233
	set pos_x [expr {[winfo pointerx .] +  5}]
	set pos_y [expr {[winfo pointery .] + 10}]

	set g "${req_w}x${req_h}"
	if {$pos_x >= 0} {append g +}
	append g $pos_x
	if {$pos_y >= 0} {append g +}
	append g $pos_y

	wm geometry $tooltip_wm $g
	raise $tooltip_wm
}

method _hide_tooltip {} {
	if {$tooltip_wm ne {}} {
		destroy $tooltip_wm
		set tooltip_wm {}
		set tooltip_commit {}
	}
	if {$tooltip_timer ne {}} {
		after cancel $tooltip_timer
		set tooltip_timer {}
	}
}

1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248
method _resize {new_height} {
	set diff [expr {$new_height - $old_height}]
	if {$diff == 0} return

	set my [expr {[winfo height $w.file_pane] - 25}]
	set o [$w.file_pane sash coord 0]
	set ox [lindex $o 0]
	set oy [expr {[lindex $o 1] + $diff}]
	if {$oy < 0}   {set oy 0}
	if {$oy > $my} {set oy $my}
	$w.file_pane sash place 0 $ox $oy

	set old_height $new_height
}

1249
}