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