// 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. Load more
// 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)