From ec76d53a95f0a42d049c5f6280f4700e460a7c4e Mon Sep 17 00:00:00 2001 From: art Date: Wed, 28 Jan 2026 10:03:23 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20Git=20=EC=9D=B4=EB=AF=B8=EC=A7=80(image?= =?UTF-8?q?)=20=ED=8C=8C=EC=9D=BC=20=EA=B6=8C=ED=95=9C=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=9A=B0=ED=9A=8C(workaround)=20=EB=AC=B8=EC=84=9C?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 이미지 파일 add/hash-object 시 Operation not permitted 발생 시 stdin+update-index 기반 우회 절차를 문서화. --- docs/git-image-permission-workaround.md | 174 ++++++++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 docs/git-image-permission-workaround.md diff --git a/docs/git-image-permission-workaround.md b/docs/git-image-permission-workaround.md new file mode 100644 index 0000000..a50f6cf --- /dev/null +++ b/docs/git-image-permission-workaround.md @@ -0,0 +1,174 @@ +# Git 이미지(image) 파일 `Operation not permitted` 우회(workaround) 문서 + +## 배경(background) / 증상(symptom) + +일부 환경(macOS 등)에서 `git add`, `git hash-object `가 특정 이미지 파일(`.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 ... `로 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 -- +``` + +예: 현재 `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: XYpath + 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 -- +``` + +### 주의사항(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`로 스테이징할 수 있습니다. +