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

git: on import_refs(), respect tracking state of existing remote refs

In this commit, new behavior is tested by using in-memory view data. Data
persistence and track/untrack commands will be implemented soon.
This commit is contained in:
Yuya Nishihara 2023-10-12 18:22:19 +09:00
parent a697175674
commit 4cd2518be0
6 changed files with 120 additions and 25 deletions

View file

@ -24,6 +24,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* `jj config set` now interprets the value as TOML also if it's a valid TOML
array or table. For example, `jj config set --user 'aliases.n' '["new"]'`
* Remote branches now have tracking or non-tracking flags. The
`git.auto-local-branch` setting is applied only to newly fetched remote
branches. Existing remote branches are migrated as follows:
* If local branch exists, the corresponding remote branches are considered
tracking branches.
* Otherwise, the remote branches are non-tracking branches.
See [automatic local branch creation](docs/config.md#automatic-local-branch-creation)
for details.
### New features
* `jj workspace add` now takes a `--revision` argument.

View file

@ -995,6 +995,7 @@ fn cmd_git_push(
),
_ => user_error(err.to_string()),
})?;
// TODO: mark pushed remote branches as tracking
let stats = git::import_refs(tx.mut_repo(), &git_repo, &command.settings().git_settings())?;
print_git_import_stats(ui, &stats)?;
tx.finish(ui)?;

View file

@ -493,7 +493,7 @@ conflict is considered fully resolved when there are no conflict markers left.
### Automatic local branch creation
By default, when `jj` imports a remote-tracking branch from Git, it also
By default, when `jj` imports a new remote-tracking branch from Git, it also
creates a local branch with the same name. In some repositories, this
may be undesirable, e.g.:
@ -502,10 +502,11 @@ may be undesirable, e.g.:
- There are multiple remotes with conflicting views of that branch,
resulting in an unhelpful conflicted state.
You can disable this behavior by setting `git.auto-local-branch` like
so,
You can disable this behavior by setting `git.auto-local-branch` like so,
git.auto-local-branch = false
```toml
git.auto-local-branch = false
```
Note that this setting may make it easier to accidentally delete remote
branches. Since the local branch isn't created, the remote branch will be

View file

@ -159,9 +159,9 @@ fn resolve_git_ref_to_commit_id(
struct RefsToImport {
/// Git ref `(full_name, new_target)`s to be copied to the view.
changed_git_refs: Vec<(String, RefTarget)>,
/// Remote `(ref_name, (old_target, new_target))`s to be merged in to the
/// local refs.
changed_remote_refs: BTreeMap<RefName, (RefTarget, RefTarget)>,
/// Remote `(ref_name, (old_remote_ref, new_target))`s to be merged in to
/// the local refs.
changed_remote_refs: BTreeMap<RefName, (RemoteRef, RefTarget)>,
}
/// Reflect changes made in the underlying Git repo in the Jujutsu repo.
@ -260,23 +260,27 @@ pub fn import_some_refs(
for (full_name, new_target) in changed_git_refs {
mut_repo.set_git_ref_target(&full_name, new_target);
}
for (ref_name, (old_target, new_target)) in &changed_remote_refs {
for (ref_name, (old_remote_ref, new_target)) in &changed_remote_refs {
let base_target = old_remote_ref.tracking_target();
let new_remote_ref = RemoteRef {
target: new_target.clone(),
// TODO: preserve the old state
state: default_remote_ref_state_for(ref_name, git_settings),
state: if old_remote_ref.is_present() {
old_remote_ref.state
} else {
default_remote_ref_state_for(ref_name, git_settings)
},
};
if let RefName::RemoteBranch { branch, remote } = ref_name {
if new_remote_ref.is_tracking() {
let local_ref_name = RefName::LocalBranch(branch.clone());
mut_repo.merge_single_ref(&local_ref_name, old_target, &new_remote_ref.target);
mut_repo.merge_single_ref(&local_ref_name, base_target, &new_remote_ref.target);
}
// Remote-tracking branch is the last known state of the branch in the remote.
// It shouldn't diverge even if we had inconsistent view.
mut_repo.set_remote_branch(branch, remote, new_remote_ref);
} else {
if new_remote_ref.is_tracking() {
mut_repo.merge_single_ref(ref_name, old_target, &new_remote_ref.target);
mut_repo.merge_single_ref(ref_name, base_target, &new_remote_ref.target);
}
if let RefName::LocalBranch(branch) = ref_name {
// Update Git-tracking branch like the other remote branches.
@ -289,7 +293,7 @@ pub fn import_some_refs(
// in jj as well.
let hidable_git_heads = changed_remote_refs
.values()
.flat_map(|(old_target, _)| old_target.added_ids())
.flat_map(|(old_remote_ref, _)| old_remote_ref.target.added_ids())
.cloned()
.collect_vec();
if hidable_git_heads.is_empty() {
@ -341,7 +345,8 @@ fn diff_refs_to_import(
git_ref_filter(&ref_name).then_some((full_name.as_ref(), target))
})
.collect();
let mut known_remote_refs: HashMap<RefName, &RefTarget> = itertools::chain(
// TODO: migrate tags to the remote view, and don't destructure &RemoteRef
let mut known_remote_refs: HashMap<RefName, (&RefTarget, RemoteRefState)> = itertools::chain(
view.all_remote_branches()
.map(|((branch, remote), remote_ref)| {
// TODO: want to abstract local ref as "git" tracking remote, but
@ -354,13 +359,14 @@ fn diff_refs_to_import(
remote: remote.to_owned(),
}
};
(ref_name, &remote_ref.target)
let RemoteRef { target, state } = remote_ref;
(ref_name, (target, *state))
}),
// TODO: compare to tags stored in the "git" remote view. Since tags should never
// be moved locally in jj, we can consider local tags as merge base.
view.tags().iter().map(|(name, target)| {
let ref_name = RefName::Tag(name.to_owned());
(ref_name, target)
(ref_name, (target, RemoteRefState::Tracking))
}),
)
.filter(|(ref_name, _)| git_ref_filter(ref_name))
@ -395,16 +401,26 @@ fn diff_refs_to_import(
}
// TODO: Make it configurable which remotes are publishing and update public
// heads here.
let old_remote_target = known_remote_refs.remove(&ref_name).flatten();
let (old_remote_target, old_remote_state) = known_remote_refs
.remove(&ref_name)
.unwrap_or_else(|| (RefTarget::absent_ref(), RemoteRefState::New));
if new_target != *old_remote_target {
changed_remote_refs.insert(ref_name, (old_remote_target.clone(), new_target));
let old_remote_ref = RemoteRef {
target: old_remote_target.clone(),
state: old_remote_state,
};
changed_remote_refs.insert(ref_name, (old_remote_ref, new_target));
}
}
for full_name in known_git_refs.into_keys() {
changed_git_refs.push((full_name.to_owned(), RefTarget::absent()));
}
for (ref_name, old_target) in known_remote_refs {
changed_remote_refs.insert(ref_name, (old_target.clone(), RefTarget::absent()));
for (ref_name, (old_target, old_state)) in known_remote_refs {
let old_remote_ref = RemoteRef {
target: old_target.clone(),
state: old_state,
};
changed_remote_refs.insert(ref_name, (old_remote_ref, RefTarget::absent()));
}
Ok(RefsToImport {
changed_git_refs,

View file

@ -175,6 +175,18 @@ impl RemoteRef {
pub fn is_tracking(&self) -> bool {
self.state == RemoteRefState::Tracking
}
/// Target that should have been merged in to the local ref.
///
/// Use this as the base or known target when merging new remote ref in to
/// local or pushing local ref to remote.
pub fn tracking_target(&self) -> &RefTarget {
if self.is_tracking() {
&self.target
} else {
RefTarget::absent_ref()
}
}
}
/// Whether the ref is tracked or not.

View file

@ -1365,15 +1365,15 @@ fn test_export_import_sequence() {
}
#[test]
fn test_import_export_no_auto_local_branch() {
fn test_import_export_non_tracking_branch() {
// Import a remote tracking branch and export it. We should not create a git
// branch.
let test_data = GitRepoData::create();
let git_settings = GitSettings {
let mut git_settings = GitSettings {
auto_local_branch: false,
};
let git_repo = test_data.git_repo;
let git_commit = empty_git_commit(&git_repo, "refs/remotes/origin/main", &[]);
let commit_main_t0 = empty_git_commit(&git_repo, "refs/remotes/origin/main", &[]);
let mut tx = test_data
.repo
@ -1386,18 +1386,72 @@ fn test_import_export_no_auto_local_branch() {
assert_eq!(
mut_repo.view().get_remote_branch("main", "origin"),
&RemoteRef {
target: RefTarget::normal(jj_id(&git_commit)),
target: RefTarget::normal(jj_id(&commit_main_t0)),
state: RemoteRefState::New,
},
);
assert_eq!(
mut_repo.get_git_ref("refs/remotes/origin/main"),
RefTarget::normal(jj_id(&git_commit))
RefTarget::normal(jj_id(&commit_main_t0))
);
// Export the branch to git
assert_eq!(git::export_refs(mut_repo, &git_repo), Ok(vec![]));
assert_eq!(mut_repo.get_git_ref("refs/heads/main"), RefTarget::absent());
// Reimport with auto-local-branch on. Local branch shouldn't be created for
// the known branch "main".
let commit_main_t1 =
empty_git_commit(&git_repo, "refs/remotes/origin/main", &[&commit_main_t0]);
let commit_feat_t1 = empty_git_commit(&git_repo, "refs/remotes/origin/feat", &[]);
git_settings.auto_local_branch = true;
git::import_refs(mut_repo, &git_repo, &git_settings).unwrap();
assert!(mut_repo.view().get_local_branch("main").is_absent());
assert_eq!(
mut_repo.view().get_local_branch("feat"),
&RefTarget::normal(jj_id(&commit_feat_t1))
);
assert_eq!(
mut_repo.view().get_remote_branch("main", "origin"),
&RemoteRef {
target: RefTarget::normal(jj_id(&commit_main_t1)),
state: RemoteRefState::New,
},
);
assert_eq!(
mut_repo.view().get_remote_branch("feat", "origin"),
&RemoteRef {
target: RefTarget::normal(jj_id(&commit_feat_t1)),
state: RemoteRefState::Tracking,
},
);
// Reimport with auto-local-branch off. Tracking branch should be imported.
let commit_main_t2 =
empty_git_commit(&git_repo, "refs/remotes/origin/main", &[&commit_main_t1]);
let commit_feat_t2 =
empty_git_commit(&git_repo, "refs/remotes/origin/feat", &[&commit_feat_t1]);
git_settings.auto_local_branch = false;
git::import_refs(mut_repo, &git_repo, &git_settings).unwrap();
assert!(mut_repo.view().get_local_branch("main").is_absent());
assert_eq!(
mut_repo.view().get_local_branch("feat"),
&RefTarget::normal(jj_id(&commit_feat_t2))
);
assert_eq!(
mut_repo.view().get_remote_branch("main", "origin"),
&RemoteRef {
target: RefTarget::normal(jj_id(&commit_main_t2)),
state: RemoteRefState::New,
},
);
assert_eq!(
mut_repo.view().get_remote_branch("feat", "origin"),
&RemoteRef {
target: RefTarget::normal(jj_id(&commit_feat_t2)),
state: RemoteRefState::Tracking,
},
);
}
#[test]