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.

Git.php 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734
  1. <?php
  2. /*
  3. * Git.php
  4. *
  5. * A PHP git library
  6. *
  7. * @package Git.php
  8. * @version 0.1.4
  9. * @author James Brumond
  10. * @copyright Copyright 2013 James Brumond
  11. * @repo http://github.com/kbjr/Git.php
  12. */
  13. if (__FILE__ == $_SERVER['SCRIPT_FILENAME']) die('Bad load order');
  14. // ------------------------------------------------------------------------
  15. /**
  16. * Git Interface Class
  17. *
  18. * This class enables the creating, reading, and manipulation
  19. * of git repositories.
  20. *
  21. * @class Git
  22. */
  23. class Git {
  24. /**
  25. * Git executable location
  26. *
  27. * @var string
  28. */
  29. protected static $bin = '/usr/bin/git';
  30. /**
  31. * Sets git executable path
  32. *
  33. * @param string $path executable location
  34. */
  35. public static function set_bin($path) {
  36. self::$bin = $path;
  37. }
  38. /**
  39. * Gets git executable path
  40. */
  41. public static function get_bin() {
  42. return self::$bin;
  43. }
  44. /**
  45. * Sets up library for use in a default Windows environment
  46. */
  47. public static function windows_mode() {
  48. self::set_bin('git.exe');
  49. }
  50. /**
  51. * Create a new git repository
  52. *
  53. * Accepts a creation path, and, optionally, a source path
  54. *
  55. * @access public
  56. * @param string repository path
  57. * @param string directory to source
  58. * @return GitRepo
  59. */
  60. public static function &create($repo_path, $source = null) {
  61. return GitRepo::create_new($repo_path, $source);
  62. }
  63. /**
  64. * Open an existing git repository
  65. *
  66. * Accepts a repository path
  67. *
  68. * @access public
  69. * @param string repository path
  70. * @return GitRepo
  71. */
  72. public static function open($repo_path) {
  73. return new GitRepo($repo_path);
  74. }
  75. /**
  76. * Clones a remote repo into a directory and then returns a GitRepo object
  77. * for the newly created local repo
  78. *
  79. * Accepts a creation path and a remote to clone from
  80. *
  81. * @access public
  82. * @param string repository path
  83. * @param string remote source
  84. * @return GitRepo
  85. **/
  86. public static function &clone_remote($repo_path, $remote) {
  87. return GitRepo::create_new($repo_path, $remote, true);
  88. }
  89. /**
  90. * Checks if a variable is an instance of GitRepo
  91. *
  92. * Accepts a variable
  93. *
  94. * @access public
  95. * @param mixed variable
  96. * @return bool
  97. */
  98. public static function is_repo($var) {
  99. return (get_class($var) == 'GitRepo');
  100. }
  101. }
  102. // ------------------------------------------------------------------------
  103. /**
  104. * Git Repository Interface Class
  105. *
  106. * This class enables the creating, reading, and manipulation
  107. * of a git repository
  108. *
  109. * @class GitRepo
  110. */
  111. class GitRepo {
  112. protected $repo_path = null;
  113. protected $bare = false;
  114. protected $envopts = array();
  115. /**
  116. * Create a new git repository
  117. *
  118. * Accepts a creation path, and, optionally, a source path
  119. *
  120. * @access public
  121. * @param string repository path
  122. * @param string directory to source
  123. * @return GitRepo
  124. */
  125. public static function &create_new($repo_path, $source = null, $remote_source = false) {
  126. if (is_dir($repo_path) && file_exists($repo_path."/.git") && is_dir($repo_path."/.git")) {
  127. throw new Exception('"'.$repo_path.'" is already a git repository');
  128. } else {
  129. $repo = new self($repo_path, true, false);
  130. if (is_string($source)) {
  131. if ($remote_source) {
  132. $repo->clone_remote($source);
  133. } else {
  134. $repo->clone_from($source);
  135. }
  136. } else {
  137. $repo->run('init');
  138. }
  139. return $repo;
  140. }
  141. }
  142. /**
  143. * Constructor
  144. *
  145. * Accepts a repository path
  146. *
  147. * @access public
  148. * @param string repository path
  149. * @param bool create if not exists?
  150. * @return void
  151. */
  152. public function __construct($repo_path = null, $create_new = false, $_init = true) {
  153. if (is_string($repo_path)) {
  154. $this->set_repo_path($repo_path, $create_new, $_init);
  155. }
  156. }
  157. /**
  158. * Set the repository's path
  159. *
  160. * Accepts the repository path
  161. *
  162. * @access public
  163. * @param string repository path
  164. * @param bool create if not exists?
  165. * @param bool initialize new Git repo if not exists?
  166. * @return void
  167. */
  168. public function set_repo_path($repo_path, $create_new = false, $_init = true) {
  169. if (is_string($repo_path)) {
  170. if ($new_path = realpath($repo_path)) {
  171. $repo_path = $new_path;
  172. if (is_dir($repo_path)) {
  173. // Is this a work tree?
  174. if (file_exists($repo_path."/.git") && is_dir($repo_path."/.git")) {
  175. $this->repo_path = $repo_path;
  176. $this->bare = false;
  177. // Is this a bare repo?
  178. } else if (is_file($repo_path."/config")) {
  179. $parse_ini = parse_ini_file($repo_path."/config");
  180. if ($parse_ini['bare']) {
  181. $this->repo_path = $repo_path;
  182. $this->bare = true;
  183. }
  184. } else {
  185. if ($create_new) {
  186. $this->repo_path = $repo_path;
  187. if ($_init) {
  188. $this->run('init');
  189. }
  190. } else {
  191. throw new Exception('"'.$repo_path.'" is not a git repository');
  192. }
  193. }
  194. } else {
  195. throw new Exception('"'.$repo_path.'" is not a directory');
  196. }
  197. } else {
  198. if ($create_new) {
  199. if ($parent = realpath(dirname($repo_path))) {
  200. mkdir($repo_path);
  201. $this->repo_path = $repo_path;
  202. if ($_init) $this->run('init');
  203. } else {
  204. throw new Exception('cannot create repository in non-existent directory');
  205. }
  206. } else {
  207. throw new Exception('"'.$repo_path.'" does not exist');
  208. }
  209. }
  210. }
  211. }
  212. /**
  213. * Tests if git is installed
  214. *
  215. * @access public
  216. * @return bool
  217. */
  218. public function test_git() {
  219. $descriptorspec = array(
  220. 1 => array('pipe', 'w'),
  221. 2 => array('pipe', 'w'),
  222. );
  223. $pipes = array();
  224. $resource = proc_open(Git::get_bin(), $descriptorspec, $pipes);
  225. $stdout = stream_get_contents($pipes[1]);
  226. $stderr = stream_get_contents($pipes[2]);
  227. foreach ($pipes as $pipe) {
  228. fclose($pipe);
  229. }
  230. $status = trim(proc_close($resource));
  231. return ($status != 127);
  232. }
  233. /**
  234. * Run a command in the git repository
  235. *
  236. * Accepts a shell command to run
  237. *
  238. * @access protected
  239. * @param string command to run
  240. * @return string
  241. */
  242. protected function run_command($command) {
  243. $descriptorspec = array(
  244. 1 => array('pipe', 'w'),
  245. 2 => array('pipe', 'w'),
  246. );
  247. $pipes = array();
  248. /* Depending on the value of variables_order, $_ENV may be empty.
  249. * In that case, we have to explicitly set the new variables with
  250. * putenv, and call proc_open with env=null to inherit the reset
  251. * of the system.
  252. *
  253. * This is kind of crappy because we cannot easily restore just those
  254. * variables afterwards.
  255. *
  256. * If $_ENV is not empty, then we can just copy it and be done with it.
  257. */
  258. if(count($_ENV) === 0) {
  259. $env = NULL;
  260. foreach($this->envopts as $k => $v) {
  261. putenv(sprintf("%s=%s",$k,$v));
  262. }
  263. } else {
  264. $env = array_merge($_ENV, $this->envopts);
  265. }
  266. $cwd = $this->repo_path;
  267. $resource = proc_open($command, $descriptorspec, $pipes, $cwd, $env);
  268. $stdout = stream_get_contents($pipes[1]);
  269. $stderr = stream_get_contents($pipes[2]);
  270. foreach ($pipes as $pipe) {
  271. fclose($pipe);
  272. }
  273. $status = trim(proc_close($resource));
  274. if ($status) throw new Exception($stderr);
  275. //exec($command, $stdout);
  276. return $stdout;
  277. }
  278. /**
  279. * Run a git command in the git repository
  280. *
  281. * Accepts a git command to run
  282. *
  283. * @access public
  284. * @param string command to run
  285. * @return string
  286. */
  287. public function run($command) {
  288. return $this->run_command(Git::get_bin().' '.$command);
  289. }
  290. /**
  291. * Runs a 'git status' call
  292. *
  293. * Accept a convert to HTML bool
  294. *
  295. * @access public
  296. * @param bool return string with <br />
  297. * @return string
  298. */
  299. public function status($html = false) {
  300. $msg = $this->run("status");
  301. if ($html == true) {
  302. $msg = str_replace("\n", "<br />", $msg);
  303. }
  304. return $msg;
  305. }
  306. /**
  307. * Runs a `git add` call
  308. *
  309. * Accepts a list of files to add
  310. *
  311. * @access public
  312. * @param mixed files to add
  313. * @return string
  314. */
  315. public function add($files = "*") {
  316. if (is_array($files)) {
  317. $files = '"'.implode('" "', $files).'"';
  318. }
  319. return $this->run("add $files -v");
  320. }
  321. /**
  322. * Runs a `git rm` call
  323. *
  324. * Accepts a list of files to remove
  325. *
  326. * @access public
  327. * @param mixed files to remove
  328. * @param Boolean use the --cached flag?
  329. * @return string
  330. */
  331. public function rm($files = "*", $cached = false) {
  332. if (is_array($files)) {
  333. $files = '"'.implode('" "', $files).'"';
  334. }
  335. return $this->run("rm ".($cached ? '--cached ' : '').$files);
  336. }
  337. /**
  338. * Runs a `git commit` call
  339. *
  340. * Accepts a commit message string
  341. *
  342. * @access public
  343. * @param string commit message
  344. * @return string
  345. */
  346. public function commit($message = "") {
  347. return $this->run("commit -av -m ".escapeshellarg($message));
  348. }
  349. /**
  350. * Runs a `git clone` call to clone the current repository
  351. * into a different directory
  352. *
  353. * Accepts a target directory
  354. *
  355. * @access public
  356. * @param string target directory
  357. * @return string
  358. */
  359. public function clone_to($target) {
  360. return $this->run("clone --local ".$this->repo_path." $target");
  361. }
  362. /**
  363. * Runs a `git clone` call to clone a different repository
  364. * into the current repository
  365. *
  366. * Accepts a source directory
  367. *
  368. * @access public
  369. * @param string source directory
  370. * @return string
  371. */
  372. public function clone_from($source) {
  373. return $this->run("clone --local $source ".$this->repo_path);
  374. }
  375. /**
  376. * Runs a `git clone` call to clone a remote repository
  377. * into the current repository
  378. *
  379. * Accepts a source url
  380. *
  381. * @access public
  382. * @param string source url
  383. * @return string
  384. */
  385. public function clone_remote($source) {
  386. return $this->run("clone $source ".$this->repo_path);
  387. }
  388. /**
  389. * Runs a `git clean` call
  390. *
  391. * Accepts a remove directories flag
  392. *
  393. * @access public
  394. * @param bool delete directories?
  395. * @param bool force clean?
  396. * @return string
  397. */
  398. public function clean($dirs = false, $force = false) {
  399. return $this->run("clean".(($force) ? " -f" : "").(($dirs) ? " -d" : ""));
  400. }
  401. /**
  402. * Runs a `git branch` call
  403. *
  404. * Accepts a name for the branch
  405. *
  406. * @access public
  407. * @param string branch name
  408. * @return string
  409. */
  410. public function create_branch($branch) {
  411. return $this->run("branch $branch");
  412. }
  413. /**
  414. * Runs a `git branch -[d|D]` call
  415. *
  416. * Accepts a name for the branch
  417. *
  418. * @access public
  419. * @param string branch name
  420. * @return string
  421. */
  422. public function delete_branch($branch, $force = false) {
  423. return $this->run("branch ".(($force) ? '-D' : '-d')." $branch");
  424. }
  425. /**
  426. * Runs a `git branch` call
  427. *
  428. * @access public
  429. * @param bool keep asterisk mark on active branch
  430. * @return array
  431. */
  432. public function list_branches($keep_asterisk = false) {
  433. $branchArray = explode("\n", $this->run("branch"));
  434. foreach($branchArray as $i => &$branch) {
  435. $branch = trim($branch);
  436. if (! $keep_asterisk) {
  437. $branch = str_replace("* ", "", $branch);
  438. }
  439. if ($branch == "") {
  440. unset($branchArray[$i]);
  441. }
  442. }
  443. return $branchArray;
  444. }
  445. /**
  446. * Lists remote branches (using `git branch -r`).
  447. *
  448. * Also strips out the HEAD reference (e.g. "origin/HEAD -> origin/master").
  449. *
  450. * @access public
  451. * @return array
  452. */
  453. public function list_remote_branches() {
  454. $branchArray = explode("\n", $this->run("branch -r"));
  455. foreach($branchArray as $i => &$branch) {
  456. $branch = trim($branch);
  457. if ($branch == "" || strpos($branch, 'HEAD -> ') !== false) {
  458. unset($branchArray[$i]);
  459. }
  460. }
  461. return $branchArray;
  462. }
  463. /**
  464. * Returns name of active branch
  465. *
  466. * @access public
  467. * @param bool keep asterisk mark on branch name
  468. * @return string
  469. */
  470. public function active_branch($keep_asterisk = false) {
  471. $branchArray = $this->list_branches(true);
  472. $active_branch = preg_grep("/^\*/", $branchArray);
  473. reset($active_branch);
  474. if ($keep_asterisk) {
  475. return current($active_branch);
  476. } else {
  477. return str_replace("* ", "", current($active_branch));
  478. }
  479. }
  480. /**
  481. * Runs a `git checkout` call
  482. *
  483. * Accepts a name for the branch
  484. *
  485. * @access public
  486. * @param string branch name
  487. * @return string
  488. */
  489. public function checkout($branch) {
  490. return $this->run("checkout $branch");
  491. }
  492. /**
  493. * Runs a `git merge` call
  494. *
  495. * Accepts a name for the branch to be merged
  496. *
  497. * @access public
  498. * @param string $branch
  499. * @return string
  500. */
  501. public function merge($branch) {
  502. return $this->run("merge $branch --no-ff");
  503. }
  504. /**
  505. * Runs a git fetch on the current branch
  506. *
  507. * @access public
  508. * @return string
  509. */
  510. public function fetch() {
  511. return $this->run("fetch");
  512. }
  513. /**
  514. * Add a new tag on the current position
  515. *
  516. * Accepts the name for the tag and the message
  517. *
  518. * @param string $tag
  519. * @param string $message
  520. * @return string
  521. */
  522. public function add_tag($tag, $message = null) {
  523. if ($message === null) {
  524. $message = $tag;
  525. }
  526. return $this->run("tag -a $tag -m $message");
  527. }
  528. /**
  529. * List all the available repository tags.
  530. *
  531. * Optionally, accept a shell wildcard pattern and return only tags matching it.
  532. *
  533. * @access public
  534. * @param string $pattern Shell wildcard pattern to match tags against.
  535. * @return array Available repository tags.
  536. */
  537. public function list_tags($pattern = null) {
  538. $tagArray = explode("\n", $this->run("tag -l $pattern"));
  539. foreach ($tagArray as $i => &$tag) {
  540. $tag = trim($tag);
  541. if ($tag == '') {
  542. unset($tagArray[$i]);
  543. }
  544. }
  545. return $tagArray;
  546. }
  547. /**
  548. * Push specific branch to a remote
  549. *
  550. * Accepts the name of the remote and local branch
  551. *
  552. * @param string $remote
  553. * @param string $branch
  554. * @return string
  555. */
  556. public function push($remote, $branch) {
  557. return $this->run("push --tags $remote $branch");
  558. }
  559. /**
  560. * Pull specific branch from remote
  561. *
  562. * Accepts the name of the remote and local branch
  563. *
  564. * @param string $remote
  565. * @param string $branch
  566. * @return string
  567. */
  568. public function pull($remote, $branch) {
  569. return $this->run("pull $remote $branch");
  570. }
  571. /**
  572. * Sets the project description.
  573. *
  574. * @param string $new
  575. */
  576. public function set_description($new) {
  577. file_put_contents($this->repo_path."\\.git\\description", $new);
  578. }
  579. /**
  580. * Gets the project description.
  581. *
  582. * @return string
  583. */
  584. public function get_description() {
  585. return file_get_contents($this->repo_path."\\.git\\description");
  586. }
  587. /**
  588. * Sets custom environment options for calling Git
  589. *
  590. * @param string key
  591. * @param string value
  592. */
  593. public function setenv($key, $value) {
  594. $this->envopts[$key] = $value;
  595. }
  596. /**
  597. * Searches for valid repositories on the specified path
  598. *
  599. * @param array $paths Array of paths where repositories will be searched
  600. * @return array Found repositories, containing their name, path and description
  601. */
  602. public function getRepositories($paths)
  603. {
  604. $allRepositories = array();
  605. foreach ($paths as $path) {
  606. $repositories = $this->recurseDirectory($path);
  607. if (empty($repositories)) {
  608. throw new \RuntimeException('There are no GIT repositories in ' . $path);
  609. }
  610. $allRepositories = array_merge($allRepositories, $repositories);
  611. }
  612. $allRepositories = array_unique($allRepositories, SORT_REGULAR);
  613. asort($allRepositories);
  614. return $allRepositories;
  615. }
  616. public function recurseDirectory($path, $topLevel = true)
  617. {
  618. $dir = new \DirectoryIterator($path);
  619. $repositories = array();
  620. foreach ($dir as $file) {
  621. if ($file->isDot()) {
  622. continue;
  623. }
  624. if (strrpos($file->getFilename(), '.') === 0) {
  625. continue;
  626. }
  627. if (!$file->isReadable()) {
  628. continue;
  629. }
  630. if ($file->isDir()) {
  631. $isBare = file_exists($file->getPathname() . '\\HEAD');
  632. $isRepository = file_exists($file->getPathname() . '\\.git\\HEAD');
  633. if ($isRepository || $isBare) {
  634. if ($isBare) {
  635. $description = $file->getPathname() . '\\description';
  636. } else {
  637. $description = $file->getPathname() . '\\.git\\description';
  638. }
  639. if (file_exists($description)) {
  640. $description = file_get_contents($description);
  641. } else {
  642. $description = null;
  643. }
  644. if (!$topLevel) {
  645. $repoName = $file->getPathInfo()->getFilename() . '\\' . $file->getFilename();
  646. } else {
  647. $repoName = $file->getFilename();
  648. }
  649. $repositories[$repoName] = array(
  650. 'name' => $repoName,
  651. 'path' => $file->getPathname(),
  652. 'description' => $description
  653. );
  654. continue;
  655. } else {
  656. $repositories = array_merge($repositories, $this->recurseDirectory($file->getPathname(), false));
  657. }
  658. }
  659. }
  660. return $repositories;
  661. }
  662. }
  663. /* End of file */