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}} 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
284 if {[string match {160000 *} [lindex $s 2]]
285 || [string match {160000 *} [lindex $s 3]]} {
286 set is_submodule_diff 1
287
288 if {[git-version >= "1.6.6"]} {
289 lappend cmd --submodule
290 }
291 }
292
293 lappend cmd -p
294 lappend cmd --no-color
295 if {$repo_config(gui.diffcontext) >= 1} {
296 lappend cmd "-U$repo_config(gui.diffcontext)"
297 }
298 if {$w eq $ui_index} {
299 lappend cmd [PARENT]
300 }
301 if {$add_opts ne {}} {
302 eval lappend cmd $add_opts
303 } else {
304 lappend cmd --
305 lappend cmd $path
306 }
307
308 if {$is_submodule_diff && [git-version < "1.6.6"]} {
309 if {$w eq $ui_index} {
310 set cmd [list submodule summary --cached -- $path]
311 } else {
312 set cmd [list submodule summary --files -- $path]
313 }
314 }
315
316 if {[catch {set fd [eval git_read --nice $cmd]} err]} {
317 set diff_active 0
318 unlock_index
319 ui_status [mc "Unable to display %s" [escape_path $path]]
320 error_popup [strcat [mc "Error loading diff:"] "\n\n$err"]
321 return
322 }
323
324 set ::current_diff_inheader 1
325 fconfigure $fd \
326 -blocking 0 \
327 -encoding [get_path_encoding $path] \
328 -translation lf
329 fileevent $fd readable [list read_diff $fd $cont_info]
330}
331
332proc read_diff {fd cont_info} {
333 global ui_diff diff_active is_submodule_diff
334 global is_3way_diff is_conflict_diff current_diff_header
335 global current_diff_queue
336 global diff_empty_count
337
338 $ui_diff conf -state normal
339 while {[gets $fd line] >= 0} {
340 # -- Cleanup uninteresting diff header lines.
341 #
342 if {$::current_diff_inheader} {
343 if { [string match {diff --git *} $line]
344 || [string match {diff --cc *} $line]
345 || [string match {diff --combined *} $line]
346 || [string match {--- *} $line]
347 || [string match {+++ *} $line]} {
348 append current_diff_header $line "\n"
349 continue
350 }
351 }
352 if {[string match {index *} $line]} continue
353 if {$line eq {deleted file mode 120000}} {
354 set line "deleted symlink"
355 }
356 set ::current_diff_inheader 0
357
358 # -- Automatically detect if this is a 3 way diff.
359 #
360 if {[string match {@@@ *} $line]} {set is_3way_diff 1}
361
362 if {[string match {mode *} $line]
363 || [string match {new file *} $line]
364 || [regexp {^(old|new) mode *} $line]
365 || [string match {deleted file *} $line]
366 || [string match {deleted symlink} $line]
367 || [string match {Binary files * and * differ} $line]
368 || $line eq {\ No newline at end of file}
369 || [regexp {^\* Unmerged path } $line]} {
370 set tags {}
371 } elseif {$is_3way_diff} {
372 set op [string range $line 0 1]
373 switch -- $op {
374 { } {set tags {}}
375 {@@} {set tags d_@}
376 { +} {set tags d_s+}
377 { -} {set tags d_s-}
378 {+ } {set tags d_+s}
379 {- } {set tags d_-s}
380 {--} {set tags d_--}
381 {++} {
382 if {[regexp {^\+\+([<>]{7} |={7})} $line _g op]} {
383 set is_conflict_diff 1
384 set line [string replace $line 0 1 { }]
385 set tags d$op
386 } else {
387 set tags d_++
388 }
389 }
390 default {
391 puts "error: Unhandled 3 way diff marker: {$op}"
392 set tags {}
393 }
394 }
395 } elseif {$is_submodule_diff} {
396 if {$line == ""} continue
397 if {[regexp {^Submodule } $line]} {
398 set tags d_@
399 } elseif {[regexp {^\* } $line]} {
400 set line [string replace $line 0 1 {Submodule }]
401 set tags d_@
402 } else {
403 set op [string range $line 0 2]
404 switch -- $op {
405 { <} {set tags d_-}
406 { >} {set tags d_+}
407 { W} {set tags {}}
408 default {
409 puts "error: Unhandled submodule diff marker: {$op}"
410 set tags {}
411 }
412 }
413 }
414 } else {
415 set op [string index $line 0]
416 switch -- $op {
417 { } {set tags {}}
418 {@} {set tags d_@}
419 {-} {set tags d_-}
420 {+} {
421 if {[regexp {^\+([<>]{7} |={7})} $line _g op]} {
422 set is_conflict_diff 1
423 set tags d$op
424 } else {
425 set tags d_+
426 }
427 }
428 default {
429 puts "error: Unhandled 2 way diff marker: {$op}"
430 set tags {}
431 }
432 }
433 }
434 $ui_diff insert end $line $tags
435 if {[string index $line end] eq "\r"} {
436 $ui_diff tag add d_cr {end - 2c}
437 }
438 $ui_diff insert end "\n" $tags
439 }
440 $ui_diff conf -state disabled
441
442 if {[eof $fd]} {
443 close $fd
444
445 if {$current_diff_queue ne {}} {
446 advance_diff_queue $cont_info
447 return
448 }
449
450 set diff_active 0
451 unlock_index
452 set scroll_pos [lindex $cont_info 0]
453 if {$scroll_pos ne {}} {
454 update
455 $ui_diff yview moveto $scroll_pos
456 }
457 ui_ready
458
459 if {[$ui_diff index end] eq {2.0}} {
460 handle_empty_diff
461 } else {
462 set diff_empty_count 0
463 }
464
465 set callback [lindex $cont_info 1]
466 if {$callback ne {}} {
467 eval $callback
468 }
469 }
470}
471
472proc apply_hunk {x y} {
473 global current_diff_path current_diff_header current_diff_side
474 global ui_diff ui_index file_states
475
476 if {$current_diff_path eq {} || $current_diff_header eq {}} return
477 if {![lock_index apply_hunk]} return
478
479 set apply_cmd {apply --cached --whitespace=nowarn}
480 set mi [lindex $file_states($current_diff_path) 0]
481 if {$current_diff_side eq $ui_index} {
482 set failed_msg [mc "Failed to unstage selected hunk."]
483 lappend apply_cmd --reverse
484 if {[string index $mi 0] ne {M}} {
485 unlock_index
486 return
487 }
488 } else {
489 set failed_msg [mc "Failed to stage selected hunk."]
490 if {[string index $mi 1] ne {M}} {
491 unlock_index
492 return
493 }
494 }
495
496 set s_lno [lindex [split [$ui_diff index @$x,$y] .] 0]
497 set s_lno [$ui_diff search -backwards -regexp ^@@ $s_lno.0 0.0]
498 if {$s_lno eq {}} {
499 unlock_index
500 return
501 }
502
503 set e_lno [$ui_diff search -forwards -regexp ^@@ "$s_lno + 1 lines" end]
504 if {$e_lno eq {}} {
505 set e_lno end
506 }
507
508 if {[catch {
509 set enc [get_path_encoding $current_diff_path]
510 set p [eval git_write $apply_cmd]
511 fconfigure $p -translation binary -encoding $enc
512 puts -nonewline $p $current_diff_header
513 puts -nonewline $p [$ui_diff get $s_lno $e_lno]
514 close $p} err]} {
515 error_popup [append $failed_msg "\n\n$err"]
516 unlock_index
517 return
518 }
519
520 $ui_diff conf -state normal
521 $ui_diff delete $s_lno $e_lno
522 $ui_diff conf -state disabled
523
524 if {[$ui_diff get 1.0 end] eq "\n"} {
525 set o _
526 } else {
527 set o ?
528 }
529
530 if {$current_diff_side eq $ui_index} {
531 set mi ${o}M
532 } elseif {[string index $mi 0] eq {_}} {
533 set mi M$o
534 } else {
535 set mi ?$o
536 }
537 unlock_index
538 display_file $current_diff_path $mi
539 # This should trigger shift to the next changed file
540 if {$o eq {_}} {
541 reshow_diff
542 }
543}
544
545proc apply_range_or_line {x y} {
546 global current_diff_path current_diff_header current_diff_side
547 global ui_diff ui_index file_states
548
549 set selected [$ui_diff tag nextrange sel 0.0]
550
551 if {$selected == {}} {
552 set first [$ui_diff index "@$x,$y"]
553 set last $first
554 } else {
555 set first [lindex $selected 0]
556 set last [lindex $selected 1]
557 }
558
559 set first_l [$ui_diff index "$first linestart"]
560 set last_l [$ui_diff index "$last lineend"]
561
562 if {$current_diff_path eq {} || $current_diff_header eq {}} return
563 if {![lock_index apply_hunk]} return
564
565 set apply_cmd {apply --cached --whitespace=nowarn}
566 set mi [lindex $file_states($current_diff_path) 0]
567 if {$current_diff_side eq $ui_index} {
568 set failed_msg [mc "Failed to unstage selected line."]
569 set to_context {+}
570 lappend apply_cmd --reverse
571 if {[string index $mi 0] ne {M}} {
572 unlock_index
573 return
574 }
575 } else {
576 set failed_msg [mc "Failed to stage selected line."]
577 set to_context {-}
578 if {[string index $mi 1] ne {M}} {
579 unlock_index
580 return
581 }
582 }
583
584 set wholepatch {}
585
586 while {$first_l < $last_l} {
587 set i_l [$ui_diff search -backwards -regexp ^@@ $first_l 0.0]
588 if {$i_l eq {}} {
589 # If there's not a @@ above, then the selected range
590 # must have come before the first_l @@
591 set i_l [$ui_diff search -regexp ^@@ $first_l $last_l]
592 }
593 if {$i_l eq {}} {
594 unlock_index
595 return
596 }
597 # $i_l is now at the beginning of a line
598
599 # pick start line number from hunk header
600 set hh [$ui_diff get $i_l "$i_l + 1 lines"]
601 set hh [lindex [split $hh ,] 0]
602 set hln [lindex [split $hh -] 1]
603
604 # There is a special situation to take care of. Consider this
605 # hunk:
606 #
607 # @@ -10,4 +10,4 @@
608 # context before
609 # -old 1
610 # -old 2
611 # +new 1
612 # +new 2
613 # context after
614 #
615 # We used to keep the context lines in the order they appear in
616 # the hunk. But then it is not possible to correctly stage only
617 # "-old 1" and "+new 1" - it would result in this staged text:
618 #
619 # context before
620 # old 2
621 # new 1
622 # context after
623 #
624 # (By symmetry it is not possible to *un*stage "old 2" and "new
625 # 2".)
626 #
627 # We resolve the problem by introducing an asymmetry, namely,
628 # when a "+" line is *staged*, it is moved in front of the
629 # context lines that are generated from the "-" lines that are
630 # immediately before the "+" block. That is, we construct this
631 # patch:
632 #
633 # @@ -10,4 +10,5 @@
634 # context before
635 # +new 1
636 # old 1
637 # old 2
638 # context after
639 #
640 # But we do *not* treat "-" lines that are *un*staged in a
641 # special way.
642 #
643 # With this asymmetry it is possible to stage the change "old
644 # 1" -> "new 1" directly, and to stage the change "old 2" ->
645 # "new 2" by first staging the entire hunk and then unstaging
646 # the change "old 1" -> "new 1".
647 #
648 # Applying multiple lines adds complexity to the special
649 # situation. The pre_context must be moved after the entire
650 # first block of consecutive staged "+" lines, so that
651 # staging both additions gives the following patch:
652 #
653 # @@ -10,4 +10,6 @@
654 # context before
655 # +new 1
656 # +new 2
657 # old 1
658 # old 2
659 # context after
660
661 # This is non-empty if and only if we are _staging_ changes;
662 # then it accumulates the consecutive "-" lines (after
663 # converting them to context lines) in order to be moved after
664 # "+" change lines.
665 set pre_context {}
666
667 set n 0
668 set m 0
669 set i_l [$ui_diff index "$i_l + 1 lines"]
670 set patch {}
671 while {[$ui_diff compare $i_l < "end - 1 chars"] &&
672 [$ui_diff get $i_l "$i_l + 2 chars"] ne {@@}} {
673 set next_l [$ui_diff index "$i_l + 1 lines"]
674 set c1 [$ui_diff get $i_l]
675 if {[$ui_diff compare $first_l <= $i_l] &&
676 [$ui_diff compare $i_l < $last_l] &&
677 ($c1 eq {-} || $c1 eq {+})} {
678 # a line to stage/unstage
679 set ln [$ui_diff get $i_l $next_l]
680 if {$c1 eq {-}} {
681 set n [expr $n+1]
682 set patch "$patch$pre_context$ln"
683 set pre_context {}
684 } else {
685 set m [expr $m+1]
686 set patch "$patch$ln"
687 }
688 } elseif {$c1 ne {-} && $c1 ne {+}} {
689 # context line
690 set ln [$ui_diff get $i_l $next_l]
691 set patch "$patch$pre_context$ln"
692 set n [expr $n+1]
693 set m [expr $m+1]
694 set pre_context {}
695 } elseif {$c1 eq $to_context} {
696 # turn change line into context line
697 set ln [$ui_diff get "$i_l + 1 chars" $next_l]
698 if {$c1 eq {-}} {
699 set pre_context "$pre_context $ln"
700 } else {
701 set patch "$patch $ln"
702 }
703 set n [expr $n+1]
704 set m [expr $m+1]
705 } else {
706 # a change in the opposite direction of
707 # to_context which is outside the range of
708 # lines to apply.
709 set patch "$patch$pre_context"
710 set pre_context {}
711 }
712 set i_l $next_l
713 }
714 set patch "$patch$pre_context"
715 set wholepatch "$wholepatch@@ -$hln,$n +$hln,$m @@\n$patch"
716 set first_l [$ui_diff index "$next_l + 1 lines"]
717 }
718
719 if {[catch {
720 set enc [get_path_encoding $current_diff_path]
721 set p [eval git_write $apply_cmd]
722 fconfigure $p -translation binary -encoding $enc
723 puts -nonewline $p $current_diff_header
724 puts -nonewline $p $wholepatch
725 close $p} err]} {
726 error_popup [append $failed_msg "\n\n$err"]
727 }
728
729 unlock_index
730}