Push-Historie aus den Gitea-Aktivitäten lesen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8d5de87ead
commit
e409c11d5c
2 files changed
+207
No files matched your search
@@ -0,0 +1,87 @@
|
|||||||
|
import { config } from '../config.js'
|
||||||
|
import { giteaFetch, splitRepo } from './giteaClient.js'
|
||||||
|
|
||||||
|
const DAY_MS = 24 * 60 * 60 * 1000
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Baut aus Gitea-1.25-Aktivitaeten-Feed-Eintraegen (`GET .../activities/feeds`) Push-Objekte
|
||||||
|
* im selben Zuschnitt wie der Push-Webhook (siehe `server/routes/webhook/gitea.js` und
|
||||||
|
* `createGitPushWorksheet`). Nur `op_type === 'commit_repo'`-Eintraege ab `since` mit
|
||||||
|
* mindestens einem Commit zaehlen. Der erste Push auf einen neuen Branch (Eintrag mit leerem
|
||||||
|
* `content`) und andere Aktivitaeten (z. B. `create_repo`) werden uebersprungen; ein nicht
|
||||||
|
* parsbarer `content` ebenso. Ergebnis aufsteigend nach `pushedAt` sortiert (aelteste
|
||||||
|
* zuerst), unabhaengig von der Reihenfolge im Feed.
|
||||||
|
*
|
||||||
|
* @param {object[]} feed Feed-Eintraege wie von Gitea geliefert.
|
||||||
|
* @param {{since: Date}} options
|
||||||
|
* @returns {Array<{branch: string, commitSha: string, pushedAt: Date,
|
||||||
|
* pusher: {login: string, username: string, full_name: string},
|
||||||
|
* commits: Array<{id: string, message: string, author: {name: string}}>}>}
|
||||||
|
*/
|
||||||
|
export function pushesFromFeed(feed, { since } = {}) {
|
||||||
|
const pushes = []
|
||||||
|
|
||||||
|
for (const entry of feed || []) {
|
||||||
|
if (entry?.op_type !== 'commit_repo') continue
|
||||||
|
if (!(new Date(entry.created) >= since)) continue
|
||||||
|
|
||||||
|
let parsed
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(entry.content)
|
||||||
|
} catch {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawCommits = parsed?.Commits
|
||||||
|
if (!Array.isArray(rawCommits) || rawCommits.length === 0) continue
|
||||||
|
|
||||||
|
const actUser = entry.act_user || {}
|
||||||
|
|
||||||
|
pushes.push({
|
||||||
|
branch: String(entry.ref_name || '').replace('refs/heads/', ''),
|
||||||
|
commitSha: parsed.HeadCommit?.Sha1 || rawCommits[0].Sha1,
|
||||||
|
pushedAt: new Date(entry.created),
|
||||||
|
pusher: {
|
||||||
|
login: actUser.login,
|
||||||
|
username: actUser.login,
|
||||||
|
full_name: actUser.full_name || '',
|
||||||
|
},
|
||||||
|
commits: rawCommits.map((c) => ({
|
||||||
|
id: c.Sha1,
|
||||||
|
message: c.Message,
|
||||||
|
author: { name: c.AuthorName },
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return pushes.sort((a, b) => a.pushedAt - b.pushedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liest die Push-Historie eines Repos der letzten `days` Tage aus dem Gitea-Aktivitaeten-Feed
|
||||||
|
* (`GET /repos/{owner}/{repo}/activities/feeds`, seitenweise mit 50 pro Seite, neueste zuerst).
|
||||||
|
* Blaettert nur so weit wie noetig: stoppt bei einer leeren Seite, einer Seite mit weniger als
|
||||||
|
* 50 Eintraegen, oder sobald der aelteste Eintrag einer Seite vor `since` liegt.
|
||||||
|
*
|
||||||
|
* @param {string} repoFullName "owner/repo"
|
||||||
|
* @param {{days?: number, now?: Date}} [options]
|
||||||
|
* @returns {Promise<ReturnType<typeof pushesFromFeed>>}
|
||||||
|
*/
|
||||||
|
export async function listRecentPushes(repoFullName, { days = config.gitea.backfillDays, now = new Date() } = {}) {
|
||||||
|
const [owner, repo] = splitRepo(repoFullName)
|
||||||
|
const since = new Date(now - days * DAY_MS)
|
||||||
|
const limit = 50
|
||||||
|
const collected = []
|
||||||
|
let page = 1
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const items = await giteaFetch(`/repos/${owner}/${repo}/activities/feeds?limit=${limit}&page=${page}`)
|
||||||
|
if (!Array.isArray(items) || items.length === 0) break
|
||||||
|
collected.push(...items)
|
||||||
|
if (items.length < limit) break
|
||||||
|
if (new Date(items[items.length - 1].created) < since) break
|
||||||
|
page += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return pushesFromFeed(collected, { since })
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import { test, before, after } from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { startFakeGitea } from './helpers/fakeGitea.js'
|
||||||
|
|
||||||
|
let fake
|
||||||
|
let pushesFromFeed
|
||||||
|
let listRecentPushes
|
||||||
|
|
||||||
|
const now = new Date('2026-09-27T12:00:00Z')
|
||||||
|
|
||||||
|
let nextFeedId = 1
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Baut einen Gitea-1.25-Feed-Eintrag vom Typ `commit_repo` nach: `{ op_type, act_user,
|
||||||
|
* ref_name, created, content }`, wobei `content` das JSON `{Commits:[...], HeadCommit:{...}}`
|
||||||
|
* als String enthaelt (siehe Task-Brief).
|
||||||
|
*
|
||||||
|
* Optionen: `sha` (ein Commit) oder `commits` (mehrere Shas, in Feed-Reihenfolge), `head`
|
||||||
|
* (HeadCommit-Sha; `null` fuer "kein HeadCommit"; Voreinstellung: erster Commit), `at`
|
||||||
|
* (created/Timestamp, ISO-String), `user` (act_user.login), `fullName` (act_user.full_name),
|
||||||
|
* `msg` (Commit-Message, sonst generisch).
|
||||||
|
*/
|
||||||
|
function pushEntry({ sha, commits, head, at, user, fullName, msg } = {}) {
|
||||||
|
const shas = commits || [sha ?? 'c0']
|
||||||
|
const login = user || 'JUSN'
|
||||||
|
const created = at || '2026-09-27T00:00:00Z'
|
||||||
|
|
||||||
|
const builtCommits = shas.map((s) => ({
|
||||||
|
Sha1: s,
|
||||||
|
Message: msg || `Commit ${s}`,
|
||||||
|
AuthorEmail: `${login.toLowerCase()}@example.com`,
|
||||||
|
AuthorName: login,
|
||||||
|
CommitterEmail: `${login.toLowerCase()}@example.com`,
|
||||||
|
CommitterName: login,
|
||||||
|
Timestamp: created,
|
||||||
|
}))
|
||||||
|
const headSha = head === undefined ? shas[0] : head
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: nextFeedId++,
|
||||||
|
op_type: 'commit_repo',
|
||||||
|
act_user: { login, full_name: fullName || '' },
|
||||||
|
ref_name: 'refs/heads/main',
|
||||||
|
created,
|
||||||
|
content: JSON.stringify({
|
||||||
|
Commits: builtCommits,
|
||||||
|
HeadCommit: headSha ? { Sha1: headSha } : null,
|
||||||
|
CompareURL: '',
|
||||||
|
Len: builtCommits.length,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Push-Feed-Eintrag `days` Tage vor `base`, ein Commit mit Sha `sha`. */
|
||||||
|
function daysAgo(base, days, sha) {
|
||||||
|
return pushEntry({ sha, at: new Date(base.getTime() - days * 864e5).toISOString() })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 120 Eintraege, 3 pro Tag ueber 40 Tage, neueste zuerst (fuer die Blaetter-Grenze). */
|
||||||
|
function manyPushes(base) {
|
||||||
|
const entries = []
|
||||||
|
for (let d = 0; d < 40; d++) {
|
||||||
|
for (let k = 0; k < 3; k++) {
|
||||||
|
const at = new Date(base.getTime() - d * 864e5 - (1 + 2 * k) * 36e5).toISOString()
|
||||||
|
entries.push(pushEntry({ sha: `d${d}k${k}`, at }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
fake = await startFakeGitea({
|
||||||
|
repos: [{ full_name: 'knso/viel' }],
|
||||||
|
feeds: { 'knso/viel': manyPushes(now) },
|
||||||
|
})
|
||||||
|
Object.assign(process.env, {
|
||||||
|
GITEA_BASE_URL: fake.baseUrl,
|
||||||
|
GITEA_API_TOKEN: 'test-token',
|
||||||
|
})
|
||||||
|
// config.js liest die Umgebung beim Import
|
||||||
|
;({ pushesFromFeed, listRecentPushes } = await import('../server/services/pushHistory.js'))
|
||||||
|
})
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
await fake.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('nur commit_repo mit Commits', () => {
|
||||||
|
const feed = [pushEntry({ sha: 'c1', at: '2026-09-27T14:46:34Z' }), { ...pushEntry({ at: '2026-09-27T14:46:33Z' }), content: '' }, { op_type: 'create_repo', created: '2026-09-27T14:02:46Z', act_user: { login: 'JUSN' } }]
|
||||||
|
assert.equal(pushesFromFeed(feed, { since: new Date('2026-09-01') }).length, 1)
|
||||||
|
})
|
||||||
|
test('Felder wie beim Webhook', () => {
|
||||||
|
const [p] = pushesFromFeed([pushEntry({ sha: 'c1', user: 'JUSN', at: '2026-09-27T14:46:34Z', msg: 'Döner Nation Website hinzufügen' })], { since: new Date('2026-09-01') })
|
||||||
|
assert.deepEqual(p, { branch: 'main', commitSha: 'c1', pushedAt: new Date('2026-09-27T14:46:34Z'),
|
||||||
|
pusher: { login: 'JUSN', username: 'JUSN', full_name: '' },
|
||||||
|
commits: [{ id: 'c1', message: 'Döner Nation Website hinzufügen', author: { name: 'JUSN' } }] })
|
||||||
|
})
|
||||||
|
test('HeadCommit bestimmt commitSha', () => {
|
||||||
|
const [p] = pushesFromFeed([pushEntry({ commits: ['c2', 'c1'], head: 'c2' })], { since: new Date(0) })
|
||||||
|
assert.equal(p.commitSha, 'c2'); assert.equal(p.commits.length, 2)
|
||||||
|
})
|
||||||
|
test('ohne HeadCommit gilt der erste Commit', () => {
|
||||||
|
const [p] = pushesFromFeed([pushEntry({ commits: ['c2', 'c1'], head: null })], { since: new Date(0) })
|
||||||
|
assert.equal(p.commitSha, 'c2')
|
||||||
|
})
|
||||||
|
test('Anzeigename wird übernommen', () => {
|
||||||
|
const [p] = pushesFromFeed([pushEntry({ user: 'knso', fullName: 'Kenso Grimm' })], { since: new Date(0) })
|
||||||
|
assert.equal(p.pusher.full_name, 'Kenso Grimm')
|
||||||
|
})
|
||||||
|
test('älteste zuerst und 30-Tage-Grenze', () => {
|
||||||
|
const now = new Date('2026-09-27T12:00:00Z')
|
||||||
|
const feed = [daysAgo(now, 1, 'n1'), daysAgo(now, 2, 'n2'), daysAgo(now, 31, 'alt')]
|
||||||
|
assert.deepEqual(pushesFromFeed(feed, { since: new Date(now - 30 * 864e5) }).map((p) => p.commitSha), ['n2', 'n1'])
|
||||||
|
})
|
||||||
|
test('listRecentPushes blättert nur so weit wie nötig', async () => {
|
||||||
|
// 120 Einträge, 3 pro Tag über 40 Tage, neueste zuerst; limit 50
|
||||||
|
const r = await listRecentPushes('knso/viel', { now })
|
||||||
|
assert.equal(r.length, 90)
|
||||||
|
assert.equal(fake.calls.filter((c) => c.path.includes('/activities/feeds')).length, 2)
|
||||||
|
})
|
||||||
Reference in new issue
Block a user