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