| // Copyright 2017 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package maintapi |
| |
| import ( |
| "context" |
| "encoding/hex" |
| "flag" |
| "fmt" |
| "strings" |
| "sync" |
| "testing" |
| "time" |
| |
| "github.com/google/go-cmp/cmp" |
| "golang.org/x/build/gerrit" |
| "golang.org/x/build/maintner" |
| "golang.org/x/build/maintner/godata" |
| "golang.org/x/build/maintner/maintnerd/apipb" |
| "grpc.go4.org" |
| "grpc.go4.org/codes" |
| ) |
| |
| func TestGetRef(t *testing.T) { |
| c := getGoData(t) |
| s := apiService{c} |
| req := &apipb.GetRefRequest{ |
| GerritServer: "go.googlesource.com", |
| GerritProject: "go", |
| Ref: "refs/heads/master", |
| } |
| res, err := s.GetRef(context.Background(), req) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if len(res.Value) != 40 { |
| t.Errorf("go master ref = %q; want length 40 string", res.Value) |
| } |
| |
| // Bogus ref |
| req.Ref = "NOT EXIST REF" |
| res, err = s.GetRef(context.Background(), req) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if len(res.Value) != 0 { |
| t.Errorf("go bogus ref = %q; want empty string", res.Value) |
| } |
| |
| // Bogus project |
| req.GerritProject = "NOT EXIST PROJ" |
| _, err = s.GetRef(context.Background(), req) |
| if got, want := fmt.Sprint(err), "unknown gerrit project"; got != want { |
| t.Errorf("error for bogus project = %q; want %q", got, want) |
| } |
| } |
| |
| var hitGerrit = flag.Bool("hit_gerrit", false, "query production Gerrit in TestFindTryWork") |
| |
| func TestFindTryWork(t *testing.T) { |
| if !*hitGerrit { |
| t.Skip("skipping without flag --hit_gerrit") |
| } |
| c := getGoData(t) |
| s := apiService{c} |
| req := &apipb.GoFindTryWorkRequest{} |
| t0 := time.Now() |
| res, err := s.GoFindTryWork(context.Background(), req) |
| d0 := time.Since(t0) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Just for interactive debugging. This is using live data. |
| // The stable tests are in TestTryWorkItem and TestTryBotStatus. |
| t.Logf("Current: %v", res) |
| |
| t1 := time.Now() |
| res, err = s.GoFindTryWork(context.Background(), req) |
| d1 := time.Since(t1) |
| t.Logf("Latency: %v, then %v", d0, d1) |
| t.Logf("Cached: %v, %v", res, err) |
| } |
| |
| func TestTryBotStatus(t *testing.T) { |
| c := getGoData(t) |
| tests := []struct { |
| proj string |
| clnum int32 |
| msgCutoff int |
| wantTry bool |
| wantDone bool |
| }{ |
| {"go", 51430, 1, true, false}, |
| {"go", 51430, 2, true, false}, |
| {"go", 51430, 3, true, true}, |
| |
| {"build", 48968, 5, true, false}, // adding trybot (coordinator ignores for "build" repo) |
| {"build", 48968, 6, false, false}, // removing it |
| } |
| for _, tt := range tests { |
| cl := c.Gerrit().Project("go.googlesource.com", tt.proj).CL(tt.clnum) |
| if cl == nil { |
| t.Errorf("CL %d in %s not found", tt.clnum, tt.proj) |
| continue |
| } |
| old := *cl // save before mutations |
| cl.Version = cl.Messages[tt.msgCutoff-1].Version |
| cl.Messages = cl.Messages[:tt.msgCutoff] |
| gotTry, gotDone := tryBotStatus(cl, false /* not staging */) |
| if gotTry != tt.wantTry || gotDone != tt.wantDone { |
| t.Errorf("tryBotStatus(%q, %d) after %d messages = try/done %v, %v; want %v, %v", |
| tt.proj, tt.clnum, tt.msgCutoff, gotTry, gotDone, tt.wantTry, tt.wantDone) |
| for _, msg := range cl.Messages { |
| t.Logf(" msg ver=%d, text=%q", msg.Version, msg.Message) |
| } |
| } |
| *cl = old // restore |
| } |
| } |
| |
| func TestTryWorkItem(t *testing.T) { |
| c := getGoData(t) |
| tests := []struct { |
| proj string |
| clnum int32 |
| ci *gerrit.ChangeInfo |
| want string |
| }{ |
| // Same Change-Id, different branch: |
| {"go", 51430, nil, `project:"go" branch:"master" change_id:"I0bcae339624e7d61037d9ea0885b7bd07491bbb6" commit:"45a4609c0ae214e448612e0bc0846e2f2682f1b2" `}, |
| {"go", 51450, nil, `project:"go" branch:"release-branch.go1.9" change_id:"I0bcae339624e7d61037d9ea0885b7bd07491bbb6" commit:"7320506bc58d3a55eff2c67b2ec65cfa94f7b0a7" `}, |
| // Different project: |
| {"build", 51432, nil, `project:"build" branch:"master" change_id:"I1f71836da7008e58d3e76e2cc3170e96cd57ddf6" commit:"9251bc9950baff61d95da0761e2e4bfab61ed210" `}, |
| |
| // With comments: |
| { |
| proj: "go", |
| clnum: 201203, |
| ci: &gerrit.ChangeInfo{ |
| CurrentRevision: "f99d33e72efdea68fce39765bc94479b5ebed0a9", |
| Revisions: map[string]gerrit.RevisionInfo{ |
| "f99d33e72efdea68fce39765bc94479b5ebed0a9": {PatchSetNumber: 88}, |
| }, |
| Messages: []gerrit.ChangeMessageInfo{ |
| { |
| Author: &gerrit.AccountInfo{NumericID: 1234}, |
| Message: "Patch Set 1: Run-TryBot+1\n\nTRY=foo", |
| }, |
| { |
| Author: &gerrit.AccountInfo{NumericID: 5678}, |
| Message: "Patch Set 2: Foo-2 Run-TryBot+1\n\nTRY=bar", |
| }, |
| }, |
| }, |
| want: `project:"go" branch:"master" change_id:"I358eb7b11768df8c80fb7e805abd4cd01d52bb9b" commit:"f99d33e72efdea68fce39765bc94479b5ebed0a9" version:88 try_message:<message:"foo" author_id:1234 version:1 > try_message:<message:"bar" author_id:5678 version:2 > `, |
| }, |
| } |
| for _, tt := range tests { |
| cl := c.Gerrit().Project("go.googlesource.com", tt.proj).CL(tt.clnum) |
| if cl == nil { |
| t.Errorf("CL %d in %s not found", tt.clnum, tt.proj) |
| continue |
| } |
| got := fmt.Sprint(tryWorkItem(cl, tt.ci)) |
| if got != tt.want { |
| t.Errorf("tryWorkItem(%q, %v) mismatch:\n got: %#q\nwant: %#q", tt.proj, tt.clnum, got, tt.want) |
| } |
| } |
| } |
| |
| var ( |
| corpusMu sync.Mutex |
| corpusCache *maintner.Corpus |
| ) |
| |
| func getGoData(tb testing.TB) *maintner.Corpus { |
| if testing.Short() { |
| tb.Skip("skipping maintner godata test in short mode") |
| } |
| corpusMu.Lock() |
| defer corpusMu.Unlock() |
| if corpusCache != nil { |
| return corpusCache |
| } |
| var err error |
| corpusCache, err = godata.Get(context.Background()) |
| if err != nil { |
| tb.Fatalf("getting corpus: %v", err) |
| } |
| return corpusCache |
| } |
| |
| func TestSupportedGoReleases(t *testing.T) { |
| tests := []struct { |
| goProj nonChangeRefLister |
| want []*apipb.GoRelease |
| }{ |
| // A sample of real data from maintner. |
| { |
| goProj: gerritProject{ |
| refs: []refHash{ |
| {"HEAD", gitHash("5168fcf63f5001b38f9ac64ce5c5e3c2d397363d")}, |
| {"refs/heads/dev.boringcrypto", gitHash("13bf5b80e8d8841a2a3c9b0d5dec65a0c8636253")}, |
| {"refs/heads/dev.boringcrypto.go1.10", gitHash("2e2a04a605b6c3fc6e733810bdcd0200d8ed25a8")}, |
| {"refs/heads/dev.boringcrypto.go1.11", gitHash("685dc1638240af70c86a146b0ddb86d51d64f269")}, |
| {"refs/heads/dev.typealias", gitHash("8a5ef1501dee0715093e87cdc1c9b6becb81c882")}, |
| {"refs/heads/master", gitHash("5168fcf63f5001b38f9ac64ce5c5e3c2d397363d")}, |
| {"refs/heads/release-branch.go1", gitHash("08b97d4061dd75ceec1d44e4335183cd791c9306")}, |
| {"refs/heads/release-branch.go1.1", gitHash("1d6d8fca241bb611af51e265c1b5a2e9ae904702")}, |
| {"refs/heads/release-branch.go1.10", gitHash("e97b7d68f107ff60152f5bd5701e0286f221ee93")}, |
| {"refs/heads/release-branch.go1.11", gitHash("97781d2ed116d2cd9cb870d0b84fc0ec598c9abc")}, |
| {"refs/heads/release-branch.go1.10-security", gitHash("25ca8f49c3fc4a68daff7a23ab613e3453be5cda")}, |
| {"refs/heads/release-branch.go1.11-security", gitHash("90c896448691b5edb0ab11110f37234f63cd28ed")}, |
| {"refs/heads/release-branch.go1.2", gitHash("43d00b0942c1c6f43993ac71e1eea48e62e22b8d")}, |
| {"refs/heads/release-branch.r59", gitHash("5d9765785dff74784bbdad43f7847b6825509032")}, |
| {"refs/heads/release-branch.r60", gitHash("394b383a1ee0ac3fec5e453a7dbe590d3ce6d6b0")}, |
| {"refs/notes/review", gitHash("c46ab9dacb2ac618d86f1c1f719bc2de46010e86")}, |
| {"refs/tags/1.10beta1.mailed", gitHash("2df74db61620771e4f878c9e1db7aeecc00808ba")}, |
| {"refs/tags/andybons/blog.mailed", gitHash("707a89416af909a3af6c26df93995bc17bf9ce81")}, |
| {"refs/tags/go1", gitHash("6174b5e21e73714c63061e66efdbe180e1c5491d")}, |
| {"refs/tags/go1.0.1", gitHash("2fffba7fe19690e038314d17a117d6b87979c89f")}, |
| {"refs/tags/go1.0.2", gitHash("cb6c6570b73a1c4d19cad94570ed277f7dae55ac")}, |
| {"refs/tags/go1.0.3", gitHash("30be9b4313622c2077539e68826194cb1028c691")}, |
| {"refs/tags/go1.1", gitHash("205f850ceacfc39d1e9d76a9569416284594ce8c")}, |
| {"refs/tags/go1.10", gitHash("bf86aec25972f3a100c3aa58a6abcbcc35bdea49")}, |
| {"refs/tags/go1.10.1", gitHash("ac7c0ee26dda18076d5f6c151d8f920b43340ae3")}, |
| {"refs/tags/go1.10.2", gitHash("71bdbf431b79dff61944f22c25c7e085ccfc25d5")}, |
| {"refs/tags/go1.10.3", gitHash("fe8a0d12b14108cbe2408b417afcaab722b0727c")}, |
| {"refs/tags/go1.10.4", gitHash("2191fce26a7fd1cd5b4975e7bd44ab44b1d9dd78")}, |
| {"refs/tags/go1.10beta1", gitHash("9ce6b5c2ed5d3d5251b9a6a0c548d5fb2c8567e8")}, |
| {"refs/tags/go1.10beta2", gitHash("594668a5a96267a46282ce3007a584ec07adf705")}, |
| {"refs/tags/go1.10rc1", gitHash("5348aed83e39bd1d450d92d7f627e994c2db6ebf")}, |
| {"refs/tags/go1.10rc2", gitHash("20e228f2fdb44350c858de941dff4aea9f3127b8")}, |
| {"refs/tags/go1.11", gitHash("41e62b8c49d21659b48a95216e3062032285250f")}, |
| {"refs/tags/go1.11.1", gitHash("26957168c4c0cdcc7ca4f0b19d0eb19474d224ac")}, |
| {"refs/tags/go1.11beta1", gitHash("a12c1f26e4cc602dae62ec065a237172a5b8f926")}, |
| {"refs/tags/go1.11beta2", gitHash("c814ac44c0571f844718f07aa52afa47e37fb1ed")}, |
| {"refs/tags/go1.11beta3", gitHash("1b870077c896379c066b41657d3c9062097a6943")}, |
| {"refs/tags/go1.11rc1", gitHash("807e7f2420c683384dc9c6db498808ba1b7aab17")}, |
| {"refs/tags/go1.11rc2", gitHash("02c0c32960f65d0b9c66ec840c612f5f9623dc51")}, |
| {"refs/tags/go1.9.7", gitHash("7df09b4a03f9e53334672674ba7983d5e7128646")}, |
| {"refs/tags/go1.9beta1", gitHash("952ecbe0a27aadd184ca3e2c342beb464d6b1653")}, |
| {"refs/tags/go1.9beta2", gitHash("eab99a8d548f8ba864647ab171a44f0a5376a6b3")}, |
| {"refs/tags/go1.9rc1", gitHash("65c6c88a9442b91d8b2fd0230337b1fda4bb6cdf")}, |
| {"refs/tags/go1.9rc2", gitHash("048c9cfaacb6fe7ac342b0acd8ca8322b6c49508")}, |
| {"refs/tags/release.r59", gitHash("5d9765785dff74784bbdad43f7847b6825509032")}, |
| {"refs/tags/release.r60", gitHash("5464bfebe723752dfc09a6dd6b361b8e79db5995")}, |
| {"refs/tags/release.r60.1", gitHash("4af7136fcf874e212d66c72178a68db969918b25")}, |
| {"refs/tags/weekly", gitHash("3895b5051df256b442d0b0af50debfffd8d75164")}, |
| {"refs/tags/weekly.2009-11-10", gitHash("78c47c36b2984058c1bec0bd72e0b127b24fcd44")}, |
| {"refs/tags/weekly.2009-11-10.1", gitHash("c57054f7b49539ca4ed6533267c1c20c39aaaaa5")}, |
| }, |
| }, |
| want: []*apipb.GoRelease{ |
| { |
| Major: 1, Minor: 11, Patch: 1, |
| TagName: "go1.11.1", |
| TagCommit: "26957168c4c0cdcc7ca4f0b19d0eb19474d224ac", |
| BranchName: "release-branch.go1.11", |
| BranchCommit: "97781d2ed116d2cd9cb870d0b84fc0ec598c9abc", |
| }, |
| { |
| Major: 1, Minor: 10, Patch: 4, |
| TagName: "go1.10.4", |
| TagCommit: "2191fce26a7fd1cd5b4975e7bd44ab44b1d9dd78", |
| BranchName: "release-branch.go1.10", |
| BranchCommit: "e97b7d68f107ff60152f5bd5701e0286f221ee93", |
| }, |
| }, |
| }, |
| |
| // Detect and handle a new major version. |
| { |
| goProj: gerritProject{ |
| refs: []refHash{ |
| {"refs/tags/go1.5", gitHash("9b82ca331d1fa30e3428e7914ba780ae7f75a702")}, |
| {"refs/tags/go1.42.1", gitHash("23982c09ae5ac811d1dd0099e1626596ade61000")}, |
| {"refs/tags/go1", gitHash("5c503fde0aa534d3259533802052f936c95fa782")}, |
| {"refs/tags/go2", gitHash("43126518de2eb0dadc0917a593f08637318986bf")}, |
| {"refs/tags/go1.11.111", gitHash("c59f000d9bb66592ff84a942014afd1a7be4c953")}, // The onesiest release ever! |
| {"refs/heads/release-branch.go1", gitHash("b0f2d801c19fc8798ecf67e50364a44dba606fcd")}, |
| {"refs/heads/release-branch.go1.5", gitHash("a6ae58c93408bcc17758d397eed0ace973de8481")}, |
| {"refs/heads/release-branch.go1.11", gitHash("f4f148ef7962271ff8ffcebf13400ded535e9957")}, |
| {"refs/heads/release-branch.go1.42", gitHash("362986e7a4b5edc911ed55324c37106c40abe3fb")}, |
| {"refs/heads/release-branch.go2", gitHash("cfbe0f14bcbf1e773f8dd9a968c80cf0b9238c59")}, |
| {"refs/heads/release-branch.go1.2", gitHash("6523e1eb33ef792df04e08462ed332b95311261e")}, |
| |
| // It doesn't count as a release if there's no corresponding release-branch.go1.43 release branch. |
| {"refs/tags/go1.43", gitHash("3aa7f7065ecf717b1dd6512bb7a9f40625fc8cb5")}, |
| }, |
| }, |
| want: []*apipb.GoRelease{ |
| { |
| Major: 2, Minor: 0, Patch: 0, |
| TagName: "go2", |
| TagCommit: "43126518de2eb0dadc0917a593f08637318986bf", |
| BranchName: "release-branch.go2", |
| BranchCommit: "cfbe0f14bcbf1e773f8dd9a968c80cf0b9238c59", |
| }, |
| { |
| Major: 1, Minor: 42, Patch: 1, |
| TagName: "go1.42.1", |
| TagCommit: "23982c09ae5ac811d1dd0099e1626596ade61000", |
| BranchName: "release-branch.go1.42", |
| BranchCommit: "362986e7a4b5edc911ed55324c37106c40abe3fb", |
| }, |
| }, |
| }, |
| } |
| for i, tt := range tests { |
| got, err := supportedGoReleases(tt.goProj) |
| if err != nil { |
| t.Fatalf("%d: supportedGoReleases: %v", i, err) |
| } |
| if diff := cmp.Diff(got, tt.want); diff != "" { |
| t.Errorf("%d: supportedGoReleases: (-got +want)\n%s", i, diff) |
| } |
| } |
| } |
| |
| func TestGetDashboard(t *testing.T) { |
| c := getGoData(t) |
| s := apiService{c} |
| |
| type check func(t *testing.T, res *apipb.DashboardResponse, resErr error) |
| var noError check = func(t *testing.T, res *apipb.DashboardResponse, resErr error) { |
| t.Helper() |
| if resErr != nil { |
| t.Fatalf("GetDashboard: %v", resErr) |
| } |
| } |
| var commitsTruncated check = func(t *testing.T, res *apipb.DashboardResponse, _ error) { |
| t.Helper() |
| if !res.CommitsTruncated { |
| t.Errorf("CommitsTruncated = false; want true") |
| } |
| if len(res.Commits) == 0 { |
| t.Errorf("no commits; expected some commits when expecting CommitsTruncated") |
| } |
| |
| } |
| hasBranch := func(branch string) check { |
| return func(t *testing.T, res *apipb.DashboardResponse, _ error) { |
| ok := false |
| for _, b := range res.Branches { |
| if b == branch { |
| ok = true |
| break |
| } |
| } |
| if !ok { |
| t.Errorf("didn't find expected branch %q; got branches: %q", branch, res.Branches) |
| } |
| } |
| } |
| hasRepoHead := func(proj string) check { |
| return func(t *testing.T, res *apipb.DashboardResponse, _ error) { |
| ok := false |
| var got []string |
| for _, rh := range res.RepoHeads { |
| if rh.GerritProject == proj { |
| ok = true |
| } |
| got = append(got, rh.GerritProject) |
| } |
| if !ok { |
| t.Errorf("didn't find expected repo head %q; got: %q", proj, got) |
| } |
| } |
| } |
| var hasThreeReleases check = func(t *testing.T, res *apipb.DashboardResponse, _ error) { |
| t.Helper() |
| var got []string |
| var gotMaster int |
| var gotReleaseBranch int |
| var uniq = map[string]bool{} |
| for _, r := range res.Releases { |
| got = append(got, r.BranchName) |
| uniq[r.BranchName] = true |
| if r.BranchName == "master" { |
| gotMaster++ |
| } |
| if strings.HasPrefix(r.BranchName, "release-branch.go") { |
| gotReleaseBranch++ |
| } |
| } |
| if len(uniq) != 3 { |
| t.Errorf("expected 3 Go releases, got: %q", got) |
| } |
| if gotMaster != 1 { |
| t.Errorf("expected 1 Go release to be master, got: %q", got) |
| } |
| if gotReleaseBranch != 2 { |
| t.Errorf("expected 2 Go releases to be release branches, got: %q", got) |
| } |
| } |
| wantRPCError := func(code codes.Code) check { |
| return func(t *testing.T, _ *apipb.DashboardResponse, err error) { |
| if grpc.Code(err) != code { |
| t.Errorf("expected RPC code %v; got %v (err %v)", code, grpc.Code(err), err) |
| } |
| } |
| } |
| basicChecks := []check{ |
| noError, |
| commitsTruncated, |
| hasBranch("master"), |
| hasBranch("release-branch.go1.4"), |
| hasBranch("release-branch.go1.13"), |
| hasRepoHead("net"), |
| hasRepoHead("sys"), |
| hasThreeReleases, |
| } |
| |
| tests := []struct { |
| name string |
| req *apipb.DashboardRequest |
| checks []check |
| }{ |
| // Verify that the default view (with no options) works. |
| { |
| name: "zero_value", |
| req: &apipb.DashboardRequest{}, |
| checks: basicChecks, |
| }, |
| // Or with explicit values: |
| { |
| name: "zero_value_effectively", |
| req: &apipb.DashboardRequest{ |
| Repo: "go", |
| Branch: "master", |
| }, |
| checks: basicChecks, |
| }, |
| // Max commits: |
| { |
| name: "max_commits", |
| req: &apipb.DashboardRequest{MaxCommits: 1}, |
| checks: []check{ |
| noError, |
| commitsTruncated, |
| func(t *testing.T, res *apipb.DashboardResponse, _ error) { |
| if got, want := len(res.Commits), 1; got != want { |
| t.Errorf("got %v commits; want %v", got, want) |
| } |
| }, |
| }, |
| }, |
| // Verify that branch=mixed doesn't return an error at least. |
| { |
| name: "mixed", |
| req: &apipb.DashboardRequest{Branch: "mixed"}, |
| checks: []check{ |
| noError, |
| commitsTruncated, |
| hasRepoHead("sys"), |
| hasThreeReleases, |
| }, |
| }, |
| // Verify non-Go repos: |
| { |
| name: "non_go_repo", |
| req: &apipb.DashboardRequest{Repo: "golang.org/x/net"}, |
| checks: []check{ |
| noError, |
| commitsTruncated, |
| func(t *testing.T, res *apipb.DashboardResponse, _ error) { |
| for _, c := range res.Commits { |
| if c.GoCommitAtTime == "" { |
| t.Errorf("response contains commit without GoCommitAtTime") |
| } |
| if c.GoCommitLatest == "" { |
| t.Errorf("response contains commit without GoCommitLatest") |
| } |
| if t.Failed() { |
| return |
| } |
| } |
| }, |
| }, |
| }, |
| |
| // Validate rejection of bad requests: |
| { |
| name: "bad-repo", |
| req: &apipb.DashboardRequest{Repo: "NOT_EXIST"}, |
| checks: []check{wantRPCError(codes.NotFound)}, |
| }, |
| { |
| name: "bad-branch", |
| req: &apipb.DashboardRequest{Branch: "NOT_EXIST"}, |
| checks: []check{wantRPCError(codes.NotFound)}, |
| }, |
| { |
| name: "mixed-with-pagination", |
| req: &apipb.DashboardRequest{Branch: "mixed", Page: 5}, |
| checks: []check{wantRPCError(codes.InvalidArgument)}, |
| }, |
| { |
| name: "negative-page", |
| req: &apipb.DashboardRequest{Page: -1}, |
| checks: []check{wantRPCError(codes.InvalidArgument)}, |
| }, |
| { |
| name: "too-big-page", |
| req: &apipb.DashboardRequest{Page: 1e6}, |
| checks: []check{wantRPCError(codes.InvalidArgument)}, |
| }, |
| } |
| |
| for _, tt := range tests { |
| t.Run(tt.name, func(t *testing.T) { |
| res, err := s.GetDashboard(context.Background(), tt.req) |
| for _, c := range tt.checks { |
| c(t, res, err) |
| } |
| }) |
| } |
| } |
| |
| type gerritProject struct { |
| refs []refHash |
| } |
| |
| func (gp gerritProject) ForeachNonChangeRef(fn func(ref string, hash maintner.GitHash) error) error { |
| for _, r := range gp.refs { |
| err := fn(r.Ref, r.Hash) |
| if err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| type refHash struct { |
| Ref string |
| Hash maintner.GitHash |
| } |
| |
| func gitHash(hexa string) maintner.GitHash { |
| if len(hexa) != 40 { |
| panic(fmt.Errorf("bogus git hash %q", hexa)) |
| } |
| binary, err := hex.DecodeString(hexa) |
| if err != nil { |
| panic(fmt.Errorf("bogus git hash %q: %v", hexa, err)) |
| } |
| return maintner.GitHash(binary) |
| } |