Skip to content

Svn 常用操作

服务端

下载

VisualSVN server 下载链接

文件定义

svn的存储地址中,文件夹表明是仓库,groups.conf是组文件,htpasswd是用户文件 svndir

htpasswd 文件

文件内容如下,如果有 #disabled# ,说明该用户现在被禁用。每行内容由3部分组成:是否禁用(不一定有),用户名,密码(加密,php可以用 password_hash($password, PASSWORD_BCRYPT) 来实现)

  • 用户被删除时,需要将所有repo authz 文件中有关这个用户的权限行删除
bash
admin:$2y$10$D3Ut76yRlWoIZc/XVs7ma.LGldlSUBeDG86mlaWghrh3nTt5ZXlOO
liubei:$2y$10$6D5G0VvB89sDi.qUG8MkSuDWx9IoP2Smhp/tTcGyMXs8G0NAOa3zq
guanyu:$2y$10$KV0ASnkyNpypniD2WKY9tur9b1euMImvE62kKALMPdZ3WnLO/LhAO
zhangfei:$2y$10$M5rSt2PulD9e11Q0luq50emBiyrTrjSPyVp4FWkQRnwuo.cM6CTsy
zhaoyun:$2y$10$HVKbgiY/eKvgRUcRSnKpkOP7VS4T61S7urv5x0//G1UM9JlLYpwOy
test1:$2y$10$wfzDTodTSVdy0nScu5BaOOLqfSJkTtocAG86e8EqQl/aIu.xw1k2W
test2:$2y$10$fZA.4PxdEq/CVo6cSnxBeunbq6E2wGiwmdS2iu6YiHEdZ1wQUP.Ka
#disabled#te:$2y$10$2BSqMmF5gaWWXxQx7hThmeAFRl8vIfEr9ypWy/FWH3GZwFWzQL4vy

一些可供操作的函数

php
// 获取用户列表
public function getUserList()
{
    if(!is_readable($this->htpasswdFile) || !is_writable($this->htpasswdFile))  return array();

    $lines = file($this->htpasswdFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
    foreach($lines as $index => $line)
    {
        $disabled = strpos($line, '#disabled#') === 0;
        $line = str_replace('#disabled#', '', $line);

        list($account, $password) = explode(':', $line, 2);
        $userList[$account] = (object)array('id' => $index + 1, 'account' => $account, 'password' => $password, 'status' => $disabled ? 'disabled' : 'normal');
    }
    return $userList;
}

// 如果用户不存在,创建,否则编辑
public function saveUser($account, $password)
{
    if(!is_readable($this->htpasswdFile) || !is_writable($this->htpasswdFile))  return false;

    $userList     = $this->getUserList();
    $hashPassword = password_hash($password, PASSWORD_BCRYPT);
    if(isset($userList[$account]))
    {
        $userList[$account]->password = $hashPassword;
    }
    else
    {
        $userList[$account] = (object)array('id' => count($userList) + 1, 'account' => $account, 'password' => $hashPassword, 'status' => 'normal');
    }

    $this->writePasswdFile($userList);
}

// 删除用户
public function deleteUser($account)
{
    if(!is_readable($this->htpasswdFile) || !is_writable($this->htpasswdFile))  return false;

    $userList = $this->getUserList();
    if(isset($userList[$account])) unset($userList[$account]);

    $this->writePasswdFile($userList);
}

// 全量写入user
public function writePasswdFile($userList)
{
    $file = fopen($this->htpasswdFile, 'w');

    foreach($userList as $user)
    {
        $info  = $user->status == 'disabled' ? '#disabled#' : '';
        $info .= "{$user->account}:{$user->password}";
        fwrite($file, $info . PHP_EOL);
    }
    fclose($file);
}

groups.conf 文件

有可能有注释,文件内容以[groups] 开头,每一行是分组名称和分组内容的key value 对,以等号连接,分组内容以逗号分割,如果带有 @,则说明这是一个分组

  • 分组的成员不能有自己,否则会导致循环引用,如p2=@p2是非法的
  • 分组被删除时,需要将所有repo authz 文件中有关这个分组的权限行删除
bash
# This configuration file stores VisualSVN Server authorization settings. The
# file is autogenerated by VisualSVN Server and is not intended to be edited
# manually.
#
# DO NOT EDIT THIS FILE MANUALLY!
#
# Use VisualSVN Server Manager or VisualSVN Server PowerShell module to
# configure access permissions on your server.
[groups]
p2=admin,liubei,guanyu,zhangfei,@p2,@p_3,@dev
p_3=admin
dev=
lai=te

一些可供操作的函数

php
// 获取分组名称和成员键值对
public function getGroupMemberPairs()
{
    if(!is_readable($this->groupsFile) || !is_writable($this->groupsFile))  return array();

    $lines            = file($this->groupsFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
    $groupMemberPairs = array();
    foreach($lines as $line)
    {
        $line = trim($line);
        if(strpos($line, '#') === 0) continue;
        if($line == '[groups]') continue;

        list($name, $member) = array_map('trim', explode('=', $line, 2));
        $groupMemberPairs[$name] = $member;
    }

    return $groupMemberPairs;
}

// 如果分组不存在,创建,否则编辑
public function saveGroup($groupName, $selectGroup = array(), $selectUser = array())
{
    $groupMemberPairs = $this->getGroupMemberPairs();

    if($selectGroup || $selectUser)
    {
        foreach($selectGroup as $index => $group) $selectGroup[$index] = '@' . $group;
        $groupMemberPairs[$groupName] = implode(',', array_merge($selectUser, $selectGroup));
    }
    else
    {
        $groupMemberPairs[$groupName] = '';
    }

    $this->writeGroupFile($groupMemberPairs);
}

// 重命名分组名称
public function renameGroup($groupName, $afterName)
{
    $groupMemberPairs = $this->getGroupMemberPairs();

    if(isset($groupMemberPairs[$groupName]))
    {
        $groupMemberPairs[$afterName] = $groupMemberPairs[$groupName];
        unset($groupMemberPairs[$groupName]);
    }
    else
    {
        $groupMemberPairs[$afterName] = '';
    }

    $this->writeGroupFile($groupMemberPairs);
}

// 删除分组
public function deleteGroup($groupName)
{
    $groupMemberPairs = $this->getGroupMemberPairs();
    unset($groupMemberPairs[$groupName]);
    $this->svn->writeGroupFile($groupMemberPairs);
}

// 全量写入分组信息
public function writeGroupFile($groupMemberPairs)
{
    $file = fopen($this->groupsFile, 'w');

    fwrite($file, "[groups]" . PHP_EOL);
    foreach($groupMemberPairs as $name => $member) fwrite($file, "{$name}={$member}" . PHP_EOL);
    fclose($file);
}

VisualSVN-SvnAuthz.ini 版本库内的权限文件,在一个仓库目录的conf文件夹内

内容一般如下,/是根目录的权限,其他是特定目录的权限,权限默认只有三种

  • rw 读写
  • r 只读
  • ''(空字符串) 无权限
bash
[/]
*=rw
admin=rw
@1=rw
[/a]
*=rw
admin=rw
@p2=rw
[/a/a1]
guanyu=rw
liubei=rw

存在继承机制,比如其他目录默认继承根目录,可以重定义继承的权限但无法删除,比如/a/a1 的权限有:

bash
guanyu(rw)
liubei(rw)
everyone(inherit rw)
admin(inherit rw)
@p2(inherit rw)
@1(inherit rw)

一些可供操作的函数

php
// 解析authz 文件的权限
/* e.g
Array
(
    [/] => Array
        (
            [*] => rw
            [admin] => rw
            [@1] => rw
        )

    [/a] => Array
        (
            [*] => rw
            [admin] => rw
            [@p2] => rw
        )

    [/a/a1] => Array
        (
            [guanyu] => rw
            [liubei] => rw
        )

)
*/
public function parseRepoAuthFile($authPath)
{
    if(!is_readable($authPath)) return array();

    $config = [];
    $section = '';

    $lines = file($authPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
    foreach($lines as $line)
    {
        $line = trim($line);

        if(strpos($line, '#') === 0) continue;

        // 解析 [section]
        if(preg_match('/^\[(.+)\]$/', $line, $matches))
        {
            $section = $matches[1];
            $config[$section] = [];
        }
        // 解析 key=value
        elseif(strpos($line, '=') !== false)
        {
            list($key, $value) = array_map('trim', explode('=', $line, 2));
            $config[$section][$key] = $value;
        }
    }
    return $config;
}

// 获得一个路径下的权限,返回一个数组,第一个数组为自身的权限,第二个数组为继承的权限
/* e.g
如果传入path = /a/a1
$selfPrivs = array
(
    [guanyu] => rw
    [liubei] => rw
)
$inPrivs = array
(
    [*] => rw
    [admin] => rw
    [@p2] => rw
    [@1] => rw
)
*/
public function getRepoPathAuth($authPath, $path)
{
    $permissions = $this->parseRepoAuthFile($authPath);

    $currentPath = $path;
    $selfPrivs   = array(); //自己的权限
    $inPrivs     = array(); //继承的权限

    $mergePriv = function($cpath, $allpriv, $inpriv)
    {
        if(isset($allpriv[$cpath]))
        {
            foreach($allpriv[$cpath] as $user => $perm)
            {
                // 如果已经有了权限,就说明离path近的目录已经单独设置了,此时用路程近的目录权限,不要再替换
                if(isset($inpriv[$user])) continue;
                $inpriv[$user] = $perm;
            }
        }
        return $inpriv;
    };

    $selfPrivs = $mergePriv($currentPath, $permissions, $selfPrivs);
    while($currentPath !== "/")
    {
        $currentPath = dirname($currentPath);
        if($currentPath == DIRECTORY_SEPARATOR) $currentPath = '/';
        $inPrivs = $mergePriv($currentPath, $permissions, $inPrivs);
    }

    // 如果已经有自定义的权限了,且还存在继承的权限,先用自定义的,不要继承的
    foreach($inPrivs as $user => $perm)
    {
        if(isset($selfPrivs[$user])) unset($inPrivs[$user]);
    }

    return array($selfPrivs, $inPrivs);
}

// 写入全量的authz 配置
public function writeRepoAnthFile($authPath, $config)
{
    if(!file_exists($authPath) && !touch($authPath)) return false;
    if(!is_writable($authPath)) return false;

    $file = fopen($authPath, 'w');
    foreach($config as $dir => $privs)
    {
        if($dir != '/') rtrim($dir, '/');
        fwrite($file, "[$dir]" . PHP_EOL);
        foreach($privs as $user => $priv)
        {
            if($priv == 'inherit') continue; // 如果是继承,不写入具体内容,visual svn server会自动处理
            if($priv == '' || $priv == 'no') $priv = '';// 如果是no access,设置为''空即可,由于zui picker不能设置空的键值(故设置为no),并且可能存在原先写入的文件中就有空的情况,所以这里坐转换
            fwrite($file, "{$user}={$priv}" . PHP_EOL);
        }
    }
    fclose($file);
}

客户端

TortoiseSVN 下载链接

服务端教程

安装好 VisualSVN server后,可以在搜索中进行搜索并启动

界面介绍

login

创建版本库

login

获取版本库url

login

进入Setting 界面

canshu

查看svn的仓库存储地址

store

查看svn的ip和port

ip通过cmdipconfig来查看,port通过查看svn的网络查看 ipport 这样就可以用url来操作svn: svn list http://10.0.0.167:81/<relativePath>

客户端教程

下载安装完小乌龟后,右键菜单会出现 TortoiseSVN,如果没有或者没有想要的选项且系统是win11,使用右键的显示更多选项 login

使用 check 操作来同步 SVN 项目,url在上文提到guo loginlogin

使用 svn update 来拉取最新代码,如果有提交,使用 svn commit 来提交 login

Linux下使用Shell远程操作SVN

安装客户端并尝试

bash
sudo apt install subversion

# 看一下仓库目录试试水
svn list <remote url> --non-interactive --username=<user> --password <password>

报错SSL认证不通过解决方案

方案1

bash
svn: E170013: Unable to connect to a repository at URL '<remote url>'
svn: E230001: Server SSL certificate verification failed: certificate issued for a different hostname, issuer is not trusted

在本地开发的时候,往往给出的远程地址也是本地起的,比如 linux 下开发,Windows 上装了 visual svn server,那么此时在 linux 中"远程"调用 Windowssvn,就会出现这种情况,此时先执行

bash
svn list <remote url>

会得到如下结果,选择p,永久忽略就行

bash
Error validating server certificate for '<remote url>':
 - The certificate is not issued by a trusted authority. Use the
   fingerprint to validate the certificate manually!
 - The certificate hostname does not match.
Certificate information:
 - Hostname: Liberty
 - Valid: from Jan 17 05:38:59 2025 GMT until Jan 15 05:38:59 2035 GMT
 - Issuer: Liberty
 - Fingerprint: B1:31:5D:99:17:65:4A:0F:DD:61:A9:F4:1F:9C:AB:31:25:6E:E2:22
(R)eject, accept (t)emporarily or accept (p)ermanently?

但这个方案只能在使用命令行的时候解决,如果是 php exec 则无法避免

方案2

使用信任证书参数

bash
svn list --trust-server-cert-failures=unknown-ca,cn-mismatch,expired,not-yet-valid,other <remote url>

可以解决 php exec 的报错

方案3

取消https,改用http loginlogin

常用命令

  • svn 命令后面可以直接跟远程 url 或者是本地 checkout 出来的副本路径 svn <command> <url>/<path>
  • svn命令中 @ 是保留字,代表版本,如果<url>/<path> 中有@ 符号,那么需要在后面再跟一个 @,比如 svn delete <url>/repo/module/a@.php@

后缀

bash
## 不要交互提示
--non-interactive
## 设置用户和密码
--username=<username> --password <password>
## linux常用的输出错误
2>&1
## 添加评论,最好用双引号,单引号在windows上有问题
-m "<comment>"
## 举个离职
svn mkdir <url> -m "<comment>" --non-interactive --username=<username> --password <password> 2>&1

svn

svn list(svn ls) 获取列表

bash
# 展示详细信息,包括版本号、作者、日期、大小
svn list -v <url>
# 递归列出所有子目录的内容
svn list -R <url>
# 以xml格式输出,可以避免字符集问题,正确输出中文
svn list --xml <url>

svn mkdir 创建文件夹

bash
# 创建文件夹
svn mkdir <url> -m "<comment>"

svn move(svn mv) 移动、重命名

bash
## 移动文件或目录,无法跨版本库迁移
svn move <beforeURL> <afterURL> -m "<comment>"
## 如果只有文件或者目录名字不一样,则是重命名功能,举例
svn move <url>/repo/module/action/contro.php <url>/repo/module/action/control.php -m "<comment>"

svn copy 常用于打标签

bash
## 通过复制来实现打tag
svn copy <url> <tagUrl> -m "tag"

svn export 导出svn目录或文件

bash
## 导出,--force参数确保如果文件或目录已经存在exportPath时,覆盖导出
svn export --force <url> <exportPath>

svnadmin

svnadmin hotcopy

bash
## 热拷贝,复制当前版本库的所有信息,直接出来一个一样的拷贝,是对目录的操作
svnadmin hotcopy <svnPath> <backPath>

svnadmin dump

bash
## 备份版本库,类似mysqldump,选择一个版本库路径然后生成备份文件
svnadmin dump <svnPath> > <filePath>

svnadmin load

bash
## 加载一个dump文件,必须是新建的版本库的path,不能有提交
svnadmin load <svnPath> < <filePath>