Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

git_backend: Support shallow git repositories #4448

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

* CommitId / ChangeId template types now support `.normal_hex()`.

* Initial support for shallow git repositories has been implemented. However
deepening the history of a shallow repository is not yet supported.

* `jj git clone` now accepts a `--depth <DEPTH>` option, which
allows to clone the repository with a given depth.

### Fixed bugs

* Fixed panic when parsing invalid conflict markers of a particular form.
Expand Down
9 changes: 3 additions & 6 deletions cli/src/command_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -448,12 +448,9 @@ impl From<GitImportError> for CommandError {
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."
Is this Git repository a partial clone (cloned with the --filter argument)?
jj currently does not support partial clones. To use jj with this repository, try re-cloning with \
the full repository contents."
.to_string(),
),
GitImportError::RemoteReservedForLocalGitRepo => {
Expand Down
7 changes: 7 additions & 0 deletions cli/src/commands/git/clone.rs
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add CLI tests of jj git clone --depth?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like local clones do not support shallow clones in git2, yielding

shallow fetch is not supported by the local transport

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, it wouldn't be easy to write tests, then. (we could test that non-zero --depth is passed to libgit2, though.)

Thanks for checking.

Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use std::fs;
use std::io;
use std::io::Write;
use std::num::NonZeroU32;
use std::path::Path;
use std::path::PathBuf;

Expand Down Expand Up @@ -60,6 +61,9 @@ pub struct GitCloneArgs {
/// Whether or not to colocate the Jujutsu repo with the git repo
#[arg(long)]
colocate: bool,
/// Create a shallow clone of the given depth
#[arg(long)]
depth: Option<NonZeroU32>,
}

fn absolute_git_source(cwd: &Path, source: &str) -> String {
Expand Down Expand Up @@ -132,6 +136,7 @@ pub fn cmd_git_clone(
ui,
command,
args.colocate,
args.depth,
remote_name,
&source,
&canonical_wc_path,
Expand Down Expand Up @@ -195,6 +200,7 @@ fn do_git_clone(
ui: &mut Ui,
command: &CommandHelper,
colocate: bool,
depth: Option<NonZeroU32>,
remote_name: &str,
source: &str,
wc_path: &Path,
Expand Down Expand Up @@ -223,6 +229,7 @@ fn do_git_clone(
&[StringPattern::everything()],
cb,
&command.settings().git_settings(),
depth,
)
})
.map_err(|err| match err {
Expand Down
1 change: 1 addition & 0 deletions cli/src/commands/git/fetch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ pub fn cmd_git_fetch(
&args.branch,
cb,
&command.settings().git_settings(),
None,
)
})
.map_err(|err| match err {
Expand Down
1 change: 1 addition & 0 deletions cli/tests/[email protected]
Original file line number Diff line number Diff line change
Expand Up @@ -968,6 +968,7 @@ The Git repo will be a bare git repo stored inside the `.jj/` directory.

Default value: `origin`
* `--colocate` — Whether or not to colocate the Jujutsu repo with the git repo
* `--depth <DEPTH>` — Create a shallow clone of the given depth



Expand Down
20 changes: 20 additions & 0 deletions cli/tests/test_git_clone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,26 @@ fn test_git_clone_with_remote_name() {
"#);
}

#[test]
fn test_git_clone_with_depth() {
let test_env = TestEnvironment::default();
test_env.add_config("git.auto-local-branch = true");
let git_repo_path = test_env.env_root().join("source");
let git_repo = git2::Repository::init(git_repo_path).unwrap();
set_up_non_empty_git_repo(&git_repo);

// local transport does not support shallow clones so we just test that the
// depth arg is passed on here
let stderr = test_env.jj_cmd_failure(
test_env.env_root(),
&["git", "clone", "--depth", "1", "source", "clone"],
);
insta::assert_snapshot!(stderr, @r#"
Fetching into new repo in "$TEST_ENV/clone"
Error: shallow fetch is not supported by the local transport; class=Net (12)
"#);
}

fn get_bookmark_output(test_env: &TestEnvironment, repo_path: &Path) -> String {
test_env.jj_cmd_success(repo_path, &["bookmark", "list", "--all-remotes"])
}
5 changes: 3 additions & 2 deletions docs/git-compatibility.md
Veykril marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,9 @@ 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: Kind of.** Shallow commits all have the virtual root commit as
their parent. However, deepening or fully unshallowing a repository is currently not yet
supported and will cause issues.
* **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
Expand Down
5 changes: 5 additions & 0 deletions lib/src/git.rs
Veykril marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: squash into previous.

Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use std::collections::HashSet;
use std::default::Default;
use std::fmt;
use std::io::Read;
use std::num::NonZeroU32;
use std::path::PathBuf;
use std::str;

Expand Down Expand Up @@ -1244,6 +1245,7 @@ pub fn fetch(
branch_names: &[StringPattern],
callbacks: RemoteCallbacks<'_>,
git_settings: &GitSettings,
depth: Option<NonZeroU32>,
) -> Result<GitFetchStats, GitFetchError> {
// Perform a `git fetch` on the local git repo, updating the remote-tracking
// branches in the git repo.
Expand All @@ -1260,6 +1262,9 @@ pub fn fetch(
fetch_options.proxy_options(proxy_options);
let callbacks = callbacks.into_git();
fetch_options.remote_callbacks(callbacks);
if let Some(depth) = depth {
fetch_options.depth(depth.get().try_into().unwrap_or(i32::MAX));
}
// At this point, we are only updating Git's remote tracking branches, not the
// local branches.
let refspecs: Vec<_> = branch_names
Expand Down
38 changes: 31 additions & 7 deletions lib/src/git_backend.rs
Veykril marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -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<Commit> {
let commit = git_object
.try_to_commit_ref()
Expand All @@ -537,10 +538,17 @@ 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
// TODO: This causes issues when a shallow repository is deepened/unshallowed
let parents = if is_shallow {
vec![]
} else {
Veykril marked this conversation as resolved.
Show resolved Hide resolved
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.
Expand Down Expand Up @@ -859,6 +867,10 @@ fn import_extra_metadata_entries_from_heads(
head_ids: &HashSet<&CommitId>,
uses_tree_conflict_format: bool,
) -> BackendResult<()> {
let shallow_commits = git_repo
.shallow_commits()
.map_err(|e| BackendError::Other(Box::new(e)))?;

let mut work_ids = head_ids
.iter()
.filter(|&id| mut_table.get_value(id.as_bytes()).is_none())
Expand All @@ -868,11 +880,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
Expand Down Expand Up @@ -1142,7 +1161,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());
Expand Down
Loading