PageRenderTime 59ms CodeModel.GetById 28ms RepoModel.GetById 0ms app.codeStats 0ms

/src/GitRepo.php

http://github.com/kbjr/Git.php
PHP | 630 lines | 295 code | 46 blank | 289 comment | 60 complexity | 2323f85769951f5c05dd1dd163056715 MD5 | raw file
Possible License(s): GPL-3.0
  1. <?php
  2. namespace Kbjr\Git;
  3. use Exception;
  4. /**
  5. * Git Repository Interface Class
  6. *
  7. * This class enables the creating, reading, and manipulation
  8. * of a git repository
  9. *
  10. * @class GitRepo
  11. */
  12. class GitRepo {
  13. protected $repoPath = null;
  14. protected $bare = false;
  15. protected $envopts = [];
  16. /**
  17. * Create a new git repository
  18. *
  19. * Accepts a creation path, and, optionally, a source path
  20. *
  21. * @access public
  22. * @param string repository path
  23. * @param string directory to source
  24. * @param string reference path
  25. * @return GitRepo
  26. */
  27. public static function createNew($repoPath, $source = null, $remoteSource = false, $reference = null)
  28. {
  29. if (is_dir($repoPath) && file_exists($repoPath . "/.git")) {
  30. throw new Exception('"' . $repoPath . '" is already a git repository');
  31. } else {
  32. $repo = new self($repoPath, true, false);
  33. if (is_string($source)) {
  34. if ($remoteSource) {
  35. if (isset($reference)) {
  36. if (!is_dir($reference) || !is_dir($reference.'/.git')) {
  37. throw new Exception('"' . $reference . '" is not a git repository. Cannot use as reference.');
  38. } else if (strlen($reference)) {
  39. $reference = realpath($reference);
  40. $reference = "--reference $reference";
  41. }
  42. }
  43. $repo->cloneRemote($source, $reference);
  44. } else {
  45. $repo->cloneFrom($source);
  46. }
  47. } else {
  48. $repo->run('init');
  49. }
  50. return $repo;
  51. }
  52. }
  53. /**
  54. * Constructor
  55. *
  56. * Accepts a repository path
  57. *
  58. * @access public
  59. * @param string repository path
  60. * @param bool create if not exists?
  61. * @return void
  62. */
  63. public function __construct($repoPath = null, $createNew = false, $_init = true)
  64. {
  65. if (is_string($repoPath)) {
  66. $this->setRepoPath($repoPath, $createNew, $_init);
  67. }
  68. }
  69. /**
  70. * Set the repository's path
  71. *
  72. * Accepts the repository path
  73. *
  74. * @access public
  75. * @param string repository path
  76. * @param bool create if not exists?
  77. * @param bool initialize new Git repo if not exists?
  78. * @return void
  79. */
  80. public function setRepoPath($repoPath, $createNew = false, $_init = true) {
  81. if (is_string($repoPath)) {
  82. if ($newPath = realpath($repoPath)) {
  83. $repoPath = $newPath;
  84. if (is_dir($repoPath)) {
  85. // Is this a work tree?
  86. if (file_exists($repoPath . "/.git")) {
  87. $this->repoPath = $repoPath;
  88. $this->bare = false;
  89. // Is this a bare repo?
  90. } else if (is_file($repoPath . "/config")) {
  91. $parse_ini = parse_ini_file($repoPath . "/config");
  92. if ($parse_ini['bare']) {
  93. $this->repoPath = $repoPath;
  94. $this->bare = true;
  95. }
  96. } else {
  97. if ($createNew) {
  98. $this->repoPath = $repoPath;
  99. if ($_init) {
  100. $this->run('init');
  101. }
  102. } else {
  103. throw new Exception('"' . $repoPath . '" is not a git repository');
  104. }
  105. }
  106. } else {
  107. throw new Exception('"' . $repoPath . '" is not a directory');
  108. }
  109. } else {
  110. if ($createNew) {
  111. if ($parent = realpath(dirname($repoPath))) {
  112. mkdir($repoPath);
  113. $this->repoPath = $repoPath;
  114. if ($_init) $this->run('init');
  115. } else {
  116. throw new Exception('cannot create repository in non-existent directory');
  117. }
  118. } else {
  119. throw new Exception('"' . $repoPath . '" does not exist');
  120. }
  121. }
  122. }
  123. }
  124. /**
  125. * Get the path to the git repo directory (eg. the ".git" directory)
  126. *
  127. * @access public
  128. * @return string
  129. */
  130. public function gitDirectoryPath()
  131. {
  132. if ($this->bare) {
  133. return $this->repoPath;
  134. } else if (is_dir($this->repoPath . "/.git")) {
  135. return $this->repoPath . "/.git";
  136. } else if (is_file($this->repoPath . "/.git")) {
  137. $gitFile = file_get_contents($this->repoPath . "/.git");
  138. if(mb_ereg("^gitdir: (.+)$", $gitFile, $matches)){
  139. if($matches[1]) {
  140. $relGitPath = $matches[1];
  141. return $this->repoPath . "/" . $relGitPath;
  142. }
  143. }
  144. }
  145. throw new Exception('could not find git dir for ' . $this->repoPath . '.');
  146. }
  147. /**
  148. * Tests if git is installed
  149. *
  150. * @access public
  151. * @return bool
  152. */
  153. public function testGit()
  154. {
  155. $descriptorspec = [
  156. 1 => ['pipe', 'w'],
  157. 2 => ['pipe', 'w'],
  158. ];
  159. $pipes = [];
  160. $resource = proc_open(Git::getBin(), $descriptorspec, $pipes);
  161. foreach ($pipes as $pipe) {
  162. fclose($pipe);
  163. }
  164. $status = trim(proc_close($resource));
  165. return ($status != 127);
  166. }
  167. /**
  168. * Run a command in the git repository
  169. *
  170. * Accepts a shell command to run
  171. *
  172. * @access protected
  173. * @param string command to run
  174. * @return string
  175. */
  176. protected function runCommand($command)
  177. {
  178. $descriptorspec = [
  179. 1 => ['pipe', 'w'],
  180. 2 => ['pipe', 'w'],
  181. ];
  182. $pipes = [];
  183. /* Depending on the value of variables_order, $_ENV may be empty.
  184. * In that case, we have to explicitly set the new variables with
  185. * putenv, and call proc_open with env=null to inherit the reset
  186. * of the system.
  187. *
  188. * This is kind of crappy because we cannot easily restore just those
  189. * variables afterwards.
  190. *
  191. * If $_ENV is not empty, then we can just copy it and be done with it.
  192. */
  193. if(count($_ENV) === 0) {
  194. $env = NULL;
  195. foreach($this->envopts as $k => $v) {
  196. putenv(sprintf("%s=%s",$k,$v));
  197. }
  198. } else {
  199. $env = array_merge($_ENV, $this->envopts);
  200. }
  201. $cwd = $this->repoPath;
  202. $resource = proc_open($command, $descriptorspec, $pipes, $cwd, $env);
  203. $stdout = stream_get_contents($pipes[1]);
  204. $stderr = stream_get_contents($pipes[2]);
  205. foreach ($pipes as $pipe) {
  206. fclose($pipe);
  207. }
  208. $status = trim(proc_close($resource));
  209. if ($status) throw new Exception($stderr . "\n" . $stdout); //Not all errors are printed to stderr, so include std out as well.
  210. return $stdout;
  211. }
  212. /**
  213. * Run a git command in the git repository
  214. *
  215. * Accepts a git command to run
  216. *
  217. * @access public
  218. * @param string command to run
  219. * @return string
  220. */
  221. public function run($command)
  222. {
  223. return $this->runCommand(Git::getBin()." ".$command);
  224. }
  225. /**
  226. * Runs a 'git status' call
  227. *
  228. * Accept a convert to HTML bool
  229. *
  230. * @access public
  231. * @param bool return string with <br />
  232. * @return string
  233. */
  234. public function status($html = false)
  235. {
  236. $msg = $this->run("status");
  237. if ($html == true) {
  238. $msg = str_replace("\n", "<br />", $msg);
  239. }
  240. return $msg;
  241. }
  242. /**
  243. * Runs a `git add` call
  244. *
  245. * Accepts a list of files to add
  246. *
  247. * @access public
  248. * @param mixed files to add
  249. * @return string
  250. */
  251. public function add($files = "*")
  252. {
  253. if (is_array($files)) {
  254. $files = '"'.implode('" "', $files).'"';
  255. }
  256. return $this->run("add $files -v");
  257. }
  258. /**
  259. * Runs a `git rm` call
  260. *
  261. * Accepts a list of files to remove
  262. *
  263. * @access public
  264. * @param mixed files to remove
  265. * @param Boolean use the --cached flag?
  266. * @return string
  267. */
  268. public function rm($files = "*", $cached = false)
  269. {
  270. if (is_array($files)) {
  271. $files = '"'.implode('" "', $files).'"';
  272. }
  273. return $this->run("rm ".($cached ? '--cached ' : '').$files);
  274. }
  275. /**
  276. * Runs a `git commit` call
  277. *
  278. * Accepts a commit message string
  279. *
  280. * @access public
  281. * @param string commit message
  282. * @param boolean should all files be committed automatically (-a flag)
  283. * @return string
  284. */
  285. public function commit($message = "", $commitAll = true)
  286. {
  287. $flags = $commitAll ? '-av' : '-v';
  288. return $this->run("commit " . $flags . " -m " . escapeshellarg($message));
  289. }
  290. /**
  291. * Runs a `git clone` call to clone the current repository
  292. * into a different directory
  293. *
  294. * Accepts a target directory
  295. *
  296. * @access public
  297. * @param string target directory
  298. * @return string
  299. */
  300. public function cloneTo($target)
  301. {
  302. return $this->run("clone --local " . $this->repoPath . " $target");
  303. }
  304. /**
  305. * Runs a `git clone` call to clone a different repository
  306. * into the current repository
  307. *
  308. * Accepts a source directory
  309. *
  310. * @access public
  311. * @param string source directory
  312. * @return string
  313. */
  314. public function cloneFrom($source) {
  315. return $this->run("clone --local $source " . $this->repoPath);
  316. }
  317. /**
  318. * Runs a `git clone` call to clone a remote repository
  319. * into the current repository
  320. *
  321. * Accepts a source url
  322. *
  323. * @access public
  324. * @param string source url
  325. * @param string reference path
  326. * @return string
  327. */
  328. public function cloneRemote($source, $reference)
  329. {
  330. return $this->run("clone $reference $source " . $this->repoPath);
  331. }
  332. /**
  333. * Runs a `git clean` call
  334. *
  335. * Accepts a remove directories flag
  336. *
  337. * @access public
  338. * @param bool delete directories?
  339. * @param bool force clean?
  340. * @return string
  341. */
  342. public function clean($dirs = false, $force = false)
  343. {
  344. return $this->run("clean".(($force) ? " -f" : "").(($dirs) ? " -d" : ""));
  345. }
  346. /**
  347. * Runs a `git branch` call
  348. *
  349. * Accepts a name for the branch
  350. *
  351. * @access public
  352. * @param string branch name
  353. * @return string
  354. */
  355. public function createBranch($branch)
  356. {
  357. return $this->run("branch " . escapeshellarg($branch));
  358. }
  359. /**
  360. * Runs a `git branch -[d|D]` call
  361. *
  362. * Accepts a name for the branch
  363. *
  364. * @access public
  365. * @param string branch name
  366. * @return string
  367. */
  368. public function deleteBranch($branch, $force = false)
  369. {
  370. return $this->run("branch ".(($force) ? '-D' : '-d')." $branch");
  371. }
  372. /**
  373. * Runs a `git branch` call
  374. *
  375. * @access public
  376. * @param bool keep asterisk mark on active branch
  377. * @return array
  378. */
  379. public function listBranches($keep_asterisk = false)
  380. {
  381. $branchArray = explode("\n", $this->run("branch"));
  382. foreach($branchArray as $i => &$branch) {
  383. $branch = trim($branch);
  384. if (! $keep_asterisk) {
  385. $branch = str_replace("* ", "", $branch);
  386. }
  387. if ($branch == "") {
  388. unset($branchArray[$i]);
  389. }
  390. }
  391. return $branchArray;
  392. }
  393. /**
  394. * Lists remote branches (using `git branch -r`).
  395. *
  396. * Also strips out the HEAD reference (e.g. "origin/HEAD -> origin/master").
  397. *
  398. * @access public
  399. * @return array
  400. */
  401. public function listRemoteBranches()
  402. {
  403. $branchArray = explode("\n", $this->run("branch -r"));
  404. foreach($branchArray as $i => &$branch) {
  405. $branch = trim($branch);
  406. if ($branch == "" || strpos($branch, 'HEAD -> ') !== false) {
  407. unset($branchArray[$i]);
  408. }
  409. }
  410. return $branchArray;
  411. }
  412. /**
  413. * Returns name of active branch
  414. *
  415. * @access public
  416. * @param bool keep asterisk mark on branch name
  417. * @return string
  418. */
  419. public function activeBranch($keep_asterisk = false)
  420. {
  421. $branchArray = $this->listBranches(true);
  422. $active_branch = preg_grep("/^\*/", $branchArray);
  423. reset($active_branch);
  424. if ($keep_asterisk) {
  425. return current($active_branch);
  426. } else {
  427. return str_replace("* ", "", current($active_branch));
  428. }
  429. }
  430. /**
  431. * Runs a `git checkout` call
  432. *
  433. * Accepts a name for the branch
  434. *
  435. * @access public
  436. * @param string branch name
  437. * @return string
  438. */
  439. public function checkout($branch)
  440. {
  441. return $this->run("checkout " . escapeshellarg($branch));
  442. }
  443. /**
  444. * Runs a `git merge` call
  445. *
  446. * Accepts a name for the branch to be merged
  447. *
  448. * @access public
  449. * @param string $branch
  450. * @return string
  451. */
  452. public function merge($branch)
  453. {
  454. return $this->run("merge " . escapeshellarg($branch) . " --no-ff");
  455. }
  456. /**
  457. * Runs a git fetch on the current branch
  458. *
  459. * @access public
  460. * @return string
  461. */
  462. public function fetch()
  463. {
  464. return $this->run("fetch");
  465. }
  466. /**
  467. * Add a new tag on the current position
  468. *
  469. * Accepts the name for the tag and the message
  470. *
  471. * @param string $tag
  472. * @param string $message
  473. * @return string
  474. */
  475. public function addTag($tag, $message = null)
  476. {
  477. if (is_null($message)) {
  478. $message = $tag;
  479. }
  480. return $this->run("tag -a $tag -m " . escapeshellarg($message));
  481. }
  482. /**
  483. * List all the available repository tags.
  484. *
  485. * Optionally, accept a shell wildcard pattern and return only tags matching it.
  486. *
  487. * @access public
  488. * @param string $pattern Shell wildcard pattern to match tags against.
  489. * @return array Available repository tags.
  490. */
  491. public function listTags($pattern = null) {
  492. $tagArray = explode("\n", $this->run("tag -l $pattern"));
  493. foreach ($tagArray as $i => &$tag) {
  494. $tag = trim($tag);
  495. if (empty($tag)) {
  496. unset($tagArray[$i]);
  497. }
  498. }
  499. return $tagArray;
  500. }
  501. /**
  502. * Push specific branch (or all branches) to a remote
  503. *
  504. * Accepts the name of the remote and local branch.
  505. * If omitted, the command will be "git push", and therefore will take
  506. * on the behavior of your "push.defualt" configuration setting.
  507. *
  508. * @param string $remote
  509. * @param string $branch
  510. * @return string
  511. */
  512. public function push($remote = "", $branch = "")
  513. {
  514. //--tags removed since this was preventing branches from being pushed (only tags were)
  515. return $this->run("push $remote $branch");
  516. }
  517. /**
  518. * Pull specific branch from remote
  519. *
  520. * Accepts the name of the remote and local branch.
  521. * If omitted, the command will be "git pull", and therefore will take on the
  522. * behavior as-configured in your clone / environment.
  523. *
  524. * @param string $remote
  525. * @param string $branch
  526. * @return string
  527. */
  528. public function pull($remote = "", $branch = "")
  529. {
  530. return $this->run("pull $remote $branch");
  531. }
  532. /**
  533. * List log entries.
  534. *
  535. * @param string $format
  536. * @return string
  537. */
  538. public function log($format = null, $fullDiff = false, $filepath = null, $follow = false)
  539. {
  540. $diff = "";
  541. if ($fullDiff){
  542. $diff = "--full-diff -p ";
  543. }
  544. if ($follow){
  545. // Can't use full-diff with follow
  546. $diff = "--follow -- ";
  547. }
  548. if ($format === null) {
  549. return $this->run('log ' . $diff . $filepath);
  550. } else {
  551. return $this->run('log --pretty=format:"' . $format . '" ' . $diff . $filepath);
  552. }
  553. }
  554. /**
  555. * Sets the project description.
  556. *
  557. * @param string $new
  558. */
  559. public function setDescription($new)
  560. {
  561. $path = $this->gitDirectoryPath();
  562. file_put_contents($path . "/description", $new);
  563. }
  564. /**
  565. * Gets the project description.
  566. *
  567. * @return string
  568. */
  569. public function getDescription()
  570. {
  571. $path = $this->gitDirectoryPath();
  572. return file_get_contents($path . "/description");
  573. }
  574. /**
  575. * Sets custom environment options for calling Git
  576. *
  577. * @param string key
  578. * @param string value
  579. */
  580. public function setenv($key, $value)
  581. {
  582. $this->envopts[$key] = $value;
  583. }
  584. }