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