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

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

前言

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

介绍

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

引入

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

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

<!-- 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进行过滤处理,然后对数据进行存储,用于展示。完整源码参考如下:

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功能统计增加和修改的代码行数,源码参考如下:

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,761评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,953评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,998评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,248评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,130评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,145评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,550评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,236评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,510评论 1 291
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,601评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,376评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,247评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,613评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,911评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,191评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,532评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,739评论 2 335

推荐阅读更多精彩内容

  • &开发过程中离不开源代码的管理, 目地:为了解决在软件开发过程中,由源代码引发的各种蛋疼、繁琐的问题。 目前开发使...
    早起的虫儿子被鸟吃阅读 2,397评论 0 16
  • SVN SVN使用 基本操作svn checkout:把项目源码下载到本地,只需要做一次svn update:将本...
    彼岸的黑色曼陀罗阅读 1,587评论 0 4
  • iOS 开发 SVN 版本控制器 更多技术交流请加群 iOS技术联盟 27512466 SVN是Subversio...
    Sunny_Fight阅读 8,709评论 7 63
  • 平地一声雷 鸭绿江畔战火起 天寒地冻腊月天 红旗漫卷 江山无限好 岂能让豺狼沾染 单衣裤 雪加面 热血烘得钢枪暖 ...
    恣意生活阅读 1,331评论 10 58
  • 退群第四次 这次是真的离开 不管有没付出 值月算是做完了 谁的也不欠 谁的也不想听 刘先生说 你着了魔吗? 一天到...
    青梅3煮酒阅读 169评论 4 0