# Git identity를 디렉터리에 매핑하기

- URL: https://soohan.dev/projects/personal/git-user-mapper
- Category: projects/personal
- Published: 2026-07-31
- Tags: git, cli, typescript, shell
- Author: Soohan Park

> 저장소마다 Git identity를 바꾸는 대신 디렉터리를 프로파일에 매핑하고, Git과 셸 프롬프트가 같은 사용자를 가리키도록 만든 CLI 프로젝트.

---

업무용과 개인용 Git 계정을 함께 쓰면 저장소마다 `user.name`과 `user.email`을 구분해야 합니다.
전역 설정 하나만 쓰기는 어렵고, 저장소를 만들거나 clone할 때마다 로컬 설정을 바꾸는 방식은
같은 선택을 계속 반복하게 합니다. 설정을 잊거나 다른 프로파일을 고를 가능성도 선택하는 사람의
기억에 남습니다.

저장소들은 이미 `~/dev/work`, `~/dev/personal`처럼 용도에 따라 나눠 두고 있었습니다.
그렇다면 identity도 저장소마다 고르는 대신 디렉터리에 한 번 연결하는 편이 자연스러웠습니다.
[git-user-mapper](https://github.com/soohanpark/git-user-mapper)는 이 경계를 Git 설정과 셸
프롬프트에 함께 적용하기 위해 만든 CLI입니다.

## 저장소마다 반복되던 선택

출발점은 [geongeorge/Git-User-Switch](https://github.com/geongeorge/Git-User-Switch)였습니다.
프로파일을 등록해 두고 대화형으로 고르는 흐름을 제공하지만, 선택한 identity는 현재 저장소의
`.git/config`에 기록합니다. 새 저장소에서는 다시 도구를 실행해야 하고, 이미 설정한 저장소가
늘어날수록 어느 곳에 어떤 값이 들어갔는지 확인할 대상도 늘어납니다.

원본의 저장소 이력과 MIT 라이선스를 유지해 포크하되, 설정 모델은 바꿨습니다. 프로파일을
고르는 경험은 이어받고 identity의 적용 단위를 개별 저장소에서 상위 디렉터리로 옮겼습니다.
패키지 이름이 바뀌면서 저장 위치가 달라진 기존 프로파일은 처음 실행할 때 한 번 가져오지만,
원본 도구가 각 저장소에 써 둔 로컬 설정을 새 매핑이라고 추측하지는 않습니다. 로컬 `[user]`는
조건부 설정보다 우선하므로, 그대로 남아 있다면 `status`에서 override로 드러내도록 했습니다.

구현도 기존 JavaScript를 조금씩 고치는 대신 TypeScript 7과 ESM을 기준으로 다시 구성했습니다.
다만 포크의 핵심은 언어 전환보다 소유하는 설정의 범위를 바꾼 데 있습니다. 새 도구는 저장소의
`.git/config`를 쓰지 않고, 자신이 만든 전역 조건과 프로파일 파일만 관리합니다.

## 디렉터리를 identity 경계로

Git은 전역 설정에서 `includeIf "gitdir:"` 조건을 만나면 저장소 경로에 따라 다른 설정 파일을
포함할 수 있습니다. `~/dev/personal`을 개인 프로파일에 매핑하면 그 아래의 저장소에서는 개인
이름과 이메일을 읽고, 다른 경로에서는 기본 프로파일을 사용하게 만들 수 있습니다.

한 번 매핑한 뒤에는 새로 clone한 저장소에서도 `git-mapper`를 다시 실행할 필요가 없습니다.
identity를 선택하는 주체가 별도 셸 스크립트가 아니라 Git이므로 터미널뿐 아니라 IDE와 GUI
클라이언트에서도 같은 설정이 적용됩니다.

프로파일과 경로 목록은 도구의 스토어 한 곳에 두고, `sync`가 실제로 쓰이는 세 가지 파생물을
만듭니다.

```text
프로파일과 경로 매핑
        ↓ sync
프로파일 gitconfig · includeIf · 프롬프트용 mapping.tsv
        ↓
Git이 identity 선택 · 셸이 현재 프로파일 표시
```

프로파일 파일에는 이름과 이메일, 선택한 경우 signing key를 기록합니다. 전역 Git 설정에는
경로와 프로파일 파일을 잇는 `includeIf`만 추가하고, `mapping.tsv`에는 셸이 읽을 경로와
프로파일을 경로 길이순으로 기록합니다. 파생물이 지워지거나 어긋나도 스토어를 기준으로 다시
만들 수 있도록 `sync`는 같은 입력에 같은 결과를 내게 했습니다.

경로 규칙은 glob이 아닌 절대경로 디렉터리 접두사로 제한했습니다. Git의 `gitdir`는 glob과
비슷한 `wildmatch` 문법을 지원하지만, 이를 그대로 허용하면 Git과 셸이 같은 패턴을 서로 다르게
해석할 수 있습니다. 중첩된 매핑은 더 긴 경로를 먼저 찾고, 디렉터리 이름에 들어간 `*`, `?`,
대괄호는 Git 조건에서 리터럴로 이스케이프합니다. 표현력을 조금 줄이는 대신 두 해석기의 규칙을
작고 검증 가능한 범위에 두었습니다.

## 프롬프트가 Git과 같은 답을 하게

경로 매핑이 자동으로 동작해도 현재 어떤 identity가 적용되는지 보이지 않으면 다시 확인 명령을
실행하게 됩니다. 그래서 `git-mapper shell-init zsh` 또는 `bash`가 생성한 스니펫으로 현재
프로파일을 프롬프트에 표시합니다.

이 기능에서 중요했던 것은 표시 자체보다 정확성이었습니다. 프롬프트가 `personal`이라고
보여주는데 Git이 다른 이메일을 사용한다면 없는 편보다 나쁩니다. 코어 매퍼와 셸 스니펫은
저장소 루트에서 가장 구체적인 경로 매핑을 찾고, 없으면 기본 identity로 돌아가는 같은 규칙을
각각 구현합니다.

저장소의 `.git/config`에 로컬 `[user]`가 있으면 그 값이 조건부 include를 덮습니다. 셸은 이
경우를 먼저 확인해 `local-override`로 표시하고, `git-mapper status`도 도구가 계산한 값과 Git이
실제로 반환한 값을 나란히 확인합니다. 기본 프로파일이 없더라도 기존 전역 `[user]`가 있다면
identity 없음으로 표시하지 않습니다. 도구가 관리하지 않는 값과 실제로 존재하지 않는 값을
구분했습니다.

프롬프트가 열릴 때마다 Node CLI나 `git`을 실행하지는 않습니다. zsh는 셸 내장 기능만으로 작은
`mapping.tsv`와 저장소 설정을 읽어 외부 프로세스를 띄우지 않습니다. bash도 외부 명령은
실행하지 않고 물리 경로를 얻기 위한 내장 `pwd`의 서브셸 하나만 사용합니다. 설정을 바꾸는
빈도보다 프롬프트가 다시 그려지는 빈도가 훨씬 높기 때문에, 쓰기 시점에 조회 테이블을 생성하는
쪽을 택했습니다.

## 사용자의 Git 설정을 직접 조립하지 않기

이 도구는 자격증명이나 URL 재작성 규칙이 함께 들어 있을 수 있는 전역 Git 설정을 수정합니다.
필요한 줄만 추가하는 작업처럼 보여도 설정 파일 전체를 문자열로 읽고 다시 쓰면 인용부호와
이스케이프, 기존 포맷을 도구가 책임지게 됩니다.

그래서 `~/.gitconfig`와 포함되는 프로파일 파일 모두 설정 텍스트를 직접 조립하지 않습니다.
모든 쓰기는 인자 배열로 `git config`를 호출해 Git 자체의 파서와 직렬화 규칙에 맡깁니다.
빈 이름이나 이메일, 탭과 줄바꿈처럼 파생 파일의 구조를 바꾸는 값은 첫 변경 전에 거부합니다.

삭제 범위도 도구가 스토어에 기록한 `managedConditions`로 제한했습니다. 사용자가 직접 만든 다른
`includeIf`는 읽거나 지우지 않습니다. 변경 직전에는 Git이 실제로 사용하는 전역 설정 파일을
백업하고, 이메일이나 자격증명이 포함될 수 있으므로 백업과 조회 테이블의 권한도 제한합니다.
동기화 도중 실패하더라도 다음 `sync`가 추가한 조건을 회수할 수 있도록, 만들 조건을 실제 쓰기
전에 관리 목록에 기록합니다.

## 실행한 결과만 기능으로

테스트는 파일에 예상 문자열이 들어갔는지만 보지 않습니다. 임시 `HOME`과 실제 Git 저장소를
만들고 `sync`한 뒤, 각 경로에서 `git config user.email`이 어떤 값을 반환하는지 확인합니다.
`[user]`와 `includeIf`의 순서, 매핑 제거 뒤 기본 프로파일 복귀, 특수문자가 든 디렉터리 이름도
같은 방식으로 검증합니다.

프롬프트에는 별도의 패리티 테스트를 뒀습니다. 같은 경로와 설정 픽스처를 코어 매퍼, 생성된
zsh·bash 스니펫, 실제 Git에 각각 입력하고 결과가 일치하는지 비교합니다. fish 스니펫도 한때
구현했지만 실행할 수 있는 fish 바이너리가 없어 관련 테스트가 모두 건너뛰어졌습니다. 패리티를
확인하지 못한 셸을 지원한다고 말할 수는 없다고 판단해 1.0.0 전에 제거했습니다.

현재 테스트 결과는 158개 통과, 실패 0개, 건너뜀 0개입니다. 테스트가 초록색이라는 사실뿐
아니라 지원한다고 적은 두 셸의 패리티 테스트가 실제로 실행됐는지도 함께 확인한 결과입니다.

## 한 번 매핑하고 잊기

[git-user-mapper 1.0.0](https://www.npmjs.com/package/git-user-mapper)은 2026년 7월 31일 npm에
공개했습니다. 지금은 업무와 개인 디렉터리를 각 프로파일에 연결하고, 프롬프트에서 현재 적용값을
확인하며 사용하고 있습니다.

저장소마다 identity를 선택하는 절차를 더 빠르게 만드는 대신, 그 선택 자체를 상위 디렉터리의
규칙으로 옮겼습니다. 새 저장소를 만들 때 할 일이 하나 줄었고, Git이 쓰는 값과 화면에 보이는
값을 같은 규칙과 테스트로 묶었습니다. 반복하던 선택을 기억에서 설정으로 옮긴 것이 이 포크에서
가장 크게 바꾼 부분입니다.
