From 7ed0c245511ac2f147c625acab61118c426934e1 Mon Sep 17 00:00:00 2001 From: Skyler Grey Date: Wed, 8 Oct 2025 21:34:05 +0000 Subject: [PATCH] chore: bump npins dependencies Change-Id: ruvpzzruutxvyovxozosqwzvqmvwpysm I'm here all ~~day~~ until we have auto-PRs set up for tangled --- packetmix/homes/development/jujutsu.nix | 2 +- packetmix/npins/sources.json | 70 +- .../jujutsu/7245-jj-gerrit-upload.patch | 732 ------------------ .../7245-jj-gerrit-upload.patch.license | 4 - packetmix/packages/jujutsu/default.nix | 18 - 5 files changed, 36 insertions(+), 790 deletions(-) delete mode 100644 packetmix/packages/jujutsu/7245-jj-gerrit-upload.patch delete mode 100644 packetmix/packages/jujutsu/7245-jj-gerrit-upload.patch.license delete mode 100644 packetmix/packages/jujutsu/default.nix diff --git a/packetmix/homes/development/jujutsu.nix b/packetmix/homes/development/jujutsu.nix index d6a11168..d82d52f7 100644 --- a/packetmix/homes/development/jujutsu.nix +++ b/packetmix/homes/development/jujutsu.nix @@ -24,7 +24,7 @@ ingredient.scriptfs.enable = true; # used for signing configuration programs.jujutsu = { enable = true; - package = project.packages.jujutsu.result.${system}; + package = project.inputs.nixos-unstable.result.${system}.jujutsu; settings = { aliases = { init = [ diff --git a/packetmix/npins/sources.json b/packetmix/npins/sources.json index 550da1f8..fdbdf079 100644 --- a/packetmix/npins/sources.json +++ b/packetmix/npins/sources.json @@ -61,9 +61,9 @@ }, "branch": "main", "submodules": false, - "revision": "d239f0bcf5e7ed391c8ced32e13aaa4c1324a757", - "url": "https://github.com/bluesky-social/atproto/archive/d239f0bcf5e7ed391c8ced32e13aaa4c1324a757.tar.gz", - "hash": "sha256-PzZhBAGwrjItNGgUTXsP3fceEXlxKWj5+OsaSnK3UbI=" + "revision": "4c15fb47cec26060bff2e710e95869a90c9d7fdd", + "url": "https://github.com/bluesky-social/atproto/archive/4c15fb47cec26060bff2e710e95869a90c9d7fdd.tar.gz", + "hash": "sha256-FhWxdePEeBX2At70YdmjiYT8oXIvJ+ry3VdtfcuBUBU=" }, "catppuccin": { "type": "Git", @@ -74,9 +74,9 @@ }, "branch": "main", "submodules": false, - "revision": "5f7dc8bab8af6ba612ef8dc7cd44e38ba6cfd51a", - "url": "https://github.com/catppuccin/nix/archive/5f7dc8bab8af6ba612ef8dc7cd44e38ba6cfd51a.tar.gz", - "hash": "sha256-aHN6dAD72IsNvNlzU3nbV4DJRb1qPvURgWIzHeYsBbc=" + "revision": "eeada12912d80d04733383d231a9d66172858718", + "url": "https://github.com/catppuccin/nix/archive/eeada12912d80d04733383d231a9d66172858718.tar.gz", + "hash": "sha256-2fzYq/m2PXie5WZO5LhyiZrTIUdUFp1SCLZAwvPL5xo=" }, "collabora-gtimelog": { "type": "Git", @@ -102,10 +102,10 @@ "version_upper_bound": null, "release_prefix": null, "submodules": false, - "version": "v1.19.15", - "revision": "daba1ab7bdf57377fb04e174a7527406c422578d", - "url": "https://api.github.com/repos/9001/copyparty/tarball/refs/tags/v1.19.15", - "hash": "sha256-FjS5UA4jWko/2+lkyzGOVhhZOEA8oTIoQ7GIcRj3ebM=" + "version": "v1.19.16", + "revision": "cd3feaac86123ecf8c73b3201b27da40faf9110c", + "url": "https://api.github.com/repos/9001/copyparty/tarball/refs/tags/v1.19.16", + "hash": "sha256-ZpQInliAuJIT4DJ/X3U0Tjvg+hweqU9FlRfR/UF9YFo=" }, "core": { "type": "Git", @@ -154,9 +154,9 @@ }, "branch": "master", "submodules": false, - "revision": "004753ae6b04c4b18aa07192c1106800aaacf6c3", - "url": "https://github.com/nix-community/home-manager/archive/004753ae6b04c4b18aa07192c1106800aaacf6c3.tar.gz", - "hash": "sha256-CcT3QvZ74NGfM+lSOILcCEeU+SnqXRvl1XCRHenZ0Us=" + "revision": "1a09eb84fa9e33748432a5253102d01251f72d6d", + "url": "https://github.com/nix-community/home-manager/archive/1a09eb84fa9e33748432a5253102d01251f72d6d.tar.gz", + "hash": "sha256-uqbhyXtqMbYIiMqVqUhNdSuh9AEEkiasoK3mIPIVRhk=" }, "impermanence": { "type": "Git", @@ -197,9 +197,9 @@ }, "branch": "main", "submodules": false, - "revision": "1c4e77387ad42686517850da5fb98d8cac11adb7", - "url": "https://git.lix.systems/lix-project/lix/archive/1c4e77387ad42686517850da5fb98d8cac11adb7.tar.gz", - "hash": "sha256-bF0zdh6b56OBlYQ3Lsu+nFk+L5F28Mw7xzeYQU9V964=" + "revision": "42691f0d943b43842e69b76608cbe4416e35e94e", + "url": "https://git.lix.systems/lix-project/lix/archive/42691f0d943b43842e69b76608cbe4416e35e94e.tar.gz", + "hash": "sha256-PthY1vlykNZmuKHQS01Qunut1fdM0imk5Myg3hcr8Oc=" }, "lix-module": { "type": "Git", @@ -298,9 +298,9 @@ }, "branch": "main", "submodules": false, - "revision": "d425163158a96a26924597574316a627d2e982aa", - "url": "https://github.com/sodiboo/niri-flake/archive/d425163158a96a26924597574316a627d2e982aa.tar.gz", - "hash": "sha256-xhUr1oMQwL/8h8xnPi5QxUHRFDHoCofhw8Jy7qTD4BY=" + "revision": "0d12957ebc8e272e3fc3830549edbb1ad63c34d4", + "url": "https://github.com/sodiboo/niri-flake/archive/0d12957ebc8e272e3fc3830549edbb1ad63c34d4.tar.gz", + "hash": "sha256-ZGEBkK8ZQ370ifJO+1TOQ87m9Gmj52uzqcqysd/lolI=" }, "nix-index-database": { "type": "Git", @@ -311,9 +311,9 @@ }, "branch": "main", "submodules": false, - "revision": "ec7a78cb0e098832d8acac091a4df393259c4839", - "url": "https://github.com/nix-community/nix-index-database/archive/ec7a78cb0e098832d8acac091a4df393259c4839.tar.gz", - "hash": "sha256-WZf+FhebP2/1pK2np5xj/NuDjD6fXK2BHnq/tPUN18o=" + "revision": "0ca69684091aa3a6b1fe994c4afeff305b15e915", + "url": "https://github.com/nix-community/nix-index-database/archive/0ca69684091aa3a6b1fe994c4afeff305b15e915.tar.gz", + "hash": "sha256-8NI1SqntLfKl6Q0Luemc3aIboezSJElofUrqipF5g78=" }, "nix-monitored": { "type": "Git", @@ -331,14 +331,14 @@ "nixos-unstable": { "type": "Channel", "name": "nixos-unstable", - "url": "https://releases.nixos.org/nixos/unstable/nixos-25.11pre868392.e9f00bd89398/nixexprs.tar.xz", - "hash": "sha256-Lz9jvhswQu/niKVttNvOds0w+OS+2x63NivPVJng5G4=" + "url": "https://releases.nixos.org/nixos/unstable/nixos-25.11pre873798.c9b6fb798541/nixexprs.tar.xz", + "hash": "sha256-Dzw+6wNlteBG6go9KYZyVjpl6CkzjKzmXrneAF5mqI0=" }, "nixpkgs": { "type": "Channel", "name": "nixos-25.05", - "url": "https://releases.nixos.org/nixos/25.05/nixos-25.05.810656.5b5be50345d4/nixexprs.tar.xz", - "hash": "sha256-1t/IP8E71w1r6aXjX/GBU/ZbbJo3GrSuUjQNYH11gJU=" + "url": "https://releases.nixos.org/nixos/25.05/nixos-25.05.810859.20c4598c84a6/nixexprs.tar.xz", + "hash": "sha256-1WvIF4N7Jj1ppU9ocZZBpsQdiVsHVzVxNt9L2+7FwPM=" }, "npins": { "type": "Git", @@ -349,9 +349,9 @@ }, "branch": "master", "submodules": false, - "revision": "e49bcf58d66fa7f586aff687feae72c23e429672", - "url": "https://github.com/andir/npins/archive/e49bcf58d66fa7f586aff687feae72c23e429672.tar.gz", - "hash": "sha256-ZLrVWa7WU+IUXPAd6mR7B9c6FDZSiGgW8ClfW0SxbMs=" + "revision": "1c1ebf233c30e14ff34472c609ded8eb988753d8", + "url": "https://github.com/andir/npins/archive/1c1ebf233c30e14ff34472c609ded8eb988753d8.tar.gz", + "hash": "sha256-yCaqGejBcWoibCRj2vRrlj5uPI7lyCGS6ivCiGeKNSo=" }, "scriptfs": { "type": "Git", @@ -374,9 +374,9 @@ }, "branch": "master", "submodules": false, - "revision": "32b7c17e266e0da0f0e1e6e6a3821cd8dd3b8362", + "revision": "eaa11ecb2112c90113a8ee49a8aa05be327b13ad", "url": null, - "hash": "sha256-Q/UnCYR8NgO5xoevJJxyCa3ZQnB/5c62F1IBxFKbF3Q=" + "hash": "sha256-lLyX1f4fSG1IYsjcsUKKCAOHpa3ZTFexJMRc/0K2g3c=" }, "treefmt-nix": { "type": "Git", @@ -402,10 +402,10 @@ "version_upper_bound": null, "release_prefix": null, "submodules": false, - "version": "v1.1.0", - "revision": "226f7f5c2d66be2acc5617cf37943075cb2ba693", - "url": "https://api.github.com/repos/abenz1267/walker/tarball/refs/tags/v1.1.0", - "hash": "sha256-fjd8N5lnZ1ee3xRCUQ2eHXiIncwe5DGj8N3MDfrYMeE=" + "version": "v2.2.0", + "revision": "cbacfea97aaf2a3408db45e499411df19f29a0c1", + "url": "https://api.github.com/repos/abenz1267/walker/tarball/refs/tags/v2.2.0", + "hash": "sha256-cSRd4ncUWjB59nRqY0X0eXioOIL7q7PwgOQggE54lTI=" } }, "version": 6 diff --git a/packetmix/packages/jujutsu/7245-jj-gerrit-upload.patch b/packetmix/packages/jujutsu/7245-jj-gerrit-upload.patch deleted file mode 100644 index 821c40d6..00000000 --- a/packetmix/packages/jujutsu/7245-jj-gerrit-upload.patch +++ /dev/null @@ -1,732 +0,0 @@ -From a85628df2f2bc1b46374de546052f624b09f172b Mon Sep 17 00:00:00 2001 -From: Austin Seipp -Date: Thu, 18 Jan 2024 00:35:09 -0600 -Subject: [PATCH] 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 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 upload -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 ---- - CHANGELOG.md | 2 + - cli/src/commands/gerrit/mod.rs | 57 +++++ - cli/src/commands/gerrit/upload.rs | 384 +++++++++++++++++++++++++++++++ - cli/src/commands/mod.rs | 7 + - cli/src/config-schema.json | 14 ++ - cli/tests/cli-reference@.md.snap | 38 +++ - cli/tests/runner.rs | 1 + - cli/tests/test_gerrit_upload.rs | 89 +++++++ - 8 files changed, 592 insertions(+) - create mode 100644 cli/src/commands/gerrit/mod.rs - create mode 100644 cli/src/commands/gerrit/upload.rs - create mode 100644 cli/tests/test_gerrit_upload.rs - -diff --git a/CHANGELOG.md b/CHANGELOG.md -index 267b5ed303..9bc1029fcf 100644 ---- a/CHANGELOG.md -+++ b/CHANGELOG.md -@@ -107,6 +107,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - * The new command `jj redo` can progressively redo operations that were - previously undone by multiple calls to `jj undo`. - -+* Gerrit support implemented with the new command `jj gerrit upload` -+ - ### Fixed bugs - - * `jj git clone` now correctly fetches all tags, unless `--fetch-tags` is -diff --git a/cli/src/commands/gerrit/mod.rs b/cli/src/commands/gerrit/mod.rs -new file mode 100644 -index 0000000000..60abdb6702 ---- /dev/null -+++ b/cli/src/commands/gerrit/mod.rs -@@ -0,0 +1,57 @@ -+// 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 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. This change is then available for -+ /// review on your Gerrit instance. -+ /// -+ /// 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. -+ /// -+ /// 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. -+ 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_upload(ui, command, review), -+ } -+} -+ -+mod upload; -diff --git a/cli/src/commands/gerrit/upload.rs b/cli/src/commands/gerrit/upload.rs -new file mode 100644 -index 0000000000..88c3ca5e97 ---- /dev/null -+++ b/cli/src/commands/gerrit/upload.rs -@@ -0,0 +1,384 @@ -+// 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 std::io::Write as _; -+use std::rc::Rc; -+use std::sync::Arc; -+ -+use bstr::BStr; -+use indexmap::IndexMap; -+use itertools::Itertools as _; -+use jj_lib::backend::CommitId; -+use jj_lib::commit::Commit; -+use jj_lib::commit::CommitIteratorExt as _; -+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_commit_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; -+ -+#[derive(clap::Args, Clone, Debug)] -+pub struct UploadArgs { -+ /// The revset, selecting which commits are sent in to Gerrit. This can be -+ /// any arbitrary set of commits; they will be modified to include a -+ /// `Change-Id` footer if one does not already exist, and then sent off to -+ /// Gerrit for review. -+ #[arg(long, short = 'r')] -+ revisions: Vec, -+ -+ /// The location where your changes are intended to land. This should be -+ /// an upstream branch. -+ #[arg(long = "remote-branch", short = 'b')] -+ remote_branch: Option, -+ -+ /// 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, -+ -+ /// If true, do not actually add `Change-Id`s to commits, and do not push -+ /// the changes to Gerrit. -+ #[arg(long = "dry-run", short = 'n')] -+ dry_run: bool, -+} -+ -+/// calculate push remote. The logic is: -+/// 1. If the user specifies `--remote`, use that -+/// 2. If the user has 'gerrit.remote' configured, use that -+/// 3. If there is a default push remote, use that -+/// 4. If the user has a remote named 'gerrit', use that -+/// 5. otherwise, bail out -+fn calculate_push_remote( -+ store: &Arc, -+ config: &UserSettings, -+ remote: Option, -+) -> Result { -+ 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); -+ } -+ 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, -+) -> Result { -+ // 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_upload(ui: &mut Ui, command: &CommandHelper, upload: &UploadArgs) -> Result<(), CommandError> { -+ let mut workspace_command = command.workspace_helper(ui)?; -+ -+ let revisions: Vec<_> = workspace_command -+ .parse_union_revsets(ui, &upload.revisions)? -+ .evaluate_to_commits()? -+ .try_collect()?; -+ if revisions.is_empty() { -+ writeln!(ui.status(), "No revisions to upload.")?; -+ return Ok(()); -+ } -+ -+ if revisions -+ .iter() -+ .any(|commit| commit.id() == workspace_command.repo().store().root_commit_id()) -+ { -+ return Err(user_error("Cannot upload the virtual 'root()' commit")); -+ } -+ -+ workspace_command.check_rewritable(revisions.iter().ids())?; -+ -+ // If you have the changes main -> A -> B, and then run `jj gerrit upload 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 = workspace_command -+ .attach_revset_evaluator( -+ // I'm unsure, but this *might* have significant performance -+ // implications. If so, we can change it to a maximum depth. -+ Rc::new(RevsetExpression::Difference( -+ // Unfortunately, DagRange{root: immutable_heads, heads: commits} -+ // doesn't work if you're, for example, working on top of an -+ // immutable commit that isn't in immutable_heads(). -+ Rc::new(RevsetExpression::Ancestors { -+ heads: RevsetExpression::commits( -+ revisions.iter().ids().cloned().collect::>(), -+ ), -+ generation: jj_lib::revset::GENERATION_RANGE_FULL, -+ parents_range: jj_lib::revset::PARENTS_RANGE_FULL, -+ }), -+ workspace_command.env().immutable_expression().clone(), -+ )), -+ ) -+ .evaluate_to_commits()? -+ .try_collect()?; -+ -+ let mut tx = workspace_command.start_transaction(); -+ let base_repo = tx.base_repo().clone(); -+ let store = base_repo.store(); -+ -+ let old_heads = base_repo -+ .index() -+ .heads(&mut revisions.iter().ids()) -+ .map_err(internal_error)?; -+ -+ let git_settings = command.settings().git_settings()?; -+ let remote = calculate_push_remote(store, command.settings(), upload.remote.clone())?; -+ let remote_branch = calculate_push_ref(command.settings(), upload.remote_branch.clone())?; -+ -+ // immediately error and reject any discardable commits, i.e. the -+ // the empty wcc -+ for commit in &to_upload { -+ if commit.is_discardable(tx.repo_mut())? { -+ return Err(user_error_with_hint( -+ format!( -+ "Refusing to upload commit {} because it is an empty commit with no description", -+ short_commit_hash(commit.id()) -+ ), -+ "Perhaps you squashed then ran upload? Maybe you meant to upload the parent commit \ -+ instead (eg. @-)", -+ )); -+ } -+ } -+ -+ let mut old_to_new: IndexMap = IndexMap::new(); -+ for commit_id in to_upload.iter().map(|c| c.id()).rev() { -+ let original_commit = store.get_commit(commit_id).unwrap(); -+ let description = original_commit.description().to_owned(); -+ let trailers = parse_description_trailers(&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 commit {}", -+ short_commit_hash(commit_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 commit {}", -+ short_commit_hash(original_commit.id()), -+ )?; -+ } -+ -+ // map the old commit to itself -+ old_to_new.insert(original_commit.id().clone(), original_commit.clone()); -+ 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", -+ description.trim(), -+ if trailers.is_empty() { "\n\n" } else { "\n" }, -+ gerrit_change_id -+ ); -+ -+ let new_parents = original_commit -+ .parents() -+ .map(|parent| { -+ let p = parent.unwrap(); -+ if let Some(rewritten_parent) = old_to_new.get(p.id()) { -+ rewritten_parent -+ } else { -+ &p -+ } -+ .id() -+ .clone() -+ }) -+ .collect(); -+ -+ // rewrite the set of parents to point to the commits that were -+ // previously rewritten in toposort order -+ // -+ // TODO FIXME (aseipp): this whole dance with toposorting, calculating -+ // new_parents, and then doing rewrite_commit is roughly equivalent to -+ // what we do in duplicate.rs as well. we should probably refactor this? -+ 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.clone()); -+ } -+ 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 upload.dry_run { -+ "Dry-run: Would push " -+ } else { -+ "Pushing " -+ } -+ )?; -+ // We have to write the old commit here, because the until we finish -+ // the transaction (which we don't), the new commit is labelled as -+ // "hidden". -+ tx.base_workspace_helper().write_commit_summary( -+ ui.stderr_formatter().as_mut(), -+ &store.get_commit(head).unwrap(), -+ )?; -+ writeln!(ui.stderr())?; -+ -+ if upload.dry_run { -+ continue; -+ } -+ -+ let new_commit = store -+ .get_commit(old_to_new.get(head).unwrap().id()) -+ .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(()) -+} -diff --git a/cli/src/commands/mod.rs b/cli/src/commands/mod.rs -index cdf3c9c3ad..cb7b4ca185 100644 ---- a/cli/src/commands/mod.rs -+++ b/cli/src/commands/mod.rs -@@ -30,6 +30,8 @@ mod evolog; - mod file; - mod fix; - #[cfg(feature = "git")] -+mod gerrit; -+#[cfg(feature = "git")] - mod git; - mod help; - mod interdiff; -@@ -115,6 +117,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), -@@ -180,6 +185,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), -diff --git a/cli/src/config-schema.json b/cli/src/config-schema.json -index 887c34e2ba..d15b334ecf 100644 ---- a/cli/src/config-schema.json -+++ b/cli/src/config-schema.json -@@ -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-tools": { - "type": "object", - "description": "Tables of custom options to pass to the given merge tool (selected in ui.merge-editor)", -diff --git a/cli/tests/cli-reference@.md.snap b/cli/tests/cli-reference@.md.snap -index a97a0ffc55..dff8bcbe37 100644 ---- a/cli/tests/cli-reference@.md.snap -+++ b/cli/tests/cli-reference@.md.snap -@@ -45,6 +45,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) -@@ -139,6 +141,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 -@@ -1177,6 +1180,41 @@ output of the first tool. - - - -+## `jj gerrit` -+ -+Interact with Gerrit Code Review -+ -+**Usage:** `jj gerrit ` -+ -+###### **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. This change is then available for review on your Gerrit instance. -+ -+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. -+ -+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 ` — The revset, selecting which commits are sent in to Gerrit. This can be any arbitrary set of commits; they will be modified to include a `Change-Id` footer if one does not already exist, and then sent off to Gerrit for review -+* `-b`, `--remote-branch ` — The location where your changes are intended to land. This should be an upstream branch -+* `--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 add `Change-Id`s to commits, and do not push the changes to Gerrit -+ -+ -+ - ## `jj git` - - Commands for working with Git remotes and the underlying Git repo -diff --git a/cli/tests/runner.rs b/cli/tests/runner.rs -index 88c1ca2319..f228da5e70 100644 ---- a/cli/tests/runner.rs -+++ b/cli/tests/runner.rs -@@ -37,6 +37,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; -diff --git a/cli/tests/test_gerrit_upload.rs b/cli/tests/test_gerrit_upload.rs -new file mode 100644 -index 0000000000..71543cedd8 ---- /dev/null -+++ b/cli/tests/test_gerrit_upload.rs -@@ -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] -+ "###); -+} - diff --git a/packetmix/packages/jujutsu/7245-jj-gerrit-upload.patch.license b/packetmix/packages/jujutsu/7245-jj-gerrit-upload.patch.license deleted file mode 100644 index 72df7956..00000000 --- a/packetmix/packages/jujutsu/7245-jj-gerrit-upload.patch.license +++ /dev/null @@ -1,4 +0,0 @@ -SPDX-FileCopyrightText: 2025 Austin Seipp -SPDX-FileCopyrightText: 2025 Matt Stark - -SPDX-License-Identifier: Apache-2.0 diff --git a/packetmix/packages/jujutsu/default.nix b/packetmix/packages/jujutsu/default.nix deleted file mode 100644 index 1630e09d..00000000 --- a/packetmix/packages/jujutsu/default.nix +++ /dev/null @@ -1,18 +0,0 @@ -# SPDX-FileCopyrightText: 2025 FreshlyBakedCake -# -# SPDX-License-Identifier: MIT - -{ config, ... }: -{ - config.packages.jujutsu = { - systems = [ "x86_64-linux" ]; - package = - { - system, - ... - }: - config.inputs.nixos-unstable.result.${system}.jujutsu.overrideAttrs (prevAttrs: { - patches = (prevAttrs.patches or [ ]) ++ [ ./7245-jj-gerrit-upload.patch ]; - }); - }; -} -- 2.43.0