Merge branch 'jh/notes' (early part)
authorJunio C Hamano <gitster@pobox.com>
Thu, 21 Jan 2010 04:28:49 +0000 (20:28 -0800)
committerJunio C Hamano <gitster@pobox.com>
Thu, 21 Jan 2010 04:28:49 +0000 (20:28 -0800)
* 'jh/notes' (early part):
Add more testcases to test fast-import of notes
Rename t9301 to t9350, to make room for more fast-import tests
fast-import: Proper notes tree manipulation

1  2 
fast-import.c
t/t9300-fast-import.sh
diff --combined fast-import.c
index 25c3588385af63fdb9782ece39a1a4533b805260,f04cd1751ae268692b128e1c8b10370c5872d548..901784fe911567752c872e5433f0295768654b5a
@@@ -19,8 -19,8 +19,8 @@@ Format of STDIN stream
  
    new_commit ::= 'commit' sp ref_str lf
      mark?
 -    ('author' sp name sp '<' email '>' sp when lf)?
 -    'committer' sp name sp '<' email '>' sp when lf
 +    ('author' (sp name)? sp '<' email '>' sp when lf)?
 +    'committer' (sp name)? sp '<' email '>' sp when lf
      commit_msg
      ('from' sp committish lf)?
      ('merge' sp committish lf)*
@@@ -47,7 -47,7 +47,7 @@@
  
    new_tag ::= 'tag' sp tag_str lf
      'from' sp committish lf
 -    ('tagger' sp name sp '<' email '>' sp when lf)?
 +    ('tagger' (sp name)? sp '<' email '>' sp when lf)?
      tag_msg;
    tag_msg ::= data;
  
@@@ -245,6 -245,7 +245,7 @@@ struct branc
        const char *name;
        struct tree_entry branch_tree;
        uintmax_t last_commit;
+       uintmax_t num_notes;
        unsigned active : 1;
        unsigned pack_id : PACK_ID_BITS;
        unsigned char sha1[20];
@@@ -295,9 -296,6 +296,9 @@@ static unsigned long branch_count
  static unsigned long branch_load_count;
  static int failure;
  static FILE *pack_edges;
 +static unsigned int show_stats = 1;
 +static int global_argc;
 +static const char **global_argv;
  
  /* Memory pools */
  static size_t mem_pool_alloc = 2*1024*1024 - sizeof(struct mem_pool);
@@@ -320,10 -318,7 +321,10 @@@ static unsigned int object_entry_alloc 
  static struct object_entry_pool *blocks;
  static struct object_entry *object_table[1 << 16];
  static struct mark_set *marks;
 -static const char *mark_file;
 +static const char *export_marks_file;
 +static const char *import_marks_file;
 +static int import_marks_file_from_stream;
 +static int relative_marks_paths;
  
  /* Our last blob */
  static struct last_object last_blob = { STRBUF_INIT, 0, 0, 0 };
@@@ -357,9 -352,6 +358,9 @@@ static struct recent_command *rc_free
  static unsigned int cmd_save = 100;
  static uintmax_t next_mark;
  static struct strbuf new_data = STRBUF_INIT;
 +static int seen_data_command;
 +
 +static void parse_argv(void);
  
  static void write_branch_report(FILE *rpt, struct branch *b)
  {
@@@ -463,8 -455,8 +464,8 @@@ static void write_crash_report(const ch
        fputc('\n', rpt);
        fputs("Marks\n", rpt);
        fputs("-----\n", rpt);
 -      if (mark_file)
 -              fprintf(rpt, "  exported to %s\n", mark_file);
 +      if (export_marks_file)
 +              fprintf(rpt, "  exported to %s\n", export_marks_file);
        else
                dump_marks_helper(rpt, 0, marks);
  
@@@ -702,6 -694,7 +703,7 @@@ static struct branch *new_branch(const 
        b->table_next_branch = branch_table[hc];
        b->branch_tree.versions[0].mode = S_IFDIR;
        b->branch_tree.versions[1].mode = S_IFDIR;
+       b->num_notes = 0;
        b->active = 0;
        b->pack_id = MAX_PACK_ID;
        branch_table[hc] = b;
@@@ -1611,13 -1604,13 +1613,13 @@@ static void dump_marks(void
        int mark_fd;
        FILE *f;
  
 -      if (!mark_file)
 +      if (!export_marks_file)
                return;
  
 -      mark_fd = hold_lock_file_for_update(&mark_lock, mark_file, 0);
 +      mark_fd = hold_lock_file_for_update(&mark_lock, export_marks_file, 0);
        if (mark_fd < 0) {
                failure |= error("Unable to write marks file %s: %s",
 -                      mark_file, strerror(errno));
 +                      export_marks_file, strerror(errno));
                return;
        }
  
                int saved_errno = errno;
                rollback_lock_file(&mark_lock);
                failure |= error("Unable to write marks file %s: %s",
 -                      mark_file, strerror(saved_errno));
 +                      export_marks_file, strerror(saved_errno));
                return;
        }
  
                int saved_errno = errno;
                rollback_lock_file(&mark_lock);
                failure |= error("Unable to write marks file %s: %s",
 -                      mark_file, strerror(saved_errno));
 +                      export_marks_file, strerror(saved_errno));
                return;
        }
  
                int saved_errno = errno;
                rollback_lock_file(&mark_lock);
                failure |= error("Unable to commit marks file %s: %s",
 -                      mark_file, strerror(saved_errno));
 +                      export_marks_file, strerror(saved_errno));
                return;
        }
  }
  
 +static void read_marks(void)
 +{
 +      char line[512];
 +      FILE *f = fopen(import_marks_file, "r");
 +      if (!f)
 +              die_errno("cannot read '%s'", import_marks_file);
 +      while (fgets(line, sizeof(line), f)) {
 +              uintmax_t mark;
 +              char *end;
 +              unsigned char sha1[20];
 +              struct object_entry *e;
 +
 +              end = strchr(line, '\n');
 +              if (line[0] != ':' || !end)
 +                      die("corrupt mark line: %s", line);
 +              *end = 0;
 +              mark = strtoumax(line + 1, &end, 10);
 +              if (!mark || end == line + 1
 +                      || *end != ' ' || get_sha1(end + 1, sha1))
 +                      die("corrupt mark line: %s", line);
 +              e = find_object(sha1);
 +              if (!e) {
 +                      enum object_type type = sha1_object_info(sha1, NULL);
 +                      if (type < 0)
 +                              die("object not found: %s", sha1_to_hex(sha1));
 +                      e = insert_object(sha1);
 +                      e->type = type;
 +                      e->pack_id = MAX_PACK_ID;
 +                      e->offset = 1; /* just not zero! */
 +              }
 +              insert_mark(mark, e);
 +      }
 +      fclose(f);
 +}
 +
 +
  static int read_next_command(void)
  {
        static int stdin_eof = 0;
                        if (stdin_eof)
                                return EOF;
  
 +                      if (!seen_data_command
 +                              && prefixcmp(command_buf.buf, "feature ")
 +                              && prefixcmp(command_buf.buf, "option ")) {
 +                              parse_argv();
 +                      }
 +
                        rc = rc_free;
                        if (rc)
                                rc_free = rc->next;
@@@ -1911,6 -1862,109 +1913,109 @@@ static void load_branch(struct branch *
        }
  }
  
+ static unsigned char convert_num_notes_to_fanout(uintmax_t num_notes)
+ {
+       unsigned char fanout = 0;
+       while ((num_notes >>= 8))
+               fanout++;
+       return fanout;
+ }
+ static void construct_path_with_fanout(const char *hex_sha1,
+               unsigned char fanout, char *path)
+ {
+       unsigned int i = 0, j = 0;
+       if (fanout >= 20)
+               die("Too large fanout (%u)", fanout);
+       while (fanout) {
+               path[i++] = hex_sha1[j++];
+               path[i++] = hex_sha1[j++];
+               path[i++] = '/';
+               fanout--;
+       }
+       memcpy(path + i, hex_sha1 + j, 40 - j);
+       path[i + 40 - j] = '\0';
+ }
+ static uintmax_t do_change_note_fanout(
+               struct tree_entry *orig_root, struct tree_entry *root,
+               char *hex_sha1, unsigned int hex_sha1_len,
+               char *fullpath, unsigned int fullpath_len,
+               unsigned char fanout)
+ {
+       struct tree_content *t = root->tree;
+       struct tree_entry *e, leaf;
+       unsigned int i, tmp_hex_sha1_len, tmp_fullpath_len;
+       uintmax_t num_notes = 0;
+       unsigned char sha1[20];
+       char realpath[60];
+       for (i = 0; t && i < t->entry_count; i++) {
+               e = t->entries[i];
+               tmp_hex_sha1_len = hex_sha1_len + e->name->str_len;
+               tmp_fullpath_len = fullpath_len;
+               /*
+                * We're interested in EITHER existing note entries (entries
+                * with exactly 40 hex chars in path, not including directory
+                * separators), OR directory entries that may contain note
+                * entries (with < 40 hex chars in path).
+                * Also, each path component in a note entry must be a multiple
+                * of 2 chars.
+                */
+               if (!e->versions[1].mode ||
+                   tmp_hex_sha1_len > 40 ||
+                   e->name->str_len % 2)
+                       continue;
+               /* This _may_ be a note entry, or a subdir containing notes */
+               memcpy(hex_sha1 + hex_sha1_len, e->name->str_dat,
+                      e->name->str_len);
+               if (tmp_fullpath_len)
+                       fullpath[tmp_fullpath_len++] = '/';
+               memcpy(fullpath + tmp_fullpath_len, e->name->str_dat,
+                      e->name->str_len);
+               tmp_fullpath_len += e->name->str_len;
+               fullpath[tmp_fullpath_len] = '\0';
+               if (tmp_hex_sha1_len == 40 && !get_sha1_hex(hex_sha1, sha1)) {
+                       /* This is a note entry */
+                       construct_path_with_fanout(hex_sha1, fanout, realpath);
+                       if (!strcmp(fullpath, realpath)) {
+                               /* Note entry is in correct location */
+                               num_notes++;
+                               continue;
+                       }
+                       /* Rename fullpath to realpath */
+                       if (!tree_content_remove(orig_root, fullpath, &leaf))
+                               die("Failed to remove path %s", fullpath);
+                       tree_content_set(orig_root, realpath,
+                               leaf.versions[1].sha1,
+                               leaf.versions[1].mode,
+                               leaf.tree);
+               } else if (S_ISDIR(e->versions[1].mode)) {
+                       /* This is a subdir that may contain note entries */
+                       if (!e->tree)
+                               load_tree(e);
+                       num_notes += do_change_note_fanout(orig_root, e,
+                               hex_sha1, tmp_hex_sha1_len,
+                               fullpath, tmp_fullpath_len, fanout);
+               }
+               /* The above may have reallocated the current tree_content */
+               t = root->tree;
+       }
+       return num_notes;
+ }
+ static uintmax_t change_note_fanout(struct tree_entry *root,
+               unsigned char fanout)
+ {
+       char hex_sha1[40], path[60];
+       return do_change_note_fanout(root, root, hex_sha1, 0, path, 0, fanout);
+ }
  static void file_change_m(struct branch *b)
  {
        const char *p = command_buf.buf + 2;
@@@ -2061,14 -2115,16 +2166,16 @@@ static void file_change_cr(struct branc
                leaf.tree);
  }
  
- static void note_change_n(struct branch *b)
+ static void note_change_n(struct branch *b, unsigned char old_fanout)
  {
        const char *p = command_buf.buf + 2;
        static struct strbuf uq = STRBUF_INIT;
        struct object_entry *oe = oe;
        struct branch *s;
        unsigned char sha1[20], commit_sha1[20];
+       char path[60];
        uint16_t inline_data = 0;
+       unsigned char new_fanout;
  
        /* <dataref> or 'inline' */
        if (*p == ':') {
                if (oe->type != OBJ_BLOB)
                        die("Not a blob (actually a %s): %s",
                                typename(oe->type), command_buf.buf);
-       } else {
+       } else if (!is_null_sha1(sha1)) {
                enum object_type type = sha1_object_info(sha1, NULL);
                if (type < 0)
                        die("Blob not found: %s", command_buf.buf);
                            typename(type), command_buf.buf);
        }
  
-       tree_content_set(&b->branch_tree, sha1_to_hex(commit_sha1), sha1,
-               S_IFREG | 0644, NULL);
+       construct_path_with_fanout(sha1_to_hex(commit_sha1), old_fanout, path);
+       if (tree_content_remove(&b->branch_tree, path, NULL))
+               b->num_notes--;
+       if (is_null_sha1(sha1))
+               return; /* nothing to insert */
+       b->num_notes++;
+       new_fanout = convert_num_notes_to_fanout(b->num_notes);
+       construct_path_with_fanout(sha1_to_hex(commit_sha1), new_fanout, path);
+       tree_content_set(&b->branch_tree, path, sha1, S_IFREG | 0644, NULL);
  }
  
  static void file_change_deleteall(struct branch *b)
        hashclr(b->branch_tree.versions[0].sha1);
        hashclr(b->branch_tree.versions[1].sha1);
        load_tree(&b->branch_tree);
+       b->num_notes = 0;
  }
  
  static void parse_from_commit(struct branch *b, char *buf, unsigned long size)
@@@ -2264,6 -2330,7 +2381,7 @@@ static void parse_new_commit(void
        char *committer = NULL;
        struct hash_list *merge_list = NULL;
        unsigned int merge_count;
+       unsigned char prev_fanout, new_fanout;
  
        /* Obtain the branch name from the rest of our command */
        sp = strchr(command_buf.buf, ' ') + 1;
                load_branch(b);
        }
  
+       prev_fanout = convert_num_notes_to_fanout(b->num_notes);
        /* file_change* */
        while (command_buf.len > 0) {
                if (!prefixcmp(command_buf.buf, "M "))
                else if (!prefixcmp(command_buf.buf, "C "))
                        file_change_cr(b, 0);
                else if (!prefixcmp(command_buf.buf, "N "))
-                       note_change_n(b);
+                       note_change_n(b, prev_fanout);
                else if (!strcmp("deleteall", command_buf.buf))
                        file_change_deleteall(b);
                else {
                        break;
        }
  
+       new_fanout = convert_num_notes_to_fanout(b->num_notes);
+       if (new_fanout != prev_fanout)
+               b->num_notes = change_note_fanout(&b->branch_tree, new_fanout);
        /* build the tree and the commit */
        store_tree(&b->branch_tree);
        hashcpy(b->branch_tree.versions[0].sha1,
@@@ -2356,7 -2429,6 +2480,7 @@@ static void parse_new_tag(void
        struct tag *t;
        uintmax_t from_mark = 0;
        unsigned char sha1[20];
 +      enum object_type type;
  
        /* Obtain the new tag name from the rest of our command */
        sp = strchr(command_buf.buf, ' ') + 1;
        s = lookup_branch(from);
        if (s) {
                hashcpy(sha1, s->sha1);
 +              type = OBJ_COMMIT;
        } else if (*from == ':') {
                struct object_entry *oe;
                from_mark = strtoumax(from + 1, NULL, 10);
                oe = find_mark(from_mark);
 -              if (oe->type != OBJ_COMMIT)
 -                      die("Mark :%" PRIuMAX " not a commit", from_mark);
 +              type = oe->type;
                hashcpy(sha1, oe->sha1);
        } else if (!get_sha1(from, sha1)) {
                unsigned long size;
                char *buf;
  
 -              buf = read_object_with_reference(sha1,
 -                      commit_type, &size, sha1);
 +              buf = read_sha1_file(sha1, &type, &size);
                if (!buf || size < 46)
                        die("Not a valid commit: %s", from);
                free(buf);
                    "object %s\n"
                    "type %s\n"
                    "tag %s\n",
 -                  sha1_to_hex(sha1), commit_type, t->name);
 +                  sha1_to_hex(sha1), typename(type), t->name);
        if (tagger)
                strbuf_addf(&new_data,
                            "tagger %s\n", tagger);
@@@ -2471,140 -2544,39 +2595,140 @@@ static void parse_progress(void
        skip_optional_lf();
  }
  
 -static void import_marks(const char *input_file)
 +static char* make_fast_import_path(const char *path)
  {
 -      char line[512];
 -      FILE *f = fopen(input_file, "r");
 -      if (!f)
 -              die_errno("cannot read '%s'", input_file);
 -      while (fgets(line, sizeof(line), f)) {
 -              uintmax_t mark;
 -              char *end;
 -              unsigned char sha1[20];
 -              struct object_entry *e;
 +      struct strbuf abs_path = STRBUF_INIT;
  
 -              end = strchr(line, '\n');
 -              if (line[0] != ':' || !end)
 -                      die("corrupt mark line: %s", line);
 -              *end = 0;
 -              mark = strtoumax(line + 1, &end, 10);
 -              if (!mark || end == line + 1
 -                      || *end != ' ' || get_sha1(end + 1, sha1))
 -                      die("corrupt mark line: %s", line);
 -              e = find_object(sha1);
 -              if (!e) {
 -                      enum object_type type = sha1_object_info(sha1, NULL);
 -                      if (type < 0)
 -                              die("object not found: %s", sha1_to_hex(sha1));
 -                      e = insert_object(sha1);
 -                      e->type = type;
 -                      e->pack_id = MAX_PACK_ID;
 -                      e->offset = 1; /* just not zero! */
 -              }
 -              insert_mark(mark, e);
 +      if (!relative_marks_paths || is_absolute_path(path))
 +              return xstrdup(path);
 +      strbuf_addf(&abs_path, "%s/info/fast-import/%s", get_git_dir(), path);
 +      return strbuf_detach(&abs_path, NULL);
 +}
 +
 +static void option_import_marks(const char *marks, int from_stream)
 +{
 +      if (import_marks_file) {
 +              if (from_stream)
 +                      die("Only one import-marks command allowed per stream");
 +
 +              /* read previous mark file */
 +              if(!import_marks_file_from_stream)
 +                      read_marks();
        }
 -      fclose(f);
 +
 +      import_marks_file = make_fast_import_path(marks);
 +      import_marks_file_from_stream = from_stream;
 +}
 +
 +static void option_date_format(const char *fmt)
 +{
 +      if (!strcmp(fmt, "raw"))
 +              whenspec = WHENSPEC_RAW;
 +      else if (!strcmp(fmt, "rfc2822"))
 +              whenspec = WHENSPEC_RFC2822;
 +      else if (!strcmp(fmt, "now"))
 +              whenspec = WHENSPEC_NOW;
 +      else
 +              die("unknown --date-format argument %s", fmt);
 +}
 +
 +static void option_max_pack_size(const char *packsize)
 +{
 +      max_packsize = strtoumax(packsize, NULL, 0) * 1024 * 1024;
 +}
 +
 +static void option_depth(const char *depth)
 +{
 +      max_depth = strtoul(depth, NULL, 0);
 +      if (max_depth > MAX_DEPTH)
 +              die("--depth cannot exceed %u", MAX_DEPTH);
 +}
 +
 +static void option_active_branches(const char *branches)
 +{
 +      max_active_branches = strtoul(branches, NULL, 0);
 +}
 +
 +static void option_export_marks(const char *marks)
 +{
 +      export_marks_file = make_fast_import_path(marks);
 +}
 +
 +static void option_export_pack_edges(const char *edges)
 +{
 +      if (pack_edges)
 +              fclose(pack_edges);
 +      pack_edges = fopen(edges, "a");
 +      if (!pack_edges)
 +              die_errno("Cannot open '%s'", edges);
 +}
 +
 +static int parse_one_option(const char *option)
 +{
 +      if (!prefixcmp(option, "max-pack-size=")) {
 +              option_max_pack_size(option + 14);
 +      } else if (!prefixcmp(option, "depth=")) {
 +              option_depth(option + 6);
 +      } else if (!prefixcmp(option, "active-branches=")) {
 +              option_active_branches(option + 16);
 +      } else if (!prefixcmp(option, "export-pack-edges=")) {
 +              option_export_pack_edges(option + 18);
 +      } else if (!prefixcmp(option, "quiet")) {
 +              show_stats = 0;
 +      } else if (!prefixcmp(option, "stats")) {
 +              show_stats = 1;
 +      } else {
 +              return 0;
 +      }
 +
 +      return 1;
 +}
 +
 +static int parse_one_feature(const char *feature, int from_stream)
 +{
 +      if (!prefixcmp(feature, "date-format=")) {
 +              option_date_format(feature + 12);
 +      } else if (!prefixcmp(feature, "import-marks=")) {
 +              option_import_marks(feature + 13, from_stream);
 +      } else if (!prefixcmp(feature, "export-marks=")) {
 +              option_export_marks(feature + 13);
 +      } else if (!prefixcmp(feature, "relative-marks")) {
 +              relative_marks_paths = 1;
 +      } else if (!prefixcmp(feature, "no-relative-marks")) {
 +              relative_marks_paths = 0;
 +      } else if (!prefixcmp(feature, "force")) {
 +              force_update = 1;
 +      } else {
 +              return 0;
 +      }
 +
 +      return 1;
 +}
 +
 +static void parse_feature(void)
 +{
 +      char *feature = command_buf.buf + 8;
 +
 +      if (seen_data_command)
 +              die("Got feature command '%s' after data command", feature);
 +
 +      if (parse_one_feature(feature, 1))
 +              return;
 +
 +      die("This version of fast-import does not support feature %s.", feature);
 +}
 +
 +static void parse_option(void)
 +{
 +      char *option = command_buf.buf + 11;
 +
 +      if (seen_data_command)
 +              die("Got option command '%s' after data command", option);
 +
 +      if (parse_one_option(option))
 +              return;
 +
 +      die("This version of fast-import does not support option: %s", option);
  }
  
  static int git_pack_config(const char *k, const char *v, void *cb)
  static const char fast_import_usage[] =
  "git fast-import [--date-format=f] [--max-pack-size=n] [--depth=n] [--active-branches=n] [--export-marks=marks.file]";
  
 +static void parse_argv(void)
 +{
 +      unsigned int i;
 +
 +      for (i = 1; i < global_argc; i++) {
 +              const char *a = global_argv[i];
 +
 +              if (*a != '-' || !strcmp(a, "--"))
 +                      break;
 +
 +              if (parse_one_option(a + 2))
 +                      continue;
 +
 +              if (parse_one_feature(a + 2, 0))
 +                      continue;
 +
 +              die("unknown option %s", a);
 +      }
 +      if (i != global_argc)
 +              usage(fast_import_usage);
 +
 +      seen_data_command = 1;
 +      if (import_marks_file)
 +              read_marks();
 +}
 +
  int main(int argc, const char **argv)
  {
 -      unsigned int i, show_stats = 1;
 +      unsigned int i;
  
        git_extract_argv0_path(argv[0]);
  
        avail_tree_table = xcalloc(avail_tree_table_sz, sizeof(struct avail_tree_content*));
        marks = pool_calloc(1, sizeof(struct mark_set));
  
 -      for (i = 1; i < argc; i++) {
 -              const char *a = argv[i];
 -
 -              if (*a != '-' || !strcmp(a, "--"))
 -                      break;
 -              else if (!prefixcmp(a, "--date-format=")) {
 -                      const char *fmt = a + 14;
 -                      if (!strcmp(fmt, "raw"))
 -                              whenspec = WHENSPEC_RAW;
 -                      else if (!strcmp(fmt, "rfc2822"))
 -                              whenspec = WHENSPEC_RFC2822;
 -                      else if (!strcmp(fmt, "now"))
 -                              whenspec = WHENSPEC_NOW;
 -                      else
 -                              die("unknown --date-format argument %s", fmt);
 -              }
 -              else if (!prefixcmp(a, "--max-pack-size="))
 -                      max_packsize = strtoumax(a + 16, NULL, 0) * 1024 * 1024;
 -              else if (!prefixcmp(a, "--depth=")) {
 -                      max_depth = strtoul(a + 8, NULL, 0);
 -                      if (max_depth > MAX_DEPTH)
 -                              die("--depth cannot exceed %u", MAX_DEPTH);
 -              }
 -              else if (!prefixcmp(a, "--active-branches="))
 -                      max_active_branches = strtoul(a + 18, NULL, 0);
 -              else if (!prefixcmp(a, "--import-marks="))
 -                      import_marks(a + 15);
 -              else if (!prefixcmp(a, "--export-marks="))
 -                      mark_file = a + 15;
 -              else if (!prefixcmp(a, "--export-pack-edges=")) {
 -                      if (pack_edges)
 -                              fclose(pack_edges);
 -                      pack_edges = fopen(a + 20, "a");
 -                      if (!pack_edges)
 -                              die_errno("Cannot open '%s'", a + 20);
 -              } else if (!strcmp(a, "--force"))
 -                      force_update = 1;
 -              else if (!strcmp(a, "--quiet"))
 -                      show_stats = 0;
 -              else if (!strcmp(a, "--stats"))
 -                      show_stats = 1;
 -              else
 -                      die("unknown option %s", a);
 -      }
 -      if (i != argc)
 -              usage(fast_import_usage);
 +      global_argc = argc;
 +      global_argv = argv;
  
        rc_free = pool_alloc(cmd_save * sizeof(*rc_free));
        for (i = 0; i < (cmd_save - 1); i++)
                        parse_checkpoint();
                else if (!prefixcmp(command_buf.buf, "progress "))
                        parse_progress();
 +              else if (!prefixcmp(command_buf.buf, "feature "))
 +                      parse_feature();
 +              else if (!prefixcmp(command_buf.buf, "option git "))
 +                      parse_option();
 +              else if (!prefixcmp(command_buf.buf, "option "))
 +                      /* ignore non-git options*/;
                else
                        die("Unsupported command: %s", command_buf.buf);
        }
 +
 +      /* argv hasn't been parsed yet, do so */
 +      if (!seen_data_command)
 +              parse_argv();
 +
        end_packfile();
  
        dump_branches();
diff --combined t/t9300-fast-import.sh
index a1b8c2bb93374ba326c00f3f87a52dd550f90b2e,bf8c50902739c685f5ff799c494b638ff5b10791..60d6f5d1ba7d5421c4c364e070e35bdb024b3b4a
@@@ -1092,9 -1092,12 +1092,12 @@@ test_expect_success 'P: fail on blob ma
  ### series Q (notes)
  ###
  
- note1_data="Note for the first commit"
- note2_data="Note for the second commit"
- note3_data="Note for the third commit"
+ note1_data="The first note for the first commit"
+ note2_data="The first note for the second commit"
+ note3_data="The first note for the third commit"
+ note1b_data="The second note for the first commit"
+ note1c_data="The third note for the first commit"
+ note2b_data="The second note for the second commit"
  
  test_tick
  cat >input <<INPUT_END
@@@ -1169,7 -1172,45 +1172,45 @@@ data <<EO
  $note3_data
  EOF
  
+ commit refs/notes/foobar
+ mark :10
+ committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+ data <<COMMIT
+ notes (:10)
+ COMMIT
+ N inline :3
+ data <<EOF
+ $note1b_data
+ EOF
+ commit refs/notes/foobar2
+ mark :11
+ committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+ data <<COMMIT
+ notes (:11)
+ COMMIT
+ N inline :3
+ data <<EOF
+ $note1c_data
+ EOF
+ commit refs/notes/foobar
+ mark :12
+ committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+ data <<COMMIT
+ notes (:12)
+ COMMIT
+ deleteall
+ N inline :5
+ data <<EOF
+ $note2b_data
+ EOF
  INPUT_END
  test_expect_success \
        'Q: commit notes' \
        'git fast-import <input &&
@@@ -1224,8 -1265,8 +1265,8 @@@ committer $GIT_COMMITTER_NAME <$GIT_COM
  notes (:9)
  EOF
  test_expect_success \
-       'Q: verify notes commit' \
-       'git cat-file commit refs/notes/foobar | sed 1d >actual &&
+       'Q: verify first notes commit' \
+       'git cat-file commit refs/notes/foobar~2 | sed 1d >actual &&
        test_cmp expect actual'
  
  cat >expect.unsorted <<EOF
  EOF
  cat expect.unsorted | sort >expect
  test_expect_success \
-       'Q: verify notes tree' \
-       'git cat-file -p refs/notes/foobar^{tree} | sed "s/ [0-9a-f]*   / /" >actual &&
+       'Q: verify first notes tree' \
+       'git cat-file -p refs/notes/foobar~2^{tree} | sed "s/ [0-9a-f]* / /" >actual &&
         test_cmp expect actual'
  
  echo "$note1_data" >expect
  test_expect_success \
-       'Q: verify note for first commit' \
-       'git cat-file blob refs/notes/foobar:$commit1 >actual && test_cmp expect actual'
+       'Q: verify first note for first commit' \
+       'git cat-file blob refs/notes/foobar~2:$commit1 >actual && test_cmp expect actual'
  
  echo "$note2_data" >expect
  test_expect_success \
-       'Q: verify note for second commit' \
-       'git cat-file blob refs/notes/foobar:$commit2 >actual && test_cmp expect actual'
+       'Q: verify first note for second commit' \
+       'git cat-file blob refs/notes/foobar~2:$commit2 >actual && test_cmp expect actual'
+ echo "$note3_data" >expect
+ test_expect_success \
+       'Q: verify first note for third commit' \
+       'git cat-file blob refs/notes/foobar~2:$commit3 >actual && test_cmp expect actual'
+ cat >expect <<EOF
+ parent `git rev-parse --verify refs/notes/foobar~2`
+ author $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+ committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+ notes (:10)
+ EOF
+ test_expect_success \
+       'Q: verify second notes commit' \
+       'git cat-file commit refs/notes/foobar^ | sed 1d >actual &&
+       test_cmp expect actual'
+ cat >expect.unsorted <<EOF
+ 100644 blob $commit1
+ 100644 blob $commit2
+ 100644 blob $commit3
+ EOF
+ cat expect.unsorted | sort >expect
+ test_expect_success \
+       'Q: verify second notes tree' \
+       'git cat-file -p refs/notes/foobar^^{tree} | sed "s/ [0-9a-f]*  / /" >actual &&
+        test_cmp expect actual'
+ echo "$note1b_data" >expect
+ test_expect_success \
+       'Q: verify second note for first commit' \
+       'git cat-file blob refs/notes/foobar^:$commit1 >actual && test_cmp expect actual'
+ echo "$note2_data" >expect
+ test_expect_success \
+       'Q: verify first note for second commit' \
+       'git cat-file blob refs/notes/foobar^:$commit2 >actual && test_cmp expect actual'
  
  echo "$note3_data" >expect
  test_expect_success \
-       'Q: verify note for third commit' \
-       'git cat-file blob refs/notes/foobar:$commit3 >actual && test_cmp expect actual'
+       'Q: verify first note for third commit' \
+       'git cat-file blob refs/notes/foobar^:$commit3 >actual && test_cmp expect actual'
+ cat >expect <<EOF
+ author $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+ committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+ notes (:11)
+ EOF
+ test_expect_success \
+       'Q: verify third notes commit' \
+       'git cat-file commit refs/notes/foobar2 | sed 1d >actual &&
+       test_cmp expect actual'
+ cat >expect.unsorted <<EOF
+ 100644 blob $commit1
+ EOF
+ cat expect.unsorted | sort >expect
+ test_expect_success \
+       'Q: verify third notes tree' \
+       'git cat-file -p refs/notes/foobar2^{tree} | sed "s/ [0-9a-f]*  / /" >actual &&
+        test_cmp expect actual'
+ echo "$note1c_data" >expect
+ test_expect_success \
+       'Q: verify third note for first commit' \
+       'git cat-file blob refs/notes/foobar2:$commit1 >actual && test_cmp expect actual'
+ cat >expect <<EOF
+ parent `git rev-parse --verify refs/notes/foobar^`
+ author $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+ committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+ notes (:12)
+ EOF
+ test_expect_success \
+       'Q: verify fourth notes commit' \
+       'git cat-file commit refs/notes/foobar | sed 1d >actual &&
+       test_cmp expect actual'
+ cat >expect.unsorted <<EOF
+ 100644 blob $commit2
+ EOF
+ cat expect.unsorted | sort >expect
+ test_expect_success \
+       'Q: verify fourth notes tree' \
+       'git cat-file -p refs/notes/foobar^{tree} | sed "s/ [0-9a-f]*   / /" >actual &&
+        test_cmp expect actual'
+ echo "$note2b_data" >expect
+ test_expect_success \
+       'Q: verify second note for second commit' \
+       'git cat-file blob refs/notes/foobar:$commit2 >actual && test_cmp expect actual'
  
 +###
 +### series R (feature and option)
 +###
 +
 +cat >input <<EOF
 +feature no-such-feature-exists
 +EOF
 +
 +test_expect_success 'R: abort on unsupported feature' '
 +      test_must_fail git fast-import <input
 +'
 +
 +cat >input <<EOF
 +feature date-format=now
 +EOF
 +
 +test_expect_success 'R: supported feature is accepted' '
 +      git fast-import <input
 +'
 +
 +cat >input << EOF
 +blob
 +data 3
 +hi
 +feature date-format=now
 +EOF
 +
 +test_expect_success 'R: abort on receiving feature after data command' '
 +      test_must_fail git fast-import <input
 +'
 +
 +cat >input << EOF
 +feature import-marks=git.marks
 +feature import-marks=git2.marks
 +EOF
 +
 +test_expect_success 'R: only one import-marks feature allowed per stream' '
 +      test_must_fail git fast-import <input
 +'
 +
 +cat >input << EOF
 +feature export-marks=git.marks
 +blob
 +mark :1
 +data 3
 +hi
 +
 +EOF
 +
 +test_expect_success \
 +    'R: export-marks feature results in a marks file being created' \
 +    'cat input | git fast-import &&
 +    grep :1 git.marks'
 +
 +test_expect_success \
 +    'R: export-marks options can be overriden by commandline options' \
 +    'cat input | git fast-import --export-marks=other.marks &&
 +    grep :1 other.marks'
 +
 +cat >input << EOF
 +feature import-marks=marks.out
 +feature export-marks=marks.new
 +EOF
 +
 +test_expect_success \
 +    'R: import to output marks works without any content' \
 +    'cat input | git fast-import &&
 +    test_cmp marks.out marks.new'
 +
 +cat >input <<EOF
 +feature import-marks=nonexistant.marks
 +feature export-marks=marks.new
 +EOF
 +
 +test_expect_success \
 +    'R: import marks prefers commandline marks file over the stream' \
 +    'cat input | git fast-import --import-marks=marks.out &&
 +    test_cmp marks.out marks.new'
 +
 +
 +cat >input <<EOF
 +feature import-marks=nonexistant.marks
 +feature export-marks=combined.marks
 +EOF
 +
 +test_expect_success 'R: multiple --import-marks= should be honoured' '
 +    head -n2 marks.out > one.marks &&
 +    tail -n +3 marks.out > two.marks &&
 +    git fast-import --import-marks=one.marks --import-marks=two.marks <input &&
 +    test_cmp marks.out combined.marks
 +'
 +
 +cat >input <<EOF
 +feature relative-marks
 +feature import-marks=relative.in
 +feature export-marks=relative.out
 +EOF
 +
 +test_expect_success 'R: feature relative-marks should be honoured' '
 +    mkdir -p .git/info/fast-import/ &&
 +    cp marks.new .git/info/fast-import/relative.in &&
 +    git fast-import <input &&
 +    test_cmp marks.new .git/info/fast-import/relative.out
 +'
 +
 +cat >input <<EOF
 +feature relative-marks
 +feature import-marks=relative.in
 +feature no-relative-marks
 +feature export-marks=non-relative.out
 +EOF
 +
 +test_expect_success 'R: feature no-relative-marks should be honoured' '
 +    git fast-import <input &&
 +    test_cmp marks.new non-relative.out
 +'
 +
 +cat >input << EOF
 +option git quiet
 +blob
 +data 3
 +hi
 +
 +EOF
 +
 +touch empty
 +
 +test_expect_success 'R: quiet option results in no stats being output' '
 +    cat input | git fast-import 2> output &&
 +    test_cmp empty output
 +'
 +
 +cat >input <<EOF
 +option git non-existing-option
 +EOF
 +
 +test_expect_success 'R: die on unknown option' '
 +    test_must_fail git fast-import <input
 +'
 +
 +test_expect_success 'R: unknown commandline options are rejected' '\
 +    test_must_fail git fast-import --non-existing-option < /dev/null
 +'
 +
 +cat >input <<EOF
 +option non-existing-vcs non-existing-option
 +EOF
 +
 +test_expect_success 'R: ignore non-git options' '
 +    git fast-import <input
 +'
 +
  test_done