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