diff options
author | Junio C Hamano <gitster@pobox.com> | 2014-06-16 10:06:01 -0700 |
---|---|---|
committer | Junio C Hamano <gitster@pobox.com> | 2014-06-16 10:06:01 -0700 |
commit | 499168af3b2a21e2d234a2093cb3c0f64e2f36a0 (patch) | |
tree | 1e978002558fdcb1766bed41cb3b36d64c35430d | |
parent | 79e539404c016e894d8a4db8f43762842a066afc (diff) | |
parent | 4e4b125c239a78f3a8fe82e47d1f89fef510980c (diff) | |
download | git-499168af3b2a21e2d234a2093cb3c0f64e2f36a0.tar.gz |
Merge branch 'cc/replace-edit'
"git replace" learns a new "--edit" option.
* cc/replace-edit:
Documentation: replace: describe new --edit option
replace: add --edit to usage string
replace: add tests for --edit
replace: die early if replace ref already exists
replace: refactor checking ref validity
replace: make sure --edit results in a different object
replace: add --edit option
replace: factor object resolution out of replace_object
replace: use OPT_CMDMODE to handle modes
replace: refactor command-mode determination
-rw-r--r-- | Documentation/git-replace.txt | 16 | ||||
-rw-r--r-- | builtin/replace.c | 225 | ||||
-rwxr-xr-x | t/t6050-replace.sh | 27 |
3 files changed, 222 insertions, 46 deletions
diff --git a/Documentation/git-replace.txt b/Documentation/git-replace.txt index 0a02f70657..61461b9f33 100644 --- a/Documentation/git-replace.txt +++ b/Documentation/git-replace.txt @@ -9,6 +9,7 @@ SYNOPSIS -------- [verse] 'git replace' [-f] <object> <replacement> +'git replace' [-f] --edit <object> 'git replace' -d <object>... 'git replace' [--format=<format>] [-l [<pattern>]] @@ -63,6 +64,15 @@ OPTIONS --delete:: Delete existing replace refs for the given objects. +--edit <object>:: + Edit an object's content interactively. The existing content + for <object> is pretty-printed into a temporary file, an + editor is launched on the file, and the result is parsed to + create a new object of the same type as <object>. A + replacement ref is then created to replace <object> with the + newly created object. See linkgit:git-var[1] for details about + how the editor will be chosen. + -l <pattern>:: --list <pattern>:: List replace refs for objects that match the given pattern (or @@ -92,7 +102,9 @@ CREATING REPLACEMENT OBJECTS linkgit:git-filter-branch[1], linkgit:git-hash-object[1] and linkgit:git-rebase[1], among other git commands, can be used to create -replacement objects from existing objects. +replacement objects from existing objects. The `--edit` option can +also be used with 'git replace' to create a replacement object by +editing an existing object. If you want to replace many blobs, trees or commits that are part of a string of commits, you may just want to create a replacement string of @@ -117,6 +129,8 @@ linkgit:git-filter-branch[1] linkgit:git-rebase[1] linkgit:git-tag[1] linkgit:git-branch[1] +linkgit:git-commit[1] +linkgit:git-var[1] linkgit:git[1] GIT diff --git a/builtin/replace.c b/builtin/replace.c index b62420a01a..1bb491d3c4 100644 --- a/builtin/replace.c +++ b/builtin/replace.c @@ -12,9 +12,11 @@ #include "builtin.h" #include "refs.h" #include "parse-options.h" +#include "run-command.h" static const char * const git_replace_usage[] = { N_("git replace [-f] <object> <replacement>"), + N_("git replace [-f] --edit <object>"), N_("git replace -d <object>..."), N_("git replace [--format=<format>] [-l [<pattern>]]"), NULL @@ -123,26 +125,36 @@ static int delete_replace_ref(const char *name, const char *ref, return 0; } -static int replace_object(const char *object_ref, const char *replace_ref, - int force) +static void check_ref_valid(unsigned char object[20], + unsigned char prev[20], + char *ref, + int ref_size, + int force) { - unsigned char object[20], prev[20], repl[20]; - enum object_type obj_type, repl_type; - char ref[PATH_MAX]; - struct ref_lock *lock; - - if (get_sha1(object_ref, object)) - die("Failed to resolve '%s' as a valid ref.", object_ref); - if (get_sha1(replace_ref, repl)) - die("Failed to resolve '%s' as a valid ref.", replace_ref); - - if (snprintf(ref, sizeof(ref), + if (snprintf(ref, ref_size, "refs/replace/%s", - sha1_to_hex(object)) > sizeof(ref) - 1) + sha1_to_hex(object)) > ref_size - 1) die("replace ref name too long: %.*s...", 50, ref); if (check_refname_format(ref, 0)) die("'%s' is not a valid ref name.", ref); + if (read_ref(ref, prev)) + hashclr(prev); + else if (!force) + die("replace ref '%s' already exists", ref); +} + +static int replace_object_sha1(const char *object_ref, + unsigned char object[20], + const char *replace_ref, + unsigned char repl[20], + int force) +{ + unsigned char prev[20]; + enum object_type obj_type, repl_type; + char ref[PATH_MAX]; + struct ref_lock *lock; + obj_type = sha1_object_info(object, NULL); repl_type = sha1_object_info(repl, NULL); if (!force && obj_type != repl_type) @@ -152,10 +164,7 @@ static int replace_object(const char *object_ref, const char *replace_ref, object_ref, typename(obj_type), replace_ref, typename(repl_type)); - if (read_ref(ref, prev)) - hashclr(prev); - else if (!force) - die("replace ref '%s' already exists", ref); + check_ref_valid(object, prev, ref, sizeof(ref), force); lock = lock_any_ref_for_update(ref, prev, 0, NULL); if (!lock) @@ -166,13 +175,140 @@ static int replace_object(const char *object_ref, const char *replace_ref, return 0; } +static int replace_object(const char *object_ref, const char *replace_ref, int force) +{ + unsigned char object[20], repl[20]; + + if (get_sha1(object_ref, object)) + die("Failed to resolve '%s' as a valid ref.", object_ref); + if (get_sha1(replace_ref, repl)) + die("Failed to resolve '%s' as a valid ref.", replace_ref); + + return replace_object_sha1(object_ref, object, replace_ref, repl, force); +} + +/* + * Write the contents of the object named by "sha1" to the file "filename", + * pretty-printed for human editing based on its type. + */ +static void export_object(const unsigned char *sha1, const char *filename) +{ + const char *argv[] = { "--no-replace-objects", "cat-file", "-p", NULL, NULL }; + struct child_process cmd = { argv }; + int fd; + + fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0666); + if (fd < 0) + die_errno("unable to open %s for writing", filename); + + argv[3] = sha1_to_hex(sha1); + cmd.git_cmd = 1; + cmd.out = fd; + + if (run_command(&cmd)) + die("cat-file reported failure"); + + close(fd); +} + +/* + * Read a previously-exported (and possibly edited) object back from "filename", + * interpreting it as "type", and writing the result to the object database. + * The sha1 of the written object is returned via sha1. + */ +static void import_object(unsigned char *sha1, enum object_type type, + const char *filename) +{ + int fd; + + fd = open(filename, O_RDONLY); + if (fd < 0) + die_errno("unable to open %s for reading", filename); + + if (type == OBJ_TREE) { + const char *argv[] = { "mktree", NULL }; + struct child_process cmd = { argv }; + struct strbuf result = STRBUF_INIT; + + cmd.argv = argv; + cmd.git_cmd = 1; + cmd.in = fd; + cmd.out = -1; + + if (start_command(&cmd)) + die("unable to spawn mktree"); + + if (strbuf_read(&result, cmd.out, 41) < 0) + die_errno("unable to read from mktree"); + close(cmd.out); + + if (finish_command(&cmd)) + die("mktree reported failure"); + if (get_sha1_hex(result.buf, sha1) < 0) + die("mktree did not return an object name"); + + strbuf_release(&result); + } else { + struct stat st; + int flags = HASH_FORMAT_CHECK | HASH_WRITE_OBJECT; + + if (fstat(fd, &st) < 0) + die_errno("unable to fstat %s", filename); + if (index_fd(sha1, fd, &st, type, NULL, flags) < 0) + die("unable to write object to database"); + /* index_fd close()s fd for us */ + } + + /* + * No need to close(fd) here; both run-command and index-fd + * will have done it for us. + */ +} + +static int edit_and_replace(const char *object_ref, int force) +{ + char *tmpfile = git_pathdup("REPLACE_EDITOBJ"); + enum object_type type; + unsigned char old[20], new[20], prev[20]; + char ref[PATH_MAX]; + + if (get_sha1(object_ref, old) < 0) + die("Not a valid object name: '%s'", object_ref); + + type = sha1_object_info(old, NULL); + if (type < 0) + die("unable to get object type for %s", sha1_to_hex(old)); + + check_ref_valid(old, prev, ref, sizeof(ref), force); + + export_object(old, tmpfile); + if (launch_editor(tmpfile, NULL, NULL) < 0) + die("editing object file failed"); + import_object(new, type, tmpfile); + + free(tmpfile); + + if (!hashcmp(old, new)) + return error("new object is the same as the old one: '%s'", sha1_to_hex(old)); + + return replace_object_sha1(object_ref, old, "replacement", new, force); +} + int cmd_replace(int argc, const char **argv, const char *prefix) { - int list = 0, delete = 0, force = 0; + int force = 0; const char *format = NULL; + enum { + MODE_UNSPECIFIED = 0, + MODE_LIST, + MODE_DELETE, + MODE_EDIT, + MODE_REPLACE + } cmdmode = MODE_UNSPECIFIED; struct option options[] = { - OPT_BOOL('l', "list", &list, N_("list replace refs")), - OPT_BOOL('d', "delete", &delete, N_("delete replace refs")), + OPT_CMDMODE('l', "list", &cmdmode, N_("list replace refs"), MODE_LIST), + OPT_CMDMODE('d', "delete", &cmdmode, N_("delete replace refs"), MODE_DELETE), + OPT_CMDMODE('e', "edit", &cmdmode, N_("edit existing object"), MODE_EDIT), OPT_BOOL('f', "force", &force, N_("replace the ref if it exists")), OPT_STRING(0, "format", &format, N_("format"), N_("use this format")), OPT_END() @@ -182,44 +318,43 @@ int cmd_replace(int argc, const char **argv, const char *prefix) argc = parse_options(argc, argv, prefix, options, git_replace_usage, 0); - if (list && delete) - usage_msg_opt("-l and -d cannot be used together", - git_replace_usage, options); + if (!cmdmode) + cmdmode = argc ? MODE_REPLACE : MODE_LIST; - if (format && delete) - usage_msg_opt("--format and -d cannot be used together", + if (format && cmdmode != MODE_LIST) + usage_msg_opt("--format cannot be used when not listing", git_replace_usage, options); - if (force && (list || delete)) - usage_msg_opt("-f cannot be used with -d or -l", + if (force && cmdmode != MODE_REPLACE && cmdmode != MODE_EDIT) + usage_msg_opt("-f only makes sense when writing a replacement", git_replace_usage, options); - /* Delete refs */ - if (delete) { + switch (cmdmode) { + case MODE_DELETE: if (argc < 1) usage_msg_opt("-d needs at least one argument", git_replace_usage, options); return for_each_replace_name(argv, delete_replace_ref); - } - /* Replace object */ - if (!list && argc) { + case MODE_REPLACE: if (argc != 2) usage_msg_opt("bad number of arguments", git_replace_usage, options); - if (format) - usage_msg_opt("--format cannot be used when not listing", - git_replace_usage, options); return replace_object(argv[0], argv[1], force); - } - /* List refs, even if "list" is not set */ - if (argc > 1) - usage_msg_opt("only one pattern can be given with -l", - git_replace_usage, options); - if (force) - usage_msg_opt("-f needs some arguments", - git_replace_usage, options); + case MODE_EDIT: + if (argc != 1) + usage_msg_opt("-e needs exactly one argument", + git_replace_usage, options); + return edit_and_replace(argv[0], force); + + case MODE_LIST: + if (argc > 1) + usage_msg_opt("only one pattern can be given with -l", + git_replace_usage, options); + return list_replace_refs(argv[0], format); - return list_replace_refs(argv[0], format); + default: + die("BUG: invalid cmdmode %d", (int)cmdmode); + } } diff --git a/t/t6050-replace.sh b/t/t6050-replace.sh index 719a11673b..68b3cb26d9 100755 --- a/t/t6050-replace.sh +++ b/t/t6050-replace.sh @@ -318,6 +318,33 @@ test_expect_success 'test --format long' ' test_cmp expected actual ' +test_expect_success 'setup a fake editor' ' + write_script fakeeditor <<-\EOF + sed -e "s/A U Thor/A fake Thor/" "$1" >"$1.new" + mv "$1.new" "$1" + EOF +' + +test_expect_success '--edit with and without already replaced object' ' + test_must_fail env GIT_EDITOR=./fakeeditor git replace --edit "$PARA3" && + GIT_EDITOR=./fakeeditor git replace --force --edit "$PARA3" && + git replace -l | grep "$PARA3" && + git cat-file commit "$PARA3" | grep "A fake Thor" && + git replace -d "$PARA3" && + GIT_EDITOR=./fakeeditor git replace --edit "$PARA3" && + git replace -l | grep "$PARA3" && + git cat-file commit "$PARA3" | grep "A fake Thor" +' + +test_expect_success '--edit and change nothing or command failed' ' + git replace -d "$PARA3" && + test_must_fail env GIT_EDITOR=true git replace --edit "$PARA3" && + test_must_fail env GIT_EDITOR="./fakeeditor;false" git replace --edit "$PARA3" && + GIT_EDITOR=./fakeeditor git replace --edit "$PARA3" && + git replace -l | grep "$PARA3" && + git cat-file commit "$PARA3" | grep "A fake Thor" +' + test_expect_success 'replace ref cleanup' ' test -n "$(git replace)" && git replace -d $(git replace) && |