Browse Source

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 <spearce@spearce.org>
maint
Shawn O. Pearce 18 years ago
parent
commit
a25c518933
  1. 122
      git-gui.sh

122
git-gui.sh

@ -545,13 +545,15 @@ proc prune_selection {} { @@ -545,13 +545,15 @@ proc prune_selection {} {
## diff

proc clear_diff {} {
global ui_diff current_diff_path ui_index ui_workdir
global ui_diff current_diff_path current_diff_header
global ui_index ui_workdir

$ui_diff conf -state normal
$ui_diff delete 0.0 end
$ui_diff conf -state disabled

set current_diff_path {}
set current_diff_header {}

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

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

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

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

fconfigure $fd -blocking 0 -translation auto
fconfigure $fd \
-blocking 0 \
-encoding binary \
-translation binary
fileevent $fd readable [list read_diff $fd]
}

proc read_diff {fd} {
global ui_diff ui_status_value is_3way_diff diff_active
global ui_diff ui_status_value diff_active
global is_3way_diff current_diff_header

$ui_diff conf -state normal
while {[gets $fd line] >= 0} {
# -- Cleanup uninteresting diff header lines.
#
if {[string match {diff --git *} $line]} continue
if {[string match {diff --cc *} $line]} continue
if {[string match {diff --combined *} $line]} continue
if {[string match {--- *} $line]} continue
if {[string match {+++ *} $line]} continue
if { [string match {diff --git *} $line]
|| [string match {diff --cc *} $line]
|| [string match {diff --combined *} $line]
|| [string match {--- *} $line]
|| [string match {+++ *} $line]} {
append current_diff_header $line "\n"
continue
}
if {[string match {index *} $line]} continue
if {$line eq {deleted file mode 120000}} {
set line "deleted symlink"
}
@ -731,8 +742,7 @@ proc read_diff {fd} { @@ -731,8 +742,7 @@ proc read_diff {fd} {
#
if {[string match {@@@ *} $line]} {set is_3way_diff 1}

if {[string match {index *} $line]
|| [string match {mode *} $line]
if {[string match {mode *} $line]
|| [string match {new file *} $line]
|| [string match {deleted file *} $line]
|| [string match {Binary files * and * differ} $line]
@ -799,6 +809,77 @@ proc read_diff {fd} { @@ -799,6 +809,77 @@ proc read_diff {fd} {
}
}

proc apply_hunk {x y} {
global current_diff_path current_diff_header current_diff_side
global ui_diff ui_index file_states

if {$current_diff_path eq {} || $current_diff_header eq {}} return
if {![lock_index apply_hunk]} return

set apply_cmd {git apply --cached --whitespace=nowarn}
set mi [lindex $file_states($current_diff_path) 0]
if {$current_diff_side eq $ui_index} {
set mode unstage
lappend apply_cmd --reverse
if {[string index $mi 0] ne {M}} {
unlock_index
return
}
} else {
set mode stage
if {[string index $mi 1] ne {M}} {
unlock_index
return
}
}

set s_lno [lindex [split [$ui_diff index @$x,$y] .] 0]
set s_lno [$ui_diff search -backwards -regexp ^@@ $s_lno.0 0.0]
if {$s_lno eq {}} {
unlock_index
return
}

set e_lno [$ui_diff search -forwards -regexp ^@@ "$s_lno + 1 lines" end]
if {$e_lno eq {}} {
set e_lno end
}

if {[catch {
set p [open "| $apply_cmd" w]
fconfigure $p -translation binary -encoding binary
puts -nonewline $p $current_diff_header
puts -nonewline $p [$ui_diff get $s_lno $e_lno]
close $p} err]} {
error_popup "Failed to $mode selected hunk.\n\n$err"
unlock_index
return
}

$ui_diff conf -state normal
$ui_diff delete $s_lno $e_lno
$ui_diff conf -state disabled

if {[$ui_diff get 1.0 end] eq "\n"} {
set o _
} else {
set o ?
}

if {$current_diff_side eq $ui_index} {
set mi ${o}M
} elseif {[string index $mi 0] eq {_}} {
set mi M$o
} else {
set mi ?$o
}
unlock_index
display_file $current_diff_path $mi
if {$o eq {_}} {
clear_diff
}
}

######################################################################
##
## commit
@ -4142,6 +4223,7 @@ bind_button3 $ui_comm "tk_popup $ctxm %X %Y" @@ -4142,6 +4223,7 @@ bind_button3 $ui_comm "tk_popup $ctxm %X %Y"
# -- Diff Header
#
set current_diff_path {}
set current_diff_side {}
set diff_actions [list]
proc trace_current_diff_path {varname args} {
global current_diff_path diff_actions file_states
@ -4282,6 +4364,13 @@ $ctxm add command \ @@ -4282,6 +4364,13 @@ $ctxm add command \
}
lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
$ctxm add separator
$ctxm add command \
-label {Apply/Reverse Hunk} \
-font font_ui \
-command {apply_hunk $cursorX $cursorY}
set ui_diff_applyhunk [$ctxm index last]
lappend diff_actions [list $ctxm entryconf $ui_diff_applyhunk -state]
$ctxm add separator
$ctxm add command \
-label {Decrease Font Size} \
-font font_ui \
@ -4313,7 +4402,16 @@ $ctxm add separator @@ -4313,7 +4402,16 @@ $ctxm add separator
$ctxm add command -label {Options...} \
-font font_ui \
-command do_options
bind_button3 $ui_diff "tk_popup $ctxm %X %Y"
bind_button3 $ui_diff "
set cursorX %x
set cursorY %y
if {\$ui_index eq \$current_diff_side} {
$ctxm entryconf $ui_diff_applyhunk -label {Unstage Hunk From Commit}
} else {
$ctxm entryconf $ui_diff_applyhunk -label {Stage Hunk For Commit}
}
tk_popup $ctxm %X %Y
"

# -- Status Bar
#

Loading…
Cancel
Save