Githubに小分けにしてcommit、pushするスクリプト


wishlistへ登録おねがいします！
https://store.steampowered.com/app/2713820/Elder_Magic_Squad/

Xもよろしくお願いします！
https://x.com/ElderMagicSquad



個人開発をしているとまとめて追加したい時に便利なpythonをchatgptに作ってもらいました。

ちゃんと細かく追加するのが一番だとは思いますが…
仕事とは別で開発を行っていると寝る時間ぎりぎりまで作業していたり
githubにあげるのを忘れがちなのでこれがあるととても便利です…

```python
import os
import subprocess
import tempfile

# 例: 100MB を上限に設定（必要に応じて 1GB = 1024*1024*1024 に変更してください）
MAX_CHUNK_SIZE = 100 * 1024 * 1024  # 100MB
COMMIT_MESSAGE_PREFIX = "Chunked commit part"

def run_git_command(args):
    """
    Gitコマンドをサブプロセスで実行し、標準出力を返すヘルパー関数
    """
    print(f"[DEBUG] run_git_command: git {' '.join(args)}")
    result = subprocess.run(["git"] + args, capture_output=True, text=True, check=False)
    print(f"[DEBUG] Return code: {result.returncode}")
    if result.stdout:
        print(f"[DEBUG] STDOUT:\n{result.stdout.strip()}")
    if result.stderr:
        print(f"[DEBUG] STDERR:\n{result.stderr.strip()}")
    if result.returncode != 0:
        print(f"[ERROR] Git command failed: git {' '.join(args)}")
    return result.stdout.strip()

def get_changed_files():
    """
    現在のGitリポジトリで「変更がある」すべての対象（ファイルのみ）をリストアップする関数。
    git status --porcelain --untracked-files=all の出力をパースし、各行から
      先頭2文字（ステータス）は無視し、3文字目以降のパス部分を取得します。
    ※ ファイルが存在するもののみ（os.path.isfile(path) が True）を対象としています。
    """
    print("[DEBUG] === Enter get_changed_files ===")
    output = run_git_command(["status", "--porcelain", "--untracked-files=all"])
    print(f"[DEBUG] Raw porcelain output:\n{output}")

    changed_files = []
    for line in output.splitlines():
        print(f"[DEBUG] Parsing line: '{line}'")
        # 先頭2文字がステータス、3文字目以降がパスと想定
        path = line[3:].strip()
        print(f"[DEBUG]  -> Extracted path: '{path}'")
        # 物理的に存在するファイルのみを対象（ディレクトリや削除されたファイルは除外）
        if path and os.path.isfile(path):
            print(f"[DEBUG]  -> '{path}' is a file. Adding to list.")
            changed_files.append(path)
        else:
            print(f"[DEBUG]  -> '{path}' is not a file. Skipping.")
    print(f"[DEBUG] Changed files: {changed_files}")
    print("[DEBUG] === Leave get_changed_files ===\n")
    return changed_files

def get_file_size(path):
    """
    指定されたファイルのサイズ(バイト)を返す。
    存在しない場合は0を返す。
    """
    if os.path.isfile(path):
        size = os.path.getsize(path)
        print(f"[DEBUG] get_file_size('{path}') -> {size} bytes")
        return size
    print(f"[DEBUG] get_file_size('{path}') -> not a file, returning 0")
    return 0

def chunk_files_by_size(file_list, max_chunk_size=MAX_CHUNK_SIZE):
    """
    ファイルのリストを、各チャンクの合計サイズが max_chunk_size 以下になるように分割する。
    1つのファイルが上限を超える場合はスキップし、警告を出す。
    """
    print("[DEBUG] === Enter chunk_files_by_size ===")
    print(f"[DEBUG] max_chunk_size = {max_chunk_size} bytes")
    chunks = []
    current_chunk = []
    current_size_sum = 0

    for f in file_list:
        size = get_file_size(f)
        if size > max_chunk_size:
            print(f"[WARNING] {f} (size: {size}) exceeds {max_chunk_size} bytes. Skipping.")
            continue

        if current_size_sum + size > max_chunk_size:
            print(f"[DEBUG] Current chunk size {current_size_sum} + {size} exceeds {max_chunk_size}.")
            if current_chunk:
                print(f"[DEBUG] Finalizing chunk: {current_chunk} (total size: {current_size_sum})")
                chunks.append(current_chunk)
            current_chunk = [f]
            current_size_sum = size
            print(f"[DEBUG] Starting new chunk with {f} (size: {size}).")
        else:
            current_chunk.append(f)
            current_size_sum += size
            print(f"[DEBUG] Added {f} (size: {size}) to current chunk. New chunk size: {current_size_sum}")

    if current_chunk:
        print(f"[DEBUG] Finalizing last chunk: {current_chunk} (total size: {current_size_sum})")
        chunks.append(current_chunk)
    print(f"[DEBUG] Resulting chunks: {chunks}")
    print("[DEBUG] === Leave chunk_files_by_size ===\n")
    return chunks

def commit_and_push_chunk(file_chunk, chunk_index):
    """
    指定されたファイル群を1つのコミットにまとめ、そのコミットをプッシュする。
    """
    print("[DEBUG] === Enter commit_and_push_chunk ===")
    print(f"[DEBUG] Chunk #{chunk_index} files: {file_chunk}")
    if not file_chunk:
        print("[DEBUG] No files in this chunk. Skipping commit.")
        return

    # 一時ファイルを作成して --pathspec-from-file 用にファイルパスを書き出す
    with tempfile.NamedTemporaryFile(mode="w", delete=False) as tmp:
        for path in file_chunk:
            tmp.write(path + "\n")
        temp_file = tmp.name
    print(f"[DEBUG] Temp file created: {temp_file}")

    # git add
    run_git_command(["add", f"--pathspec-from-file={temp_file}", "--"])
    # git commit
    commit_msg = f"{COMMIT_MESSAGE_PREFIX} #{chunk_index}"
    print(f"[DEBUG] Committing with message: '{commit_msg}'")
    run_git_command(["commit", "-m", commit_msg])
    # git push
    run_git_command(["push"])

    # 一時ファイル削除
    try:
        os.remove(temp_file)
        print(f"[DEBUG] Temp file {temp_file} removed.")
    except OSError as e:
        print(f"[WARNING] Failed to remove temp file {temp_file}: {e}")
    print("[DEBUG] === Leave commit_and_push_chunk ===\n")

def main():
    print("[DEBUG] === Enter main ===")
    # 1. 変更ファイルのリストを取得（ファイル単位のみ）
    changed_files = get_changed_files()
    if not changed_files:
        print("[INFO] 変更があるファイルはありません。終了します。")
        return
    print("[INFO] Changed files:", changed_files)

    # 2. ファイルサイズ順にソート（任意）
    print("[DEBUG] Sorting changed_files by size.")
    changed_files.sort(key=get_file_size)

    # 3. チャンク分割（各チャンクの合計サイズが MAX_CHUNK_SIZE 以下）
    chunks = chunk_files_by_size(changed_files, MAX_CHUNK_SIZE)
    print(f"[DEBUG] Total {len(chunks)} chunks created.")

    # 4. 各チャンクごとに個別にコミット＆プッシュ
    chunk_index = 1
    for chunk in chunks:
        print(f"[INFO] Commit chunk #{chunk_index}: {chunk}")
        commit_and_push_chunk(chunk, chunk_index)
        chunk_index += 1

    print("[INFO] すべてのチャンクをコミット＆プッシュしました。")
    print("[DEBUG] === Leave main ===")

if __name__ == "__main__":
    main()

```