]> git.ipfire.org Git - thirdparty/git.git/blame - git-gui/lib/diff.tcl
Merge https://github.com/prati0100/git-gui
[thirdparty/git.git] / git-gui / lib / diff.tcl
CommitLineData
f522c9b5
SP
1# git-gui diff viewer
2# Copyright (C) 2006, 2007 Shawn Pearce
3
a43c5f51
ML
4proc apply_tab_size {{firsttab {}}} {
5 global have_tk85 repo_config ui_diff
6
7 set w [font measure font_diff "0"]
8 if {$have_tk85 && $firsttab != 0} {
9 $ui_diff configure -tabs [list [expr {$firsttab * $w}] [expr {($firsttab + $repo_config(gui.tabsize)) * $w}]]
10 } elseif {$have_tk85 || $repo_config(gui.tabsize) != 8} {
11 $ui_diff configure -tabs [expr {$repo_config(gui.tabsize) * $w}]
12 } else {
13 $ui_diff configure -tabs {}
14 }
15}
16
f522c9b5
SP
17proc clear_diff {} {
18 global ui_diff current_diff_path current_diff_header
19 global ui_index ui_workdir
20
21 $ui_diff conf -state normal
22 $ui_diff delete 0.0 end
23 $ui_diff conf -state disabled
24
25 set current_diff_path {}
26 set current_diff_header {}
27
28 $ui_index tag remove in_diff 0.0 end
29 $ui_workdir tag remove in_diff 0.0 end
30}
31
7cf4566f 32proc reshow_diff {{after {}}} {
699d5601 33 global file_states file_lists
f522c9b5 34 global current_diff_path current_diff_side
25b8fb1e 35 global ui_diff
f522c9b5
SP
36
37 set p $current_diff_path
38 if {$p eq {}} {
39 # No diff is being shown.
29853b90 40 } elseif {$current_diff_side eq {}} {
f522c9b5 41 clear_diff
29853b90
AG
42 } elseif {[catch {set s $file_states($p)}]
43 || [lsearch -sorted -exact $file_lists($current_diff_side) $p] == -1} {
44
45 if {[find_next_diff $current_diff_side $p {} {[^O]}]} {
7cf4566f 46 next_diff $after
29853b90
AG
47 } else {
48 clear_diff
49 }
f522c9b5 50 } else {
25b8fb1e 51 set save_pos [lindex [$ui_diff yview] 0]
7cf4566f 52 show_diff $p $current_diff_side {} $save_pos $after
f522c9b5
SP
53 }
54}
55
3fe01623
AG
56proc force_diff_encoding {enc} {
57 global current_diff_path
5f0a516d 58
3fe01623
AG
59 if {$current_diff_path ne {}} {
60 force_path_encoding $current_diff_path $enc
61 reshow_diff
62 }
63}
64
f522c9b5
SP
65proc handle_empty_diff {} {
66 global current_diff_path file_states file_lists
584fa9cc 67 global diff_empty_count
f522c9b5
SP
68
69 set path $current_diff_path
70 set s $file_states($path)
1fbaccad 71 if {[lindex $s 0] ne {_M} || [has_textconv $path]} return
f522c9b5 72
584fa9cc
AG
73 # Prevent infinite rescan loops
74 incr diff_empty_count
75 if {$diff_empty_count > 1} return
76
1ac17950 77 info_popup [mc "No differences detected.
f522c9b5 78
1ac17950 79%s has no changes.
f522c9b5
SP
80
81The modification date of this file was updated by another application, but the content within the file was not changed.
82
1ac17950 83A rescan will be automatically started to find other files which may have the same state." [short_path $path]]
f522c9b5
SP
84
85 clear_diff
86 display_file $path __
699d5601 87 rescan ui_ready 0
f522c9b5
SP
88}
89
3e34838c 90proc show_diff {path w {lno {}} {scroll_pos {}} {callback {}}} {
f522c9b5 91 global file_states file_lists
3e34838c 92 global is_3way_diff is_conflict_diff diff_active repo_config
699d5601 93 global ui_diff ui_index ui_workdir
f522c9b5 94 global current_diff_path current_diff_side current_diff_header
b2ca4149 95 global current_diff_queue
f522c9b5
SP
96
97 if {$diff_active || ![lock_index read]} return
98
99 clear_diff
100 if {$lno == {}} {
101 set lno [lsearch -sorted -exact $file_lists($w) $path]
102 if {$lno >= 0} {
103 incr lno
104 }
105 }
106 if {$lno >= 1} {
107 $w tag add in_diff $lno.0 [expr {$lno + 1}].0
29853b90 108 $w see $lno.0
f522c9b5
SP
109 }
110
111 set s $file_states($path)
112 set m [lindex $s 0]
3e34838c 113 set is_conflict_diff 0
f522c9b5
SP
114 set current_diff_path $path
115 set current_diff_side $w
b2ca4149 116 set current_diff_queue {}
c8c4854b 117 ui_status [mc "Loading diff of %s..." [escape_path $path]]
f522c9b5 118
3e34838c
AG
119 set cont_info [list $scroll_pos $callback]
120
a43c5f51
ML
121 apply_tab_size 0
122
b2ca4149 123 if {[string first {U} $m] >= 0} {
3e34838c 124 merge_load_stages $path [list show_unmerged_diff $cont_info]
b2ca4149 125 } elseif {$m eq {_O}} {
3e34838c 126 show_other_diff $path $w $m $cont_info
b2ca4149 127 } else {
3e34838c 128 start_show_diff $cont_info
b2ca4149 129 }
a0a0c683
AR
130
131 global current_diff_path selected_paths
132 set selected_paths($current_diff_path) 1
b2ca4149
AG
133}
134
3e34838c 135proc show_unmerged_diff {cont_info} {
b2ca4149 136 global current_diff_path current_diff_side
3e34838c 137 global merge_stages ui_diff is_conflict_diff
b2ca4149
AG
138 global current_diff_queue
139
140 if {$merge_stages(2) eq {}} {
3e34838c 141 set is_conflict_diff 1
b2ca4149 142 lappend current_diff_queue \
c7ec31a3 143 [list [mc "LOCAL: deleted\nREMOTE:\n"] d= \
b2ca4149
AG
144 [list ":1:$current_diff_path" ":3:$current_diff_path"]]
145 } elseif {$merge_stages(3) eq {}} {
3e34838c 146 set is_conflict_diff 1
b2ca4149 147 lappend current_diff_queue \
c7ec31a3 148 [list [mc "REMOTE: deleted\nLOCAL:\n"] d= \
b2ca4149
AG
149 [list ":1:$current_diff_path" ":2:$current_diff_path"]]
150 } elseif {[lindex $merge_stages(1) 0] eq {120000}
151 || [lindex $merge_stages(2) 0] eq {120000}
152 || [lindex $merge_stages(3) 0] eq {120000}} {
3e34838c 153 set is_conflict_diff 1
b2ca4149 154 lappend current_diff_queue \
c7ec31a3 155 [list [mc "LOCAL:\n"] d= \
b2ca4149
AG
156 [list ":1:$current_diff_path" ":2:$current_diff_path"]]
157 lappend current_diff_queue \
c7ec31a3 158 [list [mc "REMOTE:\n"] d= \
b2ca4149
AG
159 [list ":1:$current_diff_path" ":3:$current_diff_path"]]
160 } else {
3e34838c 161 start_show_diff $cont_info
b2ca4149
AG
162 return
163 }
164
3e34838c 165 advance_diff_queue $cont_info
b2ca4149
AG
166}
167
3e34838c 168proc advance_diff_queue {cont_info} {
b2ca4149
AG
169 global current_diff_queue ui_diff
170
171 set item [lindex $current_diff_queue 0]
172 set current_diff_queue [lrange $current_diff_queue 1 end]
173
174 $ui_diff conf -state normal
175 $ui_diff insert end [lindex $item 0] [lindex $item 1]
176 $ui_diff conf -state disabled
177
3e34838c 178 start_show_diff $cont_info [lindex $item 2]
b2ca4149
AG
179}
180
3e34838c 181proc show_other_diff {path w m cont_info} {
b2ca4149
AG
182 global file_states file_lists
183 global is_3way_diff diff_active repo_config
184 global ui_diff ui_index ui_workdir
185 global current_diff_path current_diff_side current_diff_header
186
f522c9b5
SP
187 # - Git won't give us the diff, there's nothing to compare to!
188 #
189 if {$m eq {_O}} {
f2df8a5b 190 set max_sz 100000
3b9dfde3 191 set type unknown
f522c9b5 192 if {[catch {
3b9dfde3
SP
193 set type [file type $path]
194 switch -- $type {
195 directory {
196 set type submodule
197 set content {}
198 set sz 0
199 }
200 link {
2d19f8e9
MB
201 set content [file readlink $path]
202 set sz [string length $content]
3b9dfde3
SP
203 }
204 file {
2d19f8e9 205 set fd [open $path r]
1ffca60f
SP
206 fconfigure $fd \
207 -eofchar {} \
72e6b002 208 -encoding [get_path_encoding $path]
2d19f8e9
MB
209 set content [read $fd $max_sz]
210 close $fd
211 set sz [file size $path]
212 }
3b9dfde3
SP
213 default {
214 error "'$type' not supported"
215 }
216 }
f522c9b5
SP
217 } err ]} {
218 set diff_active 0
219 unlock_index
c8c4854b 220 ui_status [mc "Unable to display %s" [escape_path $path]]
31bb1d1b 221 error_popup [strcat [mc "Error loading file:"] "\n\n$err"]
f522c9b5
SP
222 return
223 }
224 $ui_diff conf -state normal
3b9dfde3 225 if {$type eq {submodule}} {
a3d97afa
VA
226 $ui_diff insert end \
227 "* [mc "Git Repository (subproject)"]\n" \
228 d_info
3b9dfde3 229 } elseif {![catch {set type [exec file $path]}]} {
f522c9b5
SP
230 set n [string length $path]
231 if {[string equal -length $n $path $type]} {
232 set type [string range $type $n end]
233 regsub {^:?\s*} $type {} type
234 }
88b21c2a 235 $ui_diff insert end "* $type\n" d_info
f522c9b5
SP
236 }
237 if {[string first "\0" $content] != -1} {
238 $ui_diff insert end \
c8c4854b 239 [mc "* Binary file (not showing content)."] \
88b21c2a 240 d_info
f522c9b5
SP
241 } else {
242 if {$sz > $max_sz} {
7f15b002
JS
243 $ui_diff insert end [mc \
244"* Untracked file is %d bytes.
245* Showing only first %d bytes.
88b21c2a 246" $sz $max_sz] d_info
f522c9b5
SP
247 }
248 $ui_diff insert end $content
249 if {$sz > $max_sz} {
7f15b002
JS
250 $ui_diff insert end [mc "
251* Untracked file clipped here by %s.
f522c9b5 252* To see the entire file, use an external editor.
88b21c2a 253" [appname]] d_info
f522c9b5
SP
254 }
255 }
256 $ui_diff conf -state disabled
257 set diff_active 0
258 unlock_index
3e34838c 259 set scroll_pos [lindex $cont_info 0]
25b8fb1e
AG
260 if {$scroll_pos ne {}} {
261 update
262 $ui_diff yview moveto $scroll_pos
263 }
699d5601 264 ui_ready
3e34838c
AG
265 set callback [lindex $cont_info 1]
266 if {$callback ne {}} {
267 eval $callback
268 }
f522c9b5
SP
269 return
270 }
b2ca4149
AG
271}
272
3e34838c 273proc start_show_diff {cont_info {add_opts {}}} {
b2ca4149 274 global file_states file_lists
246295bd 275 global is_3way_diff is_submodule_diff diff_active repo_config
b2ca4149
AG
276 global ui_diff ui_index ui_workdir
277 global current_diff_path current_diff_side current_diff_header
278
279 set path $current_diff_path
280 set w $current_diff_side
281
282 set s $file_states($path)
283 set m [lindex $s 0]
284 set is_3way_diff 0
246295bd 285 set is_submodule_diff 0
b2ca4149
AG
286 set diff_active 1
287 set current_diff_header {}
c3b57dc2 288 set conflict_size [gitattr $path conflict-marker-size 7]
f522c9b5 289
0b812616 290 set cmd [list]
f522c9b5
SP
291 if {$w eq $ui_index} {
292 lappend cmd diff-index
293 lappend cmd --cached
e0db1dd7
JL
294 if {[git-version >= "1.7.2"]} {
295 lappend cmd --ignore-submodules=dirty
296 }
f522c9b5 297 } elseif {$w eq $ui_workdir} {
ff515d81 298 if {[string first {U} $m] >= 0} {
f522c9b5
SP
299 lappend cmd diff
300 } else {
301 lappend cmd diff-files
302 }
303 }
1fbaccad
CP
304 if {![is_config_false gui.textconv] && [git-version >= 1.6.1]} {
305 lappend cmd --textconv
306 }
f522c9b5 307
a9ae14a1
JL
308 if {[string match {160000 *} [lindex $s 2]]
309 || [string match {160000 *} [lindex $s 3]]} {
310 set is_submodule_diff 1
311
312 if {[git-version >= "1.6.6"]} {
313 lappend cmd --submodule
314 }
315 }
316
f522c9b5 317 lappend cmd -p
8f85599a 318 lappend cmd --color
54531e7c 319 set cmd [concat $cmd $repo_config(gui.diffopts)]
55ba8a34 320 if {$repo_config(gui.diffcontext) >= 1} {
f522c9b5
SP
321 lappend cmd "-U$repo_config(gui.diffcontext)"
322 }
323 if {$w eq $ui_index} {
324 lappend cmd [PARENT]
325 }
b2ca4149
AG
326 if {$add_opts ne {}} {
327 eval lappend cmd $add_opts
328 } else {
329 lappend cmd --
330 lappend cmd $path
331 }
f522c9b5 332
a9ae14a1 333 if {$is_submodule_diff && [git-version < "1.6.6"]} {
af413de4 334 if {$w eq $ui_index} {
118d9388 335 set cmd [list submodule summary --cached -- $path]
af413de4 336 } else {
118d9388 337 set cmd [list submodule summary --files -- $path]
af413de4 338 }
246295bd
JL
339 }
340
0b812616 341 if {[catch {set fd [eval git_read --nice $cmd]} err]} {
f522c9b5
SP
342 set diff_active 0
343 unlock_index
c8c4854b 344 ui_status [mc "Unable to display %s" [escape_path $path]]
31bb1d1b 345 error_popup [strcat [mc "Error loading diff:"] "\n\n$err"]
f522c9b5
SP
346 return
347 }
348
ca53c3fd 349 set ::current_diff_inheader 1
b436825b
BW
350 # Detect pre-image lines of the diff3 conflict-style. They are just
351 # '++' lines which is not bijective. Thus, we need to maintain a state
352 # across lines.
353 set ::conflict_in_pre_image 0
f522c9b5
SP
354 fconfigure $fd \
355 -blocking 0 \
72e6b002 356 -encoding [get_path_encoding $path] \
1ffca60f 357 -translation lf
4590307a 358 fileevent $fd readable [list read_diff $fd $conflict_size $cont_info]
f522c9b5
SP
359}
360
8f85599a
PT
361proc parse_color_line {line} {
362 set start 0
363 set result ""
364 set markup [list]
365 set regexp {\033\[((?:\d+;)*\d+)?m}
46a0431b 366 set need_reset 0
8f85599a
PT
367 while {[regexp -indices -start $start $regexp $line match code]} {
368 foreach {begin end} $match break
369 append result [string range $line $start [expr {$begin - 1}]]
46a0431b
BW
370 set pos [string length $result]
371 set col [eval [linsert $code 0 string range $line]]
8f85599a 372 set start [incr end]
46a0431b
BW
373 if {$col eq "0" || $col eq ""} {
374 if {!$need_reset} continue
375 set need_reset 0
376 } else {
377 set need_reset 1
378 }
379 lappend markup $pos $col
8f85599a
PT
380 }
381 append result [string range $line $start end]
382 if {[llength $markup] < 4} {set markup {}}
383 return [list $result $markup]
384}
385
4590307a 386proc read_diff {fd conflict_size cont_info} {
246295bd 387 global ui_diff diff_active is_submodule_diff
3e34838c 388 global is_3way_diff is_conflict_diff current_diff_header
b2ca4149 389 global current_diff_queue
584fa9cc 390 global diff_empty_count
f522c9b5
SP
391
392 $ui_diff conf -state normal
393 while {[gets $fd line] >= 0} {
8f85599a
PT
394 foreach {line markup} [parse_color_line $line] break
395 set line [string map {\033 ^} $line]
396
6459d7c0
BW
397 set tags {}
398
c976bbff
BW
399 # -- Check for start of diff header.
400 if { [string match {diff --git *} $line]
401 || [string match {diff --cc *} $line]
402 || [string match {diff --combined *} $line]} {
403 set ::current_diff_inheader 1
404 }
405
406 # -- Check for end of diff header (any hunk line will do this).
f522c9b5 407 #
c976bbff
BW
408 if {[regexp {^@@+ } $line]} {set ::current_diff_inheader 0}
409
963ceab5
BW
410 # -- Automatically detect if this is a 3 way diff.
411 #
a43c5f51
ML
412 if {[string match {@@@ *} $line]} {
413 set is_3way_diff 1
414 apply_tab_size 1
415 }
963ceab5 416
ca53c3fd 417 if {$::current_diff_inheader} {
d1c7f8aa
BW
418
419 # -- These two lines stop a diff header and shouldn't be in there
420 if { [string match {Binary files * and * differ} $line]
421 || [regexp {^\* Unmerged path } $line]} {
422 set ::current_diff_inheader 0
423 } else {
424 append current_diff_header $line "\n"
425 }
c976bbff
BW
426
427 # -- Cleanup uninteresting diff header lines.
428 #
ca53c3fd
SP
429 if { [string match {diff --git *} $line]
430 || [string match {diff --cc *} $line]
431 || [string match {diff --combined *} $line]
432 || [string match {--- *} $line]
ebd143ff
BW
433 || [string match {+++ *} $line]
434 || [string match {index *} $line]} {
ca53c3fd
SP
435 continue
436 }
c976bbff 437
97b8ee16
BW
438 # -- Name it symlink, not 120000
439 # Note, that the original line is in $current_diff_header
440 regsub {^(deleted|new) file mode 120000} $line {\1 symlink} line
f522c9b5 441
bf594398
BW
442 } elseif { $line eq {\ No newline at end of file}} {
443 # -- Handle some special lines
f522c9b5
SP
444 } elseif {$is_3way_diff} {
445 set op [string range $line 0 1]
446 switch -- $op {
447 { } {set tags {}}
448 {@@} {set tags d_@}
449 { +} {set tags d_s+}
450 { -} {set tags d_s-}
451 {+ } {set tags d_+s}
452 {- } {set tags d_-s}
453 {--} {set tags d_--}
454 {++} {
4590307a 455 set regexp [string map [list %conflict_size $conflict_size]\
b436825b 456 {^\+\+([<>=|]){%conflict_size}(?: |$)}]
4590307a 457 if {[regexp $regexp $line _g op]} {
3e34838c 458 set is_conflict_diff 1
f522c9b5
SP
459 set line [string replace $line 0 1 { }]
460 set tags d$op
b436825b
BW
461
462 # The ||| conflict-marker marks the start of the pre-image.
463 # All those lines are also prefixed with '++'. Thus we need
464 # to maintain this state.
465 set ::conflict_in_pre_image [expr {$op eq {|}}]
466 } elseif {$::conflict_in_pre_image} {
467 # This is a pre-image line. It is the one which both sides
468 # are based on. As it has also the '++' line start, it is
469 # normally shown as 'added'. Invert this to '--' to make
470 # it a 'removed' line.
471 set line [string replace $line 0 1 {--}]
472 set tags d_--
f522c9b5
SP
473 } else {
474 set tags d_++
475 }
476 }
477 default {
478 puts "error: Unhandled 3 way diff marker: {$op}"
479 set tags {}
480 }
481 }
246295bd
JL
482 } elseif {$is_submodule_diff} {
483 if {$line == ""} continue
a9ae14a1 484 if {[regexp {^Submodule } $line]} {
88b21c2a 485 set tags d_info
a9ae14a1 486 } elseif {[regexp {^\* } $line]} {
246295bd 487 set line [string replace $line 0 1 {Submodule }]
88b21c2a 488 set tags d_info
246295bd
JL
489 } else {
490 set op [string range $line 0 2]
491 switch -- $op {
492 { <} {set tags d_-}
493 { >} {set tags d_+}
494 { W} {set tags {}}
495 default {
496 puts "error: Unhandled submodule diff marker: {$op}"
497 set tags {}
498 }
499 }
500 }
f522c9b5
SP
501 } else {
502 set op [string index $line 0]
503 switch -- $op {
504 { } {set tags {}}
505 {@} {set tags d_@}
506 {-} {set tags d_-}
507 {+} {
4590307a
BW
508 set regexp [string map [list %conflict_size $conflict_size]\
509 {^\+([<>=]){%conflict_size}(?: |$)}]
510 if {[regexp $regexp $line _g op]} {
3e34838c 511 set is_conflict_diff 1
f522c9b5
SP
512 set tags d$op
513 } else {
514 set tags d_+
515 }
516 }
517 default {
518 puts "error: Unhandled 2 way diff marker: {$op}"
519 set tags {}
520 }
521 }
522 }
8f85599a 523 set mark [$ui_diff index "end - 1 line linestart"]
f522c9b5
SP
524 $ui_diff insert end $line $tags
525 if {[string index $line end] eq "\r"} {
526 $ui_diff tag add d_cr {end - 2c}
527 }
528 $ui_diff insert end "\n" $tags
8f85599a
PT
529
530 foreach {posbegin colbegin posend colend} $markup {
531 set prefix clr
3f2fb173 532 foreach style [lsort -integer [split $colbegin ";"]] {
8f85599a 533 if {$style eq "7"} {append prefix i; continue}
9af6413b 534 if {$style != 4 && ($style < 30 || $style > 47)} {continue}
8f85599a
PT
535 set a "$mark linestart + $posbegin chars"
536 set b "$mark linestart + $posend chars"
537 catch {$ui_diff tag add $prefix$style $a $b}
538 }
539 }
f522c9b5
SP
540 }
541 $ui_diff conf -state disabled
542
543 if {[eof $fd]} {
544 close $fd
b2ca4149
AG
545
546 if {$current_diff_queue ne {}} {
3e34838c 547 advance_diff_queue $cont_info
b2ca4149
AG
548 return
549 }
550
f522c9b5
SP
551 set diff_active 0
552 unlock_index
3e34838c 553 set scroll_pos [lindex $cont_info 0]
25b8fb1e
AG
554 if {$scroll_pos ne {}} {
555 update
556 $ui_diff yview moveto $scroll_pos
557 }
699d5601 558 ui_ready
f522c9b5
SP
559
560 if {[$ui_diff index end] eq {2.0}} {
561 handle_empty_diff
584fa9cc
AG
562 } else {
563 set diff_empty_count 0
f522c9b5 564 }
584fa9cc 565
3e34838c
AG
566 set callback [lindex $cont_info 1]
567 if {$callback ne {}} {
568 eval $callback
569 }
f522c9b5
SP
570 }
571}
572
62bd9993 573proc apply_or_revert_hunk {x y revert} {
f522c9b5 574 global current_diff_path current_diff_header current_diff_side
a4fa2f0a 575 global ui_diff ui_index file_states last_revert last_revert_enc
f522c9b5
SP
576
577 if {$current_diff_path eq {} || $current_diff_header eq {}} return
578 if {![lock_index apply_hunk]} return
579
62bd9993 580 set apply_cmd {apply --whitespace=nowarn}
f522c9b5
SP
581 set mi [lindex $file_states($current_diff_path) 0]
582 if {$current_diff_side eq $ui_index} {
1ac17950 583 set failed_msg [mc "Failed to unstage selected hunk."]
62bd9993 584 lappend apply_cmd --reverse --cached
f522c9b5
SP
585 if {[string index $mi 0] ne {M}} {
586 unlock_index
587 return
588 }
589 } else {
62bd9993
PY
590 if {$revert} {
591 set failed_msg [mc "Failed to revert selected hunk."]
592 lappend apply_cmd --reverse
593 } else {
594 set failed_msg [mc "Failed to stage selected hunk."]
595 lappend apply_cmd --cached
596 }
597
f522c9b5
SP
598 if {[string index $mi 1] ne {M}} {
599 unlock_index
600 return
601 }
602 }
603
604 set s_lno [lindex [split [$ui_diff index @$x,$y] .] 0]
605 set s_lno [$ui_diff search -backwards -regexp ^@@ $s_lno.0 0.0]
606 if {$s_lno eq {}} {
607 unlock_index
608 return
609 }
610
611 set e_lno [$ui_diff search -forwards -regexp ^@@ "$s_lno + 1 lines" end]
612 if {$e_lno eq {}} {
613 set e_lno end
614 }
615
a4fa2f0a
PY
616 set wholepatch "$current_diff_header[$ui_diff get $s_lno $e_lno]"
617
f522c9b5 618 if {[catch {
72e6b002 619 set enc [get_path_encoding $current_diff_path]
0b812616 620 set p [eval git_write $apply_cmd]
72e6b002 621 fconfigure $p -translation binary -encoding $enc
a4fa2f0a 622 puts -nonewline $p $wholepatch
f522c9b5 623 close $p} err]} {
a3d97afa 624 error_popup "$failed_msg\n\n$err"
f522c9b5
SP
625 unlock_index
626 return
627 }
628
a4fa2f0a
PY
629 if {$revert} {
630 # Save a copy of this patch for undoing reverts.
631 set last_revert $wholepatch
632 set last_revert_enc $enc
633 }
634
f522c9b5
SP
635 $ui_diff conf -state normal
636 $ui_diff delete $s_lno $e_lno
637 $ui_diff conf -state disabled
638
62bd9993 639 # Check if the hunk was the last one in the file.
f522c9b5
SP
640 if {[$ui_diff get 1.0 end] eq "\n"} {
641 set o _
642 } else {
643 set o ?
644 }
645
62bd9993
PY
646 # Update the status flags.
647 if {$revert} {
648 set mi [string index $mi 0]$o
649 } elseif {$current_diff_side eq $ui_index} {
f522c9b5
SP
650 set mi ${o}M
651 } elseif {[string index $mi 0] eq {_}} {
652 set mi M$o
653 } else {
654 set mi ?$o
655 }
656 unlock_index
657 display_file $current_diff_path $mi
29853b90 658 # This should trigger shift to the next changed file
f522c9b5 659 if {$o eq {_}} {
29853b90 660 reshow_diff
f522c9b5
SP
661 }
662}
5821988f 663
5f0a516d 664proc apply_or_revert_range_or_line {x y revert} {
5821988f 665 global current_diff_path current_diff_header current_diff_side
a4fa2f0a 666 global ui_diff ui_index file_states last_revert
5821988f 667
ff07c3b6
JE
668 set selected [$ui_diff tag nextrange sel 0.0]
669
670 if {$selected == {}} {
671 set first [$ui_diff index "@$x,$y"]
672 set last $first
673 } else {
674 set first [lindex $selected 0]
675 set last [lindex $selected 1]
676 }
677
678 set first_l [$ui_diff index "$first linestart"]
679 set last_l [$ui_diff index "$last lineend"]
680
5821988f
JS
681 if {$current_diff_path eq {} || $current_diff_header eq {}} return
682 if {![lock_index apply_hunk]} return
683
5f0a516d 684 set apply_cmd {apply --whitespace=nowarn}
5821988f
JS
685 set mi [lindex $file_states($current_diff_path) 0]
686 if {$current_diff_side eq $ui_index} {
687 set failed_msg [mc "Failed to unstage selected line."]
688 set to_context {+}
5f0a516d 689 lappend apply_cmd --reverse --cached
5821988f
JS
690 if {[string index $mi 0] ne {M}} {
691 unlock_index
692 return
693 }
694 } else {
5f0a516d
PY
695 if {$revert} {
696 set failed_msg [mc "Failed to revert selected line."]
697 set to_context {+}
698 lappend apply_cmd --reverse
699 } else {
700 set failed_msg [mc "Failed to stage selected line."]
701 set to_context {-}
702 lappend apply_cmd --cached
703 }
704
5821988f
JS
705 if {[string index $mi 1] ne {M}} {
706 unlock_index
707 return
708 }
709 }
710
ff07c3b6 711 set wholepatch {}
5821988f 712
ff07c3b6
JE
713 while {$first_l < $last_l} {
714 set i_l [$ui_diff search -backwards -regexp ^@@ $first_l 0.0]
715 if {$i_l eq {}} {
716 # If there's not a @@ above, then the selected range
717 # must have come before the first_l @@
718 set i_l [$ui_diff search -regexp ^@@ $first_l $last_l]
719 }
720 if {$i_l eq {}} {
721 unlock_index
722 return
723 }
724 # $i_l is now at the beginning of a line
5821988f 725
ff07c3b6
JE
726 # pick start line number from hunk header
727 set hh [$ui_diff get $i_l "$i_l + 1 lines"]
728 set hh [lindex [split $hh ,] 0]
729 set hln [lindex [split $hh -] 1]
6d02c1e2 730 set hln [lindex [split $hln " "] 0]
5821988f 731
ff07c3b6
JE
732 # There is a special situation to take care of. Consider this
733 # hunk:
734 #
735 # @@ -10,4 +10,4 @@
736 # context before
737 # -old 1
738 # -old 2
739 # +new 1
740 # +new 2
741 # context after
742 #
743 # We used to keep the context lines in the order they appear in
744 # the hunk. But then it is not possible to correctly stage only
745 # "-old 1" and "+new 1" - it would result in this staged text:
746 #
747 # context before
748 # old 2
749 # new 1
750 # context after
751 #
752 # (By symmetry it is not possible to *un*stage "old 2" and "new
753 # 2".)
754 #
755 # We resolve the problem by introducing an asymmetry, namely,
756 # when a "+" line is *staged*, it is moved in front of the
757 # context lines that are generated from the "-" lines that are
758 # immediately before the "+" block. That is, we construct this
759 # patch:
760 #
761 # @@ -10,4 +10,5 @@
762 # context before
763 # +new 1
764 # old 1
765 # old 2
766 # context after
767 #
768 # But we do *not* treat "-" lines that are *un*staged in a
769 # special way.
770 #
771 # With this asymmetry it is possible to stage the change "old
772 # 1" -> "new 1" directly, and to stage the change "old 2" ->
773 # "new 2" by first staging the entire hunk and then unstaging
774 # the change "old 1" -> "new 1".
775 #
776 # Applying multiple lines adds complexity to the special
777 # situation. The pre_context must be moved after the entire
778 # first block of consecutive staged "+" lines, so that
779 # staging both additions gives the following patch:
780 #
781 # @@ -10,4 +10,6 @@
782 # context before
783 # +new 1
784 # +new 2
785 # old 1
786 # old 2
787 # context after
788
789 # This is non-empty if and only if we are _staging_ changes;
790 # then it accumulates the consecutive "-" lines (after
791 # converting them to context lines) in order to be moved after
792 # "+" change lines.
793 set pre_context {}
794
795 set n 0
796 set m 0
797 set i_l [$ui_diff index "$i_l + 1 lines"]
798 set patch {}
799 while {[$ui_diff compare $i_l < "end - 1 chars"] &&
800 [$ui_diff get $i_l "$i_l + 2 chars"] ne {@@}} {
801 set next_l [$ui_diff index "$i_l + 1 lines"]
802 set c1 [$ui_diff get $i_l]
803 if {[$ui_diff compare $first_l <= $i_l] &&
804 [$ui_diff compare $i_l < $last_l] &&
805 ($c1 eq {-} || $c1 eq {+})} {
806 # a line to stage/unstage
807 set ln [$ui_diff get $i_l $next_l]
808 if {$c1 eq {-}} {
809 set n [expr $n+1]
810 set patch "$patch$pre_context$ln"
811 set pre_context {}
812 } else {
813 set m [expr $m+1]
814 set patch "$patch$ln"
815 }
816 } elseif {$c1 ne {-} && $c1 ne {+}} {
817 # context line
818 set ln [$ui_diff get $i_l $next_l]
c7f74570 819 set patch "$patch$pre_context$ln"
1fcd24d0
HV
820 # Skip the "\ No newline at end of
821 # file". Depending on the locale setting
822 # we don't know what this line looks
823 # like exactly. The only thing we do
824 # know is that it starts with "\ "
825 if {![string match {\\ *} $ln]} {
826 set n [expr $n+1]
827 set m [expr $m+1]
828 }
ff07c3b6
JE
829 set pre_context {}
830 } elseif {$c1 eq $to_context} {
831 # turn change line into context line
832 set ln [$ui_diff get "$i_l + 1 chars" $next_l]
833 if {$c1 eq {-}} {
834 set pre_context "$pre_context $ln"
835 } else {
836 set patch "$patch $ln"
837 }
838 set n [expr $n+1]
839 set m [expr $m+1]
c7f74570 840 } else {
ff07c3b6
JE
841 # a change in the opposite direction of
842 # to_context which is outside the range of
843 # lines to apply.
844 set patch "$patch$pre_context"
845 set pre_context {}
c7f74570 846 }
ff07c3b6 847 set i_l $next_l
5821988f 848 }
ff07c3b6
JE
849 set patch "$patch$pre_context"
850 set wholepatch "$wholepatch@@ -$hln,$n +$hln,$m @@\n$patch"
851 set first_l [$ui_diff index "$next_l + 1 lines"]
5821988f 852 }
5821988f
JS
853
854 if {[catch {
72e6b002 855 set enc [get_path_encoding $current_diff_path]
5821988f 856 set p [eval git_write $apply_cmd]
72e6b002 857 fconfigure $p -translation binary -encoding $enc
5821988f 858 puts -nonewline $p $current_diff_header
ff07c3b6 859 puts -nonewline $p $wholepatch
5821988f 860 close $p} err]} {
a3d97afa 861 error_popup "$failed_msg\n\n$err"
2ccdfb1c
PY
862 unlock_index
863 return
5821988f
JS
864 }
865
a4fa2f0a
PY
866 if {$revert} {
867 # Save a copy of this patch for undoing reverts.
868 set last_revert $current_diff_header$wholepatch
869 set last_revert_enc $enc
870 }
871
872 unlock_index
873}
874
875# Undo the last line/hunk reverted. When hunks and lines are reverted, a copy
876# of the diff applied is saved. Re-apply that diff to undo the revert.
877#
878# Right now, we only use a single variable to hold the copy, and not a
879# stack/deque for simplicity, so multiple undos are not possible. Maybe this
880# can be added if the need for something like this is felt in the future.
881proc undo_last_revert {} {
882 global last_revert current_diff_path current_diff_header
883 global last_revert_enc
884
885 if {$last_revert eq {}} return
886 if {![lock_index apply_hunk]} return
887
888 set apply_cmd {apply --whitespace=nowarn}
889 set failed_msg [mc "Failed to undo last revert."]
890
891 if {[catch {
892 set enc $last_revert_enc
893 set p [eval git_write $apply_cmd]
894 fconfigure $p -translation binary -encoding $enc
895 puts -nonewline $p $last_revert
896 close $p} err]} {
897 error_popup "$failed_msg\n\n$err"
898 unlock_index
899 return
900 }
901
902 set last_revert {}
903
5821988f
JS
904 unlock_index
905}