GoSungrow/mmGit/gitFilesystem.go
2022-02-22 10:56:05 +11:00

693 lines
12 KiB
Go

package mmGit
import (
"GoSungrow/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
}