From 7c502b1745a2f7188299dac7c754f21690c6e858 Mon Sep 17 00:00:00 2001 From: Loic d'Anterroches Date: Sat, 25 Apr 2009 16:24:40 +0200 Subject: [PATCH] Continued the SCM backend refactor. The new backend is near completion. --- src/IDF/Project.php | 8 +- src/IDF/Scm.php | 157 +++++++++++++++------ src/IDF/Scm/Git.php | 91 ++++++------ src/IDF/Views/Source.php | 41 +++--- src/IDF/relations.php | 1 + src/IDF/templates/idf/source/git/help.html | 4 +- src/IDF/templates/idf/source/git/tree.html | 8 +- 7 files changed, 188 insertions(+), 122 deletions(-) diff --git a/src/IDF/Project.php b/src/IDF/Project.php index bee4372..8c07deb 100644 --- a/src/IDF/Project.php +++ b/src/IDF/Project.php @@ -358,7 +358,7 @@ class IDF_Project extends Pluf_Model $conf = $this->getConf(); $scm = $conf->getVal('scm', 'git'); $scms = Pluf::f('allowed_scm'); - return call_user_func(array($scms[$scm], 'getRemoteAccessUrl'), + return call_user_func(array($scms[$scm], 'getAnonymousAccessUrl'), $this); } @@ -369,13 +369,13 @@ class IDF_Project extends Pluf_Model * same as the one to read. For example, you do a checkout with * git-daemon and push with SSH. */ - public function getWriteRemoteAccessUrl() + public function getWriteRemoteAccessUrl($user) { $conf = $this->getConf(); $scm = $conf->getVal('scm', 'git'); $scms = Pluf::f('allowed_scm'); - return call_user_func(array($scms[$scm], 'getWriteRemoteAccessUrl'), - $this); + return call_user_func(array($scms[$scm], 'getAuthAccessUrl'), + $this, $user); } /** diff --git a/src/IDF/Scm.php b/src/IDF/Scm.php index faf7e57..e8c9999 100644 --- a/src/IDF/Scm.php +++ b/src/IDF/Scm.php @@ -37,7 +37,9 @@ * * Note on caching: You must not cache ephemeral information like the * changelog, but you can cache the commit info (except with - * subversion where you can change commit info...). + * subversion where you can change commit info...). It is ok to do + * some caching for the lifetime of the IDF_Scm object, for example + * not to retrieve several times the list of branches, etc. * * All the output of the methods must be serializable. This means that * if you are parsing XML you need to correctly cast the results as @@ -84,7 +86,7 @@ class IDF_Scm * @param IDF_Project * @return Object */ - public static function get($project=null) + public static function get($project) { // Get scm type from project conf ; defaults to git // We will need to cache the factory @@ -93,6 +95,29 @@ class IDF_Scm return call_user_func(array($scms[$scm], 'factory'), $project); } + /** + * Returns the URL of the git daemon. + * + * @param IDF_Project + * @return string URL + */ + public static function getAnonymousAccessUrl($project) + { + throw new Pluf_Exception_NotImplemented(); + } + + /** + * Returns the URL for SSH access + * + * @param IDF_Project + * @param Pluf_User + * @return string URL + */ + public static function getAuthAccessUrl($project, $user) + { + throw new Pluf_Exception_NotImplemented(); + } + /** * Check if the backend is available for display. * @@ -103,10 +128,58 @@ class IDF_Scm throw new Pluf_Exception_NotImplemented(); } + /** + * Check if a revision or commit is valid. + * + * @param string Revision or commit + * @return bool + */ + public function isValidRevision($rev) + { + throw new Pluf_Exception_NotImplemented(); + } + + /** + * Returns in which branches a commit/path is. + * + * A commit can be in several branches and some of the SCMs are + * managing branches using subfolders (like Subversion). + * + * This means that to know in which branch we are at the moment, + * one needs to have both the path and the commit. + * + * @param string Commit + * @param string Path + * @return array Branches + */ + public function inBranches($commit, $path) + { + throw new Pluf_Exception_NotImplemented(); + } + /** * Returns the list of branches. * - * @return array For example array('trunk', '1.0branch') + * The return value must be a branch indexed array with the + * optional path to access the branch as value. For example with + * git you would get (note that some people are using / in the + * name of their git branches): + * + *
+     * array('master' => '',
+     *       'foo-branch' => '',
+     *       'design/feature1' => '')
+     * 
+ * + * But with Subversion, as the branches are managed as subfolder + * with a special folder for trunk, you would get something like: + * + *
+     * array('trunk' => 'trunk',
+     *       'foo-branch' => 'branches/foo-branch',)
+     * 
+ * + * @return array Branches */ public function getBranches() { @@ -116,7 +189,11 @@ class IDF_Scm /** * Returns the list of tags. * - * @return array For example array('v0.9', 'v1.0') + * The format is the same as for the branches. + * + * @see self::getBranches() + * + * @return array Tags */ public function getTags() { @@ -201,67 +278,59 @@ class IDF_Scm /** * Given a revision and a file path, retrieve the file content. * - * The third parameter is to only request the command that is used - * to get the file content. This is used when downloading a file - * at a given revision as it can be passed to a + * The $cmd_only parameter is to only request the command that is + * used to get the file content. This is used when downloading a + * file at a given revision as it can be passed to a * Pluf_HTTP_Response_CommandPassThru reponse. This allows to * stream a large response without buffering it in memory. * - * The file definition can be a hash or a path depending on the - * SCM. + * The file definition is coming from getPathInfo(). * - * @param string File definition - * @param string Revision ('') + * @see self::getPathInfo() + * + * @param stdClass File definition * @param bool Returns command only (false) * @return string File content */ - public function getFile($def, $rev='', $cmd_only=false) + public function getFile($def, $cmd_only=false) { throw new Pluf_Exception_NotImplemented(); } - /** - * Equivalent to exec but with caching. + * Get information about a file or a path. * - * @param string Command - * @param &array Output - * @param &int Return value - * @return string Last line of the output + * @param string File or path + * @param string Revision (null) + * @return mixed False or stdClass with info */ - public static function exec($command, &$output=array(), &$return=0) + public function getPathInfo($file, $rev=null) { - $command = Pluf::f('idf_exec_cmd_prefix', '').$command; - $key = md5($command); - $cache = Pluf_Cache::factory(); - if (null === ($res=$cache->get($key))) { - $ll = exec($command, $output, $return); - if ($return != 0 and Pluf::f('debug_scm', false)) { - throw new IDF_Scm_Exception(sprintf('Error when running command: "%s", return code: %d', $command, $return)); - } - $cache->set($key, array($ll, $return, $output)); - } else { - list($ll, $return, $output) = $res; - } - return $ll; + throw new Pluf_Exception_NotImplemented(); } /** - * Equivalent to shell_exec but with caching. + * Given a revision and possible path returns additional properties. * - * @param string Command - * @return string Output of the command + * @param string Revision + * @param string Path ('') + * @return mixed null or array of properties */ - public static function shell_exec($command) + public function getProperties($rev, $path='') { - $command = Pluf::f('idf_exec_cmd_prefix', '').$command; - $key = md5($command); - $cache = Pluf_Cache::factory(); - if (null === ($res=$cache->get($key))) { - $res = shell_exec($command); - $cache->set($key, $res); - } - return $res; + return null; + } + + /** + * Generate the command to create a zip archive at a given commit. + * + * @param string Commit + * @param string Prefix ('repository/') + * @return string Command + */ + public function getArchiveCommand($commit, $prefix='repository/') + { + throw new Pluf_Exception_NotImplemented(); } /** diff --git a/src/IDF/Scm/Git.php b/src/IDF/Scm/Git.php index b89b44c..2361b9c 100644 --- a/src/IDF/Scm/Git.php +++ b/src/IDF/Scm/Git.php @@ -61,7 +61,7 @@ class IDF_Scm_Git extends IDF_Scm } $res = array(); foreach ($out as $b) { - $res[] = substr($b, 2); + $res[substr($b, 2)] = ''; } $this->cache['branches'] = $res; return $res; @@ -69,9 +69,29 @@ class IDF_Scm_Git extends IDF_Scm public function getMainBranch() { - return 'master'; + $possible = array('master', 'main', 'trunk', 'local'); + $branches = array_keys($this->getBranches()); + foreach ($possible as $p) { + if (in_array($p, $branches)) { + return $p; + } + } + return $branches[0]; } + /** + * Note: Running the `git branch --contains $commit` is + * theoritically the best way to do it, until you figure out that + * you cannot cache the result and that it takes several seconds + * to execute on a big tree. + */ + public function inBranches($commit, $path) + { + return (in_array($commit, array_keys($this->getBranches()))) + ? array($commit) : array(); + } + + /** * Git "tree" is not the same as the tree we get here. * @@ -152,25 +172,12 @@ class IDF_Scm_Git extends IDF_Scm return ($users->count() > 0) ? $users[0] : null; } - - /** - * Returns the URL of the git daemon. - * - * @param IDF_Project - * @return string URL - */ - public static function getRemoteAccessUrl($project) + public static function getAnonymousAccessUrl($project) { return sprintf(Pluf::f('git_remote_url'), $project->shortname); } - /** - * Returns the URL for SSH access - * - * @param IDF_Project - * @return string URL - */ - public static function getWriteRemoteAccessUrl($project) + public static function getAuthAccessUrl($project, $user) { return sprintf(Pluf::f('git_write_remote_url'), $project->shortname); } @@ -187,20 +194,25 @@ class IDF_Scm_Git extends IDF_Scm return new IDF_Scm_Git($rep, $project); } + + public function isValidRevision($commit) + { + return ('commit' == $this->testHash($commit)); + } + /** * 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) + public function testHash($hash) { $cmd = sprintf('GIT_DIR=%s '.Pluf::f('git_path', 'git').' cat-file -t %s', escapeshellarg($this->repo), escapeshellarg($hash)); $ret = 0; $out = array(); - IDF_Scm::exec($cmd, $out, $ret); + exec($cmd, $out, $ret); if ($ret != 0) return false; return trim($out[0]); } @@ -257,14 +269,14 @@ class IDF_Scm_Git extends IDF_Scm * @param string Commit ('HEAD') * @return false Information */ - public function getFileInfo($totest, $commit='HEAD') + public function getPathInfo($totest, $commit='HEAD') { $cmd_tmpl = 'GIT_DIR=%s '.Pluf::f('git_path', 'git').' ls-tree -r -t -l %s'; $cmd = sprintf($cmd_tmpl, escapeshellarg($this->repo), escapeshellarg($commit)); $out = array(); - IDF_Scm::exec($cmd, $out); + exec($cmd, $out); foreach ($out as $line) { list($perm, $type, $hash, $size, $file) = preg_split('/ |\t/', $line, 5, PREG_SPLIT_NO_EMPTY); if ($totest == $file) { @@ -276,19 +288,13 @@ class IDF_Scm_Git extends IDF_Scm 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) + public function getFile($def, $cmd_only=false) { - return shell_exec(sprintf(Pluf::f('idf_exec_cmd_prefix', ''). - 'GIT_DIR=%s '.Pluf::f('git_path', 'git').' cat-file blob %s', - escapeshellarg($this->repo), - escapeshellarg($request_file_info->hash))); + $cmd = sprintf(Pluf::f('idf_exec_cmd_prefix', ''). + 'GIT_DIR=%s '.Pluf::f('git_path', 'git').' cat-file blob %s', + escapeshellarg($this->repo), + escapeshellarg($def->hash)); + return ($cmd_only) ? $cmd : shell_exec($cmd); } @@ -345,7 +351,7 @@ class IDF_Scm_Git extends IDF_Scm "'commit %H%n'", escapeshellarg($commit)); $out = array(); - IDF_Scm::exec($cmd, $out); + exec($cmd, $out); $affected = count($out) - 2; $added = 0; $removed = 0; @@ -377,7 +383,7 @@ class IDF_Scm_Git extends IDF_Scm escapeshellarg($this->repo), $n, $this->mediumtree_fmt, escapeshellarg($commit)); $out = array(); - IDF_Scm::exec($cmd, $out); + exec($cmd, $out); return self::parseLog($out, 4); } @@ -436,14 +442,7 @@ class IDF_Scm_Git extends IDF_Scm 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='git-repo-dump/') + public function getArchiveCommand($commit, $prefix='repository/') { return sprintf(Pluf::f('idf_exec_cmd_prefix', ''). 'GIT_DIR=%s '.Pluf::f('git_path', 'git').' archive --format=zip --prefix=%s %s', @@ -471,11 +470,11 @@ class IDF_Scm_Git extends IDF_Scm { $file->type = 'extern'; $file->extern = ''; - $info = $this->getFileInfo('.gitmodules', $commit); + $info = $this->getPathInfo('.gitmodules', $commit); if ($info == false) { return $file; } - $gitmodules = $this->getBlob($info); + $gitmodules = $this->getFile($info); if (preg_match('#\[submodule\s+\"'.$file->fullpath.'\"\]\s+path\s=\s(\S+)\s+url\s=\s(\S+)#mi', $gitmodules, $matches)) { $file->extern = $matches[2]; } diff --git a/src/IDF/Views/Source.php b/src/IDF/Views/Source.php index c1344c4..074bea2 100644 --- a/src/IDF/Views/Source.php +++ b/src/IDF/Views/Source.php @@ -66,7 +66,7 @@ class IDF_Views_Source $scm = IDF_Scm::get($request->project); $branches = $scm->getBranches(); $commit = $match[2]; - if ('commit' != $scm->testHash($commit)) { + if (!$scm->isValidRevision($commit)) { if (count($branches) == 0) { // Redirect to the project source help $url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::help', @@ -119,6 +119,7 @@ class IDF_Views_Source return new Pluf_HTTP_Response_Redirect($url); } $branches = $scm->getBranches(); + $in_branches = $scm->inBranches($commit, ''); $cache = Pluf_Cache::factory(); $key = sprintf('Project:%s::IDF_Views_Source::treeBase:%s::', $request->project->id, $commit); @@ -126,13 +127,9 @@ class IDF_Views_Source $res = new Pluf_Template_ContextVars($scm->getTree($commit)); $cache->set($key, $res); } - - $tree_in = in_array($commit, $branches); + //$tree_in = in_array($commit, $branches); $scmConf = $request->conf->getVal('scm', 'git'); - $props = null; - if ($scmConf === 'svn') { - $props = $scm->getProperties($commit); - } + $props = $scm->getProperties($commit); return Pluf_Shortcuts_RenderToResponse('idf/source/'.$scmConf.'/tree.html', array( 'page_title' => $title, @@ -140,7 +137,7 @@ class IDF_Views_Source 'files' => $res, 'cobject' => $cobject, 'commit' => $commit, - 'tree_in' => $tree_in, + 'tree_in' => $in_branches, 'branches' => $branches, 'props' => $props, ), @@ -174,12 +171,12 @@ class IDF_Views_Source return new Pluf_HTTP_Response_Redirect($url, 301); } - if ('commit' != $scm->testHash($commit, $request_file)) { + if (!$scm->isValidRevision($commit, $request_file)) { // Redirect to the first branch return new Pluf_HTTP_Response_Redirect($fburl); } - $request_file_info = $scm->getFileInfo($request_file, $commit); + $request_file_info = $scm->getPathInfo($request_file, $commit); if (!$request_file_info) { // Redirect to the first branch return new Pluf_HTTP_Response_Redirect($fburl); @@ -190,7 +187,7 @@ class IDF_Views_Source $commit, $scm); if (!self::isText($info)) { - $rep = new Pluf_HTTP_Response($scm->getBlob($request_file_info, $commit), + $rep = new Pluf_HTTP_Response($scm->getFile($request_file_info), $info[0]); $rep->headers['Content-Disposition'] = 'attachment; filename="'.$info[1].'"'; return $rep; @@ -210,7 +207,7 @@ class IDF_Views_Source $page_title = $bc.' - '.$title; $cobject = $scm->getCommit($commit); - $tree_in = in_array($commit, $branches); + $in_branches = $scm->inBranches($commit, $request_file); try { $cache = Pluf_Cache::factory(); $key = sprintf('Project:%s::IDF_Views_Source::tree:%s::%s', @@ -243,7 +240,7 @@ class IDF_Views_Source 'cobject' => $cobject, 'base' => $request_file_info->file, 'prev' => $previous, - 'tree_in' => $tree_in, + 'tree_in' => $in_branches, 'branches' => $branches, 'props' => $props, ), @@ -273,7 +270,7 @@ class IDF_Views_Source $scm = IDF_Scm::get($request->project); $commit = $match[2]; $branches = $scm->getBranches(); - if ('commit' != $scm->testHash($commit)) { + if (!$scm->isValidRevision($commit)) { // Redirect to the first branch $url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::treeBase', array($request->project->shortname, @@ -309,7 +306,7 @@ class IDF_Views_Source $scm = IDF_Scm::get($request->project); $commit = $match[2]; $branches = $scm->getBranches(); - if ('commit' != $scm->testHash($commit)) { + if (!$scm->isValidRevision($commit)) { // Redirect to the first branch $url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::treeBase', array($request->project->shortname, @@ -347,7 +344,7 @@ class IDF_Views_Source if ($scmConf === 'svn') { $props = $scm->getProperties($commit, $request_file); } - $content = self::highLight($extra['mime'], $scm->getBlob($request_file_info, $commit)); + $content = self::highLight($extra['mime'], $scm->getFile($request_file_info)); return Pluf_Shortcuts_RenderToResponse('idf/source/'.$scmConf.'/file.html', array( 'page_title' => $page_title, @@ -377,14 +374,14 @@ class IDF_Views_Source $branches = $scm->getBranches(); $commit = $match[2]; $request_file = $match[3]; - if ('commit' != $scm->testHash($commit, $request_file)) { + if (!$scm->isValidRevision($commit)) { // Redirect to the first branch $url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::treeBase', array($request->project->shortname, $branches[0])); return new Pluf_HTTP_Response_Redirect($url); } - $request_file_info = $scm->getFileInfo($request_file, $commit); + $request_file_info = $scm->getPathInfo($request_file, $commit); if (!$request_file_info or $request_file_info->type == 'tree') { // Redirect to the first branch $url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::treeBase', @@ -394,7 +391,7 @@ class IDF_Views_Source } $info = self::getRequestedFileMimeType($request_file_info, $commit, $scm); - $rep = new Pluf_HTTP_Response($scm->getBlob($request_file_info, $commit), + $rep = new Pluf_HTTP_Response($scm->getFile($request_file_info), $info[0]); $rep->headers['Content-Disposition'] = 'attachment; filename="'.$info[1].'"'; return $rep; @@ -410,7 +407,7 @@ class IDF_Views_Source $commit = trim($match[2]); $scm = IDF_Scm::get($request->project); $branches = $scm->getBranches(); - if ('commit' != $scm->testHash($commit)) { + if (!$scm->isValidRevision($commit)) { // Redirect to the first branch $url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::treeBase', array($request->project->shortname, @@ -441,7 +438,7 @@ class IDF_Views_Source return $mime; } return self::getMimeTypeFromContent($file_info->file, - $scm->getBlob($file_info, $commit)); + $scm->getFile($file_info)); } /** @@ -451,7 +448,7 @@ class IDF_Views_Source * @param string File content * @return array Mime type found or 'application/octet-stream', basename, extension */ - public static function getMimeTypeFromContent($file, $filedata) + public static function getMimeTypeFromContent($file, &$filedata) { $info = pathinfo($file); $res = array('application/octet-stream', diff --git a/src/IDF/relations.php b/src/IDF/relations.php index 3f3034f..c1e482f 100644 --- a/src/IDF/relations.php +++ b/src/IDF/relations.php @@ -40,6 +40,7 @@ $m['IDF_Review_FileComment'] = array('relate_to' => array('IDF_Review_Patch', 'P $m['IDF_Key'] = array('relate_to' => array('Pluf_User')); $m['IDF_Conf'] = array('relate_to' => array('IDF_Project')); $m['IDF_Commit'] = array('relate_to' => array('IDF_Project', 'Pluf_User')); +$m['IDF_Scm_Cache_Git'] = array('relate_to' => array('IDF_Project')); Pluf_Signal::connect('Pluf_Template_Compiler::construct_template_tags_modifiers', array('IDF_Middleware', 'updateTemplateTagsModifiers')); diff --git a/src/IDF/templates/idf/source/git/help.html b/src/IDF/templates/idf/source/git/help.html index 4f90309..de939c9 100644 --- a/src/IDF/templates/idf/source/git/help.html +++ b/src/IDF/templates/idf/source/git/help.html @@ -8,7 +8,7 @@ code.{/blocktrans}

{trans 'Command-Line Access'}

-

git clone {if $project.private}{$project.getWriteRemoteAccessUrl()}{else}{$project.getRemoteAccessUrl()}{/if}

+

git clone {if $project.private}{$project.getWriteRemoteAccessUrl($user)}{else}{$project.getRemoteAccessUrl()}{/if}

{aurl 'url', 'IDF_Views_User::myAccount'}

{blocktrans}You may need to provide your SSH key. The synchronization of your SSH key can take a couple of minutes. You can learn more about SSH key authentification.{/blocktrans}

@@ -22,7 +22,7 @@ code.{/blocktrans}

git init git add . git commit -m "initial import" -git remote add origin {$project.getWriteRemoteAccessUrl()} +git remote add origin {$project.getWriteRemoteAccessUrl($url)} git push origin master diff --git a/src/IDF/templates/idf/source/git/tree.html b/src/IDF/templates/idf/source/git/tree.html index 3bcbcd3..5a4659c 100644 --- a/src/IDF/templates/idf/source/git/tree.html +++ b/src/IDF/templates/idf/source/git/tree.html @@ -47,15 +47,15 @@ {aurl 'url', 'IDF_Views_Source::download', array($project.shortname, $commit)} -

{trans 'Archive'} {trans 'Download this version'} {trans 'or'} git clone {if $project.private}{$project.getWriteRemoteAccessUrl()}{else}{$project.getRemoteAccessUrl()}{/if} {trans 'Help'}

+

{trans 'Archive'} {trans 'Download this version'} {trans 'or'} git clone {if $project.private}{$project.getWriteRemoteAccessUrl($user)}{else}{$project.getRemoteAccessUrl()}{/if} {trans 'Help'}

{/block} {block context}

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

{/block}