From 9a7f7159ceb499b18930e403186159f68fffbbaa Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Thu, 12 Sep 2024 21:16:14 +0200 Subject: [PATCH] shallow: Support shallow git repositories --- cli/src/command_error.rs | 13 ++----------- docs/git-compatibility.md | 7 +++---- lib/src/git_backend.rs | 39 ++++++++++++++++++++++++++++++--------- 3 files changed, 35 insertions(+), 24 deletions(-) diff --git a/cli/src/command_error.rs b/cli/src/command_error.rs index 8993eb2f14..1e8847630c 100644 --- a/cli/src/command_error.rs +++ b/cli/src/command_error.rs @@ -438,20 +438,11 @@ impl From for CommandError { impl From for CommandError { fn from(err: GitImportError) -> Self { let hint = match &err { - GitImportError::MissingHeadTarget { .. } - | GitImportError::MissingRefAncestor { .. } => Some( - "\ -Is this Git repository a shallow or partial clone (cloned with the --depth or --filter \ - argument)? -jj currently does not support shallow/partial clones. To use jj with this \ - repository, try -unshallowing the repository (https://stackoverflow.com/q/6802145) or re-cloning with the full -repository contents." - .to_string(), - ), GitImportError::RemoteReservedForLocalGitRepo => { Some("Run `jj git remote rename` to give different name.".to_string()) } + GitImportError::MissingHeadTarget { .. } => None, + GitImportError::MissingRefAncestor { .. } => None, GitImportError::InternalBackend(_) => None, GitImportError::InternalGitError(_) => None, GitImportError::UnexpectedBackend => None, diff --git a/docs/git-compatibility.md b/docs/git-compatibility.md index 60954b801c..6689db7904 100644 --- a/docs/git-compatibility.md +++ b/docs/git-compatibility.md @@ -31,7 +31,7 @@ a comparison with Git, including how workflows are different, see the * **.gitignore: Yes.** Ignores in `.gitignore` files are supported. So are ignores in `.git/info/exclude` or configured via Git's `core.excludesfile` config. The `.gitignore` support uses a native implementation, so please - report a bug if you notice any difference compared to `git`. + report a bug if you notice any difference compared to `git`. * **.gitattributes: No.** There's [#53](https://github.com/martinvonz/jj/issues/53) about adding support for at least the `eol` attribute. * **Hooks: No.** There's [#405](https://github.com/martinvonz/jj/issues/405) @@ -45,7 +45,7 @@ a comparison with Git, including how workflows are different, see the * **Staging area: Kind of.** The staging area will be ignored. For example, `jj diff` will show a diff from the Git HEAD to the working copy. There are [ways of fulfilling your use cases without a staging - area](https://github.com/martinvonz/jj/blob/main/docs/git-comparison.md#the-index). + area](https://github.com/martinvonz/jj/blob/main/docs/git-comparison.md#the-index). * **Garbage collection: Yes.** It should be safe to run `git gc` in the Git repo, but it's not tested, so it's probably a good idea to make a backup of the whole workspace first. There's [no garbage collection and repacking of @@ -57,8 +57,7 @@ a comparison with Git, including how workflows are different, see the not be lost either. * **Partial clones: No.** We use the [libgit2](https://libgit2.org/) library, which [doesn't have support for partial clones](https://github.com/libgit2/libgit2/issues/5564). -* **Shallow clones: No.** We use the [libgit2](https://libgit2.org/) library, - which [doesn't have support for shallow clones](https://github.com/libgit2/libgit2/issues/3058). +* **Shallow clones: Yes.** Shallow commits all have the virtual root commit as their parent. * **git-worktree: No.** However, there's native support for multiple working copies backed by a single repo. See the `jj workspace` family of commands. * **Sparse checkouts: No.** However, there's native support for sparse diff --git a/lib/src/git_backend.rs b/lib/src/git_backend.rs index 7d2b267e99..7992d6e915 100644 --- a/lib/src/git_backend.rs +++ b/lib/src/git_backend.rs @@ -519,6 +519,7 @@ fn commit_from_git_without_root_parent( id: &CommitId, git_object: &gix::Object, uses_tree_conflict_format: bool, + is_shallow: bool, ) -> BackendResult { let commit = git_object .try_to_commit_ref() @@ -537,10 +538,16 @@ fn commit_from_git_without_root_parent( .map(|b| b.reverse_bits()) .collect(), ); - let parents = commit - .parents() - .map(|oid| CommitId::from_bytes(oid.as_bytes())) - .collect_vec(); + // shallow commits don't have parents their parents actually fetched, so we + // discard them here + let parents = if is_shallow { + vec![] + } else { + commit + .parents() + .map(|oid| CommitId::from_bytes(oid.as_bytes())) + .collect_vec() + }; let tree_id = TreeId::from_bytes(commit.tree().as_bytes()); // If this commit is a conflict, we'll update the root tree later, when we read // the extra metadata. @@ -859,6 +866,8 @@ fn import_extra_metadata_entries_from_heads( head_ids: &HashSet<&CommitId>, uses_tree_conflict_format: bool, ) -> BackendResult<()> { + let shallow_commits = git_repo.shallow_commits().ok().flatten(); + let mut work_ids = head_ids .iter() .filter(|&id| mut_table.get_value(id.as_bytes()).is_none()) @@ -868,11 +877,18 @@ fn import_extra_metadata_entries_from_heads( let git_object = git_repo .find_object(validate_git_object_id(&id)?) .map_err(|err| map_not_found_err(err, &id))?; + let is_shallow = shallow_commits + .as_ref() + .is_some_and(|shallow| shallow.contains(&git_object.id)); // TODO(#1624): Should we read the root tree here and check if it has a // `.jjconflict-...` entries? That could happen if the user used `git` to e.g. // change the description of a commit with tree-level conflicts. - let commit = - commit_from_git_without_root_parent(&id, &git_object, uses_tree_conflict_format)?; + let commit = commit_from_git_without_root_parent( + &id, + &git_object, + uses_tree_conflict_format, + is_shallow, + )?; mut_table.add_entry(id.to_bytes(), serialize_extras(&commit)); work_ids.extend( commit @@ -1142,7 +1158,12 @@ impl Backend for GitBackend { let git_object = locked_repo .find_object(git_commit_id) .map_err(|err| map_not_found_err(err, id))?; - commit_from_git_without_root_parent(id, &git_object, false)? + let is_shallow = locked_repo + .shallow_commits() + .ok() + .flatten() + .is_some_and(|shallow| shallow.contains(&git_object.id)); + commit_from_git_without_root_parent(id, &git_object, false, is_shallow)? }; if commit.parents.is_empty() { commit.parents.push(self.root_commit_id.clone()); @@ -2151,8 +2172,8 @@ mod tests { author Someone 0 +0000 committer Someone 0 +0000 gpgsig test sig - - + + hash=9ad9526c3b2103c41a229f2f3c82d107a0ecd902f476a855f0e1dd5f7bef1430663de12749b73e293a877113895a8a2a0f29da4bbc5a5f9a19c3523fb0e53518 initial