1use 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;
35pub 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
141fn 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 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 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 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 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 let change_id = ChangeId::from_hex("c64ee0b6e16777fe53991f9281a6cd25");
674 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]
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 commit.parents = vec![];
760 assert_matches!(
761 backend.write_commit(&commit),
762 Err(BackendError::Other(message)) if message.contains("no parents")
763 );
764
765 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 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 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 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 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}