VersionGuesser.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. <?php
  2. /*
  3. * This file is part of Composer.
  4. *
  5. * (c) Nils Adermann <naderman@naderman.de>
  6. * Jordi Boggiano <j.boggiano@seld.be>
  7. *
  8. * For the full copyright and license information, please view the LICENSE
  9. * file that was distributed with this source code.
  10. */
  11. namespace Composer\Package\Version;
  12. use Composer\Config;
  13. use Composer\Repository\Vcs\HgDriver;
  14. use Composer\IO\NullIO;
  15. use Composer\Semver\VersionParser as SemverVersionParser;
  16. use Composer\Util\Git as GitUtil;
  17. use Composer\Util\HttpDownloader;
  18. use Composer\Util\ProcessExecutor;
  19. use Composer\Util\Svn as SvnUtil;
  20. /**
  21. * Try to guess the current version number based on different VCS configuration.
  22. *
  23. * @author Jordi Boggiano <j.boggiano@seld.be>
  24. * @author Samuel Roze <samuel.roze@gmail.com>
  25. */
  26. class VersionGuesser
  27. {
  28. /**
  29. * @var Config
  30. */
  31. private $config;
  32. /**
  33. * @var ProcessExecutor
  34. */
  35. private $process;
  36. /**
  37. * @var SemverVersionParser
  38. */
  39. private $versionParser;
  40. /**
  41. * @param Config $config
  42. * @param ProcessExecutor $process
  43. * @param SemverVersionParser $versionParser
  44. */
  45. public function __construct(Config $config, ProcessExecutor $process, SemverVersionParser $versionParser)
  46. {
  47. $this->config = $config;
  48. $this->process = $process;
  49. $this->versionParser = $versionParser;
  50. }
  51. /**
  52. * @param array $packageConfig
  53. * @param string $path Path to guess into
  54. *
  55. * @return null|array versionData, 'version', 'pretty_version' and 'commit' keys
  56. */
  57. public function guessVersion(array $packageConfig, $path)
  58. {
  59. if (function_exists('proc_open')) {
  60. $versionData = $this->guessGitVersion($packageConfig, $path);
  61. if (null !== $versionData && null !== $versionData['version']) {
  62. return $this->postprocess($versionData);
  63. }
  64. $versionData = $this->guessHgVersion($packageConfig, $path);
  65. if (null !== $versionData && null !== $versionData['version']) {
  66. return $this->postprocess($versionData);
  67. }
  68. $versionData = $this->guessFossilVersion($packageConfig, $path);
  69. if (null !== $versionData && null !== $versionData['version']) {
  70. return $this->postprocess($versionData);
  71. }
  72. $versionData = $this->guessSvnVersion($packageConfig, $path);
  73. if (null !== $versionData && null !== $versionData['version']) {
  74. return $this->postprocess($versionData);
  75. }
  76. }
  77. }
  78. private function postprocess(array $versionData)
  79. {
  80. if ('-dev' === substr($versionData['version'], -4) && preg_match('{\.9{7}}', $versionData['version'])) {
  81. $versionData['pretty_version'] = preg_replace('{(\.9{7})+}', '.x', $versionData['version']);
  82. }
  83. return $versionData;
  84. }
  85. private function guessGitVersion(array $packageConfig, $path)
  86. {
  87. GitUtil::cleanEnv();
  88. $commit = null;
  89. $version = null;
  90. $prettyVersion = null;
  91. $isDetached = false;
  92. // try to fetch current version from git branch
  93. if (0 === $this->process->execute('git branch --no-color --no-abbrev -v', $output, $path)) {
  94. $branches = array();
  95. $isFeatureBranch = false;
  96. // find current branch and collect all branch names
  97. foreach ($this->process->splitLines($output) as $branch) {
  98. if ($branch && preg_match('{^(?:\* ) *(\(no branch\)|\(detached from \S+\)|\(HEAD detached at \S+\)|\S+) *([a-f0-9]+) .*$}', $branch, $match)) {
  99. if ($match[1] === '(no branch)' || substr($match[1], 0, 10) === '(detached ' || substr($match[1], 0, 17) === '(HEAD detached at') {
  100. $version = 'dev-' . $match[2];
  101. $prettyVersion = $version;
  102. $isFeatureBranch = true;
  103. $isDetached = true;
  104. } else {
  105. $version = $this->versionParser->normalizeBranch($match[1]);
  106. $prettyVersion = 'dev-' . $match[1];
  107. $isFeatureBranch = 0 === strpos($version, 'dev-');
  108. }
  109. if ($match[2]) {
  110. $commit = $match[2];
  111. }
  112. }
  113. if ($branch && !preg_match('{^ *[^/]+/HEAD }', $branch)) {
  114. if (preg_match('{^(?:\* )? *(\S+) *([a-f0-9]+) .*$}', $branch, $match)) {
  115. $branches[] = $match[1];
  116. }
  117. }
  118. }
  119. if ($isFeatureBranch) {
  120. // try to find the best (nearest) version branch to assume this feature's version
  121. $result = $this->guessFeatureVersion($packageConfig, $version, $branches, 'git rev-list %candidate%..%branch%', $path);
  122. $version = $result['version'];
  123. $prettyVersion = $result['pretty_version'];
  124. }
  125. }
  126. if (!$version || $isDetached) {
  127. $result = $this->versionFromGitTags($path);
  128. if ($result) {
  129. $version = $result['version'];
  130. $prettyVersion = $result['pretty_version'];
  131. }
  132. }
  133. if (!$commit) {
  134. $command = 'git log --pretty="%H" -n1 HEAD';
  135. if (0 === $this->process->execute($command, $output, $path)) {
  136. $commit = trim($output) ?: null;
  137. }
  138. }
  139. return array('version' => $version, 'commit' => $commit, 'pretty_version' => $prettyVersion);
  140. }
  141. private function versionFromGitTags($path)
  142. {
  143. // try to fetch current version from git tags
  144. if (0 === $this->process->execute('git describe --exact-match --tags', $output, $path)) {
  145. try {
  146. $version = $this->versionParser->normalize(trim($output));
  147. return array('version' => $version, 'pretty_version' => trim($output));
  148. } catch (\Exception $e) {
  149. }
  150. }
  151. return null;
  152. }
  153. private function guessHgVersion(array $packageConfig, $path)
  154. {
  155. // try to fetch current version from hg branch
  156. if (0 === $this->process->execute('hg branch', $output, $path)) {
  157. $branch = trim($output);
  158. $version = $this->versionParser->normalizeBranch($branch);
  159. $isFeatureBranch = 0 === strpos($version, 'dev-');
  160. if ('9999999-dev' === $version) {
  161. return array('version' => $version, 'commit' => null, 'pretty_version' => 'dev-'.$branch);
  162. }
  163. if (!$isFeatureBranch) {
  164. return array('version' => $version, 'commit' => null, 'pretty_version' => $version);
  165. }
  166. // re-use the HgDriver to fetch branches (this properly includes bookmarks)
  167. $io = new NullIO();
  168. $driver = new HgDriver(array('url' => $path), $io, $this->config, new HttpDownloader($io, $this->config), $this->process);
  169. $branches = array_keys($driver->getBranches());
  170. // try to find the best (nearest) version branch to assume this feature's version
  171. $result = $this->guessFeatureVersion($packageConfig, $version, $branches, 'hg log -r "not ancestors(\'%candidate%\') and ancestors(\'%branch%\')" --template "{node}\\n"', $path);
  172. $result['commit'] = '';
  173. return $result;
  174. }
  175. }
  176. private function guessFeatureVersion(array $packageConfig, $version, array $branches, $scmCmdline, $path)
  177. {
  178. $prettyVersion = $version;
  179. // ignore feature branches if they have no branch-alias or self.version is used
  180. // and find the branch they came from to use as a version instead
  181. if ((isset($packageConfig['extra']['branch-alias']) && !isset($packageConfig['extra']['branch-alias'][$version]))
  182. || strpos(json_encode($packageConfig), '"self.version"')
  183. ) {
  184. $branch = preg_replace('{^dev-}', '', $version);
  185. $length = PHP_INT_MAX;
  186. $nonFeatureBranches = '';
  187. if (!empty($packageConfig['non-feature-branches'])) {
  188. $nonFeatureBranches = implode('|', $packageConfig['non-feature-branches']);
  189. }
  190. foreach ($branches as $candidate) {
  191. // return directly, if branch is configured to be non-feature branch
  192. if ($candidate === $branch && preg_match('{^(' . $nonFeatureBranches . ')$}', $candidate)) {
  193. break;
  194. }
  195. // do not compare against itself or other feature branches
  196. if ($candidate === $branch || !preg_match('{^(' . $nonFeatureBranches . '|master|trunk|default|develop|\d+\..+)$}', $candidate, $match)) {
  197. continue;
  198. }
  199. $cmdLine = str_replace(array('%candidate%', '%branch%'), array($candidate, $branch), $scmCmdline);
  200. if (0 !== $this->process->execute($cmdLine, $output, $path)) {
  201. continue;
  202. }
  203. if (strlen($output) < $length) {
  204. $length = strlen($output);
  205. $version = $this->versionParser->normalizeBranch($candidate);
  206. $prettyVersion = 'dev-' . $match[1];
  207. }
  208. }
  209. }
  210. return array('version' => $version, 'pretty_version' => $prettyVersion);
  211. }
  212. private function guessFossilVersion(array $packageConfig, $path)
  213. {
  214. $version = null;
  215. $prettyVersion = null;
  216. // try to fetch current version from fossil
  217. if (0 === $this->process->execute('fossil branch list', $output, $path)) {
  218. $branch = trim($output);
  219. $version = $this->versionParser->normalizeBranch($branch);
  220. $prettyVersion = 'dev-' . $branch;
  221. }
  222. // try to fetch current version from fossil tags
  223. if (0 === $this->process->execute('fossil tag list', $output, $path)) {
  224. try {
  225. $version = $this->versionParser->normalize(trim($output));
  226. $prettyVersion = trim($output);
  227. } catch (\Exception $e) {
  228. }
  229. }
  230. return array('version' => $version, 'commit' => '', 'pretty_version' => $prettyVersion);
  231. }
  232. private function guessSvnVersion(array $packageConfig, $path)
  233. {
  234. SvnUtil::cleanEnv();
  235. // try to fetch current version from svn
  236. if (0 === $this->process->execute('svn info --xml', $output, $path)) {
  237. $trunkPath = isset($packageConfig['trunk-path']) ? preg_quote($packageConfig['trunk-path'], '#') : 'trunk';
  238. $branchesPath = isset($packageConfig['branches-path']) ? preg_quote($packageConfig['branches-path'], '#') : 'branches';
  239. $tagsPath = isset($packageConfig['tags-path']) ? preg_quote($packageConfig['tags-path'], '#') : 'tags';
  240. $urlPattern = '#<url>.*/(' . $trunkPath . '|(' . $branchesPath . '|' . $tagsPath . ')/(.*))</url>#';
  241. if (preg_match($urlPattern, $output, $matches)) {
  242. if (isset($matches[2]) && ($branchesPath === $matches[2] || $tagsPath === $matches[2])) {
  243. // we are in a branches path
  244. $version = $this->versionParser->normalizeBranch($matches[3]);
  245. $prettyVersion = 'dev-' . $matches[3];
  246. return array('version' => $version, 'commit' => '', 'pretty_version' => $prettyVersion);
  247. }
  248. $prettyVersion = trim($matches[1]);
  249. $version = $this->versionParser->normalize($prettyVersion);
  250. return array('version' => $version, 'commit' => '', 'pretty_version' => $prettyVersion);
  251. }
  252. }
  253. }
  254. }