// 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::io::Write; use std::path::Path; use std::path::PathBuf; use std::sync::Arc; use jj_lib::file_util; use jj_lib::git; use jj_lib::git::parse_git_ref; use jj_lib::git::RefName; use jj_lib::repo::ReadonlyRepo; use jj_lib::repo::Repo; use jj_lib::workspace::Workspace; use crate::cli_util::print_trackable_remote_bookmarks; use crate::cli_util::start_repo_transaction; use crate::cli_util::CommandHelper; use crate::cli_util::WorkspaceCommandHelper; use crate::command_error::cli_error; use crate::command_error::user_error_with_hint; 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::is_colocated_git_workspace; use crate::git_util::print_failed_git_export; use crate::git_util::print_git_import_stats; use crate::ui::Ui; /// 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 given, 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, } pub fn cmd_git_init( ui: &mut Ui, command: &CommandHelper, args: &GitInitArgs, ) -> Result<(), CommandError> { if command.global_args().ignore_working_copy { return Err(cli_error("--ignore-working-copy is not respected")); } if command.global_args().at_operation.is_some() { return Err(cli_error("--at-op is not respected")); } let cwd = command.cwd(); let wc_path = cwd.join(&args.destination); let wc_path = file_util::create_or_reuse_dir(&wc_path) .and_then(|_| wc_path.canonicalize()) .map_err(|e| user_error_with_message("Failed to create workspace", e))?; do_init( ui, command, &wc_path, args.colocate, args.git_repo.as_deref(), )?; let relative_wc_path = file_util::relative_path(cwd, &wc_path); writeln!( ui.status(), r#"Initialized repo in "{}""#, relative_wc_path.display() )?; Ok(()) } pub fn do_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_workable_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_workable_repo(ui, workspace, repo)?; maybe_add_gitignore(&workspace_command)?; workspace_command.maybe_snapshot(ui)?; maybe_set_repository_level_trunk_alias(ui, &workspace_command)?; if !workspace_command.working_copy_shared_with_git() { let mut tx = workspace_command.start_transaction(); jj_lib::git::import_head(tx.repo_mut())?; if let Some(git_head_id) = tx.repo().view().git_head().as_normal().cloned() { let git_head_commit = tx.repo().store().get_commit(&git_head_id)?; tx.check_out(&git_head_commit)?; } if tx.repo().has_changes() { tx.finish(ui, "import git head")?; } } print_trackable_remote_bookmarks(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, colocated: bool, ) -> Result, 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.repo_mut(), &git_settings, // Initial import shouldn't fail because of reserved remote name. |ref_name| !git::is_reserved_git_remote_ref(ref_name), )?; if !tx.repo().has_changes() { return Ok(repo); } print_git_import_stats(ui, tx.repo(), &stats, false)?; if colocated { // If git.auto-local-bookmark = true, local bookmarks could be created for // the imported remote branches. let failed_refs = git::export_refs(tx.repo_mut())?; print_failed_git_export(ui, &failed_refs)?; } let repo = tx.commit("import git refs"); writeln!( ui.status(), "Done importing changes from the underlying Git repo." )?; Ok(repo) } // Set repository level `trunk()` alias to the default branch for "origin". pub fn maybe_set_repository_level_trunk_alias( ui: &Ui, workspace_command: &WorkspaceCommandHelper, ) -> Result<(), CommandError> { let git_repo = get_git_repo(workspace_command.repo().store())?; if let Ok(reference) = git_repo.find_reference("refs/remotes/origin/HEAD") { if let Some(reference_name) = reference.symbolic_target() { if let Some(RefName::RemoteBranch { branch: default_branch, .. }) = parse_git_ref(reference_name) { let config_path = workspace_command.repo_path().join("config.toml"); write_config_value_to_file( &ConfigNamePathBuf::from_iter(["revset-aliases", "trunk()"]), format!("{default_branch}@origin").into(), &config_path, )?; writeln!( ui.status(), "Setting the revset alias \"trunk()\" to \"{default_branch}@origin\"", )?; } }; }; Ok(()) }