mirror of
https://github.com/jj-vcs/jj.git
synced 2025-12-23 06:01:01 +00:00
cli: basic jj gerrit upload implementation
This implements the most basic workflow for submitting changes to Gerrit, through a verb called 'upload'. This verb is chosen to match the gerrit documentation (https://gerrit-documentation.storage.googleapis.com/Documentation/3.12.1/user-upload.html). Given a list of revsets (specified by multiple `-r` options), this will parse the footers of every commit, collect them, insert a `Change-Id` based off the jj change-id (if one doesn't already exist), and then push them into the given remote. It will then abandon the transaction, thus ensuring that gerrit change-IDs do not appear in jj. 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 upload -r foo:: -r baz:: Signed-off-by: Austin Seipp <aseipp@pobox.com>
This commit is contained in:
parent
84c7f5d562
commit
35ad063b85
8 changed files with 574 additions and 0 deletions
|
|
@ -45,6 +45,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||
* External diff commands now support substitution variable `$width` for the
|
||||
number of available terminal columns.
|
||||
|
||||
* Gerrit support implemented with the new command `jj gerrit upload`
|
||||
|
||||
### Fixed bugs
|
||||
|
||||
## [0.33.0] - 2025-09-03
|
||||
|
|
|
|||
40
cli/src/commands/gerrit/mod.rs
Normal file
40
cli/src/commands/gerrit/mod.rs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
// Copyright 2024 The Jujutsu Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::fmt::Debug;
|
||||
|
||||
use clap::Subcommand;
|
||||
|
||||
use crate::cli_util::CommandHelper;
|
||||
use crate::command_error::CommandError;
|
||||
use crate::commands::gerrit;
|
||||
use crate::ui::Ui;
|
||||
|
||||
/// Interact with Gerrit Code Review.
|
||||
#[derive(Subcommand, Clone, Debug)]
|
||||
pub enum GerritCommand {
|
||||
Upload(gerrit::upload::UploadArgs),
|
||||
}
|
||||
|
||||
pub fn cmd_gerrit(
|
||||
ui: &mut Ui,
|
||||
command: &CommandHelper,
|
||||
subcommand: &GerritCommand,
|
||||
) -> Result<(), CommandError> {
|
||||
match subcommand {
|
||||
GerritCommand::Upload(review) => gerrit::upload::cmd_gerrit_upload(ui, command, review),
|
||||
}
|
||||
}
|
||||
|
||||
mod upload;
|
||||
383
cli/src/commands/gerrit/upload.rs
Normal file
383
cli/src/commands/gerrit/upload.rs
Normal file
|
|
@ -0,0 +1,383 @@
|
|||
// Copyright 2024 The Jujutsu Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Debug;
|
||||
use std::io::Write as _;
|
||||
use std::sync::Arc;
|
||||
|
||||
use bstr::BStr;
|
||||
use itertools::Itertools as _;
|
||||
use jj_lib::backend::BackendError;
|
||||
use jj_lib::backend::CommitId;
|
||||
use jj_lib::commit::Commit;
|
||||
use jj_lib::git::GitRefUpdate;
|
||||
use jj_lib::git::{self};
|
||||
use jj_lib::object_id::ObjectId as _;
|
||||
use jj_lib::repo::Repo as _;
|
||||
use jj_lib::revset::RevsetExpression;
|
||||
use jj_lib::settings::UserSettings;
|
||||
use jj_lib::store::Store;
|
||||
use jj_lib::trailer::Trailer;
|
||||
use jj_lib::trailer::parse_description_trailers;
|
||||
|
||||
use crate::cli_util::CommandHelper;
|
||||
use crate::cli_util::RevisionArg;
|
||||
use crate::cli_util::short_change_hash;
|
||||
use crate::command_error::CommandError;
|
||||
use crate::command_error::internal_error;
|
||||
use crate::command_error::user_error;
|
||||
use crate::command_error::user_error_with_hint;
|
||||
use crate::command_error::user_error_with_message;
|
||||
use crate::git_util::with_remote_git_callbacks;
|
||||
use crate::ui::Ui;
|
||||
|
||||
/// Upload changes to Gerrit for code review, or update existing changes.
|
||||
///
|
||||
/// Uploading in a set of revisions to Gerrit creates a single "change" for
|
||||
/// each revision included in the revset. These changes are then available
|
||||
/// for review on your Gerrit instance.
|
||||
///
|
||||
/// Note: The gerrit commit Id may not match that of your local commit Id,
|
||||
/// since we add a `Change-Id` footer to the commit message if one does not
|
||||
/// already exist. This ID is based off the jj Change-Id, but is not the same.
|
||||
///
|
||||
/// If a change already exists for a given revision (i.e. it contains the
|
||||
/// same `Change-Id`), this command will update the contents of the existing
|
||||
/// change to match.
|
||||
///
|
||||
/// Note: this command takes 1-or-more revsets arguments, each of which can
|
||||
/// resolve to multiple revisions; so you may post trees or ranges of
|
||||
/// commits to Gerrit for review all at once.
|
||||
#[derive(clap::Args, Clone, Debug)]
|
||||
pub struct UploadArgs {
|
||||
/// The revset, selecting which revisions are sent in to Gerrit. This can be
|
||||
/// any arbitrary set of commits.
|
||||
/// Note that when you push a commit at the head of a stack, all ancestors
|
||||
/// are pushed too. This means that `jj gerrit upload -r foo` is equivalent
|
||||
/// to `jj gerrit upload -r 'mutable()::foo`
|
||||
#[arg(long, short = 'r')]
|
||||
revisions: Vec<RevisionArg>,
|
||||
|
||||
/// The location where your changes are intended to land. This should be
|
||||
/// a branch on the remote.
|
||||
#[arg(long = "remote-branch", short = 'b')]
|
||||
remote_branch: Option<String>,
|
||||
|
||||
/// The Gerrit remote to push to. Can be configured with the `gerrit.remote`
|
||||
/// repository option as well. This is typically a full SSH URL for your
|
||||
/// Gerrit instance.
|
||||
#[arg(long)]
|
||||
remote: Option<String>,
|
||||
|
||||
/// If true, do not actually push the changes to Gerrit.
|
||||
#[arg(long = "dry-run", short = 'n')]
|
||||
dry_run: bool,
|
||||
}
|
||||
|
||||
fn calculate_push_remote(
|
||||
store: &Arc<Store>,
|
||||
config: &UserSettings,
|
||||
remote: Option<&str>,
|
||||
) -> Result<String, CommandError> {
|
||||
let git_repo = git::get_git_repo(store)?; // will fail if not a git repo
|
||||
let remotes = git_repo.remote_names();
|
||||
|
||||
// case 1
|
||||
if let Some(remote) = remote {
|
||||
if remotes.contains(BStr::new(&remote)) {
|
||||
return Ok(remote.to_string());
|
||||
}
|
||||
return Err(user_error(format!(
|
||||
"The remote '{remote}' (specified via `--remote`) does not exist",
|
||||
)));
|
||||
}
|
||||
|
||||
// case 2
|
||||
if let Ok(remote) = config.get_string("gerrit.default-remote") {
|
||||
if remotes.contains(BStr::new(&remote)) {
|
||||
return Ok(remote);
|
||||
}
|
||||
return Err(user_error(format!(
|
||||
"The remote '{remote}' (configured via `gerrit.default-remote`) does not exist",
|
||||
)));
|
||||
}
|
||||
|
||||
// case 3
|
||||
if let Some(remote) = git_repo.remote_default_name(gix::remote::Direction::Push) {
|
||||
return Ok(remote.to_string());
|
||||
}
|
||||
|
||||
// case 4
|
||||
if remotes.iter().any(|r| **r == "gerrit") {
|
||||
return Ok("gerrit".to_owned());
|
||||
}
|
||||
|
||||
// case 5
|
||||
Err(user_error(
|
||||
"No remote specified, and no 'gerrit' remote was found",
|
||||
))
|
||||
}
|
||||
|
||||
/// Determine what Gerrit ref and remote to use. The logic is:
|
||||
///
|
||||
/// 1. If the user specifies `--remote-branch branch`, use that
|
||||
/// 2. If the user has 'gerrit.default-remote-branch' configured, use that
|
||||
/// 3. Otherwise, bail out
|
||||
fn calculate_push_ref(
|
||||
config: &UserSettings,
|
||||
remote_branch: Option<String>,
|
||||
) -> Result<String, CommandError> {
|
||||
// case 1
|
||||
if let Some(remote_branch) = remote_branch {
|
||||
return Ok(remote_branch);
|
||||
}
|
||||
|
||||
// case 2
|
||||
if let Ok(branch) = config.get_string("gerrit.default-remote-branch") {
|
||||
return Ok(branch);
|
||||
}
|
||||
|
||||
// case 3
|
||||
Err(user_error(
|
||||
"No target branch specified via --remote-branch, and no 'gerrit.default-remote-branch' \
|
||||
was found",
|
||||
))
|
||||
}
|
||||
|
||||
pub fn cmd_gerrit_upload(
|
||||
ui: &mut Ui,
|
||||
command: &CommandHelper,
|
||||
args: &UploadArgs,
|
||||
) -> Result<(), CommandError> {
|
||||
let mut workspace_command = command.workspace_helper(ui)?;
|
||||
|
||||
let revisions: Vec<_> = workspace_command
|
||||
.parse_union_revsets(ui, &args.revisions)?
|
||||
.evaluate_to_commit_ids()?
|
||||
.try_collect()?;
|
||||
if revisions.is_empty() {
|
||||
writeln!(ui.status(), "No revisions to upload.")?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
workspace_command.check_rewritable(revisions.iter())?;
|
||||
|
||||
// If you have the changes main -> A -> B, and then run `jj gerrit upload -r B`,
|
||||
// then that uploads both A and B. Thus, we need to ensure that A also
|
||||
// has a Change-ID.
|
||||
// We make an assumption here that all immutable commits already have a
|
||||
// Change-ID.
|
||||
let to_upload: Vec<Commit> = workspace_command
|
||||
.attach_revset_evaluator(
|
||||
workspace_command
|
||||
.env()
|
||||
.immutable_expression()
|
||||
.range(&RevsetExpression::commits(revisions.clone())),
|
||||
)
|
||||
.evaluate_to_commits()?
|
||||
.try_collect()?;
|
||||
|
||||
// Note: This transaction is intentionally never finished. This way, the
|
||||
// Change-Id is never part of the commit description in jj.
|
||||
// This avoids scenarios where you have many commits with the same
|
||||
// Change-Id, or a single commit with many Change-Ids after running
|
||||
// jj split / jj squash respectively.
|
||||
// If a user doesn't like this behavior, they can add the following to
|
||||
// their Cargo.toml.
|
||||
// commit_trailers = 'if(!trailers.contains_key("Change-Id"),
|
||||
// format_gerrit_change_id_trailer(self))'
|
||||
let mut tx = workspace_command.start_transaction();
|
||||
let base_repo = tx.base_repo();
|
||||
let store = base_repo.store().clone();
|
||||
|
||||
let old_heads = base_repo
|
||||
.index()
|
||||
.heads(&mut revisions.iter())
|
||||
.map_err(internal_error)?;
|
||||
|
||||
let git_settings = command.settings().git_settings()?;
|
||||
let remote = calculate_push_remote(&store, command.settings(), args.remote.as_deref())?;
|
||||
let remote_branch = calculate_push_ref(command.settings(), args.remote_branch.clone())?;
|
||||
|
||||
// Immediately error and reject any commits that shouldn't be uploaded.
|
||||
for commit in &to_upload {
|
||||
if commit.is_empty(tx.repo_mut())? {
|
||||
return Err(user_error_with_hint(
|
||||
format!(
|
||||
"Refusing to upload revision {} because it is empty",
|
||||
short_change_hash(commit.change_id())
|
||||
),
|
||||
"Perhaps you squashed then ran upload? Maybe you meant to upload the parent \
|
||||
commit instead (eg. @-)",
|
||||
));
|
||||
}
|
||||
if commit.description().is_empty() {
|
||||
return Err(user_error_with_hint(
|
||||
format!(
|
||||
"Refusing to upload revision {} because it is has no description",
|
||||
short_change_hash(commit.change_id())
|
||||
),
|
||||
"Maybe you meant to upload the parent commit instead (eg. @-)",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let mut old_to_new: HashMap<CommitId, Commit> = HashMap::new();
|
||||
for original_commit in to_upload {
|
||||
let trailers = parse_description_trailers(original_commit.description());
|
||||
|
||||
let change_id_trailers: Vec<&Trailer> = trailers
|
||||
.iter()
|
||||
.filter(|trailer| trailer.key == "Change-Id")
|
||||
.collect();
|
||||
|
||||
// There shouldn't be multiple change-ID fields. So just error out if
|
||||
// there is.
|
||||
if change_id_trailers.len() > 1 {
|
||||
return Err(user_error(format!(
|
||||
"multiple Change-Id footers in revision {}",
|
||||
short_change_hash(original_commit.change_id())
|
||||
)));
|
||||
}
|
||||
|
||||
// The user can choose to explicitly set their own change-ID to
|
||||
// override the default change-ID based on the jj change-ID.
|
||||
if let Some(trailer) = change_id_trailers.first() {
|
||||
// Check the change-id format is correct.
|
||||
if trailer.value.len() != 41 || !trailer.value.starts_with('I') {
|
||||
// Intentionally leave the invalid change IDs as-is.
|
||||
writeln!(
|
||||
ui.warning_default(),
|
||||
"warning: invalid Change-Id footer in revision {}",
|
||||
short_change_hash(original_commit.change_id()),
|
||||
)?;
|
||||
}
|
||||
|
||||
// map the old commit to itself
|
||||
old_to_new.insert(original_commit.id().clone(), original_commit);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Gerrit change id is 40 chars, jj change id is 32, so we need padding.
|
||||
// To be consistent with `format_gerrit_change_id_trailer``, we pad with
|
||||
// 6a6a6964 (hex of "jjid").
|
||||
let gerrit_change_id = format!("I6a6a6964{}", original_commit.change_id().hex());
|
||||
|
||||
let new_description = format!(
|
||||
"{}{}Change-Id: {}\n",
|
||||
original_commit.description().trim(),
|
||||
if trailers.is_empty() { "\n\n" } else { "\n" },
|
||||
gerrit_change_id
|
||||
);
|
||||
|
||||
let new_parents = original_commit
|
||||
.parents()
|
||||
.map(|parent| -> Result<CommitId, BackendError> {
|
||||
let p = parent?;
|
||||
Ok(old_to_new.get(p.id()).unwrap_or(&p).id().clone())
|
||||
})
|
||||
.try_collect()?;
|
||||
|
||||
// rewrite the set of parents to point to the commits that were
|
||||
// previously rewritten in toposort order
|
||||
let new_commit = tx
|
||||
.repo_mut()
|
||||
.rewrite_commit(&original_commit)
|
||||
.set_description(new_description)
|
||||
.set_parents(new_parents)
|
||||
// Set the timestamp back to the timestamp of the original commit.
|
||||
// Otherwise, `jj gerrit upload @ && jj gerrit upload @` will upload
|
||||
// two patchsets with the only difference being the timestamp.
|
||||
.set_committer(original_commit.committer().clone())
|
||||
.set_author(original_commit.author().clone())
|
||||
.write()?;
|
||||
|
||||
old_to_new.insert(original_commit.id().clone(), new_commit);
|
||||
}
|
||||
writeln!(ui.stderr())?;
|
||||
|
||||
let remote_ref = format!("refs/for/{remote_branch}");
|
||||
writeln!(
|
||||
ui.stderr(),
|
||||
"Found {} heads to push to Gerrit (remote '{}'), target branch '{}'",
|
||||
old_heads.len(),
|
||||
remote,
|
||||
remote_branch,
|
||||
)?;
|
||||
|
||||
writeln!(ui.stderr())?;
|
||||
|
||||
// NOTE (aseipp): because we are pushing everything to the same remote ref,
|
||||
// we have to loop and push each commit one at a time, even though
|
||||
// push_updates in theory supports multiple GitRefUpdates at once, because
|
||||
// we obviously can't push multiple heads to the same ref.
|
||||
for head in &old_heads {
|
||||
write!(
|
||||
ui.stderr(),
|
||||
"{}",
|
||||
if args.dry_run {
|
||||
"Dry-run: Would push "
|
||||
} else {
|
||||
"Pushing "
|
||||
}
|
||||
)?;
|
||||
// We have to write the old commit here, because until we finish
|
||||
// the transaction (which we don't), the new commit is labeled as
|
||||
// "hidden".
|
||||
tx.base_workspace_helper().write_commit_summary(
|
||||
ui.stderr_formatter().as_mut(),
|
||||
&store.get_commit(head).unwrap(),
|
||||
)?;
|
||||
writeln!(ui.stderr())?;
|
||||
|
||||
if args.dry_run {
|
||||
continue;
|
||||
}
|
||||
|
||||
let new_commit = old_to_new.get(head).unwrap();
|
||||
|
||||
// how do we get better errors from the remote? 'git push' tells us
|
||||
// about rejected refs AND ALSO '(nothing changed)' when there are no
|
||||
// changes to push, but we don't get that here.
|
||||
with_remote_git_callbacks(ui, |cb| {
|
||||
git::push_updates(
|
||||
tx.repo_mut(),
|
||||
&git_settings,
|
||||
remote.as_ref(),
|
||||
&[GitRefUpdate {
|
||||
qualified_name: remote_ref.clone().into(),
|
||||
expected_current_target: None,
|
||||
new_target: Some(new_commit.id().clone()),
|
||||
}],
|
||||
cb,
|
||||
)
|
||||
})
|
||||
// Despite the fact that a manual git push will error out with 'no new
|
||||
// changes' if you're up to date, this git backend appears to silently
|
||||
// succeed - no idea why.
|
||||
// It'd be nice if we could distinguish this. We should ideally succeed,
|
||||
// but give the user a warning.
|
||||
.map_err(|err| match err {
|
||||
git::GitPushError::NoSuchRemote(_)
|
||||
| git::GitPushError::RemoteName(_)
|
||||
| git::GitPushError::UnexpectedBackend(_) => user_error(err),
|
||||
git::GitPushError::Subprocess(_) => {
|
||||
user_error_with_message("Internal git error while pushing to gerrit", err)
|
||||
}
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -31,6 +31,8 @@ mod evolog;
|
|||
mod file;
|
||||
mod fix;
|
||||
#[cfg(feature = "git")]
|
||||
mod gerrit;
|
||||
#[cfg(feature = "git")]
|
||||
mod git;
|
||||
mod help;
|
||||
mod interdiff;
|
||||
|
|
@ -118,6 +120,9 @@ enum Command {
|
|||
Fix(fix::FixArgs),
|
||||
#[cfg(feature = "git")]
|
||||
#[command(subcommand)]
|
||||
Gerrit(gerrit::GerritCommand),
|
||||
#[cfg(feature = "git")]
|
||||
#[command(subcommand)]
|
||||
Git(git::GitCommand),
|
||||
Help(help::HelpArgs),
|
||||
Interdiff(interdiff::InterdiffArgs),
|
||||
|
|
@ -185,6 +190,8 @@ pub fn run_command(ui: &mut Ui, command_helper: &CommandHelper) -> Result<(), Co
|
|||
Command::File(args) => file::cmd_file(ui, command_helper, args),
|
||||
Command::Fix(args) => fix::cmd_fix(ui, command_helper, args),
|
||||
#[cfg(feature = "git")]
|
||||
Command::Gerrit(sub_args) => gerrit::cmd_gerrit(ui, command_helper, sub_args),
|
||||
#[cfg(feature = "git")]
|
||||
Command::Git(args) => git::cmd_git(ui, command_helper, args),
|
||||
Command::Help(args) => help::cmd_help(ui, command_helper, args),
|
||||
Command::Interdiff(args) => interdiff::cmd_interdiff(ui, command_helper, args),
|
||||
|
|
|
|||
|
|
@ -490,6 +490,20 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"gerrit": {
|
||||
"type": "object",
|
||||
"description": "Settings for interacting with Gerrit",
|
||||
"properties": {
|
||||
"default-remote": {
|
||||
"type": "string",
|
||||
"description": "The Gerrit remote to interact with"
|
||||
},
|
||||
"default-remote-branch": {
|
||||
"type": "string",
|
||||
"description": "The default branch to propose changes for"
|
||||
}
|
||||
}
|
||||
},
|
||||
"merge": {
|
||||
"type": "object",
|
||||
"description": "Merge settings",
|
||||
|
|
|
|||
|
|
@ -47,6 +47,8 @@ This document contains the help content for the `jj` command-line program.
|
|||
* [`jj file track`↴](#jj-file-track)
|
||||
* [`jj file untrack`↴](#jj-file-untrack)
|
||||
* [`jj fix`↴](#jj-fix)
|
||||
* [`jj gerrit`↴](#jj-gerrit)
|
||||
* [`jj gerrit upload`↴](#jj-gerrit-upload)
|
||||
* [`jj git`↴](#jj-git)
|
||||
* [`jj git clone`↴](#jj-git-clone)
|
||||
* [`jj git export`↴](#jj-git-export)
|
||||
|
|
@ -142,6 +144,7 @@ To get started, see the tutorial [`jj help -k tutorial`].
|
|||
* `evolog` — Show how a change has evolved over time
|
||||
* `file` — File operations
|
||||
* `fix` — Update files with formatting fixes or other changes
|
||||
* `gerrit` — Interact with Gerrit Code Review
|
||||
* `git` — Commands for working with Git remotes and the underlying Git repo
|
||||
* `help` — Print this message or the help of the given subcommand(s)
|
||||
* `interdiff` — Compare the changes of two commits
|
||||
|
|
@ -1196,6 +1199,41 @@ output of the first tool.
|
|||
|
||||
|
||||
|
||||
## `jj gerrit`
|
||||
|
||||
Interact with Gerrit Code Review
|
||||
|
||||
**Usage:** `jj gerrit <COMMAND>`
|
||||
|
||||
###### **Subcommands:**
|
||||
|
||||
* `upload` — Upload changes to Gerrit for code review, or update existing changes
|
||||
|
||||
|
||||
|
||||
## `jj gerrit upload`
|
||||
|
||||
Upload changes to Gerrit for code review, or update existing changes.
|
||||
|
||||
Uploading in a set of revisions to Gerrit creates a single "change" for each revision included in the revset. These changes are then available for review on your Gerrit instance.
|
||||
|
||||
Note: The gerrit commit Id may not match that of your local commit Id, since we add a `Change-Id` footer to the commit message if one does not already exist. This ID is based off the jj Change-Id, but is not the same.
|
||||
|
||||
If a change already exists for a given revision (i.e. it contains the same `Change-Id`), this command will update the contents of the existing change to match.
|
||||
|
||||
Note: this command takes 1-or-more revsets arguments, each of which can resolve to multiple revisions; so you may post trees or ranges of commits to Gerrit for review all at once.
|
||||
|
||||
**Usage:** `jj gerrit upload [OPTIONS]`
|
||||
|
||||
###### **Options:**
|
||||
|
||||
* `-r`, `--revisions <REVISIONS>` — The revset, selecting which revisions are sent in to Gerrit. This can be any arbitrary set of commits. Note that when you push a commit at the head of a stack, all ancestors are pushed too. This means that `jj gerrit upload -r foo` is equivalent to `jj gerrit upload -r 'mutable()::foo`
|
||||
* `-b`, `--remote-branch <REMOTE_BRANCH>` — The location where your changes are intended to land. This should be a branch on the remote
|
||||
* `--remote <REMOTE>` — The Gerrit remote to push to. Can be configured with the `gerrit.remote` repository option as well. This is typically a full SSH URL for your Gerrit instance
|
||||
* `-n`, `--dry-run` — If true, do not actually push the changes to Gerrit
|
||||
|
||||
|
||||
|
||||
## `jj git`
|
||||
|
||||
Commands for working with Git remotes and the underlying Git repo
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ mod test_file_show_command;
|
|||
mod test_file_track_untrack_commands;
|
||||
mod test_fix_command;
|
||||
mod test_generate_md_cli_help;
|
||||
mod test_gerrit_upload;
|
||||
mod test_git_clone;
|
||||
mod test_git_colocated;
|
||||
mod test_git_fetch;
|
||||
|
|
|
|||
89
cli/tests/test_gerrit_upload.rs
Normal file
89
cli/tests/test_gerrit_upload.rs
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
// Copyright 2025 The Jujutsu Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use crate::common::TestEnvironment;
|
||||
use crate::common::create_commit;
|
||||
|
||||
#[test]
|
||||
fn test_gerrit_upload_dryrun() {
|
||||
let test_env = TestEnvironment::default();
|
||||
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
|
||||
let work_dir = test_env.work_dir("repo");
|
||||
|
||||
create_commit(&work_dir, "a", &[]);
|
||||
create_commit(&work_dir, "b", &["a"]);
|
||||
create_commit(&work_dir, "c", &["a"]);
|
||||
let output = work_dir.run_jj(["gerrit", "upload", "-r", "b"]);
|
||||
insta::assert_snapshot!(output, @r###"
|
||||
------- stderr -------
|
||||
Error: No remote specified, and no 'gerrit' remote was found
|
||||
[EOF]
|
||||
[exit status: 1]
|
||||
"###);
|
||||
|
||||
// With remote specified but.
|
||||
test_env.add_config(r#"gerrit.default-remote="origin""#);
|
||||
let output = work_dir.run_jj(["gerrit", "upload", "-r", "b"]);
|
||||
insta::assert_snapshot!(output, @r###"
|
||||
------- stderr -------
|
||||
Error: The remote 'origin' (configured via `gerrit.default-remote`) does not exist
|
||||
[EOF]
|
||||
[exit status: 1]
|
||||
"###);
|
||||
|
||||
let output = work_dir.run_jj(["gerrit", "upload", "-r", "b", "--remote=origin"]);
|
||||
insta::assert_snapshot!(output, @r###"
|
||||
------- stderr -------
|
||||
Error: The remote 'origin' (specified via `--remote`) does not exist
|
||||
[EOF]
|
||||
[exit status: 1]
|
||||
"###);
|
||||
|
||||
let output = work_dir.run_jj([
|
||||
"git",
|
||||
"remote",
|
||||
"add",
|
||||
"origin",
|
||||
"http://example.com/repo/foo",
|
||||
]);
|
||||
insta::assert_snapshot!(output, @"");
|
||||
let output = work_dir.run_jj(["gerrit", "upload", "-r", "b", "--remote=origin"]);
|
||||
insta::assert_snapshot!(output, @r###"
|
||||
------- stderr -------
|
||||
Error: No target branch specified via --remote-branch, and no 'gerrit.default-remote-branch' was found
|
||||
[EOF]
|
||||
[exit status: 1]
|
||||
"###);
|
||||
|
||||
test_env.add_config(r#"gerrit.default-remote-branch="main""#);
|
||||
let output = work_dir.run_jj(["gerrit", "upload", "-r", "b", "--dry-run"]);
|
||||
insta::assert_snapshot!(output, @r###"
|
||||
------- stderr -------
|
||||
|
||||
Found 1 heads to push to Gerrit (remote 'origin'), target branch 'main'
|
||||
|
||||
Dry-run: Would push zsuskuln 123b4d91 b | b
|
||||
[EOF]
|
||||
"###);
|
||||
|
||||
let output = work_dir.run_jj(["gerrit", "upload", "-r", "b", "--dry-run", "-b", "other"]);
|
||||
insta::assert_snapshot!(output, @r###"
|
||||
------- stderr -------
|
||||
|
||||
Found 1 heads to push to Gerrit (remote 'origin'), target branch 'other'
|
||||
|
||||
Dry-run: Would push zsuskuln 123b4d91 b | b
|
||||
[EOF]
|
||||
"###);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue