From 243ceb9569b6cd4b452ba1f2fd8b328b218d220f Mon Sep 17 00:00:00 2001 From: Hector <54723548+Syycorax@users.noreply.github.com> Date: Tue, 22 Feb 2022 20:04:57 +0100 Subject: [PATCH] Gofork 1.0.0 --- .gitignore | 2 + README.md | 30 +++++ config.json.example | 3 + go.mod | 16 ++- go.sum | 36 +++++- gofork.go | 303 ++++++++++++++++++++++++++++++++++++++------ 6 files changed, 345 insertions(+), 45 deletions(-) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 config.json.example diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c5487ed --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.vscode +config.json \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a090178 --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +# Gofork +## Presentation +Gofork is a CLI tool to find forks that are ahead of a github repository. +## Usage +``` +$ gofork --help +usage: gofork [-h|--help] -r|--repo "" [-b|--branch ""] + [-p|--private] + + CLI tool to find active forks + +Arguments: + + -h --help Print help information + -r --repo Repository to check + -b --branch Branch to check. Default: repo default branch + -p --private Show private repositories +``` +## Roadmap +* [x] Print the results in table +* [x] Add support for branches (with the default being the repo default branch) +* [x] Use terminal colors +* [x] Verbose flag for private/even forks +* [x] Loading bar +* [X] Sort output +## Built with +Built with love using [Golang](https://golang.org), [Github API](https://developer.github.com/v3/) and [akamensky's argparse](github.com/akamensky/argparse), [gookit's color](github.com/gookit/color), [olekukonko's tablewriter](github.com/olekukonko/tablewriter), [schollz's progressbar](github.com/schollz/progressbar/v3) libraries. + +## License +[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) diff --git a/config.json.example b/config.json.example new file mode 100644 index 0000000..60f2805 --- /dev/null +++ b/config.json.example @@ -0,0 +1,3 @@ +{ + "PAT": "your github personal acess token, no scope required (https://github.com/settings/tokens)" +} \ No newline at end of file diff --git a/go.mod b/go.mod index b497fe0..eb4283a 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,20 @@ -module github.com/Syycorax/gofork +module github.com/syycorax/gofork go 1.17 -require github.com/gookit/color v1.5.0 +require ( + github.com/akamensky/argparse v1.3.1 + github.com/gookit/color v1.5.0 + github.com/olekukonko/tablewriter v0.0.5 + github.com/schollz/progressbar/v3 v3.8.6 +) require ( + github.com/mattn/go-runewidth v0.0.13 // indirect + github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect + github.com/rivo/uniseg v0.2.0 // indirect github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect - golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44 // indirect + golang.org/x/crypto v0.0.0-20220209195652-db638375bc3a // indirect + golang.org/x/sys v0.0.0-20220209214540-3681064d5158 // indirect + golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect ) diff --git a/go.sum b/go.sum index d799a39..e986e2c 100644 --- a/go.sum +++ b/go.sum @@ -1,16 +1,48 @@ -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/akamensky/argparse v1.3.1 h1:kP6+OyvR0fuBH6UhbE6yh/nskrDEIQgEA1SUXDPjx4g= +github.com/akamensky/argparse v1.3.1/go.mod h1:S5kwC7IuDcEr5VeXtGPRVZ5o/FdhcMlQz4IZQuw64xA= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gookit/color v1.5.0 h1:1Opow3+BWDwqor78DcJkJCIwnkviFi+rrOANki9BUFw= github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= +github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/schollz/progressbar/v3 v3.8.6 h1:QruMUdzZ1TbEP++S1m73OqRJk20ON11m6Wqv4EoGg8c= +github.com/schollz/progressbar/v3 v3.8.6/go.mod h1:W5IEwbJecncFGBvuEh4A7HT1nZZ6WNIL2i3qbnI0WKY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44 h1:Bli41pIlzTzf3KEY06n+xnzK/BESIg2ze4Pgfh/aI8c= +golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220209195652-db638375bc3a h1:atOEWVSedO4ksXBe/UrlbSLVxQQ9RxM/tT2Jy10IaHo= +golang.org/x/crypto v0.0.0-20220209195652-db638375bc3a/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158 h1:rm+CHSpPEEW2IsXUib1ThaHIjuBVZjxNgSKmBLFfD4c= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/gofork.go b/gofork.go index 31d92e2..15eb56c 100644 --- a/gofork.go +++ b/gofork.go @@ -1,74 +1,297 @@ package main import ( + "bufio" + "container/list" "encoding/json" - "fmt" "io/ioutil" - "log" "net/http" "os" + "runtime" + "strconv" + "strings" + "github.com/akamensky/argparse" "github.com/gookit/color" + "github.com/olekukonko/tablewriter" + "github.com/schollz/progressbar/v3" ) -type Repo_info struct { - Forks_count int `json:"forks_count"` - Fork_url string `json:"forks_url"` +type RepoInfo struct { + ForkCount int `json:"forks_count"` + Owner Owner + DefaultBranch string `json:"default_branch"` } -type Fork_info struct { - Forks []struct { - Fork_url string `json:"html_url"` - Author string `json:"owner.login"` - Full_name string `json:"full_name"` - } +type Owner struct { + Login string `json:"login"` } -func Error(err error) { - if err != nil { - log.Fatal(err) - } +type Fork struct { + FullName string `json:"full_name"` + Url string `json:"html_url"` + Status string `json:"status"` + AheadBy int `json:"ahead_by"` + BehindBy int `json:"behind_by"` +} + +type Auth struct { + Token string `json:"PAT"` } func main() { var ( - repo_info Repo_info - fork_info Fork_info + RepoInfo RepoInfo + forks []Fork + auth Auth ) fail := "[X]" success := "[✓]" working := "[+]" - repo := os.Args[1] - fmt.Println(working + " Looking for " + repo) - url := "https://api.github.com/repos/" + repo + mitigate := "[~]" + parser := argparse.NewParser("gofork", "CLI tool to find active forks") + repo := parser.String("r", "repo", &argparse.Options{Required: false, Help: "Repository to check"}) + branch := parser.String("b", "branch", &argparse.Options{Required: false, Help: "Branch to check", Default: "repo default branch"}) + verboseflag := parser.Flag("v", "verbose", &argparse.Options{Help: "Show private and up to date repositories"}) + page := parser.Int("p", "page", &argparse.Options{Help: "Page to check", Default: 1, Required: false}) + err := parser.Parse(os.Args) + if err != nil { + color.Error.Println(parser.Usage(err)) + os.Exit(1) + } + platform := runtime.GOOS + dat, _ := os.ReadFile("./config.json") + json.Unmarshal([]byte(dat), &auth) + if auth.Token == "" { + color.Error.Println("Please provide a PAT (https://tinyurl.com/GITHUBPAT) (no scope required)") + reader := bufio.NewReader(os.Stdin) + input, _ := reader.ReadString('\n') + if platform == "windows" { + input = strings.Replace(input, "\r\n", "", -1) + } else { + input = strings.Replace(input, "\n", "", -1) + } + output := "{\"PAT\": \"" + input + "\"}" + ioutil.WriteFile("./config.json", []byte(output), 0644) + color.Success.Println("PAT saved") + dat, _ := os.ReadFile("./config.json") + json.Unmarshal([]byte(dat), &auth) + } + if *repo == "" { + color.Error.Println("Please provide a repository") + os.Exit(1) + } + color.Notice.Println(working + " Looking for " + *repo) + url := "https://api.github.com/repos/" + *repo req, _ := http.NewRequest("GET", url, nil) - resp, err := http.DefaultClient.Do(req) - Error(err) - defer resp.Body.Close() - body, _ := ioutil.ReadAll(resp.Body) - json_response := string(body) - json.Unmarshal([]byte(json_response), &repo_info) + req.Header.Add("Authorization", "token "+string(auth.Token)) + resp, _ := http.DefaultClient.Do(req) if resp.StatusCode == http.StatusNotFound { - color.Red.Println(fail + " Repository not found") + if platform == "windows" { + color.Error.Print(fail + " Repository not found") + } else { + color.Error.Print(fail + " Repository not found\n") + } + } else if resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusUnauthorized { + color.Error.Println(fail + " Incorrect PAT, do you want to delete config file? (y/n)") + + reader := bufio.NewReader(os.Stdin) + input, _ := reader.ReadString('\n') + if platform == "windows" { + input = strings.Replace(input, "\r\n", "", -1) + } else { + input = strings.Replace(input, "\n", "", -1) + } + if input == "y" { + os.Remove("./config.json") + color.Success.Println("PAT deleted") + os.Exit(1) + } else { + if platform == "windows" { + color.Error.Println("Incorrect PAT provided exiting") + } else { + color.Error.Print("Incorrect PAT provided exiting\n") + } + os.Exit(1) + } } else { - color.Green.Println(success + " Repository found") - if repo_info.Forks_count == 0 { - color.Red.Println(fail + " No forks found") + color.Success.Println(success + " Repository found") + body, _ := ioutil.ReadAll(resp.Body) + json.Unmarshal(body, &RepoInfo) + if *branch == "repo default branch" { + *branch = RepoInfo.DefaultBranch + } + color.Notice.Println(working + " Looking for " + *repo + ":" + *branch) + if RepoInfo.ForkCount == 0 { + if platform == "windows" { + color.Error.Print(fail + " No forks found") + } else { + color.Error.Print(fail + " No forks found \n") + } } else { - color.Green.Println(success, repo_info.Forks_count, "Forks found") - url = "http://api.github.com/repos/" + repo + "/forks" + color.Success.Println(success, RepoInfo.ForkCount, "Forks found") + if RepoInfo.ForkCount > 100 && *page == 1 { + RepoInfo.ForkCount = 100 + color.Info.Println(mitigate + " More than 100 forks found, only showing first 100 (use -p to get other results)") + } + if RepoInfo.ForkCount > 100 && *page > 1 { + RepoInfo.ForkCount = 100 + color.Info.Println(mitigate + " More than 100 forks found, showing page " + strconv.Itoa(*page)) + } + url = "https://api.github.com/repos/" + *repo + "/forks?per_page=" + strconv.Itoa(RepoInfo.ForkCount) + if *page != 1 { + url = url + "&page=" + strconv.Itoa(*page) + } req, _ = http.NewRequest("GET", url, nil) - resp, err = http.DefaultClient.Do(req) - Error(err) - defer resp.Body.Close() + req.Header.Add("Authorization", "token "+string(auth.Token)) + resp, _ = http.DefaultClient.Do(req) body, _ = ioutil.ReadAll(resp.Body) - json_response = string(body) - json.Unmarshal([]byte(json_response), &fork_info) - for _, fork := range fork_info.Forks { - fmt.Println(fork.Fork_url) + json.Unmarshal(body, &forks) + ahead := list.New() + behind := list.New() + diverge := list.New() + even := list.New() + private := list.New() + bar := progressbar.Default(int64(RepoInfo.ForkCount)) + for _, fork := range forks { + url = "https://api.github.com/repos/" + fork.FullName + "/compare/" + RepoInfo.Owner.Login + ":" + *branch + "..." + *branch + req, _ = http.NewRequest("GET", url, nil) + req.Header.Add("Authorization", "token "+string(auth.Token)) + resp, _ = http.DefaultClient.Do(req) + body, _ = ioutil.ReadAll(resp.Body) + json.Unmarshal(body, &fork) + if fork.Status == "ahead" { + ahead.PushBack(fork) + } else if fork.Status == "behind" { + behind.PushBack(fork) + } else if fork.Status == "identical" { + even.PushBack(fork) + } else if fork.Status == "diverged" { + diverge.PushBack(fork) + } else { + private.PushBack(fork) + } + bar.Add(1) + } + // sort ahead by ahead_by descending + for e := ahead.Front(); e != nil; e = e.Next() { + for f := e.Next(); f != nil; f = f.Next() { + if e.Value.(Fork).AheadBy < f.Value.(Fork).AheadBy { + e.Value, f.Value = f.Value, e.Value + } + } } + // sort behind by behind_by ascending + for e := behind.Front(); e != nil; e = e.Next() { + for f := e.Next(); f != nil; f = f.Next() { + if e.Value.(Fork).BehindBy > f.Value.(Fork).BehindBy { + e.Value, f.Value = f.Value, e.Value + } + } + } + // sort diverge by ahead_by descending + for e := diverge.Front(); e != nil; e = e.Next() { + for f := e.Next(); f != nil; f = f.Next() { + if e.Value.(Fork).AheadBy < f.Value.(Fork).AheadBy { + e.Value, f.Value = f.Value, e.Value + } + } + } + aheadtable := tablewriter.NewWriter(os.Stdout) + aheadtable.SetHeader([]string{"Fork", "Ahead by", "URL"}) + aheadmap := [][]string{} + for e := ahead.Front(); e != nil; e = e.Next() { + fork := e.Value.(Fork) + aheadBy := strconv.Itoa(fork.AheadBy) + url := "https://github.com/" + string(fork.FullName) + aheadmap = append(aheadmap, []string{fork.FullName, aheadBy, url}) + } + for _, v := range aheadmap { + aheadtable.Append(v) + } + if ahead.Len() > 0 { + color.Success.Println(success + " Forks ahead:") + aheadtable.Render() + } else { + color.Notice.Println(mitigate + " No forks ahead of " + RepoInfo.Owner.Login + ":" + *branch) + } + divergetable := tablewriter.NewWriter(os.Stdout) + divergetable.SetHeader([]string{"Fork", "Ahead by", "Behind by", "URL"}) + divergemap := [][]string{} + for e := diverge.Front(); e != nil; e = e.Next() { + fork := e.Value.(Fork) + aheadBy := strconv.Itoa(fork.AheadBy) + behindBy := strconv.Itoa(fork.BehindBy) + url := "https://github.com/" + string(fork.FullName) + divergemap = append(divergemap, []string{fork.FullName, aheadBy, behindBy, url}) + } + for _, v := range divergemap { + divergetable.Append(v) + } + if diverge.Len() > 0 { + color.Notice.Println(mitigate + " Forks diverged:") + divergetable.Render() + } else { + color.Notice.Println(mitigate + " No forks diverged of " + RepoInfo.Owner.Login + ":" + *branch) + } + behindtable := tablewriter.NewWriter(os.Stdout) + behindtable.SetHeader([]string{"Fork", "Behind by", "URL"}) + behindmap := [][]string{} + for e := behind.Front(); e != nil; e = e.Next() { + fork := e.Value.(Fork) + behindBy := strconv.Itoa(fork.BehindBy) + url := "https://github.com/" + string(fork.FullName) + behindmap = append(behindmap, []string{fork.FullName, behindBy, url}) + } + for _, v := range behindmap { + behindtable.Append(v) + } + if behind.Len() > 0 { + color.Warn.Println(fail + " Forks behind:") + behindtable.Render() + } else { + color.Notice.Println(mitigate + " No forks behind of " + RepoInfo.Owner.Login + ":" + *branch) + } + if *verboseflag { + eventable := tablewriter.NewWriter(os.Stdout) + eventable.SetHeader([]string{"Fork", "URL"}) + eventmap := [][]string{} + for e := even.Front(); e != nil; e = e.Next() { + fork := e.Value.(Fork) + url := "https://github.com" + string(fork.FullName) + eventmap = append(eventmap, []string{fork.FullName, url}) + } + for _, v := range eventmap { + eventable.Append(v) + } + if even.Len() > 0 { + color.Notice.Println(mitigate + " Forks up to date:") + eventable.Render() + } else { + color.Notice.Println(mitigate + " No forks identical to " + RepoInfo.Owner.Login + ":" + *branch) + } + privatetable := tablewriter.NewWriter(os.Stdout) + privatetable.SetHeader([]string{"Fork", "URL"}) + privatemap := [][]string{} + for e := private.Front(); e != nil; e = e.Next() { + fork := e.Value.(Fork) + url := "https://github.com" + string(fork.FullName) + privatemap = append(privatemap, []string{fork.FullName, url}) + } + for _, v := range privatemap { + privatetable.Append(v) + } + if private.Len() > 0 { + color.Question.Println(mitigate + " Private forks:") + privatetable.Render() + } else { + color.Notice.Println(mitigate + " No forks private of " + RepoInfo.Owner.Login + ":" + *branch) + } + } + if ahead.Len() == 0 && behind.Len() == 0 && even.Len() == 0 && *branch == "master" { + color.Error.Println(fail, "No forks found on branch master, maybe try with main?") + } } } - }