Merge branch 'js/notes'

* js/notes:
  git-notes: fix printing of multi-line notes
  notes: fix core.notesRef documentation
  Add an expensive test for git-notes
  Speed up git notes lookup
  Add a script to edit/inspect notes
  Introduce commit notes

Conflicts:
	pretty.c
This commit is contained in:
Junio C Hamano 2009-02-05 19:40:39 -08:00
commit 7b75b331f6
15 changed files with 504 additions and 0 deletions

1
.gitignore vendored
View file

@ -82,6 +82,7 @@ git-mktag
git-mktree
git-name-rev
git-mv
git-notes
git-pack-redundant
git-pack-objects
git-pack-refs

View file

@ -422,6 +422,19 @@ relatively high IO latencies. With this set to 'true', git will do the
index comparison to the filesystem data in parallel, allowing
overlapping IO's.
core.notesRef::
When showing commit messages, also show notes which are stored in
the given ref. This ref is expected to contain files named
after the full SHA-1 of the commit they annotate.
+
If such a file exists in the given ref, the referenced blob is read, and
appended to the commit message, separated by a "Notes:" line. If the
given ref itself does not exist, it is not an error, but means that no
notes should be printed.
+
This setting defaults to "refs/notes/commits", and can be overridden by
the `GIT_NOTES_REF` environment variable.
alias.*::
Command aliases for the linkgit:git[1] command wrapper - e.g.
after defining "alias.last = cat-file commit HEAD", the invocation

View file

@ -0,0 +1,46 @@
git-notes(1)
============
NAME
----
git-notes - Add/inspect commit notes
SYNOPSIS
--------
[verse]
'git-notes' (edit | 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).
Author
------
Written by Johannes Schindelin <johannes.schindelin@gmx.de>
Documentation
-------------
Documentation by Johannes Schindelin
GIT
---
Part of the gitlink:git[7] suite

View file

@ -265,6 +265,7 @@ 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-parse-remote.sh
SCRIPT_SH += git-pull.sh
SCRIPT_SH += git-quiltimport.sh
@ -377,6 +378,7 @@ LIB_H += ll-merge.h
LIB_H += log-tree.h
LIB_H += mailmap.h
LIB_H += merge-recursive.h
LIB_H += notes.h
LIB_H += object.h
LIB_H += pack.h
LIB_H += pack-refs.h
@ -459,6 +461,7 @@ LIB_OBJS += match-trees.o
LIB_OBJS += merge-file.o
LIB_OBJS += merge-recursive.o
LIB_OBJS += name-hash.o
LIB_OBJS += notes.o
LIB_OBJS += object.o
LIB_OBJS += pack-check.o
LIB_OBJS += pack-refs.o

View file

@ -371,6 +371,8 @@ static inline enum object_type object_type(unsigned int mode)
#define GITATTRIBUTES_FILE ".gitattributes"
#define INFOATTRIBUTES_FILE "info/attributes"
#define ATTRIBUTE_MACRO_PREFIX "[attr]"
#define GIT_NOTES_REF_ENVIRONMENT "GIT_NOTES_REF"
#define GIT_NOTES_DEFAULT_REF "refs/notes/commits"
extern int is_bare_repository_cfg;
extern int is_bare_repository(void);
@ -542,6 +544,7 @@ enum rebase_setup_type {
extern enum branch_track git_branch_track;
extern enum rebase_setup_type autorebase;
extern char *notes_ref_name;
#define GIT_REPO_VERSION 0
extern int repository_format_version;

View file

@ -73,6 +73,7 @@ git-mktag plumbingmanipulators
git-mktree plumbingmanipulators
git-mv mainporcelain common
git-name-rev plumbinginterrogators
git-notes mainporcelain
git-pack-objects plumbingmanipulators
git-pack-redundant plumbinginterrogators
git-pack-refs ancillarymanipulators

View file

@ -5,6 +5,7 @@
#include "utf8.h"
#include "diff.h"
#include "revision.h"
#include "notes.h"
int save_commit_buffer = 1;

View file

@ -469,6 +469,11 @@ static int git_default_core_config(const char *var, const char *value)
return 0;
}
if (!strcmp(var, "core.notesref")) {
notes_ref_name = xstrdup(value);
return 0;
}
if (!strcmp(var, "core.pager"))
return git_config_string(&pager_program, var, value);

View file

@ -45,6 +45,7 @@ enum rebase_setup_type autorebase = AUTOREBASE_NEVER;
/* Parallel index stat data preload? */
int core_preload_index = 0;
char *notes_ref_name;
/* This is set by setup_git_dir_gently() and/or git_default_config() */
char *git_work_tree_cfg;

65
git-notes.sh Executable file
View file

@ -0,0 +1,65 @@
#!/bin/sh
USAGE="(edit | show) [commit]"
. git-sh-setup
test -n "$3" && usage
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"
COMMIT=$(git rev-parse --verify --default HEAD "$@") ||
die "Invalid commit: $@"
MESSAGE="$GIT_DIR"/new-notes-$COMMIT
trap '
test -f "$MESSAGE" && rm "$MESSAGE"
' 0
case "$ACTION" in
edit)
GIT_NOTES_REF= git log -1 $COMMIT | sed "s/^/#/" > "$MESSAGE"
GIT_INDEX_FILE="$MESSAGE".idx
export GIT_INDEX_FILE
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"
git cat-file blob :$COMMIT >> "$MESSAGE" 2> /dev/null
fi
${VISUAL:-${EDITOR:-vi}} "$MESSAGE"
grep -v ^# < "$MESSAGE" | git stripspace > "$MESSAGE".processed
mv "$MESSAGE".processed "$MESSAGE"
if [ -s "$MESSAGE" ]; then
BLOB=$(git hash-object -w "$MESSAGE") ||
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 show "$GIT_NOTES_REF":$COMMIT
;;
*)
usage
esac

160
notes.c Normal file
View file

@ -0,0 +1,160 @@
#include "cache.h"
#include "commit.h"
#include "notes.h"
#include "refs.h"
#include "utf8.h"
#include "strbuf.h"
#include "tree-walk.h"
struct entry {
unsigned char commit_sha1[20];
unsigned char notes_sha1[20];
};
struct hash_map {
struct entry *entries;
off_t count, size;
};
static int initialized;
static struct hash_map hash_map;
static int hash_index(struct hash_map *map, const unsigned char *sha1)
{
int i = ((*(unsigned int *)sha1) % map->size);
for (;;) {
unsigned char *current = map->entries[i].commit_sha1;
if (!hashcmp(sha1, current))
return i;
if (is_null_sha1(current))
return -1 - i;
if (++i == map->size)
i = 0;
}
}
static void add_entry(const unsigned char *commit_sha1,
const unsigned char *notes_sha1)
{
int index;
if (hash_map.count + 1 > hash_map.size >> 1) {
int i, old_size = hash_map.size;
struct entry *old = hash_map.entries;
hash_map.size = old_size ? old_size << 1 : 64;
hash_map.entries = (struct entry *)
xcalloc(sizeof(struct entry), hash_map.size);
for (i = 0; i < old_size; i++)
if (!is_null_sha1(old[i].commit_sha1)) {
index = -1 - hash_index(&hash_map,
old[i].commit_sha1);
memcpy(hash_map.entries + index, old + i,
sizeof(struct entry));
}
free(old);
}
index = hash_index(&hash_map, commit_sha1);
if (index < 0) {
index = -1 - index;
hash_map.count++;
}
hashcpy(hash_map.entries[index].commit_sha1, commit_sha1);
hashcpy(hash_map.entries[index].notes_sha1, notes_sha1);
}
static void initialize_hash_map(const char *notes_ref_name)
{
unsigned char sha1[20], commit_sha1[20];
unsigned mode;
struct tree_desc desc;
struct name_entry entry;
void *buf;
if (!notes_ref_name || read_ref(notes_ref_name, commit_sha1) ||
get_tree_entry(commit_sha1, "", sha1, &mode))
return;
buf = fill_tree_descriptor(&desc, sha1);
if (!buf)
die("Could not read %s for notes-index", sha1_to_hex(sha1));
while (tree_entry(&desc, &entry))
if (!get_sha1(entry.path, commit_sha1))
add_entry(commit_sha1, entry.sha1);
free(buf);
}
static unsigned char *lookup_notes(const unsigned char *commit_sha1)
{
int index;
if (!hash_map.size)
return NULL;
index = hash_index(&hash_map, commit_sha1);
if (index < 0)
return NULL;
return hash_map.entries[index].notes_sha1;
}
void get_commit_notes(const struct commit *commit, struct strbuf *sb,
const char *output_encoding)
{
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_hash_map(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)
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--;
strbuf_addstr(sb, "\nNotes:\n");
for (msg_p = msg; msg_p < msg + msglen; msg_p += linelen + 1) {
linelen = strchrnul(msg_p, '\n') - msg_p;
strbuf_addstr(sb, " ");
strbuf_add(sb, msg_p, linelen);
strbuf_addch(sb, '\n');
}
free(msg);
}

7
notes.h Normal file
View file

@ -0,0 +1,7 @@
#ifndef NOTES_H
#define NOTES_H
void get_commit_notes(const struct commit *commit, struct strbuf *sb,
const char *output_encoding);
#endif

View file

@ -6,6 +6,7 @@
#include "string-list.h"
#include "mailmap.h"
#include "log-tree.h"
#include "notes.h"
#include "color.h"
static char *user_format;
@ -920,5 +921,9 @@ void pretty_print_commit(enum cmit_fmt fmt, const struct commit *commit,
*/
if (fmt == CMIT_FMT_EMAIL && sb->len <= beginning_of_body)
strbuf_addch(sb, '\n');
if (fmt != CMIT_FMT_ONELINE)
get_commit_notes(commit, sb, encoding);
free(reencoded);
}

95
t/t3301-notes.sh Executable file
View file

@ -0,0 +1,95 @@
#!/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 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='/' git notes edit &&
! MSG=2 GIT_NOTES_REF='/' git notes show
'
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^ &&
! 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_done

View file

@ -0,0 +1,98 @@
#!/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
parent=
test -d .git || {
git init &&
tree=$(git write-tree) &&
while [ $nr -lt $number_of_commits ]; do
test_tick &&
commit=$(echo $nr | git commit-tree $tree $parent) ||
return
parent="-p $commit"
nr=$(($nr+1))
done &&
git update-ref refs/heads/master $commit &&
{
export GIT_INDEX_FILE=.git/temp;
git rev-list HEAD | cat -n | sed "s/^[ ][ ]*/ /g" |
while read nr sha1; do
blob=$(echo note $nr | git hash-object -w --stdin) &&
echo $sha1 | sed "s/^/0644 $blob 0 /"
done | git update-index --index-info &&
tree=$(git write-tree) &&
test_tick &&
commit=$(echo notes | git commit-tree $tree) &&
git update-ref refs/notes/commits $commit
} &&
git config core.notesRef refs/notes/commits
}
}
test_notes () {
count=$1 &&
git config core.notesRef refs/notes/commits &&
git log | grep "^ " > output &&
i=1 &&
while [ $i -le $count ]; do
echo " $(($count-$i))" &&
echo " note $i" &&
i=$(($i+1));
done > expect &&
git diff expect output
}
cat > time_notes << \EOF
mode=$1
i=1
while [ $i -lt $2 ]; do
case $1 in
no-notes)
export GIT_NOTES_REF=non-existing
;;
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