🧟

gitのdetatched head運用でmainの誤commitにサヨナラ

に公開

そもそもブランチを置かなければ誤commitを防げる

git管理下のディレクトリにいるとき、誤ってmainブランチで作業してしまう事故を防ぎたいと思っていました。以下の記事を読んだことがあり、ローカルにmainを置かない運用があることは知っていましたが、なかなか踏み切れずにいました。

https://qiita.com/ucan-lab/items/0bbf07daa58959c1010a

先日、筆者が使用しているGit支援ツールのghqにpartial clone機能が追加されました。これを機に、前述の運用方法に取り組んでみました。

https://github.com/x-motemen/ghq/pull/412

しばらく使ってみて、いい感じにフローが固まってきたので本記事にまとめます。

使用しているエイリアス

git remote-head

remote HEADを取得します。ヘルパーとして定義しているだけで、直接実行することは基本的にありません。

gitconfig
remote-head = symbolic-ref refs/remotes/origin/HEAD
出力例
$ git remote-head
refs/remotes/origin/master

git default-branch

remote-headの出力から、デフォルトブランチを取得します。ヘルパーとして定義しているだけで、直接実行することは基本的にありません。

gitconfig
default-branch = !git remote-head | sed 's!.*/!!'
出力例
$ git remote-head
master

git refresh

remoteの最新の状態を取ってきます。これも直接実行することは基本的にありません。

gitconfig
refresh = fetch --all --prune

git dead

本記事のメインディッシュ。ローカルを最新のdetached headにするコマンドです。detached-headだと長いなと思ったのでdeadにしました。headを取ってくるのでbeheadにしようかと思いましたが言葉が強すぎるのでやめました。deadならまぁ良いかなと思いましたがペアプロのときにはビビられました。

gitconfig
dead = !git refresh --quiet && git switch -d $(git remote-head)
出力例
$ git dead
HEAD is now at 1234567 initial commit

git single

現在チェックアウトしているブランチ以外を削除します。使用注意。

gitconfig
single = !git branch | grep -v HEAD | xargs git branch --no-run-if-empty -d

git body

detached headに新しい体を生やすコマンドです。無引数でも実行できるように実行ファイルとして作りました。パスの通ったディレクトリに配置してください。

git-body
#!/usr/bin/env bash
git dead && git switch --create "${1:-"wip-$RANDOM"}"

引数を渡すと、一度deadしてから新しいブランチを作ります。

出力例
$ git body a
HEAD is now at 1234567 initial commit
Switched to a new branch 'a'

無引数で実行すると、wip-{ランダムな数値}のブランチが作られます。

出力例
$ git body
HEAD is now at 1234567 initial commit
Switched to a new branch 'wip-1234'

fuse

デフォルトブランチの変更を取り込みます。たまに使います。

gitconfig
fuse = !git refresh --quiet && git merge $(git remote-head)

使用しているシェル関数

以下のcloneを定義して使っています。途中でcdしたいのでシェル関数として定義しています。
内部では以下を行います。

  1. 冒頭で述べたghq getのpartial cloneを行う
  2. そのディレクトリに移動する
  3. remote-headに移動し、既存のブランチを処理する
.zshrc
clone() {
  has 'ghq' || return 1
  ghq get --partial blobless "$1"
  local target
  target=$(echo "$1" | sed -r 's;https?://[^/]+|\.git$;;')
  local dir
  dir=$(ghq list | grep --color=never --fixed-strings "$target")
  [ -n "$dir" ] && cd "$(ghq root)/$dir" || return
  git dead
  git single
}

作業フロー

まず、clone {repositoryのURL}を実行してプロジェクトをローカルにクローンします。
これで、detached headのクローンとディレクトリ移動が自動で行われます。

処理例
$ clone https://github.com/vim-jp/ekiden
     clone https://github.com/vim-jp/ekiden -> /Users/kawarimidoll/ghq/github.com/vim-jp/ekiden
       git clone --recursive --filter=blob:none https://github.com/vim-jp/ekiden /Users/kawarimidoll/ghq/github.com/vim-jp/ekiden
Cloning into '/Users/kawarimidoll/ghq/github.com/vim-jp/ekiden'...
// 中略…
HEAD is now at 94a45b5 Update content.json by #1037
Deleted branch main (was 94a45b5).

作業を開始するときはgit bodyでブランチを作り、commitを積んでいきます。

pushは以下の記事のものを使っています。

https://zenn.dev/kawarimidoll/articles/5bbec22bde2448

作業後はgit deadでdetached headに戻ります。なお、git bodyは内部でgit deadを実行するので、すぐに別の作業に入る場合は直接git bodyを実行します。

所感

最初はローカルにmainを置かないことにちょっと不安がありましたが、想像以上に簡単に移行できて普通に使うことができています。単に事故が減って快適になりました。
また、外部OSSの確認だけしたい場合なども、partial cloneにすればデータ量を削減できて良い感じです。


本記事を書いている途中で以下の記事が公開されてシンパシーを感じました。

https://www.takeokunn.org/posts/fleeting/20250518144557-local_git_branch_operation/

Discussion