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

git: on push, update jj's view of remote branches without using import_refs()

This means that the commits previously pinned by remote branches are no longer
abandoned. I think that's more correct since "push" is the operation to
propagate local view to remote, and uninteresting commits should have been
locally abandoned.
This commit is contained in:
Yuya Nishihara 2023-10-15 04:27:44 +09:00
parent c8a848d260
commit e0965c4533
7 changed files with 131 additions and 18 deletions

View file

@ -38,6 +38,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
See [automatic local branch creation](docs/config.md#automatic-local-branch-creation) See [automatic local branch creation](docs/config.md#automatic-local-branch-creation)
for details. for details.
* Pushing deleted/moved branches no longer abandons the local commits referenced
by the remote branches.
### New features ### New features
* `jj workspace add` now takes a `--revision` argument. * `jj workspace add` now takes a `--revision` argument.

View file

@ -977,7 +977,7 @@ fn cmd_git_push(
force_pushed_branches, force_pushed_branches,
}; };
with_remote_callbacks(ui, |cb| { with_remote_callbacks(ui, |cb| {
git::push_branches(&git_repo, &remote, &targets, cb) git::push_branches(tx.mut_repo(), &git_repo, &remote, &targets, cb)
}) })
.map_err(|err| match err { .map_err(|err| match err {
GitPushError::InternalGitError(err) => map_git_error(err), GitPushError::InternalGitError(err) => map_git_error(err),
@ -988,9 +988,6 @@ fn cmd_git_push(
), ),
_ => user_error(err.to_string()), _ => 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)?; tx.finish(ui)?;
Ok(()) Ok(())
} }

View file

@ -300,13 +300,22 @@ fn test_git_push_multiple() {
Delete branch branch1 from 45a3aa29e907 Delete branch branch1 from 45a3aa29e907
Force branch branch2 from 8476341eb395 to 15dcdaa4f12f Force branch branch2 from 8476341eb395 to 15dcdaa4f12f
Add branch my-branch to 15dcdaa4f12f Add branch my-branch to 15dcdaa4f12f
Abandoned 2 commits that are no longer reachable.
"###); "###);
let stdout = test_env.jj_cmd_success(&workspace_root, &["branch", "list"]); let stdout = test_env.jj_cmd_success(&workspace_root, &["branch", "list"]);
insta::assert_snapshot!(stdout, @r###" insta::assert_snapshot!(stdout, @r###"
branch2: yqosqzyt 15dcdaa4 (empty) foo branch2: yqosqzyt 15dcdaa4 (empty) foo
my-branch: yqosqzyt 15dcdaa4 (empty) foo my-branch: yqosqzyt 15dcdaa4 (empty) foo
"###); "###);
let stdout = test_env.jj_cmd_success(&workspace_root, &["log", "-rall()"]);
insta::assert_snapshot!(stdout, @r###"
@ yqosqzyt test.user@example.com 2001-02-03 04:05:17.000 +07:00 branch2 my-branch 15dcdaa4
(empty) foo
rlzusymt test.user@example.com 2001-02-03 04:05:10.000 +07:00 8476341e
(empty) description 2
lzmmnrxq test.user@example.com 2001-02-03 04:05:08.000 +07:00 45a3aa29
(empty) description 1
zzzzzzzz root() 00000000
"###);
} }
#[test] #[test]
@ -588,7 +597,16 @@ fn test_git_push_deleted() {
insta::assert_snapshot!(stderr, @r###" insta::assert_snapshot!(stderr, @r###"
Branch changes to push to origin: Branch changes to push to origin:
Delete branch branch1 from 45a3aa29e907 Delete branch branch1 from 45a3aa29e907
Abandoned 1 commits that are no longer reachable. "###);
let stdout = test_env.jj_cmd_success(&workspace_root, &["log", "-rall()"]);
insta::assert_snapshot!(stdout, @r###"
rlzusymt test.user@example.com 2001-02-03 04:05:10.000 +07:00 branch2 8476341e
(empty) description 2
lzmmnrxq test.user@example.com 2001-02-03 04:05:08.000 +07:00 45a3aa29
(empty) description 1
@ yqosqzyt test.user@example.com 2001-02-03 04:05:13.000 +07:00 5b36783c
(empty) (no description set)
zzzzzzzz root() 00000000
"###); "###);
let (stdout, stderr) = test_env.jj_cmd_ok(&workspace_root, &["git", "push", "--deleted"]); let (stdout, stderr) = test_env.jj_cmd_ok(&workspace_root, &["git", "push", "--deleted"]);
insta::assert_snapshot!(stdout, @""); insta::assert_snapshot!(stdout, @"");

View file

@ -197,8 +197,7 @@ In particular, a merge of local and remote targets is
* ~`tags`~ (not implemented, but should be the same as `branches`) * ~`tags`~ (not implemented, but should be the same as `branches`)
2. Pushes diff to the remote Git repo (as well as remote tracking branches 2. Pushes diff to the remote Git repo (as well as remote tracking branches
in the backing Git repo.) in the backing Git repo.)
3. Sets `remotes[remote].branches[name].state = tracking` 3. Updates `remotes[remote]` and `git_refs` reflecting the push.
4. Import changes only for `remotes[remote].branches[glob]`
* `jj git export` * `jj git export`
1. Copies local `branches`/`tags` back to `remotes["git"]`. 1. Copies local `branches`/`tags` back to `remotes["git"]`.
@ -288,25 +287,22 @@ Note: desired behavior of `jj branch forget` is to
* Pushing new branch, remote doesn't exist * Pushing new branch, remote doesn't exist
1. Pushes `[local, absent] - [absent]` -> `local` 1. Pushes `[local, absent] - [absent]` -> `local`
2. Sets `remotes[remote].branches[name].state = tracking` 2. Sets `remotes[remote].branches[name].target = local`, `.state = tracking`
3. `import_refs()` merges `[local, local] - [absent]` -> `local` (noop)
* Pushing new branch, untracked remote exists * Pushing new branch, untracked remote exists
1. Pushes `[local, remote] - [absent]` 1. Pushes `[local, remote] - [absent]`
* Fails if `local` moved backwards or sideways * Fails if `local` moved backwards or sideways
2. Sets `remotes[remote].branches[name].state = tracking` 2. Sets `remotes[remote].branches[name].target = local`, `.state = tracking`
3. `import_refs()` merges `[local, local] - [remote]` -> `local` (noop)
* Pushing existing branch to tracking remote * Pushing existing branch to tracking remote
1. Pushes `[local, remote] - [remote]` -> `local` 1. Pushes `[local, remote] - [remote]` -> `local`
* Fails if `local` moved backwards or sideways, and if `remote` is out of * Fails if `local` moved backwards or sideways, and if `remote` is out of
sync sync
2. `import_refs()` merges `[local, local] - [remote]` -> `local` (noop) 2. Sets `remotes[remote].branches[name].target = local`
* Pushing existing branch to untracked remote * Pushing existing branch to untracked remote
* Same as "new branch" * Same as "new branch"
* Pushing deleted branch to tracking remote * Pushing deleted branch to tracking remote
1. Pushes `[absent, remote] - [remote]` -> `absent` 1. Pushes `[absent, remote] - [remote]` -> `absent`
* TODO: Fails if `remote` is out of sync? * TODO: Fails if `remote` is out of sync?
2. `import_refs()` merges `[absent, absent] - [remote]` -> `absent` 2. Removes `remotes[remote].branches[name]` (`target` becomes `absent`)
3. Removes `remotes[remote].branches[name]` (`target` becomes `absent`)
* Pushing deleted branch to untracked remote * Pushing deleted branch to untracked remote
* Noop since `[absent, remote] - [absent]` -> `remote` * Noop since `[absent, remote] - [absent]` -> `remote`
* Perhaps, UI will report error * Perhaps, UI will report error

View file

@ -1094,7 +1094,9 @@ pub struct GitRefUpdate {
pub new_target: Option<CommitId>, pub new_target: Option<CommitId>,
} }
/// Pushes the specified branches and updates the repo view accordingly.
pub fn push_branches( pub fn push_branches(
mut_repo: &mut MutableRepo,
git_repo: &git2::Repository, git_repo: &git2::Repository,
remote_name: &str, remote_name: &str,
targets: &GitBranchPushTargets, targets: &GitBranchPushTargets,
@ -1110,9 +1112,24 @@ pub fn push_branches(
}) })
.collect_vec(); .collect_vec();
push_updates(git_repo, remote_name, &ref_updates, callbacks)?; push_updates(git_repo, remote_name, &ref_updates, callbacks)?;
// TODO: add support for partially pushed refs? we could update the view
// excluding rejected refs, but the transaction would be aborted anyway
// if we returned an Err.
for (branch_name, update) in &targets.branch_updates {
let git_ref_name = format!("refs/remotes/{remote_name}/{branch_name}");
let new_remote_ref = RemoteRef {
target: RefTarget::resolved(update.new_target.clone()),
state: RemoteRefState::Tracking,
};
mut_repo.set_git_ref_target(&git_ref_name, new_remote_ref.target.clone());
mut_repo.set_remote_branch(branch_name, remote_name, new_remote_ref);
}
Ok(()) Ok(())
} }
/// Pushes the specified Git refs without updating the repo view.
pub fn push_updates( pub fn push_updates(
git_repo: &git2::Repository, git_repo: &git2::Repository,
remote_name: &str, remote_name: &str,

View file

@ -83,6 +83,11 @@ impl RefTarget {
&TARGET &TARGET
} }
/// Creates non-conflicting target that optionally points to a commit.
pub fn resolved(maybe_id: Option<CommitId>) -> Self {
Self::from_merge(Merge::resolved(maybe_id))
}
/// Creates non-conflicting target pointing to a commit. /// Creates non-conflicting target pointing to a commit.
pub fn normal(id: CommitId) -> Self { pub fn normal(id: CommitId) -> Self {
Self::from_merge(Merge::normal(id)) Self::from_merge(Merge::normal(id))

View file

@ -2256,6 +2256,20 @@ fn set_up_push_repos(settings: &UserSettings, temp_dir: &TempDir) -> PushTestSet
.set_parents(vec![initial_commit.id().clone()]) .set_parents(vec![initial_commit.id().clone()])
.write() .write()
.unwrap(); .unwrap();
tx.mut_repo().set_git_ref_target(
"refs/remotes/origin/main",
RefTarget::normal(initial_commit.id().clone()),
);
tx.mut_repo().set_remote_branch(
"main",
"origin",
RemoteRef {
target: RefTarget::normal(initial_commit.id().clone()),
// Caller expects the main branch is tracked. The corresponding local branch will
// be created (or left as deleted) by caller.
state: RemoteRefState::Tracking,
},
);
let jj_repo = tx.commit(); let jj_repo = tx.commit();
PushTestSetup { PushTestSetup {
source_repo_dir, source_repo_dir,
@ -2269,8 +2283,9 @@ fn set_up_push_repos(settings: &UserSettings, temp_dir: &TempDir) -> PushTestSet
fn test_push_branches_success() { fn test_push_branches_success() {
let settings = testutils::user_settings(); let settings = testutils::user_settings();
let temp_dir = testutils::new_temp_dir(); let temp_dir = testutils::new_temp_dir();
let setup = set_up_push_repos(&settings, &temp_dir); let mut setup = set_up_push_repos(&settings, &temp_dir);
let clone_repo = get_git_repo(&setup.jj_repo); let clone_repo = get_git_repo(&setup.jj_repo);
let mut tx = setup.jj_repo.start_transaction(&settings, "test");
let targets = GitBranchPushTargets { let targets = GitBranchPushTargets {
branch_updates: vec![( branch_updates: vec![(
@ -2283,6 +2298,7 @@ fn test_push_branches_success() {
force_pushed_branches: hashset! {}, force_pushed_branches: hashset! {},
}; };
let result = git::push_branches( let result = git::push_branches(
tx.mut_repo(),
&clone_repo, &clone_repo,
"origin", "origin",
&targets, &targets,
@ -2307,14 +2323,35 @@ fn test_push_branches_success() {
.unwrap() .unwrap()
.target(); .target();
assert_eq!(new_target, Some(new_oid)); assert_eq!(new_target, Some(new_oid));
// Check that the repo view got updated
let view = tx.mut_repo().view();
assert_eq!(
*view.get_git_ref("refs/remotes/origin/main"),
RefTarget::normal(setup.new_commit.id().clone()),
);
assert_eq!(
*view.get_remote_branch("main", "origin"),
RemoteRef {
target: RefTarget::normal(setup.new_commit.id().clone()),
state: RemoteRefState::Tracking,
},
);
// Check that the repo view reflects the changes in the Git repo
setup.jj_repo = tx.commit();
let mut tx = setup.jj_repo.start_transaction(&settings, "test");
git::import_refs(tx.mut_repo(), &clone_repo, &GitSettings::default()).unwrap();
assert!(!tx.mut_repo().has_changes());
} }
#[test] #[test]
fn test_push_branches_deletion() { fn test_push_branches_deletion() {
let settings = testutils::user_settings(); let settings = testutils::user_settings();
let temp_dir = testutils::new_temp_dir(); let temp_dir = testutils::new_temp_dir();
let setup = set_up_push_repos(&settings, &temp_dir); let mut setup = set_up_push_repos(&settings, &temp_dir);
let clone_repo = get_git_repo(&setup.jj_repo); let clone_repo = get_git_repo(&setup.jj_repo);
let mut tx = setup.jj_repo.start_transaction(&settings, "test");
let source_repo = git2::Repository::open(&setup.source_repo_dir).unwrap(); let source_repo = git2::Repository::open(&setup.source_repo_dir).unwrap();
// Test the setup // Test the setup
@ -2331,6 +2368,7 @@ fn test_push_branches_deletion() {
force_pushed_branches: hashset! {}, force_pushed_branches: hashset! {},
}; };
let result = git::push_branches( let result = git::push_branches(
tx.mut_repo(),
&get_git_repo(&setup.jj_repo), &get_git_repo(&setup.jj_repo),
"origin", "origin",
&targets, &targets,
@ -2347,14 +2385,26 @@ fn test_push_branches_deletion() {
assert!(clone_repo assert!(clone_repo
.find_reference("refs/remotes/origin/main") .find_reference("refs/remotes/origin/main")
.is_err()); .is_err());
// Check that the repo view got updated
let view = tx.mut_repo().view();
assert!(view.get_git_ref("refs/remotes/origin/main").is_absent());
assert!(view.get_remote_branch("main", "origin").is_absent());
// Check that the repo view reflects the changes in the Git repo
setup.jj_repo = tx.commit();
let mut tx = setup.jj_repo.start_transaction(&settings, "test");
git::import_refs(tx.mut_repo(), &clone_repo, &GitSettings::default()).unwrap();
assert!(!tx.mut_repo().has_changes());
} }
#[test] #[test]
fn test_push_branches_mixed_deletion_and_addition() { fn test_push_branches_mixed_deletion_and_addition() {
let settings = testutils::user_settings(); let settings = testutils::user_settings();
let temp_dir = testutils::new_temp_dir(); let temp_dir = testutils::new_temp_dir();
let setup = set_up_push_repos(&settings, &temp_dir); let mut setup = set_up_push_repos(&settings, &temp_dir);
let clone_repo = get_git_repo(&setup.jj_repo); let clone_repo = get_git_repo(&setup.jj_repo);
let mut tx = setup.jj_repo.start_transaction(&settings, "test");
let targets = GitBranchPushTargets { let targets = GitBranchPushTargets {
branch_updates: vec![ branch_updates: vec![
@ -2376,6 +2426,7 @@ fn test_push_branches_mixed_deletion_and_addition() {
force_pushed_branches: hashset! {}, force_pushed_branches: hashset! {},
}; };
let result = git::push_branches( let result = git::push_branches(
tx.mut_repo(),
&clone_repo, &clone_repo,
"origin", "origin",
&targets, &targets,
@ -2393,6 +2444,28 @@ fn test_push_branches_mixed_deletion_and_addition() {
// Check that the main ref got deleted in the source repo // Check that the main ref got deleted in the source repo
assert!(source_repo.find_reference("refs/heads/main").is_err()); assert!(source_repo.find_reference("refs/heads/main").is_err());
// Check that the repo view got updated
let view = tx.mut_repo().view();
assert!(view.get_git_ref("refs/remotes/origin/main").is_absent());
assert!(view.get_remote_branch("main", "origin").is_absent());
assert_eq!(
*view.get_git_ref("refs/remotes/origin/topic"),
RefTarget::normal(setup.new_commit.id().clone()),
);
assert_eq!(
*view.get_remote_branch("topic", "origin"),
RemoteRef {
target: RefTarget::normal(setup.new_commit.id().clone()),
state: RemoteRefState::Tracking,
},
);
// Check that the repo view reflects the changes in the Git repo
setup.jj_repo = tx.commit();
let mut tx = setup.jj_repo.start_transaction(&settings, "test");
git::import_refs(tx.mut_repo(), &clone_repo, &GitSettings::default()).unwrap();
assert!(!tx.mut_repo().has_changes());
} }
#[test] #[test]
@ -2403,6 +2476,7 @@ fn test_push_branches_not_fast_forward() {
let mut tx = setup.jj_repo.start_transaction(&settings, "test"); let mut tx = setup.jj_repo.start_transaction(&settings, "test");
let new_commit = write_random_commit(tx.mut_repo(), &settings); let new_commit = write_random_commit(tx.mut_repo(), &settings);
setup.jj_repo = tx.commit(); setup.jj_repo = tx.commit();
let mut tx = setup.jj_repo.start_transaction(&settings, "test");
let targets = GitBranchPushTargets { let targets = GitBranchPushTargets {
branch_updates: vec![( branch_updates: vec![(
@ -2415,6 +2489,7 @@ fn test_push_branches_not_fast_forward() {
force_pushed_branches: hashset! {}, force_pushed_branches: hashset! {},
}; };
let result = git::push_branches( let result = git::push_branches(
tx.mut_repo(),
&get_git_repo(&setup.jj_repo), &get_git_repo(&setup.jj_repo),
"origin", "origin",
&targets, &targets,
@ -2431,6 +2506,7 @@ fn test_push_branches_not_fast_forward_with_force() {
let mut tx = setup.jj_repo.start_transaction(&settings, "test"); let mut tx = setup.jj_repo.start_transaction(&settings, "test");
let new_commit = write_random_commit(tx.mut_repo(), &settings); let new_commit = write_random_commit(tx.mut_repo(), &settings);
setup.jj_repo = tx.commit(); setup.jj_repo = tx.commit();
let mut tx = setup.jj_repo.start_transaction(&settings, "test");
let targets = GitBranchPushTargets { let targets = GitBranchPushTargets {
branch_updates: vec![( branch_updates: vec![(
@ -2445,6 +2521,7 @@ fn test_push_branches_not_fast_forward_with_force() {
}, },
}; };
let result = git::push_branches( let result = git::push_branches(
tx.mut_repo(),
&get_git_repo(&setup.jj_repo), &get_git_repo(&setup.jj_repo),
"origin", "origin",
&targets, &targets,