1#![allow(missing_docs)]
16
17use std::any::Any;
18use std::collections::HashSet;
19use std::fmt::Debug;
20use std::fmt::Error;
21use std::fmt::Formatter;
22use std::fs;
23use std::io;
24use std::io::Cursor;
25use std::io::Read;
26use std::path::Path;
27use std::path::PathBuf;
28use std::process::Command;
29use std::process::ExitStatus;
30use std::str;
31use std::sync::Arc;
32use std::sync::Mutex;
33use std::sync::MutexGuard;
34use std::time::SystemTime;
35
36use async_trait::async_trait;
37use futures::stream::BoxStream;
38use gix::bstr::BString;
39use gix::objs::CommitRef;
40use gix::objs::CommitRefIter;
41use gix::objs::WriteTo as _;
42use itertools::Itertools as _;
43use pollster::FutureExt as _;
44use prost::Message as _;
45use smallvec::SmallVec;
46use thiserror::Error;
47
48use crate::backend::make_root_commit;
49use crate::backend::Backend;
50use crate::backend::BackendError;
51use crate::backend::BackendInitError;
52use crate::backend::BackendLoadError;
53use crate::backend::BackendResult;
54use crate::backend::ChangeId;
55use crate::backend::Commit;
56use crate::backend::CommitId;
57use crate::backend::Conflict;
58use crate::backend::ConflictId;
59use crate::backend::ConflictTerm;
60use crate::backend::CopyRecord;
61use crate::backend::FileId;
62use crate::backend::MergedTreeId;
63use crate::backend::MillisSinceEpoch;
64use crate::backend::SecureSig;
65use crate::backend::Signature;
66use crate::backend::SigningFn;
67use crate::backend::SymlinkId;
68use crate::backend::Timestamp;
69use crate::backend::Tree;
70use crate::backend::TreeId;
71use crate::backend::TreeValue;
72use crate::file_util::IoResultExt as _;
73use crate::file_util::PathError;
74use crate::index::Index;
75use crate::lock::FileLock;
76use crate::merge::Merge;
77use crate::merge::MergeBuilder;
78use crate::object_id::ObjectId;
79use crate::repo_path::RepoPath;
80use crate::repo_path::RepoPathBuf;
81use crate::repo_path::RepoPathComponentBuf;
82use crate::settings::UserSettings;
83use crate::stacked_table::MutableTable;
84use crate::stacked_table::ReadonlyTable;
85use crate::stacked_table::TableSegment as _;
86use crate::stacked_table::TableStore;
87use crate::stacked_table::TableStoreError;
88
89const HASH_LENGTH: usize = 20;
90const CHANGE_ID_LENGTH: usize = 16;
91const NO_GC_REF_NAMESPACE: &str = "refs/jj/keep/";
93const CONFLICT_SUFFIX: &str = ".jjconflict";
94
95pub const JJ_TREES_COMMIT_HEADER: &[u8] = b"jj:trees";
96
97#[derive(Debug, Error)]
98pub enum GitBackendInitError {
99 #[error("Failed to initialize git repository")]
100 InitRepository(#[source] gix::init::Error),
101 #[error("Failed to open git repository")]
102 OpenRepository(#[source] gix::open::Error),
103 #[error(transparent)]
104 Path(PathError),
105}
106
107impl From<Box<GitBackendInitError>> for BackendInitError {
108 fn from(err: Box<GitBackendInitError>) -> Self {
109 BackendInitError(err)
110 }
111}
112
113#[derive(Debug, Error)]
114pub enum GitBackendLoadError {
115 #[error("Failed to open git repository")]
116 OpenRepository(#[source] gix::open::Error),
117 #[error(transparent)]
118 Path(PathError),
119}
120
121impl From<Box<GitBackendLoadError>> for BackendLoadError {
122 fn from(err: Box<GitBackendLoadError>) -> Self {
123 BackendLoadError(err)
124 }
125}
126
127#[derive(Debug, Error)]
129pub enum GitBackendError {
130 #[error("Failed to read non-git metadata")]
131 ReadMetadata(#[source] TableStoreError),
132 #[error("Failed to write non-git metadata")]
133 WriteMetadata(#[source] TableStoreError),
134}
135
136impl From<GitBackendError> for BackendError {
137 fn from(err: GitBackendError) -> Self {
138 BackendError::Other(err.into())
139 }
140}
141
142#[derive(Debug, Error)]
143pub enum GitGcError {
144 #[error("Failed to run git gc command")]
145 GcCommand(#[source] std::io::Error),
146 #[error("git gc command exited with an error: {0}")]
147 GcCommandErrorStatus(ExitStatus),
148}
149
150pub struct GitBackend {
151 base_repo: gix::ThreadSafeRepository,
156 repo: Mutex<gix::Repository>,
157 root_commit_id: CommitId,
158 root_change_id: ChangeId,
159 empty_tree_id: TreeId,
160 extra_metadata_store: TableStore,
161 cached_extra_metadata: Mutex<Option<Arc<ReadonlyTable>>>,
162}
163
164impl GitBackend {
165 pub fn name() -> &'static str {
166 "git"
167 }
168
169 fn new(base_repo: gix::ThreadSafeRepository, extra_metadata_store: TableStore) -> Self {
170 let repo = Mutex::new(base_repo.to_thread_local());
171 let root_commit_id = CommitId::from_bytes(&[0; HASH_LENGTH]);
172 let root_change_id = ChangeId::from_bytes(&[0; CHANGE_ID_LENGTH]);
173 let empty_tree_id = TreeId::from_hex("4b825dc642cb6eb9a060e54bf8d69288fbee4904");
174 GitBackend {
175 base_repo,
176 repo,
177 root_commit_id,
178 root_change_id,
179 empty_tree_id,
180 extra_metadata_store,
181 cached_extra_metadata: Mutex::new(None),
182 }
183 }
184
185 pub fn init_internal(
186 settings: &UserSettings,
187 store_path: &Path,
188 ) -> Result<Self, Box<GitBackendInitError>> {
189 let git_repo_path = Path::new("git");
190 let git_repo = gix::ThreadSafeRepository::init_opts(
191 store_path.join(git_repo_path),
192 gix::create::Kind::Bare,
193 gix::create::Options::default(),
194 gix_open_opts_from_settings(settings),
195 )
196 .map_err(GitBackendInitError::InitRepository)?;
197 Self::init_with_repo(store_path, git_repo_path, git_repo)
198 }
199
200 pub fn init_colocated(
203 settings: &UserSettings,
204 store_path: &Path,
205 workspace_root: &Path,
206 ) -> Result<Self, Box<GitBackendInitError>> {
207 let canonical_workspace_root = {
208 let path = store_path.join(workspace_root);
209 dunce::canonicalize(&path)
210 .context(&path)
211 .map_err(GitBackendInitError::Path)?
212 };
213 let git_repo = gix::ThreadSafeRepository::init_opts(
214 canonical_workspace_root,
215 gix::create::Kind::WithWorktree,
216 gix::create::Options::default(),
217 gix_open_opts_from_settings(settings),
218 )
219 .map_err(GitBackendInitError::InitRepository)?;
220 let git_repo_path = workspace_root.join(".git");
221 Self::init_with_repo(store_path, &git_repo_path, git_repo)
222 }
223
224 pub fn init_external(
226 settings: &UserSettings,
227 store_path: &Path,
228 git_repo_path: &Path,
229 ) -> Result<Self, Box<GitBackendInitError>> {
230 let canonical_git_repo_path = {
231 let path = store_path.join(git_repo_path);
232 canonicalize_git_repo_path(&path)
233 .context(&path)
234 .map_err(GitBackendInitError::Path)?
235 };
236 let git_repo = gix::ThreadSafeRepository::open_opts(
237 canonical_git_repo_path,
238 gix_open_opts_from_settings(settings),
239 )
240 .map_err(GitBackendInitError::OpenRepository)?;
241 Self::init_with_repo(store_path, git_repo_path, git_repo)
242 }
243
244 fn init_with_repo(
245 store_path: &Path,
246 git_repo_path: &Path,
247 git_repo: gix::ThreadSafeRepository,
248 ) -> Result<Self, Box<GitBackendInitError>> {
249 let extra_path = store_path.join("extra");
250 fs::create_dir(&extra_path)
251 .context(&extra_path)
252 .map_err(GitBackendInitError::Path)?;
253 let target_path = store_path.join("git_target");
254 if cfg!(windows) && git_repo_path.is_relative() {
255 let git_repo_path_string = git_repo_path
262 .components()
263 .map(|component| component.as_os_str().to_str().unwrap().to_owned())
264 .join("/");
265 fs::write(&target_path, git_repo_path_string.as_bytes())
266 .context(&target_path)
267 .map_err(GitBackendInitError::Path)?;
268 } else {
269 fs::write(&target_path, git_repo_path.to_str().unwrap().as_bytes())
270 .context(&target_path)
271 .map_err(GitBackendInitError::Path)?;
272 };
273 let extra_metadata_store = TableStore::init(extra_path, HASH_LENGTH);
274 Ok(GitBackend::new(git_repo, extra_metadata_store))
275 }
276
277 pub fn load(
278 settings: &UserSettings,
279 store_path: &Path,
280 ) -> Result<Self, Box<GitBackendLoadError>> {
281 let git_repo_path = {
282 let target_path = store_path.join("git_target");
283 let git_repo_path_str = fs::read_to_string(&target_path)
284 .context(&target_path)
285 .map_err(GitBackendLoadError::Path)?;
286 let git_repo_path = store_path.join(git_repo_path_str);
287 canonicalize_git_repo_path(&git_repo_path)
288 .context(&git_repo_path)
289 .map_err(GitBackendLoadError::Path)?
290 };
291 let repo = gix::ThreadSafeRepository::open_opts(
292 git_repo_path,
293 gix_open_opts_from_settings(settings),
294 )
295 .map_err(GitBackendLoadError::OpenRepository)?;
296 let extra_metadata_store = TableStore::load(store_path.join("extra"), HASH_LENGTH);
297 Ok(GitBackend::new(repo, extra_metadata_store))
298 }
299
300 fn lock_git_repo(&self) -> MutexGuard<'_, gix::Repository> {
301 self.repo.lock().unwrap()
302 }
303
304 pub fn git_repo(&self) -> gix::Repository {
306 self.base_repo.to_thread_local()
307 }
308
309 pub fn git_repo_path(&self) -> &Path {
311 self.base_repo.path()
312 }
313
314 pub fn git_workdir(&self) -> Option<&Path> {
316 self.base_repo.work_dir()
317 }
318
319 fn cached_extra_metadata_table(&self) -> BackendResult<Arc<ReadonlyTable>> {
320 let mut locked_head = self.cached_extra_metadata.lock().unwrap();
321 match locked_head.as_ref() {
322 Some(head) => Ok(head.clone()),
323 None => {
324 let table = self
325 .extra_metadata_store
326 .get_head()
327 .map_err(GitBackendError::ReadMetadata)?;
328 *locked_head = Some(table.clone());
329 Ok(table)
330 }
331 }
332 }
333
334 fn read_extra_metadata_table_locked(&self) -> BackendResult<(Arc<ReadonlyTable>, FileLock)> {
335 let table = self
336 .extra_metadata_store
337 .get_head_locked()
338 .map_err(GitBackendError::ReadMetadata)?;
339 Ok(table)
340 }
341
342 fn save_extra_metadata_table(
343 &self,
344 mut_table: MutableTable,
345 _table_lock: &FileLock,
346 ) -> BackendResult<()> {
347 let table = self
348 .extra_metadata_store
349 .save_table(mut_table)
350 .map_err(GitBackendError::WriteMetadata)?;
351 *self.cached_extra_metadata.lock().unwrap() = Some(table);
354 Ok(())
355 }
356
357 #[tracing::instrument(skip(self, head_ids))]
362 pub fn import_head_commits<'a>(
363 &self,
364 head_ids: impl IntoIterator<Item = &'a CommitId>,
365 ) -> BackendResult<()> {
366 let head_ids: HashSet<&CommitId> = head_ids
367 .into_iter()
368 .filter(|&id| *id != self.root_commit_id)
369 .collect();
370 if head_ids.is_empty() {
371 return Ok(());
372 }
373
374 let locked_repo = self.lock_git_repo();
377 locked_repo
378 .edit_references(head_ids.iter().copied().map(to_no_gc_ref_update))
379 .map_err(|err| BackendError::Other(Box::new(err)))?;
380
381 tracing::debug!(
384 heads_count = head_ids.len(),
385 "import extra metadata entries"
386 );
387 let (table, table_lock) = self.read_extra_metadata_table_locked()?;
388 let mut mut_table = table.start_mutation();
389 import_extra_metadata_entries_from_heads(
390 &locked_repo,
391 &mut mut_table,
392 &table_lock,
393 &head_ids,
394 )?;
395 self.save_extra_metadata_table(mut_table, &table_lock)
396 }
397
398 fn read_file_sync(&self, id: &FileId) -> BackendResult<Box<dyn Read>> {
399 let git_blob_id = validate_git_object_id(id)?;
400 let locked_repo = self.lock_git_repo();
401 let mut blob = locked_repo
402 .find_object(git_blob_id)
403 .map_err(|err| map_not_found_err(err, id))?
404 .try_into_blob()
405 .map_err(|err| to_read_object_err(err, id))?;
406 Ok(Box::new(Cursor::new(blob.take_data())))
407 }
408
409 fn new_diff_platform(&self) -> BackendResult<gix::diff::blob::Platform> {
410 let attributes = gix::worktree::Stack::new(
411 Path::new(""),
412 gix::worktree::stack::State::AttributesStack(Default::default()),
413 gix::worktree::glob::pattern::Case::Sensitive,
414 Vec::new(),
415 Vec::new(),
416 );
417 let filter = gix::diff::blob::Pipeline::new(
418 Default::default(),
419 gix::filter::plumbing::Pipeline::new(
420 self.git_repo()
421 .command_context()
422 .map_err(|err| BackendError::Other(Box::new(err)))?,
423 Default::default(),
424 ),
425 Vec::new(),
426 Default::default(),
427 );
428 Ok(gix::diff::blob::Platform::new(
429 Default::default(),
430 filter,
431 gix::diff::blob::pipeline::Mode::ToGit,
432 attributes,
433 ))
434 }
435
436 fn read_tree_for_commit<'repo>(
437 &self,
438 repo: &'repo gix::Repository,
439 id: &CommitId,
440 ) -> BackendResult<gix::Tree<'repo>> {
441 let tree = self.read_commit(id).block_on()?.root_tree.to_merge();
442 let tree_id = tree.first().clone();
444 let gix_id = validate_git_object_id(&tree_id)?;
445 repo.find_object(gix_id)
446 .map_err(|err| map_not_found_err(err, &tree_id))?
447 .try_into_tree()
448 .map_err(|err| to_read_object_err(err, &tree_id))
449 }
450}
451
452pub fn canonicalize_git_repo_path(path: &Path) -> io::Result<PathBuf> {
459 if path.ends_with(".git") {
460 let workdir = path.parent().unwrap();
461 dunce::canonicalize(workdir).map(|dir| dir.join(".git"))
462 } else {
463 dunce::canonicalize(path)
464 }
465}
466
467fn gix_open_opts_from_settings(settings: &UserSettings) -> gix::open::Options {
468 let user_name = settings.user_name();
469 let user_email = settings.user_email();
470 gix::open::Options::default()
471 .config_overrides([
472 format!("author.name={user_name}"),
475 format!("author.email={user_email}"),
476 format!("committer.name={user_name}"),
477 format!("committer.email={user_email}"),
478 ])
479 .open_path_as_is(true)
481 .strict_config(true)
483}
484
485fn root_tree_from_header(git_commit: &CommitRef) -> Result<Option<MergedTreeId>, ()> {
487 for (key, value) in &git_commit.extra_headers {
488 if *key == JJ_TREES_COMMIT_HEADER {
489 let mut tree_ids = SmallVec::new();
490 for hex in str::from_utf8(value.as_ref()).or(Err(()))?.split(' ') {
491 let tree_id = TreeId::try_from_hex(hex).or(Err(()))?;
492 if tree_id.as_bytes().len() != HASH_LENGTH {
493 return Err(());
494 }
495 tree_ids.push(tree_id);
496 }
497 if tree_ids.len() == 1 || tree_ids.len() % 2 == 0 {
501 return Err(());
502 }
503 return Ok(Some(MergedTreeId::Merge(Merge::from_vec(tree_ids))));
504 }
505 }
506 Ok(None)
507}
508
509fn commit_from_git_without_root_parent(
510 id: &CommitId,
511 git_object: &gix::Object,
512 uses_tree_conflict_format: bool,
513 is_shallow: bool,
514) -> BackendResult<Commit> {
515 let commit = git_object
516 .try_to_commit_ref()
517 .map_err(|err| to_read_object_err(err, id))?;
518
519 let change_id = ChangeId::new(
526 id.as_bytes()[4..HASH_LENGTH]
527 .iter()
528 .rev()
529 .map(|b| b.reverse_bits())
530 .collect(),
531 );
532 let parents = if is_shallow {
536 vec![]
537 } else {
538 commit
539 .parents()
540 .map(|oid| CommitId::from_bytes(oid.as_bytes()))
541 .collect_vec()
542 };
543 let tree_id = TreeId::from_bytes(commit.tree().as_bytes());
544 let root_tree = root_tree_from_header(&commit)
547 .map_err(|()| to_read_object_err("Invalid jj:trees header", id))?;
548 let root_tree = root_tree.unwrap_or_else(|| {
549 if uses_tree_conflict_format {
550 MergedTreeId::resolved(tree_id)
551 } else {
552 MergedTreeId::Legacy(tree_id)
553 }
554 });
555 let description = String::from_utf8_lossy(commit.message).into_owned();
559 let author = signature_from_git(commit.author());
560 let committer = signature_from_git(commit.committer());
561
562 let secure_sig = commit
569 .extra_headers
570 .iter()
571 .any(|(k, _)| *k == "gpgsig" || *k == "gpgsig-sha256")
573 .then(|| CommitRefIter::signature(&git_object.data))
574 .transpose()
575 .map_err(|err| to_read_object_err(err, id))?
576 .flatten()
577 .map(|(sig, data)| SecureSig {
578 data: data.to_bstring().into(),
579 sig: sig.into_owned().into(),
580 });
581
582 Ok(Commit {
583 parents,
584 predecessors: vec![],
585 root_tree,
587 change_id,
588 description,
589 author,
590 committer,
591 secure_sig,
592 })
593}
594
595const EMPTY_STRING_PLACEHOLDER: &str = "JJ_EMPTY_STRING";
596
597fn signature_from_git(signature: gix::actor::SignatureRef) -> Signature {
598 let name = signature.name;
599 let name = if name != EMPTY_STRING_PLACEHOLDER {
600 String::from_utf8_lossy(name).into_owned()
601 } else {
602 "".to_string()
603 };
604 let email = signature.email;
605 let email = if email != EMPTY_STRING_PLACEHOLDER {
606 String::from_utf8_lossy(email).into_owned()
607 } else {
608 "".to_string()
609 };
610 let timestamp = MillisSinceEpoch(signature.time.seconds * 1000);
611 let tz_offset = signature.time.offset.div_euclid(60); Signature {
613 name,
614 email,
615 timestamp: Timestamp {
616 timestamp,
617 tz_offset,
618 },
619 }
620}
621
622fn signature_to_git(signature: &Signature) -> gix::actor::SignatureRef<'_> {
623 let name = if !signature.name.is_empty() {
625 &signature.name
626 } else {
627 EMPTY_STRING_PLACEHOLDER
628 };
629 let email = if !signature.email.is_empty() {
630 &signature.email
631 } else {
632 EMPTY_STRING_PLACEHOLDER
633 };
634 let time = gix::date::Time::new(
635 signature.timestamp.timestamp.0.div_euclid(1000),
636 signature.timestamp.tz_offset * 60, );
638 gix::actor::SignatureRef {
639 name: name.into(),
640 email: email.into(),
641 time,
642 }
643}
644
645fn serialize_extras(commit: &Commit) -> Vec<u8> {
646 let mut proto = crate::protos::git_store::Commit {
647 change_id: commit.change_id.to_bytes(),
648 ..Default::default()
649 };
650 if let MergedTreeId::Merge(tree_ids) = &commit.root_tree {
651 proto.uses_tree_conflict_format = true;
652 if !tree_ids.is_resolved() {
653 proto.root_tree = tree_ids.iter().map(|r| r.to_bytes()).collect();
657 }
658 }
659 for predecessor in &commit.predecessors {
660 proto.predecessors.push(predecessor.to_bytes());
661 }
662 proto.encode_to_vec()
663}
664
665fn deserialize_extras(commit: &mut Commit, bytes: &[u8]) {
666 let proto = crate::protos::git_store::Commit::decode(bytes).unwrap();
667 commit.change_id = ChangeId::new(proto.change_id);
668 if let MergedTreeId::Legacy(legacy_tree_id) = &commit.root_tree {
669 if proto.uses_tree_conflict_format {
670 if !proto.root_tree.is_empty() {
671 let merge_builder: MergeBuilder<_> = proto
672 .root_tree
673 .iter()
674 .map(|id_bytes| TreeId::from_bytes(id_bytes))
675 .collect();
676 commit.root_tree = MergedTreeId::Merge(merge_builder.build());
677 } else {
678 commit.root_tree = MergedTreeId::resolved(legacy_tree_id.clone());
682 }
683 }
684 }
685 for predecessor in &proto.predecessors {
686 commit.predecessors.push(CommitId::from_bytes(predecessor));
687 }
688}
689
690fn to_no_gc_ref_update(id: &CommitId) -> gix::refs::transaction::RefEdit {
693 let name = format!("{NO_GC_REF_NAMESPACE}{id}");
694 let new = gix::refs::Target::Object(gix::ObjectId::from_bytes_or_panic(id.as_bytes()));
695 let expected = gix::refs::transaction::PreviousValue::ExistingMustMatch(new.clone());
696 gix::refs::transaction::RefEdit {
697 change: gix::refs::transaction::Change::Update {
698 log: gix::refs::transaction::LogChange {
699 message: "used by jj".into(),
700 ..Default::default()
701 },
702 expected,
703 new,
704 },
705 name: name.try_into().unwrap(),
706 deref: false,
707 }
708}
709
710fn to_ref_deletion(git_ref: gix::refs::Reference) -> gix::refs::transaction::RefEdit {
711 let expected = gix::refs::transaction::PreviousValue::ExistingMustMatch(git_ref.target);
712 gix::refs::transaction::RefEdit {
713 change: gix::refs::transaction::Change::Delete {
714 expected,
715 log: gix::refs::transaction::RefLog::AndReference,
716 },
717 name: git_ref.name,
718 deref: false,
719 }
720}
721
722fn recreate_no_gc_refs(
725 git_repo: &gix::Repository,
726 new_heads: impl IntoIterator<Item = CommitId>,
727 keep_newer: SystemTime,
728) -> BackendResult<()> {
729 let new_heads: HashSet<CommitId> = new_heads.into_iter().collect();
731 let mut no_gc_refs_to_keep_count: usize = 0;
732 let mut no_gc_refs_to_delete: Vec<gix::refs::Reference> = Vec::new();
733 let git_references = git_repo
734 .references()
735 .map_err(|err| BackendError::Other(err.into()))?;
736 let no_gc_refs_iter = git_references
737 .prefixed(NO_GC_REF_NAMESPACE)
738 .map_err(|err| BackendError::Other(err.into()))?;
739 for git_ref in no_gc_refs_iter {
740 let git_ref = git_ref.map_err(BackendError::Other)?.detach();
741 let oid = git_ref.target.try_id().ok_or_else(|| {
742 let name = git_ref.name.as_bstr();
743 BackendError::Other(format!("Symbolic no-gc ref found: {name}").into())
744 })?;
745 let id = CommitId::from_bytes(oid.as_bytes());
746 let name_good = git_ref.name.as_bstr()[NO_GC_REF_NAMESPACE.len()..] == id.hex();
747 if new_heads.contains(&id) && name_good {
748 no_gc_refs_to_keep_count += 1;
749 continue;
750 }
751 let loose_ref_path = git_repo.path().join(git_ref.name.to_path());
761 if let Ok(metadata) = loose_ref_path.metadata() {
762 let mtime = metadata.modified().expect("unsupported platform?");
763 if mtime > keep_newer {
764 tracing::trace!(?git_ref, "not deleting new");
765 no_gc_refs_to_keep_count += 1;
766 continue;
767 }
768 }
769 tracing::trace!(?git_ref, ?name_good, "will delete");
771 no_gc_refs_to_delete.push(git_ref);
772 }
773 tracing::info!(
774 new_heads_count = new_heads.len(),
775 no_gc_refs_to_keep_count,
776 no_gc_refs_to_delete_count = no_gc_refs_to_delete.len(),
777 "collected reachable refs"
778 );
779
780 let ref_edits = itertools::chain(
782 no_gc_refs_to_delete.into_iter().map(to_ref_deletion),
783 new_heads.iter().map(to_no_gc_ref_update),
784 );
785 git_repo
786 .edit_references(ref_edits)
787 .map_err(|err| BackendError::Other(err.into()))?;
788
789 Ok(())
790}
791
792fn run_git_gc(git_dir: &Path) -> Result<(), GitGcError> {
793 let mut git = Command::new("git");
794 git.arg("--git-dir=."); git.arg("gc");
796 git.current_dir(git_dir);
799 let status = git.status().map_err(GitGcError::GcCommand)?;
801 if !status.success() {
802 return Err(GitGcError::GcCommandErrorStatus(status));
803 }
804 Ok(())
805}
806
807fn validate_git_object_id(id: &impl ObjectId) -> BackendResult<gix::ObjectId> {
808 if id.as_bytes().len() != HASH_LENGTH {
809 return Err(BackendError::InvalidHashLength {
810 expected: HASH_LENGTH,
811 actual: id.as_bytes().len(),
812 object_type: id.object_type(),
813 hash: id.hex(),
814 });
815 }
816 Ok(gix::ObjectId::from_bytes_or_panic(id.as_bytes()))
817}
818
819fn map_not_found_err(err: gix::object::find::existing::Error, id: &impl ObjectId) -> BackendError {
820 if matches!(err, gix::object::find::existing::Error::NotFound { .. }) {
821 BackendError::ObjectNotFound {
822 object_type: id.object_type(),
823 hash: id.hex(),
824 source: Box::new(err),
825 }
826 } else {
827 to_read_object_err(err, id)
828 }
829}
830
831fn to_read_object_err(
832 err: impl Into<Box<dyn std::error::Error + Send + Sync>>,
833 id: &impl ObjectId,
834) -> BackendError {
835 BackendError::ReadObject {
836 object_type: id.object_type(),
837 hash: id.hex(),
838 source: err.into(),
839 }
840}
841
842fn to_invalid_utf8_err(source: str::Utf8Error, id: &impl ObjectId) -> BackendError {
843 BackendError::InvalidUtf8 {
844 object_type: id.object_type(),
845 hash: id.hex(),
846 source,
847 }
848}
849
850fn import_extra_metadata_entries_from_heads(
851 git_repo: &gix::Repository,
852 mut_table: &mut MutableTable,
853 _table_lock: &FileLock,
854 head_ids: &HashSet<&CommitId>,
855) -> BackendResult<()> {
856 let shallow_commits = git_repo
857 .shallow_commits()
858 .map_err(|e| BackendError::Other(Box::new(e)))?;
859
860 let mut work_ids = head_ids
861 .iter()
862 .filter(|&id| mut_table.get_value(id.as_bytes()).is_none())
863 .map(|&id| id.clone())
864 .collect_vec();
865 while let Some(id) = work_ids.pop() {
866 let git_object = git_repo
867 .find_object(validate_git_object_id(&id)?)
868 .map_err(|err| map_not_found_err(err, &id))?;
869 let is_shallow = shallow_commits
870 .as_ref()
871 .is_some_and(|shallow| shallow.contains(&git_object.id));
872 let commit = commit_from_git_without_root_parent(&id, &git_object, true, is_shallow)?;
876 mut_table.add_entry(id.to_bytes(), serialize_extras(&commit));
877 work_ids.extend(
878 commit
879 .parents
880 .into_iter()
881 .filter(|id| mut_table.get_value(id.as_bytes()).is_none()),
882 );
883 }
884 Ok(())
885}
886
887impl Debug for GitBackend {
888 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
889 f.debug_struct("GitBackend")
890 .field("path", &self.git_repo_path())
891 .finish()
892 }
893}
894
895#[async_trait]
896impl Backend for GitBackend {
897 fn as_any(&self) -> &dyn Any {
898 self
899 }
900
901 fn name(&self) -> &str {
902 Self::name()
903 }
904
905 fn commit_id_length(&self) -> usize {
906 HASH_LENGTH
907 }
908
909 fn change_id_length(&self) -> usize {
910 CHANGE_ID_LENGTH
911 }
912
913 fn root_commit_id(&self) -> &CommitId {
914 &self.root_commit_id
915 }
916
917 fn root_change_id(&self) -> &ChangeId {
918 &self.root_change_id
919 }
920
921 fn empty_tree_id(&self) -> &TreeId {
922 &self.empty_tree_id
923 }
924
925 fn concurrency(&self) -> usize {
926 1
927 }
928
929 async fn read_file(&self, _path: &RepoPath, id: &FileId) -> BackendResult<Box<dyn Read>> {
930 self.read_file_sync(id)
931 }
932
933 async fn write_file(
934 &self,
935 _path: &RepoPath,
936 contents: &mut (dyn Read + Send),
937 ) -> BackendResult<FileId> {
938 let mut bytes = Vec::new();
939 contents.read_to_end(&mut bytes).unwrap();
940 let locked_repo = self.lock_git_repo();
941 let oid = locked_repo
942 .write_blob(bytes)
943 .map_err(|err| BackendError::WriteObject {
944 object_type: "file",
945 source: Box::new(err),
946 })?;
947 Ok(FileId::new(oid.as_bytes().to_vec()))
948 }
949
950 async fn read_symlink(&self, _path: &RepoPath, id: &SymlinkId) -> BackendResult<String> {
951 let git_blob_id = validate_git_object_id(id)?;
952 let locked_repo = self.lock_git_repo();
953 let mut blob = locked_repo
954 .find_object(git_blob_id)
955 .map_err(|err| map_not_found_err(err, id))?
956 .try_into_blob()
957 .map_err(|err| to_read_object_err(err, id))?;
958 let target = String::from_utf8(blob.take_data())
959 .map_err(|err| to_invalid_utf8_err(err.utf8_error(), id))?;
960 Ok(target)
961 }
962
963 async fn write_symlink(&self, _path: &RepoPath, target: &str) -> BackendResult<SymlinkId> {
964 let locked_repo = self.lock_git_repo();
965 let oid =
966 locked_repo
967 .write_blob(target.as_bytes())
968 .map_err(|err| BackendError::WriteObject {
969 object_type: "symlink",
970 source: Box::new(err),
971 })?;
972 Ok(SymlinkId::new(oid.as_bytes().to_vec()))
973 }
974
975 async fn read_tree(&self, _path: &RepoPath, id: &TreeId) -> BackendResult<Tree> {
976 if id == &self.empty_tree_id {
977 return Ok(Tree::default());
978 }
979 let git_tree_id = validate_git_object_id(id)?;
980
981 let locked_repo = self.lock_git_repo();
982 let git_tree = locked_repo
983 .find_object(git_tree_id)
984 .map_err(|err| map_not_found_err(err, id))?
985 .try_into_tree()
986 .map_err(|err| to_read_object_err(err, id))?;
987 let mut tree = Tree::default();
988 for entry in git_tree.iter() {
989 let entry = entry.map_err(|err| to_read_object_err(err, id))?;
990 let name =
991 str::from_utf8(entry.filename()).map_err(|err| to_invalid_utf8_err(err, id))?;
992 let (name, value) = match entry.mode().kind() {
993 gix::object::tree::EntryKind::Tree => {
994 let id = TreeId::from_bytes(entry.oid().as_bytes());
995 (name, TreeValue::Tree(id))
996 }
997 gix::object::tree::EntryKind::Blob => {
998 let id = FileId::from_bytes(entry.oid().as_bytes());
999 if let Some(basename) = name.strip_suffix(CONFLICT_SUFFIX) {
1000 (
1001 basename,
1002 TreeValue::Conflict(ConflictId::from_bytes(entry.oid().as_bytes())),
1003 )
1004 } else {
1005 (
1006 name,
1007 TreeValue::File {
1008 id,
1009 executable: false,
1010 },
1011 )
1012 }
1013 }
1014 gix::object::tree::EntryKind::BlobExecutable => {
1015 let id = FileId::from_bytes(entry.oid().as_bytes());
1016 (
1017 name,
1018 TreeValue::File {
1019 id,
1020 executable: true,
1021 },
1022 )
1023 }
1024 gix::object::tree::EntryKind::Link => {
1025 let id = SymlinkId::from_bytes(entry.oid().as_bytes());
1026 (name, TreeValue::Symlink(id))
1027 }
1028 gix::object::tree::EntryKind::Commit => {
1029 let id = CommitId::from_bytes(entry.oid().as_bytes());
1030 (name, TreeValue::GitSubmodule(id))
1031 }
1032 };
1033 tree.set(RepoPathComponentBuf::from(name), value);
1034 }
1035 Ok(tree)
1036 }
1037
1038 async fn write_tree(&self, _path: &RepoPath, contents: &Tree) -> BackendResult<TreeId> {
1039 let entries = contents
1042 .entries()
1043 .map(|entry| {
1044 let name = entry.name().as_internal_str();
1045 match entry.value() {
1046 TreeValue::File {
1047 id,
1048 executable: false,
1049 } => gix::objs::tree::Entry {
1050 mode: gix::object::tree::EntryKind::Blob.into(),
1051 filename: name.into(),
1052 oid: gix::ObjectId::from_bytes_or_panic(id.as_bytes()),
1053 },
1054 TreeValue::File {
1055 id,
1056 executable: true,
1057 } => gix::objs::tree::Entry {
1058 mode: gix::object::tree::EntryKind::BlobExecutable.into(),
1059 filename: name.into(),
1060 oid: gix::ObjectId::from_bytes_or_panic(id.as_bytes()),
1061 },
1062 TreeValue::Symlink(id) => gix::objs::tree::Entry {
1063 mode: gix::object::tree::EntryKind::Link.into(),
1064 filename: name.into(),
1065 oid: gix::ObjectId::from_bytes_or_panic(id.as_bytes()),
1066 },
1067 TreeValue::Tree(id) => gix::objs::tree::Entry {
1068 mode: gix::object::tree::EntryKind::Tree.into(),
1069 filename: name.into(),
1070 oid: gix::ObjectId::from_bytes_or_panic(id.as_bytes()),
1071 },
1072 TreeValue::GitSubmodule(id) => gix::objs::tree::Entry {
1073 mode: gix::object::tree::EntryKind::Commit.into(),
1074 filename: name.into(),
1075 oid: gix::ObjectId::from_bytes_or_panic(id.as_bytes()),
1076 },
1077 TreeValue::Conflict(id) => gix::objs::tree::Entry {
1078 mode: gix::object::tree::EntryKind::Blob.into(),
1079 filename: (name.to_owned() + CONFLICT_SUFFIX).into(),
1080 oid: gix::ObjectId::from_bytes_or_panic(id.as_bytes()),
1081 },
1082 }
1083 })
1084 .sorted_unstable()
1085 .collect();
1086 let locked_repo = self.lock_git_repo();
1087 let oid = locked_repo
1088 .write_object(gix::objs::Tree { entries })
1089 .map_err(|err| BackendError::WriteObject {
1090 object_type: "tree",
1091 source: Box::new(err),
1092 })?;
1093 Ok(TreeId::from_bytes(oid.as_bytes()))
1094 }
1095
1096 fn read_conflict(&self, _path: &RepoPath, id: &ConflictId) -> BackendResult<Conflict> {
1097 let mut file = self.read_file_sync(&FileId::new(id.to_bytes()))?;
1098 let mut data = String::new();
1099 file.read_to_string(&mut data)
1100 .map_err(|err| BackendError::ReadObject {
1101 object_type: "conflict".to_owned(),
1102 hash: id.hex(),
1103 source: err.into(),
1104 })?;
1105 let json: serde_json::Value = serde_json::from_str(&data).unwrap();
1106 Ok(Conflict {
1107 removes: conflict_term_list_from_json(json.get("removes").unwrap()),
1108 adds: conflict_term_list_from_json(json.get("adds").unwrap()),
1109 })
1110 }
1111
1112 fn write_conflict(&self, _path: &RepoPath, conflict: &Conflict) -> BackendResult<ConflictId> {
1113 let json = serde_json::json!({
1114 "removes": conflict_term_list_to_json(&conflict.removes),
1115 "adds": conflict_term_list_to_json(&conflict.adds),
1116 });
1117 let json_string = json.to_string();
1118 let bytes = json_string.as_bytes();
1119 let locked_repo = self.lock_git_repo();
1120 let oid = locked_repo
1121 .write_blob(bytes)
1122 .map_err(|err| BackendError::WriteObject {
1123 object_type: "conflict",
1124 source: Box::new(err),
1125 })?;
1126 Ok(ConflictId::from_bytes(oid.as_bytes()))
1127 }
1128
1129 #[tracing::instrument(skip(self))]
1130 async fn read_commit(&self, id: &CommitId) -> BackendResult<Commit> {
1131 if *id == self.root_commit_id {
1132 return Ok(make_root_commit(
1133 self.root_change_id().clone(),
1134 self.empty_tree_id.clone(),
1135 ));
1136 }
1137 let git_commit_id = validate_git_object_id(id)?;
1138
1139 let mut commit = {
1140 let locked_repo = self.lock_git_repo();
1141 let git_object = locked_repo
1142 .find_object(git_commit_id)
1143 .map_err(|err| map_not_found_err(err, id))?;
1144 let is_shallow = locked_repo
1145 .shallow_commits()
1146 .ok()
1147 .flatten()
1148 .is_some_and(|shallow| shallow.contains(&git_object.id));
1149 commit_from_git_without_root_parent(id, &git_object, false, is_shallow)?
1150 };
1151 if commit.parents.is_empty() {
1152 commit.parents.push(self.root_commit_id.clone());
1153 };
1154
1155 let table = self.cached_extra_metadata_table()?;
1156 if let Some(extras) = table.get_value(id.as_bytes()) {
1157 deserialize_extras(&mut commit, extras);
1158 } else {
1159 tracing::info!("unimported Git commit found");
1164 self.import_head_commits([id])?;
1165 let table = self.cached_extra_metadata_table()?;
1166 let extras = table.get_value(id.as_bytes()).unwrap();
1167 deserialize_extras(&mut commit, extras);
1168 }
1169 Ok(commit)
1170 }
1171
1172 async fn write_commit(
1173 &self,
1174 mut contents: Commit,
1175 mut sign_with: Option<&mut SigningFn>,
1176 ) -> BackendResult<(CommitId, Commit)> {
1177 assert!(contents.secure_sig.is_none(), "commit.secure_sig was set");
1178
1179 let locked_repo = self.lock_git_repo();
1180 let git_tree_id = match &contents.root_tree {
1181 MergedTreeId::Legacy(tree_id) => validate_git_object_id(tree_id)?,
1182 MergedTreeId::Merge(tree_ids) => match tree_ids.as_resolved() {
1183 Some(tree_id) => validate_git_object_id(tree_id)?,
1184 None => write_tree_conflict(&locked_repo, tree_ids)?,
1185 },
1186 };
1187 let author = signature_to_git(&contents.author);
1188 let mut committer = signature_to_git(&contents.committer);
1189 let message = &contents.description;
1190 if contents.parents.is_empty() {
1191 return Err(BackendError::Other(
1192 "Cannot write a commit with no parents".into(),
1193 ));
1194 }
1195 let mut parents = SmallVec::new();
1196 for parent_id in &contents.parents {
1197 if *parent_id == self.root_commit_id {
1198 if contents.parents.len() > 1 {
1203 return Err(BackendError::Unsupported(
1204 "The Git backend does not support creating merge commits with the root \
1205 commit as one of the parents."
1206 .to_owned(),
1207 ));
1208 }
1209 } else {
1210 parents.push(validate_git_object_id(parent_id)?);
1211 }
1212 }
1213 let mut extra_headers = vec![];
1214 if let MergedTreeId::Merge(tree_ids) = &contents.root_tree {
1215 if !tree_ids.is_resolved() {
1216 let value = tree_ids.iter().map(|id| id.hex()).join(" ").into_bytes();
1217 extra_headers.push((
1218 BString::new(JJ_TREES_COMMIT_HEADER.to_vec()),
1219 BString::new(value),
1220 ));
1221 }
1222 }
1223 let extras = serialize_extras(&contents);
1224
1225 let (table, table_lock) = self.read_extra_metadata_table_locked()?;
1232 let id = loop {
1233 let mut commit = gix::objs::Commit {
1234 message: message.to_owned().into(),
1235 tree: git_tree_id,
1236 author: author.into(),
1237 committer: committer.into(),
1238 encoding: None,
1239 parents: parents.clone(),
1240 extra_headers: extra_headers.clone(),
1241 };
1242
1243 if let Some(sign) = &mut sign_with {
1244 let mut data = Vec::with_capacity(512);
1246 commit.write_to(&mut data).unwrap();
1247
1248 let sig = sign(&data).map_err(|err| BackendError::WriteObject {
1249 object_type: "commit",
1250 source: Box::new(err),
1251 })?;
1252 commit
1253 .extra_headers
1254 .push(("gpgsig".into(), sig.clone().into()));
1255 contents.secure_sig = Some(SecureSig { data, sig });
1256 }
1257
1258 let git_id =
1259 locked_repo
1260 .write_object(&commit)
1261 .map_err(|err| BackendError::WriteObject {
1262 object_type: "commit",
1263 source: Box::new(err),
1264 })?;
1265
1266 match table.get_value(git_id.as_bytes()) {
1267 Some(existing_extras) if existing_extras != extras => {
1268 committer.time.seconds -= 1;
1271 }
1272 _ => break CommitId::from_bytes(git_id.as_bytes()),
1273 }
1274 };
1275
1276 locked_repo
1279 .edit_reference(to_no_gc_ref_update(&id))
1280 .map_err(|err| BackendError::Other(Box::new(err)))?;
1281
1282 contents.committer.timestamp.timestamp = MillisSinceEpoch(committer.time.seconds * 1000);
1285 let mut mut_table = table.start_mutation();
1286 mut_table.add_entry(id.to_bytes(), extras);
1287 self.save_extra_metadata_table(mut_table, &table_lock)?;
1288 Ok((id, contents))
1289 }
1290
1291 fn get_copy_records(
1292 &self,
1293 paths: Option<&[RepoPathBuf]>,
1294 root_id: &CommitId,
1295 head_id: &CommitId,
1296 ) -> BackendResult<BoxStream<BackendResult<CopyRecord>>> {
1297 let repo = self.git_repo();
1298 let root_tree = self.read_tree_for_commit(&repo, root_id)?;
1299 let head_tree = self.read_tree_for_commit(&repo, head_id)?;
1300
1301 let change_to_copy_record =
1302 |change: gix::object::tree::diff::Change| -> BackendResult<Option<CopyRecord>> {
1303 let gix::object::tree::diff::Change::Rewrite {
1304 source_location,
1305 source_id,
1306 location: dest_location,
1307 ..
1308 } = change
1309 else {
1310 return Ok(None);
1311 };
1312
1313 let source = str::from_utf8(source_location)
1314 .map_err(|err| to_invalid_utf8_err(err, root_id))?;
1315 let dest = str::from_utf8(dest_location)
1316 .map_err(|err| to_invalid_utf8_err(err, head_id))?;
1317
1318 let target = RepoPathBuf::from_internal_string(dest);
1319 if !paths.is_none_or(|paths| paths.contains(&target)) {
1320 return Ok(None);
1321 }
1322
1323 Ok(Some(CopyRecord {
1324 target,
1325 target_commit: head_id.clone(),
1326 source: RepoPathBuf::from_internal_string(source),
1327 source_file: FileId::from_bytes(source_id.as_bytes()),
1328 source_commit: root_id.clone(),
1329 }))
1330 };
1331
1332 let mut records: Vec<BackendResult<CopyRecord>> = Vec::new();
1333 root_tree
1334 .changes()
1335 .map_err(|err| BackendError::Other(err.into()))?
1336 .options(|opts| {
1337 opts.track_path().track_rewrites(Some(gix::diff::Rewrites {
1338 copies: Some(gix::diff::rewrites::Copies {
1339 source: gix::diff::rewrites::CopySource::FromSetOfModifiedFiles,
1340 percentage: Some(0.5),
1341 }),
1342 percentage: Some(0.5),
1343 limit: 1000,
1344 track_empty: false,
1345 }));
1346 })
1347 .for_each_to_obtain_tree_with_cache(
1348 &head_tree,
1349 &mut self.new_diff_platform()?,
1350 |change| -> BackendResult<_> {
1351 match change_to_copy_record(change) {
1352 Ok(None) => {}
1353 Ok(Some(change)) => records.push(Ok(change)),
1354 Err(err) => records.push(Err(err)),
1355 }
1356 Ok(gix::object::tree::diff::Action::Continue)
1357 },
1358 )
1359 .map_err(|err| BackendError::Other(err.into()))?;
1360 Ok(Box::pin(futures::stream::iter(records)))
1361 }
1362
1363 #[tracing::instrument(skip(self, index))]
1364 fn gc(&self, index: &dyn Index, keep_newer: SystemTime) -> BackendResult<()> {
1365 let git_repo = self.lock_git_repo();
1366 let new_heads = index
1367 .all_heads_for_gc()
1368 .map_err(|err| BackendError::Other(err.into()))?
1369 .filter(|id| *id != self.root_commit_id);
1370 recreate_no_gc_refs(&git_repo, new_heads, keep_newer)?;
1371 run_git_gc(self.git_repo_path()).map_err(|err| BackendError::Other(err.into()))?;
1377 git_repo.refs.force_refresh_packed_buffer().ok();
1380 Ok(())
1381 }
1382}
1383
1384fn write_tree_conflict(
1387 repo: &gix::Repository,
1388 conflict: &Merge<TreeId>,
1389) -> BackendResult<gix::ObjectId> {
1390 let mut entries = itertools::chain(
1392 conflict
1393 .removes()
1394 .enumerate()
1395 .map(|(i, tree_id)| (format!(".jjconflict-base-{i}"), tree_id)),
1396 conflict
1397 .adds()
1398 .enumerate()
1399 .map(|(i, tree_id)| (format!(".jjconflict-side-{i}"), tree_id)),
1400 )
1401 .map(|(name, tree_id)| gix::objs::tree::Entry {
1402 mode: gix::object::tree::EntryKind::Tree.into(),
1403 filename: name.into(),
1404 oid: gix::ObjectId::from_bytes_or_panic(tree_id.as_bytes()),
1405 })
1406 .collect_vec();
1407 let readme_id = repo
1408 .write_blob(
1409 r#"This commit was made by jj, https://github.com/jj-vcs/jj.
1410The commit contains file conflicts, and therefore looks wrong when used with plain
1411Git or other tools that are unfamiliar with jj.
1412
1413The .jjconflict-* directories represent the different inputs to the conflict.
1414For details, see
1415https://jj-vcs.github.io/jj/prerelease/git-compatibility/#format-mapping-details
1416
1417If you see this file in your working copy, it probably means that you used a
1418regular `git` command to check out a conflicted commit. Use `jj abandon` to
1419recover.
1420"#,
1421 )
1422 .map_err(|err| {
1423 BackendError::Other(format!("Failed to write README for conflict tree: {err}").into())
1424 })?
1425 .detach();
1426 entries.push(gix::objs::tree::Entry {
1427 mode: gix::object::tree::EntryKind::Blob.into(),
1428 filename: "README".into(),
1429 oid: readme_id,
1430 });
1431 entries.sort_unstable();
1432 let id = repo
1433 .write_object(gix::objs::Tree { entries })
1434 .map_err(|err| BackendError::WriteObject {
1435 object_type: "tree",
1436 source: Box::new(err),
1437 })?;
1438 Ok(id.detach())
1439}
1440
1441fn conflict_term_list_to_json(parts: &[ConflictTerm]) -> serde_json::Value {
1442 serde_json::Value::Array(parts.iter().map(conflict_term_to_json).collect())
1443}
1444
1445fn conflict_term_list_from_json(json: &serde_json::Value) -> Vec<ConflictTerm> {
1446 json.as_array()
1447 .unwrap()
1448 .iter()
1449 .map(conflict_term_from_json)
1450 .collect()
1451}
1452
1453fn conflict_term_to_json(part: &ConflictTerm) -> serde_json::Value {
1454 serde_json::json!({
1455 "value": tree_value_to_json(&part.value),
1456 })
1457}
1458
1459fn conflict_term_from_json(json: &serde_json::Value) -> ConflictTerm {
1460 let json_value = json.get("value").unwrap();
1461 ConflictTerm {
1462 value: tree_value_from_json(json_value),
1463 }
1464}
1465
1466fn tree_value_to_json(value: &TreeValue) -> serde_json::Value {
1467 match value {
1468 TreeValue::File { id, executable } => serde_json::json!({
1469 "file": {
1470 "id": id.hex(),
1471 "executable": executable,
1472 },
1473 }),
1474 TreeValue::Symlink(id) => serde_json::json!({
1475 "symlink_id": id.hex(),
1476 }),
1477 TreeValue::Tree(id) => serde_json::json!({
1478 "tree_id": id.hex(),
1479 }),
1480 TreeValue::GitSubmodule(id) => serde_json::json!({
1481 "submodule_id": id.hex(),
1482 }),
1483 TreeValue::Conflict(id) => serde_json::json!({
1484 "conflict_id": id.hex(),
1485 }),
1486 }
1487}
1488
1489fn tree_value_from_json(json: &serde_json::Value) -> TreeValue {
1490 if let Some(json_file) = json.get("file") {
1491 TreeValue::File {
1492 id: FileId::new(bytes_vec_from_json(json_file.get("id").unwrap())),
1493 executable: json_file.get("executable").unwrap().as_bool().unwrap(),
1494 }
1495 } else if let Some(json_id) = json.get("symlink_id") {
1496 TreeValue::Symlink(SymlinkId::new(bytes_vec_from_json(json_id)))
1497 } else if let Some(json_id) = json.get("tree_id") {
1498 TreeValue::Tree(TreeId::new(bytes_vec_from_json(json_id)))
1499 } else if let Some(json_id) = json.get("submodule_id") {
1500 TreeValue::GitSubmodule(CommitId::new(bytes_vec_from_json(json_id)))
1501 } else if let Some(json_id) = json.get("conflict_id") {
1502 TreeValue::Conflict(ConflictId::new(bytes_vec_from_json(json_id)))
1503 } else {
1504 panic!("unexpected json value in conflict: {json:#?}");
1505 }
1506}
1507
1508fn bytes_vec_from_json(value: &serde_json::Value) -> Vec<u8> {
1509 hex::decode(value.as_str().unwrap()).unwrap()
1510}
1511
1512#[cfg(test)]
1513mod tests {
1514 use assert_matches::assert_matches;
1515 use hex::ToHex as _;
1516 use pollster::FutureExt as _;
1517
1518 use super::*;
1519 use crate::config::StackedConfig;
1520 use crate::content_hash::blake2b_hash;
1521 use crate::tests::new_temp_dir;
1522
1523 const GIT_USER: &str = "Someone";
1524 const GIT_EMAIL: &str = "someone@example.com";
1525
1526 fn git_config() -> Vec<bstr::BString> {
1527 vec![
1528 format!("user.name = {GIT_USER}").into(),
1529 format!("user.email = {GIT_EMAIL}").into(),
1530 "init.defaultBranch = master".into(),
1531 ]
1532 }
1533
1534 fn open_options() -> gix::open::Options {
1535 gix::open::Options::isolated()
1536 .config_overrides(git_config())
1537 .strict_config(true)
1538 }
1539
1540 fn git_init(directory: impl AsRef<Path>) -> gix::Repository {
1541 gix::ThreadSafeRepository::init_opts(
1542 directory,
1543 gix::create::Kind::WithWorktree,
1544 gix::create::Options::default(),
1545 open_options(),
1546 )
1547 .unwrap()
1548 .to_thread_local()
1549 }
1550
1551 #[test]
1552 fn read_plain_git_commit() {
1553 let settings = user_settings();
1554 let temp_dir = new_temp_dir();
1555 let store_path = temp_dir.path();
1556 let git_repo_path = temp_dir.path().join("git");
1557 let git_repo = git_init(git_repo_path);
1558
1559 let blob1 = git_repo.write_blob(b"content1").unwrap().detach();
1561 let blob2 = git_repo.write_blob(b"normal").unwrap().detach();
1562 let mut dir_tree_editor = git_repo.empty_tree().edit().unwrap();
1563 dir_tree_editor
1564 .upsert("normal", gix::object::tree::EntryKind::Blob, blob1)
1565 .unwrap();
1566 dir_tree_editor
1567 .upsert("symlink", gix::object::tree::EntryKind::Link, blob2)
1568 .unwrap();
1569 let dir_tree_id = dir_tree_editor.write().unwrap().detach();
1570 let mut root_tree_builder = git_repo.empty_tree().edit().unwrap();
1571 root_tree_builder
1572 .upsert("dir", gix::object::tree::EntryKind::Tree, dir_tree_id)
1573 .unwrap();
1574 let root_tree_id = root_tree_builder.write().unwrap().detach();
1575 let git_author = gix::actor::Signature {
1576 name: "git author".into(),
1577 email: "git.author@example.com".into(),
1578 time: gix::date::Time::new(1000, 60 * 60),
1579 };
1580 let git_committer = gix::actor::Signature {
1581 name: "git committer".into(),
1582 email: "git.committer@example.com".into(),
1583 time: gix::date::Time::new(2000, -480 * 60),
1584 };
1585 let git_commit_id = git_repo
1586 .commit_as(
1587 &git_committer,
1588 &git_author,
1589 "refs/heads/dummy",
1590 "git commit message",
1591 root_tree_id,
1592 [] as [gix::ObjectId; 0],
1593 )
1594 .unwrap()
1595 .detach();
1596 git_repo
1597 .find_reference("refs/heads/dummy")
1598 .unwrap()
1599 .delete()
1600 .unwrap();
1601 let commit_id = CommitId::from_hex("efdcea5ca4b3658149f899ca7feee6876d077263");
1602 let change_id = ChangeId::from_hex("c64ee0b6e16777fe53991f9281a6cd25");
1604 assert_eq!(
1606 git_commit_id.as_bytes(),
1607 commit_id.as_bytes(),
1608 "{git_commit_id:?} vs {commit_id:?}"
1609 );
1610
1611 let git_commit_id2 = git_repo
1613 .commit_as(
1614 &git_committer,
1615 &git_author,
1616 "refs/heads/dummy2",
1617 "git commit message 2",
1618 root_tree_id,
1619 [git_commit_id],
1620 )
1621 .unwrap()
1622 .detach();
1623 git_repo
1624 .find_reference("refs/heads/dummy2")
1625 .unwrap()
1626 .delete()
1627 .unwrap();
1628 let commit_id2 = CommitId::from_bytes(git_commit_id2.as_bytes());
1629
1630 let backend = GitBackend::init_external(&settings, store_path, git_repo.path()).unwrap();
1631
1632 backend.import_head_commits([&commit_id2]).unwrap();
1634 let git_refs = backend
1636 .git_repo()
1637 .references()
1638 .unwrap()
1639 .prefixed("refs/jj/keep/")
1640 .unwrap()
1641 .map(|git_ref| git_ref.unwrap().id().detach())
1642 .collect_vec();
1643 assert_eq!(git_refs, vec![git_commit_id2]);
1644
1645 let commit = backend.read_commit(&commit_id).block_on().unwrap();
1646 assert_eq!(&commit.change_id, &change_id);
1647 assert_eq!(commit.parents, vec![CommitId::from_bytes(&[0; 20])]);
1648 assert_eq!(commit.predecessors, vec![]);
1649 assert_eq!(
1650 commit.root_tree.to_merge(),
1651 Merge::resolved(TreeId::from_bytes(root_tree_id.as_bytes()))
1652 );
1653 assert_matches!(commit.root_tree, MergedTreeId::Merge(_));
1654 assert_eq!(commit.description, "git commit message");
1655 assert_eq!(commit.author.name, "git author");
1656 assert_eq!(commit.author.email, "git.author@example.com");
1657 assert_eq!(
1658 commit.author.timestamp.timestamp,
1659 MillisSinceEpoch(1000 * 1000)
1660 );
1661 assert_eq!(commit.author.timestamp.tz_offset, 60);
1662 assert_eq!(commit.committer.name, "git committer");
1663 assert_eq!(commit.committer.email, "git.committer@example.com");
1664 assert_eq!(
1665 commit.committer.timestamp.timestamp,
1666 MillisSinceEpoch(2000 * 1000)
1667 );
1668 assert_eq!(commit.committer.timestamp.tz_offset, -480);
1669
1670 let root_tree = backend
1671 .read_tree(
1672 RepoPath::root(),
1673 &TreeId::from_bytes(root_tree_id.as_bytes()),
1674 )
1675 .block_on()
1676 .unwrap();
1677 let mut root_entries = root_tree.entries();
1678 let dir = root_entries.next().unwrap();
1679 assert_eq!(root_entries.next(), None);
1680 assert_eq!(dir.name().as_internal_str(), "dir");
1681 assert_eq!(
1682 dir.value(),
1683 &TreeValue::Tree(TreeId::from_bytes(dir_tree_id.as_bytes()))
1684 );
1685
1686 let dir_tree = backend
1687 .read_tree(
1688 RepoPath::from_internal_string("dir"),
1689 &TreeId::from_bytes(dir_tree_id.as_bytes()),
1690 )
1691 .block_on()
1692 .unwrap();
1693 let mut entries = dir_tree.entries();
1694 let file = entries.next().unwrap();
1695 let symlink = entries.next().unwrap();
1696 assert_eq!(entries.next(), None);
1697 assert_eq!(file.name().as_internal_str(), "normal");
1698 assert_eq!(
1699 file.value(),
1700 &TreeValue::File {
1701 id: FileId::from_bytes(blob1.as_bytes()),
1702 executable: false
1703 }
1704 );
1705 assert_eq!(symlink.name().as_internal_str(), "symlink");
1706 assert_eq!(
1707 symlink.value(),
1708 &TreeValue::Symlink(SymlinkId::from_bytes(blob2.as_bytes()))
1709 );
1710
1711 let commit2 = backend.read_commit(&commit_id2).block_on().unwrap();
1712 assert_eq!(commit2.parents, vec![commit_id.clone()]);
1713 assert_eq!(commit.predecessors, vec![]);
1714 assert_eq!(
1715 commit.root_tree.to_merge(),
1716 Merge::resolved(TreeId::from_bytes(root_tree_id.as_bytes()))
1717 );
1718 assert_matches!(commit.root_tree, MergedTreeId::Merge(_));
1719 }
1720
1721 #[test]
1722 fn read_git_commit_without_importing() {
1723 let settings = user_settings();
1724 let temp_dir = new_temp_dir();
1725 let store_path = temp_dir.path();
1726 let git_repo_path = temp_dir.path().join("git");
1727 let git_repo = git_init(&git_repo_path);
1728
1729 let signature = gix::actor::Signature {
1730 name: GIT_USER.into(),
1731 email: GIT_EMAIL.into(),
1732 time: gix::date::Time::now_utc(),
1733 };
1734 let empty_tree_id =
1735 gix::ObjectId::from_hex(b"4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap();
1736 let git_commit_id = git_repo
1737 .commit_as(
1738 &signature,
1739 &signature,
1740 "refs/heads/main",
1741 "git commit message",
1742 empty_tree_id,
1743 [] as [gix::ObjectId; 0],
1744 )
1745 .unwrap();
1746
1747 let backend = GitBackend::init_external(&settings, store_path, git_repo.path()).unwrap();
1748
1749 assert!(backend
1752 .read_commit(&CommitId::from_bytes(git_commit_id.as_bytes()))
1753 .block_on()
1754 .is_ok());
1755 assert!(
1756 backend
1757 .cached_extra_metadata_table()
1758 .unwrap()
1759 .get_value(git_commit_id.as_bytes())
1760 .is_some(),
1761 "extra metadata should have been be created"
1762 );
1763 }
1764
1765 #[test]
1766 fn read_signed_git_commit() {
1767 let settings = user_settings();
1768 let temp_dir = new_temp_dir();
1769 let store_path = temp_dir.path();
1770 let git_repo_path = temp_dir.path().join("git");
1771 let git_repo = git_init(git_repo_path);
1772
1773 let signature = gix::actor::Signature {
1774 name: GIT_USER.into(),
1775 email: GIT_EMAIL.into(),
1776 time: gix::date::Time::now_utc(),
1777 };
1778 let empty_tree_id =
1779 gix::ObjectId::from_hex(b"4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap();
1780
1781 let secure_sig =
1782 "here are some ASCII bytes to be used as a test signature\n\ndefinitely not PGP\n";
1783
1784 let mut commit = gix::objs::Commit {
1785 tree: empty_tree_id,
1786 parents: smallvec::SmallVec::new(),
1787 author: signature.clone(),
1788 committer: signature.clone(),
1789 encoding: None,
1790 message: "git commit message".into(),
1791 extra_headers: Vec::new(),
1792 };
1793
1794 let mut commit_buf = Vec::new();
1795 commit.write_to(&mut commit_buf).unwrap();
1796 let commit_str = std::str::from_utf8(&commit_buf).unwrap();
1797
1798 commit
1799 .extra_headers
1800 .push(("gpgsig".into(), secure_sig.into()));
1801
1802 let git_commit_id = git_repo.write_object(&commit).unwrap();
1803
1804 let backend = GitBackend::init_external(&settings, store_path, git_repo.path()).unwrap();
1805
1806 let commit = backend
1807 .read_commit(&CommitId::from_bytes(git_commit_id.as_bytes()))
1808 .block_on()
1809 .unwrap();
1810
1811 let sig = commit.secure_sig.expect("failed to read the signature");
1812
1813 assert_eq!(std::str::from_utf8(&sig.sig).unwrap(), secure_sig);
1815 assert_eq!(std::str::from_utf8(&sig.data).unwrap(), commit_str);
1816 }
1817
1818 #[test]
1819 fn read_empty_string_placeholder() {
1820 let git_signature1 = gix::actor::SignatureRef {
1821 name: EMPTY_STRING_PLACEHOLDER.into(),
1822 email: "git.author@example.com".into(),
1823 time: gix::date::Time::new(1000, 60 * 60),
1824 };
1825 let signature1 = signature_from_git(git_signature1);
1826 assert!(signature1.name.is_empty());
1827 assert_eq!(signature1.email, "git.author@example.com");
1828 let git_signature2 = gix::actor::SignatureRef {
1829 name: "git committer".into(),
1830 email: EMPTY_STRING_PLACEHOLDER.into(),
1831 time: gix::date::Time::new(2000, -480 * 60),
1832 };
1833 let signature2 = signature_from_git(git_signature2);
1834 assert_eq!(signature2.name, "git committer");
1835 assert!(signature2.email.is_empty());
1836 }
1837
1838 #[test]
1839 fn write_empty_string_placeholder() {
1840 let signature1 = Signature {
1841 name: "".to_string(),
1842 email: "someone@example.com".to_string(),
1843 timestamp: Timestamp {
1844 timestamp: MillisSinceEpoch(0),
1845 tz_offset: 0,
1846 },
1847 };
1848 let git_signature1 = signature_to_git(&signature1);
1849 assert_eq!(git_signature1.name, EMPTY_STRING_PLACEHOLDER);
1850 assert_eq!(git_signature1.email, "someone@example.com");
1851 let signature2 = Signature {
1852 name: "Someone".to_string(),
1853 email: "".to_string(),
1854 timestamp: Timestamp {
1855 timestamp: MillisSinceEpoch(0),
1856 tz_offset: 0,
1857 },
1858 };
1859 let git_signature2 = signature_to_git(&signature2);
1860 assert_eq!(git_signature2.name, "Someone");
1861 assert_eq!(git_signature2.email, EMPTY_STRING_PLACEHOLDER);
1862 }
1863
1864 #[test]
1866 fn git_commit_parents() {
1867 let settings = user_settings();
1868 let temp_dir = new_temp_dir();
1869 let store_path = temp_dir.path();
1870 let git_repo_path = temp_dir.path().join("git");
1871 let git_repo = git_init(&git_repo_path);
1872
1873 let backend = GitBackend::init_external(&settings, store_path, git_repo.path()).unwrap();
1874 let mut commit = Commit {
1875 parents: vec![],
1876 predecessors: vec![],
1877 root_tree: MergedTreeId::Legacy(backend.empty_tree_id().clone()),
1878 change_id: ChangeId::from_hex("abc123"),
1879 description: "".to_string(),
1880 author: create_signature(),
1881 committer: create_signature(),
1882 secure_sig: None,
1883 };
1884
1885 let write_commit = |commit: Commit| -> BackendResult<(CommitId, Commit)> {
1886 backend.write_commit(commit, None).block_on()
1887 };
1888
1889 commit.parents = vec![];
1891 assert_matches!(
1892 write_commit(commit.clone()),
1893 Err(BackendError::Other(err)) if err.to_string().contains("no parents")
1894 );
1895
1896 commit.parents = vec![backend.root_commit_id().clone()];
1898 let first_id = write_commit(commit.clone()).unwrap().0;
1899 let first_commit = backend.read_commit(&first_id).block_on().unwrap();
1900 assert_eq!(first_commit, commit);
1901 let first_git_commit = git_repo.find_commit(git_id(&first_id)).unwrap();
1902 assert!(first_git_commit.parent_ids().collect_vec().is_empty());
1903
1904 commit.parents = vec![first_id.clone()];
1906 let second_id = write_commit(commit.clone()).unwrap().0;
1907 let second_commit = backend.read_commit(&second_id).block_on().unwrap();
1908 assert_eq!(second_commit, commit);
1909 let second_git_commit = git_repo.find_commit(git_id(&second_id)).unwrap();
1910 assert_eq!(
1911 second_git_commit.parent_ids().collect_vec(),
1912 vec![git_id(&first_id)]
1913 );
1914
1915 commit.parents = vec![first_id.clone(), second_id.clone()];
1917 let merge_id = write_commit(commit.clone()).unwrap().0;
1918 let merge_commit = backend.read_commit(&merge_id).block_on().unwrap();
1919 assert_eq!(merge_commit, commit);
1920 let merge_git_commit = git_repo.find_commit(git_id(&merge_id)).unwrap();
1921 assert_eq!(
1922 merge_git_commit.parent_ids().collect_vec(),
1923 vec![git_id(&first_id), git_id(&second_id)]
1924 );
1925
1926 commit.parents = vec![first_id, backend.root_commit_id().clone()];
1928 assert_matches!(
1929 write_commit(commit),
1930 Err(BackendError::Unsupported(message)) if message.contains("root commit")
1931 );
1932 }
1933
1934 #[test]
1935 fn write_tree_conflicts() {
1936 let settings = user_settings();
1937 let temp_dir = new_temp_dir();
1938 let store_path = temp_dir.path();
1939 let git_repo_path = temp_dir.path().join("git");
1940 let git_repo = git_init(&git_repo_path);
1941
1942 let backend = GitBackend::init_external(&settings, store_path, git_repo.path()).unwrap();
1943 let create_tree = |i| {
1944 let blob_id = git_repo.write_blob(format!("content {i}")).unwrap();
1945 let mut tree_builder = git_repo.empty_tree().edit().unwrap();
1946 tree_builder
1947 .upsert(
1948 format!("file{i}"),
1949 gix::object::tree::EntryKind::Blob,
1950 blob_id,
1951 )
1952 .unwrap();
1953 TreeId::from_bytes(tree_builder.write().unwrap().as_bytes())
1954 };
1955
1956 let root_tree = Merge::from_removes_adds(
1957 vec![create_tree(0), create_tree(1)],
1958 vec![create_tree(2), create_tree(3), create_tree(4)],
1959 );
1960 let mut commit = Commit {
1961 parents: vec![backend.root_commit_id().clone()],
1962 predecessors: vec![],
1963 root_tree: MergedTreeId::Merge(root_tree.clone()),
1964 change_id: ChangeId::from_hex("abc123"),
1965 description: "".to_string(),
1966 author: create_signature(),
1967 committer: create_signature(),
1968 secure_sig: None,
1969 };
1970
1971 let write_commit = |commit: Commit| -> BackendResult<(CommitId, Commit)> {
1972 backend.write_commit(commit, None).block_on()
1973 };
1974
1975 let read_commit_id = write_commit(commit.clone()).unwrap().0;
1978 let read_commit = backend.read_commit(&read_commit_id).block_on().unwrap();
1979 assert_eq!(read_commit, commit);
1980 let git_commit = git_repo
1981 .find_commit(gix::ObjectId::from_bytes_or_panic(
1982 read_commit_id.as_bytes(),
1983 ))
1984 .unwrap();
1985 let git_tree = git_repo.find_tree(git_commit.tree_id().unwrap()).unwrap();
1986 assert!(git_tree
1987 .iter()
1988 .map(Result::unwrap)
1989 .filter(|entry| entry.filename() != b"README")
1990 .all(|entry| entry.mode().0 == 0o040000));
1991 let mut iter = git_tree.iter().map(Result::unwrap);
1992 let entry = iter.next().unwrap();
1993 assert_eq!(entry.filename(), b".jjconflict-base-0");
1994 assert_eq!(
1995 entry.id().as_bytes(),
1996 root_tree.get_remove(0).unwrap().as_bytes()
1997 );
1998 let entry = iter.next().unwrap();
1999 assert_eq!(entry.filename(), b".jjconflict-base-1");
2000 assert_eq!(
2001 entry.id().as_bytes(),
2002 root_tree.get_remove(1).unwrap().as_bytes()
2003 );
2004 let entry = iter.next().unwrap();
2005 assert_eq!(entry.filename(), b".jjconflict-side-0");
2006 assert_eq!(
2007 entry.id().as_bytes(),
2008 root_tree.get_add(0).unwrap().as_bytes()
2009 );
2010 let entry = iter.next().unwrap();
2011 assert_eq!(entry.filename(), b".jjconflict-side-1");
2012 assert_eq!(
2013 entry.id().as_bytes(),
2014 root_tree.get_add(1).unwrap().as_bytes()
2015 );
2016 let entry = iter.next().unwrap();
2017 assert_eq!(entry.filename(), b".jjconflict-side-2");
2018 assert_eq!(
2019 entry.id().as_bytes(),
2020 root_tree.get_add(2).unwrap().as_bytes()
2021 );
2022 let entry = iter.next().unwrap();
2023 assert_eq!(entry.filename(), b"README");
2024 assert_eq!(entry.mode().0, 0o100644);
2025 assert!(iter.next().is_none());
2026
2027 commit.root_tree = MergedTreeId::resolved(create_tree(5));
2030 let read_commit_id = write_commit(commit.clone()).unwrap().0;
2031 let read_commit = backend.read_commit(&read_commit_id).block_on().unwrap();
2032 assert_eq!(read_commit, commit);
2033 let git_commit = git_repo
2034 .find_commit(gix::ObjectId::from_bytes_or_panic(
2035 read_commit_id.as_bytes(),
2036 ))
2037 .unwrap();
2038 assert_eq!(
2039 MergedTreeId::resolved(TreeId::from_bytes(git_commit.tree_id().unwrap().as_bytes())),
2040 commit.root_tree
2041 );
2042 }
2043
2044 #[test]
2045 fn commit_has_ref() {
2046 let settings = user_settings();
2047 let temp_dir = new_temp_dir();
2048 let backend = GitBackend::init_internal(&settings, temp_dir.path()).unwrap();
2049 let git_repo = backend.git_repo();
2050 let signature = Signature {
2051 name: "Someone".to_string(),
2052 email: "someone@example.com".to_string(),
2053 timestamp: Timestamp {
2054 timestamp: MillisSinceEpoch(0),
2055 tz_offset: 0,
2056 },
2057 };
2058 let commit = Commit {
2059 parents: vec![backend.root_commit_id().clone()],
2060 predecessors: vec![],
2061 root_tree: MergedTreeId::Legacy(backend.empty_tree_id().clone()),
2062 change_id: ChangeId::new(vec![]),
2063 description: "initial".to_string(),
2064 author: signature.clone(),
2065 committer: signature,
2066 secure_sig: None,
2067 };
2068 let commit_id = backend.write_commit(commit, None).block_on().unwrap().0;
2069 let git_refs = git_repo.references().unwrap();
2070 let git_ref_ids: Vec<_> = git_refs
2071 .prefixed("refs/jj/keep/")
2072 .unwrap()
2073 .map(|x| x.unwrap().id().detach())
2074 .collect();
2075 assert!(git_ref_ids.iter().any(|id| *id == git_id(&commit_id)));
2076
2077 for git_ref in git_refs.prefixed("refs/jj/keep/").unwrap() {
2079 git_ref.unwrap().delete().unwrap();
2080 }
2081 backend.import_head_commits([&commit_id]).unwrap();
2083 let git_refs = git_repo.references().unwrap();
2084 let git_ref_ids: Vec<_> = git_refs
2085 .prefixed("refs/jj/keep/")
2086 .unwrap()
2087 .map(|x| x.unwrap().id().detach())
2088 .collect();
2089 assert!(git_ref_ids.iter().any(|id| *id == git_id(&commit_id)));
2090 }
2091
2092 #[test]
2093 fn import_head_commits_duplicates() {
2094 let settings = user_settings();
2095 let temp_dir = new_temp_dir();
2096 let backend = GitBackend::init_internal(&settings, temp_dir.path()).unwrap();
2097 let git_repo = backend.git_repo();
2098
2099 let signature = gix::actor::Signature {
2100 name: GIT_USER.into(),
2101 email: GIT_EMAIL.into(),
2102 time: gix::date::Time::now_utc(),
2103 };
2104 let empty_tree_id =
2105 gix::ObjectId::from_hex(b"4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap();
2106 let git_commit_id = git_repo
2107 .commit_as(
2108 &signature,
2109 &signature,
2110 "refs/heads/main",
2111 "git commit message",
2112 empty_tree_id,
2113 [] as [gix::ObjectId; 0],
2114 )
2115 .unwrap()
2116 .detach();
2117 let commit_id = CommitId::from_bytes(git_commit_id.as_bytes());
2118
2119 backend
2121 .import_head_commits([&commit_id, &commit_id])
2122 .unwrap();
2123 assert!(git_repo
2124 .references()
2125 .unwrap()
2126 .prefixed("refs/jj/keep/")
2127 .unwrap()
2128 .any(|git_ref| git_ref.unwrap().id().detach() == git_commit_id));
2129 }
2130
2131 #[test]
2132 fn overlapping_git_commit_id() {
2133 let settings = user_settings();
2134 let temp_dir = new_temp_dir();
2135 let backend = GitBackend::init_internal(&settings, temp_dir.path()).unwrap();
2136 let commit1 = Commit {
2137 parents: vec![backend.root_commit_id().clone()],
2138 predecessors: vec![],
2139 root_tree: MergedTreeId::Legacy(backend.empty_tree_id().clone()),
2140 change_id: ChangeId::new(vec![]),
2141 description: "initial".to_string(),
2142 author: create_signature(),
2143 committer: create_signature(),
2144 secure_sig: None,
2145 };
2146
2147 let write_commit = |commit: Commit| -> BackendResult<(CommitId, Commit)> {
2148 backend.write_commit(commit, None).block_on()
2149 };
2150
2151 let (commit_id1, mut commit2) = write_commit(commit1).unwrap();
2152 commit2.predecessors.push(commit_id1.clone());
2153 let (commit_id2, mut actual_commit2) = write_commit(commit2.clone()).unwrap();
2156 assert_eq!(
2158 backend.read_commit(&commit_id2).block_on().unwrap(),
2159 actual_commit2
2160 );
2161 assert_ne!(commit_id2, commit_id1);
2162 assert_ne!(
2164 actual_commit2.committer.timestamp.timestamp,
2165 commit2.committer.timestamp.timestamp
2166 );
2167 actual_commit2.committer.timestamp.timestamp = commit2.committer.timestamp.timestamp;
2169 assert_eq!(actual_commit2, commit2);
2170 }
2171
2172 #[test]
2173 fn write_signed_commit() {
2174 let settings = user_settings();
2175 let temp_dir = new_temp_dir();
2176 let backend = GitBackend::init_internal(&settings, temp_dir.path()).unwrap();
2177
2178 let commit = Commit {
2179 parents: vec![backend.root_commit_id().clone()],
2180 predecessors: vec![],
2181 root_tree: MergedTreeId::Legacy(backend.empty_tree_id().clone()),
2182 change_id: ChangeId::new(vec![]),
2183 description: "initial".to_string(),
2184 author: create_signature(),
2185 committer: create_signature(),
2186 secure_sig: None,
2187 };
2188
2189 let mut signer = |data: &_| {
2190 let hash: String = blake2b_hash(data).encode_hex();
2191 Ok(format!("test sig\nhash={hash}\n").into_bytes())
2192 };
2193
2194 let (id, commit) = backend
2195 .write_commit(commit, Some(&mut signer as &mut SigningFn))
2196 .block_on()
2197 .unwrap();
2198
2199 let git_repo = backend.git_repo();
2200 let obj = git_repo
2201 .find_object(gix::ObjectId::from_bytes_or_panic(id.as_bytes()))
2202 .unwrap();
2203 insta::assert_snapshot!(std::str::from_utf8(&obj.data).unwrap(), @r"
2204 tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904
2205 author Someone <someone@example.com> 0 +0000
2206 committer Someone <someone@example.com> 0 +0000
2207 gpgsig test sig
2208 hash=9ad9526c3b2103c41a229f2f3c82d107a0ecd902f476a855f0e1dd5f7bef1430663de12749b73e293a877113895a8a2a0f29da4bbc5a5f9a19c3523fb0e53518
2209
2210 initial
2211 ");
2212
2213 let returned_sig = commit.secure_sig.expect("failed to return the signature");
2214
2215 let commit = backend.read_commit(&id).block_on().unwrap();
2216
2217 let sig = commit.secure_sig.expect("failed to read the signature");
2218 assert_eq!(&sig, &returned_sig);
2219
2220 insta::assert_snapshot!(std::str::from_utf8(&sig.sig).unwrap(), @r"
2221 test sig
2222 hash=9ad9526c3b2103c41a229f2f3c82d107a0ecd902f476a855f0e1dd5f7bef1430663de12749b73e293a877113895a8a2a0f29da4bbc5a5f9a19c3523fb0e53518
2223 ");
2224 insta::assert_snapshot!(std::str::from_utf8(&sig.data).unwrap(), @r"
2225 tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904
2226 author Someone <someone@example.com> 0 +0000
2227 committer Someone <someone@example.com> 0 +0000
2228
2229 initial
2230 ");
2231 }
2232
2233 fn git_id(commit_id: &CommitId) -> gix::ObjectId {
2234 gix::ObjectId::from_bytes_or_panic(commit_id.as_bytes())
2235 }
2236
2237 fn create_signature() -> Signature {
2238 Signature {
2239 name: GIT_USER.to_string(),
2240 email: GIT_EMAIL.to_string(),
2241 timestamp: Timestamp {
2242 timestamp: MillisSinceEpoch(0),
2243 tz_offset: 0,
2244 },
2245 }
2246 }
2247
2248 fn user_settings() -> UserSettings {
2253 let config = StackedConfig::with_defaults();
2254 UserSettings::from_config(config).unwrap()
2255 }
2256}