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