add push
This commit is contained in:
@@ -517,14 +517,24 @@ pub fn root_commit(repo_path: &Path) -> Result<Option<String>> {
|
||||
.filter(|id| id.len() == 40))
|
||||
}
|
||||
|
||||
/// Add `origin` pointing at `url` when the repository has no remote yet.
|
||||
/// No-op if `origin` already exists.
|
||||
/// Add `origin` pointing at `url` when the repository has no remote yet,
|
||||
/// with the standard fetch mapping so later `git fetch origin` (and the
|
||||
/// cache's `fetch_all`) updates `refs/remotes/origin/*`. No-op if `origin`
|
||||
/// already exists.
|
||||
pub fn ensure_origin(repo_path: &Path, url: &str) -> Result<()> {
|
||||
// `git remote get-url origin` exits non-zero when the remote is absent.
|
||||
if git_in(repo_path, &["remote", "get-url", "origin"]).is_ok() {
|
||||
return Ok(());
|
||||
}
|
||||
git_in(repo_path, &["remote", "add", "origin", url])?;
|
||||
git_in(
|
||||
repo_path,
|
||||
&[
|
||||
"config",
|
||||
"remote.origin.fetch",
|
||||
"+refs/heads/*:refs/remotes/origin/*",
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -667,6 +677,55 @@ pub fn origin_url(workdir: &Path) -> Result<Option<String>> {
|
||||
Ok((!url.trim().is_empty()).then(|| url.trim().to_owned()))
|
||||
}
|
||||
|
||||
/// Fast-forward every local branch of the repository at `workdir` that is
|
||||
/// behind its remote-tracking counterpart (`refs/remotes/origin/<name>`),
|
||||
/// like a `git pull --ff-only` on each branch, so a mirror clone used for
|
||||
/// browsing catches up with the remote without ever rewriting history.
|
||||
///
|
||||
/// The checked-out branch is moved with a merge so its worktree follows
|
||||
/// (a dirty worktree fails the merge cleanly and is left for the next
|
||||
/// refresh); other branches are updated directly. Branches without a
|
||||
/// remote-tracking counterpart, or with local commits of their own, are
|
||||
/// left alone. Returns whether any branch moved.
|
||||
pub fn fast_forward_branches(workdir: &Path) -> Result<bool> {
|
||||
let current = git_in(workdir, &["branch", "--show-current"]).unwrap_or_default();
|
||||
let heads = refs_with_prefix(workdir, "refs/heads")?;
|
||||
let mut moved = false;
|
||||
|
||||
for head in heads {
|
||||
let Some(branch) = head.strip_prefix("refs/heads/") else {
|
||||
continue;
|
||||
};
|
||||
let remote = format!("refs/remotes/origin/{branch}");
|
||||
// No remote-tracking counterpart: the remote does not have it.
|
||||
let Ok(remote_oid) = git_in(workdir, &["rev-parse", "--verify", "--quiet", &remote]) else {
|
||||
continue;
|
||||
};
|
||||
let Ok(local_oid) = git_in(workdir, &["rev-parse", "--verify", "--quiet", &head]) else {
|
||||
continue;
|
||||
};
|
||||
if local_oid == remote_oid {
|
||||
continue;
|
||||
}
|
||||
// Only fast-forward: local-only commits (or diverged history) must
|
||||
// never be rewritten by a refresh.
|
||||
if git_in(workdir, &["merge-base", "--is-ancestor", &head, &remote]).is_err() {
|
||||
continue;
|
||||
}
|
||||
if current == branch {
|
||||
// Merge so the checked-out worktree follows the branch.
|
||||
if git_in(workdir, &["merge", "--ff-only", &remote]).is_ok() {
|
||||
moved = true;
|
||||
}
|
||||
} else {
|
||||
git_in(workdir, &["update-ref", &head, &remote_oid])?;
|
||||
moved = true;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(moved)
|
||||
}
|
||||
|
||||
/// Run a git command in `dir`, returning trimmed stdout. The terminal prompt
|
||||
/// is disabled so a credential request fails instead of hanging.
|
||||
fn git_in(dir: &Path, args: &[&str]) -> Result<String> {
|
||||
@@ -2456,6 +2515,12 @@ mod tests {
|
||||
git_in(&path, &["remote", "get-url", "origin"]).expect("url"),
|
||||
"https://gitnostr.com/npub1test/repo.git"
|
||||
);
|
||||
// The standard fetch mapping is configured with the remote, so a
|
||||
// later `git fetch origin` updates `refs/remotes/origin/*`.
|
||||
assert_eq!(
|
||||
git_in(&path, &["config", "remote.origin.fetch"]).expect("refspec"),
|
||||
"+refs/heads/*:refs/remotes/origin/*"
|
||||
);
|
||||
|
||||
// A second call must not override the existing remote.
|
||||
ensure_origin(&path, "https://other.example/repo.git").expect("keep");
|
||||
@@ -2533,6 +2598,75 @@ mod tests {
|
||||
assert!(destination.join("README.md").is_file());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fast_forward_branches_moves_the_mirror_and_keeps_local_work() {
|
||||
// A bare "server" like a grasp server's `{base}/{owner}/{repo}.git`
|
||||
// layout.
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let base_server = dir.path().join("npub1test").join("repo.git");
|
||||
std::fs::create_dir_all(base_server.parent().unwrap()).unwrap();
|
||||
let init_status = Command::new("git")
|
||||
.args(["init", "--bare", "-q"])
|
||||
.arg(&base_server)
|
||||
.status()
|
||||
.expect("spawn git init --bare");
|
||||
assert!(init_status.success());
|
||||
|
||||
// The owner's working repo pushes the initial commit.
|
||||
let (work_dir, work_repo) = fixture(&[("a.txt", b"one")]);
|
||||
commit_all(&work_repo, "initial");
|
||||
let work = work_dir.path();
|
||||
let base_url = format!("file://{}", dir.path().display());
|
||||
push_all(work, &base_url, "npub1test", "repo").expect("push");
|
||||
|
||||
// A mirror clone, like the app's GitCache clones.
|
||||
let mirror = dir.path().join("mirror");
|
||||
git_run(
|
||||
dir.path(),
|
||||
&[
|
||||
"clone",
|
||||
"-q",
|
||||
&format!("{base_url}/npub1test/repo.git"),
|
||||
mirror.to_str().unwrap(),
|
||||
],
|
||||
);
|
||||
let initial = git_in(&mirror, &["rev-parse", "HEAD"]).expect("initial");
|
||||
|
||||
// The owner pushes a new commit; the mirror fetches it but its
|
||||
// local `main` (and worktree) stay behind.
|
||||
std::fs::write(work.join("new.txt"), b"new\n").expect("write");
|
||||
commit_all(&gix::open(work).expect("open"), "new commit");
|
||||
push_all(work, &base_url, "npub1test", "repo").expect("push");
|
||||
git_run(&mirror, &["fetch", "origin"]);
|
||||
let remote = git_in(&mirror, &["rev-parse", "refs/remotes/origin/main"]).expect("remote");
|
||||
assert_eq!(
|
||||
git_in(&mirror, &["rev-parse", "HEAD"]).expect("local"),
|
||||
initial
|
||||
);
|
||||
assert_ne!(remote, initial);
|
||||
|
||||
// Fast-forwarding catches the branch and its worktree up; the
|
||||
// second call has nothing left to move.
|
||||
assert!(fast_forward_branches(&mirror).expect("ff"));
|
||||
assert_eq!(
|
||||
git_in(&mirror, &["rev-parse", "HEAD"]).expect("local"),
|
||||
remote
|
||||
);
|
||||
assert!(mirror.join("new.txt").is_file());
|
||||
assert!(!fast_forward_branches(&mirror).expect("idle"));
|
||||
|
||||
// A branch with local commits of its own is never touched.
|
||||
git_run(&mirror, &["checkout", "-b", "wip"]);
|
||||
std::fs::write(mirror.join("wip.txt"), b"wip\n").expect("write");
|
||||
commit_all(&gix::open(&mirror).expect("open"), "local wip");
|
||||
let wip = git_in(&mirror, &["rev-parse", "HEAD"]).expect("wip");
|
||||
assert!(!fast_forward_branches(&mirror).expect("wip skipped"));
|
||||
assert_eq!(
|
||||
git_in(&mirror, &["rev-parse", "HEAD"]).expect("wip kept"),
|
||||
wip
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fetch_repo_refs_imports_heads_under_a_prefix() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
|
||||
Reference in New Issue
Block a user