├── retrodep
├── testdata
│ ├── gosource
│ │ ├── ignored.go
│ │ └── vendor
│ │ │ └── github.com
│ │ │ ├── eggs
│ │ │ └── ham
│ │ │ │ ├── ham.go
│ │ │ │ └── spam
│ │ │ │ └── ignored.go
│ │ │ └── foo
│ │ │ └── bar
│ │ │ └── bar.go
│ ├── glide
│ │ ├── main.go
│ │ ├── vendor
│ │ │ └── github.com
│ │ │ │ ├── pborman
│ │ │ │ └── uuid
│ │ │ │ │ └── test.go
│ │ │ │ └── spf13
│ │ │ │ └── pflag
│ │ │ │ └── test.go
│ │ ├── glide.yaml
│ │ └── glide.lock
│ ├── godep
│ │ ├── nonl.txt
│ │ ├── importcomment.go
│ │ ├── nl.go
│ │ ├── nonl.go
│ │ └── Godeps
│ │ │ └── Godeps.json
│ ├── multi
│ │ ├── abc
│ │ │ ├── abc.go
│ │ │ └── vendor
│ │ │ │ └── ghi
│ │ │ │ └── ghi.go
│ │ └── def
│ │ │ ├── def.go
│ │ │ └── vendor
│ │ │ └── ghi
│ │ │ └── ghi.go
│ ├── importcommentsub
│ │ ├── main.go
│ │ └── sub
│ │ │ └── main.go
│ └── importcomment
│ │ └── main.go
├── vcsnames.go
├── glide
│ ├── glide_test.go
│ └── glide.go
├── errors.go
├── doc.go
├── exec_test.go
├── vendored_test.go
├── filehash_test.go
├── hg.go
├── filehash.go
├── git.go
├── workingtree_test.go
├── hg_test.go
├── git_test.go
├── gosource_test.go
├── workingtree.go
├── vendored.go
└── gosource.go
├── .gitignore
├── .travis.yml
├── go.mod
├── Makefile
├── go.sum
├── extras
└── retrodiff
├── main_test.go
├── README.md
├── main.go
└── LICENSE
/retrodep/testdata/gosource/ignored.go:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/retrodep/testdata/glide/main.go:
--------------------------------------------------------------------------------
1 | package glide
2 |
--------------------------------------------------------------------------------
/retrodep/testdata/godep/nonl.txt:
--------------------------------------------------------------------------------
1 | No newline at the end
--------------------------------------------------------------------------------
/retrodep/testdata/multi/abc/abc.go:
--------------------------------------------------------------------------------
1 | package abc
2 |
--------------------------------------------------------------------------------
/retrodep/testdata/multi/def/def.go:
--------------------------------------------------------------------------------
1 | package def
2 |
--------------------------------------------------------------------------------
/retrodep/testdata/gosource/vendor/github.com/eggs/ham/ham.go:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/retrodep/testdata/gosource/vendor/github.com/foo/bar/bar.go:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/retrodep/testdata/multi/abc/vendor/ghi/ghi.go:
--------------------------------------------------------------------------------
1 | package ghi
2 |
--------------------------------------------------------------------------------
/retrodep/testdata/multi/def/vendor/ghi/ghi.go:
--------------------------------------------------------------------------------
1 | package ghi
2 |
--------------------------------------------------------------------------------
/retrodep/testdata/importcommentsub/main.go:
--------------------------------------------------------------------------------
1 | package importcomment
2 |
--------------------------------------------------------------------------------
/retrodep/testdata/gosource/vendor/github.com/eggs/ham/spam/ignored.go:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/retrodep/testdata/godep/importcomment.go:
--------------------------------------------------------------------------------
1 | package foo // import "godep/foo"
2 |
--------------------------------------------------------------------------------
/retrodep/testdata/glide/vendor/github.com/pborman/uuid/test.go:
--------------------------------------------------------------------------------
1 | package uuid
2 |
--------------------------------------------------------------------------------
/retrodep/testdata/glide/vendor/github.com/spf13/pflag/test.go:
--------------------------------------------------------------------------------
1 | package pflag
2 |
--------------------------------------------------------------------------------
/retrodep/testdata/godep/nl.go:
--------------------------------------------------------------------------------
1 | package foo
2 |
3 | // No newline at the end of this line
4 |
--------------------------------------------------------------------------------
/retrodep/testdata/godep/nonl.go:
--------------------------------------------------------------------------------
1 | package foo
2 |
3 | // No newline at the end of this line
--------------------------------------------------------------------------------
/retrodep/testdata/importcomment/main.go:
--------------------------------------------------------------------------------
1 | package importcomment // import "importcomment"
2 |
--------------------------------------------------------------------------------
/retrodep/testdata/importcommentsub/sub/main.go:
--------------------------------------------------------------------------------
1 | package sub // import "importcomment/sub"
2 |
--------------------------------------------------------------------------------
/retrodep/testdata/glide/glide.yaml:
--------------------------------------------------------------------------------
1 | package: github.com/release-engineering/retrodep/testdata/glide
2 |
--------------------------------------------------------------------------------
/retrodep/testdata/godep/Godeps/Godeps.json:
--------------------------------------------------------------------------------
1 | {
2 | "ImportPath": "example.com/godep"
3 | }
4 |
--------------------------------------------------------------------------------
/retrodep/testdata/glide/glide.lock:
--------------------------------------------------------------------------------
1 | imports:
2 | - name: github.com/pborman/uuid
3 | version: ca53cad383cad2479bbba7f7a1a05797ec1386e4
4 | - name: github.com/spf13/pflag
5 | version: 583c0c0531f06d5278b7d917446061adc344b5cd
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.exe~
4 | *.dll
5 | *.so
6 | *.dylib
7 |
8 | # Test binary, build with `go test -c`
9 | *.test
10 |
11 | # Output of the go coverage tool, specifically when used with LiteIDE
12 | *.out
13 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: go
2 | branches:
3 | only:
4 | - master
5 | - v0
6 | sudo: false
7 | before_install:
8 | - make tools
9 | - go get -t ./...
10 | script:
11 | - make check
12 | - make test
13 | - make build
14 | after_success:
15 | - make coveralls
16 | notifications:
17 | email: false
18 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/release-engineering/retrodep/v2
2 |
3 | require (
4 | github.com/Masterminds/semver v1.4.2
5 | github.com/kr/pretty v0.1.0 // indirect
6 | github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
7 | github.com/pkg/errors v0.8.1
8 | golang.org/x/tools v0.0.0-20190325161752-5a8dccf5b48a
9 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
10 | gopkg.in/yaml.v2 v2.2.2
11 | )
12 |
--------------------------------------------------------------------------------
/retrodep/vcsnames.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2018 Tim Waugh
2 | //
3 | // This program is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU General Public License as published by
5 | // the Free Software Foundation, either version 3 of the License, or
6 | // (at your option) any later version.
7 | //
8 | // This program is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU General Public License
14 | // along with this program. If not, see .
15 |
16 | package retrodep
17 |
18 | const vcsGit = "git"
19 | const vcsHg = "hg"
20 |
--------------------------------------------------------------------------------
/retrodep/glide/glide_test.go:
--------------------------------------------------------------------------------
1 | package glide
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestGlideFalse(t *testing.T) {
8 | glide, err := LoadGlide("../testdata/glide/")
9 | if err != nil {
10 | t.Fatal("failed to load the lock file", err)
11 | }
12 | if glide.Imports[0].Name != "github.com/pborman/uuid" {
13 | t.Fatalf("expected '%v', got '%v'", "github.com/pborman/uuid", glide.Imports[0].Name)
14 | }
15 | if glide.Imports[0].Version != "ca53cad383cad2479bbba7f7a1a05797ec1386e4" {
16 | t.Fatalf("expected '%v', got '%v'", "ca53cad383cad2479bbba7f7a1a05797ec1386e4", glide.Imports[0].Version)
17 | }
18 | if len(glide.Imports) != 2 {
19 | t.Fatalf("expected '%v', got '%v'", 2, len(glide.Imports))
20 | }
21 | if glide.Package != "github.com/release-engineering/retrodep/testdata/glide" {
22 | t.Fatalf("expected '%v', got '%v'", "github.com/release-engineering/retrodep/testdata/glide", glide.Package)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/retrodep/errors.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2018 Tim Waugh
2 | //
3 | // This program is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU General Public License as published by
5 | // the Free Software Foundation, either version 3 of the License, or
6 | // (at your option) any later version.
7 | //
8 | // This program is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU General Public License
14 | // along with this program. If not, see .
15 |
16 | package retrodep
17 |
18 | import "errors"
19 |
20 | // ErrorNoGo indicates there is no Go source code to process.
21 | var ErrorNoGo = errors.New("no Go source code to process")
22 |
23 | // ErrorNeedImportPath indicates the import path for the project
24 | // cannot be determined automatically and must be provided.
25 | var ErrorNeedImportPath = errors.New("unable to determine import path")
26 |
27 | // ErrorVersionNotFound indicates a vendored project does not match any semantic
28 | // tag in the upstream revision control system.
29 | var ErrorVersionNotFound = errors.New("version not found")
30 |
31 | // ErrorUnknownVCS indicates the upstream version control system is not one of
32 | // those for which support is implemented in retrodep.
33 | var ErrorUnknownVCS = errors.New("unknown VCS")
34 |
35 | // ErrorNoFiles indicates there are no files to compare hashes of
36 | var ErrorNoFiles = errors.New("no files to hash")
37 |
38 | // ErrorInvalidRef indicates the ref is not a tag or a revision
39 | // (perhaps it is a branch name instead).
40 | var ErrorInvalidRef = errors.New("invalid ref")
41 |
--------------------------------------------------------------------------------
/retrodep/glide/glide.go:
--------------------------------------------------------------------------------
1 | package glide
2 |
3 | import (
4 | "os"
5 | "path/filepath"
6 |
7 | "gopkg.in/yaml.v2"
8 | )
9 |
10 | type glideLock struct {
11 | Imports []Import `json:"imports"`
12 | }
13 |
14 | type glideConf struct {
15 | Package string `json:"package"`
16 | Import []struct {
17 | Package string
18 | Repo string `json:"omitempty"`
19 | }
20 | }
21 |
22 | // Import represents an imported package.
23 | type Import struct {
24 | Name string `json:"name"`
25 | Version string `json:"version"`
26 | Repo string `json:"repo"`
27 | }
28 |
29 | // Glide represents the glide configuration.
30 | type Glide struct {
31 | Package string
32 | Imports []Import
33 | }
34 |
35 | // LoadGlide tries to load glide.lock and glide.yaml and extract
36 | // import information. In case no glide.lock is present, it will use
37 | // the import information from glide.yaml.
38 | func LoadGlide(projectRoot string) (*Glide, error) {
39 | lockImports := []Import{}
40 | lockFile, err := os.Open(filepath.Join(projectRoot, "glide.lock"))
41 | if err == nil {
42 | defer lockFile.Close()
43 | lock := glideLock{}
44 | if err != nil {
45 | return nil, err
46 | }
47 | err = yaml.NewDecoder(lockFile).Decode(&lock)
48 | if err != nil {
49 | return nil, err
50 | }
51 | lockImports = lock.Imports
52 | } else if !os.IsNotExist(err) {
53 | return nil, err
54 | }
55 |
56 | confFile, err := os.Open(filepath.Join(projectRoot, "glide.yaml"))
57 | if err != nil {
58 | return nil, err
59 | }
60 | defer confFile.Close()
61 | conf := glideConf{}
62 | err = yaml.NewDecoder(confFile).Decode(&conf)
63 | if err != nil {
64 | return nil, err
65 | }
66 |
67 | if len(lockImports) == 0 {
68 | for _, imp := range conf.Import {
69 | lockImports = append(lockImports, Import{Name: imp.Package, Repo: imp.Repo})
70 | }
71 | }
72 |
73 | return &Glide{Imports: lockImports, Package: conf.Package}, nil
74 | }
75 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | BIN_DIR = bin
2 | BIN_NAME = retrodep
3 | BIN = $(BIN_DIR)/$(BIN_NAME)
4 | PREFIX = /usr/local/bin
5 | TOOLS = golang.org/x/tools/cmd/goimports golang.org/x/lint/golint github.com/mattn/goveralls
6 | COVERPROFILE = profile.cov
7 |
8 | GOBIN = $$GOPATH/bin
9 | GOVERALLS = $(GOBIN)/goveralls
10 | GOIMPORTS = $(GOBIN)/goimports
11 | GOFMT = gofmt
12 | GOLINT = $(GOBIN)/golint
13 |
14 | DEFAULT_BRANCH = master
15 | FILES_TO_CHECK = $(shell git diff --no-renames --name-status $(DEFAULT_BRANCH) -- | grep '\.go$$' | grep -v D | cut -f 2)
16 |
17 | .PHONY: all clean deps test build fmt lint install check coveralls
18 |
19 | all: build
20 |
21 | clean:
22 | @echo '\033[0;31mRemoving generated binaries\033[0m'; \
23 | rm -rf $(BIN_DIR)
24 |
25 | build:
26 | @echo '\033[0;32mBuilding\033[0m'; \
27 | go build -o $(BIN) ./main.go
28 |
29 | install:
30 | @echo 'Installing retrodep to \033[0;32m$(PREFIX)/$(BIN_NAME)\033[0m'; \
31 | install $(BIN) $(PREFIX)/$(BIN_NAME)
32 |
33 | tools:
34 | @echo 'Installing \033[0;32m$(TOOLS)\033[0m'; \
35 | for tool in $(TOOLS); do \
36 | go get -u $$tool; \
37 | done
38 |
39 | test:
40 | @echo 'Running \033[0;32mtests\033[0m'; \
41 | go test . -v; \
42 | go test ./retrodep/glide -v; \
43 | go test ./retrodep -v -covermode=count -coverprofile=$(COVERPROFILE)
44 |
45 | fmt:
46 | @if test -n "$(FILES_TO_CHECK)"; then \
47 | echo 'Running \033[0;32mgofmt\033[0m'; \
48 | out=$$($(GOFMT) -l $(FILES_TO_CHECK)); \
49 | echo $$out; \
50 | test -z "$$out"; \
51 | fi
52 |
53 | lint:
54 | @if test -n "$(FILES_TO_CHECK)"; then \
55 | echo 'Running \033[0;32mgolint\033[0m'; \
56 | out=$$($(GOLINT) $(FILES_TO_CHECK)); \
57 | echo $$out; \
58 | test -z "$$out"; \
59 | fi
60 |
61 | imports:
62 | @if test -n "$(FILES_TO_CHECK)"; then \
63 | echo 'Running \033[0;32mgoimports\033[0m'; \
64 | out=$$($(GOIMPORTS) -l $(FILES_TO_CHECK)); \
65 | echo $$out; \
66 | test -z "$$out"; \
67 | fi
68 |
69 | check: fmt imports lint
70 |
71 | coveralls:
72 | @echo '\033[0;32mPublishing coverage\033[0m'; \
73 | $(GOVERALLS) -coverprofile=$(COVERPROFILE) -service=travis-ci
74 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc=
2 | github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
3 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
4 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
5 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
6 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
7 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
8 | github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
9 | github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
10 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
11 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
12 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
13 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
14 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
15 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
16 | golang.org/x/tools v0.0.0-20190325161752-5a8dccf5b48a h1:iEgSlyueP+hVXFS7PZk7z5e23iHin+tpXArziYTt574=
17 | golang.org/x/tools v0.0.0-20190325161752-5a8dccf5b48a/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
18 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
19 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
20 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
21 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
22 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
23 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
24 |
--------------------------------------------------------------------------------
/retrodep/doc.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2018, 2019 Tim Waugh
2 | //
3 | // This program is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU General Public License as published by
5 | // the Free Software Foundation, either version 3 of the License, or
6 | // (at your option) any later version.
7 | //
8 | // This program is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU General Public License
14 | // along with this program. If not, see .
15 |
16 | // Package retrodep provides a way to represent Go source code in a
17 | // filesystem, and taken from a source code repository. It allows
18 | // mapping vendored packages back to the original versions they came
19 | // from.
20 | //
21 | // A GoSource represents a filesystem tree containing Go source
22 | // code. Create it using NewGoSource or FindGoSources. The Project and
23 | // VendoredProjects methods return information about the top-level
24 | // project and the vendored projects it has.
25 | //
26 | // src := retrodep.NewGoSource(path, nil)
27 | // proj, perr := src.Project(importPath)
28 | // vendored, verr := src.VendoredProjects()
29 | //
30 | // Both of these methods use RepoPath to describe the projects. If a
31 | // glide configuration file is found, Version will be filled in for
32 | // each vendored dependency.
33 | //
34 | // The FindGoSources function looks for Go source code in the provided
35 | // path. If it is not found there, the immediate subdirectories are
36 | // searched. This function allows for repositories which are
37 | // collections of independently-vendored projects.
38 | //
39 | // The NewWorkingTree function makes a temporary local copy of the
40 | // upstream repository.
41 | //
42 | // wt, err := retrodep.NewWorkingTree(&proj.RepoRoot)
43 | //
44 | // The DescribeProject function takes a RepoPath, a WorkingTree, and
45 | // path within the tree, and returns a Representation, indicating the
46 | // upstream version of the project or vendored project, e.g.
47 | //
48 | // ref, rerr := retrodep.DescribeProject(proj, wt, src.Path)
49 | //
50 | // It does this by comparing file hashes of the local files with those
51 | // from commits in the upstream repository.
52 | package retrodep
53 |
--------------------------------------------------------------------------------
/retrodep/exec_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2018 Tim Waugh
2 | //
3 | // This program is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU General Public License as published by
5 | // the Free Software Foundation, either version 3 of the License, or
6 | // (at your option) any later version.
7 | //
8 | // This program is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU General Public License
14 | // along with this program. If not, see .
15 |
16 | package retrodep
17 |
18 | import (
19 | "fmt"
20 | "os"
21 | "os/exec"
22 | "strconv"
23 | "testing"
24 | )
25 |
26 | const (
27 | envHelper = "GO_WANT_HELPER_PROCESS"
28 | envStdout = "STDOUT"
29 | envStderr = "STDERR"
30 | envExitStatus = "EXIT_STATUS"
31 | )
32 |
33 | var mockedExitStatus int
34 | var mockedStdout, mockedStderr string
35 |
36 | // Capture exec.Command calls via execCommand and make them run our
37 | // fake version instead. This returns a function which the caller
38 | // should defer a call to in order to reset execCommand.
39 | func mockExecCommand() func() {
40 | execCommand = fakeExecCommand
41 |
42 | // Reset it afterwards
43 | return func() {
44 | execCommand = exec.Command
45 | mockedExitStatus = 0
46 | mockedStdout = ""
47 | mockedStderr = ""
48 | }
49 | }
50 |
51 | // Run this test binary (again!) but transfer control immediately to
52 | // TestHelper, telling it how to act.
53 | func fakeExecCommand(command string, args ...string) *exec.Cmd {
54 | testBinary := os.Args[0]
55 | opts := []string{"-test.run=TestHelper", "--", command}
56 | opts = append(opts, args...)
57 | cmd := exec.Command(testBinary, opts...)
58 | cmd.Env = []string{
59 | envHelper + "=1",
60 | envStdout + "=" + mockedStdout,
61 | envStderr + "=" + mockedStderr,
62 | envExitStatus + "=" + strconv.Itoa(mockedExitStatus),
63 | }
64 | return cmd
65 | }
66 |
67 | // This runs in its own process (see fakeExecCommand) and mocks the
68 | // command being run.
69 | func TestHelper(t *testing.T) {
70 | if os.Getenv(envHelper) != "1" {
71 | return
72 | }
73 | fmt.Print(os.Getenv(envStdout))
74 | fmt.Fprint(os.Stderr, os.Getenv(envStderr))
75 | exit, _ := strconv.Atoi(os.Getenv(envExitStatus))
76 | os.Exit(exit)
77 | }
78 |
--------------------------------------------------------------------------------
/extras/retrodiff:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Copyright (C) 2019, 2020 Tim Waugh
4 | #
5 | # This program is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation, either version 3 of the License, or
8 | # (at your option) any later version.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 |
18 | # This is a simple wrapper script to read container.yaml files
19 | # (http://osbs.readthedocs.io/) to find enough information to give to
20 | # retrodep in -diff mode. It also ignores files used for 'dist-git'
21 | # layouts.
22 |
23 | if [ $# -lt 1 ]; then
24 | echo >&2 "Supply VERSION, then optional retrodep flags"
25 | exit 1
26 | fi
27 | VERSION=$1
28 | shift
29 |
30 | # Find import path from container.yaml
31 | MODULES=$(python /dev/fd/3 3<<"EOF" .
15 |
16 | package main
17 |
18 | import (
19 | "io"
20 | "io/ioutil"
21 | "os"
22 | "syscall"
23 | "testing"
24 |
25 | "github.com/release-engineering/retrodep/v2/retrodep"
26 | )
27 |
28 | func captureStdout(t *testing.T) (r io.Reader, reset func()) {
29 | stdout := int(os.Stdout.Fd())
30 | orig, err := syscall.Dup(stdout)
31 | if err != nil {
32 | t.Fatal(err)
33 | }
34 | r, w, err := os.Pipe()
35 | if err != nil {
36 | t.Fatal(err)
37 | }
38 | err = syscall.Dup2(int(w.Fd()), stdout)
39 | if err != nil {
40 | t.Fatal(err)
41 | }
42 |
43 | reset = func() {
44 | w.Close()
45 | err := syscall.Dup2(orig, stdout)
46 | if err != nil {
47 | t.Fatal(err)
48 | }
49 | }
50 |
51 | return
52 | }
53 |
54 | func TestDisplayUnknown(t *testing.T) {
55 | tcs := []struct {
56 | name string
57 | ref *retrodep.Reference
58 | templateArg string
59 | expected string
60 | }{
61 | {
62 | "nil ref, empty templateArg",
63 | nil,
64 | "",
65 | "*example.com/foo ?\n",
66 | },
67 | {
68 | "with ref, non-zero templateArg",
69 | &retrodep.Reference{Pkg: "example.com/foo"},
70 | "filled templateArg",
71 | "*example.com/foo ?\n",
72 | },
73 | }
74 |
75 | for _, tc := range tcs {
76 | tc := tc
77 |
78 | t.Run(tc.name, func(t *testing.T) {
79 | *templateArg = tc.templateArg
80 | r, reset := captureStdout(t)
81 | displayUnknown(nil, "*", tc.ref, "example.com/foo")
82 | reset()
83 | output, err := ioutil.ReadAll(r)
84 | if err != nil {
85 | t.Fatal(err)
86 | }
87 | if string(output) != tc.expected {
88 | t.Errorf("expected %v but got %v",
89 | tc.expected, string(output))
90 | }
91 | })
92 | }
93 | }
94 |
95 | func TestGetTemplate(t *testing.T) {
96 | tcs := []struct {
97 | name string
98 | args []string
99 | expected string
100 | }{
101 | {
102 | "default",
103 | []string{"retrodep", "."},
104 | defaultTemplate,
105 | },
106 | {
107 | "go-template",
108 | []string{"retrodep", "-o", "go-template={{.Pkg}}", "."},
109 | "{{.Pkg}}",
110 | },
111 | {
112 | "compatibility",
113 | []string{"retrodep", "-template", "@{{.Rev}}", "."},
114 | "{{.Pkg}}@{{.Rev}}",
115 | },
116 | }
117 |
118 | for _, tc := range tcs {
119 | tc := tc
120 |
121 | // Reset the flags.
122 | *templateArg = ""
123 | *outputArg = ""
124 |
125 | t.Run(tc.name, func(t *testing.T) {
126 | processArgs(tc.args)
127 | tmpl := getTemplate()
128 | if tmpl != tc.expected {
129 | t.Errorf("expected %v but got %v",
130 | tc.expected, tmpl)
131 | }
132 | })
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/retrodep/vendored_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2018, 2019 Tim Waugh
2 | //
3 | // This program is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU General Public License as published by
5 | // the Free Software Foundation, either version 3 of the License, or
6 | // (at your option) any later version.
7 | //
8 | // This program is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU General Public License
14 | // along with this program. If not, see .
15 |
16 | package retrodep
17 |
18 | import (
19 | "testing"
20 | )
21 |
22 | func TestVendoredProjects(t *testing.T) {
23 | src, err := NewGoSource("testdata/gosource", nil)
24 | if err != nil {
25 | t.Fatal(err)
26 | }
27 | expected := []string{
28 | "github.com/eggs/ham",
29 | "github.com/foo/bar",
30 | }
31 | got, err := src.VendoredProjects()
32 | if err != nil {
33 | t.Fatal(err)
34 | }
35 | matched := len(got) == len(expected)
36 | if !matched {
37 | t.Errorf("%d != %d", len(got), len(expected))
38 | }
39 | if matched {
40 | for _, repo := range expected {
41 | if _, ok := got[repo]; !ok {
42 | t.Errorf("%s not returned", repo)
43 | matched = false
44 | break
45 | }
46 | }
47 | }
48 | if !matched {
49 | t.Errorf("%v != %v", got, expected)
50 | }
51 | }
52 |
53 | func TestChooseBestTag(t *testing.T) {
54 | tags := []string{
55 | "1.2.3-beta1",
56 | "1.2.2",
57 | "1.2.2-beta2",
58 | }
59 | best := chooseBestTag(tags)
60 | if best != "1.2.2" {
61 | t.Errorf("wrong best tag (%s)", best)
62 | }
63 | }
64 |
65 | type dummyHasher struct{}
66 |
67 | func (h *dummyHasher) Hash(abs, rel string) (FileHash, error) {
68 | return "foo", nil
69 | }
70 |
71 | type mockVendorWorkingTree struct {
72 | stubWorkingTree
73 |
74 | localHashes FileHashes
75 | }
76 |
77 | const matchVersion = "v1.0.0"
78 | const matchRevision = "0123456789abcdef"
79 |
80 | func (wt *mockVendorWorkingTree) FileHashesFromRef(ref, _ string) (FileHashes, error) {
81 | if ref == matchRevision || ref == matchVersion {
82 | // Pretend v1.0.0 is an exact copy of the local files.
83 | return wt.localHashes, nil
84 | }
85 |
86 | // Pretend all other refs have no content at all.
87 | return make(FileHashes), nil
88 | }
89 |
90 | func (wt *mockVendorWorkingTree) RevisionFromTag(tag string) (string, error) {
91 | if tag != matchVersion {
92 | return "", ErrorVersionNotFound
93 | }
94 | return matchRevision, nil
95 | }
96 |
97 | func (wt *mockVendorWorkingTree) ReachableTag(rev string) (tag string, err error) {
98 | if rev == matchVersion {
99 | tag = rev
100 | } else {
101 | err = ErrorVersionNotFound
102 | }
103 | return
104 | }
105 |
106 | func (wt *mockVendorWorkingTree) VersionTags() ([]string, error) {
107 | return []string{"v2.0.0", "v1.0.0"}, nil
108 | }
109 |
110 | func TestDescribeProject(t *testing.T) {
111 | src, err := NewGoSource("testdata/gosource", nil)
112 | if err != nil {
113 | t.Fatal(err)
114 | }
115 |
116 | proj, err := src.Project("github.com/foo/bar")
117 | if err != nil {
118 | t.Fatal(err)
119 | }
120 |
121 | wt := &mockVendorWorkingTree{}
122 | wt.hasher = &dummyHasher{}
123 |
124 | // Make a copy of the local file hashes, so we can mock them
125 | // for "v1.0.0" in the working tree.
126 | wt.localHashes, err = src.hashLocalFiles(wt, proj, src.Path)
127 | if err != nil {
128 | t.Fatal(err)
129 | }
130 |
131 | ref, err := src.DescribeProject(proj, wt, src.Path, nil)
132 | if err != nil {
133 | t.Fatal(err)
134 | }
135 |
136 | if ref.Ver != matchVersion {
137 | t.Errorf("Version: got %s but expected %s", ref.Ver, matchVersion)
138 | }
139 | if ref.Rev != matchRevision {
140 | t.Errorf("Revision: got %s but expected %s", ref.Rev, matchRevision)
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/retrodep/filehash_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2018, 2019 Tim Waugh
2 | //
3 | // This program is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU General Public License as published by
5 | // the Free Software Foundation, either version 3 of the License, or
6 | // (at your option) any later version.
7 | //
8 | // This program is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU General Public License
14 | // along with this program. If not, see .
15 |
16 | package retrodep
17 |
18 | import (
19 | "sort"
20 | "testing"
21 | )
22 |
23 | func TestSha256Hasher(t *testing.T) {
24 | h := sha256Hasher{}
25 | // from sha256sum:
26 | emptysum := FileHash("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
27 | fh, err := h.Hash("", "testdata/gosource/ignored.go")
28 | if err != nil {
29 | t.Fatal(err)
30 | }
31 | if fh != emptysum {
32 | t.Errorf("unexpected hash: got %s, want %s", fh, emptysum)
33 | }
34 | }
35 |
36 | func TestNewFileHashes(t *testing.T) {
37 | hashes, err := NewFileHashes(&gitHasher{}, "testdata/gosource", nil)
38 | if err != nil {
39 | t.Fatal(err)
40 | }
41 | if hashes == nil {
42 | t.Fatal("NewFileHashes returned nil map")
43 | }
44 | emptyhash := FileHash("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391")
45 | expected := map[string]FileHash{
46 | "ignored.go": emptyhash,
47 | "vendor/github.com/foo/bar/bar.go": emptyhash,
48 | "vendor/github.com/eggs/ham/ham.go": emptyhash,
49 | "vendor/github.com/eggs/ham/spam/ignored.go": emptyhash,
50 | }
51 | if len(hashes) != len(expected) {
52 | t.Fatalf("len(hashes[%v]) != %d", hashes, len(expected))
53 | }
54 | for key, value := range expected {
55 | got, ok := hashes[key]
56 | if !ok {
57 | t.Errorf("%s missing", key)
58 | continue
59 | }
60 | if got != value {
61 | t.Errorf("%s: wrong hash (%s != %s)", key, got, value)
62 | }
63 | }
64 | }
65 |
66 | func TestNewFileHashesExclude(t *testing.T) {
67 | excludes := make(map[string]struct{})
68 | excludes["testdata/gosource/ignored.go"] = struct{}{}
69 | hashes, err := NewFileHashes(&gitHasher{}, "testdata/gosource", excludes)
70 | if err != nil {
71 | t.Fatal(err)
72 | }
73 | emptyhash := FileHash("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391")
74 | expected := map[string]FileHash{
75 | "vendor/github.com/foo/bar/bar.go": emptyhash,
76 | "vendor/github.com/eggs/ham/ham.go": emptyhash,
77 | "vendor/github.com/eggs/ham/spam/ignored.go": emptyhash,
78 | }
79 | if len(hashes) != len(expected) {
80 | t.Fatalf("len(hashes[%v]) != %d", hashes, len(expected))
81 | }
82 | for key, value := range expected {
83 | got, ok := hashes[key]
84 | if !ok {
85 | t.Errorf("%s missing", key)
86 | continue
87 | }
88 | if got != value {
89 | t.Errorf("%s: wrong hash (%s != %s)", key, got, value)
90 | }
91 | }
92 | }
93 |
94 | func TestIsSubsetOf(t *testing.T) {
95 | hasher := &gitHasher{}
96 | hashes, err := NewFileHashes(hasher, "testdata/gosource", nil)
97 | if err != nil {
98 | t.Fatal(err)
99 | }
100 |
101 | if !hashes.IsSubsetOf(hashes) {
102 | t.Fatalf("not subset of self")
103 | }
104 |
105 | other := make(FileHashes)
106 | for k, v := range hashes {
107 | other[k] = v
108 | }
109 | hashes["foo"] = FileHash("")
110 | if hashes.IsSubsetOf(other) {
111 | t.Fail()
112 | }
113 | }
114 |
115 | func TestMismatches(t *testing.T) {
116 | hasher := &gitHasher{}
117 | hashes, err := NewFileHashes(hasher, "testdata/gosource", nil)
118 | if err != nil {
119 | t.Fatal(err)
120 | }
121 |
122 | if hashes.Mismatches(hashes, false) != nil {
123 | t.Fatalf("mismatches self")
124 | }
125 |
126 | other := make(FileHashes)
127 | for k, v := range hashes {
128 | other[k] = v
129 | }
130 |
131 | other["foo"] = FileHash("")
132 | if hashes.Mismatches(hashes, false) != nil {
133 | t.Fatalf("extra value in s reported as mismatch")
134 | }
135 |
136 | eq := func(a sort.StringSlice, b sort.StringSlice) bool {
137 | if len(a) != len(b) {
138 | return false
139 | }
140 | a.Sort()
141 | b.Sort()
142 | for i, v := range a {
143 | if b[i] != v {
144 | return false
145 | }
146 | }
147 | return true
148 | }
149 |
150 | hashes["foo"] = FileHash("123")
151 | hashes["bar"] = FileHash("123")
152 | mismatches := hashes.Mismatches(other, false)
153 | if !eq(mismatches, []string{"foo", "bar"}) {
154 | t.Errorf("got %v, expected {\"foo\", \"bar\"}", mismatches)
155 | }
156 |
157 | mismatches = hashes.Mismatches(other, true)
158 | if len(mismatches) != 1 {
159 | t.Errorf("too many mismatches returned: %v", mismatches)
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/retrodep/hg.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2018, 2019 Tim Waugh
2 | //
3 | // This program is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU General Public License as published by
5 | // the Free Software Foundation, either version 3 of the License, or
6 | // (at your option) any later version.
7 | //
8 | // This program is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU General Public License
14 | // along with this program. If not, see .
15 |
16 | package retrodep
17 |
18 | import (
19 | "encoding/xml"
20 | "fmt"
21 | "io/ioutil"
22 | "os"
23 | "path/filepath"
24 | "strings"
25 | "time"
26 |
27 | "github.com/Masterminds/semver"
28 | "github.com/pkg/errors"
29 | )
30 |
31 | // This file contains methods specific to working with hg.
32 |
33 | type hgWorkingTree struct {
34 | anyWorkingTree
35 | }
36 |
37 | type hgLogEntry struct {
38 | Node string `xml:"node,attr"`
39 | Date []byte `xml:"date"`
40 | Tag string `xml:"tag"`
41 | }
42 | type hgLogs struct {
43 | XMLName xml.Name `xml:"log"`
44 | LogEntries []hgLogEntry `xml:"logentry"`
45 | }
46 |
47 | /// log runs 'hg log --template xml', with the additional args if args
48 | /// is not nil, and returns the log entries. If expect is not 0, an
49 | /// error is returned if the number of log entries is different.
50 | func (h *hgWorkingTree) log(args []string, expect int) ([]hgLogEntry, error) {
51 | logArgs := []string{"log", "--encoding", "utf-8", "--template", "xml"}
52 | if args != nil {
53 | logArgs = append(logArgs, args...)
54 | }
55 | stdout, stderr, err := h.run(logArgs...)
56 | if err != nil {
57 | h.showOutput(stdout, stderr)
58 | return nil, err
59 | }
60 | var logs hgLogs
61 | err = xml.Unmarshal(stdout.Bytes(), &logs)
62 | if err != nil {
63 | return nil, err
64 | }
65 | entries := logs.LogEntries
66 | if expect != 0 && len(entries) != expect {
67 | return nil, fmt.Errorf(
68 | "unexpected log output: %s: %d logentry elements (expected %d)",
69 | strings.Join(logArgs, " "), len(entries), expect)
70 | }
71 | return entries, nil
72 | }
73 |
74 | // Revisions returns all revisions in the hg repository, using 'hg log'.
75 | func (h *hgWorkingTree) Revisions() ([]string, error) {
76 | entries, err := h.log(nil, 0)
77 | if err != nil {
78 | return nil, err
79 | }
80 | revisions := make([]string, 0)
81 | for _, entry := range entries {
82 | revisions = append(revisions, entry.Node)
83 | }
84 | return revisions, nil
85 | }
86 |
87 | // RevisionFromTag returns the revision for the given tag, using 'hg
88 | // log -r "tag(...)"'.
89 | func (h *hgWorkingTree) RevisionFromTag(tag string) (string, error) {
90 | entries, err := h.log([]string{"-r", "tag(" + tag + ")"}, 1)
91 | if err != nil {
92 | return "", err
93 | }
94 | return entries[0].Node, nil
95 | }
96 |
97 | // RevSync updates the working tree to reflect the revision rev, using
98 | // 'hg update -r ...'. The working tree must not have been locally
99 | // modified.
100 | func (h *hgWorkingTree) RevSync(rev string) error {
101 | return h.anyWorkingTree.TagSync(rev)
102 | }
103 |
104 | // TimeFromRevision returns the commit timestamp for the revision
105 | // rev, using 'hg log -r ...'.
106 | func (h *hgWorkingTree) TimeFromRevision(rev string) (time.Time, error) {
107 | var t time.Time
108 | entries, err := h.log([]string{"-r", rev}, 1)
109 | if err != nil {
110 | return t, err
111 | }
112 | err = t.UnmarshalText(entries[0].Date)
113 | return t, err
114 | }
115 |
116 | // ReachableTag returns the most recent reachable semver tag, using hg
117 | // log -r "ancestors(...) & tag(r're:...')". It fails with
118 | // ErrorVersionNotFound if no suitable tag is found.
119 | func (h *hgWorkingTree) ReachableTag(rev string) (string, error) {
120 | // Find up to 10 reachable tags from the revision that might be semver tags
121 | revset := "ancestors(" + rev + ") & tag(r're:v?[0-9]')"
122 | entries, err := h.log([]string{"-r", revset, "--limit", "10"}, 0)
123 | if err != nil {
124 | return "", err
125 | }
126 |
127 | if len(entries) == 0 {
128 | return "", ErrorVersionNotFound
129 | }
130 |
131 | // If any is a semver tag, use that
132 | for _, entry := range entries {
133 | _, err := semver.NewVersion(entry.Tag)
134 | if err == nil {
135 | return entry.Tag, nil
136 | }
137 | }
138 |
139 | // Otherwise just take the first one
140 | return entries[0].Tag, nil
141 | }
142 |
143 | // FileHashesFromRef returns the file hashes for the given tag or
144 | // revision ref.
145 | func (h *hgWorkingTree) FileHashesFromRef(ref, subPath string) (FileHashes, error) {
146 | dir, err := ioutil.TempDir("", "retrodep.")
147 | if err != nil {
148 | return nil, errors.Wrapf(err, "FileHashesFromRef(%s)", ref)
149 | }
150 | defer os.RemoveAll(dir)
151 |
152 | args := []string{"archive", "-r", ref, "--type", "files"}
153 | if subPath != "" {
154 | args = append(args, "--prefix", subPath)
155 | }
156 | args = append(args, dir)
157 | stdout, stderr, err := h.run(args...)
158 | if err != nil {
159 | h.showOutput(stdout, stderr)
160 | return nil, err
161 | }
162 | return NewFileHashes(&sha256Hasher{}, filepath.Join(dir, subPath), nil)
163 | }
164 |
--------------------------------------------------------------------------------
/retrodep/filehash.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2018, 2019 Tim Waugh
2 | //
3 | // This program is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU General Public License as published by
5 | // the Free Software Foundation, either version 3 of the License, or
6 | // (at your option) any later version.
7 | //
8 | // This program is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU General Public License
14 | // along with this program. If not, see .
15 |
16 | package retrodep
17 |
18 | import (
19 | "bufio"
20 | "crypto/sha256"
21 | "encoding/hex"
22 | "io"
23 | "os"
24 | "path"
25 | "path/filepath"
26 | "strings"
27 |
28 | "github.com/pkg/errors"
29 | )
30 |
31 | // FileHash records the hash of a file in the format preferred by the
32 | // version control system that tracks it.
33 | type FileHash string
34 |
35 | // Hasher is the interface that wraps the Hash method.
36 | type Hasher interface {
37 | // Hash returns the file hash for the filename absPath, hashed
38 | // as though it were in the repository as filename
39 | // relativePath.
40 | Hash(relativePath, absPath string) (FileHash, error)
41 | }
42 |
43 | type sha256Hasher struct{}
44 |
45 | // Hash implements the Hasher interface generically using sha256.
46 | func (h sha256Hasher) Hash(relativePath, absPath string) (FileHash, error) {
47 | f, err := os.Open(absPath)
48 | if err != nil {
49 | return FileHash(""), errors.Wrapf(err, "hashing %s", absPath)
50 | }
51 | defer f.Close()
52 |
53 | hash := sha256.New()
54 | _, err = io.Copy(hash, f)
55 | if err != nil {
56 | return FileHash(""), errors.Wrapf(err, "hashing %s", absPath)
57 | }
58 |
59 | return FileHash(hex.EncodeToString(hash.Sum(nil))), nil
60 | }
61 |
62 | // FileHashes is a map of paths, relative to the top-level of the
63 | // version control system, to their hashes.
64 | type FileHashes map[string]FileHash
65 |
66 | // NewFileHashes returns a new FileHashes from a filesystem tree at root,
67 | // whose files belong to the version control system named in vcsCmd. Keys in
68 | // the excludes map are filenames to ignore.
69 | func NewFileHashes(h Hasher, root string, excludes map[string]struct{}) (FileHashes, error) {
70 | hashes := make(FileHashes)
71 | root = path.Clean(root)
72 |
73 | // Make a local copy of excludes we can safely modify
74 | excl := make(map[string]struct{})
75 | if excludes != nil {
76 | for k, v := range excludes {
77 | excl[k] = v
78 | }
79 | }
80 |
81 | walkfn := func(path string, info os.FileInfo, err error) error {
82 | if err != nil {
83 | return err
84 | }
85 | if _, skip := excl[path]; skip {
86 | // This pathname has been ignored, either by caller
87 | // request or due to .gitattributes
88 | if info.IsDir() {
89 | return filepath.SkipDir
90 | }
91 | return nil
92 | }
93 | if info.IsDir() {
94 | // Check for .gitattributes in this directory
95 | // FIXME: gitattributes(5) describes a more complex file
96 | // format than handled here. Can git-check-attr(1) help?
97 | ga, err := os.Open(filepath.Join(path, ".gitattributes"))
98 | if err != nil {
99 | if os.IsNotExist(err) {
100 | err = nil
101 | }
102 | return err
103 | }
104 | defer ga.Close()
105 |
106 | scanner := bufio.NewScanner(bufio.NewReader(ga))
107 | for scanner.Scan() {
108 | fields := strings.Fields(scanner.Text())
109 | if len(fields) < 2 {
110 | continue
111 | }
112 | for _, field := range fields[1:] {
113 | if field == "export-subst" {
114 | // Not expected to have matching hash
115 | fn := filepath.Join(path, fields[0])
116 | excl[fn] = struct{}{}
117 | break
118 | }
119 | }
120 | }
121 |
122 | return nil
123 | }
124 | if !info.Mode().IsRegular() {
125 | return nil
126 | }
127 | relativePath, err := filepath.Rel(root, path)
128 | if err != nil {
129 | return err
130 | }
131 |
132 | fileHash, err := h.Hash(relativePath, path)
133 | if err != nil {
134 | return err
135 | }
136 | hashes[relativePath] = fileHash
137 | return nil
138 | }
139 | err := filepath.Walk(root, walkfn)
140 | if err != nil {
141 | return nil, err
142 | }
143 | return hashes, nil
144 | }
145 |
146 | // IsSubsetOf returns true if these file hashes are a subset of s.
147 | func (h FileHashes) IsSubsetOf(s FileHashes) bool {
148 | return h.Mismatches(s, true) == nil
149 | }
150 |
151 | // Mismatches returns a slice of filenames from h whose hashes
152 | // mismatch those in s. If failFast is true at most one mismatch will
153 | // be returned.
154 | func (h FileHashes) Mismatches(s FileHashes, failFast bool) []string {
155 | var mismatches []string
156 | for path, fileHash := range h {
157 | sh, ok := s[path]
158 | if !ok {
159 | // File not present in s
160 | log.Debugf("%s: not present", path)
161 | mismatches = append(mismatches, path)
162 | } else if fileHash != sh {
163 | // Hash does not match
164 | log.Debugf("%s: hash mismatch", path)
165 | mismatches = append(mismatches, path)
166 | }
167 |
168 | if failFast && mismatches != nil {
169 | return mismatches
170 | }
171 | }
172 |
173 | return mismatches
174 | }
175 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Retrodep
2 | ========
3 |
4 | [](https://goreportcard.com/report/github.com/release-engineering/retrodep)
5 | [](https://godoc.org/github.com/release-engineering/retrodep)
6 | [](https://travis-ci.org/release-engineering/retrodep)
7 | [](https://coveralls.io/github/release-engineering/retrodep)
8 |
9 | This command inspects a Go source tree with vendored packages and attempts to work out the versions of the packages which are vendored, as well as the version of top-level package itself.
10 |
11 | It does this by comparing file hashes of the packages with those from the upstream repositories.
12 |
13 | If no semantic version tag matches but a commit is found that matches, a pseudo-version is generated.
14 |
15 | Installation
16 | ------------
17 |
18 | ```
19 | go get github.com/release-engineering/retrodep
20 | ```
21 |
22 | Running
23 | -------
24 |
25 | ```
26 | retrodep: help requested
27 | usage: retrodep [OPTION]... PATH
28 | -debug
29 | show debugging output
30 | -deps
31 | show vendored dependencies (default true)
32 | -diff string
33 | compare with upstream ref (implies -deps=false)
34 | -exclude-from exclusions
35 | ignore directory entries matching globs in exclusions
36 | -help
37 | print help
38 | -importpath string
39 | top-level import path
40 | -o string
41 | output format, one of: go-template=...
42 | -only-importpath
43 | only show the top-level import path
44 | -template string
45 | go template to use for output with Reference fields (deprecated)
46 | -x exit on the first failure
47 | ```
48 |
49 | In many cases retrodep can work out the import path for the top-level project. In those cases, simply supply the directory name to examine:
50 | ```
51 | $ retrodep src
52 | ```
53 |
54 | If it cannot determine the import path, provide it with -importpath:
55 | ```
56 | $ retrodep -importpath github.com/example/name src
57 | ```
58 |
59 | By default both the top-level project and its vendored dependencies are examined. To ignore vendored dependencies supply -deps=false:
60 | ```
61 | $ retrodep -deps=false -importpath github.com/example/name src
62 | ```
63 |
64 | If there are additional local files not expected to be part of the upstream version they can be excluded:
65 | ```
66 | $ cat exclusions
67 | .git
68 | Dockerfile
69 | $ ls -d src/Dockerfile src/.git
70 | src/Dockerfile
71 | src/.git
72 | $ retrodep -exclude-from=exclusions src
73 | ```
74 |
75 | Exit code
76 | ---------
77 |
78 | | Exit code | Reason |
79 | | ---------:|:------------------------------------------------ |
80 | | 0 | all versions were found (or -diff: no changes) |
81 | | 1 | any error was encountered other than those below |
82 | | 2 | a version was missing |
83 | | 3 | import path needed but not supplied |
84 | | 4 | no Go source code was found at the provided path |
85 | | 5 | in -diff mode, changes were found |
86 |
87 | Example output
88 | --------------
89 |
90 | ```
91 | $ retrodep $GOPATH/src/github.com/docker/distribution
92 | github.com/docker/distribution:v2.7.1
93 | github.com/docker/distribution:v2.7.1/github.com/Azure/azure-sdk-for-go:v16.2.1
94 | github.com/docker/distribution:v2.7.1/github.com/Azure/go-autorest:v10.8.1
95 | github.com/docker/distribution:v2.7.1/github.com/Shopify/logrus-bugsnag:v0.0.0-0.20171204154709-577dee27f20d
96 | github.com/docker/distribution:v2.7.1/github.com/aws/aws-sdk-go:v1.15.11
97 | github.com/docker/distribution:v2.7.1/github.com/beorn7/perks:v0.0.0-0.20160804124726-4c0e84591b9a
98 | github.com/docker/distribution:v2.7.1/github.com/bshuster-repo/logrus-logstash-hook:0.4
99 | github.com/docker/distribution:v2.7.1/github.com/bugsnag/bugsnag-go:v1.0.3-0.20150204195350-f36a9b3a9e01
100 | ...
101 | ```
102 |
103 | In this example,
104 |
105 | * github.com/docker/distribution is the top-level package, and the upstream semantic version tag v2.7.1 matches
106 | * github.com/Azure/azure-sdk-for-go etc are vendored dependencies of distribution
107 | * github.com/Azure/azure-sdk-for-go, github.com/Azure/go-autorest, github.com/aws/awk-sdk-go, and github.com/bshuster-repo/logrus-logstash-hook all had matches with upstream semantic version tags
108 | * github.com/bugsnag/bugsnag-go matched a commit from which tag v1.0.2 was reachable (note: v1.0.2, not v1.0.3 -- see below)
109 | * github.com/beorn7/perks matched a commit from which there were no reachable semantic version tags
110 |
111 | Pseudo-versions
112 | ---------------
113 |
114 | The pseudo-versions generated by this tool are:
115 |
116 | * v0.0.0-0.yyyyddmmhhmmss-abcdefabcdef (commit with no relative tag)
117 | * vX.Y.Z-pre.0.yyyyddmmhhmmss-abcdefabcdef (commit after semver vX.Y.Z-pre)
118 | * vX.Y.(Z+1)-0.yyyyddmmhhmmss-abcdefabcdef (commit after semver vX.Y.Z)
119 | * tag-1.yyyyddmmhhmmss-abcdefabcdef (commit after tag)
120 |
121 | Diff mode
122 | ---------
123 |
124 | When supplying the -diff option, retrodep compares with a specific
125 | version only, and outputs the differences (in unified diff format)
126 | between the local files and the upstream files.
127 |
128 | To compare source code in src with a known upstream version of a package, use it like this:
129 | ```
130 | $ retrodep -diff v1.2.0 github.com/example/name src
131 | ```
132 |
133 | No output (and a zero exit code) means the source code in src matches
134 | the upstream version v1.2.0 of github.com/example/name. Otherwise, the
135 | differences in src compared to the upstream version are shown in
136 | unified diff format, and the exit code is 5.
137 |
138 | Files in src that are not in the upstream version are presented as
139 | diffs compared with "/dev/null". Files in the upstream version but not
140 | in src are ignored.
141 |
142 | Limitations
143 | -----------
144 |
145 | The vendor directory is assumed to be complete.
146 |
147 | Original source code is assumed to be available.
148 |
149 | Only git and repositories are currently supported, and working 'git' and 'hg' executables are assumed to be available.
150 |
151 | Non-Go code is not considered, e.g. binary-only packages, or CGo.
152 |
153 | Commits with additional files (e.g. \*\_linux.go) are identified as matching when they should not.
154 |
155 | Packages vendored from forks will not have matching commits.
156 |
157 | Files marked as "export-subst" in .gitattributes files in the vendored copy are ignored.
158 |
--------------------------------------------------------------------------------
/retrodep/git.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2018, 2019 Tim Waugh
2 | //
3 | // This program is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU General Public License as published by
5 | // the Free Software Foundation, either version 3 of the License, or
6 | // (at your option) any later version.
7 | //
8 | // This program is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU General Public License
14 | // along with this program. If not, see .
15 |
16 | package retrodep
17 |
18 | // This file contains methods specific to working with git.
19 |
20 | import (
21 | "bufio"
22 | "bytes"
23 | "fmt"
24 | "os"
25 | "os/exec"
26 | "path/filepath"
27 | "strings"
28 | "time"
29 |
30 | "github.com/pkg/errors"
31 | )
32 |
33 | type gitWorkingTree struct {
34 | anyWorkingTree
35 | }
36 |
37 | // Revisions returns all revisions in the git repository, using 'git
38 | // rev-list --all'.
39 | func (g *gitWorkingTree) Revisions() ([]string, error) {
40 | stdout, stderr, err := g.run("rev-list", "--all")
41 | if err != nil {
42 | g.showOutput(stdout, stderr)
43 | return nil, err
44 | }
45 | revisions := make([]string, 0)
46 | output := bufio.NewScanner(stdout)
47 | for output.Scan() {
48 | revisions = append(revisions, strings.TrimSpace(output.Text()))
49 | }
50 | return revisions, nil
51 | }
52 |
53 | // RevisionFromTag returns the commit hash for the given tag, using
54 | // 'git rev-parse ...'
55 | func (g *gitWorkingTree) RevisionFromTag(tag string) (string, error) {
56 | stdout, stderr, err := g.run("rev-parse", tag)
57 | if err != nil {
58 | g.showOutput(stdout, stderr)
59 | return "", err
60 | }
61 | rev := strings.TrimSpace(stdout.String())
62 | return rev, nil
63 | }
64 |
65 | // RevSync updates the working tree to reflect the revision rev, using
66 | // 'git checkout ...'. The working tree must not have been locally
67 | // modified.
68 | func (g *gitWorkingTree) RevSync(rev string) error {
69 | stdout, stderr, err := g.run("checkout", rev)
70 | if err != nil {
71 | g.showOutput(stdout, stderr)
72 | }
73 | return err
74 | }
75 |
76 | // TimeFromRevision returns the commit timestamp for the revision
77 | // rev, using 'git show -s --pretty=format:%cI ...'.
78 | func (g *gitWorkingTree) TimeFromRevision(rev string) (time.Time, error) {
79 | run := g.run
80 | var t time.Time
81 | stdout, stderr, err := run("show", "-s", "--pretty=format:%cI", rev)
82 | if err != nil {
83 | g.showOutput(stdout, stderr)
84 | return t, err
85 | }
86 |
87 | t, err = time.Parse(time.RFC3339, strings.TrimSpace(stdout.String()))
88 | return t, err
89 | }
90 |
91 | // ReachableTag returns the most recent reachable semver tag, using
92 | // 'git describe --tags --match=...', with match globs for tags that
93 | // are likely to be semvers. It returns ErrorVersionNotFound if no
94 | // suitable tag is found.
95 | func (g *gitWorkingTree) ReachableTag(rev string) (string, error) {
96 | run := g.run
97 | var tag string
98 | for _, match := range []string{"v[0-9]*", "[0-9]*"} {
99 | stdout, stderr, err := run("describe", "--tags", "--match="+match, rev)
100 | output := strings.TrimSpace(stdout.String() + stderr.String())
101 | if err == nil {
102 | tag = output
103 | break
104 | }
105 |
106 | // Catch failures due to not finding an appropriate tag
107 | output = strings.ToLower(output)
108 | switch {
109 | // fatal: no tag exactly matches ...
110 | // fatal: no tags can describe ...
111 | // fatal: no names found, cannot describe anything.
112 | // fatal: no annotated tags can describe ...
113 | case strings.HasPrefix(output, "fatal: no tag"),
114 | strings.HasPrefix(output, "fatal: no names"),
115 | strings.HasPrefix(output, "fatal: no annotated tag"):
116 | err = ErrorVersionNotFound
117 | default:
118 | g.showOutput(stdout, stderr)
119 | }
120 | return "", err
121 | }
122 |
123 | if tag == "" {
124 | return "", ErrorVersionNotFound
125 | }
126 |
127 | log.Debugf("%s is described as %s", rev, tag)
128 | fields := strings.Split(tag, "-")
129 | if len(fields) < 3 {
130 | // This matches a tag exactly (it must not be a semver tag)
131 | return tag, nil
132 | }
133 | tag = strings.Join(fields[:len(fields)-2], "-")
134 | return tag, nil
135 | }
136 |
137 | // FileHashesFromRef parses the output of 'git ls-tree -r' to
138 | // return the file hashes for the given tag or revision ref.
139 | func (g *gitWorkingTree) FileHashesFromRef(ref, subPath string) (FileHashes, error) {
140 | args := []string{"ls-tree", "-r", ref}
141 | if subPath != "" {
142 | args = append(args, subPath)
143 | }
144 | stdout, stderr, err := g.run(args...)
145 | if err != nil {
146 | output := strings.ToLower(stdout.String() + stderr.String())
147 | switch {
148 | case strings.HasPrefix(output, "fatal: not a valid object name "):
149 | // This is a branch name, not a tag name
150 | return nil, ErrorInvalidRef
151 | case strings.HasPrefix(output, "fatal: not a tree object"):
152 | // This ref is not present in the repo
153 | return nil, ErrorInvalidRef
154 | }
155 |
156 | g.showOutput(stdout, stderr)
157 | return nil, err
158 | }
159 | fh := make(FileHashes)
160 | scanner := bufio.NewScanner(stdout)
161 | for scanner.Scan() {
162 | line := scanner.Text()
163 | // SP SP