Skip to content

Instantly share code, notes, and snippets.

@samber
Last active March 4, 2026 22:40
Show Gist options
  • Select an option

  • Save samber/cf4ab9b2e9d528e1defa1ea67ff8beaa to your computer and use it in GitHub Desktop.

Select an option

Save samber/cf4ab9b2e9d528e1defa1ea67ff8beaa to your computer and use it in GitHub Desktop.
const SHEET_STARGAZER = "Github stargazers";
const GITHUB_TOKEN = "xxx"
const REPOSITORIES = [
"samber/lo",
"samber/do",
]
function makeRequest(path) {
const url = 'https://api.github.com'+path;
const response = UrlFetchApp.fetch(url, {
headers: {
Authorization: 'Bearer ' + GITHUB_TOKEN,
Accept: 'application/vnd.github.v3.star+json',
},
muteHttpExceptions: true,
});
return JSON.parse(response.getContentText());
}
function* iterStargazers(repo, firstPage) {
let page = firstPage;
while (true) {
const url = "/repos/" + repo + "/stargazers?per_page=100&page=" + page.toFixed(0);
const data = makeRequest(url);
if (!data || data.length === 0) break;
for (const item of data) {
if (!item.user) {
throw Error("malformed payload");
}
const user = item.user;
yield {
login: user.login,
id: user.id,
starred_at: item.starred_at
};
}
page++;
}
}
function getUserDetails(login) {
const url = "/users/" + login;
const data = makeRequest(url);
return {
name: data.name,
email: data.email,
company: data.company,
location: data.location,
bio: data.bio,
twitter: data.twitter_username,
blog: data.blog,
followers: data.followers,
following: data.following,
public_repos: data.public_repos,
public_gists: data.public_gists
};
}
function syncStargazers() {
const repos = REPOSITORIES;
const now = new Date().toISOString().replace(/\.\d+Z$/, "Z");
// Create or clear a sheet
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_STARGAZER);
// sheet.clear();
// Read existing logins into a Set for fast lookup
const existingLogins = sheet.getRange("A2:Q").getValues();
const countPerRepo = existingLogins.reduce((prev, curr) => {
const repo = curr[0];
if (!prev[repo]) {
prev[repo] = 0
}
prev[repo]++
return prev;
}, {})
const stargazersPerRepo = existingLogins.reduce((prev, curr) => {
const repo = curr[0];
const login = curr[2];
if (!prev[repo]) {
prev[repo] = {}
}
if (!prev[repo][login]) {
prev[repo][login] = true;
}
return prev;
}, {})
// const header = [
// "repository", "user_id", "login", "name", "email", "company", "location",
// "bio", "twitter", "blog", "followers", "following", "public_repos",
// "public_gists", "starred_at", "scrapped_at", "updated_at"
// ];
// sheet.appendRow(header);
let imported = 0;
for (let repo of repos) {
let count = 0;
if (countPerRepo[repo]) {
count = countPerRepo[repo];
}
let firstPage = Math.trunc((Math.trunc(count) / 100) + 1);
// Read one page earlier, because some people might have removed stars.
// Anyway, this is cheap since we dedup.
if (firstPage > 1) {
firstPage--;
}
for (const entry of iterStargazers(repo, firstPage)) {
if (stargazersPerRepo[repo] && stargazersPerRepo[repo][entry.login]) {
continue;
}
const details = getUserDetails(entry.login);
const row = [
repo,
entry.id,
entry.login,
details.name,
details.email,
details.company,
details.location,
details.bio,
details.twitter,
details.blog,
details.followers,
details.following,
details.public_repos,
details.public_gists,
entry.starred_at,
now,
now
];
sheet.appendRow(row);
Logger.log("Done: " + entry.login);
Utilities.sleep(100); // polite delay
if (imported >= 1000) {
return;
}
imported++
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment