diff --git a/README.md b/README.md index 0694936..16f975e 100644 --- a/README.md +++ b/README.md @@ -4,295 +4,69 @@ **[English](./README.md)** · **[Русский](./README.ru.md)** ---- - **Like `.gitignore`, but for what goes public.** -Need the full rule-by-rule explanation? Read [Advanced configuration](./docs/ADVANCED.md) / [RU](./docs/ADVANCED.ru.md). - -You have a private repo. You want a public one — without the secrets. This -tool keeps them in sync. Automatically. - -## Project philosophy - -**Nothing to learn before it becomes useful.** Install it and run -`git-private2public` inside any Git repository: it immediately scans tracked -files and history with built-in rules. No `init` or config is required for protection. The safe behavior is the default; the -common path does not require memorizing flags, building pipelines, or learning -Git internals. - -The same tool remains flexible when a project grows: `.gitpublic` lets you tune -ignored files, replacements, allow-lists, branches, tags, hooks, CI output and -remote layout without replacing the simple workflow. - -In practice: - -- easy to start; -- safe by default; -- one obvious command per action; -- useful without configuration expertise; -- universal enough for advanced private → public workflows. - -## Quick start +Install it, run it inside a Git repository, and follow the prompts. ```bash pip install git-private2public -git-private2public # scans now; can enable pre-push protection +git-private2public ``` -That already works with no config. `git-private2public scan` does the same scan explicitly. - -Only publishing needs setup, because the tool must know where the public copy lives: +That is enough to scan tracked files and Git history for known credentials. No config is required. In an interactive terminal, the tool can also enable automatic checks before every `git push`. -```bash -git-private2public init -``` +## What do you want to do? -Edit `.gitpublic/config` — set source + target. Values can be `owner/repo`, a full Git URL, or a local path: - -``` -source = you/private-repo -target = you/public-repo -``` - -Edit `.gitpublic/ignore` — files to hide, one per line (like `.gitignore`): - -``` -.env -secrets/ -*.key -``` - -Publish: +### Check this repository ```bash -git-private2public publish +git-private2public ``` -Done. Your public repo is clean. +The tool scans the repository and explains what it found. Secret values are never printed in full. -## Auto-publish on every `git push` +### Publish a safe public copy ```bash -git-private2public hook enable # on -git push # also publishes public mirror -git-private2public hook disable # off -``` - -Native git hook. No CI, no GitHub Actions. Works offline. - -## Guard — pre-push safety net - -`guard` installs a lightweight pre-push hook that **blocks** `git push` if -scanned content matches known secret patterns. Unlike `hook` (which also -rewrites history and force-pushes a public mirror), `guard` is purely a -refusal mechanism — no clone, no filter-repo, no push. - -```bash -git-private2public guard enable # install pre-push hook -git-private2public guard status # is it on? -git-private2public guard disable # remove the hook -git-private2public guard run # manual scan (also what the hook does) +git-private2public init +git-private2public publish ``` -**What it scans — by default:** -1. Every tracked file in the working tree (`git ls-files`). -2. **Every blob in git history** — catches secrets committed in old commits - and later removed from HEAD but never rewritten via filter-repo. - Use `--no-history` to skip this (faster, but won't catch old leaks). +`init` guides you through the one-time setup. `publish` creates a cleaned public mirror without changing the private repository. -**Default secret patterns (always on, no `.gitpublic/` required):** -- OpenAI and Anthropic: `sk-...`, `sk-proj-...`, `sk-ant-...` -- GitHub: `ghp_...`, `github_pat_...`, `gho_...`, `ghs_...`, `ghr_...` -- HuggingFace and Slack: `hf_...`, `xox[baprs]-...` -- AWS long-term and temporary access key IDs: `AKIA...`, `ASIA...` -- Google API/OAuth keys and GitLab PATs -- Telegram bot, npm, PyPI, Stripe, SendGrid and Discord tokens -- Twilio, Mailgun and DigitalOcean API credentials -- Generic JWTs and PEM private-key headers - -**Custom patterns:** drop them into `.gitpublic/scan`, one per line (literal -or `regex:...`). They layer on top of the defaults. - -**Allowlist:** anything in `.gitpublic/allow` is treated as an exception for -broad regex rules that happen to match a public domain. - -**Bypass for one push (NOT recommended):** +### Something is not working ```bash -GIT_PRIVATE2PUBLIC_SKIP_GUARD=1 git push -``` - -**What guard tells you when it refuses the push:** - -The error message is tailored to where the secret lives: - -- **In the working tree** — points at the file, suggests editing, - `.gitignore` + `git rm --cached`, and reminds you to rotate a live - secret before committing. -- **In git history** — explains that editing HEAD isn't enough, and - prints the exact next step: - - `git-private2public publish` (if you have a `.gitpublic/` set up), or - - `git filter-repo --replace-text replacements.txt --force` (manual), - with the `replacements.txt` format. - -When both happen, you get both sections. - -If you only want guard and not auto-publish, the zero-config prompt or -`guard enable` is enough. Guard and auto-publish can coexist; an existing -user pre-push hook is preserved and chained automatically. - -### Why scan history? - -A secret leaked into a commit last month, removed from HEAD yesterday, but -the commit object still exists in `.git/objects/`. Pushing HEAD won't -expose it directly, but anyone who already has the repo and runs -`git log -p` will see it. Guard's history scan catches this. To actually -*remove* it from history, run `git-private2public publish` (which uses -filter-repo), or `git filter-repo --replace-text` manually. - -## The `.gitpublic/` folder - -Each file is one concern. Like `.gitignore` — one rule per line, `#` for -comments. If a file is missing, that setting is just empty. - -| File | What goes in it | Format | -|------|-----------------|--------| -| `config` | source, target, push settings | `key = value` | -| `ignore` | files to NOT publish | one path/glob per line | -| `replace` | find → replace in file contents | `old ==> new` per line | -| `scan` | refuse to push if matched | one pattern per line | -| `allow` | exceptions for domain rules in `scan` | one allowed matched domain per line | - -**Easy** — just edit `ignore`: - -``` -.env -secrets/ -*.key -``` - -**Medium** — also edit `replace`: - -``` - ==> 203.0.113.5 -real-token ==> *** -regex:[A-Fa-f0-9]{64} ==> *** -``` - -**Hard** — also edit `scan` + `allow`: - -``` -# scan: -regex:github_pat_[A-Za-z0-9_]{30,} -regex:192\.168\. -regex:[a-z0-9.-]+\.[a-z]{2,} - -# allow: -github.com -get.docker.com -``` - -## Commands - -``` -init create .gitpublic/ config -scan clean into a temp repo, scan, don't push -publish clean + push -hook enable / disable / status -guard enable / disable / status / run (pre-push secret scanner) -``` - -## How `allow` / domains work - -Nothing is auto-blocked just because it is a domain. - -`allow` is an exception list for `scan`. If `.gitpublic/scan` is missing or empty, `allow` does nothing and domains are not checked at all. - -To block domain-looking strings, add a broad domain rule to `.gitpublic/scan`: - -``` -regex:[a-z0-9.-]+\.[a-z]{2,} -``` - -Now every matched domain fails the scan unless the matched domain itself is listed in `.gitpublic/allow`: - -``` -github.com -get.docker.com -example.com -``` - -`allow` does not replace private domains. Use `.gitpublic/replace` for that: - -``` -private.company.local ==> example.com -regex:.*\.corp\.internal ==> example.com +git-private2public doctor ``` -Analogy: `scan` says “ban everything matching this pattern”, `allow` says “except these exact public domains”. - -Rule of thumb: +`doctor` checks Git, configuration, hooks and access to both repositories, then tells you what to fix. -| You want to... | File | -|---|---| -| remove files | `.gitpublic/ignore` | -| rewrite private text/domain/IP | `.gitpublic/replace` | -| fail if a secret/domain/IP survived | `.gitpublic/scan` | -| make exceptions for public domains caught by scan | `.gitpublic/allow` | +## Simple by default -More examples: [Advanced configuration](./docs/ADVANCED.md) / [RU](./docs/ADVANCED.ru.md). +- no setup is needed for scanning; +- safe credential rules are built in; +- one clear command performs one clear action; +- interactive sessions explain the next step; +- advanced settings remain available when needed. -## Install +Most users only need: -```bash -pip install git-private2public +```text +git-private2public scan now +git-private2public init prepare private → public publishing +git-private2public publish publish the clean copy +git-private2public doctor diagnose a problem ``` -That's it. Now you have the `git-private2public` command. +Everything else—custom rules, replacements, CI JSON, hooks, branches, tags, YAML compatibility and implementation details—is documented separately: -> No pip? [Single-file manual install](./git_private2public.py) — download + -> `chmod +x` (needs `pip install git-filter-repo pyyaml`). +**[Advanced configuration](./docs/ADVANCED.md)** -## Why +## Safety -Git has no "private file in a public repo". So you need two repos. This keeps -them in sync — without leaking. - -| | delete files | replace text | scan | auto push | pre-push guard | -|---|:---:|:---:|:---:|:---:|:---:| -| git-filter-repo | ✅ | ✅ | ❌ | ❌ | ❌ | -| BFG | ✅ | ✅ | ❌ | ❌ | ❌ | -| dupligit | ❌ | ❌ | ❌ | ✅ | ❌ | -| gitleaks / trufflehog | ❌ | ❌ | ✅ | ❌ | ✅ (separate tool) | -| **git-private2public** | ✅ | ✅ | ✅ | ✅ | ✅ (built-in) | +Publishing works in a temporary clone. The private repository is not rewritten. Concurrent public updates are protected with `--force-with-lease`, pushed commits are verified, and existing tags are never force-overwritten. ## License MIT - -### Diagnose setup - -```bash -git-private2public doctor -``` - -`doctor` checks Git, `git-filter-repo`, repository/config health, secret-rule files, -the pre-push hook, and source/target remote access. Secret findings are always -redacted and shown as type + location + safe prefix/suffix hint + length + short SHA-256 fingerprint; the -matched credential itself is never printed. - -### Safe publication transactions - -`publish` records the current target branch SHA before sanitizing and uses an -explicit `--force-with-lease` tied to that SHA. If somebody updates the public -branch while the private history is being cleaned, publication stops instead -of overwriting their work. After a successful push, the remote SHA is verified. -Targets under 100 MiB are cloned and scanned again automatically; larger repos -skip the extra clone but still receive SHA verification. Existing tags are -never force-overwritten. - -For CI, `scan`, `publish --scan`, and `guard run` support `--format json`. - -The managed pre-push dispatcher can run both guard and auto-publish while -preserving and chaining an existing user hook. diff --git a/README.ru.md b/README.ru.md index 8434326..7e113b4 100644 --- a/README.ru.md +++ b/README.ru.md @@ -4,297 +4,69 @@ **[English](./README.md)** · **[Русский](./README.ru.md)** ---- +**Как `.gitignore`, только для того, что можно публиковать.** -**Как `.gitignore`, только для публичности.** - -Нужно полное объяснение всех правил? См. [расширенная настройка](./docs/ADVANCED.md) / [RU](./docs/ADVANCED.ru.md). - -У тебя приватный репо. Нужен публичный — без секретов. Эта тулза держит их в -синке. Автоматически. - -## Философия проекта - -**Чтобы получить пользу, не нужно сначала чему-то учиться.** Установил и запустил -`git-private2public` внутри любого Git-репозитория: сразу проверяются отслеживаемые -файлы и история по встроенным правилам. Для защиты не нужны ни `init`, ни конфиг. -Безопасное поведение включено по умолчанию: обычный сценарий не требует помнить -кучу параметров, собирать конвейер или разбираться во внутренностях Git. - -Когда проект становится сложнее, тот же инструмент остаётся гибким: через -`.gitpublic` можно настроить исключения, замены, allow-list, ветки, теги, hooks, -CI-отчёты и схему remotes, не ломая простой основной сценарий. - -На практике это означает: - -- легко начать; -- безопасно по умолчанию; -- одна очевидная команда на одно действие; -- полезно без экспертной настройки; -- достаточно универсально для сложных private → public процессов. - -## Быстрый старт +Установи, запусти внутри Git-репозитория и следуй подсказкам. ```bash pip install git-private2public -git-private2public # сканирует и предложит включить pre-push guard +git-private2public ``` -Это уже работает без настройки. `git-private2public scan` явно запускает ту же проверку. - -Настройка нужна только для публикации: программе надо один раз сказать, куда класть публичную копию. +Этого достаточно, чтобы проверить отслеживаемые файлы и всю Git-историю на известные учётные данные. Конфиг не нужен. В интерактивном терминале программа также предложит включить автоматическую проверку перед каждым `git push`. -```bash -git-private2public init -``` +## Что ты хочешь сделать? -Отредактируй `.gitpublic/config` — source и target. Значения могут быть `owner/repo`, полный Git URL или локальный путь: - -``` -source = you/private-repo -target = you/public-repo -``` - -Отредактируй `.gitpublic/ignore` — что прятать, по строке (как `.gitignore`): - -``` -.env -secrets/ -*.key -``` - -Опубликуй: +### Проверить этот репозиторий ```bash -git-private2public publish +git-private2public ``` -Готово. Публичный репо чистый. +Программа сама проверит репозиторий и объяснит, что нашла. Полное значение секрета никогда не выводится. -## Авто-публикация при каждом `git push` +### Опубликовать безопасную публичную копию ```bash -git-private2public hook enable # вкл -git push # также публикует публичный mirror -git-private2public hook disable # выкл -``` - -Нативный git-хук. Без CI, без GitHub Actions. Работает офлайн. - -## Guard — предохранитель на pre-push - -`guard` ставит лёгкий pre-push хук, который **блокирует** `git push`, если в -сканируемом контенте матчатся известные паттерны секретов. В отличие от -`hook` (который ещё и переписывает историю и force-пушит публичное -зеркало), `guard` — чисто отказ: без clone, без filter-repo, без push. - -```bash -git-private2public guard enable # поставить pre-push хук -git-private2public guard status # стоит? -git-private2public guard disable # снять -git-private2public guard run # ручное сканирование (это же делает хук) +git-private2public init +git-private2public publish ``` -**Что сканируется по умолчанию:** -1. Каждый отслеживаемый файл в рабочем дереве (`git ls-files`). -2. **Каждый blob в git-истории** — ловит секреты, закоммиченные в старых - коммитах, а потом удалённые из HEAD, но не вытертые через filter-repo. - Чтобы пропустить этот шаг (быстрее, но не ловит старые утечки): - `git-private2public guard run --no-history`. +`init` проведёт через одноразовую настройку. `publish` создаст очищенное публичное зеркало, не изменяя приватный репозиторий. -**Дефолтные паттерны секретов (всегда включены, `.gitpublic/` не нужен):** -- OpenAI и Anthropic: `sk-...`, `sk-proj-...`, `sk-ant-...` -- GitHub: `ghp_...`, `github_pat_...`, `gho_...`, `ghs_...`, `ghr_...` -- HuggingFace и Slack: `hf_...`, `xox[baprs]-...` -- постоянные и временные ключи AWS: `AKIA...`, `ASIA...` -- Google API/OAuth и GitLab PAT -- токены Telegram Bot, npm, PyPI, Stripe, SendGrid и Discord -- учётные данные Twilio, Mailgun и DigitalOcean -- универсальные JWT и заголовки приватных PEM-ключей - -**Свои паттерны** — кладёшь в `.gitpublic/scan`, по строке (литерал или -`regex:...`). Они дополняют дефолтные. - -**Allowlist** — всё, что в `.gitpublic/allow`, считается исключением для -широких regex-правил, которые могут зацепить публичный домен. - -**Одноразовый bypass (не рекомендуется):** +### Что-то не работает ```bash -GIT_PRIVATE2PUBLIC_SKIP_GUARD=1 git push -``` - -**Что guard пишет, когда блокирует push:** - -Сообщение подстраивается под то, где сидит секрет: - -- **В рабочем дереве** — указывает на файл, советует отредактировать - или добавить в `.gitignore` + `git rm --cached`, напоминает ротировать - живой секрет **до** коммита. -- **В git-истории** — объясняет, что правки HEAD недостаточно, и печатает - точную следующую команду: - - `git-private2public publish` (если `.gitpublic/` настроен), или - - `git filter-repo --replace-text replacements.txt --force` руками, - с форматом `replacements.txt`. - -Если нарушения и там и там — печатает обе секции. - -Если нужен только `guard` без автопубликации, достаточно согласиться с -zero-config предложением или выполнить `guard enable`. Guard и -автопубликация могут работать вместе, а существующий пользовательский -pre-push hook сохраняется и вызывается автоматически. - -### Зачем сканировать историю? - -Секрет ушёл в коммит месяц назад, потом его убрали из HEAD — но объект -коммита всё ещё лежит в `.git/objects/`. Push HEAD его не отдаст напрямую, -но любой, у кого уже есть репо, увидит его через `git log -p`. История- -скан в guard это ловит. Чтобы **реально стереть** из истории — запусти -`git-private2public publish` (использует filter-repo) или -`git filter-repo --replace-text` руками. - -## Папка `.gitpublic/` - -Каждый файл — одна забота. Как `.gitignore` — одно правило на строку, `#` для -комментариев. Если файла нет — настройки просто нет. - -| Файл | Что внутри | Формат | -|------|------------|--------| -| `config` | source, target, push | `key = value` | -| `ignore` | что НЕ публиковать | путь/маска на строку | -| `replace` | найти → заменить в файлах | `old ==> new` на строку | -| `scan` | отказаться пушить если найдёт | паттерн на строку | -| `allow` | исключения для правил `scan`, которые ловят домены | по одному домену-исключению в строке | - -**Простой** — редактируй только `ignore`: - -``` -.env -secrets/ -*.key -``` - -**Средний** — ещё `replace`: - -``` - ==> 203.0.113.5 -real-token ==> *** -regex:[A-Fa-f0-9]{64} ==> *** -``` - -**Сложный** — ещё `scan` + `allow`: - -``` -# scan: -regex:github_pat_[A-Za-z0-9_]{30,} -regex:192\.168\. -regex:[a-z0-9.-]+\.[a-z]{2,} - -# allow: -github.com -get.docker.com -``` - -## Команды - -``` -init создать .gitpublic/ конфиг -scan почистить во временный репозиторий, проверить, не пушить -publish вычистить + запушить -hook enable / disable / status -guard enable / disable / status / run (pre-push сканер секретов) -``` - -## Как работает `allow` / domains - -Ничего не автоблокируется просто потому, что это домен. - -`allow` — это список исключений для `scan`. Если `.gitpublic/scan` отсутствует или пустой, `allow` ничего не делает и домены вообще не проверяются. - -Чтобы блокировать похожие на домены строки, добавь широкое regex-правило в `.gitpublic/scan`: - -``` -regex:[a-z0-9.-]+\.[a-z]{2,} -``` - -Теперь любой найденный домен валит проверку, кроме случаев, когда сам найденный домен перечислен в `.gitpublic/allow`: - -``` -github.com -get.docker.com -example.com -``` - -`allow` не заменяет приватные домены. Для замены используй `.gitpublic/replace`: - -``` -private.company.local ==> example.com -regex:.*\.corp\.internal ==> example.com +git-private2public doctor ``` -Аналогия: `scan` говорит: “запретить всё, что совпало с этим шаблоном”. `allow` говорит: “но эти конкретные домены — исключения, их пропустить”. - -Коротко: +`doctor` проверит Git, конфигурацию, hooks и доступ к обоим репозиториям, а затем скажет, что исправить. -| Что нужно | Файл | -|---|---| -| удалить файлы | `.gitpublic/ignore` | -| переписать приватный текст/домен/IP | `.gitpublic/replace` | -| упасть, если секрет/домен/IP выжил | `.gitpublic/scan` | -| сделать исключения для публичных доменов, найденных проверкой | `.gitpublic/allow` | +## Просто по умолчанию -Больше примеров: [расширенная настройка](./docs/ADVANCED.md) / [RU](./docs/ADVANCED.ru.md). +- для сканирования ничего настраивать не нужно; +- безопасные правила уже встроены; +- одна понятная команда делает одно понятное действие; +- интерактивный режим сам объясняет следующий шаг; +- сложные настройки доступны только тем, кому они нужны. -## Установка +Большинству пользователей достаточно: -```bash -pip install git-private2public +```text +git-private2public проверить сейчас +git-private2public init настроить private → public +git-private2public publish опубликовать чистую копию +git-private2public doctor найти проблему ``` -Готово. Теперь есть команда `git-private2public`. +Всё остальное — свои правила, замены, JSON для CI, hooks, ветки, теги, YAML и технические детали — вынесено отдельно: -> Без pip? [Ручная установка одного файла](./git_private2public.py) — скачать + -> `chmod +x` (нужно `pip install git-filter-repo pyyaml`). +**[Расширенная настройка](./docs/ADVANCED.ru.md)** -## Зачем +## Безопасность -В Git нет «приватного файла в публичном репо». Поэтому нужно два репо. Эта -тулза держит их в синке — без утечек. - -| | удалить файлы | заменить текст | скан | авто пуш | pre-push guard | -|---|:---:|:---:|:---:|:---:|:---:| -| git-filter-repo | ✅ | ✅ | ❌ | ❌ | ❌ | -| BFG | ✅ | ✅ | ❌ | ❌ | ❌ | -| dupligit | ❌ | ❌ | ❌ | ✅ | ❌ | -| gitleaks / trufflehog | ❌ | ❌ | ✅ | ❌ | ✅ (отдельная тулза) | -| **git-private2public** | ✅ | ✅ | ✅ | ✅ | ✅ (встроен) | +Публикация выполняется во временном clone. Приватный репозиторий не переписывается. Конкурентные изменения публичной ветки защищены через `--force-with-lease`, отправленный commit проверяется, а существующие теги никогда не перезаписываются принудительно. ## Лицензия MIT - -### Диагностика установки - -```bash -git-private2public doctor -``` - -`doctor` проверяет Git, `git-filter-repo`, репозиторий и конфигурацию, локальные -файлы правил, pre-push hook и доступ к source/target. Найденные секреты всегда -редактируются: выводятся только тип, расположение, безопасная подсказка из префикса/последних 4 символов, длина и короткий SHA-256, -само значение учётных данных в отчёт не попадает. - -### Безопасная транзакция публикации - -`publish` запоминает SHA целевой ветки до очистки и использует точный -`--force-with-lease`. Если во время обработки кто-то обновил публичную ветку, -публикация остановится и не затрёт чужие изменения. После push проверяется SHA -на сервере. Репозитории меньше 100 МиБ автоматически клонируются и сканируются -повторно; для больших выполняется проверка SHA без дополнительного клона. -Существующие теги никогда не перезаписываются принудительно. - -Для CI команды `scan`, `publish --scan` и `guard run` поддерживают -`--format json`. - -Управляемый pre-push dispatcher умеет одновременно запускать guard и -автопубликацию, сохраняя и вызывая уже существующий пользовательский hook. diff --git a/docs/ADVANCED.md b/docs/ADVANCED.md index d60b282..af44490 100644 --- a/docs/ADVANCED.md +++ b/docs/ADVANCED.md @@ -16,14 +16,14 @@ private repo -> push to public repo (.gitpublic/config) ``` -Missing files mean “no rules for that step”. They do not enable hidden defaults. +Missing custom files mean “no custom rules for that step”. Built-in credential detection remains active for zero-config scans and guard mode. | File | If present | If missing or empty | |------|------------|---------------------| | `.gitpublic/config` | Defines source, target, push settings | `source`/`target` are required, so publish cannot run | | `.gitpublic/ignore` | Removes matching files/paths from all published history | No files are removed | | `.gitpublic/replace` | Rewrites matching text in remaining files | No text is rewritten | -| `.gitpublic/scan` | Fails if any scan pattern is still present after cleanup | Nothing is blocked by scan | +| `.gitpublic/scan` | Adds custom blocking patterns | Built-in credential rules still protect zero-config scans and guard mode | | `.gitpublic/allow` | Exceptions for domain-like matches found by `scan` | No scan exceptions exist | | `.gitpublic/domains` | Same as `allow` alias | Nothing is allowlisted | @@ -100,7 +100,7 @@ regex:.*\.corp\.internal ==> example.com ## `.gitpublic/scan` -`scan` is the only thing that blocks a publish after cleanup. +`.gitpublic/scan` adds custom blocking rules. Built-in credential rules are always available for zero-config scans and guard mode. One rule per line: @@ -118,7 +118,7 @@ Forms: | `literal text` | fail if exact text remains | | `regex:pattern` | fail if regex matches | -If `.gitpublic/scan` does not exist or has no active rules, nothing is blocked at this step. +If `.gitpublic/scan` is missing, no custom rules are added. Built-in credential detection still works in zero-config scans and guard mode. ## Domains and `.gitpublic/allow` @@ -214,7 +214,7 @@ Result: | Refuse publish if value survived | `scan` | | Make an exception for a public domain matched by broad domain scan | `allow` | | Auto-block every domain | add a broad domain regex to `scan` | -| Auto-block secrets without rules | not supported; write `scan` rules or use external scanners before publishing | +| Auto-block known credential formats | built in; no custom rule required | ## Hook mode @@ -228,7 +228,7 @@ Installs a Git `pre-push` hook in the current private repo. On every `git push`, git-private2public publish -c .gitpublic ``` -If a non-`git-private2public` hook already exists, it refuses to overwrite it. +Existing user hooks are preserved and chained. Guard and auto-publish can coexist in one managed dispatcher. ## YAML compatibility diff --git a/docs/ADVANCED.ru.md b/docs/ADVANCED.ru.md index af32adb..bfb7b11 100644 --- a/docs/ADVANCED.ru.md +++ b/docs/ADVANCED.ru.md @@ -16,14 +16,14 @@ -> push в публичный репозиторий (.gitpublic/config) ``` -Если какого-то файла нет, это значит “нет правил для этого шага”. Скрытых автоблокировок нет. +Если какого-то файла нет, это значит “нет пользовательских правил для этого шага”. Встроенная проверка известных учётных данных всё равно работает в zero-config и guard-режимах. | Файл | Если есть | Если файла нет или он пустой | |------|-----------|-------------------------------| | `.gitpublic/config` | задаёт source, target и push-настройки | `source`/`target` обязательны, publish не запустится | | `.gitpublic/ignore` | удаляет совпавшие файлы/пути из публичной истории | файлы не удаляются | | `.gitpublic/replace` | заменяет совпавший текст в оставшихся файлах | текст не заменяется | -| `.gitpublic/scan` | валит publish, если после чистки остался совпавший шаблоном | ничего не блокируется на шаге проверки | +| `.gitpublic/scan` | добавляет пользовательские блокирующие правила | встроенные правила продолжают работать в zero-config и guard-режимах | | `.gitpublic/allow` | исключения для доменов, найденных проверкой `scan` | исключений для проверки нет | | `.gitpublic/domains` | alias для `allow` | ничего не разрешается | @@ -100,7 +100,7 @@ regex:.*\.corp\.internal ==> example.com ## `.gitpublic/scan` -`scan` — единственный шаг, который блокирует publish после чистки. +`.gitpublic/scan` добавляет свои блокирующие правила. Встроенные правила известных секретов доступны без настройки в zero-config и guard-режимах. По одному правилу на строку: @@ -118,7 +118,7 @@ regex:[a-z0-9.-]+\.[a-z]{2,} | `literal text` | ошибка, если exact text остался | | `regex:шаблоном` | ошибка, если regex совпал | -Если `.gitpublic/scan` отсутствует или в нём нет активных правил, на этом шаге ничего не блокируется. +Если `.gitpublic/scan` отсутствует, пользовательские правила не добавляются. Встроенная проверка всё равно работает в zero-config и guard-режимах. ## Домены и `.gitpublic/allow` @@ -214,7 +214,7 @@ example.com | упасть, если значение пережило чистку | `scan` | | сделать исключение для публичного домена, найденного широким правилом проверки доменов | `allow` | | блокировать все похожие на домены строки | добавь широкое regex-правило для доменов в `scan` | -| блокировать секреты без правил | не поддерживается; напиши правила `scan` или запускай внешнюю проверку до publish | +| блокировать известные форматы секретов | встроено, свои правила не обязательны | ## Режим Git hook @@ -228,7 +228,7 @@ git-private2public hook enable git-private2public publish -c .gitpublic ``` -Если уже есть чужой `pre-push` hook, тулза не перезаписывает его. +Существующий пользовательский hook сохраняется и вызывается по цепочке. Guard и автопубликация могут работать одновременно. ## Совместимость с YAML diff --git a/git_private2public.py b/git_private2public.py index d1b26d6..041e899 100644 --- a/git_private2public.py +++ b/git_private2public.py @@ -1296,6 +1296,27 @@ def cmd_doctor(args) -> int: return 1 if failed else 0 +def _detect_source_remote() -> str: + repo = find_git_root(Path.cwd()) + if not repo: + return str(Path.cwd()) + res = subprocess.run(["git", "-C", str(repo), "remote", "get-url", "origin"], capture_output=True, text=True) + return res.stdout.strip() if res.returncode == 0 and res.stdout.strip() else str(repo) + + +def _interactive_init_values() -> tuple[str, str] | None: + if not (sys.stdin.isatty() and sys.stdout.isatty()): + return None + detected = _detect_source_remote() + print("Setting up private → public publishing. Press Enter to accept a suggestion.") + source = input(f"Private source [{mask_url(detected)}]: ").strip() or detected + target = input("Public target (owner/repo, Git URL, or local path): ").strip() + if not target: + print("No public target entered. Config was created with a placeholder; edit .gitpublic/config later.") + target = "you/public-repo" + return source, target + + def cmd_init(args) -> int: # Folder mode: .gitpublic/ with one file per concern (like .gitignore) folder = Path(args.path) @@ -1313,13 +1334,15 @@ def cmd_init(args) -> int: folder.mkdir(parents=True, exist_ok=True) for name, content in GITPUBLIC_FILES.items(): (folder / name).write_text(content) - print(f"✓ Created {folder}/ with:") - for name in GITPUBLIC_FILES: - print(f" {name}") - print() - print(f" Edit {folder}/config — set source + target") - print(f" Edit {folder}/ignore — files to hide (like .gitignore)") - print(f" Run: git-private2public publish") + values = _interactive_init_values() + if values: + source, target = values + config_text = GITPUBLIC_FILES["config"] + config_text = config_text.replace("you/private-repo", source, 1).replace("you/public-repo", target, 1) + (folder / "config").write_text(config_text) + print(f"✓ Setup created in {folder}/") + print("Next: git-private2public publish") + print("Optional: edit .gitpublic/ignore like .gitignore to hide extra files.") return 0 @@ -1386,32 +1409,32 @@ def cmd_scan(args) -> int: def main() -> int: p = argparse.ArgumentParser( prog="git-private2public", - description="Like .gitignore, but for what goes public. Folder-based config.", + description="Scan now, or publish a safe public copy. Run without arguments to start.", ) p.add_argument("--version", action="version", version=f"%(prog)s {__version__}") sub = p.add_subparsers(dest="cmd") - p_init = sub.add_parser("init", help="write an example config") + p_init = sub.add_parser("init", help="set up private → public publishing") p_init.add_argument("path", nargs="?", default=".gitpublic") p_init.add_argument("--force", action="store_true") p_init.set_defaults(func=cmd_init) - p_doctor = sub.add_parser("doctor", help="diagnose tools, config, hooks and remote access") + p_doctor = sub.add_parser("doctor", help="find setup or access problems") p_doctor.add_argument("-c", "--config", default=".gitpublic") p_doctor.set_defaults(func=cmd_doctor) - p_pub = sub.add_parser("publish", help="sanitize + push to target") + p_pub = sub.add_parser("publish", help="publish the cleaned public copy") p_pub.add_argument("-c", "--config", default=".gitpublic") p_pub.add_argument("--scan", action="store_true", help="scan only, don't push") p_pub.add_argument("--format", choices=("text", "json"), default="text", help="report format") p_pub.set_defaults(func=cmd_publish) - p_scan = sub.add_parser("scan", help="scan only (no push)") + p_scan = sub.add_parser("scan", help="check the repository without publishing") p_scan.add_argument("-c", "--config", default=".gitpublic") p_scan.add_argument("--format", choices=("text", "json"), default="text", help="report format") p_scan.set_defaults(func=cmd_scan) - p_hook = sub.add_parser("hook", help="enable/disable a local git pre-push hook") + p_hook = sub.add_parser("hook", help="advanced: auto-publish on git push") p_hook_sub = p_hook.add_subparsers(dest="action", required=True) p_hook_sub.add_parser("enable", help="install the pre-push hook (auto-publish on every git push)") p_hook_sub.add_parser("disable", help="remove the hook") @@ -1420,7 +1443,7 @@ def main() -> int: p_hook.set_defaults(func=cmd_hook) p_guard = sub.add_parser( - "guard", help="pre-push safety net: scan for secrets, refuse push if found" + "guard", help="advanced: manage automatic pre-push checks" ) p_guard_sub = p_guard.add_subparsers(dest="action", required=True) p_guard_sub.add_parser("enable", help="install the guard pre-push hook") diff --git a/tests/test_git_private2public.py b/tests/test_git_private2public.py index 4c721b4..69d853e 100644 --- a/tests/test_git_private2public.py +++ b/tests/test_git_private2public.py @@ -583,3 +583,35 @@ def test_interactive_default_can_enable_guard(tmp_path: Path, monkeypatch): hook = repo / ".git" / "hooks" / "pre-push" assert hook.exists() assert "guard run" in hook.read_text() + + +def test_interactive_init_uses_origin_and_asks_only_for_public_target(tmp_path: Path, monkeypatch): + repo = tmp_path / "repo" + repo.mkdir() + subprocess.run(["git", "init", "-q", str(repo)], check=True) + subprocess.run(["git", "-C", str(repo), "remote", "add", "origin", "git@github.com:alice/private.git"], check=True) + monkeypatch.chdir(repo) + monkeypatch.setattr(g.sys.stdin, "isatty", lambda: True) + monkeypatch.setattr(g.sys.stdout, "isatty", lambda: True) + answers = iter(["", "alice/public"]) + monkeypatch.setattr("builtins.input", lambda prompt: next(answers)) + + rc = g.cmd_init(argparse.Namespace(path=".gitpublic", force=False)) + config = (repo / ".gitpublic" / "config").read_text() + + assert rc == 0 + assert "source = git@github.com:alice/private.git" in config + assert "target = alice/public" in config + + +def test_noninteractive_init_remains_deterministic(tmp_path: Path, monkeypatch): + monkeypatch.chdir(tmp_path) + monkeypatch.setattr(g.sys.stdin, "isatty", lambda: False) + monkeypatch.setattr(g.sys.stdout, "isatty", lambda: False) + + rc = g.cmd_init(argparse.Namespace(path=".gitpublic", force=False)) + config = (tmp_path / ".gitpublic" / "config").read_text() + + assert rc == 0 + assert "source = you/private-repo" in config + assert "target = you/public-repo" in config