SVN操作
1. SVN 介绍
SVN 全称 Subversion,是一个版本控制系统(Version Control System,VCS),主要用于管理文件和目录的变更。它允许多个用户协作开发项目,跟踪文件的历史记录,并支持分支和合并操作。
SVN 属于集中式(centralized)版本控制:整个项目只有一份权威仓库放在服务器上,所有人的工作副本都从它检出,也都向它提交。这一点和 Git 那种每个克隆都是完整仓库的分布式(distributed)模型不同——离了服务器,SVN 的工作副本既看不了历史,也提交不了东西。
1.1 主要作用
- 记录历史:每一次提交都会在仓库里留下一个不可变的版本,之后可以随时查看、对比,或者把某个文件回退到任意历史版本
- 多人协作:所有人共用一个仓库,通过提交和更新交换代码,服务器负责协调谁改动了哪些文件
- 分支与标签:可以用很低的成本把一份代码复制成分支(branch)或标签(tag),支撑并行开发和版本归档
- 权限控制:可以按用户、按目录设置只读或可写的访问权限
- 统一入口:无论团队里有多少人,仓库地址只有一个,新成员检出之后拿到的就是完整代码
1.2 基本概念
- Repository(源代码库/仓库):服务端存放所有代码和历史记录的地方,是整个项目唯一的数据源
- Working Copy(工作副本):检出之后出现在本地的那份文件目录,你的编辑都发生在它里面
- Revision(版本号):仓库级别的递增编号,一次成功的提交会让所有文件共享同一个新版本号
- Checkout(提取):从仓库中获取代码到本地工作副本
- Commit(提交):将本地修改的代码提交到仓库,成功后仓库的版本号会前进一位
- Update(更新):将仓库中的最新代码更新到本地工作副本
- trunk / branches / tags:约定俗成的三个顶层目录,分别放主线、并行开发的分支、以及已发布的里程碑快照
- Conflict(冲突):同一个文件的同一处被两个人改过,仓库无法自动合并,需要人工介入
可以通过以下流程快速理解:
开始什么都没有,小明新建了一个 Repository,并写了一些代码 Commit 到了 Repository 中。小明的同事小红想要获取代码,就需要 Checkout 代码到本地工作副本。小红修改了代码后,Commit 到 Repository 中。小明想要获取小红的修改,就需要 Update 代码到本地工作副本。
2. SVN Server 的配置
服务端可以跑在 Linux 或 Windows 上。Linux 下一般直接用官方命令行工具建库,再用 svnserve 提供服务;Windows 下更常见的是可视化的一体化安装包 VisualSVN Server,建仓库、加用户、分权限都可以在图形界面操作。
2.1 创建仓库
# 安装 subversion(Debian / Ubuntu)
sudo apt install subversion
# 建一个用来存放所有仓库的根目录
sudo mkdir -p /var/svn
# 在根目录下创建名为 myrepo 的仓库
sudo svnadmin create /var/svn/myrepo
执行完,/var/svn/myrepo 下会生成仓库的完整内部结构:
myrepo/
├── conf/ # 访问控制配置
│ ├── svnserve.conf
│ ├── passwd
│ └── authz
├── db/ # 版本数据的实际存储位置
├── hooks/ # 提交前后触发的钩子脚本
└── format
注意:不要手动去改
db/里的内容,仓库的所有变更都应该通过svnadmin或客户端命令完成,直接改底层文件会把仓库弄坏。
2.2 配置访问权限
三个配置文件分工明确:svnserve.conf 决定"要不要鉴权、去哪儿找账号",passwd 存账号密码,authz 决定"谁能读写哪个目录"。
# /var/svn/myrepo/conf/svnserve.conf
[general]
# 匿名用户无任何权限
anon-access = none
# 已认证用户可读写
auth-access = write
# 账号文件
password-db = passwd
# 权限文件
authz-db = authz
# 认证域名称,客户端按它缓存凭据
realm = myrepo
# /var/svn/myrepo/conf/passwd
[users]
alice = alice123
bob = bob123
# /var/svn/myrepo/conf/authz
[groups]
dev = alice, bob
# 仓库根目录
[/]
# 其他所有人无权限
* =
# dev 组可读写
@dev = rw
注:
passwd里的密码是明文存储的,只适合内网自用。生产环境更常见的做法是把仓库交给 Apache + HTTP 认证托管,或者接 LDAP 统一认证。
2.3 启动服务与目录规划
# 以守护进程方式启动,-r 指定仓库根目录
svnserve -d -r /var/svn
# 确认服务监听在 3690 端口
sudo ss -lntp | grep 3690
Windows 下也可以直接用 svnserve,只是路径写法不同:
svnserve -d -r D:\svn
服务起来之后,客户端就可以用 svn://服务器IP/myrepo 这个地址访问仓库了。
仓库内部的目录规范建议按惯例来划分,这样后续创建分支、发布版本都不会乱:
myrepo/
├── trunk/ # 主线开发
├── branches/ # 并行开发的分支
└── tags/ # 发布快照
这三条目录本身也是版本库里的普通目录,用 svn mkdir 就能建:
svn mkdir svn://127.0.0.1/myrepo/trunk -m "创建 trunk 目录"
3. SVN Client 的使用
客户端命令统一以 svn 开头,每个子命令都有一个两三字母的简写形式。
3.1 检出与更新
# 检出 trunk 到当前目录下的 myproject 文件夹
svn checkout svn://127.0.0.1/myrepo/trunk myproject
# 简写形式
svn co svn://127.0.0.1/myrepo/trunk myproject
# 进入工作副本
cd myproject
# 随时拉取服务器上的新版本
svn update
检出之后目录里会多出一个隐藏的 .svn 目录,它记录着当前工作副本对应的仓库地址和每个文件的基准版本。从 SVN 1.7 开始,.svn 只出现在工作副本的根部,不要手动删掉它。
svn update 的输出里,每一行开头的字母代表这个文件发生了什么:
| 字母 | 含义 |
|---|---|
| U | 文件被更新(Update) |
| G | 你的修改与服务器修改被自动合并(merGed) |
| C | 出现冲突(Conflict) |
| A | 新增文件(Added) |
| D | 文件被删除(Deleted) |
| R | 文件被替换(Replaced) |
| ? | 未纳入版本控制的文件 |
3.2 修改与提交
# 新建文件后必须先纳入版本控制,服务器才会记录它
svn add main.cpp
# 删除文件也要用命令,而不是直接删掉
svn delete old.cpp
# 查看当前工作副本有哪些改动
svn status
# 查看具体改了哪些行
svn diff
# 把改动提交到仓库
svn commit -m "实现开平方函数"
svn add 和 svn delete 只是给文件打上"待新增""待删除"的标记,真正的变化发生在 svn commit 的那一刻。提交时可以整个工作副本一起提交,也可以只提交指定路径:
# 只提交一个文件
svn commit src/main.cpp -m "修正边界判断"
提交前想反悔也很简单:
# 撤销单个文件的本地修改,恢复成基准版本
svn revert main.cpp
# 递归撤销整个目录
svn revert -R .
注意:
svn revert是把文件退回到上次更新后的状态,被撤销的改动不会被记入历史,也找不回来了。
3.3 常用命令一览
| 命令 | 简写 | 作用 |
|---|---|---|
svn checkout URL [路径] |
svn co |
从仓库检出工作副本 |
svn update [路径] |
svn up |
把仓库的新版本合并到工作副本 |
svn commit -m "说明" |
svn ci |
提交工作副本的修改 |
svn status |
svn st |
查看工作副本的改动概况 |
svn diff |
— | 查看逐行差异 |
svn add 文件 |
— | 把文件纳入版本控制 |
svn delete 文件 |
svn rm / svn del |
标记删除文件 |
svn move 源 目标 |
svn mv |
移动或重命名,保留历史 |
svn copy 源 目标 |
svn cp |
复制,创建分支和标签也用它 |
svn revert 文件 |
— | 撤销本地修改 |
svn log |
— | 查看提交历史 |
svn info |
— | 查看工作副本对应的地址和版本号 |
svn resolve --accept=... |
— | 标记冲突已解决 |
3.4 一个完整的协作示例
回到开头小明和小红的场景,双方实际敲的命令是这样。
小明建好仓库后把代码传上去:
svn checkout svn://127.0.0.1/myrepo/trunk myproject
# 在新文件里写代码
svn add main.cpp
svn commit -m "初始化项目"
小红从同一个地址检出、修改、提交:
svn checkout svn://127.0.0.1/myrepo/trunk project
# 修改代码
svn commit -m "添加日志输出"
小明想拿到小红的改动,只要在自己的工作副本里更新一次:
cd myproject
svn update
创建分支同样只是一条普通的复制命令,之后用 svn switch 就能把工作副本切换到分支上:
# 从 trunk 复制出一个功能分支
svn copy svn://127.0.0.1/myrepo/trunk svn://127.0.0.1/myrepo/branches/feature-login -m "创建登录功能分支"
# 把当前工作副本切换到该分支
svn switch svn://127.0.0.1/myrepo/branches/feature-login
4. 版本冲突问题
4.1 冲突是怎么产生的
关键在于基准版本。你检出文件时,它对应仓库的某一个版本;在你编辑期间,如果小红提交了同一个文件,服务器上的版本号就前进了。这时你提交,服务器会发现你修改所基于的已经不是最新版本,于是拒绝这次提交,并要求你先更新。
Sending trunk/config.ini
Transmitting file data .
svn: E155011: Commit failed (details follow):
svn: E170004: File '/trunk/config.ini' is out of date
如果你和小红改的是不同的文件,或者改的是同一个文件的不同区域,svn update 会自动把两边合并起来(输出 U 或 G),并不会打扰你。只有当两人的修改落在同一文件的同一处时,SVN 才无法判断该保留哪一份,于是把文件标记为冲突。
4.2 冲突的表现
svn update 遇到冲突时会打印一个 C:
C main.cpp
Updated to revision 8.
Summary of conflicts:
Text conflicts: 1
同时工作副本里会多出几个临时文件:
myproject/
├── main.cpp # 被写入了冲突标记
├── main.cpp.mine # 你自己的版本
├── main.cpp.r7 # 你更新前所基于的版本
└── main.cpp.r8 # 服务器上的最新版本
被标记的 main.cpp 里会插入三行分隔符,把双方的修改直接摆在同一个文件中:
<<<<<<< .mine
std::cout << "hello" << std::endl;
=======
std::cout << "Hello, world!" << std::endl;
>>>>>>> .r8
4.3 解决冲突
解决冲突是纯手工的编辑工作:打开文件,把 <<<<<<<、=======、>>>>>>> 三行连同不需要的那一段一起删掉,留下最终想要的内容。改完之后告诉 SVN 这个文件已经处理好了:
svn resolve --accept=working main.cpp
svn commit -m "合并 hello 输出"
--accept 决定在冲突处用哪一份内容,可选值如下:
| 取值 | 含义 |
|---|---|
working |
以你手工编辑后的当前内容为准(最常用) |
mine-full |
整个文件都用自己的版本 |
theirs-full |
整个文件都用服务器上的版本 |
mine-conflict |
只在冲突块里用自己那部分 |
theirs-conflict |
只在冲突块里用服务器那部分 |
base |
退回到更新前所基于的那个版本 |
postpone |
暂时不处理,保留冲突状态 |
注:
svn resolve只负责去掉文件的冲突标记,真正的合并结果还要再执行一次svn commit才会进入仓库。
如果看过之后决定放弃自己的改动,直接用服务器版本即可:
svn revert main.cpp
svn update
4.4 减少冲突
- 提交前先
svn update,让本地版本尽量贴近服务器的最新状态 - 提交粒度尽量小、频率尽量高,一次提交只做一件事,降低和别人改到同一处的概率
- 团队内约定好分工,避免多人长时间同时编辑同一个文件
- 对图片等无法逐行合并的二进制资源,给文件设置
svn:needs-lock属性,未加锁时文件为只读,必须先svn lock才能编辑,提交后自动解锁:
svn propset svn:needs-lock 1 logo.png



