1# git-gui diff viewer
2# Copyright (C) 2006, 2007 Shawn Pearce
3
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
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
32proc 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
56proc 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
65proc handle_empty_diff {} {
66 global current_diff_path file_states file_lists
67 global diff_empty_count
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 # Prevent infinite rescan loops
74 incr diff_empty_count
75 if {$diff_empty_count > 1} return
76
77 info_popup [mc "No differences detected.
78
79%s has no changes.
80
81The modification date of this file was updated by another application, but the content within the file was not changed.
82
83A rescan will be automatically started to find other files which may have the same state." [short_path $path]]
84
85 clear_diff
86 display_file $path __
87 rescan ui_ready 0
88}
89
90proc show_diff {path w {lno {}} {scroll_pos {}} {callback {}}} {
91 global file_states file_lists
92 global is_3way_diff is_conflict_diff diff_active repo_config
93 global ui_diff ui_index ui_workdir
94 global current_diff_path current_diff_side current_diff_header
95 global current_diff_queue
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
108 $w see $lno.0
109 }
110
111 set s $file_states($path)
112 set m [lindex $s 0]
113 set is_conflict_diff 0
114 set current_diff_path $path
115 set current_diff_side $w
116 set current_diff_queue {}
117 ui_status [mc "Loading diff of %s..." [escape_path $path]]
118
119 set cont_info [list $scroll_pos $callback]
120
121 apply_tab_size 0
122
123 if {[string first {U} $m] >= 0} {
124 merge_load_stages $path [list show_unmerged_diff $cont_info]
125 } elseif {$m eq {_O}} {
126 show_other_diff $path $w $m $cont_info
127 } else {
128 start_show_diff $cont_info
129 }
130
131 global current_diff_path selected_paths
132 set selected_paths($current_diff_path) 1
133}
134
135proc show_unmerged_diff {cont_info} {
136 global current_diff_path current_diff_side
137 global merge_stages ui_diff is_conflict_diff
138 global current_diff_queue
139
140 if {$merge_stages(2) eq {}} {
141 set is_conflict_diff 1
142 lappend current_diff_queue \
143 [list [mc "LOCAL: deleted\nREMOTE:\n"] d= \
144 [list ":1:$current_diff_path" ":3:$current_diff_path"]]
145 } elseif {$merge_stages(3) eq {}} {
146 set is_conflict_diff 1
147 lappend current_diff_queue \
148 [list [mc "REMOTE: deleted\nLOCAL:\n"] d= \
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}} {
153 set is_conflict_diff 1
154 lappend current_diff_queue \
155 [list [mc "LOCAL:\n"] d= \
156 [list ":1:$current_diff_path" ":2:$current_diff_path"]]
157 lappend current_diff_queue \
158 [list [mc "REMOTE:\n"] d= \
159 [list ":1:$current_diff_path" ":3:$current_diff_path"]]
160 } else {
161 start_show_diff $cont_info
162 return
163 }
164
165 advance_diff_queue $cont_info
166}
167
168proc advance_diff_queue {cont_info} {
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
178 start_show_diff $cont_info [lindex $item 2]
179}
180
181proc show_other_diff {path w m cont_info} {
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
187 # - Git won't give us the diff, there's nothing to compare to!
188 #
189 if {$m eq {_O}} {
190 set max_sz 100000
191 set type unknown
192 if {[catch {
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 {
201 set content [file readlink $path]
202 set sz [string length $content]
203 }
204 file {
205 set fd [open $path r]
206 fconfigure $fd \
207 -eofchar {} \
208 -encoding [get_path_encoding $path]
209 set content [read $fd $max_sz]
210 close $fd
211 set sz [file size $path]
212 }
213 default {
214 error "'$type' not supported"
215 }
216 }
217 } err ]} {
218 set diff_active 0
219 unlock_index
220 ui_status [mc "Unable to display %s" [escape_path $path]]
221 error_popup [strcat [mc "Error loading file:"] "\n\n$err"]
222 return
223 }
224 $ui_diff conf -state normal
225 if {$type eq {submodule}} {
226 $ui_diff insert end \
227 "* [mc "Git Repository (subproject)"]\n" \
228 d_info
229 } elseif {![catch {set type [exec file $path]}]} {
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 }
235 $ui_diff insert end "* $type\n" d_info
236 }
237 if {[string first "\0" $content] != -1} {
238 $ui_diff insert end \
239 [mc "* Binary file (not showing content)."] \
240 d_info
241 } else {
242 if {$sz > $max_sz} {
243 $ui_diff insert end [mc \
244"* Untracked file is %d bytes.
245* Showing only first %d bytes.
246" $sz $max_sz] d_info
247 }
248 $ui_diff insert end $content
249 if {$sz > $max_sz} {
250 $ui_diff insert end [mc "
251* Untracked file clipped here by %s.
252* To see the entire file, use an external editor.
253" [appname]] d_info
254 }
255 }
256 $ui_diff conf -state disabled
257 set diff_active 0
258 unlock_index
259 set scroll_pos [lindex $cont_info 0]
260 if {$scroll_pos ne {}} {
261 update
262 $ui_diff yview moveto $scroll_pos
263 }
264 ui_ready
265 set callback [lindex $cont_info 1]
266 if {$callback ne {}} {
267 eval $callback
268 }
269 return
270 }
271}
272
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
286proc start_show_diff {cont_info {add_opts {}}} {
287 global file_states file_lists
288 global is_3way_diff is_submodule_diff diff_active repo_config
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
298 set is_submodule_diff 0
299 set diff_active 1
300 set current_diff_header {}
301 set conflict_size [get_conflict_marker_size $path]
302
303 set cmd [list]
304 if {$w eq $ui_index} {
305 lappend cmd diff-index
306 lappend cmd --cached
307 if {[git-version >= "1.7.2"]} {
308 lappend cmd --ignore-submodules=dirty
309 }
310 } elseif {$w eq $ui_workdir} {
311 if {[string first {U} $m] >= 0} {
312 lappend cmd diff
313 } else {
314 lappend cmd diff-files
315 }
316 }
317 if {![is_config_false gui.textconv] && [git-version >= 1.6.1]} {
318 lappend cmd --textconv
319 }
320
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
330 lappend cmd -p
331 lappend cmd --color
332 set cmd [concat $cmd $repo_config(gui.diffopts)]
333 if {$repo_config(gui.diffcontext) >= 1} {
334 lappend cmd "-U$repo_config(gui.diffcontext)"
335 }
336 if {$w eq $ui_index} {
337 lappend cmd [PARENT]
338 }
339 if {$add_opts ne {}} {
340 eval lappend cmd $add_opts
341 } else {
342 lappend cmd --
343 lappend cmd $path
344 }
345
346 if {$is_submodule_diff && [git-version < "1.6.6"]} {
347 if {$w eq $ui_index} {
348 set cmd [list submodule summary --cached -- $path]
349 } else {
350 set cmd [list submodule summary --files -- $path]
351 }
352 }
353
354 if {[catch {set fd [eval git_read --nice $cmd]} err]} {
355 set diff_active 0
356 unlock_index
357 ui_status [mc "Unable to display %s" [escape_path $path]]
358 error_popup [strcat [mc "Error loading diff:"] "\n\n$err"]
359 return
360 }
361
362 set ::current_diff_inheader 1
363 fconfigure $fd \
364 -blocking 0 \
365 -encoding [get_path_encoding $path] \
366 -translation lf
367 fileevent $fd readable [list read_diff $fd $conflict_size $cont_info]
368}
369
370proc parse_color_line {line} {
371 set start 0
372 set result ""
373 set markup [list]
374 set regexp {\033\[((?:\d+;)*\d+)?m}
375 set need_reset 0
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}]]
379 set pos [string length $result]
380 set col [eval [linsert $code 0 string range $line]]
381 set start [incr end]
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
389 }
390 append result [string range $line $start end]
391 if {[llength $markup] < 4} {set markup {}}
392 return [list $result $markup]
393}
394
395proc read_diff {fd conflict_size cont_info} {
396 global ui_diff diff_active is_submodule_diff
397 global is_3way_diff is_conflict_diff current_diff_header
398 global current_diff_queue
399 global diff_empty_count
400
401 $ui_diff conf -state normal
402 while {[gets $fd line] >= 0} {
403 foreach {line markup} [parse_color_line $line] break
404 set line [string map {\033 ^} $line]
405
406 set tags {}
407
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).
416 #
417 if {[regexp {^@@+ } $line]} {set ::current_diff_inheader 0}
418
419 # -- Automatically detect if this is a 3 way diff.
420 #
421 if {[string match {@@@ *} $line]} {
422 set is_3way_diff 1
423 apply_tab_size 1
424 }
425
426 if {$::current_diff_inheader} {
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 }
435
436 # -- Cleanup uninteresting diff header lines.
437 #
438 if { [string match {diff --git *} $line]
439 || [string match {diff --cc *} $line]
440 || [string match {diff --combined *} $line]
441 || [string match {--- *} $line]
442 || [string match {+++ *} $line]
443 || [string match {index *} $line]} {
444 continue
445 }
446
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
450
451 } elseif { $line eq {\ No newline at end of file}} {
452 # -- Handle some special lines
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 {++} {
464 set regexp [string map [list %conflict_size $conflict_size]\
465 {^\+\+([<>=]){%conflict_size}(?: |$)}]
466 if {[regexp $regexp $line _g op]} {
467 set is_conflict_diff 1
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 }
479 } elseif {$is_submodule_diff} {
480 if {$line == ""} continue
481 if {[regexp {^Submodule } $line]} {
482 set tags d_info
483 } elseif {[regexp {^\* } $line]} {
484 set line [string replace $line 0 1 {Submodule }]
485 set tags d_info
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 }
498 } else {
499 set op [string index $line 0]
500 switch -- $op {
501 { } {set tags {}}
502 {@} {set tags d_@}
503 {-} {set tags d_-}
504 {+} {
505 set regexp [string map [list %conflict_size $conflict_size]\
506 {^\+([<>=]){%conflict_size}(?: |$)}]
507 if {[regexp $regexp $line _g op]} {
508 set is_conflict_diff 1
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 }
520 set mark [$ui_diff index "end - 1 line linestart"]
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
526
527 foreach {posbegin colbegin posend colend} $markup {
528 set prefix clr
529 foreach style [lsort -integer [split $colbegin ";"]] {
530 if {$style eq "7"} {append prefix i; continue}
531 if {$style != 4 && ($style < 30 || $style > 47)} {continue}
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 }
537 }
538 $ui_diff conf -state disabled
539
540 if {[eof $fd]} {
541 close $fd
542
543 if {$current_diff_queue ne {}} {
544 advance_diff_queue $cont_info
545 return
546 }
547
548 set diff_active 0
549 unlock_index
550 set scroll_pos [lindex $cont_info 0]
551 if {$scroll_pos ne {}} {
552 update
553 $ui_diff yview moveto $scroll_pos
554 }
555 ui_ready
556
557 if {[$ui_diff index end] eq {2.0}} {
558 handle_empty_diff
559 } else {
560 set diff_empty_count 0
561 }
562
563 set callback [lindex $cont_info 1]
564 if {$callback ne {}} {
565 eval $callback
566 }
567 }
568}
569
570proc apply_or_revert_hunk {x y revert} {
571 global current_diff_path current_diff_header current_diff_side
572 global ui_diff ui_index file_states last_revert last_revert_enc
573
574 if {$current_diff_path eq {} || $current_diff_header eq {}} return
575 if {![lock_index apply_hunk]} return
576
577 set apply_cmd {apply --whitespace=nowarn}
578 set mi [lindex $file_states($current_diff_path) 0]
579 if {$current_diff_side eq $ui_index} {
580 set failed_msg [mc "Failed to unstage selected hunk."]
581 lappend apply_cmd --reverse --cached
582 if {[string index $mi 0] ne {M}} {
583 unlock_index
584 return
585 }
586 } else {
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
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
613 set wholepatch "$current_diff_header[$ui_diff get $s_lno $e_lno]"
614
615 if {[catch {
616 set enc [get_path_encoding $current_diff_path]
617 set p [eval git_write $apply_cmd]
618 fconfigure $p -translation binary -encoding $enc
619 puts -nonewline $p $wholepatch
620 close $p} err]} {
621 error_popup "$failed_msg\n\n$err"
622 unlock_index
623 return
624 }
625
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
632 $ui_diff conf -state normal
633 $ui_diff delete $s_lno $e_lno
634 $ui_diff conf -state disabled
635
636 # Check if the hunk was the last one in the file.
637 if {[$ui_diff get 1.0 end] eq "\n"} {
638 set o _
639 } else {
640 set o ?
641 }
642
643 # Update the status flags.
644 if {$revert} {
645 set mi [string index $mi 0]$o
646 } elseif {$current_diff_side eq $ui_index} {
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
655 # This should trigger shift to the next changed file
656 if {$o eq {_}} {
657 reshow_diff
658 }
659}
660
661proc apply_or_revert_range_or_line {x y revert} {
662 global current_diff_path current_diff_header current_diff_side
663 global ui_diff ui_index file_states last_revert
664
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
678 if {$current_diff_path eq {} || $current_diff_header eq {}} return
679 if {![lock_index apply_hunk]} return
680
681 set apply_cmd {apply --whitespace=nowarn}
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 {+}
686 lappend apply_cmd --reverse --cached
687 if {[string index $mi 0] ne {M}} {
688 unlock_index
689 return
690 }
691 } else {
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
702 if {[string index $mi 1] ne {M}} {
703 unlock_index
704 return
705 }
706 }
707
708 set wholepatch {}
709
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
722
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]
727
728 # There is a special situation to take care of. Consider this
729 # hunk:
730 #
731 # @@ -10,4 +10,4 @@
732 # context before
733 # -old 1
734 # -old 2
735 # +new 1
736 # +new 2
737 # context after
738 #
739 # We used to keep the context lines in the order they appear in
740 # the hunk. But then it is not possible to correctly stage only
741 # "-old 1" and "+new 1" - it would result in this staged text:
742 #
743 # context before
744 # old 2
745 # new 1
746 # context after
747 #
748 # (By symmetry it is not possible to *un*stage "old 2" and "new
749 # 2".)
750 #
751 # We resolve the problem by introducing an asymmetry, namely,
752 # when a "+" line is *staged*, it is moved in front of the
753 # context lines that are generated from the "-" lines that are
754 # immediately before the "+" block. That is, we construct this
755 # patch:
756 #
757 # @@ -10,4 +10,5 @@
758 # context before
759 # +new 1
760 # old 1
761 # old 2
762 # context after
763 #
764 # But we do *not* treat "-" lines that are *un*staged in a
765 # special way.
766 #
767 # With this asymmetry it is possible to stage the change "old
768 # 1" -> "new 1" directly, and to stage the change "old 2" ->
769 # "new 2" by first staging the entire hunk and then unstaging
770 # the change "old 1" -> "new 1".
771 #
772 # Applying multiple lines adds complexity to the special
773 # situation. The pre_context must be moved after the entire
774 # first block of consecutive staged "+" lines, so that
775 # staging both additions gives the following patch:
776 #
777 # @@ -10,4 +10,6 @@
778 # context before
779 # +new 1
780 # +new 2
781 # old 1
782 # old 2
783 # context after
784
785 # This is non-empty if and only if we are _staging_ changes;
786 # then it accumulates the consecutive "-" lines (after
787 # converting them to context lines) in order to be moved after
788 # "+" change lines.
789 set pre_context {}
790
791 set n 0
792 set m 0
793 set i_l [$ui_diff index "$i_l + 1 lines"]
794 set patch {}
795 while {[$ui_diff compare $i_l < "end - 1 chars"] &&
796 [$ui_diff get $i_l "$i_l + 2 chars"] ne {@@}} {
797 set next_l [$ui_diff index "$i_l + 1 lines"]
798 set c1 [$ui_diff get $i_l]
799 if {[$ui_diff compare $first_l <= $i_l] &&
800 [$ui_diff compare $i_l < $last_l] &&
801 ($c1 eq {-} || $c1 eq {+})} {
802 # a line to stage/unstage
803 set ln [$ui_diff get $i_l $next_l]
804 if {$c1 eq {-}} {
805 set n [expr $n+1]
806 set patch "$patch$pre_context$ln"
807 set pre_context {}
808 } else {
809 set m [expr $m+1]
810 set patch "$patch$ln"
811 }
812 } elseif {$c1 ne {-} && $c1 ne {+}} {
813 # context line
814 set ln [$ui_diff get $i_l $next_l]
815 set patch "$patch$pre_context$ln"
816 # Skip the "\ No newline at end of
817 # file". Depending on the locale setting
818 # we don't know what this line looks
819 # like exactly. The only thing we do
820 # know is that it starts with "\ "
821 if {![string match {\\ *} $ln]} {
822 set n [expr $n+1]
823 set m [expr $m+1]
824 }
825 set pre_context {}
826 } elseif {$c1 eq $to_context} {
827 # turn change line into context line
828 set ln [$ui_diff get "$i_l + 1 chars" $next_l]
829 if {$c1 eq {-}} {
830 set pre_context "$pre_context $ln"
831 } else {
832 set patch "$patch $ln"
833 }
834 set n [expr $n+1]
835 set m [expr $m+1]
836 } else {
837 # a change in the opposite direction of
838 # to_context which is outside the range of
839 # lines to apply.
840 set patch "$patch$pre_context"
841 set pre_context {}
842 }
843 set i_l $next_l
844 }
845 set patch "$patch$pre_context"
846 set wholepatch "$wholepatch@@ -$hln,$n +$hln,$m @@\n$patch"
847 set first_l [$ui_diff index "$next_l + 1 lines"]
848 }
849
850 if {[catch {
851 set enc [get_path_encoding $current_diff_path]
852 set p [eval git_write $apply_cmd]
853 fconfigure $p -translation binary -encoding $enc
854 puts -nonewline $p $current_diff_header
855 puts -nonewline $p $wholepatch
856 close $p} err]} {
857 error_popup "$failed_msg\n\n$err"
858 unlock_index
859 return
860 }
861
862 if {$revert} {
863 # Save a copy of this patch for undoing reverts.
864 set last_revert $current_diff_header$wholepatch
865 set last_revert_enc $enc
866 }
867
868 unlock_index
869}
870
871# Undo the last line/hunk reverted. When hunks and lines are reverted, a copy
872# of the diff applied is saved. Re-apply that diff to undo the revert.
873#
874# Right now, we only use a single variable to hold the copy, and not a
875# stack/deque for simplicity, so multiple undos are not possible. Maybe this
876# can be added if the need for something like this is felt in the future.
877proc undo_last_revert {} {
878 global last_revert current_diff_path current_diff_header
879 global last_revert_enc
880
881 if {$last_revert eq {}} return
882 if {![lock_index apply_hunk]} return
883
884 set apply_cmd {apply --whitespace=nowarn}
885 set failed_msg [mc "Failed to undo last revert."]
886
887 if {[catch {
888 set enc $last_revert_enc
889 set p [eval git_write $apply_cmd]
890 fconfigure $p -translation binary -encoding $enc
891 puts -nonewline $p $last_revert
892 close $p} err]} {
893 error_popup "$failed_msg\n\n$err"
894 unlock_index
895 return
896 }
897
898 set last_revert {}
899
900 unlock_index
901}