A file-based task manager
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Sync remote ref deletions through push and pull

git_push_refs now pushes deletion refspecs for refs present in the
shadow but missing locally, and update_remote_shadow prunes shadow
entries that no longer exist locally — without this a subsequent pull
would see the still-shadowed remote OID and recreate the local ref.

git_pull_refs fetches with --prune so the shadow tracks remote
deletions, and applies those deletions locally when the local ref
hasn't moved since the last sync (otherwise: conflict).

Surfaced by `tsk reject`: the inbox blob deletion was being undone by
the auto-pull on the next `tsk inbox`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+65 -1
+65 -1
src/workspace.rs
··· 1006 1006 let repo = git2::Repository::open(self.require_git_dir()?)?; 1007 1007 let mut leases: Vec<String> = Vec::new(); 1008 1008 let mut refspecs: Vec<String> = Vec::new(); 1009 + let mut local_rests: std::collections::BTreeSet<String> = std::collections::BTreeSet::new(); 1009 1010 for r in repo.references()? { 1010 1011 let r = r?; 1011 1012 let Some(name) = r.name() else { continue }; ··· 1015 1016 let Some(local_oid) = r.target() else { 1016 1017 continue; 1017 1018 }; 1019 + local_rests.insert(rest.to_string()); 1018 1020 if shadow.get(rest) == Some(&local_oid) { 1019 1021 continue; // up to date 1020 1022 } ··· 1023 1025 } 1024 1026 refspecs.push(format!("refs/tsk/{rest}:refs/tsk/{rest}")); 1025 1027 } 1028 + // Refs that exist on the remote (per shadow) but no longer locally: 1029 + // push as deletions so the remote stays in sync with local removals 1030 + // (e.g. `tsk reject` / `tsk accept` consuming an inbox blob). 1031 + for (rest, expected) in &shadow { 1032 + if !local_rests.contains(rest) { 1033 + leases.push(format!("--force-with-lease=refs/tsk/{rest}:{expected}")); 1034 + refspecs.push(format!(":refs/tsk/{rest}")); 1035 + } 1036 + } 1026 1037 if refspecs.is_empty() { 1027 1038 return Ok(()); 1028 1039 } ··· 1051 1062 // Snapshot pre-fetch shadow so we know the previous remote position. 1052 1063 let pre_fetch: BTreeMap<String, git2::Oid> = 1053 1064 self.read_shadow(remote)?.into_iter().collect(); 1054 - // Fetch (force, into our private shadow only). 1065 + // Fetch (force, into our private shadow only). --prune so that refs 1066 + // deleted on the remote are removed from the shadow too — otherwise 1067 + // a stale shadow entry will be re-applied to the local ref below. 1055 1068 self.run_git(&[ 1056 1069 "fetch", 1070 + "--prune", 1057 1071 remote, 1058 1072 &format!("+refs/tsk/*:refs/remotes-tsk/{remote}/*"), 1059 1073 ])?; ··· 1093 1107 PullAction::Conflict => conflicts.push(rel.clone()), 1094 1108 } 1095 1109 } 1110 + // Apply remote deletions: refs that were in pre_fetch but vanished 1111 + // post_fetch (remote dropped them, e.g. via inbox accept/reject on 1112 + // another clone). If the local ref hasn't moved since the last sync 1113 + // we delete it; if it diverged, treat as a conflict. 1114 + for (rel, &old_remote) in &pre_fetch { 1115 + if post_fetch.contains_key(rel) { 1116 + continue; 1117 + } 1118 + if rebased_handled.contains(rel) { 1119 + continue; 1120 + } 1121 + let local_refname = format!("refs/tsk/{rel}"); 1122 + let local_oid = repo 1123 + .find_reference(&local_refname) 1124 + .ok() 1125 + .and_then(|r| r.target()); 1126 + match local_oid { 1127 + None => {} // already gone 1128 + Some(l) if l == old_remote => { 1129 + if let Ok(mut r) = repo.find_reference(&local_refname) { 1130 + r.delete()?; 1131 + } 1132 + } 1133 + Some(_) => conflicts.push(rel.clone()), 1134 + } 1135 + } 1096 1136 if !conflicts.is_empty() { 1097 1137 return Err(Error::Parse(format!( 1098 1138 "pull conflicts on: {}\n(local and remote both diverged from the last sync; \ ··· 1139 1179 Some((rest, oid)) 1140 1180 }) 1141 1181 .collect(); 1182 + let local_rests: std::collections::BTreeSet<String> = 1183 + updates.iter().map(|(r, _)| r.clone()).collect(); 1142 1184 for (rest, oid) in updates { 1143 1185 repo.reference( 1144 1186 &format!("{dest_prefix}{rest}"), ··· 1146 1188 true, 1147 1189 "tsk push shadow", 1148 1190 )?; 1191 + } 1192 + // Prune shadow entries for refs that no longer exist locally — after a 1193 + // successful push the remote also dropped them, so the shadow must too 1194 + // or a future pull will see them as "remote still has it" and recreate 1195 + // the local ref. 1196 + let stale: Vec<String> = repo 1197 + .references()? 1198 + .filter_map(|r| { 1199 + let r = r.ok()?; 1200 + let name = r.name()?.to_string(); 1201 + let rest = name.strip_prefix(&dest_prefix)?.to_string(); 1202 + if local_rests.contains(&rest) { 1203 + None 1204 + } else { 1205 + Some(name) 1206 + } 1207 + }) 1208 + .collect(); 1209 + for name in stale { 1210 + if let Ok(mut r) = repo.find_reference(&name) { 1211 + r.delete()?; 1212 + } 1149 1213 } 1150 1214 Ok(()) 1151 1215 }