Skip to content

Instantly share code, notes, and snippets.

@a-h-abid
Created April 1, 2026 04:37
Show Gist options
  • Select an option

  • Save a-h-abid/ddc6d8bcbdfeac24fa7a1b3094d943b9 to your computer and use it in GitHub Desktop.

Select an option

Save a-h-abid/ddc6d8bcbdfeac24fa7a1b3094d943b9 to your computer and use it in GitHub Desktop.
Gitlab Commit Export for mentioned user across all projects & branches.

gitlab-commits-export

A Node.js script that fetches all commit messages by a specific user across all accessible GitLab repositories, filtered by date range, and outputs the results as a CSV file.

Features

  • Scans all projects the authenticated user has access to
  • Searches across all branches (not just the default branch)
  • Filters commits by author email and date range
  • Deduplicates commits that appear on multiple branches
  • Streams CSV output incrementally (memory-safe for large instances)
  • Progress and error messages go to stderr, keeping stdout clean for CSV

Requirements

  • Node.js v18+ (uses native fetch)
  • A GitLab personal access token with read_api scope
  • GitLab CE v17.5.x or compatible

Setup

No dependencies required. Clone or download the script and run it directly.

# Download the script
curl -O https://your-host/gitlab-commits.mjs

# Or clone your repo
git clone https://your-repo/gitlab-commits-export.git
cd gitlab-commits-export

Configuration

Edit the constants at the top of gitlab-commits.mjs:

const GITLAB_URL   = 'https://your-gitlab.example.com'; // Your GitLab instance URL
const TOKEN        = 'your_personal_access_token';       // GitLab PAT with read_api scope
const AUTHOR_EMAIL = 'user@example.com';                 // Commit author email to filter by
const SINCE        = '2024-01-01T00:00:00Z';             // Start of date range (ISO 8601)
const UNTIL        = '2024-12-31T23:59:59Z';             // End of date range (ISO 8601)

Generating a GitLab Personal Access Token

  1. Go to GitLab → User Settings → Access Tokens
  2. Create a token with the read_api scope
  3. Copy the token and paste it into the TOKEN constant

Usage

# Output CSV to terminal
node gitlab-commits.mjs

# Save CSV to file (progress shown in terminal)
node gitlab-commits.mjs > commits.csv

# Save CSV to file and also capture progress log
node gitlab-commits.mjs 2>progress.log > commits.csv

# Watch progress in terminal while saving CSV to file
node gitlab-commits.mjs 2>&1 1>commits.csv | cat

Output Format

The script outputs a CSV file with the following columns:

Column Description
project Full project path (e.g. group/repo)
short_id Abbreviated commit SHA (e.g. a1b2c3d4)
id Full commit SHA
date Authored date in ISO 8601 format
title First line of the commit message
message Full commit message

Example Output

project,short_id,id,date,title,message
group/my-repo,a1b2c3d4,a1b2c3d4e5f6...,2024-06-15T10:23:00.000+06:00,Fix null pointer in payment module,Fix null pointer in payment module
group/another-repo,b2c3d4e5,b2c3d4e5f6a7...,2024-06-20T14:05:00.000+06:00,Refactor invoice service,Refactor invoice service

Notes

  • Project scope: By default, the script fetches all projects where the authenticated user is a member (membership=true). To restrict to only owned projects, change membership: true to owned: true in the fetchAllPages call for projects.
  • Rate limiting: On GitLab instances with many repositories, consider adding a small delay between project iterations to avoid hitting API rate limits.
  • Large instances: CSV is written incrementally to stdout as each project is processed, so memory usage stays low regardless of the number of commits found.
  • Author matching: Filtering is done by author_email in code rather than using GitLab's loose author query parameter, which only matches on name and is unreliable.

Troubleshooting

Symptom Likely Cause Fix
API error 401 Invalid or expired token Regenerate your PAT
API error 403 Token missing read_api scope Recreate token with correct scope
Skipping <project>: ... Repo is empty or archived Expected — script skips gracefully
No commits found Wrong AUTHOR_EMAIL Verify the email matches GitLab commit history
Duplicate rows Should not happen — open an issue Deduplication is done via commit SHA

License

MIT

// gitlab-commits.mjs
// Usage: node gitlab-commits.mjs > commits.csv
const GITLAB_URL = 'https://your-gitlab.example.com';
const TOKEN = 'your_personal_access_token';
const AUTHOR_EMAIL = 'user@example.com';
const SINCE = '2024-01-01T00:00:00Z';
const UNTIL = '2024-12-31T23:59:59Z';
async function fetchAllPages(path, params = {}) {
const results = [];
let page = 1;
while (true) {
const qs = new URLSearchParams({ ...params, page, per_page: 100 }).toString();
const res = await fetch(`${GITLAB_URL}/api/v4/${path}?${qs}`, {
headers: { 'PRIVATE-TOKEN': TOKEN },
});
if (!res.ok) throw new Error(`API error ${res.status} on ${path}`);
const data = await res.json();
if (!data.length) break;
results.push(...data);
const totalPages = parseInt(res.headers.get('x-total-pages') || '1');
if (page >= totalPages) break;
page++;
}
return results;
}
function escapeCsv(value) {
if (value === null || value === undefined) return '';
const str = String(value).replace(/\r?\n|\r/g, ' '); // flatten newlines
return /[",\n]/.test(str) ? `"${str.replace(/"/g, '""')}"` : str;
}
function toCsvRow(fields) {
return fields.map(escapeCsv).join(',');
}
async function main() {
console.error('Fetching accessible projects...');
const projects = await fetchAllPages('projects', { membership: true, simple: true });
console.error(`Found ${projects.length} projects. Scanning commits...`);
const seen = new Set(); // deduplicate commits appearing on multiple branches
// CSV header to stdout
console.log(toCsvRow(['project', 'short_id', 'id', 'date', 'title', 'message']));
for (const project of projects) {
try {
const commits = await fetchAllPages(`projects/${project.id}/repository/commits`, {
all: true,
since: SINCE,
until: UNTIL,
with_stats: false,
});
const mine = commits.filter(c => {
if (c.author_email !== AUTHOR_EMAIL) return false;
if (seen.has(c.id)) return false;
seen.add(c.id);
return true;
});
if (mine.length > 0) {
console.error(` ${project.path_with_namespace}: ${mine.length} commit(s)`);
mine.forEach(c => {
console.log(toCsvRow([
project.path_with_namespace,
c.short_id,
c.id,
c.authored_date,
c.title,
c.message,
]));
});
}
} catch (err) {
console.error(` Skipping ${project.path_with_namespace}: ${err.message}`);
}
}
console.error(`\nDone. ${seen.size} unique commits written.`);
}
main().catch(console.error);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment