1<?php 2 3/* 4 * Git.php 5 * 6 * A PHP git library 7 * 8 * @package Git.php 9 * @version 0.1.1-a 10 * @author James Brumond 11 * @copyright Copyright 2010 James Brumond 12 * @license http://github.com/kbjr/Git.php 13 * @link http://code.kbjrweb.com/project/gitphp 14 */ 15 16if (__FILE__ == $_SERVER['SCRIPT_FILENAME']) die('Bad load order'); 17 18// ------------------------------------------------------------------------ 19 20/** 21 * Git Interface Class 22 * 23 * This class enables the creating, reading, and manipulation 24 * of git repositories. 25 * 26 * @class Git 27 */ 28class Git { 29 30 /** 31 * Create a new git repository 32 * 33 * Accepts a creation path, and, optionally, a source path 34 * 35 * @access public 36 * @param string repository path 37 * @param string directory to source 38 * @return GitRepo 39 */ 40 public static function &create($repo_path, $source = null) { 41 return GitRepo::create_new($repo_path, $source); 42 } 43 44 /** 45 * Open an existing git repository 46 * 47 * Accepts a repository path 48 * 49 * @access public 50 * @param string repository path 51 * @return GitRepo 52 */ 53 public static function open($repo_path) { 54 return new GitRepo($repo_path); 55 } 56 57 /** 58 * Checks if a variable is an instance of GitRepo 59 * 60 * Accepts a variable 61 * 62 * @access public 63 * @param mixed variable 64 * @return bool 65 */ 66 public static function is_repo($var) { 67 return (get_class($var) == 'GitRepo'); 68 } 69 70} 71 72// ------------------------------------------------------------------------ 73 74/** 75 * Git Repository Interface Class 76 * 77 * This class enables the creating, reading, and manipulation 78 * of a git repository 79 * 80 * @class GitRepo 81 */ 82class GitRepo { 83 84 protected $repo_path = null; 85 86 public function get_repo_path() { 87 return $this->repo_path; 88 } 89 90 public $git_path = '/usr/bin/git'; 91 /* The git path defaults to the default location for linux, the consumer of this class needs to override with setting from config: 92 93 function doSomeGitWork() { 94 global $conf; 95 $this->getConf(''); 96 $git_exe_path = $conf['plugin']['git']['git_exe_path']; 97 98 $repo = new GitRepo(.....); 99 $repo->git_path = $git_exe_path; 100 .... do more work here .... 101 } 102 103 Make sure you enclose the path with double quotes for windows paths like this: 104 $conf['plugin']['git']['git_exe_path'] = '"C:\Program Files (x86)\Git\bin\git.exe"'; 105 */ 106 107 /** 108 * Create a new git repository 109 * 110 * Accepts a creation path, and, optionally, a source path 111 * 112 * @access public 113 * @param string repository path 114 * @param string directory to source 115 * @return GitRepo 116 */ 117 public static function &create_new($repo_path, $source = null) { 118 if (is_dir($repo_path) && file_exists($repo_path."/.git") && is_dir($repo_path."/.git")) { 119 throw new Exception('"'.$repo_path.'" is already a git repository'); 120 } else { 121 $repo = new self($repo_path, true, false); 122 if (is_string($source)) 123 $repo->clone_from($source); 124 else $repo->run('init'); 125 return $repo; 126 } 127 } 128 129 /** 130 * Constructor 131 * 132 * Accepts a repository path 133 * 134 * @access public 135 * @param string repository path 136 * @param bool create if not exists? 137 * @return void 138 */ 139 public function __construct($repo_path = null, $create_new = false, $_init = true) { 140 if (is_string($repo_path)) 141 $this->set_repo_path($repo_path, $create_new, $_init); 142 } 143 144 /** 145 * Set the repository's path 146 * 147 * Accepts the repository path 148 * 149 * @access public 150 * @param string repository path 151 * @param bool create if not exists? 152 * @return void 153 */ 154 public function set_repo_path($repo_path, $create_new = false, $_init = true) { 155 if (is_string($repo_path)) { 156 if ($new_path = realpath($repo_path)) { 157 $repo_path = $new_path; 158 if (is_dir($repo_path)) { 159 if (file_exists($repo_path."/.git") && is_dir($repo_path."/.git")) { 160 $this->repo_path = $repo_path; 161 } else { 162 if ($create_new) { 163 $this->repo_path = $repo_path; 164 if ($_init) $this->run('init'); 165 } else { 166 throw new Exception('"'.$repo_path.'" is not a git repository'); 167 } 168 } 169 } else { 170 throw new Exception('"'.$repo_path.'" is not a directory'); 171 } 172 } else { 173 if ($create_new) { 174 if ($parent = realpath(dirname($repo_path))) { 175 mkdir($repo_path); 176 $this->repo_path = $repo_path; 177 if ($_init) $this->run('init'); 178 } else { 179 throw new Exception('cannot create repository in non-existent directory'); 180 } 181 } else { 182 throw new Exception('"'.$repo_path.'" does not exist'); 183 } 184 } 185 } 186 } 187 188 /** 189 * Tests if git is installed 190 * 191 * @access public 192 * @return bool 193 */ 194 public function test_git() { 195 $descriptorspec = array( 196 1 => array('pipe', 'w'), 197 2 => array('pipe', 'w'), 198 ); 199 $pipes = array(); 200 $resource = proc_open($this->git_path, $descriptorspec, $pipes); 201 202 $stdout = stream_get_contents($pipes[1]); 203 $stderr = stream_get_contents($pipes[2]); 204 foreach ($pipes as $pipe) { 205 fclose($pipe); 206 } 207 208 $status = trim(proc_close($resource)); 209 return ($status != 127); 210 } 211 212 /** 213 * Run a command in the git repository 214 * 215 * Accepts a shell command to run 216 * 217 * @access protected 218 * @param string command to run 219 * @return string 220 */ 221 protected function run_command($command) { 222 223 $descriptorspec = array( 224 1 => array('pipe', 'w'), 225 2 => array('pipe', 'w'), 226 ); 227 $pipes = array(); 228 $resource = proc_open($command, $descriptorspec, $pipes, $this->repo_path); 229 230 $stdout = stream_get_contents($pipes[1]); 231 $stderr = stream_get_contents($pipes[2]); 232 foreach ($pipes as $pipe) { 233 fclose($pipe); 234 } 235 236 $status = trim(proc_close($resource)); 237 if ($status) throw new Exception($stderr); 238 239 return $stdout; 240 } 241 242 /** 243 * Run a git command in the git repository 244 * 245 * Accepts a git command to run 246 * 247 * @access public 248 * @param string command to run 249 * @return string 250 */ 251 public function run($command) { 252 $path = $this->git_path; 253 return $this->run_command($path." ".$command); 254 } 255 256 /** 257 * Runs a `git add` call 258 * 259 * Accepts a list of files to add 260 * 261 * @access public 262 * @param mixed files to add 263 * @return string 264 */ 265 public function add($files = "*") { 266 if (is_array($files)) $files = '"'.implode('" "', $files).'"'; 267 return $this->run("add $files -v"); 268 } 269 270 /** 271 * Runs a `git log` call 272 * 273 * @access public 274 * @return string 275 */ 276 public function get_log($revision="..origin/master") { 277 return $this->run("log ".$revision." --reverse"); 278 } 279 280 /** 281 * Retieves a specific file from GIT 282 * 283 * @access public 284 * @param string filename 285 * @param string identifyer to id the branch/commit/position 286 * @return string 287 */ 288 public function getFile($filename, $branch = 'HEAD') { 289 290 $cmd = 'show '.$branch.':'.$filename; 291 try 292 { 293 return $this->run($cmd); 294 } 295 catch (Exception $e) 296 { 297 // msg('Exception during command: '.$cmd); 298 // Not really an exception, if a new page has been added the exception is part of normal operation :-( 299 return "Page not found"; 300 } 301 } 302 303 304 /** 305 * Runs a `git status` call 306 * 307 * @access public 308 * @param bool porcelain 309 * @return string 310 */ 311 public function get_status($porcelain=true) { 312 try 313 { 314 if ($porcelain) return $this->run("status -u --porcelain"); 315 return $this->run("status"); 316 } 317 catch(Exception $e) 318 { 319 return $e->getMessage(); 320 } 321 } 322 323 function LocalCommitsExist() { 324 $status = $this->get_status(false); 325 $pos = strpos($status, 'Your branch is ahead of'); 326 return $pos > 0; 327 } 328 329 // As suggested by: https://gist.github.com/961488 330 function &get_commits($log) 331 { 332 $output = explode("\n", $log); 333 $history = array(); 334 foreach($output as $line) 335 { 336 if(strpos($line, 'commit')===0){ 337 // Skip merges 338 if (strpos($line, 'merge') > 0) continue; 339 if(!empty($commit)){ 340 array_push($history, $commit); 341 unset($commit); 342 } 343 $commit['hash'] = trim(substr($line, strlen('commit'))); 344 } 345 else if(strpos($line, 'Author')===0){ 346 $commit['author'] = trim(substr($line, strlen('Author:'))); 347 } 348 else if(strpos($line, 'Date')===0){ 349 $commit['date'] = trim(substr($line, strlen('Date:'))); 350 } 351 else{ 352 if(isset($commit['message'])) 353 $commit['message'] .= trim($line); 354 else 355 $commit['message'] = trim($line); 356 } 357 } 358 if(!empty($commit)) { 359 array_push($history, $commit); 360 } 361 362 return $history; 363 } 364 365 366 /** 367 * Returns the names of the files that have changed in a commit 368 * 369 * @access public 370 * @param string hash to get the changes for 371 * @return string 372 */ 373 public function get_files_by_commit($hash) { 374 return $this->run("diff-tree -r --name-status --no-commit-id ".$hash); 375 } 376 377 /** 378 * Runs a `git commit` call 379 * 380 * Accepts a commit message string 381 * 382 * @access public 383 * @param string commit message 384 * @return string 385 */ 386 public function commit($message = "blank") { 387 try { 388 $cmd = "gc"; 389 $fullcmd = "cd \"".$this->repo_path."\" && ".$this->git_path." ".$cmd; 390 $this->run_command($fullcmd); 391 392 $cmd = "prune"; 393 $fullcmd = "cd \"".$this->repo_path."\" && ".$this->git_path." ".$cmd; 394 $this->run_command($fullcmd); 395 396 $cmd = "add . -A"; 397 $fullcmd = "cd \"".$this->repo_path."\" && ".$this->git_path." ".$cmd; 398 $this->run_command($fullcmd); 399 400 $cmd = "commit -a -m \"".$message."\""; 401 $fullcmd = "cd \"".$this->repo_path."\" && ".$this->git_path." ".$cmd; 402 $this->run_command($fullcmd); 403 return true; 404 } 405 Catch (Exception $e) 406 { 407 msg($e->getMessage()); 408 return false; 409 } 410 } 411 412 /** 413 * Runs a `git clone` call to clone the current repository 414 * into a different directory 415 * 416 * Accepts a target directory 417 * 418 * @access public 419 * @param string target directory 420 * @return string 421 */ 422 public function clone_to($target) { 423 return $this->run("clone --local ".$this->repo_path." $target"); 424 } 425 426 /** 427 * Runs a `git clone` call to clone a different repository 428 * into the current repository 429 * 430 * Accepts a source directory 431 * 432 * @access public 433 * @param string source directory 434 * @return string 435 */ 436 public function clone_from($source) { 437 438 try 439 { 440 $cmd = "clone -q $source \"".$this->repo_path."\""; 441 $fullcmd = "cd \"".$this->repo_path."\" && ".$this->git_path." ".$cmd; 442 // msg('Full command: '.$fullcmd); 443 $this->run_command($fullcmd); 444 } 445 Catch (Exception $e) 446 { 447 msg($e->getMessage()); 448 } 449 } 450 451 /** 452 * Runs a `git clone` call to clone a remote repository 453 * into the current repository 454 * 455 * Accepts a source url 456 * 457 * @access public 458 * @param string source url 459 * @return string 460 */ 461 public function clone_remote($source) { 462 return $this->run("clone $source ".$this->repo_path); 463 } 464 465 /** 466 * Runs a `git clean` call 467 * 468 * Accepts a remove directories flag 469 * 470 * @access public 471 * @param bool delete directories? 472 * @return string 473 */ 474 public function clean($dirs = false) { 475 return $this->run("clean".(($dirs) ? " -d" : "")); 476 } 477 478 /** 479 * Runs a `git branch` call 480 * 481 * Accepts a name for the branch 482 * 483 * @access public 484 * @param string branch name 485 * @return string 486 */ 487 public function create_branch($branch) { 488 return $this->run("branch $branch"); 489 } 490 491 /** 492 * Runs a `git branch -[d|D]` call 493 * 494 * Accepts a name for the branch 495 * 496 * @access public 497 * @param string branch name 498 * @return string 499 */ 500 public function delete_branch($branch, $force = false) { 501 return $this->run("branch ".(($force) ? '-D' : '-d')." $branch"); 502 } 503 504 /** 505 * Runs a `git branch` call 506 * 507 * @access public 508 * @param bool keep asterisk mark on active branch 509 * @return array 510 */ 511 public function list_branches($keep_asterisk = false) { 512 $branchArray = explode("\n", $this->run("branch")); 513 foreach($branchArray as $i => &$branch) { 514 $branch = trim($branch); 515 if (! $keep_asterisk) 516 $branch = str_replace("* ", "", $branch); 517 if ($branch == "") 518 unset($branchArray[$i]); 519 } 520 return $branchArray; 521 } 522 523 /** 524 * Returns name of active branch 525 * 526 * @access public 527 * @param bool keep asterisk mark on branch name 528 * @return string 529 */ 530 public function active_branch($keep_asterisk = false) { 531 $branchArray = $this->list_branches(true); 532 $active_branch = preg_grep("/^\*/", $branchArray); 533 reset($active_branch); 534 if ($keep_asterisk) 535 return current($active_branch); 536 else 537 return str_replace("* ", "", current($active_branch)); 538 } 539 540 /** 541 * Runs a `git checkout` call 542 * 543 * Accepts a name for the branch 544 * 545 * @access public 546 * @param string branch name 547 * @return string 548 */ 549 public function checkout($branch) { 550 return $this->run("checkout $branch"); 551 } 552 553 554 /** 555 * Runs a `git merge` call 556 * 557 * Accepts a name for the branch to be merged 558 * 559 * @access public 560 * @param string $branch 561 * @return string 562 */ 563 public function merge($branch, $msg = "") 564 { 565 if ($msg == "") return $this->run("merge $branch --no-ff"); 566 return $this->run("merge $branch --no-ff -m ".$msg); 567 } 568 569 /** 570 * Runs a `git reset` call 571 * 572 * Reverts the last commit, leaving the local files intact 573 * 574 * @access public 575 * @return string 576 */ 577 public function revertLastCommit() 578 { 579 return $this->run("reset --soft HEAD~1"); 580 } 581 582 583 /** 584 * Runs a git fetch on the current branch 585 * 586 * @access public 587 * @return string 588 */ 589 public function fetch() 590 { 591 return $this->run("fetch"); 592 } 593 594 /** 595 * Tests whether origin points to a valid repo 596 * 597 * @access public 598 * @return string 599 */ 600 public function test_origin() 601 { 602 try 603 { 604 $this->run("fetch --dry-run"); 605 return true; 606 } 607 catch (Exception $e) 608 { 609 return false; 610 } 611 } 612 613 614 /** 615 * Add a new tag on the current position 616 * 617 * Accepts the name for the tag and the message 618 * 619 * @param string $tag 620 * @param string $message 621 * @return string 622 */ 623 public function add_tag($tag, $message = null) 624 { 625 if ($message === null) { 626 $message = $tag; 627 } 628 return $this->run("tag -a $tag -m $message"); 629 } 630 631 632 /** 633 * Push specific branch to a remote 634 * 635 * @return string 636 */ 637 public function push() 638 { 639 $cmd = 'push'; 640 return $this->run($cmd); 641 } 642 643 /** 644 * Pull specific branch from remote 645 * 646 * Accepts the name of the remote and local branch 647 * 648 * @param string $remote 649 * @param string $branch 650 * @return string 651 */ 652 public function pull($remote, $branch) 653 { 654 return $this->run("pull $remote $branch"); 655 } 656 657 /** 658 * Sets the project description. 659 * 660 * @param string $new 661 */ 662 public function set_description($new) 663 { 664 file_put_contents($this->repo_path."/.git/description", $new); 665 } 666 667 /** 668 * Gets the project description. 669 * 670 * @return string 671 */ 672 public function get_description() 673 { 674 return file_get_contents($this->repo_path."/.git/description"); 675 } 676} 677 678/* End Of File */ 679