diff options
author | Junio C Hamano <gitster@pobox.com> | 2010-03-15 00:52:06 -0700 |
---|---|---|
committer | Junio C Hamano <gitster@pobox.com> | 2010-03-15 00:52:06 -0700 |
commit | 2949151fe9d92b2c3405f188e650d9bb7b46c663 (patch) | |
tree | 67aa2ee28ba316b9e8e05a4538814bbe51cd0550 | |
parent | Merge branch 'maint' (diff) | |
parent | Documentation: fix a few typos in git-notes.txt (diff) | |
download | tgif-2949151fe9d92b2c3405f188e650d9bb7b46c663.tar.xz |
Merge branch 'jh/notes'
* jh/notes: (33 commits)
Documentation: fix a few typos in git-notes.txt
notes: fix malformed tree entry
builtin-notes: Minor (mostly parse_options-related) fixes
builtin-notes: Add "copy" subcommand for copying notes between objects
builtin-notes: Misc. refactoring of argc and exit value handling
builtin-notes: Add -c/-C options for reusing notes
builtin-notes: Refactor handling of -F option to allow combining -m and -F
builtin-notes: Deprecate the -m/-F options for "git notes edit"
builtin-notes: Add "append" subcommand for appending to note objects
builtin-notes: Add "add" subcommand for adding notes to objects
builtin-notes: Add --message/--file aliases for -m/-F options
builtin-notes: Add "list" subcommand for listing note objects
Documentation: Generalize git-notes docs to 'objects' instead of 'commits'
builtin-notes: Add "prune" subcommand for removing notes for missing objects
Notes API: prune_notes(): Prune notes that belong to non-existing objects
t3305: Verify that removing notes triggers automatic fanout consolidation
builtin-notes: Add "remove" subcommand for removing existing notes
Teach builtin-notes to remove empty notes
Teach notes code to properly preserve non-notes in the notes tree
t3305: Verify that adding many notes with git-notes triggers increased fanout
...
Conflicts:
Makefile
-rw-r--r-- | Documentation/git-notes.txt | 91 | ||||
-rw-r--r-- | Makefile | 2 | ||||
-rw-r--r-- | builtin.h | 3 | ||||
-rw-r--r-- | builtin/notes.c | 455 | ||||
-rwxr-xr-x | contrib/examples/git-notes.sh (renamed from git-notes.sh) | 0 | ||||
-rw-r--r-- | git.c | 1 | ||||
-rw-r--r-- | notes.c | 843 | ||||
-rw-r--r-- | notes.h | 196 | ||||
-rw-r--r-- | pretty.c | 9 | ||||
-rwxr-xr-x | t/t3301-notes.sh | 510 | ||||
-rwxr-xr-x | t/t3303-notes-subtrees.sh | 28 | ||||
-rwxr-xr-x | t/t3304-notes-mixed.sh | 36 | ||||
-rwxr-xr-x | t/t3305-notes-fanout.sh | 95 | ||||
-rwxr-xr-x | t/t3306-notes-prune.sh | 94 |
14 files changed, 2167 insertions, 196 deletions
diff --git a/Documentation/git-notes.txt b/Documentation/git-notes.txt index d4487cab52..bef2f3942e 100644 --- a/Documentation/git-notes.txt +++ b/Documentation/git-notes.txt @@ -3,57 +3,112 @@ git-notes(1) NAME ---- -git-notes - Add/inspect commit notes +git-notes - Add/inspect object notes SYNOPSIS -------- [verse] -'git notes' (edit [-F <file> | -m <msg>] | show) [commit] +'git notes' [list [<object>]] +'git notes' add [-f] [-F <file> | -m <msg> | (-c | -C) <object>] [<object>] +'git notes' copy [-f] <from-object> <to-object> +'git notes' append [-F <file> | -m <msg> | (-c | -C) <object>] [<object>] +'git notes' edit [<object>] +'git notes' show [<object>] +'git notes' remove [<object>] +'git notes' prune + DESCRIPTION ----------- -This command allows you to add notes to commit messages, without -changing the commit. To discern these notes from the message stored -in the commit object, the notes are indented like the message, after -an unindented line saying "Notes:". +This command allows you to add/remove notes to/from objects, without +changing the objects themselves. + +A typical use of notes is to extend a commit message without having +to change the commit itself. Such commit notes can be shown by `git log` +along with the original commit message. To discern these notes from the +message stored in the commit object, the notes are indented like the +message, after an unindented line saying "Notes:". -To disable commit notes, you have to set the config variable -core.notesRef to the empty string. Alternatively, you can set it -to a different ref, something like "refs/notes/bugzilla". This setting -can be overridden by the environment variable "GIT_NOTES_REF". +To disable notes, you have to set the config variable core.notesRef to +the empty string. Alternatively, you can set it to a different ref, +something like "refs/notes/bugzilla". This setting can be overridden +by the environment variable "GIT_NOTES_REF". SUBCOMMANDS ----------- +list:: + List the notes object for a given object. If no object is + given, show a list of all note objects and the objects they + annotate (in the format "<note object> <annotated object>"). + This is the default subcommand if no subcommand is given. + +add:: + Add notes for a given object (defaults to HEAD). Abort if the + object already has notes (use `-f` to overwrite an + existing note). + +copy:: + Copy the notes for the first object onto the second object. + Abort if the second object already has notes, or if the first + object has none (use -f to overwrite existing notes to the + second object). This subcommand is equivalent to: + `git notes add [-f] -C $(git notes list <from-object>) <to-object>` + +append:: + Append to the notes of an existing object (defaults to HEAD). + Creates a new notes object if needed. + edit:: - Edit the notes for a given commit (defaults to HEAD). + Edit the notes for a given object (defaults to HEAD). show:: - Show the notes for a given commit (defaults to HEAD). + Show the notes for a given object (defaults to HEAD). + +remove:: + Remove the notes for a given object (defaults to HEAD). + This is equivalent to specifying an empty note message to + the `edit` subcommand. +prune:: + Remove all notes for non-existing/unreachable objects. OPTIONS ------- +-f:: +--force:: + When adding notes to an object that already has notes, + overwrite the existing notes (instead of aborting). + -m <msg>:: +--message=<msg>:: Use the given note message (instead of prompting). - If multiple `-m` (or `-F`) options are given, their - values are concatenated as separate paragraphs. + If multiple `-m` options are given, their values + are concatenated as separate paragraphs. -F <file>:: +--file=<file>:: Take the note message from the given file. Use '-' to read the note message from the standard input. - If multiple `-F` (or `-m`) options are given, their - values are concatenated as separate paragraphs. +-C <object>:: +--reuse-message=<object>:: + Reuse the note message from the given note object. + +-c <object>:: +--reedit-message=<object>:: + Like '-C', but with '-c' the editor is invoked, so that + the user can further edit the note message. Author ------ -Written by Johannes Schindelin <johannes.schindelin@gmx.de> +Written by Johannes Schindelin <johannes.schindelin@gmx.de> and +Johan Herland <johan@herland.net> Documentation ------------- -Documentation by Johannes Schindelin +Documentation by Johannes Schindelin and Johan Herland GIT --- @@ -335,7 +335,6 @@ SCRIPT_SH += git-merge-octopus.sh SCRIPT_SH += git-merge-one-file.sh SCRIPT_SH += git-merge-resolve.sh SCRIPT_SH += git-mergetool.sh -SCRIPT_SH += git-notes.sh SCRIPT_SH += git-pull.sh SCRIPT_SH += git-quiltimport.sh SCRIPT_SH += git-rebase--interactive.sh @@ -677,6 +676,7 @@ BUILTIN_OBJS += builtin/mktag.o BUILTIN_OBJS += builtin/mktree.o BUILTIN_OBJS += builtin/mv.o BUILTIN_OBJS += builtin/name-rev.o +BUILTIN_OBJS += builtin/notes.o BUILTIN_OBJS += builtin/pack-objects.o BUILTIN_OBJS += builtin/pack-redundant.o BUILTIN_OBJS += builtin/pack-refs.o @@ -5,6 +5,7 @@ #include "strbuf.h" #include "cache.h" #include "commit.h" +#include "notes.h" extern const char git_version_string[]; extern const char git_usage_string[]; @@ -18,6 +19,7 @@ extern int fmt_merge_msg(int merge_summary, struct strbuf *in, 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); extern int check_pager_config(const char *cmd); extern int cmd_add(int argc, const char **argv, const char *prefix); @@ -78,6 +80,7 @@ extern int cmd_mktag(int argc, const char **argv, const char *prefix); extern int cmd_mktree(int argc, const char **argv, const char *prefix); extern int cmd_mv(int argc, const char **argv, const char *prefix); extern int cmd_name_rev(int argc, const char **argv, const char *prefix); +extern int cmd_notes(int argc, const char **argv, const char *prefix); extern int cmd_pack_objects(int argc, const char **argv, const char *prefix); extern int cmd_pack_redundant(int argc, const char **argv, const char *prefix); extern int cmd_patch_id(int argc, const char **argv, const char *prefix); diff --git a/builtin/notes.c b/builtin/notes.c new file mode 100644 index 0000000000..feb710ac4a --- /dev/null +++ b/builtin/notes.c @@ -0,0 +1,455 @@ +/* + * Builtin "git notes" + * + * Copyright (c) 2010 Johan Herland <johan@herland.net> + * + * Based on git-notes.sh by Johannes Schindelin, + * and builtin-tag.c by Kristian Høgsberg and Carlos Rica. + */ + +#include "cache.h" +#include "builtin.h" +#include "notes.h" +#include "blob.h" +#include "commit.h" +#include "refs.h" +#include "exec_cmd.h" +#include "run-command.h" +#include "parse-options.h" + +static const char * const git_notes_usage[] = { + "git notes [list [<object>]]", + "git notes add [-f] [-m <msg> | -F <file> | (-c | -C) <object>] [<object>]", + "git notes copy [-f] <from-object> <to-object>", + "git notes append [-m <msg> | -F <file> | (-c | -C) <object>] [<object>]", + "git notes edit [<object>]", + "git notes show [<object>]", + "git notes remove [<object>]", + "git notes prune", + NULL +}; + +static const char note_template[] = + "\n" + "#\n" + "# Write/edit the notes for the following object:\n" + "#\n"; + +struct msg_arg { + int given; + int use_editor; + struct strbuf buf; +}; + +static int list_each_note(const unsigned char *object_sha1, + const unsigned char *note_sha1, char *note_path, + void *cb_data) +{ + printf("%s %s\n", sha1_to_hex(note_sha1), sha1_to_hex(object_sha1)); + return 0; +} + +static void write_note_data(int fd, const unsigned char *sha1) +{ + unsigned long size; + enum object_type type; + char *buf = read_sha1_file(sha1, &type, &size); + if (buf) { + if (size) + write_or_die(fd, buf, size); + free(buf); + } +} + +static void write_commented_object(int fd, const unsigned char *object) +{ + const char *show_args[5] = + {"show", "--stat", "--no-notes", sha1_to_hex(object), NULL}; + struct child_process show; + struct strbuf buf = STRBUF_INIT; + FILE *show_out; + + /* Invoke "git show --stat --no-notes $object" */ + memset(&show, 0, sizeof(show)); + show.argv = show_args; + show.no_stdin = 1; + show.out = -1; + show.err = 0; + show.git_cmd = 1; + if (start_command(&show)) + die("unable to start 'show' for object '%s'", + sha1_to_hex(object)); + + /* Open the output as FILE* so strbuf_getline() can be used. */ + show_out = xfdopen(show.out, "r"); + if (show_out == NULL) + die_errno("can't fdopen 'show' output fd"); + + /* Prepend "# " to each output line and write result to 'fd' */ + while (strbuf_getline(&buf, show_out, '\n') != EOF) { + write_or_die(fd, "# ", 2); + write_or_die(fd, buf.buf, buf.len); + write_or_die(fd, "\n", 1); + } + strbuf_release(&buf); + if (fclose(show_out)) + die_errno("failed to close pipe to 'show' for object '%s'", + sha1_to_hex(object)); + if (finish_command(&show)) + die("failed to finish 'show' for object '%s'", + sha1_to_hex(object)); +} + +static void create_note(const unsigned char *object, struct msg_arg *msg, + int append_only, const unsigned char *prev, + unsigned char *result) +{ + char *path = NULL; + + if (msg->use_editor || !msg->given) { + int fd; + + /* write the template message before editing: */ + path = git_pathdup("NOTES_EDITMSG"); + fd = open(path, O_CREAT | O_TRUNC | O_WRONLY, 0600); + if (fd < 0) + die_errno("could not create file '%s'", path); + + if (msg->given) + write_or_die(fd, msg->buf.buf, msg->buf.len); + else if (prev && !append_only) + write_note_data(fd, prev); + write_or_die(fd, note_template, strlen(note_template)); + + write_commented_object(fd, object); + + close(fd); + strbuf_reset(&(msg->buf)); + + if (launch_editor(path, &(msg->buf), NULL)) { + die("Please supply the note contents using either -m" \ + " or -F option"); + } + stripspace(&(msg->buf), 1); + } + + if (prev && append_only) { + /* Append buf to previous note contents */ + unsigned long size; + enum object_type type; + char *prev_buf = read_sha1_file(prev, &type, &size); + + strbuf_grow(&(msg->buf), size + 1); + if (msg->buf.len && prev_buf && size) + strbuf_insert(&(msg->buf), 0, "\n", 1); + if (prev_buf && size) + strbuf_insert(&(msg->buf), 0, prev_buf, size); + free(prev_buf); + } + + if (!msg->buf.len) { + fprintf(stderr, "Removing note for object %s\n", + sha1_to_hex(object)); + hashclr(result); + } else { + if (write_sha1_file(msg->buf.buf, msg->buf.len, blob_type, result)) { + error("unable to write note object"); + if (path) + error("The note contents has been left in %s", + path); + exit(128); + } + } + + if (path) { + unlink_or_warn(path); + free(path); + } +} + +static int parse_msg_arg(const struct option *opt, const char *arg, int unset) +{ + struct msg_arg *msg = opt->value; + + strbuf_grow(&(msg->buf), strlen(arg) + 2); + if (msg->buf.len) + strbuf_addch(&(msg->buf), '\n'); + strbuf_addstr(&(msg->buf), arg); + stripspace(&(msg->buf), 0); + + msg->given = 1; + return 0; +} + +static int parse_file_arg(const struct option *opt, const char *arg, int unset) +{ + struct msg_arg *msg = opt->value; + + if (msg->buf.len) + strbuf_addch(&(msg->buf), '\n'); + if (!strcmp(arg, "-")) { + if (strbuf_read(&(msg->buf), 0, 1024) < 0) + die_errno("cannot read '%s'", arg); + } else if (strbuf_read_file(&(msg->buf), arg, 1024) < 0) + die_errno("could not open or read '%s'", arg); + stripspace(&(msg->buf), 0); + + msg->given = 1; + return 0; +} + +static int parse_reuse_arg(const struct option *opt, const char *arg, int unset) +{ + struct msg_arg *msg = opt->value; + char *buf; + unsigned char object[20]; + enum object_type type; + unsigned long len; + + if (msg->buf.len) + strbuf_addch(&(msg->buf), '\n'); + + if (get_sha1(arg, object)) + die("Failed to resolve '%s' as a valid ref.", arg); + if (!(buf = read_sha1_file(object, &type, &len)) || !len) { + free(buf); + die("Failed to read object '%s'.", arg);; + } + strbuf_add(&(msg->buf), buf, len); + free(buf); + + msg->given = 1; + return 0; +} + +static int parse_reedit_arg(const struct option *opt, const char *arg, int unset) +{ + struct msg_arg *msg = opt->value; + msg->use_editor = 1; + return parse_reuse_arg(opt, arg, unset); +} + +int commit_notes(struct notes_tree *t, const char *msg) +{ + struct commit_list *parent; + unsigned char tree_sha1[20], prev_commit[20], new_commit[20]; + struct strbuf buf = STRBUF_INIT; + + if (!t) + t = &default_notes_tree; + if (!t->initialized || !t->ref || !*t->ref) + die("Cannot commit uninitialized/unreferenced notes tree"); + + /* Prepare commit message and reflog message */ + strbuf_addstr(&buf, "notes: "); /* commit message starts at index 7 */ + strbuf_addstr(&buf, msg); + if (buf.buf[buf.len - 1] != '\n') + strbuf_addch(&buf, '\n'); /* Make sure msg ends with newline */ + + /* Convert notes tree to tree object */ + if (write_notes_tree(t, tree_sha1)) + die("Failed to write current notes tree to database"); + + /* Create new commit for the tree object */ + if (!read_ref(t->ref, prev_commit)) { /* retrieve parent commit */ + parent = xmalloc(sizeof(*parent)); + parent->item = lookup_commit(prev_commit); + parent->next = NULL; + } else { + hashclr(prev_commit); + parent = NULL; + } + if (commit_tree(buf.buf + 7, tree_sha1, parent, new_commit, NULL)) + die("Failed to commit notes tree to database"); + + /* Update notes ref with new commit */ + update_ref(buf.buf, t->ref, new_commit, prev_commit, 0, DIE_ON_ERR); + + strbuf_release(&buf); + return 0; +} + +int cmd_notes(int argc, const char **argv, const char *prefix) +{ + struct notes_tree *t; + unsigned char object[20], from_obj[20], new_note[20]; + const unsigned char *note; + const char *object_ref; + char logmsg[100]; + + int list = 0, add = 0, copy = 0, append = 0, edit = 0, show = 0, + remove = 0, prune = 0, force = 0; + int given_object = 0, i = 1, retval = 0; + struct msg_arg msg = { 0, 0, STRBUF_INIT }; + struct option options[] = { + OPT_GROUP("Notes contents options"), + { OPTION_CALLBACK, 'm', "message", &msg, "MSG", + "note contents as a string", PARSE_OPT_NONEG, + parse_msg_arg}, + { OPTION_CALLBACK, 'F', "file", &msg, "FILE", + "note contents in a file", PARSE_OPT_NONEG, + parse_file_arg}, + { OPTION_CALLBACK, 'c', "reedit-message", &msg, "OBJECT", + "reuse and edit specified note object", PARSE_OPT_NONEG, + parse_reedit_arg}, + { OPTION_CALLBACK, 'C', "reuse-message", &msg, "OBJECT", + "reuse specified note object", PARSE_OPT_NONEG, + parse_reuse_arg}, + OPT_GROUP("Other options"), + OPT_BOOLEAN('f', "force", &force, "replace existing notes"), + OPT_END() + }; + + git_config(git_default_config, NULL); + + argc = parse_options(argc, argv, prefix, options, git_notes_usage, 0); + + if (argc && !strcmp(argv[0], "list")) + list = 1; + else if (argc && !strcmp(argv[0], "add")) + add = 1; + else if (argc && !strcmp(argv[0], "copy")) + copy = 1; + else if (argc && !strcmp(argv[0], "append")) + append = 1; + else if (argc && !strcmp(argv[0], "edit")) + edit = 1; + else if (argc && !strcmp(argv[0], "show")) + show = 1; + else if (argc && !strcmp(argv[0], "remove")) + remove = 1; + else if (argc && !strcmp(argv[0], "prune")) + prune = 1; + else if (!argc) { + list = 1; /* Default to 'list' if no other subcommand given */ + i = 0; + } + + if (list + add + copy + append + edit + show + remove + prune != 1) + usage_with_options(git_notes_usage, options); + + if (msg.given && !(add || append || edit)) { + error("cannot use -m/-F/-c/-C options with %s subcommand.", + argv[0]); + usage_with_options(git_notes_usage, options); + } + + if (msg.given && edit) { + fprintf(stderr, "The -m/-F/-c/-C options have been deprecated " + "for the 'edit' subcommand.\n" + "Please use 'git notes add -f -m/-F/-c/-C' instead.\n"); + } + + if (force && !(add || copy)) { + error("cannot use -f option with %s subcommand.", argv[0]); + usage_with_options(git_notes_usage, options); + } + + if (copy) { + const char *from_ref; + if (argc < 3) { + error("too few parameters"); + usage_with_options(git_notes_usage, options); + } + from_ref = argv[i++]; + if (get_sha1(from_ref, from_obj)) + die("Failed to resolve '%s' as a valid ref.", from_ref); + } + + given_object = argc > i; + object_ref = given_object ? argv[i++] : "HEAD"; + + if (argc > i || (prune && given_object)) { + error("too many parameters"); + usage_with_options(git_notes_usage, options); + } + + if (get_sha1(object_ref, object)) + die("Failed to resolve '%s' as a valid ref.", object_ref); + + init_notes(NULL, NULL, NULL, 0); + t = &default_notes_tree; + + if (prefixcmp(t->ref, "refs/notes/")) + die("Refusing to %s notes in %s (outside of refs/notes/)", + argv[0], t->ref); + + note = get_note(t, object); + + /* list command */ + + if (list) { + if (given_object) { + if (note) { + puts(sha1_to_hex(note)); + goto end; + } + } else { + retval = for_each_note(t, 0, list_each_note, NULL); + goto end; + } + } + + /* show command */ + + if ((list || show) && !note) { + error("No note found for object %s.", sha1_to_hex(object)); + retval = 1; + goto end; + } else if (show) { + const char *show_args[3] = {"show", sha1_to_hex(note), NULL}; + retval = execv_git_cmd(show_args); + goto end; + } + + /* add/append/edit/remove/prune command */ + + if ((add || copy) && note) { + if (!force) { + error("Cannot %s notes. Found existing notes for object" + " %s. Use '-f' to overwrite existing notes", + argv[0], sha1_to_hex(object)); + retval = 1; + goto end; + } + fprintf(stderr, "Overwriting existing notes for object %s\n", + sha1_to_hex(object)); + } + + if (remove) { + msg.given = 1; + msg.use_editor = 0; + strbuf_reset(&(msg.buf)); + } + + if (prune) { + hashclr(new_note); + prune_notes(t); + goto commit; + } else if (copy) { + const unsigned char *from_note = get_note(t, from_obj); + if (!from_note) { + error("Missing notes on source object %s. Cannot copy.", + sha1_to_hex(from_obj)); + retval = 1; + goto end; + } + hashcpy(new_note, from_note); + } else + create_note(object, &msg, append, note, new_note); + + if (is_null_sha1(new_note)) + remove_note(t, object); + else + add_note(t, object, new_note, combine_notes_overwrite); + +commit: + snprintf(logmsg, sizeof(logmsg), "Notes %s by 'git notes %s'", + is_null_sha1(new_note) ? "removed" : "added", argv[0]); + commit_notes(t, logmsg); + +end: + free_notes(t); + strbuf_release(&(msg.buf)); + return retval; +} diff --git a/git-notes.sh b/contrib/examples/git-notes.sh index e642e47d9f..e642e47d9f 100755 --- a/git-notes.sh +++ b/contrib/examples/git-notes.sh @@ -346,6 +346,7 @@ static void handle_internal_command(int argc, const char **argv) { "mktree", cmd_mktree, RUN_SETUP }, { "mv", cmd_mv, RUN_SETUP | NEED_WORK_TREE }, { "name-rev", cmd_name_rev, RUN_SETUP }, + { "notes", cmd_notes, RUN_SETUP }, { "pack-objects", cmd_pack_objects, RUN_SETUP }, { "pack-redundant", cmd_pack_redundant, RUN_SETUP }, { "patch-id", cmd_patch_id }, @@ -1,7 +1,7 @@ #include "cache.h" -#include "commit.h" #include "notes.h" -#include "refs.h" +#include "blob.h" +#include "tree.h" #include "utf8.h" #include "strbuf.h" #include "tree-walk.h" @@ -25,10 +25,10 @@ struct int_node { /* * Leaf nodes come in two variants, note entries and subtree entries, * distinguished by the LSb of the leaf node pointer (see above). - * As a note entry, the key is the SHA1 of the referenced commit, and the + * As a note entry, the key is the SHA1 of the referenced object, and the * value is the SHA1 of the note object. * As a subtree entry, the key is the prefix SHA1 (w/trailing NULs) of the - * referenced commit, using the last byte of the key to store the length of + * referenced object, using the last byte of the key to store the length of * the prefix. The value is the SHA1 of the tree object containing the notes * subtree. */ @@ -37,6 +37,21 @@ struct leaf_node { unsigned char val_sha1[20]; }; +/* + * A notes tree may contain entries that are not notes, and that do not follow + * the naming conventions of notes. There are typically none/few of these, but + * we still need to keep track of them. Keep a simple linked list sorted alpha- + * betically on the non-note path. The list is populated when parsing tree + * objects in load_subtree(), and the non-notes are correctly written back into + * the tree objects produced by write_notes_tree(). + */ +struct non_note { + struct non_note *next; /* grounded (last->next == NULL) */ + char *path; + unsigned int mode; + unsigned char sha1[20]; +}; + #define PTR_TYPE_NULL 0 #define PTR_TYPE_INTERNAL 1 #define PTR_TYPE_NOTE 2 @@ -46,17 +61,15 @@ struct leaf_node { #define CLR_PTR_TYPE(ptr) ((void *) ((uintptr_t) (ptr) & ~3)) #define SET_PTR_TYPE(ptr, type) ((void *) ((uintptr_t) (ptr) | (type))) -#define GET_NIBBLE(n, sha1) (((sha1[n >> 1]) >> ((~n & 0x01) << 2)) & 0x0f) +#define GET_NIBBLE(n, sha1) (((sha1[(n) >> 1]) >> ((~(n) & 0x01) << 2)) & 0x0f) #define SUBTREE_SHA1_PREFIXCMP(key_sha1, subtree_sha1) \ (memcmp(key_sha1, subtree_sha1, subtree_sha1[19])) -static struct int_node root_node; +struct notes_tree default_notes_tree; -static int initialized; - -static void load_subtree(struct leaf_node *subtree, struct int_node *node, - unsigned int n); +static void load_subtree(struct notes_tree *t, struct leaf_node *subtree, + struct int_node *node, unsigned int n); /* * Search the tree until the appropriate location for the given key is found: @@ -73,7 +86,7 @@ static void load_subtree(struct leaf_node *subtree, struct int_node *node, * - an unused leaf node (NULL) * In any case, set *tree and *n, and return pointer to the tree location. */ -static void **note_tree_search(struct int_node **tree, +static void **note_tree_search(struct notes_tree *t, struct int_node **tree, unsigned char *n, const unsigned char *key_sha1) { struct leaf_node *l; @@ -85,27 +98,27 @@ static void **note_tree_search(struct int_node **tree, if (!SUBTREE_SHA1_PREFIXCMP(key_sha1, l->key_sha1)) { /* unpack tree and resume search */ (*tree)->a[0] = NULL; - load_subtree(l, *tree, *n); + load_subtree(t, l, *tree, *n); free(l); - return note_tree_search(tree, n, key_sha1); + return note_tree_search(t, tree, n, key_sha1); } } i = GET_NIBBLE(*n, key_sha1); p = (*tree)->a[i]; - switch(GET_PTR_TYPE(p)) { + switch (GET_PTR_TYPE(p)) { case PTR_TYPE_INTERNAL: *tree = CLR_PTR_TYPE(p); (*n)++; - return note_tree_search(tree, n, key_sha1); + return note_tree_search(t, tree, n, key_sha1); case PTR_TYPE_SUBTREE: l = (struct leaf_node *) CLR_PTR_TYPE(p); if (!SUBTREE_SHA1_PREFIXCMP(key_sha1, l->key_sha1)) { /* unpack tree and resume search */ (*tree)->a[i] = NULL; - load_subtree(l, *tree, *n); + load_subtree(t, l, *tree, *n); free(l); - return note_tree_search(tree, n, key_sha1); + return note_tree_search(t, tree, n, key_sha1); } /* fall through */ default: @@ -118,10 +131,11 @@ static void **note_tree_search(struct int_node **tree, * Search to the tree location appropriate for the given key: * If a note entry with matching key, return the note entry, else return NULL. */ -static struct leaf_node *note_tree_find(struct int_node *tree, unsigned char n, +static struct leaf_node *note_tree_find(struct notes_tree *t, + struct int_node *tree, unsigned char n, const unsigned char *key_sha1) { - void **p = note_tree_search(&tree, &n, key_sha1); + void **p = note_tree_search(t, &tree, &n, key_sha1); if (GET_PTR_TYPE(*p) == PTR_TYPE_NOTE) { struct leaf_node *l = (struct leaf_node *) CLR_PTR_TYPE(*p); if (!hashcmp(key_sha1, l->key_sha1)) @@ -130,55 +144,12 @@ static struct leaf_node *note_tree_find(struct int_node *tree, unsigned char n, return NULL; } -/* Create a new blob object by concatenating the two given blob objects */ -static int concatenate_notes(unsigned char *cur_sha1, - const unsigned char *new_sha1) -{ - char *cur_msg, *new_msg, *buf; - unsigned long cur_len, new_len, buf_len; - enum object_type cur_type, new_type; - int ret; - - /* read in both note blob objects */ - new_msg = read_sha1_file(new_sha1, &new_type, &new_len); - if (!new_msg || !new_len || new_type != OBJ_BLOB) { - free(new_msg); - return 0; - } - cur_msg = read_sha1_file(cur_sha1, &cur_type, &cur_len); - if (!cur_msg || !cur_len || cur_type != OBJ_BLOB) { - free(cur_msg); - free(new_msg); - hashcpy(cur_sha1, new_sha1); - return 0; - } - - /* we will separate the notes by a newline anyway */ - if (cur_msg[cur_len - 1] == '\n') - cur_len--; - - /* concatenate cur_msg and new_msg into buf */ - buf_len = cur_len + 1 + new_len; - buf = (char *) xmalloc(buf_len); - memcpy(buf, cur_msg, cur_len); - buf[cur_len] = '\n'; - memcpy(buf + cur_len + 1, new_msg, new_len); - - free(cur_msg); - free(new_msg); - - /* create a new blob object from buf */ - ret = write_sha1_file(buf, buf_len, "blob", cur_sha1); - free(buf); - return ret; -} - /* * To insert a leaf_node: * Search to the tree location appropriate for the given leaf_node's key: * - If location is unused (NULL), store the tweaked pointer directly there * - If location holds a note entry that matches the note-to-be-inserted, then - * concatenate the two notes. + * combine the two notes (by calling the given combine_notes function). * - If location holds a note entry that matches the subtree-to-be-inserted, * then unpack the subtree-to-be-inserted into the location. * - If location holds a matching subtree entry, unpack the subtree at that @@ -186,16 +157,17 @@ static int concatenate_notes(unsigned char *cur_sha1, * - Else, create a new int_node, holding both the node-at-location and the * node-to-be-inserted, and store the new int_node into the location. */ -static void note_tree_insert(struct int_node *tree, unsigned char n, - struct leaf_node *entry, unsigned char type) +static void note_tree_insert(struct notes_tree *t, struct int_node *tree, + unsigned char n, struct leaf_node *entry, unsigned char type, + combine_notes_fn combine_notes) { struct int_node *new_node; struct leaf_node *l; - void **p = note_tree_search(&tree, &n, entry->key_sha1); + void **p = note_tree_search(t, &tree, &n, entry->key_sha1); assert(GET_PTR_TYPE(entry) == 0); /* no type bits set */ l = (struct leaf_node *) CLR_PTR_TYPE(*p); - switch(GET_PTR_TYPE(*p)) { + switch (GET_PTR_TYPE(*p)) { case PTR_TYPE_NULL: assert(!*p); *p = SET_PTR_TYPE(entry, type); @@ -208,12 +180,11 @@ static void note_tree_insert(struct int_node *tree, unsigned char n, if (!hashcmp(l->val_sha1, entry->val_sha1)) return; - if (concatenate_notes(l->val_sha1, - entry->val_sha1)) - die("failed to concatenate note %s " - "into note %s for commit %s", - sha1_to_hex(entry->val_sha1), + if (combine_notes(l->val_sha1, entry->val_sha1)) + die("failed to combine notes %s and %s" + " for object %s", sha1_to_hex(l->val_sha1), + sha1_to_hex(entry->val_sha1), sha1_to_hex(l->key_sha1)); free(entry); return; @@ -223,7 +194,7 @@ static void note_tree_insert(struct int_node *tree, unsigned char n, if (!SUBTREE_SHA1_PREFIXCMP(l->key_sha1, entry->key_sha1)) { /* unpack 'entry' */ - load_subtree(entry, tree, n); + load_subtree(t, entry, tree, n); free(entry); return; } @@ -234,9 +205,10 @@ static void note_tree_insert(struct int_node *tree, unsigned char n, if (!SUBTREE_SHA1_PREFIXCMP(entry->key_sha1, l->key_sha1)) { /* unpack 'l' and restart insert */ *p = NULL; - load_subtree(l, tree, n); + load_subtree(t, l, tree, n); free(l); - note_tree_insert(tree, n, entry, type); + note_tree_insert(t, tree, n, entry, type, + combine_notes); return; } break; @@ -246,9 +218,83 @@ static void note_tree_insert(struct int_node *tree, unsigned char n, assert(GET_PTR_TYPE(*p) == PTR_TYPE_NOTE || GET_PTR_TYPE(*p) == PTR_TYPE_SUBTREE); new_node = (struct int_node *) xcalloc(sizeof(struct int_node), 1); - note_tree_insert(new_node, n + 1, l, GET_PTR_TYPE(*p)); + note_tree_insert(t, new_node, n + 1, l, GET_PTR_TYPE(*p), + combine_notes); *p = SET_PTR_TYPE(new_node, PTR_TYPE_INTERNAL); - note_tree_insert(new_node, n + 1, entry, type); + note_tree_insert(t, new_node, n + 1, entry, type, combine_notes); +} + +/* + * How to consolidate an int_node: + * If there are > 1 non-NULL entries, give up and return non-zero. + * Otherwise replace the int_node at the given index in the given parent node + * with the only entry (or a NULL entry if no entries) from the given tree, + * and return 0. + */ +static int note_tree_consolidate(struct int_node *tree, + struct int_node *parent, unsigned char index) +{ + unsigned int i; + void *p = NULL; + + assert(tree && parent); + assert(CLR_PTR_TYPE(parent->a[index]) == tree); + + for (i = 0; i < 16; i++) { + if (GET_PTR_TYPE(tree->a[i]) != PTR_TYPE_NULL) { + if (p) /* more than one entry */ + return -2; + p = tree->a[i]; + } + } + + /* replace tree with p in parent[index] */ + parent->a[index] = p; + free(tree); + return 0; +} + +/* + * To remove a leaf_node: + * Search to the tree location appropriate for the given leaf_node's key: + * - If location does not hold a matching entry, abort and do nothing. + * - Replace the matching leaf_node with a NULL entry (and free the leaf_node). + * - Consolidate int_nodes repeatedly, while walking up the tree towards root. + */ +static void note_tree_remove(struct notes_tree *t, struct int_node *tree, + unsigned char n, struct leaf_node *entry) +{ + struct leaf_node *l; + struct int_node *parent_stack[20]; + unsigned char i, j; + void **p = note_tree_search(t, &tree, &n, entry->key_sha1); + + assert(GET_PTR_TYPE(entry) == 0); /* no type bits set */ + if (GET_PTR_TYPE(*p) != PTR_TYPE_NOTE) + return; /* type mismatch, nothing to remove */ + l = (struct leaf_node *) CLR_PTR_TYPE(*p); + if (hashcmp(l->key_sha1, entry->key_sha1)) + return; /* key mismatch, nothing to remove */ + + /* we have found a matching entry */ + free(l); + *p = SET_PTR_TYPE(NULL, PTR_TYPE_NULL); + + /* consolidate this tree level, and parent levels, if possible */ + if (!n) + return; /* cannot consolidate top level */ + /* first, build stack of ancestors between root and current node */ + parent_stack[0] = t->root; + for (i = 0; i < n; i++) { + j = GET_NIBBLE(i, entry->key_sha1); + parent_stack[i + 1] = CLR_PTR_TYPE(parent_stack[i]->a[j]); + } + assert(i == n && parent_stack[i] == tree); + /* next, unwind stack until note_tree_consolidate() is done */ + while (i > 0 && + !note_tree_consolidate(parent_stack[i], parent_stack[i - 1], + GET_NIBBLE(i - 1, entry->key_sha1))) + i--; } /* Free the entire notes data contained in the given tree */ @@ -257,7 +303,7 @@ static void note_tree_free(struct int_node *tree) unsigned int i; for (i = 0; i < 16; i++) { void *p = tree->a[i]; - switch(GET_PTR_TYPE(p)) { + switch (GET_PTR_TYPE(p)) { case PTR_TYPE_INTERNAL: note_tree_free(CLR_PTR_TYPE(p)); /* fall through */ @@ -274,7 +320,7 @@ static void note_tree_free(struct int_node *tree) * - hex_len - Length of above segment. Must be multiple of 2 between 0 and 40 * - sha1 - Partial SHA1 value is written here * - sha1_len - Max #bytes to store in sha1, Must be >= hex_len / 2, and < 20 - * Returns -1 on error (invalid arguments or invalid SHA1 (not in hex format). + * Returns -1 on error (invalid arguments or invalid SHA1 (not in hex format)). * Otherwise, returns number of bytes written to sha1 (i.e. hex_len / 2). * Pads sha1 with NULs up to sha1_len (not included in returned length). */ @@ -296,14 +342,67 @@ static int get_sha1_hex_segment(const char *hex, unsigned int hex_len, return len; } -static void load_subtree(struct leaf_node *subtree, struct int_node *node, - unsigned int n) +static int non_note_cmp(const struct non_note *a, const struct non_note *b) +{ + return strcmp(a->path, b->path); +} + +static void add_non_note(struct notes_tree *t, const char *path, + unsigned int mode, const unsigned char *sha1) +{ + struct non_note *p = t->prev_non_note, *n; + n = (struct non_note *) xmalloc(sizeof(struct non_note)); + n->next = NULL; + n->path = xstrdup(path); + n->mode = mode; + hashcpy(n->sha1, sha1); + t->prev_non_note = n; + + if (!t->first_non_note) { + t->first_non_note = n; + return; + } + + if (non_note_cmp(p, n) < 0) + ; /* do nothing */ + else if (non_note_cmp(t->first_non_note, n) <= 0) + p = t->first_non_note; + else { + /* n sorts before t->first_non_note */ + n->next = t->first_non_note; + t->first_non_note = n; + return; + } + + /* n sorts equal or after p */ + while (p->next && non_note_cmp(p->next, n) <= 0) + p = p->next; + + if (non_note_cmp(p, n) == 0) { /* n ~= p; overwrite p with n */ + assert(strcmp(p->path, n->path) == 0); + p->mode = n->mode; + hashcpy(p->sha1, n->sha1); + free(n); + t->prev_non_note = p; + return; + } + + /* n sorts between p and p->next */ + n->next = p->next; + p->next = n; +} + +static void load_subtree(struct notes_tree *t, struct leaf_node *subtree, + struct int_node *node, unsigned int n) { - unsigned char commit_sha1[20]; + unsigned char object_sha1[20]; unsigned int prefix_len; void *buf; struct tree_desc desc; struct name_entry entry; + int len, path_len; + unsigned char type; + struct leaf_node *l; buf = fill_tree_descriptor(&desc, subtree->val_sha1); if (!buf) @@ -312,86 +411,588 @@ static void load_subtree(struct leaf_node *subtree, struct int_node *node, prefix_len = subtree->key_sha1[19]; assert(prefix_len * 2 >= n); - memcpy(commit_sha1, subtree->key_sha1, prefix_len); + memcpy(object_sha1, subtree->key_sha1, prefix_len); while (tree_entry(&desc, &entry)) { - int len = get_sha1_hex_segment(entry.path, strlen(entry.path), - commit_sha1 + prefix_len, 20 - prefix_len); + path_len = strlen(entry.path); + len = get_sha1_hex_segment(entry.path, path_len, + object_sha1 + prefix_len, 20 - prefix_len); if (len < 0) - continue; /* entry.path is not a SHA1 sum. Skip */ + goto handle_non_note; /* entry.path is not a SHA1 */ len += prefix_len; /* - * If commit SHA1 is complete (len == 20), assume note object - * If commit SHA1 is incomplete (len < 20), assume note subtree + * If object SHA1 is complete (len == 20), assume note object + * If object SHA1 is incomplete (len < 20), and current + * component consists of 2 hex chars, assume note subtree */ if (len <= 20) { - unsigned char type = PTR_TYPE_NOTE; - struct leaf_node *l = (struct leaf_node *) + type = PTR_TYPE_NOTE; + l = (struct leaf_node *) xcalloc(sizeof(struct leaf_node), 1); - hashcpy(l->key_sha1, commit_sha1); + hashcpy(l->key_sha1, object_sha1); hashcpy(l->val_sha1, entry.sha1); if (len < 20) { - if (!S_ISDIR(entry.mode)) - continue; /* entry cannot be subtree */ + if (!S_ISDIR(entry.mode) || path_len != 2) + goto handle_non_note; /* not subtree */ l->key_sha1[19] = (unsigned char) len; type = PTR_TYPE_SUBTREE; } - note_tree_insert(node, n, l, type); + note_tree_insert(t, node, n, l, type, + combine_notes_concatenate); + } + continue; + +handle_non_note: + /* + * Determine full path for this non-note entry: + * The filename is already found in entry.path, but the + * directory part of the path must be deduced from the subtree + * containing this entry. We assume here that the overall notes + * tree follows a strict byte-based progressive fanout + * structure (i.e. using 2/38, 2/2/36, etc. fanouts, and not + * e.g. 4/36 fanout). This means that if a non-note is found at + * path "dead/beef", the following code will register it as + * being found on "de/ad/beef". + * On the other hand, if you use such non-obvious non-note + * paths in the middle of a notes tree, you deserve what's + * coming to you ;). Note that for non-notes that are not + * SHA1-like at the top level, there will be no problems. + * + * To conclude, it is strongly advised to make sure non-notes + * have at least one non-hex character in the top-level path + * component. + */ + { + char non_note_path[PATH_MAX]; + char *p = non_note_path; + const char *q = sha1_to_hex(subtree->key_sha1); + int i; + for (i = 0; i < prefix_len; i++) { + *p++ = *q++; + *p++ = *q++; + *p++ = '/'; + } + strcpy(p, entry.path); + add_non_note(t, non_note_path, entry.mode, entry.sha1); } } free(buf); } -static void initialize_notes(const char *notes_ref_name) +/* + * Determine optimal on-disk fanout for this part of the notes tree + * + * Given a (sub)tree and the level in the internal tree structure, determine + * whether or not the given existing fanout should be expanded for this + * (sub)tree. + * + * Values of the 'fanout' variable: + * - 0: No fanout (all notes are stored directly in the root notes tree) + * - 1: 2/38 fanout + * - 2: 2/2/36 fanout + * - 3: 2/2/2/34 fanout + * etc. + */ +static unsigned char determine_fanout(struct int_node *tree, unsigned char n, + unsigned char fanout) { - unsigned char sha1[20], commit_sha1[20]; + /* + * The following is a simple heuristic that works well in practice: + * For each even-numbered 16-tree level (remember that each on-disk + * fanout level corresponds to _two_ 16-tree levels), peek at all 16 + * entries at that tree level. If all of them are either int_nodes or + * subtree entries, then there are likely plenty of notes below this + * level, so we return an incremented fanout. + */ + unsigned int i; + if ((n % 2) || (n > 2 * fanout)) + return fanout; + for (i = 0; i < 16; i++) { + switch (GET_PTR_TYPE(tree->a[i])) { + case PTR_TYPE_SUBTREE: + case PTR_TYPE_INTERNAL: + continue; + default: + return fanout; + } + } + return fanout + 1; +} + +static void construct_path_with_fanout(const unsigned char *sha1, + unsigned char fanout, char *path) +{ + unsigned int i = 0, j = 0; + const char *hex_sha1 = sha1_to_hex(sha1); + assert(fanout < 20); + while (fanout) { + path[i++] = hex_sha1[j++]; + path[i++] = hex_sha1[j++]; + path[i++] = '/'; + fanout--; + } + strcpy(path + i, hex_sha1 + j); +} + +static int for_each_note_helper(struct notes_tree *t, struct int_node *tree, + unsigned char n, unsigned char fanout, int flags, + each_note_fn fn, void *cb_data) +{ + unsigned int i; + void *p; + int ret = 0; + struct leaf_node *l; + static char path[40 + 19 + 1]; /* hex SHA1 + 19 * '/' + NUL */ + + fanout = determine_fanout(tree, n, fanout); + for (i = 0; i < 16; i++) { +redo: + p = tree->a[i]; + switch (GET_PTR_TYPE(p)) { + case PTR_TYPE_INTERNAL: + /* recurse into int_node */ + ret = for_each_note_helper(t, CLR_PTR_TYPE(p), n + 1, + fanout, flags, fn, cb_data); + break; + case PTR_TYPE_SUBTREE: + l = (struct leaf_node *) CLR_PTR_TYPE(p); + /* + * Subtree entries in the note tree represent parts of + * the note tree that have not yet been explored. There + * is a direct relationship between subtree entries at + * level 'n' in the tree, and the 'fanout' variable: + * Subtree entries at level 'n <= 2 * fanout' should be + * preserved, since they correspond exactly to a fanout + * directory in the on-disk structure. However, subtree + * entries at level 'n > 2 * fanout' should NOT be + * preserved, but rather consolidated into the above + * notes tree level. We achieve this by unconditionally + * unpacking subtree entries that exist below the + * threshold level at 'n = 2 * fanout'. + */ + if (n <= 2 * fanout && + flags & FOR_EACH_NOTE_YIELD_SUBTREES) { + /* invoke callback with subtree */ + unsigned int path_len = + l->key_sha1[19] * 2 + fanout; + assert(path_len < 40 + 19); + construct_path_with_fanout(l->key_sha1, fanout, + path); + /* Create trailing slash, if needed */ + if (path[path_len - 1] != '/') + path[path_len++] = '/'; + path[path_len] = '\0'; + ret = fn(l->key_sha1, l->val_sha1, path, + cb_data); + } + if (n > fanout * 2 || + !(flags & FOR_EACH_NOTE_DONT_UNPACK_SUBTREES)) { + /* unpack subtree and resume traversal */ + tree->a[i] = NULL; + load_subtree(t, l, tree, n); + free(l); + goto redo; + } + break; + case PTR_TYPE_NOTE: + l = (struct leaf_node *) CLR_PTR_TYPE(p); + construct_path_with_fanout(l->key_sha1, fanout, path); + ret = fn(l->key_sha1, l->val_sha1, path, cb_data); + break; + } + if (ret) + return ret; + } + return 0; +} + +struct tree_write_stack { + struct tree_write_stack *next; + struct strbuf buf; + char path[2]; /* path to subtree in next, if any */ +}; + +static inline int matches_tree_write_stack(struct tree_write_stack *tws, + const char *full_path) +{ + return full_path[0] == tws->path[0] && + full_path[1] == tws->path[1] && + full_path[2] == '/'; +} + +static void write_tree_entry(struct strbuf *buf, unsigned int mode, + const char *path, unsigned int path_len, const + unsigned char *sha1) +{ + strbuf_addf(buf, "%o %.*s%c", mode, path_len, path, '\0'); + strbuf_add(buf, sha1, 20); +} + +static void tree_write_stack_init_subtree(struct tree_write_stack *tws, + const char *path) +{ + struct tree_write_stack *n; + assert(!tws->next); + assert(tws->path[0] == '\0' && tws->path[1] == '\0'); + n = (struct tree_write_stack *) + xmalloc(sizeof(struct tree_write_stack)); + n->next = NULL; + strbuf_init(&n->buf, 256 * (32 + 40)); /* assume 256 entries per tree */ + n->path[0] = n->path[1] = '\0'; + tws->next = n; + tws->path[0] = path[0]; + tws->path[1] = path[1]; +} + +static int tree_write_stack_finish_subtree(struct tree_write_stack *tws) +{ + int ret; + struct tree_write_stack *n = tws->next; + unsigned char s[20]; + if (n) { + ret = tree_write_stack_finish_subtree(n); + if (ret) + return ret; + ret = write_sha1_file(n->buf.buf, n->buf.len, tree_type, s); + if (ret) + return ret; + strbuf_release(&n->buf); + free(n); + tws->next = NULL; + write_tree_entry(&tws->buf, 040000, tws->path, 2, s); + tws->path[0] = tws->path[1] = '\0'; + } + return 0; +} + +static int write_each_note_helper(struct tree_write_stack *tws, + const char *path, unsigned int mode, + const unsigned char *sha1) +{ + size_t path_len = strlen(path); + unsigned int n = 0; + int ret; + + /* Determine common part of tree write stack */ + while (tws && 3 * n < path_len && + matches_tree_write_stack(tws, path + 3 * n)) { + n++; + tws = tws->next; + } + + /* tws point to last matching tree_write_stack entry */ + ret = tree_write_stack_finish_subtree(tws); + if (ret) + return ret; + + /* Start subtrees needed to satisfy path */ + while (3 * n + 2 < path_len && path[3 * n + 2] == '/') { + tree_write_stack_init_subtree(tws, path + 3 * n); + n++; + tws = tws->next; + } + + /* There should be no more directory components in the given path */ + assert(memchr(path + 3 * n, '/', path_len - (3 * n)) == NULL); + + /* Finally add given entry to the current tree object */ + write_tree_entry(&tws->buf, mode, path + 3 * n, path_len - (3 * n), + sha1); + + return 0; +} + +struct write_each_note_data { + struct tree_write_stack *root; + struct non_note *next_non_note; +}; + +static int write_each_non_note_until(const char *note_path, + struct write_each_note_data *d) +{ + struct non_note *n = d->next_non_note; + int cmp, ret; + while (n && (!note_path || (cmp = strcmp(n->path, note_path)) <= 0)) { + if (note_path && cmp == 0) + ; /* do nothing, prefer note to non-note */ + else { + ret = write_each_note_helper(d->root, n->path, n->mode, + n->sha1); + if (ret) + return ret; + } + n = n->next; + } + d->next_non_note = n; + return 0; +} + +static int write_each_note(const unsigned char *object_sha1, + const unsigned char *note_sha1, char *note_path, + void *cb_data) +{ + struct write_each_note_data *d = + (struct write_each_note_data *) cb_data; + size_t note_path_len = strlen(note_path); + unsigned int mode = 0100644; + + if (note_path[note_path_len - 1] == '/') { + /* subtree entry */ + note_path_len--; + note_path[note_path_len] = '\0'; + mode = 040000; + } + assert(note_path_len <= 40 + 19); + + /* Weave non-note entries into note entries */ + return write_each_non_note_until(note_path, d) || + write_each_note_helper(d->root, note_path, mode, note_sha1); +} + +struct note_delete_list { + struct note_delete_list *next; + const unsigned char *sha1; +}; + +static int prune_notes_helper(const unsigned char *object_sha1, + const unsigned char *note_sha1, char *note_path, + void *cb_data) +{ + struct note_delete_list **l = (struct note_delete_list **) cb_data; + struct note_delete_list *n; + + if (has_sha1_file(object_sha1)) + return 0; /* nothing to do for this note */ + + /* failed to find object => prune this note */ + n = (struct note_delete_list *) xmalloc(sizeof(*n)); + n->next = *l; + n->sha1 = object_sha1; + *l = n; + return 0; +} + +int combine_notes_concatenate(unsigned char *cur_sha1, + const unsigned char *new_sha1) +{ + char *cur_msg = NULL, *new_msg = NULL, *buf; + unsigned long cur_len, new_len, buf_len; + enum object_type cur_type, new_type; + int ret; + + /* read in both note blob objects */ + if (!is_null_sha1(new_sha1)) + new_msg = read_sha1_file(new_sha1, &new_type, &new_len); + if (!new_msg || !new_len || new_type != OBJ_BLOB) { + free(new_msg); + return 0; + } + if (!is_null_sha1(cur_sha1)) + cur_msg = read_sha1_file(cur_sha1, &cur_type, &cur_len); + if (!cur_msg || !cur_len || cur_type != OBJ_BLOB) { + free(cur_msg); + free(new_msg); + hashcpy(cur_sha1, new_sha1); + return 0; + } + + /* we will separate the notes by a newline anyway */ + if (cur_msg[cur_len - 1] == '\n') + cur_len--; + + /* concatenate cur_msg and new_msg into buf */ + buf_len = cur_len + 1 + new_len; + buf = (char *) xmalloc(buf_len); + memcpy(buf, cur_msg, cur_len); + buf[cur_len] = '\n'; + memcpy(buf + cur_len + 1, new_msg, new_len); + free(cur_msg); + free(new_msg); + + /* create a new blob object from buf */ + ret = write_sha1_file(buf, buf_len, blob_type, cur_sha1); + free(buf); + return ret; +} + +int combine_notes_overwrite(unsigned char *cur_sha1, + const unsigned char *new_sha1) +{ + hashcpy(cur_sha1, new_sha1); + return 0; +} + +int combine_notes_ignore(unsigned char *cur_sha1, + const unsigned char *new_sha1) +{ + return 0; +} + +void init_notes(struct notes_tree *t, const char *notes_ref, + combine_notes_fn combine_notes, int flags) +{ + unsigned char sha1[20], object_sha1[20]; unsigned mode; struct leaf_node root_tree; - if (!notes_ref_name || read_ref(notes_ref_name, commit_sha1) || - get_tree_entry(commit_sha1, "", sha1, &mode)) + if (!t) + t = &default_notes_tree; + assert(!t->initialized); + + if (!notes_ref) + notes_ref = getenv(GIT_NOTES_REF_ENVIRONMENT); + if (!notes_ref) + notes_ref = notes_ref_name; /* value of core.notesRef config */ + if (!notes_ref) + notes_ref = GIT_NOTES_DEFAULT_REF; + + if (!combine_notes) + combine_notes = combine_notes_concatenate; + + t->root = (struct int_node *) xcalloc(sizeof(struct int_node), 1); + t->first_non_note = NULL; + t->prev_non_note = NULL; + t->ref = notes_ref ? xstrdup(notes_ref) : NULL; + t->combine_notes = combine_notes; + t->initialized = 1; + + if (flags & NOTES_INIT_EMPTY || !notes_ref || + read_ref(notes_ref, object_sha1)) return; + if (get_tree_entry(object_sha1, "", sha1, &mode)) + die("Failed to read notes tree referenced by %s (%s)", + notes_ref, object_sha1); hashclr(root_tree.key_sha1); hashcpy(root_tree.val_sha1, sha1); - load_subtree(&root_tree, &root_node, 0); + load_subtree(t, &root_tree, t->root, 0); } -static unsigned char *lookup_notes(const unsigned char *commit_sha1) +void add_note(struct notes_tree *t, const unsigned char *object_sha1, + const unsigned char *note_sha1, combine_notes_fn combine_notes) { - struct leaf_node *found = note_tree_find(&root_node, 0, commit_sha1); - if (found) - return found->val_sha1; - return NULL; + struct leaf_node *l; + + if (!t) + t = &default_notes_tree; + assert(t->initialized); + if (!combine_notes) + combine_notes = t->combine_notes; + l = (struct leaf_node *) xmalloc(sizeof(struct leaf_node)); + hashcpy(l->key_sha1, object_sha1); + hashcpy(l->val_sha1, note_sha1); + note_tree_insert(t, t->root, 0, l, PTR_TYPE_NOTE, combine_notes); +} + +void remove_note(struct notes_tree *t, const unsigned char *object_sha1) +{ + struct leaf_node l; + + if (!t) + t = &default_notes_tree; + assert(t->initialized); + hashcpy(l.key_sha1, object_sha1); + hashclr(l.val_sha1); + return note_tree_remove(t, t->root, 0, &l); +} + +const unsigned char *get_note(struct notes_tree *t, + const unsigned char *object_sha1) +{ + struct leaf_node *found; + + if (!t) + t = &default_notes_tree; + assert(t->initialized); + found = note_tree_find(t, t->root, 0, object_sha1); + return found ? found->val_sha1 : NULL; } -void free_notes(void) +int for_each_note(struct notes_tree *t, int flags, each_note_fn fn, + void *cb_data) { - note_tree_free(&root_node); - memset(&root_node, 0, sizeof(struct int_node)); - initialized = 0; + if (!t) + t = &default_notes_tree; + assert(t->initialized); + return for_each_note_helper(t, t->root, 0, 0, flags, fn, cb_data); } -void get_commit_notes(const struct commit *commit, struct strbuf *sb, - const char *output_encoding, int flags) +int write_notes_tree(struct notes_tree *t, unsigned char *result) +{ + struct tree_write_stack root; + struct write_each_note_data cb_data; + int ret; + + if (!t) + t = &default_notes_tree; + assert(t->initialized); + + /* Prepare for traversal of current notes tree */ + root.next = NULL; /* last forward entry in list is grounded */ + strbuf_init(&root.buf, 256 * (32 + 40)); /* assume 256 entries */ + root.path[0] = root.path[1] = '\0'; + cb_data.root = &root; + cb_data.next_non_note = t->first_non_note; + + /* Write tree objects representing current notes tree */ + ret = for_each_note(t, FOR_EACH_NOTE_DONT_UNPACK_SUBTREES | + FOR_EACH_NOTE_YIELD_SUBTREES, + write_each_note, &cb_data) || + write_each_non_note_until(NULL, &cb_data) || + tree_write_stack_finish_subtree(&root) || + write_sha1_file(root.buf.buf, root.buf.len, tree_type, result); + strbuf_release(&root.buf); + return ret; +} + +void prune_notes(struct notes_tree *t) +{ + struct note_delete_list *l = NULL; + + if (!t) + t = &default_notes_tree; + assert(t->initialized); + + for_each_note(t, 0, prune_notes_helper, &l); + + while (l) { + remove_note(t, l->sha1); + l = l->next; + } +} + +void free_notes(struct notes_tree *t) +{ + if (!t) + t = &default_notes_tree; + if (t->root) + note_tree_free(t->root); + free(t->root); + while (t->first_non_note) { + t->prev_non_note = t->first_non_note->next; + free(t->first_non_note->path); + free(t->first_non_note); + t->first_non_note = t->prev_non_note; + } + free(t->ref); + memset(t, 0, sizeof(struct notes_tree)); +} + +void format_note(struct notes_tree *t, const unsigned char *object_sha1, + struct strbuf *sb, const char *output_encoding, int flags) { static const char utf8[] = "utf-8"; - unsigned char *sha1; + const unsigned char *sha1; char *msg, *msg_p; unsigned long linelen, msglen; enum object_type type; - if (!initialized) { - const char *env = getenv(GIT_NOTES_REF_ENVIRONMENT); - if (env) - notes_ref_name = getenv(GIT_NOTES_REF_ENVIRONMENT); - else if (!notes_ref_name) - notes_ref_name = GIT_NOTES_DEFAULT_REF; - initialize_notes(notes_ref_name); - initialized = 1; - } + if (!t) + t = &default_notes_tree; + if (!t->initialized) + init_notes(t, NULL, NULL, 0); - sha1 = lookup_notes(commit->object.sha1); + sha1 = get_note(t, object_sha1); if (!sha1) return; @@ -1,13 +1,201 @@ #ifndef NOTES_H #define NOTES_H -/* Free (and de-initialize) the internal notes tree structure */ -void free_notes(void); +/* + * Function type for combining two notes annotating the same object. + * + * When adding a new note annotating the same object as an existing note, it is + * up to the caller to decide how to combine the two notes. The decision is + * made by passing in a function of the following form. The function accepts + * two SHA1s -- of the existing note and the new note, respectively. The + * function then combines the notes in whatever way it sees fit, and writes the + * resulting SHA1 into the first SHA1 argument (cur_sha1). A non-zero return + * value indicates failure. + * + * The two given SHA1s must both be non-NULL and different from each other. + * + * The default combine_notes function (you get this when passing NULL) is + * combine_notes_concatenate(), which appends the contents of the new note to + * the contents of the existing note. + */ +typedef int combine_notes_fn(unsigned char *cur_sha1, const unsigned char *new_sha1); +/* Common notes combinators */ +int combine_notes_concatenate(unsigned char *cur_sha1, const unsigned char *new_sha1); +int combine_notes_overwrite(unsigned char *cur_sha1, const unsigned char *new_sha1); +int combine_notes_ignore(unsigned char *cur_sha1, const unsigned char *new_sha1); + +/* + * Notes tree object + * + * Encapsulates the internal notes tree structure associated with a notes ref. + * Whenever a struct notes_tree pointer is required below, you may pass NULL in + * order to use the default/internal notes tree. E.g. you only need to pass a + * non-NULL value if you need to refer to several different notes trees + * simultaneously. + */ +extern struct notes_tree { + struct int_node *root; + struct non_note *first_non_note, *prev_non_note; + char *ref; + combine_notes_fn *combine_notes; + int initialized; +} default_notes_tree; + +/* + * Flags controlling behaviour of notes tree initialization + * + * Default behaviour is to initialize the notes tree from the tree object + * specified by the given (or default) notes ref. + */ +#define NOTES_INIT_EMPTY 1 + +/* + * Initialize the given notes_tree with the notes tree structure at the given + * ref. If given ref is NULL, the value of the $GIT_NOTES_REF environment + * variable is used, and if that is missing, the default notes ref is used + * ("refs/notes/commits"). + * + * If you need to re-intialize a notes_tree structure (e.g. when switching from + * one notes ref to another), you must first de-initialize the notes_tree + * structure by calling free_notes(struct notes_tree *). + * + * If you pass t == NULL, the default internal notes_tree will be initialized. + * + * The combine_notes function that is passed becomes the default combine_notes + * function for the given notes_tree. If NULL is passed, the default + * combine_notes function is combine_notes_concatenate(). + * + * Precondition: The notes_tree structure is zeroed (this can be achieved with + * memset(t, 0, sizeof(struct notes_tree))) + */ +void init_notes(struct notes_tree *t, const char *notes_ref, + combine_notes_fn combine_notes, int flags); + +/* + * Add the given note object to the given notes_tree structure + * + * IMPORTANT: The changes made by add_note() to the given notes_tree structure + * are not persistent until a subsequent call to write_notes_tree() returns + * zero. + */ +void add_note(struct notes_tree *t, const unsigned char *object_sha1, + const unsigned char *note_sha1, combine_notes_fn combine_notes); + +/* + * Remove the given note object from the given notes_tree structure + * + * IMPORTANT: The changes made by remove_note() to the given notes_tree + * structure are not persistent until a subsequent call to write_notes_tree() + * returns zero. + */ +void remove_note(struct notes_tree *t, const unsigned char *object_sha1); + +/* + * Get the note object SHA1 containing the note data for the given object + * + * Return NULL if the given object has no notes. + */ +const unsigned char *get_note(struct notes_tree *t, + const unsigned char *object_sha1); + +/* + * Flags controlling behaviour of for_each_note() + * + * Default behaviour of for_each_note() is to traverse every single note object + * in the given notes tree, unpacking subtree entries along the way. + * The following flags can be used to alter the default behaviour: + * + * - DONT_UNPACK_SUBTREES causes for_each_note() NOT to unpack and recurse into + * subtree entries while traversing the notes tree. This causes notes within + * those subtrees NOT to be passed to the callback. Use this flag if you + * don't want to traverse _all_ notes, but only want to traverse the parts + * of the notes tree that have already been unpacked (this includes at least + * all notes that have been added/changed). + * + * - YIELD_SUBTREES causes any subtree entries that are encountered to be + * passed to the callback, before recursing into them. Subtree entries are + * not note objects, but represent intermediate directories in the notes + * tree. When passed to the callback, subtree entries will have a trailing + * slash in their path, which the callback may use to differentiate between + * note entries and subtree entries. Note that already-unpacked subtree + * entries are not part of the notes tree, and will therefore not be yielded. + * If this flag is used together with DONT_UNPACK_SUBTREES, for_each_note() + * will yield the subtree entry, but not recurse into it. + */ +#define FOR_EACH_NOTE_DONT_UNPACK_SUBTREES 1 +#define FOR_EACH_NOTE_YIELD_SUBTREES 2 + +/* + * Invoke the specified callback function for each note in the given notes_tree + * + * If the callback returns nonzero, the note walk is aborted, and the return + * value from the callback is returned from for_each_note(). Hence, a zero + * return value from for_each_note() indicates that all notes were walked + * successfully. + * + * IMPORTANT: The callback function is NOT allowed to change the notes tree. + * In other words, the following functions can NOT be invoked (on the current + * notes tree) from within the callback: + * - add_note() + * - remove_note() + * - free_notes() + */ +typedef int each_note_fn(const unsigned char *object_sha1, + const unsigned char *note_sha1, char *note_path, + void *cb_data); +int for_each_note(struct notes_tree *t, int flags, each_note_fn fn, + void *cb_data); + +/* + * Write the given notes_tree structure to the object database + * + * Creates a new tree object encapsulating the current state of the given + * notes_tree, and stores its SHA1 into the 'result' argument. + * + * Returns zero on success, non-zero on failure. + * + * IMPORTANT: Changes made to the given notes_tree are not persistent until + * this function has returned zero. Please also remember to create a + * corresponding commit object, and update the appropriate notes ref. + */ +int write_notes_tree(struct notes_tree *t, unsigned char *result); + +/* + * Remove all notes annotating non-existing objects from the given notes tree + * + * All notes in the given notes_tree that are associated with objects that no + * longer exist in the database, are removed from the notes tree. + * + * IMPORTANT: The changes made by prune_notes() to the given notes_tree + * structure are not persistent until a subsequent call to write_notes_tree() + * returns zero. + */ +void prune_notes(struct notes_tree *t); + +/* + * Free (and de-initialize) the given notes_tree structure + * + * IMPORTANT: Changes made to the given notes_tree since the last, successful + * call to write_notes_tree() will be lost. + */ +void free_notes(struct notes_tree *t); + +/* Flags controlling how notes are formatted */ #define NOTES_SHOW_HEADER 1 #define NOTES_INDENT 2 -void get_commit_notes(const struct commit *commit, struct strbuf *sb, - const char *output_encoding, int flags); +/* + * Fill the given strbuf with the notes associated with the given object. + * + * If the given notes_tree structure is not initialized, it will be auto- + * initialized to the default value (see documentation for init_notes() above). + * If the given notes_tree is NULL, the internal/default notes_tree will be + * used instead. + * + * 'flags' is a bitwise combination of the above formatting flags. + */ +void format_note(struct notes_tree *t, const unsigned char *object_sha1, + struct strbuf *sb, const char *output_encoding, int flags); #endif @@ -775,8 +775,9 @@ static size_t format_commit_one(struct strbuf *sb, const char *placeholder, } return 0; /* unknown %g placeholder */ case 'N': - get_commit_notes(commit, sb, git_log_output_encoding ? - git_log_output_encoding : git_commit_encoding, 0); + format_note(NULL, commit->object.sha1, sb, + git_log_output_encoding ? git_log_output_encoding + : git_commit_encoding, 0); return 1; } @@ -1095,8 +1096,8 @@ void pretty_print_commit(enum cmit_fmt fmt, const struct commit *commit, strbuf_addch(sb, '\n'); if (context->show_notes) - get_commit_notes(commit, sb, encoding, - NOTES_SHOW_HEADER | NOTES_INDENT); + format_note(NULL, commit->object.sha1, sb, encoding, + NOTES_SHOW_HEADER | NOTES_INDENT); free(reencoded); } diff --git a/t/t3301-notes.sh b/t/t3301-notes.sh index 714626d2d6..37b96871c5 100755 --- a/t/t3301-notes.sh +++ b/t/t3301-notes.sh @@ -13,11 +13,11 @@ echo "$MSG" > "$1" echo "$MSG" >& 2 EOF chmod a+x fake_editor.sh -VISUAL=./fake_editor.sh -export VISUAL +GIT_EDITOR=./fake_editor.sh +export GIT_EDITOR test_expect_success 'cannot annotate non-existing HEAD' ' - (MSG=3 && export MSG && test_must_fail git notes edit) + (MSG=3 && export MSG && test_must_fail git notes add) ' test_expect_success setup ' @@ -33,18 +33,18 @@ test_expect_success setup ' test_expect_success 'need valid notes ref' ' (MSG=1 GIT_NOTES_REF=/ && export MSG GIT_NOTES_REF && - test_must_fail git notes edit) && + test_must_fail git notes add) && (MSG=2 GIT_NOTES_REF=/ && export MSG GIT_NOTES_REF && test_must_fail git notes show) ' -test_expect_success 'refusing to edit in refs/heads/' ' +test_expect_success 'refusing to add notes in refs/heads/' ' (MSG=1 GIT_NOTES_REF=refs/heads/bogus && export MSG GIT_NOTES_REF && - test_must_fail git notes edit) + test_must_fail git notes add) ' -test_expect_success 'refusing to edit in refs/remotes/' ' +test_expect_success 'refusing to edit notes in refs/remotes/' ' (MSG=1 GIT_NOTES_REF=refs/remotes/bogus && export MSG GIT_NOTES_REF && test_must_fail git notes edit) @@ -57,8 +57,35 @@ test_expect_success 'handle empty notes gracefully' ' test_expect_success 'create notes' ' git config core.notesRef refs/notes/commits && - MSG=b1 git notes edit && - test ! -f .git/new-notes && + MSG=b4 git notes add && + test ! -f .git/NOTES_EDITMSG && + test 1 = $(git ls-tree refs/notes/commits | wc -l) && + test b4 = $(git notes show) && + git show HEAD^ && + test_must_fail git notes show HEAD^ +' + +test_expect_success 'edit existing notes' ' + MSG=b3 git notes edit && + test ! -f .git/NOTES_EDITMSG && + test 1 = $(git ls-tree refs/notes/commits | wc -l) && + test b3 = $(git notes show) && + git show HEAD^ && + test_must_fail git notes show HEAD^ +' + +test_expect_success 'cannot add note where one exists' ' + ! MSG=b2 git notes add && + test ! -f .git/NOTES_EDITMSG && + test 1 = $(git ls-tree refs/notes/commits | wc -l) && + test b3 = $(git notes show) && + git show HEAD^ && + test_must_fail git notes show HEAD^ +' + +test_expect_success 'can overwrite existing note with "git notes add -f"' ' + MSG=b1 git notes add -f && + test ! -f .git/NOTES_EDITMSG && test 1 = $(git ls-tree refs/notes/commits | wc -l) && test b1 = $(git notes show) && git show HEAD^ && @@ -81,6 +108,7 @@ test_expect_success 'show notes' ' git log -1 > output && test_cmp expect output ' + test_expect_success 'create multi-line notes (setup)' ' : > a3 && git add a3 && @@ -88,7 +116,7 @@ test_expect_success 'create multi-line notes (setup)' ' git commit -m 3rd && MSG="b3 c3c3c3c3 -d3d3d3" git notes edit +d3d3d3" git notes add ' cat > expect-multiline << EOF @@ -111,19 +139,16 @@ test_expect_success 'show multi-line notes' ' git log -2 > output && test_cmp expect-multiline output ' -test_expect_success 'create -m and -F notes (setup)' ' +test_expect_success 'create -F notes (setup)' ' : > a4 && git add a4 && test_tick && git commit -m 4th && echo "xyzzy" > note5 && - git notes edit -m spam -F note5 -m "foo -bar -baz" + git notes add -F note5 ' -whitespace=" " -cat > expect-m-and-F << EOF +cat > expect-F << EOF commit 15023535574ded8b1a89052b32673f84cf9582b8 Author: A U Thor <author@example.com> Date: Thu Apr 7 15:16:13 2005 -0700 @@ -131,21 +156,15 @@ Date: Thu Apr 7 15:16:13 2005 -0700 4th Notes: - spam -$whitespace xyzzy -$whitespace - foo - bar - baz EOF -printf "\n" >> expect-m-and-F -cat expect-multiline >> expect-m-and-F +printf "\n" >> expect-F +cat expect-multiline >> expect-F -test_expect_success 'show -m and -F notes' ' +test_expect_success 'show -F notes' ' git log -3 > output && - test_cmp expect-m-and-F output + test_cmp expect-F output ' cat >expect << EOF @@ -165,13 +184,7 @@ test_expect_success 'git log --pretty=raw does not show notes' ' cat >>expect <<EOF Notes: - spam -$whitespace xyzzy -$whitespace - foo - bar - baz EOF test_expect_success 'git log --show-notes' ' git log -1 --pretty=raw --show-notes >output && @@ -180,17 +193,17 @@ test_expect_success 'git log --show-notes' ' test_expect_success 'git log --no-notes' ' git log -1 --no-notes >output && - ! grep spam output + ! grep xyzzy output ' test_expect_success 'git format-patch does not show notes' ' git format-patch -1 --stdout >output && - ! grep spam output + ! grep xyzzy output ' test_expect_success 'git format-patch --show-notes does show notes' ' git format-patch --show-notes -1 --stdout >output && - grep spam output + grep xyzzy output ' for pretty in \ @@ -203,8 +216,433 @@ do esac test_expect_success "git show $pretty does$not show notes" ' git show $p >output && - eval "$negate grep spam output" + eval "$negate grep xyzzy output" ' done +test_expect_success 'create -m notes (setup)' ' + : > a5 && + git add a5 && + test_tick && + git commit -m 5th && + git notes add -m spam -m "foo +bar +baz" +' + +whitespace=" " +cat > expect-m << EOF +commit bd1753200303d0a0344be813e504253b3d98e74d +Author: A U Thor <author@example.com> +Date: Thu Apr 7 15:17:13 2005 -0700 + + 5th + +Notes: + spam +$whitespace + foo + bar + baz +EOF + +printf "\n" >> expect-m +cat expect-F >> expect-m + +test_expect_success 'show -m notes' ' + git log -4 > output && + test_cmp expect-m output +' + +test_expect_success 'remove note with add -f -F /dev/null (setup)' ' + git notes add -f -F /dev/null +' + +cat > expect-rm-F << EOF +commit bd1753200303d0a0344be813e504253b3d98e74d +Author: A U Thor <author@example.com> +Date: Thu Apr 7 15:17:13 2005 -0700 + + 5th +EOF + +printf "\n" >> expect-rm-F +cat expect-F >> expect-rm-F + +test_expect_success 'verify note removal with -F /dev/null' ' + git log -4 > output && + test_cmp expect-rm-F output && + ! git notes show +' + +test_expect_success 'do not create empty note with -m "" (setup)' ' + git notes add -m "" +' + +test_expect_success 'verify non-creation of note with -m ""' ' + git log -4 > output && + test_cmp expect-rm-F output && + ! git notes show +' + +cat > expect-combine_m_and_F << EOF +foo + +xyzzy + +bar + +zyxxy + +baz +EOF + +test_expect_success 'create note with combination of -m and -F' ' + echo "xyzzy" > note_a && + echo "zyxxy" > note_b && + git notes add -m "foo" -F note_a -m "bar" -F note_b -m "baz" && + git notes show > output && + test_cmp expect-combine_m_and_F output +' + +test_expect_success 'remove note with "git notes remove" (setup)' ' + git notes remove HEAD^ && + git notes remove +' + +cat > expect-rm-remove << EOF +commit bd1753200303d0a0344be813e504253b3d98e74d +Author: A U Thor <author@example.com> +Date: Thu Apr 7 15:17:13 2005 -0700 + + 5th + +commit 15023535574ded8b1a89052b32673f84cf9582b8 +Author: A U Thor <author@example.com> +Date: Thu Apr 7 15:16:13 2005 -0700 + + 4th +EOF + +printf "\n" >> expect-rm-remove +cat expect-multiline >> expect-rm-remove + +test_expect_success 'verify note removal with "git notes remove"' ' + git log -4 > output && + test_cmp expect-rm-remove output && + ! git notes show HEAD^ +' + +cat > expect << EOF +c18dc024e14f08d18d14eea0d747ff692d66d6a3 1584215f1d29c65e99c6c6848626553fdd07fd75 +c9c6af7f78bc47490dbf3e822cf2f3c24d4b9061 268048bfb8a1fb38e703baceb8ab235421bf80c5 +EOF + +test_expect_success 'list notes with "git notes list"' ' + git notes list > output && + test_cmp expect output +' + +test_expect_success 'list notes with "git notes"' ' + git notes > output && + test_cmp expect output +' + +cat > expect << EOF +c18dc024e14f08d18d14eea0d747ff692d66d6a3 +EOF + +test_expect_success 'list specific note with "git notes list <object>"' ' + git notes list HEAD^^ > output && + test_cmp expect output +' + +cat > expect << EOF +EOF + +test_expect_success 'listing non-existing notes fails' ' + test_must_fail git notes list HEAD > output && + test_cmp expect output +' + +cat > expect << EOF +Initial set of notes + +More notes appended with git notes append +EOF + +test_expect_success 'append to existing note with "git notes append"' ' + git notes add -m "Initial set of notes" && + git notes append -m "More notes appended with git notes append" && + git notes show > output && + test_cmp expect output +' + +test_expect_success 'appending empty string does not change existing note' ' + git notes append -m "" && + git notes show > output && + test_cmp expect output +' + +test_expect_success 'git notes append == add when there is no existing note' ' + git notes remove HEAD && + test_must_fail git notes list HEAD && + git notes append -m "Initial set of notes + +More notes appended with git notes append" && + git notes show > output && + test_cmp expect output +' + +test_expect_success 'appending empty string to non-existing note does not create note' ' + git notes remove HEAD && + test_must_fail git notes list HEAD && + git notes append -m "" && + test_must_fail git notes list HEAD +' + +test_expect_success 'create other note on a different notes ref (setup)' ' + : > a6 && + git add a6 && + test_tick && + git commit -m 6th && + GIT_NOTES_REF="refs/notes/other" git notes add -m "other note" +' + +cat > expect-other << EOF +commit 387a89921c73d7ed72cd94d179c1c7048ca47756 +Author: A U Thor <author@example.com> +Date: Thu Apr 7 15:18:13 2005 -0700 + + 6th + +Notes: + other note +EOF + +cat > expect-not-other << EOF +commit 387a89921c73d7ed72cd94d179c1c7048ca47756 +Author: A U Thor <author@example.com> +Date: Thu Apr 7 15:18:13 2005 -0700 + + 6th +EOF + +test_expect_success 'Do not show note on other ref by default' ' + git log -1 > output && + test_cmp expect-not-other output +' + +test_expect_success 'Do show note when ref is given in GIT_NOTES_REF' ' + GIT_NOTES_REF="refs/notes/other" git log -1 > output && + test_cmp expect-other output +' + +test_expect_success 'Do show note when ref is given in core.notesRef config' ' + git config core.notesRef "refs/notes/other" && + git log -1 > output && + test_cmp expect-other output +' + +test_expect_success 'Do not show note when core.notesRef is overridden' ' + GIT_NOTES_REF="refs/notes/wrong" git log -1 > output && + test_cmp expect-not-other output +' + +test_expect_success 'Allow notes on non-commits (trees, blobs, tags)' ' + echo "Note on a tree" > expect + git notes add -m "Note on a tree" HEAD: && + git notes show HEAD: > actual && + test_cmp expect actual && + echo "Note on a blob" > expect + filename=$(git ls-tree --name-only HEAD | head -n1) && + git notes add -m "Note on a blob" HEAD:$filename && + git notes show HEAD:$filename > actual && + test_cmp expect actual && + echo "Note on a tag" > expect + git tag -a -m "This is an annotated tag" foobar HEAD^ && + git notes add -m "Note on a tag" foobar && + git notes show foobar > actual && + test_cmp expect actual +' + +cat > expect << EOF +commit 2ede89468182a62d0bde2583c736089bcf7d7e92 +Author: A U Thor <author@example.com> +Date: Thu Apr 7 15:19:13 2005 -0700 + + 7th + +Notes: + other note +EOF + +test_expect_success 'create note from other note with "git notes add -C"' ' + : > a7 && + git add a7 && + test_tick && + git commit -m 7th && + git notes add -C $(git notes list HEAD^) && + git log -1 > actual && + test_cmp expect actual && + test "$(git notes list HEAD)" = "$(git notes list HEAD^)" +' + +test_expect_success 'create note from non-existing note with "git notes add -C" fails' ' + : > a8 && + git add a8 && + test_tick && + git commit -m 8th && + test_must_fail git notes add -C deadbeef && + test_must_fail git notes list HEAD +' + +cat > expect << EOF +commit 016e982bad97eacdbda0fcbd7ce5b0ba87c81f1b +Author: A U Thor <author@example.com> +Date: Thu Apr 7 15:21:13 2005 -0700 + + 9th + +Notes: + yet another note +EOF + +test_expect_success 'create note from other note with "git notes add -c"' ' + : > a9 && + git add a9 && + test_tick && + git commit -m 9th && + MSG="yet another note" git notes add -c $(git notes list HEAD^^) && + git log -1 > actual && + test_cmp expect actual +' + +test_expect_success 'create note from non-existing note with "git notes add -c" fails' ' + : > a10 && + git add a10 && + test_tick && + git commit -m 10th && + test_must_fail MSG="yet another note" git notes add -c deadbeef && + test_must_fail git notes list HEAD +' + +cat > expect << EOF +commit 016e982bad97eacdbda0fcbd7ce5b0ba87c81f1b +Author: A U Thor <author@example.com> +Date: Thu Apr 7 15:21:13 2005 -0700 + + 9th + +Notes: + yet another note +$whitespace + yet another note +EOF + +test_expect_success 'append to note from other note with "git notes append -C"' ' + git notes append -C $(git notes list HEAD^) HEAD^ && + git log -1 HEAD^ > actual && + test_cmp expect actual +' + +cat > expect << EOF +commit ffed603236bfa3891c49644257a83598afe8ae5a +Author: A U Thor <author@example.com> +Date: Thu Apr 7 15:22:13 2005 -0700 + + 10th + +Notes: + other note +EOF + +test_expect_success 'create note from other note with "git notes append -c"' ' + MSG="other note" git notes append -c $(git notes list HEAD^) && + git log -1 > actual && + test_cmp expect actual +' + +cat > expect << EOF +commit ffed603236bfa3891c49644257a83598afe8ae5a +Author: A U Thor <author@example.com> +Date: Thu Apr 7 15:22:13 2005 -0700 + + 10th + +Notes: + other note +$whitespace + yet another note +EOF + +test_expect_success 'append to note from other note with "git notes append -c"' ' + MSG="yet another note" git notes append -c $(git notes list HEAD) && + git log -1 > actual && + test_cmp expect actual +' + +cat > expect << EOF +commit 6352c5e33dbcab725fe0579be16aa2ba8eb369be +Author: A U Thor <author@example.com> +Date: Thu Apr 7 15:23:13 2005 -0700 + + 11th + +Notes: + other note +$whitespace + yet another note +EOF + +test_expect_success 'copy note with "git notes copy"' ' + : > a11 && + git add a11 && + test_tick && + git commit -m 11th && + git notes copy HEAD^ HEAD && + git log -1 > actual && + test_cmp expect actual && + test "$(git notes list HEAD)" = "$(git notes list HEAD^)" +' + +test_expect_success 'prevent overwrite with "git notes copy"' ' + test_must_fail git notes copy HEAD~2 HEAD && + git log -1 > actual && + test_cmp expect actual && + test "$(git notes list HEAD)" = "$(git notes list HEAD^)" +' + +cat > expect << EOF +commit 6352c5e33dbcab725fe0579be16aa2ba8eb369be +Author: A U Thor <author@example.com> +Date: Thu Apr 7 15:23:13 2005 -0700 + + 11th + +Notes: + yet another note +$whitespace + yet another note +EOF + +test_expect_success 'allow overwrite with "git notes copy -f"' ' + git notes copy -f HEAD~2 HEAD && + git log -1 > actual && + test_cmp expect actual && + test "$(git notes list HEAD)" = "$(git notes list HEAD~2)" +' + +test_expect_success 'cannot copy note from object without notes' ' + : > a12 && + git add a12 && + test_tick && + git commit -m 12th && + : > a13 && + git add a13 && + test_tick && + git commit -m 13th && + test_must_fail git notes copy HEAD^ HEAD +' + test_done diff --git a/t/t3303-notes-subtrees.sh b/t/t3303-notes-subtrees.sh index edc4bc8841..75ec18778e 100755 --- a/t/t3303-notes-subtrees.sh +++ b/t/t3303-notes-subtrees.sh @@ -95,12 +95,12 @@ INPUT_END test_expect_success 'test notes in 2/38-fanout' 'test_sha1_based "s|^..|&/|"' test_expect_success 'verify notes in 2/38-fanout' 'verify_notes' -test_expect_success 'test notes in 4/36-fanout' 'test_sha1_based "s|^....|&/|"' -test_expect_success 'verify notes in 4/36-fanout' 'verify_notes' - test_expect_success 'test notes in 2/2/36-fanout' 'test_sha1_based "s|^\(..\)\(..\)|\1/\2/|"' test_expect_success 'verify notes in 2/2/36-fanout' 'verify_notes' +test_expect_success 'test notes in 2/2/2/34-fanout' 'test_sha1_based "s|^\(..\)\(..\)\(..\)|\1/\2/\3/|"' +test_expect_success 'verify notes in 2/2/2/34-fanout' 'verify_notes' + test_same_notes () { ( start_note_commit && @@ -128,14 +128,17 @@ INPUT_END git fast-import --quiet } -test_expect_success 'test same notes in 4/36-fanout and 2/38-fanout' 'test_same_notes "s|^..|&/|" "s|^....|&/|"' -test_expect_success 'verify same notes in 4/36-fanout and 2/38-fanout' 'verify_notes' +test_expect_success 'test same notes in no fanout and 2/38-fanout' 'test_same_notes "s|^..|&/|" ""' +test_expect_success 'verify same notes in no fanout and 2/38-fanout' 'verify_notes' + +test_expect_success 'test same notes in no fanout and 2/2/36-fanout' 'test_same_notes "s|^\(..\)\(..\)|\1/\2/|" ""' +test_expect_success 'verify same notes in no fanout and 2/2/36-fanout' 'verify_notes' test_expect_success 'test same notes in 2/38-fanout and 2/2/36-fanout' 'test_same_notes "s|^\(..\)\(..\)|\1/\2/|" "s|^..|&/|"' test_expect_success 'verify same notes in 2/38-fanout and 2/2/36-fanout' 'verify_notes' -test_expect_success 'test same notes in 4/36-fanout and 2/2/36-fanout' 'test_same_notes "s|^\(..\)\(..\)|\1/\2/|" "s|^....|&/|"' -test_expect_success 'verify same notes in 4/36-fanout and 2/2/36-fanout' 'verify_notes' +test_expect_success 'test same notes in 2/2/2/34-fanout and 2/2/36-fanout' 'test_same_notes "s|^\(..\)\(..\)|\1/\2/|" "s|^\(..\)\(..\)\(..\)|\1/\2/\3/|"' +test_expect_success 'verify same notes in 2/2/2/34-fanout and 2/2/36-fanout' 'verify_notes' test_concatenated_notes () { ( @@ -176,13 +179,16 @@ verify_concatenated_notes () { test_cmp expect output } -test_expect_success 'test notes in 4/36-fanout concatenated with 2/38-fanout' 'test_concatenated_notes "s|^..|&/|" "s|^....|&/|"' -test_expect_success 'verify notes in 4/36-fanout concatenated with 2/38-fanout' 'verify_concatenated_notes' +test_expect_success 'test notes in no fanout concatenated with 2/38-fanout' 'test_concatenated_notes "s|^..|&/|" ""' +test_expect_success 'verify notes in no fanout concatenated with 2/38-fanout' 'verify_concatenated_notes' + +test_expect_success 'test notes in no fanout concatenated with 2/2/36-fanout' 'test_concatenated_notes "s|^\(..\)\(..\)|\1/\2/|" ""' +test_expect_success 'verify notes in no fanout concatenated with 2/2/36-fanout' 'verify_concatenated_notes' test_expect_success 'test notes in 2/38-fanout concatenated with 2/2/36-fanout' 'test_concatenated_notes "s|^\(..\)\(..\)|\1/\2/|" "s|^..|&/|"' test_expect_success 'verify notes in 2/38-fanout concatenated with 2/2/36-fanout' 'verify_concatenated_notes' -test_expect_success 'test notes in 4/36-fanout concatenated with 2/2/36-fanout' 'test_concatenated_notes "s|^\(..\)\(..\)|\1/\2/|" "s|^....|&/|"' -test_expect_success 'verify notes in 4/36-fanout concatenated with 2/2/36-fanout' 'verify_concatenated_notes' +test_expect_success 'test notes in 2/2/36-fanout concatenated with 2/2/2/34-fanout' 'test_concatenated_notes "s|^\(..\)\(..\)\(..\)|\1/\2/\3/|" "s|^\(..\)\(..\)|\1/\2/|"' +test_expect_success 'verify notes in 2/2/36-fanout concatenated with 2/2/2/34-fanout' 'verify_concatenated_notes' test_done diff --git a/t/t3304-notes-mixed.sh b/t/t3304-notes-mixed.sh index 256687ffb5..1709e8c00b 100755 --- a/t/t3304-notes-mixed.sh +++ b/t/t3304-notes-mixed.sh @@ -131,6 +131,17 @@ data <<EOF another non-note with SHA1-like name EOF +M 644 inline de/adbeefdeadbeefdeadbeefdeadbeefdeadbeef +data <<EOF +This is actually a valid note, albeit to a non-existing object. +It is needed in order to trigger the "mishandling" of the dead/beef non-note. +EOF + +M 644 inline dead/beef +data <<EOF +yet another non-note with SHA1-like name +EOF + INPUT_END git fast-import --quiet <input && git config core.notesRef refs/notes/commits @@ -158,6 +169,9 @@ EXPECT_END cat >expect_nn3 <<EXPECT_END another non-note with SHA1-like name EXPECT_END +cat >expect_nn4 <<EXPECT_END +yet another non-note with SHA1-like name +EXPECT_END test_expect_success "verify contents of non-notes" ' @@ -166,7 +180,27 @@ test_expect_success "verify contents of non-notes" ' git cat-file -p refs/notes/commits:deadbeef > actual_nn2 && test_cmp expect_nn2 actual_nn2 && git cat-file -p refs/notes/commits:de/adbeef > actual_nn3 && - test_cmp expect_nn3 actual_nn3 + test_cmp expect_nn3 actual_nn3 && + git cat-file -p refs/notes/commits:dead/beef > actual_nn4 && + test_cmp expect_nn4 actual_nn4 +' + +test_expect_success "git-notes preserves non-notes" ' + + test_tick && + git notes add -f -m "foo bar" +' + +test_expect_success "verify contents of non-notes after git-notes" ' + + git cat-file -p refs/notes/commits:foobar/non-note.txt > actual_nn1 && + test_cmp expect_nn1 actual_nn1 && + git cat-file -p refs/notes/commits:deadbeef > actual_nn2 && + test_cmp expect_nn2 actual_nn2 && + git cat-file -p refs/notes/commits:de/adbeef > actual_nn3 && + test_cmp expect_nn3 actual_nn3 && + git cat-file -p refs/notes/commits:dead/beef > actual_nn4 && + test_cmp expect_nn4 actual_nn4 ' test_done diff --git a/t/t3305-notes-fanout.sh b/t/t3305-notes-fanout.sh new file mode 100755 index 0000000000..b1ea64b213 --- /dev/null +++ b/t/t3305-notes-fanout.sh @@ -0,0 +1,95 @@ +#!/bin/sh + +test_description='Test that adding/removing many notes triggers automatic fanout restructuring' + +. ./test-lib.sh + +test_expect_success 'creating many notes with git-notes' ' + num_notes=300 && + i=0 && + while test $i -lt $num_notes + do + i=$(($i + 1)) && + test_tick && + echo "file for commit #$i" > file && + git add file && + git commit -q -m "commit #$i" && + git notes add -m "note #$i" || return 1 + done +' + +test_expect_success 'many notes created correctly with git-notes' ' + git log | grep "^ " > output && + i=300 && + while test $i -gt 0 + do + echo " commit #$i" && + echo " note #$i" && + i=$(($i - 1)); + done > expect && + test_cmp expect output +' + +test_expect_success 'many notes created with git-notes triggers fanout' ' + # Expect entire notes tree to have a fanout == 1 + git ls-tree -r --name-only refs/notes/commits | + while read path + do + case "$path" in + ??/??????????????????????????????????????) + : true + ;; + *) + echo "Invalid path \"$path\"" && + return 1 + ;; + esac + done +' + +test_expect_success 'deleting most notes with git-notes' ' + num_notes=250 && + i=0 && + git rev-list HEAD | + while read sha1 + do + i=$(($i + 1)) && + if test $i -gt $num_notes + then + break + fi && + test_tick && + git notes remove "$sha1" + done +' + +test_expect_success 'most notes deleted correctly with git-notes' ' + git log HEAD~250 | grep "^ " > output && + i=50 && + while test $i -gt 0 + do + echo " commit #$i" && + echo " note #$i" && + i=$(($i - 1)); + done > expect && + test_cmp expect output +' + +test_expect_success 'deleting most notes triggers fanout consolidation' ' + # Expect entire notes tree to have a fanout == 0 + git ls-tree -r --name-only refs/notes/commits | + while read path + do + case "$path" in + ????????????????????????????????????????) + : true + ;; + *) + echo "Invalid path \"$path\"" && + return 1 + ;; + esac + done +' + +test_done diff --git a/t/t3306-notes-prune.sh b/t/t3306-notes-prune.sh new file mode 100755 index 0000000000..a0ed0353e6 --- /dev/null +++ b/t/t3306-notes-prune.sh @@ -0,0 +1,94 @@ +#!/bin/sh + +test_description='Test git notes prune' + +. ./test-lib.sh + +test_expect_success 'setup: create a few commits with notes' ' + + : > file1 && + git add file1 && + test_tick && + git commit -m 1st && + git notes add -m "Note #1" && + : > file2 && + git add file2 && + test_tick && + git commit -m 2nd && + git notes add -m "Note #2" && + : > file3 && + git add file3 && + test_tick && + git commit -m 3rd && + git notes add -m "Note #3" +' + +cat > expect <<END_OF_LOG +commit 5ee1c35e83ea47cd3cc4f8cbee0568915fbbbd29 +Author: A U Thor <author@example.com> +Date: Thu Apr 7 15:15:13 2005 -0700 + + 3rd + +Notes: + Note #3 + +commit 08341ad9e94faa089d60fd3f523affb25c6da189 +Author: A U Thor <author@example.com> +Date: Thu Apr 7 15:14:13 2005 -0700 + + 2nd + +Notes: + Note #2 + +commit ab5f302035f2e7aaf04265f08b42034c23256e1f +Author: A U Thor <author@example.com> +Date: Thu Apr 7 15:13:13 2005 -0700 + + 1st + +Notes: + Note #1 +END_OF_LOG + +test_expect_success 'verify commits and notes' ' + + git log > actual && + test_cmp expect actual +' + +test_expect_success 'remove some commits' ' + + git reset --hard HEAD~2 && + git reflog expire --expire=now HEAD && + git gc --prune=now +' + +test_expect_success 'verify that commits are gone' ' + + ! git cat-file -p 5ee1c35e83ea47cd3cc4f8cbee0568915fbbbd29 && + ! git cat-file -p 08341ad9e94faa089d60fd3f523affb25c6da189 && + git cat-file -p ab5f302035f2e7aaf04265f08b42034c23256e1f +' + +test_expect_success 'verify that notes are still present' ' + + git notes show 5ee1c35e83ea47cd3cc4f8cbee0568915fbbbd29 && + git notes show 08341ad9e94faa089d60fd3f523affb25c6da189 && + git notes show ab5f302035f2e7aaf04265f08b42034c23256e1f +' + +test_expect_success 'prune notes' ' + + git notes prune +' + +test_expect_success 'verify that notes are gone' ' + + ! git notes show 5ee1c35e83ea47cd3cc4f8cbee0568915fbbbd29 && + ! git notes show 08341ad9e94faa089d60fd3f523affb25c6da189 && + git notes show ab5f302035f2e7aaf04265f08b42034c23256e1f +' + +test_done |