[F] Fix heuristic
This commit is contained in:
@@ -134,7 +134,7 @@ git-sync sync --jobs 8
|
|||||||
|
|
||||||
While jobs run, the bottom of the terminal shows one live status line per worker. When a repository finishes, its detailed log is printed as one complete block above those status lines. The default is 4 workers; use `--jobs 1` for serial sync.
|
While jobs run, the bottom of the terminal shows one live status line per worker. When a repository finishes, its detailed log is printed as one complete block above those status lines. The default is 4 workers; use `--jobs 1` for serial sync.
|
||||||
|
|
||||||
`git-sync` stores a small ref cache in the work directory. On later runs it first checks each repository with `git ls-remote --heads --tags`; when all endpoints report the same refs as the last successful sync, it skips the full fetch/push pass for that repository.
|
`git-sync` stores a small ref cache in the work directory. On later runs it first checks each repository with `git ls-remote --heads --tags`; when all endpoints report the same refs as the last successful sync, or the existing local bare mirror cache already has those refs, it skips the full fetch/push pass for that repository.
|
||||||
|
|
||||||
Use cron or another scheduler for automatic execution:
|
Use cron or another scheduler for automatic execution:
|
||||||
|
|
||||||
|
|||||||
+58
-3
@@ -109,6 +109,27 @@ impl GitMirror {
|
|||||||
self.run(["fetch", "--prune", &remote.name, &tag_refspec])
|
self.run(["fetch", "--prune", &remote.name, &tag_refspec])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn cached_remote_refs_match(
|
||||||
|
&self,
|
||||||
|
remote: &RemoteSpec,
|
||||||
|
expected: &RemoteRefSnapshot,
|
||||||
|
) -> Result<bool> {
|
||||||
|
if !self.path.exists() || self.dry_run {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
let branches = self.remote_branches(&remote.name)?;
|
||||||
|
let tags = self.remote_tags(&remote.name)?;
|
||||||
|
let mut refs = Vec::with_capacity(branches.len() + tags.len());
|
||||||
|
for (branch, sha) in branches {
|
||||||
|
refs.push(format!("{sha}\trefs/heads/{branch}"));
|
||||||
|
}
|
||||||
|
for (tag, sha) in tags {
|
||||||
|
refs.push(format!("{sha}\trefs/tags/{tag}"));
|
||||||
|
}
|
||||||
|
let snapshot = snapshot_from_refs(refs);
|
||||||
|
Ok(&snapshot == expected)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn branch_decisions(
|
pub fn branch_decisions(
|
||||||
&self,
|
&self,
|
||||||
remotes: &[RemoteSpec],
|
remotes: &[RemoteSpec],
|
||||||
@@ -432,18 +453,23 @@ pub fn ls_remote_refs(remote: &RemoteSpec, redactor: &Redactor) -> Result<Remote
|
|||||||
return Err(GitCommandError::new("git ls-remote", stdout, stderr).into());
|
return Err(GitCommandError::new("git ls-remote", stdout, stderr).into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut refs = String::from_utf8_lossy(&output.stdout)
|
let refs = String::from_utf8_lossy(&output.stdout)
|
||||||
.lines()
|
.lines()
|
||||||
.map(str::trim)
|
.map(str::trim)
|
||||||
.filter(|line| !line.is_empty())
|
.filter(|line| !line.is_empty())
|
||||||
.map(ToOwned::to_owned)
|
.map(ToOwned::to_owned)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
Ok(snapshot_from_refs(refs))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn snapshot_from_refs(mut refs: Vec<String>) -> RemoteRefSnapshot {
|
||||||
refs.sort();
|
refs.sort();
|
||||||
|
|
||||||
Ok(RemoteRefSnapshot {
|
RemoteRefSnapshot {
|
||||||
hash: stable_ref_hash(&refs),
|
hash: stable_ref_hash(&refs),
|
||||||
refs: refs.len(),
|
refs: refs.len(),
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stable_ref_hash(refs: &[String]) -> String {
|
fn stable_ref_hash(refs: &[String]) -> String {
|
||||||
@@ -698,6 +724,35 @@ mod tests {
|
|||||||
assert!(main.target_remotes.is_empty());
|
assert!(main.target_remotes.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cached_remote_refs_match_ls_remote_snapshot_after_fetch() {
|
||||||
|
let fixture = GitFixture::new();
|
||||||
|
fixture.commit("base", "base", 1_700_000_000);
|
||||||
|
fixture.tag("v1");
|
||||||
|
fixture.push_head(&fixture.remote_a, "main");
|
||||||
|
fixture.push_tag(&fixture.remote_a, "v1");
|
||||||
|
|
||||||
|
let mirror = fixture.mirror();
|
||||||
|
let remote = fixture.remotes().remove(0);
|
||||||
|
assert!(
|
||||||
|
!mirror
|
||||||
|
.cached_remote_refs_match(
|
||||||
|
&remote,
|
||||||
|
&ls_remote_refs(&remote, &Redactor::new(Vec::new())).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
mirror.fetch_remote(&remote).unwrap();
|
||||||
|
let snapshot = ls_remote_refs(&remote, &Redactor::new(Vec::new())).unwrap();
|
||||||
|
assert!(mirror.cached_remote_refs_match(&remote, &snapshot).unwrap());
|
||||||
|
|
||||||
|
fixture.commit("newer", "newer", 1_700_000_100);
|
||||||
|
fixture.push_head(&fixture.remote_a, "main");
|
||||||
|
let changed = ls_remote_refs(&remote, &Redactor::new(Vec::new())).unwrap();
|
||||||
|
assert!(!mirror.cached_remote_refs_match(&remote, &changed).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn branch_decisions_report_divergent_tips_without_force() {
|
fn branch_decisions_report_divergent_tips_without_force() {
|
||||||
let fixture = GitFixture::new();
|
let fixture = GitFixture::new();
|
||||||
|
|||||||
+40
-1
@@ -230,6 +230,15 @@ impl From<RemoteRefSnapshot> for RemoteRefState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<&RemoteRefState> for RemoteRefSnapshot {
|
||||||
|
fn from(value: &RemoteRefState) -> Self {
|
||||||
|
Self {
|
||||||
|
hash: value.hash.clone(),
|
||||||
|
refs: value.refs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||||
struct RefState {
|
struct RefState {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -659,8 +668,9 @@ fn sync_repo(
|
|||||||
let Some(initial_ref_state) = check_remote_refs(context, repo_name, &initial_remotes)? else {
|
let Some(initial_ref_state) = check_remote_refs(context, repo_name, &initial_remotes)? else {
|
||||||
return Ok(RepoSyncOutcome::default());
|
return Ok(RepoSyncOutcome::default());
|
||||||
};
|
};
|
||||||
|
let all_endpoints_present = all_configured_endpoints_present(context.mirror, repos);
|
||||||
if !context.dry_run
|
if !context.dry_run
|
||||||
&& all_configured_endpoints_present(context.mirror, repos)
|
&& all_endpoints_present
|
||||||
&& ref_state.repo_matches(&context.mirror.name, repo_name, &initial_ref_state)
|
&& ref_state.repo_matches(&context.mirror.name, repo_name, &initial_ref_state)
|
||||||
{
|
{
|
||||||
crate::logln!(
|
crate::logln!(
|
||||||
@@ -677,6 +687,19 @@ fn sync_repo(
|
|||||||
let mirror_repo = GitMirror::open(path, context.redactor.clone(), context.dry_run)?;
|
let mirror_repo = GitMirror::open(path, context.redactor.clone(), context.dry_run)?;
|
||||||
|
|
||||||
mirror_repo.configure_remotes(&initial_remotes)?;
|
mirror_repo.configure_remotes(&initial_remotes)?;
|
||||||
|
if !context.dry_run
|
||||||
|
&& all_endpoints_present
|
||||||
|
&& cached_refs_match(&mirror_repo, &initial_remotes, &initial_ref_state)?
|
||||||
|
{
|
||||||
|
crate::logln!(
|
||||||
|
" {} refs unchanged from local mirror cache",
|
||||||
|
style("up-to-date").green().bold()
|
||||||
|
);
|
||||||
|
return Ok(RepoSyncOutcome {
|
||||||
|
ref_update: Some(initial_ref_state),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
for remote in &initial_remotes {
|
for remote in &initial_remotes {
|
||||||
if let Err(error) = mirror_repo.fetch_remote(remote) {
|
if let Err(error) = mirror_repo.fetch_remote(remote) {
|
||||||
if is_disabled_repository_error(&error) {
|
if is_disabled_repository_error(&error) {
|
||||||
@@ -763,6 +786,22 @@ fn all_configured_endpoints_present(mirror: &MirrorConfig, repos: &[EndpointRepo
|
|||||||
.all(|endpoint| present.contains(endpoint))
|
.all(|endpoint| present.contains(endpoint))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn cached_refs_match(
|
||||||
|
mirror_repo: &GitMirror,
|
||||||
|
remotes: &[RemoteSpec],
|
||||||
|
expected_refs: &BTreeMap<String, RemoteRefState>,
|
||||||
|
) -> Result<bool> {
|
||||||
|
for remote in remotes {
|
||||||
|
let Some(expected) = expected_refs.get(&remote.name) else {
|
||||||
|
return Ok(false);
|
||||||
|
};
|
||||||
|
if !mirror_repo.cached_remote_refs_match(remote, &RemoteRefSnapshot::from(expected))? {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
fn check_remote_refs(
|
fn check_remote_refs(
|
||||||
context: &RepoSyncContext<'_>,
|
context: &RepoSyncContext<'_>,
|
||||||
repo_name: &str,
|
repo_name: &str,
|
||||||
|
|||||||
Reference in New Issue
Block a user