diff --git a/src/IDF/Form/SourceConf.php b/src/IDF/Form/SourceConf.php index 1e210e5..c268c49 100644 --- a/src/IDF/Form/SourceConf.php +++ b/src/IDF/Form/SourceConf.php @@ -39,6 +39,7 @@ class IDF_Form_SourceConf extends Pluf_Form array( __('git') => 'git', __('Subversion') => 'svn', + __('mercurial') => 'mercurial', ) ), 'widget' => 'Pluf_Form_Widget_SelectInput', diff --git a/src/IDF/Project.php b/src/IDF/Project.php index 9f1194a..f8581d1 100644 --- a/src/IDF/Project.php +++ b/src/IDF/Project.php @@ -353,7 +353,11 @@ class IDF_Project extends Pluf_Model public function getScmRoot() { $conf = $this->getConf(); - $roots = array('git' => 'master', 'svn' => 'HEAD'); + $roots = array( + 'git' => 'master', + 'svn' => 'HEAD', + 'mercurial' => 'tip' + ); $scm = $conf->getVal('scm', 'git'); return $roots[$scm]; } @@ -388,4 +392,4 @@ class IDF_Project extends Pluf_Model return $this->_pconf; } -} \ No newline at end of file +} diff --git a/src/IDF/Scm/Mercurial.php b/src/IDF/Scm/Mercurial.php new file mode 100644 index 0000000..d90dd99 --- /dev/null +++ b/src/IDF/Scm/Mercurial.php @@ -0,0 +1,382 @@ +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)); + } +} diff --git a/src/IDF/conf/idf.php-dist b/src/IDF/conf/idf.php-dist index 9a1c7d0..ab15567 100644 --- a/src/IDF/conf/idf.php-dist +++ b/src/IDF/conf/idf.php-dist @@ -53,6 +53,11 @@ $cfg['svn_repositories'] = 'file:///home/svn/repositories/indefero'; $cfg['svn_repositories_unique'] = true; $cfg['svn_remote_url'] = 'http://projects.ceondo.com/svn/indefero'; +// Mercurial repositories path +//$cfg['mercurial_repositories'] = '/home/mercurial/repositories'; +//$cfg['mercurial_repositories_unique'] = false; +//$cfg['mercurial_remote_url'] = 'http://projects.ceondo.com/hg'; + // Example of one *local* subversion repository for each project: // the path to the repository on disk will automatically created to be diff --git a/src/IDF/templates/idf/source/changelog.html b/src/IDF/templates/idf/source/changelog.html index 3b0afa5..124657d 100644 --- a/src/IDF/templates/idf/source/changelog.html +++ b/src/IDF/templates/idf/source/changelog.html @@ -28,15 +28,14 @@ {/block} {block context} -{if $scm == 'git'} +{if $scm != 'svn'}

{trans 'Branches:'}
{foreach $branches as $branch} {aurl 'url', 'IDF_Views_Source::changeLog', array($project.shortname, $branch)} {$branch}
{/foreach}

-{/if} -{if $scm == 'svn'} +{else}

{trans 'Revision:'} {$commit}

@@ -44,7 +43,6 @@

- {/if} {/block} diff --git a/src/IDF/templates/idf/source/mercurial/file.html b/src/IDF/templates/idf/source/mercurial/file.html new file mode 100644 index 0000000..bc659e4 --- /dev/null +++ b/src/IDF/templates/idf/source/mercurial/file.html @@ -0,0 +1,37 @@ +{extends "idf/source/base.html"} +{block extraheader}{/block} +{block docclass}yui-t1{assign $inSourceTree=true}{/block} + +{block body} +

{trans 'Root'}/{if $breadcrumb}{$breadcrumb|safe}{/if}

+ + +{if !$tree_in} +{aurl 'url', 'IDF_Views_Source::commit', array($project.shortname, $commit)} + + + +{/if} + +{$file} + +
{blocktrans}Source at commit {$commit} created {$cobject.date|dateago}.{/blocktrans}
+{blocktrans}By {$cobject.author|strip_tags|trim}, {$cobject.title}{/blocktrans} +
+{aurl 'url', 'IDF_Views_Source::getFile', array($project.shortname, $commit, $fullpath)} +

{trans 'Archive'} {trans 'Download this file'}

+{/block} + +{block context} +

{trans 'Branches:'}
+{foreach $branches as $branch} +{aurl 'url', 'IDF_Views_Source::treeBase', array($project.shortname, $branch)} +{$branch}
+{/foreach} +

+{/block} + +{block javascript} + + +{/block} diff --git a/src/IDF/templates/idf/source/mercurial/tree.html b/src/IDF/templates/idf/source/mercurial/tree.html new file mode 100644 index 0000000..9d0ba7a --- /dev/null +++ b/src/IDF/templates/idf/source/mercurial/tree.html @@ -0,0 +1,56 @@ +{extends "idf/source/base.html"} +{block docclass}yui-t1{assign $inSourceTree=true}{/block} +{block body} +

{trans 'Root'}/{if $breadcrumb}{$breadcrumb|safe}{/if}

+ + + + + + + + + +{if !$tree_in} +{aurl 'url', 'IDF_Views_Source::commit', array($project.shortname, $commit)} + + + +{/if} +{if $base} + + + + + +{/if} +{foreach $files as $file} +{aurl 'url', 'IDF_Views_Source::tree', array($project.shortname, $commit, $file.fullpath)} + + +{$file.file} +{if $file.type == 'blob'} +{if isset($file.date)} + + +{else}{/if} +{/if} + +{/foreach} + +
{trans 'File'}{trans 'Age'}{trans 'Message'}{trans 'Size'}
{blocktrans}Source at commit {$commit} created {$cobject.date|dateago}.{/blocktrans}
+{blocktrans}By {$cobject.author|strip_tags|trim}, {$cobject.title}{/blocktrans} +
  +..
{$file.type}{$file.date|dateago:"wihtout"}{$file.author|strip_tags|trim}{trans ':'} {$file.log}
+{aurl 'url', 'IDF_Views_Source::download', array($project.shortname, $commit)} +

{trans 'Archive'} {trans 'Download this version'} {trans 'or'} hg clone {$project.getRemoteAccessUrl()}

+{/block} + +{block context} +

{trans 'Branches:'}
+{foreach $branches as $branch} +{aurl 'url', 'IDF_Views_Source::treeBase', array($project.shortname, $branch)} +{$branch}
+{/foreach} +

+{/block}