Skip to content
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
16 changes: 16 additions & 0 deletions crates/gitcomet-core/src/services.rs
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,22 @@ pub trait GitRepository: Send + Sync {
)))
}

fn set_upstream_branch_with_output(
&self,
_branch: &str,
_upstream: &str,
) -> Result<CommandOutput> {
Err(Error::new(ErrorKind::Unsupported(
"setting a branch upstream is not implemented for this backend",
)))
}

fn unset_upstream_branch_with_output(&self, _branch: &str) -> Result<CommandOutput> {
Err(Error::new(ErrorKind::Unsupported(
"unsetting a branch upstream is not implemented for this backend",
)))
}

fn delete_remote_branch_with_output(
&self,
_remote: &str,
Expand Down
16 changes: 14 additions & 2 deletions crates/gitcomet-git-gix/src/repo/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,11 +239,11 @@ impl GitRepository for GixRepo {
}

fn fetch_all(&self) -> Result<()> {
self.fetch_all_impl(false)
self.fetch_all_impl(true)
}

fn fetch_all_with_output(&self) -> Result<CommandOutput> {
self.fetch_all_with_output_impl(false)
self.fetch_all_with_output_impl(true)
}

fn fetch_all_with_output_prune(&self, prune: bool) -> Result<CommandOutput> {
Expand Down Expand Up @@ -351,6 +351,18 @@ impl GitRepository for GixRepo {
self.push_set_upstream_with_output_impl(remote, branch)
}

fn set_upstream_branch_with_output(
&self,
branch: &str,
upstream: &str,
) -> Result<CommandOutput> {
self.set_upstream_branch_with_output_impl(branch, upstream)
}

fn unset_upstream_branch_with_output(&self, branch: &str) -> Result<CommandOutput> {
self.unset_upstream_branch_with_output_impl(branch)
}

fn delete_remote_branch_with_output(
&self,
remote: &str,
Expand Down
39 changes: 39 additions & 0 deletions crates/gitcomet-git-gix/src/repo/remotes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,45 @@ impl GixRepo {
self.push_set_upstream_with_optional_output_impl(remote, branch, true)
}

pub(super) fn set_upstream_branch_with_output_impl(
&self,
branch: &str,
upstream: &str,
) -> Result<CommandOutput> {
validate_ref_like_arg(branch, "branch name")?;
let Some((remote, upstream_branch)) = parse_short_remote_branch_name(upstream) else {
return Err(Error::new(ErrorKind::Backend(
"invalid upstream: expected remote/branch".to_string(),
)));
};
validate_ref_like_arg(remote, "remote name")?;
validate_ref_like_arg(upstream_branch, "branch name")?;

let label = format!("git branch --set-upstream-to {upstream} {branch}");
let mut cmd = self.git_workdir_cmd();
cmd.arg("branch")
.arg("--set-upstream-to")
.arg(upstream)
.arg("--")
.arg(branch);
run_git_with_output(cmd, &label)
}

pub(super) fn unset_upstream_branch_with_output_impl(
&self,
branch: &str,
) -> Result<CommandOutput> {
validate_ref_like_arg(branch, "branch name")?;

let label = format!("git branch --unset-upstream {branch}");
let mut cmd = self.git_workdir_cmd();
cmd.arg("branch")
.arg("--unset-upstream")
.arg("--")
.arg(branch);
run_git_with_output(cmd, &label)
}

pub(super) fn delete_remote_branch_with_output_impl(
&self,
remote: &str,
Expand Down
179 changes: 179 additions & 0 deletions crates/gitcomet-git-gix/tests/refs_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,22 @@ fn run_git(repo: &Path, args: &[&str]) {
assert!(status.success(), "git {:?} failed", args);
}

fn run_git_capture(repo: &Path, args: &[&str]) -> String {
let output = Command::new("git")
.arg("-C")
.arg(repo)
.args(args)
.output()
.expect("git command to run");
assert!(
output.status.success(),
"git {:?} failed: {}",
args,
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8_lossy(&output.stdout).to_string()
}

fn git_remote_url(path: &Path) -> String {
if cfg!(windows) {
// Ensure Windows drive-letter paths are never treated as scp-style host:path.
Expand Down Expand Up @@ -310,3 +326,166 @@ fn list_branches_reflects_new_upstream_without_reopen() {
})
);
}

#[test]
fn list_branches_reflects_tracking_upstream_set_without_push() {
if !require_git_shell_for_refs_integration_tests() {
return;
}
let dir = tempfile::tempdir().unwrap();
let root = dir.path();

let remote_repo = root.join("remote.git");
let work_repo = root.join("work");
fs::create_dir_all(&remote_repo).unwrap();
fs::create_dir_all(&work_repo).unwrap();

run_git(&remote_repo, &["init", "--bare", "-b", "main"]);

run_git(&work_repo, &["init", "-b", "main"]);
run_git(&work_repo, &["config", "user.email", "you@example.com"]);
run_git(&work_repo, &["config", "user.name", "You"]);
run_git(&work_repo, &["config", "commit.gpgsign", "false"]);
let origin_url = git_remote_url(&remote_repo);
run_git(
&work_repo,
&["remote", "add", "origin", origin_url.as_str()],
);

fs::write(work_repo.join("file.txt"), "base\n").unwrap();
run_git(&work_repo, &["add", "file.txt"]);
run_git(
&work_repo,
&["-c", "commit.gpgsign=false", "commit", "-m", "base"],
);

run_git(&work_repo, &["checkout", "-b", "feature"]);
fs::write(work_repo.join("feature.txt"), "feature\n").unwrap();
run_git(&work_repo, &["add", "feature.txt"]);
run_git(
&work_repo,
&["-c", "commit.gpgsign=false", "commit", "-m", "feature"],
);
run_git(&work_repo, &["push", "origin", "feature"]);

let backend = GixBackend;
let opened = backend.open(&work_repo).unwrap();

let before = opened.list_branches().unwrap();
let feature_before = before
.iter()
.find(|branch| branch.name == "feature")
.expect("feature branch present");
assert_eq!(feature_before.upstream, None);

let output = opened
.set_upstream_branch_with_output("feature", "origin/feature")
.expect("set upstream");
assert_eq!(output.exit_code, Some(0));

let upstream_after = run_git_capture(
&work_repo,
&[
"for-each-ref",
"--format=%(upstream:short)",
"refs/heads/feature",
],
);
assert_eq!(upstream_after.trim(), "origin/feature");

let after = opened.list_branches().unwrap();
let feature_after = after
.iter()
.find(|branch| branch.name == "feature")
.expect("feature branch present");
assert_eq!(
feature_after.upstream,
Some(Upstream {
remote: "origin".to_string(),
branch: "feature".to_string(),
})
);
}

#[test]
fn list_branches_reflects_removed_upstream_without_reopen() {
if !require_git_shell_for_refs_integration_tests() {
return;
}
let dir = tempfile::tempdir().unwrap();
let root = dir.path();

let remote_repo = root.join("remote.git");
let work_repo = root.join("work");
fs::create_dir_all(&remote_repo).unwrap();
fs::create_dir_all(&work_repo).unwrap();

run_git(&remote_repo, &["init", "--bare", "-b", "main"]);

run_git(&work_repo, &["init", "-b", "main"]);
run_git(&work_repo, &["config", "user.email", "you@example.com"]);
run_git(&work_repo, &["config", "user.name", "You"]);
run_git(&work_repo, &["config", "commit.gpgsign", "false"]);
let origin_url = git_remote_url(&remote_repo);
run_git(
&work_repo,
&["remote", "add", "origin", origin_url.as_str()],
);

fs::write(work_repo.join("file.txt"), "base\n").unwrap();
run_git(&work_repo, &["add", "file.txt"]);
run_git(
&work_repo,
&["-c", "commit.gpgsign=false", "commit", "-m", "base"],
);

run_git(&work_repo, &["checkout", "-b", "feature"]);
fs::write(work_repo.join("feature.txt"), "feature\n").unwrap();
run_git(&work_repo, &["add", "feature.txt"]);
run_git(
&work_repo,
&["-c", "commit.gpgsign=false", "commit", "-m", "feature"],
);
run_git(&work_repo, &["push", "-u", "origin", "feature"]);

let backend = GixBackend;
let opened = backend.open(&work_repo).unwrap();

let before = opened.list_branches().unwrap();
let feature_before = before
.iter()
.find(|branch| branch.name == "feature")
.expect("feature branch present");
assert_eq!(
feature_before.upstream,
Some(Upstream {
remote: "origin".to_string(),
branch: "feature".to_string(),
})
);

let output = opened
.unset_upstream_branch_with_output("feature")
.expect("unset upstream");
assert_eq!(output.exit_code, Some(0));

let upstream_after = run_git_capture(
&work_repo,
&[
"for-each-ref",
"--format=%(upstream:short)",
"refs/heads/feature",
],
);
assert!(
upstream_after.trim().is_empty(),
"expected feature to have no upstream after unlink: {upstream_after:?}"
);

let after = opened.list_branches().unwrap();
let feature_after = after
.iter()
.find(|branch| branch.name == "feature")
.expect("feature branch present");
assert_eq!(feature_after.upstream, None);
}
55 changes: 53 additions & 2 deletions crates/gitcomet-git-gix/tests/remote_management_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -518,7 +518,7 @@ fn prune_merged_branches_with_output_reports_noop_when_nothing_to_prune() {
}

#[test]
fn fetch_all_variants_without_prune_succeed() {
fn fetch_all_variants_prune_deleted_remote_tracking_branches() {
let _guard = remote_management_test_lock();
if !require_git_local_push_for_remote_management_tests() {
return;
Expand All @@ -545,13 +545,64 @@ fn fetch_all_variants_without_prune_succeed() {
);
run_git(&work_repo, &["push", "-u", "origin", "HEAD"]);

run_git(&work_repo, &["checkout", "-b", "feature"]);
fs::write(work_repo.join("feature.txt"), "feature\n").expect("write feature file");
run_git(&work_repo, &["add", "feature.txt"]);
run_git(
&work_repo,
&["-c", "commit.gpgsign=false", "commit", "-m", "feature"],
);
run_git(&work_repo, &["push", "-u", "origin", "feature"]);

let feature_commit = run_git_capture(&work_repo, &["rev-parse", "HEAD"])
.trim()
.to_string();

let tracking_ref = "refs/remotes/origin/feature";
let tracking_ref_present = || {
run_git_status(
&work_repo,
&["show-ref", "--verify", "--quiet", tracking_ref],
)
};

assert!(
tracking_ref_present().success(),
"expected local tracking ref to exist before remote deletion"
);

run_git(&remote_repo, &["update-ref", "-d", "refs/heads/feature"]);
assert!(
tracking_ref_present().success(),
"expected local tracking ref to remain stale until fetch --prune"
);

let backend = GixBackend;
let opened = backend.open(&work_repo).expect("open work repo");
opened.fetch_all().expect("fetch all");
let output = opened
.fetch_all_with_output()
.expect("fetch all with output");
assert_eq!(output.exit_code, Some(0));
assert_eq!(output.command, "git fetch --all --prune");
assert!(
!tracking_ref_present().success(),
"expected fetch_all_with_output to prune stale remote-tracking refs"
);

run_git(
&work_repo,
&["update-ref", tracking_ref, feature_commit.as_str()],
);
assert!(
tracking_ref_present().success(),
"expected stale tracking ref recreation to succeed"
);

opened.fetch_all().expect("fetch all");
assert!(
!tracking_ref_present().success(),
"expected fetch_all to prune stale remote-tracking refs"
);
}

#[test]
Expand Down
2 changes: 2 additions & 0 deletions crates/gitcomet-git/src/noop_backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,8 @@ mod tests {
assert_unsupported(repo.push_force_with_output());
assert_unsupported(repo.push_set_upstream("origin", "main"));
assert_unsupported(repo.push_set_upstream_with_output("origin", "main"));
assert_unsupported(repo.set_upstream_branch_with_output("main", "origin/main"));
assert_unsupported(repo.unset_upstream_branch_with_output("main"));
assert_unsupported(repo.delete_remote_branch_with_output("origin", "main"));
assert_unsupported(repo.commit_amend_with_output("message"));
assert_unsupported(repo.pull_branch_with_output("origin", "main"));
Expand Down
Loading
Loading