278 lines · 9.3 KB · Rust
RawDownload
// Copyright 2026 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::io::Write as _;use std::path::Path; use clap_complete::ArgValueCompleter;use jj_lib::backend::CommitId;use jj_lib::backend::TreeValue;use jj_lib::merge::Merge;use jj_lib::merge::MergedTreeValue;use jj_lib::merged_tree_builder::MergedTreeBuilder;use jj_lib::object_id::ObjectId as _;use jj_lib::repo::Repo as _;use tracing::instrument; use crate::cli_util::CommandHelper;use crate::cli_util::RevisionArg;use crate::command_error::CommandError;use crate::command_error::internal_error;use crate::command_error::user_error;use crate::command_error::user_error_with_message;use crate::complete;use crate::ui::Ui; /// Manage Git submodules.#[derive(clap::Subcommand, Clone, Debug)]pub enum SubmoduleCommand {    Bind(SubmoduleBindArgs),} pub async fn cmd_submodule(    ui: &mut Ui,    command: &CommandHelper,    subcommand: &SubmoduleCommand,) -> Result<(), CommandError> {    match subcommand {        SubmoduleCommand::Bind(args) => cmd_submodule_bind(ui, command, args).await,    }} /// Explicitly update a submodule gitlink.////// The submodule revision is resolved in the submodule repository at PATH. The/// selected superproject revision is then rewritten so PATH stores that commit/// as a `160000` Git submodule tree entry. The superproject working-copy/// snapshot does not update submodule gitlinks automatically.#[derive(clap::Args, Clone, Debug)]pub struct SubmoduleBindArgs {    /// Submodule path to update    #[arg(value_name = "PATH", value_hint = clap::ValueHint::DirPath)]    #[arg(add = ArgValueCompleter::new(complete::submodule_paths))]    path: String,     /// Revision to resolve in the submodule repository    #[arg(long, short, value_name = "SUBMODULE_REV")]    #[arg(add = ArgValueCompleter::new(complete::submodule_revision))]    revision: String,     /// Superproject revision to rewrite    #[arg(long, value_name = "REVSET")]    #[arg(add = ArgValueCompleter::new(complete::revset_expression_mutable))]    change: Option<RevisionArg>,} #[instrument(skip_all)]async fn cmd_submodule_bind(    ui: &mut Ui,    command: &CommandHelper,    args: &SubmoduleBindArgs,) -> Result<(), CommandError> {    let mut workspace_command = command.workspace_helper(ui).await?;    jj_lib::git::get_git_backend(workspace_command.repo().store())?;     let path = workspace_command.parse_file_path(&args.path)?;    if path.is_root() {        return Err(user_error("Cannot bind the root path as a submodule"));    }    let ui_path = workspace_command.format_file_path(&path);     let target_superproject_commit = workspace_command        .resolve_single_rev(ui, args.change.as_ref().unwrap_or(&RevisionArg::AT))        .await?;    workspace_command        .check_rewritable([target_superproject_commit.id()])        .await?;     let old_value = target_superproject_commit.tree().path_value(&path).await?;    let old_gitlink = inspect_existing_submodule_gitlink(&old_value, &ui_path)?;    let submodule_fs_path = path        .to_fs_path(workspace_command.workspace_root())        .map_err(internal_error)?;    let target_commit_id =        resolve_submodule_revision(ui, command, &submodule_fs_path, &args.revision, &ui_path)            .await?;    if old_gitlink.as_normal() == Some(&target_commit_id) {        writeln!(ui.status(), "Nothing changed.")?;        return Ok(());    }     let mut tree_builder = MergedTreeBuilder::new(target_superproject_commit.tree());    tree_builder.set_or_remove(        path.clone(),        Merge::normal(TreeValue::GitSubmodule(target_commit_id.clone())),    );    let new_tree = tree_builder.write_tree().await?;     let mut tx = workspace_command.start_transaction();    let new_commit = tx        .repo_mut()        .rewrite_commit(&target_superproject_commit)        .set_tree(new_tree)        .write()        .await?;    let num_rebased = tx.repo_mut().rebase_descendants().await?;    if let Some(mut formatter) = ui.status_formatter() {        writeln!(formatter, "Updated Git submodule {ui_path}:")?;        match &old_gitlink {            ExistingSubmoduleGitlink::Normal(old_commit_id) => {                writeln!(formatter, "  old: {old_commit_id}")?;            }            ExistingSubmoduleGitlink::Conflicted => {                writeln!(formatter, "  old: conflicted")?;            }        }        writeln!(formatter, "  new: {target_commit_id}")?;        write!(formatter, "Rewritten superproject commit: ")?;        tx.write_commit_summary(formatter.as_mut(), &new_commit)?;        writeln!(formatter)?;        if num_rebased > 0 {            writeln!(formatter, "Rebased {num_rebased} descendant commits")?;        }    }    tx.finish(        ui,        format!("update git submodule {ui_path} to {target_commit_id}"),    )    .await?;    Ok(())} enum ExistingSubmoduleGitlink {    Normal(CommitId),    Conflicted,} impl ExistingSubmoduleGitlink {    fn as_normal(&self) -> Option<&CommitId> {        match self {            Self::Normal(commit_id) => Some(commit_id),            Self::Conflicted => None,        }    }} fn inspect_existing_submodule_gitlink(    value: &MergedTreeValue,    ui_path: &str,) -> Result<ExistingSubmoduleGitlink, CommandError> {    match value.as_resolved() {        Some(Some(TreeValue::GitSubmodule(commit_id))) => {            return Ok(ExistingSubmoduleGitlink::Normal(commit_id.clone()));        }        Some(Some(_)) => return Err(user_error(format!("Path {ui_path} is not a Git submodule"))),        Some(None) => {            return Err(user_error(format!("Submodule path {ui_path} is absent"))                .hinted("Only existing Git submodule gitlinks can be updated."));        }        None => {}    }     let mut has_submodule = false;    let mut has_non_submodule = false;    for tree_value in value.iter().flatten() {        match tree_value {            TreeValue::GitSubmodule(_) => has_submodule = true,            TreeValue::File { .. } | TreeValue::Symlink(_) | TreeValue::Tree(_) => {                has_non_submodule = true;            }        }    }    if has_submodule && !has_non_submodule {        Ok(ExistingSubmoduleGitlink::Conflicted)    } else {        Err(user_error(format!(            "Submodule path {ui_path} is not a Git submodule conflict"        ))        .hinted("Only conflicts involving Git submodule gitlinks can be resolved."))    }} async fn resolve_submodule_revision(    ui: &Ui,    command: &CommandHelper,    submodule_path: &Path,    revision: &str,    ui_path: &str,) -> Result<jj_lib::backend::CommitId, CommandError> {    if submodule_path.join(".jj").exists() {        resolve_submodule_jj_revision(ui, command, submodule_path, revision).await    } else {        resolve_submodule_git_revision(submodule_path, revision, ui_path)    }} async fn resolve_submodule_jj_revision(    ui: &Ui,    command: &CommandHelper,    submodule_path: &Path,    revision: &str,) -> Result<jj_lib::backend::CommitId, CommandError> {    let submodule_workspace_command = command        .workspace_helper_at_head_no_snapshot(ui, submodule_path)        .await?;    let commit = submodule_workspace_command        .resolve_single_rev(ui, &RevisionArg::from(revision.to_owned()))        .await?;    parse_git_submodule_commit_id(&commit.id().hex(), revision)} fn resolve_submodule_git_revision(    submodule_path: &Path,    revision: &str,    ui_path: &str,) -> Result<jj_lib::backend::CommitId, CommandError> {    let git_repo = gix::open(submodule_path).map_err(|err| {        user_error_with_message(format!("Failed to open Git submodule at {ui_path}"), err)    })?;    let id = git_repo        .rev_parse_single(revision)        .map_err(|err| revision_resolution_error("gix", revision, ui_path, err))?;    let object = id        .object()        .map_err(|err| revision_resolution_error("gix", revision, ui_path, err))?;    let commit = object        .peel_to_commit()        .map_err(|err| revision_resolution_error("gix", revision, ui_path, err))?;    parse_git_submodule_commit_id(&commit.id.to_string(), revision)} fn parse_git_submodule_commit_id(    hex: &str,    revision: &str,) -> Result<jj_lib::backend::CommitId, CommandError> {    if hex.len() != 40 {        return Err(user_error(format!(            "Only SHA-1 Git submodule commits are supported, but {revision:?} resolved to {hex:?}"        )));    }    jj_lib::backend::CommitId::try_from_hex(hex).ok_or_else(|| {        user_error(format!(            "Resolved invalid commit id {hex:?} for submodule revision {revision:?}"        ))    })} fn revision_resolution_error(    tool: &str,    revision: &str,    ui_path: &str,    err: impl std::fmt::Display,) -> CommandError {    user_error(format!(        "Failed to resolve submodule revision {revision:?} at {ui_path} with {tool}"    ))    .hinted(err.to_string())}

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t