pub struct Repo {
pub style: Style,
pub branch_prefix: String,
pub state_store: StateStore,
pub followups: Followups,
pub drafts: Drafts,
/* private fields */
}Fields§
§style: Style§branch_prefix: String§state_store: StateStore§followups: Followups§drafts: DraftsImplementations§
Source§impl Repo
impl Repo
Sourcepub fn review_threads(&self, number: i64) -> Result<Vec<RawThread>>
pub fn review_threads(&self, number: i64) -> Result<Vec<RawThread>>
Inline review threads, with GitHub’s own resolved flag.
-F number= and not -f: -F converts a bare integer to a JSON
number, which is what Int! requires, while -f would send the string
“478” and the server would reject the whole query. -F owner={owner}
takes the placeholder from the checkout, so this works against any host
with no host handling of its own.
--paginate works because the query declares $endCursor and returns
pageInfo, and each page arrives as its own JSON document, which is the
shape parse_comment_pages already flattens.
Sourcepub fn pr_reviews(&self, number: i64) -> Vec<Value>
pub fn pr_reviews(&self, number: i64) -> Vec<Value>
Submitted review bodies. There is no thread to reply into, so these can only ever be answered with a comment on the pull request.
A PENDING review was never submitted and nobody else can see it. A DISMISSED one has been withdrawn. An empty body is every approval that came with only inline comments, which the threads already carry.
Sourcepub fn pr_review_comments(&self, number: i64) -> Vec<Value>
pub fn pr_review_comments(&self, number: i64) -> Vec<Value>
Inline comments without their threads. The fallback for a host where the GraphQL query will not run.
Sourcepub fn reply_in_thread(&self, pr: i64, root: i64, body: &str) -> Result<()>
pub fn reply_in_thread(&self, pr: i64, root: i64, body: &str) -> Result<()>
Reply inside an inline review thread.
REST and not GraphQL, deliberately. addPullRequestReviewThreadReply
needs the thread’s node id, which only the GraphQL read produces, while
this needs the id of the comment that started the thread, which spar has
on either path. So replying keeps working on a host where reading the
threads did not.
Sourcepub fn resolve_thread(&self, thread_id: &str) -> Result<()>
pub fn resolve_thread(&self, thread_id: &str) -> Result<()>
Mark a review thread resolved.
GraphQL only: REST has never exposed it. A token that cannot write to the repository cannot do this, which is not a reason to fail a run that has already said its piece, so the caller logs and carries on.
Source§impl Repo
impl Repo
pub fn open(root: impl AsRef<Path>, cfg: &Config) -> Result<Self>
pub fn root(&self) -> &Path
Sourcepub fn clean(&self, text: &str) -> Result<String>
pub fn clean(&self, text: &str) -> Result<String>
Scrub, then verify. A leak here reaches GitHub, so it is a hard error rather than a warning: silent partial compliance is how a style rule erodes over a long run.
Sourcepub fn clean_body(&self, text: &str) -> Result<String>
pub fn clean_body(&self, text: &str) -> Result<String>
Clean, and hold to a length budget. For anything a model wrote.
Sourcepub fn clean_issue_body(&self, text: &str) -> Result<String>
pub fn clean_issue_body(&self, text: &str) -> Result<String>
The same, with an issue’s far larger budget and its exemption for code.
Sourcepub fn clean_title(&self, text: &str) -> Result<String>
pub fn clean_title(&self, text: &str) -> Result<String>
The single transform every outbound title goes through.
Scrub first, clip second, and never the other way round. Clipping first lets the scrub lengthen the result past the budget (an em dash becomes two characters), so a second pass would clip again and produce a different string. That broke follow-up deduplication silently: the lookup searched for one title while GitHub had stored another, no match was ever found, and a fresh duplicate issue was filed every review round. Doing it in this order makes the transform idempotent, which the tests assert.
pub fn git(&self, args: &[&str]) -> Result<String>
pub fn git_at(&self, cwd: Option<&Path>, args: &[&str]) -> Result<String>
Sourcepub fn git_try(&self, args: &[&str]) -> String
pub fn git_try(&self, args: &[&str]) -> String
Run git, tolerating failure. Returns whatever landed on stdout.
pub fn git_try_at(&self, cwd: Option<&Path>, args: &[&str]) -> String
Sourcepub fn default_branch(&self, configured: &str) -> String
pub fn default_branch(&self, configured: &str) -> String
The base branch the remote actually points at, rather than assuming
main. Falls back to the configured value when there is no origin.
pub fn branch_for_issue(&self, issue: i64) -> String
pub fn branch_for_pr(&self, number: i64) -> String
Sourcepub fn branch_for_split(&self, parent: i64, index: usize) -> String
pub fn branch_for_split(&self, parent: i64, index: usize) -> String
One part of a split, numbered from 1 within its parent.
Its own namespace rather than issue-N, because the parts of a split
pull request have no issue of their own and would otherwise collide with
the branch of the issue that happens to share the parent’s number.
The name a part is tried on first. worktree_for_split may end up on a
suffixed one, because this name is not free forever.
pub fn known_branches(&self) -> BTreeMap<String, BranchRecord>
pub fn record_branch(&self, branch: &str, kind: &str, number: i64)
pub fn forget_branch(&self, branch: &str)
Sourcepub fn worktree_add(&self, issue: i64, base: &str) -> Result<(PathBuf, String)>
pub fn worktree_add(&self, issue: i64, base: &str) -> Result<(PathBuf, String)>
Isolate an issue so a failed run cannot poison the next one’s base.
Sourcepub fn commits_held_by(&self, branch: &str, base: &str, other: &str) -> bool
pub fn commits_held_by(&self, branch: &str, base: &str, other: &str) -> bool
Whether other already contains every commit branch has beyond
base. False when either ref fails to resolve, so a ref that is not
there cannot vouch for anything.
pub fn worktree_remove(&self, issue: i64)
Sourcepub fn worktree_for_pr(&self, pr: &PrView) -> Result<(PathBuf, String)>
pub fn worktree_for_pr(&self, pr: &PrView) -> Result<(PathBuf, String)>
Check an existing PR branch out into an isolated worktree.
Sourcepub fn worktree_for_pr_head(&self, number: i64) -> Result<PathBuf>
pub fn worktree_for_pr_head(&self, number: i64) -> Result<PathBuf>
Check a pull request’s head out read only, detached, with no branch.
Fetches refs/pull/N/head, which GitHub serves for every pull request
including one from a fork whose branch is not in this repository at all.
That is what makes reviewing an outside contribution possible when
pushing to it is not.
Detached on purpose. Review only mode has nothing to push, and a branch would only invite something to try.
Sourcepub fn worktree_for_split(
&self,
parent: i64,
index: usize,
start: &str,
) -> Result<(PathBuf, String)>
pub fn worktree_for_split( &self, parent: i64, index: usize, start: &str, ) -> Result<(PathBuf, String)>
A worktree for one part of a split, on a new branch off start.
start is the base branch for independent parts and the previous part’s
branch for stacked ones, which is the only difference between the two
shapes at this level.
The branch is whatever name was free, which is why it is returned rather than derived by the caller. Splitting the same pull request a second time would otherwise target the branch behind the first run’s pull request. Split pushes are create-only and would refuse that target, but a repeated split still needs distinct branches rather than a name that can never be created.
Sourcepub fn has_remote_split_branch(&self, parent: i64) -> Result<bool>
pub fn has_remote_split_branch(&self, parent: i64) -> Result<bool>
Whether a previous attempt pushed any branch for this split.
The parent comment is the normal retry marker. A branch is the fallback when that comment or the pull request creation failed after the push. Reading origin directly makes the guard survive a fresh clone.
Sourcepub fn release_split_worktree(&self, dir: &Path, branch: &str)
pub fn release_split_worktree(&self, dir: &Path, branch: &str)
Throw one part away: its worktree, its branch, and its record.
For a part that would not stand on its own. Nothing has been pushed at
that point, so this leaves no trace anywhere but the log. Takes what
worktree_for_split returned, since the name it settled on is not
derivable from the parent and the index.
pub fn release_review_worktree(&self, number: i64)
pub fn release_pr_worktree(&self, number: i64)
Sourcepub fn base_ref(&self, cwd: &Path, base: &str) -> String
pub fn base_ref(&self, cwd: &Path, base: &str) -> String
What to diff against: the remote tracking branch when it resolves, the local branch when it does not.
This is not a nicety. Every “did the agent do anything” check hangs off
this ref, and git log against a ref that does not exist fails silently
and reads as “no commits”. A checkout whose origin/main was never
fetched would report every implementation as abandoned and throw the
work away.
pub fn has_changes(&self, cwd: &Path, base: &str) -> bool
Sourcepub fn commit_count(&self, cwd: &Path, refname: &str, base: &str) -> usize
pub fn commit_count(&self, cwd: &Path, refname: &str, base: &str) -> usize
How many commits refname carries that the base does not.
Counted from the commits themselves rather than from commit_subjects,
which drops a commit whose message is empty. The guards in
worktree_add decide whether to delete a branch on this number, and an
empty message must not read as an empty branch.
Sourcepub fn commit_lines(&self, cwd: &Path, refname: &str, base: &str) -> Vec<String>
pub fn commit_lines(&self, cwd: &Path, refname: &str, base: &str) -> Vec<String>
One hash subject line per commit refname carries that the base does
not, oldest first. For showing a person what is on a branch, so the
hash keeps a commit with no message from listing as nothing.
Sourcepub fn commits_since(
&self,
cwd: &Path,
earlier: &str,
later: &str,
) -> Option<Vec<String>>
pub fn commits_since( &self, cwd: &Path, earlier: &str, later: &str, ) -> Option<Vec<String>>
The commits later carries that earlier does not, oldest first, when
earlier is genuinely behind it.
None when it is not an ancestor, which is not the same as nothing
having landed. rewrite_commits_if_needed rewrites hashes from the first
offending commit onward, so a head recorded before a round can still be a
readable object and no longer be on the branch. git log answers that
with every commit on the branch, so without the check the one caller
would report the whole branch as unread, which is the widest possible
wrong answer.
No base_ref resolution, unlike its neighbours: these are commits rather
than branch names, and putting a sha through it logs a fallback line
every time.
Sourcepub fn commit_subjects(
&self,
cwd: &Path,
refname: &str,
base: &str,
) -> Vec<String>
pub fn commit_subjects( &self, cwd: &Path, refname: &str, base: &str, ) -> Vec<String>
The subjects of the commits refname carries that the base does not,
oldest first.
Sourcepub fn changed_files(&self, cwd: &Path, base: &str) -> Vec<String>
pub fn changed_files(&self, cwd: &Path, base: &str) -> Vec<String>
The paths this checkout changes relative to the base, sorted.
A three dot range, matching diff_stat: what the branch did, not what
the base has done since.
--no-renames because a rename reported as its destination alone leaves
the source out of the list, and a part carrying only the destination
would be a copy. As a deletion and an addition it is two paths, which a
part can carry together or leave to the leftover report.
-z because without it git writes paths for display: anything
non-ASCII comes back escaped and wrapped in quotes, and that string is
not a path. A part built from one carries a pathspec matching no file,
so the file never reaches the slice while every list still says the part
took it. It also keeps a path with a space at either end intact.
Sourcepub fn merge_base(
&self,
cwd: &Path,
base: &str,
refname: &str,
) -> Result<String>
pub fn merge_base( &self, cwd: &Path, base: &str, refname: &str, ) -> Result<String>
Where refname left the base: the commit its own change is measured
from, and the one a slice of that change has to be taken against.
pub fn diff_stat(&self, cwd: &Path, base: &str) -> String
Sourcepub fn rewrite_commits_if_needed(&self, cwd: &Path, base: &str) -> Result<()>
pub fn rewrite_commits_if_needed(&self, cwd: &Path, base: &str) -> Result<()>
Scrub commit messages that slipped past the prompt.
git filter-branch calls back into this same binary, so there is no
interpreter to find and no second copy of the rules to drift.
Sourcepub fn push(&self, cwd: &Path, branch: &str) -> Result<()>
pub fn push(&self, cwd: &Path, branch: &str) -> Result<()>
Push by explicit refspec from HEAD.
A resumed PR is checked out under a local name (pr-N) that does not
match its remote branch, so pushing by branch name would resolve the
wrong local ref or fail outright.
Sourcepub fn push_split_branch(
&self,
cwd: &Path,
branch: &str,
) -> Result<(), SplitPushError>
pub fn push_split_branch( &self, cwd: &Path, branch: &str, ) -> Result<(), SplitPushError>
Create one remote branch for a split without ever moving an existing ref.
worktree_for_split chooses a name that is free locally and on origin,
but another writer can still take it before the push. An empty expected
value in the lease makes this an atomic create: it creates an absent ref,
accepts an identical ref as a no-op, and never moves an existing ref.
The shared push method cannot be used because its lease permits
updating a ref fetched earlier.
pub fn gh(&self, args: &[&str]) -> Result<String>
pub fn gh_at(&self, cwd: Option<&Path>, args: &[&str]) -> Result<String>
Sourcepub fn gh_stdin(&self, args: &[&str], stdin: &str) -> Result<String>
pub fn gh_stdin(&self, args: &[&str], stdin: &str) -> Result<String>
Run gh with something on its stdin.
A tracker body is far too long to pass on argv, and --body-file - is
how gh takes one. proc::exec already wires the pipe, so this is a
sibling of gh_at rather than anything new.
pub fn gh_try(&self, args: &[&str]) -> String
Sourcepub fn viewer_login(&self) -> Result<&str>
pub fn viewer_login(&self) -> Result<&str>
The login gh is authenticated as.
A hard error, never a degradation. Everything spar wrote has to be excluded from what it answers, and custody cannot be read from git authorship, so this is the only thing that tells spar’s own comments from somebody else’s. Without it the failure is not “answers a bit too much”, it is a thread where spar answers itself until somebody notices.
Not cached on disk: gh auth switch between runs would make a stored
answer wrong in exactly the way that produces that thread.
Sourcepub fn read_issue(&self, number: i64) -> Result<Issue>
pub fn read_issue(&self, number: i64) -> Result<Issue>
One issue as it stands, open or closed.
fetch_issues reads a queue to work: it drops a closed issue and fails
when nothing survives. Both are wrong for reading one issue back, where
closed is an answer and the empty case cannot arise.
pub fn fetch_issues(&self, numbers: &[i64]) -> Result<Vec<Issue>>
Sourcepub fn list_open_issues(
&self,
limit: usize,
min_number: i64,
) -> Result<Vec<i64>>
pub fn list_open_issues( &self, limit: usize, min_number: i64, ) -> Result<Vec<i64>>
Open issues, lowest numbered first. gh issue list excludes PRs.
pub fn list_open_prs(&self, limit: usize, min_number: i64) -> Result<Vec<i64>>
pub fn pr_for_branch(&self, branch: &str) -> Option<PrRef>
Sourcepub fn try_pr_for_branch(
&self,
branch: &str,
base: &str,
) -> Result<Option<PrRef>>
pub fn try_pr_for_branch( &self, branch: &str, base: &str, ) -> Result<Option<PrRef>>
The open pull request for a branch, preserving a failed lookup as an error when the caller is deciding whether a write already landed.
Sourcepub fn item_kind(&self, number: i64) -> Result<ItemKind>
pub fn item_kind(&self, number: i64) -> Result<ItemKind>
Whether a number names an issue or a pull request.
gh issue view happily returns a pull request when handed its number,
so it cannot be used to tell them apart. The issues API carries both and
marks a pull request with a pull_request key, which is definitive.
Sourcepub fn open_pr_for_issue(&self, issue: i64) -> Option<PrRef>
pub fn open_pr_for_issue(&self, issue: i64) -> Option<PrRef>
An open pull request that would close this issue, whoever opened it.
spar’s own branch naming is checked first because it is exact and cheap. Falling back to GitHub’s own issue linkage is what lets spar pick up a pull request a person started on a branch named anything at all.
pub fn pr_view(&self, number: i64) -> Result<PrView>
pub fn pr_state(&self, number: i64) -> String
Sourcepub fn pr_head_oid(&self, number: i64) -> Result<String>
pub fn pr_head_oid(&self, number: i64) -> Result<String>
The commit currently exposed as a pull request’s head.
pub fn create_pr( &self, cwd: &Path, branch: &str, base: &str, title: &str, body: &str, ) -> Result<PrRef>
pub fn comment_pr(&self, number: i64, body: &str) -> Result<()>
pub fn comment_issue(&self, number: i64, body: &str) -> Result<()>
Sourcepub fn close_issue(&self, number: i64, body: &str) -> Result<()>
pub fn close_issue(&self, number: i64, body: &str) -> Result<()>
Comment, then close as not planned.
Only ever called when both agents independently declined the issue: one agent’s opinion is not enough to close somebody’s report.
Sourcepub fn edit_issue_body(
&self,
number: i64,
expected: &str,
body: &str,
inserted: &str,
) -> Result<()>
pub fn edit_issue_body( &self, number: i64, expected: &str, body: &str, inserted: &str, ) -> Result<()>
Replace an issue body, refusing unless it is still byte for byte what the caller read and validating only the fragment spar inserted.
The only place spar rewrites text somebody else wrote, so the check is the whole point: an edit computed from a body that has since moved would silently delete whatever moved it. The caller decides whether another attempt is safe for its workflow.
Deliberately not through clean_issue_body. The body is mostly a
person’s own prose, and the scrub would rewrite their punctuation while
the length budget could truncate the end of a long report. inserted is
the only text here spar is answerable for, so it still passes through the
style gate. The full body travels over stdin because a tracker can be far
too long for one argument.
Sourcepub fn issue_body(&self, number: i64) -> Result<String>
pub fn issue_body(&self, number: i64) -> Result<String>
One issue’s body, exactly as GitHub holds it.
Sourcepub fn open_issue_rows(&self) -> Vec<Issue>
pub fn open_issue_rows(&self) -> Vec<Issue>
Every open issue with its title and body, in one call.
For a screen that has to say something about each of twenty items before anything expensive happens. One call rather than one per issue.
Sourcepub fn open_pr_rows(&self) -> Vec<PrRow>
pub fn open_pr_rows(&self) -> Vec<PrRow>
Every open pull request with its size, in one call.
pub fn create_issue(&self, title: &str, body: &str) -> Result<String>
pub fn create_issue_apart_from( &self, title: &str, body: &str, apart_from: Option<i64>, ) -> Result<String>
Source§impl Repo
impl Repo
Sourcepub fn find_similar_issue(
&self,
title: &str,
body: &str,
) -> Option<ExistingIssue>
pub fn find_similar_issue( &self, title: &str, body: &str, ) -> Option<ExistingIssue>
An issue that already describes this defect, however it was worded.
Exact title matching let duplicates through: two agents, or two runs a week apart, never word one defect identically. A real run filed two duplicates that way, and each had to be closed by hand afterwards. Titles alone are too thin to match on, so this compares titles and bodies together.
Sourcepub fn find_similar_issue_apart_from(
&self,
title: &str,
body: &str,
apart_from: Option<i64>,
) -> Option<ExistingIssue>
pub fn find_similar_issue_apart_from( &self, title: &str, body: &str, apart_from: Option<i64>, ) -> Option<ExistingIssue>
The same search, with one issue that cannot be its own duplicate.
A tracker’s body quotes every item in its checklist, so searching for an item’s words matches the tracker before it matches anything else. That would link an item to the issue it is written in.
Sourcepub fn try_find_similar_issue_apart_from(
&self,
title: &str,
body: &str,
apart_from: Option<i64>,
) -> Result<Option<ExistingIssue>>
pub fn try_find_similar_issue_apart_from( &self, title: &str, body: &str, apart_from: Option<i64>, ) -> Result<Option<ExistingIssue>>
The same search, preserving lookup failure for a caller about to write.
Sourcepub fn find_issue_by_title(&self, title: &str) -> Option<String>
pub fn find_issue_by_title(&self, title: &str) -> Option<String>
Avoid filing a duplicate when a follow-up already exists.
Sourcepub fn mark_ready(&self, number: i64) -> bool
pub fn mark_ready(&self, number: i64) -> bool
Squash merge, tolerating cleanup failures after a successful merge.
Take a pull request out of draft, once the review has converged.
Best effort. A draft that stayed a draft is a cosmetic problem, and failing the run over it would throw away a review that has already finished and been posted.
Sourcepub fn merge_pr(&self, number: i64) -> Result<()>
pub fn merge_pr(&self, number: i64) -> Result<()>
gh pr merge --delete-branch exits non-zero when it cannot delete the
local branch, which happens after the merge has already landed.
Treating that as a failure reports work as lost when it is not.
Sourcepub fn merge_pr_at_head(&self, number: i64, expected_head: &str) -> Result<()>
pub fn merge_pr_at_head(&self, number: i64, expected_head: &str) -> Result<()>
Squash merge only if the pull request still exposes the reviewed head.
Sourcepub fn followups_path(&self) -> PathBuf
pub fn followups_path(&self) -> PathBuf
The queue of follow-ups recorded locally rather than filed, which
spar followup works.
Sourcepub fn worked_followups_path(&self) -> PathBuf
pub fn worked_followups_path(&self) -> PathBuf
What spar followup already dealt with, kept beside the queue.
Two jobs. It is what stops append_local_followup re-recording a
follow-up whose entry has since left the queue, which would otherwise
turn the file into a ring buffer of things already filed. And it keeps
the text of an entry a screening pass ruled stale, so a wrong verdict
costs a re-read rather than the only copy of a real defect.
Sourcepub fn checkin_state_path(&self, number: i64) -> PathBuf
pub fn checkin_state_path(&self, number: i64) -> PathBuf
What spar checkin has already answered on one pull request or issue.
Sourcepub fn append_local_followup(&self, title: &str, body: &str) -> Followup
pub fn append_local_followup(&self, title: &str, body: &str) -> Followup
Append a follow-up to a local note instead of the tracker.
Deduplicated on the title, matching the issue path. The body arrives with its provenance already stamped by the caller, so nothing is added here.
A write that did not happen is reported as such rather than as a duplicate: the caller settles the point on the strength of this answer, and settling it on a failed write is how a real defect is lost.
Both files are checked, because spar followup removes an entry from
the queue once it has filed it. Checking only the queue would let the
next run that rediscovers the same defect append it again, on top of the
issue that now exists for it.
Sourcepub fn archive_followup(&self, title: &str, body: &str, verdict: &str)
pub fn archive_followup(&self, title: &str, body: &str, verdict: &str)
Record what spar followup did with an entry, and why.
Best effort: an archive that could not be written is not a reason to stop, since the entry has already been filed or ruled on.
Sourcepub fn pending_comment_path(&self, number: i64) -> PathBuf
pub fn pending_comment_path(&self, number: i64) -> PathBuf
Where a comment spar produced but did not post is kept.
Sourcepub fn save_pending_comment(&self, number: i64, text: &str) -> Result<PathBuf>
pub fn save_pending_comment(&self, number: i64, text: &str) -> Result<PathBuf>
Keep a comment spar decided not to post.
A dry run that prints and forgets means agreeing with what you read costs a second full review. Saving it makes the whole point of reading it first: look, edit if you like, then post what you already paid for.
pub fn read_pending_comment(&self, number: i64) -> Option<String>
pub fn state_path(&self, number: i64) -> PathBuf
pub fn read_state(&self, pr: &PrView) -> Option<PersistedState>
pub fn write_state(&self, number: i64, state: &PersistedState) -> Result<()>
Sourcepub fn issue_comments(&self, number: i64) -> Vec<Value>
pub fn issue_comments(&self, number: i64) -> Vec<Value>
Top level comments. Works for issues and pull requests alike, because GitHub serves both from the issues endpoint.
Nothing when they cannot be read, which suits a reader that is going to
go on regardless. A caller deciding whether it has already written here
wants try_issue_comments, since for that one no comments and no answer
are opposite answers.
pub fn try_issue_comments(&self, number: i64) -> Result<Vec<Value>>
Sourcepub fn clear_state(&self, number: i64)
pub fn clear_state(&self, number: i64)
Drop state once the PR is finished and there is nothing to resume.
Sourcepub fn prune_state(&self) -> Vec<String>
pub fn prune_state(&self) -> Vec<String>
Remove state files whose PR is merged or closed.
Sourcepub fn prune_pr_state(&self, numbers: Option<Vec<i64>>) -> Vec<String>
pub fn prune_pr_state(&self, numbers: Option<Vec<i64>>) -> Vec<String>
Delete state comments from PRs that are finished.
Open PRs are left alone: their state may still be live.
Sourcepub fn prune_worktrees(&self, force_all: bool) -> Vec<String>
pub fn prune_worktrees(&self, force_all: bool) -> Vec<String>
Drop worktrees whose PR is finished, then the branches they left behind.
With auto_merge off, which is the default, a run ends at “approved”, so
nothing would ever clean these up on its own and they accumulate one per
run. A stranded worktree also holds its branch checked out, which makes
a later gh pr merge --delete-branch fail to clean up.
Sourcepub fn prune_branches(&self, force_all: bool) -> Vec<String>
pub fn prune_branches(&self, force_all: bool) -> Vec<String>
Delete leftover branches spar created whose worktree is already gone.
Deletion is driven by the ledger of branches spar actually created, not
by a name pattern. Names default to issue-N, which is exactly what a
person would call a branch themselves, so a name alone can never
establish ownership. This is the data loss guard.