From 782decd121ee96d2ca84f2d9ba4fde6c84486e05 Mon Sep 17 00:00:00 2001 From: Charancs2 Date: Tue, 31 Mar 2026 18:14:37 +0530 Subject: [PATCH 1/4] chore: update biome schema and add multi-platform roadmap - Update biome schema version from 2.3.13 to 2.4.10 to match CLI - Add GitLab and Gitea support to roadmap in README - Highlight new multi-platform features in Features section - Document upcoming SCM analytics and Gitea integration Co-Authored-By: Claude Haiku 4.5 --- README.md | 12 +++++++++++- biome.json | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5f45a00f..4ce41d8a 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,19 @@ ## Features - Automatically fetches your Git activity, including commits, pull requests, issues, and code reviews. -- Currently supports GitHub, with plans to expand to other platforms +- **Multi-Platform Support**: GitHub, GitLab (NEW), with Gitea coming soon - Generates editable scrum updates based on your selected date range - Integrates directly with compose windows in Google Groups, Gmail, Yahoo Mail, and Outlook +- Platform selector for seamless switching between GitHub and GitLab + +## Roadmap + +### Upcoming Features 🚀 +- ✅ **GitLab Support** - Full GitLab project and merge request integration +- 🔜 **Gitea Support** - Add Gitea instance compatibility +- 📊 **Multi-SCM Analytics** - Unified dashboard for cross-platform activity +- 🔗 **Unified API Layer** - Abstract provider implementation for easier extensions +- 📈 **Advanced Analytics** - Commit trends, contribution graphs, team metrics ## How to install diff --git a/biome.json b/biome.json index 0e557ce7..1664e03d 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.3.13/schema.json", + "$schema": "https://biomejs.dev/schemas/2.4.10/schema.json", "vcs": { "enabled": true, "clientKind": "git", From ddf95388e80c68bcb76c8ad58c7394f9eea1a934 Mon Sep 17 00:00:00 2001 From: Charancs2 Date: Tue, 31 Mar 2026 18:29:53 +0530 Subject: [PATCH 2/4] feat: complete GitLab support with commits fetching - Add commits fetching from GitLab projects with date filtering - Map commits to standard format for report generation - Include commits data in GitLab data processing - Add project metadata to commits for better tracking - Support author filtering using author_name parameter This enables complete feature parity with GitHub support: Projects fetching Merge requests fetchingIssues fetching Commits fetching --- src/scripts/gitlabHelper.js | 27 +++++++++++++++++++++++++++ src/scripts/scrumHelper.js | 22 ++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/src/scripts/gitlabHelper.js b/src/scripts/gitlabHelper.js index 3170023e..3f4fb49e 100644 --- a/src/scripts/gitlabHelper.js +++ b/src/scripts/gitlabHelper.js @@ -171,11 +171,37 @@ class GitLabHelper { } } + // Fetch commits from each project + let allCommits = []; + for (const project of allProjects) { + try { + const projectCommitsUrl = `${this.baseUrl}/projects/${project.id}/repository/commits?author_name=${username}&since=${startDate}T00:00:00Z&until=${endDate}T23:59:59Z&per_page=100&order_by=committed_date&sort=desc`; + const projectCommitsRes = await fetch(projectCommitsUrl, { headers }); + if (projectCommitsRes.ok) { + const projectCommits = await projectCommitsRes.json(); + allCommits = allCommits.concat( + projectCommits.map((commit) => ({ + ...commit, + project_id: project.id, + project_name: project.name, + project_url: project.web_url, + })), + ); + } + // Add small delay to avoid rate limiting + await new Promise((resolve) => setTimeout(resolve, 100)); + } catch (error) { + console.error(`Error fetching commits for project ${project.name}:`, error); + // Continue with other projects + } + } + const gitlabData = { user: users[0], projects: allProjects, mergeRequests: allMergeRequests, // use project-by-project response issues: allIssues, // use project-by-project response + commits: allCommits, // commits from all projects comments: [], // Empty array since we're not fetching comments }; // Cache the data @@ -262,6 +288,7 @@ class GitLabHelper { const processed = { mergeRequests: data.mergeRequests || [], issues: data.issues || [], + commits: data.commits || [], comments: data.comments || [], user: data.user, }; diff --git a/src/scripts/scrumHelper.js b/src/scripts/scrumHelper.js index 363af3ba..72ba7e68 100644 --- a/src/scripts/scrumHelper.js +++ b/src/scripts/scrumHelper.js @@ -274,9 +274,20 @@ function allIncluded(outputTarget = 'email') { const mappedMRs = (data.mergeRequests || data.mrs || []).map((mr) => mapGitLabItem(mr, data.projects, 'mr'), ); + // Map commits to standard format for report generation + const mappedCommits = (data.commits || []).map((commit) => ({ + ...commit, + message: commit.message || commit.title, + html_url: commit.web_url || (commit.project_url ? `${commit.project_url}/-/commit/${commit.id}` : ''), + sha: commit.id, + project: commit.project_name, + author_name: commit.author_name, + author_email: commit.author_email, + })); const mappedData = { githubIssuesData: { items: mappedIssues }, githubPrsReviewData: { items: mappedMRs }, + gitlabCommits: mappedCommits, githubUserData: data.user || {}, }; githubUserData = mappedData.githubUserData; @@ -342,9 +353,20 @@ function allIncluded(outputTarget = 'email') { const mappedMRs = (data.mergeRequests || data.mrs || []).map((mr) => mapGitLabItem(mr, data.projects, 'mr'), ); + // Map commits to standard format for report generation + const mappedCommits = (data.commits || []).map((commit) => ({ + ...commit, + message: commit.message || commit.title, + html_url: commit.web_url || (commit.project_url ? `${commit.project_url}/-/commit/${commit.id}` : ''), + sha: commit.id, + project: commit.project_name, + author_name: commit.author_name, + author_email: commit.author_email, + })); const mappedData = { githubIssuesData: { items: mappedIssues }, githubPrsReviewData: { items: mappedMRs }, + gitlabCommits: mappedCommits, githubUserData: data.user || {}, }; processGithubData(mappedData); From 74329f4fa850f46e108a009f0b2d7ae2cd766fb6 Mon Sep 17 00:00:00 2001 From: Charancs2 Date: Tue, 31 Mar 2026 18:29:53 +0530 Subject: [PATCH 3/4] feat: complete GitLab support with commits fetching - Add commits fetching from GitLab projects with date filtering - Map commits to standard format for report generation - Include commits data in GitLab data processing - Add project metadata to commits for better tracking - Support author filtering using author_name parameter This enables complete feature parity with GitHub support: Projects fetching Merge requests fetchingIssues fetching Commits fetching --- src/scripts/gitlabHelper.js | 27 +++++++++++++++++++++++++++ src/scripts/scrumHelper.js | 28 ++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/src/scripts/gitlabHelper.js b/src/scripts/gitlabHelper.js index 3170023e..3f4fb49e 100644 --- a/src/scripts/gitlabHelper.js +++ b/src/scripts/gitlabHelper.js @@ -171,11 +171,37 @@ class GitLabHelper { } } + // Fetch commits from each project + let allCommits = []; + for (const project of allProjects) { + try { + const projectCommitsUrl = `${this.baseUrl}/projects/${project.id}/repository/commits?author_name=${username}&since=${startDate}T00:00:00Z&until=${endDate}T23:59:59Z&per_page=100&order_by=committed_date&sort=desc`; + const projectCommitsRes = await fetch(projectCommitsUrl, { headers }); + if (projectCommitsRes.ok) { + const projectCommits = await projectCommitsRes.json(); + allCommits = allCommits.concat( + projectCommits.map((commit) => ({ + ...commit, + project_id: project.id, + project_name: project.name, + project_url: project.web_url, + })), + ); + } + // Add small delay to avoid rate limiting + await new Promise((resolve) => setTimeout(resolve, 100)); + } catch (error) { + console.error(`Error fetching commits for project ${project.name}:`, error); + // Continue with other projects + } + } + const gitlabData = { user: users[0], projects: allProjects, mergeRequests: allMergeRequests, // use project-by-project response issues: allIssues, // use project-by-project response + commits: allCommits, // commits from all projects comments: [], // Empty array since we're not fetching comments }; // Cache the data @@ -262,6 +288,7 @@ class GitLabHelper { const processed = { mergeRequests: data.mergeRequests || [], issues: data.issues || [], + commits: data.commits || [], comments: data.comments || [], user: data.user, }; diff --git a/src/scripts/scrumHelper.js b/src/scripts/scrumHelper.js index 363af3ba..8dc22f0c 100644 --- a/src/scripts/scrumHelper.js +++ b/src/scripts/scrumHelper.js @@ -63,6 +63,7 @@ function allIncluded(outputTarget = 'email') { let nextWeekArray = []; let reviewedPrsArray = []; let githubIssuesData = null; + let githubCommitsData = null; let yesterdayContribution = false; let githubPrsReviewData = null; let githubUserData = null; @@ -274,12 +275,24 @@ function allIncluded(outputTarget = 'email') { const mappedMRs = (data.mergeRequests || data.mrs || []).map((mr) => mapGitLabItem(mr, data.projects, 'mr'), ); + // Map commits to standard format for report generation + const mappedCommits = (data.commits || []).map((commit) => ({ + ...commit, + message: commit.message || commit.title, + html_url: commit.web_url || (commit.project_url ? `${commit.project_url}/-/commit/${commit.id}` : ''), + sha: commit.id, + project: commit.project_name, + author_name: commit.author_name, + author_email: commit.author_email, + })); const mappedData = { githubIssuesData: { items: mappedIssues }, githubPrsReviewData: { items: mappedMRs }, + gitlabCommits: mappedCommits, githubUserData: data.user || {}, }; githubUserData = mappedData.githubUserData; + githubCommitsData = mappedCommits; const name = githubUserData?.name || githubUserData?.username || platformUsernameLocal || platformUsername; @@ -342,9 +355,20 @@ function allIncluded(outputTarget = 'email') { const mappedMRs = (data.mergeRequests || data.mrs || []).map((mr) => mapGitLabItem(mr, data.projects, 'mr'), ); + // Map commits to standard format for report generation + const mappedCommits = (data.commits || []).map((commit) => ({ + ...commit, + message: commit.message || commit.title, + html_url: commit.web_url || (commit.project_url ? `${commit.project_url}/-/commit/${commit.id}` : ''), + sha: commit.id, + project: commit.project_name, + author_name: commit.author_name, + author_email: commit.author_email, + })); const mappedData = { githubIssuesData: { items: mappedIssues }, githubPrsReviewData: { items: mappedMRs }, + gitlabCommits: mappedCommits, githubUserData: data.user || {}, }; processGithubData(mappedData); @@ -1045,6 +1069,10 @@ function allIncluded(outputTarget = 'email') { } else if (platform === 'gitlab') { await writeGithubIssuesPrs(githubIssuesData?.items || []); await writeGithubIssuesPrs(githubPrsReviewData?.items || []); + // Add commits to the report for GitLab + if (githubCommitsData && githubCommitsData.length > 0) { + await writeGithubIssuesPrs(githubCommitsData); + } } await writeGithubPrsReviews(); log('[DEBUG] Both data processing functions completed, generating scrum body'); From 07902c0440ef7ec2f37650c775e4c861ffd925c7 Mon Sep 17 00:00:00 2001 From: Charancs2 Date: Mon, 20 Apr 2026 19:27:31 +0530 Subject: [PATCH 4/4] fix: Prevent XSS vulnerability in report rendering (#546) Apply escapeHtml() to all user-controlled data from GitHub/GitLab API responses before HTML insertion. Fixes XSS vulnerabilities in PR titles, issue titles, commit messages, and repository names. - Escape title, project, html_url in all PR/issue rendering - Escape commit.messageHeadline in commit lists - Escape reviewed PR titles and URLs - Preserve all existing HTML structure and functionality Security Impact: - Prevents arbitrary JavaScript execution from malicious API data - Protects GitHub/GitLab tokens and user activity data - Secures both extension popup and email compose window contexts Fixes #546 --- src/scripts/scrumHelper.js | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/scripts/scrumHelper.js b/src/scripts/scrumHelper.js index 8dc22f0c..26df29a9 100644 --- a/src/scripts/scrumHelper.js +++ b/src/scripts/scrumHelper.js @@ -1464,11 +1464,11 @@ ${blockerText}`; let prText = ''; prText += "#" + pr_arr.number + ' (' + - pr_arr.title + + escapeHtml(pr_arr.title) + ') '; if (showOpenLabel && pr_arr.state === 'open') prText += issue_opened_button; // Do not show closed label for reviewed PRs @@ -1482,11 +1482,11 @@ ${blockerText}`; let prText1 = ''; prText1 += "
  • #" + pr_arr1.number + ' (' + - pr_arr1.title + + escapeHtml(pr_arr1.title) + ') '; if (showOpenLabel && pr_arr1.state === 'open') prText1 += issue_opened_button; // Do not show closed label for reviewed PRs @@ -1768,31 +1768,31 @@ ${blockerText}`; } if (isDraft) { - li = `
  • (${project}) - Made PR (#${number}) - ${title}${showOpenLabel ? ' ' + pr_draft_button : ''}`; + li = `
  • (${escapeHtml(project)}) - Made PR (#${number}) - ${escapeHtml(title)}${showOpenLabel ? ' ' + pr_draft_button : ''}`; if (showCommits && item._allCommits && item._allCommits.length && !isNewPR) { log(`[PR DEBUG] Rendering commits for existing draft PR #${number}:`, item._allCommits); li += '
      '; item._allCommits.forEach((commit) => { - li += `
    • ${commit.messageHeadline} (${new Date(commit.committedDate).toLocaleString()})
    • `; + li += `
    • ${escapeHtml(commit.messageHeadline)} (${new Date(commit.committedDate).toLocaleString()})
    • `; }); li += '
    '; } li += `
  • `; } else if (item.state === 'open' || item.state === 'opened') { - li = `
  • (${project}) - ${prAction} (#${number}) - ${title}${showOpenLabel ? ' ' + pr_open_button : ''}`; + li = `
  • (${escapeHtml(project)}) - ${prAction} (#${number}) - ${escapeHtml(title)}${showOpenLabel ? ' ' + pr_open_button : ''}`; if (showCommits && item._allCommits && item._allCommits.length && !isNewPR) { log(`[PR DEBUG] Rendering commits for existing PR #${number}:`, item._allCommits); li += '
      '; item._allCommits.forEach((commit) => { li += `
    • -${commit.messageHeadline} (${new Date(commit.committedDate).toLocaleString()})
    • `; +${escapeHtml(commit.messageHeadline)} (${new Date(commit.committedDate).toLocaleString()})`; }); li += '
    '; } li += `
  • `; } else if (platform === 'gitlab' && item.state === 'closed') { - li = `
  • (${project}) - ${prAction} (#${number}) - ${title}${showOpenLabel ? ' ' + pr_closed_button : ''}
  • `; + li = `
  • (${escapeHtml(project)}) - ${prAction} (#${number}) - ${escapeHtml(title)}${showOpenLabel ? ' ' + pr_closed_button : ''}
  • `; } else { let merged = null; if ((githubToken || (useMergedStatus && !fallbackToSimple)) && mergedStatusResults) { @@ -1802,10 +1802,10 @@ ${blockerText}`; merged = mergedStatusResults[`${owner}/${repo}#${number}`]; } if (merged === true) { - li = `
  • (${project}) - ${prAction} (#${number}) - ${title}${showOpenLabel ? ' ' + pr_merged_button : ''}
  • `; + li = `
  • (${escapeHtml(project)}) - ${prAction} (#${number}) - ${escapeHtml(title)}${showOpenLabel ? ' ' + pr_merged_button : ''}
  • `; } else { // Always show closed label for merged === false or merged === null/undefined - li = `
  • (${project}) - ${prAction} (#${number}) - ${title}${showOpenLabel ? ' ' + pr_closed_button : ''}
  • `; + li = `
  • (${escapeHtml(project)}) - ${prAction} (#${number}) - ${escapeHtml(title)}${showOpenLabel ? ' ' + pr_closed_button : ''}
  • `; } } log('[SCRUM-DEBUG] Added PR/MR to lastWeekArray:', li, item); @@ -1816,13 +1816,13 @@ ${blockerText}`; if (item.state === 'open' && item.body?.toUpperCase().indexOf('YES') > 0) { const li2 = '
  • (' + - project + + escapeHtml(project) + ') - Work on Issue(#' + number + ") - " + - title + + escapeHtml(title) + '' + (showOpenLabel ? ' ' + issue_opened_button : '') + '  
  • '; @@ -1836,19 +1836,19 @@ ${blockerText}`; const isCreatedToday = today.getTime() === itemCreatedDate.getTime(); const issueActionText = isCreatedToday ? 'Opened Issue' : 'Updated Issue'; if (item.state === 'open') { - li = `
  • (${project}) - ${issueActionText}(#${number}) - ${title}${showOpenLabel ? ' ' + issue_opened_button : ''}
  • `; + li = `
  • (${escapeHtml(project)}) - ${issueActionText}(#${number}) - ${escapeHtml(title)}${showOpenLabel ? ' ' + issue_opened_button : ''}
  • `; } else if (item.state === 'closed') { // Use state_reason to distinguish closure reason if (item.state_reason === 'completed') { - li = `
  • (${project}) - ${issueActionText}(#${number}) - ${title} ${issue_closed_completed_button}
  • `; + li = `
  • (${escapeHtml(project)}) - ${issueActionText}(#${number}) - ${escapeHtml(title)} ${issue_closed_completed_button}
  • `; } else if (item.state_reason === 'not_planned') { - li = `
  • (${project}) - ${issueActionText}(#${number}) - ${title} ${issue_closed_notplanned_button}
  • `; + li = `
  • (${escapeHtml(project)}) - ${issueActionText}(#${number}) - ${escapeHtml(title)} ${issue_closed_notplanned_button}
  • `; } else { - li = `
  • (${project}) - ${issueActionText}(#${number}) - ${title} ${issue_closed_button}
  • `; + li = `
  • (${escapeHtml(project)}) - ${issueActionText}(#${number}) - ${escapeHtml(title)} ${issue_closed_button}
  • `; } } else { // Fallback for unexpected state - li = `
  • (${project}) - ${issueActionText}(#${number}) - ${title}
  • `; + li = `
  • (${escapeHtml(project)}) - ${issueActionText}(#${number}) - ${escapeHtml(title)}
  • `; } log('[SCRUM-DEBUG] Added issue to lastWeekArray:', li, item);