git gui: show diffs with a minimum of 1 context line
[git/git.git] / lib / diff.tcl
CommitLineData
f522c9b5
SP
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 {} {
699d5601 20 global file_states file_lists
f522c9b5 21 global current_diff_path current_diff_side
25b8fb1e 22 global ui_diff
f522c9b5
SP
23
24 set p $current_diff_path
25 if {$p eq {}} {
26 # No diff is being shown.
27 } elseif {$current_diff_side eq {}
28 || [catch {set s $file_states($p)}]
29 || [lsearch -sorted -exact $file_lists($current_diff_side) $p] == -1} {
30 clear_diff
31 } else {
25b8fb1e
AG
32 set save_pos [lindex [$ui_diff yview] 0]
33 show_diff $p $current_diff_side {} $save_pos
f522c9b5
SP
34 }
35}
36
37proc handle_empty_diff {} {
38 global current_diff_path file_states file_lists
39
40 set path $current_diff_path
41 set s $file_states($path)
42 if {[lindex $s 0] ne {_M}} return
43
1ac17950 44 info_popup [mc "No differences detected.
f522c9b5 45
1ac17950 46%s has no changes.
f522c9b5
SP
47
48The modification date of this file was updated by another application, but the content within the file was not changed.
49
1ac17950 50A rescan will be automatically started to find other files which may have the same state." [short_path $path]]
f522c9b5
SP
51
52 clear_diff
53 display_file $path __
699d5601 54 rescan ui_ready 0
f522c9b5
SP
55}
56
25b8fb1e 57proc show_diff {path w {lno {}} {scroll_pos {}}} {
f522c9b5
SP
58 global file_states file_lists
59 global is_3way_diff diff_active repo_config
699d5601 60 global ui_diff ui_index ui_workdir
f522c9b5
SP
61 global current_diff_path current_diff_side current_diff_header
62
63 if {$diff_active || ![lock_index read]} return
64
65 clear_diff
66 if {$lno == {}} {
67 set lno [lsearch -sorted -exact $file_lists($w) $path]
68 if {$lno >= 0} {
69 incr lno
70 }
71 }
72 if {$lno >= 1} {
73 $w tag add in_diff $lno.0 [expr {$lno + 1}].0
74 }
75
76 set s $file_states($path)
77 set m [lindex $s 0]
78 set is_3way_diff 0
79 set diff_active 1
80 set current_diff_path $path
81 set current_diff_side $w
82 set current_diff_header {}
c8c4854b 83 ui_status [mc "Loading diff of %s..." [escape_path $path]]
f522c9b5
SP
84
85 # - Git won't give us the diff, there's nothing to compare to!
86 #
87 if {$m eq {_O}} {
88 set max_sz [expr {128 * 1024}]
3b9dfde3 89 set type unknown
f522c9b5 90 if {[catch {
3b9dfde3
SP
91 set type [file type $path]
92 switch -- $type {
93 directory {
94 set type submodule
95 set content {}
96 set sz 0
97 }
98 link {
2d19f8e9
MB
99 set content [file readlink $path]
100 set sz [string length $content]
3b9dfde3
SP
101 }
102 file {
2d19f8e9
MB
103 set fd [open $path r]
104 fconfigure $fd -eofchar {}
105 set content [read $fd $max_sz]
106 close $fd
107 set sz [file size $path]
108 }
3b9dfde3
SP
109 default {
110 error "'$type' not supported"
111 }
112 }
f522c9b5
SP
113 } err ]} {
114 set diff_active 0
115 unlock_index
c8c4854b 116 ui_status [mc "Unable to display %s" [escape_path $path]]
31bb1d1b 117 error_popup [strcat [mc "Error loading file:"] "\n\n$err"]
f522c9b5
SP
118 return
119 }
120 $ui_diff conf -state normal
3b9dfde3 121 if {$type eq {submodule}} {
5f51ccd2
SP
122 $ui_diff insert end [append \
123 "* " \
124 [mc "Git Repository (subproject)"] \
125 "\n"] d_@
3b9dfde3 126 } elseif {![catch {set type [exec file $path]}]} {
f522c9b5
SP
127 set n [string length $path]
128 if {[string equal -length $n $path $type]} {
129 set type [string range $type $n end]
130 regsub {^:?\s*} $type {} type
131 }
132 $ui_diff insert end "* $type\n" d_@
133 }
134 if {[string first "\0" $content] != -1} {
135 $ui_diff insert end \
c8c4854b 136 [mc "* Binary file (not showing content)."] \
f522c9b5
SP
137 d_@
138 } else {
139 if {$sz > $max_sz} {
140 $ui_diff insert end \
141"* Untracked file is $sz bytes.
142* Showing only first $max_sz bytes.
143" d_@
144 }
145 $ui_diff insert end $content
146 if {$sz > $max_sz} {
147 $ui_diff insert end "
148* Untracked file clipped here by [appname].
149* To see the entire file, use an external editor.
150" d_@
151 }
152 }
153 $ui_diff conf -state disabled
154 set diff_active 0
155 unlock_index
25b8fb1e
AG
156 if {$scroll_pos ne {}} {
157 update
158 $ui_diff yview moveto $scroll_pos
159 }
699d5601 160 ui_ready
f522c9b5
SP
161 return
162 }
163
0b812616 164 set cmd [list]
f522c9b5
SP
165 if {$w eq $ui_index} {
166 lappend cmd diff-index
167 lappend cmd --cached
168 } elseif {$w eq $ui_workdir} {
169 if {[string index $m 0] eq {U}} {
170 lappend cmd diff
171 } else {
172 lappend cmd diff-files
173 }
174 }
175
176 lappend cmd -p
177 lappend cmd --no-color
55ba8a34 178 if {$repo_config(gui.diffcontext) >= 1} {
f522c9b5
SP
179 lappend cmd "-U$repo_config(gui.diffcontext)"
180 }
181 if {$w eq $ui_index} {
182 lappend cmd [PARENT]
183 }
184 lappend cmd --
185 lappend cmd $path
186
0b812616 187 if {[catch {set fd [eval git_read --nice $cmd]} err]} {
f522c9b5
SP
188 set diff_active 0
189 unlock_index
c8c4854b 190 ui_status [mc "Unable to display %s" [escape_path $path]]
31bb1d1b 191 error_popup [strcat [mc "Error loading diff:"] "\n\n$err"]
f522c9b5
SP
192 return
193 }
194
195 fconfigure $fd \
196 -blocking 0 \
197 -encoding binary \
198 -translation binary
25b8fb1e 199 fileevent $fd readable [list read_diff $fd $scroll_pos]
f522c9b5
SP
200}
201
25b8fb1e 202proc read_diff {fd scroll_pos} {
699d5601 203 global ui_diff diff_active
f522c9b5
SP
204 global is_3way_diff current_diff_header
205
206 $ui_diff conf -state normal
207 while {[gets $fd line] >= 0} {
208 # -- Cleanup uninteresting diff header lines.
209 #
210 if { [string match {diff --git *} $line]
211 || [string match {diff --cc *} $line]
212 || [string match {diff --combined *} $line]
213 || [string match {--- *} $line]
214 || [string match {+++ *} $line]} {
215 append current_diff_header $line "\n"
216 continue
217 }
218 if {[string match {index *} $line]} continue
219 if {$line eq {deleted file mode 120000}} {
220 set line "deleted symlink"
221 }
222
223 # -- Automatically detect if this is a 3 way diff.
224 #
225 if {[string match {@@@ *} $line]} {set is_3way_diff 1}
226
227 if {[string match {mode *} $line]
228 || [string match {new file *} $line]
a4750dd2 229 || [regexp {^(old|new) mode *} $line]
f522c9b5 230 || [string match {deleted file *} $line]
4ed1a190 231 || [string match {deleted symlink} $line]
f522c9b5
SP
232 || [string match {Binary files * and * differ} $line]
233 || $line eq {\ No newline at end of file}
234 || [regexp {^\* Unmerged path } $line]} {
235 set tags {}
236 } elseif {$is_3way_diff} {
237 set op [string range $line 0 1]
238 switch -- $op {
239 { } {set tags {}}
240 {@@} {set tags d_@}
241 { +} {set tags d_s+}
242 { -} {set tags d_s-}
243 {+ } {set tags d_+s}
244 {- } {set tags d_-s}
245 {--} {set tags d_--}
246 {++} {
247 if {[regexp {^\+\+([<>]{7} |={7})} $line _g op]} {
248 set line [string replace $line 0 1 { }]
249 set tags d$op
250 } else {
251 set tags d_++
252 }
253 }
254 default {
255 puts "error: Unhandled 3 way diff marker: {$op}"
256 set tags {}
257 }
258 }
259 } else {
260 set op [string index $line 0]
261 switch -- $op {
262 { } {set tags {}}
263 {@} {set tags d_@}
264 {-} {set tags d_-}
265 {+} {
266 if {[regexp {^\+([<>]{7} |={7})} $line _g op]} {
267 set line [string replace $line 0 0 { }]
268 set tags d$op
269 } else {
270 set tags d_+
271 }
272 }
273 default {
274 puts "error: Unhandled 2 way diff marker: {$op}"
275 set tags {}
276 }
277 }
278 }
279 $ui_diff insert end $line $tags
280 if {[string index $line end] eq "\r"} {
281 $ui_diff tag add d_cr {end - 2c}
282 }
283 $ui_diff insert end "\n" $tags
284 }
285 $ui_diff conf -state disabled
286
287 if {[eof $fd]} {
288 close $fd
289 set diff_active 0
290 unlock_index
25b8fb1e
AG
291 if {$scroll_pos ne {}} {
292 update
293 $ui_diff yview moveto $scroll_pos
294 }
699d5601 295 ui_ready
f522c9b5
SP
296
297 if {[$ui_diff index end] eq {2.0}} {
298 handle_empty_diff
299 }
300 }
301}
302
303proc apply_hunk {x y} {
304 global current_diff_path current_diff_header current_diff_side
305 global ui_diff ui_index file_states
306
307 if {$current_diff_path eq {} || $current_diff_header eq {}} return
308 if {![lock_index apply_hunk]} return
309
0b812616 310 set apply_cmd {apply --cached --whitespace=nowarn}
f522c9b5
SP
311 set mi [lindex $file_states($current_diff_path) 0]
312 if {$current_diff_side eq $ui_index} {
1ac17950 313 set failed_msg [mc "Failed to unstage selected hunk."]
f522c9b5
SP
314 lappend apply_cmd --reverse
315 if {[string index $mi 0] ne {M}} {
316 unlock_index
317 return
318 }
319 } else {
1ac17950 320 set failed_msg [mc "Failed to stage selected hunk."]
f522c9b5
SP
321 if {[string index $mi 1] ne {M}} {
322 unlock_index
323 return
324 }
325 }
326
327 set s_lno [lindex [split [$ui_diff index @$x,$y] .] 0]
328 set s_lno [$ui_diff search -backwards -regexp ^@@ $s_lno.0 0.0]
329 if {$s_lno eq {}} {
330 unlock_index
331 return
332 }
333
334 set e_lno [$ui_diff search -forwards -regexp ^@@ "$s_lno + 1 lines" end]
335 if {$e_lno eq {}} {
336 set e_lno end
337 }
338
339 if {[catch {
0b812616 340 set p [eval git_write $apply_cmd]
f522c9b5
SP
341 fconfigure $p -translation binary -encoding binary
342 puts -nonewline $p $current_diff_header
343 puts -nonewline $p [$ui_diff get $s_lno $e_lno]
344 close $p} err]} {
1ac17950 345 error_popup [append $failed_msg "\n\n$err"]
f522c9b5
SP
346 unlock_index
347 return
348 }
349
350 $ui_diff conf -state normal
351 $ui_diff delete $s_lno $e_lno
352 $ui_diff conf -state disabled
353
354 if {[$ui_diff get 1.0 end] eq "\n"} {
355 set o _
356 } else {
357 set o ?
358 }
359
360 if {$current_diff_side eq $ui_index} {
361 set mi ${o}M
362 } elseif {[string index $mi 0] eq {_}} {
363 set mi M$o
364 } else {
365 set mi ?$o
366 }
367 unlock_index
368 display_file $current_diff_path $mi
369 if {$o eq {_}} {
370 clear_diff
a41e45ea
SP
371 } else {
372 set current_diff_path $current_diff_path
f522c9b5
SP
373 }
374}
5821988f
JS
375
376proc apply_line {x y} {
377 global current_diff_path current_diff_header current_diff_side
378 global ui_diff ui_index file_states
379
380 if {$current_diff_path eq {} || $current_diff_header eq {}} return
381 if {![lock_index apply_hunk]} return
382
383 set apply_cmd {apply --cached --whitespace=nowarn}
384 set mi [lindex $file_states($current_diff_path) 0]
385 if {$current_diff_side eq $ui_index} {
386 set failed_msg [mc "Failed to unstage selected line."]
387 set to_context {+}
388 lappend apply_cmd --reverse
389 if {[string index $mi 0] ne {M}} {
390 unlock_index
391 return
392 }
393 } else {
394 set failed_msg [mc "Failed to stage selected line."]
395 set to_context {-}
396 if {[string index $mi 1] ne {M}} {
397 unlock_index
398 return
399 }
400 }
401
402 set the_l [$ui_diff index @$x,$y]
403
404 # operate only on change lines
405 set c1 [$ui_diff get "$the_l linestart"]
406 if {$c1 ne {+} && $c1 ne {-}} {
407 unlock_index
408 return
409 }
410 set sign $c1
411
412 set i_l [$ui_diff search -backwards -regexp ^@@ $the_l 0.0]
413 if {$i_l eq {}} {
414 unlock_index
415 return
416 }
417 # $i_l is now at the beginning of a line
418
419 # pick start line number from hunk header
420 set hh [$ui_diff get $i_l "$i_l + 1 lines"]
421 set hh [lindex [split $hh ,] 0]
422 set hln [lindex [split $hh -] 1]
423
c7f74570
JS
424 # There is a special situation to take care of. Consider this hunk:
425 #
426 # @@ -10,4 +10,4 @@
427 # context before
428 # -old 1
429 # -old 2
430 # +new 1
431 # +new 2
432 # context after
433 #
434 # We used to keep the context lines in the order they appear in the
435 # hunk. But then it is not possible to correctly stage only
436 # "-old 1" and "+new 1" - it would result in this staged text:
437 #
438 # context before
439 # old 2
440 # new 1
441 # context after
442 #
443 # (By symmetry it is not possible to *un*stage "old 2" and "new 2".)
444 #
445 # We resolve the problem by introducing an asymmetry, namely, when
446 # a "+" line is *staged*, it is moved in front of the context lines
447 # that are generated from the "-" lines that are immediately before
448 # the "+" block. That is, we construct this patch:
449 #
450 # @@ -10,4 +10,5 @@
451 # context before
452 # +new 1
453 # old 1
454 # old 2
455 # context after
456 #
457 # But we do *not* treat "-" lines that are *un*staged in a special
458 # way.
459 #
460 # With this asymmetry it is possible to stage the change
461 # "old 1" -> "new 1" directly, and to stage the change
462 # "old 2" -> "new 2" by first staging the entire hunk and
463 # then unstaging the change "old 1" -> "new 1".
464
465 # This is non-empty if and only if we are _staging_ changes;
466 # then it accumulates the consecutive "-" lines (after converting
467 # them to context lines) in order to be moved after the "+" change
468 # line.
469 set pre_context {}
470
5821988f
JS
471 set n 0
472 set i_l [$ui_diff index "$i_l + 1 lines"]
473 set patch {}
474 while {[$ui_diff compare $i_l < "end - 1 chars"] &&
475 [$ui_diff get $i_l "$i_l + 2 chars"] ne {@@}} {
476 set next_l [$ui_diff index "$i_l + 1 lines"]
477 set c1 [$ui_diff get $i_l]
478 if {[$ui_diff compare $i_l <= $the_l] &&
479 [$ui_diff compare $the_l < $next_l]} {
480 # the line to stage/unstage
481 set ln [$ui_diff get $i_l $next_l]
fa6b5b39
JS
482 if {$c1 eq {-}} {
483 set n [expr $n+1]
c7f74570
JS
484 set patch "$patch$pre_context$ln"
485 } else {
486 set patch "$patch$ln$pre_context"
fa6b5b39 487 }
c7f74570 488 set pre_context {}
5821988f
JS
489 } elseif {$c1 ne {-} && $c1 ne {+}} {
490 # context line
491 set ln [$ui_diff get $i_l $next_l]
c7f74570 492 set patch "$patch$pre_context$ln"
5821988f 493 set n [expr $n+1]
c7f74570 494 set pre_context {}
5821988f
JS
495 } elseif {$c1 eq $to_context} {
496 # turn change line into context line
497 set ln [$ui_diff get "$i_l + 1 chars" $next_l]
c7f74570
JS
498 if {$c1 eq {-}} {
499 set pre_context "$pre_context $ln"
500 } else {
501 set patch "$patch $ln"
502 }
5821988f
JS
503 set n [expr $n+1]
504 }
505 set i_l $next_l
506 }
507 set patch "@@ -$hln,$n +$hln,[eval expr $n $sign 1] @@\n$patch"
508
509 if {[catch {
510 set p [eval git_write $apply_cmd]
511 fconfigure $p -translation binary -encoding binary
512 puts -nonewline $p $current_diff_header
513 puts -nonewline $p $patch
514 close $p} err]} {
515 error_popup [append $failed_msg "\n\n$err"]
516 }
517
518 unlock_index
519}