just playing with tangled
at main 400 lines 12 kB view raw
1// Copyright 2025 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::path::Path; 16use std::path::PathBuf; 17 18pub const GIT_USER: &str = "Someone"; 19pub const GIT_EMAIL: &str = "someone@example.org"; 20 21fn git_config() -> Vec<bstr::BString> { 22 vec![ 23 format!("user.name = {GIT_USER}").into(), 24 format!("user.email = {GIT_EMAIL}").into(), 25 "init.defaultBranch = master".into(), 26 ] 27} 28 29fn open_options() -> gix::open::Options { 30 gix::open::Options::isolated() 31 .config_overrides(git_config()) 32 .strict_config(true) 33} 34 35pub fn open(directory: impl Into<PathBuf>) -> gix::Repository { 36 gix::open_opts(directory, open_options()).unwrap() 37} 38 39pub fn init(directory: impl AsRef<Path>) -> gix::Repository { 40 gix::ThreadSafeRepository::init_opts( 41 directory, 42 gix::create::Kind::WithWorktree, 43 gix::create::Options::default(), 44 open_options(), 45 ) 46 .unwrap() 47 .to_thread_local() 48} 49 50pub fn init_bare(directory: impl AsRef<Path>) -> gix::Repository { 51 gix::ThreadSafeRepository::init_opts( 52 directory, 53 gix::create::Kind::Bare, 54 gix::create::Options::default(), 55 open_options(), 56 ) 57 .unwrap() 58 .to_thread_local() 59} 60 61pub fn clone(dest_path: &Path, repo_url: &str, remote_name: Option<&str>) -> gix::Repository { 62 let remote_name = remote_name.unwrap_or("origin"); 63 // gitoxide doesn't write the remote HEAD as a symbolic link, which prevents 64 // `jj` from getting it. 65 // 66 // This, plus the fact that the code to clone a repo in gitoxide is non-trivial, 67 // makes it appealing to just spawn a git subprocess 68 let output = std::process::Command::new("git") 69 .args(["clone", repo_url, "--origin", remote_name]) 70 .arg(dest_path) 71 .output() 72 .unwrap(); 73 assert!( 74 output.status.success(), 75 "git cloning failed with {}:\n{}\n----- stderr -----\n{}", 76 output.status, 77 bstr::BString::from(output.stdout), 78 bstr::BString::from(output.stderr), 79 ); 80 81 open(dest_path) 82} 83 84/// Writes out gitlink entry pointing to the `target_repo`. 85pub fn create_gitlink(src_repo: impl AsRef<Path>, target_repo: impl AsRef<Path>) { 86 let git_link_path = src_repo.as_ref().join(".git"); 87 std::fs::write( 88 git_link_path, 89 format!("gitdir: {}\n", target_repo.as_ref().display()), 90 ) 91 .unwrap(); 92} 93 94pub fn remove_config_value(mut repo: gix::Repository, section: &str, key: &str) { 95 let mut config = repo.config_snapshot_mut(); 96 let Ok(mut section) = config.section_mut(section, None) else { 97 return; 98 }; 99 section.remove(key); 100 101 let mut file = std::fs::File::create(config.meta().path.as_ref().unwrap()).unwrap(); 102 config 103 .write_to_filter(&mut file, |section| section.meta() == config.meta()) 104 .unwrap(); 105} 106 107pub struct CommitResult { 108 pub tree_id: gix::ObjectId, 109 pub commit_id: gix::ObjectId, 110} 111 112pub fn add_commit( 113 repo: &gix::Repository, 114 reference: &str, 115 filename: &str, 116 content: &[u8], 117 message: &str, 118 parents: &[gix::ObjectId], 119) -> CommitResult { 120 let blob_oid = repo.write_blob(content).unwrap(); 121 122 let parent_tree_editor = parents.first().map(|commit_id| { 123 repo.find_commit(*commit_id) 124 .unwrap() 125 .tree() 126 .unwrap() 127 .edit() 128 .unwrap() 129 }); 130 let empty_tree_editor_fn = || { 131 repo.edit_tree(gix::ObjectId::empty_tree(repo.object_hash())) 132 .unwrap() 133 }; 134 135 let mut tree_editor = parent_tree_editor.unwrap_or_else(empty_tree_editor_fn); 136 tree_editor 137 .upsert(filename, gix::object::tree::EntryKind::Blob, blob_oid) 138 .unwrap(); 139 let tree_id = tree_editor.write().unwrap().detach(); 140 let commit_id = write_commit(repo, reference, tree_id, message, parents); 141 CommitResult { tree_id, commit_id } 142} 143 144pub fn write_commit( 145 repo: &gix::Repository, 146 reference: &str, 147 tree_id: gix::ObjectId, 148 message: &str, 149 parents: &[gix::ObjectId], 150) -> gix::ObjectId { 151 let signature = signature(); 152 repo.commit_as( 153 &signature, 154 &signature, 155 reference, 156 message, 157 tree_id, 158 parents.iter().copied(), 159 ) 160 .unwrap() 161 .detach() 162} 163 164pub fn set_head_to_id(repo: &gix::Repository, target: gix::ObjectId) { 165 repo.edit_reference(gix::refs::transaction::RefEdit { 166 change: gix::refs::transaction::Change::Update { 167 log: gix::refs::transaction::LogChange::default(), 168 expected: gix::refs::transaction::PreviousValue::Any, 169 new: gix::refs::Target::Object(target), 170 }, 171 name: "HEAD".try_into().unwrap(), 172 deref: false, 173 }) 174 .unwrap(); 175} 176 177pub fn set_symbolic_reference(repo: &gix::Repository, reference: &str, target: &str) { 178 use gix::refs::transaction; 179 let change = transaction::Change::Update { 180 log: transaction::LogChange { 181 mode: transaction::RefLog::AndReference, 182 force_create_reflog: true, 183 message: "create symbolic reference".into(), 184 }, 185 expected: transaction::PreviousValue::Any, 186 new: gix::refs::Target::Symbolic(target.try_into().unwrap()), 187 }; 188 189 let ref_edit = transaction::RefEdit { 190 change, 191 name: reference.try_into().unwrap(), 192 deref: false, 193 }; 194 repo.edit_reference(ref_edit).unwrap(); 195} 196 197pub fn checkout_tree_index(repo: &gix::Repository, tree_id: gix::ObjectId) { 198 let objects = repo.objects.clone(); 199 let mut index = repo.index_from_tree(&tree_id).unwrap(); 200 gix::worktree::state::checkout( 201 &mut index, 202 repo.workdir().unwrap(), 203 objects, 204 &gix::progress::Discard, 205 &gix::progress::Discard, 206 &gix::interrupt::IS_INTERRUPTED, 207 gix::worktree::state::checkout::Options::default(), 208 ) 209 .unwrap(); 210} 211 212fn signature() -> gix::actor::Signature { 213 gix::actor::Signature { 214 name: bstr::BString::from(GIT_USER), 215 email: bstr::BString::from(GIT_EMAIL), 216 time: gix::date::Time::new(0, 0), 217 } 218} 219 220#[derive(Debug, PartialEq, Eq)] 221pub enum GitStatusInfo { 222 Index(IndexStatus), 223 Worktree(WorktreeStatus), 224} 225 226#[derive(Debug, PartialEq, Eq)] 227pub enum IndexStatus { 228 Addition, 229 Deletion, 230 Rename, 231 Modification, 232} 233 234#[derive(Debug, PartialEq, Eq)] 235pub enum WorktreeStatus { 236 Removed, 237 Added, 238 Modified, 239 TypeChange, 240 Renamed, 241 Copied, 242 IntentToAdd, 243 Conflict, 244 Ignored, 245} 246 247impl<'lhs, 'rhs> From<gix::diff::index::ChangeRef<'lhs, 'rhs>> for IndexStatus { 248 fn from(value: gix::diff::index::ChangeRef<'lhs, 'rhs>) -> Self { 249 match value { 250 gix::diff::index::ChangeRef::Addition { .. } => IndexStatus::Addition, 251 gix::diff::index::ChangeRef::Deletion { .. } => IndexStatus::Deletion, 252 gix::diff::index::ChangeRef::Rewrite { .. } => IndexStatus::Rename, 253 gix::diff::index::ChangeRef::Modification { .. } => IndexStatus::Modification, 254 } 255 } 256} 257 258impl From<Option<gix::status::index_worktree::iter::Summary>> for WorktreeStatus { 259 fn from(value: Option<gix::status::index_worktree::iter::Summary>) -> Self { 260 match value { 261 Some(gix::status::index_worktree::iter::Summary::Removed) => WorktreeStatus::Removed, 262 Some(gix::status::index_worktree::iter::Summary::Added) => WorktreeStatus::Added, 263 Some(gix::status::index_worktree::iter::Summary::Modified) => WorktreeStatus::Modified, 264 Some(gix::status::index_worktree::iter::Summary::TypeChange) => { 265 WorktreeStatus::TypeChange 266 } 267 Some(gix::status::index_worktree::iter::Summary::Renamed) => WorktreeStatus::Renamed, 268 Some(gix::status::index_worktree::iter::Summary::Copied) => WorktreeStatus::Copied, 269 Some(gix::status::index_worktree::iter::Summary::IntentToAdd) => { 270 WorktreeStatus::IntentToAdd 271 } 272 Some(gix::status::index_worktree::iter::Summary::Conflict) => WorktreeStatus::Conflict, 273 None => WorktreeStatus::Ignored, 274 } 275 } 276} 277 278impl From<gix::status::Item> for GitStatusInfo { 279 fn from(value: gix::status::Item) -> Self { 280 match value { 281 gix::status::Item::TreeIndex(change) => GitStatusInfo::Index(change.into()), 282 gix::status::Item::IndexWorktree(item) => { 283 GitStatusInfo::Worktree(item.summary().into()) 284 } 285 } 286 } 287} 288 289#[derive(Debug, PartialEq, Eq)] 290pub struct GitStatus { 291 path: String, 292 status: GitStatusInfo, 293} 294 295impl From<gix::status::Item> for GitStatus { 296 fn from(value: gix::status::Item) -> Self { 297 let path = value.location().to_string(); 298 let status = value.into(); 299 GitStatus { path, status } 300 } 301} 302 303pub fn status(repo: &gix::Repository) -> Vec<GitStatus> { 304 let mut status: Vec<GitStatus> = repo 305 .status(gix::progress::Discard) 306 .unwrap() 307 .untracked_files(gix::status::UntrackedFiles::Files) 308 .dirwalk_options(|options| { 309 options.emit_ignored(Some(gix::dir::walk::EmissionMode::Matching)) 310 }) 311 .into_iter(None) 312 .unwrap() 313 .map(Result::unwrap) 314 .map(|x| x.into()) 315 .collect(); 316 317 status.sort_by(|a, b| a.path.cmp(&b.path)); 318 status 319} 320 321pub struct IndexManager<'a> { 322 index: gix::index::File, 323 repo: &'a gix::Repository, 324} 325 326impl<'a> IndexManager<'a> { 327 pub fn new(repo: &'a gix::Repository) -> IndexManager<'a> { 328 // This would be equivalent to repo.open_index_or_empty() if such 329 // function existed. 330 let index = repo.index_or_empty().unwrap(); 331 let index = gix::index::File::clone(&index); // unshare 332 IndexManager { index, repo } 333 } 334 335 pub fn add_file(&mut self, name: &str, data: &[u8]) { 336 std::fs::write(self.repo.workdir().unwrap().join(name), data).unwrap(); 337 let blob_oid = self.repo.write_blob(data).unwrap().detach(); 338 339 self.index.dangerously_push_entry( 340 gix::index::entry::Stat::default(), 341 blob_oid, 342 gix::index::entry::Flags::from_stage(gix::index::entry::Stage::Unconflicted), 343 gix::index::entry::Mode::FILE, 344 name.as_bytes().into(), 345 ); 346 } 347 348 pub fn sync_index(&mut self) { 349 self.index.sort_entries(); 350 self.index.verify_entries().unwrap(); 351 self.index 352 .write(gix::index::write::Options::default()) 353 .unwrap(); 354 } 355} 356 357pub fn add_remote(repo_dir: impl AsRef<Path>, remote_name: &str, url: &str) { 358 let output = std::process::Command::new("git") 359 .current_dir(repo_dir) 360 .args(["remote", "add", remote_name, url]) 361 .output() 362 .unwrap(); 363 assert!( 364 output.status.success(), 365 "git remote add {remote_name} {url} failed with {}:\n{}\n----- stderr -----\n{}", 366 output.status, 367 bstr::BString::from(output.stdout), 368 bstr::BString::from(output.stderr), 369 ); 370} 371 372pub fn rename_remote(repo_dir: impl AsRef<Path>, original: &str, new: &str) { 373 let output = std::process::Command::new("git") 374 .current_dir(repo_dir) 375 .args(["remote", "rename", original, new]) 376 .output() 377 .unwrap(); 378 assert!( 379 output.status.success(), 380 "git remote rename failed with {}:\n{}\n----- stderr -----\n{}", 381 output.status, 382 bstr::BString::from(output.stdout), 383 bstr::BString::from(output.stderr), 384 ); 385} 386 387pub fn fetch(repo_dir: impl AsRef<Path>, remote: &str) { 388 let output = std::process::Command::new("git") 389 .current_dir(repo_dir) 390 .args(["fetch", remote]) 391 .output() 392 .unwrap(); 393 assert!( 394 output.status.success(), 395 "git fetch {remote} failed with {}:\n{}\n----- stderr -----\n{}", 396 output.status, 397 bstr::BString::from(output.stdout), 398 bstr::BString::from(output.stderr), 399 ); 400}