diff options
Diffstat (limited to 'git-stash.sh')
-rwxr-xr-x | git-stash.sh | 426 |
1 files changed, 333 insertions, 93 deletions
diff --git a/git-stash.sh b/git-stash.sh index 7561b374d2..8b2ce9afda 100755 --- a/git-stash.sh +++ b/git-stash.sh @@ -7,17 +7,24 @@ USAGE="list [<options>] or: $dashless drop [-q|--quiet] [<stash>] or: $dashless ( pop | apply ) [--index] [-q|--quiet] [<stash>] or: $dashless branch <branchname> [<stash>] - or: $dashless [save [--patch] [-k|--[no-]keep-index] [-q|--quiet] [<message>]] + or: $dashless save [--patch] [-k|--[no-]keep-index] [-q|--quiet] + [-u|--include-untracked] [-a|--all] [<message>] + or: $dashless [push [--patch] [-k|--[no-]keep-index] [-q|--quiet] + [-u|--include-untracked] [-a|--all] [-m <message>] + [-- <pathspec>...]] or: $dashless clear" SUBDIRECTORY_OK=Yes OPTIONS_SPEC= +START_DIR=$(pwd) . git-sh-setup require_work_tree +prefix=$(git rev-parse --show-prefix) || exit 1 cd_to_toplevel TMP="$GIT_DIR/.git-stash.$$" -trap 'rm -f "$TMP-*"' 0 +TMPindex=${GIT_INDEX_FILE-"$(git rev-parse --git-path index)"}.stash.$$ +trap 'rm -f "$TMP-"* "$TMPindex"' 0 ref_stash=refs/stash @@ -30,26 +37,59 @@ else fi no_changes () { - git diff-index --quiet --cached HEAD --ignore-submodules -- && - git diff-files --quiet --ignore-submodules + git diff-index --quiet --cached HEAD --ignore-submodules -- "$@" && + git diff-files --quiet --ignore-submodules -- "$@" && + (test -z "$untracked" || test -z "$(untracked_files)") +} + +untracked_files () { + if test "$1" = "-z" + then + shift + z=-z + else + z= + fi + excl_opt=--exclude-standard + test "$untracked" = "all" && excl_opt= + git ls-files -o $z $excl_opt -- "$@" } clear_stash () { if test $# != 0 then - die "git stash clear with parameters is unimplemented" + die "$(gettext "git stash clear with parameters is unimplemented")" fi - if current=$(git rev-parse --verify $ref_stash 2>/dev/null) + if current=$(git rev-parse --verify --quiet $ref_stash) then git update-ref -d $ref_stash $current fi } create_stash () { - stash_msg="$1" + stash_msg= + untracked= + while test $# != 0 + do + case "$1" in + -m|--message) + shift + stash_msg=${1?"BUG: create_stash () -m requires an argument"} + ;; + -u|--include-untracked) + shift + untracked=${1?"BUG: create_stash () -u requires an argument"} + ;; + --) + shift + break + ;; + esac + shift + done git update-index -q --refresh - if no_changes + if no_changes "$@" then exit 0 fi @@ -59,7 +99,7 @@ create_stash () { then head=$(git rev-list --oneline -n 1 HEAD --) else - die "You do not have the initial commit yet" + die "$(gettext "You do not have the initial commit yet")" fi if branch=$(git symbolic-ref -q HEAD) @@ -74,23 +114,42 @@ create_stash () { i_tree=$(git write-tree) && i_commit=$(printf 'index on %s\n' "$msg" | git commit-tree $i_tree -p $b_commit) || - die "Cannot save the current index state" + die "$(gettext "Cannot save the current index state")" + + if test -n "$untracked" + then + # Untracked files are stored by themselves in a parentless commit, for + # ease of unpacking later. + u_commit=$( + untracked_files -z "$@" | ( + GIT_INDEX_FILE="$TMPindex" && + export GIT_INDEX_FILE && + rm -f "$TMPindex" && + git update-index -z --add --remove --stdin && + u_tree=$(git write-tree) && + printf 'untracked files on %s\n' "$msg" | git commit-tree $u_tree && + rm -f "$TMPindex" + ) ) || die "$(gettext "Cannot save the untracked files")" + + untracked_commit_option="-p $u_commit"; + else + untracked_commit_option= + fi if test -z "$patch_mode" then # state of the working tree w_tree=$( ( - rm -f "$TMP-index" && - cp -p ${GIT_INDEX_FILE-"$GIT_DIR/index"} "$TMP-index" && - GIT_INDEX_FILE="$TMP-index" && + git read-tree --index-output="$TMPindex" -m $i_tree && + GIT_INDEX_FILE="$TMPindex" && export GIT_INDEX_FILE && - git read-tree -m $i_tree && - git diff --name-only -z HEAD | git update-index -z --add --remove --stdin && + git diff-index --name-only -z HEAD -- "$@" >"$TMP-stagenames" && + git update-index -z --add --remove --stdin <"$TMP-stagenames" && git write-tree && - rm -f "$TMP-index" + rm -f "$TMPindex" ) ) || - die "Cannot save the current worktree state" + die "$(gettext "Cannot save the current worktree state")" else @@ -99,18 +158,18 @@ create_stash () { # find out what the user wants GIT_INDEX_FILE="$TMP-index" \ - git add--interactive --patch=stash -- && + git add--interactive --patch=stash -- "$@" && # state of the working tree w_tree=$(GIT_INDEX_FILE="$TMP-index" git write-tree) || - die "Cannot save the current worktree state" + die "$(gettext "Cannot save the current worktree state")" - git diff-tree -p HEAD $w_tree > "$TMP-patch" && + git diff-tree -p HEAD $w_tree -- >"$TMP-patch" && test -s "$TMP-patch" || - die "No changes selected" + die "$(gettext "No changes selected")" rm -f "$TMP-index" || - die "Cannot remove temporary index (can't happen)" + die "$(gettext "Cannot remove temporary index (can't happen)")" fi @@ -122,13 +181,48 @@ create_stash () { stash_msg=$(printf 'On %s: %s' "$branch" "$stash_msg") fi w_commit=$(printf '%s\n' "$stash_msg" | - git commit-tree $w_tree -p $b_commit -p $i_commit) || - die "Cannot record working tree state" + git commit-tree $w_tree -p $b_commit -p $i_commit $untracked_commit_option) || + die "$(gettext "Cannot record working tree state")" } -save_stash () { +store_stash () { + while test $# != 0 + do + case "$1" in + -m|--message) + shift + stash_msg="$1" + ;; + -q|--quiet) + quiet=t + ;; + *) + break + ;; + esac + shift + done + test $# = 1 || + die "$(eval_gettext "\"$dashless store\" requires one <commit> argument")" + + w_commit="$1" + if test -z "$stash_msg" + then + stash_msg="Created via \"git stash store\"." + fi + + git update-ref --create-reflog -m "$stash_msg" $ref_stash $w_commit + ret=$? + test $ret != 0 && test -z "$quiet" && + die "$(eval_gettext "Cannot update \$ref_stash with \$w_commit")" + return $ret +} + +push_stash () { keep_index= patch_mode= + untracked= + stash_msg= while test $# != 0 do case "$1" in @@ -136,22 +230,48 @@ save_stash () { keep_index=t ;; --no-keep-index) - keep_index= + keep_index=n ;; -p|--patch) patch_mode=t - keep_index=t + # only default to keep if we don't already have an override + test -z "$keep_index" && keep_index=t ;; -q|--quiet) GIT_QUIET=t ;; + -u|--include-untracked) + untracked=untracked + ;; + -a|--all) + untracked=all + ;; + -m|--message) + shift + test -z ${1+x} && usage + stash_msg=$1 + ;; + --help) + show_help + ;; --) shift break ;; -*) - echo "error: unknown option for 'stash save': $1" - echo " To provide a message, use git stash save -- '$1'" + option="$1" + # TRANSLATORS: $option is an invalid option, like + # `--blah-blah'. The 7 spaces at the beginning of the + # second line correspond to "error: ". So you should line + # up the second line with however many characters the + # translation of "error: " takes in your language. E.g. in + # English this is: + # + # $ git stash save --blah-blah 2>&1 | head -n 2 + # error: unknown option for 'stash save': --blah-blah + # To provide a message, use git stash save -- '--blah-blah' + eval_gettextln "error: unknown option for 'stash save': \$option + To provide a message, use git stash save -- '\$option'" usage ;; *) @@ -161,58 +281,132 @@ save_stash () { shift done - stash_msg="$*" + eval "set $(git rev-parse --sq --prefix "$prefix" -- "$@")" + + if test -n "$patch_mode" && test -n "$untracked" + then + die "$(gettext "Can't use --patch and --include-untracked or --all at the same time")" + fi + + test -n "$untracked" || git ls-files --error-unmatch -- "$@" >/dev/null || exit 1 git update-index -q --refresh - if no_changes + if no_changes "$@" then - say 'No local changes to save' + say "$(gettext "No local changes to save")" exit 0 fi - test -f "$GIT_DIR/logs/$ref_stash" || - clear_stash || die "Cannot initialize stash" - - create_stash "$stash_msg" - # Make sure the reflog for stash is kept. - : >>"$GIT_DIR/logs/$ref_stash" + git reflog exists $ref_stash || + clear_stash || die "$(gettext "Cannot initialize stash")" - git update-ref -m "$stash_msg" $ref_stash $w_commit || - die "Cannot save the current status" - say Saved working directory and index state "$stash_msg" + create_stash -m "$stash_msg" -u "$untracked" -- "$@" + store_stash -m "$stash_msg" -q $w_commit || + die "$(gettext "Cannot save the current status")" + say "$(eval_gettext "Saved working directory and index state \$stash_msg")" if test -z "$patch_mode" then - git reset --hard ${GIT_QUIET:+-q} + test "$untracked" = "all" && CLEAN_X_OPTION=-x || CLEAN_X_OPTION= + if test -n "$untracked" + then + git clean --force --quiet -d $CLEAN_X_OPTION -- "$@" + fi - if test -n "$keep_index" && test -n $i_tree + if test $# != 0 then - git read-tree --reset -u $i_tree + git reset -q -- "$@" + git ls-files -z --modified -- "$@" | + git checkout-index -z --force --stdin + git clean --force -q -d -- "$@" + else + git reset --hard -q + fi + + if test "$keep_index" = "t" && test -n "$i_tree" + then + git read-tree --reset $i_tree + git ls-files -z --modified -- "$@" | + git checkout-index -z --force --stdin fi else git apply -R < "$TMP-patch" || - die "Cannot remove worktree changes" + die "$(gettext "Cannot remove worktree changes")" - if test -z "$keep_index" + if test "$keep_index" != "t" then - git reset + git reset -q -- "$@" fi fi } +save_stash () { + push_options= + while test $# != 0 + do + case "$1" in + --) + shift + break + ;; + -*) + # pass all options through to push_stash + push_options="$push_options $1" + ;; + *) + break + ;; + esac + shift + done + + stash_msg="$*" + + if test -z "$stash_msg" + then + push_stash $push_options + else + push_stash $push_options -m "$stash_msg" + fi +} + have_stash () { - git rev-parse --verify $ref_stash >/dev/null 2>&1 + git rev-parse --verify --quiet $ref_stash >/dev/null } list_stash () { have_stash || return 0 - git log --format="%gd: %gs" -g "$@" $ref_stash -- + git log --format="%gd: %gs" -g --first-parent -m "$@" $ref_stash -- } show_stash () { + ALLOW_UNKNOWN_FLAGS=t assert_stash_like "$@" - git diff ${FLAGS:---stat} $b_commit $w_commit + if test -z "$FLAGS" + then + if test "$(git config --bool stash.showStat || echo true)" = "true" + then + FLAGS=--stat + fi + + if test "$(git config --bool stash.showPatch || echo false)" = "true" + then + FLAGS=${FLAGS}${FLAGS:+ }-p + fi + + if test -z "$FLAGS" + then + return 0 + fi + fi + + git diff ${FLAGS} $b_commit $w_commit +} + +show_help () { + exec git help stash + exit 1 } # @@ -233,19 +427,22 @@ show_stash () { # w_commit is set to the commit containing the working tree # b_commit is set to the base commit # i_commit is set to the commit containing the index tree +# u_commit is set to the commit containing the untracked files tree # w_tree is set to the working tree # b_tree is set to the base tree # i_tree is set to the index tree +# u_tree is set to the untracked files tree # # GIT_QUIET is set to t if -q is specified # INDEX_OPTION is set to --index if --index is specified. -# FLAGS is set to the remaining flags +# FLAGS is set to the remaining flags (if allowed) # # dies if: # * too many revisions specified # * no revision is specified and there is no stash stack # * a revision is specified which cannot be resolve to a SHA1 # * a non-existent stash reference is specified +# * unknown flags were set and ALLOW_UNKNOWN_FLAGS is not "t" # parse_flags_and_rev() @@ -260,13 +457,14 @@ parse_flags_and_rev() w_commit= b_commit= i_commit= + u_commit= w_tree= b_tree= i_tree= - - REV=$(git rev-parse --no-flags --symbolic "$@" 2>/dev/null) + u_tree= FLAGS= + REV= for opt do case "$opt" in @@ -276,31 +474,51 @@ parse_flags_and_rev() --index) INDEX_OPTION=--index ;; + --help) + show_help + ;; -*) + test "$ALLOW_UNKNOWN_FLAGS" = t || + die "$(eval_gettext "unknown option: \$opt")" FLAGS="${FLAGS}${FLAGS:+ }$opt" ;; + *) + REV="${REV}${REV:+ }'$opt'" + ;; esac done - set -- $REV + eval set -- $REV case $# in 0) - have_stash || die "No stash found." + have_stash || die "$(gettext "No stash entries found.")" set -- ${ref_stash}@{0} ;; 1) : ;; *) - die "Too many revisions specified: $REV" + die "$(eval_gettext "Too many revisions specified: \$REV")" ;; esac - REV=$(git rev-parse --quiet --symbolic --verify $1 2>/dev/null) || die "$1 is not valid reference" + case "$1" in + *[!0-9]*) + : + ;; + *) + set -- "${ref_stash}@{$1}" + ;; + esac - i_commit=$(git rev-parse --quiet --verify $REV^2 2>/dev/null) && - set -- $(git rev-parse $REV $REV^1 $REV: $REV^1: $REV^2: 2>/dev/null) && + REV=$(git rev-parse --symbolic --verify --quiet "$1") || { + reference="$1" + die "$(eval_gettext "\$reference is not a valid reference")" + } + + i_commit=$(git rev-parse --verify --quiet "$REV^2") && + set -- $(git rev-parse "$REV" "$REV^1" "$REV:" "$REV^1:" "$REV^2:" 2>/dev/null) && s=$1 && w_commit=$1 && b_commit=$2 && @@ -311,15 +529,8 @@ parse_flags_and_rev() test "$ref_stash" = "$(git rev-parse --symbolic-full-name "${REV%@*}")" && IS_STASH_REF=t - if test "${REV}" != "${REV%{*\}}" - then - # maintainers: it would be better if git rev-parse indicated - # this condition with a non-zero status code but as of 1.7.2.1 it - # it did not. So, we use non-empty stderr output as a proxy for the - # condition of interest. - test -z "$(git rev-parse "$REV" 2>&1 >/dev/null)" || die "$REV does not exist in the stash log" - fi - + u_commit=$(git rev-parse --verify --quiet "$REV^3") && + u_tree=$(git rev-parse "$REV^3:" 2>/dev/null) } is_stash_like() @@ -329,7 +540,10 @@ is_stash_like() } assert_stash_like() { - is_stash_like "$@" || die "'$*' is not a stash-like commit" + is_stash_like "$@" || { + args="$*" + die "$(eval_gettext "'\$args' is not a stash-like commit")" + } } is_stash_ref() { @@ -337,20 +551,21 @@ is_stash_ref() { } assert_stash_ref() { - is_stash_ref "$@" || die "'$*' is not a stash reference" + is_stash_ref "$@" || { + args="$*" + die "$(eval_gettext "'\$args' is not a stash reference")" + } } apply_stash () { assert_stash_like "$@" - git update-index -q --refresh && - git diff-files --quiet --ignore-submodules || - die 'Cannot apply to a dirty working tree, please stage your changes' + git update-index -q --refresh || die "$(gettext "unable to refresh index")" # current index state c_tree=$(git write-tree) || - die 'Cannot apply a stash in the middle of a merge' + die "$(gettext "Cannot apply a stash in the middle of a merge")" unstashed_index_tree= if test -n "$INDEX_OPTION" && test "$b_tree" != "$i_tree" && @@ -358,12 +573,20 @@ apply_stash () { then git diff-tree --binary $s^2^..$s^2 | git apply --cached test $? -ne 0 && - die 'Conflicts in index. Try without --index.' + die "$(gettext "Conflicts in index. Try without --index.")" unstashed_index_tree=$(git write-tree) || - die 'Could not save index tree' + die "$(gettext "Could not save index tree")" git reset fi + if test -n "$u_tree" + then + GIT_INDEX_FILE="$TMPindex" git read-tree "$u_tree" && + GIT_INDEX_FILE="$TMPindex" git checkout-index --all && + rm -f "$TMPindex" || + die "$(gettext "Could not restore untracked files from stash entry")" + fi + eval " GITHEAD_$w_tree='Stashed changes' && GITHEAD_$c_tree='Updated upstream' && @@ -386,7 +609,7 @@ apply_stash () { git diff-index --cached --name-only --diff-filter=A $c_tree >"$a" && git read-tree --reset $c_tree && git update-index --add --stdin <"$a" || - die "Cannot unstage modified files" + die "$(gettext "Cannot unstage modified files")" rm -f "$a" fi squelch= @@ -394,13 +617,14 @@ apply_stash () { then squelch='>/dev/null 2>&1' fi - eval "git status $squelch" || : + (cd "$START_DIR" && eval "git status $squelch") || : else # Merge conflict; keep the exit status from merge-recursive status=$? + git rerere if test -n "$INDEX_OPTION" then - echo >&2 'Index was not unstashed.' + gettextln "Index was not unstashed." >&2 fi exit $status fi @@ -409,22 +633,30 @@ apply_stash () { pop_stash() { assert_stash_ref "$@" - apply_stash "$@" && - drop_stash "$@" + if apply_stash "$@" + then + drop_stash "$@" + else + status=$? + say "$(gettext "The stash entry is kept in case you need it again.")" + exit $status + fi } drop_stash () { assert_stash_ref "$@" git reflog delete --updateref --rewrite "${REV}" && - say "Dropped ${REV} ($s)" || die "${REV}: Could not drop stash entry" + say "$(eval_gettext "Dropped \${REV} (\$s)")" || + die "$(eval_gettext "\${REV}: Could not drop stash entry")" # clear_stash if we just dropped the last stash entry - git rev-parse --verify "$ref_stash@{0}" > /dev/null 2>&1 || clear_stash + git rev-parse --verify --quiet "$ref_stash@{0}" >/dev/null || + clear_stash } apply_to_branch () { - test -n "$1" || die 'No branch name specified' + test -n "$1" || die "$(gettext "No branch name specified")" branch=$1 shift 1 @@ -437,18 +669,21 @@ apply_to_branch () { } } +test "$1" = "-p" && set "push" "$@" + PARSE_CACHE='--not-parsed' -# The default command is "save" if nothing but options are given +# The default command is "push" if nothing but options are given seen_non_option= for opt do case "$opt" in + --) break ;; -*) ;; *) seen_non_option=t; break ;; esac done -test -n "$seen_non_option" || set "save" "$@" +test -n "$seen_non_option" || set "push" "$@" # Main command set case "$1" in @@ -464,6 +699,10 @@ save) shift save_stash "$@" ;; +push) + shift + push_stash "$@" + ;; apply) shift apply_stash "$@" @@ -473,11 +712,12 @@ clear) clear_stash "$@" ;; create) - if test $# -gt 0 && test "$1" = create - then - shift - fi - create_stash "$*" && echo "$w_commit" + shift + create_stash -m "$*" && echo "$w_commit" + ;; +store) + shift + store_stash "$@" ;; drop) shift @@ -494,8 +734,8 @@ branch) *) case $# in 0) - save_stash && - say '(To restore them type "git stash apply")' + push_stash && + say "$(gettext "(To restore them type \"git stash apply\")")" ;; *) usage |