diff --git a/internal/git-save/save.go b/internal/git-save/save.go new file mode 100644 index 0000000..847fa2d --- /dev/null +++ b/internal/git-save/save.go @@ -0,0 +1,224 @@ +// Copyright 2019 Sorint.lab +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied +// See the License for the specific language governing permissions and +// limitations under the License. + +package gitsave + +import ( + "context" + "io" + "os" + "path/filepath" + + "github.com/sorintlab/agola/internal/util" + + "github.com/pkg/errors" + "go.uber.org/zap" +) + +const ( + gitIndexFile = "index" + gitRefsPrefix = "refs/gitsave" +) + +func copyFile(src, dest string) error { + srcf, err := os.Open(src) + if err != nil { + return err + } + defer srcf.Close() + + destf, err := os.Create(dest) + if err != nil { + return err + } + defer destf.Close() + + _, err = io.Copy(destf, srcf) + return err +} + +func fileExists(path string) (bool, error) { + _, err := os.Stat(path) + if err != nil && !os.IsNotExist(err) { + return false, err + } + return !os.IsNotExist(err), nil +} + +// GitDir returns the git dir relative to the working dir +func GitDir() (string, error) { + git := &util.Git{} + lines, err := git.OutputLines(context.Background(), nil, "rev-parse", "--git-dir") + if err != nil { + return "", err + } + if len(lines) != 1 { + return "", errors.Errorf("received %d lines, expected one line", len(lines)) + } + return lines[0], err +} + +func currentGitBranch() (string, error) { + git := &util.Git{} + lines, err := git.OutputLines(context.Background(), nil, "symbolic-ref", "--short", "HEAD") + if err != nil { + return "", err + } + if len(lines) != 1 { + return "", errors.Errorf("received %d lines, expected one line", len(lines)) + } + return lines[0], err +} + +// gitDir returns the git dir relative to the working dir +func gitWriteTree(indexPath string) (string, error) { + git := &util.Git{Env: []string{"GIT_INDEX_FILE=" + indexPath}} + lines, err := git.OutputLines(context.Background(), nil, "write-tree") + if err != nil { + return "", err + } + if len(lines) != 1 { + return "", errors.Errorf("received %d lines, expected one line", len(lines)) + } + return lines[0], err +} + +func gitCommitTree(message, treeSHA string) (string, error) { + git := &util.Git{} + lines, err := git.OutputLines(context.Background(), nil, "commit-tree", "-m", message, treeSHA) + if err != nil { + return "", err + } + if len(lines) != 1 { + return "", errors.Errorf("received %d lines, expected one line", len(lines)) + } + return lines[0], err +} + +func gitUpdateRef(message, ref, commitSHA string) error { + git := &util.Git{} + _, err := git.Output(context.Background(), nil, "update-ref", "-m", message, ref, commitSHA) + return err +} + +func gitAddUntrackedFiles(indexPath string) error { + git := &util.Git{Env: []string{"GIT_INDEX_FILE=" + indexPath}} + _, err := git.Output(context.Background(), nil, "add", ".") + return err +} + +func gitAddIgnoredFiles(indexPath string) error { + git := &util.Git{Env: []string{"GIT_INDEX_FILE=" + indexPath}} + _, err := git.Output(context.Background(), nil, "add", "-f", "-A", ".") + return err +} + +func GitAddRemote(configPath, name, url string) error { + git := &util.Git{} + _, err := git.Output(context.Background(), nil, "remote", "add", name, url) + return err +} + +func GitPush(configPath, remote, branch string) error { + git := &util.Git{} + _, err := git.Output(context.Background(), nil, "push", remote, branch, "-f") + return err +} + +type GitSaveConfig struct { + AddUntracked bool + AddIgnored bool +} + +type GitSave struct { + log *zap.SugaredLogger + conf *GitSaveConfig +} + +func NewGitSave(logger *zap.Logger, conf *GitSaveConfig) *GitSave { + return &GitSave{log: logger.Sugar(), conf: conf} +} + +// Save adds files to the provided index, creates a tree and a commit pointing to +// that tree, finally it creates a branch poiting to that commit +// Save will use the current worktree index if available to speed the index generation +func (s *GitSave) Save(tmpIndexPath, message, branchName string) error { + gitdir, err := GitDir() + if err != nil { + return err + } + + indexPath := filepath.Join(gitdir, gitIndexFile) + + curBranch, err := currentGitBranch() + if err != nil { + return err + } + + indexExists, err := fileExists(indexPath) + if err != nil { + return err + } + + if indexExists { + // copy current git index to a temporary index + if err := copyFile(indexPath, tmpIndexPath); err != nil { + return err + } + s.log.Infof("created temporary index: %s", tmpIndexPath) + // read the current branch tree information into the index + git := &util.Git{Env: []string{"GIT_INDEX_FILE=" + tmpIndexPath}} + _, err = git.Output(context.Background(), nil, "read-tree", curBranch) + if err != nil { + return err + } + } else { + s.log.Infof("index %s does not exist", indexPath) + } + + if s.conf.AddUntracked { + s.log.Infof("adding untracked files") + if err := gitAddUntrackedFiles(tmpIndexPath); err != nil { + return err + } + } + + if s.conf.AddIgnored { + s.log.Infof("adding ignored files") + if err := gitAddIgnoredFiles(tmpIndexPath); err != nil { + return err + } + } + + s.log.Infof("writing tree file") + treeSHA, err := gitWriteTree(tmpIndexPath) + if err != nil { + return err + } + s.log.Infof("tree: %s", treeSHA) + + s.log.Infof("committing tree") + commitSHA, err := gitCommitTree("git-save", treeSHA) + if err != nil { + return err + } + s.log.Infof("commit: %s", commitSHA) + + s.log.Infof("updating ref") + if err = gitUpdateRef("git-save", filepath.Join(gitRefsPrefix, branchName), commitSHA); err != nil { + return err + } + + return nil +}