git-gui: Reimplement and enhance auto-selection of diffs.
[git/gitweb.git] / lib / diff.tcl
blob153437b18e15e31d310994df8e9f26cb563bfe2e
1 # git-gui diff viewer
2 # Copyright (C) 2006, 2007 Shawn Pearce
4 proc clear_diff {} {
5 global ui_diff current_diff_path current_diff_header
6 global ui_index ui_workdir
8 $ui_diff conf -state normal
9 $ui_diff delete 0.0 end
10 $ui_diff conf -state disabled
12 set current_diff_path {}
13 set current_diff_header {}
15 $ui_index tag remove in_diff 0.0 end
16 $ui_workdir tag remove in_diff 0.0 end
19 proc reshow_diff {} {
20 global file_states file_lists
21 global current_diff_path current_diff_side
22 global ui_diff
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} {
32 if {[find_next_diff $current_diff_side $p {} {[^O]}]} {
33 next_diff
34 } else {
35 clear_diff
37 } else {
38 set save_pos [lindex [$ui_diff yview] 0]
39 show_diff $p $current_diff_side {} $save_pos
43 proc handle_empty_diff {} {
44 global current_diff_path file_states file_lists
46 set path $current_diff_path
47 set s $file_states($path)
48 if {[lindex $s 0] ne {_M}} return
50 info_popup [mc "No differences detected.
52 %s has no changes.
54 The modification date of this file was updated by another application, but the content within the file was not changed.
56 A rescan will be automatically started to find other files which may have the same state." [short_path $path]]
58 clear_diff
59 display_file $path __
60 rescan ui_ready 0
63 proc 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
69 if {$diff_active || ![lock_index read]} return
71 clear_diff
72 if {$lno == {}} {
73 set lno [lsearch -sorted -exact $file_lists($w) $path]
74 if {$lno >= 0} {
75 incr lno
78 if {$lno >= 1} {
79 $w tag add in_diff $lno.0 [expr {$lno + 1}].0
80 $w see $lno.0
83 set s $file_states($path)
84 set m [lindex $s 0]
85 set is_3way_diff 0
86 set diff_active 1
87 set current_diff_path $path
88 set current_diff_side $w
89 set current_diff_header {}
90 ui_status [mc "Loading diff of %s..." [escape_path $path]]
92 # - Git won't give us the diff, there's nothing to compare to!
94 if {$m eq {_O}} {
95 set max_sz [expr {128 * 1024}]
96 set type unknown
97 if {[catch {
98 set type [file type $path]
99 switch -- $type {
100 directory {
101 set type submodule
102 set content {}
103 set sz 0
105 link {
106 set content [file readlink $path]
107 set sz [string length $content]
109 file {
110 set fd [open $path r]
111 fconfigure $fd -eofchar {}
112 set content [read $fd $max_sz]
113 close $fd
114 set sz [file size $path]
116 default {
117 error "'$type' not supported"
120 } err ]} {
121 set diff_active 0
122 unlock_index
123 ui_status [mc "Unable to display %s" [escape_path $path]]
124 error_popup [strcat [mc "Error loading file:"] "\n\n$err"]
125 return
127 $ui_diff conf -state normal
128 if {$type eq {submodule}} {
129 $ui_diff insert end [append \
130 "* " \
131 [mc "Git Repository (subproject)"] \
132 "\n"] d_@
133 } elseif {![catch {set type [exec file $path]}]} {
134 set n [string length $path]
135 if {[string equal -length $n $path $type]} {
136 set type [string range $type $n end]
137 regsub {^:?\s*} $type {} type
139 $ui_diff insert end "* $type\n" d_@
141 if {[string first "\0" $content] != -1} {
142 $ui_diff insert end \
143 [mc "* Binary file (not showing content)."] \
145 } else {
146 if {$sz > $max_sz} {
147 $ui_diff insert end \
148 "* Untracked file is $sz bytes.
149 * Showing only first $max_sz bytes.
150 " d_@
152 $ui_diff insert end $content
153 if {$sz > $max_sz} {
154 $ui_diff insert end "
155 * Untracked file clipped here by [appname].
156 * To see the entire file, use an external editor.
157 " d_@
160 $ui_diff conf -state disabled
161 set diff_active 0
162 unlock_index
163 if {$scroll_pos ne {}} {
164 update
165 $ui_diff yview moveto $scroll_pos
167 ui_ready
168 return
171 set cmd [list]
172 if {$w eq $ui_index} {
173 lappend cmd diff-index
174 lappend cmd --cached
175 } elseif {$w eq $ui_workdir} {
176 if {[string first {U} $m] >= 0} {
177 lappend cmd diff
178 } else {
179 lappend cmd diff-files
183 lappend cmd -p
184 lappend cmd --no-color
185 if {$repo_config(gui.diffcontext) >= 1} {
186 lappend cmd "-U$repo_config(gui.diffcontext)"
188 if {$w eq $ui_index} {
189 lappend cmd [PARENT]
191 lappend cmd --
192 lappend cmd $path
194 if {[catch {set fd [eval git_read --nice $cmd]} err]} {
195 set diff_active 0
196 unlock_index
197 ui_status [mc "Unable to display %s" [escape_path $path]]
198 error_popup [strcat [mc "Error loading diff:"] "\n\n$err"]
199 return
202 fconfigure $fd \
203 -blocking 0 \
204 -encoding binary \
205 -translation binary
206 fileevent $fd readable [list read_diff $fd $scroll_pos]
209 proc read_diff {fd scroll_pos} {
210 global ui_diff diff_active
211 global is_3way_diff current_diff_header
213 $ui_diff conf -state normal
214 while {[gets $fd line] >= 0} {
215 # -- Cleanup uninteresting diff header lines.
217 if { [string match {diff --git *} $line]
218 || [string match {diff --cc *} $line]
219 || [string match {diff --combined *} $line]
220 || [string match {--- *} $line]
221 || [string match {+++ *} $line]} {
222 append current_diff_header $line "\n"
223 continue
225 if {[string match {index *} $line]} continue
226 if {$line eq {deleted file mode 120000}} {
227 set line "deleted symlink"
230 # -- Automatically detect if this is a 3 way diff.
232 if {[string match {@@@ *} $line]} {set is_3way_diff 1}
234 if {[string match {mode *} $line]
235 || [string match {new file *} $line]
236 || [regexp {^(old|new) mode *} $line]
237 || [string match {deleted file *} $line]
238 || [string match {deleted symlink} $line]
239 || [string match {Binary files * and * differ} $line]
240 || $line eq {\ No newline at end of file}
241 || [regexp {^\* Unmerged path } $line]} {
242 set tags {}
243 } elseif {$is_3way_diff} {
244 set op [string range $line 0 1]
245 switch -- $op {
246 { } {set tags {}}
247 {@@} {set tags d_@}
248 { +} {set tags d_s+}
249 { -} {set tags d_s-}
250 {+ } {set tags d_+s}
251 {- } {set tags d_-s}
252 {--} {set tags d_--}
253 {++} {
254 if {[regexp {^\+\+([<>]{7} |={7})} $line _g op]} {
255 set line [string replace $line 0 1 { }]
256 set tags d$op
257 } else {
258 set tags d_++
261 default {
262 puts "error: Unhandled 3 way diff marker: {$op}"
263 set tags {}
266 } else {
267 set op [string index $line 0]
268 switch -- $op {
269 { } {set tags {}}
270 {@} {set tags d_@}
271 {-} {set tags d_-}
272 {+} {
273 if {[regexp {^\+([<>]{7} |={7})} $line _g op]} {
274 set line [string replace $line 0 0 { }]
275 set tags d$op
276 } else {
277 set tags d_+
280 default {
281 puts "error: Unhandled 2 way diff marker: {$op}"
282 set tags {}
286 $ui_diff insert end $line $tags
287 if {[string index $line end] eq "\r"} {
288 $ui_diff tag add d_cr {end - 2c}
290 $ui_diff insert end "\n" $tags
292 $ui_diff conf -state disabled
294 if {[eof $fd]} {
295 close $fd
296 set diff_active 0
297 unlock_index
298 if {$scroll_pos ne {}} {
299 update
300 $ui_diff yview moveto $scroll_pos
302 ui_ready
304 if {[$ui_diff index end] eq {2.0}} {
305 handle_empty_diff
310 proc apply_hunk {x y} {
311 global current_diff_path current_diff_header current_diff_side
312 global ui_diff ui_index file_states
314 if {$current_diff_path eq {} || $current_diff_header eq {}} return
315 if {![lock_index apply_hunk]} return
317 set apply_cmd {apply --cached --whitespace=nowarn}
318 set mi [lindex $file_states($current_diff_path) 0]
319 if {$current_diff_side eq $ui_index} {
320 set failed_msg [mc "Failed to unstage selected hunk."]
321 lappend apply_cmd --reverse
322 if {[string index $mi 0] ne {M}} {
323 unlock_index
324 return
326 } else {
327 set failed_msg [mc "Failed to stage selected hunk."]
328 if {[string index $mi 1] ne {M}} {
329 unlock_index
330 return
334 set s_lno [lindex [split [$ui_diff index @$x,$y] .] 0]
335 set s_lno [$ui_diff search -backwards -regexp ^@@ $s_lno.0 0.0]
336 if {$s_lno eq {}} {
337 unlock_index
338 return
341 set e_lno [$ui_diff search -forwards -regexp ^@@ "$s_lno + 1 lines" end]
342 if {$e_lno eq {}} {
343 set e_lno end
346 if {[catch {
347 set p [eval git_write $apply_cmd]
348 fconfigure $p -translation binary -encoding binary
349 puts -nonewline $p $current_diff_header
350 puts -nonewline $p [$ui_diff get $s_lno $e_lno]
351 close $p} err]} {
352 error_popup [append $failed_msg "\n\n$err"]
353 unlock_index
354 return
357 $ui_diff conf -state normal
358 $ui_diff delete $s_lno $e_lno
359 $ui_diff conf -state disabled
361 if {[$ui_diff get 1.0 end] eq "\n"} {
362 set o _
363 } else {
364 set o ?
367 if {$current_diff_side eq $ui_index} {
368 set mi ${o}M
369 } elseif {[string index $mi 0] eq {_}} {
370 set mi M$o
371 } else {
372 set mi ?$o
374 unlock_index
375 display_file $current_diff_path $mi
376 # This should trigger shift to the next changed file
377 if {$o eq {_}} {
378 reshow_diff
382 proc apply_line {x y} {
383 global current_diff_path current_diff_header current_diff_side
384 global ui_diff ui_index file_states
386 if {$current_diff_path eq {} || $current_diff_header eq {}} return
387 if {![lock_index apply_hunk]} return
389 set apply_cmd {apply --cached --whitespace=nowarn}
390 set mi [lindex $file_states($current_diff_path) 0]
391 if {$current_diff_side eq $ui_index} {
392 set failed_msg [mc "Failed to unstage selected line."]
393 set to_context {+}
394 lappend apply_cmd --reverse
395 if {[string index $mi 0] ne {M}} {
396 unlock_index
397 return
399 } else {
400 set failed_msg [mc "Failed to stage selected line."]
401 set to_context {-}
402 if {[string index $mi 1] ne {M}} {
403 unlock_index
404 return
408 set the_l [$ui_diff index @$x,$y]
410 # operate only on change lines
411 set c1 [$ui_diff get "$the_l linestart"]
412 if {$c1 ne {+} && $c1 ne {-}} {
413 unlock_index
414 return
416 set sign $c1
418 set i_l [$ui_diff search -backwards -regexp ^@@ $the_l 0.0]
419 if {$i_l eq {}} {
420 unlock_index
421 return
423 # $i_l is now at the beginning of a line
425 # pick start line number from hunk header
426 set hh [$ui_diff get $i_l "$i_l + 1 lines"]
427 set hh [lindex [split $hh ,] 0]
428 set hln [lindex [split $hh -] 1]
430 # There is a special situation to take care of. Consider this hunk:
432 # @@ -10,4 +10,4 @@
433 # context before
434 # -old 1
435 # -old 2
436 # +new 1
437 # +new 2
438 # context after
440 # We used to keep the context lines in the order they appear in the
441 # hunk. But then it is not possible to correctly stage only
442 # "-old 1" and "+new 1" - it would result in this staged text:
444 # context before
445 # old 2
446 # new 1
447 # context after
449 # (By symmetry it is not possible to *un*stage "old 2" and "new 2".)
451 # We resolve the problem by introducing an asymmetry, namely, when
452 # a "+" line is *staged*, it is moved in front of the context lines
453 # that are generated from the "-" lines that are immediately before
454 # the "+" block. That is, we construct this patch:
456 # @@ -10,4 +10,5 @@
457 # context before
458 # +new 1
459 # old 1
460 # old 2
461 # context after
463 # But we do *not* treat "-" lines that are *un*staged in a special
464 # way.
466 # With this asymmetry it is possible to stage the change
467 # "old 1" -> "new 1" directly, and to stage the change
468 # "old 2" -> "new 2" by first staging the entire hunk and
469 # then unstaging the change "old 1" -> "new 1".
471 # This is non-empty if and only if we are _staging_ changes;
472 # then it accumulates the consecutive "-" lines (after converting
473 # them to context lines) in order to be moved after the "+" change
474 # line.
475 set pre_context {}
477 set n 0
478 set i_l [$ui_diff index "$i_l + 1 lines"]
479 set patch {}
480 while {[$ui_diff compare $i_l < "end - 1 chars"] &&
481 [$ui_diff get $i_l "$i_l + 2 chars"] ne {@@}} {
482 set next_l [$ui_diff index "$i_l + 1 lines"]
483 set c1 [$ui_diff get $i_l]
484 if {[$ui_diff compare $i_l <= $the_l] &&
485 [$ui_diff compare $the_l < $next_l]} {
486 # the line to stage/unstage
487 set ln [$ui_diff get $i_l $next_l]
488 if {$c1 eq {-}} {
489 set n [expr $n+1]
490 set patch "$patch$pre_context$ln"
491 } else {
492 set patch "$patch$ln$pre_context"
494 set pre_context {}
495 } elseif {$c1 ne {-} && $c1 ne {+}} {
496 # context line
497 set ln [$ui_diff get $i_l $next_l]
498 set patch "$patch$pre_context$ln"
499 set n [expr $n+1]
500 set pre_context {}
501 } elseif {$c1 eq $to_context} {
502 # turn change line into context line
503 set ln [$ui_diff get "$i_l + 1 chars" $next_l]
504 if {$c1 eq {-}} {
505 set pre_context "$pre_context $ln"
506 } else {
507 set patch "$patch $ln"
509 set n [expr $n+1]
511 set i_l $next_l
513 set patch "@@ -$hln,$n +$hln,[eval expr $n $sign 1] @@\n$patch"
515 if {[catch {
516 set p [eval git_write $apply_cmd]
517 fconfigure $p -translation binary -encoding binary
518 puts -nonewline $p $current_diff_header
519 puts -nonewline $p $patch
520 close $p} err]} {
521 error_popup [append $failed_msg "\n\n$err"]
524 unlock_index