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