Windows로 옮기기 전에 .gitattributes로 줄끝을 LF에 고정하고 읽기 전용 시험을 먼저 RED로 본 기록
macOS에서 만든 설정 저장소를 Windows로 옮기기 전, * text=auto eol=lf를 넣고 git check-attr와 ls-files --eol을 읽는 시험을 1 passed 100 failed RED로 먼저 본 뒤 4/4로 만든 과정과 한계를 기록했다.

macOS에서 만든 셸·설정 저장소를 Windows의 Git Bash에서도 쓰려고, 옮기기 전에 줄끝을 LF로 고정했다.
방법은 .gitattributes에 * text=auto eol=lf 한 줄을 넣는 것과, 그 속성이 모든 추적 파일에 실제로 적용되는지 묻는 읽기 전용 시험을 먼저 쓰는 것이었다.
시험을 먼저 병합해 기준선에서 1 passed 100 failed라는 의도된 RED를 관찰했고, 설정 두 줄을 넣은 뒤 4 passed 0 failed로 바뀌었다.
표지 이미지는 글의 요점을 시각화한 개념 이미지(AI 생성)다.
이 일은 내가 요청하고 코딩 에이전트들이 나눠 수행했다. 시험 파일은 GPT 계열 에이전트가 썼고 Claude 계열 모델이 검토했다. 설정 두 줄은 Claude 계열 에이전트가 썼고 GPT 계열 모델이 검토했다. 그래서 이 글에서 "썼다"와 "확인했다"는 에이전트가 남긴 기록을 내가 읽었다는 뜻이다.
범위를 먼저 자른다. 이 글은 줄끝 속성 하나와 그 시험만 다룬다. 같은 이식 계획의 훅 명령 형식, 상태줄 경로, 문서 경로 수정은 다루지 않는다. Windows 실기에서 체크아웃해 확인한 결과도 이 글에는 없고, 마지막 절에 그 한계를 적었다.
시작 상태: 이미 LF인데 왜 파일을 더하나
계획 단계에서 기록한 저장소의 시작 상태는 네 가지였다.
- 추적 파일 49개가 모두
git ls-files --eol에서i/lf w/lf였다. - 이진 파일이 없었다.
.gitattributes가 없었다..gitignore가 허용 목록 방식이었다. 먼저*로 전부 무시하고 필요한 파일만!로 다시 포함하는 구조라서, 새.gitattributes는 다시 포함하기 전까지 무시된다.
이미 전부 LF라면 속성 파일이 왜 필요한가 하는 의문이 든다.
기록이 적은 요구사항은 이렇다.
.gitattributes가 추적되고, 한 줄 * text=auto eol=lf를 담고, 추적 파일 전부에서 git check-attr이 text: auto와 eol: lf를 보이고, git ls-files --eol이 색인 쪽 i/lf를 보여야 한다.
지금 내용이 LF라는 것은 현재 상태일 뿐이다. 앞으로 어떤 컴퓨터가 어떤 설정으로 체크아웃하거나 커밋하든 LF가 유지된다는 약속은 아니다. 이 구분은 기록이 한 문장으로 적은 것이 아니라 내가 요구사항을 읽고 정리한 해석이다. 다음 절의 공식 문서가 이 해석이 맞는 방향이라는 근거를 준다.
공식 문서가 말하는 text와 eol
Git 공식 문서의 gitattributes 페이지에서 이번 설정과 관련된 부분을 읽었다. 페이지에는 2.56.0 기준 갱신이라고 표시돼 있었다. 아래 표는 문서가 속성별로 설명한 것과 이 저장소에 대한 내 해석을 나눠 적는다.
| 속성 | 문서의 설명(요약) | 내 해석 |
|---|---|---|
text=auto | Git이 텍스트인지 이진인지 스스로 판단한다. 텍스트이고 이미 CRLF로 Git에 들어 있던 파일이 아니면 체크인과 체크아웃에서 줄끝을 변환한다. 그 밖에는 변환하지 않는다. | 이미 CRLF로 들어간 파일은 이 속성만으로 바뀌지 않는다. 그래서 색인 쪽 줄끝을 따로 읽는 시험이 필요하다. |
eol=lf | 체크아웃할 때 작업 트리에도 색인과 같은 줄끝을 쓴다. text나 text=auto가 있을 때만 효과가 있고, text가 지정되지 않았으면 eol을 쓰는 것이 text를 켠다. | 작업 트리의 LF를 명시하는 부분이다. |
eol 미지정 | 작업 트리의 줄끝은 core.autocrlf나 core.eol 설정을 따른다. text가 켜져 있고 둘 다 없으면 Windows는 eol=crlf, 그 밖의 플랫폼은 eol=lf가 기본이다. | text=auto만 쓰면 Windows 체크아웃은 CRLF가 될 수 있다. 그래서 eol=lf까지 적었다고 읽는다. |
세 번째 행이 이번 일의 핵심 근거다. macOS에서는 기본값이 이미 LF라서 속성이 없어도 문제가 드러나지 않는다. 같은 저장소가 Windows에서는 기본값이 달라진다고 문서가 적고 있다. 다만 이 기록의 어느 단계도 Windows에서 CRLF가 실제로 생기는 것을 관찰하지 않았으므로, 이것은 문서에서 읽은 가능성이다.
아래 그림은 이 표의 세 속성을 문서 설명과 내 해석으로 나란히 놓은 것이다.
문서는 도입 절차도 보여 준다.
text=auto 한 줄을 넣고 git add --renormalize .를 한 뒤 git status로 정규화될 파일을 확인하고 커밋하는 순서다.
이 저장소는 색인이 이미 LF라서, 이 절차에서 확인할 파일이 없을 것으로 예상했다.
실제로 재정규화 커밋은 필요 없었다.
읽기 전용 시험 네 가지
시험은 tests/eol.test.sh 파일 하나이고 112줄이 추가됐다.
저장소에는 쓰지 않는다.
git -C로 읽기 명령만 쓰고, 임시 파일은 /tmp 아래에 만들어 종료 때 지운다.
작업 지시는 정확히 한 파일만 쓰고, bash 3.2와 Git Bash와 shellcheck v0.11에서 통과하며, 400줄 미만이어야 한다는 조건을 달았다.
| 확인 | 쓰는 명령 | 실패로 보는 조건 |
|---|---|---|
| 1. 속성 파일의 내용 | grep -Fxq | .gitattributes가 일반 파일이 아니거나 정확히 * text=auto eol=lf 한 줄이 아님 |
| 2. 무시되지 않음 | git check-ignore -q --no-index .gitattributes | 종료 코드가 1이 아님. 0이면 무시됨이고, 0과 1이 아닌 값은 별도 실패 |
| 3. 모든 추적 파일의 속성 | git ls-files -z와 git check-attr -z text eol | 어느 경로든 text: auto와 eol: lf가 아님. 추적 파일이 0개여도 실패 |
| 4. 색인의 줄끝 | git ls-files --eol -z | 색인 열이 i/lf가 아님. i/-text:w/-text와 i/none:w/none은 이진이나 빈 파일에만 허용 |
마지막 줄은 Results: N passed, M failed 형식이고, 실패가 하나라도 있으면 종료 코드 1이다.
명령 선택은 공식 문서와 맞는다.
git-check-attr 문서에 따르면 -z는 출력을 기계가 읽기 쉽게 바꾸고, 구분자로 NUL을 쓴다.
git-ls-files 문서는 -z를 쓰면 파일 이름을 그대로 출력하고 줄을 NUL로 끝낸다고 적는다.
그래서 공백이나 줄바꿈이 든 파일 이름도 안전하게 읽는다고 검토가 평가했다.
git-ls-files 문서는 --eol의 i/가 색인 안의 줄끝 정보이고 w/가 작업 트리 쪽이며, 뒤에 attr/로 적용되는 속성이 나온다고 설명한다.
기록은 이 시험이 색인 열을 본다고 적는다.
작업 트리 열(w/)을 단언하는지는 기록에 없다.
이것은 한계 절에 다시 적는다.
검토가 지적한 설계 요소 몇 가지도 기록에 있다.
속성 값을 경로, 속성, 값 세 쌍으로 읽고 기본값을 missing으로 둬서, 속성이 아예 없는 경우도 실패가 된다.
파일별 반복의 통과 줄은 실패가 늘지 않았을 때만 출력하므로, 일부만 실패한 상태가 통과로 보이지 않는다.
카운터는 bash 3.2에서 안전한 파일 리다이렉션으로 현재 셸에 둔다.
의도된 RED: 1 passed 100 failed
기준 커밋에서 bash tests/eol.test.sh를 실행한 첫 관찰은 종료 코드 1, 1 passed 100 failed였다.
추적 파일은 49개였다.
이 숫자는 아래처럼 나뉜다.
| 시점 | 추적 파일 | passed | failed |
|---|---|---|---|
| 기준 커밋(시험 파일 없음) | 49 | 1 | 100 |
| 시험 파일을 더한 PR 머리 | 50(시험 파일 자신 포함) | 1 | 102 |
| 상류 변경을 합친 새 머리 | 기록에 개수 없음 | 1 | 102 |
첫 관찰의 실패 100개는 속성 파일 줄 확인 1개, 무시됨 확인 1개, 추적 파일 49개 각각의 text와 eol 속성 확인 98개(49 곱하기 2)다.
통과 1개는 49개 파일의 색인 열이 모두 i/lf라는 확인이다.
이 확인이 통과한 것은 시작 상태가 이미 LF였기 때문이다.
이 실패를 의도된 RED로 센 이유도 기록에 있다. 실패 이유는 파일 없음, 허용 목록이 무시함, 속성이 지정되지 않음뿐이었다. 가져오기 오류나 설정 오류는 없었다.
두 번째 행에서 눈여겨볼 점이 있다. 시험 파일 자신이 추적 파일이 되면서 파일이 49개에서 50개로 늘었고, 논리를 한 줄도 바꾸지 않았는데 실패가 100개에서 102개가 됐다. 파일마다 속성 두 개를 확인하므로 파일 한 개가 늘면 실패 두 개가 늘어난다. 그래서 RED 영수증에는 실패 수만이 아니라 그때의 추적 파일 수를 함께 적어야 다음 관찰과 비교할 수 있다. 이 권장은 이번 숫자 변화에서 내가 끌어낸 것이다.
아래 그림은 같은 시험의 결과를 시점 순서로 이어 놓은 것이다.
속성 파일이 존재한다는 사실과 Git이 모든 추적 파일에 그 속성을 적용한다는 사실은 다르다. 시험은 후자를 묻고, 그 파일 수가 실패 수를 정한다.
시험 검토와 병합
시험은 작성자와 다른 모델 계열의 리뷰 에이전트가 검토했다. 적합성, 단언, 경계 사례, 격리, 회귀 다섯 차원의 점수는 9, 9, 8, 9, 9였고 막는 항목은 없었다. 리뷰 요약은 이렇다. 기준 시점에 속성 파일이 아직 추적되지 않으므로 파일별 반복이 그 파일을 덮지 못하는 것은 논리 단계 전까지 예상된 일이다. check-ignore가 0과 1 밖이면 실패하고, 임시 디렉터리 생성 실패와 빈 목록, check-attr 실패도 처리한다.
첫 검토 출력은 JSON 문법 오류로 점수 게이트가 읽지 못했다. 점수가 낮아서가 아니라 출력 형식이 깨진 것이었고, 같은 내용을 제자리에서 고쳐 두 번째 출력으로 통과시켰다. 하네스 내부 절차이므로 여기서는 더 다루지 않는다.
이 시험을 뺀 나머지 시험은 모두 통과했다.
gitignore 시험은 18 passed 0 failed였고, bun test mods/는 21 pass 0 fail이었다.
shellcheck는 로컬에 없어서 CI의 v0.11이 맡았다.
그 상태에서 시험만 먼저 병합했다. 그 슬라이스의 의도된 RED만 실패하면 시험 PR을 병합할 수 있다는 규칙을 따랐다.
이 방식 자체는 의도된 RED를 CI 게이트에 기록하는 글에서 따로 다뤘다.
계획 검증 단계에서 병합 순서가 한 번 바뀐 일도 이 시험의 위치와 관련이 있다. 검증은 시험 PR 둘을 먼저 병합하면 어느 논리 PR도 전체 스위트가 초록인 채로 병합할 수 없다고 지적했다. 계획은 병합 순서를 이 설정의 시험, 이 설정의 논리, 다음 설정의 시험, 다음 설정의 논리로 직렬화하도록 고쳤다. 작성은 계속 병렬이고 병합만 직렬이다.
두 줄 변경과 GREEN 4/4
논리 변경은 파일 둘에 두 줄이었다(2 files changed, 2 insertions).
| 파일 | 변경 | 이유 |
|---|---|---|
.gitattributes | 새 파일, * text=auto eol=lf 한 줄 | 모든 추적 파일에 text=auto와 eol=lf를 적용한다. |
.gitignore | !/.gitignore 다음에 !/.gitattributes 한 줄 추가 | 허용 목록이라 이 줄이 없으면 새 파일이 무시되어 시험의 무시되지 않음 단언이 실패한다. |
시험 파일은 바뀌지 않았다.
같은 시험이 Results: 4 passed, 0 failed, 종료 코드 0이 됐다.
네 개는 위 표의 확인 네 가지에 대응한다.
리뷰된 머리에서는 추적 파일 51개가 모두 text=auto, eol=lf였고 색인은 LF였다.
숫자 하나는 설명하지 못한다.
이전 GREEN 로그는 같은 시험이 파일 50개로 센 값이었고, 머리 쪽은 51개였다.
기록은 51번째가 .gitattributes 자신으로 보인다고 적지만, 이 차이의 이유를 확정하지 않는다.
나도 그 이상을 단정하지 않는다.
전체 스위트도 초록이었다.
이 설정의 시험 4 passed 0 failed, gitignore 시험 18 passed 0 failed, bun test mods/ 21 pass 0 fail이 모두 종료 코드 0이었다.
CI 점검도 성공으로 기록돼 있다.
논리 검토는 GPT 계열 모델이 정확성 10, 포괄성 9, 유지보수성 10, 안전성 10, 통합 9로 평가했고 막는 항목은 없었다.
이 점수는 제공된 자료에 대한 평가이고, 아래 합성 저장소 실험이나 Windows 실행을 대신하지 않는다.
재정규화 커밋이 필요 없었다는 점은 변경의 성격을 보여 준다. 이번 변경은 저장된 파일 내용을 하나도 바꾸지 않았다. 앞으로의 커밋과 체크아웃이 지킬 규칙을 추가한 것이다.
합성 저장소에서 같은 명령 돌려 보기
기록의 명령이 문서의 설명대로 움직이는지 이 Mac에서 따로 확인했다.
Git 2.54.0(Apple Git)으로 빈 저장소를 만들고 허용 목록 .gitignore와 셸 파일 두 개를 커밋했다.
추적 파일은 3개였고 기록의 저장소와 무관한 합성 입력이다.
아래 출력의 숫자를 기록의 49나 100과 섞어 읽으면 안 된다.
# 속성 파일을 만들기 전(.gitignore는 '*' 다음에 필요한 것만 '!'로 포함)
$ git ls-files --eol
i/lf w/lf attr/ .gitignore
i/lf w/lf attr/ a.sh
i/lf w/lf attr/ b.sh
$ git check-attr text eol -- a.sh
a.sh: text: unspecified
a.sh: eol: unspecified
$ git check-ignore -q --no-index .gitattributes; echo "rc=$?"
rc=0
# 속성 파일을 만들고 git add (출력은 hint 줄을 뺀 발췌)
$ git add .gitattributes
The following paths are ignored by one of your .gitignore files:
.gitattributes
$ git check-ignore -v --no-index .gitattributes
.gitignore:1:* .gitattributes
첫 단계가 기록의 기준선 RED와 같은 모양이다.
속성은 unspecified이고, check-ignore는 종료 코드 0(무시됨)이며, 허용 목록 때문에 git add가 거부된다.
!/.gitattributes를 더하고 커밋하면 이렇게 바뀌었다.
$ git check-ignore -q --no-index .gitattributes; echo "rc=$?"
rc=1
$ git ls-files -z | xargs -0 git check-attr -z text eol -- | tr '\0' '|'
.gitattributes|text|auto|.gitattributes|eol|lf|.gitignore|text|auto|...
$ git ls-files --eol
i/lf w/lf attr/text=auto eol=lf .gitattributes
i/lf w/lf attr/text=auto eol=lf .gitignore
i/lf w/lf attr/text=auto eol=lf a.sh
i/lf w/lf attr/text=auto eol=lf b.sh
$ git add --renormalize . ; git diff --cached --name-only | wc -l
0
출력의 |는 NUL을 눈으로 보려고 바꾼 문자다.
재정규화 뒤 스테이지된 파일이 0개인 것은 이 합성 저장소에서도 변환할 것이 없었다는 뜻이다.
그다음 CRLF 파일을 하나 더해서 어떻게 보이는지 확인했다.
$ printf 'x\r\ny\r\n' > c.sh; git add -f c.sh
warning: in the working copy of 'c.sh', CRLF will be replaced by LF the next time Git touches it
$ git ls-files --eol c.sh
i/lf w/crlf attr/text=auto eol=lf c.sh
색인은 i/lf로 정규화되고 작업 트리는 w/crlf로 남는다.
이 경우 시험이 색인 열만 보면 통과하고, 작업 트리의 CRLF는 잡지 못한다.
기록의 시험이 w/ 열을 단언하지 않는다면 같은 빈틈이 있다는 추정이고, 그 여부는 기록으로 확인하지 못했다.
CRLF가 셸 스크립트에 왜 문제인지도 같은 Mac에서 봤다.
CRLF로 저장한 짧은 스크립트(echo hi와 exit 0)를 bash 3.2.57로 돌렸더니 hi를 출력한 뒤 exit: 0: numeric argument required(앞에 붙은 스크립트 이름과 줄 번호는 생략)로 종료 코드 255가 나왔다.
줄 끝의 캐리지 리턴이 인자 0에 붙은 모양이다.
Windows 체크아웃에서 같은 일이 생기는지는 이 글이 관찰한 것이 아니다.
마지막으로 Windows처럼 core.autocrlf=true를 켠 환경을 흉내 냈다.
* text=auto eol=lf를 커밋한 저장소에서 파일을 지우고 git -c core.autocrlf=true checkout -- s.sh를 하자 git ls-files --eol이 w/lf를 보였다.
od -c로 본 파일 끝도 \n이었다.
설정은 Windows와 같지만 운영체제는 macOS이므로, 속성이 설정을 이긴다는 문서의 설명과 방향이 같다는 정도로만 읽는다.
다른 저장소의 같은 작업
같은 날 다른 하네스 설정 저장소에서도 같은 두 줄 변경을 했다. 숫자가 달라서 위 숫자와 섞지 않는다. 점검 결과는 이렇다.
.gitattributes가 정확히* text=auto eol=lf와 일치했고,git check-ignore -q .gitattributes가 종료 코드 1이었다.git ls-files --eol의 24행이 모두i/lf w/lf attr/text=auto eol=lf였고 어긋난 행은 0개였다.- 커밋 뒤
git add --renormalize . && git diff --cached --name-only가 비어 있었다. - 커밋 전에 같은 명령을 돌렸을 때는 새로 만든 파일 두 개가 스테이지 목록에 나왔다. 아직 스테이지만 된 새 변경이라서 나온 예상된 출력이었다.
이 저장소의 변경도 두 파일이었고, 허용 목록 .gitignore에 같은 재포함 한 줄을 더했다.
GPT 계열 검토는 다섯 차원 모두 10점이었다.
이 비교는 같은 두 줄이 서로 다른 허용 목록 저장소에서 모두 필요했다는 사실까지만 보여 준다.
확인하지 못한 것
결론 전에 이 기록이 닿지 않는 부분을 분리한다.
- Windows에서 이 속성이 실제로 LF 체크아웃을 만드는지는 이 기록의 시험 단계에 없다. 시험은 macOS의 Git에서 속성과 색인을 읽었다.
- 이후 별도 실행에서 Windows 네이티브 격리 시험을 고친 뒤 Claude 쪽 셸 스위트 9개와 Codex 쪽 4개가 통과했다는 기록이 있다. 그 9개에 이 줄끝 시험이 들어 있는지는 읽은 기록에 없다.
- 시험은 색인 열을 확인한다고 적혀 있다. 작업 트리 열(
w/)을 단언하는지는 확인하지 못했다. 합성 저장소에서 본 것처럼 두 열은 서로 다를 수 있다. - 파일별 반복은
.gitattributes가 추적되기 전에는 그 파일 자신을 덮지 못한다. 논리 단계 이후에야 덮는다. - shellcheck는 로컬에서 돌지 않았고 CI가 v0.11로 돌렸다.
- 병합 시각과 앞에서 설명한 51번째 파일의 이유는 기록에 없다.
다음에 쓸 점검
아래는 이번 일에서 나온 권장 절차이고, 이 순서로 모두 실행해 성공했다는 뜻이 아니다.
- 속성을 넣기 전에
git ls-files --eol로 색인이 이미 LF인지, 이진 파일이 있는지 센다. 이번에는 49개가 모두i/lf였다. - 허용 목록
.gitignore를 쓰는 저장소는git check-ignore -q --no-index .gitattributes가 종료 코드 1인지 먼저 본다. - 시험을 먼저 쓰고 의도된 이유로 RED가 나는지 본다. 파일 없음, 무시됨, 속성 미지정이 이유여야 하고, 가져오기 오류는 RED로 세지 않는다.
- RED와 GREEN을 기록할 때 추적 파일 수를 함께 적는다. 이번에는 49에서 50으로 늘며 실패가 100에서 102로 변했다.
- Windows에서 쓸 저장소라면 Windows에서 새로 체크아웃해
git ls-files --eol의w/열을 읽는 단계를 따로 더한다. 이번 기록에는 이 단계가 없다.
git ls-files --eol
git check-ignore -q --no-index .gitattributes; echo "rc=$?"
git ls-files -z | xargs -0 git check-attr -z text eol --
위 세 명령은 합성 저장소에서 실제로 돌려 값을 읽은 것과 같은 형태다. 실제 하네스 저장소에서 같은 명령을 지금 다시 돌린 결과는 이 글에 없다.
시험을 먼저 쓰고 반대 계열 모델이 검토하는 구조는 작성자와 검증자를 나누는 교차 검증 글에도 나온다. 이번 글은 그 가운데 줄끝 속성 하나를 시험으로 묻는 경우만 다뤘다. 더 많은 기록은 프로그래밍 카테고리에 모아 둔다.
자주 묻는 질문
text=auto만 쓰면 Windows에서도 LF가 유지되나요?
문서에 따르면 보장되지 않는다. eol이 지정되지 않고 core.autocrlf와 core.eol도 없으면 Windows의 기본은 eol=crlf다. 이 글의 저장소는 eol=lf까지 적었다. Windows에서 직접 체크아웃해 확인한 결과는 이 기록에 없다.
색인이 이미 LF인데 시험이 왜 필요한가요?
현재 내용이 LF라는 것과 속성이 모든 파일에 적용된다는 것은 다르기 때문이다. 이번 기준선에서 색인 확인 1개는 통과했고, 속성 확인 98개와 파일 확인 2개는 실패해 1 passed 100 failed가 나왔다.
허용 목록 .gitignore에서는 무엇을 더해야 하나요?
새 .gitattributes가 무시되지 않도록 재포함 줄 !/.gitattributes를 더해야 했다. 이 줄이 없으면 시험의 무시되지 않음 확인이 실패하고, 합성 저장소에서는 git add가 거부됐다.
이 시험이 작업 트리의 CRLF도 잡나요?
알 수 없다. 기록은 시험이 색인 열을 확인한다고만 적고 w/ 열 단언은 적지 않았다. 합성 저장소에서는 색인이 i/lf이고 작업 트리가 w/crlf인 파일이 나왔다.
참고한 자료
- Git gitattributes 문서: text, eol, 줄끝 변환 절(조회 2026-11-05, 페이지 표시 2.56.0)
- git-check-attr 문서: -z 옵션과 출력 형식(조회 2026-11-05, 페이지 표시 2.52.0)
- git-ls-files 문서: --eol 옵션과 -z(조회 2026-11-05, 페이지 표시 2.55.0)
이 글의 초안은 AI(Claude)가 하네스 작업 기록과 위 문서를 읽고 작성했다. 합성 저장소 실험은 그 작성 과정에서 이 Mac(Git 2.54.0)에서 실행했다. 사람이 문장마다 검수했다는 뜻은 아니다.
아직 댓글이 없습니다.