本文使用 Scala 实现自定义的 Git 检查工具,读者可以基于本文的示例进行扩展与实现,也可以进行其他应用方向的尝试。
01、Git 检查工具
在实现 Git 检查工具之前需要知道程序究竟要做什么。我们知道,在管理 Git 分支时可以进行代码合并操作,这样可以将其他开发者提交的内容同步到当前分支中,当用户对自己的分支进行提交时就不会与现有版本产生冲突。
反向合并也可以理解为一种回合,在用户使用 GitLab 等版本管理软件时经常会出现这种现象,但是反向合并带来了十分严重的问题: 代码污染。
可以这样理解,用户分支是介于生产分支与测试分支中间的媒介,它必须保证与两种分支的匹配性问题,即文件差异性问题。通常用户分支是基于生产拉取出来的全新分支,而很多开发者都试图使用这个分支进行修改并提交到测试分支进行测试发布。
在理想情况下项目的测试分支与生产分支应该是一致的,因此反向合并容易被修改或纠正,但是在测试分支与生产分支差异较大的时候,反向合并会将测试分支中的内容合并到用户分支中,如果用户分支被提交到生产分支上,则将会产生不可恢复的灾难。
基于上述原因,我们使用 Scala 设计一款简单的检查工具,它可以检查指定分支或分支组中所有的提交信息,并从这些信息中过滤出带有回合操作的历史。
如果发生过反向合并的操作,则在 Git 提交历史记录中通常会带有 Mergeremotetrackingbranch...的字样信息,但是带有这种信息的提交并不一定都产生了合并问题。
当通过 Git 检查工具过滤出符合上述特征的分支后,可以通过判断与生产分支的差异数量并设定一个判断阈值的方式再次深度过滤或直接人工观察用户分支的差异化等多种方式来确保上线分支的准确性。
02、编写配置
在 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/前缀。
接下来定义一个用于控制日志输出的配置文件,代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="INFO">
<properties>
<property name="APP_HOME">$${env:APP_HOME}</property>
<property name="LOG_HOME">${APP_HOME}/logs</property>
<property name="mainFilename">${LOG_HOME}/vh.log</property>
</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%n</pattern>
</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>
复制代码
03、编写启动程序
接下来编写项目的启动程序,启动程序可以接收外界传入的参数以实现不同配置的切换使用,代码如下:
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 变量所代表的分组配置进行匹配,如果匹配成功,则执行对应用的功能调用。如果匹配不上,则输出日志提示。
04、编写校验逻辑
在 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 检查工具
评论