Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,48 @@

A Node.js/TypeScript CLI (`github-backup`) that backs up every repository you can reach through the GitHub CLI (`gh`): your personal repositories and those of all organizations you belong to, with wikis and Git LFS objects. Backups go to a local directory and/or any S3-compatible bucket (AWS S3, Cloudflare R2, Backblaze B2, MinIO) as git bundle snapshots, and a new snapshot is created only when refs change.

Status: under development. `github-backup list` works; `backup` is not implemented yet. See the plan (Turkish) in [plans/README.md](plans/README.md).
Status: under development. `list` and `backup` to local targets work; S3 targets and Git LFS are not implemented yet. See the plan (Turkish) in [plans/README.md](plans/README.md).

## Requirements

Node.js 22 or newer, `git`, and the GitHub CLI (`gh`) signed in with at least the `repo` and `read:org` scopes. Linux and macOS.

## Build

```sh
npm ci && npm run build
node dist/cli.js list
```

## Usage

```sh
github-backup list [--include <glob>] [--exclude <glob>] [--no-archived] [--no-forks] [--json]
github-backup backup --local ~/git-backup [--keep 7] [--concurrency 4] [--no-wiki] [--json]
```

`backup` mirrors every repository (and its wiki) into the cache directory (`~/.cache/github-backup-cli` by default), and writes a new snapshot to each target when the branches or tags changed since the last one. Mirrors only ever grow: branches deleted on GitHub stay in the backup. Each target keeps the newest `keep` snapshots per repository (default 7; a target's own `keep` wins over `--keep`).

`--target <name>` runs only the named targets from the config file; `--local <path>` adds a temporary `local` target. Without any target the command exits with code 2. Log lines go to stderr; `--json` prints a machine-readable summary to stdout.

Exit codes: `0` success, `1` at least one repository failed (the others are still backed up), `2` configuration or prerequisite error, `3` another run is in progress, `130` interrupted.

## Snapshot layout

```text
<root>/<source>/<owner>/<repo>/<stamp>.bundle
<root>/<source>/<owner>/<repo>/<stamp>.json # manifest, written last
<root>/<source>/<owner>/<repo>.wiki/... # wiki, same layout
```

`<stamp>` is the UTC time as `YYYYMMDDTHHMMSSZ`. A snapshot without its `.json` manifest is incomplete and is removed on the next run.

## Restoring

A bundle is a normal git repository history:

```sh
git clone ~/git-backup/github/acme/app/20260101T020000Z.bundle app
```

To get every branch and tag as local refs instead, use `git clone --mirror <bundle> app.git`.
2 changes: 1 addition & 1 deletion developer-todos.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Sıradaki fazı engelleyen işler.

### Faz 2 — Mirror, snapshot ve `local` hedefi

- [ ] 💻 Kendi repolarınla `backup --local` çalıştırıp bir bundle'ı `git clone` ile doğrula (private repo dahil): gerçek `gh` oturumu gerekir.
- [ ] 💻 `npm ci && npm run build && node dist/cli.js backup --local ~/git-backup-test --include '<kendi-kullanıcın>/*'` çalıştır, bir bundle'ı `git clone <bundle> /tmp/x` ile aç ve private repo dahil olduğunu kontrol et; komutu tekrar çalıştırınca özetin `unchanged` göstermesini doğrula: gerçek `gh` oturumu gerekir, bulutta yok.

### Faz 3 — `s3` hedefi

Expand Down
2 changes: 1 addition & 1 deletion plans/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ Fazlar tablodaki sırayla uygulanır; sıranın tek kaynağı bu tablodur. Her f
| Faz | Başlık | Durum | Dosya |
| --- | --- | --- | --- |
| 1 | İskelet, config ve `list` | Tamamlandı | [faz-01-iskelet-ve-list.md](faz-01-iskelet-ve-list.md) |
| 2 | Mirror, snapshot ve `local` hedefi | Bekliyor | [faz-02-mirror-ve-local.md](faz-02-mirror-ve-local.md) |
| 2 | Mirror, snapshot ve `local` hedefi | Tamamlandı | [faz-02-mirror-ve-local.md](faz-02-mirror-ve-local.md) |
| 3 | `s3` hedefi | Bekliyor | [faz-03-s3.md](faz-03-s3.md) |
| 4 | Git LFS | Bekliyor | [faz-04-lfs.md](faz-04-lfs.md) |
| 5 | Dokümantasyon ve son kontrol | Bekliyor | [faz-05-dokuman-ve-son-kontrol.md](faz-05-dokuman-ve-son-kontrol.md) |
Expand Down
39 changes: 27 additions & 12 deletions plans/faz-02-mirror-ve-local.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Faz 2 — Mirror, snapshot ve `local` hedefi

**Durum:** Bekliyor
**Durum:** Tamamlandı

## Amaç

Expand Down Expand Up @@ -30,19 +30,19 @@

## Görevler

- [ ] Test kaynağı (`test/` altında): verilen `file://` bare repolarını `RepoInfo` olarak döndüren sahte `Source`.
- [ ] `core/mirror.ts` + entegrasyon testi: ilk clone, yeni commit sonrası fetch, uzakta silinen branch'in mirror'da kalması, `refs/pull/*`'ın alınmaması.
- [ ] `core/snapshot.ts` + testler: aynı ref'ler → aynı özet; bundle'dan `git clone` ile geri yüklenen repoda tüm branch ve tag'ler.
- [ ] `targets/local.ts` + testler: yazma, `latestManifest`, listeleme, silme, yarım dosya temizliği.
- [ ] Saklama mantığı + birim testi.
- [ ] `core/lock.ts` + test.
- [ ] `core/run.ts` + entegrasyon testi: 3 repo, biri bozuk URL → diğer ikisi yedeklenir, çıkış 1, özet doğru; ikinci çalıştırmada değişiklik yoksa yeni snapshot yok (`unchanged`).
- [ ] Wiki akışı + test (wiki repo var / yok).
- [ ] `commands/backup.ts` bağlanır; `--json` özeti.
- [x] Test kaynağı (`test/helpers/git.ts`: `TestRemote`, `repoInfo`, `fakeSource`, `isolatedExec`): verilen `file://` bare repolarını `RepoInfo` olarak döndüren sahte `Source`.
- [x] `core/mirror.ts` + entegrasyon testi: ilk clone, yeni commit sonrası fetch, uzakta silinen branch'in mirror'da kalması, `refs/pull/*`'ın alınmaması.
- [x] `core/snapshot.ts` + testler: aynı ref'ler → aynı özet; bundle'dan `git clone` ile geri yüklenen repoda tüm branch ve tag'ler.
- [x] `targets/local.ts` + testler: yazma, `latestManifest`, listeleme, silme, yarım dosya temizliği.
- [x] Saklama mantığı + birim testi.
- [x] `core/lock.ts` + test.
- [x] `core/run.ts` + entegrasyon testi: 3 repo, biri bozuk URL → diğer ikisi yedeklenir, çıkış 1, özet doğru; ikinci çalıştırmada değişiklik yoksa yeni snapshot yok (`unchanged`).
- [x] Wiki akışı + test (wiki repo var / yok).
- [x] `commands/backup.ts` bağlanır; `--json` özeti.

## 👤 Kullanıcı aksiyonları

- [ ] 💻 Kendi makinende `node dist/cli.js backup --local ~/git-backup-test --include '<kendi-kullanıcın>/*'` çalıştır; bir bundle'ı `git clone <bundle> /tmp/x` ile açıp doğrula. Private bir repo da dahil olsun (gh kimlik doğrulamasının çalıştığını kanıtlar).
- [ ] 💻 Kendi makinende `node dist/cli.js backup --local ~/git-backup-test --include '<kendi-kullanıcın>/*'` çalıştır; bir bundle'ı `git clone <bundle> /tmp/x` ile açıp doğrula. Private bir repo da dahil olsun (gh kimlik doğrulamasının çalıştığını kanıtlar). İkinci çalıştırmada özetin `unchanged` göstermesini kontrol et.

## Kabul kriterleri

Expand All @@ -59,4 +59,19 @@

## Devam notları

_Henüz yok._
Kod tarafı bitti. Lint, typecheck, test (178) ve build temiz. Kabul kriterlerinin tamamı otomatik testlerle doğrulandı (`test/integration/backup.test.ts`, `mirror-snapshot.test.ts`, `local-target.test.ts`; `test/unit/lock|retention|async.test.ts`). Açık kalan tek madde gerçek `gh` oturumuyla elle deneme (yukarıdaki 💻; bulutta `gh` oturumu yok). Bu denemede `refs/pull/*` yüzünden beklenmedik bir şey çıkarsa `core/mirror.ts` refspec'lerine bak.

Faz 3 (s3) için:

- `Target` arayüzü `targets/types.ts`'te: `latestManifest`, `putSnapshot` (bundle/lfsTar yerel yol, manifest nesne; manifest en son yazılır), `listSnapshots` (yalnızca manifest'i olanlar, eskiden yeniye), `deleteSnapshot` (manifest önce), `cleanupIncomplete`. S3 uygulaması aynısını yapmalı; saklama mantığı `core/retention.ts` `applyRetention`'da ve hedeften bağımsız.
- `targets/index.ts` `createTarget`'ta `s3` dalı şimdilik `ConfigError("... s3 targets are not available yet")` fırlatır; Faz 3'te `createS3Target` ile değiştirilecek. `HeadBucket` ön kontrolü için `commands/backup.ts`'te `createTarget`'tan sonra, `checkGit` yanına (kilitten önce, çıkış 2) bir `checkAccess` adımı eklenmeli; arayüze isteğe bağlı `checkAccess?()` eklenebilir.
- `local` hedefinin `.partial` + `rename` mantığının S3 karşılığı: `Upload` tamamlanana kadar nesne görünmez; sıra aynı (bundle, lfs.tar, manifest). `cleanupIncomplete` S3'te yalnızca manifest'siz bundle/lfs.tar nesnelerini silmeli (tamamlanmamış multipart yüklemeler ayrıca `AbortMultipartUpload` ister).
- Test yardımcıları: `test/helpers/git.ts` (`TestRemote`, `fakeSource`, `isolatedExec`); `test/integration/backup.test.ts` içindeki `setup()` `--local` kullanıyor, S3 testleri aynı kalıbı config dosyası veya env ile kurabilir. Her `now()` çağrısı sahte saatte 1 sn ilerler; gerçek saatte aynı saniyede iki snapshot aynı `stamp`'i alırdı (repo başına çalıştırma başına tek snapshot olduğundan sorun değil).

Faz 4 (LFS) için:

- Takılacak yerler `core/run.ts` `processUnit`: `syncMirror`'dan sonra `git lfs fetch --all origin` (git zaten kaynağın `gitEnv()`'iyle çalışıyor); `createBundle` çağrısının yanında `lfs.tar` üretimi ve `SnapshotFiles.lfsTar` / `ManifestInput.lfsTarBytes` (ikisi de hazır). `BackupInput`'a `lfs: boolean` eklenmeli; `commands/backup.ts` `config.lfs`'i geçirmeli ve `checkGit` yanına `git lfs version` ön koşulunu eklemeli (çıkış 2, `--no-lfs` önerisi).
- `local` hedefi `.lfs.tar`'ı bundle ile birlikte yazar, siler ve manifest'siz bırakılanı temizler (testli); LFS tarafında yalnızca üretim kalıyor. Wiki repolarında LFS adımı atlanmalı (`unit.kind === "wiki"`).
- Bu ortamda `git-lfs` 3.4.1 kurulu; Faz 4 entegrasyon testi yerelde çalıştırılabilir.

Genel notlar: `--keep` yalnızca genel değeri belirler, hedefteki `keep` onu ezer (K-032). `exec`'in `signal` seçeneği Node'un `spawn({signal})`'ini kullanır; iptalde hata kodu `ABORT_ERR`'dir. Özet, repo ve wiki'yi ayrı birim sayar. Mirror dizini `<cacheDir>/<source>/<owner>/<repo>.git`, wiki `<repo>.wiki.git`; geçici bundle'lar `<cacheDir>/tmp/unit-*` altındadır ve her çalıştırma sonunda silinir.
4 changes: 2 additions & 2 deletions plans/faz-03-s3.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Snapshot'lar S3 uyumlu bir bucket'a (AWS S3, R2, B2, MinIO) `local` ile aynı d

## Kapsam

- `targets/s3.ts`: `@aws-sdk/client-s3` + `@aws-sdk/lib-storage` `Upload` (stream). Config: `bucket`, `prefix`, `endpoint`, `region` (varsayılan `us-east-1`), `forcePathStyle`, `accessKeyId`, `secretAccessKey`, `keep`.
- `targets/s3.ts` (`targets/index.ts`'teki yer tutucunun yerine; `Target` arayüzü `cleanupIncomplete` dahil): `@aws-sdk/client-s3` + `@aws-sdk/lib-storage` `Upload` (stream). Config: `bucket`, `prefix`, `endpoint`, `region` (varsayılan `us-east-1`), `forcePathStyle`, `accessKeyId`, `secretAccessKey`, `keep`.
- Kimlik önceliği: config → `GHBACKUP_S3_*` env → SDK varsayılan zinciri.
- `latestManifest`: `ListObjectsV2` (sayfalı) ile `<prefix>/<repoKey>/` altındaki `.json`'lar, en yenisi `GetObject`.
- Yükleme sırası: bundle, lfs.tar (varsa), manifest.
Expand Down Expand Up @@ -51,4 +51,4 @@ Snapshot'lar S3 uyumlu bir bucket'a (AWS S3, R2, B2, MinIO) `local` ile aynı d

## Devam notları

_Henüz yok._
_Henüz başlanmadı._ Faz 2'den devralınanlar için [faz-02-mirror-ve-local.md](faz-02-mirror-ve-local.md) **Devam notları**'na bak: `Target` arayüzü (`targets/types.ts`, `cleanupIncomplete` dahil), `createTarget`'taki `s3` yer tutucusu, `applyRetention`, `HeadBucket` ön kontrolünün takılacağı yer ve test yardımcıları.
2 changes: 1 addition & 1 deletion plans/faz-04-lfs.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,4 @@ LFS kullanan repoların LFS nesneleri de yedeklenir: her yeni snapshot'ın yanı

## Devam notları

_Henüz yok._
_Henüz başlanmadı._ Faz 2'den devralınanlar için [faz-02-mirror-ve-local.md](faz-02-mirror-ve-local.md) **Devam notları**'na bak: LFS adımının `core/run.ts` içinde takılacağı yer, `lfsTar`/`lfsTarBytes` için hazır alanlar ve ön koşulun `commands/backup.ts`'teki yeri.
3 changes: 3 additions & 0 deletions plans/kararlar.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,6 @@ Projenin güncel kararları. Bir karar değişirse satırı güncellenir ve tari
| K-029 | 2026-10-14 | Desteklenen OS: Linux ve macOS. | Hedef kullanım cron/launchd; Windows test edilmez. |
| K-030 | 2026-10-14 | Glob eşleşmesi `owner/repo` üzerindedir: `*` ve `?` `/` geçmez, `**` geçer; büyük/küçük harf duyarsız; boş `include` her şeyi kabul eder. | Beklenen `acme/*` davranışı; `*/*` her repoyu yakalar; ayrıntı mimari.md'de. |
| K-031 | 2026-10-14 | Config şeması katıdır (bilinmeyen alan hata verir) ve hedef adları benzersizdir. S3 anahtarı config'te yoksa `GHBACKUP_S3_ACCESS_KEY_ID`/`_SECRET_ACCESS_KEY` env'i kullanılır. | Yazım hatası sessizce yok sayılmasın; `--target` adla seçtiği için ad çakışmasın; K-010'un tek istisnası mimari.md'de belirtilen S3 anahtarı sırası. |
| K-032 | 2026-10-14 | `keep` çözümü: hedefte `keep` varsa o, yoksa genel `keep` (bayrak > env > dosya ile belirlenen). Hedefteki değer `--keep` bayrağını ezer. | Hedef başına saklama süresi config'te bilinçli verilir (ör. S3 için 14); tek `resolveKeep` fonksiyonu. |
| K-033 | 2026-10-14 | Mirror fetch'i `git fetch --no-tags --no-prune origin`; her fetch'ten sonra `HEAD`, `git ls-remote --symref` ile uzağın varsayılan branch'ine ayarlanır. | Tag'ler refspec'ten gelir, prune hiçbir koşulda yapılmaz (K-008); bundle'dan klonlanan repo checkout ile açılır. |
| K-034 | 2026-10-14 | Wiki ayrı birimdir (`<repo>.wiki` anahtarı, kendi mirror'ı ve snapshot'ları). Wiki'de "repository not found" `skipped: no wiki`; ref'siz mirror `skipped: empty`. | Wiki açık ama sayfasız repolar hata üretmesin; wiki yedeği ana repodan bağımsız saklanır. |
Loading
Loading