OLD | NEW |
| (Empty) |
1 // Copyright 2016 The LUCI Authors. All rights reserved. | |
2 // Use of this source code is governed under the Apache License, Version 2.0 | |
3 // that can be found in the LICENSE file. | |
4 | |
5 package buildbot | |
6 | |
7 import ( | |
8 "encoding/json" | |
9 "errors" | |
10 "fmt" | |
11 "io/ioutil" | |
12 "math" | |
13 "path/filepath" | |
14 "regexp" | |
15 "sort" | |
16 "strconv" | |
17 "strings" | |
18 "time" | |
19 | |
20 "golang.org/x/net/context" | |
21 | |
22 "github.com/luci/gae/service/datastore" | |
23 "github.com/luci/luci-go/common/data/stringset" | |
24 "github.com/luci/luci-go/common/logging" | |
25 "github.com/luci/luci-go/milo/api/resp" | |
26 "github.com/luci/luci-go/milo/appengine/common/model" | |
27 ) | |
28 | |
29 var errBuildNotFound = errors.New("Build not found") | |
30 | |
31 // getBuild fetches a buildbot build from the datastore and checks ACLs. | |
32 // The return code matches the master responses. | |
33 func getBuild(c context.Context, master, builder string, buildNum int) (*buildbo
tBuild, error) { | |
34 result := &buildbotBuild{ | |
35 Master: master, | |
36 Buildername: builder, | |
37 Number: buildNum, | |
38 } | |
39 | |
40 err := datastore.Get(c, result) | |
41 err = checkAccess(c, err, result.Internal) | |
42 if err == errMasterNotFound { | |
43 err = errBuildNotFound | |
44 } | |
45 | |
46 return result, err | |
47 } | |
48 | |
49 // result2Status translates a buildbot result integer into a model.Status. | |
50 func result2Status(s *int) (status model.Status) { | |
51 if s == nil { | |
52 return model.Running | |
53 } | |
54 switch *s { | |
55 case 0: | |
56 status = model.Success | |
57 case 1: | |
58 status = model.Warning | |
59 case 2: | |
60 status = model.Failure | |
61 case 3: | |
62 status = model.NotRun // Skipped | |
63 case 4: | |
64 status = model.Exception | |
65 case 5: | |
66 status = model.WaitingDependency // Retry | |
67 default: | |
68 panic(fmt.Errorf("Unknown status %d", s)) | |
69 } | |
70 return | |
71 } | |
72 | |
73 // buildbotTimeToTime converts a buildbot time representation (pointer to float | |
74 // of seconds since epoch) to a native time.Time object. | |
75 func buildbotTimeToTime(t *float64) (result time.Time) { | |
76 if t != nil { | |
77 result = time.Unix(int64(*t), int64(*t*1e9)%1e9).UTC() | |
78 } | |
79 return | |
80 } | |
81 | |
82 // parseTimes translates a buildbot time tuple (start, end) into a triplet | |
83 // of (Started time, Ending time, duration). | |
84 // If times[1] is nil and buildFinished is not, ended will be set to buildFinish
ed | |
85 // time. | |
86 func parseTimes(buildFinished *float64, times []*float64) (started, ended time.T
ime, duration time.Duration) { | |
87 if len(times) != 2 { | |
88 panic(fmt.Errorf("Expected 2 floats for times, got %v", times)) | |
89 } | |
90 if times[0] == nil { | |
91 // Some steps don't have timing info. In that case, just return
nils. | |
92 return | |
93 } | |
94 started = buildbotTimeToTime(times[0]) | |
95 switch { | |
96 case times[1] != nil: | |
97 ended = buildbotTimeToTime(times[1]) | |
98 duration = ended.Sub(started) | |
99 case buildFinished != nil: | |
100 ended = buildbotTimeToTime(buildFinished) | |
101 duration = ended.Sub(started) | |
102 default: | |
103 duration = time.Since(started) | |
104 } | |
105 return | |
106 } | |
107 | |
108 // getBanner parses the OS information from the build and maybe returns a banner
. | |
109 func getBanner(c context.Context, b *buildbotBuild) *resp.LogoBanner { | |
110 logging.Infof(c, "OS: %s/%s", b.OSFamily, b.OSVersion) | |
111 osLogo := func() *resp.Logo { | |
112 result := &resp.Logo{} | |
113 switch b.OSFamily { | |
114 case "windows": | |
115 result.LogoBase = resp.Windows | |
116 case "Darwin": | |
117 result.LogoBase = resp.OSX | |
118 case "Debian": | |
119 result.LogoBase = resp.Ubuntu | |
120 default: | |
121 return nil | |
122 } | |
123 result.Subtitle = b.OSVersion | |
124 return result | |
125 }() | |
126 if osLogo != nil { | |
127 return &resp.LogoBanner{ | |
128 OS: []resp.Logo{*osLogo}, | |
129 } | |
130 } | |
131 logging.Warningf(c, "No OS info found.") | |
132 return nil | |
133 } | |
134 | |
135 // summary extracts the top level summary from a buildbot build as a | |
136 // BuildComponent | |
137 func summary(c context.Context, b *buildbotBuild) resp.BuildComponent { | |
138 // TODO(hinoka): use b.toStatus() | |
139 // Status | |
140 var status model.Status | |
141 if b.Currentstep != nil { | |
142 status = model.Running | |
143 } else { | |
144 status = result2Status(b.Results) | |
145 } | |
146 | |
147 // Timing info | |
148 started, ended, duration := parseTimes(nil, b.Times) | |
149 | |
150 // Link to bot and original build. | |
151 host := "build.chromium.org/p" | |
152 if b.Internal { | |
153 host = "uberchromegw.corp.google.com/i" | |
154 } | |
155 bot := resp.NewLink( | |
156 b.Slave, | |
157 fmt.Sprintf("https://%s/%s/buildslaves/%s", host, b.Master, b.Sl
ave), | |
158 ) | |
159 source := resp.NewLink( | |
160 fmt.Sprintf("%s/%s/%d", b.Master, b.Buildername, b.Number), | |
161 fmt.Sprintf("https://%s/%s/builders/%s/builds/%d", | |
162 host, b.Master, b.Buildername, b.Number), | |
163 ) | |
164 | |
165 // The link to the builder page. | |
166 parent := resp.NewLink(b.Buildername, ".") | |
167 | |
168 // Do a best effort lookup for the bot information to fill in OS/Platfor
m info. | |
169 banner := getBanner(c, b) | |
170 | |
171 sum := resp.BuildComponent{ | |
172 ParentLabel: parent, | |
173 Label: fmt.Sprintf("#%d", b.Number), | |
174 Banner: banner, | |
175 Status: status, | |
176 Started: started, | |
177 Finished: ended, | |
178 Bot: bot, | |
179 Source: source, | |
180 Duration: duration, | |
181 Type: resp.Summary, // This is more or less ignored. | |
182 LevelsDeep: 1, | |
183 Text: []string{}, // Status messages. Eg "This build fai
led on..xyz" | |
184 } | |
185 | |
186 return sum | |
187 } | |
188 | |
189 var rLineBreak = regexp.MustCompile("<br */?>") | |
190 | |
191 // components takes a full buildbot build struct and extract step info from all | |
192 // of the steps and returns it as a list of milo Build Components. | |
193 func components(b *buildbotBuild) (result []*resp.BuildComponent) { | |
194 endingTime := b.Times[1] | |
195 for _, step := range b.Steps { | |
196 if step.Hidden == true { | |
197 continue | |
198 } | |
199 bc := &resp.BuildComponent{ | |
200 Label: step.Name, | |
201 } | |
202 // Step text sometimes contains <br>, which we want to parse int
o new lines. | |
203 for _, t := range step.Text { | |
204 for _, line := range rLineBreak.Split(t, -1) { | |
205 bc.Text = append(bc.Text, line) | |
206 } | |
207 } | |
208 | |
209 // Figure out the status. | |
210 if !step.IsStarted { | |
211 bc.Status = model.NotRun | |
212 } else if !step.IsFinished { | |
213 bc.Status = model.Running | |
214 } else { | |
215 if len(step.Results) > 0 { | |
216 status := int(step.Results[0].(float64)) | |
217 bc.Status = result2Status(&status) | |
218 } else { | |
219 bc.Status = model.Success | |
220 } | |
221 } | |
222 | |
223 // Raise the interesting-ness if the step is not "Success". | |
224 if bc.Status != model.Success { | |
225 bc.Verbosity = resp.Interesting | |
226 } | |
227 | |
228 remainingAliases := stringset.New(len(step.Aliases)) | |
229 for linkAnchor := range step.Aliases { | |
230 remainingAliases.Add(linkAnchor) | |
231 } | |
232 | |
233 getLinksWithAliases := func(logLink *resp.Link, isLog bool) resp
.LinkSet { | |
234 // Generate alias links. | |
235 var aliases resp.LinkSet | |
236 if remainingAliases.Del(logLink.Label) { | |
237 stepAliases := step.Aliases[logLink.Label] | |
238 aliases = make(resp.LinkSet, len(stepAliases)) | |
239 for i, alias := range stepAliases { | |
240 aliases[i] = alias.toLink() | |
241 } | |
242 } | |
243 | |
244 // Step log link takes primary, with aliases as secondar
y. | |
245 links := make(resp.LinkSet, 1, 1+len(aliases)) | |
246 links[0] = logLink | |
247 | |
248 for _, a := range aliases { | |
249 a.Alias = true | |
250 } | |
251 return append(links, aliases...) | |
252 } | |
253 | |
254 for _, l := range step.Logs { | |
255 logLink := resp.NewLink(l[0], l[1]) | |
256 | |
257 links := getLinksWithAliases(logLink, true) | |
258 if logLink.Label == "stdio" { | |
259 bc.MainLink = links | |
260 } else { | |
261 bc.SubLink = append(bc.SubLink, links) | |
262 } | |
263 } | |
264 | |
265 // Step links are stored as maps of name: url | |
266 // Because Go doesn't believe in nice things, we now create anot
her array | |
267 // just so that we can iterate through this map in order. | |
268 names := make([]string, 0, len(step.Urls)) | |
269 for name := range step.Urls { | |
270 names = append(names, name) | |
271 } | |
272 sort.Strings(names) | |
273 for _, name := range names { | |
274 logLink := resp.NewLink(name, step.Urls[name]) | |
275 | |
276 bc.SubLink = append(bc.SubLink, getLinksWithAliases(logL
ink, false)) | |
277 } | |
278 | |
279 // Add any unused aliases directly. | |
280 if remainingAliases.Len() > 0 { | |
281 unusedAliases := remainingAliases.ToSlice() | |
282 sort.Strings(unusedAliases) | |
283 | |
284 for _, label := range unusedAliases { | |
285 var baseLink resp.LinkSet | |
286 for _, alias := range step.Aliases[label] { | |
287 aliasLink := alias.toLink() | |
288 if len(baseLink) == 0 { | |
289 aliasLink.Label = label | |
290 } else { | |
291 aliasLink.Alias = true | |
292 } | |
293 baseLink = append(baseLink, aliasLink) | |
294 } | |
295 | |
296 if len(baseLink) > 0 { | |
297 bc.SubLink = append(bc.SubLink, baseLink
) | |
298 } | |
299 } | |
300 } | |
301 | |
302 // Figure out the times. | |
303 bc.Started, bc.Finished, bc.Duration = parseTimes(endingTime, st
ep.Times) | |
304 | |
305 result = append(result, bc) | |
306 } | |
307 return | |
308 } | |
309 | |
310 // parseProp returns a string representation of v. | |
311 func parseProp(v interface{}) string { | |
312 // if v is a whole number, force it into an int. json.Marshal() would t
urn | |
313 // it into what looks like a float instead. We want this to remain and | |
314 // int instead of a number. | |
315 if vf, ok := v.(float64); ok { | |
316 if math.Floor(vf) == vf { | |
317 return fmt.Sprintf("%d", int64(vf)) | |
318 } | |
319 } | |
320 // return the json representation of the value. | |
321 b, err := json.Marshal(v) | |
322 if err == nil { | |
323 return string(b) | |
324 } | |
325 return fmt.Sprintf("%v", v) | |
326 } | |
327 | |
328 // Prop is a struct used to store a value and group so that we can make a map | |
329 // of key:Prop to pass into parseProp() for the purpose of cross referencing | |
330 // one prop while working on another. | |
331 type Prop struct { | |
332 Value interface{} | |
333 Group string | |
334 } | |
335 | |
336 // properties extracts all properties from buildbot builds and groups them into | |
337 // property groups. | |
338 func properties(b *buildbotBuild) (result []*resp.PropertyGroup) { | |
339 groups := map[string]*resp.PropertyGroup{} | |
340 allProps := map[string]Prop{} | |
341 for _, prop := range b.Properties { | |
342 allProps[prop.Name] = Prop{ | |
343 Value: prop.Value, | |
344 Group: prop.Source, | |
345 } | |
346 } | |
347 for key, prop := range allProps { | |
348 value := prop.Value | |
349 groupName := prop.Group | |
350 if _, ok := groups[groupName]; !ok { | |
351 groups[groupName] = &resp.PropertyGroup{GroupName: group
Name} | |
352 } | |
353 vs := parseProp(value) | |
354 groups[groupName].Property = append(groups[groupName].Property,
&resp.Property{ | |
355 Key: key, | |
356 Value: vs, | |
357 }) | |
358 } | |
359 // Insert the groups into a list in alphabetical order. | |
360 // You have to make a separate sorting data structure because Go doesn't
like | |
361 // sorting things for you. | |
362 groupNames := []string{} | |
363 for n := range groups { | |
364 groupNames = append(groupNames, n) | |
365 } | |
366 sort.Strings(groupNames) | |
367 for _, k := range groupNames { | |
368 group := groups[k] | |
369 // Also take this oppertunity to sort the properties within the
groups. | |
370 sort.Sort(group) | |
371 result = append(result, group) | |
372 } | |
373 return | |
374 } | |
375 | |
376 // blame extracts the commit and blame information from a buildbot build and | |
377 // returns it as a list of Commits. | |
378 func blame(b *buildbotBuild) (result []*resp.Commit) { | |
379 for _, c := range b.Sourcestamp.Changes { | |
380 files := c.GetFiles() | |
381 result = append(result, &resp.Commit{ | |
382 AuthorEmail: c.Who, | |
383 Repo: c.Repository, | |
384 CommitTime: time.Unix(int64(c.When), 0).UTC(), | |
385 Revision: resp.NewLink(c.Revision, c.Revlink), | |
386 Description: c.Comments, | |
387 Title: strings.Split(c.Comments, "\n")[0], | |
388 File: files, | |
389 }) | |
390 } | |
391 return | |
392 } | |
393 | |
394 // sourcestamp extracts the source stamp from various parts of a buildbot build, | |
395 // including the properties. | |
396 func sourcestamp(c context.Context, b *buildbotBuild) *resp.SourceStamp { | |
397 ss := &resp.SourceStamp{} | |
398 rietveld := "" | |
399 gerrit := "" | |
400 got_revision := "" | |
401 repository := "" | |
402 issue := int64(-1) | |
403 for _, prop := range b.Properties { | |
404 switch prop.Name { | |
405 case "rietveld": | |
406 if v, ok := prop.Value.(string); ok { | |
407 rietveld = v | |
408 } else { | |
409 logging.Warningf(c, "Field rietveld is not a str
ing: %#v", prop.Value) | |
410 } | |
411 case "issue": | |
412 // Sometime this is a number (float), sometime it is a s
tring. | |
413 if v, ok := prop.Value.(float64); ok { | |
414 issue = int64(v) | |
415 } else if v, ok := prop.Value.(string); ok { | |
416 if vi, err := strconv.ParseInt(v, 10, 64); err =
= nil { | |
417 issue = int64(vi) | |
418 } else { | |
419 logging.Warningf(c, "Could not decode fi
eld issue: %q - %s", prop.Value, err) | |
420 } | |
421 } else { | |
422 logging.Warningf(c, "Field issue is not a string
or float: %#v", prop.Value) | |
423 } | |
424 | |
425 case "got_revision": | |
426 if v, ok := prop.Value.(string); ok { | |
427 got_revision = v | |
428 } else { | |
429 logging.Warningf(c, "Field got_revision is not a
string: %#v", prop.Value) | |
430 } | |
431 | |
432 case "patch_issue": | |
433 if v, ok := prop.Value.(float64); ok { | |
434 issue = int64(v) | |
435 } else { | |
436 logging.Warningf(c, "Field patch_issue is not a
float: %#v", prop.Value) | |
437 } | |
438 | |
439 case "patch_gerrit_url": | |
440 if v, ok := prop.Value.(string); ok { | |
441 gerrit = v | |
442 } else { | |
443 logging.Warningf(c, "Field gerrit is not a strin
g: %#v", prop.Value) | |
444 } | |
445 | |
446 case "repository": | |
447 if v, ok := prop.Value.(string); ok { | |
448 repository = v | |
449 } | |
450 } | |
451 } | |
452 if issue != -1 { | |
453 switch { | |
454 case rietveld != "": | |
455 rietveld = strings.TrimRight(rietveld, "/") | |
456 ss.Changelist = resp.NewLink( | |
457 fmt.Sprintf("Rietveld CL %d", issue), | |
458 fmt.Sprintf("%s/%d", rietveld, issue)) | |
459 case gerrit != "": | |
460 gerrit = strings.TrimRight(gerrit, "/") | |
461 ss.Changelist = resp.NewLink( | |
462 fmt.Sprintf("Gerrit CL %d", issue), | |
463 fmt.Sprintf("%s/c/%d", gerrit, issue)) | |
464 } | |
465 } | |
466 | |
467 if got_revision != "" { | |
468 ss.Revision = resp.NewLink(got_revision, "") | |
469 if repository != "" { | |
470 ss.Revision.URL = repository + "/+/" + got_revision | |
471 } | |
472 } | |
473 return ss | |
474 } | |
475 | |
476 func renderBuild(c context.Context, b *buildbotBuild) *resp.MiloBuild { | |
477 // Modify the build for rendering. | |
478 updatePostProcessBuild(b) | |
479 | |
480 // TODO(hinoka): Do all fields concurrently. | |
481 return &resp.MiloBuild{ | |
482 SourceStamp: sourcestamp(c, b), | |
483 Summary: summary(c, b), | |
484 Components: components(b), | |
485 PropertyGroup: properties(b), | |
486 Blame: blame(b), | |
487 } | |
488 } | |
489 | |
490 // DebugBuild fetches a debugging build for testing. | |
491 func DebugBuild(c context.Context, relBuildbotDir string, builder string, buildN
um int) (*resp.MiloBuild, error) { | |
492 fname := fmt.Sprintf("%s.%d.json", builder, buildNum) | |
493 // ../buildbot below assumes that | |
494 // - this code is not executed by tests outside of this dir | |
495 // - this dir is a sibling of frontend dir | |
496 path := filepath.Join(relBuildbotDir, "testdata", fname) | |
497 raw, err := ioutil.ReadFile(path) | |
498 if err != nil { | |
499 return nil, err | |
500 } | |
501 b := &buildbotBuild{} | |
502 if err := json.Unmarshal(raw, b); err != nil { | |
503 return nil, err | |
504 } | |
505 return renderBuild(c, b), nil | |
506 } | |
507 | |
508 // Build fetches a buildbot build and translates it into a miloBuild. | |
509 func Build(c context.Context, master, builder string, buildNum int) (*resp.MiloB
uild, error) { | |
510 b, err := getBuild(c, master, builder, buildNum) | |
511 if err != nil { | |
512 return nil, err | |
513 } | |
514 return renderBuild(c, b), nil | |
515 } | |
516 | |
517 // updatePostProcessBuild transforms a build from its raw JSON format into the | |
518 // format that should be presented to users. | |
519 // | |
520 // Post-processing includes: | |
521 // - If the build is LogDog-only, promotes aliases (LogDog links) to | |
522 // first-class links in the build. | |
523 func updatePostProcessBuild(b *buildbotBuild) { | |
524 // If this is a LogDog-only build, we want to promote the LogDog links. | |
525 if loc, ok := b.getPropertyValue("log_location").(string); ok && strings
.HasPrefix(loc, "logdog://") { | |
526 linkMap := map[string]string{} | |
527 for sidx := range b.Steps { | |
528 promoteLogDogLinks(&b.Steps[sidx], sidx == 0, linkMap) | |
529 } | |
530 | |
531 // Update "Logs". This field is part of BuildBot, and is the ama
lgamation | |
532 // of all logs in the build's steps. Since each log is out of co
ntext of its | |
533 // original step, we can't apply the promotion logic; instead, w
e will use | |
534 // the link map to map any old URLs that were matched in "promot
eLogDogLnks" | |
535 // to their new URLs. | |
536 for _, link := range b.Logs { | |
537 // "link" is in the form: [NAME, URL] | |
538 if len(link) != 2 { | |
539 continue | |
540 } | |
541 | |
542 if newURL, ok := linkMap[link[1]]; ok { | |
543 link[1] = newURL | |
544 } | |
545 } | |
546 } | |
547 } | |
548 | |
549 // promoteLogDogLinks updates the links in a BuildBot step to | |
550 // promote LogDog links. | |
551 // | |
552 // A build's links come in one of three forms: | |
553 // - Log Links, which link directly to BuildBot build logs. | |
554 // - URL Links, which are named links to arbitrary URLs. | |
555 // - Aliases, which attach to the label in one of the other types of links
and | |
556 // augment it with additional named links. | |
557 // | |
558 // LogDog uses aliases exclusively to attach LogDog logs to other links. When | |
559 // the build is LogDog-only, though, the original links are actually junk. What | |
560 // we want to do is remove the original junk links and replace them with their | |
561 // alias counterparts, so that the "natural" BuildBot links are actually LogDog | |
562 // links. | |
563 // | |
564 // As URLs are re-mapped, the supplied "linkMap" will be updated to map the old | |
565 // URLs to the new ones. | |
566 func promoteLogDogLinks(s *buildbotStep, isInitialStep bool, linkMap map[string]
string) { | |
567 type stepLog struct { | |
568 label string | |
569 url string | |
570 } | |
571 | |
572 remainingAliases := stringset.New(len(s.Aliases)) | |
573 for linkAnchor := range s.Aliases { | |
574 remainingAliases.Add(linkAnchor) | |
575 } | |
576 | |
577 maybePromoteAliases := func(sl *stepLog, isLog bool) []*stepLog { | |
578 // As a special case, if this is the first step ("steps" in Buil
dBot), we | |
579 // will refrain from promoting aliases for "stdio", since "stdio
" represents | |
580 // the raw BuildBot logs. | |
581 if isLog && isInitialStep && sl.label == "stdio" { | |
582 // No aliases, don't modify this log. | |
583 return []*stepLog{sl} | |
584 } | |
585 | |
586 // If there are no aliases, we should obviously not promote them
. This will | |
587 // be the case for pre-LogDog steps such as build setup. | |
588 aliases := s.Aliases[sl.label] | |
589 if len(aliases) == 0 { | |
590 return []*stepLog{sl} | |
591 } | |
592 | |
593 // We have chosen to promote the aliases. Therefore, we will not
include | |
594 // them as aliases in the modified step. | |
595 remainingAliases.Del(sl.label) | |
596 | |
597 result := make([]*stepLog, len(aliases)) | |
598 for i, alias := range aliases { | |
599 aliasStepLog := stepLog{alias.Text, alias.URL} | |
600 | |
601 // Any link named "logdog" (Annotee cosmetic implementat
ion detail) will | |
602 // inherit the name of the original log. | |
603 if isLog { | |
604 if aliasStepLog.label == "logdog" { | |
605 aliasStepLog.label = sl.label | |
606 } | |
607 } | |
608 | |
609 result[i] = &aliasStepLog | |
610 } | |
611 | |
612 // If we performed mapping, add the OLD -> NEW URL mapping to li
nkMap. | |
613 // | |
614 // Since multpiple aliases can apply to a single log, and we hav
e to pick | |
615 // one, here, we'll arbitrarily pick the last one. This is maybe
more | |
616 // consistent than the first one because linkMap, itself, will e
nd up | |
617 // holding the last mapping for any given URL. | |
618 if len(result) > 0 { | |
619 linkMap[sl.url] = result[len(result)-1].url | |
620 } | |
621 | |
622 return result | |
623 } | |
624 | |
625 // Update step logs. | |
626 newLogs := make([][]string, 0, len(s.Logs)) | |
627 for _, l := range s.Logs { | |
628 for _, res := range maybePromoteAliases(&stepLog{l[0], l[1]}, tr
ue) { | |
629 newLogs = append(newLogs, []string{res.label, res.url}) | |
630 } | |
631 } | |
632 s.Logs = newLogs | |
633 | |
634 // Update step URLs. | |
635 newURLs := make(map[string]string, len(s.Urls)) | |
636 for label, link := range s.Urls { | |
637 urlLinks := maybePromoteAliases(&stepLog{label, link}, false) | |
638 if len(urlLinks) > 0 { | |
639 // Use the last URL link, since our URL map can only tol
erate one link. | |
640 // The expected case here is that len(urlLinks) == 1, th
ough, but it's | |
641 // possible that multiple aliases can be included for a
single URL, so | |
642 // we need to handle that. | |
643 newValue := urlLinks[len(urlLinks)-1] | |
644 newURLs[newValue.label] = newValue.url | |
645 } else { | |
646 newURLs[label] = link | |
647 } | |
648 } | |
649 s.Urls = newURLs | |
650 | |
651 // Preserve any aliases that haven't been promoted. | |
652 var newAliases map[string][]*buildbotLinkAlias | |
653 if l := remainingAliases.Len(); l > 0 { | |
654 newAliases = make(map[string][]*buildbotLinkAlias, l) | |
655 remainingAliases.Iter(func(v string) bool { | |
656 newAliases[v] = s.Aliases[v] | |
657 return true | |
658 }) | |
659 } | |
660 s.Aliases = newAliases | |
661 } | |
OLD | NEW |