Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
* New `jj tag set`/`delete` commands to create/update/delete tags locally.
Updated tags will be exported to Git as lightweight tags.

* A new config option `git.auto-track-bookmarks` can be set to a string
pattern. New bookmarks that match the pattern will automatically be tracked.
See <https://jj-vcs.github.io/jj/latest/config/#automatic-tracking-of-bookmarks>.

### Fixed bugs

* `jj metaedit --author-timestamp` twice with the same value no longer
Expand Down
51 changes: 0 additions & 51 deletions cli/src/cli_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ use std::path::Path;
use std::path::PathBuf;
use std::rc::Rc;
use std::slice;
use std::str::FromStr;
use std::sync::Arc;
use std::time::SystemTime;

Expand Down Expand Up @@ -2997,56 +2996,6 @@ impl DiffSelector {
}
}

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

impl FromStr for RemoteBookmarkNamePattern {
type Err = String;

fn from_str(src: &str) -> Result<Self, Self::Err> {
// 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
.split_once(':')
.map_or((None, src), |(kind, pat)| (Some(kind), pat));
let to_pattern = |pat: &str| {
if let Some(kind) = maybe_kind {
StringPattern::from_str_kind(pat, kind).map_err(|err| err.to_string())
} else {
Ok(StringPattern::exact(pat))
}
};
// 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(Self {
bookmark: to_pattern(bookmark)?,
remote: to_pattern(remote)?,
})
}
}

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

impl fmt::Display for RemoteBookmarkNamePattern {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// TODO: use revset::format_remote_symbol() if FromStr is migrated to
// the revset parser.
let Self { bookmark, remote } = self;
write!(f, "{bookmark}@{remote}")
}
}

/// Computes the location (new parents and new children) to place commits.
///
/// The `destination` argument is mutually exclusive to the `insert_after` and
Expand Down
18 changes: 18 additions & 0 deletions cli/src/commands/bookmark/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use itertools::Itertools as _;
use jj_lib::object_id::ObjectId as _;
use jj_lib::op_store::RefTarget;
use jj_lib::ref_name::RefNameBuf;
use jj_lib::repo::Repo as _;

use crate::cli_util::CommandHelper;
use crate::cli_util::RevisionArg;
Expand Down Expand Up @@ -81,9 +82,26 @@ pub fn cmd_bookmark_create(
}

let mut tx = workspace_command.start_transaction();
let git_settings = tx.settings().git_settings()?;
let remotes = tx
.repo()
.view()
.remote_views_matching(&git_settings.auto_track_bookmarks.remote)
.map(|(remote, _)| remote.to_owned())
.collect_vec();
for name in bookmark_names {
tx.repo_mut()
.set_local_bookmark_target(name, RefTarget::normal(target_commit.id().clone()));
if git_settings
.auto_track_bookmarks
.bookmark
.is_match(name.as_str())
{
for remote in &remotes {
tx.repo_mut()
.track_remote_bookmark(name.to_remote_symbol(remote));
}
}
}

if let Some(mut formatter) = ui.status_formatter() {
Expand Down
2 changes: 1 addition & 1 deletion cli/src/commands/bookmark/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ use jj_lib::op_store::RemoteRef;
use jj_lib::ref_name::RefName;
use jj_lib::ref_name::RemoteRefSymbol;
use jj_lib::repo::Repo;
use jj_lib::str_util::RemoteBookmarkNamePattern;
use jj_lib::str_util::StringPattern;
use jj_lib::view::View;

Expand All @@ -51,7 +52,6 @@ use self::track::cmd_bookmark_track;
use self::untrack::BookmarkUntrackArgs;
use self::untrack::cmd_bookmark_untrack;
use crate::cli_util::CommandHelper;
use crate::cli_util::RemoteBookmarkNamePattern;
use crate::command_error::CommandError;
use crate::command_error::user_error;
use crate::ui::Ui;
Expand Down
18 changes: 18 additions & 0 deletions cli/src/commands/bookmark/set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use itertools::Itertools as _;
use jj_lib::object_id::ObjectId as _;
use jj_lib::op_store::RefTarget;
use jj_lib::ref_name::RefNameBuf;
use jj_lib::repo::Repo as _;

use super::is_fast_forward;
use crate::cli_util::CommandHelper;
Expand Down Expand Up @@ -90,11 +91,28 @@ pub fn cmd_bookmark_set(
}

let mut tx = workspace_command.start_transaction();
let git_settings = tx.settings().git_settings()?;
let remotes = tx
.repo()
.view()
.remote_views_matching(&git_settings.auto_track_bookmarks.remote)
.map(|(remote, _)| remote.to_owned())
.collect_vec();
for bookmark_name in bookmark_names {
tx.repo_mut().set_local_bookmark_target(
bookmark_name,
RefTarget::normal(target_commit.id().clone()),
);
if git_settings
.auto_track_bookmarks
.bookmark
.is_match(bookmark_name.as_str())
{
for remote in &remotes {
tx.repo_mut()
.track_remote_bookmark(bookmark_name.to_remote_symbol(remote));
}
}
}

if let Some(mut formatter) = ui.status_formatter() {
Expand Down
2 changes: 1 addition & 1 deletion cli/src/commands/bookmark/track.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ use std::rc::Rc;

use clap_complete::ArgValueCandidates;
use itertools::Itertools as _;
use jj_lib::str_util::RemoteBookmarkNamePattern;

use super::find_trackable_remote_bookmarks;
use crate::cli_util::CommandHelper;
use crate::cli_util::RemoteBookmarkNamePattern;
use crate::command_error::CommandError;
use crate::commit_templater::CommitRef;
use crate::complete;
Expand Down
2 changes: 1 addition & 1 deletion cli/src/commands/bookmark/untrack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@

use clap_complete::ArgValueCandidates;
use itertools::Itertools as _;
use jj_lib::str_util::RemoteBookmarkNamePattern;

use super::find_trackable_remote_bookmarks;
use crate::cli_util::CommandHelper;
use crate::cli_util::RemoteBookmarkNamePattern;
use crate::command_error::CommandError;
use crate::complete;
use crate::ui::Ui;
Expand Down
4 changes: 2 additions & 2 deletions cli/src/commands/git/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,8 +227,8 @@ fn init_git_refs(
return Ok(repo);
}
if colocated {
// If git.auto-local-bookmark = true, local bookmarks could be created for
// the imported remote branches.
// If git.auto-local-bookmark = true or git.auto-track-bookmarks is set,
// local bookmarks could be created for the imported remote branches.
let stats = git::export_refs(tx.repo_mut())?;
print_git_export_stats(ui, &stats)?;
}
Expand Down
5 changes: 5 additions & 0 deletions cli/src/config-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,11 @@
"description": "Whether jj creates a local bookmark with the same name when it imports a remote-tracking branch from git. See https://jj-vcs.github.io/jj/latest/config/#automatic-local-bookmark-creation",
"default": false
},
"auto-track-bookmarks": {
"type": "string",
"description": "A string pattern that determines which bookmarks will automatically be put in the tracked state. The default matches nothing. This applies to both newly created local bookmarks as well as remote-tracking branches imported from git. See https://jj-vcs.github.io/jj/latest/config/#automatic-tracking-of-bookmarks",
"default": "@"
},
"abandon-unreachable-commits": {
"type": "boolean",
"description": "Whether jj should abandon commits that became unreachable in Git.",
Expand Down
49 changes: 49 additions & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -1530,6 +1530,55 @@ jj bookmark delete gh-pages
jj bookmark untrack gh-pages@upstream
```

### Automatic tracking of bookmarks

You can configure a string pattern of new bookmarks to track automatically,
for example:

```toml
[git]
auto-track-bookmarks = "glob:*@*"
```

This will simply track all bookmarks, local or remote. There are various
reasons to restrict which bookmarks to track:

When collaborating with other people via the same remote, you may not want to
track all the bookmarks of your collaborators. Similarly, you may not want to
push all your bookmarks to the remote at all. Some may only be intended for
local use. Many people use a "personal prefix" in their bookmark names. This
marks a bookmark as belonging to one person, making it possible to track and
push only bookmarks with that prefix. For example, Alice may set her
configuration like so:

```toml
[git]
auto-track-bookmarks = "glob:alice/*@*"
```

That way, bookmarks pushed by other people (who probably use a different prefix
or none at all) are not tracked automatically. At the same time, Alice can
create bookmarks without the prefix for local-only use. You can also configure
Jujutsu to use your prefix for generated bookmark names, see the section
["Generated bookmark names on push"](#generated-bookmark-names-on-push).

Another reason to restrict the bookmarks to track may be that you're not
collaborating with people using the same remote, but different ones. For example,
if you make a fork on GitHub, your fork will usually be called "origin", while
the repository you forked from is usually called "upstream". In that case,
you may only want to track bookmarks with the remote "origin". The following
configuration can be used to achieve that:

```toml
[git]
auto-track-bookmarks = "glob:*@origin"
```

Lastly, you may be working on different repositories with different conventions
and needs. In that case, it can be handy to apply different configuration to
different (groups of) repositories. Read about how to do that in the section
["Conditional variables"](conditional-variables).

### Automatic local bookmark creation on `jj git clone`

When cloning a new Git repository, `jj` by default creates a local bookmark
Expand Down
1 change: 1 addition & 0 deletions lib/src/config/misc.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ register-snapshot-trigger = false
[git]
abandon-unreachable-commits = true
auto-local-bookmark = false
auto-track-bookmarks = "@" # matches nothing
executable-path = "git"
write-change-id-header = true
colocate = true
Expand Down
12 changes: 11 additions & 1 deletion lib/src/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -744,7 +744,17 @@ fn default_remote_ref_state_for(
) -> RemoteRefState {
match kind {
GitRefKind::Bookmark => {
if symbol.remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO || git_settings.auto_local_bookmark {
if symbol.remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO
|| git_settings.auto_local_bookmark
|| (git_settings
.auto_track_bookmarks
.bookmark
.is_match(symbol.name.as_str())
&& git_settings
.auto_track_bookmarks
.remote
.is_match(symbol.remote.as_str()))
{
RemoteRefState::Tracked
} else {
RemoteRefState::New
Expand Down
8 changes: 8 additions & 0 deletions lib/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ use crate::config::StackedConfig;
use crate::config::ToConfigNamePath;
use crate::fmt_util::binary_prefix;
use crate::signing::SignBehavior;
use crate::str_util::RemoteBookmarkNamePattern;

#[derive(Debug, Clone)]
pub struct UserSettings {
Expand All @@ -59,6 +60,7 @@ struct UserSettingsData {
#[derive(Debug, Clone)]
pub struct GitSettings {
pub auto_local_bookmark: bool,
pub auto_track_bookmarks: RemoteBookmarkNamePattern,
pub abandon_unreachable_commits: bool,
pub executable_path: PathBuf,
pub write_change_id_header: bool,
Expand All @@ -69,6 +71,12 @@ impl GitSettings {
pub fn from_settings(settings: &UserSettings) -> Result<Self, ConfigGetError> {
Ok(Self {
auto_local_bookmark: settings.get_bool("git.auto-local-bookmark")?,
auto_track_bookmarks: settings.get_value_with("git.auto-track-bookmarks", |value| {
value
.as_str()
.ok_or_else(|| "expected a string".to_string())?
.parse()
})?,
abandon_unreachable_commits: settings.get_bool("git.abandon-unreachable-commits")?,
executable_path: settings.get("git.executable-path")?,
write_change_id_header: settings.get("git.write-change-id-header")?,
Expand Down
56 changes: 56 additions & 0 deletions lib/src/str_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use std::collections::BTreeMap;
use std::fmt;
use std::fmt::Debug;
use std::ops::Deref;
use std::str::FromStr;

use bstr::ByteSlice as _;
use either::Either;
Expand Down Expand Up @@ -382,6 +383,61 @@ impl fmt::Display for StringPattern {
}
}

/// Pattern to be tested against a bookmark name and its remote.
#[derive(Clone, Debug)]
pub struct RemoteBookmarkNamePattern {
/// The pattern to be tested against the bookmark name.
pub bookmark: StringPattern,
/// The pattern to be tested against the remote.
pub remote: StringPattern,
}

impl FromStr for RemoteBookmarkNamePattern {
type Err = String;

fn from_str(src: &str) -> Result<Self, Self::Err> {
// 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
.split_once(':')
.map_or((None, src), |(kind, pat)| (Some(kind), pat));
let to_pattern = |pat: &str| {
if let Some(kind) = maybe_kind {
StringPattern::from_str_kind(pat, kind).map_err(|err| err.to_string())
} else {
Ok(StringPattern::exact(pat))
}
};
// 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(Self {
bookmark: to_pattern(bookmark)?,
remote: to_pattern(remote)?,
})
}
}

impl RemoteBookmarkNamePattern {
/// Returns true if both the bookmark pattern and remote pattern matches
/// input strings exactly.
pub fn is_exact(&self) -> bool {
self.bookmark.is_exact() && self.remote.is_exact()
}
}

impl fmt::Display for RemoteBookmarkNamePattern {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// TODO: use revset::format_remote_symbol() if FromStr is migrated to
// the revset parser.
let Self { bookmark, remote } = self;
write!(f, "{bookmark}@{remote}")
}
}

#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
Expand Down