248 lines · 8.7 KB · Rust
RawDownload
// Copyright 2020-2023 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::fs;use std::io;use std::io::Write;use std::num::NonZeroU32;use std::path::Path;use std::path::PathBuf; use jj_lib::git;use jj_lib::git::GitFetchError;use jj_lib::git::GitFetchStats;use jj_lib::repo::Repo;use jj_lib::str_util::StringPattern;use jj_lib::workspace::Workspace; use crate::cli_util::CommandHelper;use crate::cli_util::WorkspaceCommandHelper;use crate::command_error::cli_error;use crate::command_error::user_error;use crate::command_error::user_error_with_message;use crate::command_error::CommandError;use crate::commands::git::maybe_add_gitignore;use crate::config::write_config_value_to_file;use crate::config::ConfigNamePathBuf;use crate::git_util::get_git_repo;use crate::git_util::map_git_error;use crate::git_util::print_git_import_stats;use crate::git_util::with_remote_git_callbacks;use crate::ui::Ui; /// Create a new repo backed by a clone of a Git repo////// The Git repo will be a bare git repo stored inside the `.jj/` directory.#[derive(clap::Args, Clone, Debug)]pub struct GitCloneArgs {    /// URL or path of the Git repo to clone    #[arg(value_hint = clap::ValueHint::DirPath)]    source: String,    /// Specifies the target directory for the Jujutsu repository clone.    /// If not provided, defaults to a directory named after the last component    /// of the source URL. The full directory path will be created if it    /// doesn't exist.    #[arg(value_hint = clap::ValueHint::DirPath)]    destination: Option<String>,    /// Name of the newly created remote    #[arg(long = "remote", default_value = "origin")]    remote_name: String,    /// Whether or not to colocate the Jujutsu repo with the git repo    #[arg(long)]    colocate: bool,    /// Create a shallow clone of the given depth    #[arg(long)]    depth: Option<NonZeroU32>,} fn absolute_git_source(cwd: &Path, source: &str) -> String {    // Git appears to turn URL-like source to absolute path if local git directory    // exits, and fails because '$PWD/https' is unsupported protocol. Since it would    // be tedious to copy the exact git (or libgit2) behavior, we simply assume a    // source containing ':' is a URL, SSH remote, or absolute path with Windows    // drive letter.    if !source.contains(':') && Path::new(source).exists() {        // It's less likely that cwd isn't utf-8, so just fall back to original source.        cwd.join(source)            .into_os_string()            .into_string()            .unwrap_or_else(|_| source.to_owned())    } else {        source.to_owned()    }} fn clone_destination_for_source(source: &str) -> Option<&str> {    let destination = source.strip_suffix(".git").unwrap_or(source);    let destination = destination.strip_suffix('/').unwrap_or(destination);    destination        .rsplit_once(&['/', '\\', ':'][..])        .map(|(_, name)| name)} fn is_empty_dir(path: &Path) -> bool {    if let Ok(mut entries) = path.read_dir() {        entries.next().is_none()    } else {        false    }} pub fn cmd_git_clone(    ui: &mut Ui,    command: &CommandHelper,    args: &GitCloneArgs,) -> Result<(), CommandError> {    let remote_name = &args.remote_name;    if command.global_args().at_operation.is_some() {        return Err(cli_error("--at-op is not respected"));    }    let source = absolute_git_source(command.cwd(), &args.source);    let wc_path_str = args        .destination        .as_deref()        .or_else(|| clone_destination_for_source(&source))        .ok_or_else(|| user_error("No destination specified and wasn't able to guess it"))?;    let wc_path = command.cwd().join(wc_path_str);     let wc_path_existed = wc_path.exists();    if wc_path_existed && !is_empty_dir(&wc_path) {        return Err(user_error(            "Destination path exists and is not an empty directory",        ));    }     // will create a tree dir in case if was deleted after last check    fs::create_dir_all(&wc_path)        .map_err(|err| user_error_with_message(format!("Failed to create {wc_path_str}"), err))?;     // Canonicalize because fs::remove_dir_all() doesn't seem to like e.g.    // `/some/path/.`    let canonical_wc_path: PathBuf = wc_path        .canonicalize()        .map_err(|err| user_error_with_message(format!("Failed to create {wc_path_str}"), err))?;    let clone_result = do_git_clone(        ui,        command,        args.colocate,        args.depth,        remote_name,        &source,        &canonical_wc_path,    );    if clone_result.is_err() {        let clean_up_dirs = || -> io::Result<()> {            fs::remove_dir_all(canonical_wc_path.join(".jj"))?;            if args.colocate {                fs::remove_dir_all(canonical_wc_path.join(".git"))?;            }            if !wc_path_existed {                fs::remove_dir(&canonical_wc_path)?;            }            Ok(())        };        if let Err(err) = clean_up_dirs() {            writeln!(                ui.warning_default(),                "Failed to clean up {}: {}",                canonical_wc_path.display(),                err            )            .ok();        }    }     let (mut workspace_command, stats) = clone_result?;    if let Some(default_branch) = &stats.default_branch {        // Set repository level `trunk()` alias to the default remote branch.        let config_path = workspace_command.repo_path().join("config.toml");        write_config_value_to_file(            &ConfigNamePathBuf::from_iter(["revset-aliases", "trunk()"]),            format!("{default_branch}@{remote_name}").into(),            &config_path,        )?;        writeln!(            ui.status(),            "Setting the revset alias \"trunk()\" to \"{default_branch}@{remote_name}\""        )?;         let default_branch_remote_ref = workspace_command            .repo()            .view()            .get_remote_bookmark(default_branch, remote_name);        if let Some(commit_id) = default_branch_remote_ref.target.as_normal().cloned() {            let mut checkout_tx = workspace_command.start_transaction();            // For convenience, create local bookmark as Git would do.            checkout_tx                .repo_mut()                .track_remote_bookmark(default_branch, remote_name);            if let Ok(commit) = checkout_tx.repo().store().get_commit(&commit_id) {                checkout_tx.check_out(&commit)?;            }            checkout_tx.finish(ui, "check out git remote's default branch")?;        }    }    Ok(())} fn do_git_clone(    ui: &mut Ui,    command: &CommandHelper,    colocate: bool,    depth: Option<NonZeroU32>,    remote_name: &str,    source: &str,    wc_path: &Path,) -> Result<(WorkspaceCommandHelper, GitFetchStats), CommandError> {    let (workspace, repo) = if colocate {        Workspace::init_colocated_git(command.settings(), wc_path)?    } else {        Workspace::init_internal_git(command.settings(), wc_path)?    };    let git_repo = get_git_repo(repo.store())?;    writeln!(        ui.status(),        r#"Fetching into new repo in "{}""#,        wc_path.display()    )?;    let mut workspace_command = command.for_workable_repo(ui, workspace, repo)?;    maybe_add_gitignore(&workspace_command)?;    git_repo.remote(remote_name, source).unwrap();    let mut fetch_tx = workspace_command.start_transaction();     let stats = with_remote_git_callbacks(ui, None, |cb| {        git::fetch(            fetch_tx.repo_mut(),            &git_repo,            remote_name,            &[StringPattern::everything()],            cb,            &command.settings().git_settings(),            depth,        )    })    .map_err(|err| match err {        GitFetchError::NoSuchRemote(_) => {            panic!("shouldn't happen as we just created the git remote")        }        GitFetchError::GitImportError(err) => CommandError::from(err),        GitFetchError::InternalGitError(err) => map_git_error(err),        GitFetchError::InvalidBranchPattern => {            unreachable!("we didn't provide any globs")        }    })?;    print_git_import_stats(ui, fetch_tx.repo(), &stats.import_stats, true)?;    fetch_tx.finish(ui, "fetch from git remote into empty repo")?;    Ok((workspace_command, stats))}