mirror of
https://github.com/imjasonh/cnotes
synced 2026-07-08 00:55:37 +00:00
Add Chrome extension for viewing git notes on GitHub
- Popup-based extension that works on GitHub commit pages - Shows badge with note count when commits have notes - Displays all available git notes refs in tabbed interface - Supports cnotes format with special formatting - Works with any public GitHub repository (no auth required) - Includes caching to minimize API calls (60 req/hour limit)
This commit is contained in:
parent
ce24ed507f
commit
32be89b76d
12 changed files with 1016 additions and 0 deletions
279
chrome-extension/background.js
Normal file
279
chrome-extension/background.js
Normal file
|
|
@ -0,0 +1,279 @@
|
|||
// Background service worker for GitHub Git Notes Viewer
|
||||
// Handles API calls to bypass CORS restrictions and monitors tabs for notes
|
||||
|
||||
// Cache for API responses (5 minutes TTL)
|
||||
const cache = new Map();
|
||||
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
// Track tab states
|
||||
const tabStates = new Map();
|
||||
|
||||
// Listen for tab updates
|
||||
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
|
||||
if (changeInfo.status === 'complete' && tab.url) {
|
||||
checkTabForNotes(tabId, tab.url);
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for tab activation
|
||||
chrome.tabs.onActivated.addListener(async (activeInfo) => {
|
||||
const tab = await chrome.tabs.get(activeInfo.tabId);
|
||||
if (tab.url) {
|
||||
checkTabForNotes(activeInfo.tabId, tab.url);
|
||||
}
|
||||
});
|
||||
|
||||
// Check if a tab is on a GitHub commit page and has notes
|
||||
async function checkTabForNotes(tabId, url) {
|
||||
const commitMatch = url.match(/^https:\/\/github\.com\/([^\/]+)\/([^\/]+)\/commit\/([0-9a-f]+)/i);
|
||||
|
||||
if (!commitMatch) {
|
||||
// Not a commit page, clear badge
|
||||
chrome.action.setBadgeText({ tabId, text: '' });
|
||||
tabStates.delete(tabId);
|
||||
return;
|
||||
}
|
||||
|
||||
const [, owner, repo, commitSha] = commitMatch;
|
||||
const cacheKey = `${owner}/${repo}/${commitSha}`;
|
||||
|
||||
// Check if we already know about this tab
|
||||
const tabState = tabStates.get(tabId);
|
||||
if (tabState && tabState.cacheKey === cacheKey && tabState.checked) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Check cache first
|
||||
const cached = cache.get(cacheKey);
|
||||
let noteCount = 0;
|
||||
|
||||
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
|
||||
noteCount = Object.keys(cached.data).length;
|
||||
} else {
|
||||
// Fetch notes to check if any exist
|
||||
const notesRefs = await fetchNotesRefs(owner, repo);
|
||||
|
||||
for (const ref of notesRefs) {
|
||||
const refName = ref.replace('refs/notes/', '');
|
||||
const hasNotes = await checkCommitHasNotes(owner, repo, commitSha, refName);
|
||||
if (hasNotes) {
|
||||
noteCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update badge
|
||||
if (noteCount > 0) {
|
||||
chrome.action.setBadgeText({ tabId, text: noteCount.toString() });
|
||||
chrome.action.setBadgeBackgroundColor({ tabId, color: '#0366d6' });
|
||||
} else {
|
||||
chrome.action.setBadgeText({ tabId, text: '' });
|
||||
}
|
||||
|
||||
// Save state
|
||||
tabStates.set(tabId, {
|
||||
cacheKey,
|
||||
noteCount,
|
||||
checked: true
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error checking for notes:', error);
|
||||
// Don't show badge on error
|
||||
chrome.action.setBadgeText({ tabId, text: '' });
|
||||
}
|
||||
}
|
||||
|
||||
// Quick check if a commit has notes without fetching content
|
||||
async function checkCommitHasNotes(owner, repo, commitSha, notesRef) {
|
||||
try {
|
||||
const refResponse = await fetch(
|
||||
`https://api.github.com/repos/${owner}/${repo}/git/refs/notes/${notesRef}`
|
||||
);
|
||||
|
||||
if (!refResponse.ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const refData = await refResponse.json();
|
||||
const notesSha = refData.object.sha;
|
||||
|
||||
// Get the tree
|
||||
const commitResponse = await fetch(
|
||||
`https://api.github.com/repos/${owner}/${repo}/git/commits/${notesSha}`
|
||||
);
|
||||
|
||||
if (!commitResponse.ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const commitData = await commitResponse.json();
|
||||
const treeSha = commitData.tree.sha;
|
||||
|
||||
const treeResponse = await fetch(
|
||||
`https://api.github.com/repos/${owner}/${repo}/git/trees/${treeSha}`
|
||||
);
|
||||
|
||||
if (!treeResponse.ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const treeData = await treeResponse.json();
|
||||
|
||||
// Check if this commit exists in the tree
|
||||
return treeData.tree.some(item =>
|
||||
item.path.startsWith(commitSha) || commitSha.startsWith(item.path.substring(0, 7))
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error checking notes for ref ${notesRef}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for messages from content script
|
||||
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||
if (request.action === 'fetchGitNotes') {
|
||||
handleFetchGitNotes(request, sendResponse);
|
||||
return true; // Will respond asynchronously
|
||||
}
|
||||
});
|
||||
|
||||
async function handleFetchGitNotes(request, sendResponse) {
|
||||
const { owner, repo, commitSha } = request;
|
||||
const cacheKey = `${owner}/${repo}/${commitSha}`;
|
||||
|
||||
// Check cache first
|
||||
const cached = cache.get(cacheKey);
|
||||
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
|
||||
sendResponse({ success: true, data: cached.data });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Fetch all available notes refs
|
||||
const notesRefs = await fetchNotesRefs(owner, repo);
|
||||
const allNotes = {};
|
||||
|
||||
for (const ref of notesRefs) {
|
||||
const refName = ref.replace('refs/notes/', '');
|
||||
const notes = await fetchNotesForCommit(owner, repo, commitSha, refName);
|
||||
if (notes) {
|
||||
allNotes[refName] = notes;
|
||||
}
|
||||
}
|
||||
|
||||
// Cache the result
|
||||
cache.set(cacheKey, {
|
||||
data: allNotes,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
sendResponse({ success: true, data: allNotes });
|
||||
} catch (error) {
|
||||
console.error('Error fetching git notes:', error);
|
||||
sendResponse({
|
||||
success: false,
|
||||
error: error.message,
|
||||
rateLimitRemaining: error.rateLimitRemaining
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchNotesRefs(owner, repo) {
|
||||
const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/refs`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw createError(response);
|
||||
}
|
||||
|
||||
const refs = await response.json();
|
||||
return refs
|
||||
.filter(ref => ref.ref.startsWith('refs/notes/'))
|
||||
.map(ref => ref.ref);
|
||||
}
|
||||
|
||||
async function fetchNotesForCommit(owner, repo, commitSha, notesRef) {
|
||||
try {
|
||||
// Step 1: Get the notes reference
|
||||
const refResponse = await fetch(
|
||||
`https://api.github.com/repos/${owner}/${repo}/git/refs/notes/${notesRef}`
|
||||
);
|
||||
|
||||
if (!refResponse.ok) {
|
||||
if (refResponse.status === 404) {
|
||||
return null; // No notes in this ref
|
||||
}
|
||||
throw createError(refResponse);
|
||||
}
|
||||
|
||||
const refData = await refResponse.json();
|
||||
const notesSha = refData.object.sha;
|
||||
|
||||
// Step 2: Get the commit object
|
||||
const commitResponse = await fetch(
|
||||
`https://api.github.com/repos/${owner}/${repo}/git/commits/${notesSha}`
|
||||
);
|
||||
|
||||
if (!commitResponse.ok) {
|
||||
throw createError(commitResponse);
|
||||
}
|
||||
|
||||
const commitData = await commitResponse.json();
|
||||
const treeSha = commitData.tree.sha;
|
||||
|
||||
// Step 3: Get the tree
|
||||
const treeResponse = await fetch(
|
||||
`https://api.github.com/repos/${owner}/${repo}/git/trees/${treeSha}`
|
||||
);
|
||||
|
||||
if (!treeResponse.ok) {
|
||||
throw createError(treeResponse);
|
||||
}
|
||||
|
||||
const treeData = await treeResponse.json();
|
||||
|
||||
// Find the blob for this commit (handle short SHAs)
|
||||
const blob = treeData.tree.find(item =>
|
||||
item.path.startsWith(commitSha) || commitSha.startsWith(item.path.substring(0, 7))
|
||||
);
|
||||
|
||||
if (!blob) {
|
||||
return null; // No notes for this commit
|
||||
}
|
||||
|
||||
// Step 4: Get the blob content
|
||||
const blobResponse = await fetch(
|
||||
`https://api.github.com/repos/${owner}/${repo}/git/blobs/${blob.sha}`
|
||||
);
|
||||
|
||||
if (!blobResponse.ok) {
|
||||
throw createError(blobResponse);
|
||||
}
|
||||
|
||||
const blobData = await blobResponse.json();
|
||||
|
||||
// Decode base64 content
|
||||
const content = atob(blobData.content);
|
||||
|
||||
// Try to parse as JSON
|
||||
try {
|
||||
return JSON.parse(content);
|
||||
} catch {
|
||||
// Return as plain text if not JSON
|
||||
return { content };
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error fetching notes for ref ${notesRef}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function createError(response) {
|
||||
const error = new Error(`GitHub API error: ${response.status} ${response.statusText}`);
|
||||
error.status = response.status;
|
||||
error.rateLimitRemaining = response.headers.get('X-RateLimit-Remaining');
|
||||
return error;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue