L
L
i
i
r
r
o
o
u
u
s
s
c
c
o
o
d
d
i
i
n
n
g
g
实现自动化commit的一个小项目

写在前面

近几天看GitHub上的大佬contribution都多的一批,十分羡慕。

于是打算尝试用go写一个简单的脚本并且用docker部署到我的服务器,给自己刷一点contribution,当然主要是为了装逼

(后来发现刷的太多了 就删除了这个仓库)
2024.05.07

不懂GitHub的朋友可以自行了解一下,不多赘述。

程序构思

  • 首先要明白我们的目的,要实现持续自动 commit ,关键点在于如何一直执行,以及 commit 的条件。

  • commit的条件很简单:只要工作区的代码改变了就可以 commit 了,我们很自然想到创建个文件,对它进行内容修改之后提交代码以实现需求

所以得到我们的伪代码

go 复制代码
func main() {
	//创建文件
	for	{
		//修改文件
		
		//提交git
	}
}

既然思路清晰了,那我们先用代码写出基本的架构。

在一个无限循环中

检测文件是否创建 -->创建文件
修改文件
push到仓库

代码实现

for死循环

go 复制代码
package main

import (
	"fmt"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"time"
)

const (
	projectPath = "./" //设置当前项目目录
	fileName    = "fish.txt"
)

func main() {
	//首先得到正确的文件路径 用这个函数的好处是不用管操作系统的分隔符 管它是/还是\ go语言直接帮你解决
	path := filepath.Join(projectPath, fileName)

	//检测该文件是否存在
	if ifExist := checkFileExist(path); ifExist == false {
		if err := createFile(path); err != nil {
			log.Printf("createFile(path) failed,err:%v", err)
			return
		}
		log.Println("createFile(path) success")
	}

	//文件是否被修改
	var ifModify = false

	//根据条件来判断 对文件的操作
		for {
		if !ifModify {
			if err := modifyFile(path); err != nil {
				log.Printf("modifyFile(fileName) failed,err:%v", err)
				return
			}
			log.Println("modifyFile(fileName) success")
			ifModify = true
			time.Sleep(time.Second * 10)
		} else {
			if err := resetFile(path); err != nil {
				log.Printf("resetFile(fileName) failed,err:%v", err)
				return
			}
			log.Println("resetFile(fileName) success")
			ifModify = true
			time.Sleep(time.Second * 10)
		}
		//提交git 代码待补充
	}

// 检测文件是否存在
func checkFileExist(path string) bool {
	_, err := os.Stat(path)
	return !os.IsNotExist(err)
}

// 创建文件
func createFile(path string) error {
	//先创建文件 没有文件会默认创建一个
	fPointer, err := os.OpenFile(path, os.O_CREATE, 0644)
	if err != nil {
		log.Printf("create file failed,err:%v", err)
		return err
	}
	defer func() {
		if err:=fPointer.Close();err!=nil{
			log.Println("createFile's fPointer close failed")
			return
		}
	}()
	return err
}

// 修改文件
func modifyFile(path string) error {
	content := fmt.Sprintf("今天又摸鱼了鸭 %d", time.Now().UnixNano())
	fPointer, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0644) //os.O_APPEND | os.O_WRONLY 表示 仅写权限 | 追加内容
	defer func() {
		if err:=fPointer.Close();err!=nil{
			log.Println("modifyFile's fPointer close failed")
			return
		}
	}()
	if err != nil {
		return err
	}
	_, err = fPointer.Write([]byte(content))
	return err
}

// 重置文件
func resetFile(path string) error {
	fPointer, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0644) //os.O_TRUNC 表示清空
	defer func() {
		if err:=fPointer.Close();err!=nil{
			log.Println("resetFile's fPointer close failed")
			return
		}
	}()
	return err
}

输出结果:

sh 复制代码
2024/03/12 14:08:26 createFile(path) success
2024/03/12 14:08:26 modifyFile(fileName) success
2024/03/12 14:08:36 resetFile(fileName) success

我们这里使用了一个for的死循环来达到类似计时器的效果,显然这是不可取的。

go语言里面有内置的计时器ticker,我们把它到项目当中去!

使用ticker

以下是要修改的代码

go 复制代码
const (
	projectPath = "./" //设置当前项目目录
	fileName    = "fish.txt"
	intervalStr = "60s"
)

interval, err := time.ParseDuration(intervalStr)
	if err != nil {
		log.Printf("time.ParseDuration(intervalStr) failed,err:%v", err)
		return
	}
	ticker := time.NewTicker(interval)
	defer ticker.Stop()

for range ticker.C {
    if !ifModify {
        if err := modifyFile(path); err != nil {
            log.Printf("modifyFile(fileName) failed,err:%v", err)
            return
        }
        log.Println("modifyFile(fileName) success")
        ifModify = true
    } else {
        if err := resetFile(path); err != nil {
            log.Printf("resetFile(fileName) failed,err:%v", err)
            return
        }
        log.Println("resetFile(fileName) success")
        ifModify = false
    }
    //提交git 代码待补充

}

输出结果:

sh 复制代码
2024/03/12 14:14:31 modifyFile(fileName) success
2024/03/12 14:14:41 resetFile(fileName) success

ok,我们现在只剩下 git操作
我们思考一下,平常使用 git 都是命令行操作,是否Go也可以实现呢?

当然是可以的,go语言内置的 os/exec包 就可以达到这个功能。

gitCommand

我们来简单写一个函数

go 复制代码
func gitCommand(dir string, command ...string) (err error) {
	cmd := exec.Command("git", command...)
	cmd.Dir = dir
	var commandSql string
	for _, v := range command {
		commandSql += v + " "
	}
	output, err := cmd.CombinedOutput()
	if err != nil {
		fmt.Println(commandSql + "failed")
		fmt.Println("Command output:", string(output))
		return err
	}
	fmt.Println(commandSql + "success")
	return err
}

gitCommand解释

这个函数第一个参数是执行命令行的目录,第二个是可变参数,也就是 []string

ok,我们继续看我们调用的exec.Command()接受两个参数,第一个是指令标识符,可以理解为前缀gitdocker这样指令都需要这个前缀,不然计算机怎么识别呢?

\

第二个参数是我们传进去的就是可变参数 command 了,打个比方,就以 git 为例,我们可以执行

git init

git remote add origin https://github.com/username/Repository.git

很明显,这两个指令参数是不一样的,这个可变参数就可以帮我们解决传参这类问题,剩下的实现就交给内置函数了,我们无需在意,毕竟 对于每个问题追究太多也不是一件好事

下面的 for range 目的只是为了拼接 command 语句,然后打印日志,方便查错

其实 cmd.Run() 就可以了,但是我们要防止报错,并且得到准确的错误,所以我们使用 cmd.CombinedOutput(),然后在err不为空的情况下标准输出错误,以 达到排查错误的效果

很好,我们以及知道如何使用简单的命令行了。

操作git指令

接下来就是如何操作git指令了,我直接给出代码

go 复制代码
// 初始化仓库
func initAndRemoteAdd(projectPath string) error {
	//init
	if err := gitCommand(projectPath, "init"); err != nil {
		return err
	}

	//git add . --> 因为后续要切换分支 所以当前的分支必须add
	if err := gitCommand(projectPath, "add", "."); err != nil {
		return err
	}
	//git commit . --> 因为后续要切换分支 所以当前的分支必须commit
	if err := gitCommand(projectPath, "commit", "-m", "commit本地分支"); err != nil {
		return err
	}

	//remote add
	githubPAT := os.Getenv("GITHUB_PAT") //这里是因为要使用docker 所以设置了这个

	//拼接URL
	remoteURL := fmt.Sprintf("https://%s@github.com/username/Script.git", githubPAT)

	//这里可以不进行错误处理,一般报错就是已经有了这个远程仓库,但还是建议第一次写一下,后续改回来
	_ = gitCommand(projectPath, "remote", "add", "origin", remoteURL)

	//强行pull分支
	_ = gitCommand(projectPath, "pull", "--force", "origin", "main:main")

	//切换到main分支
	_ = gitCommand(projectPath, "checkout", "main")
	return nil
}

// push代码
func commitChanges(projectPath, message string, times int) error {
	//add
	if err := gitCommand(projectPath, "add", "."); err != nil {
		return err
	}
	//commit
	if err := gitCommand(projectPath, "commit", "-m", message); err != nil {
		return err
	}
    //push
	if err := gitCommand(projectPath, "push", "-u", "origin", "main:main"); err != nil {
		return err
	}
	log.Println(fmt.Sprintf("push %d times today", times+1))
	return nil
}

这块不打算讲解了,主要是git操作,自己学一下比较好

然后我们可以简化一下代码

  • 无序列表去除文件操作里面close的错误处理,这是不必要的
  • 将文件修改和重置合在一起,即每次清空内容,然后追加带有时间戳的内容,以保证内容的不重复性

简单版的最终代码

所以我们的代码合在一起是

go 复制代码
package main

import (
	"fmt"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"time"
)

const (
	projectPath = "./" //设置当前项目目录
	fileName    = "fish.txt"
	intervalStr = "1s"
	message     = "我没摸鱼"
)

func main() {
	//首先得到正确的文件路径 用这个函数的好处是不用管操作系统的分隔符 管它是/还是\ go语言直接帮你解决
	path := filepath.Join(projectPath, fileName)

	//检测该文件是否存在
	if ifExist := checkFileExist(path); ifExist == false {
		if err := createFile(path); err != nil {
			log.Printf("createFile(path) failed,err%v", err)
			return
		}
		log.Println("createFile(path) success")
	}

	//格式化时间
	interval, err := time.ParseDuration(intervalStr)
	if err != nil {
		log.Printf("time.ParseDuration(intervalStr) failed,err:%v", err)
		return
	}
	ticker := time.NewTicker(interval)
	defer ticker.Stop()

	//根据条件来判断 对文件的操作
	for range ticker.C {
		if err := modifyFile(path); err != nil {
			log.Printf("modifyFile(fileName) failed,err:%v", err)
			return
		}
		log.Println("modifyFile(fileName) success")
		//提交git 代码待补充 这里不要写成path哦
		if err := commitChanges(projectPath, message); err != nil {
			log.Printf("commitChanges(path,message) failed,err:%v", err)
			return
		}
	}
}

// 检测文件是否存在
func checkFileExist(path string) bool {
	_, err := os.Stat(path)
	return !os.IsNotExist(err)
}

// 创建文件
func createFile(path string) error {
	//先创建文件 没有文件会默认创建一个
	fPointer, err := os.OpenFile(path, os.O_CREATE, 0644)
	if err != nil {
		log.Printf("create file failed,err:%v", err)
		return err
	}
	defer fPointer.Close()
	return err
}

// 修改文件
func modifyFile(path string) error {
	content := fmt.Sprintf("今天又摸鱼了鸭 %d", time.Now().UnixNano())
	fPointer, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0644) // os.O_WRONLY 仅写权限  |os.O_TRUNC 清空
	defer fPointer.Close()
	if err != nil {
		return err
	}
	_, err = fPointer.Write([]byte(content))
	return err
}

// / 初始化仓库
func initAndRemoteAdd(projectPath string) error {
	//init
	if err := gitCommand(projectPath, "init"); err != nil {
		return err
	}

	//git add . --> 因为后续要切换分支 所以当前的分支必须add
	if err := gitCommand(projectPath, "add", "."); err != nil {
		return err
	}
	//git commit . --> 因为后续要切换分支 所以当前的分支必须commit
	if err := gitCommand(projectPath, "commit", "-m", "commit本地分支"); err != nil {
		return err
	}

	//remote add 这里可以不进行错误处理,一般报错就是已经有了这个远程仓库,但还是建议第一次写一下,后续改回来
	remoteURL := "https://@github.com/username/Script.git"
	_ = gitCommand(projectPath, "remote", "add", "origin", remoteURL)

	//强行pull分支 不处理错误
	_ = gitCommand(projectPath, "pull", "--force", "origin", "main:main")

	//切换到main分支
	_ = gitCommand(projectPath, "checkout", "main")
	return nil
}

// push代码
func commitChanges(projectPath, message string) error {
	//初始化仓库
	if err := initAndRemoteAdd(projectPath); err != nil {
		return err
	}
	//add
	if err := gitCommand(projectPath, "add", "."); err != nil {
		return err
	}
	//commit
	if err := gitCommand(projectPath, "commit", "-m", message); err != nil {
		return err
	}
	//push
	if err := gitCommand(projectPath, "push", "-u", "origin", "main:main"); err != nil {
		return err
	}
	return nil
}

// 指令代码
func gitCommand(dir string, command ...string) (err error) {
	cmd := exec.Command("git", command...)
	cmd.Dir = dir
	var commandSql string
	for _, v := range command {
		commandSql += v + " "
	}
	output, err := cmd.CombinedOutput()
	if err != nil {
		log.Println(commandSql + "failed")
		log.Println("Command output:", string(output))
		return err
	}
	log.Println(commandSql + "success")
	return err
}

代码存在的潜在问题的解决

  1. 把每次修改文件设置的文字改成了prefix变量,避免了硬编码的出现
go 复制代码
prefix = "今天又摸鱼了鸭"
  1. 将固定的每天上传次数修改为一个随机数,并且把随机种子封装为一个函数,供多次调用
go 复制代码
func newSeed() *rand.Rand {
	// 创建一个新的随机数生成器实例,为其设置种子
	src := rand.NewSource(time.Now().UnixNano())
	return rand.New(src)
}
  1. 每天提交次数固定化了,我们用一个随机数来解决,并且每当隔一天就重新获取这个值
go 复制代码
// 如果跨越了一天,重置计数器,日期和最大push次数
if today != currentDay {
	commitTimes = 0
	today = currentDay
	maxCommitsPerDay = getRandomPushTimes()
}

完善后的代码

go 复制代码
package main

import (
	"fmt"
	"log"
	"math/rand"
	"os"
	"os/exec"
	"path/filepath"
	"time"
)

const (
	projectPath      = "./" //设置当前项目
	fileName         = "fish.txt"
	message          = "我没摸鱼"
	prefix           = "今天又摸鱼了鸭"
	maxCommitsPerDay = 5  // 每天可能的最大提交次数
	startHour        = 9  // 允许操作的开始小时(24小时制)
	endHour          = 24 // 允许操作的结束小时(24小时制)
)

func main() {
	//首先得到正确的文件路径 用这个函数的好处是不用管操作系统的分隔符 管它是/还是\ go语言直接帮你解决
	path := filepath.Join(projectPath, fileName)

	//检测该文件是否存在
	if ifExist := checkFileExist(path); ifExist == false {
		if err := createFile(path); err != nil {
			log.Printf("createFile(path) failed,err%v", err)
			return
		}
		log.Println("createFile(path) success")
	}

	// 定时逻辑
	var commitTimes = 0
	now := getChineseTime()
	today := now.Day()

	//格式化时间
	interval := getRandomInterval()
	ticker := time.NewTicker(interval)
	defer ticker.Stop()

	//初始化仓库
	if err := initAndRemoteAdd(projectPath); err != nil {
		log.Printf("initAndRemoteAdd(projectPath) failed,err:%v", err)
		return
	}

	maxCommitsPerDay := getRandomPushTimes()
	//根据条件来判断 对文件的操作
	for range ticker.C {
		log.Printf("%d chances today", maxCommitsPerDay)
		if err := modifyFile(path); err != nil {
			log.Printf("modifyFile(fileName) failed,err:%v", err)
			return
		}
		log.Println("modifyFile(fileName) success")

		now = getChineseTime()
		currentHour := now.Hour()
		currentDay := now.Day()

		// 如果跨越了一天,重置计数器,日期和每天的最大push次数
		if today != currentDay {
			commitTimes = 0
			today = currentDay
			maxCommitsPerDay = getRandomPushTimes()
		}

		// 检查是否在允许的时间段内 已经提交次数
		if currentHour >= startHour && currentHour < endHour && commitTimes < maxCommitsPerDay {
			//提交git 代码待补充
			if err := commitChanges(projectPath, message, commitTimes); err != nil {
				log.Printf("commitChanges(path,message) failed,err:%v", err)
				return
			}
			// 提交后增加计数器
			commitTimes++
		}
		// 更新间隔
		interval = getRandomInterval()
		ticker.Reset(interval)
	}
}

// 检测文件是否存在
func checkFileExist(path string) bool {
	_, err := os.Stat(path)
	return !os.IsNotExist(err)
}

// 创建文件
func createFile(path string) error {
	//先创建文件 没有文件会默认创建一个
	fPointer, err := os.OpenFile(path, os.O_CREATE, 0644)
	if err != nil {
		log.Printf("create file failed,err:%v", err)
		return err
	}
	defer fPointer.Close()
	return err
}

// 修改文件
func modifyFile(path string) error {
	content := fmt.Sprintf("%s %d", prefix, time.Now().UnixNano())
	fPointer, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0644) // os.O_WRONLY 仅写权限  |os.O_TRUNC 清空
	defer fPointer.Close()
	if err != nil {
		return err
	}
	_, err = fPointer.Write([]byte(content))
	return err
}

// 初始化仓库
func initAndRemoteAdd(projectPath string) error {
	//init
	if err := gitCommand(projectPath, "init"); err != nil {
		return err
	}

	//git add . --> 因为后续要切换分支 所以当前的分支必须add
	if err := gitCommand(projectPath, "add", "."); err != nil {
		return err
	}
	//git commit . --> 因为后续要切换分支 所以当前的分支必须commit
	if err := gitCommand(projectPath, "commit", "-m", "commit本地分支"); err != nil {
		return err
	}

	//remote add 这里可以不进行错误处理,一般报错就是已经有了这个远程仓库,但还是建议第一次写一下,后续改回来
	remoteURL := "https://@github.com/username/Script.git"
	_ = gitCommand(projectPath, "remote", "add", "origin", remoteURL)

	//强行pull分支 不处理错误
	_ = gitCommand(projectPath, "pull", "--force", "origin", "main:main")

	//切换到main分支
	_ = gitCommand(projectPath, "checkout", "main")
	return nil
}

// push代码
func commitChanges(projectPath, message string, times int) error {
	//add
	if err := gitCommand(projectPath, "add", "."); err != nil {
		return err
	}
	//commit
	if err := gitCommand(projectPath, "commit", "-m", message); err != nil {
		return err
	}
	//push
	if err := gitCommand(projectPath, "push", "-u", "origin", "main:main"); err != nil {
		return err
	}
	log.Println(fmt.Sprintf("push %d times today", times+1))
	return nil
}

// 指令代码
func gitCommand(dir string, command ...string) (err error) {
	cmd := exec.Command("git", command...)
	cmd.Dir = dir
	var commandSql string
	for _, v := range command {
		commandSql += v + " "
	}
	output, err := cmd.CombinedOutput()
	if err != nil {
		log.Println(commandSql + "failed")
		log.Println("Command output:", string(output))
		return err
	}
	log.Println(commandSql + "success")
	return err
}

// 设置为中国时区
func getChineseTime() time.Time {
	loc, _ := time.LoadLocation("Asia/Shanghai")
	now := time.Now().In(loc)
	return now
}

// 生成种子
func newSeed() *rand.Rand {
	// 创建一个新的随机数生成器实例,为其设置种子
	src := rand.NewSource(time.Now().UnixNano())
	return rand.New(src)
}

// 得到随机时间间隔
func getRandomInterval() time.Duration {
	// 将分钟数转换为字符串,格式为"Xm"
	return time.Duration(newSeed().Intn(30)+150) * time.Second
}

// 得到随机每天的push次数
func getRandomPushTimes() int {
	return newSeed().Intn(maxCommitsPerDay + 1)
}

自行理解吧,多看多进步。

1.温馨提示 因为使用到了checkout来切换分支 所以务必保证 切换到的分支已经有了需要使用的代码
2.确保checkout到的分支为主分支 否则commit次数不会增加

下次说如何部署到docker上如何进而在服务器上运行......