Skip to content

Commit

Permalink
cli: list new remote branches during git fetch
Browse files Browse the repository at this point in the history
  • Loading branch information
0xdeafbeef committed Feb 16, 2024
1 parent 3500cd8 commit f30144a
Show file tree
Hide file tree
Showing 8 changed files with 231 additions and 33 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([#1252](https://github.com/martinvonz/jj/issues/1252),
[#2971](https://github.com/martinvonz/jj/issues/2971)). This may become the
default depending on feedback.
* `jj git fetch` now automatically prints new remote branches and tags by default.

### Fixed bugs

Expand Down
2 changes: 1 addition & 1 deletion cli/src/cli_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -948,7 +948,7 @@ impl WorkspaceCommandHelper {
return Ok(());
}

print_git_import_stats(ui, &stats)?;
print_git_import_stats(ui, &stats, None)?;
let mut tx = tx.into_inner();
// Rebase here to show slightly different status message.
let num_rebased = tx.mut_repo().rebase_descendants(&self.settings)?;
Expand Down
10 changes: 5 additions & 5 deletions cli/src/commands/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@ fn init_git_refs(
if !tx.mut_repo().has_changes() {
return Ok(repo);
}
print_git_import_stats(ui, &stats)?;
print_git_import_stats(ui, &stats, None)?;
if colocated {
// If git.auto-local-branch = true, local branches could be created for
// the imported remote branches.
Expand Down Expand Up @@ -537,7 +537,7 @@ fn cmd_git_fetch(
GitFetchError::InternalGitError(err) => map_git_error(err),
_ => user_error(err),
})?;
print_git_import_stats(ui, &stats.import_stats)?;
print_git_import_stats(ui, &stats.import_stats, Some(tx.repo().base_repo()))?;
}
tx.finish(
ui,
Expand Down Expand Up @@ -726,7 +726,7 @@ fn do_git_clone(
r#"Fetching into new repo in "{}""#,
wc_path.display()
)?;
let mut workspace_command = command.for_loaded_repo(ui, workspace, repo)?;
let mut workspace_command = command.for_loaded_repo(ui, workspace, repo.clone())?;
maybe_add_gitignore(&workspace_command)?;
git_repo.remote(remote_name, source).unwrap();
let mut fetch_tx = workspace_command.start_transaction();
Expand All @@ -751,7 +751,7 @@ fn do_git_clone(
unreachable!("we didn't provide any globs")
}
})?;
print_git_import_stats(ui, &stats.import_stats)?;
print_git_import_stats(ui, &stats.import_stats, None)?;
fetch_tx.finish(ui, "fetch from git remote into empty repo")?;
Ok((workspace_command, stats))
}
Expand Down Expand Up @@ -1190,7 +1190,7 @@ fn cmd_git_import(
// That's why cmd_git_export() doesn't export the HEAD ref.
git::import_head(tx.mut_repo())?;
let stats = git::import_refs(tx.mut_repo(), &command.settings().git_settings())?;
print_git_import_stats(ui, &stats)?;
print_git_import_stats(ui, &stats, None)?;
tx.finish(ui, "import git refs")?;
Ok(())
}
Expand Down
110 changes: 108 additions & 2 deletions cli/src/git_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ use std::sync::Mutex;
use std::time::Instant;
use std::{error, iter};

use jj_lib::git::{self, FailedRefExport, FailedRefExportReason, GitImportStats};
use jj_lib::git::{self, FailedRefExport, FailedRefExportReason, GitImportStats, RefName};
use jj_lib::git_backend::GitBackend;
use jj_lib::op_store::{RefTarget, RemoteRef};
use jj_lib::repo::{ReadonlyRepo, Repo as _};
use jj_lib::store::Store;
use jj_lib::workspace::Workspace;
Expand Down Expand Up @@ -172,17 +173,122 @@ pub fn with_remote_git_callbacks<T>(
f(callbacks)
}

pub fn print_git_import_stats(ui: &mut Ui, stats: &GitImportStats) -> Result<(), CommandError> {
pub fn print_git_import_stats(
ui: &mut Ui,
stats: &GitImportStats,
query_ref_stats: Option<&ReadonlyRepo>,
) -> Result<(), CommandError> {
if let (Some(repo), true) = (query_ref_stats, !stats.changed_remote_refs.is_empty()) {
let mut remote_branches = Vec::new();
let mut tags = Vec::new();
let mut local_branches = Vec::new();

// group the references by type
for (refname, (remote_ref, ref_target)) in &stats.changed_remote_refs {
let ref_status = RefStatus::new(refname, remote_ref, ref_target, repo);
match refname {
RefName::RemoteBranch { .. } => remote_branches.push(ref_status),
RefName::Tag(_) => tags.push(ref_status),
RefName::LocalBranch(_) => local_branches.push(ref_status),
}
}

let grouped_refs = vec![remote_branches, tags, local_branches];

for refs in grouped_refs {
if let Some(max_ref_name_len) = refs.iter().map(|status| status.ref_name.len()).max() {
for status in refs {
status.output(max_ref_name_len, &mut ui.stderr())?;
}
writeln!(ui.stderr())?;
}
}
}

if !stats.abandoned_commits.is_empty() {
writeln!(
ui.stderr(),
"Abandoned {} commits that are no longer reachable.",
stats.abandoned_commits.len()
)?;
}

Ok(())
}

struct RefStatus {
ref_name: String,
tracking_status: TrackingStatus,
import_status: ImportStatus,
}

impl RefStatus {
fn new(
ref_name: &RefName,
remote_ref: &RemoteRef,
ref_target: &RefTarget,
repo: &ReadonlyRepo,
) -> Self {
let mut tracking_status = TrackingStatus::Untracked;
let ref_name = match ref_name {
RefName::RemoteBranch { branch, remote } => {
if repo.view().get_remote_branch(branch, remote).is_tracking() {
tracking_status = TrackingStatus::Tracked;
}
format!("branch: {branch}@{remote}")
}
RefName::Tag(tag) => format!("tag: {tag}"),
RefName::LocalBranch(branch) => format!("branch: {branch}"),
};

let import_status = match (remote_ref.target.is_absent(), ref_target.is_present()) {
(true, true) => ImportStatus::New,
(false, false) => ImportStatus::Deleted,
_ => ImportStatus::Updated,
};

Self {
ref_name,
tracking_status,
import_status,
}
}

fn output(&self, max_ref_name_len: usize, out: &mut dyn Write) -> std::io::Result<()> {
const MAX_REF_NAME_LEN: usize = 40;
let tracking_status = match self.tracking_status {
TrackingStatus::Tracked => "tracked",
TrackingStatus::Untracked => "untracked",
};

let import_status = match self.import_status {
ImportStatus::New => "new",
ImportStatus::Deleted => "deleted",
ImportStatus::Updated => "updated",
};

// This ensures alignment of the statuses that follow
let padded_ref_name = format!(
"{:width$}",
self.ref_name,
width = std::cmp::min(max_ref_name_len, MAX_REF_NAME_LEN)
);

writeln!(out, "{padded_ref_name} [{import_status}] {tracking_status}")
}
}

enum TrackingStatus {
Tracked,
Untracked,
}

enum ImportStatus {
New,
Deleted,
Updated,
}

pub fn print_failed_git_export(
ui: &Ui,
failed_branches: &[FailedRefExport],
Expand Down
40 changes: 34 additions & 6 deletions cli/tests/test_branch_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -536,7 +536,10 @@ fn test_branch_forget_fetched_branch() {
// We can fetch feature1 again.
let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["git", "fetch", "--remote=origin"]);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @"");
insta::assert_snapshot!(stderr, @r###"
branch: feature1@origin [new] untracked
"###);
insta::assert_snapshot!(get_branch_output(&test_env, &repo_path), @r###"
feature1: mzyxwzks 9f01a0e0 message
@origin: mzyxwzks 9f01a0e0 message
Expand All @@ -548,7 +551,10 @@ fn test_branch_forget_fetched_branch() {
// Fetch works even without the export-import
let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["git", "fetch", "--remote=origin"]);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @"");
insta::assert_snapshot!(stderr, @r###"
branch: feature1@origin [new] untracked
"###);
insta::assert_snapshot!(get_branch_output(&test_env, &repo_path), @r###"
feature1: mzyxwzks 9f01a0e0 message
@origin: mzyxwzks 9f01a0e0 message
Expand All @@ -574,7 +580,10 @@ fn test_branch_forget_fetched_branch() {
// Fetching a moved branch does not create a conflict
let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["git", "fetch", "--remote=origin"]);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @"");
insta::assert_snapshot!(stderr, @r###"
branch: feature1@origin [new] untracked
"###);
insta::assert_snapshot!(get_branch_output(&test_env, &repo_path), @r###"
feature1: ooosovrs 38aefb17 (empty) another message
@origin: ooosovrs 38aefb17 (empty) another message
Expand Down Expand Up @@ -687,7 +696,12 @@ fn test_branch_track_untrack() {
);
test_env.add_config("git.auto-local-branch = false");
let (_stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["git", "fetch"]);
insta::assert_snapshot!(stderr, @"");
insta::assert_snapshot!(stderr, @r###"
branch: feature1@origin [new] untracked
branch: feature2@origin [new] untracked
branch: main@origin [new] untracked
"###);
insta::assert_snapshot!(get_branch_output(&test_env, &repo_path), @r###"
feature1@origin: sptzoqmo 7b33f629 commit 1
feature2@origin: sptzoqmo 7b33f629 commit 1
Expand Down Expand Up @@ -759,7 +773,12 @@ fn test_branch_track_untrack() {
],
);
let (_stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["git", "fetch"]);
insta::assert_snapshot!(stderr, @"");
insta::assert_snapshot!(stderr, @r###"
branch: feature1@origin [updated] untracked
branch: feature2@origin [updated] untracked
branch: main@origin [updated] tracked
"###);
insta::assert_snapshot!(get_branch_output(&test_env, &repo_path), @r###"
feature1: sptzoqmo 7b33f629 commit 1
feature1@origin: mmqqkyyt 40dabdaf commit 2
Expand Down Expand Up @@ -791,6 +810,11 @@ fn test_branch_track_untrack() {
test_env.add_config("git.auto-local-branch = true");
let (_stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["git", "fetch"]);
insta::assert_snapshot!(stderr, @r###"
branch: feature1@origin [updated] untracked
branch: feature2@origin [updated] untracked
branch: feature3@origin [new] untracked
branch: main@origin [updated] tracked
Abandoned 1 commits that are no longer reachable.
"###);
insta::assert_snapshot!(get_branch_output(&test_env, &repo_path), @r###"
Expand Down Expand Up @@ -847,7 +871,11 @@ fn test_branch_track_untrack_patterns() {
// Fetch new commit without auto tracking
test_env.add_config("git.auto-local-branch = false");
let (_stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["git", "fetch"]);
insta::assert_snapshot!(stderr, @"");
insta::assert_snapshot!(stderr, @r###"
branch: feature1@origin [new] untracked
branch: feature2@origin [new] untracked
"###);

// Track local branch
test_env.jj_cmd_ok(&repo_path, &["branch", "create", "main"]);
Expand Down
3 changes: 3 additions & 0 deletions cli/tests/test_git_colocated.rs
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,9 @@ fn test_git_colocated_fetch_deleted_or_moved_branch() {
let (stdout, stderr) = test_env.jj_cmd_ok(&clone_path, &["git", "fetch"]);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
branch: B_to_delete@origin [deleted] tracked
branch: C_to_move@origin [updated] tracked
Abandoned 2 commits that are no longer reachable.
"###);
// "original C" and "B_to_delete" are abandoned, as the corresponding branches
Expand Down
Loading

0 comments on commit f30144a

Please sign in to comment.