GitRule::getBranchLastCommitInfo()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 9
rs 9.6666
c 0
b 0
f 0
cc 1
eloc 6
nc 1
nop 1
1
<?php
2
/*
3
 * This file is part of project-quality-inspector.
4
 *
5
 * (c) Alexandre GESLIN <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
namespace ProjectQualityInspector\Rule;
12
13
use ProjectQualityInspector\Application\ProcessHelper;
14
use ProjectQualityInspector\Exception\ExpectationFailedException;
15
16
/**
17
 * Class GitRule
18
 *
19
 * @package ProjectQualityInspector\Rule
20
 */
21
class GitRule extends AbstractRule
22
{
23
    private $commitFormat = '%H|%ci|%cr|%an';
24
    private $commitFormatKeys = ['commitHash', 'committerDate', 'relativeCommitterDate', 'authorName', 'branchName'];
25
26
    public function __construct(array $config, $baseDir)
27
    {
28
        parent::__construct($config, $baseDir);
29
    }
30
31
    /**
32
     * @inheritdoc
33
     */
34
    public function evaluate()
35
    {
36
        $expectationsFailedExceptions = [];
37
        $stableBranches = $this->getStableBranches($this->config['remote-branches']);
38
39
        try {
40
            $this->expectsNoMergedBranches($stableBranches, $this->config['threshold-too-many-merged-branches'], $this->config['remote-branches']);
0 ignored issues
show
Bug introduced by
It seems like $stableBranches defined by $this->getStableBranches...fig['remote-branches']) on line 37 can also be of type null; however, ProjectQualityInspector\...pectsNoMergedBranches() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
41
            $this->addAssertion('expectsNoMergedBranches');
42
        } catch (ExpectationFailedException $e) {
43
            $expectationsFailedExceptions[] = $e;
44
            $this->addAssertion('expectsNoMergedBranches', [['message' => $e->getMessage() . $e->getReason(), 'type' => 'expectsNoMergedBranches']]);
45
        }
46
47
        $notMergedBranchesInfo = $this->listMergedOrNotMergedBranches($stableBranches, false, $this->config['remote-branches']);
0 ignored issues
show
Bug introduced by
It seems like $stableBranches defined by $this->getStableBranches...fig['remote-branches']) on line 37 can also be of type null; however, ProjectQualityInspector\...edOrNotMergedBranches() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
48
49
        foreach ($notMergedBranchesInfo as $notMergedBranchInfo) {
50
            try {
51
                $this->expectsBranchNotTooBehind($notMergedBranchInfo, $stableBranches);
0 ignored issues
show
Bug introduced by
It seems like $stableBranches defined by $this->getStableBranches...fig['remote-branches']) on line 37 can also be of type null; however, ProjectQualityInspector\...ctsBranchNotTooBehind() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
52
                $this->addAssertion($notMergedBranchInfo['branchName']);
53
            } catch (ExpectationFailedException $e) {
54
                $expectationsFailedExceptions[] = $e;
55
                $this->addAssertion($notMergedBranchInfo['branchName'], [['message' => $e->getMessage() . $e->getReason(), 'type' => 'expectsBranchNotTooBehind']]);
56
            }
57
        }
58
59
        if (count($expectationsFailedExceptions)) {
60
            $this->throwRuleViolationException($expectationsFailedExceptions);
61
        }
62
    }
63
64
    /**
65
     * @param array $stableBranches
66
     * @param int $threshold
67
     * @param bool $remoteBranches
68
     *
69
     * @throws ExpectationFailedException
70
     */
71
    private function expectsNoMergedBranches(array $stableBranches, $threshold, $remoteBranches = true)
72
    {
73
        if ($mergedBranches = $this->listMergedOrNotMergedBranches($stableBranches, true, $remoteBranches)) {
74
            if (count($mergedBranches) >= $threshold) {
75
                $message = sprintf('there is too much remaining merged branches (%s) : %s', count($mergedBranches), $this->stringifyMergedBranches($mergedBranches));
76
                throw new ExpectationFailedException($mergedBranches, $message);
77
            }
78
        }
79
    }
80
81
    /**
82
     * Compare stable branch first commit after common ancestor of not merged branch and stable branch, and expects days delay do not override limit
83
     *
84
     * @param array $notMergedBranchInfo
85
     * @param array $stableBranches
86
     *
87
     * @throws ExpectationFailedException
88
     */
89
    private function expectsBranchNotTooBehind(array $notMergedBranchInfo, array $stableBranches)
90
    {
91
        foreach ($stableBranches as $stableBranch) {
92
            $failed = false;
93
            $lrAheadCommitsCount = $this->getLeftRightAheadCommitsCountAfterMergeBase($stableBranch, $notMergedBranchInfo['branchName']);
94
95
            if ($lrAheadCommitsCount[$stableBranch] > 0) {
96
                $commonAncestorCommitInfo = $this->getMergeBaseCommit($notMergedBranchInfo['branchName'], $stableBranch);
97
                $stableBranchLastCommitInfo = $this->getBranchLastCommitInfo($stableBranch);
98
99
                if ($lrAheadCommitsCount[$stableBranch] >= (int)$this->config['threshold-commits-behind']) {
100
                    $failed = true;
101
                }
102
103
                $interval = $this->compareCommitInfosDatesDiff($commonAncestorCommitInfo, $stableBranchLastCommitInfo);
104
                if ((int)$interval->format('%r%a') >= (int)$this->config['threshold-days-behind']) {
105
                    $failed = true;
106
                }
107
108
                if ($failed) {
109
                    $message = sprintf('The branch <fg=green>%s</> is behind <fg=green>%s</> by %s commits spread through %s days.', $notMergedBranchInfo['branchName'], $stableBranch, $lrAheadCommitsCount[$stableBranch], (int)$interval->format('%r%a'));
110
                    $message .= sprintf(' <fg=green>%s</> should update the branch %s', $notMergedBranchInfo['authorName'], $notMergedBranchInfo['branchName']);
111
                    throw new ExpectationFailedException($notMergedBranchInfo, $message);
112
                }
113
            }
114
        }
115
    }
116
117
    /**
118
     * Get merged/not merged branches compared to $stablesBranches.
119
     * If there is multiple stable branches, and $merged = true : list branches that are merged in at least one of the stable branches
120
     * If there is multiple stable branches, and $merged = false : list branches that are not merged for any of the stable branches
121
     *
122
     * @param array $stableBranches
123
     * @param bool $merged
124
     * @param bool $remoteBranches
125
     * @return array
126
     */
127
    private function listMergedOrNotMergedBranches(array $stableBranches, $merged = true, $remoteBranches = true)
128
    {
129
        $branches = [];
130
        $mergedOption = ($merged) ? '--merged' : '--no-merged';
131
        $refsBase = ($remoteBranches) ? 'refs/remotes' : 'refs/heads';
132
133
        foreach ($stableBranches as $stableBranch) {
134
            $result = ProcessHelper::execute(sprintf('for branch in `git for-each-ref %s %s --shell --format=\'%%(refname)\' %s | tr -d \\\' | grep -ve "/HEAD" | grep -ve "%s" | grep -ve "%s"`; do echo `git show --format="%s" $branch | head -n 1`\|$branch; done | sort -r', $mergedOption, $stableBranch, $refsBase, $this->getBranchesRegex('stable-branches-regex'), $this->getBranchesRegex('ignored-branches-regex'), $this->commitFormat), $this->baseDir);
135
136
            $branches[$stableBranch] = $this->explodeCommitsArrays($result);
0 ignored issues
show
Bug introduced by
It seems like $result defined by \ProjectQualityInspector...ormat), $this->baseDir) on line 134 can also be of type null; however, ProjectQualityInspector\...:explodeCommitsArrays() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
137
            if ($merged) {
138
                foreach ($branches[$stableBranch] as $branchHash => $mergedBranchCommit) {
139
                    $branches[$stableBranch][$branchHash]['mergeCommit'] = $this->getMergeCommitInfo($stableBranch, $mergedBranchCommit['branchName']);
140
                }
141
            }
142
        }
143
144
        if (count($branches) > 1) {
145
            $branches = ($merged) ? call_user_func_array('array_merge', $branches) : call_user_func_array('array_intersect_key', $branches);
146
        } else {
147
            $branches = $branches[$stableBranches[0]];
148
        }
149
150
        return $branches;
151
    }
152
153
    /**
154
     * Get numbers of commits after common ancestor for $branchLeft and $branchRight (ex: $branchLeft => 2,  $branchRight => 5)
155
     *
156
     * @param $branchLeft
157
     * @param $branchRight
158
     * @return array
159
     */
160
    private function getLeftRightAheadCommitsCountAfterMergeBase($branchLeft, $branchRight)
161
    {
162
        $result = ProcessHelper::execute(sprintf('git rev-list --left-right --count %s...%s', $branchLeft, $branchRight), $this->baseDir);
163
        $result = explode("\t", $result[0]);
164
165
        $result = [
166
            $branchLeft => $result[0],
167
            $branchRight => $result[1]
168
        ];
169
170
        return $result;
171
    }
172
173
    /**
174
     * Get common ancestor commit
175
     *
176
     * @param string $branchLeft
177
     * @param string $branchRight
178
     * @return array
179
     */
180
    private function getMergeBaseCommit($branchLeft, $branchRight)
181
    {
182
        $commitInfo = null;
183
        $result = ProcessHelper::execute(sprintf('git merge-base %s %s', $branchLeft, $branchRight), $this->baseDir);
184
185
        if (count($result)) {
186
            $commitInfo = $this->getBranchLastCommitInfo($result[0]);
187
        }
188
189
        return $commitInfo;
190
    }
191
192
    /**
193
     * Get first commit of $branch after common ancestor commit of $baseBranch and $branch
194
     *
195
     * @param string $baseBranch
196
     * @param string $branch
197
     * @return array
198
     */
199
    private function getBranchFirstCommitInfo($baseBranch, $branch)
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
200
    {
201
        $branchInfo = null;
202
        $result = ProcessHelper::execute(sprintf('git log --format="%s" %s..%s | tail -1', $this->commitFormat, $baseBranch, $branch), $this->baseDir);
203
        if (count($result)) {
204
            $explodedCommit = explode('|', $result[0]);
205
            $branchInfo = array_combine(array_slice($this->commitFormatKeys, 0, count($explodedCommit)), $explodedCommit);
206
            $branchInfo['branchName'] = $branch;
207
        }
208
209
        return $branchInfo;
210
    }
211
212
    /**
213
     * @param $branchInfoLeft
214
     * @param $branchInfoRight
215
     * @return bool|\DateInterval
216
     */
217
    private function compareCommitInfosDatesDiff($branchInfoLeft, $branchInfoRight)
218
    {
219
        $format = 'Y-m-d H:i:s O';
220
        $dateLeft = \DateTime::createFromFormat($format, $branchInfoLeft['committerDate']);
221
        $dateRight = \DateTime::createFromFormat($format, $branchInfoRight['committerDate']);
222
223
        return $dateLeft->diff($dateRight);
224
    }
225
226
    /**
227
     * @param $branch
228
     * @return array
229
     */
230
    private function getBranchLastCommitInfo($branch)
231
    {
232
        $result = ProcessHelper::execute(sprintf('git show --format="%s" %s | head -n 1', $this->commitFormat, $branch), $this->baseDir);
233
        $explodedCommit = explode('|', $result[0]);
234
        $branchInfo = array_combine(array_slice($this->commitFormatKeys, 0, count($explodedCommit)), $explodedCommit);
235
        $branchInfo['branchName'] = $branch;
236
237
        return $branchInfo;
238
    }
239
240
    /**
241
     * @param $baseBranch
242
     * @param $mergedBranch
243
     * @return array
244
     */
245
    private function getMergeCommitInfo($baseBranch, $mergedBranch)
246
    {
247
        $branchInfo = [
248
            'authorName' => 'fast-forward'
249
        ];
250
251
        if ($result = ProcessHelper::execute(sprintf('git show --format="%s" %s ^%s --ancestry-path | head -n 1', $this->commitFormat, $baseBranch, $mergedBranch), $this->baseDir)) {
252
            $explodedCommit = explode('|', $result[0]);
253
            $branchInfo = array_combine(array_slice($this->commitFormatKeys, 0, count($explodedCommit)), $explodedCommit);
254
        }
255
        $branchInfo['branchName'] = $baseBranch;
256
257
        return $branchInfo;
258
    }
259
260
    /**
261
     * @param bool $remoteBranches
262
     * @return array
263
     */
264
    private function getStableBranches($remoteBranches = true)
265
    {
266
        $refsBase = ($remoteBranches) ? 'refs/remotes' : 'refs/heads';
267
268
        $result = ProcessHelper::execute(sprintf('git for-each-ref --shell --format=\'%%(refname)\' %s | tr -d \\\' | grep -e "%s"', $refsBase, $this->getBranchesRegex('stable-branches-regex')), $this->baseDir);
269
270
        return $result;
271
    }
272
273
    /**
274
     * @param string $configKey
275
     * @return string
276
     */
277
    private function getBranchesRegex($configKey)
278
    {
279
        $branchesRegex = ['^$'];
280
281
        if (is_array($this->config[$configKey]) && count($this->config[$configKey])) {
282
            $branchesRegex = array_map(function ($element) {
283
                return '\(^[ ]*'.$element.'$\)';
284
            }, $this->config[$configKey]);
285
        }
286
287
        return implode('\|', $branchesRegex);
288
    }
289
290
    /**
291
     * @param array $mergedBranches
292
     * @return string
293
     */
294
    private function stringifyMergedBranches(array $mergedBranches)
295
    {
296
        $mergedBranches = array_map(function($mergedBranch) {
297
            return ($mergedBranch['mergeCommit']['authorName'] == 'fast-forward')
298
                ? sprintf('<fg=green>%s</> - %s by %s, merged in <fg=green>%s</> by %s', $mergedBranch['branchName'], $mergedBranch['relativeCommitterDate'], $mergedBranch['authorName'], $mergedBranch['mergeCommit']['branchName'], $mergedBranch['mergeCommit']['authorName'])
299
                : sprintf('<fg=green>%s</> - %s by %s, merged %s in <fg=green>%s</> by %s', $mergedBranch['branchName'], $mergedBranch['relativeCommitterDate'], $mergedBranch['authorName'], $mergedBranch['mergeCommit']['relativeCommitterDate'], $mergedBranch['mergeCommit']['branchName'], $mergedBranch['mergeCommit']['authorName']);
300
        }, $mergedBranches);
301
        return "\n\t" . implode("\n\t", $mergedBranches);
302
    }
303
304
    /**
305
     * @param  array  $commits
306
     * @return array
307
     */
308
    private function explodeCommitsArrays(array $commits)
309
    {
310
          $explodedCommits = [];
311
312
          foreach ($commits as $commit) {
313
              $explodedCommit = explode('|', $commit);
314
              $explodedCommit = array_combine(array_slice($this->commitFormatKeys, 0, count($explodedCommit)), $explodedCommit);
315
              $explodedCommits[$explodedCommit['commitHash']] = $explodedCommit;
316
          }
317
318
          return $explodedCommits;
319
    }
320
}