From 0c81fdae9944fc6502e8e17b3884683c6e4aab78 Mon Sep 17 00:00:00 2001 From: Abhijeet Date: Fri, 15 May 2026 23:30:12 +0530 Subject: [PATCH 1/3] feat: implement basic commit classification system and tracking tab --- src/hooks/useGitHubData.ts | 38 ++++++++++- src/pages/Tracker/Tracker.tsx | 125 +++++++++++++++++++++++++--------- src/utils/commitClassifier.ts | 65 ++++++++++++++++++ 3 files changed, 191 insertions(+), 37 deletions(-) create mode 100644 src/utils/commitClassifier.ts diff --git a/src/hooks/useGitHubData.ts b/src/hooks/useGitHubData.ts index a0ebe10..020aba0 100644 --- a/src/hooks/useGitHubData.ts +++ b/src/hooks/useGitHubData.ts @@ -1,12 +1,15 @@ import { useState, useCallback } from 'react'; +import { classifyCommit } from '../utils/commitClassifier'; export const useGitHubData = (getOctokit: () => any) => { - const [issues, setIssues] = useState([]); - const [prs, setPrs] = useState([]); + const [issues, setIssues] = useState([]); + const [prs, setPrs] = useState([]); + const [commits, setCommits] = useState([]); 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) => { @@ -25,6 +28,30 @@ export const useGitHubData = (getOctokit: () => any) => { }; }; + const fetchCommitsPaginated = async (octokit: any, username: string, page = 1, per_page = 10) => { + 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', + }, + }); + + const items = response.data.items.map((item: any) => ({ + ...item, + classifiedInfo: classifyCommit(item.commit.message), + })); + + return { + items, + total: response.data.total_count, + }; + }; + const fetchData = useCallback( async (username: string, page = 1, perPage = 10) => { @@ -36,15 +63,18 @@ export const useGitHubData = (getOctokit: () => any) => { setError(''); try { - const [issueRes, prRes] = await Promise.all([ + const [issueRes, prRes, commitRes] = await Promise.all([ fetchPaginated(octokit, username, 'issue', page, perPage), fetchPaginated(octokit, username, 'pr', page, perPage), + fetchCommitsPaginated(octokit, username, page, perPage), ]); setIssues(issueRes.items); setPrs(prRes.items); + setCommits(commitRes.items); setTotalIssues(issueRes.total); setTotalPrs(prRes.total); + setTotalCommits(commitRes.total); } catch (err: any) { if (err.status === 403) { setError('GitHub API rate limit exceeded. Please wait or use a token.'); @@ -62,8 +92,10 @@ export const useGitHubData = (getOctokit: () => any) => { return { issues, prs, + commits, totalIssues, totalPrs, + totalCommits, loading, error, fetchData, diff --git a/src/pages/Tracker/Tracker.tsx b/src/pages/Tracker/Tracker.tsx index 2bd4d30..f5eb6ba 100644 --- a/src/pages/Tracker/Tracker.tsx +++ b/src/pages/Tracker/Tracker.tsx @@ -5,6 +5,7 @@ import { GitPullRequestIcon, GitPullRequestClosedIcon, GitMergeIcon, + GitCommitIcon, } from '@primer/octicons-react'; import { Container, @@ -43,6 +44,17 @@ interface GitHubItem { pull_request?: { merged_at: string | null }; repository_url: string; html_url: string; + commit?: { + message: string; + }; + repository?: { + html_url: string; + }; + classifiedInfo?: { + importance: string; + category: string; + score: number; + }; } const Home: React.FC = () => { @@ -61,8 +73,10 @@ const Home: React.FC = () => { const { issues, prs, + commits, totalIssues, totalPrs, + totalCommits, loading, error: dataError, fetchData, @@ -73,6 +87,7 @@ const Home: React.FC = () => { const [issueFilter, setIssueFilter] = useState("all"); const [prFilter, setPrFilter] = useState("all"); + const [commitFilter, setCommitFilter] = useState("all"); const [searchTitle, setSearchTitle] = useState(""); const [selectedRepo, setSelectedRepo] = useState(""); const [startDate, setStartDate] = useState(""); @@ -114,15 +129,20 @@ const Home: React.FC = () => { } }); } + if (["High", "Medium", "Low"].includes(filterType)) { + filtered = filtered.filter(item => item.classifiedInfo?.importance === filterType); + } if (searchTitle) { - filtered = filtered.filter((item) => - item.title.toLowerCase().includes(searchTitle.toLowerCase()) - ); + filtered = filtered.filter((item) => { + const title = item.commit ? item.commit.message : item.title; + return title.toLowerCase().includes(searchTitle.toLowerCase()); + }); } if (selectedRepo) { - filtered = filtered.filter((item) => - item.repository_url.includes(selectedRepo) - ); + filtered = filtered.filter((item) => { + const repoUrl = item.repository?.html_url || item.repository_url; + return repoUrl.includes(selectedRepo); + }); } if (startDate) { filtered = filtered.filter( @@ -139,6 +159,10 @@ const Home: React.FC = () => { const getStatusIcon = (item: GitHubItem) => { + if (item.commit) { + return ; + } + if (item.pull_request) { if (item.pull_request.merged_at) @@ -158,9 +182,10 @@ const Home: React.FC = () => { // Current data and filtered data according to tab and filters - const currentRawData = tab === 0 ? issues : prs; - const currentFilteredData = filterData(currentRawData, tab === 0 ? issueFilter : prFilter); - const totalCount = tab === 0 ? totalIssues : totalPrs; + const currentRawData = tab === 0 ? issues : (tab === 1 ? prs : commits); + const currentFilter = tab === 0 ? issueFilter : (tab === 1 ? prFilter : commitFilter); + const currentFilteredData = filterData(currentRawData, currentFilter); + const totalCount = tab === 0 ? totalIssues : (tab === 1 ? totalPrs : totalCommits); return ( @@ -243,17 +268,18 @@ const Home: React.FC = () => { > + - State + {tab === 2 ? 'Importance' : 'State'} @@ -291,9 +329,9 @@ const Home: React.FC = () => { - Title + Title / Message Repository - State + Status / Importance Created @@ -304,24 +342,43 @@ const Home: React.FC = () => { {getStatusIcon(item)} - - {item.title} - + + + {item.commit ? item.commit.message.split('\n')[0] : item.title} + + {item.classifiedInfo && ( + + + {item.classifiedInfo.category} + + + )} + - {item.repository_url.split("/").slice(-1)[0]} + {(item.repository?.html_url || item.repository_url || "").split("/").slice(-1)[0]} - {item.pull_request?.merged_at ? "merged" : item.state} + {item.commit ? ( + + {item.classifiedInfo?.importance} + + ) : ( + item.pull_request?.merged_at ? "merged" : item.state + )} {formatDate(item.created_at)} diff --git a/src/utils/commitClassifier.ts b/src/utils/commitClassifier.ts new file mode 100644 index 0000000..5c35824 --- /dev/null +++ b/src/utils/commitClassifier.ts @@ -0,0 +1,65 @@ +export type CommitImportance = 'High' | 'Medium' | 'Low'; +export type CommitCategory = 'Feature' | 'Bugfix' | 'Refactor' | 'Chore' | 'Docs' | 'Test' | 'Unknown'; + +export interface ClassifiedCommit { + importance: CommitImportance; + category: CommitCategory; + score: number; +} + +export function classifyCommit( + message: string, + filesChanged: number = 0, + additions: number = 0, + deletions: number = 0 +): ClassifiedCommit { + const lowerMsg = message.toLowerCase(); + + let category: CommitCategory = 'Unknown'; + if (/feat\b|feature\b|add\b|implement\b|create\b/.test(lowerMsg)) { + category = 'Feature'; + } else if (/fix\b|bug\b|patch\b|resolve\b/.test(lowerMsg)) { + category = 'Bugfix'; + } else if (/refactor\b|clean\b|rework\b/.test(lowerMsg)) { + category = 'Refactor'; + } else if (/chore\b|bump\b|update\b|depend\b|config\b|format\b|style\b/.test(lowerMsg)) { + category = 'Chore'; + } else if (/doc\b|readme\b|comment\b/.test(lowerMsg)) { + category = 'Docs'; + } else if (/test\b|mock\b|spec\b/.test(lowerMsg)) { + category = 'Test'; + } + + let score = 0; + + // Base score from category + if (category === 'Feature') score += 5; + if (category === 'Bugfix') score += 4; + if (category === 'Refactor') score += 3; + if (category === 'Test') score += 2; + if (category === 'Docs') score += 1; + if (category === 'Chore') score += 1; + + // Impact from size (if available, e.g., if fetched specifically, but search API might omit it, defaulting to 0) + const totalChanges = additions + deletions; + if (totalChanges > 500) score += 3; + else if (totalChanges > 100) score += 2; + else if (totalChanges > 20) score += 1; + + if (filesChanged > 10) score += 2; + else if (filesChanged > 3) score += 1; + + // Keyword modifiers + if (/wip\b|temp\b|typo\b|minor\b|init\b/.test(lowerMsg)) { + score -= 2; + } + if (/major\b|breaking\b|critical\b|core\b/.test(lowerMsg)) { + score += 3; + } + + let importance: CommitImportance = 'Medium'; + if (score >= 6) importance = 'High'; + else if (score <= 2) importance = 'Low'; + + return { importance, category, score }; +} From f5e292af04e6440da922ec9a8d8b50d7f986ad91 Mon Sep 17 00:00:00 2001 From: Abhijeet Date: Sat, 16 May 2026 15:53:54 +0530 Subject: [PATCH 2/3] fix: resolve PR review issues for commit tracking and UI --- src/hooks/useGitHubData.ts | 6 +++++- src/pages/Tracker/Tracker.tsx | 12 ++++++++---- src/utils/commitClassifier.ts | 15 ++++++++++----- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/hooks/useGitHubData.ts b/src/hooks/useGitHubData.ts index 020aba0..f26be06 100644 --- a/src/hooks/useGitHubData.ts +++ b/src/hooks/useGitHubData.ts @@ -43,6 +43,7 @@ export const useGitHubData = (getOctokit: () => any) => { const items = response.data.items.map((item: any) => ({ ...item, + created_at: item.commit.author?.date || item.commit.committer?.date, classifiedInfo: classifyCommit(item.commit.message), })); @@ -66,7 +67,10 @@ export const useGitHubData = (getOctokit: () => any) => { const [issueRes, prRes, commitRes] = await Promise.all([ fetchPaginated(octokit, username, 'issue', page, perPage), fetchPaginated(octokit, username, 'pr', page, perPage), - fetchCommitsPaginated(octokit, username, page, perPage), + fetchCommitsPaginated(octokit, username, page, perPage).catch((err) => { + console.error('Commit fetch failed:', err); + return { items: [], total: 0 }; + }), ]); setIssues(issueRes.items); diff --git a/src/pages/Tracker/Tracker.tsx b/src/pages/Tracker/Tracker.tsx index f5eb6ba..fc65e53 100644 --- a/src/pages/Tracker/Tracker.tsx +++ b/src/pages/Tracker/Tracker.tsx @@ -110,8 +110,11 @@ const Home: React.FC = () => { setPage(newPage); }; - const formatDate = (dateString: string): string => - new Date(dateString).toLocaleDateString(); + const formatDate = (dateString: string): string => { + if (!dateString) return 'N/A'; + const date = new Date(dateString); + return isNaN(date.getTime()) ? 'N/A' : date.toLocaleDateString(); + }; const filterData = (data: GitHubItem[], filterType: string): GitHubItem[] => { let filtered = [...data]; @@ -141,7 +144,7 @@ const Home: React.FC = () => { if (selectedRepo) { filtered = filtered.filter((item) => { const repoUrl = item.repository?.html_url || item.repository_url; - return repoUrl.includes(selectedRepo); + return (repoUrl || '').includes(selectedRepo); }); } if (startDate) { @@ -304,6 +307,7 @@ const Home: React.FC = () => { High Medium Low + Unknown )} @@ -371,7 +375,7 @@ const Home: React.FC = () => { {item.commit ? ( {item.classifiedInfo?.importance} diff --git a/src/utils/commitClassifier.ts b/src/utils/commitClassifier.ts index 5c35824..7950e25 100644 --- a/src/utils/commitClassifier.ts +++ b/src/utils/commitClassifier.ts @@ -1,4 +1,4 @@ -export type CommitImportance = 'High' | 'Medium' | 'Low'; +export type CommitImportance = 'High' | 'Medium' | 'Low' | 'Unknown'; export type CommitCategory = 'Feature' | 'Bugfix' | 'Refactor' | 'Chore' | 'Docs' | 'Test' | 'Unknown'; export interface ClassifiedCommit { @@ -22,12 +22,12 @@ export function classifyCommit( category = 'Bugfix'; } else if (/refactor\b|clean\b|rework\b/.test(lowerMsg)) { category = 'Refactor'; - } else if (/chore\b|bump\b|update\b|depend\b|config\b|format\b|style\b/.test(lowerMsg)) { - category = 'Chore'; } else if (/doc\b|readme\b|comment\b/.test(lowerMsg)) { category = 'Docs'; } else if (/test\b|mock\b|spec\b/.test(lowerMsg)) { category = 'Test'; + } else if (/chore\b|bump\b|update\b|depend\b|config\b|format\b|style\b/.test(lowerMsg)) { + category = 'Chore'; } let score = 0; @@ -58,8 +58,13 @@ export function classifyCommit( } let importance: CommitImportance = 'Medium'; - if (score >= 6) importance = 'High'; - else if (score <= 2) importance = 'Low'; + if (category === 'Unknown' && score === 0) { + importance = 'Unknown'; + } else if (score >= 6) { + importance = 'High'; + } else if (score <= 2) { + importance = 'Low'; + } return { importance, category, score }; } From 3b33d5c92715d3c5f389a18b6d39958db39b6323 Mon Sep 17 00:00:00 2001 From: Abhijeet Date: Wed, 20 May 2026 00:31:07 +0530 Subject: [PATCH 3/3] feat(hooks): debounce and cancel GitHub search requests in useGitHubData --- src/hooks/useGitHubData.ts | 142 ++++++++++++++++++++++++------------- 1 file changed, 94 insertions(+), 48 deletions(-) diff --git a/src/hooks/useGitHubData.ts b/src/hooks/useGitHubData.ts index 3bf2948..5c5722d 100644 --- a/src/hooks/useGitHubData.ts +++ b/src/hooks/useGitHubData.ts @@ -1,4 +1,4 @@ -import { useState, useCallback } from 'react'; +import { useState, useCallback, useRef, useEffect } from 'react'; import { classifyCommit } from '../utils/commitClassifier'; export const useGitHubData = (getOctokit: () => any) => { @@ -12,7 +12,7 @@ export const useGitHubData = (getOctokit: () => any) => { 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, @@ -20,6 +20,7 @@ export const useGitHubData = (getOctokit: () => any) => { order: 'desc', per_page, page, + request: signal ? { signal } : undefined, }); return { @@ -28,7 +29,7 @@ export const useGitHubData = (getOctokit: () => any) => { }; }; - const fetchCommitsPaginated = async (octokit: any, username: string, page = 1, per_page = 10) => { + 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, @@ -39,6 +40,7 @@ export const useGitHubData = (getOctokit: () => any) => { headers: { accept: 'application/vnd.github.cloak-preview+json', }, + request: signal ? { signal } : undefined, }); const items = response.data.items.map((item: any) => ({ @@ -53,60 +55,104 @@ export const useGitHubData = (getOctokit: () => any) => { }; }; + // refs for debounce timer and abort controller + const debounceRef = useRef(null); + const abortControllerRef = useRef(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, commitRes] = await Promise.all([ - fetchPaginated(octokit, username, 'issue', page, perPage), - fetchPaginated(octokit, username, 'pr', page, perPage), - fetchCommitsPaginated(octokit, username, page, perPage).catch((err) => { - 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) { - 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); + 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,