Merge branch 'js/notes'
authorJunio C Hamano <gitster@pobox.com>
Fri, 6 Feb 2009 03:40:39 +0000 (19:40 -0800)
committerJunio C Hamano <gitster@pobox.com>
Fri, 6 Feb 2009 03:40:39 +0000 (19:40 -0800)
* 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

15 files changed:
.gitignore
Documentation/config.txt
Documentation/git-notes.txt [new file with mode: 0644]
Makefile
cache.h
command-list.txt
commit.c
config.c
environment.c
git-notes.sh [new file with mode: 0755]
notes.c [new file with mode: 0644]
notes.h [new file with mode: 0644]
pretty.c
t/t3301-notes.sh [new file with mode: 0755]
t/t3302-notes-index-expensive.sh [new file with mode: 0755]
index 1c57d4c958bc5e8ff539c5f5ddb1c784d923271b..13311f1d5e32db381cc555a97e6eb405e63d5aba 100644 (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
index e2b8775dd308d66027494d8705643c30581e2e65..7fbf64d24cabbe609cf10b34055165a323d4e8b0 100644 (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
diff --git a/Documentation/git-notes.txt b/Documentation/git-notes.txt
new file mode 100644 (file)
index 0000000..3d93625
--- /dev/null
@@ -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
index 627d4d09ba55259d23a2270a2d464ac97348d410..27b9569746179e68c635bdaab8e57395f63faf01 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -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
diff --git a/cache.h b/cache.h
index 45e713e9283dcf7b241291ac121e4d4a771f5796..2d889deb264f189e16c2e9d2fc9d6fb795180773 100644 (file)
--- a/cache.h
+++ b/cache.h
@@ -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;
index 3583a33ee90647d8e6ded02643eb75753760d94f..2dc2c3320cef1d2b8218b6e75e27e6399294d6cf 100644 (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
index aa3b35b6a86891ac9d0628e20a6a46d506bf7700..cf72143f58346c93879a98709d5d6fa8a0981b40 100644 (file)
--- a/commit.c
+++ b/commit.c
@@ -5,6 +5,7 @@
 #include "utf8.h"
 #include "diff.h"
 #include "revision.h"
+#include "notes.h"
 
 int save_commit_buffer = 1;
 
index 790405a213b12a4d1c62d9354e1292e0dc6af057..e5d5b4bd0689d8c2932d501779356fc33a0d0379 100644 (file)
--- a/config.c
+++ b/config.c
@@ -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);
 
index e278bce0ea5f1ddda2dab9012663c6d1d4c6bd89..0edae21e74d53b89b55cd987ed5a6f6ff053e8ee 100644 (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;
diff --git a/git-notes.sh b/git-notes.sh
new file mode 100755 (executable)
index 0000000..bfdbaa8
--- /dev/null
@@ -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
diff --git a/notes.c b/notes.c
new file mode 100644 (file)
index 0000000..bd73784
--- /dev/null
+++ b/notes.c
@@ -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);
+}
diff --git a/notes.h b/notes.h
new file mode 100644 (file)
index 0000000..79d21b6
--- /dev/null
+++ b/notes.h
@@ -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
index cc460b56970c3869e67feb816f76f4804e37e7ce..8d4dbc9fbbcffb73e8669ac08a1292ad2a0f0919 100644 (file)
--- a/pretty.c
+++ b/pretty.c
@@ -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);
 }
diff --git a/t/t3301-notes.sh b/t/t3301-notes.sh
new file mode 100755 (executable)
index 0000000..9393a25
--- /dev/null
@@ -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
diff --git a/t/t3302-notes-index-expensive.sh b/t/t3302-notes-index-expensive.sh
new file mode 100755 (executable)
index 0000000..00d27bf
--- /dev/null
@@ -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