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