37
27

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Excel、Excel VBA をGitで管理する

Posted at

はじめに

可能な限り避けたいのですが、稀に大量のExcelやExcel VBAを管理しなくてはならないときってありませんか?

App Scriptであれば、まだ管理する方法は幾つかあります。

しかし、ExcelやExcel VBAだと管理する方法が無く、どこかクラウド上のドライブで保管する。に行き着くことが多いです。

なにか良い管理方法はないかと色々と考えた結果、やはりGitで管理するのが良さそうだと思ったので、記事にしました。

Excel、Excel VBAをGitで管理する

Excel、Excel VBAをGitで管理すると、結局バイナリ管理になります。

これらを解決するための手段として、Chart GPT から、Git XLをオススメされましたが、更新が停滞しており、使用に不安を感じました。

やはり、Excel自体はバイナリ管理するしかなさそう。。。というのが結論でした。

VBAの方だけでも、ちゃんとGitで管理したい。そう思って色々と調べてたどり着いたのが以下記事。

Excel の commit タイミングで、VBAの部分だけを抽出し、既存コミットに混ぜてしまう。というもの。
これは「良さそう」ということで早速チャレンジしてみました。

なお、使用するのはPythonと、ライブラリは pre-commitoletools の2つのみ。
両方とも 2025.09 現時点で、ちゃんとメンテナンスされています。

以下、pre-commitのYAMLファイル。

# .pre-commit-config.yaml
repos:
  - repo: local
    hooks:
      - id: extract-vba-macros
        name: Extract VBA Macros from Excel files
        entry: uv run python .githooks/pre-commit.py
        language: system
        files: \.(xlsb|xls|xlsm|xla|xlt|xlam)$
        pass_filenames: false
        always_run: false

以下、コミット時に発火するソースコード。

# .githooks/pre-commit.py
import os
import shutil
import sys
import subprocess
from oletools.olevba3 import VBA_Parser

EXCEL_FILE_EXTENSIONS = ('xlsb', 'xls', 'xlsm', 'xla', 'xlt', 'xlam')
KEEP_NAME = False
VBA_ROOT_PATH = 'src.vba'

def get_staged_excel_files() -> list[str]:
    try:
        result = subprocess.run(
            ['git', 'diff', '--cached', '--name-only', '-z'],
            capture_output=True,
            text=True,
            check=True
        )
        # git diff --cached --name-only -z で取得したファイル名は、null文字(\0)で区切られている
        # 例: "src.vba/変換ツール/Sheet1.cls\0src.vba/変換ツール/Module1.bas\0"
        # そのため、null文字(\0)で区切られたファイル名を取得し、空文字列を除外して、ファイル名を取得する
        staged_files = result.stdout.strip().split('\0') if result.stdout.strip() else []
        staged_files = [f for f in staged_files if f] # 空文字列を除外
        
        # Excelファイルを取得(一時ファイルを除外)
        excel_files = [
            f for f in staged_files 
            if f.endswith(EXCEL_FILE_EXTENSIONS) and os.path.exists(f) and not os.path.basename(f).startswith('~$')
        ]
        return excel_files
    except subprocess.CalledProcessError:
        # Fallback to scanning all files if git command fails
        return []

def parse_excel_file(excel_file: str) -> bool:
    excel_filename = os.path.splitext(os.path.basename(excel_file))[0]
    vba_file_dir = os.path.join(VBA_ROOT_PATH, excel_filename)

    try:
        vba_parser = VBA_Parser(excel_file)
        if not vba_parser.detect_vba_macros():
            print(f"No VBA macros found in {excel_file}")
            return True
            
        vba_modules = vba_parser.extract_all_macros()
        
        if not vba_modules:
            print(f"No VBA modules extracted from {excel_file}")
            return True
        
        print(f"Extracting VBA macros from {excel_file}...")
        
        for _, _, filename, content in vba_modules:
            lines = content.split('\r\n') if '\r\n' in content else content.split('\n')
            
            if not lines:
                continue
                
            filtered_content = []
            for line in lines:
                if line.startswith('Attribute') and 'VB_' in line:
                    if 'VB_Name' in line and KEEP_NAME:
                        filtered_content.append(line)
                else:
                    filtered_content.append(line)

            if filtered_content and filtered_content[-1] == '':
                filtered_content.pop()

            non_empty_lines = [line for line in filtered_content if line.strip()]

            if non_empty_lines:
                if not os.path.exists(vba_file_dir):
                    os.makedirs(vba_file_dir)
                
                output_file = os.path.join(vba_file_dir, filename)
                with open(output_file, 'w', encoding='utf-8') as f:
                    f.write('\n'.join(filtered_content))

                print(f"  → Extracted: {output_file}")

                # Stage the extracted VBA file
                subprocess.run(['git', 'add', output_file], check=False)

        vba_parser.close()
        return True
    except Exception as e:
        print(f"Error processing {excel_file}: {e}")
        return False

def clean_old_vba_files(staged_excel_files: list[str]) -> None:
    excel_file_names = [
        os.path.splitext(os.path.basename(excel_file))[0]
        for excel_file in staged_excel_files
    ]

    for excel_filename in excel_file_names:
        vba_file_dir = os.path.join(VBA_ROOT_PATH, excel_filename)
        if not os.path.exists(vba_file_dir):
            try:
                print(f"Removing old VBA directory: {vba_file_dir}")
                shutil.rmtree(vba_file_dir)
            except Exception as e:
                print(f"Error removing old VBA directory: {e}")

def main():
    staged_excel_files = get_staged_excel_files()

    if not staged_excel_files:
        print("No Excel files found.")
        return 0

    clean_old_vba_files(staged_excel_files)

    success = True
    for excel_file in staged_excel_files:
        if not parse_excel_file(excel_file):
            success = False

    if success:
        print("VBA macro extraction completed successfully.")
        return 0
    else:
        print("Some files failed to process.")
        return 1


if __name__ == '__main__':
    sys.exit(main())

残念ながら、Excelについては継続してバイナリ管理となってしまいますが、VBAについてはExcelから切り離してGitを使ってのソースコード管理を行うことができるようになりました。
少なくとも「どこかクラウド上のドライブで保管する」よりは幾分マシな方法で管理できるのは、良いと思いました。

image-1757260595664.png

さいごに

ExcelやExcel VBAは、非エンジニアにとっては大変便利なもので、つい量産されてしまいがちです。

ただ、管理する側にとって見れば、ExcelやExcel VBAは管理し辛く、可能な限り避けたいものです。

特に、システム利用の一部としてExcelやExcel VBAを使っているものであれば、可能な限りソースコードといっしょに管理したいですよね。
今回はそんなExcelやExcel VBAの管理方法について、色々と調べて、試してみました。

👋👋👋

37
27
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
37
27

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?