-
Notifications
You must be signed in to change notification settings - Fork 130
feat: implement basic commit classification system and tracking tab #266
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
0c81fda
f5e292a
8546082
3b33d5c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,22 +1,26 @@ | ||
| import { useState, useCallback } from 'react'; | ||
| import { useState, useCallback, useRef, useEffect } from 'react'; | ||
| import { classifyCommit } from '../utils/commitClassifier'; | ||
|
|
||
| export const useGitHubData = (getOctokit: () => any) => { | ||
| const [issues, setIssues] = useState([]); | ||
| const [prs, setPrs] = useState([]); | ||
| const [issues, setIssues] = useState<any[]>([]); | ||
| const [prs, setPrs] = useState<any[]>([]); | ||
| const [commits, setCommits] = useState<any[]>([]); | ||
| const [loading, setLoading] = useState(false); | ||
| const [error, setError] = useState(''); | ||
| const [totalIssues, setTotalIssues] = useState(0); | ||
| const [totalPrs, setTotalPrs] = useState(0); | ||
| const [totalCommits, setTotalCommits] = useState(0); | ||
| const [rateLimited, setRateLimited] = useState(false); | ||
|
|
||
| const fetchPaginated = async (octokit: any, username: string, type: string, page = 1, per_page = 10) => { | ||
| const fetchPaginated = async (octokit: any, username: string, type: string, page = 1, per_page = 10, signal?: AbortSignal) => { | ||
| const q = `author:${username} is:${type}`; | ||
| const response = await octokit.request('GET /search/issues', { | ||
| q, | ||
| sort: 'created', | ||
| order: 'desc', | ||
| per_page, | ||
| page, | ||
| request: signal ? { signal } : undefined, | ||
| }); | ||
|
|
||
| return { | ||
|
|
@@ -25,59 +29,137 @@ export const useGitHubData = (getOctokit: () => any) => { | |
| }; | ||
| }; | ||
|
|
||
| const fetchCommitsPaginated = async (octokit: any, username: string, page = 1, per_page = 10, signal?: AbortSignal) => { | ||
| const q = `author:${username}`; | ||
| const response = await octokit.request('GET /search/commits', { | ||
| q, | ||
| sort: 'author-date', | ||
| order: 'desc', | ||
| per_page, | ||
| page, | ||
| headers: { | ||
| accept: 'application/vnd.github.cloak-preview+json', | ||
| }, | ||
| request: signal ? { signal } : undefined, | ||
| }); | ||
|
|
||
| const items = response.data.items.map((item: any) => ({ | ||
| ...item, | ||
| created_at: item.commit.author?.date || item.commit.committer?.date, | ||
| classifiedInfo: classifyCommit(item.commit.message), | ||
| })); | ||
|
|
||
| return { | ||
| items, | ||
| total: response.data.total_count, | ||
| }; | ||
| }; | ||
|
|
||
| // refs for debounce timer and abort controller | ||
| const debounceRef = useRef<number | null>(null); | ||
| const abortControllerRef = useRef<AbortController | null>(null); | ||
|
|
||
| const fetchData = useCallback( | ||
| async (username: string, page = 1, perPage = 10) => { | ||
|
|
||
| (username: string, page = 1, perPage = 10) => { | ||
| const DEBOUNCE_MS = 350; | ||
|
|
||
| const octokit = getOctokit(); | ||
|
|
||
| if (!octokit || !username) return; | ||
|
|
||
| setLoading(true); | ||
| setError(''); | ||
|
|
||
| try { | ||
| const [issueRes, prRes] = await Promise.all([ | ||
| fetchPaginated(octokit, username, 'issue', page, perPage), | ||
| fetchPaginated(octokit, username, 'pr', page, perPage), | ||
| ]); | ||
|
|
||
| setIssues(issueRes.items); | ||
| setPrs(prRes.items); | ||
| setTotalIssues(issueRes.total); | ||
| setTotalPrs(prRes.total); | ||
| setRateLimited(false); | ||
| } catch (err: any) { | ||
| const errorMessage = err.message?.toLowerCase() || ""; | ||
| if (err.status === 403) { | ||
| setError('GitHub API rate limit exceeded. Please provide a PAT to continue.'); | ||
| setRateLimited(true); | ||
| } else if (errorMessage.includes("do not exist")){ | ||
| setError('User not found. Please check the spelling of the GitHub username.'); | ||
| } else if (err.status === 401 || errorMessage.includes("permission")){ | ||
| setError('Private repository detected. Please input PAT.'); | ||
| }else if(err.status===404){ | ||
| setError('Resource not found.'); | ||
| } | ||
| else if (errorMessage.includes("validation failed")) { | ||
| setError('Invalid GitHub username or insufficient permissions.'); | ||
| if (!octokit) return; | ||
|
|
||
| // debounce: clear existing timer | ||
| if (debounceRef.current) { | ||
| clearTimeout(debounceRef.current); | ||
| } | ||
|
|
||
| debounceRef.current = window.setTimeout(async () => { | ||
| // cancel previous in-flight requests | ||
| if (abortControllerRef.current) { | ||
| try { | ||
| abortControllerRef.current.abort(); | ||
| } catch (e) { | ||
| // ignore | ||
| } | ||
| } | ||
| else { | ||
| setError( | ||
| 'Unable to fetch GitHub data. Please verify the username, token, or network connection.' | ||
| ); | ||
|
|
||
| const controller = new AbortController(); | ||
| abortControllerRef.current = controller; | ||
|
|
||
| setLoading(true); | ||
|
Comment on lines
+70
to
+88
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Abort the previous request before starting the next debounce window. The old request keeps running until the new timer fires, so it can still resolve during the 350ms gap and overwrite newer Suggested fix- // debounce: clear existing timer
+ // cancel any in-flight request immediately so stale responses can't win
+ if (abortControllerRef.current) {
+ abortControllerRef.current.abort();
+ abortControllerRef.current = null;
+ }
+
+ // debounce: clear existing timer
if (debounceRef.current) {
clearTimeout(debounceRef.current);
}
debounceRef.current = window.setTimeout(async () => {
- // cancel previous in-flight requests
- if (abortControllerRef.current) {
- try {
- abortControllerRef.current.abort();
- } catch (e) {
- // ignore
- }
- }
-
const controller = new AbortController();
abortControllerRef.current = controller;
setLoading(true);
setError('');
@@
} finally {
- setLoading(false);
- if (abortControllerRef.current === controller) abortControllerRef.current = null;
+ if (abortControllerRef.current === controller) {
+ abortControllerRef.current = null;
+ setLoading(false);
+ }
}
}, DEBOUNCE_MS);Also applies to: 130-132 🤖 Prompt for AI Agents |
||
| setError(''); | ||
|
|
||
| try { | ||
| const [issueRes, prRes, commitRes] = await Promise.all([ | ||
| fetchPaginated(octokit, username, 'issue', page, perPage, controller.signal), | ||
| fetchPaginated(octokit, username, 'pr', page, perPage, controller.signal), | ||
| fetchCommitsPaginated(octokit, username, page, perPage, controller.signal).catch((err) => { | ||
| if (err.name === 'AbortError') return { items: [], total: 0 }; | ||
| console.error('Commit fetch failed:', err); | ||
| return { items: [], total: 0 }; | ||
| }), | ||
| ]); | ||
|
|
||
| setIssues(issueRes.items); | ||
| setPrs(prRes.items); | ||
| setCommits(commitRes.items); | ||
| setTotalIssues(issueRes.total); | ||
| setTotalPrs(prRes.total); | ||
| setTotalCommits(commitRes.total); | ||
| setRateLimited(false); | ||
| } catch (err: any) { | ||
| if (err.name === 'AbortError') { | ||
| return; | ||
| } | ||
| const errorMessage = err.message?.toLowerCase() || ""; | ||
| if (err.status === 403) { | ||
| setError('GitHub API rate limit exceeded. Please provide a PAT to continue.'); | ||
| setRateLimited(true); | ||
| } else if (errorMessage.includes("do not exist")){ | ||
| setError('User not found. Please check the spelling of the GitHub username.'); | ||
| } else if (err.status === 401 || errorMessage.includes("permission")){ | ||
| setError('Private repository detected. Please input PAT.'); | ||
| } else if(err.status===404){ | ||
| setError('Resource not found.'); | ||
| } else if (errorMessage.includes("validation failed")) { | ||
| setError('Invalid GitHub username or insufficient permissions.'); | ||
| } else { | ||
| setError( | ||
| 'Unable to fetch GitHub data. Please verify the username, token, or network connection.' | ||
| ); | ||
| } | ||
| } finally { | ||
| setLoading(false); | ||
| if (abortControllerRef.current === controller) abortControllerRef.current = null; | ||
| } | ||
| } finally { | ||
| setLoading(false); | ||
| } | ||
| }, DEBOUNCE_MS); | ||
| }, | ||
| [getOctokit] | ||
| ); | ||
|
|
||
| // cleanup on unmount: clear debounce timer and abort any in-flight requests | ||
| useEffect(() => { | ||
| return () => { | ||
| if (debounceRef.current) { | ||
| clearTimeout(debounceRef.current); | ||
| } | ||
| if (abortControllerRef.current) { | ||
| try { | ||
| abortControllerRef.current.abort(); | ||
| } catch (e) { | ||
| // ignore | ||
| } | ||
| abortControllerRef.current = null; | ||
| } | ||
| }; | ||
| }, []); | ||
|
|
||
| return { | ||
| issues, | ||
| prs, | ||
| commits, | ||
| totalIssues, | ||
| totalPrs, | ||
| totalCommits, | ||
| loading, | ||
| error, | ||
| fetchData, | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.