1
0
Fork 0
mirror of https://github.com/git/git.git synced 2024-05-07 11:36:15 +02:00

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
This commit is contained in:
Junio C Hamano 2010-03-15 00:52:06 -07:00
commit 2949151fe9
14 changed files with 2167 additions and 196 deletions

View File

@ -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.
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".
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 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
---

View File

@ -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

View File

@ -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);

455
builtin/notes.c Normal file
View File

@ -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;
}

1
git.c
View File

@ -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 },

843
notes.c
View File

@ -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)
{
unsigned char commit_sha1[20];
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 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 free_notes(void)
void remove_note(struct notes_tree *t, const unsigned char *object_sha1)
{
note_tree_free(&root_node);
memset(&root_node, 0, sizeof(struct int_node));
initialized = 0;
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);
}
void get_commit_notes(const struct commit *commit, struct strbuf *sb,
const char *output_encoding, int flags)
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;
}
int for_each_note(struct notes_tree *t, int flags, each_note_fn fn,
void *cb_data)
{
if (!t)
t = &default_notes_tree;
assert(t->initialized);
return for_each_note_helper(t, t->root, 0, 0, flags, fn, cb_data);
}
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;

196
notes.h
View File

@ -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

View File

@ -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);
}

View File

@ -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

View File

@ -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

View File

@ -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

95
t/t3305-notes-fanout.sh Executable file
View File

@ -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

94
t/t3306-notes-prune.sh Executable file
View File

@ -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