#!/bin/sh # Copyright (c) 2007, Nanako Shiraishi setglobal dashless = $[basename $0 | sed -e 's/-/ /] setglobal USAGE = ""list [] or: $dashless show [] or: $dashless drop [-q|--quiet] [] or: $dashless ( pop | apply ) [--index] [-q|--quiet] [] or: $dashless branch [] or: $dashless [save [--patch] [-k|--[no-]keep-index] [-q|--quiet] [-u|--include-untracked] [-a|--all] []] or: $dashless clear"" setglobal SUBDIRECTORY_OK = 'Yes' setglobal OPTIONS_SPEC = '' setglobal START_DIR = $[pwd] source git-sh-setup require_work_tree cd_to_toplevel setglobal TMP = ""$GIT_DIR/.git-stash.$Pid"" setglobal TMPindex = "$(GIT_INDEX_FILE-"$(git rev-parse --git-path index)").stash.$Pid" trap 'rm -f "$TMP-"* "$TMPindex"' 0 setglobal ref_stash = 'refs/stash' if git config --get-colorbool color.interactive { setglobal help_color = $[git config --get-color color.interactive.help 'red bold] setglobal reset_color = $[git config --get-color '' reset] } else { setglobal help_color = '' setglobal reset_color = '' } proc no_changes { git diff-index --quiet --cached HEAD --ignore-submodules -- && git diff-files --quiet --ignore-submodules && shell {test -z $untracked || test -z $[untracked_files]} } proc untracked_files { setglobal excl_opt = '--exclude-standard' test $untracked = "all" && setglobal excl_opt = '' git ls-files -o -z $excl_opt } proc clear_stash { if test $Argc != 0 { die $[gettext "git stash clear with parameters is unimplemented] } if setglobal current = $[git rev-parse --verify --quiet $ref_stash] { git update-ref -d $ref_stash $current } } proc create_stash { setglobal stash_msg = $1 setglobal untracked = $2 git update-index -q --refresh if no_changes { exit 0 } # state of the base commit if setglobal b_commit = $[git rev-parse --verify HEAD] { setglobal head = $[git rev-list --oneline -n 1 HEAD --] } else { die $[gettext "You do not have the initial commit yet] } if setglobal branch = $[git symbolic-ref -q HEAD] { setglobal branch = $(branch#refs/heads/) } else { setglobal branch = ''(no branch)'' } setglobal msg = $[printf '%s: %s' $branch $head] # state of the index setglobal i_tree = $[git write-tree] && setglobal i_commit = $[printf 'index on %s\n' $msg | git commit-tree $i_tree -p $b_commit] || die $[gettext "Cannot save the current index state] if test -n $untracked { # Untracked files are stored by themselves in a parentless commit, for # ease of unpacking later. setglobal u_commit = $[ untracked_files | shell { setglobal GIT_INDEX_FILE = $TMPindex && export GIT_INDEX_FILE && rm -f $TMPindex && git update-index -z --add --remove --stdin && setglobal 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] setglobal untracked_commit_option = ""-p $u_commit""; } else { setglobal untracked_commit_option = '' } if test -z $patch_mode { # state of the working tree setglobal w_tree = $[ shell { git read-tree --index-output="$TMPindex" -m $i_tree && setglobal GIT_INDEX_FILE = $TMPindex && export GIT_INDEX_FILE && git diff --name-only -z HEAD -- >"$TMP-stagenames" && git update-index -z --add --remove --stdin <"$TMP-stagenames" && git write-tree && rm -f $TMPindex }] || die $[gettext "Cannot save the current worktree state] } else { rm -f "$TMP-index" && env GIT_INDEX_FILE="$TMP-index" git read-tree HEAD && # find out what the user wants env GIT_INDEX_FILE="$TMP-index" \ git add--interactive --patch=stash -- && # state of the working tree setglobal w_tree = $[env GIT_INDEX_FILE="$TMP-index" git write-tree] || die $[gettext "Cannot save the current worktree state] git diff-tree -p HEAD $w_tree -- >"$TMP-patch" && test -s "$TMP-patch" || die $[gettext "No changes selected] rm -f "$TMP-index" || die $[gettext "Cannot remove temporary index (can't happen)] } # create the stash if test -z $stash_msg { setglobal stash_msg = $[printf 'WIP on %s' $msg] } else { setglobal stash_msg = $[printf 'On %s: %s' $branch $stash_msg] } setglobal w_commit = $[printf '%s\n' $stash_msg | git commit-tree $w_tree -p $b_commit -p $i_commit $untracked_commit_option] || die $[gettext "Cannot record working tree state] } proc store_stash { while test $# != 0 { match $1 { with -m|--message shift setglobal stash_msg = $1 with -q|--quiet setglobal quiet = 't' with * break } shift } test $Argc = 1 || die $[eval_gettext "\"$dashless store\" requires one argument] setglobal w_commit = $1 if test -z $stash_msg { setglobal stash_msg = '"Created via \"git stash store\".'" } git update-ref --create-reflog -m $stash_msg $ref_stash $w_commit setglobal ret = $Status test $ret != 0 && test -z $quiet && die $[eval_gettext "Cannot update \$ref_stash with \$w_commit] return $ret } proc save_stash { setglobal keep_index = '' setglobal patch_mode = '' setglobal untracked = '' while test $# != 0 { match $1 { with -k|--keep-index setglobal keep_index = 't' with --no-keep-index setglobal keep_index = 'n' with -p|--patch setglobal patch_mode = 't' # only default to keep if we don't already have an override test -z $keep_index && setglobal keep_index = 't' with -q|--quiet setglobal GIT_QUIET = 't' with -u|--include-untracked setglobal untracked = 'untracked' with -a|--all setglobal untracked = 'all' with --help show_help with -- shift break with -* setglobal 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 with * break } shift } if test -n $patch_mode && test -n $untracked { die $[gettext "Can't use --patch and --include-untracked or --all at the same time] } setglobal stash_msg = "$ifsjoin(Argv)" git update-index -q --refresh if no_changes { say $[gettext "No local changes to save] exit 0 } git reflog exists $ref_stash || clear_stash || die $[gettext "Cannot initialize stash] create_stash $stash_msg $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 { git reset --hard $(GIT_QUIET:+-q) test $untracked = "all" && setglobal CLEAN_X_OPTION = '-x' || setglobal CLEAN_X_OPTION = '' if test -n $untracked { git clean --force --quiet -d $CLEAN_X_OPTION } if test $keep_index = "t" && test -n $i_tree { git read-tree --reset -u $i_tree } } else { git apply -R < "$TMP-patch" || die $[gettext "Cannot remove worktree changes] if test $keep_index != "t" { git reset } } } proc have_stash { git rev-parse --verify --quiet $ref_stash >/dev/null } proc list_stash { have_stash || return 0 git log --format="%gd: %gs" -g --first-parent -m @Argv $ref_stash -- } proc show_stash { setglobal ALLOW_UNKNOWN_FLAGS = 't' assert_stash_like @Argv if test -z $FLAGS { if test $[git config --bool stash.showStat || echo true] = "true" { setglobal FLAGS = '--stat' } if test $[git config --bool stash.showPatch || echo false] = "true" { setglobal FLAGS = "$(FLAGS)$(FLAGS:+ )-p" } if test -z $FLAGS { return 0 } } git diff $(FLAGS) $b_commit $w_commit } proc show_help { exec git help stash exit 1 } # # Parses the remaining options looking for flags and # at most one revision defaulting to ${ref_stash}@{0} # if none found. # # Derives related tree and commit objects from the # revision, if one is found. # # stash records the work tree, and is a merge between the # base commit (first parent) and the index tree (second parent). # # REV is set to the symbolic version of the specified stash-like commit # IS_STASH_LIKE is non-blank if ${REV} looks like a stash # IS_STASH_REF is non-blank if the ${REV} looks like a stash ref # s is set to the SHA1 of the stash commit # 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 (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" # proc parse_flags_and_rev { test $PARSE_CACHE = "$ifsjoin(Argv)" && return 0 # optimisation setglobal PARSE_CACHE = "$ifsjoin(Argv)" setglobal IS_STASH_LIKE = '' setglobal IS_STASH_REF = '' setglobal INDEX_OPTION = '' setglobal s = '' setglobal w_commit = '' setglobal b_commit = '' setglobal i_commit = '' setglobal u_commit = '' setglobal w_tree = '' setglobal b_tree = '' setglobal i_tree = '' setglobal u_tree = '' setglobal REV = $[git rev-parse --no-flags --symbolic --sq @Argv] || exit 1 setglobal FLAGS = ''for opt in @Argv { match $opt { with -q|--quiet setglobal GIT_QUIET = '-t' with --index setglobal INDEX_OPTION = '--index' with --help show_help with -* test $ALLOW_UNKNOWN_FLAGS = t || die $[eval_gettext "unknown option: \$opt] setglobal FLAGS = ""$(FLAGS)$(FLAGS:+ )$opt"" } } eval set -- $REV match $Argc { with 0 have_stash || die $[gettext "No stash found.] set -- $(ref_stash)@{0} with 1 : with * die $[eval_gettext "Too many revisions specified: \$REV] } setglobal REV = $[git rev-parse --symbolic --verify --quiet $1] || do { setglobal reference = $1 die $[eval_gettext "\$reference is not a valid reference] } setglobal i_commit = $[git rev-parse --verify --quiet "$REV^2] && set -- $[git rev-parse $REV "$REV^1" "$REV:" "$REV^1:" "$REV^2:" !2 >/dev/null] && setglobal s = $1 && setglobal w_commit = $1 && setglobal b_commit = $2 && setglobal w_tree = $3 && setglobal b_tree = $4 && setglobal i_tree = $5 && setglobal IS_STASH_LIKE = 't' && test $ref_stash = $[git rev-parse --symbolic-full-name $(REV%@*)] && setglobal IS_STASH_REF = 't' setglobal u_commit = $[git rev-parse --verify --quiet "$REV^3] && setglobal u_tree = $[git rev-parse "$REV^3:" !2 >/dev/null] } proc is_stash_like { parse_flags_and_rev @Argv test -n $IS_STASH_LIKE } proc assert_stash_like { is_stash_like @Argv || do { setglobal args = "$ifsjoin(Argv)" die $[eval_gettext "'\$args' is not a stash-like commit] } } proc is_stash_ref { is_stash_like @Argv && test -n $IS_STASH_REF } proc assert_stash_ref { is_stash_ref @Argv || do { setglobal args = "$ifsjoin(Argv)" die $[eval_gettext "'\$args' is not a stash reference] } } proc apply_stash { assert_stash_like @Argv git update-index -q --refresh || die $[gettext "unable to refresh index] # current index state setglobal c_tree = $[git write-tree] || die $[gettext "Cannot apply a stash in the middle of a merge] setglobal unstashed_index_tree = '' if test -n $INDEX_OPTION && test $b_tree != $i_tree && test $c_tree != $i_tree { git diff-tree --binary $s^2^..$s^2 | git apply --cached test $Status -ne 0 && die $[gettext "Conflicts in index. Try without --index.] setglobal unstashed_index_tree = $[git write-tree] || die $[gettext "Could not save index tree] git reset } if test -n $u_tree { env GIT_INDEX_FILE=$TMPindex git-read-tree $u_tree && env GIT_INDEX_FILE=$TMPindex git checkout-index --all && rm -f $TMPindex || die $[gettext "Could not restore untracked files from stash] } eval " GITHEAD_$w_tree='Stashed changes' && GITHEAD_$c_tree='Updated upstream' && GITHEAD_$b_tree='Version stash was based on' && export GITHEAD_$w_tree GITHEAD_$c_tree GITHEAD_$b_tree " if test -n $GIT_QUIET { setglobal GIT_MERGE_VERBOSITY = '0' && export GIT_MERGE_VERBOSITY } if git merge-recursive $b_tree -- $c_tree $w_tree { # No conflict if test -n $unstashed_index_tree { git read-tree $unstashed_index_tree } else { setglobal a = ""$TMP-added"" && 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 $[gettext "Cannot unstage modified files] rm -f $a } setglobal squelch = '' if test -n $GIT_QUIET { setglobal squelch = ''>/dev/null 2>&1'' } shell {cd $START_DIR && eval "git status $squelch"} || : } else { # Merge conflict; keep the exit status from merge-recursive setglobal status = $Status git rerere if test -n $INDEX_OPTION { gettextln "Index was not unstashed." > !2 } exit $status } } proc pop_stash { assert_stash_ref @Argv if apply_stash @Argv { drop_stash @Argv } else { setglobal status = $Status say $[gettext "The stash is kept in case you need it again.] exit $status } } proc drop_stash { assert_stash_ref @Argv git reflog delete --updateref --rewrite $(REV) && 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 --quiet "$ref_stash@{0}" >/dev/null || clear_stash } proc apply_to_branch { test -n $1 || die $[gettext "No branch name specified] setglobal branch = $1 shift 1 set -- --index @Argv assert_stash_like @Argv git checkout -b $branch $REV^ && apply_stash @Argv && do { test -z $IS_STASH_REF || drop_stash @Argv } } setglobal PARSE_CACHE = ''--not-parsed'' # The default command is "save" if nothing but options are given setglobal seen_non_option = ''for opt in @Argv { match $opt { with -* with * setglobal seen_non_option = 't'; break } } test -n $seen_non_option || set "save" @Argv # Main command set match $1 { with list shift list_stash @Argv with show shift show_stash @Argv with save shift save_stash @Argv with apply shift apply_stash @Argv with clear shift clear_stash @Argv with create shift create_stash "$ifsjoin(Argv)" && echo $w_commit with store shift store_stash @Argv with drop shift drop_stash @Argv with pop shift pop_stash @Argv with branch shift apply_to_branch @Argv with * match $Argc { with 0 save_stash && say $[gettext "(To restore them type \"git stash apply\")] with * usage } }