spt-mods/docs/git-image-permission-workar...

175 lines
5.8 KiB
Markdown

# Git 이미지(image) 파일 `Operation not permitted` 우회(workaround) 문서
## 배경(background) / 증상(symptom)
일부 환경(macOS 등)에서 `git add`, `git hash-object <path>`가 특정 이미지 파일(`.png`, `.jpg` 등)을 읽기 위해 열 때(open) 아래 오류로 실패할 수 있습니다.
```
error: open("path/to/file.png"): Operation not permitted
fatal: could not open 'file.png' for reading: Operation not permitted
```
이 경우 OS 레벨에서는 파일 읽기가 되지만(Python 등으로는 `open()` 가능), Git이 **경로(path)를 직접 열어** 객체(object)를 생성하려는 단계에서 차단될 수 있습니다.
## 목표(goal)
- **변경사항(changes) 전체를 커밋(commit)** 해야 하는데,
- Git이 이미지 파일을 직접 열 수 없어서 `git add`가 실패할 때,
- **Git이 파일을 “직접 열지 않도록”** 우회해서 staging/index에 올리는 방법을 제공.
## 우회(workaround) 개요
핵심 아이디어는 2단계입니다.
1) 이미지 파일을 제외하고 나머지 파일은 정상적으로 `git add`로 스테이징(staging)
2) 이미지 파일은
- Python이 파일 bytes를 읽고
- `git hash-object -w --stdin`으로 **stdin(표준입력)** 을 통해 blob을 만들어(= Git이 파일 path를 직접 open하지 않음)
- `git update-index --add --cacheinfo ... <path>`로 index에 **직접 등록**
## 1) 이미지 제외하고 먼저 스테이징(staging)
아래처럼 이미지 확장자를 제외(exclude)하고 `git add`를 실행합니다.
```bash
git add -A -- . \
':(exclude)**/*.png' \
':(exclude)**/*.jpg' \
':(exclude)**/*.jpeg' \
':(exclude)**/*.gif' \
':(exclude)**/*.webp'
```
## 2) 이미지 파일을 stdin + update-index로 스테이징(staging)
아래 스크립트는 **untracked(추적 안됨)** 이미지 파일을 찾아서, 각 파일을 우회 방식으로 스테이징합니다.
```bash
python3 - <<'PY'
import subprocess
from pathlib import Path
image_exts = {'.png', '.jpg', '.jpeg', '.gif', '.webp'}
# repo에 새로 추가된(untracked) 파일 목록
res = subprocess.run(
['git', 'ls-files', '--others', '--exclude-standard'],
check=True,
capture_output=True,
text=True,
)
paths = [p for p in res.stdout.splitlines() if p]
img_paths = [p for p in paths if Path(p).suffix.lower() in image_exts]
print(f'found_images={len(img_paths)}')
for p in img_paths:
data = Path(p).read_bytes() # OS 레벨 read
# Git이 file path를 직접 open하지 않도록 stdin으로 blob 작성(write)
ho = subprocess.run(
['git', 'hash-object', '-w', '--stdin'],
input=data,
capture_output=True,
)
if ho.returncode != 0:
raise SystemExit(
f'hash-object failed for {p}: {ho.stderr.decode(errors="replace")}'
)
blob = ho.stdout.decode().strip()
# index에 직접 등록(add): mode 100644 + blob sha + path
ui = subprocess.run(
['git', 'update-index', '--add', '--cacheinfo', '100644', blob, p],
capture_output=True,
text=True,
)
if ui.returncode != 0:
raise SystemExit(f'update-index failed for {p}: {ui.stderr}')
print('done')
PY
```
### 확인(check)
```bash
git status
git diff --staged --stat
```
## 3) 커밋(commit)
```bash
git commit -m "..."
```
## (선택) 커밋 후 `git status`에 이미지가 `M`으로 남아 보일 때
환경에 따라 커밋 후에도 이미지 파일들이 `git status`에서 `modified(M)`로 보일 수 있습니다.
이 경우 실제로 내용이 바뀐 게 아니라, Git이 워킹트리(working tree)와 index를 비교하는 과정에서
다시 파일 open/read가 막히거나(또는 메타데이터 차이로) 변경으로 인식할 수 있습니다.
이때는 **로컬(local)에서만** 해당 파일들을 “변경 무시(assume-unchanged)” 처리해 작업 트리를 깨끗하게 보이게 할 수 있습니다.
### assume-unchanged 설정(set)
```bash
git update-index --assume-unchanged -- <file...>
```
예: 현재 `M`인 파일을 자동으로 찾아 처리하려면:
```bash
python3 - <<'PY'
import subprocess
res = subprocess.run(
['git', 'status', '--porcelain=v1'],
check=True,
capture_output=True,
text=True,
)
paths = []
for line in res.stdout.splitlines():
if not line:
continue
# format: XY<space>path
x, y = line[0], line[1]
path = line[3:]
if y == 'M':
paths.append(path)
print('mark_assume_unchanged', len(paths))
chunk_size = 200
for i in range(0, len(paths), chunk_size):
chunk = paths[i:i + chunk_size]
subprocess.run(['git', 'update-index', '--assume-unchanged', '--', *chunk], check=True)
print('done')
PY
```
### assume-unchanged 해제(unset)
```bash
git update-index --no-assume-unchanged -- <file>
```
### 주의사항(warnings)
- `assume-unchanged`**커밋에 포함되지 않는 로컬 상태(local state)** 입니다.
- 설정 후에는 해당 파일이 실제로 변경돼도 Git이 변경을 잘 감지하지 않을 수 있습니다.
- 실제로 수정할 일이 생기면, 먼저 `--no-assume-unchanged`로 해제하고 작업하세요.
## FAQ / 트러블슈팅(troubleshooting)
- **Q. 왜 Python은 읽히는데 Git은 못 읽나요?**
- A. 환경/보안 정책에 따라 특정 프로세스/바이너리의 파일 접근이 차단되는 케이스가 있습니다. 여기서는 “Git이 경로로 파일을 여는(open) 방식”이 막혔고, stdin으로 bytes를 전달하는 방식은 통과했습니다.
- **Q. 이미지뿐 아니라 다른 바이너리(binary)도 막히면?**
- A. 동일한 방식으로 확장자(ext)를 늘리거나, 대상 파일 목록을 바꿔서 stdin + `update-index`로 스테이징할 수 있습니다.