Something to play with so we can evaluate which is the best strategy
for non-full clone (or whatever you call it).
The idea is the same: pack only enough to access a subtree, rewrite
commits at client side, rewrite again when pushing. However I put
git-replace into the mix, so at least commit SHA-1 looks as same as from
upstream. git-subtree is not needed (although it's still an option)
With this, I can clone Documentaion/ from git.git, update and push. I
haven't tested it further. Space consumption is 24MB (58MB for full
repo). Not really impressive, but if one truely cares about disk
space, he/she should also use shallow clone.
Performance is impacted, due to bulk commit replacement. There is a
split second delay for every command. It's the price of replacing 24k
commits every time. I think the delay could be improved a little bit
(caching or mmap..)
Rewriting commits at clone takes time too. Doing individual object
writing takes lots of space and time. I put all new objects directly
to a pack now. Rewriting time now becomes quite acceptable (a few
seconds). Although deep subtree/repo may take longer. Rewriting on
demand can be considered in such cases.
Repo-care commands like fsck, repack, gc are left out for now.
Finally, it's more of a hack just to see how far I can go. It will
break things.
Nguyễn Thái Ngọc Duy (16):
Add core.subtree
list-objects: limit traversing within the given subtree if
core.subtree is set
parse_object: keep sha1 even when parsing replaced one
Allow to invalidate a commit in in-memory object store
Hook up replace-object to allow bulk commit replacement
upload-pack: use a separate variable to control whether internal
rev-list is used
upload-pack: support subtree pack
fetch-pack: support --subtree
subtree: rewrite incoming commits
clone: support subtree clone with parameter --subtree
pack-objects: add --subtree (for pushing)
subtree: rewriting outgoing commits
Update commit_tree() interface to take base tree too
commit_tree(): rewriting/replacing new commits
commit: rewrite outgoing commits
do not use thin packs and subtree together (just a bad feeling about
this)
Makefile | 2 +
builtin/clone.c | 10 +
builtin/commit-tree.c | 2 +-
builtin/commit.c | 4 +-
builtin/fetch-pack.c | 8 +
builtin/merge.c | 4 +-
builtin/notes.c | 2 +-
builtin/pack-objects.c | 4 +
builtin/send-pack.c | 2 +
cache.h | 1 +
commit.c | 25 +++-
commit.h | 4 +-
config.c | 3 +
environment.c | 2 +
list-objects.c | 23 ++-
notes-cache.c | 2 +-
object.c | 2 +-
replace_object.c | 5 +
subtree.c | 534 ++++++++++++++++++++++++++++++++++++++++++++++++
subtree.h | 4 +
upload-pack.c | 28 ++-
21 files changed, 651 insertions(+), 20 deletions(-)
create mode 100644 subtree.c
create mode 100644 subtree.h
This variable contains the subtree. With core_subtree being non-empty
behavior of git may be totally different.
Perhaps this should not stay in .git/config, rather .git/subtree
Signed-off-by: Nguyễn Thái Ngọc Duy <redacted>
---
cache.h | 1 +
config.c | 3 +++
environment.c | 2 ++
3 files changed, 6 insertions(+), 0 deletions(-)
@@ -595,6 +595,9 @@ static int git_default_core_config(const char *var, const char *value)return0;}+if(!strcmp(var,"core.subtree"))+returngit_config_string(&core_subtree,var,value);+/* Add other config variables here and to Documentation/config.txt. */return0;}
@@ -57,6 +57,8 @@ int core_apply_sparse_checkout;/* Parallel index stat data preload? */intcore_preload_index=0;+constchar*core_subtree;+/* This is set by setup_git_dir_gently() and/or git_default_config() */char*git_work_tree_cfg;staticchar*work_tree;
$GIT_DIR/subtree contains commit mapping in subtree mode. It's quite
large that putting it in $GIT_DIR/refs/replace may slow git down
significantly. Even with this, there will be a split second delay for
every git command.
Signed-off-by: Nguyễn Thái Ngọc Duy <redacted>
---
Makefile | 2 +
replace_object.c | 5 ++
subtree.c | 117 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
subtree.h | 2 +
4 files changed, 126 insertions(+), 0 deletions(-)
create mode 100644 subtree.c
create mode 100644 subtree.h
@@ -197,7 +198,7 @@ static void create_pack_file(void)die("git upload-pack: unable to fork git-pack-objects");/* pass on revisions we (don't) want */-if(!shallow_nr){+if(!internal_rev_list){FILE*pipe_fd=xfdopen(pack_objects.in,"w");if(!create_full_pack){inti;
@@ -311,7 +312,7 @@ static void create_pack_file(void)error("git upload-pack: git-pack-objects died with error.");gotofail;}-if(shallow_nr&&finish_async(&rev_list))+if(internal_rev_list&&finish_async(&rev_list))gotofail;/* error was already reported *//* flush the data */
With core_subtree turned on (capability "subtree", request "subtree"
from fetch-pack), traverse_commit_list will be in "subtree mode",
which will not go farther than the given subtree.
As the result, the pack is broken be design, only contains enough
blobs/trees/commits to reach the given subtree.
Signed-off-by: Nguyễn Thái Ngọc Duy <redacted>
---
upload-pack.c | 18 ++++++++++++++++--
1 files changed, 16 insertions(+), 2 deletions(-)
@@ -505,6 +505,20 @@ static void receive_needs(void)if(debug_fd)write_in_full(debug_fd,line,len);+if(!prefixcmp(line,"subtree ")){+intlen;+char*subtree;+if(core_subtree)+die("sorry, only one subtree supported");+len=strlen(line+8);+subtree=malloc(len+1);+memcpy(subtree,line+8,len-1);+subtree[len-1]='\0';/* \n */+if(subtree[len-2]!='/')+die("subtree request must end with a slash");+core_subtree=subtree;+continue;+}if(!prefixcmp(line,"shallow ")){unsignedcharsha1[20];structobject*object;
@@ -692,6 +694,8 @@ static struct ref *do_fetch_pack(int fd[2],if(is_repository_shallow()&&!server_supports("shallow"))die("Server does not support shallow clients");+if(core_subtree&&!server_supports("subtree"))+die("Server does not support subtree");if(server_supports("multi_ack_detailed")){if(args.verbose)fprintf(stderr,"Server supports multi_ack_detailed\n");
This adds the main function, subtree_import(), which is intended to be
used by "git clone".
Because subtree packs are not complete. They are barely usable. Git
client will cry out missing objects here and there... Theortically,
client code could be adapted to only look for objects within
subtree. That was painful to try.
Alternatively, subtree_import() rewrites commits to have only the
specified subtree, sealing all broken path. Git client now happily
works with these new commits.
However, users might not, because it's different commit, different
SHA-1. They can't use those SHA-1 to communicate within their team. To
work around this, all original commits are replaced by new commits
using git-replace.
Of course this is still not perfect. Users may be able to send SHA-1
around, which is consistent. They may not do the same with tree SHA-1.
Rewriting/replacing commits takes time and space. For replacing _all_
commits, the current replace mechanism is not suitable, which is why
subtree_lookup_object() was introduced in previous patches.
For rewriting, writing a huge number of objects is slow. So
subtree_import() builds a pack for all new objects. These packs are
not optimized. But it does reduce wait time for rewriting.
Signed-off-by: Nguyễn Thái Ngọc Duy <redacted>
---
subtree.c | 244 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
subtree.h | 1 +
2 files changed, 245 insertions(+), 0 deletions(-)
@@ -115,3 +115,247 @@ const unsigned char *subtree_lookup_object(const unsigned char *sha1)returnsubtree_commit[pos]->sha1[1];returnsha1;}++staticunsignedlongdo_compress(void**pptr,unsignedlongsize)+{+z_streamstream;+void*in,*out;+unsignedlongmaxsize;++memset(&stream,0,sizeof(stream));+deflateInit(&stream,Z_DEFAULT_COMPRESSION);+maxsize=deflateBound(&stream,size);++in=*pptr;+out=xmalloc(maxsize);+*pptr=out;++stream.next_in=in;+stream.avail_in=size;+stream.next_out=out;+stream.avail_out=maxsize;+while(deflate(&stream,Z_FINISH)==Z_OK)+;/* nothing */+deflateEnd(&stream);++returnstream.total_out;+}++staticintnr_written;+staticintadd_sha1_to_pack(intfd,void*buf,unsignedlongsize,enumobject_typetype)+{+unsignedlongdatalen;+unsignedhdrlen;+unsignedcharheader[10];++datalen=do_compress(&buf,size);+hdrlen=encode_in_pack_object_header(type,size,header);+write(fd,header,hdrlen);+write(fd,buf,datalen);+nr_written++;+free(buf);+return0;+}++/*+*Takesha1ofatree,rewriteittoonlyreturntheprefixandreturn+*thenewsha1.+*+*Ififiszero,writetoobjectstore.Iffdisgreaterthanzero,+*it'sapackfilehandle.+*/+staticintnarrow_tree(constunsignedchar*sha1,unsignedchar*newsha1,+constchar*prefix,intfd)+{+structtree_descdesc;+structname_entryentry;+structstrbufbuffer;+constchar*slash;+intsubtree_len;+enumobject_typetype;+unsignedlongsize;+char*tree;+structobject*obj;++slash=strchr(prefix,'/');+subtree_len=slash?slash-prefix:strlen(prefix);++tree=read_sha1_file(sha1,&type,&size);+if(type!=OBJ_TREE)+die("%s is not a tree",sha1_to_hex(sha1));++init_tree_desc(&desc,tree,size);+strbuf_init(&buffer,1024);+while(tree_entry(&desc,&entry)){+if(!S_ISDIR(entry.mode))+continue;++if(subtree_len==strlen(entry.path)&&+!strncmp(entry.path,prefix,subtree_len)){+unsignedcharnewtree_sha1[20];++if(slash&&slash[1])/* trailing slash does not count */+narrow_tree(entry.sha1,newtree_sha1,prefix+subtree_len+1,fd);+else+memcpy(newtree_sha1,entry.sha1,20);++strbuf_addf(&buffer,"%o %.*s%c",entry.mode,strlen(entry.path),entry.path,'\0');+strbuf_add(&buffer,newtree_sha1,20);+break;+}+}+free(tree);++if(fd==0){+if(write_sha1_file(buffer.buf,buffer.len,tree_type,newsha1)){+error("Could not write replaced tree for %s",sha1_to_hex(sha1));+strbuf_release(&buffer);+return1;+}+strbuf_release(&buffer);+return0;+}++hash_sha1_file(buffer.buf,buffer.len,tree_type,newsha1);+obj=(structobject*)lookup_tree(newsha1);+if(fd>0&&+!(obj->flags&SEEN)&&+add_sha1_to_pack(fd,buffer.buf,buffer.len,OBJ_TREE)){+error("Could not write replaced tree for %s",sha1_to_hex(sha1));+strbuf_release(&buffer);+return1;+}+obj->flags|=SEEN;++strbuf_release(&buffer);+return0;+}++/*+*Takesha1ofacommit,rewriteitstreeusingnarrow_tree(),then+*addareplaceentrytofilepointerfp(whichis$GIT_DIR/subtree).+*+*Alsoupdatereplace-objectdatabasesothatthegivensha1canbe+*replacedwiththenewonerightafterthisfunctionreturns.+*/+staticintshadow_commit(constunsignedchar*sha1,constchar*prefix,intfd,FILE*fp)+{+unsignedcharnewsha1[20],treesha1[20];+enumobject_typetype;+unsignedlongsize;+void*buffer;+structobject*obj;+intsaved_read_replace_refs=read_replace_refs;++read_replace_refs=0;+buffer=read_sha1_file(sha1,&type,&size);+read_replace_refs=saved_read_replace_refs;+get_sha1_hex(buffer+5,treesha1);++if(!buffer||type!=OBJ_COMMIT||+narrow_tree(treesha1,newsha1,prefix,fd)){+free(buffer);+error("Failed to narrow tree for commit %s",sha1_to_hex(sha1));+return1;+}++/* replace new tree in */+memcpy((char*)buffer+5,sha1_to_hex(newsha1),40);++if(fd==0){+if(write_sha1_file(buffer,size,commit_type,newsha1)){+free(buffer);+error("Could not write replaced commit for %s",sha1_to_hex(sha1));+return1;+}+}+else{+hash_sha1_file(buffer,size,commit_type,newsha1);+obj=(structobject*)lookup_commit(newsha1);+if(fd>0&&+!(obj->flags&SEEN)&&+add_sha1_to_pack(fd,buffer,size,OBJ_COMMIT)){+free(buffer);+error("Could not write replaced commit for %s",sha1_to_hex(sha1));+return1;+}+obj->flags|=SEEN;+}++if(fp){+charbuf[82];+memcpy(buf,sha1_to_hex(sha1),40);+buf[40]=' ';+memcpy(buf+41,sha1_to_hex(newsha1),40);+buf[81]='\n';+fwrite(buf,82,1,fp);+}+free(buffer);++return0;+}++/*+*Rewriteallreachablecommitsinrepousingshadow_commit().+*Writeoutthepackthatcontainsnewtree/commitobjects.+*/+voidsubtree_import()+{+constchar*args[]={"rev-list","--all",NULL};+structpack_headerhdr;+structprogress*ps;+structrev_inforevs;+structcommit*c;+unsignedcharsha1[20];+unsignedcommit_nr=0;+char*pack_tmp_name;+chartmpname[PATH_MAX];+intpack_fd,i;+FILE*fp;+charcmd[1024];++/* Packing */+init_revisions(&revs,NULL);+setup_revisions(2,args,&revs,NULL);+if(prepare_revision_walk(&revs))+die("revision walk setup failed");+fp=fopen(git_path("subtree"),"w+");++pack_fd=odb_mkstemp(tmpname,sizeof(tmpname),"pack/tmp_pack_XXXXXX");+pack_tmp_name=xstrdup(tmpname);++hdr.hdr_signature=htonl(PACK_SIGNATURE);+hdr.hdr_version=htonl(PACK_VERSION);+hdr.hdr_entries=htonl(0);+write(pack_fd,&hdr,sizeof(hdr));++ps=start_progress("Preparing subtree commits",0);+while((c=get_revision(&revs))!=NULL){+if(shadow_commit(c->object.sha1,core_subtree,pack_fd,fp))+die("Failed to shadow commit %s",c->object.sha1);+display_progress(ps,++commit_nr);+}+stop_progress(&ps);+fclose(fp);+fixup_pack_header_footer(pack_fd,sha1,pack_tmp_name,nr_written,NULL,0);+close(pack_fd);+sprintf(cmd,"git index-pack --stdin < %s",pack_tmp_name);+system(cmd);+unlink(pack_tmp_name);++reprepare_packed_git();+if(subtree_commit)+free(subtree_commit);+prepare_subtree_commit();++/* Invalidate all replaced commits */+for(i=0;i<subtree_commit_nr;i++){+/* lookup_commit() would create new objects, we don't want that */+c=(structcommit*)lookup_object(subtree_commit[i]->sha1[0]);+if(c)+invalidate_commit(c);+}++if(revs.pending.nr)+free(revs.pending.objects);+}
With all the preparation work, here comes --subtree. So clone away!
Signed-off-by: Nguyễn Thái Ngọc Duy <redacted>
---
builtin/clone.c | 10 ++++++++++
1 files changed, 10 insertions(+), 0 deletions(-)
@@ -78,6 +79,8 @@ static struct option builtin_clone_options[] = {"path to git-upload-pack on the remote"),OPT_STRING(0,"depth",&option_depth,"depth","create a shallow clone of that depth"),+OPT_STRING(0,"subtree",&core_subtree,"subtree",+"subtree clone"),OPT_END()};
@@ -515,6 +518,8 @@ int cmd_clone(int argc, const char **argv, const char *prefix)strbuf_reset(&value);if(path&&!is_bundle){+if(core_subtree)+die("Local subtree clone does not work (now)");refs=clone_local(path,git_dir);mapped_refs=wanted_peer_refs(refs,refspec);}else{
@@ -359,3 +359,176 @@ void subtree_import()if(revs.pending.nr)free(revs.pending.objects);}++/*+*Theoppositeofnarrow_tree().Putthesubtreebacktotheoriginaltree.+*/+staticintwiden_tree(constunsignedchar*sha1,+unsignedchar*newsha1,+constunsignedchar*subtree_sha1,+constchar*prefix)+{+structtree_descdesc;+structname_entryentry;+structstrbufbuffer;+constchar*slash;+intsubtree_len;+enumobject_typetype;+unsignedlongsize;+char*tree;++slash=strchr(prefix,'/');+subtree_len=slash?slash-prefix:strlen(prefix);++tree=read_sha1_file(sha1,&type,&size);+if(type!=OBJ_TREE)+die("%s is not a tree",sha1_to_hex(sha1));++init_tree_desc(&desc,tree,size);+strbuf_init(&buffer,8192);+while(tree_entry(&desc,&entry)){+strbuf_addf(&buffer,"%o %.*s%c",entry.mode,strlen(entry.path),entry.path,'\0');++if(S_ISDIR(entry.mode)&&+subtree_len==strlen(entry.path)&&+!strncmp(entry.path,prefix,subtree_len)){+unsignedcharnewtree_sha1[20];++if(slash&&slash[1])/* trailing slash does not count */+widen_tree(entry.sha1,newtree_sha1,subtree_sha1,+prefix+subtree_len+1);+else+/* replace the tree */+memcpy(newtree_sha1,subtree_sha1,20);++strbuf_add(&buffer,newtree_sha1,20);+}+else+strbuf_add(&buffer,entry.sha1,20);+}+free(tree);++if(write_sha1_file(buffer.buf,buffer.len,tree_type,newsha1)){+error("Could not write replaced tree for %s",sha1_to_hex(sha1));+strbuf_release(&buffer);+return1;+}+strbuf_release(&buffer);+return0;+}++staticintfind_subtree(constunsignedchar*sha1,unsignedchar*newsha1,constchar*prefix)+{+structtree_descdesc;+structname_entryentry;+constchar*slash;+enumobject_typetype;+unsignedlongsize;+intsubtree_len;+char*tree;++slash=strchr(prefix,'/');+subtree_len=slash?slash-prefix:strlen(prefix);++tree=read_sha1_file(sha1,&type,&size);+if(type!=OBJ_TREE)+die("%s is not a tree",sha1_to_hex(sha1));++init_tree_desc(&desc,tree,size);+while(tree_entry(&desc,&entry)){+if(!S_ISDIR(entry.mode))+continue;++if(subtree_len==strlen(entry.path)&&+!strncmp(entry.path,prefix,subtree_len)){++if(slash&&slash[1]){/* trailing slash does not count */+if(find_subtree(entry.sha1,newsha1,prefix+subtree_len+1))+return1;+}+else+memcpy(newsha1,entry.sha1,20);+free(tree);+return0;+}+}+free(tree);++return1;+}++/* The opposite of shadow_commit() */+staticintexpose_commit(constunsignedchar*sha1,unsignedchar*newsha1,+constunsignedchar*basesha1,+constchar*prefix,FILE*fp)+{+unsignedchartreesha1[20],subtree_sha1[20];+enumobject_typetype;+unsignedlongsize,base_size;+void*base_buffer,*buffer;+intsaved_read_replace_refs=read_replace_refs;++/* Get subtree from the new commit, sha1 */+read_replace_refs=0;+buffer=read_sha1_file(sha1,&type,&size);+read_replace_refs=saved_read_replace_refs;+get_sha1_hex(buffer+5,treesha1);++if(!buffer||type!=OBJ_COMMIT||+find_subtree(treesha1,subtree_sha1,prefix)){+free(buffer);+error("Failed to find subtree tree in base commit %s",sha1_to_hex(sha1));+return1;+}++/* Get the old base tree from basesha1 */+read_replace_refs=0;+base_buffer=read_sha1_file(basesha1,&type,&base_size);+read_replace_refs=saved_read_replace_refs;+get_sha1_hex(base_buffer+5,treesha1);++if(!buffer||type!=OBJ_COMMIT||+widen_tree(treesha1,newsha1,subtree_sha1,prefix)){+free(buffer);+error("Failed to widen tree for commit %s",sha1_to_hex(sha1));+return1;+}+free(base_buffer);++/* replace new tree in */+memcpy((char*)buffer+5,sha1_to_hex(newsha1),40);++if(write_sha1_file(buffer,size,commit_type,newsha1)){+free(buffer);+error("Could not write replaced commit for %s",sha1_to_hex(sha1));+return1;+}++if(fp){+charbuf[82];+memcpy(buf,sha1_to_hex(newsha1),40);+buf[40]=' ';+memcpy(buf+41,sha1_to_hex(sha1),40);+buf[81]='\n';+fwrite(buf,82,1,fp);+}+free(buffer);++return0;+}++intsubtree_export(unsignedchar*sha1,unsignedchar*basesha1,unsignedchar*newsha1)+{+FILE*fp;++fp=fopen(git_path("subtree"),"a+");+if(expose_commit(sha1,newsha1,basesha1,core_subtree,fp))+die("Failed to rewrite commit %s",sha1_to_hex(sha1));+fclose(fp);++if(subtree_commit)+free(subtree_commit);+prepare_subtree_commit();++return0;+}
In subtree mode, you work on a narrowed trees. You make narrowed
commits. If you want to push upstream, you would need to put your
updated subtree back to the full tree again. Otherwise upstream would
complain you delete all trees but your subtree, not good.
In order to do that, commit_tree() now takes the base tree SHA-1. With
that, it can create upstream-compatible commits. It does not now,
though.
Signed-off-by: Nguyễn Thái Ngọc Duy <redacted>
---
builtin/commit-tree.c | 2 +-
builtin/commit.c | 2 +-
builtin/merge.c | 4 ++--
builtin/notes.c | 2 +-
commit.c | 2 +-
commit.h | 2 +-
notes-cache.c | 2 +-
7 files changed, 8 insertions(+), 8 deletions(-)
@@ -779,7 +779,7 @@ static int merge_trivial(void)parent->next=xmalloc(sizeof(*parent->next));parent->next->item=remoteheads->item;parent->next->next=NULL;-commit_tree(merge_msg.buf,result_tree,parent,result_commit,NULL);+commit_tree(merge_msg.buf,result_tree,NULL,parent,result_commit,NULL);finish(result_commit,"In-index merge");drop_save();return0;
@@ -808,7 +808,7 @@ static int finish_automerge(struct commit_list *common,}free_commit_list(remoteheads);strbuf_addch(&merge_msg,'\n');-commit_tree(merge_msg.buf,result_tree,parents,result_commit,NULL);+commit_tree(merge_msg.buf,result_tree,NULL,parents,result_commit,NULL);strbuf_addf(&buf,"Merge made by %s.",wt_strategy);finish(result_commit,buf.buf);strbuf_release(&buf);
@@ -303,7 +303,7 @@ int commit_notes(struct notes_tree *t, const char *msg)hashclr(prev_commit);parent=NULL;}-if(commit_tree(buf.buf+7,tree_sha1,parent,new_commit,NULL))+if(commit_tree(buf.buf+7,tree_sha1,NULL,parent,new_commit,NULL))die("Failed to commit notes tree to database");/* Update notes ref with new commit */
@@ -811,7 +811,7 @@ static const char commit_utf8_warn[] ="You may want to amend it after fixing the message, or set the config\n""variable i18n.commitencoding to the encoding your project uses.\n";-intcommit_tree(constchar*msg,unsignedchar*tree,+intcommit_tree(constchar*msg,unsignedchar*tree,unsignedchar*base_tree,structcommit_list*parents,unsignedchar*ret,constchar*author){
Only one subdirectory allowed? What if someone wants a sparse clone
containing two or more directories? (Actually, that's not so much of
a "what if" -- it's exactly what I want in about half my usecases for
sparse clones.)
ssize_t sz;
const char *argv[10];
int arg = 0;
+ int internal_rev_list = shallow_nr;
<snip>
I've got the exact same changes in one of my in-progress-patches in my
sparse-clone branch. That is, other than the variable name, but I
like yours better. Needless to say, I agree with this change. :-)
if (debug_fd)
write_in_full(debug_fd, line, len);
+ if (!prefixcmp(line, "subtree ")) {
+ int len;
+ char *subtree;
+ if (core_subtree)
+ die("sorry, only one subtree supported");
I'm not sure users would understand this error message; perhaps
something more like "Fetching/cloning from a subtree-sparse repository
not supported"?
This adds the main function, subtree_import(), which is intended to be
used by "git clone".
Because subtree packs are not complete. They are barely usable. Git
client will cry out missing objects here and there... Theortically,
client code could be adapted to only look for objects within
subtree. That was painful to try.
It may have been painful, but personally I think it's still the right
way to do it. Of course, that's a pretty easy thing for me to say,
since you're pretty far ahead of me and I haven't felt your pain yet.
Maybe I'll change my mind after trying it for a while, but I'm not
convinced just yet.
+/*
+ * Take sha1 of a tree, rewrite it to only return the prefix and return
+ * the newsha1.
+ *
+ * If if is zero, write to object store. If fd is greater than zero,
+ * it's a pack file handle.
Should the second word of the second paragraph be 'fd' rather than another 'if'?
Again, gcc here complains that "subtree.c:390: warning: field
precision should have type ‘int’, but argument 4 has type ‘size_t’" --
typecast the return value of strlen to int?
Something to play with so we can evaluate which is the best strategy
for non-full clone (or whatever you call it).
Very nice, it's awesome you're working on this. I'm of the same
opinion that Shawn stated earlier, namely that I don't like the route
of rewriting commits on the fly like this (more on that later), but
it's really cool to see some ideas being tried and pushed to their
limits.
The idea is the same: pack only enough to access a subtree, rewrite
commits at client side, rewrite again when pushing. However I put
git-replace into the mix, so at least commit SHA-1 looks as same as from
upstream. git-subtree is not needed (although it's still an option)
With this, I can clone Documentaion/ from git.git, update and push. I
I tried it out, but I seem to be doing something wrong. I applied
your patches to current master, and tried the following -- am I doing
something wrong or omitting any important steps?
$ git --version
git version 1.7.2.1.22.g236df
$ git clone file://$(pwd)/git fullclone
Cloning into fullclone...
warning: templates not found /home/newren/share/git-core/templates
remote: Counting objects: 96220, done.
remote: Compressing objects: 100% (24925/24925), done.
remote: Total 96220 (delta 70575), reused 95687 (delta 70236)
Receiving objects: 100% (96220/96220), 18.45 MiB | 11.43 MiB/s, done.
Resolving deltas: 100% (70575/70575), done.
fatal: unable to read tree 49374ea4780c0db6db7c604697194bc9b148f3dc
$ git clone --subtree=Documentation/ file://$(pwd)/git docclone
Cloning into docclone...
warning: templates not found /home/newren/share/git-core/templates
fatal: The remote end hung up unexpectedly
fatal: early EOF
fatal: index-pack failed
haven't tested it further. Space consumption is 24MB (58MB for full
repo). Not really impressive, but if one truely cares about disk
space, he/she should also use shallow clone.
58 MB for full repo? What are you counting? For me, I get 25M:
$ git clone git://git.kernel.org/pub/scm/git/git.git
$ ls -lh git/.git/objects/pack/*.pack
-r--r--r--. 1 newren newren 25M 2010-08-01 18:05
git/.git/objects/pack/pack-d41d36a8f0f34d5bc647b3c83c5d6b64fbc059c8.pack
Are you counting the full checkout too or something? If so, that
varies very wildly between systems, making it hard to compare numbers.
(For me, 'du -hs git/' returns 44 MB.) I'd like to be able to
duplicate your numbers and investigate further. It seems to me that
we ought to be able to get that lower.
Performance is impacted, due to bulk commit replacement. There is a
split second delay for every command. It's the price of replacing 24k
commits every time. I think the delay could be improved a little bit
(caching or mmap..)
Rewriting commits at clone takes time too. Doing individual object
writing takes lots of space and time. I put all new objects directly
to a pack now. Rewriting time now becomes quite acceptable (a few
seconds). Although deep subtree/repo may take longer. Rewriting on
demand can be considered in such cases.
Repo-care commands like fsck, repack, gc are left out for now.
Finally, it's more of a hack just to see how far I can go. It will
break things.
I think it's a pretty nifty hack. It's fun to see. :-) However, I
do have a number of reservations about the general strategy: As
mentioned earlier, I'm not sure I like the on-the-fly commit
rewriting, as mentioned by Shawn in your previous
subtree-for-upload-pack patch series. You did take care of the
"referring to commit-sha1" issue he brought up by using the replace
mechanism, but I'm still not sure I'm comfortable with it. The
performance implications also worry me (a lot of the reason for sparse
clones was to improve performance, at least from my view), as does the
fact that it only works on exactly one subtree (at least your current
implementation; most of my usecases involve multiple sibling
subdirectories that I'd like to get), as does the fact that it
(currently) only handles trees and does not handle files (ruling out
the translator usecase I'd like to see covered, e.g. cloning just
po/de.po and its history without all sibling files).
Also, I couldn't tell if your implementation downloaded full commit
information for commits that didn't touch any of the files under the
relevant subtree. I think it does, but couldn't tell for sure (I
wanted to use a clone and dig into it to find out, but ran into the
problems I mentioned above). If so, that also worries me a bit -- see
http://article.gmane.org/gmane.comp.version-control.git/152343.
Your implementation also suffers from the same limitations as current
shallow clones. For example, you can't clone or fetch from a subtree
clone. That limits collaboration between people needing to work on
the same subset of history, and was a limitation I was hoping to see
fixed, rather than propagated to more features.
I hope I'm not coming across as too critical. I'm really excited to
see work in this area. Hopefully I can get more time to pursue my
route a bit further; currently I don't have too much more than a
detailed idea write-up (heavily revised since the previous thread --
thanks for the feedback, btw). Or maybe you just know how to address
all my concerns and you beat me to the punch. That'd be awesome.
Elijah
Only one subdirectory allowed? What if someone wants a sparse clone
containing two or more directories? (Actually, that's not so much of
a "what if" -- it's exactly what I want in about half my usecases for
sparse clones.)
One is simpler. So one first, multiple may come later.
The idea is the same: pack only enough to access a subtree, rewrite
commits at client side, rewrite again when pushing. However I put
git-replace into the mix, so at least commit SHA-1 looks as same as from
upstream. git-subtree is not needed (although it's still an option)
With this, I can clone Documentaion/ from git.git, update and push. I
I tried it out, but I seem to be doing something wrong. I applied
your patches to current master, and tried the following -- am I doing
something wrong or omitting any important steps?
$ git --version
git version 1.7.2.1.22.g236df
$ git clone file://$(pwd)/git fullclone
Cloning into fullclone...
warning: templates not found /home/newren/share/git-core/templates
remote: Counting objects: 96220, done.
remote: Compressing objects: 100% (24925/24925), done.
remote: Total 96220 (delta 70575), reused 95687 (delta 70236)
Receiving objects: 100% (96220/96220), 18.45 MiB | 11.43 MiB/s, done.
Resolving deltas: 100% (70575/70575), done.
fatal: unable to read tree 49374ea4780c0db6db7c604697194bc9b148f3dc
This one looks like the unintialized case you pointed out in
process_tree(). No I did not try full clone on my patched git :-P
$ git clone --subtree=Documentation/ file://$(pwd)/git docclone
Cloning into docclone...
warning: templates not found /home/newren/share/git-core/templates
fatal: The remote end hung up unexpectedly
fatal: early EOF
fatal: index-pack failed
Not sure. Does file:// use receive-pack/upload-pack? I tested it over
local ssh. Will try again soon.
quoted
haven't tested it further. Space consumption is 24MB (58MB for full
repo). Not really impressive, but if one truely cares about disk
space, he/she should also use shallow clone.
58 MB for full repo? What are you counting? For me, I get 25M:
$ git clone git://git.kernel.org/pub/scm/git/git.git
$ ls -lh git/.git/objects/pack/*.pack
-r--r--r--. 1 newren newren 25M 2010-08-01 18:05
git/.git/objects/pack/pack-d41d36a8f0f34d5bc647b3c83c5d6b64fbc059c8.pack
Are you counting the full checkout too or something? If so, that
varies very wildly between systems, making it hard to compare numbers.
(For me, 'du -hs git/' returns 44 MB.) I'd like to be able to
duplicate your numbers and investigate further. It seems to me that
we ought to be able to get that lower.
It's my git.git, probably has more topic branches plus junk stuff. If
you are only interested in numbers, playing with git pack-objects is
enough. You need changes in list-objects.c and builtin/pack-objects.c,
then you can
git pack-objects --stdout --subtree=foo/ > temp.pack
and examine it with verify-pack.
quoted
Finally, it's more of a hack just to see how far I can go. It will
break things.
I think it's a pretty nifty hack. It's fun to see. :-) However, I
do have a number of reservations about the general strategy: As
mentioned earlier, I'm not sure I like the on-the-fly commit
rewriting, as mentioned by Shawn in your previous
subtree-for-upload-pack patch series. You did take care of the
"referring to commit-sha1" issue he brought up by using the replace
mechanism, but I'm still not sure I'm comfortable with it. The
performance implications also worry me (a lot of the reason for sparse
clones was to improve performance, at least from my view), as does the
fact that it only works on exactly one subtree (at least your current
implementation; most of my usecases involve multiple sibling
subdirectories that I'd like to get), as does the fact that it
(currently) only handles trees and does not handle files (ruling out
the translator usecase I'd like to see covered, e.g. cloning just
po/de.po and its history without all sibling files).
And it's also fun to try. I'd like to try it on larger repos but I
have quite limited network until October.
Also, I couldn't tell if your implementation downloaded full commit
information for commits that didn't touch any of the files under the
relevant subtree. I think it does, but couldn't tell for sure (I
wanted to use a clone and dig into it to find out, but ran into the
problems I mentioned above). If so, that also worries me a bit -- see
http://article.gmane.org/gmane.comp.version-control.git/152343.
It does. Yes, that's also something to think of.
Your implementation also suffers from the same limitations as current
shallow clones. For example, you can't clone or fetch from a subtree
clone. That limits collaboration between people needing to work on
the same subset of history, and was a limitation I was hoping to see
fixed, rather than propagated to more features.
I agree. Being able to fetch from an incomplete repo is very nice.
Though I admit I don't know how to do it. I think sparse clone would
suffer the same, wouldn't it?
I hope I'm not coming across as too critical. I'm really excited to
see work in this area. Hopefully I can get more time to pursue my
route a bit further; currently I don't have too much more than a
detailed idea write-up (heavily revised since the previous thread --
thanks for the feedback, btw). Or maybe you just know how to address
all my concerns and you beat me to the punch. That'd be awesome.
Look forward to see sparse clone realized. Although I think that would
be painful :-)
--
Duy
haven't tested it further. Space consumption is 24MB (58MB for full
repo). Not really impressive, but if one truely cares about disk
space, he/she should also use shallow clone.
58 MB for full repo? What are you counting? For me, I get 25M:
My number 24MB was incorrect because process_tree() leaked too many
blobs. It should have been 16MB. Anyway I have updated my series and
put it here (to spam git mailing less)
http://repo.or.cz/w/git/pclouds.git/shortlog/refs/heads/subtree
(caveat: constantly rebased tree)
if you still want to play with it. For number lovers, fetching only
Documentation from linux-2.6.git took 94MB (full repo 366MB). Yeah
Documentation was an easy target.
--
Duy