-
Notifications
You must be signed in to change notification settings - Fork 2.5k
Shallow support #4331
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Shallow support #4331
Conversation
I'll keep working on handling protocol part (eg. |
b1b91e4
to
ccaa705
Compare
Just realized I should ping #3058 so we don't get duplicated work. Also, I have a local branch with the beginning of the protocol support which I'll happily publish if someone plans to work on that. |
Is there any progress on this merge? Looking to see a fix for shallow clones! |
38d217d
to
a9d8e11
Compare
I'd appreciate a quick technical "looks good" review, since actually shallow support at the protocol level will depend on some of the infrastructure I laid out here — except I'll need an atomic way of updating the shallow file which wasn't needed by the local-only fix. So, here are the questions :
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So first the usual apology that we've taken so long to review. I now finally got some time to read up to commit c54ea4c. For all my comments, keep in mind I'm not familiar with the grafting/replace mechanism.
So one thing I'm a bit confused about is how grafting and replace work with each other or against each other. As far as I understand it, a graft is a new commit with the exact same content as another commit, but with parents replaced. This allows you to create for example shallow histories by just replacing a cutoff-point commit by a graft with zero commits. The thing to note here is that this graft is part of the ODB, that is it is a commit object part of the ODB with its own OID.
Afterwards, there is the replace-mechanism coming into the picture. This is a refdb-level mechanism in that it creates references in refs/replace/<OID>
, where each OID in that namespace points directly at another OID. The meaning is then to replace the OID of the reference's name with the OID that is being pointed to.
A common usecase is then to use grafts for those replaces. Given a commit c
with cid=OID(c)
, you simply first create a graft commit g
with gid=OID(g)
. It holds that CONTENTS(c)=CONTENTS(g)
, where contents is the root tree, commit message etc. except for the parents. You then create a ref refs/replace/$cid
with contents $gid
to put that graft into place.
If I'm not mistaken, you are replacing the old logic used by grafts, which weren't using the new replacement logic. Was this a knowing decision, and if so why?
One error I can see is in how you create the grafts. Instead of creating new graft commits which have the same contents but different parents and inserting them into the ODB, you create them ad-hoc and just replace existing commit's parents if they're grafted. I think this is wrong. Especially the replace mechanism makes use of the graft being present in the ODB, so your implementation does not allow for that usecase.
So what I'd have expected is a function git_odb__create_graft(&graft, odb, commit)
which creates a new grafting object. I'm not yet decided if the ODB is the right place to put it or whether that should be somewhere else, but we definitly need such a function. After the user has put the graft (which is simply a git_commit
) into the ODB, you would then have an API in place to enable the user to put those grafts into place with the refdb. I guess this should be an API of the repository, as we need to access both the refdb to create the replace-reference as well as the ODB to locate the grafting and grafted commits. Something like git_repository__replace
, but I'm also undecided on that one.
Please let me know if you think my reasoning makes any sense. Even so, I'm glad somebody dared to tackle that topic! It's been on my own todo list for too long, so please keep up that good work! 👍
src/object.c
Outdated
@@ -51,14 +51,39 @@ static git_object_def git_objects_table[] = { | |||
{ "REF_DELTA", 0, NULL, NULL }, | |||
}; | |||
|
|||
int git_object__init_with_odb(git_object **object_out, git_odb_object *odb_obj, git_repository *repo) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would really love some explanation inside of the commit message detailing why exactly you do this step :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would have loved too 😆. IIRC it's a side-effect of the parse merging. After a more thorough look, it might just be confusion : the revwalk-parsing works on git_commit
, and the normal parsing did it, so I made them both do it. Will investigate if it's really necessary (I remember a crash when freeing though).
src/commit.c
Outdated
@@ -476,6 +496,17 @@ int git_commit__parse(void *_commit, git_odb_object *odb_obj) | |||
return -1; | |||
} | |||
|
|||
int git_commit__parse(void *_commit, git_odb_object *odb_obj) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, so it seems you created the new function to be able to parse commits from raw data. This looks a lot like what I have been doing in PR #4374, but I went even further and actually decoupled the commits completely from the ODB. Seeing that your approach wouldn't solve my particular use case (as I cannot use the ODB to parse the object), would my approach be able to solve your use case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As I think you saw below, this is all in the name of merging the parsing done when quickly revwalk with the "standard" way. After a quick look, I don't think your approach allows me to do that (it's just decoupling IIUC).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, you're right. I tend to review by commits, so I didn't see this until later.
GIT_COMMIT_PARSE_RAW_HEADER = (1 << 5), | ||
GIT_COMMIT_PARSE_RAW_MESSAGE = (1 << 6), | ||
GIT_COMMIT_PARSE_ALL = 0xFFFF, | ||
} git_commit__parse_flags; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the purpose of those flags? Is it an optimization or is it that you cannot parse some stuff because of certain conditions? If it's for the purpose of optimization, I'd like to avoid doing that. We currently don't have any notion of "partial" objects, and I bet this could theoretically lead to subtle bugs when passing around those partial objects.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Idem, revwalk parse merge.
src/commit_list.c
Outdated
buffer = parents_start; | ||
for (i = 0; i < parents; ++i) { | ||
git_oid oid; | ||
if ((error = git_commit__parse_commit(commit, obj, GIT_COMMIT_PARSE_PARENTS|GIT_COMMIT_PARSE_COMMITTER)) < 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I guess that answers my question. Nice to see some reduction in duplicated code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
src/commit.c
Outdated
/* Perform necessary grafts */ | ||
git_repository_odb__weakptr(&odb, repo); | ||
if (odb) | ||
git_odb__graft_object(commit, odb); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Huh? Either it is GitHub which confused the commit ordering or this commit is too early in that it introduces using git_odb__graft_object
previous to it being implemented.
src/repository.c
Outdated
{ | ||
git_buf odb_path = GIT_BUF_INIT; | ||
git_odb *odb; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not your fault, but god is that diff awful to read.
src/repository.c
Outdated
(error = git_odb__add_default_backends(odb, odb_path.ptr, 0, 0)) < 0) { | ||
git_buf_free(&odb_path); | ||
git_odb_free(odb); | ||
return error; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A goto out with common cleanup would be nice here.
src/repository.c
Outdated
assert(repo && out); | ||
if ((error = git_repository_item_path(&odb_path, repo, | ||
GIT_REPOSITORY_ITEM_OBJECTS)) < 0 || | ||
(error = git_odb_new(&odb)) < 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know you're just using the preexisting code, but we actually have a memory leak here whenever git_odb_new
errors. I mean in that case we probably have other problems because that indicates memory allocation errors, but still. Would be cool if you could refactor this a bit to not have multiple statements in a single if-condition and use goto cleanup
-style to avoid those memory leaks.
src/buffer.h
Outdated
@@ -215,4 +215,8 @@ int git_buf_splice( | |||
const char *data, | |||
size_t nb_to_insert); | |||
|
|||
#define git_buf_foreach_line(line_start, line_end, line_num, buf) \ | |||
while ((line_start = git__strsep(buf.ptr, "\n")) != NULL && \ | |||
(line_end = line_start + strlen(line_start)) != NULL && ++line_num) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please indent those lines part of the macro
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Furthermore, please wrap all uses of the macro parameters in braces.
src/repository.c
Outdated
return error; | ||
|
||
if (git_buf_joinpath(&graft_path, graft_path.ptr, "grafts")) { | ||
git_buf_free(&graft_path); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe have a common goto cleanup
again? It's becoming hard to grasp what needs cleaning at which part of the code with those interspersed free
s and return
s.
I'll repeat what I said on Slack for the record : since I'm coming at this from the shallow-support angle, which This PR only exists because of shallow-support, hence I've checked how
"replaces" are real objects and AFAIU work like you expected. "grafts" are just some OIDs loaded from
…thus, no. "grafts" have no real existence (IOW object form). I located my graft function at the ODB level because 1) I got confused thinking that
"replacing" I don't understand. If you meant "implementing", then yes, because that's how We seem to agree on how I'd be happy to discuss what final API we want : for shallow support, I only need "internal" things, so I don't particularly care (I mean, I could not provide any public API apart from the list of shallow roots and be done with it 😉). Right now, that's handled by I'll reinvestigate the layering I chose : because of my 2 points above, I might not need to go near the odb at all, but I have to consider a If it's warranted I can split out that PR in 3 pieces (already have them locally, and if it fixes GitHub's confusion that's be even better) : the commit parser merging (because of #4374 and being generally cleanup), the graft support itself (this), and shallow support (which requires this). |
src/commit.c
Outdated
@@ -476,6 +496,17 @@ int git_commit__parse(void *_commit, git_odb_object *odb_obj) | |||
return -1; | |||
} | |||
|
|||
int git_commit__parse(void *_commit, git_odb_object *odb_obj) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As I think you saw below, this is all in the name of merging the parsing done when quickly revwalk with the "standard" way. After a quick look, I don't think your approach allows me to do that (it's just decoupling IIUC).
GIT_COMMIT_PARSE_RAW_HEADER = (1 << 5), | ||
GIT_COMMIT_PARSE_RAW_MESSAGE = (1 << 6), | ||
GIT_COMMIT_PARSE_ALL = 0xFFFF, | ||
} git_commit__parse_flags; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Idem, revwalk parse merge.
src/commit_list.c
Outdated
buffer = parents_start; | ||
for (i = 0; i < parents; ++i) { | ||
git_oid oid; | ||
if ((error = git_commit__parse_commit(commit, obj, GIT_COMMIT_PARSE_PARENTS|GIT_COMMIT_PARSE_COMMITTER)) < 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
src/odb.c
Outdated
return 0; | ||
} | ||
|
||
int git_odb__graft_object(void *obj, git_odb *odb) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was expecting a "layering violation", because of git_odb_object
. Also, I think I saw a precedent, hence thought it would be fine. If it's not, it should be changed to git_commit *
because you only graft onto commits.
src/odb.c
Outdated
GITERR_CHECK_ALLOC(id); | ||
|
||
git_oid_cpy(id, oid); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's the cgit
graft approach, replacing parents on object parse.
src/object.c
Outdated
@@ -51,14 +51,39 @@ static git_object_def git_objects_table[] = { | |||
{ "REF_DELTA", 0, NULL, NULL }, | |||
}; | |||
|
|||
int git_object__init_with_odb(git_object **object_out, git_odb_object *odb_obj, git_repository *repo) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would have loved too 😆. IIRC it's a side-effect of the parse merging. After a more thorough look, it might just be confusion : the revwalk-parsing works on git_commit
, and the normal parsing did it, so I made them both do it. Will investigate if it's really necessary (I remember a crash when freeing though).
src/commit_list.c
Outdated
@@ -196,8 +158,7 @@ int git_commit_list_parse(git_revwalk *walk, git_commit_list_node *commit) | |||
} else | |||
error = commit_quick_parse( | |||
walk, commit, | |||
(const uint8_t *)git_odb_object_data(obj), | |||
git_odb_object_size(obj)); | |||
obj); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indent
src/commit_list.c
Outdated
const uint8_t *buffer, | ||
size_t buffer_len) | ||
git_commit_list_node *node, | ||
git_odb_object *obj) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indent
9dc2027
to
d6198bf
Compare
Rebased. I managed to remove most of the "weirdness" by moving the grafts at the repo level. |
size_t i; | ||
git_oid *parent_oid; | ||
git_commit *commit = git__calloc(1, git_odb_object_size(obj)); | ||
commit->object.repo = walk->repo; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note : that was what my old __init_from_odb
function was about. Right now I'm cheating by providing only what's necessary for the parser to work (access to the repo for applying the grafts). I can either bring back the __init_from_odb
function, or make the parser not crash if there's no repo around. Suggestions welcome !
src/commit.h
Outdated
GIT_COMMIT_PARSE_ALL = (1 << 1), | ||
} git_commit__parse_flags; | ||
|
||
int git_commit__parse_commit(git_commit *commit, git_odb_object *obj, git_commit__parse_flags flags); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've cut down on the number of flags here because of a bug parsing parents. I think it's better this way, as it makes it less likely you ask for an invalid combination, and we can always split those apart if the need arises.
Okay, so I think I've been confused a bit by grafts and replaces. What makes me curious is the following part from git-replace(1):
This paragraph implies that the grafted commit is stored inside of the ODB, as otherwise it would be impossible to put the reference into place. From what you say, it looks to me like there's two different things here:
So I guess both grafting and replaces use the same underlying mechanism of grafting, but differ only in how they are being referenced and persisted. While old-style grafts make use of out-of-band information, the new replace mechanism is simpler by making use of the ODB and refdb. It would be nice though if we could also check how git.git does it, instead of relying on another third-party implementation. Anyway. Seeing that both replaces and grafts make use of the same mechanisms, it would be nice if they shared some common logic with each other later on. |
Your interpretation of What I'm sure of is that I think that because of that specific point, both "object replacement methods" cannot share the same implementation, but I'll have to investigate how git-replace(1) actually works and what kind of functionality it uses to be sure. |
Okay, that's most welcome, thanks a lot. By the way, I'm all in favor of splitting this up, as you proposed. Especially if you've already got the parts ready it's always easier to get smaller batches reviewed and merged than one big batch. |
d6198bf
to
6f3e6f9
Compare
@pks-t Okay, I've split the 3 logical parts off. Sadly I can't really base them on top of one another because they're in my fork (and opening PRs against that would be silly), so it's not as clean as we might have wanted. One thing I'd like to discuss still is how we report those shallow objects. Right now, this is completely "transparent" (as per spec, may I say), but you, as a user, would have no way of knowing whether this is a "pristine commit" or a shallow one, without suspiciously checking the OID of every "root" commit against the list of shallow roots. For example, in GitX, I'd be able to open a prompt asking whether the user wants a few more commits loaded when revwalking (or something). Arguably, I might be overthinking it though 😉. |
6f3e6f9
to
3d9668e
Compare
d7d149e
to
fb472e9
Compare
fb472e9
to
220610f
Compare
e39e357
to
27ed7ce
Compare
38e0e57
to
cf8a290
Compare
This full PR/patch set has been rebased, and should be ready for review. I'm drawing a line in the sand as to what this is about: this only makes detection of shallow repositories possible. I'll do another PR about the "protocol" parts of shallow when I feel it's ready, because it's (arguably) harder. Hence, some safety measures are missing: I think a fetch done by us from a shallow repo would unshallow it, since we can't tell the remote about what we're purposefully not interested in (though it could also confuse the server 😬 ?). Same for pushing, though I'm clueless about what's involved there. It might be worthwhile to prevent misbehaving by erroring when those operations are tried on a shallow repository. My point about checking for a grafted commits when a revwalk ends is also still undecided. I like "erroring differently" better, but I understand that might be considered an API-break. Maybe |
This allows us to pick which data from a commit we're interested in. This will be used by the revwalk code, which is only interested in parents' and committer data.
This represents (old-style) grafted commits, a.k.a an array of overridden parents for a commit's OID.
This wires git_repository to open the .git/info/grafts file and load its contents as git_commit_grafts objects.
cf8a290
to
a165a6f
Compare
What's the status of this? |
Is there any progress on this problem? There are lots of issues that are demand on this feature. |
Ping @pks-t. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So. I do think that by now, this really is a very important feature and the one that's most commonly requested by users of libgit2, which is why I've decided to finally take the bitter pill and review this. But I'm pleasantly surprised, this really is a lot more straight forward than I initially expected. Good job, @tiennou!
Please don't let yourself be discouraged by all the different requests for changes, most of it really is quite trivial. Let me know if you don't expect to be able to work on this anytime soon, then I will help you out to get this landed more quickly.
} | ||
|
||
git_oid_cpy(&graft->oid, oid); | ||
k = git_oidmap_put(grafts, &graft->oid, &res); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, this really shows how old the PR is as it still uses the old-style map interfaces 🙄
|
||
#include "graft.h" | ||
|
||
int git__graft_register(git_graftmap *grafts, const git_oid *oid, git_array_oid_t parents) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd like to avoid the term "register" here. It really sounds a lot like our global registration API for certain subsystems, which caused some confusion for me. I'd thus prefer more generic "add"/"remove" functions.
Also, the double underscore for private functions is usually done after the subsystem. So it should rather be "git_graft__add".
And a third thing: as we're really talking about grafts here and not about a graft, maybe the final version should read "git_grafts__add"?
git_oidmap_set_value_at(grafts, idx, NULL); | ||
git__free(graft); | ||
} | ||
git_oidmap_delete_at(grafts, idx); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is hopefully all going to look a lot lett confusing with the new map APIs. It really has been a pain to use the old macro-based functions.
git_oidmap_clear(grafts); | ||
} | ||
|
||
int git__graft_for_oid(git_commit_graft **out, git_graftmap *grafts, const git_oid *oid) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about git_grafts__get
or git_grafts__lookup
?
} git_commit_graft; | ||
|
||
/* A special type of git_oidmap with git_commit_grafts as values */ | ||
typedef git_oidmap git_graftmap; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good enough for the time being, I guess. How about we directly use a structure typedef struct { git_oidmap map; } git_graftmap
? This would make it a lot easier to extend in the future, for example to use things like file caches if one had the need to refresh grafts or store things like the source of the graft if I were to add new graft entries?
if ((error = git_commit__parse_internal(commit, | ||
git_odb_object_data(odb_obj), | ||
git_odb_object_size(odb_obj), | ||
flags)) < 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alignment is a bit off here.
git_graftmap *grafts = git_oidmap_alloc(); | ||
git_array_oid_t parents = GIT_ARRAY_INIT; | ||
|
||
git_oid *oid1 = git_array_alloc(parents); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs to be checked for allocation errors
cl_git_pass(git_oid_fromstr(&oid_src, "2f3053cbff8a4ca2f0666de364ddb734a28a31a9")); | ||
git_oid_cpy(oid1, &oid_src); | ||
|
||
git_oid_fromstr(&oid_src, "f503807ffa920e407a600cfaee96b7152259acc7"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cl_git_pass
* @param repo The repository | ||
* @return 0 on success, an error otherwise. | ||
*/ | ||
GIT_EXTERN(int) git_repository_shallow_roots(git_oidarray *out, git_repository *repo); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're probably unable to mark git_oidarray
as const, right? Maybe we should instead just return a git_grafts
structure? After all, that's how shallow roots are implemented.
cl_assert_equal_s(git_oid_tostr_s(&oid_2), "be3563ae3f795b2b4353bcce3a527ad0a4f7f644"); | ||
|
||
git_revwalk_free(w); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test's commit can probably be amended to the other commit that introduced shallow tests.
If OP has gone inactive since submitting this let me know and I'll be happy to take over with a new PR. |
@tiennou is one of the most active people nowadays in the project, so I'd be surprised if he was inactive now. But open source being what it is it might still take some time ;) |
It's likely I'll only able to take a look at it in a couple of days. No sweat for the "late" review @pks-t, better late than never ! Frankly, if you feel up for it @Qix- I'll gladly defer the PR/polishing part, as I'd personally prefer to keep working on the ssh backend issue — as the review shows it's been a little while. Just a quick discussion point : as the overarching goal is to be |
I fully agree that we shouldn't blow up the grafts mechanism more than needed. But my intention is to have it as self-contained as possible to not spill grafts-related logic into other places. Thus the standalone I'll create a separate PR that builds on yours to show what I imagine it to look like :) |
@pks-t This can be closed, I believe. |
This PR enhance our shallow object support in the following ways :
GIT_ENOTFOUND
).Supersedes #3853, depends on #4445 and #4446.
This PR adds graft support at the ODB layer, withgit_repository
being responsible for tellinggit_odb
about known grafts. Shallow object support is then grafted (heh) on top of that.Notes :
- I'm not that convinced that having grafts at the ODB layer makes sense. FWIW, the crux of the issue is that object parsing is not done when objects are returned by the ODB, but by the caller (usually theobject
parse machinery, butrevwalk
also perform crude parsing for obvious reasons).git
does) is to have grafts be "transparent". IMHO, reporting an error in that case might help in the following cases :GIT_ENOTFOUND
.GIT_EITEROVER
if I don't actually care).