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