Skip to content

Commit eadd6aa

Browse files
committed
feat(bookmark): add bookmark bump command to advance bookmarks to latest descendants
Enables automatic bookmark advancement without manual targeting, streamlining workflow after making several commits
1 parent 22900c9 commit eadd6aa

File tree

5 files changed

+396
-0
lines changed

5 files changed

+396
-0
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1414

1515
### New features
1616

17+
* `jj bookmark bump` finds the closest bookmark on a revision or its ancestors
18+
and moves it forward to the latest non-empty descendant. This is useful for
19+
advancing a bookmark after making several commits without having to manually
20+
specify the bookmark name or target revision.
21+
1722
### Fixed bugs
1823

1924
## [0.34.0] - 2025-10-01

cli/src/commands/bookmark/bump.rs

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
// Copyright 2020-2023 The Jujutsu Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
use itertools::Itertools as _;
16+
use jj_lib::object_id::ObjectId as _;
17+
use jj_lib::op_store::RefTarget;
18+
use jj_lib::repo::Repo as _;
19+
use jj_lib::str_util::StringPattern;
20+
21+
use super::find_local_bookmarks;
22+
use super::is_fast_forward;
23+
use crate::cli_util::CommandHelper;
24+
use crate::cli_util::RevisionArg;
25+
use crate::command_error::CommandError;
26+
use crate::command_error::user_error;
27+
use crate::command_error::user_error_with_hint;
28+
use crate::revset_util;
29+
use crate::ui::Ui;
30+
31+
/// Move a bookmark to the latest non-empty descendant
32+
///
33+
/// Finds the closest bookmark on the specified revision or any of its
34+
/// ancestors, then moves that bookmark forward to the topologically latest
35+
/// non-empty descendant.
36+
///
37+
/// This is useful for advancing a bookmark after making several commits,
38+
/// without having to manually specify the bookmark name or target revision.
39+
///
40+
/// If multiple bookmarks exist on the same commit, the alphabetically first one
41+
/// is selected.
42+
///
43+
/// Example: After creating commits on top of a bookmarked commit, move the
44+
/// bookmark forward to the latest non-empty commit:
45+
///
46+
/// ```shell
47+
/// $ jj bookmark bump
48+
/// ```
49+
#[derive(clap::Args, Clone, Debug)]
50+
pub struct BookmarkBumpArgs {
51+
/// Revision to start searching for bookmarks from (searches ancestors too)
52+
#[arg(long, short, value_name = "REVSET", default_value = "@")]
53+
from: RevisionArg,
54+
55+
/// Allow moving the bookmark backwards or sideways
56+
#[arg(long, short = 'B')]
57+
allow_backwards: bool,
58+
}
59+
60+
pub fn cmd_bookmark_bump(
61+
ui: &mut Ui,
62+
command: &CommandHelper,
63+
args: &BookmarkBumpArgs,
64+
) -> Result<(), CommandError> {
65+
let mut workspace_command = command.workspace_helper(ui)?;
66+
let repo = workspace_command.repo();
67+
let from_commit = workspace_command.resolve_single_rev(ui, &args.from)?;
68+
69+
let (bookmark_name, bookmark_commit) = {
70+
let ancestors_expression = workspace_command.parse_revset(
71+
ui,
72+
&RevisionArg::from(format!("::{}", from_commit.id().hex())),
73+
)?;
74+
let store = repo.store();
75+
let ancestors = ancestors_expression.evaluate()?;
76+
77+
let ancestor_ids: Vec<_> = ancestors.iter().try_collect()?;
78+
79+
let pattern = StringPattern::everything();
80+
let all_bookmarks: Vec<_> = repo.view().local_bookmarks_matching(&pattern).collect();
81+
82+
let mut bookmark_on_ancestor = None;
83+
for ancestor_id in &ancestor_ids {
84+
let mut bookmarks_here: Vec<_> = all_bookmarks
85+
.iter()
86+
.filter(|(_, target)| target.added_ids().any(|id| id == ancestor_id))
87+
.collect();
88+
89+
if !bookmarks_here.is_empty() {
90+
bookmarks_here.sort_by_key(|(name, _)| *name);
91+
92+
if bookmarks_here.len() > 1 {
93+
writeln!(
94+
ui.warning_default(),
95+
"Multiple bookmarks found on revision {}: {}",
96+
ancestor_id.hex(),
97+
bookmarks_here
98+
.iter()
99+
.map(|(name, _)| name.as_symbol())
100+
.join(", ")
101+
)?;
102+
writeln!(
103+
ui.hint_default(),
104+
"Using bookmark: {}",
105+
bookmarks_here[0].0.as_symbol()
106+
)?;
107+
}
108+
109+
let (name, _) = bookmarks_here[0];
110+
let name_string = name.as_symbol().to_string();
111+
bookmark_on_ancestor = Some((name_string, store.get_commit(ancestor_id)?));
112+
break;
113+
}
114+
}
115+
116+
bookmark_on_ancestor.ok_or_else(|| {
117+
user_error(format!(
118+
"No bookmarks found on {} or its ancestors",
119+
from_commit.id().hex()
120+
))
121+
})?
122+
};
123+
124+
let target_commit = {
125+
let revset_expression = workspace_command.parse_revset(
126+
ui,
127+
&RevisionArg::from(format!("heads({}+ & ~empty())", bookmark_commit.id().hex())),
128+
)?;
129+
let store = repo.store();
130+
let descendants = revset_expression.evaluate()?;
131+
132+
let commit_ids: Vec<_> = descendants.iter().try_collect()?;
133+
134+
commit_ids
135+
.into_iter()
136+
.map(|id| store.get_commit(&id))
137+
.collect::<Result<Vec<_>, _>>()?
138+
.into_iter()
139+
.next()
140+
.ok_or_else(|| {
141+
user_error(format!(
142+
"No non-empty descendants found for revision {}",
143+
bookmark_commit.id().hex()
144+
))
145+
})?
146+
};
147+
148+
if !args.allow_backwards {
149+
let matches =
150+
find_local_bookmarks(repo.view(), &[StringPattern::exact(bookmark_name.clone())])?;
151+
if let Some((name, _old_target)) = matches
152+
.into_iter()
153+
.find(|(_, old_target)| !is_fast_forward(repo.as_ref(), old_target, target_commit.id()))
154+
{
155+
return Err(user_error_with_hint(
156+
format!(
157+
"Refusing to move bookmark backwards or sideways: {}",
158+
name.as_symbol()
159+
),
160+
"Use --allow-backwards to allow it.",
161+
));
162+
}
163+
}
164+
165+
let bookmark_ref_name = revset_util::parse_bookmark_name(&bookmark_name).map_err(|e| {
166+
user_error(format!(
167+
"Failed to parse bookmark name '{bookmark_name}': {e}"
168+
))
169+
})?;
170+
171+
let mut tx = workspace_command.start_transaction();
172+
tx.repo_mut().set_local_bookmark_target(
173+
&bookmark_ref_name,
174+
RefTarget::normal(target_commit.id().clone()),
175+
);
176+
177+
if let Some(mut formatter) = ui.status_formatter() {
178+
write!(formatter, "Moved bookmark {bookmark_name} to ")?;
179+
tx.write_commit_summary(formatter.as_mut(), &target_commit)?;
180+
writeln!(formatter)?;
181+
}
182+
183+
tx.finish(
184+
ui,
185+
format!(
186+
"point bookmark {} to commit {}",
187+
bookmark_name,
188+
target_commit.id().hex()
189+
),
190+
)?;
191+
Ok(())
192+
}

cli/src/commands/bookmark/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
mod bump;
1516
mod create;
1617
mod delete;
1718
mod forget;
@@ -32,6 +33,8 @@ use jj_lib::repo::Repo;
3233
use jj_lib::str_util::StringPattern;
3334
use jj_lib::view::View;
3435

36+
use self::bump::BookmarkBumpArgs;
37+
use self::bump::cmd_bookmark_bump;
3538
use self::create::BookmarkCreateArgs;
3639
use self::create::cmd_bookmark_create;
3740
use self::delete::BookmarkDeleteArgs;
@@ -84,6 +87,8 @@ pub enum BookmarkCommand {
8487
#[command(visible_alias("t"))]
8588
Track(BookmarkTrackArgs),
8689
Untrack(BookmarkUntrackArgs),
90+
#[command(visible_alias("b"))]
91+
Bump(BookmarkBumpArgs),
8792
}
8893

8994
pub fn cmd_bookmark(
@@ -92,6 +97,7 @@ pub fn cmd_bookmark(
9297
subcommand: &BookmarkCommand,
9398
) -> Result<(), CommandError> {
9499
match subcommand {
100+
BookmarkCommand::Bump(args) => cmd_bookmark_bump(ui, command, args),
95101
BookmarkCommand::Create(args) => cmd_bookmark_create(ui, command, args),
96102
BookmarkCommand::Delete(args) => cmd_bookmark_delete(ui, command, args),
97103
BookmarkCommand::Forget(args) => cmd_bookmark_forget(ui, command, args),

cli/tests/[email protected]

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
---
22
source: cli/tests/test_generate_md_cli_help.rs
3+
assertion_line: 45
34
description: "AUTO-GENERATED FILE, DO NOT EDIT. This cli reference is generated by a test as an `insta` snapshot. MkDocs includes this snapshot from docs/cli-reference.md."
45
---
56
<!-- BEGIN MARKDOWN-->
@@ -25,6 +26,7 @@ This document contains the help content for the `jj` command-line program.
2526
* [`jj bookmark set`↴](#jj-bookmark-set)
2627
* [`jj bookmark track`↴](#jj-bookmark-track)
2728
* [`jj bookmark untrack`↴](#jj-bookmark-untrack)
29+
* [`jj bookmark bump`↴](#jj-bookmark-bump)
2830
* [`jj commit`↴](#jj-commit)
2931
* [`jj config`↴](#jj-config)
3032
* [`jj config edit`↴](#jj-config-edit)
@@ -336,6 +338,7 @@ See [`jj help -k bookmarks`] for more information.
336338
* `set` — Create or update a bookmark to point to a certain commit
337339
* `track` — Start tracking given remote bookmarks
338340
* `untrack` — Stop tracking given remote bookmarks
341+
* `bump` — Move a bookmark to the latest non-empty descendant
339342

340343

341344

@@ -578,6 +581,33 @@ If you want to forget a local bookmark while also untracking the corresponding r
578581

579582

580583

584+
## `jj bookmark bump`
585+
586+
Move a bookmark to the latest non-empty descendant
587+
588+
Finds the closest bookmark on the specified revision or any of its ancestors, then moves that bookmark forward to the topologically latest non-empty descendant.
589+
590+
This is useful for advancing a bookmark after making several commits, without having to manually specify the bookmark name or target revision.
591+
592+
If multiple bookmarks exist on the same commit, the alphabetically first one is selected.
593+
594+
Example: After creating commits on top of a bookmarked commit, move the bookmark forward to the latest non-empty commit:
595+
596+
```shell $ jj bookmark bump ```
597+
598+
**Usage:** `jj bookmark bump [OPTIONS]`
599+
600+
**Command Alias:** `b`
601+
602+
###### **Options:**
603+
604+
* `-f`, `--from <REVSET>` — Revision to start searching for bookmarks from (searches ancestors too)
605+
606+
Default value: `@`
607+
* `-B`, `--allow-backwards` — Allow moving the bookmark backwards or sideways
608+
609+
610+
581611
## `jj commit`
582612

583613
Update the description and create a new change on top [default alias: ci]

0 commit comments

Comments
 (0)