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
19proc reshow_diff {{after {}}} {
20 global file_states file_lists
21 global current_diff_path current_diff_side
22 global ui_diff
23
24 set p $current_diff_path
25 if {$p eq {}} {
26 # No diff is being shown.
27 } elseif {$current_diff_side eq {}} {
28 clear_diff
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]}]} {
33 next_diff $after
34 } else {
35 clear_diff
36 }
37 } else {
38 set save_pos [lindex [$ui_diff yview] 0]
39 show_diff $p $current_diff_side {} $save_pos $after
40 }
41}
42
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
52proc handle_empty_diff {} {
53 global current_diff_path file_states file_lists
54 global diff_empty_count
55
56 set path $current_diff_path
57 set s $file_states($path)
58 if {[lindex $s 0] ne {_M} || [has_textconv $path]} return
59
60 # Prevent infinite rescan loops
61 incr diff_empty_count
62 if {$diff_empty_count > 1} return
63
64 info_popup [mc "No differences detected.
65
66%s has no changes.
67
68The modification date of this file was updated by another application, but the content within the file was not changed.
69
70A rescan will be automatically started to find other files which may have the same state." [short_path $path]]
71
72 clear_diff
73 display_file $path __
74 rescan ui_ready 0
75}
76
77proc show_diff {path w {lno {}} {scroll_pos {}} {callback {}}} {
78 global file_states file_lists
79 global is_3way_diff is_conflict_diff diff_active repo_config
80 global ui_diff ui_index ui_workdir
81 global current_diff_path current_diff_side current_diff_header
82 global current_diff_queue
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
95 $w see $lno.0
96 }
97
98 set s $file_states($path)
99 set m [lindex $s 0]
100 set is_conflict_diff 0
101 set current_diff_path $path
102 set current_diff_side $w
103 set current_diff_queue {}
104 ui_status [mc "Loading diff of %s..." [escape_path $path]]
105
106 set cont_info [list $scroll_pos $callback]
107
108 if {[string first {U} $m] >= 0} {
109 merge_load_stages $path [list show_unmerged_diff $cont_info]
110 } elseif {$m eq {_O}} {
111 show_other_diff $path $w $m $cont_info
112 } else {
113 start_show_diff $cont_info
114 }
115}
116
117proc show_unmerged_diff {cont_info} {
118 global current_diff_path current_diff_side
119 global merge_stages ui_diff is_conflict_diff
120 global current_diff_queue
121
122 if {$merge_stages(2) eq {}} {
123 set is_conflict_diff 1
124 lappend current_diff_queue \
125 [list [mc "LOCAL: deleted\nREMOTE:\n"] d======= \
126 [list ":1:$current_diff_path" ":3:$current_diff_path"]]
127 } elseif {$merge_stages(3) eq {}} {
128 set is_conflict_diff 1
129 lappend current_diff_queue \
130 [list [mc "REMOTE: deleted\nLOCAL:\n"] d======= \
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}} {
135 set is_conflict_diff 1
136 lappend current_diff_queue \
137 [list [mc "LOCAL:\n"] d======= \
138 [list ":1:$current_diff_path" ":2:$current_diff_path"]]
139 lappend current_diff_queue \
140 [list [mc "REMOTE:\n"] d======= \
141 [list ":1:$current_diff_path" ":3:$current_diff_path"]]
142 } else {
143 start_show_diff $cont_info
144 return
145 }
146
147 advance_diff_queue $cont_info
148}
149
150proc advance_diff_queue {cont_info} {
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
160 start_show_diff $cont_info [lindex $item 2]
161}
162
163proc show_other_diff {path w m cont_info} {
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
169 # - Git won't give us the diff, there's nothing to compare to!
170 #
171 if {$m eq {_O}} {
172 set max_sz 100000
173 set type unknown
174 if {[catch {
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 {
183 set content [file readlink $path]
184 set sz [string length $content]
185 }
186 file {
187 set fd [open $path r]
188 fconfigure $fd \
189 -eofchar {} \
190 -encoding [get_path_encoding $path]
191 set content [read $fd $max_sz]
192 close $fd
193 set sz [file size $path]
194 }
195 default {
196 error "'$type' not supported"
197 }
198 }
199 } err ]} {
200 set diff_active 0
201 unlock_index
202 ui_status [mc "Unable to display %s" [escape_path $path]]
203 error_popup [strcat [mc "Error loading file:"] "\n\n$err"]
204 return
205 }
206 $ui_diff conf -state normal
207 if {$type eq {submodule}} {
208 $ui_diff insert end [append \
209 "* " \
210 [mc "Git Repository (subproject)"] \
211 "\n"] d_@
212 } elseif {![catch {set type [exec file $path]}]} {
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 \
222 [mc "* Binary file (not showing content)."] \
223 d_@
224 } else {
225 if {$sz > $max_sz} {
226 $ui_diff insert end [mc \
227"* Untracked file is %d bytes.
228* Showing only first %d bytes.
229" $sz $max_sz] d_@
230 }
231 $ui_diff insert end $content
232 if {$sz > $max_sz} {
233 $ui_diff insert end [mc "
234* Untracked file clipped here by %s.
235* To see the entire file, use an external editor.
236" [appname]] d_@
237 }
238 }
239 $ui_diff conf -state disabled
240 set diff_active 0
241 unlock_index
242 set scroll_pos [lindex $cont_info 0]
243 if {$scroll_pos ne {}} {
244 update
245 $ui_diff yview moveto $scroll_pos
246 }
247 ui_ready
248 set callback [lindex $cont_info 1]
249 if {$callback ne {}} {
250 eval $callback
251 }
252 return
253 }
254}
255
256proc start_show_diff {cont_info {add_opts {}}} {
257 global file_states file_lists
258 global is_3way_diff is_submodule_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 is_submodule_diff 0
269 set diff_active 1
270 set current_diff_header {}
271
272 set cmd [list]
273 if {$w eq $ui_index} {
274 lappend cmd diff-index
275 lappend cmd --cached
276 } elseif {$w eq $ui_workdir} {
277 if {[string first {U} $m] >= 0} {
278 lappend cmd diff
279 } else {
280 lappend cmd diff-files
281 }
282 }
283 if {![is_config_false gui.textconv] && [git-version >= 1.6.1]} {
284 lappend cmd --textconv
285 }
286
287 if {[string match {160000 *} [lindex $s 2]]
288 || [string match {160000 *} [lindex $s 3]]} {
289 set is_submodule_diff 1
290
291 if {[git-version >= "1.6.6"]} {
292 lappend cmd --submodule
293 }
294 }
295
296 lappend cmd -p
297 lappend cmd --color
298 if {$repo_config(gui.diffcontext) >= 1} {
299 lappend cmd "-U$repo_config(gui.diffcontext)"
300 }
301 if {$w eq $ui_index} {
302 lappend cmd [PARENT]
303 }
304 if {$add_opts ne {}} {
305 eval lappend cmd $add_opts
306 } else {
307 lappend cmd --
308 lappend cmd $path
309 }
310
311 if {$is_submodule_diff && [git-version < "1.6.6"]} {
312 if {$w eq $ui_index} {
313 set cmd [list submodule summary --cached -- $path]
314 } else {
315 set cmd [list submodule summary --files -- $path]
316 }
317 }
318
319 if {[catch {set fd [eval git_read --nice $cmd]} err]} {
320 set diff_active 0
321 unlock_index
322 ui_status [mc "Unable to display %s" [escape_path $path]]
323 error_popup [strcat [mc "Error loading diff:"] "\n\n$err"]
324 return
325 }
326
327 set ::current_diff_inheader 1
328 fconfigure $fd \
329 -blocking 0 \
330 -encoding [get_path_encoding $path] \
331 -translation lf
332 fileevent $fd readable [list read_diff $fd $cont_info]
333}
334
335proc parse_color_line {line} {
336 set start 0
337 set result ""
338 set markup [list]
339 set regexp {\033\[((?:\d+;)*\d+)?m}
340 while {[regexp -indices -start $start $regexp $line match code]} {
341 foreach {begin end} $match break
342 append result [string range $line $start [expr {$begin - 1}]]
343 lappend markup [string length $result] \
344 [eval [linsert $code 0 string range $line]]
345 set start [incr end]
346 }
347 append result [string range $line $start end]
348 if {[llength $markup] < 4} {set markup {}}
349 return [list $result $markup]
350}
351
352proc read_diff {fd cont_info} {
353 global ui_diff diff_active is_submodule_diff
354 global is_3way_diff is_conflict_diff current_diff_header
355 global current_diff_queue
356 global diff_empty_count
357
358 $ui_diff conf -state normal
359 while {[gets $fd line] >= 0} {
360 foreach {line markup} [parse_color_line $line] break
361 set line [string map {\033 ^} $line]
362
363 # -- Cleanup uninteresting diff header lines.
364 #
365 if {$::current_diff_inheader} {
366 if { [string match {diff --git *} $line]
367 || [string match {diff --cc *} $line]
368 || [string match {diff --combined *} $line]
369 || [string match {--- *} $line]
370 || [string match {+++ *} $line]} {
371 append current_diff_header $line "\n"
372 continue
373 }
374 }
375 if {[string match {index *} $line]} continue
376 if {$line eq {deleted file mode 120000}} {
377 set line "deleted symlink"
378 }
379 set ::current_diff_inheader 0
380
381 # -- Automatically detect if this is a 3 way diff.
382 #
383 if {[string match {@@@ *} $line]} {set is_3way_diff 1}
384
385 if {[string match {mode *} $line]
386 || [string match {new file *} $line]
387 || [regexp {^(old|new) mode *} $line]
388 || [string match {deleted file *} $line]
389 || [string match {deleted symlink} $line]
390 || [string match {Binary files * and * differ} $line]
391 || $line eq {\ No newline at end of file}
392 || [regexp {^\* Unmerged path } $line]} {
393 set tags {}
394 } elseif {$is_3way_diff} {
395 set op [string range $line 0 1]
396 switch -- $op {
397 { } {set tags {}}
398 {@@} {set tags d_@}
399 { +} {set tags d_s+}
400 { -} {set tags d_s-}
401 {+ } {set tags d_+s}
402 {- } {set tags d_-s}
403 {--} {set tags d_--}
404 {++} {
405 if {[regexp {^\+\+([<>]{7} |={7})} $line _g op]} {
406 set is_conflict_diff 1
407 set line [string replace $line 0 1 { }]
408 set tags d$op
409 } else {
410 set tags d_++
411 }
412 }
413 default {
414 puts "error: Unhandled 3 way diff marker: {$op}"
415 set tags {}
416 }
417 }
418 } elseif {$is_submodule_diff} {
419 if {$line == ""} continue
420 if {[regexp {^Submodule } $line]} {
421 set tags d_@
422 } elseif {[regexp {^\* } $line]} {
423 set line [string replace $line 0 1 {Submodule }]
424 set tags d_@
425 } else {
426 set op [string range $line 0 2]
427 switch -- $op {
428 { <} {set tags d_-}
429 { >} {set tags d_+}
430 { W} {set tags {}}
431 default {
432 puts "error: Unhandled submodule diff marker: {$op}"
433 set tags {}
434 }
435 }
436 }
437 } else {
438 set op [string index $line 0]
439 switch -- $op {
440 { } {set tags {}}
441 {@} {set tags d_@}
442 {-} {set tags d_-}
443 {+} {
444 if {[regexp {^\+([<>]{7} |={7})} $line _g op]} {
445 set is_conflict_diff 1
446 set tags d$op
447 } else {
448 set tags d_+
449 }
450 }
451 default {
452 puts "error: Unhandled 2 way diff marker: {$op}"
453 set tags {}
454 }
455 }
456 }
457 set mark [$ui_diff index "end - 1 line linestart"]
458 $ui_diff insert end $line $tags
459 if {[string index $line end] eq "\r"} {
460 $ui_diff tag add d_cr {end - 2c}
461 }
462 $ui_diff insert end "\n" $tags
463
464 foreach {posbegin colbegin posend colend} $markup {
465 set prefix clr
466 foreach style [split $colbegin ";"] {
467 if {$style eq "7"} {append prefix i; continue}
468 if {$style < 30 || $style > 47} {continue}
469 set a "$mark linestart + $posbegin chars"
470 set b "$mark linestart + $posend chars"
471 catch {$ui_diff tag add $prefix$style $a $b}
472 }
473 }
474 }
475 $ui_diff conf -state disabled
476
477 if {[eof $fd]} {
478 close $fd
479
480 if {$current_diff_queue ne {}} {
481 advance_diff_queue $cont_info
482 return
483 }
484
485 set diff_active 0
486 unlock_index
487 set scroll_pos [lindex $cont_info 0]
488 if {$scroll_pos ne {}} {
489 update
490 $ui_diff yview moveto $scroll_pos
491 }
492 ui_ready
493
494 if {[$ui_diff index end] eq {2.0}} {
495 handle_empty_diff
496 } else {
497 set diff_empty_count 0
498 }
499
500 set callback [lindex $cont_info 1]
501 if {$callback ne {}} {
502 eval $callback
503 }
504 }
505}
506
507proc apply_hunk {x y} {
508 global current_diff_path current_diff_header current_diff_side
509 global ui_diff ui_index file_states
510
511 if {$current_diff_path eq {} || $current_diff_header eq {}} return
512 if {![lock_index apply_hunk]} return
513
514 set apply_cmd {apply --cached --whitespace=nowarn}
515 set mi [lindex $file_states($current_diff_path) 0]
516 if {$current_diff_side eq $ui_index} {
517 set failed_msg [mc "Failed to unstage selected hunk."]
518 lappend apply_cmd --reverse
519 if {[string index $mi 0] ne {M}} {
520 unlock_index
521 return
522 }
523 } else {
524 set failed_msg [mc "Failed to stage selected hunk."]
525 if {[string index $mi 1] ne {M}} {
526 unlock_index
527 return
528 }
529 }
530
531 set s_lno [lindex [split [$ui_diff index @$x,$y] .] 0]
532 set s_lno [$ui_diff search -backwards -regexp ^@@ $s_lno.0 0.0]
533 if {$s_lno eq {}} {
534 unlock_index
535 return
536 }
537
538 set e_lno [$ui_diff search -forwards -regexp ^@@ "$s_lno + 1 lines" end]
539 if {$e_lno eq {}} {
540 set e_lno end
541 }
542
543 if {[catch {
544 set enc [get_path_encoding $current_diff_path]
545 set p [eval git_write $apply_cmd]
546 fconfigure $p -translation binary -encoding $enc
547 puts -nonewline $p $current_diff_header
548 puts -nonewline $p [$ui_diff get $s_lno $e_lno]
549 close $p} err]} {
550 error_popup [append $failed_msg "\n\n$err"]
551 unlock_index
552 return
553 }
554
555 $ui_diff conf -state normal
556 $ui_diff delete $s_lno $e_lno
557 $ui_diff conf -state disabled
558
559 if {[$ui_diff get 1.0 end] eq "\n"} {
560 set o _
561 } else {
562 set o ?
563 }
564
565 if {$current_diff_side eq $ui_index} {
566 set mi ${o}M
567 } elseif {[string index $mi 0] eq {_}} {
568 set mi M$o
569 } else {
570 set mi ?$o
571 }
572 unlock_index
573 display_file $current_diff_path $mi
574 # This should trigger shift to the next changed file
575 if {$o eq {_}} {
576 reshow_diff
577 }
578}
579
580proc apply_range_or_line {x y} {
581 global current_diff_path current_diff_header current_diff_side
582 global ui_diff ui_index file_states
583
584 set selected [$ui_diff tag nextrange sel 0.0]
585
586 if {$selected == {}} {
587 set first [$ui_diff index "@$x,$y"]
588 set last $first
589 } else {
590 set first [lindex $selected 0]
591 set last [lindex $selected 1]
592 }
593
594 set first_l [$ui_diff index "$first linestart"]
595 set last_l [$ui_diff index "$last lineend"]
596
597 if {$current_diff_path eq {} || $current_diff_header eq {}} return
598 if {![lock_index apply_hunk]} return
599
600 set apply_cmd {apply --cached --whitespace=nowarn}
601 set mi [lindex $file_states($current_diff_path) 0]
602 if {$current_diff_side eq $ui_index} {
603 set failed_msg [mc "Failed to unstage selected line."]
604 set to_context {+}
605 lappend apply_cmd --reverse
606 if {[string index $mi 0] ne {M}} {
607 unlock_index
608 return
609 }
610 } else {
611 set failed_msg [mc "Failed to stage selected line."]
612 set to_context {-}
613 if {[string index $mi 1] ne {M}} {
614 unlock_index
615 return
616 }
617 }
618
619 set wholepatch {}
620
621 while {$first_l < $last_l} {
622 set i_l [$ui_diff search -backwards -regexp ^@@ $first_l 0.0]
623 if {$i_l eq {}} {
624 # If there's not a @@ above, then the selected range
625 # must have come before the first_l @@
626 set i_l [$ui_diff search -regexp ^@@ $first_l $last_l]
627 }
628 if {$i_l eq {}} {
629 unlock_index
630 return
631 }
632 # $i_l is now at the beginning of a line
633
634 # pick start line number from hunk header
635 set hh [$ui_diff get $i_l "$i_l + 1 lines"]
636 set hh [lindex [split $hh ,] 0]
637 set hln [lindex [split $hh -] 1]
638
639 # There is a special situation to take care of. Consider this
640 # hunk:
641 #
642 # @@ -10,4 +10,4 @@
643 # context before
644 # -old 1
645 # -old 2
646 # +new 1
647 # +new 2
648 # context after
649 #
650 # We used to keep the context lines in the order they appear in
651 # the hunk. But then it is not possible to correctly stage only
652 # "-old 1" and "+new 1" - it would result in this staged text:
653 #
654 # context before
655 # old 2
656 # new 1
657 # context after
658 #
659 # (By symmetry it is not possible to *un*stage "old 2" and "new
660 # 2".)
661 #
662 # We resolve the problem by introducing an asymmetry, namely,
663 # when a "+" line is *staged*, it is moved in front of the
664 # context lines that are generated from the "-" lines that are
665 # immediately before the "+" block. That is, we construct this
666 # patch:
667 #
668 # @@ -10,4 +10,5 @@
669 # context before
670 # +new 1
671 # old 1
672 # old 2
673 # context after
674 #
675 # But we do *not* treat "-" lines that are *un*staged in a
676 # special way.
677 #
678 # With this asymmetry it is possible to stage the change "old
679 # 1" -> "new 1" directly, and to stage the change "old 2" ->
680 # "new 2" by first staging the entire hunk and then unstaging
681 # the change "old 1" -> "new 1".
682 #
683 # Applying multiple lines adds complexity to the special
684 # situation. The pre_context must be moved after the entire
685 # first block of consecutive staged "+" lines, so that
686 # staging both additions gives the following patch:
687 #
688 # @@ -10,4 +10,6 @@
689 # context before
690 # +new 1
691 # +new 2
692 # old 1
693 # old 2
694 # context after
695
696 # This is non-empty if and only if we are _staging_ changes;
697 # then it accumulates the consecutive "-" lines (after
698 # converting them to context lines) in order to be moved after
699 # "+" change lines.
700 set pre_context {}
701
702 set n 0
703 set m 0
704 set i_l [$ui_diff index "$i_l + 1 lines"]
705 set patch {}
706 while {[$ui_diff compare $i_l < "end - 1 chars"] &&
707 [$ui_diff get $i_l "$i_l + 2 chars"] ne {@@}} {
708 set next_l [$ui_diff index "$i_l + 1 lines"]
709 set c1 [$ui_diff get $i_l]
710 if {[$ui_diff compare $first_l <= $i_l] &&
711 [$ui_diff compare $i_l < $last_l] &&
712 ($c1 eq {-} || $c1 eq {+})} {
713 # a line to stage/unstage
714 set ln [$ui_diff get $i_l $next_l]
715 if {$c1 eq {-}} {
716 set n [expr $n+1]
717 set patch "$patch$pre_context$ln"
718 set pre_context {}
719 } else {
720 set m [expr $m+1]
721 set patch "$patch$ln"
722 }
723 } elseif {$c1 ne {-} && $c1 ne {+}} {
724 # context line
725 set ln [$ui_diff get $i_l $next_l]
726 set patch "$patch$pre_context$ln"
727 set n [expr $n+1]
728 set m [expr $m+1]
729 set pre_context {}
730 } elseif {$c1 eq $to_context} {
731 # turn change line into context line
732 set ln [$ui_diff get "$i_l + 1 chars" $next_l]
733 if {$c1 eq {-}} {
734 set pre_context "$pre_context $ln"
735 } else {
736 set patch "$patch $ln"
737 }
738 set n [expr $n+1]
739 set m [expr $m+1]
740 } else {
741 # a change in the opposite direction of
742 # to_context which is outside the range of
743 # lines to apply.
744 set patch "$patch$pre_context"
745 set pre_context {}
746 }
747 set i_l $next_l
748 }
749 set patch "$patch$pre_context"
750 set wholepatch "$wholepatch@@ -$hln,$n +$hln,$m @@\n$patch"
751 set first_l [$ui_diff index "$next_l + 1 lines"]
752 }
753
754 if {[catch {
755 set enc [get_path_encoding $current_diff_path]
756 set p [eval git_write $apply_cmd]
757 fconfigure $p -translation binary -encoding $enc
758 puts -nonewline $p $current_diff_header
759 puts -nonewline $p $wholepatch
760 close $p} err]} {
761 error_popup [append $failed_msg "\n\n$err"]
762 }
763
764 unlock_index
765}