b0ecfbcb5931ee160dbf24dee49940102029e8f5
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 enc [gitattr $path encoding UTF-8]
168 set fd [open $path r]
169 fconfigure $fd \
170 -eofchar {} \
171 -encoding [tcl_encoding $enc]
172 set content [read $fd $max_sz]
173 close $fd
174 set sz [file size $path]
175 }
176 default {
177 error "'$type' not supported"
178 }
179 }
180 } err ]} {
181 set diff_active 0
182 unlock_index
183 ui_status [mc "Unable to display %s" [escape_path $path]]
184 error_popup [strcat [mc "Error loading file:"] "\n\n$err"]
185 return
186 }
187 $ui_diff conf -state normal
188 if {$type eq {submodule}} {
189 $ui_diff insert end [append \
190 "* " \
191 [mc "Git Repository (subproject)"] \
192 "\n"] d_@
193 } elseif {![catch {set type [exec file $path]}]} {
194 set n [string length $path]
195 if {[string equal -length $n $path $type]} {
196 set type [string range $type $n end]
197 regsub {^:?\s*} $type {} type
198 }
199 $ui_diff insert end "* $type\n" d_@
200 }
201 if {[string first "\0" $content] != -1} {
202 $ui_diff insert end \
203 [mc "* Binary file (not showing content)."] \
204 d_@
205 } else {
206 if {$sz > $max_sz} {
207 $ui_diff insert end \
208"* Untracked file is $sz bytes.
209* Showing only first $max_sz bytes.
210" d_@
211 }
212 $ui_diff insert end $content
213 if {$sz > $max_sz} {
214 $ui_diff insert end "
215* Untracked file clipped here by [appname].
216* To see the entire file, use an external editor.
217" d_@
218 }
219 }
220 $ui_diff conf -state disabled
221 set diff_active 0
222 unlock_index
223 if {$scroll_pos ne {}} {
224 update
225 $ui_diff yview moveto $scroll_pos
226 }
227 ui_ready
228 return
229 }
230}
231
232proc start_show_diff {scroll_pos {add_opts {}}} {
233 global file_states file_lists
234 global is_3way_diff diff_active repo_config
235 global ui_diff ui_index ui_workdir
236 global current_diff_path current_diff_side current_diff_header
237
238 set path $current_diff_path
239 set w $current_diff_side
240
241 set s $file_states($path)
242 set m [lindex $s 0]
243 set is_3way_diff 0
244 set diff_active 1
245 set current_diff_header {}
246
247 set cmd [list]
248 if {$w eq $ui_index} {
249 lappend cmd diff-index
250 lappend cmd --cached
251 } elseif {$w eq $ui_workdir} {
252 if {[string first {U} $m] >= 0} {
253 lappend cmd diff
254 } else {
255 lappend cmd diff-files
256 }
257 }
258
259 lappend cmd -p
260 lappend cmd --no-color
261 if {$repo_config(gui.diffcontext) >= 1} {
262 lappend cmd "-U$repo_config(gui.diffcontext)"
263 }
264 if {$w eq $ui_index} {
265 lappend cmd [PARENT]
266 }
267 if {$add_opts ne {}} {
268 eval lappend cmd $add_opts
269 } else {
270 lappend cmd --
271 lappend cmd $path
272 }
273
274 if {[catch {set fd [eval git_read --nice $cmd]} err]} {
275 set diff_active 0
276 unlock_index
277 ui_status [mc "Unable to display %s" [escape_path $path]]
278 error_popup [strcat [mc "Error loading diff:"] "\n\n$err"]
279 return
280 }
281
282 set ::current_diff_inheader 1
283 fconfigure $fd \
284 -blocking 0 \
285 -encoding [tcl_encoding [gitattr $path encoding UTF-8]] \
286 -translation lf
287 fileevent $fd readable [list read_diff $fd $scroll_pos]
288}
289
290proc read_diff {fd scroll_pos} {
291 global ui_diff diff_active
292 global is_3way_diff current_diff_header
293 global current_diff_queue
294
295 $ui_diff conf -state normal
296 while {[gets $fd line] >= 0} {
297 # -- Cleanup uninteresting diff header lines.
298 #
299 if {$::current_diff_inheader} {
300 if { [string match {diff --git *} $line]
301 || [string match {diff --cc *} $line]
302 || [string match {diff --combined *} $line]
303 || [string match {--- *} $line]
304 || [string match {+++ *} $line]} {
305 append current_diff_header $line "\n"
306 continue
307 }
308 }
309 if {[string match {index *} $line]} continue
310 if {$line eq {deleted file mode 120000}} {
311 set line "deleted symlink"
312 }
313 set ::current_diff_inheader 0
314
315 # -- Automatically detect if this is a 3 way diff.
316 #
317 if {[string match {@@@ *} $line]} {set is_3way_diff 1}
318
319 if {[string match {mode *} $line]
320 || [string match {new file *} $line]
321 || [regexp {^(old|new) mode *} $line]
322 || [string match {deleted file *} $line]
323 || [string match {deleted symlink} $line]
324 || [string match {Binary files * and * differ} $line]
325 || $line eq {\ No newline at end of file}
326 || [regexp {^\* Unmerged path } $line]} {
327 set tags {}
328 } elseif {$is_3way_diff} {
329 set op [string range $line 0 1]
330 switch -- $op {
331 { } {set tags {}}
332 {@@} {set tags d_@}
333 { +} {set tags d_s+}
334 { -} {set tags d_s-}
335 {+ } {set tags d_+s}
336 {- } {set tags d_-s}
337 {--} {set tags d_--}
338 {++} {
339 if {[regexp {^\+\+([<>]{7} |={7})} $line _g op]} {
340 set line [string replace $line 0 1 { }]
341 set tags d$op
342 } else {
343 set tags d_++
344 }
345 }
346 default {
347 puts "error: Unhandled 3 way diff marker: {$op}"
348 set tags {}
349 }
350 }
351 } else {
352 set op [string index $line 0]
353 switch -- $op {
354 { } {set tags {}}
355 {@} {set tags d_@}
356 {-} {set tags d_-}
357 {+} {
358 if {[regexp {^\+([<>]{7} |={7})} $line _g op]} {
359 set line [string replace $line 0 0 { }]
360 set tags d$op
361 } else {
362 set tags d_+
363 }
364 }
365 default {
366 puts "error: Unhandled 2 way diff marker: {$op}"
367 set tags {}
368 }
369 }
370 }
371 $ui_diff insert end $line $tags
372 if {[string index $line end] eq "\r"} {
373 $ui_diff tag add d_cr {end - 2c}
374 }
375 $ui_diff insert end "\n" $tags
376 }
377 $ui_diff conf -state disabled
378
379 if {[eof $fd]} {
380 close $fd
381
382 if {$current_diff_queue ne {}} {
383 advance_diff_queue $scroll_pos
384 return
385 }
386
387 set diff_active 0
388 unlock_index
389 if {$scroll_pos ne {}} {
390 update
391 $ui_diff yview moveto $scroll_pos
392 }
393 ui_ready
394
395 if {[$ui_diff index end] eq {2.0}} {
396 handle_empty_diff
397 }
398 }
399}
400
401proc apply_hunk {x y} {
402 global current_diff_path current_diff_header current_diff_side
403 global ui_diff ui_index file_states
404
405 if {$current_diff_path eq {} || $current_diff_header eq {}} return
406 if {![lock_index apply_hunk]} return
407
408 set apply_cmd {apply --cached --whitespace=nowarn}
409 set mi [lindex $file_states($current_diff_path) 0]
410 if {$current_diff_side eq $ui_index} {
411 set failed_msg [mc "Failed to unstage selected hunk."]
412 lappend apply_cmd --reverse
413 if {[string index $mi 0] ne {M}} {
414 unlock_index
415 return
416 }
417 } else {
418 set failed_msg [mc "Failed to stage selected hunk."]
419 if {[string index $mi 1] ne {M}} {
420 unlock_index
421 return
422 }
423 }
424
425 set s_lno [lindex [split [$ui_diff index @$x,$y] .] 0]
426 set s_lno [$ui_diff search -backwards -regexp ^@@ $s_lno.0 0.0]
427 if {$s_lno eq {}} {
428 unlock_index
429 return
430 }
431
432 set e_lno [$ui_diff search -forwards -regexp ^@@ "$s_lno + 1 lines" end]
433 if {$e_lno eq {}} {
434 set e_lno end
435 }
436
437 if {[catch {
438 set p [eval git_write $apply_cmd]
439 fconfigure $p -translation binary -encoding binary
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 p [eval git_write $apply_cmd]
608 fconfigure $p -translation binary -encoding binary
609 puts -nonewline $p $current_diff_header
610 puts -nonewline $p $patch
611 close $p} err]} {
612 error_popup [append $failed_msg "\n\n$err"]
613 }
614
615 unlock_index
616}