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