diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..032d429
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,34 @@
+name: CI
+
+on:
+ pull_request:
+ push:
+ branches:
+ - main
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v3
+
+ - name: Set up PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: '8.2'
+ extensions: mbstring, dom
+ coverage: none
+
+ - name: Install dependencies
+ run: composer install --prefer-dist --no-progress --no-suggest --no-interaction
+
+ - name: Check code style
+ run: composer check-cs
+
+ - name: Analyze code with PHPStan
+ run: composer analyse
+
+ - name: Run PHPUnit tests
+ run: composer test
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..9928d5e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,33 @@
+# Composer dependencies
+/vendor/
+composer.lock
+
+# PHPStorm / JetBrains IDEs
+/.idea/
+/*.iml
+
+# VSCode
+.vscode/
+
+# Coverage reports
+/coverage/
+/.clover
+.php_cs.cache
+/.php_cs.cache
+/.php_cs_fixer.cache
+
+# Log files
+*.log
+
+# Cache and temp files
+*.cache
+*.tmp
+
+# OS-specific files
+.DS_Store
+Thumbs.db
+
+# Environment variables
+.env
+.env.local
+.env.*.local
\ No newline at end of file
diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php
new file mode 100644
index 0000000..1fdb7f0
--- /dev/null
+++ b/.php-cs-fixer.php
@@ -0,0 +1,25 @@
+in(__DIR__ . '/') // Scans the src directory for PHP files
+ ->name('*.php')
+ ->exclude('vendor'); // Excludes vendor files
+
+return (new Config())
+ ->setRules([
+ '@PSR12' => true, // Enforces PSR12 standards
+ 'array_syntax' => ['syntax' => 'short'], // Enforces short array syntax
+ 'binary_operator_spaces' => [
+ 'default' => 'single_space'
+ ],
+ 'blank_line_after_namespace' => true, // Adds blank line after the namespace declaration
+ 'blank_line_after_opening_tag' => true,
+ 'no_unused_imports' => true, // Removes unused imports
+ 'ordered_imports' => ['sort_algorithm' => 'alpha'], // Orders imports alphabetically
+ 'phpdoc_align' => ['align' => 'vertical'], // Aligns PHPDoc tags vertically
+ 'single_trait_insert_per_statement' => true // Enforces one trait per statement
+ ])
+ ->setFinder($finder);
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..3d34d1d
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,9 @@
+The MIT License (MIT)
+
+Copyright (c) Yigit Cukuren code@yigit.dev
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..db93045
--- /dev/null
+++ b/README.md
@@ -0,0 +1,155 @@
+# TimeLock
+
+**TimeLock** is a command-line tool designed to help developers identify files in a Git repository that have remained unchanged since a specific date. It can be configured to exclude specific authors, paths, and file types using regex patterns.
+
+## Table of Contents
+
+- [Features](#features)
+- [Installation](#installation)
+- [Usage](#usage)
+- [Configuration](#configuration)
+- [Examples](#examples)
+ - [Example Output](#example-output)
+- [Running Tests](#running-tests)
+- [Contributing](#contributing)
+- [License](#license)
+
+## Features
+
+- **File Detection**: Detects files in a Git repository that have not changed since a specified date.
+- **Author Exclusion**: Exclude files from specific authors.
+- **Path and Regex Exclusions**: Exclude files based on paths or regex patterns.
+- **Output Formats**: Supports both table and JSON output formats.
+- **Customizable**: Easily extendable to support other version control systems.
+
+## Installation
+
+To install **TimeLock** via Composer, run the following command:
+
+```bash
+composer require timelock/timelock
+```
+
+After installation, the `timelock` binary will be available in the `vendor/bin` directory.
+
+## Usage
+
+The `check` command is the main CLI tool provided by TimeLock. Below is an example of how to use it:
+
+```bash
+vendor/bin/timelock check --config=path/to/timelock.yml
+```
+
+### Command-Line Options
+
+- `path` (optional): The directory path to check. Defaults to the current directory.
+- `--config` (optional): The path to the configuration file. Defaults to `timelock.yml` in the current directory.
+- `--output-format` (optional): The output format (`table` or `json`). Defaults to `table`.
+
+## Configuration
+
+TimeLock is configured using a YAML file (`timelock.yml`). Below is an example configuration file:
+
+```yaml
+since: '5 years ago' # Files unchanged since this date will be flagged
+excludeAuthors: # Authors to exclude from the check
+ - 'John Doe'
+ - 'Jane Smith'
+exclude: # Paths to exclude from the check
+ - 'vendor/'
+ - 'tests/'
+excludeRegex: # Regex patterns to exclude from the check
+ - '/.*Controller\.php$/'
+vcs: 'git' # Version control system to use (default is 'git')
+```
+
+### Configuration Options
+
+- `since`: A date string or timestamp to check files against.
+- `excludeAuthors`: A list of author names to exclude.
+- `exclude`: A list of paths to exclude.
+- `excludeRegex`: A list of regex patterns to exclude specific files.
+- `vcs`: The version control system to use. Currently supports `git`.
+
+## Examples
+
+### Basic Usage
+
+Check for files unchanged in the current directory:
+
+```bash
+vendor/bin/timelock check
+```
+
+### Custom Configuration
+
+Use a specific configuration file:
+
+```bash
+vendor/bin/timelock check --config=/path/to/your-config.yml
+```
+
+### JSON Output
+
+Get the output in JSON format:
+
+```bash
+vendor/bin/timelock check --output-format=json
+```
+
+### Example Output
+
+Here’s an example of what the output might look like when using the `table` format:
+
+```bash
+vendor/bin/timelock check --config=path/to/timelock.yml
+```
+
+Output:
+
+```
++------------+-----------+---------------------+---------+
+| File | Author | Last Modified | Changes |
++------------+-----------+---------------------+---------+
+| file1.txt | John Doe | 2017-06-01 12:00:00 | 1 |
++------------+-----------+---------------------+---------+
+| file2.txt | Jane Doe | 2019-03-15 15:30:00 | 3 |
++------------+-----------+---------------------+---------+
+
+Check completed.
+Execution time: 0.42 seconds
+```
+
+In this example:
+
+- `File`: The name of the file that has been unchanged since the specified date.
+- `Author`: The author of the last commit to that file.
+- `Last Modified`: The date and time when the file was last modified.
+- `Changes`: The number of changes made to the file.
+
+## Running Tests
+
+To run the test suite, use PHPUnit. If you haven’t installed PHPUnit globally, you can use the local installation:
+
+```bash
+composer test
+```
+
+The tests are located in the `tests` directory and cover the core functionality of the TimeLock tool, including Git integration and configuration handling.
+
+## Contributing
+
+We welcome contributions! Here’s how you can get involved:
+
+1. Fork the repository.
+2. Create a new branch (`git checkout -b feature/your-feature`).
+3. Make your changes.
+4. Commit your changes (`git commit -m 'Add some feature'`).
+5. Push to the branch (`git push origin feature/your-feature`).
+6. Open a pull request.
+
+Please make sure to write tests for your changes and ensure all existing tests pass.
+
+## License
+
+This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
\ No newline at end of file
diff --git a/bin/timelock b/bin/timelock
new file mode 100755
index 0000000..aa41b52
--- /dev/null
+++ b/bin/timelock
@@ -0,0 +1,20 @@
+#!/usr/bin/env php
+add(new CheckCommand(new VCSFactory));
+
+// Run the application
+$application->run();
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..063c82d
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,38 @@
+{
+ "name": "timelock/timelock",
+ "description": "A PHP package to find files unchanged since a specified date.",
+ "type": "library",
+ "version": "0.1.0",
+ "license": "MIT",
+ "autoload": {
+ "psr-4": {
+ "TimeLock\\": "src/"
+ }
+ },
+ "authors": [
+ {
+ "name": "Yigit Cukuren",
+ "email": "code@yigit.dev"
+ }
+ ],
+ "require": {
+ "php": ">=8.2",
+ "symfony/yaml": "^7.1",
+ "symfony/console": "^7.1",
+ "symfony/process": "^7.1"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^1.11",
+ "phpunit/phpunit": "^11.3",
+ "friendsofphp/php-cs-fixer": "^3.62"
+ },
+ "scripts": {
+ "check-cs": "php-cs-fixer fix --dry-run --diff",
+ "fix-cs": "php-cs-fixer fix",
+ "analyse": "phpstan analyse",
+ "test": "phpunit"
+ },
+ "bin": [
+ "bin/timelock"
+ ]
+}
diff --git a/phpstan.neon b/phpstan.neon
new file mode 100644
index 0000000..8886f7f
--- /dev/null
+++ b/phpstan.neon
@@ -0,0 +1,4 @@
+parameters:
+ level: 5
+ paths:
+ - src
\ No newline at end of file
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100644
index 0000000..de73dc0
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,13 @@
+
+
+
+
+ tests
+
+
+
+
diff --git a/src/Command/CheckCommand.php b/src/Command/CheckCommand.php
new file mode 100644
index 0000000..5b80354
--- /dev/null
+++ b/src/Command/CheckCommand.php
@@ -0,0 +1,207 @@
+vcsFactory = $vcsFactory;
+ parent::__construct();
+ }
+
+ /**
+ * Configures the command options and arguments.
+ */
+ protected function configure(): void
+ {
+ $this
+ ->setName('check')
+ ->setDescription('Checks for files unchanged since a specified date.')
+ ->setHelp('This command allows you to check for files that haven\'t been changed since a specific date...')
+ ->addArgument(
+ 'path',
+ InputArgument::OPTIONAL,
+ 'The directory path to check'
+ )
+ ->addOption(
+ 'config',
+ 'c',
+ InputOption::VALUE_OPTIONAL,
+ 'Path to the configuration file'
+ )
+ ->addOption(
+ 'output-format',
+ 'o',
+ InputOption::VALUE_OPTIONAL,
+ 'Output format (table or json)',
+ 'table'
+ );
+ }
+
+ /**
+ * Executes the command logic.
+ *
+ * @param InputInterface $input The input interface
+ * @param OutputInterface $output The output interface
+ *
+ * @return int Command exit status
+ */
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $startTime = microtime(true);
+
+ $directory = realpath($input->getArgument('path'));
+ $configFile = $input->getOption('config');
+
+ if (empty($configFile) || !file_exists($configFile)) {
+ $output->writeln('Configuration file \'timelock.yml\' not found.');
+ return Command::FAILURE;
+ }
+
+ $config = Yaml::parseFile($configFile);
+ $vcsType = $config['vcs'] ?? 'git';
+ $outputFormat = $input->getOption('output-format');
+ $excludeRegex = $config['excludeRegex'] ?? [];
+
+ try {
+ /** @var VCSInterface $vcs */
+ $vcs = $this->vcsFactory->create($vcsType, $directory);
+ } catch (\InvalidArgumentException $e) {
+ $output->writeln("{$e->getMessage()}");
+ return Command::FAILURE;
+ }
+
+ try {
+ $files = $vcs->getFiles();
+ $fileInfo = $vcs->getFileInfo();
+ } catch (\RuntimeException $e) {
+ $output->writeln("{$e->getMessage()}");
+ return Command::FAILURE;
+ }
+
+ $since = strtotime($config['since'] ?? '5 years ago');
+ $excludeAuthors = $config['excludeAuthors'] ?? [];
+ $excludePaths = $config['exclude'] ?? [];
+
+ $unchangedFiles = $this->processFiles($files, $fileInfo, $since, $excludeAuthors, $excludePaths, $excludeRegex);
+
+ if (empty($unchangedFiles)) {
+ $output->writeln('No unchanged files found.');
+ } else {
+ if ($outputFormat === 'json') {
+ $output->writeln(json_encode($unchangedFiles, JSON_PRETTY_PRINT));
+ } else {
+ $table = new Table($output);
+ $table->setHeaders(['File', 'Author', 'Last Modified', 'Changes']);
+
+ foreach ($unchangedFiles as $fileData) {
+ $table->addRow([
+ $fileData['file'],
+ $fileData['author'],
+ $fileData['last_modified'],
+ $fileData['changes']
+ ]);
+ }
+
+ $table->render();
+ }
+ }
+
+ $endTime = microtime(true);
+ $executionTime = $endTime - $startTime;
+
+ if ($outputFormat !== 'json') {
+ $output->writeln(sprintf('Execution time: %.2f seconds', $executionTime));
+ }
+
+ return Command::SUCCESS;
+ }
+
+ /**
+ * Processes files to find those that have remained unchanged since the specified date.
+ *
+ * @param array $files List of files to process
+ * @param array $fileInfo Information about each file
+ * @param int $since Timestamp to compare last modified dates against
+ * @param array $excludeAuthors Authors to exclude from the check
+ * @param array $excludePaths Paths to exclude from the check
+ * @param array $excludeRegex Regex patterns to exclude from the check
+ *
+ * @return array List of unchanged files with details
+ */
+ private function processFiles(
+ array $files,
+ array $fileInfo,
+ int $since,
+ array $excludeAuthors,
+ array $excludePaths,
+ array $excludeRegex
+ ): array {
+ $unchangedFiles = [];
+
+ foreach ($files as $file) {
+ if (!isset($fileInfo[$file])) {
+ continue;
+ }
+
+ $author = $fileInfo[$file]['author'] ?? null;
+ $lastModified = $fileInfo[$file]['timestamp'] ?? null;
+ $changes = $fileInfo[$file]['changes'] ?? 0;
+
+ if ($lastModified > $since) {
+ continue;
+ }
+
+ if (in_array($author, $excludeAuthors)) {
+ continue;
+ }
+
+ foreach ($excludePaths as $path) {
+ if (strpos($file, $path) !== false) {
+ continue 2; // skip to next file
+ }
+ }
+
+ foreach ($excludeRegex as $pattern) {
+ if (preg_match($pattern, $file)) {
+ continue 2; // skip to next file
+ }
+ }
+
+ $unchangedFiles[] = [
+ 'file' => $file,
+ 'author' => $author,
+ 'last_modified' => date('Y-m-d H:i:s', $lastModified),
+ 'changes' => $changes,
+ ];
+ }
+
+ return $unchangedFiles;
+ }
+}
diff --git a/src/GitVCS.php b/src/GitVCS.php
new file mode 100644
index 0000000..5e22477
--- /dev/null
+++ b/src/GitVCS.php
@@ -0,0 +1,150 @@
+directory = realpath($directory);
+ }
+
+ /**
+ * Checks if the directory is a valid Git repository.
+ *
+ * @return bool True if the directory is a Git repository, false otherwise.
+ */
+ public function isGitRepository(): bool
+ {
+ $process = $this->createProcess([
+ 'git',
+ '-C',
+ $this->directory,
+ 'rev-parse',
+ '--is-inside-work-tree'
+ ]);
+ $process->run();
+
+ return $process->isSuccessful();
+ }
+
+ /**
+ * Retrieves a list of files tracked by the Git repository.
+ *
+ * @return array The list of file paths.
+ *
+ * @throws \RuntimeException If the directory is not a Git repository.
+ * @throws ProcessFailedException If the Git process fails.
+ */
+ public function getFiles(): array
+ {
+ if (!$this->isGitRepository()) {
+ throw new \RuntimeException(
+ "Directory '{$this->directory}' is not a Git repository."
+ );
+ }
+
+ $process = $this->createProcess([
+ 'git',
+ '-C',
+ $this->directory,
+ 'ls-files'
+ ]);
+ $process->run();
+
+ if (!$process->isSuccessful()) {
+ throw new ProcessFailedException($process);
+ }
+
+ return explode("\n", trim($process->getOutput()));
+ }
+
+ /**
+ * Retrieves information about files in the Git repository,
+ * including author, last modification date, and number of changes.
+ *
+ * @return array An associative array of file information.
+ *
+ * @throws ProcessFailedException If the Git process fails.
+ */
+ public function getFileInfo(): array
+ {
+ $process = $this->createProcess([
+ 'git',
+ '-C',
+ $this->directory,
+ 'log',
+ '--pretty=format:%H,%an,%at',
+ '--name-only'
+ ]);
+ $process->run();
+
+ if (!$process->isSuccessful()) {
+ throw new ProcessFailedException($process);
+ }
+
+ $lines = explode("\n", trim($process->getOutput()));
+ $info = [];
+ $currentCommit = null;
+
+ foreach ($lines as $line) {
+ if (strpos($line, ',') !== false) {
+ $parts = explode(',', $line);
+ $commit = $this->sanitizeString($parts[0] ?? '');
+ $author = $this->sanitizeString($parts[1] ?? '');
+ $timestamp = $parts[2] ?? strtotime('50 years ago');
+ $currentCommit = compact('author', 'timestamp');
+ } elseif ($line) {
+ $sanitizedFile = $this->sanitizeString($line);
+ if (!isset($info[$sanitizedFile])) {
+ $info[$sanitizedFile] = $currentCommit + ['changes' => 0];
+ }
+ $info[$sanitizedFile]['changes']++;
+ }
+ }
+
+ return $info;
+ }
+
+ /**
+ * Creates a new process instance for a given command.
+ *
+ * @param array $command The command to be executed.
+ *
+ * @return Process The process instance.
+ */
+ protected function createProcess(array $command): Process
+ {
+ return new Process($command);
+ }
+
+ /**
+ * Sanitizes a string by removing control characters.
+ *
+ * @param string $string The string to sanitize.
+ *
+ * @return string The sanitized string.
+ */
+ private function sanitizeString(string $string): string
+ {
+ return preg_replace('/[\x00-\x1F\x7F]/u', '', $string);
+ }
+}
diff --git a/src/VCSFactory.php b/src/VCSFactory.php
new file mode 100644
index 0000000..328aec8
--- /dev/null
+++ b/src/VCSFactory.php
@@ -0,0 +1,25 @@
+application = new Application();
+
+ // Create a mock of VCSFactory
+ $mockFactory = $this->createMock(VCSFactory::class);
+
+ // Mock the create method to return a mocked GitVCS object
+ $mockVCS = $this->createMock(GitVCS::class);
+ $mockVCS->method('getFiles')->willReturn(['file1.txt', 'file2.txt']);
+ $mockVCS->method('getFileInfo')->willReturn([
+ 'file1.txt' => ['author' => 'John Doe', 'timestamp' => strtotime('-6 years'), 'changes' => 1],
+ 'file2.txt' => ['author' => 'Jane Doe', 'timestamp' => strtotime('-4 years'), 'changes' => 3],
+ ]);
+
+ $mockFactory->method('create')->willReturn($mockVCS);
+
+ // Initialize the CheckCommand with the mocked factory
+ $command = new CheckCommand($mockFactory);
+ $this->application->add($command);
+
+ // Prepare CommandTester for the CheckCommand
+ $command = $this->application->find('check');
+ $this->commandTester = new CommandTester($command);
+ }
+
+ /**
+ * Tests the CheckCommand with default settings.
+ */
+ public function testCheckCommandWithDefaultSettings(): void
+ {
+ $this->commandTester->execute([
+ 'path' => __DIR__,
+ '--config' => __DIR__ . '/../fixtures/timelock.yml',
+ ]);
+
+ $output = $this->commandTester->getDisplay();
+ $this->assertStringContainsString('file1.txt', $output);
+ $this->assertStringNotContainsString('file2.txt', $output);
+ $this->assertStringContainsString('Execution time:', $output);
+ }
+
+ /**
+ * Tests the CheckCommand with JSON output format.
+ */
+ public function testCheckCommandWithJsonOutput(): void
+ {
+ $this->commandTester->execute([
+ 'path' => __DIR__,
+ '--config' => __DIR__ . '/../fixtures/timelock.yml',
+ '--output-format' => 'json',
+ ]);
+
+ $output = $this->commandTester->getDisplay();
+ $this->assertJson($output);
+ $this->assertStringContainsString('file1.txt', $output);
+ $this->assertStringNotContainsString('file2.txt', $output);
+ }
+
+ /**
+ * Tests that the CheckCommand fails when no configuration file is provided.
+ */
+ public function testCheckCommandFailsWithoutConfig(): void
+ {
+ $this->commandTester->execute([
+ 'path' => __DIR__,
+ ]);
+
+ $output = $this->commandTester->getDisplay();
+ $this->assertStringContainsString('Configuration file \'timelock.yml\' not found.', $output);
+ $this->assertEquals(Command::FAILURE, $this->commandTester->getStatusCode());
+ }
+}
diff --git a/tests/GitVCSTest.php b/tests/GitVCSTest.php
new file mode 100644
index 0000000..665a53a
--- /dev/null
+++ b/tests/GitVCSTest.php
@@ -0,0 +1,109 @@
+gitVCS = $this->getMockBuilder(GitVCS::class)
+ ->setConstructorArgs([__DIR__])
+ ->onlyMethods(['createProcess'])
+ ->getMock();
+ }
+
+ /**
+ * Tests that the isGitRepository method correctly identifies a Git repository.
+ */
+ public function testIsGitRepository(): void
+ {
+ $process = $this->createMock(Process::class);
+ $process->method('run')->willReturn(0); // Simulate successful run
+ $process->method('isSuccessful')->willReturn(true);
+
+ $this->gitVCS->expects($this->once())
+ ->method('createProcess')
+ ->willReturn($process);
+
+ $this->assertTrue($this->gitVCS->isGitRepository());
+ }
+
+ /**
+ * Tests that the getFiles method throws a RuntimeException for a non-Git repository.
+ */
+ public function testGetFilesThrowsExceptionForNonGitRepo(): void
+ {
+ $process = $this->createMock(Process::class);
+ $process->method('run')->willReturn(1); // Simulate failed run
+ $process->method('isSuccessful')->willReturn(false);
+
+ $this->gitVCS->expects($this->once())
+ ->method('createProcess')
+ ->willReturn($process);
+
+ $this->expectException(\RuntimeException::class);
+ $this->gitVCS->getFiles();
+ }
+
+ /**
+ * Tests that the getFiles method correctly returns a list of files in the repository.
+ */
+ public function testGetFiles(): void
+ {
+ $process = $this->createMock(Process::class);
+ $process->method('run')->willReturn(0); // Simulate successful run
+ $process->method('isSuccessful')->willReturn(true);
+ $process->method('getOutput')->willReturn("file1.txt\nfile2.txt");
+
+ // Ensure that createProcess is called twice: once for isGitRepository and once for getFiles
+ $this->gitVCS->expects($this->exactly(2))
+ ->method('createProcess')
+ ->willReturn($process);
+
+ $files = $this->gitVCS->getFiles();
+
+ // Add assertions to check the returned files
+ $this->assertIsArray($files);
+ $this->assertCount(2, $files);
+ $this->assertEquals('file1.txt', $files[0]);
+ $this->assertEquals('file2.txt', $files[1]);
+ }
+
+ /**
+ * Tests that the getFileInfo method returns correct information about files in the repository.
+ */
+ public function testGetFileInfo(): void
+ {
+ $process = $this->createMock(Process::class);
+ $process->method('run')->willReturn(0); // Simulate successful run
+ $process->method('isSuccessful')->willReturn(true);
+ $process->method('getOutput')->willReturn("hash1,John Doe,1609459200\nfile1.txt\nfile2.txt");
+
+ $this->gitVCS->expects($this->once())
+ ->method('createProcess')
+ ->willReturn($process);
+
+ $fileInfo = $this->gitVCS->getFileInfo();
+ $this->assertIsArray($fileInfo);
+ $this->assertArrayHasKey('file1.txt', $fileInfo);
+ $this->assertArrayHasKey('file2.txt', $fileInfo);
+ $this->assertEquals('John Doe', $fileInfo['file1.txt']['author']);
+ }
+}
diff --git a/tests/VCSFactoryTest.php b/tests/VCSFactoryTest.php
new file mode 100644
index 0000000..82a8902
--- /dev/null
+++ b/tests/VCSFactoryTest.php
@@ -0,0 +1,36 @@
+create('git', __DIR__);
+ $this->assertInstanceOf(GitVCS::class, $vcs);
+ }
+
+ /**
+ * Tests that the factory throws an InvalidArgumentException for an unsupported VCS type.
+ */
+ public function testCreateThrowsExceptionForUnsupportedVCS(): void
+ {
+ $this->expectException(\InvalidArgumentException::class);
+
+ $factory = new VCSFactory();
+ $factory->create('unsupported_vcs', __DIR__);
+ }
+}
diff --git a/tests/fixtures/timelock.yml b/tests/fixtures/timelock.yml
new file mode 100644
index 0000000..4e62390
--- /dev/null
+++ b/tests/fixtures/timelock.yml
@@ -0,0 +1,8 @@
+since: 5 years ago
+excludeAuthors:
+ - Jane Doe
+exclude:
+ - vendor/
+excludeRegex:
+ - '/.*Test\.php$/'
+vcs: git
\ No newline at end of file
diff --git a/timelock.yml b/timelock.yml
new file mode 100644
index 0000000..ee7d0f3
--- /dev/null
+++ b/timelock.yml
@@ -0,0 +1,5 @@
+vcs: git
+since: "5 years ago"
+exclude:
+ - vendor/
+ - tests/
\ No newline at end of file