Skip to content

Commit a25c518

Browse files
committed
git-gui: Allow staging/unstaging individual diff hunks.
Just like `git-add --interactive` we can now stage and unstage individual hunks within a file, rather than the entire file at once. This works on the basic idea of scanning backwards from the mouse position to find the hunk header, then going forwards to find the end of the hunk. Everything in that is sent to `git apply --cached`, prefixed by the diff header lines. We ignore whitespace errors while applying a hunk, as we expect the user's pre-commit hook to catch any possible problems. This matches our existing behavior with regards to adding an entire file with no whitespace error checking. Applying hunks means that we now have to capture and save the diff header lines, rather than chucking them. Not really a big deal, we just needed a new global to hang onto that current header information. We probably could have recreated it on demand during apply_hunk but that would mean we need to implement all of the funny rules about how to encode weird path names (e.g. ones containing LF) into a diff header so that the `git apply` process would understand what we are asking it to do. Much simpler to just store this small amount of data in a global and replay it when needed. I'm making absolutely no attempt to correct the line numbers on the remaining hunk headers after one hunk has been applied. This may cause some hunks to fail, as the position information would not be correct. Users can always refresh the current diff before applying a failing hunk to work around the issue. Perhaps if we ever implement hunk splitting we could also fix the remaining hunk headers. Applying hunks directly means that we need to process the diff data in binary, rather than using the system encoding and an automatic linefeed translation. This ensures that CRLF formatted files will be able to be fed directly to `git apply` without failures. Unfortunately it also means we will see CRs show up in the GUI as ugly little boxes at the end of each line in a CRLF file. Signed-off-by: Shawn O. Pearce <[email protected]>
1 parent 86773d9 commit a25c518

File tree

1 file changed

+110
-12
lines changed

1 file changed

+110
-12
lines changed

git-gui.sh

Lines changed: 110 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -545,13 +545,15 @@ proc prune_selection {} {
545545
## diff
546546

547547
proc clear_diff {} {
548-
global ui_diff current_diff_path ui_index ui_workdir
548+
global ui_diff current_diff_path current_diff_header
549+
global ui_index ui_workdir
549550

550551
$ui_diff conf -state normal
551552
$ui_diff delete 0.0 end
552553
$ui_diff conf -state disabled
553554

554555
set current_diff_path {}
556+
set current_diff_header {}
555557

556558
$ui_index tag remove in_diff 0.0 end
557559
$ui_workdir tag remove in_diff 0.0 end
@@ -599,7 +601,7 @@ proc show_diff {path w {lno {}}} {
599601
global file_states file_lists
600602
global is_3way_diff diff_active repo_config
601603
global ui_diff ui_status_value ui_index ui_workdir
602-
global current_diff_path current_diff_side
604+
global current_diff_path current_diff_side current_diff_header
603605

604606
if {$diff_active || ![lock_index read]} return
605607

@@ -623,6 +625,7 @@ proc show_diff {path w {lno {}}} {
623625
set diff_active 1
624626
set current_diff_path $path
625627
set current_diff_side $w
628+
set current_diff_header {}
626629
set ui_status_value "Loading diff of [escape_path $path]..."
627630

628631
# - Git won't give us the diff, there's nothing to compare to!
@@ -707,22 +710,30 @@ proc show_diff {path w {lno {}}} {
707710
return
708711
}
709712

710-
fconfigure $fd -blocking 0 -translation auto
713+
fconfigure $fd \
714+
-blocking 0 \
715+
-encoding binary \
716+
-translation binary
711717
fileevent $fd readable [list read_diff $fd]
712718
}
713719

714720
proc read_diff {fd} {
715-
global ui_diff ui_status_value is_3way_diff diff_active
721+
global ui_diff ui_status_value diff_active
722+
global is_3way_diff current_diff_header
716723

717724
$ui_diff conf -state normal
718725
while {[gets $fd line] >= 0} {
719726
# -- Cleanup uninteresting diff header lines.
720727
#
721-
if {[string match {diff --git *} $line]} continue
722-
if {[string match {diff --cc *} $line]} continue
723-
if {[string match {diff --combined *} $line]} continue
724-
if {[string match {--- *} $line]} continue
725-
if {[string match {+++ *} $line]} continue
728+
if { [string match {diff --git *} $line]
729+
|| [string match {diff --cc *} $line]
730+
|| [string match {diff --combined *} $line]
731+
|| [string match {--- *} $line]
732+
|| [string match {+++ *} $line]} {
733+
append current_diff_header $line "\n"
734+
continue
735+
}
736+
if {[string match {index *} $line]} continue
726737
if {$line eq {deleted file mode 120000}} {
727738
set line "deleted symlink"
728739
}
@@ -731,8 +742,7 @@ proc read_diff {fd} {
731742
#
732743
if {[string match {@@@ *} $line]} {set is_3way_diff 1}
733744

734-
if {[string match {index *} $line]
735-
|| [string match {mode *} $line]
745+
if {[string match {mode *} $line]
736746
|| [string match {new file *} $line]
737747
|| [string match {deleted file *} $line]
738748
|| [string match {Binary files * and * differ} $line]
@@ -799,6 +809,77 @@ proc read_diff {fd} {
799809
}
800810
}
801811

812+
proc apply_hunk {x y} {
813+
global current_diff_path current_diff_header current_diff_side
814+
global ui_diff ui_index file_states
815+
816+
if {$current_diff_path eq {} || $current_diff_header eq {}} return
817+
if {![lock_index apply_hunk]} return
818+
819+
set apply_cmd {git apply --cached --whitespace=nowarn}
820+
set mi [lindex $file_states($current_diff_path) 0]
821+
if {$current_diff_side eq $ui_index} {
822+
set mode unstage
823+
lappend apply_cmd --reverse
824+
if {[string index $mi 0] ne {M}} {
825+
unlock_index
826+
return
827+
}
828+
} else {
829+
set mode stage
830+
if {[string index $mi 1] ne {M}} {
831+
unlock_index
832+
return
833+
}
834+
}
835+
836+
set s_lno [lindex [split [$ui_diff index @$x,$y] .] 0]
837+
set s_lno [$ui_diff search -backwards -regexp ^@@ $s_lno.0 0.0]
838+
if {$s_lno eq {}} {
839+
unlock_index
840+
return
841+
}
842+
843+
set e_lno [$ui_diff search -forwards -regexp ^@@ "$s_lno + 1 lines" end]
844+
if {$e_lno eq {}} {
845+
set e_lno end
846+
}
847+
848+
if {[catch {
849+
set p [open "| $apply_cmd" w]
850+
fconfigure $p -translation binary -encoding binary
851+
puts -nonewline $p $current_diff_header
852+
puts -nonewline $p [$ui_diff get $s_lno $e_lno]
853+
close $p} err]} {
854+
error_popup "Failed to $mode selected hunk.\n\n$err"
855+
unlock_index
856+
return
857+
}
858+
859+
$ui_diff conf -state normal
860+
$ui_diff delete $s_lno $e_lno
861+
$ui_diff conf -state disabled
862+
863+
if {[$ui_diff get 1.0 end] eq "\n"} {
864+
set o _
865+
} else {
866+
set o ?
867+
}
868+
869+
if {$current_diff_side eq $ui_index} {
870+
set mi ${o}M
871+
} elseif {[string index $mi 0] eq {_}} {
872+
set mi M$o
873+
} else {
874+
set mi ?$o
875+
}
876+
unlock_index
877+
display_file $current_diff_path $mi
878+
if {$o eq {_}} {
879+
clear_diff
880+
}
881+
}
882+
802883
######################################################################
803884
##
804885
## commit
@@ -4142,6 +4223,7 @@ bind_button3 $ui_comm "tk_popup $ctxm %X %Y"
41424223
# -- Diff Header
41434224
#
41444225
set current_diff_path {}
4226+
set current_diff_side {}
41454227
set diff_actions [list]
41464228
proc trace_current_diff_path {varname args} {
41474229
global current_diff_path diff_actions file_states
@@ -4282,6 +4364,13 @@ $ctxm add command \
42824364
}
42834365
lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
42844366
$ctxm add separator
4367+
$ctxm add command \
4368+
-label {Apply/Reverse Hunk} \
4369+
-font font_ui \
4370+
-command {apply_hunk $cursorX $cursorY}
4371+
set ui_diff_applyhunk [$ctxm index last]
4372+
lappend diff_actions [list $ctxm entryconf $ui_diff_applyhunk -state]
4373+
$ctxm add separator
42854374
$ctxm add command \
42864375
-label {Decrease Font Size} \
42874376
-font font_ui \
@@ -4313,7 +4402,16 @@ $ctxm add separator
43134402
$ctxm add command -label {Options...} \
43144403
-font font_ui \
43154404
-command do_options
4316-
bind_button3 $ui_diff "tk_popup $ctxm %X %Y"
4405+
bind_button3 $ui_diff "
4406+
set cursorX %x
4407+
set cursorY %y
4408+
if {\$ui_index eq \$current_diff_side} {
4409+
$ctxm entryconf $ui_diff_applyhunk -label {Unstage Hunk From Commit}
4410+
} else {
4411+
$ctxm entryconf $ui_diff_applyhunk -label {Stage Hunk For Commit}
4412+
}
4413+
tk_popup $ctxm %X %Y
4414+
"
43174415

43184416
# -- Status Bar
43194417
#

0 commit comments

Comments
 (0)