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
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion biome.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
27 changes: 27 additions & 0 deletions src/scripts/gitlabHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => ({
Comment on lines +174 to +183
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.

suggestion (bug_risk): Per-project commit fetching is sequential and limited to 100 items, which may be slow and can miss commits for active projects.

The per-project loop fetches only a single page (per_page=100) with an added 100ms delay. This can (a) silently miss commits for very active projects due to missing pagination, and (b) be slow for users with many projects. Please either add pagination support (e.g., follow X-Next-Page or equivalent) or explicitly enforce and document the 100-commit limit in code. You could also improve throughput by batching, reducing the delay, limiting the queried projects, or running a bounded number of requests in parallel instead of fully sequentially.

...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
Expand Down Expand Up @@ -262,6 +288,7 @@ class GitLabHelper {
const processed = {
mergeRequests: data.mergeRequests || [],
issues: data.issues || [],
commits: data.commits || [],
comments: data.comments || [],
user: data.user,
};
Expand Down
66 changes: 47 additions & 19 deletions src/scripts/scrumHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -1436,11 +1464,11 @@ ${blockerText}`;
let prText = '';
prText +=
"<a href='" +
pr_arr.html_url +
escapeHtml(pr_arr.html_url) +
"' target='_blank' rel='noopener noreferrer'>#" +
pr_arr.number +
'</a> (' +
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
Expand All @@ -1454,11 +1482,11 @@ ${blockerText}`;
let prText1 = '';
prText1 +=
"<li><a href='" +
pr_arr1.html_url +
escapeHtml(pr_arr1.html_url) +
"' target='_blank' rel='noopener noreferrer'>#" +
pr_arr1.number +
'</a> (' +
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
Expand Down Expand Up @@ -1740,31 +1768,31 @@ ${blockerText}`;
}

if (isDraft) {
li = `<li><i>(${project})</i> - Made PR <a href='${html_url}' target='_blank' rel='noopener noreferrer' contenteditable='false'>(#${number})</a> - <a href='${html_url}' target='_blank' rel='noopener noreferrer' contenteditable='false'>${title}</a>${showOpenLabel ? ' ' + pr_draft_button : ''}`;
li = `<li><i>(${escapeHtml(project)})</i> - Made PR <a href='${escapeHtml(html_url)}' target='_blank' rel='noopener noreferrer' contenteditable='false'>(#${number})</a> - <a href='${escapeHtml(html_url)}' target='_blank' rel='noopener noreferrer' contenteditable='false'>${escapeHtml(title)}</a>${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 += '<ul>';
item._allCommits.forEach((commit) => {
li += `<li style=\"list-style: disc; color: #666;\"><span style=\"color:#2563eb;\">${commit.messageHeadline}</span><span style=\"color:#666; font-size: 11px;\"> (${new Date(commit.committedDate).toLocaleString()})</span></li>`;
li += `<li style=\"list-style: disc; color: #666;\"><span style=\"color:#2563eb;\">${escapeHtml(commit.messageHeadline)}</span><span style=\"color:#666; font-size: 11px;\"> (${new Date(commit.committedDate).toLocaleString()})</span></li>`;
});
li += '</ul>';
}
li += `</li>`;
} else if (item.state === 'open' || item.state === 'opened') {
li = `<li><i>(${project})</i> - ${prAction} <a href='${html_url}' target='_blank' rel='noopener noreferrer' contenteditable='false'>(#${number})</a> - <a href='${html_url}' target='_blank' rel='noopener noreferrer' contenteditable='false'>${title}</a>${showOpenLabel ? ' ' + pr_open_button : ''}`;
li = `<li><i>(${escapeHtml(project)})</i> - ${prAction} <a href='${escapeHtml(html_url)}' target='_blank' rel='noopener noreferrer' contenteditable='false'>(#${number})</a> - <a href='${escapeHtml(html_url)}' target='_blank' rel='noopener noreferrer' contenteditable='false'>${escapeHtml(title)}</a>${showOpenLabel ? ' ' + pr_open_button : ''}`;

if (showCommits && item._allCommits && item._allCommits.length && !isNewPR) {
log(`[PR DEBUG] Rendering commits for existing PR #${number}:`, item._allCommits);
li += '<ul>';
item._allCommits.forEach((commit) => {
li += `<li style="list-style: disc; color: #666;">
<span style="color:#2563eb;">${commit.messageHeadline}</span><span style="color:#666; font-size: 11px;"> (${new Date(commit.committedDate).toLocaleString()})</span></li>`;
<span style="color:#2563eb;">${escapeHtml(commit.messageHeadline)}</span><span style="color:#666; font-size: 11px;"> (${new Date(commit.committedDate).toLocaleString()})</span></li>`;
});
li += '</ul>';
}
li += `</li>`;
} else if (platform === 'gitlab' && item.state === 'closed') {
li = `<li><i>(${project})</i> - ${prAction} <a href='${html_url}' target='_blank' rel='noopener noreferrer' contenteditable='false'>(#${number})</a> - <a href='${html_url}' target='_blank' rel='noopener noreferrer' contenteditable='false'>${title}</a>${showOpenLabel ? ' ' + pr_closed_button : ''}</li>`;
li = `<li><i>(${escapeHtml(project)})</i> - ${prAction} <a href='${escapeHtml(html_url)}' target='_blank' rel='noopener noreferrer' contenteditable='false'>(#${number})</a> - <a href='${escapeHtml(html_url)}' target='_blank' rel='noopener noreferrer' contenteditable='false'>${escapeHtml(title)}</a>${showOpenLabel ? ' ' + pr_closed_button : ''}</li>`;
} else {
let merged = null;
if ((githubToken || (useMergedStatus && !fallbackToSimple)) && mergedStatusResults) {
Expand All @@ -1774,10 +1802,10 @@ ${blockerText}`;
merged = mergedStatusResults[`${owner}/${repo}#${number}`];
}
if (merged === true) {
li = `<li><i>(${project})</i> - ${prAction} <a href='${html_url}' target='_blank' rel='noopener noreferrer' contenteditable='false'>(#${number})</a> - <a href='${html_url}' target='_blank' rel='noopener noreferrer' contenteditable='false'>${title}</a>${showOpenLabel ? ' ' + pr_merged_button : ''}</li>`;
li = `<li><i>(${escapeHtml(project)})</i> - ${prAction} <a href='${escapeHtml(html_url)}' target='_blank' rel='noopener noreferrer' contenteditable='false'>(#${number})</a> - <a href='${escapeHtml(html_url)}' target='_blank' rel='noopener noreferrer' contenteditable='false'>${escapeHtml(title)}</a>${showOpenLabel ? ' ' + pr_merged_button : ''}</li>`;
} else {
// Always show closed label for merged === false or merged === null/undefined
li = `<li><i>(${project})</i> - ${prAction} <a href='${html_url}' target='_blank' rel='noopener noreferrer' contenteditable='false'>(#${number})</a> - <a href='${html_url}' target='_blank' rel='noopener noreferrer' contenteditable='false'>${title}</a>${showOpenLabel ? ' ' + pr_closed_button : ''}</li>`;
li = `<li><i>(${escapeHtml(project)})</i> - ${prAction} <a href='${escapeHtml(html_url)}' target='_blank' rel='noopener noreferrer' contenteditable='false'>(#${number})</a> - <a href='${escapeHtml(html_url)}' target='_blank' rel='noopener noreferrer' contenteditable='false'>${escapeHtml(title)}</a>${showOpenLabel ? ' ' + pr_closed_button : ''}</li>`;
}
}
log('[SCRUM-DEBUG] Added PR/MR to lastWeekArray:', li, item);
Expand All @@ -1788,13 +1816,13 @@ ${blockerText}`;
if (item.state === 'open' && item.body?.toUpperCase().indexOf('YES') > 0) {
const li2 =
'<li><i>(' +
project +
escapeHtml(project) +
')</i> - Work on Issue(#' +
number +
") - <a href='" +
html_url +
escapeHtml(html_url) +
"' target='_blank' rel='noopener noreferrer'>" +
title +
escapeHtml(title) +
'</a>' +
(showOpenLabel ? ' ' + issue_opened_button : '') +
'&nbsp;&nbsp;</li>';
Expand All @@ -1808,19 +1836,19 @@ ${blockerText}`;
const isCreatedToday = today.getTime() === itemCreatedDate.getTime();
const issueActionText = isCreatedToday ? 'Opened Issue' : 'Updated Issue';
if (item.state === 'open') {
li = `<li><i>(${project})</i> - ${issueActionText}(#${number}) - <a href='${html_url}'>${title}</a>${showOpenLabel ? ' ' + issue_opened_button : ''}</li>`;
li = `<li><i>(${escapeHtml(project)})</i> - ${issueActionText}(#${number}) - <a href='${escapeHtml(html_url)}'>${escapeHtml(title)}</a>${showOpenLabel ? ' ' + issue_opened_button : ''}</li>`;
} else if (item.state === 'closed') {
// Use state_reason to distinguish closure reason
if (item.state_reason === 'completed') {
li = `<li><i>(${project})</i> - ${issueActionText}(#${number}) - <a href='${html_url}'>${title}</a> ${issue_closed_completed_button}</li>`;
li = `<li><i>(${escapeHtml(project)})</i> - ${issueActionText}(#${number}) - <a href='${escapeHtml(html_url)}'>${escapeHtml(title)}</a> ${issue_closed_completed_button}</li>`;
} else if (item.state_reason === 'not_planned') {
li = `<li><i>(${project})</i> - ${issueActionText}(#${number}) - <a href='${html_url}'>${title}</a> ${issue_closed_notplanned_button}</li>`;
li = `<li><i>(${escapeHtml(project)})</i> - ${issueActionText}(#${number}) - <a href='${escapeHtml(html_url)}'>${escapeHtml(title)}</a> ${issue_closed_notplanned_button}</li>`;
} else {
li = `<li><i>(${project})</i> - ${issueActionText}(#${number}) - <a href='${html_url}'>${title}</a> ${issue_closed_button}</li>`;
li = `<li><i>(${escapeHtml(project)})</i> - ${issueActionText}(#${number}) - <a href='${escapeHtml(html_url)}'>${escapeHtml(title)}</a> ${issue_closed_button}</li>`;
}
} else {
// Fallback for unexpected state
li = `<li><i>(${project})</i> - ${issueActionText}(#${number}) - <a href='${html_url}'>${title}</a></li>`;
li = `<li><i>(${escapeHtml(project)})</i> - ${issueActionText}(#${number}) - <a href='${escapeHtml(html_url)}'>${escapeHtml(title)}</a></li>`;
}

log('[SCRUM-DEBUG] Added issue to lastWeekArray:', li, item);
Expand Down
Loading