дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
479 рядки
18 KiB
Rust
479 рядки
18 KiB
Rust
//! Rebase / cherry-pick / revert / reset / tags, the shelf and patches, against
|
|
//! throwaway repositories created with the git CLI.
|
|
|
|
use ide_core::CancellationToken;
|
|
use ide_vcs::*;
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::Command;
|
|
|
|
struct Fixture {
|
|
_tmp: tempfile::TempDir,
|
|
dir: PathBuf,
|
|
}
|
|
|
|
fn git_in(dir: &Path, args: &[&str]) -> String {
|
|
let out = Command::new("git")
|
|
.current_dir(dir)
|
|
.args(["-c", "init.defaultBranch=main", "-c", "core.quotepath=off"])
|
|
.args(args)
|
|
.env("GIT_TERMINAL_PROMPT", "0")
|
|
.output()
|
|
.expect("git runs");
|
|
assert!(out.status.success(), "git {args:?} failed: {}", String::from_utf8_lossy(&out.stderr));
|
|
String::from_utf8_lossy(&out.stdout).trim().to_string()
|
|
}
|
|
|
|
impl Fixture {
|
|
fn new() -> Self {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let dir = tmp.path().join("work");
|
|
std::fs::create_dir(&dir).unwrap();
|
|
git_in(&dir, &["init", "-q"]);
|
|
for (k, v) in [
|
|
("user.name", "Test User"),
|
|
("user.email", "[email protected]"),
|
|
("commit.gpgsign", "false"),
|
|
("tag.gpgsign", "false"),
|
|
("core.autocrlf", "false"),
|
|
("core.hooksPath", ".git/hooks"),
|
|
("merge.conflictstyle", "merge"),
|
|
] {
|
|
git_in(&dir, &["config", k, v]);
|
|
}
|
|
Fixture { _tmp: tmp, dir }
|
|
}
|
|
fn git(&self, args: &[&str]) -> String {
|
|
git_in(&self.dir, args)
|
|
}
|
|
fn write(&self, rel: &str, content: &str) {
|
|
let p = self.dir.join(rel);
|
|
std::fs::create_dir_all(p.parent().unwrap()).unwrap();
|
|
std::fs::write(p, content).unwrap();
|
|
}
|
|
fn read(&self, rel: &str) -> String {
|
|
std::fs::read_to_string(self.dir.join(rel)).unwrap()
|
|
}
|
|
fn commit_all(&self, msg: &str) -> String {
|
|
self.git(&["add", "-A"]);
|
|
self.git(&["commit", "-q", "-m", msg]);
|
|
self.git(&["rev-parse", "HEAD"])
|
|
}
|
|
fn repo(&self) -> Repo {
|
|
Repo::discover(&self.dir).unwrap().expect("repository found")
|
|
}
|
|
fn path(&self, rel: &str) -> PathBuf {
|
|
self.dir.join(rel)
|
|
}
|
|
}
|
|
|
|
fn quiet() -> (CancellationToken, impl Fn(String)) {
|
|
(CancellationToken::new(), |_line: String| {})
|
|
}
|
|
|
|
/// main: base → "main" edit of f.txt; topic: base → "topic" edit of f.txt.
|
|
fn diverged() -> Fixture {
|
|
let fx = Fixture::new();
|
|
fx.write("f.txt", "base\n");
|
|
fx.commit_all("base");
|
|
fx.git(&["checkout", "-q", "-b", "topic"]);
|
|
fx.write("f.txt", "topic\n");
|
|
fx.write("t.txt", "t\n");
|
|
fx.commit_all("topic edit");
|
|
fx.git(&["checkout", "-q", "main"]);
|
|
fx.write("f.txt", "main\n");
|
|
fx.commit_all("main edit");
|
|
fx.git(&["checkout", "-q", "topic"]);
|
|
fx
|
|
}
|
|
|
|
#[test]
|
|
fn rebase_conflict_then_continue() {
|
|
let fx = diverged();
|
|
let repo = fx.repo();
|
|
let (cancel, out) = quiet();
|
|
assert!(repo.rebase("main").is_err());
|
|
assert_eq!(repo.branch_info().unwrap().operation.as_deref(), Some("rebase"));
|
|
// Unresolved conflicts block Continue.
|
|
let err = repo.continue_operation(&cancel, &out).unwrap_err();
|
|
assert!(err.message.contains("f.txt"), "{}", err.message);
|
|
|
|
fx.write("f.txt", "resolved\n");
|
|
repo.stage(&[fx.path("f.txt")]).unwrap();
|
|
repo.continue_operation(&cancel, &out).unwrap();
|
|
assert_eq!(repo.branch_info().unwrap().operation, None);
|
|
assert_eq!(fx.read("f.txt"), "resolved\n");
|
|
assert_eq!(fx.git(&["log", "-1", "--format=%s"]), "topic edit");
|
|
assert_eq!(fx.git(&["rev-parse", "HEAD~1"]), fx.git(&["rev-parse", "main"]));
|
|
}
|
|
|
|
#[test]
|
|
fn rebase_conflict_then_abort_and_skip() {
|
|
let fx = diverged();
|
|
let repo = fx.repo();
|
|
let (cancel, out) = quiet();
|
|
let before = fx.git(&["rev-parse", "HEAD"]);
|
|
assert!(repo.rebase("main").is_err());
|
|
repo.abort_operation(&cancel, &out).unwrap();
|
|
assert_eq!(repo.branch_info().unwrap().operation, None);
|
|
assert_eq!(fx.git(&["rev-parse", "HEAD"]), before);
|
|
assert_eq!(fx.read("f.txt"), "topic\n");
|
|
|
|
assert!(repo.rebase("main").is_err());
|
|
repo.skip_operation(&cancel, &out).unwrap();
|
|
assert_eq!(repo.branch_info().unwrap().operation, None);
|
|
assert_eq!(fx.git(&["rev-parse", "HEAD"]), fx.git(&["rev-parse", "main"]));
|
|
|
|
// Nothing in progress.
|
|
assert!(repo.abort_operation(&cancel, &out).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn merge_conflict_continue_commits() {
|
|
let fx = diverged();
|
|
fx.git(&["checkout", "-q", "main"]);
|
|
let repo = fx.repo();
|
|
let (cancel, out) = quiet();
|
|
assert!(repo.merge("topic").is_err());
|
|
assert_eq!(repo.branch_info().unwrap().operation.as_deref(), Some("merge"));
|
|
assert!(repo.skip_operation(&cancel, &out).is_err(), "merges cannot be skipped");
|
|
fx.write("f.txt", "both\n");
|
|
repo.stage(&[fx.path("f.txt")]).unwrap();
|
|
repo.continue_operation(&cancel, &out).unwrap();
|
|
assert_eq!(repo.branch_info().unwrap().operation, None);
|
|
assert_eq!(fx.git(&["rev-list", "--count", "--merges", "HEAD"]), "1");
|
|
}
|
|
|
|
#[test]
|
|
fn cherry_pick_and_conflicting_cherry_pick() {
|
|
let fx = diverged();
|
|
let repo = fx.repo();
|
|
let (cancel, out) = quiet();
|
|
fx.git(&["checkout", "-q", "-b", "side", "main~1"]);
|
|
fx.write("s.txt", "side\n");
|
|
let side = fx.commit_all("side work");
|
|
fx.git(&["checkout", "-q", "main"]);
|
|
|
|
repo.cherry_pick(&[side], &cancel, &out).unwrap();
|
|
assert_eq!(fx.read("s.txt"), "side\n");
|
|
assert_eq!(fx.git(&["log", "-1", "--format=%s"]), "side work");
|
|
|
|
let topic = fx.git(&["rev-parse", "topic"]);
|
|
assert!(repo.cherry_pick(std::slice::from_ref(&topic), &cancel, &out).is_err());
|
|
assert_eq!(repo.branch_info().unwrap().operation.as_deref(), Some("cherry-pick"));
|
|
fx.write("f.txt", "picked\n");
|
|
repo.stage(&[fx.path("f.txt")]).unwrap();
|
|
repo.continue_operation(&cancel, &out).unwrap();
|
|
assert_eq!(repo.branch_info().unwrap().operation, None);
|
|
assert_eq!(fx.git(&["log", "-1", "--format=%s"]), "topic edit");
|
|
assert!(repo.cherry_pick(&[], &cancel, &out).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn revert_creates_inverse_commit() {
|
|
let fx = Fixture::new();
|
|
fx.write("a.txt", "1\n");
|
|
fx.commit_all("one");
|
|
fx.write("a.txt", "2\n");
|
|
let two = fx.commit_all("two");
|
|
let repo = fx.repo();
|
|
let (cancel, out) = quiet();
|
|
repo.revert(&[two], &cancel, &out).unwrap();
|
|
assert_eq!(fx.read("a.txt"), "1\n");
|
|
assert_eq!(fx.git(&["log", "-1", "--format=%s"]), "Revert \"two\"");
|
|
assert_eq!(fx.git(&["rev-list", "--count", "HEAD"]), "3");
|
|
}
|
|
|
|
#[test]
|
|
fn reset_modes() {
|
|
let fx = Fixture::new();
|
|
fx.write("a.txt", "1\n");
|
|
let one = fx.commit_all("one");
|
|
fx.write("a.txt", "2\n");
|
|
let two = fx.commit_all("two");
|
|
let repo = fx.repo();
|
|
let (cancel, out) = quiet();
|
|
let status = |repo: &Repo| {
|
|
let st = repo.status().unwrap();
|
|
st.iter().find(|s| s.rel_path == "a.txt").map(|s| (s.staged, s.unstaged))
|
|
};
|
|
|
|
repo.reset(&one, ResetMode::Soft, &cancel, &out).unwrap();
|
|
assert_eq!(fx.git(&["rev-parse", "HEAD"]), one);
|
|
assert_eq!(status(&repo), Some((Some(ChangeKind::Modified), None)));
|
|
|
|
fx.git(&["reset", "-q", "--hard", &two]);
|
|
repo.reset(&one, ResetMode::Mixed, &cancel, &out).unwrap();
|
|
assert_eq!(status(&repo), Some((None, Some(ChangeKind::Modified))));
|
|
assert_eq!(fx.read("a.txt"), "2\n");
|
|
|
|
fx.git(&["reset", "-q", "--hard", &two]);
|
|
fx.write("other.txt", "keep me\n");
|
|
fx.git(&["add", "other.txt"]);
|
|
fx.git(&["commit", "-q", "-m", "three"]);
|
|
fx.write("other.txt", "local edit\n");
|
|
repo.reset(&two, ResetMode::Keep, &cancel, &out).unwrap_err();
|
|
fx.git(&["checkout", "--", "other.txt"]);
|
|
fx.write("a.txt", "local\n");
|
|
// `--keep` keeps local changes of files the reset does not touch.
|
|
repo.reset(&two, ResetMode::Keep, &cancel, &out).unwrap();
|
|
assert_eq!(fx.read("a.txt"), "local\n");
|
|
assert!(!fx.path("other.txt").exists());
|
|
|
|
repo.reset(&one, ResetMode::Hard, &cancel, &out).unwrap();
|
|
assert_eq!(fx.read("a.txt"), "1\n");
|
|
assert!(repo.status().unwrap().is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn tags_lightweight_and_annotated() {
|
|
let fx = Fixture::new();
|
|
fx.write("a.txt", "1\n");
|
|
let one = fx.commit_all("one");
|
|
fx.write("a.txt", "2\n");
|
|
fx.commit_all("two");
|
|
let repo = fx.repo();
|
|
repo.create_tag("v1.0", &one, None, false).unwrap();
|
|
repo.create_tag("v2.0", "HEAD", Some("Release 2"), false).unwrap();
|
|
assert_eq!(repo.tags().unwrap(), vec!["v1.0".to_string(), "v2.0".to_string()]);
|
|
assert_eq!(fx.git(&["rev-parse", "v1.0^{commit}"]), one);
|
|
assert_eq!(fx.git(&["cat-file", "-t", "v2.0"]), "tag");
|
|
assert!(repo.create_tag("v1.0", "HEAD", None, false).is_err(), "existing tag needs force");
|
|
repo.create_tag("v1.0", "HEAD", None, true).unwrap();
|
|
assert!(repo.create_tag("bad..name", "HEAD", None, false).is_err());
|
|
assert!(repo.create_tag("-x", "HEAD", None, false).is_err());
|
|
assert!(repo.create_tag("ok", "no-such-rev", None, false).is_err());
|
|
let log = repo.log(&LogQuery { branch: Some("HEAD".into()), ..Default::default() }).unwrap();
|
|
assert!(log[0].refs.contains(&"tag: v2.0".to_string()), "{:?}", log[0].refs);
|
|
}
|
|
|
|
#[test]
|
|
fn compare_with_local_lists_differences() {
|
|
let fx = Fixture::new();
|
|
fx.write("a.txt", "1\n");
|
|
fx.write("b.txt", "b\n");
|
|
let one = fx.commit_all("one");
|
|
fx.write("a.txt", "2\n");
|
|
fx.write("c.txt", "c\n");
|
|
fx.commit_all("two");
|
|
fx.write("b.txt", "local\n");
|
|
let files = fx.repo().diff_with_worktree(&one).unwrap();
|
|
let got: Vec<(&str, ChangeKind)> = files.iter().map(|f| (f.path.as_str(), f.kind)).collect();
|
|
assert_eq!(
|
|
got,
|
|
vec![("a.txt", ChangeKind::Modified), ("b.txt", ChangeKind::Modified), ("c.txt", ChangeKind::Added)]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn shelve_unshelve_round_trip() {
|
|
let fx = Fixture::new();
|
|
fx.write("mod.txt", "one\ntwo\n");
|
|
fx.write("del.txt", "doomed\n");
|
|
fx.write("keep.txt", "keep\n");
|
|
fx.commit_all("base");
|
|
fx.write("mod.txt", "one\nTWO\n");
|
|
std::fs::remove_file(fx.path("del.txt")).unwrap();
|
|
fx.write("new/untracked.txt", "fresh\n");
|
|
fx.write("keep.txt", "not shelved\n");
|
|
fx.git(&["add", "mod.txt"]);
|
|
let repo = fx.repo();
|
|
let shelf = fx._tmp.path().join("shelf");
|
|
|
|
let paths = [fx.path("mod.txt"), fx.path("del.txt"), fx.path("new/untracked.txt")];
|
|
let entry = repo.shelve(&shelf, "My work", &paths).unwrap();
|
|
assert_eq!(entry.name, "My work");
|
|
assert_eq!(entry.branch.as_deref(), Some("main"));
|
|
assert_eq!(entry.files.len(), 3);
|
|
// Shelved changes are gone; the rest stays.
|
|
assert_eq!(fx.read("mod.txt"), "one\ntwo\n");
|
|
assert_eq!(fx.read("del.txt"), "doomed\n");
|
|
assert!(!fx.path("new/untracked.txt").exists());
|
|
assert_eq!(fx.read("keep.txt"), "not shelved\n");
|
|
let st = repo.status().unwrap();
|
|
assert_eq!(st.len(), 1, "{st:#?}");
|
|
|
|
let listed = shelf::list(&shelf).unwrap();
|
|
assert_eq!(listed.len(), 1);
|
|
assert_eq!(listed[0].id, entry.id);
|
|
shelf::rename(&shelf, &entry.id, "Renamed").unwrap();
|
|
assert_eq!(shelf::list(&shelf).unwrap()[0].name, "Renamed");
|
|
assert!(shelf::patch_of(&shelf, &entry.id).unwrap().contains("TWO"));
|
|
|
|
let outcome = repo.unshelve(&shelf, &entry.id, true).unwrap();
|
|
assert!(outcome.conflicts.is_empty());
|
|
assert_eq!(fx.read("mod.txt"), "one\nTWO\n");
|
|
assert!(!fx.path("del.txt").exists());
|
|
assert_eq!(fx.read("new/untracked.txt"), "fresh\n");
|
|
assert!(shelf::list(&shelf).unwrap().is_empty());
|
|
|
|
assert!(repo.shelve(&shelf, "nothing", &[fx.path("del-none.txt")]).is_err());
|
|
assert!(shelf::delete(&shelf, "../work").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn unshelve_with_conflict_leaves_merge_state() {
|
|
let fx = Fixture::new();
|
|
fx.write("f.txt", "a\nb\nc\n");
|
|
fx.commit_all("base");
|
|
fx.write("f.txt", "a\nSHELVED\nc\n");
|
|
let repo = fx.repo();
|
|
let shelf = fx._tmp.path().join("shelf");
|
|
let entry = repo.shelve(&shelf, "conflicting", &[fx.path("f.txt")]).unwrap();
|
|
fx.write("f.txt", "a\nCOMMITTED\nc\n");
|
|
fx.commit_all("diverge");
|
|
|
|
let outcome = repo.unshelve(&shelf, &entry.id, false).unwrap();
|
|
assert_eq!(outcome.conflicts, vec!["f.txt".to_string()]);
|
|
let text = fx.read("f.txt");
|
|
assert!(text.contains("<<<<<<<") && text.contains("SHELVED") && text.contains("COMMITTED"), "{text}");
|
|
let (base, ours, theirs) = repo.conflict_versions(&fx.path("f.txt")).unwrap();
|
|
assert!(base.is_some() && ours.unwrap().contains("COMMITTED") && theirs.unwrap().contains("SHELVED"));
|
|
// Kept on the shelf when not removing.
|
|
assert_eq!(shelf::list(&shelf).unwrap().len(), 1);
|
|
shelf::delete(&shelf, &entry.id).unwrap();
|
|
assert!(shelf::list(&shelf).unwrap().is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn create_and_apply_patches() {
|
|
let fx = Fixture::new();
|
|
fx.write("a.txt", "1\n2\n3\n");
|
|
fx.commit_all("base");
|
|
fx.write("a.txt", "1\nTWO\n3\n");
|
|
fx.write("n.txt", "new\n");
|
|
let repo = fx.repo();
|
|
let patch = repo.create_patch(&[fx.path("a.txt"), fx.path("n.txt")], false).unwrap();
|
|
assert!(patch.contains("+TWO") && patch.contains("new file mode"), "{patch}");
|
|
// The real index is untouched.
|
|
assert!(repo.status().unwrap().iter().all(|s| s.staged.is_none()));
|
|
|
|
let reverse = repo.create_patch(&[fx.path("a.txt")], true).unwrap();
|
|
assert!(reverse.contains("-TWO") && reverse.contains("+2"));
|
|
|
|
fx.git(&["checkout", "--", "a.txt"]);
|
|
std::fs::remove_file(fx.path("n.txt")).unwrap();
|
|
let preview = repo.patch_preview(&patch).unwrap();
|
|
let got: Vec<(&str, ChangeKind, u32, u32, bool)> =
|
|
preview.iter().map(|f| (f.path.as_str(), f.kind, f.added, f.removed, f.applies)).collect();
|
|
assert_eq!(got, vec![("a.txt", ChangeKind::Modified, 1, 1, true), ("n.txt", ChangeKind::Added, 1, 0, true)]);
|
|
|
|
let outcome = repo.apply_patch(&patch).unwrap();
|
|
assert!(outcome.conflicts.is_empty());
|
|
assert_eq!(fx.read("a.txt"), "1\nTWO\n3\n");
|
|
assert_eq!(fx.read("n.txt"), "new\n");
|
|
|
|
// Applying again: n.txt exists, a.txt no longer matches.
|
|
let preview = repo.patch_preview(&patch).unwrap();
|
|
assert!(preview.iter().all(|f| !f.applies), "{preview:#?}");
|
|
assert!(repo.apply_patch("").is_err());
|
|
assert!(repo.patch_preview("not a patch\n").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn patch_from_commits_applies_elsewhere() {
|
|
let fx = Fixture::new();
|
|
fx.write("a.txt", "1\n");
|
|
let root = fx.commit_all("root");
|
|
fx.write("a.txt", "1\n2\n");
|
|
let c2 = fx.commit_all("two");
|
|
fx.write("b.txt", "b\n");
|
|
let c3 = fx.commit_all("three");
|
|
let repo = fx.repo();
|
|
let patch = repo.create_commits_patch(&[c2, c3], false).unwrap();
|
|
assert!(patch.contains("+2") && patch.contains("b.txt"));
|
|
let root_patch = repo.create_commits_patch(std::slice::from_ref(&root), false).unwrap();
|
|
assert!(root_patch.contains("new file mode"));
|
|
|
|
fx.git(&["checkout", "-q", "--detach", &root]);
|
|
let outcome = repo.apply_patch(&patch).unwrap();
|
|
assert!(outcome.conflicts.is_empty());
|
|
assert_eq!(fx.read("a.txt"), "1\n2\n");
|
|
assert_eq!(fx.read("b.txt"), "b\n");
|
|
}
|
|
|
|
#[test]
|
|
fn push_with_tags() {
|
|
let fx = Fixture::new();
|
|
let bare = fx._tmp.path().join("origin.git");
|
|
git_in(fx._tmp.path(), &["init", "-q", "--bare", bare.to_str().unwrap()]);
|
|
fx.write("a.txt", "1\n");
|
|
fx.commit_all("one");
|
|
fx.git(&["remote", "add", "origin", bare.to_str().unwrap()]);
|
|
fx.git(&["push", "-q", "-u", "origin", "main"]);
|
|
let repo = fx.repo();
|
|
let (cancel, out) = quiet();
|
|
repo.create_tag("light", "HEAD", None, false).unwrap();
|
|
fx.write("a.txt", "2\n");
|
|
fx.commit_all("two");
|
|
repo.create_tag("v2", "HEAD", Some("annotated"), false).unwrap();
|
|
|
|
repo.push_with_tags(false, false, Some(PushTags::CurrentBranch), &cancel, &out).unwrap();
|
|
let remote_tags = || git_in(&bare, &["tag", "--list"]);
|
|
assert_eq!(remote_tags(), "v2");
|
|
assert_eq!(git_in(&bare, &["rev-parse", "main"]), fx.git(&["rev-parse", "HEAD"]));
|
|
|
|
fx.write("a.txt", "3\n");
|
|
fx.commit_all("three");
|
|
repo.push_with_tags(false, false, Some(PushTags::All), &cancel, &out).unwrap();
|
|
assert_eq!(remote_tags(), "light\nv2");
|
|
assert_eq!(git_in(&bare, &["rev-parse", "main"]), fx.git(&["rev-parse", "HEAD"]));
|
|
}
|
|
|
|
#[test]
|
|
fn push_branch_and_up_to_revision() {
|
|
let fx = Fixture::new();
|
|
let bare = fx._tmp.path().join("origin.git");
|
|
git_in(fx._tmp.path(), &["init", "-q", "--bare", bare.to_str().unwrap()]);
|
|
fx.write("a.txt", "1\n");
|
|
fx.commit_all("one");
|
|
fx.git(&["remote", "add", "origin", bare.to_str().unwrap()]);
|
|
fx.git(&["push", "-q", "-u", "origin", "main"]);
|
|
let c2 = {
|
|
fx.write("a.txt", "2\n");
|
|
fx.commit_all("two")
|
|
};
|
|
fx.write("a.txt", "3\n");
|
|
let c3 = fx.commit_all("three");
|
|
let repo = fx.repo();
|
|
let (cancel, out) = quiet();
|
|
|
|
repo.push_branch("main", Some(&c2), false, &cancel, &out).unwrap();
|
|
assert_eq!(git_in(&bare, &["rev-parse", "main"]), c2);
|
|
repo.push_branch("main", None, false, &cancel, &out).unwrap();
|
|
assert_eq!(git_in(&bare, &["rev-parse", "main"]), c3);
|
|
|
|
// A branch other than HEAD, without upstream: pushed under its name and tracked.
|
|
fx.git(&["branch", "side", &c2]);
|
|
repo.push_branch("side", None, false, &cancel, &out).unwrap();
|
|
assert_eq!(git_in(&bare, &["rev-parse", "side"]), c2);
|
|
assert_eq!(fx.git(&["rev-parse", "--abbrev-ref", "side@{upstream}"]), "origin/side");
|
|
assert!(repo.push_branch("nope", None, false, &cancel, &out).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn update_branch_fast_forwards_a_branch_not_checked_out() {
|
|
let fx = Fixture::new();
|
|
let bare = fx._tmp.path().join("origin.git");
|
|
git_in(fx._tmp.path(), &["init", "-q", "--bare", bare.to_str().unwrap()]);
|
|
fx.write("a.txt", "1\n");
|
|
fx.commit_all("one");
|
|
fx.git(&["remote", "add", "origin", bare.to_str().unwrap()]);
|
|
fx.git(&["push", "-q", "-u", "origin", "main"]);
|
|
fx.git(&["checkout", "-q", "-b", "work"]);
|
|
// Someone else advances origin/main.
|
|
let other = fx._tmp.path().join("other");
|
|
git_in(fx._tmp.path(), &["clone", "-q", bare.to_str().unwrap(), other.to_str().unwrap()]);
|
|
std::fs::write(other.join("a.txt"), "2\n").unwrap();
|
|
git_in(&other, &["-c", "user.name=O", "-c", "user.email=o@x", "commit", "-q", "-am", "two"]);
|
|
git_in(&other, &["push", "-q", "origin", "main"]);
|
|
let tip = git_in(&other, &["rev-parse", "HEAD"]);
|
|
|
|
let repo = fx.repo();
|
|
let (cancel, out) = quiet();
|
|
repo.update_branch("main", &cancel, &out).unwrap();
|
|
assert_eq!(fx.git(&["rev-parse", "main"]), tip);
|
|
assert_eq!(fx.git(&["rev-parse", "origin/main"]), tip);
|
|
// The checked-out branch and branches without upstream are refused.
|
|
assert!(repo.update_branch("work", &cancel, &out).is_err());
|
|
}
|