GIT repositories

Index page of all the GIT repositories that are clonable form this server via HTTPS. Übersichtsseite aller GIT-Repositories, die von diesem Server aus über git clone (HTTPS) erreichbar sind.

Services

A bunch of service scripts to convert, analyse and generate data. Ein paar Services zum Konvertieren, Analysieren und Generieren von Daten.

GNU octave web interface

A web interface for GNU Octave, which allows to run scientific calculations from netbooks, tables or smartphones. The interface provides a web form generator for Octave script parameters with pre-validation, automatic script list generation, as well presenting of output text, figures and files in a output HTML page. Ein Webinterface für GNU-Octave, mit dem wissenschaftliche Berechnungen von Netbooks, Tablets oder Smartphones aus durchgeführt werden können. Die Schnittstelle beinhaltet einen Formulargenerator für Octave-Scriptparameter, mit Einheiten und Einfabevalidierung. Textausgabe, Abbildungen und generierte Dateien werden abgefangen und in einer HTML-Seite dem Nutzer als Ergebnis zur Verfügung gestellt.

GIT-Project Klasse

GIT project handling class

I implemented this GIT class originally to manage gitosis/gitolite reporitories, and nightly build purposes. The example shows you the main features, in short:

  • Inspect a local repository directly
  • Clone a remote or local directory
  • Get log, commits, tags, the tree, branches etc.
  • Commit, tag, checkout, push (pull not yet)

Diese Klasse habe ich ursprünglich für Nightly Builds und gitosis/gitolite Repository Management erstellt. Das Beispiel zeigt die Hauptfunktionalitäten, in aller Kürze:

  • Ein lokales Repository direkt inspizieren
  • Ein lokales oder entferntes Repository "clonen".
  • Getters für Log, Commits, Tags, "Trees", "Branches" usw.
  • Commit, Tag, Checkout, Push (allerdings noch kein Pull)

Example source code

Anwendungsbeispiel

<?php
require_once('swlib/swlib.class.php');
use sw\GitProject as git;
 
/**
 * First thing: configure (or let it use the defaults). You can ommit configuration
 * keys as well to leave the defaults.
 */
git::config(array(
  // default is 'git'. You can use any other path as well
  'git-bin' => 'git',
  // The default directory where to get local projects from
  'local-repository-base-dir' => '/tmp/git/repositories',
  // You can define your own file format, this is the default:
  'time-format' => '%Y-%m-%d %H:%M:%S',
  // Data for committing using this class. Null means "Derive it from the
  // Apache server data."
  'git-config-committer-name' => null,
  'git-config-committer-email' => null,
  'git-config-author-name' => null,
  'git-config-author-email' => null,
  // SSH binary for SSH connections NULL = "search it yourself"
  'git-config-ssh-bin' => null,
));
 
 
/**
 *
 * Cloned remote: Clone, get some infos, modify, commit, tag, push:
 *
 */
print "\n\n------------------------------------------------------------------";
print "\n--- CLONE, MODIFY, COMMIT, TAG, PUSH";
print "\n------------------------------------------------------------------\n\n";
 
$git = git::remoteRepositoryClone('/tmp/git/repositories/libeemc.git', '/tmp/libeemc');
print "Cloned '/tmp/git/repositories/libeemc.git' to '/tmp/libeemc'\n\n";
 
$r = $git->getBranches();
print "Branches: " . print_r($r, true) . "\n";
 
$r = $git->getTags();
print "Tags: " . print_r($r, true) . "\n";
 
$r = $git->getTreeList();
print "Tree list: " . print_r($r, true) . "\n";
 
// COMMIT SOMETHING AND PUSH BACK
@file_put_contents('/tmp/libeemc/VERSION', '1.0.0', LOCK_EX);
$git->commit('Set VERSION to 1.0.0', true);
$git->tag('v1.0.0');
$git->push();
 
$r = $git->getTags();
print "Tags after adding a tag: " . print_r($r, true) . "\n";
 
$r = $git->log();
print "LOG: " . print_r($r, true) . "\n";
 
 
/**
 *
 * Repository inspection. Not all methods are allowed, so the class (or git)
 * will complain if you do something you shouldn't.
 *
 */
print "\n\n------------------------------------------------------------------";
print "\n--- REPOSITORY INSPECTION";
print "\n------------------------------------------------------------------\n\n";
 
 
// Full path also possible: '/tmp/git/repositories/gitolite-admin.git'
$git = git::localRepository('swlib-php.git');
 
// Heads
$r = $git->getHeads();
print "Heads: " . print_r($r, true) . "\n" ;
 
// Commit info of tag or hash, default is HEAD
$r = $git->getCommitInfo();
print "Commit info: " . print_r($r, true) . "\n" ;

Output

Ausgabe

$ php git.test.php
 
------------------------------------------------------------------
--- CLONE, MODIFY, COMMIT, TAG, PUSH
------------------------------------------------------------------
 
Cloned '/tmp/git/repositories/libeemc.git' to '/tmp/libeemc'
 
Branches: Array
(
    [0] => master
)
 
Tags: Array
(
)
 
Tree list: Array
(
    [efe4aad6fad506462437220c455c301f5ac02658] => Array
        (
            [sha] => efe4aad6fad506462437220c455c301f5ac02658
            [path] => .gitignore
            [type] => blob
            [mode] => 100644
        )
 
    [ec9de690e53a4d149efe22428359088b44605367] => Array
        (
            [sha] => ec9de690e53a4d149efe22428359088b44605367
            [path] => Makefile
            [type] => blob
            [mode] => 100644
        )
 
    [e52ae9b3c2641eabd66b2c871ba456b9f4cafaa3] => Array
        (
            [sha] => e52ae9b3c2641eabd66b2c871ba456b9f4cafaa3
            [path] => conf
            [type] => tree
            [mode] => 040000
        )
 
    [4b087f9308a6cebf26d2cfd1ad62bc99e5d7b14a] => Array
        (
            [sha] => 4b087f9308a6cebf26d2cfd1ad62bc99e5d7b14a
            [path] => include
            [type] => tree
            [mode] => 040000
        )
 
    [b384c297b366dadaa61e78370548e059a2ca8712] => Array
        (
            [sha] => b384c297b366dadaa61e78370548e059a2ca8712
            [path] => nbproject
            [type] => tree
            [mode] => 040000
        )
 
    [0fde5b58d0bd72184407a20ffc25d7f682576212] => Array
        (
            [sha] => 0fde5b58d0bd72184407a20ffc25d7f682576212
            [path] => src
            [type] => tree
            [mode] => 040000
        )
)
 
Tags after adding a tag: Array
(
    [0] => Array
        (
            [hash] => 61f7d1e70715df318df3e591e87aa10ce3f33423
            [name] => v1.0.0
        )
)
 
LOG: Array
(
    [0] => Array
        (
            [commit] => 61f7d1e70715df318df3e591e87aa10ce3f33423
            [author] => Server <admin@my-server>
            [date] => Mon Sep 2 15:12:35 2013 +0200
            [comment] => Set VERSION to 1.0.0
        )
 
    [1] => Array
        (
            [commit] => 9a0e53f2642480dbc71706ff2774a4eb9f60b3e8
            [author] => stfwi <[email.replaced]>
            [date] => Mon Aug 5 17:06:59 2013 +0200
            [comment] => ...
        )
 
    [2] => Array
        (
            [commit] => 2e995d1dcdf8df6fb487e5ac11dcc32fe810a77a
            [author] => stfwi <[email.replaced]>
            [date] => Thu Aug 1 12:29:26 2013 +0200
            [comment] => adapted .gitignore
        )
 
    [3] => Array
        (
            [commit] => c2d4b85514e9734ba9ff1e580e4b67180c68b933
            [author] => stfwi <[email.replaced]>
            [date] => Thu Aug 1 12:18:20 2013 +0200
            [comment] => Init
        )
)
 
------------------------------------------------------------------
--- REPOSITORY INSPECTION
------------------------------------------------------------------
 
Heads: Array
(
    [0] => Array
        (
            [hash] => 0ae932874559f3230fcafa3ca74c63a4fd582375
            [name] => localize
        )
 
    [1] => Array
        (
            [hash] => b5faf7a4115e8494c7e2fc678d8577f6c51fc389
            [name] => master
        )
)
 
Commit info: Array
(
    [commit] => HEAD
    [file] =>
    [hash] => b5faf7a4115e8494c7e2fc678d8577f6c51fc389
    [parents] => Array
        (
            [c4e51c1142c9d69e39c9122a8479b7ecb94a34d7] => c4e51c1142c9d69e39c9122a8479b7ecb94a34d7
        )
 
    [trees] => Array
        (
            [48417baae32e32061f77b057809e86d59ffbca1a] => 48417baae32e32061f77b057809e86d59ffbca1a
        )
 
    [author] => Array
        (
            [name] => stfwi
            [email] => [email.replaced]
            [time] => 2013-09-02 10:21:28
            [utc] =>
        )
 
    [committer] => Array
        (
            [name] => stfwi
            [email] => [email.replaced]
            [time] => 2013-09-02 10:21:28
            [utc] =>
        )
 
    [message] => ...
)

Class source code

Klassen-Quelltext

<?php
 
/**
 * Enables git operation in the local file system. The class is derived from the
 * functions.php file of the viewgit project.
 *
 * @gpackage de.atwillys.sw.php.swLib
 * @author Stefan Wilhelm
 * @copyright Stefan Wilhelm, 2005-2011
 * @license GPL2
 * @version 1.0
 * @uses Tracer
 * @uses GitProjectException
 */
 
namespace sw;
 
class GitProject {
 
  /**
   * Class configuration
   * @var array
   */
  private static $config = array(
      'git-bin' => 'git',
      'local-repository-base-dir' => '/home/gitolite/repositories',
      'time-format' => '%Y-%m-%d %H:%M:%S',
      'git-config-committer-name' => null,
      'git-config-committer-email' => null,
      'git-config-author-name' => null,
      'git-config-author-email' => null,
      'git-config-ssh-bin' => null,
  );
 
  /**
   * The project name (basename of the repository path without '.git')
   * Changed when set getRepositoryPath() is called.
   * @var string
   */
  protected $projectName = '';
 
  /**
   * Contains the project description
   * @var string
   */
  protected $projectDescription = '';
 
  /**
   * Directory of the referred repository
   * @var string
   */
  protected $projectPath = '';
 
  /**
   * Defines if the project location is a gitosis repository path (true) or
   * if the location referrs to a cloned project.
   * @var bool
   */
  protected $isRepository = false;
 
  /**
   * Contains the repository location of which the project is cloned form.
   * @var string
   */
  protected $clonedFrom = '';
 
  /**
   * Contains the heads
   * @var array
   */
  protected $heads = array();
 
  /**
   * Contains the defined tags
   * @var array
   */
  protected $tags = array();
 
  /**
   * Contains existing branches
   * @var array
   */
  protected $branches = array();
 
  /**
   * Contains the active branch
   * @var string
   */
  protected $activeBranch = null;
 
  /**
   * Sets/returns the class configuration
   * @param array $config=null
   * @return array
   */
  public static function config($config=null) {
    if (!is_null($config)) {
      if (!is_array($config)) {
        throw new GitProjectException('Configuration must be an assoc. array.');
      } else {
        self::$config = array_merge(self::$config, $config);
      }
      $default_name = "Server: {$_SERVER['HTTP_HOST']}";
      $default_email = "no-email@{$_SERVER['HTTP_HOST']}";
      if (empty(self::$config['git-config-committer-name'])) {
        self::$config['git-config-committer-name'] = $default_name;
      }
      if (empty(self::$config['git-config-committer-email'])) {
        self::$config['git-config-committer-email'] = $default_email;
      }
      if (empty(self::$config['git-config-author-name'])) {
        self::$config['git-config-author-name'] = $default_name;
      }
      if (empty(self::$config['git-config-author-email'])) {
        self::$config['git-config-author-email'] = $default_email;
      }
      if (empty(self::$config['git-config-ssh-bin'])) {
        $ssh = trim(shell_exec('which ssh'), "\t\n\r ");
        if (empty($ssh)) {
          $ssh = 'ssh';
          Tracer::trace('Could not automatically get the ssh client using "which ssh"', 2);
        }
        self::$config['git-config-ssh-bin'] = $ssh;
      }
      putenv('GIT_COMMITTER_NAME=' . escapeshellarg(self::$config['git-config-committer-name']));
      putenv('GIT_COMMITTER_EMAIL=' . escapeshellarg(self::$config['git-config-committer-email']));
      putenv('GIT_AUTHOR_NAME=' . escapeshellarg(self::$config['git-config-author-name']));
      putenv('GIT_AUTHOR_EMAIL=' . escapeshellarg(self::$config['git-config-author-email']));
      putenv('GIT_SSH=' . self::$config['git-config-ssh-bin']);
    }
    return self::$config;
  }
 
  /**
   * Returns a GitProject object referred to the given $repository name.
   * @param string $repository
   * @return GitProject
   */
  public static function localRepository($repository) {
    $o = new self();
    $o->isRepository = true;
    if (stripos($repository, self::$config['local-repository-base-dir']) === false && strtolower(FileSystem::getExtension($repository)) == 'git' && stripos($repository, '/.git') === false) {
      $repository = str_replace('//', '/', self::$config['local-repository-base-dir'] . '/' . trim(str_ireplace('.git', '', $repository), ' /') . '.git');
    }
    $o->setPath($repository);
    return $o;
  }
 
  /**
   * Clone a project from a remote ssh repository
   * @param string $repository
   * @param string $destinationDirectory
   * @param string $sshKeyFile
   * @return GitProject
   */
  public static function remoteRepositoryClone($repository, $destinationDirectory) {
    $o = new self();
    $return = array();
    $code = 0;
    $command = " clone --no-hardlinks " . escapeshellarg($repository) . ' ' . escapeshellarg($destinationDirectory);
    $cmd = self::$config['git-bin'] . "$command 2>&1";
    Tracer::trace("GIT command = $cmd");
    $t = ini_get('max_execution_time');
    set_time_limit(300);
    exec($cmd, $return, $code);
    set_time_limit($t);
    $o->setPath($destinationDirectory);
    return $o;
  }
 
  /**
   * Constructor
   * @param string $path
   * @return GitProject
   */
  public function __construct($path='') {
    if (!empty($path)) {
      $this->setPath($path);
    }
  }
 
  /**
   * Executes Git in the repository directory
   * @param string $command
   * @param bool $addGitDir=true
   * @param bool $addWorkTree=true
   * @param bool $includeExitCode=false
   * @return array
   */
  protected function runGit($command, $addGitDir=null, $addWorkTree=null, $includeExitCode=null) {
    if (empty($this->projectPath)) {
      throw new GitProjectException("Cannot run GIT without a repository path specified before");
    }
 
    $addGitDir = $addGitDir === null ? true : $addGitDir;
    $addWorkTree = $addWorkTree === null ? true : $addWorkTree;
    $includeExitCode = $includeExitCode === null ? false : $includeExitCode;
 
    $return = array();
    $code = 0;
    $addGitDir = $addGitDir ? (' --git-dir=' . escapeshellarg($this->projectPath . ($this->isRepository ? '' : '/.git'))) : '';
    $workTree = $addWorkTree ? ($this->isRepository ? '' : ' --work-tree=' . escapeshellarg($this->projectPath)) : '';
    $cmd = self::$config['git-bin'] . "$addGitDir $workTree $command";
    Tracer::trace("GIT command = $cmd 2>&1");
    $t = ini_get('max_execution_time');
    set_time_limit(300);
    exec("$cmd 2>&1", $return, $code);
    set_time_limit($t);
    while (!empty($return) && strlen(trim(reset($return), "\r\t ")) == 0) {
      array_shift($return);
    }
    while (!empty($return) && strlen(trim(end($return), "\r\t ")) == 0) {
      array_pop($return);
    }
    if ($code !== 0) {
      Tracer::trace("GIT exit code $code, stdout:" . implode("\n", $return));
    }
    if ($includeExitCode) {
      return array('exitcode' => $code, 'output' => $return);
    } else {
      return $return;
    }
  }
 
  /**
   * The project name (basename of the repository path without '.git') Changed
   * when set getRepositoryPath() is called.
   * @return string
   */
  public function getProjectName() {
    return $this->projectName;
  }
 
  /**
   * Returns the description file contents (if thie file exists) or ""
   * @return string
   */
  public function getProjectDescription() {
    return $this->projectDescription;
  }
 
  /**
   * Returns the repository path
   * @return string
   */
  public function getPath() {
    return $this->projectPath;
  }
 
  /**
   * Sets the project path. If this path ends with ".git", the methods assumes
   * the project location is in the in the repository. This means only read
   * operations are allowed.
   * @param string $path
   * @return GitProject
   */
  public function setPath($path) {
    $path = rtrim($path, ' /');
    if (!FileSystem::isDirectory($path)) {
      throw new GitProjectException("The directory !path does not exist.", array('!path' => $path));
    } else if (!FileSystem::isDirectory($path)) {
      throw new GitProjectException("The directory !path is not readable for you.", array('!path' => $path));
    } else if (stripos($path, self::$config['local-repository-base-dir']) !== false) {
      $this->isRepository = true;
    } else if (!FileSystem::isDirectory($path . '/.git')) {
      throw new GitProjectException("The path !path does not contain a .git directory.", array('!path' => $path));
    }
    $this->projectPath = '';
    $this->projectDescription = '';
    if (!FileSystem::isDirectory($path)) {
      throw new GitProjectException("The repository path does not exist: :path", array(':path' => $path));
    } else if (!FileSystem::isReadable($path) || !FileSystem::isExecutable($path)) {
      throw new GitProjectException("The repository path is not readable for you: :path", array(':path' => $path));
    } else if (stripos(FileSystem::getBasename($path), 'gitosis-admin.git') !== false && $this->isRepository) {
      throw new GitProjectException("The repository path does not exist :-) :path", array(':path' => $path));
    } else {
      $this->projectPath = $path;
      $this->projectName = FileSystem::getBasename($path, '.git');
      if (FileSystem::isFile($this->projectPath . '/description')) {
        $this->projectDescription = trim(FileSystem::readFile($this->projectPath . '/description'), "\n\r\t ");
      } else if (FileSystem::isFile($this->projectPath . '/.git/description')) {
        $this->projectDescription = trim(FileSystem::readFile($this->projectPath . '/.git/description'), "\n\r\t ");
      }
    }
    return $this;
  }
 
  /**
   * Returns if the project located in the server repository project
   * @return bool
   */
  public function isRepository() {
    return $this->isRepository;
  }
 
  /**
   * Returns the branch heads as array
   * @return array
   */
  public function getHeads() {
    if (!empty($this->heads)) {
      return $this->heads;
    }
    $this->heads = array();
    foreach ($this->runGit('show-ref --heads') as $line) {
      list($hash, $line) = explode(' ', $line, 2);
      $line = explode('/', $line);
      $this->heads[] = array(
          'hash' => $hash,
          'name' => array_pop($line)
      );
      if (($line = implode('/', $line)) != 'refs/heads') {
        Tracer::trace("Warning: Head $hash: path does not start with 'refs/heads', instad with $line");
      }
    }
    return $this->heads;
  }
 
  /**
   * Returns the set tags as array
   * @return array
   */
  public function getTags() {
    if (!empty($this->tags)) {
      return $this->tags;
    }
    $this->tags = array();
    foreach ($this->runGit('show-ref --tags') as $line) {
      list($hash, $line) = explode(' ', $line, 2);
      $line = explode('/', $line);
      $this->tags[] = array(
          'hash' => $hash,
          'name' => array_pop($line)
      );
      if (($line = implode('/', $line)) != 'refs/tags') {
        Tracer::trace("Warning: Tag $hash: tag path does not start with 'refs/tags', instad with $line");
      }
    }
    return $this->tags;
  }
 
  /**
   * Returns the branches
   * @return array
   */
  public function getBranches() {
    if ($this->isRepository) {
      throw new GitProjectException("You cannot get or switch branches in a repository, but you can use getHeads() instead");
    }
    $this->activeBranch = '';
    if (empty($this->branches)) {
      $this->branches = array();
      foreach ($this->runGit('branch') as $v) {
        $v = trim($v);
        if (!empty($v)) {
          if (ltrim($v, '* ') != $v) {
            $this->activeBranch = ltrim($v, '* ');
            $this->branches[] = $this->activeBranch;
          } else {
            $this->branches[] = $v;
          }
        }
      }
    }
    return $this->branches;
  }
 
  /**
   * Returns the name of the active branch or empty if not on a branch
   * @return string
   */
  public function getActiveBranch() {
    if (is_null($this->activeBranch)) {
      $this->getBranches();
    }
    return $this->activeBranch;
  }
 
  /**
   * Returns information about a commit as associative array
   * @param string $commit
   * @param string $file
   * @return array
   */
  public function getCommitInfo($commit='HEAD', $file='') {
    $return = array(
        'commit' => $commit,
        'file' => $file,
        'hash' => '',
        'parents' => array(),
        'trees' => array(),
        'author' => array(),
        'committer' => array(),
        'message' => ''
    );
 
    $commit = escapeshellarg($commit);
    $file = empty($file) ? '' : ('-- ' . escapeshellarg($file));
    $lines = $this->runGit("rev-list --header --max-count=1 $commit $file");
    $return['hash'] = array_shift($lines);
 
    while (!empty($lines)) {
      list($key, $line) = explode(' ', trim(array_shift($lines)), 2);
      if (empty($key)) {
        // Rest is message
        while (!empty($lines)) {
          $return['message'] .= trim(array_shift($lines)) . "\n";
        }
        break;
      } else if ($key == 'tree' || $key == 'parent') {
        $return["{$key}s"][$line] = $line;
      } else if ($key == 'committer' || $key == 'author') {
        list($name, $line) = explode('<', $line, 2);
        $line = explode(' ', $line);
        $email = trim(array_shift($line), ' >');
        $time = gmstrftime(self::$config['time-format'], array_shift($line));
        $offs = array_shift($line);
        $utc = gmstrftime(self::$config['time-format'], trim(implode(' ', $line)));
        $return[$key] = array(
            'name' => $name,
            'email' => $email,
            'time' => $time,
            'utc' => $utc
        );
      }
    }
    return $return;
  }
 
  /**
   * Returns the 'git describe' result, which is the last tag it can find before
   * this commit was committed. Returns empty string if no tag was found. The
   * $whichMatchEreg is a bash EREG match, only those tags are returned if the
   * argument is not empty.
   * @param string $commit
   * @return string
   */
  public function getLastReachableTagOfCommit($commit='HEAD', $whichMatchEreg='') {
    if (!empty($whichMatchEreg)) {
      $whichMatchEreg = '--match=' . escapeshellarg($whichMatchEreg) . ' ';
    }
    $r = $this->runGit("describe --abbrev=0 $whichMatchEreg" . escapeshellarg($commit));
    $r = trim(reset($r));
    if(stripos($r, 'No names found') !== false) $r = '';
    return $r;
  }
 
  /**
   * Returns the file tree of a tag, commit or tree hash
   * @param array $tree
   * @return array
   */
  public function getTreeList($tree='HEAD', $recursive=false) {
    $return = array();
    foreach ($this->runGit('ls-tree ' . ($recursive ? '-r ' : '') . escapeshellarg($tree)) as $v) {
      list($mode, $type, $sha, $path) = preg_split('/[\s]+/', $v, 4);
      $return[$sha] = array(
        'sha' => $sha,
        'path' => $path,
        'type' => $type,
        'mode' => $mode
      );
    }
    return $return;
  }
 
  /**
   * Clones a repository to a target directory
   * @param string $destinationDirectory
   * @return GitProject
   */
  public function cloneTo($destinationDirectory) {
    if (empty($this->projectPath)) {
      throw new GitProjectException("Cannot run GIT without a repository path specified before");
    } else if (!FileSystem::isDirectory($destinationDirectory)) {
      throw new GitProjectException("Destination directory does not exist");
    } else if (!$this->isRepository) {
      throw new GitProjectException("Source project is not a repository project and cannot be cloned");
    }
    if (FileSystem::getBasename($destinationDirectory) != $this->projectName) {
      $destinationDirectory = rtrim($destinationDirectory, '/') . '/' . $this->projectName;
    }
    $this->runGit("clone --no-hardlinks " . escapeshellarg($this->projectPath) . ' ' . escapeshellarg($destinationDirectory), false, false);
    $o = new self($destinationDirectory);
    $o->clonedFrom = $this->projectPath;
    return $o;
  }
 
  /**
   * Export into a tar/zip archive
   * @param string $archiveFile
   * @param string $tag
   * @param string $format="tar"
   */
  public function archiveTo($archiveFile, $tag, $format='tar') {
    if (empty($this->projectPath)) {
      throw new GitProjectException("Cannot run GIT without a repository path specified before");
    } else if (!FileSystem::isDirectory(FileSystem::getDirname($archiveFile))) {
      throw new GitProjectException("Destination file parent directory does not exist");
    } else if (empty($tag)) {
      throw new GitProjectException("No tag given to export");
    } else if ($format != 'tar' && $format != 'zip' && $format != 'tar.gz' && $format != 'tgz') {
      throw new GitProjectException("Unsupported archive format ':format', must be 'tar', 'tar.gz' or 'zip'", array(':format' => $format));
    }
    if ($format == 'tar.gz' || $format == 'tgz') {
      $gz = true;
      $format = 'tar';
    }
    if (strtolower(FileSystem::getExtension($archiveFile)) != strtolower($format)) {
      $archiveFile = "$archiveFile.$format";
    }
    $this->runGit("archive --format=$format --remote=" . escapeshellarg('file://' . $this->projectPath) . ' ' . escapeshellarg($tag) . " --output=" . escapeshellarg($archiveFile), false, false);
    if (!FileSystem::isFile($archiveFile)) {
      throw new GitProjectException("Failed to create archive file ':file'", array(':file' => $archiveFile));
    }
    if (isset($gz)) {
      print_r(exec("gzip -9 " . escapeshellarg($archiveFile)));
      $archiveFile = "$archiveFile.gz";
      if (!FileSystem::isFile($archiveFile)) {
        throw new GitProjectException("Failed to create archive file ':file'", array(':file' => $archiveFile));
      }
    }
    return $archiveFile;
  }
 
  /**
   * Checks out a branch (or given by tag or hash)
   * @param string $tag
   * @return GitProject
   */
  public function checkout($branch) {
    if ($this->isRepository) {
      throw new GitProjectException("You cannot checkout a branch in the repository");
    }
    $r = $this->runGit("checkout " . escapeshellarg($branch), null, null, true);
    if ($r['exitcode'] != 0) {
      Tracer::trace_r($r, 'git checkout ' . escapeshellarg($branch));
      throw new GitProjectException('Failed to checkout branch "!branch"', array('!branch' => $branch));
    }
    $this->branches = array();
    return $this;
  }
 
  /**
   * Commits changes
   * @param string $message
   * @param bool $addNewFiles
   * @return GitProject
   */
  public function commit($message, $addNewFiles=true) {
    $message = trim(str_replace(array("'", '"'), '', $message), "\n\t\r ");
    if ($this->isRepository) {
      throw new GitProjectException('You cannot commit changes to a project located in the repository; clone it commit and push it back.');
    } else if (empty($message)) {
      throw new GitProjectException('You must commit changes with a message');
    }
    if ($addNewFiles) {
      $r = $this->runGit('add .', null, null, true);
      if ($r['exitcode'] != 0) {
        Tracer::trace_r($r, 'git add .');
        if (empty($r['output'])) {
          throw new GitProjectException('Failed to add files to during the commit process.');
        } else {
          throw new GitProjectException('Failed to add files to during the commit process, git says: !error', array('!error' => trim(implode("\n", $r['output'], "\n"))));
        }
      }
    }
    $r = $this->runGit('commit -m ' . escapeshellarg($message), null, null, true);
    if ($r['exitcode'] != 0) {
      Tracer::trace_r($r, 'git commit -m ' . escapeshellarg($message));
      if (empty($r['output'])) {
        throw new GitProjectException('Failed to commit changes');
      } else {
        throw new GitProjectException('Failed to commit, git says: "!error"', array('!error' => end($r['output'])));
      }
    }
    return $this;
  }
 
  /**
   * Pushes the actual branch up to the repository (using "git push '<remote path>' '<active branch>'")
   *
   * @param $remote = null
   * @param $branch = null
   * @return GitProject
   */
  public function push($remote=null, $branch=null) {
    if ($this->isRepository) {
      throw new GitProjectException('You cannot push a project located in the repository');
    }
    $branch = strlen(trim($branch)) > 0 ? $branch : $this->getActiveBranch();
    $remote = strlen(trim($remote)) > 0 ? $branch : $this->getPath();
    $r = $this->runGit("push '$remote' '$branch'", null, null, true);
    if ($r['exitcode'] != 0) {
      Tracer::trace_r($r, 'git push');
      if (!empty($this->clonedFrom) && !FileSystem::isWritable($this->clonedFrom)) {
        throw new GitProjectException('Pushing changes to the repository failed: Repository directory of which the project is cloned from is not writable.');
      } else if (empty($r['output'])) {
        throw new GitProjectException('Pushing changes to the repository failed');
      } else {
        throw new GitProjectException('Pushing changes to the repository failed, git says: !error', array('!error' => trim(implode("\n", $r['output'], "\n"))));
      }
    }
    return $this;
  }
 
  /**
   * Adds the tag $tagname to the actual commit.
   *
   * @param string $tagname
   * @return GitProject
   */
  public function tag($tagname) {
    if ($this->isRepository) {
      throw new GitProjectException('You cannot tag in the repository');
    } else if(strlen($tagname=trim($tagname)) == 0) {
      throw new GitProjectException('No tag name given');
    }
    $r = $this->runGit("tag '$tagname'", null, null, true);
    if ($r['exitcode'] != 0) {
      Tracer::trace_r($r, 'git tag');
      throw new GitProjectException('Failed to tag, git says: !error', array('!error' => trim(implode("\n", $r['output'], "\n"))));
    }
    return $this;
  }
 
  /**
   * Returns the commit log of cloned projects
   * @param int $numLastCommits=null
   * @return array
   */
  public function log($numLastCommits=null) {
    if ($this->isRepository) {
      throw new GitProjectException('The git log command is only valid in cloned projects');
    }
    $numLastCommits = (is_numeric($numLastCommits) && intval($numLastCommits) > 0) ? (' -' . intval($numLastCommits)) : '';
    $r = $this->runGit("log $numLastCommits", null, null, true);
    if ($r['exitcode'] != 0) {
      if (empty($r['output'])) {
        throw new GitProjectException('Getting the log failed: !error', array('!error' => trim(implode("\n", $r['output'], "\n"))));
      } else {
        throw new GitProjectException('Getting the log failed');
      }
    }
    $log = $matches = $commit = array();
    while (!empty($r['output'])) {
      $line = array_shift($r['output']);
      if (preg_match('/^commit[\s]([0-9a-f]+)/i', $line, $matches)) {
        $log[] = array();
        end($log);
        $commit = &$log[key($log)];
        $commit['commit'] = end($matches);
      } else if (preg_match('/^author:[\s](.+)/i', $line, $matches)) {
        $commit['author'] = trim(end($matches));
      } else if (preg_match('/^date:[\s](.+)/i', $line, $matches)) {
        $commit['date'] = trim(end($matches));
      } else if (preg_match('/^[\s](.+)/i', $line, $matches)) {
        if (!isset($commit['comment'])) {
          $commit['comment'] = trim(end($matches), " \t\n\r");
        } else {
          $commit['comment'] .= ' ' . trim(end($matches), " \t\n\r");
        }
      }
    }
    return $log;
  }
}