在群体协同的软件开发中,代码提交作为开发者最频繁的日常操作之一,有必要遵循“代码提交原子性”这一最佳实践。然而,多项研究发现,在实际的开源和工业项目中普遍存在“复合提交”现象,即开发者经常将一段时间内做出的所有代码变更一次性提交,即使其中的代码修改包含了多种不相关的变更意图或对应于多个开发和维护任务。本文将介绍一个代码提交辅助工具SmartCommit [1],其主要功能是通过杂糅变更分解算法自动生成分组提交方案,接受开发者的反馈和交互式调整,渐进式地引导和辅助开发者做出符合最佳实践的原子提交。
代码提交的原子性
代码变更提交是以Git为代表的版本管理系统基础功能,也是开发者最频繁的日常操作之一。在群体参与的软件开发和维护中,个体开发者从不同的目的出发对代码做出变更,被Git以代码提交(commit)为单位进行记录。代码提交是版本管理系统其他功能工作的基础,时间顺序上的提交序列构成了代码仓库的提交历史(commit history),跟踪和记录了软件仓库每一次变更的内容、时间、描述、提交者等信息。因此,清晰的代码提交历史在代码审查、团队协作、分支管理、持续集成、问题定位和修复等活动中具有重要作用。
如何恰当地使用代码提交记录、组织和管理个体贡献,是群体高效协同进行软件开发和维护的基础。“面向任务的原子性代码提交”是Git官方文档中倡导的最佳实践,也是一种被开源社区(如Angular、Vue等)和知名软件公司(如Google、Microsoft等)明确提倡的规范和要求。
“代码提交原子性”的具体含义是:每次提交中的代码变更应该高内聚(cohesive)和自包含(inclusive),聚焦于一个软件开发或维护任务(如添加新特性,修复 bug,重构等)。遵循这种最佳实践,对于软件开发者来说,在群体协同进行软件开发时,有利于他人理解和审查代码变更、定位问题引入提交并回退、拣选复用历史变更等;对于软件仓库挖掘(MSR)的研究者来说,面向任务的提交有助于减少数据中的噪音,从而提供更清晰的演化历史数据。
Git文档中对于代码提交原子的描述 [2]
Google工程实践中对于变更提交者的要求 [3]
Angular项目中对于提交信息的规范 [4]
原子提交与复合提交
在群体协同的软件开发中, “原子提交”(Atomic Commits)对于开发者群体的有效协同有着重要作用;然而,多项研究已发现,在开源项目和工业项目中普遍存在“复合提交” (Composite Commits)现象(大概占总commit数目的10~40%)。
与原子提交相反,复合提交指开发者将一段时间内做出的所有代码变更一次性提交,即使其中杂糅了针对多个开发/维护任务的代码修改。复合提交产生的原因主要包含三个方面:
1. 在日常开发中开发人员经常会有意识或无意识地同时进行多个任务,例如在开发新功能时重构代码结构(即 floss refactoring),或在优化功能的同时修复 bug或code smell。
2. 尽管部分团队或开源社区为开发者提供了代码贡献规范,但其中很少涉及对提交风格的明确规定或指导。
3. 虽然部分版本控制系统或工具提供了挑选和组织细粒度代码变更的功能(如 Git interactive staging,GitKraken/Fork等),但基本上完全依赖开发者的主动触发和手动选择,需要较高的认知和使用成本。
典型的原子提交(左)与复合提交(右)对比
代码变更分解相关研究
针对复合提交问题,学术界提出了多种方法和技术对杂糅的代变更进行分解,如基于启发式规则的方法[5]、基于程序切片技术的方法[6]、基于数据流和控制流依赖的方法[7],基于模式匹配的方法[8] 等。但是,这些方法存在以下共性的限制和不足:
大部分采用事后分解的方式,在复合提交已经被记录为版本历史之后的某个阶段(如代码评审、提交拣选、历史切片等)再检测或分解复合提交。
现有技术的分解结果往往因粒度过细而难以与开发者意图和任务对应,因此不适用于代码提交阶段。
现有技术未在代码变更分解算法中考虑开发者的背景知识。考虑到原子代码提交这一最佳实践在实际执行中与项目背景、团队要求、开发者习惯等因素相关,因此代码变更分解过程需要结合开发者对于问题背景、领域、项目的知识,允许一定的灵活性和主观性。
小结
面向任务、高内聚、自包含的原子提交是一种被广泛,但由于多任务并行、缺乏统一规范、工具支持不足等原因,违反这一最佳实践的复合提交现象仍然普遍发生。虽然现有研究中针对此问题已经存在很多研究,但其限制和不足使得现有方法难以真正被应用于实际的代码提交工作流中。
代码变更交互式分解工具SmartCommit
为了克服现有方法的限制和不足,本文将介绍SmartCommit,一个基于图(Graph)的交互式代码变更分解算法,其目标是在开发阶段渐进式地引导和辅助开发者面向开发和维护任务进行原子代码提交,从而从根源上避免复合提交的产生。
首先,SmartCommit将杂糅变更分解问题转换成一个渐进式的方案优化问题:
给定一个变更集 ? = {c1, c2, c3, …, cn} 作为输入,变更分解问题的目的是将 ? 划分为一个由若干非空集合构成的列表 G = [?1, ?2, …, ??],每个集合称为一个变更分组(Change Group),对应于一个开发或维护任务。
如下图所示,若将所有细粒度变更提交为一个 commit 视为初始状态(Initial State),将每个细粒度代码变更被独立提交为一个commit 视为极端状态(Extreme State),则变更分解的目的是在初始状态和极端状态之间寻找一个满足代码提交原子性的可接受状态(Accepted Decomposition)。
但是,由于实际情况的复杂性和代码提交的灵活性,变更分解过程很难完全自动地通过算法完成。因此,SmartCommit将变更分解过程视为一个人机交互的渐进式优化过程:由算法提供初始的分解方案建议(Initial Suggestion),再通过粗粒度控制(Coarse Control)和细粒度调整(Fine Tuning)两种交互机制,辅助开发者快速形成期望的变更分组提交方案。算法可利用计算机的程序分析能力和精细量化评估算法,生成的分解方案可以为开发者提供一个比较好的起点;交互机制可以利用开发者对算法生成方案的反馈和调整,引导算法生成的分解方案尽量靠近期望状态。考虑到代码提交本身就是一个人机交互的过程(开发者需要通过命令添加变更、描述变更、关联问题单等),SmartCommit的变更分解机制是一个自然的人机结合方法,可综合利用算法的计算优势和和开发者的信息优势,共同完成杂糅变更分解任务。
杂糅变更集的交互式分解过程示意
以上思路被实现为SmartCommit算法,下图为其工作流程,主要包含以下四个阶段:
1. 变更集预处理(Preprocessing)
给定一个Git 工作区或一次复合提交,抽取出其中的代码变更并以代码变更块(diff hunk)为单位进行表示,将所有代码变更块构成的集合称为变更集(changeset);
2. 变更关系图构建(Graph construction)
在代码变更关系图的基础上,将变更中涉及的节点以代码变更块为单位进行聚合,将每个代码变更块作为点,将代码变更块之间的显式和隐式关系作为边,构建一个代码变更块关系图(Diff Hunk Graph);
3. 变更交互式分解(Interactive decomposition)
通过一个以边为中心的图划分算法, 将代码变更块关系图的点集划分为若干独立子集,将每个子集转换为一个变更组(change group),作为建议的分解方案提供给用户进行交互式调整;
4. 变更一键式提交(Commits submission)
当分解方案达到可提交状态时,开发者可以选择需要提交的若干个变更组,附上描述该分组中代码变更的信息,一键式将多个变更组提交到版本控制系统中。
SmartCommit算法流程概览
由于篇幅所限,下文将主要围绕SmartCommit算法的三个核心部分进行介绍:
1. 数据结构:用于建模分布在项目不同位置的代码变更间关联关系的图结构
2. 分解算法:基于图划分算法、以关系为核心生成代码变更分解方案的算法
3. 交互机制:通过交互机制综合算法分析能力与开发者背景知识的交互机制
数据结构
为了建模和管理细粒度的代码变更间关系,我们采用了图(Graph)这种数据结构,设计了“代码变更关系图(Change Relation Graph)”。代码变更关系图的点集由变更块(Diff Hunk)组成,每个变更块对应工作区或某次提交中一个独立的代码编辑/修改;边集由变更块间关系(Relation)组成,每条边从某一个维度刻画了所连接的两个变更块之间的关联及其强度。
对于点集,SmartCommit通过分析输入的工作区或某个复合提交,根据Git diff结果(基于文本)和代码抽象语法树(AST)抽取变更块相关信息。必要的信息包括:
1. index:由file_index:hunk_index组成,唯一定位一个变更块在源码中的位置。file_index表示当前变更所属的源文件(父文件)在变更集中的索引,从 0 开始编号;hunk_index表示当前变更在父文件内所有变更中的索引,从 0 开始按起始行号排序;
2. change_type:变更动作类型,如新增、删除、修改等;
3. base_hunk/current_hunk:该变更块在变更前(base)和变更后(current)版本中对应的代码块,其中包含文件类型、文件路径、行号范围、代码片段、AST子树等信息。
对于边集,SmartCommit综合了相关研究中被证明与变更间耦合性存在一定相关性的指标,从多个维度评估变更块之间的关联关系以及强度:
1. 结构性关联(Structural Correlation)
指代码变更块之间直接或间接的语法和语义依赖,这些关系通常有方向性且不能在提交中被破坏(例如,方法的调用不能在其声明/定义之前被提交,否则将在中间提交版本中包含编译错误);
2. 启发式关联(Heuristic Correlation)
指可能推断出多个细粒度变更源于同一编辑动作的启发式规则,如代码变更块之间的相似度(similarity)和邻近度(proximity),其目的是检测系统性变更(systematic edits)、应用于克隆代码的变更、定义域相同的相邻变更等;
3. 重构性关联(Refactoring Correlation)
旨在检测出由同一次系统性或结构性变更产生的、分布在项目不同位置的多处细粒度代码变更,主要指各种类型的重构操作;
4. 逻辑性关联(Logical Correlation)
由程序语义上并不相关但编辑动作上一致的常见操作产生的多处变更,如代码格式化变更、死代码清理、文本上的移动等。
以上每种类型的关联关系分别对应一种类型的边,关联关系的强度被作为边的权值,其计算方法详见论文中的细节描述和代码实现。除了以上关系之外,代码变更关系图可以很容易地扩展其他维度的关联关系,例如演化耦合关系(evolutionary coupling)、编辑时间差(time stamp difference)等。但是,为了获得这些信息,算法在实现中需要依赖于特定类型的版本管理工具(VCS)或集成开发环境(IDE)对代码编辑历史的日志记录。
分解算法
得到当前工作区中变更集对应的代码变更块关系图后,我们把对变更集的分解问题转化为针对代码变更块关系图的图划分问题,即综合节点之间存在的边以及边上的权重,将代码变更块关系图的点集划分为一组相互独立(mutually exclusive)的子集。
借鉴多层次图划分(Multi-level Graph Partitioning)思想,SmartCommit采用了一种基于 Kruskal 算法、以边为中心的图划分算法。该算法以一个代码变更块关系图和一个可选的权重阈值作为输入(当用户未设置阈值时采用Max-gap Splitter算法动态确定一个临界点)。首先,算法创建一个空的优先级队列 ? 用于保存边,以及一个并查集数组 ? 用于保存变更分组(每个顶点初始化为其中的一个元素,即将每个代码变更块单独作为一个分组)。对于图中的每条边 (?, ?),将其以三元组 (?, (?, ?)) 的格式添加到优先级队列 ? 中,其优先级的确定首先根据边的权值降序排序,其次按边起始节点的ID 和目标节点的ID 升序排序。然后,算法进入一个循环:弹出当前队列中优先级最高的边;如果当前边的两个端点已经在同一分组中,则这条边被忽略,循环继续;否则,如果其权重大于阈值,则合并两个端点所在的分组。如果边的权重小于阈值,或者 ? 为空,则循环终止,从 ? 中生成连通集作为结果;如果还存在单独作为一组的节点,则将这些节点合并为一组,并追加到其他分组之后。最后,所有节点分组被输出,作为代码变更块关系图划分的结果。
基于Kruskal和Max-gap Splitter算法的图划分过程
交互机制
基于图划分算法得到的节点分组将被转换为代码变更分组,并作为算法建议的分组方案,以适当的形式提供给开发者进行检查。如果建议的分组方案与开发者的期望有所偏差,开发者可以通过两种交互式操作进行调整:
粗粒度控制(Coarse control)
通过控制算法终止条件,重新运行分解算法以产生不同粒度的分解方案。在SmartCommit的实际实现中,代码变更块关系图会被缓存到内存或硬盘中,因此重新运行算法不需要重新进行图的构建,生成速度较快。粗粒度控制的目的是利用开发者对于算法生成方案的反馈,指导算法更快速地接近期望的分组状态,生成不需要或仅需少量细粒度调整的方案。
细粒度调整(Fine tuning)
通过在不同分组之间移动单个或多个代码变更块,将少数分配错误的变更块移动到所应属于的分组中。细粒度调整的目的是允许开发者对提交进行细粒度的微调,从而修正算法的结果或排除部分不需要提交的变更。
在通过调整得到可接受的分解方案后,开发者可以选择部分或全部的变更组一键式产生多个提交,将选择的变更组记录为版本管理系统中的一系列连续的commit。
需要声明的是,由于需要考虑不同项目之间的通用性,SmartCommit默认的分组粒度为广义上的开发和维护任务(如实现新功能、修复问题、重构等),而非具体的细粒度变更(如添加类、改变方法参数、修改返回值类型等)。SmartCommit遵循semantic-release [9](一个自动化的版本发布工具)和commitizen [10](一个规范commit message的工具)对开发和维护任务的分类,因此可以配合这些工具进行使用。
Commitzen的提交类型分类规约
小结
为了从根源上避免复合提交的产生,SmartCommit着眼于在开发阶段引导和辅助开发者考虑代码变更的原子性。与此前工作的不同的是,SmartCommit基于图结构从多个维度建模代码变更间关系,面向广义的维护和开发任务对变更进行分组,并引入了人机交互机制结合算法和开发者各自的优势。
工具原型实现
SmartCommit的核心算法使用了Java进行实现[11],配套了一个基于NodeJS和Electron的GUI界面[12]。借助Java和Electron的跨平台特性,该工具支持 Windows、Linux、以及macOS 三种操作系统;既可以被包装为独立的桌面软件或IntelliJ IDEA 插件进行使用,也可以通过配置成Git 子命令git sc,在代码提交时通过Git 命令行进行调用。
作为华为-北京大学2019-2020技术合作项目,SmartCommit已于2020年初落地为一个Intellj IDEA插件,被华为云、消费者云、欧拉、云核心网等部门多个团队的工程师用于日常的代码提交中。除了开源的基础变更分解功能之外,华为内部的版本还额外提供了提交类型分类、提交信息自动生成、关联问题单(Issue ID)推荐等功能。
实验评估与验证
研究者同时在开源项目和工业环境中对SmartCommit进行了实验评估,并通过比对实验结果交叉验证算法和工具的效果。通过在3000个来自知名开源项目的模拟复合提交上进行受控实验,以及对华为内部83名工程师长达36周的使用数据分析,结果表明:
1. 准确率:SmartCommit 产生的初始分解方案在 10 个开源项目上可达 71.00%–83.50% 的准确率(中位数) ,在2个工业项目上分别达到了74.70% 和 70.45% 的准确率(中位数)。
2. 交互性:在没有用户参与的情况下,仅通过细粒度调整需要更多的步骤(1-15 次操作);但在实际使用中存在粗粒度控制的情况下,所需的调整步数在 80% 的情况下不超过 5 步。
3. 运行性能:在90%的情况下,SmartCommit 可以在 5 秒内完成分析,且运行时间不随着输入变更集规模的增大而显著增加,大多数用户认为SmartCommit 的性能在日常工作中可以接受。
4. 实用价值:受访的10名活跃用户表示SmartCommit 可以有效地帮助开发者遵循最佳实践,并且带来了额外的收益,例如针对分组后的变更组更容易编写commit message、在分组后更容易发现不应该提交的变更(如本地配置、个人信息、敏感数据)等。
总结
针对复合代码提交问题,本文介绍了一个基于静态程序分析和图划分算法的代码变更辅助分解与提交工具SmartCommit。该工具可自动分析细粒度变更间关系,并自动对杂糅变更或非原子提交进行分解;配合GUI前端界面,可交互式、渐进式地辅助开发者遵守面向任务进行提交这一最佳实践。SmartCommit在开源和工业项目中分别进行了实验验证,结果表明其自动分解算法为开发者提供了一个提高代码提交原子性的起点,且通过交互机制降低了开发者遵循最佳实践的成本。
作为一项从研究成果中构建的工具原型,SmartCommit在实际使用中也存在一些限制和不足,例如:
1. 目前的实现只支持Git项目和Java语言代码。
2. 算法未充分利用所有维度的变更间关联,自动分解准确率存在进一步提升的空间。
3. 工具目前主要面向开发和提交阶段,可替代git diff/add/commit/push等命令,是否可以应用于代码评审阶段?
为了满足以上需求,进一步提升SmartCommit的效果并扩大可用范围,我们目前正在开发SmartCommit的2.0版本,通过重构实现以下改进:
1. 图构建方面:抽取并解耦语言相关的图构建部分,使用通用的代码分析器替代目前专用于Java的JDT Parser,以支持更多的语言。
2. 图分解方面:使用矩阵形式存储多重图,增加更多维度的变更间关联信息,并结合自顶向下的图划分与自顶向上的点聚类算法,进一步引入数据驱动的方式提高自动分解算法的准确率。
3. 交互与应用:收集用户在交互时产生的反馈数据并加以利用;为核心算法增加适用于处理Pull request的 API,集成变更描述生成算法等。
【参考资料】
[1] Bo Shen, Wei Zhang, Christian Kästner, Haiyan Zhao, Zhao Wei, Guangtai Liang, and Zhi Jin. SmartCommit: a graph-based interactive assistant for activity-oriented commits. In Proceedings of the 29th ACM Joint Meeting on European Software Engineering Conference and Symposium on the Foundations of Software Engineering (FSE), pp. 379-390. 2021.
[2] https://git-scm.com/docs/gitworkflows#_separate_changes
[3] https://google.github.io/eng-practices/
[4] https://github.com/angular/angular/blob/master/CONTRIBUTING.md
[5] K. Herzig and A. Zeller. “The impact of tangled code changes”. In: 2013 10th Working Conference on Mining Software Repositories (MSR). 2013: 121–130.
[6] W. Muylaert and C. De Roover. “Untangling Composite Commits Using Program Slicing”. In: 2018 IEEE 18th International Working Conference on Source Code Analysis and Manipulation (SCAM). 2018: 193–202.
[7] M. Barnett, C. Bird, J. Brunet et al. “Helping developers help themselves: Automatic decomposition of code review changesets”. In: 2015 IEEE/ACM 37th IEEE International Conference on Software Engineering. 2015: 134–144.
[8] M. Dias, A. Bacchelli, G. Gousios et al. “Untangling fine-grained code changes”. In: 2015 IEEE 22nd International Conference on Software Analysis, Evolution, and Reengineering (SANER). 2015: 341–350.
[9] https://github.com/semantic-release/semantic-release
[10] https://github.com/commitizen/conventional-commit-types
[11] https://github.com/Symbolk/SmartCommitCore
[12] https://github.com/Symbolk/SmartCommit
本文分享自华为云社区,作者: 敏捷的小智。