1297 lines · 46.3 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::sync::Arc;use std::{fmt, fs, io}; use clap::{ArgGroup, Subcommand};use itertools::Itertools;use jj_lib::backend::TreeValue;use jj_lib::file_util;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, LocalAndRemoteRef,};use jj_lib::repo::{ReadonlyRepo, 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, print_trackable_remote_branches, resolve_multiple_nonempty_revsets,    short_change_hash, short_commit_hash, start_repo_transaction, CommandHelper, RevisionArg,    WorkspaceCommandHelper,};use crate::command_error::{    user_error, user_error_with_hint, user_error_with_hint_opt, user_error_with_message,    CommandError,};use crate::git_util::{    get_git_repo, is_colocated_git_workspace, 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),    Init(GitInitArgs),    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 {} /// Create a new Git backed repo.#[derive(clap::Args, Clone, Debug)]pub struct GitInitArgs {    /// The destination directory where the `jj` repo will be created.    /// If the directory does not exist, it will be created.    /// If no directory is diven, the current directory is used.    ///    /// By default the `git` repo is under `$destination/.jj`    #[arg(default_value = ".", value_hint = clap::ValueHint::DirPath)]    destination: String,     /// Specifies that the `jj` repo should also be a valid    /// `git` repo, allowing the use of both `jj` and `git` commands    /// in the same directory.    ///    /// This is done by placing the backing git repo into a `.git` directory in    /// the root of the `jj` repo along with the `.jj` directory. If the `.git`    /// directory already exists, all the existing commits will be imported.    ///    /// This option is mutually exclusive with `--git-repo`.    #[arg(long, conflicts_with = "git_repo")]    colocate: bool,     /// Specifies a path to an **existing** git repository to be    /// used as the backing git repo for the newly created `jj` repo.    ///    /// If the specified `--git-repo` path happens to be the same as    /// the `jj` repo path (both .jj and .git directories are in the    /// same working directory), then both `jj` and `git` commands    /// will work on the same repo. This is called a co-located repo.    ///    /// This option is mutually exclusive with `--colocate`.    #[arg(long, conflicts_with = "colocate", value_hint = clap::ValueHint::DirPath)]    git_repo: Option<String>,} /// Fetch from a Git remote////// If a working-copy commit gets abandoned, it will be given a new, empty/// commit. This is true in general; it is not specific to this command.#[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, short, 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", "tracked"]).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, or branches matching a pattern (can be repeated)    ///    /// By default, the specified name matches exactly. Use `glob:` prefix to    /// select branches by wildcard pattern. For details, see    /// https://martinvonz.github.io/jj/latest/revsets#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 tracked branches (including deleted branches)    ///    /// This usually means that the branch was already pushed to or fetched from    /// the relevant remote. For details, see    /// https://martinvonz.github.io/jj/latest/branches#remotes-and-tracked-branches    #[arg(long)]    tracked: bool,    /// Push all deleted branches    ///    /// Only tracked branches can be successfully deleted on the remote. A    /// warning will be printed if any untracked branches on the remote    /// correspond to missing local branches.    #[arg(long)]    deleted: bool,    /// Push branches pointing to these commits (can be repeated)    #[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////// If a working-copy commit gets abandoned, it will be given a new, empty/// commit. This is true in general; it is not specific to this command.#[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, 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_with_message("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(())} pub fn git_init(    ui: &mut Ui,    command: &CommandHelper,    workspace_root: &Path,    colocate: bool,    git_repo: Option<&str>,) -> Result<(), CommandError> {    #[derive(Clone, Debug)]    enum GitInitMode {        Colocate,        External(PathBuf),        Internal,    }     let colocated_git_repo_path = workspace_root.join(".git");    let init_mode = if colocate {        if colocated_git_repo_path.exists() {            GitInitMode::External(colocated_git_repo_path)        } else {            GitInitMode::Colocate        }    } else if let Some(path_str) = git_repo {        let mut git_repo_path = command.cwd().join(path_str);        if !git_repo_path.ends_with(".git") {            git_repo_path.push(".git");            // Undo if .git doesn't exist - likely a bare repo.            if !git_repo_path.exists() {                git_repo_path.pop();            }        }        GitInitMode::External(git_repo_path)    } else {        if colocated_git_repo_path.exists() {            return Err(user_error_with_hint(                "Did not create a jj repo because there is an existing Git repo in this directory.",                "To create a repo backed by the existing Git repo, run `jj git init --colocate` \                 instead.",            ));        }        GitInitMode::Internal    };     match &init_mode {        GitInitMode::Colocate => {            let (workspace, repo) =                Workspace::init_colocated_git(command.settings(), workspace_root)?;            let workspace_command = command.for_loaded_repo(ui, workspace, repo)?;            maybe_add_gitignore(&workspace_command)?;        }        GitInitMode::External(git_repo_path) => {            let (workspace, repo) =                Workspace::init_external_git(command.settings(), workspace_root, git_repo_path)?;            // Import refs first so all the reachable commits are indexed in            // chronological order.            let colocated = is_colocated_git_workspace(&workspace, &repo);            let repo = init_git_refs(ui, command, repo, colocated)?;            let mut workspace_command = command.for_loaded_repo(ui, workspace, repo)?;            maybe_add_gitignore(&workspace_command)?;            workspace_command.maybe_snapshot(ui)?;            if !workspace_command.working_copy_shared_with_git() {                let mut tx = workspace_command.start_transaction();                jj_lib::git::import_head(tx.mut_repo())?;                if let Some(git_head_id) = tx.mut_repo().view().git_head().as_normal().cloned() {                    let git_head_commit = tx.mut_repo().store().get_commit(&git_head_id)?;                    tx.check_out(&git_head_commit)?;                }                if tx.mut_repo().has_changes() {                    tx.finish(ui, "import git head")?;                }            }            print_trackable_remote_branches(ui, workspace_command.repo().view())?;        }        GitInitMode::Internal => {            Workspace::init_internal_git(command.settings(), workspace_root)?;        }    }    Ok(())} /// Imports branches and tags from the underlying Git repo, exports changes if/// the repo is colocated.////// This is similar to `WorkspaceCommandHelper::import_git_refs()`, but never/// moves the Git HEAD to the working copy parent.fn init_git_refs(    ui: &mut Ui,    command: &CommandHelper,    repo: Arc<ReadonlyRepo>,    colocated: bool,) -> Result<Arc<ReadonlyRepo>, CommandError> {    let mut tx = start_repo_transaction(&repo, command.settings(), command.string_args());    // There should be no old refs to abandon, but enforce it.    let mut git_settings = command.settings().git_settings();    git_settings.abandon_unreachable_commits = false;    let stats = git::import_some_refs(        tx.mut_repo(),        &git_settings,        // Initial import shouldn't fail because of reserved remote name.        |ref_name| !git::is_reserved_git_remote_ref(ref_name),    )?;    if !tx.mut_repo().has_changes() {        return Ok(repo);    }    print_git_import_stats(ui, tx.repo(), &stats, false)?;    if colocated {        // If git.auto-local-branch = true, local branches could be created for        // the imported remote branches.        let failed_branches = git::export_refs(tx.mut_repo())?;        print_failed_git_export(ui, &failed_branches)?;    }    let repo = tx.commit("import git refs");    writeln!(        ui.stderr(),        "Done importing changes from the underlying Git repo."    )?;    Ok(repo)} fn cmd_git_init(    ui: &mut Ui,    command: &CommandHelper,    args: &GitInitArgs,) -> Result<(), CommandError> {    let cwd = command.cwd();    let wc_path = cwd.join(&args.destination);    let wc_path = file_util::create_or_reuse_dir(&wc_path)

Lines 1–500 of 1297.