just playing with tangled
at gvimdiff 231 lines 8.6 kB view raw
1// Copyright 2020-2023 The Jujutsu Authors 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// https://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15use std::fs; 16use std::io; 17use std::io::Write as _; 18use std::num::NonZeroU32; 19use std::path::Path; 20 21use jj_lib::git; 22use jj_lib::git::GitFetch; 23use jj_lib::ref_name::RefNameBuf; 24use jj_lib::ref_name::RemoteName; 25use jj_lib::ref_name::RemoteNameBuf; 26use jj_lib::repo::Repo as _; 27use jj_lib::str_util::StringPattern; 28use jj_lib::workspace::Workspace; 29 30use super::write_repository_level_trunk_alias; 31use crate::cli_util::CommandHelper; 32use crate::cli_util::WorkspaceCommandHelper; 33use crate::command_error::cli_error; 34use crate::command_error::user_error; 35use crate::command_error::user_error_with_message; 36use crate::command_error::CommandError; 37use crate::commands::git::maybe_add_gitignore; 38use crate::git_util::absolute_git_url; 39use crate::git_util::print_git_import_stats; 40use crate::git_util::with_remote_git_callbacks; 41use crate::ui::Ui; 42 43/// Create a new repo backed by a clone of a Git repo 44/// 45/// The Git repo will be a bare git repo stored inside the `.jj/` directory. 46#[derive(clap::Args, Clone, Debug)] 47pub struct GitCloneArgs { 48 /// URL or path of the Git repo to clone 49 /// 50 /// Local path will be resolved to absolute form. 51 #[arg(value_hint = clap::ValueHint::Url)] 52 source: String, 53 /// Specifies the target directory for the Jujutsu repository clone. 54 /// If not provided, defaults to a directory named after the last component 55 /// of the source URL. The full directory path will be created if it 56 /// doesn't exist. 57 #[arg(value_hint = clap::ValueHint::DirPath)] 58 destination: Option<String>, 59 /// Name of the newly created remote 60 #[arg(long = "remote", default_value = "origin")] 61 remote_name: RemoteNameBuf, 62 /// Whether or not to colocate the Jujutsu repo with the git repo 63 #[arg(long)] 64 colocate: bool, 65 /// Create a shallow clone of the given depth 66 #[arg(long)] 67 depth: Option<NonZeroU32>, 68} 69 70fn clone_destination_for_source(source: &str) -> Option<&str> { 71 let destination = source.strip_suffix(".git").unwrap_or(source); 72 let destination = destination.strip_suffix('/').unwrap_or(destination); 73 destination 74 .rsplit_once(&['/', '\\', ':'][..]) 75 .map(|(_, name)| name) 76} 77 78fn is_empty_dir(path: &Path) -> bool { 79 if let Ok(mut entries) = path.read_dir() { 80 entries.next().is_none() 81 } else { 82 false 83 } 84} 85 86pub fn cmd_git_clone( 87 ui: &mut Ui, 88 command: &CommandHelper, 89 args: &GitCloneArgs, 90) -> Result<(), CommandError> { 91 let remote_name = &args.remote_name; 92 if command.global_args().at_operation.is_some() { 93 return Err(cli_error("--at-op is not respected")); 94 } 95 let source = absolute_git_url(command.cwd(), &args.source)?; 96 let wc_path_str = args 97 .destination 98 .as_deref() 99 .or_else(|| clone_destination_for_source(&source)) 100 .ok_or_else(|| user_error("No destination specified and wasn't able to guess it"))?; 101 let wc_path = command.cwd().join(wc_path_str); 102 103 let wc_path_existed = wc_path.exists(); 104 if wc_path_existed && !is_empty_dir(&wc_path) { 105 return Err(user_error( 106 "Destination path exists and is not an empty directory", 107 )); 108 } 109 110 // will create a tree dir in case if was deleted after last check 111 fs::create_dir_all(&wc_path) 112 .map_err(|err| user_error_with_message(format!("Failed to create {wc_path_str}"), err))?; 113 114 // Canonicalize because fs::remove_dir_all() doesn't seem to like e.g. 115 // `/some/path/.` 116 let canonical_wc_path = dunce::canonicalize(&wc_path) 117 .map_err(|err| user_error_with_message(format!("Failed to create {wc_path_str}"), err))?; 118 119 let clone_result = (|| -> Result<_, CommandError> { 120 let workspace_command = init_workspace(ui, command, &canonical_wc_path, args.colocate)?; 121 let mut workspace_command = 122 configure_remote(ui, command, workspace_command, remote_name, &source)?; 123 let default_branch = fetch_new_remote(ui, &mut workspace_command, remote_name, args.depth)?; 124 Ok((workspace_command, default_branch)) 125 })(); 126 if clone_result.is_err() { 127 let clean_up_dirs = || -> io::Result<()> { 128 fs::remove_dir_all(canonical_wc_path.join(".jj"))?; 129 if args.colocate { 130 fs::remove_dir_all(canonical_wc_path.join(".git"))?; 131 } 132 if !wc_path_existed { 133 fs::remove_dir(&canonical_wc_path)?; 134 } 135 Ok(()) 136 }; 137 if let Err(err) = clean_up_dirs() { 138 writeln!( 139 ui.warning_default(), 140 "Failed to clean up {}: {}", 141 canonical_wc_path.display(), 142 err 143 ) 144 .ok(); 145 } 146 } 147 148 let (mut workspace_command, default_branch) = clone_result?; 149 if let Some(name) = &default_branch { 150 let default_symbol = name.to_remote_symbol(remote_name); 151 write_repository_level_trunk_alias(ui, workspace_command.repo_path(), default_symbol)?; 152 153 let default_branch_remote_ref = workspace_command 154 .repo() 155 .view() 156 .get_remote_bookmark(default_symbol); 157 if let Some(commit_id) = default_branch_remote_ref.target.as_normal().cloned() { 158 let mut checkout_tx = workspace_command.start_transaction(); 159 // For convenience, create local bookmark as Git would do. 160 checkout_tx.repo_mut().track_remote_bookmark(default_symbol); 161 if let Ok(commit) = checkout_tx.repo().store().get_commit(&commit_id) { 162 checkout_tx.check_out(&commit)?; 163 } 164 checkout_tx.finish(ui, "check out git remote's default branch")?; 165 } 166 } 167 Ok(()) 168} 169 170fn init_workspace( 171 ui: &Ui, 172 command: &CommandHelper, 173 wc_path: &Path, 174 colocate: bool, 175) -> Result<WorkspaceCommandHelper, CommandError> { 176 let settings = command.settings_for_new_workspace(wc_path)?; 177 let (workspace, repo) = if colocate { 178 Workspace::init_colocated_git(&settings, wc_path)? 179 } else { 180 Workspace::init_internal_git(&settings, wc_path)? 181 }; 182 let workspace_command = command.for_workable_repo(ui, workspace, repo)?; 183 maybe_add_gitignore(&workspace_command)?; 184 Ok(workspace_command) 185} 186 187fn configure_remote( 188 ui: &Ui, 189 command: &CommandHelper, 190 workspace_command: WorkspaceCommandHelper, 191 remote_name: &RemoteName, 192 source: &str, 193) -> Result<WorkspaceCommandHelper, CommandError> { 194 git::add_remote(workspace_command.repo().store(), remote_name, source)?; 195 // Reload workspace to apply new remote configuration to 196 // gix::ThreadSafeRepository behind the store. 197 let workspace = command.load_workspace_at( 198 workspace_command.workspace_root(), 199 workspace_command.settings(), 200 )?; 201 let op = workspace 202 .repo_loader() 203 .load_operation(workspace_command.repo().op_id())?; 204 let repo = workspace.repo_loader().load_at(&op)?; 205 command.for_workable_repo(ui, workspace, repo) 206} 207 208fn fetch_new_remote( 209 ui: &Ui, 210 workspace_command: &mut WorkspaceCommandHelper, 211 remote_name: &RemoteName, 212 depth: Option<NonZeroU32>, 213) -> Result<Option<RefNameBuf>, CommandError> { 214 writeln!( 215 ui.status(), 216 r#"Fetching into new repo in "{}""#, 217 workspace_command.workspace_root().display() 218 )?; 219 let git_settings = workspace_command.settings().git_settings()?; 220 let mut fetch_tx = workspace_command.start_transaction(); 221 let mut git_fetch = GitFetch::new(fetch_tx.repo_mut(), &git_settings)?; 222 with_remote_git_callbacks(ui, |cb| { 223 git_fetch.fetch(remote_name, &[StringPattern::everything()], cb, depth) 224 })?; 225 let default_branch = 226 with_remote_git_callbacks(ui, |cb| git_fetch.get_default_branch(remote_name, cb))?; 227 let import_stats = git_fetch.import_refs()?; 228 print_git_import_stats(ui, fetch_tx.repo(), &import_stats, true)?; 229 fetch_tx.finish(ui, "fetch from git remote into empty repo")?; 230 Ok(default_branch) 231}