Skip to content

Commit

Permalink
Implement advance-branches for jj commit
Browse files Browse the repository at this point in the history
## Feature Description

If enabled in the user or repository settings, the local branches pointing to the
parents of the revision targeted by `jj commit` will be advanced to the newly
created commit. Support for `jj new` will be added in a future change.

This behavior can be enabled by default for all branches by setting
the following in the config.toml:

```
[experimental-advance-branches]
enabled = true
```

The default value of `experimental-advance-branches.enabled` is `false`. You
can override the behavior for specific branches by setting the value of
`experimental-advance-branches.overrides` in the config.toml:

```
[experimental-advance-branches]
overrides = ["main"]
```

Branches that have been overridden behave as if the value of
`experimental-advance-branches.enabled` is negated.


This implements feature request 2338.
  • Loading branch information
emesterhazy committed Mar 19, 2024
1 parent 1ec6a8f commit 4ee0d7d
Show file tree
Hide file tree
Showing 7 changed files with 376 additions and 1 deletion.
84 changes: 83 additions & 1 deletion cli/src/cli_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,10 @@ use jj_lib::gitignore::{GitIgnoreError, GitIgnoreFile};
use jj_lib::hex_util::to_reverse_hex;
use jj_lib::id_prefix::IdPrefixContext;
use jj_lib::matchers::{EverythingMatcher, Matcher, PrefixMatcher};
use jj_lib::merge::MergeBuilder;
use jj_lib::merged_tree::MergedTree;
use jj_lib::object_id::ObjectId;
use jj_lib::op_store::{OpStoreError, OperationId, WorkspaceId};
use jj_lib::op_store::{OpStoreError, OperationId, RefTarget, WorkspaceId};
use jj_lib::op_walk::OpsetEvaluationError;
use jj_lib::operation::Operation;
use jj_lib::repo::{
Expand Down Expand Up @@ -382,6 +383,19 @@ impl ReadonlyUserRepo {
}
}

/// A branch that must be advanced to satisfy the "advance-branches" feature.
/// This is a helper for the `WorkspaceCommandTransaction` type. It provides a
/// type-safe way to separate the work of checking whether a branch can be
/// advanced and actually advancing it. This is important since the check can
/// fail and return an error to the user, and we want to fail fast so that the
/// user doesn't lose any work, such as the commit message. Advancing the branch
/// never fails, but can't be done until the new `CommitId` is available.
pub struct AdvanceableBranch {
name: String,
old_commit_id: CommitId,
old_target: RefTarget,
}

/// Provides utilities for writing a command that works on a [`Workspace`]
/// (which most commands do).
pub struct WorkspaceCommandHelper {
Expand Down Expand Up @@ -1312,6 +1326,46 @@ Then run `jj squash` to move the resolution into the conflicted commit."#,

Ok(())
}

/// If enabled by the user or repository config, identifies branches in
/// `repo` pointing to any of the `from` commits which should be advanced to
/// the parent (aka the child of one of the `from` commits) of a new commit.
/// Branches are not moved until
/// `WorkspaceCommandTransaction::advance_branches()` is called with the
/// `AdvanceableBranch`s returned by this function.
///
/// Returns an empty `std::Vec` if no branches are eligible to advance.
pub fn get_advanceable_branches<'a>(
&self,
repo: &impl Repo,
from: impl IntoIterator<Item = &'a CommitId>,
) -> Vec<AdvanceableBranch> {
let advance_branches = self.settings.advance_branches();
let overrides = self.settings.advance_branches_overrides();
let allow_branch = |branch: &str| {
let overridden = overrides.iter().any(|x| x.eq(branch));
(advance_branches && !overridden) || (!advance_branches && overridden)
};
// Return early if we know that there's no work to do.
if !advance_branches && overrides.is_empty() {
return Vec::new();
}

let mut advanceable_branches = Vec::new();
for from_commit in from.into_iter() {
for (name, target) in repo.view().local_branches_for_commit(from_commit) {
if allow_branch(name) {
advanceable_branches.push(AdvanceableBranch {
name: name.to_owned(),
old_commit_id: from_commit.clone(),
old_target: target.clone(),
});
}
}
}

advanceable_branches
}
}

/// A [`Transaction`] tied to a particular workspace.
Expand Down Expand Up @@ -1394,6 +1448,34 @@ impl WorkspaceCommandTransaction<'_> {
pub fn into_inner(self) -> Transaction {
self.tx
}

/// Moves each branch in `branches` from an old commit it's associated with
/// (configured by `get_advanceable_branches`) to the `move_to` commit. If
/// the branch is conflicted before the update, it will remain conflicted
/// after the update, but the conflict will involve the `move_to` commit
/// instead of the old commit.
pub fn advance_branches(&mut self, branches: Vec<AdvanceableBranch>, move_to: &CommitId) {
for branch in branches {
// We are going to remove the old commit and add the new commit. The
// removed commit must be listed first in order for the `MergeBuilder`
// to recognize that it's being removed.
let remove_add = [Some(branch.old_commit_id), Some(move_to.clone())];
let new_target = RefTarget::from_merge(
MergeBuilder::from_iter(
branch
.old_target
.as_merge()
.iter()
.chain(&remove_add)
.cloned(),
)
.build()
.simplify(),
);
self.mut_repo()
.set_local_branch_target(&branch.name, new_target);
}
}
}

fn find_workspace_dir(cwd: &Path) -> &Path {
Expand Down
7 changes: 7 additions & 0 deletions cli/src/commands/commit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ pub(crate) fn cmd_commit(
.get_wc_commit_id()
.ok_or_else(|| user_error("This command requires a working copy"))?;
let commit = workspace_command.repo().store().get_commit(commit_id)?;
let advanceable_branches = workspace_command
.get_advanceable_branches(&**workspace_command.repo(), commit.parent_ids());
let matcher = workspace_command.matcher_from_values(&args.paths)?;
let diff_selector =
workspace_command.diff_selector(ui, args.tool.as_deref(), args.interactive)?;
Expand Down Expand Up @@ -119,6 +121,11 @@ new working-copy commit.
commit.tree_id().clone(),
)
.write()?;

if !advanceable_branches.is_empty() {
tx.advance_branches(advanceable_branches, new_commit.id());
}

for workspace_id in workspace_ids {
tx.mut_repo().edit(workspace_id, &new_wc_commit).unwrap();
}
Expand Down
18 changes: 18 additions & 0 deletions cli/src/config-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,24 @@
}
}
},
"experimental-advance-branches": {
"type": "object",
"description": "Parameters governing how and whether branch pointers are automatically updated.",
"properties": {
"enabled": {
"type": "boolean",
"description": "Whether to automatically move any branches pointing to a revision targeted by `jj new` or `jj commit` to the new commit.",
"default": false
},
"overrides": {
"type": "array",
"description": "Per-branch overrides for the global 'advance-branch' setting. Branches listed here will use the opposite of the global setting.",
"items": {
"type": "string"
}
}
}
},
"signing": {
"type": "object",
"description": "Settings for verifying and creating cryptographic commit signatures",
Expand Down
1 change: 1 addition & 0 deletions cli/tests/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ fn test_no_forgotten_test_files() {
}

mod test_abandon_command;
mod test_advance_branches;
mod test_alias;
mod test_branch_command;
mod test_builtin_aliases;
Expand Down
Loading

0 comments on commit 4ee0d7d

Please sign in to comment.