<?php␊ |
/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */␊ |
/*␊ |
# ***** BEGIN LICENSE BLOCK *****␊ |
# This file is part of InDefero, an open source project management application.␊ |
# Copyright (C) 2008 CĂ©ondo Ltd and contributors.␊ |
#␊ |
# InDefero is free software; you can redistribute it and/or modify␊ |
# it under the terms of the GNU General Public License as published by␊ |
# the Free Software Foundation; either version 2 of the License, or␊ |
# (at your option) any later version.␊ |
#␊ |
# InDefero is distributed in the hope that it will be useful,␊ |
# but WITHOUT ANY WARRANTY; without even the implied warranty of␊ |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the␊ |
# GNU General Public License for more details.␊ |
#␊ |
# You should have received a copy of the GNU General Public License␊ |
# along with this program; if not, write to the Free Software␊ |
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA␊ |
#␊ |
# ***** END LICENSE BLOCK ***** */␊ |
␊ |
/**␊ |
* Mercurial utils.␊ |
*␊ |
*/␊ |
class IDF_Scm_Mercurial␊ |
{␊ |
public $repo = '';␊ |
␊ |
public function __construct($repo)␊ |
{␊ |
$this->repo = $repo;␊ |
}␊ |
␊ |
/**␊ |
* Returns the URL of the git daemon.␊ |
*␊ |
* @param IDF_Project␊ |
* @return string URL␊ |
*/␊ |
public static function getRemoteAccessUrl($project)␊ |
{␊ |
$url = Pluf::f('mercurial_remote_url');␊ |
if (Pluf::f('mercurial_repositories_unique', true)) {␊ |
return $url;␊ |
}␊ |
return $url.'/'.$project->shortname;␊ |
}␊ |
␊ |
/**␊ |
* Returns this object correctly initialized for the project.␊ |
*␊ |
* @param IDF_Project␊ |
* @return IDF_Scm_Git␊ |
*/␊ |
public static function factory($project)␊ |
{␊ |
$rep = Pluf::f('mercurial_repositories');␊ |
if (false == Pluf::f('mercurial_repositories_unique', false)) {␊ |
$rep = $rep.'/'.$project->shortname;␊ |
}␊ |
return new IDF_Scm_Mercurial($rep);␊ |
}␊ |
␊ |
/**␊ |
* Test a given object hash.␊ |
*␊ |
* @param string Object hash.␊ |
* @param null to be svn client compatible␊ |
* @return mixed false if not valid or 'blob', 'tree', 'commit'␊ |
*/␊ |
public function testHash($hash, $dummy=null)␊ |
{␊ |
$cmd = sprintf('hg log -R %s -r %s',␊ |
escapeshellarg($this->repo),␊ |
escapeshellarg($hash));␊ |
$ret = 0; ␊ |
$out = array();␊ |
IDF_Scm::exec($cmd, &$out, &$ret);␊ |
return ($ret != 0) ? false : 'commit'; ␊ |
}␊ |
␊ |
/**␊ |
* Given a commit hash returns an array of files in it.␊ |
*␊ |
* A file is a class with the following properties:␊ |
*␊ |
* 'perm', 'type', 'size', 'hash', 'file'␊ |
*␊ |
* @param string Commit ('HEAD')␊ |
* @param string Base folder ('')␊ |
* @return array ␊ |
*/␊ |
public function filesAtCommit($commit='tip', $folder='')␊ |
{␊ |
// now we grab the info about this commit including its tree.␊ |
$co = $this->getCommit($commit);␊ |
if ($folder) {␊ |
// As we are limiting to a given folder, we need to find␊ |
// the tree corresponding to this folder.␊ |
$found = false;␊ |
foreach ($this->getTreeInfo($co->tree, true, '', true) as $file) {␊ |
if ($file->type == 'tree' and $file->file == $folder) {␊ |
$found = true;␊ |
break;␊ |
}␊ |
} ␊ |
if (!$found) {␊ |
throw new Exception(sprintf(__('Folder %1$s not found in commit %2$s.'), $folder, $commit));␊ |
}␊ |
}␊ |
$res = $this->getTreeInfo($commit, $recurse=true, $folder);␊ |
return $res;␊ |
}␊ |
␊ |
/**␊ |
* Get the tree info.␊ |
*␊ |
* @param string Tree hash ␊ |
* @param bool Do we recurse in subtrees (true)␊ |
* @return array Array of file information.␊ |
*/␊ |
public function getTreeInfo($tree, $recurse=true, $folder='', $root=false)␊ |
{␊ |
if ('commit' != $this->testHash($tree)) {␊ |
throw new Exception(sprintf(__('Not a valid tree: %s.'), $tree));␊ |
}␊ |
$cmd_tmpl = 'hg manifest -R %s --debug -r %s';␊ |
$cmd = sprintf($cmd_tmpl, escapeshellarg($this->repo), $tree, ($recurse) ? '' : ''); ␊ |
$out = array();␊ |
$res = array();␊ |
IDF_Scm::exec($cmd, &$out);␊ |
$out_hack = array();␊ |
foreach ($out as $line) {␊ |
list($hash, $perm, $exec, $file) = preg_split('/ |\t/', $line, 4);␊ |
$file = trim($file);␊ |
$dir = explode('/', $file, -1);␊ |
preg_match_all('|(\w+)/|', $file, $dir);␊ |
$tmp = '';␊ |
for ($i=0; $i < count($dir[1]); $i++) {␊ |
if ($i > 0) {␊ |
$tmp .= '/';␊ |
}␊ |
$tmp .= $dir[1][$i];␊ |
if (!in_array("empty\t000\t\t$tmp/", $out_hack))␊ |
$out_hack[] = "empty\t000\t\t$tmp/";␊ |
}␊ |
$out_hack[] = "$hash\t$perm\t$exec\t$file";␊ |
}␊ |
foreach ($out_hack as $line) {␊ |
list($hash, $perm, $exec, $file) = preg_split('/ |\t/', $line, 4);␊ |
$file = trim($file);␊ |
if (preg_match('/^(.*)\/$/', $file, $match)) {␊ |
$type = 'tree';␊ |
$file = $match[1];␊ |
} else {␊ |
$type = 'blob';␊ |
}␊ |
if (!$root and !$folder and preg_match('/^.*\/.*$/', $file)) {␊ |
continue;␊ |
}␊ |
if ($folder) {␊ |
preg_match('|^'.$folder.'[/]?([^/]+)?$|', $file,$match);␊ |
if (count($match) > 1) {␊ |
$file = $match[1];␊ |
} else {␊ |
continue;␊ |
}␊ |
}␊ |
$res[] = (object) array('perm' => $perm, 'type' => $type, ␊ |
'hash' => $hash, 'fullpath' => ($folder) ? $folder.'/'.$file : $file,␊ |
'file' => $file);␊ |
}␊ |
return $res;␊ |
}␊ |
␊ |
/**␊ |
* Get the file info.␊ |
*␊ |
* @param string Commit ('HEAD')␊ |
* @return false Information␊ |
*/␊ |
public function getFileInfo($totest, $commit='tip')␊ |
{␊ |
$cmd_tmpl = 'hg manifest -R %s --debug -r %s | sed \'s/*/ /\' | sort -k 3| uniq -f2';␊ |
$cmd = sprintf($cmd_tmpl, escapeshellarg($this->repo), $commit); ␊ |
$out = array();␊ |
$res = array();␊ |
IDF_Scm::exec($cmd, &$out);␊ |
$out_hack = array();␊ |
foreach ($out as $line) {␊ |
list($hash, $perm, $exec, $file) = preg_split('/ |\t/', $line, 4);␊ |
$file = trim($file);␊ |
$dir = explode('/', $file, -1);␊ |
preg_match_all('|(\w+)/|', $file, $dir);␊ |
$tmp = '';␊ |
for ($i=0; $i < count($dir[1]); $i++) {␊ |
if ($i > 0) {␊ |
$tmp .= '/';␊ |
}␊ |
$tmp .= $dir[1][$i];␊ |
if (!in_array("empty\t000\t\t$tmp/", $out_hack)) {␊ |
$out_hack[] = "emtpy\t000\t\t$tmp/";␊ |
}␊ |
}␊ |
$out_hack[] = "$hash\t$perm\t$exec\t$file";␊ |
}␊ |
␊ |
foreach ($out_hack as $line) {␊ |
list($hash, $perm, $exec, $file) = preg_split('/ |\t/', $line, 4);␊ |
$file = trim ($file);␊ |
if (preg_match('/^(.*)\/$/', $file, $match)) {␊ |
$type = 'tree';␊ |
$file = $match[1];␊ |
} else {␊ |
$type = 'blob';␊ |
}␊ |
if ($totest == $file) {␊ |
return (object) array('perm' => $perm, 'type' => $type, ␊ |
'hash' => $hash, ␊ |
'file' => $file,␊ |
'commit' => $commit␊ |
);␊ |
␊ |
}␊ |
}␊ |
return false;␊ |
}␊ |
␊ |
/**␊ |
* Get a blob.␊ |
*␊ |
* @param string request_file_info␊ |
* @param null to be svn client compatible␊ |
* @return string Raw blob␊ |
*/␊ |
public function getBlob($request_file_info, $dummy=null)␊ |
{␊ |
return IDF_Scm::shell_exec(sprintf('hg cat -R %s -r %s %s',␊ |
escapeshellarg($this->repo), ␊ |
$dummy,␊ |
escapeshellarg($this->repo . '/' . $request_file_info->file)));␊ |
}␊ |
␊ |
/**␊ |
* Get the branches.␊ |
*␊ |
* @return array Branches.␊ |
*/␊ |
public function getBranches()␊ |
{␊ |
$out = array();␊ |
IDF_Scm::exec(sprintf('hg branches -R %s', ␊ |
escapeshellarg($this->repo)), &$out);␊ |
$res = array();␊ |
foreach ($out as $b) {␊ |
preg_match('/(\S+).*\S+:(\S+)/', $b, $match);␊ |
$res[] = $match[1];␊ |
}␊ |
return $res;␊ |
}␊ |
␊ |
/**␊ |
* Get commit details.␊ |
*␊ |
* @param string Commit ('HEAD').␊ |
* @return array Changes.␊ |
*/␊ |
public function getCommit($commit='tip')␊ |
{␊ |
␊ |
$cmd = sprintf('hg log -p -r %s -R %s', escapeshellarg($commit), escapeshellarg($this->repo));␊ |
$out = array();␊ |
IDF_Scm::exec($cmd, &$out);␊ |
$log = array();␊ |
$change = array();␊ |
$inchange = false;␊ |
foreach ($out as $line) {␊ |
if (!$inchange and 0 === strpos($line, 'diff -r')) {␊ |
$inchange = true;␊ |
}␊ |
if ($inchange) {␊ |
$change[] = $line;␊ |
} else {␊ |
$log[] = $line;␊ |
}␊ |
}␊ |
$out = self::parseLog($log, 6);␊ |
$out[0]->changes = implode("\n", $change);␊ |
return $out[0];␊ |
}␊ |
␊ |
␊ |
/**␊ |
* Get latest changes.␊ |
*␊ |
* @param string Commit ('HEAD').␊ |
* @param int Number of changes (10).␊ |
* @return array Changes.␊ |
*/␊ |
public function getChangeLog($commit='tip', $n=10)␊ |
{␊ |
$cmd = sprintf('hg log -R %s -l%s ', escapeshellarg($this->repo), $n, $commit);␊ |
$out = array();␊ |
IDF_Scm::exec($cmd, &$out);␊ |
return self::parseLog($out, 6);␊ |
}␊ |
␊ |
/**␊ |
* Parse the log lines of a --pretty=medium log output.␊ |
*␊ |
* @param array Lines.␊ |
* @param int Number of lines in the headers (3)␊ |
* @return array Change log.␊ |
*/␊ |
␊ |
public static function parseLog($lines, $hdrs=3)␊ |
{␊ |
$res = array();␊ |
$c = array();␊ |
$i = 0;␊ |
$hdrs += 1;␊ |
foreach ($lines as $line) {␊ |
$i++;␊ |
if (0 === strpos($line, 'changeset:')) {␊ |
if (count($c) > 0) {␊ |
$c['full_message'] = trim($c['full_message']);␊ |
$res[] = (object) $c;␊ |
}␊ |
$c = array();␊ |
$c['commit'] = substr(strrchr($line, ':'), 1);␊ |
$c['full_message'] = '';␊ |
$i=1;␊ |
continue;␊ |
␊ |
}␊ |
if ($i == $hdrs) {␊ |
$c['title'] = trim($line);␊ |
continue;␊ |
}␊ |
$match = array();␊ |
if (preg_match('/(\S+)\s*:\s*(.*)/', $line, $match)) {␊ |
$match[1] = strtolower($match[1]);␊ |
if ($match[1] == 'user') {␊ |
$c['author'] = $match[2];␊ |
} elseif ($match[1] == 'summary') {␊ |
$c['title'] = $match[2];␊ |
} else {␊ |
$c[$match[1]] = trim($match[2]);␊ |
}␊ |
if ($match[1] == 'date') {␊ |
$c['date'] = gmdate('Y-m-d H:i:s', strtotime($match[2]));␊ |
}␊ |
continue;␊ |
}␊ |
if ($i > ($hdrs+1)) {␊ |
$c['full_message'] .= trim($line)."\n";␊ |
continue;␊ |
}␊ |
}␊ |
$c['tree'] = $c['commit'];␊ |
$c['full_message'] = trim($c['full_message']);␊ |
$res[] = (object) $c;␊ |
return $res;␊ |
}␊ |
␊ |
/**␊ |
* Generate the command to create a zip archive at a given commit.␊ |
*␊ |
* @param string Commit␊ |
* @param string Prefix ('git-repo-dump')␊ |
* @return string Command␊ |
*/␊ |
public function getArchiveCommand($commit, $prefix='')␊ |
{␊ |
return sprintf('hg archive --type=zip -R %s -r %s -',␊ |
escapeshellarg($this->repo),␊ |
escapeshellarg($commit));␊ |
}␊ |
}␊ |