diff.tcl 15.8 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
# git-gui diff viewer
# Copyright (C) 2006, 2007 Shawn Pearce

proc clear_diff {} {
	global ui_diff current_diff_path current_diff_header
	global ui_index ui_workdir

	$ui_diff conf -state normal
	$ui_diff delete 0.0 end
	$ui_diff conf -state disabled

	set current_diff_path {}
	set current_diff_header {}

	$ui_index tag remove in_diff 0.0 end
	$ui_workdir tag remove in_diff 0.0 end
}

proc reshow_diff {} {
20
	global file_states file_lists
21
	global current_diff_path current_diff_side
22
	global ui_diff
23 24 25 26

	set p $current_diff_path
	if {$p eq {}} {
		# No diff is being shown.
27
	} elseif {$current_diff_side eq {}} {
28
		clear_diff
29 30 31 32 33 34 35 36
	} elseif {[catch {set s $file_states($p)}]
		|| [lsearch -sorted -exact $file_lists($current_diff_side) $p] == -1} {

		if {[find_next_diff $current_diff_side $p {} {[^O]}]} {
			next_diff
		} else {
			clear_diff
		}
37
	} else {
38 39
		set save_pos [lindex [$ui_diff yview] 0]
		show_diff $p $current_diff_side {} $save_pos
40 41 42
	}
}

43 44 45 46 47 48 49 50 51
proc force_diff_encoding {enc} {
	global current_diff_path
	
	if {$current_diff_path ne {}} {
		force_path_encoding $current_diff_path $enc
		reshow_diff
	}
}

52 53 54 55 56 57 58
proc handle_empty_diff {} {
	global current_diff_path file_states file_lists

	set path $current_diff_path
	set s $file_states($path)
	if {[lindex $s 0] ne {_M}} return

59
	info_popup [mc "No differences detected.
60

61
%s has no changes.
62 63 64

The modification date of this file was updated by another application, but the content within the file was not changed.

65
A rescan will be automatically started to find other files which may have the same state." [short_path $path]]
66 67 68

	clear_diff
	display_file $path __
69
	rescan ui_ready 0
70 71
}

72
proc show_diff {path w {lno {}} {scroll_pos {}} {callback {}}} {
73
	global file_states file_lists
74
	global is_3way_diff is_conflict_diff diff_active repo_config
75
	global ui_diff ui_index ui_workdir
76
	global current_diff_path current_diff_side current_diff_header
77
	global current_diff_queue
78 79 80 81 82 83 84 85 86 87 88 89

	if {$diff_active || ![lock_index read]} return

	clear_diff
	if {$lno == {}} {
		set lno [lsearch -sorted -exact $file_lists($w) $path]
		if {$lno >= 0} {
			incr lno
		}
	}
	if {$lno >= 1} {
		$w tag add in_diff $lno.0 [expr {$lno + 1}].0
90
		$w see $lno.0
91 92 93 94
	}

	set s $file_states($path)
	set m [lindex $s 0]
95
	set is_conflict_diff 0
96 97
	set current_diff_path $path
	set current_diff_side $w
98
	set current_diff_queue {}
99
	ui_status [mc "Loading diff of %s..." [escape_path $path]]
100

101 102
	set cont_info [list $scroll_pos $callback]

103
	if {[string first {U} $m] >= 0} {
104
		merge_load_stages $path [list show_unmerged_diff $cont_info]
105
	} elseif {$m eq {_O}} {
106
		show_other_diff $path $w $m $cont_info
107
	} else {
108
		start_show_diff $cont_info
109 110 111
	}
}

112
proc show_unmerged_diff {cont_info} {
113
	global current_diff_path current_diff_side
114
	global merge_stages ui_diff is_conflict_diff
115 116 117
	global current_diff_queue

	if {$merge_stages(2) eq {}} {
118
		set is_conflict_diff 1
119 120 121 122
		lappend current_diff_queue \
			[list "LOCAL: deleted\nREMOTE:\n" d======= \
			    [list ":1:$current_diff_path" ":3:$current_diff_path"]]
	} elseif {$merge_stages(3) eq {}} {
123
		set is_conflict_diff 1
124 125 126 127 128 129
		lappend current_diff_queue \
			[list "REMOTE: deleted\nLOCAL:\n" d======= \
			    [list ":1:$current_diff_path" ":2:$current_diff_path"]]
	} elseif {[lindex $merge_stages(1) 0] eq {120000}
		|| [lindex $merge_stages(2) 0] eq {120000}
		|| [lindex $merge_stages(3) 0] eq {120000}} {
130
		set is_conflict_diff 1
131 132 133 134 135 136 137
		lappend current_diff_queue \
			[list "LOCAL:\n" d======= \
			    [list ":1:$current_diff_path" ":2:$current_diff_path"]]
		lappend current_diff_queue \
			[list "REMOTE:\n" d======= \
			    [list ":1:$current_diff_path" ":3:$current_diff_path"]]
	} else {
138
		start_show_diff $cont_info
139 140 141
		return
	}

142
	advance_diff_queue $cont_info
143 144
}

145
proc advance_diff_queue {cont_info} {
146 147 148 149 150 151 152 153 154
	global current_diff_queue ui_diff

	set item [lindex $current_diff_queue 0]
	set current_diff_queue [lrange $current_diff_queue 1 end]

	$ui_diff conf -state normal
	$ui_diff insert end [lindex $item 0] [lindex $item 1]
	$ui_diff conf -state disabled

155
	start_show_diff $cont_info [lindex $item 2]
156 157
}

158
proc show_other_diff {path w m cont_info} {
159 160 161 162 163
	global file_states file_lists
	global is_3way_diff diff_active repo_config
	global ui_diff ui_index ui_workdir
	global current_diff_path current_diff_side current_diff_header

164 165 166
	# - Git won't give us the diff, there's nothing to compare to!
	#
	if {$m eq {_O}} {
167
		set max_sz 100000
168
		set type unknown
169
		if {[catch {
170 171 172 173 174 175 176 177
				set type [file type $path]
				switch -- $type {
				directory {
					set type submodule
					set content {}
					set sz 0
				}
				link {
178 179
					set content [file readlink $path]
					set sz [string length $content]
180 181
				}
				file {
182
					set fd [open $path r]
183 184
					fconfigure $fd \
						-eofchar {} \
185
						-encoding [get_path_encoding $path]
186 187 188 189
					set content [read $fd $max_sz]
					close $fd
					set sz [file size $path]
				}
190 191 192 193
				default {
					error "'$type' not supported"
				}
				}
194 195 196
			} err ]} {
			set diff_active 0
			unlock_index
197
			ui_status [mc "Unable to display %s" [escape_path $path]]
198
			error_popup [strcat [mc "Error loading file:"] "\n\n$err"]
199 200 201
			return
		}
		$ui_diff conf -state normal
202
		if {$type eq {submodule}} {
S
Shawn O. Pearce 已提交
203 204 205 206
			$ui_diff insert end [append \
				"* " \
				[mc "Git Repository (subproject)"] \
				"\n"] d_@
207
		} elseif {![catch {set type [exec file $path]}]} {
208 209 210 211 212 213 214 215 216
			set n [string length $path]
			if {[string equal -length $n $path $type]} {
				set type [string range $type $n end]
				regsub {^:?\s*} $type {} type
			}
			$ui_diff insert end "* $type\n" d_@
		}
		if {[string first "\0" $content] != -1} {
			$ui_diff insert end \
217
				[mc "* Binary file (not showing content)."] \
218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236
				d_@
		} else {
			if {$sz > $max_sz} {
				$ui_diff insert end \
"* Untracked file is $sz bytes.
* Showing only first $max_sz bytes.
" d_@
			}
			$ui_diff insert end $content
			if {$sz > $max_sz} {
				$ui_diff insert end "
* Untracked file clipped here by [appname].
* To see the entire file, use an external editor.
" d_@
			}
		}
		$ui_diff conf -state disabled
		set diff_active 0
		unlock_index
237
		set scroll_pos [lindex $cont_info 0]
238 239 240 241
		if {$scroll_pos ne {}} {
			update
			$ui_diff yview moveto $scroll_pos
		}
242
		ui_ready
243 244 245 246
		set callback [lindex $cont_info 1]
		if {$callback ne {}} {
			eval $callback
		}
247 248
		return
	}
249 250
}

251
proc start_show_diff {cont_info {add_opts {}}} {
252 253 254 255 256 257 258 259 260 261 262 263 264
	global file_states file_lists
	global is_3way_diff diff_active repo_config
	global ui_diff ui_index ui_workdir
	global current_diff_path current_diff_side current_diff_header

	set path $current_diff_path
	set w $current_diff_side

	set s $file_states($path)
	set m [lindex $s 0]
	set is_3way_diff 0
	set diff_active 1
	set current_diff_header {}
265

266
	set cmd [list]
267 268 269 270
	if {$w eq $ui_index} {
		lappend cmd diff-index
		lappend cmd --cached
	} elseif {$w eq $ui_workdir} {
271
		if {[string first {U} $m] >= 0} {
272 273 274 275 276 277 278 279
			lappend cmd diff
		} else {
			lappend cmd diff-files
		}
	}

	lappend cmd -p
	lappend cmd --no-color
280
	if {$repo_config(gui.diffcontext) >= 1} {
281 282 283 284 285
		lappend cmd "-U$repo_config(gui.diffcontext)"
	}
	if {$w eq $ui_index} {
		lappend cmd [PARENT]
	}
286 287 288 289 290 291
	if {$add_opts ne {}} {
		eval lappend cmd $add_opts
	} else {
		lappend cmd --
		lappend cmd $path
	}
292

293
	if {[catch {set fd [eval git_read --nice $cmd]} err]} {
294 295
		set diff_active 0
		unlock_index
296
		ui_status [mc "Unable to display %s" [escape_path $path]]
297
		error_popup [strcat [mc "Error loading diff:"] "\n\n$err"]
298 299 300
		return
	}

301
	set ::current_diff_inheader 1
302 303
	fconfigure $fd \
		-blocking 0 \
304
		-encoding [get_path_encoding $path] \
305
		-translation lf
306
	fileevent $fd readable [list read_diff $fd $cont_info]
307 308
}

309
proc read_diff {fd cont_info} {
310
	global ui_diff diff_active
311
	global is_3way_diff is_conflict_diff current_diff_header
312
	global current_diff_queue
313 314 315 316 317

	$ui_diff conf -state normal
	while {[gets $fd line] >= 0} {
		# -- Cleanup uninteresting diff header lines.
		#
318 319 320 321 322 323 324 325 326
		if {$::current_diff_inheader} {
			if {   [string match {diff --git *}      $line]
			    || [string match {diff --cc *}       $line]
			    || [string match {diff --combined *} $line]
			    || [string match {--- *}             $line]
			    || [string match {+++ *}             $line]} {
				append current_diff_header $line "\n"
				continue
			}
327 328 329 330 331
		}
		if {[string match {index *} $line]} continue
		if {$line eq {deleted file mode 120000}} {
			set line "deleted symlink"
		}
332
		set ::current_diff_inheader 0
333 334 335 336 337 338 339

		# -- Automatically detect if this is a 3 way diff.
		#
		if {[string match {@@@ *} $line]} {set is_3way_diff 1}

		if {[string match {mode *} $line]
			|| [string match {new file *} $line]
340
			|| [regexp {^(old|new) mode *} $line]
341
			|| [string match {deleted file *} $line]
342
			|| [string match {deleted symlink} $line]
343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358
			|| [string match {Binary files * and * differ} $line]
			|| $line eq {\ No newline at end of file}
			|| [regexp {^\* Unmerged path } $line]} {
			set tags {}
		} elseif {$is_3way_diff} {
			set op [string range $line 0 1]
			switch -- $op {
			{  } {set tags {}}
			{@@} {set tags d_@}
			{ +} {set tags d_s+}
			{ -} {set tags d_s-}
			{+ } {set tags d_+s}
			{- } {set tags d_-s}
			{--} {set tags d_--}
			{++} {
				if {[regexp {^\+\+([<>]{7} |={7})} $line _g op]} {
359
					set is_conflict_diff 1
360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378
					set line [string replace $line 0 1 {  }]
					set tags d$op
				} else {
					set tags d_++
				}
			}
			default {
				puts "error: Unhandled 3 way diff marker: {$op}"
				set tags {}
			}
			}
		} else {
			set op [string index $line 0]
			switch -- $op {
			{ } {set tags {}}
			{@} {set tags d_@}
			{-} {set tags d_-}
			{+} {
				if {[regexp {^\+([<>]{7} |={7})} $line _g op]} {
379
					set is_conflict_diff 1
380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401
					set line [string replace $line 0 0 { }]
					set tags d$op
				} else {
					set tags d_+
				}
			}
			default {
				puts "error: Unhandled 2 way diff marker: {$op}"
				set tags {}
			}
			}
		}
		$ui_diff insert end $line $tags
		if {[string index $line end] eq "\r"} {
			$ui_diff tag add d_cr {end - 2c}
		}
		$ui_diff insert end "\n" $tags
	}
	$ui_diff conf -state disabled

	if {[eof $fd]} {
		close $fd
402 403

		if {$current_diff_queue ne {}} {
404
			advance_diff_queue $cont_info
405 406 407
			return
		}

408 409
		set diff_active 0
		unlock_index
410
		set scroll_pos [lindex $cont_info 0]
411 412 413 414
		if {$scroll_pos ne {}} {
			update
			$ui_diff yview moveto $scroll_pos
		}
415
		ui_ready
416 417 418 419

		if {[$ui_diff index end] eq {2.0}} {
			handle_empty_diff
		}
420 421 422 423
		set callback [lindex $cont_info 1]
		if {$callback ne {}} {
			eval $callback
		}
424 425 426 427 428 429 430 431 432 433
	}
}

proc apply_hunk {x y} {
	global current_diff_path current_diff_header current_diff_side
	global ui_diff ui_index file_states

	if {$current_diff_path eq {} || $current_diff_header eq {}} return
	if {![lock_index apply_hunk]} return

434
	set apply_cmd {apply --cached --whitespace=nowarn}
435 436
	set mi [lindex $file_states($current_diff_path) 0]
	if {$current_diff_side eq $ui_index} {
437
		set failed_msg [mc "Failed to unstage selected hunk."]
438 439 440 441 442 443
		lappend apply_cmd --reverse
		if {[string index $mi 0] ne {M}} {
			unlock_index
			return
		}
	} else {
444
		set failed_msg [mc "Failed to stage selected hunk."]
445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463
		if {[string index $mi 1] ne {M}} {
			unlock_index
			return
		}
	}

	set s_lno [lindex [split [$ui_diff index @$x,$y] .] 0]
	set s_lno [$ui_diff search -backwards -regexp ^@@ $s_lno.0 0.0]
	if {$s_lno eq {}} {
		unlock_index
		return
	}

	set e_lno [$ui_diff search -forwards -regexp ^@@ "$s_lno + 1 lines" end]
	if {$e_lno eq {}} {
		set e_lno end
	}

	if {[catch {
464
		set enc [get_path_encoding $current_diff_path]
465
		set p [eval git_write $apply_cmd]
466
		fconfigure $p -translation binary -encoding $enc
467 468 469
		puts -nonewline $p $current_diff_header
		puts -nonewline $p [$ui_diff get $s_lno $e_lno]
		close $p} err]} {
470
		error_popup [append $failed_msg "\n\n$err"]
471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493
		unlock_index
		return
	}

	$ui_diff conf -state normal
	$ui_diff delete $s_lno $e_lno
	$ui_diff conf -state disabled

	if {[$ui_diff get 1.0 end] eq "\n"} {
		set o _
	} else {
		set o ?
	}

	if {$current_diff_side eq $ui_index} {
		set mi ${o}M
	} elseif {[string index $mi 0] eq {_}} {
		set mi M$o
	} else {
		set mi ?$o
	}
	unlock_index
	display_file $current_diff_path $mi
494
	# This should trigger shift to the next changed file
495
	if {$o eq {_}} {
496
		reshow_diff
497 498
	}
}
499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547

proc apply_line {x y} {
	global current_diff_path current_diff_header current_diff_side
	global ui_diff ui_index file_states

	if {$current_diff_path eq {} || $current_diff_header eq {}} return
	if {![lock_index apply_hunk]} return

	set apply_cmd {apply --cached --whitespace=nowarn}
	set mi [lindex $file_states($current_diff_path) 0]
	if {$current_diff_side eq $ui_index} {
		set failed_msg [mc "Failed to unstage selected line."]
		set to_context {+}
		lappend apply_cmd --reverse
		if {[string index $mi 0] ne {M}} {
			unlock_index
			return
		}
	} else {
		set failed_msg [mc "Failed to stage selected line."]
		set to_context {-}
		if {[string index $mi 1] ne {M}} {
			unlock_index
			return
		}
	}

	set the_l [$ui_diff index @$x,$y]

	# operate only on change lines
	set c1 [$ui_diff get "$the_l linestart"]
	if {$c1 ne {+} && $c1 ne {-}} {
		unlock_index
		return
	}
	set sign $c1

	set i_l [$ui_diff search -backwards -regexp ^@@ $the_l 0.0]
	if {$i_l eq {}} {
		unlock_index
		return
	}
	# $i_l is now at the beginning of a line

	# pick start line number from hunk header
	set hh [$ui_diff get $i_l "$i_l + 1 lines"]
	set hh [lindex [split $hh ,] 0]
	set hln [lindex [split $hh -] 1]

548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594
	# There is a special situation to take care of. Consider this hunk:
	#
	#    @@ -10,4 +10,4 @@
	#     context before
	#    -old 1
	#    -old 2
	#    +new 1
	#    +new 2
	#     context after
	#
	# We used to keep the context lines in the order they appear in the
	# hunk. But then it is not possible to correctly stage only
	# "-old 1" and "+new 1" - it would result in this staged text:
	#
	#    context before
	#    old 2
	#    new 1
	#    context after
	#
	# (By symmetry it is not possible to *un*stage "old 2" and "new 2".)
	#
	# We resolve the problem by introducing an asymmetry, namely, when
	# a "+" line is *staged*, it is moved in front of the context lines
	# that are generated from the "-" lines that are immediately before
	# the "+" block. That is, we construct this patch:
	#
	#    @@ -10,4 +10,5 @@
	#     context before
	#    +new 1
	#     old 1
	#     old 2
	#     context after
	#
	# But we do *not* treat "-" lines that are *un*staged in a special
	# way.
	#
	# With this asymmetry it is possible to stage the change
	# "old 1" -> "new 1" directly, and to stage the change
	# "old 2" -> "new 2" by first staging the entire hunk and
	# then unstaging the change "old 1" -> "new 1".

	# This is non-empty if and only if we are _staging_ changes;
	# then it accumulates the consecutive "-" lines (after converting
	# them to context lines) in order to be moved after the "+" change
	# line.
	set pre_context {}

595 596 597 598 599 600 601 602 603 604 605
	set n 0
	set i_l [$ui_diff index "$i_l + 1 lines"]
	set patch {}
	while {[$ui_diff compare $i_l < "end - 1 chars"] &&
	       [$ui_diff get $i_l "$i_l + 2 chars"] ne {@@}} {
		set next_l [$ui_diff index "$i_l + 1 lines"]
		set c1 [$ui_diff get $i_l]
		if {[$ui_diff compare $i_l <= $the_l] &&
		    [$ui_diff compare $the_l < $next_l]} {
			# the line to stage/unstage
			set ln [$ui_diff get $i_l $next_l]
606 607
			if {$c1 eq {-}} {
				set n [expr $n+1]
608 609 610
				set patch "$patch$pre_context$ln"
			} else {
				set patch "$patch$ln$pre_context"
611
			}
612
			set pre_context {}
613 614 615
		} elseif {$c1 ne {-} && $c1 ne {+}} {
			# context line
			set ln [$ui_diff get $i_l $next_l]
616
			set patch "$patch$pre_context$ln"
617
			set n [expr $n+1]
618
			set pre_context {}
619 620 621
		} elseif {$c1 eq $to_context} {
			# turn change line into context line
			set ln [$ui_diff get "$i_l + 1 chars" $next_l]
622 623 624 625 626
			if {$c1 eq {-}} {
				set pre_context "$pre_context $ln"
			} else {
				set patch "$patch $ln"
			}
627 628 629 630 631 632 633
			set n [expr $n+1]
		}
		set i_l $next_l
	}
	set patch "@@ -$hln,$n +$hln,[eval expr $n $sign 1] @@\n$patch"

	if {[catch {
634
		set enc [get_path_encoding $current_diff_path]
635
		set p [eval git_write $apply_cmd]
636
		fconfigure $p -translation binary -encoding $enc
637 638 639 640 641 642 643 644
		puts -nonewline $p $current_diff_header
		puts -nonewline $p $patch
		close $p} err]} {
		error_popup [append $failed_msg "\n\n$err"]
	}

	unlock_index
}