Browse Source
* 'jh/notes' (early part): Add selftests verifying concatenation of multiple notes for the same commit Refactor notes code to concatenate multiple notes annotating the same object Add selftests verifying that we can parse notes trees with various fanouts Teach the notes lookup code to parse notes trees with various fanout schemes Teach notes code to free its internal data structures on request Add '%N'-format for pretty-printing commit notes Add flags to get_commit_notes() to control the format of the note string t3302-notes-index-expensive: Speed up create_repo() fast-import: Add support for importing commit notes Teach "-m <msg>" and "-F <file>" to "git notes edit" Add an expensive test for git-notes Speed up git notes lookup Add a script to edit/inspect notes Introduce commit notes Conflicts: .gitignore Documentation/pretty-formats.txt pretty.cmaint

20 changed files with 1408 additions and 10 deletions
@ -0,0 +1,60 @@
@@ -0,0 +1,60 @@
|
||||
git-notes(1) |
||||
============ |
||||
|
||||
NAME |
||||
---- |
||||
git-notes - Add/inspect commit notes |
||||
|
||||
SYNOPSIS |
||||
-------- |
||||
[verse] |
||||
'git-notes' (edit [-F <file> | -m <msg>] | show) [commit] |
||||
|
||||
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:". |
||||
|
||||
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". |
||||
|
||||
|
||||
SUBCOMMANDS |
||||
----------- |
||||
|
||||
edit:: |
||||
Edit the notes for a given commit (defaults to HEAD). |
||||
|
||||
show:: |
||||
Show the notes for a given commit (defaults to HEAD). |
||||
|
||||
|
||||
OPTIONS |
||||
------- |
||||
-m <msg>:: |
||||
Use the given note message (instead of prompting). |
||||
If multiple `-m` (or `-F`) options are given, their |
||||
values are concatenated as separate paragraphs. |
||||
|
||||
-F <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. |
||||
|
||||
|
||||
Author |
||||
------ |
||||
Written by Johannes Schindelin <johannes.schindelin@gmx.de> |
||||
|
||||
Documentation |
||||
------------- |
||||
Documentation by Johannes Schindelin |
||||
|
||||
GIT |
||||
--- |
||||
Part of the linkgit:git[7] suite |
@ -0,0 +1,121 @@
@@ -0,0 +1,121 @@
|
||||
#!/bin/sh |
||||
|
||||
USAGE="(edit [-F <file> | -m <msg>] | show) [commit]" |
||||
. git-sh-setup |
||||
|
||||
test -z "$1" && usage |
||||
ACTION="$1"; shift |
||||
|
||||
test -z "$GIT_NOTES_REF" && GIT_NOTES_REF="$(git config core.notesref)" |
||||
test -z "$GIT_NOTES_REF" && GIT_NOTES_REF="refs/notes/commits" |
||||
|
||||
MESSAGE= |
||||
while test $# != 0 |
||||
do |
||||
case "$1" in |
||||
-m) |
||||
test "$ACTION" = "edit" || usage |
||||
shift |
||||
if test "$#" = "0"; then |
||||
die "error: option -m needs an argument" |
||||
else |
||||
if [ -z "$MESSAGE" ]; then |
||||
MESSAGE="$1" |
||||
else |
||||
MESSAGE="$MESSAGE |
||||
|
||||
$1" |
||||
fi |
||||
shift |
||||
fi |
||||
;; |
||||
-F) |
||||
test "$ACTION" = "edit" || usage |
||||
shift |
||||
if test "$#" = "0"; then |
||||
die "error: option -F needs an argument" |
||||
else |
||||
if [ -z "$MESSAGE" ]; then |
||||
MESSAGE="$(cat "$1")" |
||||
else |
||||
MESSAGE="$MESSAGE |
||||
|
||||
$(cat "$1")" |
||||
fi |
||||
shift |
||||
fi |
||||
;; |
||||
-*) |
||||
usage |
||||
;; |
||||
*) |
||||
break |
||||
;; |
||||
esac |
||||
done |
||||
|
||||
COMMIT=$(git rev-parse --verify --default HEAD "$@") || |
||||
die "Invalid commit: $@" |
||||
|
||||
case "$ACTION" in |
||||
edit) |
||||
if [ "${GIT_NOTES_REF#refs/notes/}" = "$GIT_NOTES_REF" ]; then |
||||
die "Refusing to edit notes in $GIT_NOTES_REF (outside of refs/notes/)" |
||||
fi |
||||
|
||||
MSG_FILE="$GIT_DIR/new-notes-$COMMIT" |
||||
GIT_INDEX_FILE="$MSG_FILE.idx" |
||||
export GIT_INDEX_FILE |
||||
|
||||
trap ' |
||||
test -f "$MSG_FILE" && rm "$MSG_FILE" |
||||
test -f "$GIT_INDEX_FILE" && rm "$GIT_INDEX_FILE" |
||||
' 0 |
||||
|
||||
CURRENT_HEAD=$(git show-ref "$GIT_NOTES_REF" | cut -f 1 -d ' ') |
||||
if [ -z "$CURRENT_HEAD" ]; then |
||||
PARENT= |
||||
else |
||||
PARENT="-p $CURRENT_HEAD" |
||||
git read-tree "$GIT_NOTES_REF" || die "Could not read index" |
||||
fi |
||||
|
||||
if [ -z "$MESSAGE" ]; then |
||||
GIT_NOTES_REF= git log -1 $COMMIT | sed "s/^/#/" > "$MSG_FILE" |
||||
if [ ! -z "$CURRENT_HEAD" ]; then |
||||
git cat-file blob :$COMMIT >> "$MSG_FILE" 2> /dev/null |
||||
fi |
||||
core_editor="$(git config core.editor)" |
||||
${GIT_EDITOR:-${core_editor:-${VISUAL:-${EDITOR:-vi}}}} "$MSG_FILE" |
||||
else |
||||
echo "$MESSAGE" > "$MSG_FILE" |
||||
fi |
||||
|
||||
grep -v ^# < "$MSG_FILE" | git stripspace > "$MSG_FILE".processed |
||||
mv "$MSG_FILE".processed "$MSG_FILE" |
||||
if [ -s "$MSG_FILE" ]; then |
||||
BLOB=$(git hash-object -w "$MSG_FILE") || |
||||
die "Could not write into object database" |
||||
git update-index --add --cacheinfo 0644 $BLOB $COMMIT || |
||||
die "Could not write index" |
||||
else |
||||
test -z "$CURRENT_HEAD" && |
||||
die "Will not initialise with empty tree" |
||||
git update-index --force-remove $COMMIT || |
||||
die "Could not update index" |
||||
fi |
||||
|
||||
TREE=$(git write-tree) || die "Could not write tree" |
||||
NEW_HEAD=$(echo Annotate $COMMIT | git commit-tree $TREE $PARENT) || |
||||
die "Could not annotate" |
||||
git update-ref -m "Annotate $COMMIT" \ |
||||
"$GIT_NOTES_REF" $NEW_HEAD $CURRENT_HEAD |
||||
;; |
||||
show) |
||||
git rev-parse -q --verify "$GIT_NOTES_REF":$COMMIT > /dev/null || |
||||
die "No note for commit $COMMIT." |
||||
git show "$GIT_NOTES_REF":$COMMIT |
||||
;; |
||||
*) |
||||
usage |
||||
esac |
@ -0,0 +1,429 @@
@@ -0,0 +1,429 @@
|
||||
#include "cache.h" |
||||
#include "commit.h" |
||||
#include "notes.h" |
||||
#include "refs.h" |
||||
#include "utf8.h" |
||||
#include "strbuf.h" |
||||
#include "tree-walk.h" |
||||
|
||||
/* |
||||
* Use a non-balancing simple 16-tree structure with struct int_node as |
||||
* internal nodes, and struct leaf_node as leaf nodes. Each int_node has a |
||||
* 16-array of pointers to its children. |
||||
* The bottom 2 bits of each pointer is used to identify the pointer type |
||||
* - ptr & 3 == 0 - NULL pointer, assert(ptr == NULL) |
||||
* - ptr & 3 == 1 - pointer to next internal node - cast to struct int_node * |
||||
* - ptr & 3 == 2 - pointer to note entry - cast to struct leaf_node * |
||||
* - ptr & 3 == 3 - pointer to subtree entry - cast to struct leaf_node * |
||||
* |
||||
* The root node is a statically allocated struct int_node. |
||||
*/ |
||||
struct int_node { |
||||
void *a[16]; |
||||
}; |
||||
|
||||
/* |
||||
* 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 |
||||
* 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 |
||||
* the prefix. The value is the SHA1 of the tree object containing the notes |
||||
* subtree. |
||||
*/ |
||||
struct leaf_node { |
||||
unsigned char key_sha1[20]; |
||||
unsigned char val_sha1[20]; |
||||
}; |
||||
|
||||
#define PTR_TYPE_NULL 0 |
||||
#define PTR_TYPE_INTERNAL 1 |
||||
#define PTR_TYPE_NOTE 2 |
||||
#define PTR_TYPE_SUBTREE 3 |
||||
|
||||
#define GET_PTR_TYPE(ptr) ((uintptr_t) (ptr) & 3) |
||||
#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 SUBTREE_SHA1_PREFIXCMP(key_sha1, subtree_sha1) \ |
||||
(memcmp(key_sha1, subtree_sha1, subtree_sha1[19])) |
||||
|
||||
static struct int_node root_node; |
||||
|
||||
static int initialized; |
||||
|
||||
static void load_subtree(struct leaf_node *subtree, struct int_node *node, |
||||
unsigned int n); |
||||
|
||||
/* |
||||
* Search the tree until the appropriate location for the given key is found: |
||||
* 1. Start at the root node, with n = 0 |
||||
* 2. If a[0] at the current level is a matching subtree entry, unpack that |
||||
* subtree entry and remove it; restart search at the current level. |
||||
* 3. Use the nth nibble of the key as an index into a: |
||||
* - If a[n] is an int_node, recurse from #2 into that node and increment n |
||||
* - If a matching subtree entry, unpack that subtree entry (and remove it); |
||||
* restart search at the current level. |
||||
* - Otherwise, we have found one of the following: |
||||
* - a subtree entry which does not match the key |
||||
* - a note entry which may or may not match the key |
||||
* - 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, |
||||
unsigned char *n, const unsigned char *key_sha1) |
||||
{ |
||||
struct leaf_node *l; |
||||
unsigned char i; |
||||
void *p = (*tree)->a[0]; |
||||
|
||||
if (GET_PTR_TYPE(p) == 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[0] = NULL; |
||||
load_subtree(l, *tree, *n); |
||||
free(l); |
||||
return note_tree_search(tree, n, key_sha1); |
||||
} |
||||
} |
||||
|
||||
i = GET_NIBBLE(*n, key_sha1); |
||||
p = (*tree)->a[i]; |
||||
switch(GET_PTR_TYPE(p)) { |
||||
case PTR_TYPE_INTERNAL: |
||||
*tree = CLR_PTR_TYPE(p); |
||||
(*n)++; |
||||
return note_tree_search(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); |
||||
free(l); |
||||
return note_tree_search(tree, n, key_sha1); |
||||
} |
||||
/* fall through */ |
||||
default: |
||||
return &((*tree)->a[i]); |
||||
} |
||||
} |
||||
|
||||
/* |
||||
* To find a leaf_node: |
||||
* 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, |
||||
const unsigned char *key_sha1) |
||||
{ |
||||
void **p = note_tree_search(&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)) |
||||
return l; |
||||
} |
||||
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. |
||||
* - 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 |
||||
* location, and restart the insert operation from that level. |
||||
* - 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) |
||||
{ |
||||
struct int_node *new_node; |
||||
struct leaf_node *l; |
||||
void **p = note_tree_search(&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)) { |
||||
case PTR_TYPE_NULL: |
||||
assert(!*p); |
||||
*p = SET_PTR_TYPE(entry, type); |
||||
return; |
||||
case PTR_TYPE_NOTE: |
||||
switch (type) { |
||||
case PTR_TYPE_NOTE: |
||||
if (!hashcmp(l->key_sha1, entry->key_sha1)) { |
||||
/* skip concatenation if l == entry */ |
||||
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), |
||||
sha1_to_hex(l->val_sha1), |
||||
sha1_to_hex(l->key_sha1)); |
||||
free(entry); |
||||
return; |
||||
} |
||||
break; |
||||
case PTR_TYPE_SUBTREE: |
||||
if (!SUBTREE_SHA1_PREFIXCMP(l->key_sha1, |
||||
entry->key_sha1)) { |
||||
/* unpack 'entry' */ |
||||
load_subtree(entry, tree, n); |
||||
free(entry); |
||||
return; |
||||
} |
||||
break; |
||||
} |
||||
break; |
||||
case PTR_TYPE_SUBTREE: |
||||
if (!SUBTREE_SHA1_PREFIXCMP(entry->key_sha1, l->key_sha1)) { |
||||
/* unpack 'l' and restart insert */ |
||||
*p = NULL; |
||||
load_subtree(l, tree, n); |
||||
free(l); |
||||
note_tree_insert(tree, n, entry, type); |
||||
return; |
||||
} |
||||
break; |
||||
} |
||||
|
||||
/* non-matching leaf_node */ |
||||
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)); |
||||
*p = SET_PTR_TYPE(new_node, PTR_TYPE_INTERNAL); |
||||
note_tree_insert(new_node, n + 1, entry, type); |
||||
} |
||||
|
||||
/* Free the entire notes data contained in the given tree */ |
||||
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)) { |
||||
case PTR_TYPE_INTERNAL: |
||||
note_tree_free(CLR_PTR_TYPE(p)); |
||||
/* fall through */ |
||||
case PTR_TYPE_NOTE: |
||||
case PTR_TYPE_SUBTREE: |
||||
free(CLR_PTR_TYPE(p)); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/* |
||||
* Convert a partial SHA1 hex string to the corresponding partial SHA1 value. |
||||
* - hex - Partial SHA1 segment in ASCII hex format |
||||
* - 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). |
||||
* 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). |
||||
*/ |
||||
static int get_sha1_hex_segment(const char *hex, unsigned int hex_len, |
||||
unsigned char *sha1, unsigned int sha1_len) |
||||
{ |
||||
unsigned int i, len = hex_len >> 1; |
||||
if (hex_len % 2 != 0 || len > sha1_len) |
||||
return -1; |
||||
for (i = 0; i < len; i++) { |
||||
unsigned int val = (hexval(hex[0]) << 4) | hexval(hex[1]); |
||||
if (val & ~0xff) |
||||
return -1; |
||||
*sha1++ = val; |
||||
hex += 2; |
||||
} |
||||
for (; i < sha1_len; i++) |
||||
*sha1++ = 0; |
||||
return len; |
||||
} |
||||
|
||||
static void load_subtree(struct leaf_node *subtree, struct int_node *node, |
||||
unsigned int n) |
||||
{ |
||||
unsigned char commit_sha1[20]; |
||||
unsigned int prefix_len; |
||||
void *buf; |
||||
struct tree_desc desc; |
||||
struct name_entry entry; |
||||
|
||||
buf = fill_tree_descriptor(&desc, subtree->val_sha1); |
||||
if (!buf) |
||||
die("Could not read %s for notes-index", |
||||
sha1_to_hex(subtree->val_sha1)); |
||||
|
||||
prefix_len = subtree->key_sha1[19]; |
||||
assert(prefix_len * 2 >= n); |
||||
memcpy(commit_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); |
||||
if (len < 0) |
||||
continue; /* entry.path is not a SHA1 sum. Skip */ |
||||
len += prefix_len; |
||||
|
||||
/* |
||||
* If commit SHA1 is complete (len == 20), assume note object |
||||
* If commit SHA1 is incomplete (len < 20), assume note subtree |
||||
*/ |
||||
if (len <= 20) { |
||||
unsigned char type = PTR_TYPE_NOTE; |
||||
struct leaf_node *l = (struct leaf_node *) |
||||
xcalloc(sizeof(struct leaf_node), 1); |
||||
hashcpy(l->key_sha1, commit_sha1); |
||||
hashcpy(l->val_sha1, entry.sha1); |
||||
if (len < 20) { |
||||
l->key_sha1[19] = (unsigned char) len; |
||||
type = PTR_TYPE_SUBTREE; |
||||
} |
||||
note_tree_insert(node, n, l, type); |
||||
} |
||||
} |
||||
free(buf); |
||||
} |
||||
|
||||
static void initialize_notes(const char *notes_ref_name) |
||||
{ |
||||
unsigned char sha1[20], commit_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)) |
||||
return; |
||||
|
||||
hashclr(root_tree.key_sha1); |
||||
hashcpy(root_tree.val_sha1, sha1); |
||||
load_subtree(&root_tree, &root_node, 0); |
||||
} |
||||
|
||||
static unsigned char *lookup_notes(const unsigned char *commit_sha1) |
||||
{ |
||||
struct leaf_node *found = note_tree_find(&root_node, 0, commit_sha1); |
||||
if (found) |
||||
return found->val_sha1; |
||||
return NULL; |
||||
} |
||||
|
||||
void free_notes(void) |
||||
{ |
||||
note_tree_free(&root_node); |
||||
memset(&root_node, 0, sizeof(struct int_node)); |
||||
initialized = 0; |
||||
} |
||||
|
||||
void get_commit_notes(const struct commit *commit, struct strbuf *sb, |
||||
const char *output_encoding, int flags) |
||||
{ |
||||
static const char utf8[] = "utf-8"; |
||||
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; |
||||
} |
||||
|
||||
sha1 = lookup_notes(commit->object.sha1); |
||||
if (!sha1) |
||||
return; |
||||
|
||||
if (!(msg = read_sha1_file(sha1, &type, &msglen)) || !msglen || |
||||
type != OBJ_BLOB) { |
||||
free(msg); |
||||
return; |
||||
} |
||||
|
||||
if (output_encoding && *output_encoding && |
||||
strcmp(utf8, output_encoding)) { |
||||
char *reencoded = reencode_string(msg, output_encoding, utf8); |
||||
if (reencoded) { |
||||
free(msg); |
||||
msg = reencoded; |
||||
msglen = strlen(msg); |
||||
} |
||||
} |
||||
|
||||
/* we will end the annotation by a newline anyway */ |
||||
if (msglen && msg[msglen - 1] == '\n') |
||||
msglen--; |
||||
|
||||
if (flags & NOTES_SHOW_HEADER) |
||||
strbuf_addstr(sb, "\nNotes:\n"); |
||||
|
||||
for (msg_p = msg; msg_p < msg + msglen; msg_p += linelen + 1) { |
||||
linelen = strchrnul(msg_p, '\n') - msg_p; |
||||
|
||||
if (flags & NOTES_INDENT) |
||||
strbuf_addstr(sb, " "); |
||||
strbuf_add(sb, msg_p, linelen); |
||||
strbuf_addch(sb, '\n'); |
||||
} |
||||
|
||||
free(msg); |
||||
} |
@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
|
||||
#ifndef NOTES_H |
||||
#define NOTES_H |
||||
|
||||
/* Free (and de-initialize) the internal notes tree structure */ |
||||
void free_notes(void); |
||||
|
||||
#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); |
||||
|
||||
#endif |
@ -0,0 +1,150 @@
@@ -0,0 +1,150 @@
|
||||
#!/bin/sh |
||||
# |
||||
# Copyright (c) 2007 Johannes E. Schindelin |
||||
# |
||||
|
||||
test_description='Test commit notes' |
||||
|
||||
. ./test-lib.sh |
||||
|
||||
cat > fake_editor.sh << \EOF |
||||
echo "$MSG" > "$1" |
||||
echo "$MSG" >& 2 |
||||
EOF |
||||
chmod a+x fake_editor.sh |
||||
VISUAL=./fake_editor.sh |
||||
export VISUAL |
||||
|
||||
test_expect_success 'cannot annotate non-existing HEAD' ' |
||||
(MSG=3 && export MSG && test_must_fail git notes edit) |
||||
' |
||||
|
||||
test_expect_success setup ' |
||||
: > a1 && |
||||
git add a1 && |
||||
test_tick && |
||||
git commit -m 1st && |
||||
: > a2 && |
||||
git add a2 && |
||||
test_tick && |
||||
git commit -m 2nd |
||||
' |
||||
|
||||
test_expect_success 'need valid notes ref' ' |
||||
(MSG=1 GIT_NOTES_REF=/ && export MSG GIT_NOTES_REF && |
||||
test_must_fail git notes edit) && |
||||
(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/' ' |
||||
(MSG=1 GIT_NOTES_REF=refs/heads/bogus && |
||||
export MSG GIT_NOTES_REF && |
||||
test_must_fail git notes edit) |
||||
' |
||||
|
||||
test_expect_success 'refusing to edit in refs/remotes/' ' |
||||
(MSG=1 GIT_NOTES_REF=refs/remotes/bogus && |
||||
export MSG GIT_NOTES_REF && |
||||
test_must_fail git notes edit) |
||||
' |
||||
|
||||
# 1 indicates caught gracefully by die, 128 means git-show barked |
||||
test_expect_success 'handle empty notes gracefully' ' |
||||
git notes show ; test 1 = $? |
||||
' |
||||
|
||||
test_expect_success 'create notes' ' |
||||
git config core.notesRef refs/notes/commits && |
||||
MSG=b1 git notes edit && |
||||
test ! -f .git/new-notes && |
||||
test 1 = $(git ls-tree refs/notes/commits | wc -l) && |
||||
test b1 = $(git notes show) && |
||||
git show HEAD^ && |
||||
test_must_fail git notes show HEAD^ |
||||
' |
||||
|
||||
cat > expect << EOF |
||||
commit 268048bfb8a1fb38e703baceb8ab235421bf80c5 |
||||
Author: A U Thor <author@example.com> |
||||
Date: Thu Apr 7 15:14:13 2005 -0700 |
||||
|
||||
2nd |
||||
|
||||
Notes: |
||||
b1 |
||||
EOF |
||||
|
||||
test_expect_success 'show notes' ' |
||||
! (git cat-file commit HEAD | grep b1) && |
||||
git log -1 > output && |
||||
test_cmp expect output |
||||
' |
||||
test_expect_success 'create multi-line notes (setup)' ' |
||||
: > a3 && |
||||
git add a3 && |
||||
test_tick && |
||||
git commit -m 3rd && |
||||
MSG="b3 |
||||
c3c3c3c3 |
||||
d3d3d3" git notes edit |
||||
' |
||||
|
||||
cat > expect-multiline << EOF |
||||
commit 1584215f1d29c65e99c6c6848626553fdd07fd75 |
||||
Author: A U Thor <author@example.com> |
||||
Date: Thu Apr 7 15:15:13 2005 -0700 |
||||
|
||||
3rd |
||||
|
||||
Notes: |
||||
b3 |
||||
c3c3c3c3 |
||||
d3d3d3 |
||||
EOF |
||||
|
||||
printf "\n" >> expect-multiline |
||||
cat expect >> expect-multiline |
||||
|
||||
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)' ' |
||||
: > a4 && |
||||
git add a4 && |
||||
test_tick && |
||||
git commit -m 4th && |
||||
echo "xyzzy" > note5 && |
||||
git notes edit -m spam -F note5 -m "foo |
||||
bar |
||||
baz" |
||||
' |
||||
|
||||
whitespace=" " |
||||
cat > expect-m-and-F << EOF |
||||
commit 15023535574ded8b1a89052b32673f84cf9582b8 |
||||
Author: A U Thor <author@example.com> |
||||
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 |
||||
|
||||
test_expect_success 'show -m and -F notes' ' |
||||
git log -3 > output && |
||||
test_cmp expect-m-and-F output |
||||
' |
||||
|
||||
test_done |
@ -0,0 +1,118 @@
@@ -0,0 +1,118 @@
|
||||
#!/bin/sh |
||||
# |
||||
# Copyright (c) 2007 Johannes E. Schindelin |
||||
# |
||||
|
||||
test_description='Test commit notes index (expensive!)' |
||||
|
||||
. ./test-lib.sh |
||||
|
||||
test -z "$GIT_NOTES_TIMING_TESTS" && { |
||||
say Skipping timing tests |
||||
test_done |
||||
exit |
||||
} |
||||
|
||||
create_repo () { |
||||
number_of_commits=$1 |
||||
nr=0 |
||||
test -d .git || { |
||||
git init && |
||||
( |
||||
while [ $nr -lt $number_of_commits ]; do |
||||
nr=$(($nr+1)) |
||||
mark=$(($nr+$nr)) |
||||
notemark=$(($mark+1)) |
||||
test_tick && |
||||
cat <<INPUT_END && |
||||
commit refs/heads/master |
||||
mark :$mark |
||||
committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE |
||||
data <<COMMIT |
||||
commit #$nr |
||||
COMMIT |
||||
|
||||
M 644 inline file |
||||
data <<EOF |
||||
file in commit #$nr |
||||
EOF |
||||
|
||||
blob |
||||
mark :$notemark |
||||
data <<EOF |
||||
note for commit #$nr |
||||
EOF |
||||
|
||||
INPUT_END |
||||
|
||||
echo "N :$notemark :$mark" >> note_commit |
||||
done && |
||||
test_tick && |
||||
cat <<INPUT_END && |
||||
commit refs/notes/commits |
||||
committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE |
||||
data <<COMMIT |
||||
notes |
||||
COMMIT |
||||
|
||||
INPUT_END |
||||
|
||||
cat note_commit |
||||
) | |
||||
git fast-import --quiet && |
||||
git config core.notesRef refs/notes/commits |
||||
} |
||||
} |
||||
|
||||
test_notes () { |
||||
count=$1 && |
||||
git config core.notesRef refs/notes/commits && |
||||
git log | grep "^ " > output && |
||||
i=$count && |
||||
while [ $i -gt 0 ]; do |
||||
echo " commit #$i" && |
||||
echo " note for commit #$i" && |
||||
i=$(($i-1)); |
||||
done > expect && |
||||
test_cmp expect output |
||||
} |
||||
|
||||
cat > time_notes << \EOF |
||||
mode=$1 |
||||
i=1 |
||||
while [ $i -lt $2 ]; do |
||||
case $1 in |
||||
no-notes) |
||||
GIT_NOTES_REF=non-existing; export GIT_NOTES_REF |
||||
;; |
||||
notes) |
||||
unset GIT_NOTES_REF |
||||
;; |
||||
esac |
||||
git log >/dev/null |
||||
i=$(($i+1)) |
||||
done |
||||
EOF |
||||
|
||||
time_notes () { |
||||
for mode in no-notes notes |
||||
do |
||||
echo $mode |
||||
/usr/bin/time sh ../time_notes $mode $1 |
||||
done |
||||
} |
||||
|
||||
for count in 10 100 1000 10000; do |
||||
|
||||
mkdir $count |
||||
(cd $count; |
||||
|
||||
test_expect_success "setup $count" "create_repo $count" |
||||
|
||||
test_expect_success 'notes work' "test_notes $count" |
||||
|
||||
test_expect_success 'notes timing' "time_notes 100" |
||||
) |
||||
done |
||||
|
||||
test_done |
@ -0,0 +1,188 @@
@@ -0,0 +1,188 @@
|
||||
#!/bin/sh |
||||
|
||||
test_description='Test commit notes organized in subtrees' |
||||
|
||||
. ./test-lib.sh |
||||
|
||||
number_of_commits=100 |
||||
|
||||
start_note_commit () { |
||||
test_tick && |
||||
cat <<INPUT_END |
||||
commit refs/notes/commits |
||||
committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE |
||||
data <<COMMIT |
||||
notes |
||||
COMMIT |
||||
|
||||
from refs/notes/commits^0 |
||||
deleteall |
||||
INPUT_END |
||||
|
||||
} |
||||
|
||||
verify_notes () { |
||||
git log | grep "^ " > output && |
||||
i=$number_of_commits && |
||||
while [ $i -gt 0 ]; do |
||||
echo " commit #$i" && |
||||
echo " note for commit #$i" && |
||||
i=$(($i-1)); |
||||
done > expect && |
||||
test_cmp expect output |
||||
} |
||||
|
||||
test_expect_success "setup: create $number_of_commits commits" ' |
||||
|
||||
( |
||||
nr=0 && |
||||
while [ $nr -lt $number_of_commits ]; do |
||||
nr=$(($nr+1)) && |
||||
test_tick && |
||||
cat <<INPUT_END |
||||
commit refs/heads/master |
||||
committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE |
||||
data <<COMMIT |
||||
commit #$nr |
||||
COMMIT |
||||
|
||||
M 644 inline file |
||||
data <<EOF |
||||
file in commit #$nr |
||||
EOF |
||||
|
||||
INPUT_END |
||||
|
||||
done && |
||||
test_tick && |
||||
cat <<INPUT_END |
||||
commit refs/notes/commits |
||||
committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE |
||||
data <<COMMIT |
||||
no notes |
||||
COMMIT |
||||
|
||||
deleteall |
||||
|
||||
INPUT_END |
||||
|
||||
) | |
||||
git fast-import --quiet && |
||||
git config core.notesRef refs/notes/commits |
||||
' |
||||
|
||||
test_sha1_based () { |
||||
( |
||||
start_note_commit && |
||||
nr=$number_of_commits && |
||||
git rev-list refs/heads/master | |
||||
while read sha1; do |
||||
note_path=$(echo "$sha1" | sed "$1") |
||||
cat <<INPUT_END && |
||||
M 100644 inline $note_path |
||||
data <<EOF |
||||
note for commit #$nr |
||||
EOF |
||||
|
||||
INPUT_END |
||||
|
||||
nr=$(($nr-1)) |
||||
done |
||||
) | |
||||
git fast-import --quiet |
||||
} |
||||
|
||||
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_same_notes () { |
||||
( |
||||
start_note_commit && |
||||
nr=$number_of_commits && |
||||
git rev-list refs/heads/master | |
||||
while read sha1; do |
||||
first_note_path=$(echo "$sha1" | sed "$1") |
||||
second_note_path=$(echo "$sha1" | sed "$2") |
||||
cat <<INPUT_END && |
||||
M 100644 inline $second_note_path |
||||
data <<EOF |
||||
note for commit #$nr |
||||
EOF |
||||
|
||||
M 100644 inline $first_note_path |
||||
data <<EOF |
||||
note for commit #$nr |
||||
EOF |
||||
|
||||
INPUT_END |
||||
|
||||
nr=$(($nr-1)) |
||||
done |
||||
) | |
||||
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 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_concatenated_notes () { |
||||
( |
||||
start_note_commit && |
||||
nr=$number_of_commits && |
||||
git rev-list refs/heads/master | |
||||
while read sha1; do |
||||
first_note_path=$(echo "$sha1" | sed "$1") |
||||
second_note_path=$(echo "$sha1" | sed "$2") |
||||
cat <<INPUT_END && |
||||
M 100644 inline $second_note_path |
||||
data <<EOF |
||||
second note for commit #$nr |
||||
EOF |
||||
|
||||
M 100644 inline $first_note_path |
||||
data <<EOF |
||||
first note for commit #$nr |
||||
EOF |
||||
|
||||
INPUT_END |
||||
|
||||
nr=$(($nr-1)) |
||||
done |
||||
) | |
||||
git fast-import --quiet |
||||
} |
||||
|
||||
verify_concatenated_notes () { |
||||
git log | grep "^ " > output && |
||||
i=$number_of_commits && |
||||
while [ $i -gt 0 ]; do |
||||
echo " commit #$i" && |
||||
echo " first note for commit #$i" && |
||||
echo " second note for commit #$i" && |
||||
i=$(($i-1)); |
||||
done > expect && |
||||
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 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_done |
Loading…
Reference in new issue