Skip to content

Commit 50be967

Browse files
authored
Merge pull request #151 from context-hub/refactor/git-diff-source
Git Components Refactoring
2 parents 7ac5bf2 + 9d2da05 commit 50be967

32 files changed

+601
-1083
lines changed

context.yaml

+17-46
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,30 @@
1-
$schema: https://raw.githubusercontent.com/context-hub/generator/refs/heads/main/json-schema.json
1+
$schema: 'https://raw.githubusercontent.com/context-hub/generator/refs/heads/main/json-schema.json'
22

33
import:
4-
- path: src/*/context.yaml
4+
- path: src/**/context.yaml
5+
6+
variables:
7+
name: Context Generator
8+
9+
prompts:
10+
- id: my-local-prompt
11+
description: My local prompt
12+
messages:
13+
- role: user
14+
content: |
15+
You are an expert in generating {{name}}. You love your work and always aim for clean, efficient, and
16+
well-structured PHP code, focusing on detail and best practices.
517
618
documents:
7-
- description: "Context Generator Project Structure"
8-
outputPath: "project-structure.md"
19+
- description: 'Project structure overview'
20+
outputPath: project-structure.md
21+
overwrite: true
922
sources:
1023
- type: tree
1124
sourcePaths:
1225
- src
1326
showCharCount: true
1427
showSize: true
15-
dirContext:
16-
"src": "Root directory containing all Context Generator source code."
17-
"src/ConfigLoader": "Configuration loading system that reads, parses, and validates config files in JSON, PHP, and YAML formats."
18-
"src/Console": "Command-line interface components providing user interaction through commands."
19-
"src/Document": "Document definition and compilation system that transforms source content into output files."
20-
"src/Fetcher": "Content fetching interfaces and registry for retrieving data from various sources."
21-
"src/Lib": "Utility libraries providing supporting functionality for the core components."
22-
"src/Modifier": "Content transformation system for filtering, formatting, and sanitizing source content."
23-
"src/Source": "Source implementations for various content locations (files, URLs, GitHub, etc.)."
24-
"src/Source/Composer": "Composer integration for accessing package dependencies."
25-
"src/Source/File": "Local filesystem source implementation."
26-
"src/Source/GitDiff": "Git diff source for accessing changes in repositories."
27-
"src/Source/Github": "GitHub API integration for remote repository access."
28-
"src/Source/Text": "Text source for embedding custom content."
29-
"src/Source/Tree": "Directory structure visualization source."
30-
"src/Source/Url": "Web URL source for retrieving online content."
31-
"src/Lib/Content": "Content building and rendering system for structured document output."
32-
"src/Lib/Finder": "File discovery components for locating content across different storage types."
33-
"src/Lib/GithubClient": "GitHub API client for repository access."
34-
"src/Lib/Html": "HTML processing utilities for web content."
35-
"src/Lib/HttpClient": "HTTP client abstraction for web requests."
36-
"src/Lib/Logger": "Logging system for operation visibility."
37-
"src/Lib/PathFilter": "Path filtering utilities for including/excluding content by pattern."
38-
"src/Lib/Sanitizer": "Content sanitization for removing sensitive information."
39-
"src/Lib/TreeBuilder": "Tree visualization generation for directory structures."
40-
"src/Lib/Variable": "Variable substitution system for configuration values."
41-
description: >-
42-
A hierarchical visualization of the Context Generator project structure, showing
43-
the main directories and files with explanations of their purpose. This provides
44-
a high-level overview of the project organization and helps understand the
45-
relationships between different components.
4628

4729
- description: Core Interfaces
4830
outputPath: core/interfaces.md
@@ -51,17 +33,6 @@ documents:
5133
sourcePaths: src
5234
filePattern:
5335
- '*Interface.php'
54-
- 'SourceInterface.php'
55-
- 'SourceModifierInterface.php'
56-
- 'FilesInterface.php'
57-
showTreeView: true
58-
59-
- description: Config parser
60-
outputPath: core/config-parser.md
61-
sources:
62-
- type: file
63-
sourcePaths: src/ConfigLoader
64-
filePattern: '*.php'
6536
showTreeView: true
6637

6738
- description: "Changes in the Project"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Butschster\ContextGenerator\Application\Bootloader;
6+
7+
use Butschster\ContextGenerator\Lib\Git\CommandsExecutor;
8+
use Butschster\ContextGenerator\Lib\Git\CommandsExecutorInterface;
9+
use Spiral\Boot\Bootloader\Bootloader;
10+
11+
final class GitClientBootloader extends Bootloader
12+
{
13+
#[\Override]
14+
public function defineSingletons(): array
15+
{
16+
return [
17+
CommandsExecutorInterface::class => CommandsExecutor::class,
18+
];
19+
}
20+
}

src/Lib/Git/Command.php

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Butschster\ContextGenerator\Lib\Git;
6+
7+
/**
8+
* A value object that represents a Git command to be executed.
9+
*/
10+
final readonly class Command implements \Stringable
11+
{
12+
/**
13+
* @param string $repository Path to the Git repository
14+
* @param array<string>|string $command Git command to execute (without 'git' prefix)
15+
*/
16+
public function __construct(
17+
public string $repository,
18+
private array|string $command,
19+
) {}
20+
21+
/**
22+
* Get the command as an array.
23+
*
24+
* @return array<string>
25+
*/
26+
public function getCommandParts(): array
27+
{
28+
if (\is_array($this->command)) {
29+
return $this->command;
30+
}
31+
32+
$command = \trim($this->command);
33+
34+
// If the command already starts with 'git', remove it
35+
if (\str_starts_with($command, 'git ')) {
36+
$command = \substr($command, 4);
37+
}
38+
39+
return \array_filter(\explode(' ', $command));
40+
}
41+
42+
public function __toString(): string
43+
{
44+
if (\is_string($this->command)) {
45+
$command = \trim($this->command);
46+
47+
// If the command already starts with 'git', use it as is
48+
if (\str_starts_with($command, 'git ')) {
49+
$command = \substr($command, 4);
50+
}
51+
52+
return $command;
53+
}
54+
55+
return \implode(' ', $this->command);
56+
}
57+
}

src/Lib/Git/CommandsExecutor.php

+206
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Butschster\ContextGenerator\Lib\Git;
6+
7+
use Butschster\ContextGenerator\Application\Logger\LoggerPrefix;
8+
use Butschster\ContextGenerator\DirectoriesInterface;
9+
use Butschster\ContextGenerator\Lib\Git\Exception\GitCommandException;
10+
use Psr\Log\LoggerInterface;
11+
use Spiral\Files\Exception\FilesException;
12+
use Spiral\Files\FilesInterface;
13+
use Symfony\Component\Process\Exception\ProcessFailedException;
14+
use Symfony\Component\Process\Process;
15+
16+
final class CommandsExecutor implements CommandsExecutorInterface
17+
{
18+
/**
19+
* Static cache of validated repositories
20+
* @var array<string, bool>
21+
*/
22+
private static array $validatedRepositories = [];
23+
24+
public function __construct(
25+
private readonly FilesInterface $files,
26+
private readonly DirectoriesInterface $dirs,
27+
#[LoggerPrefix(prefix: 'git-commands-executor')]
28+
private readonly ?LoggerInterface $logger = null,
29+
) {}
30+
31+
public function executeString(Command $command): string
32+
{
33+
$repository = $command->repository;
34+
$repositoryPath = $this->resolvePath($repository);
35+
36+
if (!$this->isValidRepository($repositoryPath)) {
37+
$this->logger?->error('Not a valid Git repository', [
38+
'repository' => $repositoryPath,
39+
]);
40+
41+
throw new \InvalidArgumentException(\sprintf('"%s" is not a valid Git repository', $repositoryPath));
42+
}
43+
44+
$commandParts = ['git', ...$command->getCommandParts()];
45+
46+
$this->logger?->debug('Executing Git command', [
47+
'command' => \implode(' ', $commandParts),
48+
'repository' => $repositoryPath,
49+
]);
50+
51+
try {
52+
$process = new Process($commandParts, $repositoryPath);
53+
$process->run();
54+
55+
if (!$process->isSuccessful()) {
56+
$this->logger?->error('Git command failed', [
57+
'command' => \implode(' ', $commandParts),
58+
'exitCode' => $process->getExitCode(),
59+
'errorOutput' => $process->getErrorOutput(),
60+
]);
61+
62+
throw new GitCommandException(
63+
\sprintf(
64+
'Git command "%s" failed with exit code %d: %s',
65+
\implode(' ', $commandParts),
66+
$process->getExitCode(),
67+
$process->getErrorOutput(),
68+
),
69+
$process->getExitCode(),
70+
);
71+
}
72+
73+
$this->logger?->debug('Git command executed successfully', [
74+
'command' => \implode(' ', $commandParts),
75+
'outputLength' => \strlen($process->getOutput()),
76+
]);
77+
78+
return $process->getOutput();
79+
} catch (ProcessFailedException $e) {
80+
$this->logger?->error('Git command process failed', [
81+
'command' => \implode(' ', $commandParts),
82+
'error' => $e->getMessage(),
83+
]);
84+
85+
throw new GitCommandException(
86+
\sprintf('Git command process failed: %s', $e->getMessage()),
87+
$e->getCode(),
88+
$e,
89+
);
90+
}
91+
}
92+
93+
public function isValidRepository(string $repository): bool
94+
{
95+
// Return cached result if available
96+
if (isset(self::$validatedRepositories[$repository])) {
97+
$this->logger?->debug('Using cached repository validation result', [
98+
'repository' => $repository,
99+
'isValid' => self::$validatedRepositories[$repository],
100+
]);
101+
return self::$validatedRepositories[$repository];
102+
}
103+
104+
$repositoryPath = $this->resolvePath($repository);
105+
106+
if (!\is_dir($repositoryPath)) {
107+
$this->logger?->debug('Repository directory does not exist', [
108+
'repository' => $repository,
109+
]);
110+
self::$validatedRepositories[$repository] = false;
111+
return false;
112+
}
113+
114+
try {
115+
$process = new Process(
116+
['git', 'rev-parse', '--is-inside-work-tree'],
117+
$repositoryPath,
118+
);
119+
120+
$process->run();
121+
122+
$isValid = $process->isSuccessful() && \trim($process->getOutput()) === 'true';
123+
124+
$this->logger?->debug('Repository validation result', [
125+
'repository' => $repository,
126+
'isValid' => $isValid,
127+
]);
128+
129+
// Cache the result in static array
130+
self::$validatedRepositories[$repository] = $isValid;
131+
132+
return $isValid;
133+
} catch (\Exception $e) {
134+
$this->logger?->error('Error validating repository', [
135+
'repository' => $repository,
136+
'error' => $e->getMessage(),
137+
]);
138+
self::$validatedRepositories[$repository] = false;
139+
return false;
140+
}
141+
}
142+
143+
public function applyPatch(string $filePath, string $patchContent): string
144+
{
145+
$rootPath = $this->dirs->getRootPath();
146+
147+
if (!$this->isValidRepository((string) $rootPath)) {
148+
$this->logger?->error('Not a valid Git repository', [
149+
'repository' => (string) $rootPath,
150+
]);
151+
152+
throw new \InvalidArgumentException(\sprintf('"%s" is not a valid Git repository', $rootPath));
153+
}
154+
155+
$file = $rootPath->join($filePath);
156+
157+
// Ensure the file exists
158+
if (!$file->exists()) {
159+
throw new GitCommandException(\sprintf('File "%s" does not exist', $filePath));
160+
}
161+
162+
// Create a temporary file for the patch
163+
try {
164+
$patchFile = $this->files->tempFilename();
165+
} catch (FilesException $e) {
166+
$this->logger?->error('Failed to create temporary file for patch', [
167+
'error' => $e->getMessage(),
168+
]);
169+
170+
throw new GitCommandException('Failed to create temporary file for patch', 0, $e);
171+
}
172+
173+
try {
174+
// Write the patch content to a temporary file
175+
$this->files->write($patchFile, $patchContent, FilesInterface::READONLY);
176+
177+
// Apply the patch using git apply command
178+
$process = new Process(
179+
['git', 'apply', '--whitespace=nowarn', $patchFile],
180+
(string) $rootPath,
181+
);
182+
183+
$process->run();
184+
185+
// Check if the command was successful
186+
if (!$process->isSuccessful()) {
187+
throw new GitCommandException(
188+
\sprintf('Failed to apply patch: %s', $process->getErrorOutput()),
189+
$process->getExitCode(),
190+
);
191+
}
192+
193+
return \sprintf('Successfully applied patch to %s', $filePath);
194+
} finally {
195+
$this->files->delete($patchFile);
196+
}
197+
}
198+
199+
/**
200+
* Resolve repository path relative to the root path.
201+
*/
202+
private function resolvePath(string $repository): string
203+
{
204+
return (string) $this->dirs->getRootPath()->join($repository);
205+
}
206+
}

0 commit comments

Comments
 (0)