Skip to content

Commit

Permalink
cli: add API and branch subcommand to track/untrack remote branches
Browse files Browse the repository at this point in the history
This patch adds MutableRepo::track_remote_branch() as we'll probably need to
track the default branch on "jj git clone". untrack_remote_branch() is also
added for consistency.
  • Loading branch information
yuja committed Oct 16, 2023
1 parent f74d793 commit 9cafff8
Show file tree
Hide file tree
Showing 6 changed files with 392 additions and 10 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
tracking branches.
* Otherwise, the remote branches are non-tracking branches.

If the deduced tracking flags are wrong, use `jj branch track`/`untrack`
commands to fix them up.

See [automatic local branch creation](docs/config.md#automatic-local-branch-creation)
for details.

Expand Down
113 changes: 113 additions & 0 deletions cli/src/commands/branch.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use std::collections::{BTreeSet, HashSet};
use std::fmt;
use std::io::Write as _;
use std::str::FromStr;

use clap::builder::NonEmptyStringValueParser;
use itertools::Itertools;
Expand Down Expand Up @@ -31,6 +33,8 @@ pub enum BranchSubcommand {
List(BranchListArgs),
#[command(visible_alias("s"))]
Set(BranchSetArgs),
Track(BranchTrackArgs),
Untrack(BranchUntrackArgs),
}

/// Create a new branch.
Expand Down Expand Up @@ -107,6 +111,57 @@ pub struct BranchSetArgs {
pub names: Vec<String>,
}

/// Start tracking given remote branches
///
/// A tracking remote branch will be imported as a local branch of the same
/// name. Changes to it will propagate to the existing local branch on future
/// pulls.
#[derive(clap::Args, Clone, Debug)]
pub struct BranchTrackArgs {
/// Remote branches to track
#[arg(required = true)]
pub names: Vec<RemoteBranchName>,
}

/// Stop tracking given remote branches
///
/// A non-tracking remote branch is just a pointer to the last-fetched remote
/// branch. It won't be imported as a local branch on future pulls.
#[derive(clap::Args, Clone, Debug)]
pub struct BranchUntrackArgs {
/// Remote branches to untrack
#[arg(required = true)]
pub names: Vec<RemoteBranchName>,
}

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

impl FromStr for RemoteBranchName {
type Err = &'static str;

fn from_str(s: &str) -> Result<Self, Self::Err> {
// TODO: maybe reuse revset parser to handle branch/remote name containing @
let (branch, remote) = s
.rsplit_once('@')
.ok_or("remote branch must be specified in branch@remote form")?;
Ok(RemoteBranchName {
branch: branch.to_owned(),
remote: remote.to_owned(),
})
}
}

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

pub fn cmd_branch(
ui: &mut Ui,
command: &CommandHelper,
Expand All @@ -117,6 +172,8 @@ pub fn cmd_branch(
BranchSubcommand::Set(sub_args) => cmd_branch_set(ui, command, sub_args),
BranchSubcommand::Delete(sub_args) => cmd_branch_delete(ui, command, sub_args),
BranchSubcommand::Forget(sub_args) => cmd_branch_forget(ui, command, sub_args),
BranchSubcommand::Track(sub_args) => cmd_branch_track(ui, command, sub_args),
BranchSubcommand::Untrack(sub_args) => cmd_branch_untrack(ui, command, sub_args),
BranchSubcommand::List(sub_args) => cmd_branch_list(ui, command, sub_args),
}
}
Expand Down Expand Up @@ -310,6 +367,62 @@ fn cmd_branch_forget(
Ok(())
}

fn cmd_branch_track(
ui: &mut Ui,
command: &CommandHelper,
args: &BranchTrackArgs,
) -> Result<(), CommandError> {
let mut workspace_command = command.workspace_helper(ui)?;
let view = workspace_command.repo().view();
for name in &args.names {
let remote_ref = view.get_remote_branch(&name.branch, &name.remote);
if remote_ref.is_absent() {
return Err(user_error(format!("No such remote branch: {name}")));
}
if remote_ref.is_tracking() {
return Err(user_error(format!("Remote branch already tracked: {name}")));
}
}
let mut tx = workspace_command
.start_transaction(&format!("track remote {}", make_branch_term(&args.names)));
for name in &args.names {
tx.mut_repo()
.track_remote_branch(&name.branch, &name.remote);
}
tx.finish(ui)?;
Ok(())
}

fn cmd_branch_untrack(
ui: &mut Ui,
command: &CommandHelper,
args: &BranchUntrackArgs,
) -> Result<(), CommandError> {
let mut workspace_command = command.workspace_helper(ui)?;
let view = workspace_command.repo().view();
for name in &args.names {
if name.remote == git::REMOTE_NAME_FOR_LOCAL_GIT_REPO {
// This restriction can be lifted if we want to support untracked @git branches.
return Err(user_error("Git-tracking branch cannot be untracked"));
}
let remote_ref = view.get_remote_branch(&name.branch, &name.remote);
if remote_ref.is_absent() {
return Err(user_error(format!("No such remote branch: {name}")));
}
if !remote_ref.is_tracking() {
return Err(user_error(format!("Remote branch not tracked yet: {name}")));
}
}
let mut tx = workspace_command
.start_transaction(&format!("untrack remote {}", make_branch_term(&args.names)));
for name in &args.names {
tx.mut_repo()
.untrack_remote_branch(&name.branch, &name.remote);
}
tx.finish(ui)?;
Ok(())
}

fn cmd_branch_list(
ui: &mut Ui,
command: &CommandHelper,
Expand Down
13 changes: 4 additions & 9 deletions cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use std::fmt::Debug;
use std::io::{BufRead, Read, Seek, SeekFrom, Write};
use std::path::Path;
use std::sync::Arc;
use std::{fs, io};
use std::{fmt, fs, io};

use clap::builder::NonEmptyStringValueParser;
use clap::parser::ValueSource;
Expand Down Expand Up @@ -3658,15 +3658,10 @@ fn cmd_backout(
Ok(())
}

fn make_branch_term(branch_names: &[impl AsRef<str>]) -> String {
fn make_branch_term(branch_names: &[impl fmt::Display]) -> String {
match branch_names {
[branch_name] => format!("branch {}", branch_name.as_ref()),
branch_names => {
format!(
"branches {}",
branch_names.iter().map(AsRef::as_ref).join(", ")
)
}
[branch_name] => format!("branch {}", branch_name),
branch_names => format!("branches {}", branch_names.iter().join(", ")),
}
}

Expand Down
Loading

0 comments on commit 9cafff8

Please sign in to comment.