Merge branch 'js/reflog-delete'

* js/reflog-delete:
  t3903-stash.sh: Add tests for new stash commands drop and pop
  git-reflog.txt: Document new commands --updateref and --rewrite
  t3903-stash.sh: Add missing '&&' to body of testcase
  git-stash: add new 'pop' subcommand
  git-stash: add new 'drop' subcommand
  git-reflog: add option --updateref to write the last reflog sha1 into the ref
  refs.c: make close_ref() and commit_ref() non-static
  git-reflog: add option --rewrite to update reflog entries while expiring
  reflog-delete: parse standard reflog options
  builtin-reflog.c: fix typo that accesses an unset variable
  Teach "git reflog" a subcommand to delete single entries
This commit is contained in:
Junio C Hamano 2008-03-07 22:34:26 -08:00
commit d33046c1ed
8 changed files with 245 additions and 7 deletions

View File

@ -19,6 +19,8 @@ depending on the subcommand:
git reflog expire [--dry-run] [--stale-fix] [--verbose] git reflog expire [--dry-run] [--stale-fix] [--verbose]
[--expire=<time>] [--expire-unreachable=<time>] [--all] <refs>... [--expire=<time>] [--expire-unreachable=<time>] [--all] <refs>...
git reflog delete ref@\{specifier\}...
git reflog [show] [log-options] [<ref>] git reflog [show] [log-options] [<ref>]
Reflog is a mechanism to record when the tip of branches are Reflog is a mechanism to record when the tip of branches are
@ -43,6 +45,9 @@ two moves ago", `master@\{one.week.ago\}` means "where master used to
point to one week ago", and so on. See linkgit:git-rev-parse[1] for point to one week ago", and so on. See linkgit:git-rev-parse[1] for
more details. more details.
To delete single entries from the reflog, use the subcommand "delete"
and specify the _exact_ entry (e.g. ``git reflog delete master@\{2\}'').
OPTIONS OPTIONS
------- -------
@ -75,6 +80,15 @@ them.
--all:: --all::
Instead of listing <refs> explicitly, prune all refs. Instead of listing <refs> explicitly, prune all refs.
--updateref::
Update the ref with the sha1 of the top reflog entry (i.e.
<ref>@\{0\}) after expiring or deleting.
--rewrite::
While expiring or deleting, adjust each reflog entry to ensure
that the `old` sha1 field points to the `new` sha1 field of the
previous entry.
--verbose:: --verbose::
Print extra information on screen. Print extra information on screen.

View File

@ -8,7 +8,7 @@ git-stash - Stash the changes in a dirty working directory away
SYNOPSIS SYNOPSIS
-------- --------
[verse] [verse]
'git-stash' (list | show [<stash>] | apply [<stash>] | clear) 'git-stash' (list | show [<stash>] | apply [<stash>] | clear | drop [<stash>] | pop [<stash>])
'git-stash' [save [<message>]] 'git-stash' [save [<message>]]
DESCRIPTION DESCRIPTION
@ -85,6 +85,17 @@ clear::
Remove all the stashed states. Note that those states will then Remove all the stashed states. Note that those states will then
be subject to pruning, and may be difficult or impossible to recover. be subject to pruning, and may be difficult or impossible to recover.
drop [<stash>]::
Remove a single stashed state from the stash list. When no `<stash>`
is given, it removes the latest one. i.e. `stash@\{0}`
pop [<stash>]::
Remove a single stashed state from the stash list and apply on top
of the current working tree state. When no `<stash>` is given,
`stash@\{0}` is assumed. See also `apply`.
DISCUSSION DISCUSSION
---------- ----------

View File

@ -14,6 +14,8 @@
static const char reflog_expire_usage[] = static const char reflog_expire_usage[] =
"git-reflog (show|expire) [--verbose] [--dry-run] [--stale-fix] [--expire=<time>] [--expire-unreachable=<time>] [--all] <refs>..."; "git-reflog (show|expire) [--verbose] [--dry-run] [--stale-fix] [--expire=<time>] [--expire-unreachable=<time>] [--all] <refs>...";
static const char reflog_delete_usage[] =
"git-reflog delete [--verbose] [--dry-run] [--rewrite] [--updateref] <refs>...";
static unsigned long default_reflog_expire; static unsigned long default_reflog_expire;
static unsigned long default_reflog_expire_unreachable; static unsigned long default_reflog_expire_unreachable;
@ -22,9 +24,12 @@ struct cmd_reflog_expire_cb {
struct rev_info revs; struct rev_info revs;
int dry_run; int dry_run;
int stalefix; int stalefix;
int rewrite;
int updateref;
int verbose; int verbose;
unsigned long expire_total; unsigned long expire_total;
unsigned long expire_unreachable; unsigned long expire_unreachable;
int recno;
}; };
struct expire_reflog_cb { struct expire_reflog_cb {
@ -32,6 +37,7 @@ struct expire_reflog_cb {
const char *ref; const char *ref;
struct commit *ref_commit; struct commit *ref_commit;
struct cmd_reflog_expire_cb *cmd; struct cmd_reflog_expire_cb *cmd;
unsigned char last_kept_sha1[20];
}; };
struct collected_reflog { struct collected_reflog {
@ -213,6 +219,9 @@ static int expire_reflog_ent(unsigned char *osha1, unsigned char *nsha1,
if (timestamp < cb->cmd->expire_total) if (timestamp < cb->cmd->expire_total)
goto prune; goto prune;
if (cb->cmd->rewrite)
osha1 = cb->last_kept_sha1;
old = new = NULL; old = new = NULL;
if (cb->cmd->stalefix && if (cb->cmd->stalefix &&
(!keep_entry(&old, osha1) || !keep_entry(&new, nsha1))) (!keep_entry(&old, osha1) || !keep_entry(&new, nsha1)))
@ -230,6 +239,9 @@ static int expire_reflog_ent(unsigned char *osha1, unsigned char *nsha1,
goto prune; goto prune;
} }
if (cb->cmd->recno && --(cb->cmd->recno) == 0)
goto prune;
if (cb->newlog) { if (cb->newlog) {
char sign = (tz < 0) ? '-' : '+'; char sign = (tz < 0) ? '-' : '+';
int zone = (tz < 0) ? (-tz) : tz; int zone = (tz < 0) ? (-tz) : tz;
@ -237,6 +249,7 @@ static int expire_reflog_ent(unsigned char *osha1, unsigned char *nsha1,
sha1_to_hex(osha1), sha1_to_hex(nsha1), sha1_to_hex(osha1), sha1_to_hex(nsha1),
email, timestamp, sign, zone, email, timestamp, sign, zone,
message); message);
hashcpy(cb->last_kept_sha1, nsha1);
} }
if (cb->cmd->verbose) if (cb->cmd->verbose)
printf("keep %s", message); printf("keep %s", message);
@ -280,10 +293,20 @@ static int expire_reflog(const char *ref, const unsigned char *sha1, int unused,
status |= error("%s: %s", strerror(errno), status |= error("%s: %s", strerror(errno),
newlog_path); newlog_path);
unlink(newlog_path); unlink(newlog_path);
} else if (cmd->updateref &&
(write_in_full(lock->lock_fd,
sha1_to_hex(cb.last_kept_sha1), 40) != 40 ||
write_in_full(lock->lock_fd, "\n", 1) != 1 ||
close_ref(lock) < 0)) {
status |= error("Couldn't write %s",
lock->lk->filename);
unlink(newlog_path);
} else if (rename(newlog_path, log_file)) { } else if (rename(newlog_path, log_file)) {
status |= error("cannot rename %s to %s", status |= error("cannot rename %s to %s",
newlog_path, log_file); newlog_path, log_file);
unlink(newlog_path); unlink(newlog_path);
} else if (cmd->updateref && commit_ref(lock)) {
status |= error("Couldn't set %s", lock->ref_name);
} }
} }
free(newlog_path); free(newlog_path);
@ -358,6 +381,10 @@ static int cmd_reflog_expire(int argc, const char **argv, const char *prefix)
cb.expire_unreachable = approxidate(arg + 21); cb.expire_unreachable = approxidate(arg + 21);
else if (!strcmp(arg, "--stale-fix")) else if (!strcmp(arg, "--stale-fix"))
cb.stalefix = 1; cb.stalefix = 1;
else if (!strcmp(arg, "--rewrite"))
cb.rewrite = 1;
else if (!strcmp(arg, "--updateref"))
cb.updateref = 1;
else if (!strcmp(arg, "--all")) else if (!strcmp(arg, "--all"))
do_all = 1; do_all = 1;
else if (!strcmp(arg, "--verbose")) else if (!strcmp(arg, "--verbose"))
@ -406,6 +433,78 @@ static int cmd_reflog_expire(int argc, const char **argv, const char *prefix)
return status; return status;
} }
static int count_reflog_ent(unsigned char *osha1, unsigned char *nsha1,
const char *email, unsigned long timestamp, int tz,
const char *message, void *cb_data)
{
struct cmd_reflog_expire_cb *cb = cb_data;
if (!cb->expire_total || timestamp < cb->expire_total)
cb->recno++;
return 0;
}
static int cmd_reflog_delete(int argc, const char **argv, const char *prefix)
{
struct cmd_reflog_expire_cb cb;
int i, status = 0;
memset(&cb, 0, sizeof(cb));
for (i = 1; i < argc; i++) {
const char *arg = argv[i];
if (!strcmp(arg, "--dry-run") || !strcmp(arg, "-n"))
cb.dry_run = 1;
else if (!strcmp(arg, "--rewrite"))
cb.rewrite = 1;
else if (!strcmp(arg, "--updateref"))
cb.updateref = 1;
else if (!strcmp(arg, "--verbose"))
cb.verbose = 1;
else if (!strcmp(arg, "--")) {
i++;
break;
}
else if (arg[0] == '-')
usage(reflog_delete_usage);
else
break;
}
if (argc - i < 1)
return error("Nothing to delete?");
for ( ; i < argc; i++) {
const char *spec = strstr(argv[i], "@{");
unsigned char sha1[20];
char *ep, *ref;
int recno;
if (!spec) {
status |= error("Not a reflog: %s", argv[i]);
continue;
}
if (!dwim_ref(argv[i], spec - argv[i], sha1, &ref)) {
status |= error("%s points nowhere!", argv[i]);
continue;
}
recno = strtoul(spec + 2, &ep, 10);
if (*ep == '}') {
cb.recno = -recno;
for_each_reflog_ent(ref, count_reflog_ent, &cb);
} else {
cb.expire_total = approxidate(spec + 2);
for_each_reflog_ent(ref, count_reflog_ent, &cb);
cb.expire_total = 0;
}
status |= expire_reflog(ref, sha1, 0, &cb);
free(ref);
}
return status;
}
/* /*
* main "reflog" * main "reflog"
*/ */
@ -425,6 +524,9 @@ int cmd_reflog(int argc, const char **argv, const char *prefix)
if (!strcmp(argv[1], "expire")) if (!strcmp(argv[1], "expire"))
return cmd_reflog_expire(argc - 1, argv + 1, prefix); return cmd_reflog_expire(argc - 1, argv + 1, prefix);
if (!strcmp(argv[1], "delete"))
return cmd_reflog_delete(argc - 1, argv + 1, prefix);
/* Not a recognized reflog command..*/ /* Not a recognized reflog command..*/
usage(reflog_usage); usage(reflog_usage);
} }

View File

@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
# Copyright (c) 2007, Nanako Shiraishi # Copyright (c) 2007, Nanako Shiraishi
USAGE='[ | save | list | show | apply | clear | create ]' USAGE='[ | save | list | show | apply | clear | drop | pop | create ]'
SUBDIRECTORY_OK=Yes SUBDIRECTORY_OK=Yes
OPTIONS_SPEC= OPTIONS_SPEC=
@ -196,6 +196,28 @@ apply_stash () {
fi fi
} }
drop_stash () {
have_stash || die 'No stash entries to drop'
if test $# = 0
then
set x "$ref_stash@{0}"
shift
fi
# Verify supplied argument looks like a stash entry
s=$(git rev-parse --revs-only --no-flags "$@") &&
git rev-parse --verify "$s:" > /dev/null 2>&1 &&
git rev-parse --verify "$s^1:" > /dev/null 2>&1 &&
git rev-parse --verify "$s^2:" > /dev/null 2>&1 ||
die "$*: not a valid stashed state"
git reflog delete --updateref --rewrite "$@" &&
echo "Dropped $* ($s)" || die "$*: 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
}
# Main command set # Main command set
case "$1" in case "$1" in
list) list)
@ -230,6 +252,18 @@ create)
fi fi
create_stash "$*" && echo "$w_commit" create_stash "$*" && echo "$w_commit"
;; ;;
drop)
shift
drop_stash "$@"
;;
pop)
shift
if apply_stash "$@"
then
test -z "$unstash_index" || shift
drop_stash "$@"
fi
;;
*) *)
if test $# -eq 0 if test $# -eq 0
then then

4
refs.c
View File

@ -1033,7 +1033,7 @@ int rename_ref(const char *oldref, const char *newref, const char *logmsg)
return 1; return 1;
} }
static int close_ref(struct ref_lock *lock) int close_ref(struct ref_lock *lock)
{ {
if (close_lock_file(lock->lk)) if (close_lock_file(lock->lk))
return -1; return -1;
@ -1041,7 +1041,7 @@ static int close_ref(struct ref_lock *lock)
return 0; return 0;
} }
static int commit_ref(struct ref_lock *lock) int commit_ref(struct ref_lock *lock)
{ {
if (commit_lock_file(lock->lk)) if (commit_lock_file(lock->lk))
return -1; return -1;

6
refs.h
View File

@ -33,6 +33,12 @@ extern struct ref_lock *lock_ref_sha1(const char *ref, const unsigned char *old_
#define REF_NODEREF 0x01 #define REF_NODEREF 0x01
extern struct ref_lock *lock_any_ref_for_update(const char *ref, const unsigned char *old_sha1, int flags); extern struct ref_lock *lock_any_ref_for_update(const char *ref, const unsigned char *old_sha1, int flags);
/** Close the file descriptor owned by a lock and return the status */
extern int close_ref(struct ref_lock *lock);
/** Close and commit the ref locked by the lock */
extern int commit_ref(struct ref_lock *lock);
/** Release any lock taken but not written. **/ /** Release any lock taken but not written. **/
extern void unlock_ref(struct ref_lock *lock); extern void unlock_ref(struct ref_lock *lock);

View File

@ -175,6 +175,33 @@ test_expect_success 'recover and check' '
' '
test_expect_success 'delete' '
echo 1 > C &&
test_tick &&
git commit -m rat C &&
echo 2 > C &&
test_tick &&
git commit -m ox C &&
echo 3 > C &&
test_tick &&
git commit -m tiger C &&
test 5 = $(git reflog | wc -l) &&
git reflog delete master@{1} &&
git reflog show master > output &&
test 4 = $(wc -l < output) &&
! grep ox < output &&
git reflog delete master@{07.04.2005.15:15:00.-0700} &&
git reflog show master > output &&
test 3 = $(wc -l < output) &&
! grep dragon < output
'
test_expect_success 'prune --expire' ' test_expect_success 'prune --expire' '
before=$(git count-objects | sed "s/ .*//") && before=$(git count-objects | sed "s/ .*//") &&

View File

@ -40,8 +40,8 @@ test_expect_success 'parents of stash' '
test_expect_success 'apply needs clean working directory' ' test_expect_success 'apply needs clean working directory' '
echo 4 > other-file && echo 4 > other-file &&
git add other-file && git add other-file &&
echo 5 > other-file echo 5 > other-file &&
! git stash apply test_must_fail git stash apply
' '
test_expect_success 'apply stashed changes' ' test_expect_success 'apply stashed changes' '
@ -70,7 +70,51 @@ test_expect_success 'unstashing in a subdirectory' '
git reset --hard HEAD && git reset --hard HEAD &&
mkdir subdir && mkdir subdir &&
cd subdir && cd subdir &&
git stash apply git stash apply &&
cd ..
'
test_expect_success 'drop top stash' '
git reset --hard &&
git stash list > stashlist1 &&
echo 7 > file &&
git stash &&
git stash drop &&
git stash list > stashlist2 &&
diff stashlist1 stashlist2 &&
git stash apply &&
test 3 = $(cat file) &&
test 1 = $(git show :file) &&
test 1 = $(git show HEAD:file)
'
test_expect_success 'drop middle stash' '
git reset --hard &&
echo 8 > file &&
git stash &&
echo 9 > file &&
git stash &&
git stash drop stash@{1} &&
test 2 = $(git stash list | wc -l) &&
git stash apply &&
test 9 = $(cat file) &&
test 1 = $(git show :file) &&
test 1 = $(git show HEAD:file) &&
git reset --hard &&
git stash drop &&
git stash apply &&
test 3 = $(cat file) &&
test 1 = $(git show :file) &&
test 1 = $(git show HEAD:file)
'
test_expect_success 'stash pop' '
git reset --hard &&
git stash pop &&
test 3 = $(cat file) &&
test 1 = $(git show :file) &&
test 1 = $(git show HEAD:file) &&
test 0 = $(git stash list | wc -l)
' '
test_done test_done