]> git.ipfire.org Git - thirdparty/git.git/blame - lib/diff.tcl
git-gui: Fixes for Mac OS X TkAqua
[thirdparty/git.git] / lib / diff.tcl
CommitLineData
f522c9b5
SP
1# git-gui diff viewer
2# Copyright (C) 2006, 2007 Shawn Pearce
3
4proc clear_diff {} {
5 global ui_diff current_diff_path current_diff_header
6 global ui_index ui_workdir
7
8 $ui_diff conf -state normal
9 $ui_diff delete 0.0 end
10 $ui_diff conf -state disabled
11
12 set current_diff_path {}
13 set current_diff_header {}
14
15 $ui_index tag remove in_diff 0.0 end
16 $ui_workdir tag remove in_diff 0.0 end
17}
18
7cf4566f 19proc reshow_diff {{after {}}} {
699d5601 20 global file_states file_lists
f522c9b5 21 global current_diff_path current_diff_side
25b8fb1e 22 global ui_diff
f522c9b5
SP
23
24 set p $current_diff_path
25 if {$p eq {}} {
26 # No diff is being shown.
29853b90 27 } elseif {$current_diff_side eq {}} {
f522c9b5 28 clear_diff
29853b90
AG
29 } elseif {[catch {set s $file_states($p)}]
30 || [lsearch -sorted -exact $file_lists($current_diff_side) $p] == -1} {
31
32 if {[find_next_diff $current_diff_side $p {} {[^O]}]} {
7cf4566f 33 next_diff $after
29853b90
AG
34 } else {
35 clear_diff
36 }
f522c9b5 37 } else {
25b8fb1e 38 set save_pos [lindex [$ui_diff yview] 0]
7cf4566f 39 show_diff $p $current_diff_side {} $save_pos $after
f522c9b5
SP
40 }
41}
42
3fe01623
AG
43proc force_diff_encoding {enc} {
44 global current_diff_path
45
46 if {$current_diff_path ne {}} {
47 force_path_encoding $current_diff_path $enc
48 reshow_diff
49 }
50}
51
f522c9b5
SP
52proc handle_empty_diff {} {
53 global current_diff_path file_states file_lists
584fa9cc 54 global diff_empty_count
f522c9b5
SP
55
56 set path $current_diff_path
57 set s $file_states($path)
58 if {[lindex $s 0] ne {_M}} return
59
584fa9cc
AG
60 # Prevent infinite rescan loops
61 incr diff_empty_count
62 if {$diff_empty_count > 1} return
63
1ac17950 64 info_popup [mc "No differences detected.
f522c9b5 65
1ac17950 66%s has no changes.
f522c9b5
SP
67
68The modification date of this file was updated by another application, but the content within the file was not changed.
69
1ac17950 70A rescan will be automatically started to find other files which may have the same state." [short_path $path]]
f522c9b5
SP
71
72 clear_diff
73 display_file $path __
699d5601 74 rescan ui_ready 0
f522c9b5
SP
75}
76
3e34838c 77proc show_diff {path w {lno {}} {scroll_pos {}} {callback {}}} {
f522c9b5 78 global file_states file_lists
3e34838c 79 global is_3way_diff is_conflict_diff diff_active repo_config
699d5601 80 global ui_diff ui_index ui_workdir
f522c9b5 81 global current_diff_path current_diff_side current_diff_header
b2ca4149 82 global current_diff_queue
f522c9b5
SP
83
84 if {$diff_active || ![lock_index read]} return
85
86 clear_diff
87 if {$lno == {}} {
88 set lno [lsearch -sorted -exact $file_lists($w) $path]
89 if {$lno >= 0} {
90 incr lno
91 }
92 }
93 if {$lno >= 1} {
94 $w tag add in_diff $lno.0 [expr {$lno + 1}].0
29853b90 95 $w see $lno.0
f522c9b5
SP
96 }
97
98 set s $file_states($path)
99 set m [lindex $s 0]
3e34838c 100 set is_conflict_diff 0
f522c9b5
SP
101 set current_diff_path $path
102 set current_diff_side $w
b2ca4149 103 set current_diff_queue {}
c8c4854b 104 ui_status [mc "Loading diff of %s..." [escape_path $path]]
f522c9b5 105
3e34838c
AG
106 set cont_info [list $scroll_pos $callback]
107
b2ca4149 108 if {[string first {U} $m] >= 0} {
3e34838c 109 merge_load_stages $path [list show_unmerged_diff $cont_info]
b2ca4149 110 } elseif {$m eq {_O}} {
3e34838c 111 show_other_diff $path $w $m $cont_info
b2ca4149 112 } else {
3e34838c 113 start_show_diff $cont_info
b2ca4149
AG
114 }
115}
116
3e34838c 117proc show_unmerged_diff {cont_info} {
b2ca4149 118 global current_diff_path current_diff_side
3e34838c 119 global merge_stages ui_diff is_conflict_diff
b2ca4149
AG
120 global current_diff_queue
121
122 if {$merge_stages(2) eq {}} {
3e34838c 123 set is_conflict_diff 1
b2ca4149 124 lappend current_diff_queue \
7f15b002 125 [list [mc "LOCAL: deleted\nREMOTE:\n"] d======= \
b2ca4149
AG
126 [list ":1:$current_diff_path" ":3:$current_diff_path"]]
127 } elseif {$merge_stages(3) eq {}} {
3e34838c 128 set is_conflict_diff 1
b2ca4149 129 lappend current_diff_queue \
7f15b002 130 [list [mc "REMOTE: deleted\nLOCAL:\n"] d======= \
b2ca4149
AG
131 [list ":1:$current_diff_path" ":2:$current_diff_path"]]
132 } elseif {[lindex $merge_stages(1) 0] eq {120000}
133 || [lindex $merge_stages(2) 0] eq {120000}
134 || [lindex $merge_stages(3) 0] eq {120000}} {
3e34838c 135 set is_conflict_diff 1
b2ca4149 136 lappend current_diff_queue \
7f15b002 137 [list [mc "LOCAL:\n"] d======= \
b2ca4149
AG
138 [list ":1:$current_diff_path" ":2:$current_diff_path"]]
139 lappend current_diff_queue \
7f15b002 140 [list [mc "REMOTE:\n"] d======= \
b2ca4149
AG
141 [list ":1:$current_diff_path" ":3:$current_diff_path"]]
142 } else {
3e34838c 143 start_show_diff $cont_info
b2ca4149
AG
144 return
145 }
146
3e34838c 147 advance_diff_queue $cont_info
b2ca4149
AG
148}
149
3e34838c 150proc advance_diff_queue {cont_info} {
b2ca4149
AG
151 global current_diff_queue ui_diff
152
153 set item [lindex $current_diff_queue 0]
154 set current_diff_queue [lrange $current_diff_queue 1 end]
155
156 $ui_diff conf -state normal
157 $ui_diff insert end [lindex $item 0] [lindex $item 1]
158 $ui_diff conf -state disabled
159
3e34838c 160 start_show_diff $cont_info [lindex $item 2]
b2ca4149
AG
161}
162
3e34838c 163proc show_other_diff {path w m cont_info} {
b2ca4149
AG
164 global file_states file_lists
165 global is_3way_diff diff_active repo_config
166 global ui_diff ui_index ui_workdir
167 global current_diff_path current_diff_side current_diff_header
168
f522c9b5
SP
169 # - Git won't give us the diff, there's nothing to compare to!
170 #
171 if {$m eq {_O}} {
f2df8a5b 172 set max_sz 100000
3b9dfde3 173 set type unknown
f522c9b5 174 if {[catch {
3b9dfde3
SP
175 set type [file type $path]
176 switch -- $type {
177 directory {
178 set type submodule
179 set content {}
180 set sz 0
181 }
182 link {
2d19f8e9
MB
183 set content [file readlink $path]
184 set sz [string length $content]
3b9dfde3
SP
185 }
186 file {
2d19f8e9 187 set fd [open $path r]
1ffca60f
SP
188 fconfigure $fd \
189 -eofchar {} \
72e6b002 190 -encoding [get_path_encoding $path]
2d19f8e9
MB
191 set content [read $fd $max_sz]
192 close $fd
193 set sz [file size $path]
194 }
3b9dfde3
SP
195 default {
196 error "'$type' not supported"
197 }
198 }
f522c9b5
SP
199 } err ]} {
200 set diff_active 0
201 unlock_index
c8c4854b 202 ui_status [mc "Unable to display %s" [escape_path $path]]
31bb1d1b 203 error_popup [strcat [mc "Error loading file:"] "\n\n$err"]
f522c9b5
SP
204 return
205 }
206 $ui_diff conf -state normal
3b9dfde3 207 if {$type eq {submodule}} {
5f51ccd2
SP
208 $ui_diff insert end [append \
209 "* " \
210 [mc "Git Repository (subproject)"] \
211 "\n"] d_@
3b9dfde3 212 } elseif {![catch {set type [exec file $path]}]} {
f522c9b5
SP
213 set n [string length $path]
214 if {[string equal -length $n $path $type]} {
215 set type [string range $type $n end]
216 regsub {^:?\s*} $type {} type
217 }
218 $ui_diff insert end "* $type\n" d_@
219 }
220 if {[string first "\0" $content] != -1} {
221 $ui_diff insert end \
c8c4854b 222 [mc "* Binary file (not showing content)."] \
f522c9b5
SP
223 d_@
224 } else {
225 if {$sz > $max_sz} {
7f15b002
JS
226 $ui_diff insert end [mc \
227"* Untracked file is %d bytes.
228* Showing only first %d bytes.
229" $sz $max_sz] d_@
f522c9b5
SP
230 }
231 $ui_diff insert end $content
232 if {$sz > $max_sz} {
7f15b002
JS
233 $ui_diff insert end [mc "
234* Untracked file clipped here by %s.
f522c9b5 235* To see the entire file, use an external editor.
7f15b002 236" [appname]] d_@
f522c9b5
SP
237 }
238 }
239 $ui_diff conf -state disabled
240 set diff_active 0
241 unlock_index
3e34838c 242 set scroll_pos [lindex $cont_info 0]
25b8fb1e
AG
243 if {$scroll_pos ne {}} {
244 update
245 $ui_diff yview moveto $scroll_pos
246 }
699d5601 247 ui_ready
3e34838c
AG
248 set callback [lindex $cont_info 1]
249 if {$callback ne {}} {
250 eval $callback
251 }
f522c9b5
SP
252 return
253 }
b2ca4149
AG
254}
255
3e34838c 256proc start_show_diff {cont_info {add_opts {}}} {
b2ca4149
AG
257 global file_states file_lists
258 global is_3way_diff diff_active repo_config
259 global ui_diff ui_index ui_workdir
260 global current_diff_path current_diff_side current_diff_header
261
262 set path $current_diff_path
263 set w $current_diff_side
264
265 set s $file_states($path)
266 set m [lindex $s 0]
267 set is_3way_diff 0
268 set diff_active 1
269 set current_diff_header {}
f522c9b5 270
0b812616 271 set cmd [list]
f522c9b5
SP
272 if {$w eq $ui_index} {
273 lappend cmd diff-index
274 lappend cmd --cached
275 } elseif {$w eq $ui_workdir} {
ff515d81 276 if {[string first {U} $m] >= 0} {
f522c9b5
SP
277 lappend cmd diff
278 } else {
279 lappend cmd diff-files
280 }
281 }
282
283 lappend cmd -p
284 lappend cmd --no-color
55ba8a34 285 if {$repo_config(gui.diffcontext) >= 1} {
f522c9b5
SP
286 lappend cmd "-U$repo_config(gui.diffcontext)"
287 }
288 if {$w eq $ui_index} {
289 lappend cmd [PARENT]
290 }
b2ca4149
AG
291 if {$add_opts ne {}} {
292 eval lappend cmd $add_opts
293 } else {
294 lappend cmd --
295 lappend cmd $path
296 }
f522c9b5 297
0b812616 298 if {[catch {set fd [eval git_read --nice $cmd]} err]} {
f522c9b5
SP
299 set diff_active 0
300 unlock_index
c8c4854b 301 ui_status [mc "Unable to display %s" [escape_path $path]]
31bb1d1b 302 error_popup [strcat [mc "Error loading diff:"] "\n\n$err"]
f522c9b5
SP
303 return
304 }
305
ca53c3fd 306 set ::current_diff_inheader 1
f522c9b5
SP
307 fconfigure $fd \
308 -blocking 0 \
72e6b002 309 -encoding [get_path_encoding $path] \
1ffca60f 310 -translation lf
3e34838c 311 fileevent $fd readable [list read_diff $fd $cont_info]
f522c9b5
SP
312}
313
3e34838c 314proc read_diff {fd cont_info} {
699d5601 315 global ui_diff diff_active
3e34838c 316 global is_3way_diff is_conflict_diff current_diff_header
b2ca4149 317 global current_diff_queue
584fa9cc 318 global diff_empty_count
f522c9b5
SP
319
320 $ui_diff conf -state normal
321 while {[gets $fd line] >= 0} {
322 # -- Cleanup uninteresting diff header lines.
323 #
ca53c3fd
SP
324 if {$::current_diff_inheader} {
325 if { [string match {diff --git *} $line]
326 || [string match {diff --cc *} $line]
327 || [string match {diff --combined *} $line]
328 || [string match {--- *} $line]
329 || [string match {+++ *} $line]} {
330 append current_diff_header $line "\n"
331 continue
332 }
f522c9b5
SP
333 }
334 if {[string match {index *} $line]} continue
335 if {$line eq {deleted file mode 120000}} {
336 set line "deleted symlink"
337 }
ca53c3fd 338 set ::current_diff_inheader 0
f522c9b5
SP
339
340 # -- Automatically detect if this is a 3 way diff.
341 #
342 if {[string match {@@@ *} $line]} {set is_3way_diff 1}
343
344 if {[string match {mode *} $line]
345 || [string match {new file *} $line]
a4750dd2 346 || [regexp {^(old|new) mode *} $line]
f522c9b5 347 || [string match {deleted file *} $line]
4ed1a190 348 || [string match {deleted symlink} $line]
f522c9b5
SP
349 || [string match {Binary files * and * differ} $line]
350 || $line eq {\ No newline at end of file}
351 || [regexp {^\* Unmerged path } $line]} {
352 set tags {}
353 } elseif {$is_3way_diff} {
354 set op [string range $line 0 1]
355 switch -- $op {
356 { } {set tags {}}
357 {@@} {set tags d_@}
358 { +} {set tags d_s+}
359 { -} {set tags d_s-}
360 {+ } {set tags d_+s}
361 {- } {set tags d_-s}
362 {--} {set tags d_--}
363 {++} {
364 if {[regexp {^\+\+([<>]{7} |={7})} $line _g op]} {
3e34838c 365 set is_conflict_diff 1
f522c9b5
SP
366 set line [string replace $line 0 1 { }]
367 set tags d$op
368 } else {
369 set tags d_++
370 }
371 }
372 default {
373 puts "error: Unhandled 3 way diff marker: {$op}"
374 set tags {}
375 }
376 }
377 } else {
378 set op [string index $line 0]
379 switch -- $op {
380 { } {set tags {}}
381 {@} {set tags d_@}
382 {-} {set tags d_-}
383 {+} {
384 if {[regexp {^\+([<>]{7} |={7})} $line _g op]} {
3e34838c 385 set is_conflict_diff 1
f522c9b5
SP
386 set tags d$op
387 } else {
388 set tags d_+
389 }
390 }
391 default {
392 puts "error: Unhandled 2 way diff marker: {$op}"
393 set tags {}
394 }
395 }
396 }
397 $ui_diff insert end $line $tags
398 if {[string index $line end] eq "\r"} {
399 $ui_diff tag add d_cr {end - 2c}
400 }
401 $ui_diff insert end "\n" $tags
402 }
403 $ui_diff conf -state disabled
404
405 if {[eof $fd]} {
406 close $fd
b2ca4149
AG
407
408 if {$current_diff_queue ne {}} {
3e34838c 409 advance_diff_queue $cont_info
b2ca4149
AG
410 return
411 }
412
f522c9b5
SP
413 set diff_active 0
414 unlock_index
3e34838c 415 set scroll_pos [lindex $cont_info 0]
25b8fb1e
AG
416 if {$scroll_pos ne {}} {
417 update
418 $ui_diff yview moveto $scroll_pos
419 }
699d5601 420 ui_ready
f522c9b5
SP
421
422 if {[$ui_diff index end] eq {2.0}} {
423 handle_empty_diff
584fa9cc
AG
424 } else {
425 set diff_empty_count 0
f522c9b5 426 }
584fa9cc 427
3e34838c
AG
428 set callback [lindex $cont_info 1]
429 if {$callback ne {}} {
430 eval $callback
431 }
f522c9b5
SP
432 }
433}
434
435proc apply_hunk {x y} {
436 global current_diff_path current_diff_header current_diff_side
437 global ui_diff ui_index file_states
438
439 if {$current_diff_path eq {} || $current_diff_header eq {}} return
440 if {![lock_index apply_hunk]} return
441
0b812616 442 set apply_cmd {apply --cached --whitespace=nowarn}
f522c9b5
SP
443 set mi [lindex $file_states($current_diff_path) 0]
444 if {$current_diff_side eq $ui_index} {
1ac17950 445 set failed_msg [mc "Failed to unstage selected hunk."]
f522c9b5
SP
446 lappend apply_cmd --reverse
447 if {[string index $mi 0] ne {M}} {
448 unlock_index
449 return
450 }
451 } else {
1ac17950 452 set failed_msg [mc "Failed to stage selected hunk."]
f522c9b5
SP
453 if {[string index $mi 1] ne {M}} {
454 unlock_index
455 return
456 }
457 }
458
459 set s_lno [lindex [split [$ui_diff index @$x,$y] .] 0]
460 set s_lno [$ui_diff search -backwards -regexp ^@@ $s_lno.0 0.0]
461 if {$s_lno eq {}} {
462 unlock_index
463 return
464 }
465
466 set e_lno [$ui_diff search -forwards -regexp ^@@ "$s_lno + 1 lines" end]
467 if {$e_lno eq {}} {
468 set e_lno end
469 }
470
471 if {[catch {
72e6b002 472 set enc [get_path_encoding $current_diff_path]
0b812616 473 set p [eval git_write $apply_cmd]
72e6b002 474 fconfigure $p -translation binary -encoding $enc
f522c9b5
SP
475 puts -nonewline $p $current_diff_header
476 puts -nonewline $p [$ui_diff get $s_lno $e_lno]
477 close $p} err]} {
1ac17950 478 error_popup [append $failed_msg "\n\n$err"]
f522c9b5
SP
479 unlock_index
480 return
481 }
482
483 $ui_diff conf -state normal
484 $ui_diff delete $s_lno $e_lno
485 $ui_diff conf -state disabled
486
487 if {[$ui_diff get 1.0 end] eq "\n"} {
488 set o _
489 } else {
490 set o ?
491 }
492
493 if {$current_diff_side eq $ui_index} {
494 set mi ${o}M
495 } elseif {[string index $mi 0] eq {_}} {
496 set mi M$o
497 } else {
498 set mi ?$o
499 }
500 unlock_index
501 display_file $current_diff_path $mi
29853b90 502 # This should trigger shift to the next changed file
f522c9b5 503 if {$o eq {_}} {
29853b90 504 reshow_diff
f522c9b5
SP
505 }
506}
5821988f
JS
507
508proc apply_line {x y} {
509 global current_diff_path current_diff_header current_diff_side
510 global ui_diff ui_index file_states
511
512 if {$current_diff_path eq {} || $current_diff_header eq {}} return
513 if {![lock_index apply_hunk]} return
514
515 set apply_cmd {apply --cached --whitespace=nowarn}
516 set mi [lindex $file_states($current_diff_path) 0]
517 if {$current_diff_side eq $ui_index} {
518 set failed_msg [mc "Failed to unstage selected line."]
519 set to_context {+}
520 lappend apply_cmd --reverse
521 if {[string index $mi 0] ne {M}} {
522 unlock_index
523 return
524 }
525 } else {
526 set failed_msg [mc "Failed to stage selected line."]
527 set to_context {-}
528 if {[string index $mi 1] ne {M}} {
529 unlock_index
530 return
531 }
532 }
533
534 set the_l [$ui_diff index @$x,$y]
535
536 # operate only on change lines
537 set c1 [$ui_diff get "$the_l linestart"]
538 if {$c1 ne {+} && $c1 ne {-}} {
539 unlock_index
540 return
541 }
542 set sign $c1
543
544 set i_l [$ui_diff search -backwards -regexp ^@@ $the_l 0.0]
545 if {$i_l eq {}} {
546 unlock_index
547 return
548 }
549 # $i_l is now at the beginning of a line
550
551 # pick start line number from hunk header
552 set hh [$ui_diff get $i_l "$i_l + 1 lines"]
553 set hh [lindex [split $hh ,] 0]
554 set hln [lindex [split $hh -] 1]
555
c7f74570
JS
556 # There is a special situation to take care of. Consider this hunk:
557 #
558 # @@ -10,4 +10,4 @@
559 # context before
560 # -old 1
561 # -old 2
562 # +new 1
563 # +new 2
564 # context after
565 #
566 # We used to keep the context lines in the order they appear in the
567 # hunk. But then it is not possible to correctly stage only
568 # "-old 1" and "+new 1" - it would result in this staged text:
569 #
570 # context before
571 # old 2
572 # new 1
573 # context after
574 #
575 # (By symmetry it is not possible to *un*stage "old 2" and "new 2".)
576 #
577 # We resolve the problem by introducing an asymmetry, namely, when
578 # a "+" line is *staged*, it is moved in front of the context lines
579 # that are generated from the "-" lines that are immediately before
580 # the "+" block. That is, we construct this patch:
581 #
582 # @@ -10,4 +10,5 @@
583 # context before
584 # +new 1
585 # old 1
586 # old 2
587 # context after
588 #
589 # But we do *not* treat "-" lines that are *un*staged in a special
590 # way.
591 #
592 # With this asymmetry it is possible to stage the change
593 # "old 1" -> "new 1" directly, and to stage the change
594 # "old 2" -> "new 2" by first staging the entire hunk and
595 # then unstaging the change "old 1" -> "new 1".
596
597 # This is non-empty if and only if we are _staging_ changes;
598 # then it accumulates the consecutive "-" lines (after converting
599 # them to context lines) in order to be moved after the "+" change
600 # line.
601 set pre_context {}
602
5821988f
JS
603 set n 0
604 set i_l [$ui_diff index "$i_l + 1 lines"]
605 set patch {}
606 while {[$ui_diff compare $i_l < "end - 1 chars"] &&
607 [$ui_diff get $i_l "$i_l + 2 chars"] ne {@@}} {
608 set next_l [$ui_diff index "$i_l + 1 lines"]
609 set c1 [$ui_diff get $i_l]
610 if {[$ui_diff compare $i_l <= $the_l] &&
611 [$ui_diff compare $the_l < $next_l]} {
612 # the line to stage/unstage
613 set ln [$ui_diff get $i_l $next_l]
fa6b5b39
JS
614 if {$c1 eq {-}} {
615 set n [expr $n+1]
c7f74570
JS
616 set patch "$patch$pre_context$ln"
617 } else {
618 set patch "$patch$ln$pre_context"
fa6b5b39 619 }
c7f74570 620 set pre_context {}
5821988f
JS
621 } elseif {$c1 ne {-} && $c1 ne {+}} {
622 # context line
623 set ln [$ui_diff get $i_l $next_l]
c7f74570 624 set patch "$patch$pre_context$ln"
5821988f 625 set n [expr $n+1]
c7f74570 626 set pre_context {}
5821988f
JS
627 } elseif {$c1 eq $to_context} {
628 # turn change line into context line
629 set ln [$ui_diff get "$i_l + 1 chars" $next_l]
c7f74570
JS
630 if {$c1 eq {-}} {
631 set pre_context "$pre_context $ln"
632 } else {
633 set patch "$patch $ln"
634 }
5821988f
JS
635 set n [expr $n+1]
636 }
637 set i_l $next_l
638 }
639 set patch "@@ -$hln,$n +$hln,[eval expr $n $sign 1] @@\n$patch"
640
641 if {[catch {
72e6b002 642 set enc [get_path_encoding $current_diff_path]
5821988f 643 set p [eval git_write $apply_cmd]
72e6b002 644 fconfigure $p -translation binary -encoding $enc
5821988f
JS
645 puts -nonewline $p $current_diff_header
646 puts -nonewline $p $patch
647 close $p} err]} {
648 error_popup [append $failed_msg "\n\n$err"]
649 }
650
651 unlock_index
652}