1// Package repository contains helper methods for working with the Git repo.
2package repository
3
4import (
5 "bytes"
6 "crypto/sha1"
7 "fmt"
8 "github.com/MichaelMure/git-bug/util"
9 "io"
10 "os/exec"
11 "strings"
12)
13
14// This is used to have a different staging area than the regular git index
15// when creating data in git
16const gitEnvConfig = "GIT_INDEX_FILE=BUG_STAGING_INDEX"
17
18// GitRepo represents an instance of a (local) git repository.
19type GitRepo struct {
20 Path string
21}
22
23// Run the given git command with the given I/O reader/writers, returning an error if it fails.
24func (repo *GitRepo) runGitCommandWithIO(stdin io.Reader, stdout, stderr io.Writer, args ...string) error {
25 cmd := exec.Command("git", args...)
26 cmd.Dir = repo.Path
27 cmd.Stdin = stdin
28 cmd.Stdout = stdout
29 cmd.Stderr = stderr
30
31 //cmd.Env = append(cmd.Env, gitEnvConfig)
32
33 return cmd.Run()
34}
35
36// Run the given git command and return its stdout, or an error if the command fails.
37func (repo *GitRepo) runGitCommandRaw(stdin io.Reader, args ...string) (string, string, error) {
38 var stdout bytes.Buffer
39 var stderr bytes.Buffer
40 err := repo.runGitCommandWithIO(stdin, &stdout, &stderr, args...)
41 return strings.TrimSpace(stdout.String()), strings.TrimSpace(stderr.String()), err
42}
43
44// Run the given git command and return its stdout, or an error if the command fails.
45func (repo *GitRepo) runGitCommandWithStdin(stdin io.Reader, args ...string) (string, error) {
46 stdout, stderr, err := repo.runGitCommandRaw(stdin, args...)
47 if err != nil {
48 if stderr == "" {
49 stderr = "Error running git command: " + strings.Join(args, " ")
50 }
51 err = fmt.Errorf(stderr)
52 }
53 return stdout, err
54}
55
56// Run the given git command and return its stdout, or an error if the command fails.
57func (repo *GitRepo) runGitCommand(args ...string) (string, error) {
58 return repo.runGitCommandWithStdin(nil, args...)
59}
60
61// NewGitRepo determines if the given working directory is inside of a git repository,
62// and returns the corresponding GitRepo instance if it is.
63func NewGitRepo(path string) (*GitRepo, error) {
64 repo := &GitRepo{Path: path}
65 _, err := repo.runGitCommand("rev-parse")
66 if err == nil {
67 return repo, nil
68 }
69 if _, ok := err.(*exec.ExitError); ok {
70 return nil, err
71 }
72 return nil, err
73}
74
75// GetPath returns the path to the repo.
76func (repo *GitRepo) GetPath() string {
77 return repo.Path
78}
79
80// GetRepoStateHash returns a hash which embodies the entire current state of a repository.
81func (repo *GitRepo) GetRepoStateHash() (string, error) {
82 stateSummary, err := repo.runGitCommand("show-ref")
83 return fmt.Sprintf("%x", sha1.Sum([]byte(stateSummary))), err
84}
85
86// GetUserName returns the name the the user has used to configure git
87func (repo *GitRepo) GetUserName() (string, error) {
88 return repo.runGitCommand("config", "user.name")
89}
90
91// GetUserEmail returns the email address that the user has used to configure git.
92func (repo *GitRepo) GetUserEmail() (string, error) {
93 return repo.runGitCommand("config", "user.email")
94}
95
96// GetCoreEditor returns the name of the editor that the user has used to configure git.
97func (repo *GitRepo) GetCoreEditor() (string, error) {
98 return repo.runGitCommand("var", "GIT_EDITOR")
99}
100
101// PullRefs pull git refs from a remote
102func (repo *GitRepo) PullRefs(remote string, refPattern string) error {
103 fetchRefSpec := fmt.Sprintf("+%s:%s", refPattern, refPattern)
104 _, err := repo.runGitCommand("fetch", remote, fetchRefSpec)
105
106 // TODO: merge new data
107
108 return err
109}
110
111// PushRefs push git refs to a remote
112func (repo *GitRepo) PushRefs(remote string, refPattern string) error {
113 // The push is liable to fail if the user forgot to do a pull first, so
114 // we treat errors as user errors rather than fatal errors.
115 _, err := repo.runGitCommand("push", remote, refPattern)
116 if err != nil {
117 return fmt.Errorf("failed to push to the remote '%s': %v", remote, err)
118 }
119 return nil
120}
121
122// StoreData will store arbitrary data and return the corresponding hash
123func (repo *GitRepo) StoreData(data []byte) (util.Hash, error) {
124 var stdin = bytes.NewReader(data)
125
126 stdout, err := repo.runGitCommandWithStdin(stdin, "hash-object", "--stdin", "-w")
127
128 return util.Hash(stdout), err
129}
130
131// StoreTree will store a mapping key-->Hash as a Git tree
132func (repo *GitRepo) StoreTree(mapping map[string]util.Hash) (util.Hash, error) {
133 var buffer bytes.Buffer
134
135 for key, hash := range mapping {
136 buffer.WriteString(fmt.Sprintf("100644 blob %s\t%s\n", hash, key))
137 }
138
139 stdout, err := repo.runGitCommandWithStdin(&buffer, "mktree")
140 if err != nil {
141 return "", err
142 }
143
144 return util.Hash(stdout), nil
145}
146
147// StoreCommit will store a Git commit with the given Git tree
148func (repo *GitRepo) StoreCommit(treeHash util.Hash) (util.Hash, error) {
149 stdout, err := repo.runGitCommand("commit-tree", string(treeHash))
150
151 if err != nil {
152 return "", err
153 }
154
155 return util.Hash(stdout), nil
156}
157
158// StoreCommitWithParent will store a Git commit with the given Git tree
159func (repo *GitRepo) StoreCommitWithParent(treeHash util.Hash, parent util.Hash) (util.Hash, error) {
160 stdout, err := repo.runGitCommand("commit-tree", string(treeHash),
161 "-p", string(parent))
162
163 if err != nil {
164 return "", err
165 }
166
167 return util.Hash(stdout), nil
168}
169
170// UpdateRef will create or update a Git reference
171func (repo *GitRepo) UpdateRef(ref string, hash util.Hash) error {
172 _, err := repo.runGitCommand("update-ref", ref, string(hash))
173
174 return err
175}