Completed
Push — master ( f60249...3a275e )
by Marcel
02:27
created

VcsVersionInfo::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 9
rs 9.6666
cc 2
eloc 6
nc 2
nop 4
1
<?php
2
namespace nochso\Omni;
3
4
/**
5
 * VcsVersionInfo wraps and enriches VersionInfo with the latest tag and repository state.
6
 *
7
 * If the working directory is clean and at an exact tag, only the tag is returned:
8
 *
9
 *     1.0.0
10
 *
11
 * If dirty and at an exact tag, `-dirty` is appended:
12
 *
13
 *     1.0.0-dirty
14
 *
15
 * If there are no tags present, the revision id is returned:
16
 *
17
 *     4319e00
18
 *
19
 * If there have been commits since a tag:
20
 *
21
 *     0.3.1-14-gf602496-dirty
22
 */
23
final class VcsVersionInfo
24
{
25
    /**
26
     * @var \nochso\Omni\VersionInfo
27
     */
28
    private $version;
29
    /**
30
     * @var string
31
     */
32
    private $repositoryRoot;
33
34
    /**
35
     * @param string $name            Package or application name.
36
     * @param string $fallBackVersion Version to fall back to if no repository info was found.
37
     * @param string $repositoryRoot  Path the VCS repository root (e.g. folder that contains ".git", ".hg", etc.)
38
     * @param string $infoFormat      Optional format to use for `getInfo`. Defaults to `self::INFO_FORMAT_DEFAULT`
39
     */
40
    public function __construct($name, $fallBackVersion = null, $repositoryRoot = '."', $infoFormat = VersionInfo::INFO_FORMAT_DEFAULT)
41
    {
42
        $this->repositoryRoot = $repositoryRoot;
43
        $tag = $this->extractTag();
44
        if ($tag === null) {
45
            $tag = $fallBackVersion;
46
        }
47
        $this->version = new VersionInfo($name, $tag, $infoFormat);
48
    }
49
50
    /**
51
     * @return string
52
     */
53
    public function getInfo()
54
    {
55
        return $this->version->getInfo();
56
    }
57
58
    /**
59
     * @return string
60
     */
61
    public function getVersion()
62
    {
63
        return $this->version->getVersion();
64
    }
65
66
    /**
67
     * @return string
68
     */
69
    public function getName()
70
    {
71
        return $this->version->getName();
72
    }
73
74
    private function extractTag()
75
    {
76
        $tag = $this->readGit();
77
        if ($tag === null) {
78
            $tag = $this->readMercurial();
79
        }
80
        return $tag;
81
    }
82
83
    /**
84
     * @return null|string
85
     */
86
    private function readGit()
87
    {
88
        $gitDir = Path::combine($this->repositoryRoot, '.git');
89
        if (!is_dir($gitDir)) {
90
            return null;
91
        }
92
        $describe = $this->execEscaped(
93
            'git --git-dir=%s --work-tree=%s describe --tags --always --dirty',
94
            $gitDir,
95
            $this->repositoryRoot
96
        );
97
        return Dot::get($describe, 0);
98
    }
99
100
    /**
101
     * @return null|string
102
     */
103
    private function readMercurial()
104
    {
105
        $hgDir = Path::combine($this->repositoryRoot, '.hg');
106
        if (!is_dir($hgDir)) {
107
            return null;
108
        }
109
110
        // Removes everything but the tag if distance is zero.
111
        $log = $this->execEscaped(
112
            'hg --repository %s log -r . -T "{latesttag}{sub(\'^-0-m.*\', \'\', \'-{latesttagdistance}-m{node|short}\')}"',
113
            $this->repositoryRoot
114
        );
115
116
        $tag = Dot::get($log, 0);
117
        // Actual null if no lines were returned or `hg log` returned actual "null".
118
        // Either way, need to fall back to the revision id.
119
        if ($tag === null || $tag === 'null') {
120
            $id = $this->execEscaped('hg --repository %s id -i', $this->repositoryRoot);
121
            $tag = Dot::get($id, 0);
122
        }
123
124
        // Check if working directory is dirty
125
        $summary = $this->execEscaped(
126
            'hg --repository %s summary',
127
            $this->repositoryRoot
128
        );
129
        $isDirty = 0 === count(array_filter($summary, function ($line) {
130
                return preg_match('/^commit: .*\(clean\)$/', $line) === 1;
131
            }));
132
        if ($isDirty) {
133
            $tag .= '-dirty';
134
        }
135
        return $tag;
136
    }
137
138
    /**
139
     * execEscaped executes a command with automatically escaped parameters.
140
     *
141
     * @param string $cmdFormat
142
     * @param array ...$params
143
     *
144
     * @see sprintf()
145
     *
146
     * @return string[] Only lines with content after trimming are returned.
147
     */
148
    private function execEscaped($cmdFormat, ...$params)
149
    {
150
        $quotedParams = array_filter($params, 'escapeshellarg');
151
        $cmd = sprintf($cmdFormat, ...$quotedParams);
152
        exec($cmd, $out, $ret);
153
        return array_filter($out, 'strlen');
154
    }
155
}
156