基于SVNkit的项目代码贡献量统计

SVNkit

SVNKit (JavaSVN) 是一个纯 Java 的 SVN 客户端库,使用 SVNKit 无需安装任何 SVN 的客户端,支持各种操作系统。

思路

  1. checkout 仓库
  2. 筛选需要统计的文件
  3. 遍历每个文件的修改版本
    1. 获取文件的修改版本和各版本的修改作者
    2. 比对各版本相对前一个版本的代码变化数据
    3. 修改成员的代码贡献的各个参数

实现

环境准备

LineNumber.java

一个用于记录某个成员的共享贡献指标的bean类

/**
 * 各成员的代码贡献的各项指标<br/>
 * 当前统计:新增代码、和修改行数
 */
class LineNumber{
    // 作者
    String name;
    // 新增行数
    long add = 0l;
    // 修改行数
    long modify = 0l;
    // 总数
    long sum = 0l;

    /**
     * 构造器,必须传入该记录所属作者
     * @param name 作者姓名
     */
    public LineNumber(String name) {
        this.name = name;
    }

    /**
     * 设置新增行数,并在总数加上新增行数
     * @param add 新增行数
     */
    public void setAdd(long add) {
        this.add += add;
        this.sum += add;
    }

    /**
     * 设置修改行数,并在总数加上修改行数
     * @param add 修改行数
     */
    public void setModify(long modify) {
        this.modify += modify;
        this.sum += modify;
    }

    // getter 和 toString
    public long getAdd() {
        return add;
    }

    public long getModify() {
        return modify;
    }

    public long getSum() {
        return sum;
    }

    @Override
    public String toString() {
        return "LineNumber{" +
                "name='" + name + '\'' +
                ", add=" + add +
                ", modify=" + modify +
                ", sum=" + sum +
                '}';
    }
}

SVNkitUtil.java

基于svnkit的统计功能核心实现,本初代码仅初始化该工具类的基础属性

/**
 * Created by zhao.wu on 2017/2/13.
 * svn 代码统计工具
 */
public class SvnKitUtils {

    static {
        // 开启SVN的 http和https的访问方式
        DAVRepositoryFactory.setup();
    }

    // svn各种客户端的管理工具,使用svnkit-hl接口进行开发
    private SVNClientManager ourClientManager;
    // 仓库的统计开始版本,默认为1
    private long defaultPreVersion = 1;

    /**
     * 初始化工具类,为工具类连接仓库设置认证信息
     * @param username 连接用户名
     * @param password 连接密码
     */
    public SvnKitUtils(String username, String password) {
        ISVNOptions options = SVNWCUtil.createDefaultOptions(true);
        ourClientManager =SVNClientManager.newInstance((DefaultSVNOptions) options, username, password);
    }

    // getter 和 setter
    public long getDefaultPreVersion() {
        return defaultPreVersion;
    }

    public void setDefaultPreVersion(long defaultPreVersion) {
        this.defaultPreVersion = defaultPreVersion;
    }

    // 其他实现,如checkout等
}

checkOut的实现

当前的统计功能设计一切均是基于本地工作目录中的代码进行操作,然后计算相关指标。所以检出功能为重中之重的核心功能,好在是svnkit的hl接口中提供了这个方法,可以直接使用。

/**
 * 从仓库检出代码到工作目录下
 * @param repositoryUrl 仓库地址
 * @param workCopyPath 本地工作路径
 * @return 当前最新版本号
 */
public long checkOut(String repositoryUrl, String workCopyPath) throws SVNException {
    SVNURL svnurl = SVNURL.parseURIEncoded(repositoryUrl);
    File wcDir = new File(workCopyPath);
    wcDir.mkdirs();
    SVNUpdateClient updateClient = ourClientManager.getUpdateClient();
    updateClient.setIgnoreExternals(false);
    return updateClient.doCheckout(svnurl, wcDir, SVNRevision.HEAD, SVNRevision.HEAD, SVNDepth.INFINITY, true);
}

筛选需要统计的文件

通过上边的checkout方法可将目标仓库的最新代码检出到本地工作目录,我们可以在本地工作目录中获取需要统计的文件名和路径等信息。一般情况下,我们仅能统计某一些文本文件,其他的一些安装包等非文本文件是无法进行统计。所以,我们需要对其进行筛选。

/**
 * 递归遍历文件夹下指定格式的文件,并返回
 * @param rootFolder 遍历的文件根目录
 * @param filters 指定的文件类型,如:new String[]{".java", ".html", ".css", ".js", ".jsp", ".properties"}
 * @return 目录下所有为指定类型的文件列表
 */
private List<File> scanFolder(File rootFolder, String[] filters){
    List<File> paths = new ArrayList<File>();
    File[] childFiles = rootFolder.listFiles();
    for (File childFile : childFiles) {
        if(childFile.isDirectory()){
            paths.addAll(scanFolder(childFile, filters));
        }else {
            for (String filter : filters) {
                if(childFile.getName().endsWith(filter)){
                    paths.add(childFile);
                }
            }
        }
    }
   return paths;
}

获取某一个文件的修改版本

该方法为统计某一个文件贡献量的文件,循环调用该方法即可完成某个仓库的代码贡献量。该方法线程不安全,切记不可在多线程中调用统一实例的该方法。

/**
 * 获取文件的所有更新版本,并遍历
 * @param file 需要遍历的文件
 * @param authors 用于保存项目作者和其指标的键值对
 */
private void computeDiff(final File file, final Map<String, LineNumber> authors) throws SVNException, IOException {
    long preVersion = defaultPreVersion;
    final Map<Long, String> versions = new HashMap<Long, String>();
    ourClientManager.getLogClient().doLog(new File[]{file}, SVNRevision.create(defaultPreVersion), SVNRevision.HEAD, true, true, 1000000l, new ISVNLogEntryHandler() {
        public void handleLogEntry(SVNLogEntry logEntry) throws SVNException {
            Map<String, SVNPropertyValue> meta = logEntry.getRevisionProperties().asMap();
            String value = meta.get("svn:author").getString();
            if(value != null){
                if(!authors.containsKey(value)){
                    authors.put(value, new LineNumber(value));
                }
            }
            versions.put(logEntry.getRevision(), value);
        }
    });

    // 遍历计算
    compute(file, versions, authors);
}

遍历各版本的的差异,计算每次修改的代码贡献量

/**
 * 遍历文件修改版本<br/>
 * 遍历修正版本,生成diff文件
 * @param file 本次统计文件
 * @param versions 文件的修订版本与修订作者的键值对
 * @param authors 作者与作者贡献指标键值对
 */
private void compute(File file, Map<Long, String> versions, Map<String, LineNumber> authors) throws IOException, SVNException {
    File out = new File("temp");
    List<String> changeList = new ArrayList<String>();

    long preVersion = defaultPreVersion;
    for (Map.Entry<Long, String> longStringEntry : versions.entrySet()) {
        ourClientManager.getDiffClient().doDiff(file,SVNRevision.HEAD, SVNRevision.create(preVersion),SVNRevision.create(longStringEntry.getKey()),SVNDepth.INFINITY,true, new FileOutputStream(out), changeList);
        computeFileLineNum(out, authors.get(longStringEntry.getValue()));
        preVersion = longStringEntry.getKey();
    }
}

计算某次更新记录的代码贡献量

该功能是与某个成员的贡献量指标息息相关的,决定具体统计哪些指标,当前方法仅可统计新增代码量和修改代码量。

/**
 * 统计该修改版本的各项指标。<br/>
 * 使用BufferedReader遍历文件的每一行数据,
 * 然后判断该行代码属于指标属性,
 * 最后对该作者对应的指标积进行相关操作
 * @param file 版本修改文件(svn的diff文件)
 * @param lineNumber 该作者的指标对象
 */
private void computeFileLineNum(File file, LineNumber lineNumber) throws IOException {
    int addCount = -1;//需要减去首行‘++++’开始的一行
    int removeCount = -1;//需要减去首行‘----’开始的一行
    BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file)));
    String line = null;
    // 遍历diff文件,计算指标
    while((line = reader.readLine())!=null) {
        if(line.startsWith("+")){
            addCount++;
        }else if(line.startsWith("-")) {
            removeCount ++;
        }
    }
    reader.close();

    // 设置作者指标
    lineNumber.setModify(removeCount);
    lineNumber.setAdd(addCount-removeCount);
}

统计某一个仓库中代码贡献量

该方法是综合上述各个小功能点而集成的统计接口

/**
 * 某一个仓库的代码贡献量的统计故居
 * @param repository 仓库地址
 * @param workCopyPath 本地工作路径
 * @param filters 文件类型过滤字符串数组
 * @return 该仓库的代码贡献量的统计结果
 */
public Collection<LineNumber> computeCodeLineForEveryone(String repository, String workCopyPath, String[] filters) throws IOException, SVNException {
    // 仓库检出
    checkOut(repository, workCopyPath);

    // 获取需要统计的文件
    List<File> files = scanFolder(new File(workCopyPath), filters );

    // 遍历文件,并计算文件各个更新版本中的代码共享量
    Map<String, LineNumber> authors = new HashMap<String, LineNumber>();
    for (File file : files) {
        System.out.println("compute:"+file.getAbsolutePath());
        computeDiff(file, authors);
    }

    // 返回计算记过
    return authors.values();
}

测试

写了个简单的main方法进行测试该功能

public static void main(final String[] args) throws SVNException, IOException {
    // 初始化统计工具
    SvnKitUtils test = new SvnKitUtils("test", "123456");
    //test.setDefaultPreVersion();  //设置统计版本起点, 默认从第1版开始
    // 仓库地址
    String repositoryURL ="http://172.28.1.238:8080/svn/ProductCode/Java/Test";
    // 本地工作目录
    String workingCopyPath = "E:/temp1/";
    // 需要统计的文件格式
    String[] filters = new String[]{".java", ".html", ".css", ".js", ".jsp", ".properties"};

    // 执行统计
    Collection<LineNumber> lineNumbers = test.computeCodeLineForEveryone(repositoryURL, workingCopyPath, filters);
    // 输出所有作者的贡献指标
    for (LineNumber lineNumber : lineNumbers) {
        System.out.println(lineNumber);
    }
}

总结

经过半天的磕磕碰碰终于将这个小功能实现了,才发现自己的基础薄弱,知其然,不知其所以然。用svn很久了,却不清楚SVN各个操作的具体原理,导致自己不断的在试错,才让自己完成这个小功能花了如此多的时间。虽然该功能完成了,我知道这绝不是最有的解决方案。就拿我自己知道的就可以从单线程改为多线程,还有一些我不知道的svn内幕,使用low level接口解决效率应该会更高。

posted @ 2017-02-14 15:12  吴昭  阅读(252)  评论(0编辑  收藏  举报