Git fork
1# git-gui diff viewer
2# Copyright (C) 2006, 2007 Shawn Pearce
3
4proc 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
15proc 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
30proc 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
54proc 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
63proc 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
77proc 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
122proc 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
155proc 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
168proc 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
259proc 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
336proc 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
361proc 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 1
389 }
390
391 if {$::current_diff_inheader} {
392
393 # -- These two lines stop a diff header and shouldn't be in there
394 if { [string match {Binary files * and * differ} $line]
395 || [regexp {^\* Unmerged path } $line]} {
396 set ::current_diff_inheader 0
397 } else {
398 append current_diff_header $line "\n"
399 }
400
401 # -- Cleanup uninteresting diff header lines.
402 #
403 if { [string match {diff --git *} $line]
404 || [string match {diff --cc *} $line]
405 || [string match {diff --combined *} $line]
406 || [string match {--- *} $line]
407 || [string match {+++ *} $line]
408 || [string match {index *} $line]} {
409 continue
410 }
411
412 # -- Name it symlink, not 120000
413 # Note, that the original line is in $current_diff_header
414 regsub {^(deleted|new) file mode 120000} $line {\1 symlink} line
415
416 } elseif { $line eq {\ No newline at end of file}} {
417 # -- Handle some special lines
418 } elseif {$is_3way_diff} {
419 set op [string range $line 0 1]
420 switch -- $op {
421 { } {set tags {}}
422 {@@} {set tags d_@}
423 { +} {set tags d_s+}
424 { -} {set tags d_s-}
425 {+ } {set tags d_+s}
426 {- } {set tags d_-s}
427 {--} {set tags d_--}
428 {++} {
429 set regexp [string map [list %conflict_size $conflict_size]\
430 {^\+\+([<>=|]){%conflict_size}(?: |$)}]
431 if {[regexp $regexp $line _g op]} {
432 set is_conflict_diff 1
433 set line [string replace $line 0 1 { }]
434 set tags d$op
435
436 # The ||| conflict-marker marks the start of the pre-image.
437 # All those lines are also prefixed with '++'. Thus we need
438 # to maintain this state.
439 set ::conflict_in_pre_image [expr {$op eq {|}}]
440 } elseif {$::conflict_in_pre_image} {
441 # This is a pre-image line. It is the one which both sides
442 # are based on. As it has also the '++' line start, it is
443 # normally shown as 'added'. Invert this to '--' to make
444 # it a 'removed' line.
445 set line [string replace $line 0 1 {--}]
446 set tags d_--
447 } else {
448 set tags d_++
449 }
450 }
451 default {
452 puts "error: Unhandled 3 way diff marker: {$op}"
453 set tags {}
454 }
455 }
456 } elseif {$is_submodule_diff} {
457 if {$line == ""} continue
458 if {[regexp {^Submodule } $line]} {
459 set tags d_info
460 } elseif {[regexp {^\* } $line]} {
461 set line [string replace $line 0 1 {Submodule }]
462 set tags d_info
463 } else {
464 set op [string range $line 0 2]
465 switch -- $op {
466 { <} {set tags d_-}
467 { >} {set tags d_+}
468 { W} {set tags {}}
469 default {
470 puts "error: Unhandled submodule diff marker: {$op}"
471 set tags {}
472 }
473 }
474 }
475 } else {
476 set op [string index $line 0]
477 switch -- $op {
478 { } {set tags {}}
479 {@} {set tags d_@}
480 {-} {set tags d_-}
481 {+} {
482 set regexp [string map [list %conflict_size $conflict_size]\
483 {^\+([<>=]){%conflict_size}(?: |$)}]
484 if {[regexp $regexp $line _g op]} {
485 set is_conflict_diff 1
486 set tags d$op
487 } else {
488 set tags d_+
489 }
490 }
491 default {
492 puts "error: Unhandled 2 way diff marker: {$op}"
493 set tags {}
494 }
495 }
496 }
497 set mark [$ui_diff index "end - 1 line linestart"]
498 $ui_diff insert end $line $tags
499 if {[string index $line end] eq "\r"} {
500 $ui_diff tag add d_cr {end - 2c}
501 }
502 $ui_diff insert end "\n" $tags
503
504 foreach {posbegin colbegin posend colend} $markup {
505 set prefix clr
506 foreach style [lsort -integer [split $colbegin ";"]] {
507 if {$style eq "7"} {append prefix i; continue}
508 if {$style != 4 && ($style < 30 || $style > 47)} {continue}
509 set a "$mark linestart + $posbegin chars"
510 set b "$mark linestart + $posend chars"
511 catch {$ui_diff tag add $prefix$style $a $b}
512 }
513 }
514 }
515 $ui_diff conf -state disabled
516
517 if {[eof $fd]} {
518 close $fd
519
520 if {$current_diff_queue ne {}} {
521 advance_diff_queue $cont_info
522 return
523 }
524
525 set diff_active 0
526 unlock_index
527 set scroll_pos [lindex $cont_info 0]
528 if {$scroll_pos ne {}} {
529 update
530 $ui_diff yview moveto $scroll_pos
531 }
532 ui_ready
533
534 if {[$ui_diff index end] eq {2.0}} {
535 handle_empty_diff
536 }
537
538 set callback [lindex $cont_info 1]
539 if {$callback ne {}} {
540 eval $callback
541 }
542 }
543}
544
545proc apply_or_revert_hunk {x y revert} {
546 global current_diff_path current_diff_header current_diff_side
547 global ui_diff ui_index file_states last_revert last_revert_enc
548
549 if {$current_diff_path eq {} || $current_diff_header eq {}} return
550 if {![lock_index apply_hunk]} return
551
552 set apply_cmd {apply --whitespace=nowarn}
553 set mi [lindex $file_states($current_diff_path) 0]
554 if {$current_diff_side eq $ui_index} {
555 set failed_msg [mc "Failed to unstage selected hunk."]
556 lappend apply_cmd --reverse --cached
557 if {[string index $mi 0] ne {M}} {
558 unlock_index
559 return
560 }
561 } else {
562 if {$revert} {
563 set failed_msg [mc "Failed to revert selected hunk."]
564 lappend apply_cmd --reverse
565 } else {
566 set failed_msg [mc "Failed to stage selected hunk."]
567 lappend apply_cmd --cached
568 }
569
570 if {[string index $mi 1] ne {M}} {
571 unlock_index
572 return
573 }
574 }
575
576 set s_lno [lindex [split [$ui_diff index @$x,$y] .] 0]
577 set s_lno [$ui_diff search -backwards -regexp ^@@ $s_lno.0 0.0]
578 if {$s_lno eq {}} {
579 unlock_index
580 return
581 }
582
583 set e_lno [$ui_diff search -forwards -regexp ^@@ "$s_lno + 1 lines" end]
584 if {$e_lno eq {}} {
585 set e_lno end
586 }
587
588 set wholepatch "$current_diff_header[$ui_diff get $s_lno $e_lno]"
589
590 if {[catch {
591 set enc [get_path_encoding $current_diff_path]
592 set p [git_write $apply_cmd]
593 fconfigure $p -translation binary -encoding $enc
594 puts -nonewline $p $wholepatch
595 close $p} err]} {
596 error_popup "$failed_msg\n\n$err"
597 unlock_index
598 return
599 }
600
601 if {$revert} {
602 # Save a copy of this patch for undoing reverts.
603 set last_revert $wholepatch
604 set last_revert_enc $enc
605 }
606
607 $ui_diff conf -state normal
608 $ui_diff delete $s_lno $e_lno
609 $ui_diff conf -state disabled
610
611 # Check if the hunk was the last one in the file.
612 if {[$ui_diff get 1.0 end] eq "\n"} {
613 set o _
614 } else {
615 set o ?
616 }
617
618 # Update the status flags.
619 if {$revert} {
620 set mi [string index $mi 0]$o
621 } elseif {$current_diff_side eq $ui_index} {
622 set mi ${o}M
623 } elseif {[string index $mi 0] eq {_}} {
624 set mi M$o
625 } else {
626 set mi ?$o
627 }
628 unlock_index
629 display_file $current_diff_path $mi
630 # This should trigger shift to the next changed file
631 if {$o eq {_}} {
632 reshow_diff
633 }
634}
635
636proc apply_or_revert_range_or_line {x y revert} {
637 global current_diff_path current_diff_header current_diff_side
638 global ui_diff ui_index file_states last_revert
639
640 set selected [$ui_diff tag nextrange sel 0.0]
641
642 if {$selected == {}} {
643 set first [$ui_diff index "@$x,$y"]
644 set last $first
645 } else {
646 set first [lindex $selected 0]
647 set last [lindex $selected 1]
648 }
649
650 set first_l [$ui_diff index "$first linestart"]
651 set last_l [$ui_diff index "$last lineend"]
652
653 if {$current_diff_path eq {} || $current_diff_header eq {}} return
654 if {![lock_index apply_hunk]} return
655
656 set apply_cmd {apply --whitespace=nowarn}
657 set mi [lindex $file_states($current_diff_path) 0]
658 if {$current_diff_side eq $ui_index} {
659 set failed_msg [mc "Failed to unstage selected line."]
660 set to_context {+}
661 lappend apply_cmd --reverse --cached
662 if {[string index $mi 0] ne {M}} {
663 unlock_index
664 return
665 }
666 } else {
667 if {$revert} {
668 set failed_msg [mc "Failed to revert selected line."]
669 set to_context {+}
670 lappend apply_cmd --reverse
671 } else {
672 set failed_msg [mc "Failed to stage selected line."]
673 set to_context {-}
674 lappend apply_cmd --cached
675 }
676
677 if {[string index $mi 1] ne {M}} {
678 unlock_index
679 return
680 }
681 }
682
683 set wholepatch {}
684
685 while {$first_l < $last_l} {
686 set i_l [$ui_diff search -backwards -regexp ^@@ $first_l 0.0]
687 if {$i_l eq {}} {
688 # If there's not a @@ above, then the selected range
689 # must have come before the first_l @@
690 set i_l [$ui_diff search -regexp ^@@ $first_l $last_l]
691 }
692 if {$i_l eq {}} {
693 unlock_index
694 return
695 }
696 # $i_l is now at the beginning of a line
697
698 # pick start line number from hunk header
699 set hh [$ui_diff get $i_l "$i_l + 1 lines"]
700 set hh [lindex [split $hh ,] 0]
701 set hln [lindex [split $hh -] 1]
702 set hln [lindex [split $hln " "] 0]
703
704 # There is a special situation to take care of. Consider this
705 # hunk:
706 #
707 # @@ -10,4 +10,4 @@
708 # context before
709 # -old 1
710 # -old 2
711 # +new 1
712 # +new 2
713 # context after
714 #
715 # We used to keep the context lines in the order they appear in
716 # the hunk. But then it is not possible to correctly stage only
717 # "-old 1" and "+new 1" - it would result in this staged text:
718 #
719 # context before
720 # old 2
721 # new 1
722 # context after
723 #
724 # (By symmetry it is not possible to *un*stage "old 2" and "new
725 # 2".)
726 #
727 # We resolve the problem by introducing an asymmetry, namely,
728 # when a "+" line is *staged*, it is moved in front of the
729 # context lines that are generated from the "-" lines that are
730 # immediately before the "+" block. That is, we construct this
731 # patch:
732 #
733 # @@ -10,4 +10,5 @@
734 # context before
735 # +new 1
736 # old 1
737 # old 2
738 # context after
739 #
740 # But we do *not* treat "-" lines that are *un*staged in a
741 # special way.
742 #
743 # With this asymmetry it is possible to stage the change "old
744 # 1" -> "new 1" directly, and to stage the change "old 2" ->
745 # "new 2" by first staging the entire hunk and then unstaging
746 # the change "old 1" -> "new 1".
747 #
748 # Applying multiple lines adds complexity to the special
749 # situation. The pre_context must be moved after the entire
750 # first block of consecutive staged "+" lines, so that
751 # staging both additions gives the following patch:
752 #
753 # @@ -10,4 +10,6 @@
754 # context before
755 # +new 1
756 # +new 2
757 # old 1
758 # old 2
759 # context after
760
761 # This is non-empty if and only if we are _staging_ changes;
762 # then it accumulates the consecutive "-" lines (after
763 # converting them to context lines) in order to be moved after
764 # "+" change lines.
765 set pre_context {}
766
767 set n 0
768 set m 0
769 set i_l [$ui_diff index "$i_l + 1 lines"]
770 set patch {}
771 while {[$ui_diff compare $i_l < "end - 1 chars"] &&
772 [$ui_diff get $i_l "$i_l + 2 chars"] ne {@@}} {
773 set next_l [$ui_diff index "$i_l + 1 lines"]
774 set c1 [$ui_diff get $i_l]
775 if {[$ui_diff compare $first_l <= $i_l] &&
776 [$ui_diff compare $i_l < $last_l] &&
777 ($c1 eq {-} || $c1 eq {+})} {
778 # a line to stage/unstage
779 set ln [$ui_diff get $i_l $next_l]
780 if {$c1 eq {-}} {
781 set n [expr $n+1]
782 set patch "$patch$pre_context$ln"
783 set pre_context {}
784 } else {
785 set m [expr $m+1]
786 set patch "$patch$ln"
787 }
788 } elseif {$c1 ne {-} && $c1 ne {+}} {
789 # context line
790 set ln [$ui_diff get $i_l $next_l]
791 set patch "$patch$pre_context$ln"
792 # Skip the "\ No newline at end of
793 # file". Depending on the locale setting
794 # we don't know what this line looks
795 # like exactly. The only thing we do
796 # know is that it starts with "\ "
797 if {![string match {\\ *} $ln]} {
798 set n [expr $n+1]
799 set m [expr $m+1]
800 }
801 set pre_context {}
802 } elseif {$c1 eq $to_context} {
803 # turn change line into context line
804 set ln [$ui_diff get "$i_l + 1 chars" $next_l]
805 if {$c1 eq {-}} {
806 set pre_context "$pre_context $ln"
807 } else {
808 set patch "$patch $ln"
809 }
810 set n [expr $n+1]
811 set m [expr $m+1]
812 } else {
813 # a change in the opposite direction of
814 # to_context which is outside the range of
815 # lines to apply.
816 set patch "$patch$pre_context"
817 set pre_context {}
818 }
819 set i_l $next_l
820 }
821 set patch "$patch$pre_context"
822 set wholepatch "$wholepatch@@ -$hln,$n +$hln,$m @@\n$patch"
823 set first_l [$ui_diff index "$next_l + 1 lines"]
824 }
825
826 if {[catch {
827 set enc [get_path_encoding $current_diff_path]
828 set p [git_write $apply_cmd]
829 fconfigure $p -translation binary -encoding $enc
830 puts -nonewline $p $current_diff_header
831 puts -nonewline $p $wholepatch
832 close $p} err]} {
833 error_popup "$failed_msg\n\n$err"
834 unlock_index
835 return
836 }
837
838 if {$revert} {
839 # Save a copy of this patch for undoing reverts.
840 set last_revert $current_diff_header$wholepatch
841 set last_revert_enc $enc
842 }
843
844 unlock_index
845}
846
847# Undo the last line/hunk reverted. When hunks and lines are reverted, a copy
848# of the diff applied is saved. Re-apply that diff to undo the revert.
849#
850# Right now, we only use a single variable to hold the copy, and not a
851# stack/deque for simplicity, so multiple undos are not possible. Maybe this
852# can be added if the need for something like this is felt in the future.
853proc undo_last_revert {} {
854 global last_revert current_diff_path current_diff_header
855 global last_revert_enc
856
857 if {$last_revert eq {}} return
858 if {![lock_index apply_hunk]} return
859
860 set apply_cmd {apply --whitespace=nowarn}
861 set failed_msg [mc "Failed to undo last revert."]
862
863 if {[catch {
864 set enc $last_revert_enc
865 set p [git_write $apply_cmd]
866 fconfigure $p -translation binary -encoding $enc
867 puts -nonewline $p $last_revert
868 close $p} err]} {
869 error_popup "$failed_msg\n\n$err"
870 unlock_index
871 return
872 }
873
874 set last_revert {}
875
876 unlock_index
877}