Repository: bmitch/churn-php (php)

Issue:
Add footer with misc data under table?
Could show:
* Files analyzed.
* Time it took to run.
* ???


Discussion:

Patch:
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -8,36 +8,45 @@ Helps discover good candidates for refactoring.
 * [Compatibility](#compatibility)
 * [How to Install?](#how-to-install)
 * [How to Use?](#how-to-use)
+* [How to Configure?](#how-to-configure)
 * [Similar Packages](#similar-packages)
 * [Contributing](#contributing)
 * [License](#license)
 
 ## What is it? ##
-`churn-php` is a package that helps you identify files in your project that could be good candidates for refactoring. It examines each PHP file in the path it is provided and:
+`churn-php` is a package that helps you identify php files in your project that could be good candidates for refactoring. It examines each PHP file in the path it is provided and:
 * Checks how many commits it has.
 * Calculates the cyclomatic complexity.
 * Creates a score based on these two values.
 
 The results are displayed in a table:
 ```
+    ___  _   _  __  __  ____  _  _     ____  _   _  ____
+   / __)( )_( )(  )(  )(  _ \( \( )___(  _ \( )_( )(  _ \
+  ( (__  ) _ (  )(__)(  )   / )  ((___))___/ ) _ (  )___/
+   \___)(_) (_)(______)(_)\_)(_)\_)   (__)  (_) (_)(__)      https://github.com/bmitch/churn-php
+
 +---------------------------------------------------------------------+---------------+------------+-------+
 | File                                                                | Times Changed | Complexity | Score |
 +---------------------------------------------------------------------+---------------+------------+-------+
+| src/Managers/FileManager.php                                        | 5             | 4          | 9     |
 | src/Assessors/CyclomaticComplexity/CyclomaticComplexityAssessor.php | 4             | 4          | 8     |
-| src/Assessors/GitCommitCount/GitCommitCountAssessor.php             | 3             | 4          | 7     |
-| src/Commands/ChurnCommand.php                                       | 3             | 2          | 5     |
-| src/Managers/FileManager.php                                        | 2             | 3          | 5     |
-| src/Results/ResultsGenerator.php                                    | 2             | 2          | 4     |
+| src/Results/ResultsParser.php                                       | 3             | 3          | 6     |
 | src/Results/Result.php                                              | 2             | 1          | 3     |
-| src/Services/CommandService.php                                     | 2             | 1          | 3     |
+| src/Factories/ProcessFactory.php                                    | 2             | 1          | 3     |
 | src/Results/ResultCollection.php                                    | 1             | 1          | 2     |
+| src/Values/File.php                                                 | 1             | 1          | 2     |
+| src/Collections/FileCollection.php                                  | 1             | 1          | 2     |
+| src/Values/Config.php                                               | 1             | 1          | 2     |
+| src/Processes/ChurnProcess.php                                      | 1             | 1          | 2     |
 +---------------------------------------------------------------------+---------------+------------+-------+
+  10 files analysed in 0.24276995658875 seconds using 15 parallel jobs.
 ```
 
 
-A file that changes a lot and has a high complexity might be a higher candidate for refactoring than a file that doesn't change a lot and has a low complexity.
+A file that changes a lot and has a high complexity might be a better candidate for refactoring than a file that doesn't change a lot and has a low complexity.
 
-`churn-php` only intends to assist the developer identifying files for refactoring. It's best to use the results in addition to your own judgement to decide which files you may want to refactor.
+`churn-php` only assists the developer to identify files for refactoring. It's best to use the results in addition to your own judgment to decide which files you may want to refactor.
 
 ## Compatibility ##
 * PHP 7+
@@ -53,6 +62,32 @@ composer require bmitch/churn-php --dev
 vendor/bin/churn run <path to source code>
 ```
 
+## How to Configure?
+You may add an optional `churn.yml` file to the root of your project which can be used to configure churn-php. A sample `churm.yml` file looks like:
+
+```yml
+# The maximum number of files to display in the results table.
+# Default: 10
+filesToShow: 10
+
+# The number of parallel jobs to use when processing files.
+# Default 10:
+parallelJobs: 10
+
+# How far back in the git history to count the number of commits to a file
+# Can be a human readable date like 'One week ago' or a date like '2017-07-12'
+# Default '10 Years ago'
+commitsSince: One year ago
+
+# Files to ignore when processing. The full path to the file relative to the root of your project is required
+# Default: All PHP files in the path provided to churn-php are processed.
+filesToIgnore:
+ - src/Commands/ChurnCommand.php
+ - src/Results/ResultsParser.php
+ ```
+
+If a `churm.yml` file is omitted or an individual setting is omitted the default values above will be used.
+
 ## Similar Packages
 * https://github.com/danmayer/churn (Ruby)
 
diff --git a/composer.json b/composer.json
--- a/composer.json
+++ b/composer.json
@@ -21,7 +21,8 @@
         "php": ">=7.0.0",
         "symfony/console": "~3.2",
         "tightenco/collect": "^5.4",
-        "symfony/process": "^3.3"
+        "symfony/process": "^3.3",
+        "symfony/yaml": "^3.3"
     },
     "require-dev": {
         "phpunit/phpunit": "^5.7",
diff --git a/src/Commands/ChurnCommand.php b/src/Commands/ChurnCommand.php
--- a/src/Commands/ChurnCommand.php
+++ b/src/Commands/ChurnCommand.php
@@ -3,6 +3,7 @@
 namespace Churn\Commands;
 
 use Churn\Factories\ProcessFactory;
+use Churn\Values\Config;
 use Symfony\Component\Console\Command\Command;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Output\OutputInterface;
@@ -12,6 +13,7 @@
 use Churn\Results\ResultCollection;
 use Illuminate\Support\Collection;
 use Churn\Results\ResultsParser;
+use Symfony\Component\Yaml\Yaml;
 
 class ChurnCommand extends Command
 {
@@ -51,9 +53,10 @@ class ChurnCommand extends Command
     public function __construct()
     {
         parent::__construct();
-        $this->fileManager = new FileManager;
+        $this->config = new Config(Yaml::parse(@file_get_contents(getcwd() . '/churn.yml')) ?? []);
+        $this->fileManager = new FileManager($this->config);
+        $this->processFactory = new ProcessFactory($this->config);
         $this->resultsParser = new ResultsParser;
-        $this->processFactory = new ProcessFactory;
     }
 
     /**
@@ -97,7 +100,7 @@ protected function execute(InputInterface $input, OutputInterface $output)
      */
     private function getProcessResults()
     {
-        for ($index = $this->runningProcesses->count(); $this->filesCollection->hasFiles() > 0 && $index < 10; $index++) {
+        for ($index = $this->runningProcesses->count(); $this->filesCollection->hasFiles() > 0 && $index < $this->config->getParallelJobs(); $index++) {
             $file = $this->filesCollection->getNextFile();
 
             $process = $this->processFactory->createGitCommitProcess($file);
@@ -135,11 +138,10 @@ protected function displayResults(OutputInterface $output, ResultCollection $res
 
         $table = new Table($output);
         $table->setHeaders(['File', 'Times Changed', 'Complexity', 'Score']);
-
-        foreach ($results->orderByScoreDesc()->take(10) as $result) {
+        foreach ($results->orderByScoreDesc()->take($this->config->getFilesToShow()) as $result) {
             $table->addRow($result->toArray());
         }
         $table->render();
-        echo "  " . $this->filesCount . " files analysed in {$totalTime} seconds.\n\n";
+        echo "  " . $this->filesCount . " files analysed in {$totalTime} seconds using " . $this->config->getParallelJobs() .  " parallel jobs.\n\n";
     }
 }
diff --git a/src/Factories/ProcessFactory.php b/src/Factories/ProcessFactory.php
--- a/src/Factories/ProcessFactory.php
+++ b/src/Factories/ProcessFactory.php
@@ -3,11 +3,21 @@
 namespace Churn\Factories;
 
 use Churn\Processes\ChurnProcess;
+use Churn\Values\Config;
 use Churn\Values\File;
 use Symfony\Component\Process\Process;
 
 class ProcessFactory
 {
+    /**
+     * ProcessFactory constructor.
+     * @param Config $config Configuration Settings.
+     */
+    public function __construct(Config $config)
+    {
+        $this->config = $config;
+    }
+
     /**
      * Creates a Git Commit Process that will run on $file.
      * @param File $file File that the process will execute on.
@@ -16,7 +26,7 @@ class ProcessFactory
     public function createGitCommitProcess(File $file): ChurnProcess
     {
         $process = new Process(
-            'git -C ' . getcwd() . " log --name-only --pretty=format: " . $file->getFullPath(). " | sort | uniq -c | sort -nr"
+            'git -C ' . getcwd() . " log --since=\"" . $this->config->getCommitsSince() . "\"  --name-only --pretty=format: " . $file->getFullPath(). " | sort | uniq -c | sort -nr"
         );
 
         return new ChurnProcess($file, $process, 'GitCommitProcess');
diff --git a/src/Managers/FileManager.php b/src/Managers/FileManager.php
--- a/src/Managers/FileManager.php
+++ b/src/Managers/FileManager.php
@@ -3,12 +3,22 @@
 namespace Churn\Managers;
 
 use Churn\Collections\FileCollection;
+use Churn\Values\Config;
 use RecursiveDirectoryIterator;
 use RecursiveIteratorIterator;
 use Churn\Values\File;
 
 class FileManager
 {
+    /**
+     * FileManager constructor.
+     * @param Config $config Configuration Settings.
+     */
+    public function __construct(Config $config)
+    {
+        $this->config = $config;
+    }
+
     /**
      * Recursively finds all files with the .php extension in the provided
      * $path and returns list as array.
@@ -24,6 +34,10 @@ public function getPhpFiles(string $path): FileCollection
                 continue;
             }
 
+            if (in_array($file->getPathname(), $this->config->getFilesToIgnore())) {
+                continue;
+            }
+
             $files->push(new File(['displayPath' => $file->getPathName(), 'fullPath' => $file->getRealPath()]));
         }
 
diff --git a/src/Values/Config.php b/src/Values/Config.php
new file mode 100644
--- /dev/null
+++ b/src/Values/Config.php
@@ -0,0 +1,79 @@
+<?php declare(strict_types = 1);
+
+
+namespace Churn\Values;
+
+class Config
+{
+    /**
+     * The number of files to display in the results table.
+     * @var integer
+     */
+    private $filesToShow;
+
+    /**
+     * The number of parallel jobs to use to process the files.
+     * @var integer
+     */
+    private $parallelJobs;
+
+    /**
+     * How far back in the git history to go to count commits.
+     * @var string
+     */
+    private $commitsSince;
+
+    /**
+     * The paths to files to ignore when processing.
+     * @var array
+     */
+    private $filesToIgnore;
+
+    /**
+     * Config constructor.
+     * @param array $rawData Raw config data.
+     */
+    public function __construct(array $rawData = [])
+    {
+        $this->filesToShow = $rawData['filesToShow'] ?? 10;
+        $this->parallelJobs = $rawData['parallelJobs'] ?? 10;
+        $this->commitsSince = $rawData['commitsSince'] ?? '10 years ago';
+        $this->filesToIgnore = $rawData['filesToIgnore'] ?? [];
+    }
+
+    /**
+     * Get the number of files to display in the results table.
+     * @return integer
+     */
+    public function getFilesToShow(): int
+    {
+        return $this->filesToShow;
+    }
+
+    /**
+     * Get the number of parallel jobs to use to process the files.
+     * @return integer
+     */
+    public function getParallelJobs(): int
+    {
+        return $this->parallelJobs;
+    }
+
+    /**
+     * Get how far back in the git history to go to count commits.
+     * @return string
+     */
+    public function getCommitsSince(): string
+    {
+        return $this->commitsSince;
+    }
+
+    /**
+     * Get the paths to files to ignore when processing.
+     * @return array
+     */
+    public function getFilesToIgnore(): array
+    {
+        return $this->filesToIgnore;
+    }
+}


Test patch:
diff --git a/tests/Integration/Managers/FileManagerTest.php b/tests/Integration/Managers/FileManagerTest.php
--- a/tests/Integration/Managers/FileManagerTest.php
+++ b/tests/Integration/Managers/FileManagerTest.php
@@ -4,6 +4,7 @@
 
 use Churn\Managers\FileManager;
 use Churn\Tests\BaseTestCase;
+use Churn\Values\Config;
 use Churn\Values\File;
 use Illuminate\Support\Collection;
 
@@ -35,6 +36,6 @@ public function setUp()
     {
         parent::setup();
 
-        $this->fileManager = new FileManager;
+        $this->fileManager = new FileManager(new Config);
     }
 }
\ No newline at end of file
diff --git a/tests/Unit/Factories/ProcessFactoryTest.php b/tests/Unit/Factories/ProcessFactoryTest.php
--- a/tests/Unit/Factories/ProcessFactoryTest.php
+++ b/tests/Unit/Factories/ProcessFactoryTest.php
@@ -6,6 +6,7 @@
 use Churn\Tests\BaseTestCase;
 use Churn\Factories\ProcessFactory;
 use Churn\Values\File;
+use Churn\Values\Config;
 
 class ProcessFactoryTest extends BaseTestCase
 {
@@ -38,6 +39,6 @@ public function it_can_create_a_cyclomatic_complexity_process()
 
     public function setup()
     {
-        $this->processFactory = new ProcessFactory;
+        $this->processFactory = new ProcessFactory(new Config);
     }
 }
\ No newline at end of file
diff --git a/tests/Unit/Managers/FileManagerTest.php b/tests/Unit/Managers/FileManagerTest.php
--- a/tests/Unit/Managers/FileManagerTest.php
+++ b/tests/Unit/Managers/FileManagerTest.php
@@ -4,6 +4,7 @@
 
 use Churn\Tests\BaseTestCase;
 use Churn\Managers\FileManager;
+use Churn\Values\Config;
 
 class FileManagerTest extends BaseTestCase
 {
@@ -27,6 +28,6 @@ public function it_can_get_the_php_files_in_a_filter()
 
     public function setup()
     {
-        $this->fileManager = new FileManager();
+        $this->fileManager = new FileManager(new Config);
     }
 }
\ No newline at end of file
diff --git a/tests/Unit/Values/ConfigTest.php b/tests/Unit/Values/ConfigTest.php
new file mode 100644
--- /dev/null
+++ b/tests/Unit/Values/ConfigTest.php
@@ -0,0 +1,49 @@
+<?php
+
+
+namespace Churn\Tests\Unit\Values;
+
+
+use Churn\Tests\BaseTestCase;
+use Churn\Values\Config;
+
+class ConfigTest extends BaseTestCase
+{
+    /** @test **/
+    public function it_can_be_instantiated()
+    {
+        $this->assertInstanceOf(Config::class, new Config([]));
+    }
+
+    /** @test **/
+    public function it_can_be_instantiated_without_any_parameters()
+    {
+        $this->assertInstanceOf(Config::class, new Config);
+    }
+
+    /** @test **/
+    public function it_can_return_its_default_values_when_instantiated_without_any_parameters()
+    {
+        $config = new Config;
+        $this->assertSame(10, $config->getFilesToShow());
+        $this->assertSame(10, $config->getParallelJobs());
+        $this->assertSame('10 years ago', $config->getCommitsSince());
+        $this->assertSame([], $config->getFilesToIgnore());
+    }
+
+    /** @test **/
+    public function it_can_return_its_values_when_instantiated_parameters()
+    {
+        $config = new Config([
+            'filesToShow' => 13,
+            'parallelJobs' => 7,
+            'commitsSince' => '4 years ago',
+            'filesToIgnore' => ['foo.php', 'bar.php', 'baz.php']
+        ]);
+        $this->assertSame(13, $config->getFilesToShow());
+        $this->assertSame(7, $config->getParallelJobs());
+        $this->assertSame('4 years ago', $config->getCommitsSince());
+        $this->assertSame(['foo.php', 'bar.php', 'baz.php'], $config->getFilesToIgnore());
+    }
+
+}
\ No newline at end of file