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