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