ok/jj
1
0
Fork 0
forked from mirrors/jj

git: update own branch and tag records based on git refs

Now that we have our own representation of branches and tags, let's
update them when we import git refs. The View object's git refs are
now just a record of what the refs are in the underlying git ref last
time we imported them (we don't -- and won't -- provide a way for the
user to update our record of the git refs). We can therefore do a nice
3-way ref-merge using the `refs` module we added recently. That means
that we'll detect conflicts caused by changes made concurrently in the
underlying git repo and in jj's view.
This commit is contained in:
Martin von Zweigbergk 2021-07-15 01:31:48 -07:00
parent 044f23bc33
commit 8738421990
2 changed files with 158 additions and 26 deletions

View file

@ -12,13 +12,15 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use itertools::Itertools;
use std::collections::BTreeMap;
use thiserror::Error;
use crate::commit::Commit;
use crate::op_store::RefTarget;
use crate::repo::MutableRepo;
use crate::store::CommitId;
use crate::view::RefName;
#[derive(Error, Debug, PartialEq)]
pub enum GitImportError {
@ -26,6 +28,23 @@ pub enum GitImportError {
InternalGitError(#[from] git2::Error),
}
fn parse_git_ref(ref_name: &str) -> Option<RefName> {
if let Some(branch_name) = ref_name.strip_prefix("refs/heads/") {
Some(RefName::LocalBranch(branch_name.to_string()))
} else if let Some(remote_and_branch) = ref_name.strip_prefix("refs/remotes/") {
remote_and_branch
.split_once("/")
.map(|(remote, branch)| RefName::RemoteBranch {
remote: remote.to_string(),
branch: branch.to_string(),
})
} else {
ref_name
.strip_prefix("refs/tags/")
.map(|tag_name| RefName::Tag(tag_name.to_string()))
}
}
// Reflect changes made in the underlying Git repo in the Jujutsu repo.
pub fn import_refs(
mut_repo: &mut MutableRepo,
@ -33,16 +52,8 @@ pub fn import_refs(
) -> Result<(), GitImportError> {
let store = mut_repo.store().clone();
let git_refs = git_repo.references()?;
let existing_git_refs = mut_repo.view().git_refs().keys().cloned().collect_vec();
// TODO: Store the id of the previous import and read it back here, so we can
// merge the views instead of overwriting.
for existing_git_ref in existing_git_refs {
mut_repo.remove_git_ref(&existing_git_ref);
// TODO: We should probably also remove heads pointing to the same
// commits and commits no longer reachable from other refs.
// If the underlying git repo has a branch that gets rewritten, we
// should probably not keep the commits it used to point to.
}
let mut existing_git_refs = mut_repo.view().git_refs().clone();
let mut changed_git_refs = BTreeMap::new();
for git_ref in git_refs {
let git_ref = git_ref?;
if !(git_ref.is_tag() || git_ref.is_branch() || git_ref.is_remote())
@ -62,12 +73,41 @@ pub fn import_refs(
let id = CommitId(git_commit.id().as_bytes().to_vec());
let commit = store.get_commit(&id).unwrap();
mut_repo.add_head(&commit);
mut_repo.set_git_ref(git_ref.name().unwrap().to_string(), RefTarget::Normal(id));
// For now, we consider all remotes "publishing".
// TODO: Make it configurable which remotes are publishing.
if git_ref.is_remote() {
mut_repo.add_public_head(&commit);
}
let full_name = git_ref.name().unwrap().to_string();
mut_repo.set_git_ref(full_name.clone(), RefTarget::Normal(id.clone()));
let old_target = existing_git_refs.remove(&full_name);
let new_target = Some(RefTarget::Normal(id));
if new_target != old_target {
changed_git_refs.insert(full_name, (old_target, new_target));
}
}
for (full_name, target) in existing_git_refs {
mut_repo.remove_git_ref(&full_name);
// TODO: We should probably also remove heads pointing to the same
// commits and commits no longer reachable from other refs.
// If the underlying git repo has a branch that gets rewritten, we
// should probably not keep the commits it used to point to.
changed_git_refs.insert(full_name, (Some(target), None));
}
for (full_name, (old_git_target, new_git_target)) in changed_git_refs {
if let Some(ref_name) = parse_git_ref(&full_name) {
// Apply the change that happened in git since last time we imported refs
mut_repo.merge_single_ref(&ref_name, old_git_target.as_ref(), new_git_target.as_ref());
// If a git remote-tracking branch changed, apply the change to the local branch
// as well
if let RefName::RemoteBranch { branch, remote: _ } = ref_name {
mut_repo.merge_single_ref(
&RefName::LocalBranch(branch),
old_git_target.as_ref(),
new_git_target.as_ref(),
);
}
}
}
Ok(())
}

View file

@ -18,12 +18,13 @@ use std::sync::Arc;
use git2::Oid;
use jujutsu_lib::commit::Commit;
use jujutsu_lib::git::{GitFetchError, GitPushError};
use jujutsu_lib::op_store::RefTarget;
use jujutsu_lib::op_store::{BranchTarget, RefTarget};
use jujutsu_lib::repo::ReadonlyRepo;
use jujutsu_lib::settings::UserSettings;
use jujutsu_lib::store::CommitId;
use jujutsu_lib::testutils::create_random_commit;
use jujutsu_lib::{git, testutils};
use maplit::hashset;
use maplit::{btreemap, hashset};
use tempfile::TempDir;
fn empty_git_commit<'r>(
@ -58,10 +59,11 @@ fn test_import_refs() {
let git_repo = repo.store().git_repo().unwrap();
let commit1 = empty_git_commit(&git_repo, "refs/heads/main", &[]);
git_ref(&git_repo, "refs/remotes/origin/main", commit1.id());
let commit2 = empty_git_commit(&git_repo, "refs/heads/main", &[&commit1]);
let commit3 = empty_git_commit(&git_repo, "refs/heads/feature1", &[&commit2]);
let commit4 = empty_git_commit(&git_repo, "refs/heads/feature2", &[&commit2]);
let commit5 = empty_git_commit(&git_repo, "refs/remotes/origin/main", &[&commit2]);
let commit5 = empty_git_commit(&git_repo, "refs/tags/v1.0", &[&commit1]);
// Should not be imported
empty_git_commit(&git_repo, "refs/notes/x", &[&commit2]);
@ -70,15 +72,49 @@ fn test_import_refs() {
jujutsu_lib::git::import_refs(tx.mut_repo(), &git_repo).unwrap();
let repo = tx.commit();
let view = repo.view();
let expected_heads = hashset! {
view.checkout().clone(),
commit_id(&commit3),
commit_id(&commit4),
commit_id(&commit5)
view.checkout().clone(),
commit_id(&commit3),
commit_id(&commit4),
commit_id(&commit5)
};
assert_eq!(*view.heads(), expected_heads);
assert_eq!(*view.public_heads(), hashset!(commit_id(&commit5)));
assert_eq!(view.git_refs().len(), 4);
assert_eq!(*view.public_heads(), hashset!(commit_id(&commit1)));
let expected_main_branch = BranchTarget {
local_target: Some(RefTarget::Normal(commit_id(&commit2))),
remote_targets: btreemap! {
"origin".to_string() => RefTarget::Normal(commit_id(&commit1)),
},
};
assert_eq!(
view.branches().get("main"),
Some(expected_main_branch).as_ref()
);
let expected_feature1_branch = BranchTarget {
local_target: Some(RefTarget::Normal(commit_id(&commit3))),
remote_targets: btreemap! {},
};
assert_eq!(
view.branches().get("feature1"),
Some(expected_feature1_branch).as_ref()
);
let expected_feature2_branch = BranchTarget {
local_target: Some(RefTarget::Normal(commit_id(&commit4))),
remote_targets: btreemap! {},
};
assert_eq!(
view.branches().get("feature2"),
Some(expected_feature2_branch).as_ref()
);
assert_eq!(
view.tags().get("v1.0"),
Some(RefTarget::Normal(commit_id(&commit5))).as_ref()
);
assert_eq!(view.git_refs().len(), 5);
assert_eq!(
view.git_refs().get("refs/heads/main"),
Some(RefTarget::Normal(commit_id(&commit2))).as_ref()
@ -93,6 +129,10 @@ fn test_import_refs() {
);
assert_eq!(
view.git_refs().get("refs/remotes/origin/main"),
Some(RefTarget::Normal(commit_id(&commit1))).as_ref()
);
assert_eq!(
view.git_refs().get("refs/tags/v1.0"),
Some(RefTarget::Normal(commit_id(&commit5))).as_ref()
);
}
@ -104,6 +144,7 @@ fn test_import_refs_reimport() {
let git_repo = repo.store().git_repo().unwrap();
let commit1 = empty_git_commit(&git_repo, "refs/heads/main", &[]);
git_ref(&git_repo, "refs/remotes/origin/main", commit1.id());
let commit2 = empty_git_commit(&git_repo, "refs/heads/main", &[&commit1]);
let commit3 = empty_git_commit(&git_repo, "refs/heads/feature1", &[&commit2]);
let commit4 = empty_git_commit(&git_repo, "refs/heads/feature2", &[&commit2]);
@ -121,6 +162,17 @@ fn test_import_refs_reimport() {
delete_git_ref(&git_repo, "refs/heads/feature2");
let commit5 = empty_git_commit(&git_repo, "refs/heads/feature2", &[&commit2]);
// Also modify feature2 on the jj side
let mut tx = repo.start_transaction("test");
let commit6 = create_random_commit(&settings, &repo)
.set_parents(vec![commit_id(&commit2)])
.write_to_repo(tx.mut_repo());
tx.mut_repo().set_local_branch(
"feature2".to_string(),
RefTarget::Normal(commit6.id().clone()),
);
let repo = tx.commit();
let mut tx = repo.start_transaction("test");
jujutsu_lib::git::import_refs(tx.mut_repo(), &git_repo).unwrap();
let repo = tx.commit();
@ -131,18 +183,56 @@ fn test_import_refs_reimport() {
view.checkout().clone(),
commit_id(&commit3),
commit_id(&commit4),
commit_id(&commit5)
commit_id(&commit5),
commit6.id().clone(),
};
assert_eq!(*view.heads(), expected_heads);
assert_eq!(view.git_refs().len(), 2);
assert_eq!(view.branches().len(), 2);
let commit1_target = RefTarget::Normal(commit_id(&commit1));
let commit2_target = RefTarget::Normal(commit_id(&commit2));
let expected_main_branch = BranchTarget {
local_target: Some(RefTarget::Normal(commit_id(&commit2))),
remote_targets: btreemap! {
"origin".to_string() => commit1_target.clone(),
},
};
assert_eq!(
view.branches().get("main"),
Some(expected_main_branch).as_ref()
);
let expected_feature2_branch = BranchTarget {
local_target: Some(RefTarget::Conflict {
removes: vec![commit_id(&commit4)],
adds: vec![commit6.id().clone(), commit_id(&commit5)],
}),
remote_targets: btreemap! {},
};
assert_eq!(
view.branches().get("feature2"),
Some(expected_feature2_branch).as_ref()
);
assert!(view.tags().is_empty());
assert_eq!(view.git_refs().len(), 3);
assert_eq!(
view.git_refs().get("refs/heads/main"),
Some(RefTarget::Normal(commit_id(&commit2))).as_ref()
Some(commit2_target).as_ref()
);
assert_eq!(
view.git_refs().get("refs/heads/feature2"),
Some(RefTarget::Normal(commit_id(&commit5))).as_ref()
view.git_refs().get("refs/remotes/origin/main"),
Some(commit1_target).as_ref()
);
let commit5_target = RefTarget::Normal(commit_id(&commit5));
assert_eq!(
view.git_refs().get("refs/heads/feature2"),
Some(commit5_target).as_ref()
);
}
fn git_ref(git_repo: &git2::Repository, name: &str, target: Oid) {
git_repo.reference(name, target, true, "").unwrap();
}
fn delete_git_ref(git_repo: &git2::Repository, name: &str) {
@ -165,6 +255,8 @@ fn test_import_refs_empty_git_repo() {
jujutsu_lib::git::import_refs(tx.mut_repo(), &git_repo).unwrap();
let repo = tx.commit();
assert_eq!(*repo.view().heads(), heads_before);
assert_eq!(repo.view().branches().len(), 0);
assert_eq!(repo.view().tags().len(), 0);
assert_eq!(repo.view().git_refs().len(), 0);
}