]> git.ipfire.org Git - thirdparty/git.git/blame - git-gui/lib/diff.tcl
Merge branch 'bw/format-patch-o-create-leading-dirs'
[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
4590307a
BW
273proc get_conflict_marker_size {path} {
274 set size 7
275 catch {
276 set fd_rc [eval [list git_read check-attr "conflict-marker-size" -- $path]]
277 set ret [gets $fd_rc line]
278 close $fd_rc
279 if {$ret > 0} {
280 regexp {.*: conflict-marker-size: (\d+)$} $line line size
281 }
282 }
283 return $size
284}
285
3e34838c 286proc start_show_diff {cont_info {add_opts {}}} {
b2ca4149 287 global file_states file_lists
246295bd 288 global is_3way_diff is_submodule_diff diff_active repo_config
b2ca4149
AG
289 global ui_diff ui_index ui_workdir
290 global current_diff_path current_diff_side current_diff_header
291
292 set path $current_diff_path
293 set w $current_diff_side
294
295 set s $file_states($path)
296 set m [lindex $s 0]
297 set is_3way_diff 0
246295bd 298 set is_submodule_diff 0
b2ca4149
AG
299 set diff_active 1
300 set current_diff_header {}
4590307a 301 set conflict_size [get_conflict_marker_size $path]
f522c9b5 302
0b812616 303 set cmd [list]
f522c9b5
SP
304 if {$w eq $ui_index} {
305 lappend cmd diff-index
306 lappend cmd --cached
e0db1dd7
JL
307 if {[git-version >= "1.7.2"]} {
308 lappend cmd --ignore-submodules=dirty
309 }
f522c9b5 310 } elseif {$w eq $ui_workdir} {
ff515d81 311 if {[string first {U} $m] >= 0} {
f522c9b5
SP
312 lappend cmd diff
313 } else {
314 lappend cmd diff-files
315 }
316 }
1fbaccad
CP
317 if {![is_config_false gui.textconv] && [git-version >= 1.6.1]} {
318 lappend cmd --textconv
319 }
f522c9b5 320
a9ae14a1
JL
321 if {[string match {160000 *} [lindex $s 2]]
322 || [string match {160000 *} [lindex $s 3]]} {
323 set is_submodule_diff 1
324
325 if {[git-version >= "1.6.6"]} {
326 lappend cmd --submodule
327 }
328 }
329
f522c9b5 330 lappend cmd -p
8f85599a 331 lappend cmd --color
54531e7c 332 set cmd [concat $cmd $repo_config(gui.diffopts)]
55ba8a34 333 if {$repo_config(gui.diffcontext) >= 1} {
f522c9b5
SP
334 lappend cmd "-U$repo_config(gui.diffcontext)"
335 }
336 if {$w eq $ui_index} {
337 lappend cmd [PARENT]
338 }
b2ca4149
AG
339 if {$add_opts ne {}} {
340 eval lappend cmd $add_opts
341 } else {
342 lappend cmd --
343 lappend cmd $path
344 }
f522c9b5 345
a9ae14a1 346 if {$is_submodule_diff && [git-version < "1.6.6"]} {
af413de4 347 if {$w eq $ui_index} {
118d9388 348 set cmd [list submodule summary --cached -- $path]
af413de4 349 } else {
118d9388 350 set cmd [list submodule summary --files -- $path]
af413de4 351 }
246295bd
JL
352 }
353
0b812616 354 if {[catch {set fd [eval git_read --nice $cmd]} err]} {
f522c9b5
SP
355 set diff_active 0
356 unlock_index
c8c4854b 357 ui_status [mc "Unable to display %s" [escape_path $path]]
31bb1d1b 358 error_popup [strcat [mc "Error loading diff:"] "\n\n$err"]
f522c9b5
SP
359 return
360 }
361
ca53c3fd 362 set ::current_diff_inheader 1
f522c9b5
SP
363 fconfigure $fd \
364 -blocking 0 \
72e6b002 365 -encoding [get_path_encoding $path] \
1ffca60f 366 -translation lf
4590307a 367 fileevent $fd readable [list read_diff $fd $conflict_size $cont_info]
f522c9b5
SP
368}
369
8f85599a
PT
370proc parse_color_line {line} {
371 set start 0
372 set result ""
373 set markup [list]
374 set regexp {\033\[((?:\d+;)*\d+)?m}
46a0431b 375 set need_reset 0
8f85599a
PT
376 while {[regexp -indices -start $start $regexp $line match code]} {
377 foreach {begin end} $match break
378 append result [string range $line $start [expr {$begin - 1}]]
46a0431b
BW
379 set pos [string length $result]
380 set col [eval [linsert $code 0 string range $line]]
8f85599a 381 set start [incr end]
46a0431b
BW
382 if {$col eq "0" || $col eq ""} {
383 if {!$need_reset} continue
384 set need_reset 0
385 } else {
386 set need_reset 1
387 }
388 lappend markup $pos $col
8f85599a
PT
389 }
390 append result [string range $line $start end]
391 if {[llength $markup] < 4} {set markup {}}
392 return [list $result $markup]
393}
394
4590307a 395proc read_diff {fd conflict_size cont_info} {
246295bd 396 global ui_diff diff_active is_submodule_diff
3e34838c 397 global is_3way_diff is_conflict_diff current_diff_header
b2ca4149 398 global current_diff_queue
584fa9cc 399 global diff_empty_count
f522c9b5
SP
400
401 $ui_diff conf -state normal
402 while {[gets $fd line] >= 0} {
8f85599a
PT
403 foreach {line markup} [parse_color_line $line] break
404 set line [string map {\033 ^} $line]
405
6459d7c0
BW
406 set tags {}
407
c976bbff
BW
408 # -- Check for start of diff header.
409 if { [string match {diff --git *} $line]
410 || [string match {diff --cc *} $line]
411 || [string match {diff --combined *} $line]} {
412 set ::current_diff_inheader 1
413 }
414
415 # -- Check for end of diff header (any hunk line will do this).
f522c9b5 416 #
c976bbff
BW
417 if {[regexp {^@@+ } $line]} {set ::current_diff_inheader 0}
418
963ceab5
BW
419 # -- Automatically detect if this is a 3 way diff.
420 #
a43c5f51
ML
421 if {[string match {@@@ *} $line]} {
422 set is_3way_diff 1
423 apply_tab_size 1
424 }
963ceab5 425
ca53c3fd 426 if {$::current_diff_inheader} {
d1c7f8aa
BW
427
428 # -- These two lines stop a diff header and shouldn't be in there
429 if { [string match {Binary files * and * differ} $line]
430 || [regexp {^\* Unmerged path } $line]} {
431 set ::current_diff_inheader 0
432 } else {
433 append current_diff_header $line "\n"
434 }
c976bbff
BW
435
436 # -- Cleanup uninteresting diff header lines.
437 #
ca53c3fd
SP
438 if { [string match {diff --git *} $line]
439 || [string match {diff --cc *} $line]
440 || [string match {diff --combined *} $line]
441 || [string match {--- *} $line]
ebd143ff
BW
442 || [string match {+++ *} $line]
443 || [string match {index *} $line]} {
ca53c3fd
SP
444 continue
445 }
c976bbff 446
97b8ee16
BW
447 # -- Name it symlink, not 120000
448 # Note, that the original line is in $current_diff_header
449 regsub {^(deleted|new) file mode 120000} $line {\1 symlink} line
f522c9b5 450
bf594398
BW
451 } elseif { $line eq {\ No newline at end of file}} {
452 # -- Handle some special lines
f522c9b5
SP
453 } elseif {$is_3way_diff} {
454 set op [string range $line 0 1]
455 switch -- $op {
456 { } {set tags {}}
457 {@@} {set tags d_@}
458 { +} {set tags d_s+}
459 { -} {set tags d_s-}
460 {+ } {set tags d_+s}
461 {- } {set tags d_-s}
462 {--} {set tags d_--}
463 {++} {
4590307a
BW
464 set regexp [string map [list %conflict_size $conflict_size]\
465 {^\+\+([<>=]){%conflict_size}(?: |$)}]
466 if {[regexp $regexp $line _g op]} {
3e34838c 467 set is_conflict_diff 1
f522c9b5
SP
468 set line [string replace $line 0 1 { }]
469 set tags d$op
470 } else {
471 set tags d_++
472 }
473 }
474 default {
475 puts "error: Unhandled 3 way diff marker: {$op}"
476 set tags {}
477 }
478 }
246295bd
JL
479 } elseif {$is_submodule_diff} {
480 if {$line == ""} continue
a9ae14a1 481 if {[regexp {^Submodule } $line]} {
88b21c2a 482 set tags d_info
a9ae14a1 483 } elseif {[regexp {^\* } $line]} {
246295bd 484 set line [string replace $line 0 1 {Submodule }]
88b21c2a 485 set tags d_info
246295bd
JL
486 } else {
487 set op [string range $line 0 2]
488 switch -- $op {
489 { <} {set tags d_-}
490 { >} {set tags d_+}
491 { W} {set tags {}}
492 default {
493 puts "error: Unhandled submodule diff marker: {$op}"
494 set tags {}
495 }
496 }
497 }
f522c9b5
SP
498 } else {
499 set op [string index $line 0]
500 switch -- $op {
501 { } {set tags {}}
502 {@} {set tags d_@}
503 {-} {set tags d_-}
504 {+} {
4590307a
BW
505 set regexp [string map [list %conflict_size $conflict_size]\
506 {^\+([<>=]){%conflict_size}(?: |$)}]
507 if {[regexp $regexp $line _g op]} {
3e34838c 508 set is_conflict_diff 1
f522c9b5
SP
509 set tags d$op
510 } else {
511 set tags d_+
512 }
513 }
514 default {
515 puts "error: Unhandled 2 way diff marker: {$op}"
516 set tags {}
517 }
518 }
519 }
8f85599a 520 set mark [$ui_diff index "end - 1 line linestart"]
f522c9b5
SP
521 $ui_diff insert end $line $tags
522 if {[string index $line end] eq "\r"} {
523 $ui_diff tag add d_cr {end - 2c}
524 }
525 $ui_diff insert end "\n" $tags
8f85599a
PT
526
527 foreach {posbegin colbegin posend colend} $markup {
528 set prefix clr
3f2fb173 529 foreach style [lsort -integer [split $colbegin ";"]] {
8f85599a 530 if {$style eq "7"} {append prefix i; continue}
9af6413b 531 if {$style != 4 && ($style < 30 || $style > 47)} {continue}
8f85599a
PT
532 set a "$mark linestart + $posbegin chars"
533 set b "$mark linestart + $posend chars"
534 catch {$ui_diff tag add $prefix$style $a $b}
535 }
536 }
f522c9b5
SP
537 }
538 $ui_diff conf -state disabled
539
540 if {[eof $fd]} {
541 close $fd
b2ca4149
AG
542
543 if {$current_diff_queue ne {}} {
3e34838c 544 advance_diff_queue $cont_info
b2ca4149
AG
545 return
546 }
547
f522c9b5
SP
548 set diff_active 0
549 unlock_index
3e34838c 550 set scroll_pos [lindex $cont_info 0]
25b8fb1e
AG
551 if {$scroll_pos ne {}} {
552 update
553 $ui_diff yview moveto $scroll_pos
554 }
699d5601 555 ui_ready
f522c9b5
SP
556
557 if {[$ui_diff index end] eq {2.0}} {
558 handle_empty_diff
584fa9cc
AG
559 } else {
560 set diff_empty_count 0
f522c9b5 561 }
584fa9cc 562
3e34838c
AG
563 set callback [lindex $cont_info 1]
564 if {$callback ne {}} {
565 eval $callback
566 }
f522c9b5
SP
567 }
568}
569
62bd9993 570proc apply_or_revert_hunk {x y revert} {
f522c9b5 571 global current_diff_path current_diff_header current_diff_side
a4fa2f0a 572 global ui_diff ui_index file_states last_revert last_revert_enc
f522c9b5
SP
573
574 if {$current_diff_path eq {} || $current_diff_header eq {}} return
575 if {![lock_index apply_hunk]} return
576
62bd9993 577 set apply_cmd {apply --whitespace=nowarn}
f522c9b5
SP
578 set mi [lindex $file_states($current_diff_path) 0]
579 if {$current_diff_side eq $ui_index} {
1ac17950 580 set failed_msg [mc "Failed to unstage selected hunk."]
62bd9993 581 lappend apply_cmd --reverse --cached
f522c9b5
SP
582 if {[string index $mi 0] ne {M}} {
583 unlock_index
584 return
585 }
586 } else {
62bd9993
PY
587 if {$revert} {
588 set failed_msg [mc "Failed to revert selected hunk."]
589 lappend apply_cmd --reverse
590 } else {
591 set failed_msg [mc "Failed to stage selected hunk."]
592 lappend apply_cmd --cached
593 }
594
f522c9b5
SP
595 if {[string index $mi 1] ne {M}} {
596 unlock_index
597 return
598 }
599 }
600
601 set s_lno [lindex [split [$ui_diff index @$x,$y] .] 0]
602 set s_lno [$ui_diff search -backwards -regexp ^@@ $s_lno.0 0.0]
603 if {$s_lno eq {}} {
604 unlock_index
605 return
606 }
607
608 set e_lno [$ui_diff search -forwards -regexp ^@@ "$s_lno + 1 lines" end]
609 if {$e_lno eq {}} {
610 set e_lno end
611 }
612
a4fa2f0a
PY
613 set wholepatch "$current_diff_header[$ui_diff get $s_lno $e_lno]"
614
f522c9b5 615 if {[catch {
72e6b002 616 set enc [get_path_encoding $current_diff_path]
0b812616 617 set p [eval git_write $apply_cmd]
72e6b002 618 fconfigure $p -translation binary -encoding $enc
a4fa2f0a 619 puts -nonewline $p $wholepatch
f522c9b5 620 close $p} err]} {
a3d97afa 621 error_popup "$failed_msg\n\n$err"
f522c9b5
SP
622 unlock_index
623 return
624 }
625
a4fa2f0a
PY
626 if {$revert} {
627 # Save a copy of this patch for undoing reverts.
628 set last_revert $wholepatch
629 set last_revert_enc $enc
630 }
631
f522c9b5
SP
632 $ui_diff conf -state normal
633 $ui_diff delete $s_lno $e_lno
634 $ui_diff conf -state disabled
635
62bd9993 636 # Check if the hunk was the last one in the file.
f522c9b5
SP
637 if {[$ui_diff get 1.0 end] eq "\n"} {
638 set o _
639 } else {
640 set o ?
641 }
642
62bd9993
PY
643 # Update the status flags.
644 if {$revert} {
645 set mi [string index $mi 0]$o
646 } elseif {$current_diff_side eq $ui_index} {
f522c9b5
SP
647 set mi ${o}M
648 } elseif {[string index $mi 0] eq {_}} {
649 set mi M$o
650 } else {
651 set mi ?$o
652 }
653 unlock_index
654 display_file $current_diff_path $mi
29853b90 655 # This should trigger shift to the next changed file
f522c9b5 656 if {$o eq {_}} {
29853b90 657 reshow_diff
f522c9b5
SP
658 }
659}
5821988f 660
5f0a516d 661proc apply_or_revert_range_or_line {x y revert} {
5821988f 662 global current_diff_path current_diff_header current_diff_side
a4fa2f0a 663 global ui_diff ui_index file_states last_revert
5821988f 664
ff07c3b6
JE
665 set selected [$ui_diff tag nextrange sel 0.0]
666
667 if {$selected == {}} {
668 set first [$ui_diff index "@$x,$y"]
669 set last $first
670 } else {
671 set first [lindex $selected 0]
672 set last [lindex $selected 1]
673 }
674
675 set first_l [$ui_diff index "$first linestart"]
676 set last_l [$ui_diff index "$last lineend"]
677
5821988f
JS
678 if {$current_diff_path eq {} || $current_diff_header eq {}} return
679 if {![lock_index apply_hunk]} return
680
5f0a516d 681 set apply_cmd {apply --whitespace=nowarn}
5821988f
JS
682 set mi [lindex $file_states($current_diff_path) 0]
683 if {$current_diff_side eq $ui_index} {
684 set failed_msg [mc "Failed to unstage selected line."]
685 set to_context {+}
5f0a516d 686 lappend apply_cmd --reverse --cached
5821988f
JS
687 if {[string index $mi 0] ne {M}} {
688 unlock_index
689 return
690 }
691 } else {
5f0a516d
PY
692 if {$revert} {
693 set failed_msg [mc "Failed to revert selected line."]
694 set to_context {+}
695 lappend apply_cmd --reverse
696 } else {
697 set failed_msg [mc "Failed to stage selected line."]
698 set to_context {-}
699 lappend apply_cmd --cached
700 }
701
5821988f
JS
702 if {[string index $mi 1] ne {M}} {
703 unlock_index
704 return
705 }
706 }
707
ff07c3b6 708 set wholepatch {}
5821988f 709
ff07c3b6
JE
710 while {$first_l < $last_l} {
711 set i_l [$ui_diff search -backwards -regexp ^@@ $first_l 0.0]
712 if {$i_l eq {}} {
713 # If there's not a @@ above, then the selected range
714 # must have come before the first_l @@
715 set i_l [$ui_diff search -regexp ^@@ $first_l $last_l]
716 }
717 if {$i_l eq {}} {
718 unlock_index
719 return
720 }
721 # $i_l is now at the beginning of a line
5821988f 722
ff07c3b6
JE
723 # pick start line number from hunk header
724 set hh [$ui_diff get $i_l "$i_l + 1 lines"]
725 set hh [lindex [split $hh ,] 0]
726 set hln [lindex [split $hh -] 1]
6d02c1e2 727 set hln [lindex [split $hln " "] 0]
5821988f 728
ff07c3b6
JE
729 # There is a special situation to take care of. Consider this
730 # hunk:
731 #
732 # @@ -10,4 +10,4 @@
733 # context before
734 # -old 1
735 # -old 2
736 # +new 1
737 # +new 2
738 # context after
739 #
740 # We used to keep the context lines in the order they appear in
741 # the hunk. But then it is not possible to correctly stage only
742 # "-old 1" and "+new 1" - it would result in this staged text:
743 #
744 # context before
745 # old 2
746 # new 1
747 # context after
748 #
749 # (By symmetry it is not possible to *un*stage "old 2" and "new
750 # 2".)
751 #
752 # We resolve the problem by introducing an asymmetry, namely,
753 # when a "+" line is *staged*, it is moved in front of the
754 # context lines that are generated from the "-" lines that are
755 # immediately before the "+" block. That is, we construct this
756 # patch:
757 #
758 # @@ -10,4 +10,5 @@
759 # context before
760 # +new 1
761 # old 1
762 # old 2
763 # context after
764 #
765 # But we do *not* treat "-" lines that are *un*staged in a
766 # special way.
767 #
768 # With this asymmetry it is possible to stage the change "old
769 # 1" -> "new 1" directly, and to stage the change "old 2" ->
770 # "new 2" by first staging the entire hunk and then unstaging
771 # the change "old 1" -> "new 1".
772 #
773 # Applying multiple lines adds complexity to the special
774 # situation. The pre_context must be moved after the entire
775 # first block of consecutive staged "+" lines, so that
776 # staging both additions gives the following patch:
777 #
778 # @@ -10,4 +10,6 @@
779 # context before
780 # +new 1
781 # +new 2
782 # old 1
783 # old 2
784 # context after
785
786 # This is non-empty if and only if we are _staging_ changes;
787 # then it accumulates the consecutive "-" lines (after
788 # converting them to context lines) in order to be moved after
789 # "+" change lines.
790 set pre_context {}
791
792 set n 0
793 set m 0
794 set i_l [$ui_diff index "$i_l + 1 lines"]
795 set patch {}
796 while {[$ui_diff compare $i_l < "end - 1 chars"] &&
797 [$ui_diff get $i_l "$i_l + 2 chars"] ne {@@}} {
798 set next_l [$ui_diff index "$i_l + 1 lines"]
799 set c1 [$ui_diff get $i_l]
800 if {[$ui_diff compare $first_l <= $i_l] &&
801 [$ui_diff compare $i_l < $last_l] &&
802 ($c1 eq {-} || $c1 eq {+})} {
803 # a line to stage/unstage
804 set ln [$ui_diff get $i_l $next_l]
805 if {$c1 eq {-}} {
806 set n [expr $n+1]
807 set patch "$patch$pre_context$ln"
808 set pre_context {}
809 } else {
810 set m [expr $m+1]
811 set patch "$patch$ln"
812 }
813 } elseif {$c1 ne {-} && $c1 ne {+}} {
814 # context line
815 set ln [$ui_diff get $i_l $next_l]
c7f74570 816 set patch "$patch$pre_context$ln"
1fcd24d0
HV
817 # Skip the "\ No newline at end of
818 # file". Depending on the locale setting
819 # we don't know what this line looks
820 # like exactly. The only thing we do
821 # know is that it starts with "\ "
822 if {![string match {\\ *} $ln]} {
823 set n [expr $n+1]
824 set m [expr $m+1]
825 }
ff07c3b6
JE
826 set pre_context {}
827 } elseif {$c1 eq $to_context} {
828 # turn change line into context line
829 set ln [$ui_diff get "$i_l + 1 chars" $next_l]
830 if {$c1 eq {-}} {
831 set pre_context "$pre_context $ln"
832 } else {
833 set patch "$patch $ln"
834 }
835 set n [expr $n+1]
836 set m [expr $m+1]
c7f74570 837 } else {
ff07c3b6
JE
838 # a change in the opposite direction of
839 # to_context which is outside the range of
840 # lines to apply.
841 set patch "$patch$pre_context"
842 set pre_context {}
c7f74570 843 }
ff07c3b6 844 set i_l $next_l
5821988f 845 }
ff07c3b6
JE
846 set patch "$patch$pre_context"
847 set wholepatch "$wholepatch@@ -$hln,$n +$hln,$m @@\n$patch"
848 set first_l [$ui_diff index "$next_l + 1 lines"]
5821988f 849 }
5821988f
JS
850
851 if {[catch {
72e6b002 852 set enc [get_path_encoding $current_diff_path]
5821988f 853 set p [eval git_write $apply_cmd]
72e6b002 854 fconfigure $p -translation binary -encoding $enc
5821988f 855 puts -nonewline $p $current_diff_header
ff07c3b6 856 puts -nonewline $p $wholepatch
5821988f 857 close $p} err]} {
a3d97afa 858 error_popup "$failed_msg\n\n$err"
2ccdfb1c
PY
859 unlock_index
860 return
5821988f
JS
861 }
862
a4fa2f0a
PY
863 if {$revert} {
864 # Save a copy of this patch for undoing reverts.
865 set last_revert $current_diff_header$wholepatch
866 set last_revert_enc $enc
5821988f
JS
867 }
868
869 unlock_index
870}
a4fa2f0a
PY
871
872# Undo the last line/hunk reverted. When hunks and lines are reverted, a copy
873# of the diff applied is saved. Re-apply that diff to undo the revert.
874#
875# Right now, we only use a single variable to hold the copy, and not a
876# stack/deque for simplicity, so multiple undos are not possible. Maybe this
877# can be added if the need for something like this is felt in the future.
878proc undo_last_revert {} {
879 global last_revert current_diff_path current_diff_header
880 global last_revert_enc
881
882 if {$last_revert eq {}} return
883 if {![lock_index apply_hunk]} return
884
885 set apply_cmd {apply --whitespace=nowarn}
886 set failed_msg [mc "Failed to undo last revert."]
887
888 if {[catch {
889 set enc $last_revert_enc
890 set p [eval git_write $apply_cmd]
891 fconfigure $p -translation binary -encoding $enc
892 puts -nonewline $p $last_revert
893 close $p} err]} {
894 error_popup "$failed_msg\n\n$err"
895 unlock_index
896 return
897 }
898
899 set last_revert {}
900
5821988f
JS
901 unlock_index
902}