Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement tracking/non-tracking remote branches for import/fetch #2384

Merged
merged 5 commits into from
Oct 16, 2023
Merged
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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* `jj config set` now interprets the value as TOML also if it's a valid TOML
array or table. For example, `jj config set --user 'aliases.n' '["new"]'`

* Remote branches now have tracking or non-tracking flags. The
`git.auto-local-branch` setting is applied only to newly fetched remote
branches. Existing remote branches are migrated as follows:

* If local branch exists, the corresponding remote branches are considered
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.

### New features

* `jj workspace add` now takes a `--revision` argument.
Expand Down
152 changes: 139 additions & 13 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 @@ -60,10 +64,10 @@ pub struct BranchDeleteArgs {

/// List branches and their targets
///
/// A remote branch will be included only if its target is different from
/// the local target. For a conflicted branch (both local and remote), old
/// target revisions are preceded by a "-" and new target revisions are
/// preceded by a "+". For information about branches, see
/// A tracking remote branch will be included only if its target is different
/// from the local target. For a conflicted branch (both local and remote), old
/// target revisions are preceded by a "-" and new target revisions are preceded
/// by a "+". For information about branches, see
/// https://github.com/martinvonz/jj/blob/main/docs/branches.md.
#[derive(clap::Args, Clone, Debug)]
pub struct BranchListArgs {
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 Expand Up @@ -389,14 +502,22 @@ fn cmd_branch_list(
.map_or(true, |branch_names| branch_names.contains(name))
});
for (name, branch_target) in branches_to_list {
write!(formatter.labeled("branch"), "{name}")?;
if branch_target.local_target.is_present() {
print_branch_target(formatter, branch_target.local_target)?;
} else {
writeln!(formatter, " (deleted)")?;
let (tracking_remote_refs, untracked_remote_refs) =
branch_target
.remote_refs
.into_iter()
.partition::<Vec<_>, _>(|&(_, remote_ref)| remote_ref.is_tracking());

if branch_target.local_target.is_present() || !tracking_remote_refs.is_empty() {
write!(formatter.labeled("branch"), "{name}")?;
if branch_target.local_target.is_present() {
print_branch_target(formatter, branch_target.local_target)?;
} else {
writeln!(formatter, " (deleted)")?;
}
}

for &(remote, remote_ref) in &branch_target.remote_refs {
for &(remote, remote_ref) in &tracking_remote_refs {
if remote_ref.target == *branch_target.local_target {
continue;
}
Expand Down Expand Up @@ -425,9 +546,8 @@ fn cmd_branch_list(
print_branch_target(formatter, &remote_ref.target)?;
}

if branch_target.local_target.is_absent() {
let found_non_git_remote = branch_target
.remote_refs
if branch_target.local_target.is_absent() && !tracking_remote_refs.is_empty() {
let found_non_git_remote = tracking_remote_refs
.iter()
.any(|&(remote, _)| remote != git::REMOTE_NAME_FOR_LOCAL_GIT_REPO);
if found_non_git_remote {
Expand All @@ -444,6 +564,12 @@ fn cmd_branch_list(
)?;
}
}

// TODO: hide non-tracking remotes by default?
for &(remote, remote_ref) in &untracked_remote_refs {
write!(formatter.labeled("branch"), "{name}@{remote}")?;
print_branch_target(formatter, &remote_ref.target)?;
}
}

Ok(())
Expand Down
1 change: 1 addition & 0 deletions cli/src/commands/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -995,6 +995,7 @@ fn cmd_git_push(
),
_ => user_error(err.to_string()),
})?;
// TODO: mark pushed remote branches as tracking
let stats = git::import_refs(tx.mut_repo(), &git_repo, &command.settings().git_settings())?;
print_git_import_stats(ui, &stats)?;
tx.finish(ui)?;
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