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