175 lines
5.8 KiB
Markdown
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`로 스테이징할 수 있습니다.
|
|
|