写在前面
近几天看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()接受两个参数,第一个是指令标识符,可以理解为前缀,git,docker这样指令都需要这个前缀,不然计算机怎么识别呢?
\
第二个参数是我们传进去的就是可变参数 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
}
代码存在的潜在问题的解决
- 把每次修改文件设置的文字改成了prefix变量,避免了硬编码的出现
go
prefix = "今天又摸鱼了鸭"
- 将固定的每天上传次数修改为一个随机数,并且把随机种子封装为一个函数,供多次调用
go
func newSeed() *rand.Rand {
// 创建一个新的随机数生成器实例,为其设置种子
src := rand.NewSource(time.Now().UnixNano())
return rand.New(src)
}
- 每天提交次数固定化了,我们用一个随机数来解决,并且每当隔一天就重新获取这个值
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上如何进而在服务器上运行......