-
Notifications
You must be signed in to change notification settings - Fork 349
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
cli: implement jj gerrit send
for sending changes to Gerrit
#2845
base: main
Are you sure you want to change the base?
Conversation
ae28e5f
to
1fb8e16
Compare
jj gerrit mail
for submitting changes to Gerritjj gerrit mail
for sending changes to Gerrit
1fb8e16
to
0d6be43
Compare
0d6be43
to
65e7135
Compare
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 still haven't reviewed the last commit, but it's getting late, so here's what I have for now.
lib/src/footer.rs
Outdated
// to ensure we parse the footer in an unambiguous manner; this avoids cases | ||
// where a colon in the body of the message is mistaken for a footer line | ||
let mut footer = IndexMap::new(); | ||
let lines: Vec<&str> = body.trim().lines().rev().collect(); |
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.
nit: the first line could be skipped by .next()
and could do .rev().map().take_while()
or something.
cli/src/commands/gerrit.rs
Outdated
/// the form `branch@remote`, where `branch` is the target branch name that | ||
/// the change will be submitted to, and `remote` is the remote to push to. | ||
/// This remote MUST be a Git push URL for your Gerrit instance. | ||
#[arg(long = "for", short = 'f', default_value = "main@origin")] |
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.
might be better to use trunk()
as the default. We can't determine the remote if the trunk revision had multiple remote branches, though.
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 while trunk()
probably does make some sense since it tracks what the upstream would be implicitly (without hardcoding pre-existing branch/remote names), I think it's rather confusing because revsets are already used to specify the trees you want to push, and can be specified multiple times, and trunk()
looks quite like one at a glance. For example,
jj gerrit mail -r 'open() ~ empty()' -f 'trunk()'
reads quite strangely to me, especially because trunk()
would be rather special in this context where it actually specifies a pair of (branch, remote)
to send the change to; it's not clear at a glance what that means. I think that the branch@remote
syntax is more familiar in this case since it's what shows up in the default log output, it's used for all other @git
refs, etc. At the same time, it's also reasonably easy to translate to Gerrit's git push origin HEAD:refs/for/main
so it's not too unfamiliar. It's half-way between us and them.
Related, but what I would actually like to do is have this default be over-rideable in the repository configuration, so that people can also do things like set the URL to the Gerrit instance. e.g.
jj config set --repo gerrit.default-remote canon@upstream
or something of this manner to override main@origin
as the default --for
. We could also have a nested TOML list in the same manner so that remotes can have individually specified default branch targets, e.g. set gerrit.<remote>.default-for "master"
where <remote>
can be any Git remote.
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.
jj gerrit mail -r 'open() ~ empty()' -f 'trunk()'
Yeah, it's a bit odd. I don't think --for
should support a revset in general, but I mean the default could be derived from the trunk()
. If the user configured per-repository trunk()
, it would be something like trunk() = "foo@origin"
, so the purpose is quite similar to gerrit.default-remote
. Maybe we should have a parsable configuration for that, and both trunk()
and the gerrit --for
should default to it?
BTW, is this origin
in --for foo@origin
the gerrit endpoint? The project I worked before had a gerrit-specific SSH remote, and we usually don't pull from it. In that setup, the user-visible remote branch is something like master@origin
, but the remote to push to is gerrit
.
cli/src/commands/gerrit.rs
Outdated
// mark the old commit as rewritten to itself, but only because it | ||
// was a --dry-run, so we can give better error messages later | ||
old_to_new.insert(original_commit.clone(), (original_commit.clone(), true)); | ||
continue; |
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 we can just rewrite the commits, and discard the transaction? iirc, git push --dry-run -c
does a similar thing.
cli/src/commands/gerrit.rs
Outdated
} else { | ||
write!(ui.stderr(), "Skipped Change-Id (it already exists) for ")?; | ||
} | ||
tx.write_commit_summary(ui.stderr_formatter().as_mut(), new)?; |
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.
Perhaps, this loop can be moved to the previous "rewriting" loop, and we wouldn't have to maintain the mapping between old and new commits.
I think the commit summary can be printed by tx.base_workspace_helper()
to suppress scary markers.
From an outside perspective of a non-gerrit user, the word 'mail' in something seemingly unrelated to email (it is, right?) feels.. really weird Just my input, I've no real arguments for or against |
e41fca1
to
4fb99d4
Compare
What do you think we should call it? @thoughtpolice's first suggestion was |
I think I also plan on adding some other commands to fetch changes from Gerrit too (though sending them is the most important since fetching them has some workarounds.) Not sure how that factors in to the vocabulary, but something to consider... |
I suggest
Oh, and Git has
and Darcs has
|
I too am very supportive of |
4fb99d4
to
462f966
Compare
jj gerrit mail
for sending changes to Gerritjj gerrit send
for sending changes to Gerrit
bcf8a9c
to
699346f
Compare
8a40482
to
08e210b
Compare
c16fc43
to
3afc3e4
Compare
3afc3e4
to
3c2e0b0
Compare
This PR appears to be maintained, constantly rebasing on top of main, but it's been two months since the last comment. Could we either get this merged, or come up with a list of blockers that we need resolved before it can be merged? I see the tests aren't passing on CI - is that the only blocker? From what I could tell after looking at the PR, there are some minor improvements that could be made, but as @thoughtpolice said, it works, and I think it'd be nice to submit now and iterate on this later. |
As it happens, there was a thread about this on Discord today. @benbrittain also wanted to this merged. IIUC, the main reason it's not merged is that @thoughtpolice has run into some libgit2 (?) bug once in a while which manifests like this: https://stackoverflow.com/questions/16586642/git-unpack-error-on-push-to-gerrit. It still seems useful even if that happens sometimes. It looks like there are some unresolved comments too, and maybe we'll want another review of the code since it's been a while. I basically don't remember anything from my previous rounds of review. Do you have time to review it, @matts1? |
Sure, I can review - I'll take a look once CI is passing. |
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.
Random drive-by comments.
/// | ||
/// In this case, there are four footer lines: two `Co-authored-by` lines, one | ||
/// `Reviewed-by` line, and one `Change-Id` line. | ||
pub fn get_footer_lines(body: &str) -> Vec<FooterEntry> { |
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.
Is it worth handling this case from git-interpret-trailers
' man page?
The may be split over multiple lines with each subsequent line starting with at least one whitespace, like the "folding" in RFC 822. Example:
key: This is a very long value, with spaces and newlines in it.
Gerrit code uses RevCommit#getFooterLines which seems to handle this 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.
Nice catch. Yes, I'll look into fixing this.
3c2e0b0
to
e71a4ee
Compare
} | ||
|
||
// case 2 | ||
if let Ok(remote) = config.get_string("gerrit.default_remote") { |
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.
Should case 3 and case 4 come before case 2? If the user sets gerrit.default_remote
to foo
, and there's only one remote bar
, should we just upload to bar
? I don't think there's a right or wrong answer here, but whatever we decide, I think we should add a comment to the code to show that the behaviour is intentional.
Following on from that, should case 4 be combined with case 2? We could just make it let remote = config.get_string("gerrit.default_remote").unwrap_or("gerrit")
.
if footer.iter().filter(|entry| entry.0 == "Change-Id").count() > 1 { | ||
writeln!( | ||
ui.warning_default(), | ||
"warning: multiple Change-Id footers in 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.
You called this an error in the comment above, but this isn't an error - it looks like it'll specifically just skip uploading this one commit, but upload all others.
I'd personally prefer we made this into an actual error. I think that if, for example, we have a parent commit which has duplicate change IDs, and a child commit which is valid, uploading the child without the parent seems like a bad idea.
|
||
// check the change-id format is correct in any case | ||
if cid.len() != 41 || !cid.starts_with('I') { | ||
writeln!( |
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.
Same as above - I'd prefer an error
// them, and I realized my old signoff.sh script created invalid | ||
// ones, so this is a helpful barrier. | ||
|
||
continue; // fallthrough |
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 think all of these continues make it very difficult to understand the code path. I'd prefer we use if/else instead.
If it's too nested that way or something like that, putting it into functions may make it easier
Err(user_error(err.to_string())) | ||
} | ||
// XXX (aseipp): more cases to handle here? | ||
_ => Err(user_error(err.to_string())), |
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.
Should this be a user error?
"warning: internal git error while pushing to gerrit: {}", | ||
err | ||
)?; | ||
Err(user_error(err.to_string())) |
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 shouldn't be a user error
e71a4ee
to
11ed301
Compare
/// | ||
/// This command modifies each commit in the revset to include a `Change-Id` | ||
/// footer in its commit message if one does not already exist. Note that | ||
/// this ID is NOT compatible with jj IDs, and is Gerrit-specific. |
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.
Can't remember if this was discussed earlier, but have we considered making this a transient hash of the JJ change ID that isn't actually stored in the committee description?
I've implemented something roughly similar to what you've done, and one problem I had was that when I run JJ split you end up with, by default, two commits with the same gerrit change ID.
That being said, I consider this a very good improvement over the current behaviour, so I'd recommend we submit as is even if we were to change it to that in the future.
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 like using jj change hex as Change-Id (see #4477 (comment)) but I'm not sure if we can get away with not storing it explicitly -- splits could get very confusing.
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.
Even git interactions could get weird (git commit --amend --no-edit
, for example).
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 think that a hybrid approach where we don't store it explicitly automatically, but if you have manually stored it explicitly then we use that Change-ID would work relatively well. It'd also work well with splits by providing reasonable defaults, but could be overridden if required.
Re splits, it should work just fine as long as the user knows exactly what's gonna happen with the split (specifically, jj split
to associate the change-ID with the second one, and jj commit
to associate the Change-ID with the first one). Maybe we need something like jj change-id swap foo bar
or jj change-id set -r foo <change-id>
.
This merges PR martinvonz#3191 (OpenSSH) and PR martinvonz#2845 (Gerrit) in order to build from the combination.
This merges PR martinvonz#3191 (OpenSSH) and PR martinvonz#2845 (Gerrit) in order to build from the combination.
Natural extension of the existing `[T]` instance. Signed-off-by: Austin Seipp <[email protected]> Change-Id: Ib7f6fd829096b2cac8e3d7b9471a92ddb76a621b
To be used for parsing `Change-Id`s from commits, in service of Gerrit support. Signed-off-by: Austin Seipp <[email protected]> Change-Id: I434d76b1229b36b815622ad7409ced3a405cbe22
This implements the most basic workflow for submitting changes to Gerrit, through a verb called 'send'. This verb is intended to be distinct from the word 'submit', which for Gerrit means 'merge a change into the repository.' Given a list of revsets (specified by multiple `-r` options), this will parse the footers of every commit, collect them, insert a `Change-Id` (if one doesn't already exist), and then push them into the given remote. Because the argument is a revset, you may submit entire trees of changes at once, including multiple trees of independent changes, e.g. jj gerrit send -r foo:: -r baz:: There are many other improvements that can be applied on top of this, including a ton of consistency and "does this make sense?" checks. However, it is flexible and a good starting point, and you can in fact both submit and cycle reviews with this interface. Signed-off-by: Austin Seipp <[email protected]>
11ed301
to
83b9b3a
Compare
I understand that this PR is working to send a special changeID for gerrit, I would assume that ideally the changeID for jj would similarly come from the gerrit changeID when commits come with a gerrit changeID, implementing at least for gerrit issue #4706 |
Oh man, with all of the recent smattering of new jj buzz, I decided to take the plunge. I am so happy to have found this PR, which is the last remaining need for me (I really don't want to go do the EDITOR Change-Id hack). What is left to get this merged? Sure seems like this branch is stable, well maintained, in use, etc. |
My attention has been split elsewhere — among other JJ stuff, and a new job that does not use Gerrit — so I haven't had the time to push this over the finish line. There are many people using it, so I agree it's not ideal. It mostly just needs some tests and I would consider it in a reviewable state (modulo typical review cycles), it's just low on my stack of TODO items. |
Would you like someone else to help finish it? I'm not volunteering, but it maybe @jtolio or someone else is interested. |
That would be nice, but it is kind of annoying for a number of reasons. A common one (expressed elsewhere) is that you might want to abandon a change on Gerrit, then try to re-push a new version, but it will reject that if the I don't think there's a clear "best way" to make it work when the Change-Id trailer is not part of the actual commit object. That's part of the reason I changed the logic from using the stable Honestly, I still think the best long-term strategy might be to just cooperate with the Git developers and for the two of us (Git and Jujutsu) to standardize on a |
Yes, I'd be happy to get some help; someone else could even re-file this PR (assuming they kept my name as a coauthor) and I'd be happy to review it and suggest some things I wanted. I think some people have even offered previously; but of course life finds a way to make us busy... |
In case it helps anyone, I set [template-aliases]
'gerrit_change_id(change_id)' = '"Id0000000" ++ change_id.normal_hex()'
[templates]
draft_commit_description = '''
separate("\n",
description.remove_suffix("\n"),
if(!description.contains(change_id.normal_hex()),
"\nChange-Id: " ++ gerrit_change_id(change_id)
),
"\n",
surround("JJ: Changes:\n", "", indent("JJ: \t", diff.summary()))
)
''' |
Warning
This PR is very rough, and needs significant polish to handle edge cases and other small nits, but functions for basic uses just fine, I think. Please only use it on copies of your existing repositories, or throwaway repositories that you can
jj undo
on. This warning will be removed once I feel it's more stable.Please remember: Do not taunt Happy Fun Ball.
This implements the most basic workflow for submitting changes to Gerrit, through a verb called 'send'. Send was chosen to avoid conflicting terminology with terms like "submit" (which merges a change in Gerrit) or "review" which is ambiguous.
Given a list of revsets (specified by multiple
-r
options), this will parse the footers of every commit, collect them, insert aChange-Id
(if one doesn't already exist), and then push them into the given remote.Because the argument is a revset, you may submit entire trees of changes at once, including multiple trees of independent changes, e.g.
The remote to push into is, by default,
main@origin
, and will translate torefs/for/main
at that remote. I.e. it is expected that you are using Gerrit as the source of truth, of course. You can specify another remote using the argument--for
however, e.g.--for main@gerrit
which will change the remote. This syntax is intended to resemble existing syntax for remote references injj
for the sake of UX consistency, rather than blindly exposing the Gitrefs/
syntax to users, which isn't needed, practically speaking.There are many other improvements that can be applied on top of this, including a ton of consistency and "does this make sense?" checks. However, it is flexible and a good starting point, and you can in fact both submit and cycle reviews with this interface.