package mmGit import ( "GoSungro/Only" "bufio" "context" "errors" "fmt" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" "github.com/go-git/go-git/v5/plumbing/transport/ssh" "os" "os/exec" "os/signal" "os/user" "path/filepath" "strings" "syscall" "time" ) func (z *Git) Connect() error { for range Only.Once { if z.IsNotOk() { break } var ok bool ok, z.Error = IsDirExists(z.RepoDir) if z.Error != nil { break } if ok { z.Error = z.Open() break } z.Error = z.Clone() if z.Error != nil { break } } return z.Error } func (z *Git) Open() error { for range Only.Once { if z.IsNotOk() { break } z.repo, z.Error = git.PlainOpen(z.RepoDir) if z.Error != nil { break } z.worktree, z.Error = z.repo.Worktree() if z.Error != nil { break } var ref *plumbing.Reference ref, z.Error = z.repo.Head() if z.Error != nil { break } if ref.Hash().IsZero() { z.Error = errors.New("invalid HEAD reference") break } fmt.Printf("Git opened\n\trepo: %s\n\tdir: %s\n", z.RepoUrl, z.RepoDir) } return z.Error } func (z *Git) Clone() error { for range Only.Once { if z.IsNotOk() { break } var ok bool ok, z.Error = IsDirExists(z.RepoDir) if z.Error != nil { break } if ok { z.Error = errors.New(fmt.Sprintf("Cannot clone - directory '%s' already exists.", z.RepoDir)) break } // CONTEXT: Provide Ctrl-C capability as well as operation timeouts. stop := make(chan os.Signal, 1) signal.Notify(stop, os.Interrupt) ctx, cancel := context.WithCancel(context.Background()) defer cancel() go func() { <-stop fmt.Println("\nCanceling operation...") cancel() }() // CONTEXT: Provide Ctrl-C capability as well as operation timeouts. pk := z.GetSshAuth() if z.Error != nil { break } options := &git.CloneOptions { URL: z.RepoUrl, Auth: pk, RemoteName: "", ReferenceName: "", SingleBranch: false, NoCheckout: false, Depth: 0, RecurseSubmodules: 0, Progress: os.Stdout, Tags: 0, InsecureSkipTLS: false, CABundle: nil, } z.repo, z.Error = git.PlainCloneContext(ctx, z.RepoDir, false, options) if z.Error != nil { break } z.worktree, z.Error = z.repo.Worktree() if z.Error != nil { break } var ref *plumbing.Reference ref, z.Error = z.repo.Head() if z.Error != nil { break } if ref.Hash().IsZero() { z.Error = errors.New("invalid HEAD reference") break } fmt.Printf("Git cloned\n\trepo: %s\n\tdir: %s\n", z.RepoUrl, z.RepoDir) } return z.Error } // GetSshAuth: Gitlab keys need to be created with at least 3072 bits. // ssh-keygen -t rsa -b 3072 -C 'root@everywhere' -f gitlab_rsa -N '' func (z *Git) GetSshAuth() *ssh.PublicKeys { var pk *ssh.PublicKeys for range Only.Once { if z.IsNotOk() { break } var u *user.User u, z.Error = user.Current() paths := []string { z.KeyFile, filepath.Join(u.HomeDir, ".ssh", "id_rsa"), } var path string for _, path = range paths { if path == "" { continue } z.Error = checkKeyFile(path) if z.Error != nil { continue } break } // Try without password first. var password string pk, z.Error = ssh.NewPublicKeysFromFile("git", path, password) if z.Error == nil { fmt.Printf("AUTH: %v\n", pk) break } // Then with password. password = getPassword("ApiPassword: ") pk, z.Error = ssh.NewPublicKeysFromFile("git", path, password) if z.Error == nil { fmt.Printf("AUTH: %v\n", pk) break } } return pk } func checkKeyFile(path string) error { var err error for range Only.Once { if path == "" { continue } var fi os.FileInfo fi, err = os.Stat(path) if os.IsNotExist(err) { continue } if fi.IsDir() { err = errors.New("SSH publickey file is a directory") continue } } return err } // techEcho() - turns terminal echo on or off. func termEcho(on bool) { // Common settings and variables for both stty calls. attrs := syscall.ProcAttr{ Dir: "", Env: []string{}, Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd()}, Sys: nil} var ws syscall.WaitStatus cmd := "echo" if on == false { cmd = "-echo" } // Enable/disable echoing. pid, err := syscall.ForkExec( "/bin/stty", []string{"stty", cmd}, &attrs) if err != nil { panic(err) } // Wait for the stty process to complete. _, err = syscall.Wait4(pid, &ws, 0, nil) if err != nil { panic(err) } } // getPassword - Prompt for password. func getPassword(prompt string) string { fmt.Print(prompt) // Catch a ^C interrupt. // Make sure that we reset term echo before exiting. signalChannel := make(chan os.Signal, 1) signal.Notify(signalChannel, os.Interrupt) go func() { for _ = range signalChannel { fmt.Println("\n^C interrupt.") termEcho(true) os.Exit(1) } }() // Echo is disabled, now grab the data. termEcho(false) // disable terminal echo reader := bufio.NewReader(os.Stdin) text, err := reader.ReadString('\n') termEcho(true) // always re-enable terminal echo fmt.Println("") if err != nil { // The terminal has been reset, go ahead and exit. fmt.Println("ERROR:", err.Error()) os.Exit(1) } return strings.TrimSpace(text) } //func (z *Git) setContext() error { // // for range Only.Once { // if z.IsNotOk() { // break // } // // stop := make(chan os.Signal, 1) // signal.Notify(stop, os.Interrupt) // ctx, cancel := context.WithCancel(context.Background()) // defer cancel() // cancel when we are finished consuming integers // // go func() { // <-stop // Warning("\nSignal detected, canceling operation...") // cancel() // }() // // var auth ssh.AuthMethod // auth, z.Error = ssh.DefaultAuthBuilder("admin-mickh") // if z.Error != nil { // break // } // // z.repo, z.Error = git.PlainClone(z.RepoDir, false, &git.CloneOptions { // URL: z.RepoUrl, // Auth: auth, // }) // if z.Error != nil { // break // } // // var ref *plumbing.Reference // ref, z.Error = z.repo.Head() // if z.Error != nil { // break // } // // var commit *object.Commit // commit, z.Error = z.repo.CommitObject(ref.Hash()) // if z.Error != nil { // break // } // // fmt.Println(commit) // } // // return z.Error //} func (z *Git) SaveFile(fn string, data []byte) error { for range Only.Once { if z.IsNotOk() { break } //z.worktree, z.Error = z.repo.Worktree() //if z.Error != nil { // break //} // //var fh fs.File //var fi os.FileInfo //fi, z.Error = z.fs.Stat(fn) //if errors.Is(z.Error, os.ErrNotExist) { // // Create new file // fh, z.Error = z.fs.Create(fn) //} else { // // Open file // fh, z.Error = z.fs.OpenFile(fn, os.O_RDWR|os.O_CREATE, 0664) //} fh, err := os.OpenFile(filepath.Join(z.RepoDir, fn), os.O_RDWR|os.O_CREATE, 0664) if err != nil { z.Error = err break } defer fh.Close() fmt.Printf("Saved file '%s'\n", fn) _, z.Error = fh.Write(data) if z.Error != nil { break } // Run git status before adding the file to the worktree //fmt.Println(z.worktree.Status()) // git add $filePath _, z.Error = z.worktree.Add(fn) if z.Error != nil { break } //// Run git status after the file has been added adding to the worktree //fmt.Println(z.worktree.Status()) // //// git commit -m $message //msg := fmt.Sprintf("Updated file '%s'", fn) //_, z.Error = z.worktree.Commit(msg, &git.CommitOptions{}) //if z.Error != nil { // break //} } return z.Error } func (z *Git) Status() error { for range Only.Once { if z.IsNotOk() { break } var status git.Status status, z.Error = z.worktree.Status() if z.Error != nil { break } if status.String() != "" { fmt.Printf("Status of Git\n\trepo: %s\n\tdir: %s\n%s\n", z.RepoUrl, z.RepoDir, status.String(), ) } } return z.Error } func (z *Git) Add(path string) error { for range Only.Once { if z.IsNotOk() { break } if path == "" { path = "." } fmt.Printf("Adding to Git\n\trepo: %s\n\tdir: %s\n", z.RepoUrl, z.RepoDir) _, z.Error = z.worktree.Add(path) if z.Error != nil { break } z.Error = z.Status() if z.Error != nil { break } } //PrintError(z.Error) return z.Error } func (z *Git) Commit(msg string, args ...interface{}) error { for range Only.Once { if z.IsNotOk() { break } z.Error = z.Add(".") if z.Error != nil { break } cn := &object.Signature { Name: os.Getenv("USERNAME"), Email: "", When: time.Now(), } fmt.Printf("Committing Git\n\trepo: %s\n\tdir: %s\n", z.RepoUrl, z.RepoDir) // Similar to git commit -m $message var ph plumbing.Hash msg := fmt.Sprintf(msg, args...) ph, z.Error = z.worktree.Commit(msg, &git.CommitOptions{ All: false, Author: cn, Committer: cn, Parents: nil, SignKey: nil, }) if z.Error != nil { break } // Similar to git show -s var obj *object.Commit obj, z.Error = z.repo.CommitObject(ph) if z.Error != nil { break } if obj.String() != "" { fmt.Printf("Status of Git\n\trepo: %s\n\tdir: %s\n%s\n", z.RepoUrl, z.RepoDir, obj.String(), ) } } //PrintError(z.Error) return z.Error } func (z *Git) Pull() error { for range Only.Once { if z.IsNotOk() { break } pk := z.GetSshAuth() if z.Error != nil { break } fmt.Printf("Pulling Git\n\trepo: %s\n\tdir: %s\n", z.RepoUrl, z.RepoDir) z.Error = z.worktree.Pull(&git.PullOptions { RemoteName: "", ReferenceName: "", SingleBranch: false, Depth: 0, Auth: pk, RecurseSubmodules: 0, Progress: os.Stdout, Force: false, InsecureSkipTLS: false, CABundle: nil, }) if z.Error.Error() == "already up-to-date" { z.Error = nil break } if z.Error != nil { break } } //PrintError(z.Error) return z.Error } func (z *Git) Push() error { for range Only.Once { if z.IsNotOk() { break } z.Error = z.Commit("Updated") if z.Error != nil { break } pk := z.GetSshAuth() if z.Error != nil { break } fmt.Printf("Pushing Git\n\trepo: %s\n\tdir: %s\n", z.RepoUrl, z.RepoDir) z.Error = z.repo.Push(&git.PushOptions{ RemoteName: "", RefSpecs: nil, Auth: pk, Progress: os.Stdout, Prune: false, Force: false, InsecureSkipTLS: false, CABundle: nil, RequireRemoteRefs: nil, }) if z.Error != nil { break } } //PrintError(z.Error) return z.Error } func (z *Git) Diff(path string) error { for range Only.Once { var c []CommitDiffs c, z.Error = z.GetDiffs(path) if z.Error != nil { break } if len(c) < 2 { fmt.Printf("Not enough revisions to compare.\n") break } f1 := fmt.Sprintf("%s-%s", path, c[0].Hash) f1, z.Error = WriteTempFile(f1, c[0].Contents) if z.Error != nil { break } f2 := fmt.Sprintf("%s-%s", path, c[1].Hash) f2, z.Error = WriteTempFile(f2, c[1].Contents) if z.Error != nil { break } if z.DiffCmd == "" { z.DiffCmd = "tkdiff" } z.DiffCmd, z.Error = exec.LookPath(z.DiffCmd) cmd := exec.Command(z.DiffCmd, f1, f2) var out []byte out, z.Error = cmd.Output() //if z.Error != nil { // break //} fmt.Printf("# %s\n", cmd.String()) fmt.Println(string(out)) if z.Error != nil { break } } //PrintError(z.Error) return z.Error } type CommitDiffs struct { Hash string Contents string } func (z *Git) GetDiffs(path string) ([]CommitDiffs, error) { var ret []CommitDiffs for range Only.Once { if z.IsNotOk() { break } fmt.Printf("Diff Git\n\trepo: %s\n\tdir: %s\n", z.RepoUrl, z.RepoDir) ref, _ := z.repo.Head() //fmt.Printf("ref '%s'\n", ref.String()) commit, _ := z.repo.CommitObject(ref.Hash()) //fmt.Printf("commit '%s'\n", commit.String()) var comm []*object.Commit commitIter, _ := z.repo.Log(&git.LogOptions{From: commit.Hash}) _ = commitIter.ForEach(func(c *object.Commit) error { comm = append(comm, c) return nil }) var lastHash string //for k := 0; k < len(comm)-1; k++ { for k, _ := range comm { fmt.Printf("# Commit number[%d]: %s", k, comm[k].Hash) f2, _ := comm[k].File(path) if f2 == nil { fmt.Println(" - no path") continue } fc, _ := f2.Contents() hs := GetHash(fc) if hs == lastHash { fmt.Println(" - no change") continue } lastHash = hs ret = append(ret, CommitDiffs{ Hash: comm[k].Hash.String(), Contents: fc, }) fmt.Println(" - changed") } } return ret, z.Error }