1// Package repository contains helper methods for working with the Git repo.
2package repository
3
4import (
5 "bytes"
6 "fmt"
7 "path"
8 "strings"
9 "sync"
10
11 "github.com/MichaelMure/git-bug/util/lamport"
12)
13
14const (
15 clockPath = "git-bug"
16)
17
18var _ ClockedRepo = &GitRepo{}
19var _ TestedRepo = &GitRepo{}
20
21// GitRepo represents an instance of a (local) git repository.
22type GitRepo struct {
23 gitCli
24 path string
25
26 clocksMutex sync.Mutex
27 clocks map[string]lamport.Clock
28
29 keyring Keyring
30}
31
32// NewGitRepo determines if the given working directory is inside of a git repository,
33// and returns the corresponding GitRepo instance if it is.
34func NewGitRepo(path string, clockLoaders []ClockLoader) (*GitRepo, error) {
35 k, err := defaultKeyring()
36 if err != nil {
37 return nil, err
38 }
39
40 repo := &GitRepo{
41 gitCli: gitCli{path: path},
42 path: path,
43 clocks: make(map[string]lamport.Clock),
44 keyring: k,
45 }
46
47 // Check the repo and retrieve the root path
48 stdout, err := repo.runGitCommand("rev-parse", "--git-dir")
49
50 // Now dir is fetched with "git rev-parse --git-dir". May be it can
51 // still return nothing in some cases. Then empty stdout check is
52 // kept.
53 if err != nil || stdout == "" {
54 return nil, ErrNotARepo
55 }
56
57 // Fix the path to be sure we are at the root
58 repo.path = stdout
59 repo.gitCli.path = stdout
60
61 for _, loader := range clockLoaders {
62 allExist := true
63 for _, name := range loader.Clocks {
64 if _, err := repo.getClock(name); err != nil {
65 allExist = false
66 }
67 }
68
69 if !allExist {
70 err = loader.Witnesser(repo)
71 if err != nil {
72 return nil, err
73 }
74 }
75 }
76
77 return repo, nil
78}
79
80// InitGitRepo create a new empty git repo at the given path
81func InitGitRepo(path string) (*GitRepo, error) {
82 repo := &GitRepo{
83 gitCli: gitCli{path: path},
84 path: path + "/.git",
85 clocks: make(map[string]lamport.Clock),
86 }
87
88 _, err := repo.runGitCommand("init", path)
89 if err != nil {
90 return nil, err
91 }
92
93 return repo, nil
94}
95
96// InitBareGitRepo create a new --bare empty git repo at the given path
97func InitBareGitRepo(path string) (*GitRepo, error) {
98 repo := &GitRepo{
99 gitCli: gitCli{path: path},
100 path: path,
101 clocks: make(map[string]lamport.Clock),
102 }
103
104 _, err := repo.runGitCommand("init", "--bare", path)
105 if err != nil {
106 return nil, err
107 }
108
109 return repo, nil
110}
111
112// LocalConfig give access to the repository scoped configuration
113func (repo *GitRepo) LocalConfig() Config {
114 return newGitConfig(repo.gitCli, false)
115}
116
117// GlobalConfig give access to the global scoped configuration
118func (repo *GitRepo) GlobalConfig() Config {
119 return newGitConfig(repo.gitCli, true)
120}
121
122// AnyConfig give access to a merged local/global configuration
123func (repo *GitRepo) AnyConfig() ConfigRead {
124 return mergeConfig(repo.LocalConfig(), repo.GlobalConfig())
125}
126
127// Keyring give access to a user-wide storage for secrets
128func (repo *GitRepo) Keyring() Keyring {
129 return repo.keyring
130}
131
132// GetPath returns the path to the repo.
133func (repo *GitRepo) GetPath() string {
134 return repo.path
135}
136
137// GetUserName returns the name the the user has used to configure git
138func (repo *GitRepo) GetUserName() (string, error) {
139 return repo.runGitCommand("config", "user.name")
140}
141
142// GetUserEmail returns the email address that the user has used to configure git.
143func (repo *GitRepo) GetUserEmail() (string, error) {
144 return repo.runGitCommand("config", "user.email")
145}
146
147// GetCoreEditor returns the name of the editor that the user has used to configure git.
148func (repo *GitRepo) GetCoreEditor() (string, error) {
149 return repo.runGitCommand("var", "GIT_EDITOR")
150}
151
152// GetRemotes returns the configured remotes repositories.
153func (repo *GitRepo) GetRemotes() (map[string]string, error) {
154 stdout, err := repo.runGitCommand("remote", "--verbose")
155 if err != nil {
156 return nil, err
157 }
158
159 lines := strings.Split(stdout, "\n")
160 remotes := make(map[string]string, len(lines))
161
162 for _, line := range lines {
163 if strings.TrimSpace(line) == "" {
164 continue
165 }
166 elements := strings.Fields(line)
167 if len(elements) != 3 {
168 return nil, fmt.Errorf("git remote: unexpected output format: %s", line)
169 }
170
171 remotes[elements[0]] = elements[1]
172 }
173
174 return remotes, nil
175}
176
177// FetchRefs fetch git refs from a remote
178func (repo *GitRepo) FetchRefs(remote, refSpec string) (string, error) {
179 stdout, err := repo.runGitCommand("fetch", remote, refSpec)
180
181 if err != nil {
182 return stdout, fmt.Errorf("failed to fetch from the remote '%s': %v", remote, err)
183 }
184
185 return stdout, err
186}
187
188// PushRefs push git refs to a remote
189func (repo *GitRepo) PushRefs(remote string, refSpec string) (string, error) {
190 stdout, stderr, err := repo.runGitCommandRaw(nil, "push", remote, refSpec)
191
192 if err != nil {
193 return stdout + stderr, fmt.Errorf("failed to push to the remote '%s': %v", remote, stderr)
194 }
195 return stdout + stderr, nil
196}
197
198// StoreData will store arbitrary data and return the corresponding hash
199func (repo *GitRepo) StoreData(data []byte) (Hash, error) {
200 var stdin = bytes.NewReader(data)
201
202 stdout, err := repo.runGitCommandWithStdin(stdin, "hash-object", "--stdin", "-w")
203
204 return Hash(stdout), err
205}
206
207// ReadData will attempt to read arbitrary data from the given hash
208func (repo *GitRepo) ReadData(hash Hash) ([]byte, error) {
209 var stdout bytes.Buffer
210 var stderr bytes.Buffer
211
212 err := repo.runGitCommandWithIO(nil, &stdout, &stderr, "cat-file", "-p", string(hash))
213
214 if err != nil {
215 return []byte{}, err
216 }
217
218 return stdout.Bytes(), nil
219}
220
221// StoreTree will store a mapping key-->Hash as a Git tree
222func (repo *GitRepo) StoreTree(entries []TreeEntry) (Hash, error) {
223 buffer := prepareTreeEntries(entries)
224
225 stdout, err := repo.runGitCommandWithStdin(&buffer, "mktree")
226
227 if err != nil {
228 return "", err
229 }
230
231 return Hash(stdout), nil
232}
233
234// StoreCommit will store a Git commit with the given Git tree
235func (repo *GitRepo) StoreCommit(treeHash Hash) (Hash, error) {
236 stdout, err := repo.runGitCommand("commit-tree", string(treeHash))
237
238 if err != nil {
239 return "", err
240 }
241
242 return Hash(stdout), nil
243}
244
245// StoreCommitWithParent will store a Git commit with the given Git tree
246func (repo *GitRepo) StoreCommitWithParent(treeHash Hash, parent Hash) (Hash, error) {
247 stdout, err := repo.runGitCommand("commit-tree", string(treeHash),
248 "-p", string(parent))
249
250 if err != nil {
251 return "", err
252 }
253
254 return Hash(stdout), nil
255}
256
257// UpdateRef will create or update a Git reference
258func (repo *GitRepo) UpdateRef(ref string, hash Hash) error {
259 _, err := repo.runGitCommand("update-ref", ref, string(hash))
260
261 return err
262}
263
264// RemoveRef will remove a Git reference
265func (repo *GitRepo) RemoveRef(ref string) error {
266 _, err := repo.runGitCommand("update-ref", "-d", ref)
267
268 return err
269}
270
271// ListRefs will return a list of Git ref matching the given refspec
272func (repo *GitRepo) ListRefs(refPrefix string) ([]string, error) {
273 stdout, err := repo.runGitCommand("for-each-ref", "--format=%(refname)", refPrefix)
274
275 if err != nil {
276 return nil, err
277 }
278
279 split := strings.Split(stdout, "\n")
280
281 if len(split) == 1 && split[0] == "" {
282 return []string{}, nil
283 }
284
285 return split, nil
286}
287
288// RefExist will check if a reference exist in Git
289func (repo *GitRepo) RefExist(ref string) (bool, error) {
290 stdout, err := repo.runGitCommand("for-each-ref", ref)
291
292 if err != nil {
293 return false, err
294 }
295
296 return stdout != "", nil
297}
298
299// CopyRef will create a new reference with the same value as another one
300func (repo *GitRepo) CopyRef(source string, dest string) error {
301 _, err := repo.runGitCommand("update-ref", dest, source)
302
303 return err
304}
305
306// ListCommits will return the list of commit hashes of a ref, in chronological order
307func (repo *GitRepo) ListCommits(ref string) ([]Hash, error) {
308 stdout, err := repo.runGitCommand("rev-list", "--first-parent", "--reverse", ref)
309
310 if err != nil {
311 return nil, err
312 }
313
314 split := strings.Split(stdout, "\n")
315
316 casted := make([]Hash, len(split))
317 for i, line := range split {
318 casted[i] = Hash(line)
319 }
320
321 return casted, nil
322
323}
324
325// ReadTree will return the list of entries in a Git tree
326func (repo *GitRepo) ReadTree(hash Hash) ([]TreeEntry, error) {
327 stdout, err := repo.runGitCommand("ls-tree", string(hash))
328
329 if err != nil {
330 return nil, err
331 }
332
333 return readTreeEntries(stdout)
334}
335
336// FindCommonAncestor will return the last common ancestor of two chain of commit
337func (repo *GitRepo) FindCommonAncestor(hash1 Hash, hash2 Hash) (Hash, error) {
338 stdout, err := repo.runGitCommand("merge-base", string(hash1), string(hash2))
339
340 if err != nil {
341 return "", err
342 }
343
344 return Hash(stdout), nil
345}
346
347// GetTreeHash return the git tree hash referenced in a commit
348func (repo *GitRepo) GetTreeHash(commit Hash) (Hash, error) {
349 stdout, err := repo.runGitCommand("rev-parse", string(commit)+"^{tree}")
350
351 if err != nil {
352 return "", err
353 }
354
355 return Hash(stdout), nil
356}
357
358// GetOrCreateClock return a Lamport clock stored in the Repo.
359// If the clock doesn't exist, it's created.
360func (repo *GitRepo) GetOrCreateClock(name string) (lamport.Clock, error) {
361 c, err := repo.getClock(name)
362 if err == nil {
363 return c, nil
364 }
365 if err != ErrClockNotExist {
366 return nil, err
367 }
368
369 repo.clocksMutex.Lock()
370 defer repo.clocksMutex.Unlock()
371
372 p := path.Join(repo.path, clockPath, name+"-clock")
373
374 c, err = lamport.NewPersistedClock(p)
375 if err != nil {
376 return nil, err
377 }
378
379 repo.clocks[name] = c
380 return c, nil
381}
382
383func (repo *GitRepo) getClock(name string) (lamport.Clock, error) {
384 repo.clocksMutex.Lock()
385 defer repo.clocksMutex.Unlock()
386
387 if c, ok := repo.clocks[name]; ok {
388 return c, nil
389 }
390
391 p := path.Join(repo.path, clockPath, name+"-clock")
392
393 c, err := lamport.LoadPersistedClock(p)
394 if err == nil {
395 repo.clocks[name] = c
396 return c, nil
397 }
398 if err == lamport.ErrClockNotExist {
399 return nil, ErrClockNotExist
400 }
401 return nil, err
402}
403
404// AddRemote add a new remote to the repository
405// Not in the interface because it's only used for testing
406func (repo *GitRepo) AddRemote(name string, url string) error {
407 _, err := repo.runGitCommand("remote", "add", name, url)
408
409 return err
410}