Teknik is a suite of services with attractive and functional interfaces. https://www.teknik.io/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Repository.php 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. <?php
  2. namespace GitList\Git;
  3. use Gitter\Repository as BaseRepository;
  4. use Gitter\Model\Commit\Commit;
  5. use Gitter\Model\Commit\Diff;
  6. use Gitter\PrettyFormat;
  7. use Symfony\Component\Filesystem\Filesystem;
  8. class Repository extends BaseRepository
  9. {
  10. /**
  11. * Return true if the repo contains this commit.
  12. *
  13. * @param $commitHash Hash of commit whose existence we want to check
  14. * @return boolean Whether or not the commit exists in this repo
  15. */
  16. public function hasCommit($commitHash)
  17. {
  18. $logs = $this->getClient()->run($this, "show $commitHash");
  19. $logs = explode("\n", $logs);
  20. return strpos($logs[0], 'commit') === 0;
  21. }
  22. /**
  23. * Get the current branch, returning a default value when HEAD is detached.
  24. */
  25. public function getHead($default = null)
  26. {
  27. $client = $this->getClient();
  28. return parent::getHead($client->getDefaultBranch());
  29. }
  30. /**
  31. * Show the data from a specific commit
  32. *
  33. * @param string $commitHash Hash of the specific commit to read data
  34. * @return array Commit data
  35. */
  36. public function getCommit($commitHash)
  37. {
  38. $logs = $this->getClient()->run($this,
  39. "show --pretty=format:\"<item><hash>%H</hash>"
  40. . "<short_hash>%h</short_hash><tree>%T</tree><parents>%P</parents>"
  41. . "<author>%an</author><author_email>%ae</author_email>"
  42. . "<date>%at</date><commiter>%cn</commiter><commiter_email>%ce</commiter_email>"
  43. . "<commiter_date>%ct</commiter_date>"
  44. . "<message><![CDATA[%s]]></message>"
  45. . "<body><![CDATA[%b]]></body>"
  46. . "</item>\" $commitHash"
  47. );
  48. $xmlEnd = strpos($logs, '</item>') + 7;
  49. $commitInfo = substr($logs, 0, $xmlEnd);
  50. $commitData = substr($logs, $xmlEnd);
  51. $logs = explode("\n", $commitData);
  52. // Read commit metadata
  53. $format = new PrettyFormat;
  54. $data = $format->parse($commitInfo);
  55. $commit = new Commit;
  56. $commit->importData($data[0]);
  57. if ($commit->getParentsHash()) {
  58. $command = 'diff ' . $commitHash . '~1..' . $commitHash;
  59. $logs = explode("\n", $this->getClient()->run($this, $command));
  60. }
  61. $commit->setDiffs($this->readDiffLogs($logs));
  62. return $commit;
  63. }
  64. /**
  65. * Blames the provided file and parses the output
  66. *
  67. * @param string $file File that will be blamed
  68. * @return array Commits hashes containing the lines
  69. */
  70. public function getBlame($file)
  71. {
  72. $blame = array();
  73. $logs = $this->getClient()->run($this, "blame --root -sl $file");
  74. $logs = explode("\n", $logs);
  75. $i = 0;
  76. $previousCommit = '';
  77. foreach ($logs as $log) {
  78. if ($log == '') {
  79. continue;
  80. }
  81. preg_match_all("/([a-zA-Z0-9]{40})\s+.*?([0-9]+)\)(.+)/", $log, $match);
  82. $currentCommit = $match[1][0];
  83. if ($currentCommit != $previousCommit) {
  84. ++$i;
  85. $blame[$i] = array(
  86. 'line' => '',
  87. 'commit' => $currentCommit,
  88. 'commitShort' => substr($currentCommit, 0, 8)
  89. );
  90. }
  91. $blame[$i]['line'] .= PHP_EOL . $match[3][0];
  92. $previousCommit = $currentCommit;
  93. }
  94. return $blame;
  95. }
  96. /**
  97. * Read diff logs and generate a collection of diffs
  98. *
  99. * @param array $logs Array of log rows
  100. * @return array Array of diffs
  101. */
  102. public function readDiffLogs(array $logs)
  103. {
  104. $diffs = array();
  105. $lineNumOld = 0;
  106. $lineNumNew = 0;
  107. foreach ($logs as $log) {
  108. # Skip empty lines
  109. if ($log == "") {
  110. continue;
  111. }
  112. if ('diff' === substr($log, 0, 4)) {
  113. if (isset($diff)) {
  114. $diffs[] = $diff;
  115. }
  116. $diff = new Diff;
  117. if (preg_match('/^diff --[\S]+ a\/?(.+) b\/?/', $log, $name)) {
  118. $diff->setFile($name[1]);
  119. }
  120. continue;
  121. }
  122. if ('index' === substr($log, 0, 5)) {
  123. $diff->setIndex($log);
  124. continue;
  125. }
  126. if ('---' === substr($log, 0, 3)) {
  127. $diff->setOld($log);
  128. continue;
  129. }
  130. if ('+++' === substr($log, 0, 3)) {
  131. $diff->setNew($log);
  132. continue;
  133. }
  134. // Handle binary files properly.
  135. if ('Binary' === substr($log, 0, 6)) {
  136. $m = array();
  137. if (preg_match('/Binary files (.+) and (.+) differ/', $log, $m)) {
  138. $diff->setOld($m[1]);
  139. $diff->setNew(" {$m[2]}");
  140. }
  141. }
  142. if (!empty($log)) {
  143. switch ($log[0]) {
  144. case "@":
  145. // Set the line numbers
  146. preg_match('/@@ -([0-9]+)/', $log, $matches);
  147. $lineNumOld = $matches[1] - 1;
  148. $lineNumNew = $matches[1] - 1;
  149. break;
  150. case "-":
  151. $lineNumOld++;
  152. break;
  153. case "+":
  154. $lineNumNew++;
  155. break;
  156. default:
  157. $lineNumOld++;
  158. $lineNumNew++;
  159. }
  160. } else {
  161. $lineNumOld++;
  162. $lineNumNew++;
  163. }
  164. if (isset($diff)) {
  165. $diff->addLine($log, $lineNumOld, $lineNumNew);
  166. }
  167. }
  168. if (isset($diff)) {
  169. $diffs[] = $diff;
  170. }
  171. return $diffs;
  172. }
  173. /**
  174. * Show the repository commit log with pagination
  175. *
  176. * @access public
  177. * @return array Commit log
  178. */
  179. public function getPaginatedCommits($file = null, $page = 0)
  180. {
  181. $page = 15 * $page;
  182. $pager = "--skip=$page --max-count=15";
  183. $command =
  184. "log $pager --pretty=format:\"<item><hash>%H</hash>"
  185. . "<short_hash>%h</short_hash><tree>%T</tree><parents>%P</parents>"
  186. . "<author>%an</author><author_email>%ae</author_email>"
  187. . "<date>%at</date><commiter>%cn</commiter>"
  188. . "<commiter_email>%ce</commiter_email>"
  189. . "<commiter_date>%ct</commiter_date>"
  190. . "<message><![CDATA[%s]]></message></item>\"";
  191. if ($file) {
  192. $command .= " $file";
  193. }
  194. try {
  195. $logs = $this->getPrettyFormat($command);
  196. } catch (\RuntimeException $e) {
  197. return array();
  198. }
  199. foreach ($logs as $log) {
  200. $commit = new Commit;
  201. $commit->importData($log);
  202. $commits[] = $commit;
  203. }
  204. return $commits;
  205. }
  206. public function searchCommitLog($query)
  207. {
  208. $query = escapeshellarg($query);
  209. $query = strtr($query, array('[' => '\\[', ']' => '\\]'));
  210. $command =
  211. "log --grep={$query} --pretty=format:\"<item><hash>%H</hash>"
  212. . "<short_hash>%h</short_hash><tree>%T</tree><parents>%P</parents>"
  213. . "<author>%an</author><author_email>%ae</author_email>"
  214. . "<date>%at</date><commiter>%cn</commiter>"
  215. . "<commiter_email>%ce</commiter_email>"
  216. . "<commiter_date>%ct</commiter_date>"
  217. . "<message><![CDATA[%s]]></message></item>\"";
  218. try {
  219. $logs = $this->getPrettyFormat($command);
  220. } catch (\RuntimeException $e) {
  221. return array();
  222. }
  223. foreach ($logs as $log) {
  224. $commit = new Commit;
  225. $commit->importData($log);
  226. $commits[] = $commit;
  227. }
  228. return $commits;
  229. }
  230. public function searchTree($query, $branch)
  231. {
  232. $query = escapeshellarg($query);
  233. try {
  234. $results = $this->getClient()->run($this, "grep -I --line-number {$query} $branch");
  235. } catch (\RuntimeException $e) {
  236. return false;
  237. }
  238. $results = explode("\n", $results);
  239. foreach ($results as $result) {
  240. if ($result == '') {
  241. continue;
  242. }
  243. preg_match_all('/([\w-._]+):([^:]+):([0-9]+):(.+)/', $result, $matches, PREG_SET_ORDER);
  244. $data['branch'] = $matches[0][1];
  245. $data['file'] = $matches[0][2];
  246. $data['line'] = $matches[0][3];
  247. $data['match'] = $matches[0][4];
  248. $searchResults[] = $data;
  249. }
  250. return $searchResults;
  251. }
  252. public function getAuthorStatistics($branch)
  253. {
  254. $logs = $this->getClient()->run($this, 'log --pretty=format:"%an||%ae" ' . $branch);
  255. if (empty($logs)) {
  256. throw new \RuntimeException('No statistics available');
  257. }
  258. $logs = explode("\n", $logs);
  259. $logs = array_count_values($logs);
  260. arsort($logs);
  261. foreach ($logs as $user => $count) {
  262. $user = explode('||', $user);
  263. $data[] = array('name' => $user[0], 'email' => $user[1], 'commits' => $count);
  264. }
  265. return $data;
  266. }
  267. public function getStatistics($branch)
  268. {
  269. // Calculate amount of files, extensions and file size
  270. $logs = $this->getClient()->run($this, 'ls-tree -r -l ' . $branch);
  271. $lines = explode("\n", $logs);
  272. $files = array();
  273. $data['extensions'] = array();
  274. $data['size'] = 0;
  275. $data['files'] = 0;
  276. foreach ($lines as $key => $line) {
  277. if (empty($line)) {
  278. unset($lines[$key]);
  279. continue;
  280. }
  281. $files[] = preg_split("/[\s]+/", $line);
  282. }
  283. foreach ($files as $file) {
  284. if ($file[1] == 'blob') {
  285. $data['files']++;
  286. }
  287. if (is_numeric($file[3])) {
  288. $data['size'] += $file[3];
  289. }
  290. if (($pos = strrpos($file[4], '.')) !== false) {
  291. $extension = substr($file[4], $pos);
  292. if (($pos = strrpos($extension, '/')) === false) {
  293. $data['extensions'][] = $extension;
  294. }
  295. }
  296. }
  297. $data['extensions'] = array_count_values($data['extensions']);
  298. arsort($data['extensions']);
  299. return $data;
  300. }
  301. /**
  302. * Create a TAR or ZIP archive of a git tree
  303. *
  304. * @param string $tree Tree-ish reference
  305. * @param string $output Output File name
  306. * @param string $format Archive format
  307. */
  308. public function createArchive($tree, $output, $format = 'zip')
  309. {
  310. $fs = new Filesystem;
  311. $fs->mkdir(dirname($output));
  312. $this->getClient()->run($this, "archive --format=$format --output='$output' $tree");
  313. }
  314. /**
  315. * Return true if $path exists in $branch; return false otherwise.
  316. *
  317. * @param string $commitish Commitish reference; branch, tag, SHA1, etc.
  318. * @param string $path Path whose existence we want to verify.
  319. *
  320. * GRIPE Arguably belongs in Gitter, as it's generally useful functionality.
  321. * Also, this really may not be the best way to do this.
  322. */
  323. public function pathExists($commitish, $path)
  324. {
  325. $output = $this->getClient()->run($this, "ls-tree $commitish '$path'");
  326. if (strlen($output) > 0) {
  327. return true;
  328. }
  329. return false;
  330. }
  331. }