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
Filter branch #429
Filter branch #429
Conversation
"refs/tags/lw", | ||
"refs/tags/test", | ||
}; | ||
Assert.Equal(expected, result.Select(x => x.CanonicalName).OrderBy(x => x)); |
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.
Generally helpful to tack on a ToArray()
after OrderBy()
to get a better message on failure.
I just pushed up what I did on my flight. Let me try to synthesize the feedback here and match it up with where things stand now:
Also, I checked off a few of the boxes, so this is a bit more powerful now. Let me know what you think. |
repo.RewriteHistory(commits, commitHeaderRewriter: c => CommitRewriteInfo.From(c, message: "abc")); | ||
Assert.Empty(repo.Refs.Where(x => x.CanonicalName.StartsWith("refs/original/original/"))); | ||
} | ||
} |
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.
The travis build is going to fail here. The question is, what should happen if we try to back up the refs to refs/original
(or wherever), and it already exists? We could:
- Pretend we're doing
filter-branch -f
, and overwrite them. - Throw an exception of some sort, though I'd want to do a checking pass first so we don't actually do a rewrite if it's going to fail.
- All of the above, with some way of turning it on.
- A callback for collisions? I only mention it because I've seen it done inside libgit2.
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.
Throw an exception of some sort, though I'd want to do a checking pass first so we don't actually do a rewrite if it's going to fail.
👍
R# will complain, but it's perfectly legal to have overlapping methods with and without optional parameters so you can still use
I had overlooked the use of Unrelated microoptimization: you could capture
|
|
||
|
||
// This test should rewrite br2, but not packed-test: | ||
// * a4a7dce (br2) Merge branch 'master' into br2 |
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.
Shouldn't this comment rather decorate the test above?
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.
Oh yeah. It should. 😊
Hey, you're right! I was trusting the squigglies too much.
Yup. I like cheating. That makes it much nicer to use, too. Now I'm wondering if it makes sense to make |
/// <returns>A new <see cref="CommitRewriteInfo"/> object that matches the info for the <paramref name="commit"/>.</returns> | ||
public static CommitRewriteInfo From(Commit commit) | ||
{ | ||
return new CommitRewriteInfo |
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.
Just personal preference, but I'm inclined to capture the values from commit
in the other overload, and just defer to that one here.
@ben Could you please rather move it into |
var newName = referenceNameRewriter(reference.CanonicalName.Substring(5)); | ||
if (newName != null) | ||
{ | ||
Refs.Add(newName, reference.TargetIdentifier, true, "rewrite history"); |
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.
Hmmm. Maybe would the reflog also deserve some love 😜
More insight here
@nulltoken mentioned the reflog. Looking at git's code, it seems like it only generates a few distinct reflog messages.
I've corrected 1 and 2; Cases 3 and 4 aren't directly supported, but you could simulate them with the parent-rewrite hook (by either omitting or inserting parent links). The problem is that, since |
/// If this returns null, the tag will be deleted. | ||
/// If it returns the empty string, the tag will not be changed. | ||
/// If it returns the input, the tag will be moved. | ||
/// Any other value results in a new tag.</param> |
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 an emulation of what git provides, but now that I've written it I'm not totally convinced it's the right thing. There's a lot of behavior here that's keying off the value of a returned string. Would it make more sense to have this be an Action<Tag, GitObject>
that receives the old tag and the new target? We could even call it in the right order for chained tags.
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.
If this returns null, the tag will be deleted.
Can this handle the following use case?
initial:
refs/tags/my-tag -> TagA -> TagB -> TagC -> GitObject (either a Blob, Tree or a Commit)
expected output:
refs/original/tags/my-tag -> TagC -> GitObject (either a Blob, Tree or a Commit)
or
refs/original/tags/my-tag -> TagA -> GitObject (either a Blob, Tree or a Commit)
or
refs/original/tags/my-tag -> GitObject (either a Blob, Tree or a Commit)
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.
Any other value results in a new tag
Can this handle the following use case?
rewriter = x => { return x == "v0.0.1" ? "v0.0.1rc" : x }
initial:
refs/tags/my-tag -> TagA (name: test) -> TagB (name: v0.0.1) -> TagC (name: another) -> GitObject (either a Blob, Tree or a Commit)
expected output:
refs/original/tags/my-tag -> TagA (name: test) -> TagB (name: v0.0.1rc) -> TagC (name: another) -> GitObject (either a Blob, Tree or a Commit)
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.
initial:
refs/tags/my-tag -> TagA -> TagB -> TagC -> GitObject (either a Blob, Tree or a Commit)
expected output:
refs/original/tags/my-tag -> TagC -> GitObject (either a Blob, Tree or a Commit) or refs/original/tags/my-tag -> TagA -> GitObject (either a Blob, Tree or a Commit) or refs/original/tags/my-tag -> GitObject (either a Blob, Tree or a Commit)
My gut feeling is that refs/original/tags/my-tag
should now point to refs/original/tags/TagA
– the tag-chain rewriting should act a lot more like the commit rewriting than it currently does. I'll add this case to the tests, but it may already work.
initial:
refs/tags/my-tag -> TagA (name: test) -> TagB (name: v0.0.1) -> TagC (name: another) -> GitObject (either a Blob, Tree or a Commit)
expected output:
refs/original/tags/my-tag -> TagA (name: test) -> TagB (name: v0.0.1rc) -> TagC (name: another) -> GitObject (either a Blob, Tree or a Commit)
This one probably already works. Whatever the result of the name callback, the code records the before and after states of every rewritten tag in tagMap
. So when rewriting TagA
, the code notices that it's pointing to a TagAnnotation
. It tries to rewrite the target tag TagB
first (using a cached result if that's already done), and creates a new TagA
that points to TagB
's annotation.
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'm coming back to this, and more and more I'm thinking that tag handling should be left to the caller. The default behavior is to do nothing to tags (to preserve signing, etc) – not even backing them up. If the caller wants to rewrite them, they're completely able to.
I think it'd be ok to keep those outside the scope of this PR. Could you please open an issue to keep track on those? Regarding this, does initial:
expected output:
|
What do you think is the correct thing to do in this situation?
The commit's tree has been rewritten to include a new blob in the place the old blob was. Should the tag now point to the new blob in the new tree? My gut feel is no; if you went out of your way to tag a blob, you probably want that to always point to that blob. |
It kind of looks that way. There's a reflog message for "rewritten to multiple", meaning the commit that I'm comfortable passing this off to the caller. If she wants to split a commit into an entire topology (using some combination of the |
directReferenceRewriter: | ||
(r, t) => repo.Refs.DefaultDirectReferenceRewriter(r, t, "refs/rewritten")); | ||
Assert.NotEmpty(repo.Refs.Where(x => x.CanonicalName.StartsWith("refs/rewritten"))); | ||
} |
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 haven't done this for tags yet, but this is what it looks like if you don't restrict the rewriting hooks to just naming the backups. I provide a default implementation that does the right things, but it's easy to override for just naming, or you can do all your own logic in there.
@ben |
So now this properly rewrites chains of tag annotations without creating spurious tags in the process. Is there anything left, or should we ship it? |
@ben I found a couple of issues and eventually ended on a coding spree. Sorry 😁 I've tried to simplify the code a bit (mainly to help me get a better understanding of it). Main changes are:
|
this.backupRefsNamespace = backupRefsNamespace; | ||
} | ||
|
||
public void AmazeMe() |
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.
Yes. I'm really that bad at naming...
Ideas?
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.
Rewrite? Execute?
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.
Execute!
@dahlbyk Thanks. Fixed.
🔥 @yorah 🔥 ❗ Making a pull request to a pull request? 🙀 |
return td; | ||
}); | ||
|
||
Assert.Empty(repo.Head.Commits.Where(c => c["README"] != null)); |
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.
Super nitpicky: would this read better as Assert.True(repo.Head.Commits.All(c => c["README"] == null));
?
I think it's ready! 💯 💴 |
✨ ❗ ✨ |
This adds an idiomatic c-sharpy api for doing filter-branch-like operations. This works without ever touching the index or the working directory, so it should be wicked fast.
TreeDefinition
--tag-filter
)--parent-filter
)refs/original
The API looks like this: