Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
522bf60
[docs][backlog] append-only 이력 정책 decision과 실측 문서
coralstay Sep 26, 2026
2958649
[docs][backlog] decision-24 우회 경로 전수 조사
coralstay Sep 26, 2026
558b335
[docs][backlog] decision-24에 강제 계층 기록 + 원격 설정 적용
coralstay Sep 26, 2026
9ca09ab
[docs][backlog] doc-20 원격 강제 설정 관리 문서
coralstay Sep 26, 2026
0e05bed
[docs][backlog] 원격 쪽 남은 구멍 드래프트 2건
coralstay Sep 26, 2026
e97b4e8
[docs][backlog] 정책 결정·문서화 작업을 드래프트로 세운다
coralstay Sep 26, 2026
7fef01e
[docs][backlog] DRAFT-21을 GF-137로 승격
coralstay Oct 3, 2026
574859c
[docs][backlog] 다이어그램 빌드 파이프라인 드래프트
coralstay Oct 3, 2026
318a958
[docs][backlog] 목적 재정의·git-logbook 개명 드래프트
coralstay Oct 3, 2026
8f9148d
[docs][backlog] DRAFT-22를 GF-138로 승격
coralstay Oct 3, 2026
e206c24
[docs][backlog] decision-25 목적 재정의와 개명
coralstay Oct 3, 2026
ca65ac4
[docs][readme] 목적을 세 기둥으로 다시 쓰고 개명 반영
coralstay Oct 3, 2026
6d37e36
[chore] 표시 이름을 git-logbook으로 바꾼다
coralstay Oct 3, 2026
8462dd0
[docs][backlog] GF-138 착수 기록
coralstay Oct 3, 2026
02e6e94
[docs][backlog] GF-133에 설정 키 이전 AC 추가
coralstay Oct 3, 2026
990bda4
[docs] 리뷰 반영: 원격 보호 범위를 사실대로 적는다
coralstay Oct 3, 2026
63b8ee1
[docs][backlog] GF-133 설정 키 이전 AC 문구 명확화
coralstay Oct 3, 2026
d224659
[docs] Signed-off-by 유지를 기록하고 GF-128 AC를 맞춘다
coralstay Oct 3, 2026
6a03888
[docs][backlog] 문서 6개를 decision-25에 맞춘다
coralstay Oct 3, 2026
1e307d5
[docs][backlog] GF-138 완료 처리
coralstay Oct 3, 2026
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
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: test

# git-format 자체의 개발용 CI(GF-21, decision-8). 컨슈머에 배포하는 verify.yml과
# git-logbook 자체의 개발용 CI(GF-21, decision-8). 컨슈머에 배포하는 verify.yml과
# 달리 이건 이 저장소 자신의 훅 코드 품질만 검사한다 — 훅도 테스트도 Python 표준
# 라이브러리만 쓰므로(decision-16, decision-21) 테스트 프레임워크를 따로 설치하는
# 스텝이 없다. 아래 ruff 설치는 이 저장소 자신의 Python 코드를 검사하는 개발 도구다 —
Expand Down
2 changes: 1 addition & 1 deletion .gitmessage
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,4 @@
#
# ── 이 템플릿 사용법 ──
# git config commit.template .gitmessage
# (git-format install.sh를 사용하면 자동으로 설정됩니다 — GF-8 참고)
# (git-logbook install.sh를 사용하면 자동으로 설정됩니다 — GF-8 참고)
52 changes: 34 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
<div align="center">

# 🧬 git-format
# 📒 git-logbook

**git 자체 기능만으로 동작하는 커밋 메시지 규칙 · 검증 · 이력 정형화 도구**
**git 이력을 작업 로그로 만드는 도구 — 커밋 형식 · 출처 기록 · 이력 불변**

[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
[![Hooks: Python 3](https://img.shields.io/badge/hooks-Python%203-3776ab.svg)](./hooks/)
[![Installer: POSIX sh](https://img.shields.io/badge/installer-POSIX%20sh-89e051.svg)](./install.sh)
[![Requires: git + python3](https://img.shields.io/badge/requires-git%20%2B%20python3-brightgreen.svg)](#-필요조건을-말씀드립니다)

`core.hooksPath` · `commit.template` · `git interpret-trailers` 등 **git 자체 기능만으로**
여러 저장소가 하나의 커밋 규칙을 공유할 수 있게 만들어 사용하고 있는 도구입니다.
에이전트와 사람이 남기는 커밋을 정해진 양식으로 쓰고, 누가·무엇으로·얼마를 들여
만들었는지 자동으로 기록하고, 한 번 남긴 기록은 다시 쓰지 않게 지키는 도구입니다.
`core.hooksPath` · `git interpret-trailers` 등 **git 자체 기능**으로 동작합니다.
훅은 Python 3 표준 라이브러리만 쓰므로 설치할 패키지는 없지만, `python3` 자체는
필요합니다([필요조건](#-필요조건을-말씀드립니다)).

Expand Down Expand Up @@ -65,7 +66,7 @@ Signed-off-by: cpu-once <231006716+cpu-once@users.noreply.github.com>
| `Tokens-Used` | 이 커밋에 든 토큰. 작업 비용을 이력에서 읽게 합니다 | `post-commit` · 직전 커밋 이후 세션 구간의 델타(누적 아님) | 서버 발급 사실(Claude Code 한정), **측정 방법론은 실험 단계** |
| `Tool-Calls` | 같은 구간의 도구 호출 수 | `post-commit` · `tool_use` 블록 개수 | 위와 동일 |
| `Co-Authored-By` | 공동저자 귀속 | `post-commit` · `AI-Tool`이 `claude-code`일 때만 | 자동 |
| `Hooks-Commit` | 이 커밋을 검사한 git-format 자체의 버전. 훅에 버그가 있었을 때 어느 커밋들이 그 훅을 거쳤는지 역추적합니다 | `post-commit` · 훅 클론의 `rev-parse --short HEAD` | 완전 자동, 모든 커밋 |
| `Hooks-Commit` | 이 커밋을 검사한 git-logbook 자체의 버전. 훅에 버그가 있었을 때 어느 커밋들이 그 훅을 거쳤는지 역추적합니다 | `post-commit` · 훅 클론의 `rev-parse --short HEAD` | 완전 자동, 모든 커밋 |
| `Signed-off-by` | 커미터 정보(DCO 관례) | `post-commit` · `git log -1 --format='%cn <%ce>'` | 완전 자동, 모든 커밋 |
| `Verify-Bypassed` | `--no-verify`로 검사를 건너뛴 사실. 우회를 막지는 못하니 대신 기록합니다(decision-3) | `post-commit` · `prepare-commit-msg`가 남긴 검증 마커의 **부재** | 자동 |
| `Fixes` | 이 버그를 만든 커밋 | **사람이 씀**(원인 커밋을 아는 경우만) · `commit-msg`가 해시 실재를 검증 | 검증됨 |
Expand All @@ -76,11 +77,11 @@ Signed-off-by: cpu-once <231006716+cpu-once@users.noreply.github.com>

## 🪝 커밋 한 번에 훅이 도는 순서

> 공식 문서: [githooks(5)](https://git-scm.com/docs/githooks). git이 제공하는 훅은 28개지만
> git-format은 커밋 단계만 다룹니다(decision-11, decision-12) — `git push`는 아무 훅도
> 거치지 않는 평범한 push입니다.
> 공식 문서: [githooks(5)](https://git-scm.com/docs/githooks). git이 제공하는 훅은 28개이고,
> git-logbook의 훅은 지금 커밋 단계만 다룹니다. push된 이력의 보호는 원격 설정이 맡습니다
> ([다루는 범위](#-다루는-범위)).

| 순서 | 훅 | 커밋 객체가 있는가 | `--no-verify`로 건너뛰나 | git-format이 하는 일 |
| 순서 | 훅 | 커밋 객체가 있는가 | `--no-verify`로 건너뛰나 | git-logbook이 하는 일 |
| --- | --- | --- | --- | --- |
| 1 | [`prepare-commit-msg`](hooks/prepare-commit-msg) | 아직 없음 | **건너뛸 수 없음** | 재생·병합 커밋 면제, 에디터 경로 거부, 검증 마커 기록 |
| 2 | (에디터) | 아직 없음 | — | 사람이 메시지를 씁니다. 훅보다 **뒤**라서 1번은 최종 메시지를 볼 수 없습니다 |
Expand All @@ -95,11 +96,26 @@ Signed-off-by: cpu-once <231006716+cpu-once@users.noreply.github.com>

## 🎯 다루는 범위

커밋 메시지 형식과 트레일러입니다. **언어별 lint는 다루지 않습니다**(decision-23) —
커밋 형식을 맞추는 도구가 언어 도구를 돌릴 이유가 없고, 그 결합이 컨슈머에게
npm/ruff/clang-format/mvn/sqlfluff의 존재를 전제하게 만들었습니다. 언어 검사가 필요하면
CI에서 돌리시기 바랍니다 — git-format이 `core.hooksPath`를 점유하므로 그 저장소의
`.git/hooks/*`는 무시되고, 자기 훅과 함께 쓰는 방법은 아직 정하지 않았습니다.
git 이력을 작업 로그로 만드는 데 필요한 세 가지입니다(decision-25).

| 기둥 | 하는 일 | 지금 있는 것 | 계획 |
| --- | --- | --- | --- |
| **형식** | 사람이 쓰는 제목·본문을 정해진 양식으로 | 제목·본문 검증, 에디터 경로 거부, 커밋 단위 규칙(decision-22, 강제 없음) | 검증을 `prepare-commit-msg`로 통합(GF-127) |
| **출처 기록** | 누가·어떤 도구와 모델로·얼마를 들여 만들었는지 자동으로 남김 | 위 트레일러 전체, `Task-Id` 브랜치 강제, `AI-Model` 게이트 | 측정 재작성(GF-129), 트레일러 집합 재정의(decision-19, `Signed-off-by`는 유지 — decision-25) |
| **이력 불변** | 한 번 남긴 기록은 다시 쓰지 않음(append-only, decision-24) | 이 저장소의 GitHub 설정: 머지 커밋만 허용, main의 force push·삭제 금지(doc-20). 컨슈머는 doc-20을 참고해 직접 설정 | 로컬 훅 강제(DRAFT-18 append-only 훅 강제), CI 트레일러 검사(DRAFT-19) |

세 기둥은 서로를 필요로 합니다. 이력이 다시 쓰이면 트레일러가 자기 커밋에 대해 거짓이
되고, 기록할 내용이 정형화돼 있지 않으면 지킬 가치가 줄어듭니다. 로컬 훅은
`--no-verify`를 막지 못합니다. 그렇게 다시 쓴 이력이 main에 force push되는 것은 원격
설정이 거부합니다. 웹 UI 커밋과 트레일러 검사는 CI로 할 계획입니다(DRAFT-19).

**언어별 lint는 다루지 않습니다**(decision-23) — 세 기둥 어디에도 속하지 않고, 컨슈머에게
npm/ruff/clang-format 같은 도구를 전제하게 만듭니다. 언어 검사가 필요하면 CI에서 돌리시기
바랍니다. git-logbook이 `core.hooksPath`를 점유하므로 그 저장소의 `.git/hooks/*`는
무시되고, 자기 훅과 함께 쓰는 방법은 아직 정하지 않았습니다.

> 이름을 git-format에서 git-logbook으로 바꿨습니다(decision-25). 설정 키와 파일명
> (`gitformat.*`, `gitformat.conf`, `.gitformat-*`)은 기존 설치를 깨지 않도록 아직 예전 이름입니다(GF-133).

구성은 Python 훅 + 설정 파일 하나(`hooks/gitformat.conf`) + POSIX sh 설치
스크립트(`install.sh`)입니다. `core.hooksPath` · `init.templateDir` ·
Expand Down Expand Up @@ -135,17 +151,17 @@ python3는 이런 환경에서 보이지 않을 수 있습니다.
### 기존 저장소에 적용하는 방법입니다

```sh
git clone https://github.com/amosQP/git-format.git ~/git-format # 원하는 위치에 한 번만 클론
git clone https://github.com/coralstay/git-logbook.git ~/git-logbook # 원하는 위치에 한 번만 클론
cd ~/my-project
~/git-format/install.sh
~/git-logbook/install.sh
```

대상 디렉터리를 인자로 주셔도 됩니다: `~/git-format/install.sh ~/my-project`.
대상 디렉터리를 인자로 주셔도 됩니다: `~/git-logbook/install.sh ~/my-project`.

### 앞으로 만들 모든 새 저장소에 자동 적용하는 방법입니다

```sh
~/git-format/install.sh --global
~/git-logbook/install.sh --global
```

`git init`/`git clone`을 실행할 때마다 훅과 커밋 템플릿이 자동으로 심어집니다
Expand Down
2 changes: 1 addition & 1 deletion backlog/config.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
project_name: "git-format"
project_name: "git-logbook"
default_status: "To Do"
statuses: ["To Do", "In Progress", "Done"]
labels: []
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
---
id: decision-24
title: '이력은 append-only — 병합은 머지 커밋, rebase는 로컬만, cherry-pick 금지'
date: '2026-09-26 08:28'
status: accepted
---
## Context

이 프로젝트의 전제는 **에이전트가 프로그래밍하고 git이 그 작업의 로그로 동작한다**는
것이다(README). 그런데 이력을 다시 쓰는 연산에 대한 정책이 없었고, 오히려 PR을 GitHub의
rebase-merge로 병합하는 관행이 있었다.

**그 관행이 실제로 기록을 거짓으로 만들고 있었다.** 실측(2026-09-26, main의 최근 3개 커밋):

```
커밋 72755d0 committer = coralstay <...> ← git이 기록한 실제 커미터
Signed-off-by: cpu-once <...> ← 트레일러가 주장하는 커미터
%G? = N (세 커밋 모두)
```

`post-commit`이 로컬 커밋 시점의 커미터로 `Signed-off-by`를 썼는데, GitHub이 rebase로
커밋을 다시 만들면서 커미터가 바뀌었다. **트레일러가 자기가 앉아 있는 커밋에 대해 거짓을
말한다.**

같은 논리가 다른 트레일러에도 적용된다. `Tokens-Used`는 "그 세션 그 구간"의 측정값이고
`Hooks-Commit`은 "이 커밋을 검사한 훅 버전"인데, 재생된 객체는 그 세션에서 만들어진 것도
아니고 그 훅이 검사한 것도 아니다.

## 외부 근거

- **Linux 커널 메인테이너 문서**: "History that has been exposed to the world beyond your
private system should usually not be changed." / "Reparenting a patch series ... invalidates
much of the testing that was done." / "The kernel community is not scared by seeing merge
commits in its development history." — rebase로 merge를 피하려는 시도 자체를 문제로 본다.
단 금지 선은 "공개된 이력"이다.
- **서명**: rebase는 부모를 바꾸고 부모는 서명 대상이므로 서명이 무효화된다. GitHub의
rebase-merge는 서명 검증 없이 커밋을 추가한다(GitHub이 서명 키를 갖고 있지 않다).
- **provenance 연구**(arXiv 2607.02774): force-push 1억 6,671만 건 / 저장소 1,992만 개.
이력 재작성이 "이력은 불변"이라는 가정을 무너뜨려 저자 귀속과 취약점 추적 분석을
무효화한다.
- **Gerrit `Change-Id`**: 커밋 SHA가 불안정하다는 전제로 SHA와 독립된 안정 식별자를
트레일러에 둔 선례. 그 `commit-msg` 훅 규칙이 우리와 같다 — 이미 있으면 건드리지 않는다.

## Decision

**커밋이 만들어진 뒤에는 그 객체를 다시 만들지 않는다(append-only).**

금지선은 커널 문서와 같이 **공개(push)된 이력**에 둔다 — 아직 push하지 않은 로컬 작업
정리는 로그 위조가 아니다.

| 연산 | 정책 | 판별 방법 (전부 실측, doc 참고) |
| --- | --- | --- |
| 커밋 | 허용 | — |
| **revert** | **권장** — append-only의 되돌리기 수단 | 제목 `Revert "` + 본문 `This reverts commit <실재 해시>` |
| **merge** | 허용 — 통합 수단 | `MERGE_HEAD` |
| **rebase (push 전 로컬)** | 허용 | `CHERRY_PICK_HEAD` + `$GIT_DIR/rebase-merge/` 디렉터리 |
| **rebase (push 후)** | 거부 | `pre-rebase`에서 재생 대상이 원격 ref에 이미 있는지 |
| **cherry-pick** | 거부 | `CHERRY_PICK_HEAD` 있고 `rebase-*/` **없음** |
| **`--amend`** | 거부 | `source=commit` |
| **squash merge** | 거부 | `source=squash` / `SQUASH_MSG` |

**PR 병합은 머지 커밋으로 한다.** rebase-merge와 squash는 쓰지 않는다. 선형 뷰가 필요하면
`git log --first-parent`를 쓴다.

**이미 어긋난 과거 커밋은 고치지 않는다.** 그것을 고치는 것 자체가 이력 재작성이고, 그때의
결정에는 그때의 이유가 있었다.

## 강제 계층 — 로컬과 원격이 맞물리는 지점

**`--no-verify`는 로컬 훅을 건너뛸 수 있다.** 그래서 로컬만으로는 정책이 완결되지 않는다.
계층을 나눈다.

### 로컬 (git 훅) — 볼 수 있는 모든 재작성 경로를 막는다

| 대상 | 판별 |
| --- | --- |
| cherry-pick | `CHERRY_PICK_HEAD` 있고 `rebase-merge/`·`rebase-apply/` 없음 |
| `--amend` | `source=commit` + `$3`이 `HEAD` + `rebase-merge/` 없음 |
| squash | `source=squash` 또는 `SQUASH_MSG` |
| 에디터 경로 | 메시지 파일에 비주석 내용 없음(GF-125) |
| push된 커밋을 다시 쓰는 rebase | `pre-rebase`에서 재생 대상이 원격 ref에 있는지 |

`--no-verify`로 건너뛸 수 있는 것은 `pre-rebase`와 `pre-push`다. `prepare-commit-msg`는
건너뛸 수 없으므로 커밋 단계 강제는 그대로 유지된다(doc-15).

### 원격 (GitHub) — 로컬이 닿지 않는 것을 막는다

**github.com에는 `pre-receive` 훅을 설치할 수 없다** — 그건 GitHub Enterprise 전용 기능이다.
대신 rulesets와 저장소 설정이 그 자리를 대신한다. 2026-09-26 적용 완료:

| 설정 | 값 | 무엇을 막나 |
| --- | --- | --- |
| `allow_merge_commit` | `true` | — |
| `allow_rebase_merge` | **`false`** | rebase-merge가 커밋을 다시 만들어 트레일러를 거짓으로 만드는 것 |
| `allow_squash_merge` | **`false`** | squash |
| ruleset `non_fast_forward` | 활성 | **force push** — `--no-verify`로 로컬 훅을 건너뛰어 다시 쓴 이력이 원격에 도달하는 것 |
| ruleset `deletion` | 활성 | 브랜치 삭제 |

**이게 두 계층이 맞물리는 지점이다.** `--no-verify`로 로컬에서 이력을 다시 쓰는 것 자체는
막을 수 없다. 그러나 다시 쓴 결과는 **fast-forward가 아니므로 원격이 거부한다.** 즉 로컬
재작성은 가능하지만 **공개될 수 없다** — 금지선을 "공개된 이력"에 둔 것과 정확히 일치한다.

**켜면 안 되는 규칙**: rulesets의 `required_linear_history`는 이름이 좋아 보이지만
**merge commit을 금지하는 규칙**이다. append-only와 정반대다.

**켜지 않은 것**: `required_signatures`(서명 강제). 현재 이 저장소의 커밋은 `%G?`가 `N`으로
나오므로 켜면 자기 push가 막힌다. 서명 설정을 먼저 정리한 뒤 검토할 사항이다.

원격 설정의 현재 상태, 확인·변경 명령, 함정은 **doc-20(GitHub 원격 강제 설정 관리)** 에
정리했다. 연산별 판별 방법과 우회 경로 전수 조사는 doc-19다.

## Consequences

- 트레일러가 자기 커밋에 대해 참인 상태가 유지된다. `Signed-off-by`가 실제 커미터와,
`Hooks-Commit`이 실제로 검사한 훅과, `Tokens-Used`가 실제 그 작업 구간과 맞물린다.
- 서명이 병합 후에도 유효하게 남는다.
- 실제 분기 구조가 보존된다 — rebase가 만드는 가짜 선형 순서 대신, 병렬로 일어난 작업이
병렬로 기록된다. 에이전트 작업 이력을 분석할 때 이게 사실과 일치한다.
- **기존 정책이 뒤집힌다.** 로컬 claude-rails 훅이 `git merge`를 `--ff-only` 없이 거부하고
있고, 유저 메모리에 "항상 rebase-merge" 지시가 있었다. 둘 다 갱신해야 한다.
- **cherry-pick과 rebase는 훅 인자로 구분되지 않는다** — `source`와 상태 파일이 동일하고
진행 디렉터리(`rebase-merge/`)만 다르다. cherry-pick만 막으려면 그 디렉터리를 봐야 한다.
- **merge와 revert는 git이 메시지를 지어주므로** `[type][subsystem]` 규칙에 맞지 않는다.
둘 다 새 커밋이라 트레일러는 붙여야 하고 제목 형식 검사만 예외로 둬야 한다.
- `git log`가 머지 커밋으로 덜 선형이 된다. `--first-parent`로 완화한다.
Loading
Loading