SVNKit --记一次获取SVN提交信息的经历

学会记录,时常反思,不断积累

前言

在工作中,遇到一个需求,需要监控研发人员每天提交的代码行数和代码bug数,关于代码的bug可以使用源码质量扫描工具sonar解决,但是对于svn每天提交的代码记录,哪种方式获取,对以后的数据筛选、存储、展示更有利,值得深思,后来,查阅资料,了解到了SVNKit 这个工具。

介绍

SVNKit (JavaSVN) 是一个纯 Java 的 SVN 客户端库,使用 SVNKit 无需安装任何 SVN 的客户端,支持各种操作系统。 这不是一个开源的类库,但你可以免费使用。它是Jar包形式,很好的可以引入到你的java工程中。

引入

SVNKIT官网:https://svnkit.com/

对于maven工程,在项目的pom中加入相关依赖:

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.tmatesoft.svnkit/svnkit -->
<dependency>
<groupId>org.tmatesoft.svnkit</groupId>
<artifactId>svnkit</artifactId>
<version>1.8.14</version>
</dependency>

运行示例

统计代码总提交行数.png

每次的提交信息.png

获取固定时间段内的提交记录,例如文件路径

说明:SVNLogEntry 这个实体类,保存了获取到的所有svn提交日志信息,包含,提交的文件,版本号,提交注释,提交人等,可以对SVNLogEntry进行过滤处理,然后对数据进行存储,用于展示。完整源码参考如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import org.tmatesoft.svn.core.ISVNLogEntryHandler;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.SVNLogEntry;
import org.tmatesoft.svn.core.SVNURL;
import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager;
import org.tmatesoft.svn.core.internal.io.dav.DAVRepositoryFactory;
import org.tmatesoft.svn.core.internal.io.fs.FSRepositoryFactory;
import org.tmatesoft.svn.core.internal.io.svn.SVNRepositoryFactoryImpl;
import org.tmatesoft.svn.core.io.SVNRepository;
import org.tmatesoft.svn.core.io.SVNRepositoryFactory;
import org.tmatesoft.svn.core.wc.SVNWCUtil;

public class ModerOption {

//svn地址
private static String url = "http://xxxx:xxx/svn/project/anka/trunk/Anka";
private static SVNRepository repository = null;


public void filterCommitHistory() throws Exception {
DAVRepositoryFactory.setup();
SVNRepositoryFactoryImpl.setup();
FSRepositoryFactory.setup();
try {
repository = SVNRepositoryFactory.create(SVNURL.parseURIEncoded(url));
}
catch (SVNException e) {
e.printStackTrace();
}
// 身份验证
ISVNAuthenticationManager authManager = SVNWCUtil.createDefaultAuthenticationManager("svn用户名","svn密码");
repository.setAuthenticationManager(authManager);


// 过滤条件
final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
final Date begin = format.parse("2019-04-13");
final Date end = format.parse("2019-05-14");
final String author = ""; //过滤提交人
long startRevision = 0;
long endRevision = -1;//表示最后一个版本
final List<String> history = new ArrayList<String>();
//String[] 为过滤的文件路径前缀,为空表示不进行过滤
repository.log(new String[]{""},
startRevision,
endRevision,
true,
true,
new ISVNLogEntryHandler() {
@Override
public void handleLogEntry(SVNLogEntry svnlogentry)
throws SVNException {
//依据提交时间进行过滤
if (svnlogentry.getDate().after(begin)
&& svnlogentry.getDate().before(end)) {
// 依据提交人过滤
if (!"".equals(author)) {
if (author.equals(svnlogentry.getAuthor())) {
fillResult(svnlogentry);
}
} else {
fillResult(svnlogentry);
}
}
}

public void fillResult(SVNLogEntry svnlogentry) {
//getChangedPaths为提交的历史记录MAP key为文件名,value为文件详情
history.addAll(svnlogentry.getChangedPaths().keySet());
}
});
for (String path : history) {
System.out.println(path);
}
}

public static void main(String[] args) throws Exception {
ModerOption test = new ModerOption();
test.filterCommitHistory();
}
}

统计提交文件的代码行数

思路:由于提交的文件,都会跟随着提交的形式,例如:增加,修改,删除等。这里统计增加和修改的文件,通过svn diff功能统计增加和修改的代码行数,源码参考如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;

import org.tmatesoft.svn.core.ISVNLogEntryHandler;
import org.tmatesoft.svn.core.SVNDirEntry;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.SVNLogEntry;
import org.tmatesoft.svn.core.SVNLogEntryPath;
import org.tmatesoft.svn.core.SVNNodeKind;
import org.tmatesoft.svn.core.SVNProperties;
import org.tmatesoft.svn.core.SVNURL;
import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager;
import org.tmatesoft.svn.core.internal.wc.DefaultSVNOptions;
import org.tmatesoft.svn.core.io.SVNRepository;
import org.tmatesoft.svn.core.io.SVNRepositoryFactory;
import org.tmatesoft.svn.core.wc.SVNDiffClient;
import org.tmatesoft.svn.core.wc.SVNLogClient;
import org.tmatesoft.svn.core.wc.SVNRevision;
import org.tmatesoft.svn.core.wc.SVNWCUtil;


public class SvnkitDemo {

private String userName = "svn用户名";
private String password = "svn密码";
//svn地址
private String urlString = "http://xxx:xxx/svn/project/anka/trunk/Anka";
boolean readonly = true;
private String tempDir = System.getProperty("java.io.tmpdir");
private DefaultSVNOptions options = SVNWCUtil.createDefaultOptions( readonly );
private Random random = new Random();

private SVNRepository repos;
private ISVNAuthenticationManager authManager;



public SvnkitDemo() {
try {
init();
} catch (SVNException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

public void init() throws SVNException{
authManager = SVNWCUtil.createDefaultAuthenticationManager(new File(tempDir+"/auth"), userName, password.toCharArray());
options.setDiffCommand("-x -w");
repos = SVNRepositoryFactory.create(SVNURL
.parseURIEncoded(urlString));
repos.setAuthenticationManager(authManager);
System.out.println("init completed");
}

/**获取一段时间内,所有的commit记录
* @param st 开始时间
* @param et 结束时间
* @return
* @throws SVNException
*/
public SVNLogEntry[] getLogByTime(Date st, Date et) throws SVNException{
long startRevision = repos.getDatedRevision(st);
long endRevision = repos.getDatedRevision(et);

@SuppressWarnings("unchecked")
Collection<SVNLogEntry> logEntries = repos.log(new String[]{""}, null,
startRevision, endRevision, true, true);
SVNLogEntry[] svnLogEntries = logEntries.toArray(new SVNLogEntry[0]);
return svnLogEntries;
}

/**获取版本比较日志,并存入临时文件
* @param startVersion
* @param endVersion
* @return
* @throws SVNException
* @throws IOException
*/
public File getChangeLog(long startVersion, long endVersion) throws SVNException, IOException{
SVNDiffClient diffClient = new SVNDiffClient(authManager, options);
diffClient.setGitDiffFormat(true);
File tempLogFile = null;
OutputStream outputStream = null;
String svnDiffFile = null;


do {
svnDiffFile = tempDir + "/svn_diff_file_"+startVersion+"_"+endVersion+"_"+random.nextInt(10000)+".txt";
tempLogFile = new File(svnDiffFile);
} while (tempLogFile != null && tempLogFile.exists());
try {
tempLogFile.createNewFile();
outputStream = new FileOutputStream(svnDiffFile);
diffClient.doDiff(SVNURL.parseURIEncoded(urlString),
SVNRevision.create(startVersion),
SVNURL.parseURIEncoded(urlString),
SVNRevision.create(endVersion),
org.tmatesoft.svn.core.SVNDepth.UNKNOWN, true, outputStream);
} catch (Exception e) {
e.printStackTrace();
}finally {
if(outputStream!=null)
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return tempLogFile;
}

/**分析变更的代码,统计代码增量
* @param file
* @return
* @throws Exception
*/
public int staticticsCodeAdd(File file) throws Exception{
System.out.println("开始统计修改代码行数");
FileReader fileReader = new FileReader(file);
BufferedReader in = new BufferedReader(fileReader);
int sum = 0;
String line = null;
StringBuffer buffer = new StringBuffer(1024);
boolean start = false;
while((line=in.readLine()) != null){
if(line.startsWith("Index:")){
if(start){
ChangeFile changeFile = parseChangeFile(buffer);
int oneSize = staticOneFileChange(changeFile);
System.out.println("filePath="+changeFile.getFilePath()+" changeType="+changeFile.getChangeType()+" addLines="+oneSize);
sum += oneSize;
buffer.setLength(0);
}
start = true;
}
buffer.append(line).append('\n');
}
if(buffer.length() > 0){
ChangeFile changeFile = parseChangeFile(buffer);
int oneSize = staticOneFileChange(changeFile);
System.out.println("filePath="+changeFile.getFilePath()+" changeType="+changeFile.getChangeType()+" addLines="+oneSize);
sum += oneSize;
}
in.close();
fileReader.close();
boolean deleteFile = file.delete();
System.out.println("-----delete file-----"+deleteFile);
return sum;
}

/**统计单个文件的增加行数,(先通过过滤器,如文件后缀、文件路径等等),也可根据修改类型来统计等,这里只统计增加或者修改的文件
* @param changeFile
* @return
*/
public int staticOneFileChange(ChangeFile changeFile){
char changeType = changeFile.getChangeType();
if(changeType == 'A'){
return countAddLine(changeFile.getFileContent());
}else if(changeType == 'M'){
return countAddLine(changeFile.getFileContent());
}
return 0;
}

/**解析单个文件变更日志
* @param str
* @return
*/
public ChangeFile parseChangeFile(StringBuffer str){
int index = str.indexOf("\n@@");
if(index > 0){
String header = str.substring(0, index);
String[] headers = header.split("\n");
String filePath = headers[0].substring(7);
char changeType = 'U';
boolean oldExist = !headers[2].endsWith("(nonexistent)");
boolean newExist = !headers[3].endsWith("(nonexistent)");
if(oldExist && !newExist){
changeType = 'D';
}else if(!oldExist && newExist){
changeType = 'A';
}else if(oldExist && newExist){
changeType = 'M';
}
int bodyIndex = str.indexOf("@@\n")+3;
String body = str.substring(bodyIndex);
ChangeFile changeFile = new ChangeFile(filePath, changeType, body);
return changeFile;
}else{
String[] headers = str.toString().split("\n");
String filePath = headers[0].substring(7);
ChangeFile changeFile = new ChangeFile(filePath, 'U', null);
return changeFile;
}
}

/**通过比较日志,统计以+号开头的非空行
* @param content
* @return
*/
public int countAddLine(String content){
int sum = 0;
if(content !=null){
content = '\n' + content +'\n';
char[] chars = content.toCharArray();
int len = chars.length;
//判断当前行是否以+号开头
boolean startPlus = false;
//判断当前行,是否为空行(忽略第一个字符为加号)
boolean notSpace = false;

for(int i=0;i<len;i++){
char ch = chars[i];
if(ch =='\n'){
//当当前行是+号开头,同时其它字符都不为空,则行数+1
if(startPlus && notSpace){
sum++;
notSpace = false;
}
//为下一行做准备,判断下一行是否以+头
if(i < len-1 && chars[i+1] == '+'){
startPlus = true;
//跳过下一个字符判断,因为已经判断了
i++;
}else{
startPlus = false;
}
}else if(startPlus && ch > ' '){//如果当前行以+开头才进行非空行判断
notSpace = true;
}
}
}

return sum;
}

/**统计一段时间内代码增加量
* @param st
* @param et
* @return
* @throws Exception
*/
public int staticticsCodeAddByTime(Date st, Date et) throws Exception{
int sum = 0;
SVNLogEntry[] logs = getLogByTime(st, et);
if(logs.length > 0){
long lastVersion = logs[0].getRevision()-1;
for(SVNLogEntry log:logs){
File logFile = getChangeLog(lastVersion, log.getRevision());
int addSize = staticticsCodeAdd(logFile);
sum+=addSize;
lastVersion = log.getRevision();

}
}
return sum;
}

/**获取某一版本有变动的文件路径
* @param version
* @return
* @throws SVNException
*/
static List<SVNLogEntryPath> result = new ArrayList<>();
public List<SVNLogEntryPath> getChangeFileList(long version) throws SVNException{

SVNLogClient logClient = new SVNLogClient( authManager, options );
SVNURL url = SVNURL.parseURIEncoded(urlString);
String[] paths = { "." };
SVNRevision pegRevision = SVNRevision.create( version );
SVNRevision startRevision = SVNRevision.create( version );
SVNRevision endRevision = SVNRevision.create( version );
boolean stopOnCopy = false;
boolean discoverChangedPaths = true;
long limit = 9999l;

ISVNLogEntryHandler handler = new ISVNLogEntryHandler() {

/**
* This method will process when doLog() is done
*/
@Override
public void handleLogEntry( SVNLogEntry logEntry ) throws SVNException {
System.out.println( "Author: " + logEntry.getAuthor() );
System.out.println( "Date: " + logEntry.getDate() );
System.out.println( "Message: " + logEntry.getMessage() );
System.out.println( "Revision: " + logEntry.getRevision() );
System.out.println("-------------------------");
Map<String, SVNLogEntryPath> maps = logEntry.getChangedPaths();
Set<Map.Entry<String, SVNLogEntryPath>> entries = maps.entrySet();
for(Map.Entry<String, SVNLogEntryPath> entry : entries){
//System.out.println(entry.getKey());
SVNLogEntryPath entryPath = entry.getValue();
result.add(entryPath);
System.out.println(entryPath.getType()+" "+entryPath.getPath());
}
}
};
// Do log
try {
logClient.doLog( url, paths, pegRevision, startRevision, endRevision, stopOnCopy, discoverChangedPaths, limit, handler );
}
catch ( SVNException e ) {
System.out.println( "Error in doLog() " );
e.printStackTrace();
}
return result;
}

/**获取指定文件内容
* @param url svn地址
* @return
*/
public String checkoutFileToString(String url){//"", -1, null
try {
SVNDirEntry entry = repos.getDir("", -1, false, null);
int size = (int)entry.getSize();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(size);
SVNProperties properties = new SVNProperties();
repos.getFile("", -1, properties, outputStream);
String doc = new String(outputStream.toByteArray(),Charset.forName("utf-8"));
return doc;
} catch (SVNException e) {
e.printStackTrace();
}
return null;
}

/**列出指定SVN 地址目录下的子目录
* @param url
* @return
* @throws SVNException
*/
public List<SVNDirEntry> listFolder(String url){
if(checkPath(url)==1){

try {
Collection<SVNDirEntry> list = repos.getDir("", -1, null, (List<SVNDirEntry>)null);
List<SVNDirEntry> dirs = new ArrayList<SVNDirEntry>(list.size());
dirs.addAll(list);
return dirs;
} catch (SVNException e) {
e.printStackTrace();
}

}
return null;
}

/**检查路径是否存在
* @param url
* @return 1:存在 0:不存在 -1:出错
*/
public int checkPath(String url){
SVNNodeKind nodeKind;
try {
nodeKind = repos.checkPath("", -1);
boolean result = nodeKind == SVNNodeKind.NONE ? false : true;
if(result) return 1;
} catch (SVNException e) {
e.printStackTrace();
return -1;
}
return 0;
}



public static void main(String[] args) throws ParseException {
SvnkitDemo demo = new SvnkitDemo();
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
Date now = format.parse("2019-04-06");
Date twoDayAgo = format.parse("2019-05-14");
try {
int sum = demo.staticticsCodeAddByTime(now, twoDayAgo);
System.out.println("sum="+sum);
demo.getChangeFileList(128597L);
demo.getChangeFileList(128599L);
demo.getChangeFileList(128621L);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}

}

class ChangeFile {

private String filePath;
private String fileType;

/**A表示增加文件,M表示修改文件,D表示删除文件,U表示末知
*
*/
private Character changeType;
private String fileContent;




public ChangeFile() {
}
public ChangeFile(String filePath) {
this.filePath = filePath;
this.fileType = getFileTypeFromPath(filePath);
}
public ChangeFile(String filePath, Character changeType, String fileContent) {
this.filePath = filePath;
this.changeType = changeType;
this.fileContent = fileContent;
this.fileType = getFileTypeFromPath(filePath);
}
public String getFilePath() {
return filePath;
}
public void setFilePath(String filePath) {
this.filePath = filePath;
}
public String getFileType() {
return fileType;
}
public void setFileType(String fileType) {
this.fileType = fileType;
}
public Character getChangeType() {
return changeType;
}
public void setChangeType(Character changeType) {
this.changeType = changeType;
}
public String getFileContent() {
return fileContent;
}
public void setFileContent(String fileContent) {
this.fileContent = fileContent;
}

private static String getFileTypeFromPath(String path) {
String FileType = "";
int idx = path.lastIndexOf(".");
if (idx > -1) {
FileType = path.substring(idx + 1).trim().toLowerCase();
}
return FileType;
}
}

这次svn提交信息的获取经历,就记录这些,希望对有需要的朋友有所帮助。

分享,也是自我回顾的开始。

宁波市象山县松兰山-记一次海滩之旅

离别,是为了更好的遇见,也可能是为了再重逢

趁着五一小长假,来了一次心灵的旅行,5月4日,我来到了宁波市象山县松兰山风景区,写下这篇游记,除了是对同事的承诺,也是为了给需要的朋友们。

路线(上海出发)
1.上海虹桥站-宁波南站 (乘坐高铁,约2小时)票价144元
2.宁波客运南站-象山客运中心 (乘坐大巴,宁波客运南站在高铁站外面,全程约1小时40分钟,车次很多,30分钟内一班) 票价40元左右
3.象山111路 象山客运中心-松兰山风景区 (乘坐公共汽车,公共汽车站在象山客运中心附近,全程约40分钟,车次很多,15分钟内一班)票价一元

住宿
松兰山风景区有很多的民宿和客栈,节假日标间的价格在200元/晚,平时会很便宜。景区内也有酒店,价格较高。

海滩
从入口处进入,门票成人30元,学生有半价优惠15元,需要学生证。

景区入口

穿过入口处的隧道

入口隧道

进入眼帘的是一片沙滩,这里来游玩的旅客较多,旅游项目有海上游艇和摩托艇,还要海上轮车。价格50元/人次。沙滩上有包裹寄存处,需要30元,外加20元的押金。

入口沙滩

入口处的南沙滩,可以游玩一下,建议携带拖鞋,沙滩上的小贝壳比较硌脚,能捡到许多小的贝壳,平时,会开放东沙滩浴场,在海上的一小块区域可以游泳。

入口南沙滩

入口处的南沙滩其实只是一小块区域,不过这里的游客较多,娱乐项目也较多,继续向东边走,其实有东沙滩,海面更广,人较少,还有岛上栈道,适合拍照和看风景。

松兰山景区示意图

岛上栈道

栈道风景

东沙滩风景

浪花一朵朵

东沙滩海面

小贝壳

海鲜
当游玩了一天,可以去民宿附近,那里会有很多的海鲜排挡,吃饭的人很多,氛围不错。

爆炒望潮

不知名的鱼,很嫩,很嫩

贝类大杂烩

排挡的价格一般,望潮(小鱿鱼)50元左右一盘,鱼40多一盘,贝类大杂烩120元左右,还有一盘家常豆腐,两瓶啤酒,加上不限量的米饭,总共吃饭在200多元,如果是出来游玩体验的话,还是比较值得的,贝类的量挺多的。

这次出来游玩,在沙滩上捡了很多的小贝壳,喜欢的朋友,可以自己去捡,哈哈哈

小贝壳们

最后,希望这篇文章对想去松兰山的朋友,一些帮助。

跟着心走,哪里都是旅行,祖国风光,山川湖海,旅行的意义,不仅仅是为了遇见,也是为了洗涤心灵,更好的前行,加油!

面朝大海,春暖花开

旅行,在路上。