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