├── gopkgvc.png ├── config.json ├── .gitignore ├── README.md ├── LICENSE ├── main.go ├── version.go ├── version_test.go ├── refs_test.go ├── page.go └── handler.go /gopkgvc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bjdgyc/gopkgvc/HEAD/gopkgvc.png -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "addr" : ":8080", 3 | "gopkg_url":"http://mygopkg.com", 4 | "vcs_url": "http://mygitlab.com", 5 | "vcs_auth_user":"gitlab_user", 6 | "vcs_auth_pass":"gitlab_pass" 7 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | 16 | #goland 17 | .idea 18 | 19 | #program 20 | gopkgvc -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gopkgvc 2 | 3 | ## Introduction 4 | gopkgvc 5 | go包的版本管理工具,基于 [http://gopkg.in](http://gopkg.in) 开发。 6 | 主要用于企业内部包管理。现支持 (gitlab)等仓库的版本管理。 7 | 8 | # Screenshot 9 | ![Screenshot](https://raw.githubusercontent.com/bjdgyc/gopkgvc/master/gopkgvc.png) 10 | 11 | ## TODO 12 | * 该程序仅实现了 `http` 协议,如需要 `https` 功能,需结合 `nginx` 等代理工具实现。 13 | * 该程序版本控制是基于 项目的 `tag` 或者 `branch` 实现的 14 | * 程序版本应严格按照 `语义化版本` 写法 [http://semver.org/lang/zh-CN/](http://semver.org/lang/zh-CN/) 15 | 16 | ## Installation 17 | 18 | `go get github.com/bjdgyc/gopkgvc` 19 | 20 | ## Json config 21 | 22 | ``` json 23 | 24 | { 25 | "addr" : ":8080", //程序监听地址 26 | "gopkg_url":"http://mygopkg.com", //包管理地址名 27 | "vcs_url": "http://mygitlab.com", //gitlab等仓库地址 28 | "vcs_auth_user":"gitlab_user", //gitlab用户名 29 | "vcs_auth_pass":"gitlab_pass" //gitlab密码 30 | } 31 | 32 | 33 | ``` 34 | 35 | ## Start 36 | 37 | `go build && ./gopkgvc -c ./config.json` 38 | 39 | 40 | ## Use 41 | 42 | 命令行执行 `go get -insecure mygopkg.com/user/project` 下载对应的包 43 | 44 | 请使用浏览器打开 `http://mygopkg.com/v/user/project.v1` 根据页面操作即可 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 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 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "net/url" 10 | "os" 11 | "time" 12 | "log" 13 | ) 14 | 15 | var ( 16 | configFile = flag.String("c", "./config.json", "The config file") 17 | ) 18 | 19 | var httpServer = &http.Server{ 20 | ReadTimeout: 30 * time.Second, 21 | WriteTimeout: 30 * time.Second, 22 | } 23 | 24 | var httpClient = &http.Client{ 25 | Timeout: 20 * time.Second, 26 | } 27 | 28 | type Config struct { 29 | //监听地址 30 | Addr string `json:"addr"` 31 | //gopkg服务地址 32 | GopkgUrl string `json:"gopkg_url"` 33 | //版本控制服务器地址 34 | VCSUrl string `json:"vcs_url"` 35 | //用户名 36 | VCSAuthUser string `json:"vcs_auth_user"` 37 | //密码 38 | VCSAuthPass string `json:"vcs_auth_pass"` 39 | //是否需要授权验证 40 | vcsNeedAuth bool 41 | //gopkg域名 42 | GopkgHost string 43 | //http协议 44 | GopkgScheme string 45 | VCSHost string 46 | } 47 | 48 | var config = Config{} 49 | 50 | func main() { 51 | log.SetFlags(log.LstdFlags | log.Lshortfile) 52 | flag.Parse() 53 | data, err := ioutil.ReadFile(*configFile) 54 | if err != nil { 55 | panic(err) 56 | } 57 | err = json.Unmarshal(data, &config) 58 | if err != nil { 59 | panic(err) 60 | } 61 | 62 | parseUrl, err := url.Parse(config.GopkgUrl) 63 | if err != nil { 64 | panic(err) 65 | } 66 | 67 | config.GopkgHost = parseUrl.Host 68 | config.GopkgScheme = parseUrl.Scheme 69 | 70 | pu, _ := url.Parse(config.VCSUrl) 71 | config.VCSHost = pu.Host 72 | 73 | if config.VCSAuthUser != "" && config.VCSAuthPass != "" { 74 | config.vcsNeedAuth = true 75 | } 76 | 77 | http.HandleFunc("/", handler) 78 | httpServer.Addr = config.Addr 79 | 80 | fmt.Println("http start " + config.Addr) 81 | err = httpServer.ListenAndServe() 82 | if err != nil { 83 | fmt.Fprintf(os.Stderr, "error: %v\n", err) 84 | os.Exit(1) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Version represents a version number. 8 | // An element that is not present is represented as -1. 9 | type Version struct { 10 | Major int 11 | Minor int 12 | Patch int 13 | Unstable bool 14 | } 15 | 16 | const unstableSuffix = "-unstable" 17 | 18 | func (v Version) String() string { 19 | if v.Major < 0 { 20 | panic(fmt.Sprintf("cannot stringify invalid version (major is %d)", v.Major)) 21 | } 22 | suffix := "" 23 | if v.Unstable { 24 | suffix = unstableSuffix 25 | } 26 | if v.Minor < 0 { 27 | return fmt.Sprintf("v%d%s", v.Major, suffix) 28 | } 29 | if v.Patch < 0 { 30 | return fmt.Sprintf("v%d.%d%s", v.Major, v.Minor, suffix) 31 | } 32 | return fmt.Sprintf("v%d.%d.%d%s", v.Major, v.Minor, v.Patch, suffix) 33 | } 34 | 35 | // Less returns whether v is less than other. 36 | func (v Version) Less(other Version) bool { 37 | if v.Major != other.Major { 38 | return v.Major < other.Major 39 | } 40 | if v.Minor != other.Minor { 41 | return v.Minor < other.Minor 42 | } 43 | if v.Patch != other.Patch { 44 | return v.Patch < other.Patch 45 | } 46 | return v.Unstable && !other.Unstable 47 | } 48 | 49 | // Contains returns whether version v contains version other. 50 | // Version v is defined to contain version other when they both have the same Major 51 | // version and v.Minor and v.Patch are either undefined or are equal to other's. 52 | // 53 | // For example, Version{1, 1, -1} contains both Version{1, 1, -1} and Version{1, 1, 2}, 54 | // but not Version{1, -1, -1} or Version{1, 2, -1}. 55 | // 56 | // Unstable versions (-unstable) only contain unstable versions, and stable 57 | // versions only contain stable versions. 58 | func (v Version) Contains(other Version) bool { 59 | if v.Unstable != other.Unstable { 60 | return false 61 | } 62 | if v.Patch != -1 { 63 | return v == other 64 | } 65 | if v.Minor != -1 { 66 | return v.Major == other.Major && v.Minor == other.Minor 67 | } 68 | return v.Major == other.Major 69 | } 70 | 71 | func (v Version) IsValid() bool { 72 | return v != InvalidVersion 73 | } 74 | 75 | // InvalidVersion represents a version that can't be parsed. 76 | var InvalidVersion = Version{-1, -1, -1, false} 77 | 78 | func parseVersion(s string) (v Version, ok bool) { 79 | v = InvalidVersion 80 | if len(s) < 2 { 81 | return 82 | } 83 | if s[0] != 'v' { 84 | return 85 | } 86 | vout := InvalidVersion 87 | unstable := false 88 | i := 1 89 | for _, vptr := range []*int{&vout.Major, &vout.Minor, &vout.Patch} { 90 | *vptr, unstable, i = parseVersionPart(s, i) 91 | if i < 0 { 92 | return 93 | } 94 | if i == len(s) { 95 | vout.Unstable = unstable 96 | return vout, true 97 | } 98 | } 99 | return 100 | } 101 | 102 | func parseVersionPart(s string, i int) (part int, unstable bool, newi int) { 103 | j := i 104 | for j < len(s) && s[j] != '.' && s[j] != '-' { 105 | j++ 106 | } 107 | if j == i || j-i > 1 && s[i] == '0' { 108 | return -1, false, -1 109 | } 110 | c := s[i] 111 | for { 112 | if c < '0' || c > '9' { 113 | return -1, false, -1 114 | } 115 | part *= 10 116 | part += int(c - '0') 117 | if part < 0 { 118 | return -1, false, -1 119 | } 120 | i++ 121 | if i == len(s) { 122 | return part, false, i 123 | } 124 | c = s[i] 125 | if i+1 < len(s) { 126 | if c == '.' { 127 | return part, false, i + 1 128 | } 129 | if c == '-' && s[i:] == unstableSuffix { 130 | return part, true, i + len(unstableSuffix) 131 | } 132 | } 133 | } 134 | panic("unreachable") 135 | } 136 | 137 | // VersionList implements sort.Interface 138 | type VersionList []Version 139 | 140 | func (vl VersionList) Len() int { return len(vl) } 141 | func (vl VersionList) Less(i, j int) bool { return vl[i].Less(vl[j]) } 142 | func (vl VersionList) Swap(i, j int) { vl[i], vl[j] = vl[j], vl[i] } 143 | -------------------------------------------------------------------------------- /version_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | . "gopkg.in/check.v1" 7 | ) 8 | 9 | func Test(t *testing.T) { TestingT(t) } 10 | 11 | var _ = Suite(&VersionSuite{}) 12 | 13 | type VersionSuite struct{} 14 | 15 | var versionParseTests = []struct { 16 | major int 17 | minor int 18 | patch int 19 | dev bool 20 | s string 21 | }{ 22 | {-1, -1, -1, false, "v"}, 23 | {-1, -1, -1, false, "v-1"}, 24 | {-1, -1, -1, false, "v-deb"}, 25 | {-1, -1, -1, false, "v01"}, 26 | {-1, -1, -1, false, "v1.01"}, 27 | {-1, -1, -1, false, "a1"}, 28 | {-1, -1, -1, false, "v1a"}, 29 | {-1, -1, -1, false, "v1..2"}, 30 | {-1, -1, -1, false, "v1.2.3.4"}, 31 | {-1, -1, -1, false, "v1."}, 32 | {-1, -1, -1, false, "v1.2."}, 33 | {-1, -1, -1, false, "v1.2.3."}, 34 | 35 | {0, -1, -1, false, "v0"}, 36 | {0, -1, -1, true, "v0-unstable"}, 37 | {1, -1, -1, false, "v1"}, 38 | {1, -1, -1, true, "v1-unstable"}, 39 | {1, 2, -1, false, "v1.2"}, 40 | {1, 2, -1, true, "v1.2-unstable"}, 41 | {1, 2, 3, false, "v1.2.3"}, 42 | {1, 2, 3, true, "v1.2.3-unstable"}, 43 | {12, 34, 56, false, "v12.34.56"}, 44 | {12, 34, 56, true, "v12.34.56-unstable"}, 45 | } 46 | 47 | func (s *VersionSuite) TestParse(c *C) { 48 | for _, t := range versionParseTests { 49 | got, ok := parseVersion(t.s) 50 | if t.major == -1 { 51 | if ok || got != InvalidVersion { 52 | c.Fatalf("version %q is invalid but parsed as %#v", t.s, got) 53 | } 54 | } else { 55 | want := Version{t.major, t.minor, t.patch, t.dev} 56 | if got != want { 57 | c.Fatalf("version %q must parse as %#v, got %#v", t.s, want, got) 58 | } 59 | if got.String() != t.s { 60 | c.Fatalf("version %q got parsed as %#v and stringified as %q", t.s, got, got.String()) 61 | } 62 | } 63 | } 64 | } 65 | 66 | var versionLessTests = []struct { 67 | oneMajor, oneMinor, onePatch int 68 | oneUnstable bool 69 | twoMajor, twoMinor, twoPatch int 70 | twoUnstable, less bool 71 | }{ 72 | {0, 0, 0, false, 0, 0, 0, false, false}, 73 | {1, 0, 0, false, 1, 0, 0, false, false}, 74 | {1, 0, 0, false, 1, 1, 0, false, true}, 75 | {1, 0, 0, false, 2, 0, 0, false, true}, 76 | {0, 1, 0, false, 0, 1, 0, false, false}, 77 | {0, 1, 0, false, 0, 1, 1, false, true}, 78 | {0, 0, 0, false, 0, 2, 0, false, true}, 79 | {0, 0, 1, false, 0, 0, 1, false, false}, 80 | {0, 0, 1, false, 0, 0, 2, false, true}, 81 | 82 | {0, 0, 0, false, 0, 0, 0, true, false}, 83 | {0, 0, 0, true, 0, 0, 0, false, true}, 84 | {0, 0, 1, true, 0, 0, 0, false, false}, 85 | } 86 | 87 | func (s *VersionSuite) TestLess(c *C) { 88 | for _, t := range versionLessTests { 89 | one := Version{t.oneMajor, t.oneMinor, t.onePatch, t.oneUnstable} 90 | two := Version{t.twoMajor, t.twoMinor, t.twoPatch, t.twoUnstable} 91 | if one.Less(two) != t.less { 92 | c.Fatalf("version %s < %s returned %v", one, two, !t.less) 93 | } 94 | } 95 | } 96 | 97 | var versionContainsTests = []struct { 98 | oneMajor, oneMinor, onePatch int 99 | oneUnstable bool 100 | twoMajor, twoMinor, twoPatch int 101 | twoUnstable, contains bool 102 | }{ 103 | {12, 34, 56, false, 12, 34, 56, false, true}, 104 | {12, 34, 56, false, 12, 34, 78, false, false}, 105 | {12, 34, -1, false, 12, 34, 56, false, true}, 106 | {12, 34, -1, false, 12, 78, 56, false, false}, 107 | {12, -1, -1, false, 12, 34, 56, false, true}, 108 | {12, -1, -1, false, 78, 34, 56, false, false}, 109 | 110 | {12, -1, -1, true, 12, -1, -1, false, false}, 111 | {12, -1, -1, false, 12, -1, -1, true, false}, 112 | } 113 | 114 | func (s *VersionSuite) TestContains(c *C) { 115 | for _, t := range versionContainsTests { 116 | one := Version{t.oneMajor, t.oneMinor, t.onePatch, t.oneUnstable} 117 | two := Version{t.twoMajor, t.twoMinor, t.twoPatch, t.twoUnstable} 118 | if one.Contains(two) != t.contains { 119 | c.Fatalf("version %s.Contains(%s) returned %v", one, two, !t.contains) 120 | } 121 | } 122 | } 123 | 124 | func (s *VersionSuite) TestIsValid(c *C) { 125 | c.Assert(InvalidVersion.IsValid(), Equals, false) 126 | c.Assert(Version{0, 0, 0, false}.IsValid(), Equals, true) 127 | } 128 | -------------------------------------------------------------------------------- /refs_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "sort" 7 | 8 | . "gopkg.in/check.v1" 9 | ) 10 | 11 | var _ = Suite(&RefsSuite{}) 12 | 13 | type RefsSuite struct{} 14 | 15 | type refsTest struct { 16 | summary string 17 | original string 18 | version string 19 | changed string 20 | versions []string 21 | } 22 | 23 | var refsTests = []refsTest{{ 24 | "Version v0 works even without any references", 25 | reflines( 26 | "hash1 HEAD", 27 | ), 28 | "v0", 29 | reflines( 30 | "hash1 HEAD", 31 | ), 32 | nil, 33 | }, { 34 | "Preserve original capabilities", 35 | reflines( 36 | "hash1 HEAD\x00caps", 37 | ), 38 | "v0", 39 | reflines( 40 | "hash1 HEAD\x00caps", 41 | ), 42 | nil, 43 | }, { 44 | "Matching major version branch", 45 | reflines( 46 | "00000000000000000000000000000000000hash1 HEAD", 47 | "00000000000000000000000000000000000hash2 refs/heads/v0", 48 | "00000000000000000000000000000000000hash3 refs/heads/v1", 49 | "00000000000000000000000000000000000hash4 refs/heads/v2", 50 | ), 51 | "v1", 52 | reflines( 53 | "00000000000000000000000000000000000hash3 HEAD\x00symref=HEAD:refs/heads/v1", 54 | "00000000000000000000000000000000000hash3 refs/heads/master", 55 | "00000000000000000000000000000000000hash2 refs/heads/v0", 56 | "00000000000000000000000000000000000hash3 refs/heads/v1", 57 | "00000000000000000000000000000000000hash4 refs/heads/v2", 58 | ), 59 | []string{"v0", "v1", "v2"}, 60 | }, { 61 | "Matching minor version branch", 62 | reflines( 63 | "00000000000000000000000000000000000hash1 HEAD", 64 | "00000000000000000000000000000000000hash2 refs/heads/v1.1", 65 | "00000000000000000000000000000000000hash3 refs/heads/v1.3", 66 | "00000000000000000000000000000000000hash4 refs/heads/v1.2", 67 | ), 68 | "v1", 69 | reflines( 70 | "00000000000000000000000000000000000hash3 HEAD\x00symref=HEAD:refs/heads/v1.3", 71 | "00000000000000000000000000000000000hash3 refs/heads/master", 72 | "00000000000000000000000000000000000hash2 refs/heads/v1.1", 73 | "00000000000000000000000000000000000hash3 refs/heads/v1.3", 74 | "00000000000000000000000000000000000hash4 refs/heads/v1.2", 75 | ), 76 | []string{"v1.1", "v1.2", "v1.3"}, 77 | }, { 78 | "Disable original symref capability", 79 | reflines( 80 | "00000000000000000000000000000000000hash1 HEAD\x00foo symref=bar baz", 81 | "00000000000000000000000000000000000hash2 refs/heads/v1", 82 | ), 83 | "v1", 84 | reflines( 85 | "00000000000000000000000000000000000hash2 HEAD\x00symref=HEAD:refs/heads/v1 foo oldref=bar baz", 86 | "00000000000000000000000000000000000hash2 refs/heads/master", 87 | "00000000000000000000000000000000000hash2 refs/heads/v1", 88 | ), 89 | []string{"v1"}, 90 | }, { 91 | "Replace original master branch", 92 | reflines( 93 | "00000000000000000000000000000000000hash1 HEAD", 94 | "00000000000000000000000000000000000hash1 refs/heads/master", 95 | "00000000000000000000000000000000000hash2 refs/heads/v1", 96 | ), 97 | "v1", 98 | reflines( 99 | "00000000000000000000000000000000000hash2 HEAD\x00symref=HEAD:refs/heads/v1", 100 | "00000000000000000000000000000000000hash2 refs/heads/master", 101 | "00000000000000000000000000000000000hash2 refs/heads/v1", 102 | ), 103 | []string{"v1"}, 104 | }, { 105 | "Matching tag", 106 | reflines( 107 | "00000000000000000000000000000000000hash1 HEAD", 108 | "00000000000000000000000000000000000hash2 refs/tags/v0", 109 | "00000000000000000000000000000000000hash3 refs/tags/v1", 110 | "00000000000000000000000000000000000hash4 refs/tags/v2", 111 | ), 112 | "v1", 113 | reflines( 114 | "00000000000000000000000000000000000hash3 HEAD", 115 | "00000000000000000000000000000000000hash3 refs/heads/master", 116 | "00000000000000000000000000000000000hash2 refs/tags/v0", 117 | "00000000000000000000000000000000000hash3 refs/tags/v1", 118 | "00000000000000000000000000000000000hash4 refs/tags/v2", 119 | ), 120 | []string{"v0", "v1", "v2"}, 121 | }, { 122 | "Tag peeling", 123 | reflines( 124 | "00000000000000000000000000000000000hash1 HEAD", 125 | "00000000000000000000000000000000000hash2 refs/heads/master", 126 | "00000000000000000000000000000000000hash3 refs/tags/v1", 127 | "00000000000000000000000000000000000hash4 refs/tags/v1^{}", 128 | "00000000000000000000000000000000000hash5 refs/tags/v2", 129 | ), 130 | "v1", 131 | reflines( 132 | "00000000000000000000000000000000000hash4 HEAD", 133 | "00000000000000000000000000000000000hash4 refs/heads/master", 134 | "00000000000000000000000000000000000hash3 refs/tags/v1", 135 | "00000000000000000000000000000000000hash4 refs/tags/v1^{}", 136 | "00000000000000000000000000000000000hash5 refs/tags/v2", 137 | ), 138 | []string{"v1", "v1", "v2"}, 139 | }, { 140 | "Matching unstable versions", 141 | reflines( 142 | "00000000000000000000000000000000000hash1 HEAD", 143 | "00000000000000000000000000000000000hash2 refs/heads/master", 144 | "00000000000000000000000000000000000hash3 refs/heads/v1", 145 | "00000000000000000000000000000000000hash4 refs/heads/v1.1-unstable", 146 | "00000000000000000000000000000000000hash5 refs/heads/v1.3-unstable", 147 | "00000000000000000000000000000000000hash6 refs/heads/v1.2-unstable", 148 | "00000000000000000000000000000000000hash7 refs/heads/v2", 149 | ), 150 | "v1-unstable", 151 | reflines( 152 | "00000000000000000000000000000000000hash5 HEAD\x00symref=HEAD:refs/heads/v1.3-unstable", 153 | "00000000000000000000000000000000000hash5 refs/heads/master", 154 | "00000000000000000000000000000000000hash3 refs/heads/v1", 155 | "00000000000000000000000000000000000hash4 refs/heads/v1.1-unstable", 156 | "00000000000000000000000000000000000hash5 refs/heads/v1.3-unstable", 157 | "00000000000000000000000000000000000hash6 refs/heads/v1.2-unstable", 158 | "00000000000000000000000000000000000hash7 refs/heads/v2", 159 | ), 160 | []string{"v1", "v1.1-unstable", "v1.2-unstable", "v1.3-unstable", "v2"}, 161 | }} 162 | 163 | func reflines(lines ...string) string { 164 | var buf bytes.Buffer 165 | buf.WriteString("001e# service=git-upload-pack\n0000") 166 | for _, l := range lines { 167 | buf.WriteString(fmt.Sprintf("%04x%s\n", len(l)+5, l)) 168 | } 169 | buf.WriteString("0000") 170 | return buf.String() 171 | } 172 | 173 | func (s *RefsSuite) TestChangeRefs(c *C) { 174 | for _, test := range refsTests { 175 | c.Logf(test.summary) 176 | 177 | v, ok := parseVersion(test.version) 178 | if !ok { 179 | c.Fatalf("Test has an invalid version: %q", test.version) 180 | } 181 | 182 | changed, versions, err := changeRefs([]byte(test.original), v) 183 | c.Assert(err, IsNil) 184 | 185 | c.Assert(string(changed), Equals, test.changed) 186 | 187 | sort.Sort(versions) 188 | 189 | var vs []string 190 | for _, v := range versions { 191 | vs = append(vs, v.String()) 192 | } 193 | c.Assert(vs, DeepEquals, test.versions) 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /page.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "html/template" 6 | "log" 7 | "net/http" 8 | "os" 9 | "sort" 10 | ) 11 | 12 | const packageTemplateString = ` 13 | 14 | 15 | 16 | {{.Repo.Name}}.{{.Repo.MajorVersion}}{{.Repo.SubPath}} - {{.Repo.GopkgPath}} 17 | 18 | 19 | 20 | 103 | 104 | 105 | 106 |
107 |
108 |
109 |
110 | 114 |
115 |
116 | {{ if .Repo.MajorVersion.Unstable }} 117 |
118 | This is an unstable package and should not be used in released code. 119 |
120 | {{ end }} 121 |
122 |
123 | Source Code 124 | Documentation 125 |
126 |
127 |
128 |
129 |
130 |

Getting started

131 |
132 |

To get the package, execute:

133 |
go get {{if eq .Repo.GopkgScheme "http"}}-insecure {{end}}{{.Repo.GopkgPath}}
134 |
135 |
136 |

To import this package, add the following line to your code:

137 |
import "{{.Repo.GopkgPath}}"
138 | {{if .PackageName}}

Refer to it as {{.PackageName}}.{{end}} 139 |

140 |
141 |

For more details, see the API documentation.

142 |
143 |
144 |
145 |
146 |

Versions

147 | {{ if .LatestVersions }} 148 | {{ range .LatestVersions }} 149 |
150 | v{{.Major}}{{if .Unstable}}-unstable{{end}} 151 | → 152 | {{.}} 153 |
154 | {{ end }} 155 | {{ else }} 156 |
157 | v0 158 | → 159 | master 160 |
161 | {{ end }} 162 |
163 |
164 |
165 |
166 | 167 | 177 | 178 | 179 | ` 180 | 181 | var packageTemplate *template.Template 182 | 183 | func gopkgVersionRoot(repo *Repo, version Version) string { 184 | return repo.GopkgVersionRoot(version) 185 | } 186 | 187 | var packageFuncs = template.FuncMap{ 188 | "gopkgVersionRoot": gopkgVersionRoot, 189 | } 190 | 191 | func init() { 192 | var err error 193 | packageTemplate, err = template.New("page").Funcs(packageFuncs).Parse(packageTemplateString) 194 | if err != nil { 195 | fmt.Fprintf(os.Stderr, "fatal: parsing package template failed: %s\n", err) 196 | os.Exit(1) 197 | } 198 | } 199 | 200 | type packageData struct { 201 | Repo *Repo 202 | LatestVersions VersionList // Contains only the latest version for each major 203 | PackageName string // Actual package identifier as specified in http://golang.org/ref/spec#PackageClause 204 | Synopsis string 205 | GitTreeName string 206 | } 207 | 208 | func renderPackagePage(resp http.ResponseWriter, req *http.Request, repo *Repo) { 209 | data := &packageData{ 210 | Repo: repo, 211 | } 212 | 213 | // Calculate the latest version for each major version, both stable and unstable. 214 | latestVersions := make(map[int]Version) 215 | for _, v := range repo.AllVersions { 216 | if v.Unstable { 217 | continue 218 | } 219 | v2, exists := latestVersions[v.Major] 220 | if !exists || v2.Less(v) { 221 | latestVersions[v.Major] = v 222 | } 223 | } 224 | data.LatestVersions = make(VersionList, 0, len(latestVersions)) 225 | for _, v := range latestVersions { 226 | data.LatestVersions = append(data.LatestVersions, v) 227 | } 228 | sort.Sort(sort.Reverse(data.LatestVersions)) 229 | 230 | if repo.FullVersion.Unstable { 231 | // Prepend post-sorting so it shows first. 232 | data.LatestVersions = append([]Version{repo.FullVersion}, data.LatestVersions...) 233 | } 234 | 235 | err := packageTemplate.Execute(resp, data) 236 | if err != nil { 237 | log.Printf("error executing package page template: %v", err) 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /handler.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "html/template" 8 | "io" 9 | "io/ioutil" 10 | "log" 11 | "net/http" 12 | "regexp" 13 | "strconv" 14 | "strings" 15 | ) 16 | 17 | var gogetTemplate = template.Must(template.New("").Parse(` 18 | 19 | 20 | 21 | 22 | {{$root := .VCSRoot}}{{$tree := .VCSTree}} 23 | 24 | 25 | 26 | go get {{.GopkgPath}} 27 | 28 | 29 | `)) 30 | 31 | var gopkgTemplate = template.Must(template.New("").Parse(` 32 | 33 | 34 | 35 | {{$root := .VCSRoot}} 36 | 37 | 38 | 39 | go get {{.GopkgPath}} 40 | 41 | 42 | `)) 43 | 44 | //TODO 访问地址 45 | // /group/project/sub 46 | // /v/group/project.v2/sub 47 | var patternVersion = regexp.MustCompile(`^/([-a-zA-Z0-9_]+)/([-a-zA-Z0-9_]+)\.(v0|v[1-9][0-9]*)((/[-.a-zA-Z0-9_]+)*)$`) 48 | var patternPkg = regexp.MustCompile(`^/([-a-zA-Z0-9_]+)/([-a-zA-Z0-9_]+)((/[-.a-zA-Z0-9_]+)*)$`) 49 | 50 | func handler(resp http.ResponseWriter, req *http.Request) { 51 | urlPath := req.URL.Path 52 | 53 | //b, _ := httputil.DumpRequest(req, false) 54 | //fmt.Println(string(b)) 55 | 56 | if urlPath == "/health-check" { 57 | resp.Write([]byte("ok")) 58 | return 59 | } 60 | 61 | log.Printf("%s requested %s", req.Header.Get("X-Forwarded-For"), req.URL) 62 | 63 | if urlPath == "/" { 64 | resp.Header().Set("Location", config.VCSUrl) 65 | resp.WriteHeader(http.StatusTemporaryRedirect) 66 | return 67 | } 68 | 69 | log.Printf("req.URL.Path %s", urlPath) 70 | 71 | var ( 72 | isGopkg = false 73 | ok bool 74 | m []string 75 | ) 76 | 77 | //版本管理 78 | if strings.HasPrefix(urlPath, "/v/") { 79 | urlPath = urlPath[2:] 80 | m = patternVersion.FindStringSubmatch(urlPath) 81 | } else { 82 | m = patternPkg.FindStringSubmatch(urlPath) 83 | isGopkg = true 84 | } 85 | 86 | log.Println(m) 87 | 88 | if m == nil { 89 | sendErrMsg(resp, http.StatusNotFound, "Unsupported URL pattern; see the documentation at gopkg.in for details.\n") 90 | return 91 | } 92 | 93 | repo := &Repo{ 94 | Group: m[1], 95 | Name: m[2], 96 | SubPath: m[3], 97 | FullVersion: InvalidVersion, 98 | } 99 | 100 | //TODO gopkg 101 | if isGopkg == true { 102 | repo.Gopkg = true 103 | resp.Header().Set("Content-Type", "text/html") 104 | if req.FormValue("go-get") == "1" { 105 | // execute simple template when this is a go-get request 106 | err := gopkgTemplate.Execute(resp, repo) 107 | if err != nil { 108 | log.Printf("error executing go get template: %s\n", err) 109 | } 110 | } 111 | return 112 | } 113 | 114 | //==========================以下为版本管理=============================== 115 | 116 | if strings.Contains(m[3], ".") { 117 | sendErrMsg(resp, http.StatusOK, "Import paths take the major version only (.%s instead of .%s); see docs at gopkg.in for the reasoning.\n", 118 | m[3][:strings.Index(m[3], ".")], m[3]) 119 | return 120 | } 121 | 122 | repo.SubPath = m[4] 123 | 124 | repo.MajorVersion, ok = parseVersion(m[3]) 125 | if !ok { 126 | sendErrMsg(resp, http.StatusNotFound, "Version %q improperly considered invalid; please warn the service maintainers.", m[3]) 127 | return 128 | } 129 | 130 | var changed []byte 131 | var versions VersionList 132 | original, err := fetchRefs(repo) 133 | if err == nil { 134 | changed, versions, err = changeRefs(original, repo.MajorVersion) 135 | repo.SetVersions(versions) 136 | } 137 | 138 | switch err { 139 | case nil: 140 | // all ok 141 | case ErrNoRepo: 142 | sendErrMsg(resp, http.StatusNotFound, "GitHub repository not found at https://%s", repo.VCSRoot()) 143 | return 144 | case ErrNoVersion: 145 | major := repo.MajorVersion 146 | suffix := "" 147 | if major.Unstable { 148 | major.Unstable = false 149 | suffix = unstableSuffix 150 | } 151 | v := major.String() 152 | sendErrMsg(resp, http.StatusNotFound, `GitHub repository at https://%s has no branch or tag "%s%s", "%s.N%s" or "%s.N.M%s"`, 153 | repo.VCSRoot(), v, suffix, v, suffix, v, suffix) 154 | return 155 | default: 156 | sendErrMsg(resp, http.StatusBadGateway, "Cannot obtain refs from GitHub: %v", err) 157 | return 158 | } 159 | 160 | //程序中转远程仓库数据 161 | if repo.SubPath == "/git-upload-pack" { 162 | //请求远端gilab 163 | url := fmt.Sprintf("%s.git%s", repo.VCSRoot(), "/git-upload-pack") 164 | reqRemote, err := http.NewRequest("POST", url, req.Body) 165 | if err != nil { 166 | sendErrMsg(resp, http.StatusBadGateway, "GitLab get url is error: %v", err) 167 | return 168 | } 169 | reqRemote.Header.Set("User-Agent", req.UserAgent()) 170 | reqRemote.Header.Set("Accept-Encoding", req.Header.Get("Accept-Encoding")) 171 | reqRemote.Header.Set("Accept", req.Header.Get("Accept")) 172 | reqRemote.Header.Set("Content-Type", req.Header.Get("Content-Type")) 173 | if config.vcsNeedAuth { 174 | reqRemote.SetBasicAuth(config.VCSAuthUser, config.VCSAuthPass) 175 | } 176 | respRemote, err := httpClient.Do(reqRemote) 177 | if err != nil { 178 | sendErrMsg(resp, http.StatusBadGateway, "GitLab get git-upload-pack error: %v", err) 179 | return 180 | } 181 | defer respRemote.Body.Close() 182 | if respRemote.StatusCode != http.StatusOK { 183 | sendErrMsg(resp, http.StatusBadGateway, "GitLab get data error:http_code %d", respRemote.StatusCode) 184 | return 185 | } 186 | //数据写给客户端 187 | resp.Header().Set("Cache-Control", "no-cache") 188 | resp.Header().Set("Content-Type", respRemote.Header.Get("Content-Type")) 189 | resp.Header().Set("X-Accel-Buffering", respRemote.Header.Get("X-Accel-Buffering")) 190 | 191 | _, err = io.Copy(resp, respRemote.Body) 192 | if err != nil { 193 | sendErrMsg(resp, http.StatusBadGateway, "GitLab write data error: %v", err) 194 | return 195 | } 196 | 197 | return 198 | } 199 | 200 | if repo.SubPath == "/info/refs" { 201 | resp.Header().Set("Content-Type", "application/x-git-upload-pack-advertisement") 202 | resp.Write(changed) 203 | return 204 | } 205 | 206 | resp.Header().Set("Content-Type", "text/html") 207 | if req.FormValue("go-get") == "1" { 208 | // execute simple template when this is a go-get request 209 | err := gogetTemplate.Execute(resp, repo) 210 | if err != nil { 211 | log.Printf("error executing go get template: %s\n", err) 212 | } 213 | return 214 | } 215 | 216 | renderPackagePage(resp, req, repo) 217 | } 218 | 219 | // Repo represents a source code repository on GitHub. 220 | type Repo struct { 221 | Gopkg bool 222 | Group string 223 | Name string 224 | SubPath string 225 | MajorVersion Version 226 | 227 | // FullVersion is the best version in AllVersions that matches MajorVersion. 228 | // It defaults to InvalidVersion if there are no matches. 229 | FullVersion Version 230 | 231 | // AllVersions holds all versions currently available in the repository, 232 | // either coming from branch names or from tag names. Version zero (v0) 233 | // is only present in the list if it really exists in the repository. 234 | AllVersions VersionList 235 | } 236 | 237 | // SetVersions records in the relevant fields the details about which 238 | // package versions are available in the repository. 239 | func (repo *Repo) SetVersions(all []Version) { 240 | repo.AllVersions = all 241 | for _, v := range repo.AllVersions { 242 | if v.Major == repo.MajorVersion.Major && v.Unstable == repo.MajorVersion.Unstable && repo.FullVersion.Less(v) { 243 | repo.FullVersion = v 244 | } 245 | } 246 | } 247 | 248 | // VCSRoot returns the repository root at VCS, with a schema. 249 | func (repo *Repo) VCSRoot() string { 250 | if repo.Gopkg { 251 | //gitlab需要ssh协议 252 | return "ssh://git@" + config.VCSHost + "/" + repo.Group + "/" + repo.Name + ".git" 253 | } 254 | return config.VCSUrl + "/" + repo.Group + "/" + repo.Name 255 | 256 | } 257 | 258 | // VCSTree returns the repository tree name at VCS for the selected version. 259 | func (repo *Repo) VCSTree() string { 260 | if repo.FullVersion == InvalidVersion { 261 | return "master" 262 | } 263 | return repo.FullVersion.String() 264 | } 265 | 266 | func (repo *Repo) GopkgScheme() string { 267 | return config.GopkgScheme 268 | } 269 | 270 | // GopkgRoot returns the package root at gopkg.in, without a schema. 271 | func (repo *Repo) GopkgRoot() string { 272 | return repo.GopkgVersionRoot(repo.MajorVersion) 273 | } 274 | 275 | // GopkgPath returns the package path at gopkg.in, without a schema. 276 | func (repo *Repo) GopkgPath() string { 277 | return repo.GopkgVersionRoot(repo.MajorVersion) + repo.SubPath 278 | } 279 | 280 | // GopkgVersionRoot returns the package root in gopkg.in for the 281 | // provided version, without a schema. 282 | func (repo *Repo) GopkgVersionRoot(version Version) string { 283 | if repo.Gopkg { 284 | return config.GopkgHost + "/" + repo.Group + "/" + repo.Name 285 | } 286 | 287 | version.Minor = -1 288 | version.Patch = -1 289 | v := version.String() 290 | return config.GopkgHost + "/v/" + repo.Group + "/" + repo.Name + "." + v 291 | } 292 | 293 | func sendErrMsg(resp http.ResponseWriter, httpCode int, msg string, args ...interface{}) { 294 | if len(args) > 0 { 295 | msg = fmt.Sprintf(msg, args...) 296 | } 297 | resp.WriteHeader(httpCode) 298 | resp.Write([]byte(msg)) 299 | } 300 | 301 | const refsSuffix = ".git/info/refs?service=git-upload-pack" 302 | 303 | var ErrNoRepo = errors.New("repository not found in GitHub") 304 | var ErrNoVersion = errors.New("version reference not found in GitHub") 305 | 306 | func fetchRefs(repo *Repo) (data []byte, err error) { 307 | req, err := http.NewRequest("GET", repo.VCSRoot()+refsSuffix, nil) 308 | if err != nil { 309 | return 310 | } 311 | if config.vcsNeedAuth { 312 | req.SetBasicAuth(config.VCSAuthUser, config.VCSAuthPass) 313 | } 314 | 315 | resp, err := httpClient.Do(req) 316 | if err != nil { 317 | return nil, fmt.Errorf("cannot talk to GitHub: %v", err) 318 | } 319 | defer resp.Body.Close() 320 | 321 | switch resp.StatusCode { 322 | case 200: 323 | // ok 324 | case 401, 404: 325 | return nil, ErrNoRepo 326 | default: 327 | return nil, fmt.Errorf("error from GitHub: %v", resp.Status) 328 | } 329 | 330 | data, err = ioutil.ReadAll(resp.Body) 331 | if err != nil { 332 | return nil, fmt.Errorf("error reading from GitHub: %v", err) 333 | } 334 | return data, err 335 | } 336 | 337 | func changeRefs(data []byte, major Version) (changed []byte, versions VersionList, err error) { 338 | var hlinei, hlinej int // HEAD reference line start/end 339 | var mlinei, mlinej int // master reference line start/end 340 | var vrefhash string 341 | var vrefname string 342 | var vrefv = InvalidVersion 343 | 344 | // Record all available versions, the locations of the master and HEAD lines, 345 | // and details of the best reference satisfying the requested major version. 346 | versions = make([]Version, 0) 347 | sdata := string(data) 348 | for i, j := 0, 0; i < len(data); i = j { 349 | size, err := strconv.ParseInt(sdata[i:i+4], 16, 32) 350 | if err != nil { 351 | return nil, nil, fmt.Errorf("cannot parse refs line size: %s", string(data[i:i+4])) 352 | } 353 | if size == 0 { 354 | size = 4 355 | } 356 | j = i + int(size) 357 | if j > len(sdata) { 358 | return nil, nil, fmt.Errorf("incomplete refs data received from GitHub") 359 | } 360 | if sdata[0] == '#' { 361 | continue 362 | } 363 | 364 | hashi := i + 4 365 | hashj := strings.IndexByte(sdata[hashi:j], ' ') 366 | if hashj < 0 || hashj != 40 { 367 | continue 368 | } 369 | hashj += hashi 370 | 371 | namei := hashj + 1 372 | namej := strings.IndexAny(sdata[namei:j], "\n\x00") 373 | if namej < 0 { 374 | namej = j 375 | } else { 376 | namej += namei 377 | } 378 | 379 | name := sdata[namei:namej] 380 | 381 | if name == "HEAD" { 382 | hlinei = i 383 | hlinej = j 384 | } 385 | if name == "refs/heads/master" { 386 | mlinei = i 387 | mlinej = j 388 | } 389 | 390 | if strings.HasPrefix(name, "refs/heads/v") || strings.HasPrefix(name, "refs/tags/v") { 391 | if strings.HasSuffix(name, "^{}") { 392 | // Annotated tag is peeled off and overrides the same version just parsed. 393 | name = name[:len(name)-3] 394 | } 395 | v, ok := parseVersion(name[strings.IndexByte(name, 'v'):]) 396 | if ok && major.Contains(v) && (v == vrefv || !vrefv.IsValid() || vrefv.Less(v)) { 397 | vrefv = v 398 | vrefhash = sdata[hashi:hashj] 399 | vrefname = name 400 | } 401 | if ok { 402 | versions = append(versions, v) 403 | } 404 | } 405 | } 406 | 407 | // If there were absolutely no versions, and v0 was requested, accept the master as-is. 408 | if len(versions) == 0 && major == (Version{0, -1, -1, false}) { 409 | return data, nil, nil 410 | } 411 | 412 | // If the file has no HEAD line or the version was not found, report as unavailable. 413 | if hlinei == 0 || vrefhash == "" { 414 | return nil, nil, ErrNoVersion 415 | } 416 | 417 | var buf bytes.Buffer 418 | buf.Grow(len(data) + 256) 419 | 420 | // Copy the header as-is. 421 | buf.Write(data[:hlinei]) 422 | 423 | // Extract the original capabilities. 424 | caps := "" 425 | if i := strings.Index(sdata[hlinei:hlinej], "\x00"); i > 0 { 426 | caps = strings.Replace(sdata[hlinei+i+1:hlinej-1], "symref=", "oldref=", -1) 427 | } 428 | 429 | // Insert the HEAD reference line with the right hash and a proper symref capability. 430 | var line string 431 | if strings.HasPrefix(vrefname, "refs/heads/") { 432 | if caps == "" { 433 | line = fmt.Sprintf("%s HEAD\x00symref=HEAD:%s\n", vrefhash, vrefname) 434 | } else { 435 | line = fmt.Sprintf("%s HEAD\x00symref=HEAD:%s %s\n", vrefhash, vrefname, caps) 436 | } 437 | } else { 438 | if caps == "" { 439 | line = fmt.Sprintf("%s HEAD\n", vrefhash) 440 | } else { 441 | line = fmt.Sprintf("%s HEAD\x00%s\n", vrefhash, caps) 442 | } 443 | } 444 | fmt.Fprintf(&buf, "%04x%s", 4+len(line), line) 445 | 446 | // Insert the master reference line. 447 | line = fmt.Sprintf("%s refs/heads/master\n", vrefhash) 448 | fmt.Fprintf(&buf, "%04x%s", 4+len(line), line) 449 | 450 | // Append the rest, dropping the original master line if necessary. 451 | if mlinei > 0 { 452 | buf.Write(data[hlinej:mlinei]) 453 | buf.Write(data[mlinej:]) 454 | } else { 455 | buf.Write(data[hlinej:]) 456 | } 457 | 458 | return buf.Bytes(), versions, nil 459 | } 460 | --------------------------------------------------------------------------------