1package bug
2
3import (
4 "fmt"
5 "strings"
6
7 "github.com/MichaelMure/git-bug/entity"
8 "github.com/MichaelMure/git-bug/repository"
9 "github.com/pkg/errors"
10)
11
12// Fetch retrieve updates from a remote
13// This does not change the local bugs state
14func Fetch(repo repository.Repo, remote string) (string, error) {
15 // "refs/bugs/*:refs/remotes/<remote>>/bugs/*"
16 remoteRefSpec := fmt.Sprintf(bugsRemoteRefPattern, remote)
17 fetchRefSpec := fmt.Sprintf("%s*:%s*", bugsRefPattern, remoteRefSpec)
18
19 return repo.FetchRefs(remote, fetchRefSpec)
20}
21
22// Push update a remote with the local changes
23func Push(repo repository.Repo, remote string) (string, error) {
24 // "refs/bugs/*:refs/bugs/*"
25 refspec := fmt.Sprintf("%s*:%s*", bugsRefPattern, bugsRefPattern)
26
27 return repo.PushRefs(remote, refspec)
28}
29
30// Pull will do a Fetch + MergeAll
31// This function will return an error if a merge fail
32func Pull(repo repository.ClockedRepo, remote string) error {
33 _, err := Fetch(repo, remote)
34 if err != nil {
35 return err
36 }
37
38 for merge := range MergeAll(repo, remote) {
39 if merge.Err != nil {
40 return merge.Err
41 }
42 if merge.Status == entity.MergeStatusInvalid {
43 return errors.Errorf("merge failure: %s", merge.Reason)
44 }
45 }
46
47 return nil
48}
49
50// MergeAll will merge all the available remote bug:
51//
52// - If the remote has new commit, the local bug is updated to match the same history
53// (fast-forward update)
54// - if the local bug has new commits but the remote don't, nothing is changed
55// - if both local and remote bug have new commits (that is, we have a concurrent edition),
56// new local commits are rewritten at the head of the remote history (that is, a rebase)
57func MergeAll(repo repository.ClockedRepo, remote string) <-chan entity.MergeResult {
58 out := make(chan entity.MergeResult)
59
60 go func() {
61 defer close(out)
62
63 remoteRefSpec := fmt.Sprintf(bugsRemoteRefPattern, remote)
64 remoteRefs, err := repo.ListRefs(remoteRefSpec)
65
66 if err != nil {
67 out <- entity.MergeResult{Err: err}
68 return
69 }
70
71 for _, remoteRef := range remoteRefs {
72 refSplit := strings.Split(remoteRef, "/")
73 id := entity.Id(refSplit[len(refSplit)-1])
74
75 if err := id.Validate(); err != nil {
76 out <- entity.NewMergeInvalidStatus(id, errors.Wrap(err, "invalid ref").Error())
77 continue
78 }
79
80 remoteBug, err := readBug(repo, remoteRef)
81
82 if err != nil {
83 out <- entity.NewMergeInvalidStatus(id, errors.Wrap(err, "remote bug is not readable").Error())
84 continue
85 }
86
87 // Check for error in remote data
88 if err := remoteBug.Validate(); err != nil {
89 out <- entity.NewMergeInvalidStatus(id, errors.Wrap(err, "remote bug is invalid").Error())
90 continue
91 }
92
93 localRef := bugsRefPattern + remoteBug.Id().String()
94 localExist, err := repo.RefExist(localRef)
95
96 if err != nil {
97 out <- entity.NewMergeError(err, id)
98 continue
99 }
100
101 // the bug is not local yet, simply create the reference
102 if !localExist {
103 err := repo.CopyRef(remoteRef, localRef)
104
105 if err != nil {
106 out <- entity.NewMergeError(err, id)
107 return
108 }
109
110 out <- entity.NewMergeStatus(entity.MergeStatusNew, id, remoteBug)
111 continue
112 }
113
114 localBug, err := readBug(repo, localRef)
115
116 if err != nil {
117 out <- entity.NewMergeError(errors.Wrap(err, "local bug is not readable"), id)
118 return
119 }
120
121 updated, err := localBug.Merge(repo, remoteBug)
122
123 if err != nil {
124 out <- entity.NewMergeInvalidStatus(id, errors.Wrap(err, "merge failed").Error())
125 return
126 }
127
128 if updated {
129 out <- entity.NewMergeStatus(entity.MergeStatusUpdated, id, localBug)
130 } else {
131 out <- entity.NewMergeStatus(entity.MergeStatusNothing, id, localBug)
132 }
133 }
134 }()
135
136 return out
137}