chore: and reconcile pushes with server only refs
Rust / build (macos-latest, stable) (push) Canceled after 0s
Rust / build (ubuntu-latest, stable) (push) Canceled after 0s
Rust / build (windows-latest, stable) (push) Canceled after 0s

This commit is contained in:
2026-10-16 20:07:13 +07:00
parent a2e7907193
commit 4b4b8b5cf2
8 changed files with 546 additions and 82 deletions
+33 -3
View File
@@ -1222,22 +1222,25 @@ impl Repo {
base_url,
owner,
repo_id,
&["refs/heads/main:refs/heads/main"],
&["+refs/heads/main:refs/heads/main"],
)
}
/// Mirrors all branches and tags to the grasp repository at `base_url`.
///
/// Force refspecs keep the server ref map following the local repository,
/// grasp authorizes pushes by ref map equality, not fast-forwards.
pub fn push_all(&self, base_url: &str, owner: &str, repo_id: &str) -> Result<()> {
self.push_refspecs(
base_url,
owner,
repo_id,
&["refs/heads/*:refs/heads/*", "refs/tags/*:refs/tags/*"],
&["+refs/heads/*:refs/heads/*", "+refs/tags/*:refs/tags/*"],
)
}
/// Pushes explicit refspecs to the grasp repository at `base_url`.
fn push_refspecs(
pub fn push_refspecs(
&self,
base_url: &str,
owner: &str,
@@ -1262,6 +1265,33 @@ impl Repo {
Ok(())
}
/// Lists branches and tags advertised by the remote at `url`.
pub fn ls_remote(&self, url: &str) -> Result<Vec<(String, String)>> {
let url = Self::transport_url(url);
let output = Self::run_git(self.workdir_or_dot(), &["ls-remote", &url], "git ls-remote")?;
if !output.status.success() {
bail!(
"git ls-remote on {url} failed: {}",
String::from_utf8_lossy(&output.stderr).trim()
);
}
let refs = String::from_utf8_lossy(&output.stdout)
.lines()
.filter_map(|line| {
let (oid, name) = line.split_once('\t')?;
let name = name.trim();
(name.starts_with("refs/heads/") || name.starts_with("refs/tags/"))
.then(|| (name.to_owned(), oid.to_owned()))
.filter(|(name, _)| !name.ends_with("^{}"))
})
.collect();
Ok(refs)
}
/// Sets the `origin` URL when the repository has no remote yet.
pub fn ensure_origin(&self, url: &str) -> Result<()> {
if self.inner.find_remote("origin").is_ok() {
+160
View File
@@ -122,6 +122,166 @@ fn push_all_mirrors_branches_and_tags() {
assert!(refs.contains("refs/tags/v1.0"));
}
#[test]
fn ls_remote_lists_branches_and_tags_only() {
let server = tempfile::tempdir().unwrap();
let server_repo = bare_server(server.path(), "npub1test", "my-repo");
let (dir, repo) = fixture(&[("a.txt", b"one")]);
commit_all(&repo, "initial");
let dir = dir.path();
git_run(dir, &["branch", "feature"]);
git_run(dir, &["tag", "v1"]);
git_run(dir, &["tag", "-a", "v2", "-m", "annotated"]);
let base_url = format!("file://{}", server.path().display());
let local = Repo::open(dir).expect("open");
local
.push_all(&base_url, "npub1test", "my-repo")
.expect("push");
// Refs outside heads and tags, and the HEAD symref line, are not listed.
let main = git_in(dir, &["rev-parse", "refs/heads/main"]).expect("main oid");
git_run(&server_repo, &["update-ref", "refs/nostr/abc", &main]);
let remote = local
.ls_remote(&format!("file://{}", server_repo.display()))
.expect("ls-remote");
let names: Vec<String> = remote.iter().map(|(name, _)| name.clone()).collect();
assert_eq!(
names,
vec![
"refs/heads/feature".to_owned(),
"refs/heads/main".to_owned(),
"refs/tags/v1".to_owned(),
"refs/tags/v2".to_owned(),
]
);
assert_eq!(
remote
.iter()
.find(|(name, _)| name == "refs/heads/main")
.map(|(_, oid)| oid.as_str()),
Some(main.as_str())
);
// The annotated tag lists the tag object, matching the local ref state,
// without a peeled `^{}` duplicate.
let v2 = git_in(dir, &["rev-parse", "refs/tags/v2"]).expect("v2 oid");
assert_eq!(
remote
.iter()
.find(|(name, _)| name == "refs/tags/v2")
.map(|(_, oid)| oid.as_str()),
Some(v2.as_str())
);
}
#[test]
fn push_all_force_updates_diverged_remote_branches() {
let server = tempfile::tempdir().unwrap();
let server_repo = bare_server(server.path(), "npub1test", "my-repo");
let (dir, repo) = fixture(&[("a.txt", b"one")]);
commit_all(&repo, "initial");
std::fs::write(dir.path().join("b.txt"), b"two").expect("write");
commit_all(&repo, "second");
let dir = dir.path();
let base_url = format!("file://{}", server.path().display());
let local = Repo::open(dir).expect("open");
local
.push_all(&base_url, "npub1test", "my-repo")
.expect("initial push");
// Diverge local main from the pushed state.
git_run(dir, &["reset", "-q", "--hard", "HEAD~1"]);
std::fs::write(dir.join("c.txt"), b"three").expect("write");
commit_all(&repo, "diverged");
local
.push_all(&base_url, "npub1test", "my-repo")
.expect("force push");
assert_eq!(
git_in(dir, &["rev-parse", "refs/heads/main"]).expect("local main"),
git_in(&server_repo, &["rev-parse", "refs/heads/main"]).expect("server main")
);
}
#[test]
fn push_refspecs_deletes_server_refs_missing_locally() {
let server = tempfile::tempdir().unwrap();
let server_repo = bare_server(server.path(), "npub1test", "my-repo");
let (dir, repo) = fixture(&[("a.txt", b"one")]);
commit_all(&repo, "initial");
let dir = dir.path();
git_run(dir, &["branch", "feature"]);
git_run(dir, &["tag", "v1"]);
let base_url = format!("file://{}", server.path().display());
let local = Repo::open(dir).expect("open");
local
.push_all(&base_url, "npub1test", "my-repo")
.expect("initial push");
git_run(dir, &["branch", "-D", "feature"]);
git_run(dir, &["tag", "-d", "v1"]);
local
.push_refspecs(
&base_url,
"npub1test",
"my-repo",
&[
"+refs/heads/*:refs/heads/*",
"+refs/tags/*:refs/tags/*",
":refs/heads/feature",
":refs/tags/v1",
],
)
.expect("push with deletions");
let refs = git_in(&server_repo, &["show-ref"]).expect("server refs");
assert!(refs.contains("refs/heads/main"));
assert!(!refs.contains("refs/heads/feature"));
assert!(!refs.contains("refs/tags/v1"));
}
#[test]
fn fetch_refs_brings_server_only_branches_into_local() {
let server = tempfile::tempdir().unwrap();
bare_server(server.path(), "npub1test", "my-repo");
let (dir, repo) = fixture(&[("a.txt", b"one")]);
commit_all(&repo, "initial");
let dir = dir.path();
git_run(dir, &["branch", "feature"]);
let base_url = format!("file://{}", server.path().display());
Repo::open(dir)
.expect("open")
.push_all(&base_url, "npub1test", "my-repo")
.expect("push");
let feature = git_in(dir, &["rev-parse", "refs/heads/feature"]).expect("feature oid");
// A repository that never had the branch fetches it by name.
let (other_dir, _other_repo) = fixture(&[("unrelated.txt", b"x")]);
let other = Repo::open(other_dir.path()).expect("open other");
let server_url = format!("file://{}/npub1test/my-repo.git", server.path().display());
other
.fetch_refs(&[&server_url], "+refs/heads/feature:refs/heads/feature")
.expect("fetch ref");
assert_eq!(
git_in(other_dir.path(), &["rev-parse", "refs/heads/feature"]).expect("fetched feature"),
feature
);
}
#[test]
fn ref_state_matches_the_git_cli_ref_listing() {
let (dir, repo) = fixture(&[("a.txt", b"one")]);