Windows環境におけるGit運用のための改行コード・ファイル権限・シンボリックリンク対策

概要

異なるOS間でGitリポジトリを共有する際、WindowsとUnix系システムの差異により発生する三つの主要な問題点とその解決策について解説します。特にUnix系環境で作成されたリポジトリをWindowsで利用する場合の実践的な対処法を中心に、設定ベースの対応からスクリプトによる自動化までを網羅します。

改行コードの自動変換設定

各OSにおける改行表現の違いは、ファイルの差分検出に深刻な影響を与えます。WindowsがCRLF(\r\n)を採用するのに対し、LinuxはLF(\n)、macOSはCR(\r)を使用します。この不一致により、コードをcheckoutしただけでmodified状態になってしまう事象が頻発します。

Gitには組み込みの変換機構が存在し、コア設定を有効化するだけで自動的に変換を行います。チェックアウト時にLFをCRLFに、コミット時にCRLFをLFに変換する設定は以下の通りです。

git config --global core.autocrlf true

ファイル権限差異の無視設定

Unix系システムの所有者・グループ・その他ユーザに対する実行権限は、Windowsには相当する概念が存在しません。この乖離により、実行ビットの設定差異だけで大量のファイルがmodifiedとして検出されるtypechange問題が発生します。

Gitのfilemode設定を無効化することで、権限変更を差分として認識させないよう制御できます。ローカルリポジトリ単位での設定例:

git config core.filemode false

すでにtypechange状態になってしまった場合、以下のワンライナーで一括復元が可能です。

git status | grep "typechange" | cut -d' ' -f2- | xargs -I{} git checkout -- {}

シンボリックリンクの変換処理

Unix系システムで活用されるシンボリックリンクは、Windows環境では単なるテキストファイルとして扱われ、IDEやビルドシステムから正しく認識されません。重要なディレクトリにリンクが設定されている場合、リポジトリが事実上利用不能になることもあります。

Gitは現在のところ、改行コードのようにシンボリックリンクをネイティブに変換する機能を提供していません。Windowsのmklinkコマンドを活用した手動変換が必要です。mklinkの主要なオプションは以下の通りです。

  • /D: シンボリックリンク(ソフトリンク)を作成
  • /J: ディレクトリジャンクション(ディレクトリ向けハードリンク)を作成
  • /H: ハードリンクを作成(ファイル専用)

変換プロセスは以下の三ステップで構成されます。

  1. git ls-files -sでモード120000のエントリを特定
  2. mklinkコマンドでWindows形式のリンクを再作成
  3. git update-index --assume-unchangedでGitの追跡を除外

自動変換スクリプト実装例

上記手順を自動化するPythonスクリプトを示します。リポジトリルートで実行してください。

#!/usr/bin/env python3
import os
import subprocess
import sys

def extract_symlink_entries():
    """Gitインデックスからシンボリックリンクパスを抽出"""
    try:
        result = subprocess.run(
            ["git", "ls-files", "-s"],
            capture_output=True,
            text=True,
            check=True
        )
        symlink_paths = []
        for entry in result.stdout.splitlines():
            fields = entry.split()
            if len(fields) >= 4 and fields[0] == "120000":
                symlink_paths.append(fields[3])
        return symlink_paths
    except subprocess.CalledProcessError:
        print("Gitコマンドの実行に失敗しました", file=sys.stderr)
        return []

def retrieve_link_target(link_file):
    """シンボリックリンクファイル内の対象パスを取得"""
    try:
        with open(link_file, 'r', encoding='utf-8') as handle:
            return handle.read().strip()
    except Exception as error:
        print(f"リンク先読取エラー: {error}", file=sys.stderr)
        return None

def generate_windows_link(link_location, target_location):
    """Windowsネイティブリンクを生成"""
    if not os.path.isabs(target_location):
        base_path = os.path.dirname(link_location)
        target_location = os.path.join(base_path, target_location)
    
    if os.path.exists(link_location):
        os.unlink(link_location)
    
    is_target_file = os.path.isfile(target_location)
    
    try:
        if is_target_file:
            command = f'cmd /c mklink /H "{link_location}" "{target_location}"'
        else:
            command = f'cmd /c mklink /J "{link_location}" "{target_location}"'
        
        subprocess.run(command, shell=True, check=True, capture_output=True)
        return True
    except subprocess.CalledProcessError as cmd_error:
        print(f"リンク作成失敗: {cmd_error}", file=sys.stderr)
        return False

def execute_conversion():
    """メイン変換処理を実行"""
    repository_path = os.getcwd()
    symlinks = extract_symlink_entries()
    
    if not symlinks:
        print("対象となるシンボリックリンクが存在しません")
        sys.exit(0)
    
    print(f"{len(symlinks)}件のシンボリックリンクを検出")
    
    for symlink_path in symlinks:
        absolute_link = os.path.join(repository_path, symlink_path)
        
        if not os.path.exists(absolute_link):
            continue
        
        destination = retrieve_link_target(absolute_link)
        if not destination:
            continue
        
        print(f"変換開始: {symlink_path} → {destination}")
        
        if generate_windows_link(absolute_link, destination):
            try:
                subprocess.run(
                    ["git", "update-index", "--assume-unchanged", symlink_path],
                    cwd=repository_path,
                    check=True,
                    capture_output=True
                )
                print(f"  完了: {symlink_path}")
            except subprocess.CalledProcessError:
                print(f"  警告: Gitインデックス更新失敗: {symlink_path}")

if __name__ == "__main__":
    execute_conversion()

タグ: Git Windows symlink line-endings file-permissions

9月19日 15:21 投稿