Skip to content

Added file permission tests.#175

Merged
AlexSkrypnyk merged 1 commit into
mainfrom
feature/exec-bit
Sep 11, 2025
Merged

Added file permission tests.#175
AlexSkrypnyk merged 1 commit into
mainfrom
feature/exec-bit

Conversation

@AlexSkrypnyk

@AlexSkrypnyk AlexSkrypnyk commented Sep 11, 2025

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • Bug Fixes
    • Ensures file permissions (e.g., executables) are preserved in generated artifacts and during transfers, improving consistency across environments.
  • Documentation
    • Added a comprehensive developer guide describing project tooling, workflows, and standards to streamline contributions.
  • Tests
    • Introduced functional tests that verify file permission retention throughout artifact creation and delivery, increasing confidence in deployment reliability.

@coderabbitai

coderabbitai Bot commented Sep 11, 2025

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds a developer guide (CLAUDE.md), configures Git to respect file permissions by running git config core.filemode true during ArtifactGitRepository initialization, and introduces a functional test verifying file permission preservation across artifact creation and transfer in force-push mode.

Changes

Cohort / File(s) Summary
Documentation
CLAUDE.md
New repository-level guide describing the Git Artifact PHP CLI, architecture, tokens, deployment modes, testing, standards, and commands.
Git repository configuration
src/Git/ArtifactGitRepository.php
In constructor, after logger setup, invokes internal executor to run git config core.filemode true to ensure Git tracks file mode changes.
Functional tests
tests/Functional/FilePermissionsTest.php
New test validating 0644/0755 permissions persist from source to destination when creating and transferring artifacts in MODE_FORCE_PUSH.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant ArtifactCommand
  participant ArtifactGitRepository as GitRepository
  participant Git

  User->>ArtifactCommand: Run artifact command (force-push)
  ArtifactCommand->>GitRepository: Construct repository helper
  activate GitRepository
  GitRepository->>Git: config core.filemode true
  Note right of GitRepository: Ensure Git respects file permissions
  deactivate GitRepository

  ArtifactCommand->>GitRepository: Prepare artifact from source
  GitRepository->>Git: Commit files (preserving modes)
  ArtifactCommand->>GitRepository: Push artifact to target branch
  GitRepository->>Git: Force-push

  User-->>ArtifactCommand: Validate result (e.g., tests check perms)
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Pre-merge checks (2 passed, 1 warning)

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "Added file permission tests." is concise and accurately describes the primary change in the PR — the new functional test verifying preservation of file permissions. The changeset also includes a small implementation tweak (setting git core.filemode) and a documentation file, but those are secondary to the tests. Overall the title clearly communicates the main intent and is appropriate for a teammate scanning history.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/exec-bit

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.17%. Comparing base (9fefd95) to head (bf9a0b9).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #175   +/-   ##
=======================================
  Coverage   95.17%   95.17%           
=======================================
  Files           5        5           
  Lines         394      394           
=======================================
  Hits          375      375           
  Misses         19       19           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9fefd95 and bf9a0b9.

📒 Files selected for processing (2)
  • CLAUDE.md (1 hunks)
  • tests/Functional/ExecutableBitTest.php (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
tests/Functional/ExecutableBitTest.php (3)
tests/Functional/FunctionalTestBase.php (2)
  • FunctionalTestBase (15-220)
  • assertArtifactCommandSuccess (109-120)
tests/Traits/FixtureTrait.php (1)
  • fixtureCreateFile (48-66)
tests/Traits/GitTrait.php (2)
  • gitCommitAll (241-245)
  • gitCheckout (41-59)
🪛 markdownlint-cli2 (0.17.2)
CLAUDE.md

11-11: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


12-12: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


15-15: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


16-16: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


19-19: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


20-20: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


24-24: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


25-25: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


32-32: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


33-33: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


36-36: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


38-38: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


41-41: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


43-43: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


50-50: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


52-52: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


57-57: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


58-58: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


61-61: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


62-62: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


88-88: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


94-94: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


96-96: Files should end with a single newline character

(MD047, single-trailing-newline)

🪛 PHPMD (2.15.0)
tests/Functional/ExecutableBitTest.php

32-32: The variable $file_path is not named in camelCase. (undefined)

(CamelCaseVariableName)


45-45: The variable $expected_perm is not named in camelCase. (undefined)

(CamelCaseVariableName)


46-46: The variable $dst_file is not named in camelCase. (undefined)

(CamelCaseVariableName)


48-48: The variable $actual_perm is not named in camelCase. (undefined)

(CamelCaseVariableName)

🔇 Additional comments (1)
tests/Functional/ExecutableBitTest.php (1)

14-24: Good: targeted coverage with FORCE_PUSH and precise @Covers.

Covers the right classes and forces the intended mode in setup.

Comment thread CLAUDE.md
Comment on lines +26 to +29
public function testFilePermissions(): void {
$permissions = [0644, 0755];
$files = [];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Skip on Windows where chmod/fileperms are unreliable.

Prevents false negatives on Windows workers.

   public function testFilePermissions(): void {
+    if (PHP_OS_FAMILY === 'Windows') {
+      $this->markTestSkipped('File permission bits are not reliable on Windows.');
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public function testFilePermissions(): void {
$permissions = [0644, 0755];
$files = [];
public function testFilePermissions(): void {
if (PHP_OS_FAMILY === 'Windows') {
$this->markTestSkipped('File permission bits are not reliable on Windows.');
}
$permissions = [0644, 0755];
$files = [];
🤖 Prompt for AI Agents
In tests/Functional/ExecutableBitTest.php around lines 26 to 29, the test should
be skipped on Windows because chmod/fileperms behavior is unreliable there; add
a platform check at the start of testFilePermissions (for example using
PHP_OS_FAMILY === 'Windows' or a WIN prefix check) and call
$this->markTestSkipped('Skipping on Windows where chmod/fileperms are
unreliable.') if running on Windows so the test does not run and produce false
negatives.

Comment on lines +30 to +51
foreach ($permissions as $perm) {
$filename = sprintf('file_%o.txt', $perm);
$file_path = $this->fixtureCreateFile($this->src, $filename, sprintf("Content with %o permissions\n", $perm));
chmod($file_path, $perm);
$files[$filename] = $perm;

$this->assertSame($perm, fileperms($file_path) & 0777);
}

$this->gitCommitAll($this->src, 'Added files with various permissions');

$this->assertArtifactCommandSuccess(['--branch' => 'testbranch']);

$this->gitCheckout($this->dst, 'testbranch');

foreach ($files as $filename => $expected_perm) {
$dst_file = $this->dst . DIRECTORY_SEPARATOR . $filename;
$this->assertFileExists($dst_file);
$actual_perm = fileperms($dst_file) & 0777;
$this->assertSame($expected_perm, $actual_perm,
sprintf('File %s should preserve %o permissions, got %o', $filename, $expected_perm, $actual_perm));
}

@coderabbitai coderabbitai Bot Sep 11, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Make the test robust across different umasks: assert only the executable bit.

Git tracks only the executable bit; exact 0644/0755 bits after checkout depend on the runner’s umask (e.g., 002 → 0664/0775), which can make this test flaky. Assert owner exec bit instead and fix camelCase names while at it.

@@
-    foreach ($permissions as $perm) {
-      $filename = sprintf('file_%o.txt', $perm);
-      $file_path = $this->fixtureCreateFile($this->src, $filename, sprintf("Content with %o permissions\n", $perm));
-      chmod($file_path, $perm);
-      $files[$filename] = $perm;
-
-      $this->assertSame($perm, fileperms($file_path) & 0777);
-    }
+    foreach ($permissions as $perm) {
+      $filename = sprintf('file_%o.txt', $perm);
+      $filePath = $this->fixtureCreateFile($this->src, $filename, sprintf("Content with %o permissions\n", $perm));
+      chmod($filePath, $perm);
+      // Store only the expectation for the executable bit (owner).
+      $files[$filename] = (bool) ($perm & 0100);
+
+      // Local pre-check can assert exact perms after chmod.
+      $this->assertSame($perm, fileperms($filePath) & 0777);
+    }
@@
-    foreach ($files as $filename => $expected_perm) {
-      $dst_file = $this->dst . DIRECTORY_SEPARATOR . $filename;
-      $this->assertFileExists($dst_file);
-      $actual_perm = fileperms($dst_file) & 0777;
-      $this->assertSame($expected_perm, $actual_perm,
-        sprintf('File %s should preserve %o permissions, got %o', $filename, $expected_perm, $actual_perm));
-    }
+    foreach ($files as $filename => $expectedExec) {
+      $dstFile = $this->dst . DIRECTORY_SEPARATOR . $filename;
+      $this->assertFileExists($dstFile);
+      $actualPerm = fileperms($dstFile) & 0777;
+      $actualExec = (bool) ($actualPerm & 0100);
+      $this->assertSame(
+        $expectedExec,
+        $actualExec,
+        sprintf('File %s should preserve owner executable bit; got perms %o', $filename, $actualPerm)
+      );
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
foreach ($permissions as $perm) {
$filename = sprintf('file_%o.txt', $perm);
$file_path = $this->fixtureCreateFile($this->src, $filename, sprintf("Content with %o permissions\n", $perm));
chmod($file_path, $perm);
$files[$filename] = $perm;
$this->assertSame($perm, fileperms($file_path) & 0777);
}
$this->gitCommitAll($this->src, 'Added files with various permissions');
$this->assertArtifactCommandSuccess(['--branch' => 'testbranch']);
$this->gitCheckout($this->dst, 'testbranch');
foreach ($files as $filename => $expected_perm) {
$dst_file = $this->dst . DIRECTORY_SEPARATOR . $filename;
$this->assertFileExists($dst_file);
$actual_perm = fileperms($dst_file) & 0777;
$this->assertSame($expected_perm, $actual_perm,
sprintf('File %s should preserve %o permissions, got %o', $filename, $expected_perm, $actual_perm));
}
foreach ($permissions as $perm) {
$filename = sprintf('file_%o.txt', $perm);
$filePath = $this->fixtureCreateFile($this->src, $filename, sprintf("Content with %o permissions\n", $perm));
chmod($filePath, $perm);
// Store only the expectation for the executable bit (owner).
$files[$filename] = (bool) ($perm & 0100);
// Local pre-check can assert exact perms after chmod.
$this->assertSame($perm, fileperms($filePath) & 0777);
}
$this->gitCommitAll($this->src, 'Added files with various permissions');
$this->assertArtifactCommandSuccess(['--branch' => 'testbranch']);
$this->gitCheckout($this->dst, 'testbranch');
foreach ($files as $filename => $expectedExec) {
$dstFile = $this->dst . DIRECTORY_SEPARATOR . $filename;
$this->assertFileExists($dstFile);
$actualPerm = fileperms($dstFile) & 0777;
$actualExec = (bool) ($actualPerm & 0100);
$this->assertSame(
$expectedExec,
$actualExec,
sprintf('File %s should preserve owner executable bit; got perms %o', $filename, $actualPerm)
);
}
🧰 Tools
🪛 PHPMD (2.15.0)

32-32: The variable $file_path is not named in camelCase. (undefined)

(CamelCaseVariableName)


45-45: The variable $expected_perm is not named in camelCase. (undefined)

(CamelCaseVariableName)


46-46: The variable $dst_file is not named in camelCase. (undefined)

(CamelCaseVariableName)


48-48: The variable $actual_perm is not named in camelCase. (undefined)

(CamelCaseVariableName)

🤖 Prompt for AI Agents
In tests/Functional/ExecutableBitTest.php around lines 30 to 51, the test
currently compares full permission bits which is flaky due to differing umasks;
change it to assert only the owner-executable bit (mask 0100) by storing
$files[$filename] = $perm & 0100 when creating fixtures and later comparing
(fileperms($dstFile) & 0100) to the expected value; also rename snake_case
variables to camelCase (e.g., $file_path → $filePath, $dst_file → $dstFile,
$expected_perm → $expectedPerm, $actual_perm → $actualPerm) and update the
assertion message accordingly so it reports the owner-exec bit only.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but I actually need a bulletproof way to preserve those bits

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🌐 Web query:

git preserve file permissions umask core.filemode

💡 Result:

Short answer

  • Git only records the owner-executable bit (two modes: 100644 non-executable, 100755 executable). It does not track full rwx permissions or ownership. [1][3]
  • core.fileMode tells Git whether to pay attention to executable-bit differences (true by default). Set it false to ignore mode-only changes, true to record/notice executable changes. [1][3]
  • umask affects the permission bits that the OS gives newly checked-out files (and Git’s checkout behavior), so checkouts may lose non-tracked bits; Git will apply the stored exec/non-exec mode but the final mode is masked by your umask (and core.sharedRepository). [2][3]

Useful commands

  • See current setting for this repo:
    git config core.fileMode
  • Make Git ignore exec-bit changes (per-repo or global):
    git config core.fileMode false
    git config --global core.fileMode false
  • Make a file executable in the repo (so Git records it):
    git update-index --chmod=+x path/to/file
    git commit -m "make script executable"
  • Control repository checkout permissions (instead of umask):
    git config core.sharedRepository 0644
  • If you need to preserve full permissions/ownership you must use external tooling (hooks, tar, git-archive, or metadata plugins) because Git itself only stores the exec bit. [1][2][3]

References
[1] Stack Overflow — Git stores only exec bit; core.fileMode docs.
[2] Unix.StackExchange — checkout uses umask and core.sharedRepository.
[3] Baeldung (Linux) / other Git docs — explanation of 100644 vs 100755 and core.fileMode.


🏁 Script executed:

#!/bin/bash
# Look for permission-related code in the Git Artifact codebase
echo "=== Searching for permission-related code ==="
rg -n -C3 "chmod|fileperms|permission|0[67][4-7][4-7]" --type=php

echo -e "\n=== Looking for git config usage ==="
rg -n -C3 "fileMode|sharedRepository|core\." --type=php

echo -e "\n=== Checking for hooks or special git handling ==="
rg -n -C3 "hook|git.*config|update-index" --type=php

echo -e "\n=== Looking at the main Git repository class ==="
fd -e php ArtifactGitRepository | head -5 | xargs cat

Length of output: 21108


@AlexSkrypnyk I understand you need bulletproof permission preservation. Based on my research, Git only tracks the executable bit (100644 vs 100755), not full rwx permissions. Here are several approaches to achieve bulletproof permission preservation:

Option 1: Metadata File Approach (Recommended)

Store permissions in a separate metadata file that gets committed alongside your code:

Option 2: Enhanced Test with umask Control

For your test, you can control the umask to ensure consistent behavior:

public function testFilePermissions(): void {
    // Store original umask and set predictable one
    $originalUmask = umask(0022);
    
    try {
        $permissions = [0644, 0755];
        $files = [];

        foreach ($permissions as $perm) {
            $filename = sprintf('file_%o.txt', $perm);
            $filePath = $this->fixtureCreateFile($this->src, $filename, sprintf("Content with %o permissions\n", $perm));
            chmod($filePath, $perm);
            $files[$filename] = $perm;

            $this->assertSame($perm, fileperms($filePath) & 0777);
        }

        // Store permissions metadata before git operations
        $this->storePermissionsMetadata($this->src, $files);
        
        $this->gitCommitAll($this->src, 'Added files with various permissions');
        $this->assertArtifactCommandSuccess(['--branch' => 'testbranch']);
        $this->gitCheckout($this->dst, 'testbranch');
        
        // Restore permissions from metadata after checkout
        $this->restorePermissionsFromMetadata($this->dst);

        foreach ($files as $filename => $expectedPerm) {
            $dstFile = $this->dst . DIRECTORY_SEPARATOR . $filename;
            $this->assertFileExists($dstFile);
            $actualPerm = fileperms($dstFile) & 0777;
            $this->assertSame($expectedPerm, $actualPerm,
                sprintf('File %s should preserve %o permissions, got %o', $filename, $expectedPerm, $actualPerm));
        }
    } finally {
        // Restore original umask
        umask($originalUmask);
    }
}

Option 3: Git Configuration Approach

Configure git settings to be more predictable:

Option 4: Integration into ArtifactCommand

You could extend your ArtifactCommand to handle permissions:

// Add to ArtifactCommand class
private function preservePermissions(string $sourcePath, string $destPath): void {
    $permissionsFile = $sourcePath . '/.permissions.json';
    $permissions = [];
    
    // Collect all file permissions
    $iterator = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($sourcePath)
    );
    
    foreach ($iterator as $file) {
        if ($file->isFile() && !str_contains($file->getPathname(), '.git/')) {
            $relativePath = substr($file->getPathname(), strlen($sourcePath) + 1);
            $permissions[$relativePath] = fileperms($file->getPathname()) & 0777;
        }
    }
    
    // Store permissions metadata
    file_put_contents($permissionsFile, json_encode($permissions, JSON_PRETTY_PRINT));
}

private function restorePermissions(string $basePath): void {
    $permissionsFile = $basePath . '/.permissions.json';
    if (!file_exists($permissionsFile)) {
        return;
    }
    
    $permissions = json_decode(file_get_contents($permissionsFile), true);
    foreach ($permissions as $relativePath => $perm) {
        $fullPath = $basePath . '/' . $relativePath;
        if (file_exists($fullPath)) {
            chmod($fullPath, $perm);
        }
    }
}

Recommendation

For bulletproof permission preservation, I recommend Option 1 (Metadata File Approach) because:

  1. Git-agnostic: Works regardless of git's limitations
  2. Comprehensive: Preserves all permission bits, not just executable
  3. Versioned: Permissions are tracked in git alongside code
  4. Portable: Works across different systems and umasks
  5. Auditable: Permission changes are visible in git history

Would you like me to help you implement this metadata-based approach in your Git Artifact tool?


🏁 Script executed:

#!/bin/bash
# Create example implementation for permission preservation
cat > /tmp/permission_preservation_example.php << 'EOF'
<?php

// Example: Store and restore file permissions using metadata file
class PermissionPreserver {
    private const METADATA_FILE = '.permissions.json';
    
    public function storePermissions(string $basePath): void {
        $permissions = [];
        $iterator = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($basePath)
        );
        
        foreach ($iterator as $file) {
            if ($file->isFile()) {
                $relativePath = substr($file->getPathname(), strlen($basePath) + 1);
                $permissions[$relativePath] = fileperms($file->getPathname()) & 0777;
            }
        }
        
        file_put_contents(
            $basePath . '/' . self::METADATA_FILE, 
            json_encode($permissions, JSON_PRETTY_PRINT)
        );
    }
    
    public function restorePermissions(string $basePath): void {
        $metadataPath = $basePath . '/' . self::METADATA_FILE;
        if (!file_exists($metadataPath)) {
            return;
        }
        
        $permissions = json_decode(file_get_contents($metadataPath), true);
        foreach ($permissions as $relativePath => $perm) {
            $fullPath = $basePath . '/' . $relativePath;
            if (file_exists($fullPath)) {
                chmod($fullPath, $perm);
            }
        }
    }
}
EOF

echo "=== Example Permission Preservation Implementation ==="
cat /tmp/permission_preservation_example.php

Length of output: 1385


🏁 Script executed:

#!/bin/bash
# Show git configuration options for permission handling
cat << 'EOF'
# In your Git Artifact tool, you could set these configurations:

# Ensure git tracks executable bit changes
git config core.fileMode true

# Set shared repository permissions (affects new files)
git config core.sharedRepository 0644  # or 0755 for group writable

# Alternative: Use git hooks to preserve/restore permissions
# Create pre-commit hook to store permissions
# Create post-checkout hook to restore permissions

EOF

Length of output: 423


The metadata file approach is the most reliable solution for bulletproof permission preservation. Here's why it's superior to the other options:

Why Metadata File Approach Works Best:

  1. Complete Independence from Git: Stores full permission data separately from git's limited tracking
  2. Umask Immunity: Works regardless of the system's umask settings
  3. Version Controlled: Permission changes are tracked in git history
  4. Cross-Platform: Consistent behavior across different environments
  5. Auditable: Easy to see what permissions changed and when

For your test to be bulletproof, you'd need to:

public function testFilePermissions(): void {
    $permissions = [0644, 0755];
    $files = [];

    // Create files with specific permissions
    foreach ($permissions as $perm) {
        $filename = sprintf('file_%o.txt', $perm);
        $filePath = $this->fixtureCreateFile($this->src, $filename, sprintf("Content with %o permissions\n", $perm));
        chmod($filePath, $perm);
        $files[$filename] = $perm;
        $this->assertSame($perm, fileperms($filePath) & 0777);
    }

    // Store permissions metadata BEFORE git operations
    $this->storePermissionsMetadata($this->src, $files);
    
    $this->gitCommitAll($this->src, 'Added files with various permissions');
    $this->assertArtifactCommandSuccess(['--branch' => 'testbranch']);
    $this->gitCheckout($this->dst, 'testbranch');
    
    // Restore permissions AFTER checkout
    $this->restorePermissionsFromMetadata($this->dst);

    // Now assert the restored permissions
    foreach ($files as $filename => $expectedPerm) {
        $dstFile = $this->dst . DIRECTORY_SEPARATOR . $filename;
        $this->assertFileExists($dstFile);
        $actualPerm = fileperms($dstFile) & 0777;
        $this->assertSame($expectedPerm, $actualPerm,
            sprintf('File %s should preserve %o permissions, got %o', $filename, $expectedPerm, $actualPerm));
    }
}

Next Steps:

  1. Integrate into ArtifactCommand: Add permission preservation logic to your artifact creation/restoration process
  2. Add configuration option: Allow users to enable/disable permission preservation
  3. Handle .permissions.json in .gitignore: Decide if you want it tracked or not
  4. Add logging: Track when permissions are stored/restored

Would you like me to help you implement this permission preservation system directly into your ArtifactCommand and ArtifactGitRepository classes? I can show you exactly where to add the hooks and how to integrate it with your existing workflow.


✏️ Learnings added
Learnt from: AlexSkrypnyk
PR: drevops/git-artifact#175
File: tests/Functional/ExecutableBitTest.php:30-51
Timestamp: 2025-09-11T08:24:01.852Z
Learning: The user AlexSkrypnyk needs bulletproof file permission preservation in the Git Artifact tool, not just executable bit tracking that git provides natively. Full rwx permission bits must be preserved during artifact operations.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


$this->gitCommitAll($this->src, 'Added files with various permissions');

$this->assertArtifactCommandSuccess(['--branch' => 'testbranch']);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Remove redundant branch arg.

The helper already defaults to branch "testbranch".

-    $this->assertArtifactCommandSuccess(['--branch' => 'testbranch']);
+    $this->assertArtifactCommandSuccess();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$this->assertArtifactCommandSuccess(['--branch' => 'testbranch']);
$this->assertArtifactCommandSuccess();
🤖 Prompt for AI Agents
In tests/Functional/ExecutableBitTest.php around line 41, the test calls
$this->assertArtifactCommandSuccess(['--branch' => 'testbranch']) but the helper
already defaults to branch "testbranch"; remove the redundant ['--branch' =>
'testbranch'] argument and call the helper with no branch override so the
default is used.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
CLAUDE.md (1)

1-96: Fix markdownlint issues (blank lines, trailing spaces, fenced blocks, final newline).

Brings the doc in line with MD022/MD032/MD031/MD047.

 # CLAUDE.md
 
 This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
 
 ## Project Overview
+
 Git Artifact is a PHP CLI tool that packages code artifacts from a source repository and pushes them to a destination repository. It's designed for deployment workflows where built artifacts need to be deployed to a separate repository.
 
 ## Development Commands
 
 ### Code Quality and Linting
+
 - `composer lint` - Run code quality checks (PHPCS, PHPStan, Rector dry-run)
 - `composer lint-fix` - Fix code quality issues (Rector, PHPCBF)
 
-### Testing  
+### Testing
+
 - `composer test` - Run PHPUnit tests without coverage
 - `composer test-coverage` - Run PHPUnit tests with coverage
 
 ### Building
+
 - `composer build` - Build standalone PHAR executable using Box
 
 ## Architecture
 
 ### Core Components
+
 - **ArtifactCommand** (`src/Commands/ArtifactCommand.php`) - Main CLI command that orchestrates the artifact creation process
 - **ArtifactGitRepository** (`src/Git/ArtifactGitRepository.php`) - Extended Git repository class with artifact-specific operations
 - **Traits** (`src/Traits/`) - Reusable functionality:
   - `TokenTrait` - Handle token replacement in branch names and commit messages
   - `LoggerTrait` - Logging functionality
   - `FilesystemTrait` - File system operations
 
 ### Application Entry Points
+
 - `git-artifact` - Main executable script with autoloader discovery
 - `src/app.php` - Application bootstrap using Symfony Console
 
 ### Deployment Modes
+
 The tool supports two deployment modes:
+
 1. **force-push** (default) - Pushes to same branch, overwrites destination history
 2. **branch** - Creates new branches in destination based on source tags
 
 ### Token System
+
 Supports dynamic token replacement in branch names and commit messages:
+
 - `[timestamp:FORMAT]` - Current timestamp with PHP date format
 - `[branch]` - Current branch name
 - `[safebranch]` - Branch name with non-alphanumeric chars replaced
 - `[tags:DELIMITER]` - Tags from latest commit
 
 ## Testing Structure
 
 ### Functional Tests (`tests/Functional/`)
+
 Integration tests that verify end-to-end functionality:
+
 - `BranchModeTest.php` - Tests branch mode deployment
 - `ForcePushModeTest.php` - Tests force-push mode deployment  
 - `GeneralTest.php` - General functionality tests
 - `TagTest.php` - Token replacement and tagging tests
 
 ### Unit Tests (`tests/Unit/`)
+
 - Component-level tests for individual classes
 - Follow PHPUnit conventions with strict typing
 
 ### Test Fixtures Naming Convention
+
 - `f*` - Files with counter suffix
 - `i` - Ignored files
 - `c` - Committed files  
 - `u` - Uncommitted files
 - `d` - Deleted files
 - `d_` - Directories
 - `sub_` - Sub-directories/files for wildcard testing
 - `f*_l` - Symlinks
 
 ## Code Standards
+
 - Follows Drupal coding standards via PHPCS
 - Uses PHPStan level 9 for static analysis  
 - Rector for automated code modernization to PHP 8.2+
 - PSR-4 autoloading: `DrevOps\GitArtifact\` namespace maps to `src/`
 
 ## Key Dependencies
+
 - `symfony/console` - CLI framework
 - `czproject/git-php` - Git operations
 - `symfony/filesystem` & `symfony/finder` - File operations
 - `monolog/monolog` - Logging
 
 ## Single Test Execution
+
 Run specific test classes:
+
 ```bash
 ./vendor/bin/phpunit tests/Unit/TokenTest.php
 ./vendor/bin/phpunit tests/Functional/BranchModeTest.php

Run specific test methods:
+

./vendor/bin/phpunit --filter testTokenReplacement tests/Unit/TokenTest.php

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used**: CodeRabbit UI

**Review profile**: ASSERTIVE

**Plan**: Pro

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between bf9a0b9cffaa4a3782080881593955ed7647fd0e and 9f3fc7a471da47612fcf98cfed58b750983be333.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `CLAUDE.md` (1 hunks)
* `src/Git/ArtifactGitRepository.php` (1 hunks)
* `tests/Functional/FilePermissionsTest.php` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧠 Learnings (2)</summary>

<details>
<summary>📓 Common learnings</summary>

Learnt from: AlexSkrypnyk
PR: #175
File: tests/Functional/ExecutableBitTest.php:30-51
Timestamp: 2025-09-11T08:24:01.852Z
Learning: The user AlexSkrypnyk needs bulletproof file permission preservation in the Git Artifact tool, not just executable bit tracking that git provides natively. Full rwx permission bits must be preserved during artifact operations.


</details>
<details>
<summary>📚 Learning: 2025-09-11T08:24:01.852Z</summary>

Learnt from: AlexSkrypnyk
PR: #175
File: tests/Functional/ExecutableBitTest.php:30-51
Timestamp: 2025-09-11T08:24:01.852Z
Learning: The user AlexSkrypnyk needs bulletproof file permission preservation in the Git Artifact tool, not just executable bit tracking that git provides natively. Full rwx permission bits must be preserved during artifact operations.


**Applied to files:**
- `tests/Functional/FilePermissionsTest.php`

</details>

</details><details>
<summary>🧬 Code graph analysis (1)</summary>

<details>
<summary>tests/Functional/FilePermissionsTest.php (4)</summary><blockquote>

<details>
<summary>src/Git/ArtifactGitRepository.php (1)</summary>

* `ArtifactGitRepository` (20-479)

</details>
<details>
<summary>tests/Functional/FunctionalTestBase.php (1)</summary>

* `FunctionalTestBase` (15-220)

</details>
<details>
<summary>tests/Traits/FixtureTrait.php (1)</summary>

* `fixtureCreateFile` (48-66)

</details>
<details>
<summary>tests/Traits/GitTrait.php (2)</summary>

* `gitCommitAll` (241-245)
* `gitCheckout` (41-59)

</details>

</blockquote></details>

</details><details>
<summary>🪛 PHPMD (2.15.0)</summary>

<details>
<summary>tests/Functional/FilePermissionsTest.php</summary>

32-32: The variable $file_path is not named in camelCase. (undefined)

(CamelCaseVariableName)

---

45-45: The variable $expected_perm is not named in camelCase. (undefined)

(CamelCaseVariableName)

---

46-46: The variable $dst_file is not named in camelCase. (undefined)

(CamelCaseVariableName)

---

48-48: The variable $actual_perm is not named in camelCase. (undefined)

(CamelCaseVariableName)

</details>

</details>
<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

<details>
<summary>CLAUDE.md</summary>

11-11: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

---

12-12: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)

---

15-15: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

---

16-16: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)

---

19-19: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

---

20-20: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)

---

24-24: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

---

25-25: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)

---

32-32: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

---

33-33: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)

---

36-36: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

---

38-38: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)

---

41-41: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

---

43-43: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)

---

50-50: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

---

52-52: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)

---

57-57: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

---

58-58: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)

---

61-61: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

---

62-62: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)

---

88-88: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)

---

94-94: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)

---

96-96: Files should end with a single newline character

(MD047, single-trailing-newline)

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (2)</summary><blockquote>

<details>
<summary>src/Git/ArtifactGitRepository.php (1)</summary><blockquote>

`47-49`: **Good call: enforce Git filemode tracking.**

Setting core.filemode=true at repo init ensures executable-bit changes are detected.

</blockquote></details>
<details>
<summary>tests/Functional/FilePermissionsTest.php (1)</summary><blockquote>

`26-51`: **Solid end-to-end check for 0644 vs 0755 preservation.**

Covers creation, commit, artifact run, and verification on destination.

</blockquote></details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Comment thread src/Git/ArtifactGitRepository.php Outdated
Comment on lines +47 to +49
// Ensure Git respects file permissions.
$this->execute('config', ['core.filemode', 'true']);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

This only preserves the executable bit; full rwx preservation needs extra handling.

If the goal is to keep all permission bits (e.g., 0600 vs 0644), Git alone won’t suffice. Consider:

  • Archive-based transfer (git archive | tar) and extract with preserved modes.
  • Sidecar metadata (e.g., .gitartifact-perms.json mapping path→mode) applied post-checkout.
  • Clean/smudge filter to store/restore modes via .gitattributes.

Also verify both source and destination repos used by ArtifactCommand are instantiated as ArtifactGitRepository so this config is applied end-to-end.


🏁 Script executed:

#!/bin/bash
# Locate ArtifactGitRepository usage in the app code (not tests).
rg -nP --type=php -C3 '\bnew\s+ArtifactGitRepository\s*\(' src || true

# Sanity-check: where repos for src/dst are created inside ArtifactCommand.
rg -nP --type=php -C3 'class\s+ArtifactCommand\b|ArtifactGitRepository' src/Commands || true

Length of output: 3017


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- new ArtifactGitRepository usages ----"
rg -nP --type=php '\bnew\s+ArtifactGitRepository\b' -S || true
echo

echo "---- class definition locations for ArtifactGitRepository ----"
rg -nP --type=php 'class\s+ArtifactGitRepository\b' -n -S || true
echo

echo "---- show src/Git/ArtifactGitRepository.php (first 240 lines) ----"
sed -n '1,240p' src/Git/ArtifactGitRepository.php || true
echo

echo "---- search for core.filemode occurrences ----"
rg -n 'core.filemode' -S || true
echo

echo "---- show src/Commands/ArtifactCommand.php (lines 1-520) ----"
sed -n '1,520p' src/Commands/ArtifactCommand.php || true
echo

echo "---- search for destination-related variables/usages in src/Commands ----"
rg -nP --type=php '\bdestination\b|\bdestinationDir\b|\bdestinationRepo\b|\bdest\b' src/Commands -S || true
echo

echo "---- search for archive/tar/gitattributes/filter/smudge/clean mentions ----"
rg -nP --type=php 'archive|tar\b|gitattributes|smudge|clean|filter' -S || true

Length of output: 25316


Preserve full file modes — core.filemode only tracks the executable bit

  • core.filemode only preserves the executable bit (does not keep full rwx like 0600 vs 0644). The constructor currently sets it: src/Git/ArtifactGitRepository.php:48.
  • Options to implement full-mode preservation: (a) transfer via git archive | tar and extract (preserves modes); (b) sidecar metadata (e.g. .gitartifact-perms.json mapping path→mode) reapplied after checkout; (c) clean/smudge filter with .gitattributes to encode/restore modes.
  • ArtifactCommand instantiates ArtifactGitRepository only for the source ($this->repo = new ArtifactGitRepository(...); src/Commands/ArtifactCommand.php:327). The destination remote is not instantiated/configured — if the remote can be a local repo you must either configure the destination repo or use a transfer mechanism that does not rely on remote git config.
  • Action: choose and implement one of the preservation strategies and ensure destination handling for local remotes; update tests to assert mode preservation.
🤖 Prompt for AI Agents
In src/Git/ArtifactGitRepository.php around lines 47-49, the constructor enables
Git core.filemode which only preserves the executable bit and not full rwx
modes; replace this by implementing one preservation strategy across the
repository transfer path and ensure destination handling for local remotes. Pick
one approach (recommended: use git archive | tar for transfers to preserve full
filesystem modes, or alternatively implement a sidecar JSON mapping of path→mode
written at source and reapplied at destination, or implement a clean/smudge
filter with .gitattributes to encode modes), then: update ArtifactGitRepository
to perform transfers via git archive piped to tar when exporting/importing
artifacts (or write a sidecar file after checkout and apply chmods on checkout
at destination), ensure ArtifactCommand handles local destination repos by
detecting local remotes and applying the same transfer path without relying on
remote git config, and add/adjust unit/integration tests to assert that original
file modes (e.g., 0600 vs 0644 and executable bits) are preserved after
transfer.

Comment on lines +21 to +24
protected function setUp(): void {
parent::setUp();
$this->mode = ArtifactCommand::MODE_FORCE_PUSH;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Skip on Windows to avoid false failures.

Windows filesystems don’t reliably reflect POSIX permission bits; guard in setUp().

   protected function setUp(): void {
     parent::setUp();
+    if (PHP_OS_FAMILY === 'Windows') {
+      $this->markTestSkipped('POSIX file permission bits are not reliable on Windows.');
+    }
     $this->mode = ArtifactCommand::MODE_FORCE_PUSH;
   }
🤖 Prompt for AI Agents
In tests/Functional/FilePermissionsTest.php around lines 21 to 24, the test must
be skipped on Windows because NTFS doesn't reliably reflect POSIX permission
bits; modify setUp() to detect Windows (e.g. PHP_OS_FAMILY === 'Windows' or
DIRECTORY_SEPARATOR === '\\') and call $this->markTestSkipped('Skipping on
Windows: filesystem permissions not reliable') before setting $this->mode so the
rest of the test doesn't run on Windows.

@AlexSkrypnyk AlexSkrypnyk enabled auto-merge (squash) September 11, 2025 10:06
@AlexSkrypnyk AlexSkrypnyk merged commit 5161d7f into main Sep 11, 2025
10 checks passed
@AlexSkrypnyk AlexSkrypnyk deleted the feature/exec-bit branch September 11, 2025 10:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants