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 18, 2024
1 parent a1b16c5 commit 06d67f0
Show file tree
Hide file tree
Showing 10 changed files with 251 additions and 37 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* When creating a new workspace, the sparse patterns are now copied over from
the current workspace.

* `jj git fetch` now automatically prints new remote branches and tags by default.

### Fixed bugs

* On Windows, symlinks in the repo are now materialized as regular files in the
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 @@ -953,7 +953,7 @@ impl WorkspaceCommandHelper {
return Ok(());
}

print_git_import_stats(ui, &stats)?;
print_git_import_stats(ui, tx.repo(), &stats, false)?;
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
8 changes: 4 additions & 4 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, tx.repo(), &stats, false)?;
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, tx.repo(), &stats.import_stats, true)?;
}
tx.finish(
ui,
Expand Down Expand Up @@ -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, fetch_tx.repo(), &stats.import_stats, true)?;
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, tx.repo(), &stats, true)?;
tx.finish(ui, "import git refs")?;
Ok(())
}
Expand Down
136 changes: 133 additions & 3 deletions cli/src/git_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,17 @@ use std::sync::Mutex;
use std::time::Instant;
use std::{error, iter};

use jj_lib::git::{self, FailedRefExport, FailedRefExportReason, GitImportStats};
use itertools::Itertools;
use jj_lib::git::{self, FailedRefExport, FailedRefExportReason, GitImportStats, RefName};
use jj_lib::git_backend::GitBackend;
use jj_lib::repo::{ReadonlyRepo, Repo as _};
use jj_lib::op_store::{RefTarget, RemoteRef};
use jj_lib::repo::{ReadonlyRepo, Repo};
use jj_lib::store::Store;
use jj_lib::workspace::Workspace;
use unicode_width::UnicodeWidthStr;

use crate::cli_util::{user_error, CommandError};
use crate::formatter::Formatter;
use crate::progress::Progress;
use crate::ui::Ui;

Expand Down Expand Up @@ -172,17 +176,143 @@ 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,
repo: &dyn Repo,
stats: &GitImportStats,
show_ref_stats: bool,
) -> Result<(), CommandError> {
if show_ref_stats {
let refs_stats = stats
.changed_remote_refs
.iter()
.map(|(ref_name, (remote_ref, ref_target))| {
RefStatus::new(ref_name, remote_ref, ref_target, repo)
})
.collect_vec();

let has_both_ref_kinds = refs_stats
.iter()
.any(|x| matches!(x.ref_kind, RefKind::Branch))
&& refs_stats
.iter()
.any(|x| matches!(x.ref_kind, RefKind::Tag));

let max_width = refs_stats.iter().map(|x| x.ref_name.width()).max();
if let Some(max_width) = max_width {
let mut stderr = ui.stderr_formatter();
for status in refs_stats {
status.output(max_width, has_both_ref_kinds, &mut *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_kind: RefKind,
ref_name: String,
tracking_status: TrackingStatus,
import_status: ImportStatus,
}

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

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

Self {
ref_name,
tracking_status,
import_status,
ref_kind,
}
}

fn output(
&self,
max_ref_name_width: usize,
has_both_ref_kinds: bool,
out: &mut dyn Formatter,
) -> std::io::Result<()> {
let tracking_status = match self.tracking_status {
TrackingStatus::Tracked => "tracked",
TrackingStatus::Untracked => "untracked",
TrackingStatus::NotApplicable => "",
};

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

let ref_name_display_width = self.ref_name.width();
let pad_width = max_ref_name_width.saturating_sub(ref_name_display_width);
let padded_ref_name = format!("{}{:>pad_width$}", self.ref_name, "", pad_width = pad_width);

let ref_kind = match self.ref_kind {
RefKind::Branch => "branch: ",
RefKind::Tag if !has_both_ref_kinds => "tag: ",
RefKind::Tag => "tag: ",
};

write!(out, "{ref_kind}")?;
write!(out.labeled("branch"), "{padded_ref_name}")?;
writeln!(out, " [{import_status}] {tracking_status}")
}
}

enum RefKind {
Branch,
Tag,
}

enum TrackingStatus {
Tracked,
Untracked,
NotApplicable, // for tags
}

enum ImportStatus {
New,
Deleted,
Updated,
}

pub fn print_failed_git_export(
ui: &Ui,
failed_branches: &[FailedRefExport],
Expand Down
36 changes: 30 additions & 6 deletions cli/tests/test_branch_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -536,7 +536,9 @@ 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] tracked
"###);
insta::assert_snapshot!(get_branch_output(&test_env, &repo_path), @r###"
feature1: mzyxwzks 9f01a0e0 message
@origin: mzyxwzks 9f01a0e0 message
Expand All @@ -548,7 +550,9 @@ 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] tracked
"###);
insta::assert_snapshot!(get_branch_output(&test_env, &repo_path), @r###"
feature1: mzyxwzks 9f01a0e0 message
@origin: mzyxwzks 9f01a0e0 message
Expand All @@ -574,7 +578,9 @@ 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] tracked
"###);
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 +693,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 +770,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 +807,10 @@ 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] tracked
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 +867,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
6 changes: 6 additions & 0 deletions cli/tests/test_git_clone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ fn test_git_clone() {
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Fetching into new repo in "$TEST_ENV/clone"
branch: main@origin [new] tracked
Working copy now at: uuqppmxq 1f0b881a (empty) (no description set)
Parent commit : mzyxwzks 9f01a0e0 main | message
Added 1 files, modified 0 files, removed 0 files
Expand Down Expand Up @@ -173,6 +174,7 @@ fn test_git_clone_colocate() {
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Fetching into new repo in "$TEST_ENV/clone"
branch: main@origin [new] tracked
Working copy now at: uuqppmxq 1f0b881a (empty) (no description set)
Parent commit : mzyxwzks 9f01a0e0 main | message
Added 1 files, modified 0 files, removed 0 files
Expand Down Expand Up @@ -328,6 +330,8 @@ fn test_git_clone_remote_default_branch() {
test_env.jj_cmd_ok(test_env.env_root(), &["git", "clone", "source", "clone1"]);
insta::assert_snapshot!(stderr, @r###"
Fetching into new repo in "$TEST_ENV/clone1"
branch: feature1@origin [new] tracked
branch: main@origin [new] tracked
Working copy now at: sqpuoqvx cad212e1 (empty) (no description set)
Parent commit : mzyxwzks 9f01a0e0 feature1 main | message
Added 1 files, modified 0 files, removed 0 files
Expand All @@ -346,6 +350,8 @@ fn test_git_clone_remote_default_branch() {
test_env.jj_cmd_ok(test_env.env_root(), &["git", "clone", "source", "clone2"]);
insta::assert_snapshot!(stderr, @r###"
Fetching into new repo in "$TEST_ENV/clone2"
branch: feature1@origin [new] untracked
branch: main@origin [new] untracked
Working copy now at: pmmvwywv fa729b1e (empty) (no description set)
Parent commit : mzyxwzks 9f01a0e0 feature1@origin main | message
Added 1 files, modified 0 files, removed 0 files
Expand Down
2 changes: 2 additions & 0 deletions cli/tests/test_git_colocated.rs
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,8 @@ 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] untracked
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 06d67f0

Please sign in to comment.