package main import ( "errors" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing/object" gitHttp "github.com/go-git/go-git/v5/plumbing/transport/http" "io/ioutil" "os" "path" "sync" "time" ) type Storage interface { CreateFile(path string, file string, message string) (err error) UpdateFile(path string, file string, message string) (err error) DeleteFile(path string, message string) (err error) } type Git struct { filepath string url string username string password string name string email string lock *sync.Mutex } func (g *Git) init() (r *git.Repository, w *git.Worktree, err error) { // Open repo r, err = git.PlainOpen(g.filepath) if err == nil { // Try to get work tree w, err = r.Worktree() if err == nil { // Try to pull err = w.Pull(&git.PullOptions{ Auth: &gitHttp.BasicAuth{ Username: g.username, Password: g.password, }, SingleBranch: true, }) if err == git.NoErrAlreadyUpToDate { err = nil } } } if err != nil { // Delete old things g.destroyRepo() // Clone r, err = git.PlainClone(g.filepath, false, &git.CloneOptions{ Auth: &gitHttp.BasicAuth{ Username: g.username, Password: g.password, }, URL: g.url, Depth: 1, SingleBranch: true, }) if err != nil { err = errors.New("failed to clone repo") } if err == nil { w, err = r.Worktree() if err != nil { err = errors.New("failed to get work tree") } } } return } func (g *Git) destroyRepo() { _ = os.RemoveAll(g.filepath) } func (g *Git) push(repository *git.Repository) error { err := repository.Push(&git.PushOptions{ Auth: &gitHttp.BasicAuth{ Username: g.username, Password: g.password, }, }) if err == nil || err == git.NoErrAlreadyUpToDate { return nil } else { // Destroy repo to prevent errors when trying to create same post again g.destroyRepo() return errors.New("failed to push to remote") } } func (g *Git) CreateFile(filepath string, file string, message string) (err error) { g.lock.Lock() defer g.lock.Unlock() _, _, err = g.init() if err != nil { err = errors.New("failed to initialize repo") return } joinedPath := path.Join(g.filepath, filepath) if _, e := os.Stat(joinedPath); e == nil { return errors.New("file already exists") } else { return g.unsafeUpdateFile(filepath, file, message) } } func (g *Git) UpdateFile(filepath string, file string, message string) error { g.lock.Lock() defer g.lock.Unlock() return g.unsafeUpdateFile(filepath, file, message) } func (g *Git) unsafeUpdateFile(filepath string, file string, message string) error { repo, w, err := g.init() if err != nil { return errors.New("failed to initialize repo") } joinedPath := path.Join(g.filepath, filepath) _ = os.MkdirAll(path.Dir(joinedPath), 0755) err = ioutil.WriteFile(joinedPath, []byte(file), 0644) if err != nil { return errors.New("failed to write to file") } status, err := w.Status() if err == nil && status.IsClean() { // No file changes, prevent empty commit return nil } else { err = nil } _, err = w.Add(filepath) if err != nil { return errors.New("failed to stage file") } if len(message) == 0 { message = "Add " + filepath } _, err = w.Commit(message, &git.CommitOptions{ Author: &object.Signature{ Name: g.name, Email: g.email, When: time.Now(), }, }) if err != nil { return errors.New("failed to commit file") } return g.push(repo) } func (g *Git) DeleteFile(filepath string, message string) (err error) { g.lock.Lock() defer g.lock.Unlock() repo, w, err := g.init() if err != nil { return errors.New("failed to initialize repo") } joinedPath := path.Join(g.filepath, filepath) err = os.Remove(joinedPath) if err != nil { return errors.New("failed to delete file") } _, err = w.Add(filepath) if err != nil { return errors.New("failed to stage deletion") } _, err = w.Commit(message, &git.CommitOptions{ Author: &object.Signature{ Name: g.name, Email: g.email, When: time.Now(), }, }) if err != nil { return errors.New("failed to commit deletion") } return g.push(repo) }