# 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`로 스테이징할 수 있습니다.