Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 39 additions & 3 deletions src/hooks/useGitHubData.ts
Original file line number Diff line number Diff line change
@@ -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<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) => {
Expand All @@ -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),
}));
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return {
items,
total: response.data.total_count,
};
};

const fetchData = useCallback(
async (username: string, page = 1, perPage = 10) => {

Expand All @@ -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 };
}),
]);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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.');
Expand All @@ -62,8 +96,10 @@ export const useGitHubData = (getOctokit: () => any) => {
return {
issues,
prs,
commits,
totalIssues,
totalPrs,
totalCommits,
loading,
error,
fetchData,
Expand Down
133 changes: 97 additions & 36 deletions src/pages/Tracker/Tracker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
GitPullRequestIcon,
GitPullRequestClosedIcon,
GitMergeIcon,
GitCommitIcon,
} from '@primer/octicons-react';
import {
Container,
Expand Down Expand Up @@ -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 = () => {
Expand All @@ -61,8 +73,10 @@ const Home: React.FC = () => {
const {
issues,
prs,
commits,
totalIssues,
totalPrs,
totalCommits,
loading,
error: dataError,
fetchData,
Expand All @@ -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("");
Expand All @@ -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];
Expand All @@ -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);
});
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if (startDate) {
filtered = filtered.filter(
Expand All @@ -139,6 +162,10 @@ const Home: React.FC = () => {

const getStatusIcon = (item: GitHubItem) => {

if (item.commit) {
return <GitCommitIcon size={16} className="icon-commit" />;
}

if (item.pull_request) {

if (item.pull_request.merged_at)
Expand All @@ -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 (
<Container maxWidth="lg" sx={{ mt: 4, minHeight: "80vh", color: theme.palette.text.primary }}>
Expand Down Expand Up @@ -243,17 +271,18 @@ const Home: React.FC = () => {
>
<Tab label={`Issues (${totalIssues})`} />
<Tab label={`Pull Requests (${totalPrs})`} />
<Tab label={`Commits (${totalCommits})`} />
</Tabs>
<FormControl sx={{ minWidth: 150 }}>
<InputLabel sx={{ fontSize: "14px" }}>State</InputLabel>
<InputLabel sx={{ fontSize: "14px" }}>{tab === 2 ? 'Importance' : 'State'}</InputLabel>
<Select
value={tab === 0 ? issueFilter : prFilter}
onChange={(e) =>
tab === 0
? setIssueFilter(e.target.value)
: setPrFilter(e.target.value)
}
label="State"
value={currentFilter}
onChange={(e) => {
if (tab === 0) setIssueFilter(e.target.value);
else if (tab === 1) setPrFilter(e.target.value);
else setCommitFilter(e.target.value);
}}
label={tab === 2 ? 'Importance' : 'State'}
sx={{
backgroundColor: theme.palette.background.paper,
color: theme.palette.text.primary,
Expand All @@ -264,10 +293,23 @@ const Home: React.FC = () => {
},
}}
>
<MenuItem value="all">All</MenuItem>
<MenuItem value="open">Open</MenuItem>
<MenuItem value="closed">Closed</MenuItem>
{tab === 1 && <MenuItem value="merged">Merged</MenuItem>}
{tab !== 2 && (
<>
<MenuItem value="all">All</MenuItem>
<MenuItem value="open">Open</MenuItem>
<MenuItem value="closed">Closed</MenuItem>
{tab === 1 && <MenuItem value="merged">Merged</MenuItem>}
</>
)}
{tab === 2 && (
<>
<MenuItem value="all">All</MenuItem>
<MenuItem value="High">High</MenuItem>
<MenuItem value="Medium">Medium</MenuItem>
<MenuItem value="Low">Low</MenuItem>
<MenuItem value="Unknown">Unknown</MenuItem>
</>
)}
</Select>
</FormControl>
</Box>
Expand All @@ -291,9 +333,9 @@ const Home: React.FC = () => {

<TableHead>
<TableRow>
<TableCell>Title</TableCell>
<TableCell>Title / Message</TableCell>
<TableCell align="center">Repository</TableCell>
<TableCell align="center">State</TableCell>
<TableCell align="center">Status / Importance</TableCell>
<TableCell>Created</TableCell>
</TableRow>
</TableHead>
Expand All @@ -304,24 +346,43 @@ const Home: React.FC = () => {

<TableCell sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{getStatusIcon(item)}
<Link
href={item.html_url}
target="_blank"
rel="noopener noreferrer"
underline="hover"
sx={{ color: theme.palette.primary.main }}
>
{item.title}
</Link>
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
<Link
href={item.html_url}
target="_blank"
rel="noopener noreferrer"
underline="hover"
sx={{ color: theme.palette.primary.main, wordBreak: 'break-word', maxWidth: '300px' }}
>
{item.commit ? item.commit.message.split('\n')[0] : item.title}
</Link>
{item.classifiedInfo && (
<Box sx={{ display: 'flex', gap: 1, mt: 0.5 }}>
<Box sx={{ fontSize: '0.75rem', px: 1, py: 0.25, borderRadius: '12px', bgcolor: theme.palette.primary.light, color: theme.palette.primary.contrastText }}>
{item.classifiedInfo.category}
</Box>
</Box>
)}
</Box>
</TableCell>


<TableCell align="center">
{item.repository_url.split("/").slice(-1)[0]}
{(item.repository?.html_url || item.repository_url || "").split("/").slice(-1)[0]}
</TableCell>

<TableCell align="center">
{item.pull_request?.merged_at ? "merged" : item.state}
{item.commit ? (
<Box component="span" sx={{
px: 1, py: 0.5, borderRadius: '4px', fontSize: '0.8rem',
bgcolor: item.classifiedInfo?.importance === 'High' ? 'error.light' : (item.classifiedInfo?.importance === 'Medium' ? 'warning.light' : (item.classifiedInfo?.importance === 'Low' ? 'success.light' : 'grey.300')),
color: 'black'
}}>
{item.classifiedInfo?.importance}
</Box>
) : (
item.pull_request?.merged_at ? "merged" : item.state
)}
</TableCell>
Comment thread
coderabbitai[bot] marked this conversation as resolved.

<TableCell>{formatDate(item.created_at)}</TableCell>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Expand Down
70 changes: 70 additions & 0 deletions src/utils/commitClassifier.ts
Original file line number Diff line number Diff line change
@@ -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 };
}