just playing with tangled
at gvimdiff 289 lines 11 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::io; 16use std::io::Write as _; 17use std::path::Path; 18use std::path::PathBuf; 19use std::str; 20use std::sync::Arc; 21 22use itertools::Itertools as _; 23use jj_lib::file_util; 24use jj_lib::git; 25use jj_lib::git::parse_git_ref; 26use jj_lib::git::GitRefKind; 27use jj_lib::repo::ReadonlyRepo; 28use jj_lib::repo::Repo as _; 29use jj_lib::view::View; 30use jj_lib::workspace::Workspace; 31 32use super::write_repository_level_trunk_alias; 33use crate::cli_util::start_repo_transaction; 34use crate::cli_util::CommandHelper; 35use crate::cli_util::WorkspaceCommandHelper; 36use crate::command_error::cli_error; 37use crate::command_error::internal_error; 38use crate::command_error::user_error_with_hint; 39use crate::command_error::user_error_with_message; 40use crate::command_error::CommandError; 41use crate::commands::git::maybe_add_gitignore; 42use crate::git_util::is_colocated_git_workspace; 43use crate::git_util::print_git_export_stats; 44use crate::git_util::print_git_import_stats; 45use crate::ui::Ui; 46 47/// Create a new Git backed repo. 48#[derive(clap::Args, Clone, Debug)] 49pub struct GitInitArgs { 50 /// The destination directory where the `jj` repo will be created. 51 /// If the directory does not exist, it will be created. 52 /// If no directory is given, the current directory is used. 53 /// 54 /// By default the `git` repo is under `$destination/.jj` 55 #[arg(default_value = ".", value_hint = clap::ValueHint::DirPath)] 56 destination: String, 57 58 /// Specifies that the `jj` repo should also be a valid 59 /// `git` repo, allowing the use of both `jj` and `git` commands 60 /// in the same directory. 61 /// 62 /// This is done by placing the backing git repo into a `.git` directory in 63 /// the root of the `jj` repo along with the `.jj` directory. If the `.git` 64 /// directory already exists, all the existing commits will be imported. 65 /// 66 /// This option is mutually exclusive with `--git-repo`. 67 #[arg(long, conflicts_with = "git_repo")] 68 colocate: bool, 69 70 /// Specifies a path to an **existing** git repository to be 71 /// used as the backing git repo for the newly created `jj` repo. 72 /// 73 /// If the specified `--git-repo` path happens to be the same as 74 /// the `jj` repo path (both .jj and .git directories are in the 75 /// same working directory), then both `jj` and `git` commands 76 /// will work on the same repo. This is called a co-located repo. 77 /// 78 /// This option is mutually exclusive with `--colocate`. 79 #[arg(long, conflicts_with = "colocate", value_hint = clap::ValueHint::DirPath)] 80 git_repo: Option<String>, 81} 82 83pub fn cmd_git_init( 84 ui: &mut Ui, 85 command: &CommandHelper, 86 args: &GitInitArgs, 87) -> Result<(), CommandError> { 88 if command.global_args().ignore_working_copy { 89 return Err(cli_error("--ignore-working-copy is not respected")); 90 } 91 if command.global_args().at_operation.is_some() { 92 return Err(cli_error("--at-op is not respected")); 93 } 94 let cwd = command.cwd(); 95 let wc_path = cwd.join(&args.destination); 96 let wc_path = file_util::create_or_reuse_dir(&wc_path) 97 .and_then(|_| dunce::canonicalize(wc_path)) 98 .map_err(|e| user_error_with_message("Failed to create workspace", e))?; 99 100 do_init( 101 ui, 102 command, 103 &wc_path, 104 args.colocate, 105 args.git_repo.as_deref(), 106 )?; 107 108 let relative_wc_path = file_util::relative_path(cwd, &wc_path); 109 writeln!( 110 ui.status(), 111 r#"Initialized repo in "{}""#, 112 relative_wc_path.display() 113 )?; 114 115 Ok(()) 116} 117 118fn do_init( 119 ui: &mut Ui, 120 command: &CommandHelper, 121 workspace_root: &Path, 122 colocate: bool, 123 git_repo: Option<&str>, 124) -> Result<(), CommandError> { 125 #[derive(Clone, Debug)] 126 enum GitInitMode { 127 Colocate, 128 External(PathBuf), 129 Internal, 130 } 131 132 let colocated_git_repo_path = workspace_root.join(".git"); 133 let init_mode = if colocate { 134 if colocated_git_repo_path.exists() { 135 GitInitMode::External(colocated_git_repo_path) 136 } else { 137 GitInitMode::Colocate 138 } 139 } else if let Some(path_str) = git_repo { 140 let mut git_repo_path = command.cwd().join(path_str); 141 if !git_repo_path.ends_with(".git") { 142 git_repo_path.push(".git"); 143 // Undo if .git doesn't exist - likely a bare repo. 144 if !git_repo_path.exists() { 145 git_repo_path.pop(); 146 } 147 } 148 GitInitMode::External(git_repo_path) 149 } else { 150 if colocated_git_repo_path.exists() { 151 return Err(user_error_with_hint( 152 "Did not create a jj repo because there is an existing Git repo in this directory.", 153 "To create a repo backed by the existing Git repo, run `jj git init --colocate` \ 154 instead.", 155 )); 156 } 157 GitInitMode::Internal 158 }; 159 160 let settings = command.settings_for_new_workspace(workspace_root)?; 161 match &init_mode { 162 GitInitMode::Colocate => { 163 let (workspace, repo) = Workspace::init_colocated_git(&settings, workspace_root)?; 164 let workspace_command = command.for_workable_repo(ui, workspace, repo)?; 165 maybe_add_gitignore(&workspace_command)?; 166 } 167 GitInitMode::External(git_repo_path) => { 168 let (workspace, repo) = 169 Workspace::init_external_git(&settings, workspace_root, git_repo_path)?; 170 // Import refs first so all the reachable commits are indexed in 171 // chronological order. 172 let colocated = is_colocated_git_workspace(&workspace, &repo); 173 let repo = init_git_refs(ui, repo, command.string_args(), colocated)?; 174 let mut workspace_command = command.for_workable_repo(ui, workspace, repo)?; 175 maybe_add_gitignore(&workspace_command)?; 176 workspace_command.maybe_snapshot(ui)?; 177 maybe_set_repository_level_trunk_alias(ui, &workspace_command)?; 178 if !workspace_command.working_copy_shared_with_git() { 179 let mut tx = workspace_command.start_transaction(); 180 jj_lib::git::import_head(tx.repo_mut())?; 181 if let Some(git_head_id) = tx.repo().view().git_head().as_normal().cloned() { 182 let git_head_commit = tx.repo().store().get_commit(&git_head_id)?; 183 tx.check_out(&git_head_commit)?; 184 } 185 if tx.repo().has_changes() { 186 tx.finish(ui, "import git head")?; 187 } 188 } 189 print_trackable_remote_bookmarks(ui, workspace_command.repo().view())?; 190 } 191 GitInitMode::Internal => { 192 Workspace::init_internal_git(&settings, workspace_root)?; 193 } 194 } 195 Ok(()) 196} 197 198/// Imports branches and tags from the underlying Git repo, exports changes if 199/// the repo is colocated. 200/// 201/// This is similar to `WorkspaceCommandHelper::import_git_refs()`, but never 202/// moves the Git HEAD to the working copy parent. 203fn init_git_refs( 204 ui: &mut Ui, 205 repo: Arc<ReadonlyRepo>, 206 string_args: &[String], 207 colocated: bool, 208) -> Result<Arc<ReadonlyRepo>, CommandError> { 209 let mut git_settings = repo.settings().git_settings()?; 210 let mut tx = start_repo_transaction(&repo, string_args); 211 // There should be no old refs to abandon, but enforce it. 212 git_settings.abandon_unreachable_commits = false; 213 let stats = git::import_refs(tx.repo_mut(), &git_settings)?; 214 print_git_import_stats(ui, tx.repo(), &stats, false)?; 215 if !tx.repo().has_changes() { 216 return Ok(repo); 217 } 218 if colocated { 219 // If git.auto-local-bookmark = true, local bookmarks could be created for 220 // the imported remote branches. 221 let stats = git::export_refs(tx.repo_mut())?; 222 print_git_export_stats(ui, &stats)?; 223 } 224 let repo = tx.commit("import git refs")?; 225 writeln!( 226 ui.status(), 227 "Done importing changes from the underlying Git repo." 228 )?; 229 Ok(repo) 230} 231 232// Set repository level `trunk()` alias to the default branch for "origin". 233pub fn maybe_set_repository_level_trunk_alias( 234 ui: &Ui, 235 workspace_command: &WorkspaceCommandHelper, 236) -> Result<(), CommandError> { 237 let git_repo = git::get_git_repo(workspace_command.repo().store())?; 238 if let Some(reference) = git_repo 239 .try_find_reference("refs/remotes/origin/HEAD") 240 .map_err(internal_error)? 241 { 242 if let Some(reference_name) = reference.target().try_name() { 243 if let Some((GitRefKind::Bookmark, symbol)) = str::from_utf8(reference_name.as_bstr()) 244 .ok() 245 .and_then(|name| parse_git_ref(name.as_ref())) 246 { 247 // TODO: Can we assume the symbolic target points to the same remote? 248 let symbol = symbol.name.to_remote_symbol("origin".as_ref()); 249 write_repository_level_trunk_alias(ui, workspace_command.repo_path(), symbol)?; 250 } 251 }; 252 }; 253 254 Ok(()) 255} 256 257fn print_trackable_remote_bookmarks(ui: &Ui, view: &View) -> io::Result<()> { 258 let remote_bookmark_symbols = view 259 .bookmarks() 260 .filter(|(_, bookmark_target)| bookmark_target.local_target.is_present()) 261 .flat_map(|(name, bookmark_target)| { 262 bookmark_target 263 .remote_refs 264 .into_iter() 265 .filter(|&(_, remote_ref)| !remote_ref.is_tracked()) 266 .map(move |(remote, _)| name.to_remote_symbol(remote)) 267 }) 268 .collect_vec(); 269 if remote_bookmark_symbols.is_empty() { 270 return Ok(()); 271 } 272 273 if let Some(mut formatter) = ui.status_formatter() { 274 writeln!( 275 formatter.labeled("hint").with_heading("Hint: "), 276 "The following remote bookmarks aren't associated with the existing local bookmarks:" 277 )?; 278 for symbol in &remote_bookmark_symbols { 279 write!(formatter, " ")?; 280 writeln!(formatter.labeled("bookmark"), "{symbol}")?; 281 } 282 writeln!( 283 formatter.labeled("hint").with_heading("Hint: "), 284 "Run `jj bookmark track {syms}` to keep local bookmarks updated on future pulls.", 285 syms = remote_bookmark_symbols.iter().join(" "), 286 )?; 287 } 288 Ok(()) 289}