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