diff --git a/src/hooks/useGitHubData.ts b/src/hooks/useGitHubData.ts index a0ebe10..f26be06 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,31 @@ 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, + created_at: item.commit.author?.date || item.commit.committer?.date, + classifiedInfo: classifyCommit(item.commit.message), + })); + + return { + items, + total: response.data.total_count, + }; + }; + const fetchData = useCallback( async (username: string, page = 1, perPage = 10) => { @@ -36,15 +64,21 @@ 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).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); } catch (err: any) { if (err.status === 403) { setError('GitHub API rate limit exceeded. Please wait or use a token.'); @@ -62,8 +96,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..fc65e53 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(""); @@ -95,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]; @@ -114,15 +132,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 +162,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 +185,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 +271,18 @@ const Home: React.FC = () => { > + - State + {tab === 2 ? 'Importance' : 'State'} @@ -291,9 +333,9 @@ const Home: React.FC = () => { - Title + Title / Message Repository - State + Status / Importance Created @@ -304,24 +346,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..7950e25 --- /dev/null +++ b/src/utils/commitClassifier.ts @@ -0,0 +1,70 @@ +export type CommitImportance = 'High' | 'Medium' | 'Low' | 'Unknown'; +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 (/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; + + // 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 (category === 'Unknown' && score === 0) { + importance = 'Unknown'; + } else if (score >= 6) { + importance = 'High'; + } else if (score <= 2) { + importance = 'Low'; + } + + return { importance, category, score }; +}