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