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