本文使用Scala实现自定义的Git检查工具,读者可以基于本文的示例进行扩展与实现,也可以进行其他应用方向的尝试。
在实现Git检查工具之前需要知道程序究竟要做什么。我们知道,在管理Git分支时可以进行代码合并操作,这样可以将其他开发者提交的内容同步到当前分支中,当用户对自己的分支进行提交时就不会与现有版本产生冲突。
反向合并也可以理解为一种回合,在用户使用GitLab等版本管理软件时经常会出现这种现象,但是反向合并带来了十分严重的问题: 代码污染。
可以这样理解,用户分支是介于生产分支与测试分支中间的媒介,它必须保证与两种分支的匹配性问题,即文件差异性问题。通常用户分支是基于生产拉取出来的全新分支,而很多开发者都试图使用这个分支进行修改并提交到测试分支进行测试发布。
在理想情况下项目的测试分支与生产分支应该是一致的,因此反向合并容易被修改或纠正,但是在测试分支与生产分支差异较大的时候,反向合并会将测试分支中的内容合并到用户分支中,如果用户分支被提交到生产分支上,则将会产生不可恢复的灾难。
基于上述原因,我们使用Scala设计一款简单的检查工具,它可以检查指定分支或分支组中所有的提交信息,并从这些信息中过滤出带有回合操作的历史。
如果发生过反向合并的操作,则在Git提交历史记录中通常会带有Mergeremotetrackingbranch...的字样信息,但是带有这种信息的提交并不一定都产生了合并问题。
当通过Git检查工具过滤出符合上述特征的分支后,可以通过判断与生产分支的差异数量并设定一个判断阈值的方式再次深度过滤或直接人工观察用户分支的差异化等多种方式来确保上线分支的准确性。
在Git版本控制管理章节里提到过,反向合并会对开发者的项目分支带来污染,因此可以实现一个用于Git分支检查的工具,这样在每次例行版本维护时可以帮助我们快速定位反向合并的问题。
工具不一定能解决所有的问题,因为每个问题的出现都有其随机性,但是工具却能从某些方面提升我们的效率。读者在学习完本章后,可以根据需要自行扩展并定制更多的功能。
首先在resources资源目录下,创建一个名为config.conf的文件,它用于Git检查工具的基础配置。config.conf配置文件中定义了本地Git项目的根目录及待检查的分支,代码如下:
- {
- group1 = {
- workDir = "Git项目目录"
- }
- group2 = {
- workDir = "Git项目目录"
- base = master
- branches = [
- user_local_branch
- ]
- }
- }
在上述配置中对待检查目标进行了分组,运行时用户可以将需要对比的项目及分支预先定义好,这样可以在项目启动后通过接收参数的方式来动态调整使用哪一组配置进行目标分支的检查与分析。
在每一组配置里,workDir指定本地Git项目的根目录。base用于指定项目的主分支(master)。branches是一个分支列表,它代表了待检查的分支,这些分支既可以是本地分支,也可以是远程分支。如果是远程分支,则通常要在其前面添加origin/前缀。
接下来定义一个用于控制日志输出的配置文件,代码如下:
- "1.0" encoding="UTF-8"?>
- <Configuration status="INFO">
- <properties>
- <property name="APP_HOME">$${env:APP_HOME}property>
- <property name="LOG_HOME">${APP_HOME}/logsproperty>
- <property name="mainFilename">${LOG_HOME}/vh.logproperty>
- properties>
- <Appenders>
- <Console name="Console" target="SYSTEM_OUT" follow="true">
- <PatternLayout pattern="%date{yyyy-MM-dd HH:mm:ss.SSS} %level - %msg%n" />
- Console>
- <RollingFile name="FileMain" fileName="${mainFilename}"
- filePattern="${LOG_HOME}/vh%date{yyyyMMdd}_%i.log.gz">
- <PatternLayout>
- <pattern>%date{yyyy-MM-dd HH:mm:ss.SSS} %level - %msg%npattern>
- PatternLayout>
- <Policies>
- <CronTriggeringPolicy schedule="0 0 0 * * ?" evaluateOnStartup="true"/>
- <SizeBasedTriggeringPolicy size="20 MB" />
- Policies>
- RollingFile>
- Appenders>
- <Loggers>
- <Root level="info">
- <AppenderRef ref="Console" />
- <AppenderRef ref="FileMain" />
- Root>
- Loggers>
- Configuration>
接下来编写项目的启动程序,启动程序可以接收外界传入的参数以实现不同配置的切换使用,代码如下:
- package com.scala.git
- import org.slf4j.LoggerFactory
-
- object MainCheck {
- private val log = LoggerFactory.getLogger(getClass)
- def main(args: Array[String]): Unit = {
- log.info(s"接收外界传递的切换配置: ${args.group}")
- var group = "group2"
- if(args.length > 0){
- group = args(0)
- }
- log.info(s"当前配置为$group")
- group match {
- case "group2" => CheckTask.main(args)
- case _ => log.error(s"not found $group")
- }
- }
- }
因为Scala程序可以与Java语言混合编写,因此Java开发人员在阅读Scala程序时相对容易理解一些。
在MainCheck对象的主方法中接收了外界传递进来的group参数,它可以在程序启动时动态传递到主方法中并替代默认配置组group2。
接下来通过match操作对group变量所代表的分组配置进行匹配,如果匹配成功,则执行对应用的功能调用。如果匹配不上,则输出日志提示。
在MainCheck.scala应用程序中,当外界变量group匹配成功后会调用具体的执行逻辑,此逻辑封装在CheckTask对象方法中。
在编写CheckTask对象之前先来编写GitUtil.scala程序文件,其作用为调用并执行CMD命令以便获取指定分支的所有提交信息,这些提交信息将以数组的形式返回,代码如下:
- package com.scala.util
- import java.io.File
- import org.slf4j.LoggerFactory
-
- import scala.sys.process.{Process, ProcessLogger}
-
- object GitUtil {
- private val isWin = System.getProperty("os.name").toLowerCase.contains("Windows")
- private val log = LoggerFactory.getLogger(getClass)
-
- def getCommits(from: String, to: String, workDir: String): String = {
- val cols = Array("%H", "%s", "%an", "%ae", "%ci")
- val tem = from + ".." + to + " --pretty=format:\"" + cols.mkString("/") + "\"";
- val value = cmdCommits(s"git log " + tem, new File(workDir))
- value
- }
-
- def cmdCommits(cmd: String, workDir: File): String = {
- var commits:Array[String] = null;
- if(!isWin){
- commits = cmd.split("\\s")
- }else{
- commits = Array("cmd", "/c") ++ cmd.split("\\s")
- }
- Process(commits, workDir).!!(ProcessLogger(s => log.error(s"err => $s")))
- }
- }
接下来实现CheckTask.scala程序文件,代码如下:
- package com.scala.git
-
- import com.scala.util.GitUtil
- import com.typesafe.config.ConfigFactory
- import scala.collection.JavaConverters._
-
- object CheckTask {
-
- private val config = ConfigFactory.load("config.conf").getConfig("group2")
- private val orderWorkDir = config.getString("workDir");
- private val base = config.getString("base");
- private val branchs = config.getStringList("branchs");
-
- def main(args: Array[String]): Unit = {
- println(s"参照对比分支[$base]")
- println(s"待检查分支集合$branchs")
- checkBraches(base, asScalaBuffer(branchs).toArray).foreach(b => println(s"发现可疑分支 $b"))
- }
-
- def checkBraches(base: String, brans: Array[String]): Array[String] = {
- brans.filter(b => checkMergeError(base, b))
- }
-
- private def checkMergeError(base: String, target: String): Boolean = {
- println(s"对比分支:$base,检查分支:$target")
- //取得所有提交信息
- val commits = getDiffCommits(base, target)
- //从历史提交记录过滤出回合过的分支
- val targets = commits.filter(isMergeReverse)
- targets.foreach(c => {println(c.mkString("\t"))})
- println(s"分支[$target]中可疑提交次数: ${targets.length}")
- targets.length != 0
- }
-
- private def isMergeReverse(messages: Array[String]): Boolean = {
- val msg = messages(1)
- if(msg.startsWith("Merge branch 'int_") || msg.startsWith("Merge remote-tracking branch ")){
- val splits = msg.split("\\s")
- val end = splits(splits.length-1)
- val flag = end.startsWith("int_") || end.startsWith("local_int_")
- return !flag
- }
- false
- }
-
- private def getDiffCommits(from: String, to: String): Array[Array[String]] = {
- GitUtil.getCommits(from, to, orderWorkDir).lines.map(_.split("/")).toArray
- }
- }
现在尝试运行工具,随便选取系统中的某个Git项目并修改config.conf配置文件以使其与Git项目中的分支对应,然后运行MainCheck.scala程序文件,运行效果如图1所示。
■ 图1 运行Git检查工具