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