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