Completed
Push — master ( 7b4929...cefe8d )
by Andrii
01:50
created

History::hasLink()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 4
ccs 0
cts 4
cp 0
rs 10
cc 1
eloc 2
nc 1
nop 1
crap 2
1
<?php
2
/**
3
 * Changelog keeper
4
 *
5
 * @link      https://github.com/hiqdev/chkipper
6
 * @package   chkipper
7
 * @license   BSD-3-Clause
8
 * @copyright Copyright (c) 2016, HiQDev (http://hiqdev.com/)
9
 */
10
11
namespace hiqdev\chkipper\history;
12
13
/**
14
 * History class.
15
 *
16
 * @property array $headers: header => header
17
 * @property array $hashes:  hash => hash
18
 * @property array $links:   link => href
19
 * @property array $tags:    tag name => tag object
20
 *
21
 * @author Andrii Vasyliev <[email protected]>
22
 */
23
class History
24
{
25
    public $lastTag = 'Under development';
26
27
    public $initTag = 'Development started';
28
29
    protected $_config;
30
    protected $_project;
31
    protected $_headers = [];
32
    protected $_hashes  = [];
33
    protected $_links   = [];
34
    protected $_tags    = [];
35
36
    public function __construct(ConfigInterface $config)
37
    {
38
        $this->_config = $config;
39
    }
40
41
    public function isInitTag($tag)
42
    {
43
        return $tag === $this->initTag;
44
    }
45
46
    public function isLastTag($tag)
47
    {
48
        return $tag === $this->lastTag;
49
    }
50
51
    public function setProject($value)
52
    {
53
        $this->_project = $value;
54
    }
55
56
    public function getProject()
57
    {
58
        if ($this->_project === null) {
59
            $this->_project = $this->_config->getName() ?: $this->detectProject();
60
        }
61
62
        return $this->_project;
63
    }
64
65
    public function detectProject()
66
    {
67
        foreach ($this->getHeaders() as $line) {
68
            if (preg_match('/\b([a-z0-9._-]{2,}\/[a-z0-9._-]{2,})\b/i', $line, $m)) {
69
                return $m[1];
70
            }
71
        }
72
    }
73
74
    public function addHeader($str)
75
    {
76
        $this->_headers[$str] = $str;
77
    }
78
79
    public function addHeaders(array $headers)
80
    {
81
        foreach ($headers as $header) {
82
            $this->addHeader($header);
83
        }
84
    }
85
86
    public function setHeaders(array $headers)
87
    {
88
        $this->_headers = [];
89
        $this->addHeaders($headers);
90
    }
91
92
    public function getHeaders()
93
    {
94
        return $this->_headers;
95
    }
96
97
    public function hasLink($link)
98
    {
99
        return isset($this->_links[$link]);
100
    }
101
102
    public function removeLink($link)
103
    {
104
        unset($this->_links[$link]);
105
    }
106
107
    public function addLink($link, $href)
108
    {
109
        $this->_links[$link] = $href;
110
    }
111
112
    public function addLinks(array $links)
113
    {
114
        foreach ($links as $link => $href) {
115
            $this->addLink($link, $href);
116
        }
117
    }
118
119
    public function setLinks(array $links)
120
    {
121
        $this->_links = $links;
122
    }
123
124
    public function getLinks()
125
    {
126
        return $this->_links;
127
    }
128
129
    public function hasHash($hash)
130
    {
131
        return isset($this->_hashes[(string) $hash]);
132
    }
133
134
    public function addHash($hash)
135
    {
136
        $this->_hashes[(string) $hash] = $hash;
137
    }
138
139
    public function addHashes(array $hashes)
140
    {
141
        foreach ($hashes as $hash) {
142
            $this->addHash($hash);
143
        }
144
    }
145
146
    public function setHashes(array $hashes)
147
    {
148
        $this->_hashes = [];
149
        $this->addHashes($hashes);
150
    }
151
152
    public function getHashes()
153
    {
154
        return $this->_hashes;
155
    }
156
157
    public function getFirstTag()
158
    {
159
        return reset($this->_tags);
160
    }
161
162
    public function setFirstTag($name)
163
    {
164
        $this->getFirstTag()->setName($name);
165
    }
166
167
    public function countTags()
168
    {
169
        return count($this->_tags);
170
    }
171
172
    public function initTags()
173
    {
174
        if (!$this->countTags()) {
175
            $this->addTag(new Tag($this->lastTag));
176
        }
177
    }
178
179
    public function getTags()
180
    {
181
        return $this->_tags;
182
    }
183
184
    /**
185
     * Adds given tags to the history.
186
     * @param Tag[] $tags
187
     * @param boolean $prependNotes default is append
188
     */
189
    public function addTags(array $tags, $prependNotes = false)
190
    {
191
        foreach ($tags as $name => $tag) {
192
            $this->addTag($tag, $prependNotes);
193
        }
194
    }
195
196
    public function setTags(array $tags)
197
    {
198
        $this->_tags = [];
199
        $this->addTags($tags);
200
    }
201
202
    /**
203
     * Returns tag by name.
204
     * Creates if not exists.
205
     * Returns first tag when given empty name.
206
     * @param string|Tag $tag tag name or tag object
207
     * @return Tag
208
     */
209
    public function findTag($tag)
210
    {
211
        if (!$tag) {
212
            $tag = reset($this->_tags) ?: $this->lastTag;
213
        }
214
        $name = $tag instanceof Tag ? $tag->getName() : $tag;
215
        if (!$this->hasTag($name)) {
216
            $this->_tags[$name] = new Tag($name);
217
        }
218
219
        return $this->_tags[$name];
220
    }
221
222
    public function hasTag($tag)
223
    {
224
        return isset($this->_tags[$tag]);
225
    }
226
227
    public function removeTag($name)
228
    {
229
        foreach ($this->_tags as $k => $tag) {
230
            if ($tag->getName() === $name) {
231
                unset($this->_tags[$k]);
232
233
                return;
234
            }
235
        }
236
    }
237
238
    /**
239
     * Adds tag.
240
     * @param Tag $tag
241
     * @param boolean $prependNotes default is append
242
     * @return Tag the added tag
243
     */
244
    public function addTag(Tag $tag, $prependNotes = false)
245
    {
246
        return $this->findTag($tag->getName())->setDate($tag->getDate())->addNotes($tag->getNotes(), $prependNotes);
247
    }
248
249
    /**
250
     * Merges given history into the current.
251
     * @param History $history
252
     * @param boolean $prependNotes default is append
253
     */
254
    public function merge(History $history, $prependNotes = false)
255
    {
256
        $this->mergeTags($history->getTags(), $prependNotes);
257
        $this->addLinks($history->getLinks());
258
        $this->addHashes($history->getHashes());
259
    }
260
261
    /**
262
     * Merge given tags into the current history.
263
     * @param Tag[] $tags
264
     * @param boolean $prependNotes default is append
265
     */
266
    public function mergeTags(array $tags, $prependNotes = false)
267
    {
268
        foreach ($tags as $tag) {
269
            foreach ($tag->getNotes() as $note) {
270
                $note->removeCommits($this->getHashes());
271
            }
272
        }
273
        $this->addTags($tags, $prependNotes);
274
        //$olds = $this->getTags();
0 ignored issues
show
Unused Code Comprehensibility introduced by
60% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
275
        //$this->_tags = $tags;
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
276
        //$this->addTags($$olds);
0 ignored issues
show
Unused Code Comprehensibility introduced by
88% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
277
    }
278
279
    /**
280
     * Normalizes the history.
281
     */
282
    public function normalize($options = [])
283
    {
284
        static $defaults = [
285
            'removeEmptyFirstTag' => [],
286
            'addInitTag'          => [],
287
            'setTagDates'         => [],
288
            'addCommitLinks'      => [],
289
            'addTagLinks'         => [],
290
            'removeCommitLinks'   => [],
291
            'prettifyUserLinks'   => [],
292
        ];
293
        $options = array_merge($defaults, $options);
294
        foreach ($options as $func => $args) {
295
            if (is_array($args)) {
296
                call_user_func_array([$this, $func], $args);
297
            }
298
        }
299
    }
300
301
    /**
302
     * Removes first tag if it is empty: has no notes and no commits.
303
     */
304
    public function removeEmptyFirstTag()
305
    {
306
        $tag = $this->getFirstTag();
307
        $notes = $tag->getNotes();
308
        if (count($notes) > 1) {
309
            return;
310
        }
311
        if (count($notes) > 0) {
312
            $note = reset($notes);
313
            if ($note->getNote() || count($note->getCommits()) > 0) {
314
                return;
315
            }
316
        }
317
        $this->removeTag($tag->getName());
318
    }
319
320
    /**
321
     * Adds init tag with oldest commit date.
322
     */
323
    public function addInitTag()
324
    {
325
        if (!$this->hasTag($this->initTag)) {
326
            $min = '';
327
            foreach ($this->getTags() as $tag) {
328
                foreach ($tag->getNotes() as $note) {
329 View Code Duplication
                    foreach ($note->getCommits() as $commit) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
330
                        $date = $commit->getDate();
331
                        if (!$min || strcmp($date, $min) < 0) {
332
                            $min = $date;
333
                        }
334
                    }
335
                }
336
            }
337
            if ($min) {
338
                $this->addTag(new Tag($this->initTag, $min));
339
            }
340
        }
341
    }
342
343
    /**
344
     * Normalizes dates to all the tags.
345
     * Drops date for the last tag and sets for others.
346
     */
347
    public function setTagDates()
348
    {
349
        foreach ($this->getTags() as $tag) {
350
            if ($tag->getName() === $this->lastTag) {
351
                $tag->unsetDate();
352
            } elseif (!$tag->getDate()) {
353
                $tag->setDate($tag->findDate());
354
            }
355
        }
356
    }
357
358
    /**
359
     * Adds tag links.
360
     */
361
    public function addTagLinks()
362
    {
363
        $prev = null;
364
        foreach (array_keys($this->getTags()) as $tag) {
365
            if ($prev && ($this->isLastTag($prev) || !$this->hasLink($prev))) {
366
                $this->addLink($prev, $this->generateTagHref($prev, $tag));
367
            }
368
            $prev = $tag;
369
        }
370
    }
371
372
    public function generateTagHref($prev, $curr)
373
    {
374
        $project = $this->getProject();
375
        if ($this->isInitTag($curr)) {
376
            return "https://github.com/$project/releases/tag/$prev";
377
        }
378
        if ($this->isLastTag($prev)) {
379
            $prev = 'HEAD';
380
        }
381
382
        return "https://github.com/$project/compare/$curr...$prev";
383
    }
384
385
    /**
386
     * Adds links for commits not having ones.
387
     */
388
    public function addCommitLinks()
389
    {
390
        foreach ($this->getHashes() as $hash) {
391
            if (!$this->hasLink($hash)) {
392
                $this->addLink($hash, $this->generateHashHref($hash));
393
            }
394
        }
395
    }
396
397
    public function generateHashHref($hash)
398
    {
399
        $project = $this->getProject();
400
401
        return "https://github.com/$project/commit/$hash";
402
    }
403
404
    /**
405
     * Removes commit links that are not present in the history.
406
     */
407
    public function removeCommitLinks($all = false)
408
    {
409
        foreach ($this->getLinks() as $link => $href) {
410
            if (preg_match('/^[0-9a-f]{7}$/', $link)) {
411
                if ($all || !$this->hasHash($link)) {
412
                    $this->removeLink($link);
413
                }
414
            }
415
        }
416
    }
417
418
    /**
419
     * Converts user links to given links.
420
     * Usage: add 2 links to `history.md` like this:
421
     *
422
     * [@hiqsol]: https://github.com/hiqsol
423
     * [[email protected]]: https://github.com/hiqsol
424
     */
425
    public function prettifyUserLinks()
426
    {
427
        $users = [];
428
        $subs = [];
429
        foreach ($this->getLinks() as $link => $href) {
430
            if ($link[0] === '@') {
431
                $users[$href] = $link;
432
            } else if (isset($users[$href])) {
433
                $subs[$link] = $users[$href];
434
            }
435
        }
436
        if (!$subs) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $subs of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
437
            return;
438
        }
439
        foreach ($this->getTags() as $tag) {
440
            foreach ($tag->getNotes() as $note) {
441
                foreach ($note->getCommits() as $commit) {
442
                    $author = $commit->getAuthor();
443
                    if (isset($subs[$author])) {
444
                        $commit->setAuthor($subs[$author]);
445
                    }
446
                }
447
            }
448
        }
449
    }
450
}
451