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 {} {
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
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
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
55 set path $current_diff_path
56 set s $file_states($path)
57 if {[lindex $s 0] ne {_M}} return
58
59 info_popup [mc "No differences detected.
60
61%s has no changes.
62
63The modification date of this file was updated by another application, but the content within the file was not changed.
64
65A rescan will be automatically started to find other files which may have the same state." [short_path $path]]
66
67 clear_diff
68 display_file $path __
69 rescan ui_ready 0
70}
71
72proc show_diff {path w {lno {}} {scroll_pos {}} {callback {}}} {
73 global file_states file_lists
74 global is_3way_diff is_conflict_diff diff_active repo_config
75 global ui_diff ui_index ui_workdir
76 global current_diff_path current_diff_side current_diff_header
77 global current_diff_queue
78
79 if {$diff_active || ![lock_index read]} return
80
81 clear_diff
82 if {$lno == {}} {
83 set lno [lsearch -sorted -exact $file_lists($w) $path]
84 if {$lno >= 0} {
85 incr lno
86 }
87 }
88 if {$lno >= 1} {
89 $w tag add in_diff $lno.0 [expr {$lno + 1}].0
90 $w see $lno.0
91 }
92
93 set s $file_states($path)
94 set m [lindex $s 0]
95 set is_conflict_diff 0
96 set current_diff_path $path
97 set current_diff_side $w
98 set current_diff_queue {}
99 ui_status [mc "Loading diff of %s..." [escape_path $path]]
100
101 set cont_info [list $scroll_pos $callback]
102
103 if {[string first {U} $m] >= 0} {
104 merge_load_stages $path [list show_unmerged_diff $cont_info]
105 } elseif {$m eq {_O}} {
106 show_other_diff $path $w $m $cont_info
107 } else {
108 start_show_diff $cont_info
109 }
110}
111
112proc show_unmerged_diff {cont_info} {
113 global current_diff_path current_diff_side
114 global merge_stages ui_diff is_conflict_diff
115 global current_diff_queue
116
117 if {$merge_stages(2) eq {}} {
118 set is_conflict_diff 1
119 lappend current_diff_queue \
120 [list "LOCAL: deleted\nREMOTE:\n" d======= \
121 [list ":1:$current_diff_path" ":3:$current_diff_path"]]
122 } elseif {$merge_stages(3) eq {}} {
123 set is_conflict_diff 1
124 lappend current_diff_queue \
125 [list "REMOTE: deleted\nLOCAL:\n" d======= \
126 [list ":1:$current_diff_path" ":2:$current_diff_path"]]
127 } elseif {[lindex $merge_stages(1) 0] eq {120000}
128 || [lindex $merge_stages(2) 0] eq {120000}
129 || [lindex $merge_stages(3) 0] eq {120000}} {
130 set is_conflict_diff 1
131 lappend current_diff_queue \
132 [list "LOCAL:\n" d======= \
133 [list ":1:$current_diff_path" ":2:$current_diff_path"]]
134 lappend current_diff_queue \
135 [list "REMOTE:\n" d======= \
136 [list ":1:$current_diff_path" ":3:$current_diff_path"]]
137 } else {
138 start_show_diff $cont_info
139 return
140 }
141
142 advance_diff_queue $cont_info
143}
144
145proc advance_diff_queue {cont_info} {
146 global current_diff_queue ui_diff
147
148 set item [lindex $current_diff_queue 0]
149 set current_diff_queue [lrange $current_diff_queue 1 end]
150
151 $ui_diff conf -state normal
152 $ui_diff insert end [lindex $item 0] [lindex $item 1]
153 $ui_diff conf -state disabled
154
155 start_show_diff $cont_info [lindex $item 2]
156}
157
158proc show_other_diff {path w m cont_info} {
159 global file_states file_lists
160 global is_3way_diff diff_active repo_config
161 global ui_diff ui_index ui_workdir
162 global current_diff_path current_diff_side current_diff_header
163
164 # - Git won't give us the diff, there's nothing to compare to!
165 #
166 if {$m eq {_O}} {
167 set max_sz [expr {128 * 1024}]
168 set type unknown
169 if {[catch {
170 set type [file type $path]
171 switch -- $type {
172 directory {
173 set type submodule
174 set content {}
175 set sz 0
176 }
177 link {
178 set content [file readlink $path]
179 set sz [string length $content]
180 }
181 file {
182 set fd [open $path r]
183 fconfigure $fd \
184 -eofchar {} \
185 -encoding [get_path_encoding $path]
186 set content [read $fd $max_sz]
187 close $fd
188 set sz [file size $path]
189 }
190 default {
191 error "'$type' not supported"
192 }
193 }
194 } err ]} {
195 set diff_active 0
196 unlock_index
197 ui_status [mc "Unable to display %s" [escape_path $path]]
198 error_popup [strcat [mc "Error loading file:"] "\n\n$err"]
199 return
200 }
201 $ui_diff conf -state normal
202 if {$type eq {submodule}} {
203 $ui_diff insert end [append \
204 "* " \
205 [mc "Git Repository (subproject)"] \
206 "\n"] d_@
207 } elseif {![catch {set type [exec file $path]}]} {
208 set n [string length $path]
209 if {[string equal -length $n $path $type]} {
210 set type [string range $type $n end]
211 regsub {^:?\s*} $type {} type
212 }
213 $ui_diff insert end "* $type\n" d_@
214 }
215 if {[string first "\0" $content] != -1} {
216 $ui_diff insert end \
217 [mc "* Binary file (not showing content)."] \
218 d_@
219 } else {
220 if {$sz > $max_sz} {
221 $ui_diff insert end \
222"* Untracked file is $sz bytes.
223* Showing only first $max_sz bytes.
224" d_@
225 }
226 $ui_diff insert end $content
227 if {$sz > $max_sz} {
228 $ui_diff insert end "
229* Untracked file clipped here by [appname].
230* To see the entire file, use an external editor.
231" d_@
232 }
233 }
234 $ui_diff conf -state disabled
235 set diff_active 0
236 unlock_index
237 set scroll_pos [lindex $cont_info 0]
238 if {$scroll_pos ne {}} {
239 update
240 $ui_diff yview moveto $scroll_pos
241 }
242 ui_ready
243 set callback [lindex $cont_info 1]
244 if {$callback ne {}} {
245 eval $callback
246 }
247 return
248 }
249}
250
251proc start_show_diff {cont_info {add_opts {}}} {
252 global file_states file_lists
253 global is_3way_diff diff_active repo_config
254 global ui_diff ui_index ui_workdir
255 global current_diff_path current_diff_side current_diff_header
256
257 set path $current_diff_path
258 set w $current_diff_side
259
260 set s $file_states($path)
261 set m [lindex $s 0]
262 set is_3way_diff 0
263 set diff_active 1
264 set current_diff_header {}
265
266 set cmd [list]
267 if {$w eq $ui_index} {
268 lappend cmd diff-index
269 lappend cmd --cached
270 } elseif {$w eq $ui_workdir} {
271 if {[string first {U} $m] >= 0} {
272 lappend cmd diff
273 } else {
274 lappend cmd diff-files
275 }
276 }
277
278 lappend cmd -p
279 lappend cmd --no-color
280 if {$repo_config(gui.diffcontext) >= 1} {
281 lappend cmd "-U$repo_config(gui.diffcontext)"
282 }
283 if {$w eq $ui_index} {
284 lappend cmd [PARENT]
285 }
286 if {$add_opts ne {}} {
287 eval lappend cmd $add_opts
288 } else {
289 lappend cmd --
290 lappend cmd $path
291 }
292
293 if {[catch {set fd [eval git_read --nice $cmd]} err]} {
294 set diff_active 0
295 unlock_index
296 ui_status [mc "Unable to display %s" [escape_path $path]]
297 error_popup [strcat [mc "Error loading diff:"] "\n\n$err"]
298 return
299 }
300
301 set ::current_diff_inheader 1
302 fconfigure $fd \
303 -blocking 0 \
304 -encoding [get_path_encoding $path] \
305 -translation lf
306 fileevent $fd readable [list read_diff $fd $cont_info]
307}
308
309proc read_diff {fd cont_info} {
310 global ui_diff diff_active
311 global is_3way_diff is_conflict_diff current_diff_header
312 global current_diff_queue
313
314 $ui_diff conf -state normal
315 while {[gets $fd line] >= 0} {
316 # -- Cleanup uninteresting diff header lines.
317 #
318 if {$::current_diff_inheader} {
319 if { [string match {diff --git *} $line]
320 || [string match {diff --cc *} $line]
321 || [string match {diff --combined *} $line]
322 || [string match {--- *} $line]
323 || [string match {+++ *} $line]} {
324 append current_diff_header $line "\n"
325 continue
326 }
327 }
328 if {[string match {index *} $line]} continue
329 if {$line eq {deleted file mode 120000}} {
330 set line "deleted symlink"
331 }
332 set ::current_diff_inheader 0
333
334 # -- Automatically detect if this is a 3 way diff.
335 #
336 if {[string match {@@@ *} $line]} {set is_3way_diff 1}
337
338 if {[string match {mode *} $line]
339 || [string match {new file *} $line]
340 || [regexp {^(old|new) mode *} $line]
341 || [string match {deleted file *} $line]
342 || [string match {deleted symlink} $line]
343 || [string match {Binary files * and * differ} $line]
344 || $line eq {\ No newline at end of file}
345 || [regexp {^\* Unmerged path } $line]} {
346 set tags {}
347 } elseif {$is_3way_diff} {
348 set op [string range $line 0 1]
349 switch -- $op {
350 { } {set tags {}}
351 {@@} {set tags d_@}
352 { +} {set tags d_s+}
353 { -} {set tags d_s-}
354 {+ } {set tags d_+s}
355 {- } {set tags d_-s}
356 {--} {set tags d_--}
357 {++} {
358 if {[regexp {^\+\+([<>]{7} |={7})} $line _g op]} {
359 set is_conflict_diff 1
360 set line [string replace $line 0 1 { }]
361 set tags d$op
362 } else {
363 set tags d_++
364 }
365 }
366 default {
367 puts "error: Unhandled 3 way diff marker: {$op}"
368 set tags {}
369 }
370 }
371 } else {
372 set op [string index $line 0]
373 switch -- $op {
374 { } {set tags {}}
375 {@} {set tags d_@}
376 {-} {set tags d_-}
377 {+} {
378 if {[regexp {^\+([<>]{7} |={7})} $line _g op]} {
379 set is_conflict_diff 1
380 set line [string replace $line 0 0 { }]
381 set tags d$op
382 } else {
383 set tags d_+
384 }
385 }
386 default {
387 puts "error: Unhandled 2 way diff marker: {$op}"
388 set tags {}
389 }
390 }
391 }
392 $ui_diff insert end $line $tags
393 if {[string index $line end] eq "\r"} {
394 $ui_diff tag add d_cr {end - 2c}
395 }
396 $ui_diff insert end "\n" $tags
397 }
398 $ui_diff conf -state disabled
399
400 if {[eof $fd]} {
401 close $fd
402
403 if {$current_diff_queue ne {}} {
404 advance_diff_queue $cont_info
405 return
406 }
407
408 set diff_active 0
409 unlock_index
410 set scroll_pos [lindex $cont_info 0]
411 if {$scroll_pos ne {}} {
412 update
413 $ui_diff yview moveto $scroll_pos
414 }
415 ui_ready
416
417 if {[$ui_diff index end] eq {2.0}} {
418 handle_empty_diff
419 }
420 set callback [lindex $cont_info 1]
421 if {$callback ne {}} {
422 eval $callback
423 }
424 }
425}
426
427proc apply_hunk {x y} {
428 global current_diff_path current_diff_header current_diff_side
429 global ui_diff ui_index file_states
430
431 if {$current_diff_path eq {} || $current_diff_header eq {}} return
432 if {![lock_index apply_hunk]} return
433
434 set apply_cmd {apply --cached --whitespace=nowarn}
435 set mi [lindex $file_states($current_diff_path) 0]
436 if {$current_diff_side eq $ui_index} {
437 set failed_msg [mc "Failed to unstage selected hunk."]
438 lappend apply_cmd --reverse
439 if {[string index $mi 0] ne {M}} {
440 unlock_index
441 return
442 }
443 } else {
444 set failed_msg [mc "Failed to stage selected hunk."]
445 if {[string index $mi 1] ne {M}} {
446 unlock_index
447 return
448 }
449 }
450
451 set s_lno [lindex [split [$ui_diff index @$x,$y] .] 0]
452 set s_lno [$ui_diff search -backwards -regexp ^@@ $s_lno.0 0.0]
453 if {$s_lno eq {}} {
454 unlock_index
455 return
456 }
457
458 set e_lno [$ui_diff search -forwards -regexp ^@@ "$s_lno + 1 lines" end]
459 if {$e_lno eq {}} {
460 set e_lno end
461 }
462
463 if {[catch {
464 set enc [get_path_encoding $current_diff_path]
465 set p [eval git_write $apply_cmd]
466 fconfigure $p -translation binary -encoding $enc
467 puts -nonewline $p $current_diff_header
468 puts -nonewline $p [$ui_diff get $s_lno $e_lno]
469 close $p} err]} {
470 error_popup [append $failed_msg "\n\n$err"]
471 unlock_index
472 return
473 }
474
475 $ui_diff conf -state normal
476 $ui_diff delete $s_lno $e_lno
477 $ui_diff conf -state disabled
478
479 if {[$ui_diff get 1.0 end] eq "\n"} {
480 set o _
481 } else {
482 set o ?
483 }
484
485 if {$current_diff_side eq $ui_index} {
486 set mi ${o}M
487 } elseif {[string index $mi 0] eq {_}} {
488 set mi M$o
489 } else {
490 set mi ?$o
491 }
492 unlock_index
493 display_file $current_diff_path $mi
494 # This should trigger shift to the next changed file
495 if {$o eq {_}} {
496 reshow_diff
497 }
498}
499
500proc apply_line {x y} {
501 global current_diff_path current_diff_header current_diff_side
502 global ui_diff ui_index file_states
503
504 if {$current_diff_path eq {} || $current_diff_header eq {}} return
505 if {![lock_index apply_hunk]} return
506
507 set apply_cmd {apply --cached --whitespace=nowarn}
508 set mi [lindex $file_states($current_diff_path) 0]
509 if {$current_diff_side eq $ui_index} {
510 set failed_msg [mc "Failed to unstage selected line."]
511 set to_context {+}
512 lappend apply_cmd --reverse
513 if {[string index $mi 0] ne {M}} {
514 unlock_index
515 return
516 }
517 } else {
518 set failed_msg [mc "Failed to stage selected line."]
519 set to_context {-}
520 if {[string index $mi 1] ne {M}} {
521 unlock_index
522 return
523 }
524 }
525
526 set the_l [$ui_diff index @$x,$y]
527
528 # operate only on change lines
529 set c1 [$ui_diff get "$the_l linestart"]
530 if {$c1 ne {+} && $c1 ne {-}} {
531 unlock_index
532 return
533 }
534 set sign $c1
535
536 set i_l [$ui_diff search -backwards -regexp ^@@ $the_l 0.0]
537 if {$i_l eq {}} {
538 unlock_index
539 return
540 }
541 # $i_l is now at the beginning of a line
542
543 # pick start line number from hunk header
544 set hh [$ui_diff get $i_l "$i_l + 1 lines"]
545 set hh [lindex [split $hh ,] 0]
546 set hln [lindex [split $hh -] 1]
547
548 # There is a special situation to take care of. Consider this hunk:
549 #
550 # @@ -10,4 +10,4 @@
551 # context before
552 # -old 1
553 # -old 2
554 # +new 1
555 # +new 2
556 # context after
557 #
558 # We used to keep the context lines in the order they appear in the
559 # hunk. But then it is not possible to correctly stage only
560 # "-old 1" and "+new 1" - it would result in this staged text:
561 #
562 # context before
563 # old 2
564 # new 1
565 # context after
566 #
567 # (By symmetry it is not possible to *un*stage "old 2" and "new 2".)
568 #
569 # We resolve the problem by introducing an asymmetry, namely, when
570 # a "+" line is *staged*, it is moved in front of the context lines
571 # that are generated from the "-" lines that are immediately before
572 # the "+" block. That is, we construct this patch:
573 #
574 # @@ -10,4 +10,5 @@
575 # context before
576 # +new 1
577 # old 1
578 # old 2
579 # context after
580 #
581 # But we do *not* treat "-" lines that are *un*staged in a special
582 # way.
583 #
584 # With this asymmetry it is possible to stage the change
585 # "old 1" -> "new 1" directly, and to stage the change
586 # "old 2" -> "new 2" by first staging the entire hunk and
587 # then unstaging the change "old 1" -> "new 1".
588
589 # This is non-empty if and only if we are _staging_ changes;
590 # then it accumulates the consecutive "-" lines (after converting
591 # them to context lines) in order to be moved after the "+" change
592 # line.
593 set pre_context {}
594
595 set n 0
596 set i_l [$ui_diff index "$i_l + 1 lines"]
597 set patch {}
598 while {[$ui_diff compare $i_l < "end - 1 chars"] &&
599 [$ui_diff get $i_l "$i_l + 2 chars"] ne {@@}} {
600 set next_l [$ui_diff index "$i_l + 1 lines"]
601 set c1 [$ui_diff get $i_l]
602 if {[$ui_diff compare $i_l <= $the_l] &&
603 [$ui_diff compare $the_l < $next_l]} {
604 # the line to stage/unstage
605 set ln [$ui_diff get $i_l $next_l]
606 if {$c1 eq {-}} {
607 set n [expr $n+1]
608 set patch "$patch$pre_context$ln"
609 } else {
610 set patch "$patch$ln$pre_context"
611 }
612 set pre_context {}
613 } elseif {$c1 ne {-} && $c1 ne {+}} {
614 # context line
615 set ln [$ui_diff get $i_l $next_l]
616 set patch "$patch$pre_context$ln"
617 set n [expr $n+1]
618 set pre_context {}
619 } elseif {$c1 eq $to_context} {
620 # turn change line into context line
621 set ln [$ui_diff get "$i_l + 1 chars" $next_l]
622 if {$c1 eq {-}} {
623 set pre_context "$pre_context $ln"
624 } else {
625 set patch "$patch $ln"
626 }
627 set n [expr $n+1]
628 }
629 set i_l $next_l
630 }
631 set patch "@@ -$hln,$n +$hln,[eval expr $n $sign 1] @@\n$patch"
632
633 if {[catch {
634 set enc [get_path_encoding $current_diff_path]
635 set p [eval git_write $apply_cmd]
636 fconfigure $p -translation binary -encoding $enc
637 puts -nonewline $p $current_diff_header
638 puts -nonewline $p $patch
639 close $p} err]} {
640 error_popup [append $failed_msg "\n\n$err"]
641 }
642
643 unlock_index
644}