1#![expect(missing_docs)]
16
17use std::collections::HashSet;
18use std::ffi::OsStr;
19use std::fmt::Debug;
20use std::fmt::Error;
21use std::fmt::Formatter;
22use std::fs;
23use std::io;
24use std::io::Cursor;
25use std::path::Path;
26use std::path::PathBuf;
27use std::pin::Pin;
28use std::process::Command;
29use std::process::ExitStatus;
30use std::str::Utf8Error;
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::CommitRefIter;
40use gix::objs::WriteTo as _;
41use itertools::Itertools as _;
42use once_cell::sync::OnceCell as OnceLock;
43use pollster::FutureExt as _;
44use prost::Message as _;
45use smallvec::SmallVec;
46use thiserror::Error;
47use tokio::io::AsyncRead;
48use tokio::io::AsyncReadExt as _;
49
50use crate::backend::Backend;
51use crate::backend::BackendError;
52use crate::backend::BackendInitError;
53use crate::backend::BackendLoadError;
54use crate::backend::BackendResult;
55use crate::backend::ChangeId;
56use crate::backend::Commit;
57use crate::backend::CommitId;
58use crate::backend::CopyHistory;
59use crate::backend::CopyId;
60use crate::backend::CopyRecord;
61use crate::backend::FileId;
62use crate::backend::MillisSinceEpoch;
63use crate::backend::SecureSig;
64use crate::backend::Signature;
65use crate::backend::SigningFn;
66use crate::backend::SymlinkId;
67use crate::backend::Timestamp;
68use crate::backend::Tree;
69use crate::backend::TreeId;
70use crate::backend::TreeValue;
71use crate::backend::make_root_commit;
72use crate::config::ConfigGetError;
73use crate::file_util;
74use crate::file_util::BadPathEncoding;
75use crate::file_util::IoResultExt as _;
76use crate::file_util::PathError;
77use crate::git::GitSettings;
78use crate::index::Index;
79use crate::lock::FileLock;
80use crate::merge::Merge;
81use crate::merge::MergeBuilder;
82use crate::object_id::ObjectId;
83use crate::repo_path::RepoPath;
84use crate::repo_path::RepoPathBuf;
85use crate::repo_path::RepoPathComponentBuf;
86use crate::settings::UserSettings;
87use crate::stacked_table::MutableTable;
88use crate::stacked_table::ReadonlyTable;
89use crate::stacked_table::TableSegment as _;
90use crate::stacked_table::TableStore;
91use crate::stacked_table::TableStoreError;
92
93const HASH_LENGTH: usize = 20;
94const CHANGE_ID_LENGTH: usize = 16;
95const NO_GC_REF_NAMESPACE: &str = "refs/jj/keep/";
97
98pub const JJ_CONFLICT_README_FILE_NAME: &str = "JJ-CONFLICT-README";
99
100pub const JJ_TREES_COMMIT_HEADER: &str = "jj:trees";
101pub const JJ_CONFLICT_LABELS_COMMIT_HEADER: &str = "jj:conflict-labels";
102pub const CHANGE_ID_COMMIT_HEADER: &str = "change-id";
103
104#[derive(Debug, Error)]
105pub enum GitBackendInitError {
106 #[error("Failed to initialize git repository")]
107 InitRepository(#[source] gix::init::Error),
108 #[error("Failed to open git repository")]
109 OpenRepository(#[source] gix::open::Error),
110 #[error("Failed to encode git repository path")]
111 EncodeRepositoryPath(#[source] BadPathEncoding),
112 #[error(transparent)]
113 Config(ConfigGetError),
114 #[error(transparent)]
115 Path(PathError),
116}
117
118impl From<Box<GitBackendInitError>> for BackendInitError {
119 fn from(err: Box<GitBackendInitError>) -> Self {
120 Self(err)
121 }
122}
123
124#[derive(Debug, Error)]
125pub enum GitBackendLoadError {
126 #[error("Failed to open git repository")]
127 OpenRepository(#[source] gix::open::Error),
128 #[error("Failed to decode git repository path")]
129 DecodeRepositoryPath(#[source] BadPathEncoding),
130 #[error(transparent)]
131 Config(ConfigGetError),
132 #[error(transparent)]
133 Path(PathError),
134}
135
136impl From<Box<GitBackendLoadError>> for BackendLoadError {
137 fn from(err: Box<GitBackendLoadError>) -> Self {
138 Self(err)
139 }
140}
141
142#[derive(Debug, Error)]
144pub enum GitBackendError {
145 #[error("Failed to read non-git metadata")]
146 ReadMetadata(#[source] TableStoreError),
147 #[error("Failed to write non-git metadata")]
148 WriteMetadata(#[source] TableStoreError),
149}
150
151impl From<GitBackendError> for BackendError {
152 fn from(err: GitBackendError) -> Self {
153 Self::Other(err.into())
154 }
155}
156
157#[derive(Debug, Error)]
158pub enum GitGcError {
159 #[error("Failed to run git gc command")]
160 GcCommand(#[source] std::io::Error),
161 #[error("git gc command exited with an error: {0}")]
162 GcCommandErrorStatus(ExitStatus),
163}
164
165pub struct GitBackend {
166 base_repo: gix::ThreadSafeRepository,
171 repo: Mutex<gix::Repository>,
172 root_commit_id: CommitId,
173 root_change_id: ChangeId,
174 empty_tree_id: TreeId,
175 shallow_root_ids: OnceLock<Vec<CommitId>>,
176 extra_metadata_store: TableStore,
177 cached_extra_metadata: Mutex<Option<Arc<ReadonlyTable>>>,
178 git_executable: PathBuf,
179 write_change_id_header: bool,
180}
181
182impl GitBackend {
183 pub fn name() -> &'static str {
184 "git"
185 }
186
187 fn new(
188 base_repo: gix::ThreadSafeRepository,
189 extra_metadata_store: TableStore,
190 git_settings: GitSettings,
191 ) -> Self {
192 let repo = Mutex::new(base_repo.to_thread_local());
193 let root_commit_id = CommitId::from_bytes(&[0; HASH_LENGTH]);
194 let root_change_id = ChangeId::from_bytes(&[0; CHANGE_ID_LENGTH]);
195 let empty_tree_id = TreeId::from_hex("4b825dc642cb6eb9a060e54bf8d69288fbee4904");
196 Self {
197 base_repo,
198 repo,
199 root_commit_id,
200 root_change_id,
201 empty_tree_id,
202 shallow_root_ids: OnceLock::new(),
203 extra_metadata_store,
204 cached_extra_metadata: Mutex::new(None),
205 git_executable: git_settings.executable_path,
206 write_change_id_header: git_settings.write_change_id_header,
207 }
208 }
209
210 pub fn init_internal(
211 settings: &UserSettings,
212 store_path: &Path,
213 ) -> Result<Self, Box<GitBackendInitError>> {
214 let git_repo_path = Path::new("git");
215 let git_repo = gix::ThreadSafeRepository::init_opts(
216 store_path.join(git_repo_path),
217 gix::create::Kind::Bare,
218 gix::create::Options::default(),
219 gix_open_opts_from_settings(settings),
220 )
221 .map_err(GitBackendInitError::InitRepository)?;
222 let git_settings =
223 GitSettings::from_settings(settings).map_err(GitBackendInitError::Config)?;
224 Self::init_with_repo(store_path, git_repo_path, git_repo, git_settings)
225 }
226
227 pub fn init_colocated(
230 settings: &UserSettings,
231 store_path: &Path,
232 workspace_root: &Path,
233 ) -> Result<Self, Box<GitBackendInitError>> {
234 let canonical_workspace_root = {
235 let path = store_path.join(workspace_root);
236 dunce::canonicalize(&path)
237 .context(&path)
238 .map_err(GitBackendInitError::Path)?
239 };
240 let git_repo = gix::ThreadSafeRepository::init_opts(
241 canonical_workspace_root,
242 gix::create::Kind::WithWorktree,
243 gix::create::Options::default(),
244 gix_open_opts_from_settings(settings),
245 )
246 .map_err(GitBackendInitError::InitRepository)?;
247 let git_repo_path = workspace_root.join(".git");
248 let git_settings =
249 GitSettings::from_settings(settings).map_err(GitBackendInitError::Config)?;
250 Self::init_with_repo(store_path, &git_repo_path, git_repo, git_settings)
251 }
252
253 pub fn init_external(
255 settings: &UserSettings,
256 store_path: &Path,
257 git_repo_path: &Path,
258 ) -> Result<Self, Box<GitBackendInitError>> {
259 let canonical_git_repo_path = {
260 let path = store_path.join(git_repo_path);
261 canonicalize_git_repo_path(&path)
262 .context(&path)
263 .map_err(GitBackendInitError::Path)?
264 };
265 let git_repo = gix::ThreadSafeRepository::open_opts(
266 canonical_git_repo_path,
267 gix_open_opts_from_settings(settings),
268 )
269 .map_err(GitBackendInitError::OpenRepository)?;
270 let git_settings =
271 GitSettings::from_settings(settings).map_err(GitBackendInitError::Config)?;
272 Self::init_with_repo(store_path, git_repo_path, git_repo, git_settings)
273 }
274
275 fn init_with_repo(
276 store_path: &Path,
277 git_repo_path: &Path,
278 repo: gix::ThreadSafeRepository,
279 git_settings: GitSettings,
280 ) -> Result<Self, Box<GitBackendInitError>> {
281 let extra_path = store_path.join("extra");
282 fs::create_dir(&extra_path)
283 .context(&extra_path)
284 .map_err(GitBackendInitError::Path)?;
285 let target_path = store_path.join("git_target");
286 let git_repo_path = if cfg!(windows) && git_repo_path.is_relative() {
287 file_util::slash_path(git_repo_path)
294 } else {
295 git_repo_path.into()
296 };
297 let git_repo_path_bytes = file_util::path_to_bytes(&git_repo_path)
298 .map_err(GitBackendInitError::EncodeRepositoryPath)?;
299 fs::write(&target_path, git_repo_path_bytes)
300 .context(&target_path)
301 .map_err(GitBackendInitError::Path)?;
302 let extra_metadata_store = TableStore::init(extra_path, HASH_LENGTH);
303 Ok(Self::new(repo, extra_metadata_store, git_settings))
304 }
305
306 pub fn load(
307 settings: &UserSettings,
308 store_path: &Path,
309 ) -> Result<Self, Box<GitBackendLoadError>> {
310 let git_repo_path = {
311 let target_path = store_path.join("git_target");
312 let git_repo_path_bytes = fs::read(&target_path)
313 .context(&target_path)
314 .map_err(GitBackendLoadError::Path)?;
315 let git_repo_path = file_util::path_from_bytes(&git_repo_path_bytes)
316 .map_err(GitBackendLoadError::DecodeRepositoryPath)?;
317 let git_repo_path = store_path.join(git_repo_path);
318 canonicalize_git_repo_path(&git_repo_path)
319 .context(&git_repo_path)
320 .map_err(GitBackendLoadError::Path)?
321 };
322 let repo = gix::ThreadSafeRepository::open_opts(
323 git_repo_path,
324 gix_open_opts_from_settings(settings),
325 )
326 .map_err(GitBackendLoadError::OpenRepository)?;
327 let extra_metadata_store = TableStore::load(store_path.join("extra"), HASH_LENGTH);
328 let git_settings =
329 GitSettings::from_settings(settings).map_err(GitBackendLoadError::Config)?;
330 Ok(Self::new(repo, extra_metadata_store, git_settings))
331 }
332
333 fn lock_git_repo(&self) -> MutexGuard<'_, gix::Repository> {
334 self.repo.lock().unwrap()
335 }
336
337 pub fn git_repo(&self) -> gix::Repository {
339 self.base_repo.to_thread_local()
340 }
341
342 pub fn git_repo_path(&self) -> &Path {
344 self.base_repo.path()
345 }
346
347 pub fn git_workdir(&self) -> Option<&Path> {
349 self.base_repo.work_dir()
350 }
351
352 fn shallow_root_ids(&self, git_repo: &gix::Repository) -> BackendResult<&[CommitId]> {
353 self.shallow_root_ids
357 .get_or_try_init(|| {
358 let maybe_oids = git_repo
359 .shallow_commits()
360 .map_err(|err| BackendError::Other(err.into()))?;
361 let commit_ids = maybe_oids.map_or(vec![], |oids| {
362 oids.iter()
363 .map(|oid| CommitId::from_bytes(oid.as_bytes()))
364 .collect()
365 });
366 Ok(commit_ids)
367 })
368 .map(AsRef::as_ref)
369 }
370
371 fn cached_extra_metadata_table(&self) -> BackendResult<Arc<ReadonlyTable>> {
372 let mut locked_head = self.cached_extra_metadata.lock().unwrap();
373 match locked_head.as_ref() {
374 Some(head) => Ok(head.clone()),
375 None => {
376 let table = self
377 .extra_metadata_store
378 .get_head()
379 .map_err(GitBackendError::ReadMetadata)?;
380 *locked_head = Some(table.clone());
381 Ok(table)
382 }
383 }
384 }
385
386 fn read_extra_metadata_table_locked(&self) -> BackendResult<(Arc<ReadonlyTable>, FileLock)> {
387 let table = self
388 .extra_metadata_store
389 .get_head_locked()
390 .map_err(GitBackendError::ReadMetadata)?;
391 Ok(table)
392 }
393
394 fn save_extra_metadata_table(
395 &self,
396 mut_table: MutableTable,
397 _table_lock: &FileLock,
398 ) -> BackendResult<()> {
399 let table = self
400 .extra_metadata_store
401 .save_table(mut_table)
402 .map_err(GitBackendError::WriteMetadata)?;
403 *self.cached_extra_metadata.lock().unwrap() = Some(table);
406 Ok(())
407 }
408
409 #[tracing::instrument(skip(self, head_ids))]
414 pub fn import_head_commits<'a>(
415 &self,
416 head_ids: impl IntoIterator<Item = &'a CommitId>,
417 ) -> BackendResult<()> {
418 let head_ids: HashSet<&CommitId> = head_ids
419 .into_iter()
420 .filter(|&id| *id != self.root_commit_id)
421 .collect();
422 if head_ids.is_empty() {
423 return Ok(());
424 }
425
426 let locked_repo = self.lock_git_repo();
429 locked_repo
430 .edit_references(head_ids.iter().copied().map(to_no_gc_ref_update))
431 .map_err(|err| BackendError::Other(Box::new(err)))?;
432
433 tracing::debug!(
436 heads_count = head_ids.len(),
437 "import extra metadata entries"
438 );
439 let (table, table_lock) = self.read_extra_metadata_table_locked()?;
440 let mut mut_table = table.start_mutation();
441 import_extra_metadata_entries_from_heads(
442 &locked_repo,
443 &mut mut_table,
444 &table_lock,
445 &head_ids,
446 self.shallow_root_ids(&locked_repo)?,
447 )?;
448 self.save_extra_metadata_table(mut_table, &table_lock)
449 }
450
451 fn read_file_sync(&self, id: &FileId) -> BackendResult<Vec<u8>> {
452 let git_blob_id = validate_git_object_id(id)?;
453 let locked_repo = self.lock_git_repo();
454 let mut blob = locked_repo
455 .find_object(git_blob_id)
456 .map_err(|err| map_not_found_err(err, id))?
457 .try_into_blob()
458 .map_err(|err| to_read_object_err(err, id))?;
459 Ok(blob.take_data())
460 }
461
462 fn new_diff_platform(&self) -> BackendResult<gix::diff::blob::Platform> {
463 let attributes = gix::worktree::Stack::new(
464 Path::new(""),
465 gix::worktree::stack::State::AttributesStack(Default::default()),
466 gix::worktree::glob::pattern::Case::Sensitive,
467 Vec::new(),
468 Vec::new(),
469 );
470 let filter = gix::diff::blob::Pipeline::new(
471 Default::default(),
472 gix::filter::plumbing::Pipeline::new(
473 self.git_repo()
474 .command_context()
475 .map_err(|err| BackendError::Other(Box::new(err)))?,
476 Default::default(),
477 ),
478 Vec::new(),
479 Default::default(),
480 );
481 Ok(gix::diff::blob::Platform::new(
482 Default::default(),
483 filter,
484 gix::diff::blob::pipeline::Mode::ToGit,
485 attributes,
486 ))
487 }
488
489 fn read_tree_for_commit<'repo>(
490 &self,
491 repo: &'repo gix::Repository,
492 id: &CommitId,
493 ) -> BackendResult<gix::Tree<'repo>> {
494 let tree = self.read_commit(id).block_on()?.root_tree;
495 let tree_id = tree.first().clone();
497 let gix_id = validate_git_object_id(&tree_id)?;
498 repo.find_object(gix_id)
499 .map_err(|err| map_not_found_err(err, &tree_id))?
500 .try_into_tree()
501 .map_err(|err| to_read_object_err(err, &tree_id))
502 }
503}
504
505pub fn canonicalize_git_repo_path(path: &Path) -> io::Result<PathBuf> {
512 if path.ends_with(".git") {
513 let workdir = path.parent().unwrap();
514 dunce::canonicalize(workdir).map(|dir| dir.join(".git"))
515 } else {
516 dunce::canonicalize(path)
517 }
518}
519
520fn gix_open_opts_from_settings(settings: &UserSettings) -> gix::open::Options {
521 let user_name = settings.user_name();
522 let user_email = settings.user_email();
523 gix::open::Options::default()
524 .config_overrides([
525 format!("author.name={user_name}"),
528 format!("author.email={user_email}"),
529 format!("committer.name={user_name}"),
530 format!("committer.email={user_email}"),
531 ])
532 .open_path_as_is(true)
534 .strict_config(true)
536}
537
538fn extract_conflict_labels_from_commit(commit: &gix::objs::CommitRef) -> Merge<String> {
540 let Some(value) = commit
541 .extra_headers()
542 .find(JJ_CONFLICT_LABELS_COMMIT_HEADER)
543 else {
544 return Merge::resolved(String::new());
545 };
546
547 str::from_utf8(value)
548 .expect("labels should be valid utf8")
549 .split_terminator('\n')
550 .map(str::to_owned)
551 .collect::<MergeBuilder<_>>()
552 .build()
553}
554
555fn extract_root_tree_from_commit(commit: &gix::objs::CommitRef) -> Result<Merge<TreeId>, ()> {
558 let Some(value) = commit.extra_headers().find(JJ_TREES_COMMIT_HEADER) else {
559 let tree_id = TreeId::from_bytes(commit.tree().as_bytes());
560 return Ok(Merge::resolved(tree_id));
561 };
562
563 let mut tree_ids = SmallVec::new();
564 for hex in value.split(|b| *b == b' ') {
565 let tree_id = TreeId::try_from_hex(hex).ok_or(())?;
566 if tree_id.as_bytes().len() != HASH_LENGTH {
567 return Err(());
568 }
569 tree_ids.push(tree_id);
570 }
571 if tree_ids.len() == 1 || tree_ids.len() % 2 == 0 {
575 return Err(());
576 }
577 Ok(Merge::from_vec(tree_ids))
578}
579
580fn commit_from_git_without_root_parent(
581 id: &CommitId,
582 git_object: &gix::Object,
583 is_shallow: bool,
584) -> BackendResult<Commit> {
585 let decode_err = |err: gix::objs::decode::Error| to_read_object_err(err, id);
586 let commit = git_object
587 .try_to_commit_ref()
588 .map_err(|err| to_read_object_err(err, id))?;
589
590 let change_id = extract_change_id_from_commit(&commit)
593 .unwrap_or_else(|| synthetic_change_id_from_git_commit_id(id));
594
595 let parents = if is_shallow {
599 vec![]
600 } else {
601 commit
602 .parents()
603 .map(|oid| CommitId::from_bytes(oid.as_bytes()))
604 .collect_vec()
605 };
606 let conflict_labels = extract_conflict_labels_from_commit(&commit);
609 let root_tree = extract_root_tree_from_commit(&commit)
614 .map_err(|()| to_read_object_err("Invalid jj:trees header", id))?;
615 let description = String::from_utf8_lossy(commit.message).into_owned();
619 let author = signature_from_git(commit.author().map_err(decode_err)?);
620 let committer = signature_from_git(commit.committer().map_err(decode_err)?);
621
622 let secure_sig = commit
629 .extra_headers
630 .iter()
631 .any(|(k, _)| *k == "gpgsig" || *k == "gpgsig-sha256")
633 .then(|| CommitRefIter::signature(&git_object.data))
634 .transpose()
635 .map_err(decode_err)?
636 .flatten()
637 .map(|(sig, data)| SecureSig {
638 data: data.to_bstring().into(),
639 sig: sig.into_owned().into(),
640 });
641
642 Ok(Commit {
643 parents,
644 predecessors: vec![],
645 root_tree,
647 conflict_labels,
648 change_id,
649 description,
650 author,
651 committer,
652 secure_sig,
653 })
654}
655
656pub fn extract_change_id_from_commit(commit: &gix::objs::CommitRef) -> Option<ChangeId> {
658 commit
659 .extra_headers()
660 .find(CHANGE_ID_COMMIT_HEADER)
661 .and_then(ChangeId::try_from_reverse_hex)
662 .filter(|val| val.as_bytes().len() == CHANGE_ID_LENGTH)
663}
664
665pub fn synthetic_change_id_from_git_commit_id(id: &CommitId) -> ChangeId {
670 let bytes = id.as_bytes()[4..HASH_LENGTH]
677 .iter()
678 .rev()
679 .map(|b| b.reverse_bits())
680 .collect();
681 ChangeId::new(bytes)
682}
683
684const EMPTY_STRING_PLACEHOLDER: &str = "JJ_EMPTY_STRING";
685
686fn signature_from_git(signature: gix::actor::SignatureRef) -> Signature {
687 let name = signature.name;
688 let name = if name != EMPTY_STRING_PLACEHOLDER {
689 String::from_utf8_lossy(name).into_owned()
690 } else {
691 "".to_string()
692 };
693 let email = signature.email;
694 let email = if email != EMPTY_STRING_PLACEHOLDER {
695 String::from_utf8_lossy(email).into_owned()
696 } else {
697 "".to_string()
698 };
699 let time = signature.time().unwrap_or_default();
700 let timestamp = MillisSinceEpoch(time.seconds * 1000);
701 let tz_offset = time.offset.div_euclid(60); Signature {
703 name,
704 email,
705 timestamp: Timestamp {
706 timestamp,
707 tz_offset,
708 },
709 }
710}
711
712fn signature_to_git(signature: &Signature) -> gix::actor::Signature {
713 let name = if !signature.name.is_empty() {
715 &signature.name
716 } else {
717 EMPTY_STRING_PLACEHOLDER
718 };
719 let email = if !signature.email.is_empty() {
720 &signature.email
721 } else {
722 EMPTY_STRING_PLACEHOLDER
723 };
724 let time = gix::date::Time::new(
725 signature.timestamp.timestamp.0.div_euclid(1000),
726 signature.timestamp.tz_offset * 60, );
728 gix::actor::Signature {
729 name: name.into(),
730 email: email.into(),
731 time,
732 }
733}
734
735fn serialize_extras(commit: &Commit) -> Vec<u8> {
736 let mut proto = crate::protos::git_store::Commit {
737 change_id: commit.change_id.to_bytes(),
738 ..Default::default()
739 };
740 proto.uses_tree_conflict_format = true;
741 if !commit.root_tree.is_resolved() {
742 proto.root_tree = commit.root_tree.iter().map(|r| r.to_bytes()).collect();
746 }
747 for predecessor in &commit.predecessors {
748 proto.predecessors.push(predecessor.to_bytes());
749 }
750 proto.encode_to_vec()
751}
752
753fn deserialize_extras(commit: &mut Commit, bytes: &[u8]) {
754 let proto = crate::protos::git_store::Commit::decode(bytes).unwrap();
755 if !proto.change_id.is_empty() {
756 commit.change_id = ChangeId::new(proto.change_id);
757 }
758 if commit.root_tree.is_resolved()
759 && proto.uses_tree_conflict_format
760 && !proto.root_tree.is_empty()
761 {
762 let merge_builder: MergeBuilder<_> = proto
763 .root_tree
764 .iter()
765 .map(|id_bytes| TreeId::from_bytes(id_bytes))
766 .collect();
767 commit.root_tree = merge_builder.build();
768 }
769 for predecessor in &proto.predecessors {
770 commit.predecessors.push(CommitId::from_bytes(predecessor));
771 }
772}
773
774fn to_no_gc_ref_update(id: &CommitId) -> gix::refs::transaction::RefEdit {
777 let name = format!("{NO_GC_REF_NAMESPACE}{id}");
778 let new = gix::refs::Target::Object(gix::ObjectId::from_bytes_or_panic(id.as_bytes()));
779 let expected = gix::refs::transaction::PreviousValue::ExistingMustMatch(new.clone());
780 gix::refs::transaction::RefEdit {
781 change: gix::refs::transaction::Change::Update {
782 log: gix::refs::transaction::LogChange {
783 message: "used by jj".into(),
784 ..Default::default()
785 },
786 expected,
787 new,
788 },
789 name: name.try_into().unwrap(),
790 deref: false,
791 }
792}
793
794fn to_ref_deletion(git_ref: gix::refs::Reference) -> gix::refs::transaction::RefEdit {
795 let expected = gix::refs::transaction::PreviousValue::ExistingMustMatch(git_ref.target);
796 gix::refs::transaction::RefEdit {
797 change: gix::refs::transaction::Change::Delete {
798 expected,
799 log: gix::refs::transaction::RefLog::AndReference,
800 },
801 name: git_ref.name,
802 deref: false,
803 }
804}
805
806fn recreate_no_gc_refs(
809 git_repo: &gix::Repository,
810 new_heads: impl IntoIterator<Item = CommitId>,
811 keep_newer: SystemTime,
812) -> BackendResult<()> {
813 let new_heads: HashSet<CommitId> = new_heads.into_iter().collect();
815 let mut no_gc_refs_to_keep_count: usize = 0;
816 let mut no_gc_refs_to_delete: Vec<gix::refs::Reference> = Vec::new();
817 let git_references = git_repo
818 .references()
819 .map_err(|err| BackendError::Other(err.into()))?;
820 let no_gc_refs_iter = git_references
821 .prefixed(NO_GC_REF_NAMESPACE)
822 .map_err(|err| BackendError::Other(err.into()))?;
823 for git_ref in no_gc_refs_iter {
824 let git_ref = git_ref.map_err(BackendError::Other)?.detach();
825 let oid = git_ref.target.try_id().ok_or_else(|| {
826 let name = git_ref.name.as_bstr();
827 BackendError::Other(format!("Symbolic no-gc ref found: {name}").into())
828 })?;
829 let id = CommitId::from_bytes(oid.as_bytes());
830 let name_good = git_ref.name.as_bstr()[NO_GC_REF_NAMESPACE.len()..] == id.hex();
831 if new_heads.contains(&id) && name_good {
832 no_gc_refs_to_keep_count += 1;
833 continue;
834 }
835 let loose_ref_path = git_repo.path().join(git_ref.name.to_path());
845 if let Ok(metadata) = loose_ref_path.metadata() {
846 let mtime = metadata.modified().expect("unsupported platform?");
847 if mtime > keep_newer {
848 tracing::trace!(?git_ref, "not deleting new");
849 no_gc_refs_to_keep_count += 1;
850 continue;
851 }
852 }
853 tracing::trace!(?git_ref, ?name_good, "will delete");
855 no_gc_refs_to_delete.push(git_ref);
856 }
857 tracing::info!(
858 new_heads_count = new_heads.len(),
859 no_gc_refs_to_keep_count,
860 no_gc_refs_to_delete_count = no_gc_refs_to_delete.len(),
861 "collected reachable refs"
862 );
863
864 let ref_edits = itertools::chain(
866 no_gc_refs_to_delete.into_iter().map(to_ref_deletion),
867 new_heads.iter().map(to_no_gc_ref_update),
868 );
869 git_repo
870 .edit_references(ref_edits)
871 .map_err(|err| BackendError::Other(err.into()))?;
872
873 Ok(())
874}
875
876fn run_git_gc(program: &OsStr, git_dir: &Path, keep_newer: SystemTime) -> Result<(), GitGcError> {
877 let keep_newer = keep_newer
878 .duration_since(SystemTime::UNIX_EPOCH)
879 .unwrap_or_default(); let mut git = Command::new(program);
881 git.arg("--git-dir=.") .arg("gc")
883 .arg(format!("--prune=@{} +0000", keep_newer.as_secs()));
884 git.current_dir(git_dir);
887 tracing::info!(?git, "running git gc");
889 let status = git.status().map_err(GitGcError::GcCommand)?;
890 tracing::info!(?status, "git gc exited");
891 if !status.success() {
892 return Err(GitGcError::GcCommandErrorStatus(status));
893 }
894 Ok(())
895}
896
897fn validate_git_object_id(id: &impl ObjectId) -> BackendResult<gix::ObjectId> {
898 if id.as_bytes().len() != HASH_LENGTH {
899 return Err(BackendError::InvalidHashLength {
900 expected: HASH_LENGTH,
901 actual: id.as_bytes().len(),
902 object_type: id.object_type(),
903 hash: id.hex(),
904 });
905 }
906 Ok(gix::ObjectId::from_bytes_or_panic(id.as_bytes()))
907}
908
909fn map_not_found_err(err: gix::object::find::existing::Error, id: &impl ObjectId) -> BackendError {
910 if matches!(err, gix::object::find::existing::Error::NotFound { .. }) {
911 BackendError::ObjectNotFound {
912 object_type: id.object_type(),
913 hash: id.hex(),
914 source: Box::new(err),
915 }
916 } else {
917 to_read_object_err(err, id)
918 }
919}
920
921fn to_read_object_err(
922 err: impl Into<Box<dyn std::error::Error + Send + Sync>>,
923 id: &impl ObjectId,
924) -> BackendError {
925 BackendError::ReadObject {
926 object_type: id.object_type(),
927 hash: id.hex(),
928 source: err.into(),
929 }
930}
931
932fn to_invalid_utf8_err(source: Utf8Error, id: &impl ObjectId) -> BackendError {
933 BackendError::InvalidUtf8 {
934 object_type: id.object_type(),
935 hash: id.hex(),
936 source,
937 }
938}
939
940fn import_extra_metadata_entries_from_heads(
941 git_repo: &gix::Repository,
942 mut_table: &mut MutableTable,
943 _table_lock: &FileLock,
944 head_ids: &HashSet<&CommitId>,
945 shallow_roots: &[CommitId],
946) -> BackendResult<()> {
947 let mut work_ids = head_ids
948 .iter()
949 .filter(|&id| mut_table.get_value(id.as_bytes()).is_none())
950 .map(|&id| id.clone())
951 .collect_vec();
952 while let Some(id) = work_ids.pop() {
953 let git_object = git_repo
954 .find_object(validate_git_object_id(&id)?)
955 .map_err(|err| map_not_found_err(err, &id))?;
956 let is_shallow = shallow_roots.contains(&id);
957 let commit = commit_from_git_without_root_parent(&id, &git_object, is_shallow)?;
961 mut_table.add_entry(id.to_bytes(), serialize_extras(&commit));
962 work_ids.extend(
963 commit
964 .parents
965 .into_iter()
966 .filter(|id| mut_table.get_value(id.as_bytes()).is_none()),
967 );
968 }
969 Ok(())
970}
971
972impl Debug for GitBackend {
973 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
974 f.debug_struct("GitBackend")
975 .field("path", &self.git_repo_path())
976 .finish()
977 }
978}
979
980#[async_trait]
981impl Backend for GitBackend {
982 fn name(&self) -> &str {
983 Self::name()
984 }
985
986 fn commit_id_length(&self) -> usize {
987 HASH_LENGTH
988 }
989
990 fn change_id_length(&self) -> usize {
991 CHANGE_ID_LENGTH
992 }
993
994 fn root_commit_id(&self) -> &CommitId {
995 &self.root_commit_id
996 }
997
998 fn root_change_id(&self) -> &ChangeId {
999 &self.root_change_id
1000 }
1001
1002 fn empty_tree_id(&self) -> &TreeId {
1003 &self.empty_tree_id
1004 }
1005
1006 fn concurrency(&self) -> usize {
1007 1
1008 }
1009
1010 async fn read_file(
1011 &self,
1012 _path: &RepoPath,
1013 id: &FileId,
1014 ) -> BackendResult<Pin<Box<dyn AsyncRead + Send>>> {
1015 let data = self.read_file_sync(id)?;
1016 Ok(Box::pin(Cursor::new(data)))
1017 }
1018
1019 async fn write_file(
1020 &self,
1021 _path: &RepoPath,
1022 contents: &mut (dyn AsyncRead + Send + Unpin),
1023 ) -> BackendResult<FileId> {
1024 let mut bytes = Vec::new();
1025 contents.read_to_end(&mut bytes).await.unwrap();
1026 let locked_repo = self.lock_git_repo();
1027 let oid = locked_repo
1028 .write_blob(bytes)
1029 .map_err(|err| BackendError::WriteObject {
1030 object_type: "file",
1031 source: Box::new(err),
1032 })?;
1033 Ok(FileId::new(oid.as_bytes().to_vec()))
1034 }
1035
1036 async fn read_symlink(&self, _path: &RepoPath, id: &SymlinkId) -> BackendResult<String> {
1037 let git_blob_id = validate_git_object_id(id)?;
1038 let locked_repo = self.lock_git_repo();
1039 let mut blob = locked_repo
1040 .find_object(git_blob_id)
1041 .map_err(|err| map_not_found_err(err, id))?
1042 .try_into_blob()
1043 .map_err(|err| to_read_object_err(err, id))?;
1044 let target = String::from_utf8(blob.take_data())
1045 .map_err(|err| to_invalid_utf8_err(err.utf8_error(), id))?;
1046 Ok(target)
1047 }
1048
1049 async fn write_symlink(&self, _path: &RepoPath, target: &str) -> BackendResult<SymlinkId> {
1050 let locked_repo = self.lock_git_repo();
1051 let oid =
1052 locked_repo
1053 .write_blob(target.as_bytes())
1054 .map_err(|err| BackendError::WriteObject {
1055 object_type: "symlink",
1056 source: Box::new(err),
1057 })?;
1058 Ok(SymlinkId::new(oid.as_bytes().to_vec()))
1059 }
1060
1061 async fn read_copy(&self, _id: &CopyId) -> BackendResult<CopyHistory> {
1062 Err(BackendError::Unsupported(
1063 "The Git backend doesn't support tracked copies yet".to_string(),
1064 ))
1065 }
1066
1067 async fn write_copy(&self, _contents: &CopyHistory) -> BackendResult<CopyId> {
1068 Err(BackendError::Unsupported(
1069 "The Git backend doesn't support tracked copies yet".to_string(),
1070 ))
1071 }
1072
1073 async fn get_related_copies(&self, _copy_id: &CopyId) -> BackendResult<Vec<CopyHistory>> {
1074 Err(BackendError::Unsupported(
1075 "The Git backend doesn't support tracked copies yet".to_string(),
1076 ))
1077 }
1078
1079 async fn read_tree(&self, _path: &RepoPath, id: &TreeId) -> BackendResult<Tree> {
1080 if id == &self.empty_tree_id {
1081 return Ok(Tree::default());
1082 }
1083 let git_tree_id = validate_git_object_id(id)?;
1084
1085 let locked_repo = self.lock_git_repo();
1086 let git_tree = locked_repo
1087 .find_object(git_tree_id)
1088 .map_err(|err| map_not_found_err(err, id))?
1089 .try_into_tree()
1090 .map_err(|err| to_read_object_err(err, id))?;
1091 let mut entries: Vec<_> = git_tree
1092 .iter()
1093 .map(|entry| -> BackendResult<_> {
1094 let entry = entry.map_err(|err| to_read_object_err(err, id))?;
1095 let name = RepoPathComponentBuf::new(
1096 str::from_utf8(entry.filename()).map_err(|err| to_invalid_utf8_err(err, id))?,
1097 )
1098 .unwrap();
1099 let value = match entry.mode().kind() {
1100 gix::object::tree::EntryKind::Tree => {
1101 let id = TreeId::from_bytes(entry.oid().as_bytes());
1102 TreeValue::Tree(id)
1103 }
1104 gix::object::tree::EntryKind::Blob => {
1105 let id = FileId::from_bytes(entry.oid().as_bytes());
1106 TreeValue::File {
1107 id,
1108 executable: false,
1109 copy_id: CopyId::placeholder(),
1110 }
1111 }
1112 gix::object::tree::EntryKind::BlobExecutable => {
1113 let id = FileId::from_bytes(entry.oid().as_bytes());
1114 TreeValue::File {
1115 id,
1116 executable: true,
1117 copy_id: CopyId::placeholder(),
1118 }
1119 }
1120 gix::object::tree::EntryKind::Link => {
1121 let id = SymlinkId::from_bytes(entry.oid().as_bytes());
1122 TreeValue::Symlink(id)
1123 }
1124 gix::object::tree::EntryKind::Commit => {
1125 let id = CommitId::from_bytes(entry.oid().as_bytes());
1126 TreeValue::GitSubmodule(id)
1127 }
1128 };
1129 Ok((name, value))
1130 })
1131 .try_collect()?;
1132 if !entries.is_sorted_by_key(|(name, _)| name) {
1135 entries.sort_unstable_by(|(a, _), (b, _)| a.cmp(b));
1136 }
1137 Ok(Tree::from_sorted_entries(entries))
1138 }
1139
1140 async fn write_tree(&self, _path: &RepoPath, contents: &Tree) -> BackendResult<TreeId> {
1141 let entries = contents
1144 .entries()
1145 .map(|entry| {
1146 let filename = BString::from(entry.name().as_internal_str());
1147 match entry.value() {
1148 TreeValue::File {
1149 id,
1150 executable: false,
1151 copy_id: _, } => gix::objs::tree::Entry {
1153 mode: gix::object::tree::EntryKind::Blob.into(),
1154 filename,
1155 oid: gix::ObjectId::from_bytes_or_panic(id.as_bytes()),
1156 },
1157 TreeValue::File {
1158 id,
1159 executable: true,
1160 copy_id: _, } => gix::objs::tree::Entry {
1162 mode: gix::object::tree::EntryKind::BlobExecutable.into(),
1163 filename,
1164 oid: gix::ObjectId::from_bytes_or_panic(id.as_bytes()),
1165 },
1166 TreeValue::Symlink(id) => gix::objs::tree::Entry {
1167 mode: gix::object::tree::EntryKind::Link.into(),
1168 filename,
1169 oid: gix::ObjectId::from_bytes_or_panic(id.as_bytes()),
1170 },
1171 TreeValue::Tree(id) => gix::objs::tree::Entry {
1172 mode: gix::object::tree::EntryKind::Tree.into(),
1173 filename,
1174 oid: gix::ObjectId::from_bytes_or_panic(id.as_bytes()),
1175 },
1176 TreeValue::GitSubmodule(id) => gix::objs::tree::Entry {
1177 mode: gix::object::tree::EntryKind::Commit.into(),
1178 filename,
1179 oid: gix::ObjectId::from_bytes_or_panic(id.as_bytes()),
1180 },
1181 }
1182 })
1183 .sorted_unstable()
1184 .collect();
1185 let locked_repo = self.lock_git_repo();
1186 let oid = locked_repo
1187 .write_object(gix::objs::Tree { entries })
1188 .map_err(|err| BackendError::WriteObject {
1189 object_type: "tree",
1190 source: Box::new(err),
1191 })?;
1192 Ok(TreeId::from_bytes(oid.as_bytes()))
1193 }
1194
1195 #[tracing::instrument(skip(self))]
1196 async fn read_commit(&self, id: &CommitId) -> BackendResult<Commit> {
1197 if *id == self.root_commit_id {
1198 return Ok(make_root_commit(
1199 self.root_change_id().clone(),
1200 self.empty_tree_id.clone(),
1201 ));
1202 }
1203 let git_commit_id = validate_git_object_id(id)?;
1204
1205 let mut commit = {
1206 let locked_repo = self.lock_git_repo();
1207 let git_object = locked_repo
1208 .find_object(git_commit_id)
1209 .map_err(|err| map_not_found_err(err, id))?;
1210 let is_shallow = self.shallow_root_ids(&locked_repo)?.contains(id);
1211 commit_from_git_without_root_parent(id, &git_object, is_shallow)?
1212 };
1213 if commit.parents.is_empty() {
1214 commit.parents.push(self.root_commit_id.clone());
1215 }
1216
1217 let table = self.cached_extra_metadata_table()?;
1218 if let Some(extras) = table.get_value(id.as_bytes()) {
1219 deserialize_extras(&mut commit, extras);
1220 } else {
1221 tracing::info!("unimported Git commit found");
1226 self.import_head_commits([id])?;
1227 let table = self.cached_extra_metadata_table()?;
1228 let extras = table.get_value(id.as_bytes()).unwrap();
1229 deserialize_extras(&mut commit, extras);
1230 }
1231 Ok(commit)
1232 }
1233
1234 async fn write_commit(
1235 &self,
1236 mut contents: Commit,
1237 mut sign_with: Option<&mut SigningFn>,
1238 ) -> BackendResult<(CommitId, Commit)> {
1239 assert!(contents.secure_sig.is_none(), "commit.secure_sig was set");
1240
1241 let locked_repo = self.lock_git_repo();
1242 let tree_ids = &contents.root_tree;
1243 let git_tree_id = match tree_ids.as_resolved() {
1244 Some(tree_id) => validate_git_object_id(tree_id)?,
1245 None => write_tree_conflict(&locked_repo, tree_ids)?,
1246 };
1247 let author = signature_to_git(&contents.author);
1248 let mut committer = signature_to_git(&contents.committer);
1249 let message = &contents.description;
1250 if contents.parents.is_empty() {
1251 return Err(BackendError::Other(
1252 "Cannot write a commit with no parents".into(),
1253 ));
1254 }
1255 let mut parents = SmallVec::new();
1256 for parent_id in &contents.parents {
1257 if *parent_id == self.root_commit_id {
1258 if contents.parents.len() > 1 {
1263 return Err(BackendError::Unsupported(
1264 "The Git backend does not support creating merge commits with the root \
1265 commit as one of the parents."
1266 .to_owned(),
1267 ));
1268 }
1269 } else {
1270 parents.push(validate_git_object_id(parent_id)?);
1271 }
1272 }
1273 let mut extra_headers: Vec<(BString, BString)> = vec![];
1274 if !contents.conflict_labels.is_resolved() {
1275 assert!(
1277 contents
1278 .conflict_labels
1279 .iter()
1280 .all(|label| !label.contains('\n'))
1281 );
1282 let mut joined_with_newlines = contents.conflict_labels.iter().join("\n");
1283 joined_with_newlines.push('\n');
1284 extra_headers.push((
1285 JJ_CONFLICT_LABELS_COMMIT_HEADER.into(),
1286 joined_with_newlines.into(),
1287 ));
1288 }
1289 if !tree_ids.is_resolved() {
1290 let value = tree_ids.iter().map(|id| id.hex()).join(" ");
1291 extra_headers.push((JJ_TREES_COMMIT_HEADER.into(), value.into()));
1292 }
1293 if self.write_change_id_header {
1294 extra_headers.push((
1295 CHANGE_ID_COMMIT_HEADER.into(),
1296 contents.change_id.reverse_hex().into(),
1297 ));
1298 }
1299
1300 if tree_ids.iter().any(|id| id == &self.empty_tree_id) {
1301 let tree = gix::objs::Tree::empty();
1302 let tree_id =
1303 locked_repo
1304 .write_object(&tree)
1305 .map_err(|err| BackendError::WriteObject {
1306 object_type: "tree",
1307 source: Box::new(err),
1308 })?;
1309 assert!(tree_id.is_empty_tree());
1310 }
1311
1312 let extras = serialize_extras(&contents);
1313
1314 let (table, table_lock) = self.read_extra_metadata_table_locked()?;
1321 let id = loop {
1322 let mut commit = gix::objs::Commit {
1323 message: message.to_owned().into(),
1324 tree: git_tree_id,
1325 author: author.clone(),
1326 committer: committer.clone(),
1327 encoding: None,
1328 parents: parents.clone(),
1329 extra_headers: extra_headers.clone(),
1330 };
1331
1332 if let Some(sign) = &mut sign_with {
1333 let mut data = Vec::with_capacity(512);
1335 commit.write_to(&mut data).unwrap();
1336
1337 let sig = sign(&data).map_err(|err| BackendError::WriteObject {
1338 object_type: "commit",
1339 source: Box::new(err),
1340 })?;
1341 commit
1342 .extra_headers
1343 .push(("gpgsig".into(), sig.clone().into()));
1344 contents.secure_sig = Some(SecureSig { data, sig });
1345 }
1346
1347 let git_id =
1348 locked_repo
1349 .write_object(&commit)
1350 .map_err(|err| BackendError::WriteObject {
1351 object_type: "commit",
1352 source: Box::new(err),
1353 })?;
1354
1355 match table.get_value(git_id.as_bytes()) {
1356 Some(existing_extras) if existing_extras != extras => {
1357 committer.time.seconds -= 1;
1371 }
1372 _ => break CommitId::from_bytes(git_id.as_bytes()),
1373 }
1374 };
1375
1376 locked_repo
1379 .edit_reference(to_no_gc_ref_update(&id))
1380 .map_err(|err| BackendError::Other(Box::new(err)))?;
1381
1382 contents.committer.timestamp.timestamp = MillisSinceEpoch(committer.time.seconds * 1000);
1385 let mut mut_table = table.start_mutation();
1386 mut_table.add_entry(id.to_bytes(), extras);
1387 self.save_extra_metadata_table(mut_table, &table_lock)?;
1388 Ok((id, contents))
1389 }
1390
1391 fn get_copy_records(
1392 &self,
1393 paths: Option<&[RepoPathBuf]>,
1394 root_id: &CommitId,
1395 head_id: &CommitId,
1396 ) -> BackendResult<BoxStream<'_, BackendResult<CopyRecord>>> {
1397 let repo = self.git_repo();
1398 let root_tree = self.read_tree_for_commit(&repo, root_id)?;
1399 let head_tree = self.read_tree_for_commit(&repo, head_id)?;
1400
1401 let change_to_copy_record =
1402 |change: gix::object::tree::diff::Change| -> BackendResult<Option<CopyRecord>> {
1403 let gix::object::tree::diff::Change::Rewrite {
1404 source_location,
1405 source_entry_mode,
1406 source_id,
1407 entry_mode: dest_entry_mode,
1408 location: dest_location,
1409 ..
1410 } = change
1411 else {
1412 return Ok(None);
1413 };
1414 if !source_entry_mode.is_blob() || !dest_entry_mode.is_blob() {
1417 return Ok(None);
1418 }
1419
1420 let source = str::from_utf8(source_location)
1421 .map_err(|err| to_invalid_utf8_err(err, root_id))?;
1422 let dest = str::from_utf8(dest_location)
1423 .map_err(|err| to_invalid_utf8_err(err, head_id))?;
1424
1425 let target = RepoPathBuf::from_internal_string(dest).unwrap();
1426 if !paths.is_none_or(|paths| paths.contains(&target)) {
1427 return Ok(None);
1428 }
1429
1430 Ok(Some(CopyRecord {
1431 target,
1432 target_commit: head_id.clone(),
1433 source: RepoPathBuf::from_internal_string(source).unwrap(),
1434 source_file: FileId::from_bytes(source_id.as_bytes()),
1435 source_commit: root_id.clone(),
1436 }))
1437 };
1438
1439 let mut records: Vec<BackendResult<CopyRecord>> = Vec::new();
1440 root_tree
1441 .changes()
1442 .map_err(|err| BackendError::Other(err.into()))?
1443 .options(|opts| {
1444 opts.track_path().track_rewrites(Some(gix::diff::Rewrites {
1445 copies: Some(gix::diff::rewrites::Copies {
1446 source: gix::diff::rewrites::CopySource::FromSetOfModifiedFiles,
1447 percentage: Some(0.5),
1448 }),
1449 percentage: Some(0.5),
1450 limit: 1000,
1451 track_empty: false,
1452 }));
1453 })
1454 .for_each_to_obtain_tree_with_cache(
1455 &head_tree,
1456 &mut self.new_diff_platform()?,
1457 |change| -> BackendResult<_> {
1458 match change_to_copy_record(change) {
1459 Ok(None) => {}
1460 Ok(Some(change)) => records.push(Ok(change)),
1461 Err(err) => records.push(Err(err)),
1462 }
1463 Ok(gix::object::tree::diff::Action::Continue(()))
1464 },
1465 )
1466 .map_err(|err| BackendError::Other(err.into()))?;
1467 Ok(Box::pin(futures::stream::iter(records)))
1468 }
1469
1470 #[tracing::instrument(skip(self, index))]
1471 fn gc(&self, index: &dyn Index, keep_newer: SystemTime) -> BackendResult<()> {
1472 let git_repo = self.lock_git_repo();
1473 let new_heads = index
1474 .all_heads_for_gc()
1475 .map_err(|err| BackendError::Other(err.into()))?
1476 .filter(|id| *id != self.root_commit_id);
1477 recreate_no_gc_refs(&git_repo, new_heads, keep_newer)?;
1478
1479 let table = self.cached_extra_metadata_table()?;
1481 self.extra_metadata_store
1485 .gc(&table, keep_newer)
1486 .map_err(|err| BackendError::Other(err.into()))?;
1487
1488 run_git_gc(
1489 self.git_executable.as_ref(),
1490 self.git_repo_path(),
1491 keep_newer,
1492 )
1493 .map_err(|err| BackendError::Other(err.into()))?;
1494 git_repo.refs.force_refresh_packed_buffer().ok();
1497 Ok(())
1498 }
1499}
1500
1501fn write_tree_conflict(
1507 repo: &gix::Repository,
1508 conflict: &Merge<TreeId>,
1509) -> BackendResult<gix::ObjectId> {
1510 let mut entries = itertools::chain(
1512 conflict
1513 .removes()
1514 .enumerate()
1515 .map(|(i, tree_id)| (format!(".jjconflict-base-{i}"), tree_id)),
1516 conflict
1517 .adds()
1518 .enumerate()
1519 .map(|(i, tree_id)| (format!(".jjconflict-side-{i}"), tree_id)),
1520 )
1521 .map(|(name, tree_id)| gix::objs::tree::Entry {
1522 mode: gix::object::tree::EntryKind::Tree.into(),
1523 filename: name.into(),
1524 oid: gix::ObjectId::from_bytes_or_panic(tree_id.as_bytes()),
1525 })
1526 .collect_vec();
1527 let readme_id = repo
1528 .write_blob(
1529 r#"This commit was made by jj, https://jj-vcs.dev/.
1530The commit contains file conflicts, and therefore looks wrong when used with
1531plain Git or other tools that are unfamiliar with jj.
1532
1533The .jjconflict-* directories represent the different inputs to the conflict.
1534For details, see
1535https://docs.jj-vcs.dev/latest/git-compatibility/#format-mapping-details
1536
1537If you see this file in your working copy, it probably means that you used a
1538regular `git` command to check out a conflicted commit. Use `jj abandon` to
1539recover.
1540"#,
1541 )
1542 .map_err(|err| {
1543 BackendError::Other(format!("Failed to write README for conflict tree: {err}").into())
1544 })?
1545 .detach();
1546 entries.push(gix::objs::tree::Entry {
1547 mode: gix::object::tree::EntryKind::Blob.into(),
1548 filename: JJ_CONFLICT_README_FILE_NAME.into(),
1549 oid: readme_id,
1550 });
1551 let first_tree_id = conflict.first();
1552 let first_tree = repo
1553 .find_tree(gix::ObjectId::from_bytes_or_panic(first_tree_id.as_bytes()))
1554 .map_err(|err| to_read_object_err(err, first_tree_id))?;
1555 for entry in first_tree.iter() {
1556 let entry = entry.map_err(|err| to_read_object_err(err, first_tree_id))?;
1557 if !entry.filename().starts_with(b".jjconflict")
1558 && entry.filename() != JJ_CONFLICT_README_FILE_NAME
1559 {
1560 entries.push(entry.detach().into());
1561 }
1562 }
1563 entries.sort_unstable();
1564 let id = repo
1565 .write_object(gix::objs::Tree { entries })
1566 .map_err(|err| BackendError::WriteObject {
1567 object_type: "tree",
1568 source: Box::new(err),
1569 })?;
1570 Ok(id.detach())
1571}
1572
1573#[cfg(test)]
1574mod tests {
1575 use assert_matches::assert_matches;
1576 use gix::date::parse::TimeBuf;
1577 use gix::objs::CommitRef;
1578 use indoc::indoc;
1579 use pollster::FutureExt as _;
1580
1581 use super::*;
1582 use crate::config::StackedConfig;
1583 use crate::content_hash::blake2b_hash;
1584 use crate::hex_util;
1585 use crate::tests::new_temp_dir;
1586
1587 const GIT_USER: &str = "Someone";
1588 const GIT_EMAIL: &str = "someone@example.com";
1589
1590 fn git_config() -> Vec<bstr::BString> {
1591 vec![
1592 format!("user.name = {GIT_USER}").into(),
1593 format!("user.email = {GIT_EMAIL}").into(),
1594 "init.defaultBranch = master".into(),
1595 ]
1596 }
1597
1598 fn open_options() -> gix::open::Options {
1599 gix::open::Options::isolated()
1600 .config_overrides(git_config())
1601 .strict_config(true)
1602 }
1603
1604 fn git_init(directory: impl AsRef<Path>) -> gix::Repository {
1605 gix::ThreadSafeRepository::init_opts(
1606 directory,
1607 gix::create::Kind::WithWorktree,
1608 gix::create::Options::default(),
1609 open_options(),
1610 )
1611 .unwrap()
1612 .to_thread_local()
1613 }
1614
1615 #[test]
1616 fn read_plain_git_commit() {
1617 let settings = user_settings();
1618 let temp_dir = new_temp_dir();
1619 let store_path = temp_dir.path();
1620 let git_repo_path = temp_dir.path().join("git");
1621 let git_repo = git_init(git_repo_path);
1622
1623 let blob1 = git_repo.write_blob(b"content1").unwrap().detach();
1625 let blob2 = git_repo.write_blob(b"normal").unwrap().detach();
1626 let mut dir_tree_editor = git_repo.empty_tree().edit().unwrap();
1627 dir_tree_editor
1628 .upsert("normal", gix::object::tree::EntryKind::Blob, blob1)
1629 .unwrap();
1630 dir_tree_editor
1631 .upsert("symlink", gix::object::tree::EntryKind::Link, blob2)
1632 .unwrap();
1633 let dir_tree_id = dir_tree_editor.write().unwrap().detach();
1634 let mut root_tree_builder = git_repo.empty_tree().edit().unwrap();
1635 root_tree_builder
1636 .upsert("dir", gix::object::tree::EntryKind::Tree, dir_tree_id)
1637 .unwrap();
1638 let root_tree_id = root_tree_builder.write().unwrap().detach();
1639 let git_author = gix::actor::Signature {
1640 name: "git author".into(),
1641 email: "git.author@example.com".into(),
1642 time: gix::date::Time::new(1000, 60 * 60),
1643 };
1644 let git_committer = gix::actor::Signature {
1645 name: "git committer".into(),
1646 email: "git.committer@example.com".into(),
1647 time: gix::date::Time::new(2000, -480 * 60),
1648 };
1649 let git_commit_id = git_repo
1650 .commit_as(
1651 git_committer.to_ref(&mut TimeBuf::default()),
1652 git_author.to_ref(&mut TimeBuf::default()),
1653 "refs/heads/dummy",
1654 "git commit message",
1655 root_tree_id,
1656 [] as [gix::ObjectId; 0],
1657 )
1658 .unwrap()
1659 .detach();
1660 git_repo
1661 .find_reference("refs/heads/dummy")
1662 .unwrap()
1663 .delete()
1664 .unwrap();
1665 let commit_id = CommitId::from_hex("efdcea5ca4b3658149f899ca7feee6876d077263");
1666 let change_id = ChangeId::from_hex("c64ee0b6e16777fe53991f9281a6cd25");
1668 assert_eq!(
1670 git_commit_id.as_bytes(),
1671 commit_id.as_bytes(),
1672 "{git_commit_id:?} vs {commit_id:?}"
1673 );
1674
1675 let git_commit_id2 = git_repo
1677 .commit_as(
1678 git_committer.to_ref(&mut TimeBuf::default()),
1679 git_author.to_ref(&mut TimeBuf::default()),
1680 "refs/heads/dummy2",
1681 "git commit message 2",
1682 root_tree_id,
1683 [git_commit_id],
1684 )
1685 .unwrap()
1686 .detach();
1687 git_repo
1688 .find_reference("refs/heads/dummy2")
1689 .unwrap()
1690 .delete()
1691 .unwrap();
1692 let commit_id2 = CommitId::from_bytes(git_commit_id2.as_bytes());
1693
1694 let backend = GitBackend::init_external(&settings, store_path, git_repo.path()).unwrap();
1695
1696 backend.import_head_commits([&commit_id2]).unwrap();
1698 let git_refs = backend
1700 .git_repo()
1701 .references()
1702 .unwrap()
1703 .prefixed("refs/jj/keep/")
1704 .unwrap()
1705 .map(|git_ref| git_ref.unwrap().id().detach())
1706 .collect_vec();
1707 assert_eq!(git_refs, vec![git_commit_id2]);
1708
1709 let commit = backend.read_commit(&commit_id).block_on().unwrap();
1710 assert_eq!(&commit.change_id, &change_id);
1711 assert_eq!(commit.parents, vec![CommitId::from_bytes(&[0; 20])]);
1712 assert_eq!(commit.predecessors, vec![]);
1713 assert_eq!(
1714 commit.root_tree,
1715 Merge::resolved(TreeId::from_bytes(root_tree_id.as_bytes()))
1716 );
1717 assert_eq!(commit.description, "git commit message");
1718 assert_eq!(commit.author.name, "git author");
1719 assert_eq!(commit.author.email, "git.author@example.com");
1720 assert_eq!(
1721 commit.author.timestamp.timestamp,
1722 MillisSinceEpoch(1000 * 1000)
1723 );
1724 assert_eq!(commit.author.timestamp.tz_offset, 60);
1725 assert_eq!(commit.committer.name, "git committer");
1726 assert_eq!(commit.committer.email, "git.committer@example.com");
1727 assert_eq!(
1728 commit.committer.timestamp.timestamp,
1729 MillisSinceEpoch(2000 * 1000)
1730 );
1731 assert_eq!(commit.committer.timestamp.tz_offset, -480);
1732
1733 let root_tree = backend
1734 .read_tree(
1735 RepoPath::root(),
1736 &TreeId::from_bytes(root_tree_id.as_bytes()),
1737 )
1738 .block_on()
1739 .unwrap();
1740 let mut root_entries = root_tree.entries();
1741 let dir = root_entries.next().unwrap();
1742 assert_eq!(root_entries.next(), None);
1743 assert_eq!(dir.name().as_internal_str(), "dir");
1744 assert_eq!(
1745 dir.value(),
1746 &TreeValue::Tree(TreeId::from_bytes(dir_tree_id.as_bytes()))
1747 );
1748
1749 let dir_tree = backend
1750 .read_tree(
1751 RepoPath::from_internal_string("dir").unwrap(),
1752 &TreeId::from_bytes(dir_tree_id.as_bytes()),
1753 )
1754 .block_on()
1755 .unwrap();
1756 let mut entries = dir_tree.entries();
1757 let file = entries.next().unwrap();
1758 let symlink = entries.next().unwrap();
1759 assert_eq!(entries.next(), None);
1760 assert_eq!(file.name().as_internal_str(), "normal");
1761 assert_eq!(
1762 file.value(),
1763 &TreeValue::File {
1764 id: FileId::from_bytes(blob1.as_bytes()),
1765 executable: false,
1766 copy_id: CopyId::placeholder(),
1767 }
1768 );
1769 assert_eq!(symlink.name().as_internal_str(), "symlink");
1770 assert_eq!(
1771 symlink.value(),
1772 &TreeValue::Symlink(SymlinkId::from_bytes(blob2.as_bytes()))
1773 );
1774
1775 let commit2 = backend.read_commit(&commit_id2).block_on().unwrap();
1776 assert_eq!(commit2.parents, vec![commit_id.clone()]);
1777 assert_eq!(commit.predecessors, vec![]);
1778 assert_eq!(
1779 commit.root_tree,
1780 Merge::resolved(TreeId::from_bytes(root_tree_id.as_bytes()))
1781 );
1782 }
1783
1784 #[test]
1785 fn read_git_commit_without_importing() {
1786 let settings = user_settings();
1787 let temp_dir = new_temp_dir();
1788 let store_path = temp_dir.path();
1789 let git_repo_path = temp_dir.path().join("git");
1790 let git_repo = git_init(&git_repo_path);
1791
1792 let signature = gix::actor::Signature {
1793 name: GIT_USER.into(),
1794 email: GIT_EMAIL.into(),
1795 time: gix::date::Time::now_utc(),
1796 };
1797 let empty_tree_id =
1798 gix::ObjectId::from_hex(b"4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap();
1799 let git_commit_id = git_repo
1800 .commit_as(
1801 signature.to_ref(&mut TimeBuf::default()),
1802 signature.to_ref(&mut TimeBuf::default()),
1803 "refs/heads/main",
1804 "git commit message",
1805 empty_tree_id,
1806 [] as [gix::ObjectId; 0],
1807 )
1808 .unwrap();
1809
1810 let backend = GitBackend::init_external(&settings, store_path, git_repo.path()).unwrap();
1811
1812 assert!(
1815 backend
1816 .read_commit(&CommitId::from_bytes(git_commit_id.as_bytes()))
1817 .block_on()
1818 .is_ok()
1819 );
1820 assert!(
1821 backend
1822 .cached_extra_metadata_table()
1823 .unwrap()
1824 .get_value(git_commit_id.as_bytes())
1825 .is_some(),
1826 "extra metadata should have been be created"
1827 );
1828 }
1829
1830 #[test]
1831 fn read_signed_git_commit() {
1832 let settings = user_settings();
1833 let temp_dir = new_temp_dir();
1834 let store_path = temp_dir.path();
1835 let git_repo_path = temp_dir.path().join("git");
1836 let git_repo = git_init(git_repo_path);
1837
1838 let signature = gix::actor::Signature {
1839 name: GIT_USER.into(),
1840 email: GIT_EMAIL.into(),
1841 time: gix::date::Time::now_utc(),
1842 };
1843 let empty_tree_id =
1844 gix::ObjectId::from_hex(b"4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap();
1845
1846 let secure_sig =
1847 "here are some ASCII bytes to be used as a test signature\n\ndefinitely not PGP\n";
1848
1849 let mut commit = gix::objs::Commit {
1850 tree: empty_tree_id,
1851 parents: smallvec::SmallVec::new(),
1852 author: signature.clone(),
1853 committer: signature.clone(),
1854 encoding: None,
1855 message: "git commit message".into(),
1856 extra_headers: Vec::new(),
1857 };
1858
1859 let mut commit_buf = Vec::new();
1860 commit.write_to(&mut commit_buf).unwrap();
1861 let commit_str = str::from_utf8(&commit_buf).unwrap();
1862
1863 commit
1864 .extra_headers
1865 .push(("gpgsig".into(), secure_sig.into()));
1866
1867 let git_commit_id = git_repo.write_object(&commit).unwrap();
1868
1869 let backend = GitBackend::init_external(&settings, store_path, git_repo.path()).unwrap();
1870
1871 let commit = backend
1872 .read_commit(&CommitId::from_bytes(git_commit_id.as_bytes()))
1873 .block_on()
1874 .unwrap();
1875
1876 let sig = commit.secure_sig.expect("failed to read the signature");
1877
1878 assert_eq!(str::from_utf8(&sig.sig).unwrap(), secure_sig);
1880 assert_eq!(str::from_utf8(&sig.data).unwrap(), commit_str);
1881 }
1882
1883 #[test]
1884 fn change_id_parsing() {
1885 let id = |commit_object_bytes: &[u8]| {
1886 extract_change_id_from_commit(&CommitRef::from_bytes(commit_object_bytes).unwrap())
1887 };
1888
1889 let commit_with_id = indoc! {b"
1890 tree 126799bf8058d1b5c531e93079f4fe79733920dd
1891 parent bd50783bdf38406dd6143475cd1a3c27938db2ee
1892 author JJ Fan <jjfan@example.com> 1757112665 -0700
1893 committer JJ Fan <jjfan@example.com> 1757359886 -0700
1894 extra-header blah
1895 change-id lkonztmnvsxytrwkxpvuutrmompwylqq
1896
1897 test-commit
1898 "};
1899 insta::assert_compact_debug_snapshot!(
1900 id(commit_with_id),
1901 @r#"Some(ChangeId("efbc06dc4721683f2a45568dbda31e99"))"#
1902 );
1903
1904 let commit_without_id = indoc! {b"
1905 tree 126799bf8058d1b5c531e93079f4fe79733920dd
1906 parent bd50783bdf38406dd6143475cd1a3c27938db2ee
1907 author JJ Fan <jjfan@example.com> 1757112665 -0700
1908 committer JJ Fan <jjfan@example.com> 1757359886 -0700
1909 extra-header blah
1910
1911 no id in header
1912 "};
1913 insta::assert_compact_debug_snapshot!(
1914 id(commit_without_id),
1915 @"None"
1916 );
1917
1918 let commit = indoc! {b"
1919 tree 126799bf8058d1b5c531e93079f4fe79733920dd
1920 parent bd50783bdf38406dd6143475cd1a3c27938db2ee
1921 author JJ Fan <jjfan@example.com> 1757112665 -0700
1922 committer JJ Fan <jjfan@example.com> 1757359886 -0700
1923 change-id lkonztmnvsxytrwkxpvuutrmompwylqq
1924 extra-header blah
1925 change-id abcabcabcabcabcabcabcabcabcabcab
1926
1927 valid change id first
1928 "};
1929 insta::assert_compact_debug_snapshot!(
1930 id(commit),
1931 @r#"Some(ChangeId("efbc06dc4721683f2a45568dbda31e99"))"#
1932 );
1933
1934 let commit = indoc! {b"
1937 tree 126799bf8058d1b5c531e93079f4fe79733920dd
1938 parent bd50783bdf38406dd6143475cd1a3c27938db2ee
1939 author JJ Fan <jjfan@example.com> 1757112665 -0700
1940 committer JJ Fan <jjfan@example.com> 1757359886 -0700
1941 change-id abcabcabcabcabcabcabcabcabcabcab
1942 extra-header blah
1943 change-id lkonztmnvsxytrwkxpvuutrmompwylqq
1944
1945 valid change id first
1946 "};
1947 insta::assert_compact_debug_snapshot!(
1948 id(commit),
1949 @"None"
1950 );
1951 }
1952
1953 #[test]
1954 fn round_trip_change_id_via_git_header() {
1955 let settings = user_settings();
1956 let temp_dir = new_temp_dir();
1957
1958 let store_path = temp_dir.path().join("store");
1959 fs::create_dir(&store_path).unwrap();
1960 let empty_store_path = temp_dir.path().join("empty_store");
1961 fs::create_dir(&empty_store_path).unwrap();
1962 let git_repo_path = temp_dir.path().join("git");
1963 let git_repo = git_init(git_repo_path);
1964
1965 let backend = GitBackend::init_external(&settings, &store_path, git_repo.path()).unwrap();
1966 let original_change_id = ChangeId::from_hex("1111eeee1111eeee1111eeee1111eeee");
1967 let commit = Commit {
1968 parents: vec![backend.root_commit_id().clone()],
1969 predecessors: vec![],
1970 root_tree: Merge::resolved(backend.empty_tree_id().clone()),
1971 conflict_labels: Merge::resolved(String::new()),
1972 change_id: original_change_id.clone(),
1973 description: "initial".to_string(),
1974 author: create_signature(),
1975 committer: create_signature(),
1976 secure_sig: None,
1977 };
1978
1979 let (initial_commit_id, _init_commit) =
1980 backend.write_commit(commit, None).block_on().unwrap();
1981 let commit = backend.read_commit(&initial_commit_id).block_on().unwrap();
1982 assert_eq!(
1983 commit.change_id, original_change_id,
1984 "The change-id header did not roundtrip"
1985 );
1986
1987 let no_extra_backend =
1991 GitBackend::init_external(&settings, &empty_store_path, git_repo.path()).unwrap();
1992 let no_extra_commit = no_extra_backend
1993 .read_commit(&initial_commit_id)
1994 .block_on()
1995 .unwrap();
1996
1997 assert_eq!(
1998 no_extra_commit.change_id, original_change_id,
1999 "The change-id header did not roundtrip"
2000 );
2001 }
2002
2003 #[test]
2004 fn read_empty_string_placeholder() {
2005 let git_signature1 = gix::actor::Signature {
2006 name: EMPTY_STRING_PLACEHOLDER.into(),
2007 email: "git.author@example.com".into(),
2008 time: gix::date::Time::new(1000, 60 * 60),
2009 };
2010 let signature1 = signature_from_git(git_signature1.to_ref(&mut TimeBuf::default()));
2011 assert!(signature1.name.is_empty());
2012 assert_eq!(signature1.email, "git.author@example.com");
2013 let git_signature2 = gix::actor::Signature {
2014 name: "git committer".into(),
2015 email: EMPTY_STRING_PLACEHOLDER.into(),
2016 time: gix::date::Time::new(2000, -480 * 60),
2017 };
2018 let signature2 = signature_from_git(git_signature2.to_ref(&mut TimeBuf::default()));
2019 assert_eq!(signature2.name, "git committer");
2020 assert!(signature2.email.is_empty());
2021 }
2022
2023 #[test]
2024 fn write_empty_string_placeholder() {
2025 let signature1 = Signature {
2026 name: "".to_string(),
2027 email: "someone@example.com".to_string(),
2028 timestamp: Timestamp {
2029 timestamp: MillisSinceEpoch(0),
2030 tz_offset: 0,
2031 },
2032 };
2033 let git_signature1 = signature_to_git(&signature1);
2034 assert_eq!(git_signature1.name, EMPTY_STRING_PLACEHOLDER);
2035 assert_eq!(git_signature1.email, "someone@example.com");
2036 let signature2 = Signature {
2037 name: "Someone".to_string(),
2038 email: "".to_string(),
2039 timestamp: Timestamp {
2040 timestamp: MillisSinceEpoch(0),
2041 tz_offset: 0,
2042 },
2043 };
2044 let git_signature2 = signature_to_git(&signature2);
2045 assert_eq!(git_signature2.name, "Someone");
2046 assert_eq!(git_signature2.email, EMPTY_STRING_PLACEHOLDER);
2047 }
2048
2049 #[test]
2051 fn git_commit_parents() {
2052 let settings = user_settings();
2053 let temp_dir = new_temp_dir();
2054 let store_path = temp_dir.path();
2055 let git_repo_path = temp_dir.path().join("git");
2056 let git_repo = git_init(&git_repo_path);
2057
2058 let backend = GitBackend::init_external(&settings, store_path, git_repo.path()).unwrap();
2059 let mut commit = Commit {
2060 parents: vec![],
2061 predecessors: vec![],
2062 root_tree: Merge::resolved(backend.empty_tree_id().clone()),
2063 conflict_labels: Merge::resolved(String::new()),
2064 change_id: ChangeId::from_hex("abc123"),
2065 description: "".to_string(),
2066 author: create_signature(),
2067 committer: create_signature(),
2068 secure_sig: None,
2069 };
2070
2071 let write_commit = |commit: Commit| -> BackendResult<(CommitId, Commit)> {
2072 backend.write_commit(commit, None).block_on()
2073 };
2074
2075 commit.parents = vec![];
2077 assert_matches!(
2078 write_commit(commit.clone()),
2079 Err(BackendError::Other(err)) if err.to_string().contains("no parents")
2080 );
2081
2082 commit.parents = vec![backend.root_commit_id().clone()];
2084 let first_id = write_commit(commit.clone()).unwrap().0;
2085 let first_commit = backend.read_commit(&first_id).block_on().unwrap();
2086 assert_eq!(first_commit, commit);
2087 let first_git_commit = git_repo.find_commit(git_id(&first_id)).unwrap();
2088 assert!(first_git_commit.parent_ids().collect_vec().is_empty());
2089
2090 commit.parents = vec![first_id.clone()];
2092 let second_id = write_commit(commit.clone()).unwrap().0;
2093 let second_commit = backend.read_commit(&second_id).block_on().unwrap();
2094 assert_eq!(second_commit, commit);
2095 let second_git_commit = git_repo.find_commit(git_id(&second_id)).unwrap();
2096 assert_eq!(
2097 second_git_commit.parent_ids().collect_vec(),
2098 vec![git_id(&first_id)]
2099 );
2100
2101 commit.parents = vec![first_id.clone(), second_id.clone()];
2103 let merge_id = write_commit(commit.clone()).unwrap().0;
2104 let merge_commit = backend.read_commit(&merge_id).block_on().unwrap();
2105 assert_eq!(merge_commit, commit);
2106 let merge_git_commit = git_repo.find_commit(git_id(&merge_id)).unwrap();
2107 assert_eq!(
2108 merge_git_commit.parent_ids().collect_vec(),
2109 vec![git_id(&first_id), git_id(&second_id)]
2110 );
2111
2112 commit.parents = vec![first_id, backend.root_commit_id().clone()];
2114 assert_matches!(
2115 write_commit(commit),
2116 Err(BackendError::Unsupported(message)) if message.contains("root commit")
2117 );
2118 }
2119
2120 #[test]
2121 fn write_tree_conflicts() {
2122 let settings = user_settings();
2123 let temp_dir = new_temp_dir();
2124 let store_path = temp_dir.path();
2125 let git_repo_path = temp_dir.path().join("git");
2126 let git_repo = git_init(&git_repo_path);
2127
2128 let backend = GitBackend::init_external(&settings, store_path, git_repo.path()).unwrap();
2129 let create_tree = |i| {
2130 let blob_id = git_repo.write_blob(format!("content {i}")).unwrap();
2131 let mut tree_builder = git_repo.empty_tree().edit().unwrap();
2132 tree_builder
2133 .upsert(
2134 format!("file{i}"),
2135 gix::object::tree::EntryKind::Blob,
2136 blob_id,
2137 )
2138 .unwrap();
2139 TreeId::from_bytes(tree_builder.write().unwrap().as_bytes())
2140 };
2141
2142 let root_tree = Merge::from_removes_adds(
2143 vec![create_tree(0), create_tree(1)],
2144 vec![create_tree(2), create_tree(3), create_tree(4)],
2145 );
2146 let mut commit = Commit {
2147 parents: vec![backend.root_commit_id().clone()],
2148 predecessors: vec![],
2149 root_tree: root_tree.clone(),
2150 conflict_labels: Merge::resolved(String::new()),
2151 change_id: ChangeId::from_hex("abc123"),
2152 description: "".to_string(),
2153 author: create_signature(),
2154 committer: create_signature(),
2155 secure_sig: None,
2156 };
2157
2158 let write_commit = |commit: Commit| -> BackendResult<(CommitId, Commit)> {
2159 backend.write_commit(commit, None).block_on()
2160 };
2161
2162 let read_commit_id = write_commit(commit.clone()).unwrap().0;
2165 let read_commit = backend.read_commit(&read_commit_id).block_on().unwrap();
2166 assert_eq!(read_commit, commit);
2167 let git_commit = git_repo
2168 .find_commit(gix::ObjectId::from_bytes_or_panic(
2169 read_commit_id.as_bytes(),
2170 ))
2171 .unwrap();
2172 let git_tree = git_repo.find_tree(git_commit.tree_id().unwrap()).unwrap();
2173 let jj_conflict_entries = git_tree
2174 .iter()
2175 .map(Result::unwrap)
2176 .filter(|entry| {
2177 entry.filename().starts_with(b".jjconflict")
2178 || entry.filename() == JJ_CONFLICT_README_FILE_NAME
2179 })
2180 .collect_vec();
2181 assert!(
2182 jj_conflict_entries
2183 .iter()
2184 .filter(|entry| entry.filename() != JJ_CONFLICT_README_FILE_NAME)
2185 .all(|entry| entry.mode().value() == 0o040000)
2186 );
2187 let mut iter = jj_conflict_entries.iter();
2188 let entry = iter.next().unwrap();
2189 assert_eq!(entry.filename(), b".jjconflict-base-0");
2190 assert_eq!(
2191 entry.id().as_bytes(),
2192 root_tree.get_remove(0).unwrap().as_bytes()
2193 );
2194 let entry = iter.next().unwrap();
2195 assert_eq!(entry.filename(), b".jjconflict-base-1");
2196 assert_eq!(
2197 entry.id().as_bytes(),
2198 root_tree.get_remove(1).unwrap().as_bytes()
2199 );
2200 let entry = iter.next().unwrap();
2201 assert_eq!(entry.filename(), b".jjconflict-side-0");
2202 assert_eq!(
2203 entry.id().as_bytes(),
2204 root_tree.get_add(0).unwrap().as_bytes()
2205 );
2206 let entry = iter.next().unwrap();
2207 assert_eq!(entry.filename(), b".jjconflict-side-1");
2208 assert_eq!(
2209 entry.id().as_bytes(),
2210 root_tree.get_add(1).unwrap().as_bytes()
2211 );
2212 let entry = iter.next().unwrap();
2213 assert_eq!(entry.filename(), b".jjconflict-side-2");
2214 assert_eq!(
2215 entry.id().as_bytes(),
2216 root_tree.get_add(2).unwrap().as_bytes()
2217 );
2218 let entry = iter.next().unwrap();
2219 assert_eq!(entry.filename(), b"JJ-CONFLICT-README");
2220 assert_eq!(entry.mode().value(), 0o100644);
2221 assert!(iter.next().is_none());
2222
2223 commit.root_tree = Merge::resolved(create_tree(5));
2226 let read_commit_id = write_commit(commit.clone()).unwrap().0;
2227 let read_commit = backend.read_commit(&read_commit_id).block_on().unwrap();
2228 assert_eq!(read_commit, commit);
2229 let git_commit = git_repo
2230 .find_commit(gix::ObjectId::from_bytes_or_panic(
2231 read_commit_id.as_bytes(),
2232 ))
2233 .unwrap();
2234 assert_eq!(
2235 Merge::resolved(TreeId::from_bytes(git_commit.tree_id().unwrap().as_bytes())),
2236 commit.root_tree
2237 );
2238 }
2239
2240 #[test]
2241 fn commit_has_ref() {
2242 let settings = user_settings();
2243 let temp_dir = new_temp_dir();
2244 let backend = GitBackend::init_internal(&settings, temp_dir.path()).unwrap();
2245 let git_repo = backend.git_repo();
2246 let signature = Signature {
2247 name: "Someone".to_string(),
2248 email: "someone@example.com".to_string(),
2249 timestamp: Timestamp {
2250 timestamp: MillisSinceEpoch(0),
2251 tz_offset: 0,
2252 },
2253 };
2254 let commit = Commit {
2255 parents: vec![backend.root_commit_id().clone()],
2256 predecessors: vec![],
2257 root_tree: Merge::resolved(backend.empty_tree_id().clone()),
2258 conflict_labels: Merge::resolved(String::new()),
2259 change_id: ChangeId::new(vec![42; 16]),
2260 description: "initial".to_string(),
2261 author: signature.clone(),
2262 committer: signature,
2263 secure_sig: None,
2264 };
2265 let commit_id = backend.write_commit(commit, None).block_on().unwrap().0;
2266 let git_refs = git_repo.references().unwrap();
2267 let git_ref_ids: Vec<_> = git_refs
2268 .prefixed("refs/jj/keep/")
2269 .unwrap()
2270 .map(|x| x.unwrap().id().detach())
2271 .collect();
2272 assert!(git_ref_ids.iter().any(|id| *id == git_id(&commit_id)));
2273
2274 for git_ref in git_refs.prefixed("refs/jj/keep/").unwrap() {
2276 git_ref.unwrap().delete().unwrap();
2277 }
2278 backend.import_head_commits([&commit_id]).unwrap();
2280 let git_refs = git_repo.references().unwrap();
2281 let git_ref_ids: Vec<_> = git_refs
2282 .prefixed("refs/jj/keep/")
2283 .unwrap()
2284 .map(|x| x.unwrap().id().detach())
2285 .collect();
2286 assert!(git_ref_ids.iter().any(|id| *id == git_id(&commit_id)));
2287 }
2288
2289 #[test]
2290 fn import_head_commits_duplicates() {
2291 let settings = user_settings();
2292 let temp_dir = new_temp_dir();
2293 let backend = GitBackend::init_internal(&settings, temp_dir.path()).unwrap();
2294 let git_repo = backend.git_repo();
2295
2296 let signature = gix::actor::Signature {
2297 name: GIT_USER.into(),
2298 email: GIT_EMAIL.into(),
2299 time: gix::date::Time::now_utc(),
2300 };
2301 let empty_tree_id =
2302 gix::ObjectId::from_hex(b"4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap();
2303 let git_commit_id = git_repo
2304 .commit_as(
2305 signature.to_ref(&mut TimeBuf::default()),
2306 signature.to_ref(&mut TimeBuf::default()),
2307 "refs/heads/main",
2308 "git commit message",
2309 empty_tree_id,
2310 [] as [gix::ObjectId; 0],
2311 )
2312 .unwrap()
2313 .detach();
2314 let commit_id = CommitId::from_bytes(git_commit_id.as_bytes());
2315
2316 backend
2318 .import_head_commits([&commit_id, &commit_id])
2319 .unwrap();
2320 assert!(
2321 git_repo
2322 .references()
2323 .unwrap()
2324 .prefixed("refs/jj/keep/")
2325 .unwrap()
2326 .any(|git_ref| git_ref.unwrap().id().detach() == git_commit_id)
2327 );
2328 }
2329
2330 #[test]
2331 fn overlapping_git_commit_id() {
2332 let settings = user_settings();
2333 let temp_dir = new_temp_dir();
2334 let backend = GitBackend::init_internal(&settings, temp_dir.path()).unwrap();
2335 let commit1 = Commit {
2336 parents: vec![backend.root_commit_id().clone()],
2337 predecessors: vec![],
2338 root_tree: Merge::resolved(backend.empty_tree_id().clone()),
2339 conflict_labels: Merge::resolved(String::new()),
2340 change_id: ChangeId::from_hex("7f0a7ce70354b22efcccf7bf144017c4"),
2341 description: "initial".to_string(),
2342 author: create_signature(),
2343 committer: create_signature(),
2344 secure_sig: None,
2345 };
2346
2347 let write_commit = |commit: Commit| -> BackendResult<(CommitId, Commit)> {
2348 backend.write_commit(commit, None).block_on()
2349 };
2350
2351 let (commit_id1, mut commit2) = write_commit(commit1).unwrap();
2352 commit2.predecessors.push(commit_id1.clone());
2353 let (commit_id2, mut actual_commit2) = write_commit(commit2.clone()).unwrap();
2356 assert_eq!(
2358 backend.read_commit(&commit_id2).block_on().unwrap(),
2359 actual_commit2
2360 );
2361 assert_ne!(commit_id2, commit_id1);
2362 assert_ne!(
2364 actual_commit2.committer.timestamp.timestamp,
2365 commit2.committer.timestamp.timestamp
2366 );
2367 actual_commit2.committer.timestamp.timestamp = commit2.committer.timestamp.timestamp;
2369 assert_eq!(actual_commit2, commit2);
2370 }
2371
2372 #[test]
2373 fn write_signed_commit() {
2374 let settings = user_settings();
2375 let temp_dir = new_temp_dir();
2376 let backend = GitBackend::init_internal(&settings, temp_dir.path()).unwrap();
2377
2378 let commit = Commit {
2379 parents: vec![backend.root_commit_id().clone()],
2380 predecessors: vec![],
2381 root_tree: Merge::resolved(backend.empty_tree_id().clone()),
2382 conflict_labels: Merge::resolved(String::new()),
2383 change_id: ChangeId::new(vec![42; 16]),
2384 description: "initial".to_string(),
2385 author: create_signature(),
2386 committer: create_signature(),
2387 secure_sig: None,
2388 };
2389
2390 let mut signer = |data: &_| {
2391 let hash: String = hex_util::encode_hex(&blake2b_hash(data));
2392 Ok(format!("test sig\nhash={hash}\n").into_bytes())
2393 };
2394
2395 let (id, commit) = backend
2396 .write_commit(commit, Some(&mut signer as &mut SigningFn))
2397 .block_on()
2398 .unwrap();
2399
2400 let git_repo = backend.git_repo();
2401 let obj = git_repo
2402 .find_object(gix::ObjectId::from_bytes_or_panic(id.as_bytes()))
2403 .unwrap();
2404 insta::assert_snapshot!(str::from_utf8(&obj.data).unwrap(), @"
2405 tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904
2406 author Someone <someone@example.com> 0 +0000
2407 committer Someone <someone@example.com> 0 +0000
2408 change-id xpxpxpxpxpxpxpxpxpxpxpxpxpxpxpxp
2409 gpgsig test sig
2410 hash=03feb0caccbacce2e7b7bca67f4c82292dd487e669ed8a813120c9f82d3fd0801420a1f5d05e1393abfe4e9fc662399ec4a9a1898c5f1e547e0044a52bd4bd29
2411
2412 initial
2413 ");
2414
2415 let returned_sig = commit.secure_sig.expect("failed to return the signature");
2416
2417 let commit = backend.read_commit(&id).block_on().unwrap();
2418
2419 let sig = commit.secure_sig.expect("failed to read the signature");
2420 assert_eq!(&sig, &returned_sig);
2421
2422 insta::assert_snapshot!(str::from_utf8(&sig.sig).unwrap(), @"
2423 test sig
2424 hash=03feb0caccbacce2e7b7bca67f4c82292dd487e669ed8a813120c9f82d3fd0801420a1f5d05e1393abfe4e9fc662399ec4a9a1898c5f1e547e0044a52bd4bd29
2425 ");
2426 insta::assert_snapshot!(str::from_utf8(&sig.data).unwrap(), @"
2427 tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904
2428 author Someone <someone@example.com> 0 +0000
2429 committer Someone <someone@example.com> 0 +0000
2430 change-id xpxpxpxpxpxpxpxpxpxpxpxpxpxpxpxp
2431
2432 initial
2433 ");
2434 }
2435
2436 fn git_id(commit_id: &CommitId) -> gix::ObjectId {
2437 gix::ObjectId::from_bytes_or_panic(commit_id.as_bytes())
2438 }
2439
2440 fn create_signature() -> Signature {
2441 Signature {
2442 name: GIT_USER.to_string(),
2443 email: GIT_EMAIL.to_string(),
2444 timestamp: Timestamp {
2445 timestamp: MillisSinceEpoch(0),
2446 tz_offset: 0,
2447 },
2448 }
2449 }
2450
2451 fn user_settings() -> UserSettings {
2456 let config = StackedConfig::with_defaults();
2457 UserSettings::from_config(config).unwrap()
2458 }
2459}