Skip to content

Commit 0c032f0

Browse files
author
keeper
committed
fix(pipeline): keep the metadata of a repository with no commits
Issues, labels and milestones exist without a single commit, and losing them because nobody pushed is the silent kind of loss this tool exists to prevent. The bundle and its sidecar are the only things skipped.
1 parent f75ee94 commit 0c032f0

2 files changed

Lines changed: 34 additions & 21 deletions

File tree

SPEC.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -230,8 +230,9 @@ Every object is written create-only under object-lock retention.
230230
- `already backed up for this date` — the resume path found the artifacts already written.
231231
- `repository has no commits` — nothing to bundle. A repository created and never pushed to
232232
has no refs, and `git bundle create` refuses to write an empty bundle. Skipping it is what
233-
keeps one unused project in an organisation from failing every backup of it for ever;
234-
nothing is written because there is nothing to write, and nothing is lost.
233+
keeps one unused project in an organisation from failing every backup of it for ever.
234+
The **metadata is still stored**: a repository with no code can still carry issues, labels
235+
and milestones, so such an entry has a `meta` artifact and no `bundle` or `sha256`.
235236

236237
Neither case fails the run. Added as a field rather than as new `status` values, so a
237238
consumer switching on `status` is unaffected; the field is optional and absent on any repo

internal/pipeline/backup.go

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -271,26 +271,35 @@ func (r *backupRun) backupRepo(ctx context.Context, repo source.Repo, authHeader
271271
if err != nil {
272272
return fail(fmt.Errorf("check refs: %w", err))
273273
}
274+
275+
date := r.now().UTC().Format("2006-01-02")
276+
prefix := path.Join(repo.Host, repo.Owner, repo.Name, date)
277+
278+
// No commits, so no bundle — but the metadata is still stored below. A repository with no
279+
// code can still carry issues, labels and milestones, and dropping those because nobody
280+
// pushed a commit would be a silent loss of exactly the kind this tool exists to prevent.
274281
if !hasRefs {
275-
r.log.Info("repo skipped (no commits)", "repo", repo.Slug())
282+
r.log.Info("repo has no commits; storing metadata only", "repo", repo.Slug())
276283
entry.Status = StatusSkipped
277284
entry.Reason = ReasonEmpty
278-
return entry
279-
}
280-
281-
if err := r.git.BundleAll(ctx, mirror, bundlePath); err != nil {
282-
return fail(err)
285+
} else {
286+
if err := r.git.BundleAll(ctx, mirror, bundlePath); err != nil {
287+
return fail(err)
288+
}
283289
}
284290

285-
date := r.now().UTC().Format("2006-01-02")
286-
prefix := path.Join(repo.Host, repo.Owner, repo.Name, date)
287-
288291
// bundle (git data); the stored SHA covers the on-disk object (ciphertext if encrypted).
289-
bres, bundleSHA, err := r.putFile(ctx, path.Join(prefix, repo.Name+".bundle"), bundlePath, ret)
290-
if err != nil {
291-
return fail(err)
292+
// Skipped entirely for a repository with no commits: there is no bundle, and so no sha256
293+
// sidecar either, since that file describes the bundle.
294+
var bundleSHA string
295+
if hasRefs {
296+
bres, sha, err := r.putFile(ctx, path.Join(prefix, repo.Name+".bundle"), bundlePath, ret)
297+
if err != nil {
298+
return fail(err)
299+
}
300+
bundleSHA = sha
301+
entry.Artifacts = append(entry.Artifacts, artifact("bundle", bres, bundleSHA))
292302
}
293-
entry.Artifacts = append(entry.Artifacts, artifact("bundle", bres, bundleSHA))
294303

295304
// per-resource metadata
296305
meta, err := r.src.FetchMetadata(ctx, repo)
@@ -304,15 +313,18 @@ func (r *backupRun) backupRepo(ctx context.Context, repo source.Repo, authHeader
304313
entry.Artifacts = append(entry.Artifacts, artifact("meta", mres, metaSHA))
305314

306315
// sha256 sidecar (sha256sum format) over the stored bundle object
307-
shaLine := fmt.Sprintf("%s %s\n", bundleSHA, repo.Name+".bundle")
308-
sres, shaSHA, err := r.putBytes(ctx, path.Join(prefix, repo.Name+".sha256"), []byte(shaLine), ret)
309-
if err != nil {
310-
return fail(err)
316+
if hasRefs {
317+
shaLine := fmt.Sprintf("%s %s\n", bundleSHA, repo.Name+".bundle")
318+
sres, shaSHA, err := r.putBytes(ctx, path.Join(prefix, repo.Name+".sha256"), []byte(shaLine), ret)
319+
if err != nil {
320+
return fail(err)
321+
}
322+
entry.Artifacts = append(entry.Artifacts, artifact("sha256", sres, shaSHA))
311323
}
312-
entry.Artifacts = append(entry.Artifacts, artifact("sha256", sres, shaSHA))
313324

314325
// LFS objects (optional): fetch and store as a separate immutable tar artifact.
315-
if r.cfg.Backup.LFS && gitexec.LFSAvailable() {
326+
// Nothing to fetch without refs: LFS objects are pointed at by commits.
327+
if hasRefs && r.cfg.Backup.LFS && gitexec.LFSAvailable() {
316328
if err := r.git.LFSFetchAll(ctx, mirror, cloneURL, gitexec.Options{AuthHeader: authHeader}); err != nil {
317329
return fail(fmt.Errorf("lfs fetch: %w", err))
318330
}

0 commit comments

Comments
 (0)