From: Johannes Schindelin <hidden> Date: 2016-06-15 22:43:21
Hi,
This patch series replaces the series I sent out a while ago, which added
"commit annotations". Since "commit notes" was liked much better, here
they are.
It picks up the same idea, having a pseudo-branch whose revisions contain
a .git/objects/??/* like file structure, and whose blobs are the commit
notes.
By default, that pseudo-branch is "refs/notes/commits", but it is
overridable by the config variable core.notesRef, which in turn can be
overridden by the environment variable GIT_NOTES_REF. If the given ref
does not exist yet, it is interpreted as empty.
The biggest obstacle was a thinko about the scalability. Tree objects
take free form name entries, and therefore a binary search by name is not
possible.
Patch 6/6 is only a WIP patch, but it shows the road ahead. It adds code
to generate .git/notes-index from refs/notes/commits (or any other ref you
specify as notes ref), which is reused until refs/notes/commits^{tree}
changes. Patch 6/6 is only meant to assess which data structure yields
best performance, and how big the costs are.
However, as long as there are no public, fetchable commit notes, I think
the first 5 patches are safe for application and testing.
Ciao,
Dscho
Johannes Schindelin (6):
Rename git_one_line() to git_line_length() and export it
Introduce commit notes
Add git-notes
Add a test script for "git notes"
Document git-notes
notes: add notes-index for a substantial speedup.
.gitignore | 1 +
Documentation/cmd-list.perl | 1 +
Documentation/config.txt | 15 ++
Documentation/git-notes.txt | 45 ++++
Makefile | 5 +-
cache.h | 1 +
commit.c | 15 +-
commit.h | 1 +
config.c | 5 +
environment.c | 1 +
git-notes.sh | 61 ++++++
notes.c | 416 ++++++++++++++++++++++++++++++++++++++
notes.h | 9 +
t/t3301-notes.sh | 63 ++++++
t/t3302-notes-index-expensive.sh | 118 +++++++++++
15 files changed, 750 insertions(+), 7 deletions(-)
create mode 100644 Documentation/git-notes.txt
create mode 100755 git-notes.sh
create mode 100644 notes.c
create mode 100644 notes.h
create mode 100755 t/t3301-notes.sh
create mode 100755 t/t3302-notes-index-expensive.sh
From: Johannes Schindelin <hidden> Date: 2016-06-15 22:43:21
The function get_one_line() really returns the line length, not the
whole line, but it is really useful, so do not hide it in commit.c,
under the wrong name.
Signed-off-by: Johannes Schindelin <redacted>
---
commit.c | 10 +++++-----
commit.h | 1 +
2 files changed, 6 insertions(+), 5 deletions(-)
@@ -1214,7 +1214,7 @@ unsigned long pretty_print_commit(enum cmit_fmt fmt,/* Skip excess blank lines at the beginning of body, if any... */for(;;){-intlinelen=get_one_line(msg,len);+intlinelen=get_line_length(msg,len);intll=linelen;if(!linelen)break;
From: Johannes Schindelin <hidden> Date: 2016-06-15 22:43:21
Commit notes are blobs which are shown together with the commit
message. These blobs are taken from the notes ref, which you can
configure by the config variable core.notesRef, which in turn can
be overridden by the environment variable GIT_NOTES_REF.
The notes ref is a branch which contains trees much like the
loose object trees in .git/objects/. In other words, to get
at the commit notes for a given SHA-1, take the first two
hex characters as directory name, and the remaining 38 hex
characters as base name, and look that up in the notes ref.
The rationale for putting this information into a ref is this: we
want to be able to fetch and possibly union-merge the notes,
maybe even look at the date when a note was introduced, and we
want to store them efficiently together with the other objects.
There is one severe shortcoming, though. Since tree objects can
contain file names of a variable length, it is not possible to
do a binary search for the correct base name in the tree object's
contents. Therefore this approach does not scale well, because
the average lookup time will be proportional to the number of
commit objects, and therefore the slowdown will be quadratic in
that number.
However, a remedy is near: in a later commit, a .git/notes-index
will be introduced, a cached mapping from commits to commit notes,
to be written when the tree name of the notes ref changes. In
case that notes-index cannot be written, the current (possibly
slow) code will come into effect again.
Signed-off-by: Johannes Schindelin <redacted>
---
Documentation/config.txt | 15 +++++++++++
Makefile | 3 +-
cache.h | 1 +
commit.c | 5 +++
config.c | 5 +++
environment.c | 1 +
notes.c | 64 ++++++++++++++++++++++++++++++++++++++++++++++
notes.h | 9 ++++++
8 files changed, 102 insertions(+), 1 deletions(-)
create mode 100644 notes.c
create mode 100644 notes.h
@@ -285,6 +285,21 @@ core.pager:: The command that git will use to paginate output. Can be overridden with the `GIT_PAGER` environment variable.+core.notesRef::+ When showing commit messages, also show notes which are stored in+ the given ref. This ref is expected to contain paths of the form+ ??/*, where the directory name consists of the first two+ characters of the commit name, and the base name consists of+ the remaining 38 characters.+++If such a path 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 print.+++This setting defaults to "refs/notes/commits", and can be overridden by+the `GIT_NOTES_REF` environment variable.+ alias.*:: Command aliases for the gitlink:git[1] command wrapper - e.g. after defining "alias.last = cat-file commit HEAD", the invocation
@@ -0,0 +1,64 @@+#include"cache.h"+#include"commit.h"+#include"notes.h"+#include"refs.h"++staticintinitialized;++voidget_commit_notes(conststructcommit*commit,+char**buf_p,unsignedlong*offset_p,unsignedlong*space_p)+{+charname[80];+constchar*hex;+unsignedcharsha1[20];+char*msg;+unsignedlongmsgoffset,msglen;+enumobject_typetype;++if(!initialized){+constchar*env=getenv(GIT_NOTES_REF);+if(env){+if(notes_ref_name)+free(notes_ref_name);+notes_ref_name=xstrdup(getenv(GIT_NOTES_REF));+}elseif(!notes_ref_name)+notes_ref_name=xstrdup("refs/notes/commits");+if(notes_ref_name&&read_ref(notes_ref_name,sha1)){+free(notes_ref_name);+notes_ref_name=NULL;+}+initialized=1;+}+if(!notes_ref_name)+return;++hex=sha1_to_hex(commit->object.sha1);+snprintf(name,sizeof(name),"%s:%.*s/%.*s",+notes_ref_name,2,hex,38,hex+2);+if(get_sha1(name,sha1))+return;++if(!(msg=read_sha1_file(sha1,&type,&msglen))||!msglen)+return;+/* we will end the annotation by a newline anyway. */+if(msg[msglen-1]=='\n')+msglen--;++ALLOC_GROW(*buf_p,*offset_p+14+msglen,*space_p);+*offset_p+=sprintf(*buf_p+*offset_p,"\nNotes:\n");++for(msgoffset=0;msgoffset<msglen;){+intlinelen=get_line_length(msg+msgoffset,msglen);++ALLOC_GROW(*buf_p,*offset_p+linelen+6,*space_p);+*offset_p+=sprintf(*buf_p+*offset_p,+" %.*s",linelen,msg+msgoffset);+msgoffset+=linelen;+}+ALLOC_GROW(*buf_p,*offset_p+1,*space_p);+(*buf_p)[*offset_p]='\n';+(*offset_p)++;+free(msg);+}++
@@ -0,0 +1,61 @@+#!/bin/sh++USAGE="(edit | show) [commit]"+.git-sh-setup++test-n"$3"&&usage++test-z"$GIT_NOTES_REF"&&GIT_NOTES_REF="$(gitconfigcore.notesref)"+test-z"$GIT_NOTES_REF"&&+die"No notes ref set."++COMMIT=$(gitrev-parse--verify--defaultHEAD"$2")+NAME=$(echo$COMMIT|sed"s/^../&\//")++case"$1"in+edit)+MESSAGE="$GIT_DIR"/new-notes+GIT_NOTES_REF=gitlog-1$COMMIT|sed"s/^/#/">"$MESSAGE"++GIT_INDEX_FILE="$MESSAGE".idx+exportGIT_INDEX_FILE++CURRENT_HEAD=$(gitshow-ref$GIT_NOTES_REF|cut-f1-d' ')+if[-z"$CURRENT_HEAD"];then+PARENT=+else+PARENT="-p $OLDTIP"+gitread-tree$GIT_NOTES_REF||die"Could not read index"+gitcat-fileblob:$NAME>>"$MESSAGE"2>/dev/null+fi++${VISUAL:-${EDITOR:-vi}}"$MESSAGE"++grep-v^#<"$MESSAGE"|gitstripspace>"$MESSAGE".processed+mv"$MESSAGE".processed"$MESSAGE"+if[-z"$(cat"$MESSAGE")"];then+test-z"$CURRENT_HEAD"&&+die"Will not initialise with empty tree"+gitupdate-index--force-remove$NAME||+die"Could not update index"+else+BLOB=$(githash-object-w"$MESSAGE")||+die"Could not write into object database"+gitupdate-index--add--cacheinfo0644$BLOB$NAME||+die"Could not write index"+fi++TREE=$(gitwrite-tree)||die"Could not write tree"+NEW_HEAD=$(:|gitcommit-tree$TREE$PARENT)||+die"Could not annotate"+case"$CURRENT_HEAD"in+'')gitupdate-ref$GIT_NOTES_REF$NEW_HEAD;;+*)gitupdate-ref$GIT_NOTES_REF$NEW_HEAD$CURRENT_HEAD;;+esac+;;+show)+gitshow"$GIT_NOTES_REF":$NAME+;;+*)+usage+esac
From: Johannes Schindelin <hidden> Date: 2016-06-15 22:43:21
Incidentally, a test for "git notes" implies a test for the
whole commit notes machinery.
Signed-off-by: Johannes Schindelin <redacted>
---
t/t3301-notes.sh | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 63 insertions(+), 0 deletions(-)
create mode 100755 t/t3301-notes.sh
@@ -0,0 +1,45 @@+git-notes(1)+============++NAME+----+git-notes - Add commit notes++SYNOPSIS+--------+[verse]+'git-notes' (edit | show) [commit++DESCRIPTION+-----------+This command allows you to add notes to commit messages, after the+fact. 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 enable commit notes, you have to set the config variable+core.notesRef to something like "refs/notes/commits". 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
From: Johannes Schindelin <hidden> Date: 2016-06-15 22:43:21
Actually, this commit adds two methods for a notes index:
- a sorted list with a fan out to help binary search, and
- a modified hash table.
It also adds a test which is used to determine the best algorithm.
---
Not signed off because this is not suitable to be applied as-is.
It is only meant to test the different approaches.
notes.c | 392 ++++++++++++++++++++++++++++++++++++--
t/t3302-notes-index-expensive.sh | 118 ++++++++++++
2 files changed, 490 insertions(+), 20 deletions(-)
create mode 100755 t/t3302-notes-index-expensive.sh
@@ -1,10 +1,370 @@#include"cache.h"#include"commit.h"+#include"tree-walk.h"#include"notes.h"#include"refs.h"staticintinitialized;+/*+*Therearetwochoicesofdatastructureforthenotesindex.+*+*A)Fanoutenhancedsortedlist.+*+*Thisisaregularsortedlistwitha256entryfanout.Inotherwords,+*everytimeanentryislookedup,abinarysearchisperformedoverthe+*sublistdefinedbythefirstbyteoftheSHA-1.+*+*Thedisadvantageisanaverageruntimelogarithmicinthenumberof+*commitnotes.Theadvantagesareacompactrepresentationondisk,+*anda_guaranteed_logarithmicruntime.+*+*Youcouldevensqueezeoutonemorebyteperentry,sincethe+*firstbyteisknownfromthefanoutlist.Thiswouldcomplicateour+*algorithm,though.+*+*B)Hash+*+*Thisisnotyourclassicalhash.Itis_mostly_likeahash,with+*afewnotableexceptions:+*+*-itispossiblylargerthansizesuggests:sinceitisfilebased,+*itiseasiertowriteattheendthantowraparound.+*+*-asaconsequencewecanmaketheentries_strictly_sorted.This+*isnotonlynicetolookat,butmakesincrementalupdatesmuch,+*mucheasier.+*+*Thedisadvantagesofahashisitsloosepacking.Inordertooperate+*reasonablywell,itneedsasizeroughlydoublethenumberofentries.+*Italsohasaworstruntimelinearinthenumberofentries.+*+*Theadvantageisanexpectedconstantlookuptime.+*+*Theperformanceofahashmapdependshighlyonagoodhashing+*algorithm,toavoidcollisions.Luckyus!SHA-1isaprettygood+*hashingalgorithm.+*+*Thereisanotheradvantagetohashmaps:withnotmucheffort,the+*incrementalupdatecanbeperformedinplace,relyingonO_TRUNCto+*detectinterruptions.Thisoperationhasanexpectedconstantruntime.+*/++structnotes_entry{+unsignedcharcommit_sha1[20];+unsignedcharnotes_sha1[20];+};++structnotes_index{+charsignature[4];/* FANO for fan our, HASH for hash */++unsignedchartree_sha1[20];+unsignedcharsubtree_sha1[256][20];/* for incremental caching */+off_toffsets[256];/* for fan out */+off_tcount,size;/* for hash */+}notes_index;++staticintnotes_index_fd;+staticint(*get_notes)(constunsignedchar*commit_sha1,+unsignedchar*notes_sha1);++#define GIT_NOTES_MODE "GIT_NOTES_MODE"+staticintuse_hash;++staticintindex_uptodate_check(structtree*tree){+constchar*signature=use_hash?"HASH":"FANO";+intfd=open(git_path("notes-index"),O_RDONLY);++if(fd<0)+returnfd;++notes_index_fd=fd;++returnread_in_full(fd,¬es_index,sizeof(notes_index))<0||+memcmp(notes_index.signature,signature,4)||+memcmp(notes_index.tree_sha1,+&tree->object.sha1,20);+}++structlock_fileupdate_lock;++/* this reads the remaining 38 hexchars */+staticintget_remaining_hexchars(unsignedchar*sha1,constchar*path)+{+inti,j1,j2;+for(i=0;i<38;i+=2)+if((j1=hexval(path[i]))<0||+(j2=hexval(path[i+1]))<0)+return-1;+else+sha1[1+i/2]=(j1<<4)|j2;+returnpath[38]!='\0';+}++staticintget_notes_hash_count(structtree*tree){+structtree_descdesc,desc2;+structname_entryentry;+void*buf;+unsignedlongcount=0;++buf=fill_tree_descriptor(&desc,notes_index.tree_sha1);+if(!buf)+return0;+while(tree_entry(&desc,&entry)){+void*buf2=fill_tree_descriptor(&desc2,entry.sha1);+if(!buf2)+continue;+while(tree_entry(&desc2,&entry))+count++;+free(buf2);+}+free(buf);++returncount;+}++staticunsignedlongget_hash_index(constunsignedchar*sha1)+{+return(ntohl(*(unsignedlong*)sha1)%notes_index.size);+}++staticintwrite_hash_gap(intfd,unsignedchar*sha1)+{+off_tmin_offset=sizeof(notes_index)++get_hash_index(sha1)*sizeof(structnotes_entry);+while(min_offset>lseek(fd,0,SEEK_CUR))+if(write_in_full(fd,null_sha1,20)<0||+write_in_full(fd,null_sha1,20)<0)+returnerror("Could not write gaps in notes-index");+return0;+}++staticintupdate_index(structtree*tree){+/*+*Fanoutsortedlist:+*+*Writeouttheheader,andseekbacktoit,inordertoupdateit.+*Actuallyonlyseekattheend,andmakesurethatyouwrite+*somethingbig-endian.+*+*Planforincremental:ifsubtree_sha1isequal,copyout.+*Otherwiseconstruct,andrememberinthecopyoftheheader.+*+*Hash:+*+*Alwaysuseapoweroftwoassize.Notthenexthigherone,but+*thenextnexthigherone.+*+*Readthetreerecursively,andleaveasmanyzerosasneeded+*untilthenextentrycomes.Oriftheentryhasahashlarger+*thanthelastfreeentry,writeitatonce.+*/++/* Plan for incremental: (not in-place)+*Lookattreedifferences.Writenull_sha1untilnext,ornext+*subtree.Continuewritinguntiloriginalentryisnull_sha1or+*greaterthancurrentsubtreeentry.+*/++intnew_fd=hold_lock_file_for_update(&update_lock,+git_path("notes-index"),0);+structtree_descdesc;+structname_entryentry;+void*buf;+inti;++if(new_fd<0)+returnerror("Could not construct notes-index");++memset(¬es_index,0,sizeof(notes_index));+hashcpy(notes_index.tree_sha1,tree->object.sha1);+notes_index.offsets[0]=sizeof(notes_index);+if(use_hash){+notes_index.count=get_notes_hash_count(tree);+for(notes_index.size=1;notes_index.size/2+>=notes_index.count;notes_index.size<<=1)+;/* do nothing */+memcpy(notes_index.signature,"HASH",4);+}else+memcpy(notes_index.signature,"FANO",4);++if(write_in_full(new_fd,¬es_index,sizeof(notes_index))<0)+returnerror("Could not write notes-index");++buf=fill_tree_descriptor(&desc,notes_index.tree_sha1);+if(!buf)+returnerror("Could not read %s for notes-index",+sha1_to_hex(notes_index.tree_sha1));++i=0;+while(tree_entry(&desc,&entry)){+intj1,j2;+unsignedcharsha1[20];+structtree_descdesc2;+structname_entryentry2;+void*buf2;++if(!S_ISDIR(entry.mode)||+(j1=hexval(entry.path[0]))<0||+(j2=hexval(entry.path[1]))<0)+continue;+sha1[0]=j1*16+j2;+while(++i<sha1[0])+notes_index.offsets[i]=notes_index.offsets[i-1];++hashcpy(notes_index.subtree_sha1[i],entry.sha1);+buf2=fill_tree_descriptor(&desc2,entry.sha1);+if(!buf2)+continue;+while(tree_entry(&desc2,&entry2)){+if(get_remaining_hexchars(sha1,entry2.path))+continue;+if(use_hash&&write_hash_gap(new_fd,sha1))+return-1;+if(write_in_full(new_fd,sha1,20)<0||+write_in_full(new_fd,+entry2.sha1,20)<0)+returnerror("Could not write notes-index");+}+free(buf2);+notes_index.offsets[i]=lseek(new_fd,0,SEEK_CUR);+}+free(buf);++while(++i<256)+notes_index.offsets[i]=notes_index.offsets[i-1];++/* update fan_out */+lseek(new_fd,0,SEEK_SET);+write(new_fd,¬es_index,sizeof(notes_index));+lseek(new_fd,notes_index.offsets[255],SEEK_SET);++returnclose(new_fd)||commit_lock_file(&update_lock)||+(notes_index_fd=open(git_path("notes-index"),O_RDONLY));+}++staticvoid*notes_mmap;++staticvoidunmap_notes_mmap(void)+{+munmap(notes_mmap,notes_index.offsets[255]);+}++staticintget_notes_fan_out(constunsignedchar*commit_sha1,+unsignedchar*notes_sha1)+{+/*+*Headerisassumedtoberead.+*+*mmap()thearea,andbisect.+*/+off_toff;+size_tsize;+inti,i2,ret=-1;+structnotes_entry*list;++i=commit_sha1[0];+off=i?notes_index.offsets[i-1]:sizeof(notes_index);+size=notes_index.offsets[i]-off;+if(!size)+return-1;++if(!notes_mmap){+notes_mmap=xmmap(NULL,notes_index.offsets[255],+PROT_READ,MAP_PRIVATE,notes_index_fd,0);+atexit(unmap_notes_mmap);+}++list=(void*)((char*)notes_mmap+off);++i=0;+i2=size/sizeof(*list);+while(i+1<i2){+intmiddle=(i+i2)/2;+intcmp=hashcmp(commit_sha1,list[middle].commit_sha1);+if(cmp<0)+i2=middle;+elseif(cmp>0)+i=middle;+else{+hashcpy(notes_sha1,list[middle].notes_sha1);+i=middle;+ret=0;+break;+}+}+if(i==0&&!hashcmp(commit_sha1,list[i].commit_sha1)){+hashcpy(notes_sha1,list[i].notes_sha1);+ret=0;+}++returnret;+}++staticintget_notes_hash(constunsignedchar*commit_sha1,+unsignedchar*notes_sha1)+{+/*+*Headerisassumedtoberead.fdisstillopen.+*+*Seektohash,readuntillowerorequal(0000...islower...)+*/+inti=get_hash_index(commit_sha1);+structnotes_entryentry;++lseek(notes_index_fd,+sizeof(notes_index)+i*sizeof(entry),SEEK_SET);+while(!read_in_full(notes_index_fd,&entry,sizeof(entry))&&+!is_null_sha1(entry.commit_sha1)){+intcmp=hashcmp(commit_sha1,entry.commit_sha1);+if(!cmp){+hashcpy(notes_sha1,entry.notes_sha1);+return0;+}elseif(cmp<0)+break;+}+return-1;+}++staticinlinevoidinit_notes_index(void)+{+constchar*env;+structcommit*notes_ref;+unsignedcharsha1[20];++if(initialized)+return;++initialized=1;+env=getenv(GIT_NOTES_REF);+if(env){+if(notes_ref_name)+free(notes_ref_name);+notes_ref_name=xstrdup(env);+}elseif(!notes_ref_name)+notes_ref_name=xstrdup("refs/notes/commits");++if(!notes_ref_name)+return;+if(read_ref(notes_ref_name,sha1)){+free(notes_ref_name);+notes_ref_name=NULL;+return;+}+env=getenv("GIT_NOTES_MODE");+if(env&&!strcmp("HASH",env)){+use_hash=1;+get_notes=get_notes_hash;+}elseif(env&&!strcmp("FANO",env))+get_notes=get_notes_fan_out;+if(get_notes&&!get_sha1(notes_ref_name,sha1)&&+(notes_ref=(structcommit*)parse_object(sha1))&&+notes_ref->object.type==OBJ_COMMIT)+if(index_uptodate_check(notes_ref->tree))+if(update_index(notes_ref->tree))+get_notes=NULL;/* disable notes-index */+}+voidget_commit_notes(conststructcommit*commit,char**buf_p,unsignedlong*offset_p,unsignedlong*space_p){
From: Johannes Schindelin <hidden> Date: 2016-06-15 22:43:21
Hi,
[this explains what Patch 6/6 is all about:]
If GIT_NOTES_TIMING_TESTS is set, t3302 will output some timing data.
It will create three repositories, the first with 10 commits and a
commit note for each, the second with 100, the third with 1000.
For each repository, it times "git log" 100 times in several modes:
- with GIT_NOTES_REF set to a non-existing ref (should be equivalent to
the timings without this patch series),
- with no .git/notes-index,
- recreating .git/notes-index as a hash map _every_ time,
- creating .git/notes-index as a hash map, and using it the rest of the time,
- recreating .git/notes-index as a sorted list _every_ time, and
- creating .git/notes-index as a sorted list only the first time, and then
using it to find the notes by binary search.
Here is the output:
* expecting success: create_repo 10
* ok 1: setup 10
* expecting success: test_notes 10
* ok 2: notes work
* expecting success: time_notes 100
no-notes
2.95user 1.19system 0:04.18elapsed 99%CPU (0avgtext+0avgdata 0maxresident)k
0inputs+0outputs (0major+144766minor)pagefaults 0swaps
no-cash
23.05user 5.86system 0:33.06elapsed 87%CPU (0avgtext+0avgdata 0maxresident)k
0inputs+0outputs (0major+639774minor)pagefaults 0swaps
hash-cache-create
23.86user 7.21system 0:32.67elapsed 95%CPU (0avgtext+0avgdata 0maxresident)k
0inputs+0outputs (0major+710958minor)pagefaults 0swaps
hash-cache
3.16user 1.18system 0:04.35elapsed 99%CPU (0avgtext+0avgdata 0maxresident)k
0inputs+0outputs (0major+145160minor)pagefaults 0swaps
sorted-list-cache-create
23.22user 7.32system 0:31.66elapsed 96%CPU (0avgtext+0avgdata 0maxresident)k
0inputs+0outputs (0major+686007minor)pagefaults 0swaps
sorted-list-cache
3.74user 1.81system 0:05.77elapsed 96%CPU (0avgtext+0avgdata 0maxresident)k
0inputs+0outputs (0major+241987minor)pagefaults 0swaps
* ok 3: notes timing
Results:
These timings were taken from a desktop machine with a few background
processes running, so take them with a grain of salt.
As expected, without a .git/notes-index, it scales pretty badly. Creating
.git/notes-index is slightly worse than that, but it typically happens
much less often than looking at a commit message. Therefore the work is
worth it, since the lookup _with_ .git/notes-index is in the same ball park
as no notes at all, with the hash map being better than the sorted
list lookup.
Therefore I will go with the hash map approach, when cleaning up patch 6/6.
But not tonight.
Ciao,
Dscho
test -n "${GIT_NOTES_REF=$(git config core.notesref)}" || die
+COMMIT=$(git rev-parse --verify --default HEAD "$2")
This silently annotates the HEAD commit if $2 is misspelled, I
suspect. Also if HEAD does not exist, COMMIT will be empty and
this whole command will exit with non-zero status, which you
would want to catch here...
+NAME=$(echo $COMMIT | sed "s/^../&\//")
... or here.
+case "$1" in
+edit)
+ MESSAGE="$GIT_DIR"/new-notes
+ GIT_NOTES_REF= git log -1 $COMMIT | sed "s/^/#/" > "$MESSAGE"
$MESSAGE and its associated temporary file needs to be cleaned
up upon command exit; perhaps a trap is in order.
There are some places that have "$GIT_NOTES_REF" in dq and some
places you don't. I think GIT_NOTES_REF begins with refs/ and
consists only of valid refname characters, so unless the user
wants to shoot himself in the foot it should be Ok, but we
probably would want to quote it.
Also, as unquoted $CURRENT_HEAD will not even count as a
parameter to update-ref, you do not have to do that case/esac,
but simply do:
git update-ref "$GIT_NOTES_REF" $NEW_HEAD $CURRENT_HEAD
Would we have reflog for this ref? What would we want to see as
the message if we do?
From: Junio C Hamano <hidden> Date: 2016-06-15 22:43:21
Johannes Schindelin [off-list ref] writes:
+core.notesRef::
+ When showing commit messages, also show notes which are stored in
+ the given ref. This ref is expected to contain paths of the form
+ ??/*, where the directory name consists of the first two
+ characters of the commit name, and the base name consists of
+ the remaining 38 characters.
++
+If such a path 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 print.
++
+This setting defaults to "refs/notes/commits", and can be overridden by
+the `GIT_NOTES_REF` environment variable.
+
This design forces "one blob and only one blob decorates a
commit". It certainly makes the implementation and semantics
simpler -- if I have this note and you have that note on the
same commit, comparing notes eventually should result in a merge
of our notes. But is it sufficient in real life usage scenarios
(what's the use case)? One example that was raised on the list
is to collect "Acked-by", "Tested-by", etc., and in that case
perhaps one set "refs/notes/acks" may hold the former while
"refs/notes/tests" the latter. If we wanted to show both at the
same time, is it the only option to put them in the same "note"
blob and not use "refs/notes/{acks,tests}"?
@@ -1254,6 +1255,10 @@ unsigned long pretty_print_commit(enum cmit_fmt fmt,*/if(fmt==CMIT_FMT_EMAIL&&offset<=beginning_of_body)buf[offset++]='\n';++if(fmt!=CMIT_FMT_ONELINE)+get_commit_notes(commit,buf_p,&offset,space_p);+buf[offset]='\0';free(reencoded);returnoffset;
This makes me wonder if there are cases where "notes" need to be
reencoded to honor log_output_encoding.
Since more and more people live in UTF-8 only world, and this is
a _new_ feature anyway, we could declare that "notes" blobs MUST
be encoded in UTF-8 upfront, but even if we did so we would need
reencoding to log_output_encoding, I suspect.
Judging from the existing entries in cache.h, it seems that
GIT_NOTES_REF_ENVIRONMENT would be more appropriate preprocessor
symbol for this. Also let's have this in cache.h next to
GIT_DIR_ENVIRONMENT and friends, with another definition for
"refs/notes/commits".
Is there particular reason for that (lack of) indentation for
the two lines among them?
I think it is a bug to leave ".git/new-notes" and friends
behind.
From: Shawn O. Pearce <hidden> Date: 2016-06-15 22:43:21
Johannes Schindelin [off-list ref] wrote:
Actually, this commit adds two methods for a notes index:
- a sorted list with a fan out to help binary search, and
- a modified hash table.
It also adds a test which is used to determine the best algorithm.
I know this is a nice backwards compatible way to organize notes,
and to make them reasonably efficiently found, but I'd almost
rather just see them crammed into the packfile alongside of the
commit it annotates, so that the packfile reader can quickly find
the annotation at the same time it finds the commit.
aka packv4.
Ok, enough dreaming for today.
--
Shawn.
From: Andy Parkins <hidden> Date: 2016-06-15 22:43:21
On Monday 2007 July 16, Johannes Schindelin wrote:
The biggest obstacle was a thinko about the scalability. Tree objects
take free form name entries, and therefore a binary search by name is not
possible.
I might be misunderstanding, but in the case of the notes tree objects isn't
it true that the name entries aren't free form, but are guaranteed to be of a
fixed length form:
XX/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
In which case you can binary search?
Andy
--
Dr Andy Parkins, M Eng (hons), MIET
andyparkins@gmail.com
From: Johannes Schindelin <hidden> Date: 2016-06-15 22:43:21
Hi,
On Mon, 16 Jul 2007, Shawn O. Pearce wrote:
Johannes Schindelin [off-list ref] wrote:
quoted
Actually, this commit adds two methods for a notes index:
- a sorted list with a fan out to help binary search, and
- a modified hash table.
It also adds a test which is used to determine the best algorithm.
I know this is a nice backwards compatible way to organize notes,
and to make them reasonably efficiently found, but I'd almost
rather just see them crammed into the packfile alongside of the
commit it annotates, so that the packfile reader can quickly find
the annotation at the same time it finds the commit.
aka packv4.
Ok, enough dreaming for today.
Yes, I also dream of having the time to play with packv4. If you read my
comments in the commit-annotation thread, you'll see that I stated that
packv4 would solve the problem, too.
The reason I did this series was not to push commit notes, but to make
good for stalling Johan's efforts. Including a proof that the commit
notes as I introduced them can be relatively cheap, too.
Ciao,
Dscho
From: Johannes Schindelin <hidden> Date: 2016-06-15 22:43:22
Commit notes are blobs which are shown together with the commit
message. These blobs are taken from the notes ref, which you can
configure by the config variable core.notesRef, which in turn can
be overridden by the environment variable GIT_NOTES_REF.
The notes ref is a branch which contains trees much like the
loose object trees in .git/objects/. In other words, to get
at the commit notes for a given SHA-1, take the first two
hex characters as directory name, and the remaining 38 hex
characters as base name, and look that up in the notes ref.
The rationale for putting this information into a ref is this: we
want to be able to fetch and possibly union-merge the notes,
maybe even look at the date when a note was introduced, and we
want to store them efficiently together with the other objects.
There is one severe shortcoming, though. Since tree objects can
contain file names of a variable length, it is not possible to
do a binary search for the correct base name in the tree object's
contents. Therefore this approach does not scale well, because
the average lookup time will be proportional to the number of
commit objects, and therefore the slowdown will be quadratic in
that number.
However, a remedy is near: in a later commit, a .git/notes-index
will be introduced, a cached mapping from commits to commit notes,
to be written when the tree name of the notes ref changes. In
case that notes-index cannot be written, the current (possibly
slow) code will come into effect again.
Signed-off-by: Johannes Schindelin <redacted>
---
On Sun, 15 Jul 2007, Junio C Hamano wrote:
> This design forces "one blob and only one blob decorates a
> commit". It certainly makes the implementation and semantics
> simpler -- if I have this note and you have that note on the
> same commit, comparing notes eventually should result in a merge
> of our notes. But is it sufficient in real life usage scenarios
> (what's the use case)? One example that was raised on the list
> is to collect "Acked-by", "Tested-by", etc., and in that case
> perhaps one set "refs/notes/acks" may hold the former while
> "refs/notes/tests" the latter. If we wanted to show both at the
> same time, is it the only option to put them in the same "note"
> blob and not use "refs/notes/{acks,tests}"?
Would that not make things even slower? I am hesitant.
All other concerns should be addressed, here and in the two
upcoming revised patches.
Documentation/config.txt | 15 +++++++++
Makefile | 3 +-
cache.h | 3 ++
commit.c | 5 +++
config.c | 5 +++
environment.c | 1 +
notes.c | 77 ++++++++++++++++++++++++++++++++++++++++++++++
notes.h | 8 +++++
8 files changed, 116 insertions(+), 1 deletions(-)
create mode 100644 notes.c
create mode 100644 notes.h
@@ -285,6 +285,21 @@ core.pager:: The command that git will use to paginate output. Can be overridden with the `GIT_PAGER` environment variable.+core.notesRef::+ When showing commit messages, also show notes which are stored in+ the given ref. This ref is expected to contain paths of the form+ ??/*, where the directory name consists of the first two+ characters of the commit name, and the base name consists of+ the remaining 38 characters.+++If such a path 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 print.+++This setting defaults to "refs/notes/commits", and can be overridden by+the `GIT_NOTES_REF` environment variable.+ alias.*:: Command aliases for the gitlink:git[1] command wrapper - e.g. after defining "alias.last = cat-file commit HEAD", the invocation
@@ -0,0 +1,77 @@+#include"cache.h"+#include"commit.h"+#include"notes.h"+#include"refs.h"+#include"utf8.h"++staticintinitialized;++voidget_commit_notes(conststructcommit*commit,+char**buf_p,unsignedlong*offset_p,unsignedlong*space_p,+constchar*output_encoding)+{+staticconstchar*utf8="utf-8";+charname[80];+constchar*hex;+unsignedcharsha1[20];+char*msg;+unsignedlongmsgoffset,msglen;+enumobject_typetype;++if(!initialized){+constchar*env=getenv(GIT_NOTES_REF_ENVIRONMENT);+if(env)+notes_ref_name=getenv(GIT_NOTES_REF_ENVIRONMENT);+elseif(!notes_ref_name)+notes_ref_name=GIT_NOTES_DEFAULT_REF;+if(notes_ref_name&&read_ref(notes_ref_name,sha1))+notes_ref_name=NULL;+initialized=1;+}+if(!notes_ref_name)+return;++hex=sha1_to_hex(commit->object.sha1);+if(snprintf(name,sizeof(name),"%s:%.*s/%.*s",+notes_ref_name,2,hex,38,hex+2)+>=sizeof(name)-1){+error("Notes ref name too long: %.*s",60,notes_ref_name);+return;+}+if(get_sha1(name,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(msg[msglen-1]=='\n')+msglen--;++ALLOC_GROW(*buf_p,*offset_p+8+msglen,*space_p);+*offset_p+=sprintf(*buf_p+*offset_p,"\nNotes:\n");++for(msgoffset=0;msgoffset<msglen;){+intlinelen=get_line_length(msg+msgoffset,msglen);++ALLOC_GROW(*buf_p,*offset_p+linelen+5,*space_p);+*offset_p+=sprintf(*buf_p+*offset_p,+" %.*s",linelen,msg+msgoffset);+msgoffset+=linelen;+}+ALLOC_GROW(*buf_p,*offset_p+1,*space_p);+(*buf_p)[*offset_p]='\n';+(*offset_p)++;+free(msg);+}++
From: Johannes Schindelin <hidden> Date: 2016-06-15 22:43:22
Incidentally, a test for "git notes" implies a test for the
whole commit notes machinery.
Signed-off-by: Johannes Schindelin <redacted>
---
t/t3301-notes.sh | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 65 insertions(+), 0 deletions(-)
create mode 100755 t/t3301-notes.sh
@@ -0,0 +1,62 @@+#!/bin/sh++USAGE="(edit | show) [commit]"+.git-sh-setup++test-n"$3"&&usage++test-z"$GIT_NOTES_REF"&&GIT_NOTES_REF="$(gitconfigcore.notesref)"+test-z"$GIT_NOTES_REF"&&GIT_NOTES_REF="refs/notes/commits"++COMMIT=$(gitrev-parse--verify--defaultHEAD"$2")||die"Invalid ref: $2"+NAME=$(echo$COMMIT|sed"s/^../&\//")++MESSAGE="$GIT_DIR"/new-notes+trap'+test-f"$MESSAGE"&&rm"$MESSAGE"+'0++case"$1"in+edit)+GIT_NOTES_REF=gitlog-1$COMMIT|sed"s/^/#/">"$MESSAGE"++GIT_INDEX_FILE="$MESSAGE".idx+exportGIT_INDEX_FILE++CURRENT_HEAD=$(gitshow-ref"$GIT_NOTES_REF"|cut-f1-d' ')+if[-z"$CURRENT_HEAD"];then+PARENT=+else+PARENT="-p $CURRENT_HEAD"+gitread-tree"$GIT_NOTES_REF"||die"Could not read index"+gitcat-fileblob:$NAME>>"$MESSAGE"2>/dev/null+fi++${VISUAL:-${EDITOR:-vi}}"$MESSAGE"++grep-v^#<"$MESSAGE"|gitstripspace>"$MESSAGE".processed+mv"$MESSAGE".processed"$MESSAGE"+if[-s"$MESSAGE"];then+BLOB=$(githash-object-w"$MESSAGE")||+die"Could not write into object database"+gitupdate-index--add--cacheinfo0644$BLOB$NAME||+die"Could not write index"+else+test-z"$CURRENT_HEAD"&&+die"Will not initialise with empty tree"+gitupdate-index--force-remove$NAME||+die"Could not update index"+fi++TREE=$(gitwrite-tree)||die"Could not write tree"+NEW_HEAD=$(echoAnnotate$COMMIT|gitcommit-tree$TREE$PARENT)||+die"Could not annotate"+gitupdate-ref-m"Annotate $COMMIT"\+"$GIT_NOTES_REF"$NEW_HEAD$CURRENT_HEAD+;;+show)+gitshow"$GIT_NOTES_REF":$NAME+;;+*)+usage+esac
There is one severe shortcoming, though. Since tree objects can
contain file names of a variable length, it is not possible to
do a binary search for the correct base name in the tree object's
contents.
Well, I've been thinking about this, and that's not really entirely
correct.
It *is* possible to do a binary search, it's just a bit complicated,
because you have to take the "halfway" thing, and find the beginning of
an entry.
But the good news is that the tree entries have a very fixed format, and
one that is actually amenable to finding where they start. It gets a bit
complicated, but:
- SHA1's contain random bytes, so we cannot really depend on their
content. Fair enough. But on the other hand, they are fixed length,
which means..
- Each SHA1 is always preceded by a zero byte (it is what separates the
filename from the SHA1), and while filenames too can have arbitrary
content (and arbitrary length), we know that the *filename* doesn't
have a zero byte in it.
- so finding the beginning of a tree entry should be as easy as finding
two zero bytes that are have at least 20 bytes in between them, and
then you *know* that the second zero byte is the one that starts a new
SHA1 (it cannot be _inside_ a SHA1: if it was, there would be less
than twenty bytes to the previous '\0', and it cannot be inside the
filename either).
- And you know that 20 bytes after that '\0' is the next tree entry!
Now, what does this mean? It means that if we actually know the filename
we're looking for, and we're looking at a large range, we really *could*
start out with binary searching. We would do something like this:
unsigned char *start;
unsigned long size;
while (size > 200) {
/*
* Look halfway, and then back up a bit, because we
* expect it to take us about 20 characters to find
* the zero we look for, and an additional 20
* characters is the subsequent SHA1.
*/
unsigned long guess = size / 2 - 40;
/*
* This is the offset past which a zero means that
* we're good. If we don't find a zero in the first
* twenty bytes, that means that the first zero we
* find must be the beginning of a SHA1!
*/
unsigned long goal_zero = guess + 20;
for (;;) {
unsigned char c;
/*
* We need at least 22 characters more: the
* '\0' and the SHA1, and then the next entry.
* We know the ASCII mode is 4 characters, so
* we migth as well make the rule "within 26 of
* end end".
*/
if (guess >= size-26)
goto fall_back_to_linear_search;
c = start[guess++];
if (c)
continue;
/* Found it? */
if (guess > goal_zero)
break;
/*
* We found a zero that wasn't 20 bytes away,
* that means we have to reset out goal..
*/
last_zero = guess + 20;
}
/*
* "guess" now points to one past the '\0': the SHA1 of
* the previous entry. Add 20, and it points at the start
* of a valid tree entry.
*/
guess = guess + 20;
/* Length of the entry: ascii string + '\0' + SHA1 */
thisentrylen = strlen(start + guess) + 1 + 20;
.. compare the entry we found with
.. the entry we are looking for!
if (found < lookedfor) {
size = guess;
continue;
} else if (found == lookedfor) {
Yay! FOUND!
} else {
guess += thisentry;
size -= guess;
start += guess;
continue;
}
}
fall_back_to_linear_search:
.. linear search in [ start, size ] ..
Anyway, as you can tell, the above is totally untested, but I really think
it should work. Whether it really helps, I dunno. But if somebody is
interested in trying, it might be cool to see.
And yes, the "search for zero bytes" is not *guaranteed* to find any
beginning at all, if you have lots of short names, *and* lots of zero
bytes in the SHA1's. But while short names may be common, zero bytes in
SHA1's are not so much (since you should expect to see a very even
distribution of bytes, and as such most SHA1's by far should have no zero
bytes at all!)
So if you're really really *really* unlucky, you might end up having to
fall back on the linear search. But it still works!
Can anybody see anything wrong in my thinking above?
(And the real question is whether it really helps. I suspect it does
actually help for big directories, and that it is worth doing, but maybe
the magic number in "while (size > 200)" could be tweaked.
The logic of that was that binary searching doesn't work very well for
just a few entries (and "size < 200" implies ~5-6 directory entries), but
also that linear search is actually perfectly good when it's just a couple
of cache-lines, and binary searching - especially with the complication of
having to find the beginning - isn't worth it unless it really means that
we can avoid a cache miss.
Of course, it may well be that the *real* cost of the directories is just
the uncompression thing, and that the search is not the problem. Who
knows?
Linus
El 19/7/2007, a las 4:30, Johannes Schindelin escribió:
Commit notes are blobs which are shown together with the commit
message. These blobs are taken from the notes ref, which you can
configure by the config variable core.notesRef, which in turn can
be overridden by the environment variable GIT_NOTES_REF.
I was trying to look back and find out what the rationale/usage
scenario for these commit notes might be but Googling for 'git
"commit notes"' doesn't turn up much other than the original patch
you sent a few days ago.
Is this an evolution of the "git-note: A mechanisim for providing
free-form after-the-fact annotations on commits" first introduced here?:
<http://lists.zerezo.com/git/msg465441.html>
Cheers,
Wincent
From: Johannes Schindelin <hidden> Date: 2016-06-15 22:43:22
Hi,
On Thu, 19 Jul 2007, Wincent Colaiuta wrote:
El 19/7/2007, a las 4:30, Johannes Schindelin escribi?:
quoted
Commit notes are blobs which are shown together with the commit
message. These blobs are taken from the notes ref, which you can
configure by the config variable core.notesRef, which in turn can
be overridden by the environment variable GIT_NOTES_REF.
I was trying to look back and find out what the rationale/usage scenario for
these commit notes might be but Googling for 'git "commit notes"' doesn't
turn up much other than the original patch you sent a few days ago.
Is this an evolution of the "git-note: A mechanisim for providing free-form
after-the-fact annotations on commits" first introduced here?:
<http://lists.zerezo.com/git/msg465441.html>
Almost. It is an evolution of the evolution of this.
http://thread.gmane.org/gmane.comp.version-control.git/52598/focus=52603
(which started this thread you were replying to) hints at that, but you're
right, I failed to give an explicit reference:
http://article.gmane.org/gmane.comp.version-control.git/49588
Background: It was discussed how to go about storing notes (in the mail
you cited). I was convinced that Johan's 15-strong patch series was not
optimal, in that it tried to introduce a _second_ object store,
_exclusively_ for commit notes, with all kinds of problems like "how to
fetch it?".
After thinking about how to avoid duplicating the object store, I posted
my proposal, in the second link I gave.
It was shot down, because of scalability problems. They were not serious,
but hurt enough that I stalled working on it, until Alberto reminded me.
Since I felt bad about shooting down Johan's patch series, and then not
completing my alternative solution, I ended up working on it some more.
The WIP patch 6/6 hints at what I will submit in the next days, to speed
up in a transparent manner what would otherwise not scale well.
Ciao,
Dscho
From: Johannes Schindelin <hidden> Date: 2016-06-15 22:43:22
Hi,
On Wed, 18 Jul 2007, Linus Torvalds wrote:
On Thu, 19 Jul 2007, Johannes Schindelin wrote:
quoted
There is one severe shortcoming, though. Since tree objects can
contain file names of a variable length, it is not possible to do a
binary search for the correct base name in the tree object's contents.
Well, I've been thinking about this, and that's not really entirely
correct.
It *is* possible to do a binary search, it's just a bit complicated,
because you have to take the "halfway" thing, and find the beginning of
an entry.
I will try to work from your proposal, and do some timings. But for the
notes, I really, really like the average constant running time of the hash
map. As you can see from my timings in
http://thread.gmane.org/gmane.comp.version-control.git/52598/focus=52603
it does make a difference, compared to binary search.
Ciao,
Dscho
From: Sven Verdoolaege <hidden> Date: 2016-06-15 22:43:22
On Thu, Jul 19, 2007 at 03:30:43AM +0100, Johannes Schindelin wrote:
+If such a path 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 print.
On Wed, Jul 18, 2007 at 08:28:27PM -0700, Linus Torvalds wrote:
And yes, the "search for zero bytes" is not *guaranteed* to find any
beginning at all, if you have lots of short names, *and* lots of zero
bytes in the SHA1's. But while short names may be common, zero bytes in
SHA1's are not so much (since you should expect to see a very even
distribution of bytes, and as such most SHA1's by far should have no zero
bytes at all!)
The probability of a sha1 to have a zero is approximatively 0.075.
That's 1 in 13, more or less.
OG.
On Wed, Jul 18, 2007 at 08:28:27PM -0700, Linus Torvalds wrote:
quoted
And yes, the "search for zero bytes" is not *guaranteed* to find any
beginning at all, if you have lots of short names, *and* lots of zero
bytes in the SHA1's. But while short names may be common, zero bytes in
SHA1's are not so much (since you should expect to see a very even
distribution of bytes, and as such most SHA1's by far should have no zero
bytes at all!)
The probability of a sha1 to have a zero is approximatively 0.075.
That's 1 in 13, more or less.
Sure. And since we handle it fine even when it does happen, we don't care.
In fact, since we only need 20 non-zero bytes in between zeroes to know
that it's ok, and since the ASCII part is already 7 bytes of "mode +
space" plus <n> bytes of actual name (let's say that names average to be
about 8 characters - which is low: in the kernel it seems to be about 10.5
characters), we can say that the ASCII part of a tree tends to be about 15
characters.
So in order to be unlucky, it's not enough for the previous SHA1 to have a
NUL character in it, it actually has to be in the last five bytes of the
SHA1 - so now we're talking something like a 1:50 chance.
And with longer names, it matters even less (to the point where it
matters not at all if all filenames are >= 14 characters in length).
So we can be unlucky, but it's fairly rare, and when it happens, at worst
we'll just need to scan to the next entry (and if we're *really* unlucky
and it keeps happening until we scan until the end, we'll have to do the
linear search).
The point being that you always get the right answer, and the likelihood
that you have to do something slow to get that rigth answer is really
really low.
Linus