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提交信息的获取经历,就记录这些,希望对有需要的朋友有所帮助。

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