1062 lines · 37.4 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::collections::HashSet;use std::io::Write;use std::ops::Deref;use std::path::{Path, PathBuf};use std::{fmt, fs, io}; use clap::{ArgGroup, Subcommand};use itertools::Itertools;use jj_lib::backend::TreeValue;use jj_lib::git::{    self, parse_gitmodules, GitBranchPushTargets, GitFetchError, GitFetchStats, GitPushError,};use jj_lib::object_id::ObjectId;use jj_lib::op_store::RefTarget;use jj_lib::refs::{    classify_branch_push_action, BranchPushAction, BranchPushUpdate, TrackingRefPair,};use jj_lib::repo::Repo;use jj_lib::repo_path::RepoPath;use jj_lib::revset::{self, RevsetExpression, RevsetIteratorExt as _};use jj_lib::settings::{ConfigResultExt as _, UserSettings};use jj_lib::str_util::StringPattern;use jj_lib::view::View;use jj_lib::workspace::Workspace;use maplit::hashset; use crate::cli_util::{    parse_string_pattern, resolve_multiple_nonempty_revsets, short_change_hash, short_commit_hash,    user_error, user_error_with_hint, CommandError, CommandHelper, RevisionArg,    WorkspaceCommandHelper,};use crate::git_util::{    get_git_repo, print_failed_git_export, print_git_import_stats, with_remote_git_callbacks,};use crate::ui::Ui; /// Commands for working with the underlying Git repo////// For a comparison with Git, including a table of commands, see/// https://github.com/martinvonz/jj/blob/main/docs/git-comparison.md.#[derive(Subcommand, Clone, Debug)]pub enum GitCommand {    #[command(subcommand)]    Remote(GitRemoteCommand),    Fetch(GitFetchArgs),    Clone(GitCloneArgs),    Push(GitPushArgs),    Import(GitImportArgs),    Export(GitExportArgs),    #[command(subcommand, hide = true)]    Submodule(GitSubmoduleCommand),} /// Manage Git remotes////// The Git repo will be a bare git repo stored inside the `.jj/` directory.#[derive(Subcommand, Clone, Debug)]pub enum GitRemoteCommand {    Add(GitRemoteAddArgs),    Remove(GitRemoteRemoveArgs),    Rename(GitRemoteRenameArgs),    List(GitRemoteListArgs),} /// Add a Git remote#[derive(clap::Args, Clone, Debug)]pub struct GitRemoteAddArgs {    /// The remote's name    remote: String,    /// The remote's URL    url: String,} /// Remove a Git remote and forget its branches#[derive(clap::Args, Clone, Debug)]pub struct GitRemoteRemoveArgs {    /// The remote's name    remote: String,} /// Rename a Git remote#[derive(clap::Args, Clone, Debug)]pub struct GitRemoteRenameArgs {    /// The name of an existing remote    old: String,    /// The desired name for `old`    new: String,} /// List Git remotes#[derive(clap::Args, Clone, Debug)]pub struct GitRemoteListArgs {} /// Fetch from a Git remote#[derive(clap::Args, Clone, Debug)]pub struct GitFetchArgs {    /// Fetch only some of the branches    ///    /// By default, the specified name matches exactly. Use `glob:` prefix to    /// expand `*` as a glob. The other wildcard characters aren't supported.    #[arg(long, default_value = "glob:*", value_parser = parse_string_pattern)]    branch: Vec<StringPattern>,    /// The remote to fetch from (only named remotes are supported, can be    /// repeated)    #[arg(long = "remote", value_name = "remote")]    remotes: Vec<String>,    /// Fetch from all remotes    #[arg(long, conflicts_with = "remotes")]    all_remotes: bool,} /// 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,    /// The directory to write the Jujutsu repo to    #[arg(value_hint = clap::ValueHint::DirPath)]    destination: Option<String>,    /// Whether or not to colocate the Jujutsu repo with the git repo    #[arg(long)]    colocate: bool,} /// Push to a Git remote////// By default, pushes any branches pointing to/// `remote_branches(remote=<remote>)..@`. Use `--branch` to push specific/// branches. Use `--all` to push all branches. Use `--change` to generate/// branch names based on the change IDs of specific commits.#[derive(clap::Args, Clone, Debug)]#[command(group(ArgGroup::new("specific").args(&["branch", "change", "revisions"]).multiple(true)))]#[command(group(ArgGroup::new("what").args(&["all", "deleted"]).conflicts_with("specific")))]pub struct GitPushArgs {    /// The remote to push to (only named remotes are supported)    #[arg(long)]    remote: Option<String>,    /// Push only this branch (can be repeated)    ///    /// By default, the specified name matches exactly. Use `glob:` prefix to    /// select branches by wildcard pattern. For details, see    /// https://github.com/martinvonz/jj/blob/main/docs/revsets.md#string-patterns.    #[arg(long, short, value_parser = parse_string_pattern)]    branch: Vec<StringPattern>,    /// Push all branches (including deleted branches)    #[arg(long)]    all: bool,    /// Push all deleted branches    #[arg(long)]    deleted: bool,    /// Push branches pointing to these commits    #[arg(long, short)]    revisions: Vec<RevisionArg>,    /// Push this commit by creating a branch based on its change ID (can be    /// repeated)    #[arg(long, short)]    change: Vec<RevisionArg>,    /// Only display what will change on the remote    #[arg(long)]    dry_run: bool,} /// Update repo with changes made in the underlying Git repo#[derive(clap::Args, Clone, Debug)]pub struct GitImportArgs {} /// Update the underlying Git repo with changes made in the repo#[derive(clap::Args, Clone, Debug)]pub struct GitExportArgs {} /// FOR INTERNAL USE ONLY Interact with git submodules#[derive(Subcommand, Clone, Debug)]pub enum GitSubmoduleCommand {    /// Print the relevant contents from .gitmodules. For debugging purposes    /// only.    PrintGitmodules(GitSubmodulePrintGitmodulesArgs),} /// Print debugging info about Git submodules#[derive(clap::Args, Clone, Debug)]#[command(hide = true)]pub struct GitSubmodulePrintGitmodulesArgs {    /// Read .gitmodules from the given revision.    #[arg(long, short = 'r', default_value = "@")]    revisions: RevisionArg,} fn make_branch_term(branch_names: &[impl fmt::Display]) -> String {    match branch_names {        [branch_name] => format!("branch {}", branch_name),        branch_names => format!("branches {}", branch_names.iter().join(", ")),    }} fn map_git_error(err: git2::Error) -> CommandError {    if err.class() == git2::ErrorClass::Ssh {        let hint =            if err.code() == git2::ErrorCode::Certificate && std::env::var_os("HOME").is_none() {                "The HOME environment variable is not set, and might be required for Git to \                 successfully load certificates. Try setting it to the path of a directory that \                 contains a `.ssh` directory."            } else {                "Jujutsu uses libssh2, which doesn't respect ~/.ssh/config. Does `ssh -F \                 /dev/null` to the host work?"            };         user_error_with_hint(err.to_string(), hint)    } else {        user_error(err.to_string())    }} pub fn maybe_add_gitignore(workspace_command: &WorkspaceCommandHelper) -> Result<(), CommandError> {    if workspace_command.working_copy_shared_with_git() {        std::fs::write(            workspace_command                .workspace_root()                .join(".jj")                .join(".gitignore"),            "/*\n",        )        .map_err(|e| user_error(format!("Failed to write .jj/.gitignore file: {e}")))    } else {        Ok(())    }} fn cmd_git_remote_add(    ui: &mut Ui,    command: &CommandHelper,    args: &GitRemoteAddArgs,) -> Result<(), CommandError> {    let workspace_command = command.workspace_helper(ui)?;    let repo = workspace_command.repo();    let git_repo = get_git_repo(repo.store())?;    git::add_remote(&git_repo, &args.remote, &args.url)?;    Ok(())} fn cmd_git_remote_remove(    ui: &mut Ui,    command: &CommandHelper,    args: &GitRemoteRemoveArgs,) -> Result<(), CommandError> {    let mut workspace_command = command.workspace_helper(ui)?;    let repo = workspace_command.repo();    let git_repo = get_git_repo(repo.store())?;    let mut tx = workspace_command.start_transaction();    git::remove_remote(tx.mut_repo(), &git_repo, &args.remote)?;    if tx.mut_repo().has_changes() {        tx.finish(ui, format!("remove git remote {}", &args.remote))    } else {        Ok(()) // Do not print "Nothing changed."    }} fn cmd_git_remote_rename(    ui: &mut Ui,    command: &CommandHelper,    args: &GitRemoteRenameArgs,) -> Result<(), CommandError> {    let mut workspace_command = command.workspace_helper(ui)?;    let repo = workspace_command.repo();    let git_repo = get_git_repo(repo.store())?;    let mut tx = workspace_command.start_transaction();    git::rename_remote(tx.mut_repo(), &git_repo, &args.old, &args.new)?;    if tx.mut_repo().has_changes() {        tx.finish(            ui,            format!("rename git remote {} to {}", &args.old, &args.new),        )    } else {        Ok(()) // Do not print "Nothing changed."    }} fn cmd_git_remote_list(    ui: &mut Ui,    command: &CommandHelper,    _args: &GitRemoteListArgs,) -> Result<(), CommandError> {    let workspace_command = command.workspace_helper(ui)?;    let repo = workspace_command.repo();    let git_repo = get_git_repo(repo.store())?;    for remote_name in git_repo.remotes()?.iter().flatten() {        let remote = git_repo.find_remote(remote_name)?;        writeln!(            ui.stdout(),            "{} {}",            remote_name,            remote.url().unwrap_or("<no URL>")        )?;    }    Ok(())} #[tracing::instrument(skip(ui, command))]fn cmd_git_fetch(    ui: &mut Ui,    command: &CommandHelper,    args: &GitFetchArgs,) -> Result<(), CommandError> {    let mut workspace_command = command.workspace_helper(ui)?;    let git_repo = get_git_repo(workspace_command.repo().store())?;    let remotes = if args.all_remotes {        get_all_remotes(&git_repo)?    } else if args.remotes.is_empty() {        get_default_fetch_remotes(ui, command.settings(), &git_repo)?    } else {        args.remotes.clone()    };    let mut tx = workspace_command.start_transaction();    for remote in &remotes {        let stats = with_remote_git_callbacks(ui, |cb| {            git::fetch(                tx.mut_repo(),                &git_repo,                remote,                &args.branch,                cb,                &command.settings().git_settings(),            )        })        .map_err(|err| match err {            GitFetchError::InvalidBranchPattern => {                if args                    .branch                    .iter()                    .any(|pattern| pattern.as_exact().map_or(false, |s| s.contains('*')))                {                    user_error_with_hint(                        err.to_string(),                        "Prefix the pattern with `glob:` to expand `*` as a glob",                    )                } else {                    user_error(err.to_string())                }            }            GitFetchError::GitImportError(err) => err.into(),            GitFetchError::InternalGitError(err) => map_git_error(err),            _ => user_error(err.to_string()),        })?;        print_git_import_stats(ui, &stats.import_stats)?;    }    tx.finish(        ui,        format!("fetch from git remote(s) {}", remotes.iter().join(",")),    )?;    Ok(())} fn get_single_remote(git_repo: &git2::Repository) -> Result<Option<String>, CommandError> {    let git_remotes = git_repo.remotes()?;    Ok(match git_remotes.len() {        1 => git_remotes.get(0).map(ToOwned::to_owned),        _ => None,    })} const DEFAULT_REMOTE: &str = "origin"; fn get_default_fetch_remotes(    ui: &Ui,    settings: &UserSettings,    git_repo: &git2::Repository,) -> Result<Vec<String>, CommandError> {    const KEY: &str = "git.fetch";    if let Ok(remotes) = settings.config().get(KEY) {        Ok(remotes)    } else if let Some(remote) = settings.config().get_string(KEY).optional()? {        Ok(vec![remote])    } else if let Some(remote) = get_single_remote(git_repo)? {        // if nothing was explicitly configured, try to guess        if remote != DEFAULT_REMOTE {            writeln!(                ui.hint(),                "Fetching from the only existing remote: {}",                remote            )?;        }        Ok(vec![remote])    } else {        Ok(vec![DEFAULT_REMOTE.to_owned()])    }} fn get_all_remotes(git_repo: &git2::Repository) -> Result<Vec<String>, CommandError> {    let git_remotes = git_repo.remotes()?;    Ok(git_remotes        .iter()        .filter_map(|x| x.map(ToOwned::to_owned))        .collect())} 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    }} fn cmd_git_clone(    ui: &mut Ui,    command: &CommandHelper,    args: &GitCloneArgs,) -> Result<(), CommandError> {    let remote_name = "origin";    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 = match fs::create_dir(&wc_path) {        Ok(()) => false,        Err(err) if err.kind() == io::ErrorKind::AlreadyExists => true,        Err(err) => return Err(user_error(format!("Failed to create {wc_path_str}: {err}"))),    };    if wc_path_existed && !is_empty_dir(&wc_path) {        return Err(user_error(            "Destination path exists and is not an empty directory",        ));    }     // 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(format!("Failed to create {wc_path_str}: {err}")))?;    let clone_result = do_git_clone(        ui,        command,        args.colocate,        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(),                "Failed to clean up {}: {}",                canonical_wc_path.display(),                err            )            .ok();

Lines 1–500 of 1062.