-
Notifications
You must be signed in to change notification settings - Fork 60
/
bzr.go
342 lines (289 loc) · 9.9 KB
/
bzr.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
package vcs
import (
"fmt"
"net/url"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"time"
)
var bzrDetectURL = regexp.MustCompile("parent branch: (?P<foo>.+)\n")
// NewBzrRepo creates a new instance of BzrRepo. The remote and local directories
// need to be passed in.
func NewBzrRepo(remote, local string) (*BzrRepo, error) {
ins := depInstalled("bzr")
if !ins {
return nil, NewLocalError("bzr is not installed", nil, "")
}
ltype, err := DetectVcsFromFS(local)
// Found a VCS other than Bzr. Need to report an error.
if err == nil && ltype != Bzr {
return nil, ErrWrongVCS
}
r := &BzrRepo{}
r.setRemote(remote)
r.setLocalPath(local)
r.Logger = Logger
// With the other VCS we can check if the endpoint locally is different
// from the one configured internally. But, with Bzr you can't. For example,
// if you do `bzr branch https://launchpad.net/govcstestbzrrepo` and then
// use `bzr info` to get the parent branch you'll find it set to
// http://bazaar.launchpad.net/~mattfarina/govcstestbzrrepo/trunk/. Notice
// the change from https to http and the path chance.
// Here we set the remote to be the local one if none is passed in.
if err == nil && r.CheckLocal() && remote == "" {
c := exec.Command("bzr", "info")
c.Dir = local
c.Env = envForDir(c.Dir)
out, err := c.CombinedOutput()
if err != nil {
return nil, NewLocalError("Unable to retrieve local repo information", err, string(out))
}
m := bzrDetectURL.FindStringSubmatch(string(out))
// If no remote was passed in but one is configured for the locally
// checked out Bzr repo use that one.
if m[1] != "" {
r.setRemote(m[1])
}
}
return r, nil
}
// BzrRepo implements the Repo interface for the Bzr source control.
type BzrRepo struct {
base
}
// Vcs retrieves the underlying VCS being implemented.
func (s BzrRepo) Vcs() Type {
return Bzr
}
// Get is used to perform an initial clone of a repository.
func (s *BzrRepo) Get() error {
basePath := filepath.Dir(filepath.FromSlash(s.LocalPath()))
if _, err := os.Stat(basePath); os.IsNotExist(err) {
err = os.MkdirAll(basePath, 0755)
if err != nil {
return NewLocalError("Unable to create directory", err, "")
}
}
out, err := s.run("bzr", "branch", "--", s.Remote(), s.LocalPath())
if err != nil {
return NewRemoteError("Unable to get repository", err, string(out))
}
return nil
}
// Init initializes a bazaar repository at local location.
func (s *BzrRepo) Init() error {
out, err := s.run("bzr", "init", "--", s.LocalPath())
// There are some windows cases where bazaar cannot create the parent
// directory if it does not already exist, to the location it's trying
// to create the repo. Catch that error and try to handle it.
if err != nil && s.isUnableToCreateDir(err) {
basePath := filepath.Dir(filepath.FromSlash(s.LocalPath()))
if _, err := os.Stat(basePath); os.IsNotExist(err) {
err = os.MkdirAll(basePath, 0755)
if err != nil {
return NewLocalError("Unable to initialize repository", err, "")
}
out, err = s.run("bzr", "init", "--", s.LocalPath())
if err != nil {
return NewLocalError("Unable to initialize repository", err, string(out))
}
return nil
}
} else if err != nil {
return NewLocalError("Unable to initialize repository", err, string(out))
}
return nil
}
// Update performs a Bzr pull and update to an existing checkout.
func (s *BzrRepo) Update() error {
out, err := s.RunFromDir("bzr", "pull")
if err != nil {
return NewRemoteError("Unable to update repository", err, string(out))
}
out, err = s.RunFromDir("bzr", "update")
if err != nil {
return NewRemoteError("Unable to update repository", err, string(out))
}
return nil
}
// UpdateVersion sets the version of a package currently checked out via Bzr.
func (s *BzrRepo) UpdateVersion(version string) error {
out, err := s.RunFromDir("bzr", "update", "-r", version)
if err != nil {
return NewLocalError("Unable to update checked out version", err, string(out))
}
return nil
}
// Version retrieves the current version.
func (s *BzrRepo) Version() (string, error) {
out, err := s.RunFromDir("bzr", "revno", "--tree")
if err != nil {
return "", NewLocalError("Unable to retrieve checked out version", err, string(out))
}
return strings.TrimSpace(string(out)), nil
}
// Current returns the current version-ish. This means:
// * -1 if on the tip of the branch (this is the Bzr value for HEAD)
// * A tag if on a tag
// * Otherwise a revision
func (s *BzrRepo) Current() (string, error) {
tip, err := s.CommitInfo("-1")
if err != nil {
return "", err
}
curr, err := s.Version()
if err != nil {
return "", err
}
if tip.Commit == curr {
return "-1", nil
}
ts, err := s.TagsFromCommit(curr)
if err != nil {
return "", err
}
if len(ts) > 0 {
return ts[0], nil
}
return curr, nil
}
// Date retrieves the date on the latest commit.
func (s *BzrRepo) Date() (time.Time, error) {
out, err := s.RunFromDir("bzr", "version-info", "--custom", "--template={date}")
if err != nil {
return time.Time{}, NewLocalError("Unable to retrieve revision date", err, string(out))
}
t, err := time.Parse(longForm, string(out))
if err != nil {
return time.Time{}, NewLocalError("Unable to retrieve revision date", err, string(out))
}
return t, nil
}
// CheckLocal verifies the local location is a Bzr repo.
func (s *BzrRepo) CheckLocal() bool {
if _, err := os.Stat(s.LocalPath() + "/.bzr"); err == nil {
return true
}
return false
}
// Branches returns a list of available branches on the repository.
// In Bazaar (Bzr) clones and branches are the same. A different branch will
// have a different URL location which we cannot detect from the repo. This
// is a little different from other VCS.
func (s *BzrRepo) Branches() ([]string, error) {
var branches []string
return branches, nil
}
// Tags returns a list of available tags on the repository.
func (s *BzrRepo) Tags() ([]string, error) {
out, err := s.RunFromDir("bzr", "tags")
if err != nil {
return []string{}, NewLocalError("Unable to retrieve tags", err, string(out))
}
tags := s.referenceList(string(out), `(?m-s)^(\S+)`)
return tags, nil
}
// IsReference returns if a string is a reference. A reference can be a
// commit id or tag.
func (s *BzrRepo) IsReference(r string) bool {
_, err := s.RunFromDir("bzr", "revno", "-r", r)
return err == nil
}
// IsDirty returns if the checkout has been modified from the checked
// out reference.
func (s *BzrRepo) IsDirty() bool {
out, err := s.RunFromDir("bzr", "diff")
return err != nil || len(out) != 0
}
// CommitInfo retrieves metadata about a commit.
func (s *BzrRepo) CommitInfo(id string) (*CommitInfo, error) {
r := "-r" + id
out, err := s.RunFromDir("bzr", "log", r, "--log-format=long")
if err != nil {
return nil, ErrRevisionUnavailable
}
ci := &CommitInfo{}
lines := strings.Split(string(out), "\n")
const format = "Mon 2006-01-02 15:04:05 -0700"
var track int
var trackOn bool
// Note, bzr does not appear to use i18m.
for i, l := range lines {
if strings.HasPrefix(l, "revno:") {
ci.Commit = strings.TrimSpace(strings.TrimPrefix(l, "revno:"))
} else if strings.HasPrefix(l, "committer:") {
ci.Author = strings.TrimSpace(strings.TrimPrefix(l, "committer:"))
} else if strings.HasPrefix(l, "timestamp:") {
ts := strings.TrimSpace(strings.TrimPrefix(l, "timestamp:"))
ci.Date, err = time.Parse(format, ts)
if err != nil {
return nil, NewLocalError("Unable to retrieve commit information", err, string(out))
}
} else if strings.TrimSpace(l) == "message:" {
track = i
trackOn = true
} else if trackOn && i > track {
ci.Message = ci.Message + l
}
}
ci.Message = strings.TrimSpace(ci.Message)
// Didn't find the revision
if ci.Author == "" {
return nil, ErrRevisionUnavailable
}
return ci, nil
}
// TagsFromCommit retrieves tags from a commit id.
func (s *BzrRepo) TagsFromCommit(id string) ([]string, error) {
out, err := s.RunFromDir("bzr", "tags", "-r", id)
if err != nil {
return []string{}, NewLocalError("Unable to retrieve tags", err, string(out))
}
tags := s.referenceList(string(out), `(?m-s)^(\S+)`)
return tags, nil
}
// Ping returns if remote location is accessible.
func (s *BzrRepo) Ping() bool {
// Running bzr info is slow. Many of the projects are on launchpad which
// has a public 1.0 API we can use.
u, err := url.Parse(s.Remote())
if err == nil {
if u.Host == "launchpad.net" {
try := strings.TrimPrefix(u.Path, "/")
// get returns the body and an err. If the status code is not a 200
// an error is returned. Launchpad returns a 404 for a codebase that
// does not exist. Otherwise it returns a JSON object describing it.
_, er := get("https://api.launchpad.net/1.0/" + try)
return er == nil
}
}
// This is the same command that Go itself uses but it's not fast (or fast
// enough by my standards). A faster method would be useful.
_, err = s.run("bzr", "info", "--", s.Remote())
return err == nil
}
// ExportDir exports the current revision to the passed in directory.
func (s *BzrRepo) ExportDir(dir string) error {
out, err := s.RunFromDir("bzr", "export", "--", dir)
s.log(out)
if err != nil {
return NewLocalError("Unable to export source", err, string(out))
}
return nil
}
// Multi-lingual manner check for the VCS error that it couldn't create directory.
// https://bazaar.launchpad.net/~bzr-pqm/bzr/bzr.dev/files/head:/po/
func (s *BzrRepo) isUnableToCreateDir(err error) bool {
msg := err.Error()
if strings.HasPrefix(msg, fmt.Sprintf("Parent directory of %s does not exist.", s.LocalPath())) ||
strings.HasPrefix(msg, fmt.Sprintf("Nadřazený adresář %s neexistuje.", s.LocalPath())) ||
strings.HasPrefix(msg, fmt.Sprintf("El directorio padre de %s no existe.", s.LocalPath())) ||
strings.HasPrefix(msg, fmt.Sprintf("%s の親ディレクトリがありません。", s.LocalPath())) ||
strings.HasPrefix(msg, fmt.Sprintf("Родительская директория для %s не существует.", s.LocalPath())) {
return true
}
return false
}