├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── assets ├── css │ ├── main.css │ ├── octicons.css │ └── primer.css ├── fonts │ ├── octicons-local.ttf │ ├── octicons.eot │ ├── octicons.less │ ├── octicons.svg │ ├── octicons.ttf │ └── octicons.woff └── images │ └── mini-logo.svg ├── client.go ├── db ├── db.go ├── db_darwin.go ├── db_linux.go └── types.go ├── github └── github.go ├── main.go ├── render └── render.go ├── server.go └── templates ├── add.tmpl ├── error.tmpl ├── index.tmpl └── layout.tmpl /.gitignore: -------------------------------------------------------------------------------- 1 | bolt.db 2 | gh-rel 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.4.2 2 | 3 | RUN mkdir -p /go/src/github.com/calavera/gh-rel /data 4 | WORKDIR /go/src/github.com/calavera/gh-rel 5 | 6 | COPY . /go/src/github.com/calavera/gh-rel 7 | 8 | RUN go-wrapper download && \ 9 | go-wrapper install 10 | 11 | VOLUME ["/data"] 12 | 13 | ENTRYPOINT ["/go/bin/gh-rel", "--db=/data/db"] 14 | CMD ["serve"] 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 David Calavera 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gh-rel 2 | 3 | GH Rel is a small dashboard to keep track of releases in several github projects at once. 4 | 5 | It includes a web server to render the dashboard and a cli application to add new projects to the dashboard. 6 | 7 | ## Howto 8 | 9 | The program is built as a single binary with `go build .`. 10 | 11 | To add a new project to the dashboard, run `gh-rel add org/name`. 12 | The server needs to be ofline for this because boltdb doesn't support multiple processes accessing the same database. 13 | 14 | To start the server, run `gh-rel serve`. 15 | 16 | The server takes the releases information from GitHub's releases API described in https://developer.github.com/v3/repos/releases. 17 | 18 | ## Disclaimer 19 | 20 | I wrote this code in a few hours, use it at your own discretion. Pull Requests are very welcome. 21 | 22 | ## License 23 | 24 | MIT 25 | -------------------------------------------------------------------------------- /assets/css/main.css: -------------------------------------------------------------------------------- 1 | 2 | /*@media (min-width: 34em)*/ 3 | body { 4 | font-family: "Helvetica Neue",Helvetica,arial,nimbussansl,liberationsans,freesans,clean,sans-serif,"Segoe UI Emoji","Segoe UI Symbol"; 5 | font-size: 1rem; 6 | } 7 | 8 | object{ 9 | pointer-events: none; 10 | } 11 | 12 | .page-header { 13 | text-align: left; 14 | padding-top: .8rem; 15 | padding-bottom: .8rem; 16 | background-color: #023858; /* purple #4F3871; */ 17 | color: #FFFFFF; 18 | } 19 | 20 | .page-header .logo { 21 | width: 8%; 22 | margin-top: 8px; 23 | } 24 | 25 | .page-header .logo a { 26 | display: inline-block; 27 | } 28 | 29 | .projects { 30 | padding-top: 2rem; 31 | padding: 50px 200px 50px; 32 | } 33 | 34 | .row { 35 | margin-bottom: 20px; 36 | } 37 | 38 | .project { 39 | text-align: left; 40 | font-size: 1.2em; 41 | } 42 | 43 | .owner, 44 | .path-divider, 45 | .repo { 46 | vertical-align: top; 47 | } 48 | 49 | .prerelease { 50 | background-color: #c9510c; 51 | } 52 | 53 | .latest { 54 | background-color: #6cc644; 55 | } 56 | 57 | .wip { 58 | background-color: #767676; 59 | } 60 | 61 | .release-label { 62 | display: inline-block; 63 | margin-top: 1px; 64 | margin-bottom: 10px; 65 | padding: 5px 10px; 66 | font-size: 14px; 67 | font-weight: bold; 68 | color: #fff; 69 | border-radius: 3px; 70 | } 71 | 72 | 73 | .release-label a { 74 | color: #fff; 75 | } 76 | 77 | .release { 78 | text-align: center; 79 | } 80 | 81 | .projects-header { 82 | margin-bottom: 40px; 83 | } 84 | 85 | .none { 86 | visibility: hidden; 87 | } 88 | 89 | .add-repo { 90 | padding-bottom: 20px; 91 | } 92 | 93 | .add-repo .repo { 94 | font-size: 1.2em; 95 | vertical-align: middle; 96 | padding-right: 12px; 97 | } 98 | 99 | .add-repo .octicon-repo { 100 | vertical-align: bottom; 101 | } 102 | 103 | .flash { 104 | margin-bottom: 20px; 105 | } 106 | 107 | .left { 108 | float: left; 109 | } 110 | -------------------------------------------------------------------------------- /assets/css/octicons.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'octicons'; 3 | src: url('../fonts/octicons.eot?#iefix') format('embedded-opentype'), 4 | url('../fonts/octicons.woff') format('woff'), 5 | url('../fonts/octicons.ttf') format('truetype'), 6 | url('../fonts/octicons.svg#octicons') format('svg'); 7 | font-weight: normal; 8 | font-style: normal; 9 | } 10 | 11 | /* 12 | 13 | .octicon is optimized for 16px. 14 | .mega-octicon is optimized for 32px but can be used larger. 15 | 16 | */ 17 | .octicon, .mega-octicon { 18 | font: normal normal normal 16px/1 octicons; 19 | display: inline-block; 20 | text-decoration: none; 21 | text-rendering: auto; 22 | -webkit-font-smoothing: antialiased; 23 | -moz-osx-font-smoothing: grayscale; 24 | -webkit-user-select: none; 25 | -moz-user-select: none; 26 | -ms-user-select: none; 27 | user-select: none; 28 | } 29 | .mega-octicon { font-size: 32px; } 30 | 31 | .octicon-alert:before { content: '\f02d'} /* */ 32 | .octicon-alignment-align:before { content: '\f08a'} /* */ 33 | .octicon-alignment-aligned-to:before { content: '\f08e'} /* */ 34 | .octicon-alignment-unalign:before { content: '\f08b'} /* */ 35 | .octicon-arrow-down:before { content: '\f03f'} /* */ 36 | .octicon-arrow-left:before { content: '\f040'} /* */ 37 | .octicon-arrow-right:before { content: '\f03e'} /* */ 38 | .octicon-arrow-small-down:before { content: '\f0a0'} /* */ 39 | .octicon-arrow-small-left:before { content: '\f0a1'} /* */ 40 | .octicon-arrow-small-right:before { content: '\f071'} /* */ 41 | .octicon-arrow-small-up:before { content: '\f09f'} /* */ 42 | .octicon-arrow-up:before { content: '\f03d'} /* */ 43 | .octicon-beer:before { content: '\f069'} /* */ 44 | .octicon-book:before { content: '\f007'} /* */ 45 | .octicon-bookmark:before { content: '\f07b'} /* */ 46 | .octicon-briefcase:before { content: '\f0d3'} /* */ 47 | .octicon-broadcast:before { content: '\f048'} /* */ 48 | .octicon-browser:before { content: '\f0c5'} /* */ 49 | .octicon-bug:before { content: '\f091'} /* */ 50 | .octicon-calendar:before { content: '\f068'} /* */ 51 | .octicon-check:before { content: '\f03a'} /* */ 52 | .octicon-checklist:before { content: '\f076'} /* */ 53 | .octicon-chevron-down:before { content: '\f0a3'} /* */ 54 | .octicon-chevron-left:before { content: '\f0a4'} /* */ 55 | .octicon-chevron-right:before { content: '\f078'} /* */ 56 | .octicon-chevron-up:before { content: '\f0a2'} /* */ 57 | .octicon-circle-slash:before { content: '\f084'} /* */ 58 | .octicon-circuit-board:before { content: '\f0d6'} /* */ 59 | .octicon-clippy:before { content: '\f035'} /* */ 60 | .octicon-clock:before { content: '\f046'} /* */ 61 | .octicon-cloud-download:before { content: '\f00b'} /* */ 62 | .octicon-cloud-upload:before { content: '\f00c'} /* */ 63 | .octicon-code:before { content: '\f05f'} /* */ 64 | .octicon-color-mode:before { content: '\f065'} /* */ 65 | .octicon-comment-add:before, 66 | .octicon-comment:before { content: '\f02b'} /* */ 67 | .octicon-comment-discussion:before { content: '\f04f'} /* */ 68 | .octicon-credit-card:before { content: '\f045'} /* */ 69 | .octicon-dash:before { content: '\f0ca'} /* */ 70 | .octicon-dashboard:before { content: '\f07d'} /* */ 71 | .octicon-database:before { content: '\f096'} /* */ 72 | .octicon-device-camera:before { content: '\f056'} /* */ 73 | .octicon-device-camera-video:before { content: '\f057'} /* */ 74 | .octicon-device-desktop:before { content: '\f27c'} /* */ 75 | .octicon-device-mobile:before { content: '\f038'} /* */ 76 | .octicon-diff:before { content: '\f04d'} /* */ 77 | .octicon-diff-added:before { content: '\f06b'} /* */ 78 | .octicon-diff-ignored:before { content: '\f099'} /* */ 79 | .octicon-diff-modified:before { content: '\f06d'} /* */ 80 | .octicon-diff-removed:before { content: '\f06c'} /* */ 81 | .octicon-diff-renamed:before { content: '\f06e'} /* */ 82 | .octicon-ellipsis:before { content: '\f09a'} /* */ 83 | .octicon-eye-unwatch:before, 84 | .octicon-eye-watch:before, 85 | .octicon-eye:before { content: '\f04e'} /* */ 86 | .octicon-file-binary:before { content: '\f094'} /* */ 87 | .octicon-file-code:before { content: '\f010'} /* */ 88 | .octicon-file-directory:before { content: '\f016'} /* */ 89 | .octicon-file-media:before { content: '\f012'} /* */ 90 | .octicon-file-pdf:before { content: '\f014'} /* */ 91 | .octicon-file-submodule:before { content: '\f017'} /* */ 92 | .octicon-file-symlink-directory:before { content: '\f0b1'} /* */ 93 | .octicon-file-symlink-file:before { content: '\f0b0'} /* */ 94 | .octicon-file-text:before { content: '\f011'} /* */ 95 | .octicon-file-zip:before { content: '\f013'} /* */ 96 | .octicon-flame:before { content: '\f0d2'} /* */ 97 | .octicon-fold:before { content: '\f0cc'} /* */ 98 | .octicon-gear:before { content: '\f02f'} /* */ 99 | .octicon-gift:before { content: '\f042'} /* */ 100 | .octicon-gist:before { content: '\f00e'} /* */ 101 | .octicon-gist-secret:before { content: '\f08c'} /* */ 102 | .octicon-git-branch-create:before, 103 | .octicon-git-branch-delete:before, 104 | .octicon-git-branch:before { content: '\f020'} /* */ 105 | .octicon-git-commit:before { content: '\f01f'} /* */ 106 | .octicon-git-compare:before { content: '\f0ac'} /* */ 107 | .octicon-git-merge:before { content: '\f023'} /* */ 108 | .octicon-git-pull-request-abandoned:before, 109 | .octicon-git-pull-request:before { content: '\f009'} /* */ 110 | .octicon-globe:before { content: '\f0b6'} /* */ 111 | .octicon-graph:before { content: '\f043'} /* */ 112 | .octicon-heart:before { content: '\2665'} /* ♥ */ 113 | .octicon-history:before { content: '\f07e'} /* */ 114 | .octicon-home:before { content: '\f08d'} /* */ 115 | .octicon-horizontal-rule:before { content: '\f070'} /* */ 116 | .octicon-hourglass:before { content: '\f09e'} /* */ 117 | .octicon-hubot:before { content: '\f09d'} /* */ 118 | .octicon-inbox:before { content: '\f0cf'} /* */ 119 | .octicon-info:before { content: '\f059'} /* */ 120 | .octicon-issue-closed:before { content: '\f028'} /* */ 121 | .octicon-issue-opened:before { content: '\f026'} /* */ 122 | .octicon-issue-reopened:before { content: '\f027'} /* */ 123 | .octicon-jersey:before { content: '\f019'} /* */ 124 | .octicon-jump-down:before { content: '\f072'} /* */ 125 | .octicon-jump-left:before { content: '\f0a5'} /* */ 126 | .octicon-jump-right:before { content: '\f0a6'} /* */ 127 | .octicon-jump-up:before { content: '\f073'} /* */ 128 | .octicon-key:before { content: '\f049'} /* */ 129 | .octicon-keyboard:before { content: '\f00d'} /* */ 130 | .octicon-law:before { content: '\f0d8'} /* */ 131 | .octicon-light-bulb:before { content: '\f000'} /* */ 132 | .octicon-link:before { content: '\f05c'} /* */ 133 | .octicon-link-external:before { content: '\f07f'} /* */ 134 | .octicon-list-ordered:before { content: '\f062'} /* */ 135 | .octicon-list-unordered:before { content: '\f061'} /* */ 136 | .octicon-location:before { content: '\f060'} /* */ 137 | .octicon-gist-private:before, 138 | .octicon-mirror-private:before, 139 | .octicon-git-fork-private:before, 140 | .octicon-lock:before { content: '\f06a'} /* */ 141 | .octicon-logo-github:before { content: '\f092'} /* */ 142 | .octicon-mail:before { content: '\f03b'} /* */ 143 | .octicon-mail-read:before { content: '\f03c'} /* */ 144 | .octicon-mail-reply:before { content: '\f051'} /* */ 145 | .octicon-mark-github:before { content: '\f00a'} /* */ 146 | .octicon-markdown:before { content: '\f0c9'} /* */ 147 | .octicon-megaphone:before { content: '\f077'} /* */ 148 | .octicon-mention:before { content: '\f0be'} /* */ 149 | .octicon-microscope:before { content: '\f089'} /* */ 150 | .octicon-milestone:before { content: '\f075'} /* */ 151 | .octicon-mirror-public:before, 152 | .octicon-mirror:before { content: '\f024'} /* */ 153 | .octicon-mortar-board:before { content: '\f0d7'} /* */ 154 | .octicon-move-down:before { content: '\f0a8'} /* */ 155 | .octicon-move-left:before { content: '\f074'} /* */ 156 | .octicon-move-right:before { content: '\f0a9'} /* */ 157 | .octicon-move-up:before { content: '\f0a7'} /* */ 158 | .octicon-mute:before { content: '\f080'} /* */ 159 | .octicon-no-newline:before { content: '\f09c'} /* */ 160 | .octicon-octoface:before { content: '\f008'} /* */ 161 | .octicon-organization:before { content: '\f037'} /* */ 162 | .octicon-package:before { content: '\f0c4'} /* */ 163 | .octicon-paintcan:before { content: '\f0d1'} /* */ 164 | .octicon-pencil:before { content: '\f058'} /* */ 165 | .octicon-person-add:before, 166 | .octicon-person-follow:before, 167 | .octicon-person:before { content: '\f018'} /* */ 168 | .octicon-pin:before { content: '\f041'} /* */ 169 | .octicon-playback-fast-forward:before { content: '\f0bd'} /* */ 170 | .octicon-playback-pause:before { content: '\f0bb'} /* */ 171 | .octicon-playback-play:before { content: '\f0bf'} /* */ 172 | .octicon-playback-rewind:before { content: '\f0bc'} /* */ 173 | .octicon-plug:before { content: '\f0d4'} /* */ 174 | .octicon-repo-create:before, 175 | .octicon-gist-new:before, 176 | .octicon-file-directory-create:before, 177 | .octicon-file-add:before, 178 | .octicon-plus:before { content: '\f05d'} /* */ 179 | .octicon-podium:before { content: '\f0af'} /* */ 180 | .octicon-primitive-dot:before { content: '\f052'} /* */ 181 | .octicon-primitive-square:before { content: '\f053'} /* */ 182 | .octicon-pulse:before { content: '\f085'} /* */ 183 | .octicon-puzzle:before { content: '\f0c0'} /* */ 184 | .octicon-question:before { content: '\f02c'} /* */ 185 | .octicon-quote:before { content: '\f063'} /* */ 186 | .octicon-radio-tower:before { content: '\f030'} /* */ 187 | .octicon-repo-delete:before, 188 | .octicon-repo:before { content: '\f001'} /* */ 189 | .octicon-repo-clone:before { content: '\f04c'} /* */ 190 | .octicon-repo-force-push:before { content: '\f04a'} /* */ 191 | .octicon-gist-fork:before, 192 | .octicon-repo-forked:before { content: '\f002'} /* */ 193 | .octicon-repo-pull:before { content: '\f006'} /* */ 194 | .octicon-repo-push:before { content: '\f005'} /* */ 195 | .octicon-rocket:before { content: '\f033'} /* */ 196 | .octicon-rss:before { content: '\f034'} /* */ 197 | .octicon-ruby:before { content: '\f047'} /* */ 198 | .octicon-screen-full:before { content: '\f066'} /* */ 199 | .octicon-screen-normal:before { content: '\f067'} /* */ 200 | .octicon-search-save:before, 201 | .octicon-search:before { content: '\f02e'} /* */ 202 | .octicon-server:before { content: '\f097'} /* */ 203 | .octicon-settings:before { content: '\f07c'} /* */ 204 | .octicon-log-in:before, 205 | .octicon-sign-in:before { content: '\f036'} /* */ 206 | .octicon-log-out:before, 207 | .octicon-sign-out:before { content: '\f032'} /* */ 208 | .octicon-split:before { content: '\f0c6'} /* */ 209 | .octicon-squirrel:before { content: '\f0b2'} /* */ 210 | .octicon-star-add:before, 211 | .octicon-star-delete:before, 212 | .octicon-star:before { content: '\f02a'} /* */ 213 | .octicon-steps:before { content: '\f0c7'} /* */ 214 | .octicon-stop:before { content: '\f08f'} /* */ 215 | .octicon-repo-sync:before, 216 | .octicon-sync:before { content: '\f087'} /* */ 217 | .octicon-tag-remove:before, 218 | .octicon-tag-add:before, 219 | .octicon-tag:before { content: '\f015'} /* */ 220 | .octicon-telescope:before { content: '\f088'} /* */ 221 | .octicon-terminal:before { content: '\f0c8'} /* */ 222 | .octicon-three-bars:before { content: '\f05e'} /* */ 223 | .octicon-thumbsdown:before { content: '\f0db'} /* */ 224 | .octicon-thumbsup:before { content: '\f0da'} /* */ 225 | .octicon-tools:before { content: '\f031'} /* */ 226 | .octicon-trashcan:before { content: '\f0d0'} /* */ 227 | .octicon-triangle-down:before { content: '\f05b'} /* */ 228 | .octicon-triangle-left:before { content: '\f044'} /* */ 229 | .octicon-triangle-right:before { content: '\f05a'} /* */ 230 | .octicon-triangle-up:before { content: '\f0aa'} /* */ 231 | .octicon-unfold:before { content: '\f039'} /* */ 232 | .octicon-unmute:before { content: '\f0ba'} /* */ 233 | .octicon-versions:before { content: '\f064'} /* */ 234 | .octicon-remove-close:before, 235 | .octicon-x:before { content: '\f081'} /* */ 236 | .octicon-zap:before { content: '\26A1'} /* ⚡ */ 237 | -------------------------------------------------------------------------------- /assets/css/primer.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}h1{font-size:2em;margin:0.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace, monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0}input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{height:auto}input[type="search"]{-webkit-appearance:textfield;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:0.35em 0.625em 0.75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:bold}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}*{box-sizing:border-box}input,select,textarea,button{font:13px/1.4 Helvetica,arial,nimbussansl,liberationsans,freesans,clean,sans-serif,"Segoe UI Emoji","Segoe UI Symbol"}body{font:13px/1.4 Helvetica,arial,nimbussansl,liberationsans,freesans,clean,sans-serif,"Segoe UI Emoji","Segoe UI Symbol";color:#333;background-color:#fff}a{color:#4078c0;text-decoration:none}a:hover,a:active{text-decoration:underline}hr,.rule{height:0;margin:15px 0;overflow:hidden;background:transparent;border:0;border-bottom:1px solid #ddd}hr:before,.rule:before{display:table;content:""}hr:after,.rule:after{display:table;clear:both;content:""}h1,h2,h3,h4,h5,h6{margin-top:15px;margin-bottom:15px;line-height:1.1}h1{font-size:30px}h2{font-size:21px}h3{font-size:16px}h4{font-size:14px}h5{font-size:12px}h6{font-size:11px}small{font-size:90%}blockquote{margin:0}.lead{margin-bottom:30px;font-size:20px;font-weight:300;color:#555}.text-muted{color:#767676}.text-danger{color:#bd2c00}.text-emphasized{font-weight:bold;color:#333}ul,ol{padding:0;margin-top:0;margin-bottom:0}ol ol,ul ol{list-style-type:lower-roman}ul ul ol,ul ol ol,ol ul ol,ol ol ol{list-style-type:lower-alpha}dd{margin-left:0}tt,code{font-family:Consolas,"Liberation Mono",Menlo,Courier,monospace;font-size:12px}pre{margin-top:0;margin-bottom:0;font:12px Consolas,"Liberation Mono",Menlo,Courier,monospace}.container{width:980px;margin-right:auto;margin-left:auto}.container:before{display:table;content:""}.container:after{display:table;clear:both;content:""}.columns{margin-right:-10px;margin-left:-10px}.columns:before{display:table;content:""}.columns:after{display:table;clear:both;content:""}.column{float:left;padding-right:10px;padding-left:10px}.one-third{width:33.333333%}.two-thirds{width:66.666667%}.one-fourth{width:25%}.one-half{width:50%}.three-fourths{width:75%}.one-fifth{width:20%}.four-fifths{width:80%}.single-column{padding-right:10px;padding-left:10px}.table-column{display:table-cell;width:1%;padding-right:10px;padding-left:10px;vertical-align:top}fieldset{padding:0;margin:0;border:0}label{font-size:13px;font-weight:bold}.form-control,input[type="text"],input[type="password"],input[type="email"],input[type="number"],input[type="tel"],input[type="url"],select,textarea{min-height:34px;padding:7px 8px;font-size:13px;color:#333;vertical-align:middle;background-color:#fff;background-repeat:no-repeat;background-position:right 8px center;border:1px solid #ccc;border-radius:3px;outline:none;box-shadow:inset 0 1px 2px rgba(0,0,0,0.075)}.form-control.focus,.form-control:focus,input[type="text"].focus,input[type="text"]:focus,input[type="password"].focus,input[type="password"]:focus,input[type="email"].focus,input[type="email"]:focus,input[type="number"].focus,input[type="number"]:focus,input[type="tel"].focus,input[type="tel"]:focus,input[type="url"].focus,input[type="url"]:focus,select.focus,select:focus,textarea.focus,textarea:focus{border-color:#51a7e8;box-shadow:inset 0 1px 2px rgba(0,0,0,0.075),0 0 5px rgba(81,167,232,0.5)}select:not([multiple]){height:34px;vertical-align:middle}input.input-contrast,.input-contrast{background-color:#fafafa}input.input-contrast:focus,.input-contrast:focus{background-color:#fff}::-webkit-input-placeholder{color:#aaa}::-moz-placeholder{color:#aaa}:-ms-input-placeholder{color:#aaa}::placeholder{color:#aaa}input.input-mini{min-height:26px;padding-top:4px;padding-bottom:4px;font-size:12px}input.input-large{padding:6px 10px;font-size:16px}.input-block{display:block;width:100%}.input-monospace{font-family:Consolas,"Liberation Mono",Menlo,Courier,monospace}dl.form{margin:15px 0}dl.form input[type="text"],dl.form input[type="password"],dl.form input[type="email"],dl.form input[type="url"],dl.form select,dl.form textarea{background-color:#fafafa}dl.form input[type="text"]:focus,dl.form input[type="password"]:focus,dl.form input[type="email"]:focus,dl.form input[type="url"]:focus,dl.form select:focus,dl.form textarea:focus{background-color:#fff}dl.form>dt{margin:0 0 6px}dl.form>dt label{position:relative}dl.form.flattened>dt{float:left;margin:0;line-height:32px}dl.form.flattened>dd{line-height:32px}dl.form>dd input[type="text"],dl.form>dd input[type="password"],dl.form>dd input[type="email"],dl.form>dd input[type="url"]{width:440px;max-width:100%;margin-right:5px}dl.form>dd input.shorter{width:130px}dl.form>dd input.short{width:250px}dl.form>dd input.long{width:100%}dl.form>dd textarea{width:100%;height:200px;min-height:200px}dl.form>dd textarea.short{height:50px;min-height:50px}dl.form>dd h4{margin:4px 0 0}dl.form>dd h4.is-error{color:#bd2c00}dl.form>dd h4.is-success{color:#6cc644}dl.form>dd h4+p.note{margin-top:0}dl.form.required>dt>label:after{padding-left:5px;color:#9f1006;content:"*"}dl.form .success,dl.form .error,dl.form .indicator{display:none;font-size:12px;font-weight:bold}dl.form.loading{opacity:0.5}dl.form.loading .indicator{display:inline}dl.form.loading .spinner{display:inline-block;vertical-align:middle}dl.form.successful .success{display:inline;color:#390}dl.form.warn dd.warning,dl.form.warn dd.error,dl.form.errored dd.warning,dl.form.errored dd.error{display:inline-block;position:absolute;max-width:450px;z-index:10;margin:2px 0 0;padding:5px 8px;font-size:13px;font-weight:normal;border-radius:3px}dl.form.warn dd.warning:after,dl.form.warn dd.warning:before,dl.form.warn dd.error:after,dl.form.warn dd.error:before,dl.form.errored dd.warning:after,dl.form.errored dd.warning:before,dl.form.errored dd.error:after,dl.form.errored dd.error:before{bottom:100%;z-index:15;left:10px;border:solid transparent;content:" ";height:0;width:0;position:absolute;pointer-events:none}dl.form.warn dd.warning:after,dl.form.warn dd.error:after,dl.form.errored dd.warning:after,dl.form.errored dd.error:after{border-width:5px}dl.form.warn dd.warning:before,dl.form.warn dd.error:before,dl.form.errored dd.warning:before,dl.form.errored dd.error:before{border-width:6px;margin-left:-1px}dl.form.warn dd.warning{color:#4e401e;background-color:#ffe5a7;border:1px solid #e7ce94}dl.form.warn dd.warning:after{border-bottom-color:#ffe5a7}dl.form.warn dd.warning:before{border-bottom-color:#cdb683}dl.form.errored>dt label{color:#bd2c00}dl.form.errored dd.error{color:#fff;background-color:#bf1515;border-color:#911;font-size:13px}dl.form.errored dd.error:after{border-bottom-color:#bf1515}dl.form.errored dd.error:before{border-bottom-color:#911}.note{min-height:17px;margin:4px 0 2px;font-size:12px;color:#767676}.note .spinner{margin-right:3px;vertical-align:middle}.form-checkbox{padding-left:20px;margin:15px 0;vertical-align:middle}.form-checkbox label em.highlight{position:relative;left:-4px;padding:2px 4px;font-style:normal;background:#fffbdc;border-radius:3px}.form-checkbox input[type=checkbox],.form-checkbox input[type=radio]{float:left;margin:2px 0 0 -20px;vertical-align:middle}.form-checkbox .note{display:block;margin:0;font-size:12px;font-weight:normal;color:#666}.hfields{margin:15px 0}.hfields:before{display:table;content:""}.hfields:after{display:table;clear:both;content:""}.hfields dl.form{float:left;margin:0 30px 0 0}.hfields dl.form>dt label{display:inline-block;margin:5px 0 0;color:#666}.hfields dl.form>dt img{position:relative;top:-2px}.hfields .btn{float:left;margin:28px 25px 0 -20px}.hfields select{margin-top:5px}input::-webkit-outer-spin-button,input::-webkit-inner-spin-button{margin:0;-webkit-appearance:none}.input-group{display:table}.input-group input{position:relative;width:100%}.input-group input:focus{z-index:2}.input-group input[type="text"]+.btn{margin-left:0}.input-group.inline{display:inline-table}.input-group input,.input-group-button{display:table-cell}.input-group-button{width:1%;vertical-align:middle}.input-group input:first-child,.input-group-button:first-child .btn{border-top-right-radius:0;border-bottom-right-radius:0}.input-group-button:first-child .btn{margin-right:-1px}.input-group input:last-child,.input-group-button:last-child .btn{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-button:last-child .btn{margin-left:-1px}.form-actions:before{display:table;content:""}.form-actions:after{display:table;clear:both;content:""}.form-actions .btn{float:right}.form-actions .btn+.btn{margin-right:5px}.form-warning{padding:8px 10px;margin:10px 0;font-size:14px;color:#333;background:#ffffe2;border:1px solid #e7e4c2;border-radius:4px}.form-warning p{margin:0;line-height:1.5}.form-warning strong{color:#000}.form-warning a{font-weight:bold}.status-indicator{font:normal normal 16px/1 "octicons";display:inline-block;text-decoration:none;-webkit-font-smoothing:antialiased;margin-left:5px}.status-indicator-success:before{color:#6cc644;content:"\f03a"}.status-indicator-failed:before{color:#bd2c00;content:"\f02d"}.select{display:inline-block;max-width:100%;padding:7px 24px 7px 8px;vertical-align:middle;background:#fff url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAUCAMAAACzvE1FAAAADFBMVEUzMzMzMzMzMzMzMzMKAG/3AAAAA3RSTlMAf4C/aSLHAAAAPElEQVR42q3NMQ4AIAgEQTn//2cLdRKppSGzBYwzVXvznNWs8C58CiussPJj8h6NwgorrKRdTvuV9v16Afn0AYFOB7aYAAAAAElFTkSuQmCC) no-repeat right 8px center;background-size:8px 10px;box-shadow:inset 0 -1px 2px rgba(0,0,0,0.075);-webkit-appearance:none;-moz-appearance:none;appearance:none;padding-right:8px \9;background-image:none \9}.select:focus{outline:none;border-color:#51a7e8;box-shadow:inset 0 1px 2px rgba(0,0,0,0.075),0 0 5px rgba(81,167,232,0.5)}.select::-ms-expand{opacity:0}.select-sm{padding-top:3px;padding-bottom:3px;font-size:12px}.select-sm:not([multiple]){height:26px;min-height:26px}.clearfix:before{display:table;content:""}.clearfix:after{display:table;clear:both;content:""}.right{float:right}.left{float:left}.centered{display:block;float:none;margin-left:auto;margin-right:auto}.text-right{text-align:right}.text-left{text-align:left}.text-center{text-align:center}.danger{color:#c00}.mute{color:#000}.text-diff-added{color:#55a532}.text-diff-deleted{color:#bd2c00}.text-open,.text-success{color:#6cc644}.text-closed{color:#bd2c00}.text-reverted{color:#bd2c00}.text-merged{color:#6e5494}.text-renamed{color:#fffa5d}.text-pending{color:#cea61b}.text-error,.text-failure{color:#bd2c00}.muted-link{color:#767676}.muted-link:hover{color:#4078c0;text-decoration:none}.hidden{display:none}.warning{padding:0.5em;margin-bottom:0.8em;font-weight:bold;background-color:#fffccc}.error_box{padding:1em;font-weight:bold;background-color:#ffebe8;border:1px solid #dd3c10}.flash-messages{margin-top:15px;margin-bottom:15px}.flash,.flash-global{position:relative;font-size:14px;line-height:1.6;color:#246;background-color:#e2eef9;border:solid 1px #bac6d3}.flash.flash-warn,.flash-global.flash-warn{color:#4c4a42;background-color:#fff9ea;border-color:#dfd8c2}.flash.flash-error,.flash-global.flash-error{color:#911;background-color:#fcdede;border-color:#d2b2b2}.flash .flash-close,.flash-global .flash-close{float:right;padding:17px;margin-top:-15px;margin-right:-15px;margin-left:20px;color:inherit;text-decoration:none;cursor:pointer;opacity:0.6}.flash .flash-close:hover,.flash-global .flash-close:hover{opacity:1}.flash p:last-child,.flash-global p:last-child{margin-bottom:0}.flash .flash-action,.flash-global .flash-action{float:right;margin-top:-4px;margin-left:20px}.flash a,.flash-global a{font-weight:bold}.flash{padding:15px;border-radius:3px}.flash+.flash{margin-top:5px}.flash-with-icon{padding-left:40px}.flash-with-icon>.octicon{float:left;margin-top:3px;margin-left:-25px}.flash-global{padding:10px;margin-top:-1px;border-width:1px 0}.flash-global h2,.flash-global p{margin-top:0;margin-bottom:0;font-size:14px;line-height:1.4}.flash-global .flash-action{margin-top:5px}.flash-title{margin-top:0;margin-bottom:5px}.avatar{display:inline-block;overflow:hidden;line-height:1;vertical-align:middle;border-radius:3px}.avatar-small{border-radius:2px}.avatar-link{float:left;line-height:1}.avatar-group-item{display:inline-block;margin-bottom:3px}.avatar-parent-child{position:relative}.avatar-child{position:absolute;right:-15%;bottom:-9%;background-color:#fff;border-radius:2px;box-shadow:-2px -2px 0 rgba(255,255,255,0.8)}.blankslate{position:relative;padding:30px;text-align:center;background-color:#fafafa;border:1px solid #e5e5e5;border-radius:3px;box-shadow:inset 0 0 10px rgba(0,0,0,0.05)}.blankslate.clean-background{background:none;border:0;box-shadow:none}.blankslate.capped{border-radius:0 0 3px 3px}.blankslate.spacious{padding:100px 60px 120px}.blankslate.has-fixed-width{width:485px;margin:0 auto}.blankslate.large-format h3{margin:0.75em 0;font-size:20px}.blankslate.large-format p{font-size:16px}.blankslate.large-format p.has-fixed-width{width:540px;margin:0 auto;text-align:left}.blankslate.large-format .mega-octicon{width:40px;height:40px;font-size:40px;color:#aaa}.blankslate.large-format .octicon-inbox{font-size:48px;line-height:40px}.blankslate code{padding:2px 5px 3px;font-size:14px;background:#fff;border:1px solid #eee;border-radius:3px}.blankslate>.mega-octicon{color:#aaa}.blankslate .mega-octicon+.mega-octicon{margin-left:10px}.tabnav+.blankslate{margin-top:20px}.blankslate .context-loader.large-format-loader{padding-top:50px}.counter{display:inline-block;padding:2px 5px;font-size:11px;font-weight:bold;line-height:1;color:#666;background-color:#eee;border-radius:20px}.btn{position:relative;display:inline-block;padding:6px 12px;font-size:13px;font-weight:bold;line-height:20px;color:#333;white-space:nowrap;vertical-align:middle;cursor:pointer;background-color:#eee;background-image:linear-gradient(#fcfcfc, #eee);border:1px solid #d5d5d5;border-radius:3px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-appearance:none}.btn i{font-style:normal;font-weight:500;opacity:0.6}.btn .octicon{vertical-align:text-top}.btn .counter{text-shadow:none;background-color:#e5e5e5}.btn:focus{text-decoration:none;border-color:#51a7e8;outline:none;box-shadow:0 0 5px rgba(81,167,232,0.5)}.btn:focus:hover,.btn.selected:focus{border-color:#51a7e8}.btn:hover,.btn:active,.btn.zeroclipboard-is-hover,.btn.zeroclipboard-is-active{text-decoration:none;background-color:#ddd;background-image:linear-gradient(#eee, #ddd);border-color:#ccc}.btn:active,.btn.selected,.btn.zeroclipboard-is-active{background-color:#dcdcdc;background-image:none;border-color:#b5b5b5;box-shadow:inset 0 2px 4px rgba(0,0,0,0.15)}.btn.selected:hover{background-color:#cfcfcf}.btn:disabled,.btn:disabled:hover,.btn.disabled,.btn.disabled:hover{color:rgba(102,102,102,0.5);cursor:default;background-color:rgba(229,229,229,0.5);background-image:none;border-color:rgba(197,197,197,0.5);box-shadow:none}.btn-primary{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.15);background-color:#60b044;background-image:linear-gradient(#8add6d, #60b044);border-color:#5ca941}.btn-primary .counter{color:#60b044;background-color:#fff}.btn-primary:hover{color:#fff;background-color:#569e3d;background-image:linear-gradient(#79d858, #569e3d);border-color:#4a993e}.btn-primary:active,.btn-primary.selected{text-shadow:0 1px 0 rgba(0,0,0,0.15);background-color:#569e3d;background-image:none;border-color:#418737}.btn-primary.selected:hover{background-color:#4c8b36}.btn-primary:disabled,.btn-primary:disabled:hover,.btn-primary.disabled,.btn-primary.disabled:hover{color:#fefefe;background-color:#add39f;background-image:linear-gradient(#c3ecb4, #add39f);border-color:#b9dcac #b9dcac #a7c89b}.btn-danger{color:#900}.btn-danger:hover{color:#fff;background-color:#b33630;background-image:linear-gradient(#dc5f59, #b33630);border-color:#cd504a}.btn-danger:active,.btn-danger.selected{color:#fff;background-color:#b33630;background-image:none;border-color:#9f312c}.btn-danger.selected:hover{background-color:#9f302b}.btn-danger:disabled,.btn-danger:disabled:hover,.btn-danger.disabled,.btn-danger.disabled:hover{color:#cb7f7f;background-color:#efefef;background-image:linear-gradient(#fefefe, #efefef);border-color:#e1e1e1}.btn-danger:hover .counter,.btn-danger:active .counter,.btn-danger.selected .counter{color:#b33630;background-color:#fff}.btn-outline{color:#4078c0;background-color:#fff;background-image:none;border:1px solid #e5e5e5}.btn-outline .counter{background-color:#eee}.btn-outline:hover,.btn-outline:active,.btn-outline.selected,.btn-outline.zeroclipboard-is-hover,.btn-outline.zeroclipboard-is-active{color:#fff;background-color:#4078c0;background-image:none;border-color:#4078c0}.btn-outline:hover .counter,.btn-outline:active .counter,.btn-outline.selected .counter,.btn-outline.zeroclipboard-is-hover .counter,.btn-outline.zeroclipboard-is-active .counter{color:#4078c0;background-color:#fff}.btn-outline.selected:hover{background-color:#396cad}.btn-outline:disabled,.btn-outline:disabled:hover,.btn-outline.disabled,.btn-outline.disabled:hover{color:#767676;background-color:#fff;background-image:none;border-color:#e5e5e5}.btn-with-count{float:left;border-top-right-radius:0;border-bottom-right-radius:0}.btn-sm{padding:2px 10px}.hidden-text-expander{display:block}.hidden-text-expander.inline{position:relative;top:-1px;display:inline-block;margin-left:5px;line-height:0}.hidden-text-expander a{display:inline-block;height:12px;padding:0 5px;font-size:12px;font-weight:bold;line-height:6px;color:#555;text-decoration:none;vertical-align:middle;background:#ddd;border-radius:1px}.hidden-text-expander a:hover{text-decoration:none;background-color:#ccc}.hidden-text-expander a:active{color:#fff;background-color:#4183c4}.social-count{float:left;padding:2px 7px;font-size:11px;font-weight:bold;line-height:20px;color:#333;vertical-align:middle;background-color:#fff;border:1px solid #ddd;border-left:0;border-top-right-radius:3px;border-bottom-right-radius:3px}.social-count:hover,.social-count:active{text-decoration:none}.social-count:hover{color:#4078c0;cursor:pointer}.btn-block{display:block;width:100%;text-align:center}.btn-group{display:inline-block;vertical-align:middle}.btn-group:before{display:table;content:""}.btn-group:after{display:table;clear:both;content:""}.btn-group .btn{position:relative;float:left}.btn-group .btn:not(:first-child):not(:last-child){border-radius:0}.btn-group .btn:first-child{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group .btn:last-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .btn:hover,.btn-group .btn:active,.btn-group .btn.selected{z-index:2}.btn-group .btn:focus{z-index:3}.btn-group .btn+.btn{margin-left:-1px}.btn-group .btn+.button_to,.btn-group .button_to+.btn,.btn-group .button_to+.button_to{margin-left:-1px}.btn-group .button_to{float:left}.btn-group .button_to .btn{border-radius:0}.btn-group .button_to:first-child .btn{border-top-left-radius:3px;border-bottom-left-radius:3px}.btn-group .button_to:last-child .btn{border-top-right-radius:3px;border-bottom-right-radius:3px}.btn-group+.btn-group,.btn-group+.btn{margin-left:5px}.btn-link{display:inline-block;padding:0;font-size:inherit;color:#4078c0;white-space:nowrap;cursor:pointer;background-color:transparent;border:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-appearance:none}.btn-link:hover,.btn-link:focus{text-decoration:underline}.btn-link:focus{outline:none}.menu{margin-bottom:15px;list-style:none;background-color:#fff;border:1px solid #d8d8d8;border-radius:3px}.menu-item{position:relative;display:block;padding:8px 10px;text-shadow:0 1px 0 #fff;border-bottom:1px solid #eee}.menu-item:first-child{border-top:0;border-top-right-radius:2px;border-top-left-radius:2px}.menu-item:first-child:before{border-top-left-radius:2px}.menu-item:last-child{border-bottom:0;border-bottom-right-radius:2px;border-bottom-left-radius:2px}.menu-item:last-child:before{border-bottom-left-radius:2px}.menu-item:hover{text-decoration:none;background-color:#f9f9f9}.menu-item.selected{font-weight:bold;color:#222;cursor:default;background-color:#fff}.menu-item.selected:before{position:absolute;top:0;left:0;bottom:0;width:2px;content:"";background-color:#d26911}.menu-item .octicon{margin-right:5px;width:16px;color:#333;text-align:center}.menu-item .counter{float:right;margin-left:5px}.menu-item .menu-warning{float:right;color:#d26911}.menu-item .avatar{float:left;margin-right:5px}.menu-item.alert .counter{color:#bd2c00}.menu-heading{display:block;padding:8px 10px;margin-top:0;margin-bottom:0;font-size:13px;font-weight:bold;line-height:20px;color:#555;background-color:#f7f7f7;border-bottom:1px solid #eee}.menu-heading:hover{text-decoration:none}.menu-heading:first-child{border-top-right-radius:2px;border-top-left-radius:2px}.menu-heading:last-child{border-bottom-right-radius:2px;border-bottom-left-radius:2px;border-bottom:0}.tabnav{margin-top:0;margin-bottom:15px;border-bottom:1px solid #ddd}.tabnav .counter{margin-left:5px}.tabnav-tabs{margin-bottom:-1px}.tabnav-tab{display:inline-block;padding:8px 12px;font-size:14px;line-height:20px;color:#666;text-decoration:none;border:1px solid transparent;border-bottom:0}.tabnav-tab.selected{color:#333;background-color:#fff;border-color:#ddd;border-radius:3px 3px 0 0}.tabnav-tab:hover{text-decoration:none}.tabnav-extra{display:inline-block;padding-top:10px;margin-left:10px;font-size:12px;color:#666}.tabnav-extra>.octicon{margin-right:2px}a.tabnav-extra:hover{color:#4078c0;text-decoration:none}.tabnav-btn{margin-left:10px}.filter-list{list-style-type:none}.filter-list.small .filter-item{padding:4px 10px;margin:0 0 2px;font-size:12px}.filter-list.pjax-active .filter-item{color:#767676;background-color:transparent}.filter-list.pjax-active .filter-item.pjax-active{color:#fff;background-color:#4078c0}.filter-item{position:relative;display:block;padding:8px 10px;margin-bottom:5px;overflow:hidden;font-size:14px;color:#767676;text-decoration:none;text-overflow:ellipsis;white-space:nowrap;cursor:pointer;border-radius:3px}.filter-item:hover{text-decoration:none;background-color:#eee}.filter-item.selected{color:#fff;background-color:#4078c0}.filter-item.selected .octicon-remove-close{float:right;opacity:0.8}.filter-item .count{float:right;font-weight:bold}.filter-item .bar{position:absolute;top:2px;right:0;bottom:2px;z-index:-1;display:inline-block;background-color:#f1f1f1}.state{display:inline-block;padding:4px 8px;font-weight:bold;line-height:20px;color:#fff;text-align:center;border-radius:3px;background-color:#999}.state-open,.state-proposed,.state-reopened{background-color:#6cc644}.state-merged{background-color:#6e5494}.state-closed{background-color:#bd2c00}.state-renamed{background-color:#fffa5d}.tooltipped{position:relative}.tooltipped:after{position:absolute;z-index:1000000;display:none;padding:5px 8px;font:normal normal 11px/1.5 Helvetica,arial,nimbussansl,liberationsans,freesans,clean,sans-serif,"Segoe UI Emoji","Segoe UI Symbol";color:#fff;text-align:center;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-wrap:break-word;white-space:pre;pointer-events:none;content:attr(aria-label);background:rgba(0,0,0,0.8);border-radius:3px;-webkit-font-smoothing:subpixel-antialiased}.tooltipped:before{position:absolute;z-index:1000001;display:none;width:0;height:0;color:rgba(0,0,0,0.8);pointer-events:none;content:"";border:5px solid transparent}.tooltipped:hover:before,.tooltipped:hover:after,.tooltipped:active:before,.tooltipped:active:after,.tooltipped:focus:before,.tooltipped:focus:after{display:inline-block;text-decoration:none}.tooltipped-multiline:hover:after,.tooltipped-multiline:active:after,.tooltipped-multiline:focus:after{display:table-cell}.tooltipped-s:after,.tooltipped-se:after,.tooltipped-sw:after{top:100%;right:50%;margin-top:5px}.tooltipped-s:before,.tooltipped-se:before,.tooltipped-sw:before{top:auto;right:50%;bottom:-5px;margin-right:-5px;border-bottom-color:rgba(0,0,0,0.8)}.tooltipped-se:after{right:auto;left:50%;margin-left:-15px}.tooltipped-sw:after{margin-right:-15px}.tooltipped-n:after,.tooltipped-ne:after,.tooltipped-nw:after{right:50%;bottom:100%;margin-bottom:5px}.tooltipped-n:before,.tooltipped-ne:before,.tooltipped-nw:before{top:-5px;right:50%;bottom:auto;margin-right:-5px;border-top-color:rgba(0,0,0,0.8)}.tooltipped-ne:after{right:auto;left:50%;margin-left:-15px}.tooltipped-nw:after{margin-right:-15px}.tooltipped-s:after,.tooltipped-n:after{-webkit-transform:translateX(50%);-ms-transform:translateX(50%);transform:translateX(50%)}.tooltipped-w:after{right:100%;bottom:50%;margin-right:5px;-webkit-transform:translateY(50%);-ms-transform:translateY(50%);transform:translateY(50%)}.tooltipped-w:before{top:50%;bottom:50%;left:-5px;margin-top:-5px;border-left-color:rgba(0,0,0,0.8)}.tooltipped-e:after{bottom:50%;left:100%;margin-left:5px;-webkit-transform:translateY(50%);-ms-transform:translateY(50%);transform:translateY(50%)}.tooltipped-e:before{top:50%;right:-5px;bottom:50%;margin-top:-5px;border-right-color:rgba(0,0,0,0.8)}.tooltipped-multiline:after{width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:250px;word-break:break-word;word-wrap:normal;white-space:pre-line;border-collapse:separate}.tooltipped-multiline.tooltipped-s:after,.tooltipped-multiline.tooltipped-n:after{right:auto;left:50%;-webkit-transform:translateX(-50%);-ms-transform:translateX(-50%);transform:translateX(-50%)}.tooltipped-multiline.tooltipped-w:after,.tooltipped-multiline.tooltipped-e:after{right:100%}@media screen and (min-width: 0\0){.tooltipped-multiline:after{width:250px}}.tooltipped-sticky:before,.tooltipped-sticky:after{display:inline-block}.tooltipped-sticky.tooltipped-multiline:after{display:table-cell}.fullscreen-overlay-enabled.dark-theme .tooltipped:after{color:#000;background:rgba(255,255,255,0.8)}.fullscreen-overlay-enabled.dark-theme .tooltipped .tooltipped-s:before,.fullscreen-overlay-enabled.dark-theme .tooltipped .tooltipped-se:before,.fullscreen-overlay-enabled.dark-theme .tooltipped .tooltipped-sw:before{border-bottom-color:rgba(255,255,255,0.8)}.fullscreen-overlay-enabled.dark-theme .tooltipped.tooltipped-n:before,.fullscreen-overlay-enabled.dark-theme .tooltipped.tooltipped-ne:before,.fullscreen-overlay-enabled.dark-theme .tooltipped.tooltipped-nw:before{border-top-color:rgba(255,255,255,0.8)}.fullscreen-overlay-enabled.dark-theme .tooltipped.tooltipped-e:before{border-right-color:rgba(255,255,255,0.8)}.fullscreen-overlay-enabled.dark-theme .tooltipped.tooltipped-w:before{border-left-color:rgba(255,255,255,0.8)}.flex-table{display:table}.flex-table-item{display:table-cell;width:1%;white-space:nowrap;vertical-align:middle}.flex-table-item-primary{width:99%}.css-truncate.css-truncate-target,.css-truncate .css-truncate-target{display:inline-block;max-width:125px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:top}.css-truncate.expandable.zeroclipboard-is-hover .css-truncate-target,.css-truncate.expandable.zeroclipboard-is-hover.css-truncate-target,.css-truncate.expandable:hover .css-truncate-target,.css-truncate.expandable:hover.css-truncate-target{max-width:10000px !important} -------------------------------------------------------------------------------- /assets/fonts/octicons-local.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/calavera/gh-rel/4607c25cd62b8ccc55f4d8703ac99b09c9ae016f/assets/fonts/octicons-local.ttf -------------------------------------------------------------------------------- /assets/fonts/octicons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/calavera/gh-rel/4607c25cd62b8ccc55f4d8703ac99b09c9ae016f/assets/fonts/octicons.eot -------------------------------------------------------------------------------- /assets/fonts/octicons.less: -------------------------------------------------------------------------------- 1 | @octicons-font-path: "."; 2 | @octicons-version: "675c3211eac589bbda193fdb306ce567a2c4569f"; 3 | 4 | @font-face { 5 | font-family: 'octicons'; 6 | src: ~"url('@{octicons-font-path}/octicons.eot?#iefix&v=@{octicons-version}') format('embedded-opentype')", 7 | ~"url('@{octicons-font-path}/octicons.woff?v=@{octicons-version}') format('woff')", 8 | ~"url('@{octicons-font-path}/octicons.ttf?v=@{octicons-version}') format('truetype')", 9 | ~"url('@{octicons-font-path}/octicons.svg?v=@{octicons-version}#octicons') format('svg')"; 10 | font-weight: normal; 11 | font-style: normal; 12 | } 13 | 14 | // .octicon is optimized for 16px. 15 | // .mega-octicon is optimized for 32px but can be used larger. 16 | .octicon, .mega-octicon { 17 | font: normal normal normal 16px/1 octicons; 18 | display: inline-block; 19 | text-decoration: none; 20 | text-rendering: auto; 21 | -webkit-font-smoothing: antialiased; 22 | -moz-osx-font-smoothing: grayscale; 23 | -webkit-user-select: none; 24 | -moz-user-select: none; 25 | -ms-user-select: none; 26 | user-select: none; 27 | } 28 | .mega-octicon { font-size: 32px; } 29 | 30 | .octicon-alert:before { content: '\f02d'} /* */ 31 | .octicon-alignment-align:before { content: '\f08a'} /* */ 32 | .octicon-alignment-aligned-to:before { content: '\f08e'} /* */ 33 | .octicon-alignment-unalign:before { content: '\f08b'} /* */ 34 | .octicon-arrow-down:before { content: '\f03f'} /* */ 35 | .octicon-arrow-left:before { content: '\f040'} /* */ 36 | .octicon-arrow-right:before { content: '\f03e'} /* */ 37 | .octicon-arrow-small-down:before { content: '\f0a0'} /* */ 38 | .octicon-arrow-small-left:before { content: '\f0a1'} /* */ 39 | .octicon-arrow-small-right:before { content: '\f071'} /* */ 40 | .octicon-arrow-small-up:before { content: '\f09f'} /* */ 41 | .octicon-arrow-up:before { content: '\f03d'} /* */ 42 | .octicon-beer:before { content: '\f069'} /* */ 43 | .octicon-book:before { content: '\f007'} /* */ 44 | .octicon-bookmark:before { content: '\f07b'} /* */ 45 | .octicon-briefcase:before { content: '\f0d3'} /* */ 46 | .octicon-broadcast:before { content: '\f048'} /* */ 47 | .octicon-browser:before { content: '\f0c5'} /* */ 48 | .octicon-bug:before { content: '\f091'} /* */ 49 | .octicon-calendar:before { content: '\f068'} /* */ 50 | .octicon-check:before { content: '\f03a'} /* */ 51 | .octicon-checklist:before { content: '\f076'} /* */ 52 | .octicon-chevron-down:before { content: '\f0a3'} /* */ 53 | .octicon-chevron-left:before { content: '\f0a4'} /* */ 54 | .octicon-chevron-right:before { content: '\f078'} /* */ 55 | .octicon-chevron-up:before { content: '\f0a2'} /* */ 56 | .octicon-circle-slash:before { content: '\f084'} /* */ 57 | .octicon-circuit-board:before { content: '\f0d6'} /* */ 58 | .octicon-clippy:before { content: '\f035'} /* */ 59 | .octicon-clock:before { content: '\f046'} /* */ 60 | .octicon-cloud-download:before { content: '\f00b'} /* */ 61 | .octicon-cloud-upload:before { content: '\f00c'} /* */ 62 | .octicon-code:before { content: '\f05f'} /* */ 63 | .octicon-color-mode:before { content: '\f065'} /* */ 64 | .octicon-comment-add:before, 65 | .octicon-comment:before { content: '\f02b'} /* */ 66 | .octicon-comment-discussion:before { content: '\f04f'} /* */ 67 | .octicon-credit-card:before { content: '\f045'} /* */ 68 | .octicon-dash:before { content: '\f0ca'} /* */ 69 | .octicon-dashboard:before { content: '\f07d'} /* */ 70 | .octicon-database:before { content: '\f096'} /* */ 71 | .octicon-device-camera:before { content: '\f056'} /* */ 72 | .octicon-device-camera-video:before { content: '\f057'} /* */ 73 | .octicon-device-desktop:before { content: '\f27c'} /* */ 74 | .octicon-device-mobile:before { content: '\f038'} /* */ 75 | .octicon-diff:before { content: '\f04d'} /* */ 76 | .octicon-diff-added:before { content: '\f06b'} /* */ 77 | .octicon-diff-ignored:before { content: '\f099'} /* */ 78 | .octicon-diff-modified:before { content: '\f06d'} /* */ 79 | .octicon-diff-removed:before { content: '\f06c'} /* */ 80 | .octicon-diff-renamed:before { content: '\f06e'} /* */ 81 | .octicon-ellipsis:before { content: '\f09a'} /* */ 82 | .octicon-eye-unwatch:before, 83 | .octicon-eye-watch:before, 84 | .octicon-eye:before { content: '\f04e'} /* */ 85 | .octicon-file-binary:before { content: '\f094'} /* */ 86 | .octicon-file-code:before { content: '\f010'} /* */ 87 | .octicon-file-directory:before { content: '\f016'} /* */ 88 | .octicon-file-media:before { content: '\f012'} /* */ 89 | .octicon-file-pdf:before { content: '\f014'} /* */ 90 | .octicon-file-submodule:before { content: '\f017'} /* */ 91 | .octicon-file-symlink-directory:before { content: '\f0b1'} /* */ 92 | .octicon-file-symlink-file:before { content: '\f0b0'} /* */ 93 | .octicon-file-text:before { content: '\f011'} /* */ 94 | .octicon-file-zip:before { content: '\f013'} /* */ 95 | .octicon-flame:before { content: '\f0d2'} /* */ 96 | .octicon-fold:before { content: '\f0cc'} /* */ 97 | .octicon-gear:before { content: '\f02f'} /* */ 98 | .octicon-gift:before { content: '\f042'} /* */ 99 | .octicon-gist:before { content: '\f00e'} /* */ 100 | .octicon-gist-secret:before { content: '\f08c'} /* */ 101 | .octicon-git-branch-create:before, 102 | .octicon-git-branch-delete:before, 103 | .octicon-git-branch:before { content: '\f020'} /* */ 104 | .octicon-git-commit:before { content: '\f01f'} /* */ 105 | .octicon-git-compare:before { content: '\f0ac'} /* */ 106 | .octicon-git-merge:before { content: '\f023'} /* */ 107 | .octicon-git-pull-request-abandoned:before, 108 | .octicon-git-pull-request:before { content: '\f009'} /* */ 109 | .octicon-globe:before { content: '\f0b6'} /* */ 110 | .octicon-graph:before { content: '\f043'} /* */ 111 | .octicon-heart:before { content: '\2665'} /* ♥ */ 112 | .octicon-history:before { content: '\f07e'} /* */ 113 | .octicon-home:before { content: '\f08d'} /* */ 114 | .octicon-horizontal-rule:before { content: '\f070'} /* */ 115 | .octicon-hourglass:before { content: '\f09e'} /* */ 116 | .octicon-hubot:before { content: '\f09d'} /* */ 117 | .octicon-inbox:before { content: '\f0cf'} /* */ 118 | .octicon-info:before { content: '\f059'} /* */ 119 | .octicon-issue-closed:before { content: '\f028'} /* */ 120 | .octicon-issue-opened:before { content: '\f026'} /* */ 121 | .octicon-issue-reopened:before { content: '\f027'} /* */ 122 | .octicon-jersey:before { content: '\f019'} /* */ 123 | .octicon-jump-down:before { content: '\f072'} /* */ 124 | .octicon-jump-left:before { content: '\f0a5'} /* */ 125 | .octicon-jump-right:before { content: '\f0a6'} /* */ 126 | .octicon-jump-up:before { content: '\f073'} /* */ 127 | .octicon-key:before { content: '\f049'} /* */ 128 | .octicon-keyboard:before { content: '\f00d'} /* */ 129 | .octicon-law:before { content: '\f0d8'} /* */ 130 | .octicon-light-bulb:before { content: '\f000'} /* */ 131 | .octicon-link:before { content: '\f05c'} /* */ 132 | .octicon-link-external:before { content: '\f07f'} /* */ 133 | .octicon-list-ordered:before { content: '\f062'} /* */ 134 | .octicon-list-unordered:before { content: '\f061'} /* */ 135 | .octicon-location:before { content: '\f060'} /* */ 136 | .octicon-gist-private:before, 137 | .octicon-mirror-private:before, 138 | .octicon-git-fork-private:before, 139 | .octicon-lock:before { content: '\f06a'} /* */ 140 | .octicon-logo-github:before { content: '\f092'} /* */ 141 | .octicon-mail:before { content: '\f03b'} /* */ 142 | .octicon-mail-read:before { content: '\f03c'} /* */ 143 | .octicon-mail-reply:before { content: '\f051'} /* */ 144 | .octicon-mark-github:before { content: '\f00a'} /* */ 145 | .octicon-markdown:before { content: '\f0c9'} /* */ 146 | .octicon-megaphone:before { content: '\f077'} /* */ 147 | .octicon-mention:before { content: '\f0be'} /* */ 148 | .octicon-microscope:before { content: '\f089'} /* */ 149 | .octicon-milestone:before { content: '\f075'} /* */ 150 | .octicon-mirror-public:before, 151 | .octicon-mirror:before { content: '\f024'} /* */ 152 | .octicon-mortar-board:before { content: '\f0d7'} /* */ 153 | .octicon-move-down:before { content: '\f0a8'} /* */ 154 | .octicon-move-left:before { content: '\f074'} /* */ 155 | .octicon-move-right:before { content: '\f0a9'} /* */ 156 | .octicon-move-up:before { content: '\f0a7'} /* */ 157 | .octicon-mute:before { content: '\f080'} /* */ 158 | .octicon-no-newline:before { content: '\f09c'} /* */ 159 | .octicon-octoface:before { content: '\f008'} /* */ 160 | .octicon-organization:before { content: '\f037'} /* */ 161 | .octicon-package:before { content: '\f0c4'} /* */ 162 | .octicon-paintcan:before { content: '\f0d1'} /* */ 163 | .octicon-pencil:before { content: '\f058'} /* */ 164 | .octicon-person-add:before, 165 | .octicon-person-follow:before, 166 | .octicon-person:before { content: '\f018'} /* */ 167 | .octicon-pin:before { content: '\f041'} /* */ 168 | .octicon-playback-fast-forward:before { content: '\f0bd'} /* */ 169 | .octicon-playback-pause:before { content: '\f0bb'} /* */ 170 | .octicon-playback-play:before { content: '\f0bf'} /* */ 171 | .octicon-playback-rewind:before { content: '\f0bc'} /* */ 172 | .octicon-plug:before { content: '\f0d4'} /* */ 173 | .octicon-repo-create:before, 174 | .octicon-gist-new:before, 175 | .octicon-file-directory-create:before, 176 | .octicon-file-add:before, 177 | .octicon-plus:before { content: '\f05d'} /* */ 178 | .octicon-podium:before { content: '\f0af'} /* */ 179 | .octicon-primitive-dot:before { content: '\f052'} /* */ 180 | .octicon-primitive-square:before { content: '\f053'} /* */ 181 | .octicon-pulse:before { content: '\f085'} /* */ 182 | .octicon-puzzle:before { content: '\f0c0'} /* */ 183 | .octicon-question:before { content: '\f02c'} /* */ 184 | .octicon-quote:before { content: '\f063'} /* */ 185 | .octicon-radio-tower:before { content: '\f030'} /* */ 186 | .octicon-repo-delete:before, 187 | .octicon-repo:before { content: '\f001'} /* */ 188 | .octicon-repo-clone:before { content: '\f04c'} /* */ 189 | .octicon-repo-force-push:before { content: '\f04a'} /* */ 190 | .octicon-gist-fork:before, 191 | .octicon-repo-forked:before { content: '\f002'} /* */ 192 | .octicon-repo-pull:before { content: '\f006'} /* */ 193 | .octicon-repo-push:before { content: '\f005'} /* */ 194 | .octicon-rocket:before { content: '\f033'} /* */ 195 | .octicon-rss:before { content: '\f034'} /* */ 196 | .octicon-ruby:before { content: '\f047'} /* */ 197 | .octicon-screen-full:before { content: '\f066'} /* */ 198 | .octicon-screen-normal:before { content: '\f067'} /* */ 199 | .octicon-search-save:before, 200 | .octicon-search:before { content: '\f02e'} /* */ 201 | .octicon-server:before { content: '\f097'} /* */ 202 | .octicon-settings:before { content: '\f07c'} /* */ 203 | .octicon-log-in:before, 204 | .octicon-sign-in:before { content: '\f036'} /* */ 205 | .octicon-log-out:before, 206 | .octicon-sign-out:before { content: '\f032'} /* */ 207 | .octicon-split:before { content: '\f0c6'} /* */ 208 | .octicon-squirrel:before { content: '\f0b2'} /* */ 209 | .octicon-star-add:before, 210 | .octicon-star-delete:before, 211 | .octicon-star:before { content: '\f02a'} /* */ 212 | .octicon-steps:before { content: '\f0c7'} /* */ 213 | .octicon-stop:before { content: '\f08f'} /* */ 214 | .octicon-repo-sync:before, 215 | .octicon-sync:before { content: '\f087'} /* */ 216 | .octicon-tag-remove:before, 217 | .octicon-tag-add:before, 218 | .octicon-tag:before { content: '\f015'} /* */ 219 | .octicon-telescope:before { content: '\f088'} /* */ 220 | .octicon-terminal:before { content: '\f0c8'} /* */ 221 | .octicon-three-bars:before { content: '\f05e'} /* */ 222 | .octicon-thumbsdown:before { content: '\f0db'} /* */ 223 | .octicon-thumbsup:before { content: '\f0da'} /* */ 224 | .octicon-tools:before { content: '\f031'} /* */ 225 | .octicon-trashcan:before { content: '\f0d0'} /* */ 226 | .octicon-triangle-down:before { content: '\f05b'} /* */ 227 | .octicon-triangle-left:before { content: '\f044'} /* */ 228 | .octicon-triangle-right:before { content: '\f05a'} /* */ 229 | .octicon-triangle-up:before { content: '\f0aa'} /* */ 230 | .octicon-unfold:before { content: '\f039'} /* */ 231 | .octicon-unmute:before { content: '\f0ba'} /* */ 232 | .octicon-versions:before { content: '\f064'} /* */ 233 | .octicon-remove-close:before, 234 | .octicon-x:before { content: '\f081'} /* */ 235 | .octicon-zap:before { content: '\26A1'} /* ⚡ */ 236 | -------------------------------------------------------------------------------- /assets/fonts/octicons.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 201 | -------------------------------------------------------------------------------- /assets/fonts/octicons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/calavera/gh-rel/4607c25cd62b8ccc55f4d8703ac99b09c9ae016f/assets/fonts/octicons.ttf -------------------------------------------------------------------------------- /assets/fonts/octicons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/calavera/gh-rel/4607c25cd62b8ccc55f4d8703ac99b09c9ae016f/assets/fonts/octicons.woff -------------------------------------------------------------------------------- /assets/images/mini-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/calavera/gh-rel/github" 4 | 5 | func addProject(nwo string) error { 6 | return github.AddProject(nwo) 7 | } 8 | -------------------------------------------------------------------------------- /db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "path" 8 | 9 | "github.com/boltdb/bolt" 10 | ) 11 | 12 | var ( 13 | internal *bolt.DB 14 | projectsBucket = []byte("projects") 15 | etagsBucket = []byte("etags") 16 | releasesBucket = []byte("releases") 17 | ) 18 | 19 | func Open(dbPath string) (err error) { 20 | if err := os.MkdirAll(path.Dir(dbPath), 0755); err != nil { 21 | return err 22 | } 23 | internal, err = bolt.Open(dbPath, 0600, nil) 24 | if err != nil { 25 | return err 26 | } 27 | return createBuckets() 28 | } 29 | 30 | func Close() error { 31 | if internal != nil { 32 | return internal.Close() 33 | } 34 | return nil 35 | } 36 | 37 | func createBuckets() error { 38 | return internal.Update(func(tx *bolt.Tx) error { 39 | if _, err := tx.CreateBucketIfNotExists(projectsBucket); err != nil { 40 | return err 41 | } 42 | if _, err := tx.CreateBucketIfNotExists(etagsBucket); err != nil { 43 | return err 44 | } 45 | if _, err := tx.CreateBucketIfNotExists(releasesBucket); err != nil { 46 | return err 47 | } 48 | return nil 49 | }) 50 | } 51 | 52 | func AddProject(name, htmlURL string) error { 53 | return internal.Update(func(tx *bolt.Tx) error { 54 | b := tx.Bucket(projectsBucket) 55 | 56 | n := []byte(name) 57 | if b.Get(n) != nil { 58 | return fmt.Errorf("the project already exists: %s", name) 59 | } 60 | b.Put(n, []byte(htmlURL)) 61 | return nil 62 | }) 63 | } 64 | 65 | func ListProjects() (projects []Project, err error) { 66 | err = internal.View(func(tx *bolt.Tx) error { 67 | c := tx.Bucket(projectsBucket).Cursor() 68 | etags := tx.Bucket(etagsBucket) 69 | releases := tx.Bucket(releasesBucket) 70 | 71 | for k, v := c.First(); k != nil; k, v = c.Next() { 72 | name := string(k) 73 | latestEtag, releasesEtag := getEtags(etags, name) 74 | latestRel, nextRcRel := getReleases(releases, name) 75 | 76 | p := Project{ 77 | Name: name, 78 | HTMLURL: string(v), 79 | LatestReleaseEtag: latestEtag, 80 | ReleasesEtag: releasesEtag, 81 | LatestRelease: latestRel, 82 | NextPreRelease: nextRcRel, 83 | } 84 | 85 | projects = append(projects, p) 86 | } 87 | 88 | return nil 89 | }) 90 | return 91 | } 92 | 93 | func SaveLatest(project, tag, url, etag string) error { 94 | return saveProjectRelease(project, "latest", tag, url, etag, false) 95 | } 96 | 97 | func SaveNextRcRelease(project, tag, url, etag string) error { 98 | return saveProjectRelease(project, "rc", tag, url, etag, true) 99 | } 100 | 101 | func saveProjectRelease(project, from, tag, url, etag string, prerelease bool) error { 102 | return internal.Update(func(tx *bolt.Tx) error { 103 | if err := saveEtag(tx, project, from, etag); err != nil { 104 | return err 105 | } 106 | if err := saveRelease(tx, project, from, tag, url, prerelease); err != nil { 107 | return err 108 | } 109 | return nil 110 | }) 111 | } 112 | 113 | func getEtags(etags *bolt.Bucket, name string) (string, string) { 114 | latestEtagKey := fmt.Sprintf("%s-latest", name) 115 | releasesEtagKey := fmt.Sprintf("%s-releases-list", name) 116 | 117 | var latestEtag string 118 | if e := etags.Get([]byte(latestEtagKey)); e != nil { 119 | latestEtag = string(e) 120 | } 121 | var releasesEtag string 122 | if e := etags.Get([]byte(releasesEtagKey)); e != nil { 123 | releasesEtag = string(e) 124 | } 125 | return latestEtag, releasesEtag 126 | } 127 | 128 | func getReleases(releases *bolt.Bucket, name string) (Release, Release) { 129 | latestKey := fmt.Sprintf("%s-latest", name) 130 | rcKey := fmt.Sprintf("%s-rc", name) 131 | 132 | latest := wipRelease() 133 | rc := wipRelease() 134 | 135 | if t := releases.Get([]byte(latestKey)); t != nil { 136 | json.Unmarshal(t, &latest) 137 | } 138 | 139 | if t := releases.Get([]byte(rcKey)); t != nil { 140 | json.Unmarshal(t, &rc) 141 | } 142 | 143 | return latest, rc 144 | } 145 | 146 | func saveRelease(tx *bolt.Tx, name, from, tag, url string, prerelease bool) error { 147 | b := tx.Bucket(releasesBucket) 148 | key := fmt.Sprintf("%s-%s", name, from) 149 | 150 | release := Release{tag, url, prerelease} 151 | bytes, err := json.Marshal(release) 152 | if err != nil { 153 | return err 154 | } 155 | 156 | return b.Put([]byte(key), bytes) 157 | } 158 | 159 | func saveEtag(tx *bolt.Tx, name, from, value string) error { 160 | b := tx.Bucket(etagsBucket) 161 | 162 | k := fmt.Sprintf("%s-%s", name, from) 163 | return b.Put([]byte(k), []byte(value)) 164 | } 165 | -------------------------------------------------------------------------------- /db/db_darwin.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | const DefaultPath = "bolt.db" 4 | -------------------------------------------------------------------------------- /db/db_linux.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | const DefaultPath = "/usr/lib/gh-rel/bolt.db" 4 | -------------------------------------------------------------------------------- /db/types.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import "strings" 4 | 5 | const WIP = "WIP" 6 | 7 | type Project struct { 8 | Name string 9 | HTMLURL string 10 | LatestReleaseEtag string 11 | ReleasesEtag string 12 | LatestRelease Release 13 | NextPreRelease Release 14 | } 15 | 16 | func (p Project) Owner() string { 17 | return strings.Split(p.Name, "/")[0] 18 | } 19 | 20 | func (p Project) Repo() string { 21 | return strings.Split(p.Name, "/")[1] 22 | } 23 | 24 | type Release struct { 25 | Tag string 26 | HTMLURL string 27 | Prerelease bool 28 | } 29 | 30 | func wipRelease() Release { 31 | return Release{Tag: WIP} 32 | } 33 | -------------------------------------------------------------------------------- /github/github.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "net/url" 5 | "strings" 6 | 7 | "github.com/calavera/gh-rel/db" 8 | "github.com/octokit/go-octokit/octokit" 9 | ) 10 | 11 | const ( 12 | etagKey = "ETag" 13 | noneMatchKey = "If-None-Match" 14 | defaultOrg = "docker" 15 | ) 16 | 17 | var internal *octokit.Client 18 | 19 | func InitClient(token string) { 20 | var auth octokit.AuthMethod 21 | if token != "" { 22 | auth = octokit.TokenAuth{token} 23 | } 24 | internal = octokit.NewClientWith("https://api.github.com", "gh-rel-dashboard", auth, nil) 25 | } 26 | 27 | func Project(owner, name string) (*octokit.Repository, *octokit.Result) { 28 | return internal.Repositories().One(nil, repoParams(owner, name)) 29 | } 30 | 31 | func LatestRelease(owner, name, etag string) (release *octokit.Release, result *octokit.Result, respEtag string) { 32 | result, respEtag = releaseRequest(owner, name, etag, octokit.ReleasesLatestURL, &release) 33 | return 34 | } 35 | 36 | func NextRcRelease(owner, name, etag string) (*octokit.Release, *octokit.Result, string) { 37 | var releases []octokit.Release 38 | result, respEtag := releaseRequest(owner, name, etag, octokit.ReleasesURL, &releases) 39 | if result.HasError() { 40 | return nil, result, "" 41 | } 42 | 43 | if len(releases) == 0 { 44 | return nil, result, "" 45 | } 46 | 47 | for _, r := range releases { 48 | if !r.Draft && !r.Prerelease { 49 | break 50 | } 51 | if r.Prerelease { 52 | return &r, result, respEtag 53 | } 54 | } 55 | 56 | return nil, result, "" 57 | } 58 | 59 | func AddProject(nwo string) error { 60 | nameWithOwner := strings.Split(nwo, "/") 61 | if len(nameWithOwner) == 1 { 62 | nameWithOwner = []string{defaultOrg, nameWithOwner[0]} 63 | } 64 | 65 | p, r := Project(nameWithOwner[0], nameWithOwner[1]) 66 | if r.HasError() { 67 | return r.Err 68 | } 69 | 70 | return db.AddProject(p.FullName, p.HTMLURL) 71 | } 72 | 73 | func releaseRequest(owner, name, etag string, uri octokit.Hyperlink, output interface{}) (result *octokit.Result, respEtag string) { 74 | params := repoParams(owner, name) 75 | url, err := uri.Expand(params) 76 | if err != nil { 77 | return &octokit.Result{Err: err}, "" 78 | } 79 | 80 | result, respEtag = sendRequest(url, func(req *octokit.Request) (*octokit.Response, error) { 81 | req.Header.Set(noneMatchKey, etag) 82 | return req.Get(output) 83 | }) 84 | 85 | return 86 | } 87 | 88 | func repoParams(owner, name string) octokit.M { 89 | return octokit.M{"owner": owner, "repo": name} 90 | } 91 | 92 | func sendRequest(url *url.URL, fn func(r *octokit.Request) (*octokit.Response, error)) (result *octokit.Result, respEtag string) { 93 | req, err := internal.NewRequest(url.String()) 94 | if err != nil { 95 | result = &octokit.Result{Response: nil, Err: err} 96 | return 97 | } 98 | 99 | resp, err := fn(req) 100 | result = &octokit.Result{Response: resp, Err: err} 101 | if resp != nil { 102 | respEtag = resp.Header.Get(etagKey) 103 | } 104 | 105 | return 106 | } 107 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/calavera/gh-rel/db" 8 | "github.com/calavera/gh-rel/github" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func fullMain() { 13 | var port uint 14 | var dbPath string 15 | var githubAuthToken string 16 | var adminPassword string 17 | 18 | cmdAdd := &cobra.Command{ 19 | Use: "add [owner/name]", 20 | Short: "Add a new project to the dashboard", 21 | Run: func(cmd *cobra.Command, args []string) { 22 | if len(args) != 1 { 23 | fmt.Printf("Invalid arguments: %v\n", args) 24 | cmd.Usage() 25 | return 26 | } 27 | 28 | setup(dbPath, githubAuthToken) 29 | defer teardown() 30 | 31 | if err := addProject(args[0]); err != nil { 32 | fmt.Println(err) 33 | os.Exit(1) 34 | } 35 | }, 36 | } 37 | 38 | cmdServe := &cobra.Command{ 39 | Use: "serve", 40 | Short: "Start the web server", 41 | Run: func(cmd *cobra.Command, args []string) { 42 | setup(dbPath, githubAuthToken) 43 | defer teardown() 44 | startServer(port, adminPassword) 45 | }, 46 | } 47 | cmdServe.Flags().UintVarP(&port, "port", "p", 8888, "port to serve the web application") 48 | cmdServe.Flags().StringVarP(&githubAuthToken, "auth", "a", "", "GitHub auth token") 49 | cmdServe.Flags().StringVarP(&adminPassword, "passwd", "", "passw0rd", "Admin password to add projects") 50 | 51 | rootCmd := &cobra.Command{Use: "gh-rel"} 52 | rootCmd.AddCommand(cmdAdd, cmdServe) 53 | rootCmd.PersistentFlags().StringVarP(&dbPath, "db", "d", db.DefaultPath, "path to the database file") 54 | 55 | rootCmd.Execute() 56 | } 57 | 58 | func setup(dbPath, githubAuthToken string) { 59 | if err := db.Open(dbPath); err != nil { 60 | fmt.Println(err) 61 | os.Exit(1) 62 | } 63 | github.InitClient(githubAuthToken) 64 | } 65 | 66 | func teardown() { 67 | db.Close() 68 | } 69 | 70 | func main() { 71 | fullMain() 72 | } 73 | -------------------------------------------------------------------------------- /render/render.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "bytes" 5 | "html/template" 6 | "io" 7 | "net/http" 8 | 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | const ( 13 | ContentType = "Content-Type" 14 | ContentHTML = "text/html; charset=UTF-8" 15 | ) 16 | 17 | var templates *template.Template 18 | 19 | type Renderer struct { 20 | http.ResponseWriter 21 | req *http.Request 22 | } 23 | 24 | func init() { 25 | loadTemplates() 26 | } 27 | 28 | func loadTemplates() { 29 | templates = template.New("templates") 30 | funcs := template.FuncMap{ 31 | "yield": func() (template.HTML, error) { 32 | return "", nil 33 | }, 34 | } 35 | template.Must(templates.Funcs(funcs).ParseGlob("templates/*")) 36 | } 37 | 38 | func New(context *gin.Context) *Renderer { 39 | if gin.IsDebugging() { 40 | loadTemplates() 41 | } 42 | 43 | return &Renderer{ 44 | ResponseWriter: context.Writer, 45 | req: context.Request, 46 | } 47 | } 48 | 49 | func (r *Renderer) HTML(status int, name string, binding interface{}) { 50 | addYield(name, binding) 51 | 52 | out, err := execute("layout.tmpl", binding) 53 | if err != nil { 54 | http.Error(r, err.Error(), http.StatusInternalServerError) 55 | return 56 | } 57 | 58 | // template rendered fine, write out the result 59 | r.Header().Set(ContentType, ContentHTML) 60 | r.WriteHeader(status) 61 | io.Copy(r, out) 62 | } 63 | 64 | func execute(name string, binding interface{}) (*bytes.Buffer, error) { 65 | buf := new(bytes.Buffer) 66 | return buf, templates.ExecuteTemplate(buf, name, binding) 67 | } 68 | 69 | func addYield(name string, binding interface{}) { 70 | funcs := template.FuncMap{ 71 | "yield": func() (template.HTML, error) { 72 | buf, err := execute(name, binding) 73 | return template.HTML(buf.String()), err 74 | }, 75 | } 76 | templates.Funcs(funcs) 77 | } 78 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "strings" 8 | 9 | "github.com/calavera/gh-rel/db" 10 | "github.com/calavera/gh-rel/github" 11 | "github.com/calavera/gh-rel/render" 12 | "github.com/gin-gonic/gin" 13 | ) 14 | 15 | type release struct { 16 | Tag string 17 | URL string 18 | prerelease bool 19 | } 20 | 21 | func (r release) WIP() bool { 22 | return r.Tag == db.WIP 23 | } 24 | 25 | func (r release) Label() string { 26 | if r.WIP() { 27 | return "wip" 28 | } 29 | if r.prerelease || strings.Contains(strings.ToLower(r.Tag), "rc") { 30 | return "prerelease" 31 | } 32 | return "latest" 33 | } 34 | 35 | type project struct { 36 | Owner string 37 | Repo string 38 | LatestRelease release 39 | NextPreRelease release 40 | } 41 | 42 | type index struct { 43 | Projects []project 44 | } 45 | 46 | type add struct { 47 | Error error 48 | } 49 | 50 | func startServer(port uint, adminPassword string) { 51 | router := gin.Default() 52 | router.Static("/assets", "./assets") 53 | 54 | router.GET("/", func(c *gin.Context) { 55 | showProjects(c) 56 | }) 57 | 58 | authorized := router.Group("/add", gin.BasicAuth(gin.Accounts{ 59 | "admin": adminPassword, 60 | })) 61 | 62 | authorized.GET("", func(c *gin.Context) { 63 | render.New(c).HTML(http.StatusOK, "add.tmpl", nil) 64 | }) 65 | 66 | authorized.POST("", func(c *gin.Context) { 67 | addNewProject(c) 68 | }) 69 | 70 | router.Run(fmt.Sprintf(":%v", port)) 71 | } 72 | 73 | func showProjects(c *gin.Context) { 74 | projects, err := db.ListProjects() 75 | if err != nil { 76 | c.HTML(http.StatusInternalServerError, "error.tmpl", nil) 77 | return 78 | } 79 | 80 | var prs []project 81 | for _, p := range projects { 82 | lr := getLatestRelease(p) 83 | rc := getRcRelease(p) 84 | 85 | prs = append(prs, project{p.Owner(), p.Repo(), lr, rc}) 86 | } 87 | 88 | render.New(c).HTML(http.StatusOK, "index.tmpl", index{prs}) 89 | } 90 | 91 | func addNewProject(c *gin.Context) { 92 | nwo := strings.TrimSpace(c.PostForm("repo")) 93 | if nwo == "" { 94 | render.New(c).HTML(http.StatusOK, "add.tmpl", add{fmt.Errorf("the repository name cannot be empty")}) 95 | return 96 | } 97 | 98 | if err := github.AddProject(nwo); err != nil { 99 | render.New(c).HTML(http.StatusOK, "add.tmpl", add{err}) 100 | return 101 | } 102 | 103 | showProjects(c) 104 | } 105 | 106 | func getLatestRelease(p db.Project) release { 107 | rel, res, etag := github.LatestRelease(p.Owner(), p.Repo(), p.LatestReleaseEtag) 108 | if res.HasError() { 109 | log.Println(res.Err) 110 | return release{p.LatestRelease.Tag, p.LatestRelease.HTMLURL, false} 111 | } 112 | 113 | if res.Response.StatusCode == http.StatusOK { 114 | db.SaveLatest(p.Name, rel.TagName, rel.HTMLURL, etag) 115 | return release{rel.TagName, rel.HTMLURL, false} 116 | } 117 | return release{p.LatestRelease.Tag, p.LatestRelease.HTMLURL, false} 118 | } 119 | 120 | func getRcRelease(p db.Project) release { 121 | rel, res, etag := github.NextRcRelease(p.Owner(), p.Repo(), p.ReleasesEtag) 122 | if res.HasError() { 123 | log.Println(res.Err) 124 | return release{p.LatestRelease.Tag, p.LatestRelease.HTMLURL, true} 125 | } 126 | 127 | if rel == nil { 128 | return release{Tag: db.WIP} 129 | } 130 | 131 | if res.Response.StatusCode == http.StatusOK { 132 | db.SaveNextRcRelease(p.Name, rel.TagName, rel.HTMLURL, etag) 133 | return release{rel.TagName, rel.HTMLURL, true} 134 | } 135 | return release{p.NextPreRelease.Tag, p.NextPreRelease.HTMLURL, true} 136 | } 137 | -------------------------------------------------------------------------------- /templates/add.tmpl: -------------------------------------------------------------------------------- 1 |