├── LICENSE ├── README.md ├── Screenshot.png ├── _data ├── component │ └── main.go └── style.css ├── assets ├── assets.go ├── assets_vfsdata.go ├── doc.go └── external.go ├── cmd └── Go-Package-Store │ ├── buildutil.go │ ├── dep.go │ ├── dev.go │ ├── errorhandler.go │ ├── godep.go │ ├── index.go │ ├── main.go │ ├── prod.go │ ├── update.go │ └── updates.go ├── component ├── doc.go ├── header.go ├── presentation.go └── updates.go ├── doc.go ├── frontend ├── action │ └── action.go ├── main.go ├── model │ └── model.go └── store │ └── store.go ├── go.mod ├── presenter ├── github │ ├── github.go │ └── gopkgin.go ├── gitiles │ ├── gitiles.go │ ├── gitiles_test.go │ └── testdata │ │ └── +log.json └── presenter.go ├── repo.go ├── updater.go ├── updater ├── dep.go ├── doc.go ├── gopath.go └── mock.go └── workspace └── workspace.go /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2014 Dmitri Shuralyov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Go Package Store 2 | ================ 3 | 4 | [![Go Reference](https://pkg.go.dev/badge/github.com/shurcooL/Go-Package-Store.svg)](https://pkg.go.dev/github.com/shurcooL/Go-Package-Store) 5 | 6 | Go Package Store displays updates for the Go packages in your GOPATH. 7 | 8 | ![](Screenshot.png) 9 | 10 | Installation 11 | ------------ 12 | 13 | ```sh 14 | go install github.com/shurcooL/Go-Package-Store/cmd/Go-Package-Store@latest 15 | ``` 16 | 17 | Usage 18 | ----- 19 | 20 | ``` 21 | Usage: Go-Package-Store [flags] 22 | [newline separated packages] | Go-Package-Store -stdin [flags] 23 | -dep string 24 | Determine the list of Go packages from the specified Gopkg.toml file. 25 | -git-subrepo string 26 | Look for Go packages vendored using git-subrepo in the specified vendor directory. 27 | -godeps string 28 | Read the list of Go packages from the specified Godeps.json file. 29 | -http string 30 | Listen for HTTP connections on this address. (default "localhost:7043") 31 | -stdin 32 | Read the list of newline separated Go packages from stdin. 33 | 34 | GitHub Access Token: 35 | To display updates for private repositories on GitHub, or when 36 | you've exceeded the unauthenticated rate limit, you can provide 37 | a GitHub access token for Go Package Store to use via the 38 | GO_PACKAGE_STORE_GITHUB_TOKEN environment variable. 39 | 40 | Examples: 41 | # Check for updates for all Go packages in GOPATH. 42 | Go-Package-Store 43 | 44 | # Show updates for all golang.org/x/... packages. 45 | go list golang.org/x/... | Go-Package-Store -stdin 46 | 47 | # Show updates for all dependencies within Gopkg.toml constraints. 48 | Go-Package-Store -dep=/path/to/repo/Gopkg.toml 49 | 50 | # Show updates for all Go packages vendored using git-subrepo 51 | # in the specified vendor directory. 52 | Go-Package-Store -git-subrepo=/path/to/repo/vendor 53 | ``` 54 | 55 | Development 56 | ----------- 57 | 58 | This package relies on `go generate` directives to process and statically embed assets. For development only, you may need extra dependencies. You can build and run the package in development mode, where all assets are always read and processed from disk: 59 | 60 | ```sh 61 | go build -tags=dev github.com/shurcooL/Go-Package-Store/cmd/Go-Package-Store 62 | ``` 63 | 64 | When you're done with development, you should run `go generate` before committing: 65 | 66 | ```sh 67 | go generate github.com/shurcooL/Go-Package-Store/... 68 | ``` 69 | 70 | Alternatives 71 | ------------ 72 | 73 | - [GoFresh](https://github.com/divan/gofresh) - Console tool for checking and updating package dependencies (imports). 74 | 75 | License 76 | ------- 77 | 78 | - [MIT License](LICENSE) 79 | -------------------------------------------------------------------------------- /Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shurcooL/Go-Package-Store/d13d54834edd0c3add2369e10cfc03be7a0a7c78/Screenshot.png -------------------------------------------------------------------------------- /_data/component/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/gopherjs/vecty" 5 | "github.com/gopherjs/vecty/elem" 6 | gpscomponent "github.com/shurcooL/Go-Package-Store/component" 7 | "github.com/shurcooL/Go-Package-Store/frontend/model" 8 | ) 9 | 10 | func main() { 11 | vecty.RenderBody(&UpdatesBody{}) 12 | } 13 | 14 | type UpdatesBody struct { 15 | vecty.Core 16 | } 17 | 18 | func (*UpdatesBody) Render() vecty.ComponentOrHTML { 19 | return elem.Body( 20 | gpscomponent.UpdatesContent( 21 | mockActive, 22 | mockHistory, 23 | true, 24 | )..., 25 | ) 26 | } 27 | 28 | var mockActive = []*model.RepoPresentation{ 29 | { 30 | RepoRoot: "github.com/gopherjs/gopherjs", 31 | ImportPathPattern: "github.com/gopherjs/gopherjs/...", 32 | LocalRevision: "", 33 | RemoteRevision: "", 34 | HomeURL: "https://github.com/gopherjs/gopherjs", 35 | ImageURL: "https://avatars.githubusercontent.com/u/6654647?v=3", 36 | Changes: []model.Change{ 37 | { 38 | Message: "improved reflect support for blocking functions", 39 | URL: "https://github.com/gopherjs/gopherjs/commit/87bf7e405aa3df6df0dcbb9385713f997408d7b9", 40 | Comments: model.Comments{ 41 | Count: 0, 42 | URL: "", 43 | }, 44 | }, 45 | { 46 | Message: "small cleanup", 47 | URL: "https://github.com/gopherjs/gopherjs/commit/77a838f965881a888416bae38f790f76bb1f64bd", 48 | Comments: model.Comments{ 49 | Count: 1, 50 | URL: "https://www.example.com/", 51 | }, 52 | }, 53 | { 54 | Message: "replaced js.This and js.Arguments by js.MakeFunc", 55 | URL: "https://github.com/gopherjs/gopherjs/commit/29dd054a0753760fe6e826ded0982a1bf69f702a", 56 | Comments: model.Comments{ 57 | Count: 0, 58 | URL: "", 59 | }, 60 | }, 61 | }, 62 | Error: "", 63 | UpdateState: model.Available, 64 | UpdateSupported: true, 65 | }, 66 | { 67 | RepoRoot: "golang.org/x/image", 68 | ImportPathPattern: "golang.org/x/image/...", 69 | LocalRevision: "", 70 | RemoteRevision: "", 71 | HomeURL: "http://golang.org/x/image", 72 | ImageURL: "https://avatars.githubusercontent.com/u/4314092?v=3", 73 | Changes: []model.Change{ 74 | { 75 | Message: "draw: generate code paths for image.Gray sources.", 76 | URL: "https://github.com/golang/image/commit/f510ad81a1256ee96a2870647b74fa144a30c249", 77 | Comments: model.Comments{ 78 | Count: 0, 79 | URL: "", 80 | }, 81 | }, 82 | }, 83 | Error: "", 84 | UpdateState: model.Updating, 85 | UpdateSupported: true, 86 | }, 87 | { 88 | RepoRoot: "unknown.com/package", 89 | ImportPathPattern: "unknown.com/package/...", 90 | LocalRevision: "abcdef0123456789000000000000000000000000", 91 | RemoteRevision: "d34db33f01010101010101010101010101010101", 92 | HomeURL: "https://unknown.com/package", 93 | ImageURL: "https://github.com/images/gravatars/gravatar-user-420.png", 94 | Changes: nil, 95 | Error: "", 96 | UpdateState: model.Available, 97 | UpdateSupported: true, 98 | }, 99 | } 100 | 101 | var mockHistory = []*model.RepoPresentation{ 102 | { 103 | RepoRoot: "golang.org/x/image", 104 | ImportPathPattern: "golang.org/x/image/...", 105 | LocalRevision: "", 106 | RemoteRevision: "", 107 | HomeURL: "http://golang.org/x/image", 108 | ImageURL: "https://avatars.githubusercontent.com/u/4314092?v=3", 109 | Changes: []model.Change{ 110 | { 111 | Message: "draw: generate code paths for image.Gray sources.", 112 | URL: "https://github.com/golang/image/commit/f510ad81a1256ee96a2870647b74fa144a30c249", 113 | Comments: model.Comments{ 114 | Count: 0, 115 | URL: "", 116 | }, 117 | }, 118 | }, 119 | Error: "", 120 | UpdateState: model.Updated, 121 | UpdateSupported: true, 122 | }, 123 | } 124 | -------------------------------------------------------------------------------- /_data/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Go; 3 | font-size: 14px; 4 | line-height: 1.428571429; 5 | margin: 0px; 6 | } 7 | header { 8 | background-color: hsl(209, 51%, 92%); 9 | } 10 | header span { 11 | background-color: hsl(209, 51%, 88%); 12 | } 13 | div.center-max-width { 14 | max-width: 900px; 15 | margin-left: auto; 16 | margin-right: auto; 17 | } 18 | div.content { 19 | margin: 12px; 20 | margin-top: 20px; 21 | margin-bottom: 80px; 22 | } 23 | a, a span { 24 | color: #4183c4; 25 | text-decoration: none; 26 | } 27 | a:hover, a span:hover { 28 | text-decoration: underline; 29 | } 30 | h2 { 31 | margin-top: 40px; 32 | margin-bottom: 40px; 33 | } 34 | 35 | div.list-entry { 36 | border: 1px solid #ddd; 37 | border-radius: 4px; 38 | margin-bottom: 12px; 39 | } 40 | div.list-entry-header { 41 | background-color: #f8f8f8; 42 | padding: 10px; 43 | border-radius: 4px 4px 0 0; 44 | border-bottom: 1px solid #eee; 45 | } 46 | div.list-entry-body { 47 | padding: 10px; 48 | } 49 | .changes-list { 50 | margin-top: 0px; 51 | margin-bottom: 0px; 52 | padding-left: 64px; 53 | } 54 | .presentation-error { 55 | white-space: pre-wrap; 56 | margin-bottom: 0px; 57 | padding-left: 64px; 58 | } 59 | 60 | .highlight-on-hover a { 61 | color: gray; 62 | } 63 | .highlight-on-hover { 64 | float: right; 65 | opacity: 0.25; 66 | } 67 | li:hover .highlight-on-hover { 68 | opacity: 1.0; 69 | } 70 | 71 | .commitID { 72 | font-family: "Go Mono"; 73 | background-color: rgb(238, 238, 238); 74 | padding: 1px 4px; 75 | } 76 | 77 | @media (prefers-color-scheme: dark) { 78 | body { 79 | color: white; 80 | background-color: hsl(210, 15%, 22%); 81 | } 82 | header { 83 | background-color: hsl(209, 51%, 21%); 84 | } 85 | header span { 86 | background-color: hsl(209, 51%, 16%); 87 | } 88 | div.list-entry { 89 | border-color: hsl(210, 15%, 32%); 90 | } 91 | div.list-entry-header { 92 | border-color: hsl(210, 15%, 28%); 93 | } 94 | div.list-entry-header { 95 | background-color: hsl(210, 15%, 18%); 96 | } 97 | .highlight-on-hover a { 98 | color: #ddd; 99 | } 100 | .commitID { 101 | background-color: hsl(210, 15%, 32%); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /assets/assets.go: -------------------------------------------------------------------------------- 1 | //go:build dev 2 | 3 | package assets 4 | 5 | import ( 6 | "go/build" 7 | "log" 8 | "net/http" 9 | 10 | "github.com/shurcooL/go/gopherjs_http" 11 | "github.com/shurcooL/httpfs/union" 12 | ) 13 | 14 | // Assets contains assets for Go Package Store. 15 | var Assets = union.New(map[string]http.FileSystem{ 16 | "/assets": gopherjs_http.NewFS(http.Dir(importPathToDir("github.com/shurcooL/Go-Package-Store/_data"))), 17 | "/frontend.js": gopherjs_http.Package("github.com/shurcooL/Go-Package-Store/frontend"), 18 | }) 19 | 20 | func importPathToDir(importPath string) string { 21 | p, err := build.Import(importPath, "", build.FindOnly) 22 | if err != nil { 23 | log.Fatalln(err) 24 | } 25 | return p.Dir 26 | } 27 | -------------------------------------------------------------------------------- /assets/doc.go: -------------------------------------------------------------------------------- 1 | // Package assets contains assets for Go Package Store. 2 | package assets 3 | 4 | //go:generate vfsgendev -source="github.com/shurcooL/Go-Package-Store/assets".Assets 5 | -------------------------------------------------------------------------------- /assets/external.go: -------------------------------------------------------------------------------- 1 | package assets 2 | 3 | import "github.com/shurcooL/gofontwoff" 4 | 5 | var ( 6 | // Fonts contains the Go font family in Web Open Font Format. 7 | Fonts = gofontwoff.Assets 8 | ) 9 | -------------------------------------------------------------------------------- /cmd/Go-Package-Store/buildutil.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "go/build" 6 | "log" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "strings" 11 | 12 | "github.com/shurcooL/Go-Package-Store/workspace" 13 | "github.com/shurcooL/vcsstate" 14 | "golang.org/x/tools/go/vcs" 15 | ) 16 | 17 | // forEachRepository calls found for each repository it finds in all GOPATH workspaces. 18 | func forEachRepository(found func(workspace.LocalRepo)) { 19 | for _, w := range filepath.SplitList(build.Default.GOPATH) { 20 | srcRoot := filepath.Join(w, "src") 21 | if _, err := os.Stat(srcRoot); os.IsNotExist(err) { 22 | continue 23 | } 24 | // TODO: Confirm that ignoring filepath.Walk error is correct/desired behavior. 25 | _ = filepath.Walk(srcRoot, func(path string, fi os.FileInfo, err error) error { 26 | if err != nil { 27 | log.Printf("can't stat file %s: %v\n", path, err) 28 | return nil 29 | } 30 | if !fi.IsDir() { 31 | return nil 32 | } 33 | if strings.HasPrefix(fi.Name(), ".") || strings.HasPrefix(fi.Name(), "_") || fi.Name() == "testdata" { 34 | return filepath.SkipDir 35 | } 36 | // Determine repo root. This is potentially somewhat slow. 37 | vcsCmd, root, err := vcs.FromDir(path, srcRoot) 38 | if err != nil { 39 | // Directory not under VCS. 40 | return nil 41 | } 42 | found(workspace.LocalRepo{Path: path, Root: root, VCS: vcsCmd}) 43 | return filepath.SkipDir // No need to descend inside repositories. 44 | }) 45 | } 46 | } 47 | 48 | // forEachGitSubrepo calls found for each git subrepo inside vendorDir. 49 | func forEachGitSubrepo(vendorDir string, found func(workspace.Subrepo)) error { 50 | remoteVCS, err := vcsstate.NewRemoteVCS(vcs.ByCmd("git")) 51 | if err != nil { 52 | return fmt.Errorf("git repos not supported by vcsstate: %v", err) 53 | } 54 | 55 | err = filepath.Walk(vendorDir, func(path string, fi os.FileInfo, err error) error { 56 | switch { 57 | case err != nil && path == vendorDir: 58 | return err 59 | case err != nil && path != vendorDir: 60 | log.Printf("can't stat file %s: %v\n", path, err) 61 | return nil 62 | } 63 | if !fi.IsDir() { 64 | return nil 65 | } 66 | if strings.HasPrefix(fi.Name(), ".") { 67 | return filepath.SkipDir 68 | } 69 | remote, commit, err := parseGitRepoFile(path) 70 | if err != nil { 71 | return nil 72 | } 73 | // Root is the import path corresponding to the root of the repository. 74 | // It can be determined relative to the vendor directory. 75 | root, err := filepath.Rel(vendorDir, path) 76 | if err != nil { 77 | return err 78 | } 79 | found(workspace.Subrepo{Root: root, RemoteVCS: remoteVCS, RemoteURL: remote, Revision: commit}) 80 | return filepath.SkipDir // No need to descend inside repositories. 81 | }) 82 | return err 83 | } 84 | 85 | // parseGitRepoFile parses a .gitrepo file in directory, returning 86 | // the remote and commit specified within that file. 87 | func parseGitRepoFile(dir string) (remote string, commit string, _ error) { 88 | remoteBytes, err := exec.Command("git", "config", "--file", filepath.Join(dir, ".gitrepo"), "subrepo.remote").Output() 89 | if err != nil { 90 | return "", "", err 91 | } 92 | remote = strings.TrimSuffix(string(remoteBytes), "\n") 93 | 94 | commitBytes, err := exec.Command("git", "config", "--file", filepath.Join(dir, ".gitrepo"), "subrepo.commit").Output() 95 | if err != nil { 96 | return "", "", err 97 | } 98 | commit = strings.TrimSuffix(string(commitBytes), "\n") 99 | 100 | return remote, commit, nil 101 | } 102 | -------------------------------------------------------------------------------- /cmd/Go-Package-Store/dep.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | ) 10 | 11 | // depDir ensures that "Gopkg.toml" file exists at path, 12 | // and returns the directory that contains it. 13 | func depDir(path string) (string, error) { 14 | // Check that "Gopkg.toml" file exists. 15 | if fi, err := os.Stat(path); err != nil { 16 | return "", err 17 | } else if !(fi.Name() == "Gopkg.toml" && !fi.IsDir()) { 18 | return "", fmt.Errorf("%v is not a Gopkg.toml file", path) 19 | } 20 | dir := filepath.Dir(path) // Directory containing the Gopkg.toml file. 21 | return dir, nil 22 | } 23 | 24 | // runDepStatus runs dep status in directory dir, 25 | // returning a parsed list of dependencies and their status. 26 | func runDepStatus(dir string) (depDependencies, error) { 27 | cmd := exec.Command("dep", "status", "-json") 28 | cmd.Dir = dir 29 | stdout, err := cmd.StdoutPipe() 30 | if err != nil { 31 | return nil, err 32 | } 33 | cmd.Stderr = os.Stderr 34 | err = cmd.Start() 35 | if err != nil { 36 | return nil, err 37 | } 38 | var dependencies depDependencies 39 | err = json.NewDecoder(stdout).Decode(&dependencies) 40 | if err != nil { 41 | return nil, err 42 | } 43 | err = cmd.Wait() 44 | return dependencies, err 45 | } 46 | 47 | type depDependencies []depDependency 48 | 49 | type depDependency struct { 50 | ProjectRoot string // E.g., "github.com/google/go-github". 51 | Revision string // E.g., "6afafa88c26eb51b33a8307c944bd2f0ef227af7". 52 | Latest string // E.g., "fe4b6036cb400908b8903d3df9444b9599a60d57". 53 | } 54 | -------------------------------------------------------------------------------- /cmd/Go-Package-Store/dev.go: -------------------------------------------------------------------------------- 1 | //go:build dev 2 | 3 | package main 4 | 5 | import ( 6 | "errors" 7 | "io" 8 | "net/http" 9 | "time" 10 | 11 | "github.com/shurcooL/Go-Package-Store" 12 | "github.com/shurcooL/Go-Package-Store/presenter" 13 | "github.com/shurcooL/Go-Package-Store/workspace" 14 | "github.com/shurcooL/httperror" 15 | ) 16 | 17 | import _ "net/http/pprof" 18 | 19 | const production = false 20 | 21 | func init() { 22 | http.Handle("/mock.html", errorHandler(mockHandler)) 23 | http.Handle("/component.html", errorHandler(componentHandler)) 24 | } 25 | 26 | func mockHandler(w http.ResponseWriter, req *http.Request) error { 27 | if req.Method != "GET" { 28 | return httperror.Method{Allowed: []string{"GET"}} 29 | } 30 | 31 | // Reset the pipeline and populate it with mock repo presentations, 32 | // complete with artificial delays (to simulate processing time). 33 | c.pipeline = workspace.NewPipeline(wd) 34 | go func() { 35 | for _, rp := range mockWorkspaceRPs { 36 | time.Sleep(5 * time.Second) 37 | rp := rp 38 | c.pipeline.AddPresented(&rp) 39 | } 40 | time.Sleep(5 * time.Second) 41 | c.pipeline.Done() 42 | }() 43 | 44 | return indexHandler(w, req) 45 | } 46 | 47 | func componentHandler(w http.ResponseWriter, req *http.Request) error { 48 | if req.Method != "GET" { 49 | return httperror.Method{Allowed: []string{"GET"}} 50 | } 51 | 52 | w.Header().Set("Content-Type", "text/html; charset=utf-8") 53 | 54 | _, err := io.WriteString(w, ` 55 | 56 | Go Package Store 57 | 58 | 59 | 60 | 61 | `) 62 | return err 63 | } 64 | 65 | var mockWorkspaceRPs = []workspace.RepoPresentation{ 66 | { 67 | Repo: &gps.Repo{ 68 | Root: (string)("github.com/gopherjs/gopherjs"), 69 | }, 70 | Presentation: &presenter.Presentation{ 71 | HomeURL: (string)("https://github.com/gopherjs/gopherjs"), 72 | ImageURL: (string)("https://avatars.githubusercontent.com/u/6654647?v=3"), 73 | Changes: ([]presenter.Change)([]presenter.Change{ 74 | (presenter.Change)(presenter.Change{ 75 | Message: (string)("improved reflect support for blocking functions"), 76 | URL: (string)("https://github.com/gopherjs/gopherjs/commit/87bf7e405aa3df6df0dcbb9385713f997408d7b9"), 77 | Comments: (presenter.Comments)(presenter.Comments{ 78 | Count: (int)(0), 79 | URL: (string)(""), 80 | }), 81 | }), 82 | (presenter.Change)(presenter.Change{ 83 | Message: (string)("small cleanup"), 84 | URL: (string)("https://github.com/gopherjs/gopherjs/commit/77a838f965881a888416bae38f790f76bb1f64bd"), 85 | Comments: (presenter.Comments)(presenter.Comments{ 86 | Count: (int)(1), 87 | URL: (string)("https://www.example.com/"), 88 | }), 89 | }), 90 | (presenter.Change)(presenter.Change{ 91 | Message: (string)("replaced js.This and js.Arguments by js.MakeFunc"), 92 | URL: (string)("https://github.com/gopherjs/gopherjs/commit/29dd054a0753760fe6e826ded0982a1bf69f702a"), 93 | Comments: (presenter.Comments)(presenter.Comments{ 94 | Count: (int)(0), 95 | URL: (string)(""), 96 | }), 97 | }), 98 | }), 99 | }, 100 | }, 101 | 102 | { 103 | Repo: &gps.Repo{ 104 | Root: (string)("golang.org/x/image"), 105 | }, 106 | Presentation: &presenter.Presentation{ 107 | HomeURL: (string)("http://golang.org/x/image"), 108 | ImageURL: (string)("https://avatars.githubusercontent.com/u/4314092?v=3"), 109 | Changes: ([]presenter.Change)([]presenter.Change{ 110 | (presenter.Change)(presenter.Change{ 111 | Message: (string)("draw: generate code paths for image.Gray sources."), 112 | URL: (string)("https://github.com/golang/image/commit/f510ad81a1256ee96a2870647b74fa144a30c249"), 113 | Comments: (presenter.Comments)(presenter.Comments{ 114 | Count: (int)(0), 115 | URL: (string)(""), 116 | }), 117 | }), 118 | }), 119 | }, 120 | }, 121 | 122 | { 123 | Repo: &gps.Repo{ 124 | Root: (string)("unknown.com/package"), 125 | Local: struct { 126 | RemoteURL string 127 | Revision string 128 | }{Revision: "abcdef0123456789000000000000000000000000"}, 129 | Remote: struct { 130 | RepoURL string 131 | Branch string 132 | Revision string 133 | }{Revision: "d34db33f01010101010101010101010101010101"}, 134 | }, 135 | Presentation: &presenter.Presentation{ 136 | HomeURL: (string)("https://unknown.com/package"), 137 | ImageURL: (string)("https://github.com/images/gravatars/gravatar-user-420.png"), 138 | }, 139 | }, 140 | 141 | { 142 | Repo: &gps.Repo{ 143 | Root: (string)("golang.org/x/foobar"), 144 | }, 145 | Presentation: &presenter.Presentation{ 146 | HomeURL: (string)("http://golang.org/x/foobar"), 147 | ImageURL: (string)("https://avatars.githubusercontent.com/u/4314092?v=3"), 148 | Changes: ([]presenter.Change)(nil), 149 | Error: (error)(errors.New("something went wrong\n\nnew lines are kept - spaces are too.")), 150 | }, 151 | }, 152 | 153 | { 154 | UpdateState: workspace.Updated, 155 | 156 | Repo: &gps.Repo{ 157 | Root: (string)("github.com/influxdb/influxdb"), 158 | }, 159 | Presentation: &presenter.Presentation{ 160 | HomeURL: (string)("https://github.com/influxdb/influxdb"), 161 | ImageURL: (string)("https://avatars.githubusercontent.com/u/5713248?v=3"), 162 | Changes: ([]presenter.Change)([]presenter.Change{ 163 | (presenter.Change)(presenter.Change{ 164 | Message: (string)("Add link to \"How to Report Bugs Effectively\""), 165 | URL: (string)("https://github.com/influxdb/influxdb/commit/6f398c1daf88fe34faede69f4404a334202acae8"), 166 | Comments: (presenter.Comments)(presenter.Comments{ 167 | Count: (int)(0), 168 | URL: (string)(""), 169 | }), 170 | }), 171 | (presenter.Change)(presenter.Change{ 172 | Message: (string)("Update CONTRIBUTING.md"), 173 | URL: (string)("https://github.com/influxdb/influxdb/commit/37fa6056009dd4e84e9852ec50ce747e22375a99"), 174 | Comments: (presenter.Comments)(presenter.Comments{ 175 | Count: (int)(0), 176 | URL: (string)(""), 177 | }), 178 | }), 179 | (presenter.Change)(presenter.Change{ 180 | Message: (string)("Update CONTRIBUTING.md"), 181 | URL: (string)("https://github.com/influxdb/influxdb/commit/87a6a8f15a13c5bf0ac60608edc1be570e7b023e"), 182 | Comments: (presenter.Comments)(presenter.Comments{ 183 | Count: (int)(0), 184 | URL: (string)(""), 185 | }), 186 | }), 187 | (presenter.Change)(presenter.Change{ 188 | Message: (string)("Add note about requiring distro details"), 189 | URL: (string)("https://github.com/influxdb/influxdb/commit/901f91dc9559bebddf9b49607eac4ffd5caa4158"), 190 | Comments: (presenter.Comments)(presenter.Comments{ 191 | Count: (int)(4), 192 | URL: (string)("https://www.example.com/"), 193 | }), 194 | }), 195 | (presenter.Change)(presenter.Change{ 196 | Message: (string)("Correct typo in change log"), 197 | URL: (string)("https://github.com/influxdb/influxdb/commit/8eefdba0d3ef3ab5a408073ae275d495b67c9535"), 198 | Comments: (presenter.Comments)(presenter.Comments{ 199 | Count: (int)(0), 200 | URL: (string)(""), 201 | }), 202 | }), 203 | (presenter.Change)(presenter.Change{ 204 | Message: (string)("Correct markdown for URL"), 205 | URL: (string)("https://github.com/influxdb/influxdb/commit/41688ea6af78d45d051c7f6ac24a6468d36b9fad"), 206 | Comments: (presenter.Comments)(presenter.Comments{ 207 | Count: (int)(0), 208 | URL: (string)(""), 209 | }), 210 | }), 211 | (presenter.Change)(presenter.Change{ 212 | Message: (string)("Update with PR1744"), 213 | URL: (string)("https://github.com/influxdb/influxdb/commit/db09b20d199c973a209e181c9e2f890969bd0b57"), 214 | Comments: (presenter.Comments)(presenter.Comments{ 215 | Count: (int)(0), 216 | URL: (string)(""), 217 | }), 218 | }), 219 | (presenter.Change)(presenter.Change{ 220 | Message: (string)("Merge pull request #1770 from kylezh/dev"), 221 | URL: (string)("https://github.com/influxdb/influxdb/commit/a7c0d71d9ccadde17e7aa5cbba538b4a99670633"), 222 | Comments: (presenter.Comments)(presenter.Comments{ 223 | Count: (int)(0), 224 | URL: (string)(""), 225 | }), 226 | }), 227 | (presenter.Change)(presenter.Change{ 228 | Message: (string)("Merge pull request #1787 from influxdb/measurement_batch_in_series"), 229 | URL: (string)("https://github.com/influxdb/influxdb/commit/40479784e2bd690b9021ec730287c426124230dd"), 230 | Comments: (presenter.Comments)(presenter.Comments{ 231 | Count: (int)(0), 232 | URL: (string)(""), 233 | }), 234 | }), 235 | (presenter.Change)(presenter.Change{ 236 | Message: (string)("Store Measurement commands in batches"), 237 | URL: (string)("https://github.com/influxdb/influxdb/commit/a5749bebfb40239b8fd7b25d2ab1aa234c31c6b2"), 238 | Comments: (presenter.Comments)(presenter.Comments{ 239 | Count: (int)(0), 240 | URL: (string)(""), 241 | }), 242 | }), 243 | (presenter.Change)(presenter.Change{ 244 | Message: (string)("Merge pull request #1786 from influxdb/remove-syslog"), 245 | URL: (string)("https://github.com/influxdb/influxdb/commit/2facd6158620e86262407ae3c4c131860f6953c5"), 246 | Comments: (presenter.Comments)(presenter.Comments{ 247 | Count: (int)(0), 248 | URL: (string)(""), 249 | }), 250 | }), 251 | (presenter.Change)(presenter.Change{ 252 | Message: (string)("Merge pull request #1785 from influxdb/1784"), 253 | URL: (string)("https://github.com/influxdb/influxdb/commit/4a5fdcc9ea3bf6dc178f45758332b871e45b93eb"), 254 | Comments: (presenter.Comments)(presenter.Comments{ 255 | Count: (int)(0), 256 | URL: (string)(""), 257 | }), 258 | }), 259 | (presenter.Change)(presenter.Change{ 260 | Message: (string)("Fix urlgen to work on Ubuntu"), 261 | URL: (string)("https://github.com/influxdb/influxdb/commit/666d09367690627f9c3212c1c25c566416c645da"), 262 | Comments: (presenter.Comments)(presenter.Comments{ 263 | Count: (int)(0), 264 | URL: (string)(""), 265 | }), 266 | }), 267 | (presenter.Change)(presenter.Change{ 268 | Message: (string)("Remove unused syslog.go"), 269 | URL: (string)("https://github.com/influxdb/influxdb/commit/06bfd9c496becacff404e6768e7c0fd8ce9603c2"), 270 | Comments: (presenter.Comments)(presenter.Comments{ 271 | Count: (int)(0), 272 | URL: (string)(""), 273 | }), 274 | }), 275 | (presenter.Change)(presenter.Change{ 276 | Message: (string)("Fix timezone abbreviation."), 277 | URL: (string)("https://github.com/influxdb/influxdb/commit/06eac99c230dcc24bee9c3e1c1ef01725ce017ad"), 278 | Comments: (presenter.Comments)(presenter.Comments{ 279 | Count: (int)(0), 280 | URL: (string)(""), 281 | }), 282 | }), 283 | (presenter.Change)(presenter.Change{ 284 | Message: (string)("Merge pull request #1782 from influxdb/more_contains_unit_tests"), 285 | URL: (string)("https://github.com/influxdb/influxdb/commit/fffbcf3fbe953e03e69ac1d22c142ecd6b3aba3b"), 286 | Comments: (presenter.Comments)(presenter.Comments{ 287 | Count: (int)(0), 288 | URL: (string)(""), 289 | }), 290 | }), 291 | (presenter.Change)(presenter.Change{ 292 | Message: (string)("More shard \"contains\" unit tests"), 293 | URL: (string)("https://github.com/influxdb/influxdb/commit/ec93341f3fddd294f404fd1469fb651d4ba16e4c"), 294 | Comments: (presenter.Comments)(presenter.Comments{ 295 | Count: (int)(0), 296 | URL: (string)(""), 297 | }), 298 | }), 299 | (presenter.Change)(presenter.Change{ 300 | Message: (string)("Update changelog for rc6 release"), 301 | URL: (string)("https://github.com/influxdb/influxdb/commit/65b4d1a060883a5901bd7c40492a3345d2eabc77"), 302 | Comments: (presenter.Comments)(presenter.Comments{ 303 | Count: (int)(0), 304 | URL: (string)(""), 305 | }), 306 | }), 307 | (presenter.Change)(presenter.Change{ 308 | Message: (string)("Merge pull request #1781 from influxdb/single_shard_data"), 309 | URL: (string)("https://github.com/influxdb/influxdb/commit/5889b12832b2e43424951c92089db03f31df1078"), 310 | Comments: (presenter.Comments)(presenter.Comments{ 311 | Count: (int)(0), 312 | URL: (string)(""), 313 | }), 314 | }), 315 | (presenter.Change)(presenter.Change{ 316 | Message: (string)("Refactor shard group time bound checking"), 317 | URL: (string)("https://github.com/influxdb/influxdb/commit/05d630bfb8041362c89249e3e6fabe6261cecc66"), 318 | Comments: (presenter.Comments)(presenter.Comments{ 319 | Count: (int)(0), 320 | URL: (string)(""), 321 | }), 322 | }), 323 | (presenter.Change)(presenter.Change{ 324 | Message: (string)("Fix error when alter retention policy"), 325 | URL: (string)("https://github.com/influxdb/influxdb/commit/9f8639ded8778a270cc99cf2d9ee1a09f635d67d"), 326 | Comments: (presenter.Comments)(presenter.Comments{ 327 | Count: (int)(0), 328 | URL: (string)(""), 329 | }), 330 | }), 331 | }), 332 | }, 333 | }, 334 | } 335 | -------------------------------------------------------------------------------- /cmd/Go-Package-Store/errorhandler.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "os" 8 | 9 | "github.com/shurcooL/httperror" 10 | ) 11 | 12 | // errorHandler factors error handling out of the HTTP handler. 13 | type errorHandler func(w http.ResponseWriter, req *http.Request) error 14 | 15 | func (h errorHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { 16 | err := h(w, req) 17 | if err == nil { 18 | // Do nothing. 19 | return 20 | } 21 | if err, ok := httperror.IsMethod(err); ok { 22 | httperror.HandleMethod(w, err) 23 | return 24 | } 25 | if err, ok := httperror.IsRedirect(err); ok { 26 | http.Redirect(w, req, err.URL, http.StatusSeeOther) 27 | return 28 | } 29 | if err, ok := httperror.IsBadRequest(err); ok { 30 | httperror.HandleBadRequest(w, err) 31 | return 32 | } 33 | if err, ok := httperror.IsHTTP(err); ok { 34 | code := err.Code 35 | error := fmt.Sprintf("%d %s\n\n%v", code, http.StatusText(code), err) 36 | http.Error(w, error, code) 37 | return 38 | } 39 | if os.IsNotExist(err) { 40 | log.Println(err) 41 | http.Error(w, "404 Not Found\n\n"+err.Error(), http.StatusNotFound) 42 | return 43 | } 44 | if os.IsPermission(err) { 45 | log.Println(err) 46 | http.Error(w, "403 Forbidden\n\n"+err.Error(), http.StatusForbidden) 47 | return 48 | } 49 | 50 | log.Println(err) 51 | http.Error(w, "500 Internal Server Error\n\n"+err.Error(), http.StatusInternalServerError) 52 | } 53 | -------------------------------------------------------------------------------- /cmd/Go-Package-Store/godep.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | ) 7 | 8 | // Godeps describes what a package needs to be rebuilt reproducibly. 9 | // It's the same information stored in file Godeps. 10 | type Godeps struct { 11 | ImportPath string 12 | GoVersion string 13 | Packages []string `json:",omitempty"` // Arguments to save, if any. 14 | Deps []Dependency 15 | } 16 | 17 | // A Dependency is a specific revision of a package. 18 | type Dependency struct { 19 | ImportPath string 20 | Comment string `json:",omitempty"` // Description of commit, if present. 21 | Rev string // VCS-specific commit ID. 22 | } 23 | 24 | // readGodeps reads a Godeps.json file at path. 25 | func readGodeps(path string) (Godeps, error) { 26 | f, err := os.Open(path) 27 | if err != nil { 28 | return Godeps{}, err 29 | } 30 | defer f.Close() 31 | var g Godeps 32 | err = json.NewDecoder(f).Decode(&g) 33 | return g, err 34 | } 35 | -------------------------------------------------------------------------------- /cmd/Go-Package-Store/index.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "html/template" 5 | "io" 6 | "net/http" 7 | 8 | "github.com/shurcooL/httperror" 9 | ) 10 | 11 | var headHTML = template.Must(template.New("").Parse(` 12 | 13 | Go Package Store 14 | 15 | 16 | 17 | {{if .Production}}{{end}} 27 | 28 | `)) 29 | 30 | func indexHandler(w http.ResponseWriter, req *http.Request) error { 31 | if req.Method != "GET" { 32 | return httperror.Method{Allowed: []string{"GET"}} 33 | } 34 | 35 | w.Header().Set("Content-Type", "text/html; charset=utf-8") 36 | 37 | data := struct{ Production bool }{production} 38 | err := headHTML.Execute(w, data) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | err = renderInitialBody(w) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | _, err = io.WriteString(w, ``) 49 | return err 50 | } 51 | 52 | func renderInitialBody(w io.Writer) error { 53 | _, err := io.WriteString(w, `
Updates

Checking for updates...

`) 54 | return err 55 | } 56 | -------------------------------------------------------------------------------- /cmd/Go-Package-Store/main.go: -------------------------------------------------------------------------------- 1 | // Go Package Store displays updates for the Go packages in your GOPATH. 2 | package main 3 | 4 | import ( 5 | "bufio" 6 | "flag" 7 | "fmt" 8 | "log" 9 | "net" 10 | "net/http" 11 | "os" 12 | "os/exec" 13 | "path/filepath" 14 | 15 | "github.com/gregjones/httpcache" 16 | "github.com/gregjones/httpcache/diskcache" 17 | "github.com/shurcooL/Go-Package-Store" 18 | "github.com/shurcooL/Go-Package-Store/assets" 19 | "github.com/shurcooL/Go-Package-Store/presenter/github" 20 | "github.com/shurcooL/Go-Package-Store/presenter/gitiles" 21 | "github.com/shurcooL/Go-Package-Store/updater" 22 | "github.com/shurcooL/Go-Package-Store/workspace" 23 | "github.com/shurcooL/go/browser" 24 | "github.com/shurcooL/go/ospath" 25 | "github.com/shurcooL/httpgzip" 26 | "golang.org/x/oauth2" 27 | ) 28 | 29 | var ( 30 | httpFlag = flag.String("http", "localhost:7043", "Listen for HTTP connections on this address.") 31 | stdinFlag = flag.Bool("stdin", false, "Read the list of newline separated Go packages from stdin.") 32 | depFlag = flag.String("dep", "", "Determine the list of Go packages from the specified Gopkg.toml file.") 33 | godepsFlag = flag.String("godeps", "", "Read the list of Go packages from the specified Godeps.json file.") 34 | gitSubrepoFlag = flag.String("git-subrepo", "", "Look for Go packages vendored using git-subrepo in the specified vendor directory.") 35 | ) 36 | 37 | func usage() { 38 | fmt.Fprint(os.Stderr, "Usage: Go-Package-Store [flags]\n") 39 | fmt.Fprint(os.Stderr, " [newline separated packages] | Go-Package-Store -stdin [flags]\n") 40 | flag.PrintDefaults() 41 | fmt.Fprint(os.Stderr, ` 42 | GitHub Access Token: 43 | To display updates for private repositories on GitHub, or when 44 | you've exceeded the unauthenticated rate limit, you can provide 45 | a GitHub access token for Go Package Store to use via the 46 | GO_PACKAGE_STORE_GITHUB_TOKEN environment variable. 47 | 48 | Examples: 49 | # Check for updates for all Go packages in GOPATH. 50 | Go-Package-Store 51 | 52 | # Show updates for all golang.org/x/... packages. 53 | go list golang.org/x/... | Go-Package-Store -stdin 54 | 55 | # Show updates for all dependencies within Gopkg.toml constraints. 56 | Go-Package-Store -dep=/path/to/repo/Gopkg.toml 57 | 58 | # Show updates for all Go packages vendored using git-subrepo 59 | # in the specified vendor directory. 60 | Go-Package-Store -git-subrepo=/path/to/repo/vendor 61 | `) 62 | } 63 | 64 | func main() { 65 | flag.Usage = usage 66 | flag.Parse() 67 | 68 | log.SetFlags(0) 69 | 70 | c.pipeline = workspace.NewPipeline(wd) 71 | registerPresenters(c.pipeline) 72 | c.updater = populatePipelineAndCreateUpdater(c.pipeline) 73 | if c.updater != nil { 74 | updateWorker := newUpdateWorker(c.updater) 75 | updateWorker.Start() 76 | http.Handle("/api/update", errorHandler(updateWorker.Handler)) 77 | } 78 | http.Handle("/api/updates", errorHandler(updatesHandler)) 79 | http.Handle("/updates", errorHandler(indexHandler)) 80 | assetsFS := httpgzip.FileServer(assets.Assets, httpgzip.FileServerOptions{ServeError: httpgzip.Detailed}) 81 | http.Handle("/assets/", assetsFS) 82 | http.Handle("/frontend.js", assetsFS) 83 | fontsFS := httpgzip.FileServer(assets.Fonts, httpgzip.FileServerOptions{ServeError: httpgzip.Detailed}) 84 | http.Handle("/assets/fonts/", http.StripPrefix("/assets/fonts", fontsFS)) 85 | 86 | // Start listening first. 87 | listener, err := net.Listen("tcp", *httpFlag) 88 | if err != nil { 89 | log.Fatalln(fmt.Errorf("failed to listen on %q: %v", *httpFlag, err)) 90 | } 91 | 92 | if production { 93 | // Open a browser tab and navigate to the main page. 94 | go browser.Open("http://" + *httpFlag + "/updates") 95 | } 96 | 97 | fmt.Println("Go Package Store server is running at http://" + *httpFlag + "/updates.") 98 | 99 | err = http.Serve(listener, nil) 100 | if err != nil { 101 | log.Fatalln(err) 102 | } 103 | } 104 | 105 | // c is a global context. 106 | var c = struct { 107 | pipeline *workspace.Pipeline 108 | 109 | // updater is set based on the source of Go packages. If nil, it means 110 | // we don't have support to update Go packages from the current source. 111 | // It's used to update repos in the backend, and if set to nil, to disable 112 | // the frontend UI for updating packages. 113 | updater gps.Updater 114 | }{} 115 | 116 | func registerPresenters(pipeline *workspace.Pipeline) { 117 | // If we can have access to a cache directory on this system, use it for 118 | // caching HTTP requests of presenters. 119 | cacheDir, err := ospath.CacheDir("github.com/shurcooL/Go-Package-Store") 120 | if err != nil { 121 | log.Println("skipping persistent on-disk caching, because unable to acquire a cache dir:", err) 122 | cacheDir = "" 123 | } 124 | 125 | // Register GitHub presenter. 126 | { 127 | var transport http.RoundTripper 128 | 129 | // Optionally, perform GitHub API authentication with provided token. 130 | if token := os.Getenv("GO_PACKAGE_STORE_GITHUB_TOKEN"); token != "" { 131 | transport = &oauth2.Transport{ 132 | Source: oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}), 133 | } 134 | } 135 | 136 | if cacheDir != "" { 137 | transport = &httpcache.Transport{ 138 | Transport: transport, 139 | Cache: diskcache.New(filepath.Join(cacheDir, "github-presenter")), 140 | MarkCachedResponses: true, 141 | } 142 | } 143 | 144 | pipeline.RegisterPresenter(github.NewPresenter(&http.Client{Transport: transport})) 145 | } 146 | 147 | // Register Gitiles presenter. 148 | { 149 | var transport http.RoundTripper 150 | 151 | if cacheDir != "" { 152 | transport = &httpcache.Transport{ 153 | Transport: transport, 154 | Cache: diskcache.New(filepath.Join(cacheDir, "gitiles-presenter")), 155 | MarkCachedResponses: true, 156 | } 157 | } 158 | 159 | pipeline.RegisterPresenter(gitiles.NewPresenter(&http.Client{Transport: transport})) 160 | } 161 | } 162 | 163 | func populatePipelineAndCreateUpdater(pipeline *workspace.Pipeline) gps.Updater { 164 | switch { 165 | case !production: 166 | fmt.Println("Using no real packages (hit /mock.html or /component.html endpoint for mocks).") 167 | pipeline.Done() 168 | return updater.Mock{} 169 | default: 170 | fmt.Println("Using all Go packages in GOPATH.") 171 | go func() { // This needs to happen in the background because sending input will be blocked on processing. 172 | forEachRepository(func(r workspace.LocalRepo) { 173 | pipeline.AddRepository(r) 174 | }) 175 | pipeline.Done() 176 | }() 177 | return updater.Gopath{} 178 | case *stdinFlag: 179 | fmt.Println("Reading the list of newline separated Go packages from stdin.") 180 | go func() { // This needs to happen in the background because sending input will be blocked on processing. 181 | br := bufio.NewReader(os.Stdin) 182 | for line, err := br.ReadString('\n'); err == nil; line, err = br.ReadString('\n') { 183 | importPath := line[:len(line)-1] // Trim last newline. 184 | pipeline.AddImportPath(importPath) 185 | } 186 | pipeline.Done() 187 | }() 188 | return updater.Gopath{} 189 | case *depFlag != "": 190 | // Check dep binary exists in PATH. 191 | if _, err := exec.LookPath("dep"); err != nil { 192 | log.Fatalln(fmt.Errorf("dep binary is required, but not available: %v", err)) 193 | } 194 | fmt.Println("Determining the list of Go packages from Gopkg.toml file:", *depFlag) 195 | dir, err := depDir(*depFlag) 196 | if err != nil { 197 | log.Fatalln(err) 198 | } 199 | go func() { 200 | // Running dep status is pretty slow, because it tries to figure out remote updates 201 | // that are within Gopkg.toml constraints. So run it in background. 202 | dependencies, err := runDepStatus(dir) 203 | if err != nil { 204 | log.Println("failed to run dep status on Gopkg.toml file:", err) 205 | dependencies = nil 206 | } 207 | 208 | // This needs to happen in the background because sending input will be blocked on processing. 209 | for _, d := range dependencies { 210 | pipeline.AddRevisionLatest(d.ProjectRoot, d.Revision, d.Latest) 211 | } 212 | pipeline.Done() 213 | }() 214 | return updater.Dep{Dir: dir} 215 | case *godepsFlag != "": 216 | fmt.Println("Reading the list of Go packages from Godeps.json file:", *godepsFlag) 217 | g, err := readGodeps(*godepsFlag) 218 | if err != nil { 219 | log.Fatalln("failed to read Godeps.json file", err) 220 | } 221 | go func() { // This needs to happen in the background because sending input will be blocked on processing. 222 | for _, dependency := range g.Deps { 223 | pipeline.AddRevision(dependency.ImportPath, dependency.Rev) 224 | } 225 | pipeline.Done() 226 | }() 227 | return nil 228 | case *gitSubrepoFlag != "": 229 | if _, err := exec.LookPath("git"); err != nil { 230 | log.Fatalln(fmt.Errorf("git binary is required, but not available: %v", err)) 231 | } 232 | fmt.Println("Using Go packages vendored using git-subrepo in the specified vendor directory.") 233 | go func() { // This needs to happen in the background because sending input will be blocked on processing. 234 | err := forEachGitSubrepo(*gitSubrepoFlag, func(s workspace.Subrepo) { 235 | pipeline.AddSubrepo(s) 236 | }) 237 | if err != nil { 238 | log.Println("warning: there was problem iterating over subrepos:", err) 239 | } 240 | pipeline.Done() 241 | }() 242 | return nil // An updater for this can easily be added by anyone who uses this style of vendoring. 243 | } 244 | } 245 | 246 | // wd is current working directory at process start. 247 | var wd = func() string { 248 | wd, err := os.Getwd() 249 | if err != nil { 250 | log.Fatalln("os.Getwd:", err) 251 | } 252 | return wd 253 | }() 254 | -------------------------------------------------------------------------------- /cmd/Go-Package-Store/prod.go: -------------------------------------------------------------------------------- 1 | //go:build !dev 2 | 3 | package main 4 | 5 | const production = true 6 | -------------------------------------------------------------------------------- /cmd/Go-Package-Store/update.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/shurcooL/Go-Package-Store" 9 | "github.com/shurcooL/Go-Package-Store/workspace" 10 | "github.com/shurcooL/httperror" 11 | ) 12 | 13 | func newUpdateWorker(updater gps.Updater) updateWorker { 14 | return updateWorker{ 15 | updater: updater, 16 | updateRequests: make(chan updateRequest), 17 | } 18 | } 19 | 20 | type updateWorker struct { 21 | updater gps.Updater 22 | updateRequests chan updateRequest 23 | } 24 | 25 | type updateRequest struct { 26 | Root string 27 | ResponseChan chan error 28 | } 29 | 30 | // Handler for update endpoint. 31 | func (u updateWorker) Handler(w http.ResponseWriter, req *http.Request) error { 32 | if req.Method != "POST" { 33 | return httperror.Method{Allowed: []string{"POST"}} 34 | } 35 | 36 | ur := updateRequest{ 37 | Root: req.PostFormValue("RepoRoot"), 38 | ResponseChan: make(chan error), 39 | } 40 | u.updateRequests <- ur 41 | 42 | err := <-ur.ResponseChan 43 | // TODO: Display error in frontend. 44 | if err != nil { 45 | log.Println("update error:", err) 46 | } 47 | 48 | return nil 49 | } 50 | 51 | // Start performing sequential updates of Go packages. It does not update 52 | // in parallel to avoid race conditions. 53 | func (u updateWorker) Start() { 54 | go u.run() 55 | } 56 | 57 | func (u updateWorker) run() { 58 | for ur := range u.updateRequests { 59 | c.pipeline.Packages.Lock() 60 | rp, ok := c.pipeline.Packages.ByRoot[ur.Root] 61 | c.pipeline.Packages.Unlock() 62 | if !ok { 63 | ur.ResponseChan <- fmt.Errorf("root %q not found", ur.Root) 64 | continue 65 | } 66 | if rp.UpdateState != workspace.Available { 67 | ur.ResponseChan <- fmt.Errorf("root %q not available for update: %v", ur.Root, rp.UpdateState) 68 | continue 69 | } 70 | 71 | // Mark repo as updating. 72 | c.pipeline.Packages.Lock() 73 | c.pipeline.Packages.ByRoot[ur.Root].UpdateState = workspace.Updating 74 | c.pipeline.Packages.Unlock() 75 | 76 | updateError := u.updater.Update(rp.Repo) 77 | 78 | if updateError == nil { 79 | c.pipeline.Packages.Lock() 80 | for i, rp := range c.pipeline.Packages.Active { 81 | if rp.Repo.Root == ur.Root { 82 | // Remove from active. 83 | copy(c.pipeline.Packages.Active[i:], c.pipeline.Packages.Active[i+1:]) 84 | c.pipeline.Packages.Active = c.pipeline.Packages.Active[:len(c.pipeline.Packages.Active)-1] 85 | 86 | // Mark repo as updated. 87 | rp.UpdateState = workspace.Updated 88 | 89 | // Append to history. 90 | c.pipeline.Packages.History = append(c.pipeline.Packages.History, rp) 91 | 92 | break 93 | } 94 | } 95 | c.pipeline.Packages.Unlock() 96 | } 97 | 98 | ur.ResponseChan <- updateError 99 | fmt.Println("\nDone.") 100 | } 101 | } 102 | 103 | // TODO: Currently lots of logic (for manipulating repo presentations as they 104 | // get updated, etc.) haphazardly present both in backend and frontend, 105 | // need to think about that. Probably want to unify workspace.RepoPresentation 106 | // and component.RepoPresentation types, maybe. Try it. 107 | -------------------------------------------------------------------------------- /cmd/Go-Package-Store/updates.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/shurcooL/Go-Package-Store/frontend/model" 9 | "github.com/shurcooL/httperror" 10 | ) 11 | 12 | func updatesHandler(w http.ResponseWriter, req *http.Request) error { 13 | if req.Method != "GET" { 14 | return httperror.Method{Allowed: []string{"GET"}} 15 | } 16 | w.Header().Set("Content-Type", "application/json") 17 | w.Header().Set("X-Content-Type-Options", "nosniff") 18 | jw := json.NewEncoder(w) 19 | jw.SetIndent("", "\t") 20 | flusher, ok := w.(http.Flusher) 21 | if !ok { 22 | return fmt.Errorf("ResponseWriter %v is not a Flusher", w) 23 | } 24 | for rp := range c.pipeline.RepoPresentations() { 25 | var cs []model.Change 26 | for _, c := range rp.Presentation.Changes { 27 | cs = append(cs, model.Change{ 28 | Message: c.Message, 29 | URL: c.URL, 30 | Comments: model.Comments{Count: c.Comments.Count, URL: c.Comments.URL}, 31 | }) 32 | } 33 | repoPresentation := model.RepoPresentation{ 34 | RepoRoot: rp.Repo.Root, 35 | ImportPathPattern: rp.Repo.ImportPathPattern(), 36 | LocalRevision: rp.Repo.Local.Revision, 37 | RemoteRevision: rp.Repo.Remote.Revision, 38 | HomeURL: rp.Presentation.HomeURL, 39 | ImageURL: rp.Presentation.ImageURL, 40 | Changes: cs, 41 | UpdateState: model.UpdateState(rp.UpdateState), 42 | UpdateSupported: c.updater != nil, 43 | } 44 | if err := rp.Presentation.Error; err != nil { 45 | repoPresentation.Error = err.Error() 46 | } 47 | err := jw.Encode(repoPresentation) 48 | if err != nil { 49 | return fmt.Errorf("error encoding repoPresentation: %v", err) 50 | } 51 | flusher.Flush() 52 | } 53 | return nil 54 | } 55 | -------------------------------------------------------------------------------- /component/doc.go: -------------------------------------------------------------------------------- 1 | // Package component contains Vecty HTML components used by Go Package Store. 2 | package component 3 | -------------------------------------------------------------------------------- /component/header.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gopherjs/gopherjs/js" 7 | "github.com/gopherjs/vecty" 8 | "github.com/gopherjs/vecty/elem" 9 | "github.com/gopherjs/vecty/event" 10 | "github.com/gopherjs/vecty/prop" 11 | "github.com/gopherjs/vecty/style" 12 | "github.com/shurcooL/Go-Package-Store/frontend/model" 13 | "golang.org/x/net/html/atom" 14 | ) 15 | 16 | // Header is a component that displays the header with tabs on top. 17 | type Header struct { 18 | vecty.Core 19 | } 20 | 21 | // Render renders the component. 22 | func (*Header) Render() vecty.ComponentOrHTML { 23 | return elem.Header( 24 | vecty.Markup(style.Width("100%"), vecty.Style("text-align", "center")), 25 | elem.Span( 26 | vecty.Markup(vecty.Style("padding", string(style.Px(15))), vecty.Style("display", "inline-block")), 27 | vecty.Text("Updates"), 28 | ), 29 | ) 30 | } 31 | 32 | // updatesHeader combines checkingForUpdates, noUpdates and updatesHeading 33 | // into one high level component. 34 | type updatesHeader struct { 35 | Active []*model.RepoPresentation 36 | CheckingUpdates bool 37 | } 38 | 39 | func (u updatesHeader) Render() []vecty.MarkupOrChild { 40 | var ns []vecty.MarkupOrChild 41 | switch { 42 | case u.CheckingUpdates: 43 | // Show "Checking for updates..." while still checking. 44 | ns = append(ns, heading(elem.Heading2, "Checking for updates...")) 45 | case !u.CheckingUpdates && len(u.Active) == 0: 46 | // Show "No Updates Available" if we're done checking and there are no remaining updates. 47 | ns = append(ns, 48 | elem.Heading2( 49 | vecty.Markup(vecty.Style("text-align", "center"), vecty.Style("margin-bottom", "2px")), 50 | vecty.Text("No Updates Available"), 51 | ), 52 | elem.Heading4( 53 | vecty.Markup(vecty.Style("text-align", "center"), vecty.Style("margin-top", "2px"), vecty.Style("font-weight", "normal")), 54 | vecty.Text("All your Go packages are up to date"), 55 | ), 56 | ) 57 | } 58 | // Show number of updates available and Update All button. 59 | available, updating, supported := u.status() 60 | ns = append(ns, &updatesHeading{ 61 | Available: available, 62 | Updating: updating, 63 | UpdateSupported: supported, // TODO: Fetch this value from backend once. 64 | }) 65 | return ns 66 | } 67 | 68 | // status reports available, updating, supported updates in u.Active. 69 | func (u updatesHeader) status() (available uint, updating bool, supported bool) { 70 | for _, rp := range u.Active { 71 | switch rp.UpdateState { 72 | case model.Available: 73 | available++ 74 | supported = rp.UpdateSupported 75 | case model.Updating: 76 | updating = true 77 | } 78 | } 79 | return available, updating, supported 80 | } 81 | 82 | // updatesHeading is a heading that displays number of updates available, 83 | // whether updates are installing, and an Update All button. 84 | type updatesHeading struct { 85 | vecty.Core 86 | Available uint `vecty:"prop"` 87 | Updating bool `vecty:"prop"` 88 | 89 | // TODO: Find a place for this. 90 | UpdateSupported bool `vecty:"prop"` 91 | } 92 | 93 | func (u *updatesHeading) Render() vecty.ComponentOrHTML { 94 | if u.Available == 0 && !u.Updating { 95 | return nil 96 | } 97 | var status string 98 | if u.Available > 0 { 99 | status = fmt.Sprintf("%d Updates Available", u.Available) 100 | } 101 | if u.Updating { 102 | if status != "" { 103 | status += ", " 104 | } 105 | status += "Installing Updates..." 106 | } 107 | return elem.Heading4( 108 | vecty.Markup(vecty.Style("text-align", "left")), 109 | vecty.Text(status), 110 | elem.Span( 111 | vecty.Markup(vecty.Style("float", "right")), 112 | u.updateAllButton(), 113 | ), 114 | ) 115 | } 116 | 117 | func (u *updatesHeading) updateAllButton() *vecty.HTML { 118 | if !u.UpdateSupported { 119 | return elem.Span( 120 | vecty.Markup( 121 | style.Color("gray"), vecty.Style("cursor", "default"), 122 | vecty.Property(atom.Title.String(), "Updating repos is not currently supported for this source of repos."), 123 | ), 124 | vecty.Text("Update All"), 125 | ) 126 | } 127 | switch { 128 | case u.Available > 0: 129 | return elem.Anchor( 130 | vecty.Markup( 131 | prop.Href("/api/update-all"), // TODO: Should it be a separate endpoint or what? 132 | event.Click(func(e *vecty.Event) { 133 | // TODO. 134 | fmt.Println("UpdateAll()") 135 | js.Global.Get("UpdateAll").Invoke() // TODO: Do this via action? 136 | }).PreventDefault(), 137 | ), 138 | vecty.Text("Update All"), 139 | ) 140 | case u.Available == 0: 141 | return elem.Span( 142 | vecty.Markup(style.Color("gray"), vecty.Style("cursor", "default")), 143 | vecty.Text("Update All"), 144 | ) 145 | default: 146 | panic("unreachable") 147 | } 148 | } 149 | 150 | func heading(heading func(markup ...vecty.MarkupOrChild) *vecty.HTML, text string) *vecty.HTML { 151 | return heading( 152 | vecty.Markup(vecty.Style("text-align", "center")), 153 | vecty.Text(text), 154 | ) 155 | } 156 | -------------------------------------------------------------------------------- /component/presentation.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/gopherjs/gopherjs/js" 9 | "github.com/gopherjs/vecty" 10 | "github.com/gopherjs/vecty/elem" 11 | "github.com/gopherjs/vecty/event" 12 | "github.com/gopherjs/vecty/prop" 13 | "github.com/gopherjs/vecty/style" 14 | "github.com/shurcooL/Go-Package-Store/frontend/model" 15 | "github.com/shurcooL/octicon" 16 | "golang.org/x/net/html" 17 | "golang.org/x/net/html/atom" 18 | ) 19 | 20 | // RepoPresentation is a component for presenting a repository update. 21 | // 22 | // TODO: Dedup with workspace.RepoPresentation. Maybe. 23 | type RepoPresentation struct { 24 | vecty.Core 25 | *model.RepoPresentation `vecty:"prop"` 26 | } 27 | 28 | // Render renders the component. 29 | func (p *RepoPresentation) Render() vecty.ComponentOrHTML { 30 | return elem.Div( 31 | vecty.Markup( 32 | vecty.Class("list-entry", "go-package-update"), 33 | vecty.Property(atom.Id.String(), p.RepoRoot), 34 | vecty.Style("position", "relative"), 35 | ), 36 | elem.Div( 37 | vecty.Markup(vecty.Class("list-entry-header")), 38 | elem.Span( 39 | vecty.Markup(vecty.Property(atom.Title.String(), p.ImportPathPattern)), 40 | p.importPathPattern(), 41 | ), 42 | elem.Div( 43 | vecty.Markup(vecty.Style("float", "right")), 44 | p.updateState(), 45 | ), 46 | ), 47 | elem.Div( 48 | vecty.Markup(vecty.Class("list-entry-body")), 49 | elem.Image( 50 | vecty.Markup( 51 | vecty.Style("float", "left"), vecty.Style("border-radius", string(style.Px(4))), 52 | vecty.Property(atom.Src.String(), p.ImageURL), 53 | vecty.Property(atom.Width.String(), "36"), 54 | vecty.Property(atom.Height.String(), "36"), 55 | ), 56 | ), 57 | elem.Div( 58 | p.presentationChangesAndError()..., 59 | ), 60 | elem.Div( 61 | vecty.Markup(vecty.Style("clear", "both")), 62 | ), 63 | ), 64 | ) 65 | } 66 | 67 | // TODO: Turn this into a maybeLink, etc. 68 | func (p *RepoPresentation) importPathPattern() *vecty.HTML { 69 | switch p.HomeURL { 70 | default: 71 | return elem.Anchor( 72 | vecty.Markup( 73 | prop.Href(p.HomeURL), 74 | // TODO: Add rel="noopener", see https://dev.to/ben/the-targetblank-vulnerability-by-example. 75 | vecty.Property(atom.Target.String(), "_blank"), 76 | ), 77 | elem.Strong(vecty.Text(p.ImportPathPattern)), 78 | ) 79 | case "": 80 | return elem.Strong(vecty.Text(p.ImportPathPattern)) 81 | } 82 | } 83 | 84 | func (p *RepoPresentation) updateState() *vecty.HTML { 85 | if !p.UpdateSupported { 86 | return elem.Span( 87 | vecty.Markup( 88 | style.Color("gray"), vecty.Style("cursor", "default"), 89 | vecty.Property(atom.Title.String(), "Updating repos is not currently supported for this source of repos."), 90 | ), 91 | vecty.Text("Update"), 92 | ) 93 | } 94 | switch p.UpdateState { 95 | case model.Available: 96 | return elem.Anchor( 97 | vecty.Markup( 98 | prop.Href("/api/update"), 99 | event.Click(func(e *vecty.Event) { 100 | // TODO. 101 | fmt.Printf("UpdateRepository(%q)\n", p.RepoRoot) 102 | // TODO: Modifying underlying model is bad because Restore can't tell if something changed... 103 | p.UpdateState = model.Updating // TODO: Do this via action. 104 | started := time.Now() 105 | vecty.Rerender(p) 106 | fmt.Println("render RepoPresentation:", time.Since(started)) 107 | js.Global.Get("UpdateRepository").Invoke(p.RepoRoot) 108 | 109 | }).PreventDefault(), 110 | ), 111 | vecty.Text("Update"), 112 | ) 113 | case model.Updating: 114 | return elem.Span( 115 | vecty.Markup(style.Color("gray"), vecty.Style("cursor", "default")), 116 | vecty.Text("Updating..."), 117 | ) 118 | case model.Updated: 119 | // TODO. 120 | return nil 121 | default: 122 | panic("unreachable") 123 | } 124 | } 125 | 126 | func (p *RepoPresentation) presentationChangesAndError() []vecty.MarkupOrChild { 127 | return []vecty.MarkupOrChild{ 128 | vecty.Markup(vecty.Style("word-break", "break-word")), 129 | &PresentationChanges{ 130 | RepoPresentation: p.RepoPresentation, 131 | }, 132 | vecty.If(p.Error != "", 133 | elem.Paragraph( 134 | vecty.Markup(vecty.Class("presentation-error")), 135 | elem.Strong(vecty.Text("Error:")), 136 | vecty.Text(" "), 137 | vecty.Text(p.Error), 138 | ), 139 | ), 140 | } 141 | } 142 | 143 | // PresentationChanges is a component containing changes within an update. 144 | type PresentationChanges struct { 145 | vecty.Core 146 | //Changes []*Change 147 | //LocalRevision string // Only needed if len(Changes) == 0. 148 | //RemoteRevision string // Only needed if len(Changes) == 0. 149 | *model.RepoPresentation `vecty:"prop"` // Only uses Changes, and if len(Changes) == 0, then LocalRevision and RemoteRevision. 150 | } 151 | 152 | // Restore is called when the component should restore itself against a 153 | // previous instance of a component. The previous component may be nil or 154 | // of a different type than this Restorer itself, thus a type assertion 155 | // should be used. 156 | // 157 | // If skip = true is returned, restoration of this component's body is 158 | // skipped. That is, the component is not rerendered. If the component can 159 | // prove when Restore is called that the HTML rendered by Component.Render 160 | // would not change, true should be returned. 161 | //func (p *PresentationChanges) Restore(prev vecty.Component) (skip bool) { 162 | // //fmt.Print("Restore: ") 163 | // old, ok := prev.(*PresentationChanges) 164 | // if !ok { 165 | // //fmt.Println("not *PresentationChanges") 166 | // return false 167 | // } 168 | // _ = old //fmt.Println("old.RepoPresentation == p.RepoPresentation:", old.RepoPresentation == p.RepoPresentation) 169 | // return false 170 | // //return old.RepoPresentation == p.RepoPresentation 171 | //} 172 | 173 | // Render renders the component. 174 | func (p *PresentationChanges) Render() vecty.ComponentOrHTML { 175 | //fmt.Println("PresentationChanges.Render()") 176 | switch len(p.Changes) { 177 | default: 178 | ns := []vecty.MarkupOrChild{ 179 | vecty.Markup(vecty.Class("changes-list")), 180 | } 181 | //for _, c := range p.Changes { 182 | // ns = append(ns, &Change{ 183 | // Change: c, 184 | // }) 185 | //} 186 | for i := range p.Changes { // TODO: Consider changing model.RepoPresentation.Changes type to []*Change to simplify this. 187 | ns = append(ns, &Change{ 188 | Change: &p.Changes[i], 189 | }) 190 | } 191 | return elem.UnorderedList(ns...) 192 | case 0: 193 | return elem.Div( 194 | vecty.Markup(vecty.Class("changes-list")), 195 | vecty.Text("unknown changes"), 196 | vecty.If(p.LocalRevision != "", 197 | vecty.Text(" from "), 198 | &CommitID{ID: p.LocalRevision}, 199 | ), 200 | vecty.If(p.RemoteRevision != "", 201 | vecty.Text(" to "), 202 | &CommitID{ID: p.RemoteRevision}, 203 | ), 204 | ) 205 | } 206 | } 207 | 208 | // Change is a component for a single commit message. 209 | type Change struct { 210 | vecty.Core 211 | *model.Change `vecty:"prop"` 212 | } 213 | 214 | // Render renders the component. 215 | func (c *Change) Render() vecty.ComponentOrHTML { 216 | return elem.ListItem( 217 | vecty.Text(c.Message), 218 | elem.Span( 219 | vecty.Markup(vecty.Class("highlight-on-hover")), 220 | elem.Anchor( 221 | vecty.Markup( 222 | prop.Href(c.URL), 223 | // TODO: Add rel="noopener", see https://dev.to/ben/the-targetblank-vulnerability-by-example. 224 | vecty.Property(atom.Target.String(), "_blank"), 225 | vecty.Property(atom.Title.String(), "Commit"), 226 | vecty.UnsafeHTML(octiconGitCommit), 227 | ), 228 | ), 229 | ), 230 | elem.Span( 231 | vecty.Markup(vecty.Style("float", "right"), vecty.Style("margin-right", string(style.Px(6)))), 232 | &Comments{Comments: &c.Comments}, 233 | ), 234 | ) 235 | } 236 | 237 | // Comments is a component for displaying a change discussion. 238 | // 239 | // TODO: Consider inlining this into Change component, we'll see. 240 | type Comments struct { 241 | vecty.Core 242 | *model.Comments `vecty:"prop"` 243 | } 244 | 245 | // Render renders the component. 246 | func (c *Comments) Render() vecty.ComponentOrHTML { 247 | if c.Count == 0 { 248 | return nil 249 | } 250 | return elem.Anchor( 251 | vecty.Markup( 252 | prop.Href(c.URL), 253 | // TODO: Add rel="noopener", see https://dev.to/ben/the-targetblank-vulnerability-by-example. 254 | vecty.Property(atom.Target.String(), "_blank"), 255 | vecty.Style("color", "gray"), 256 | vecty.Property(atom.Title.String(), fmt.Sprintf("%d comments", c.Count)), 257 | ), 258 | elem.Span( 259 | vecty.Markup( 260 | style.Color("currentColor"), vecty.Style("margin-right", string(style.Px(4))), 261 | vecty.UnsafeHTML(octiconComment), 262 | ), 263 | ), 264 | vecty.Text(fmt.Sprint(c.Count)), 265 | ) 266 | } 267 | 268 | // CommitID is a component that displays a short commit ID, with the full one available in tooltip. 269 | type CommitID struct { 270 | vecty.Core 271 | ID string `vecty:"prop"` 272 | } 273 | 274 | // Render renders the component. 275 | func (c *CommitID) Render() vecty.ComponentOrHTML { 276 | return elem.Abbreviation( 277 | vecty.Markup(vecty.Property(atom.Title.String(), c.ID)), 278 | elem.Code( 279 | vecty.Markup(vecty.Class("commitID")), 280 | vecty.Text(c.commitID()), 281 | ), 282 | ) 283 | } 284 | 285 | func (c *CommitID) commitID() string { return c.ID[:8] } 286 | 287 | var ( 288 | octiconGitCommit = render(octicon.GitCommit) 289 | octiconComment = render(octicon.Comment) 290 | ) 291 | 292 | func render(icon func() *html.Node) string { 293 | var buf bytes.Buffer 294 | err := html.Render(&buf, icon()) 295 | if err != nil { 296 | panic(err) 297 | } 298 | return buf.String() 299 | } 300 | -------------------------------------------------------------------------------- /component/updates.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "github.com/gopherjs/vecty" 5 | "github.com/gopherjs/vecty/elem" 6 | "github.com/shurcooL/Go-Package-Store/frontend/model" 7 | ) 8 | 9 | // UpdatesContent returns the entire content of updates tab. 10 | func UpdatesContent(active, history []*model.RepoPresentation, checkingUpdates bool) []vecty.MarkupOrChild { 11 | return []vecty.MarkupOrChild{ 12 | &Header{}, 13 | elem.Div( 14 | vecty.Markup(vecty.Class("center-max-width")), 15 | elem.Div( 16 | updatesContent(active, history, checkingUpdates)..., 17 | ), 18 | ), 19 | } 20 | } 21 | 22 | func updatesContent(active, history []*model.RepoPresentation, checkingUpdates bool) []vecty.MarkupOrChild { 23 | var content = []vecty.MarkupOrChild{ 24 | vecty.Markup(vecty.Class("content")), 25 | } 26 | 27 | // Updates header. 28 | content = append(content, 29 | updatesHeader{ 30 | Active: active, 31 | CheckingUpdates: checkingUpdates, 32 | }.Render()..., 33 | ) 34 | 35 | // Active updates. 36 | for _, rp := range active { 37 | content = append(content, &RepoPresentation{ 38 | RepoPresentation: rp, 39 | }) 40 | } 41 | 42 | // History with "Recently Installed Updates" heading, if any. 43 | if len(history) > 0 { 44 | content = append(content, elem.Heading3( 45 | vecty.Markup(vecty.Style("text-align", "center"), vecty.Style("margin-top", "80px")), 46 | vecty.Text("Recently Installed Updates"), 47 | )) 48 | 49 | for i := len(history) - 1; i >= 0; i-- { 50 | content = append(content, &RepoPresentation{ 51 | RepoPresentation: history[i], 52 | }) 53 | } 54 | } 55 | 56 | return content 57 | } 58 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package gps defines domain types for Go Package Store. 2 | // 3 | // Specifically, it provides a definition of a repository. 4 | // It defines an interface for a repository presentation, 5 | // and a presenter type that is capable of creating presentations. 6 | // Finally, it defines an interface for a repositry updater. 7 | package gps 8 | -------------------------------------------------------------------------------- /frontend/action/action.go: -------------------------------------------------------------------------------- 1 | // Package action defines actions that can be applied to the data model in store. 2 | package action 3 | 4 | import "github.com/shurcooL/Go-Package-Store/frontend/model" 5 | 6 | // Action represents any of the supported actions. 7 | type Action interface{} 8 | 9 | // Response represents any of the supported responses. 10 | type Response interface{} 11 | 12 | // AppendRP is an action for appending a single update to the end. 13 | type AppendRP struct { 14 | RP *model.RepoPresentation 15 | } 16 | 17 | // SetUpdating is an action for setting an update with RepoRoot to updating state. 18 | type SetUpdating struct { 19 | RepoRoot string 20 | } 21 | 22 | // SetUpdatingAll is an action for setting all available updates to updating state. 23 | type SetUpdatingAll struct{} 24 | 25 | // SetUpdatingAllResponse is the response from SetUpdatingAll action, 26 | // listing RepoRoot of all updates that were affected. 27 | type SetUpdatingAllResponse struct { 28 | RepoRoots []string 29 | } 30 | 31 | // SetUpdated is an action for setting an update with RepoRoot to updated state. 32 | type SetUpdated struct { 33 | RepoRoot string 34 | } 35 | 36 | // DoneCheckingUpdates is an action for when the update checking process is completed. 37 | type DoneCheckingUpdates struct{} 38 | -------------------------------------------------------------------------------- /frontend/main.go: -------------------------------------------------------------------------------- 1 | // Command frontend runs on frontend of Go Package Store. 2 | package main 3 | 4 | import ( 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "log" 10 | "net/http" 11 | "net/url" 12 | "time" 13 | 14 | "github.com/gopherjs/gopherjs/js" 15 | "github.com/gopherjs/vecty" 16 | "github.com/gopherjs/vecty/elem" 17 | gpscomponent "github.com/shurcooL/Go-Package-Store/component" 18 | "github.com/shurcooL/Go-Package-Store/frontend/action" 19 | "github.com/shurcooL/Go-Package-Store/frontend/model" 20 | "github.com/shurcooL/Go-Package-Store/frontend/store" 21 | "honnef.co/go/js/dom" 22 | ) 23 | 24 | var document = dom.GetWindow().Document().(dom.HTMLDocument) 25 | 26 | func main() { 27 | js.Global.Set("UpdateRepository", UpdateRepository) 28 | js.Global.Set("UpdateAll", UpdateAll) 29 | 30 | switch readyState := document.ReadyState(); readyState { 31 | case "loading": 32 | document.AddEventListener("DOMContentLoaded", false, func(dom.Event) { 33 | go run() 34 | }) 35 | case "interactive", "complete": 36 | run() 37 | default: 38 | panic(fmt.Errorf("internal error: unexpected document.ReadyState value: %v", readyState)) 39 | } 40 | } 41 | 42 | func run() { 43 | // Initial frontend render. 44 | err := vecty.RenderInto("body", body) 45 | if err != nil { 46 | panic(fmt.Errorf("internal error: unexpected error from vecty.RenderInto: %v", err)) 47 | } 48 | 49 | // Start the scheduler loop. 50 | go scheduler() 51 | 52 | // Start streaming repo presentations from the backend. 53 | err = stream() 54 | if err != nil { 55 | log.Println(err) 56 | } 57 | } 58 | 59 | // stream streams the list of repo presentations from the backend, 60 | // and appends them to the store as they arrive. 61 | func stream() error { 62 | started := time.Now() 63 | defer func() { fmt.Println("stream:", time.Since(started)) }() 64 | 65 | resp, err := http.Get("/api/updates") 66 | if err != nil { 67 | return err 68 | } 69 | defer resp.Body.Close() 70 | dec := json.NewDecoder(resp.Body) 71 | for { 72 | var rp model.RepoPresentation 73 | err := dec.Decode(&rp) 74 | if err == io.EOF { 75 | break 76 | } else if err != nil { 77 | return err 78 | } 79 | 80 | apply(&action.AppendRP{RP: &rp}) 81 | } 82 | 83 | apply(&action.DoneCheckingUpdates{}) 84 | return nil 85 | } 86 | 87 | // scheduler runs a loop that is responsible for 88 | // applying actions to the store as they're made available, 89 | // and rendering the body after processing new actions. 90 | // 91 | // It coalesces temporally adjacent actions, processing 92 | // them in batches without performing rendering in between. 93 | func scheduler() { 94 | var renderCh <-chan time.Time 95 | 96 | for { 97 | select { 98 | case a := <-actionCh: 99 | resp := store.Apply(a.Action) 100 | a.RespCh <- resp 101 | 102 | renderCh = time.After(10 * time.Millisecond) 103 | case <-renderCh: 104 | renderBody() 105 | renderCh = nil 106 | } 107 | } 108 | } 109 | 110 | // TODO: Consider using time.NewTimer and Timer.Stop instead of time.After. 111 | 112 | var actionCh = make(chan actionAndResponse) // TODO: Consider/try buffered channel of size 10. 113 | 114 | type actionAndResponse struct { 115 | Action action.Action 116 | RespCh chan<- action.Response 117 | } 118 | 119 | // apply applies the given action to the store, 120 | // and returns the response. 121 | func apply(a action.Action) action.Response { 122 | respCh := make(chan action.Response) 123 | actionCh <- actionAndResponse{Action: a, RespCh: respCh} 124 | resp := <-respCh 125 | return resp 126 | } 127 | 128 | func renderBody() { 129 | started := time.Now() 130 | defer func() { fmt.Println("renderBody:", time.Since(started)) }() 131 | 132 | vecty.Rerender(body) 133 | } 134 | 135 | var body = &UpdatesBody{} 136 | 137 | // UpdatesBody is the entire body of the updates tab. 138 | type UpdatesBody struct { 139 | vecty.Core 140 | } 141 | 142 | // Render renders the component. 143 | func (b *UpdatesBody) Render() vecty.ComponentOrHTML { 144 | return elem.Body( 145 | gpscomponent.UpdatesContent( 146 | store.Active(), 147 | store.History(), 148 | store.CheckingUpdates(), 149 | )..., 150 | ) 151 | } 152 | 153 | // UpdateAll marks all available updates as updating, and performs updates in background in sequence. 154 | func UpdateAll() { 155 | go func() { 156 | started := time.Now() 157 | defer func() { fmt.Println("update all:", time.Since(started)) }() 158 | 159 | resp := apply(&action.SetUpdatingAll{}).(*action.SetUpdatingAllResponse) 160 | 161 | for _, root := range resp.RepoRoots { 162 | update(root) 163 | } 164 | }() 165 | } 166 | 167 | // UpdateRepository updates specified repository. 168 | // root is the import path corresponding to the root of the repository. 169 | func UpdateRepository(root string) { 170 | go func() { 171 | apply(&action.SetUpdating{RepoRoot: root}) 172 | // No need to render body because the component updated itself internally. 173 | // TODO: Improve and centralize this when-and-what-to-rerender logic, maybe? 174 | 175 | update(root) 176 | }() 177 | } 178 | 179 | // update updates specified repository. 180 | // root is the import path corresponding to the root of the repository. 181 | func update(root string) { 182 | started := time.Now() 183 | defer func() { fmt.Println("update:", time.Since(started)) }() 184 | 185 | resp, err := http.PostForm("/api/update", url.Values{"RepoRoot": {root}}) 186 | if err != nil { 187 | log.Println(err) 188 | return 189 | } 190 | defer resp.Body.Close() 191 | 192 | // TODO: Check response for success or not, etc. 193 | // This is a great chance to display update errors in frontend! 194 | _, err = io.Copy(ioutil.Discard, resp.Body) 195 | if err != nil { 196 | log.Println(err) 197 | return 198 | } 199 | 200 | apply(&action.SetUpdated{RepoRoot: root}) 201 | } 202 | -------------------------------------------------------------------------------- /frontend/model/model.go: -------------------------------------------------------------------------------- 1 | // Package model is a frontend data model for updates. 2 | package model 3 | 4 | // RepoPresentation represents a repository update presentation. 5 | // 6 | // TODO: Dedup with workspace.RepoPresentation. Maybe. 7 | type RepoPresentation struct { 8 | RepoRoot string 9 | ImportPathPattern string 10 | LocalRevision string 11 | RemoteRevision string 12 | HomeURL string 13 | ImageURL string 14 | Changes []Change // TODO: Consider []*Change. 15 | Error string 16 | 17 | UpdateState UpdateState 18 | 19 | // TODO: Find a place for this. 20 | UpdateSupported bool 21 | } 22 | 23 | // UpdateState represents the state of an update. 24 | type UpdateState uint8 25 | 26 | const ( 27 | // Available represents an available update. 28 | Available UpdateState = iota 29 | 30 | // Updating represents an update in progress. 31 | Updating 32 | 33 | // Updated represents a completed update. 34 | Updated 35 | ) 36 | 37 | // Change represents a single commit message. 38 | type Change struct { 39 | Message string // Commit message of this change. 40 | URL string // URL of this change. 41 | Comments Comments // Comments on this change. 42 | } 43 | 44 | // Comments represents a change discussion. 45 | // 46 | // TODO: Consider inlining this into Change, we'll see. 47 | type Comments struct { 48 | Count int 49 | URL string 50 | } 51 | -------------------------------------------------------------------------------- /frontend/store/store.go: -------------------------------------------------------------------------------- 1 | // Package store is a store for updates. 2 | // Its contents can only be modified by appling actions. 3 | package store 4 | 5 | import ( 6 | "fmt" 7 | 8 | "github.com/shurcooL/Go-Package-Store/frontend/action" 9 | "github.com/shurcooL/Go-Package-Store/frontend/model" 10 | ) 11 | 12 | var ( 13 | active []*model.RepoPresentation // Latest at the end. 14 | history []*model.RepoPresentation // Latest at the end. 15 | checkingUpdates = true 16 | ) 17 | 18 | // Active returns the active repo presentations in store. 19 | // Most recently added ones are last. 20 | func Active() []*model.RepoPresentation { return active } 21 | 22 | // History returns the historical repo presentations in store. 23 | // Most recently added ones are last. 24 | func History() []*model.RepoPresentation { return history } 25 | 26 | // CheckingUpdates reports whether the process of checking for updates is still running. 27 | func CheckingUpdates() bool { return checkingUpdates } 28 | 29 | // Apply applies action a to the store. 30 | func Apply(a action.Action) action.Response { 31 | switch a := a.(type) { 32 | case *action.AppendRP: 33 | switch a.RP.UpdateState { 34 | case model.Available, model.Updating: 35 | active = append(active, a.RP) 36 | case model.Updated: 37 | history = append(history, a.RP) 38 | } 39 | return nil 40 | 41 | case *action.SetUpdating: 42 | for _, rp := range active { 43 | if rp.RepoRoot == a.RepoRoot { 44 | rp.UpdateState = model.Updating 45 | return nil 46 | } 47 | } 48 | panic(fmt.Errorf("RepoRoot %q was not found in store", a.RepoRoot)) 49 | 50 | case *action.SetUpdatingAll: 51 | var repoRoots []string 52 | for _, rp := range active { 53 | if rp.UpdateState == model.Available { 54 | repoRoots = append(repoRoots, rp.RepoRoot) 55 | rp.UpdateState = model.Updating 56 | } 57 | } 58 | // TODO: Instead of response, look into async-action-creators: 59 | // - http://redux.js.org/docs/advanced/AsyncActions.html#async-action-creators 60 | // - https://gophers.slack.com/archives/D02LBN6UW/p1488335043280451 61 | return &action.SetUpdatingAllResponse{RepoRoots: repoRoots} 62 | 63 | case *action.SetUpdated: 64 | for i, rp := range active { 65 | if rp.RepoRoot == a.RepoRoot { 66 | // Remove from active. 67 | copy(active[i:], active[i+1:]) 68 | active = active[:len(active)-1] 69 | 70 | // Set UpdateState. 71 | rp.UpdateState = model.Updated 72 | 73 | // Append to history. 74 | history = append(history, rp) 75 | 76 | return nil 77 | } 78 | } 79 | panic(fmt.Errorf("RepoRoot %q was not found in store", a.RepoRoot)) 80 | 81 | case *action.DoneCheckingUpdates: 82 | checkingUpdates = false 83 | return nil 84 | 85 | default: 86 | panic(fmt.Errorf("%v (type %T) is not a valid action", a, a)) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/shurcooL/Go-Package-Store 2 | 3 | go 1.19 4 | -------------------------------------------------------------------------------- /presenter/github/github.go: -------------------------------------------------------------------------------- 1 | // Package github provides a GitHub API-powered presenter. It supports repositories that are on github.com. 2 | package github 3 | 4 | import ( 5 | "context" 6 | "fmt" 7 | "net/http" 8 | "strings" 9 | 10 | "github.com/dustin/go-humanize" 11 | "github.com/google/go-github/github" 12 | "github.com/shurcooL/Go-Package-Store/presenter" 13 | ) 14 | 15 | // NewPresenter returns a GitHub API-powered presenter. 16 | // httpClient is the HTTP client to be used by the presenter for accessing the GitHub API. 17 | // If httpClient is nil, then http.DefaultClient is used. 18 | func NewPresenter(httpClient *http.Client) presenter.Presenter { 19 | gh := github.NewClient(httpClient) 20 | gh.UserAgent = "github.com/shurcooL/Go-Package-Store/presenter/github" 21 | 22 | return func(ctx context.Context, repo presenter.Repo) *presenter.Presentation { 23 | switch { 24 | // Import path begins with "github.com/". 25 | case strings.HasPrefix(repo.Root, "github.com/"): 26 | elems := strings.Split(repo.Root, "/") 27 | if len(elems) != 3 { 28 | return nil 29 | } 30 | return presentGitHubRepo(ctx, gh, repo, elems[1], elems[2]) 31 | // gopkg.in package. 32 | case strings.HasPrefix(repo.Root, "gopkg.in/"): 33 | githubOwner, githubRepo, err := gopkgInImportPathToGitHub(repo.Root) 34 | if err != nil { 35 | return nil 36 | } 37 | return presentGitHubRepo(ctx, gh, repo, githubOwner, githubRepo) 38 | // Underlying GitHub remote. 39 | case strings.HasPrefix(repo.RepoURL, "https://github.com/"): 40 | elems := strings.Split(strings.TrimSuffix(repo.RepoURL[len("https://"):], ".git"), "/") 41 | if len(elems) != 3 { 42 | return nil 43 | } 44 | return presentGitHubRepo(ctx, gh, repo, elems[1], elems[2]) 45 | // Go repo remote has a GitHub mirror repo. 46 | case strings.HasPrefix(repo.RepoURL, "https://go.googlesource.com/"): 47 | repoName := repo.RepoURL[len("https://go.googlesource.com/"):] 48 | return presentGitHubRepo(ctx, gh, repo, "golang", repoName) 49 | // upspin.io. 50 | case strings.HasPrefix(repo.RepoURL, "https://upspin.googlesource.com/"): 51 | repoName := repo.RepoURL[len("https://upspin.googlesource.com/"):] 52 | return presentGitHubRepo(ctx, gh, repo, "upspin", repoName) 53 | default: 54 | return nil 55 | } 56 | } 57 | } 58 | 59 | func presentGitHubRepo(ctx context.Context, gh *github.Client, repo presenter.Repo, ghOwner, ghRepo string) *presenter.Presentation { 60 | p := &presenter.Presentation{ 61 | HomeURL: "https://" + repo.Root, 62 | ImageURL: "https://github.com/images/gravatars/gravatar-user-420.png", // Default fallback. 63 | } 64 | 65 | // This might take a while. 66 | if cc, _, err := gh.Repositories.CompareCommits(ctx, ghOwner, ghRepo, repo.LocalRevision, repo.RemoteRevision); err == nil { 67 | p.Changes = extractChanges(cc) 68 | } else if rateLimitErr, ok := err.(*github.RateLimitError); ok { 69 | setFirstError(p, rateLimitError{rateLimitErr}) 70 | } else { 71 | setFirstError(p, fmt.Errorf("gh.Repositories.CompareCommits: %v", err)) 72 | } 73 | 74 | // Use the repo owner avatar image. 75 | if repo, _, err := gh.Repositories.Get(ctx, ghOwner, ghRepo); err == nil && repo.Owner != nil && repo.Owner.AvatarURL != nil { 76 | p.ImageURL = *repo.Owner.AvatarURL 77 | } else if rateLimitErr, ok := err.(*github.RateLimitError); ok { 78 | setFirstError(p, rateLimitError{rateLimitErr}) 79 | } else { 80 | setFirstError(p, fmt.Errorf("gh.Repositories.Get: %v", err)) 81 | } 82 | 83 | return p 84 | } 85 | 86 | func extractChanges(cc *github.CommitsComparison) []presenter.Change { 87 | var cs []presenter.Change 88 | for i := range cc.Commits { 89 | c := cc.Commits[len(cc.Commits)-1-i] // Reverse order. 90 | change := presenter.Change{ 91 | Message: firstParagraph(*c.Commit.Message), 92 | URL: *c.HTMLURL, 93 | } 94 | if commentCount := c.Commit.CommentCount; commentCount != nil && *commentCount > 0 { 95 | change.Comments.Count = *commentCount 96 | change.Comments.URL = *c.HTMLURL + "#comments" 97 | } 98 | cs = append(cs, change) 99 | } 100 | return cs 101 | } 102 | 103 | // firstParagraph returns the first paragraph of text s. 104 | func firstParagraph(s string) string { 105 | i := strings.Index(s, "\n\n") 106 | if i == -1 { 107 | return s 108 | } 109 | return s[:i] 110 | } 111 | 112 | // rateLimitError is an error presentation wrapper for consistent display of *github.RateLimitError. 113 | type rateLimitError struct { 114 | err *github.RateLimitError 115 | } 116 | 117 | func (r rateLimitError) Error() string { 118 | return fmt.Sprintf("GitHub API rate limit exceeded; it will be reset in %v (but you can set GO_PACKAGE_STORE_GITHUB_TOKEN env var for higher rate limit)", humanize.Time(r.err.Rate.Reset.Time)) 119 | } 120 | 121 | // setFirstError sets error if it's the first one. It does nothing otherwise. 122 | func setFirstError(p *presenter.Presentation, err error) { 123 | if p.Error != nil { 124 | return 125 | } 126 | p.Error = err 127 | } 128 | -------------------------------------------------------------------------------- /presenter/github/gopkgin.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | func gopkgInImportPathToGitHub(gopkgInImportPath string) (githubOwner, githubRepo string, err error) { 9 | afterPrefix := gopkgInImportPath[len("gopkg.in/"):] 10 | importPathElements0 := strings.Split(afterPrefix, ".") 11 | if len(importPathElements0) != 2 { 12 | return "", "", fmt.Errorf("len(importPathElements0) != 2: %v", importPathElements0) 13 | } 14 | importPathElements1 := strings.Split(importPathElements0[0], "/") 15 | importPath := "github.com/" 16 | if len(importPathElements1) == 1 { // gopkg.in/pkg.v3 -> github.com/go-pkg/pkg 17 | importPath += "go-" + importPathElements1[0] + "/" + importPathElements1[0] 18 | } else if len(importPathElements1) == 2 { // gopkg.in/user/pkg.v3 -> github.com/user/pkg 19 | importPath += importPathElements1[0] + "/" + importPathElements1[1] 20 | } else { 21 | return "", "", fmt.Errorf("len(importPathElements1) != 1 nor 2: %v", importPathElements1) 22 | } 23 | importPathElements := strings.Split(importPath, "/") 24 | return importPathElements[1], importPathElements[2], nil 25 | } 26 | -------------------------------------------------------------------------------- /presenter/gitiles/gitiles.go: -------------------------------------------------------------------------------- 1 | // Package gitiles provides a Gitiles API-powered presenter. It supports repositories that are on code.googlesource.com. 2 | package gitiles 3 | 4 | import ( 5 | "bytes" 6 | "context" 7 | "encoding/json" 8 | "fmt" 9 | "io" 10 | "net/http" 11 | "strings" 12 | 13 | "github.com/shurcooL/Go-Package-Store/presenter" 14 | ) 15 | 16 | // NewPresenter returns a Gitiles API-powered presenter. 17 | // httpClient is the HTTP client to be used by the presenter for accessing the Gitiles API. 18 | // If httpClient is nil, then http.DefaultClient is used. 19 | func NewPresenter(httpClient *http.Client) presenter.Presenter { 20 | return func(ctx context.Context, repo presenter.Repo) *presenter.Presentation { 21 | switch { 22 | case strings.HasPrefix(repo.RepoURL, "https://code.googlesource.com/"): 23 | return presentGitilesRepo(ctx, httpClient, repo) 24 | default: 25 | return nil 26 | } 27 | } 28 | } 29 | 30 | func presentGitilesRepo(ctx context.Context, client *http.Client, repo presenter.Repo) *presenter.Presentation { 31 | // This might take a while. 32 | log, err := fetchLog(ctx, client, repo.RepoURL+"/+log?format=JSON") 33 | if err != nil { 34 | return &presenter.Presentation{Error: err} 35 | } 36 | 37 | return &presenter.Presentation{ 38 | HomeURL: "https://" + repo.Root, 39 | ImageURL: "https://ssl.gstatic.com/codesite/ph/images/defaultlogo.png", 40 | Changes: extractChanges(repo, log), 41 | } 42 | } 43 | 44 | // fetchLog fetches a Gitiles log at a given url, using client. 45 | func fetchLog(ctx context.Context, client *http.Client, url string) (log, error) { 46 | req, err := http.NewRequest("GET", url, nil) 47 | if err != nil { 48 | return log{}, err 49 | } 50 | req.Header.Set("User-Agent", "github.com/shurcooL/Go-Package-Store/presenter/gitiles") 51 | resp, err := client.Do(req.WithContext(ctx)) 52 | if err != nil { 53 | return log{}, err 54 | } 55 | defer resp.Body.Close() 56 | if resp.StatusCode != http.StatusOK { 57 | return log{}, fmt.Errorf("non-200 status code: %v", resp.StatusCode) 58 | } 59 | 60 | // Consume and verify header. 61 | buf := make([]byte, len(header)) 62 | if _, err := io.ReadFull(resp.Body, buf); err != nil { 63 | return log{}, err 64 | } 65 | if !bytes.Equal(buf, []byte(header)) { 66 | return log{}, fmt.Errorf("header %q doesn't match expected %q", string(buf), header) 67 | } 68 | 69 | var l log 70 | err = json.NewDecoder(resp.Body).Decode(&l) 71 | return l, err 72 | } 73 | 74 | // Note, that JSON format has a ")]}'" line at the top, to prevent cross-site scripting. 75 | // When parsing, assert that the first line has ")]}'", strip it, and parse the rest of 76 | // JSON normally. 77 | // 78 | // Source: https://www.chromium.org/developers/change-logs. 79 | const header = `)]}'` + "\n" 80 | 81 | type log struct { 82 | Log []commit `json:"log"` 83 | Next string `json:"next"` 84 | } 85 | 86 | type commit struct { 87 | Commit string `json:"commit"` 88 | Message string `json:"message"` 89 | } 90 | 91 | func extractChanges(repo presenter.Repo, l log) []presenter.Change { 92 | // Verify/find Repo.RemoteRevision. 93 | log := l.Log 94 | for len(log) > 0 && log[0].Commit != repo.RemoteRevision { 95 | log = log[1:] 96 | } 97 | 98 | var cs []presenter.Change 99 | for _, commit := range log { 100 | if commit.Commit == repo.LocalRevision { 101 | break 102 | } 103 | cs = append(cs, presenter.Change{ 104 | Message: firstParagraph(commit.Message), 105 | URL: repo.RepoURL + "/+/" + commit.Commit + "%5e%21", 106 | }) 107 | } 108 | return cs 109 | } 110 | 111 | // firstParagraph returns the first paragraph of text s. 112 | func firstParagraph(s string) string { 113 | i := strings.Index(s, "\n\n") 114 | if i == -1 { 115 | return s 116 | } 117 | return s[:i] 118 | } 119 | -------------------------------------------------------------------------------- /presenter/gitiles/gitiles_test.go: -------------------------------------------------------------------------------- 1 | package gitiles 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "os" 7 | "path/filepath" 8 | "testing" 9 | ) 10 | 11 | func TestFetchLog(t *testing.T) { 12 | logFile, err := os.Open(filepath.Join("testdata", "+log.json")) 13 | if err != nil { 14 | panic(err) 15 | } 16 | // logFile will be closed by defer resp.Body.Close(). 17 | client := &http.Client{ 18 | Transport: roundTripperFunc(func(*http.Request) (*http.Response, error) { 19 | return &http.Response{ 20 | StatusCode: http.StatusOK, 21 | Body: logFile, 22 | }, nil 23 | }), 24 | } 25 | 26 | log, err := fetchLog(context.Background(), client, "") 27 | if err != nil { 28 | t.Fatalf("fetchLog: %v", err) 29 | } 30 | 31 | if got, want := len(log.Log), 100; got != want { 32 | t.Fatalf("got %q, want %q", got, want) 33 | } 34 | if got, want := log.Log[0], (commit{ 35 | Commit: "7a1b48a03240285fcdb91f7890f647cd90358f84", 36 | Message: "google-api-go-client: update all APIs\n\nChange-Id: If682f3b0bcf992351f82763cd76561c7c30466a5\nReviewed-on: https://code-review.googlesource.com/5051\nReviewed-by: Brad Fitzpatrick \u003cbradfitz@golang.org\u003e\n", 37 | }); got != want { 38 | t.Errorf("got %q, want %q", got, want) 39 | } 40 | if got, want := log.Next, "0bacdc65dfd3dae28a124e21ecebb6f3f3c22087"; got != want { 41 | t.Errorf("got %q, want %q", got, want) 42 | } 43 | } 44 | 45 | type roundTripperFunc func(req *http.Request) (*http.Response, error) 46 | 47 | func (f roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) { return f(req) } 48 | -------------------------------------------------------------------------------- /presenter/gitiles/testdata/+log.json: -------------------------------------------------------------------------------- 1 | )]}' 2 | { 3 | "log": [ 4 | { 5 | "commit": "7a1b48a03240285fcdb91f7890f647cd90358f84", 6 | "tree": "12329a8bb567f1aca059f01da73983a99f807e5a", 7 | "parents": [ 8 | "63ade871fd3aec1225809d496e81ec91ab76ea29" 9 | ], 10 | "author": { 11 | "name": "Chris Broadfoot", 12 | "email": "cbro@golang.org", 13 | "time": "Fri Jun 24 17:27:31 2016 -0700" 14 | }, 15 | "committer": { 16 | "name": "Chris Broadfoot", 17 | "email": "cbro@google.com", 18 | "time": "Mon Jun 27 18:00:52 2016 +0000" 19 | }, 20 | "message": "google-api-go-client: update all APIs\n\nChange-Id: If682f3b0bcf992351f82763cd76561c7c30466a5\nReviewed-on: https://code-review.googlesource.com/5051\nReviewed-by: Brad Fitzpatrick \u003cbradfitz@golang.org\u003e\n" 21 | }, 22 | { 23 | "commit": "63ade871fd3aec1225809d496e81ec91ab76ea29", 24 | "tree": "9499b72a98f2c17f448795c087279b5e2da8e90c", 25 | "parents": [ 26 | "aa89374d6f4a7f9ab85dcb3b01c95d2d3bd7f10a" 27 | ], 28 | "author": { 29 | "name": "Michael McGreevy", 30 | "email": "mcgreevy@golang.org", 31 | "time": "Tue May 31 16:42:46 2016 +1000" 32 | }, 33 | "committer": { 34 | "name": "Michael McGreevy", 35 | "email": "mcgreevy@golang.org", 36 | "time": "Wed Jun 01 00:40:28 2016 +0000" 37 | }, 38 | "message": "generator: support data wrapper for responses.\n\nThe change is in the generator. This should result in no change in\nfunctionality to any generated file except\ntranslate/v2/translate-gen.go.\n\nFixes #79.\n\nChange-Id: I3c22e230e1b5cc88ebe037cbf4704430232eb70e\nReviewed-on: https://code-review.googlesource.com/4901\nReviewed-by: Michael McGreevy \u003cmcgreevy@golang.org\u003e\n" 39 | }, 40 | { 41 | "commit": "aa89374d6f4a7f9ab85dcb3b01c95d2d3bd7f10a", 42 | "tree": "b6f94235935913d26e5ec6361c188ec3f09d2fb3", 43 | "parents": [ 44 | "7059a7f3d456870ff1e8424a6b1f62abe5f95f36" 45 | ], 46 | "author": { 47 | "name": "Glenn Lewis", 48 | "email": "gmlewis@google.com", 49 | "time": "Mon May 23 13:06:21 2016 -0700" 50 | }, 51 | "committer": { 52 | "name": "Michael McGreevy", 53 | "email": "mcgreevy@golang.org", 54 | "time": "Mon May 23 23:40:04 2016 +0000" 55 | }, 56 | "message": "google-api-go-client: update all APIs\n\nRequested by Classroom API team.\n\nChange-Id: I0f1ad39757688a9c577f6606880fd25d17c6beb7\nReviewed-on: https://code-review.googlesource.com/4860\nReviewed-by: Michael McGreevy \u003cmcgreevy@golang.org\u003e\n" 57 | }, 58 | { 59 | "commit": "7059a7f3d456870ff1e8424a6b1f62abe5f95f36", 60 | "tree": "fc8b4035f565a289f62fca310f5e82afdc068f27", 61 | "parents": [ 62 | "b34a26664e9b96e9d4aab8a6e8175ea07af5b8b6" 63 | ], 64 | "author": { 65 | "name": "Michael McGreevy", 66 | "email": "mcgreevy@golang.org", 67 | "time": "Thu May 12 15:45:26 2016 +1000" 68 | }, 69 | "committer": { 70 | "name": "Michael McGreevy", 71 | "email": "mcgreevy@golang.org", 72 | "time": "Wed May 18 01:00:10 2016 +0000" 73 | }, 74 | "message": "generator: simplify doRequest.\n\ndoRequest has organically grown into a bit of a mess.\n\nThis is a limited cleanup which reduces the number of code paths a bit.\n\nChange-Id: Ida634aa1be9a13c761fc7df173726b9bc4cb3e0c\nReviewed-on: https://code-review.googlesource.com/4760\nReviewed-by: Dave Day \u003cdjd@golang.org\u003e\n" 75 | }, 76 | { 77 | "commit": "b34a26664e9b96e9d4aab8a6e8175ea07af5b8b6", 78 | "tree": "1ab84e166fcdcfd71c2bcf73ff77d60780f8ea79", 79 | "parents": [ 80 | "4300f6b0c8a7f09e521dd0af2cee27e28846e037" 81 | ], 82 | "author": { 83 | "name": "Michael McGreevy", 84 | "email": "mcgreevy@golang.org", 85 | "time": "Tue May 17 15:00:45 2016 +1000" 86 | }, 87 | "committer": { 88 | "name": "Brad Fitzpatrick", 89 | "email": "bradfitz@golang.org", 90 | "time": "Tue May 17 14:33:37 2016 +0000" 91 | }, 92 | "message": "google-api-go-client: update all APIs.\n\nChange-Id: Idc616709138d8152420af1435966db844748ea75\nReviewed-on: https://code-review.googlesource.com/4782\nReviewed-by: Brad Fitzpatrick \u003cbradfitz@golang.org\u003e\n" 93 | }, 94 | { 95 | "commit": "4300f6b0c8a7f09e521dd0af2cee27e28846e037", 96 | "tree": "8b2a93973fd95ba85f8658e4286e827a46300d14", 97 | "parents": [ 98 | "68d2045ae9f51df061629c98988f210ed6bb0127" 99 | ], 100 | "author": { 101 | "name": "Glenn Lewis", 102 | "email": "gmlewis@google.com", 103 | "time": "Thu May 12 12:12:15 2016 -0700" 104 | }, 105 | "committer": { 106 | "name": "Glenn Lewis", 107 | "email": "gmlewis@google.com", 108 | "time": "Thu May 12 21:25:20 2016 +0000" 109 | }, 110 | "message": "google-api-go-client: update all APIs.\n\nRequested by the sqladmin API team.\n\nNo old APIs have been deleted.\n\nChange-Id: I2cfedf6b60aab0fa43db94f591b881f9502bccb8\nReviewed-on: https://code-review.googlesource.com/4751\nReviewed-by: Brad Fitzpatrick \u003cbradfitz@golang.org\u003e\n" 111 | }, 112 | { 113 | "commit": "68d2045ae9f51df061629c98988f210ed6bb0127", 114 | "tree": "23c96084ebe83ffe3aeb7260f17921cacb893ef9", 115 | "parents": [ 116 | "f9a4669e07732c84854dce1f5c451c22427228fb" 117 | ], 118 | "author": { 119 | "name": "Michael McGreevy", 120 | "email": "mcgreevy@golang.org", 121 | "time": "Wed May 11 11:39:49 2016 +1000" 122 | }, 123 | "committer": { 124 | "name": "Michael McGreevy", 125 | "email": "mcgreevy@golang.org", 126 | "time": "Wed May 11 04:33:00 2016 +0000" 127 | }, 128 | "message": "gensupport: rename ResumableBuffer -\u003e MediaBuffer.\n\nMediaBuffer will be used to aid retries of uploads that use the\nnon-resumable code path, so this more general name makes more sense.\n\nChange-Id: Iff641a38b06898b837ed017436ea88f27c07070c\nReviewed-on: https://code-review.googlesource.com/4740\nReviewed-by: Dave Day \u003cdjd@golang.org\u003e\n" 129 | }, 130 | { 131 | "commit": "f9a4669e07732c84854dce1f5c451c22427228fb", 132 | "tree": "aeb05efb0c5c354241e1ad9436d77bba6d4f7ce4", 133 | "parents": [ 134 | "ff0a1ff302946b997eb1832381419d1f95143483" 135 | ], 136 | "author": { 137 | "name": "David Symonds", 138 | "email": "dsymonds@golang.org", 139 | "time": "Tue May 03 17:43:48 2016 -0700" 140 | }, 141 | "committer": { 142 | "name": "David Symonds", 143 | "email": "dsymonds@golang.org", 144 | "time": "Wed May 04 00:54:13 2016 +0000" 145 | }, 146 | "message": "Special-case appengine/v1beta5 \"Service\" type to avoid it being \"Service1\".\n\nPut it back to its previous name, \"Module\".\n\nChange-Id: Ibd5331c142c755fd0af2e0a88b0f31c37143fac5\nReviewed-on: https://code-review.googlesource.com/4680\nReviewed-by: Dave Day \u003cdjd@golang.org\u003e\nReviewed-by: Russ Cox \u003crsc@swtch.com\u003e\nReviewed-by: Chris Broadfoot \u003ccbro@google.com\u003e\n" 147 | }, 148 | { 149 | "commit": "ff0a1ff302946b997eb1832381419d1f95143483", 150 | "tree": "982bfeae62c24940671365cd69cd863d33b2ded1", 151 | "parents": [ 152 | "9737cc9e103c00d06a8f3993361dec083df3d252" 153 | ], 154 | "author": { 155 | "name": "Michael Marineau", 156 | "email": "michael.marineau@coreos.com", 157 | "time": "Thu Apr 28 20:50:51 2016 -0700" 158 | }, 159 | "committer": { 160 | "name": "Brad Fitzpatrick", 161 | "email": "bradfitz@golang.org", 162 | "time": "Sun May 01 19:00:44 2016 +0000" 163 | }, 164 | "message": "Regenerate APIs.\n\nChange-Id: Id7cc4597b6b7d9439063d6358512cdb504b5376f\nReviewed-on: https://code-review.googlesource.com/4651\nReviewed-by: Brad Fitzpatrick \u003cbradfitz@golang.org\u003e\n" 165 | }, 166 | { 167 | "commit": "9737cc9e103c00d06a8f3993361dec083df3d252", 168 | "tree": "9b005e8308f17c1db1d222209b5f249f7070085a", 169 | "parents": [ 170 | "6dcd0d8ed711248e845137a35720e88b3ed2e04a" 171 | ], 172 | "author": { 173 | "name": "Michael McGreevy", 174 | "email": "mcgreevy@golang.org", 175 | "time": "Fri Apr 08 16:59:26 2016 +1000" 176 | }, 177 | "committer": { 178 | "name": "Michael McGreevy", 179 | "email": "mcgreevy@golang.org", 180 | "time": "Fri Apr 08 07:11:26 2016 +0000" 181 | }, 182 | "message": "generator: don\u0027t retry initial media upload request.\n\nWhen non-resumable upload is used, the media to be uploaded is read from\nan io.Reader, and the contents of this reader are read in the first\nupload attempt. If this first attempt fails, the existing code tries\nagain with a second attempt, and reads from the Reader again, which\nreturns 0 bytes.\n\nResumable upload handles this case by buffering the data to be uploaded,\nbut the non-resumable case does not.\n\nThis change prevents erroneously attempting to re-read the media body by\nnot retrying failed initial requests. This will result in more observed\nupload failures, but will prevent truncated upload being reported as\nsuccessful.\n\nA future change can reinstate retries while avoiding this problem.\n\nChange-Id: I00389ea0196cf4d6bb567c3aa5109f3830d91c7e\nReviewed-on: https://code-review.googlesource.com/4501\nReviewed-by: Dave Day \u003cdjd@golang.org\u003e\n" 183 | }, 184 | { 185 | "commit": "6dcd0d8ed711248e845137a35720e88b3ed2e04a", 186 | "tree": "91f03693827ee8a6c9adbd9f8b52552bc3242f1e", 187 | "parents": [ 188 | "43c645d4bcf9251ced36c823a93b6d198764aae4" 189 | ], 190 | "author": { 191 | "name": "Michael McGreevy", 192 | "email": "mcgreevy@golang.org", 193 | "time": "Wed Apr 06 10:26:44 2016 +1000" 194 | }, 195 | "committer": { 196 | "name": "Michael McGreevy", 197 | "email": "mcgreevy@golang.org", 198 | "time": "Wed Apr 06 06:27:27 2016 +0000" 199 | }, 200 | "message": "googleapi: Add support for trace tokens.\n\nChange-Id: I0d3226f7aa429b50fc122e10ef47d479985dc36a\nReviewed-on: https://code-review.googlesource.com/4453\nReviewed-by: Brad Fitzpatrick \u003cbradfitz@golang.org\u003e\n" 201 | }, 202 | { 203 | "commit": "43c645d4bcf9251ced36c823a93b6d198764aae4", 204 | "tree": "ffb1bb8f4e4068543746183dd4e10550491fa54b", 205 | "parents": [ 206 | "9bf6e6e569ff057f75d9604a46c52928f17d2b54" 207 | ], 208 | "author": { 209 | "name": "Chris Broadfoot", 210 | "email": "cbro@golang.org", 211 | "time": "Thu Mar 24 10:48:31 2016 -0700" 212 | }, 213 | "committer": { 214 | "name": "Chris Broadfoot", 215 | "email": "cbro@google.com", 216 | "time": "Thu Mar 24 17:57:52 2016 +0000" 217 | }, 218 | "message": "container: regenerate container:v1\n\nUpdates #127.\n\nChange-Id: I93a7cbe79546535bb07f67b1eb8697142d1bacd5\nReviewed-on: https://code-review.googlesource.com/4381\nReviewed-by: Evan Brown \u003cevanbrown@google.com\u003e\nReviewed-by: Chris Broadfoot \u003ccbro@google.com\u003e\n" 219 | }, 220 | { 221 | "commit": "9bf6e6e569ff057f75d9604a46c52928f17d2b54", 222 | "tree": "d2e1579c1d850d9f45e7eab14e204a8563d5e8c6", 223 | "parents": [ 224 | "fceeaa645c4015c833842e6ed6052b2dda667079" 225 | ], 226 | "author": { 227 | "name": "Chris Broadfoot", 228 | "email": "cbro@golang.org", 229 | "time": "Mon Mar 21 16:44:42 2016 -0700" 230 | }, 231 | "committer": { 232 | "name": "Chris Broadfoot", 233 | "email": "cbro@google.com", 234 | "time": "Tue Mar 22 02:51:52 2016 +0000" 235 | }, 236 | "message": "monitoring: regenerate monitoring:v3\n\nChange-Id: Ic61497994af45f683898776b8c0432f3b0584460\nReviewed-on: https://code-review.googlesource.com/4343\nReviewed-by: Brad Fitzpatrick \u003cbradfitz@golang.org\u003e\n" 237 | }, 238 | { 239 | "commit": "fceeaa645c4015c833842e6ed6052b2dda667079", 240 | "tree": "34aff06236352964b18b8dc453c433cc258c5592", 241 | "parents": [ 242 | "910020f453d58af7ffc8e53499c4fdfbbda05656" 243 | ], 244 | "author": { 245 | "name": "Michael McGreevy", 246 | "email": "mcgreevy@golang.org", 247 | "time": "Thu Feb 18 15:56:17 2016 +1100" 248 | }, 249 | "committer": { 250 | "name": "Michael McGreevy", 251 | "email": "mcgreevy@golang.org", 252 | "time": "Wed Mar 16 22:48:02 2016 +0000" 253 | }, 254 | "message": "generator: support map from string to array of \"any\" type.\n\nFixes #130\n\nChange-Id: I6f7212a74e30906fc5831e2c389c1d447490f768\nReviewed-on: https://code-review.googlesource.com/4191\nReviewed-by: Brad Fitzpatrick \u003cbradfitz@golang.org\u003e\n" 255 | }, 256 | { 257 | "commit": "910020f453d58af7ffc8e53499c4fdfbbda05656", 258 | "tree": "834e8d5c31e7c4b4d5ab5642e48304ff1327c584", 259 | "parents": [ 260 | "e61f6e00cdf8b78313641288c50f6e2d4e3d749c" 261 | ], 262 | "author": { 263 | "name": "Chris Broadfoot", 264 | "email": "cbro@golang.org", 265 | "time": "Wed Mar 16 12:18:57 2016 -0700" 266 | }, 267 | "committer": { 268 | "name": "Brad Fitzpatrick", 269 | "email": "bradfitz@golang.org", 270 | "time": "Wed Mar 16 20:41:43 2016 +0000" 271 | }, 272 | "message": "Regenerate APIs.\n\nChange-Id: I79d882a9a77cf49bcdbd394dbb98e3f285aa4dfb\nReviewed-on: https://code-review.googlesource.com/4320\nReviewed-by: Brad Fitzpatrick \u003cbradfitz@golang.org\u003e\n" 273 | }, 274 | { 275 | "commit": "e61f6e00cdf8b78313641288c50f6e2d4e3d749c", 276 | "tree": "338ed4285feb8784f15f4b00cd6ebba98ddcd722", 277 | "parents": [ 278 | "9dd147171c9080fd31fe304a6d852aaac845f9f6" 279 | ], 280 | "author": { 281 | "name": "Michael McGreevy", 282 | "email": "mcgreevy@golang.org", 283 | "time": "Thu Mar 03 16:36:50 2016 +1100" 284 | }, 285 | "committer": { 286 | "name": "Michael McGreevy", 287 | "email": "mcgreevy@golang.org", 288 | "time": "Thu Mar 03 23:25:31 2016 +0000" 289 | }, 290 | "message": "generator: Allow supression of Content-Type header.\n\nThis allows users to upload media via the Media method without sending a\nContent-Type header. Some APIs (e.g. storage) can detect this and use\nanother mechanism for determining the content type (e.g. the storage API\ncan inspect the contentType field in the object).\n\nFixes #134\n\nChange-Id: Ib39a405a179358dfc736a08f0af47856155319d4\nReviewed-on: https://code-review.googlesource.com/4276\nReviewed-by: Dave Day \u003cdjd@golang.org\u003e\n" 291 | }, 292 | { 293 | "commit": "9dd147171c9080fd31fe304a6d852aaac845f9f6", 294 | "tree": "07812166bc1d0d640f1ae28748be1a44493a227c", 295 | "parents": [ 296 | "a1a3189e71b3a1266c30d8f07bf6e469f7541f79" 297 | ], 298 | "author": { 299 | "name": "Brad Fitzpatrick", 300 | "email": "bradfitz@golang.org", 301 | "time": "Wed Mar 02 01:48:40 2016 +0000" 302 | }, 303 | "committer": { 304 | "name": "Brad Fitzpatrick", 305 | "email": "bradfitz@golang.org", 306 | "time": "Wed Mar 02 03:26:17 2016 +0000" 307 | }, 308 | "message": "More doc tweaks to GettingStarted\n\nChange-Id: I4f6b9e27c5c897509b63ea166bbb85b2f6b089f9\nReviewed-on: https://code-review.googlesource.com/4242\nReviewed-by: Brad Fitzpatrick \u003cbradfitz@golang.org\u003e\n" 309 | }, 310 | { 311 | "commit": "a1a3189e71b3a1266c30d8f07bf6e469f7541f79", 312 | "tree": "fc47822dfe9193e5b696aad5773e31930f47b11f", 313 | "parents": [ 314 | "286e8c9bcb5f84d8dcac21634d79ab16428b1b78" 315 | ], 316 | "author": { 317 | "name": "Brad Fitzpatrick", 318 | "email": "bradfitz@golang.org", 319 | "time": "Tue Mar 01 11:00:25 2016 -0800" 320 | }, 321 | "committer": { 322 | "name": "Brad Fitzpatrick", 323 | "email": "bradfitz@golang.org", 324 | "time": "Wed Mar 02 00:55:19 2016 +0000" 325 | }, 326 | "message": "Update GettingStarted a bit.\n\nProbably needs much more love, but this is a bit better.\n\nChange-Id: I3b509a0e2ff0488acc2e15e505d52ec4c01b00f7\nReviewed-on: https://code-review.googlesource.com/4241\nReviewed-by: Brad Fitzpatrick \u003cbradfitz@golang.org\u003e\n" 327 | }, 328 | { 329 | "commit": "286e8c9bcb5f84d8dcac21634d79ab16428b1b78", 330 | "tree": "04a1f8d427bf6c659365bf696efeab53a7acdada", 331 | "parents": [ 332 | "33bb8a035a2fb15e13125a3c3096e1b65fbb751b" 333 | ], 334 | "author": { 335 | "name": "Dave Day", 336 | "email": "djd@golang.org", 337 | "time": "Fri Feb 26 15:15:36 2016 +1100" 338 | }, 339 | "committer": { 340 | "name": "Dave Day", 341 | "email": "djd@golang.org", 342 | "time": "Fri Feb 26 05:56:01 2016 +0000" 343 | }, 344 | "message": "uritemplates: correct handling of empty values\n\nAs per the spec, empty values and absent values have different\nbehaviour.\n\nWhile I\u0027m here, remove error returns from methods when the error is\nalways nil.\n\nChange-Id: I9652c020a58cb14f69ce037aa1c33e5fa781ab49\nReviewed-on: https://code-review.googlesource.com/4266\nReviewed-by: Michael McGreevy \u003cmcgreevy@golang.org\u003e\n" 345 | }, 346 | { 347 | "commit": "33bb8a035a2fb15e13125a3c3096e1b65fbb751b", 348 | "tree": "f14033f360aa718f9a60e27434b129a0bc0bcf85", 349 | "parents": [ 350 | "e6294e63a06b2be522ff3d328d8cacded0b1bd31" 351 | ], 352 | "author": { 353 | "name": "Dave Day", 354 | "email": "djd@golang.org", 355 | "time": "Fri Feb 26 14:12:26 2016 +1100" 356 | }, 357 | "committer": { 358 | "name": "Dave Day", 359 | "email": "djd@golang.org", 360 | "time": "Fri Feb 26 04:19:41 2016 +0000" 361 | }, 362 | "message": "uritemplates: simplify and add tests\n\nThe generated APIs only ever pass map[string]string as the values, which\nwas leaving a lot of complex, unexercised code in the uritemplates\npackage. Simplifying this package will allow future changes to be\nimplemented more safely.\n\nAdd unit tests, all taken directly from the RFC, to ensure correctness.\n\nChange-Id: I5a4ef07347475649d79e02b957105ef018f14fbc\nReviewed-on: https://code-review.googlesource.com/4265\nReviewed-by: Michael McGreevy \u003cmcgreevy@golang.org\u003e\n" 363 | }, 364 | { 365 | "commit": "e6294e63a06b2be522ff3d328d8cacded0b1bd31", 366 | "tree": "3198bdaa0814eb1ddb73a129ad918d53dd3d4d32", 367 | "parents": [ 368 | "e95532744d7a13e9a8098c13f0868dfff77bb108" 369 | ], 370 | "author": { 371 | "name": "Dave Day", 372 | "email": "djd@golang.org", 373 | "time": "Fri Feb 19 16:41:36 2016 +1100" 374 | }, 375 | "committer": { 376 | "name": "Michael McGreevy", 377 | "email": "mcgreevy@golang.org", 378 | "time": "Fri Feb 19 05:54:22 2016 +0000" 379 | }, 380 | "message": "travis: drop 1.4 build, add 1.6 build\n\nChange-Id: I7c6858452271b04fb7943f899c931ff4bbc4dcde\nReviewed-on: https://code-review.googlesource.com/4211\nReviewed-by: Michael McGreevy \u003cmcgreevy@golang.org\u003e\n" 381 | }, 382 | { 383 | "commit": "e95532744d7a13e9a8098c13f0868dfff77bb108", 384 | "tree": "7ac861495436d5aebb6858dd133c6b53fc71dc28", 385 | "parents": [ 386 | "d2bfc4131a034197ecc039fe5326701001e90254" 387 | ], 388 | "author": { 389 | "name": "Dave Day", 390 | "email": "djd@golang.org", 391 | "time": "Mon Feb 15 17:10:33 2016 +1100" 392 | }, 393 | "committer": { 394 | "name": "Dave Day", 395 | "email": "djd@golang.org", 396 | "time": "Tue Feb 16 00:30:52 2016 +0000" 397 | }, 398 | "message": "Automatically retry failed media upload requests.\n\nPrevious retry changes have only added retry logic to uploading the main\nmedia body. This change also adds retry to the initial (or sometimes\nonly) request.\n\nTo be conservative, we don\u0027t add equivalent retry logic to other request\ntypes yet.\n\nFixes GoogleCloudPlatform/gcloud-golang#179\n\nChange-Id: I580a872e504faa2ca0b1761c19602a37fc233e16\nReviewed-on: https://code-review.googlesource.com/4175\nReviewed-by: Michael McGreevy \u003cmcgreevy@golang.org\u003e\n" 399 | }, 400 | { 401 | "commit": "d2bfc4131a034197ecc039fe5326701001e90254", 402 | "tree": "ab20440d62946058d344ab7a13abdd7b0ce1b058", 403 | "parents": [ 404 | "8a448fd57af58b0eaeb8fde0345ec7ae88e55076" 405 | ], 406 | "author": { 407 | "name": "Dave Day", 408 | "email": "djd@golang.org", 409 | "time": "Mon Feb 15 15:21:21 2016 +1100" 410 | }, 411 | "committer": { 412 | "name": "Dave Day", 413 | "email": "djd@golang.org", 414 | "time": "Mon Feb 15 05:41:24 2016 +0000" 415 | }, 416 | "message": "gensupport: add a helper Retry function\n\nThis function invokes the given function a number of times using the\nsame logic as used by ResumableUpload.Upload. I\u0027m introducing it to add\nsimilar retry logic to single-request uploads too for #179.\n\nWhile I\u0027m here, simplify NoPauseStrategy.\n\nChange-Id: I8c5247bcba5bb704fc2e35dec4f3b61c6b0b59e9\nReviewed-on: https://code-review.googlesource.com/4173\nReviewed-by: Michael McGreevy \u003cmcgreevy@golang.org\u003e\n" 417 | }, 418 | { 419 | "commit": "8a448fd57af58b0eaeb8fde0345ec7ae88e55076", 420 | "tree": "d2de1244884078418d0d37b99782afe74e72f044", 421 | "parents": [ 422 | "ad6c2cd766aa31d72fe3fce08189e0ba6e5002b9" 423 | ], 424 | "author": { 425 | "name": "Dave Day", 426 | "email": "djd@golang.org", 427 | "time": "Fri Feb 12 10:41:35 2016 +1100" 428 | }, 429 | "committer": { 430 | "name": "Dave Day", 431 | "email": "djd@golang.org", 432 | "time": "Fri Feb 12 02:24:20 2016 +0000" 433 | }, 434 | "message": "gensupport: rework retry logic\n\nChange retry logic so that only some HTTP response codes and errors\nwill cause subsequent retries.\n\nThe HTTP response codes that trigger retry are server errors [500, 600)\nas well as over limit error 429.\nThe errors that trigger retry are those that indicate a temporary error\nor interrupted connection.\n\nFixes #128\nFixes GoogleCloudPlatform/gcloud-golang#179\n\nChange-Id: I531abf2ded273783e66467898833672cfe9c5241\nReviewed-on: https://code-review.googlesource.com/4157\nReviewed-by: Michael McGreevy \u003cmcgreevy@golang.org\u003e\n" 435 | }, 436 | { 437 | "commit": "ad6c2cd766aa31d72fe3fce08189e0ba6e5002b9", 438 | "tree": "34a358e1e9b23da0e59d0a8ff3b47fc40fbc5536", 439 | "parents": [ 440 | "ddff2aff599105a55549cf173852507dfa094b7f" 441 | ], 442 | "author": { 443 | "name": "Dave Day", 444 | "email": "djd@golang.org", 445 | "time": "Thu Feb 04 16:19:49 2016 +1100" 446 | }, 447 | "committer": { 448 | "name": "Dave Day", 449 | "email": "djd@golang.org", 450 | "time": "Sun Feb 07 22:45:04 2016 +0000" 451 | }, 452 | "message": "gensupport: add jitter and timeout to exponential back off\n\nThis also reduces the minimum retry duration to something more\nreasonable.\n\nChange-Id: I437042f63740cf73ed1c1589ae1269f430f1a0e6\nReviewed-on: https://code-review.googlesource.com/4116\nReviewed-by: Michael McGreevy \u003cmcgreevy@golang.org\u003e\n" 453 | }, 454 | { 455 | "commit": "ddff2aff599105a55549cf173852507dfa094b7f", 456 | "tree": "2ca2334855be047f317391fb134e5bc82ee14430", 457 | "parents": [ 458 | "56858378d684b61701ab955fc11429dcf6ce5331" 459 | ], 460 | "author": { 461 | "name": "David Symonds", 462 | "email": "dsymonds@golang.org", 463 | "time": "Thu Feb 04 15:02:10 2016 +1100" 464 | }, 465 | "committer": { 466 | "name": "Michael McGreevy", 467 | "email": "mcgreevy@golang.org", 468 | "time": "Thu Feb 04 04:43:19 2016 +0000" 469 | }, 470 | "message": "Update codegen testdata.\n\nChange-Id: Ia9d11ccc1b572ddd659bb0957b2a47ff87c67de5\nReviewed-on: https://code-review.googlesource.com/4114\nReviewed-by: Michael McGreevy \u003cmcgreevy@golang.org\u003e\n" 471 | }, 472 | { 473 | "commit": "56858378d684b61701ab955fc11429dcf6ce5331", 474 | "tree": "fd27853947690f657da6fcd230f27e94b25ada48", 475 | "parents": [ 476 | "61d74df3f9f3a66898c8e08aa7e702337b34dda3" 477 | ], 478 | "author": { 479 | "name": "Michael McGreevy", 480 | "email": "mcgreevy@golang.org", 481 | "time": "Tue Feb 02 12:25:58 2016 +1100" 482 | }, 483 | "committer": { 484 | "name": "Michael McGreevy", 485 | "email": "mcgreevy@golang.org", 486 | "time": "Thu Feb 04 03:00:58 2016 +0000" 487 | }, 488 | "message": "generator: Fix documenation for {,Resumable}Media.\n\nChange-Id: I219447619d4432911666831e18225d84d3840373\nReviewed-on: https://code-review.googlesource.com/4112\nReviewed-by: Dave Day \u003cdjd@golang.org\u003e\n" 489 | }, 490 | { 491 | "commit": "61d74df3f9f3a66898c8e08aa7e702337b34dda3", 492 | "tree": "fe19da160a3a5c4d55b4132c88b59d0b436c94fc", 493 | "parents": [ 494 | "8363a38d2c47439699b871a586b8946e8ffad8d2" 495 | ], 496 | "author": { 497 | "name": "David Symonds", 498 | "email": "dsymonds@golang.org", 499 | "time": "Thu Feb 04 11:47:03 2016 +1100" 500 | }, 501 | "committer": { 502 | "name": "David Symonds", 503 | "email": "dsymonds@golang.org", 504 | "time": "Thu Feb 04 00:55:12 2016 +0000" 505 | }, 506 | "message": "Update to new mechanism for finding the discovery URL for APIs.\n\nThe discovery API itself got broken under us; discoveryLink is no\nlonger populated and discoveryRestUrl is now an absolute URL to use.\n\nRegenerate all the APIs as a result, and add a few new ones that\nhave now appeared.\n\nChange-Id: Ie68f6af5288f3e3c5ee77536d6a255af90689cca\nReviewed-on: https://code-review.googlesource.com/4111\nReviewed-by: Michael McGreevy \u003cmcgreevy@golang.org\u003e\n" 507 | }, 508 | { 509 | "commit": "8363a38d2c47439699b871a586b8946e8ffad8d2", 510 | "tree": "f7adf5b3f9c024e6fabb0fc9eeae1bb925b17fd7", 511 | "parents": [ 512 | "8fa1015948e6fc21c025050624e4c4e2f4f405c4" 513 | ], 514 | "author": { 515 | "name": "David Symonds", 516 | "email": "dsymonds@golang.org", 517 | "time": "Mon Jan 11 13:44:40 2016 +1100" 518 | }, 519 | "committer": { 520 | "name": "David Symonds", 521 | "email": "dsymonds@golang.org", 522 | "time": "Thu Feb 04 00:14:01 2016 +0000" 523 | }, 524 | "message": "Add generated paging helpers.\n\nWhen an API method meets certain requirements, generate a Pages method\non the call type that will yield successive pages to a provided func.\nThe requirements are, roughly:\n\t- the call has a non-required pageToken parameter\n\t- the call returns a struct with a parameter that looks like a\n\t page token\n\nThe sophistication of those rules could be increased later to\nidentify a few more places where paging helpers could be added.\n\nFixes #114.\n\nChange-Id: I22865b7768a77320072f939e3f1bf742206385fa\nReviewed-on: https://code-review.googlesource.com/3993\nReviewed-by: David Symonds \u003cdsymonds@golang.org\u003e\n" 525 | }, 526 | { 527 | "commit": "8fa1015948e6fc21c025050624e4c4e2f4f405c4", 528 | "tree": "a7e660da7b1db4fe14601a9e2dbb1f30eeee14a9", 529 | "parents": [ 530 | "716f353926648bf50fbecb557ca557f5518c79fd" 531 | ], 532 | "author": { 533 | "name": "Michael McGreevy", 534 | "email": "mcgreevy@golang.org", 535 | "time": "Tue Feb 02 12:25:58 2016 +1100" 536 | }, 537 | "committer": { 538 | "name": "Michael McGreevy", 539 | "email": "mcgreevy@golang.org", 540 | "time": "Tue Feb 02 02:05:59 2016 +0000" 541 | }, 542 | "message": "generator: Remove dead code.\n\nSince cb1db97 , overrideParameterName is no longer needed.\n\nChange-Id: I1819b7befe78e6facbd730c9d8070d61edd4b5ab\nReviewed-on: https://code-review.googlesource.com/4095\nReviewed-by: David Symonds \u003cdsymonds@golang.org\u003e\n" 543 | }, 544 | { 545 | "commit": "716f353926648bf50fbecb557ca557f5518c79fd", 546 | "tree": "d697a2df73d7bcfa9a8c6ffdea0e2a2782a635da", 547 | "parents": [ 548 | "f6ba98773d96b877b246f3a9266493dfc11e276b" 549 | ], 550 | "author": { 551 | "name": "Michael McGreevy", 552 | "email": "mcgreevy@golang.org", 553 | "time": "Mon Feb 01 14:17:04 2016 +1100" 554 | }, 555 | "committer": { 556 | "name": "Michael McGreevy", 557 | "email": "mcgreevy@golang.org", 558 | "time": "Tue Feb 02 01:19:48 2016 +0000" 559 | }, 560 | "message": "all: Clean up resumable upload request loop.\n\nThis is mostly refactoring of old code.\nIt also introduces an exponential backoff retry strategy.\n\nChange-Id: Ia325ff2dae4715805080b3a6cf21b79a9345b0ca\nReviewed-on: https://code-review.googlesource.com/4092\nReviewed-by: Michael McGreevy \u003cmcgreevy@golang.org\u003e\n" 561 | }, 562 | { 563 | "commit": "f6ba98773d96b877b246f3a9266493dfc11e276b", 564 | "tree": "e71418ad25cd566b2e36571fe7dcd299bf4fc793", 565 | "parents": [ 566 | "0caa37974a5f5ae67172acf68b4970f7864f994c" 567 | ], 568 | "author": { 569 | "name": "Michael McGreevy", 570 | "email": "mcgreevy@golang.org", 571 | "time": "Thu Jan 28 11:22:04 2016 +1100" 572 | }, 573 | "committer": { 574 | "name": "Michael McGreevy", 575 | "email": "mcgreevy@golang.org", 576 | "time": "Thu Jan 28 02:07:50 2016 +0000" 577 | }, 578 | "message": "gensupport: Allow ReaderAt to be reused.\n\nReaderAtToReader currently checks to see whether the supplied ReaderAt\nalso implements Reader, and if so, just returns that Reader. This is\nincorrect, since Reader may already have been read from, but the\nsemantics of supplying a ReaderAt is that it should be read from the\nbeginning. This change ensures that the returned Reader always reads\nfrom the beginning of the ReaderAt.\n\nFixes #126\n\nChange-Id: I09c32f9a8a9f2c3e514679a7c822451d5d2cc16c\nReviewed-on: https://code-review.googlesource.com/4070\nReviewed-by: David Symonds \u003cdsymonds@golang.org\u003e\n" 579 | }, 580 | { 581 | "commit": "0caa37974a5f5ae67172acf68b4970f7864f994c", 582 | "tree": "742d98b31aa905ec31b1f9f05a6bc943235042cb", 583 | "parents": [ 584 | "cb1db97256a1b26df34011e6fc504e7c074c91ec" 585 | ], 586 | "author": { 587 | "name": "David Symonds", 588 | "email": "dsymonds@golang.org", 589 | "time": "Tue Jan 19 13:45:30 2016 +1100" 590 | }, 591 | "committer": { 592 | "name": "Michael McGreevy", 593 | "email": "mcgreevy@golang.org", 594 | "time": "Tue Jan 19 03:14:59 2016 +0000" 595 | }, 596 | "message": "Update golden data for generator test.\n\nChange-Id: I925ed0a813aed2173f29ea92fbd09301111643cb\nReviewed-on: https://code-review.googlesource.com/4028\nReviewed-by: Michael McGreevy \u003cmcgreevy@golang.org\u003e\n" 597 | }, 598 | { 599 | "commit": "cb1db97256a1b26df34011e6fc504e7c074c91ec", 600 | "tree": "1520c734cb2e7af353e05ca8e4106cd821514898", 601 | "parents": [ 602 | "f70ad682fde08f28c80f8e7a5cf17c21244ddaaa" 603 | ], 604 | "author": { 605 | "name": "David Symonds", 606 | "email": "dsymonds@golang.org", 607 | "time": "Mon Jan 11 10:59:52 2016 +1100" 608 | }, 609 | "committer": { 610 | "name": "Michael McGreevy", 611 | "email": "mcgreevy@golang.org", 612 | "time": "Tue Jan 19 02:36:52 2016 +0000" 613 | }, 614 | "message": "Simplify per-call API by factoring common cross-API parameters.\n\nThis introduces googleapi.CallOption to replace the QuotaUser and UserIP\nmethods that are attached to many Call types.\n\nInstead of\n\ttype AclDeleteCall\n\tfunc (c *AclDeleteCall) Do() error\n\tfunc (c *AclDeleteCall) QuotaUser(quotaUser string) *AclDeleteCall\n\tfunc (c *AclDeleteCall) UserIP(userIP string) *AclDeleteCall\nwe now generate\n\ttype AclDeleteCall\n\tfunc (c *AclDeleteCall) Do(...googleapi.CallOption) error\nand supply in package googleapi\n\ttype CallOption\n\tfunc QuotaUser(u string) CallOption\n\tfunc UserIP(ip string) CallOption\n\nChange-Id: I41518390cfd9d13150aaf35b5fe703e812e3643e\nReviewed-on: https://code-review.googlesource.com/3992\nReviewed-by: Michael McGreevy \u003cmcgreevy@golang.org\u003e\n" 615 | }, 616 | { 617 | "commit": "f70ad682fde08f28c80f8e7a5cf17c21244ddaaa", 618 | "tree": "1ee0bc0495046e40f8eb71052109e963ec917f7c", 619 | "parents": [ 620 | "89913f6d79db461b370e87affbed92536c6029a2" 621 | ], 622 | "author": { 623 | "name": "Michael McGreevy", 624 | "email": "mcgreevy@golang.org", 625 | "time": "Mon Jan 18 16:21:33 2016 +1100" 626 | }, 627 | "committer": { 628 | "name": "Michael McGreevy", 629 | "email": "mcgreevy@golang.org", 630 | "time": "Tue Jan 19 00:18:39 2016 +0000" 631 | }, 632 | "message": "google-api-go-client: Make Media support resumable uploads.\n\nAllow users to use Media methods for resumable uploads. This means that:\n* A ReaderAt is no longer needed for resumable uploads.\n* The size of the content no long needs to be known a priori.\n* The use can configure the chunk size to be used.\n\nResumableMedia is now deprecated in favor of Media.\n\nChange-Id: Ifa3845f88abe82773392d36f34bbd3ce0e3c1f0a\nReviewed-on: https://code-review.googlesource.com/4027\nReviewed-by: Dave Day \u003cdjd@golang.org\u003e\n" 633 | }, 634 | { 635 | "commit": "89913f6d79db461b370e87affbed92536c6029a2", 636 | "tree": "83e4a48a6bff24c21e0e8031db72655e2066e7d2", 637 | "parents": [ 638 | "b55544c941dfadb60c112dbeb8e1354f1c2c34bf" 639 | ], 640 | "author": { 641 | "name": "Michael McGreevy", 642 | "email": "mcgreevy@golang.org", 643 | "time": "Mon Jan 18 16:21:33 2016 +1100" 644 | }, 645 | "committer": { 646 | "name": "Michael McGreevy", 647 | "email": "mcgreevy@golang.org", 648 | "time": "Mon Jan 18 05:41:23 2016 +0000" 649 | }, 650 | "message": "gensupport: fix the build.\n\nChange-Id: Ie818ee564e5c115d5719721fc6357248c0bf5236\nReviewed-on: https://code-review.googlesource.com/4026\nReviewed-by: Dave Day \u003cdjd@golang.org\u003e\n" 651 | }, 652 | { 653 | "commit": "b55544c941dfadb60c112dbeb8e1354f1c2c34bf", 654 | "tree": "b821175d21a1f05a6a6a7b6e13d62408097f4e2e", 655 | "parents": [ 656 | "ec89ab60f4b5d66700aba395c0ec1a9f9811e998" 657 | ], 658 | "author": { 659 | "name": "Michael McGreevy", 660 | "email": "mcgreevy@golang.org", 661 | "time": "Thu Jan 14 14:56:10 2016 +1100" 662 | }, 663 | "committer": { 664 | "name": "Michael McGreevy", 665 | "email": "mcgreevy@golang.org", 666 | "time": "Mon Jan 18 05:16:04 2016 +0000" 667 | }, 668 | "message": "googleapi: Share more code between Media \u0026 ResumableMedia.\n\nUntil now, Media and ResumableMedia stored content to be uploaded in\nseparate fields of the Call struct. This merges those fields.\n\nChange-Id: I67baecc85e149eea1cc2413c7fa585f891850a03\nReviewed-on: https://code-review.googlesource.com/4025\nReviewed-by: Dave Day \u003cdjd@golang.org\u003e\n" 669 | }, 670 | { 671 | "commit": "ec89ab60f4b5d66700aba395c0ec1a9f9811e998", 672 | "tree": "f10465253f82c2386cd8124ea2b47b624ab9ed0e", 673 | "parents": [ 674 | "b2be3c69519a81292faa7016d3c87f8b68c6f0d9" 675 | ], 676 | "author": { 677 | "name": "Michael McGreevy", 678 | "email": "mcgreevy@golang.org", 679 | "time": "Thu Jan 14 14:56:10 2016 +1100" 680 | }, 681 | "committer": { 682 | "name": "Michael McGreevy", 683 | "email": "mcgreevy@golang.org", 684 | "time": "Mon Jan 18 02:53:39 2016 +0000" 685 | }, 686 | "message": "googleapi: Refactor ResumableUpload to use ResumableBuffer.\n\nAlso cleaned up some tests along the way.\n\nChange-Id: I9537f403951103b8b9a42eb348079f117230b274\nReviewed-on: https://code-review.googlesource.com/4022\nReviewed-by: Dave Day \u003cdjd@golang.org\u003e\n" 687 | }, 688 | { 689 | "commit": "b2be3c69519a81292faa7016d3c87f8b68c6f0d9", 690 | "tree": "e21ebd666efe8be22b840880180f94b6d453f9d3", 691 | "parents": [ 692 | "5f60caa8e6b14382dde84104e808dadb51e1180a" 693 | ], 694 | "author": { 695 | "name": "Michael McGreevy", 696 | "email": "mcgreevy@golang.org", 697 | "time": "Thu Jan 14 14:56:10 2016 +1100" 698 | }, 699 | "committer": { 700 | "name": "Michael McGreevy", 701 | "email": "mcgreevy@golang.org", 702 | "time": "Thu Jan 14 05:05:31 2016 +0000" 703 | }, 704 | "message": "googleapi: Move ResumableUpload to gensupport.\n\nChange-Id: Ib4b5c633068fc91db935f1a584482aa0d9e66b93\nReviewed-on: https://code-review.googlesource.com/4020\nReviewed-by: Dave Day \u003cdjd@golang.org\u003e\n" 705 | }, 706 | { 707 | "commit": "5f60caa8e6b14382dde84104e808dadb51e1180a", 708 | "tree": "41b290ae4f0d412d5628452a2b34e232879339a7", 709 | "parents": [ 710 | "83f801e4c423dc658849f683d8d9611df1fd4bae" 711 | ], 712 | "author": { 713 | "name": "Michael McGreevy", 714 | "email": "mcgreevy@golang.org", 715 | "time": "Thu Jan 14 10:31:12 2016 +1100" 716 | }, 717 | "committer": { 718 | "name": "Michael McGreevy", 719 | "email": "mcgreevy@golang.org", 720 | "time": "Thu Jan 14 05:02:53 2016 +0000" 721 | }, 722 | "message": "gensupport: Add a buffer for resumable uploads.\n\nresumableBuffer is a new type which buffers chunks of data to be\nuploaded via resumable upload.\n\nChange-Id: Iacd9694ec36f5da579aedc0b8da7f644d42d9f69\nReviewed-on: https://code-review.googlesource.com/4005\nReviewed-by: Michael McGreevy \u003cmcgreevy@golang.org\u003e\n" 723 | }, 724 | { 725 | "commit": "83f801e4c423dc658849f683d8d9611df1fd4bae", 726 | "tree": "8b16c4e53d20ab5d3010399dc285d0e0518d9127", 727 | "parents": [ 728 | "2e67b990535ff1a7a7fe084ca0ee69d3aa031fbc" 729 | ], 730 | "author": { 731 | "name": "Michael McGreevy", 732 | "email": "mcgreevy@golang.org", 733 | "time": "Wed Jan 13 11:06:39 2016 +1100" 734 | }, 735 | "committer": { 736 | "name": "Michael McGreevy", 737 | "email": "mcgreevy@golang.org", 738 | "time": "Wed Jan 13 00:26:21 2016 +0000" 739 | }, 740 | "message": "all: Update all APIs.\n\nFixes #120\n\nChange-Id: Id53302e28911f1b9cfb0950524e805b6ffa94aca\nReviewed-on: https://code-review.googlesource.com/4004\nReviewed-by: Dave Day \u003cdjd@golang.org\u003e\n" 741 | }, 742 | { 743 | "commit": "2e67b990535ff1a7a7fe084ca0ee69d3aa031fbc", 744 | "tree": "3a2e676c04955b1d71fda86ff34772a3cc6385eb", 745 | "parents": [ 746 | "2897ed5fbf1057b44c2f012e0ae1f90e3f82970f" 747 | ], 748 | "author": { 749 | "name": "Michael McGreevy", 750 | "email": "mcgreevy@golang.org", 751 | "time": "Tue Jan 12 11:56:22 2016 +1100" 752 | }, 753 | "committer": { 754 | "name": "Michael McGreevy", 755 | "email": "mcgreevy@golang.org", 756 | "time": "Tue Jan 12 23:02:35 2016 +0000" 757 | }, 758 | "message": "gensupport: Simplify IncludeMedia.\n\nThis remove some of the in/out parameter weirdness and simplifies some\nof the implementation.\n\nChange-Id: I48fecd74042014ed718f12d3ae99234e1281c59a\nReviewed-on: https://code-review.googlesource.com/4001\nReviewed-by: Dave Day \u003cdjd@golang.org\u003e\n" 759 | }, 760 | { 761 | "commit": "2897ed5fbf1057b44c2f012e0ae1f90e3f82970f", 762 | "tree": "fc5b20e022fadffdb9ca8dbde9562fd548679678", 763 | "parents": [ 764 | "b2650c8219cf09f3279bd64db247f2b768b32307" 765 | ], 766 | "author": { 767 | "name": "Michael McGreevy", 768 | "email": "mcgreevy@golang.org", 769 | "time": "Tue Jan 12 16:39:09 2016 +1100" 770 | }, 771 | "committer": { 772 | "name": "Michael McGreevy", 773 | "email": "mcgreevy@golang.org", 774 | "time": "Tue Jan 12 23:01:54 2016 +0000" 775 | }, 776 | "message": "gensupport: Dead code removal.\n\nRemove unused getMediaType function.\n\nChange-Id: I2779b49dec069cd0c5ad437ae1ff57c1c6f13b58\nReviewed-on: https://code-review.googlesource.com/4002\nReviewed-by: Dave Day \u003cdjd@golang.org\u003e\n" 777 | }, 778 | { 779 | "commit": "b2650c8219cf09f3279bd64db247f2b768b32307", 780 | "tree": "7644bb075ff94a7005ad3aedbed0e1b2fd87e236", 781 | "parents": [ 782 | "f98effadce73f357b6887f9d357a4371bd0859db" 783 | ], 784 | "author": { 785 | "name": "Michael McGreevy", 786 | "email": "mcgreevy@golang.org", 787 | "time": "Tue Jan 12 11:56:22 2016 +1100" 788 | }, 789 | "committer": { 790 | "name": "Michael McGreevy", 791 | "email": "mcgreevy@golang.org", 792 | "time": "Tue Jan 12 05:33:03 2016 +0000" 793 | }, 794 | "message": "generator: Add MediaOptions to Media methods.\n\nThe first MediaOption allows specifying the content type of the supplied\nreader.\n\nChange-Id: If77c2253849fc1a13efcf89c847ac143a743782f\nReviewed-on: https://code-review.googlesource.com/4000\nReviewed-by: Dave Day \u003cdjd@golang.org\u003e\n" 795 | }, 796 | { 797 | "commit": "f98effadce73f357b6887f9d357a4371bd0859db", 798 | "tree": "fe1f3d4ea2c8010d7a843ca255835a8cf5d6340b", 799 | "parents": [ 800 | "77e7d383beb96054547729f49c372b3d01e196ff" 801 | ], 802 | "author": { 803 | "name": "David Symonds", 804 | "email": "dsymonds@golang.org", 805 | "time": "Mon Jan 11 10:28:04 2016 +1100" 806 | }, 807 | "committer": { 808 | "name": "David Symonds", 809 | "email": "dsymonds@golang.org", 810 | "time": "Sun Jan 10 23:51:19 2016 +0000" 811 | }, 812 | "message": "Regenerate APIs.\n\nChange-Id: I55ac2f61097aeb9fd976d255861295cda9bbd5c5\nReviewed-on: https://code-review.googlesource.com/3991\nReviewed-by: Michael McGreevy \u003cmcgreevy@golang.org\u003e\n" 813 | }, 814 | { 815 | "commit": "77e7d383beb96054547729f49c372b3d01e196ff", 816 | "tree": "0f259b166507b030658fb210e53664a40add3a4a", 817 | "parents": [ 818 | "7285216b0ce086018feab8ff428178f0a6bd95d0" 819 | ], 820 | "author": { 821 | "name": "Michael McGreevy", 822 | "email": "mcgreevy@golang.org", 823 | "time": "Wed Jan 06 14:23:34 2016 +1100" 824 | }, 825 | "committer": { 826 | "name": "Michael McGreevy", 827 | "email": "mcgreevy@golang.org", 828 | "time": "Thu Jan 07 23:54:28 2016 +0000" 829 | }, 830 | "message": "google-api-go-client/all: Move nil media check to callers.\n\nConditionallyIncludeMedia does nothing if media is nil.\n\nSimplify by not calling this function if media is nil.\n\nChange-Id: I4657a0c5a7e02b78d28317b3bab94f713261ced9\nReviewed-on: https://code-review.googlesource.com/3976\nReviewed-by: Michael McGreevy \u003cmcgreevy@golang.org\u003e\n" 831 | }, 832 | { 833 | "commit": "7285216b0ce086018feab8ff428178f0a6bd95d0", 834 | "tree": "649790af3814df0fa190e97ba8ef5613a1c056f0", 835 | "parents": [ 836 | "52ece9b8dc561f8ba70cc2ad17d958bc06fb601a" 837 | ], 838 | "author": { 839 | "name": "Michael McGreevy", 840 | "email": "mcgreevy@golang.org", 841 | "time": "Wed Jan 06 14:23:34 2016 +1100" 842 | }, 843 | "committer": { 844 | "name": "Michael McGreevy", 845 | "email": "mcgreevy@golang.org", 846 | "time": "Thu Jan 07 23:38:14 2016 +0000" 847 | }, 848 | "message": "google-api-go-client/all: Use content sniffer.\n\nUse ContentSniffer instead of a pipe \u0026 goroutine in getMediaType.\n\nChange-Id: I95353eb49d8dc38e62e491c2dcf3edb9e4d0ba7e\nReviewed-on: https://code-review.googlesource.com/3974\nReviewed-by: Dave Day \u003cdjd@golang.org\u003e\n" 849 | }, 850 | { 851 | "commit": "52ece9b8dc561f8ba70cc2ad17d958bc06fb601a", 852 | "tree": "7bb86a1da3ea4f6eb889ccdcf1d58304b1c5b5ec", 853 | "parents": [ 854 | "ef50e660312352c0d2792bf264c0aaeb893171b5" 855 | ], 856 | "author": { 857 | "name": "Michael McGreevy", 858 | "email": "mcgreevy@golang.org", 859 | "time": "Wed Jan 06 14:23:34 2016 +1100" 860 | }, 861 | "committer": { 862 | "name": "Michael McGreevy", 863 | "email": "mcgreevy@golang.org", 864 | "time": "Thu Jan 07 04:31:32 2016 +0000" 865 | }, 866 | "message": "google-api-go-client/all: Add content sniffer.\n\nThis adds a wrapper around io.Reader which sniffs the Reader\u0027s media\ntype without the use of pipes \u0026 goroutines.\n\nVarious other content-sniffing code in gensupport will be refactored in\na subsequent change to remove the pipe/goroutine-based code.\n\nChange-Id: Ic1260fa28ea39116a0531a333edd49679ab8e9e8\nReviewed-on: https://code-review.googlesource.com/3973\nReviewed-by: Dave Day \u003cdjd@golang.org\u003e\n" 867 | }, 868 | { 869 | "commit": "ef50e660312352c0d2792bf264c0aaeb893171b5", 870 | "tree": "54a9fe4e94811c5f4a8b79575f5cab937cd67d06", 871 | "parents": [ 872 | "f5b7ec483f357a211c03c6722a840444c2d395dc" 873 | ], 874 | "author": { 875 | "name": "Michael McGreevy", 876 | "email": "mcgreevy@golang.org", 877 | "time": "Wed Jan 06 14:23:34 2016 +1100" 878 | }, 879 | "committer": { 880 | "name": "Michael McGreevy", 881 | "email": "mcgreevy@golang.org", 882 | "time": "Thu Jan 07 03:52:48 2016 +0000" 883 | }, 884 | "message": "google-api-go-client/all: Move media funcs to gensupport.\n\nChange-Id: I5f7f94af50336a05b0824bf255c4834cb1460ce3\nReviewed-on: https://code-review.googlesource.com/3971\nReviewed-by: Dave Day \u003cdjd@golang.org\u003e\n" 885 | }, 886 | { 887 | "commit": "f5b7ec483f357a211c03c6722a840444c2d395dc", 888 | "tree": "d7151ec468a547d891336af12ed070f41b0434ff", 889 | "parents": [ 890 | "5eb6bc39fc2e09e11aec26f00e901104bbb6f3e9" 891 | ], 892 | "author": { 893 | "name": "Michael McGreevy", 894 | "email": "mcgreevy@golang.org", 895 | "time": "Wed Jan 06 14:29:43 2016 +1100" 896 | }, 897 | "committer": { 898 | "name": "Michael McGreevy", 899 | "email": "mcgreevy@golang.org", 900 | "time": "Wed Jan 06 05:21:53 2016 +0000" 901 | }, 902 | "message": "google-api-go-client: gensupport package cleanup.\n\nMove package doc to its own file -- it\u0027s not json-specific.\n\nChange-Id: Ia93f040367fca97a94664d62a3721843f2c32ec4\nReviewed-on: https://code-review.googlesource.com/3972\nReviewed-by: Dave Day \u003cdjd@golang.org\u003e\n" 903 | }, 904 | { 905 | "commit": "5eb6bc39fc2e09e11aec26f00e901104bbb6f3e9", 906 | "tree": "e8955cbcc49fc7b8ba7669af5ce8a232aaeaa0a3", 907 | "parents": [ 908 | "743ca501d8146d453ed0d7a56f135bdb54943dd2" 909 | ], 910 | "author": { 911 | "name": "Michael McGreevy", 912 | "email": "mcgreevy@golang.org", 913 | "time": "Tue Jan 05 15:08:20 2016 +1100" 914 | }, 915 | "committer": { 916 | "name": "Michael McGreevy", 917 | "email": "mcgreevy@golang.org", 918 | "time": "Wed Jan 06 03:12:27 2016 +0000" 919 | }, 920 | "message": "google-api-go-client/generator: simplify function signature.\n\nSimplify ConditionallyIncludeMedia by removing unused return parameter,\nand always returning a non-nil cancel func.\n\nChange-Id: Ic5c8a36cc6c8157867024b799fdba369a83b99bb\nReviewed-on: https://code-review.googlesource.com/3946\nReviewed-by: Michael McGreevy \u003cmcgreevy@golang.org\u003e\n" 921 | }, 922 | { 923 | "commit": "743ca501d8146d453ed0d7a56f135bdb54943dd2", 924 | "tree": "fb5f036fc0ed8b9434dd241a2955fdcd26afb0d6", 925 | "parents": [ 926 | "40e8982bc18ef49b340c9da2c2bd9d49f94befcd" 927 | ], 928 | "author": { 929 | "name": "Michael McGreevy", 930 | "email": "mcgreevy@golang.org", 931 | "time": "Wed Jan 06 13:40:33 2016 +1100" 932 | }, 933 | "committer": { 934 | "name": "Michael McGreevy", 935 | "email": "mcgreevy@golang.org", 936 | "time": "Wed Jan 06 03:04:26 2016 +0000" 937 | }, 938 | "message": "google-api-go-client/googleapi: remove unused interface.\n\nLengther was used by code that has now been deleted. Remove it.\n\nChange-Id: If1f3fab50ede1bd1b4190552a1160c8f5e772c9c\nReviewed-on: https://code-review.googlesource.com/3970\nReviewed-by: Dave Day \u003cdjd@golang.org\u003e\n" 939 | }, 940 | { 941 | "commit": "40e8982bc18ef49b340c9da2c2bd9d49f94befcd", 942 | "tree": "dd7505be0ab78d6a730fa4e2c3a642cd80b2ab9d", 943 | "parents": [ 944 | "40188352bd74db962ac45ffa44ddda18d719a14b" 945 | ], 946 | "author": { 947 | "name": "Michael McGreevy", 948 | "email": "mcgreevy@golang.org", 949 | "time": "Tue Jan 05 15:11:57 2016 +1100" 950 | }, 951 | "committer": { 952 | "name": "Michael McGreevy", 953 | "email": "mcgreevy@golang.org", 954 | "time": "Tue Jan 05 04:48:26 2016 +0000" 955 | }, 956 | "message": "google-api-go-client: Fix Makefile.\n\nThe command `make local` was not matching all local JSON files. This\nchange fixes that.\n\nChange-Id: Ib06dbad3968376fb7d9f61cc212e1ffc93a9d525\nReviewed-on: https://code-review.googlesource.com/3945\nReviewed-by: Dave Day \u003cdjd@golang.org\u003e\n" 957 | }, 958 | { 959 | "commit": "40188352bd74db962ac45ffa44ddda18d719a14b", 960 | "tree": "d6b2cfdab982ba30a14c58176e4dd04ec667569d", 961 | "parents": [ 962 | "1a6dd38b47790eaa9db23f4b66a379d8e2b981e8" 963 | ], 964 | "author": { 965 | "name": "Michael McGreevy", 966 | "email": "mcgreevy@golang.org", 967 | "time": "Tue Jan 05 15:11:57 2016 +1100" 968 | }, 969 | "committer": { 970 | "name": "Michael McGreevy", 971 | "email": "mcgreevy@golang.org", 972 | "time": "Tue Jan 05 04:34:50 2016 +0000" 973 | }, 974 | "message": "google-api-go-client: update all APIs.\n\nChange-Id: I0d5225c903edd5d7e1e7d15be3b7c121f6dbdd47\nReviewed-on: https://code-review.googlesource.com/3943\nReviewed-by: Dave Day \u003cdjd@golang.org\u003e\n" 975 | }, 976 | { 977 | "commit": "1a6dd38b47790eaa9db23f4b66a379d8e2b981e8", 978 | "tree": "cc10ef1fedde91dfa3f06a4e2ccdc249814eb7d2", 979 | "parents": [ 980 | "dc6d2353af16e2a2b0ff6986af051d473a4ed468" 981 | ], 982 | "author": { 983 | "name": "Michael McGreevy", 984 | "email": "mcgreevy@golang.org", 985 | "time": "Mon Jan 04 17:05:39 2016 +1100" 986 | }, 987 | "committer": { 988 | "name": "Michael McGreevy", 989 | "email": "mcgreevy@golang.org", 990 | "time": "Tue Jan 05 02:53:47 2016 +0000" 991 | }, 992 | "message": "generator: Remove a dependence on content size.\n\nResumable uploads store the content size to pass to the ProgressUpdater\ncallback. This refactoring change removes the need to store the size for\nthat purpose.\n\nChange-Id: I91e85584cb5451f33efca60925f3cec46625a646\nReviewed-on: https://code-review.googlesource.com/3942\nReviewed-by: Michael McGreevy \u003cmcgreevy@golang.org\u003e\n" 993 | }, 994 | { 995 | "commit": "dc6d2353af16e2a2b0ff6986af051d473a4ed468", 996 | "tree": "82bab4814565ac4f718ba5f692350dfd8af4e421", 997 | "parents": [ 998 | "fb9a762b1ba5eb828d4c3ea6b53755f290ca27ad" 999 | ], 1000 | "author": { 1001 | "name": "Michael McGreevy", 1002 | "email": "mcgreevy@golang.org", 1003 | "time": "Wed Dec 16 13:09:16 2015 +1100" 1004 | }, 1005 | "committer": { 1006 | "name": "Michael McGreevy", 1007 | "email": "mcgreevy@golang.org", 1008 | "time": "Thu Dec 17 00:24:15 2015 +0000" 1009 | }, 1010 | "message": "generator: support quotaUser,userIp parameters.\n\nThis change adds support for the commmon parameters \"quotaUser\" and\n\"userIp\" for APIs which support them.\n\nChange-Id: Ieffcf932bbbcc58642f6703f0a6f9ecfb907a88a\nReviewed-on: https://code-review.googlesource.com/3890\nReviewed-by: Brad Fitzpatrick \u003cbradfitz@golang.org\u003e\n" 1011 | }, 1012 | { 1013 | "commit": "fb9a762b1ba5eb828d4c3ea6b53755f290ca27ad", 1014 | "tree": "6fc3016950cca85ccf652f4321e77f2bf748e3e0", 1015 | "parents": [ 1016 | "80b32e23dd3e75da09373e0566232a3fe91ec43e" 1017 | ], 1018 | "author": { 1019 | "name": "Michael McGreevy", 1020 | "email": "mcgreevy@golang.org", 1021 | "time": "Mon Dec 14 10:25:01 2015 +1100" 1022 | }, 1023 | "committer": { 1024 | "name": "Michael McGreevy", 1025 | "email": "mcgreevy@golang.org", 1026 | "time": "Sun Dec 13 23:36:15 2015 +0000" 1027 | }, 1028 | "message": "all: update apis.\n\nChange-Id: Icbb1d36830ef7ff10b605df25c1c927753304f15\nReviewed-on: https://code-review.googlesource.com/3880\nReviewed-by: Dave Day \u003cdjd@golang.org\u003e\n" 1029 | }, 1030 | { 1031 | "commit": "80b32e23dd3e75da09373e0566232a3fe91ec43e", 1032 | "tree": "50c7220a591f053b30d02a1debd58313cbbadac9", 1033 | "parents": [ 1034 | "ac3c98838f3e512d7636b885f3a021b60dacd1c8" 1035 | ], 1036 | "author": { 1037 | "name": "Michael McGreevy", 1038 | "email": "mcgreevy@golang.org", 1039 | "time": "Fri Dec 11 15:56:33 2015 +1100" 1040 | }, 1041 | "committer": { 1042 | "name": "Michael McGreevy", 1043 | "email": "mcgreevy@golang.org", 1044 | "time": "Fri Dec 11 04:57:38 2015 +0000" 1045 | }, 1046 | "message": "storage: fix integration test by removing unconditional error.\n\nChange-Id: I8ee836b307883edb0ff15d99a05d66b1af3be044\nReviewed-on: https://code-review.googlesource.com/3862\nReviewed-by: Michael McGreevy \u003cmcgreevy@golang.org\u003e\n" 1047 | }, 1048 | { 1049 | "commit": "ac3c98838f3e512d7636b885f3a021b60dacd1c8", 1050 | "tree": "ad4e578ce96575c0a8c3752dcc62710d7c5e4689", 1051 | "parents": [ 1052 | "a5fc36f5bea02232ff3221875504c71a1de9a98b" 1053 | ], 1054 | "author": { 1055 | "name": "Michael McGreevy", 1056 | "email": "mcgreevy@golang.org", 1057 | "time": "Fri Dec 11 10:44:50 2015 +1100" 1058 | }, 1059 | "committer": { 1060 | "name": "Michael McGreevy", 1061 | "email": "mcgreevy@golang.org", 1062 | "time": "Thu Dec 10 23:47:49 2015 +0000" 1063 | }, 1064 | "message": "google-api-go-client: Don\u0027t strip body when doing resumable uploads.\n\nAlso add integration test for resumable uploads using the storage API.\n\nChange-Id: I4a9d85b55431975db0c17161766ab6f8af1e3c73\nReviewed-on: https://code-review.googlesource.com/3861\nReviewed-by: David Symonds \u003cdsymonds@golang.org\u003e\n" 1065 | }, 1066 | { 1067 | "commit": "a5fc36f5bea02232ff3221875504c71a1de9a98b", 1068 | "tree": "a0939570e5717fb8dfcc831a46bd247a4d28540e", 1069 | "parents": [ 1070 | "ece7143efeb53ec1839b960a0849db4e57d3cfa2" 1071 | ], 1072 | "author": { 1073 | "name": "Glenn Lewis", 1074 | "email": "gmlewis@google.com", 1075 | "time": "Thu Oct 29 19:53:30 2015 -0700" 1076 | }, 1077 | "committer": { 1078 | "name": "Dave Day", 1079 | "email": "djd@golang.org", 1080 | "time": "Thu Dec 10 23:42:22 2015 +0000" 1081 | }, 1082 | "message": "google-api-go-client: rollback commits due to licensing issues\n\nThis reverts the following commits:\nhttps://github.com/google/google-api-go-client/commit/661ac69e128b9f939c5b7b73b963a74808ec3220\nhttps://github.com/google/google-api-go-client/commit/fd68af889f2caf67cfe5eba4669f24fa2d9d2520\nhttps://github.com/google/google-api-go-client/commit/b036f299349024651e818cd2641fddfb9ddadf59\n\nChange-Id: I2b45d76c69bb97617cef44c71386e4ba5587e8c5\nReviewed-on: https://code-review.googlesource.com/3701\nReviewed-by: Dave Day \u003cdjd@golang.org\u003e\nReviewed-by: Brad Fitzpatrick \u003cbradfitz@golang.org\u003e\n" 1083 | }, 1084 | { 1085 | "commit": "ece7143efeb53ec1839b960a0849db4e57d3cfa2", 1086 | "tree": "a43d30b6096dac075beef02958329f7bc637dfc6", 1087 | "parents": [ 1088 | "030d584ade5f79aa2ed0ce067e8f7da50c9a10d5" 1089 | ], 1090 | "author": { 1091 | "name": "Glenn Lewis", 1092 | "email": "gmlewis@google.com", 1093 | "time": "Mon Nov 30 12:43:58 2015 -0800" 1094 | }, 1095 | "committer": { 1096 | "name": "Glenn Lewis", 1097 | "email": "gmlewis@google.com", 1098 | "time": "Mon Nov 30 21:09:38 2015 +0000" 1099 | }, 1100 | "message": "google-api-go-client: update all APIs\n\nChange-Id: I1c46a757151ef0b288982c6010027bb6bc5c90ee\nReviewed-on: https://code-review.googlesource.com/3800\nReviewed-by: Brad Fitzpatrick \u003cbradfitz@golang.org\u003e\n" 1101 | }, 1102 | { 1103 | "commit": "030d584ade5f79aa2ed0ce067e8f7da50c9a10d5", 1104 | "tree": "815633b4220384785e9e7cc76b3552aba6d4fd79", 1105 | "parents": [ 1106 | "b21645f24c3cc4fdbcd52c3b8ee7e43fb02a3953" 1107 | ], 1108 | "author": { 1109 | "name": "Glenn Lewis", 1110 | "email": "gmlewis@google.com", 1111 | "time": "Sun Nov 15 22:49:48 2015 -0800" 1112 | }, 1113 | "committer": { 1114 | "name": "Glenn Lewis", 1115 | "email": "gmlewis@google.com", 1116 | "time": "Mon Nov 16 23:50:37 2015 +0000" 1117 | }, 1118 | "message": "google-api-go-client: switch [] repeated params to ...\n\nChange-Id: I67a49d06f9c812c14092320f8e9706b0a2c656b2\nReviewed-on: https://code-review.googlesource.com/3758\nReviewed-by: Michael McGreevy \u003cmcgreevy@golang.org\u003e\n" 1119 | }, 1120 | { 1121 | "commit": "b21645f24c3cc4fdbcd52c3b8ee7e43fb02a3953", 1122 | "tree": "5db47abafa9fe2da9d0e60b3966b15f6e9bf7a0d", 1123 | "parents": [ 1124 | "4d6c29dd67d8f208c6924fb66ce80b12a0db1920" 1125 | ], 1126 | "author": { 1127 | "name": "Glenn Lewis", 1128 | "email": "gmlewis@google.com", 1129 | "time": "Sun Nov 15 21:11:34 2015 -0800" 1130 | }, 1131 | "committer": { 1132 | "name": "Glenn Lewis", 1133 | "email": "gmlewis@google.com", 1134 | "time": "Mon Nov 16 05:20:28 2015 +0000" 1135 | }, 1136 | "message": "google-api-go-client: update all APIs and test against Go 1.5, not tip\n\nChange-Id: Ifa7101c4ab09c9fe64c0343652e34375ac1bfa3c\nReviewed-on: https://code-review.googlesource.com/3757\nReviewed-by: Michael McGreevy \u003cmcgreevy@golang.org\u003e\n" 1137 | }, 1138 | { 1139 | "commit": "4d6c29dd67d8f208c6924fb66ce80b12a0db1920", 1140 | "tree": "1cb53cb6d73b797c6b938896c565a36cb5d69830", 1141 | "parents": [ 1142 | "c12ea1cd9ca9fc80d03f056ff6d1dea3eb41630c" 1143 | ], 1144 | "author": { 1145 | "name": "Glenn Lewis", 1146 | "email": "gmlewis@google.com", 1147 | "time": "Fri Nov 06 15:08:30 2015 -0800" 1148 | }, 1149 | "committer": { 1150 | "name": "Glenn Lewis", 1151 | "email": "gmlewis@google.com", 1152 | "time": "Mon Nov 16 04:51:22 2015 +0000" 1153 | }, 1154 | "message": "google-api-go-client: add support for \"repeated\" params\n\nFixes #105 and #110.\n\nRecommended reading order:\n1) google-api-go-generator/gen.go\n2) google-api-go-generator/gen_test.go\n3) internal/params.go\n4) google-api-go-generator/testdata/repeated.*\n5) google-api-go-generator/testdata/param-rename.*\n6) spot-check random APIs and/or other testdata/*\n\nThe problem with the last solution was that sometimes the generator\nchanges the argument name from the API name, and this was not being\nhandled properly. There is now a new test (param-rename.*) that\nchecks for the proper renaming of argument to API names.\n\nSpecifically, the only changes made since the last\nreview (apart from the new unit test) were to\nlines 1577-1585 (arg.goname \u003d\u003e arg.apiname) in\ngoogle-api-go-generator/gen.go.\n\nThe original implementation (which was reverted) and its review\nis located here: https://code-review.googlesource.com/#/c/3520/\nand the accidentally-submitted version is located here:\nhttps://code-review.googlesource.com/#/c/3610/\nwhich was also reverted.\n\nChange-Id: I8ccf11cd2ef62430beac842fb6ef237ef7c730ca\nReviewed-on: https://code-review.googlesource.com/3732\nReviewed-by: Michael McGreevy \u003cmcgreevy@golang.org\u003e\n" 1155 | }, 1156 | { 1157 | "commit": "c12ea1cd9ca9fc80d03f056ff6d1dea3eb41630c", 1158 | "tree": "6c95aef888fad6e23533e55ef3d4420c210491ee", 1159 | "parents": [ 1160 | "e4bb4e4880a5cabeec41cbff1095e86059f2f908" 1161 | ], 1162 | "author": { 1163 | "name": "Glenn Lewis", 1164 | "email": "gmlewis@google.com", 1165 | "time": "Fri Nov 13 15:05:10 2015 -0800" 1166 | }, 1167 | "committer": { 1168 | "name": "Glenn Lewis", 1169 | "email": "gmlewis@google.com", 1170 | "time": "Mon Nov 16 04:32:48 2015 +0000" 1171 | }, 1172 | "message": "google-api-go-client: update all APIs and remove old ones\n\nThe following APIs have been removed:\n container:v1beta1\n pubsub:v1beta1\n\nNote that the small change in google-api-go-generator/gen.go\nwill be addressed with unit tests in\nhttps://code-review.googlesource.com/#/c/3732/\n\nChange-Id: Ic978e4b2ba125e46d00c856b511a7a752dc0400d\nReviewed-on: https://code-review.googlesource.com/3756\nReviewed-by: Michael McGreevy \u003cmcgreevy@golang.org\u003e\n" 1173 | }, 1174 | { 1175 | "commit": "e4bb4e4880a5cabeec41cbff1095e86059f2f908", 1176 | "tree": "24b08df887d66896b92b20b9bb43ef07aa04aa8f", 1177 | "parents": [ 1178 | "baa070d77756cbc5be50095dfac3544e5b7b67d1" 1179 | ], 1180 | "author": { 1181 | "name": "Glenn Lewis", 1182 | "email": "gmlewis@google.com", 1183 | "time": "Wed Nov 11 21:56:56 2015 -0800" 1184 | }, 1185 | "committer": { 1186 | "name": "Glenn Lewis", 1187 | "email": "gmlewis@google.com", 1188 | "time": "Fri Nov 13 05:54:42 2015 +0000" 1189 | }, 1190 | "message": "google-api-go-client/integration_test: pass key to test for Travis CI\n\nChange-Id: If4bae655fa7f81159f497bb80f7802a5503ffb07\nReviewed-on: https://code-review.googlesource.com/3755\nReviewed-by: Michael McGreevy \u003cmcgreevy@golang.org\u003e\n" 1191 | }, 1192 | { 1193 | "commit": "baa070d77756cbc5be50095dfac3544e5b7b67d1", 1194 | "tree": "45f47631c223fdb84cbfb0ca2aae11b80108f1ac", 1195 | "parents": [ 1196 | "1b8ee7376ba9dcdc9ed4e9b701fa38ea9bddede3" 1197 | ], 1198 | "author": { 1199 | "name": "Glenn Lewis", 1200 | "email": "gmlewis@google.com", 1201 | "time": "Wed Nov 11 21:21:42 2015 -0800" 1202 | }, 1203 | "committer": { 1204 | "name": "Michael McGreevy", 1205 | "email": "mcgreevy@golang.org", 1206 | "time": "Thu Nov 12 05:27:52 2015 +0000" 1207 | }, 1208 | "message": "google-api-go-client: check all errors in storage integration_test.go\n\nChange-Id: I690cba230d666ecb9863fd8281d6c9e1a76d067d\nReviewed-on: https://code-review.googlesource.com/3754\nReviewed-by: Michael McGreevy \u003cmcgreevy@golang.org\u003e\n" 1209 | }, 1210 | { 1211 | "commit": "1b8ee7376ba9dcdc9ed4e9b701fa38ea9bddede3", 1212 | "tree": "6371da238ff8df3be18cd74076272454a25121d0", 1213 | "parents": [ 1214 | "890a90146f8ab6cb3cc95054b1f566400aea139a" 1215 | ], 1216 | "author": { 1217 | "name": "Glenn Lewis", 1218 | "email": "gmlewis@google.com", 1219 | "time": "Wed Nov 11 16:20:51 2015 -0800" 1220 | }, 1221 | "committer": { 1222 | "name": "Glenn Lewis", 1223 | "email": "gmlewis@google.com", 1224 | "time": "Thu Nov 12 04:48:39 2015 +0000" 1225 | }, 1226 | "message": "google-api-go-client: fix passing of env vars to Travis CI tests\n\nChange-Id: I05544e6e4372057b1dd14851da7f1f9835690c24\nReviewed-on: https://code-review.googlesource.com/3752\nReviewed-by: Michael McGreevy \u003cmcgreevy@golang.org\u003e\n" 1227 | }, 1228 | { 1229 | "commit": "890a90146f8ab6cb3cc95054b1f566400aea139a", 1230 | "tree": "cf8948735827dc25de60edf2e028397c0d2aff2f", 1231 | "parents": [ 1232 | "fd081149e482b10c55262756934088ffe3197ea3" 1233 | ], 1234 | "author": { 1235 | "name": "Glenn Lewis", 1236 | "email": "gmlewis@google.com", 1237 | "time": "Mon Oct 26 21:49:49 2015 -0700" 1238 | }, 1239 | "committer": { 1240 | "name": "Glenn Lewis", 1241 | "email": "gmlewis@google.com", 1242 | "time": "Thu Nov 12 00:17:44 2015 +0000" 1243 | }, 1244 | "message": "google-api-go-client: add start of integration tests for major APIs\n\nNOTE that any GCS bucket this test is run on will destroy *ALL*\ncontents of the bucket! Use only a test bucket for this integration\ntest!\n\nChange-Id: I25f3e01cd658786420859548927abb2b0e1effa1\nReviewed-on: https://code-review.googlesource.com/3680\nReviewed-by: Michael McGreevy \u003cmcgreevy@golang.org\u003e\n" 1245 | }, 1246 | { 1247 | "commit": "fd081149e482b10c55262756934088ffe3197ea3", 1248 | "tree": "44906e11acd0004678f580bab496bc5d22a2a7a0", 1249 | "parents": [ 1250 | "cd02e6c188e4cf3d6f0cdede8e86ac89e2c81702" 1251 | ], 1252 | "author": { 1253 | "name": "Michael McGreevy", 1254 | "email": "mcgreevy@golang.org", 1255 | "time": "Tue Nov 10 12:36:16 2015 +1100" 1256 | }, 1257 | "committer": { 1258 | "name": "Michael McGreevy", 1259 | "email": "mcgreevy@golang.org", 1260 | "time": "Tue Nov 10 02:23:05 2015 +0000" 1261 | }, 1262 | "message": "container/v1beta1: regenerate.\n\nChange-Id: I260b7c5253c632f6caa644a31389d3eeea747778\nReviewed-on: https://code-review.googlesource.com/3740\nReviewed-by: Glenn Lewis \u003cgmlewis@google.com\u003e\n" 1263 | }, 1264 | { 1265 | "commit": "cd02e6c188e4cf3d6f0cdede8e86ac89e2c81702", 1266 | "tree": "215e2362be9b189233e1444a1fc4c7848348e82c", 1267 | "parents": [ 1268 | "a42d61d9c2a5d4ec49ca228ecfcd5ad85843ac8c" 1269 | ], 1270 | "author": { 1271 | "name": "Dan Jacques", 1272 | "email": "dnj@google.com", 1273 | "time": "Sun Nov 01 09:34:18 2015 -0800" 1274 | }, 1275 | "committer": { 1276 | "name": "Michael McGreevy", 1277 | "email": "mcgreevy@golang.org", 1278 | "time": "Tue Nov 10 01:18:32 2015 +0000" 1279 | }, 1280 | "message": "google-api-go-client: external friendliness\n\nAdds some features to `google-api-go-client` to enable its use for\nservice generation outside of \"google.golang.org/api\". This allows\nthe generation of consistent code for other Cloud Endpoints services.\n\nAdded features:\n- Move remaining \"golang.google.org/api/internal\" code into \"gensupport\"\n so services resident in other packages can compile under Go 1.5.\n- Add `-base_url` flag to override the default service URL. This enables\n generation against local `dev_appserver.py` instances to use a base\n URL other than \"http://localhost:8080\".\n- Add `-header_path` optional flag, which, when supplied, is a path to a\n file whose contents will be prepended to generated stubs. This is\n useful for adding project licenses to generated code.\n- Fix a bug where generated code references to overridden package path\n require the overriding packages to use the default package\u0027s name.\n- Fix a bug where \"compute\" API was included when generating \u0027*\u0027 for any\n discovery URL, not just Google\u0027s.\n\nFixes #112.\n\nChange-Id: I16d0a5992fc41f958468620cbe13a9177414fe41\nReviewed-on: https://code-review.googlesource.com/3706\nReviewed-by: Michael McGreevy \u003cmcgreevy@golang.org\u003e\n" 1281 | }, 1282 | { 1283 | "commit": "a42d61d9c2a5d4ec49ca228ecfcd5ad85843ac8c", 1284 | "tree": "497358bdff6f040181d2a5e27e1290e4d77d05bd", 1285 | "parents": [ 1286 | "6e98bbb11ec677f838f4ccd2c573080d8fb45ebc" 1287 | ], 1288 | "author": { 1289 | "name": "Glenn Lewis", 1290 | "email": "gmlewis@google.com", 1291 | "time": "Fri Nov 06 16:43:17 2015 +0000" 1292 | }, 1293 | "committer": { 1294 | "name": "Glenn Lewis", 1295 | "email": "gmlewis@google.com", 1296 | "time": "Fri Nov 06 22:48:07 2015 +0000" 1297 | }, 1298 | "message": "Shoot - I meant to submit the integration tests first.\nReverting.\n\nRevert \"google-api-go-client: add support for \"repeated\" params\"\n\nThis reverts commit 6e98bbb11ec677f838f4ccd2c573080d8fb45ebc.\n\nChange-Id: I277fa1556107b4a08d14a22f004318bddce7164b\nReviewed-on: https://code-review.googlesource.com/3731\nReviewed-by: Glenn Lewis \u003cgmlewis@google.com\u003e\n" 1299 | }, 1300 | { 1301 | "commit": "6e98bbb11ec677f838f4ccd2c573080d8fb45ebc", 1302 | "tree": "a845302e3309b5e48d8df460746323fdb90b6498", 1303 | "parents": [ 1304 | "c83ee8e9b7e6c40a486c0992a963ea8b6911de67" 1305 | ], 1306 | "author": { 1307 | "name": "Glenn Lewis", 1308 | "email": "gmlewis@google.com", 1309 | "time": "Thu Oct 15 15:50:27 2015 -0700" 1310 | }, 1311 | "committer": { 1312 | "name": "Glenn Lewis", 1313 | "email": "gmlewis@google.com", 1314 | "time": "Fri Nov 06 16:42:26 2015 +0000" 1315 | }, 1316 | "message": "google-api-go-client: add support for \"repeated\" params\n\nFixes #105 and #110.\n\nRecommended reading order:\n1) google-api-go-generator/gen.go\n2) google-api-go-generator/gen_test.go\n3) internal/params.go\n4) google-api-go-generator/testdata/repeated.*\n5) google-api-go-generator/testdata/param-rename.*\n6) spot-check random APIs and/or other testdata/*\n\nThe problem with the last solution was that sometimes the generator\nchanges the argument name from the API name, and this was not being\nhandled properly. There is now a new test (param-rename.*) that\nchecks for the proper renaming of argument to API names.\n\nSpecifically, the only changes made since the last\nreview (apart from the new unit test) were to\nlines 1577-1585 (arg.goname \u003d\u003e arg.apiname) in\ngoogle-api-go-generator/gen.go.\n\nThe original implementation (which was reverted) and its review\nis located here: https://code-review.googlesource.com/#/c/3520/\n\nChange-Id: Iea5673a800f0e1aec4ad788fd2f621965d4386a1\nReviewed-on: https://code-review.googlesource.com/3610\nReviewed-by: Michael McGreevy \u003cmcgreevy@golang.org\u003e\n" 1317 | }, 1318 | { 1319 | "commit": "c83ee8e9b7e6c40a486c0992a963ea8b6911de67", 1320 | "tree": "497358bdff6f040181d2a5e27e1290e4d77d05bd", 1321 | "parents": [ 1322 | "73781a6ca4d12290ff2db685952206a502e5fd41" 1323 | ], 1324 | "author": { 1325 | "name": "Glenn Lewis", 1326 | "email": "gmlewis@google.com", 1327 | "time": "Fri Oct 16 22:06:20 2015 -0700" 1328 | }, 1329 | "committer": { 1330 | "name": "Glenn Lewis", 1331 | "email": "gmlewis@google.com", 1332 | "time": "Sat Oct 17 18:01:37 2015 +0000" 1333 | }, 1334 | "message": "google-api-go-client: update all APIs\n\nRequested by sqladmin team.\n\nChange-Id: Ifd7f163af74a099acb82d1985095c46fc90c39c2\nReviewed-on: https://code-review.googlesource.com/3620\nReviewed-by: Brad Fitzpatrick \u003cbradfitz@golang.org\u003e\n" 1335 | }, 1336 | { 1337 | "commit": "73781a6ca4d12290ff2db685952206a502e5fd41", 1338 | "tree": "79fd9f9a25164acde9543e9b4fbe7a52ab2d0d3b", 1339 | "parents": [ 1340 | "e2903ca9e33d6cbaedda541d96996219056e8214" 1341 | ], 1342 | "author": { 1343 | "name": "Michael McGreevy", 1344 | "email": "mcgreevy@golang.org", 1345 | "time": "Tue Oct 13 16:44:16 2015 +1100" 1346 | }, 1347 | "committer": { 1348 | "name": "Michael McGreevy", 1349 | "email": "mcgreevy@golang.org", 1350 | "time": "Fri Oct 16 00:49:47 2015 +0000" 1351 | }, 1352 | "message": "all: make imported package paths configurable.\n\nThis facilitates building the APIs within a custom build environment,\nsuch as the one used by Google.\n\nChange-Id: I76c110d493813d1944dd99a39eb05793914d961c\nReviewed-on: https://code-review.googlesource.com/3576\nReviewed-by: David Symonds \u003cdsymonds@golang.org\u003e\nReviewed-by: Glenn Lewis \u003cgmlewis@google.com\u003e\nReviewed-by: Brad Fitzpatrick \u003cbradfitz@golang.org\u003e\n" 1353 | }, 1354 | { 1355 | "commit": "e2903ca9e33d6cbaedda541d96996219056e8214", 1356 | "tree": "2fe12ae98d473869404b30193ea9624f7dff5825", 1357 | "parents": [ 1358 | "69e492629e49bc7ff3c791ac7630836813268b45" 1359 | ], 1360 | "author": { 1361 | "name": "Glenn Lewis", 1362 | "email": "gmlewis@google.com", 1363 | "time": "Thu Oct 15 03:48:07 2015 +0000" 1364 | }, 1365 | "committer": { 1366 | "name": "Michael McGreevy", 1367 | "email": "mcgreevy@golang.org", 1368 | "time": "Thu Oct 15 03:51:45 2015 +0000" 1369 | }, 1370 | "message": "Revert \"google-api-go-client: add support for \"repeated\" params\"\n\nThis reverts commit 69e492629e49bc7ff3c791ac7630836813268b45.\n\nFixes #110.\n\nChange-Id: I00c32ad2865fa2eef667b2a97261f920317ca6fd\nReviewed-on: https://code-review.googlesource.com/3592\nReviewed-by: Michael McGreevy \u003cmcgreevy@golang.org\u003e\n" 1371 | }, 1372 | { 1373 | "commit": "69e492629e49bc7ff3c791ac7630836813268b45", 1374 | "tree": "a7a5b1bb6a517797586ade97eaa3f3ddb29c9a3a", 1375 | "parents": [ 1376 | "5be63c51278ae030647b2c8f5b4cbf99f3559f5d" 1377 | ], 1378 | "author": { 1379 | "name": "Glenn Lewis", 1380 | "email": "gmlewis@google.com", 1381 | "time": "Wed Sep 30 14:45:43 2015 -0700" 1382 | }, 1383 | "committer": { 1384 | "name": "Glenn Lewis", 1385 | "email": "gmlewis@google.com", 1386 | "time": "Wed Oct 14 22:55:46 2015 +0000" 1387 | }, 1388 | "message": "google-api-go-client: add support for \"repeated\" params\n\nFixes #105.\n\nRecommended reading order:\n1) google-api-go-generator/gen.go\n2) google-api-go-generator/gen_test.go\n3) internal/params.go\n4) google-api-go-generator/testdata/repeated.*\n5) spot-check random APIs and/or other testdata/*\n\nChange-Id: I69f453afcba8d4e724a46ed2fa0e2b7197b4c8b9\nReviewed-on: https://code-review.googlesource.com/3520\nReviewed-by: Michael McGreevy \u003cmcgreevy@golang.org\u003e\n" 1389 | }, 1390 | { 1391 | "commit": "5be63c51278ae030647b2c8f5b4cbf99f3559f5d", 1392 | "tree": "2fe12ae98d473869404b30193ea9624f7dff5825", 1393 | "parents": [ 1394 | "bce17bfa8913f8777eb5221beb361c03edf5817e" 1395 | ], 1396 | "author": { 1397 | "name": "John ShaggyTwoDope Jenkins", 1398 | "email": "twodopeshaggy@gmail.com", 1399 | "time": "Tue Oct 13 14:28:12 2015 -0700" 1400 | }, 1401 | "committer": { 1402 | "name": "Glenn Lewis", 1403 | "email": "gmlewis@google.com", 1404 | "time": "Tue Oct 13 23:38:39 2015 +0000" 1405 | }, 1406 | "message": "small typo in googleapi_test.go\n\nChange-Id: Ic49745dca9f4958bba21e21badb8ccd11ccb7892\nReviewed-on: https://code-review.googlesource.com/3591\nReviewed-by: Glenn Lewis \u003cgmlewis@google.com\u003e\n" 1407 | }, 1408 | { 1409 | "commit": "bce17bfa8913f8777eb5221beb361c03edf5817e", 1410 | "tree": "3def7fabaf9416e21c48573ea337bbcd74864be3", 1411 | "parents": [ 1412 | "7f3774db0aa426a2390fa9abcccbc272dbb93111" 1413 | ], 1414 | "author": { 1415 | "name": "Glenn Lewis", 1416 | "email": "gmlewis@google.com", 1417 | "time": "Mon Oct 05 11:54:10 2015 -0700" 1418 | }, 1419 | "committer": { 1420 | "name": "Glenn Lewis", 1421 | "email": "gmlewis@google.com", 1422 | "time": "Thu Oct 08 19:46:11 2015 +0000" 1423 | }, 1424 | "message": "google-api-go-client: make apiPackageBase a flag\n\nhttps://github.com/google/google-api-go-client/commit/d604dad6\nadded the following line to google-api-go-generator/gen:\n\n const apiPackageBase \u003d \"google.golang.org/api\"\n\nwhich hard-codes the location of this collection of APIs to\na base of \"google.golang.org/api/...\".\nThis breaks the use of this collection of APIs within a custom\nbuild environment, such as the one used by Google.\n\nThis change turns \"apiPackageBase\" into a flag so that it can\nbe used in custom build environments.\n\nNote that if you used to use the flag: -googleapi_pkg\u003dsome/path/googleapi\nyou will now instead use the new flag: -api_pkg_base\u003dsome/path\n\nChange-Id: Iacb66f8374addfbba7060d2a58807a39924da45f\nReviewed-on: https://code-review.googlesource.com/3540\nReviewed-by: Brad Fitzpatrick \u003cbradfitz@golang.org\u003e\n" 1425 | }, 1426 | { 1427 | "commit": "7f3774db0aa426a2390fa9abcccbc272dbb93111", 1428 | "tree": "7951824545c7a4d844e261ccd1ccf69d1fb8f69d", 1429 | "parents": [ 1430 | "09c6521081a8b11e2c277b747b24324cfdfa5c25" 1431 | ], 1432 | "author": { 1433 | "name": "Glenn Lewis", 1434 | "email": "gmlewis@google.com", 1435 | "time": "Fri Sep 04 21:43:50 2015 -0700" 1436 | }, 1437 | "committer": { 1438 | "name": "Glenn Lewis", 1439 | "email": "gmlewis@google.com", 1440 | "time": "Fri Oct 02 17:49:24 2015 +0000" 1441 | }, 1442 | "message": "google-api-go-client: add If-None-Match support\n\nFixes #86.\n\nNote that examples/storage.go demonstrates the use of the new\nIf-None-Match support.\n\nAdditionally, this change allows the headers of all responses\nto be inspected so that \"ETag\", for example, could be extracted\nfrom the response. This could be especially useful for the\nCompute API and other APIs that do not explicitly return ETag\nwithin the body of their responses.\n\nNote also that examples/compute.go now demonstrates the use of the\nnew googleapi.ServiceResponse and \"ETag\" extraction from the header.\n\nChange-Id: Ic6fc35c6bdd4aca87ba0ee51b59f94b8de34d5ea\nReviewed-on: https://code-review.googlesource.com/3375\nReviewed-by: Glenn Lewis \u003cgmlewis@google.com\u003e\n" 1443 | }, 1444 | { 1445 | "commit": "09c6521081a8b11e2c277b747b24324cfdfa5c25", 1446 | "tree": "7926f3266fea324728432c7b4b0d96db5c8cc09a", 1447 | "parents": [ 1448 | "a4024444ccab011ed7d21b11f38f990a7bfdb4c6" 1449 | ], 1450 | "author": { 1451 | "name": "Glenn Lewis", 1452 | "email": "gmlewis@google.com", 1453 | "time": "Tue Sep 29 08:39:37 2015 -0700" 1454 | }, 1455 | "committer": { 1456 | "name": "Glenn Lewis", 1457 | "email": "gmlewis@google.com", 1458 | "time": "Tue Sep 29 16:38:52 2015 +0000" 1459 | }, 1460 | "message": "google-api-go-client: add canonical import paths\n\nhttps://golang.org/s/go14customimport\n\nFixes #106.\n\nChange-Id: I49516da7293f76f62e3a534b360be284681b7107\nReviewed-on: https://code-review.googlesource.com/3511\nReviewed-by: Brad Fitzpatrick \u003cbradfitz@golang.org\u003e\n" 1461 | }, 1462 | { 1463 | "commit": "a4024444ccab011ed7d21b11f38f990a7bfdb4c6", 1464 | "tree": "3a49cb558999de8600d11aa2a1d1c44fcd529400", 1465 | "parents": [ 1466 | "78786a0c27510823260988be047c0c5f6a26dee5" 1467 | ], 1468 | "author": { 1469 | "name": "Glenn Lewis", 1470 | "email": "gmlewis@google.com", 1471 | "time": "Tue Sep 29 08:51:40 2015 -0700" 1472 | }, 1473 | "committer": { 1474 | "name": "Glenn Lewis", 1475 | "email": "gmlewis@google.com", 1476 | "time": "Tue Sep 29 16:15:19 2015 +0000" 1477 | }, 1478 | "message": "google-api-go-client: fix breakage caused by d604dad\n\nChange-Id: Iad8de83866bc60c5265d6dd784482e18a964bac9\nReviewed-on: https://code-review.googlesource.com/3510\nReviewed-by: Brad Fitzpatrick \u003cbradfitz@golang.org\u003e\n" 1479 | }, 1480 | { 1481 | "commit": "78786a0c27510823260988be047c0c5f6a26dee5", 1482 | "tree": "313bbc6bf19723d8b9f7da9ab478a1f7381fd230", 1483 | "parents": [ 1484 | "253d40011ee46ae2f022f74886564cdf64b7f5ef" 1485 | ], 1486 | "author": { 1487 | "name": "Michael McGreevy", 1488 | "email": "mcgreevy@golang.org", 1489 | "time": "Mon Sep 07 11:58:22 2015 +1000" 1490 | }, 1491 | "committer": { 1492 | "name": "Michael McGreevy", 1493 | "email": "mcgreevy@golang.org", 1494 | "time": "Tue Sep 29 06:21:44 2015 +0000" 1495 | }, 1496 | "message": "google-api-go-client: Use pointers for Point values in cloud monitoring.\n\nFixes #54.\n\nChange-Id: Ic525fa520f7295604fe546c01ff829e41bb48071\nReviewed-on: https://code-review.googlesource.com/3384\nReviewed-by: Michael McGreevy \u003cmcgreevy@golang.org\u003e\n" 1497 | }, 1498 | { 1499 | "commit": "253d40011ee46ae2f022f74886564cdf64b7f5ef", 1500 | "tree": "6d22fb2285ddbfcf4735393800380d1a41adcbe9", 1501 | "parents": [ 1502 | "d604dad6fa7f098a8ac36a19598c161acf70c5c3" 1503 | ], 1504 | "author": { 1505 | "name": "Michael McGreevy", 1506 | "email": "mcgreevy@golang.org", 1507 | "time": "Tue Sep 29 15:54:41 2015 +1000" 1508 | }, 1509 | "committer": { 1510 | "name": "Michael McGreevy", 1511 | "email": "mcgreevy@golang.org", 1512 | "time": "Tue Sep 29 06:17:36 2015 +0000" 1513 | }, 1514 | "message": "generator: remove check for string-encoded pointer fields.\n\nencoding/json handles them correctly, as does the ForceSendFields code.\n\nThis change also makes Property.IntegerValue a pointer field for the\ndatastore:v1beta2 API.\n\nAlso fix json_test so that the PIStr field name matches that in the\nForceSendFields list. The test was not broken since PIStr is a pointer\nfield, and ForceSendFields has no effect on it, but it\u0027s nice to be\nconsistent.\n\nChange-Id: Ia5a060c54bef1a5597edfbf716bdd95b02b690fa\nReviewed-on: https://code-review.googlesource.com/3501\nReviewed-by: Dave Day \u003cdjd@golang.org\u003e\nReviewed-by: Michael McGreevy \u003cmcgreevy@golang.org\u003e\n" 1515 | }, 1516 | { 1517 | "commit": "d604dad6fa7f098a8ac36a19598c161acf70c5c3", 1518 | "tree": "bcae7f670c2d90d76de41b0f329b05a33b2f237e", 1519 | "parents": [ 1520 | "77b0a8cf996ff3872eaa73c089d272613e63204c" 1521 | ], 1522 | "author": { 1523 | "name": "Michael McGreevy", 1524 | "email": "mcgreevy@golang.org", 1525 | "time": "Mon Sep 07 16:48:35 2015 +1000" 1526 | }, 1527 | "committer": { 1528 | "name": "Michael McGreevy", 1529 | "email": "mcgreevy@golang.org", 1530 | "time": "Mon Sep 28 23:30:39 2015 +0000" 1531 | }, 1532 | "message": "all: add ForceSendFields support\n\nAllow users to nominate fields that should be included in requests to\nthe server, regardless of field contents. This makes it possible to\nperform patch requests involving fields with emtpy values -- a common\nuse case.\n\nThis change will also enable immediate workarounds for APIs which expect\nvalues to be present for particular fields in update requests (such as\nthose in #54).\n\nThe custom JSON marshaling code introduced here is applicable to both\nupdate and patch payloads, and extends naturally to hierarchical\nschemas.\n\nNote: this change will result in non-deterministic ordering of fields in the JSON\nrepresentation of schemas, due to the intermediate use of a map.\n\nChange-Id: I141cfabcbd500ca15b512cf17665344ba4a66887\nReviewed-on: https://code-review.googlesource.com/3386\nReviewed-by: Michael McGreevy \u003cmcgreevy@golang.org\u003e\n" 1533 | }, 1534 | { 1535 | "commit": "77b0a8cf996ff3872eaa73c089d272613e63204c", 1536 | "tree": "e5be21dc68f9c50d02bd6e6bf101b87e5ef0ea30", 1537 | "parents": [ 1538 | "b82cbee675293441769a97b99710295d58dac769" 1539 | ], 1540 | "author": { 1541 | "name": "Dave Day", 1542 | "email": "djd@golang.org", 1543 | "time": "Tue Sep 22 12:55:28 2015 +1000" 1544 | }, 1545 | "committer": { 1546 | "name": "Michael McGreevy", 1547 | "email": "mcgreevy@golang.org", 1548 | "time": "Mon Sep 28 00:17:16 2015 +0000" 1549 | }, 1550 | "message": "Add golang.org/x/net/context support.\n\nAdd a Context method to every call object which associates a context\nwith the call\u0027s Do (and Download) method. Make sure all HTTP calls\nare made context-aware when a context is available.\n\nChange-Id: I493cce4d43411810eb30b6c02dc151ef970a6254\nReviewed-on: https://code-review.googlesource.com/3472\nReviewed-by: Michael McGreevy \u003cmcgreevy@golang.org\u003e\n" 1551 | }, 1552 | { 1553 | "commit": "b82cbee675293441769a97b99710295d58dac769", 1554 | "tree": "50c0ed4f53bdffb221a52f760b1bf96f1ed36f8a", 1555 | "parents": [ 1556 | "ebc7799043c9e8d3c71fedcb958ac491caad03d4" 1557 | ], 1558 | "author": { 1559 | "name": "Glenn Lewis", 1560 | "email": "gmlewis@google.com", 1561 | "time": "Tue Sep 22 16:01:42 2015 -0700" 1562 | }, 1563 | "committer": { 1564 | "name": "Glenn Lewis", 1565 | "email": "gmlewis@google.com", 1566 | "time": "Wed Sep 23 14:17:06 2015 +0000" 1567 | }, 1568 | "message": "google-api-go-client: update all APIs\n\nThis was requested by the script:v1 team.\n\nChange-Id: Idf87e2bb3ef4f76b84cf3ffb09ea7f9d2cecc0e6\nReviewed-on: https://code-review.googlesource.com/3464\nReviewed-by: Brad Fitzpatrick \u003cbradfitz@golang.org\u003e\n" 1569 | }, 1570 | { 1571 | "commit": "ebc7799043c9e8d3c71fedcb958ac491caad03d4", 1572 | "tree": "f210f047d12b2f29e548ca63aaf1463bc89b8850", 1573 | "parents": [ 1574 | "ed920dd8e2feb14c95aa03d78c8057e02ef61190" 1575 | ], 1576 | "author": { 1577 | "name": "Glenn Lewis", 1578 | "email": "gmlewis@google.com", 1579 | "time": "Fri Sep 18 10:01:55 2015 -0700" 1580 | }, 1581 | "committer": { 1582 | "name": "Glenn Lewis", 1583 | "email": "gmlewis@google.com", 1584 | "time": "Fri Sep 18 20:44:04 2015 +0000" 1585 | }, 1586 | "message": "google-api-go-client: update all APIs\n\nRequested by clouduseraccounts API team.\n\nChange-Id: I36afb21396ecd091f8c8b0858601c124b479d19a\nReviewed-on: https://code-review.googlesource.com/3463\nReviewed-by: Brad Fitzpatrick \u003cbradfitz@golang.org\u003e\n" 1587 | }, 1588 | { 1589 | "commit": "ed920dd8e2feb14c95aa03d78c8057e02ef61190", 1590 | "tree": "70387211ddaccf7f55d2f49617cc8da835fba4c4", 1591 | "parents": [ 1592 | "8cbf97d5e925c857511aedf459affeed490e6861" 1593 | ], 1594 | "author": { 1595 | "name": "Glenn Lewis", 1596 | "email": "gmlewis@google.com", 1597 | "time": "Thu Sep 17 11:50:45 2015 -0700" 1598 | }, 1599 | "committer": { 1600 | "name": "Glenn Lewis", 1601 | "email": "gmlewis@google.com", 1602 | "time": "Thu Sep 17 19:26:59 2015 +0000" 1603 | }, 1604 | "message": "google-api-go-client: resurrect deleted container:v1beta1 API\n\nThese two files were extracted from git with the following commands:\n$ git show f9554945d4885e1b1fe619ee7cd306203fb0f1b7:container/v1beta1/container-api.json \u003e container/v1beta1/container-api.json\n$ git show f9554945d4885e1b1fe619ee7cd306203fb0f1b7:container/v1beta1/container-gen.go \u003e container/v1beta1/container-gen.go\n\nChange-Id: I8c01d280199f93e20616c61408b5db747a2cef81\nReviewed-on: https://code-review.googlesource.com/3445\nReviewed-by: Brad Fitzpatrick \u003cbradfitz@golang.org\u003e\n" 1605 | }, 1606 | { 1607 | "commit": "8cbf97d5e925c857511aedf459affeed490e6861", 1608 | "tree": "2ede1d17dbcdcca092c669ae52b7bac0c5a536e8", 1609 | "parents": [ 1610 | "e7c0934161b3fabbb4495f81509a646aeae54f1b" 1611 | ], 1612 | "author": { 1613 | "name": "Glenn Lewis", 1614 | "email": "gmlewis@google.com", 1615 | "time": "Wed Jul 29 10:54:04 2015 -0700" 1616 | }, 1617 | "committer": { 1618 | "name": "Glenn Lewis", 1619 | "email": "gmlewis@google.com", 1620 | "time": "Fri Sep 11 01:10:17 2015 +0000" 1621 | }, 1622 | "message": "google-api-go-client: add Application Default Credential example\n\nI\u0027ve copied the example that Chris wrote here:\nhttps://developers.google.com/identity/protocols/application-default-credentials#callinggo\n\nFixes #89\n\nChange-Id: Ib565a0280f6b42151d7876e1a50a0d5d416e2e90\nReviewed-on: https://code-review.googlesource.com/3263\nReviewed-by: Brad Fitzpatrick \u003cbradfitz@golang.org\u003e\n" 1623 | }, 1624 | { 1625 | "commit": "e7c0934161b3fabbb4495f81509a646aeae54f1b", 1626 | "tree": "6f9989881ee8edbcd331103ce2c658c60e94e3cd", 1627 | "parents": [ 1628 | "85c92808928b0629b3532cefeed044f09c1a7c67" 1629 | ], 1630 | "author": { 1631 | "name": "Glenn Lewis", 1632 | "email": "gmlewis@google.com", 1633 | "time": "Thu Sep 10 10:18:16 2015 -0700" 1634 | }, 1635 | "committer": { 1636 | "name": "Glenn Lewis", 1637 | "email": "gmlewis@google.com", 1638 | "time": "Thu Sep 10 21:17:28 2015 +0000" 1639 | }, 1640 | "message": "google-api-go-client: update all APIs\n\nChange-Id: If32e320fc93c1118a8e36f629dc1db12cbc89e23\nReviewed-on: https://code-review.googlesource.com/3411\nReviewed-by: Brad Fitzpatrick \u003cbradfitz@golang.org\u003e\n" 1641 | }, 1642 | { 1643 | "commit": "85c92808928b0629b3532cefeed044f09c1a7c67", 1644 | "tree": "87102a9256029fef3514c56a940bb777640d1e67", 1645 | "parents": [ 1646 | "f9554945d4885e1b1fe619ee7cd306203fb0f1b7" 1647 | ], 1648 | "author": { 1649 | "name": "Glenn Lewis", 1650 | "email": "gmlewis@google.com", 1651 | "time": "Tue Sep 08 10:27:54 2015 -0700" 1652 | }, 1653 | "committer": { 1654 | "name": "Glenn Lewis", 1655 | "email": "gmlewis@google.com", 1656 | "time": "Tue Sep 08 17:36:36 2015 +0000" 1657 | }, 1658 | "message": "google-api-go-client: update all APIs\n\nChange-Id: Ic72bbb5924295d9aeab7d4f6ba05115b6f5b857d\nReviewed-on: https://code-review.googlesource.com/3378\nReviewed-by: Brad Fitzpatrick \u003cbradfitz@golang.org\u003e\n" 1659 | }, 1660 | { 1661 | "commit": "f9554945d4885e1b1fe619ee7cd306203fb0f1b7", 1662 | "tree": "5dac50514a7b6f7253fd6c472dc36c4a7c34e006", 1663 | "parents": [ 1664 | "09c6edb917cedacf98ad78257552abedabb5f5c8" 1665 | ], 1666 | "author": { 1667 | "name": "Michael McGreevy", 1668 | "email": "mcgreevy@golang.org", 1669 | "time": "Mon Sep 07 14:27:35 2015 +1000" 1670 | }, 1671 | "committer": { 1672 | "name": "Michael McGreevy", 1673 | "email": "mcgreevy@golang.org", 1674 | "time": "Mon Sep 07 04:33:06 2015 +0000" 1675 | }, 1676 | "message": "google-api-go-generator: Fix broken IntegerValue field in datastore API.\n\nIntervalue is string-encoded, which does not play well with the change\nto make IntegerValue a pointer field.\n\nChange-Id: I7a8fbe21b599e0b7de25073920bbc611f1ba35bc\nReviewed-on: https://code-review.googlesource.com/3385\nReviewed-by: Dave Day \u003cdjd@golang.org\u003e\nReviewed-by: Michael McGreevy \u003cmcgreevy@golang.org\u003e\n" 1677 | }, 1678 | { 1679 | "commit": "09c6edb917cedacf98ad78257552abedabb5f5c8", 1680 | "tree": "4831ff68ea193733b8a28c06e5ba8d30be9b95d9", 1681 | "parents": [ 1682 | "2b9259b4009849e00be1bdbc6e44fd880550f19f" 1683 | ], 1684 | "author": { 1685 | "name": "Glenn Lewis", 1686 | "email": "gmlewis@google.com", 1687 | "time": "Thu Aug 27 19:55:27 2015 -0700" 1688 | }, 1689 | "committer": { 1690 | "name": "Glenn Lewis", 1691 | "email": "gmlewis@google.com", 1692 | "time": "Sat Sep 05 16:29:33 2015 +0000" 1693 | }, 1694 | "message": "google-api-go-client: clean up README.md and remove tabs.\n\nChange-Id: Icf4b4d9ed8051162c8d8b4c6c3af4ea1f42637b7\nReviewed-on: https://code-review.googlesource.com/3352\nReviewed-by: Brad Fitzpatrick \u003cbradfitz@golang.org\u003e\n" 1695 | }, 1696 | { 1697 | "commit": "2b9259b4009849e00be1bdbc6e44fd880550f19f", 1698 | "tree": "e49b09c8a7f9ff65116e8d973306f3ece2c7d56a", 1699 | "parents": [ 1700 | "636998d1deefd6bd0cd18f6dd7d8d1e04fc163fc" 1701 | ], 1702 | "author": { 1703 | "name": "Glenn Lewis", 1704 | "email": "gmlewis@google.com", 1705 | "time": "Fri Sep 04 22:15:34 2015 -0700" 1706 | }, 1707 | "committer": { 1708 | "name": "Glenn Lewis", 1709 | "email": "gmlewis@google.com", 1710 | "time": "Sat Sep 05 16:29:03 2015 +0000" 1711 | }, 1712 | "message": "google-api-go-client: restore accidentally removed tests\n\nI commented out most of the tests in:\nhttps://github.com/google/google-api-go-client/commit/ca0499560ea76ac6561548f36ffe841364fe2348\nI apologize. This restores the tests and updates them.\n\nChange-Id: I66a235d244517143d90c20b30707b0456e2f6b56\nReviewed-on: https://code-review.googlesource.com/3376\nReviewed-by: Brad Fitzpatrick \u003cbradfitz@golang.org\u003e\n" 1713 | }, 1714 | { 1715 | "commit": "636998d1deefd6bd0cd18f6dd7d8d1e04fc163fc", 1716 | "tree": "46c82fd32e69c1f945d7aae9552138267f91b11f", 1717 | "parents": [ 1718 | "381e13c897e25a83973e3ea8beed668ff6f46805" 1719 | ], 1720 | "author": { 1721 | "name": "Michael McGreevy", 1722 | "email": "mcgreevy@golang.org", 1723 | "time": "Fri Sep 04 11:58:43 2015 +1000" 1724 | }, 1725 | "committer": { 1726 | "name": "Glenn Lewis", 1727 | "email": "gmlewis@google.com", 1728 | "time": "Fri Sep 04 14:16:26 2015 +0000" 1729 | }, 1730 | "message": "google-api-go-client: Use pointers for property values in datastore.\n\nFixes bug mentioned in #61 (which is dupe of #54).\n\nChange-Id: I82a5202ea98abaf211e5857693ede0ad2c0e3f5b\nReviewed-on: https://code-review.googlesource.com/3383\nReviewed-by: Dave Day \u003cdjd@golang.org\u003e\nReviewed-by: Glenn Lewis \u003cgmlewis@google.com\u003e\n" 1731 | }, 1732 | { 1733 | "commit": "381e13c897e25a83973e3ea8beed668ff6f46805", 1734 | "tree": "17fb345dbc9952297d91fde38828a87b79b9babe", 1735 | "parents": [ 1736 | "146d5758d64a078e697703fc482cec7293afe3d7" 1737 | ], 1738 | "author": { 1739 | "name": "Michael McGreevy", 1740 | "email": "mcgreevy@golang.org", 1741 | "time": "Thu Sep 03 16:25:09 2015 +1000" 1742 | }, 1743 | "committer": { 1744 | "name": "Michael McGreevy", 1745 | "email": "mcgreevy@golang.org", 1746 | "time": "Thu Sep 03 21:03:35 2015 +0000" 1747 | }, 1748 | "message": "google-api-go-client: Force use of some pointer fields.\n\nThis fixes most of the issues in mentioned in #54, but I need to dig a\nbit further into datastore (specifically the example mentioned in #61)\nand cloud monitoring to see if there are extra fields that need the same\ntreatment.\n\nChange-Id: I462336d31fef8ca7a87ab45494e6f1733a6dac1c\nReviewed-on: https://code-review.googlesource.com/3382\nReviewed-by: Glenn Lewis \u003cgmlewis@google.com\u003e\n" 1749 | }, 1750 | { 1751 | "commit": "146d5758d64a078e697703fc482cec7293afe3d7", 1752 | "tree": "2a547f3197927221ab2e787f6e42c0df00968b74", 1753 | "parents": [ 1754 | "4af91da60108e4a6d2547bb4adce3f611c4d1f20" 1755 | ], 1756 | "author": { 1757 | "name": "Michael McGreevy", 1758 | "email": "mcgreevy@golang.org", 1759 | "time": "Mon Aug 31 16:45:02 2015 +1000" 1760 | }, 1761 | "committer": { 1762 | "name": "Michael McGreevy", 1763 | "email": "mcgreevy@golang.org", 1764 | "time": "Mon Aug 31 23:55:02 2015 +0000" 1765 | }, 1766 | "message": "google-api-go-generator: add helpers for converting basic types to pointers.\n\nThese helpers are derived from github.com/golang/protobuf/proto.\nFloat32 has been dropped, as float32 does not appear in the googleapi\ngenerated code.\n\nChange-Id: Ifc36c15c1b254370dbeaf243f9fbcfc93440b0a3\nReviewed-on: https://code-review.googlesource.com/3363\nReviewed-by: Michael McGreevy \u003cmcgreevy@golang.org\u003e\n" 1767 | }, 1768 | { 1769 | "commit": "4af91da60108e4a6d2547bb4adce3f611c4d1f20", 1770 | "tree": "cb630b06e9791cea62ab3d1101d5e8cdf7b6ad90", 1771 | "parents": [ 1772 | "a5c3e2a4792aff40e59840d9ecdff0542a202a80" 1773 | ], 1774 | "author": { 1775 | "name": "Michael McGreevy", 1776 | "email": "mcgreevy@golang.org", 1777 | "time": "Tue Jun 23 16:54:01 2015 +1000" 1778 | }, 1779 | "committer": { 1780 | "name": "Michael McGreevy", 1781 | "email": "mcgreevy@golang.org", 1782 | "time": "Mon Aug 31 01:35:35 2015 +0000" 1783 | }, 1784 | "message": "google-api-go-generator: Add the ability to use pointer types for a\nspecific list of API fields.\n\nChange-Id: Iebef16ff5a7cc57141021caf41ed46a974a85742\nReviewed-on: https://code-review.googlesource.com/2907\nReviewed-by: Glenn Lewis \u003cgmlewis@google.com\u003e\nReviewed-by: Brad Fitzpatrick \u003cbradfitz@golang.org\u003e\n" 1785 | }, 1786 | { 1787 | "commit": "a5c3e2a4792aff40e59840d9ecdff0542a202a80", 1788 | "tree": "f7c8a755c0dc1483ceb95dbaa4893ee42e19fc28", 1789 | "parents": [ 1790 | "0bacdc65dfd3dae28a124e21ecebb6f3f3c22087" 1791 | ], 1792 | "author": { 1793 | "name": "Glenn Lewis", 1794 | "email": "gmlewis@google.com", 1795 | "time": "Fri Aug 07 21:35:18 2015 -0700" 1796 | }, 1797 | "committer": { 1798 | "name": "Glenn Lewis", 1799 | "email": "gmlewis@google.com", 1800 | "time": "Thu Aug 20 04:12:44 2015 +0000" 1801 | }, 1802 | "message": "google-api-go-client: update all APIs.\n\nThis fixes issue #94.\n\nChange-Id: Ib9129331170389eb71881077cc32f25535039fdb\nReviewed-on: https://code-review.googlesource.com/3310\nReviewed-by: Michael McGreevy \u003cmcgreevy@golang.org\u003e\n" 1803 | } 1804 | ], 1805 | "next": "0bacdc65dfd3dae28a124e21ecebb6f3f3c22087" 1806 | } 1807 | -------------------------------------------------------------------------------- /presenter/presenter.go: -------------------------------------------------------------------------------- 1 | // Package presenter defines domain types for Go Package Store presenters. 2 | package presenter 3 | 4 | import ( 5 | "context" 6 | ) 7 | 8 | // Presenter returns a Presentation for r, or nil if it can't. 9 | type Presenter func(ctx context.Context, r Repo) *Presentation 10 | 11 | // Repo represents a single repository to be presented. 12 | // It contains the input for a Presenter. 13 | type Repo struct { 14 | // Root is the import path corresponding to the root of the repository. 15 | Root string 16 | 17 | // RepoURL is the repository URL, including scheme, as determined dynamically from the import path. 18 | RepoURL string 19 | 20 | LocalRevision string 21 | RemoteRevision string 22 | } 23 | 24 | // Presentation provides information about a Go package repo with an available update. 25 | // It contains the output of a Presenter. 26 | type Presentation struct { 27 | HomeURL string // Home URL of the Go package. Optional (empty string means none available). 28 | ImageURL string // Image representing the Go package, typically its owner. 29 | Changes []Change // List of changes, starting with the most recent. 30 | Error error // Any error that occurred during presentation, to be displayed to user. 31 | } 32 | 33 | // Change represents a single commit message. 34 | type Change struct { 35 | Message string // Commit message of this change. 36 | URL string // URL of this change. 37 | Comments Comments // Comments on this change. 38 | } 39 | 40 | // Comments represents change discussion. 41 | type Comments struct { 42 | Count int // Count of comments on this change. 43 | URL string // URL of change discussion. Optional (empty string means none available). 44 | } 45 | -------------------------------------------------------------------------------- /repo.go: -------------------------------------------------------------------------------- 1 | package gps 2 | 3 | import ( 4 | "github.com/shurcooL/vcsstate" 5 | "golang.org/x/tools/go/vcs" 6 | ) 7 | 8 | // Repo represents the state of a single repository. 9 | type Repo struct { 10 | // Root is the import path corresponding to the root of the repository. 11 | Root string 12 | 13 | // At most one of VCS or RemoteVCS should be not nil. 14 | // If both are nil, then Local and Remote structs are expected to be already populated. 15 | // TODO: Consider if it'd be better to split this into two distinct structs. 16 | 17 | // VCS allows getting the state of the VCS. 18 | VCS vcsstate.VCS 19 | // Path is the local filesystem path to the repository. 20 | // It must be set if VCS is not nil. 21 | Path string 22 | // Cmd can be used to update this local repository inside a GOPATH workspace. 23 | // It must be set if VCS is not nil. 24 | Cmd *vcs.Cmd 25 | 26 | // RemoteVCS allows getting the remote state of the VCS. 27 | RemoteVCS vcsstate.RemoteVCS 28 | // RemoteURL is the remote URL, including scheme. 29 | // It must be set if RemoteVCS is not nil. 30 | RemoteURL string 31 | 32 | Local struct { 33 | // RemoteURL is the remote URL, including scheme. 34 | RemoteURL string 35 | 36 | Revision string // Revision of the default branch (not necessarily the checked out one). 37 | } 38 | Remote struct { 39 | // RepoURL is the repository URL, including scheme, as determined dynamically from the import path. 40 | RepoURL string 41 | 42 | Branch string // Default branch, as determined from remote. Only populated if VCS or RemoteVCS is non-nil. 43 | Revision string // Revision of the default branch. 44 | } 45 | } 46 | 47 | // ImportPathPattern returns an import path pattern that matches all of the Go packages in this repo. 48 | // E.g.: 49 | // 50 | // "github.com/owner/repo/..." 51 | func (r Repo) ImportPathPattern() string { 52 | return r.Root + "/..." 53 | } 54 | -------------------------------------------------------------------------------- /updater.go: -------------------------------------------------------------------------------- 1 | package gps 2 | 3 | // Updater is able to update Go packages contained in repositories. 4 | type Updater interface { 5 | // Update specified repository to latest version. 6 | Update(repo *Repo) error 7 | } 8 | -------------------------------------------------------------------------------- /updater/dep.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "strings" 8 | 9 | "github.com/shurcooL/Go-Package-Store" 10 | ) 11 | 12 | // Dep is an Updater that updates Go packages in a project managed by dep. 13 | // 14 | // It requires the dep binary to be available in PATH. 15 | type Dep struct { 16 | // Dir specifies where the dep binary is executed. 17 | // If empty, current working directory is used. 18 | Dir string 19 | } 20 | 21 | // Update specified repository to latest version by calling 22 | // "dep ensure -update " in d.Dir directory. 23 | func (d Dep) Update(repo *gps.Repo) error { 24 | cmd := exec.Command("dep", "ensure", "-update", repo.Root) 25 | fmt.Println(strings.Join(cmd.Args, " ")) 26 | cmd.Dir = d.Dir 27 | cmd.Stdout = os.Stdout 28 | cmd.Stderr = os.Stderr 29 | err := cmd.Run() 30 | return err 31 | } 32 | -------------------------------------------------------------------------------- /updater/doc.go: -------------------------------------------------------------------------------- 1 | // Package updater contains gps.Updater implementations. 2 | package updater 3 | -------------------------------------------------------------------------------- /updater/gopath.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/shurcooL/Go-Package-Store" 7 | ) 8 | 9 | // Gopath is an Updater that updates Go packages in local GOPATH workspaces. 10 | type Gopath struct{} 11 | 12 | // Update specified repository to latest version. 13 | func (Gopath) Update(repo *gps.Repo) error { 14 | if repo.VCS == nil || repo.Path == "" || repo.Cmd == nil { 15 | return fmt.Errorf("missing information needed to update Go package in GOPATH: %#v", repo) 16 | } 17 | 18 | fmt.Printf("cd %s\n", repo.Path) 19 | fmt.Printf("%s %s", repo.Cmd.Cmd, repo.Cmd.DownloadCmd) 20 | err := repo.Cmd.Download(repo.Path) 21 | return err 22 | } 23 | -------------------------------------------------------------------------------- /updater/mock.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/shurcooL/Go-Package-Store" 8 | ) 9 | 10 | // Mock is a mock updater. 11 | type Mock struct{} 12 | 13 | // Update pretends to update specified repository to latest version. 14 | func (Mock) Update(repo *gps.Repo) error { 15 | fmt.Println("Mock: got update request:", repo.Root) 16 | const mockDelay = 3 * time.Second 17 | fmt.Printf("pretending to update (actually sleeping for %v)", mockDelay) 18 | time.Sleep(mockDelay) 19 | return nil 20 | } 21 | -------------------------------------------------------------------------------- /workspace/workspace.go: -------------------------------------------------------------------------------- 1 | // Package workspace contains a pipeline for processing a Go workspace. 2 | package workspace 3 | 4 | import ( 5 | "context" 6 | "fmt" 7 | "go/build" 8 | "log" 9 | "sync" 10 | 11 | "github.com/bradfitz/iter" 12 | "github.com/shurcooL/Go-Package-Store" 13 | "github.com/shurcooL/Go-Package-Store/presenter" 14 | "github.com/shurcooL/gostatus/status" 15 | "github.com/shurcooL/vcsstate" 16 | "golang.org/x/tools/go/vcs" 17 | ) 18 | 19 | // Pipeline for processing a Go workspace, where each repo has local and remote components. 20 | type Pipeline struct { 21 | wd string // Working directory. Used to resolve relative import paths. 22 | 23 | // presenters are presenters registered with RegisterPresenter. 24 | presenters []presenter.Presenter 25 | 26 | importPaths chan string 27 | importPathRevisions chan importPathRevision 28 | rootRevisionLatests chan rootRevisionLatest 29 | repositories chan LocalRepo 30 | subrepos chan Subrepo 31 | 32 | // unique is the output of finding unique repositories from diverse possible inputs. 33 | unique chan *gps.Repo 34 | // processedFiltered is the output of processed repos (complete with local and remote revisions), 35 | // with just enough information to decide if an update should be displayed. 36 | processedFiltered chan *gps.Repo 37 | // presented is the output of processed and presented repos (complete with presenter.Presentation). 38 | presented chan *RepoPresentation 39 | 40 | reposMu sync.Mutex 41 | repos map[string]*gps.Repo // Map key is the import path corresponding to the root of the repository. 42 | 43 | newObserver chan observerRequest 44 | observers map[chan *RepoPresentation]struct{} 45 | 46 | // Packages is the working list of Go packages. 47 | // It's implemented as two slices and a map kept in sync, protected by a mutex. 48 | Packages struct { 49 | sync.Mutex 50 | Active []*RepoPresentation // Active repo presentations, latest at the end. 51 | History []*RepoPresentation // Historical repo presentations, latest at the end. 52 | ByRoot map[string]*RepoPresentation // Map key is repoRoot. 53 | } 54 | } 55 | 56 | type observerRequest struct { 57 | Response chan chan *RepoPresentation 58 | } 59 | 60 | // RepoPresentation represents a repository update presentation. 61 | type RepoPresentation struct { 62 | Repo *gps.Repo 63 | Presentation *presenter.Presentation 64 | 65 | UpdateState UpdateState 66 | } 67 | 68 | // UpdateState represents the state of an update. 69 | // 70 | // TODO: Dedup. 71 | type UpdateState uint8 72 | 73 | const ( 74 | // Available represents an available update. 75 | Available UpdateState = iota 76 | 77 | // Updating represents an update in progress. 78 | Updating 79 | 80 | // Updated represents a completed update. 81 | Updated 82 | ) 83 | 84 | // NewPipeline creates a Pipeline with working directory wd. 85 | // Working directory is used to resolve relative import paths. 86 | // 87 | // First, available presenters should be registered via RegisterPresenter. 88 | // Then Go packages can be added via various means. Call Done once done adding. 89 | // Processing begins as soon as Go packages are added to the pipeline. 90 | // Results can be accessed via RepoPresentations at any time, as often as needed. 91 | func NewPipeline(wd string) *Pipeline { 92 | p := &Pipeline{ 93 | wd: wd, 94 | 95 | importPaths: make(chan string, 64), 96 | importPathRevisions: make(chan importPathRevision, 64), 97 | rootRevisionLatests: make(chan rootRevisionLatest, 64), 98 | repositories: make(chan LocalRepo, 64), 99 | subrepos: make(chan Subrepo, 64), 100 | unique: make(chan *gps.Repo, 64), 101 | processedFiltered: make(chan *gps.Repo, 64), 102 | presented: make(chan *RepoPresentation, 64), 103 | 104 | repos: make(map[string]*gps.Repo), 105 | 106 | newObserver: make(chan observerRequest), 107 | observers: make(map[chan *RepoPresentation]struct{}), 108 | } 109 | p.Packages.ByRoot = make(map[string]*RepoPresentation) 110 | 111 | // It is a lot of work to 112 | // find all Go packages in one's GOPATH workspace (or vendor.json file), 113 | // then group them by VCS repository, 114 | // and determine their local state (current revision, etc.), 115 | // then determine their remote state (latest remote revision, etc.), 116 | // then hit an API like GitHub or Gitiles to fetch descriptions of all commits 117 | // between the current local revision and latest remote revision for display purposes. 118 | // 119 | // That work is heavily blocked on local disk IO and network IO, 120 | // and also consists of dependencies. E.g., we can't ask for commit descriptions 121 | // until we know both the local and remote revisions, and we can't figure out local 122 | // revisions before we know which repository a Go package belongs to. 123 | // 124 | // Luckily, Go is great at concurrency, ʕ◔ϖ◔ʔ 125 | // which also makes parallelism easy! 126 | // (See https://blog.golang.org/concurrency-is-not-parallelism.) 127 | // 128 | // Let's make gophers do all this work for us in multiple interconnected stages, 129 | // and parallelize each stage with many worker goroutines. 130 | 131 | // Stage 1, grouping all inputs into a set of unique repositories. 132 | // 133 | // We populate the workspace from any of the 3 sources: 134 | // 135 | // - via AddImportPath - import paths of Go packages from the GOPATH workspace. 136 | // - via AddRevision - import paths of Go packages and their revisions from vendor.json or Godeps.json. 137 | // - via AddRevisionLatest - roots of Go packages, their revisions and latest versions via dep. 138 | // - via AddRepository - by directly adding local VCS repositories. 139 | // - via AddSubrepo - by directly adding remote subrepos. 140 | // 141 | // The goal of processing in stage 1 is to take in diverse possible inputs 142 | // and convert them into a unique set of repositories for further processing by next stages. 143 | // When finished, all unique repositories are sent to p.unique channel 144 | // and the channel is closed. 145 | { 146 | var wg0 sync.WaitGroup 147 | for range iter.N(8) { 148 | wg0.Add(1) 149 | go p.importPathWorker(&wg0) 150 | } 151 | var wg1 sync.WaitGroup 152 | for range iter.N(8) { 153 | wg1.Add(1) 154 | go p.importPathRevisionWorker(&wg1) 155 | } 156 | var wg2 sync.WaitGroup 157 | for range iter.N(8) { 158 | wg2.Add(1) 159 | go p.rootRevisionLatestWorker(&wg2) 160 | } 161 | var wg3 sync.WaitGroup 162 | for range iter.N(8) { 163 | wg3.Add(1) 164 | go p.repositoriesWorker(&wg3) 165 | } 166 | var wg4 sync.WaitGroup 167 | for range iter.N(8) { 168 | wg4.Add(1) 169 | go p.subreposWorker(&wg4) 170 | } 171 | go func() { 172 | wg0.Wait() 173 | wg1.Wait() 174 | wg2.Wait() 175 | wg3.Wait() 176 | wg4.Wait() 177 | close(p.unique) 178 | }() 179 | } 180 | 181 | // Stage 2, figuring out which repositories have updates available. 182 | // 183 | // We compute repository remote revision (and local if needed) 184 | // in order to figure out if repositories should be presented, 185 | // or filtered out (for example, because there are no updates available). 186 | // When finished, all non-filtered-out repositories are sent to p.processedFiltered channel 187 | // and the channel is closed. 188 | { 189 | var wg sync.WaitGroup 190 | for range iter.N(8) { 191 | wg.Add(1) 192 | go p.processFilterWorker(&wg) 193 | } 194 | go func() { 195 | wg.Wait() 196 | close(p.processedFiltered) 197 | }() 198 | } 199 | 200 | // Stage 3, filling in the update presentation information. 201 | // 202 | // We talk to remote APIs to fill in the missing presentation details 203 | // that are not available from VCS (unless we fetch commits, but we choose not to that). 204 | // Primarily, we get the commit messages for all the new commits that are available. 205 | // When finished, all repositories complete with full presentation information 206 | // are sent to p.presented channel and the channel is closed. 207 | { 208 | var wg sync.WaitGroup 209 | for range iter.N(8) { 210 | wg.Add(1) 211 | go p.presentWorker(&wg) 212 | } 213 | go func() { 214 | wg.Wait() 215 | close(p.presented) 216 | }() 217 | } 218 | 219 | go p.run() 220 | 221 | return p 222 | } 223 | 224 | // RegisterPresenter registers a presenter. 225 | // Presenters are consulted in the same order that they were registered. 226 | func (p *Pipeline) RegisterPresenter(pr presenter.Presenter) { 227 | p.presenters = append(p.presenters, pr) 228 | } 229 | 230 | // AddImportPath adds a package with specified import path for processing. 231 | func (p *Pipeline) AddImportPath(importPath string) { 232 | p.importPaths <- importPath 233 | } 234 | 235 | // AddRevision adds a package with specified import path and revision for processing. 236 | func (p *Pipeline) AddRevision(importPath string, revision string) { 237 | p.importPathRevisions <- importPathRevision{ 238 | importPath: importPath, 239 | revision: revision, 240 | } 241 | } 242 | 243 | type importPathRevision struct { 244 | importPath string 245 | revision string 246 | } 247 | 248 | // AddRevisionLatest adds a package with specified root, revision and latest. 249 | func (p *Pipeline) AddRevisionLatest(root, revision, latest string) { 250 | p.rootRevisionLatests <- rootRevisionLatest{ 251 | root: root, 252 | revision: revision, 253 | latest: latest, 254 | } 255 | } 256 | 257 | type rootRevisionLatest struct { 258 | root string 259 | revision string 260 | latest string 261 | } 262 | 263 | // LocalRepo represents a local repository on disk. 264 | type LocalRepo struct { 265 | Path string // Full path to repository on disk. 266 | Root string // Import path corresponding to the root of the repository. 267 | VCS *vcs.Cmd 268 | } 269 | 270 | // AddRepository adds the specified repository for processing. 271 | func (p *Pipeline) AddRepository(r LocalRepo) { 272 | p.repositories <- r 273 | } 274 | 275 | // Subrepo represents a "virtual" sub-repository inside a larger actual VCS repository. 276 | type Subrepo struct { 277 | Root string 278 | RemoteVCS vcsstate.RemoteVCS // RemoteVCS allows getting the remote state of the VCS. 279 | RemoteURL string // RemoteURL is the remote URL, including scheme. 280 | Revision string 281 | } 282 | 283 | // AddSubrepo adds the specified Subrepo for processing. 284 | func (p *Pipeline) AddSubrepo(s Subrepo) { 285 | p.subrepos <- s 286 | } 287 | 288 | // Done should be called after the workspace is finished being populated. 289 | func (p *Pipeline) Done() { 290 | close(p.importPaths) 291 | close(p.importPathRevisions) 292 | close(p.rootRevisionLatests) 293 | close(p.repositories) 294 | close(p.subrepos) 295 | } 296 | 297 | // AddPresented adds a RepoPresentation the pipeline. 298 | // It enables mocks to directly add presented repos. 299 | func (p *Pipeline) AddPresented(r *RepoPresentation) { 300 | p.presented <- r 301 | } 302 | 303 | // RepoPresentations returns a channel of all repo presentations. 304 | // Repo presentations that are ready will be sent immediately. 305 | // The remaining repo presentations will be sent onto the channel 306 | // as they become available. Once all repo presentations have been 307 | // sent, the channel will be closed. Therefore, iterating over 308 | // the channel may block until all processing is done, but it 309 | // will effectively return all repo presentations as soon as possible. 310 | // 311 | // It's safe to call RepoPresentations at any time and concurrently 312 | // to get multiple such channels. 313 | func (p *Pipeline) RepoPresentations() <-chan *RepoPresentation { 314 | response := make(chan chan *RepoPresentation) 315 | p.newObserver <- observerRequest{Response: response} 316 | return <-response 317 | } 318 | 319 | func (p *Pipeline) run() { 320 | Outer: 321 | for { 322 | select { 323 | // New repoPresentation available. 324 | case repoPresentation, ok := <-p.presented: 325 | // We're done streaming. 326 | if !ok { 327 | break Outer 328 | } 329 | 330 | // Append repoPresentation to current list. 331 | p.Packages.Lock() 332 | switch repoPresentation.UpdateState { 333 | case Available, Updating: 334 | p.Packages.Active = append(p.Packages.Active, repoPresentation) 335 | case Updated: 336 | p.Packages.History = append(p.Packages.History, repoPresentation) 337 | } 338 | p.Packages.ByRoot[repoPresentation.Repo.Root] = repoPresentation 339 | p.Packages.Unlock() 340 | 341 | // Send new repoPresentation to all existing observers. 342 | for ch := range p.observers { 343 | // TODO: If an observer isn't listening, this will block. Should we defend against that here? 344 | ch <- repoPresentation 345 | } 346 | // New observer request. 347 | case req := <-p.newObserver: 348 | p.Packages.Lock() 349 | ch := make(chan *RepoPresentation, len(p.Packages.Active)+len(p.Packages.History)) 350 | for _, repoPresentation := range p.Packages.Active { 351 | ch <- repoPresentation 352 | } 353 | for _, repoPresentation := range p.Packages.History { 354 | ch <- repoPresentation 355 | } 356 | p.Packages.Unlock() 357 | 358 | p.observers[ch] = struct{}{} 359 | 360 | req.Response <- ch 361 | } 362 | } 363 | 364 | // At this point, streaming has finished, so finish up existing observers. 365 | for ch := range p.observers { 366 | close(ch) 367 | } 368 | p.observers = nil 369 | 370 | // Respond to new observer requests directly. 371 | for req := range p.newObserver { 372 | p.Packages.Lock() 373 | ch := make(chan *RepoPresentation, len(p.Packages.Active)+len(p.Packages.History)) 374 | for _, repoPresentation := range p.Packages.Active { 375 | ch <- repoPresentation 376 | } 377 | for _, repoPresentation := range p.Packages.History { 378 | ch <- repoPresentation 379 | } 380 | p.Packages.Unlock() 381 | 382 | close(ch) 383 | 384 | req.Response <- ch 385 | } 386 | } 387 | 388 | // importPathWorker sends unique repositories to phase 2. 389 | func (p *Pipeline) importPathWorker(wg *sync.WaitGroup) { 390 | defer wg.Done() 391 | for importPath := range p.importPaths { 392 | // Determine repo root. 393 | // This is potentially somewhat slow. 394 | bpkg, err := build.Import(importPath, p.wd, build.FindOnly|build.IgnoreVendor) // THINK: This (build.FindOnly) may find repos even when importPath has no actual package... Is that okay? 395 | if err != nil { 396 | log.Println("build.Import:", err) 397 | continue 398 | } 399 | if bpkg.Goroot { 400 | // Go-Package-Store has no support for updating packages in GOROOT, so skip those. 401 | continue 402 | } 403 | vcsCmd, root, err := vcs.FromDir(bpkg.Dir, bpkg.SrcRoot) 404 | if err != nil { 405 | // Go package not under VCS. 406 | continue 407 | } 408 | vcs, err := vcsstate.NewVCS(vcsCmd) 409 | if err != nil { 410 | log.Printf("repo %v not supported by vcsstate: %v", root, err) 411 | continue 412 | } 413 | 414 | var repo *gps.Repo 415 | p.reposMu.Lock() 416 | if _, ok := p.repos[root]; !ok { 417 | repo = &gps.Repo{ 418 | Root: root, 419 | 420 | // This is a local repository inside GOPATH. Set all of its fields. 421 | VCS: vcs, 422 | Path: bpkg.Dir, 423 | Cmd: vcsCmd, 424 | 425 | // TODO: Maybe keep track of import paths inside, etc. 426 | } 427 | p.repos[root] = repo 428 | //} else { 429 | // TODO: Maybe keep track of import paths inside, etc. 430 | } 431 | p.reposMu.Unlock() 432 | 433 | // If new repo, send off to phase 2 channel. 434 | if repo != nil { 435 | p.unique <- repo 436 | } 437 | } 438 | } 439 | 440 | // importPathRevisionWorker sends unique repositories to phase 2. 441 | func (p *Pipeline) importPathRevisionWorker(wg *sync.WaitGroup) { 442 | defer wg.Done() 443 | for ipr := range p.importPathRevisions { 444 | // Determine repo root. 445 | // This is potentially somewhat slow. 446 | rr, err := vcs.RepoRootForImportPath(ipr.importPath, false) 447 | if err != nil { 448 | log.Printf("failed to dynamically determine repo root for %v: %v\n", ipr.importPath, err) 449 | continue 450 | } 451 | remoteVCS, err := vcsstate.NewRemoteVCS(rr.VCS) 452 | if err != nil { 453 | log.Printf("repo %v not supported by vcsstate: %v\n", rr.Root, err) 454 | continue 455 | } 456 | 457 | var repo *gps.Repo 458 | p.reposMu.Lock() 459 | if _, ok := p.repos[rr.Root]; !ok { 460 | repo = &gps.Repo{ 461 | Root: rr.Root, 462 | 463 | // This is a remote repository only. Set all of its fields. 464 | RemoteVCS: remoteVCS, 465 | RemoteURL: rr.Repo, 466 | } 467 | repo.Local.Revision = ipr.revision 468 | repo.Remote.RepoURL = rr.Repo 469 | p.repos[rr.Root] = repo 470 | } 471 | p.reposMu.Unlock() 472 | 473 | // If new repo, send off to phase 2 channel. 474 | if repo != nil { 475 | p.unique <- repo 476 | } 477 | } 478 | } 479 | 480 | // rootRevisionLatestWorker sends unique repositories to phase 2. 481 | func (p *Pipeline) rootRevisionLatestWorker(wg *sync.WaitGroup) { 482 | defer wg.Done() 483 | for rrl := range p.rootRevisionLatests { 484 | // Determine repo root. 485 | // This is potentially somewhat slow. 486 | rr, err := vcs.RepoRootForImportPath(rrl.root, false) 487 | if err != nil { 488 | log.Printf("failed to dynamically determine repo root for %v: %v\n", rrl.root, err) 489 | continue 490 | } 491 | if rr.Root != rrl.root { 492 | log.Printf("dynamically determined repo root (%q) doesn't match input root (%q)\n", rr.Root, rrl.root) 493 | continue 494 | } 495 | 496 | var repo *gps.Repo 497 | p.reposMu.Lock() 498 | if _, ok := p.repos[rr.Root]; !ok { 499 | repo = new(gps.Repo) 500 | repo.Root = rr.Root 501 | repo.Local.Revision = rrl.revision 502 | repo.Remote.Revision = rrl.latest 503 | repo.Remote.RepoURL = rr.Repo 504 | p.repos[rr.Root] = repo 505 | } 506 | p.reposMu.Unlock() 507 | 508 | // If new repo, send off to phase 2 channel. 509 | if repo != nil { 510 | p.unique <- repo 511 | } 512 | } 513 | } 514 | 515 | // repositoriesWorker sends unique repositories to phase 2. 516 | func (p *Pipeline) repositoriesWorker(wg *sync.WaitGroup) { 517 | defer wg.Done() 518 | for r := range p.repositories { 519 | vcsCmd, root := r.VCS, r.Root 520 | vcs, err := vcsstate.NewVCS(vcsCmd) 521 | if err != nil { 522 | log.Printf("repo %v not supported by vcsstate: %v", root, err) 523 | continue 524 | } 525 | 526 | var repo *gps.Repo 527 | p.reposMu.Lock() 528 | if _, ok := p.repos[root]; !ok { 529 | repo = &gps.Repo{ 530 | Root: root, 531 | 532 | // This is a local repository inside GOPATH. Set all of its fields. 533 | VCS: vcs, 534 | Path: r.Path, 535 | Cmd: vcsCmd, 536 | } 537 | p.repos[root] = repo 538 | } 539 | p.reposMu.Unlock() 540 | 541 | // If new repo, send off to phase 2 channel. 542 | if repo != nil { 543 | p.unique <- repo 544 | } 545 | } 546 | } 547 | 548 | // subreposWorker sends unique subrepos to phase 2. 549 | func (p *Pipeline) subreposWorker(wg *sync.WaitGroup) { 550 | defer wg.Done() 551 | for r := range p.subrepos { 552 | // Determine repo root. 553 | // This is potentially somewhat slow. 554 | rr, err := vcs.RepoRootForImportPath(r.Root, false) 555 | if err != nil { 556 | log.Printf("failed to dynamically determine repo root for %v: %v\n", r.Root, err) 557 | continue 558 | } 559 | 560 | var repo *gps.Repo 561 | p.reposMu.Lock() 562 | if _, ok := p.repos[r.Root]; !ok { 563 | repo = &gps.Repo{ 564 | Root: r.Root, 565 | 566 | // This is a remote repository only. Set all of its fields. 567 | RemoteVCS: r.RemoteVCS, 568 | RemoteURL: r.RemoteURL, 569 | } 570 | repo.Local.RemoteURL = r.RemoteURL // TODO: Consider having r.RemoteURL take precedence over rr.Repo. But need to make that play nicely with the updaters; see TODO at bottom of gps.Repo struct. 571 | repo.Local.Revision = r.Revision 572 | repo.Remote.RepoURL = rr.Repo 573 | p.repos[r.Root] = repo 574 | } 575 | p.reposMu.Unlock() 576 | 577 | // If new repo, send off to phase 2 channel. 578 | if repo != nil { 579 | p.unique <- repo 580 | } 581 | } 582 | } 583 | 584 | // processFilterWorker computes repository remote revision (and local if needed) 585 | // in order to figure out if repositories should be presented. 586 | func (p *Pipeline) processFilterWorker(wg *sync.WaitGroup) { 587 | defer wg.Done() 588 | for r := range p.unique { 589 | // Determine remote revision. 590 | // This is slow because it requires a network operation. 591 | switch { 592 | case r.VCS != nil: 593 | var err error 594 | r.Remote.Branch, r.Remote.Revision, err = r.VCS.RemoteBranchAndRevision(r.Path) 595 | if err != nil { 596 | log.Printf("skipping %q because of remote error:\n\t%v\n", r.Root, err) 597 | continue 598 | } 599 | 600 | if r.Local.Revision == "" { 601 | if rev, err := r.VCS.LocalRevision(r.Path, r.Remote.Branch); err == nil { 602 | r.Local.Revision = rev 603 | } 604 | } 605 | if ru, err := r.VCS.RemoteURL(r.Path); err == nil { 606 | r.Local.RemoteURL = ru 607 | } 608 | if rr, err := vcs.RepoRootForImportPath(r.Root, false); err == nil { 609 | r.Remote.RepoURL = rr.Repo 610 | } else { 611 | log.Printf("failed to dynamically determine repo root for %v: %v\n", r.Root, err) 612 | } 613 | case r.RemoteVCS != nil: 614 | var err error 615 | r.Remote.Branch, r.Remote.Revision, err = r.RemoteVCS.RemoteBranchAndRevision(r.RemoteURL) 616 | if err != nil { 617 | log.Printf("skipping %q because of remote error:\n\t%v\n", r.Root, err) 618 | continue 619 | } 620 | default: 621 | // Do nothing. If both r.VCS and r.RemoteVCS are nil, then we expect 622 | // the Local and Remote structs to already be populated. 623 | } 624 | 625 | if ok, reason := shouldPresentUpdate(r); !ok { 626 | if reason != "" { 627 | log.Printf("skipping %q because:\n\t%v\n", r.Root, reason) 628 | } 629 | continue 630 | } 631 | 632 | p.processedFiltered <- r 633 | } 634 | } 635 | 636 | // shouldPresentUpdate reports if the given goPackage should be presented as an available update. 637 | // It checks that the Go package is on default branch, does not have a dirty working tree, and does not have the remote revision. 638 | // It returns a non-empty reason for why an update should be skipped, or empty string if it's not interesting (e.g., repository is up to date). 639 | func shouldPresentUpdate(repo *gps.Repo) (ok bool, reason string) { 640 | // Ensure sufficient remote information is available, otherwise we can't present updates. 641 | if repo.Remote.RepoURL == "" { 642 | return false, "repository URL (as determined dynamically from the import path) is empty" 643 | } 644 | if (repo.VCS != nil || repo.RemoteVCS != nil) && repo.Remote.Branch == "" { 645 | return false, "remote branch is empty" 646 | } 647 | if repo.Remote.Revision == "" { 648 | return false, "remote revision is empty" 649 | } 650 | 651 | // Check repository state before presenting updates, and report most useful 652 | // reasons first. 653 | switch { 654 | case repo.VCS != nil: 655 | // Local remote URL should match Repo URL derived from import path. 656 | // This is the very first thing to verify, because it affects default branch. 657 | if !status.EqualRepoURLs(repo.Local.RemoteURL, repo.Remote.RepoURL) { 658 | return false, "remote URL doesn't match repo URL inferred from import path:" + 659 | fmt.Sprintf("\n (actual) %s", repo.Local.RemoteURL) + 660 | fmt.Sprintf("\n (expected) %s", status.FormatRepoURL(repo.Local.RemoteURL, repo.Remote.RepoURL)) 661 | } 662 | 663 | // Local branch should match remote branch. 664 | localBranch, err := repo.VCS.Branch(repo.Path) 665 | if err != nil { 666 | return false, "error determining local branch:\n" + err.Error() 667 | } 668 | if localBranch != repo.Remote.Branch { 669 | return false, fmt.Sprintf("local branch %q doesn't match remote branch %q", localBranch, repo.Remote.Branch) 670 | } 671 | 672 | case repo.RemoteVCS != nil: 673 | // TODO: Consider taking care of this difference in remote URLs earlier, inside, e.g., subreposWorker. But need to make that play nicely with the updaters; see TODO at bottom of gps.Repo struct. 674 | // 675 | // Local remote URL, if set, should match Repo URL derived from import path. 676 | if repo.Local.RemoteURL != "" && !status.EqualRepoURLs(repo.Local.RemoteURL, repo.Remote.RepoURL) { 677 | return false, "remote URL doesn't match repo URL inferred from import path:" + 678 | fmt.Sprintf("\n (actual) %s", repo.Local.RemoteURL) + 679 | fmt.Sprintf("\n (expected) %s", status.FormatRepoURL(repo.Local.RemoteURL, repo.Remote.RepoURL)) 680 | } 681 | } 682 | 683 | if repo.Local.Revision == "" { 684 | return false, "local revision is empty" 685 | } 686 | 687 | // Check if repo is already up to date. 688 | if repo.Local.Revision == repo.Remote.Revision { 689 | // No reason provided because it's not worth mentioning. 690 | return false, "" 691 | } 692 | 693 | // Check rest of local repository state before presenting updates, 694 | // and report most useful reasons first. 695 | if repo.VCS != nil { 696 | // Local default branch shouldn't contain remote commit. 697 | // Otherwise, it means local revision is different because it's 698 | // ahead of remote revision, rather than because there's an update. 699 | localContainsRemoteRevision, err := repo.VCS.Contains(repo.Path, repo.Remote.Revision, repo.Remote.Branch) 700 | if err != nil { 701 | return false, "error determining if local default branch contains remote revision:\n" + err.Error() 702 | } 703 | if localContainsRemoteRevision { 704 | // Local revision is ahead of remote revision, and there's no update. 705 | // This isn't worth reporting in detail, since there's no update anyway. 706 | return false, "" 707 | } 708 | 709 | // Remote default branch should contain local commit. 710 | // Otherwise, it means there's an update, but it won't be able to apply 711 | // cleanly because the local revision is ahead of remote revision. 712 | remoteContainsLocalRevision, err := repo.VCS.RemoteContains(repo.Path, repo.Local.Revision, repo.Remote.Branch) 713 | if err != nil { 714 | return false, "error determining if remote default branch contains local revision:\n" + err.Error() 715 | } 716 | if !remoteContainsLocalRevision { 717 | return false, fmt.Sprintf("local revision %q is ahead of remote revision %q", repo.Local.Revision, repo.Remote.Revision) 718 | } 719 | 720 | // There shouldn't be a dirty working tree. 721 | treeStatus, err := repo.VCS.Status(repo.Path) 722 | if err != nil { 723 | return false, "error determining if working tree is dirty:\n" + err.Error() 724 | } 725 | if treeStatus != "" { 726 | return false, "working tree is dirty:\n" + treeStatus 727 | } 728 | } 729 | 730 | // If we got this far, there's an update available and everything looks normal. Present it. 731 | return true, "" 732 | } 733 | 734 | // presentWorker works with repos that should be displayed, creating a presentation for each. 735 | func (p *Pipeline) presentWorker(wg *sync.WaitGroup) { 736 | defer wg.Done() 737 | for repo := range p.processedFiltered { 738 | // This part might take a while. 739 | presentation := p.present(presenter.Repo{ 740 | Root: repo.Root, 741 | RepoURL: repo.Remote.RepoURL, 742 | LocalRevision: repo.Local.Revision, 743 | RemoteRevision: repo.Remote.Revision, 744 | }) 745 | 746 | p.presented <- &RepoPresentation{ 747 | Repo: repo, 748 | Presentation: presentation, 749 | } 750 | } 751 | } 752 | 753 | // present takes a repository containing 1 or more Go packages, and returns a presentation for it. 754 | // It tries to find the best presenter for the given repository out of the registered ones, 755 | // but falls back to a generic presentation if there's nothing better. 756 | func (p *Pipeline) present(repo presenter.Repo) *presenter.Presentation { 757 | for _, presenter := range p.presenters { 758 | if presentation := presenter(context.Background(), repo); presentation != nil { 759 | return presentation 760 | } 761 | } 762 | 763 | // Generic presentation is the fallback if no presenters matched. 764 | return &presenter.Presentation{ 765 | HomeURL: "https://" + repo.Root, 766 | ImageURL: "https://github.com/images/gravatars/gravatar-user-420.png", 767 | Changes: nil, 768 | Error: nil, 769 | } 770 | } 771 | --------------------------------------------------------------------------------