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
139 changes: 139 additions & 0 deletions src/hooks/useGitHubData.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
import { useState, useCallback, useRef, useEffect } from 'react';
import { classifyCommit } from '../utils/commitClassifier';

export const useGitHubData = (getOctokit: () => any) => {
const [issues, setIssues] = useState<any[]>([]);
const [prs, setPrs] = useState<any[]>([]);
const [commits, setCommits] = useState<any[]>([]);
import { useState, useCallback, useRef } from 'react';
Comment on lines +4 to 8
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/hooks/useGitHubData.ts"

echo "Checking duplicate declarations in ${FILE}..."
rg -n "export const useGitHubData|const fetchPaginated = async|^\\s*async\\s*\\(" "$FILE"

echo
echo "Expected: each declaration should appear once in final code."
echo "If multiple hits appear for the same declaration, remove the legacy block."

Repository: GitMetricsLab/github_tracker

Length of output: 553


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/hooks/useGitHubData.ts"

echo "---- file head (1-90) ----"
nl -ba "$FILE" | sed -n '1,90p'

echo
echo "---- middle (90-160) ----"
nl -ba "$FILE" | sed -n '90,160p'

echo
echo "---- tail (160-260) ----"
nl -ba "$FILE" | sed -n '160,260p'

Repository: GitMetricsLab/github_tracker

Length of output: 160


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/hooks/useGitHubData.ts"

echo "---- file head (1-90) ----"
cat -n "$FILE" | sed -n '1,90p'

echo
echo "---- middle (90-160) ----"
cat -n "$FILE" | sed -n '90,160p'

echo
echo "---- tail (160-260) ----"
cat -n "$FILE" | sed -n '160,260p'

Repository: GitMetricsLab/github_tracker

Length of output: 9642


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/hooks/useGitHubData.ts"

echo "---- tail (260-420) ----"
cat -n "$FILE" | sed -n '260,420p'

echo
echo "---- very end (last 120 lines) ----"
# print last 120 lines without assuming total line count
TOTAL=$(wc -l < "$FILE")
START=$(( TOTAL>120 ? TOTAL-119 : 1 ))
cat -n "$FILE" | sed -n "${START},${TOTAL}p"

Repository: GitMetricsLab/github_tracker

Length of output: 8072


Fix broken module structure in src/hooks/useGitHubData.ts (duplicate hook + stray code blocks break compilation)

  • useGitHubData is exported twice (starts at line 4 and again at line 31) without properly closing the first export.
  • An import { useState, useCallback, useRef } from 'react'; appears inside the hook body (line 8), which is invalid syntax in TS/JS modules.
  • fetchPaginated is declared twice and the earlier declaration is left in an incomplete state before the later one (lines ~43-56).
  • fetchData contains a stray legacy async (...) => { ... } block starting around line 210, corrupting the useCallback callback structure.

Remove the leftover legacy implementation so the file contains exactly one export const useGitHubData and one coherent fetchPaginated/fetchData implementation.

🧰 Tools
🪛 Biome (2.4.15)

[error] 8-8: Illegal use of an import declaration not at the top level

(parse)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useGitHubData.ts` around lines 4 - 8, The file has duplicated and
misplaced code: move the import "useState, useCallback, useRef" to the top of
the module, remove the stray/duplicate export block so there is exactly one
export const useGitHubData, delete the earlier incomplete fetchPaginated
declaration and keep only the coherent fetchPaginated implementation, and remove
the legacy stray async (...) => { ... } block inside fetchData so that fetchData
is a single valid useCallback function; reference the exports/useGitHubData,
fetchPaginated, and fetchData symbols while making these edits.

import { Octokit } from '@octokit/core';

Expand Down Expand Up @@ -30,8 +37,19 @@ export const useGitHubData = (
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, 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,
});
// Prevent stale responses overwriting latest data
const lastRequestId = useRef(0);

Expand Down Expand Up @@ -86,7 +104,109 @@ export const useGitHubData = (
};
};

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(
(username: string, page = 1, perPage = 10) => {
const DEBOUNCE_MS = 350;

const octokit = getOctokit();

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
}
}

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;
}
Comment on lines +205 to +208
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard setLoading(false) so stale aborted requests don’t clear active loading state.

Only the currently active controller should finalize loading state.

Suggested fix
         } finally {
-          setLoading(false);
-          if (abortControllerRef.current === controller) abortControllerRef.current = null;
+          if (abortControllerRef.current === controller) {
+            setLoading(false);
+            abortControllerRef.current = null;
+          }
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} finally {
setLoading(false);
if (abortControllerRef.current === controller) abortControllerRef.current = null;
}
} finally {
if (abortControllerRef.current === controller) {
setLoading(false);
abortControllerRef.current = null;
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useGitHubData.ts` around lines 205 - 208, In the finally block of
the fetch flow in useGitHubData.ts, guard the setLoading(false) so a stale
aborted request cannot clear a newer active loading state: only call
setLoading(false) (and null-out abortControllerRef.current) if
abortControllerRef.current === controller; otherwise leave loading unchanged.
Update the finally block that currently references abortControllerRef,
controller and setLoading to perform this equality check before mutating state.

}, DEBOUNCE_MS);
async (
username: string,
page = 1,
Expand Down Expand Up @@ -239,11 +359,30 @@ export const useGitHubData = (
[getOctokit, rateLimited]
);

// 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,
rateLimited,
Expand Down
Loading