在Linux上设置Subversion存储库
在本教程中,我将介绍如何设置用于PHP JavaScript开发的子版本库。 我是Group-Office组件的主要开发人员和Intermesh的所有者。
SVN存储库将由多个用户使用SSH密钥登录到服务器。 所有用户将使用svn系统用户,但我们将使用SSH密钥来识别用户。 将使用预提交钩子来验证PHP和Javascript以符合编码规则。
我们使用一个新鲜的最小的debian安装来做到这一点。
安装所需的软件包
在最小的Debian服务器上安装所需的软件:
$ apt-get install subversion php-codesniffer php5-cli
设置颠覆系统用户和隧道
我们将只创建一个将读取和写入SVN信息库的系统用户。 我们将使用SSH的authorized_keys文件将不同的SSH密钥隧道传送到svnserve命令。
以root身份登录到shell并键入以下命令:
$ adduser svn
$ mkdir /home/svn/.ssh
$ touch /home/svn/.ssh/authorized_keys
$ chown -R svn:svn /home/svn/.ssh
现在编辑/home/svn/.ssh/authorized_keys
并为每个用户添加一个公共SSH密钥:
command="/usr/bin/svnserve -t -r /usr/local/svn --tunnel-user=repouser1",no-agent-forwarding,no-port-forwarding,no-X11-forwarding,no-pty,from="*" ssh-rsa AAAAB3NzaC1yc2EAAAADAQAadsadadAAABAQCrJDzuDALKnKpv9xMaypcTcHHCfhaGuPEoVNyyZ2CrdwCI/GaB04A3YLjaJgdsdsadaB96Lpt5aoxunayx5n0OhRXZZZAioG8CX4Lvmddi/O2fSTU3qfe5iJ4fXUDMzCsadsadsadsadsadFpbTZnbLn6pMjM3KrX+SZlFuBY3zO/WB6i3vcnKpZ1N2wuWcTeOzFm5AMKJGMb4s7rMkA0WlzLZ0xMdzraJK2I43trZf8DUeFeydYRi29Dm2Sr5Jm1uuFt9E3N8VFaIdb7K3IK954lp8ks2ueEXcWLArVFcrAJ15AWS6fzLFQfJeqGaumHjGZTBPnrAg41KHRYXb9Oj65LquNFCrazN3laLIKh repouser1@Intermesh-2
设置存储库
我们将使用以下命令在“/ usr / local / svn”中创建一个存储库“testrepository”:
$ mkdir /usr/local/svn
$ svnadmin create /usr/local/svn/testrepository
$ chown svn:svn -R /usr/local/svn
您可能需要控制“/ usr / local / svn / testrepository / conf / authz”中的权限。 确保在svnserv.conf中启用了authz-db = authz。
存储库根目录的URL将是:“svn + ssh://svn@svn.example.com/testrepository”。
设置PHP预提交钩子
我们想在提交时测试我们的PHP语法和编码风格,以便安装php-codesniffer:
$ apt-get install php-codesniffer
创建文件/usr/local/svn/CommitTests.php
:
<?php /** * Subversion pre-commit hook script validating commit log message */ /** * Class for performing various tests in subversion pre-commit hooks */ class CommitTests { /** * path to php binary */ public static $PHP = "/usr/bin/php"; /* * path to phpcs library */ public static $PHPCS = "/usr/bin/phpcs"; /** * Commit message string * @var string */ protected $_logMessage; /** * Commit files list * @var array */ protected $_commitList; /** * Changed files list * @var array */ protected $_changedFiles; /** * Subversion repository path * @var string */ protected $_repository; /** * Transaction number * @var int */ protected $_transaction; /** * Class constructor * * @param string $repository * @param string $transaction * @param array $tests array of test names to run */ public function __construct($repository, $transaction, array $tests) { $this->_repository = $repository; $this->_transaction = $transaction; exit($this->_runTests($tests)); } /** * Run subversion pre-commit tests * * @param array $tests array of test names to run * @return int result code, 0 == all test passed, other value represents * number of failed tests */ protected function _runTests(array $tests) { $result = 0; $messages = ''; foreach ($tests as $k => $v) { if (is_numeric($k)) { $test = $v; $params = array(); } else { $test = $k; $params = $v; if (!is_array($params)) { throw new Exception('Test arguments should be in an array.'); } } $method = "_test$test"; $msg = ''; array_unshift($params, &$msg); $result +=!call_user_func_array(array($this, $method), $params); if ($msg) { $messages .= " *) $msg\n"; } } if ($messages) { $messages = rtrim($messages); fwrite(STDERR, "----------------\n$messages\n----------------"); } return $result; } /** * Get commit log message * * @return string */ protected function _getLogMessage() { if (null !== $this->_logMessage) { return $this->_logMessage; } $output = null; $cmd = "svnlook log -t '{$this->_transaction}' '{$this->_repository}'"; exec($cmd, $output); $this->_logMessage = implode($output); return $this->_logMessage; } /** * Get content of file from current transaction * * @param string $file * @return string * @throws Exception */ protected function _getFileContent($file) { $content = ''; $cmd = "svnlook cat -t '{$this->_transaction}' '{$this->_repository}' '$file' 2>&1"; // can't use exec() here because it will strip trailing spaces $handle = popen($cmd, 'r'); while (!feof($handle)) { $content .= fread($handle, 1024); } $return = pclose($handle); if (0 != $return) { throw new Exception($content, $return); } return $content; } /** * Get svn properties for file * * @param string $file * @return array */ protected function _getFileProps($file) { $props = array(); $cmd = "svnlook proplist -t '{$this->_transaction}' '{$this->_repository}' '$file'"; $output = null; exec($cmd, $output); foreach ($output as $line) { $propname = trim($line); $cmd = "svnlook propget -t '{$this->_transaction}' '{$this->_repository}' $propname" . " '$file'"; $output2 = null; exec($cmd, $output2); $propval = trim(implode($output2)); $props[] = "$propname=$propval"; } return $props; } /** * Get commit files list * * @return array filenames are keys and status letters are values */ protected function _getCommitList() { if (null !== $this->_commitList) { return $this->_commitList; } $output = null; $cmd = "svnlook changed -t '{$this->_transaction}' '{$this->_repository}'"; exec($cmd, $output); $list = array(); foreach ($output as $item) { $pos = strpos($item, ' '); $status = substr($item, 0, $pos); $file = trim(substr($item, $pos)); $list[$file] = $status; } $this->_commitList = $list; return $this->_commitList; } /** * Get array of modified and added files * * @param array $filetypes array of file types used for filtering * @return array */ protected function _getChangedFiles(array $filetypes = array()) { if (null === $this->_changedFiles) { $list = $this->_getCommitList(); $files = array(); foreach ($list as $file => $status) { if ('D' == $status || substr($file, -1) == DIRECTORY_SEPARATOR) { continue; } $files[] = $file; } $this->_changedFiles = $files; } $files = array(); foreach ($this->_changedFiles as $file) { $extension = pathinfo($file, PATHINFO_EXTENSION); $extension = strtolower($extension); if ($filetypes && !in_array($extension, $filetypes)) { continue; } $files[$file] = $extension; } return $files; } /** * trialing comma's in objects or arrays break internet explorer. * * @param string $msg * @return boolean */ protected function _testIECommaBug(&$msg) { $files = $this->_getChangedFiles(array('js')); foreach ($files as $file => $extension) { $content = $this->_getFileContent($file); if (preg_match('/,\s*[}\]]/s', $content)) { $msg = "You have a trailing , in your javascript file $file. This will cause problems in IE"; return false; } } return true; } /** * Check if log message validates length rules * * @param string $msg error messages placeholder * @param int $minlength minimum length of log message * @return bool */ protected function _testLogMessageLength(&$msg, $minlength = 1) { $length = strlen(trim($this->_getLogMessage())); if ($length < $minlength) { if ($minlength <= 1) { $msg = "Log message should not be empty. Please specify descriptive log message."; } else { $msg = "You log message is too short ($length). It should be at least $minlength" . " characters long."; } return false; } return true; } /** * Check if tabs are used as indents instead of spaces * * @param string $msg error messages placeholder * @param array $filetypes array of file types which should be tested * @return bool */ protected function _testTabIndents(&$msg, array $filetypes = array()) { $result = true; $files = $this->_getChangedFiles($filetypes); foreach ($files as $file => $extension) { $content = $this->_getFileContent($file); // check if file contains tabs $m = null; $tablines = preg_match('/^[ ]{2,}\}/m', $content, $m); if ($tablines) { $result = false; $msg .= "\t[$file] Indents with spaces found\n"; } } if (!$result) { $msg = rtrim($msg); $msg = "You should use tabs instead of spaces for indents. Following files violate this rule:\n$msg"; } return $result; } /** * Check if there are trailing spaces in files * * @param string $msg error messages placeholder * @param array $filetypes array of file types which should be tested * @return bool */ protected function _testTrailingSpaces(&$msg, array $filetypes = array()) { $result = true; $files = $this->_getChangedFiles($filetypes); foreach ($files as $file => $extension) { $content = $this->_getFileContent($file); // check if file contains trailing spaces $m = null; $spacelines = preg_match_all('/\\h$/m', $content, $m); if ($spacelines) { $result = false; $msg .= "\t[$file] Trailing spaces found on $spacelines lines\n"; } } if (!$result) { $msg = rtrim($msg); $msg = "Trailing spaces are not allowed. Following files violate this rule:\n$msg"; } return $result; } /** * Check if files have required svn properties set * * @param string $msg error messages placeholder * @param array $proprules svn properties rules * @return bool */ protected function _testSvnProperties(&$msg, array $proprules = array()) { $result = true; $files = $this->_getChangedFiles(array_keys($proprules)); foreach ($files as $file => $extension) { $props = $this->_getFileProps($file); foreach ($proprules[$extension] as $proprule) { if (!in_array($proprule, $props)) { $result = false; $msg .= "\t[$file]\n"; break; } } } if (!$result) { $rules = ''; foreach ($proprules as $filetype => $rule) { $rule = "*.$filetype = " . implode(',', $rule); $rules .= "\t$rule\n"; } $rules = rtrim($rules); $msg = rtrim($msg); $msg = "Some files are missing required svn properties.\n" . "\t= Rules =\n$rules\n" . "\t= Files violating these rules =\n$msg"; } return $result; } /** * Check if files violate eol rules * * @param string $msg error messages placeholder * @param array $eolrules * @return bool * @throws Exception */ protected function _testEol(&$msg, array $eolrules = array()) { $patterns = array( 'CR' => '(?:\\x0d(?!\\x0a))', 'LF' => '(?:(?<!\\x0d)\\x0a)', 'CRLF' => '(?:\\x0d\\x0a)', ); $result = true; $files = $this->_getChangedFiles(array_keys($eolrules)); foreach ($files as $file => $extension) { $content = $this->_getFileContent($file); $rules = explode(',', $eolrules[$extension]); // create reqular expression for checking eol violations foreach ($rules as $eol) { $eol = strtoupper(trim($eol)); if (!isset($patterns[$eol])) { throw new Exception("Unknown EOL: $eol"); } unset($patterns[$eol]); } $m = null; $pattern = '/' . implode('|', $patterns) . '/'; $badlines = preg_match_all($pattern, $content, $m); if ($badlines) { $result = false; $msg .= "\t[$file] Not allowed EOL characters found on $badlines lines\n"; } } if (!$result) { $msg = rtrim($msg); $msg = "There is some files violating EOL rules:\n$msg"; } return $result; } /** * Tests if the committed files pass PHP syntax checking * * @param string $msg error messages placeholder * @param array $filetypes array of file types which should be tested * @return bool */ protected function _testPHPSyntax(&$msg, array $filetypes = array()) { $result = true; $files = $this->_getChangedFiles($filetypes); $tempDir = sys_get_temp_dir(); foreach ($files as $file => $extension) { $content = $this->_getFileContent($file); $tempfile = tempnam($tempDir, "stax_"); file_put_contents($tempfile, $content); $tempfile = realpath($tempfile); //sort out the formatting of the filename $output = shell_exec(self::$PHP . ' -l "' . $tempfile . '"'); //try to find a parse error text and chop it off $syntaxErrorMsg = preg_replace("/Errors parsing.*$/", "", $output, -1, $count); if ($count > 0) { //found errors $result = false; $syntaxErrorMsg = str_replace($tempfile, $file, $syntaxErrorMsg); //replace temp filename with real filename $msg .= "\t[$file] PHP Syntax error in file. Message: $syntaxErrorMsg\n"; } unlink($tempfile); } return $result; } /** * extension -> standard */ protected function _testPHPCodeSniffer(&$msg, array $csrules = array()) { $result = true; $files = $this->_getChangedFiles(array_keys($csrules)); $tempDir = sys_get_temp_dir(); foreach ($files as $file => $extension) { $explodedFileName = explode('.', $file); $length = count($explodedFileName); if ($length > 2) { if (in_array($explodedFileName[$length - 1], array('php')) && in_array($explodedFileName[$length - 2], array('tpl', 'min')) ) { continue; } } if (!in_array($extension, array_keys($csrules))) { continue; } $standard = strtolower($csrules[$extension]); $content = $this->_getFileContent($file); $tempfile = tempnam($tempDir, "stax_"); file_put_contents($tempfile, $content); $tempfile = realpath($tempfile); switch ($standard) { case 'intermesh': $output = shell_exec( self::$PHPCS . ' -n --standard=/usr/share/php/PHP/CodeSniffer/Standards/Intermesh/ruleset.xml ' . $tempfile ); break; default: $output = shell_exec(self::$PHPCS . ' -n --standard=PEAR ' . $tempfile); } $count = preg_match_all("/ERROR/", $output, $matches); if ($count > 1) { $result = false; $syntaxErrorMsg = str_replace($tempfile, $file, $output); $msg .= "\t[$file] PHP CodeSmoffer error in file. Message: $syntaxErrorMsg\n"; } unlink($tempfile); } return $result; } }
创建文件:
/ usr / local / svn / testrepository / hooks / pre-commit
:
#!/usr/bin/php <?php require '/usr/local/svn/CommitTests.php'; putenv('PATH=/usr/bin'); new CommitTests($argv[1], $argv[2], array( 'LogMessageLength' => array(3), 'TabIndents' => array(array('php', 'php4', 'php5')), //'TrailingSpaces' => array(array('php', 'php4', 'php5', 'ini')), 'SvnProperties' => array(array( 'php' => array('svn:keywords=Author Id Revision', 'svn:eol-style=LF'), 'php4' => array('svn:keywords=Author Id Revision', 'svn:eol-style=LF'), 'php5' => array('svn:keywords=Author Id Revision', 'svn:eol-style=LF'), 'html' => array('svn:eol-style=LF'), 'js' => array('svn:eol-style=LF'), )), 'IECommaBug'=>array(), // 'PHPCodeSniffer'=>array(array( // 'php' => '', // )), 'PHPSyntax'=>array(array('php')), ));
设置正确的文件权限。
$ chown -R svn:svn /usr/local/svn
$ chmod u+x /usr/local/svn/testrepository/hooks/pre-commit
Subversion客户端配置
我们的预提交钩子需要在文件上设置某些svn属性。 我们可以修改客户端自动执行此操作。 如果您使用Linux,您可以在主目录中编辑“.subversion / config”,并添加或取消注释:
enable-auto-props = yes
在[auto-props]部分文件中添加:
*.php = svn:keywords=Author Id Revision;svn:eol-style=LF *.js = svn:keywords=Author Id Revision;svn:eol-style=LF
这样可以确保所有文件都具有Linux样式的换行符。 作者,编号和版本标签对于添加到文档块很有用。
设置现有文件所需的属性
修改现有存储库上的预提交钩子时,可能还没有设置这些属性的文件。 您可以在命令行中更改属性,如下所示:
$ svn propset svn:keywords “Author Id Revision” Example.php
$ svn propset svn: eol-style “LF” Example.php
使用第二个命令可能会遇到错误。 该文件可能有不同的换行符或不同的换行符的混合。 在这种情况下,您可以使用dos2unix将它们转换为linux换行符:
$ apt-get install dos2unix
$ dos2unix Example.php
存储库结构
我将描述我们使用我们的存储库的方式。 您可能需要采用不同的方法,但可能会帮助您。 在根我们创建:
- trunk
- branches
- stable-1.0
- stable-1.1
- tags
- stable-release-1.0.1
- stable-release-1.1.1
干线是一切开始的原始发展路线。 在某些时候,现在是发布稳定版本的时候了。 然后我们在“branches”文件夹中创建一个trunk的副本。 这就是我们所说的稳定分支。 以下命令将这样做:
$ svn cp svn+ssh://svn@svn.example.com/testrepository/trunk svn+ssh://svn@svn.example.com/testrepository/branches/stable-1.0 -m “Creating stable 1.0 branch from trunk”
现在我们可以做一些更改来发布一个版本或者一些bug修复。 当我们觉得代码准备发布时,我们可以创建一个“标签”。
$ svn cp svn+ssh://svn@svn.example.com/testrepository/branches/stable-1.0 svn+ssh://svn@svn.example.com/testrepository/tags/stable-release-1.0.1 -m “Creating stable release 1.0.1”
标签永远不会被修改!
当bug报告进来时,我们会将它们修复到stable-1.0分支中,并在更新发布时创建一个新的标签。
合并
稳定1.0中的错误修复应该也可以返回到中继。 我们可以通过将分支更改合并到主干中来实现。
进入中继线工作副本或签出副本:
$ svn co svn+ssh://svn@svn.example.com/testrepository/trunk
然后进入工作副本并输入合并命令:
$ svn merge svn+ssh://svn@svn.example.com/testrepository/branches/stable-1.0
这将对树干应用所有更改。 您可能需要解决一些冲突。 最终你可以提交合并:
$ svn commit -m 'merged revision 1 to 3 from stable-1.0' branch.
结束
这应该让你开始使用你自己的subversion版本库。 我希望这是有帮助的!