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