Skip to content

Commit

Permalink
everything: Rename branches to bookmarks
Browse files Browse the repository at this point in the history
Jujutsu's branches do not behave like Git branches, which is a major
hurdle for people adopting it from Git. They rather behave like
Mercurial's (hg) bookmarks. 

We've had multiple discussions about it in the last ~1.5 years about this rename in the Discord, 
where multiple people agreed that this _false_ familiarity does not help anyone. Initially we were 
reluctant to do it but overtime, more and more users agreed that `bookmark` was a better for name 
the current mechanism. This may be hard break for current `jj branch` users, but it will immensly 
help Jujutsu's future, by defining it as our first own term. The `[experimental-moving-branches]` 
config option is currently left alone, to force not another large config update for
users, since the last time this happened was when `jj log -T show` was removed, which immediately 
resulted in breaking users and introduced soft deprecations.

This name change will also make it easier to introduce Topics (#3402) as _topological branches_ 
with a easier model. 

This was mostly done via LSP, ripgrep and sed and a whole bunch of manual changes either from
me being lazy or thankfully pointed out by reviewers.
  • Loading branch information
PhilipMetzger committed Sep 11, 2024
1 parent 8314046 commit d9c68e0
Show file tree
Hide file tree
Showing 96 changed files with 4,574 additions and 4,273 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,22 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

* Invalid `ui.graph.style` configuration is now an error.

* The builtin template `branch_list` has been renamed to `bookmark_list` in
lieu of the `jj branch` deprecation.

### Deprecations

* `jj obslog` is now called `jj evolution-log`/`jj evolog`. `jj obslog` remains
as an alias.

* `jj branch` has been deprecated in favor of `jj bookmark`.

**Rationale:** Jujutsu's branches don't behave like Git branches, which a
confused many newcomers, as they expected a similar behavior given the name.
We've renamed them to "bookmarks" to match the actual behavior, as we think
that describes them better, and they also behave similar to Mercurial's
bookmarks.

### New features

* The new config option `snapshot.auto-track` lets you automatically track only
Expand Down
183 changes: 93 additions & 90 deletions cli/src/cli_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -512,33 +512,33 @@ 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 and
/// return config errors to the user early.
pub struct AdvanceableBranch {
/// A bookmark that should be advanced to satisfy the "advance-bookmarks"
/// feature. This is a helper for `WorkspaceCommandTransaction`. It provides a
/// type-safe way to separate the work of checking whether a bookmark can be
/// advanced and actually advancing it. Advancing the bookmark 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 bookmarks without actually
/// moving them and return config errors to the user early.
pub struct AdvanceableBookmark {
name: String,
old_commit_id: CommitId,
}

/// Helper for parsing and evaluating settings for the advance-branches feature.
/// Settings are configured in the jj config.toml as lists of [`StringPattern`]s
/// for enabled and disabled branches. Example:
/// Helper for parsing and evaluating settings for the advance-bookmarks
/// feature. Settings are configured in the jj config.toml as lists of
/// [`StringPattern`]s for enabled and disabled bookmarks. Example:
/// ```toml
/// [experimental-advance-branches]
/// # Enable the feature for all branches except "main".
/// enabled-branches = ["glob:*"]
/// disabled-branches = ["main"]
/// ```
struct AdvanceBranchesSettings {
enabled_branches: Vec<StringPattern>,
disabled_branches: Vec<StringPattern>,
struct AdvanceBookmarksSettings {
enabled_bookmarks: Vec<StringPattern>,
disabled_bookmarks: Vec<StringPattern>,
}

impl AdvanceBranchesSettings {
impl AdvanceBookmarksSettings {
fn from_config(config: &config::Config) -> Result<Self, CommandError> {
let get_setting = |setting_key| {
let setting = format!("experimental-advance-branches.{setting_key}");
Expand All @@ -558,28 +558,30 @@ impl AdvanceBranchesSettings {
}
};
Ok(Self {
enabled_branches: get_setting("enabled-branches")?,
disabled_branches: get_setting("disabled-branches")?,
enabled_bookmarks: get_setting("enabled-branches")?,
disabled_bookmarks: get_setting("disabled-branches")?,
})
}

/// Returns true if the advance-branches feature is enabled for
/// `branch_name`.
fn branch_is_eligible(&self, branch_name: &str) -> bool {
/// Returns true if the advance-bookmarks feature is enabled for
/// `bookmark_name`.
fn bookmark_is_eligible(&self, bookmark_name: &str) -> bool {
if self
.disabled_branches
.disabled_bookmarks
.iter()
.any(|d| d.matches(branch_name))
.any(|d| d.matches(bookmark_name))
{
return false;
}
self.enabled_branches.iter().any(|e| e.matches(branch_name))
self.enabled_bookmarks
.iter()
.any(|e| e.matches(bookmark_name))
}

/// Returns true if the config includes at least one "enabled-branches"
/// pattern.
fn feature_enabled(&self) -> bool {
!self.enabled_branches.is_empty()
!self.enabled_bookmarks.is_empty()
}
}

Expand Down Expand Up @@ -855,10 +857,10 @@ impl WorkspaceCommandHelper {
}

/// Imports branches and tags from the underlying Git repo, abandons old
/// branches.
/// bookmarks.
///
/// If the working-copy branch is rebased, and if update is allowed, the new
/// working-copy commit will be checked out.
/// If the working-copy branch is rebased, and if update is allowed, the
/// new working-copy commit will be checked out.
///
/// This function does not import the Git HEAD, but the HEAD may be reset to
/// the working copy parent if the repository is colocated.
Expand Down Expand Up @@ -1479,8 +1481,8 @@ See https://martinvonz.github.io/jj/latest/working-copy/#stale-working-copy \
}

if self.working_copy_shared_with_git {
let failed_branches = git::export_refs(mut_repo)?;
print_failed_git_export(ui, &failed_branches)?;
let refs = git::export_refs(mut_repo)?;
print_failed_git_export(ui, &refs)?;
}

self.user_repo = ReadonlyUserRepo::new(tx.commit("snapshot working copy"));
Expand Down Expand Up @@ -1596,8 +1598,8 @@ See https://martinvonz.github.io/jj/latest/working-copy/#stale-working-copy \
if let Some(wc_commit) = &maybe_new_wc_commit {
git::reset_head(tx.repo_mut(), &git_repo, wc_commit)?;
}
let failed_branches = git::export_refs(tx.repo_mut())?;
print_failed_git_export(ui, &failed_branches)?;
let refs = git::export_refs(tx.repo_mut())?;
print_failed_git_export(ui, &refs)?;
}

self.user_repo = ReadonlyUserRepo::new(tx.commit(description));
Expand Down Expand Up @@ -1781,42 +1783,43 @@ Then run `jj squash` to move the resolution into the conflicted commit."#,
Ok(())
}

/// 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".
/// Identifies bookmarks which are eligible to be moved automatically
/// during `jj commit` and `jj new`. Whether a bookmark is eligible is
/// determined by its target and the user and repo config for
/// "advance-bookmarks".
///
/// Returns a Vec of branches in `repo` that point to any of the `from`
/// Returns a Vec of bookmarks 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.
/// `WorkspaceCommandTransaction::advance_bookmarks()` is called with the
/// `AdvanceableBookmark`s returned by this function.
///
/// Returns an empty `std::Vec` if no branches are eligible to advance.
pub fn get_advanceable_branches<'a>(
/// Returns an empty `std::Vec` if no bookmarks are eligible to advance.
pub fn get_advanceable_bookmarks<'a>(
&self,
from: impl IntoIterator<Item = &'a CommitId>,
) -> Result<Vec<AdvanceableBranch>, CommandError> {
let ab_settings = AdvanceBranchesSettings::from_config(self.settings().config())?;
) -> Result<Vec<AdvanceableBookmark>, CommandError> {
let ab_settings = AdvanceBookmarksSettings::from_config(self.settings().config())?;
if !ab_settings.feature_enabled() {
// Return early if we know that there's no work to do.
return Ok(Vec::new());
}

let mut advanceable_branches = Vec::new();
let mut advanceable_bookmarks = Vec::new();
for from_commit in from {
for (name, _) in self.repo().view().local_branches_for_commit(from_commit) {
if ab_settings.branch_is_eligible(name) {
advanceable_branches.push(AdvanceableBranch {
for (name, _) in self.repo().view().local_bookmarks_for_commit(from_commit) {
if ab_settings.bookmark_is_eligible(name) {
advanceable_bookmarks.push(AdvanceableBookmark {
name: name.to_owned(),
old_commit_id: from_commit.clone(),
});
}
}
}

Ok(advanceable_branches)
Ok(advanceable_bookmarks)
}
}

Expand Down Expand Up @@ -1928,18 +1931,18 @@ impl WorkspaceCommandTransaction<'_> {
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 {
// This removes the old commit ID from the branch's RefTarget and
/// Moves each bookmark in `bookmarks` from an old commit it's associated
/// with (configured by `get_advanceable_bookmarks`) to the `move_to`
/// commit. If the bookmark 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_bookmarks(&mut self, bookmarks: Vec<AdvanceableBookmark>, move_to: &CommitId) {
for bookmark in bookmarks {
// This removes the old commit ID from the bookmark's RefTarget and
// replaces it with the `move_to` ID.
self.repo_mut().merge_local_branch(
&branch.name,
&RefTarget::normal(branch.old_commit_id),
self.repo_mut().merge_local_bookmark(
&bookmark.name,
&RefTarget::normal(bookmark.old_commit_id),
&RefTarget::normal(move_to.clone()),
);
}
Expand Down Expand Up @@ -2233,35 +2236,35 @@ pub fn print_unmatched_explicit_paths<'a>(
Ok(())
}

pub fn print_trackable_remote_branches(ui: &Ui, view: &View) -> io::Result<()> {
let remote_branch_names = view
.branches()
.filter(|(_, branch_target)| branch_target.local_target.is_present())
.flat_map(|(name, branch_target)| {
branch_target
pub fn print_trackable_remote_bookmarks(ui: &Ui, view: &View) -> io::Result<()> {
let remote_bookmark_names = view
.bookmarks()
.filter(|(_, bookmark_target)| bookmark_target.local_target.is_present())
.flat_map(|(name, bookmark_target)| {
bookmark_target
.remote_refs
.into_iter()
.filter(|&(_, remote_ref)| !remote_ref.is_tracking())
.map(move |(remote, _)| format!("{name}@{remote}"))
})
.collect_vec();
if remote_branch_names.is_empty() {
if remote_bookmark_names.is_empty() {
return Ok(());
}

if let Some(mut formatter) = ui.status_formatter() {
writeln!(
formatter.labeled("hint").with_heading("Hint: "),
"The following remote branches aren't associated with the existing local branches:"
"The following remote bookmarks aren't associated with the existing local bookmarks:"
)?;
for full_name in &remote_branch_names {
for full_name in &remote_bookmark_names {
write!(formatter, " ")?;
writeln!(formatter.labeled("branch"), "{full_name}")?;
writeln!(formatter.labeled("bookmark"), "{full_name}")?;
}
writeln!(
formatter.labeled("hint").with_heading("Hint: "),
"Run `jj branch track {names}` to keep local branches updated on future pulls.",
names = remote_branch_names.join(" "),
"Run `jj bookmark track {names}` to keep local bookmarks updated on future pulls.",
names = remote_bookmark_names.join(" "),
)?;
}
Ok(())
Expand Down Expand Up @@ -2509,30 +2512,30 @@ impl DiffSelector {
}

#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct RemoteBranchName {
pub branch: String,
pub struct RemoteBookmarkName {
pub bookmark: String,
pub remote: String,
}

impl fmt::Display for RemoteBranchName {
impl fmt::Display for RemoteBookmarkName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let RemoteBranchName { branch, remote } = self;
write!(f, "{branch}@{remote}")
let RemoteBookmarkName { bookmark, remote } = self;
write!(f, "{bookmark}@{remote}")
}
}

#[derive(Clone, Debug)]
pub struct RemoteBranchNamePattern {
pub branch: StringPattern,
pub struct RemoteBookmarkNamePattern {
pub bookmark: StringPattern,
pub remote: StringPattern,
}

impl FromStr for RemoteBranchNamePattern {
impl FromStr for RemoteBookmarkNamePattern {
type Err = String;

fn from_str(src: &str) -> Result<Self, Self::Err> {
// The kind prefix applies to both branch and remote fragments. It's
// weird that unanchored patterns like substring:branch@remote is split
// The kind prefix applies to both bookmark and remote fragments. It's
// weird that unanchored patterns like substring:bookmark@remote is split
// into two, but I can't think of a better syntax.
// TODO: should we disable substring pattern? what if we added regex?
let (maybe_kind, pat) = src
Expand All @@ -2545,27 +2548,27 @@ impl FromStr for RemoteBranchNamePattern {
Ok(StringPattern::exact(pat))
}
};
// TODO: maybe reuse revset parser to handle branch/remote name containing @
let (branch, remote) = pat
.rsplit_once('@')
.ok_or_else(|| "remote branch must be specified in branch@remote form".to_owned())?;
Ok(RemoteBranchNamePattern {
branch: to_pattern(branch)?,
// TODO: maybe reuse revset parser to handle bookmark/remote name containing @
let (bookmark, remote) = pat.rsplit_once('@').ok_or_else(|| {
"remote bookmark must be specified in bookmark@remote form".to_owned()
})?;
Ok(RemoteBookmarkNamePattern {
bookmark: to_pattern(bookmark)?,
remote: to_pattern(remote)?,
})
}
}

impl RemoteBranchNamePattern {
impl RemoteBookmarkNamePattern {
pub fn is_exact(&self) -> bool {
self.branch.is_exact() && self.remote.is_exact()
self.bookmark.is_exact() && self.remote.is_exact()
}
}

impl fmt::Display for RemoteBranchNamePattern {
impl fmt::Display for RemoteBookmarkNamePattern {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let RemoteBranchNamePattern { branch, remote } = self;
write!(f, "{branch}@{remote}")
let RemoteBookmarkNamePattern { bookmark, remote } = self;
write!(f, "{bookmark}@{remote}")
}
}

Expand Down
Loading

0 comments on commit d9c68e0

Please sign in to comment.