// Copyright 2020-2023 The Jujutsu Authors//// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at//// https://www.apache.org/licenses/LICENSE-2.0//// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License. use std::io;use std::io::Write as _;use std::path::Path;use std::path::PathBuf;use std::sync::Arc; use indoc::writedoc;use itertools::Itertools as _;use jj_lib::file_util;use jj_lib::git;use jj_lib::git::GitImportOptions;use jj_lib::git::GitRefKind;use jj_lib::git::GitSettings;use jj_lib::git::parse_git_ref;use jj_lib::repo::ReadonlyRepo;use jj_lib::repo::Repo as _;use jj_lib::view::View;use jj_lib::workspace::Workspace; use super::ObjectHash;use super::RepoPresets;use super::write_repo_presets;use crate::cli_util::CommandHelper;use crate::cli_util::shell_quote;use crate::cli_util::start_repo_transaction;use crate::command_error::CommandError;use crate::command_error::cli_error;use crate::command_error::internal_error;use crate::command_error::user_error;use crate::command_error::user_error_with_message;use crate::commands::git::maybe_add_gitignore;use crate::config::ConfigEnv;use crate::formatter::FormatterExt as _;use crate::git_util::is_colocated_git_workspace;use crate::git_util::load_git_import_options;use crate::git_util::print_git_export_stats;use crate::git_util::print_git_import_stats_summary;use crate::ui::Ui; /// Create a new Git backed repo.////// To use an existing Git repository as the backing store, run `jj git init`/// from that repository's root directory. This is the usual way to start using/// Jujutsu in an existing Git checkout. See [colocation docs].////// If the setting `git.colocate` has the value `false`, then new repositories/// will not be colocated. Pass `--colocate` to override this.////// [colocation docs]:/// https://docs.jj-vcs.dev/latest/git-compatibility/#creating-a-repo-backed-by-an-existing-git-repo#[derive(clap::Args, Clone, Debug)]pub struct GitInitArgs { /// The destination directory where the `jj` repo will be created. /// If the directory does not exist, it will be created. /// If no directory is given, the current directory is used. #[arg(default_value = ".", value_hint = clap::ValueHint::DirPath)] destination: String, /// Colocate the workspace of the Jujutsu repo with the Git repo /// /// Specifies that the workspace of the `jj` repo should also be a valid /// `git` repo, allowing the use of both `jj` and `git` commands in the same /// directory. The workspace will contain a `.git` dir in the top-level, so /// regular Git tools will be able to operate on the repo. /// /// **This is the default** (and this option has no effect) unless the /// [`git.colocate` config] is set to `false`. /// /// The `--colocate` option is equivalent to `--git-repo=DESTINATION`. These /// options are mutually exclusive. /// /// [`git.colocate` config]: /// https://docs.jj-vcs.dev/latest/config/#default-colocation #[arg(long, conflicts_with = "git_repo")] colocate: bool, /// Disable colocation for the workspace of the Jujutsu repo with the Git /// repo /// /// Prevent Git tools that are unaware of `jj` and regular Git commands from /// operating on the repo. The Git repository that stores most of the repo /// data will be hidden inside a sub-directory of the `.jj` directory. /// /// See [colocation docs] for some minor advantages of non-colocated /// workspaces. /// /// [colocation docs]: /// https://docs.jj-vcs.dev/latest/git-compatibility/#colocated-jujutsugit-workspaces #[arg(long, conflicts_with = "colocate")] no_colocate: bool, /// Object hash algorithm for the newly created Git repository. /// /// Corresponds to `git init`'s `--object-format` option. If not given, the /// [git.object-hash config] determines the default value. /// /// Note that the object hash cannot currently be changed for an existing /// repo, and not all code forges support SHA-256 repositories yet. See also /// Git's [hash-function-transition] document for an in-depth explanation of /// the migration towards stronger hash functions. /// /// [git.object-hash config]: /// https://docs.jj-vcs.dev/latest/config/#default-object-hash-format /// [hash-function-transition]: /// https://git-scm.com/docs/hash-function-transition #[arg(long, value_enum, conflicts_with = "git_repo")] object_hash: Option<ObjectHash>, /// Specifies a path to an **existing** git repository to be /// used as the backing git repo for the newly created `jj` repo. /// /// If the specified `--git-repo` path is the same as `DESTINATION`, then a /// [colocated workspace] will be created, and both `jj` and `git` commands /// will work on the repo. The `git.colocate` setting is ignored in this /// case. /// /// If the specified `--git-repo` path is outside of the given /// `DESTINATION`, then the Jujutsa workspace will store a reference to the /// external Git repository (not a colocated workspace). This option is /// mutually exclusive with `--collate`. /// /// [colocated workspace]: /// https://docs.jj-vcs.dev/latest/git-compatibility/#colocated-jujutsugit-workspaces #[arg( long, conflicts_with_all = ["colocate", "object_hash"], value_hint = clap::ValueHint::DirPath, )] git_repo: Option<String>,} pub async fn cmd_git_init( ui: &mut Ui, command: &CommandHelper, args: &GitInitArgs,) -> Result<(), CommandError> { if command.global_args().no_integrate_operation { return Err(cli_error("--no-integrate-operation is not respected")); } if command.global_args().ignore_working_copy { return Err(cli_error("--ignore-working-copy is not respected")); } if command.global_args().at_operation.is_some() { return Err(cli_error("--at-op is not respected")); } let cwd = command.cwd(); let wc_path = cwd.join(&args.destination); let wc_path = file_util::create_or_reuse_dir(&wc_path) .and_then(|_| dunce::canonicalize(wc_path)) .map_err(|e| user_error_with_message("Failed to create workspace", e))?; let colocate = if command.settings().get_bool("git.colocate")? { !args.no_colocate } else { args.colocate }; let object_hash = args.object_hash.map_or_else( || command.settings().get::<ObjectHash>("git.object-hash"), Result::Ok, )?; do_init( ui, command, &wc_path, colocate, object_hash.into(), args.git_repo.as_deref(), ) .await?; let relative_wc_path = file_util::relative_path(cwd, &wc_path); writeln!( ui.status(), r#"Initialized repo in "{}""#, relative_wc_path.display() )?; Ok(())} async fn do_init( ui: &mut Ui, command: &CommandHelper, workspace_root: &Path, colocate: bool, object_hash: gix::hash::Kind, git_repo: Option<&str>,) -> Result<(), CommandError> { #[derive(Clone, Debug)] enum GitInitMode { Colocate, External(PathBuf), Internal, } let colocated_git_repo_path = workspace_root.join(".git"); let init_mode = if let Some(path_str) = git_repo { let mut git_repo_path = command.cwd().join(path_str); if !git_repo_path.ends_with(".git") { git_repo_path.push(".git"); // Undo if .git doesn't exist - likely a bare repo. if !git_repo_path.exists() { git_repo_path.pop(); } } GitInitMode::External(git_repo_path) } else if colocate { if colocated_git_repo_path.exists() { GitInitMode::External(colocated_git_repo_path) } else { GitInitMode::Colocate } } else { if colocated_git_repo_path.exists() { return Err(user_error( "Did not create a jj repo because there is an existing Git repo in this directory.", ) .hinted( "To create a repo backed by the existing Git repo, run `jj git init --colocate` \ instead.", )); } GitInitMode::Internal }; let (settings, config_env) = command.settings_for_new_workspace(ui, workspace_root)?; match &init_mode { GitInitMode::Colocate => { let (workspace, repo) = Workspace::init_colocated_git(&settings, workspace_root, object_hash).await?; let workspace_command = command.for_workable_repo(ui, workspace, repo)?; maybe_add_gitignore(&workspace_command)?; } GitInitMode::External(git_repo_path) => { let (workspace, repo) = Workspace::init_external_git(&settings, workspace_root, git_repo_path).await?; // Import refs first so all the reachable commits are indexed in // chronological order. let colocated = is_colocated_git_workspace(&workspace)?; let repo = init_git_refs(ui, repo, command.string_args(), &workspace, colocated).await?; let mut workspace_command = command.for_workable_repo(ui, workspace, repo)?; let git_repo = git::get_git_repo(workspace_command.repo().store())?; maybe_add_gitignore(&workspace_command)?; workspace_command.maybe_snapshot(ui).await?; maybe_set_repository_level_trunk_alias(ui, &git_repo, &config_env)?; if !workspace_command.working_copy_shared_with_git() { let mut tx = workspace_command.start_transaction(); if let Some(git_head_commit) = git::import_head_commit(tx.repo_mut()).await? { tx.check_out(&git_head_commit)?; } if tx.repo().has_changes() { tx.finish(ui, "import git head").await?; } } print_trackable_remote_bookmarks(ui, workspace_command.repo().view())?; if colocated && git_repo.kind() == gix::repository::Kind::LinkedWorkTree { writeln!( ui.warning_default(), "Initialized a new colocated jj repo inside a Git worktree." )?; writeln!( ui.hint_default(), "To add a workspace to an existing repository, use `jj workspace add` instead." )?; } } GitInitMode::Internal => { Workspace::init_internal_git(&settings, workspace_root, object_hash).await?; } } Ok(())} /// Imports branches and tags from the underlying Git repo, exports changes if/// the repo is colocated.////// This is similar to `WorkspaceCommandHelper::import_git_refs()`, but never/// moves the Git HEAD to the working copy parent.async fn init_git_refs( ui: &mut Ui, repo: Arc<ReadonlyRepo>, string_args: &[String], workspace: &Workspace, colocated: bool,) -> Result<Arc<ReadonlyRepo>, CommandError> { let git_settings = GitSettings::from_settings(repo.settings())?; let remote_settings = repo.settings().remote_settings()?; let import_options = GitImportOptions { // There should be no old refs to abandon, but enforce it. abandon_unreachable_commits: false, // There may be a large number of new commits. Don't record synthetic // predecessors. record_synthetic_predecessors: false, ..load_git_import_options(ui, &git_settings, &remote_settings)? }; let mut tx = start_repo_transaction(&repo, workspace.workspace_name(), string_args); let stats = git::import_refs(tx.repo_mut(), &import_options).await?; print_git_import_stats_summary(ui, &stats)?; if !tx.repo().has_changes() { return Ok(repo); } if colocated { // If remotes.<name>.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)?; } let repo = tx.commit("import git refs").await?; writeln!( ui.status(), "Done importing changes from the underlying Git repo." )?; Ok(repo)} // Set repository level `trunk()` alias to the default branch.// Checks "upstream" first, then "origin" as fallback.pub fn maybe_set_repository_level_trunk_alias( ui: &Ui, git_repo: &gix::Repository, config_env: &ConfigEnv,) -> Result<(), CommandError> { // Try "upstream" first, then fall back to "origin" for remote in ["upstream", "origin"] { let ref_name = format!("refs/remotes/{remote}/HEAD"); if let Some(reference) = git_repo .try_find_reference(&ref_name) .map_err(internal_error)? { // Found a HEAD reference for this remote. Even if we can't parse it, // we should stop here and not try other remotes because it doesn't // really make sense if "origin" were to be set as the default if we // know "upstream" exists. if let Some(reference_name) = reference.target().try_name() && let Some((GitRefKind::Bookmark, symbol)) = str::from_utf8(reference_name.as_bstr()) .ok() .and_then(|name| parse_git_ref(name.as_ref())) { // TODO: Can we assume the symbolic target points to the same remote? let symbol = symbol.name.to_remote_symbol(remote.as_ref()); write_repo_presets( ui, config_env, RepoPresets { remote: remote.as_ref(), fetch_bookmarks: None, fetch_tags: None, trunk: Some(symbol), }, )?; } return Ok(()); } } Ok(())} fn print_trackable_remote_bookmarks(ui: &Ui, view: &View) -> io::Result<()> { let remote_bookmark_symbols = 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_tracked()) .map(move |(remote, _)| name.to_remote_symbol(remote)) }) .collect_vec(); if remote_bookmark_symbols.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 symbol in &remote_bookmark_symbols { write!(formatter, " ")?; writeln!(formatter.labeled("bookmark"), "{symbol}")?; } writedoc!( formatter.labeled("hint").with_heading("Hint: "), " Run the following command to keep local bookmarks updated on future pulls: jj bookmark track {syms} ", syms = remote_bookmark_symbols .iter() .map(|s| shell_quote(&s.to_string()).into_owned()) .join(" ") )?; } Ok(())}
// Copyright 2020-2023 The Jujutsu Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::io;
use std::io::Write as _;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use indoc::writedoc;
use itertools::Itertools as _;
use jj_lib::file_util;
use jj_lib::git;
use jj_lib::git::GitImportOptions;
use jj_lib::git::GitRefKind;
use jj_lib::git::GitSettings;
use jj_lib::git::parse_git_ref;
use jj_lib::repo::ReadonlyRepo;
use jj_lib::repo::Repo as _;
use jj_lib::view::View;
use jj_lib::workspace::Workspace;
use super::ObjectHash;
use super::RepoPresets;
use super::write_repo_presets;
use crate::cli_util::CommandHelper;
use crate::cli_util::shell_quote;
use crate::cli_util::start_repo_transaction;
use crate::command_error::CommandError;
use crate::command_error::cli_error;
use crate::command_error::internal_error;
use crate::command_error::user_error;
use crate::command_error::user_error_with_message;
use crate::commands::git::maybe_add_gitignore;
use crate::config::ConfigEnv;
use crate::formatter::FormatterExt as _;
use crate::git_util::is_colocated_git_workspace;
use crate::git_util::load_git_import_options;
use crate::git_util::print_git_export_stats;
use crate::git_util::print_git_import_stats_summary;
use crate::ui::Ui;
/// Create a new Git backed repo.
///
/// To use an existing Git repository as the backing store, run `jj git init`
/// from that repository's root directory. This is the usual way to start using
/// Jujutsu in an existing Git checkout. See [colocation docs].
///
/// If the setting `git.colocate` has the value `false`, then new repositories
/// will not be colocated. Pass `--colocate` to override this.
///
/// [colocation docs]:
/// https://docs.jj-vcs.dev/latest/git-compatibility/#creating-a-repo-backed-by-an-existing-git-repo
#[derive(clap::Args, Clone, Debug)]
pub struct GitInitArgs {
/// The destination directory where the `jj` repo will be created.
/// If the directory does not exist, it will be created.
/// If no directory is given, the current directory is used.
#[arg(default_value = ".", value_hint = clap::ValueHint::DirPath)]
destination: String,
/// Colocate the workspace of the Jujutsu repo with the Git repo
///
/// Specifies that the workspace of the `jj` repo should also be a valid
/// `git` repo, allowing the use of both `jj` and `git` commands in the same
/// directory. The workspace will contain a `.git` dir in the top-level, so
/// regular Git tools will be able to operate on the repo.
///
/// **This is the default** (and this option has no effect) unless the
/// [`git.colocate` config] is set to `false`.
///
/// The `--colocate` option is equivalent to `--git-repo=DESTINATION`. These
/// options are mutually exclusive.
///
/// [`git.colocate` config]:
/// https://docs.jj-vcs.dev/latest/config/#default-colocation
#[arg(long, conflicts_with = "git_repo")]
colocate: bool,
/// Disable colocation for the workspace of the Jujutsu repo with the Git
/// repo
///
/// Prevent Git tools that are unaware of `jj` and regular Git commands from
/// operating on the repo. The Git repository that stores most of the repo
/// data will be hidden inside a sub-directory of the `.jj` directory.
///
/// See [colocation docs] for some minor advantages of non-colocated
/// workspaces.
///
/// [colocation docs]:
/// https://docs.jj-vcs.dev/latest/git-compatibility/#colocated-jujutsugit-workspaces
#[arg(long, conflicts_with = "colocate")]
no_colocate: bool,
/// Object hash algorithm for the newly created Git repository.
///
/// Corresponds to `git init`'s `--object-format` option. If not given, the
/// [git.object-hash config] determines the default value.
///
/// Note that the object hash cannot currently be changed for an existing
/// repo, and not all code forges support SHA-256 repositories yet. See also
/// Git's [hash-function-transition] document for an in-depth explanation of
/// the migration towards stronger hash functions.
///
/// [git.object-hash config]:
/// https://docs.jj-vcs.dev/latest/config/#default-object-hash-format
/// [hash-function-transition]:
/// https://git-scm.com/docs/hash-function-transition
#[arg(long, value_enum, conflicts_with = "git_repo")]
object_hash: Option<ObjectHash>,
/// Specifies a path to an **existing** git repository to be
/// used as the backing git repo for the newly created `jj` repo.
///
/// If the specified `--git-repo` path is the same as `DESTINATION`, then a
/// [colocated workspace] will be created, and both `jj` and `git` commands
/// will work on the repo. The `git.colocate` setting is ignored in this
/// case.
///
/// If the specified `--git-repo` path is outside of the given
/// `DESTINATION`, then the Jujutsa workspace will store a reference to the
/// external Git repository (not a colocated workspace). This option is
/// mutually exclusive with `--collate`.
///
/// [colocated workspace]:
/// https://docs.jj-vcs.dev/latest/git-compatibility/#colocated-jujutsugit-workspaces
#[arg(
long,
conflicts_with_all = ["colocate", "object_hash"],
value_hint = clap::ValueHint::DirPath,
)]
git_repo: Option<String>,
}
pub async fn cmd_git_init(
ui: &mut Ui,
command: &CommandHelper,
args: &GitInitArgs,
) -> Result<(), CommandError> {
if command.global_args().no_integrate_operation {
return Err(cli_error("--no-integrate-operation is not respected"));
}
if command.global_args().ignore_working_copy {
return Err(cli_error("--ignore-working-copy is not respected"));
}
if command.global_args().at_operation.is_some() {
return Err(cli_error("--at-op is not respected"));
}
let cwd = command.cwd();
let wc_path = cwd.join(&args.destination);
let wc_path = file_util::create_or_reuse_dir(&wc_path)
.and_then(|_| dunce::canonicalize(wc_path))
.map_err(|e| user_error_with_message("Failed to create workspace", e))?;
let colocate = if command.settings().get_bool("git.colocate")? {
!args.no_colocate
} else {
args.colocate
};
let object_hash = args.object_hash.map_or_else(
|| command.settings().get::<ObjectHash>("git.object-hash"),
Result::Ok,
)?;
do_init(
ui,
command,
&wc_path,
colocate,
object_hash.into(),
args.git_repo.as_deref(),
)
.await?;
let relative_wc_path = file_util::relative_path(cwd, &wc_path);
writeln!(
ui.status(),
r#"Initialized repo in "{}""#,
relative_wc_path.display()
)?;
Ok(())
}
async fn do_init(
ui: &mut Ui,
command: &CommandHelper,
workspace_root: &Path,
colocate: bool,
object_hash: gix::hash::Kind,
git_repo: Option<&str>,
) -> Result<(), CommandError> {
#[derive(Clone, Debug)]
enum GitInitMode {
Colocate,
External(PathBuf),
Internal,
}
let colocated_git_repo_path = workspace_root.join(".git");
let init_mode = if let Some(path_str) = git_repo {
let mut git_repo_path = command.cwd().join(path_str);
if !git_repo_path.ends_with(".git") {
git_repo_path.push(".git");
// Undo if .git doesn't exist - likely a bare repo.
if !git_repo_path.exists() {
git_repo_path.pop();
}
}
GitInitMode::External(git_repo_path)
} else if colocate {
if colocated_git_repo_path.exists() {
GitInitMode::External(colocated_git_repo_path)
} else {
GitInitMode::Colocate
}
} else {
if colocated_git_repo_path.exists() {
return Err(user_error(
"Did not create a jj repo because there is an existing Git repo in this directory.",
)
.hinted(
"To create a repo backed by the existing Git repo, run `jj git init --colocate` \
instead.",
));
}
GitInitMode::Internal
};
let (settings, config_env) = command.settings_for_new_workspace(ui, workspace_root)?;
match &init_mode {
GitInitMode::Colocate => {
let (workspace, repo) =
Workspace::init_colocated_git(&settings, workspace_root, object_hash).await?;
let workspace_command = command.for_workable_repo(ui, workspace, repo)?;
maybe_add_gitignore(&workspace_command)?;
}
GitInitMode::External(git_repo_path) => {
let (workspace, repo) =
Workspace::init_external_git(&settings, workspace_root, git_repo_path).await?;
// Import refs first so all the reachable commits are indexed in
// chronological order.
let colocated = is_colocated_git_workspace(&workspace)?;
let repo =
init_git_refs(ui, repo, command.string_args(), &workspace, colocated).await?;
let mut workspace_command = command.for_workable_repo(ui, workspace, repo)?;
let git_repo = git::get_git_repo(workspace_command.repo().store())?;
maybe_add_gitignore(&workspace_command)?;
workspace_command.maybe_snapshot(ui).await?;
maybe_set_repository_level_trunk_alias(ui, &git_repo, &config_env)?;
if !workspace_command.working_copy_shared_with_git() {
let mut tx = workspace_command.start_transaction();
if let Some(git_head_commit) = git::import_head_commit(tx.repo_mut()).await? {
tx.check_out(&git_head_commit)?;
}
if tx.repo().has_changes() {
tx.finish(ui, "import git head").await?;
}
}
print_trackable_remote_bookmarks(ui, workspace_command.repo().view())?;
if colocated && git_repo.kind() == gix::repository::Kind::LinkedWorkTree {
writeln!(
ui.warning_default(),
"Initialized a new colocated jj repo inside a Git worktree."
)?;
writeln!(
ui.hint_default(),
"To add a workspace to an existing repository, use `jj workspace add` instead."
)?;
}
}
GitInitMode::Internal => {
Workspace::init_internal_git(&settings, workspace_root, object_hash).await?;
}
}
Ok(())
}
/// Imports branches and tags from the underlying Git repo, exports changes if
/// the repo is colocated.
///
/// This is similar to `WorkspaceCommandHelper::import_git_refs()`, but never
/// moves the Git HEAD to the working copy parent.
async fn init_git_refs(
ui: &mut Ui,
repo: Arc<ReadonlyRepo>,
string_args: &[String],
workspace: &Workspace,
colocated: bool,
) -> Result<Arc<ReadonlyRepo>, CommandError> {
let git_settings = GitSettings::from_settings(repo.settings())?;
let remote_settings = repo.settings().remote_settings()?;
let import_options = GitImportOptions {
// There should be no old refs to abandon, but enforce it.
abandon_unreachable_commits: false,
// There may be a large number of new commits. Don't record synthetic
// predecessors.
record_synthetic_predecessors: false,
..load_git_import_options(ui, &git_settings, &remote_settings)?
};
let mut tx = start_repo_transaction(&repo, workspace.workspace_name(), string_args);
let stats = git::import_refs(tx.repo_mut(), &import_options).await?;
print_git_import_stats_summary(ui, &stats)?;
if !tx.repo().has_changes() {
return Ok(repo);
}
if colocated {
// If remotes.<name>.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)?;
}
let repo = tx.commit("import git refs").await?;
writeln!(
ui.status(),
"Done importing changes from the underlying Git repo."
)?;
Ok(repo)
}
// Set repository level `trunk()` alias to the default branch.
// Checks "upstream" first, then "origin" as fallback.
pub fn maybe_set_repository_level_trunk_alias(
ui: &Ui,
git_repo: &gix::Repository,
config_env: &ConfigEnv,
) -> Result<(), CommandError> {
// Try "upstream" first, then fall back to "origin"
for remote in ["upstream", "origin"] {
let ref_name = format!("refs/remotes/{remote}/HEAD");
if let Some(reference) = git_repo
.try_find_reference(&ref_name)
.map_err(internal_error)?
{
// Found a HEAD reference for this remote. Even if we can't parse it,
// we should stop here and not try other remotes because it doesn't
// really make sense if "origin" were to be set as the default if we
// know "upstream" exists.
if let Some(reference_name) = reference.target().try_name()
&& let Some((GitRefKind::Bookmark, symbol)) =
str::from_utf8(reference_name.as_bstr())
.ok()
.and_then(|name| parse_git_ref(name.as_ref()))
{
// TODO: Can we assume the symbolic target points to the same remote?
let symbol = symbol.name.to_remote_symbol(remote.as_ref());
write_repo_presets(
ui,
config_env,
RepoPresets {
remote: remote.as_ref(),
fetch_bookmarks: None,
fetch_tags: None,
trunk: Some(symbol),
},
)?;
}
return Ok(());
}
}
Ok(())
}
fn print_trackable_remote_bookmarks(ui: &Ui, view: &View) -> io::Result<()> {
let remote_bookmark_symbols = 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_tracked())
.map(move |(remote, _)| name.to_remote_symbol(remote))
})
.collect_vec();
if remote_bookmark_symbols.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 symbol in &remote_bookmark_symbols {
write!(formatter, " ")?;
writeln!(formatter.labeled("bookmark"), "{symbol}")?;
}
writedoc!(
formatter.labeled("hint").with_heading("Hint: "),
"
Run the following command to keep local bookmarks updated on future pulls:
jj bookmark track {syms}
",
syms = remote_bookmark_symbols
.iter()
.map(|s| shell_quote(&s.to_string()).into_owned())
.join(" ")
)?;
}
Ok(())
}