Merge branch 'cc/replace-graft'
authorJunio C Hamano <gitster@pobox.com>
Sun, 27 Jul 2014 22:14:18 +0000 (15:14 -0700)
committerJunio C Hamano <gitster@pobox.com>
Sun, 27 Jul 2014 22:14:18 +0000 (15:14 -0700)
"git replace" learned a "--graft" option to rewrite parents of a
commit.

* cc/replace-graft:
replace: add test for --graft with a mergetag
replace: check mergetags when using --graft
replace: add test for --graft with signed commit
replace: remove signature when using --graft
contrib: add convert-grafts-to-replace-refs.sh
Documentation: replace: add --graft option
replace: add test for --graft
replace: add --graft option
replace: cleanup redirection style in tests

1  2 
Documentation/git-replace.txt
builtin/replace.c
commit.c
commit.h
index 089dcac0478407a89aea702988adcb4135510180,e1be2e62709ee8b50e199e86310f6c5536869788..8fff598fd6e86c3e4a3e30e3c41f1c63ffb6a212
@@@ -10,6 -10,7 +10,7 @@@ SYNOPSI
  [verse]
  'git replace' [-f] <object> <replacement>
  'git replace' [-f] --edit <object>
+ 'git replace' [-f] --graft <commit> [<parent>...]
  'git replace' -d <object>...
  'git replace' [--format=<format>] [-l [<pattern>]]
  
@@@ -73,14 -74,15 +74,23 @@@ OPTION
        newly created object. See linkgit:git-var[1] for details about
        how the editor will be chosen.
  
 +--raw::
 +      When editing, provide the raw object contents rather than
 +      pretty-printed ones. Currently this only affects trees, which
 +      will be shown in their binary form. This is harder to work with,
 +      but can help when repairing a tree that is so corrupted it
 +      cannot be pretty-printed. Note that you may need to configure
 +      your editor to cleanly read and write binary data.
 +
+ --graft <commit> [<parent>...]::
+       Create a graft commit. A new commit is created with the same
+       content as <commit> except that its parents will be
+       [<parent>...] instead of <commit>'s parents. A replacement ref
+       is then created to replace <commit> with the newly created
+       commit. See contrib/convert-grafts-to-replace-refs.sh for an
+       example script based on this option that can convert grafts to
+       replace refs.
  -l <pattern>::
  --list <pattern>::
        List replace refs for objects that match the given pattern (or
diff --combined builtin/replace.c
index d1ea2c2e5606f2091efb487dc1599ec0a9116a6e,d29026fe215f428824753aba12f2e5782607cf2c..294b61b97e20ac9b5684bd6a180da37ebef43db9
  #include "refs.h"
  #include "parse-options.h"
  #include "run-command.h"
+ #include "tag.h"
  
  static const char * const git_replace_usage[] = {
        N_("git replace [-f] <object> <replacement>"),
        N_("git replace [-f] --edit <object>"),
+       N_("git replace [-f] --graft <commit> [<parent>...]"),
        N_("git replace -d <object>..."),
        N_("git replace [--format=<format>] [-l [<pattern>]]"),
        NULL
  };
  
  enum replace_format {
 -      REPLACE_FORMAT_SHORT,
 -      REPLACE_FORMAT_MEDIUM,
 -      REPLACE_FORMAT_LONG
 +      REPLACE_FORMAT_SHORT,
 +      REPLACE_FORMAT_MEDIUM,
 +      REPLACE_FORMAT_LONG
  };
  
  struct show_data {
@@@ -188,32 -190,27 +190,32 @@@ static int replace_object(const char *o
  }
  
  /*
 - * Write the contents of the object named by "sha1" to the file "filename",
 - * pretty-printed for human editing based on its type.
 + * Write the contents of the object named by "sha1" to the file "filename".
 + * If "raw" is true, then the object's raw contents are printed according to
 + * "type". Otherwise, we pretty-print the contents for human editing.
   */
 -static void export_object(const unsigned char *sha1, const char *filename)
 +static void export_object(const unsigned char *sha1, enum object_type type,
 +                        int raw, const char *filename)
  {
 -      const char *argv[] = { "--no-replace-objects", "cat-file", "-p", NULL, NULL };
 -      struct child_process cmd = { argv };
 +      struct child_process cmd = { NULL };
        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);
 +      argv_array_push(&cmd.args, "--no-replace-objects");
 +      argv_array_push(&cmd.args, "cat-file");
 +      if (raw)
 +              argv_array_push(&cmd.args, typename(type));
 +      else
 +              argv_array_push(&cmd.args, "-p");
 +      argv_array_push(&cmd.args, sha1_to_hex(sha1));
        cmd.git_cmd = 1;
        cmd.out = fd;
  
        if (run_command(&cmd))
                die("cat-file reported failure");
 -
 -      close(fd);
  }
  
  /*
   * 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 raw, const char *filename)
  {
        int fd;
  
        if (fd < 0)
                die_errno("unable to open %s for reading", filename);
  
 -      if (type == OBJ_TREE) {
 +      if (!raw && type == OBJ_TREE) {
                const char *argv[] = { "mktree", NULL };
                struct child_process cmd = { argv };
                struct strbuf result = STRBUF_INIT;
         */
  }
  
 -static int edit_and_replace(const char *object_ref, int force)
 +static int edit_and_replace(const char *object_ref, int force, int raw)
  {
        char *tmpfile = git_pathdup("REPLACE_EDITOBJ");
        enum object_type type;
  
        check_ref_valid(old, prev, ref, sizeof(ref), force);
  
 -      export_object(old, tmpfile);
 +      export_object(old, type, raw, tmpfile);
        if (launch_editor(tmpfile, NULL, NULL) < 0)
                die("editing object file failed");
 -      import_object(new, type, tmpfile);
 +      import_object(new, type, raw, tmpfile);
  
        free(tmpfile);
  
        return replace_object_sha1(object_ref, old, "replacement", new, force);
  }
  
+ static void replace_parents(struct strbuf *buf, int argc, const char **argv)
+ {
+       struct strbuf new_parents = STRBUF_INIT;
+       const char *parent_start, *parent_end;
+       int i;
+       /* find existing parents */
+       parent_start = buf->buf;
+       parent_start += 46; /* "tree " + "hex sha1" + "\n" */
+       parent_end = parent_start;
+       while (starts_with(parent_end, "parent "))
+               parent_end += 48; /* "parent " + "hex sha1" + "\n" */
+       /* prepare new parents */
+       for (i = 0; i < argc; i++) {
+               unsigned char sha1[20];
+               if (get_sha1(argv[i], sha1) < 0)
+                       die(_("Not a valid object name: '%s'"), argv[i]);
+               lookup_commit_or_die(sha1, argv[i]);
+               strbuf_addf(&new_parents, "parent %s\n", sha1_to_hex(sha1));
+       }
+       /* replace existing parents with new ones */
+       strbuf_splice(buf, parent_start - buf->buf, parent_end - parent_start,
+                     new_parents.buf, new_parents.len);
+       strbuf_release(&new_parents);
+ }
+ struct check_mergetag_data {
+       int argc;
+       const char **argv;
+ };
+ static void check_one_mergetag(struct commit *commit,
+                              struct commit_extra_header *extra,
+                              void *data)
+ {
+       struct check_mergetag_data *mergetag_data = (struct check_mergetag_data *)data;
+       const char *ref = mergetag_data->argv[0];
+       unsigned char tag_sha1[20];
+       struct tag *tag;
+       int i;
+       hash_sha1_file(extra->value, extra->len, typename(OBJ_TAG), tag_sha1);
+       tag = lookup_tag(tag_sha1);
+       if (!tag)
+               die(_("bad mergetag in commit '%s'"), ref);
+       if (parse_tag_buffer(tag, extra->value, extra->len))
+               die(_("malformed mergetag in commit '%s'"), ref);
+       /* iterate over new parents */
+       for (i = 1; i < mergetag_data->argc; i++) {
+               unsigned char sha1[20];
+               if (get_sha1(mergetag_data->argv[i], sha1) < 0)
+                       die(_("Not a valid object name: '%s'"), mergetag_data->argv[i]);
+               if (!hashcmp(tag->tagged->sha1, sha1))
+                       return; /* found */
+       }
+       die(_("original commit '%s' contains mergetag '%s' that is discarded; "
+             "use --edit instead of --graft"), ref, sha1_to_hex(tag_sha1));
+ }
+ static void check_mergetags(struct commit *commit, int argc, const char **argv)
+ {
+       struct check_mergetag_data mergetag_data;
+       mergetag_data.argc = argc;
+       mergetag_data.argv = argv;
+       for_each_mergetag(check_one_mergetag, commit, &mergetag_data);
+ }
+ static int create_graft(int argc, const char **argv, int force)
+ {
+       unsigned char old[20], new[20];
+       const char *old_ref = argv[0];
+       struct commit *commit;
+       struct strbuf buf = STRBUF_INIT;
+       const char *buffer;
+       unsigned long size;
+       if (get_sha1(old_ref, old) < 0)
+               die(_("Not a valid object name: '%s'"), old_ref);
+       commit = lookup_commit_or_die(old, old_ref);
+       buffer = get_commit_buffer(commit, &size);
+       strbuf_add(&buf, buffer, size);
+       unuse_commit_buffer(commit, buffer);
+       replace_parents(&buf, argc - 1, &argv[1]);
+       if (remove_signature(&buf)) {
+               warning(_("the original commit '%s' has a gpg signature."), old_ref);
+               warning(_("the signature will be removed in the replacement commit!"));
+       }
+       check_mergetags(commit, argc, argv);
+       if (write_sha1_file(buf.buf, buf.len, commit_type, new))
+               die(_("could not write replacement commit for: '%s'"), old_ref);
+       strbuf_release(&buf);
+       if (!hashcmp(old, new))
+               return error("new commit is the same as the old one: '%s'", sha1_to_hex(old));
+       return replace_object_sha1(old_ref, old, "replacement", new, force);
+ }
  int cmd_replace(int argc, const char **argv, const char *prefix)
  {
        int force = 0;
 +      int raw = 0;
        const char *format = NULL;
        enum {
                MODE_UNSPECIFIED = 0,
                MODE_LIST,
                MODE_DELETE,
                MODE_EDIT,
+               MODE_GRAFT,
                MODE_REPLACE
        } cmdmode = MODE_UNSPECIFIED;
        struct option options[] = {
                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_CMDMODE('g', "graft", &cmdmode, N_("change a commit's parents"), MODE_GRAFT),
                OPT_BOOL('f', "force", &force, N_("replace the ref if it exists")),
 +              OPT_BOOL(0, "raw", &raw, N_("do not pretty-print contents for --edit")),
                OPT_STRING(0, "format", &format, N_("format"), N_("use this format")),
                OPT_END()
        };
                usage_msg_opt("--format cannot be used when not listing",
                              git_replace_usage, options);
  
-       if (force && cmdmode != MODE_REPLACE && cmdmode != MODE_EDIT)
+       if (force &&
+           cmdmode != MODE_REPLACE &&
+           cmdmode != MODE_EDIT &&
+           cmdmode != MODE_GRAFT)
                usage_msg_opt("-f only makes sense when writing a replacement",
                              git_replace_usage, options);
  
 +      if (raw && cmdmode != MODE_EDIT)
 +              usage_msg_opt("--raw only makes sense with --edit",
 +                            git_replace_usage, options);
 +
        switch (cmdmode) {
        case MODE_DELETE:
                if (argc < 1)
                if (argc != 1)
                        usage_msg_opt("-e needs exactly one argument",
                                      git_replace_usage, options);
 -              return edit_and_replace(argv[0], force);
 +              return edit_and_replace(argv[0], force, raw);
  
+       case MODE_GRAFT:
+               if (argc < 1)
+                       usage_msg_opt("-g needs at least one argument",
+                                     git_replace_usage, options);
+               return create_graft(argc, argv, force);
        case MODE_LIST:
                if (argc > 1)
                        usage_msg_opt("only one pattern can be given with -l",
diff --combined commit.c
index 2274e43fdfa03f2622cd3c461d65795af1ac2155,ca26c69452d681fb8e1bb1cfddb305491a086270..ae7f2b10f4e08a549614346c8262b10503cca953
+++ b/commit.c
@@@ -18,6 -18,19 +18,6 @@@ int save_commit_buffer = 1
  
  const char *commit_type = "commit";
  
 -static struct commit *check_commit(struct object *obj,
 -                                 const unsigned char *sha1,
 -                                 int quiet)
 -{
 -      if (obj->type != OBJ_COMMIT) {
 -              if (!quiet)
 -                      error("Object %s is a %s, not a commit",
 -                            sha1_to_hex(sha1), typename(obj->type));
 -              return NULL;
 -      }
 -      return (struct commit *) obj;
 -}
 -
  struct commit *lookup_commit_reference_gently(const unsigned char *sha1,
                                              int quiet)
  {
@@@ -25,7 -38,7 +25,7 @@@
  
        if (!obj)
                return NULL;
 -      return check_commit(obj, sha1, quiet);
 +      return object_as_type(obj, OBJ_COMMIT, quiet);
  }
  
  struct commit *lookup_commit_reference(const unsigned char *sha1)
@@@ -48,9 -61,13 +48,9 @@@ struct commit *lookup_commit_or_die(con
  struct commit *lookup_commit(const unsigned char *sha1)
  {
        struct object *obj = lookup_object(sha1);
 -      if (!obj) {
 -              struct commit *c = alloc_commit_node();
 -              return create_object(sha1, OBJ_COMMIT, c);
 -      }
 -      if (!obj->type)
 -              obj->type = OBJ_COMMIT;
 -      return check_commit(obj, sha1, 0);
 +      if (!obj)
 +              return create_object(sha1, alloc_commit_node());
 +      return object_as_type(obj, OBJ_COMMIT, 0);
  }
  
  struct commit *lookup_commit_reference_by_name(const char *name)
@@@ -430,7 -447,12 +430,7 @@@ struct commit_list *copy_commit_list(st
        struct commit_list *head = NULL;
        struct commit_list **pp = &head;
        while (list) {
 -              struct commit_list *new;
 -              new = xmalloc(sizeof(struct commit_list));
 -              new->item = list->item;
 -              new->next = NULL;
 -              *pp = new;
 -              pp = &new->next;
 +              pp = commit_list_append(list->item, pp);
                list = list->next;
        }
        return head;
@@@ -592,7 -614,8 +592,7 @@@ static void record_author_date(struct a
  
        for (buf = buffer; buf; buf = line_end + 1) {
                line_end = strchrnul(buf, '\n');
 -              ident_line = skip_prefix(buf, "author ");
 -              if (!ident_line) {
 +              if (!skip_prefix(buf, "author ", &ident_line)) {
                        if (!line_end[0] || line_end[1] == '\n')
                                return; /* end of header */
                        continue;
@@@ -764,41 -787,45 +764,41 @@@ void sort_in_topological_order(struct c
  
  static const unsigned all_flags = (PARENT1 | PARENT2 | STALE | RESULT);
  
 -static struct commit *interesting(struct commit_list *list)
 +static int queue_has_nonstale(struct prio_queue *queue)
  {
 -      while (list) {
 -              struct commit *commit = list->item;
 -              list = list->next;
 -              if (commit->object.flags & STALE)
 -                      continue;
 -              return commit;
 +      int i;
 +      for (i = 0; i < queue->nr; i++) {
 +              struct commit *commit = queue->array[i].data;
 +              if (!(commit->object.flags & STALE))
 +                      return 1;
        }
 -      return NULL;
 +      return 0;
  }
  
  /* all input commits in one and twos[] must have been parsed! */
  static struct commit_list *paint_down_to_common(struct commit *one, int n, struct commit **twos)
  {
 -      struct commit_list *list = NULL;
 +      struct prio_queue queue = { compare_commits_by_commit_date };
        struct commit_list *result = NULL;
        int i;
  
        one->object.flags |= PARENT1;
 -      commit_list_insert_by_date(one, &list);
 -      if (!n)
 -              return list;
 +      if (!n) {
 +              commit_list_append(one, &result);
 +              return result;
 +      }
 +      prio_queue_put(&queue, one);
 +
        for (i = 0; i < n; i++) {
                twos[i]->object.flags |= PARENT2;
 -              commit_list_insert_by_date(twos[i], &list);
 +              prio_queue_put(&queue, twos[i]);
        }
  
 -      while (interesting(list)) {
 -              struct commit *commit;
 +      while (queue_has_nonstale(&queue)) {
 +              struct commit *commit = prio_queue_get(&queue);
                struct commit_list *parents;
 -              struct commit_list *next;
                int flags;
  
 -              commit = list->item;
 -              next = list->next;
 -              free(list);
 -              list = next;
 -
                flags = commit->object.flags & (PARENT1 | PARENT2 | STALE);
                if (flags == (PARENT1 | PARENT2)) {
                        if (!(commit->object.flags & RESULT)) {
                        if (parse_commit(p))
                                return NULL;
                        p->object.flags |= flags;
 -                      commit_list_insert_by_date(p, &list);
 +                      prio_queue_put(&queue, p);
                }
        }
  
 -      free_commit_list(list);
 +      clear_prio_queue(&queue);
        return result;
  }
  
@@@ -966,7 -993,12 +966,7 @@@ struct commit_list *get_merge_bases_man
        }
  
        /* There are more than one */
 -      cnt = 0;
 -      list = result;
 -      while (list) {
 -              list = list->next;
 -              cnt++;
 -      }
 +      cnt = commit_list_count(result);
        rslt = xcalloc(cnt, sizeof(*rslt));
        for (list = result, i = 0; list; list = list->next)
                rslt[i++] = list->item;
@@@ -1146,6 -1178,40 +1146,40 @@@ int parse_signed_commit(const struct co
        return saw_signature;
  }
  
+ int remove_signature(struct strbuf *buf)
+ {
+       const char *line = buf->buf;
+       const char *tail = buf->buf + buf->len;
+       int in_signature = 0;
+       const char *sig_start = NULL;
+       const char *sig_end = NULL;
+       while (line < tail) {
+               const char *next = memchr(line, '\n', tail - line);
+               next = next ? next + 1 : tail;
+               if (in_signature && line[0] == ' ')
+                       sig_end = next;
+               else if (starts_with(line, gpg_sig_header) &&
+                        line[gpg_sig_header_len] == ' ') {
+                       sig_start = line;
+                       sig_end = next;
+                       in_signature = 1;
+               } else {
+                       if (*line == '\n')
+                               /* dump the whole remainder of the buffer */
+                               next = tail;
+                       in_signature = 0;
+               }
+               line = next;
+       }
+       if (sig_start)
+               strbuf_remove(buf, sig_start - buf->buf, sig_end - sig_start);
+       return sig_start != NULL;
+ }
  static void handle_signed_tag(struct commit *parent, struct commit_extra_header ***tail)
  {
        struct merge_remote_desc *desc;
@@@ -1205,7 -1271,8 +1239,7 @@@ static void parse_gpg_output(struct sig
        for (i = 0; i < ARRAY_SIZE(sigcheck_gpg_status); i++) {
                const char *found, *next;
  
 -              found = skip_prefix(buf, sigcheck_gpg_status[i].check + 1);
 -              if (!found) {
 +              if (!skip_prefix(buf, sigcheck_gpg_status[i].check + 1, &found)) {
                        found = strstr(buf, sigcheck_gpg_status[i].check);
                        if (!found)
                                continue;
@@@ -1239,7 -1306,6 +1273,7 @@@ void check_commit_signature(const struc
                                      &gpg_output, &gpg_status);
        if (status && !gpg_output.len)
                goto out;
 +      sigc->payload = strbuf_detach(&payload, NULL);
        sigc->gpg_output = strbuf_detach(&gpg_output, NULL);
        sigc->gpg_status = strbuf_detach(&gpg_status, NULL);
        parse_gpg_output(sigc);
diff --combined commit.h
index f43079c0c3a980664d155b41fa729f68cd647713,c81ba8530ce340cf4e81ca8f403361081d1513d3..a8cbf52f15ff01778ba61a5a888263d424b2d8d2
+++ b/commit.h
@@@ -271,7 -271,6 +271,7 @@@ extern void assign_shallow_commits_to_r
                                           int *ref_status);
  extern int delayed_reachability_test(struct shallow_info *si, int c);
  extern void prune_shallow(int show_only);
 +extern struct trace_key trace_shallow;
  
  int is_descendant_of(struct commit *, struct commit_list *);
  int in_merge_bases(struct commit *, struct commit *);
@@@ -333,6 -332,8 +333,8 @@@ struct commit *get_merge_parent(const c
  
  extern int parse_signed_commit(const struct commit *commit,
                               struct strbuf *message, struct strbuf *signature);
+ extern int remove_signature(struct strbuf *buf);
  extern void print_commit_list(struct commit_list *list,
                              const char *format_cur,
                              const char *format_last);