Skip to main content

Repo

Struct Repo 

Source
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: Drafts

Implementations§

Source§

impl Repo

Source

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.

Source

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.

Source

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.

Source

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.

Source

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

Source

pub fn open(root: impl AsRef<Path>, cfg: &Config) -> Result<Self>

Source

pub fn root(&self) -> &Path

Source

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.

Source

pub fn clean_body(&self, text: &str) -> Result<String>

Clean, and hold to a length budget. For anything a model wrote.

Source

pub fn clean_issue_body(&self, text: &str) -> Result<String>

The same, with an issue’s far larger budget and its exemption for code.

Source

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.

Source

pub fn git(&self, args: &[&str]) -> Result<String>

Source

pub fn git_at(&self, cwd: Option<&Path>, args: &[&str]) -> Result<String>

Source

pub fn git_try(&self, args: &[&str]) -> String

Run git, tolerating failure. Returns whatever landed on stdout.

Source

pub fn git_try_at(&self, cwd: Option<&Path>, args: &[&str]) -> String

Source

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.

Source

pub fn branch_for_issue(&self, issue: i64) -> String

Source

pub fn branch_for_pr(&self, number: i64) -> String

Source

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.

Source

pub fn known_branches(&self) -> BTreeMap<String, BranchRecord>

Source

pub fn record_branch(&self, branch: &str, kind: &str, number: i64)

Source

pub fn forget_branch(&self, branch: &str)

Source

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.

Source

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.

Source

pub fn worktree_remove(&self, issue: i64)

Source

pub fn worktree_for_pr(&self, pr: &PrView) -> Result<(PathBuf, String)>

Check an existing PR branch out into an isolated worktree.

Source

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.

Source

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.

Source

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.

Source

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.

Source

pub fn release_review_worktree(&self, number: i64)

Source

pub fn release_pr_worktree(&self, number: i64)

Source

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.

Source

pub fn has_changes(&self, cwd: &Path, base: &str) -> bool

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

pub fn diff_stat(&self, cwd: &Path, base: &str) -> String

Source

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.

Source

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.

Source

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.

Source

pub fn gh(&self, args: &[&str]) -> Result<String>

Source

pub fn gh_at(&self, cwd: Option<&Path>, args: &[&str]) -> Result<String>

Source

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.

Source

pub fn gh_try(&self, args: &[&str]) -> String

Source

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.

Source

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.

Source

pub fn fetch_issues(&self, numbers: &[i64]) -> Result<Vec<Issue>>

Source

pub fn list_open_issues( &self, limit: usize, min_number: i64, ) -> Result<Vec<i64>>

Open issues, lowest numbered first. gh issue list excludes PRs.

Source

pub fn list_open_prs(&self, limit: usize, min_number: i64) -> Result<Vec<i64>>

Source

pub fn pr_for_branch(&self, branch: &str) -> Option<PrRef>

Source

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.

Source

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.

Source

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.

Source

pub fn pr_view(&self, number: i64) -> Result<PrView>

Source

pub fn pr_state(&self, number: i64) -> String

Source

pub fn pr_head_oid(&self, number: i64) -> Result<String>

The commit currently exposed as a pull request’s head.

Source

pub fn create_pr( &self, cwd: &Path, branch: &str, base: &str, title: &str, body: &str, ) -> Result<PrRef>

Source

pub fn comment_pr(&self, number: i64, body: &str) -> Result<()>

Source

pub fn comment_issue(&self, number: i64, body: &str) -> Result<()>

Source

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.

Source

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.

Source

pub fn issue_body(&self, number: i64) -> Result<String>

One issue’s body, exactly as GitHub holds it.

Source

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.

Source

pub fn open_pr_rows(&self) -> Vec<PrRow>

Every open pull request with its size, in one call.

Source

pub fn create_issue(&self, title: &str, body: &str) -> Result<String>

Source

pub fn create_issue_apart_from( &self, title: &str, body: &str, apart_from: Option<i64>, ) -> Result<String>

Source§

impl Repo

Source

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.

Source

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.

Source

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.

Source

pub fn find_issue_by_title(&self, title: &str) -> Option<String>

Avoid filing a duplicate when a follow-up already exists.

Source

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.

Source

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.

Source

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.

Source

pub fn followups_path(&self) -> PathBuf

The queue of follow-ups recorded locally rather than filed, which spar followup works.

Source

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.

Source

pub fn checkin_state_path(&self, number: i64) -> PathBuf

What spar checkin has already answered on one pull request or issue.

Source

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.

Source

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.

Source

pub fn pending_comment_path(&self, number: i64) -> PathBuf

Where a comment spar produced but did not post is kept.

Source

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.

Source

pub fn read_pending_comment(&self, number: i64) -> Option<String>

Source

pub fn state_path(&self, number: i64) -> PathBuf

Source

pub fn read_state(&self, pr: &PrView) -> Option<PersistedState>

Source

pub fn write_state(&self, number: i64, state: &PersistedState) -> Result<()>

Source

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.

Source

pub fn try_issue_comments(&self, number: i64) -> Result<Vec<Value>>

Source

pub fn clear_state(&self, number: i64)

Drop state once the PR is finished and there is nothing to resume.

Source

pub fn prune_state(&self) -> Vec<String>

Remove state files whose PR is merged or closed.

Source

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.

Source

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.

Source

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.

Trait Implementations§

Source§

impl Debug for Repo

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

§

impl !Freeze for Repo

§

impl RefUnwindSafe for Repo

§

impl Send for Repo

§

impl Sync for Repo

§

impl Unpin for Repo

§

impl UnsafeUnpin for Repo

§

impl UnwindSafe for Repo

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.