]> git.ipfire.org Git - thirdparty/git.git/blob - git-gui/lib/diff.tcl
d657bfec05b49865627f321ab260633f250f71c6
[thirdparty/git.git] / git-gui / lib / diff.tcl
1 # git-gui diff viewer
2 # Copyright (C) 2006, 2007 Shawn Pearce
3
4 proc 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
17 proc 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
32 proc reshow_diff {{after {}}} {
33 global file_states file_lists
34 global current_diff_path current_diff_side
35 global ui_diff
36
37 set p $current_diff_path
38 if {$p eq {}} {
39 # No diff is being shown.
40 } elseif {$current_diff_side eq {}} {
41 clear_diff
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]}]} {
46 next_diff $after
47 } else {
48 clear_diff
49 }
50 } else {
51 set save_pos [lindex [$ui_diff yview] 0]
52 show_diff $p $current_diff_side {} $save_pos $after
53 }
54 }
55
56 proc force_diff_encoding {enc} {
57 global current_diff_path
58
59 if {$current_diff_path ne {}} {
60 force_path_encoding $current_diff_path $enc
61 reshow_diff
62 }
63 }
64
65 proc handle_empty_diff {} {
66 global current_diff_path file_states
67 global ui_diff
68
69 set path $current_diff_path
70 set s $file_states($path)
71 if {[lindex $s 0] ne {_M} || [has_textconv $path]} return
72
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
77 }
78
79 proc show_diff {path w {lno {}} {scroll_pos {}} {callback {}}} {
80 global file_states file_lists
81 global is_3way_diff is_conflict_diff diff_active repo_config
82 global ui_diff ui_index ui_workdir
83 global current_diff_path current_diff_side current_diff_header
84 global current_diff_queue
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
97 $w see $lno.0
98 }
99
100 set s $file_states($path)
101 set m [lindex $s 0]
102 set is_conflict_diff 0
103 set current_diff_path $path
104 set current_diff_side $w
105 set current_diff_queue {}
106 ui_status [mc "Loading diff of %s..." [escape_path $path]]
107
108 set cont_info [list $scroll_pos $callback]
109
110 apply_tab_size 0
111
112 if {[string first {U} $m] >= 0} {
113 merge_load_stages $path [list show_unmerged_diff $cont_info]
114 } elseif {$m eq {_O}} {
115 show_other_diff $path $w $m $cont_info
116 } else {
117 start_show_diff $cont_info
118 }
119
120 global current_diff_path selected_paths
121 set selected_paths($current_diff_path) 1
122 }
123
124 proc show_unmerged_diff {cont_info} {
125 global current_diff_path current_diff_side
126 global merge_stages ui_diff is_conflict_diff
127 global current_diff_queue
128
129 if {$merge_stages(2) eq {}} {
130 set is_conflict_diff 1
131 lappend current_diff_queue \
132 [list [mc "LOCAL: deleted\nREMOTE:\n"] d= \
133 [list ":1:$current_diff_path" ":3:$current_diff_path"]]
134 } elseif {$merge_stages(3) eq {}} {
135 set is_conflict_diff 1
136 lappend current_diff_queue \
137 [list [mc "REMOTE: deleted\nLOCAL:\n"] d= \
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}} {
142 set is_conflict_diff 1
143 lappend current_diff_queue \
144 [list [mc "LOCAL:\n"] d= \
145 [list ":1:$current_diff_path" ":2:$current_diff_path"]]
146 lappend current_diff_queue \
147 [list [mc "REMOTE:\n"] d= \
148 [list ":1:$current_diff_path" ":3:$current_diff_path"]]
149 } else {
150 start_show_diff $cont_info
151 return
152 }
153
154 advance_diff_queue $cont_info
155 }
156
157 proc advance_diff_queue {cont_info} {
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
167 start_show_diff $cont_info [lindex $item 2]
168 }
169
170 proc show_other_diff {path w m cont_info} {
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
176 # - Git won't give us the diff, there's nothing to compare to!
177 #
178 if {$m eq {_O}} {
179 set max_sz 100000
180 set type unknown
181 if {[catch {
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 {
190 set content [file readlink $path]
191 set sz [string length $content]
192 }
193 file {
194 set fd [open $path r]
195 fconfigure $fd \
196 -eofchar {} \
197 -encoding [get_path_encoding $path]
198 set content [read $fd $max_sz]
199 close $fd
200 set sz [file size $path]
201 }
202 default {
203 error "'$type' not supported"
204 }
205 }
206 } err ]} {
207 set diff_active 0
208 unlock_index
209 ui_status [mc "Unable to display %s" [escape_path $path]]
210 error_popup [strcat [mc "Error loading file:"] "\n\n$err"]
211 return
212 }
213 $ui_diff conf -state normal
214 if {$type eq {submodule}} {
215 $ui_diff insert end \
216 "* [mc "Git Repository (subproject)"]\n" \
217 d_info
218 } elseif {![catch {set type [exec file $path]}]} {
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 }
224 $ui_diff insert end "* $type\n" d_info
225 }
226 if {[string first "\0" $content] != -1} {
227 $ui_diff insert end \
228 [mc "* Binary file (not showing content)."] \
229 d_info
230 } else {
231 if {$sz > $max_sz} {
232 $ui_diff insert end [mc \
233 "* Untracked file is %d bytes.
234 * Showing only first %d bytes.
235 " $sz $max_sz] d_info
236 }
237 $ui_diff insert end $content
238 if {$sz > $max_sz} {
239 $ui_diff insert end [mc "
240 * Untracked file clipped here by %s.
241 * To see the entire file, use an external editor.
242 " [appname]] d_info
243 }
244 }
245 $ui_diff conf -state disabled
246 set diff_active 0
247 unlock_index
248 set scroll_pos [lindex $cont_info 0]
249 if {$scroll_pos ne {}} {
250 update
251 $ui_diff yview moveto $scroll_pos
252 }
253 ui_ready
254 set callback [lindex $cont_info 1]
255 if {$callback ne {}} {
256 eval $callback
257 }
258 return
259 }
260 }
261
262 proc start_show_diff {cont_info {add_opts {}}} {
263 global file_states file_lists
264 global is_3way_diff is_submodule_diff diff_active repo_config
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
274 set is_submodule_diff 0
275 set diff_active 1
276 set current_diff_header {}
277 set conflict_size [gitattr $path conflict-marker-size 7]
278
279 set cmd [list]
280 if {$w eq $ui_index} {
281 lappend cmd diff-index
282 lappend cmd --cached
283 if {[git-version >= "1.7.2"]} {
284 lappend cmd --ignore-submodules=dirty
285 }
286 } elseif {$w eq $ui_workdir} {
287 if {[string first {U} $m] >= 0} {
288 lappend cmd diff
289 } else {
290 lappend cmd diff-files
291 }
292 }
293 if {![is_config_false gui.textconv] && [git-version >= 1.6.1]} {
294 lappend cmd --textconv
295 }
296
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
306 lappend cmd -p
307 lappend cmd --color
308 set cmd [concat $cmd $repo_config(gui.diffopts)]
309 if {$repo_config(gui.diffcontext) >= 1} {
310 lappend cmd "-U$repo_config(gui.diffcontext)"
311 }
312 if {$w eq $ui_index} {
313 lappend cmd [PARENT]
314 }
315 if {$add_opts ne {}} {
316 eval lappend cmd $add_opts
317 } else {
318 lappend cmd --
319 lappend cmd $path
320 }
321
322 if {$is_submodule_diff && [git-version < "1.6.6"]} {
323 if {$w eq $ui_index} {
324 set cmd [list submodule summary --cached -- $path]
325 } else {
326 set cmd [list submodule summary --files -- $path]
327 }
328 }
329
330 if {[catch {set fd [eval git_read --nice $cmd]} err]} {
331 set diff_active 0
332 unlock_index
333 ui_status [mc "Unable to display %s" [escape_path $path]]
334 error_popup [strcat [mc "Error loading diff:"] "\n\n$err"]
335 return
336 }
337
338 set ::current_diff_inheader 1
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
343 fconfigure $fd \
344 -blocking 0 \
345 -encoding [get_path_encoding $path] \
346 -translation lf
347 fileevent $fd readable [list read_diff $fd $conflict_size $cont_info]
348 }
349
350 proc parse_color_line {line} {
351 set start 0
352 set result ""
353 set markup [list]
354 set regexp {\033\[((?:\d+;)*\d+)?m}
355 set need_reset 0
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}]]
359 set pos [string length $result]
360 set col [eval [linsert $code 0 string range $line]]
361 set start [incr end]
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
369 }
370 append result [string range $line $start end]
371 if {[llength $markup] < 4} {set markup {}}
372 return [list $result $markup]
373 }
374
375 proc read_diff {fd conflict_size cont_info} {
376 global ui_diff diff_active is_submodule_diff
377 global is_3way_diff is_conflict_diff current_diff_header
378 global current_diff_queue
379
380 $ui_diff conf -state normal
381 while {[gets $fd line] >= 0} {
382 foreach {line markup} [parse_color_line $line] break
383 set line [string map {\033 ^} $line]
384
385 set tags {}
386
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).
395 #
396 if {[regexp {^@@+ } $line]} {set ::current_diff_inheader 0}
397
398 # -- Automatically detect if this is a 3 way diff.
399 #
400 if {[string match {@@@ *} $line]} {
401 set is_3way_diff 1
402 apply_tab_size 1
403 }
404
405 if {$::current_diff_inheader} {
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 }
414
415 # -- Cleanup uninteresting diff header lines.
416 #
417 if { [string match {diff --git *} $line]
418 || [string match {diff --cc *} $line]
419 || [string match {diff --combined *} $line]
420 || [string match {--- *} $line]
421 || [string match {+++ *} $line]
422 || [string match {index *} $line]} {
423 continue
424 }
425
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
429
430 } elseif { $line eq {\ No newline at end of file}} {
431 # -- Handle some special lines
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 {++} {
443 set regexp [string map [list %conflict_size $conflict_size]\
444 {^\+\+([<>=|]){%conflict_size}(?: |$)}]
445 if {[regexp $regexp $line _g op]} {
446 set is_conflict_diff 1
447 set line [string replace $line 0 1 { }]
448 set tags d$op
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_--
461 } else {
462 set tags d_++
463 }
464 }
465 default {
466 puts "error: Unhandled 3 way diff marker: {$op}"
467 set tags {}
468 }
469 }
470 } elseif {$is_submodule_diff} {
471 if {$line == ""} continue
472 if {[regexp {^Submodule } $line]} {
473 set tags d_info
474 } elseif {[regexp {^\* } $line]} {
475 set line [string replace $line 0 1 {Submodule }]
476 set tags d_info
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 }
489 } else {
490 set op [string index $line 0]
491 switch -- $op {
492 { } {set tags {}}
493 {@} {set tags d_@}
494 {-} {set tags d_-}
495 {+} {
496 set regexp [string map [list %conflict_size $conflict_size]\
497 {^\+([<>=]){%conflict_size}(?: |$)}]
498 if {[regexp $regexp $line _g op]} {
499 set is_conflict_diff 1
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 }
511 set mark [$ui_diff index "end - 1 line linestart"]
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
517
518 foreach {posbegin colbegin posend colend} $markup {
519 set prefix clr
520 foreach style [lsort -integer [split $colbegin ";"]] {
521 if {$style eq "7"} {append prefix i; continue}
522 if {$style != 4 && ($style < 30 || $style > 47)} {continue}
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 }
528 }
529 $ui_diff conf -state disabled
530
531 if {[eof $fd]} {
532 close $fd
533
534 if {$current_diff_queue ne {}} {
535 advance_diff_queue $cont_info
536 return
537 }
538
539 set diff_active 0
540 unlock_index
541 set scroll_pos [lindex $cont_info 0]
542 if {$scroll_pos ne {}} {
543 update
544 $ui_diff yview moveto $scroll_pos
545 }
546 ui_ready
547
548 if {[$ui_diff index end] eq {2.0}} {
549 handle_empty_diff
550 }
551
552 set callback [lindex $cont_info 1]
553 if {$callback ne {}} {
554 eval $callback
555 }
556 }
557 }
558
559 proc apply_or_revert_hunk {x y revert} {
560 global current_diff_path current_diff_header current_diff_side
561 global ui_diff ui_index file_states last_revert last_revert_enc
562
563 if {$current_diff_path eq {} || $current_diff_header eq {}} return
564 if {![lock_index apply_hunk]} return
565
566 set apply_cmd {apply --whitespace=nowarn}
567 set mi [lindex $file_states($current_diff_path) 0]
568 if {$current_diff_side eq $ui_index} {
569 set failed_msg [mc "Failed to unstage selected hunk."]
570 lappend apply_cmd --reverse --cached
571 if {[string index $mi 0] ne {M}} {
572 unlock_index
573 return
574 }
575 } else {
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
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
602 set wholepatch "$current_diff_header[$ui_diff get $s_lno $e_lno]"
603
604 if {[catch {
605 set enc [get_path_encoding $current_diff_path]
606 set p [eval git_write $apply_cmd]
607 fconfigure $p -translation binary -encoding $enc
608 puts -nonewline $p $wholepatch
609 close $p} err]} {
610 error_popup "$failed_msg\n\n$err"
611 unlock_index
612 return
613 }
614
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
621 $ui_diff conf -state normal
622 $ui_diff delete $s_lno $e_lno
623 $ui_diff conf -state disabled
624
625 # Check if the hunk was the last one in the file.
626 if {[$ui_diff get 1.0 end] eq "\n"} {
627 set o _
628 } else {
629 set o ?
630 }
631
632 # Update the status flags.
633 if {$revert} {
634 set mi [string index $mi 0]$o
635 } elseif {$current_diff_side eq $ui_index} {
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
644 # This should trigger shift to the next changed file
645 if {$o eq {_}} {
646 reshow_diff
647 }
648 }
649
650 proc apply_or_revert_range_or_line {x y revert} {
651 global current_diff_path current_diff_header current_diff_side
652 global ui_diff ui_index file_states last_revert
653
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
667 if {$current_diff_path eq {} || $current_diff_header eq {}} return
668 if {![lock_index apply_hunk]} return
669
670 set apply_cmd {apply --whitespace=nowarn}
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 {+}
675 lappend apply_cmd --reverse --cached
676 if {[string index $mi 0] ne {M}} {
677 unlock_index
678 return
679 }
680 } else {
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
691 if {[string index $mi 1] ne {M}} {
692 unlock_index
693 return
694 }
695 }
696
697 set wholepatch {}
698
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
711
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]
716 set hln [lindex [split $hln " "] 0]
717
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]
805 set patch "$patch$pre_context$ln"
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 }
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]
826 } else {
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 {}
832 }
833 set i_l $next_l
834 }
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"]
838 }
839
840 if {[catch {
841 set enc [get_path_encoding $current_diff_path]
842 set p [eval git_write $apply_cmd]
843 fconfigure $p -translation binary -encoding $enc
844 puts -nonewline $p $current_diff_header
845 puts -nonewline $p $wholepatch
846 close $p} err]} {
847 error_popup "$failed_msg\n\n$err"
848 unlock_index
849 return
850 }
851
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.
867 proc 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
890 unlock_index
891 }