From ea41cfc4f54f884582dbda307287f12bb1fc15e9 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 27 Jul 2009 20:37:10 +0200 Subject: Make 'git stash -k' a short form for 'git stash save --keep-index' To save me from the carpal tunnel syndrome, make 'git stash' accept the short option '-k' instead of '--keep-index', and for even more convenience, let's DWIM when this developer forgot to type the 'save' command. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano diff --git a/Documentation/git-stash.txt b/Documentation/git-stash.txt index 1c64a02..a031836 100644 --- a/Documentation/git-stash.txt +++ b/Documentation/git-stash.txt @@ -13,7 +13,8 @@ SYNOPSIS 'git stash' drop [-q|--quiet] [] 'git stash' ( pop | apply ) [--index] [-q|--quiet] [] 'git stash' branch [] -'git stash' [save [--keep-index] [-q|--quiet] []] +'git stash' [save [-k|--keep-index] [-q|--quiet] []] +'git stash' [-k|--keep-index] 'git stash' clear 'git stash' create diff --git a/git-stash.sh b/git-stash.sh index 03e589f..13edc0e 100755 --- a/git-stash.sh +++ b/git-stash.sh @@ -7,7 +7,8 @@ USAGE="list [] or: $dashless drop [-q|--quiet] [] or: $dashless ( pop | apply ) [--index] [-q|--quiet] [] or: $dashless branch [] - or: $dashless [save [--keep-index] [-q|--quiet] []] + or: $dashless [save [-k|--keep-index] [-q|--quiet] []] + or: $dashless [-k|--keep-index] or: $dashless clear" SUBDIRECTORY_OK=Yes @@ -98,7 +99,7 @@ save_stash () { while test $# != 0 do case "$1" in - --keep-index) + -k|--keep-index) keep_index=t ;; -q|--quiet) @@ -353,12 +354,13 @@ branch) apply_to_branch "$@" ;; *) - if test $# -eq 0 - then - save_stash && + case $#,"$1" in + 0,|1,-k|1,--keep-index) + save_stash "$@" && say '(To restore them type "git stash apply")' - else + ;; + *) usage - fi + esac ;; esac diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh index 7a3fb67..e16ad93 100755 --- a/t/t3903-stash.sh +++ b/t/t3903-stash.sh @@ -200,4 +200,12 @@ test_expect_success 'drop -q is quiet' ' test ! -s output.out ' +test_expect_success 'stash -k' ' + echo bar3 > file && + echo bar4 > file2 && + git add file2 && + git stash -k && + test bar,bar4 = $(cat file),$(cat file2) +' + test_done -- cgit v0.10.2-6-g49f6 From 8f0bef6df91c48d79f982bdb55f784ce445ba5b2 Mon Sep 17 00:00:00 2001 From: Thomas Rast Date: Thu, 13 Aug 2009 14:29:39 +0200 Subject: git-apply--interactive: Refactor patch mode code This makes some aspects of the 'git add -p' loop configurable (within the code), so that we can later reuse git-add--interactive for other similar tools. Most fields are fairly straightforward, but APPLY gets a subroutine (instead of just a string a la 'apply --cached') so that we can handle 'checkout -p', which will need to atomically apply the patch twice (index and worktree). Signed-off-by: Thomas Rast Signed-off-by: Junio C Hamano diff --git a/git-add--interactive.perl b/git-add--interactive.perl index df9f231..3606103 100755 --- a/git-add--interactive.perl +++ b/git-add--interactive.perl @@ -73,6 +73,22 @@ sub colored { # command line options my $patch_mode; +sub apply_patch; + +my %patch_modes = ( + 'stage' => { + DIFF => 'diff-files -p', + APPLY => sub { apply_patch 'apply --cached', @_; }, + APPLY_CHECK => 'apply --cached', + VERB => 'Stage', + TARGET => '', + PARTICIPLE => 'staging', + FILTER => 'file-only', + }, +); + +my %patch_mode_flavour = %{$patch_modes{stage}}; + sub run_cmd_pipe { if ($^O eq 'MSWin32' || $^O eq 'msys') { my @invalid = grep {m/[":*]/} @_; @@ -613,12 +629,21 @@ sub add_untracked_cmd { print "\n"; } +sub run_git_apply { + my $cmd = shift; + my $fh; + open $fh, '| git ' . $cmd; + print $fh @_; + return close $fh; +} + sub parse_diff { my ($path) = @_; - my @diff = run_cmd_pipe(qw(git diff-files -p --), $path); + my @diff_cmd = split(" ", $patch_mode_flavour{DIFF}); + my @diff = run_cmd_pipe("git", @diff_cmd, "--", $path); my @colored = (); if ($diff_use_color) { - @colored = run_cmd_pipe(qw(git diff-files -p --color --), $path); + @colored = run_cmd_pipe("git", @diff_cmd, qw(--color --), $path); } my (@hunk) = { TEXT => [], DISPLAY => [], TYPE => 'header' }; @@ -877,6 +902,7 @@ sub edit_hunk_manually { or die "failed to open hunk edit file for writing: " . $!; print $fh "# Manual hunk edit mode -- see bottom for a quick guide\n"; print $fh @$oldtext; + my $participle = $patch_mode_flavour{PARTICIPLE}; print $fh <{TEXT}}; - } - return close $fh; + return run_git_apply($patch_mode_flavour{APPLY_CHECK} . ' --recount --check', + map { @{$_->{TEXT}} } @_); } sub _restore_terminal_and_die { @@ -988,12 +1011,14 @@ sub edit_hunk_loop { } sub help_patch_cmd { - print colored $help_color, <<\EOF ; -y - stage this hunk -n - do not stage this hunk -q - quit, do not stage this hunk nor any of the remaining ones -a - stage this and all the remaining hunks in the file -d - do not stage this hunk nor any of the remaining hunks in the file + my $verb = lc $patch_mode_flavour{VERB}; + my $target = $patch_mode_flavour{TARGET}; + print colored $help_color, <{BINARY}) } @all_mods; my @them; @@ -1138,8 +1172,9 @@ sub patch_update_file { for (@{$hunk[$ix]{DISPLAY}}) { print; } - print colored $prompt_color, 'Stage ', - ($hunk[$ix]{TYPE} eq 'mode' ? 'mode change' : 'this hunk'), + print colored $prompt_color, $patch_mode_flavour{VERB}, + ($hunk[$ix]{TYPE} eq 'mode' ? ' mode change' : ' this hunk'), + $patch_mode_flavour{TARGET}, " [y,n,q,a,d,/$other,?]? "; my $line = prompt_single_character; if ($line) { @@ -1313,16 +1348,9 @@ sub patch_update_file { if (@result) { my $fh; - - open $fh, '| git apply --cached --recount'; - for (@{$head->{TEXT}}, @result) { - print $fh $_; - } - if (!close $fh) { - for (@{$head->{TEXT}}, @result) { - print STDERR $_; - } - } + my @patch = (@{$head->{TEXT}}, @result); + my $apply_routine = $patch_mode_flavour{APPLY}; + &$apply_routine(@patch); refresh(); } -- cgit v0.10.2-6-g49f6 From b319ef70a94731a5c6f18d07a49d5dda3f06f5d3 Mon Sep 17 00:00:00 2001 From: Thomas Rast Date: Thu, 13 Aug 2009 14:29:40 +0200 Subject: Add a small patch-mode testing library The tests for {reset,commit,stash} -p will frequently have to set both worktree and index states to known values, and verify that the outcome (again both worktree and index) are what was expected. Add a small helper library that lets us do these tasks more easily. Signed-off-by: Thomas Rast Signed-off-by: Junio C Hamano diff --git a/t/lib-patch-mode.sh b/t/lib-patch-mode.sh new file mode 100755 index 0000000..afb4b66 --- /dev/null +++ b/t/lib-patch-mode.sh @@ -0,0 +1,36 @@ +. ./test-lib.sh + +set_state () { + echo "$3" > "$1" && + git add "$1" && + echo "$2" > "$1" +} + +save_state () { + noslash="$(echo "$1" | tr / _)" && + cat "$1" > _worktree_"$noslash" && + git show :"$1" > _index_"$noslash" +} + +set_and_save_state () { + set_state "$@" && + save_state "$1" +} + +verify_state () { + test "$(cat "$1")" = "$2" && + test "$(git show :"$1")" = "$3" +} + +verify_saved_state () { + noslash="$(echo "$1" | tr / _)" && + verify_state "$1" "$(cat _worktree_"$noslash")" "$(cat _index_"$noslash")" +} + +save_head () { + git rev-parse HEAD > _head +} + +verify_saved_head () { + test "$(cat _head)" = "$(git rev-parse HEAD)" +} -- cgit v0.10.2-6-g49f6 From 46b5139cae7306194a39fdaf5c6abc12ab531c84 Mon Sep 17 00:00:00 2001 From: Thomas Rast Date: Thu, 13 Aug 2009 14:29:41 +0200 Subject: builtin-add: refactor the meat of interactive_add() This moves the call setup for 'git add--interactive' to a separate function, as other users will call it without running validate_pathspec() first. Signed-off-by: Thomas Rast Signed-off-by: Junio C Hamano diff --git a/builtin-add.c b/builtin-add.c index 581a2a1..c422a62 100644 --- a/builtin-add.c +++ b/builtin-add.c @@ -131,27 +131,27 @@ static const char **validate_pathspec(int argc, const char **argv, const char *p return pathspec; } -int interactive_add(int argc, const char **argv, const char *prefix) +int run_add_interactive(const char *revision, const char *patch_mode, + const char **pathspec) { - int status, ac; + int status, ac, pc = 0; const char **args; - const char **pathspec = NULL; - if (argc) { - pathspec = validate_pathspec(argc, argv, prefix); - if (!pathspec) - return -1; - } + if (pathspec) + while (pathspec[pc]) + pc++; - args = xcalloc(sizeof(const char *), (argc + 4)); + args = xcalloc(sizeof(const char *), (pc + 5)); ac = 0; args[ac++] = "add--interactive"; - if (patch_interactive) - args[ac++] = "--patch"; + if (patch_mode) + args[ac++] = patch_mode; + if (revision) + args[ac++] = revision; args[ac++] = "--"; - if (argc) { - memcpy(&(args[ac]), pathspec, sizeof(const char *) * argc); - ac += argc; + if (pc) { + memcpy(&(args[ac]), pathspec, sizeof(const char *) * pc); + ac += pc; } args[ac] = NULL; @@ -160,6 +160,21 @@ int interactive_add(int argc, const char **argv, const char *prefix) return status; } +int interactive_add(int argc, const char **argv, const char *prefix) +{ + const char **pathspec = NULL; + + if (argc) { + pathspec = validate_pathspec(argc, argv, prefix); + if (!pathspec) + return -1; + } + + return run_add_interactive(NULL, + patch_interactive ? "--patch" : NULL, + pathspec); +} + static int edit_patch(int argc, const char **argv, const char *prefix) { char *file = xstrdup(git_path("ADD_EDIT.patch")); diff --git a/commit.h b/commit.h index ba9f638..339f1f6 100644 --- a/commit.h +++ b/commit.h @@ -137,6 +137,8 @@ int is_descendant_of(struct commit *, struct commit_list *); int in_merge_bases(struct commit *, struct commit **, int); extern int interactive_add(int argc, const char **argv, const char *prefix); +extern int run_add_interactive(const char *revision, const char *patch_mode, + const char **pathspec); static inline int single_parent(struct commit *commit) { -- cgit v0.10.2-6-g49f6 From d002ef4d9446b9fe4d0c397131edce58781df2f1 Mon Sep 17 00:00:00 2001 From: Thomas Rast Date: Sat, 15 Aug 2009 13:48:31 +0200 Subject: Implement 'git reset --patch' This introduces a --patch mode for git-reset. The basic case is git reset --patch -- [files...] which acts as the opposite of 'git add --patch -- [files...]': it offers hunks for *un*staging. Advanced usage is git reset --patch -- [files...] which offers hunks from the diff between the index and for forward application to the index. (That is, the basic case is just = HEAD.) Signed-off-by: Thomas Rast Signed-off-by: Junio C Hamano diff --git a/Documentation/git-reset.txt b/Documentation/git-reset.txt index abb25d1..469cf6d 100644 --- a/Documentation/git-reset.txt +++ b/Documentation/git-reset.txt @@ -10,6 +10,7 @@ SYNOPSIS [verse] 'git reset' [--mixed | --soft | --hard | --merge] [-q] [] 'git reset' [-q] [] [--] ... +'git reset' --patch [] [--] [...] DESCRIPTION ----------- @@ -23,8 +24,9 @@ the undo in the history. If you want to undo a commit other than the latest on a branch, linkgit:git-revert[1] is your friend. -The second form with 'paths' is used to revert selected paths in -the index from a given commit, without moving HEAD. +The second and third forms with 'paths' and/or --patch are used to +revert selected paths in the index from a given commit, without moving +HEAD. OPTIONS @@ -50,6 +52,15 @@ OPTIONS and updates the files that are different between the named commit and the current commit in the working tree. +-p:: +--patch:: + Interactively select hunks in the difference between the index + and (defaults to HEAD). The chosen hunks are applied + in reverse to the index. ++ +This means that `git reset -p` is the opposite of `git add -p` (see +linkgit:git-add[1]). + -q:: Be quiet, only report errors. diff --git a/builtin-reset.c b/builtin-reset.c index 5fa1789..246a127 100644 --- a/builtin-reset.c +++ b/builtin-reset.c @@ -142,6 +142,17 @@ static void update_index_from_diff(struct diff_queue_struct *q, } } +static int interactive_reset(const char *revision, const char **argv, + const char *prefix) +{ + const char **pathspec = NULL; + + if (*argv) + pathspec = get_pathspec(prefix, argv); + + return run_add_interactive(revision, "--patch=reset", pathspec); +} + static int read_from_tree(const char *prefix, const char **argv, unsigned char *tree_sha1, int refresh_flags) { @@ -183,6 +194,7 @@ static void prepend_reflog_action(const char *action, char *buf, size_t size) int cmd_reset(int argc, const char **argv, const char *prefix) { int i = 0, reset_type = NONE, update_ref_status = 0, quiet = 0; + int patch_mode = 0; const char *rev = "HEAD"; unsigned char sha1[20], *orig = NULL, sha1_orig[20], *old_orig = NULL, sha1_old_orig[20]; @@ -198,6 +210,7 @@ int cmd_reset(int argc, const char **argv, const char *prefix) "reset HEAD, index and working tree", MERGE), OPT_BOOLEAN('q', NULL, &quiet, "disable showing new HEAD in hard reset and progress message"), + OPT_BOOLEAN('p', "patch", &patch_mode, "select hunks interactively"), OPT_END() }; @@ -251,6 +264,12 @@ int cmd_reset(int argc, const char **argv, const char *prefix) die("Could not parse object '%s'.", rev); hashcpy(sha1, commit->object.sha1); + if (patch_mode) { + if (reset_type != NONE) + die("--patch is incompatible with --{hard,mixed,soft}"); + return interactive_reset(rev, argv + i, prefix); + } + /* git reset tree [--] paths... can be used to * load chosen paths from the tree into the index without * affecting the working tree nor HEAD. */ diff --git a/git-add--interactive.perl b/git-add--interactive.perl index 3606103..d14f48c 100755 --- a/git-add--interactive.perl +++ b/git-add--interactive.perl @@ -72,6 +72,7 @@ sub colored { # command line options my $patch_mode; +my $patch_mode_revision; sub apply_patch; @@ -85,6 +86,24 @@ my %patch_modes = ( PARTICIPLE => 'staging', FILTER => 'file-only', }, + 'reset_head' => { + DIFF => 'diff-index -p --cached', + APPLY => sub { apply_patch 'apply -R --cached', @_; }, + APPLY_CHECK => 'apply -R --cached', + VERB => 'Unstage', + TARGET => '', + PARTICIPLE => 'unstaging', + FILTER => 'index-only', + }, + 'reset_nothead' => { + DIFF => 'diff-index -R -p --cached', + APPLY => sub { apply_patch 'apply --cached', @_; }, + APPLY_CHECK => 'apply --cached', + VERB => 'Apply', + TARGET => ' to index', + PARTICIPLE => 'applying', + FILTER => 'index-only', + }, ); my %patch_mode_flavour = %{$patch_modes{stage}}; @@ -206,7 +225,14 @@ sub list_modified { return if (!@tracked); } - my $reference = is_initial_commit() ? get_empty_tree() : 'HEAD'; + my $reference; + if (defined $patch_mode_revision and $patch_mode_revision ne 'HEAD') { + $reference = $patch_mode_revision; + } elsif (is_initial_commit()) { + $reference = get_empty_tree(); + } else { + $reference = 'HEAD'; + } for (run_cmd_pipe(qw(git diff-index --cached --numstat --summary), $reference, '--', @tracked)) { @@ -640,6 +666,9 @@ sub run_git_apply { sub parse_diff { my ($path) = @_; my @diff_cmd = split(" ", $patch_mode_flavour{DIFF}); + if (defined $patch_mode_revision) { + push @diff_cmd, $patch_mode_revision; + } my @diff = run_cmd_pipe("git", @diff_cmd, "--", $path); my @colored = (); if ($diff_use_color) { @@ -1391,11 +1420,31 @@ EOF sub process_args { return unless @ARGV; my $arg = shift @ARGV; - if ($arg eq "--patch") { - $patch_mode = 1; - $arg = shift @ARGV or die "missing --"; + if ($arg =~ /--patch(?:=(.*))?/) { + if (defined $1) { + if ($1 eq 'reset') { + $patch_mode = 'reset_head'; + $patch_mode_revision = 'HEAD'; + $arg = shift @ARGV or die "missing --"; + if ($arg ne '--') { + $patch_mode_revision = $arg; + $patch_mode = ($arg eq 'HEAD' ? + 'reset_head' : 'reset_nothead'); + $arg = shift @ARGV or die "missing --"; + } + } elsif ($1 eq 'stage') { + $patch_mode = 'stage'; + $arg = shift @ARGV or die "missing --"; + } else { + die "unknown --patch mode: $1"; + } + } else { + $patch_mode = 'stage'; + $arg = shift @ARGV or die "missing --"; + } die "invalid argument $arg, expecting --" unless $arg eq "--"; + %patch_mode_flavour = %{$patch_modes{$patch_mode}}; } elsif ($arg ne "--") { die "invalid argument $arg, expecting --"; diff --git a/t/t7105-reset-patch.sh b/t/t7105-reset-patch.sh new file mode 100755 index 0000000..c1f4fc3 --- /dev/null +++ b/t/t7105-reset-patch.sh @@ -0,0 +1,69 @@ +#!/bin/sh + +test_description='git reset --patch' +. ./lib-patch-mode.sh + +test_expect_success 'setup' ' + mkdir dir && + echo parent > dir/foo && + echo dummy > bar && + git add dir && + git commit -m initial && + test_tick && + test_commit second dir/foo head && + set_and_save_state bar bar_work bar_index && + save_head +' + +# note: bar sorts before foo, so the first 'n' is always to skip 'bar' + +test_expect_success 'saying "n" does nothing' ' + set_and_save_state dir/foo work work + (echo n; echo n) | git reset -p && + verify_saved_state dir/foo && + verify_saved_state bar +' + +test_expect_success 'git reset -p' ' + (echo n; echo y) | git reset -p && + verify_state dir/foo work head && + verify_saved_state bar +' + +test_expect_success 'git reset -p HEAD^' ' + (echo n; echo y) | git reset -p HEAD^ && + verify_state dir/foo work parent && + verify_saved_state bar +' + +# The idea in the rest is that bar sorts first, so we always say 'y' +# first and if the path limiter fails it'll apply to bar instead of +# dir/foo. There's always an extra 'n' to reject edits to dir/foo in +# the failure case (and thus get out of the loop). + +test_expect_success 'git reset -p dir' ' + set_state dir/foo work work + (echo y; echo n) | git reset -p dir && + verify_state dir/foo work head && + verify_saved_state bar +' + +test_expect_success 'git reset -p -- foo (inside dir)' ' + set_state dir/foo work work + (echo y; echo n) | (cd dir && git reset -p -- foo) && + verify_state dir/foo work head && + verify_saved_state bar +' + +test_expect_success 'git reset -p HEAD^ -- dir' ' + (echo y; echo n) | git reset -p HEAD^ -- dir && + verify_state dir/foo work parent && + verify_saved_state bar +' + +test_expect_success 'none of this moved HEAD' ' + verify_saved_head +' + + +test_done -- cgit v0.10.2-6-g49f6 From 4f353658b9c15e9188530fac5ae79d0aa1538e85 Mon Sep 17 00:00:00 2001 From: Thomas Rast Date: Sat, 15 Aug 2009 13:48:30 +0200 Subject: Implement 'git checkout --patch' This introduces a --patch mode for git-checkout. In the index usage git checkout --patch -- [files...] it lets the user discard edits from the at the granularity of hunks (by selecting hunks from 'git diff' and then reverse applying them to the worktree). We also accept a revision argument. In the case git checkout --patch HEAD -- [files...] we offer hunks from the difference between HEAD and the worktree, and reverse applies them to both index and worktree, allowing you to discard staged changes completely. In the non-HEAD usage git checkout --patch -- [files...] it offers hunks from the difference between the worktree and . The chosen hunks are then applied to both index and worktree. The application to worktree and index is done "atomically" in the sense that we first check if the patch applies to the index (it should always apply to the worktree). If it does not, we give the user a choice to either abort or apply to the worktree anyway. Signed-off-by: Thomas Rast Signed-off-by: Junio C Hamano diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt index ad4b31e..26a5447 100644 --- a/Documentation/git-checkout.txt +++ b/Documentation/git-checkout.txt @@ -11,6 +11,7 @@ SYNOPSIS 'git checkout' [-q] [-f] [-m] [] 'git checkout' [-q] [-f] [-m] [-b ] [] 'git checkout' [-f|--ours|--theirs|-m|--conflict=