notes: implement helpers needed for note copying during rewrite
authorThomas Rast <trast@student.ethz.ch>
Fri, 12 Mar 2010 17:04:32 +0000 (18:04 +0100)
committerJunio C Hamano <gitster@pobox.com>
Sat, 13 Mar 2010 05:55:39 +0000 (21:55 -0800)
Implement helper functions to load the rewriting config, and to
actually copy the notes.  Also document the config.

Secondly, also implement an undocumented --for-rewrite=<cmd> option to
'git notes copy' which is used like --stdin, but also puts the
configuration for <cmd> into effect.  It will be needed to support the
copying in git-rebase.

Signed-off-by: Thomas Rast <trast@student.ethz.ch>
Acked-by: Johan Herland <johan@herland.net>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
Documentation/config.txt
Documentation/git-notes.txt
Documentation/githooks.txt
builtin-notes.c
builtin.h
cache.h
t/t3301-notes.sh
t/test-lib.sh

index 503942a2e4e929d999e1b483fc9862b1ef2e726d..2e02f1b075de1b47cc05e2dc5cbe68526ebc0719 100644 (file)
@@ -1305,6 +1305,36 @@ The effective value of "core.notesRef" (possibly overridden by
 GIT_NOTES_REF) is also implicitly added to the list of refs to be
 displayed.
 
+notes.rewrite.<command>::
+       When rewriting commits with <command> (currently `amend` or
+       `rebase`) and this variable is set to `true`, git
+       automatically copies your notes from the original to the
+       rewritten commit.  Defaults to `true`, but see
+       "notes.rewriteRef" below.
++
+This setting can be overridden with the `GIT_NOTES_REWRITE_REF`
+environment variable, which must be a colon separated list of refs or
+globs.
+
+notes.rewriteMode::
+       When copying notes during a rewrite (see the
+       "notes.rewrite.<command>" option), determines what to do if
+       the target commit already has a note.  Must be one of
+       `overwrite`, `concatenate`, or `ignore`.  Defaults to
+       `concatenate`.
++
+This setting can be overridden with the `GIT_NOTES_REWRITE_MODE`
+environment variable.
+
+notes.rewriteRef::
+       When copying notes during a rewrite, specifies the (fully
+       qualified) ref whose notes should be copied.  The ref may be a
+       glob, in which case notes in all matching refs will be copied.
+       You may also specify this configuration several times.
++
+Does not have a default value; you must configure this variable to
+enable note rewriting.
+
 pack.window::
        The size of the window used by linkgit:git-pack-objects[1] when no
        window size is given on the command line. Defaults to 10.
index 6ab3f982b9e1d946c8fdd8bb63bf9b7d327dbe62..b12d1cf5397081f5bd07beeec5e705e9a221255f 100644 (file)
@@ -35,6 +35,10 @@ This command always manipulates the notes specified in "core.notesRef"
 To change which notes are shown by 'git-log', see the
 "notes.displayRef" configuration.
 
+See the description of "notes.rewrite.<command>" in
+linkgit:git-config[1] for a way of carrying your notes across commands
+that rewrite commits.
+
 
 SUBCOMMANDS
 -----------
index a741769742a0b65cf940a14b3fd391c621ca5f7d..7183aa9abbc35018dc50a7c0e5254cc72115af23 100644 (file)
@@ -335,6 +335,10 @@ The 'extra-info' is again command-dependent.  If it is empty, the
 preceding SP is also omitted.  Currently, no commands pass any
 'extra-info'.
 
+The hook always runs after the automatic note copying (see
+"notes.rewrite.<command>" in linkgit:git-config.txt) has happened, and
+thus has access to these notes.
+
 The following command-specific comments apply:
 
 rebase::
index daeb14e1d908cf22a3a6c21c5e4fb763f487ca4f..026cfd32a823027b26811b007c98b2fcc9f2394a 100644 (file)
@@ -16,6 +16,7 @@
 #include "exec_cmd.h"
 #include "run-command.h"
 #include "parse-options.h"
+#include "string-list.h"
 
 static const char * const git_notes_usage[] = {
        "git notes [list [<object>]]",
@@ -278,14 +279,121 @@ int commit_notes(struct notes_tree *t, const char *msg)
        return 0;
 }
 
-int notes_copy_from_stdin(int force)
+
+combine_notes_fn *parse_combine_notes_fn(const char *v)
+{
+       if (!strcasecmp(v, "overwrite"))
+               return combine_notes_overwrite;
+       else if (!strcasecmp(v, "ignore"))
+               return combine_notes_ignore;
+       else if (!strcasecmp(v, "concatenate"))
+               return combine_notes_concatenate;
+       else
+               return NULL;
+}
+
+static int notes_rewrite_config(const char *k, const char *v, void *cb)
+{
+       struct notes_rewrite_cfg *c = cb;
+       if (!prefixcmp(k, "notes.rewrite.") && !strcmp(k+14, c->cmd)) {
+               c->enabled = git_config_bool(k, v);
+               return 0;
+       } else if (!c->mode_from_env && !strcmp(k, "notes.rewritemode")) {
+               if (!v)
+                       config_error_nonbool(k);
+               c->combine = parse_combine_notes_fn(v);
+               if (!c->combine) {
+                       error("Bad notes.rewriteMode value: '%s'", v);
+                       return 1;
+               }
+               return 0;
+       } else if (!c->refs_from_env && !strcmp(k, "notes.rewriteref")) {
+               /* note that a refs/ prefix is implied in the
+                * underlying for_each_glob_ref */
+               if (!prefixcmp(v, "refs/notes/"))
+                       string_list_add_refs_by_glob(c->refs, v);
+               else
+                       warning("Refusing to rewrite notes in %s"
+                               " (outside of refs/notes/)", v);
+               return 0;
+       }
+
+       return 0;
+}
+
+
+struct notes_rewrite_cfg *init_copy_notes_for_rewrite(const char *cmd)
+{
+       struct notes_rewrite_cfg *c = xmalloc(sizeof(struct notes_rewrite_cfg));
+       const char *rewrite_mode_env = getenv(GIT_NOTES_REWRITE_MODE_ENVIRONMENT);
+       const char *rewrite_refs_env = getenv(GIT_NOTES_REWRITE_REF_ENVIRONMENT);
+       c->cmd = cmd;
+       c->enabled = 1;
+       c->combine = combine_notes_concatenate;
+       c->refs = xcalloc(1, sizeof(struct string_list));
+       c->refs->strdup_strings = 1;
+       c->refs_from_env = 0;
+       c->mode_from_env = 0;
+       if (rewrite_mode_env) {
+               c->mode_from_env = 1;
+               c->combine = parse_combine_notes_fn(rewrite_mode_env);
+               if (!c->combine)
+                       error("Bad " GIT_NOTES_REWRITE_MODE_ENVIRONMENT
+                             " value: '%s'", rewrite_mode_env);
+       }
+       if (rewrite_refs_env) {
+               c->refs_from_env = 1;
+               string_list_add_refs_from_colon_sep(c->refs, rewrite_refs_env);
+       }
+       git_config(notes_rewrite_config, c);
+       if (!c->enabled || !c->refs->nr) {
+               string_list_clear(c->refs, 0);
+               free(c->refs);
+               free(c);
+               return NULL;
+       }
+       c->trees = load_notes_trees(c->refs);
+       string_list_clear(c->refs, 0);
+       free(c->refs);
+       return c;
+}
+
+int copy_note_for_rewrite(struct notes_rewrite_cfg *c,
+                         const unsigned char *from_obj, const unsigned char *to_obj)
+{
+       int ret = 0;
+       int i;
+       for (i = 0; c->trees[i]; i++)
+               ret = copy_note(c->trees[i], from_obj, to_obj, 1, c->combine) || ret;
+       return ret;
+}
+
+void finish_copy_notes_for_rewrite(struct notes_rewrite_cfg *c)
+{
+       int i;
+       for (i = 0; c->trees[i]; i++) {
+               commit_notes(c->trees[i], "Notes added by 'git notes copy'");
+               free_notes(c->trees[i]);
+       }
+       free(c->trees);
+       free(c);
+}
+
+int notes_copy_from_stdin(int force, const char *rewrite_cmd)
 {
        struct strbuf buf = STRBUF_INIT;
+       struct notes_rewrite_cfg *c = NULL;
        struct notes_tree *t;
        int ret = 0;
 
-       init_notes(NULL, NULL, NULL, 0);
-       t = &default_notes_tree;
+       if (rewrite_cmd) {
+               c = init_copy_notes_for_rewrite(rewrite_cmd);
+               if (!c)
+                       return 0;
+       } else {
+               init_notes(NULL, NULL, NULL, 0);
+               t = &default_notes_tree;
+       }
 
        while (strbuf_getline(&buf, stdin, '\n') != EOF) {
                unsigned char from_obj[20], to_obj[20];
@@ -302,7 +410,11 @@ int notes_copy_from_stdin(int force)
                if (get_sha1(split[1]->buf, to_obj))
                        die("Failed to resolve '%s' as a valid ref.", split[1]->buf);
 
-               err = copy_note(t, from_obj, to_obj, force, combine_notes_overwrite);
+               if (rewrite_cmd)
+                       err = copy_note_for_rewrite(c, from_obj, to_obj);
+               else
+                       err = copy_note(t, from_obj, to_obj, force,
+                                       combine_notes_overwrite);
 
                if (err) {
                        error("Failed to copy notes from '%s' to '%s'",
@@ -313,8 +425,12 @@ int notes_copy_from_stdin(int force)
                strbuf_list_free(split);
        }
 
-       commit_notes(t, "Notes added by 'git notes copy'");
-       free_notes(t);
+       if (!rewrite_cmd) {
+               commit_notes(t, "Notes added by 'git notes copy'");
+               free_notes(t);
+       } else {
+               finish_copy_notes_for_rewrite(c);
+       }
        return ret;
 }
 
@@ -330,6 +446,7 @@ int cmd_notes(int argc, const char **argv, const char *prefix)
            remove = 0, prune = 0, force = 0, from_stdin = 0;
        int given_object = 0, i = 1, retval = 0;
        struct msg_arg msg = { 0, 0, STRBUF_INIT };
+       const char *rewrite_cmd = NULL;
        struct option options[] = {
                OPT_GROUP("Notes options"),
                OPT_CALLBACK('m', "message", &msg, "MSG",
@@ -342,6 +459,8 @@ int cmd_notes(int argc, const char **argv, const char *prefix)
                           "reuse specified note object", parse_reuse_arg),
                OPT_BOOLEAN('f', "force", &force, "replace existing notes"),
                OPT_BOOLEAN(0, "stdin", &from_stdin, "read objects from stdin"),
+               OPT_STRING(0, "for-rewrite", &rewrite_cmd, "command",
+                          "load rewriting config for <command> (implies --stdin)"),
                OPT_END()
        };
 
@@ -390,6 +509,10 @@ int cmd_notes(int argc, const char **argv, const char *prefix)
                usage_with_options(git_notes_usage, options);
        }
 
+       if (!copy && rewrite_cmd) {
+               error("cannot use --for-rewrite with %s subcommand.", argv[0]);
+               usage_with_options(git_notes_usage, options);
+       }
        if (!copy && from_stdin) {
                error("cannot use --stdin with %s subcommand.", argv[0]);
                usage_with_options(git_notes_usage, options);
@@ -397,12 +520,12 @@ int cmd_notes(int argc, const char **argv, const char *prefix)
 
        if (copy) {
                const char *from_ref;
-               if (from_stdin) {
+               if (from_stdin || rewrite_cmd) {
                        if (argc > 1) {
                                error("too many parameters");
                                usage_with_options(git_notes_usage, options);
                        } else {
-                               return notes_copy_from_stdin(force);
+                               return notes_copy_from_stdin(force, rewrite_cmd);
                        }
                }
                if (argc < 3) {
index cdf98477a662a32790b55b70c23221141377633e..464588b299a473e9e1ee58cbc61e2f981030e37d 100644 (file)
--- a/builtin.h
+++ b/builtin.h
@@ -20,6 +20,23 @@ extern int commit_tree(const char *msg, unsigned char *tree,
                struct commit_list *parents, unsigned char *ret,
                const char *author);
 extern int commit_notes(struct notes_tree *t, const char *msg);
+
+struct notes_rewrite_cfg {
+       struct notes_tree **trees;
+       const char *cmd;
+       int enabled;
+       combine_notes_fn *combine;
+       struct string_list *refs;
+       int refs_from_env;
+       int mode_from_env;
+};
+
+combine_notes_fn *parse_combine_notes_fn(const char *v);
+struct notes_rewrite_cfg *init_copy_notes_for_rewrite(const char *cmd);
+int copy_note_for_rewrite(struct notes_rewrite_cfg *c,
+                         const unsigned char *from_obj, const unsigned char *to_obj);
+void finish_copy_notes_for_rewrite(struct notes_rewrite_cfg *c);
+
 extern int check_pager_config(const char *cmd);
 
 extern int cmd_add(int argc, const char **argv, const char *prefix);
diff --git a/cache.h b/cache.h
index fab53d66e4add0747c32086cfc91084869c19c0d..a04f37f14be608bb9b8edb5580a4473e20a9d582 100644 (file)
--- a/cache.h
+++ b/cache.h
@@ -386,6 +386,8 @@ static inline enum object_type object_type(unsigned int mode)
 #define GIT_NOTES_REF_ENVIRONMENT "GIT_NOTES_REF"
 #define GIT_NOTES_DEFAULT_REF "refs/notes/commits"
 #define GIT_NOTES_DISPLAY_REF_ENVIRONMENT "GIT_NOTES_DISPLAY_REF"
+#define GIT_NOTES_REWRITE_REF_ENVIRONMENT "GIT_NOTES_REWRITE_REF"
+#define GIT_NOTES_REWRITE_MODE_ENVIRONMENT "GIT_NOTES_REWRITE_MODE"
 
 extern int is_bare_repository_cfg;
 extern int is_bare_repository(void);
index 60ad6a1675cac0ba64bb8bf88ea40386f89ae3ff..aeec90a8e9b367ddefe5bfef4e188cc797ea0c28 100755 (executable)
@@ -810,4 +810,199 @@ test_expect_success 'git notes copy --stdin' '
        test "$(git notes list HEAD^)" = "$(git notes list HEAD~3)"
 '
 
+cat > expect << EOF
+commit 37a0d4cba38afef96ba54a3ea567e6dac575700b
+Author: A U Thor <author@example.com>
+Date:   Thu Apr 7 15:27:13 2005 -0700
+
+    15th
+
+commit be28d8b4d9951ad940d229ee3b0b9ee3b1ec273d
+Author: A U Thor <author@example.com>
+Date:   Thu Apr 7 15:26:13 2005 -0700
+
+    14th
+EOF
+
+test_expect_success 'git notes copy --for-rewrite (unconfigured)' '
+       test_commit 14th &&
+       test_commit 15th &&
+       (echo $(git rev-parse HEAD~3) $(git rev-parse HEAD^); \
+       echo $(git rev-parse HEAD~2) $(git rev-parse HEAD)) |
+       git notes copy --for-rewrite=foo &&
+       git log -2 > output &&
+       test_cmp expect output
+'
+
+cat > expect << EOF
+commit 37a0d4cba38afef96ba54a3ea567e6dac575700b
+Author: A U Thor <author@example.com>
+Date:   Thu Apr 7 15:27:13 2005 -0700
+
+    15th
+
+Notes (other):
+    yet another note
+$whitespace
+    yet another note
+
+commit be28d8b4d9951ad940d229ee3b0b9ee3b1ec273d
+Author: A U Thor <author@example.com>
+Date:   Thu Apr 7 15:26:13 2005 -0700
+
+    14th
+
+Notes (other):
+    other note
+$whitespace
+    yet another note
+EOF
+
+test_expect_success 'git notes copy --for-rewrite (enabled)' '
+       git config notes.rewriteMode overwrite &&
+       git config notes.rewriteRef "refs/notes/*" &&
+       (echo $(git rev-parse HEAD~3) $(git rev-parse HEAD^); \
+       echo $(git rev-parse HEAD~2) $(git rev-parse HEAD)) |
+       git notes copy --for-rewrite=foo &&
+       git log -2 > output &&
+       test_cmp expect output
+'
+
+test_expect_success 'git notes copy --for-rewrite (disabled)' '
+       git config notes.rewrite.bar false &&
+       echo $(git rev-parse HEAD~3) $(git rev-parse HEAD) |
+       git notes copy --for-rewrite=bar &&
+       git log -2 > output &&
+       test_cmp expect output
+'
+
+cat > expect << EOF
+commit 37a0d4cba38afef96ba54a3ea567e6dac575700b
+Author: A U Thor <author@example.com>
+Date:   Thu Apr 7 15:27:13 2005 -0700
+
+    15th
+
+Notes (other):
+    a fresh note
+EOF
+
+test_expect_success 'git notes copy --for-rewrite (overwrite)' '
+       git notes add -f -m"a fresh note" HEAD^ &&
+       echo $(git rev-parse HEAD^) $(git rev-parse HEAD) |
+       git notes copy --for-rewrite=foo &&
+       git log -1 > output &&
+       test_cmp expect output
+'
+
+test_expect_success 'git notes copy --for-rewrite (ignore)' '
+       git config notes.rewriteMode ignore &&
+       echo $(git rev-parse HEAD^) $(git rev-parse HEAD) |
+       git notes copy --for-rewrite=foo &&
+       git log -1 > output &&
+       test_cmp expect output
+'
+
+cat > expect << EOF
+commit 37a0d4cba38afef96ba54a3ea567e6dac575700b
+Author: A U Thor <author@example.com>
+Date:   Thu Apr 7 15:27:13 2005 -0700
+
+    15th
+
+Notes (other):
+    a fresh note
+    another fresh note
+EOF
+
+test_expect_success 'git notes copy --for-rewrite (append)' '
+       git notes add -f -m"another fresh note" HEAD^ &&
+       git config notes.rewriteMode concatenate &&
+       echo $(git rev-parse HEAD^) $(git rev-parse HEAD) |
+       git notes copy --for-rewrite=foo &&
+       git log -1 > output &&
+       test_cmp expect output
+'
+
+cat > expect << EOF
+commit 37a0d4cba38afef96ba54a3ea567e6dac575700b
+Author: A U Thor <author@example.com>
+Date:   Thu Apr 7 15:27:13 2005 -0700
+
+    15th
+
+Notes (other):
+    a fresh note
+    another fresh note
+    append 1
+    append 2
+EOF
+
+test_expect_success 'git notes copy --for-rewrite (append two to one)' '
+       git notes add -f -m"append 1" HEAD^ &&
+       git notes add -f -m"append 2" HEAD^^ &&
+       (echo $(git rev-parse HEAD^) $(git rev-parse HEAD);
+       echo $(git rev-parse HEAD^^) $(git rev-parse HEAD)) |
+       git notes copy --for-rewrite=foo &&
+       git log -1 > output &&
+       test_cmp expect output
+'
+
+test_expect_success 'git notes copy --for-rewrite (append empty)' '
+       git notes remove HEAD^ &&
+       echo $(git rev-parse HEAD^) $(git rev-parse HEAD) |
+       git notes copy --for-rewrite=foo &&
+       git log -1 > output &&
+       test_cmp expect output
+'
+
+cat > expect << EOF
+commit 37a0d4cba38afef96ba54a3ea567e6dac575700b
+Author: A U Thor <author@example.com>
+Date:   Thu Apr 7 15:27:13 2005 -0700
+
+    15th
+
+Notes (other):
+    replacement note 1
+EOF
+
+test_expect_success 'GIT_NOTES_REWRITE_MODE works' '
+       git notes add -f -m"replacement note 1" HEAD^ &&
+       echo $(git rev-parse HEAD^) $(git rev-parse HEAD) |
+       GIT_NOTES_REWRITE_MODE=overwrite git notes copy --for-rewrite=foo &&
+       git log -1 > output &&
+       test_cmp expect output
+'
+
+cat > expect << EOF
+commit 37a0d4cba38afef96ba54a3ea567e6dac575700b
+Author: A U Thor <author@example.com>
+Date:   Thu Apr 7 15:27:13 2005 -0700
+
+    15th
+
+Notes (other):
+    replacement note 2
+EOF
+
+test_expect_success 'GIT_NOTES_REWRITE_REF works' '
+       git config notes.rewriteMode overwrite &&
+       git notes add -f -m"replacement note 2" HEAD^ &&
+       git config --unset-all notes.rewriteRef &&
+       echo $(git rev-parse HEAD^) $(git rev-parse HEAD) |
+       GIT_NOTES_REWRITE_REF=refs/notes/commits:refs/notes/other \
+               git notes copy --for-rewrite=foo &&
+       git log -1 > output &&
+       test_cmp expect output
+'
+
+test_expect_success 'GIT_NOTES_REWRITE_REF overrides config' '
+       git config notes.rewriteRef refs/notes/other &&
+       git notes add -f -m"replacement note 3" HEAD^ &&
+       echo $(git rev-parse HEAD^) $(git rev-parse HEAD) |
+       GIT_NOTES_REWRITE_REF= git notes copy --for-rewrite=foo &&
+       git log -1 > output &&
+       test_cmp expect output
+'
 test_done
index 90115863bcd7682faefda9fab336e86bd8527163..806b83292f972ba816bbce76ff13d38e6f6e0850 100644 (file)
@@ -56,6 +56,8 @@ unset SHA1_FILE_DIRECTORIES
 unset SHA1_FILE_DIRECTORY
 unset GIT_NOTES_REF
 unset GIT_NOTES_DISPLAY_REF
+unset GIT_NOTES_REWRITE_REF
+unset GIT_NOTES_REWRITE_MODE
 GIT_MERGE_VERBOSITY=5
 export GIT_MERGE_VERBOSITY
 export GIT_AUTHOR_EMAIL GIT_AUTHOR_NAME