pub struct Repo {
pub style: Style,
pub branch_prefix: String,
pub state_store: StateStore,
pub followups: Followups,
pub drafts: Drafts,
pub fork_when_denied: bool,
/* private fields */
}Fields§
§style: Style§branch_prefix: String§state_store: StateStore§followups: Followups§drafts: Drafts§fork_when_denied: boolPublish a new branch from a fork when origin refuses the push.
Implementations§
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. Nothing when they cannot be read.
Sourcepub fn try_pr_reviews(&self, number: i64) -> Result<Vec<Value>>
pub fn try_pr_reviews(&self, number: i64) -> Result<Vec<Value>>
Every submitted review, unfiltered, and an error when GitHub could not be read. The duplicate check before a verdict is posted needs to tell “no reviews” from “no answer”.
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 try_pr_review_comments(&self, number: i64) -> Result<Vec<Value>>
pub fn try_pr_review_comments(&self, number: i64) -> Result<Vec<Value>>
The same, and an error when GitHub could not be read.
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 failure does not stop the remaining replies, but it is included in the final write summary and makes the command return non-zero after that work finishes.
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) -> bool
Sourcepub fn worktree_for_pr(
&self,
pr: &PrView,
target: &PushTarget,
) -> Result<(PathBuf, String)>
pub fn worktree_for_pr( &self, pr: &PrView, target: &PushTarget, ) -> Result<(PathBuf, String)>
Check an existing PR branch out into an isolated worktree, from
wherever target says the branch is published.
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.
Sourcepub fn discard_split_worktree(
&self,
dir: &Path,
branch: &str,
disposable_head: &str,
) -> bool
pub fn discard_split_worktree( &self, dir: &Path, branch: &str, disposable_head: &str, ) -> bool
Discard one exact mechanical slice that the split workflow just made.
Unlike ordinary release, this intentionally removes an unpushed commit. The caller supplies the exact disposable tip, and every file, worktree, ownership, and ref check must still match before anything is removed.
pub fn release_review_worktree(&self, number: i64)
pub fn release_pr_worktree(&self, number: i64) -> bool
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 spar made onward, so a head recorded part way through 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 files_changed_between(
&self,
cwd: &Path,
from: &str,
to: &str,
) -> Vec<String>
pub fn files_changed_between( &self, cwd: &Path, from: &str, to: &str, ) -> Vec<String>
The files one commit range touched, for checking a claim against the diff rather than against the report that made it.
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,
floor: Option<&str>,
) -> Result<()>
pub fn rewrite_commits_if_needed( &self, cwd: &Path, base: &str, floor: Option<&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.
floor is the head spar found when it started, and nothing at or below
it is rewritten. The style gate is for what spar writes, and a person’s
commit message is not that: rewriting one changes a published SHA, which
diverges the branch under somebody’s own checkout and is exactly the
history rewrite this design promises never to do. A None floor means
every commit above the base is spar’s own, which is true of a branch
spar created in this invocation and of nothing else.
Sourcepub fn push(
&self,
cwd: &Path,
branch: &str,
expected: Option<&str>,
) -> Result<()>
pub fn push( &self, cwd: &Path, branch: &str, expected: Option<&str>, ) -> Result<()>
Push by explicit refspec from HEAD, leasing the head spar reviewed.
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.
expected is the remote head spar last read. A bare --force-with-lease
leases the implicit tracking ref instead, which is shared across every
worktree and is updated by any git fetch. The editing agent is a real
CLI with shell access, and fetching is a normal thing for it to do: one
git fetch mid round moves the tracking ref to whatever the remote holds
and the lease then passes against a head spar never reviewed, which
silently drops somebody’s commit. Naming the head makes the push refuse
exactly that case.
Sourcepub fn push_to(
&self,
cwd: &Path,
target: &PushTarget,
branch: &str,
expected: Option<&str>,
) -> Result<()>
pub fn push_to( &self, cwd: &Path, target: &PushTarget, branch: &str, expected: Option<&str>, ) -> Result<()>
push, to wherever the branch was first published.
Sourcepub fn publish_new_branch(
&self,
cwd: &Path,
branch: &str,
base: &str,
) -> Result<PushTarget>
pub fn publish_new_branch( &self, cwd: &Path, branch: &str, base: &str, ) -> Result<PushTarget>
Publish a branch created in this invocation, from a fork if origin refuses it.
Only a new branch can move. A pull request that already exists has its branch where it is, and pushing it anywhere else would not update it. A new one has no pull request yet, so a fork is just where it is opened from, which is how anybody without write access contributes.
One write whichever way it goes. The refusal is not counted as a failure when the fork takes the branch: the run did what it set out to, and a failed write makes the exit status say otherwise.
Sourcepub fn pr_push_target(&self, pr: &PrView) -> Option<PushTarget>
pub fn pr_push_target(&self, pr: &PrView) -> Option<PushTarget>
Where spar can push a pull request’s branch: origin for a branch in this repository, the fork for one on the authenticated user’s own fork, such as one spar opened after origin refused it, and nowhere for anybody else’s.
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 gh_api_raw(
&self,
endpoint: &str,
headers: &[(&str, &str)],
) -> Result<ApiResponse>
pub fn gh_api_raw( &self, endpoint: &str, headers: &[(&str, &str)], ) -> Result<ApiResponse>
gh api -i: the status line and headers with the body, and the exit
code ignored on purpose. A 304 Not Modified, which is the answer a
conditional poll wants, makes gh exit 1 while printing exactly what is
needed. Only a spawn failure or a timeout is an error here.
Sourcepub fn sweep_open_prs(&self) -> Result<Vec<PrSweepRow>>
pub fn sweep_open_prs(&self) -> Result<Vec<PrSweepRow>>
Every open pull request, with what a sweep needs to know about each.
Sourcepub fn sweep_pr(&self, number: i64) -> Result<PrSweepRow>
pub fn sweep_pr(&self, number: i64) -> Result<PrSweepRow>
One pull request, open or not, with what a sweep needs to know.
Sourcepub fn team_has_member(&self, team: &str, login: &str) -> Option<bool>
pub fn team_has_member(&self, team: &str, login: &str) -> Option<bool>
Whether login is in a team, named org/slug as gh gives a team in
reviewRequests. GitHub answers 404 for a team the login is not in, and
a team the viewer is in is one it can always see. None when GitHub did
not answer either way.
Sourcepub fn review_requests_made(&self, number: i64) -> Option<Vec<ReviewRequested>>
pub fn review_requests_made(&self, number: i64) -> Option<Vec<ReviewRequested>>
Every review request made on a pull request, oldest first. None when GitHub did not answer.
A review spar posts as a comment leaves the request it answered open,
so reviewRequests alone cannot tell that request from one made after
the review. When it was made can.
Sourcepub fn commits_between(&self, from: &str, to: &str) -> Option<u64>
pub fn commits_between(&self, from: &str, to: &str) -> Option<u64>
How many commits lie between two SHAs, when GitHub will say.
Sourcepub fn repo_name(&self) -> Option<String>
pub fn repo_name(&self) -> Option<String>
owner/name, for the line that says what is being watched.
pub fn listen_state_path(&self) -> PathBuf
pub fn listen_lock_path(&self) -> PathBuf
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 try_open_pr_for_issue(
&self,
issue: i64,
base: &str,
) -> Result<Option<PrRef>>
pub fn try_open_pr_for_issue( &self, issue: i64, base: &str, ) -> Result<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.
An error rather than None when the question could not be asked. The
two answers are not interchangeable: “no pull request exists” is the one
that makes spar implement over the top of somebody’s work and open a
second pull request for the same issue.
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.
Sourcepub fn live_head(&self, pr: &PrView) -> Result<LiveHead>
pub fn live_head(&self, pr: &PrView) -> Result<LiveHead>
Where a pull request’s head stands now: what GitHub’s pull request
names, and, for a branch in this repository, where origin has the
branch. The two differ until GitHub catches up with a push.
Sourcepub fn create_pr(
&self,
cwd: &Path,
branch: &str,
target: &PushTarget,
base: &str,
title: &str,
body: &str,
) -> Result<PrRef>
pub fn create_pr( &self, cwd: &Path, branch: &str, target: &PushTarget, base: &str, title: &str, body: &str, ) -> Result<PrRef>
Open a pull request from branch, published at target.
The lookup that confirms it landed goes by branch name alone, which
gh pr list --head matches on a fork’s pull request too.
pub fn comment_pr(&self, number: i64, body: &str) -> Result<()>
Sourcepub fn submit_review(
&self,
number: i64,
verdict: Option<ReviewVerdict>,
body: &str,
commit: &str,
) -> Result<()>
pub fn submit_review( &self, number: i64, verdict: Option<ReviewVerdict>, body: &str, commit: &str, ) -> Result<()>
Submit a formal review, or a comment carrying the verdict’s words when GitHub will not take the verdict: the viewer’s own pull request, or no verdict at all.
Mirrors comment_pr step for step: the style gate, the signature, a
duplicate check against the viewer’s own reviews, the write, and a read
back on failure so an unverifiable write is reported as one.
commit is the commit the reviewers read. GitHub attaches a review
that names no commit to whatever the head is when it arrives, so a push
made while a review ran would otherwise receive an approval nobody
gave it. gh pr review cannot name one, hence the REST call.
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. The search a tracker runs, which wants closed issues too: a checklist item matching a closed issue is a done item that wants a link.
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 search a filing runs, over open issues only.
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 for the remaining workflow. A failure does not discard the review or stop later independent work, but the final write summary reports it and the command returns non-zero.
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,
delete_branch: bool,
) -> Result<()>
pub fn merge_pr_at_head( &self, number: i64, expected_head: &str, delete_branch: bool, ) -> 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 brainstorms_dir(&self) -> PathBuf
pub fn brainstorms_dir(&self) -> PathBuf
Where spar brainstorm writes a session unless told otherwise.
Under the state directory, so it is kept out of the repository’s own
git status; --out puts one somewhere a person would commit.
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 spend_path(&self) -> PathBuf
pub fn spend_path(&self) -> PathBuf
What spar checkin has already answered on one pull request or issue.
Where what a run spent is kept.
Sourcepub fn record_spend(&self, calls: &[Spent]) -> Result<()>
pub fn record_spend(&self, calls: &[Spent]) -> Result<()>
Append this run’s calls to the record.
Appended rather than replaced, so a resumed pull request adds to the count of what it has cost so far rather than starting it again, and so anybody charting this has a file to chart.
Sourcepub fn implementors_path(&self) -> PathBuf
pub fn implementors_path(&self) -> PathBuf
Who wrote the first draft of each issue, by number.
Custody alternates within a pull request and the record of it lives in that pull request’s state. This is the other question, which nothing answered: over a queue, which model authored what.
pub fn read_implementors(&self) -> BTreeMap<i64, String>
Sourcepub fn remember_implementor(&self, issue: i64, agent: &str)
pub fn remember_implementor(&self, issue: i64, agent: &str)
Best effort: this is a record for a person, not something the loop reads back, so a failure to write it is not a reason to stop.
Sourcepub fn contested_path(&self) -> PathBuf
pub fn contested_path(&self) -> PathBuf
Where the issues triage disagreed about are remembered.
One file rather than one per issue: it is read once per run, before any triage call, to decide what not to ask about again.
Sourcepub fn read_contested(&self) -> BTreeMap<i64, Contested>
pub fn read_contested(&self) -> BTreeMap<i64, Contested>
The issues a person has been asked to decide, by number.
Sourcepub fn remember_contested(&self, items: &[ContestedItem])
pub fn remember_contested(&self, items: &[ContestedItem])
Remember an issue triage could not agree on.
Best effort: a run that cannot write this asks again next time, which is what it did before this existed.
Sourcepub fn forget_contested(&self, numbers: &[i64])
pub fn forget_contested(&self, numbers: &[i64])
Ask about them again, because a person has acted on them.
pub fn checkin_state_path(&self, number: i64) -> PathBuf
Sourcepub fn review_record_path(&self, number: i64) -> PathBuf
pub fn review_record_path(&self, number: i64) -> PathBuf
Where spar review records what it reviewed, and a second look reads it.
Sourcepub fn read_review_record(&self, number: i64) -> Option<ReviewRecord>
pub fn read_review_record(&self, number: i64) -> Option<ReviewRecord>
None when there is no record, or when it cannot be read. A record spar cannot read is one it reviews afresh over, and the log says so.
pub fn write_review_record( &self, number: i64, record: &ReviewRecord, ) -> Result<()>
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: the custody loop’s
state, the review record, and the check-in watermark alike. Grouped by
number so each finished pull request costs one gh read however many
files it left.
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.