jujutsu_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
15use std::fmt::{Debug, Error, Formatter};
16use std::fs::File;
17use std::io::{Cursor, Read, Write};
18use std::path::Path;
19use std::sync::{Arc, Mutex};
20
21use git2::Oid;
22use itertools::Itertools;
23use prost::Message;
24
25use crate::backend::{
26    make_root_commit, Backend, BackendError, BackendResult, ChangeId, Commit, CommitId, Conflict,
27    ConflictId, ConflictPart, FileId, MillisSinceEpoch, ObjectId, Signature, SymlinkId, Timestamp,
28    Tree, TreeId, TreeValue,
29};
30use crate::repo_path::{RepoPath, RepoPathComponent};
31use crate::stacked_table::{ReadonlyTable, TableSegment, TableStore};
32
33const HASH_LENGTH: usize = 20;
34const CHANGE_ID_LENGTH: usize = 16;
35/// Ref namespace used only for preventing GC.
36pub const NO_GC_REF_NAMESPACE: &str = "refs/jj/keep/";
37const CONFLICT_SUFFIX: &str = ".jjconflict";
38
39pub struct GitBackend {
40    repo: Mutex<git2::Repository>,
41    root_commit_id: CommitId,
42    root_change_id: ChangeId,
43    empty_tree_id: TreeId,
44    extra_metadata_store: TableStore,
45    cached_extra_metadata: Mutex<Option<Arc<ReadonlyTable>>>,
46}
47
48impl GitBackend {
49    fn new(repo: git2::Repository, extra_metadata_store: TableStore) -> Self {
50        let root_commit_id = CommitId::from_bytes(&[0; HASH_LENGTH]);
51        let root_change_id = ChangeId::from_bytes(&[0; CHANGE_ID_LENGTH]);
52        let empty_tree_id = TreeId::from_hex("4b825dc642cb6eb9a060e54bf8d69288fbee4904");
53        GitBackend {
54            repo: Mutex::new(repo),
55            root_commit_id,
56            root_change_id,
57            empty_tree_id,
58            extra_metadata_store,
59            cached_extra_metadata: Mutex::new(None),
60        }
61    }
62
63    pub fn init_internal(store_path: &Path) -> Self {
64        let git_repo = git2::Repository::init_bare(store_path.join("git")).unwrap();
65        let extra_path = store_path.join("extra");
66        std::fs::create_dir(&extra_path).unwrap();
67        let mut git_target_file = File::create(store_path.join("git_target")).unwrap();
68        git_target_file.write_all(b"git").unwrap();
69        let extra_metadata_store = TableStore::init(extra_path, HASH_LENGTH);
70        GitBackend::new(git_repo, extra_metadata_store)
71    }
72
73    pub fn init_external(store_path: &Path, git_repo_path: &Path) -> Self {
74        let extra_path = store_path.join("extra");
75        std::fs::create_dir(&extra_path).unwrap();
76        let mut git_target_file = File::create(store_path.join("git_target")).unwrap();
77        git_target_file
78            .write_all(git_repo_path.to_str().unwrap().as_bytes())
79            .unwrap();
80        let repo = git2::Repository::open(store_path.join(git_repo_path)).unwrap();
81        let extra_metadata_store = TableStore::init(extra_path, HASH_LENGTH);
82        GitBackend::new(repo, extra_metadata_store)
83    }
84
85    pub fn load(store_path: &Path) -> Self {
86        let mut git_target_file = File::open(store_path.join("git_target")).unwrap();
87        let mut buf = Vec::new();
88        git_target_file.read_to_end(&mut buf).unwrap();
89        let git_repo_path_str = String::from_utf8(buf).unwrap();
90        let git_repo_path = store_path.join(git_repo_path_str).canonicalize().unwrap();
91        let repo = git2::Repository::open(git_repo_path).unwrap();
92        let extra_metadata_store = TableStore::load(store_path.join("extra"), HASH_LENGTH);
93        GitBackend::new(repo, extra_metadata_store)
94    }
95}
96
97fn signature_from_git(signature: git2::Signature) -> Signature {
98    let name = signature.name().unwrap_or("<no name>").to_owned();
99    let email = signature.email().unwrap_or("<no email>").to_owned();
100    let timestamp = MillisSinceEpoch(signature.when().seconds() * 1000);
101    let tz_offset = signature.when().offset_minutes();
102    Signature {
103        name,
104        email,
105        timestamp: Timestamp {
106            timestamp,
107            tz_offset,
108        },
109    }
110}
111
112fn signature_to_git(signature: &Signature) -> git2::Signature {
113    let name = &signature.name;
114    let email = &signature.email;
115    let time = git2::Time::new(
116        signature.timestamp.timestamp.0.div_euclid(1000),
117        signature.timestamp.tz_offset,
118    );
119    git2::Signature::new(name, email, &time).unwrap()
120}
121
122fn serialize_extras(commit: &Commit) -> Vec<u8> {
123    let mut proto = crate::protos::store::Commit {
124        change_id: commit.change_id.to_bytes(),
125        ..Default::default()
126    };
127    for predecessor in &commit.predecessors {
128        proto.predecessors.push(predecessor.to_bytes());
129    }
130    proto.encode_to_vec()
131}
132
133fn deserialize_extras(commit: &mut Commit, bytes: &[u8]) {
134    let proto = crate::protos::store::Commit::decode(bytes).unwrap();
135    commit.change_id = ChangeId::new(proto.change_id);
136    for predecessor in &proto.predecessors {
137        commit.predecessors.push(CommitId::from_bytes(predecessor));
138    }
139}
140
141/// Creates a random ref in refs/jj/. Used for preventing GC of commits we
142/// create.
143fn create_no_gc_ref() -> String {
144    let random_bytes: [u8; 16] = rand::random();
145    format!("{NO_GC_REF_NAMESPACE}{}", hex::encode(random_bytes))
146}
147
148fn validate_git_object_id(id: &impl ObjectId) -> Result<git2::Oid, BackendError> {
149    if id.as_bytes().len() != HASH_LENGTH {
150        return Err(BackendError::InvalidHashLength {
151            expected: HASH_LENGTH,
152            actual: id.as_bytes().len(),
153            object_type: id.object_type(),
154            hash: id.hex(),
155        });
156    }
157    let oid = git2::Oid::from_bytes(id.as_bytes()).map_err(|err| BackendError::InvalidHash {
158        object_type: id.object_type(),
159        hash: id.hex(),
160        source: Box::new(err),
161    })?;
162    Ok(oid)
163}
164
165fn map_not_found_err(err: git2::Error, id: &impl ObjectId) -> BackendError {
166    if err.code() == git2::ErrorCode::NotFound {
167        BackendError::ObjectNotFound {
168            object_type: id.object_type(),
169            hash: id.hex(),
170            source: Box::new(err),
171        }
172    } else {
173        BackendError::ReadObject {
174            object_type: id.object_type(),
175            hash: id.hex(),
176            source: Box::new(err),
177        }
178    }
179}
180
181impl Debug for GitBackend {
182    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
183        f.debug_struct("GitStore")
184            .field("path", &self.repo.lock().unwrap().path())
185            .finish()
186    }
187}
188
189impl Backend for GitBackend {
190    fn name(&self) -> &str {
191        "git"
192    }
193
194    fn commit_id_length(&self) -> usize {
195        HASH_LENGTH
196    }
197
198    fn change_id_length(&self) -> usize {
199        CHANGE_ID_LENGTH
200    }
201
202    fn git_repo(&self) -> Option<git2::Repository> {
203        let path = self.repo.lock().unwrap().path().to_owned();
204        Some(git2::Repository::open(path).unwrap())
205    }
206
207    fn read_file(&self, _path: &RepoPath, id: &FileId) -> BackendResult<Box<dyn Read>> {
208        let git_blob_id = validate_git_object_id(id)?;
209        let locked_repo = self.repo.lock().unwrap();
210        let blob = locked_repo
211            .find_blob(git_blob_id)
212            .map_err(|err| map_not_found_err(err, id))?;
213        let content = blob.content().to_owned();
214        Ok(Box::new(Cursor::new(content)))
215    }
216
217    fn write_file(&self, _path: &RepoPath, contents: &mut dyn Read) -> BackendResult<FileId> {
218        let mut bytes = Vec::new();
219        contents.read_to_end(&mut bytes).unwrap();
220        let locked_repo = self.repo.lock().unwrap();
221        let oid = locked_repo
222            .blob(&bytes)
223            .map_err(|err| BackendError::WriteObject {
224                object_type: "file",
225                source: Box::new(err),
226            })?;
227        Ok(FileId::new(oid.as_bytes().to_vec()))
228    }
229
230    fn read_symlink(&self, _path: &RepoPath, id: &SymlinkId) -> Result<String, BackendError> {
231        let git_blob_id = validate_git_object_id(id)?;
232        let locked_repo = self.repo.lock().unwrap();
233        let blob = locked_repo
234            .find_blob(git_blob_id)
235            .map_err(|err| map_not_found_err(err, id))?;
236        let target = String::from_utf8(blob.content().to_owned()).map_err(|err| {
237            BackendError::InvalidUtf8 {
238                object_type: id.object_type(),
239                hash: id.hex(),
240                source: err,
241            }
242        })?;
243        Ok(target)
244    }
245
246    fn write_symlink(&self, _path: &RepoPath, target: &str) -> Result<SymlinkId, BackendError> {
247        let locked_repo = self.repo.lock().unwrap();
248        let oid = locked_repo
249            .blob(target.as_bytes())
250            .map_err(|err| BackendError::WriteObject {
251                object_type: "symlink",
252                source: Box::new(err),
253            })?;
254        Ok(SymlinkId::new(oid.as_bytes().to_vec()))
255    }
256
257    fn root_commit_id(&self) -> &CommitId {
258        &self.root_commit_id
259    }
260
261    fn root_change_id(&self) -> &ChangeId {
262        &self.root_change_id
263    }
264
265    fn empty_tree_id(&self) -> &TreeId {
266        &self.empty_tree_id
267    }
268
269    fn read_tree(&self, _path: &RepoPath, id: &TreeId) -> BackendResult<Tree> {
270        if id == &self.empty_tree_id {
271            return Ok(Tree::default());
272        }
273        let git_tree_id = validate_git_object_id(id)?;
274
275        let locked_repo = self.repo.lock().unwrap();
276        let git_tree = locked_repo.find_tree(git_tree_id).unwrap();
277        let mut tree = Tree::default();
278        for entry in git_tree.iter() {
279            let name = entry.name().unwrap();
280            let (name, value) = match entry.kind().unwrap() {
281                git2::ObjectType::Tree => {
282                    let id = TreeId::from_bytes(entry.id().as_bytes());
283                    (entry.name().unwrap(), TreeValue::Tree(id))
284                }
285                git2::ObjectType::Blob => match entry.filemode() {
286                    0o100644 => {
287                        let id = FileId::from_bytes(entry.id().as_bytes());
288                        if name.ends_with(CONFLICT_SUFFIX) {
289                            (
290                                &name[0..name.len() - CONFLICT_SUFFIX.len()],
291                                TreeValue::Conflict(ConflictId::from_bytes(entry.id().as_bytes())),
292                            )
293                        } else {
294                            (
295                                name,
296                                TreeValue::File {
297                                    id,
298                                    executable: false,
299                                },
300                            )
301                        }
302                    }
303                    0o100755 => {
304                        let id = FileId::from_bytes(entry.id().as_bytes());
305                        (
306                            name,
307                            TreeValue::File {
308                                id,
309                                executable: true,
310                            },
311                        )
312                    }
313                    0o120000 => {
314                        let id = SymlinkId::from_bytes(entry.id().as_bytes());
315                        (name, TreeValue::Symlink(id))
316                    }
317                    mode => panic!("unexpected file mode {mode:?}"),
318                },
319                git2::ObjectType::Commit => {
320                    let id = CommitId::from_bytes(entry.id().as_bytes());
321                    (name, TreeValue::GitSubmodule(id))
322                }
323                kind => panic!("unexpected object type {kind:?}"),
324            };
325            tree.set(RepoPathComponent::from(name), value);
326        }
327        Ok(tree)
328    }
329
330    fn write_tree(&self, _path: &RepoPath, contents: &Tree) -> BackendResult<TreeId> {
331        let locked_repo = self.repo.lock().unwrap();
332        let mut builder = locked_repo.treebuilder(None).unwrap();
333        for entry in contents.entries() {
334            let name = entry.name().string();
335            let (name, id, filemode) = match entry.value() {
336                TreeValue::File {
337                    id,
338                    executable: false,
339                } => (name, id.as_bytes(), 0o100644),
340                TreeValue::File {
341                    id,
342                    executable: true,
343                } => (name, id.as_bytes(), 0o100755),
344                TreeValue::Symlink(id) => (name, id.as_bytes(), 0o120000),
345                TreeValue::Tree(id) => (name, id.as_bytes(), 0o040000),
346                TreeValue::GitSubmodule(id) => (name, id.as_bytes(), 0o160000),
347                TreeValue::Conflict(id) => (
348                    entry.name().string() + CONFLICT_SUFFIX,
349                    id.as_bytes(),
350                    0o100644,
351                ),
352            };
353            builder
354                .insert(name, Oid::from_bytes(id).unwrap(), filemode)
355                .unwrap();
356        }
357        let oid = builder.write().map_err(|err| BackendError::WriteObject {
358            object_type: "tree",
359            source: Box::new(err),
360        })?;
361        Ok(TreeId::from_bytes(oid.as_bytes()))
362    }
363
364    fn read_conflict(&self, _path: &RepoPath, id: &ConflictId) -> BackendResult<Conflict> {
365        let mut file = self.read_file(
366            &RepoPath::from_internal_string("unused"),
367            &FileId::new(id.to_bytes()),
368        )?;
369        let mut data = String::new();
370        file.read_to_string(&mut data)?;
371        let json: serde_json::Value = serde_json::from_str(&data).unwrap();
372        Ok(Conflict {
373            removes: conflict_part_list_from_json(json.get("removes").unwrap()),
374            adds: conflict_part_list_from_json(json.get("adds").unwrap()),
375        })
376    }
377
378    fn write_conflict(&self, _path: &RepoPath, conflict: &Conflict) -> BackendResult<ConflictId> {
379        let json = serde_json::json!({
380            "removes": conflict_part_list_to_json(&conflict.removes),
381            "adds": conflict_part_list_to_json(&conflict.adds),
382        });
383        let json_string = json.to_string();
384        let bytes = json_string.as_bytes();
385        let locked_repo = self.repo.lock().unwrap();
386        let oid = locked_repo
387            .blob(bytes)
388            .map_err(|err| BackendError::WriteObject {
389                object_type: "conflict",
390                source: Box::new(err),
391            })?;
392        Ok(ConflictId::from_bytes(oid.as_bytes()))
393    }
394
395    fn read_commit(&self, id: &CommitId) -> BackendResult<Commit> {
396        if *id == self.root_commit_id {
397            return Ok(make_root_commit(
398                self.root_change_id().clone(),
399                self.empty_tree_id.clone(),
400            ));
401        }
402        let git_commit_id = validate_git_object_id(id)?;
403
404        let locked_repo = self.repo.lock().unwrap();
405        let commit = locked_repo
406            .find_commit(git_commit_id)
407            .map_err(|err| map_not_found_err(err, id))?;
408        // We reverse the bits of the commit id to create the change id. We don't want
409        // to use the first bytes unmodified because then it would be ambiguous
410        // if a given hash prefix refers to the commit id or the change id. It
411        // would have been enough to pick the last 16 bytes instead of the
412        // leading 16 bytes to address that. We also reverse the bits to make it less
413        // likely that users depend on any relationship between the two ids.
414        let change_id = ChangeId::new(
415            id.as_bytes()[4..HASH_LENGTH]
416                .iter()
417                .rev()
418                .map(|b| b.reverse_bits())
419                .collect(),
420        );
421        let mut parents = commit
422            .parent_ids()
423            .map(|oid| CommitId::from_bytes(oid.as_bytes()))
424            .collect_vec();
425        if parents.is_empty() {
426            parents.push(self.root_commit_id.clone());
427        };
428        let tree_id = TreeId::from_bytes(commit.tree_id().as_bytes());
429        let description = commit.message().unwrap_or("<no message>").to_owned();
430        let author = signature_from_git(commit.author());
431        let committer = signature_from_git(commit.committer());
432
433        let mut commit = Commit {
434            parents,
435            predecessors: vec![],
436            root_tree: tree_id,
437            change_id,
438            description,
439            author,
440            committer,
441        };
442
443        let table = {
444            let mut locked_head = self.cached_extra_metadata.lock().unwrap();
445            match locked_head.as_ref() {
446                Some(head) => Ok(head.clone()),
447                None => self.extra_metadata_store.get_head().map(|x| {
448                    *locked_head = Some(x.clone());
449                    x
450                }),
451            }
452        }
453        .map_err(|err| BackendError::Other(format!("Failed to read non-git metadata: {err}")))?;
454        let maybe_extras = table.get_value(git_commit_id.as_bytes());
455        if let Some(extras) = maybe_extras {
456            deserialize_extras(&mut commit, extras);
457        }
458
459        Ok(commit)
460    }
461
462    fn write_commit(&self, contents: &Commit) -> BackendResult<CommitId> {
463        let locked_repo = self.repo.lock().unwrap();
464        let git_tree_id = validate_git_object_id(&contents.root_tree)?;
465        let git_tree = locked_repo
466            .find_tree(git_tree_id)
467            .map_err(|err| map_not_found_err(err, &contents.root_tree))?;
468        let author = signature_to_git(&contents.author);
469        let mut committer = signature_to_git(&contents.committer);
470        let message = &contents.description;
471        if contents.parents.is_empty() {
472            return Err(BackendError::Other(
473                "Cannot write a commit with no parents".to_string(),
474            ));
475        }
476        let mut parents = vec![];
477        for parent_id in &contents.parents {
478            if *parent_id == self.root_commit_id {
479                // Git doesn't have a root commit, so if the parent is the root commit, we don't
480                // add it to the list of parents to write in the Git commit. We also check that
481                // there are no other parents since Git cannot represent a merge between a root
482                // commit and another commit.
483                if contents.parents.len() > 1 {
484                    return Err(BackendError::Other(
485                        "The Git backend does not support creating merge commits with the root \
486                         commit as one of the parents."
487                            .to_string(),
488                    ));
489                }
490            } else {
491                let git_commit_id = validate_git_object_id(parent_id)?;
492                let parent_git_commit = locked_repo
493                    .find_commit(git_commit_id)
494                    .map_err(|err| map_not_found_err(err, parent_id))?;
495                parents.push(parent_git_commit);
496            }
497        }
498        let parent_refs = parents.iter().collect_vec();
499        let extras = serialize_extras(contents);
500        let mut mut_table = self
501            .extra_metadata_store
502            .get_head()
503            .unwrap()
504            .start_mutation();
505        let id = loop {
506            let git_id = locked_repo
507                .commit(
508                    Some(&create_no_gc_ref()),
509                    &author,
510                    &committer,
511                    message,
512                    &git_tree,
513                    &parent_refs,
514                )
515                .map_err(|err| BackendError::WriteObject {
516                    object_type: "commit",
517                    source: Box::new(err),
518                })?;
519            let id = CommitId::from_bytes(git_id.as_bytes());
520            match mut_table.get_value(id.as_bytes()) {
521                Some(existing_extras) if existing_extras != extras => {
522                    // It's possible a commit already exists with the same commit id but different
523                    // change id. Adjust the timestamp until this is no longer the case.
524                    let new_when = git2::Time::new(
525                        committer.when().seconds() - 1,
526                        committer.when().offset_minutes(),
527                    );
528                    committer = git2::Signature::new(
529                        committer.name().unwrap(),
530                        committer.email().unwrap(),
531                        &new_when,
532                    )
533                    .unwrap();
534                }
535                _ => {
536                    break id;
537                }
538            }
539        };
540        mut_table.add_entry(id.to_bytes(), extras);
541        self.extra_metadata_store
542            .save_table(mut_table)
543            .map_err(|err| {
544                BackendError::Other(format!("Failed to write non-git metadata: {err}"))
545            })?;
546        *self.cached_extra_metadata.lock().unwrap() = None;
547        Ok(id)
548    }
549}
550
551fn conflict_part_list_to_json(parts: &[ConflictPart]) -> serde_json::Value {
552    serde_json::Value::Array(parts.iter().map(conflict_part_to_json).collect())
553}
554
555fn conflict_part_list_from_json(json: &serde_json::Value) -> Vec<ConflictPart> {
556    json.as_array()
557        .unwrap()
558        .iter()
559        .map(conflict_part_from_json)
560        .collect()
561}
562
563fn conflict_part_to_json(part: &ConflictPart) -> serde_json::Value {
564    serde_json::json!({
565        "value": tree_value_to_json(&part.value),
566    })
567}
568
569fn conflict_part_from_json(json: &serde_json::Value) -> ConflictPart {
570    let json_value = json.get("value").unwrap();
571    ConflictPart {
572        value: tree_value_from_json(json_value),
573    }
574}
575
576fn tree_value_to_json(value: &TreeValue) -> serde_json::Value {
577    match value {
578        TreeValue::File { id, executable } => serde_json::json!({
579             "file": {
580                 "id": id.hex(),
581                 "executable": executable,
582             },
583        }),
584        TreeValue::Symlink(id) => serde_json::json!({
585             "symlink_id": id.hex(),
586        }),
587        TreeValue::Tree(id) => serde_json::json!({
588             "tree_id": id.hex(),
589        }),
590        TreeValue::GitSubmodule(id) => serde_json::json!({
591             "submodule_id": id.hex(),
592        }),
593        TreeValue::Conflict(id) => serde_json::json!({
594             "conflict_id": id.hex(),
595        }),
596    }
597}
598
599fn tree_value_from_json(json: &serde_json::Value) -> TreeValue {
600    if let Some(json_file) = json.get("file") {
601        TreeValue::File {
602            id: FileId::new(bytes_vec_from_json(json_file.get("id").unwrap())),
603            executable: json_file.get("executable").unwrap().as_bool().unwrap(),
604        }
605    } else if let Some(json_id) = json.get("symlink_id") {
606        TreeValue::Symlink(SymlinkId::new(bytes_vec_from_json(json_id)))
607    } else if let Some(json_id) = json.get("tree_id") {
608        TreeValue::Tree(TreeId::new(bytes_vec_from_json(json_id)))
609    } else if let Some(json_id) = json.get("submodule_id") {
610        TreeValue::GitSubmodule(CommitId::new(bytes_vec_from_json(json_id)))
611    } else if let Some(json_id) = json.get("conflict_id") {
612        TreeValue::Conflict(ConflictId::new(bytes_vec_from_json(json_id)))
613    } else {
614        panic!("unexpected json value in conflict: {json:#?}");
615    }
616}
617
618fn bytes_vec_from_json(value: &serde_json::Value) -> Vec<u8> {
619    hex::decode(value.as_str().unwrap()).unwrap()
620}
621
622#[cfg(test)]
623mod tests {
624    use assert_matches::assert_matches;
625
626    use super::*;
627    use crate::backend::{FileId, MillisSinceEpoch};
628
629    #[test]
630    fn read_plain_git_commit() {
631        let temp_dir = testutils::new_temp_dir();
632        let store_path = temp_dir.path();
633        let git_repo_path = temp_dir.path().join("git");
634        let git_repo = git2::Repository::init(&git_repo_path).unwrap();
635
636        // Add a commit with some files in
637        let blob1 = git_repo.blob(b"content1").unwrap();
638        let blob2 = git_repo.blob(b"normal").unwrap();
639        let mut dir_tree_builder = git_repo.treebuilder(None).unwrap();
640        dir_tree_builder.insert("normal", blob1, 0o100644).unwrap();
641        dir_tree_builder.insert("symlink", blob2, 0o120000).unwrap();
642        let dir_tree_id = dir_tree_builder.write().unwrap();
643        let mut root_tree_builder = git_repo.treebuilder(None).unwrap();
644        root_tree_builder
645            .insert("dir", dir_tree_id, 0o040000)
646            .unwrap();
647        let root_tree_id = root_tree_builder.write().unwrap();
648        let git_author = git2::Signature::new(
649            "git author",
650            "git.author@example.com",
651            &git2::Time::new(1000, 60),
652        )
653        .unwrap();
654        let git_committer = git2::Signature::new(
655            "git committer",
656            "git.committer@example.com",
657            &git2::Time::new(2000, -480),
658        )
659        .unwrap();
660        let git_tree = git_repo.find_tree(root_tree_id).unwrap();
661        let git_commit_id = git_repo
662            .commit(
663                None,
664                &git_author,
665                &git_committer,
666                "git commit message",
667                &git_tree,
668                &[],
669            )
670            .unwrap();
671        let commit_id = CommitId::from_hex("efdcea5ca4b3658149f899ca7feee6876d077263");
672        // The change id is the leading reverse bits of the commit id
673        let change_id = ChangeId::from_hex("c64ee0b6e16777fe53991f9281a6cd25");
674        // Check that the git commit above got the hash we expect
675        assert_eq!(git_commit_id.as_bytes(), commit_id.as_bytes());
676
677        let store = GitBackend::init_external(store_path, &git_repo_path);
678        let commit = store.read_commit(&commit_id).unwrap();
679        assert_eq!(&commit.change_id, &change_id);
680        assert_eq!(commit.parents, vec![CommitId::from_bytes(&[0; 20])]);
681        assert_eq!(commit.predecessors, vec![]);
682        assert_eq!(commit.root_tree.as_bytes(), root_tree_id.as_bytes());
683        assert_eq!(commit.description, "git commit message");
684        assert_eq!(commit.author.name, "git author");
685        assert_eq!(commit.author.email, "git.author@example.com");
686        assert_eq!(
687            commit.author.timestamp.timestamp,
688            MillisSinceEpoch(1000 * 1000)
689        );
690        assert_eq!(commit.author.timestamp.tz_offset, 60);
691        assert_eq!(commit.committer.name, "git committer");
692        assert_eq!(commit.committer.email, "git.committer@example.com");
693        assert_eq!(
694            commit.committer.timestamp.timestamp,
695            MillisSinceEpoch(2000 * 1000)
696        );
697        assert_eq!(commit.committer.timestamp.tz_offset, -480);
698
699        let root_tree = store
700            .read_tree(
701                &RepoPath::root(),
702                &TreeId::from_bytes(root_tree_id.as_bytes()),
703            )
704            .unwrap();
705        let mut root_entries = root_tree.entries();
706        let dir = root_entries.next().unwrap();
707        assert_eq!(root_entries.next(), None);
708        assert_eq!(dir.name().as_str(), "dir");
709        assert_eq!(
710            dir.value(),
711            &TreeValue::Tree(TreeId::from_bytes(dir_tree_id.as_bytes()))
712        );
713
714        let dir_tree = store
715            .read_tree(
716                &RepoPath::from_internal_string("dir"),
717                &TreeId::from_bytes(dir_tree_id.as_bytes()),
718            )
719            .unwrap();
720        let mut entries = dir_tree.entries();
721        let file = entries.next().unwrap();
722        let symlink = entries.next().unwrap();
723        assert_eq!(entries.next(), None);
724        assert_eq!(file.name().as_str(), "normal");
725        assert_eq!(
726            file.value(),
727            &TreeValue::File {
728                id: FileId::from_bytes(blob1.as_bytes()),
729                executable: false
730            }
731        );
732        assert_eq!(symlink.name().as_str(), "symlink");
733        assert_eq!(
734            symlink.value(),
735            &TreeValue::Symlink(SymlinkId::from_bytes(blob2.as_bytes()))
736        );
737    }
738
739    /// Test that parents get written correctly
740    #[test]
741    fn git_commit_parents() {
742        let temp_dir = testutils::new_temp_dir();
743        let store_path = temp_dir.path();
744        let git_repo_path = temp_dir.path().join("git");
745        let git_repo = git2::Repository::init(&git_repo_path).unwrap();
746
747        let backend = GitBackend::init_external(store_path, &git_repo_path);
748        let mut commit = Commit {
749            parents: vec![],
750            predecessors: vec![],
751            root_tree: backend.empty_tree_id().clone(),
752            change_id: ChangeId::from_hex("abc123"),
753            description: "".to_string(),
754            author: create_signature(),
755            committer: create_signature(),
756        };
757
758        // No parents
759        commit.parents = vec![];
760        assert_matches!(
761            backend.write_commit(&commit),
762            Err(BackendError::Other(message)) if message.contains("no parents")
763        );
764
765        // Only root commit as parent
766        commit.parents = vec![backend.root_commit_id().clone()];
767        let first_id = backend.write_commit(&commit).unwrap();
768        let first_commit = backend.read_commit(&first_id).unwrap();
769        assert_eq!(first_commit, commit);
770        let first_git_commit = git_repo.find_commit(git_id(&first_id)).unwrap();
771        assert_eq!(first_git_commit.parent_ids().collect_vec(), vec![]);
772
773        // Only non-root commit as parent
774        commit.parents = vec![first_id.clone()];
775        let second_id = backend.write_commit(&commit).unwrap();
776        let second_commit = backend.read_commit(&second_id).unwrap();
777        assert_eq!(second_commit, commit);
778        let second_git_commit = git_repo.find_commit(git_id(&second_id)).unwrap();
779        assert_eq!(
780            second_git_commit.parent_ids().collect_vec(),
781            vec![git_id(&first_id)]
782        );
783
784        // Merge commit
785        commit.parents = vec![first_id.clone(), second_id.clone()];
786        let merge_id = backend.write_commit(&commit).unwrap();
787        let merge_commit = backend.read_commit(&merge_id).unwrap();
788        assert_eq!(merge_commit, commit);
789        let merge_git_commit = git_repo.find_commit(git_id(&merge_id)).unwrap();
790        assert_eq!(
791            merge_git_commit.parent_ids().collect_vec(),
792            vec![git_id(&first_id), git_id(&second_id)]
793        );
794
795        // Merge commit with root as one parent
796        commit.parents = vec![first_id, backend.root_commit_id().clone()];
797        assert_matches!(
798            backend.write_commit(&commit),
799            Err(BackendError::Other(message)) if message.contains("root commit")
800        );
801    }
802
803    #[test]
804    fn commit_has_ref() {
805        let temp_dir = testutils::new_temp_dir();
806        let store = GitBackend::init_internal(temp_dir.path());
807        let signature = Signature {
808            name: "Someone".to_string(),
809            email: "someone@example.com".to_string(),
810            timestamp: Timestamp {
811                timestamp: MillisSinceEpoch(0),
812                tz_offset: 0,
813            },
814        };
815        let commit = Commit {
816            parents: vec![store.root_commit_id().clone()],
817            predecessors: vec![],
818            root_tree: store.empty_tree_id().clone(),
819            change_id: ChangeId::new(vec![]),
820            description: "initial".to_string(),
821            author: signature.clone(),
822            committer: signature,
823        };
824        let commit_id = store.write_commit(&commit).unwrap();
825        let git_refs = store
826            .git_repo()
827            .unwrap()
828            .references_glob("refs/jj/keep/*")
829            .unwrap()
830            .map(|git_ref| git_ref.unwrap().target().unwrap())
831            .collect_vec();
832        assert_eq!(git_refs, vec![git_id(&commit_id)]);
833    }
834
835    #[test]
836    fn overlapping_git_commit_id() {
837        let temp_dir = testutils::new_temp_dir();
838        let store = GitBackend::init_internal(temp_dir.path());
839        let commit1 = Commit {
840            parents: vec![store.root_commit_id().clone()],
841            predecessors: vec![],
842            root_tree: store.empty_tree_id().clone(),
843            change_id: ChangeId::new(vec![]),
844            description: "initial".to_string(),
845            author: create_signature(),
846            committer: create_signature(),
847        };
848        let commit_id1 = store.write_commit(&commit1).unwrap();
849        let mut commit2 = commit1;
850        commit2.predecessors.push(commit_id1.clone());
851        // `write_commit` should prevent the ids from being the same by changing the
852        // committer timestamp of the commit it actually writes.
853        assert_ne!(store.write_commit(&commit2).unwrap(), commit_id1);
854    }
855
856    fn git_id(commit_id: &CommitId) -> Oid {
857        Oid::from_bytes(commit_id.as_bytes()).unwrap()
858    }
859
860    fn create_signature() -> Signature {
861        Signature {
862            name: "Someone".to_string(),
863            email: "someone@example.com".to_string(),
864            timestamp: Timestamp {
865                timestamp: MillisSinceEpoch(0),
866                tz_offset: 0,
867            },
868        }
869    }
870}