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