From d147c9af18f0a8e50ee175adad3e201519b7cafb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 20 Jan 2026 15:54:04 +0000 Subject: [PATCH 1/6] Initial plan From e69b41a2c5eefba9f9c9739bb9f28910c8dba123 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 20 Jan 2026 15:57:47 +0000 Subject: [PATCH 2/6] Add Python-based tests with multi-platform support Co-authored-by: doitian <35768+doitian@users.noreply.github.com> --- .github/workflows/test-hooks.yml | 53 ++++++++ TESTING.md | 72 +++++++++++ test_hooks.py | 210 +++++++++++++++++++++++++++++++ 3 files changed, 335 insertions(+) create mode 100644 .github/workflows/test-hooks.yml create mode 100644 TESTING.md create mode 100755 test_hooks.py diff --git a/.github/workflows/test-hooks.yml b/.github/workflows/test-hooks.yml new file mode 100644 index 0000000..8731603 --- /dev/null +++ b/.github/workflows/test-hooks.yml @@ -0,0 +1,53 @@ +name: Test Hooks +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + test: + name: Test on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + + # Windows-specific: Install git via Scoop mingit + - name: Install Scoop and mingit (Windows) + if: runner.os == 'Windows' + shell: powershell + run: | + # Install Scoop if not already installed + if (-not (Get-Command scoop -ErrorAction SilentlyContinue)) { + Set-ExecutionPolicy RemoteSigned -Scope CurrentUser -Force + Invoke-RestMethod get.scoop.sh | Invoke-Expression + } + + # Install mingit via Scoop + scoop install mingit + + # Verify git installation + git --version + + # macOS and Linux: Ensure git is available + - name: Verify git installation (Unix) + if: runner.os != 'Windows' + run: git --version + + - name: Make test script executable (Unix) + if: runner.os != 'Windows' + run: chmod +x test_hooks.py + + - name: Run Python tests + run: python test_hooks.py diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000..149f06c --- /dev/null +++ b/TESTING.md @@ -0,0 +1,72 @@ +# Testing Unity Git Hooks + +This document describes how to run tests for the Unity Git Hooks project. + +## Overview + +Tests are written in Python and can run on Linux, macOS, and Windows. + +## Prerequisites + +- Python 3.x +- Git + +### Windows-specific + +On Windows, the tests are designed to work with git installed via Scoop's mingit package: + +```powershell +# Install Scoop (if not already installed) +Set-ExecutionPolicy RemoteSigned -Scope CurrentUser -Force +Invoke-RestMethod get.scoop.sh | Invoke-Expression + +# Install mingit +scoop install mingit +``` + +Alternatively, the tests will work with any git installation (Git for Windows, etc.). + +## Running Tests + +### Linux and macOS + +```bash +python3 test_hooks.py +``` + +or + +```bash +./test_hooks.py +``` + +### Windows + +```powershell +python test_hooks.py +``` + +## Test Structure + +The test suite (`test_hooks.py`) includes: + +- **TestPreCommitHook**: Tests for the pre-commit hook + - `test_ensuring_meta_is_committed`: Verifies that .meta files must be committed with their assets + - `test_ignoring_assets_file_starting_with_dot`: Verifies that hidden files (starting with `.`) don't require .meta files + - `test_renaming_directory`: Verifies that renaming directories properly handles .meta files + +## Continuous Integration + +The project uses GitHub Actions to run tests on multiple platforms: + +- Ubuntu (Linux) +- macOS +- Windows (with Scoop mingit) + +See `.github/workflows/test-hooks.yml` for the CI configuration. + +## Migrating from BATS + +Previous tests were written using BATS (Bash Automated Testing System) in `tests.bat`. These have been replaced with Python-based tests for better cross-platform compatibility. + +The old BATS workflow can be found in `.github/workflows/bats.yml`. diff --git a/test_hooks.py b/test_hooks.py new file mode 100755 index 0000000..76c9fc7 --- /dev/null +++ b/test_hooks.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python3 +""" +Test suite for Unity Git Hooks +Replaces BATS-based tests with Python-based tests that work on macOS and Windows +""" + +import os +import platform +import shutil +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + + +class GitHooksTestCase(unittest.TestCase): + """Base test case with setup and teardown for git repository""" + + def setUp(self): + """Create a temporary git repository for testing""" + # Create temporary directory + self.test_dir = tempfile.mkdtemp(prefix='unity-git-hooks-test-') + self.repo_dir = os.path.join(self.test_dir, 'repo') + os.makedirs(self.repo_dir) + + # Initialize git repository + self._run_git(['init']) + self._run_git(['config', 'user.email', 'test@ci']) + self._run_git(['config', 'user.name', 'test']) + + # Create Assets directory + self.assets_dir = os.path.join(self.repo_dir, 'Assets') + os.makedirs(self.assets_dir) + + # Set up hook script path + script_dir = Path(__file__).parent / 'scripts' + self.pre_commit_hook = str(script_dir / 'pre-commit') + + def tearDown(self): + """Clean up temporary directory""" + if os.path.exists(self.test_dir): + shutil.rmtree(self.test_dir) + + def _run_git(self, args, check=True, capture_output=False): + """ + Run git command in the test repository + + On Windows with Scoop mingit, use PowerShell to invoke git + """ + if platform.system() == 'Windows': + # Check if git is available via Scoop mingit + git_cmd = self._find_git_windows() + cmd = [git_cmd] + args + else: + cmd = ['git'] + args + + result = subprocess.run( + cmd, + cwd=self.repo_dir, + check=check, + capture_output=capture_output, + text=True + ) + + if capture_output: + return result + return result.returncode + + def _find_git_windows(self): + """ + Find git executable on Windows + Prioritize Scoop mingit installation + """ + # Try Scoop mingit first + scoop_git = os.path.expandvars(r'%USERPROFILE%\scoop\apps\mingit\current\cmd\git.exe') + if os.path.exists(scoop_git): + return scoop_git + + # Try Scoop shims directory + scoop_shim = os.path.expandvars(r'%USERPROFILE%\scoop\shims\git.exe') + if os.path.exists(scoop_shim): + return scoop_shim + + # Fallback to system git + return 'git' + + def _run_hook(self, hook_path, check=False): + """ + Run a git hook script + + On Windows, we need to use bash or sh to run the hook + On Unix systems, we can run it directly + """ + if platform.system() == 'Windows': + # Use Git Bash on Windows + git_bash = self._find_git_bash_windows() + if git_bash: + cmd = [git_bash, hook_path] + else: + # Try with sh from Git installation + cmd = ['sh', hook_path] + else: + # On Unix systems, run directly + cmd = [hook_path] + + result = subprocess.run( + cmd, + cwd=self.repo_dir, + capture_output=True, + text=True + ) + + if check and result.returncode != 0: + raise subprocess.CalledProcessError( + result.returncode, cmd, result.stdout, result.stderr + ) + + return result + + def _find_git_bash_windows(self): + """Find Git Bash executable on Windows""" + # Try Scoop mingit installation + scoop_bash = os.path.expandvars(r'%USERPROFILE%\scoop\apps\mingit\current\usr\bin\bash.exe') + if os.path.exists(scoop_bash): + return scoop_bash + + # Try standard Git for Windows installation + git_bash = r'C:\Program Files\Git\bin\bash.exe' + if os.path.exists(git_bash): + return git_bash + + return None + + +class TestPreCommitHook(GitHooksTestCase): + """Test cases for pre-commit hook""" + + def test_ensuring_meta_is_committed(self): + """Test that missing .meta file causes pre-commit to fail""" + # Create an asset file without .meta + asset_file = os.path.join(self.assets_dir, 'assets') + Path(asset_file).touch() + self._run_git(['add', 'Assets/assets']) + + # Run pre-commit hook - should fail + result = self._run_hook(self.pre_commit_hook) + self.assertEqual(result.returncode, 1) + self.assertIn('Error: Missing meta file', result.stderr) + + # Now add the .meta file + meta_file = os.path.join(self.assets_dir, 'assets.meta') + Path(meta_file).touch() + self._run_git(['add', 'Assets/assets.meta']) + + # Run pre-commit hook - should succeed + result = self._run_hook(self.pre_commit_hook, check=True) + self.assertEqual(result.returncode, 0) + + def test_ignoring_assets_file_starting_with_dot(self): + """Test that asset files starting with dot are ignored""" + # Create a hidden asset file (no .meta needed) + hidden_file = os.path.join(self.assets_dir, '.assets') + Path(hidden_file).touch() + self._run_git(['add', '--force', 'Assets/.assets']) + + # Run pre-commit hook - should succeed + result = self._run_hook(self.pre_commit_hook, check=True) + self.assertEqual(result.returncode, 0) + + def test_renaming_directory(self): + """Test that renaming a directory requires updating .meta files""" + # Create directory with .gitkeep and .meta + dir_path = os.path.join(self.assets_dir, 'dir') + os.makedirs(dir_path) + + gitkeep_file = os.path.join(dir_path, '.gitkeep') + Path(gitkeep_file).touch() + + meta_file = os.path.join(self.assets_dir, 'dir.meta') + Path(meta_file).touch() + + self._run_git(['add', '--force', '--all']) + self._run_git(['commit', '-n', '-m', 'add Assets/dir']) + + # Rename the directory + new_dir_path = os.path.join(self.assets_dir, 'dir-new') + shutil.move(dir_path, new_dir_path) + self._run_git(['add', '--force', '--all']) + + # Run pre-commit hook - should fail because old .meta still exists + result = self._run_hook(self.pre_commit_hook) + self.assertEqual(result.returncode, 1) + self.assertIn('Error: Redudant meta file', result.stderr) + + +def run_tests(): + """Run all tests""" + # Discover and run tests + loader = unittest.TestLoader() + suite = loader.loadTestsFromModule(sys.modules[__name__]) + runner = unittest.TextTestRunner(verbosity=2) + result = runner.run(suite) + + # Return exit code based on test results + return 0 if result.wasSuccessful() else 1 + + +if __name__ == '__main__': + sys.exit(run_tests()) From bf5bbded9d9e6c95e549dcbedd6d2b61ed7b8194 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 20 Jan 2026 15:58:40 +0000 Subject: [PATCH 3/6] Update README with testing information Co-authored-by: doitian <35768+doitian@users.noreply.github.com> --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 096eb87..e918f56 100644 --- a/README.md +++ b/README.md @@ -43,3 +43,17 @@ Otherwise you need set full path relative `.git`. Following example tells the sc ``` git config unity3d.assets-dir client/Assets ``` + +## Testing + +This project includes automated tests that work on Linux, macOS, and Windows. + +To run tests locally: + +```bash +python3 test_hooks.py +``` + +For more information about testing, see [TESTING.md](TESTING.md). + +The tests are automatically run on GitHub Actions for all three platforms. From 14307dec261ba1c3279e3ed8be3bc92024b6574d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 20 Jan 2026 16:01:04 +0000 Subject: [PATCH 4/6] Make git run hooks automatically and delete TESTING.md Co-authored-by: doitian <35768+doitian@users.noreply.github.com> --- README.md | 2 -- TESTING.md | 72 ---------------------------------------- test_hooks.py | 91 +++++++++++++++++++-------------------------------- 3 files changed, 33 insertions(+), 132 deletions(-) delete mode 100644 TESTING.md diff --git a/README.md b/README.md index e918f56..de095a8 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,4 @@ To run tests locally: python3 test_hooks.py ``` -For more information about testing, see [TESTING.md](TESTING.md). - The tests are automatically run on GitHub Actions for all three platforms. diff --git a/TESTING.md b/TESTING.md deleted file mode 100644 index 149f06c..0000000 --- a/TESTING.md +++ /dev/null @@ -1,72 +0,0 @@ -# Testing Unity Git Hooks - -This document describes how to run tests for the Unity Git Hooks project. - -## Overview - -Tests are written in Python and can run on Linux, macOS, and Windows. - -## Prerequisites - -- Python 3.x -- Git - -### Windows-specific - -On Windows, the tests are designed to work with git installed via Scoop's mingit package: - -```powershell -# Install Scoop (if not already installed) -Set-ExecutionPolicy RemoteSigned -Scope CurrentUser -Force -Invoke-RestMethod get.scoop.sh | Invoke-Expression - -# Install mingit -scoop install mingit -``` - -Alternatively, the tests will work with any git installation (Git for Windows, etc.). - -## Running Tests - -### Linux and macOS - -```bash -python3 test_hooks.py -``` - -or - -```bash -./test_hooks.py -``` - -### Windows - -```powershell -python test_hooks.py -``` - -## Test Structure - -The test suite (`test_hooks.py`) includes: - -- **TestPreCommitHook**: Tests for the pre-commit hook - - `test_ensuring_meta_is_committed`: Verifies that .meta files must be committed with their assets - - `test_ignoring_assets_file_starting_with_dot`: Verifies that hidden files (starting with `.`) don't require .meta files - - `test_renaming_directory`: Verifies that renaming directories properly handles .meta files - -## Continuous Integration - -The project uses GitHub Actions to run tests on multiple platforms: - -- Ubuntu (Linux) -- macOS -- Windows (with Scoop mingit) - -See `.github/workflows/test-hooks.yml` for the CI configuration. - -## Migrating from BATS - -Previous tests were written using BATS (Bash Automated Testing System) in `tests.bat`. These have been replaced with Python-based tests for better cross-platform compatibility. - -The old BATS workflow can be found in `.github/workflows/bats.yml`. diff --git a/test_hooks.py b/test_hooks.py index 76c9fc7..36cf831 100755 --- a/test_hooks.py +++ b/test_hooks.py @@ -33,9 +33,8 @@ def setUp(self): self.assets_dir = os.path.join(self.repo_dir, 'Assets') os.makedirs(self.assets_dir) - # Set up hook script path - script_dir = Path(__file__).parent / 'scripts' - self.pre_commit_hook = str(script_dir / 'pre-commit') + # Install git hooks so they run automatically + self._install_hooks() def tearDown(self): """Clean up temporary directory""" @@ -83,54 +82,30 @@ def _find_git_windows(self): return scoop_shim # Fallback to system git - return 'git' + return shutil.which('git') or 'git' - def _run_hook(self, hook_path, check=False): + def _install_hooks(self): """ - Run a git hook script - - On Windows, we need to use bash or sh to run the hook - On Unix systems, we can run it directly + Install git hooks into the test repository + Hooks will be run automatically by git """ - if platform.system() == 'Windows': - # Use Git Bash on Windows - git_bash = self._find_git_bash_windows() - if git_bash: - cmd = [git_bash, hook_path] - else: - # Try with sh from Git installation - cmd = ['sh', hook_path] - else: - # On Unix systems, run directly - cmd = [hook_path] - - result = subprocess.run( - cmd, - cwd=self.repo_dir, - capture_output=True, - text=True - ) - - if check and result.returncode != 0: - raise subprocess.CalledProcessError( - result.returncode, cmd, result.stdout, result.stderr - ) + # Get the scripts directory + script_dir = Path(__file__).parent / 'scripts' - return result - - def _find_git_bash_windows(self): - """Find Git Bash executable on Windows""" - # Try Scoop mingit installation - scoop_bash = os.path.expandvars(r'%USERPROFILE%\scoop\apps\mingit\current\usr\bin\bash.exe') - if os.path.exists(scoop_bash): - return scoop_bash - - # Try standard Git for Windows installation - git_bash = r'C:\Program Files\Git\bin\bash.exe' - if os.path.exists(git_bash): - return git_bash - - return None + # Create hooks directory if it doesn't exist + hooks_dir = os.path.join(self.repo_dir, '.git', 'hooks') + os.makedirs(hooks_dir, exist_ok=True) + + # Copy hook files + hooks = ['pre-commit', 'post-checkout', 'post-merge'] + for hook in hooks: + src = script_dir / hook + dst = os.path.join(hooks_dir, hook) + if src.exists(): + shutil.copy2(str(src), dst) + # Make executable on Unix systems + if platform.system() != 'Windows': + os.chmod(dst, 0o755) class TestPreCommitHook(GitHooksTestCase): @@ -143,9 +118,9 @@ def test_ensuring_meta_is_committed(self): Path(asset_file).touch() self._run_git(['add', 'Assets/assets']) - # Run pre-commit hook - should fail - result = self._run_hook(self.pre_commit_hook) - self.assertEqual(result.returncode, 1) + # Try to commit - pre-commit hook should fail automatically + result = self._run_git(['commit', '-m', 'test commit'], check=False, capture_output=True) + self.assertNotEqual(result.returncode, 0) self.assertIn('Error: Missing meta file', result.stderr) # Now add the .meta file @@ -153,8 +128,8 @@ def test_ensuring_meta_is_committed(self): Path(meta_file).touch() self._run_git(['add', 'Assets/assets.meta']) - # Run pre-commit hook - should succeed - result = self._run_hook(self.pre_commit_hook, check=True) + # Try to commit again - should succeed + result = self._run_git(['commit', '-m', 'test commit'], check=False, capture_output=True) self.assertEqual(result.returncode, 0) def test_ignoring_assets_file_starting_with_dot(self): @@ -164,8 +139,8 @@ def test_ignoring_assets_file_starting_with_dot(self): Path(hidden_file).touch() self._run_git(['add', '--force', 'Assets/.assets']) - # Run pre-commit hook - should succeed - result = self._run_hook(self.pre_commit_hook, check=True) + # Commit should succeed via pre-commit hook + result = self._run_git(['commit', '-m', 'test commit'], check=False, capture_output=True) self.assertEqual(result.returncode, 0) def test_renaming_directory(self): @@ -181,16 +156,16 @@ def test_renaming_directory(self): Path(meta_file).touch() self._run_git(['add', '--force', '--all']) - self._run_git(['commit', '-n', '-m', 'add Assets/dir']) + self._run_git(['commit', '-m', 'add Assets/dir']) # Rename the directory new_dir_path = os.path.join(self.assets_dir, 'dir-new') shutil.move(dir_path, new_dir_path) self._run_git(['add', '--force', '--all']) - # Run pre-commit hook - should fail because old .meta still exists - result = self._run_hook(self.pre_commit_hook) - self.assertEqual(result.returncode, 1) + # Try to commit - pre-commit hook should fail because old .meta still exists + result = self._run_git(['commit', '-m', 'rename dir'], check=False, capture_output=True) + self.assertNotEqual(result.returncode, 0) self.assertIn('Error: Redudant meta file', result.stderr) From e0455da34a55619bb031bc812398a5c376f0008f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 20 Jan 2026 16:06:48 +0000 Subject: [PATCH 5/6] Remove useless chmod step and delete bats workflow Co-authored-by: doitian <35768+doitian@users.noreply.github.com> --- .github/workflows/bats.yml | 20 -------------------- .github/workflows/test-hooks.yml | 4 ---- 2 files changed, 24 deletions(-) delete mode 100644 .github/workflows/bats.yml diff --git a/.github/workflows/bats.yml b/.github/workflows/bats.yml deleted file mode 100644 index e88e262..0000000 --- a/.github/workflows/bats.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: Bats -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] - -jobs: - unit-test: - runs-on: ubuntu-latest - name: Bats - steps: - - uses: actions/checkout@v2 - - - name: install bats - run: | - git clone --depth=1 https://github.com/bats-core/bats-core.git - - - name: run tests - run: bats-core/bin/bats ./tests.bat diff --git a/.github/workflows/test-hooks.yml b/.github/workflows/test-hooks.yml index 8731603..ed5df8d 100644 --- a/.github/workflows/test-hooks.yml +++ b/.github/workflows/test-hooks.yml @@ -45,9 +45,5 @@ jobs: if: runner.os != 'Windows' run: git --version - - name: Make test script executable (Unix) - if: runner.os != 'Windows' - run: chmod +x test_hooks.py - - name: Run Python tests run: python test_hooks.py From 86d0d2d88b0fd54f76b1f45f59995be4fbb66a6a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 20 Jan 2026 16:11:33 +0000 Subject: [PATCH 6/6] Fix Windows CI failure and delete tests.bat Co-authored-by: doitian <35768+doitian@users.noreply.github.com> --- test_hooks.py | 13 ++++++++++++- tests.bat | 49 ------------------------------------------------- 2 files changed, 12 insertions(+), 50 deletions(-) delete mode 100755 tests.bat diff --git a/test_hooks.py b/test_hooks.py index 36cf831..efd98fe 100755 --- a/test_hooks.py +++ b/test_hooks.py @@ -39,7 +39,18 @@ def setUp(self): def tearDown(self): """Clean up temporary directory""" if os.path.exists(self.test_dir): - shutil.rmtree(self.test_dir) + # On Windows, git may keep file handles open, so we need to handle errors + def handle_remove_readonly(func, path, exc): + """Error handler for Windows readonly files""" + import stat + if not os.access(path, os.W_OK): + # Make the file writable and try again + os.chmod(path, stat.S_IWUSR) + func(path) + else: + raise + + shutil.rmtree(self.test_dir, onerror=handle_remove_readonly) def _run_git(self, args, check=True, capture_output=False): """ diff --git a/tests.bat b/tests.bat deleted file mode 100755 index 7213454..0000000 --- a/tests.bat +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env bats - -PRE_COMMIT="$BATS_TEST_DIRNAME/scripts/pre-commit" - -setup() { - git init repo >&2 - cd repo - git config user.email "bats@ci" - git config user.name "bats" - mkdir Assets -} - -teardown() { - cd .. - rm -rf repo -} - -@test "ensuring .meta is commit" { - touch Assets/assets - git add Assets/assets - run "$PRE_COMMIT" - echo "$output" - [ "$status" -eq 1 ] - [ "${lines[0]}" = "Error: Missing meta file." ] - - touch Assets/assets.meta - git add Assets/assets.meta - "$PRE_COMMIT" -} - -@test "ignoring assets file which starts with dot" { - touch Assets/.assets - git add --force Assets/.assets - "$PRE_COMMIT" -} - -@test "renaming directory" { - mkdir Assets/dir - touch Assets/dir/.gitkeep - touch Assets/dir.meta - git add --force --all - git commit -n -m 'add Assets/dir' - mv Assets/dir Assets/dir-new - git add --force --all - run "$PRE_COMMIT" - echo "$output" - [ "$status" -eq 1 ] - [ "${lines[0]}" = "Error: Redudant meta file." ] -}