이메일이 여러 개인 사람을 한 명으로 합치는 방법
같은 사람이 집과 학교에서 다른 이메일로 커밋하면 기여자 목록에 두 번 나와요. 커밋 작성자를 GitHub 계정으로 이어 붙이는 규칙을 정리했어요.
기여도를 계산하다 보면 가장 먼저 만나는 문제가 있어요. 세 명이 만든 프로젝트인데 기여자가 다섯 명으로 나오는 것이에요. 원인은 대부분 같아요. 한 사람이 여러 이메일로 커밋했기 때문이에요.
커밋에는 계정이 아니라 이름과 이메일이 적혀요
Git 커밋에 남는 작성자 정보는 GitHub 계정이 아니에요. 컴퓨터에 설정해 둔 user.name과 user.email이에요. GitHub는 푸시된 커밋의 이메일을 보고, 그 이메일을 등록해 둔 계정이 있으면 커밋과 계정을 이어 줘요.
그래서 이런 일이 생겨요.
- 집 컴퓨터와 학교 컴퓨터에 서로 다른 이메일이 설정되어 있어요.
- 새 컴퓨터에 Git을 처음 깔고 이메일을 대충 적었어요.
- 계정에 등록하지 않은 회사 이메일로 커밋했어요.
GitHub API로 커밋을 받아 보면 계정이 연결된 커밋에는 author.login이 있고, 연결되지 않은 커밋은 author가 비어 있어요. 비어 있는 커밋에는 이름과 이메일만 남아요.
이미 아는 정보로 주인을 찾아요
다행히 같은 레포 안에는 단서가 많아요. 계정이 연결된 커밋들에서 "이 이메일과 이 이름은 이 계정"이라는 대응표를 먼저 만들어요.
for (const commit of commits) {
const login = commit.author?.login
const git = commit.commit.author
if (login) knownLogins.set(login.toLowerCase(), login)
if (!login || !git) continue
if (git.email) loginByEmail.set(git.email.toLowerCase(), login)
if (git.name) {
const set = loginsByName.get(git.name) ?? new Set<string>()
set.add(login)
loginsByName.set(git.name, set)
}
}
그런 다음 계정이 없는 커밋을 만나면 아래 순서로 주인을 찾아요.
- 같은 이메일: 다른 커밋에서 이 이메일이 어떤 계정과 연결된 적이 있으면 그 계정이에요.
- noreply 주소:
12345+yunh03@users.noreply.github.com같은 주소에는 계정 아이디가 그대로 들어 있어요. - 같은 이름: 이 이름을 쓴 계정이 레포 안에 딱 하나뿐이면 그 계정이에요.
- 계정 아이디와 같은 이름: 작성자 이름이 참여 중인 계정의 아이디와 같으면 그 계정이에요.
const NOREPLY_PATTERN = /^(?:\d+\+)?([^@]+)@users\.noreply\.github\.com$/
function resolveLogin(commit: CommitSummary): string | null {
if (commit.author?.login) return commit.author.login
const git = commit.commit.author
if (!git) return null
const email = git.email?.toLowerCase() ?? ''
const byEmail = loginByEmail.get(email)
if (byEmail) return byEmail
const noreply = NOREPLY_PATTERN.exec(email)
if (noreply) return noreply[1]
const byName = loginsByName.get(git.name)
if (byName?.size === 1) return [...byName][0]
return knownLogins.get(git.name?.trim().toLowerCase() ?? '') ?? null
}
순서가 중요한 이유
확실한 단서부터 쓰고, 추측에 가까운 단서는 뒤로 미뤄요.
이메일은 한 사람만 쓰니 가장 믿을 만해요. 반면 이름은 겹칠 수 있어요. 한 팀에 "민수"가 두 명일 수도 있어요. 그래서 이름으로 찾을 때는 그 이름을 쓴 계정이 하나뿐일 때만 합쳐요. 두 계정이 같은 이름을 썼다면 누구 것인지 알 수 없으니 합치지 않아요.
잘못 합치는 것은 합치지 못하는 것보다 나빠요. 한 사람이 둘로 나뉘면 어색할 뿐이지만, 두 사람이 하나로 합쳐지면 한 사람의 기여가 통째로 사라지기 때문이에요.
끝내 찾지 못한 작성자
네 단계를 다 거쳐도 계정을 찾지 못하는 작성자가 있어요. GitHub 계정이 없거나, 한 번도 계정에 연결된 적 없는 이메일만 쓴 경우예요.
이때는 작성자 이름을 키로 삼아서, 이름이 같으면 한 사람으로 묶어요.
const key = login
? `u:${login.toLowerCase()}`
: `n:${(git?.name?.trim() || git?.email || 'unknown').toLowerCase()}`
키 앞에 u:와 n:을 붙인 이유는 계정 아이디와 작성자 이름이 우연히 같은 글자일 때 섞이지 않게 하려는 것이에요.
통계 API가 엉뚱한 사람에게 준 커밋 되찾기
여기까지는 커밋을 하나씩 읽을 때의 이야기예요. 빠른 길인 통계 API를 쓸 때는 다른 문제가 있어요. 통계 API는 이미 계정 기준으로 합쳐진 숫자를 주는데, 가끔 계정이 연결되지 않은 커밋을 전혀 다른 계정의 몫으로 돌려줘요.
증상은 이래요. 통계에는 어떤 계정이 커밋 12개를 한 것으로 나오는데, 커밋 목록 어디에도 그 계정이 작성자로 나오지 않아요.
그래서 커밋 목록과 대조해요.
- 커밋 목록에서 한 번이라도 작성자로 나온 계정을 모아요.
- 계정이 없는 커밋은 위의 규칙으로 주인을 찾아서, 주인별로 몇 개인지 세요.
- 통계에는 있는데 커밋 목록에는 없는 계정이 있고, 그 계정의 커밋 수와 정확히 같은 수의 주인 없는 커밋이 다른 참여자의 것으로 확인되면 그 참여자에게 합쳐요.
수가 정확히 맞고, 맞는 후보가 한 명뿐일 때만 합쳐요. 조건을 일부러 빡빡하게 잡았어요. 애매할 때는 손대지 않는 편이 안전해요.
이 방법은 커밋 목록 전체를 받았을 때만 쓸 수 있어요. 그래서 커밋이 300개 미만인 레포에서만 적용해요.
봇과 병합 커밋은 빼요
사람을 합치는 것만큼 중요한 것이 사람이 아닌 것을 빼는 일이에요.
const isBot = (account: { login: string; type: string } | null) =>
!!account && (account.type === 'Bot' || account.login.endsWith('[bot]'))
const isMergeCommit = (commit: CommitSummary) => commit.parents.length > 1
의존성을 올려 주는 봇은 계정 종류가 Bot이거나 아이디가 [bot]으로 끝나요. 병합 커밋은 부모 커밋이 두 개 이상이에요. 브랜치를 합칠 때 자동으로 생기는 커밋이라, 세면 병합 버튼을 누른 사람의 커밋 수만 늘어나요.
사용자 쪽에서 할 수 있는 일
코드로 최대한 합치지만, 가장 확실한 방법은 커밋한 이메일을 GitHub 계정에 등록하는 것이에요. GitHub 설정의 Emails에서 이메일을 추가하면 그 이메일로 올린 지난 커밋까지 계정에 연결돼요.
분석 결과에 내 이름이 두 번 나온다면 한 번 확인해 보세요.