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-branches = ["glob:*"]
```

Specific branches can also be disabled:
```
[experimental-advance-branches]
enabled-branches = ["glob:*"]
disabled-branches = ["main"]
```

Branches that match a disabled pattern will not be advanced, even if they also
match an enabled pattern.

This implements feature request #2338.

WIP: use patterns instead of overrides list to control advance-branches
  • Loading branch information
emesterhazy committed Mar 29, 2024
1 parent dd1def0 commit 619cd37
Show file tree
Hide file tree
Showing 7 changed files with 411 additions and 1 deletion.
133 changes: 132 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 @@ -389,6 +390,18 @@ impl ReadonlyUserRepo {
}
}

/// A branch that should be advanced to satisfy the "advance-branches" feature.
/// This is a helper for `WorkspaceCommandTransaction`. It provides a type-safe
/// way to separate the work of checking whether a branch can be advanced and
/// actually advancing it. Advancing the branch never fails, but can't be done
/// until the new `CommitId` is available. Splitting the work in this way also
/// allows us to identify eligible branches without actually moving them.
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 @@ -1362,6 +1375,96 @@ Then run `jj squash` to move the resolution into the conflicted commit."#,

Ok(())
}

// Retrieves pattern literals from the user's advance-branches config and parses
// them.
fn advance_branches_patterns(
&self,
setting_key: &str,
) -> Result<Vec<StringPattern>, CommandError> {
let setting = format!("experimental-advance-branches.{setting_key}");
let err_msg = format!("Error parsing pattern for {setting}");
match self.settings.config().get_array(&setting) {
Ok(patterns) => patterns
.into_iter()
.map(|p| {
p.into_string()
.map_err(|e| config_error_with_message(&err_msg, e))
.and_then(|s| {
StringPattern::parse(&s)
.map_err(|e| config_error_with_message(&err_msg, e))
})
})
.collect(),
Err(_) => Ok(Vec::new()),
}
}

// Returns a set of patterns that can be used to identify local branches
// which have the advance-branches setting enabled.
fn advance_branches_enabled_branches(&self) -> Result<Vec<StringPattern>, CommandError> {
self.advance_branches_patterns("enabled-branches")
}

// Returns a set of patterns that can be used to identify local branches
// which have the advance-branches setting disabled.
fn advance_branches_disabled_branches(&self) -> Result<Vec<StringPattern>, CommandError> {
self.advance_branches_patterns("disabled-branches")
}

/// Identifies branches which are eligible to be moved automatically during
/// `jj commit` and `jj new`. Whether a branch is eligible is determined by
/// its target and the user and repo config for "advance-branches".
///
/// Returns a Vec of branches in `repo` that point to any of the `from`
/// commits and that are eligible to advance. The `from` commits are
/// typically the parents of the target commit of `jj commit` or `jj new`.
///
/// 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,
from: impl IntoIterator<Item = &'a CommitId>,
) -> Result<Vec<AdvanceableBranch>, CommandError> {
let enabled_branches = self.advance_branches_enabled_branches()?;
let disabled_branches = self.advance_branches_disabled_branches()?;
let allow_branch = |branch: &str| {
for disabled in disabled_branches.iter() {
if disabled.matches(branch) {
return false;
}
}
for enabled in enabled_branches.iter() {
if enabled.matches(branch) {
return true;
}
}
false
};

// Return early if we know that there's no work to do.
if enabled_branches.is_empty() {
return Ok(Vec::new());
}

let mut advanceable_branches = Vec::new();
for from_commit in from {
for (name, target) in self.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(),
});
}
}
}

Ok(advanceable_branches)
}
}

/// A [`Transaction`] tied to a particular workspace.
Expand Down Expand Up @@ -1448,6 +1551,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
6 changes: 6 additions & 0 deletions cli/src/commands/commit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ 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(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 +120,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
20 changes: 20 additions & 0 deletions cli/src/config-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,26 @@
}
}
},
"experimental-advance-branches": {
"type": "object",
"description": "Settings controlling the 'advance-branches' feature which moves branches forward when new commits are created.",
"properties": {
"enabled-branches": {
"type": "array",
"description": "Patterns used to identify branches which may be advanced.",
"items": {
"type": "string"
}
},
"disabled-branches": {
"type": "array",
"description": "Patterns used to identify branches which are not advanced. Takes precedence over 'enabled-branches'.",
"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 619cd37

Please sign in to comment.