2024-12-13 17:31:09 +00:00
|
|
|
import * as plugins from './codefeed.plugins.js';
|
|
|
|
|
|
|
|
|
|
|
|
export class CodeFeed {
|
|
|
|
private baseUrl: string;
|
|
|
|
private token?: string;
|
2024-12-13 18:51:42 +00:00
|
|
|
private npmRegistry = new plugins.smartnpm.NpmRegistry();
|
|
|
|
private smartxmlInstance = new plugins.smartxml.SmartXml();
|
|
|
|
private lastRunTimestamp: string;
|
2024-12-13 17:31:09 +00:00
|
|
|
|
2024-12-13 18:51:42 +00:00
|
|
|
constructor(baseUrl: string, token?: string, lastRunTimestamp?: string) {
|
2024-12-13 17:31:09 +00:00
|
|
|
this.baseUrl = baseUrl;
|
|
|
|
this.token = token;
|
2024-12-13 18:51:42 +00:00
|
|
|
this.lastRunTimestamp = lastRunTimestamp || new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
|
|
|
|
console.log('CodeFeed initialized with last run timestamp:', this.lastRunTimestamp);
|
2024-12-13 17:31:09 +00:00
|
|
|
}
|
|
|
|
|
2024-12-13 18:51:42 +00:00
|
|
|
/**
|
|
|
|
* Fetch all organizations from the Gitea instance.
|
|
|
|
*/
|
|
|
|
private async fetchAllOrganizations(): Promise<string[]> {
|
|
|
|
const url = `${this.baseUrl}/api/v1/orgs`;
|
|
|
|
const response = await fetch(url, {
|
|
|
|
headers: this.token ? { Authorization: `token ${this.token}` } : {},
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
throw new Error(`Failed to fetch organizations: ${response.statusText}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
const data: { username: string }[] = await response.json();
|
|
|
|
return data.map((org) => org.username);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Fetch organization-level activity RSS feed.
|
|
|
|
*/
|
|
|
|
private async fetchOrgRssFeed(optionsArg: {
|
|
|
|
orgName: string,
|
|
|
|
repoName?: string,
|
|
|
|
}): Promise<any[]> {
|
|
|
|
let rssUrl: string
|
|
|
|
if (optionsArg.orgName && !optionsArg.repoName) {
|
|
|
|
rssUrl = `${this.baseUrl}/${optionsArg.orgName}.atom`;
|
|
|
|
} else if (optionsArg.orgName && optionsArg.repoName) {
|
|
|
|
rssUrl = `${this.baseUrl}/${optionsArg.orgName}/${optionsArg.repoName}.atom`;
|
|
|
|
}
|
|
|
|
|
|
|
|
const response = await fetch(rssUrl);
|
|
|
|
if (!response.ok) {
|
|
|
|
throw new Error(`Failed to fetch RSS feed for organization ${optionsArg.orgName}/${optionsArg.repoName}: ${response.statusText}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
const rssText = await response.text();
|
|
|
|
|
|
|
|
// Parse the Atom feed using fast-xml-parser
|
|
|
|
const rssData = this.smartxmlInstance.parseXmlToObject(rssText);
|
|
|
|
|
|
|
|
// Return the <entry> elements from the feed
|
|
|
|
return rssData.feed.entry || [];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if the organization's RSS feed has any new activities since the last run.
|
|
|
|
*/
|
|
|
|
private async hasNewActivity(optionsArg: {
|
|
|
|
orgName: string,
|
|
|
|
repoName?: string,
|
|
|
|
}): Promise<boolean> {
|
|
|
|
const entries = await this.fetchOrgRssFeed(optionsArg);
|
|
|
|
|
|
|
|
// Filter entries to find new activities since the last run
|
|
|
|
return entries.some((entry: any) => {
|
|
|
|
const updated = new Date(entry.updated);
|
|
|
|
return updated > new Date(this.lastRunTimestamp);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Fetch all repositories accessible to the token/user.
|
|
|
|
*/
|
2024-12-13 23:30:35 +00:00
|
|
|
private async fetchAllRepositories(): Promise<plugins.interfaces.Repository[]> {
|
2024-12-13 17:31:09 +00:00
|
|
|
let page = 1;
|
2024-12-13 23:30:35 +00:00
|
|
|
const allRepos: plugins.interfaces.Repository[] = [];
|
2024-12-13 17:31:09 +00:00
|
|
|
|
|
|
|
while (true) {
|
|
|
|
const url = new URL(`${this.baseUrl}/api/v1/repos/search`);
|
|
|
|
url.searchParams.set('limit', '50');
|
|
|
|
url.searchParams.set('page', page.toString());
|
|
|
|
|
|
|
|
const resp = await fetch(url.href, {
|
2024-12-13 18:51:42 +00:00
|
|
|
headers: this.token ? { 'Authorization': `token ${this.token}` } : {},
|
2024-12-13 17:31:09 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
if (!resp.ok) {
|
|
|
|
throw new Error(`Failed to fetch repositories: ${resp.statusText}`);
|
|
|
|
}
|
|
|
|
|
2024-12-13 23:30:35 +00:00
|
|
|
const data: plugins.interfaces.RepoSearchResponse = await resp.json();
|
2024-12-13 17:31:09 +00:00
|
|
|
allRepos.push(...data.data);
|
|
|
|
|
|
|
|
if (data.data.length < 50) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
page++;
|
|
|
|
}
|
|
|
|
|
|
|
|
return allRepos;
|
|
|
|
}
|
|
|
|
|
2024-12-13 18:51:42 +00:00
|
|
|
/**
|
|
|
|
* Fetch all tags for a given repository.
|
|
|
|
*/
|
2024-12-13 17:31:09 +00:00
|
|
|
private async fetchTags(owner: string, repo: string): Promise<Set<string>> {
|
|
|
|
let page = 1;
|
2024-12-13 23:30:35 +00:00
|
|
|
const tags: plugins.interfaces.Tag[] = [];
|
2024-12-13 17:31:09 +00:00
|
|
|
|
|
|
|
while (true) {
|
|
|
|
const url = new URL(`${this.baseUrl}/api/v1/repos/${owner}/${repo}/tags`);
|
|
|
|
url.searchParams.set('limit', '50');
|
|
|
|
url.searchParams.set('page', page.toString());
|
|
|
|
|
|
|
|
const resp = await fetch(url.href, {
|
2024-12-13 18:51:42 +00:00
|
|
|
headers: this.token ? { 'Authorization': `token ${this.token}` } : {},
|
2024-12-13 17:31:09 +00:00
|
|
|
});
|
2024-12-13 18:51:42 +00:00
|
|
|
|
2024-12-13 17:31:09 +00:00
|
|
|
if (!resp.ok) {
|
|
|
|
console.error(`Failed to fetch tags for ${owner}/${repo}: ${resp.status} ${resp.statusText} at ${url.href}`);
|
|
|
|
throw new Error(`Failed to fetch tags for ${owner}/${repo}: ${resp.statusText}`);
|
|
|
|
}
|
|
|
|
|
2024-12-13 23:30:35 +00:00
|
|
|
const data: plugins.interfaces.Tag[] = await resp.json();
|
2024-12-13 17:31:09 +00:00
|
|
|
tags.push(...data);
|
|
|
|
|
|
|
|
if (data.length < 50) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
page++;
|
|
|
|
}
|
|
|
|
|
|
|
|
const taggedCommitShas = new Set<string>();
|
|
|
|
for (const t of tags) {
|
|
|
|
if (t.commit?.sha) {
|
|
|
|
taggedCommitShas.add(t.commit.sha);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return taggedCommitShas;
|
|
|
|
}
|
|
|
|
|
2024-12-13 18:51:42 +00:00
|
|
|
/**
|
|
|
|
* Fetch commits from the last 24 hours for a repository.
|
|
|
|
*/
|
2024-12-13 23:30:35 +00:00
|
|
|
private async fetchRecentCommitsForRepo(owner: string, repo: string): Promise<plugins.interfaces.Commit[]> {
|
2024-12-13 17:31:09 +00:00
|
|
|
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
|
|
|
let page = 1;
|
2024-12-13 23:30:35 +00:00
|
|
|
const recentCommits: plugins.interfaces.Commit[] = [];
|
2024-12-13 17:31:09 +00:00
|
|
|
|
|
|
|
while (true) {
|
|
|
|
const url = new URL(`${this.baseUrl}/api/v1/repos/${owner}/${repo}/commits`);
|
2024-12-13 18:51:42 +00:00
|
|
|
url.searchParams.set('limit', '50');
|
2024-12-13 17:31:09 +00:00
|
|
|
url.searchParams.set('page', page.toString());
|
|
|
|
|
|
|
|
const resp = await fetch(url.href, {
|
2024-12-13 18:51:42 +00:00
|
|
|
headers: this.token ? { 'Authorization': `token ${this.token}` } : {},
|
2024-12-13 17:31:09 +00:00
|
|
|
});
|
2024-12-13 18:51:42 +00:00
|
|
|
|
2024-12-13 17:31:09 +00:00
|
|
|
if (!resp.ok) {
|
|
|
|
console.error(`Failed to fetch commits for ${owner}/${repo}: ${resp.status} ${resp.statusText} at ${url.href}`);
|
|
|
|
throw new Error(`Failed to fetch commits for ${owner}/${repo}: ${resp.statusText}`);
|
|
|
|
}
|
|
|
|
|
2024-12-13 23:30:35 +00:00
|
|
|
const data: plugins.interfaces.Commit[] = await resp.json();
|
2024-12-13 17:31:09 +00:00
|
|
|
if (data.length === 0) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const commit of data) {
|
|
|
|
const commitDate = new Date(commit.commit.author.date);
|
|
|
|
if (commitDate > twentyFourHoursAgo) {
|
|
|
|
recentCommits.push(commit);
|
|
|
|
} else {
|
|
|
|
return recentCommits;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
page++;
|
|
|
|
}
|
|
|
|
|
|
|
|
return recentCommits;
|
|
|
|
}
|
|
|
|
|
2024-12-13 18:51:42 +00:00
|
|
|
/**
|
|
|
|
* Fetch all commits by querying all organizations.
|
|
|
|
*/
|
2024-12-13 23:30:35 +00:00
|
|
|
public async fetchAllCommitsFromInstance(): Promise<plugins.interfaces.CommitResult[]> {
|
2024-12-13 18:51:42 +00:00
|
|
|
const orgs = await this.fetchAllOrganizations();
|
|
|
|
console.log(`Found ${orgs.length} organizations`);
|
2024-12-13 23:30:35 +00:00
|
|
|
let allCommits: plugins.interfaces.CommitResult[] = [];
|
2024-12-13 17:31:09 +00:00
|
|
|
|
2024-12-13 18:51:42 +00:00
|
|
|
for (const orgName of orgs) {
|
|
|
|
console.log(`Checking activity for organization: ${orgName}`);
|
2024-12-13 17:31:09 +00:00
|
|
|
|
|
|
|
try {
|
2024-12-13 18:51:42 +00:00
|
|
|
const hasActivity = await this.hasNewActivity({
|
|
|
|
orgName,
|
|
|
|
});
|
|
|
|
if (!hasActivity) {
|
|
|
|
console.log(`No new activity for organization: ${orgName}`);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
console.error(`Error fetching activity for organization ${orgName}:`, error.message);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
console.log(`New activity detected for organization: ${orgName}. Processing repositories...`);
|
|
|
|
|
|
|
|
const repos = await this.fetchAllRepositories();
|
|
|
|
for (const r of repos.filter((repo) => repo.owner.login === orgName)) {
|
|
|
|
try {
|
|
|
|
const hasActivity = await this.hasNewActivity({
|
|
|
|
orgName,
|
|
|
|
repoName: r.name,
|
|
|
|
});
|
|
|
|
if (!hasActivity) {
|
|
|
|
console.log(`No new activity for repository: ${orgName}/${r.name}`);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
console.error(`Error fetching activity for repository ${orgName}/${r.name}:`, error.message);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
const org = r.owner.login;
|
|
|
|
const repo = r.name;
|
|
|
|
console.log(`Processing repository ${org}/${repo}`);
|
|
|
|
|
|
|
|
try {
|
|
|
|
const taggedCommitShas = await this.fetchTags(org, repo);
|
|
|
|
const commits = await this.fetchRecentCommitsForRepo(org, repo);
|
|
|
|
|
2024-12-13 21:15:33 +00:00
|
|
|
const commitResults = commits.map((c) => {
|
2024-12-13 23:30:35 +00:00
|
|
|
const commit: plugins.interfaces.CommitResult = {
|
2024-12-13 21:15:33 +00:00
|
|
|
baseUrl: this.baseUrl,
|
|
|
|
org,
|
|
|
|
repo,
|
|
|
|
timestamp: c.commit.author.date,
|
|
|
|
prettyAgoTime: plugins.smarttime.getMilliSecondsAsHumanReadableAgoTime(new Date(c.commit.author.date).getTime()),
|
|
|
|
hash: c.sha,
|
|
|
|
commitMessage: c.commit.message,
|
|
|
|
tagged: taggedCommitShas.has(c.sha),
|
|
|
|
publishedOnNpm: false,
|
|
|
|
}
|
|
|
|
return commit;
|
|
|
|
});
|
2024-12-13 18:51:42 +00:00
|
|
|
|
|
|
|
if (commitResults.length > 0) {
|
|
|
|
try {
|
|
|
|
const packageInfo = await this.npmRegistry.getPackageInfo(`@${org}/${repo}`);
|
|
|
|
for (const commit of commitResults.filter((c) => c.tagged)) {
|
|
|
|
const correspondingVersion = packageInfo.allVersions.find((versionArg) => {
|
|
|
|
return versionArg.version === commit.commitMessage.replace('\n', '');
|
|
|
|
});
|
|
|
|
if (correspondingVersion) {
|
|
|
|
commit.publishedOnNpm = true;
|
|
|
|
}
|
2024-12-13 18:24:09 +00:00
|
|
|
}
|
2024-12-13 18:51:42 +00:00
|
|
|
} catch (error) {
|
|
|
|
console.error(`Failed to fetch package info for ${org}/${repo}:`, error.message);
|
2024-12-13 18:24:09 +00:00
|
|
|
}
|
|
|
|
}
|
2024-12-13 17:31:09 +00:00
|
|
|
|
2024-12-13 18:51:42 +00:00
|
|
|
allCommits.push(...commitResults);
|
|
|
|
} catch (error) {
|
|
|
|
console.error(`Skipping repository ${org}/${repo} due to error:`, error.message);
|
|
|
|
}
|
2024-12-13 17:31:09 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-12-13 18:51:42 +00:00
|
|
|
console.log(`Processed ${allCommits.length} commits in total.`);
|
2024-12-13 18:24:09 +00:00
|
|
|
for (const c of allCommits) {
|
|
|
|
console.log(`______________________________________________________
|
2024-12-13 21:16:47 +00:00
|
|
|
${c.prettyAgoTime} ago:
|
2024-12-13 18:24:09 +00:00
|
|
|
Commit ${c.hash} by ${c.org}/${c.repo} at ${c.timestamp}
|
|
|
|
${c.commitMessage}
|
|
|
|
Published on npm: ${c.publishedOnNpm}
|
|
|
|
`);
|
|
|
|
}
|
2024-12-13 17:31:09 +00:00
|
|
|
return allCommits;
|
|
|
|
}
|
|
|
|
}
|