Skip to content

Commit

Permalink
git init: add option to track local bookmarks
Browse files Browse the repository at this point in the history
Uses the same list of bookmarks that is otherwise printed to the user
as local bookmarks not associated with their remote. Essentially, just
automatically runs the hinted `jj bookmark track ...`.

If a bookmark is not at the same position as a given remote, will skip
tracking that remote.
  • Loading branch information
bryceberger committed Jan 18, 2025
1 parent 83d40d2 commit cea7fe7
Show file tree
Hide file tree
Showing 6 changed files with 279 additions and 42 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
can be customized using `format_detailed_cryptographic_signature(signature)` and
`format_short_cryptographic_signature(signature)`.

* New `git.init-track-local-bookmarks` config option to automatically set local
bookmarks as tracking remote bookmarks during `jj git init --colocate`.

### Fixed bugs

* Fixed diff selection by external tools with `jj split`/`commit -i FILESETS`.
Expand Down
35 changes: 0 additions & 35 deletions cli/src/cli_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,6 @@ use jj_lib::settings::HumanByteSize;
use jj_lib::settings::UserSettings;
use jj_lib::str_util::StringPattern;
use jj_lib::transaction::Transaction;
use jj_lib::view::View;
use jj_lib::working_copy;
use jj_lib::working_copy::CheckoutOptions;
use jj_lib::working_copy::CheckoutStats;
Expand Down Expand Up @@ -2755,40 +2754,6 @@ pub fn print_unmatched_explicit_paths<'a>(
Ok(())
}

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_bookmark_names.is_empty() {
return Ok(());
}

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

pub fn update_working_copy(
repo: &Arc<ReadonlyRepo>,
workspace: &mut Workspace,
Expand Down
147 changes: 140 additions & 7 deletions cli/src/commands/git/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,28 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use std::io;
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;

use itertools::Itertools;
use jj_lib::file_util;
use jj_lib::git;
use jj_lib::git::parse_git_ref;
use jj_lib::git::RefName;
use jj_lib::op_store::BookmarkTarget;
use jj_lib::op_store::RefTarget;
use jj_lib::op_store::RemoteRef;
use jj_lib::repo::MutableRepo;
use jj_lib::repo::ReadonlyRepo;
use jj_lib::repo::Repo;
use jj_lib::revset::RevsetExpression;
use jj_lib::view::View;
use jj_lib::workspace::Workspace;

use super::write_repository_level_trunk_alias;
use crate::cli_util::print_trackable_remote_bookmarks;
use crate::cli_util::start_repo_transaction;
use crate::cli_util::CommandHelper;
use crate::cli_util::WorkspaceCommandHelper;
Expand All @@ -35,6 +42,7 @@ use crate::command_error::user_error_with_hint;
use crate::command_error::user_error_with_message;
use crate::command_error::CommandError;
use crate::commands::git::maybe_add_gitignore;
use crate::formatter::Formatter;
use crate::git_util::get_git_repo;
use crate::git_util::is_colocated_git_workspace;
use crate::git_util::print_failed_git_export;
Expand Down Expand Up @@ -172,18 +180,23 @@ fn do_init(
maybe_add_gitignore(&workspace_command)?;
workspace_command.maybe_snapshot(ui)?;
maybe_set_repository_level_trunk_alias(ui, &workspace_command)?;
if !workspace_command.working_copy_shared_with_git() {
let mut tx = workspace_command.start_transaction();
let working_copy_shared_with_git = workspace_command.working_copy_shared_with_git();
let mut tx = workspace_command.start_transaction();
if !working_copy_shared_with_git {
jj_lib::git::import_head(tx.repo_mut())?;
if let Some(git_head_id) = tx.repo().view().git_head().as_normal().cloned() {
let git_head_commit = tx.repo().store().get_commit(&git_head_id)?;
tx.check_out(&git_head_commit)?;
}
if tx.repo().has_changes() {
tx.finish(ui, "import git head")?;
}
}
print_trackable_remote_bookmarks(ui, workspace_command.repo().view())?;
if settings.get_bool("git.init-track-local-bookmarks")? {
track_trackable_remote_bookmarks(ui, tx.repo_mut())?;
} else {
print_trackable_remote_bookmarks(ui, tx.repo().view())?;
}
if tx.repo().has_changes() {
tx.finish(ui, "import git head")?;
}
}
GitInitMode::Internal => {
Workspace::init_internal_git(&settings, workspace_root)?;
Expand Down Expand Up @@ -252,3 +265,123 @@ pub fn maybe_set_repository_level_trunk_alias(

Ok(())
}

fn present_local_bookmarks(view: &View) -> impl Iterator<Item = (&str, BookmarkTarget)> {
view.bookmarks()
.filter(|(_, bookmark_target)| bookmark_target.local_target.is_present())
}

fn print_trackable_remote_bookmarks(ui: &Ui, view: &View) -> io::Result<()> {
let remote_bookmark_names = present_local_bookmarks(view)
.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_bookmark_names.is_empty() {
return Ok(());
}

if let Some(mut formatter) = ui.status_formatter() {
print_skipped_tracking(formatter.as_mut(), &remote_bookmark_names)?;
}
Ok(())
}

fn track_trackable_remote_bookmarks(
ui: &mut Ui,
repo: &mut MutableRepo,
) -> Result<(), CommandError> {
let present_local_bookmarks = present_local_bookmarks(repo.view());
let (remote_bookmarks, skipped_tracking): (Vec<_>, Vec<_>) = present_local_bookmarks
.flat_map(|(name, bookmark_target)| {
let repo: &_ = repo;
let local_target = bookmark_target.local_target;
bookmark_target
.remote_refs
.into_iter()
.filter_map(|(remote_name, remote_ref)| {
should_track(repo, name, local_target, remote_name, remote_ref)
})
})
.partition_result();

if let Some(mut formatter) = ui.status_formatter() {
if !remote_bookmarks.is_empty() {
writeln!(formatter, "Tracking the following remote bookmarks:")?;
for (name, remote_name) in &remote_bookmarks {
write!(formatter, " ")?;
writeln!(formatter.labeled("bookmark"), "{name}@{remote_name}")?;
}
}

print_skipped_tracking(formatter.as_mut(), &skipped_tracking)?;
}

for (name, remote_name) in &remote_bookmarks {
repo.track_remote_bookmark(name, remote_name);
}

Ok(())
}

fn print_skipped_tracking(
formatter: &mut dyn Formatter,
remote_bookmark_names: &[String],
) -> Result<(), io::Error> {
if remote_bookmark_names.is_empty() {
return Ok(());
}

writeln!(
formatter.labeled("hint").with_heading("Hint: "),
"The following remote bookmarks aren't associated with the existing local bookmarks:"
)?;
for full_name in remote_bookmark_names {
write!(formatter, " ")?;
writeln!(formatter.labeled("bookmark"), "{full_name}")?;
}
writeln!(
formatter.labeled("hint").with_heading("Hint: "),
"Run `jj bookmark track {names}` to keep local bookmarks updated on future pulls.",
names = remote_bookmark_names.join(" "),
)?;
Ok(())
}

/// Returns:
/// `None`: already tracking, or name@git
/// `Some(Err("name@remote"))`: should skip tracking, local is behind
/// `Some(Ok((name, remote)))`: should track
fn should_track(
repo: &dyn Repo,
name: &str,
local_target: &RefTarget,
remote_name: &str,
remote_ref: &RemoteRef,
) -> Option<Result<(String, String), String>> {
if remote_ref.is_tracking() || jj_lib::git::is_special_git_remote(remote_name) {
return None;
}

let remote_added = remote_ref.target.added_ids().cloned().collect();
let Ok(ahead_of_remote) = RevsetExpression::commits(remote_added)
.descendants()
.evaluate(repo)
.map(|r| r.containing_fn())
else {
return Some(Err(format!("{name}@{remote_name}")));
};

if local_target
.added_ids()
.any(|add| ahead_of_remote(add).unwrap_or(false))
{
Some(Ok((name.to_owned(), remote_name.to_owned())))
} else {
Some(Err(format!("{name}@{remote_name}")))
}
}
5 changes: 5 additions & 0 deletions cli/src/config-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,11 @@
"description": "Whether jj should abandon commits that became unreachable in Git.",
"default": true
},
"init-track-local-bookmarks": {
"type": "boolean",
"description": "Whether jj should track bookmarks that were already tracked when initializing a colocated repository.",
"default": false
},
"push-bookmark-prefix": {
"type": "string",
"description": "Prefix used when pushing a bookmark based on a change ID",
Expand Down
1 change: 1 addition & 0 deletions cli/src/config/misc.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ private-commits = "none()"
push-bookmark-prefix = "push-"
push-new-bookmarks = false
sign-on-push = false
init-track-local-bookmarks = false

[ui]
# TODO: delete ui.allow-filesets in jj 0.26+
Expand Down
Loading

0 comments on commit cea7fe7

Please sign in to comment.