여러 Git 저장소를 한 번에 Pull하는 방법: find·xargs·PowerShell 실전 가이드
프론트엔드, 백엔드, 배치, 인프라처럼 프로젝트가 여러 저장소로 나뉘어 있으면 작업을 시작할 때마다 같은 명령을 반복하게 됩니다.
cd frontend && git pull
cd ../backend && git pull
cd ../batch && git pull
저장소가 두세 개라면 괜찮지만, 수가 늘어날수록 빠뜨리는 프로젝트가 생기고 현재 어느 저장소를 갱신하는지도 헷갈리기 쉽습니다. 이럴 때 상위 폴더에서 Git 저장소를 자동으로 찾은 뒤 git pull을 순차 또는 병렬로 실행하면 작업을 크게 줄일 수 있습니다.
이 글에서는 macOS·Linux·Windows Git Bash에서 사용할 수 있는 find와 xargs, Windows PowerShell 명령을 함께 살펴봅니다. 단순히 명령만 나열하지 않고 공백이 포함된 경로를 안전하게 처리하는 방법, git pull --ff-only를 사용하는 이유, 실패한 저장소를 확인하는 방법까지 설명합니다.
먼저 결론: 어떤 방법을 선택하면 될까?
| 상황 | 권장 방법 | 특징 |
|---|---|---|
| 저장소가 많지 않거나 처음 실행할 때 | find 순차 실행 |
로그를 읽기 쉽고 오류 원인을 찾기 편함 |
| 저장소가 많고 다운로드 대기 시간이 길 때 | xargs -P 4 병렬 실행 |
최대 4개 저장소를 동시에 갱신 |
| Windows PowerShell 5.1을 사용할 때 | Get-ChildItem + ForEach-Object |
별도 도구 없이 순차 실행 |
| PowerShell 7 이상을 사용할 때 | ForEach-Object -Parallel |
ThrottleLimit으로 동시 실행 수 제한 |
| 매일 여러 저장소를 관리할 때 | myrepos |
등록한 저장소의 상태·업데이트를 반복 관리 |
처음에는 순차 실행으로 저장소 목록과 인증 상태를 확인한 뒤, 문제가 없을 때 병렬 실행으로 전환하는 것이 가장 안정적입니다.
실행 전에 알아둘 점
git pull은 다운로드만 하는 명령이 아니다
git pull은 원격 변경 사항을 가져오는 git fetch를 실행한 다음, 현재 브랜치에 그 변경을 통합합니다. 즉, 로컬 브랜치가 실제로 변경될 수 있습니다. 인자 없이 실행하면 현재 브랜치에 설정된 upstream을 대상으로 하며, 저장소 안의 모든 로컬 브랜치를 한꺼번에 갱신하는 명령은 아닙니다.
원격 상태만 미리 받아 보고 싶다면 다음 명령이 더 보수적입니다.
git fetch --all --prune
--all: 등록된 모든 원격 저장소를 가져옵니다.--prune: 원격에서 삭제된 브랜치에 대응하는 오래된 원격 추적 참조를 정리합니다.
일괄 작업에서 자동 병합이나 자동 리베이스를 피하고 싶다면 git pull --ff-only를 명시하는 편이 좋습니다. 로컬과 원격 이력이 갈라진 저장소는 자동으로 합치지 않고 실패로 남기므로, 해당 저장소만 따로 확인할 수 있습니다.
다만 --ff-only는 분기된 이력에서 자동 병합을 막는 옵션이지, 수정 중인 작업 트리를 보호하는 기능은 아닙니다. staged·unstaged·untracked 변경은 별도로 검사해야 합니다.
최신 Git은 분기된 브랜치의 조정 방식을 명시하도록 안내합니다. Git 버전과 사용자 설정에 따른 모호함을 없애기 위해 이 글의 명령에서는
--ff-only를 명시합니다.
로컬 변경 사항을 먼저 확인하자
여러 저장소를 한 번에 갱신하기 전에 수정 중인 파일과 현재 브랜치를 확인하는 것이 좋습니다.
find . -type d -name .git -prune -exec sh -c '
for git_dir do
repo=${git_dir%/.git}
printf "\n[Status %s]\n" "$repo"
git -C "$repo" status --short --branch
done
' sh {} +
출력에 M, A, ?? 등이 표시되면 커밋하지 않은 변경 사항이 있다는 뜻입니다. 모든 저장소에 자동으로 stash를 적용하는 방식은 작업 내용을 숨기거나 복구를 어렵게 만들 수 있으므로 권장하지 않습니다. 위 명령은 상태를 보여 주기만 하므로, 사람이 확인하지 않는 cron·CI 작업에는 다음과 같은 사전 검사가 필요합니다.
무인 실행이라면 dirty·detached HEAD·upstream을 검사하자
다음 Bash 스크립트는 먼저 저장소 목록을 모두 수집한 다음, 수정 사항이 있거나 detached HEAD 상태이거나 upstream이 없는 저장소를 건너뜁니다. 탐색 중에 Pull이 작업 트리를 바꾸지 않도록 “탐색”과 “업데이트” 단계도 분리했습니다.
#!/usr/bin/env bash
root=${1:-.}
markers=()
failed=0
while IFS= read -r -d '' marker; do
markers+=("$marker")
done < <(find -P "$root" -type d -name .git -prune -print0)
for marker in "${markers[@]}"; do
repo=${marker%/.git}
if ! git -C "$repo" symbolic-ref --quiet HEAD >/dev/null 2>&1; then
printf "[SKIP detached HEAD] %q\n" "$repo"
continue
fi
if ! git -C "$repo" rev-parse --verify --quiet '@{upstream}' >/dev/null; then
printf "[SKIP no upstream] %q\n" "$repo"
continue
fi
if ! state=$(git -C "$repo" status \
--porcelain=v1 \
--untracked-files=normal \
--ignore-submodules=none); then
printf "[FAILED status] %q\n" "$repo" >&2
failed=1
continue
fi
if [[ -n $state ]]; then
printf "[SKIP dirty] %q\n" "$repo"
continue
fi
printf "\n[Updating %s]\n" "$repo"
if ! GIT_TERMINAL_PROMPT=0 \
git -C "$repo" pull --ff-only --no-recurse-submodules; then
printf "[FAILED pull] %q\n" "$repo" >&2
failed=1
fi
done
exit "$failed"
이 예제는 Bash 배열과 [[ ... ]]을 사용하므로 sh script.sh가 아니라 bash script.sh로 실행해야 합니다. GIT_TERMINAL_PROMPT=0은 무인 실행에서 Git의 자격 증명 질문을 기다리지 않고 실패하게 하지만, 외부 SSH 프로그램이 만드는 모든 프롬프트까지 차단하는 것은 아닙니다.
macOS·Linux·Git Bash에서 순차로 Pull하기
업데이트할 저장소들이 들어 있는 최상위 폴더로 이동합니다.
cd ~/workspace
먼저 실제로 어떤 저장소가 발견되는지 확인합니다.
find . -type d -name .git -prune -exec sh -c '
for git_dir do
printf "%s\n" "${git_dir%/.git}"
done
' sh {} +
목록이 예상과 같다면 다음 명령으로 하나씩 업데이트합니다.
find . -type d -name .git -prune -exec sh -c '
for git_dir do
repo=${git_dir%/.git}
printf "\n[Updating %s]\n" "$repo"
git -C "$repo" pull --ff-only ||
printf "[FAILED] %s\n" "$repo" >&2
done
' sh {} +
이 명령은 한 저장소의 Pull이 실패하더라도 [FAILED]를 표시하고 다음 저장소를 계속 처리합니다.
명령을 부분별로 이해하기
find .: 현재 폴더부터 하위 항목을 탐색합니다.-type d -name .git: 이름이.git인 디렉터리를 찾습니다.-prune: 찾은.git내부까지 불필요하게 다시 탐색하지 않습니다.-exec sh -c '...' sh {} +: 찾은 경로를 셸 코드에 직접 삽입하지 않고 위치 인자로 전달합니다.repo=${git_dir%/.git}: 경로 끝의/.git을 제거해 저장소 루트 경로를 얻습니다.git -C "$repo": 현재 셸의 위치를 바꾸지 않고 해당 폴더에서 Git을 실행합니다.pull --ff-only: fast-forward가 가능한 경우에만 현재 브랜치를 업데이트합니다.
경로를 위치 인자로 전달하고 변수를 큰따옴표로 감쌌기 때문에 폴더 이름에 공백, 한글, 작은따옴표가 있어도 안전하게 처리할 수 있습니다.
왜 cd 대신 git -C를 사용할까?
다음과 같이 매번 폴더를 이동해도 동작은 합니다.
cd "$repo" && git pull
하지만 반복문에서 현재 위치가 계속 바뀌면 실패 이후의 흐름을 추적하기 어려워질 수 있습니다. git -C <경로>는 “해당 경로에서 Git을 실행한 것처럼” 동작하므로 원래 작업 위치를 유지하면서 여러 저장소를 다루기에 적합합니다.
pwd와 $(pwd)는 무엇이 다를까?
pwd는 Print Working Directory의 약자로 현재 작업 중인 폴더의 경로를 출력합니다.
pwd
예를 들어 다음과 같이 출력될 수 있습니다.
/Users/example/workspace/backend-api
$(pwd)는 pwd 명령의 출력 결과를 다른 문자열 안에 넣는 명령 치환입니다.
printf "[Updating %s]\n" "$(pwd)"
따라서 저장소로 먼저 이동한 뒤 $(pwd)를 출력하면 현재 어떤 저장소를 처리하는지 확인할 수 있습니다. 중첩된 sh -c 명령 안에서 실행 시점을 늦추려면 바깥 큰따옴표 문맥에서 \$(pwd)처럼 이스케이프하는 예제도 볼 수 있습니다.
다만 이 글의 권장 명령은 폴더를 이동하지 않고 git -C를 사용하므로 $(pwd) 대신 이미 확보한 $repo 경로를 출력합니다. 이 방식이 따옴표와 실행 시점을 이해하기 더 쉽습니다.
xargs -P 4로 여러 저장소 병렬 업데이트하기
저장소가 수십 개라면 네트워크 응답을 기다리는 시간이 누적됩니다. 다음 명령은 최대 4개 저장소를 동시에 처리합니다.
find . -type d -name .git -prune -print0 |
xargs -0 -n 1 -P 4 sh -c '
[ "$#" -eq 1 ] || exit 0
repo=${1%/.git}
printf "\n[Pulling %s]\n" "$repo"
git -C "$repo" pull --ff-only
' sh
공백이 있는 경로를 안전하게 처리하는 핵심
일반적인 파이프는 줄바꿈을 기준으로 경로를 나누기 때문에 공백이나 특수문자가 포함된 폴더에서 오류가 생길 수 있습니다.
find ... -print0: 각 경로의 끝을 NUL 문자로 구분합니다.xargs -0: NUL 문자로 구분된 입력을 그대로 읽습니다.-n 1: 한 번의 셸 실행에 저장소 하나만 전달합니다.-P 4: 최대 4개의 프로세스를 동시에 실행합니다.
-P 4는 무조건 큰 숫자로 바꾼다고 빨라지지 않습니다. 원격 서버의 요청 제한, 디스크 I/O, SSH 인증 처리, 네트워크 대역폭이 병목이 될 수 있습니다. 일반적인 개발 PC에서는 4부터 시작하고, 문제가 없다면 6~8 정도로 조금씩 늘리는 편이 좋습니다.
병렬 방식은 서로 독립적인 형제 저장소에만 사용하는 것이 좋습니다. 부모 저장소와 그 안의 중첩 저장소, 동일한 Git 공용 디렉터리를 공유하는 worktree, 상위 저장소와 서브모듈을 같은 병렬 그룹에 넣으면 서로의 작업 트리나 잠금에 영향을 줄 수 있습니다. 여러 저장소 갱신은 하나의 원자적 트랜잭션도 아니므로, 중간에 하나가 실패해도 이미 성공한 저장소는 자동으로 이전 상태로 돌아가지 않습니다.
병렬 실행 중에는 여러 저장소의 로그가 섞여 보일 수 있습니다. 오류 원인을 자세히 봐야 하거나 인증 입력이 필요하다면 먼저 순차 방식으로 실행하세요.
Windows PowerShell에서 재귀적으로 Pull하기
단순히 Get-ChildItem -Directory만 사용하면 현재 위치 바로 아래의 모든 폴더에 Git 명령을 시도하고, 더 깊은 위치의 저장소는 놓칠 수 있습니다. .git 디렉터리만 재귀적으로 찾도록 조건을 지정해야 합니다. 아래 PowerShell 예제도 일반 clone처럼 .git이 디렉터리인 저장소를 대상으로 하며, .git이 파일인 worktree·서브모듈은 의도적으로 제외합니다.
Windows PowerShell 5.1에서도 가능한 순차 실행
$repos = Get-ChildItem -Path . -Directory -Filter .git -Recurse -Force |
ForEach-Object { $_.Parent.FullName } |
Sort-Object -Unique
$repos | ForEach-Object {
$repo = $_
Write-Host "`n[Updating $repo]" -ForegroundColor Green
git -C $repo pull --ff-only
if ($LASTEXITCODE -ne 0) {
Write-Warning "[FAILED] $repo"
}
}
-Recurse: 모든 하위 폴더를 탐색합니다.-Force: 숨김 항목인.git도 탐색 대상에 포함합니다.Parent.FullName:.git의 상위 폴더, 즉 저장소 루트 경로를 가져옵니다.$LASTEXITCODE: 직전에 실행한 Git 프로세스의 종료 코드를 확인합니다.
PowerShell 7 이상에서 4개씩 병렬 실행
PowerShell 7부터 ForEach-Object -Parallel을 사용할 수 있습니다.
$repos = Get-ChildItem -Path . -Directory -Filter .git -Recurse -Force |
ForEach-Object { $_.Parent.FullName } |
Sort-Object -Unique
$repos | ForEach-Object -Parallel {
$repo = $_
Write-Host "`n[Pulling $repo]" -ForegroundColor Cyan
git -C $repo pull --ff-only
if ($LASTEXITCODE -ne 0) {
Write-Warning "[FAILED] $repo"
}
} -ThrottleLimit 4
ThrottleLimit 4는 동시에 실행되는 스크립트 블록을 4개로 제한합니다. Windows에 기본 포함된 Windows PowerShell 5.1에서는 -Parallel을 사용할 수 없으므로 앞의 순차 명령을 사용해야 합니다.
git pull 대신 git fetch가 더 나은 경우
업무 시작 전에 원격 변경 여부만 확인하거나, 로컬 브랜치를 자동으로 움직이고 싶지 않다면 모든 저장소에 fetch만 실행하는 편이 안전합니다.
find . -type d -name .git -prune -exec sh -c '
for git_dir do
repo=${git_dir%/.git}
printf "\n[Fetching %s]\n" "$repo"
git -C "$repo" fetch --all --prune ||
printf "[FAILED] %s\n" "$repo" >&2
done
' sh {} +
이후 필요한 저장소에 들어가 다음과 같이 차이를 확인할 수 있습니다.
git status --short --branch
git log --oneline --decorate --graph --all -n 20
정리하면 다음과 같습니다.
- 로컬 브랜치를 바로 최신 상태로 맞추려면
git pull --ff-only - 원격 정보만 받아 검토한 뒤 반영하려면
git fetch --all --prune
자주 발생하는 오류와 대응 방법
로컬과 원격 브랜치가 갈라진 경우
--ff-only가 실패하는 대표적인 상황입니다. 해당 저장소로 이동해 이력을 확인한 뒤 팀 정책에 맞게 rebase 또는 merge를 선택해야 합니다.
git status
git log --oneline --graph --decorate --all -n 30
일괄 스크립트 안에서 자동으로 --rebase나 강제 초기화를 적용하면 여러 저장소의 작업 내용이 예상치 못하게 바뀔 수 있으므로 피하는 것이 좋습니다.
추적 브랜치가 설정되지 않은 경우
현재 브랜치에 upstream이 없으면 Pull 대상이 불분명하다는 오류가 발생할 수 있습니다.
git branch -vv
git branch --set-upstream-to=origin/main main
브랜치 이름과 원격 이름은 실제 저장소 구성에 맞게 바꿔야 합니다.
인증·권한 오류가 반복되는 경우
병렬 실행 전에 저장소 하나에서 git pull --ff-only가 정상 동작하는지 확인하세요. SSH 저장소라면 SSH agent, HTTPS 저장소라면 운영체제의 Git Credential Manager가 설정되어 있어야 여러 인증 프롬프트가 동시에 뜨는 일을 줄일 수 있습니다. 무인 Bash 작업에서는 GIT_TERMINAL_PROMPT=0을 Git 명령 앞에 붙여 대화형 자격 증명 요청을 즉시 실패 처리할 수 있습니다. 토큰을 원격 URL에 직접 넣는 방식은 로그와 설정에 노출될 수 있으므로 사용하지 않는 것이 좋습니다.
일부 저장소만 실패하는 경우
순차 예제는 [FAILED]를 표시한 뒤 나머지 저장소를 계속 처리합니다. 병렬 예제도 이미 시작된 작업은 계속 진행되며, xargs는 하위 명령 실패를 비정상 종료 코드로 반환할 수 있습니다. 마지막 로그에서 실패 경로를 찾아 해당 저장소만 다시 점검하세요.
로그가 너무 뒤섞이는 경우
병렬 수를 -P 2로 낮추거나 순차 실행으로 되돌리면 됩니다. 속도보다 실패 원인 확인이 중요한 최초 실행에서는 순차 방식이 더 낫습니다.
자주 사용한다면 셸 함수로 저장하기
매번 긴 명령을 입력하기 어렵다면 ~/.bashrc 또는 ~/.zshrc에 다음 함수를 추가할 수 있습니다.
git_pull_all() {
find "${1:-.}" -type d -name .git -prune -exec sh -c '
for git_dir do
repo=${git_dir%/.git}
printf "\n[Updating %s]\n" "$repo"
git -C "$repo" pull --ff-only ||
printf "[FAILED] %s\n" "$repo" >&2
done
' sh {} +
}
셸 설정을 다시 불러온 뒤 상위 폴더를 인자로 전달합니다.
source ~/.zshrc
git_pull_all ~/workspace
Bash를 사용한다면 source ~/.bashrc로 불러오면 됩니다.
저장소가 더 많다면 myrepos 활용하기
저장소 업데이트를 자주 반복한다면 명령 조합보다 다중 저장소 관리 도구가 편할 수 있습니다. myrepos는 mr 명령으로 등록한 여러 버전 관리 저장소를 한 번에 관리합니다.
macOS에서는 Homebrew로 설치할 수 있습니다.
brew install mr
Debian·Ubuntu 계열에서는 다음과 같이 설치합니다.
sudo apt install myrepos
각 저장소 안에서 한 번씩 등록합니다.
mr register
등록한 저장소 전체를 업데이트하거나 4개씩 병렬로 처리할 수 있습니다.
mr update
mr -j4 update
mr update가 각 저장소에서 실행할 명령은 설정으로 바꿀 수 있습니다. 팀마다 rebase·merge·fast-forward 정책이 다르다면 도구를 도입하기 전에 업데이트 동작을 명시적으로 맞추세요.
Git worktree와 서브모듈은 별도로 생각해야 한다
이 글의 기본 명령은 .git이 디렉터리인 일반적인 clone 저장소를 대상으로 합니다. Git worktree와 일부 서브모듈에서는 저장소 루트의 .git이 디렉터리가 아니라 실제 Git 디렉터리를 가리키는 파일일 수 있어 -type d 탐색에 포함되지 않습니다.
서브모듈은 상위 저장소가 기록한 커밋에 맞춰 다음 명령으로 갱신하는 것이 일반적입니다.
git submodule update --init --recursive
--remote를 추가하면 기록된 커밋이 아니라 서브모듈의 원격 추적 브랜치를 기준으로 동작하므로 의미가 달라집니다. 일괄 Pull 명령에 서브모듈과 worktree를 무조건 섞기보다 각각의 관리 정책을 정한 뒤 별도로 처리하는 편이 안전합니다.
자주 묻는 질문
커밋하지 않은 변경 사항이 있어도 일괄 Pull해도 될까?
Git이 항상 거부하는 것은 아니지만, 원격 변경과 겹치면 Pull이 중단되거나 충돌이 발생할 수 있습니다. 먼저 일괄 git status --short --branch를 실행하고, 변경 사항을 커밋하거나 저장소별로 정리한 뒤 진행하세요.
git pull --ff-only는 일반 git pull과 무엇이 다른가?
fast-forward로 곧바로 이동할 수 있을 때만 업데이트하고, 로컬과 원격 이력이 갈라졌다면 자동으로 합치지 않고 실패합니다. 여러 저장소를 무인 또는 반자동으로 갱신할 때 예상치 못한 병합을 줄이는 데 유용합니다.
폴더 이름에 공백이나 한글이 있어도 동작할까?
이 글의 권장 명령은 경로를 위치 인자로 전달하고 변수를 큰따옴표로 감쌉니다. 병렬 명령도 -print0와 -0을 사용하므로 공백과 한글이 포함된 일반적인 경로를 안전하게 처리합니다.
xargs -P의 숫자는 어느 정도가 적당할까?
4부터 시작하는 것이 무난합니다. 저장소가 작고 네트워크와 인증에 문제가 없다면 6~8로 늘릴 수 있지만, 원격 서비스의 요청 제한이나 디스크 병목이 나타나면 오히려 느려질 수 있습니다.
한 저장소가 실패하면 나머지도 중단될까?
이 글의 순차 명령은 실패를 출력하고 다음 저장소로 넘어갑니다. 병렬 명령도 이미 시작된 다른 작업은 진행됩니다. 다만 최종 종료 코드는 실패를 나타낼 수 있으므로 CI나 자동화에서 사용할 때는 종료 코드와 로그를 함께 확인해야 합니다.
PowerShell 5.1에서도 병렬 실행할 수 있을까?
ForEach-Object -Parallel은 PowerShell 7 이상에서 지원됩니다. Windows PowerShell 5.1에서는 순차 예제를 사용하거나 별도의 Job 기반 스크립트를 작성해야 합니다.
마무리 체크리스트
- 처음에는 저장소 목록과
git status만 출력해 대상을 확인합니다. - 자동 병합을 피하려면
git pull --ff-only를 명시합니다. - 공백이 포함된 경로를 위해
-print0과xargs -0을 함께 사용합니다. - 병렬 실행 수는 4부터 시작합니다.
- 인증 입력이나 오류 분석이 필요하면 순차 실행으로 전환합니다.
- worktree와 서브모듈은 일반 clone 저장소와 분리해 관리합니다.
- 반복 작업이 많다면 셸 함수 또는
myrepos를 사용합니다.
저장소 몇 개를 가끔 갱신한다면 순차 find 명령만으로 충분합니다. 수십 개의 저장소를 반복 관리한다면 병렬 실행이나 전용 도구가 시간을 크게 줄여 줍니다. 중요한 점은 “모두 자동으로 합치기”가 아니라, 안전하게 업데이트할 수 있는 저장소만 처리하고 예외를 분명하게 남기는 것입니다.