Skip to main content

jj_lib/
git_backend.rs

1// Copyright 2020 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#![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;
99/// Ref namespace used only for preventing GC.
100const 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/// `GitBackend`-specific error that may occur after the backend is loaded.
147#[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    // While gix::Repository can be created from gix::ThreadSafeRepository, it's
184    // cheaper to cache the thread-local instance behind a mutex than creating
185    // one for each backend method call. Our GitBackend is most likely to be
186    // used in a single-threaded context.
187    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    /// Initializes backend by creating a new Git repo at the specified
250    /// workspace path. The workspace directory must exist.
251    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    /// Initializes backend with an existing Git repo at the specified path.
280    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            // When a repository is created in Windows, format the path with *forward
314            // slashes* and not backwards slashes. This makes it possible to use the same
315            // repository under Windows Subsystem for Linux.
316            //
317            // This only works for relative paths. If the path is absolute, there's not much
318            // we can do, and it simply won't work inside and outside WSL at the same time.
319            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    /// Returns a new thread-local handle for the underlying Git repository.
370    ///
371    /// Use [`Self::open_git_repo_at_workdir()`] for worktree operations.
372    pub fn git_repo(&self) -> gix::Repository {
373        self.base_repo.to_thread_local()
374    }
375
376    /// Reopens the repository at the given workspace path. Returns a new
377    /// thread-local handle.
378    pub fn open_git_repo_at_workdir(
379        &self,
380        path: &Path,
381    ) -> Result<gix::Repository, GitRepoAtWorkdirError> {
382        // Try the open repository first.
383        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        // The input path doesn't include ".git".
391        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            // The last (path, work_repo) can be cached if needed.
407            Ok(work_repo)
408        } else {
409            let path = path.to_owned();
410            Err(GitRepoAtWorkdirError::Unrelated { path })
411        }
412    }
413
414    /// Path to the `.git` directory or the repository itself if it's bare.
415    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        // The list of shallow roots is cached by gix, but it's still expensive
421        // to stat file on every read_object() call. Refreshing shallow roots is
422        // also bad for consistency reasons.
423        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        // Since the parent table was the head, saved table are likely to be new head.
471        // If it's not, cache will be reloaded when entry can't be found.
472        *self.cached_extra_metadata.lock().unwrap() = Some(table);
473        Ok(())
474    }
475
476    /// Imports the given commits and ancestors from the backing Git repo.
477    ///
478    /// The `head_ids` may contain commits that have already been imported, but
479    /// the caller should filter them out to eliminate redundant I/O processing.
480    #[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        // Create no-gc ref even if known to the extras table. Concurrent GC
494        // process might have deleted the no-gc ref.
495        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        // These commits are imported from Git. Make our change ids persist (otherwise
501        // future write_commit() could reassign new change id.)
502        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        // TODO(kfm): probably want to do something here if it is a merge
563        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    // Similar to gix's write_blob, but compute the hash outside our lock to
572    // reduce contention.
573    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            // reuse the precomputed hash, since Gitoxide provides an API for it (otherwise
591            // Gitoxide recomputes it).
592            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
605/// Canonicalizes the given `path` except for the last `".git"` component.
606///
607/// The last path component matters when opening a Git repo without `core.bare`
608/// config. This config is usually set, but the "repo" tool will set up such
609/// repositories and symlinks. Opening such repo with fully-canonicalized path
610/// would turn a colocated Git repo into a bare repo.
611pub 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            // Committer has to be configured to record reflog. Author isn't
626            // needed, but let's copy the same values.
627            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        // The git_target path should point the repository, not the working directory.
633        .open_path_as_is(true)
634        // Gitoxide recommends this when correctness is preferred
635        .strict_config(true)
636}
637
638/// Parses the `jj:conflict-labels` header value if present.
639fn 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
655/// Parses the `jj:trees` header value if present, otherwise returns the
656/// resolved tree ID from Git.
657fn 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    // It is invalid to use `jj:trees` with a non-conflicted tree. If this were
673    // allowed, it would be possible to construct a commit which appears to have
674    // different contents depending on whether it is viewed using `jj` or `git`.
675    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    // If the git header has a change-id field, we attempt to convert that to a
692    // valid JJ Change Id
693    let change_id = extract_change_id_from_commit(&commit)
694        .unwrap_or_else(|| synthetic_change_id_from_git_commit_id(id));
695
696    // shallow commits don't have parents their parents actually fetched, so we
697    // discard them here
698    // TODO: This causes issues when a shallow repository is deepened/unshallowed
699    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    // If the commit is a conflict, the conflict labels are stored in a commit
708    // header separately from the trees.
709    let conflict_labels = extract_conflict_labels_from_commit(&commit);
710    // Conflicted commits written before we started using the `jj:trees` header
711    // (~March 2024) may have the root trees stored in the extra metadata table
712    // instead. For such commits, we'll update the root tree later when we read the
713    // extra metadata.
714    let root_tree = extract_root_tree_from_commit(&commit)
715        .map_err(|()| to_read_object_err("Invalid jj:trees header", id))?;
716    // Use lossy conversion as commit message with "mojibake" is still better than
717    // nothing.
718    // TODO: what should we do with commit.encoding?
719    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    // If the commit is signed, extract both the signature and the signed data
724    // (which is the commit buffer with the gpgsig header omitted).
725    // We have to re-parse the raw commit data because gix CommitRef does not give
726    // us the sogned data, only the signature.
727    // Ideally, we could use try_to_commit_ref_iter at the beginning of this
728    // function and extract everything from that. For now, this works
729    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        // If this commit has associated extra metadata, we may reset this later.
746        root_tree,
747        conflict_labels,
748        change_id,
749        description,
750        author,
751        committer,
752        secure_sig,
753    })
754}
755
756/// Extracts change id from commit headers.
757pub 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
765/// Deterministically creates a change id based on the commit id
766///
767/// Used when we get a commit without a change id. The exact algorithm for the
768/// computation should not be relied upon.
769pub fn synthetic_change_id_from_git_commit_id(id: &CommitId) -> ChangeId {
770    // We reverse the bits of the commit id to create the change id. We don't
771    // want to use the first bytes unmodified because then it would be ambiguous
772    // if a given hash prefix refers to the commit id or the change id. It would
773    // have been enough to pick the last 16 bytes instead of the leading 16
774    // bytes to address that. We also reverse the bits to make it less likely
775    // that users depend on any relationship between the two ids.
776    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); // in minutes
802    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    // git does not support empty names or emails
814    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, // in seconds
827    );
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
868/// Returns `RefEdit` that will create a ref in `refs/jj/keep` if not exist.
869/// Used for preventing GC of commits we create.
870fn 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
900/// Recreates `refs/jj/keep` refs for the `new_heads`, and removes the other
901/// unreachable and non-head refs.
902fn recreate_no_gc_refs(
903    git_repo: &gix::Repository,
904    new_heads: impl IntoIterator<Item = CommitId>,
905    keep_newer: SystemTime,
906) -> BackendResult<()> {
907    // Calculate diff between existing no-gc refs and new heads.
908    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        // Check timestamp of loose ref, but this is still racy on re-import
930        // because:
931        // - existing packed ref won't be demoted to loose ref
932        // - existing loose ref won't be touched
933        //
934        // TODO: might be better to switch to a dummy merge, where new no-gc ref
935        // will always have a unique name. Doing that with the current
936        // ref-per-head strategy would increase the number of the no-gc refs.
937        // https://github.com/jj-vcs/jj/pull/2659#issuecomment-1837057782
938        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        // Also deletes no-gc ref of random name created by old jj.
948        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    // It's slow to delete packed refs one by one, so update refs all at once.
959    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(); // underflow
974    let mut git = Command::new(program);
975    git.arg("--git-dir=.") // turn off discovery
976        .arg("gc")
977        .arg(format!("--prune=@{} +0000", keep_newer.as_secs()));
978    // Don't specify it by GIT_DIR/--git-dir. On Windows, the path could be
979    // canonicalized as UNC path, which wouldn't be supported by git.
980    git.current_dir(git_dir);
981    // TODO: pass output to UI layer instead of printing directly here
982    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        // TODO(#1624): Should we read the root tree here and check if it has a
1056        // `.jjconflict-...` entries? That could happen if the user used `git` to e.g.
1057        // change the description of a commit with tree-level conflicts.
1058        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        // While Git tree entries are sorted, the rule is slightly different.
1219        // Directory names are sorted as if they had trailing "/".
1220        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        // Tree entries to be written must be sorted by Entry::filename(), which
1228        // is slightly different from the order of our backend::Tree.
1229        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: _, // TODO: Use the value
1238                    } => 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: _, // TODO: Use the value
1247                    } => 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            // TODO: Remove this hack and map to ObjectNotFound error if we're sure that
1308            // there are no reachable ancestor commits without extras metadata. Git commits
1309            // imported by jj < 0.8.0 might not have extras (#924).
1310            // https://github.com/jj-vcs/jj/issues/2343
1311            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                // Git doesn't have a root commit, so if the parent is the root commit, we don't
1345                // add it to the list of parents to write in the Git commit. We also check that
1346                // there are no other parents since Git cannot represent a merge between a root
1347                // commit and another commit.
1348                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            // Labels cannot contain '\n' since we use it as a separator in the header.
1362            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        // If two writers write commits of the same id with different metadata, they
1401        // will both succeed and the metadata entries will be "merged" later. Since
1402        // metadata entry is keyed by the commit id, one of the entries would be lost.
1403        // To prevent such race condition locally, we extend the scope covered by the
1404        // table lock. This is still racy if multiple machines are involved and the
1405        // repository is rsync-ed.
1406        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                // we don't use gix pool, but at least use their heuristic
1420                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                    // It's possible a commit already exists with the same
1445                    // commit id but different change id. Adjust the timestamp
1446                    // until this is no longer the case.
1447                    //
1448                    // For example, this can happen when rebasing duplicate
1449                    // commits, https://github.com/jj-vcs/jj/issues/694.
1450                    //
1451                    // `jj` resets the committer timestamp to the current
1452                    // timestamp whenever it rewrites a commit. So, it's
1453                    // unlikely for the timestamp to be 0 even if the original
1454                    // commit had its timestamp set to 0. Moreover, we test that
1455                    // a commit with a negative timestamp can still be written
1456                    // and read back by `jj`.
1457                    committer.time.seconds -= 1;
1458                }
1459                _ => break CommitId::from_bytes(git_id.as_bytes()),
1460            }
1461        };
1462
1463        // Everything up to this point had no permanent effect on the repo except
1464        // GC-able objects
1465        locked_repo
1466            .edit_reference(to_no_gc_ref_update(&id))
1467            .map_err(|err| BackendError::Other(Box::new(err)))?;
1468
1469        // Update the signature to match the one that was actually written to the object
1470        // store
1471        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                // TODO: Renamed symlinks cannot be returned because CopyRecord
1502                // expects `source_file: FileId`.
1503                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        // No locking is needed since we aren't going to add new "commits".
1567        let table = self.cached_extra_metadata_table()?;
1568        // TODO: remove unreachable entries from extras table if segment file
1569        // mtime <= keep_newer? (it won't be consistent with no-gc refs
1570        // preserved by the keep_newer timestamp though)
1571        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        // Since "git gc" will move loose refs into packed refs, in-memory
1582        // packed-refs cache should be invalidated without relying on mtime.
1583        git_repo.refs.force_refresh_packed_buffer().ok();
1584        Ok(())
1585    }
1586}
1587
1588/// Write a tree conflict as a special tree with `.jjconflict-base-N` and
1589/// `.jjconflict-side-N` subtrees. This ensure that the parts are not GC'd.
1590/// Also includes a `JJ-CONFLICT-README` file explaining why these trees are
1591/// present. The rest of the tree is copied from the first term of the conflict,
1592/// which prevents editors with Git support from highlighting all files as new.
1593fn write_tree_conflict(
1594    repo: &gix::Repository,
1595    conflict: &Merge<TreeId>,
1596) -> BackendResult<gix::ObjectId> {
1597    // Tree entries to be written must be sorted by Entry::filename().
1598    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        // Add a commit with some files in
1758        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        // The change id is the leading reverse bits of the commit id
1789        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        // Check that the git commit above got the hash we expect
1803        assert_eq!(
1804            git_commit_id.as_bytes(),
1805            commit_id.as_bytes(),
1806            "{git_commit_id:?} vs {commit_id:?}"
1807        );
1808
1809        // Add an empty commit on top
1810        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        // Import the head commit and its ancestors
1826        backend.import_head_commits([&commit_id2])?;
1827        // Ref should be created only for the head commit
1828        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        // read_commit() without import_head_commits() works as of now. This might be
1940        // changed later.
1941        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        // converting to string for nicer assert diff
2004        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        // We only look at the first change id if multiple are present, so this should
2063        // error
2064        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        // Because of how change ids are also persisted in extra proto files,
2116        // initialize a new store without those files, but reuse the same git
2117        // storage. This change-id must be derived from the git commit header.
2118        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 that parents get written correctly
2178    #[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        // No parents
2205        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        // Only root commit as parent
2212        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        // Only non-root commit as parent
2220        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        // Merge commit
2231        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        // Merge commit with root as one parent
2242        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        // When writing a tree-level conflict, the root tree on the git side has the
2294        // individual trees as subtrees.
2295        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        // When writing a single tree using the new format, it's represented by a
2353        // regular git tree.
2354        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        // Concurrently-running GC deletes the ref, leaving the extra metadata.
2403        for git_ref in git_refs.prefixed("refs/jj/keep/")? {
2404            git_ref.unwrap().delete().unwrap();
2405        }
2406        // Re-imported commit should have new ref.
2407        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        // Ref creation shouldn't fail because of duplicated head ids.
2444        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        // `write_commit` should prevent the ids from being the same by changing the
2479        // committer timestamp of the commit it actually writes.
2480        let (commit_id2, mut actual_commit2) = write_commit(commit2.clone())?;
2481        // The returned matches the ID
2482        assert_eq!(backend.read_commit(&commit_id2).block_on()?, actual_commit2);
2483        assert_ne!(commit_id2, commit_id1);
2484        // The committer timestamp should differ
2485        assert_ne!(
2486            actual_commit2.committer.timestamp.timestamp,
2487            commit2.committer.timestamp.timestamp
2488        );
2489        // The rest of the commit should be the same
2490        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    // Not using testutils::user_settings() because there is a dependency cycle
2603    // 'jj_lib (1) -> testutils -> jj_lib (2)' which creates another distinct
2604    // UserSettings type. testutils returns jj_lib (2)'s UserSettings, whereas
2605    // our UserSettings type comes from jj_lib (1).
2606    fn user_settings() -> UserSettings {
2607        let config = StackedConfig::with_defaults();
2608        UserSettings::from_config(config).unwrap()
2609    }
2610}