GitHub
The GitHub module creates issues from TODO comments, detects duplicates, and handles rate limiting.
GitHubClient
#![allow(unused)] fn main() { pub struct GitHubClient { // private fields } }
Authenticated GitHub API client for creating issues from TODO comments. Maintains a cache of existing issue titles and TODO IDs for deduplication. Includes rate-limit handling with configurable delays and automatic retries.
new
#![allow(unused)] fn main() { pub fn new(config: &GitHubConfig) -> Result<Self, TowlGitHubError> }
Creates a new client from a GitHubConfig. Exposes the SecretString token once to build the Octocrab API client.
Errors:
MissingToken-- Token is emptyApiError-- Octocrab client failed to build
load_existing_issues
#![allow(unused)] fn main() { pub async fn load_existing_issues(&mut self) -> Result<(), TowlGitHubError> }
Paginates through all existing issues (open and closed) in the repository, caching their titles and embedded TODO IDs. Call this before create_issue to enable duplicate detection.
Errors:
ApiError-- GitHub API call failedAuthError-- Invalid or expired tokenRepositoryNotFound-- Owner/repo combination does not exist
issue_exists
#![allow(unused)] fn main() { pub fn issue_exists(&self, todo: &TodoComment) -> bool }
Returns true if a matching issue already exists, checked by TODO ID (embedded in issue body) or by generated title.
create_issue
#![allow(unused)] fn main() { pub async fn create_issue( &mut self, todo: &TodoComment, ) -> Result<CreatedIssue, TowlGitHubError> }
Creates a GitHub issue for a TODO comment. Generates a title with type prefix, truncated description, and file location. The body includes file path, line number, column range, description, function context, original comment, and surrounding code.
Automatically retries on rate limiting (up to 3 attempts).
Errors:
IssueAlreadyExists-- Duplicate detectedRateLimitExceeded-- Rate limit hit after max retriesApiError-- GitHub API failureAuthError-- Authentication failure
Issue Title Format
[TODO] Implement caching (cache.rs:42)
Titles are capped at 50 characters (excluding the type prefix and location suffix). Long descriptions are truncated at word boundaries with ....
Issue Body Sections
- TODO Details -- Type, file, line, column range
- Description -- Extracted description text (Markdown-escaped)
- Function Context -- Enclosing function name (if detected)
- Original Comment -- Full comment line in a code block
- Context -- Surrounding source lines in a code block
- TODO ID -- Embedded identifier for deduplication
Duplicate Detection
Issues are deduplicated by two methods:
- TODO ID -- Each issue body contains
*TODO ID: {file_path}_L{line_number}*. If any existing issue body contains the same ID, the TODO is skipped. - Title match -- If the generated title matches an existing issue title exactly, the TODO is skipped.
CreatedIssue
#![allow(unused)] fn main() { pub struct CreatedIssue { pub number: u64, pub title: String, pub html_url: String, pub todo_id: String, } }
Metadata for a successfully created GitHub issue. Implements Serialize and Deserialize for JSON roundtripping.
Errors
#![allow(unused)] fn main() { pub enum TowlGitHubError { ApiError { message: String, source: Option<octocrab::Error> }, AuthError, RateLimitExceeded { retry_after_secs: u64 }, IssueAlreadyExists { title: String }, RepositoryNotFound { owner: String, repo: String }, MissingToken, } }
| Variant | Cause |
|---|---|
ApiError | General GitHub API failure |
AuthError | 401 response -- invalid or expired token |
RateLimitExceeded | 403 with "rate limit" in message |
IssueAlreadyExists | Duplicate detected before creation |
RepositoryNotFound | 404 response -- owner/repo not found |
MissingToken | TOWL_GITHUB_TOKEN not set or empty |
Example
#![allow(unused)] fn main() { use towl::config::TowlConfig; use towl::github::GitHubClient; let config = TowlConfig::load(None)?; let mut client = GitHubClient::new(&config.github)?; // Load existing issues for duplicate detection client.load_existing_issues().await?; // Create an issue (skips if duplicate) if !client.issue_exists(&todo) { let issue = client.create_issue(&todo).await?; println!("Created #{}: {}", issue.number, issue.html_url); } }