記事「CI/CDでGitブランチの存在確認をする(ローカル/リモート別)」のイメージ

CI/CDでGitブランチの存在確認をする(ローカル/リモート別)

公開日: 2025-12-27

  • Git
  • CI/CD
  • TypeScript

CI/CDの処理中、Gitリポジトリにブランチが存在するかどうかを知りたい場面がしばしばあります。
たとえば、デプロイ対象が存在するかでジョブを分岐したり、バックポート用のブランチが切られているかを確認したり。

私も最近の業務でそういった処理を実装する機会があったので、今回は実際にどういったコードで処理するかといった実例も踏まえて紹介していこうかと思います。

実際に業務で実装したコードを流出させているわけじゃないのでご安心ください


まずは結論から

  • ローカルのリポジトリを確認する:

    それまでの処理で、すでにローカルにリポジトリが存在する場合は以下のコマンドでブランチの存在確認を行います。

    git refs exists refs/heads/main
  • リモートのリポジトリを確認する:

    リポジトリの容量が大きいなどの理由で、クローンする前に目的のブランチが存在するか確認しておきたい場合は以下のコマンドでリポジトリをクローンすることなくブランチの存在確認ができます。

    git ls-remote --exit-code --branches --refs {リポジトリのURL} "refs/heads/main"

ポイントは「出力文字列」ではなく、「終了コード」で判定することです。
文字列で判定すると、認証エラーや通信エラーなどが発生したときに事故ったり、テストケースが増えてしまったり面倒です。


詳細説明: git refs exists <ref>(Git 2.52.0+)

ローカルにリポジトリがあるなら、git refs exists <ref> が一番読みやすくて、意図が明確です。

git refs exists refs/heads/main

git refs exists を実行したときに発生しうる終了コードは以下のように分かれます。

終了コードコマンドの実行結果
0<ref> が存在する
1リポジトリの内部処理に失敗した(通常はほぼ発生しない)
2<ref> が存在しない
128Gitの汎用エラー(ディレクトリがGitリポジトリではない、など)

つまり、以下のような分岐にしてやると安全にブランチの存在チェックができます。

function hasBranch(branch) {
  // git コマンドを実行して終了コードを受け取る
  const exitCode = /* git refs exists `refs/heads/${branch}` */;
  switch (exitCode) {
    case 0: return true;  // ブランチが存在する
    case 2: return false; // ブランチが存在しない
    default: {
      // 0, 2 のどちらでもなければ異常終了
      throw new Error("想定外のエラーが発生しました")
    }
  }
}

なお、git refs exists は「<ref>が存在するか」を見るだけで、「<ref>が実オブジェクトに解決できるか」までは検証しません。

参考リンク: Git - git-refs Documentation


互換: git show-ref --exists <ref>(Git 2.43+)

CI/CDのランナーに含まれるGitのバージョンが古く、git refs exists が使用できない場合は、git show-ref --exists <ref> でもほぼ同じことができます。

git show-ref --exists refs/heads/main

こちらも終了コードが 0/1/2/128 と分かれるので、置き換えがしやすいです。
git refs exists意図がコマンドから明確に読み取れることが一番の利点、という理解でOKだと思います)


詳細説明: git ls-remote …(Git 2.46.0+)

リモートリポジトリに対する存在確認では、git ls-remote を使います。
ここも「終了コードで判定できる」ようにしてやるのがポイントです。

git ls-remote --exit-code --branches --refs {リポジトリのURL} "refs/heads/main"

【オプション説明】

  • --branches: 検索対象を refs/heads/* に限定(タグ等に誤爆するのを防ぐ)

    Gitのバージョンが 2.8.6 ~ 2.45.0 の場合、代替として --heads オプションが使用できます

    git ls-remote --exit-code --heads --refs {リポジトリのURL} "refs/heads/main"
  • --refs: HEAD や peeled tag(refs/tags/v2.40.0^{} のような行)などを避ける

  • --exit-code: マッチ0件を終了コード 2 にする

    このオプションを指定しない場合、git ls-remote は<ref>が存在するかどうかにかかわらず終了コード0を返します。

git ls-remote を実行したときに発生しうる終了コードは以下のように分かれます。

終了コードコマンドの実行結果
0<ref> が存在する
2<ref> が存在しない
128Gitの汎用エラー(URLが不正、認証に失敗、など)

実例: TypeScript + execa で存在確認(ローカル/リモート)

ここでは「終了コードが 2 なら未存在、それ以外の非0は異常」として扱う方針で書きます。
execa の使い方の詳細は execa のドキュメントを参照してください)


ローカルブランチの存在確認(関数定義)

import { execa } from "execa";

export async function branchExistsLocal(repoPath: string, branch: string): Promise<boolean> {
  // ref名(refs/heads/<name>)に正規化してから判定する
  const ref = branch.startsWith("refs/") ? branch : `refs/heads/${branch}`;

  try {
    // コマンドが成功したら「存在する」
    await execa("git", ["refs", "exists", ref], { cwd: repoPath });
    return true;
  } catch (error) {
    // 終了コードを見て「未存在」と「異常」を分ける
    const exitCode = (error as { exitCode?: unknown }).exitCode;
    if (exitCode === 2) {
      // 終了コード2だけは「未存在」として扱う
      return false;
    }

    // エラーメッセージを取得
    // stderr を優先して拾う(Gitは原因をstderrに出すことが多い)
    const stderr = String((error as { stderr?: unknown }).stderr ?? "").trim();
    const message = stderr || String((error as { message?: unknown }).message ?? error);

    // 終了コード2以外のエラーは異常終了
    throw new Error(
      `gitコマンドの実行に失敗しました(refs exists / 終了コード=${String(
        exitCode ?? "unknown"
      )}): ${message}`
    );
  }
}

ローカルブランチの存在確認(使用例)

try {
  const exists = await branchExistsLocal(process.cwd(), "release/2025-12-27");

  if (exists) {
    console.log("ブランチが存在します(ローカル)");
  } else {
    console.log("ブランチは存在しません(ローカル)");
  }
} catch (error) {
  const message = String((error as { message?: unknown }).message ?? error);
  console.error(`確認に失敗しました(ローカル): ${message}`);
}

リモートブランチの存在確認(関数定義)

import { execa } from "execa";

export async function branchExistsRemote(repoUrl: string, branch: string): Promise<boolean> {
  // ref名(refs/heads/<name>)に正規化してから判定する
  const ref = branch.startsWith("refs/") ? branch : `refs/heads/${branch}`;

  try {
    await execa("git", ["ls-remote", "--exit-code", "--branches", "--refs", repoUrl, ref]);

    return true;
  } catch (error) {
    // 終了コードを見て「未存在」と「異常」を分ける
    const exitCode = (error as { exitCode?: unknown }).exitCode;
    if (exitCode === 2) {
      // 終了コード2だけは「未存在」として扱う
      return false;
    }

    // エラーメッセージを取得
    // stderr を優先して拾う(認証/通信系の失敗が分かりやすい)
    const stderr = String((error as { stderr?: unknown }).stderr ?? "").trim();
    const message = stderr || String((error as { message?: unknown }).message ?? error);

    // 終了コード2以外のエラーは異常終了
    throw new Error(
      `gitコマンドの実行に失敗しました(ls-remote / 終了コード ${String(
        exitCode ?? "unknown"
      )}): ${message}`
    );
  }
}

リモートブランチの存在確認(使用例)

try {
  const exists = await branchExistsRemote("https://github.com/OWNER/REPO.git", "main");

  if (exists) {
    console.log("ブランチが存在します(リモート)");
  } else {
    console.log("ブランチは存在しません(リモート)");
  }
} catch (error) {
  const message = String((error as { message?: unknown }).message ?? error);
  console.error(`確認に失敗しました(リモート): ${message}`);
}

まとめ

  • ローカルにリポジトリがあるなら、Git 2.52.0+ では git refs exists が読みやすい
    • 古い環境でも、git show-ref --exists に置き換えれば同じノリで書ける
  • リモート確認は、Git 2.46.0+ では --branches をメインにする
    • 古い環境では --heads を代替として扱う
  • どの方法でも、未存在(終了コード 2)と、通信/認証などの失敗(それ以外の非0)を混同しない