1
0
Fork 0
mirror of https://github.com/imjasonh/cnotes synced 2026-07-08 00:55:37 +00:00
cnotes/chrome-extension/background.js
Jason Hall 32be89b76d
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)
2025-07-21 22:22:32 -04:00

279 lines
No EOL
7.6 KiB
JavaScript

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