From cd70cbdf927e8f46eef47439c7080d6d1e46a441 Mon Sep 17 00:00:00 2001 From: "homeboy-ci[bot]" <266378653+homeboy-ci[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 09:22:47 -0400 Subject: [PATCH] feat(workspace): enforce configured Git identities --- inc/Workspace/Workspace.php | 2 + inc/Workspace/WorkspaceGitIdentityPolicy.php | 118 +++++++++++++++++++ inc/Workspace/WorkspaceGitOperations.php | 8 ++ inc/Workspace/WorkspaceWorktreeLifecycle.php | 6 + tests/workspace-git-identity-policy.php | 92 +++++++++++++++ 5 files changed, 226 insertions(+) create mode 100644 inc/Workspace/WorkspaceGitIdentityPolicy.php create mode 100644 tests/workspace-git-identity-policy.php diff --git a/inc/Workspace/Workspace.php b/inc/Workspace/Workspace.php index 3744ad3b..dffa4b1a 100644 --- a/inc/Workspace/Workspace.php +++ b/inc/Workspace/Workspace.php @@ -21,6 +21,7 @@ require_once __DIR__ . '/WorkspaceArtifactCleanup.php'; require_once __DIR__ . '/WorkspaceCleanupPlan.php'; require_once __DIR__ . '/WorkspaceGitOperations.php'; +require_once __DIR__ . '/WorkspaceGitIdentityPolicy.php'; require_once __DIR__ . '/WorkspaceHygieneReport.php'; require_once __DIR__ . '/WorkspaceMetadataReconciliation.php'; require_once __DIR__ . '/WorkspaceRepositoryLifecycle.php'; @@ -41,6 +42,7 @@ class Workspace { use WorkspaceArtifactCleanup; use WorkspaceCleanupPlan; use WorkspaceGitOperations; + use WorkspaceGitIdentityPolicy; use WorkspaceHygieneReport; use WorkspaceMetadataReconciliation; use WorkspaceRepositoryLifecycle; diff --git a/inc/Workspace/WorkspaceGitIdentityPolicy.php b/inc/Workspace/WorkspaceGitIdentityPolicy.php new file mode 100644 index 00000000..b9939b88 --- /dev/null +++ b/inc/Workspace/WorkspaceGitIdentityPolicy.php @@ -0,0 +1,118 @@ +repository_git_identity_policy($repo_path); + if ( is_wp_error($identity) ) { + return $identity; + } + if ( null === $identity ) { + return null; + } + + $name_result = $this->run_git($repo_path, 'config --get user.name'); + $email_result = $this->run_git($repo_path, 'config --get user.email'); + $name = $this->git_config_value($name_result); + $email = $this->git_config_value($email_result); + if ( $identity['name'] !== $name || $identity['email'] !== $email ) { + return new \WP_Error( + 'repository_git_identity_mismatch', + 'Refusing to commit because the effective Git user.name and user.email do not satisfy this repository host identity policy.', + array( 'status' => 409 ) + ); + } + + return null; + } + + /** + * Configure a managed worktree when its remote-specific policy provides identity. + */ + private function configure_repository_git_identity( string $repo_path ): ?\WP_Error { + $identity = $this->repository_git_identity_policy($repo_path); + if ( is_wp_error($identity) ) { + return $identity; + } + if ( null === $identity ) { + return null; + } + + // Worktree config prevents an identity selected for one branch from changing its primary checkout. + foreach ( array( + 'config extensions.worktreeConfig true', + 'config --worktree user.name ' . escapeshellarg($identity['name']), + 'config --worktree user.email ' . escapeshellarg($identity['email']), + ) as $command ) { + $result = $this->run_git($repo_path, $command); + if ( is_wp_error($result) ) { + return $result; + } + } + + return null; + } + + /** + * Resolve a configured identity policy for the repository's origin host. + * + * @return array{name:string,email:string}|null|\WP_Error + */ + private function repository_git_identity_policy( string $repo_path ): array|null|\WP_Error { + $remote_result = $this->run_git($repo_path, 'remote get-url origin'); + if ( is_wp_error($remote_result) ) { + return null; + } + + $remote = trim((string) ($remote_result['output'] ?? '')); + $descriptor = GitHubRemote::descriptor($remote); + if ( null === $descriptor || ! function_exists('apply_filters') ) { + return null; + } + + $identity = apply_filters('datamachine_code_git_identity_policy', null, $descriptor, $remote); + if ( null === $identity ) { + return null; + } + if ( ! is_array($identity) ) { + return new \WP_Error('invalid_git_identity_policy', 'Git identity policy must return an array with non-empty name and email values.', array( 'status' => 500 )); + } + + $name = trim((string) ($identity['name'] ?? '')); + $email = trim((string) ($identity['email'] ?? '')); + if ( '' === $name || '' === $email ) { + return new \WP_Error('invalid_git_identity_policy', 'Git identity policy must provide non-empty name and email values.', array( 'status' => 500 )); + } + + return array( 'name' => $name, 'email' => $email ); + } + + /** + * Extract the effective Git config value. + */ + private function git_config_value( array|\WP_Error $result ): string { + if ( is_wp_error($result) ) { + return ''; + } + + return trim((string) ($result['output'] ?? '')); + } +} diff --git a/inc/Workspace/WorkspaceGitOperations.php b/inc/Workspace/WorkspaceGitOperations.php index 46154a64..350a01a6 100644 --- a/inc/Workspace/WorkspaceGitOperations.php +++ b/inc/Workspace/WorkspaceGitOperations.php @@ -518,6 +518,10 @@ public function git_commit( string $handle, string $message, bool $allow_primary if ( is_wp_error($attestation) ) { return $attestation; } + $identity_check = $this->enforce_repository_git_identity($repo_path); + if ( null !== $identity_check ) { + return $identity_check; + } $commit = $this->run_git($repo_path, 'commit -m ' . escapeshellarg($message)); if ( is_wp_error($commit) ) { @@ -953,6 +957,10 @@ public function pr_rebase( string $handle, string|int|null $pr = null, bool $squ if ( '' !== $body ) { $commit_cmd .= ' -m ' . escapeshellarg($body); } + $identity_check = $this->enforce_repository_git_identity($repo_path); + if ( null !== $identity_check ) { + return $identity_check; + } $commit = $this->run_git($repo_path, $commit_cmd); if ( is_wp_error($commit) ) { return $commit; diff --git a/inc/Workspace/WorkspaceWorktreeLifecycle.php b/inc/Workspace/WorkspaceWorktreeLifecycle.php index 23e28194..3eb44375 100644 --- a/inc/Workspace/WorkspaceWorktreeLifecycle.php +++ b/inc/Workspace/WorkspaceWorktreeLifecycle.php @@ -313,6 +313,12 @@ private function worktree_add_locked( return $result; } + $identity_configuration = $this->configure_repository_git_identity($wt_path); + if ( null !== $identity_configuration ) { + $this->rollback_rejected_worktree($primary_path, $wt_path, $branch, $created_branch); + return $identity_configuration; + } + $response = array( 'success' => true, 'handle' => $wt_handle, diff --git a/tests/workspace-git-identity-policy.php b/tests/workspace-git-identity-policy.php new file mode 100644 index 00000000..b2a4bd79 --- /dev/null +++ b/tests/workspace-git-identity-policy.php @@ -0,0 +1,92 @@ +code; } +} + +function is_wp_error( mixed $value ): bool { + return $value instanceof WP_Error; +} + +$GLOBALS['workspace_git_identity_filter'] = null; +function apply_filters( string $hook, mixed $value, mixed ...$args ): mixed { + $filter = $GLOBALS['workspace_git_identity_filter']; + return 'datamachine_code_git_identity_policy' === $hook && is_callable($filter) ? $filter($value, ...$args) : $value; +} + +require_once dirname(__DIR__) . '/inc/Support/GitHubRemote.php'; +require_once dirname(__DIR__) . '/inc/Workspace/WorkspaceGitIdentityPolicy.php'; + +use DataMachineCode\Workspace\WorkspaceGitIdentityPolicy; + +function identity_policy_assert( bool $condition, string $message ): void { + if ( ! $condition ) { + throw new RuntimeException($message); + } +} + +function identity_policy_run( string $path, string $command ): array|WP_Error { + $lines = array(); + $status = 0; + exec('GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null git -C ' . escapeshellarg($path) . ' ' . $command . ' 2>&1', $lines, $status); + return 0 === $status ? array( 'output' => implode("\n", $lines) ) : new WP_Error('git_failed', implode("\n", $lines)); +} + +function identity_policy_output( array|WP_Error $result ): string { + return is_wp_error($result) ? '' : (string) ($result['output'] ?? ''); +} + +$harness = new class { + use WorkspaceGitIdentityPolicy; + public function enforce( string $path ): ?WP_Error { return $this->enforce_repository_git_identity($path); } + public function configure( string $path ): ?WP_Error { return $this->configure_repository_git_identity($path); } + private function run_git( string $path, string $command ): array|WP_Error { return identity_policy_run($path, $command); } +}; + +$root = sys_get_temp_dir() . '/dmc-git-identity-' . getmypid() . '-' . bin2hex(random_bytes(4)); +$primary = $root . '/primary'; +$worktree = $root . '/primary@identity'; + +try { + mkdir($primary, 0777, true); + identity_policy_run($primary, 'init -b main'); + identity_policy_run($primary, '-c user.name=Fixture -c user.email=fixture@example.test commit --allow-empty -m initial'); + identity_policy_run($primary, 'remote add origin git@github.com:Example-Owner/example-repo.git'); + identity_policy_run($primary, 'worktree add -b feature/identity ' . escapeshellarg($worktree)); + + $GLOBALS['workspace_git_identity_filter'] = static function ( mixed $identity, array $descriptor, string $remote ): ?array { + identity_policy_assert('Example-Owner/example-repo' === $descriptor['slug'], 'Policy receives the parsed GitHub remote descriptor.'); + identity_policy_assert('git@github.com:Example-Owner/example-repo.git' === $remote, 'Policy receives the original remote URL.'); + return array( 'name' => 'Managed Identity', 'email' => 'managed@example.test' ); + }; + identity_policy_assert(null === $harness->configure($worktree), 'Policy-backed managed worktree configures a valid identity.'); + identity_policy_assert('Managed Identity' === trim(identity_policy_output(identity_policy_run($worktree, 'config --get user.name'))), 'Managed worktree uses the policy name.'); + identity_policy_assert('managed@example.test' === trim(identity_policy_output(identity_policy_run($worktree, 'config --get user.email'))), 'Managed worktree uses the policy email.'); + identity_policy_assert('' === trim(identity_policy_output(identity_policy_run($primary, 'config --get user.name'))), 'Worktree policy identity does not alter the primary checkout.'); + identity_policy_assert(null === $harness->enforce($worktree), 'Configured host accepts its configured effective identity.'); + + identity_policy_run($worktree, 'config --worktree user.name ' . escapeshellarg('Wrong Identity')); + identity_policy_assert('repository_git_identity_mismatch' === $harness->enforce($worktree)?->get_error_code(), 'Configured host rejects a mismatched effective identity.'); + identity_policy_run($worktree, 'config --worktree --unset user.name'); + identity_policy_assert('repository_git_identity_mismatch' === $harness->enforce($worktree)?->get_error_code(), 'Configured host rejects a missing effective identity.'); + + $GLOBALS['workspace_git_identity_filter'] = null; + $bare = $root . '/bare'; + mkdir($bare, 0777, true); + identity_policy_run($bare, 'init -b main'); + identity_policy_run($bare, 'remote add origin git@github.com:Example-Owner/unconfigured-repo.git'); + identity_policy_assert(null === $harness->enforce($bare), 'Unconfigured host preserves ambient Git identity behavior.'); + + echo "workspace-git-identity-policy: ok\n"; +} finally { + if ( is_dir($root) ) { + exec('rm -rf ' . escapeshellarg($root)); + } +}