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
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 [mc "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 [mc "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 [mc "LOCAL:\n"] d======= \
133 [list ":1:$current_diff_path" ":2:$current_diff_path"]]
134 lappend current_diff_queue \
135 [list [mc "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 100000
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 [mc \
222"* Untracked file is %d bytes.
223* Showing only first %d bytes.
224" $sz $max_sz] d_@
225 }
226 $ui_diff insert end $content
227 if {$sz > $max_sz} {
228 $ui_diff insert end [mc "
229* Untracked file clipped here by %s.
230* To see the entire file, use an external editor.
231" [appname]] 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 tags d$op
381 } else {
382 set tags d_+
383 }
384 }
385 default {
386 puts "error: Unhandled 2 way diff marker: {$op}"
387 set tags {}
388 }
389 }
390 }
391 $ui_diff insert end $line $tags
392 if {[string index $line end] eq "\r"} {
393 $ui_diff tag add d_cr {end - 2c}
394 }
395 $ui_diff insert end "\n" $tags
396 }
397 $ui_diff conf -state disabled
398
399 if {[eof $fd]} {
400 close $fd
401
402 if {$current_diff_queue ne {}} {
403 advance_diff_queue $cont_info
404 return
405 }
406
407 set diff_active 0
408 unlock_index
409 set scroll_pos [lindex $cont_info 0]
410 if {$scroll_pos ne {}} {
411 update
412 $ui_diff yview moveto $scroll_pos
413 }
414 ui_ready
415
416 if {[$ui_diff index end] eq {2.0}} {
417 handle_empty_diff
418 }
419 set callback [lindex $cont_info 1]
420 if {$callback ne {}} {
421 eval $callback
422 }
423 }
424}
425
426proc apply_hunk {x y} {
427 global current_diff_path current_diff_header current_diff_side
428 global ui_diff ui_index file_states
429
430 if {$current_diff_path eq {} || $current_diff_header eq {}} return
431 if {![lock_index apply_hunk]} return
432
433 set apply_cmd {apply --cached --whitespace=nowarn}
434 set mi [lindex $file_states($current_diff_path) 0]
435 if {$current_diff_side eq $ui_index} {
436 set failed_msg [mc "Failed to unstage selected hunk."]
437 lappend apply_cmd --reverse
438 if {[string index $mi 0] ne {M}} {
439 unlock_index
440 return
441 }
442 } else {
443 set failed_msg [mc "Failed to stage selected hunk."]
444 if {[string index $mi 1] ne {M}} {
445 unlock_index
446 return
447 }
448 }
449
450 set s_lno [lindex [split [$ui_diff index @$x,$y] .] 0]
451 set s_lno [$ui_diff search -backwards -regexp ^@@ $s_lno.0 0.0]
452 if {$s_lno eq {}} {
453 unlock_index
454 return
455 }
456
457 set e_lno [$ui_diff search -forwards -regexp ^@@ "$s_lno + 1 lines" end]
458 if {$e_lno eq {}} {
459 set e_lno end
460 }
461
462 if {[catch {
463 set enc [get_path_encoding $current_diff_path]
464 set p [eval git_write $apply_cmd]
465 fconfigure $p -translation binary -encoding $enc
466 puts -nonewline $p $current_diff_header
467 puts -nonewline $p [$ui_diff get $s_lno $e_lno]
468 close $p} err]} {
469 error_popup [append $failed_msg "\n\n$err"]
470 unlock_index
471 return
472 }
473
474 $ui_diff conf -state normal
475 $ui_diff delete $s_lno $e_lno
476 $ui_diff conf -state disabled
477
478 if {[$ui_diff get 1.0 end] eq "\n"} {
479 set o _
480 } else {
481 set o ?
482 }
483
484 if {$current_diff_side eq $ui_index} {
485 set mi ${o}M
486 } elseif {[string index $mi 0] eq {_}} {
487 set mi M$o
488 } else {
489 set mi ?$o
490 }
491 unlock_index
492 display_file $current_diff_path $mi
493 # This should trigger shift to the next changed file
494 if {$o eq {_}} {
495 reshow_diff
496 }
497}
498
499proc apply_line {x y} {
500 global current_diff_path current_diff_header current_diff_side
501 global ui_diff ui_index file_states
502
503 if {$current_diff_path eq {} || $current_diff_header eq {}} return
504 if {![lock_index apply_hunk]} return
505
506 set apply_cmd {apply --cached --whitespace=nowarn}
507 set mi [lindex $file_states($current_diff_path) 0]
508 if {$current_diff_side eq $ui_index} {
509 set failed_msg [mc "Failed to unstage selected line."]
510 set to_context {+}
511 lappend apply_cmd --reverse
512 if {[string index $mi 0] ne {M}} {
513 unlock_index
514 return
515 }
516 } else {
517 set failed_msg [mc "Failed to stage selected line."]
518 set to_context {-}
519 if {[string index $mi 1] ne {M}} {
520 unlock_index
521 return
522 }
523 }
524
525 set the_l [$ui_diff index @$x,$y]
526
527 # operate only on change lines
528 set c1 [$ui_diff get "$the_l linestart"]
529 if {$c1 ne {+} && $c1 ne {-}} {
530 unlock_index
531 return
532 }
533 set sign $c1
534
535 set i_l [$ui_diff search -backwards -regexp ^@@ $the_l 0.0]
536 if {$i_l eq {}} {
537 unlock_index
538 return
539 }
540 # $i_l is now at the beginning of a line
541
542 # pick start line number from hunk header
543 set hh [$ui_diff get $i_l "$i_l + 1 lines"]
544 set hh [lindex [split $hh ,] 0]
545 set hln [lindex [split $hh -] 1]
546
547 # There is a special situation to take care of. Consider this hunk:
548 #
549 # @@ -10,4 +10,4 @@
550 # context before
551 # -old 1
552 # -old 2
553 # +new 1
554 # +new 2
555 # context after
556 #
557 # We used to keep the context lines in the order they appear in the
558 # hunk. But then it is not possible to correctly stage only
559 # "-old 1" and "+new 1" - it would result in this staged text:
560 #
561 # context before
562 # old 2
563 # new 1
564 # context after
565 #
566 # (By symmetry it is not possible to *un*stage "old 2" and "new 2".)
567 #
568 # We resolve the problem by introducing an asymmetry, namely, when
569 # a "+" line is *staged*, it is moved in front of the context lines
570 # that are generated from the "-" lines that are immediately before
571 # the "+" block. That is, we construct this patch:
572 #
573 # @@ -10,4 +10,5 @@
574 # context before
575 # +new 1
576 # old 1
577 # old 2
578 # context after
579 #
580 # But we do *not* treat "-" lines that are *un*staged in a special
581 # way.
582 #
583 # With this asymmetry it is possible to stage the change
584 # "old 1" -> "new 1" directly, and to stage the change
585 # "old 2" -> "new 2" by first staging the entire hunk and
586 # then unstaging the change "old 1" -> "new 1".
587
588 # This is non-empty if and only if we are _staging_ changes;
589 # then it accumulates the consecutive "-" lines (after converting
590 # them to context lines) in order to be moved after the "+" change
591 # line.
592 set pre_context {}
593
594 set n 0
595 set i_l [$ui_diff index "$i_l + 1 lines"]
596 set patch {}
597 while {[$ui_diff compare $i_l < "end - 1 chars"] &&
598 [$ui_diff get $i_l "$i_l + 2 chars"] ne {@@}} {
599 set next_l [$ui_diff index "$i_l + 1 lines"]
600 set c1 [$ui_diff get $i_l]
601 if {[$ui_diff compare $i_l <= $the_l] &&
602 [$ui_diff compare $the_l < $next_l]} {
603 # the line to stage/unstage
604 set ln [$ui_diff get $i_l $next_l]
605 if {$c1 eq {-}} {
606 set n [expr $n+1]
607 set patch "$patch$pre_context$ln"
608 } else {
609 set patch "$patch$ln$pre_context"
610 }
611 set pre_context {}
612 } elseif {$c1 ne {-} && $c1 ne {+}} {
613 # context line
614 set ln [$ui_diff get $i_l $next_l]
615 set patch "$patch$pre_context$ln"
616 set n [expr $n+1]
617 set pre_context {}
618 } elseif {$c1 eq $to_context} {
619 # turn change line into context line
620 set ln [$ui_diff get "$i_l + 1 chars" $next_l]
621 if {$c1 eq {-}} {
622 set pre_context "$pre_context $ln"
623 } else {
624 set patch "$patch $ln"
625 }
626 set n [expr $n+1]
627 }
628 set i_l $next_l
629 }
630 set patch "@@ -$hln,$n +$hln,[eval expr $n $sign 1] @@\n$patch"
631
632 if {[catch {
633 set enc [get_path_encoding $current_diff_path]
634 set p [eval git_write $apply_cmd]
635 fconfigure $p -translation binary -encoding $enc
636 puts -nonewline $p $current_diff_header
637 puts -nonewline $p $patch
638 close $p} err]} {
639 error_popup [append $failed_msg "\n\n$err"]
640 }
641
642 unlock_index
643}